Better Filename Handling for Abilities, Dash Directional Improvements, Fixed Duplicate Ability Name Handling

This commit is contained in:
Kam committed 2026-02-22 16:33:01 -05:00
1 parent 71dbf77def
commit 3de8cd49b6
23 files changed
+752 -178

No files matched your search

@@ -11,6 +11,7 @@ import kamkeel.npcs.controllers.data.ability.AbilityVariant;
import kamkeel.npcs.controllers.data.ability.data.IAbilityAction;
import kamkeel.npcs.controllers.data.ability.data.effect.IEffectAction;
import kamkeel.npcs.controllers.data.ability.data.ChainedAbility;
import kamkeel.npcs.controllers.data.ability.data.entry.ChainedAbilityEntry;
import kamkeel.npcs.controllers.data.ability.enums.AbilityPhase;
import kamkeel.npcs.controllers.data.ability.enums.UserType;
import kamkeel.npcs.controllers.data.ability.extender.IAbilityExtender;
@@ -45,6 +46,7 @@ import noppes.npcs.api.handler.IAbilityHandler;
import noppes.npcs.controllers.PlayerDataController;
import noppes.npcs.controllers.data.PlayerData;
import noppes.npcs.util.NBTJsonUtil;
import kamkeel.npcs.util.FileNameHelper;
import java.io.File;
import java.util.*;
@@ -213,6 +215,7 @@ public class AbilityController implements IAbilityHandler {
customAbilities.clear();
customAbilitiesById.clear();
int migrated = 0;
Map<String, String> migratedAbilityNames = new HashMap<String, String>();
File dir = getDir();
File[] files = dir.exists() ? dir.listFiles() : null;
@@ -221,7 +224,7 @@ public class AbilityController implements IAbilityHandler {
if (!file.isFile() || !file.getName().endsWith(".json")) continue;
try {
String filename = file.getName();
String key = filename.substring(0, filename.length() - 5);
String fileKey = filename.substring(0, filename.length() - 5);
NBTTagCompound nbt = NBTJsonUtil.LoadFile(file);
Ability ability = fromNBT(nbt);
@@ -234,48 +237,55 @@ public class AbilityController implements IAbilityHandler {
continue;
}
// 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
}
String storedName = ability.getName();
String preferredName;
if (UUID_PATTERN.matcher(fileKey).matches()) {
preferredName = (storedName != null && !storedName.isEmpty()) ? storedName : fileKey;
} else {
preferredName = fileKey;
}
String name = FileNameHelper.sanitizeName(preferredName, "Ability");
name = makeUniqueNameForLoad(customAbilities, dir, file, name, fileKey);
// 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");
boolean dirty = false;
if (!name.equals(fileKey)) {
File renamedFile = new File(dir, name + ".json");
if (file.renameTo(renamedFile)) {
LogWriter.info("Migrated ability file: " + fileKey + ".json -> " + name + ".json");
file = renamedFile;
migrated++;
dirty = true;
} else {
LogWriter.error("Failed to rename ability file: " + key + ".json -> " + name + ".json");
LogWriter.error("Failed to rename ability file: " + fileKey + ".json -> " + name + ".json");
}
}
key = name;
migrated++;
ability.setName(name);
if (storedName != null && !storedName.equals(name)) {
dirty = true;
if (!storedName.isEmpty()) {
migratedAbilityNames.put(storedName, name);
}
}
if (!fileKey.equals(name)) {
migratedAbilityNames.put(fileKey, name);
}
// Ensure ability has a UUID; generate one for legacy abilities
String uuid = ability.getId();
if (uuid == null || uuid.isEmpty() || uuid.equals(key)) {
if (uuid == null || uuid.isEmpty() || uuid.equals(fileKey) || uuid.equals(name)) {
// 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++;
dirty = true;
}
ability.setName(key);
customAbilities.put(key, ability);
if (dirty) {
NBTJsonUtil.SaveFile(file, ability.writeNBT());
}
customAbilities.put(name, ability);
customAbilitiesById.put(uuid, ability);
} catch (Exception e) {
LogWriter.error("Error loading custom ability: " + file.getAbsolutePath(), e);
@@ -285,23 +295,68 @@ public class AbilityController implements IAbilityHandler {
customAbilityRevision++;
if (migrated > 0) {
LogWriter.info("Migrated " + migrated + " abilities (UUID-named files or missing UUIDs)");
LogWriter.info("Migrated " + migrated + " abilities (filename normalization / UUID repair)");
}
LogWriter.info("Loaded " + customAbilities.size() + " custom abilities");
loadChainedAbilities();
loadChainedAbilities(migratedAbilityNames);
}
private boolean nameFileExists(File dir, String name) {
return new File(dir, name + ".json").exists();
private boolean nameFileExists(File dir, String name, File ignoredFile) {
File existing = new File(dir, name + ".json");
if (!existing.exists()) return false;
return ignoredFile == null || !existing.getAbsolutePath().equals(ignoredFile.getAbsolutePath());
}
private String makeUniqueNameForLoad(Map<String, ?> loadedMap, File dir, File currentFile, String baseName, String selfKey) {
String candidate = baseName;
while ((loadedMap.containsKey(candidate) && !candidate.equals(selfKey))
|| nameFileExists(dir, candidate, currentFile)) {
candidate = candidate + "_";
}
return candidate;
}
private String makeUniqueNameForMap(Map<String, ?> map, String baseName, String selfName) {
String candidate = baseName;
while (map.containsKey(candidate) && (selfName == null || !candidate.equals(selfName))) {
candidate = candidate + "_";
}
return candidate;
}
private boolean remapChainEntryReferences(ChainedAbility chain, Map<String, String> migratedAbilityNames) {
if (chain == null || migratedAbilityNames == null || migratedAbilityNames.isEmpty()) return false;
boolean changed = false;
for (ChainedAbilityEntry entry : chain.getEntries()) {
if (entry == null || !entry.isReference()) continue;
String ref = entry.getAbilityReference();
if (ref == null || ref.isEmpty()) continue;
String mapped = migratedAbilityNames.get(ref);
if (mapped == null) {
for (Map.Entry<String, String> e : migratedAbilityNames.entrySet()) {
if (e.getKey().equalsIgnoreCase(ref)) {
mapped = e.getValue();
break;
}
}
}
if (mapped != null && !mapped.equals(ref)) {
entry.setAbilityReference(mapped);
changed = true;
}
}
return changed;
}
public boolean saveCustomAbility(Ability ability) {
if (ability == null) return false;
if (ability.isBuiltIn()) return false;
String name = ability.getName();
if (name == null || name.isEmpty()) return false;
String name = FileNameHelper.sanitizeName(ability.getName(), "Ability");
ability.setName(name);
String uuid = ability.getId();
boolean isNew = (uuid == null || uuid.isEmpty());
@@ -312,26 +367,17 @@ public class AbilityController implements IAbilityHandler {
ability.setId(uuid);
// Ensure unique name
while (hasCustomAbilityName(name)) {
name = name + "_";
}
name = makeUniqueNameForMap(customAbilities, name, null);
ability.setName(name);
} 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);
}
name = makeUniqueNameForMap(customAbilities, name, oldName);
ability.setName(name);
if (!oldName.equals(name)) {
// Delete old file and remove old map entry
File oldFile = new File(getDir(), oldName + ".json");
if (oldFile.exists()) {
@@ -339,6 +385,9 @@ public class AbilityController implements IAbilityHandler {
}
customAbilities.remove(oldName);
}
} else {
name = makeUniqueNameForMap(customAbilities, name, null);
ability.setName(name);
}
}
@@ -374,6 +423,15 @@ public class AbilityController implements IAbilityHandler {
if (name == null || name.isEmpty()) return false;
Ability removed = customAbilities.remove(name);
if (removed == null) {
String sanitized = FileNameHelper.sanitizeTextInput(name);
if (!sanitized.equals(name)) {
removed = customAbilities.remove(sanitized);
if (removed != null) {
name = sanitized;
}
}
}
if (removed == null) return false;
// Also remove from UUID index
@@ -412,7 +470,13 @@ public class AbilityController implements IAbilityHandler {
}
public Ability getCustomAbility(String name) {
return customAbilities.get(name);
Ability ability = customAbilities.get(name);
if (ability != null) return ability;
String sanitized = FileNameHelper.sanitizeTextInput(name);
if (!sanitized.equals(name)) {
return customAbilities.get(sanitized);
}
return null;
}
public Set<String> getCustomAbilityNames() {
@@ -452,9 +516,10 @@ public class AbilityController implements IAbilityHandler {
// CHAINED ABILITIES
// ═══════════════════════════════════════════════════════════════════════════
private void loadChainedAbilities() {
private void loadChainedAbilities(Map<String, String> migratedAbilityNames) {
chainedAbilities.clear();
chainedAbilitiesById.clear();
int migrated = 0;
File dir = getChainedDir();
File[] files = dir.exists() ? dir.listFiles() : null;
@@ -463,19 +528,51 @@ public class AbilityController implements IAbilityHandler {
if (!file.isFile() || !file.getName().endsWith(".json")) continue;
try {
String filename = file.getName();
String key = filename.substring(0, filename.length() - 5);
String fileKey = 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 storedName = chain.getName();
String name = FileNameHelper.sanitizeName(fileKey, "Chain");
name = makeUniqueNameForLoad(chainedAbilities, dir, file, name, fileKey);
boolean dirty = false;
if (!name.equals(fileKey)) {
File renamedFile = new File(dir, name + ".json");
if (file.renameTo(renamedFile)) {
LogWriter.info("Migrated chained ability file: " + fileKey + ".json -> " + name + ".json");
file = renamedFile;
dirty = true;
} else {
LogWriter.error("Failed to rename chained ability file: " + fileKey + ".json -> " + name + ".json");
}
}
chain.setName(name);
if (storedName != null && !storedName.equals(name)) {
dirty = true;
}
if (remapChainEntryReferences(chain, migratedAbilityNames)) {
dirty = true;
}
String uuid = chain.getId();
if (uuid != null && !uuid.isEmpty()) {
chainedAbilitiesById.put(uuid, chain);
if (uuid == null || uuid.isEmpty() || uuid.equals(fileKey) || uuid.equals(name)) {
uuid = UUID.randomUUID().toString();
chain.setId(uuid);
dirty = true;
}
if (dirty) {
NBTJsonUtil.SaveFile(file, chain.writeNBT());
migrated++;
}
chainedAbilities.put(name, chain);
chainedAbilitiesById.put(uuid, chain);
} catch (Exception e) {
LogWriter.error("Error loading chained ability: " + file.getAbsolutePath(), e);
}
@@ -483,14 +580,17 @@ public class AbilityController implements IAbilityHandler {
}
chainedAbilityRevision++;
if (migrated > 0) {
LogWriter.info("Migrated " + migrated + " chained abilities");
}
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 name = FileNameHelper.sanitizeName(chain.getName(), "Chain");
chain.setName(name);
String uuid = chain.getId();
boolean isNew = (uuid == null || uuid.isEmpty());
@@ -499,30 +599,25 @@ public class AbilityController implements IAbilityHandler {
uuid = UUID.randomUUID().toString();
chain.setId(uuid);
while (hasChainedAbilityName(name)) {
name = name + "_";
}
name = makeUniqueNameForMap(chainedAbilities, name, null);
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);
}
name = makeUniqueNameForMap(chainedAbilities, name, oldName);
chain.setName(name);
if (!oldName.equals(name)) {
File oldFile = new File(getChainedDir(), oldName + ".json");
if (oldFile.exists()) {
oldFile.delete();
}
chainedAbilities.remove(oldName);
}
} else {
name = makeUniqueNameForMap(chainedAbilities, name, null);
chain.setName(name);
}
}
@@ -558,6 +653,15 @@ public class AbilityController implements IAbilityHandler {
if (name == null || name.isEmpty()) return false;
ChainedAbility removed = chainedAbilities.remove(name);
if (removed == null) {
String sanitized = FileNameHelper.sanitizeTextInput(name);
if (!sanitized.equals(name)) {
removed = chainedAbilities.remove(sanitized);
if (removed != null) {
name = sanitized;
}
}
}
if (removed == null) return false;
String uuid = removed.getId();
@@ -608,6 +712,12 @@ public class AbilityController implements IAbilityHandler {
}
}
String sanitizedKey = FileNameHelper.sanitizeTextInput(key);
if (!sanitizedKey.equals(key)) {
ChainedAbility sanitized = chainedAbilities.get(sanitizedKey);
if (sanitized != null) return sanitized.deepCopy();
}
return null;
}
@@ -618,12 +728,20 @@ public class AbilityController implements IAbilityHandler {
for (String name : chainedAbilities.keySet()) {
if (name.equalsIgnoreCase(key)) return true;
}
String sanitizedKey = FileNameHelper.sanitizeTextInput(key);
if (!sanitizedKey.equals(key) && chainedAbilities.containsKey(sanitizedKey)) return true;
return false;
}
@Override
public ChainedAbility getChainedAbility(String name) {
ChainedAbility chain = chainedAbilities.get(name);
if (chain == null) {
String sanitized = FileNameHelper.sanitizeTextInput(name);
if (!sanitized.equals(name)) {
chain = chainedAbilities.get(sanitized);
}
}
return chain != null ? chain.deepCopy() : null;
}
@@ -646,7 +764,9 @@ public class AbilityController implements IAbilityHandler {
@Override
public boolean hasChainedAbilityName(String name) {
return chainedAbilities.containsKey(name);
if (chainedAbilities.containsKey(name)) return true;
String sanitized = FileNameHelper.sanitizeTextInput(name);
return !sanitized.equals(name) && chainedAbilities.containsKey(sanitized);
}
@Override
@@ -734,6 +854,12 @@ public class AbilityController implements IAbilityHandler {
}
}
String sanitizedKey = FileNameHelper.sanitizeTextInput(key);
if (!sanitizedKey.equals(key)) {
Ability sanitized = customAbilities.get(sanitizedKey);
if (sanitized != null) return sanitized.deepCopy();
}
return null;
}
@@ -778,8 +904,14 @@ public class AbilityController implements IAbilityHandler {
}
public boolean hasAbility(String key) {
return builtAbilities.containsKey(key) || customAbilities.containsKey(key)
|| customAbilitiesById.containsKey(key);
if (builtAbilities.containsKey(key) || customAbilities.containsKey(key)
|| customAbilitiesById.containsKey(key)) {
return true;
}
String sanitized = FileNameHelper.sanitizeTextInput(key);
return !sanitized.equals(key)
&& (builtAbilities.containsKey(sanitized) || customAbilities.containsKey(sanitized)
|| customAbilitiesById.containsKey(sanitized));
}
public Ability getCustomAbilityByUUID(String uuid) {
@@ -787,7 +919,13 @@ public class AbilityController implements IAbilityHandler {
}
public Ability getCustomAbilityByName(String name) {
return customAbilities.get(name);
Ability ability = customAbilities.get(name);
if (ability != null) return ability;
String sanitized = FileNameHelper.sanitizeTextInput(name);
if (!sanitized.equals(name)) {
return customAbilities.get(sanitized);
}
return null;
}
/**
@@ -821,6 +959,9 @@ public class AbilityController implements IAbilityHandler {
if (ability.getName() != null && ability.getName().equalsIgnoreCase(key)) return true;
}
String sanitizedKey = FileNameHelper.sanitizeTextInput(key);
if (!sanitizedKey.equals(key) && customAbilities.containsKey(sanitizedKey)) return true;
return false;
}
@@ -1128,7 +1269,9 @@ public class AbilityController implements IAbilityHandler {
@Override
public boolean hasCustomAbilityName(String name) {
return customAbilities.containsKey(name);
if (customAbilities.containsKey(name)) return true;
String sanitized = FileNameHelper.sanitizeTextInput(name);
return !sanitized.equals(name) && customAbilities.containsKey(sanitized);
}
@Override
@@ -7,6 +7,7 @@ import kamkeel.npcs.controllers.data.ability.data.AbilityIconData;
import kamkeel.npcs.controllers.data.ability.data.IAbilityAction;
import kamkeel.npcs.controllers.data.ability.enums.AbilityPhase;
import kamkeel.npcs.controllers.data.ability.data.effect.AbilityPotionEffect;
import kamkeel.npcs.controllers.data.ability.enums.InvulnerableMode;
import kamkeel.npcs.controllers.data.ability.enums.LockMode;
import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler;
import kamkeel.npcs.controllers.data.ability.util.AbilityTargetHelper;
@@ -21,6 +22,7 @@ import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
import kamkeel.npcs.entity.EntityAbilityBarrier;
import kamkeel.npcs.entity.EntityAbilityDome;
import kamkeel.npcs.entity.EntityAbilityPanel;
import kamkeel.npcs.util.FileNameHelper;
import net.minecraft.block.Block;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityLivingBase;
@@ -85,6 +87,7 @@ public abstract class Ability implements IAbility, IAbilityAction {
// Interruption
protected boolean interruptible = true;
protected InvulnerableMode invulnerableMode = InvulnerableMode.NONE;
// Feedback
protected LockMode lockMovement = LockMode.WINDUP;
@@ -687,10 +690,24 @@ public abstract class Ability implements IAbility, IAbilityAction {
).tab("General"));
defs.add(FieldDef.row(
FieldDef.boolField("ability.interruptible", this::isInterruptible, this::setInterruptible)
.hover("ability.hover.interruptible"),
.hover("ability.hover.interruptible")
.enabledWhen(() -> !invulnerableMode.invulnerableDuringWindup()),
FieldDef.intField("ability.dazedTicks", this::getDazedTicks, this::setDazedTicks)
.range(0, 1000).visibleWhen(this::isInterruptible)
.range(0, 1000)
.visibleWhen(() -> isInterruptible() && !invulnerableMode.invulnerableDuringWindup())
).tab("General"));
defs.add(FieldDef.stringEnumField("ability.invulnerable", InvulnerableMode.getDisplayKeys(),
() -> this.getInvulnerableMode().getDisplayKey(),
v -> {
for (InvulnerableMode mode : InvulnerableMode.values()) {
if (mode.getDisplayKey().equals(v)) {
this.setInvulnerableMode(mode);
break;
}
}
})
.hover("ability.hover.invulnerable")
.tab("General"));
// ── Burst section ────────────────────────────────────────────
if (allowBurst()) {
@@ -1174,6 +1191,9 @@ public abstract class Ability implements IAbility, IAbilityAction {
if (!interruptible || (phase != AbilityPhase.WINDUP && phase != AbilityPhase.BURST_DELAY)) {
return false;
}
if (isInvulnerableForCurrentPhase()) {
return false;
}
// Only direct physical hits can interrupt, not magic, fire, or other indirect damage
if (source == null) {
@@ -1332,6 +1352,7 @@ public abstract class Ability implements IAbility, IAbilityAction {
nbt.setBoolean("syncWindup", syncWindupWithAnimation);
nbt.setInteger("recovery", dazedTicks);
nbt.setBoolean("interruptible", interruptible);
nbt.setInteger("invulnerableMode", invulnerableMode.ordinal());
nbt.setInteger("lockMovement", lockMovement.ordinal());
nbt.setInteger("rotationMode", rotationMode.ordinal());
nbt.setInteger("rotationPhase", rotationPhase.ordinal());
@@ -1411,6 +1432,9 @@ public abstract class Ability implements IAbility, IAbilityAction {
syncWindupWithAnimation = nbt.hasKey("syncWindup") ? nbt.getBoolean("syncWindup") : true;
dazedTicks = nbt.hasKey("recovery") ? nbt.getInteger("recovery") : 80;
interruptible = nbt.hasKey("interruptible") ? nbt.getBoolean("interruptible") : true;
invulnerableMode = nbt.hasKey("invulnerableMode")
? InvulnerableMode.fromOrdinal(nbt.getInteger("invulnerableMode"))
: InvulnerableMode.NONE;
lockMovement = LockMode.fromOrdinal(nbt.getInteger("lockMovement"));
rotationMode = RotationMode.fromOrdinal(nbt.getInteger("rotationMode"));
rotationPhase = LockMode.fromOrdinal(nbt.getInteger("rotationPhase"));
@@ -1501,7 +1525,12 @@ public abstract class Ability implements IAbility, IAbilityAction {
}
public void setName(String name) {
this.name = name;
if (builtIn) {
this.name = name != null ? name : "";
return;
}
String fallback = (this.name != null && !this.name.isEmpty()) ? this.name : "Ability";
this.name = FileNameHelper.sanitizeName(name, fallback);
}
/**
@@ -1510,7 +1539,9 @@ public abstract class Ability implements IAbility, IAbilityAction {
* Converts &amp; color codes to § for rendering.
*/
public String getDisplayName() {
String result = (displayName != null && !displayName.isEmpty()) ? displayName : name;
String result = (displayName != null && !displayName.isEmpty())
? displayName
: FileNameHelper.toDisplayName(name);
return result != null ? result.replaceAll("&([0-9a-fk-or])", "\u00A7$1") : "";
}
@@ -1664,6 +1695,22 @@ public abstract class Ability implements IAbility, IAbilityAction {
this.interruptible = interruptible;
}
public InvulnerableMode getInvulnerableMode() {
return invulnerableMode;
}
public void setInvulnerableMode(InvulnerableMode invulnerableMode) {
this.invulnerableMode = invulnerableMode != null ? invulnerableMode : InvulnerableMode.NONE;
}
public boolean isInvulnerableDuringWindup() {
return invulnerableMode.invulnerableDuringWindup();
}
public boolean isInvulnerableDuringActive() {
return invulnerableMode.invulnerableDuringActive();
}
public LockMode getLockMovement() {
return lockMovement;
}
@@ -1796,6 +1843,20 @@ public abstract class Ability implements IAbility, IAbilityAction {
}
}
/**
* Check if this ability grants invulnerability during its current execution phase.
*/
public boolean isInvulnerableForCurrentPhase() {
return invulnerableMode.isInvulnerableInPhase(phase);
}
/**
* Check if this ability grants invulnerability for a specific phase.
*/
public boolean isInvulnerableForPhase(AbilityPhase phase) {
return invulnerableMode.isInvulnerableInPhase(phase);
}
/**
* Check if rotation should be locked during the current phase.
*/
@@ -9,6 +9,7 @@ import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.nbt.NBTTagList;
import noppes.npcs.api.ability.IChainedAbility;
import kamkeel.npcs.util.FileNameHelper;
import java.util.ArrayList;
import java.util.List;
@@ -77,7 +78,7 @@ public class ChainedAbility implements IChainedAbility, IAbilityAction {
}
public ChainedAbility(String name) {
this.name = name != null ? name : "";
setName(name);
}
// ═══════════════════════════════════════════════════════════════════
@@ -97,7 +98,8 @@ public class ChainedAbility implements IChainedAbility, IAbilityAction {
}
public void setName(String name) {
this.name = name != null ? name : "";
String fallback = (this.name != null && !this.name.isEmpty()) ? this.name : "Chain";
this.name = FileNameHelper.sanitizeName(name, fallback);
}
/**
@@ -106,7 +108,9 @@ public class ChainedAbility implements IChainedAbility, IAbilityAction {
* Converts &amp; color codes to § for rendering.
*/
public String getDisplayName() {
String result = (displayName != null && !displayName.isEmpty()) ? displayName : name;
String result = (displayName != null && !displayName.isEmpty())
? displayName
: FileNameHelper.toDisplayName(name);
return result != null ? result.replaceAll("&([0-9a-fk-or])", "\u00A7$1") : "";
}
@@ -406,7 +410,7 @@ public class ChainedAbility implements IChainedAbility, IAbilityAction {
public void readNBT(NBTTagCompound nbt) {
id = nbt.getString("Id");
name = nbt.getString("Name");
setName(nbt.getString("Name"));
displayName = nbt.getString("DisplayName");
enabled = !nbt.hasKey("Enabled") || nbt.getBoolean("Enabled");
weight = nbt.hasKey("Weight") ? nbt.getInteger("Weight") : 10;
@@ -0,0 +1,66 @@
package kamkeel.npcs.controllers.data.ability.enums;
/**
* Defines when a caster is invulnerable while an ability is executing.
*/
public enum InvulnerableMode {
NONE,
WINDUP,
ACTIVE,
BOTH;
public boolean invulnerableDuringWindup() {
return this == WINDUP || this == BOTH;
}
public boolean invulnerableDuringActive() {
return this == ACTIVE || this == BOTH;
}
/**
* Treat BURST_DELAY as windup-like for interruption/damage immunity checks.
*/
public boolean isInvulnerableInPhase(AbilityPhase phase) {
if (phase == null) return false;
switch (phase) {
case WINDUP:
case BURST_DELAY:
return invulnerableDuringWindup();
case ACTIVE:
return invulnerableDuringActive();
default:
return false;
}
}
public String getDisplayKey() {
switch (this) {
case WINDUP:
return "ability.invulnerable.windup";
case ACTIVE:
return "ability.invulnerable.active";
case BOTH:
return "ability.invulnerable.both";
case NONE:
default:
return "ability.invulnerable.none";
}
}
public static String[] getDisplayKeys() {
return new String[]{
"ability.invulnerable.none",
"ability.invulnerable.windup",
"ability.invulnerable.active",
"ability.invulnerable.both"
};
}
public static InvulnerableMode fromOrdinal(int ordinal) {
InvulnerableMode[] values = values();
if (ordinal >= 0 && ordinal < values.length) {
return values[ordinal];
}
return NONE;
}
}
@@ -91,6 +91,8 @@ public class AbilityDash extends AbilityMovement implements IAbilityDash {
// Type-specific runtime state
private transient DashDirection chosenDirection;
private transient double preDashMotionX;
private transient double preDashMotionZ;
public AbilityDash() {
this.typeId = "ability.cnpc.dash";
@@ -125,6 +127,8 @@ public class AbilityDash extends AbilityMovement implements IAbilityDash {
@Override
public void onExecute(EntityLivingBase caster, EntityLivingBase target) {
preDashMotionX = caster.motionX;
preDashMotionZ = caster.motionZ;
initMovement(caster, dashDistance, dashSpeed);
if (dashMode == DashMode.DIRECTIONAL) {
@@ -151,37 +155,46 @@ public class AbilityDash extends AbilityMovement implements IAbilityDash {
@Override
public void onActiveTick(EntityLivingBase caster, EntityLivingBase target, int tick) {
if (checkTimeout(tick)) {
stopMomentum(caster);
finishDash(caster);
signalCompletion();
return;
}
if (movementDirection == null) {
finishDash(caster);
signalCompletion();
return;
}
if (checkStall(caster, tick)) {
stopMomentum(caster);
finishDash(caster);
signalCompletion();
return;
}
updatePrevPosition(caster);
if (getDistanceTraveledSq(caster) >= (double) dashDistance * dashDistance) {
stopMomentum(caster);
finishDash(caster);
signalCompletion();
return;
}
if (checkBlocked(caster, dashSpeed)) {
stopMomentum(caster);
finishDash(caster);
signalCompletion();
return;
}
// Move caster (motionY left to gravity for skip arc)
applyVelocity(caster, dashSpeed);
// Move caster (motionY left to gravity for skip arc).
// Directional mode layers the dash over incoming movement so momentum
// entering/exiting the dash stays fluid.
if (dashMode == DashMode.DIRECTIONAL) {
applyHorizontalMomentum(caster,
preDashMotionX + movementDirection.xCoord * dashSpeed,
preDashMotionZ + movementDirection.zCoord * dashSpeed);
} else {
applyVelocity(caster, dashSpeed);
}
if (!isPreview()) {
// Trail particles
@@ -191,24 +204,28 @@ public class AbilityDash extends AbilityMovement implements IAbilityDash {
@Override
public void onComplete(EntityLivingBase caster, EntityLivingBase target) {
stopMomentum(caster);
finishDash(caster);
}
@Override
public void onInterrupt(EntityLivingBase caster, DamageSource source, float damage) {
stopMomentum(caster);
finishDash(caster);
}
@Override
public void cleanup() {
super.cleanup();
chosenDirection = null;
preDashMotionX = 0;
preDashMotionZ = 0;
}
@Override
public void resetForBurst() {
super.resetForBurst();
chosenDirection = null;
preDashMotionX = 0;
preDashMotionZ = 0;
}
@Override
@@ -279,6 +296,14 @@ public class AbilityDash extends AbilityMovement implements IAbilityDash {
return chosenDirection;
}
private void finishDash(EntityLivingBase caster) {
if (dashMode == DashMode.DIRECTIONAL) {
applyHorizontalMomentum(caster, preDashMotionX, preDashMotionZ);
} else {
stopMomentum(caster);
}
}
@SideOnly(Side.CLIENT)
@Override
public void getAbilityDefinitions(List<FieldDef> defs) {
@@ -226,7 +226,7 @@ public abstract class AbilityMovement extends Ability {
* By sending S12 manually here — before friction — the client receives the full
* velocity and moves at the correct speed.
*/
private void sendPlayerVelocity(EntityLivingBase caster) {
protected void sendPlayerVelocity(EntityLivingBase caster) {
if (caster instanceof EntityPlayerMP) {
((EntityPlayerMP) caster).playerNetServerHandler.sendPacket(
new S12PacketEntityVelocity(caster));
@@ -234,11 +234,11 @@ public abstract class AbilityMovement extends Ability {
}
/**
* Apply movement velocity to the caster (horizontal only, Y unchanged).
* Apply horizontal momentum and sync to client.
*/
protected void applyVelocity(EntityLivingBase caster, float speed) {
caster.motionX = movementDirection.xCoord * speed;
caster.motionZ = movementDirection.zCoord * speed;
protected void applyHorizontalMomentum(EntityLivingBase caster, double motionX, double motionZ) {
caster.motionX = motionX;
caster.motionZ = motionZ;
if (!isPreview()) {
if (caster instanceof EntityPlayerMP) {
sendPlayerVelocity(caster);
@@ -248,36 +248,27 @@ public abstract class AbilityMovement extends Ability {
}
}
/**
* Apply movement velocity to the caster (horizontal only, Y unchanged).
*/
protected void applyVelocity(EntityLivingBase caster, float speed) {
applyHorizontalMomentum(caster, movementDirection.xCoord * speed, movementDirection.zCoord * speed);
}
/**
* Apply movement velocity with zero vertical motion.
* Used for ground-locked movement like Charge.
*/
protected void applyVelocityFlat(EntityLivingBase caster, float speed) {
caster.motionX = movementDirection.xCoord * speed;
applyHorizontalMomentum(caster, movementDirection.xCoord * speed, movementDirection.zCoord * speed);
caster.motionY = 0;
caster.motionZ = movementDirection.zCoord * speed;
if (!isPreview()) {
if (caster instanceof EntityPlayerMP) {
sendPlayerVelocity(caster);
} else {
caster.velocityChanged = true;
}
}
}
/**
* Stop all horizontal momentum.
*/
protected void stopMomentum(EntityLivingBase caster) {
caster.motionX = 0;
caster.motionZ = 0;
if (!isPreview()) {
if (caster instanceof EntityPlayerMP) {
sendPlayerVelocity(caster);
} else {
caster.velocityChanged = true;
}
}
applyHorizontalMomentum(caster, 0, 0);
}
// ═══════════════════════════════════════════════════════════════════
@@ -22,6 +22,7 @@ import java.io.IOException;
* bit 1: rotation locked (freeze yaw/pitch)
* bit 2: has ability movement (ability controls motion — suppress WASD but don't zero)
* bit 3: position locked (freeze position)
* bit 5: ability is in ACTIVE phase
*/
public final class PlayerAbilityStatePacket extends AbstractPacket {
public static final String packetName = "Data|PlayerAbilityState";
@@ -31,6 +32,7 @@ public final class PlayerAbilityStatePacket extends AbstractPacket {
public static final byte FLAG_HAS_ABILITY_MOVEMENT = 4;
public static final byte FLAG_POSITION_LOCKED = 8;
public static final byte FLAG_WAS_FLYING_AT_LOCK = 16;
public static final byte FLAG_ACTIVE_PHASE = 32;
private byte flags;
private float lockedYaw;
@@ -0,0 +1,97 @@
package kamkeel.npcs.util;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
/**
* Utility helpers for filename-safe ability/chained names.
* Allowed characters are letters, digits, underscore, and dash.
*/
public final class FileNameHelper {
private static final Set<String> WINDOWS_RESERVED_NAMES = new HashSet<String>(Arrays.asList(
"CON", "PRN", "AUX", "NUL",
"COM1", "COM2", "COM3", "COM4", "COM5", "COM6", "COM7", "COM8", "COM9",
"LPT1", "LPT2", "LPT3", "LPT4", "LPT5", "LPT6", "LPT7", "LPT8", "LPT9"
));
private FileNameHelper() {
}
/**
* Sanitize a name to a filename-safe identifier.
* Whitespace becomes underscores; unsupported chars are removed.
*/
public static String sanitizeName(String input, String fallback) {
String sanitized = sanitizeInternal(input);
if (sanitized.isEmpty()) {
sanitized = sanitizeInternal(fallback);
}
if (sanitized.isEmpty()) {
sanitized = "Ability";
}
if (isReservedWindowsName(sanitized)) {
sanitized = "_" + sanitized;
}
return sanitized;
}
/**
* Sanitize textfield input without forcing a fallback.
*/
public static String sanitizeTextInput(String input) {
return sanitizeInternal(input);
}
/**
* Quick check for compatibility with sanitized identifier rules.
*/
public static boolean isFileNameCompatible(String value) {
if (value == null || value.isEmpty()) return false;
if (isReservedWindowsName(value)) return false;
for (int i = 0; i < value.length(); i++) {
char c = value.charAt(i);
if (!Character.isLetterOrDigit(c) && c != '_' && c != '-') {
return false;
}
}
return true;
}
/**
* Human-readable fallback display text for sanitized names.
*/
public static String toDisplayName(String value) {
if (value == null || value.isEmpty()) return "";
return value.replace('_', ' ');
}
private static String sanitizeInternal(String raw) {
if (raw == null) return "";
String trimmed = raw.trim();
if (trimmed.isEmpty()) return "";
StringBuilder out = new StringBuilder(trimmed.length());
for (int i = 0; i < trimmed.length(); i++) {
char c = trimmed.charAt(i);
if (Character.isLetterOrDigit(c)) {
out.append(c);
} else if (c == '_' || c == '-') {
out.append(c);
} else if (Character.isWhitespace(c)) {
if (out.length() == 0) continue;
char last = out.charAt(out.length() - 1);
if (last == '_') continue;
out.append('_');
}
}
return out.toString();
}
private static boolean isReservedWindowsName(String value) {
return WINDOWS_RESERVED_NAMES.contains(value.toUpperCase());
}
}
@@ -268,6 +268,15 @@ public abstract class AbstractDataAbilities {
return currentAbility != null && currentAbility.isExecuting();
}
/**
* True when an executing ability grants invulnerability for its current phase.
*/
public boolean isCurrentAbilityInvulnerable() {
return currentAbility != null
&& currentAbility.isExecuting()
&& currentAbility.isInvulnerableForCurrentPhase();
}
/**
* Get the currently executing ability.
*/
+6 -3
View File
@@ -555,12 +555,15 @@ public class DataAbilities extends AbstractDataAbilities {
* @return true if ability was interrupted
*/
public boolean onDamage(DamageSource source, float amount) {
// Track hit for hit count condition
recordHit();
if (currentAbility == null || !currentAbility.isExecuting()) {
return false;
}
if (currentAbility.isInvulnerableForCurrentPhase()) {
return false;
}
// Track hit for hit count condition
recordHit();
net.minecraft.entity.Entity sourceEntity = source.getEntity();
EntityLivingBase attacker = sourceEntity instanceof EntityLivingBase ? (EntityLivingBase) sourceEntity : null;
@@ -629,7 +629,12 @@ public class ScriptPlayerEventHandler {
// Handle ability damage interactions (Guard counter, damage reduction)
PlayerData pData = PlayerData.get((EntityPlayer) event.entityLiving);
if (pData != null && pData.abilityData != null && pData.abilityData.isExecutingAbility()) {
event.ammount = pData.abilityData.onDamage(event.source, event.ammount);
if (pData.abilityData.isCurrentAbilityInvulnerable()) {
event.ammount = 0;
cancel = true;
} else {
event.ammount = pData.abilityData.onDamage(event.source, event.ammount);
}
}
PlayerDataScript handler = ScriptController.Instance.getPlayerScripts((EntityPlayer) event.entityLiving);
@@ -16,6 +16,7 @@ public class ClientAbilityState {
public static boolean hasAbilityMovement = false;
public static boolean positionLocked = false;
public static boolean wasFlyingAtLock = false;
public static boolean activePhase = false;
public static float lockedYaw = 0;
public static float lockedPitch = 0;
@@ -28,6 +29,7 @@ public class ClientAbilityState {
hasAbilityMovement = (flags & PlayerAbilityStatePacket.FLAG_HAS_ABILITY_MOVEMENT) != 0;
positionLocked = (flags & PlayerAbilityStatePacket.FLAG_POSITION_LOCKED) != 0;
wasFlyingAtLock = (flags & PlayerAbilityStatePacket.FLAG_WAS_FLYING_AT_LOCK) != 0;
activePhase = (flags & PlayerAbilityStatePacket.FLAG_ACTIVE_PHASE) != 0;
lockedYaw = yaw;
lockedPitch = pitch;
}
@@ -56,6 +58,7 @@ public class ClientAbilityState {
hasAbilityMovement = false;
positionLocked = false;
wasFlyingAtLock = false;
activePhase = false;
lockedYaw = 0;
lockedPitch = 0;
}
@@ -54,6 +54,7 @@ public class ClientTickHandler {
private long buttonTime = 0L;
private final int[] ignoreKeys = new int[]{157, 29, 54, 42, 184, 56, 220, 219};
private boolean lastSpecialKeyDown = false;
private boolean wasMovementSuppressed = false;
@SubscribeEvent(priority = EventPriority.LOWEST)
public void onClientTick(TickEvent.ClientTickEvent event) {
@@ -84,9 +85,15 @@ public class ClientTickHandler {
lastSpecialKeyDown = specialKeyDown;
}
boolean suppressMovement = mc.currentScreen == null && ClientAbilityState.shouldSuppressMovementInput();
if (!suppressMovement && wasMovementSuppressed) {
syncMovementKeyStates(mc);
}
wasMovementSuppressed = suppressMovement;
// Suppress player input during ability-controlled phases.
// Only suppress when no GUI screen is open (screens already capture input).
if (mc.currentScreen == null && ClientAbilityState.shouldSuppressMovementInput()) {
if (suppressMovement) {
// Unpress movement keybinds at the source BEFORE updatePlayerMoveState() reads them.
KeyBinding.setKeyBindState(mc.gameSettings.keyBindForward.getKeyCode(), false);
KeyBinding.setKeyBindState(mc.gameSettings.keyBindBack.getKeyCode(), false);
@@ -301,6 +308,27 @@ public class ClientTickHandler {
|| keyCode == mc.gameSettings.keyBindSprint.getKeyCode();
}
private void syncMovementKeyStates(Minecraft mc) {
syncKeyBindingState(mc.gameSettings.keyBindForward);
syncKeyBindingState(mc.gameSettings.keyBindBack);
syncKeyBindingState(mc.gameSettings.keyBindLeft);
syncKeyBindingState(mc.gameSettings.keyBindRight);
syncKeyBindingState(mc.gameSettings.keyBindJump);
syncKeyBindingState(mc.gameSettings.keyBindSneak);
syncKeyBindingState(mc.gameSettings.keyBindSprint);
}
private void syncKeyBindingState(KeyBinding keyBinding) {
int keyCode = keyBinding.getKeyCode();
boolean pressed;
if (keyCode < 0) {
pressed = Mouse.isButtonDown(keyCode + 100);
} else {
pressed = Keyboard.isKeyDown(keyCode);
}
KeyBinding.setKeyBindState(keyCode, pressed);
}
private boolean isIgnoredKey(int key) {
int[] var2 = this.ignoreKeys;
int var3 = var2.length;
@@ -84,8 +84,12 @@ public class SubGuiAbilitySaveConfirm extends SubGuiInterface implements ISubGui
@Override
public void subGuiClosed(SubGuiInterface subgui) {
if (subgui instanceof SubGuiDuplicateNameConfirm) {
if (((SubGuiDuplicateNameConfirm) subgui).isConfirmed()) {
SubGuiDuplicateNameConfirm confirm = (SubGuiDuplicateNameConfirm) subgui;
if (confirm.isConfirmed()) {
doSave();
} else if (confirm.isBack()) {
// Keep this save-confirm dialog open.
return;
} else {
close();
}
@@ -10,7 +10,13 @@ import noppes.npcs.client.gui.util.SubGuiInterface;
*/
public class SubGuiDuplicateNameConfirm extends SubGuiInterface {
private boolean confirmed = false;
public enum Result {
CONTINUE,
BACK,
CANCEL
}
private Result result = Result.CANCEL;
public SubGuiDuplicateNameConfirm() {
setBackground("menubg.png");
@@ -26,22 +32,38 @@ public class SubGuiDuplicateNameConfirm extends SubGuiInterface {
addLabel(new GuiNpcLabel(0, "ability.duplicate.name", guiLeft + 10, y));
y += 30;
addButton(new GuiNpcButton(0, guiLeft + 20, y, 80, 20, "gui.continue"));
addButton(new GuiNpcButton(1, guiLeft + 120, y, 80, 20, "gui.cancel"));
addButton(new GuiNpcButton(0, guiLeft + 10, y, 65, 20, "gui.continue"));
addButton(new GuiNpcButton(1, guiLeft + 78, y, 65, 20, "gui.back"));
addButton(new GuiNpcButton(2, guiLeft + 146, y, 65, 20, "gui.cancel"));
}
@Override
public void buttonEvent(GuiButton guibutton) {
if (guibutton.id == 0) {
confirmed = true;
result = Result.CONTINUE;
close();
} else if (guibutton.id == 1) {
confirmed = false;
result = Result.BACK;
close();
} else if (guibutton.id == 2) {
result = Result.CANCEL;
close();
}
}
public Result getResult() {
return result;
}
public boolean isConfirmed() {
return confirmed;
return result == Result.CONTINUE;
}
public boolean isBack() {
return result == Result.BACK;
}
public boolean isCancelled() {
return result == Result.CANCEL;
}
}
@@ -245,6 +245,9 @@ public class GuiFieldBuilder {
case STRING: {
String sVal = def.getValue() != null ? def.getValue().toString() : "";
GuiNpcTextField tf = new GuiNpcTextField(widgetId, parent, fontRenderer, fieldX, y, fieldW, 20, sVal);
if ("gui.name".equals(def.getLabel())) {
tf.setFileNameSafe();
}
if (!def.isEnabled()) tf.setEnabled(false);
if (hover != null) tf.setHoverText(hover);
sw.addTextField(tf);
@@ -104,7 +104,6 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface
// ==================== PENDING SAVE ====================
private Ability pendingSaveAbility = null;
private boolean pendingNewCreation = false;
// ==================== PENDING VARIANT ====================
private String pendingTypeId = null;
@@ -511,13 +510,6 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface
Ability newAbility = AbilityController.Instance.create(pendingTypeId);
if (newAbility != null) {
gui.getVariants().get(idx).apply(newAbility);
if (hasDuplicateName(newAbility)) {
pendingSaveAbility = newAbility;
pendingNewCreation = true;
pendingTypeId = null;
setSubGui(new SubGuiDuplicateNameConfirm());
return true;
}
pendingTypeId = null;
openConfig(newAbility);
return true;
@@ -544,12 +536,6 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface
if (variants.size() == 1) {
variants.get(0).apply(newAbility);
}
if (hasDuplicateName(newAbility)) {
pendingSaveAbility = newAbility;
pendingNewCreation = true;
setSubGui(new SubGuiDuplicateNameConfirm());
return true;
}
openConfig(newAbility);
return true;
}
@@ -561,27 +547,22 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface
* @return true if a SubGui was opened (caller should return early, skipping initGui)
*/
private boolean handleDuplicateNameClosed(SubGuiDuplicateNameConfirm gui) {
if (gui.isConfirmed() && pendingSaveAbility != null) {
if (pendingNewCreation) {
openConfig(pendingSaveAbility);
pendingSaveAbility = null;
pendingNewCreation = false;
return true;
} else {
PacketClient.sendClient(new CustomAbilitySavePacket(pendingSaveAbility.writeNBT()));
pendingSaveAbility = null;
}
} else if (pendingSaveAbility != null) {
if (pendingNewCreation) {
pendingSaveAbility = null;
pendingNewCreation = false;
} else {
Ability ability = pendingSaveAbility;
pendingSaveAbility = null;
setSubGui(ability.createConfigGui(this));
return true;
}
if (pendingSaveAbility == null) return false;
if (gui.isConfirmed()) {
PacketClient.sendClient(new CustomAbilitySavePacket(pendingSaveAbility.writeNBT()));
pendingSaveAbility = null;
return false;
}
if (gui.isBack()) {
Ability ability = pendingSaveAbility;
pendingSaveAbility = null;
setSubGui(ability.createConfigGui(this));
return true;
}
pendingSaveAbility = null;
return false;
}
@@ -590,7 +571,6 @@ public class GuiNpcManageAbilities extends GuiAbilityInterface
*/
private boolean handlePendingSave() {
if (hasDuplicateName(pendingSaveAbility)) {
pendingNewCreation = false;
setSubGui(new SubGuiDuplicateNameConfirm());
return true;
}
@@ -9,6 +9,7 @@ import kamkeel.npcs.network.PacketHandler;
import kamkeel.npcs.network.packets.player.ability.AbilityHotbarSelectPacket;
import net.minecraft.client.Minecraft;
import noppes.npcs.client.ClientProxy;
import noppes.npcs.client.ClientAbilityState;
import net.minecraft.client.gui.FontRenderer;
import net.minecraft.client.gui.GuiButton;
import net.minecraft.client.gui.ScaledResolution;
@@ -189,6 +190,7 @@ public class AbilityHotbarComponent extends HudComponent {
int centerX = overlayWidth / 2;
int centerY = overlayHeight / 2;
boolean activePhase = ClientAbilityState.activePhase;
// Compute text visibility: 0=Shown, 1=Hidden, 2=Held
boolean showText;
@@ -246,16 +248,16 @@ public class AbilityHotbarComponent extends HudComponent {
if (rawSlotIndex == -1) {
// Deselect slot — draw empty marker
drawDeselectSlot(cx, cy, scaledSize, isCenter, slotAlpha);
drawDeselectSlot(cx, cy, scaledSize, isCenter, slotAlpha, activePhase);
} else {
slots[rawSlotIndex].drawCarousel(mc, sr, slotCooldown, cx, cy, scaledSize, isCenter, slotAlpha, showText);
slots[rawSlotIndex].drawCarousel(mc, sr, slotCooldown, cx, cy, scaledSize, isCenter, slotAlpha, showText, activePhase);
}
}
GL11.glPopMatrix();
}
private void drawDeselectSlot(int cx, int cy, int size, boolean isCenter, float alpha) {
private void drawDeselectSlot(int cx, int cy, int size, boolean isCenter, float alpha, boolean activePhase) {
float radius = size / 2f;
float baseAlpha = isCenter ? 0.9f : 0.6f;
float a = baseAlpha * alpha;
@@ -268,7 +270,13 @@ public class AbilityHotbarComponent extends HudComponent {
GL11.glEnable(GL11.GL_BLEND);
GL11.glBlendFunc(GL11.GL_SRC_ALPHA, GL11.GL_ONE_MINUS_SRC_ALPHA);
if (isCenter) {
if (activePhase) {
if (isCenter) {
GL11.glColor4f(0.65f, 0.14f, 0.14f, a);
} else {
GL11.glColor4f(0.38f, 0.10f, 0.10f, a);
}
} else if (isCenter) {
GL11.glColor4f(0.5f, 0.2f, 0.2f, a);
} else {
GL11.glColor4f(0.3f, 0.3f, 0.3f, a);
@@ -292,7 +300,13 @@ public class AbilityHotbarComponent extends HudComponent {
}
// Draw X mark
if (isCenter) {
if (activePhase) {
if (isCenter) {
GL11.glColor4f(1.0f, 0.45f, 0.35f, alpha);
} else {
GL11.glColor4f(0.75f, 0.30f, 0.30f, alpha * 0.8f);
}
} else if (isCenter) {
GL11.glColor4f(0.8f, 0.4f, 0.4f, alpha);
} else {
GL11.glColor4f(0.5f, 0.5f, 0.5f, alpha * 0.8f);
@@ -48,7 +48,7 @@ public class AbilityHotbarSlotRenderer extends Gui {
public void drawCarousel(Minecraft mc, ScaledResolution sr, float cooldownProgress,
int cx, int cy, int size, boolean isCenter, float slotAlpha,
boolean showText) {
boolean showText, boolean activePhase) {
float nameAlpha = this.nameAlpha;
boolean isHorizontal = ConfigClient.AbilityHotbarHorizontal;
if (size <= 0 || slotAlpha <= 0) return;
@@ -61,21 +61,54 @@ public class AbilityHotbarSlotRenderer extends Gui {
boolean altTexture = ConfigClient.AbilityHotbarAltTexture;
if (altTexture) {
drawRoundedRect(-radius, -radius, radius, radius, 0.6f, 0.6f, 0.6f, 0.75f * alpha, false);
float fillR;
float fillG;
float fillB;
float outlineR;
float outlineG;
float outlineB;
if (activePhase) {
if (isCenter) {
drawRoundedRect(-radius, -radius, radius, radius, 0.8f, 0.8f, 0.2f, alpha, true);
fillR = 0.95f;
fillG = 0.26f;
fillB = 0.22f;
outlineR = 1.0f;
outlineG = 0.52f;
outlineB = 0.28f;
} else {
drawRoundedRect(-radius, -radius, radius, radius, 0.4f, 0.4f, 0.4f, alpha * 0.8f, true);
fillR = 0.36f;
fillG = 0.14f;
fillB = 0.14f;
outlineR = 0.65f;
outlineG = 0.22f;
outlineB = 0.20f;
}
} else if (isCenter) {
fillR = 0.8f;
fillG = 0.8f;
fillB = 0.2f;
outlineR = 0.8f;
outlineG = 0.8f;
outlineB = 0.2f;
} else {
if (isCenter) {
drawCircle(radius, 0.8f, 0.8f, 0.2f, 0.66f * alpha, false);
drawCircle(radius, 0.8f, 0.8f, 0.2f, alpha, true);
} else {
drawCircle(radius, 1f, 1f, 1f, 0.66f * alpha, false);
drawCircle(radius, 0.4f, 0.4f, 0.4f, alpha * 0.8f, true);
}
fillR = 0.4f;
fillG = 0.4f;
fillB = 0.4f;
outlineR = 1f;
outlineG = 1f;
outlineB = 1f;
}
if (altTexture) {
float baseR = activePhase ? 0.30f : 0.6f;
float baseG = activePhase ? 0.12f : 0.6f;
float baseB = activePhase ? 0.12f : 0.6f;
drawRoundedRect(-radius, -radius, radius, radius, baseR, baseG, baseB, 0.75f * alpha, false);
drawRoundedRect(-radius, -radius, radius, radius, fillR, fillG, fillB, alpha * 0.8f, true);
} else {
drawCircle(radius, outlineR, outlineG, outlineB, 0.66f * alpha, false);
drawCircle(radius, fillR, fillG, fillB, alpha * 0.8f, true);
}
// Ability icon
@@ -1,6 +1,7 @@
package noppes.npcs.client.gui.util;
import kamkeel.npcs.util.TextSplitter;
import kamkeel.npcs.util.FileNameHelper;
import net.minecraft.client.Minecraft;
import net.minecraft.client.gui.FontRenderer;
import net.minecraft.client.gui.GuiScreen;
@@ -26,6 +27,7 @@ public class GuiNpcTextField extends GuiTextField {
public float minFloat = 0, maxFloat = Float.MAX_VALUE, defFloat = 0;
protected static GuiNpcTextField activeTextfield = null;
public boolean canEdit = true;
private boolean fileNameSafe = false;
public String hoverableText = "";
private boolean wasHovered = false;
private int hoverCount = 0;
@@ -71,6 +73,11 @@ public class GuiNpcTextField extends GuiTextField {
public boolean textboxKeyTyped(char c, int i) {
if (!canEdit || !isFocused())
return false;
if (fileNameSafe && !GuiScreen.isCtrlKeyDown() && !isFileNameCharAllowed(c, i)) {
return false;
}
// Allow Ctrl shortcuts (select all, copy, paste, cut) to bypass char filter
if (GuiScreen.isCtrlKeyDown()) {
// For Ctrl+V (paste) on numeric fields, filter clipboard to valid chars
@@ -87,7 +94,17 @@ public class GuiNpcTextField extends GuiTextField {
}
return true;
}
return super.textboxKeyTyped(c, i);
if (i == 47 && fileNameSafe) {
String clipboard = GuiScreen.getClipboardString();
if (clipboard != null) {
String filtered = filterFileNameClipboard(clipboard);
if (!filtered.isEmpty())
writeText(filtered);
}
return true;
}
boolean handled = super.textboxKeyTyped(c, i);
return handled;
}
if (!charAllowed(c, i))
return false;
@@ -179,6 +196,9 @@ public class GuiNpcTextField extends GuiTextField {
else if (getFloat() > maxFloat)
setText(maxFloat + "");
}
if (fileNameSafe) {
normalizeFileNameText();
}
setCursorPositionZero();
if (listener != null)
listener.unFocused(this);
@@ -238,6 +258,51 @@ public class GuiNpcTextField extends GuiTextField {
return this;
}
public GuiNpcTextField setFileNameSafe() {
fileNameSafe = true;
normalizeFileNameText();
return this;
}
private boolean isSpecialNavigationKey(int keyCode) {
for (int allowed : allowedSpecialChars) {
if (allowed == keyCode) {
return true;
}
}
return false;
}
private boolean isFileNameCharAllowed(char c, int keyCode) {
if (isSpecialNavigationKey(keyCode) || c < 32) {
return true;
}
return Character.isLetterOrDigit(c) || c == '_' || c == '-' || c == ' ';
}
private String filterFileNameClipboard(String clipboard) {
StringBuilder filtered = new StringBuilder(clipboard.length());
for (int idx = 0; idx < clipboard.length(); idx++) {
char c = clipboard.charAt(idx);
if (Character.isLetterOrDigit(c) || c == '_' || c == '-') {
filtered.append(c);
} else if (Character.isWhitespace(c)) {
filtered.append(' ');
}
}
return filtered.toString();
}
private void normalizeFileNameText() {
String current = getText();
String sanitized = FileNameHelper.sanitizeTextInput(current);
if (sanitized.equals(current)) return;
int cursor = getCursorPosition();
setText(sanitized);
setCursorPosition(Math.min(cursor, sanitized.length()));
}
public void setHoverText(String text) {
this.hoverableText = StatCollector.translateToLocal(text);
}
@@ -806,6 +806,9 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA
if (currentAbility == null || !currentAbility.isExecuting()) {
return amount;
}
if (currentAbility.isInvulnerableForCurrentPhase()) {
return 0;
}
net.minecraft.entity.Entity sourceEntity = source.getEntity();
EntityLivingBase attacker = sourceEntity instanceof EntityLivingBase ? (EntityLivingBase) sourceEntity : null;
@@ -937,6 +940,9 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA
if (wasFlyingAtLock) {
flags |= PlayerAbilityStatePacket.FLAG_WAS_FLYING_AT_LOCK;
}
if (currentAbility.getPhase() == AbilityPhase.ACTIVE) {
flags |= PlayerAbilityStatePacket.FLAG_ACTIVE_PHASE;
}
return flags;
}
@@ -659,6 +659,9 @@ public abstract class EntityNPCInterface extends EntityCreature implements IEnti
if (damagesource.damageType != null && damagesource.damageType.equals("outOfWorld") && isKilled()) {
reset();
}
if (this.abilities != null && this.abilities.isCurrentAbilityInvulnerable()) {
return false;
}
// Check for custom weapon attack speed - bypass immunity if enough time has passed
Entity sourceEntity = damagesource.getEntity();