mirror of
https://github.com/KAMKEEL/CustomNPC-Plus.git
synced 2026-11-13 00:47:50 +00:00
HUGE CORE MIGRATION (WIP, WONT BUILD)
This commit is contained in:
1 parent
f4aa04dcb6
commit
2e2b52252d
335 files changed
+64280
-454
No files matched your search
+112
-9
@@ -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*
|
||||
|
||||
________________________________________________________________________________________________________________________________________________________________________________________________________________________________________________
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
+147
@@ -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;
|
||||
}
|
||||
}
|
||||
+138
@@ -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;
|
||||
}
|
||||
}
|
||||
+33
@@ -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";
|
||||
}
|
||||
}
|
||||
+128
@@ -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;
|
||||
}
|
||||
}
|
||||
+101
@@ -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"));
|
||||
}
|
||||
}
|
||||
+208
@@ -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); }
|
||||
}
|
||||
+110
@@ -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;
|
||||
}
|
||||
}
|
||||
+195
@@ -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 & 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;
|
||||
|
||||
+118
@@ -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);
|
||||
}
|
||||
}
|
||||
+138
@@ -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;
|
||||
}
|
||||
}
|
||||
+50
@@ -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();
|
||||
}
|
||||
}
|
||||
+11
-3
@@ -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
|
||||
|
||||
+39
-30
@@ -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;
|
||||
|
||||
+10
-1
@@ -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;
|
||||
|
||||
+10
-1
@@ -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;
|
||||
|
||||
+10
-1
@@ -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; }
|
||||
}
|
||||
+93
@@ -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();
|
||||
}
|
||||
}
|
||||
+51
@@ -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);
|
||||
}
|
||||
}
|
||||
+230
@@ -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,
|
||||
|
||||
+141
@@ -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) {}
|
||||
}
|
||||
+25
@@ -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
|
||||
}
|
||||
+25
@@ -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 {
|
||||
}
|
||||
+25
@@ -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 {
|
||||
}
|
||||
+13
@@ -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; }
|
||||
}
|
||||
+12
@@ -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() {}
|
||||
}
|
||||
+25
@@ -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;
|
||||
}
|
||||
+443
@@ -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);
|
||||
}
|
||||
}
|
||||
+259
@@ -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));
|
||||
}
|
||||
}
|
||||
+258
@@ -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));
|
||||
}
|
||||
}
|
||||
+164
@@ -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"));
|
||||
}
|
||||
}
|
||||
+211
@@ -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"));
|
||||
}
|
||||
}
|
||||
+946
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
+246
@@ -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));
|
||||
}
|
||||
}
|
||||
+178
@@ -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)
|
||||
));
|
||||
}
|
||||
}
|
||||
+207
@@ -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));
|
||||
}
|
||||
}
|
||||
+336
@@ -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);
|
||||
}
|
||||
}
|
||||
+255
@@ -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));
|
||||
}
|
||||
}
|
||||
+136
@@ -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<>();
|
||||
|
||||
+31
@@ -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);
|
||||
}
|
||||
+48
@@ -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);
|
||||
}
|
||||
}
|
||||
+59
@@ -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;
|
||||
}
|
||||
}
|
||||
+62
@@ -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;
|
||||
}
|
||||
}
|
||||
+62
@@ -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;
|
||||
}
|
||||
}
|
||||
+132
@@ -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;
|
||||
}
|
||||
}
|
||||
+44
@@ -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);
|
||||
}
|
||||
}
|
||||
+19
@@ -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);
|
||||
}
|
||||
+266
@@ -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
Reference in new issue
Block a user