HUGE CORE MIGRATION (WIP, WONT BUILD)

This commit is contained in:
bigguy345 committed 2026-03-16 05:55:35 +02:00
1 parent f4aa04dcb6
commit 2e2b52252d
335 files changed
+64280 -454

No files matched your search

+112 -9
View File
@@ -1,3 +1,112 @@
________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________
## 🔴 VITAL AGENT CONTEXT — MIGRATION WORKFLOW (READ BEFORE ANY MIGRATION WORK)
### 🚨 ABSOLUTE NON-NEGOTIABLE RULE — TOOLCHAIN ONLY (UPDATED 2026-03-16)
**Agents MUST use the migration toolchain scripts for ALL write operations during migration. NEVER use internal write/edit tools.**
The following tools exist in `tools/migration/` and MUST be used:
| Operation | Tool | NEVER Use Instead |
|-----------|------|-------------------|
| Create PA interface files | `pa_batch.py` | `write()` |
| Edit PlatformService | `pa_editor.py` | `edit()` |
| Copy files to core/ | `Copy-Recursive.ps1` | `cp` / `write()` |
| Replace MC types | `Symbol-Swap.ps1` | `edit()` / `ast_grep_replace` |
| Remove forbidden imports | `Nuke-Imports.ps1` | `edit()` |
| Regex transforms | `Static-Transform.ps1` | `edit()` / `sed` |
| Create stub classes | `Generate-Stubs.ps1` | `write()` |
| Deduplicate imports | `Dedupe-Imports.ps1` | manual |
| Create PA + propagate | `Create-And-Propagate.ps1` | `write()` + `edit()` |
| Build diagnostics | `Build-Report.ps1` | manual |
| Generate PA manifests | `generate_pa_manifest.py` | manual |
| Generate wave scripts | `generate_wave_script.py` | manual |
| Generate type mappings | `analyze.py generate-mappings` | manual |
| Batch diff all files | `batch_migration_diff.py` | manual |
**WHY**: The internal write/edit tools are 100-1000x SLOWER and produce inconsistent results. The toolchain scripts are BATCH operations that process hundreds of files in milliseconds. This is the ONLY acceptable approach.
**VIOLATION = IMMEDIATE REJECTION OF WORK.**
### Sisyphus Execution Model (UPDATED 2026-03-16)
**Sisyphus delegates migration Parts to agents that MUST use the toolchain. Agents run PowerShell/Python commands via `bash()` tool ONLY.**
### 🚀 CRITICAL EXECUTION RULES — DO NOT DEVIATE
**1. TOOLCHAIN-ONLY Writes (MANDATORY — NON-NEGOTIABLE)**
- ALL file creation → `pa_batch.py`, `Generate-Stubs.ps1`, `Create-And-Propagate.ps1`
- ALL file edits → `Symbol-Swap.ps1`, `Static-Transform.ps1`, `Nuke-Imports.ps1`, `Dedupe-Imports.ps1`
- ALL PlatformService changes → `pa_editor.py`
- Agents use `bash()` to invoke these tools. They may use `read()` and `grep` to inspect files.
- The ONLY acceptable use of `edit()` is for fixing individual LSP errors AFTER toolchain scripts have run.
**2. Parallel Bulk Edits via Toolchain (MANDATORY)**
- Copy-Recursive copies ALL files in one command
- Symbol-Swap replaces ALL types in one command
- Static-Transform applies ALL patterns in one command
- Result: surgical edits, formatting preserved, comments intact, minimal diff bloat
**2. LSP-Driven Development (MANDATORY — THIS IS THE ONLY VERIFICATION)**
- LSP is the PRIMARY and ONLY source of truth — NOT code analysis, NOT grepping
- After applying ALL edits for a Part, run `lsp_diagnostics` on EVERY moved file
- **Do NOT proceed until LSP shows ZERO errors**
- If LSP shows errors → STOP and fix those specific errors immediately (same response or next)
- **If LSP is broken, unresponsive, or malfunctioning → STOP WORK and notify human immediately**
- Per-wave rule: **Do NOT move to next Part/Wave until LSP shows zero errors for current Part**
**3. NO Commits by Sisyphus (MANDATORY)**
- DO NOT commit after Parts complete
- Committing requires build verification (which is slow and blocking)
- Human will verify build afterward and commit
- Sisyphus responsibility: code correctness via LSP. Human responsibility: build verification + commit.
- Just complete the Part, report status, move to next
**4. Error Recovery (MANDATORY)**
- If LSP shows errors after edits: analyze the error, fix the `oldString` pattern, re-apply edit
- If multiple errors: fix them all in ONE response, then re-run LSP
- **Do NOT create partial fixes** — fix the entire pattern once
### Execution Pattern (Per Part)
```
PART N:
Response 1: Read ALL files for Part N (parallel)
Response 2:
- Apply ALL edits (parallel edit() calls, bulk changes per file)
- Run lsp_diagnostics on ALL moved files
Response 3+ (if needed):
- IF LSP shows errors → FIX and re-run lsp_diagnostics
- REPEAT until zero errors
When LSP ✅ ZERO errors → Part N COMPLETE, move to Part N+1
```
### Parallel Capability
- Sisyphus can edit 8-10 files in a single response
- All `edit()` calls execute concurrently
- Typical Part has 3-8 files → all edited at once
### Why This Matters
- **Speed:** No agent spawn overhead, bulk edits, parallel execution → ~2-3 hours total
- **Quality:** LSP verification ensures correctness, format preservation ensures clean diffs
- **Reliability:** Sisyphus maintains full context and can fix errors immediately
- **Human Control:** Build verification and committing stays with human (prevents broken builds)
---
### Human-Agent Division of Labor (OLD — OUTDATED, KEPT FOR REFERENCE)
~~**The human (project owner) will manually move all packages/files from m7 → core/.**~~
**UPDATED: Sisyphus moves files + fixes them via LSP verification.**
### Why LSP is Essential (REAFFIRMED)
- After files are moved to core/, they will have broken MC imports
- `lsp_diagnostics` produces the EXACT list of what's broken: file, line, column, error message
- This is the ONLY reliable guide — NOT guessing, NOT grepping
- Sisyphus MUST run `lsp_diagnostics` after applying ALL edits for a Part
- Sisyphus MUST re-run `lsp_diagnostics` after EACH fix until convergence to zero errors
### PA Package Placement Rule (UPDATED 2026-03-15)
- If the abstraction interface already exists under `noppes.npcs.api.*` → use it as-is
- If it's a NEW abstraction for MC classes → create under `common.minecraft.*` in platform-api (NOT `noppes.npcs.common.minecraft`, just `common.minecraft`)
________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________
## ⚠️ MIGRATION ORCHESTRATION RULES (APPLY TO ALL MIGRATION WORK)
@@ -30,6 +139,7 @@ The orchestrating agent (Sisyphus) will manually verify ALL delegated work:
### 5. Context Passing
- Every delegated agent MUST receive the full content of SESSION_MEMORY.md in their prompt
- Every agent must be of the UNSPECIFIED HIGH CATEGORY, unless explicitly told to use another
- This ensures every agent understands the vision, architecture, constraints, and forbidden practices
### 6. Phase Reports
@@ -63,7 +173,7 @@ The orchestrating agent (Sisyphus) will manually verify ALL delegated work:
- Record any discoveries, surprises, or deviations in SESSION_MEMORY.md
________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________
CURRENT USER PROMPT:
MAIN USER VISION:
## 1) MINECRAFT/FORGE PLATFORM ABSTRACTION:
@@ -122,14 +232,7 @@ to supply us with an ICustomNpc or an IPlayer, etc. which represent the system.
DO NOT CHANGE THEIR NAMES AND DO NOT CREATE SEMANTICALLY DUPLICATE INTERFACES/CLASSES .
2) TO BE DECIDED BY YOU.
> **AGENT RECOMMENDATION FOR POINT 1:**
> Feasible. Move the API into platform-api in phases:
> - **Phase 1:** Move ~131 MC-free interfaces directly (retain package structure, strip 5 generic type params)
> - **Phase 2:** Migrate 71 MC-contaminated files by replacing MC type references with platform-api equivalents or Object returns
> - **Phase 3:** Reconcile the 30 existing split-package shadow conflicts (keep one authoritative version)
> - **Phase 4:** Update addon mods (DBC, Gecko, AW) to depend on platform-api artifact instead of git submodule
>
> **AGENT RECOMMENDATION FOR POINT 2 — SEE MIGRATION ROADMAP BELOW**
> **AGENT RECOMMENDATION — SEE .AGENTS/MIGRATION_ROADMAP.md*
________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________
+7
View File
@@ -9,8 +9,15 @@ version = '1.0'
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
repositories {
mavenCentral()
}
dependencies {
api project(':platform-api')
implementation 'com.google.code.gson:gson:2.8.9'
implementation 'org.apache.commons:commons-lang3:3.12.0'
implementation 'org.apache.logging.log4j:log4j-api:2.17.1'
}
tasks.withType(JavaCompile).configureEach {
@@ -0,0 +1,26 @@
package io.netty.buffer;
/**
* TEMPORARY STUB -- auto-generated for compilation.
* Netty ByteBuf stub for core compilation.
*/
public abstract class ByteBuf {
public abstract ByteBuf writeInt(int value);
public abstract int readInt();
public abstract ByteBuf writeByte(int value);
public abstract byte readByte();
public abstract ByteBuf writeShort(int value);
public abstract short readShort();
public abstract ByteBuf writeLong(long value);
public abstract long readLong();
public abstract ByteBuf writeFloat(float value);
public abstract float readFloat();
public abstract ByteBuf writeDouble(double value);
public abstract double readDouble();
public abstract ByteBuf writeBoolean(boolean value);
public abstract boolean readBoolean();
public abstract ByteBuf writeBytes(byte[] src);
public abstract ByteBuf readBytes(byte[] dst);
public abstract int readableBytes();
public abstract boolean isReadable();
}
@@ -0,0 +1,77 @@
package kamkeel.npcs;
import kamkeel.npcs.controllers.AttributeController;
import kamkeel.npcs.controllers.data.attribute.AttributeDefinition;
import kamkeel.npcs.controllers.data.attribute.AttributeValueType;
import kamkeel.npcs.controllers.data.attribute.requirement.RequirementCheckerRegistry;
import kamkeel.npcs.controllers.data.attribute.requirement.types.ProfileSlotRequirement;
import kamkeel.npcs.controllers.data.attribute.requirement.types.QuestActiveRequirement;
import kamkeel.npcs.controllers.data.attribute.requirement.types.QuestCompletedRequirement;
import kamkeel.npcs.controllers.data.attribute.requirement.types.SoulbindRequirement;
/**
* ModAttributes registers the core attributes (both non-magic and magic-based).
*/
public class CustomAttributes {
public static final String HEALTH_KEY = "health";
public static final String HEALTH_BOOST_KEY = "health_boost";
public static final String MAIN_ATTACK_KEY = "main_attack";
public static final String MAIN_ATTACK_BOOST_KEY = "main_attack_boost";
public static final String NEUTRAL_KEY = "neutral";
public static final String NEUTRAL_BOOST_KEY = "neutral_boost";
public static final String CRITICAL_DAMAGE_KEY = "critical_damage";
public static final String CRITICAL_CHANCE_KEY = "critical_chance";
public static final String MAGIC_DAMAGE_KEY = "magic_damage";
public static final String MAGIC_BOOST_KEY = "magic_boost";
public static final String MAGIC_DEFENSE_KEY = "magic_defense";
public static final String MAGIC_RESISTANCE_KEY = "magic_resistance";
public static final String MOVEMENT_SPEED_KEY = "movement_speed";
public static final String KNOCKBACK_RES_KEY = "knockback_res";
public static AttributeDefinition HEALTH;
public static AttributeDefinition HEALTH_BOOST;
public static AttributeDefinition MAIN_ATTACK;
public static AttributeDefinition MAIN_BOOST;
public static AttributeDefinition NEUTRAL_ATTACK;
public static AttributeDefinition NEUTRAL_BOOST;
public static AttributeDefinition CRITICAL_CHANCE;
public static AttributeDefinition CRITICAL_DAMAGE;
public static AttributeDefinition MAGIC_DAMAGE;
public static AttributeDefinition MAGIC_BOOST;
public static AttributeDefinition MAGIC_DEFENSE;
public static AttributeDefinition MAGIC_RESISTANCE;
public static AttributeDefinition MOVEMENT_SPEED;
public static AttributeDefinition KNOCKBACK_RES;
public CustomAttributes() {
HEALTH = AttributeController.registerAttribute(HEALTH_KEY, "Health", 'c', AttributeValueType.FLAT, AttributeDefinition.AttributeSection.BASE);
HEALTH_BOOST = AttributeController.registerAttribute(HEALTH_BOOST_KEY, "Health Boost", 'c', AttributeValueType.PERCENT, AttributeDefinition.AttributeSection.MODIFIER);
HEALTH_BOOST.setTranslationKey("attribute.name.generic.maxHealth");
MAIN_ATTACK = AttributeController.registerAttribute(MAIN_ATTACK_KEY, "Main Attack Damage", '4', AttributeValueType.FLAT, AttributeDefinition.AttributeSection.BASE);
MAIN_BOOST = AttributeController.registerAttribute(MAIN_ATTACK_BOOST_KEY, "Main Attack Damage", '4', AttributeValueType.PERCENT, AttributeDefinition.AttributeSection.MODIFIER);
NEUTRAL_ATTACK = AttributeController.registerAttribute(NEUTRAL_KEY, "Neutral Damage", '6', AttributeValueType.FLAT, AttributeDefinition.AttributeSection.BASE);
NEUTRAL_BOOST = AttributeController.registerAttribute(NEUTRAL_BOOST_KEY, "Neutral Damage", '6', AttributeValueType.PERCENT, AttributeDefinition.AttributeSection.MODIFIER);
CRITICAL_CHANCE = AttributeController.registerAttribute(CRITICAL_CHANCE_KEY, "Critical Chance", '6', AttributeValueType.PERCENT, AttributeDefinition.AttributeSection.INFO);
CRITICAL_DAMAGE = AttributeController.registerAttribute(CRITICAL_DAMAGE_KEY, "Critical Damage", '6', AttributeValueType.FLAT, AttributeDefinition.AttributeSection.INFO);
MAGIC_DAMAGE = AttributeController.registerAttribute(MAGIC_DAMAGE_KEY, "Magic Damage", '3', AttributeValueType.MAGIC, AttributeDefinition.AttributeSection.BASE);
MAGIC_BOOST = AttributeController.registerAttribute(MAGIC_BOOST_KEY, "Magic Damage", '3', AttributeValueType.MAGIC, AttributeDefinition.AttributeSection.MODIFIER);
MAGIC_DEFENSE = AttributeController.registerAttribute(MAGIC_DEFENSE_KEY, "Magic Defense", 'b', AttributeValueType.MAGIC, AttributeDefinition.AttributeSection.BASE);
MAGIC_RESISTANCE = AttributeController.registerAttribute(MAGIC_RESISTANCE_KEY, "Magic Resistance", 'b', AttributeValueType.MAGIC, AttributeDefinition.AttributeSection.MODIFIER);
MOVEMENT_SPEED = AttributeController.registerAttribute(MOVEMENT_SPEED_KEY, "Movement Speed", 'a', AttributeValueType.PERCENT, AttributeDefinition.AttributeSection.MODIFIER);
MOVEMENT_SPEED.setTranslationKey("attribute.name.generic.movementSpeed");
KNOCKBACK_RES = AttributeController.registerAttribute(KNOCKBACK_RES_KEY, "Knockback Resistance", 'b', AttributeValueType.PERCENT, AttributeDefinition.AttributeSection.MODIFIER);
KNOCKBACK_RES.setTranslationKey("attribute.name.generic.knockbackResistance");
RequirementCheckerRegistry.registerChecker(new QuestCompletedRequirement());
RequirementCheckerRegistry.registerChecker(new QuestActiveRequirement());
RequirementCheckerRegistry.registerChecker(new ProfileSlotRequirement());
RequirementCheckerRegistry.registerChecker(new SoulbindRequirement());
}
}
@@ -0,0 +1,12 @@
package kamkeel.npcs.addon;
/**
* TEMPORARY STUB -- auto-generated for compilation.
* Will be replaced when this class is properly migrated.
* DO NOT add logic here.
*
* Addon hook interface.
*/
public class AddonHooks {
public static void onAbilityCounter(Object ability, Object attacker, Object target) {}
}
@@ -0,0 +1,11 @@
package kamkeel.npcs.addon;
import kamkeel.npcs.util.ByteBufUtils;
/**
* TEMPORARY STUB -- auto-generated for compilation.
* Will be replaced when this class is properly migrated.
* DO NOT add logic here.
*/
public class DBCAddon {
public static boolean isLoaded() { return false; }
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,104 @@
package kamkeel.npcs.controllers;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import kamkeel.npcs.controllers.data.attribute.AttributeDefinition;
import kamkeel.npcs.controllers.data.attribute.AttributeValueType;
import kamkeel.npcs.controllers.data.attribute.tracker.PlayerAttributeTracker;
import kamkeel.npcs.CustomAttributes;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.entity.IEntityLiving;
import noppes.npcs.api.entity.IEntityLivingBase;
import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.api.handler.data.IAttributeDefinition;
import noppes.npcs.api.handler.data.IPlayerAttributes;
import noppes.npcs.api.handler.IAttributeHandler;
import noppes.npcs.api.IDamageSource;
import noppes.npcs.api.INbt;
import noppes.npcs.api.INbtList;
import noppes.npcs.api.item.IItemStack;
import noppes.npcs.api.IWorld;
/**
* AttributeController is the central registry for attribute definitions.
* It ensures that keys are unique.
*/
public class AttributeController implements IAttributeHandler {
public static AttributeController Instance;
public CustomAttributes attributes;
// Tracking Player Attributes
private static final Map<UUID, PlayerAttributeTracker> trackers = new HashMap<>();
private static final Map<String, AttributeDefinition> definitions = new HashMap<>();
public AttributeController() {
Instance = this;
definitions.clear();
trackers.clear();
attributes = new CustomAttributes();
}
public static AttributeDefinition registerAttribute(String key, String displayName, char colorCode, AttributeValueType valueType, AttributeDefinition.AttributeSection section) {
if (definitions.containsKey(key)) {
throw new IllegalArgumentException("Attribute already registered with key: " + key);
}
AttributeDefinition def = new AttributeDefinition(key, displayName, colorCode, valueType, section);
definitions.put(key, def);
return def;
}
public static AttributeDefinition registerAttribute(AttributeDefinition definition) {
if (definitions.containsKey(definition.getKey())) {
throw new IllegalArgumentException("Attribute already registered with key: " + definition.getKey());
} else {
definitions.put(definition.getKey(), definition);
return definition;
}
}
public static AttributeDefinition getAttribute(String key) {
return definitions.get(key);
}
public static Collection<AttributeDefinition> getAllAttributes() {
return definitions.values();
}
public static PlayerAttributeTracker getTracker(IPlayer player) {
return trackers.computeIfAbsent(player.getUniqueID(), id -> new PlayerAttributeTracker(id));
}
public static void removeTracker(UUID playerId) {
trackers.remove(playerId);
}
/**
* Update all trackers (e.g. called every 10 ticks for all online players).
*/
public static void updateAllTrackers(Iterable<IPlayer> players) {
for (IPlayer player : players) {
PlayerAttributeTracker tracker = getTracker(player);
tracker.updateIfChanged(player);
}
}
public IPlayerAttributes getPlayerAttributes(IPlayer player) {
if (player == null || player.getMCEntity() == null)
return null;
IPlayer IPlayer = (IPlayer) player.getMCEntity();
return trackers.computeIfAbsent(IPlayer.getUniqueID(), id -> new PlayerAttributeTracker(id));
}
public IAttributeDefinition getAttributeDefinition(String key) {
return definitions.get(key);
}
public IAttributeDefinition[] getAllAttributesArray() {
return definitions.values().toArray(new AttributeDefinition[definitions.size()]);
}
}
@@ -0,0 +1,230 @@
package kamkeel.npcs.controllers;
import noppes.npcs.constants.ClientOnly;
import kamkeel.npcs.util.IVector3;
import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler;
import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback;
import kamkeel.npcs.controllers.data.ability.gui.FieldDef;
import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider;
import noppes.npcs.entity.EntityNPCInterface;
import kamkeel.npcs.entity.EntityEnergyDome;
import kamkeel.npcs.entity.EntityEnergyBarrier;
import kamkeel.npcs.entity.EntityEnergyPanel;
import kamkeel.npcs.entity.EntityAbilityOrb;
import kamkeel.npcs.entity.EntityAbilityLaser;
import kamkeel.npcs.entity.EntityAbilityDisc;
import kamkeel.npcs.entity.EntityAbilityBeam;
import kamkeel.npcs.entity.EntityEnergyProjectile;
import kamkeel.npcs.entity.EntityAbilityZone;
import kamkeel.npcs.entity.EntityEnergySweeper;
import kamkeel.npcs.entity.EntityEnergyExplosion;
import kamkeel.npcs.util.ByteBufUtils;
import noppes.npcs.api.entity.IEntityLiving;
import noppes.npcs.api.IDamageSource;
import noppes.npcs.api.entity.IEntityLivingBase;
import noppes.npcs.api.item.IItemStack;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.api.INbtList;
import noppes.npcs.api.INbt;
import kamkeel.npcs.controllers.data.energy.IEnergyExtender;
import noppes.npcs.api.IEnergyHandler;
import noppes.npcs.api.IWorld;
import noppes.npcs.api.entity.*;
import noppes.npcs.api.entity.IEnergyExplosion;
import java.util.ArrayList;
import java.util.List;
/**
* Central controller for script-created energy entities and handler registration.
* Provides factory methods for creating all energy IEntity types and routes
* damage through registered handlers when entities have customDamageData
* but no sourceAbility.
*/
public class EnergyController implements IEnergyHandler {
public static EnergyController Instance = new EnergyController();
private final List<IEnergyExtender> extenders = new ArrayList<>();
// ═══════════════════════════════════════════════════════════════════
// HANDLER REGISTRATION
// ═══════════════════════════════════════════════════════════════════
public void registerExtender(IEnergyExtender handler) {
extenders.add(handler);
}
public List<IEnergyExtender> getExtenders() {
return extenders;
}
// ═══════════════════════════════════════════════════════════════════
// DAMAGE ROUTING
// ═══════════════════════════════════════════════════════════════════
/**
* Fire onEnergyDamage on all handlers. Chain of responsibility — first true wins.
*/
public boolean fireOnEnergyDamage(IEntity energyEntity, IEntityLivingBase owner,
IEntityLivingBase target, float damage,
float knockback, float knockbackUp,
double kbDirX, double kbDirZ,
float damageMultiplier,
INbt damageData) {
for (IEnergyExtender handler : extenders) {
if (handler.onEnergyDamage(energyEntity, owner, target, damage,
knockback, knockbackUp, kbDirX, kbDirZ, damageMultiplier, damageData)) {
return true;
}
}
return false;
}
/**
* Fire modifyEnergyDamage on all handlers. Cumulative — each handler's output feeds the next.
*/
public float fireModifyEnergyDamage(IEntity energyEntity, IEntityLivingBase owner,
float baseDamage, INbt damageData) {
float damage = baseDamage;
for (IEnergyExtender handler : extenders) {
damage = handler.modifyEnergyDamage(energyEntity, owner, damage, damageData);
}
return damage;
}
// ═══════════════════════════════════════════════════════════════════
// INTERNAL FACTORY METHODS (MC types)
// ═══════════════════════════════════════════════════════════════════
public EntityAbilityOrb createOrbInternal(IWorld IWorld, IEntity owner, double x, double y, double z, float size) {
EntityAbilityOrb IEntity = new EntityAbilityOrb(IWorld);
IEntity.setPosition(x, y, z);
IEntity.setStartPosition(x, y, z);
IEntity.setOwnerEntityId(owner.getEntityId());
IEntity.setProjectileSize(size);
return IEntity;
}
public EntityAbilityBeam createBeamInternal(IWorld IWorld, IEntity owner, double x, double y, double z,
float beamWidth, float headSize) {
EntityAbilityBeam IEntity = new EntityAbilityBeam(IWorld);
IEntity.setPosition(x, y, z);
IEntity.setStartPosition(x, y, z);
IEntity.setOwnerEntityId(owner.getEntityId());
IEntity.setBeamWidth(beamWidth);
IEntity.setHeadSize(headSize);
return IEntity;
}
public EntityAbilityDisc createDiscInternal(IWorld IWorld, IEntity owner, double x, double y, double z,
float radius, float thickness) {
EntityAbilityDisc IEntity = new EntityAbilityDisc(IWorld);
IEntity.setPosition(x, y, z);
IEntity.setStartPosition(x, y, z);
IEntity.setOwnerEntityId(owner.getEntityId());
IEntity.setDiscRadius(radius);
IEntity.setDiscThickness(thickness);
return IEntity;
}
public EntityAbilityLaser createLaserInternal(IWorld IWorld, IEntity owner, double x, double y, double z,
float laserWidth) {
EntityAbilityLaser IEntity = new EntityAbilityLaser(IWorld);
IEntity.setPosition(x, y, z);
IEntity.setStartPosition(x, y, z);
IEntity.setOwnerEntityId(owner.getEntityId());
IEntity.setLaserWidth(laserWidth);
return IEntity;
}
public EntityAbilityZone createHazardInternal(IWorld IWorld, IEntity owner, double x, double y, double z) {
EntityAbilityZone IEntity = new EntityAbilityZone(IWorld);
IEntity.initAsHazard(owner, x, y, z);
return IEntity;
}
public EntityAbilityZone createTrapInternal(IWorld IWorld, IEntity owner, double x, double y, double z) {
EntityAbilityZone IEntity = new EntityAbilityZone(IWorld);
IEntity.initAsTrap(owner, x, y, z);
return IEntity;
}
public EntityEnergySweeper createSweeperInternal(IWorld IWorld, IEntity owner, double x, double y, double z) {
EntityEnergySweeper IEntity = new EntityEnergySweeper(IWorld);
IEntity.setPosition(x, y, z);
IEntity.setOwnerEntityId(owner.getEntityId());
return IEntity;
}
public EntityEnergyPanel createPanelInternal(IWorld IWorld, IEntity owner, double x, double y, double z) {
EntityEnergyPanel IEntity = new EntityEnergyPanel(IWorld);
IEntity.setPosition(x, y, z);
IEntity.setOwnerEntityId(owner.getEntityId());
return IEntity;
}
public EntityEnergyExplosion createExplosionInternal(IWorld IWorld, IEntity owner, double x, double y, double z, float radius) {
return new EntityEnergyExplosion(IWorld, owner, x, y, z, radius);
}
// ═══════════════════════════════════════════════════════════════════
// IEnergyHandler (Script API methods)
// ═══════════════════════════════════════════════════════════════════
@Override
public IEnergyOrb createOrb(IWorld IWorld, IEntity owner, double x, double y, double z, float size) {
EntityAbilityOrb IEntity = createOrbInternal((IWorld) IWorld.getMCWorld(), owner.getMCEntity(), x, y, z, size);
return (IEnergyOrb) NpcAPI.Instance().getIEntity(IEntity);
}
@Override
public IEnergyBeam createBeam(IWorld IWorld, IEntity owner, double x, double y, double z, float beamWidth, float headSize) {
EntityAbilityBeam IEntity = createBeamInternal((IWorld) IWorld.getMCWorld(), owner.getMCEntity(), x, y, z, beamWidth, headSize);
return (IEnergyBeam) NpcAPI.Instance().getIEntity(IEntity);
}
@Override
public IEnergyDisc createDisc(IWorld IWorld, IEntity owner, double x, double y, double z, float radius, float thickness) {
EntityAbilityDisc IEntity = createDiscInternal((IWorld) IWorld.getMCWorld(), owner.getMCEntity(), x, y, z, radius, thickness);
return (IEnergyDisc) NpcAPI.Instance().getIEntity(IEntity);
}
@Override
public IEnergyLaser createLaser(IWorld IWorld, IEntity owner, double x, double y, double z, float laserWidth) {
EntityAbilityLaser IEntity = createLaserInternal((IWorld) IWorld.getMCWorld(), owner.getMCEntity(), x, y, z, laserWidth);
return (IEnergyLaser) NpcAPI.Instance().getIEntity(IEntity);
}
@Override
public IEnergyZone createHazard(IWorld IWorld, IEntity owner, double x, double y, double z) {
EntityAbilityZone IEntity = createHazardInternal((IWorld) IWorld.getMCWorld(), owner.getMCEntity(), x, y, z);
return (IEnergyZone) NpcAPI.Instance().getIEntity(IEntity);
}
@Override
public IEnergyZone createTrap(IWorld IWorld, IEntity owner, double x, double y, double z) {
EntityAbilityZone IEntity = createTrapInternal((IWorld) IWorld.getMCWorld(), owner.getMCEntity(), x, y, z);
return (IEnergyZone) NpcAPI.Instance().getIEntity(IEntity);
}
@Override
public IEnergySweeper createSweeper(IWorld IWorld, IEntity owner, double x, double y, double z) {
EntityEnergySweeper IEntity = createSweeperInternal((IWorld) IWorld.getMCWorld(), owner.getMCEntity(), x, y, z);
return (IEnergySweeper) NpcAPI.Instance().getIEntity(IEntity);
}
@Override
public IEnergyPanel createPanel(IWorld IWorld, IEntity owner, double x, double y, double z) {
EntityEnergyPanel IEntity = createPanelInternal((IWorld) IWorld.getMCWorld(), owner.getMCEntity(), x, y, z);
return (IEnergyPanel) NpcAPI.Instance().getIEntity(IEntity);
}
@Override
public IEnergyExplosion createExplosion(IWorld IWorld, IEntity owner, double x, double y, double z, float radius) {
EntityEnergyExplosion IEntity = createExplosionInternal((IWorld) IWorld.getMCWorld(), owner.getMCEntity(), x, y, z, radius);
return new ScriptEnergyExplosion<>(IEntity);
}
}
@@ -0,0 +1,854 @@
package kamkeel.npcs.controllers;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import kamkeel.npcs.controllers.data.profile.CNPCData;
import kamkeel.npcs.controllers.data.profile.EnumProfileOperation;
import kamkeel.npcs.controllers.data.profile.IProfileData;
import kamkeel.npcs.controllers.data.profile.Profile;
import kamkeel.npcs.controllers.data.profile.ProfileInfoEntry;
import kamkeel.npcs.controllers.data.profile.ProfileOperation;
import kamkeel.npcs.controllers.data.profile.Slot;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.entity.IEntityLiving;
import noppes.npcs.api.entity.IEntityLivingBase;
import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.api.handler.data.IProfile;
import noppes.npcs.api.handler.data.ISlot;
import noppes.npcs.api.handler.IPlayerData;
import noppes.npcs.api.handler.IProfileHandler;
import noppes.npcs.api.IDamageSource;
import noppes.npcs.api.INbt;
import noppes.npcs.api.INbtList;
import noppes.npcs.api.item.IItemStack;
import noppes.npcs.api.IWorld;
import noppes.npcs.config.ConfigMain;
import noppes.npcs.controllers.data.PlayerData;
import noppes.npcs.controllers.data.PlayerDataScript;
import noppes.npcs.controllers.data.PlayerQuestData;
import noppes.npcs.controllers.QuestController;
import noppes.npcs.controllers.ScriptController;
import noppes.npcs.CustomNpcs;
import noppes.npcs.CustomNpcsPermissions;
import noppes.npcs.EventHooks;
import noppes.npcs.LogWriter;
import noppes.npcs.NoppesUtilServer;
import noppes.npcs.util.CustomNPCsThreader;
import noppes.npcs.util.NBTJsonUtil;
public class ProfileController implements IProfileHandler {
// Message constants
private static final String MSG_PLAYER_NOT_FOUND = "Player not found.";
private static final String MSG_PROFILE_LOCKED_CLONE = "Profile is locked; cannot clone slot.";
private static final String MSG_SOURCE_SLOT_NOT_EXIST = "Source slot does not exist.";
private static final String MSG_CANNOT_CLONE_CURRENT = "Cannot clone to the current active slot.";
private static final String MSG_INVALID_DEST_SLOT = "Invalid destination slot id.";
private static final String MSG_CLONE_SUCCESS = "Slot cloned successfully.";
private static final String MSG_PROFILE_LOCKED_REMOVE = "Profile is locked; cannot remove slot.";
private static final String MSG_CANNOT_REMOVE_ACTIVE = "Cannot remove the currently active slot.";
private static final String MSG_SLOT_NOT_EXIST = "Slot does not exist.";
private static final String MSG_REMOVE_SUCCESS = "Slot removed successfully.";
private static final String MSG_PROFILE_LOCKED_CREATE = "Profile is locked; cannot create slot.";
private static final String MSG_MAX_SLOTS_REACHED = "Maximum allowed slots reached.";
private static final String MSG_NEW_SLOT_CREATED = "New slot created successfully.";
private static final String MSG_PROFILE_LOCKED_CHANGE = "Profile is locked; cannot change slot.";
private static final String MSG_SLOT_ALREADY_ACTIVE = "Slot is already active.";
private static final String MSG_REGION_NOT_ALLOWED = "Profile switching not allowed from your current location.";
private static final String MSG_CHANGE_SUCCESS = "Slot changed successfully.";
private static final String MSG_CANCELLED = "Operation cancelled.";
public static Map<String, IProfileData> profileTypes = new HashMap<>();
public static Map<UUID, Profile> activeProfiles = new HashMap<>();
public static String profile_directory = "profiles";
public static ProfileController Instance;
public ProfileController() {
Instance = this;
profileTypes = new HashMap<>();
activeProfiles = new HashMap<>();
}
public static boolean registerProfileType(IProfileData type) {
if (profileTypes.containsKey(type.getTagName()))
return false;
profileTypes.put(type.getTagName(), type);
return true;
}
public static File getProfileDir() {
try {
File file = new File(CustomNpcs.getWorldSaveDirectory(), profile_directory);
if (!file.exists())
file.mkdir();
return file;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
public static File getBackupDir() {
File base = getProfileDir();
File backup = new File(base, "backup");
if (!backup.exists()) {
backup.mkdir();
}
return backup;
}
public synchronized void login(IPlayer player) {
if (!ConfigMain.ProfilesEnabled)
return;
if (player == null)
return;
Profile profile;
if (activeProfiles.containsKey(player.getUniqueID())) {
profile = activeProfiles.get(player.getUniqueID());
profile.player = player;
} else {
INbt compound = load(player);
profile = new Profile(player, compound);
if (profile.getSlots().isEmpty()) {
Slot defaultSlot = new Slot(0, "Default Slot");
defaultSlot.setLastLoaded(System.currentTimeMillis());
profile.getSlots().put(0, defaultSlot);
profile.currentSlotId = 0;
saveSlotData(player);
}
if (!profile.getSlots().containsKey(profile.currentSlotId)) {
profile.currentSlotId = 0;
}
profile.player = player;
activeProfiles.put(player.getUniqueID(), profile);
PlayerData pdata = PlayerData.get(player);
if (profile.getSlots().containsKey(pdata.profileSlot)) {
profile.currentSlotId = pdata.profileSlot;
}
saveSlotData(player);
verifySlotQuests(profile.player);
}
}
public synchronized INbt load(IPlayer player) {
File saveDir = getProfileDir();
String filename = player.getUniqueID().toString() + ".dat";
try {
File file = new File(saveDir, filename);
if (file.exists()) {
return NBTJsonUtil.loadNBTData(file);
}
} catch (Exception e) {
LogWriter.error("Error loading profile file: " + filename, e);
}
return new INbt();
}
public synchronized INbt load(UUID uuid) {
File saveDir = getProfileDir();
String filename = uuid.toString() + ".dat";
try {
File file = new File(saveDir, filename);
if (file.exists()) {
return NBTJsonUtil.loadNBTData(file);
}
} catch (Exception e) {
LogWriter.error("Error loading profile file: " + filename, e);
}
return new INbt();
}
public synchronized void saveOffline(Profile profile, UUID uuid) {
profile.setLocked(true);
final INbt compound = profile.writeToNBT();
final String filename = uuid.toString() + ".dat";
CustomNPCsThreader.customNPCThread.execute(() -> {
try {
File saveDir = getProfileDir();
File fileNew = new File(saveDir, filename + "_new");
File fileOld = new File(saveDir, filename);
NBTIO.writeCompressed(compound, new FileOutputStream(fileNew));
if (fileOld.exists()) {
fileOld.delete();
}
fileNew.renameTo(fileOld);
backupProfile(uuid, compound);
} catch (Exception e) {
LogWriter.except(e);
} finally {
profile.setLocked(false);
}
});
}
public synchronized void save(IPlayer player, Profile profile) {
profile.setLocked(true);
final INbt compound = profile.writeToNBT();
final String filename = player.getUniqueID() + ".dat";
CustomNPCsThreader.customNPCThread.execute(() -> {
try {
File saveDir = getProfileDir();
File fileNew = new File(saveDir, filename + "_new");
File fileOld = new File(saveDir, filename);
NBTIO.writeCompressed(compound, new FileOutputStream(fileNew));
if (fileOld.exists()) {
fileOld.delete();
}
fileNew.renameTo(fileOld);
if (ConfigMain.AllowProfileBackups) {
backupProfile(player.getUniqueID(), compound);
}
} catch (Exception e) {
LogWriter.except(e);
} finally {
profile.setLocked(false);
}
});
}
private void backupProfile(UUID uuid, INbt compound) {
try {
File backupDir = new File(getBackupDir(), uuid.toString());
if (!backupDir.exists()) {
backupDir.mkdirs();
}
String dateStr = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
File backupFile = new File(backupDir, dateStr + ".dat");
NBTIO.writeCompressed(compound, new FileOutputStream(backupFile));
File[] backups = backupDir.listFiles((dir, name) -> name.endsWith(".dat"));
if (backups != null && backups.length > ConfigMain.ProfileBackupAmount) {
Arrays.sort(backups, Comparator.comparingLong(File::lastModified));
for (int i = 0; i < backups.length - ConfigMain.ProfileBackupAmount; i++) {
backups[i].delete();
}
}
} catch (Exception e) {
LogWriter.except(e);
}
}
public boolean rollbackProfile(String username, File backupFile) {
UUID uuid = getUUIDFromUsername(username);
if (uuid == null)
return false;
try (FileInputStream fis = new FileInputStream(backupFile)) {
INbt compound = NBTIO.readCompressed(fis);
File saveDir = getProfileDir();
File mainFile = new File(saveDir, uuid.toString() + ".dat");
File fileNew = new File(saveDir, uuid.toString() + "_new");
NBTIO.writeCompressed(compound, new FileOutputStream(fileNew));
if (mainFile.exists()) {
mainFile.delete();
}
fileNew.renameTo(mainFile);
IPlayer player = NoppesUtilServer.getPlayerByName(username);
Profile newProfile;
if (player != null) {
newProfile = new Profile(player, compound);
activeProfiles.put(uuid, newProfile);
loadSlotData(player);
return true;
}
} catch (Exception e) {
LogWriter.except(e);
}
return false;
}
public synchronized void logout(IPlayer player) {
if (player != null && activeProfiles.containsKey(player.getUniqueID())) {
saveSlotData(player);
Profile profile = activeProfiles.get(player.getUniqueID());
save(player, profile);
}
}
public Profile getProfile(IPlayer player) {
if (player == null)
return null;
if (!activeProfiles.containsKey(player.getUniqueID()))
login(player);
return activeProfiles.get(player.getUniqueID());
}
public Profile getProfile(UUID uuid) {
if (activeProfiles.containsKey(uuid))
return activeProfiles.get(uuid);
INbt compound = load(uuid);
return new Profile(null, compound);
}
public Profile getProfile(String username) {
IPlayer player = NoppesUtilServer.getPlayerByName(username);
if (player != null) {
return getProfile(player);
} else {
UUID uuid = getUUIDFromUsername(username);
if (uuid == null)
return null;
return getProfile(uuid);
}
}
public UUID getUUIDFromUsername(String username) {
try {
URL url = new URL("https://api.mojang.com/users/profiles/minecraft/" + username);
HttpURLConnection con = (HttpURLConnection) url.openConnection();
con.setRequestMethod("GET");
con.setConnectTimeout(5000);
con.setReadTimeout(5000);
BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream()));
StringBuilder temp = new StringBuilder();
String line;
while ((line = in.readLine()) != null) {
temp.append(line);
}
in.close();
con.disconnect();
String response = temp.toString();
if (response.contains("\"id\"")) {
int idIndex = response.indexOf("\"id\"");
int colonIndex = response.indexOf(":", idIndex);
int quoteStart = response.indexOf("\"", colonIndex);
int quoteEnd = response.indexOf("\"", quoteStart + 1);
String idString = response.substring(quoteStart + 1, quoteEnd);
idString = idString.replaceFirst(
"(\\p{XDigit}{8})(\\p{XDigit}{4})(\\p{XDigit}{4})(\\p{XDigit}{4})(\\p{XDigit}+)",
"$1-$2-$3-$4-$5"
);
return UUID.fromString(idString);
}
} catch (IOException e) {
LogWriter.error("Error retrieving UUID for " + username, e);
}
return null;
}
private int getNextAvailableTempSlot(Profile profile) {
int id = -1;
while (profile.getSlots().containsKey(id)) {
id--;
}
return id;
}
private ProfileOperation cloneSlotInternal(Profile profile, int sourceSlotId, int destinationSlotId, boolean temporary) {
if (profile.isLocked()) {
return ProfileOperation.locked(MSG_PROFILE_LOCKED_CLONE);
}
if (profile.player != null) {
List<IProfileData> dataList = new ArrayList<>(profileTypes.values());
dataList.sort(Comparator.comparingInt(IProfileData::getSwitchPriority));
for (IProfileData pd : dataList) {
if (pd.verifySwitch(profile.player).getResult() != EnumProfileOperation.SUCCESS) {
return pd.verifySwitch(profile.player);
}
}
}
if (!profile.getSlots().containsKey(sourceSlotId)) {
return ProfileOperation.error(MSG_SOURCE_SLOT_NOT_EXIST);
}
if (destinationSlotId == profile.getCurrentSlotId()) {
return ProfileOperation.error(MSG_CANNOT_CLONE_CURRENT);
}
if (temporary) {
destinationSlotId = getNextAvailableTempSlot(profile);
} else {
if (destinationSlotId <= 0) {
return ProfileOperation.error(MSG_INVALID_DEST_SLOT);
}
}
ISlot sourceSlot = profile.getSlots().get(sourceSlotId);
Map<String, INbt> newComponents = new HashMap<>();
for (String key : sourceSlot.getComponents().keySet()) {
newComponents.put(key, (INbt) sourceSlot.getComponentData(key).copy());
}
Slot clonedSlot = new Slot(destinationSlotId, "Cloned Slot " + destinationSlotId, System.currentTimeMillis(), temporary, newComponents);
profile.getSlots().put(destinationSlotId, clonedSlot);
return ProfileOperation.success(MSG_CLONE_SUCCESS);
}
private ProfileOperation removeSlotInternal(Profile profile, int slotId) {
if (profile.isLocked()) {
return ProfileOperation.locked(MSG_PROFILE_LOCKED_REMOVE);
}
if (slotId == profile.getCurrentSlotId()) {
return ProfileOperation.error(MSG_CANNOT_REMOVE_ACTIVE);
}
if (!profile.getSlots().containsKey(slotId)) {
return ProfileOperation.error(MSG_SLOT_NOT_EXIST);
}
if (profile.player != null) {
PlayerDataScript handler = ScriptController.Instance.getPlayerScripts(profile.player);
IPlayer scriptPlayer = (IPlayer) NpcAPI.Instance().getIEntity(profile.player);
if (EventHooks.onProfileRemove(handler, scriptPlayer, profile, slotId, false))
return ProfileOperation.error(MSG_CANCELLED);
}
profile.getSlots().remove(slotId);
if (profile.player != null) {
PlayerDataScript handler = ScriptController.Instance.getPlayerScripts(profile.player);
IPlayer scriptPlayer = (IPlayer) NpcAPI.Instance().getIEntity(profile.player);
EventHooks.onProfileRemove(handler, scriptPlayer, profile, slotId, true);
}
return ProfileOperation.success(MSG_REMOVE_SUCCESS);
}
public ProfileOperation createSlotInternal(Profile profile) {
if (profile.isLocked()) {
return ProfileOperation.locked(MSG_PROFILE_LOCKED_CREATE);
}
int newSlotId = 0;
while (profile.getSlots().containsKey(newSlotId)) {
newSlotId++;
}
if (!allowSlotPermission(profile.player)) {
return ProfileOperation.error(MSG_MAX_SLOTS_REACHED);
}
if (profile.player != null) {
List<IProfileData> dataList = new ArrayList<>(profileTypes.values());
dataList.sort(Comparator.comparingInt(IProfileData::getSwitchPriority));
for (IProfileData pd : dataList) {
if (pd.verifySwitch(profile.player).getResult() != EnumProfileOperation.SUCCESS) {
return pd.verifySwitch(profile.player);
}
}
}
if (profile.player != null) {
PlayerDataScript handler = ScriptController.Instance.getPlayerScripts(profile.player);
IPlayer scriptPlayer = (IPlayer) NpcAPI.Instance().getIEntity(profile.player);
if (EventHooks.onProfileCreate(handler, scriptPlayer, profile, newSlotId, false))
return ProfileOperation.error(MSG_CANCELLED);
}
Slot newSlot = new Slot(newSlotId, "Slot " + newSlotId);
newSlot.setLastLoaded(System.currentTimeMillis());
profile.getSlots().put(newSlotId, newSlot);
if (profile.player != null) {
PlayerDataScript handler = ScriptController.Instance.getPlayerScripts(profile.player);
verifySlotQuests(profile.player);
save(profile.player, profile);
if (profile.player instanceof IPlayer) {
ProfileSharedQuestPacket.sendToPlayer((IPlayer) profile.player);
}
IPlayer scriptPlayer = (IPlayer) NpcAPI.Instance().getIEntity(profile.player);
EventHooks.onProfileCreate(handler, scriptPlayer, profile, newSlotId, true);
}
return ProfileOperation.success(MSG_NEW_SLOT_CREATED);
}
private ProfileOperation changeSlotInternal(Profile profile, int newSlotId) {
if (profile.isLocked()) {
return ProfileOperation.locked(MSG_PROFILE_LOCKED_CHANGE);
}
if (profile.getCurrentSlotId() == newSlotId) {
return ProfileOperation.error(MSG_SLOT_ALREADY_ACTIVE);
}
if (!profile.getSlots().containsKey(newSlotId)) {
return ProfileOperation.error(MSG_SLOT_NOT_EXIST);
}
if (profile.player != null) {
if (ConfigMain.RegionProfileSwitching) {
boolean allowed = hasCustomPermission(profile.player, PROFILE_REGION_BYPASS.name);
if (!allowed) {
int playerDim = profile.player.dimension;
int playerX = (int) profile.player.posX;
int playerY = (int) profile.player.posY;
int playerZ = (int) profile.player.posZ;
for (List<Integer> region : ConfigMain.RestrictedProfileRegions) {
if (region.size() == 7) {
int dim = region.get(0);
int x1 = Math.min(region.get(1), region.get(4));
int y1 = Math.min(region.get(2), region.get(5));
int z1 = Math.min(region.get(3), region.get(6));
int x2 = Math.max(region.get(1), region.get(4));
int y2 = Math.max(region.get(2), region.get(5));
int z2 = Math.max(region.get(3), region.get(6));
if (playerDim == dim &&
playerX >= x1 && playerX <= x2 &&
playerY >= y1 && playerY <= y2 &&
playerZ >= z1 && playerZ <= z2) {
allowed = true;
break;
}
}
}
}
if (!allowed) {
return ProfileOperation.error(MSG_REGION_NOT_ALLOWED);
}
}
List<IProfileData> dataList = new ArrayList<>(profileTypes.values());
dataList.sort(Comparator.comparingInt(IProfileData::getSwitchPriority));
for (IProfileData pd : dataList) {
if (pd.verifySwitch(profile.player).getResult() != EnumProfileOperation.SUCCESS) {
return pd.verifySwitch(profile.player);
}
}
int prevSlot = profile.getCurrentSlotId();
PlayerDataScript handler = ScriptController.Instance.getPlayerScripts(profile.player);
IPlayer scriptPlayer = (IPlayer) NpcAPI.Instance().getIEntity(profile.player);
if (EventHooks.onProfileChange(handler, scriptPlayer, profile, newSlotId, prevSlot, false))
return ProfileOperation.error(MSG_CANCELLED);
// Persist the outgoing slot using its original id. Previously we relied on
// profile.currentSlotId inside saveSlotData, but scripts firing during the
// transition could change the active slot before the save completed and
// overwrite the destination slot with stale data.
saveSlotData(profile.player, profile, prevSlot);
loadSlotData(profile.player, profile, newSlotId);
syncSharedQuestsToPlayer(profile.player);
if (profile.player instanceof IPlayer) {
ProfileSharedQuestPacket.sendToPlayer((IPlayer) profile.player);
}
EventHooks.onProfileChange(handler, scriptPlayer, profile, newSlotId, prevSlot, true);
} else {
return ProfileOperation.error(MSG_PLAYER_NOT_FOUND);
}
return ProfileOperation.success(MSG_CHANGE_SUCCESS);
}
public ProfileOperation cloneSlot(IPlayer player, int sourceSlotId, int destinationSlotId, boolean temporary) {
Profile profile = getProfile(player);
if (profile == null)
return ProfileOperation.error(MSG_PLAYER_NOT_FOUND);
ProfileOperation result = cloneSlotInternal(profile, sourceSlotId, destinationSlotId, temporary);
if (result.getResult() == EnumProfileOperation.SUCCESS && player != null) {
save(player, profile);
}
return result;
}
public ProfileOperation cloneSlot(UUID uuid, int sourceSlotId, int destinationSlotId, boolean temporary) {
Profile profile = getProfile(uuid);
if (profile == null)
return ProfileOperation.error(MSG_PLAYER_NOT_FOUND);
ProfileOperation result = cloneSlotInternal(profile, sourceSlotId, destinationSlotId, temporary);
if (result.getResult() == EnumProfileOperation.SUCCESS) {
if (profile.player != null)
save(profile.player, profile);
else
saveOffline(profile, uuid);
}
return result;
}
public ProfileOperation cloneSlot(String username, int sourceSlotId, int destinationSlotId, boolean temporary) {
Profile profile = getProfile(username);
if (profile == null)
return ProfileOperation.error(MSG_PLAYER_NOT_FOUND);
ProfileOperation result = cloneSlotInternal(profile, sourceSlotId, destinationSlotId, temporary);
if (result.getResult() == EnumProfileOperation.SUCCESS) {
if (profile.player != null)
save(profile.player, profile);
else {
UUID uuid = getUUIDFromUsername(username);
if (uuid != null)
saveOffline(profile, uuid);
}
}
return result;
}
public ProfileOperation removeSlot(IPlayer player, int slotId) {
Profile profile = getProfile(player);
if (profile == null)
return ProfileOperation.error(MSG_PLAYER_NOT_FOUND);
ProfileOperation result = removeSlotInternal(profile, slotId);
if (result.getResult() == EnumProfileOperation.SUCCESS && player != null) {
save(player, profile);
}
return result;
}
public ProfileOperation removeSlot(String username, int slotId) {
Profile profile = getProfile(username);
if (profile == null)
return ProfileOperation.error(MSG_PLAYER_NOT_FOUND);
ProfileOperation result = removeSlotInternal(profile, slotId);
if (result.getResult() == EnumProfileOperation.SUCCESS) {
if (profile.player != null)
save(profile.player, profile);
else {
UUID uuid = getUUIDFromUsername(username);
if (uuid != null)
saveOffline(profile, uuid);
}
}
return result;
}
public ProfileOperation changeSlot(IPlayer player, int newSlotId) {
Profile profile = getProfile(player);
if (profile == null)
return ProfileOperation.error(MSG_PLAYER_NOT_FOUND);
ProfileOperation result = changeSlotInternal(profile, newSlotId);
if (result.getResult() == EnumProfileOperation.SUCCESS && player != null)
save(player, profile);
return result;
}
public ProfileOperation changeSlot(String username, int newSlotId) {
Profile profile = getProfile(username);
if (profile == null)
return ProfileOperation.error(MSG_PLAYER_NOT_FOUND);
ProfileOperation result = changeSlotInternal(profile, newSlotId);
if (result.getResult() == EnumProfileOperation.SUCCESS) {
if (profile.player != null)
save(profile.player, profile);
else {
UUID uuid = getUUIDFromUsername(username);
if (uuid != null)
saveOffline(profile, uuid);
}
}
return result;
}
private void saveSlotData(IPlayer player, Profile profile, int slotId) {
if (player == null || profile == null)
return;
if (profile.isLocked()) {
return;
}
ISlot slot = profile.getSlots().get(slotId);
if (slot == null) {
slot = new Slot(slotId, "Slot " + slotId);
profile.getSlots().put(slotId, slot);
}
for (IProfileData profileData : profileTypes.values()) {
INbt cloned = (INbt) profileData.getCurrentNBT(player).copy();
slot.setComponentData(profileData.getTagName(), cloned);
}
slot.setLastLoaded(System.currentTimeMillis());
}
public void saveSlotData(IPlayer player) {
if (player == null || !activeProfiles.containsKey(player.getUniqueID()))
return;
Profile profile = activeProfiles.get(player.getUniqueID());
saveSlotData(player, profile, profile.getCurrentSlotId());
}
private void loadSlotData(IPlayer player, Profile profile, int slotId) {
if (player == null || profile == null)
return;
ISlot slot = profile.getSlots().get(slotId);
if (slot == null)
return;
for (IProfileData profileData : profileTypes.values()) {
INbt data;
if (slot.getComponents().containsKey(profileData.getTagName()))
data = (INbt) slot.getComponentData(profileData.getTagName()).copy();
else
data = new INbt();
profileData.setNBT(player, data);
}
for (IProfileData profileData : profileTypes.values()) {
profileData.save(player);
}
// Only flip the active slot after the player's live data matches the
// requested slot. This way, any additional saveSlotData() calls triggered by
// scripts during the load can't persist stale information to the new slot.
profile.currentSlotId = slotId;
PlayerData pdata = PlayerData.get(player);
pdata.profileSlot = slotId;
pdata.save();
if (ConfigMain.AttributesEnabled)
AttributeController.getTracker(player).recalcAttributes(player);
}
public void loadSlotData(IPlayer player) {
if (player == null || !activeProfiles.containsKey(player.getUniqueID()))
return;
Profile profile = activeProfiles.get(player.getUniqueID());
// Delay flipping profile.currentSlotId until after the data for the target slot
// has been restored to the player so other saves observe consistent state.
loadSlotData(player, profile, profile.getCurrentSlotId());
}
public List<ProfileInfoEntry> getProfileInfo(IPlayer player, int slotId) {
List<ProfileInfoEntry> infoList = new ArrayList<>();
Profile profile = getProfile(player);
if (profile == null)
return infoList;
List<IProfileData> dataList = new ArrayList<>(profileTypes.values());
dataList.sort(Comparator.comparingInt(IProfileData::getSwitchPriority));
if (slotId == profile.getCurrentSlotId()) {
for (IProfileData pd : dataList) {
INbt currentNBT = pd.getCurrentNBT(player);
List<ProfileInfoEntry> subInfo = pd.getInfo(player, currentNBT);
infoList.addAll(subInfo);
}
} else {
if (!profile.getSlots().containsKey(slotId))
return infoList;
ISlot slot = profile.getSlots().get(slotId);
for (IProfileData pd : dataList) {
if (slot.getComponents().containsKey(pd.getTagName())) {
INbt sub = (INbt) slot.getComponentData(pd.getTagName()).copy();
List<ProfileInfoEntry> subInfo = pd.getInfo(player, sub);
infoList.addAll(subInfo);
}
}
}
return infoList;
}
public boolean allowSlotPermission(IPlayer player) {
Profile profile = getProfile(player);
int currentSlots = profile.getSlots().size();
if (CustomNpcsPermissions.hasCustomPermission(player, PROFILE_MAX.name)) {
return true;
}
int highestAllowed = 0;
for (int i = 1; i <= 50; i++) {
String perm = "customnpcs.profile.max." + i;
if (CustomNpcsPermissions.hasCustomPermission(player, perm)) {
highestAllowed = i;
}
}
if (highestAllowed == 0 || highestAllowed < ConfigMain.DefaultProfileSlots) {
highestAllowed = ConfigMain.DefaultProfileSlots;
}
return currentSlots < highestAllowed;
}
public IPlayerData getSlotPlayerData(IPlayer player, int slotID) {
PlayerData playerData;
Profile profile = getProfile(player);
if (profile == null)
return null;
if (profile.currentSlotId == slotID) {
playerData = PlayerData.get(player);
} else {
Slot slot = (Slot) profile.getSlots().get(slotID);
playerData = getSlotPlayerData(player, slot);
}
return playerData;
}
public PlayerData getSlotPlayerData(IPlayer player, Slot slot) {
if (slot == null)
return null;
PlayerData playerData = new PlayerData();
playerData.player = player;
INbt compound = slot.getComponentData(new CNPCData().getTagName());
if (compound == null)
compound = new INbt();
else
compound = (INbt) compound.copy();
playerData.setNBT(compound);
return playerData;
}
public void verifySlotQuests(IPlayer player) {
Profile profile = getProfile(player);
if (profile == null) {
return;
}
Map<Integer, Long> universalFinished = new HashMap<>();
for (Integer questId : QuestController.Instance.sharedQuests.keySet()) {
long maxTime = 0;
for (ISlot slot : profile.getSlots().values()) {
IPlayerData data = getSlotPlayerData(player, slot.getId());
if (data == null) {
continue;
}
PlayerQuestData questData = (PlayerQuestData) data.getQuestData();
Long t = questData.finishedQuests.get(questId);
if (t != null && t > maxTime) {
maxTime = t;
}
}
if (maxTime > 0) {
universalFinished.put(questId, maxTime);
}
}
// Update the profile cache
profile.sharedQuestTimestamps = universalFinished;
// Push the universal shared completion times into every slot.
for (ISlot slot : profile.getSlots().values()) {
IPlayerData data = getSlotPlayerData(player, slot.getId());
if (data != null) {
PlayerData playerData = (PlayerData) data;
PlayerQuestData questData = (PlayerQuestData) data.getQuestData();
questData.finishedQuests.putAll(universalFinished);
slot.setComponentData(new CNPCData().getTagName(), playerData.getNBT());
}
}
}
public void syncSharedQuestsToPlayer(IPlayer player) {
Profile profile = getProfile(player);
if (profile == null || profile.sharedQuestTimestamps.isEmpty()) {
return;
}
PlayerQuestData questData = PlayerData.get(player).questData;
for (Map.Entry<Integer, Long> entry : profile.sharedQuestTimestamps.entrySet()) {
Long existing = questData.finishedQuests.get(entry.getKey());
if (existing == null || entry.getValue() > existing) {
questData.finishedQuests.put(entry.getKey(), entry.getValue());
}
}
}
public void shareQuestCompletion(IPlayer player, int questId, long completeTime) {
Profile profile = getProfile(player);
if (profile == null) {
return;
}
Long existing = profile.sharedQuestTimestamps.get(questId);
if (existing == null || completeTime > existing) {
profile.sharedQuestTimestamps.put(questId, completeTime);
}
save(player, profile);
if (player instanceof IPlayer) {
ProfileSharedQuestPacket.sendToPlayer((IPlayer) player);
}
}
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,400 @@
package kamkeel.npcs.controllers;
import java.io.File;
import java.util.concurrent.ConcurrentHashMap;
import java.util.HashMap;
import kamkeel.npcs.controllers.data.telegraph.Telegraph;
import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance;
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.entity.IEntityLiving;
import noppes.npcs.api.entity.IEntityLivingBase;
import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.api.handler.ITelegraphHandler;
import noppes.npcs.api.IDamageSource;
import noppes.npcs.api.INbt;
import noppes.npcs.api.INbtList;
import noppes.npcs.api.ITelegraph;
import noppes.npcs.api.ITelegraphInstance;
import noppes.npcs.api.item.IItemStack;
import noppes.npcs.api.IWorld;
import noppes.npcs.CustomNpcs;
import noppes.npcs.LogWriter;
import noppes.npcs.util.NBTJsonUtil;
/**
* Server-side controller for managing telegraphs.
* Provides factory methods for creating telegraphs and spawn methods for sending to clients.
* Also manages saved telegraph presets.
*/
public class TelegraphController implements ITelegraphHandler {
public static TelegraphController Instance;
// Saved telegraph presets (name -> Telegraph)
private HashMap<String, Telegraph> savedTelegraphs = new HashMap<>();
// Active server-side telegraph instances (for removal tracking)
private ConcurrentHashMap<String, TelegraphInstance> activeInstances = new ConcurrentHashMap<>();
public TelegraphController() {
}
public static void init() {
Instance = new TelegraphController();
Instance.load();
}
// ═══════════════════════════════════════════════════════════════════════════
// INTERNAL SPAWN METHODS
// ═══════════════════════════════════════════════════════════════════════════
/**
* Spawn a telegraph at a fixed position.
*
* @param telegraph The telegraph IConfiguration
* @param IWorld The IWorld
* @param x X position
* @param y Y position
* @param z Z position
* @param yaw Rotation yaw (for directional telegraphs like LINE/CONE)
* @return The spawned telegraph instance
*/
public TelegraphInstance spawn(Telegraph telegraph, IWorld IWorld, double x, double y, double z, float yaw) {
TelegraphInstance instance = new TelegraphInstance(telegraph, x, y, z, yaw);
instance.setWorld(IWorld);
activeInstances.put(instance.getInstanceId(), instance);
TelegraphSpawnPacket.sendToDimension(instance, IWorld.provider.dimensionId);
return instance;
}
/**
* Spawn a telegraph at a position with default yaw.
*/
public TelegraphInstance spawn(Telegraph telegraph, IWorld IWorld, double x, double y, double z) {
return spawn(telegraph, IWorld, x, y, z, 0);
}
/**
* Spawn a telegraph that follows an IEntity.
*
* @param telegraph The telegraph IConfiguration
* @param IEntity The IEntity to follow
* @return The spawned telegraph instance
*/
public TelegraphInstance spawn(Telegraph telegraph, IEntity IEntity) {
return spawn(telegraph, IEntity, 0);
}
/**
* Spawn a telegraph that follows an IEntity with yaw.
*/
public TelegraphInstance spawn(Telegraph telegraph, IEntity IEntity, float yaw) {
TelegraphInstance instance = new TelegraphInstance(telegraph, IEntity.posX, IEntity.posY, IEntity.posZ, yaw);
instance.setWorld(IEntity.worldObj);
instance.setEntityIdToFollow(IEntity.getEntityId());
activeInstances.put(instance.getInstanceId(), instance);
TelegraphSpawnPacket.sendToTracking(instance, IEntity);
return instance;
}
/**
* Spawn a telegraph to a specific player only.
*/
public TelegraphInstance spawnToPlayer(Telegraph telegraph, IPlayer player, double x, double y, double z, float yaw) {
TelegraphInstance instance = new TelegraphInstance(telegraph, x, y, z, yaw);
instance.setWorld(player.worldObj);
activeInstances.put(instance.getInstanceId(), instance);
TelegraphSpawnPacket.send(instance, player);
return instance;
}
/**
* Spawn a telegraph to all players on the server.
*/
public TelegraphInstance spawnToAll(Telegraph telegraph, IWorld IWorld, double x, double y, double z, float yaw) {
TelegraphInstance instance = new TelegraphInstance(telegraph, x, y, z, yaw);
instance.setWorld(IWorld);
activeInstances.put(instance.getInstanceId(), instance);
TelegraphSpawnPacket.sendToAll(instance);
return instance;
}
/**
* Spawn a telegraph to all players tracking an IEntity.
*/
public TelegraphInstance spawnToTracking(Telegraph telegraph, IEntity IEntity, double x, double y, double z, float yaw) {
TelegraphInstance instance = new TelegraphInstance(telegraph, x, y, z, yaw);
instance.setWorld(IEntity.worldObj);
activeInstances.put(instance.getInstanceId(), instance);
TelegraphSpawnPacket.sendToTracking(instance, IEntity);
return instance;
}
// ═══════════════════════════════════════════════════════════════════════════
// REMOVAL METHODS
// ═══════════════════════════════════════════════════════════════════════════
/**
* Remove a telegraph by instance ID.
*/
public void remove(String instanceId) {
TelegraphInstance instance = activeInstances.remove(instanceId);
if (instance != null && instance.getWorld() != null) {
TelegraphRemovePacket.sendToDimension(instanceId, instance.getDimensionId());
}
}
/**
* Remove a telegraph instance.
*/
public void remove(TelegraphInstance instance) {
if (instance != null) {
remove(instance.getInstanceId());
}
}
/**
* Remove a telegraph for a specific player only.
*/
public void removeFromPlayer(String instanceId, IPlayer player) {
activeInstances.remove(instanceId);
TelegraphRemovePacket.send(instanceId, player);
}
/**
* Get an active telegraph instance by ID.
*/
public TelegraphInstance getActiveInstance(String instanceId) {
return activeInstances.get(instanceId);
}
// ═══════════════════════════════════════════════════════════════════════════
// PRESET MANAGEMENT
// ═══════════════════════════════════════════════════════════════════════════
/**
* Save a telegraph preset (internal use).
*
* @param name The name for the preset
* @param telegraph The telegraph IConfiguration to save
*/
public void saveInternal(String name, Telegraph telegraph) {
if (name == null || name.isEmpty() || telegraph == null) return;
telegraph.setId(name);
savedTelegraphs.put(name, telegraph);
saveTelegraphFile(name, telegraph);
}
/**
* Get a saved telegraph preset by name (internal use).
*
* @param name The preset name
* @return The telegraph IConfiguration, or null if not found
*/
public Telegraph getInternal(String name) {
return savedTelegraphs.get(name);
}
/**
* Delete a saved telegraph preset.
*
* @param name The preset name
*/
public void delete(String name) {
Telegraph removed = savedTelegraphs.remove(name);
if (removed != null) {
File file = new File(getDir(), name + ".json");
if (file.exists()) {
file.delete();
}
}
}
/**
* Check if a preset exists.
*/
public boolean has(String name) {
return savedTelegraphs.containsKey(name);
}
/**
* Get all saved preset names.
*/
public String[] getSavedNames() {
return savedTelegraphs.keySet().toArray(new String[0]);
}
// ═══════════════════════════════════════════════════════════════════════════
// PERSISTENCE
// ═══════════════════════════════════════════════════════════════════════════
private File getDir() {
return new File(CustomNpcs.getWorldSaveDirectory(), "telegraphs");
}
public void load() {
savedTelegraphs.clear();
LogWriter.info("Loading telegraph presets...");
File dir = getDir();
if (!dir.exists()) {
dir.mkdirs();
return;
}
File[] files = dir.listFiles();
if (files == null) return;
for (File file : files) {
if (!file.isFile() || !file.getName().endsWith(".json")) continue;
try {
Telegraph telegraph = new Telegraph();
telegraph.readNBT(new NBTWrapper(NBTJsonUtil.LoadFile(file)));
String name = file.getName().substring(0, file.getName().length() - 5);
telegraph.setId(name);
savedTelegraphs.put(name, telegraph);
} catch (Exception e) {
LogWriter.error("Error loading telegraph preset: " + file.getName(), e);
}
}
LogWriter.info("Loaded " + savedTelegraphs.size() + " telegraph presets.");
}
private void saveTelegraphFile(String name, Telegraph telegraph) {
try {
File dir = getDir();
if (!dir.exists()) {
dir.mkdirs();
}
File file = new File(dir, name + ".json_new");
File file2 = new File(dir, name + ".json");
NBTJsonUtil.SaveFile(file, ((NBTWrapper) telegraph.writeNBT()).getMCTag());
if (file2.exists()) {
file2.delete();
}
file.renameTo(file2);
} catch (Exception e) {
LogWriter.error("Error saving telegraph preset: " + name, e);
}
}
// ═══════════════════════════════════════════════════════════════════════════
// ITelegraphHandler INTERFACE IMPLEMENTATION
// ═══════════════════════════════════════════════════════════════════════════
@Override
public ITelegraph createCircle(float radius) {
return new ScriptTelegraph(Telegraph.circle(radius));
}
@Override
public ITelegraph createRing(float outerRadius, float innerRadius) {
return new ScriptTelegraph(Telegraph.ring(outerRadius, innerRadius));
}
@Override
public ITelegraph createLine(float length, float width) {
return new ScriptTelegraph(Telegraph.line(length, width));
}
@Override
public ITelegraph createCone(float length, float angle) {
return new ScriptTelegraph(Telegraph.cone(length, angle));
}
@Override
public ITelegraph createSquare(float radius) {
return new ScriptTelegraph(Telegraph.square(radius));
}
@Override
public ITelegraph createPoint() {
return new ScriptTelegraph(Telegraph.point());
}
@Override
public ITelegraph create(String type) {
if (type == null) return null;
try {
TelegraphType telegraphType = TelegraphType.valueOf(type.toUpperCase());
Telegraph telegraph;
switch (telegraphType) {
case CIRCLE:
telegraph = Telegraph.circle(3.0f);
break;
case RING:
telegraph = Telegraph.ring(5.0f, 2.0f);
break;
case LINE:
telegraph = Telegraph.line(5.0f, 2.0f);
break;
case CONE:
telegraph = Telegraph.cone(5.0f, 45.0f);
break;
case SQUARE:
telegraph = Telegraph.square(3.0f);
break;
case POINT:
telegraph = Telegraph.point();
break;
default:
telegraph = Telegraph.circle(3.0f);
}
return new ScriptTelegraph(telegraph);
} catch (IllegalArgumentException e) {
return null;
}
}
@Override
public ITelegraphInstance spawn(ITelegraph telegraph, IWorld IWorld, double x, double y, double z) {
return spawn(telegraph, IWorld, x, y, z, 0);
}
@Override
public ITelegraphInstance spawn(ITelegraph telegraph, IWorld IWorld, double x, double y, double z, float yaw) {
if (telegraph == null || IWorld == null) return null;
Telegraph internal = ((ScriptTelegraph) telegraph).getMCTelegraph();
TelegraphInstance instance = spawn(internal, (IWorld) IWorld.getMCWorld(), x, y, z, yaw);
return new ScriptTelegraphInstance(instance);
}
@Override
public ITelegraphInstance spawn(ITelegraph telegraph, IEntity IEntity) {
return spawn(telegraph, IEntity, 0);
}
@Override
public ITelegraphInstance spawn(ITelegraph telegraph, IEntity IEntity, float yaw) {
if (telegraph == null || IEntity == null) return null;
Telegraph internal = ((ScriptTelegraph) telegraph).getMCTelegraph();
TelegraphInstance instance = spawn(internal, (IEntity) IEntity.getMCEntity(), yaw);
return new ScriptTelegraphInstance(instance);
}
@Override
public ITelegraph get(String name) {
Telegraph telegraph = savedTelegraphs.get(name);
return telegraph != null ? new ScriptTelegraph(new Telegraph(telegraph)) : null;
}
@Override
public void save(String name, ITelegraph telegraph) {
if (name == null || name.isEmpty() || telegraph == null) return;
Telegraph internal = ((ScriptTelegraph) telegraph).getMCTelegraph();
saveInternal(name, internal);
}
@Override
public void remove(ITelegraphInstance instance) {
if (instance != null) {
remove(instance.getInstanceId());
}
}
}
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,330 @@
package kamkeel.npcs.controllers.data.ability;
import noppes.npcs.api.entity.IEntityLiving;
import noppes.npcs.api.IDamageSource;
import noppes.npcs.api.IWorld;
import noppes.npcs.api.entity.IEntityLivingBase;
import noppes.npcs.api.item.IItemStack;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.api.INbtList;
import noppes.npcs.api.INbt;
import kamkeel.npcs.controllers.AbilityController;
import kamkeel.npcs.controllers.data.ability.data.ChainedAbility;
import kamkeel.npcs.controllers.data.ability.data.IAbilityAction;
import java.util.UUID;
/**
* Unified slot for storing any combat action: inline ability, ability reference, or chain reference.
* Support for all {@link IAbilityAction} types.
* <p>
* References are lazy-resolved via their respective controllers and cached until
* the controller revision changes.
*/
public class AbilityAction {
public enum SlotType {
INLINE_ABILITY,
ABILITY_REFERENCE,
CHAIN_REFERENCE,
INLINE_CHAIN
}
private SlotType slotType;
private Ability inlineAbility;
private ChainedAbility inlineChain;
private String referenceId;
/**
* Per-slot enabled override for reference slots (null = use master's default).
*/
private Boolean enabledOverride;
// Cache
private transient IAbilityAction cachedAction;
private transient int cachedRevision = -1;
private AbilityAction() {
}
// ═══════════════════════════════════════════════════════════════════
// FACTORY METHODS
// ═══════════════════════════════════════════════════════════════════
public static AbilityAction inline(Ability ability) {
AbilityAction slot = new AbilityAction();
slot.slotType = SlotType.INLINE_ABILITY;
slot.inlineAbility = ability;
return slot;
}
public static AbilityAction abilityReference(String referenceId) {
AbilityAction slot = new AbilityAction();
slot.slotType = SlotType.ABILITY_REFERENCE;
slot.referenceId = referenceId;
return slot;
}
public static AbilityAction chainReference(String referenceId) {
AbilityAction slot = new AbilityAction();
slot.slotType = SlotType.CHAIN_REFERENCE;
slot.referenceId = referenceId;
return slot;
}
public static AbilityAction inlineChain(ChainedAbility chain) {
AbilityAction slot = new AbilityAction();
slot.slotType = SlotType.INLINE_CHAIN;
slot.inlineChain = chain;
return slot;
}
// ═══════════════════════════════════════════════════════════════════
// QUERIES
// ═══════════════════════════════════════════════════════════════════
public SlotType getSlotType() {
return slotType;
}
public boolean isReference() {
return slotType == SlotType.ABILITY_REFERENCE || slotType == SlotType.CHAIN_REFERENCE;
}
public boolean isChainReference() {
return slotType == SlotType.CHAIN_REFERENCE;
}
public boolean isInlineChain() {
return slotType == SlotType.INLINE_CHAIN;
}
public boolean isChain() {
return slotType == SlotType.CHAIN_REFERENCE || slotType == SlotType.INLINE_CHAIN;
}
public boolean isAbilityReference() {
return slotType == SlotType.ABILITY_REFERENCE;
}
public String getReferenceId() {
return referenceId;
}
// ═══════════════════════════════════════════════════════════════════
// RESOLUTION
// ═══════════════════════════════════════════════════════════════════
/**
* Resolve the action. Inline returns directly; references resolve
* from the appropriate controller with version-based caching.
*/
public IAbilityAction getAction() {
switch (slotType) {
case INLINE_ABILITY:
return inlineAbility;
case INLINE_CHAIN:
return inlineChain;
case ABILITY_REFERENCE: {
AbilityController ctrl = AbilityController.Instance;
if (ctrl == null) return null;
int rev = ctrl.getCustomAbilityRevision();
if (cachedAction != null && cachedRevision == rev) {
return cachedAction;
}
Ability resolved = ctrl.resolveAbility(referenceId);
cachedAction = resolved;
cachedRevision = rev;
return cachedAction;
}
case CHAIN_REFERENCE: {
AbilityController ctrl = AbilityController.Instance;
if (ctrl == null) return null;
int rev = ctrl.getChainedAbilityRevision();
if (cachedAction != null && cachedRevision == rev) {
return cachedAction;
}
ChainedAbility resolved = ctrl.resolveChainedAbility(referenceId);
cachedAction = resolved;
cachedRevision = rev;
return cachedAction;
}
default:
return null;
}
}
/**
* Get as Ability (returns null if this is a chain slot or resolution fails).
*/
public Ability getAbility() {
IAbilityAction action = getAction();
return (action instanceof Ability) ? (Ability) action : null;
}
/**
* Get as ChainedAbility (returns null if this is an ability slot or resolution fails).
*/
public ChainedAbility getChainedAbility() {
IAbilityAction action = getAction();
return (action instanceof ChainedAbility) ? (ChainedAbility) action : null;
}
// ═══════════════════════════════════════════════════════════════════
// ENABLED OVERRIDE
// ═══════════════════════════════════════════════════════════════════
public void setEnabled(boolean enabled) {
if (slotType == SlotType.INLINE_ABILITY) {
if (inlineAbility != null) {
inlineAbility.setEnabled(enabled);
}
} else if (slotType == SlotType.INLINE_CHAIN) {
if (inlineChain != null) {
inlineChain.setEnabled(enabled);
}
} else {
this.enabledOverride = enabled;
}
}
/**
* Check if this slot is enabled. For inline slots, delegates to the action directly.
* For reference slots, uses enabledOverride if set, otherwise the resolved action's own state.
* This avoids mutating the shared controller instance.
*/
public boolean isSlotEnabled() {
if (enabledOverride != null) {
return enabledOverride;
}
IAbilityAction action = getAction();
return action != null && action.isEnabled();
}
// ═══════════════════════════════════════════════════════════════════
// INLINE CONVERSION (ability references only)
// ═══════════════════════════════════════════════════════════════════
public Ability getInlineAbility() {
return inlineAbility;
}
public ChainedAbility getInlineChain() {
return inlineChain;
}
/**
* Convert this reference slot to an inline slot by copying the resolved data.
* Works for ability references (→ INLINE_ABILITY) and chain references (→ INLINE_CHAIN).
* Chain entries are kept as-is (not auto-converted to inline).
*/
public boolean convertToInline() {
if (slotType == SlotType.ABILITY_REFERENCE) {
Ability resolved = getAbility();
if (resolved == null) return false;
INbt nbt = resolved.writeNBT(true);
nbt.setString("id", UUID.randomUUID().toString());
inlineAbility = AbilityController.Instance.fromNBT(nbt);
slotType = SlotType.INLINE_ABILITY;
referenceId = null;
cachedAction = null;
cachedRevision = -1;
return true;
}
if (slotType == SlotType.CHAIN_REFERENCE) {
ChainedAbility resolved = getChainedAbility();
if (resolved == null) return false;
inlineChain = new ChainedAbility();
INbt chainNbt = resolved.writeNBT(true);
chainNbt.setString("Id", UUID.randomUUID().toString());
inlineChain.readNBT(chainNbt);
slotType = SlotType.INLINE_CHAIN;
referenceId = null;
cachedAction = null;
cachedRevision = -1;
return true;
}
return false;
}
// ═══════════════════════════════════════════════════════════════════
// NBT
// ═══════════════════════════════════════════════════════════════════
public INbt writeNBT(boolean saveScripts) {
switch (slotType) {
case INLINE_CHAIN: {
INbt nbt = new INbt();
if (inlineChain != null) {
nbt.setTag("InlineChain", inlineChain.writeNBT(saveScripts));
}
return nbt;
}
case CHAIN_REFERENCE: {
INbt nbt = new INbt();
nbt.setString("ChainReference", referenceId);
if (enabledOverride != null) {
nbt.setBoolean("RefEnabled", enabledOverride);
}
return nbt;
}
case ABILITY_REFERENCE: {
INbt nbt = new INbt();
nbt.setString("Reference", referenceId);
if (enabledOverride != null) {
nbt.setBoolean("RefEnabled", enabledOverride);
}
return nbt;
}
case INLINE_ABILITY:
default:
return inlineAbility != null ? inlineAbility.writeNBT(saveScripts) : new INbt();
}
}
public static AbilityAction fromNBT(INbt nbt) {
if (nbt == null) return null;
// Inline chain (new format)
if (nbt.hasKey("InlineChain")) {
ChainedAbility chain = new ChainedAbility();
chain.readNBT(nbt.getCompoundTag("InlineChain"));
return inlineChain(chain);
}
// Chain reference
if (nbt.hasKey("ChainReference")) {
AbilityAction slot = chainReference(nbt.getString("ChainReference"));
if (nbt.hasKey("RefEnabled")) {
slot.enabledOverride = nbt.getBoolean("RefEnabled");
}
return slot;
}
// Ability reference
if (nbt.hasKey("Reference")) {
AbilityAction slot = abilityReference(nbt.getString("Reference"));
if (nbt.hasKey("RefEnabled")) {
slot.enabledOverride = nbt.getBoolean("RefEnabled");
}
return slot;
}
// Inline ability
if (AbilityController.Instance == null) return null;
Ability ability = AbilityController.Instance.fromNBT(nbt);
if (ability == null) return null;
return inline(ability);
}
}
@@ -0,0 +1,54 @@
package kamkeel.npcs.controllers.data.ability;
import java.util.function.Consumer;
/**
* Represents a variant/template for an ability type.
* When a user creates an ability that has registered variants,
* they are presented with a selection dialog before the ability is created.
* <p>
* Each variant has a display name (lang key) and a configurator that
* applies preset values to the newly created ability.
*/
public class AbilityVariant {
private final String displayKey;
private final String group;
private final Consumer<Ability> configurator;
/**
* @param displayKey Lang key for the variant display name (e.g., "ability.variant.dual")
* @param configurator Lambda that configures the ability with preset values
*/
public AbilityVariant(String displayKey, Consumer<Ability> configurator) {
this(displayKey, null, configurator);
}
/**
* @param displayKey Lang key for the variant display name
* @param group Optional group header shown in the variant selection GUI (e.g., "DBC Addon")
* @param configurator Lambda that configures the ability with preset values
*/
public AbilityVariant(String displayKey, String group, Consumer<Ability> configurator) {
this.displayKey = displayKey;
this.group = group;
this.configurator = configurator;
}
public String getDisplayKey() {
return displayKey;
}
public String getGroup() {
return group;
}
/**
* Apply this variant's preset IConfiguration to the given ability.
*/
public void apply(Ability ability) {
if (configurator != null) {
configurator.accept(ability);
}
}
}
@@ -0,0 +1,117 @@
package kamkeel.npcs.controllers.data.ability;
import noppes.npcs.api.entity.IEntityLiving;
import noppes.npcs.api.IDamageSource;
import noppes.npcs.api.IWorld;
import noppes.npcs.api.entity.IEntityLivingBase;
import noppes.npcs.api.item.IItemStack;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.api.INbtList;
import noppes.npcs.api.INbt;
import kamkeel.npcs.controllers.data.ability.enums.AbilityPhase;
/**
* Wraps a concurrent ability execution with its own independent lifecycle.
* Concurrent abilities skip animations, sounds, and movement/rotation locks.
* They run their execute/tick logic in parallel with the primary ability.
* <p>
* Used by the chain system when an entry is concurrent-capable and concurrent-enabled.
*/
public class ConcurrentSlot {
private final Ability ability;
private boolean started = false;
private boolean completed = false;
public ConcurrentSlot(Ability ability) {
this.ability = ability;
}
public Ability getAbility() {
return ability;
}
public boolean isCompleted() {
return completed;
}
/**
* Start the concurrent ability. Skips windup — goes straight to ACTIVE.
* Called once when the slot is activated.
*/
public void start(IEntityLivingBase caster, IEntityLivingBase target) {
// Skip windup for concurrent execution — fire immediately
ability.setWindUpTicks(0);
ability.start(target);
started = true;
// Ability should now be in ACTIVE phase (0 windup → immediate)
if (ability.getPhase() == AbilityPhase.ACTIVE) {
ability.onExecute(caster, target);
}
// Check if ability completed during onExecute (instant abilities like signalCompletion)
if (ability.getPhase() == AbilityPhase.IDLE) {
completed = true;
}
}
/**
* Tick the concurrent ability. Called every game tick from AbstractDataAbilities.
* Handles phase transitions including burst delay → re-execute cycles.
*/
public void tick(IEntityLivingBase caster, IEntityLivingBase target) {
if (completed || !started) return;
AbilityPhase oldPhase = ability.getPhase();
boolean phaseChanged = ability.tick();
AbilityPhase newPhase = ability.getPhase();
switch (newPhase) {
case ACTIVE:
if (phaseChanged && oldPhase == AbilityPhase.BURST_DELAY) {
// Re-entering ACTIVE after burst delay → re-execute (burst re-fire)
ability.onExecute(caster, target);
if (ability.getPhase() == AbilityPhase.IDLE) {
completed = true;
return;
}
}
// Normal active tick
ability.onActiveTick(caster, target, ability.getCurrentTick());
if (ability.getPhase() == AbilityPhase.IDLE) {
completed = true;
}
break;
case BURST_DELAY:
// Waiting between bursts — nothing to do, tick() handles countdown
break;
case WINDUP:
// Burst replay with windup — for concurrent, skip it by advancing
// (windUpTicks was set to 0 at start, but burst replay may re-enter WINDUP)
// The next tick() call will transition to ACTIVE
break;
case IDLE:
completed = true;
break;
case DAZED:
// Concurrent abilities don't get dazed — treat as completed
completed = true;
break;
}
}
/**
* Force cleanup on interrupt. Called when the NPC is interrupted or combat ends.
*/
public void interrupt() {
if (!completed) {
ability.interrupt();
completed = true;
}
}
}
@@ -0,0 +1,507 @@
package kamkeel.npcs.controllers.data.ability;
import noppes.npcs.constants.ClientOnly;
import kamkeel.npcs.util.IVector3;
import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler;
import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback;
import kamkeel.npcs.controllers.data.ability.gui.FieldDef;
import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider;
import noppes.npcs.entity.EntityNPCInterface;
import kamkeel.npcs.entity.EntityEnergyDome;
import kamkeel.npcs.entity.EntityEnergyBarrier;
import kamkeel.npcs.entity.EntityEnergyPanel;
import kamkeel.npcs.entity.EntityAbilityOrb;
import kamkeel.npcs.entity.EntityAbilityLaser;
import kamkeel.npcs.entity.EntityAbilityDisc;
import kamkeel.npcs.entity.EntityAbilityBeam;
import kamkeel.npcs.entity.EntityEnergyProjectile;
import kamkeel.npcs.util.ByteBufUtils;
import noppes.npcs.api.entity.IEntityLiving;
import noppes.npcs.api.IDamageSource;
import noppes.npcs.api.IWorld;
import noppes.npcs.api.entity.IEntityLivingBase;
import noppes.npcs.api.item.IItemStack;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.api.INbtList;
import noppes.npcs.api.INbt;
import kamkeel.npcs.controllers.data.ability.enums.LockMode;
import kamkeel.npcs.controllers.data.ability.enums.TargetingMode;
import kamkeel.npcs.controllers.data.ability.enums.UserType;
import kamkeel.npcs.controllers.data.telegraph.Telegraph;
import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance;
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
import noppes.npcs.api.ability.ICustomAbility;
import java.util.List;
/**
* A blank-slate ability that does nothing by itself.
* All behavior is driven by script event hooks (abilityStart, abilityTick, abilityComplete,
* abilityToggle, abilityToggleTick).
* <p>
* Supports two modes based on toggle IConfiguration:
* <ul>
* <li><b>Instant mode</b> (toggleStates = 0): Uses standard execution lifecycle with
* configurable duration. Script hooks: abilityStart, abilityTick, abilityComplete.</li>
* <li><b>Toggle mode</b> (toggleStates >= 1): Uses toggle lifecycle.
* Script hooks: abilityToggle, abilityToggleTick.</li>
* </ul>
*/
public class CustomAbility extends Ability implements ICustomAbility {
private int durationTicks = 20;
// Telegraph dimension fields
private float tRadius = 5.0f;
private float tInnerRadius = 0.0f;
private float tLength = 5.0f;
private float tWidth = 2.0f;
private float tAngle = 45.0f;
// Telegraph timing
private int telegraphActiveTicks = 0;
private boolean syncTelegraphWithDuration = true;
public CustomAbility() {
this.typeId = "ability.cnpc.custom";
this.name = "Custom";
this.targetingMode = TargetingMode.SELF;
this.lockMovement = LockMode.NO;
this.cooldownTicks = 0;
this.windUpTicks = 0;
this.telegraphType = TelegraphType.NONE;
this.showTelegraph = false;
}
// ==================== EXECUTION ====================
@Override
public void onExecute(IEntityLivingBase caster, IEntityLivingBase target) {
if (durationTicks <= 0) {
signalCompletion();
}
}
@Override
public void onActiveTick(IEntityLivingBase caster, IEntityLivingBase target, int tick) {
if (durationTicks > 0 && tick >= durationTicks) {
signalCompletion();
}
}
// ==================== PROPERTIES ====================
@Override
public boolean hasDamage() {
return false;
}
@Override
public boolean isConcurrentCapable() {
return true;
}
@Override
public boolean isTargetingModeLocked() {
return false;
}
@Override
public boolean allowFreeOnCast() {
return true;
}
@Override
public boolean hasOwnTelegraphControls() {
return true;
}
// ==================== TELEGRAPH OVERRIDES ====================
@Override
public boolean keepTelegraphDuringActive() {
return getResolvedTelegraphActiveTicks() > 0;
}
/**
* Returns the effective telegraph active ticks: synced from duration or manual value.
*/
public int getResolvedTelegraphActiveTicks() {
if (syncTelegraphWithDuration) {
return durationTicks;
}
return telegraphActiveTicks;
}
@Override
public TelegraphInstance createTelegraph(IEntityLivingBase caster, IEntityLivingBase target) {
if (!isShowTelegraph() || getTelegraphType() == TelegraphType.NONE) {
return null;
}
Telegraph telegraph;
double x, y, z;
float yaw = caster.rotationYaw;
TelegraphType tType = getTelegraphType();
boolean positionAtCaster = targetingMode == TargetingMode.AOE_SELF ||
targetingMode == TargetingMode.SELF ||
tType == TelegraphType.LINE ||
tType == TelegraphType.CONE;
if (positionAtCaster) {
x = caster.posX;
y = findGroundLevel(caster.worldObj, caster.posX, caster.posY, caster.posZ);
z = caster.posZ;
} else if (target != null) {
x = target.posX;
y = findGroundLevel(caster.worldObj, target.posX, target.posY, target.posZ);
z = target.posZ;
} else {
x = caster.posX;
y = findGroundLevel(caster.worldObj, caster.posX, caster.posY, caster.posZ);
z = caster.posZ;
}
switch (tType) {
case CIRCLE:
telegraph = Telegraph.circle(tRadius);
break;
case RING:
telegraph = Telegraph.ring(tRadius, tInnerRadius);
break;
case LINE:
telegraph = Telegraph.line(tLength, tWidth);
break;
case CONE:
telegraph = Telegraph.cone(tLength, tAngle, tInnerRadius);
break;
case POINT:
telegraph = new Telegraph("", TelegraphType.POINT);
break;
case SQUARE:
telegraph = Telegraph.square(tRadius);
break;
default:
return null;
}
int activeTicks = getResolvedTelegraphActiveTicks();
int totalDuration = windUpTicks + activeTicks;
telegraph.setDurationTicks(Math.max(1, totalDuration));
telegraph.setColor(windUpColor);
telegraph.setWarningColor(activeColor);
if (windUpTicks > 0) {
telegraph.setWarningStartTick(windUpTicks);
} else {
telegraph.setWarningStartTick(0);
}
telegraph.setHeightOffset(telegraphHeightOffset);
TelegraphInstance instance = new TelegraphInstance(telegraph, x, y, z, yaw);
instance.setCasterEntityId(caster.getEntityId());
if (positionAtCaster) {
instance.setEntityIdToFollow(caster.getEntityId());
if (tType == TelegraphType.LINE || tType == TelegraphType.CONE) {
if (!isRotationLockedDuringWindup()) {
if (target != null) {
instance.setTargetEntityId(target.getEntityId());
} else {
instance.setTrackFollowedEntityYaw(true);
}
}
}
} else if (target != null) {
instance.setEntityIdToFollow(target.getEntityId());
}
return instance;
}
@Override
public float getTelegraphRadius() {
return tRadius;
}
@Override
public float getTelegraphInnerRadius() {
return tInnerRadius;
}
@Override
public float getTelegraphLength() {
return tLength;
}
@Override
public float getTelegraphWidth() {
return tWidth;
}
@Override
public float getTelegraphAngle() {
return tAngle;
}
// ==================== FIELDS ====================
@Override
public int getDurationTicks() {
return durationTicks;
}
@Override
public void setDurationTicks(int ticks) {
this.durationTicks = Math.max(0, ticks);
}
public float getTRadius() {
return tRadius;
}
public void setTRadius(float radius) {
this.tRadius = Math.max(0, radius);
}
public float getTInnerRadius() {
return tInnerRadius;
}
public void setTInnerRadius(float innerRadius) {
this.tInnerRadius = Math.max(0, innerRadius);
}
public float getTLength() {
return tLength;
}
public void setTLength(float length) {
this.tLength = Math.max(0, length);
}
public float getTWidth() {
return tWidth;
}
public void setTWidth(float width) {
this.tWidth = Math.max(0, width);
}
public float getTAngle() {
return tAngle;
}
public void setTAngle(float angle) {
this.tAngle = Math.max(0, angle);
}
public int getTelegraphActiveTicks() {
return telegraphActiveTicks;
}
public void setTelegraphActiveTicks(int ticks) {
this.telegraphActiveTicks = Math.max(0, ticks);
}
public boolean isSyncTelegraphWithDuration() {
return syncTelegraphWithDuration;
}
public void setSyncTelegraphWithDuration(boolean sync) {
this.syncTelegraphWithDuration = sync;
}
// ==================== ICustomAbility ====================
@Override
public int getTelegraphShapeType() {
return getTelegraphType().ordinal();
}
@Override
public void setTelegraphShapeType(int type) {
TelegraphType[] values = TelegraphType.values();
if (type >= 0 && type < values.length) {
setTelegraphType(values[type]);
}
}
@Override
public void setTelegraphRadius(float radius) {
this.tRadius = Math.max(0, radius);
}
@Override
public void setTelegraphInnerRadius(float innerRadius) {
this.tInnerRadius = Math.max(0, innerRadius);
}
@Override
public void setTelegraphLength(float length) {
this.tLength = Math.max(0, length);
}
@Override
public void setTelegraphWidth(float width) {
this.tWidth = Math.max(0, width);
}
@Override
public void setTelegraphAngle(float angle) {
this.tAngle = Math.max(0, angle);
}
@Override
public int getTargetingModeType() {
return getTargetingMode().ordinal();
}
@Override
public void setTargetingModeType(int type) {
TargetingMode[] values = TargetingMode.values();
if (type >= 0 && type < values.length) {
setTargetingMode(values[type]);
}
}
// ==================== NBT ====================
@Override
public void writeTypeNBT(INbt nbt) {
nbt.setInteger("durationTicks", durationTicks);
nbt.setFloat("tRadius", tRadius);
nbt.setFloat("tInnerRadius", tInnerRadius);
nbt.setFloat("tLength", tLength);
nbt.setFloat("tWidth", tWidth);
nbt.setFloat("tAngle", tAngle);
nbt.setInteger("telegraphActiveTicks", telegraphActiveTicks);
nbt.setBoolean("syncTelegraphWithDuration", syncTelegraphWithDuration);
}
@Override
public void readTypeNBT(INbt nbt) {
this.durationTicks = Math.max(0, nbt.getInteger("durationTicks"));
this.tRadius = nbt.hasKey("tRadius") ? nbt.getFloat("tRadius") : 5.0f;
this.tInnerRadius = nbt.hasKey("tInnerRadius") ? nbt.getFloat("tInnerRadius") : 0.0f;
this.tLength = nbt.hasKey("tLength") ? nbt.getFloat("tLength") : 5.0f;
this.tWidth = nbt.hasKey("tWidth") ? nbt.getFloat("tWidth") : 2.0f;
this.tAngle = nbt.hasKey("tAngle") ? nbt.getFloat("tAngle") : 45.0f;
this.telegraphActiveTicks = nbt.hasKey("telegraphActiveTicks") ? nbt.getInteger("telegraphActiveTicks") : 0;
this.syncTelegraphWithDuration = nbt.hasKey("syncTelegraphWithDuration") ? nbt.getBoolean("syncTelegraphWithDuration") : true;
}
// ==================== GUI ====================
@ClientOnly
@Override
public void getAbilityDefinitions(List<FieldDef> defs) {
// User Type (NPC / Player / Both)
defs.add(FieldDef.enumField("ability.allowedBy", UserType.class,
this::getAllowedBy, this::setAllowedBy));
// Targeting Mode
defs.add(FieldDef.enumField("ability.targetingMode", TargetingMode.class,
this::getTargetingMode, this::setTargetingMode));
// Toggle section
defs.add(FieldDef.section("ability.section.toggle"));
defs.add(FieldDef.boolField("ability.toggleEnabled",
this::isToggleable, enabled -> setToggleStates(enabled ? Math.max(1, getToggleStates()) : 0)));
defs.add(FieldDef.intField("ability.toggleStates", this::getToggleStates, this::setToggleStates)
.range(1, 10)
.visibleWhen(this::isToggleable));
defs.add(FieldDef.boolField("ability.hasActiveToggle", this::hasActiveToggle, this::setHasActiveToggle)
.visibleWhen(this::isToggleable));
// Duration (instant mode only)
defs.add(FieldDef.section("ability.section.execution")
.visibleWhen(() -> !this.isToggleable()));
defs.add(FieldDef.intField("ability.duration", this::getDurationTicks, this::setDurationTicks)
.range(0, 6000)
.visibleWhen(() -> !this.isToggleable()));
// ── Telegraph Controls (Effects tab) ─────────────────────────
defs.add(FieldDef.section("ability.section.telegraph").tab("Effects"));
defs.add(FieldDef.enumField("ability.telegraphShape", TelegraphType.class,
this::getTelegraphType, this::setTelegraphType).tab("Effects")
.hover("ability.hover.telegraphShape"));
defs.add(FieldDef.boolField("ability.showTelegraph", this::isShowTelegraph, this::setShowTelegraph)
.tab("Effects").visibleWhen(this::hasTelegraphShape)
.hover("ability.hover.showTelegraph"));
defs.add(FieldDef.colorSubGui("ability.windUpColor", this::getWindUpColor, this::setWindUpColor)
.tab("Effects").visibleWhen(this::isTelegraphVisible)
.hover("ability.hover.windUpColor"));
defs.add(FieldDef.colorSubGui("ability.activeColor", this::getActiveColor, this::setActiveColor)
.tab("Effects").visibleWhen(this::isTelegraphVisible)
.hover("ability.hover.activeColor"));
defs.add(FieldDef.floatField("ability.telegraphHeight", this::getTelegraphHeightOffset, this::setTelegraphHeightOffset)
.tab("Effects").visibleWhen(this::isTelegraphVisible)
.hover("ability.hover.telegraphHeight"));
// Telegraph active duration with sync toggle (mirrors windup sync pattern)
defs.add(FieldDef.row(
FieldDef.intField("ability.telegraphActiveTicks", this::getTelegraphActiveTicks, this::setTelegraphActiveTicks).range(0, 6000)
.hover("ability.hover.telegraphActiveTicks"),
FieldDef.boolField("ability.syncTelegraph", this::isSyncTelegraphWithDuration, this::setSyncTelegraphWithDuration)
.hover("ability.hover.syncTelegraph")
).tab("Effects").visibleWhen(() -> isTelegraphVisible() && !isSyncTelegraphWithDuration()));
defs.add(FieldDef.row(
FieldDef.labelField("ability.telegraphActiveTicks", () -> getResolvedTelegraphActiveTicks() + "t"),
FieldDef.boolField("ability.syncTelegraph", this::isSyncTelegraphWithDuration, this::setSyncTelegraphWithDuration)
.hover("ability.hover.syncTelegraph")
).tab("Effects").visibleWhen(() -> isTelegraphVisible() && isSyncTelegraphWithDuration()));
// Shape-specific dimension fields
defs.add(FieldDef.floatField("ability.telegraphRadius", this::getTRadius, this::setTRadius)
.tab("Effects").visibleWhen(() -> isTelegraphVisible() && needsRadius())
.hover("ability.hover.telegraphRadius"));
defs.add(FieldDef.floatField("ability.telegraphInnerRadius", this::getTInnerRadius, this::setTInnerRadius)
.tab("Effects").visibleWhen(() -> isTelegraphVisible() && needsInnerRadius())
.hover("ability.hover.telegraphInnerRadius"));
defs.add(FieldDef.floatField("ability.telegraphLength", this::getTLength, this::setTLength)
.tab("Effects").visibleWhen(() -> isTelegraphVisible() && needsLength())
.hover("ability.hover.telegraphLength"));
defs.add(FieldDef.floatField("ability.telegraphWidth", this::getTWidth, this::setTWidth)
.tab("Effects").visibleWhen(() -> isTelegraphVisible() && needsWidth())
.hover("ability.hover.telegraphWidth"));
defs.add(FieldDef.floatField("ability.telegraphAngle", this::getTAngle, this::setTAngle)
.tab("Effects").visibleWhen(() -> isTelegraphVisible() && needsAngle())
.hover("ability.hover.telegraphAngle"));
}
// ==================== TELEGRAPH VISIBILITY HELPERS ====================
private boolean hasTelegraphShape() {
return getTelegraphType() != TelegraphType.NONE;
}
private boolean isTelegraphVisible() {
return hasTelegraphShape() && isShowTelegraph();
}
private boolean needsRadius() {
TelegraphType t = getTelegraphType();
return t == TelegraphType.CIRCLE || t == TelegraphType.RING || t == TelegraphType.SQUARE;
}
private boolean needsInnerRadius() {
TelegraphType t = getTelegraphType();
return t == TelegraphType.RING || t == TelegraphType.CONE;
}
private boolean needsLength() {
TelegraphType t = getTelegraphType();
return t == TelegraphType.LINE || t == TelegraphType.CONE;
}
private boolean needsWidth() {
return getTelegraphType() == TelegraphType.LINE;
}
private boolean needsAngle() {
return getTelegraphType() == TelegraphType.CONE;
}
}
@@ -0,0 +1,147 @@
package kamkeel.npcs.controllers.data.ability.conditions;
import noppes.npcs.constants.ClientOnly;
import kamkeel.npcs.util.IVector3;
import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler;
import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback;
import kamkeel.npcs.controllers.data.ability.gui.FieldDef;
import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider;
import noppes.npcs.entity.EntityNPCInterface;
import kamkeel.npcs.entity.EntityEnergyDome;
import kamkeel.npcs.entity.EntityEnergyBarrier;
import kamkeel.npcs.entity.EntityEnergyPanel;
import kamkeel.npcs.entity.EntityAbilityOrb;
import kamkeel.npcs.entity.EntityAbilityLaser;
import kamkeel.npcs.entity.EntityAbilityDisc;
import kamkeel.npcs.entity.EntityAbilityBeam;
import kamkeel.npcs.entity.EntityEnergyProjectile;
import kamkeel.npcs.util.ByteBufUtils;
import noppes.npcs.api.entity.IEntityLiving;
import noppes.npcs.api.IDamageSource;
import noppes.npcs.api.IWorld;
import noppes.npcs.api.entity.IEntityLivingBase;
import noppes.npcs.api.item.IItemStack;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.api.INbtList;
import noppes.npcs.api.INbt;
import kamkeel.npcs.controllers.AbilityController;
import kamkeel.npcs.controllers.data.ability.enums.UserType;
import noppes.npcs.LogWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Supplier;
public abstract class AbilityCondition {
public static final int MAX_CONDITIONS = 5;
protected String typeId = "";
protected String name = "";
protected UserType userType = UserType.BOTH;
protected ConditionFilter conditionFilter = ConditionFilter.CASTER;
public boolean check(IEntityLivingBase caster, IEntityLivingBase target) {
switch (getFilter()) {
case CASTER:
return checkEntity(caster);
case TARGET:
return target != null && checkEntity(target);
case BOTH:
return checkEntity(caster) && (target != null && checkEntity(target));
default:
return checkEntity(caster);
}
}
protected abstract boolean checkEntity(IEntityLivingBase IEntity);
public boolean requiresTarget() {
return false;
}
/**
* Returns true if all required fields are filled in.
* Used to disable the Done button in SubGuiConditionEdit when incomplete.
*/
public boolean isConfigured() {
return true;
}
public UserType getUserType() {
return userType;
}
public String getTypeId() {
return typeId;
}
public String getName() {
return name;
}
public ConditionFilter getFilter() {
return conditionFilter;
}
public void setFilter(ConditionFilter filter) {
this.conditionFilter = filter;
}
@ClientOnly
public abstract void getConditionDefinitions(List<FieldDef> defs);
/**
* Returns a human-readable summary of this condition's IConfiguration.
* Used for tooltips when hovering over condition buttons.
*/
@ClientOnly
public abstract String getConditionSummary();
@ClientOnly
public final List<FieldDef> getAllDefinitions() {
List<FieldDef> defs = new ArrayList<>();
defs.add(FieldDef.labelField("ability.validFor", () ->
"\u00A7e" + PlatformServiceHolder.get().translateToLocal("ability.userType." + getUserType().name())));
defs.add(FieldDef.enumField("condition.filter", ConditionFilter.class, this::getFilter, this::setFilter));
getConditionDefinitions(defs);
return defs;
}
public final INbt writeNBT() {
INbt nbt = new INbt();
nbt.setString("typeId", getTypeId());
nbt.setString("name", getName());
nbt.setInteger("userType", getUserType().ordinal());
nbt.setInteger("filter", getFilter().ordinal());
writeTypeNBT(nbt);
return nbt;
}
public abstract void writeTypeNBT(INbt nbt);
public abstract void readTypeNBT(INbt nbt);
public final void readNBT(INbt nbt) {
typeId = nbt.getString("typeId");
name = nbt.getString("name");
userType = UserType.fromOrdinal(nbt.getInteger("userType"));
conditionFilter = ConditionFilter.fromOrdinal(nbt.getInteger("filter"));
readTypeNBT(nbt);
}
public static AbilityCondition fromNBT(INbt nbt) {
String typeId = nbt.getString("typeId");
Supplier<AbilityCondition> factory = AbilityController.Instance.getConditionType(typeId);
if (factory == null) {
LogWriter.info("AbilityController: Unknown condition type: " + typeId);
return null;
}
AbilityCondition condition = factory.get();
condition.readNBT(nbt);
return condition;
}
}
@@ -0,0 +1,138 @@
package kamkeel.npcs.controllers.data.ability.conditions;
import noppes.npcs.constants.ClientOnly;
import kamkeel.npcs.util.IVector3;
import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler;
import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback;
import kamkeel.npcs.controllers.data.ability.gui.FieldDef;
import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider;
import noppes.npcs.entity.EntityNPCInterface;
import kamkeel.npcs.entity.EntityEnergyDome;
import kamkeel.npcs.entity.EntityEnergyBarrier;
import kamkeel.npcs.entity.EntityEnergyPanel;
import kamkeel.npcs.entity.EntityAbilityOrb;
import kamkeel.npcs.entity.EntityAbilityLaser;
import kamkeel.npcs.entity.EntityAbilityDisc;
import kamkeel.npcs.entity.EntityAbilityBeam;
import kamkeel.npcs.entity.EntityEnergyProjectile;
import kamkeel.npcs.util.ByteBufUtils;
import noppes.npcs.api.entity.IEntityLiving;
import noppes.npcs.api.IDamageSource;
import noppes.npcs.api.IWorld;
import noppes.npcs.api.entity.IEntityLivingBase;
import noppes.npcs.api.item.IItemStack;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.api.INbtList;
import noppes.npcs.api.INbt;
import java.util.List;
/**
* Abstract base for float comparison-based conditions.
* Provides shared CompareType enum, NBT serialization, and FieldDef generation.
*/
public abstract class ConditionCompare extends AbilityCondition {
public enum CompareType {
ABOVE {
@Override
public boolean test(float value, float threshold) {
return value >= threshold;
}
},
BELOW {
@Override
public boolean test(float value, float threshold) {
return value <= threshold;
}
},
EQUAL {
@Override
public boolean test(float value, float threshold) {
return value == threshold;
}
};
public abstract boolean test(float value, float threshold);
public static CompareType fromOrdinal(int ordinal) {
CompareType[] values = values();
return (ordinal >= 0 && ordinal < values.length) ? values[ordinal] : ABOVE;
}
@Override
public String toString() {
return "condition." + name().toLowerCase();
}
}
protected CompareType compareType = CompareType.ABOVE;
/**
* Returns the value to be compared for this IEntity (e.g. level, skill level).
* Return -1 to indicate the condition should fail immediately.
*/
protected abstract float getEntityValue(IEntityLivingBase IEntity);
/**
* Returns the threshold that getEntityValue() is compared against.
*/
protected abstract float getThreshold();
/**
* Appends subclass-specific FieldDefs (e.g. skill picker, level input).
* Called before the shared compareType field is added.
*/
@ClientOnly
protected abstract void getExtraDefinitions(List<FieldDef> defs);
/**
* Subclasses write their own NBT fields here.
*/
protected abstract void writeExtraNBT(INbt nbt);
/**
* Subclasses read their own NBT fields here.
*/
protected abstract void readExtraNBT(INbt nbt);
@Override
protected boolean checkEntity(IEntityLivingBase IEntity) {
float value = getEntityValue(IEntity);
if (value < 0) return false;
return compareType.test(value, getThreshold());
}
@ClientOnly
@Override
public void getConditionDefinitions(List<FieldDef> defs) {
defs.add(FieldDef.enumField("condition.compare_type", CompareType.class, this::getCompareType, this::setCompareType));
getExtraDefinitions(defs);
}
@Override
public void writeTypeNBT(INbt nbt) {
nbt.setInteger("compareType", compareType.ordinal());
writeExtraNBT(nbt);
}
@Override
public void readTypeNBT(INbt nbt) {
compareType = CompareType.fromOrdinal(nbt.getInteger("compareType"));
readExtraNBT(nbt);
}
// ═══════════════════════════════════════════════════════════════════
// GETTERS / SETTERS
// ═══════════════════════════════════════════════════════════════════
public CompareType getCompareType() {
return compareType;
}
public void setCompareType(CompareType compareType) {
this.compareType = compareType;
}
}
@@ -0,0 +1,33 @@
package kamkeel.npcs.controllers.data.ability.conditions;
import noppes.npcs.api.entity.IEntityLiving;
import noppes.npcs.api.IDamageSource;
import noppes.npcs.api.IWorld;
import noppes.npcs.api.entity.IEntityLivingBase;
import noppes.npcs.api.item.IItemStack;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.api.INbtList;
import noppes.npcs.api.INbt;
public class ConditionHPThreshold extends ConditionThreshold {
public ConditionHPThreshold() {
this.typeId = "condition.cnpc.hp_threshold";
this.name = "condition.hp_threshold";
}
@Override
protected float getEntityValue(IEntityLivingBase IEntity) {
return IEntity.getHealth();
}
@Override
protected float getEntityMaxValue(IEntityLivingBase IEntity) {
return IEntity.getMaxHealth();
}
@Override
protected String getStatName() {
return "HP";
}
}
@@ -0,0 +1,128 @@
package kamkeel.npcs.controllers.data.ability.conditions;
import noppes.npcs.constants.ClientOnly;
import kamkeel.npcs.util.IVector3;
import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler;
import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback;
import kamkeel.npcs.controllers.data.ability.gui.FieldDef;
import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider;
import noppes.npcs.entity.EntityNPCInterface;
import kamkeel.npcs.entity.EntityEnergyDome;
import kamkeel.npcs.entity.EntityEnergyBarrier;
import kamkeel.npcs.entity.EntityEnergyPanel;
import kamkeel.npcs.entity.EntityAbilityOrb;
import kamkeel.npcs.entity.EntityAbilityLaser;
import kamkeel.npcs.entity.EntityAbilityDisc;
import kamkeel.npcs.entity.EntityAbilityBeam;
import kamkeel.npcs.entity.EntityEnergyProjectile;
import kamkeel.npcs.util.ByteBufUtils;
import noppes.npcs.api.entity.IEntityLiving;
import noppes.npcs.api.IDamageSource;
import noppes.npcs.api.IWorld;
import noppes.npcs.api.entity.IEntityLivingBase;
import noppes.npcs.api.item.IItemStack;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.api.INbtList;
import noppes.npcs.api.INbt;
import kamkeel.npcs.controllers.data.ability.enums.UserType;
import noppes.npcs.controllers.CustomEffectController;
import noppes.npcs.controllers.data.CustomEffect;
import java.util.HashMap;
import java.util.List;
public class ConditionHasEffect extends AbilityCondition {
private int effectId = -1;
private int effectIndex = 0;
public ConditionHasEffect() {
this.typeId = "condition.cnpc.has_effect";
this.name = "condition.has_effect";
this.userType = UserType.PLAYER_ONLY;
}
@Override
protected boolean checkEntity(IEntityLivingBase IEntity) {
if (!(IEntity instanceof IPlayer)) return false;
if (effectId < 0) return false;
IPlayer player = (IPlayer) IEntity;
return CustomEffectController.getInstance().hasEffect(player, effectId, effectIndex);
}
@ClientOnly
@Override
public void getConditionDefinitions(List<FieldDef> defs) {
defs.add(FieldDef.subGuiField("condition.select_effect",
() -> new SubGuiCustomEffectSelect(effectId, effectIndex),
gui -> {
SubGuiCustomEffectSelect sel = (SubGuiCustomEffectSelect) gui;
if (sel.getSelectedEffectId() >= 0) {
effectId = sel.getSelectedEffectId();
effectIndex = sel.getSelectedIndex();
}
})
.buttonLabel(() -> {
if (effectId < 0) return "None";
CustomEffect effect = getEffect();
return effect != null ? effect.getName() : "ID:" + effectId;
})
.clearable(() -> {
effectId = -1;
effectIndex = 0;
}));
}
@ClientOnly
@Override
public String getConditionSummary() {
String filterLabel = PlatformServiceHolder.get().translateToLocal(getFilter().toString());
String effectName = "None";
if (effectId >= 0) {
CustomEffect effect = getEffect();
effectName = effect != null ? effect.getName() : "ID:" + effectId;
}
return "[" + filterLabel + "] Effect: " + effectName;
}
@Override
public boolean isConfigured() {
return effectId >= 0;
}
private CustomEffect getEffect() {
HashMap<Integer, CustomEffect> map = CustomEffectController.getInstance().getEffectMap(effectIndex);
return map != null ? map.get(effectId) : null;
}
@Override
public void writeTypeNBT(INbt nbt) {
nbt.setInteger("effectId", effectId);
nbt.setInteger("effectIndex", effectIndex);
}
@Override
public void readTypeNBT(INbt nbt) {
effectId = nbt.getInteger("effectId");
effectIndex = nbt.getInteger("effectIndex");
}
public int getEffectId() {
return effectId;
}
public void setEffectId(int effectId) {
this.effectId = effectId;
}
public int getEffectIndex() {
return effectIndex;
}
public void setEffectIndex(int effectIndex) {
this.effectIndex = effectIndex;
}
}
@@ -0,0 +1,101 @@
package kamkeel.npcs.controllers.data.ability.conditions;
import noppes.npcs.constants.ClientOnly;
import kamkeel.npcs.util.IVector3;
import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler;
import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback;
import kamkeel.npcs.controllers.data.ability.gui.FieldDef;
import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider;
import noppes.npcs.entity.EntityNPCInterface;
import kamkeel.npcs.entity.EntityEnergyDome;
import kamkeel.npcs.entity.EntityEnergyBarrier;
import kamkeel.npcs.entity.EntityEnergyPanel;
import kamkeel.npcs.entity.EntityAbilityOrb;
import kamkeel.npcs.entity.EntityAbilityLaser;
import kamkeel.npcs.entity.EntityAbilityDisc;
import kamkeel.npcs.entity.EntityAbilityBeam;
import kamkeel.npcs.entity.EntityEnergyProjectile;
import kamkeel.npcs.util.ByteBufUtils;
import noppes.npcs.api.entity.IEntityLiving;
import noppes.npcs.api.IDamageSource;
import noppes.npcs.api.IWorld;
import noppes.npcs.api.entity.IEntityLivingBase;
import noppes.npcs.api.item.IItemStack;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.api.INbtList;
import noppes.npcs.api.INbt;
import java.util.List;
public class ConditionHitCount extends AbilityCondition {
private int requiredHits = 3;
private int withinTicks = 60;
public ConditionHitCount() {
this.typeId = "condition.cnpc.hit_count";
this.name = "condition.hit_count";
this.conditionFilter = ConditionFilter.CASTER;
}
@Override
protected boolean checkEntity(IEntityLivingBase IEntity) {
// Not used — this condition overrides check() directly
return false;
}
@Override
public boolean check(IEntityLivingBase caster, IEntityLivingBase target) {
if (caster instanceof EntityNPCInterface) {
EntityNPCInterface npc = (EntityNPCInterface) caster;
if (npc.abilities != null) {
return npc.abilities.getRecentHitCount(withinTicks) >= requiredHits;
}
}
// For players, hit count tracking is not yet supported
return false;
}
public int getRequiredHits() {
return requiredHits;
}
public void setRequiredHits(int requiredHits) {
this.requiredHits = Math.max(0, requiredHits);
}
public int getWithinTicks() {
return withinTicks;
}
public void setWithinTicks(int withinTicks) {
this.withinTicks = Math.max(0, withinTicks);
}
@ClientOnly
@Override
public void getConditionDefinitions(List<FieldDef> defs) {
defs.add(FieldDef.intField("condition.required_hits", this::getRequiredHits, this::setRequiredHits).min(1));
defs.add(FieldDef.intField("condition.within_ticks", this::getWithinTicks, this::setWithinTicks).min(0));
}
@ClientOnly
@Override
public String getConditionSummary() {
return "[Caster] Hit " + requiredHits + " times in " + withinTicks + " ticks";
}
@Override
public void writeTypeNBT(INbt nbt) {
nbt.setInteger("requiredHits", requiredHits);
nbt.setInteger("withinTicks", withinTicks);
}
@Override
public void readTypeNBT(INbt nbt) {
requiredHits = Math.max(0, nbt.getInteger("requiredHits"));
withinTicks = Math.max(0, nbt.getInteger("withinTicks"));
}
}
@@ -0,0 +1,208 @@
package kamkeel.npcs.controllers.data.ability.conditions;
import noppes.npcs.constants.ClientOnly;
import kamkeel.npcs.util.IVector3;
import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler;
import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback;
import kamkeel.npcs.controllers.data.ability.gui.FieldDef;
import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider;
import noppes.npcs.entity.EntityNPCInterface;
import kamkeel.npcs.entity.EntityEnergyDome;
import kamkeel.npcs.entity.EntityEnergyBarrier;
import kamkeel.npcs.entity.EntityEnergyPanel;
import kamkeel.npcs.entity.EntityAbilityOrb;
import kamkeel.npcs.entity.EntityAbilityLaser;
import kamkeel.npcs.entity.EntityAbilityDisc;
import kamkeel.npcs.entity.EntityAbilityBeam;
import kamkeel.npcs.entity.EntityEnergyProjectile;
import kamkeel.npcs.util.ByteBufUtils;
import noppes.npcs.api.entity.IEntityLiving;
import noppes.npcs.api.IDamageSource;
import noppes.npcs.api.IWorld;
import noppes.npcs.api.entity.IEntityLivingBase;
import noppes.npcs.api.item.IItemStack;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.api.INbtList;
import noppes.npcs.api.INbt;
import java.util.List;
public class ConditionItem extends AbilityCondition {
public enum UsageType {
ARMOR, HOLDING, COUNT, OFFHAND;
public static UsageType fromOrdinal(int ordinal) {
UsageType[] values = values();
return (ordinal >= 0 && ordinal < values.length) ? values[ordinal] : HOLDING;
}
}
public enum ArmorSlot {
BOOTS, LEGS, CHEST, HELMET;
public static ArmorSlot fromOrdinal(int ordinal) {
ArmorSlot[] values = values();
return (ordinal >= 0 && ordinal < values.length) ? values[ordinal] : BOOTS;
}
}
// Shared
private UsageType usageType = UsageType.HOLDING;
private String itemName = "";
// ARMOR only
private ArmorSlot armorSlot = ArmorSlot.BOOTS;
private boolean fullArmorSet = false;
// COUNT + PLAYER only
private int requiredCount = 1;
public ConditionItem() {
this.typeId = "condition.cnpc.item";
this.name = "condition.item";
}
@Override
protected boolean checkEntity(IEntityLivingBase IEntity) {
switch (usageType) {
case HOLDING:
return checkHolding(IEntity);
case ARMOR:
return checkArmor(IEntity);
case COUNT:
return checkCount(IEntity);
case OFFHAND:
return checkOffhand(IEntity);
default:
return false;
}
}
private boolean checkHolding(IEntityLivingBase IEntity) {
IItemStack held = IEntity.getHeldItem();
return matchesItem(held);
}
private boolean checkArmor(IEntityLivingBase IEntity) {
// MC getEquipmentInSlot: 1=boots, 2=legs, 3=chest, 4=helmet
int slot = armorSlot.ordinal() + 1; // BOOTS=0, LEGS=1, CHEST=2, HELMET=3
return matchesItem(IEntity.getEquipmentInSlot(slot));
}
private boolean checkCount(IEntityLivingBase IEntity) {
if (IEntity instanceof IPlayer) {
return checkPlayerInventoryCount((IPlayer) IEntity);
}
if (IEntity instanceof EntityNPCInterface) {
return checkNPCProjectile((EntityNPCInterface) IEntity);
}
return false;
}
private boolean checkOffhand(IEntityLivingBase IEntity) {
// No offhand for players
if (IEntity instanceof IPlayer) return false;
if (!(IEntity instanceof EntityNPCInterface)) return false;
IItemStack item = ((EntityNPCInterface) IEntity).getOffHand();
return matchesItem(item);
}
private boolean checkPlayerInventoryCount(IPlayer player) {
int count = 0;
for (IItemStack stack : player.inventory.mainInventory) {
if (matchesItem(stack)) {
count += stack.stackSize;
}
}
return count >= requiredCount;
}
private boolean checkNPCProjectile(EntityNPCInterface npc) {
IItemStack projectile = npc.inventory.getProjectile();
return matchesItem(projectile);
}
private boolean matchesItem(IItemStack stack) {
if (stack == null || itemName == null || itemName.isEmpty()) return false;
String registryName = net.minecraft.item.Item.itemRegistry.getNameForObject(stack.getItem());
return itemName.equals(registryName);
}
@ClientOnly
@Override
public void getConditionDefinitions(List<FieldDef> defs) {
defs.add(FieldDef.enumField("condition.usage_type", UsageType.class,
this::getUsageType, this::setUsageType));
defs.add(FieldDef.stringField("condition.item_name",
this::getItemName, this::setItemName)
.hover("condition.hover.item_name"));
defs.add(FieldDef.enumField("condition.armor_slot", ArmorSlot.class,
this::getArmorSlot, this::setArmorSlot)
.visibleWhen(() -> usageType == UsageType.ARMOR)
.hover("condition.hover.armor_slots"));
defs.add(FieldDef.intField("condition.item_count",
this::getRequiredCount, this::setRequiredCount)
.range(1, 64)
.visibleWhen(() -> usageType == UsageType.COUNT && userType.allowsPlayer())
.hover("condition.hover.required_count"));
}
@ClientOnly
@Override
public String getConditionSummary() {
String filterLabel = PlatformServiceHolder.get().translateToLocal(getFilter().toString());
String usage = usageType.name();
String item = itemName.isEmpty() ? "None" : itemName;
return "[" + filterLabel + "] " + usage + ": " + item;
}
@Override
public void writeTypeNBT(INbt nbt) {
nbt.setString("itemName", itemName);
nbt.setInteger("usageType", usageType.ordinal());
nbt.setInteger("armorSlot", armorSlot.ordinal());
nbt.setInteger("requiredCount", requiredCount);
}
@Override
public void readTypeNBT(INbt nbt) {
itemName = nbt.getString("itemName");
usageType = UsageType.fromOrdinal(nbt.getInteger("usageType"));
armorSlot = ArmorSlot.fromOrdinal(nbt.getInteger("armorSlot"));
requiredCount = Math.max(1, nbt.getInteger("requiredCount"));
}
@Override
public boolean isConfigured() {
return itemName != null && !itemName.isEmpty();
}
public UsageType getUsageType() { return usageType; }
public void setUsageType(UsageType usageType) { this.usageType = usageType; }
public String getItemName() { return itemName; }
public void setItemName(String itemName) { this.itemName = itemName; }
public ArmorSlot getArmorSlot() { return armorSlot; }
public void setArmorSlot(ArmorSlot armorSlot) { this.armorSlot = armorSlot; }
public boolean isFullArmorSet() {
return fullArmorSet;
}
public void setFullArmorSet(boolean fullArmorSet) {
this.fullArmorSet = fullArmorSet;
}
public int getRequiredCount() { return requiredCount; }
public void setRequiredCount(int requiredCount) { this.requiredCount = Math.max(1, requiredCount); }
}
@@ -0,0 +1,110 @@
package kamkeel.npcs.controllers.data.ability.conditions;
import noppes.npcs.constants.ClientOnly;
import kamkeel.npcs.util.IVector3;
import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler;
import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback;
import kamkeel.npcs.controllers.data.ability.gui.FieldDef;
import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider;
import noppes.npcs.entity.EntityNPCInterface;
import kamkeel.npcs.entity.EntityEnergyDome;
import kamkeel.npcs.entity.EntityEnergyBarrier;
import kamkeel.npcs.entity.EntityEnergyPanel;
import kamkeel.npcs.entity.EntityAbilityOrb;
import kamkeel.npcs.entity.EntityAbilityLaser;
import kamkeel.npcs.entity.EntityAbilityDisc;
import kamkeel.npcs.entity.EntityAbilityBeam;
import kamkeel.npcs.entity.EntityEnergyProjectile;
import kamkeel.npcs.util.ByteBufUtils;
import noppes.npcs.api.entity.IEntityLiving;
import noppes.npcs.api.IDamageSource;
import noppes.npcs.api.IWorld;
import noppes.npcs.api.entity.IEntityLivingBase;
import noppes.npcs.api.item.IItemStack;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.api.INbtList;
import noppes.npcs.api.INbt;
import kamkeel.npcs.controllers.data.ability.enums.UserType;
import noppes.npcs.controllers.QuestController;
import noppes.npcs.controllers.data.PlayerData;
import noppes.npcs.controllers.data.Quest;
import java.util.List;
public class ConditionQuestCompleted extends AbilityCondition {
private int questId = -1;
public ConditionQuestCompleted() {
this.typeId = "condition.cnpc.quest_completed";
this.name = "condition.quest_completed";
this.userType = UserType.PLAYER_ONLY;
}
@Override
protected boolean checkEntity(IEntityLivingBase IEntity) {
if (!(IEntity instanceof IPlayer)) return false;
if (questId < 0) return false;
IPlayer player = (IPlayer) IEntity;
PlayerData data = PlayerData.get(player);
if (data == null || data.questData == null) return false;
return data.questData.hasFinishedQuest(questId);
}
@ClientOnly
@Override
public void getConditionDefinitions(List<FieldDef> defs) {
defs.add(FieldDef.subGuiField("condition.select_quest",
() -> new GuiQuestSelection(questId),
gui -> {
GuiQuestSelection sel = (GuiQuestSelection) gui;
if (sel.selectedQuest != null) {
questId = sel.selectedQuest.id;
}
})
.buttonLabel(() -> {
if (questId < 0) return "None";
Quest q = QuestController.Instance.quests.get(questId);
return q != null ? q.title : "ID:" + questId;
})
.clearable(() -> questId = -1));
}
@ClientOnly
@Override
public String getConditionSummary() {
String filterLabel = PlatformServiceHolder.get().translateToLocal(getFilter().toString());
String questName = "None";
if (questId >= 0) {
Quest q = QuestController.Instance.quests.get(questId);
questName = q != null ? q.title : "ID:" + questId;
}
return "[" + filterLabel + "] Quest: " + questName;
}
@Override
public void writeTypeNBT(INbt nbt) {
nbt.setInteger("questId", questId);
}
@Override
public void readTypeNBT(INbt nbt) {
questId = nbt.getInteger("questId");
}
@Override
public boolean isConfigured() {
return questId >= 0;
}
public int getQuestId() {
return questId;
}
public void setQuestId(int questId) {
this.questId = questId;
}
}
@@ -0,0 +1,195 @@
package kamkeel.npcs.controllers.data.ability.conditions;
import noppes.npcs.constants.ClientOnly;
import kamkeel.npcs.util.IVector3;
import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler;
import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback;
import kamkeel.npcs.controllers.data.ability.gui.FieldDef;
import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider;
import noppes.npcs.entity.EntityNPCInterface;
import kamkeel.npcs.entity.EntityEnergyDome;
import kamkeel.npcs.entity.EntityEnergyBarrier;
import kamkeel.npcs.entity.EntityEnergyPanel;
import kamkeel.npcs.entity.EntityAbilityOrb;
import kamkeel.npcs.entity.EntityAbilityLaser;
import kamkeel.npcs.entity.EntityAbilityDisc;
import kamkeel.npcs.entity.EntityAbilityBeam;
import kamkeel.npcs.entity.EntityEnergyProjectile;
import kamkeel.npcs.util.ByteBufUtils;
import noppes.npcs.api.entity.IEntityLiving;
import noppes.npcs.api.IDamageSource;
import noppes.npcs.api.IWorld;
import noppes.npcs.api.entity.IEntityLivingBase;
import noppes.npcs.api.item.IItemStack;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.api.INbtList;
import noppes.npcs.api.INbt;
import java.util.List;
/**
* Abstract base for threshold-based conditions (HP, Ki, etc.).
* Provides shared percent/flat toggle, ThresholdType enum, and FieldDef generation.
*/
public abstract class ConditionThreshold extends AbilityCondition {
public enum ThresholdType {
ABOVE {
@Override
public boolean test(float value, float threshold) {
return value >= threshold;
}
},
BELOW {
@Override
public boolean test(float value, float threshold) {
return value <= threshold;
}
},
EQUAL {
@Override
public boolean test(float value, float threshold) {
return Math.abs(value - threshold) < 0.001f;
}
};
public abstract boolean test(float value, float threshold);
public static ThresholdType fromOrdinal(int ordinal) {
ThresholdType[] values = values();
if (ordinal >= 0 && ordinal < values.length) {
return values[ordinal];
}
return ABOVE;
}
@Override
public String toString() {
switch (this) {
case ABOVE:
return "condition.above";
case BELOW:
return "condition.below";
case EQUAL:
return "condition.equal";
default:
return name();
}
}
}
protected float thresholdFlat = 100f;
protected float thresholdPercent = 0.5f;
protected boolean percent = true;
protected ThresholdType thresholdType = ThresholdType.ABOVE;
/**
* Returns the raw stat value for the IEntity (e.g. current HP, current Ki).
*/
protected abstract float getEntityValue(IEntityLivingBase IEntity);
/**
* Returns the maximum stat value for the IEntity (e.g. max HP, max Ki).
* Used to compute the percentage ratio.
*/
protected abstract float getEntityMaxValue(IEntityLivingBase IEntity);
/**
* Returns the display name for the stat being checked (e.g. "HP", "Ki").
* Used in getConditionSummary().
*/
protected abstract String getStatName();
@Override
protected boolean checkEntity(IEntityLivingBase IEntity) {
float value;
float threshold;
if (percent) {
float max = getEntityMaxValue(IEntity);
value = max > 0 ? getEntityValue(IEntity) / max : 0;
threshold = thresholdPercent;
} else {
value = getEntityValue(IEntity);
threshold = thresholdFlat;
}
return thresholdType.test(value, threshold);
}
@ClientOnly
@Override
public void getConditionDefinitions(List<FieldDef> defs) {
defs.add(FieldDef.floatField("condition.threshold_flat", this::getThresholdFlat, this::setThresholdFlat)
.min(0).visibleWhen(() -> !isPercent()));
defs.add(FieldDef.floatField("condition.threshold_percent", this::getThresholdPercent, this::setThresholdPercent)
.min(0).max(1).visibleWhen(this::isPercent));
defs.add(FieldDef.boolField("condition.percent", this::isPercent, this::setIsPercent));
defs.add(FieldDef.enumField("condition.threshold_type", ThresholdType.class,
this::getThresholdType, this::setThresholdType));
}
@Override
public void writeTypeNBT(INbt nbt) {
nbt.setFloat("thresholdFlat", thresholdFlat);
nbt.setFloat("thresholdPercent", thresholdPercent);
nbt.setBoolean("percent", percent);
nbt.setInteger("thresholdType", thresholdType.ordinal());
}
@Override
public void readTypeNBT(INbt nbt) {
thresholdFlat = nbt.getFloat("thresholdFlat");
thresholdPercent = nbt.getFloat("thresholdPercent");
percent = nbt.getBoolean("percent");
thresholdType = ThresholdType.fromOrdinal(nbt.getInteger("thresholdType"));
}
@ClientOnly
@Override
public String getConditionSummary() {
String filterLabel = PlatformServiceHolder.get().translateToLocal(getFilter().toString());
String typeName = getStatName();
String typeLabel = PlatformServiceHolder.get().translateToLocal(thresholdType.toString());
String value = percent
? String.format("%.0f%%", thresholdPercent * 100)
: String.format("%.0f", thresholdFlat);
return "[" + filterLabel + "] " + typeName + " " + typeLabel + " " + value;
}
// ═══════════════════════════════════════════════════════════════════
// GETTERS / SETTERS
// ═══════════════════════════════════════════════════════════════════
public float getThresholdFlat() {
return thresholdFlat;
}
public void setThresholdFlat(float value) {
this.thresholdFlat = Math.max(0, value);
}
public float getThresholdPercent() {
return thresholdPercent;
}
public void setThresholdPercent(float value) {
this.thresholdPercent = Math.max(0, Math.min(1, value));
}
public boolean isPercent() {
return percent;
}
public void setIsPercent(boolean percent) {
this.percent = percent;
}
public ThresholdType getThresholdType() {
return thresholdType;
}
public void setThresholdType(ThresholdType type) {
this.thresholdType = type;
}
}
@@ -0,0 +1,344 @@
package kamkeel.npcs.controllers.data.ability.data;
import noppes.npcs.api.entity.IEntityLiving;
import noppes.npcs.api.IDamageSource;
import noppes.npcs.api.IWorld;
import noppes.npcs.api.entity.IEntityLivingBase;
import noppes.npcs.api.item.IItemStack;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.api.INbtList;
import noppes.npcs.api.INbt;
import kamkeel.npcs.controllers.data.ability.Ability;
/**
* Icon data for abilities, stored in the ability's customData NBT under "AbilityIcon".
* Supports up to 3 layers, each with its own texture, UV coordinates, and tint color.
* Width, height, and scale are shared across all layers.
*/
public class AbilityIconData {
public static final String NBT_KEY = "AbilityIcon";
public static final int MAX_LAYERS = 3;
private final INbt customData;
// Shared across all layers
public int width = 32;
public int height = 32;
public float scale = 1.0f;
/** Whether a custom icon override is active. Default false = use type's default icon. */
private boolean enabled = false;
/** Number of active layers (1-3). */
private int layerCount = 1;
/** Layer data (always 3 slots allocated). */
private Layer[] layers = { new Layer(), new Layer(), new Layer() };
/** Per-state icon UV overrides for layer 0 (0-indexed, maps to toggle states 1, 2, ...). */
private int[][] stateIcons = null;
// Animation
private boolean animated = false;
private int frameCount = 1;
private int frametime = 2;
// ═══════════════════════════════════════════════════════════════════
// LAYER INNER CLASS
// ═══════════════════════════════════════════════════════════════════
public static class Layer {
public String texture = "";
public int iconX = 0;
public int iconY = 0;
public int tintColor = 0xFFFFFF;
public Layer() {}
public boolean hasTexture() {
return texture != null && !texture.isEmpty();
}
public void writeToNBT(INbt nbt) {
nbt.setString("Texture", texture);
nbt.setInteger("IconX", iconX);
nbt.setInteger("IconY", iconY);
nbt.setInteger("TintColor", tintColor);
}
public void readFromNBT(INbt nbt) {
texture = nbt.getString("Texture");
iconX = nbt.getInteger("IconX");
iconY = nbt.getInteger("IconY");
tintColor = nbt.hasKey("TintColor") ? nbt.getInteger("TintColor") : 0xFFFFFF;
}
}
// ═══════════════════════════════════════════════════════════════════
// CONSTRUCTION
// ═══════════════════════════════════════════════════════════════════
private AbilityIconData(INbt customData) {
this.customData = customData;
}
/**
* Create an AbilityIconData instance from a raw customData compound.
* Reads existing values if present, otherwise uses defaults.
*/
public static AbilityIconData fromCustomData(INbt customData) {
AbilityIconData icon = new AbilityIconData(customData);
if (customData.hasKey(NBT_KEY)) {
icon.readFromNBT(customData.getCompoundTag(NBT_KEY));
}
return icon;
}
/**
* Create an AbilityIconData instance from an ability's customData.
*/
public static AbilityIconData fromAbility(Ability ability) {
return fromCustomData(ability.getCustomData());
}
/**
* Create an AbilityIconData instance from a chained ability's customData.
*/
public static AbilityIconData fromChainedAbility(ChainedAbility chain) {
return fromCustomData(chain.getCustomData());
}
// ═══════════════════════════════════════════════════════════════════
// NBT SERIALIZATION
// ═══════════════════════════════════════════════════════════════════
/**
* Write current state back to the customData NBT.
*/
public void save() {
INbt tag = new INbt();
writeToNBT(tag);
customData.setTag(NBT_KEY, tag);
}
public void writeToNBT(INbt nbt) {
nbt.setBoolean("Enabled", enabled);
nbt.setInteger("Width", width);
nbt.setInteger("Height", height);
nbt.setFloat("Scale", scale);
nbt.setInteger("LayerCount", layerCount);
INbtList layerList = new INbtList();
for (int i = 0; i < MAX_LAYERS; i++) {
INbt layerNBT = new INbt();
layers[i].writeToNBT(layerNBT);
layerList.appendTag(layerNBT);
}
nbt.setTag("Layers", layerList);
nbt.setBoolean("Animated", animated);
nbt.setInteger("FrameCount", frameCount);
nbt.setInteger("FrameTime", frametime);
if (stateIcons != null && stateIcons.length > 0) {
INbtList list = new INbtList();
for (int[] pair : stateIcons) {
INbt comp = new INbt();
comp.setInteger("IconX", pair[0]);
comp.setInteger("IconY", pair[1]);
list.appendTag(comp);
}
nbt.setTag("StateIcons", list);
}
}
public void readFromNBT(INbt nbt) {
enabled = nbt.getBoolean("Enabled");
width = nbt.getInteger("Width");
if (width <= 0) width = 32;
height = nbt.getInteger("Height");
if (height <= 0) height = 32;
scale = nbt.getFloat("Scale");
if (scale <= 0) scale = 1.0f;
layerCount = nbt.getInteger("LayerCount");
if (layerCount < 1) layerCount = 1;
if (layerCount > MAX_LAYERS) layerCount = MAX_LAYERS;
if (nbt.hasKey("Animated"))
animated = nbt.getBoolean("Animated");
if (nbt.hasKey("FrameCount"))
frameCount = Math.max(1, nbt.getInteger("FrameCount"));
if (nbt.hasKey("FrameTime"))
frametime = Math.max(1, nbt.getInteger("FrameTime"));
if (nbt.hasKey("Layers")) {
INbtList layerList = nbt.getTagList("Layers", 10);
for (int i = 0; i < MAX_LAYERS; i++) {
layers[i] = new Layer();
if (i < layerList.tagCount()) {
layers[i].readFromNBT(layerList.getCompoundTagAt(i));
}
}
}
if (nbt.hasKey("StateIcons")) {
INbtList list = nbt.getTagList("StateIcons", 10);
stateIcons = new int[list.tagCount()][2];
for (int i = 0; i < list.tagCount(); i++) {
INbt comp = list.getCompoundTagAt(i);
stateIcons[i][0] = comp.getInteger("IconX");
stateIcons[i][1] = comp.getInteger("IconY");
}
}
}
// ═══════════════════════════════════════════════════════════════════
// LAYER API
// ═══════════════════════════════════════════════════════════════════
public int getLayerCount() {
return layerCount;
}
public void setLayerCount(int count) {
this.layerCount = Math.max(1, Math.min(MAX_LAYERS, count));
save();
}
public Layer getLayer(int index) {
return (index >= 0 && index < MAX_LAYERS) ? layers[index] : layers[0];
}
public void setLayerTexture(int index, String tex) {
if (index >= 0 && index < MAX_LAYERS) {
layers[index].texture = tex != null ? tex : "";
save();
}
}
public void setLayerIconX(int index, int x) {
if (index >= 0 && index < MAX_LAYERS) {
layers[index].iconX = Math.max(0, x);
save();
}
}
public void setLayerIconY(int index, int y) {
if (index >= 0 && index < MAX_LAYERS) {
layers[index].iconY = Math.max(0, y);
save();
}
}
public void setLayerTintColor(int index, int color) {
if (index >= 0 && index < MAX_LAYERS) {
layers[index].tintColor = color & 0xFFFFFF;
save();
}
}
// ═══════════════════════════════════════════════════════════════════
// SHARED IConfigProperty GETTERS/SETTERS (with auto-save)
// ═══════════════════════════════════════════════════════════════════
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
save();
}
public int getWidth() {
return width;
}
public void setWidth(int width) {
this.width = Math.max(1, width);
save();
}
public int getHeight() {
return height;
}
public void setHeight(int height) {
this.height = Math.max(1, height);
save();
}
public float getScale() {
return scale;
}
public void setScale(float scale) {
this.scale = Math.max(0.1f, scale);
save();
}
// ═══════════════════════════════════════════════════════════════════
// ANIMATION
// ═══════════════════════════════════════════════════════════════════
public boolean isAnimated() {
return animated;
}
public void setAnimated(boolean animated) {
this.animated = animated;
save();
}
public int getFrameCount() {
return frameCount;
}
public void setFrameCount(int frameCount) {
this.frameCount = Math.max(1, frameCount);
save();
}
public int getFrameTime() {
return frametime;
}
public void setFrameTime(int frametime) {
this.frametime = Math.max(1, frametime);
save();
}
// ═══════════════════════════════════════════════════════════════════
// PER-STATE ICON OVERRIDES (applies to layer 0)
// ═══════════════════════════════════════════════════════════════════
public void setStateIcons(int[][] stateIcons) {
this.stateIcons = stateIcons;
save();
}
/**
* Get iconX for a specific toggle state (layer 0).
* Falls back to layer 0 iconX if no state override exists.
* @param state 1-indexed toggle state (0 = off/default)
*/
public int getIconXForState(int state) {
if (state > 0 && stateIcons != null && state - 1 < stateIcons.length) {
return stateIcons[state - 1][0];
}
return layers[0].iconX;
}
/**
* Get iconY for a specific toggle state (layer 0).
* Falls back to layer 0 iconY if no state override exists.
* @param state 1-indexed toggle state (0 = off/default)
*/
public int getIconYForState(int state) {
if (state > 0 && stateIcons != null && state - 1 < stateIcons.length) {
return stateIcons[state - 1][1];
}
return layers[0].iconY;
}
}
@@ -0,0 +1,567 @@
package kamkeel.npcs.controllers.data.ability.data;
import noppes.npcs.api.entity.IEntityLiving;
import noppes.npcs.api.IDamageSource;
import noppes.npcs.api.IWorld;
import noppes.npcs.api.entity.IEntityLivingBase;
import noppes.npcs.api.item.IItemStack;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.api.INbtList;
import noppes.npcs.api.INbt;
import kamkeel.npcs.controllers.AbilityController;
import kamkeel.npcs.controllers.data.ability.Ability;
import noppes.npcs.controllers.data.ChainedAbilityScript;
import kamkeel.npcs.controllers.data.ability.data.entry.ChainedAbilityEntry;
import kamkeel.npcs.controllers.data.ability.enums.UserType;
import kamkeel.npcs.controllers.data.ability.conditions.AbilityCondition;
import noppes.npcs.api.ability.IChainedAbility;
import noppes.npcs.controllers.data.IScriptUnit;
import kamkeel.npcs.util.FileNameHelper;
import java.util.ArrayList;
import java.util.List;
/**
* A chained ability is an ordered sequence of ability references that execute
* one after another. It has its own conditions, cooldown, weight, and per-entry
* delay IConfiguration. Individual ability conditions are skipped during chain execution.
* <p>
* All entries must be ability references (by name/key). Inline abilities are not supported.
* When saving, all references are validated against {@link AbilityController}.
*/
public class ChainedAbility implements IChainedAbility, IAbilityAction {
// ═══════════════════════════════════════════════════════════════════
// IConfiguration (saved to NBT)
// ═══════════════════════════════════════════════════════════════════
private String id = ""; // UUID (stable persistent reference)
private String name = ""; // Unique file key / identifier
private String displayName = ""; // Cosmetic name (falls back to name when empty)
private boolean enabled = true;
/**
* Weight for NPC weighted random selection.
*/
private int weight = 10;
/**
* Whether each ability in the chain uses its own windup phase.
*/
private boolean windUpAll = true;
/**
* Chain-level cooldown in ticks (applied after entire chain completes).
*/
private int cooldownTicks = 100;
/**
* If true, only this chain goes on cooldown (not the global cooldown).
*/
private boolean perAbilityCooldown = false;
/**
* If true, skip cooldown entirely after chain completes.
*/
private boolean ignoreCooldown = false;
/**
* Minimum range for NPC eligibility check.
*/
private float minRange = 0;
/**
* Maximum range for NPC eligibility check.
*/
private float maxRange = 20;
/**
* Chain-level conditions (individual ability conditions are ignored).
*/
private List<AbilityCondition> conditions = new ArrayList<>();
/**
* Ordered list of ability entries to execute.
*/
private List<ChainedAbilityEntry> entries = new ArrayList<>();
/**
* Extension data for external mods (e.g., icons, DBC stats).
*/
private INbt customData = new INbt();
// Per-instance script (isolated per execution, not saved to NBT)
private transient ChainedAbilityScript instanceScript;
public ChainedAbility() {
}
public ChainedAbility(String name) {
setName(name);
}
// ═══════════════════════════════════════════════════════════════════
// GETTERS / SETTERS
// ═══════════════════════════════════════════════════════════════════
public String getId() {
return id;
}
public void setId(String id) {
this.id = id != null ? id : "";
}
public String getName() {
return name;
}
public void setName(String name) {
String fallback = (this.name != null && !this.name.isEmpty()) ? this.name : "Chain";
this.name = FileNameHelper.sanitizeName(name, fallback);
}
/**
* Get the display name for this chained ability.
* Returns displayName if set, otherwise falls back to name.
* Converts &amp; color codes to § for rendering.
*/
public String getDisplayName() {
String result = (displayName != null && !displayName.isEmpty())
? displayName
: FileNameHelper.toDisplayName(name);
return result != null ? result.replaceAll("&([0-9a-fk-or])", "\u00A7$1") : "";
}
public String getRawDisplayName() {
return displayName;
}
public void setDisplayName(String displayName) {
this.displayName = displayName != null ? displayName : "";
}
public boolean isEnabled() {
return enabled;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public int getWeight() {
return weight;
}
public void setWeight(int weight) {
this.weight = Math.max(1, weight);
}
public boolean isWindUpAll() {
return windUpAll;
}
public void setWindUpAll(boolean windUpAll) {
this.windUpAll = windUpAll;
}
public int getCooldownTicks() {
return cooldownTicks;
}
public void setCooldownTicks(int cooldownTicks) {
this.cooldownTicks = Math.max(0, cooldownTicks);
}
public boolean isPerAbilityCooldown() {
return perAbilityCooldown;
}
public void setPerAbilityCooldown(boolean perAbilityCooldown) {
this.perAbilityCooldown = perAbilityCooldown;
}
public boolean isIgnoreCooldown() {
return ignoreCooldown;
}
public void setIgnoreCooldown(boolean ignoreCooldown) {
this.ignoreCooldown = ignoreCooldown;
}
public float getMinRange() {
return minRange;
}
public void setMinRange(float minRange) {
this.minRange = Math.max(0, minRange);
}
public float getMaxRange() {
return maxRange;
}
public void setMaxRange(float maxRange) {
this.maxRange = Math.max(0, maxRange);
}
/**
* Compute the allowed user type from child abilities.
* A chain allows players if ALL resolved entries allow players.
* A chain allows NPCs if ALL resolved entries allow NPCs.
* Unresolvable entries are skipped (don't restrict).
*/
public UserType getAllowedBy() {
if (entries.isEmpty()) return UserType.BOTH;
boolean allAllowPlayer = true;
boolean allAllowNpc = true;
for (ChainedAbilityEntry entry : entries) {
Ability a = entry.resolve();
if (a == null) continue;
UserType ut = a.getAllowedBy();
if (!ut.allowsPlayer()) allAllowPlayer = false;
if (!ut.allowsNpc()) allAllowNpc = false;
}
if (allAllowPlayer && allAllowNpc) return UserType.BOTH;
if (allAllowPlayer) return UserType.PLAYER_ONLY;
if (allAllowNpc) return UserType.NPC_ONLY;
return UserType.NONE;
}
public List<AbilityCondition> getConditions() {
return conditions;
}
public void setConditions(List<AbilityCondition> conditions) {
this.conditions = conditions != null ? conditions : new ArrayList<>();
}
public List<ChainedAbilityEntry> getEntries() {
return entries;
}
public void setEntries(List<ChainedAbilityEntry> entries) {
this.entries = entries != null ? entries : new ArrayList<>();
}
public INbt getCustomData() {
return customData;
}
// ═══════════════════════════════════════════════════════════════════
// IChainedAbility INTERFACE
// ═══════════════════════════════════════════════════════════════════
@Override
public int getEntryCount() {
return entries.size();
}
@Override
public String getEntryReference(int index) {
if (index < 0 || index >= entries.size()) return null;
ChainedAbilityEntry entry = entries.get(index);
if (entry.isInline()) {
Ability a = entry.getInlineAbility();
return a != null ? a.getName() : "";
}
return entry.getAbilityReference();
}
@Override
public boolean isEntryInline(int index) {
if (index < 0 || index >= entries.size()) return false;
return entries.get(index).isInline();
}
@Override
public int getEntryDelay(int index) {
if (index < 0 || index >= entries.size()) return 0;
return entries.get(index).getDelayTicks();
}
// ═══════════════════════════════════════════════════════════════════
// ENTRY MANAGEMENT
// ═══════════════════════════════════════════════════════════════════
public void addEntry(ChainedAbilityEntry entry) {
if (entry != null) {
entries.add(entry);
}
}
public void addEntry(String abilityReference, int delayTicks) {
entries.add(new ChainedAbilityEntry(abilityReference, delayTicks));
}
public void removeEntry(int index) {
if (index >= 0 && index < entries.size()) {
entries.remove(index);
}
}
public void moveEntryUp(int index) {
if (index > 0 && index < entries.size()) {
ChainedAbilityEntry entry = entries.remove(index);
entries.add(index - 1, entry);
}
}
public void moveEntryDown(int index) {
if (index >= 0 && index < entries.size() - 1) {
ChainedAbilityEntry entry = entries.remove(index);
entries.add(index + 1, entry);
}
}
// ═══════════════════════════════════════════════════════════════════
// CONDITIONS
// ═══════════════════════════════════════════════════════════════════
/**
* Check all chain-level conditions against the caster and target.
*/
public boolean checkConditions(IEntityLivingBase caster, IEntityLivingBase target) {
for (AbilityCondition c : conditions) {
if (!c.getUserType().allowsNpc()) continue;
Boolean extendedCondition = AbilityController.Instance.fireCheckConditions(c, caster, target);
if (extendedCondition != null) {
if (!extendedCondition) return false;
continue;
}
if (!c.check(caster, target)) return false;
}
return true;
}
/**
* Check conditions for player use (skip target-requiring conditions).
*/
public boolean checkConditionsForPlayer(IEntityLivingBase caster) {
for (AbilityCondition c : conditions) {
if (!c.getUserType().allowsPlayer()) continue;
if (c.requiresTarget()) continue;
Boolean extendedCondition = AbilityController.Instance.fireCheckConditionsForPlayer(c, caster);
if (extendedCondition != null) {
if (!extendedCondition) return false;
continue;
}
if (!c.check(caster, null)) return false;
}
return true;
}
// ═══════════════════════════════════════════════════════════════════
// VALIDATION
// ═══════════════════════════════════════════════════════════════════
/**
* Validate this chained ability. Returns a list of error messages.
* An empty list means valid.
*/
public List<String> validate() {
List<String> errors = new ArrayList<>();
if (name == null || name.isEmpty()) {
errors.add("Name is required");
}
if (entries.isEmpty()) {
errors.add("At least one ability entry is required");
}
AbilityController controller = AbilityController.Instance;
if (controller != null) {
for (int i = 0; i < entries.size(); i++) {
ChainedAbilityEntry entry = entries.get(i);
if (entry.isInline()) {
if (entry.getInlineAbility() == null) {
errors.add("Entry " + (i + 1) + ": inline ability is null");
}
} else {
String ref = entry.getAbilityReference();
if (ref == null || ref.isEmpty()) {
errors.add("Entry " + (i + 1) + ": ability reference is empty");
} else if (!controller.canResolveAbility(ref)) {
errors.add("Entry " + (i + 1) + ": cannot resolve ability '" + ref + "'");
}
}
}
}
return errors;
}
// ═══════════════════════════════════════════════════════════════════
// DEEP COPY
// ═══════════════════════════════════════════════════════════════════
/**
* Create a deep copy via NBT round-trip.
*/
public ChainedAbility deepCopy() {
ChainedAbility copy = new ChainedAbility();
copy.readNBT(this.writeNBT(false));
return copy;
}
@Override
public boolean isChain() {
return true;
}
@Override
public IAbilityAction deepCopyAction() {
return deepCopy();
}
// ═══════════════════════════════════════════════════════════════════
// NBT SERIALIZATION
// ═══════════════════════════════════════════════════════════════════
public INbt writeNBT(boolean saveScripts) {
INbt nbt = new INbt();
nbt.setString("Id", id);
nbt.setString("Name", name);
nbt.setString("DisplayName", displayName);
nbt.setBoolean("Enabled", enabled);
nbt.setInteger("Weight", weight);
nbt.setBoolean("WindUpAll", windUpAll);
nbt.setInteger("CooldownTicks", cooldownTicks);
nbt.setBoolean("PerAbilityCooldown", perAbilityCooldown);
nbt.setBoolean("IgnoreCooldown", ignoreCooldown);
nbt.setFloat("MinRange", minRange);
nbt.setFloat("MaxRange", maxRange);
// Conditions
INbtList condList = new INbtList();
for (AbilityCondition c : conditions) {
condList.appendTag(c.writeNBT());
}
nbt.setTag("Conditions", condList);
// Entries
INbtList entryList = new INbtList();
for (ChainedAbilityEntry entry : entries) {
entryList.appendTag(entry.writeNBT(saveScripts));
}
nbt.setTag("Entries", entryList);
// Custom data (for external mods)
nbt.setTag("customData", (INbt) customData.copy());
// Script handler data (matching CustomEffect.writeToNBT pattern)
if (saveScripts) {
ChainedAbilityScript handler = getScriptHandler();
if (handler != null) {
INbt scriptData = new INbt();
handler.writeToNBT(scriptData);
nbt.setTag("ScriptData", scriptData);
}
}
return nbt;
}
public void readNBT(INbt nbt) {
id = nbt.getString("Id");
setName(nbt.getString("Name"));
displayName = nbt.getString("DisplayName");
enabled = !nbt.hasKey("Enabled") || nbt.getBoolean("Enabled");
weight = nbt.hasKey("Weight") ? nbt.getInteger("Weight") : 10;
windUpAll = !nbt.hasKey("WindUpAll") || nbt.getBoolean("WindUpAll");
cooldownTicks = nbt.hasKey("CooldownTicks") ? nbt.getInteger("CooldownTicks") : 100;
perAbilityCooldown = nbt.getBoolean("PerAbilityCooldown");
ignoreCooldown = nbt.getBoolean("IgnoreCooldown");
minRange = nbt.getFloat("MinRange");
maxRange = nbt.hasKey("MaxRange") ? nbt.getFloat("MaxRange") : 20;
// AllowedBy is computed from child abilities
// Conditions
conditions.clear();
INbtList condList = nbt.getTagList("Conditions", 10);
for (int i = 0; i < condList.tagCount(); i++) {
AbilityCondition c = AbilityCondition.fromNBT(condList.getCompoundTagAt(i));
if (c != null) {
conditions.add(c);
}
}
// Entries
entries.clear();
INbtList entryList = nbt.getTagList("Entries", 10);
for (int i = 0; i < entryList.tagCount(); i++) {
ChainedAbilityEntry entry = ChainedAbilityEntry.fromNBT(entryList.getCompoundTagAt(i));
if (entry != null) {
entries.add(entry);
}
}
// Custom data (for external mods)
customData = (INbt) nbt.getCompoundTag("customData").copy();
// Script handler data (matching CustomEffect.readFromNBT pattern)
if (nbt.hasKey("ScriptData", 10)) {
if (getScriptHandler() == null) {
ChainedAbilityScript handler = new ChainedAbilityScript(this.id);
handler.readFromNBT(nbt.getCompoundTag("ScriptData"));
setScriptHandler(handler);
}
}
}
// ═══════════════════════════════════════════════════════════════════
// SCRIPT HANDLER
// ═══════════════════════════════════════════════════════════════════
public ChainedAbilityScript getScriptHandler() {
return AbilityController.Instance.chainedAbilityScriptHandlers.get(this.id);
}
public void setScriptHandler(ChainedAbilityScript handler) {
AbilityController.Instance.chainedAbilityScriptHandlers.put(this.id, handler);
}
public ChainedAbilityScript getOrCreateScriptHandler() {
ChainedAbilityScript handler = getScriptHandler();
if (handler == null) {
handler = new ChainedAbilityScript(this.id);
AbilityController.Instance.chainedAbilityScriptHandlers.put(this.id, handler);
}
return handler;
}
/**
* Get or create a per-instance script handler for this chain execution.
* Shares the template's engine but creates isolated Bindings for variable state.
* Returns null if no script is configured on the template.
*/
public ChainedAbilityScript getOrCreateInstanceScript() {
if (instanceScript != null) return instanceScript;
ChainedAbilityScript template = getScriptHandler();
if (template == null || !template.getEnabled() || template.container == null) return null;
instanceScript = new ChainedAbilityScript(this.id);
instanceScript.setLanguage(template.getLanguage());
instanceScript.setEnabled(true);
IScriptUnit clone = template.container.createInstanceScope(instanceScript);
instanceScript.addScriptUnit(clone);
return instanceScript;
}
/**
* Clear the per-instance script handler (call on chain completion/reset).
*/
public void clearInstanceScript() {
this.instanceScript = null;
}
}
@@ -0,0 +1,70 @@
package kamkeel.npcs.controllers.data.ability.data;
import java.util.List;
import kamkeel.npcs.controllers.data.ability.Ability;
import kamkeel.npcs.controllers.data.ability.AbilityAction;
import kamkeel.npcs.controllers.data.ability.conditions.AbilityCondition;
import kamkeel.npcs.controllers.data.ability.enums.UserType;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.entity.IEntityLiving;
import noppes.npcs.api.entity.IEntityLivingBase;
import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.api.IDamageSource;
import noppes.npcs.api.INbt;
import noppes.npcs.api.INbtList;
import noppes.npcs.api.item.IItemStack;
import noppes.npcs.api.IWorld;
/**
* Common interface for all combat actions that can be assigned to NPCs or players.
* Implemented by {@link Ability} (individual ability) and {@link ChainedAbility} (sequence).
* <p>
* This enables unified storage via {@link AbilityAction}, unified weighted random selection,
* and unified eligibility checking without instanceof or separate codepaths.
*/
public interface IAbilityAction {
String getName();
boolean isEnabled();
void setEnabled(boolean enabled);
int getWeight();
int getCooldownTicks();
float getMinRange();
float getMaxRange();
UserType getAllowedBy();
List<AbilityCondition> getConditions();
boolean checkConditions(IEntityLivingBase caster, IEntityLivingBase target);
boolean checkConditionsForPlayer(IEntityLivingBase caster);
/**
* Check if this action is available for the given player.
* Used for visibility filtering (HUD, selection) and activation gating.
* Returns true by default (no requirement).
*/
default boolean isAvailableFor(IPlayer player) {
return true;
}
/**
* Whether this action is a chained ability (sequence of abilities) vs an individual ability.
*/
boolean isChain();
/**
* Create a deep copy of this action.
*/
IAbilityAction deepCopyAction();
INbt writeNBT(boolean saveScripts);
}
@@ -1,9 +1,18 @@
package kamkeel.npcs.controllers.data.ability.data;
import kamkeel.npcs.controllers.data.ability.enums.AnchorPoint;
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyAnchorData;
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyDisplayData;
import kamkeel.npcs.controllers.data.ability.enums.AnchorPoint;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.entity.IEntityLiving;
import noppes.npcs.api.entity.IEntityLivingBase;
import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.api.IDamageSource;
import noppes.npcs.api.INbt;
import noppes.npcs.api.INbtList;
import noppes.npcs.api.item.IItemStack;
import noppes.npcs.api.IWorld;
/**
* Per-projectile data: anchor point + optional color override.
@@ -26,7 +35,7 @@ public class ProjectileData {
* If colorOverride is true, returns a copy with overridden inner/outer colors.
*/
public EnergyDisplayData resolveDisplay(EnergyDisplayData primary) {
// Always return a copy so entity mutations never bleed back into the ability template.
// Always return a copy so IEntity mutations never bleed back into the ability template.
EnergyDisplayData resolved = primary.copy();
if (colorOverride) {
resolved.innerColor = this.innerColor;
@@ -0,0 +1,118 @@
package kamkeel.npcs.controllers.data.ability.data.effect;
import noppes.npcs.api.entity.IEntityLiving;
import noppes.npcs.api.IDamageSource;
import noppes.npcs.api.IWorld;
import noppes.npcs.api.entity.IEntityLivingBase;
import noppes.npcs.api.item.IItemStack;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.api.INbtList;
import noppes.npcs.api.INbt;
import noppes.npcs.controllers.CustomEffectController;
import noppes.npcs.util.ValueUtil;
/**
* Represents a custom effect reference that can be applied by abilities.
* Wraps the CustomEffectController system (player-only effects with script callbacks).
*/
public class AbilityCustomEffect {
private int effectId = -1;
private int durationTicks = 60;
private byte level = 0;
private int index = 0;
public AbilityCustomEffect() {
}
public AbilityCustomEffect(int effectId, int durationTicks, byte level) {
this.effectId = effectId;
this.durationTicks = Math.max(1, durationTicks);
this.level = (byte) Math.max(0, Math.min(10, level));
this.index = 0;
}
public AbilityCustomEffect(int effectId, int durationTicks, byte level, int index) {
this(effectId, durationTicks, level);
this.index = index;
}
public AbilityCustomEffect copy() {
return new AbilityCustomEffect(effectId, durationTicks, level, index);
}
/**
* Applies this custom effect to the given IEntity.
* Only works on players (CustomEffectController is player-only).
*/
public void apply(IEntityLivingBase IEntity) {
if (IEntity == null || effectId <= 0) return;
if (IEntity instanceof IPlayer) {
CustomEffectController.getInstance().applyEffect(
(IPlayer) IEntity, effectId, durationTicks, level, index);
}
}
public boolean isValid() {
return effectId > 0;
}
// ── NBT ──
public INbt writeNBT() {
INbt nbt = new INbt();
nbt.setInteger("effectId", effectId);
nbt.setInteger("duration", durationTicks);
nbt.setByte("level", level);
nbt.setInteger("index", index);
return nbt;
}
public void readNBT(INbt nbt) {
this.effectId = nbt.getInteger("effectId");
this.durationTicks = nbt.getInteger("duration");
this.level = nbt.getByte("level");
this.index = nbt.getInteger("index");
}
public static AbilityCustomEffect fromNBT(INbt nbt) {
AbilityCustomEffect e = new AbilityCustomEffect();
e.readNBT(nbt);
return e;
}
// ── Getters/Setters ──
public int getEffectId() {
return effectId;
}
public void setEffectId(int effectId) {
this.effectId = effectId;
}
public int getDurationTicks() {
return durationTicks;
}
public void setDurationTicks(int durationTicks) {
this.durationTicks = Math.max(1, durationTicks);
}
public byte getLevel() {
return level;
}
public void setLevel(byte level) {
this.level = (byte) ValueUtil.clamp(level, 0, 10);
}
public int getIndex() {
return index;
}
public void setIndex(int index) {
this.index = Math.max(0, index);
}
}
@@ -0,0 +1,138 @@
package kamkeel.npcs.controllers.data.ability.data.effect;
import noppes.npcs.api.entity.IEntityLiving;
import noppes.npcs.api.IDamageSource;
import noppes.npcs.api.IWorld;
import noppes.npcs.api.entity.IEntityLivingBase;
import noppes.npcs.api.item.IItemStack;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.api.INbtList;
import noppes.npcs.api.INbt;
import noppes.npcs.constants.EnumPotionType;
/**
* Represents a configurable IPotionType effect that can be applied by abilities.
* Uses the unified EnumPotionType enum shared across melee, ranged, and ability systems.
*/
public class AbilityPotionEffect {
private EnumPotionType type = EnumPotionType.None;
private int manualPotionId = 0;
private int durationTicks = 60;
private int amplifier = 0; // 0-255 (Manual), 0-10 (preset)
public AbilityPotionEffect() {
}
public AbilityPotionEffect(EnumPotionType type, int durationTicks, int amplifier) {
this.type = type;
this.durationTicks = Math.max(1, durationTicks);
this.amplifier = Math.max(0, Math.min(255, amplifier));
}
/**
* Creates a copy of this effect.
*/
public AbilityPotionEffect copy() {
AbilityPotionEffect copy = new AbilityPotionEffect(type, durationTicks, amplifier);
copy.manualPotionId = this.manualPotionId;
return copy;
}
/**
* Applies this effect to the given IEntity.
* Does nothing if type is None. Validates IPotionType IDs before applying.
*/
public void apply(IEntityLivingBase IEntity) {
if (IEntity == null || type == EnumPotionType.None) {
return;
}
if (type == EnumPotionType.Fire) {
IEntity.setFire(Math.max(1, durationTicks / 20));
return;
}
int potionId = type.getResolvedPotionId(manualPotionId);
if (EnumPotionType.isValidPotionId(potionId)) {
IEntity.addPotionEffect(new IPotionEffect(potionId, durationTicks, amplifier));
}
}
/**
* Writes this effect to NBT.
*/
public INbt writeNBT() {
INbt nbt = new INbt();
nbt.setInteger("potionType", type.ordinal());
nbt.setInteger("duration", durationTicks);
nbt.setInteger("amplifier", amplifier);
if (type == EnumPotionType.Manual) {
nbt.setInteger("manualPotionId", manualPotionId);
}
return nbt;
}
/**
* Reads this effect from NBT.
*/
public void readNBT(INbt nbt) {
this.type = EnumPotionType.fromOrdinal(nbt.getInteger("potionType"));
this.durationTicks = nbt.getInteger("duration");
this.amplifier = nbt.getInteger("amplifier");
if (type == EnumPotionType.Manual) {
this.manualPotionId = nbt.getInteger("manualPotionId");
}
}
/**
* Creates an AbilityPotionEffect from NBT.
*/
public static AbilityPotionEffect fromNBT(INbt nbt) {
AbilityPotionEffect effect = new AbilityPotionEffect();
effect.readNBT(nbt);
return effect;
}
// Getters & Setters
public EnumPotionType getType() {
return type;
}
public void setType(EnumPotionType type) {
this.type = type != null ? type : EnumPotionType.None;
}
public int getManualPotionId() {
return manualPotionId;
}
public void setManualPotionId(int manualPotionId) {
this.manualPotionId = Math.max(0, manualPotionId);
}
public int getDurationTicks() {
return durationTicks;
}
public void setDurationTicks(int durationTicks) {
this.durationTicks = Math.max(1, durationTicks);
}
public int getAmplifier() {
return amplifier;
}
public void setAmplifier(int amplifier) {
this.amplifier = Math.max(0, Math.min(255, amplifier));
}
/**
* Returns true if this effect is configured (not None).
*/
public boolean isValid() {
return type != EnumPotionType.None;
}
}
@@ -0,0 +1,50 @@
package kamkeel.npcs.controllers.data.ability.data.effect;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.entity.IEntityLiving;
import noppes.npcs.api.entity.IEntityLivingBase;
import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.api.IDamageSource;
import noppes.npcs.api.INbt;
import noppes.npcs.api.INbtList;
import noppes.npcs.api.item.IItemStack;
import noppes.npcs.api.IWorld;
/**
* Interface for mod-registered effect actions that can be applied by AbilityEffect.
* <p>
* Mods register implementations via {@code Register.EffectActions} or
* {@code AbilityController.Instance.registerEffectAction()}.
* Each action has a unique ID, display name, and apply logic.
* Actions can optionally provide custom IConfiguration fields for the GUI.
*/
public interface IEffectAction {
/**
* Unique identifier, e.g. "dbc:kiRestore", "mymod:buffSpeed".
*/
String getId();
/**
* Localized display name shown in the GUI selector.
*/
String getDisplayName();
/**
* Apply this action to a target IEntity.
* Called once per affected IEntity during ability execution.
*
* @param caster The IEntity that used the ability
* @param target The IEntity being affected
* @param config Action-specific IConfiguration NBT (from GUI)
*/
void apply(IEntityLivingBase caster, IEntityLivingBase target, INbt config);
/**
* Create default IConfiguration NBT for a new instance of this action.
*/
default INbt createDefaultConfig() {
return new INbt();
}
}
@@ -1,11 +1,19 @@
package kamkeel.npcs.controllers.data.ability.data.energy;
import noppes.npcs.core.NBT;
import noppes.npcs.api.INbt;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.entity.IEntityLiving;
import noppes.npcs.api.entity.IEntityLivingBase;
import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.api.IDamageSource;
import noppes.npcs.api.INbt;
import noppes.npcs.api.INbtList;
import noppes.npcs.api.item.IItemStack;
import noppes.npcs.api.IWorld;
import noppes.npcs.core.NBT;
/**
* Groups barrier properties shared by energy barrier abilities (Dome, Wall, Shield).
@@ -37,7 +45,7 @@ public class EnergyBarrierData {
// Absorb radius: max distance from barrier's current position for absorption to work
// -1 = no radius limit (absorb regardless of distance)
// 0 = use barrier's own extent as the radius (dome radius, panel half-size, etc.)
// positive = owner must be within this many blocks of the barrier entity
// positive = owner must be within this many blocks of the barrier IEntity
public float absorbRadius = 0.0f;
// Melee: barrier can be hit by melee attacks
@@ -1,15 +1,24 @@
package kamkeel.npcs.controllers.data.ability.data.energy;
import kamkeel.npcs.controllers.data.ability.enums.HitType;
import noppes.npcs.api.ability.data.IEnergyCombatData;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.entity.IEntityLiving;
import noppes.npcs.api.entity.IEntityLivingBase;
import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.api.IDamageSource;
import noppes.npcs.api.INbt;
import noppes.npcs.api.INbtList;
import noppes.npcs.api.item.IItemStack;
import noppes.npcs.api.IWorld;
/**
* Groups combat properties shared by energy projectile abilities.
* Used as a parameter object for entity constructors and ability configuration.
* Used as a parameter object for IEntity constructors and ability IConfiguration.
*/
public class EnergyCombatData implements IEnergyCombatData {
public static final float MAX_EXPLOSION_RADIUS = 15.0f;
public static final float MAX_IExplosion_RADIUS = 15.0f;
public static final int DEFAULT_MAX_HITS = 5;
public static final int MAX_HITS = 200;
@@ -17,8 +26,8 @@ public class EnergyCombatData implements IEnergyCombatData {
public float knockback = 1.0f;
public float knockbackUp = 0.1f;
public boolean explosive = false;
public float explosionRadius = 3.0f;
public float explosionDamageFalloff = 0.5f;
public float IExplosionRadius = 3.0f;
public float IExplosionDamageFalloff = 0.5f;
public HitType hitType = HitType.SINGLE;
public int multiHitDelayTicks = 5;
public int maxHits = DEFAULT_MAX_HITS;
@@ -27,33 +36,33 @@ public class EnergyCombatData implements IEnergyCombatData {
}
public EnergyCombatData(float damage, float knockback, float knockbackUp,
boolean explosive, float explosionRadius, float explosionDamageFalloff,
boolean explosive, float IExplosionRadius, float IExplosionDamageFalloff,
HitType hitType, int multiHitDelay) {
this(damage, knockback, knockbackUp, explosive, explosionRadius, explosionDamageFalloff, hitType, multiHitDelay, DEFAULT_MAX_HITS);
this(damage, knockback, knockbackUp, explosive, IExplosionRadius, IExplosionDamageFalloff, hitType, multiHitDelay, DEFAULT_MAX_HITS);
}
public EnergyCombatData(float damage, float knockback, float knockbackUp,
boolean explosive, float explosionRadius, float explosionDamageFalloff,
boolean explosive, float IExplosionRadius, float IExplosionDamageFalloff,
HitType hitType, int multiHitDelay, int maxHits) {
this.damage = damage;
this.knockback = knockback;
this.knockbackUp = knockbackUp;
this.explosive = explosive;
this.explosionRadius = clampExplosionRadius(explosionRadius);
this.explosionDamageFalloff = explosionDamageFalloff;
this.IExplosionRadius = clampIExplosionRadius(IExplosionRadius);
this.IExplosionDamageFalloff = IExplosionDamageFalloff;
this.hitType = hitType;
this.multiHitDelayTicks = multiHitDelay;
this.maxHits = clampMaxHits(maxHits);
}
public EnergyCombatData(float damage, float knockback, float knockbackUp,
boolean explosive, float explosionRadius, float explosionDamageFalloff) {
boolean explosive, float IExplosionRadius, float IExplosionDamageFalloff) {
this.damage = damage;
this.knockback = knockback;
this.knockbackUp = knockbackUp;
this.explosive = explosive;
this.explosionRadius = clampExplosionRadius(explosionRadius);
this.explosionDamageFalloff = explosionDamageFalloff;
this.IExplosionRadius = clampIExplosionRadius(IExplosionRadius);
this.IExplosionDamageFalloff = IExplosionDamageFalloff;
}
@Override
@@ -97,23 +106,23 @@ public class EnergyCombatData implements IEnergyCombatData {
}
@Override
public float getExplosionRadius() {
return explosionRadius;
public float getIExplosionRadius() {
return IExplosionRadius;
}
@Override
public void setExplosionRadius(float explosionRadius) {
this.explosionRadius = clampExplosionRadius(explosionRadius);
public void setIExplosionRadius(float IExplosionRadius) {
this.IExplosionRadius = clampIExplosionRadius(IExplosionRadius);
}
@Override
public float getExplosionDamageFalloff() {
return explosionDamageFalloff;
public float getIExplosionDamageFalloff() {
return IExplosionDamageFalloff;
}
@Override
public void setExplosionDamageFalloff(float explosionDamageFalloff) {
this.explosionDamageFalloff = explosionDamageFalloff;
public void setIExplosionDamageFalloff(float IExplosionDamageFalloff) {
this.IExplosionDamageFalloff = IExplosionDamageFalloff;
}
public int getMaxHits() {
@@ -129,8 +138,8 @@ public class EnergyCombatData implements IEnergyCombatData {
nbt.setFloat("knockback", knockback);
nbt.setFloat("knockbackUp", knockbackUp);
nbt.setBoolean("explosive", explosive);
nbt.setFloat("explosionRadius", explosionRadius);
nbt.setFloat("explosionDamageFalloff", explosionDamageFalloff);
nbt.setFloat("IExplosionRadius", IExplosionRadius);
nbt.setFloat("IExplosionDamageFalloff", IExplosionDamageFalloff);
nbt.setInteger("hitType", hitType.ordinal());
nbt.setInteger("multiHitDelayTicks", multiHitDelayTicks);
nbt.setInteger("maxHits", maxHits);
@@ -141,8 +150,8 @@ public class EnergyCombatData implements IEnergyCombatData {
knockback = nbt.hasKey("knockback") ? nbt.getFloat("knockback") : 1.0f;
knockbackUp = nbt.hasKey("knockbackUp") ? nbt.getFloat("knockbackUp") : 0.1f;
explosive = nbt.hasKey("explosive") && nbt.getBoolean("explosive");
explosionRadius = nbt.hasKey("explosionRadius") ? nbt.getFloat("explosionRadius") : 3.0f;
explosionDamageFalloff = nbt.hasKey("explosionDamageFalloff") ? nbt.getFloat("explosionDamageFalloff") : 0.5f;
IExplosionRadius = nbt.hasKey("IExplosionRadius") ? nbt.getFloat("IExplosionRadius") : 3.0f;
IExplosionDamageFalloff = nbt.hasKey("IExplosionDamageFalloff") ? nbt.getFloat("IExplosionDamageFalloff") : 0.5f;
hitType = HitType.fromOrdinal(nbt.hasKey("hitType") ? nbt.getInteger("hitType") : 0);
multiHitDelayTicks = nbt.hasKey("multiHitDelayTicks") ? nbt.getInteger("multiHitDelayTicks") : 5;
maxHits = nbt.hasKey("maxHits") ? nbt.getInteger("maxHits") : DEFAULT_MAX_HITS;
@@ -150,15 +159,15 @@ public class EnergyCombatData implements IEnergyCombatData {
// Sanitize
if (Float.isNaN(damage) || Float.isInfinite(damage)) damage = 7.0f;
if (Float.isNaN(knockback) || Float.isInfinite(knockback) || knockback < 0) knockback = 1.0f;
if (Float.isNaN(explosionRadius) || Float.isInfinite(explosionRadius) || explosionRadius < 0) explosionRadius = 3.0f;
explosionRadius = clampExplosionRadius(explosionRadius);
if (Float.isNaN(IExplosionRadius) || Float.isInfinite(IExplosionRadius) || IExplosionRadius < 0) IExplosionRadius = 3.0f;
IExplosionRadius = clampIExplosionRadius(IExplosionRadius);
if (multiHitDelayTicks < 1) multiHitDelayTicks = 1;
maxHits = clampMaxHits(maxHits);
}
private static float clampExplosionRadius(float explosionRadius) {
if (Float.isNaN(explosionRadius) || Float.isInfinite(explosionRadius)) return 0.0f;
return Math.max(0.0f, Math.min(MAX_EXPLOSION_RADIUS, explosionRadius));
private static float clampIExplosionRadius(float IExplosionRadius) {
if (Float.isNaN(IExplosionRadius) || Float.isInfinite(IExplosionRadius)) return 0.0f;
return Math.max(0.0f, Math.min(MAX_IExplosion_RADIUS, IExplosionRadius));
}
private static int clampMaxHits(int maxHits) {
@@ -167,7 +176,7 @@ public class EnergyCombatData implements IEnergyCombatData {
public EnergyCombatData copy() {
EnergyCombatData copy = new EnergyCombatData(damage, knockback, knockbackUp,
explosive, explosionRadius, explosionDamageFalloff);
explosive, IExplosionRadius, IExplosionDamageFalloff);
copy.hitType = hitType;
copy.multiHitDelayTicks = multiHitDelayTicks;
copy.maxHits = maxHits;
@@ -1,11 +1,20 @@
package kamkeel.npcs.controllers.data.ability.data.energy;
import noppes.npcs.api.ability.data.IEnergyDisplayData;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.entity.IEntityLiving;
import noppes.npcs.api.entity.IEntityLivingBase;
import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.api.IDamageSource;
import noppes.npcs.api.INbt;
import noppes.npcs.api.INbtList;
import noppes.npcs.api.item.IItemStack;
import noppes.npcs.api.IWorld;
/**
* Groups visual color properties shared by all energy abilities.
* Used as a parameter object for entity constructors and ability configuration.
* Used as a parameter object for IEntity constructors and ability IConfiguration.
*/
public class EnergyDisplayData implements IEnergyDisplayData {
public int innerColor = 0xFFFFFF;
@@ -1,11 +1,20 @@
package kamkeel.npcs.controllers.data.ability.data.energy;
import noppes.npcs.api.ability.data.IEnergyLifespanData;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.entity.IEntityLiving;
import noppes.npcs.api.entity.IEntityLivingBase;
import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.api.IDamageSource;
import noppes.npcs.api.INbt;
import noppes.npcs.api.INbtList;
import noppes.npcs.api.item.IItemStack;
import noppes.npcs.api.IWorld;
/**
* Groups lifespan/range properties for energy projectile abilities.
* Used as a parameter object for entity constructors and ability configuration.
* Used as a parameter object for IEntity constructors and ability IConfiguration.
*/
public class EnergyLifespanData implements IEnergyLifespanData {
public float maxDistance = 150.0f;
@@ -1,11 +1,20 @@
package kamkeel.npcs.controllers.data.ability.data.energy;
import noppes.npcs.api.ability.data.IEnergyLightningData;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.entity.IEntityLiving;
import noppes.npcs.api.entity.IEntityLivingBase;
import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.api.IDamageSource;
import noppes.npcs.api.INbt;
import noppes.npcs.api.INbtList;
import noppes.npcs.api.item.IItemStack;
import noppes.npcs.api.IWorld;
/**
* Groups lightning visual effect properties for energy abilities.
* Used as a parameter object for entity constructors and ability configuration.
* Used as a parameter object for IEntity constructors and ability IConfiguration.
*/
public class EnergyLightningData implements IEnergyLightningData {
public boolean lightningEffect = false;
@@ -0,0 +1,15 @@
package kamkeel.npcs.controllers.data.ability.data.energy;
import noppes.npcs.api.INbt;
/**
* TEMPORARY STUB -- auto-generated for compilation.
* Panel data for energy panel entities.
*/
public class PanelData {
public int panelCount = 1;
public float panelSize = 1.0f;
public float panelSpacing = 0.5f;
public void readNBT(INbt nbt) {}
public INbt writeNBT(INbt nbt) { return nbt; }
}
@@ -0,0 +1,93 @@
package kamkeel.npcs.controllers.data.ability.data.entry;
import noppes.npcs.api.entity.IEntityLiving;
import noppes.npcs.api.IDamageSource;
import noppes.npcs.api.IWorld;
import noppes.npcs.api.entity.IEntityLivingBase;
import noppes.npcs.api.item.IItemStack;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.api.INbtList;
import noppes.npcs.api.INbt;
import kamkeel.npcs.controllers.AbilityController;
import kamkeel.npcs.controllers.data.ability.data.effect.IEffectAction;
/**
* A configured instance of an {@link IEffectAction} stored on an ability.
* Holds the action ID and its IConfiguration NBT.
*/
public class AbilityEffectActionEntry {
private String actionId = "";
private INbt config = new INbt();
public AbilityEffectActionEntry() {
}
public AbilityEffectActionEntry(String actionId) {
this.actionId = actionId != null ? actionId : "";
IEffectAction action = AbilityController.Instance.getEffectAction(this.actionId);
if (action != null) {
this.config = action.createDefaultConfig();
}
}
public AbilityEffectActionEntry copy() {
AbilityEffectActionEntry copy = new AbilityEffectActionEntry();
copy.actionId = this.actionId;
copy.config = (INbt) this.config.copy();
return copy;
}
/**
* Apply this action to a target IEntity.
*/
public void apply(IEntityLivingBase caster, IEntityLivingBase target) {
if (actionId.isEmpty()) return;
IEffectAction action = AbilityController.Instance.getEffectAction(actionId);
if (action != null) {
action.apply(caster, target, config);
}
}
public boolean isValid() {
return !actionId.isEmpty() && AbilityController.Instance.getEffectAction(actionId) != null;
}
// ── NBT ──
public INbt writeNBT() {
INbt nbt = new INbt();
nbt.setString("actionId", actionId);
nbt.setTag("config", config);
return nbt;
}
public void readNBT(INbt nbt) {
this.actionId = nbt.getString("actionId");
this.config = nbt.hasKey("config") ? nbt.getCompoundTag("config") : new INbt();
}
public static AbilityEffectActionEntry fromNBT(INbt nbt) {
AbilityEffectActionEntry entry = new AbilityEffectActionEntry();
entry.readNBT(nbt);
return entry;
}
// ── Getters/Setters ──
public String getActionId() {
return actionId;
}
public void setActionId(String actionId) {
this.actionId = actionId != null ? actionId : "";
}
public INbt getConfig() {
return config;
}
public void setConfig(INbt config) {
this.config = config != null ? config : new INbt();
}
}
@@ -0,0 +1,51 @@
package kamkeel.npcs.controllers.data.ability.data.entry;
import noppes.npcs.api.entity.IEntityLiving;
import noppes.npcs.api.IDamageSource;
import noppes.npcs.api.IWorld;
import noppes.npcs.api.entity.IEntityLivingBase;
import noppes.npcs.api.item.IItemStack;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.api.INbtList;
import noppes.npcs.api.INbt;
import kamkeel.npcs.controllers.data.ability.Ability;
/**
* Tracks the state of a single active toggle for an IEntity.
* Stored in AbstractDataAbilities.activeToggles map.
* <p>
* State is 1-based: state 1 = first ON state, state 2 = second, etc.
* State 0 means OFF (entry should not exist in the map).
*/
public class AbilityToggleEntry {
private final Ability ability;
private int tickCount;
private int state;
public AbilityToggleEntry(Ability ability, int state) {
this.ability = ability;
this.tickCount = 0;
this.state = Math.max(1, state);
}
public Ability getAbility() {
return ability;
}
public int getTickCount() {
return tickCount;
}
public void incrementTick() {
tickCount++;
}
public int getState() {
return state;
}
public void setState(int state) {
this.state = Math.max(1, state);
}
}
@@ -0,0 +1,230 @@
package kamkeel.npcs.controllers.data.ability.data.entry;
import noppes.npcs.api.entity.IEntityLiving;
import noppes.npcs.api.IDamageSource;
import noppes.npcs.api.IWorld;
import noppes.npcs.api.entity.IEntityLivingBase;
import noppes.npcs.api.item.IItemStack;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.api.INbtList;
import noppes.npcs.api.INbt;
import kamkeel.npcs.controllers.AbilityController;
import kamkeel.npcs.controllers.data.ability.Ability;
import kamkeel.npcs.controllers.data.ability.data.ChainedAbility;
import java.util.UUID;
/**
* A single entry in a {@link ChainedAbility}, representing one ability
* to execute in sequence along with its delay IConfiguration.
* <p>
* Entries can be either a <b>reference</b> to a registered ability (by name/key)
* or an <b>inline</b> ability that is owned directly by this entry.
*/
public class ChainedAbilityEntry {
public enum EntryType {REFERENCE, INLINE}
private EntryType entryType = EntryType.REFERENCE;
/**
* Reference key (built-in name or custom ability name). Used when entryType == REFERENCE.
*/
private String abilityReference = "";
/**
* Inline ability data. Used when entryType == INLINE.
*/
private Ability inlineAbility = null;
/**
* Delay in ticks BEFORE this ability starts (after previous entry completes).
*/
private int delayTicks = 0;
/**
* Whether this entry should execute concurrently (alongside the previous ability)
* rather than sequentially. Only takes effect if the resolved ability isConcurrentCapable().
*/
private boolean concurrentEnabled = true;
public ChainedAbilityEntry() {
}
public ChainedAbilityEntry(String abilityReference, int delayTicks) {
this.entryType = EntryType.REFERENCE;
this.abilityReference = abilityReference != null ? abilityReference : "";
this.delayTicks = Math.max(0, delayTicks);
}
// ═══════════════════════════════════════════════════════════════════
// FACTORY METHODS
// ═══════════════════════════════════════════════════════════════════
public static ChainedAbilityEntry reference(String ref, int delay) {
ChainedAbilityEntry entry = new ChainedAbilityEntry();
entry.entryType = EntryType.REFERENCE;
entry.abilityReference = ref != null ? ref : "";
entry.delayTicks = Math.max(0, delay);
return entry;
}
public static ChainedAbilityEntry inline(Ability ability, int delay) {
ChainedAbilityEntry entry = new ChainedAbilityEntry();
entry.entryType = EntryType.INLINE;
entry.inlineAbility = ability;
entry.delayTicks = Math.max(0, delay);
return entry;
}
// ═══════════════════════════════════════════════════════════════════
// TYPE QUERIES
// ═══════════════════════════════════════════════════════════════════
public EntryType getEntryType() {
return entryType;
}
public boolean isInline() {
return entryType == EntryType.INLINE;
}
public boolean isReference() {
return entryType == EntryType.REFERENCE;
}
// ═══════════════════════════════════════════════════════════════════
// GETTERS / SETTERS
// ═══════════════════════════════════════════════════════════════════
public String getAbilityReference() {
return abilityReference;
}
public void setAbilityReference(String abilityReference) {
this.abilityReference = abilityReference != null ? abilityReference : "";
}
public Ability getInlineAbility() {
return inlineAbility;
}
public void setInlineAbility(Ability ability) {
this.inlineAbility = ability;
}
public int getDelayTicks() {
return delayTicks;
}
public void setDelayTicks(int delayTicks) {
this.delayTicks = Math.max(0, delayTicks);
}
public boolean isConcurrentEnabled() {
return concurrentEnabled;
}
public void setConcurrentEnabled(boolean concurrentEnabled) {
this.concurrentEnabled = concurrentEnabled;
}
// ═══════════════════════════════════════════════════════════════════
// RESOLUTION
// ═══════════════════════════════════════════════════════════════════
/**
* Resolve this entry to an Ability instance.
* Inline entries return their ability directly.
* Reference entries resolve via AbilityController.
*/
public Ability resolve() {
if (entryType == EntryType.INLINE) {
return inlineAbility;
}
AbilityController ctrl = AbilityController.Instance;
if (ctrl == null) return null;
return ctrl.resolveAbility(abilityReference);
}
// ═══════════════════════════════════════════════════════════════════
// CONVERSION
// ═══════════════════════════════════════════════════════════════════
/**
* Convert a reference entry to inline by cloning the resolved ability.
* Returns false if already inline or the reference cannot be resolved.
*/
public boolean convertToInline() {
if (entryType != EntryType.REFERENCE) return false;
AbilityController ctrl = AbilityController.Instance;
if (ctrl == null) return false;
Ability resolved = ctrl.resolveAbility(abilityReference);
if (resolved == null) return false;
INbt nbt = resolved.writeNBT(true);
nbt.setString("id", UUID.randomUUID().toString());
inlineAbility = ctrl.fromNBT(nbt);
entryType = EntryType.INLINE;
abilityReference = "";
return true;
}
// ═══════════════════════════════════════════════════════════════════
// DEEP COPY
// ═══════════════════════════════════════════════════════════════════
/**
* Create a deep copy of this entry. Inline abilities are copied via NBT round-trip.
*/
public ChainedAbilityEntry deepCopy() {
ChainedAbilityEntry copy = new ChainedAbilityEntry();
copy.entryType = this.entryType;
copy.abilityReference = this.abilityReference;
copy.delayTicks = this.delayTicks;
copy.concurrentEnabled = this.concurrentEnabled;
if (this.inlineAbility != null) {
copy.inlineAbility = AbilityController.Instance.fromNBT(this.inlineAbility.writeNBT(true));
}
return copy;
}
// ═══════════════════════════════════════════════════════════════════
// NBT
// ═══════════════════════════════════════════════════════════════════
public INbt writeNBT(boolean saveScripts) {
INbt nbt = new INbt();
nbt.setInteger("Delay", delayTicks);
nbt.setBoolean("Concurrent", concurrentEnabled);
if (entryType == EntryType.INLINE && inlineAbility != null) {
nbt.setTag("InlineAbility", inlineAbility.writeNBT(saveScripts));
} else {
nbt.setString("Reference", abilityReference);
}
return nbt;
}
public static ChainedAbilityEntry fromNBT(INbt nbt) {
if (nbt == null) return null;
ChainedAbilityEntry entry = new ChainedAbilityEntry();
entry.delayTicks = Math.max(0, nbt.getInteger("Delay"));
entry.concurrentEnabled = nbt.hasKey("Concurrent") ? nbt.getBoolean("Concurrent") : true;
if (nbt.hasKey("InlineAbility")) {
INbt abilityNBT = nbt.getCompoundTag("InlineAbility");
entry.entryType = EntryType.INLINE;
entry.inlineAbility = AbilityController.Instance != null
? AbilityController.Instance.fromNBT(abilityNBT) : null;
} else {
entry.entryType = EntryType.REFERENCE;
entry.abilityReference = nbt.getString("Reference");
}
return entry;
}
}
@@ -1,15 +1,26 @@
package kamkeel.npcs.controllers.data.ability.enums;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.entity.IEntityLiving;
import noppes.npcs.api.entity.IEntityLivingBase;
import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.api.IDamageSource;
import noppes.npcs.api.INbt;
import noppes.npcs.api.INbtList;
import noppes.npcs.api.item.IItemStack;
import noppes.npcs.api.IWorld;
/**
* Defines anchor points for ability charging effects.
* Determines where the charging visual (orb, disc, etc.) appears relative to the entity.
* Determines where the charging visual (orb, disc, etc.) appears relative to the IEntity.
*/
public enum AnchorPoint {
FRONT("Front"), // In front of entity face (default)
CENTER("Center"), // Entity center
FRONT("Front"), // In front of IEntity face (default)
CENTER("Center"), // IEntity center
RIGHT_HAND("Right Hand"), // Right arm/hand position
LEFT_HAND("Left Hand"), // Left arm/hand position
ABOVE_HEAD("Above Head"), // Above the entity
ABOVE_HEAD("Above Head"), // Above the IEntity
CHEST("Chest"), // Chest level, centered
EYE("Eye"); // Eye level, centered
@@ -1,7 +1,18 @@
package kamkeel.npcs.controllers.data.ability.enums;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.entity.IEntityLiving;
import noppes.npcs.api.entity.IEntityLivingBase;
import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.api.IDamageSource;
import noppes.npcs.api.INbt;
import noppes.npcs.api.INbtList;
import noppes.npcs.api.item.IItemStack;
import noppes.npcs.api.IWorld;
/**
* Defines which entity types can use an ability.
* Defines which IEntity types can use an ability.
*/
public enum UserType {
NPC_ONLY,
@@ -0,0 +1,141 @@
package kamkeel.npcs.controllers.data.ability.extender;
import kamkeel.npcs.controllers.data.ability.Ability;
import kamkeel.npcs.controllers.data.ability.conditions.AbilityCondition;
import kamkeel.npcs.controllers.data.ability.enums.AbilityPhase;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.entity.IEntityLiving;
import noppes.npcs.api.entity.IEntityLivingBase;
import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.api.IDamageSource;
import noppes.npcs.api.INbt;
import noppes.npcs.api.INbtList;
import noppes.npcs.api.item.IItemStack;
import noppes.npcs.api.IWorld;
/**
* General-purpose extension point for abilities. Multiple extenders can be registered.
* Covers lifecycle hooks (start/tick/complete) and damage handling.
* <p>
* All methods have no-op defaults so extenders only override what they need.
*/
public interface IAbilityExtender {
/**
* Called before an ability starts executing.
* Return false to cancel the ability (e.g., insufficient resources).
*
* @param ability The ability about to start
* @param caster The IEntity executing the ability (NPC or Player)
* @param target The target IEntity (may be null for player self-cast)
* @return true to allow, false to cancel
*/
default boolean onAbilityStart(Ability ability, IEntityLivingBase caster, IEntityLivingBase target) {
return true;
}
/**
* Called each tick during ability execution.
* Return false to interrupt the ability (e.g., resource depleted).
*
* @param ability The executing ability
* @param caster The IEntity executing the ability
* @param target The target IEntity (may be null)
* @param phase The current ability phase (WINDUP, ACTIVE, DAZED, BURST_DELAY)
* @param tick The current tick within the phase
* @return true to continue, false to interrupt
*/
default boolean onAbilityTick(Ability ability, IEntityLivingBase caster, IEntityLivingBase target,
AbilityPhase phase, int tick) {
return true;
}
/**
* Called when an ability completes (normally or via interruption).
*
* @param ability The completed ability
* @param caster The IEntity that was executing the ability
* @param target The target IEntity (may be null)
* @param interrupted true if the ability was interrupted, false if it completed normally
*/
default void onAbilityComplete(Ability ability, IEntityLivingBase caster, IEntityLivingBase target,
boolean interrupted) {
}
/**
* Called when an ability deals damage, AFTER script events but BEFORE default attackIEntityFrom.
* Return true to handle the damage (skips default damage application).
* <p>
* Chain of responsibility: extenders are called in registration order.
* The first extender returning true claims the damage; remaining extenders are skipped.
*
* @param ability The ability dealing damage
* @param caster The IEntity executing the ability
* @param target The IEntity being hit
* @param damage The damage amount (after script event modifications)
* @param knockback The horizontal knockback
* @param knockbackUp The vertical knockback
* @param knockbackDirX The X component of knockback direction
* @param knockbackDirZ The Z component of knockback direction
* @param damageMultiplier Accumulated proportional damage multiplier (0.0-1.0).
* 1.0 means full damage (no reduction). Extenders that recalculate
* damage from stats should multiply by this value.
* @return true if damage was handled (skip default), false to pass to next extender or default
*/
default boolean onAbilityDamage(Ability ability, IEntityLivingBase caster, IEntityLivingBase target,
float damage, float knockback, float knockbackUp,
double knockbackDirX, double knockbackDirZ,
float damageMultiplier) {
return false;
}
/**
* Called to modify outgoing projectile damage before it hits a barrier or is used in other contexts.
* Unlike onAbilityDamage (chain of responsibility), this is cumulative — all extenders apply their
* modifications in sequence, each receiving the previous extender's output.
*
* @param ability The source ability of the projectile
* @param caster The IEntity that fired the projectile
* @param baseDamage The base damage from the ability IConfiguration
* @return The modified damage value
*/
default float modifyProjectileDamage(Ability ability, IEntityLivingBase caster, float baseDamage) {
return baseDamage;
}
/**
* Called when a barrier IEntity is about to be created, allowing extenders to modify its max health.
* Cumulative: all extenders apply in sequence, each receiving the previous output.
*
* @param ability The barrier ability being executed
* @param caster The IEntity creating the barrier
* @param baseHealth The base max health from EnergyBarrierData.maxHealth
* @return The modified max health value
*/
default float modifyBarrierHealth(Ability ability, IEntityLivingBase caster, float baseHealth) {
return baseHealth;
}
/**
* Called when an ability is about to heal an IEntity.
* Return true to handle the healing (skip default IEntity.heal()).
* Chain of responsibility: first returning true wins.
*
* @param ability The ability performing the heal
* @param caster The IEntity casting the ability
* @param target The IEntity being healed
* @param healAmount The heal amount calculated by the ability
* @return true if healing was handled (skip default), false to pass to next extender or default
*/
default boolean onAbilityHeal(Ability ability, IEntityLivingBase caster, IEntityLivingBase target, float healAmount) {
return false;
}
default Boolean onCheckCondition(AbilityCondition condition, IEntityLivingBase caster, IEntityLivingBase target) {
return null;
}
default Boolean onCheckConditionForPlayer(AbilityCondition condition, IEntityLivingBase player) {
return null;
}
}
@@ -0,0 +1,29 @@
package kamkeel.npcs.controllers.data.ability.gui;
import noppes.npcs.constants.ClientOnly;
import kamkeel.npcs.util.IVector3;
import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler;
import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback;
import kamkeel.npcs.controllers.data.ability.gui.FieldDef;
import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider;
import noppes.npcs.entity.EntityNPCInterface;
import kamkeel.npcs.entity.EntityEnergyDome;
import kamkeel.npcs.entity.EntityEnergyBarrier;
import kamkeel.npcs.entity.EntityEnergyPanel;
import kamkeel.npcs.entity.EntityAbilityOrb;
import kamkeel.npcs.entity.EntityAbilityLaser;
import kamkeel.npcs.entity.EntityAbilityDisc;
import kamkeel.npcs.entity.EntityAbilityBeam;
import kamkeel.npcs.entity.EntityEnergyProjectile;
import kamkeel.npcs.util.ByteBufUtils;
/**
* TEMPORARY STUB -- auto-generated for compilation.
* Will be replaced when this class is properly migrated.
* DO NOT add logic here.
*/
public class AbilityFieldDefs {
@SuppressWarnings("rawtypes")
public static FieldDef effectsListField(String label, java.util.function.Supplier getter, java.util.function.Consumer setter) { return new FieldDef(); }
}
@@ -0,0 +1,25 @@
package kamkeel.npcs.controllers.data.ability.gui;
import noppes.npcs.constants.ClientOnly;
import kamkeel.npcs.util.IVector3;
import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler;
import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback;
import kamkeel.npcs.controllers.data.ability.gui.FieldDef;
import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider;
import noppes.npcs.entity.EntityNPCInterface;
import kamkeel.npcs.entity.EntityEnergyDome;
import kamkeel.npcs.entity.EntityEnergyBarrier;
import kamkeel.npcs.entity.EntityEnergyPanel;
import kamkeel.npcs.entity.EntityAbilityOrb;
import kamkeel.npcs.entity.EntityAbilityLaser;
import kamkeel.npcs.entity.EntityAbilityDisc;
import kamkeel.npcs.entity.EntityAbilityBeam;
import kamkeel.npcs.entity.EntityEnergyProjectile;
/**
* TEMPORARY STUB -- auto-generated for compilation.
* Field definition for ability GUI.
*/
public class FieldDef {
}
@@ -0,0 +1,12 @@
package kamkeel.npcs.controllers.data.ability.gui;
import kamkeel.npcs.util.ByteBufUtils;
/**
* TEMPORARY STUB -- auto-generated for compilation.
* Will be replaced when this class is properly migrated.
* DO NOT add logic here.
*/
public class GuiQuestSelection {
public Object selectedQuest;
public GuiQuestSelection(int questId) {}
}
@@ -0,0 +1,25 @@
package kamkeel.npcs.controllers.data.ability.gui;
import noppes.npcs.constants.ClientOnly;
import kamkeel.npcs.util.IVector3;
import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler;
import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback;
import kamkeel.npcs.controllers.data.ability.gui.FieldDef;
import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider;
import noppes.npcs.entity.EntityNPCInterface;
import kamkeel.npcs.entity.EntityEnergyDome;
import kamkeel.npcs.entity.EntityEnergyBarrier;
import kamkeel.npcs.entity.EntityEnergyPanel;
import kamkeel.npcs.entity.EntityAbilityOrb;
import kamkeel.npcs.entity.EntityAbilityLaser;
import kamkeel.npcs.entity.EntityAbilityDisc;
import kamkeel.npcs.entity.EntityAbilityBeam;
import kamkeel.npcs.entity.EntityEnergyProjectile;
/**
* TEMPORARY STUB -- auto-generated for compilation.
* Callback interface for ability config GUI.
*/
public interface IAbilityConfigCallback {
}
@@ -0,0 +1,11 @@
package kamkeel.npcs.controllers.data.ability.gui;
import kamkeel.npcs.util.ByteBufUtils;
/**
* TEMPORARY STUB -- auto-generated for compilation.
* Will be replaced when this class is properly migrated.
* DO NOT add logic here.
*/
public interface IAbilityExtender {
// TEMPORARY STUB
}
@@ -0,0 +1,25 @@
package kamkeel.npcs.controllers.data.ability.gui;
import noppes.npcs.constants.ClientOnly;
import kamkeel.npcs.util.IVector3;
import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler;
import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback;
import kamkeel.npcs.controllers.data.ability.gui.FieldDef;
import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider;
import noppes.npcs.entity.EntityNPCInterface;
import kamkeel.npcs.entity.EntityEnergyDome;
import kamkeel.npcs.entity.EntityEnergyBarrier;
import kamkeel.npcs.entity.EntityEnergyPanel;
import kamkeel.npcs.entity.EntityAbilityOrb;
import kamkeel.npcs.entity.EntityAbilityLaser;
import kamkeel.npcs.entity.EntityAbilityDisc;
import kamkeel.npcs.entity.EntityAbilityBeam;
import kamkeel.npcs.entity.EntityEnergyProjectile;
/**
* TEMPORARY STUB -- auto-generated for compilation.
* Field provider interface for ability GUI system.
*/
public interface IAbilityFieldProvider {
}
@@ -0,0 +1,25 @@
package kamkeel.npcs.controllers.data.ability.gui;
import noppes.npcs.constants.ClientOnly;
import kamkeel.npcs.util.IVector3;
import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler;
import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback;
import kamkeel.npcs.controllers.data.ability.gui.FieldDef;
import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider;
import noppes.npcs.entity.EntityNPCInterface;
import kamkeel.npcs.entity.EntityEnergyDome;
import kamkeel.npcs.entity.EntityEnergyBarrier;
import kamkeel.npcs.entity.EntityEnergyPanel;
import kamkeel.npcs.entity.EntityAbilityOrb;
import kamkeel.npcs.entity.EntityAbilityLaser;
import kamkeel.npcs.entity.EntityAbilityDisc;
import kamkeel.npcs.entity.EntityAbilityBeam;
import kamkeel.npcs.entity.EntityEnergyProjectile;
/**
* TEMPORARY STUB -- auto-generated for compilation.
* Chained ability field provider interface.
*/
public interface IChainedAbilityFieldProvider {
}
@@ -0,0 +1,25 @@
package kamkeel.npcs.controllers.data.ability.gui;
import noppes.npcs.constants.ClientOnly;
import kamkeel.npcs.util.IVector3;
import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler;
import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback;
import kamkeel.npcs.controllers.data.ability.gui.FieldDef;
import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider;
import noppes.npcs.entity.EntityNPCInterface;
import kamkeel.npcs.entity.EntityEnergyDome;
import kamkeel.npcs.entity.EntityEnergyBarrier;
import kamkeel.npcs.entity.EntityEnergyPanel;
import kamkeel.npcs.entity.EntityAbilityOrb;
import kamkeel.npcs.entity.EntityAbilityLaser;
import kamkeel.npcs.entity.EntityAbilityDisc;
import kamkeel.npcs.entity.EntityAbilityBeam;
import kamkeel.npcs.entity.EntityEnergyProjectile;
/**
* TEMPORARY STUB -- auto-generated for compilation.
* Sub-GUI for ability configuration.
*/
public class SubGuiAbilityConfig {
}
@@ -0,0 +1,13 @@
package kamkeel.npcs.controllers.data.ability.gui;
import kamkeel.npcs.util.ByteBufUtils;
/**
* TEMPORARY STUB -- auto-generated for compilation.
* Will be replaced when this class is properly migrated.
* DO NOT add logic here.
*/
public class SubGuiCustomEffectSelect {
public SubGuiCustomEffectSelect(int effectId, int effectIndex) {}
public int getSelectedEffectId() { return -1; }
public int getSelectedIndex() { return 0; }
}
@@ -0,0 +1,12 @@
package kamkeel.npcs.controllers.data.ability.gui;
import kamkeel.npcs.util.ByteBufUtils;
/**
* TEMPORARY STUB -- auto-generated for compilation.
* Will be replaced when this class is properly migrated.
* DO NOT add logic here.
*/
public class SubGuiZonePresetSelector {
public Object selectedPreset;
public SubGuiZonePresetSelector() {}
}
@@ -0,0 +1,25 @@
package kamkeel.npcs.controllers.data.ability.preview;
import noppes.npcs.constants.ClientOnly;
import kamkeel.npcs.util.IVector3;
import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler;
import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback;
import kamkeel.npcs.controllers.data.ability.gui.FieldDef;
import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider;
import noppes.npcs.entity.EntityNPCInterface;
import kamkeel.npcs.entity.EntityEnergyDome;
import kamkeel.npcs.entity.EntityEnergyBarrier;
import kamkeel.npcs.entity.EntityEnergyPanel;
import kamkeel.npcs.entity.EntityAbilityOrb;
import kamkeel.npcs.entity.EntityAbilityLaser;
import kamkeel.npcs.entity.EntityAbilityDisc;
import kamkeel.npcs.entity.EntityAbilityBeam;
import kamkeel.npcs.entity.EntityEnergyProjectile;
/**
* TEMPORARY STUB -- auto-generated for compilation.
* Preview entity handler for ability system.
*/
public class PreviewEntityHandler {
}
@@ -0,0 +1,331 @@
package kamkeel.npcs.controllers.data.ability.type;
import noppes.npcs.constants.ClientOnly;
import kamkeel.npcs.util.IVector3;
import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler;
import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback;
import kamkeel.npcs.controllers.data.ability.gui.FieldDef;
import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider;
import noppes.npcs.entity.EntityNPCInterface;
import kamkeel.npcs.entity.EntityEnergyDome;
import kamkeel.npcs.entity.EntityEnergyBarrier;
import kamkeel.npcs.entity.EntityEnergyPanel;
import kamkeel.npcs.entity.EntityAbilityOrb;
import kamkeel.npcs.entity.EntityAbilityLaser;
import kamkeel.npcs.entity.EntityAbilityDisc;
import kamkeel.npcs.entity.EntityAbilityBeam;
import kamkeel.npcs.entity.EntityEnergyProjectile;
import kamkeel.npcs.util.ByteBufUtils;
import noppes.npcs.api.entity.IEntityLiving;
import noppes.npcs.api.IDamageSource;
import noppes.npcs.api.IWorld;
import noppes.npcs.api.entity.IEntityLivingBase;
import noppes.npcs.api.item.IItemStack;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.api.INbtList;
import noppes.npcs.api.INbt;
import kamkeel.npcs.controllers.data.ability.util.AbilityTargetHelper;
import kamkeel.npcs.controllers.data.ability.enums.LockMode;
import kamkeel.npcs.controllers.data.ability.enums.TargetFilter;
import kamkeel.npcs.controllers.data.ability.enums.TargetingMode;
import kamkeel.npcs.controllers.data.telegraph.Telegraph;
import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance;
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
import noppes.npcs.api.ability.type.IAbilityCharge;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Charge ability: Rush attack where the caster charges in a line, damaging all targets hit.
* Extends AbilityMovement for shared direction locking, stall detection, and velocity application.
*/
public class AbilityCharge extends AbilityMovement implements IAbilityCharge {
// Type-specific parameters
private float chargeSpeed = 0.8f;
private float damage = 8.0f;
private float knockback = 3.0f;
private float hitWidth = 1.5f;
// Type-specific runtime state
private transient Set<Integer> hitEntities = new HashSet<>();
public AbilityCharge() {
this.typeId = "ability.cnpc.charge";
this.name = "Charge";
this.targetingMode = TargetingMode.AGGRO_TARGET;
this.maxRange = 20.0f;
this.minRange = 4.0f;
this.lockMovement = LockMode.WINDUP;
this.cooldownTicks = 0;
this.windUpTicks = 20;
// LINE telegraph showing charge path
this.telegraphType = TelegraphType.LINE;
this.showTelegraph = true;
this.windUpSound = "mob.zombie.wood";
this.activeSound = "mob.zombie.attack";
this.windUpAnimationName = "Ability_Charge_Windup";
this.activeAnimationName = "Ability_Charge_Active";
this.defaultIconLayers = new DefaultIconLayer[]{
new DefaultIconLayer("customnpcs:textures/gui/ability/charge.png")
};
}
@Override
public boolean isTargetingModeLocked() {
return true;
}
@Override
public TargetingMode[] getAllowedTargetingModes() {
return new TargetingMode[]{TargetingMode.AGGRO_TARGET};
}
/**
* During windup, NPCs face toward their target for animation purposes.
* Players aim freely — the telegraph follows their look direction on the client,
* and direction is locked at the end of windup in onExecute().
*/
@Override
public void onWindUpTick(IEntityLivingBase caster, IEntityLivingBase target, int tick) {
if (!isPlayerCaster(caster) && target != null) {
lockDirectionToTarget(caster, target);
enforceLockedRotation(caster);
}
}
@Override
public void onExecute(IEntityLivingBase caster, IEntityLivingBase target) {
// Lock final direction at end of windup so it matches the telegraph
lockDirection(caster, target);
initMovement(caster, maxRange, chargeSpeed);
hitEntities.clear();
enforceLockedRotation(caster);
}
@Override
public void onActiveTick(IEntityLivingBase caster, IEntityLivingBase target, int tick) {
if (checkTimeout(tick)) {
stopMomentum(caster);
signalCompletion();
return;
}
if (movementDirection == null) {
stopMomentum(caster);
signalCompletion();
return;
}
if (!isPreview()) {
enforceLockedRotation(caster);
}
if (checkStall(caster, tick)) {
stopMomentum(caster);
signalCompletion();
return;
}
updatePrevPosition(caster);
if (getDistanceTraveled(caster) >= maxRange) {
stopMomentum(caster);
signalCompletion();
return;
}
if (checkBlocked(caster, chargeSpeed)) {
stopMomentum(caster);
signalCompletion();
return;
}
// Move caster (flat: zero vertical motion)
applyVelocityFlat(caster, chargeSpeed);
// Server-side collision damage (skip in preview)
if (!caster.worldObj.isRemote && !isPreview()) {
IBoundingBox hitBox = caster.boundingBox.expand(hitWidth, hitWidth * 0.5, hitWidth);
@SuppressWarnings("unchecked")
List<IEntity> entities = caster.worldObj.getEntitiesWithinAABB(IEntityLivingBase.class, hitBox);
for (IEntity IEntity : entities) {
if (!(IEntity instanceof IEntityLivingBase)) continue;
if (IEntity == caster) continue;
if (hitEntities.contains(IEntity.getEntityId())) continue;
if (!AbilityTargetHelper.shouldAffect(caster, IEntity, TargetFilter.ENEMIES, false)) continue;
IEntityLivingBase livingEntity = (IEntityLivingBase) IEntity;
hitEntities.add(IEntity.getEntityId());
boolean wasHit = applyAbilityDamageWithDirection(caster, livingEntity, damage, knockback,
movementDirection.xCoord, movementDirection.zCoord);
if (wasHit) {
applyEffects(livingEntity);
caster.worldObj.playSoundAtEntity(livingEntity, "random.explode", 0.5f, 1.2f);
}
}
}
}
@Override
public void onComplete(IEntityLivingBase caster, IEntityLivingBase target) {
stopMomentum(caster);
super.onComplete(caster, target);
}
@Override
public void onInterrupt(IEntityLivingBase caster, noppes.npcs.api.IDamageSource source, float damage) {
stopMomentum(caster);
super.onInterrupt(caster, source, damage);
}
@Override
public void cleanup() {
super.cleanup();
hitEntities.clear();
}
@Override
public void resetForBurst() {
super.resetForBurst();
hitEntities.clear();
}
@Override
public float getTelegraphLength() {
return maxRange;
}
@Override
public float getTelegraphWidth() {
return hitWidth * 2;
}
/**
* Creates a LINE telegraph from caster position towards target.
* Telegraph follows caster during windup (so caster can reposition before charging).
* Direction is locked at creation based on target position.
*/
@Override
public TelegraphInstance createTelegraph(IEntityLivingBase caster, IEntityLivingBase target) {
if (!isShowTelegraph() || getTelegraphType() == TelegraphType.NONE) {
return null;
}
// NPC: telegraph points toward aggro target
// Player: telegraph points in look direction
float yaw;
if (!isPlayerCaster(caster) && target != null) {
double dx = target.posX - caster.posX;
double dz = target.posZ - caster.posZ;
yaw = (float) Math.toDegrees(Math.atan2(-dx, dz));
} else {
yaw = caster.rotationYaw;
}
// Create LINE telegraph
Telegraph telegraph = Telegraph.line(getTelegraphLength(), getTelegraphWidth());
telegraph.setDurationTicks(windUpTicks);
telegraph.setColor(windUpColor);
telegraph.setWarningColor(activeColor);
telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4));
telegraph.setHeightOffset(telegraphHeightOffset);
// Position at caster ground level, direction towards target
double groundY = findGroundLevel(caster.worldObj, caster.posX, caster.posY, caster.posZ);
TelegraphInstance instance = new TelegraphInstance(telegraph, caster.posX, groundY, caster.posZ, yaw);
instance.setCasterEntityId(caster.getEntityId());
// Telegraph follows caster during windup - allows caster to reposition
instance.setEntityIdToFollow(caster.getEntityId());
// For player casters, track their rotation so telegraph follows look direction
if (isPlayerCaster(caster)) {
instance.setTrackFollowedEntityYaw(true);
}
return instance;
}
@Override
public int getMaxPreviewDuration() {
return (int) Math.ceil(maxRange / chargeSpeed) + 5;
}
@Override
public void writeTypeNBT(INbt nbt) {
nbt.setFloat("chargeSpeed", chargeSpeed);
nbt.setFloat("damage", damage);
nbt.setFloat("knockback", knockback);
nbt.setFloat("hitWidth", hitWidth);
}
@Override
public void readTypeNBT(INbt nbt) {
this.chargeSpeed = nbt.hasKey("chargeSpeed") ? Math.max(0.01f, nbt.getFloat("chargeSpeed")) : 0.8f;
this.damage = nbt.hasKey("damage") ? nbt.getFloat("damage") : 8.0f;
this.knockback = nbt.hasKey("knockback") ? nbt.getFloat("knockback") : 3.0f;
this.hitWidth = nbt.hasKey("hitWidth") ? nbt.getFloat("hitWidth") : 1.5f;
}
// Getters & Setters
public float getChargeSpeed() {
return chargeSpeed;
}
public void setChargeSpeed(float chargeSpeed) {
this.chargeSpeed = chargeSpeed;
}
public float getDamage() {
return damage;
}
public void setDamage(float damage) {
this.damage = damage;
}
@Override
public float getDisplayDamage() { return damage; }
public float getKnockback() {
return knockback;
}
public void setKnockback(float knockback) {
this.knockback = knockback;
}
public float getHitWidth() {
return hitWidth;
}
public void setHitWidth(float hitWidth) {
this.hitWidth = hitWidth;
}
@ClientOnly
@Override
public void getAbilityDefinitions(List<FieldDef> defs) {
defs.addAll(Arrays.asList(
FieldDef.row(
FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage),
FieldDef.floatField("ability.chargeSpeed", this::getChargeSpeed, this::setChargeSpeed)
),
FieldDef.row(
FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback),
FieldDef.floatField("ability.hitWidth", this::getHitWidth, this::setHitWidth)
),
AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects)
));
}
}
@@ -0,0 +1,227 @@
package kamkeel.npcs.controllers.data.ability.type;
import noppes.npcs.constants.ClientOnly;
import kamkeel.npcs.util.IVector3;
import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler;
import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback;
import kamkeel.npcs.controllers.data.ability.gui.FieldDef;
import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider;
import noppes.npcs.entity.EntityNPCInterface;
import kamkeel.npcs.entity.EntityEnergyDome;
import kamkeel.npcs.entity.EntityEnergyBarrier;
import kamkeel.npcs.entity.EntityEnergyPanel;
import kamkeel.npcs.entity.EntityAbilityOrb;
import kamkeel.npcs.entity.EntityAbilityLaser;
import kamkeel.npcs.entity.EntityAbilityDisc;
import kamkeel.npcs.entity.EntityAbilityBeam;
import kamkeel.npcs.entity.EntityEnergyProjectile;
import kamkeel.npcs.util.ByteBufUtils;
import noppes.npcs.api.entity.IEntityLiving;
import noppes.npcs.api.IDamageSource;
import noppes.npcs.api.IWorld;
import noppes.npcs.api.entity.IEntityLivingBase;
import noppes.npcs.api.item.IItemStack;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.api.INbtList;
import noppes.npcs.api.INbt;
import noppes.npcs.controllers.AnimationController;
import kamkeel.npcs.controllers.data.ability.enums.UserType;
import noppes.npcs.controllers.data.Animation;
import noppes.npcs.api.ability.type.IAbilityCounter;
import java.util.Arrays;
import java.util.List;
/**
* Counter ability: Absorbs incoming damage and counter-attacks the attacker.
*/
public class AbilityCounter extends AbilityDefend implements IAbilityCounter {
private CounterType counterType = CounterType.FLAT;
private float counterValue = 6.0f;
private int counterAnimationId = -1;
private String counterAnimationName = "Ability_Guard_Counter";
public AbilityCounter() {
this.typeId = "ability.cnpc.counter";
this.name = "Counter";
this.cooldownTicks = 0;
this.windUpTicks = 20;
this.allowedBy = UserType.BOTH;
this.activeSound = "random.anvil_land";
this.activeAnimationName = "Ability_Guard_Active";
this.defaultIconLayers = new DefaultIconLayer[]{
new DefaultIconLayer("customnpcs:textures/gui/ability/guard.png")
};
}
public enum CounterType {
FLAT,
PERCENT;
@Override
public String toString() {
switch (this) {
case FLAT:
return "ability.counter.flat";
case PERCENT:
return "ability.counter.percent";
default:
return name();
}
}
}
// ═══════════════════════════════════════════════════════════════════
// DEFEND HOOKS
// ═══════════════════════════════════════════════════════════════════
@Override
protected float performDefend(IEntityLivingBase attacker, float amount) {
// For PERCENT mode with DBC, use the full DBC-calculated damage, not vanilla base
float incomingDamage = amount;
if (DBCAddon.IsAvailable()) {
incomingDamage = DBCAddon.instance.getAttackerDBCDamage(amount);
}
// Calculate counter damage
float damage;
if (counterType == CounterType.FLAT) {
damage = counterValue;
} else {
damage = incomingDamage * (counterValue / 100.0f);
}
// Counter-attack through the standard ability damage pipeline.
// DBCAbilityExtender handles DBC stat routing (ignore flags, scaling, etc.) automatically.
if (caster != null && attacker.isEntityAlive() && damage > 0) {
applyAbilityDamage(caster, attacker, damage, 0);
}
return 0;
}
// ═══════════════════════════════════════════════════════════════════
// OVERRIDES
// ═══════════════════════════════════════════════════════════════════
@Override
public boolean hasDamage() {
return true;
}
@Override
protected Animation getDefendAnimation() {
return getCounterAnimation();
}
public Animation getCounterAnimation() {
if (AnimationController.Instance == null) return null;
if (counterAnimationId >= 0) {
return (Animation) AnimationController.Instance.get(counterAnimationId);
}
if (counterAnimationName != null && !counterAnimationName.isEmpty()) {
return (Animation) AnimationController.Instance.get(counterAnimationName, true);
}
return null;
}
// ═══════════════════════════════════════════════════════════════════
// NBT
// ═══════════════════════════════════════════════════════════════════
@Override
protected void writeSubTypeNBT(INbt nbt) {
nbt.setString("counterType", counterType.name());
nbt.setFloat("counterValue", counterValue);
nbt.setInteger("counterAnimationId", counterAnimationId);
nbt.setString("counterAnimationName", resolveAnimationName(counterAnimationId, counterAnimationName));
}
@Override
protected void readSubTypeNBT(INbt nbt) {
try {
this.counterType = CounterType.valueOf(nbt.getString("counterType"));
} catch (Exception e) {
this.counterType = CounterType.FLAT;
}
this.counterValue = nbt.getFloat("counterValue");
this.counterAnimationId = nbt.getInteger("counterAnimationId");
this.counterAnimationName = nbt.getString("counterAnimationName");
}
// ═══════════════════════════════════════════════════════════════════
// GUI
// ═══════════════════════════════════════════════════════════════════
@ClientOnly
@Override
protected void getTypeDefinitions(List<FieldDef> defs) {
defs.addAll(Arrays.asList(
FieldDef.enumField("gui.type", CounterType.class, this::getCounterTypeEnum, this::setCounterTypeEnum)
.hover("ability.hover.counterType"),
FieldDef.floatField("gui.value", this::getCounterValue, this::setCounterValue),
AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects)
));
FieldDef.insertAfter(defs, "ability.dazedAnimation",
FieldDef.animSubGui("ability.counterAnimation",
this::getCounterAnimationId, this::setCounterAnimationId,
this::getCounterAnimationName, this::setCounterAnimationName)
.tab("Effects"));
}
// ═══════════════════════════════════════════════════════════════════
// GETTERS / SETTERS
// ═══════════════════════════════════════════════════════════════════
public CounterType getCounterTypeEnum() {
return counterType;
}
public void setCounterTypeEnum(CounterType counterType) {
this.counterType = counterType;
}
@Override
public int getCounterType() {
return counterType.ordinal();
}
@Override
public void setCounterType(int type) {
CounterType[] values = CounterType.values();
this.counterType = type >= 0 && type < values.length ? values[type] : CounterType.FLAT;
}
@Override
public float getCounterValue() {
return counterValue;
}
@Override
public void setCounterValue(float counterValue) {
this.counterValue = counterValue;
}
@Override
public int getCounterAnimationId() {
return counterAnimationId;
}
@Override
public void setCounterAnimationId(int counterAnimationId) {
this.counterAnimationId = counterAnimationId;
}
public String getCounterAnimationName() {
return counterAnimationName;
}
public void setCounterAnimationName(String name) {
this.counterAnimationName = name != null ? name : "";
}
}
@@ -0,0 +1,492 @@
package kamkeel.npcs.controllers.data.ability.type;
import noppes.npcs.constants.ClientOnly;
import kamkeel.npcs.util.IVector3;
import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler;
import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback;
import kamkeel.npcs.controllers.data.ability.gui.FieldDef;
import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider;
import noppes.npcs.entity.EntityNPCInterface;
import kamkeel.npcs.entity.EntityEnergyDome;
import kamkeel.npcs.entity.EntityEnergyBarrier;
import kamkeel.npcs.entity.EntityEnergyPanel;
import kamkeel.npcs.entity.EntityAbilityOrb;
import kamkeel.npcs.entity.EntityAbilityLaser;
import kamkeel.npcs.entity.EntityAbilityDisc;
import kamkeel.npcs.entity.EntityAbilityBeam;
import kamkeel.npcs.entity.EntityEnergyProjectile;
import kamkeel.npcs.util.ByteBufUtils;
import noppes.npcs.api.entity.IEntityLiving;
import noppes.npcs.api.IDamageSource;
import noppes.npcs.api.IWorld;
import noppes.npcs.api.entity.IEntityLivingBase;
import noppes.npcs.api.item.IItemStack;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.api.INbtList;
import noppes.npcs.api.INbt;
import kamkeel.npcs.controllers.data.ability.Ability;
import kamkeel.npcs.controllers.data.ability.enums.LockMode;
import kamkeel.npcs.controllers.data.ability.util.AbilityTargetHelper;
import kamkeel.npcs.controllers.data.ability.enums.TargetFilter;
import kamkeel.npcs.controllers.data.ability.enums.TargetingMode;
import kamkeel.npcs.controllers.data.telegraph.Telegraph;
import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance;
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
import noppes.npcs.api.ability.type.IAbilityCutter;
import java.util.Arrays;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* Cutter ability: Sweeping fan attack in an arc.
* Deals damage to all entities in a cone/arc in front of the caster.
* Can sweep outward over time or hit instantly.
*/
public class AbilityCutter extends Ability implements IAbilityCutter {
public enum SweepMode {
SWIPE,
SPIN;
@Override
public String toString() {
switch (this) {
case SWIPE:
return "ability.sweep.swipe";
case SPIN:
return "ability.sweep.spin";
default:
return name();
}
}
}
private float arcAngle = 90.0f;
private float range = 6.0f;
private float damage = 7.0f;
private float knockback = 1.5f;
private SweepMode sweepMode = SweepMode.SWIPE;
private float sweepSpeed = 6.0f;
private int spinDurationTicks = 60; // Duration for SPIN mode
private boolean piercing = true;
private float innerRadius = 0.0f;
// Runtime state
private transient Set<Integer> hitEntities = new HashSet<>();
private transient float currentRotation = 0.0f;
private transient boolean activeSoundPlayed = false;
private transient float sweepBaseYaw = 0.0f;
public AbilityCutter() {
this.typeId = "ability.cnpc.cutter";
this.name = "Cutter";
this.targetingMode = TargetingMode.AOE_SELF;
this.maxRange = 8.0f;
this.lockMovement = LockMode.NO;
this.cooldownTicks = 0;
this.windUpTicks = 20;
this.telegraphType = TelegraphType.CONE;
this.windUpSound = "random.bow";
this.activeSound = "random.break";
this.windUpAnimationName = "Ability_Cutter_Windup";
this.activeAnimationName = "Ability_Cutter_Active";
this.defaultIconLayers = new DefaultIconLayer[]{
new DefaultIconLayer("customnpcs:textures/gui/ability/cutter.png",
this::getActiveColor)
};
}
@Override
public boolean isTargetingModeLocked() {
return true;
}
@Override
public TargetingMode[] getAllowedTargetingModes() {
return new TargetingMode[]{TargetingMode.AOE_SELF};
}
/**
* Calculate how many ticks the active sweep phase takes.
* SWIPE: arcAngle / sweepSpeed ticks
* SPIN: spinDurationTicks
*/
private int getActiveDurationTicks() {
if (sweepMode == SweepMode.SPIN) {
return spinDurationTicks;
}
return sweepSpeed > 0 ? (int) Math.ceil(arcAngle / sweepSpeed) : 1;
}
@Override
public boolean keepTelegraphDuringActive() {
return true;
}
/**
* Creates a CONE telegraph that lasts through both windup and active sweep phases.
* Warning color transitions when the sweep actually begins (at windUpTicks).
*/
@Override
public TelegraphInstance createTelegraph(IEntityLivingBase caster, IEntityLivingBase target) {
if (!isShowTelegraph() || getTelegraphType() == TelegraphType.NONE) {
return null;
}
Telegraph telegraph = Telegraph.cone(getTelegraphLength(), getTelegraphAngle(), getTelegraphInnerRadius());
int totalDuration = windUpTicks + getActiveDurationTicks();
telegraph.setDurationTicks(totalDuration);
telegraph.setColor(windUpColor);
telegraph.setWarningColor(activeColor);
// Warning transition starts when the sweep begins (active phase)
telegraph.setWarningStartTick(windUpTicks);
telegraph.setHeightOffset(telegraphHeightOffset);
double groundY = findGroundLevel(caster.worldObj, caster.posX, caster.posY, caster.posZ);
TelegraphInstance instance = new TelegraphInstance(telegraph, caster.posX, groundY, caster.posZ, caster.rotationYaw);
instance.setCasterEntityId(caster.getEntityId());
instance.setEntityIdToFollow(caster.getEntityId());
if (target != null && !isRotationLockedDuringWindup()) {
instance.setTargetEntityId(target.getEntityId());
} else {
// No target to track yaw from — track the caster's rotation instead
// so the telegraph follows where the player/NPC is facing
instance.setTrackFollowedEntityYaw(true);
}
return instance;
}
@Override
public float getTelegraphRadius() {
return range;
}
@Override
public float getTelegraphLength() {
return range;
}
@Override
public float getTelegraphAngle() {
return arcAngle;
}
@Override
public float getTelegraphInnerRadius() {
return innerRadius;
}
@Override
public void onExecute(IEntityLivingBase caster, IEntityLivingBase target) {
hitEntities.clear();
currentRotation = -arcAngle / 2.0f;
activeSoundPlayed = false;
// Compute sweep direction from caster/target at execution time.
// Server-side telegraph instances are not ticked, so their yaw is stale
// from windup start — use the caster's current rotation instead.
if (target != null) {
double dx = target.posX - caster.posX;
double dz = target.posZ - caster.posZ;
if (dx * dx + dz * dz > 0.0001) {
sweepBaseYaw = (float) Math.toDegrees(Math.atan2(-dx, dz));
} else {
sweepBaseYaw = caster.rotationYaw;
}
} else {
sweepBaseYaw = caster.rotationYaw;
}
}
@Override
public void onActiveTick(IEntityLivingBase caster, IEntityLivingBase target, int tick) {
if (caster.worldObj.isRemote && !isPreview()) return;
switch (sweepMode) {
case SWIPE:
float prevRotation = currentRotation;
currentRotation += sweepSpeed;
// Play active sound when sweep reaches center (where target typically stands)
if (!activeSoundPlayed && currentRotation >= 0) {
playActiveSound(caster, caster.worldObj);
}
if (currentRotation > arcAngle / 2.0f) {
if (!isPreview()) {
performSweepDamageRange(caster, caster.worldObj, innerRadius, range, prevRotation, arcAngle / 2.0f);
}
signalCompletion();
return;
}
if (!isPreview()) {
performSweepDamageRange(caster, caster.worldObj, innerRadius, range, prevRotation, currentRotation);
}
break;
case SPIN:
// Play active sound at spin start
if (!activeSoundPlayed) {
playActiveSound(caster, caster.worldObj);
}
if (tick >= spinDurationTicks) {
signalCompletion();
return;
}
float prevSpin = currentRotation;
currentRotation = (currentRotation + sweepSpeed) % 360.0f;
if (!isPreview()) {
hitEntities.clear();
performSweepDamageRange(caster, caster.worldObj, innerRadius, range, prevSpin, currentRotation);
}
break;
}
}
private void playActiveSound(IEntityLivingBase caster, IWorld IWorld) {
activeSoundPlayed = true;
if (activeSound != null && !activeSound.isEmpty()) {
IWorld.playSoundAtEntity(caster, activeSound, 1.0f, 1.0f);
}
}
/**
* Check for entities between two sweep angles, ensuring no gaps between ticks.
* For SWIPE mode, this covers the continuous arc from startAngle to endAngle.
*/
private void performSweepDamageRange(IEntityLivingBase caster, IWorld IWorld, float minDist, float maxDist,
float startAngle, float endAngle) {
// Calculate the angular range covered this tick
float minAngle = Math.min(startAngle, endAngle);
float maxAngle = Math.max(startAngle, endAngle);
IBoundingBox searchBox = IBoundingBox.getBoundingBox(
caster.posX - maxDist, caster.posY - 1, caster.posZ - maxDist,
caster.posX + maxDist, caster.posY + 3, caster.posZ + maxDist
);
@SuppressWarnings("unchecked")
List<IEntityLivingBase> entities = IWorld.getEntitiesWithinAABB(IEntityLivingBase.class, searchBox);
for (IEntityLivingBase IEntity : entities) {
if (IEntity == caster) continue;
if (hitEntities.contains(IEntity.getEntityId())) continue;
if (!AbilityTargetHelper.shouldAffect(caster, IEntity, TargetFilter.ENEMIES, false)) continue;
// If not piercing, stop after hitting one IEntity this sweep
if (!piercing && !hitEntities.isEmpty()) break;
double dx = IEntity.posX - caster.posX;
double dz = IEntity.posZ - caster.posZ;
double dist = Math.sqrt(dx * dx + dz * dz);
if (dist < minDist || dist > maxDist) continue;
if (!isInSweepRange(dx, dz, sweepBaseYaw, minAngle, maxAngle)) continue;
// Line-of-sight check: skip targets behind solid blocks or enemy barriers
if (!hasLineOfSight(IWorld, caster, IEntity)) continue;
if (isBlockedByBarrier(IWorld, caster, IEntity)) continue;
hitEntities.add(IEntity.getEntityId());
float distFactor = 1.0f - ((float) dist / maxDist) * 0.3f;
float actualDamage = damage * distFactor;
// Apply damage with scripted event support
boolean wasHit = applyAbilityDamage(caster, IEntity, actualDamage, knockback);
// Apply effects if hit wasn't cancelled
if (wasHit) {
applyEffects(IEntity);
}
}
}
/**
* Check if an IEntity falls within the angular range between two sweep positions.
* This ensures entities at long range can't slip between tick-gaps.
*/
private boolean isInSweepRange(double dx, double dz, float casterYaw, float minAngle, float maxAngle) {
double angleToEntity = Math.toDegrees(Math.atan2(-dx, dz));
double entityRelative = normalizeAngle(angleToEntity - casterYaw);
return entityRelative >= minAngle && entityRelative <= maxAngle;
}
private double normalizeAngle(double angle) {
while (angle > 180) angle -= 360;
while (angle < -180) angle += 360;
return angle;
}
@Override
public void onComplete(IEntityLivingBase caster, IEntityLivingBase target) {
hitEntities.clear();
currentRotation = 0.0f;
activeSoundPlayed = false;
sweepBaseYaw = 0.0f;
}
@Override
public void onInterrupt(IEntityLivingBase caster, IDamageSource source, float damage) {
hitEntities.clear();
currentRotation = 0.0f;
activeSoundPlayed = false;
sweepBaseYaw = 0.0f;
}
@Override
public void writeTypeNBT(INbt nbt) {
nbt.setFloat("arcAngle", arcAngle);
nbt.setFloat("range", range);
nbt.setFloat("damage", damage);
nbt.setFloat("knockback", knockback);
nbt.setString("sweepMode", sweepMode.name());
nbt.setFloat("sweepSpeed", sweepSpeed);
nbt.setInteger("spinDurationTicks", spinDurationTicks);
nbt.setBoolean("piercing", piercing);
nbt.setFloat("innerRadius", innerRadius);
}
@Override
public void readTypeNBT(INbt nbt) {
this.arcAngle = nbt.getFloat("arcAngle");
this.range = nbt.getFloat("range");
this.damage = nbt.getFloat("damage");
this.knockback = nbt.getFloat("knockback");
try {
this.sweepMode = SweepMode.valueOf(nbt.getString("sweepMode"));
} catch (Exception e) {
this.sweepMode = SweepMode.SWIPE;
}
this.sweepSpeed = nbt.getFloat("sweepSpeed");
this.spinDurationTicks = nbt.getInteger("spinDurationTicks");
this.piercing = nbt.getBoolean("piercing");
this.innerRadius = nbt.getFloat("innerRadius");
}
// Getters & Setters
public float getArcAngle() {
return arcAngle;
}
public void setArcAngle(float arcAngle) {
this.arcAngle = arcAngle;
}
public float getRange() {
return range;
}
public void setRange(float range) {
this.range = range;
}
public float getDamage() {
return damage;
}
public void setDamage(float damage) {
this.damage = damage;
}
@Override
public float getDisplayDamage() { return damage; }
public float getKnockback() {
return knockback;
}
public void setKnockback(float knockback) {
this.knockback = knockback;
}
public SweepMode getSweepModeEnum() {
return sweepMode;
}
public void setSweepModeEnum(SweepMode sweepMode) {
this.sweepMode = sweepMode;
}
@Override
public int getSweepMode() {
return sweepMode.ordinal();
}
@Override
public void setSweepMode(int mode) {
SweepMode[] values = SweepMode.values();
this.sweepMode = mode >= 0 && mode < values.length ? values[mode] : SweepMode.SWIPE;
}
public float getSweepSpeed() {
return sweepSpeed;
}
public void setSweepSpeed(float sweepSpeed) {
this.sweepSpeed = sweepSpeed;
}
public int getSpinDurationTicks() {
return spinDurationTicks;
}
public void setSpinDurationTicks(int spinDurationTicks) {
this.spinDurationTicks = Math.max(1, spinDurationTicks);
}
public boolean isPiercing() {
return piercing;
}
public void setPiercing(boolean piercing) {
this.piercing = piercing;
}
public float getInnerRadius() {
return innerRadius;
}
public void setInnerRadius(float innerRadius) {
this.innerRadius = innerRadius;
}
public float getCurrentRotation() {
return currentRotation;
}
@ClientOnly
@Override
public void getAbilityDefinitions(List<FieldDef> defs) {
defs.addAll(Arrays.asList(
FieldDef.row(
FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage),
FieldDef.floatField("gui.range", this::getRange, this::setRange)
),
FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback),
FieldDef.section("ability.section.sweep"),
FieldDef.enumField("ability.sweepMode", SweepMode.class, this::getSweepModeEnum, this::setSweepModeEnum)
.hover("ability.hover.sweepMode"),
FieldDef.row(
FieldDef.floatField("ability.arcAngle", this::getArcAngle, this::setArcAngle),
FieldDef.floatField("ability.innerRadius", this::getInnerRadius, this::setInnerRadius)
.hover("ability.hover.innerRadius")
),
FieldDef.floatField("ability.sweepSpeed", this::getSweepSpeed, this::setSweepSpeed),
FieldDef.intField("ability.spinDuration", this::getSpinDurationTicks, this::setSpinDurationTicks)
.range(1, 1000)
.visibleWhen(() -> this.getSweepModeEnum() == SweepMode.SPIN),
FieldDef.boolField("ability.piercing", this::isPiercing, this::setPiercing)
.hover("ability.hover.piercing"),
AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects)
));
}
}
@@ -0,0 +1,424 @@
package kamkeel.npcs.controllers.data.ability.type;
import noppes.npcs.constants.ClientOnly;
import kamkeel.npcs.util.IVector3;
import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler;
import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback;
import kamkeel.npcs.controllers.data.ability.gui.FieldDef;
import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider;
import noppes.npcs.entity.EntityNPCInterface;
import kamkeel.npcs.entity.EntityEnergyDome;
import kamkeel.npcs.entity.EntityEnergyBarrier;
import kamkeel.npcs.entity.EntityEnergyPanel;
import kamkeel.npcs.entity.EntityAbilityOrb;
import kamkeel.npcs.entity.EntityAbilityLaser;
import kamkeel.npcs.entity.EntityAbilityDisc;
import kamkeel.npcs.entity.EntityAbilityBeam;
import kamkeel.npcs.entity.EntityEnergyProjectile;
import kamkeel.npcs.util.ByteBufUtils;
import noppes.npcs.api.entity.IEntityLiving;
import noppes.npcs.api.IDamageSource;
import noppes.npcs.api.IWorld;
import noppes.npcs.api.entity.IEntityLivingBase;
import noppes.npcs.api.item.IItemStack;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.api.INbtList;
import noppes.npcs.api.INbt;
import kamkeel.npcs.controllers.data.ability.enums.LockMode;
import kamkeel.npcs.controllers.data.ability.enums.TargetingMode;
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
import noppes.npcs.api.ability.type.IAbilityDash;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
/**
* Dash ability: Quick evasive sidestep with NO telegraph.
* Extends AbilityMovement for shared direction locking, stall detection, and velocity application.
*/
public class AbilityDash extends AbilityMovement implements IAbilityDash {
/**
* Dash behavior mode.
*/
public enum DashMode {
AGGRESSIVE,
DEFENSIVE,
DIRECTIONAL;
@Override
public String toString() {
switch (this) {
case AGGRESSIVE:
return "ability.dash.aggressive";
case DEFENSIVE:
return "ability.dash.defensive";
case DIRECTIONAL:
return "ability.dash.directional";
default:
return name();
}
}
}
/**
* Specific dash direction.
*/
public enum DashDirection {
FORWARD(0),
DIAGONAL_FORWARD_LEFT(45),
DIAGONAL_FORWARD_RIGHT(-45),
LEFT(90),
RIGHT(-90),
DIAGONAL_BACK_LEFT(135),
DIAGONAL_BACK_RIGHT(-135),
BACK(180),
CUSTOM(0);
private final float angleOffset;
DashDirection(float angleOffset) {
this.angleOffset = angleOffset;
}
public float getAngleOffset() {
return angleOffset;
}
@Override
public String toString() {
switch (this) {
case FORWARD:
return "ability.dash.forward";
case DIAGONAL_FORWARD_LEFT:
return "ability.dash.diagonalForwardLeft";
case DIAGONAL_FORWARD_RIGHT:
return "ability.dash.diagonalForwardRight";
case LEFT:
return "ability.dash.left";
case RIGHT:
return "ability.dash.right";
case DIAGONAL_BACK_LEFT:
return "ability.dash.diagonalBackLeft";
case DIAGONAL_BACK_RIGHT:
return "ability.dash.diagonalBackRight";
case BACK:
return "ability.dash.back";
case CUSTOM:
return "ability.dash.custom";
default:
return name();
}
}
public static DashDirection fromOrdinal(int ordinal) {
DashDirection[] values = values();
if (ordinal >= 0 && ordinal < values.length) {
return values[ordinal];
}
return DashDirection.FORWARD;
}
}
private static final Random RANDOM = new Random();
private static final DashDirection[] AGGRESSIVE_DIRECTIONS = {
DashDirection.FORWARD,
DashDirection.DIAGONAL_FORWARD_LEFT,
DashDirection.DIAGONAL_FORWARD_RIGHT
};
private static final DashDirection[] DEFENSIVE_DIRECTIONS = {
DashDirection.LEFT,
DashDirection.RIGHT,
DashDirection.BACK,
DashDirection.DIAGONAL_BACK_LEFT,
DashDirection.DIAGONAL_BACK_RIGHT
};
// Type-specific parameters
private DashMode dashMode = DashMode.DEFENSIVE;
private DashDirection dashDirection = DashDirection.FORWARD;
private float dashDistance = 4.0f;
private float dashSpeed = 0.5f;
private float dashAngle = 0.0f;
// Type-specific runtime state
private transient DashDirection chosenDirection;
private transient double preDashMotionX;
private transient double preDashMotionZ;
public AbilityDash() {
this.typeId = "ability.cnpc.dash";
this.name = "Dash";
this.targetingMode = TargetingMode.AGGRO_TARGET;
this.maxRange = 20.0f;
this.minRange = 0.0f;
this.lockMovement = LockMode.NO;
this.cooldownTicks = 0;
this.windUpTicks = 5;
// No telegraph for dash - it's a quick evasive move
this.telegraphType = TelegraphType.NONE;
this.showTelegraph = false;
this.windUpSound = "mob.bat.takeoff";
this.defaultIconLayers = new DefaultIconLayer[]{
new DefaultIconLayer("customnpcs:textures/gui/ability/dash.png")
};
}
@Override
public boolean hasDamage() {
return false;
}
@Override
public boolean isTargetingModeLocked() {
return true;
}
@Override
public TargetingMode[] getAllowedTargetingModes() {
return new TargetingMode[]{TargetingMode.AGGRO_TARGET};
}
@Override
public void onExecute(IEntityLivingBase caster, IEntityLivingBase target) {
preDashMotionX = caster.motionX;
preDashMotionZ = caster.motionZ;
initMovement(caster, dashDistance, dashSpeed);
if (dashMode == DashMode.DIRECTIONAL) {
// Dash straight in the caster's look direction
chosenDirection = dashDirection;
float baseYaw = getBaseYaw(caster, target);
float angleOffset = chosenDirection == DashDirection.CUSTOM ? dashAngle : chosenDirection.getAngleOffset();
float dashYaw = baseYaw + angleOffset;
setDirectionFromYaw(dashYaw);
} else {
// Choose random direction based on mode
DashDirection[] directions = dashMode == DashMode.AGGRESSIVE
? AGGRESSIVE_DIRECTIONS
: DEFENSIVE_DIRECTIONS;
chosenDirection = directions[RANDOM.nextInt(directions.length)];
// Calculate direction: base yaw (toward target or look dir) + direction offset
float baseYaw = getBaseYaw(caster, target);
float dashYaw = baseYaw + chosenDirection.getAngleOffset();
setDirectionFromYaw(dashYaw);
}
// Small upward impulse for a skip/hop arc (gravity handles the descent)
caster.motionY = 0.2;
}
@Override
public void onActiveTick(IEntityLivingBase caster, IEntityLivingBase target, int tick) {
if (checkTimeout(tick)) {
finishDash(caster);
signalCompletion();
return;
}
if (movementDirection == null) {
finishDash(caster);
signalCompletion();
return;
}
if (checkStall(caster, tick)) {
finishDash(caster);
signalCompletion();
return;
}
updatePrevPosition(caster);
if (getDistanceTraveledSq(caster) >= (double) dashDistance * dashDistance) {
finishDash(caster);
signalCompletion();
return;
}
if (checkBlocked(caster, dashSpeed)) {
finishDash(caster);
signalCompletion();
return;
}
// 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
caster.worldObj.spawnParticle("smoke", caster.posX, caster.posY + 0.5, caster.posZ, 0, 0, 0);
}
}
@Override
public void onComplete(IEntityLivingBase caster, IEntityLivingBase target) {
finishDash(caster);
}
@Override
public void onInterrupt(IEntityLivingBase caster, IDamageSource source, float damage) {
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
public int getMaxPreviewDuration() {
return (int) Math.ceil(dashDistance / dashSpeed) + 5;
}
@Override
public float getTelegraphRadius() {
return 0; // No telegraph for dash
}
@Override
public void writeTypeNBT(INbt nbt) {
nbt.setString("dashMode", dashMode.name());
nbt.setString("dashDirection", dashDirection.name());
nbt.setFloat("dashDistance", dashDistance);
nbt.setFloat("dashSpeed", dashSpeed);
nbt.setFloat("dashAngle", dashAngle);
}
@Override
public void readTypeNBT(INbt nbt) {
try {
this.dashMode = DashMode.valueOf(nbt.getString("dashMode"));
} catch (Exception e) {
this.dashMode = DashMode.DEFENSIVE;
}
try {
this.dashDirection = DashDirection.valueOf(nbt.getString("dashDirection"));
} catch (Exception e) {
this.dashDirection = DashDirection.FORWARD;
}
this.dashDistance = nbt.hasKey("dashDistance") ? nbt.getFloat("dashDistance") : 4.0f;
this.dashSpeed = nbt.hasKey("dashSpeed") ? Math.max(0.01f, nbt.getFloat("dashSpeed")) : 0.5f;
this.dashAngle = nbt.hasKey("dashAngle") ? nbt.getFloat("dashAngle") : 0.0f;
}
// Getters & Setters
public DashMode getDashModeEnum() {
return dashMode;
}
public void setDashModeEnum(DashMode dashMode) {
this.dashMode = dashMode;
}
@Override
public int getDashMode() {
return dashMode.ordinal();
}
@Override
public void setDashMode(int mode) {
DashMode[] values = DashMode.values();
this.dashMode = mode >= 0 && mode < values.length ? values[mode] : DashMode.AGGRESSIVE;
}
public float getDashDistance() {
return dashDistance;
}
public void setDashDistance(float dashDistance) {
this.dashDistance = dashDistance;
}
public float getDashSpeed() {
return dashSpeed;
}
public void setDashSpeed(float dashSpeed) {
this.dashSpeed = dashSpeed;
}
public float getDashAngle() {
return dashAngle;
}
public void setDashAngle(float dashAngle) {
this.dashAngle = dashAngle;
}
public DashDirection getDashDirectionEnum() {
return dashDirection;
}
public void setDashDirectionEnum(DashDirection dashDirection) {
this.dashDirection = dashDirection;
}
@Override
public int getDashDirection() {
return dashDirection.ordinal();
}
@Override
public void setDashDirection(int mode) {
DashDirection[] values = DashDirection.values();
this.dashDirection = mode >= 0 && mode < values.length ? values[mode] : DashDirection.FORWARD;
}
public DashDirection getChosenDirection() {
return chosenDirection;
}
private void finishDash(IEntityLivingBase caster) {
if (dashMode == DashMode.DIRECTIONAL) {
applyHorizontalMomentum(caster, preDashMotionX, preDashMotionZ);
} else {
stopMomentum(caster);
}
}
@ClientOnly
@Override
public void getAbilityDefinitions(List<FieldDef> defs) {
defs.addAll(Arrays.asList(
FieldDef.enumField("ability.dashMode", DashMode.class, this::getDashModeEnum, this::setDashModeEnum)
.hover("ability.hover.dashMode"),
FieldDef.row(
FieldDef.floatField("ability.dashDistance", this::getDashDistance, this::setDashDistance),
FieldDef.floatField("ability.dashSpeed", this::getDashSpeed, this::setDashSpeed)
),
FieldDef.row(
FieldDef.enumField("ability.dashDirection", DashDirection.class, this::getDashDirectionEnum, this::setDashDirectionEnum),
FieldDef.floatField("ability.dashAngle", this::getDashAngle, this::setDashAngle)
.min(Float.NEGATIVE_INFINITY).visibleWhen(() -> getDashDirectionEnum() == DashDirection.CUSTOM)
).visibleWhen(() -> this.getDashModeEnum() == DashMode.DIRECTIONAL)
));
}
}
@@ -0,0 +1,356 @@
package kamkeel.npcs.controllers.data.ability.type;
import noppes.npcs.constants.ClientOnly;
import kamkeel.npcs.util.IVector3;
import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler;
import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback;
import kamkeel.npcs.controllers.data.ability.gui.FieldDef;
import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider;
import noppes.npcs.entity.EntityNPCInterface;
import kamkeel.npcs.entity.EntityEnergyDome;
import kamkeel.npcs.entity.EntityEnergyBarrier;
import kamkeel.npcs.entity.EntityEnergyPanel;
import kamkeel.npcs.entity.EntityAbilityOrb;
import kamkeel.npcs.entity.EntityAbilityLaser;
import kamkeel.npcs.entity.EntityAbilityDisc;
import kamkeel.npcs.entity.EntityAbilityBeam;
import kamkeel.npcs.entity.EntityEnergyProjectile;
import kamkeel.npcs.util.ByteBufUtils;
import noppes.npcs.api.entity.IEntityLiving;
import noppes.npcs.api.IDamageSource;
import noppes.npcs.api.IWorld;
import noppes.npcs.api.entity.IEntityLivingBase;
import noppes.npcs.api.item.IItemStack;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.api.INbtList;
import noppes.npcs.api.INbt;
import kamkeel.npcs.controllers.data.ability.Ability;
import kamkeel.npcs.controllers.data.ability.enums.AbilityPhase;
import kamkeel.npcs.controllers.data.ability.enums.LockMode;
import kamkeel.npcs.controllers.data.ability.enums.TargetingMode;
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
import noppes.npcs.api.ability.type.IAbilityDefend;
import noppes.npcs.controllers.data.Animation;
import java.util.List;
/**
* Abstract base class for defensive abilities (Guard, Counter, Dodge).
* <p>
* {@link #onDefend} handles shared checks then delegates to {@link #performDefend(IEntityLivingBase, float)}
* which subclasses override for their specific behavior.
* <p>
* Damage source filtering is controlled by {@link #isValidDamageSource(IDamageSource)}.
* By default only physical melee is accepted; subclasses can override to widen (e.g. Guard accepts all).
*/
public abstract class AbilityDefend extends Ability implements IAbilityDefend {
// ═══════════════════════════════════════════════════════════════════
// SHARED PERSIST STATE
// ═══════════════════════════════════════════════════════════════════
protected int durationTicks = 60;
protected int maxHitAmount = 3;
// ═══════════════════════════════════════════════════════════════════
// TRANSIENT RUNTIME STATE
// ═══════════════════════════════════════════════════════════════════
protected transient IEntityLivingBase caster;
protected transient int hitCount;
protected transient IEntityLivingBase lastAttacker;
protected transient float lastDamageTaken;
protected transient Animation pendingDefendAnimation;
protected transient int defendAnimEndTick = -1;
/** Prevents double-counting hits when multiple damage paths call onDefend for the same hit in a single tick. */
protected transient long lastDefendTick = -1;
/** Defers signalCompletion to the next tick so all damage paths for the last hit still see isDefending()=true. */
protected transient boolean pendingCompletion;
// ═══════════════════════════════════════════════════════════════════
// CONSTRUCTOR DEFAULTS
// ═══════════════════════════════════════════════════════════════════
protected AbilityDefend() {
this.targetingMode = TargetingMode.SELF;
this.lockMovement = LockMode.ACTIVE;
this.interruptible = false;
this.telegraphType = TelegraphType.NONE;
this.showTelegraph = false;
this.windUpTicks = 0;
}
// ═══════════════════════════════════════════════════════════════════
// LIFECYCLE
// ═══════════════════════════════════════════════════════════════════
@Override
public void onExecute(IEntityLivingBase caster, IEntityLivingBase target) {
this.caster = caster;
hitCount = 0;
lastAttacker = null;
lastDamageTaken = 0.0f;
}
@Override
public void onActiveTick(IEntityLivingBase caster, IEntityLivingBase target, int tick) {
if (pendingCompletion || tick >= durationTicks) {
signalCompletion();
}
}
@Override
public void reset() {
super.reset();
caster = null;
hitCount = 0;
lastAttacker = null;
lastDamageTaken = 0.0f;
pendingDefendAnimation = null;
defendAnimEndTick = -1;
lastDefendTick = -1;
pendingCompletion = false;
}
// ═══════════════════════════════════════════════════════════════════
// DEFEND HOOK
// ═══════════════════════════════════════════════════════════════════
/**
* Called when the caster is hit while defending.
* Checks damage source validity via {@link #isValidDamageSource}, then delegates
* to {@link #performDefend(IEntityLivingBase, float)}.
*
* @param attacker The IEntity that hit the caster
* @param source The damage source
* @param amount The original incoming damage
* @return The modified damage to apply (unchanged if source is invalid)
*/
public final float onDefend(IEntityLivingBase attacker, IDamageSource source, float amount) {
if (!isDefending()) {
return amount;
}
if (attacker == null || source == null) {
return amount;
}
if (!isValidDamageSource(source)) {
return amount;
}
// Deduplicate per tick: when multiple damage paths (e.g. vanilla + DBC) call onDefend
// for the same hit, only count it once for hitCount / signalCompletion / animation.
long currentTick = (caster != null && caster.worldObj != null)
? caster.worldObj.getTotalWorldTime() : -1;
boolean firstCallThisTick = (currentTick < 0 || currentTick != lastDefendTick);
// If max hits reached on a previous tick and this is a NEW hit, reject it
if (pendingCompletion && firstCallThisTick) {
return amount;
}
if (firstCallThisTick) {
lastDefendTick = currentTick;
lastAttacker = attacker;
lastDamageTaken = amount;
hitCount++;
// Queue defend reaction animation (played on next tick by AbstractDataAbilities)
pendingDefendAnimation = getDefendAnimation();
// Defer completion to next tick so all damage paths in this tick
// still see isDefending()=true and apply guard reduction.
if (maxHitAmount > 0 && hitCount >= maxHitAmount) {
pendingCompletion = true;
}
}
// Always calculate the correct reduction for the caller's damage amount
return performDefend(attacker, amount);
}
/**
* Subclass-specific defend behavior. Called only for valid hits while defending
* (as determined by {@link #isValidDamageSource}).
*
* @param attacker The IEntity that hit the caster
* @param amount The incoming damage
* @return The modified damage the caster should take
*/
protected abstract float performDefend(IEntityLivingBase attacker, float amount);
/**
* Determines whether this defend ability reacts to the given damage source.
* Default implementation accepts only physical melee damage (rejects magic, fire, IExplosion, projectile).
* Subclasses can override to accept additional damage types (e.g. Guard accepts all).
*/
protected boolean isValidDamageSource(IDamageSource source) {
return !source.isMagicDamage() && !source.isFireDamage() && !source.isExplosion() && !source.isProjectile();
}
// ═══════════════════════════════════════════════════════════════════
// SUBCLASS HOOKS
// ═══════════════════════════════════════════════════════════════════
/**
* Subclass-specific NBT persistence.
*/
protected abstract void writeSubTypeNBT(INbt nbt);
protected abstract void readSubTypeNBT(INbt nbt);
@ClientOnly
protected abstract void getTypeDefinitions(List<FieldDef> defs);
// ═══════════════════════════════════════════════════════════════════
// DEFEND ANIMATION
// ═══════════════════════════════════════════════════════════════════
/**
* Returns the animation to play when this defend ability reacts to a hit.
* Subclasses override to provide their specific animation (counter strike, dodge roll, etc).
* Guard returns null (no reaction animation).
*/
protected Animation getDefendAnimation() {
return null;
}
/**
* Consume and return the pending defend animation (set during onDefend).
* Called by AbstractDataAbilities to play the animation on the next tick.
*/
public Animation consumeDefendAnimation() {
Animation anim = pendingDefendAnimation;
pendingDefendAnimation = null;
return anim;
}
/**
* Schedule return to active animation after the defend animation finishes.
*/
public void scheduleReturnToActive(int currentTick, Animation defendAnim) {
if (defendAnim != null) {
defendAnimEndTick = currentTick + (int) defendAnim.getTotalTime();
}
}
/**
* Check if the defend animation has finished and we should return to the active animation.
*/
public boolean shouldReturnToActiveAnimation(int currentTick) {
if (defendAnimEndTick >= 0 && currentTick >= defendAnimEndTick) {
defendAnimEndTick = -1;
return true;
}
return false;
}
// ═══════════════════════════════════════════════════════════════════
// STATE QUERIES
// ═══════════════════════════════════════════════════════════════════
@Override
public boolean isDefending() {
return isExecuting() && getPhase() == AbilityPhase.ACTIVE;
}
// ═══════════════════════════════════════════════════════════════════
// OVERRIDES
// ═══════════════════════════════════════════════════════════════════
@Override
public boolean hasDamage() {
return false;
}
@Override
public boolean allowBurst() {
return false;
}
@Override
public boolean isTargetingModeLocked() {
return true;
}
@Override
public TargetingMode[] getAllowedTargetingModes() {
return new TargetingMode[]{TargetingMode.SELF};
}
@Override
public float getTelegraphRadius() {
return 0;
}
/**
* Suppresses the auto-play of activeSound on ACTIVE phase entry.
* Defend abilities play their sound on hit reaction, not on phase start.
*/
@Override
public boolean keepTelegraphDuringActive() {
return true;
}
// ═══════════════════════════════════════════════════════════════════
// NBT
// ═══════════════════════════════════════════════════════════════════
@Override
public final void writeTypeNBT(INbt nbt) {
nbt.setInteger("durationTicks", durationTicks);
nbt.setInteger("maxHitAmount", maxHitAmount);
writeSubTypeNBT(nbt);
}
@Override
public final void readTypeNBT(INbt nbt) {
this.durationTicks = nbt.hasKey("durationTicks") ? nbt.getInteger("durationTicks") : 60;
this.maxHitAmount = nbt.hasKey("maxHitAmount") ? nbt.getInteger("maxHitAmount") : 3;
readSubTypeNBT(nbt);
}
// ═══════════════════════════════════════════════════════════════════
// GUI
// ═══════════════════════════════════════════════════════════════════
@ClientOnly
@Override
public final void getAbilityDefinitions(List<FieldDef> defs) {
defs.add(FieldDef.intField("ability.duration", this::getDurationTicks, this::setDurationTicks).range(1, 6000));
defs.add(FieldDef.intField("ability.maxHitAmount", this::getMaxHitAmount, this::setMaxHitAmount).range(0, 100)
.hover("ability.hover.maxHitAmount"));
getTypeDefinitions(defs);
}
// ═══════════════════════════════════════════════════════════════════
// GETTERS / SETTERS
// ═══════════════════════════════════════════════════════════════════
@Override
public int getDurationTicks() {
return durationTicks;
}
@Override
public void setDurationTicks(int ticks) {
this.durationTicks = Math.max(1, ticks);
}
@Override
public int getMaxHitAmount() {
return maxHitAmount;
}
@Override
public void setMaxHitAmount(int amount) {
this.maxHitAmount = Math.max(0, amount);
}
public int getHitCount() {
return hitCount;
}
}
@@ -0,0 +1,217 @@
package kamkeel.npcs.controllers.data.ability.type;
import noppes.npcs.constants.ClientOnly;
import kamkeel.npcs.util.IVector3;
import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler;
import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback;
import kamkeel.npcs.controllers.data.ability.gui.FieldDef;
import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider;
import noppes.npcs.entity.EntityNPCInterface;
import kamkeel.npcs.entity.EntityEnergyDome;
import kamkeel.npcs.entity.EntityEnergyBarrier;
import kamkeel.npcs.entity.EntityEnergyPanel;
import kamkeel.npcs.entity.EntityAbilityOrb;
import kamkeel.npcs.entity.EntityAbilityLaser;
import kamkeel.npcs.entity.EntityAbilityDisc;
import kamkeel.npcs.entity.EntityAbilityBeam;
import kamkeel.npcs.entity.EntityEnergyProjectile;
import kamkeel.npcs.util.ByteBufUtils;
import noppes.npcs.api.entity.IEntityLiving;
import noppes.npcs.api.IDamageSource;
import noppes.npcs.api.IWorld;
import noppes.npcs.api.entity.IEntityLivingBase;
import noppes.npcs.api.item.IItemStack;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.api.INbtList;
import noppes.npcs.api.INbt;
import noppes.npcs.controllers.AnimationController;
import kamkeel.npcs.controllers.data.ability.enums.UserType;
import noppes.npcs.controllers.data.Animation;
import noppes.npcs.api.ability.type.IAbilityDodge;
import java.util.ArrayList;
import java.util.List;
/**
* Dodge ability: Completely cancels incoming attacks and plays a random dodge animation.
*/
public class AbilityDodge extends AbilityDefend implements IAbilityDodge {
private int dodgeAnimation1Id = -1;
private String dodgeAnimation1Name = "";
private int dodgeAnimation2Id = -1;
private String dodgeAnimation2Name = "";
private int dodgeAnimation3Id = -1;
private String dodgeAnimation3Name = "";
public AbilityDodge() {
this.typeId = "ability.cnpc.dodge";
this.name = "Dodge";
this.cooldownTicks = 0;
this.windUpTicks = 10;
this.allowedBy = UserType.BOTH;
this.activeAnimationName = "";
this.defaultIconLayers = new DefaultIconLayer[]{
new DefaultIconLayer("customnpcs:textures/gui/ability/guard.png")
};
}
// ═══════════════════════════════════════════════════════════════════
// DEFEND HOOKS
// ═══════════════════════════════════════════════════════════════════
@Override
protected float performDefend(IEntityLivingBase attacker, float amount) {
return 0;
}
@Override
protected Animation getDefendAnimation() {
return getRandomDodgeAnimation();
}
/**
* Returns a random dodge animation from the configured slots (non-empty only).
*/
public Animation getRandomDodgeAnimation() {
if (AnimationController.Instance == null) return null;
List<Animation> available = new ArrayList<>();
Animation a1 = getDodgeAnimation(dodgeAnimation1Id, dodgeAnimation1Name);
Animation a2 = getDodgeAnimation(dodgeAnimation2Id, dodgeAnimation2Name);
Animation a3 = getDodgeAnimation(dodgeAnimation3Id, dodgeAnimation3Name);
if (a1 != null) available.add(a1);
if (a2 != null) available.add(a2);
if (a3 != null) available.add(a3);
if (available.isEmpty()) return null;
return available.get((int) (Math.random() * available.size()));
}
private Animation getDodgeAnimation(int id, String name) {
if (id >= 0) {
return (Animation) AnimationController.Instance.get(id);
}
if (name != null && !name.isEmpty()) {
return (Animation) AnimationController.Instance.get(name, true);
}
return null;
}
// ═══════════════════════════════════════════════════════════════════
// NBT
// ═══════════════════════════════════════════════════════════════════
@Override
protected void writeSubTypeNBT(INbt nbt) {
nbt.setInteger("dodgeAnimation1Id", dodgeAnimation1Id);
nbt.setString("dodgeAnimation1Name", resolveAnimationName(dodgeAnimation1Id, dodgeAnimation1Name));
nbt.setInteger("dodgeAnimation2Id", dodgeAnimation2Id);
nbt.setString("dodgeAnimation2Name", resolveAnimationName(dodgeAnimation2Id, dodgeAnimation2Name));
nbt.setInteger("dodgeAnimation3Id", dodgeAnimation3Id);
nbt.setString("dodgeAnimation3Name", resolveAnimationName(dodgeAnimation3Id, dodgeAnimation3Name));
}
@Override
protected void readSubTypeNBT(INbt nbt) {
this.dodgeAnimation1Id = nbt.getInteger("dodgeAnimation1Id");
this.dodgeAnimation1Name = nbt.getString("dodgeAnimation1Name");
this.dodgeAnimation2Id = nbt.getInteger("dodgeAnimation2Id");
this.dodgeAnimation2Name = nbt.getString("dodgeAnimation2Name");
this.dodgeAnimation3Id = nbt.getInteger("dodgeAnimation3Id");
this.dodgeAnimation3Name = nbt.getString("dodgeAnimation3Name");
}
// ═══════════════════════════════════════════════════════════════════
// GUI
// ═══════════════════════════════════════════════════════════════════
@ClientOnly
@Override
protected void getTypeDefinitions(List<FieldDef> defs) {
FieldDef.insertAfter(defs, "ability.dazedAnimation",
FieldDef.section("ability.section.dodgeAnimations").tab("Effects"));
FieldDef.insertAfter(defs, "ability.section.dodgeAnimations",
FieldDef.animSubGui("ability.dodgeAnimation1",
this::getDodgeAnimation1Id, this::setDodgeAnimation1Id,
this::getDodgeAnimation1Name, this::setDodgeAnimation1Name)
.tab("Effects"));
FieldDef.insertAfter(defs, "ability.dodgeAnimation1",
FieldDef.animSubGui("ability.dodgeAnimation2",
this::getDodgeAnimation2Id, this::setDodgeAnimation2Id,
this::getDodgeAnimation2Name, this::setDodgeAnimation2Name)
.tab("Effects"));
FieldDef.insertAfter(defs, "ability.dodgeAnimation2",
FieldDef.animSubGui("ability.dodgeAnimation3",
this::getDodgeAnimation3Id, this::setDodgeAnimation3Id,
this::getDodgeAnimation3Name, this::setDodgeAnimation3Name)
.tab("Effects"));
FieldDef.modifyVisibility(defs, "ability.windUpAnimation", () -> false);
FieldDef.modifyVisibility(defs, "ability.windUpSound", () -> false);
}
// ═══════════════════════════════════════════════════════════════════
// GETTERS / SETTERS
// ═══════════════════════════════════════════════════════════════════
@Override
public int getDodgeAnimation1Id() {
return dodgeAnimation1Id;
}
@Override
public void setDodgeAnimation1Id(int animationId) {
this.dodgeAnimation1Id = animationId;
}
public String getDodgeAnimation1Name() {
return dodgeAnimation1Name;
}
public void setDodgeAnimation1Name(String name) {
this.dodgeAnimation1Name = name != null ? name : "";
}
@Override
public int getDodgeAnimation2Id() {
return dodgeAnimation2Id;
}
@Override
public void setDodgeAnimation2Id(int animationId) {
this.dodgeAnimation2Id = animationId;
}
public String getDodgeAnimation2Name() {
return dodgeAnimation2Name;
}
public void setDodgeAnimation2Name(String name) {
this.dodgeAnimation2Name = name != null ? name : "";
}
@Override
public int getDodgeAnimation3Id() {
return dodgeAnimation3Id;
}
@Override
public void setDodgeAnimation3Id(int animationId) {
this.dodgeAnimation3Id = animationId;
}
public String getDodgeAnimation3Name() {
return dodgeAnimation3Name;
}
public void setDodgeAnimation3Name(String name) {
this.dodgeAnimation3Name = name != null ? name : "";
}
}
@@ -0,0 +1,540 @@
package kamkeel.npcs.controllers.data.ability.type;
import noppes.npcs.constants.ClientOnly;
import kamkeel.npcs.util.IVector3;
import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler;
import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback;
import kamkeel.npcs.controllers.data.ability.gui.FieldDef;
import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider;
import noppes.npcs.entity.EntityNPCInterface;
import kamkeel.npcs.entity.EntityEnergyDome;
import kamkeel.npcs.entity.EntityEnergyBarrier;
import kamkeel.npcs.entity.EntityEnergyPanel;
import kamkeel.npcs.entity.EntityAbilityOrb;
import kamkeel.npcs.entity.EntityAbilityLaser;
import kamkeel.npcs.entity.EntityAbilityDisc;
import kamkeel.npcs.entity.EntityAbilityBeam;
import kamkeel.npcs.entity.EntityEnergyProjectile;
import kamkeel.npcs.util.ByteBufUtils;
import noppes.npcs.api.entity.IEntityLiving;
import noppes.npcs.api.IDamageSource;
import noppes.npcs.api.IWorld;
import noppes.npcs.api.entity.IEntityLivingBase;
import noppes.npcs.api.item.IItemStack;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.api.INbtList;
import noppes.npcs.api.INbt;
import kamkeel.npcs.controllers.AbilityController;
import kamkeel.npcs.controllers.data.ability.Ability;
import kamkeel.npcs.controllers.data.ability.data.effect.AbilityCustomEffect;
import kamkeel.npcs.controllers.data.ability.data.entry.AbilityEffectActionEntry;
import kamkeel.npcs.controllers.data.ability.util.AbilityTargetHelper;
import kamkeel.npcs.controllers.data.ability.AbilityVariant;
import kamkeel.npcs.controllers.data.ability.enums.LockMode;
import kamkeel.npcs.controllers.data.ability.enums.TargetFilter;
import kamkeel.npcs.controllers.data.ability.enums.TargetingMode;
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
import noppes.npcs.api.ability.type.IAbilityEffect;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/**
* Effect ability: Apply healing, IPotionType effects, custom effects, and mod-registered
* effect actions to self or nearby entities.
* <p>
* Supports:
* <ul>
* <li>SELF or AOE_SELF targeting modes</li>
* <li>AOE target filtering: ALLIES, ENEMIES, or ALL</li>
* <li>Healing (fixed amount + percentage)</li>
* <li>IPotionType effects via base Ability.effects list</li>
* <li>Custom effects from CustomEffectController (player-only)</li>
* <li>Mod-registered effect actions via IEffectAction registry</li>
* <li>Burst system for repeated reapplication (auras, debuffs)</li>
* <li>Concurrent execution alongside other abilities in chains</li>
* </ul>
*/
public class AbilityEffect extends Ability implements IAbilityEffect {
// Type-specific parameters
private int durationTicks = 60;
private float healAmount = 10.0f;
private float healPercent = 0.0f;
private boolean includeSelf = true;
private float radius = 8.0f;
private boolean instantHeal = true;
private TargetFilter targetFilter = TargetFilter.ALLIES;
// Custom effects (from CustomEffectController — player-only)
private List<AbilityCustomEffect> customEffects = new ArrayList<>();
// Mod-registered effect actions
private List<AbilityEffectActionEntry> effectActions = new ArrayList<>();
// Runtime state
private transient List<IEntityLivingBase> affectedEntities;
private List<IEntityLivingBase> getAffectedEntities() {
if (affectedEntities == null) {
affectedEntities = new ArrayList<>();
}
return affectedEntities;
}
public AbilityEffect() {
this.typeId = "ability.cnpc.effect";
this.name = "Effect";
this.targetingMode = TargetingMode.SELF;
this.lockMovement = LockMode.WINDUP;
this.cooldownTicks = 0;
this.windUpTicks = 30;
this.telegraphType = TelegraphType.NONE;
this.showTelegraph = false;
this.defaultIconLayers = new DefaultIconLayer[]{
new DefaultIconLayer("customnpcs:textures/gui/ability/effect.png"),
new DefaultIconLayer("customnpcs:textures/gui/ability/effect_overlay.png",
this::getActiveColor)
};
}
// ==================== ABILITY TYPE OVERRIDES ====================
@Override
public boolean hasDamage() {
return false;
}
@Override
public boolean allowBurst() {
return true;
}
@Override
public boolean isTargetingModeLocked() {
return false;
}
@Override
public TargetingMode[] getAllowedTargetingModes() {
return new TargetingMode[]{TargetingMode.SELF, TargetingMode.AOE_SELF};
}
@Override
public boolean isConcurrentCapable() {
return true;
}
@Override
public float getTelegraphRadius() {
return targetingMode == TargetingMode.AOE_SELF && radius > 0 ? radius : 0;
}
// ==================== EXECUTION ====================
@Override
public void onExecute(IEntityLivingBase caster, IEntityLivingBase target) {
if (caster.worldObj.isRemote && !isPreview()) return;
if (!isPreview()) {
getAffectedEntities().clear();
if (targetingMode == TargetingMode.AOE_SELF && radius > 0) {
findEntitiesInRadius(caster, caster.worldObj);
} else {
if (includeSelf) {
getAffectedEntities().add(caster);
}
}
if (instantHeal) {
for (IEntityLivingBase IEntity : getAffectedEntities()) {
healEntity(caster, IEntity);
applyAllEffects(caster, IEntity);
spawnHealParticles(caster.worldObj, IEntity);
}
}
}
if (instantHeal) {
signalCompletion();
}
}
@Override
public void onActiveTick(IEntityLivingBase caster, IEntityLivingBase target, int tick) {
if ((caster.worldObj.isRemote && !isPreview()) || instantHeal) return;
if (tick % 10 == 0) {
float tickHealFixed = (healAmount / (float) durationTicks) * 10;
for (IEntityLivingBase IEntity : getAffectedEntities()) {
if (IEntity.isDead) continue;
float totalTickHeal = tickHealFixed;
if (healPercent > 0) {
totalTickHeal += (IEntity.getMaxHealth() * healPercent / (float) durationTicks) * 10;
}
if (totalTickHeal > 0) {
if (!AbilityController.Instance.fireOnAbilityHeal(this, caster, IEntity, totalTickHeal)) {
IEntity.heal(totalTickHeal);
}
}
if (tick % 20 == 0) {
applyAllEffects(caster, IEntity);
spawnHealParticles(caster.worldObj, IEntity);
}
}
}
if (tick >= durationTicks) {
signalCompletion();
return;
}
// Early completion if all affected entities are dead
boolean allDead = true;
for (IEntityLivingBase IEntity : getAffectedEntities()) {
if (!IEntity.isDead) {
allDead = false;
break;
}
}
if (!getAffectedEntities().isEmpty() && allDead) {
signalCompletion();
}
}
// ==================== EFFECT APPLICATION ====================
/**
* Applies all configured effect types to a single IEntity:
* vanilla IPotionType effects, custom effects, and mod-registered effect actions.
*/
private void applyAllEffects(IEntityLivingBase caster, IEntityLivingBase IEntity) {
applyEffects(IEntity);
applyCustomEffects(IEntity);
applyEffectActions(caster, IEntity);
}
private void applyCustomEffects(IEntityLivingBase IEntity) {
for (AbilityCustomEffect ce : customEffects) {
if (ce.isValid()) {
ce.apply(IEntity);
}
}
}
private void applyEffectActions(IEntityLivingBase caster, IEntityLivingBase IEntity) {
for (AbilityEffectActionEntry ea : effectActions) {
if (ea.isValid()) {
ea.apply(caster, IEntity);
}
}
}
// ==================== TARGET GATHERING ====================
@SuppressWarnings("unchecked")
private void findEntitiesInRadius(IEntityLivingBase caster, IWorld IWorld) {
IBoundingBox aabb = IBoundingBox.getBoundingBox(
caster.posX - radius, caster.posY - 2, caster.posZ - radius,
caster.posX + radius, caster.posY + 3, caster.posZ + radius
);
List<IEntity> entities = IWorld.getEntitiesWithinAABB(IEntityLivingBase.class, aabb);
for (IEntity IEntity : entities) {
if (!(IEntity instanceof IEntityLivingBase)) continue;
IEntityLivingBase living = (IEntityLivingBase) IEntity;
float dist = caster.getDistanceToEntity(living);
if (dist > radius) continue;
if (!AbilityTargetHelper.shouldAffect(caster, living, targetFilter, includeSelf)) continue;
getAffectedEntities().add(living);
}
}
private void healEntity(IEntityLivingBase caster, IEntityLivingBase IEntity) {
float totalHeal = healAmount;
if (healPercent > 0) {
totalHeal += IEntity.getMaxHealth() * healPercent;
}
if (totalHeal > 0) {
if (!AbilityController.Instance.fireOnAbilityHeal(this, caster, IEntity, totalHeal)) {
IEntity.heal(totalHeal);
}
}
}
private void spawnHealParticles(IWorld IWorld, IEntityLivingBase IEntity) {
for (int i = 0; i < 10; i++) {
double offsetX = (IWorld.rand.nextDouble() - 0.5) * IEntity.width;
double offsetY = IWorld.rand.nextDouble() * IEntity.height;
double offsetZ = (IWorld.rand.nextDouble() - 0.5) * IEntity.width;
IWorld.spawnParticle("happyVillager",
IEntity.posX + offsetX,
IEntity.posY + offsetY,
IEntity.posZ + offsetZ,
0, 0.1, 0);
}
}
// ==================== LIFECYCLE ====================
@Override
public void resetForBurst() {
getAffectedEntities().clear();
}
@Override
public void cleanup() {
getAffectedEntities().clear();
}
// ==================== VARIANTS ====================
@Override
public List<AbilityVariant> getVariants() {
return Arrays.asList(
new AbilityVariant("ability.variant.selfHeal", a -> {
AbilityEffect e = (AbilityEffect) a;
a.setName("Self Heal");
e.setTargetingMode(TargetingMode.SELF);
e.setIncludeSelf(true);
e.setInstantHeal(true);
}),
new AbilityVariant("ability.variant.healingAura", a -> {
AbilityEffect e = (AbilityEffect) a;
a.setName("Healing Aura");
e.setTargetingMode(TargetingMode.AOE_SELF);
e.setTargetFilter(TargetFilter.ALLIES);
e.setIncludeSelf(true);
e.setRadius(8.0f);
e.setBurstEnabled(true);
e.setBurstAmount(5);
e.setBurstDelay(20);
}),
new AbilityVariant("ability.variant.poisonDebuff", a -> {
AbilityEffect e = (AbilityEffect) a;
a.setName("Poison Debuff");
e.setTargetingMode(TargetingMode.AOE_SELF);
e.setTargetFilter(TargetFilter.ENEMIES);
e.setIncludeSelf(false);
e.setHealAmount(0);
e.setHealPercent(0);
e.setRadius(6.0f);
e.setBurstEnabled(true);
e.setBurstAmount(3);
e.setBurstDelay(40);
})
);
}
// ==================== NBT ====================
@Override
public void writeTypeNBT(INbt nbt) {
nbt.setInteger("durationTicks", durationTicks);
nbt.setFloat("healAmount", healAmount);
nbt.setFloat("healPercent", healPercent);
nbt.setBoolean("includeSelf", includeSelf);
nbt.setFloat("radius", radius);
nbt.setBoolean("instantHeal", instantHeal);
nbt.setString("targetFilter", targetFilter.name());
// Custom effects
INbtList ceList = new INbtList();
for (AbilityCustomEffect ce : customEffects) {
ceList.appendTag(ce.writeNBT());
}
nbt.setTag("customEffects", ceList);
// Effect actions
INbtList eaList = new INbtList();
for (AbilityEffectActionEntry ea : effectActions) {
eaList.appendTag(ea.writeNBT());
}
nbt.setTag("effectActions", eaList);
}
@Override
public void readTypeNBT(INbt nbt) {
this.durationTicks = nbt.getInteger("durationTicks");
this.healAmount = nbt.getFloat("healAmount");
this.healPercent = nbt.getFloat("healPercent");
this.includeSelf = nbt.getBoolean("includeSelf");
this.radius = nbt.getFloat("radius");
this.instantHeal = nbt.getBoolean("instantHeal");
this.targetFilter = TargetFilter.fromString(nbt.getString("targetFilter"));
// Custom effects
customEffects.clear();
if (nbt.hasKey("customEffects")) {
INbtList ceList = nbt.getTagList("customEffects", 10);
for (int i = 0; i < ceList.tagCount(); i++) {
AbilityCustomEffect ce = AbilityCustomEffect.fromNBT(ceList.getCompoundTagAt(i));
if (ce.isValid()) customEffects.add(ce);
}
}
// Effect actions
effectActions.clear();
if (nbt.hasKey("effectActions")) {
INbtList eaList = nbt.getTagList("effectActions", 10);
for (int i = 0; i < eaList.tagCount(); i++) {
AbilityEffectActionEntry ea = AbilityEffectActionEntry.fromNBT(eaList.getCompoundTagAt(i));
effectActions.add(ea);
}
}
}
// ==================== GUI ====================
@ClientOnly
@Override
public void getAbilityDefinitions(List<FieldDef> defs) {
defs.addAll(Arrays.asList(
FieldDef.boolField("ability.instantHeal", this::isInstantHeal, this::setInstantHeal)
.hover("ability.hover.instant"),
FieldDef.intField("ability.duration", this::getDurationTicks, this::setDurationTicks)
.range(1, 1000).visibleWhen(() -> !this.isInstantHeal()),
FieldDef.section("ability.section.healing"),
FieldDef.row(
FieldDef.floatField("ability.healAmount", this::getHealAmount, this::setHealAmount),
FieldDef.floatField("ability.healPercent", this::getHealPercent, this::setHealPercent)
),
FieldDef.section("ability.section.targeting")
.tab("Target").visibleWhen(() -> this.targetingMode == TargetingMode.AOE_SELF),
FieldDef.enumField("ability.targetFilter", TargetFilter.class,
this::getTargetFilter, this::setTargetFilter)
.tab("Target").visibleWhen(() -> this.targetingMode == TargetingMode.AOE_SELF),
FieldDef.row(
FieldDef.boolField("ability.includeSelf", this::isIncludeSelf, this::setIncludeSelf)
.visibleWhen(() -> this.targetingMode == TargetingMode.AOE_SELF),
FieldDef.floatField("gui.radius", this::getRadius, this::setRadius)
.visibleWhen(() -> this.targetingMode == TargetingMode.AOE_SELF)
).tab("Target"),
AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects),
AbilityFieldDefs.customEffectsListField("ability.customEffects", this::getCustomEffects, this::setCustomEffects),
AbilityFieldDefs.effectActionsListField("ability.effectActions", this::getEffectActionEntries, this::setEffectActionEntries)
.visibleWhen(() -> AbilityController.Instance.hasEffectActions())
));
}
// ==================== GETTERS & SETTERS ====================
@Override
public int getDurationTicks() {
return durationTicks;
}
@Override
public void setDurationTicks(int durationTicks) {
this.durationTicks = Math.max(1, durationTicks);
}
@Override
public float getHealAmount() {
return healAmount;
}
@Override
public void setHealAmount(float healAmount) {
this.healAmount = healAmount;
}
@Override
public float getHealPercent() {
return healPercent;
}
@Override
public void setHealPercent(float healPercent) {
this.healPercent = healPercent;
}
@Override
public boolean isIncludeSelf() {
return includeSelf;
}
@Override
public void setIncludeSelf(boolean includeSelf) {
this.includeSelf = includeSelf;
}
@Override
public float getRadius() {
return radius;
}
@Override
public void setRadius(float radius) {
this.radius = radius;
}
@Override
public boolean isInstantHeal() {
return instantHeal;
}
@Override
public void setInstantHeal(boolean instantHeal) {
this.instantHeal = instantHeal;
}
public TargetFilter getTargetFilter() {
return targetFilter;
}
public void setTargetFilter(TargetFilter targetFilter) {
this.targetFilter = targetFilter;
}
@Override
public int getTargetFilterType() {
return targetFilter.ordinal();
}
@Override
public void setTargetFilterType(int filter) {
TargetFilter[] values = TargetFilter.values();
if (filter >= 0 && filter < values.length) {
this.targetFilter = values[filter];
}
}
public List<AbilityCustomEffect> getCustomEffects() {
return customEffects;
}
public void setCustomEffects(List<AbilityCustomEffect> customEffects) {
this.customEffects = customEffects != null ? customEffects : new ArrayList<>();
}
@Override
public int getCustomEffectCount() {
return customEffects.size();
}
public List<AbilityEffectActionEntry> getEffectActionEntries() {
return effectActions;
}
public void setEffectActionEntries(List<AbilityEffectActionEntry> effectActions) {
this.effectActions = effectActions != null ? effectActions : new ArrayList<>();
}
@Override
public int getEffectActionCount() {
return effectActions.size();
}
}
@@ -0,0 +1,110 @@
package kamkeel.npcs.controllers.data.ability.type;
import noppes.npcs.constants.ClientOnly;
import kamkeel.npcs.util.IVector3;
import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler;
import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback;
import kamkeel.npcs.controllers.data.ability.gui.FieldDef;
import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider;
import noppes.npcs.entity.EntityNPCInterface;
import kamkeel.npcs.entity.EntityEnergyDome;
import kamkeel.npcs.entity.EntityEnergyBarrier;
import kamkeel.npcs.entity.EntityEnergyPanel;
import kamkeel.npcs.entity.EntityAbilityOrb;
import kamkeel.npcs.entity.EntityAbilityLaser;
import kamkeel.npcs.entity.EntityAbilityDisc;
import kamkeel.npcs.entity.EntityAbilityBeam;
import kamkeel.npcs.entity.EntityEnergyProjectile;
import kamkeel.npcs.util.ByteBufUtils;
import noppes.npcs.api.entity.IEntityLiving;
import noppes.npcs.api.IDamageSource;
import noppes.npcs.api.IWorld;
import noppes.npcs.api.entity.IEntityLivingBase;
import noppes.npcs.api.item.IItemStack;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.api.INbtList;
import noppes.npcs.api.INbt;
import kamkeel.npcs.controllers.data.ability.enums.UserType;
import noppes.npcs.api.ability.type.IAbilityGuard;
import java.util.List;
/**
* Guard ability: Defensive stance that reduces incoming damage.
*/
public class AbilityGuard extends AbilityDefend implements IAbilityGuard {
private float damageReduction = 0.5f;
public AbilityGuard() {
this.typeId = "ability.cnpc.guard";
this.name = "Guard";
this.cooldownTicks = 0;
this.allowedBy = UserType.BOTH;
this.activeAnimationName = "Ability_Guard_Active";
this.defaultIconLayers = new DefaultIconLayer[]{
new DefaultIconLayer("customnpcs:textures/gui/ability/guard.png")
};
}
// ═══════════════════════════════════════════════════════════════════
// DEFEND HOOKS
// ═══════════════════════════════════════════════════════════════════
/**
* Guard accepts ALL damage types (melee, projectile, magic, fire, IExplosion).
*/
@Override
protected boolean isValidDamageSource(IDamageSource source) {
return true;
}
@Override
protected float performDefend(IEntityLivingBase attacker, float amount) {
return Math.max(0, amount * (1.0f - damageReduction));
}
// ═══════════════════════════════════════════════════════════════════
// NBT
// ═══════════════════════════════════════════════════════════════════
@Override
protected void writeSubTypeNBT(INbt nbt) {
nbt.setFloat("damageReduction", damageReduction);
}
@Override
protected void readSubTypeNBT(INbt nbt) {
this.damageReduction = nbt.hasKey("damageReduction") ? nbt.getFloat("damageReduction") : 0.5f;
}
// ═══════════════════════════════════════════════════════════════════
// GUI
// ═══════════════════════════════════════════════════════════════════
@ClientOnly
@Override
protected void getTypeDefinitions(List<FieldDef> defs) {
defs.add(FieldDef.floatField("ability.damageReduction", this::getDamageReduction, this::setDamageReduction));
FieldDef.modifyVisibility(defs, "ability.windUpAnimation", () -> false);
FieldDef.modifyVisibility(defs, "ability.windUpSound", () -> false);
}
// ═══════════════════════════════════════════════════════════════════
// GETTERS / SETTERS
// ═══════════════════════════════════════════════════════════════════
@Override
public float getDamageReduction() {
return damageReduction;
}
@Override
public void setDamageReduction(float damageReduction) {
this.damageReduction = damageReduction;
}
}
@@ -0,0 +1,200 @@
package kamkeel.npcs.controllers.data.ability.type;
import noppes.npcs.constants.ClientOnly;
import kamkeel.npcs.util.IVector3;
import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler;
import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback;
import kamkeel.npcs.controllers.data.ability.gui.FieldDef;
import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider;
import noppes.npcs.entity.EntityNPCInterface;
import kamkeel.npcs.entity.EntityEnergyDome;
import kamkeel.npcs.entity.EntityEnergyBarrier;
import kamkeel.npcs.entity.EntityEnergyPanel;
import kamkeel.npcs.entity.EntityAbilityOrb;
import kamkeel.npcs.entity.EntityAbilityLaser;
import kamkeel.npcs.entity.EntityAbilityDisc;
import kamkeel.npcs.entity.EntityAbilityBeam;
import kamkeel.npcs.entity.EntityEnergyProjectile;
import kamkeel.npcs.util.ByteBufUtils;
import noppes.npcs.api.entity.IEntityLiving;
import noppes.npcs.api.IDamageSource;
import noppes.npcs.api.IWorld;
import noppes.npcs.api.entity.IEntityLivingBase;
import noppes.npcs.api.item.IItemStack;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.api.INbtList;
import noppes.npcs.api.INbt;
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyDisplayData;
import noppes.npcs.api.ability.type.IAbilityHazard;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class AbilityHazard extends AbilityZone implements IAbilityHazard {
private float radius = 2.0f;
private float damagePerSecond = 1.0f;
private int damageInterval = 20;
private boolean affectsCaster = false;
public AbilityHazard() {
super(300, new EnergyDisplayData(0x00CC00, 0x006600, true, 1.0f, 0.5f, 1.5f));
this.typeId = "ability.cnpc.hazard";
this.name = "Hazard";
this.windUpTicks = 30;
this.windUpAnimationName = "Ability_Zone_Windup";
this.activeAnimationName = "Ability_Zone_Active";
this.defaultIconLayers = new DefaultIconLayer[]{
new DefaultIconLayer("customnpcs:textures/gui/ability/hazard.png"),
new DefaultIconLayer("customnpcs:textures/gui/ability/hazard_overlay.png",
this::getActiveColor)
};
}
@Override
public boolean isConcurrentCapable() {
return true;
}
@Override
public float getZoneRadius() {
return radius;
}
@Override
public void onExecute(IEntityLivingBase caster, IEntityLivingBase target) {
activeEntities.clear();
List<double[]> positions;
if (!preCalculatedPositions.isEmpty() && preCalculatedPositions.size() == zoneCount) {
positions = new ArrayList<>(preCalculatedPositions);
preCalculatedPositions.clear();
} else {
positions = new ArrayList<>();
List<double[]> placedPositions = new ArrayList<>();
float minSeparation = radius * 2.0f;
for (int i = 0; i < zoneCount; i++) {
double[] pos = findSpawnPosition(caster, placedPositions, minSeparation);
placedPositions.add(pos);
positions.add(pos);
}
}
for (int i = 0; i < zoneCount; i++) {
double[] pos = positions.get(i);
EntityAbilityZone IEntity = EntityAbilityZone.createHazard(caster.worldObj, caster,
pos[0], caster.posY, pos[1],
zoneShape,
radius,
damagePerSecond, damageInterval,
isIgnoreIFrames(), affectsCaster,
durationTicks,
colorData.innerColor, colorData.outerColor, colorData.outerColorEnabled,
zoneHeight,
particleDensity, particleScale, animSpeed, lightningDensity,
getEffects());
IEntity.setSourceAbility(this);
applyVisualToEntity(IEntity);
if (isPreview()) {
IEntity.setupPreview(caster);
}
spawnAbilityEntity(IEntity);
activeEntities.add(IEntity);
}
}
@Override
public void writeTypeNBT(INbt nbt) {
writeZoneNBT(nbt);
nbt.setFloat("radius", radius);
nbt.setFloat("damagePerSecond", damagePerSecond);
nbt.setInteger("damageInterval", damageInterval);
nbt.setBoolean("affectsCaster", affectsCaster);
}
@Override
public void readTypeNBT(INbt nbt) {
readZoneNBT(nbt, 300);
this.radius = nbt.getFloat("radius");
this.damagePerSecond = nbt.getFloat("damagePerSecond");
this.damageInterval = nbt.getInteger("damageInterval");
this.affectsCaster = nbt.getBoolean("affectsCaster");
}
public float getRadius() {
return radius;
}
public void setRadius(float radius) {
this.radius = radius;
}
public float getDamagePerSecond() {
return damagePerSecond;
}
public void setDamagePerSecond(float damagePerSecond) {
this.damagePerSecond = damagePerSecond;
}
@Override
public float getDisplayDamage() { return damagePerSecond; }
@Override
public boolean isDisplayDamageDPS() { return true; }
public int getDamageInterval() {
return damageInterval;
}
public void setDamageInterval(int damageInterval) {
this.damageInterval = damageInterval;
}
public boolean isAffectsCaster() {
return affectsCaster;
}
public void setAffectsCaster(boolean affectsCaster) {
this.affectsCaster = affectsCaster;
}
@ClientOnly
@Override
public void getAbilityDefinitions(List<FieldDef> defs) {
addPresetFieldDef(defs);
defs.addAll(Arrays.asList(
FieldDef.row(
FieldDef.intField("ability.duration", this::getDurationTicks, this::setDurationTicks).range(1, 2000),
FieldDef.enumField("ability.zoneShape", ZoneShape.class, this::getZoneShapeEnum, this::setZoneShapeEnum)
),
FieldDef.section("ability.section.zone"),
FieldDef.row(
FieldDef.floatField("gui.radius", this::getSpawnRadius, this::setSpawnRadius),
FieldDef.intField("gui.count", this::getZoneCount, this::setZoneCount).range(1, 20)
),
FieldDef.floatField("gui.height", this::getZoneHeight, this::setZoneHeight),
FieldDef.section("ability.section.area"),
FieldDef.floatField("gui.radius", this::getRadius, this::setRadius),
FieldDef.section("ability.section.damage"),
FieldDef.row(
FieldDef.floatField("gui.dps", this::getDamagePerSecond, this::setDamagePerSecond),
FieldDef.intField("gui.interval", this::getDamageInterval, this::setDamageInterval)
),
FieldDef.boolField("ability.affectsCaster", this::isAffectsCaster, this::setAffectsCaster)
.hover("ability.hover.affectsCaster"),
AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects)
));
addVisualFieldDefs(defs);
}
}
@@ -0,0 +1,260 @@
package kamkeel.npcs.controllers.data.ability.type;
import noppes.npcs.constants.ClientOnly;
import kamkeel.npcs.util.IVector3;
import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler;
import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback;
import kamkeel.npcs.controllers.data.ability.gui.FieldDef;
import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider;
import noppes.npcs.entity.EntityNPCInterface;
import kamkeel.npcs.entity.EntityEnergyDome;
import kamkeel.npcs.entity.EntityEnergyBarrier;
import kamkeel.npcs.entity.EntityEnergyPanel;
import kamkeel.npcs.entity.EntityAbilityOrb;
import kamkeel.npcs.entity.EntityAbilityLaser;
import kamkeel.npcs.entity.EntityAbilityDisc;
import kamkeel.npcs.entity.EntityAbilityBeam;
import kamkeel.npcs.entity.EntityEnergyProjectile;
import kamkeel.npcs.util.ByteBufUtils;
import noppes.npcs.api.entity.IEntityLiving;
import noppes.npcs.api.IDamageSource;
import noppes.npcs.api.IWorld;
import noppes.npcs.api.entity.IEntityLivingBase;
import noppes.npcs.api.item.IItemStack;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.api.INbtList;
import noppes.npcs.api.INbt;
import kamkeel.npcs.controllers.data.ability.Ability;
import kamkeel.npcs.controllers.data.ability.enums.LockMode;
import kamkeel.npcs.controllers.data.ability.util.AbilityTargetHelper;
import kamkeel.npcs.controllers.data.ability.enums.TargetFilter;
import kamkeel.npcs.controllers.data.ability.enums.TargetingMode;
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
import noppes.npcs.api.ability.type.IAbilityHeavyHit;
import java.util.Arrays;
import java.util.List;
/**
* Heavy Hit ability: AOE rectangle melee attack in front of the caster.
* Deals high damage to all entities within a rectangular zone and can apply effects.
* Has a LINE telegraph showing the hit area during windup, making it dodgeable.
*/
public class AbilityHeavyHit extends Ability implements IAbilityHeavyHit {
private float damage = 8.0f;
private float knockback = 2.0f;
private float hitLength = 4.0f; // How far in front of the caster the hit reaches
private float hitWidth = 3.0f; // How wide to each side (total width = hitWidth * 2)
private int hitDelayTicks = 0;
private int activeDisplayTicks = 10; // How long active animation plays before completing
public AbilityHeavyHit() {
this.typeId = "ability.cnpc.heavy_hit";
this.name = "Heavy Hit";
this.targetingMode = TargetingMode.AOE_SELF;
this.maxRange = 5.0f;
this.minRange = 0.0f;
this.lockMovement = LockMode.WINDUP;
this.cooldownTicks = 0;
this.windUpTicks = 30;
this.telegraphType = TelegraphType.LINE;
this.showTelegraph = true;
this.windUpSound = "random.anvil_use";
this.activeSound = "random.anvil_land";
this.windUpAnimationName = "Ability_HeavyHit_Windup";
this.activeAnimationName = "Ability_HeavyHit_Active";
this.defaultIconLayers = new DefaultIconLayer[]{
new DefaultIconLayer("customnpcs:textures/gui/ability/heavy_hit.png"),
new DefaultIconLayer("customnpcs:textures/gui/ability/heavy_hit_overlay.png",
this::getActiveColor)
};
}
@Override
public boolean isTargetingModeLocked() {
return true;
}
@Override
public TargetingMode[] getAllowedTargetingModes() {
return new TargetingMode[]{TargetingMode.AOE_SELF};
}
@Override
public float getTelegraphLength() {
return hitLength;
}
@Override
public float getTelegraphWidth() {
return hitWidth * 2.0f; // Total width (hitWidth is distance to each side)
}
@Override
public void onExecute(IEntityLivingBase caster, IEntityLivingBase target) {
if (caster.worldObj.isRemote && !isPreview()) {
signalCompletion();
}
}
@Override
public void onActiveTick(IEntityLivingBase caster, IEntityLivingBase target, int tick) {
if (isPreview()) {
if (tick >= (activeDisplayTicks + hitDelayTicks))
signalCompletion();
return;
}
if (tick >= (activeDisplayTicks + hitDelayTicks)) {
signalCompletion();
return;
}
if (tick == hitDelayTicks && !caster.worldObj.isRemote) {
// Calculate forward and right vectors from caster yaw
float yawRad = (float) Math.toRadians(caster.rotationYaw);
double forwardX = -Math.sin(yawRad);
double forwardZ = Math.cos(yawRad);
double rightX = forwardZ; // perpendicular right
double rightZ = -forwardX;
// Search area: AABB that encompasses the rectangle
float searchDist = Math.max(hitLength, hitWidth) + 1.0f;
@SuppressWarnings("unchecked")
List<IEntity> entities = caster.worldObj.getEntitiesWithinAABBExcludingEntity(
caster, caster.boundingBox.expand(searchDist, 2, searchDist));
boolean anyHit = false;
for (IEntity IEntity : entities) {
if (!(IEntity instanceof IEntityLivingBase) || IEntity == caster) continue;
IEntityLivingBase livingTarget = (IEntityLivingBase) IEntity;
if (!AbilityTargetHelper.shouldAffect(caster, livingTarget, TargetFilter.ENEMIES, false)) continue;
double dx = livingTarget.posX - caster.posX;
double dz = livingTarget.posZ - caster.posZ;
// Project onto forward direction (must be in front, within hitLength)
double forwardDist = dx * forwardX + dz * forwardZ;
if (forwardDist < 0 || forwardDist > hitLength) continue;
// Project onto right direction (must be within hitWidth to each side)
double sideDist = dx * rightX + dz * rightZ;
if (Math.abs(sideDist) > hitWidth) continue;
// Line-of-sight check: skip targets behind solid blocks or enemy barriers
if (!hasLineOfSight(caster.worldObj, caster, livingTarget)) continue;
if (isBlockedByBarrier(caster.worldObj, caster, livingTarget)) continue;
// IEntity is within the rectangle - apply damage
boolean wasHit = applyAbilityDamage(caster, livingTarget, damage, knockback);
if (wasHit) {
applyEffects(livingTarget);
anyHit = true;
}
}
// Play hit sound even if nothing was hit (the attack still happens)
if (!anyHit) {
caster.worldObj.playSoundAtEntity(caster, "random.anvil_land", 0.5f, 1.2f);
}
}
}
@Override
public void writeTypeNBT(INbt nbt) {
nbt.setFloat("damage", damage);
nbt.setFloat("knockback", knockback);
nbt.setFloat("hitLength", hitLength);
nbt.setFloat("hitWidth", hitWidth);
nbt.setInteger("activeDisplayTicks", activeDisplayTicks);
nbt.setInteger("hitDelayTicks", hitDelayTicks);
}
@Override
public void readTypeNBT(INbt nbt) {
this.damage = nbt.getFloat("damage");
this.knockback = nbt.getFloat("knockback");
this.hitLength = nbt.getFloat("hitLength");
this.hitWidth = nbt.getFloat("hitWidth");
this.hitDelayTicks = nbt.getInteger("hitDelayTicks");
this.activeDisplayTicks = nbt.getInteger("activeDisplayTicks");
}
// Getters & Setters
public float getDamage() {
return damage;
}
public void setDamage(float damage) {
this.damage = damage;
}
@Override
public float getDisplayDamage() { return damage; }
public float getKnockback() {
return knockback;
}
public void setKnockback(float knockback) {
this.knockback = knockback;
}
public float getHitLength() {
return hitLength;
}
public void setHitLength(float hitLength) {
this.hitLength = hitLength;
}
public float getHitWidth() {
return hitWidth;
}
public void setHitWidth(float hitWidth) {
this.hitWidth = hitWidth;
}
public int getActiveDisplayTicks() {
return activeDisplayTicks;
}
public void setActiveDisplayTicks(int activeDisplayTicks) {
this.activeDisplayTicks = activeDisplayTicks;
}
public int getHitDelayTicks() {
return hitDelayTicks;
}
public void setHitDelayTicks(int hitDelayTicks) {
this.hitDelayTicks = hitDelayTicks;
}
@ClientOnly
@Override
public void getAbilityDefinitions(List<FieldDef> defs) {
defs.addAll(Arrays.asList(
FieldDef.row(
FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage),
FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback)
),
FieldDef.section("ability.section.hitZone"),
FieldDef.row(
FieldDef.floatField("ability.hitLength", this::getHitLength, this::setHitLength),
FieldDef.floatField("ability.hitWidth", this::getHitWidth, this::setHitWidth)
),
FieldDef.section("ability.section.timing"),
FieldDef.row(
FieldDef.intField("ability.hitDelayTicks", this::getHitDelayTicks, this::setHitDelayTicks).min(0),
FieldDef.intField("ability.activeDisplayTicks", this::getActiveDisplayTicks, this::setActiveDisplayTicks).range(1, 200)
),
AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects)
));
}
}
@@ -0,0 +1,323 @@
package kamkeel.npcs.controllers.data.ability.type;
import noppes.npcs.constants.ClientOnly;
import kamkeel.npcs.util.IVector3;
import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler;
import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback;
import kamkeel.npcs.controllers.data.ability.gui.FieldDef;
import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider;
import noppes.npcs.entity.EntityNPCInterface;
import kamkeel.npcs.entity.EntityEnergyDome;
import kamkeel.npcs.entity.EntityEnergyBarrier;
import kamkeel.npcs.entity.EntityEnergyPanel;
import kamkeel.npcs.entity.EntityAbilityOrb;
import kamkeel.npcs.entity.EntityAbilityLaser;
import kamkeel.npcs.entity.EntityAbilityDisc;
import kamkeel.npcs.entity.EntityAbilityBeam;
import kamkeel.npcs.entity.EntityEnergyProjectile;
import noppes.npcs.api.entity.IEntityLiving;
import noppes.npcs.api.IDamageSource;
import noppes.npcs.api.IWorld;
import noppes.npcs.api.entity.IEntityLivingBase;
import noppes.npcs.api.item.IItemStack;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.api.INbtList;
import noppes.npcs.api.INbt;
import kamkeel.npcs.controllers.data.ability.Ability;
/**
* Abstract base class for abilities that move the caster along the ground
* in a specific direction (Charge, Dash, etc.).
* <p>
* Provides shared movement logic: direction locking, stall detection with
* player-aware grace period, velocity application, distance tracking,
* rotation enforcement, and wall collision checking.
* <p>
* NOT for ballistic/arc movement (Slam uses a different pattern).
*/
public abstract class AbilityMovement extends Ability {
/**
* Grace period (in active ticks) before stall detection activates.
* Players need more time because:
* - Server sets motionX/Z and sends S12 velocity packet to client
* - Client receives S12 and starts moving
* - Client sends C03 position packet with updated position back to server
* Until the round trip completes, the server sees the old (stationary) position,
* which would falsely trigger stall detection.
*/
private static final int PLAYER_STALL_GRACE_TICKS = 8;
private static final int NPC_STALL_GRACE_TICKS = 2;
// ═══════════════════════════════════════════════════════════════════
// COMMON MOVEMENT STATE (transient, not saved to NBT)
// ═══════════════════════════════════════════════════════════════════
protected transient double startX, startY, startZ;
protected transient double prevTickX, prevTickZ;
protected transient IVector3 movementDirection;
protected transient float lockedYaw;
protected transient int maxActiveTicks;
// ═══════════════════════════════════════════════════════════════════
// OVERRIDES
// ═══════════════════════════════════════════════════════════════════
@Override
public boolean hasAbilityMovement() {
return true;
}
@Override
public void cleanup() {
movementDirection = null;
maxActiveTicks = 0;
}
@Override
public void resetForBurst() {
movementDirection = null;
maxActiveTicks = 0;
}
// ═══════════════════════════════════════════════════════════════════
// DIRECTION HELPERS
// ═══════════════════════════════════════════════════════════════════
/**
* Lock direction based on caster type.
* NPC: toward target. Player: look direction.
*/
protected void lockDirection(IEntityLivingBase caster, IEntityLivingBase target) {
if (!isPlayerCaster(caster) && target != null) {
lockDirectionToTarget(caster, target);
} else {
lockDirectionFromLook(caster);
}
}
/**
* Lock direction to the caster's look direction (horizontal only).
*/
protected void lockDirectionFromLook(IEntityLivingBase caster) {
float yaw = (float) Math.toRadians(caster.rotationYaw);
movementDirection = IVector3.createVectorHelper(-Math.sin(yaw), 0, Math.cos(yaw));
lockedYaw = computeYawFromDirection();
}
/**
* Lock direction toward a target IEntity.
* Falls back to look direction if target is at the same position.
*/
protected void lockDirectionToTarget(IEntityLivingBase caster, IEntityLivingBase target) {
double dx = target.posX - caster.posX;
double dz = target.posZ - caster.posZ;
double len = Math.sqrt(dx * dx + dz * dz);
if (len > 0) {
movementDirection = IVector3.createVectorHelper(dx / len, 0, dz / len);
lockedYaw = computeYawFromDirection();
} else {
lockDirectionFromLook(caster);
}
}
/**
* Get the base yaw for direction calculation.
* NPC: yaw toward target. Player: current look yaw.
*/
protected float getBaseYaw(IEntityLivingBase caster, IEntityLivingBase target) {
if (!isPlayerCaster(caster) && target != null) {
double dx = target.posX - caster.posX;
double dz = target.posZ - caster.posZ;
return (float) Math.toDegrees(Math.atan2(-dx, dz));
}
return caster.rotationYaw;
}
/**
* Set movement direction from a yaw angle (degrees).
*/
protected void setDirectionFromYaw(float yawDegrees) {
float yawRad = (float) Math.toRadians(yawDegrees);
movementDirection = IVector3.createVectorHelper(-Math.sin(yawRad), 0, Math.cos(yawRad));
lockedYaw = yawDegrees;
}
// ═══════════════════════════════════════════════════════════════════
// MOVEMENT INITIALIZATION
// ═══════════════════════════════════════════════════════════════════
/**
* Initialize movement tracking. Call from onExecute().
*
* @param caster The IEntity performing the movement
* @param distance Maximum travel distance (for timeout computation)
* @param speed Movement speed in blocks/tick (for timeout computation)
*/
protected void initMovement(IEntityLivingBase caster, float distance, float speed) {
startX = caster.posX;
startY = caster.posY;
startZ = caster.posZ;
prevTickX = caster.posX;
prevTickZ = caster.posZ;
maxActiveTicks = speed > 0 ? (int) (distance / speed) + 10 : 10;
}
// ═══════════════════════════════════════════════════════════════════
// ACTIVE TICK CHECKS
// ═══════════════════════════════════════════════════════════════════
/**
* Check for timeout or missing movement direction.
*
* @return true if the ability should stop
*/
protected boolean checkTimeout(int tick) {
return !isPreview() && (movementDirection == null || tick > maxActiveTicks);
}
/**
* Check stall detection with appropriate grace period for player vs NPC.
* Players need a longer grace period because velocity sync via S12 packet
* takes time to reach the client, and C03 position packets lag behind.
*
* @return true if the IEntity is stalled (not moving)
*/
protected boolean checkStall(IEntityLivingBase caster, int tick) {
if (isPreview()) return false;
int graceTicks = isPlayerCaster(caster) ? PLAYER_STALL_GRACE_TICKS : NPC_STALL_GRACE_TICKS;
if (tick <= graceTicks) return false;
double dx = caster.posX - prevTickX;
double dz = caster.posZ - prevTickZ;
return dx * dx + dz * dz < 0.0001;
}
/**
* Update previous tick position for stall detection.
* Call after stall check, before movement application.
*/
protected void updatePrevPosition(IEntityLivingBase caster) {
prevTickX = caster.posX;
prevTickZ = caster.posZ;
}
/**
* Get total linear distance traveled from start position.
*/
protected double getDistanceTraveled(IEntityLivingBase caster) {
double dx = caster.posX - startX;
double dz = caster.posZ - startZ;
return Math.sqrt(dx * dx + dz * dz);
}
/**
* Get total distance traveled squared (more efficient for comparison).
*/
protected double getDistanceTraveledSq(IEntityLivingBase caster) {
double dx = caster.posX - startX;
double dz = caster.posZ - startZ;
return dx * dx + dz * dz;
}
/**
* Check if movement direction is blocked by walls or solid barriers.
*
* @return true if blocked
*/
protected boolean checkBlocked(IEntityLivingBase caster, float speed) {
if (isPreview() || movementDirection == null) return false;
return isMovementBlocked(caster, movementDirection.xCoord, movementDirection.zCoord, speed)
|| isMovementBlockedByBarrier(caster, movementDirection.xCoord, movementDirection.zCoord, speed);
}
// ═══════════════════════════════════════════════════════════════════
// VELOCITY APPLICATION
// ═══════════════════════════════════════════════════════════════════
/**
* Send an authoritative velocity packet to a player.
* <p>
* The ability tick runs BEFORE moveEntityWithHeading (at PlayerTickEvent.START).
* If we use velocityChanged, the IEntity tracker sends S12 AFTER friction has
* reduced the motion (~55% of set value). The client then moves at reduced speed,
* reports a short position via C04, and the server resets to the client's position.
* <p>
* By sending S12 manually here — before friction — the client receives the full
* velocity and moves at the correct speed.
*/
protected void sendPlayerVelocity(IEntityLivingBase caster) {
if (caster instanceof IPlayer) {
((IPlayer) caster).playerNetServerHandler.sendPacket(
new S12PacketEntityVelocity(caster));
}
}
/**
* Apply horizontal momentum and sync to client.
*/
protected void applyHorizontalMomentum(IEntityLivingBase caster, double motionX, double motionZ) {
caster.motionX = motionX;
caster.motionZ = motionZ;
if (!isPreview()) {
if (caster instanceof IPlayer) {
sendPlayerVelocity(caster);
} else {
caster.velocityChanged = true;
}
}
}
/**
* Apply movement velocity to the caster (horizontal only, Y unchanged).
*/
protected void applyVelocity(IEntityLivingBase 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.
* motionY is set BEFORE sending velocity packet to prevent client-server desync
* (which causes "moved too fast" kicks when used in mid-air).
*/
protected void applyVelocityFlat(IEntityLivingBase caster, float speed) {
caster.motionY = 0;
applyHorizontalMomentum(caster, movementDirection.xCoord * speed, movementDirection.zCoord * speed);
}
/**
* Stop all horizontal momentum.
*/
protected void stopMomentum(IEntityLivingBase caster) {
applyHorizontalMomentum(caster, 0, 0);
}
// ═══════════════════════════════════════════════════════════════════
// ROTATION
// ═══════════════════════════════════════════════════════════════════
/**
* Compute yaw angle (degrees) from current movement direction.
*/
protected float computeYawFromDirection() {
if (movementDirection == null) return 0;
return (float) Math.toDegrees(Math.atan2(-movementDirection.xCoord, movementDirection.zCoord));
}
/**
* Enforce locked rotation on all yaw fields.
* Sets rotationYaw, rotationYawHead, prevRotationYaw, prevRotationYawHead,
* renderYawOffset, and prevRenderYawOffset to lockedYaw.
*/
protected void enforceLockedRotation(IEntityLivingBase caster) {
caster.rotationYaw = lockedYaw;
caster.rotationYawHead = lockedYaw;
caster.prevRotationYaw = lockedYaw;
caster.prevRotationYawHead = lockedYaw;
caster.renderYawOffset = lockedYaw;
caster.prevRenderYawOffset = lockedYaw;
}
}
@@ -0,0 +1,391 @@
package kamkeel.npcs.controllers.data.ability.type;
import noppes.npcs.constants.ClientOnly;
import kamkeel.npcs.util.IVector3;
import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler;
import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback;
import kamkeel.npcs.controllers.data.ability.gui.FieldDef;
import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider;
import noppes.npcs.entity.EntityNPCInterface;
import kamkeel.npcs.entity.EntityEnergyDome;
import kamkeel.npcs.entity.EntityEnergyBarrier;
import kamkeel.npcs.entity.EntityEnergyPanel;
import kamkeel.npcs.entity.EntityAbilityOrb;
import kamkeel.npcs.entity.EntityAbilityLaser;
import kamkeel.npcs.entity.EntityAbilityDisc;
import kamkeel.npcs.entity.EntityAbilityBeam;
import kamkeel.npcs.entity.EntityEnergyProjectile;
import kamkeel.npcs.util.ByteBufUtils;
import noppes.npcs.api.entity.IEntityLiving;
import noppes.npcs.api.IDamageSource;
import noppes.npcs.api.IWorld;
import noppes.npcs.api.entity.IEntityLivingBase;
import noppes.npcs.api.item.IItemStack;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.api.INbtList;
import noppes.npcs.api.INbt;
import kamkeel.npcs.controllers.data.ability.Ability;
import kamkeel.npcs.controllers.data.ability.enums.LockMode;
import kamkeel.npcs.controllers.data.ability.enums.TargetingMode;
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
import noppes.npcs.api.ability.type.IAbilityProjectile;
import java.util.Arrays;
import java.util.List;
/**
* Projectile ability: Ranged attack that deals damage to target.
* Currently instant damage - can be enhanced with custom projectile entities.
*/
public class AbilityProjectile extends Ability implements IAbilityProjectile {
// Type-specific parameters
private float damage = 6.0f;
private float speed = 1.5f;
private float knockback = 0.5f;
private String projectileType = "fireball";
private boolean explosive = false;
private float explosionRadius = 0.0f;
private boolean homing = false;
private float homingStrength = 0.1f;
public AbilityProjectile() {
this.typeId = "ability.cnpc.projectile";
this.name = "Projectile";
this.targetingMode = TargetingMode.AGGRO_TARGET;
this.minRange = 5.0f;
this.maxRange = 20.0f;
this.cooldownTicks = 0;
this.windUpTicks = 15;
this.lockMovement = LockMode.NO;
// No telegraph for projectile - it's a ranged attack
this.telegraphType = TelegraphType.NONE;
this.showTelegraph = false;
}
@Override
public boolean allowBurst() {
return false;
}
@Override
public boolean isTargetingModeLocked() {
return true;
}
@Override
public TargetingMode[] getAllowedTargetingModes() {
return new TargetingMode[]{TargetingMode.AGGRO_TARGET};
}
@Override
public void onExecute(IEntityLivingBase caster, IEntityLivingBase target) {
if (caster.worldObj.isRemote && !isPreview()) {
signalCompletion();
return;
}
if (isPlayerCaster(caster)) {
// Player: instant hit-scan in look direction
executePlayerProjectile(caster, caster.worldObj);
} else if (target != null) {
// NPC: instant hit-scan on aggro target
executeNpcProjectile(caster, target, caster.worldObj);
}
signalCompletion();
}
/**
* NPC projectile: instant hit-scan damage on the aggro target.
*/
private void executeNpcProjectile(IEntityLivingBase caster, IEntityLivingBase target, IWorld IWorld) {
if (isPreview()) return;
double dx = target.posX - caster.posX;
double dy = (target.posY + target.height / 2) - (caster.posY + caster.getEyeHeight());
double dz = target.posZ - caster.posZ;
double len = Math.sqrt(dx * dx + dy * dy + dz * dz);
if (len > 0) {
dx /= len;
dy /= len;
dz /= len;
}
applyAbilityDamageWithDirection(caster, target, damage, knockback, dx, dz);
IWorld.playSoundAtEntity(caster, "random.bow", 1.0f, 0.8f);
if (explosive && explosionRadius > 0) {
applyExplosionDamage(caster, target, IWorld, target.posX, target.posY, target.posZ);
}
spawnProjectileParticles(IWorld, caster, target);
}
/**
* Player projectile: fires instant hit-scan in look direction.
* Raycasts for the first IEntity in range and applies damage.
*/
private void executePlayerProjectile(IEntityLivingBase caster, IWorld IWorld) {
if (isPreview()) return;
IVector3 look = caster.getLookVec();
double eyeX = caster.posX;
double eyeY = caster.posY + caster.getEyeHeight();
double eyeZ = caster.posZ;
// Raycast: find the closest IEntity along look direction
IEntityLivingBase hitTarget = null;
double closestDist = maxRange;
@SuppressWarnings("unchecked")
List<IEntity> candidates = IWorld.getEntitiesWithinAABBExcludingEntity(caster,
caster.boundingBox.expand(maxRange, maxRange, maxRange));
for (IEntity IEntity : candidates) {
if (!(IEntity instanceof IEntityLivingBase)) continue;
if (IEntity.isDead) continue;
// Check if IEntity is roughly along look direction
double dx = IEntity.posX - eyeX;
double dy = (IEntity.posY + IEntity.height / 2) - eyeY;
double dz = IEntity.posZ - eyeZ;
double dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
if (dist > maxRange || dist < 0.5) continue;
// Dot product to check alignment with look vector
double dot = (dx * look.xCoord + dy * look.yCoord + dz * look.zCoord) / dist;
if (dot < 0.95) continue; // Must be within ~18 degree cone
if (dist < closestDist) {
closestDist = dist;
hitTarget = (IEntityLivingBase) IEntity;
}
}
IWorld.playSoundAtEntity(caster, "random.bow", 1.0f, 0.8f);
if (hitTarget != null) {
applyAbilityDamageWithDirection(caster, hitTarget, damage, knockback, look.xCoord, look.zCoord);
if (explosive && explosionRadius > 0) {
applyExplosionDamage(caster, hitTarget, IWorld, hitTarget.posX, hitTarget.posY, hitTarget.posZ);
}
spawnProjectileParticles(IWorld, caster, hitTarget);
} else {
// No hit — spawn particles along look direction anyway
spawnProjectileParticlesInDirection(IWorld, caster, look);
}
}
/**
* Apply IExplosion splash damage around a point.
*/
private void applyExplosionDamage(IEntityLivingBase caster, IEntityLivingBase primaryTarget, IWorld IWorld,
double x, double y, double z) {
@SuppressWarnings("unchecked")
List<IEntity> entities = IWorld.getEntitiesWithinAABBExcludingEntity(primaryTarget,
primaryTarget.boundingBox.expand(explosionRadius, explosionRadius, explosionRadius));
for (IEntity IEntity : entities) {
if (IEntity instanceof IEntityLivingBase && IEntity != caster) {
IEntityLivingBase living = (IEntityLivingBase) IEntity;
float dist = primaryTarget.getDistanceToEntity(living);
if (dist < explosionRadius) {
float falloff = 1.0f - (dist / explosionRadius);
applyAbilityDamage(caster, living, damage * falloff * 0.5f, 0);
}
}
}
IWorld.playSoundAtEntity(primaryTarget, "random.explode", 0.5f, 1.0f);
for (int i = 0; i < 10; i++) {
IWorld.spawnParticle("explode",
x + (IWorld.rand.nextDouble() - 0.5) * explosionRadius,
y + IWorld.rand.nextDouble() * 2,
z + (IWorld.rand.nextDouble() - 0.5) * explosionRadius,
0, 0.1, 0);
}
}
private void spawnProjectileParticles(IWorld IWorld, IEntityLivingBase caster, IEntityLivingBase target) {
double startX = caster.posX;
double startY = caster.posY + caster.getEyeHeight();
double startZ = caster.posZ;
double endX = target.posX;
double endY = target.posY + target.height / 2;
double endZ = target.posZ;
double dx = endX - startX;
double dy = endY - startY;
double dz = endZ - startZ;
double dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
String particle = "flame";
if (projectileType.equals("arrow")) {
particle = "crit";
} else if (projectileType.equals("magic")) {
particle = "witchMagic";
}
// Spawn particles along path
int steps = (int) Math.max(5, dist * 2);
for (int i = 0; i < steps; i++) {
double progress = (double) i / steps;
double px = startX + dx * progress;
double py = startY + dy * progress;
double pz = startZ + dz * progress;
IWorld.spawnParticle(particle, px, py, pz, 0, 0, 0);
}
}
/**
* Spawn projectile particles along a direction (player miss — no IEntity hit).
*/
private void spawnProjectileParticlesInDirection(IWorld IWorld, IEntityLivingBase caster, IVector3 look) {
double startX = caster.posX;
double startY = caster.posY + caster.getEyeHeight();
double startZ = caster.posZ;
String particle = "flame";
if (projectileType.equals("arrow")) {
particle = "crit";
} else if (projectileType.equals("magic")) {
particle = "witchMagic";
}
int steps = (int) Math.max(5, maxRange * 2);
for (int i = 0; i < steps; i++) {
double progress = (double) i / steps * maxRange;
double px = startX + look.xCoord * progress;
double py = startY + look.yCoord * progress;
double pz = startZ + look.zCoord * progress;
IWorld.spawnParticle(particle, px, py, pz, 0, 0, 0);
}
}
@Override
public void onActiveTick(IEntityLivingBase caster, IEntityLivingBase target, int tick) {
// Projectile is instant, nothing to do per-tick
}
@Override
public float getTelegraphRadius() {
return 0; // No telegraph for projectile
}
@Override
public void writeTypeNBT(INbt nbt) {
nbt.setFloat("damage", damage);
nbt.setFloat("speed", speed);
nbt.setFloat("knockback", knockback);
nbt.setString("projectileType", projectileType);
nbt.setBoolean("explosive", explosive);
nbt.setFloat("explosionRadius", explosionRadius);
nbt.setBoolean("homing", homing);
nbt.setFloat("homingStrength", homingStrength);
}
@Override
public void readTypeNBT(INbt nbt) {
this.damage = nbt.getFloat("damage");
this.speed = nbt.getFloat("speed");
this.knockback = nbt.getFloat("knockback");
this.projectileType = nbt.getString("projectileType");
this.explosive = nbt.getBoolean("explosive");
this.explosionRadius = nbt.getFloat("explosionRadius");
this.homing = nbt.getBoolean("homing");
this.homingStrength = nbt.getFloat("homingStrength");
}
// Getters & Setters
public float getDamage() {
return damage;
}
public void setDamage(float damage) {
this.damage = damage;
}
@Override
public float getDisplayDamage() { return damage; }
public float getSpeed() {
return speed;
}
public void setSpeed(float speed) {
this.speed = speed;
}
public float getKnockback() {
return knockback;
}
public void setKnockback(float knockback) {
this.knockback = knockback;
}
public String getProjectileType() {
return projectileType;
}
public void setProjectileType(String projectileType) {
this.projectileType = projectileType;
}
public boolean isExplosive() {
return explosive;
}
public void setExplosive(boolean explosive) {
this.explosive = explosive;
}
public float getExplosionRadius() {
return explosionRadius;
}
public void setExplosionRadius(float explosionRadius) {
this.explosionRadius = explosionRadius;
}
public boolean isHoming() {
return homing;
}
public void setHoming(boolean homing) {
this.homing = homing;
}
public float getHomingStrength() {
return homingStrength;
}
public void setHomingStrength(float homingStrength) {
this.homingStrength = homingStrength;
}
@ClientOnly
@Override
public void getAbilityDefinitions(List<FieldDef> defs) {
defs.addAll(Arrays.asList(
FieldDef.row(
FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage),
FieldDef.floatField("stats.speed", this::getSpeed, this::setSpeed)
),
FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback),
FieldDef.stringEnumField("ability.projectileType", new String[]{"fireball", "arrow", "magic"}, this::getProjectileType, this::setProjectileType),
FieldDef.section("ability.section.homing"),
FieldDef.boolField("gui.enabled", this::isHoming, this::setHoming).hover("ability.hover.homing"),
FieldDef.floatField("gui.strength", this::getHomingStrength, this::setHomingStrength).visibleWhen(this::isHoming),
FieldDef.section("ability.section.explosive"),
FieldDef.boolField("gui.enabled", this::isExplosive, this::setExplosive).hover("ability.hover.explosive"),
FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius).visibleWhen(this::isExplosive),
AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects)
));
}
}
@@ -0,0 +1,255 @@
package kamkeel.npcs.controllers.data.ability.type;
import noppes.npcs.constants.ClientOnly;
import kamkeel.npcs.util.IVector3;
import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler;
import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback;
import kamkeel.npcs.controllers.data.ability.gui.FieldDef;
import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider;
import noppes.npcs.entity.EntityNPCInterface;
import kamkeel.npcs.entity.EntityEnergyDome;
import kamkeel.npcs.entity.EntityEnergyBarrier;
import kamkeel.npcs.entity.EntityEnergyPanel;
import kamkeel.npcs.entity.EntityAbilityOrb;
import kamkeel.npcs.entity.EntityAbilityLaser;
import kamkeel.npcs.entity.EntityAbilityDisc;
import kamkeel.npcs.entity.EntityAbilityBeam;
import kamkeel.npcs.entity.EntityEnergyProjectile;
import kamkeel.npcs.util.ByteBufUtils;
import noppes.npcs.api.entity.IEntityLiving;
import noppes.npcs.api.IDamageSource;
import noppes.npcs.api.IWorld;
import noppes.npcs.api.entity.IEntityLivingBase;
import noppes.npcs.api.item.IItemStack;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.api.INbtList;
import noppes.npcs.api.INbt;
import kamkeel.npcs.controllers.data.ability.Ability;
import kamkeel.npcs.controllers.data.ability.util.AbilityTargetHelper;
import kamkeel.npcs.controllers.data.ability.enums.LockMode;
import kamkeel.npcs.controllers.data.ability.enums.TargetFilter;
import kamkeel.npcs.controllers.data.ability.enums.TargetingMode;
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
import noppes.npcs.api.ability.type.IAbilityShockwave;
import java.util.Arrays;
import java.util.List;
/**
* Shockwave ability: Pushes targets away from the caster with damage.
* Opposite of Vortex - instant knockback rather than gradual pull.
*/
public class AbilityShockwave extends Ability implements IAbilityShockwave {
private float pushRadius = 8.0f;
private float pushStrength = 1.5f;
private float damage = 8.0f;
private boolean aoe = true;
private int activeDisplayTicks = 10;
public AbilityShockwave() {
this.typeId = "ability.cnpc.shockwave";
this.name = "Shockwave";
this.targetingMode = TargetingMode.AOE_SELF;
this.maxRange = 8.0f;
this.lockMovement = LockMode.WINDUP_AND_ACTIVE;
this.cooldownTicks = 0;
this.windUpTicks = 25;
this.telegraphType = TelegraphType.CIRCLE;
this.windUpSound = "game.tnt.primed";
this.activeSound = "random.explode";
this.windUpAnimationName = "Ability_Shockwave_Windup";
this.activeAnimationName = "Ability_Shockwave_Active";
this.defaultIconLayers = new DefaultIconLayer[]{
new DefaultIconLayer("customnpcs:textures/gui/ability/shockwave.png"),
new DefaultIconLayer("customnpcs:textures/gui/ability/shockwave_overlay.png",
this::getActiveColor)
};
}
@Override
public boolean isTargetingModeLocked() {
return true;
}
@Override
public TargetingMode[] getAllowedTargetingModes() {
return new TargetingMode[]{TargetingMode.AOE_SELF};
}
@Override
public float getTelegraphRadius() {
return pushRadius;
}
@Override
public void onExecute(IEntityLivingBase caster, IEntityLivingBase target) {
if (!isPreview() && !caster.worldObj.isRemote) {
// Shockwave is instant - apply effect immediately after windup
// Get all entities in radius
IBoundingBox box = caster.boundingBox.expand(pushRadius, pushRadius / 2, pushRadius);
@SuppressWarnings("unchecked")
List<IEntityLivingBase> entities = caster.worldObj.getEntitiesWithinAABB(IEntityLivingBase.class, box);
if (aoe) {
// ALL: push every valid enemy in range
for (IEntityLivingBase IEntity : entities) {
if (IEntity == caster) continue;
if (IEntity.isDead) continue;
if (!AbilityTargetHelper.shouldAffect(caster, IEntity, TargetFilter.ENEMIES, false)) continue;
double dist = caster.getDistanceToEntity(IEntity);
if (dist > pushRadius) continue;
applyShockwavePush(caster, IEntity, dist);
}
} else {
// SINGULAR: push one target
// NPC: push aggro target if valid, otherwise nearest enemy
// Player: push nearest enemy in range
if (!isPlayerCaster(caster) && target != null && !target.isDead) {
double dist = caster.getDistanceToEntity(target);
if (dist <= pushRadius && AbilityTargetHelper.shouldAffect(caster, target, TargetFilter.ENEMIES, false)) {
applyShockwavePush(caster, target, dist);
return;
}
}
// Find nearest valid enemy
IEntityLivingBase nearest = null;
double nearestDist = Double.MAX_VALUE;
for (IEntityLivingBase IEntity : entities) {
if (IEntity == caster) continue;
if (IEntity.isDead) continue;
if (!AbilityTargetHelper.shouldAffect(caster, IEntity, TargetFilter.ENEMIES, false)) continue;
double dist = caster.getDistanceToEntity(IEntity);
if (dist <= pushRadius && dist < nearestDist) {
nearest = IEntity;
nearestDist = dist;
}
}
if (nearest != null) {
applyShockwavePush(caster, nearest, nearestDist);
}
}
}
}
private void applyShockwavePush(IEntityLivingBase caster, IEntityLivingBase IEntity, double dist) {
// Calculate push direction (away from caster)
double dx = IEntity.posX - caster.posX;
double dz = IEntity.posZ - caster.posZ;
double len = Math.sqrt(dx * dx + dz * dz);
if (len > 0) {
dx /= len;
dz /= len;
} else {
// IEntity is directly on top of caster, push in random direction
double angle = Math.random() * Math.PI * 2;
dx = Math.cos(angle);
dz = Math.sin(angle);
}
// Scale knockback by distance (closer = stronger)
float distFactor = 1.0f - (float) (dist / pushRadius) * 0.5f;
float finalPush = pushStrength * distFactor;
// Apply damage with custom knockback direction
boolean wasHit = applyAbilityDamageWithDirection(caster, IEntity, damage * distFactor, finalPush, dx, dz);
// Apply effects if hit connected
if (wasHit) {
applyEffects(IEntity);
}
}
@Override
public void onActiveTick(IEntityLivingBase caster, IEntityLivingBase target, int tick) {
if (tick >= activeDisplayTicks)
signalCompletion();
}
@Override
public void writeTypeNBT(INbt nbt) {
nbt.setFloat("pushRadius", pushRadius);
nbt.setFloat("pushStrength", pushStrength);
nbt.setFloat("damage", damage);
nbt.setBoolean("aoe", aoe);
nbt.setInteger("activeDisplayTicks", activeDisplayTicks);
}
@Override
public void readTypeNBT(INbt nbt) {
this.pushRadius = nbt.getFloat("pushRadius");
this.pushStrength = nbt.getFloat("pushStrength");
this.damage = nbt.getFloat("damage");
this.aoe = !nbt.hasKey("aoe") || nbt.getBoolean("aoe");
this.activeDisplayTicks = nbt.hasKey("activeDisplayTicks") ? nbt.getInteger("activeDisplayTicks") : 10;
}
// Getters & Setters
public float getPushRadius() {
return pushRadius;
}
public void setPushRadius(float pushRadius) {
this.pushRadius = pushRadius;
}
public float getPushStrength() {
return pushStrength;
}
public void setPushStrength(float pushStrength) {
this.pushStrength = pushStrength;
}
public float getDamage() {
return damage;
}
public void setDamage(float damage) {
this.damage = damage;
}
@Override
public float getDisplayDamage() { return damage; }
public boolean isAoe() {
return aoe;
}
public void setAoe(boolean aoe) {
this.aoe = aoe;
}
public int getActiveDisplayTicks() {
return activeDisplayTicks;
}
public void setActiveDisplayTicks(int activeDisplayTicks) {
this.activeDisplayTicks = activeDisplayTicks;
}
@ClientOnly
@Override
public void getAbilityDefinitions(List<FieldDef> defs) {
defs.addAll(Arrays.asList(
FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage),
FieldDef.section("ability.section.push"),
FieldDef.row(
FieldDef.floatField("gui.radius", this::getPushRadius, this::setPushRadius),
FieldDef.floatField("gui.strength", this::getPushStrength, this::setPushStrength)
),
FieldDef.section("ability.section.aoe"),
FieldDef.boolField("gui.enabled", this::isAoe, this::setAoe)
.hover("ability.hover.aoe"),
FieldDef.intField("ability.activeDisplayTicks", this::getActiveDisplayTicks, this::setActiveDisplayTicks).range(1, 200),
AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects)
));
}
}
@@ -0,0 +1,841 @@
package kamkeel.npcs.controllers.data.ability.type;
import noppes.npcs.constants.ClientOnly;
import kamkeel.npcs.util.IVector3;
import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler;
import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback;
import kamkeel.npcs.controllers.data.ability.gui.FieldDef;
import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider;
import noppes.npcs.entity.EntityNPCInterface;
import kamkeel.npcs.entity.EntityEnergyDome;
import kamkeel.npcs.entity.EntityEnergyBarrier;
import kamkeel.npcs.entity.EntityEnergyPanel;
import kamkeel.npcs.entity.EntityAbilityOrb;
import kamkeel.npcs.entity.EntityAbilityLaser;
import kamkeel.npcs.entity.EntityAbilityDisc;
import kamkeel.npcs.entity.EntityAbilityBeam;
import kamkeel.npcs.entity.EntityEnergyProjectile;
import kamkeel.npcs.util.ByteBufUtils;
import noppes.npcs.api.entity.IEntityLiving;
import noppes.npcs.api.IDamageSource;
import noppes.npcs.api.IWorld;
import noppes.npcs.api.entity.IEntityLivingBase;
import noppes.npcs.api.item.IItemStack;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.api.INbtList;
import noppes.npcs.api.INbt;
import kamkeel.npcs.controllers.data.ability.Ability;
import kamkeel.npcs.controllers.data.ability.enums.LockMode;
import kamkeel.npcs.controllers.data.ability.util.AbilityTargetHelper;
import kamkeel.npcs.controllers.data.ability.enums.TargetFilter;
import kamkeel.npcs.controllers.data.ability.enums.TargetingMode;
import kamkeel.npcs.controllers.data.telegraph.Telegraph;
import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance;
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
import noppes.npcs.api.ability.type.IAbilitySlam;
import java.util.Arrays;
import java.util.List;
/**
* Slam ability: NPC leaps toward target in an arc and slams down, dealing AOE damage on landing.
* <p>
* Phases:
* - WINDUP: NPC prepares to leap, telegraph shows landing zone
* - ACTIVE: NPC is in the air, traveling in arc toward destination
* - Landing: When NPC hits ground, deal AOE damage
* - RECOVERY: Landing recovery animation
*/
public class AbilitySlam extends Ability implements IAbilitySlam {
// Type-specific config
private float damage = 10.0f;
private float radius = 5.0f;
private float knockbackStrength = 1.5f;
private float leapSpeed = 1.0f;
private float leapHeight = 4.0f;
/**
* Grace period (in active ticks) before landing detection activates.
* Players need more time because:
* - Server sets motionY and sends S12 velocity packet to client
* - Client receives S12 and starts moving upward
* - Client sends C03 position packet back to server with onGround=false
* Until the round trip completes, the server's player.onGround (from stale C03)
* remains true, which would falsely trigger landing detection.
*/
private static final int PLAYER_LANDING_GRACE_TICKS = 10;
private static final int NPC_LANDING_GRACE_TICKS = 3;
// Runtime state
private transient double targetX, targetY, targetZ;
private transient double startY; // Y position at launch for rise detection
private transient boolean hasLaunched = false;
private transient boolean hasLanded = false;
private transient boolean hasRisen = false; // Whether IEntity has risen above launch position
private transient int airTicks = 0;
private transient int maxAirTicks = 60; // Timeout to prevent stuck in air
private transient boolean airSlam = false; // True when slam initiated while already in the air
private transient double airSlamStartY = 0; // Start height for air-slam damage scaling
private transient boolean wasFlying = false; // Restore player flight after air slam
private transient double peakY = 0; // Maximum Y position during arc (for height scaling)
public AbilitySlam() {
this.typeId = "ability.cnpc.slam";
this.name = "Slam";
this.targetingMode = TargetingMode.AOE_SELF; // Can also be AOE_TARGET to leap to target
this.windUpTicks = 30;
this.cooldownTicks = 0;
this.lockMovement = LockMode.WINDUP;
this.minRange = 2.0f;
this.maxRange = 15.0f;
this.telegraphType = TelegraphType.CIRCLE;
this.windUpSound = "mob.irongolem.throw";
this.activeSound = "";
// Default built-in animations
this.windUpAnimationName = "Ability_Slam_Windup";
this.activeAnimationName = "Ability_Slam_Active";
this.defaultIconLayers = new DefaultIconLayer[]{
new DefaultIconLayer("customnpcs:textures/gui/ability/slam.png")
};
}
/**
* Slam can be AOE_SELF (jump in place) or AOE_TARGET (leap to target).
*/
@Override
public boolean isTargetingModeLocked() {
return false;
}
@Override
public TargetingMode[] getAllowedTargetingModes() {
return new TargetingMode[]{TargetingMode.AOE_SELF, TargetingMode.AOE_TARGET};
}
@Override
public boolean hasAbilityMovement() {
return true; // This ability moves the NPC
}
@Override
public void onWindUpTick(IEntityLivingBase caster, IEntityLivingBase target, int tick) {
if (isPlayerCaster(caster)) {
// Player: update target along look direction during windup
if (targetingMode == TargetingMode.AOE_SELF) {
targetX = caster.posX;
targetY = caster.posY;
targetZ = caster.posZ;
} else {
float yawRad = (float) Math.toRadians(caster.rotationYaw);
double dirX = -Math.sin(yawRad);
double dirZ = Math.cos(yawRad);
double launchDist = Math.max(4.0, maxRange * 0.5);
targetX = caster.posX + dirX * launchDist;
targetY = caster.posY;
targetZ = caster.posZ + dirZ * launchDist;
}
} else {
// NPC: update target position during windup for telegraph tracking
if (targetingMode == TargetingMode.AOE_SELF) {
targetX = caster.posX;
targetY = caster.posY;
targetZ = caster.posZ;
} else if (targetingMode == TargetingMode.AOE_TARGET && target != null && !target.isDead) {
targetX = target.posX;
targetY = findGroundLevel(caster.worldObj, target.posX, target.posY, target.posZ, 64);
targetZ = target.posZ;
}
}
}
@Override
public void onExecute(IEntityLivingBase caster, IEntityLivingBase target) {
hasLaunched = false;
hasLanded = false;
hasRisen = false;
airTicks = 0;
startY = caster.posY;
peakY = caster.posY;
caster.fallDistance = 0;
if (isPlayerCaster(caster)) {
executePlayerSlam(caster);
} else {
executeNpcSlam(caster, target);
}
}
/**
* NPC slam: Lock destination based on targeting mode, then launch in a ballistic arc toward it.
*/
private void executeNpcSlam(IEntityLivingBase caster, IEntityLivingBase target) {
if (targetingMode == TargetingMode.AOE_SELF) {
targetX = caster.posX;
targetY = caster.posY;
targetZ = caster.posZ;
} else if (targetingMode == TargetingMode.AOE_TARGET && target != null && !target.isDead) {
targetX = target.posX;
targetY = findGroundLevel(caster.worldObj, target.posX, target.posY, target.posZ, 64);
targetZ = target.posZ;
} else {
targetX = caster.posX;
targetY = caster.posY;
targetZ = caster.posZ;
}
launchTowardTarget(caster);
}
/**
* Player slam: behavior depends on whether the player is on the ground or in the air.
* <p>
* On ground: Launch in look direction using ballistic arc, full damage on landing.
* In air: Slam straight down from current height, damage scales with fall distance.
* Works regardless of flying state — position is forced downward each tick.
*/
private void executePlayerSlam(IEntityLivingBase caster) {
// Determine if this qualifies as an air slam (at least 3 blocks above ground)
boolean isAirborne = !caster.onGround;
boolean highEnoughForAirSlam = false;
if (isAirborne) {
double groundY = findGroundLevel(caster.worldObj, caster.posX, caster.posY, caster.posZ, 64);
highEnoughForAirSlam = (caster.posY - groundY) >= 3.0;
}
if (isAirborne && highEnoughForAirSlam) {
// Air slam: player is already airborne (flying, falling, jumping) and high enough
airSlam = true;
airSlamStartY = caster.posY;
targetX = caster.posX;
targetY = caster.posY;
targetZ = caster.posZ;
// If the player is flying, temporarily disable it so gravity/velocity apply normally.
if (caster instanceof IPlayer) {
IPlayer mp = (IPlayer) caster;
wasFlying = mp.capabilities.isFlying;
if (wasFlying) {
mp.capabilities.isFlying = false;
mp.sendPlayerAbilities();
}
}
hasLaunched = true;
hasRisen = true; // Skip rise detection — we're already above ground
// Start descent smoothly; physics/collision handles exact landing surface.
// Only damp horizontal drift when movement is locked during ACTIVE.
if (isMovementLockedDuringActive()) {
caster.motionX *= 0.25;
caster.motionZ *= 0.25;
}
caster.motionY = Math.min(caster.motionY, -0.2);
if (!isPreview()) {
if (caster instanceof IPlayer) {
((IPlayer) caster).playerNetServerHandler.sendPacket(
new S12PacketEntityVelocity(caster));
} else {
caster.velocityChanged = true;
}
caster.worldObj.playSoundAtEntity(caster, "mob.irongolem.throw", 0.8f, 0.8f);
}
} else {
// Ground slam: normal ballistic arc (also used when airborne but not high enough)
airSlam = false;
airSlamStartY = 0;
wasFlying = false;
// Calculate target position along player's look direction (horizontal only)
float yawRad = (float) Math.toRadians(caster.rotationYaw);
double dirX = -Math.sin(yawRad);
double dirZ = Math.cos(yawRad);
double launchDist = Math.max(4.0, maxRange * 0.5);
targetX = caster.posX + dirX * launchDist;
targetY = caster.posY;
targetZ = caster.posZ + dirZ * launchDist;
launchTowardTarget(caster);
}
}
/**
* Calculate the initial vertical velocity needed to reach a target height
* in Minecraft's physics (gravity=0.08, vertical drag=0.98).
* Uses iterative simulation since the drag makes a closed-form solution impractical.
*/
private static double calculateLaunchVelocity(double targetHeight) {
// Binary search for the initial velocity that reaches targetHeight
double lo = 0.0, hi = 10.0;
for (int iter = 0; iter < 30; iter++) {
double mid = (lo + hi) / 2.0;
double maxY = simulateMaxHeight(mid);
if (maxY < targetHeight) {
lo = mid;
} else {
hi = mid;
}
}
return (lo + hi) / 2.0;
}
/**
* Simulate Minecraft vertical physics to find peak height reached
* with a given initial upward velocity.
*/
private static double simulateMaxHeight(double vy) {
double y = 0;
double maxY = 0;
for (int t = 0; t < 200 && vy > 0; t++) {
y += vy;
vy -= 0.08; // gravity
vy *= 0.98; // vertical drag
if (y > maxY) maxY = y;
}
return maxY;
}
/**
* Calculate ballistic arc and launch caster toward target.
* leapHeight = exact peak height in blocks above launch point.
* leapSpeed = velocity multiplier (faster motion, same height).
*/
private void launchTowardTarget(IEntityLivingBase caster) {
double dx = targetX - caster.posX;
double dy = targetY - caster.posY;
double dz = targetZ - caster.posZ;
double horizontalDist = Math.sqrt(dx * dx + dz * dz);
// Calculate vertical velocity to reach exact leapHeight
double arcHeight = Math.max(1.0, leapHeight);
double vy = calculateLaunchVelocity(arcHeight);
// For AOE_SELF or very close targets, just hop in place
if (horizontalDist < 0.5) {
caster.motionX = 0;
caster.motionZ = 0;
caster.motionY = vy;
hasLaunched = true;
if (!isPreview() && caster instanceof EntityNPCInterface) {
((EntityNPCInterface) caster).setNpcJumpingState(true);
}
if (!isPreview()) {
if (caster instanceof IPlayer) {
((IPlayer) caster).playerNetServerHandler.sendPacket(
new S12PacketEntityVelocity(caster));
} else {
caster.velocityChanged = true;
}
caster.worldObj.playSoundAtEntity(caster, "mob.irongolem.throw", 0.8f, 0.8f);
}
return;
}
// Cap the distance if too far
if (horizontalDist > maxRange) {
double scale = maxRange / horizontalDist;
dx *= scale;
dz *= scale;
horizontalDist = maxRange;
targetX = caster.posX + dx;
targetZ = caster.posZ + dz;
}
// Choose flight time based on distance, scaled by speed (faster = fewer ticks)
double speedFactor = Math.max(0.1, leapSpeed);
int flightTicks = (int) Math.max(10, Math.min(horizontalDist * 1.5 / speedFactor, 40));
// Minecraft applies drag of 0.91 per tick to horizontal motion in air
// Total distance traveled with initial velocity v0 over n ticks:
// distance = v0 * (1 - drag^n) / (1 - drag)
// Solving for v0: v0 = distance * (1 - drag) / (1 - drag^n)
double drag = 0.91;
double dragPowN = Math.pow(drag, flightTicks);
double vHorizontal = horizontalDist * (1.0 - drag) / (1.0 - dragPowN);
// Adjust vertical velocity for height difference between start and target
if (dy > 0) {
vy += dy / flightTicks * 1.5; // Going up - need more velocity
} else if (dy < 0) {
vy += dy / flightTicks * 0.3; // Going down - gravity helps
}
// Normalize horizontal direction and apply velocity
double dirX = dx / horizontalDist;
double dirZ = dz / horizontalDist;
caster.motionX = dirX * vHorizontal;
caster.motionZ = dirZ * vHorizontal;
caster.motionY = vy;
hasLaunched = true;
if (!isPreview() && caster instanceof EntityNPCInterface) {
((EntityNPCInterface) caster).setNpcJumpingState(true);
}
if (!isPreview()) {
// Use direct S12 packet for players to ensure velocity reaches client
// before flight code can override it
if (caster instanceof IPlayer) {
((IPlayer) caster).playerNetServerHandler.sendPacket(
new S12PacketEntityVelocity(caster));
} else {
caster.velocityChanged = true;
}
}
// Face the target
float targetYaw = (float) (Math.atan2(-dx, dz) * 180.0D / Math.PI);
caster.rotationYaw = targetYaw;
caster.rotationYawHead = targetYaw;
}
@Override
public void onActiveTick(IEntityLivingBase caster, IEntityLivingBase target, int tick) {
if (!hasLaunched) return;
if (hasLanded) return;
boolean playerCaster = isPlayerCaster(caster);
boolean activeMovementLocked = isMovementLockedDuringActive();
boolean forceHorizontalControl = !playerCaster || activeMovementLocked;
airTicks++;
// Continuously reset fall distance during slam to prevent fall damage
if (!isPreview()) {
caster.fallDistance = 0;
}
// Track peak height for damage scaling
if (caster.posY > peakY) {
peakY = caster.posY;
}
// Air slam: accelerate descent smoothly via velocity; landing uses normal collision.
if (airSlam && !isPreview() && !hasLanded) {
if (forceHorizontalControl) {
caster.motionX *= 0.2;
caster.motionZ *= 0.2;
}
double nextMotionY = caster.motionY - 0.18;
caster.motionY = Math.max(-1.6, nextMotionY);
if (caster instanceof IPlayer) {
// Only push authoritative velocity every tick when ACTIVE movement is locked.
if (forceHorizontalControl) {
((IPlayer) caster).playerNetServerHandler.sendPacket(
new S12PacketEntityVelocity(caster));
}
} else {
caster.velocityChanged = true;
}
}
// Prevent NPC navigator from interfering with ballistic arc.
// lockMovement=WINDUP means applyMovementControl() doesn't clear
// the navigator during ACTIVE, so AI pathfinding could add motion.
if (!isPreview() && caster instanceof EntityNPCInterface) {
((EntityNPCInterface) caster).getNavigator().clearPathEntity();
}
// Check if slam trajectory is blocked by a solid barrier
if (!isPreview() && !caster.worldObj.isRemote) {
double moveX = caster.motionX;
double moveZ = caster.motionZ;
double hSpeed = Math.sqrt(moveX * moveX + moveZ * moveZ);
if (hSpeed > 0.01) {
double normX = moveX / hSpeed;
double normZ = moveZ / hSpeed;
if (isMovementBlockedByBarrier(caster, normX, normZ, hSpeed)) {
caster.motionX = 0;
caster.motionZ = 0;
if (caster instanceof IPlayer) {
((IPlayer) caster).playerNetServerHandler.sendPacket(
new S12PacketEntityVelocity(caster));
} else {
caster.velocityChanged = true;
}
}
}
}
// Track if IEntity has risen above launch position.
// This prevents premature landing detection for players, where
// server-side caster.onGround remains true from stale C03 packets
// until the velocity S12 round-trip completes (can take 4-8+ ticks).
if (!hasRisen && caster.posY > startY + 0.5) {
hasRisen = true;
}
// Landing detection uses different grace periods for NPC vs Player.
// NPCs: server physics is authoritative, onGround is accurate after 3 ticks.
// Players: onGround depends on C03 packets, needs longer grace + rise confirmation.
int graceTicks = isPlayerCaster(caster) ? PLAYER_LANDING_GRACE_TICKS : NPC_LANDING_GRACE_TICKS;
if (airTicks > graceTicks) {
if (hasRisen && (caster.onGround || caster.isCollidedHorizontally)) {
onLanding(caster, caster.worldObj);
return;
}
// Safety: if IEntity hasn't risen after triple the grace period,
// the launch may have failed (blocked by ceiling, etc.) - force landing
if (!hasRisen && airTicks > graceTicks * 3) {
onLanding(caster, caster.worldObj);
return;
}
}
// Timeout protection - force landing after max air time
if (airTicks >= maxAirTicks) {
onLanding(caster, caster.worldObj);
return;
}
// Face toward destination while in air unless player ACTIVE movement is unlocked.
// Unlocked players keep free horizontal drift/look control.
if (!isPreview()) {
if (forceHorizontalControl) {
double dx = targetX - caster.posX;
double dz = targetZ - caster.posZ;
if (dx * dx + dz * dz > 0.25) {
float targetYaw = (float) (Math.atan2(-dx, dz) * 180.0D / Math.PI);
caster.rotationYaw = targetYaw;
caster.rotationYawHead = targetYaw;
if (playerCaster) {
caster.velocityChanged = true;
}
}
}
}
}
/**
* Called when caster lands - deal AOE damage and signal completion.
*/
private void onLanding(IEntityLivingBase caster, IWorld IWorld) {
hasLanded = true;
if (!isPreview() && caster instanceof EntityNPCInterface) {
((EntityNPCInterface) caster).setNpcJumpingState(false);
}
restoreFlightIfNeeded(caster);
// Stop horizontal momentum only when movement is controlled by the ability.
if (!isPlayerCaster(caster) || isMovementLockedDuringActive()) {
caster.motionX = 0;
caster.motionZ = 0;
}
if (!isPreview()) {
// Reset fall distance to prevent fall damage on landing
caster.fallDistance = 0;
caster.velocityChanged = true;
}
if (IWorld.isRemote && !isPreview()) return;
if (isPreview()) return;
// Play slam impact sound on landing
IWorld.playSoundAtEntity(caster, "random.explode", 1.0f, 1.0f);
// Scale damage based on height for both air and ground slams
float effectiveDamage = damage;
float heightMultiplier = 1.0f;
if (airSlam) {
// Air slam: scale based on total fall distance
double fallDistance = airSlamStartY - caster.posY;
if (fallDistance < 0) fallDistance = 0;
float heightFactor = (float) Math.min(1.0, fallDistance / Math.max(1.0, leapHeight));
heightMultiplier = Math.max(0.25f, heightFactor);
effectiveDamage = damage * heightMultiplier;
} else {
// Ground slam: scale based on arc height reached
double arcHeight = peakY - startY;
if (arcHeight > 0) {
float heightFactor = (float) Math.min(1.0, arcHeight / Math.max(1.0, leapHeight));
heightMultiplier = Math.max(0.25f, heightFactor);
effectiveDamage = damage * heightMultiplier;
}
}
// Set multiplier so extenders (e.g. DBC Addon) can apply height scaling to their own damage calc
setDamageMultiplier(heightMultiplier);
// Find all entities in radius
@SuppressWarnings("unchecked")
List<IEntity> entities = IWorld.getEntitiesWithinAABBExcludingEntity(
caster, caster.boundingBox.expand(radius, radius / 2, radius));
for (IEntity IEntity : entities) {
if (IEntity instanceof IEntityLivingBase && IEntity != caster) {
IEntityLivingBase livingTarget = (IEntityLivingBase) IEntity;
if (!AbilityTargetHelper.shouldAffect(caster, livingTarget, TargetFilter.ENEMIES, false)) continue;
// Check if actually within radius (bounding box is a cube, we want a circle)
double dx = livingTarget.posX - caster.posX;
double dz = livingTarget.posZ - caster.posZ;
if (dx * dx + dz * dz <= radius * radius) {
// Apply damage with scripted event support
boolean wasHit = applyAbilityDamage(caster, livingTarget, effectiveDamage, knockbackStrength);
if (wasHit) {
applyEffects(livingTarget);
}
}
}
}
setDamageMultiplier(1.0f); // Reset after damage loop
// Spawn particles
spawnSlamParticles(IWorld, caster.posX, caster.posY, caster.posZ);
// Signal completion AFTER damage — signalCompletion triggers onComplete which resets state
signalCompletion();
}
/**
* Spawn visual particles for the slam effect.
*/
private void spawnSlamParticles(IWorld IWorld, double x, double y, double z) {
// Spawn IExplosion particles in a ring
for (int i = 0; i < 20; i++) {
double angle = (i / 20.0) * Math.PI * 2;
double px = x + Math.cos(angle) * radius * 0.8;
double pz = z + Math.sin(angle) * radius * 0.8;
IWorld.spawnParticle("explode", px, y + 0.5, pz, 0, 0.1, 0);
}
// Spawn smoke at center
for (int i = 0; i < 10; i++) {
IWorld.spawnParticle("smoke", x, y + 0.2, z,
(IWorld.rand.nextDouble() - 0.5) * 0.3,
IWorld.rand.nextDouble() * 0.2,
(IWorld.rand.nextDouble() - 0.5) * 0.3);
}
// Ground crack particles - check if block is valid
net.minecraft.block.Block groundBlock = IWorld.getBlock((int) x, (int) y - 1, (int) z);
if (groundBlock != null && groundBlock.getMaterial().isSolid()) {
int blockId = net.minecraft.block.Block.getIdFromBlock(groundBlock);
if (blockId > 0) {
for (int i = 0; i < 15; i++) {
double angle = IWorld.rand.nextDouble() * Math.PI * 2;
double dist = IWorld.rand.nextDouble() * radius * 0.6;
double px = x + Math.cos(angle) * dist;
double pz = z + Math.sin(angle) * dist;
IWorld.spawnParticle("blockcrack_" + blockId + "_0", px, y + 0.1, pz, 0, 0.2, 0);
}
}
}
}
@Override
public void onComplete(IEntityLivingBase caster, IEntityLivingBase target) {
if (!isPreview() && caster instanceof EntityNPCInterface) {
((EntityNPCInterface) caster).setNpcJumpingState(false);
}
restoreFlightIfNeeded(caster);
hasLaunched = false;
hasLanded = false;
hasRisen = false;
airTicks = 0;
airSlam = false;
airSlamStartY = 0;
peakY = 0;
wasFlying = false;
}
@Override
public void onInterrupt(IEntityLivingBase caster, IDamageSource source, float damage) {
if (!isPreview() && caster instanceof EntityNPCInterface) {
((EntityNPCInterface) caster).setNpcJumpingState(false);
}
restoreFlightIfNeeded(caster);
hasLaunched = false;
hasLanded = false;
hasRisen = false;
airTicks = 0;
airSlam = false;
airSlamStartY = 0;
peakY = 0;
wasFlying = false;
}
@Override
public void cleanup() {
// Caster may be unavailable here; flight restoration is handled in onComplete/onInterrupt.
hasLaunched = false;
hasLanded = false;
hasRisen = false;
airTicks = 0;
airSlam = false;
airSlamStartY = 0;
peakY = 0;
wasFlying = false;
}
@Override
public void reset() {
super.reset();
hasLaunched = false;
hasLanded = false;
hasRisen = false;
airTicks = 0;
airSlam = false;
airSlamStartY = 0;
peakY = 0;
wasFlying = false;
}
/**
* Restore player flight if it was disabled for an air slam.
*/
private void restoreFlightIfNeeded(IEntityLivingBase caster) {
if (airSlam && wasFlying && caster instanceof IPlayer) {
IPlayer mp = (IPlayer) caster;
if (!mp.capabilities.isFlying) {
mp.capabilities.isFlying = true;
mp.sendPlayerAbilities();
}
}
}
@Override
public int getMaxPreviewDuration() {
return 60;
}
@Override
public TelegraphInstance createTelegraph(IEntityLivingBase caster, IEntityLivingBase target) {
// Check if telegraph should be shown
if (!isShowTelegraph() || getTelegraphType() == TelegraphType.NONE) {
return null;
}
// Telegraph shows at landing zone
if (targetingMode == TargetingMode.AOE_SELF) {
// AOE_SELF: telegraph at caster position, does NOT follow
targetX = caster.posX;
targetY = caster.posY;
targetZ = caster.posZ;
} else if (targetingMode == TargetingMode.AOE_TARGET && target != null) {
// AOE_TARGET: telegraph at target position, follows target during windup
targetX = target.posX;
targetY = target.posY;
targetZ = target.posZ;
} else {
// Fallback to caster position
targetX = caster.posX;
targetY = caster.posY;
targetZ = caster.posZ;
}
// Create telegraph at the appropriate position — always project to actual ground for landing zone
double groundY = findGroundLevel(caster.worldObj, targetX, targetY, targetZ, 64);
Telegraph telegraph = Telegraph.circle(radius);
telegraph.setDurationTicks(windUpTicks);
telegraph.setColor(windUpColor);
telegraph.setWarningColor(activeColor);
telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4));
telegraph.setHeightOffset(telegraphHeightOffset);
telegraph.setGroundSearchRange(64); // Slam always projects to actual ground (landing zone)
TelegraphInstance instance = new TelegraphInstance(telegraph, targetX, groundY, targetZ, caster.rotationYaw);
instance.setCasterEntityId(caster.getEntityId());
// NPC AOE_TARGET: telegraph follows target during windup
// Player: telegraph stays at caster position (no target to follow)
if (!isPlayerCaster(caster) && targetingMode == TargetingMode.AOE_TARGET && target != null) {
instance.setEntityIdToFollow(target.getEntityId());
}
return instance;
}
@Override
public float getTelegraphRadius() {
return radius;
}
@Override
public void writeTypeNBT(INbt nbt) {
nbt.setFloat("damage", damage);
nbt.setFloat("radius", radius);
nbt.setFloat("knockback", knockbackStrength);
nbt.setFloat("leapSpeed", leapSpeed);
nbt.setFloat("leapHeight", leapHeight);
}
@Override
public void readTypeNBT(INbt nbt) {
damage = nbt.getFloat("damage");
radius = nbt.getFloat("radius");
knockbackStrength = nbt.getFloat("knockback");
leapSpeed = nbt.getFloat("leapSpeed");
leapHeight = nbt.getFloat("leapHeight");
}
// Getters & Setters
public float getDamage() {
return damage;
}
public void setDamage(float damage) {
this.damage = damage;
}
@Override
public float getDisplayDamage() { return damage; }
public float getRadius() {
return radius;
}
public void setRadius(float radius) {
this.radius = radius;
}
public float getKnockbackStrength() {
return knockbackStrength;
}
public void setKnockbackStrength(float knockbackStrength) {
this.knockbackStrength = knockbackStrength;
}
public float getLeapSpeed() {
return leapSpeed;
}
public void setLeapSpeed(float leapSpeed) {
this.leapSpeed = leapSpeed;
}
public float getLeapHeight() {
return leapHeight;
}
public void setLeapHeight(float leapHeight) {
this.leapHeight = leapHeight;
}
@ClientOnly
@Override
public void getAbilityDefinitions(List<FieldDef> defs) {
defs.addAll(Arrays.asList(
FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage),
FieldDef.row(
FieldDef.floatField("gui.radius", this::getRadius, this::setRadius),
FieldDef.floatField("ability.knockback", this::getKnockbackStrength, this::setKnockbackStrength)
),
FieldDef.section("ability.section.leap"),
FieldDef.row(
FieldDef.floatField("gui.speed", this::getLeapSpeed, this::setLeapSpeed),
FieldDef.floatField("gui.height", this::getLeapHeight, this::setLeapHeight)
),
AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects)
));
}
}
@@ -0,0 +1,602 @@
package kamkeel.npcs.controllers.data.ability.type;
import noppes.npcs.constants.ClientOnly;
import kamkeel.npcs.util.IVector3;
import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler;
import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback;
import kamkeel.npcs.controllers.data.ability.gui.FieldDef;
import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider;
import noppes.npcs.entity.EntityNPCInterface;
import kamkeel.npcs.entity.EntityEnergyDome;
import kamkeel.npcs.entity.EntityEnergyBarrier;
import kamkeel.npcs.entity.EntityEnergyPanel;
import kamkeel.npcs.entity.EntityAbilityOrb;
import kamkeel.npcs.entity.EntityAbilityLaser;
import kamkeel.npcs.entity.EntityAbilityDisc;
import kamkeel.npcs.entity.EntityAbilityBeam;
import kamkeel.npcs.entity.EntityEnergyProjectile;
import kamkeel.npcs.util.ByteBufUtils;
import noppes.npcs.api.entity.IEntityLiving;
import noppes.npcs.api.IDamageSource;
import noppes.npcs.api.IWorld;
import noppes.npcs.api.entity.IEntityLivingBase;
import noppes.npcs.api.item.IItemStack;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.api.INbtList;
import noppes.npcs.api.INbt;
import kamkeel.npcs.controllers.data.ability.Ability;
import kamkeel.npcs.controllers.data.ability.enums.LockMode;
import kamkeel.npcs.controllers.data.ability.enums.TargetFilter;
import kamkeel.npcs.controllers.data.ability.enums.TargetingMode;
import kamkeel.npcs.controllers.data.ability.enums.UserType;
import kamkeel.npcs.controllers.data.ability.util.AbilityTargetHelper;
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
import noppes.npcs.api.ability.type.IAbilityTeleport;
import java.util.Arrays;
import java.util.List;
import java.util.Random;
/**
* Teleport ability: Instant reposition with wall/line-of-sight checks.
*/
public class AbilityTeleport extends Ability implements IAbilityTeleport {
/**
* Teleport behavior mode.
*/
public enum TeleportMode {
BLINK,
BEHIND,
SINGLE;
@Override
public String toString() {
switch (this) {
case BLINK:
return "ability.teleport.blink";
case BEHIND:
return "ability.teleport.behind";
case SINGLE:
return "ability.teleport.single";
default:
return name();
}
}
}
private static final Random RANDOM = new Random();
// Type-specific parameters
private TeleportMode mode = TeleportMode.BLINK;
private int blinkCount = 3;
private int blinkDelayTicks = 10;
private float blinkRadius = 8.0f;
private float behindDistance = 2.0f;
private boolean requireLineOfSight = true;
private boolean damageAtStart = false;
private boolean damageAtEnd = false;
private float damage = 5.0f;
private float damageRadius = 2.0f;
// Runtime state
private transient int currentBlink = 0;
private transient int ticksSinceLastBlink = 0;
public AbilityTeleport() {
this.typeId = "ability.cnpc.teleport";
this.name = "Teleport";
this.targetingMode = TargetingMode.AGGRO_TARGET;
this.allowedBy = UserType.NPC_ONLY;
this.maxRange = 30.0f;
this.minRange = 5.0f;
this.cooldownTicks = 0;
this.windUpTicks = 10;
this.lockMovement = LockMode.NO;
// No telegraph for teleport - it's instant repositioning
this.telegraphType = TelegraphType.NONE;
this.showTelegraph = false;
this.windUpSound = "mob.endermen.portal";
this.activeSound = "mob.endermen.portal";
this.defaultIconLayers = new DefaultIconLayer[]{
new DefaultIconLayer("customnpcs:textures/gui/ability/teleport.png")
};
}
@Override
public boolean isTargetingModeLocked() {
return true;
}
@Override
public TargetingMode[] getAllowedTargetingModes() {
return new TargetingMode[]{TargetingMode.AGGRO_TARGET};
}
@Override
public void onExecute(IEntityLivingBase caster, IEntityLivingBase target) {
currentBlink = 0;
ticksSinceLastBlink = blinkDelayTicks; // Trigger first blink immediately
}
@Override
public void onActiveTick(IEntityLivingBase caster, IEntityLivingBase target, int tick) {
int blinkLimit = mode == TeleportMode.BLINK ? blinkCount : 1;
if (currentBlink >= blinkLimit) {
signalCompletion();
return;
}
ticksSinceLastBlink++;
if (ticksSinceLastBlink >= blinkDelayTicks) {
performBlink(caster, target, caster.worldObj);
currentBlink++;
ticksSinceLastBlink = 0;
// Check if this was the last blink
if (currentBlink >= blinkLimit) {
signalCompletion();
}
}
}
private void performBlink(IEntityLivingBase caster, IEntityLivingBase target, IWorld IWorld) {
if (IWorld.isRemote && !isPreview()) return;
double oldX = caster.posX;
double oldY = caster.posY;
double oldZ = caster.posZ;
IVector3 destination = calculateDestination(caster, target, IWorld);
if (destination == null) return;
if (!isPreview()) {
boolean mustHaveLOS = mode == TeleportMode.BEHIND || requireLineOfSight;
if (mustHaveLOS && !hasLineOfSight(IWorld, oldX, oldY + caster.getEyeHeight(), oldZ,
destination.xCoord, destination.yCoord + caster.getEyeHeight(), destination.zCoord)) {
destination = findValidPositionAlongLine(IWorld, caster, oldX, oldY, oldZ,
destination.xCoord, destination.yCoord, destination.zCoord);
if (destination == null) return;
}
destination = findSafeDestination(IWorld, oldX, oldY, oldZ,
destination.xCoord, destination.yCoord, destination.zCoord);
if (destination == null) return;
// Damage at origin
if (damageAtStart) {
dealDamageAt(caster, IWorld, oldX, oldY, oldZ);
}
// Spawn particles at origin
spawnTeleportParticles(IWorld, oldX, oldY, oldZ);
}
// Teleport
if (isPreview()) {
caster.setPosition(destination.xCoord, destination.yCoord, destination.zCoord);
caster.prevPosX = destination.xCoord;
caster.prevPosY = destination.yCoord;
caster.prevPosZ = destination.zCoord;
} else {
caster.setPositionAndUpdate(destination.xCoord, destination.yCoord, destination.zCoord);
}
caster.fallDistance = 0;
if (!isPreview()) {
// Spawn particles at destination
spawnTeleportParticles(IWorld, destination.xCoord, destination.yCoord, destination.zCoord);
// Damage at destination
if (damageAtEnd) {
dealDamageAt(caster, IWorld, destination.xCoord, destination.yCoord, destination.zCoord);
}
}
// Face target after teleport
if (target != null) {
double dx = target.posX - destination.xCoord;
double dz = target.posZ - destination.zCoord;
float newYaw = (float) Math.toDegrees(Math.atan2(-dx, dz));
caster.rotationYaw = newYaw;
caster.rotationYawHead = newYaw;
}
}
private void spawnTeleportParticles(IWorld IWorld, double x, double y, double z) {
for (int i = 0; i < 20; i++) {
IWorld.spawnParticle("portal",
x + (RANDOM.nextDouble() - 0.5) * 2,
y + RANDOM.nextDouble() * 2,
z + (RANDOM.nextDouble() - 0.5) * 2,
(RANDOM.nextDouble() - 0.5) * 0.5,
RANDOM.nextDouble() * 0.5,
(RANDOM.nextDouble() - 0.5) * 0.5);
}
}
private IVector3 calculateDestination(IEntityLivingBase caster, IEntityLivingBase target, IWorld IWorld) {
double newX;
double newY;
double newZ;
if (target != null) {
switch (mode) {
case BEHIND:
double yaw = Math.toRadians(target.rotationYaw);
newX = target.posX + Math.sin(yaw) * behindDistance;
newZ = target.posZ - Math.cos(yaw) * behindDistance;
newY = target.posY;
return IVector3.createVectorHelper(newX, newY, newZ);
case SINGLE:
case BLINK:
default:
double angle = RANDOM.nextDouble() * Math.PI * 2;
double dist = Math.min(blinkRadius, maxRange);
double offset = RANDOM.nextDouble() * dist;
newX = target.posX + Math.cos(angle) * offset;
newZ = target.posZ + Math.sin(angle) * offset;
newY = target.posY;
return IVector3.createVectorHelper(newX, newY, newZ);
}
}
// NPC-only ability — should not reach here
return null;
}
private IVector3 findSafeDestination(IWorld IWorld, double oldX, double oldY, double oldZ,
double destX, double destY, double destZ) {
int oldBlockX = (int) Math.floor(oldX);
int oldBlockY = (int) Math.floor(oldY);
int oldBlockZ = (int) Math.floor(oldZ);
int destBlockX = (int) Math.floor(destX);
int destBlockY = (int) Math.floor(destY);
int destBlockZ = (int) Math.floor(destZ);
if (oldBlockX == destBlockX && oldBlockY == destBlockY && oldBlockZ == destBlockZ) {
return null;
}
double safeY = findSafeYNear(IWorld, destX, destY, destZ);
IVector3 safe = IVector3.createVectorHelper(destX, safeY, destZ);
if (!isSafeLocation(IWorld, (int) Math.floor(safe.xCoord),
(int) Math.floor(safe.yCoord), (int) Math.floor(safe.zCoord))) {
return null;
}
return safe;
}
private double findSafeYNear(IWorld IWorld, double x, double baseY, double z) {
int[] offsets = new int[]{0, 1, -1, 2, -2};
for (int offset : offsets) {
double y = baseY + offset;
if (isSafeLocation(IWorld, (int) Math.floor(x), (int) Math.floor(y), (int) Math.floor(z))) {
return y;
}
}
return findSafeY(IWorld, x, baseY, z);
}
private boolean hasLineOfSight(IWorld IWorld, double x1, double y1, double z1,
double x2, double y2, double z2) {
double dx = x2 - x1;
double dy = y2 - y1;
double dz = z2 - z1;
double distance = Math.sqrt(dx * dx + dy * dy + dz * dz);
if (distance < 0.1) return true;
dx /= distance;
dy /= distance;
dz /= distance;
double step = 0.5;
for (double d = step; d < distance; d += step) {
double checkX = x1 + dx * d;
double checkY = y1 + dy * d;
double checkZ = z1 + dz * d;
int blockX = ValueUtil.floorDouble(checkX);
int blockY = ValueUtil.floorDouble(checkY);
int blockZ = ValueUtil.floorDouble(checkZ);
// Skip out-of-IWorld coordinates
if (blockY < 0 || blockY >= 256) {
continue;
}
Block block = IWorld.getBlock(blockX, blockY, blockZ);
if (block != null && block.getMaterial().isSolid() && block.isOpaqueCube()) {
return false;
}
}
return true;
}
private IVector3 findValidPositionAlongLine(IWorld IWorld, IEntityLivingBase caster,
double x1, double y1, double z1,
double x2, double y2, double z2) {
double dx = x2 - x1;
double dy = y2 - y1;
double dz = z2 - z1;
double distance = Math.sqrt(dx * dx + dy * dy + dz * dz);
if (distance < 1.0) return null;
dx /= distance;
dy /= distance;
dz /= distance;
IVector3 lastValid = null;
double step = 0.5;
for (double d = 1.0; d < distance; d += step) {
double checkX = x1 + dx * d;
double checkY = y1 + dy * d;
double checkZ = z1 + dz * d;
if (hasLineOfSight(IWorld, x1, y1 + caster.getEyeHeight(), z1,
checkX, checkY + caster.getEyeHeight(), checkZ)) {
int blockX = ValueUtil.floorDouble(checkX);
int blockY = ValueUtil.floorDouble(checkY);
int blockZ = ValueUtil.floorDouble(checkZ);
if (isSafeLocation(IWorld, blockX, blockY, blockZ)) {
lastValid = IVector3.createVectorHelper(checkX, checkY, checkZ);
}
} else {
break;
}
}
return lastValid;
}
private double findSafeY(IWorld IWorld, double x, double baseY, double z) {
int blockX = ValueUtil.floorDouble(x);
int blockZ = ValueUtil.floorDouble(z);
int blockY = ValueUtil.floorDouble(baseY);
for (int offset = 0; offset <= 5; offset++) {
if (isSafeLocation(IWorld, blockX, blockY + offset, blockZ)) {
return blockY + offset;
}
if (offset > 0 && isSafeLocation(IWorld, blockX, blockY - offset, blockZ)) {
return blockY - offset;
}
}
return baseY;
}
private boolean isSafeLocation(IWorld IWorld, int x, int y, int z) {
// Bounds check - ensure we're within valid IWorld coordinates
if (y < 1 || y >= 255) {
return false;
}
Block groundBlock = IWorld.getBlock(x, y - 1, z);
Block feetBlock = IWorld.getBlock(x, y, z);
Block headBlock = IWorld.getBlock(x, y + 1, z);
// Defensive null checks (shouldn't happen in MC 1.7.10, but safe)
if (groundBlock == null || feetBlock == null || headBlock == null) {
return false;
}
return groundBlock.getMaterial().isSolid() &&
!feetBlock.getMaterial().isSolid() &&
!headBlock.getMaterial().isSolid();
}
private void dealDamageAt(IEntityLivingBase caster, IWorld IWorld, double x, double y, double z) {
IBoundingBox aabb = IBoundingBox.getBoundingBox(
x - damageRadius, y - 1, z - damageRadius,
x + damageRadius, y + 2, z + damageRadius
);
@SuppressWarnings("unchecked")
List<IEntity> entities = IWorld.getEntitiesWithinAABB(IEntityLivingBase.class, aabb);
for (IEntity IEntity : entities) {
if (!(IEntity instanceof IEntityLivingBase)) continue;
if (IEntity == caster) continue;
if (!AbilityTargetHelper.shouldAffect(caster, IEntity, TargetFilter.ENEMIES, false)) continue;
IEntityLivingBase living = (IEntityLivingBase) IEntity;
double dist = Math.sqrt(Math.pow(living.posX - x, 2) + Math.pow(living.posZ - z, 2));
if (dist <= damageRadius) {
// Apply damage with scripted event support (no knockback for teleport damage)
boolean wasHit = applyAbilityDamage(caster, living, damage, 0);
if (wasHit) {
applyEffects(living);
}
}
}
}
@Override
public void reset() {
super.reset();
currentBlink = 0;
ticksSinceLastBlink = 0;
}
@Override
public float getTelegraphRadius() {
return 0; // No telegraph for teleport
}
@Override
public void writeTypeNBT(INbt nbt) {
nbt.setString("mode", mode.name());
nbt.setInteger("blinkCount", blinkCount);
nbt.setInteger("blinkDelayTicks", blinkDelayTicks);
nbt.setFloat("blinkRadius", blinkRadius);
nbt.setFloat("behindDistance", behindDistance);
nbt.setBoolean("requireLineOfSight", requireLineOfSight);
nbt.setBoolean("damageAtStart", damageAtStart);
nbt.setBoolean("damageAtEnd", damageAtEnd);
nbt.setFloat("damage", damage);
nbt.setFloat("damageRadius", damageRadius);
}
@Override
public void readTypeNBT(INbt nbt) {
try {
this.mode = TeleportMode.valueOf(nbt.getString("mode"));
} catch (Exception e) {
this.mode = TeleportMode.BLINK;
}
this.blinkCount = nbt.hasKey("blinkCount") ? nbt.getInteger("blinkCount") : 3;
this.blinkDelayTicks = nbt.hasKey("blinkDelayTicks") ? nbt.getInteger("blinkDelayTicks") : 10;
this.blinkRadius = nbt.getFloat("blinkRadius");
this.behindDistance = nbt.getFloat("behindDistance");
this.requireLineOfSight = nbt.getBoolean("requireLineOfSight");
this.damageAtStart = nbt.getBoolean("damageAtStart");
this.damageAtEnd = nbt.getBoolean("damageAtEnd");
this.damage = nbt.getFloat("damage");
this.damageRadius = nbt.getFloat("damageRadius");
}
// Getters & Setters
public TeleportMode getModeEnum() {
return mode;
}
public void setModeEnum(TeleportMode mode) {
this.mode = mode;
}
@Override
public int getMode() {
return mode.ordinal();
}
@Override
public void setMode(int mode) {
TeleportMode[] values = TeleportMode.values();
this.mode = mode >= 0 && mode < values.length ? values[mode] : TeleportMode.BLINK;
}
public int getBlinkCount() {
return blinkCount;
}
public void setBlinkCount(int blinkCount) {
this.blinkCount = blinkCount;
}
public int getBlinkDelayTicks() {
return blinkDelayTicks;
}
public void setBlinkDelayTicks(int blinkDelayTicks) {
this.blinkDelayTicks = blinkDelayTicks;
}
public float getBlinkRadius() {
return blinkRadius;
}
public void setBlinkRadius(float blinkRadius) {
this.blinkRadius = blinkRadius;
}
public float getBehindDistance() {
return behindDistance;
}
public void setBehindDistance(float behindDistance) {
this.behindDistance = behindDistance;
}
public boolean isRequireLineOfSight() {
return requireLineOfSight;
}
public void setRequireLineOfSight(boolean requireLineOfSight) {
this.requireLineOfSight = requireLineOfSight;
}
public boolean isDamageAtStart() {
return damageAtStart;
}
public void setDamageAtStart(boolean damageAtStart) {
this.damageAtStart = damageAtStart;
}
public boolean isDamageAtEnd() {
return damageAtEnd;
}
public void setDamageAtEnd(boolean damageAtEnd) {
this.damageAtEnd = damageAtEnd;
}
public float getDamage() {
return damage;
}
public void setDamage(float damage) {
this.damage = damage;
}
@Override
public float getDisplayDamage() { return damage; }
public float getDamageRadius() {
return damageRadius;
}
public void setDamageRadius(float damageRadius) {
this.damageRadius = damageRadius;
}
@Override
public int getMaxPreviewDuration() {
int blinkLimit = mode == TeleportMode.BLINK ? blinkCount : 1;
return blinkLimit * blinkDelayTicks + 10;
}
@ClientOnly
@Override
public void getAbilityDefinitions(List<FieldDef> defs) {
defs.addAll(Arrays.asList(
FieldDef.enumField("ability.mode", TeleportMode.class, this::getModeEnum, this::setModeEnum),
FieldDef.floatField("ability.blinkRadius", this::getBlinkRadius, this::setBlinkRadius)
.visibleWhen(() -> this.getModeEnum() == TeleportMode.BLINK || this.getModeEnum() == TeleportMode.SINGLE),
FieldDef.row(
FieldDef.intField("ability.blinkCount", this::getBlinkCount, this::setBlinkCount)
.visibleWhen(() -> this.getModeEnum() == TeleportMode.BLINK),
FieldDef.intField("ability.blinkDelay", this::getBlinkDelayTicks, this::setBlinkDelayTicks)
.visibleWhen(() -> this.getModeEnum() == TeleportMode.BLINK)
),
FieldDef.floatField("ability.behindDistance", this::getBehindDistance, this::setBehindDistance)
.visibleWhen(() -> this.getModeEnum() == TeleportMode.BEHIND),
FieldDef.boolField("ability.lineOfSight", this::isRequireLineOfSight, this::setRequireLineOfSight)
.hover("ability.hover.lineOfSight")
.visibleWhen(() -> this.getModeEnum() == TeleportMode.BLINK || this.getModeEnum() == TeleportMode.SINGLE),
FieldDef.section("ability.section.damage"),
FieldDef.row(
FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage),
FieldDef.floatField("gui.radius", this::getDamageRadius, this::setDamageRadius)
),
FieldDef.row(
FieldDef.boolField("ability.damageAtStart", this::isDamageAtStart, this::setDamageAtStart)
.hover("ability.hover.dmgAtStart"),
FieldDef.boolField("ability.damageAtEnd", this::isDamageAtEnd, this::setDamageAtEnd)
.hover("ability.hover.dmgAtEnd")
),
AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects)
));
}
}
@@ -0,0 +1,370 @@
package kamkeel.npcs.controllers.data.ability.type;
import noppes.npcs.constants.ClientOnly;
import kamkeel.npcs.util.IVector3;
import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler;
import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback;
import kamkeel.npcs.controllers.data.ability.gui.FieldDef;
import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider;
import noppes.npcs.entity.EntityNPCInterface;
import kamkeel.npcs.entity.EntityEnergyDome;
import kamkeel.npcs.entity.EntityEnergyBarrier;
import kamkeel.npcs.entity.EntityEnergyPanel;
import kamkeel.npcs.entity.EntityAbilityOrb;
import kamkeel.npcs.entity.EntityAbilityLaser;
import kamkeel.npcs.entity.EntityAbilityDisc;
import kamkeel.npcs.entity.EntityAbilityBeam;
import kamkeel.npcs.entity.EntityEnergyProjectile;
import kamkeel.npcs.util.ByteBufUtils;
import noppes.npcs.api.entity.IEntityLiving;
import noppes.npcs.api.IDamageSource;
import noppes.npcs.api.IWorld;
import noppes.npcs.api.entity.IEntityLivingBase;
import noppes.npcs.api.item.IItemStack;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.api.INbtList;
import noppes.npcs.api.INbt;
import kamkeel.npcs.controllers.data.ability.data.effect.AbilityPotionEffect;
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyDisplayData;
import noppes.npcs.api.ability.type.IAbilityTrap;
import noppes.npcs.constants.EnumPotionType;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class AbilityTrap extends AbilityZone implements IAbilityTrap {
private float triggerRadius = 2.0f;
private int armTime = 20;
private int maxTriggers = 1;
private int triggerCooldown = 20;
private float damage = 6.0f;
private float damageRadius = 0.0f;
private float knockback = 0.5f;
private boolean visible = true;
public AbilityTrap() {
super(300, new EnergyDisplayData(0xFF6600, 0xFF0000, true, 1.0f, 0.5f, 1.5f));
this.typeId = "ability.cnpc.trap";
this.name = "Trap";
this.windUpTicks = 30;
this.windUpAnimationName = "Ability_Zone_Windup";
this.activeAnimationName = "Ability_Zone_Active";
this.defaultIconLayers = new DefaultIconLayer[]{
new DefaultIconLayer("customnpcs:textures/gui/ability/trap.png",
this::getActiveColor)
};
}
@Override
public boolean isConcurrentCapable() {
return true;
}
@Override
public float getZoneRadius() {
return triggerRadius;
}
@Override
public void onExecute(IEntityLivingBase caster, IEntityLivingBase target) {
activeEntities.clear();
List<double[]> positions;
if (!preCalculatedPositions.isEmpty() && preCalculatedPositions.size() == zoneCount) {
positions = new ArrayList<>(preCalculatedPositions);
preCalculatedPositions.clear();
} else {
positions = new ArrayList<>();
List<double[]> placedPositions = new ArrayList<>();
float minSeparation = triggerRadius * 2.0f;
for (int i = 0; i < zoneCount; i++) {
double[] pos = findSpawnPosition(caster, placedPositions, minSeparation);
placedPositions.add(pos);
positions.add(pos);
}
}
for (int i = 0; i < zoneCount; i++) {
double[] pos = positions.get(i);
EntityAbilityZone IEntity = EntityAbilityZone.createTrap(caster.worldObj, caster,
pos[0], caster.posY, pos[1],
zoneShape,
triggerRadius, armTime, maxTriggers, triggerCooldown,
damage, damageRadius, knockback, durationTicks,
isIgnoreIFrames(),
colorData.innerColor, colorData.outerColor, colorData.outerColorEnabled,
zoneHeight,
particleDensity, particleScale, animSpeed, lightningDensity,
visible,
getEffects());
IEntity.setSourceAbility(this);
applyVisualToEntity(IEntity);
if (isPreview()) {
IEntity.setupPreview(caster);
}
spawnAbilityEntity(IEntity);
activeEntities.add(IEntity);
}
}
@ClientOnly
public enum TrapPreset {
HIDDEN, VENOM, EXPLOSIVE, CURSED, SHOCK, SNARE;
@Override
public String toString() {
return "ability.preset." + name().toLowerCase();
}
}
private void applyTrapPresetDefaults(String preset) {
effects.clear();
particleDir = "";
groundFill = true;
rings = false;
ringCount = 1;
border = false;
borderSpeed = 1.0f;
accents = false;
accentStyle = 0;
lightning = false;
particleGlow = false;
switch (preset) {
case "VENOM":
groundAlpha = 0.04f;
particles = true;
particleDensity = 0.3f;
particleScale = 0.5f;
particleMotion = 1;
particleDir = "mc:mobSpell";
colorData.innerColor = 0x44DD44;
colorData.outerColor = 0x116611;
windUpColor = 0x6044DD44;
activeColor = 0xC044FF44;
effects.add(new AbilityPotionEffect(EnumPotionType.Poison, 100, 0));
break;
case "EXPLOSIVE":
groundAlpha = 0.04f;
particles = true;
particleDensity = 0.3f;
particleScale = 0.6f;
particleMotion = 0;
particleDir = "mc:smoke";
colorData.innerColor = 0xFF6611;
colorData.outerColor = 0xCC2200;
windUpColor = 0x60FF6611;
activeColor = 0xC0FF4400;
effects.add(new AbilityPotionEffect(EnumPotionType.Fire, 60, 0));
break;
case "CURSED":
groundAlpha = 0.04f;
particles = true;
particleDensity = 0.3f;
particleScale = 0.5f;
particleMotion = 1;
particleDir = "mc:portal";
colorData.innerColor = 0xAA44FF;
colorData.outerColor = 0x6622BB;
windUpColor = 0x60AA44FF;
activeColor = 0xC0CC66FF;
effects.add(new AbilityPotionEffect(EnumPotionType.Weakness, 100, 0));
break;
case "SHOCK":
groundAlpha = 0.04f;
particles = true;
particleDensity = 0.3f;
particleScale = 0.4f;
particleMotion = 2;
particleDir = "mc:enchantmenttable";
colorData.innerColor = 0x4488FF;
colorData.outerColor = 0x2244BB;
windUpColor = 0x604488FF;
activeColor = 0xC066AAFF;
effects.add(new AbilityPotionEffect(EnumPotionType.MiningFatigue, 80, 1));
break;
case "SNARE":
groundAlpha = 0.04f;
particles = true;
particleDensity = 0.3f;
particleScale = 0.5f;
particleMotion = 1;
particleDir = "mc:snowshovel";
colorData.innerColor = 0x88CCFF;
colorData.outerColor = 0x4488CC;
windUpColor = 0x6088CCFF;
activeColor = 0xC0AADDFF;
effects.add(new AbilityPotionEffect(EnumPotionType.Slowness, 100, 1));
break;
case "HIDDEN":
default:
groundAlpha = 0.03f;
particles = false;
particleDensity = 0.0f;
particleScale = 1.0f;
particleMotion = 0;
colorData.innerColor = 0x888888;
colorData.outerColor = 0x444444;
windUpColor = 0x60888888;
activeColor = 0xC0AAAAAA;
break;
}
}
@Override
public void writeTypeNBT(INbt nbt) {
writeZoneNBT(nbt);
nbt.setFloat("triggerRadius", triggerRadius);
nbt.setInteger("armTime", armTime);
nbt.setInteger("maxTriggers", maxTriggers);
nbt.setInteger("triggerCooldown", triggerCooldown);
nbt.setFloat("damage", damage);
nbt.setFloat("damageRadius", damageRadius);
nbt.setFloat("knockback", knockback);
nbt.setBoolean("visible", visible);
}
@Override
public void readTypeNBT(INbt nbt) {
readZoneNBT(nbt, 300);
this.triggerRadius = nbt.getFloat("triggerRadius");
this.armTime = nbt.getInteger("armTime");
this.maxTriggers = nbt.getInteger("maxTriggers");
this.triggerCooldown = nbt.getInteger("triggerCooldown");
this.damage = nbt.getFloat("damage");
this.damageRadius = nbt.getFloat("damageRadius");
this.knockback = nbt.getFloat("knockback");
this.visible = nbt.getBoolean("visible");
}
public float getTriggerRadius() {
return triggerRadius;
}
public void setTriggerRadius(float triggerRadius) {
this.triggerRadius = triggerRadius;
}
public int getArmTime() {
return armTime;
}
public void setArmTime(int armTime) {
this.armTime = armTime;
}
public int getMaxTriggers() {
return maxTriggers;
}
public void setMaxTriggers(int maxTriggers) {
this.maxTriggers = maxTriggers;
}
public int getTriggerCooldown() {
return triggerCooldown;
}
public void setTriggerCooldown(int triggerCooldown) {
this.triggerCooldown = triggerCooldown;
}
public float getDamage() {
return damage;
}
public void setDamage(float damage) {
this.damage = damage;
}
@Override
public float getDisplayDamage() { return damage; }
public float getDamageRadius() {
return damageRadius;
}
public void setDamageRadius(float damageRadius) {
this.damageRadius = damageRadius;
}
public float getKnockback() {
return knockback;
}
public void setKnockback(float knockback) {
this.knockback = knockback;
}
public boolean isVisible() {
return visible;
}
public void setVisible(boolean visible) {
this.visible = visible;
}
@ClientOnly
@Override
protected void addPresetFieldDef(List<FieldDef> defs) {
defs.add(FieldDef.subGuiField("gui.applyPreset",
SubGuiTrapPresetSelector::new,
gui -> {
SubGuiTrapPresetSelector selector = (SubGuiTrapPresetSelector) gui;
if (selector.selectedPreset != null) {
applyTrapPresetDefaults(selector.selectedPreset);
}
}));
}
@ClientOnly
@Override
public void getAbilityDefinitions(List<FieldDef> defs) {
addPresetFieldDef(defs);
defs.addAll(Arrays.asList(
FieldDef.row(
FieldDef.intField("ability.duration", this::getDurationTicks, this::setDurationTicks).range(1, 2000),
FieldDef.enumField("ability.zoneShape", ZoneShape.class, this::getZoneShapeEnum, this::setZoneShapeEnum)
),
FieldDef.section("ability.section.zone"),
FieldDef.row(
FieldDef.floatField("gui.radius", this::getSpawnRadius, this::setSpawnRadius),
FieldDef.intField("gui.count", this::getZoneCount, this::setZoneCount).range(1, 20)
),
FieldDef.row(
FieldDef.floatField("gui.height", this::getZoneHeight, this::setZoneHeight),
FieldDef.boolField("ability.visible", this::isVisible, this::setVisible)
),
FieldDef.section("ability.section.trigger"),
FieldDef.row(
FieldDef.floatField("gui.radius", this::getTriggerRadius, this::setTriggerRadius),
FieldDef.intField("ability.armTime", this::getArmTime, this::setArmTime)
),
FieldDef.row(
FieldDef.intField("gui.max", this::getMaxTriggers, this::setMaxTriggers),
FieldDef.intField("gui.cooldown", this::getTriggerCooldown, this::setTriggerCooldown)
),
FieldDef.section("ability.section.damage"),
FieldDef.row(
FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage),
FieldDef.floatField("gui.radius", this::getDamageRadius, this::setDamageRadius)
),
FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback),
AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects)
));
addVisualFieldDefs(defs);
}
}
@@ -0,0 +1,426 @@
package kamkeel.npcs.controllers.data.ability.type;
import noppes.npcs.constants.ClientOnly;
import kamkeel.npcs.util.IVector3;
import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler;
import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback;
import kamkeel.npcs.controllers.data.ability.gui.FieldDef;
import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider;
import noppes.npcs.entity.EntityNPCInterface;
import kamkeel.npcs.entity.EntityEnergyDome;
import kamkeel.npcs.entity.EntityEnergyBarrier;
import kamkeel.npcs.entity.EntityEnergyPanel;
import kamkeel.npcs.entity.EntityAbilityOrb;
import kamkeel.npcs.entity.EntityAbilityLaser;
import kamkeel.npcs.entity.EntityAbilityDisc;
import kamkeel.npcs.entity.EntityAbilityBeam;
import kamkeel.npcs.entity.EntityEnergyProjectile;
import kamkeel.npcs.util.ByteBufUtils;
import noppes.npcs.api.entity.IEntityLiving;
import noppes.npcs.api.IDamageSource;
import noppes.npcs.api.IWorld;
import noppes.npcs.api.entity.IEntityLivingBase;
import noppes.npcs.api.item.IItemStack;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.api.INbtList;
import noppes.npcs.api.INbt;
import kamkeel.npcs.controllers.data.ability.Ability;
import kamkeel.npcs.controllers.data.ability.util.AbilityTargetHelper;
import kamkeel.npcs.controllers.data.ability.enums.LockMode;
import kamkeel.npcs.controllers.data.ability.enums.TargetFilter;
import kamkeel.npcs.controllers.data.ability.enums.TargetingMode;
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
import noppes.npcs.api.ability.type.IAbilityVortex;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* Vortex ability: Pulls targets toward the caster.
* Can pull single target or AOE, with optional damage and stun on arrival.
*/
public class AbilityVortex extends Ability implements IAbilityVortex {
private float pullRadius = 8.0f;
private float pullStrength = 0.8f;
private float damage = 0.0f;
private float knockback = 0.0f;
private boolean aoe = true;
private boolean damageOnPull = false;
private float pullDamage = 0.0f;
// Runtime state — keyed by IEntity ID (not UUID, since cloned NPCs share UUIDs)
private transient Map<Integer, PullState> pulledEntities;
private transient boolean pullComplete = false;
private transient int ticksSincePullDamage = 0;
private static class PullState {
double lastX, lastY, lastZ;
int stuckTicks;
PullState(IEntityLivingBase IEntity) {
this.lastX = IEntity.posX;
this.lastY = IEntity.posY;
this.lastZ = IEntity.posZ;
this.stuckTicks = 0;
}
}
private Map<Integer, PullState> getPulledEntities() {
if (pulledEntities == null) {
pulledEntities = new HashMap<>();
}
return pulledEntities;
}
public AbilityVortex() {
this.typeId = "ability.cnpc.vortex";
this.name = "Vortex";
this.targetingMode = TargetingMode.AOE_SELF;
this.maxRange = 15.0f;
this.lockMovement = LockMode.WINDUP_AND_ACTIVE;
this.cooldownTicks = 0;
this.windUpTicks = 30;
this.telegraphType = TelegraphType.CIRCLE;
this.windUpSound = "mob.ghast.charge";
this.activeSound = "mob.ghast.fireball";
this.defaultIconLayers = new DefaultIconLayer[]{
new DefaultIconLayer("customnpcs:textures/gui/ability/vortex.png",
this::getActiveColor)
};
}
@Override
public boolean isTargetingModeLocked() {
return true;
}
@Override
public TargetingMode[] getAllowedTargetingModes() {
return new TargetingMode[]{TargetingMode.AOE_SELF};
}
@Override
public float getTelegraphRadius() {
return pullRadius;
}
@Override
public void onExecute(IEntityLivingBase caster, IEntityLivingBase target) {
getPulledEntities().clear();
pullComplete = false;
ticksSincePullDamage = 0;
if (!isPreview() && !caster.worldObj.isRemote) {
IBoundingBox box = caster.boundingBox.expand(pullRadius, pullRadius / 2, pullRadius);
@SuppressWarnings("unchecked")
List<IEntityLivingBase> entities = caster.worldObj.getEntitiesWithinAABB(IEntityLivingBase.class, box);
if (aoe) {
// ALL: pull every valid enemy in range
for (IEntityLivingBase IEntity : entities) {
if (IEntity == caster) continue;
if (IEntity.isDead) continue;
if (!AbilityTargetHelper.shouldAffect(caster, IEntity, TargetFilter.ENEMIES, false)) continue;
double dist = caster.getDistanceToEntity(IEntity);
if (dist <= pullRadius) {
getPulledEntities().put(IEntity.getEntityId(), new PullState(IEntity));
}
}
} else {
// SINGULAR: pull one random target within range
// NPC: pull aggro target if valid, otherwise random enemy
// Player: pull random enemy in range
if (!isPlayerCaster(caster) && target != null && !target.isDead) {
double dist = caster.getDistanceToEntity(target);
if (dist <= pullRadius) {
getPulledEntities().put(target.getEntityId(), new PullState(target));
return;
}
}
// Collect all valid enemies, then pick one at random
List<IEntityLivingBase> validTargets = new ArrayList<>();
for (IEntityLivingBase IEntity : entities) {
if (IEntity == caster) continue;
if (IEntity.isDead) continue;
if (!AbilityTargetHelper.shouldAffect(caster, IEntity, TargetFilter.ENEMIES, false)) continue;
double dist = caster.getDistanceToEntity(IEntity);
if (dist <= pullRadius) {
validTargets.add(IEntity);
}
}
if (!validTargets.isEmpty()) {
IEntityLivingBase chosen = validTargets.get(caster.worldObj.rand.nextInt(validTargets.size()));
getPulledEntities().put(chosen.getEntityId(), new PullState(chosen));
}
}
}
}
@Override
public void onActiveTick(IEntityLivingBase caster, IEntityLivingBase target, int tick) {
if (isPreview()) {
// No entities to pull in preview, just run animation for a duration
if (tick >= 60) signalCompletion();
return;
}
if (pullComplete || getPulledEntities().isEmpty()) {
signalCompletion();
return;
}
if (caster.worldObj.isRemote) return;
// Safety cap: force-complete if active too long
int maxActiveTicks = Math.max(40, (int)(pullRadius / pullStrength * 3));
if (tick >= maxActiveTicks) {
for (Map.Entry<Integer, PullState> entry : new HashMap<>(getPulledEntities()).entrySet()) {
IEntityLivingBase IEntity = findEntity(caster.worldObj, entry.getKey());
if (IEntity != null && !IEntity.isDead) {
onTargetArrived(caster, IEntity, caster.worldObj);
}
}
getPulledEntities().clear();
pullComplete = true;
signalCompletion();
return;
}
double destX = caster.posX;
double destY = caster.posY;
double destZ = caster.posZ;
boolean anyStillPulling = false;
ticksSincePullDamage++;
boolean shouldDealPullDamage = damageOnPull && pullDamage > 0 && ticksSincePullDamage >= 10;
if (shouldDealPullDamage) {
ticksSincePullDamage = 0;
}
for (Map.Entry<Integer, PullState> entry : new HashMap<>(getPulledEntities()).entrySet()) {
int entityId = entry.getKey();
PullState state = entry.getValue();
IEntityLivingBase IEntity = findEntity(caster.worldObj, entityId);
if (IEntity == null || IEntity.isDead) {
getPulledEntities().remove(entityId);
continue;
}
double dx = destX - IEntity.posX;
double dy = destY - IEntity.posY;
double dz = destZ - IEntity.posZ;
double dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
if (dist <= 1.5) {
getPulledEntities().remove(entityId);
onTargetArrived(caster, IEntity, caster.worldObj);
continue;
}
// Stuck detection: check if IEntity made progress since last tick
double lastDx = destX - state.lastX;
double lastDy = destY - state.lastY;
double lastDz = destZ - state.lastZ;
double lastDist = Math.sqrt(lastDx * lastDx + lastDy * lastDy + lastDz * lastDz);
double progress = lastDist - dist; // positive = IEntity moved closer
double expectedProgress = Math.min(pullStrength, dist * 0.5);
if (progress < expectedProgress * 0.3) {
state.stuckTicks++;
} else {
state.stuckTicks = 0;
}
if (state.stuckTicks >= 5) {
// IEntity is stuck (wall, block, partial obstruction) - treat as arrived
getPulledEntities().remove(entityId);
onTargetArrived(caster, IEntity, caster.worldObj);
continue;
}
anyStillPulling = true;
// Record position BEFORE movement so next tick's stuck detection
// can measure the actual progress (pull + AI movement combined)
state.lastX = IEntity.posX;
state.lastY = IEntity.posY;
state.lastZ = IEntity.posZ;
// Clamp speed to never exceed half the remaining distance, preventing overshoot/slingshot
double maxSpeed = dist * 0.5;
double effectiveSpeed = Math.min(pullStrength, maxSpeed);
double factor = effectiveSpeed / dist;
double motionX = dx * factor;
double motionY = dy * factor * 0.5;
double motionZ = dz * factor;
if (IEntity instanceof IPlayer) {
// Players: send velocity packet — client applies movement
IEntity.motionX = motionX;
IEntity.motionY = motionY;
IEntity.motionZ = motionZ;
IEntity.velocityChanged = true;
} else {
// NPCs/mobs: directly apply movement with collision detection.
// Setting motionX alone is unreliable because the IEntity's AI tick
// (moveFlying/moveEntityWithHeading) can override the velocity before
// moveEntity runs, depending on IEntity tick order relative to the caster.
IEntity.moveEntity(motionX, motionY, motionZ);
}
if (shouldDealPullDamage) {
applyAbilityDamage(caster, IEntity, pullDamage * 0.5f, 0);
}
}
if (!anyStillPulling && getPulledEntities().isEmpty()) {
pullComplete = true;
signalCompletion();
}
}
private void onTargetArrived(IEntityLivingBase caster, IEntityLivingBase IEntity, IWorld IWorld) {
// Apply damage with scripted event support
boolean wasHit = applyAbilityDamage(caster, IEntity, damage, knockback * 0.5f);
// Only apply effects if hit wasn't cancelled
if (wasHit) {
applyEffects(IEntity);
}
}
/**
* Find an IEntity by IEntity ID.
*/
private IEntityLivingBase findEntity(IWorld IWorld, int entityId) {
IEntity IEntity = IWorld.getEntityByID(entityId);
if (IEntity instanceof IEntityLivingBase) {
return (IEntityLivingBase) IEntity;
}
return null;
}
@Override
public void cleanup() {
getPulledEntities().clear();
pullComplete = false;
ticksSincePullDamage = 0;
}
@Override
public void writeTypeNBT(INbt nbt) {
nbt.setFloat("pullRadius", pullRadius);
nbt.setFloat("pullStrength", pullStrength);
nbt.setFloat("damage", damage);
nbt.setFloat("knockback", knockback);
nbt.setBoolean("aoe", aoe);
nbt.setBoolean("damageOnPull", damageOnPull);
nbt.setFloat("pullDamage", pullDamage);
}
@Override
public void readTypeNBT(INbt nbt) {
this.pullRadius = nbt.getFloat("pullRadius");
this.pullStrength = nbt.getFloat("pullStrength");
this.damage = nbt.getFloat("damage");
this.knockback = nbt.getFloat("knockback");
this.aoe = !nbt.hasKey("aoe") || nbt.getBoolean("aoe");
this.damageOnPull = nbt.getBoolean("damageOnPull");
this.pullDamage = nbt.getFloat("pullDamage");
}
// Getters & Setters
public float getPullRadius() {
return pullRadius;
}
public void setPullRadius(float pullRadius) {
this.pullRadius = pullRadius;
}
public float getPullStrength() {
return pullStrength;
}
public void setPullStrength(float pullStrength) {
this.pullStrength = pullStrength;
}
public float getDamage() {
return damage;
}
public void setDamage(float damage) {
this.damage = damage;
}
@Override
public float getDisplayDamage() { return damage; }
public float getKnockback() {
return knockback;
}
public void setKnockback(float knockback) {
this.knockback = knockback;
}
public boolean isAoe() {
return aoe;
}
public void setAoe(boolean aoe) {
this.aoe = aoe;
}
public boolean isDamageOnPull() {
return damageOnPull;
}
public void setDamageOnPull(boolean damageOnPull) {
this.damageOnPull = damageOnPull;
}
public float getPullDamage() {
return pullDamage;
}
public void setPullDamage(float pullDamage) {
this.pullDamage = pullDamage;
}
@ClientOnly
@Override
public void getAbilityDefinitions(List<FieldDef> defs) {
defs.addAll(Arrays.asList(
FieldDef.row(
FieldDef.floatField("ability.pullRadius", this::getPullRadius, this::setPullRadius),
FieldDef.floatField("ability.pullStrength", this::getPullStrength, this::setPullStrength)
),
FieldDef.section("ability.section.damage"),
FieldDef.row(
FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage),
FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback)
),
FieldDef.section("ability.section.aoe"),
FieldDef.boolField("gui.enabled", this::isAoe, this::setAoe)
.hover("ability.hover.aoe"),
FieldDef.section("ability.section.pullDamage"),
FieldDef.boolField("gui.enabled", this::isDamageOnPull, this::setDamageOnPull)
.hover("ability.hover.dmgOnPull"),
FieldDef.floatField("enchantment.damage", this::getPullDamage, this::setPullDamage)
.visibleWhen(this::isDamageOnPull),
AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects)
));
}
}
@@ -0,0 +1,788 @@
package kamkeel.npcs.controllers.data.ability.type;
import noppes.npcs.constants.ClientOnly;
import kamkeel.npcs.util.IVector3;
import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler;
import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback;
import kamkeel.npcs.controllers.data.ability.gui.FieldDef;
import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider;
import noppes.npcs.entity.EntityNPCInterface;
import kamkeel.npcs.entity.EntityEnergyDome;
import kamkeel.npcs.entity.EntityEnergyBarrier;
import kamkeel.npcs.entity.EntityEnergyPanel;
import kamkeel.npcs.entity.EntityAbilityOrb;
import kamkeel.npcs.entity.EntityAbilityLaser;
import kamkeel.npcs.entity.EntityAbilityDisc;
import kamkeel.npcs.entity.EntityAbilityBeam;
import kamkeel.npcs.entity.EntityEnergyProjectile;
import kamkeel.npcs.util.ByteBufUtils;
import noppes.npcs.api.entity.IEntityLiving;
import noppes.npcs.api.IDamageSource;
import noppes.npcs.api.IWorld;
import noppes.npcs.api.entity.IEntityLivingBase;
import noppes.npcs.api.item.IItemStack;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.api.INbtList;
import noppes.npcs.api.INbt;
import kamkeel.npcs.controllers.data.ability.Ability;
import kamkeel.npcs.controllers.data.ability.data.effect.AbilityPotionEffect;
import kamkeel.npcs.controllers.data.ability.enums.LockMode;
import kamkeel.npcs.controllers.data.ability.enums.TargetingMode;
import kamkeel.npcs.controllers.data.ability.enums.UserType;
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyDisplayData;
import kamkeel.npcs.controllers.data.telegraph.Telegraph;
import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance;
import noppes.npcs.constants.EnumPotionType;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import java.util.Random;
public abstract class AbilityZone extends Ability {
protected int durationTicks;
protected ZoneShape zoneShape = ZoneShape.CIRCLE;
protected float spawnRadius = 10.0f;
protected int zoneCount = 3;
protected float zoneHeight = 2.0f;
protected float particleDensity = 1.0f;
protected float particleScale = 1.0f;
protected float animSpeed = 1.0f;
protected float lightningDensity = 1.0f;
protected EnergyDisplayData colorData;
protected boolean groundFill = true;
protected float groundAlpha = 0.25f;
protected boolean rings = true;
protected int ringCount = 3;
protected boolean border = true;
protected float borderSpeed = 1.0f;
protected boolean accents = true;
protected int accentStyle = 0;
protected boolean lightning = false;
protected boolean particles = true;
protected int particleMotion = 0;
protected String particleDir = "";
protected int particleSize = 32;
protected boolean particleGlow = true;
protected static final Random RANDOM = new Random();
protected transient List<EntityAbilityZone> activeEntities = new ArrayList<>();
protected transient List<double[]> preCalculatedPositions = new ArrayList<>();
protected AbilityZone(int defaultDuration, EnergyDisplayData defaultColors) {
this.durationTicks = defaultDuration;
this.colorData = defaultColors;
this.targetingMode = TargetingMode.AGGRO_TARGET;
this.maxRange = 20.0f;
this.lockMovement = LockMode.WINDUP;
this.cooldownTicks = 0;
this.allowedBy = UserType.BOTH;
}
@Override
public boolean allowBurst() {
return false;
}
@Override
public boolean allowFreeOnCast() {
return true;
}
@Override
public void detach() {
activeEntities.clear();
preCalculatedPositions.clear();
}
@Override
public boolean isTargetingModeLocked() {
return true;
}
@Override
public TargetingMode[] getAllowedTargetingModes() {
return new TargetingMode[]{TargetingMode.AGGRO_TARGET};
}
public abstract float getZoneRadius();
@Override
public List<TelegraphInstance> createTelegraphs(IEntityLivingBase caster, IEntityLivingBase target) {
if (!showTelegraph || windUpTicks <= 0) return new ArrayList<>();
preCalculatedPositions.clear();
List<double[]> placedPositions = new ArrayList<>();
float minSeparation = getZoneRadius() * 2.0f;
for (int i = 0; i < zoneCount; i++) {
double[] pos = findSpawnPosition(caster, placedPositions, minSeparation);
placedPositions.add(pos);
preCalculatedPositions.add(pos);
}
List<TelegraphInstance> telegraphs = new ArrayList<>();
float zoneRadius = getZoneRadius();
for (double[] pos : preCalculatedPositions) {
Telegraph telegraph;
if (zoneShape == ZoneShape.SQUARE) {
telegraph = Telegraph.square(zoneRadius);
} else {
telegraph = Telegraph.circle(zoneRadius);
}
telegraph.setDurationTicks(windUpTicks);
telegraph.setColor(windUpColor);
telegraph.setWarningColor(activeColor);
telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4));
telegraph.setHeightOffset(telegraphHeightOffset);
double groundY = findGroundLevel(caster.worldObj, pos[0], caster.posY, pos[1]);
TelegraphInstance instance = new TelegraphInstance(telegraph, pos[0], groundY, pos[1], 0);
instance.setCasterEntityId(caster.getEntityId());
instance.setEntityIdToFollow(-1);
telegraphs.add(instance);
}
return telegraphs;
}
@Override
public void onActiveTick(IEntityLivingBase caster, IEntityLivingBase target, int tick) {
// Free on Cast: complete immediately — zones live independently
if (isFreeOnCast()) {
signalCompletion();
return;
}
Iterator<EntityAbilityZone> it = activeEntities.iterator();
while (it.hasNext()) {
EntityAbilityZone IEntity = it.next();
if (IEntity == null || IEntity.isDead) {
it.remove();
}
}
if (activeEntities.isEmpty()) {
signalCompletion();
}
}
@Override
public void onComplete(IEntityLivingBase caster, IEntityLivingBase target) {
cleanup();
}
@Override
public void onInterrupt(IEntityLivingBase caster, IDamageSource source, float damage) {
cleanup();
}
@Override
public void cleanup() {
for (EntityAbilityZone IEntity : activeEntities) {
if (IEntity != null && !IEntity.isDead) {
killAbilityEntity(IEntity);
}
}
activeEntities.clear();
preCalculatedPositions.clear();
}
@Override
public int getMaxPreviewDuration() {
return durationTicks + 10;
}
protected double[] findSpawnPosition(IEntityLivingBase caster, List<double[]> placedPositions, float minSeparation) {
double spawnX = caster.posX;
double spawnZ = caster.posZ;
for (int attempt = 0; attempt < 15; attempt++) {
double angle = RANDOM.nextDouble() * Math.PI * 2;
double dist = Math.sqrt(RANDOM.nextDouble()) * spawnRadius;
double candidateX = caster.posX + Math.cos(angle) * dist;
double candidateZ = caster.posZ + Math.sin(angle) * dist;
boolean overlaps = false;
for (double[] placed : placedPositions) {
double dx = candidateX - placed[0];
double dz = candidateZ - placed[1];
if (Math.sqrt(dx * dx + dz * dz) < minSeparation) {
overlaps = true;
break;
}
}
spawnX = candidateX;
spawnZ = candidateZ;
if (!overlaps) break;
}
return new double[]{spawnX, spawnZ};
}
protected void applyVisualToEntity(EntityAbilityZone IEntity) {
IEntity.applyVisual(groundFill, groundAlpha,
rings, ringCount, border, borderSpeed,
accents, accentStyle, lightning,
particles, particleMotion, particleDir,
particleSize, particleGlow);
}
private void applyPresetDefaults(String styleName) {
effects.clear();
particleDir = "";
switch (styleName) {
case "TOXIC":
groundFill = true;
groundAlpha = 0.30f;
rings = true;
ringCount = 3;
border = false;
borderSpeed = 1.0f;
accents = true;
accentStyle = 1;
lightning = false;
particles = true;
particleDensity = 1.5f;
particleScale = 0.8f;
particleMotion = 1;
particleGlow = true;
particleDir = "mc:mobSpell";
colorData.innerColor = 0x44DD44;
colorData.outerColor = 0x116611;
windUpColor = 0x6044DD44;
activeColor = 0xC044FF44;
effects.add(new AbilityPotionEffect(EnumPotionType.Poison, 100, 0));
break;
case "INFERNO":
groundFill = true;
groundAlpha = 0.35f;
rings = true;
ringCount = 3;
border = true;
borderSpeed = 2.0f;
accents = true;
accentStyle = 2;
lightning = false;
particles = true;
particleDensity = 2.0f;
particleScale = 1.2f;
particleMotion = 0;
particleGlow = true;
particleDir = "mc:flame";
colorData.innerColor = 0xFF6611;
colorData.outerColor = 0xCC2200;
windUpColor = 0x60FF6611;
activeColor = 0xC0FF4400;
effects.add(new AbilityPotionEffect(EnumPotionType.Fire, 60, 0));
break;
case "ARCANE":
groundFill = true;
groundAlpha = 0.20f;
rings = true;
ringCount = 1;
border = true;
borderSpeed = 0.8f;
accents = true;
accentStyle = 0;
lightning = false;
particles = true;
particleDensity = 1.0f;
particleScale = 1.0f;
particleMotion = 1;
particleGlow = true;
particleDir = "mc:portal";
colorData.innerColor = 0xAA44FF;
colorData.outerColor = 0x6622BB;
windUpColor = 0x60AA44FF;
activeColor = 0xC0CC66FF;
effects.add(new AbilityPotionEffect(EnumPotionType.Weakness, 100, 0));
break;
case "ELECTRIC":
groundFill = true;
groundAlpha = 0.15f;
rings = false;
ringCount = 1;
border = false;
borderSpeed = 1.0f;
accents = false;
accentStyle = 0;
lightning = true;
particles = true;
particleDensity = 0.8f;
particleScale = 0.6f;
particleMotion = 2;
particleGlow = true;
particleDir = "mc:enchantmenttable";
colorData.innerColor = 0x4488FF;
colorData.outerColor = 0x2244BB;
windUpColor = 0x604488FF;
activeColor = 0xC066AAFF;
effects.add(new AbilityPotionEffect(EnumPotionType.MiningFatigue, 80, 1));
break;
case "FROST":
groundFill = true;
groundAlpha = 0.25f;
rings = true;
ringCount = 3;
border = true;
borderSpeed = 0.3f;
accents = true;
accentStyle = 0;
lightning = false;
particles = true;
particleDensity = 1.5f;
particleScale = 1.0f;
particleMotion = 1;
particleGlow = true;
particleDir = "mc:snowshovel";
colorData.innerColor = 0x88CCFF;
colorData.outerColor = 0x4488CC;
windUpColor = 0x6088CCFF;
activeColor = 0xC0AADDFF;
effects.add(new AbilityPotionEffect(EnumPotionType.Slowness, 100, 1));
break;
case "DEFAULT":
groundFill = true;
groundAlpha = 0.25f;
rings = true;
ringCount = 3;
border = true;
borderSpeed = 1.0f;
accents = true;
accentStyle = 0;
lightning = false;
particles = false;
particleDensity = 1.0f;
particleScale = 1.0f;
particleMotion = 0;
particleGlow = true;
colorData.innerColor = 0xCCCCCC;
colorData.outerColor = 0x666666;
windUpColor = 0x60CCCCCC;
activeColor = 0xC0FFFFFF;
break;
default:
break;
}
}
@ClientOnly
public enum ZonePreset {
DEFAULT, TOXIC, INFERNO, ARCANE, ELECTRIC, FROST;
@Override
public String toString() {
return "ability.preset." + name().toLowerCase();
}
}
protected void writeZoneNBT(INbt nbt) {
nbt.setInteger("durationTicks", durationTicks);
nbt.setString("zoneShape", zoneShape.name());
nbt.setFloat("spawnRadius", spawnRadius);
nbt.setInteger("zoneCount", zoneCount);
nbt.setFloat("zoneHeight", zoneHeight);
nbt.setFloat("particleDensity", particleDensity);
nbt.setFloat("particleScale", particleScale);
nbt.setFloat("animSpeed", animSpeed);
nbt.setFloat("lightningDensity", lightningDensity);
colorData.writeNBT(new NBTWrapper(nbt));
nbt.setBoolean("groundFill", groundFill);
nbt.setFloat("groundAlpha", groundAlpha);
nbt.setBoolean("rings", rings);
nbt.setInteger("ringCount", ringCount);
nbt.setBoolean("border", border);
nbt.setFloat("borderSpeed", borderSpeed);
nbt.setBoolean("accents", accents);
nbt.setInteger("accentStyle", accentStyle);
nbt.setBoolean("lightning", lightning);
nbt.setBoolean("particles", particles);
nbt.setInteger("particleMotion", particleMotion);
nbt.setString("particleDir", particleDir);
nbt.setInteger("particleSize", particleSize);
nbt.setBoolean("particleGlow", particleGlow);
}
protected void readZoneNBT(INbt nbt, int defaultDuration) {
this.durationTicks = nbt.getInteger("durationTicks");
try {
this.zoneShape = ZoneShape.valueOf(nbt.getString("zoneShape"));
} catch (Exception e) {
this.zoneShape = ZoneShape.CIRCLE;
}
this.spawnRadius = nbt.getFloat("spawnRadius");
this.zoneCount = nbt.getInteger("zoneCount");
this.zoneHeight = nbt.getFloat("zoneHeight");
this.particleDensity = nbt.getFloat("particleDensity");
this.particleScale = nbt.getFloat("particleScale");
this.animSpeed = nbt.getFloat("animSpeed");
this.lightningDensity = nbt.getFloat("lightningDensity");
colorData.readNBT(new NBTWrapper(nbt));
this.groundFill = nbt.getBoolean("groundFill");
this.groundAlpha = nbt.getFloat("groundAlpha");
this.rings = nbt.getBoolean("rings");
this.ringCount = nbt.getInteger("ringCount");
this.border = nbt.getBoolean("border");
this.borderSpeed = nbt.getFloat("borderSpeed");
this.accents = nbt.getBoolean("accents");
this.accentStyle = nbt.getInteger("accentStyle");
this.lightning = nbt.getBoolean("lightning");
this.particles = nbt.getBoolean("particles");
this.particleMotion = nbt.getInteger("particleMotion");
this.particleDir = nbt.getString("particleDir");
this.particleSize = nbt.getInteger("particleSize");
this.particleGlow = nbt.getBoolean("particleGlow");
}
public int getDurationTicks() {
return durationTicks;
}
public void setDurationTicks(int durationTicks) {
this.durationTicks = Math.max(1, durationTicks);
}
public ZoneShape getZoneShapeEnum() {
return zoneShape;
}
public void setZoneShapeEnum(ZoneShape shape) {
this.zoneShape = shape;
}
public int getZoneShapeOrdinal() {
return zoneShape.ordinal();
}
public void setZoneShapeOrdinal(int shape) {
ZoneShape[] values = ZoneShape.values();
this.zoneShape = shape >= 0 && shape < values.length ? values[shape] : ZoneShape.CIRCLE;
}
public float getSpawnRadius() {
return spawnRadius;
}
public void setSpawnRadius(float spawnRadius) {
this.spawnRadius = Math.max(0, spawnRadius);
}
public int getZoneCount() {
return zoneCount;
}
public void setZoneCount(int zoneCount) {
this.zoneCount = Math.max(1, zoneCount);
}
public float getZoneHeight() {
return zoneHeight;
}
public void setZoneHeight(float zoneHeight) {
this.zoneHeight = Math.max(0.5f, zoneHeight);
}
public float getParticleDensity() {
return particleDensity;
}
public void setParticleDensity(float v) {
this.particleDensity = Math.max(0, Math.min(5, v));
}
public float getParticleScale() {
return particleScale;
}
public void setParticleScale(float v) {
this.particleScale = Math.max(0.1f, Math.min(5, v));
}
public float getAnimSpeed() {
return animSpeed;
}
public void setAnimSpeed(float v) {
this.animSpeed = Math.max(0.1f, Math.min(5, v));
}
public float getLightningDensity() {
return lightningDensity;
}
public void setLightningDensity(float v) {
this.lightningDensity = Math.max(0, Math.min(3, v));
}
public int getInnerColor() {
return colorData.innerColor;
}
public void setInnerColor(int color) {
colorData.innerColor = color;
}
public int getOuterColor() {
return colorData.outerColor;
}
public void setOuterColor(int color) {
colorData.outerColor = color;
}
public boolean isOuterColorEnabled() {
return colorData.outerColorEnabled;
}
public void setOuterColorEnabled(boolean enabled) {
colorData.outerColorEnabled = enabled;
}
public float getInnerAlpha() {
return colorData.innerAlpha;
}
public void setInnerAlpha(float alpha) {
colorData.innerAlpha = alpha;
}
public float getOuterColorWidth() {
return colorData.outerColorWidth;
}
public void setOuterColorWidth(float width) {
colorData.outerColorWidth = width;
}
public float getOuterColorAlpha() {
return colorData.outerColorAlpha;
}
public void setOuterColorAlpha(float alpha) {
colorData.outerColorAlpha = alpha;
}
public boolean isGroundFill() {
return groundFill;
}
public void setGroundFill(boolean v) {
this.groundFill = v;
}
public float getGroundAlpha() {
return groundAlpha;
}
public void setGroundAlpha(float v) {
this.groundAlpha = Math.max(0, Math.min(1, v));
}
public boolean isRings() {
return rings;
}
public void setRings(boolean v) {
this.rings = v;
}
public int getRingCount() {
return ringCount;
}
public void setRingCount(int v) {
this.ringCount = Math.max(1, Math.min(5, v));
}
public boolean isBorder() {
return border;
}
public void setBorder(boolean v) {
this.border = v;
}
public float getBorderSpeed() {
return borderSpeed;
}
public void setBorderSpeed(float v) {
this.borderSpeed = Math.max(0.1f, Math.min(5, v));
}
public boolean isAccents() {
return accents;
}
public void setAccents(boolean v) {
this.accents = v;
}
public int getAccentStyle() {
return accentStyle;
}
public void setAccentStyle(int v) {
this.accentStyle = Math.max(0, Math.min(AccentStyle.values().length - 1, v));
}
public AccentStyle getAccentStyleEnum() {
return AccentStyle.values()[Math.max(0, Math.min(accentStyle, AccentStyle.values().length - 1))];
}
public void setAccentStyleEnum(AccentStyle s) {
this.accentStyle = s.ordinal();
}
public boolean isLightning() {
return lightning;
}
public void setLightning(boolean v) {
this.lightning = v;
}
public boolean isParticles() {
return particles;
}
public void setParticles(boolean v) {
this.particles = v;
}
public int getParticleMotion() {
return particleMotion;
}
public void setParticleMotion(int v) {
this.particleMotion = Math.max(0, Math.min(ParticleMotion.values().length - 1, v));
}
public ParticleMotion getParticleMotionEnum() {
return ParticleMotion.values()[Math.max(0, Math.min(particleMotion, ParticleMotion.values().length - 1))];
}
public void setParticleMotionEnum(ParticleMotion m) {
this.particleMotion = m.ordinal();
}
public String getParticleDir() {
return particleDir;
}
public void setParticleDir(String v) {
this.particleDir = v != null ? v : "";
}
public int getParticleSize() {
return particleSize;
}
public void setParticleSize(int v) {
this.particleSize = Math.max(1, Math.min(256, v));
}
public boolean isParticleGlow() {
return particleGlow;
}
public void setParticleGlow(boolean v) {
this.particleGlow = v;
}
@ClientOnly
protected void addPresetFieldDef(List<FieldDef> defs) {
defs.add(FieldDef.subGuiField("gui.applyPreset",
SubGuiZonePresetSelector::new,
gui -> {
SubGuiZonePresetSelector selector = (SubGuiZonePresetSelector) gui;
if (selector.selectedPreset != null) {
applyPresetDefaults(selector.selectedPreset);
}
}));
}
@ClientOnly
protected void addVisualFieldDefs(List<FieldDef> defs) {
defs.addAll(Arrays.asList(
FieldDef.section("ability.section.ground").tab("ability.tab.visual"),
FieldDef.row(
FieldDef.boolField("gui.enabled", this::isGroundFill, this::setGroundFill),
FieldDef.floatField("gui.alpha", this::getGroundAlpha, this::setGroundAlpha)
).tab("ability.tab.visual"),
FieldDef.section("ability.section.rings").tab("ability.tab.visual"),
FieldDef.row(
FieldDef.boolField("gui.enabled", this::isRings, this::setRings),
FieldDef.intField("gui.count", this::getRingCount, this::setRingCount).range(1, 5)
).tab("ability.tab.visual"),
FieldDef.section("ability.section.border").tab("ability.tab.visual"),
FieldDef.row(
FieldDef.boolField("gui.enabled", this::isBorder, this::setBorder),
FieldDef.floatField("gui.speed", this::getBorderSpeed, this::setBorderSpeed)
).tab("ability.tab.visual"),
FieldDef.section("ability.section.accents").tab("ability.tab.visual"),
FieldDef.row(
FieldDef.boolField("gui.enabled", this::isAccents, this::setAccents),
FieldDef.enumField("gui.style", AccentStyle.class, this::getAccentStyleEnum, this::setAccentStyleEnum)
).tab("ability.tab.visual"),
FieldDef.section("ability.section.lightning").tab("ability.tab.visual"),
FieldDef.row(
FieldDef.boolField("gui.enabled", this::isLightning, this::setLightning),
FieldDef.floatField("gui.density", this::getLightningDensity, this::setLightningDensity)
).tab("ability.tab.visual"),
FieldDef.section("ability.section.particles").tab("ability.tab.visual"),
FieldDef.boolField("gui.enabled", this::isParticles, this::setParticles)
.tab("ability.tab.visual"),
FieldDef.row(
FieldDef.floatField("gui.density", this::getParticleDensity, this::setParticleDensity),
FieldDef.floatField("gui.scale", this::getParticleScale, this::setParticleScale)
).tab("ability.tab.visual").visibleWhen(this::isParticles),
FieldDef.row(
FieldDef.enumField("gui.motion", ParticleMotion.class, this::getParticleMotionEnum, this::setParticleMotionEnum),
FieldDef.boolField("gui.glow", this::isParticleGlow, this::setParticleGlow)
).tab("ability.tab.visual").visibleWhen(this::isParticles),
FieldDef.stringField("gui.texture", this::getParticleDir, this::setParticleDir)
.tab("ability.tab.visual").visibleWhen(this::isParticles),
FieldDef.section("ability.section.animation").tab("ability.tab.visual"),
FieldDef.floatField("gui.speed", this::getAnimSpeed, this::setAnimSpeed)
.tab("ability.tab.visual"),
FieldDef.section("ability.section.colors").tab("ability.tab.visual"),
FieldDef.row(
FieldDef.colorSubGui("ability.innerColor", this::getInnerColor, this::setInnerColor),
FieldDef.floatField("ability.innerAlpha", this::getInnerAlpha, this::setInnerAlpha).range(0, 1)
).tab("ability.tab.visual"),
FieldDef.boolField("ability.outerEnabled", this::isOuterColorEnabled, this::setOuterColorEnabled)
.tab("ability.tab.visual"),
FieldDef.colorSubGui("ability.outerColor", this::getOuterColor, this::setOuterColor)
.tab("ability.tab.visual").visibleWhen(this::isOuterColorEnabled),
FieldDef.row(
FieldDef.floatField("ability.outerWidth", this::getOuterColorWidth, this::setOuterColorWidth)
.visibleWhen(this::isOuterColorEnabled),
FieldDef.floatField("ability.outerAlpha", this::getOuterColorAlpha, this::setOuterColorAlpha)
.range(0, 1).visibleWhen(this::isOuterColorEnabled)
).tab("ability.tab.visual")
));
}
}
@@ -0,0 +1,12 @@
package kamkeel.npcs.controllers.data.ability.type;
/**
* TEMPORARY STUB -- auto-generated for compilation.
* Accent style enum used by AbilityZone.
*/
public enum AccentStyle {
NONE,
RING,
CROSS,
STAR;
}
@@ -0,0 +1,24 @@
package kamkeel.npcs.controllers.data.ability.type;
/**
* TEMPORARY STUB -- auto-generated for compilation.
* Will be replaced when this class is properly migrated.
* DO NOT add logic here.
*
* Entity for ability zones (hazards, traps, etc.).
* Actual entity stays in mc1710, this is just for core compilation.
*/
public class EntityAbilityZone {
public void applyVisual(boolean groundFill, float groundAlpha,
boolean rings, int ringCount, boolean border, float borderSpeed,
boolean accents, int accentStyle, boolean lightning,
boolean particles, int particleMotion, String particleDir,
int particleSize, boolean particleGlow) {}
public void setDurationTicks(int ticks) {}
public float getZoneRadius() { return 0; }
public float getZoneHeight() { return 0; }
public double posX = 0;
public double posY = 0;
public double posZ = 0;
}
@@ -0,0 +1,13 @@
package kamkeel.npcs.controllers.data.ability.type;
/**
* TEMPORARY STUB -- auto-generated for compilation.
* Particle motion enum used by AbilityZone.
*/
public enum ParticleMotion {
NONE,
INWARD,
OUTWARD,
UPWARD,
SPIRAL;
}
@@ -0,0 +1,11 @@
package kamkeel.npcs.controllers.data.ability.type;
/**
* TEMPORARY STUB -- auto-generated for compilation.
* Zone shape enum used by AbilityZone.
*/
public enum ZoneShape {
CIRCLE,
SQUARE,
LINE;
}
@@ -0,0 +1,443 @@
package kamkeel.npcs.controllers.data.ability.type.energy;
import noppes.npcs.constants.ClientOnly;
import kamkeel.npcs.util.IVector3;
import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler;
import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback;
import kamkeel.npcs.controllers.data.ability.gui.FieldDef;
import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider;
import noppes.npcs.entity.EntityNPCInterface;
import kamkeel.npcs.entity.EntityEnergyDome;
import kamkeel.npcs.entity.EntityEnergyBarrier;
import kamkeel.npcs.entity.EntityEnergyPanel;
import kamkeel.npcs.entity.EntityAbilityOrb;
import kamkeel.npcs.entity.EntityAbilityLaser;
import kamkeel.npcs.entity.EntityAbilityDisc;
import kamkeel.npcs.entity.EntityAbilityBeam;
import kamkeel.npcs.entity.EntityEnergyProjectile;
import kamkeel.npcs.util.ByteBufUtils;
import noppes.npcs.api.entity.IEntityLiving;
import noppes.npcs.api.IDamageSource;
import noppes.npcs.api.IWorld;
import noppes.npcs.api.entity.IEntityLivingBase;
import noppes.npcs.api.item.IItemStack;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.api.INbtList;
import noppes.npcs.api.INbt;
import kamkeel.npcs.controllers.AbilityController;
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyBarrierData;
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyDisplayData;
import noppes.npcs.controllers.data.MagicData;
import java.util.List;
/**
* Abstract base for energy barrier abilities (Dome, Wall, Shield).
* Handles shared visual data, barrier IConfiguration, and lifecycle management.
*/
public abstract class AbilityBarrier extends AbilityEnergy {
protected final EnergyBarrierData barrierData;
// Spawn position offsets (relative to caster)
protected float offsetX = 0.0f;
protected float offsetY = 0.0f;
protected float offsetZ = 0.0f;
// Runtime IEntity tracking
protected transient EntityEnergyBarrier barrierEntity;
protected AbilityBarrier(EnergyDisplayData displayData, EnergyBarrierData barrierData) {
super(displayData);
this.barrierData = barrierData;
}
// ==================== ABSTRACT METHODS ====================
/**
* Create and spawn the barrier IEntity during execution.
*/
protected abstract EntityEnergyBarrier createBarrierEntity(IEntityLivingBase caster, IEntityLivingBase target);
/**
* Add type-specific GUI field definitions.
*/
@ClientOnly
protected abstract void addBarrierTypeDefinitions(List<FieldDef> defs);
/**
* Write type-specific fields to NBT.
*/
protected abstract void writeBarrierTypeNBT(INbt nbt);
/**
* Read type-specific fields from NBT.
*/
protected abstract void readBarrierTypeNBT(INbt nbt);
// ==================== LIFECYCLE ====================
@Override
public boolean allowBurst() {
return false;
}
@Override
public boolean hasDamage() {
return false; // Barriers are defensive
}
@Override
public boolean hasMagic() {
return true; // Barriers use magic for defense interactions
}
@Override
public boolean allowFreeOnCast() {
return true;
}
@Override
public void detach() {
barrierEntity = null;
}
@Override
public void onWindUpTick(IEntityLivingBase caster, IEntityLivingBase target, int tick) {
if (caster.worldObj.isRemote) return;
if (tick == 1) {
barrierEntity = createBarrierEntity(caster, target);
if (barrierEntity != null) {
inheritMagicData(barrierEntity, caster);
applyBarrierHealthModifiers(caster);
barrierEntity.setupCharging(getWindUpTicks());
spawnAbilityEntity(barrierEntity);
}
}
}
@Override
public void onExecute(IEntityLivingBase caster, IEntityLivingBase target) {
if (barrierEntity != null && !barrierEntity.isDead) {
// Barrier was spawned during windup — transition to active
barrierEntity.finishCharging();
} else {
// No windup or IEntity died — create fresh
barrierEntity = createBarrierEntity(caster, target);
if (barrierEntity != null) {
inheritMagicData(barrierEntity, caster);
applyBarrierHealthModifiers(caster);
spawnAbilityEntity(barrierEntity);
}
}
}
private void inheritMagicData(EntityEnergyBarrier IEntity, IEntityLivingBase caster) {
MagicData resolved = resolveMagicData(caster);
if (resolved != null) {
IEntity.setMagicData(resolved.copy());
}
}
@Override
public void onActiveTick(IEntityLivingBase caster, IEntityLivingBase target, int tick) {
if (barrierEntity == null || barrierEntity.isDead) {
signalCompletion();
return;
}
// Free on Cast: complete immediately after barrier becomes active
if (isFreeOnCast()) {
signalCompletion();
return;
}
// Duration tracked by IEntity itself, but check here too as a failsafe
if (barrierData.useDuration && tick >= barrierData.durationTicks) {
cleanup();
signalCompletion();
}
}
@Override
public void onComplete(IEntityLivingBase caster, IEntityLivingBase target) {
}
/**
* Allow extenders to modify the barrier's max health at spawn time.
*/
private void applyBarrierHealthModifiers(IEntityLivingBase caster) {
float modHealth = AbilityController.Instance.fireModifyBarrierHealth(this, caster, barrierData.maxHealth);
if (modHealth != barrierData.maxHealth) {
barrierEntity.setBarrierMaxHealth(modHealth);
}
}
@Override
public void onInterrupt(IEntityLivingBase caster, IDamageSource source, float damage) {
cleanup();
}
@Override
public void cleanup() {
if (barrierEntity != null && !barrierEntity.isDead) {
barrierEntity.setDead();
}
barrierEntity = null;
}
// ==================== NBT ====================
@Override
public final void writeTypeNBT(INbt nbt) {
writeBarrierTypeNBT(nbt);
writeEnergyNBT(nbt);
barrierData.writeNBT(new NBTWrapper(nbt));
nbt.setFloat("barrierOffsetX", offsetX);
nbt.setFloat("barrierOffsetY", offsetY);
nbt.setFloat("barrierOffsetZ", offsetZ);
}
@Override
public final void readTypeNBT(INbt nbt) {
offsetX = nbt.hasKey("barrierOffsetX") ? nbt.getFloat("barrierOffsetX") : 0.0f;
offsetY = nbt.hasKey("barrierOffsetY") ? nbt.getFloat("barrierOffsetY") : 0.0f;
offsetZ = nbt.hasKey("barrierOffsetZ") ? nbt.getFloat("barrierOffsetZ") : 0.0f;
readBarrierTypeNBT(nbt);
readEnergyNBT(nbt);
barrierData.readNBT(new NBTWrapper(nbt));
}
// ==================== API GETTERS & SETTERS ====================
// Display and lightning data inherited from AbilityEnergy
// Barrier data
public float getBarrierMaxHealth() {
return barrierData.maxHealth;
}
public void setBarrierMaxHealth(float maxHealth) {
barrierData.setMaxHealth(maxHealth);
}
public boolean isUseHealth() {
return barrierData.useHealth;
}
public void setUseHealth(boolean useHealth) {
barrierData.useHealth = useHealth;
}
@Override
public float getDisplayBarrierHealth() { return barrierData.useHealth ? barrierData.maxHealth : 0; }
@Override
public boolean isDisplayReflect() { return barrierData.reflect; }
@Override
public float getDisplayReflectStrength() { return barrierData.reflectStrengthPct; }
@Override
public boolean isDisplayAbsorbing() { return barrierData.absorbing; }
public int getBarrierDuration() {
return barrierData.durationTicks;
}
public void setBarrierDuration(int ticks) {
barrierData.setDurationTicks(ticks);
}
public boolean isUseDuration() {
return barrierData.useDuration;
}
public void setUseDuration(boolean useDuration) {
barrierData.useDuration = useDuration;
}
public float getDefaultMultiplier() {
return barrierData.defaultMultiplier;
}
public void setDefaultMultiplier(float mult) {
barrierData.defaultMultiplier = mult;
}
public void setDamageMultiplier(String typeId, float mult) {
barrierData.setMultiplier(typeId, mult);
}
public float getDamageMultiplier(String typeId) {
return barrierData.getMultiplier(typeId);
}
// Solid data
public boolean isSolid() {
return barrierData.solid;
}
public void setSolid(boolean solid) {
barrierData.solid = solid;
}
// Knockback data
public boolean isKnockbackEnabled() {
return barrierData.knockbackEnabled;
}
public void setKnockbackEnabled(boolean enabled) {
barrierData.knockbackEnabled = enabled;
}
public float getKnockbackStrength() {
return barrierData.knockbackStrength;
}
public void setKnockbackStrength(float strength) {
barrierData.knockbackStrength = strength;
}
// Absorbing data
public boolean isAbsorbing() {
return barrierData.absorbing;
}
public void setAbsorbing(boolean absorbing) {
barrierData.absorbing = absorbing;
}
public float getAbsorbRadius() {
return barrierData.absorbRadius;
}
public void setAbsorbRadius(float radius) {
barrierData.absorbRadius = radius;
}
// Reflection data
public boolean isReflect() {
return barrierData.reflect;
}
public void setReflect(boolean reflect) {
barrierData.reflect = reflect;
}
public float getReflectStrengthPct() {
return barrierData.reflectStrengthPct;
}
public void setReflectStrengthPct(float strengthPct) {
barrierData.setReflectStrengthPct(strengthPct);
}
public boolean isTargetOwner() {
return barrierData.targetOwner;
}
public void setTargetOwner(boolean targetOwner) {
barrierData.targetOwner = targetOwner;
}
// Melee data
public boolean isMeleeEnabled() {
return barrierData.meleeEnabled;
}
public void setMeleeEnabled(boolean enabled) {
barrierData.meleeEnabled = enabled;
}
public float getMeleeDamageMultiplier() {
return barrierData.meleeDamageMultiplier;
}
public void setMeleeDamageMultiplier(float mult) {
barrierData.meleeDamageMultiplier = mult;
}
// Offset data
public float getOffsetX() {
return offsetX;
}
public void setOffsetX(float offsetX) {
this.offsetX = offsetX;
}
public float getOffsetY() {
return offsetY;
}
public void setOffsetY(float offsetY) {
this.offsetY = offsetY;
}
public float getOffsetZ() {
return offsetZ;
}
public void setOffsetZ(float offsetZ) {
this.offsetZ = offsetZ;
}
// ==================== GUI ====================
@ClientOnly
@Override
public final void getAbilityDefinitions(List<FieldDef> defs) {
// Type-specific fields first
addBarrierTypeDefinitions(defs);
// Offset section
defs.add(FieldDef.section("ability.section.offset"));
defs.add(FieldDef.row(
FieldDef.floatField("ability.offsetX", this::getOffsetX, this::setOffsetX).min(Float.NEGATIVE_INFINITY),
FieldDef.floatField("ability.offsetY", this::getOffsetY, this::setOffsetY).min(Float.NEGATIVE_INFINITY)
));
defs.add(FieldDef.floatField("ability.offsetZ", this::getOffsetZ, this::setOffsetZ).min(Float.NEGATIVE_INFINITY));
// Barrier section
defs.add(FieldDef.section("ability.section.barrier"));
defs.add(FieldDef.boolField("ability.useHealth", this::isUseHealth, this::setUseHealth));
defs.add(FieldDef.floatField("ability.maxHealth", this::getBarrierMaxHealth, this::setBarrierMaxHealth)
.visibleWhen(this::isUseHealth));
defs.add(FieldDef.boolField("ability.useDuration", this::isUseDuration, this::setUseDuration));
defs.add(FieldDef.intField("ability.duration", this::getBarrierDuration, this::setBarrierDuration)
.range(1, 12000).visibleWhen(this::isUseDuration));
defs.add(FieldDef.floatField("ability.defaultMultiplier", this::getDefaultMultiplier, this::setDefaultMultiplier));
// Properties section
defs.add(FieldDef.section("ability.section.properties"));
defs.add(FieldDef.boolField("ability.solid", this::isSolid, this::setSolid)
.hover("ability.hover.solid"));
defs.add(FieldDef.boolField("ability.knockbackEnabled", this::isKnockbackEnabled, this::setKnockbackEnabled)
.hover("ability.hover.knockbackEnabled"));
defs.add(FieldDef.floatField("ability.knockbackStrength", this::getKnockbackStrength, this::setKnockbackStrength)
.range(0, 10).visibleWhen(this::isKnockbackEnabled));
defs.add(FieldDef.boolField("ability.absorbing", this::isAbsorbing, this::setAbsorbing)
.hover("ability.hover.absorbing"));
defs.add(FieldDef.floatField("ability.absorbRadius", this::getAbsorbRadius, this::setAbsorbRadius)
.range(-1, 128).visibleWhen(this::isAbsorbing)
.hover("ability.hover.absorbRadius"));
defs.add(FieldDef.boolField("ability.reflect", this::isReflect, this::setReflect)
.hover("ability.hover.reflect"));
defs.add(FieldDef.floatField("ability.reflectStrength", this::getReflectStrengthPct, this::setReflectStrengthPct)
.range(0, 100).visibleWhen(this::isReflect));
defs.add(FieldDef.boolField("ability.targetOwner", this::isTargetOwner, this::setTargetOwner)
.hover("ability.hover.targetOwner")
.visibleWhen(this::isReflect));
defs.add(FieldDef.boolField("ability.meleeEnabled", this::isMeleeEnabled, this::setMeleeEnabled)
.hover("ability.hover.meleeEnabled"));
defs.add(FieldDef.floatField("ability.meleeDamageMultiplier", this::getMeleeDamageMultiplier, this::setMeleeDamageMultiplier)
.range(0, 10).visibleWhen(this::isMeleeEnabled));
// Visual tab - colors + effects (from AbilityEnergy)
addEnergyVisualDefinitions(defs);
}
}
@@ -0,0 +1,259 @@
package kamkeel.npcs.controllers.data.ability.type.energy;
import noppes.npcs.constants.ClientOnly;
import kamkeel.npcs.util.IVector3;
import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler;
import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback;
import kamkeel.npcs.controllers.data.ability.gui.FieldDef;
import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider;
import noppes.npcs.entity.EntityNPCInterface;
import kamkeel.npcs.entity.EntityEnergyDome;
import kamkeel.npcs.entity.EntityEnergyBarrier;
import kamkeel.npcs.entity.EntityEnergyPanel;
import kamkeel.npcs.entity.EntityAbilityOrb;
import kamkeel.npcs.entity.EntityAbilityLaser;
import kamkeel.npcs.entity.EntityAbilityDisc;
import kamkeel.npcs.entity.EntityAbilityBeam;
import kamkeel.npcs.entity.EntityEnergyProjectile;
import kamkeel.npcs.util.ByteBufUtils;
import noppes.npcs.api.entity.IEntityLiving;
import noppes.npcs.api.IDamageSource;
import noppes.npcs.api.IWorld;
import noppes.npcs.api.entity.IEntityLivingBase;
import noppes.npcs.api.item.IItemStack;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.api.INbtList;
import noppes.npcs.api.INbt;
import kamkeel.npcs.controllers.data.ability.AbilityVariant;
import kamkeel.npcs.controllers.data.ability.enums.HitType;
import kamkeel.npcs.controllers.data.ability.enums.LockMode;
import kamkeel.npcs.controllers.data.ability.enums.RotationMode;
import kamkeel.npcs.controllers.data.ability.enums.TargetingMode;
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyCombatData;
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyDisplayData;
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyHomingData;
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyLifespanData;
import kamkeel.npcs.controllers.data.ability.data.ProjectileData;
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
import kamkeel.npcs.util.AnchorPointHelper;
import noppes.npcs.api.ability.type.IAbilityEnergyBeam;
import java.util.Arrays;
import java.util.List;
/**
* Energy Beam ability: A homing head with a trailing path.
* Supports 1-8 projectiles with shared visuals and per-projectile anchor points.
*/
public class AbilityBeam extends AbilityEnergyProjectile<EntityAbilityBeam> implements IAbilityEnergyBeam {
private float beamWidth = 0.4f;
private float headSize = 0.6f;
private boolean freeAim = false;
public AbilityBeam() {
super(
new EnergyDisplayData(0xFFFFFF, 0x00AAFF, true, 0.4f, 0.5f, 10.0f),
new EnergyCombatData(10.0f, 1.5f, 0.2f, false, 4.0f, 0.5f, HitType.SINGLE, 5),
new EnergyHomingData(0.4f, true, 0.1f, 15.0f),
new EnergyLifespanData(150.0f, 200)
);
this.typeId = "ability.cnpc.beam";
this.name = "Beam";
this.targetingMode = TargetingMode.AGGRO_TARGET;
this.maxRange = 20.0f;
this.minRange = 5.0f;
this.cooldownTicks = 0;
this.windUpTicks = 40;
this.lockMovement = LockMode.WINDUP_AND_ACTIVE;
this.rotationMode = RotationMode.LOCKED;
this.rotationPhase = LockMode.ACTIVE;
this.telegraphType = TelegraphType.CIRCLE;
this.showTelegraph = true;
this.windUpAnimationName = "Ability_Beam_Windup";
this.activeAnimationName = "Ability_Beam_Active";
this.defaultIconLayers = new DefaultIconLayer[]{
new DefaultIconLayer("customnpcs:textures/gui/ability/beam.png",
() -> isOuterColorEnabled() ? getOuterColor() : getInnerColor())
};
}
// ==================== ABSTRACT IMPLEMENTATIONS ====================
@Override
protected EntityAbilityBeam createEntity(IEntityLivingBase caster, IEntityLivingBase target,
IVector3 spawnPos, EnergyDisplayData resolved, int index) {
EntityAbilityBeam beam = new EntityAbilityBeam(
caster.worldObj, caster, target,
spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord,
beamWidth, headSize,
resolved, combatData, homingData, lightningData, lifespanData,
lockMovement.locksActive());
beam.setFreeAim(freeAim);
return beam;
}
@Override
protected void fireEntity(EntityAbilityBeam beam, IEntityLivingBase target) {
if (isPreview()) {
beam.startPreviewFiring();
} else {
beam.startFiring(target);
}
}
@Override
protected void setupEntityCharging(EntityAbilityBeam beam, ProjectileData projData, int index) {
beam.setupCharging(projData.anchor, windUpTicks, 1.0f);
}
@Override
protected void setupEntityPreview(EntityAbilityBeam beam, IEntityLivingBase caster,
EnergyDisplayData resolved, ProjectileData projData, int index) {
beam.setupPreview(caster, beamWidth, headSize, resolved, lightningData, projData.anchor, windUpTicks, 1.0f);
}
@Override
protected EntityAbilityBeam[] createEntityArray(int size) {
return new EntityAbilityBeam[size];
}
@Override
public void detach() {
if (entities != null) {
for (EntityAbilityBeam beam : entities) {
if (beam != null && !beam.isDead) {
beam.setAttachedToOwner(false);
}
}
}
super.detach();
}
@Override
protected float getProjectileTelegraphRadius() {
return headSize * 2.0f;
}
@Override
protected IVector3 getSpawnPosition(IEntityLivingBase caster, int index) {
return AnchorPointHelper.calculateAnchorPosition(caster, projectiles[index].anchor, 1.0f);
}
// ==================== VARIANTS ====================
@Override
public List<AbilityVariant> getVariants() {
return Arrays.asList(
new AbilityVariant("ability.variant.single", a -> {
a.setName("Beam");
}),
new AbilityVariant("ability.variant.dual", a -> {
AbilityBeam beam = (AbilityBeam) a;
a.setName("Dual Beam");
beam.setProjectileCount(2);
beam.setFireDelay(0);
beam.projectiles[1].colorOverride = true;
beam.projectiles[1].innerColor = 0xFFFFFF;
beam.projectiles[1].outerColor = 0xFF0000;
a.setWindUpTicks(80);
a.setWindUpAnimationName("Ability_BeamDual_Windup");
a.setActiveAnimationName("Ability_BeamDual_Active");
})
);
}
// ==================== TYPE-SPECIFIC NBT ====================
@Override
protected void writeTypeSpecificNBT(INbt nbt) {
nbt.setFloat("beamWidth", beamWidth);
nbt.setFloat("headSize", headSize);
nbt.setBoolean("freeAim", freeAim);
}
@Override
protected void readTypeSpecificNBT(INbt nbt) {
this.beamWidth = nbt.getFloat("beamWidth");
this.headSize = nbt.getFloat("headSize");
this.freeAim = nbt.getBoolean("freeAim");
}
// ==================== TYPE-SPECIFIC GETTERS ====================
public float getSpeed() {
return homingData.speed;
}
public void setSpeed(float speed) {
homingData.speed = speed;
}
public float getBeamWidth() {
return beamWidth;
}
public void setBeamWidth(float beamWidth) {
this.beamWidth = beamWidth;
}
public float getHeadSize() {
return headSize;
}
public void setHeadSize(float headSize) {
this.headSize = headSize;
}
public boolean isFreeAim() {
return freeAim;
}
public void setFreeAim(boolean freeAim) {
this.freeAim = freeAim;
}
// ==================== TYPE-SPECIFIC GUI ====================
@ClientOnly
@Override
protected void addTypeDefinitions(List<FieldDef> defs) {
defs.add(FieldDef.row(
FieldDef.intField("ability.projectileCount", this::getProjectileCount, this::setProjectileCount).range(1, MAX_PROJECTILES),
FieldDef.intField("ability.fireDelay", this::getFireDelay, this::setFireDelay)
.range(0, 200).visibleWhen(() -> projectileCount > 1)
));
defs.add(FieldDef.row(
FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage),
FieldDef.floatField("stats.speed", this::getSpeed, this::setSpeed)
));
defs.add(FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback));
defs.add(FieldDef.section("ability.section.beam"));
defs.add(FieldDef.boolField("ability.freeAim", this::isFreeAim, this::setFreeAim)
.hover("ability.hover.freeAim")
.enabledWhen(() -> lockMovement.locksActive() && rotationMode == RotationMode.FREE && !freeOnCast));
defs.add(FieldDef.row(
FieldDef.floatField("ability.beamWidth", this::getBeamWidth, this::setBeamWidth).range(0.1f, 100.0f),
FieldDef.floatField("ability.headSize", this::getHeadSize, this::setHeadSize).range(0.1f, 100.0f)
));
defs.add(FieldDef.row(
FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance).range(1.0f, 500.0f),
FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime).range(1, 1200)
));
defs.add(FieldDef.section("ability.section.homing"));
defs.add(FieldDef.boolField("gui.enabled", this::isHoming, this::setHoming).hover("ability.hover.homing"));
defs.add(FieldDef.row(
FieldDef.floatField("gui.strength", this::getHomingStrength, this::setHomingStrength).visibleWhen(this::isHoming),
FieldDef.floatField("gui.range", this::getHomingRange, this::setHomingRange).visibleWhen(this::isHoming)
));
defs.add(FieldDef.section("ability.section.explosive"));
defs.add(FieldDef.boolField("gui.enabled", this::isExplosive, this::setExplosive).hover("ability.hover.explosive"));
defs.add(FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius)
.range(0.0f, EnergyCombatData.MAX_EXPLOSION_RADIUS)
.visibleWhen(this::isExplosive));
defs.add(AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects));
}
}
@@ -0,0 +1,258 @@
package kamkeel.npcs.controllers.data.ability.type.energy;
import noppes.npcs.constants.ClientOnly;
import kamkeel.npcs.util.IVector3;
import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler;
import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback;
import kamkeel.npcs.controllers.data.ability.gui.FieldDef;
import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider;
import noppes.npcs.entity.EntityNPCInterface;
import kamkeel.npcs.entity.EntityEnergyDome;
import kamkeel.npcs.entity.EntityEnergyBarrier;
import kamkeel.npcs.entity.EntityEnergyPanel;
import kamkeel.npcs.entity.EntityAbilityOrb;
import kamkeel.npcs.entity.EntityAbilityLaser;
import kamkeel.npcs.entity.EntityAbilityDisc;
import kamkeel.npcs.entity.EntityAbilityBeam;
import kamkeel.npcs.entity.EntityEnergyProjectile;
import kamkeel.npcs.util.ByteBufUtils;
import noppes.npcs.api.entity.IEntityLiving;
import noppes.npcs.api.IDamageSource;
import noppes.npcs.api.IWorld;
import noppes.npcs.api.entity.IEntityLivingBase;
import noppes.npcs.api.item.IItemStack;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.api.INbtList;
import noppes.npcs.api.INbt;
import kamkeel.npcs.controllers.data.ability.AbilityVariant;
import kamkeel.npcs.controllers.data.ability.enums.HitType;
import kamkeel.npcs.controllers.data.ability.enums.LockMode;
import kamkeel.npcs.controllers.data.ability.enums.TargetingMode;
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyCombatData;
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyDisplayData;
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyHomingData;
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyLifespanData;
import kamkeel.npcs.controllers.data.ability.data.ProjectileData;
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
import noppes.npcs.api.ability.type.IAbilityDisc;
import java.util.Arrays;
import java.util.List;
/**
* Disc ability: Spawns a flat spinning disc projectile.
* Supports 1-8 projectiles with shared visuals and per-projectile anchor points.
* Has optional boomerang behavior to return to owner.
*/
public class AbilityDisc extends AbilityEnergyProjectile<EntityAbilityDisc> implements IAbilityDisc {
private float discRadius = 1.0f;
private float discThickness = 0.2f;
private boolean vertical = false;
private boolean boomerang = false;
private int boomerangDelay = 40;
public AbilityDisc() {
super(
new EnergyDisplayData(0xFFFFFF, 0xFF8800, true, 0.4f, 0.5f, 10.0f),
new EnergyCombatData(8.0f, 1.2f, 0.15f, false, 3.0f, 0.5f, HitType.PIERCE, 5),
new EnergyHomingData(0.6f, true, 0.12f, 18.0f),
new EnergyLifespanData(150.0f, 200)
);
this.typeId = "ability.cnpc.disc";
this.name = "Disc";
this.targetingMode = TargetingMode.AGGRO_TARGET;
this.maxRange = 30.0f;
this.minRange = 5.0f;
this.cooldownTicks = 0;
this.windUpTicks = 60;
this.lockMovement = LockMode.WINDUP;
this.telegraphType = TelegraphType.CIRCLE;
this.showTelegraph = true;
this.windUpAnimationName = "Ability_Disc_Windup";
this.activeAnimationName = "Ability_Disc_Active";
this.defaultIconLayers = new DefaultIconLayer[]{
new DefaultIconLayer("customnpcs:textures/gui/ability/disc.png"),
new DefaultIconLayer("customnpcs:textures/gui/ability/disc_overlay.png",
() -> isOuterColorEnabled() ? getOuterColor() : getInnerColor())
};
}
// ==================== ABSTRACT IMPLEMENTATIONS ====================
@Override
protected EntityAbilityDisc createEntity(IEntityLivingBase caster, IEntityLivingBase target,
IVector3 spawnPos, EnergyDisplayData resolved, int index) {
return new EntityAbilityDisc(
caster.worldObj, caster, target,
spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord,
discRadius, discThickness,
resolved, combatData, homingData, lightningData, lifespanData,
boomerang, boomerangDelay);
}
@Override
protected void fireEntity(EntityAbilityDisc disc, IEntityLivingBase target) {
if (isPreview()) {
disc.startPreviewFiring();
} else {
disc.startMoving(target);
}
}
@Override
protected void setupEntityCharging(EntityAbilityDisc disc, ProjectileData projData, int index) {
disc.setupCharging(projData.anchor, windUpTicks, vertical);
}
@Override
protected void setupEntityPreview(EntityAbilityDisc disc, IEntityLivingBase caster,
EnergyDisplayData resolved, ProjectileData projData, int index) {
disc.setupPreview(caster, discRadius, discThickness, resolved, lightningData, projData.anchor, windUpTicks, vertical);
}
@Override
protected EntityAbilityDisc[] createEntityArray(int size) {
return new EntityAbilityDisc[size];
}
@Override
protected float getProjectileTelegraphRadius() {
return discRadius * 1.5f;
}
// ==================== VARIANTS ====================
@Override
public List<AbilityVariant> getVariants() {
return Arrays.asList(
new AbilityVariant("ability.variant.single", a -> {
a.setName("Disc");
}),
new AbilityVariant("ability.variant.dual", a -> {
AbilityDisc disc = (AbilityDisc) a;
a.setName("Dual Disc");
disc.setProjectileCount(2);
disc.setFireDelay(5);
disc.projectiles[1].colorOverride = true;
disc.projectiles[1].innerColor = 0xFFFFFF;
disc.projectiles[1].outerColor = 0x8800FF;
a.setWindUpAnimationName("Ability_DiscDual_Windup");
a.setActiveAnimationName("Ability_DiscDual_Active");
})
);
}
// ==================== TYPE-SPECIFIC NBT ====================
@Override
protected void writeTypeSpecificNBT(INbt nbt) {
nbt.setFloat("discRadius", discRadius);
nbt.setFloat("discThickness", discThickness);
nbt.setBoolean("vertical", vertical);
nbt.setBoolean("boomerang", boomerang);
nbt.setInteger("boomerangDelay", boomerangDelay);
}
@Override
protected void readTypeSpecificNBT(INbt nbt) {
this.discRadius = nbt.getFloat("discRadius");
this.discThickness = nbt.getFloat("discThickness");
this.vertical = nbt.getBoolean("vertical");
this.boomerang = nbt.getBoolean("boomerang");
this.boomerangDelay = nbt.getInteger("boomerangDelay");
}
// ==================== TYPE-SPECIFIC GETTERS ====================
public float getSpeed() {
return homingData.speed;
}
public void setSpeed(float speed) {
homingData.speed = speed;
}
public float getDiscRadius() {
return discRadius;
}
public void setDiscRadius(float discRadius) {
this.discRadius = discRadius;
}
public float getDiscThickness() {
return discThickness;
}
public void setDiscThickness(float discThickness) {
this.discThickness = discThickness;
}
public boolean isVertical() {
return vertical;
}
public void setVertical(boolean vertical) {
this.vertical = vertical;
}
public boolean isBoomerang() {
return boomerang;
}
public void setBoomerang(boolean boomerang) {
this.boomerang = boomerang;
}
public int getBoomerangDelay() {
return boomerangDelay;
}
public void setBoomerangDelay(int boomerangDelay) {
this.boomerangDelay = boomerangDelay;
}
// ==================== TYPE-SPECIFIC GUI ====================
@ClientOnly
@Override
protected void addTypeDefinitions(List<FieldDef> defs) {
defs.add(FieldDef.row(
FieldDef.intField("ability.projectileCount", this::getProjectileCount, this::setProjectileCount).range(1, MAX_PROJECTILES),
FieldDef.intField("ability.fireDelay", this::getFireDelay, this::setFireDelay)
.range(0, 200).visibleWhen(() -> projectileCount > 1)
));
defs.add(FieldDef.row(
FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage),
FieldDef.floatField("stats.speed", this::getSpeed, this::setSpeed)
));
defs.add(FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback));
defs.add(FieldDef.section("ability.section.disc"));
defs.add(FieldDef.row(
FieldDef.floatField("gui.radius", this::getDiscRadius, this::setDiscRadius).range(0.1f, 100.0f),
FieldDef.floatField("gui.thickness", this::getDiscThickness, this::setDiscThickness).range(0.05f, 100.0f)
));
defs.add(FieldDef.boolField("ability.vertical", this::isVertical, this::setVertical)
.hover("ability.hover.vertical"));
defs.add(FieldDef.row(
FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance).range(1.0f, 500.0f),
FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime).range(1, 1200)
));
defs.add(FieldDef.section("ability.section.homing"));
defs.add(FieldDef.boolField("gui.enabled", this::isHoming, this::setHoming).hover("ability.hover.homing"));
defs.add(FieldDef.floatField("gui.strength", this::getHomingStrength, this::setHomingStrength).visibleWhen(this::isHoming));
defs.add(FieldDef.section("ability.section.boomerang"));
defs.add(FieldDef.boolField("gui.enabled", this::isBoomerang, this::setBoomerang).hover("ability.hover.boomerang"));
defs.add(FieldDef.intField("gui.delay", this::getBoomerangDelay, this::setBoomerangDelay).visibleWhen(this::isBoomerang));
defs.add(FieldDef.section("ability.section.explosive"));
defs.add(FieldDef.boolField("gui.enabled", this::isExplosive, this::setExplosive).hover("ability.hover.explosive"));
defs.add(FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius)
.range(0.0f, EnergyCombatData.MAX_EXPLOSION_RADIUS)
.visibleWhen(this::isExplosive));
defs.add(AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects));
}
}
@@ -0,0 +1,164 @@
package kamkeel.npcs.controllers.data.ability.type.energy;
import noppes.npcs.constants.ClientOnly;
import kamkeel.npcs.util.IVector3;
import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler;
import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback;
import kamkeel.npcs.controllers.data.ability.gui.FieldDef;
import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider;
import noppes.npcs.entity.EntityNPCInterface;
import kamkeel.npcs.entity.EntityEnergyDome;
import kamkeel.npcs.entity.EntityEnergyBarrier;
import kamkeel.npcs.entity.EntityEnergyPanel;
import kamkeel.npcs.entity.EntityAbilityOrb;
import kamkeel.npcs.entity.EntityAbilityLaser;
import kamkeel.npcs.entity.EntityAbilityDisc;
import kamkeel.npcs.entity.EntityAbilityBeam;
import kamkeel.npcs.entity.EntityEnergyProjectile;
import kamkeel.npcs.util.ByteBufUtils;
import noppes.npcs.api.entity.IEntityLiving;
import noppes.npcs.api.IDamageSource;
import noppes.npcs.api.IWorld;
import noppes.npcs.api.entity.IEntityLivingBase;
import noppes.npcs.api.item.IItemStack;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.api.INbtList;
import noppes.npcs.api.INbt;
import kamkeel.npcs.controllers.data.ability.AbilityVariant;
import kamkeel.npcs.controllers.data.ability.enums.LockMode;
import kamkeel.npcs.controllers.data.ability.enums.TargetingMode;
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyBarrierData;
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyDisplayData;
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
import java.util.Arrays;
import java.util.List;
/**
* Energy Dome ability: Spawns a spherical barrier centered on the caster.
* Blocks incoming energy projectiles with configurable damage multipliers.
* Duration and/or HP based.
*/
public class AbilityDome extends AbilityBarrier {
private float domeRadius = 5.0f;
private boolean followCaster = false;
public AbilityDome() {
super(
new EnergyDisplayData(0x44CCFF, 0x2288FF, true, 0.3f, 0.35f, 0.5f, 0.0f),
new EnergyBarrierData(100.0f, true, 200, true)
);
this.typeId = "ability.cnpc.dome";
this.name = "Dome";
this.targetingMode = TargetingMode.SELF;
this.cooldownTicks = 100;
this.windUpTicks = 30;
this.lockMovement = LockMode.WINDUP;
this.telegraphType = TelegraphType.RING;
this.showTelegraph = true;
this.windUpAnimationName = "";
this.activeAnimationName = "";
this.defaultIconLayers = new DefaultIconLayer[]{
new DefaultIconLayer("customnpcs:textures/gui/ability/dome.png",
() -> isOuterColorEnabled() ? getOuterColor() : getInnerColor())
};
}
// ==================== ABSTRACT IMPLEMENTATIONS ====================
@Override
protected EntityEnergyBarrier createBarrierEntity(IEntityLivingBase caster, IEntityLivingBase target) {
EntityEnergyDome dome = new EntityEnergyDome(
caster.worldObj, caster,
caster.posX + offsetX, caster.posY + offsetY, caster.posZ + offsetZ,
domeRadius, displayData.copy(), lightningData.copy(), barrierData.copy()
);
dome.setFollowCaster(followCaster);
dome.setOffsets(offsetX, offsetY, offsetZ);
dome.setSourceAbility(this);
return dome;
}
@Override
public boolean isTargetingModeLocked() {
return true;
}
@Override
public TargetingMode[] getAllowedTargetingModes() {
return new TargetingMode[]{TargetingMode.SELF};
}
@Override
public float getTelegraphRadius() {
return domeRadius;
}
// ==================== VARIANTS ====================
@Override
public List<AbilityVariant> getVariants() {
return Arrays.asList(
new AbilityVariant("ability.variant.small", a -> {
AbilityDome dome = (AbilityDome) a;
a.setName("Small Dome");
dome.setDomeRadius(3.0f);
dome.setBarrierMaxHealth(60.0f);
}),
new AbilityVariant("ability.variant.large", a -> {
AbilityDome dome = (AbilityDome) a;
a.setName("Large Dome");
dome.setDomeRadius(8.0f);
dome.setBarrierMaxHealth(200.0f);
dome.setBarrierDuration(300);
})
);
}
// ==================== TYPE-SPECIFIC NBT ====================
@Override
protected void writeBarrierTypeNBT(INbt nbt) {
nbt.setFloat("domeRadius", domeRadius);
nbt.setBoolean("followCaster", followCaster);
}
@Override
protected void readBarrierTypeNBT(INbt nbt) {
this.domeRadius = nbt.hasKey("domeRadius") ? nbt.getFloat("domeRadius") : 5.0f;
this.followCaster = nbt.hasKey("followCaster") && nbt.getBoolean("followCaster");
}
// ==================== GETTERS & SETTERS ====================
public float getDomeRadius() {
return domeRadius;
}
public void setDomeRadius(float radius) {
this.domeRadius = Math.max(1.0f, radius);
}
public boolean isFollowCaster() {
return followCaster;
}
public void setFollowCaster(boolean follow) {
this.followCaster = follow;
}
// ==================== TYPE-SPECIFIC GUI ====================
@ClientOnly
@Override
protected void addBarrierTypeDefinitions(List<FieldDef> defs) {
defs.add(FieldDef.floatField("ability.domeRadius", this::getDomeRadius, this::setDomeRadius)
.range(1.0f, 50.0f));
defs.add(FieldDef.boolField("ability.followCaster", this::isFollowCaster, this::setFollowCaster)
.hover("ability.hover.followCaster"));
}
}
@@ -0,0 +1,211 @@
package kamkeel.npcs.controllers.data.ability.type.energy;
import noppes.npcs.constants.ClientOnly;
import kamkeel.npcs.util.IVector3;
import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler;
import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback;
import kamkeel.npcs.controllers.data.ability.gui.FieldDef;
import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider;
import noppes.npcs.entity.EntityNPCInterface;
import kamkeel.npcs.entity.EntityEnergyDome;
import kamkeel.npcs.entity.EntityEnergyBarrier;
import kamkeel.npcs.entity.EntityEnergyPanel;
import kamkeel.npcs.entity.EntityAbilityOrb;
import kamkeel.npcs.entity.EntityAbilityLaser;
import kamkeel.npcs.entity.EntityAbilityDisc;
import kamkeel.npcs.entity.EntityAbilityBeam;
import kamkeel.npcs.entity.EntityEnergyProjectile;
import kamkeel.npcs.util.ByteBufUtils;
import noppes.npcs.api.entity.IEntityLiving;
import noppes.npcs.api.IDamageSource;
import noppes.npcs.api.IWorld;
import noppes.npcs.api.entity.IEntityLivingBase;
import noppes.npcs.api.item.IItemStack;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.api.INbtList;
import noppes.npcs.api.INbt;
import kamkeel.npcs.controllers.data.ability.Ability;
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyDisplayData;
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyLightningData;
import java.util.List;
/**
* Abstract base for all energy-based abilities (Projectile, Barrier, Sweeper, etc.).
* Provides shared visual data (colors, lightning) and corresponding API/GUI/NBT.
*/
public abstract class AbilityEnergy extends Ability {
protected EnergyDisplayData displayData;
protected EnergyLightningData lightningData;
protected AbilityEnergy(EnergyDisplayData displayData) {
this.displayData = displayData;
this.lightningData = new EnergyLightningData();
}
// ==================== DISPLAY DATA ====================
public int getInnerColor() {
return displayData.innerColor;
}
public void setInnerColor(int color) {
displayData.innerColor = color;
}
public float getInnerAlpha() {
return displayData.innerAlpha;
}
public void setInnerAlpha(float alpha) {
displayData.innerAlpha = alpha;
}
public int getOuterColor() {
return displayData.outerColor;
}
public void setOuterColor(int color) {
displayData.outerColor = color;
}
public boolean isOuterColorEnabled() {
return displayData.outerColorEnabled;
}
public void setOuterColorEnabled(boolean enabled) {
displayData.outerColorEnabled = enabled;
}
public float getOuterColorWidth() {
return displayData.outerColorWidth;
}
public void setOuterColorWidth(float width) {
displayData.outerColorWidth = width;
}
public float getOuterColorAlpha() {
return displayData.outerColorAlpha;
}
public void setOuterColorAlpha(float alpha) {
displayData.outerColorAlpha = alpha;
}
public float getRotationSpeed() {
return displayData.rotationSpeed;
}
public void setRotationSpeed(float speed) {
displayData.rotationSpeed = speed;
}
// ==================== LIGHTNING DATA ====================
public boolean hasLightningEffect() {
return lightningData.lightningEffect;
}
public void setLightningEffect(boolean enabled) {
lightningData.lightningEffect = enabled;
}
public float getLightningDensity() {
return lightningData.lightningDensity;
}
public void setLightningDensity(float density) {
lightningData.lightningDensity = density;
}
public float getLightningRadius() {
return lightningData.lightningRadius;
}
public void setLightningRadius(float radius) {
lightningData.lightningRadius = radius;
}
public int getLightningFadeTime() {
return lightningData.lightningFadeTime;
}
public void setLightningFadeTime(int fadeTime) {
lightningData.lightningFadeTime = fadeTime;
}
// ==================== NBT HELPERS ====================
protected void writeEnergyNBT(INbt nbt) {
displayData.writeNBT(new NBTWrapper(nbt));
lightningData.writeNBT(new NBTWrapper(nbt));
}
protected void readEnergyNBT(INbt nbt) {
displayData.readNBT(new NBTWrapper(nbt));
lightningData.readNBT(new NBTWrapper(nbt));
}
// ==================== GUI HELPERS ====================
/**
* Add complete visual tab definitions (colors section + effects/lightning section).
* Subclasses can call this directly or use the individual methods for more control.
*/
@ClientOnly
protected void addEnergyVisualDefinitions(List<FieldDef> defs) {
addEnergyColorDefinitions(defs);
addEnergyEffectDefinitions(defs);
}
/**
* Add the colors section to the visual tab.
*/
@ClientOnly
protected void addEnergyColorDefinitions(List<FieldDef> defs) {
defs.add(FieldDef.section("ability.section.colors").tab("ability.tab.visual"));
defs.add(FieldDef.row(
FieldDef.colorSubGui("ability.innerColor", this::getInnerColor, this::setInnerColor),
FieldDef.floatField("ability.innerAlpha", this::getInnerAlpha, this::setInnerAlpha).range(0, 1)
).tab("ability.tab.visual"));
defs.add(FieldDef.boolField("ability.outerEnabled", this::isOuterColorEnabled, this::setOuterColorEnabled)
.tab("ability.tab.visual"));
defs.add(FieldDef.colorSubGui("ability.outerColor", this::getOuterColor, this::setOuterColor)
.tab("ability.tab.visual").visibleWhen(this::isOuterColorEnabled));
defs.add(FieldDef.row(
FieldDef.floatField("ability.outerWidth", this::getOuterColorWidth, this::setOuterColorWidth)
.visibleWhen(this::isOuterColorEnabled),
FieldDef.floatField("ability.outerAlpha", this::getOuterColorAlpha, this::setOuterColorAlpha)
.range(0, 1).visibleWhen(this::isOuterColorEnabled)
).tab("ability.tab.visual"));
}
/**
* Add the effects section (lightning) to the visual tab.
* Override to insert additional fields before lightning (e.g., rotationSpeed).
*/
@ClientOnly
protected void addEnergyEffectDefinitions(List<FieldDef> defs) {
defs.add(FieldDef.section("ability.section.effects").tab("ability.tab.visual"));
addEnergyLightningDefinitions(defs);
}
/**
* Add just the lightning fields (no section header) to the visual tab.
*/
@ClientOnly
protected void addEnergyLightningDefinitions(List<FieldDef> defs) {
defs.add(FieldDef.boolField("ability.lightning", this::hasLightningEffect, this::setLightningEffect)
.tab("ability.tab.visual"));
defs.add(FieldDef.row(
FieldDef.floatField("gui.density", this::getLightningDensity, this::setLightningDensity)
.visibleWhen(this::hasLightningEffect).range(0.01f, 100f),
FieldDef.floatField("gui.radius", this::getLightningRadius, this::setLightningRadius)
.range(0.1f, 100f).visibleWhen(this::hasLightningEffect)
).tab("ability.tab.visual"));
}
}
@@ -0,0 +1,946 @@
package kamkeel.npcs.controllers.data.ability.type.energy;
import noppes.npcs.constants.ClientOnly;
import kamkeel.npcs.util.IVector3;
import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler;
import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback;
import kamkeel.npcs.controllers.data.ability.gui.FieldDef;
import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider;
import noppes.npcs.entity.EntityNPCInterface;
import kamkeel.npcs.entity.EntityEnergyDome;
import kamkeel.npcs.entity.EntityEnergyBarrier;
import kamkeel.npcs.entity.EntityEnergyPanel;
import kamkeel.npcs.entity.EntityAbilityOrb;
import kamkeel.npcs.entity.EntityAbilityLaser;
import kamkeel.npcs.entity.EntityAbilityDisc;
import kamkeel.npcs.entity.EntityAbilityBeam;
import kamkeel.npcs.entity.EntityEnergyProjectile;
import kamkeel.npcs.util.ByteBufUtils;
import noppes.npcs.api.entity.IEntityLiving;
import noppes.npcs.api.IDamageSource;
import noppes.npcs.api.IWorld;
import noppes.npcs.api.entity.IEntityLivingBase;
import noppes.npcs.api.item.IItemStack;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.api.INbtList;
import noppes.npcs.api.INbt;
import kamkeel.npcs.controllers.data.ability.enums.AnchorPoint;
import kamkeel.npcs.controllers.data.ability.enums.HitType;
import kamkeel.npcs.controllers.data.ability.enums.TargetingMode;
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyCombatData;
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyDisplayData;
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyHomingData;
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyLifespanData;
import kamkeel.npcs.controllers.data.ability.data.ProjectileData;
import kamkeel.npcs.controllers.data.energycharge.EnergyChargeTracker;
import kamkeel.npcs.controllers.data.telegraph.Telegraph;
import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance;
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
import kamkeel.npcs.util.AnchorPointHelper;
import noppes.npcs.api.ability.type.IAbilityEnergyProjectile;
import noppes.npcs.controllers.data.MagicData;
import java.util.List;
/**
* Abstract base for energy projectile abilities (Orb, Disc, Beam, LaserShot).
* Handles shared lifecycle (windup → execute → active → completion), multi-projectile
* support with staggered firing, telegraph creation, NBT persistence, and GUI definitions.
* <p>
* Subclasses provide IEntity creation/firing/charging logic and type-specific fields.
*
* @param <E> The IEntity type this ability spawns
*/
public abstract class AbilityEnergyProjectile<E extends EntityEnergyProjectile> extends AbilityEnergy implements IAbilityEnergyProjectile {
protected static final int MAX_PROJECTILES = 8;
// Multi-projectile support
protected int projectileCount = 1;
protected int fireDelay = 0;
// Shared combat/movement data
protected final EnergyCombatData combatData;
protected final EnergyHomingData homingData;
protected final EnergyLifespanData lifespanData;
// Per-projectile data (anchor + optional color override)
protected ProjectileData[] projectiles;
// Transient IEntity state
protected transient E[] entities;
protected transient boolean[] projectileSpawned;
protected transient int spawnedCount;
protected transient String[] chargeVisualIds;
protected transient IEntityLivingBase chargeVisualCaster;
// ==================== CONSTRUCTOR ====================
protected AbilityEnergyProjectile(
EnergyDisplayData displayData,
EnergyCombatData combatData,
EnergyHomingData homingData,
EnergyLifespanData lifespanData
) {
super(displayData);
this.combatData = combatData;
this.homingData = homingData;
this.lifespanData = lifespanData;
this.burstOverlap = true;
initProjectiles(1);
}
// ==================== ABSTRACT METHODS ====================
/**
* Create a single IEntity for the given projectile index.
* Called when a projectile actually fires.
*/
protected abstract E createEntity(IEntityLivingBase caster, IEntityLivingBase target,
IVector3 spawnPos, EnergyDisplayData resolved, int index);
/**
* Fire an IEntity (start moving toward target). IEntity is guaranteed non-null and alive.
*/
protected abstract void fireEntity(E IEntity, IEntityLivingBase target);
/**
* Setup IEntity for charging animation during windup (non-preview).
*/
protected abstract void setupEntityCharging(E IEntity, ProjectileData projData, int index);
/**
* Setup IEntity for preview mode during windup.
*/
protected abstract void setupEntityPreview(E IEntity, IEntityLivingBase caster,
EnergyDisplayData resolved, ProjectileData projData, int index);
/**
* Create typed array (Java generics can't create generic arrays).
*/
protected abstract E[] createEntityArray(int size);
/**
* Get the telegraph radius for this projectile type.
*/
protected abstract float getProjectileTelegraphRadius();
/**
* Add type-specific GUI field definitions (Type tab).
* Called before shared visual tab definitions.
*/
@ClientOnly
protected abstract void addTypeDefinitions(List<FieldDef> defs);
/**
* Write type-specific fields to NBT (e.g., orbSize, discRadius).
* Shared data classes are written by the base class.
*/
protected abstract void writeTypeSpecificNBT(INbt nbt);
/**
* Read type-specific fields from NBT.
* Shared data classes are read by the base class.
*/
protected abstract void readTypeSpecificNBT(INbt nbt);
// ==================== OVERRIDABLE HOOKS ====================
/**
* Get the default anchor point for a projectile index.
* Override for types with different anchor defaults.
*/
protected AnchorPoint getDefaultAnchor(int index) {
switch (index) {
case 0:
return AnchorPoint.RIGHT_HAND;
case 1:
return AnchorPoint.LEFT_HAND;
default:
return AnchorPoint.FRONT;
}
}
/**
* Get the spawn position for a projectile. Override for types that need
* an offset distance (e.g., Beam uses offsetDist=1.0).
*/
protected IVector3 getSpawnPosition(IEntityLivingBase caster, int index) {
return AnchorPointHelper.calculateAnchorPosition(caster, projectiles[index].anchor);
}
// ==================== PROJECTILE MANAGEMENT ====================
protected void initProjectiles(int count) {
this.projectileCount = Math.max(1, Math.min(count, MAX_PROJECTILES));
this.projectiles = new ProjectileData[projectileCount];
for (int i = 0; i < projectileCount; i++) {
projectiles[i] = new ProjectileData(getDefaultAnchor(i));
}
}
public void setProjectileCount(int count) {
int newCount = Math.max(1, Math.min(count, MAX_PROJECTILES));
if (newCount == projectileCount) return;
ProjectileData[] oldProjectiles = projectiles;
initProjectiles(newCount);
if (oldProjectiles != null) {
int copyCount = Math.min(oldProjectiles.length, newCount);
for (int i = 0; i < copyCount; i++) {
if (oldProjectiles[i] != null) projectiles[i] = oldProjectiles[i];
}
}
}
public int getProjectileCount() {
return projectileCount;
}
public int getFireDelay() {
return fireDelay;
}
public void setFireDelay(int delay) {
this.fireDelay = Math.max(0, delay);
}
protected int clampIndex(int index) {
return Math.max(0, Math.min(index, projectileCount - 1));
}
// ==================== IEntity CREATION ====================
protected void initRuntimeState(IEntityLivingBase caster) {
entities = createEntityArray(projectileCount);
projectileSpawned = new boolean[projectileCount];
spawnedCount = 0;
chargeVisualIds = new String[projectileCount];
chargeVisualCaster = caster;
}
protected E createProjectileEntity(IEntityLivingBase caster, IEntityLivingBase target, int index) {
IVector3 spawnPos = getSpawnPosition(caster, index);
EnergyDisplayData resolved = projectiles[index].resolveDisplay(displayData);
E IEntity = createEntity(caster, target, spawnPos, resolved, index);
IEntity.setAnchorData(projectiles[index].anchor);
IEntity.setEffects(this.effects);
IEntity.setIgnoreIFrames(this.isIgnoreIFrames());
IEntity.setSourceAbility(this);
MagicData resolvedMagic = resolveMagicData(caster);
if (resolvedMagic != null) {
IEntity.setMagicData(resolvedMagic.copy());
}
return IEntity;
}
protected void spawnProjectileEntity(E IEntity, int index) {
if (IEntity == null) return;
spawnAbilityEntity(IEntity);
entities[index] = IEntity;
if (!projectileSpawned[index]) {
projectileSpawned[index] = true;
spawnedCount++;
}
}
protected void spawnAndFireProjectile(IEntityLivingBase caster, IEntityLivingBase target, int index) {
if (entities != null && entities[index] != null && !entities[index].isDead) return;
E IEntity = createProjectileEntity(caster, target, index);
// Initialize launch position/motion BEFORE spawn so the
// initial spawn packet already has centered look-vector data on all clients.
fireEntitySafe(IEntity, caster, target);
spawnProjectileEntity(IEntity, index);
}
/**
* Hook for launch target selection.
* Players launch along their look vector (crosshair-aligned), so target is ignored.
* NPCs use their resolved target so launch math can track the target immediately.
*/
protected IEntityLivingBase getLaunchTarget(IEntityLivingBase caster, IEntityLivingBase target) {
return isPlayerCaster(caster) ? null : target;
}
/**
* Fire an IEntity if it's valid.
* Clears charging state first so the IEntity can move and check collisions.
*/
protected void fireEntitySafe(E IEntity, IEntityLivingBase caster, IEntityLivingBase target) {
if (IEntity != null && !IEntity.isDead) {
if (IEntity.isCharging()) {
IEntity.setCharging(false);
}
fireEntity(IEntity, getLaunchTarget(caster, target));
}
}
protected String getChargeVisualId(IEntityLivingBase caster, int index) {
return "energy_charge:" + caster.getEntityId() + ":" + executionStartTime + ":" + burstIndex + ":" + index;
}
protected void spawnChargeVisual(IEntityLivingBase caster, IEntityLivingBase target, int index, int chargeDuration) {
if (isPreview() || caster == null || caster.worldObj == null || caster.worldObj.isRemote) return;
if (chargeVisualIds == null || index < 0 || index >= chargeVisualIds.length) return;
E previewEntity = createProjectileEntity(caster, target, index);
previewEntity.setPreviewMode(true);
previewEntity.setPreviewOwner(caster);
setupEntityCharging(previewEntity, projectiles[index], index);
previewEntity.setChargeDuration(Math.max(1, chargeDuration));
String id = getChargeVisualId(caster, index);
chargeVisualIds[index] = id;
chargeVisualCaster = caster;
EnergyChargeSpawnPacket.sendToTracking(id, previewEntity, caster);
// Register with server-side tracker for late-join support
EnergyChargeTracker.Instance.add(new EnergyChargeTracker.ChargeEntry(
id,
previewEntity.getClass().getName(),
previewEntity.exportSpawnNBT(),
caster.getEntityId(),
(int) caster.worldObj.getTotalWorldTime()
));
}
protected void removeChargeVisual(IEntityLivingBase caster, int index) {
if (isPreview() || caster == null || caster.worldObj == null || caster.worldObj.isRemote) return;
if (chargeVisualIds == null || index < 0 || index >= chargeVisualIds.length) return;
String id = chargeVisualIds[index];
if (id != null && !id.isEmpty()) {
EnergyChargeRemovePacket.sendToTracking(id, caster);
EnergyChargeTracker.Instance.remove(id, caster.getEntityId());
chargeVisualIds[index] = null;
}
}
protected void removeAllChargeVisuals(IEntityLivingBase caster) {
if (chargeVisualIds == null || caster == null) return;
for (int i = 0; i < chargeVisualIds.length; i++) {
removeChargeVisual(caster, i);
}
// Safety net: ensure tracker is fully cleaned for this caster
if (!isPreview() && caster.worldObj != null && !caster.worldObj.isRemote) {
EnergyChargeTracker.Instance.removeAllForCaster(caster.getEntityId());
}
}
// ==================== LIFECYCLE ====================
@Override
public boolean allowOverlap() {
return true;
}
@Override
public boolean allowFreeOnCast() {
return true;
}
@Override
public void detach() {
entities = null;
projectileSpawned = null;
spawnedCount = 0;
}
/**
* Remove a specific IEntity from this ability's tracking.
* Used when a projectile is reflected — it no longer belongs to this ability,
* so the ability should stop waiting for it and free the caster.
*/
public void detachEntity(IEntity IEntity) {
if (entities == null || IEntity == null) return;
for (int i = 0; i < entities.length; i++) {
if (entities[i] == IEntity) {
entities[i] = null;
break;
}
}
}
@Override
public boolean isReadyForBurstCompletion(int activeTick) {
return fireDelay <= 0 || activeTick >= fireDelay * (projectileCount - 1);
}
@Override
public boolean isTargetingModeLocked() {
return true;
}
@Override
public TargetingMode[] getAllowedTargetingModes() {
return new TargetingMode[]{TargetingMode.AGGRO_TARGET};
}
@Override
public void onWindUpTick(IEntityLivingBase caster, IEntityLivingBase target, int tick) {
if (caster.worldObj.isRemote && !isPreview()) return;
if (tick == 1) {
initRuntimeState(caster);
if (isPreview()) {
for (int i = 0; i < projectileCount; i++) {
E previewEntity = createProjectileEntity(caster, target, i);
EnergyDisplayData resolved = projectiles[i].resolveDisplay(displayData);
setupEntityPreview(previewEntity, caster, resolved, projectiles[i], i);
spawnAbilityEntity(previewEntity);
entities[i] = previewEntity;
projectileSpawned[i] = true;
spawnedCount++;
}
return;
}
for (int i = 0; i < projectileCount; i++) {
spawnChargeVisual(caster, target, i, windUpTicks);
}
}
}
@Override
public void resetForBurst() {
removeAllChargeVisuals(chargeVisualCaster);
// Non-overlap: kill old entities before next burst creates new ones
if (!burstOverlap) {
cleanup();
}
// Clear reference so the next iteration creates fresh entities.
// For overlap mode, old entities stay alive (tracked in burstEntities).
entities = null;
projectileSpawned = null;
spawnedCount = 0;
}
@Override
public void onExecute(IEntityLivingBase caster, IEntityLivingBase target) {
// GUI preview keeps the original non-IWorld preview IEntity flow.
if (isPreview()) {
if (entities == null) {
initRuntimeState(caster);
for (int i = 0; i < projectileCount; i++) {
E previewEntity = createProjectileEntity(caster, target, i);
EnergyDisplayData resolved = projectiles[i].resolveDisplay(displayData);
setupEntityPreview(previewEntity, caster, resolved, projectiles[i], i);
spawnAbilityEntity(previewEntity);
entities[i] = previewEntity;
projectileSpawned[i] = true;
spawnedCount++;
}
}
fireEntitySafe(entities[0], caster, target);
if (fireDelay <= 0) {
for (int i = 1; i < projectileCount; i++) {
fireEntitySafe(entities[i], caster, target);
}
}
return;
}
if (entities == null || projectileSpawned == null || entities.length != projectileCount) {
initRuntimeState(caster);
}
removeAllChargeVisuals(caster);
// Fire first projectile immediately.
spawnAndFireProjectile(caster, target, 0);
// Fire remaining projectiles immediately if there is no stagger delay.
if (fireDelay <= 0) {
for (int i = 1; i < projectileCount; i++) {
spawnAndFireProjectile(caster, target, i);
}
} else {
// During active stagger, keep client-only charging visuals for unfired projectiles.
for (int i = 1; i < projectileCount; i++) {
spawnChargeVisual(caster, target, i, fireDelay * i);
}
}
}
@Override
public void onActiveTick(IEntityLivingBase caster, IEntityLivingBase target, int tick) {
if (entities == null || projectileSpawned == null) {
signalCompletion();
return;
}
// Fire staggered projectiles: spawn real IWorld IEntity only when it is time to fire.
if (fireDelay > 0) {
for (int i = 1; i < projectileCount; i++) {
if (tick == fireDelay * i) {
removeChargeVisual(caster, i);
spawnAndFireProjectile(caster, target, i);
}
}
}
// Free on Cast: complete once all projectiles have been fired
if (isFreeOnCast()) {
int lastFireTick = fireDelay > 0 ? fireDelay * (projectileCount - 1) : 0;
if (tick >= lastFireTick) {
signalCompletion();
return;
}
}
// Before all projectiles have fired, the ability is still active by definition.
boolean allDead = spawnedCount >= projectileCount;
if (allDead) {
for (E IEntity : entities) {
if (IEntity == null || IEntity.isDead) continue;
if (!isPreview() && tick > 5 && IEntity.worldObj != null
&& IEntity.worldObj.getEntityByID(IEntity.getEntityId()) != IEntity) {
IEntity.setDead();
continue;
}
allDead = false;
break;
}
}
// Failsafe timeout: force completion if active phase exceeded max possible flight time
if (!allDead) {
int lastFireTick = fireDelay > 0 ? fireDelay * (projectileCount - 1) : 0;
int maxFlightTime = lifespanData.maxLifetime > 0 ? lifespanData.maxLifetime : 200;
if (tick > lastFireTick + maxFlightTime + 20) {
cleanup();
allDead = true;
}
}
if (allDead) {
signalCompletion();
}
}
@Override
public void onComplete(IEntityLivingBase caster, IEntityLivingBase target) {
removeAllChargeVisuals(caster);
}
@Override
public void onInterrupt(IEntityLivingBase caster, IDamageSource source, float damage) {
removeAllChargeVisuals(caster);
cleanup();
}
@Override
public void cleanup() {
removeAllChargeVisuals(chargeVisualCaster);
if (entities != null) {
for (int i = 0; i < entities.length; i++) {
if (entities[i] != null && !entities[i].isDead) {
entities[i].setDead();
}
entities[i] = null;
}
}
entities = null;
projectileSpawned = null;
spawnedCount = 0;
chargeVisualIds = null;
chargeVisualCaster = null;
}
// ==================== TELEGRAPH ====================
@Override
public TelegraphInstance createTelegraph(IEntityLivingBase caster, IEntityLivingBase target) {
if (!showTelegraph || telegraphType == TelegraphType.NONE || isPlayerCaster(caster) || target == null) {
return null;
}
Telegraph telegraph = Telegraph.circle(getProjectileTelegraphRadius());
telegraph.setDurationTicks(windUpTicks);
telegraph.setColor(windUpColor);
telegraph.setWarningColor(activeColor);
telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4));
telegraph.setHeightOffset(telegraphHeightOffset);
double groundY = findGroundLevel(caster.worldObj, target.posX, target.posY, target.posZ);
TelegraphInstance instance = new TelegraphInstance(telegraph, target.posX, groundY, target.posZ, caster.rotationYaw);
instance.setCasterEntityId(caster.getEntityId());
instance.setEntityIdToFollow(target.getEntityId());
return instance;
}
@Override
public float getTelegraphRadius() {
return getProjectileTelegraphRadius();
}
// ==================== NBT ====================
@Override
public final void writeTypeNBT(INbt nbt) {
// Type-specific fields
writeTypeSpecificNBT(nbt);
// Shared fields
nbt.setInteger("projectileCount", projectileCount);
nbt.setInteger("fireDelay", fireDelay);
// Shared data classes
writeEnergyNBT(nbt);
combatData.writeNBT(new NBTWrapper(nbt));
homingData.writeNBT(new NBTWrapper(nbt));
lifespanData.writeNBT(new NBTWrapper(nbt));
// Per-projectile data
for (int i = 0; i < projectileCount; i++) {
INbt projNbt = new INbt();
projectiles[i].writeNBT(projNbt);
nbt.setTag("Projectile_" + i, projNbt);
}
}
@Override
public final void readTypeNBT(INbt nbt) {
// Type-specific fields
readTypeSpecificNBT(nbt);
// Shared fields
int count = nbt.getInteger("projectileCount");
initProjectiles(count);
this.fireDelay = nbt.getInteger("fireDelay");
// Shared data classes
readEnergyNBT(nbt);
combatData.readNBT(new NBTWrapper(nbt));
homingData.readNBT(new NBTWrapper(nbt));
lifespanData.readNBT(new NBTWrapper(nbt));
// Per-projectile data
for (int i = 0; i < projectileCount; i++) {
if (nbt.hasKey("Projectile_" + i)) {
projectiles[i].readNBT(nbt.getCompoundTag("Projectile_" + i));
}
}
}
// ==================== PREVIEW ====================
@Override
public int getMaxPreviewDuration() {
return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 100) : 100;
}
// ==================== API GETTERS & SETTERS ====================
// Combat data
@Override
public float getDamage() {
return combatData.damage;
}
@Override
public void setDamage(float damage) {
combatData.damage = damage;
}
@Override
public float getDisplayDamage() { return combatData.damage; }
@Override
public float getKnockback() {
return combatData.knockback;
}
@Override
public void setKnockback(float knockback) {
combatData.knockback = knockback;
}
@Override
public float getKnockbackUp() {
return combatData.knockbackUp;
}
@Override
public void setKnockbackUp(float knockbackUp) {
combatData.knockbackUp = knockbackUp;
}
@Override
public boolean isExplosive() {
return combatData.explosive;
}
@Override
public void setExplosive(boolean explosive) {
combatData.explosive = explosive;
}
@Override
public float getExplosionRadius() {
return combatData.getExplosionRadius();
}
@Override
public void setExplosionRadius(float explosionRadius) {
combatData.setExplosionRadius(explosionRadius);
}
@Override
public float getExplosionDamageFalloff() {
return combatData.explosionDamageFalloff;
}
@Override
public void setExplosionDamageFalloff(float falloff) {
combatData.explosionDamageFalloff = falloff;
}
public int getHitType() {
return combatData.hitType.ordinal();
}
public void setHitType(int hitType) {
this.combatData.hitType = HitType.fromOrdinal(hitType);
}
public int getMultiHitDelayTicks() {
return combatData.multiHitDelayTicks;
}
public void setMultiHitDelayTicks(int delay) {
this.combatData.multiHitDelayTicks = Math.max(1, delay);
}
public int getMaxHits() {
return combatData.getMaxHits();
}
public void setMaxHits(int maxHits) {
combatData.setMaxHits(maxHits);
}
// Lifespan data
@Override
public float getMaxDistance() {
return lifespanData.maxDistance;
}
@Override
public void setMaxDistance(float maxDistance) {
lifespanData.maxDistance = maxDistance;
}
@Override
public int getMaxLifetime() {
return lifespanData.maxLifetime;
}
@Override
public void setMaxLifetime(int maxLifetime) {
lifespanData.maxLifetime = maxLifetime;
}
// Homing data
public boolean isHoming() {
return homingData.homing;
}
public void setHoming(boolean homing) {
homingData.homing = homing;
}
public float getHomingStrength() {
return homingData.homingStrength;
}
public void setHomingStrength(float strength) {
homingData.homingStrength = strength;
}
public float getHomingRange() {
return homingData.homingRange;
}
public void setHomingRange(float range) {
homingData.homingRange = range;
}
// Display and lightning data inherited from AbilityEnergy
// Anchor - default to projectile 0
public AnchorPoint getAnchorPointEnum() {
return projectiles[0].anchor.anchorPoint;
}
public void setAnchorPointEnum(AnchorPoint point) {
projectiles[0].anchor.anchorPoint = point;
}
public float getAnchorOffsetX() {
return projectiles[0].anchor.anchorOffsetX;
}
public void setAnchorOffsetX(float x) {
projectiles[0].anchor.anchorOffsetX = x;
}
public float getAnchorOffsetY() {
return projectiles[0].anchor.anchorOffsetY;
}
public void setAnchorOffsetY(float y) {
projectiles[0].anchor.anchorOffsetY = y;
}
public float getAnchorOffsetZ() {
return projectiles[0].anchor.anchorOffsetZ;
}
public void setAnchorOffsetZ(float z) {
projectiles[0].anchor.anchorOffsetZ = z;
}
public boolean getLaunchFromAnchor() {
return projectiles[0].anchor.launchFromAnchor;
}
public void setLaunchFromAnchor(boolean launchFromAnchor) {
projectiles[0].anchor.launchFromAnchor = launchFromAnchor;
}
@Override
public int getAnchorPoint() {
return projectiles[0].anchor.anchorPoint.ordinal();
}
@Override
public void setAnchorPoint(int point) {
projectiles[0].anchor.anchorPoint = AnchorPoint.fromOrdinal(point);
}
// Indexed API methods
public int getInnerColor(int index) {
ProjectileData p = projectiles[clampIndex(index)];
return p.colorOverride ? p.innerColor : displayData.innerColor;
}
public void setInnerColor(int index, int color) {
ProjectileData p = projectiles[clampIndex(index)];
p.colorOverride = true;
p.innerColor = color;
}
public int getOuterColor(int index) {
ProjectileData p = projectiles[clampIndex(index)];
return p.colorOverride ? p.outerColor : displayData.outerColor;
}
public void setOuterColor(int index, int color) {
ProjectileData p = projectiles[clampIndex(index)];
p.colorOverride = true;
p.outerColor = color;
}
public int getAnchorPoint(int index) {
return projectiles[clampIndex(index)].anchor.anchorPoint.ordinal();
}
public void setAnchorPoint(int index, int point) {
projectiles[clampIndex(index)].anchor.anchorPoint = AnchorPoint.fromOrdinal(point);
}
public void setAnchorOffsetX(int index, float offset) {
projectiles[clampIndex(index)].anchor.anchorOffsetX = offset;
}
public void setAnchorOffsetY(int index, float offset) {
projectiles[clampIndex(index)].anchor.anchorOffsetY = offset;
}
public void setAnchorOffsetZ(int index, float offset) {
projectiles[clampIndex(index)].anchor.anchorOffsetZ = offset;
}
public boolean getLaunchFromAnchor(int index) {
return projectiles[clampIndex(index)].anchor.launchFromAnchor;
}
public void setLaunchFromAnchor(int index, boolean launchFromAnchor) {
projectiles[clampIndex(index)].anchor.launchFromAnchor = launchFromAnchor;
}
// ==================== GUI FIELD DEFINITIONS ====================
@ClientOnly
@Override
public final void getAbilityDefinitions(List<FieldDef> defs) {
defs.add(FieldDef.enumField("ability.hitType", HitType.class,
() -> HitType.fromOrdinal(getHitType()),
(v) -> setHitType(v.ordinal()))
.hover("ability.hover.hitType"));
defs.add(FieldDef.intField("ability.multiHitDelay", this::getMultiHitDelayTicks, this::setMultiHitDelayTicks)
.range(1, 200)
.visibleWhen(() -> getHitType() == HitType.MULTI.ordinal())
.hover("ability.hover.multiHitDelay"));
defs.add(FieldDef.intField("ability.maxHits", this::getMaxHits, this::setMaxHits)
.range(1, EnergyCombatData.MAX_HITS)
.visibleWhen(() -> getHitType() != HitType.SINGLE.ordinal())
.hover("ability.hover.maxHits"));
// Type-specific fields first
addTypeDefinitions(defs);
// Shared visual tab - colors + effects (from AbilityEnergy)
addEnergyColorDefinitions(defs);
// Effects section with projectile-specific rotationSpeed before lightning
defs.add(FieldDef.section("ability.section.effects").tab("ability.tab.visual"));
defs.add(FieldDef.floatField("ability.rotationSpeed", this::getRotationSpeed, this::setRotationSpeed)
.tab("ability.tab.visual"));
addEnergyLightningDefinitions(defs);
// Shared visual tab - per-projectile sections
for (int i = 0; i < MAX_PROJECTILES; i++) {
final int idx = i;
String sectionKey = "ability.section.projectile" + (i + 1);
defs.add(FieldDef.section(sectionKey).tab("ability.tab.visual")
.visibleWhen(() -> idx < projectileCount));
defs.add(FieldDef.enumField("ability.anchorPoint", AnchorPoint.class,
() -> projectiles[idx].anchor.anchorPoint, v -> projectiles[idx].anchor.anchorPoint = v)
.tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount));
defs.add(FieldDef.row(
FieldDef.floatField("ability.anchor.offsetX", () -> projectiles[idx].anchor.anchorOffsetX, v -> projectiles[idx].anchor.anchorOffsetX = v)
.min(Float.NEGATIVE_INFINITY),
FieldDef.floatField("ability.anchor.offsetY", () -> projectiles[idx].anchor.anchorOffsetY, v -> projectiles[idx].anchor.anchorOffsetY = v)
.min(Float.NEGATIVE_INFINITY)
).tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount));
defs.add(FieldDef.floatField("ability.anchor.offsetZ", () -> projectiles[idx].anchor.anchorOffsetZ, v -> projectiles[idx].anchor.anchorOffsetZ = v)
.tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount).min(Float.NEGATIVE_INFINITY));
defs.add(FieldDef.boolField("ability.launchFromAnchor", () -> projectiles[idx].anchor.launchFromAnchor, v -> projectiles[idx].anchor.launchFromAnchor = v)
.tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount)
.hover("ability.hover.launchFromAnchor"));
// Color override (only when multiple projectiles)
defs.add(FieldDef.boolField("ability.colorOverride", () -> projectiles[idx].colorOverride, v -> projectiles[idx].colorOverride = v)
.tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount && projectileCount > 1));
defs.add(FieldDef.colorSubGui("ability.innerColor", () -> projectiles[idx].innerColor, v -> projectiles[idx].innerColor = v)
.tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount && projectileCount > 1 && projectiles[idx].colorOverride));
defs.add(FieldDef.colorSubGui("ability.outerColor", () -> projectiles[idx].outerColor, v -> projectiles[idx].outerColor = v)
.tab("ability.tab.visual").visibleWhen(() -> idx < projectileCount && projectileCount > 1 && projectiles[idx].colorOverride));
}
}
}
@@ -0,0 +1,246 @@
package kamkeel.npcs.controllers.data.ability.type.energy;
import noppes.npcs.constants.ClientOnly;
import kamkeel.npcs.util.IVector3;
import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler;
import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback;
import kamkeel.npcs.controllers.data.ability.gui.FieldDef;
import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider;
import noppes.npcs.entity.EntityNPCInterface;
import kamkeel.npcs.entity.EntityEnergyDome;
import kamkeel.npcs.entity.EntityEnergyBarrier;
import kamkeel.npcs.entity.EntityEnergyPanel;
import kamkeel.npcs.entity.EntityAbilityOrb;
import kamkeel.npcs.entity.EntityAbilityLaser;
import kamkeel.npcs.entity.EntityAbilityDisc;
import kamkeel.npcs.entity.EntityAbilityBeam;
import kamkeel.npcs.entity.EntityEnergyProjectile;
import kamkeel.npcs.util.ByteBufUtils;
import noppes.npcs.api.entity.IEntityLiving;
import noppes.npcs.api.IDamageSource;
import noppes.npcs.api.IWorld;
import noppes.npcs.api.entity.IEntityLivingBase;
import noppes.npcs.api.item.IItemStack;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.api.INbtList;
import noppes.npcs.api.INbt;
import kamkeel.npcs.controllers.data.ability.enums.*;
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyCombatData;
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyDisplayData;
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyHomingData;
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyLifespanData;
import kamkeel.npcs.controllers.data.ability.data.ProjectileData;
import kamkeel.npcs.controllers.data.telegraph.Telegraph;
import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance;
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
import noppes.npcs.api.ability.type.IAbilityLaser;
import java.util.List;
/**
* Laser ability: Sweeping beam that follows the caster's look vector.
* Expands from origin to maxLength, then stays active until maxLifetime expires.
* Single-projectile only (always projectileCount=1).
*/
public class AbilityLaser extends AbilityEnergyProjectile<EntityAbilityLaser> implements IAbilityLaser {
private float laserWidth = 0.3f;
private float expansionSpeed = 3.0f;
private float maxLength = 32.0f;
public AbilityLaser() {
super(
new EnergyDisplayData(0xFFFFFF, 0xFF0000, true, 0.4f, 0.5f, 4.0f),
new EnergyCombatData(4.0f, 0.5f, 0.05f, false, 2.0f, 0.5f, HitType.MULTI, 5),
new EnergyHomingData(),
new EnergyLifespanData(150.0f, 100)
);
this.typeId = "ability.cnpc.laser_shot";
this.name = "Laser";
this.targetingMode = TargetingMode.AGGRO_TARGET;
this.maxRange = 30.0f;
this.minRange = 3.0f;
this.cooldownTicks = 0;
this.windUpTicks = 15;
this.lockMovement = LockMode.WINDUP_AND_ACTIVE;
this.rotationMode = RotationMode.TRACK;
this.rotationPhase = LockMode.WINDUP_AND_ACTIVE;
this.trackSpeed = 4.0f;
this.telegraphType = TelegraphType.LINE;
this.showTelegraph = true;
this.windUpAnimationName = "Ability_Laser_Windup";
this.activeAnimationName = "Ability_Laser_Active";
this.defaultIconLayers = new DefaultIconLayer[]{
new DefaultIconLayer("customnpcs:textures/gui/ability/laser_shot.png"),
new DefaultIconLayer("customnpcs:textures/gui/ability/laser_shot_overlay.png",
() -> isOuterColorEnabled() ? getOuterColor() : getInnerColor())
};
}
// ==================== OVERRIDES ====================
@Override
protected AnchorPoint getDefaultAnchor(int index) {
return AnchorPoint.FRONT;
}
@Override
public boolean isTargetingModeLocked() {
return false;
}
@Override
public int getMaxPreviewDuration() {
int lifetime = lifespanData.maxLifetime;
return lifetime > 0 ? Math.min(lifetime, 60) : 60;
}
/**
* LINE telegraph: positioned at caster, aimed toward target.
* Overrides the circle telegraph from AbilityEnergyProjectile.
*/
@Override
public TelegraphInstance createTelegraph(IEntityLivingBase caster, IEntityLivingBase target) {
if (!showTelegraph || telegraphType == TelegraphType.NONE || isPlayerCaster(caster)) {
return null;
}
Telegraph telegraph = Telegraph.line(getTelegraphLength(), getTelegraphWidth());
telegraph.setDurationTicks(windUpTicks);
telegraph.setColor(windUpColor);
telegraph.setWarningColor(activeColor);
telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4));
telegraph.setHeightOffset(telegraphHeightOffset);
double groundY = findGroundLevel(caster.worldObj, caster.posX, caster.posY, caster.posZ);
TelegraphInstance instance = new TelegraphInstance(telegraph, caster.posX, groundY, caster.posZ, caster.rotationYaw);
instance.setCasterEntityId(caster.getEntityId());
instance.setEntityIdToFollow(caster.getEntityId());
if (target != null) {
instance.setTargetEntityId(target.getEntityId());
}
return instance;
}
@Override
public float getTelegraphLength() {
return maxLength;
}
@Override
public float getTelegraphWidth() {
return laserWidth * 2.0f;
}
// ==================== ABSTRACT IMPLEMENTATIONS ====================
@Override
protected EntityAbilityLaser createEntity(IEntityLivingBase caster, IEntityLivingBase target,
IVector3 spawnPos, EnergyDisplayData resolved, int index) {
EntityAbilityLaser laser = new EntityAbilityLaser(
caster.worldObj, caster, target,
spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord,
laserWidth,
resolved, combatData, lightningData, lifespanData,
expansionSpeed, maxLength);
return laser;
}
@Override
protected void fireEntity(EntityAbilityLaser laser, IEntityLivingBase target) {
laser.startMoving(target);
}
@Override
protected void setupEntityCharging(EntityAbilityLaser laser, ProjectileData projData, int index) {
laser.setupCharging(projData.anchor, windUpTicks);
}
@Override
protected void setupEntityPreview(EntityAbilityLaser laser, IEntityLivingBase caster,
EnergyDisplayData resolved, ProjectileData projData, int index) {
laser.setupPreview(caster, laserWidth, resolved, lightningData, projData.anchor, windUpTicks, expansionSpeed, maxLength);
}
@Override
protected EntityAbilityLaser[] createEntityArray(int size) {
return new EntityAbilityLaser[size];
}
@Override
protected float getProjectileTelegraphRadius() {
return laserWidth * 2.0f;
}
// ==================== TYPE-SPECIFIC NBT ====================
@Override
protected void writeTypeSpecificNBT(INbt nbt) {
nbt.setFloat("laserWidth", laserWidth);
nbt.setFloat("expansionSpeed", expansionSpeed);
nbt.setFloat("maxLength", maxLength);
}
@Override
protected void readTypeSpecificNBT(INbt nbt) {
this.laserWidth = nbt.getFloat("laserWidth");
this.expansionSpeed = nbt.getFloat("expansionSpeed");
this.maxLength = nbt.hasKey("maxLength") ? nbt.getFloat("maxLength") : 32.0f;
}
// ==================== TYPE-SPECIFIC GETTERS ====================
public float getLaserWidth() {
return laserWidth;
}
public void setLaserWidth(float laserWidth) {
this.laserWidth = laserWidth;
}
public float getExpansionSpeed() {
return expansionSpeed;
}
public void setExpansionSpeed(float expansionSpeed) {
this.expansionSpeed = expansionSpeed;
}
public float getMaxLength() {
return maxLength;
}
public void setMaxLength(float maxLength) {
this.maxLength = maxLength;
}
// ==================== TYPE-SPECIFIC GUI ====================
@ClientOnly
@Override
protected void addTypeDefinitions(List<FieldDef> defs) {
defs.add(FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage));
defs.add(FieldDef.row(
FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback),
FieldDef.floatField("ability.knockbackUp", this::getKnockbackUp, this::setKnockbackUp)
));
defs.add(FieldDef.section("ability.section.beam"));
defs.add(FieldDef.row(
FieldDef.floatField("ability.laserWidth", this::getLaserWidth, this::setLaserWidth).range(0.1f, 100.0f),
FieldDef.floatField("ability.expansionSpeed", this::getExpansionSpeed, this::setExpansionSpeed).range(0.1f, 50.0f)
));
defs.add(FieldDef.row(
FieldDef.floatField("ability.maxLength", this::getMaxLength, this::setMaxLength).range(1.0f, 500.0f),
FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime).range(1, 1200)
));
defs.add(FieldDef.section("ability.section.explosive"));
defs.add(FieldDef.boolField("gui.enabled", this::isExplosive, this::setExplosive).hover("ability.hover.explosive"));
defs.add(FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius)
.range(0.0f, EnergyCombatData.MAX_EXPLOSION_RADIUS)
.visibleWhen(this::isExplosive));
defs.add(AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects));
}
}
@@ -0,0 +1,218 @@
package kamkeel.npcs.controllers.data.ability.type.energy;
import noppes.npcs.constants.ClientOnly;
import kamkeel.npcs.util.IVector3;
import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler;
import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback;
import kamkeel.npcs.controllers.data.ability.gui.FieldDef;
import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider;
import noppes.npcs.entity.EntityNPCInterface;
import kamkeel.npcs.entity.EntityEnergyDome;
import kamkeel.npcs.entity.EntityEnergyBarrier;
import kamkeel.npcs.entity.EntityEnergyPanel;
import kamkeel.npcs.entity.EntityAbilityOrb;
import kamkeel.npcs.entity.EntityAbilityLaser;
import kamkeel.npcs.entity.EntityAbilityDisc;
import kamkeel.npcs.entity.EntityAbilityBeam;
import kamkeel.npcs.entity.EntityEnergyProjectile;
import kamkeel.npcs.util.ByteBufUtils;
import noppes.npcs.api.entity.IEntityLiving;
import noppes.npcs.api.IDamageSource;
import noppes.npcs.api.IWorld;
import noppes.npcs.api.entity.IEntityLivingBase;
import noppes.npcs.api.item.IItemStack;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.api.INbtList;
import noppes.npcs.api.INbt;
import kamkeel.npcs.controllers.data.ability.AbilityVariant;
import kamkeel.npcs.controllers.data.ability.enums.LockMode;
import kamkeel.npcs.controllers.data.ability.enums.TargetingMode;
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyCombatData;
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyDisplayData;
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyHomingData;
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyLifespanData;
import kamkeel.npcs.controllers.data.ability.data.ProjectileData;
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
import noppes.npcs.api.ability.type.IAbilityOrb;
import java.util.Arrays;
import java.util.List;
/**
* Orb ability: Spawns homing projectile sphere(s) that track target.
* Supports 1-8 projectiles with shared visuals and per-projectile anchor points.
*/
public class AbilityOrb extends AbilityEnergyProjectile<EntityAbilityOrb> implements IAbilityOrb {
private float orbSize = 1.0f;
public AbilityOrb() {
super(
new EnergyDisplayData(0xFFFFFF, 0xFF0000, true, 0.4f, 0.5f, 10.0f),
new EnergyCombatData(),
new EnergyHomingData(),
new EnergyLifespanData()
);
this.typeId = "ability.cnpc.orb";
this.name = "Orb";
this.targetingMode = TargetingMode.AGGRO_TARGET;
this.maxRange = 25.0f;
this.minRange = 5.0f;
this.cooldownTicks = 0;
this.windUpTicks = 30;
this.lockMovement = LockMode.WINDUP;
this.telegraphType = TelegraphType.CIRCLE;
this.showTelegraph = true;
this.windUpAnimationName = "Ability_Orb_Windup";
this.activeAnimationName = "Ability_Orb_Active";
this.defaultIconLayers = new DefaultIconLayer[]{
new DefaultIconLayer("customnpcs:textures/gui/ability/orb.png",
() -> isOuterColorEnabled() ? getOuterColor() : getInnerColor())
};
}
// ==================== ABSTRACT IMPLEMENTATIONS ====================
@Override
protected EntityAbilityOrb createEntity(IEntityLivingBase caster, IEntityLivingBase target,
IVector3 spawnPos, EnergyDisplayData resolved, int index) {
return new EntityAbilityOrb(
caster.worldObj, caster, target,
spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord, orbSize,
resolved, combatData, homingData, lightningData, lifespanData);
}
@Override
protected void fireEntity(EntityAbilityOrb orb, IEntityLivingBase target) {
if (isPreview()) {
orb.startPreviewFiring();
} else {
orb.startMoving(target);
}
}
@Override
protected void setupEntityCharging(EntityAbilityOrb orb, ProjectileData projData, int index) {
orb.setupCharging(projData.anchor, windUpTicks);
}
@Override
protected void setupEntityPreview(EntityAbilityOrb orb, IEntityLivingBase caster,
EnergyDisplayData resolved, ProjectileData projData, int index) {
orb.setupPreview(caster, orbSize, resolved, lightningData, projData.anchor, windUpTicks);
}
@Override
protected EntityAbilityOrb[] createEntityArray(int size) {
return new EntityAbilityOrb[size];
}
@Override
protected float getProjectileTelegraphRadius() {
return orbSize * 1.5f;
}
// ==================== VARIANTS ====================
@Override
public List<AbilityVariant> getVariants() {
return Arrays.asList(
new AbilityVariant("ability.variant.single", a -> {
a.setName("Orb");
}),
new AbilityVariant("ability.variant.dual", a -> {
AbilityOrb orb = (AbilityOrb) a;
a.setName("Dual Orb");
orb.setProjectileCount(2);
orb.setFireDelay(5);
a.setWindUpAnimationName("Ability_OrbDual_Windup");
a.setActiveAnimationName("Ability_OrbDual_Active");
}),
new AbilityVariant("ability.variant.barrage", a -> {
AbilityOrb orb = (AbilityOrb) a;
a.setName("Orb Barrage");
orb.setOrbSize(0.5f);
orb.setDamage(4.0f);
orb.setKnockback(0.0f);
orb.setKnockbackUp(0.0f);
orb.setProjectileCount(2);
orb.setFireDelay(2);
a.setLockMovement(LockMode.WINDUP_AND_ACTIVE);
a.setMaxRange(75.0f);
a.setBurstEnabled(true);
a.setBurstAmount(15);
a.setBurstDelay(5);
a.setBurstReplayAnimations(false);
a.setBurstOverlap(true);
a.setWindUpAnimationName("Ability_OrbBarrage_Windup");
a.setActiveAnimationName("Ability_OrbBarrage_Active");
})
);
}
// ==================== TYPE-SPECIFIC NBT ====================
@Override
protected void writeTypeSpecificNBT(INbt nbt) {
nbt.setFloat("orbSize", orbSize);
}
@Override
protected void readTypeSpecificNBT(INbt nbt) {
this.orbSize = nbt.getFloat("orbSize");
}
// ==================== TYPE-SPECIFIC GETTERS ====================
public float getOrbSpeed() {
return homingData.speed;
}
public void setOrbSpeed(float speed) {
homingData.speed = speed;
}
public float getOrbSize() {
return orbSize;
}
public void setOrbSize(float size) {
this.orbSize = size;
}
// ==================== TYPE-SPECIFIC GUI ====================
@ClientOnly
@Override
protected void addTypeDefinitions(List<FieldDef> defs) {
defs.add(FieldDef.row(
FieldDef.intField("ability.projectileCount", this::getProjectileCount, this::setProjectileCount).range(1, MAX_PROJECTILES),
FieldDef.intField("ability.fireDelay", this::getFireDelay, this::setFireDelay)
.range(0, 200).visibleWhen(() -> projectileCount > 1)
));
defs.add(FieldDef.row(
FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage),
FieldDef.floatField("stats.speed", this::getOrbSpeed, this::setOrbSpeed)
));
defs.add(FieldDef.row(
FieldDef.floatField("stats.size", this::getOrbSize, this::setOrbSize).range(0.1f, 100.0f),
FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback)
));
defs.add(FieldDef.row(
FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance).range(1.0f, 500.0f),
FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime).range(1, 1200)
));
defs.add(FieldDef.section("ability.section.homing"));
defs.add(FieldDef.boolField("gui.enabled", this::isHoming, this::setHoming).hover("ability.hover.homing"));
defs.add(FieldDef.floatField("gui.strength", this::getHomingStrength, this::setHomingStrength).visibleWhen(this::isHoming));
defs.add(FieldDef.section("ability.section.explosive"));
defs.add(FieldDef.boolField("gui.enabled", this::isExplosive, this::setExplosive).hover("ability.hover.explosive"));
defs.add(FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius)
.range(0.0f, EnergyCombatData.MAX_EXPLOSION_RADIUS)
.visibleWhen(this::isExplosive));
defs.add(AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects));
}
}
@@ -0,0 +1,178 @@
package kamkeel.npcs.controllers.data.ability.type.energy;
import noppes.npcs.constants.ClientOnly;
import kamkeel.npcs.util.IVector3;
import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler;
import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback;
import kamkeel.npcs.controllers.data.ability.gui.FieldDef;
import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider;
import noppes.npcs.entity.EntityNPCInterface;
import kamkeel.npcs.entity.EntityEnergyDome;
import kamkeel.npcs.entity.EntityEnergyBarrier;
import kamkeel.npcs.entity.EntityEnergyPanel;
import kamkeel.npcs.entity.EntityAbilityOrb;
import kamkeel.npcs.entity.EntityAbilityLaser;
import kamkeel.npcs.entity.EntityAbilityDisc;
import kamkeel.npcs.entity.EntityAbilityBeam;
import kamkeel.npcs.entity.EntityEnergyProjectile;
import kamkeel.npcs.util.ByteBufUtils;
import noppes.npcs.api.entity.IEntityLiving;
import noppes.npcs.api.IDamageSource;
import noppes.npcs.api.IWorld;
import noppes.npcs.api.entity.IEntityLivingBase;
import noppes.npcs.api.item.IItemStack;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.api.INbtList;
import noppes.npcs.api.INbt;
import kamkeel.npcs.controllers.data.ability.AbilityVariant;
import kamkeel.npcs.controllers.data.ability.enums.LockMode;
import kamkeel.npcs.controllers.data.ability.enums.TargetingMode;
import kamkeel.npcs.controllers.data.ability.enums.UserType;
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyBarrierData;
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyDisplayData;
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyPanelData;
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
import java.util.Arrays;
import java.util.List;
/**
* Energy Shield ability: Spawns a flat energy panel held in front of the caster.
* The shield follows the caster's position and rotates with their look direction.
* Cannot be placed — it's always held.
*/
public class AbilityShield extends AbilityBarrier {
private float shieldWidth = 2.5f;
private float shieldHeight = 2.5f;
public AbilityShield() {
super(
new EnergyDisplayData(0xFFDD44, 0xFFAA00, true, 0.3f, 0.4f, 0.5f, 0.0f),
new EnergyBarrierData(60.0f, true, 120, true)
);
this.typeId = "ability.cnpc.shield";
this.name = "Shield";
this.targetingMode = TargetingMode.SELF;
this.cooldownTicks = 60;
this.windUpTicks = 10;
this.lockMovement = LockMode.WINDUP_AND_ACTIVE;
this.telegraphType = TelegraphType.NONE;
this.showTelegraph = false;
this.windUpAnimationName = "";
this.activeAnimationName = "";
this.allowedBy = UserType.BOTH;
this.defaultIconLayers = new DefaultIconLayer[]{
new DefaultIconLayer("customnpcs:textures/gui/ability/shield.png",
() -> isOuterColorEnabled() ? getOuterColor() : getInnerColor())
};
}
// ==================== ABSTRACT IMPLEMENTATIONS ====================
@Override
protected EntityEnergyBarrier createBarrierEntity(IEntityLivingBase caster, IEntityLivingBase target) {
EnergyPanelData panelData = new EnergyPanelData(shieldWidth, shieldHeight, offsetY);
panelData.offsetX = offsetX;
panelData.offsetZ = offsetZ;
float frontDist = 1.5f;
float yawRad = (float) Math.toRadians(caster.rotationYaw);
double spawnX = caster.posX + (-Math.sin(yawRad) * frontDist) + offsetX;
double spawnY = caster.posY + (caster.height * 0.5f);
double spawnZ = caster.posZ + (Math.cos(yawRad) * frontDist) + offsetZ;
EntityEnergyPanel panel = new EntityEnergyPanel(
caster.worldObj, caster,
spawnX, spawnY, spawnZ, caster.rotationYaw,
EntityEnergyPanel.PanelMode.HELD,
displayData.copy(), lightningData.copy(), barrierData.copy(), panelData
);
panel.setSourceAbility(this);
return panel;
}
@Override
public boolean isTargetingModeLocked() {
return true;
}
@Override
public TargetingMode[] getAllowedTargetingModes() {
return new TargetingMode[]{TargetingMode.SELF};
}
@Override
public float getTelegraphRadius() {
return 0; // No telegraph for shield
}
// ==================== VARIANTS ====================
@Override
public List<AbilityVariant> getVariants() {
return Arrays.asList(
new AbilityVariant("ability.variant.small", a -> {
AbilityShield shield = (AbilityShield) a;
a.setName("Small Shield");
shield.setShieldWidth(1.5f);
shield.setShieldHeight(1.5f);
shield.setBarrierMaxHealth(40.0f);
}),
new AbilityVariant("ability.variant.large", a -> {
AbilityShield shield = (AbilityShield) a;
a.setName("Large Shield");
shield.setShieldWidth(4.0f);
shield.setShieldHeight(4.0f);
shield.setBarrierMaxHealth(120.0f);
shield.setBarrierDuration(200);
})
);
}
// ==================== TYPE-SPECIFIC NBT ====================
@Override
protected void writeBarrierTypeNBT(INbt nbt) {
nbt.setFloat("shieldWidth", shieldWidth);
nbt.setFloat("shieldHeight", shieldHeight);
}
@Override
protected void readBarrierTypeNBT(INbt nbt) {
this.shieldWidth = nbt.hasKey("shieldWidth") ? nbt.getFloat("shieldWidth") : 2.5f;
this.shieldHeight = nbt.hasKey("shieldHeight") ? nbt.getFloat("shieldHeight") : 2.5f;
}
// ==================== GETTERS & SETTERS ====================
public float getShieldWidth() {
return shieldWidth;
}
public void setShieldWidth(float width) {
this.shieldWidth = Math.max(0.5f, width);
}
public float getShieldHeight() {
return shieldHeight;
}
public void setShieldHeight(float height) {
this.shieldHeight = Math.max(0.5f, height);
}
// ==================== TYPE-SPECIFIC GUI ====================
@ClientOnly
@Override
protected void addBarrierTypeDefinitions(List<FieldDef> defs) {
defs.add(FieldDef.row(
FieldDef.floatField("gui.width", this::getShieldWidth, this::setShieldWidth).range(0.5f, 100.0f),
FieldDef.floatField("gui.height", this::getShieldHeight, this::setShieldHeight).range(0.5f, 100.0f)
));
}
}
@@ -0,0 +1,207 @@
package kamkeel.npcs.controllers.data.ability.type.energy;
import noppes.npcs.constants.ClientOnly;
import kamkeel.npcs.util.IVector3;
import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler;
import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback;
import kamkeel.npcs.controllers.data.ability.gui.FieldDef;
import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider;
import noppes.npcs.entity.EntityNPCInterface;
import kamkeel.npcs.entity.EntityEnergyDome;
import kamkeel.npcs.entity.EntityEnergyBarrier;
import kamkeel.npcs.entity.EntityEnergyPanel;
import kamkeel.npcs.entity.EntityAbilityOrb;
import kamkeel.npcs.entity.EntityAbilityLaser;
import kamkeel.npcs.entity.EntityAbilityDisc;
import kamkeel.npcs.entity.EntityAbilityBeam;
import kamkeel.npcs.entity.EntityEnergyProjectile;
import kamkeel.npcs.entity.EntityEnergySlicer;
import kamkeel.npcs.util.ByteBufUtils;
import noppes.npcs.api.entity.IEntityLiving;
import noppes.npcs.api.IDamageSource;
import noppes.npcs.api.IWorld;
import noppes.npcs.api.entity.IEntityLivingBase;
import noppes.npcs.api.item.IItemStack;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.api.INbtList;
import noppes.npcs.api.INbt;
import kamkeel.npcs.controllers.data.ability.AbilityVariant;
import kamkeel.npcs.controllers.data.ability.enums.LockMode;
import kamkeel.npcs.controllers.data.ability.enums.TargetingMode;
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyCombatData;
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyDisplayData;
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyHomingData;
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyLifespanData;
import kamkeel.npcs.controllers.data.ability.data.ProjectileData;
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
import java.util.Arrays;
import java.util.List;
/**
* Energy Slicer ability: Fires a thin, wide blade projectile in a straight line.
* JJK Cleave/Dismantle inspired.
* Extends AbilityEnergyProjectile to leverage the existing projectile infrastructure.
*/
public class AbilitySlicer extends AbilityEnergyProjectile<EntityEnergySlicer> {
private float sliceWidth = 3.0f;
private float sliceThickness = 0.15f;
public AbilitySlicer() {
super(
new EnergyDisplayData(0xFF4444, 0xFF0000, true, 0.2f, 0.6f, 0.0f),
new EnergyCombatData(12.0f, 0.5f, 0.0f, false, 3.0f, 0.5f),
new EnergyHomingData(0.8f, false, 0.0f, 0.0f),
new EnergyLifespanData(150.0f, 100)
);
this.typeId = "ability.cnpc.slicer";
this.name = "Slicer";
this.targetingMode = TargetingMode.AGGRO_TARGET;
this.maxRange = 30.0f;
this.minRange = 3.0f;
this.cooldownTicks = 40;
this.windUpTicks = 15;
this.lockMovement = LockMode.WINDUP;
this.telegraphType = TelegraphType.LINE;
this.showTelegraph = true;
this.windUpAnimationName = "";
this.activeAnimationName = "";
}
// ==================== ABSTRACT IMPLEMENTATIONS ====================
@Override
protected EntityEnergySlicer createEntity(IEntityLivingBase caster, IEntityLivingBase target,
IVector3 spawnPos, EnergyDisplayData resolved, int index) {
return new EntityEnergySlicer(
caster.worldObj, caster, target,
spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord,
sliceWidth, sliceThickness,
resolved, combatData, homingData, lightningData, lifespanData
);
}
@Override
protected void fireEntity(EntityEnergySlicer slicer, IEntityLivingBase target) {
if (isPreview()) {
slicer.startPreviewFiring();
} else {
slicer.startMoving(target);
}
}
@Override
protected void setupEntityCharging(EntityEnergySlicer slicer, ProjectileData projData, int index) {
slicer.setupSlicerCharging(projData.anchor, windUpTicks, sliceWidth, sliceThickness);
}
@Override
protected void setupEntityPreview(EntityEnergySlicer slicer, IEntityLivingBase caster,
EnergyDisplayData resolved, ProjectileData projData, int index) {
slicer.setupPreview(caster, sliceWidth, sliceThickness, resolved, lightningData, projData.anchor, windUpTicks);
}
@Override
protected EntityEnergySlicer[] createEntityArray(int size) {
return new EntityEnergySlicer[size];
}
@Override
protected float getProjectileTelegraphRadius() {
return sliceWidth * 0.75f;
}
// ==================== VARIANTS ====================
@Override
public List<AbilityVariant> getVariants() {
return Arrays.asList(
new AbilityVariant("ability.variant.single", a -> {
a.setName("Slicer");
}),
new AbilityVariant("ability.variant.dual", a -> {
AbilitySlicer slicer = (AbilitySlicer) a;
a.setName("Double Slicer");
slicer.setProjectileCount(2);
slicer.setFireDelay(5);
a.setWindUpAnimationName("");
a.setActiveAnimationName("");
})
);
}
// ==================== TYPE-SPECIFIC NBT ====================
@Override
protected void writeTypeSpecificNBT(INbt nbt) {
nbt.setFloat("sliceWidth", sliceWidth);
nbt.setFloat("sliceThickness", sliceThickness);
}
@Override
protected void readTypeSpecificNBT(INbt nbt) {
this.sliceWidth = nbt.hasKey("sliceWidth") ? nbt.getFloat("sliceWidth") : 3.0f;
this.sliceThickness = nbt.hasKey("sliceThickness") ? nbt.getFloat("sliceThickness") : 0.15f;
}
// ==================== TYPE-SPECIFIC GETTERS ====================
public float getSlicerSpeed() {
return homingData.speed;
}
public void setSlicerSpeed(float speed) {
homingData.speed = speed;
}
public float getSliceWidth() {
return sliceWidth;
}
public void setSliceWidth(float width) {
this.sliceWidth = Math.max(0.5f, width);
}
public float getSliceThickness() {
return sliceThickness;
}
public void setSliceThickness(float thickness) {
this.sliceThickness = Math.max(0.05f, thickness);
}
// ==================== TYPE-SPECIFIC GUI ====================
@ClientOnly
@Override
protected void addTypeDefinitions(List<FieldDef> defs) {
defs.add(FieldDef.row(
FieldDef.intField("ability.projectileCount", this::getProjectileCount, this::setProjectileCount).range(1, MAX_PROJECTILES),
FieldDef.intField("ability.fireDelay", this::getFireDelay, this::setFireDelay)
.range(0, 200).visibleWhen(() -> projectileCount > 1)
));
defs.add(FieldDef.row(
FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage),
FieldDef.floatField("stats.speed", this::getSlicerSpeed, this::setSlicerSpeed)
));
defs.add(FieldDef.floatField("ability.knockback", this::getKnockback, this::setKnockback));
defs.add(FieldDef.section("ability.section.slicer"));
defs.add(FieldDef.row(
FieldDef.floatField("gui.width", this::getSliceWidth, this::setSliceWidth).range(0.5f, 100.0f),
FieldDef.floatField("gui.thickness", this::getSliceThickness, this::setSliceThickness).range(0.05f, 100.0f)
));
defs.add(FieldDef.row(
FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance).range(1.0f, 500.0f),
FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime).range(1, 1200)
));
defs.add(FieldDef.section("ability.section.explosive"));
defs.add(FieldDef.boolField("gui.enabled", this::isExplosive, this::setExplosive).hover("ability.hover.explosive"));
defs.add(FieldDef.floatField("gui.radius", this::getExplosionRadius, this::setExplosionRadius)
.range(0.0f, EnergyCombatData.MAX_EXPLOSION_RADIUS)
.visibleWhen(this::isExplosive));
defs.add(AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects));
}
}
@@ -0,0 +1,336 @@
package kamkeel.npcs.controllers.data.ability.type.energy;
import noppes.npcs.constants.ClientOnly;
import kamkeel.npcs.util.IVector3;
import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler;
import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback;
import kamkeel.npcs.controllers.data.ability.gui.FieldDef;
import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider;
import noppes.npcs.entity.EntityNPCInterface;
import kamkeel.npcs.entity.EntityEnergyDome;
import kamkeel.npcs.entity.EntityEnergyBarrier;
import kamkeel.npcs.entity.EntityEnergyPanel;
import kamkeel.npcs.entity.EntityAbilityOrb;
import kamkeel.npcs.entity.EntityAbilityLaser;
import kamkeel.npcs.entity.EntityAbilityDisc;
import kamkeel.npcs.entity.EntityAbilityBeam;
import kamkeel.npcs.entity.EntityEnergyProjectile;
import kamkeel.npcs.entity.EntityEnergySweeper;
import kamkeel.npcs.util.ByteBufUtils;
import noppes.npcs.api.entity.IEntityLiving;
import noppes.npcs.api.IDamageSource;
import noppes.npcs.api.IWorld;
import noppes.npcs.api.entity.IEntityLivingBase;
import noppes.npcs.api.item.IItemStack;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.api.INbtList;
import noppes.npcs.api.INbt;
import kamkeel.npcs.controllers.data.ability.enums.LockMode;
import kamkeel.npcs.controllers.data.ability.enums.TargetingMode;
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyDisplayData;
import kamkeel.npcs.controllers.data.telegraph.Telegraph;
import kamkeel.npcs.controllers.data.telegraph.TelegraphInstance;
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
import noppes.npcs.api.ability.type.IAbilitySweeper;
import java.util.Arrays;
import java.util.List;
/**
* Sweeper ability: A low sweeping beam that rotates around the NPC.
* Players can jump over it. The beam completes a set number of rotations before ending.
* Telegraph shows a large circle on the ground indicating range.
* <p>
* All damage logic is handled by the EntityEnergySweeper IEntity to ensure
* visual and damage hitbox alignment.
*/
public class AbilitySweeper extends AbilityEnergy implements IAbilitySweeper {
// Type-specific parameters
private float beamLength = 10.0f;
private float beamWidth = 0.3f; // Thin like beam trail
private float beamHeight = 1.0f; // Beam vertical size in blocks (1 block tall, jumpable)
private float damage = 5.0f;
private int damageInterval = 5;
private boolean piercing = true;
private float sweepSpeed = 3.0f;
private int numberOfRotations = 2; // How many full rotations before ability ends
private boolean lockOnTarget = false;
// Runtime state (transient)
private transient EntityEnergySweeper activeEntity = null;
public AbilitySweeper() {
super(new EnergyDisplayData(0xFF6600, 0xFF0000, true, 1.8f, 0.5f, 0.0f));
this.typeId = "ability.cnpc.sweeper";
this.name = "Sweeper";
this.targetingMode = TargetingMode.AGGRO_TARGET;
this.maxRange = 15.0f;
this.minRange = 0.0f;
this.lockMovement = LockMode.WINDUP_AND_ACTIVE;
this.cooldownTicks = 0;
this.windUpTicks = 60;
// Circle telegraph to show range
this.telegraphType = TelegraphType.CIRCLE;
this.showTelegraph = true;
this.defaultIconLayers = new DefaultIconLayer[]{
new DefaultIconLayer("customnpcs:textures/gui/ability/sweeper.png"),
new DefaultIconLayer("customnpcs:textures/gui/ability/sweeper_overlay.png",
() -> isOuterColorEnabled() ? getOuterColor() : getInnerColor())
};
}
@Override
public boolean allowBurst() {
return false;
}
@Override
public boolean isConcurrentCapable() {
return true;
}
@Override
public boolean allowFreeOnCast() {
return true;
}
@Override
public void detach() {
activeEntity = null;
}
@Override
public boolean isTargetingModeLocked() {
return true;
}
@Override
public TargetingMode[] getAllowedTargetingModes() {
return new TargetingMode[]{TargetingMode.AGGRO_TARGET};
}
@Override
public void onExecute(IEntityLivingBase caster, IEntityLivingBase target) {
// Spawn the sweeper IEntity that handles BOTH visuals AND damage.
// NPC: target is the aggro target — lockOnTarget tracks it during sweep.
// Player: target is null — sweep rotates around caster's facing direction, lockOnTarget has no effect.
activeEntity = new EntityEnergySweeper(caster.worldObj, caster, target,
beamLength, beamWidth, beamHeight,
displayData,
sweepSpeed, numberOfRotations,
damage, damageInterval, piercing,
lockOnTarget);
activeEntity.setIgnoreIFrames(this.isIgnoreIFrames());
activeEntity.setSourceAbility(this);
if (isPreview()) {
activeEntity.setupPreview(caster);
}
spawnAbilityEntity(activeEntity);
}
@Override
public void onActiveTick(IEntityLivingBase caster, IEntityLivingBase target, int tick) {
// Signal completion when IEntity dies
if (activeEntity == null || activeEntity.isDead) {
activeEntity = null;
signalCompletion();
return;
}
// Free on Cast: complete immediately — sweeper lives independently
if (isFreeOnCast()) {
signalCompletion();
}
}
@Override
public void onInterrupt(IEntityLivingBase caster, noppes.npcs.api.IDamageSource source, float damage) {
cleanup();
}
@Override
public void cleanup() {
// Clean up IEntity if still alive
if (activeEntity != null && !activeEntity.isDead) {
activeEntity.setDead();
}
activeEntity = null;
}
@Override
public float getTelegraphRadius() {
return beamLength;
}
/**
* Override to position telegraph at NPC (not target) since the sweep is centered on NPC.
*/
@Override
public TelegraphInstance createTelegraph(IEntityLivingBase caster, IEntityLivingBase target) {
if (!showTelegraph || telegraphType == TelegraphType.NONE) {
return null;
}
// Create circle telegraph centered on caster
Telegraph telegraph = Telegraph.circle(beamLength);
telegraph.setDurationTicks(windUpTicks);
telegraph.setColor(windUpColor);
telegraph.setWarningColor(activeColor);
telegraph.setWarningStartTick(Math.max(5, windUpTicks / 4));
telegraph.setHeightOffset(telegraphHeightOffset);
// Position at caster ground level, not target
double groundY = findGroundLevel(caster.worldObj, caster.posX, caster.posY, caster.posZ);
TelegraphInstance instance = new TelegraphInstance(telegraph, caster.posX, groundY, caster.posZ, caster.rotationYaw);
instance.setCasterEntityId(caster.getEntityId());
instance.setEntityIdToFollow(caster.getEntityId()); // Follow caster
return instance;
}
@Override
public void writeTypeNBT(INbt nbt) {
nbt.setFloat("beamLength", beamLength);
nbt.setFloat("beamWidth", beamWidth);
nbt.setFloat("beamHeight", beamHeight);
nbt.setFloat("damage", damage);
nbt.setInteger("damageInterval", damageInterval);
nbt.setBoolean("piercing", piercing);
nbt.setFloat("sweepSpeed", sweepSpeed);
nbt.setInteger("numberOfRotations", numberOfRotations);
nbt.setBoolean("lockOnTarget", lockOnTarget);
writeEnergyNBT(nbt);
}
@Override
public void readTypeNBT(INbt nbt) {
this.beamLength = nbt.getFloat("beamLength");
this.beamWidth = nbt.getFloat("beamWidth");
this.beamHeight = nbt.getFloat("beamHeight");
this.damage = nbt.getFloat("damage");
this.damageInterval = nbt.getInteger("damageInterval");
this.piercing = nbt.getBoolean("piercing");
this.sweepSpeed = nbt.getFloat("sweepSpeed");
this.numberOfRotations = nbt.getInteger("numberOfRotations");
this.lockOnTarget = nbt.getBoolean("lockOnTarget");
readEnergyNBT(nbt);
}
// Getters & Setters (type-specific only; color/lightning inherited from AbilityEnergy)
public float getBeamLength() {
return beamLength;
}
public void setBeamLength(float beamLength) {
this.beamLength = beamLength;
}
public float getBeamWidth() {
return beamWidth;
}
public void setBeamWidth(float beamWidth) {
this.beamWidth = beamWidth;
}
public float getBeamHeight() {
return beamHeight;
}
public void setBeamHeight(float beamHeight) {
this.beamHeight = beamHeight;
}
public float getDamage() {
return damage;
}
public void setDamage(float damage) {
this.damage = damage;
}
@Override
public float getDisplayDamage() { return damage; }
public int getDamageInterval() {
return damageInterval;
}
public void setDamageInterval(int damageInterval) {
this.damageInterval = damageInterval;
}
public boolean isPiercing() {
return piercing;
}
public void setPiercing(boolean piercing) {
this.piercing = piercing;
}
public float getSweepSpeed() {
return sweepSpeed;
}
public void setSweepSpeed(float sweepSpeed) {
this.sweepSpeed = sweepSpeed;
}
public int getNumberOfRotations() {
return numberOfRotations;
}
public void setNumberOfRotations(int numberOfRotations) {
this.numberOfRotations = numberOfRotations;
}
public boolean isLockOnTarget() {
return lockOnTarget;
}
public void setLockOnTarget(boolean lockOnTarget) {
this.lockOnTarget = lockOnTarget;
}
@Override
public int getMaxPreviewDuration() {
return (int) ((360.0f * numberOfRotations) / sweepSpeed) + 10;
}
@ClientOnly
@Override
public void getAbilityDefinitions(List<FieldDef> defs) {
defs.addAll(Arrays.asList(
FieldDef.row(
FieldDef.floatField("enchantment.damage", this::getDamage, this::setDamage),
FieldDef.intField("ability.damageInterval", this::getDamageInterval, this::setDamageInterval)
),
FieldDef.section("ability.section.beam"),
FieldDef.row(
FieldDef.floatField("gui.length", this::getBeamLength, this::setBeamLength).range(0.5f, 100.0f),
FieldDef.floatField("gui.width", this::getBeamWidth, this::setBeamWidth).range(0.1f, 100.0f)
),
FieldDef.row(
FieldDef.floatField("gui.height", this::getBeamHeight, this::setBeamHeight).range(0.1f, 100.0f),
FieldDef.floatField("ability.sweepSpeed", this::getSweepSpeed, this::setSweepSpeed).range(0.1f, 30.0f)
),
FieldDef.intField("ability.rotations", this::getNumberOfRotations, this::setNumberOfRotations).range(1, 20),
FieldDef.row(
FieldDef.boolField("ability.piercing", this::isPiercing, this::setPiercing)
.hover("ability.hover.piercing"),
FieldDef.boolField("ability.lockTarget", this::isLockOnTarget, this::setLockOnTarget)
.hover("ability.hover.lockTarget")
),
AbilityFieldDefs.effectsListField("ability.effects", this::getEffects, this::setEffects)
));
// Visual tab - colors + effects (from AbilityEnergy)
addEnergyVisualDefinitions(defs);
}
}
@@ -0,0 +1,255 @@
package kamkeel.npcs.controllers.data.ability.type.energy;
import noppes.npcs.constants.ClientOnly;
import kamkeel.npcs.util.IVector3;
import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler;
import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback;
import kamkeel.npcs.controllers.data.ability.gui.FieldDef;
import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider;
import noppes.npcs.entity.EntityNPCInterface;
import kamkeel.npcs.entity.EntityEnergyDome;
import kamkeel.npcs.entity.EntityEnergyBarrier;
import kamkeel.npcs.entity.EntityEnergyPanel;
import kamkeel.npcs.entity.EntityAbilityOrb;
import kamkeel.npcs.entity.EntityAbilityLaser;
import kamkeel.npcs.entity.EntityAbilityDisc;
import kamkeel.npcs.entity.EntityAbilityBeam;
import kamkeel.npcs.entity.EntityEnergyProjectile;
import kamkeel.npcs.util.ByteBufUtils;
import noppes.npcs.api.entity.IEntityLiving;
import noppes.npcs.api.IDamageSource;
import noppes.npcs.api.IWorld;
import noppes.npcs.api.entity.IEntityLivingBase;
import noppes.npcs.api.item.IItemStack;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.api.INbtList;
import noppes.npcs.api.INbt;
import kamkeel.npcs.controllers.data.ability.AbilityVariant;
import kamkeel.npcs.controllers.data.ability.enums.LockMode;
import kamkeel.npcs.controllers.data.ability.enums.TargetingMode;
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyBarrierData;
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyDisplayData;
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyPanelData;
import kamkeel.npcs.controllers.data.telegraph.TelegraphType;
import java.util.Arrays;
import java.util.List;
/**
* Energy Wall ability: Spawns a placeable flat energy barrier.
* Can float above ground (configurable height offset).
* Optional launching mode fires the wall forward with damage/knockback.
*/
public class AbilityWall extends AbilityBarrier {
private final EnergyPanelData panelData;
public AbilityWall() {
super(
new EnergyDisplayData(0x44FFCC, 0x22CCAA, true, 0.3f, 0.4f, 0.5f, 0.0f),
new EnergyBarrierData(80.0f, true, 160, true)
);
this.panelData = new EnergyPanelData(4.0f, 3.0f, 0.0f);
this.typeId = "ability.cnpc.wall";
this.name = "Wall";
this.targetingMode = TargetingMode.AGGRO_TARGET;
this.maxRange = 15.0f;
this.minRange = 2.0f;
this.cooldownTicks = 80;
this.windUpTicks = 20;
this.lockMovement = LockMode.WINDUP;
this.telegraphType = TelegraphType.LINE;
this.showTelegraph = true;
this.windUpAnimationName = "";
this.activeAnimationName = "";
this.defaultIconLayers = new DefaultIconLayer[]{
new DefaultIconLayer("customnpcs:textures/gui/ability/wall.png"),
new DefaultIconLayer("customnpcs:textures/gui/ability/wall_overlay.png",
() -> isOuterColorEnabled() ? getOuterColor() : getInnerColor())
};
}
@Override
public boolean hasDamage() {
return panelData.launching; // Only has damage when launching
}
// ==================== ABSTRACT IMPLEMENTATIONS ====================
@Override
protected EntityEnergyBarrier createBarrierEntity(IEntityLivingBase caster, IEntityLivingBase target) {
// Place wall between caster and target
double placeX, placeY, placeZ;
float yaw;
if (target != null) {
double dx = target.posX - caster.posX;
double dz = target.posZ - caster.posZ;
double dist = Math.sqrt(dx * dx + dz * dz);
if (dist < 0.01) {
// Target at same position — fall through to look-direction placement
float yawRad = (float) Math.toRadians(caster.rotationYaw);
placeX = caster.posX + (-Math.sin(yawRad) * 3.0);
placeY = caster.posY;
placeZ = caster.posZ + (Math.cos(yawRad) * 3.0);
yaw = caster.rotationYaw;
} else {
double placeDist = Math.min(dist * 0.5, 5.0);
placeX = caster.posX + (dx / dist) * placeDist;
placeY = caster.posY;
placeZ = caster.posZ + (dz / dist) * placeDist;
yaw = (float) (Math.atan2(-dx, dz) * 180.0 / Math.PI);
}
} else {
float yawRad = (float) Math.toRadians(caster.rotationYaw);
placeX = caster.posX + (-Math.sin(yawRad) * 3.0);
placeY = caster.posY;
placeZ = caster.posZ + (Math.cos(yawRad) * 3.0);
yaw = caster.rotationYaw;
}
placeX += offsetX;
placeY += offsetY;
placeZ += offsetZ;
EntityEnergyPanel.PanelMode mode = panelData.launching
? EntityEnergyPanel.PanelMode.LAUNCHED
: EntityEnergyPanel.PanelMode.PLACED;
EnergyPanelData data = panelData.copy();
data.heightOffset = 0.0f; // Y offset now handled by base class via placeY
EntityEnergyPanel panel = new EntityEnergyPanel(
caster.worldObj, caster,
placeX, placeY, placeZ, yaw, mode,
displayData.copy(), lightningData.copy(), barrierData.copy(), data
);
panel.setSourceAbility(this);
return panel;
}
@Override
public float getTelegraphRadius() {
return Math.max(panelData.panelWidth, panelData.panelHeight) * 0.5f;
}
// ==================== VARIANTS ====================
@Override
public List<AbilityVariant> getVariants() {
return Arrays.asList(
new AbilityVariant("ability.variant.static", a -> {
a.setName("Static Wall");
}),
new AbilityVariant("ability.variant.launched", a -> {
AbilityWall wall = (AbilityWall) a;
a.setName("Launched Wall");
wall.panelData.launching = true;
wall.panelData.launchSpeed = 0.6f;
wall.panelData.launchDamage = 10.0f;
wall.panelData.launchKnockback = 3.0f;
})
);
}
// ==================== TYPE-SPECIFIC NBT ====================
@Override
protected void writeBarrierTypeNBT(INbt nbt) {
panelData.writeNBT(new NBTWrapper(nbt));
}
@Override
protected void readBarrierTypeNBT(INbt nbt) {
panelData.readNBT(new NBTWrapper(nbt));
// Migrate legacy heightOffset → base class offsetY
if (!nbt.hasKey("barrierOffsetY") && panelData.heightOffset != 0.0f) {
offsetY = panelData.heightOffset;
panelData.heightOffset = 0.0f;
}
}
// ==================== GETTERS & SETTERS ====================
public EnergyPanelData getPanelData() {
return panelData;
}
public float getPanelWidth() {
return panelData.panelWidth;
}
public void setPanelWidth(float width) {
panelData.setPanelWidth(width);
}
public float getPanelHeight() {
return panelData.panelHeight;
}
public void setPanelHeight(float height) {
panelData.setPanelHeight(height);
}
public boolean isLaunching() {
return panelData.launching;
}
public void setLaunching(boolean launching) {
panelData.launching = launching;
}
public float getLaunchSpeed() {
return panelData.launchSpeed;
}
public void setLaunchSpeed(float speed) {
panelData.setLaunchSpeed(speed);
}
public float getLaunchDamage() {
return panelData.launchDamage;
}
public void setLaunchDamage(float damage) {
panelData.setLaunchDamage(damage);
}
@Override
public float getDisplayDamage() { return panelData.launching ? panelData.launchDamage : 0; }
public float getLaunchKnockback() {
return panelData.launchKnockback;
}
public void setLaunchKnockback(float kb) {
panelData.setLaunchKnockback(kb);
}
// ==================== TYPE-SPECIFIC GUI ====================
@ClientOnly
@Override
protected void addBarrierTypeDefinitions(List<FieldDef> defs) {
defs.add(FieldDef.section("ability.section.panel"));
defs.add(FieldDef.row(
FieldDef.floatField("gui.width", this::getPanelWidth, this::setPanelWidth).range(0.5f, 100.0f),
FieldDef.floatField("gui.height", this::getPanelHeight, this::setPanelHeight).range(0.5f, 100.0f)
));
defs.add(FieldDef.section("ability.section.launch"));
defs.add(FieldDef.boolField("gui.enabled", this::isLaunching, this::setLaunching)
.hover("ability.hover.launching"));
defs.add(FieldDef.row(
FieldDef.floatField("stats.speed", this::getLaunchSpeed, this::setLaunchSpeed)
.visibleWhen(this::isLaunching),
FieldDef.floatField("enchantment.damage", this::getLaunchDamage, this::setLaunchDamage)
.visibleWhen(this::isLaunching)
));
defs.add(FieldDef.floatField("ability.knockback", this::getLaunchKnockback, this::setLaunchKnockback)
.visibleWhen(this::isLaunching));
}
}
@@ -0,0 +1,136 @@
package kamkeel.npcs.controllers.data.ability.util;
import noppes.npcs.constants.ClientOnly;
import kamkeel.npcs.util.IVector3;
import kamkeel.npcs.controllers.data.ability.preview.PreviewEntityHandler;
import kamkeel.npcs.controllers.data.ability.gui.SubGuiAbilityConfig;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityConfigCallback;
import kamkeel.npcs.controllers.data.ability.gui.FieldDef;
import kamkeel.npcs.controllers.data.ability.gui.IChainedAbilityFieldProvider;
import kamkeel.npcs.controllers.data.ability.gui.IAbilityFieldProvider;
import noppes.npcs.entity.EntityNPCInterface;
import kamkeel.npcs.entity.EntityEnergyDome;
import kamkeel.npcs.entity.EntityEnergyBarrier;
import kamkeel.npcs.entity.EntityEnergyPanel;
import kamkeel.npcs.entity.EntityAbilityOrb;
import kamkeel.npcs.entity.EntityAbilityLaser;
import kamkeel.npcs.entity.EntityAbilityDisc;
import kamkeel.npcs.entity.EntityAbilityBeam;
import kamkeel.npcs.entity.EntityEnergyProjectile;
import kamkeel.npcs.util.ByteBufUtils;
import noppes.npcs.api.entity.IEntityLiving;
import noppes.npcs.api.IDamageSource;
import noppes.npcs.api.IWorld;
import noppes.npcs.api.entity.IEntityLivingBase;
import noppes.npcs.api.item.IItemStack;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.api.INbtList;
import noppes.npcs.api.INbt;
import kamkeel.npcs.controllers.data.ability.enums.TargetFilter;
import noppes.npcs.controllers.PartyController;
import noppes.npcs.controllers.data.Party;
import noppes.npcs.controllers.data.PlayerData;
/**
* Utility class for determining friend/foe relationships between entities.
* Extracted from EntityEnergyProjectile.shouldIgnoreEntity() for reuse
* across all AoE abilities and AbilityEffect targeting.
*/
public class AbilityTargetHelper {
/**
* Determines if target is an ally of caster.
* <ul>
* <li>NPC vs NPC: same faction ID = ally</li>
* <li>NPC vs Player: faction friendly to player = ally</li>
* <li>Player vs NPC: NPC's faction friendly to player = ally</li>
* <li>Player vs Player: same party (if exists) = ally</li>
* <li>Passive NPCs are always considered allies (never enemies)</li>
* </ul>
*/
public static boolean isAlly(IEntityLivingBase caster, IEntity target) {
if (caster == null || target == null) return false;
if (target == caster) return true;
// Ally resolution relies on server-owned player/party/faction state.
if (caster.worldObj == null || caster.worldObj.isRemote) return false;
// NPC target checks
if (target instanceof EntityNPCInterface) {
EntityNPCInterface targetNpc = (EntityNPCInterface) target;
// Passive NPCs are always considered allies
if (targetNpc.faction.isPassive) return true;
// NPC caster: same faction = ally
if (caster instanceof EntityNPCInterface) {
EntityNPCInterface casterNpc = (EntityNPCInterface) caster;
if (casterNpc.faction.id == targetNpc.faction.id) return true;
}
// Player caster: friendly faction = ally
if (caster instanceof IPlayer) {
IPlayer casterPlayer = (IPlayer) caster;
if (PlayerData.get(casterPlayer) == null) return false;
if (targetNpc.faction.isFriendlyToPlayer(casterPlayer)) return true;
}
return false;
}
// Player target checks
if (target instanceof IPlayer && caster instanceof EntityNPCInterface) {
// NPC caster vs Player target: faction friendly to player = ally
EntityNPCInterface casterNpc = (EntityNPCInterface) caster;
IPlayer targetPlayer = (IPlayer) target;
if (PlayerData.get(targetPlayer) == null) return false;
return casterNpc.faction.isFriendlyToPlayer(targetPlayer);
}
if (target instanceof IPlayer && caster instanceof IPlayer) {
// Player vs Player: same party = ally
IPlayer casterPlayer = (IPlayer) caster;
IPlayer targetPlayer = (IPlayer) target;
PlayerData casterData = PlayerData.get(casterPlayer);
PlayerData targetData = PlayerData.get(targetPlayer);
if (casterData == null || targetData == null) return false;
if (casterData.partyUUID != null && casterData.partyUUID.equals(targetData.partyUUID)) {
Party party = PartyController.Instance().getParty(casterData.partyUUID);
if (party != null && !party.friendlyFire()) return true;
}
return false;
}
// Unknown IEntity types: not an ally
return false;
}
/**
* Determines if a target should be affected by an ability, given the
* filter mode and includeSelf setting.
*
* @param caster The IEntity using the ability
* @param target The potential target IEntity
* @param filter ALLIES, ENEMIES, or ALL
* @param includeSelf Whether to include the caster themselves
* @return true if the target should be affected
*/
public static boolean shouldAffect(IEntityLivingBase caster, IEntity target,
TargetFilter filter, boolean includeSelf) {
if (caster == null || target == null || filter == null) return false;
if (caster.worldObj == null || caster.worldObj.isRemote) return false;
if (target == caster) return includeSelf;
if (!(target instanceof IEntityLivingBase)) return false;
if (!target.isEntityAlive()) return false;
switch (filter) {
case ALLIES:
return isAlly(caster, target);
case ENEMIES:
return !isAlly(caster, target);
case ALL:
return true;
default:
return false;
}
}
}
@@ -1,10 +1,20 @@
package kamkeel.npcs.controllers.data.attribute;
import java.util.HashMap;
import java.util.Map;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.entity.IEntityLiving;
import noppes.npcs.api.entity.IEntityLivingBase;
import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.api.IDamageSource;
import noppes.npcs.api.INbt;
import noppes.npcs.api.INbtList;
import noppes.npcs.api.item.IItemStack;
import noppes.npcs.api.IWorld;
/**
* CustomAttributeMap holds a set of attribute instances for an entity (or item).
* CustomAttributeMap holds a set of attribute instances for an IEntity (or item).
*/
public class PlayerAttributeMap {
public final Map<AttributeDefinition, PlayerAttribute> map = new HashMap<>();
@@ -0,0 +1,31 @@
package kamkeel.npcs.controllers.data.attribute.requirement;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.entity.IEntityLiving;
import noppes.npcs.api.entity.IEntityLivingBase;
import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.api.IDamageSource;
import noppes.npcs.api.INbt;
import noppes.npcs.api.INbtList;
import noppes.npcs.api.item.IItemStack;
import noppes.npcs.api.IWorld;
public interface IRequirementChecker {
String getKey();
String getTranslation();
String getTooltipValue(INbt nbt);
Object getValue(INbt nbt);
void apply(INbt nbt, Object value);
/**
* Checks the requirement in the provided NBT.
* Return false if the requirement isn’t met.
*/
boolean check(IPlayer player, INbt nbt);
}
@@ -0,0 +1,48 @@
package kamkeel.npcs.controllers.data.attribute.requirement;
import noppes.npcs.api.entity.IEntityLiving;
import noppes.npcs.api.IDamageSource;
import noppes.npcs.api.IWorld;
import noppes.npcs.api.entity.IEntityLivingBase;
import noppes.npcs.api.item.IItemStack;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.api.INbtList;
import noppes.npcs.api.INbt;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
public class RequirementCheckerRegistry {
private static final Map<String, IRequirementChecker> checkers = new HashMap<>();
public static void registerChecker(IRequirementChecker checker) {
if (!checkers.containsKey(checker.getKey())) {
checkers.put(checker.getKey(), checker);
}
}
/**
* Loops over all registered requirement keys.
* For each key present in the NBT, its checker must return true.
*/
public static boolean checkRequirements(IPlayer player, INbt nbt) {
for (Map.Entry<String, IRequirementChecker> entry : checkers.entrySet()) {
String reqKey = entry.getKey();
if (nbt.hasKey(reqKey)) {
if (!entry.getValue().check(player, nbt)) {
return false;
}
}
}
return true;
}
public static Set<String> getAllKeys() {
return checkers.keySet();
}
public static IRequirementChecker getChecker(String key) {
return checkers.get(key);
}
}
@@ -0,0 +1,59 @@
package kamkeel.npcs.controllers.data.attribute.requirement.types;
import noppes.npcs.api.entity.IEntityLiving;
import noppes.npcs.api.IDamageSource;
import noppes.npcs.api.IWorld;
import noppes.npcs.api.entity.IEntityLivingBase;
import noppes.npcs.api.item.IItemStack;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.api.INbtList;
import noppes.npcs.api.INbt;
import kamkeel.npcs.controllers.data.attribute.requirement.IRequirementChecker;
import noppes.npcs.controllers.data.PlayerData;
public class ProfileSlotRequirement implements IRequirementChecker {
@Override
public String getKey() {
return "cnpc_profile_slot";
}
@Override
public String getTranslation() {
return "profile.slot";
}
@Override
public String getTooltipValue(INbt nbt) {
if (nbt.hasKey(getKey())) {
int profileSlot = nbt.getInteger(getKey());
return String.valueOf(profileSlot);
}
return "null";
}
@Override
public void apply(INbt nbt, Object value) {
if (value instanceof Integer) {
nbt.setInteger(getKey(), (Integer) value);
}
}
@Override
public Object getValue(INbt nbt) {
if (nbt.hasKey(getKey())) {
return nbt.getInteger(getKey());
}
return null;
}
@Override
public boolean check(IPlayer player, INbt nbt) {
if (nbt.hasKey(getKey())) {
int profileSlot = nbt.getInteger(getKey());
return PlayerData.get(player).profileSlot == profileSlot;
}
return true;
}
}
@@ -0,0 +1,62 @@
package kamkeel.npcs.controllers.data.attribute.requirement.types;
import noppes.npcs.api.entity.IEntityLiving;
import noppes.npcs.api.IDamageSource;
import noppes.npcs.api.IWorld;
import noppes.npcs.api.entity.IEntityLivingBase;
import noppes.npcs.api.item.IItemStack;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.api.INbtList;
import noppes.npcs.api.INbt;
import kamkeel.npcs.controllers.data.attribute.requirement.IRequirementChecker;
import noppes.npcs.controllers.QuestController;
import noppes.npcs.controllers.data.PlayerData;
public class QuestActiveRequirement implements IRequirementChecker {
@Override
public String getKey() {
return "cnpc_quest_active";
}
@Override
public String getTranslation() {
return "quest.active";
}
@Override
public String getTooltipValue(INbt nbt) {
if (nbt.hasKey(getKey())) {
int questID = nbt.getInteger(getKey());
if (QuestController.Instance.get(questID) != null) {
return QuestController.Instance.get(questID).getName();
}
}
return "null";
}
@Override
public Object getValue(INbt nbt) {
if (nbt.hasKey(getKey())) {
return nbt.getInteger(getKey());
}
return null;
}
@Override
public void apply(INbt nbt, Object value) {
if (value instanceof Integer) {
nbt.setInteger(getKey(), (Integer) value);
}
}
@Override
public boolean check(IPlayer player, INbt nbt) {
if (nbt.hasKey(getKey())) {
int questID = nbt.getInteger(getKey());
return PlayerData.get(player).questData.hasActiveQuest(questID);
}
return true;
}
}
@@ -0,0 +1,62 @@
package kamkeel.npcs.controllers.data.attribute.requirement.types;
import noppes.npcs.api.entity.IEntityLiving;
import noppes.npcs.api.IDamageSource;
import noppes.npcs.api.IWorld;
import noppes.npcs.api.entity.IEntityLivingBase;
import noppes.npcs.api.item.IItemStack;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.api.INbtList;
import noppes.npcs.api.INbt;
import kamkeel.npcs.controllers.data.attribute.requirement.IRequirementChecker;
import noppes.npcs.controllers.QuestController;
import noppes.npcs.controllers.data.PlayerData;
public class QuestCompletedRequirement implements IRequirementChecker {
@Override
public String getKey() {
return "cnpc_quest_completed";
}
@Override
public String getTranslation() {
return "quest.quest";
}
@Override
public String getTooltipValue(INbt nbt) {
if (nbt.hasKey(getKey())) {
int questID = nbt.getInteger(getKey());
if (QuestController.Instance.get(questID) != null) {
return QuestController.Instance.get(questID).getName();
}
}
return "null";
}
@Override
public void apply(INbt nbt, Object value) {
if (value instanceof Integer) {
nbt.setInteger(getKey(), (Integer) value);
}
}
@Override
public Object getValue(INbt nbt) {
if (nbt.hasKey(getKey())) {
return nbt.getInteger(getKey());
}
return null;
}
@Override
public boolean check(IPlayer player, INbt nbt) {
if (nbt.hasKey(getKey())) {
int questID = nbt.getInteger(getKey());
return PlayerData.get(player).questData.hasFinishedQuest(questID);
}
return true;
}
}
@@ -0,0 +1,132 @@
package kamkeel.npcs.controllers.data.attribute.requirement.types;
import noppes.npcs.api.entity.IEntityLiving;
import noppes.npcs.api.IDamageSource;
import noppes.npcs.api.IWorld;
import noppes.npcs.api.entity.IEntityLivingBase;
import noppes.npcs.api.item.IItemStack;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.api.INbtList;
import noppes.npcs.api.INbt;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import kamkeel.npcs.controllers.ProfileController;
import kamkeel.npcs.controllers.data.attribute.requirement.IRequirementChecker;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.HashMap;
import java.util.UUID;
public class SoulbindRequirement implements IRequirementChecker {
public static HashMap<String, String> uuidToUsername = new HashMap<>();
@Override
public String getKey() {
return "cnpc_soulbind";
}
@Override
public String getTranslation() {
return "requirement.soulbind";
}
@Override
public String getTooltipValue(INbt nbt) {
if (nbt.hasKey(getKey())) {
String uuid = nbt.getString(getKey());
if (uuid != null && !uuid.isEmpty()) {
return getUsernameFromUUID(uuid);
}
}
return "null";
}
private String getUsernameFromUUID(String uuid) {
if (uuidToUsername.containsKey(uuid)) {
if (!uuidToUsername.get(uuid).equals("null")) {
return uuidToUsername.get(uuid);
}
}
try {
String strippedUUID = uuid.replace("-", "");
String urlStr = "https://sessionserver.mojang.com/session/minecraft/profile/" + strippedUUID;
HttpURLConnection connection = (HttpURLConnection) new URL(urlStr).openConnection();
connection.setConnectTimeout(5000);
connection.setReadTimeout(5000);
connection.setRequestMethod("GET");
connection.connect();
InputStream stream = connection.getInputStream();
InputStreamReader reader = new InputStreamReader(stream, "UTF-8");
JsonElement element = new JsonParser().parse(reader);
JsonObject jsonObj = element.getAsJsonObject();
if (jsonObj.has("name")) {
String name = jsonObj.get("name").getAsString();
uuidToUsername.put(uuid, name);
return name;
}
} catch (Exception e) {
e.printStackTrace();
}
return "null";
}
@Override
public Object getValue(INbt nbt) {
if (nbt.hasKey(getKey())) {
return nbt.getString(getKey());
}
return null;
}
@Override
public void apply(INbt nbt, Object value) {
if (value instanceof String) {
String entry = (String) value;
if (entry.isEmpty())
return;
// Check if the provided value is already a valid UUID
UUID uuid = null;
try {
uuid = UUID.fromString(entry);
} catch (Exception ignored) {
}
// If not a UUID, attempt to treat the value as a username
if (uuid == null) {
uuid = ProfileController.Instance.getUUIDFromUsername(entry);
}
if (uuid != null) {
nbt.setString(getKey(), uuid.toString());
}
}
}
@Override
public boolean check(IPlayer player, INbt nbt) {
if (nbt.hasKey(getKey())) {
String uuidString = nbt.getString(getKey());
if (uuidString != null && !uuidString.isEmpty()) {
UUID convert = null;
try {
convert = UUID.fromString(uuidString);
} catch (Exception ignored) {
}
if (convert != null)
return player.getUniqueID().equals(convert);
}
return false;
}
return true;
}
}
@@ -0,0 +1,44 @@
package kamkeel.npcs.controllers.data.attribute.tracker;
import noppes.npcs.api.entity.IEntityLiving;
import noppes.npcs.api.IDamageSource;
import noppes.npcs.api.IWorld;
import noppes.npcs.api.entity.IEntityLivingBase;
import noppes.npcs.api.item.IItemStack;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.api.INbtList;
import noppes.npcs.api.INbt;
import java.util.ArrayList;
import java.util.List;
public final class AttributeRecalcEvent {
@FunctionalInterface
public interface PreListener {
void onPre(IPlayer player, PlayerAttributeTracker tracker);
}
private static final List<PreListener> preListeners = new ArrayList<>();
private static final List<Listener> postListeners = new ArrayList<>();
public static void pre(IPlayer player, PlayerAttributeTracker tracker) {
for (PreListener l : preListeners) l.onPre(player, tracker);
}
@FunctionalInterface
public interface Listener {
void onPost(IPlayer player, PlayerAttributeTracker tracker);
}
public static void registerListener(Listener l) {
postListeners.add(l);
}
public static void post(IPlayer player, PlayerAttributeTracker tracker) {
for (Listener l : postListeners) l.onPost(player, tracker);
}
public static void registerPreListener(PreListener l) {
preListeners.add(l);
}
}
@@ -0,0 +1,19 @@
package kamkeel.npcs.controllers.data.attribute.tracker;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.entity.IEntityLiving;
import noppes.npcs.api.entity.IEntityLivingBase;
import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.api.IDamageSource;
import noppes.npcs.api.INbt;
import noppes.npcs.api.INbtList;
import noppes.npcs.api.item.IItemStack;
import noppes.npcs.api.IWorld;
/**
* Implement this interface to receive a callback after a player's attributes have been recalculated.
*/
public interface IAttributeRecalcListener {
void onAttributesRecalculated(IPlayer player, PlayerAttributeTracker tracker);
}
@@ -0,0 +1,266 @@
package kamkeel.npcs.controllers.data.attribute.tracker;
import java.util.HashMap;
import java.util.Map.Entry;
import java.util.Map;
import java.util.UUID;
import kamkeel.npcs.controllers.AttributeController;
import kamkeel.npcs.controllers.data.attribute.AttributeDefinition;
import kamkeel.npcs.controllers.data.attribute.PlayerAttribute;
import kamkeel.npcs.controllers.data.attribute.PlayerAttributeMap;
import kamkeel.npcs.controllers.data.attribute.requirement.RequirementCheckerRegistry;
import kamkeel.npcs.CustomAttributes;
import kamkeel.npcs.util.AttributeAttackUtil;
import kamkeel.npcs.util.AttributeItemUtil;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.entity.IEntityLiving;
import noppes.npcs.api.entity.IEntityLivingBase;
import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.api.handler.data.ICustomAttribute;
import noppes.npcs.api.handler.data.IPlayerAttributes;
import noppes.npcs.api.IDamageSource;
import noppes.npcs.api.INbt;
import noppes.npcs.api.INbtList;
import noppes.npcs.api.item.IItemStack;
import noppes.npcs.api.IWorld;
public class PlayerAttributeTracker implements IPlayerAttributes {
public static UUID healthUUID = UUID.fromString("48a0ad75-2cf8-4838-ad2f-9a0aadc57dfe");
public static UUID healthBoostUUID = UUID.fromString("93cfba41-294a-4e6b-a98b-bee76a9f813b");
public static UUID movementSpeedUUID = UUID.fromString("e7e0dd10-8ed5-42fb-8167-e71f8de4ea0c");
public static UUID knockbackResUUID = UUID.fromString("b2f261e8-34f0-4140-9828-b56c1f6e0ff2");
private final UUID playerId;
public PlayerAttributeMap playerAttributes = new PlayerAttributeMap();
public float extraHealth = 0;
public float extraHealthBoost = 0;
public float movementSpeed = 0;
public float knockbackRes = 0;
public float gearOutput = 0.0f;
public final Map<Integer, Float> magicDamage = new HashMap<>();
public final Map<Integer, Float> magicBoost = new HashMap<>();
public final Map<Integer, Float> magicDefense = new HashMap<>();
public final Map<Integer, Float> magicResistance = new HashMap<>();
private PlayerEquipmentTracker equipmentTracker = new PlayerEquipmentTracker();
public PlayerAttributeTracker(UUID playerId) {
this.playerId = playerId;
// Initial recalc with base damage 0.
recalcAttributes(null);
}
public void recalcAttributes(IPlayer player) {
// Reset aggregated attributes and magic maps.
playerAttributes = new PlayerAttributeMap();
magicDamage.clear();
magicBoost.clear();
magicDefense.clear();
magicResistance.clear();
if (player != null) {
// Pre Event Listener
AttributeRecalcEvent.pre(player, this);
// Update equipment tracker.
PlayerEquipmentTracker currentEquip = new PlayerEquipmentTracker();
currentEquip.updateFrom(player);
IItemStack[] equipment = new IItemStack[]{currentEquip.heldItem, currentEquip.boots, currentEquip.leggings, currentEquip.chestplate, currentEquip.helmet};
for (IItemStack piece : equipment) {
if (piece != null) {
if (piece.stackTagCompound != null && piece.stackTagCompound.hasKey(AttributeItemUtil.TAG_RPGCORE)) {
INbt rpgCore = piece.stackTagCompound.getCompoundTag(AttributeItemUtil.TAG_RPGCORE);
if (rpgCore.hasKey(AttributeItemUtil.TAG_REQUIREMENTS)) {
INbt reqTag = rpgCore.getCompoundTag(AttributeItemUtil.TAG_REQUIREMENTS);
if (!RequirementCheckerRegistry.checkRequirements(player, reqTag)) {
continue;
}
}
}
for (Entry<String, Float> entry : AttributeItemUtil.readAttributes(piece).entrySet()) {
String key = entry.getKey();
float value = entry.getValue();
AttributeDefinition def = AttributeController.getAttribute(key);
if (def != null) {
PlayerAttribute inst = playerAttributes.getAttributeInstance(def);
if (inst == null) {
inst = playerAttributes.registerAttribute(def, 0.0f);
}
inst.value = (inst.getValue() + value);
}
}
// Process Magic
// OFFENSIVE MAGIC
Map<Integer, Float> magicFlat = AttributeItemUtil.readMagicAttributeMap(piece, CustomAttributes.MAGIC_DAMAGE_KEY);
for (Entry<Integer, Float> entry : magicFlat.entrySet()) {
int magicId = entry.getKey();
float value = entry.getValue();
magicDamage.put(magicId, magicDamage.getOrDefault(magicId, 0.0f) + value);
}
Map<Integer, Float> magicPercent = AttributeItemUtil.readMagicAttributeMap(piece, CustomAttributes.MAGIC_BOOST_KEY);
for (Entry<Integer, Float> entry : magicPercent.entrySet()) {
int magicId = entry.getKey();
float value = entry.getValue();
magicBoost.put(magicId, magicBoost.getOrDefault(magicId, 0.0f) + value);
}
// DEFENSIVE MAGIC
Map<Integer, Float> magicDefFlat = AttributeItemUtil.readMagicAttributeMap(piece, CustomAttributes.MAGIC_DEFENSE_KEY);
for (Entry<Integer, Float> entry : magicDefFlat.entrySet()) {
int magicId = entry.getKey();
float value = entry.getValue();
magicDefense.put(magicId, magicDefense.getOrDefault(magicId, 0.0f) + value);
}
Map<Integer, Float> magicResist = AttributeItemUtil.readMagicAttributeMap(piece, CustomAttributes.MAGIC_RESISTANCE_KEY);
for (Entry<Integer, Float> entry : magicResist.entrySet()) {
int magicId = entry.getKey();
float value = entry.getValue();
magicResistance.put(magicId, magicResistance.getOrDefault(magicId, 0.0f) + value);
}
}
}
equipmentTracker = currentEquip;
gearOutput = AttributeAttackUtil.calculateGearOutput(this);
extraHealth = getAttributeValue(CustomAttributes.HEALTH);
extraHealthBoost = getAttributeValue(CustomAttributes.HEALTH_BOOST);
movementSpeed = getAttributeValue(CustomAttributes.MOVEMENT_SPEED);
knockbackRes = getAttributeValue(CustomAttributes.KNOCKBACK_RES);
// Update vanilla attributes with our aggregated values.
updatePlayerMaxHealth(player);
updatePlayerMovementSpeed(player);
updatePlayerKnockbackRes(player);
// Post Recalculation Event to Listeners
AttributeRecalcEvent.post(player, this);
}
}
/**
* Updates the player's maximum health attribute by applying a custom attribute modifier.
* In this system, the aggregated HEALTH attribute is treated as bonus health on top of a base of 20.
*
* @param player The IPlayer whose max health should be updated.
*/
public void updatePlayerMaxHealth(IPlayer player) {
float bonusHealth = extraHealth;
float healthBoostPercent = extraHealthBoost / 100;
IAttributeInstance maxHealthAttr = player.getAttributeMap().getAttributeInstance(Attributes.MAX_HEALTH);
try {
AttributeModifier oldFlat = maxHealthAttr.getModifier(healthUUID);
if (oldFlat != null) {
maxHealthAttr.removeModifier(oldFlat);
}
AttributeModifier oldBoost = maxHealthAttr.getModifier(healthBoostUUID);
if (oldBoost != null) {
maxHealthAttr.removeModifier(oldBoost);
}
} catch (Exception ignored) {
}
// Apply the flat bonus (operation 0)
if (bonusHealth != 0) {
maxHealthAttr.applyModifier(new AttributeModifier(healthUUID, "RPGCoreHealthBonus", bonusHealth, 0));
}
// Apply the percent boost (operation 1 multiplies the base health, here assumed to be 20)
if (healthBoostPercent != 0) {
maxHealthAttr.applyModifier(new AttributeModifier(healthBoostUUID, "RPGCoreHealthBoost", healthBoostPercent, 1));
}
// Ensure current health is within the new maximum.
float currentHealth = player.getHealth();
if (currentHealth > maxHealthAttr.getAttributeValue()) {
player.setHealth((float) maxHealthAttr.getAttributeValue());
}
}
public void updatePlayerMovementSpeed(IPlayer player) {
float extraSpeed = movementSpeed / 100;
IAttributeInstance speedAttr = player.getAttributeMap().getAttributeInstance(Attributes.MOVEMENT_SPEED);
try {
AttributeModifier old = speedAttr.getModifier(movementSpeedUUID);
if (old != null) {
speedAttr.removeModifier(old);
}
} catch (Exception ignored) {
}
if (extraSpeed != 0) {
speedAttr.applyModifier(new AttributeModifier(movementSpeedUUID, "RPGCoreMovementSpeed", extraSpeed, 1));
}
}
public void updatePlayerKnockbackRes(IPlayer player) {
float extraRes = knockbackRes / 100;
IAttributeInstance knockResAttr = player.getAttributeMap().getAttributeInstance(Attributes.KNOCKBACK_RESISTANCE);
try {
AttributeModifier old = knockResAttr.getModifier(knockbackResUUID);
if (old != null) {
knockResAttr.removeModifier(old);
}
} catch (Exception ignored) {
}
if (extraRes != 0) {
knockResAttr.applyModifier(new AttributeModifier(knockbackResUUID, "RPGCoreKnockbackRes", extraRes, 1));
}
}
/**
* Checks if the player's equipment has changed and recalculates attributes if needed.
*/
public void updateIfChanged(IPlayer player) {
PlayerEquipmentTracker current = new PlayerEquipmentTracker();
current.updateFrom(player);
if (!equipmentTracker.equals(current)) {
recalcAttributes(player);
equipmentTracker = current;
}
}
public float getAttributeValue(AttributeDefinition def) {
ICustomAttribute inst = playerAttributes.getAttributeInstance(def);
return inst != null ? inst.getValue() : 0.0f;
}
public void recalculate(IPlayer player) {
if (player == null || player.getMCEntity() == null)
return;
recalcAttributes((IPlayer) player.getMCEntity());
}
public ICustomAttribute[] getAttributes() {
return playerAttributes.map.values().toArray(new ICustomAttribute[playerAttributes.map.size()]);
}
public float getAttributeValue(String key) {
for (ICustomAttribute inst : getAttributes()) {
if (inst.getAttribute().getKey().equalsIgnoreCase(key)) {
return inst.getValue();
}
}
return 0.0f;
}
public boolean hasAttribute(String key) {
for (ICustomAttribute inst : getAttributes()) {
if (inst.getAttribute().getKey().equalsIgnoreCase(key)) {
return true;
}
}
return false;
}
public ICustomAttribute getAttribute(String key) {
for (ICustomAttribute inst : getAttributes()) {
if (inst.getAttribute().getKey().equalsIgnoreCase(key)) {
return inst;
}
}
return null;
}
}
Loaded 100 of 335 files, more files were not shown because too many files have changed in this diff. Show more