Merge remote-tracking branch 'origin/syntax-highlighter-unified' into syntax-highlighter-unified

This commit is contained in:
bigguy345 committed 2026-02-25 17:14:12 +02:00
commit f9f1cb760c
66 files changed
+1471 -610

No files matched your search

+1 -1
View File
@@ -6,7 +6,7 @@ plugins {
id "dts.typescript-generator"
}
version = "1.11-beta9"
version = "1.11-beta10"
group= "kamkeel.customnpc-plus"
archivesBaseName = "CustomNPC-Plus"
+1 -1
Submodule src/api updated: 3c063d8bc1...52cfe77bb7.
@@ -0,0 +1,42 @@
package kamkeel.npcs.client.command;
import kamkeel.npcs.util.CNPCDebug;
import net.minecraft.command.CommandBase;
import net.minecraft.command.ICommandSender;
import net.minecraft.util.ChatComponentText;
/**
* Client-only command to toggle debug logging.
* Registered via ClientCommandHandler so it runs entirely on the client
* without sending a packet to the server.
*/
public class CommandCNPCDebugger extends CommandBase {
@Override
public String getCommandName() {
return "cnpcdebugger";
}
@Override
public String getCommandUsage(ICommandSender sender) {
return "/cnpcdebugger <type> - Toggle client debug logging for a type (e.g. energy)";
}
@Override
public int getRequiredPermissionLevel() {
return 0;
}
@Override
public void processCommand(ICommandSender sender, String[] args) {
if (args.length == 0) {
sender.addChatMessage(new ChatComponentText("\u00A7cUsage: /cnpcdebugger <type>"));
return;
}
String type = args[0].toLowerCase();
boolean newState = CNPCDebug.toggleClient(type);
String stateText = newState ? "\u00A7aENABLED" : "\u00A7cDISABLED";
sender.addChatMessage(new ChatComponentText("\u00A77[CNPC+] \u00A7eClient debug '" + type + "' " + stateText));
}
}
@@ -41,7 +41,7 @@ public class RenderAbilityLaser extends RenderEnergyAbility {
double endY = laser.getEndY();
double endZ = laser.getEndZ();
float width = laser.getLaserWidth();
float alpha = laser.getLingerAlpha();
float alpha = 1.0f;
// Calculate render offset (entity position is used as reference)
double offsetX = x - laser.posX;
@@ -95,6 +95,25 @@ public class RenderAbilityLaser extends RenderEnergyAbility {
double vertY = dz * horzX - dx * horzZ;
double vertZ = dx * horzY - dy * horzX;
// Rotate perpendicular vectors around beam axis (Rodrigues' rotation)
float rotationSpeed = laser.getRotationSpeed();
if (rotationSpeed != 0.0f) {
double angle = (laser.ticksExisted + partialTicks) * rotationSpeed * Math.PI / 180.0;
double cosA = Math.cos(angle);
double sinA = Math.sin(angle);
double rHorzX = horzX * cosA + vertX * sinA;
double rHorzY = horzY * cosA + vertY * sinA;
double rHorzZ = horzZ * cosA + vertZ * sinA;
double rVertX = -horzX * sinA + vertX * cosA;
double rVertY = -horzY * sinA + vertY * cosA;
double rVertZ = -horzZ * sinA + vertZ * cosA;
horzX = rHorzX; horzY = rHorzY; horzZ = rHorzZ;
vertX = rVertX; vertY = rVertY; vertZ = rVertZ;
}
// Inner scale defines the core width
float innerScale = 0.6f;
float innerWidth = width * innerScale;
@@ -134,7 +153,7 @@ public class RenderAbilityLaser extends RenderEnergyAbility {
}
private void renderChargingOrb(EntityAbilityLaser laser, double x, double y, double z, float partialTicks) {
float headSize = laser.getLaserWidth() * 1.5f;
float headSize = laser.getLaserWidth() * 0.5f;
float chargeProgress = laser.getInterpolatedChargeProgress(partialTicks);
float size = headSize * chargeProgress;
@@ -52,6 +52,7 @@ public class CommandKamkeel extends CommandBase {
registerCommand(new AuctionCommand());
if (ConfigMain.AttributesEnabled)
registerCommand(new AttributeCommand());
registerCommand(new DebugCommand());
}
public void registerCommand(CommandKamkeelBase command) {
@@ -0,0 +1,41 @@
package kamkeel.npcs.command;
import kamkeel.npcs.util.CNPCDebug;
import net.minecraft.command.CommandException;
import net.minecraft.command.ICommandSender;
import static kamkeel.npcs.util.ColorUtil.sendResult;
public class DebugCommand extends CommandKamkeelBase {
@Override
public String getCommandName() {
return "debug";
}
@Override
public String getDescription() {
return "Toggle debug logging types";
}
@Override
public String getUsage() {
return "<type>";
}
@Override
public boolean runSubCommands() {
return false;
}
@Override
public void processCommand(ICommandSender sender, String[] args) throws CommandException {
if (args.length == 0) {
throw new CommandException("Usage: /kam debug <type> (e.g. energy)");
}
String type = args[0].toLowerCase();
boolean newState = CNPCDebug.toggleServer(type);
sendResult(sender, "Server debug '" + type + "' " + (newState ? "\u00A7aENABLED" : "\u00A7cDISABLED"));
}
}
@@ -32,7 +32,7 @@ import kamkeel.npcs.controllers.data.ability.type.AbilityVortex;
import kamkeel.npcs.controllers.data.ability.type.energy.AbilityBeam;
import kamkeel.npcs.controllers.data.ability.type.energy.AbilityDisc;
import kamkeel.npcs.controllers.data.ability.type.energy.AbilityDome;
import kamkeel.npcs.controllers.data.ability.type.energy.AbilityLaserShot;
import kamkeel.npcs.controllers.data.ability.type.energy.AbilityLaser;
import kamkeel.npcs.controllers.data.ability.type.energy.AbilityOrb;
import kamkeel.npcs.controllers.data.ability.type.energy.AbilityShield;
import kamkeel.npcs.controllers.data.ability.type.energy.AbilitySweeper;
@@ -143,7 +143,7 @@ public class AbilityController implements IAbilityHandler {
registerType("cnpc:sweeper", AbilitySweeper::new);
registerType("cnpc:orb", AbilityOrb::new);
registerType("cnpc:disc", AbilityDisc::new);
registerType("cnpc:laser_shot", AbilityLaserShot::new);
registerType("cnpc:laser_shot", AbilityLaser::new);
registerType("cnpc:beam", AbilityBeam::new);
// registerType("cnpc:slicer", AbilitySlicer::new);
@@ -1132,10 +1132,11 @@ public class AbilityController implements IAbilityHandler {
*/
public boolean fireOnAbilityDamage(Ability ability, EntityLivingBase caster, EntityLivingBase target,
float damage, float knockback, float knockbackUp,
double knockbackDirX, double knockbackDirZ) {
double knockbackDirX, double knockbackDirZ,
float damageMultiplier) {
for (IAbilityExtender ext : extenders) {
if (ext.onAbilityDamage(ability, caster, target, damage, knockback, knockbackUp,
knockbackDirX, knockbackDirZ)) {
knockbackDirX, knockbackDirZ, damageMultiplier)) {
return true;
}
}
@@ -476,7 +476,7 @@ public abstract class Ability implements IAbility, IAbilityAction {
// Check for ability extenders (e.g., DBC Addon damage routing)
boolean handled = AbilityController.Instance.fireOnAbilityDamage(
this, caster, hitEntity, damage, knockback, knockbackUp,
knockbackDirX, knockbackDirZ);
knockbackDirX, knockbackDirZ, 1.0f);
if (!handled) {
// Default damage path
if (caster instanceof EntityNPCInterface) {
@@ -26,10 +26,12 @@ public class ProjectileData {
* If colorOverride is true, returns a copy with overridden inner/outer colors.
*/
public EnergyDisplayData resolveDisplay(EnergyDisplayData primary) {
if (!colorOverride) return primary;
// Always return a copy so entity mutations never bleed back into the ability template.
EnergyDisplayData resolved = primary.copy();
resolved.innerColor = this.innerColor;
resolved.outerColor = this.outerColor;
if (colorOverride) {
resolved.innerColor = this.innerColor;
resolved.outerColor = this.outerColor;
}
return resolved;
}
@@ -69,11 +69,15 @@ public interface IAbilityExtender {
* @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, EntityLivingBase caster, EntityLivingBase target,
float damage, float knockback, float knockbackUp,
double knockbackDirX, double knockbackDirZ) {
double knockbackDirX, double knockbackDirZ,
float damageMultiplier) {
return false;
}
@@ -128,7 +128,7 @@ public class AbilitySlam extends Ability implements IAbilitySlam {
targetZ = caster.posZ;
} else if (targetingMode == TargetingMode.AOE_TARGET && target != null && !target.isDead) {
targetX = target.posX;
targetY = target.posY;
targetY = findGroundLevel(caster.worldObj, target.posX, target.posY, target.posZ, 64);
targetZ = target.posZ;
}
}
@@ -161,7 +161,7 @@ public class AbilitySlam extends Ability implements IAbilitySlam {
targetZ = caster.posZ;
} else if (targetingMode == TargetingMode.AOE_TARGET && target != null && !target.isDead) {
targetX = target.posX;
targetY = target.posY;
targetY = findGroundLevel(caster.worldObj, target.posX, target.posY, target.posZ, 64);
targetZ = target.posZ;
} else {
targetX = caster.posX;
@@ -186,11 +186,8 @@ public class AbilitySlam extends Ability implements IAbilitySlam {
if (isAirborne) {
double groundY = findGroundLevel(caster.worldObj, caster.posX, caster.posY, caster.posZ, 64);
highEnoughForAirSlam = (caster.posY - groundY) >= 3.0;
System.out.println("[Slam] AIR CHECK: posY=" + caster.posY + " groundY=" + groundY + " dist=" + (caster.posY - groundY) + " highEnough=" + highEnoughForAirSlam);
}
System.out.println("[Slam] EXECUTE: onGround=" + caster.onGround + " isAirborne=" + isAirborne + " highEnough=" + highEnoughForAirSlam + " -> " + (isAirborne && highEnoughForAirSlam ? "AIR SLAM" : "GROUND SLAM"));
if (isAirborne && highEnoughForAirSlam) {
// Air slam: player is already airborne (flying, falling, jumping) and high enough
airSlam = true;
@@ -10,6 +10,7 @@ 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;
@@ -17,6 +18,7 @@ import kamkeel.npcs.entity.EntityEnergyProjectile;
import kamkeel.npcs.network.packets.data.energycharge.EnergyChargeRemovePacket;
import kamkeel.npcs.network.packets.data.energycharge.EnergyChargeSpawnPacket;
import kamkeel.npcs.util.AnchorPointHelper;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.DamageSource;
@@ -287,6 +289,15 @@ public abstract class AbilityEnergyProjectile<E extends EntityEnergyProjectile>
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(EntityLivingBase caster, int index) {
@@ -296,6 +307,7 @@ public abstract class AbilityEnergyProjectile<E extends EntityEnergyProjectile>
String id = chargeVisualIds[index];
if (id != null && !id.isEmpty()) {
EnergyChargeRemovePacket.sendToTracking(id, caster);
EnergyChargeTracker.Instance.remove(id, caster.getEntityId());
chargeVisualIds[index] = null;
}
}
@@ -305,6 +317,10 @@ public abstract class AbilityEnergyProjectile<E extends EntityEnergyProjectile>
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 ====================
@@ -326,6 +342,21 @@ public abstract class AbilityEnergyProjectile<E extends EntityEnergyProjectile>
spawnedCount = 0;
}
/**
* Remove a specific entity 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(Entity entity) {
if (entities == null || entity == null) return;
for (int i = 0; i < entities.length; i++) {
if (entities[i] == entity) {
entities[i] = null;
break;
}
}
}
@Override
public boolean isReadyForBurstCompletion(int activeTick) {
return fireDelay <= 0 || activeTick >= fireDelay * (projectileCount - 1);
@@ -16,39 +16,39 @@ import kamkeel.npcs.entity.EntityAbilityLaser;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.Vec3;
import noppes.npcs.api.ability.type.IAbilityLaserShot;
import noppes.npcs.api.ability.type.IAbilityLaser;
import noppes.npcs.client.gui.builder.FieldDef;
import java.util.List;
/**
* Laser Shot ability: Fast expanding thin line.
* Travels in a straight line from origin.
* 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 AbilityLaserShot extends AbilityEnergyProjectile<EntityAbilityLaser> implements IAbilityLaserShot {
public class AbilityLaser extends AbilityEnergyProjectile<EntityAbilityLaser> implements IAbilityLaser {
private float laserWidth = 0.3f;
private float expansionSpeed = 3.0f;
private int lingerTicks = 8;
private float maxLength = 32.0f;
public AbilityLaserShot() {
public AbilityLaser() {
super(
new EnergyDisplayData(0xFFFFFF, 0xFF0000, true, 0.4f, 0.5f, 0.0f),
new EnergyCombatData(6.0f, 0.5f, 0.05f, false, 2.0f, 0.5f, HitType.PIERCE, 5),
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 Shot";
this.name = "Laser";
this.targetingMode = TargetingMode.AGGRO_TARGET;
this.maxRange = 35.0f;
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;
this.rotationPhase = LockMode.WINDUP_AND_ACTIVE;
this.telegraphType = TelegraphType.LINE;
this.showTelegraph = true;
this.windUpAnimationName = "Ability_Laser_Windup";
@@ -69,7 +69,8 @@ public class AbilityLaserShot extends AbilityEnergyProjectile<EntityAbilityLaser
@Override
public int getMaxPreviewDuration() {
return lifespanData.maxLifetime > 0 ? Math.min(lifespanData.maxLifetime, 60) : 60;
int lifetime = lifespanData.maxLifetime;
return lifetime > 0 ? Math.min(lifetime, 60) : 60;
}
/**
@@ -101,7 +102,7 @@ public class AbilityLaserShot extends AbilityEnergyProjectile<EntityAbilityLaser
@Override
public float getTelegraphLength() {
return lifespanData.maxDistance;
return maxLength;
}
@Override
@@ -119,13 +120,12 @@ public class AbilityLaserShot extends AbilityEnergyProjectile<EntityAbilityLaser
spawnPos.xCoord, spawnPos.yCoord, spawnPos.zCoord,
laserWidth,
resolved, combatData, lightningData, lifespanData,
expansionSpeed, lingerTicks);
expansionSpeed, maxLength);
return laser;
}
@Override
protected void fireEntity(EntityAbilityLaser laser, EntityLivingBase target) {
laser.setLockVerticalDirection(true);
laser.startMoving(target);
}
@@ -137,7 +137,7 @@ public class AbilityLaserShot extends AbilityEnergyProjectile<EntityAbilityLaser
@Override
protected void setupEntityPreview(EntityAbilityLaser laser, EntityLivingBase caster,
EnergyDisplayData resolved, ProjectileData projData, int index) {
laser.setupPreview(caster, laserWidth, resolved, lightningData, projData.anchor, windUpTicks, expansionSpeed, lifespanData.maxDistance);
laser.setupPreview(caster, laserWidth, resolved, lightningData, projData.anchor, windUpTicks, expansionSpeed, maxLength);
}
@Override
@@ -156,14 +156,14 @@ public class AbilityLaserShot extends AbilityEnergyProjectile<EntityAbilityLaser
protected void writeTypeSpecificNBT(NBTTagCompound nbt) {
nbt.setFloat("laserWidth", laserWidth);
nbt.setFloat("expansionSpeed", expansionSpeed);
nbt.setInteger("lingerTicks", lingerTicks);
nbt.setFloat("maxLength", maxLength);
}
@Override
protected void readTypeSpecificNBT(NBTTagCompound nbt) {
this.laserWidth = nbt.getFloat("laserWidth");
this.expansionSpeed = nbt.getFloat("expansionSpeed");
this.lingerTicks = nbt.getInteger("lingerTicks");
this.maxLength = nbt.hasKey("maxLength") ? nbt.getFloat("maxLength") : 32.0f;
}
// ==================== TYPE-SPECIFIC GETTERS ====================
@@ -184,12 +184,12 @@ public class AbilityLaserShot extends AbilityEnergyProjectile<EntityAbilityLaser
this.expansionSpeed = expansionSpeed;
}
public int getLingerTicks() {
return lingerTicks;
public float getMaxLength() {
return maxLength;
}
public void setLingerTicks(int lingerTicks) {
this.lingerTicks = lingerTicks;
public void setMaxLength(float maxLength) {
this.maxLength = maxLength;
}
// ==================== TYPE-SPECIFIC GUI ====================
@@ -207,11 +207,7 @@ public class AbilityLaserShot extends AbilityEnergyProjectile<EntityAbilityLaser
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.intField("ability.lingerTicks", this::getLingerTicks, this::setLingerTicks).range(1, 200),
FieldDef.intField("ability.lifetime", this::getMaxLifetime, this::setMaxLifetime).range(1, 1200)
));
defs.add(FieldDef.floatField("ability.maxDistance", this::getMaxDistance, this::setMaxDistance).range(1.0f, 500.0f));
defs.add(FieldDef.floatField("ability.maxLength", this::getMaxLength, this::setMaxLength).range(1.0f, 500.0f));
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)
@@ -0,0 +1,112 @@
package kamkeel.npcs.controllers.data.energycharge;
import kamkeel.npcs.network.PacketHandler;
import kamkeel.npcs.network.packets.data.energycharge.EnergyChargeSpawnPacket;
import net.minecraft.entity.player.EntityPlayerMP;
import net.minecraft.nbt.NBTTagCompound;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
/**
* Server-side tracker for active charging preview visuals.
* Stores enough data to resend {@link EnergyChargeSpawnPacket} to late-joining players
* who enter tracking range after the initial broadcast.
* <p>
* Keyed by caster entity ID so {@code PlayerEvent.StartTracking} can look up
* active charges for a newly tracked entity.
*/
public class EnergyChargeTracker {
public static final EnergyChargeTracker Instance = new EnergyChargeTracker();
public static class ChargeEntry {
public final String instanceId;
public final String entityClassName;
public final NBTTagCompound spawnNbt;
public final int casterEntityId;
public final int startTick;
public ChargeEntry(String instanceId, String entityClassName,
NBTTagCompound spawnNbt, int casterEntityId, int startTick) {
this.instanceId = instanceId;
this.entityClassName = entityClassName;
this.spawnNbt = spawnNbt;
this.casterEntityId = casterEntityId;
this.startTick = startTick;
}
}
private final Map<Integer, List<ChargeEntry>> activeCharges = new HashMap<>();
public void add(ChargeEntry entry) {
List<ChargeEntry> list = activeCharges.get(entry.casterEntityId);
if (list == null) {
list = new ArrayList<>();
activeCharges.put(entry.casterEntityId, list);
}
list.add(entry);
}
public void remove(String instanceId, int casterEntityId) {
List<ChargeEntry> list = activeCharges.get(casterEntityId);
if (list == null) return;
Iterator<ChargeEntry> it = list.iterator();
while (it.hasNext()) {
if (it.next().instanceId.equals(instanceId)) {
it.remove();
break;
}
}
if (list.isEmpty()) {
activeCharges.remove(casterEntityId);
}
}
public void removeAllForCaster(int casterEntityId) {
activeCharges.remove(casterEntityId);
}
/**
* Send all active charges for a caster to a specific player.
* Adjusts ChargeTick in the NBT to reflect elapsed time so the
* late-joining client starts at the correct visual progress.
*/
public void sendToPlayer(int casterEntityId, EntityPlayerMP player, int currentWorldTick) {
List<ChargeEntry> entries = activeCharges.get(casterEntityId);
if (entries == null || entries.isEmpty()) return;
for (ChargeEntry entry : entries) {
int elapsed = currentWorldTick - entry.startTick;
int chargeDuration = entry.spawnNbt.hasKey("ChargeDuration")
? entry.spawnNbt.getInteger("ChargeDuration") : 0;
// Charge already complete — server remove packet should follow shortly
if (chargeDuration > 0 && elapsed >= chargeDuration) {
continue;
}
NBTTagCompound adjustedNbt = (NBTTagCompound) entry.spawnNbt.copy();
if (elapsed > 0) {
adjustedNbt.setInteger("ChargeTick", elapsed);
}
PacketHandler.Instance.sendToPlayer(
new EnergyChargeSpawnPacket(entry.instanceId, entry.entityClassName, adjustedNbt),
player
);
}
}
public boolean hasCharges(int casterEntityId) {
List<ChargeEntry> list = activeCharges.get(casterEntityId);
return list != null && !list.isEmpty();
}
public void clear() {
activeCharges.clear();
}
}
@@ -63,6 +63,9 @@ public class CNPCData implements IProfileData {
if (playerData.partyUUID != null)
return ProfileOperation.error("Cannot switch while in Party");
if (playerData.abilityData.isExecutingAbility())
return ProfileOperation.error("Cannot switch while performing an Ability");
return ProfileOperation.success("");
}
@@ -2,6 +2,7 @@ package kamkeel.npcs.entity;
import kamkeel.npcs.controllers.data.ability.util.AbilityTargetHelper;
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyBarrierData;
import kamkeel.npcs.util.CNPCDebug;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.entity.player.EntityPlayer;
@@ -617,4 +618,31 @@ public abstract class EntityAbilityBarrier extends EntityEnergyAbility {
this.dataWatcher.updateObject(DW_CHARGING, (byte) (charging ? 1 : 0));
barrierData.readNBT(nbt);
}
// ==================== DEBUG LOGGING ====================
/**
* Debug log called every tick for barriers. Subclasses override debugLogBarrierExtra().
*/
protected void debugLogBarrierTick() {
boolean isClient = worldObj.isRemote;
if (isClient ? !CNPCDebug.isClientEnabled("energy") : !CNPCDebug.isServerEnabled("energy"))
return;
String className = getClass().getSimpleName();
String base = String.format("[%s id=%d tick=%d] pos=(%.2f,%.2f,%.2f) charging=%b health=%.1f",
className, getEntityId(), ticksAlive,
posX, posY, posZ, isCharging(), currentHealth);
String extra = debugLogBarrierExtra();
String full = extra.isEmpty() ? base : base + " " + extra;
CNPCDebug.log("energy", isClient, full);
}
/**
* Subclass hook for barrier-specific debug data.
*/
protected String debugLogBarrierExtra() {
return "";
}
}
@@ -16,7 +16,6 @@ import net.minecraft.nbt.NBTTagList;
import net.minecraft.util.AxisAlignedBB;
import net.minecraft.util.Vec3;
import net.minecraft.world.World;
import noppes.npcs.LogWriter;
import java.util.ArrayList;
import java.util.Collections;
@@ -61,9 +60,6 @@ public class EntityAbilityBeam extends EntityEnergyProjectile {
private int trailFadeTime = 20; // Ticks for trail to fully fade
private List<Integer> trailPointAges = new ArrayList<>();
// Debug logging
private static final boolean DEBUG_LOGGING = false;
public EntityAbilityBeam(World world) {
super(world);
}
@@ -147,7 +143,7 @@ public class EntityAbilityBeam extends EntityEnergyProjectile {
// Beam-specific properties from homing data
this.homingData = homing;
this.homingData = homing != null ? homing.copy() : new EnergyHomingData();
this.beamWidth = beamWidth;
this.headSize = headSize;
@@ -160,10 +156,6 @@ public class EntityAbilityBeam extends EntityEnergyProjectile {
// Add initial trail point at origin (relative 0,0,0)
trailPoints.add(Vec3.createVectorHelper(0, 0, 0));
if (DEBUG_LOGGING && !world.isRemote) {
LogWriter.info("[Beam] Created at origin " + x + ", " + y + ", " + z + " maxDist=" + lifespan.maxDistance);
}
// Calculate initial velocity toward target
calculateInitialVelocity(owner, target, x, y, z);
}
@@ -222,11 +214,6 @@ public class EntityAbilityBeam extends EntityEnergyProjectile {
trailPoints.add(Vec3.createVectorHelper(0, 0, 0));
if (isFadingMode()) trailPointAges.add(0);
if (DEBUG_LOGGING && !worldObj.isRemote) {
LogWriter.info("[Beam] startFiring: origin=(" + startX + "," + startY + "," + startZ +
") headOffset=(" + headOffsetX + "," + headOffsetY + "," + headOffsetZ +
") motion=(" + motionX + "," + motionY + "," + motionZ + ")");
}
}
@Override
@@ -237,11 +224,7 @@ public class EntityAbilityBeam extends EntityEnergyProjectile {
headOffsetY * headOffsetY +
headOffsetZ * headOffsetZ
);
boolean exceeded = distFromOrigin >= getMaxDistance();
if (exceeded && !worldObj.isRemote && DEBUG_LOGGING) {
LogWriter.info("[Beam] DEAD: Max distance exceeded. dist=" + distFromOrigin + " max=" + getMaxDistance());
}
return exceeded;
return distFromOrigin >= getMaxDistance();
}
@Override
@@ -312,17 +295,6 @@ public class EntityAbilityBeam extends EntityEnergyProjectile {
// Add trail point
addTrailPoint();
// Debug log periodically
if (DEBUG_LOGGING && ticksExisted % 20 == 0) {
LogWriter.info("[Beam] Server tick=" + ticksExisted + " headOffset=(" +
String.format("%.2f", headOffsetX) + "," +
String.format("%.2f", headOffsetY) + "," +
String.format("%.2f", headOffsetZ) + ") origin=(" +
String.format("%.2f", startX) + "," +
String.format("%.2f", startY) + "," +
String.format("%.2f", startZ) + ") trail=" + getActiveTrailSize());
}
// Skip collision checks on first few ticks
if (ticksExisted > 2) {
checkBlockCollision(headWorldX, headWorldY, headWorldZ);
@@ -528,9 +500,27 @@ public class EntityAbilityBeam extends EntityEnergyProjectile {
Math.max(prevHeadWorldZ, headZ) + hitSize
);
if (processEntitiesInHitBox(hitBox, headX, headY, headZ) && DEBUG_LOGGING) {
LogWriter.info("[Beam] DEAD: Entity collision at tick " + ticksExisted);
}
processEntitiesInHitBox(hitBox, headX, headY, headZ);
}
// ==================== DEBUG ====================
@Override
protected String debugLogExtra() {
double headWorldX = startX + headOffsetX;
double headWorldY = startY + headOffsetY;
double headWorldZ = startZ + headOffsetZ;
return String.format("headOff=(%.2f,%.2f,%.2f) prevHead=(%.2f,%.2f,%.2f) " +
"origin=(%.2f,%.2f,%.2f) headWorld=(%.2f,%.2f,%.2f) " +
"trail=%d mode=%s anchored=%b beamW=%.2f headSz=%.2f " +
"motion=(%.3f,%.3f,%.3f)",
headOffsetX, headOffsetY, headOffsetZ,
prevHeadOffsetX, prevHeadOffsetY, prevHeadOffsetZ,
startX, startY, startZ,
headWorldX, headWorldY, headWorldZ,
getActiveTrailSize(),
beamMode.name(), isAnchoredMode(), beamWidth, headSize,
motionX, motionY, motionZ);
}
// ==================== GETTERS FOR RENDERER ====================
@@ -637,6 +627,50 @@ public class EntityAbilityBeam extends EntityEnergyProjectile {
return trailFadeTime;
}
// ==================== REFLECTION ====================
@Override
protected boolean reflectFromBarrier(EntityAbilityBarrier barrier, float reflectStrengthPct) {
boolean reflected = super.reflectFromBarrier(barrier, reflectStrengthPct);
if (reflected) {
// Switch to FREE_TRAIL: detach from caster (tail-less), trail fades out.
setBeamMode(BeamMode.FREE_TRAIL);
// Reset origin to the reflection point (current head position).
startX = posX;
startY = posY;
startZ = posZ;
resetHeadOffsets();
resetTrailStorage();
// Disable homing — reflected beams fly straight.
homingData.setHoming(false);
setTargetEntityId(-1);
}
return reflected;
}
@Override
protected void writeProjectileReflectionData(NBTTagCompound nbt) {
nbt.setByte("BeamMode", (byte) beamMode.ordinal());
nbt.setDouble("StartX", startX);
nbt.setDouble("StartY", startY);
nbt.setDouble("StartZ", startZ);
}
@Override
protected void applyProjectileReflectionData(NBTTagCompound nbt) {
int mode = nbt.getByte("BeamMode");
setBeamMode(mode >= 0 && mode < BeamMode.values().length ? BeamMode.values()[mode] : BeamMode.FREE_TRAIL);
startX = nbt.getDouble("StartX");
startY = nbt.getDouble("StartY");
startZ = nbt.getDouble("StartZ");
resetHeadOffsets();
resetTrailStorage();
homingData.setHoming(false);
setTargetEntityId(-1);
}
// ==================== NBT ====================
@Override
@@ -64,7 +64,7 @@ public class EntityAbilityDisc extends EntityEnergyProjectile {
// Initialize base properties
initProjectile(owner, target, x, y, z, 1.0f, display, combat, lightning, lifespan);
this.homingData = homing;
this.homingData = homing != null ? homing.copy() : new EnergyHomingData();
this.boomerang = boomerang;
this.boomerangDelay = boomerangDelay;
@@ -330,6 +330,17 @@ public class EntityAbilityDisc extends EntityEnergyProjectile {
updateRotation();
}
// ==================== DEBUG ====================
@Override
protected String debugLogExtra() {
return String.format("motion=(%.3f,%.3f,%.3f) radius=%.2f thickness=%.2f " +
"boomerang=%b returning=%b vertical=%b ticksSinceMiss=%d",
motionX, motionY, motionZ,
discRadius, discThickness,
boomerang, returning, vertical, ticksSinceMiss);
}
// ==================== GETTERS ====================
public float getDiscRadius() {
@@ -395,6 +406,32 @@ public class EntityAbilityDisc extends EntityEnergyProjectile {
return lastTravelYaw;
}
// ==================== REFLECTION ====================
@Override
protected boolean reflectFromBarrier(EntityAbilityBarrier barrier, float reflectStrengthPct) {
boolean reflected = super.reflectFromBarrier(barrier, reflectStrengthPct);
if (reflected) {
// Disable boomerang on reflected discs — the original owner is no longer
// relevant and the return-to-owner logic causes erratic flight paths.
this.boomerang = false;
this.returning = false;
}
return reflected;
}
@Override
protected void writeProjectileReflectionData(NBTTagCompound nbt) {
nbt.setBoolean("Boomerang", boomerang);
nbt.setBoolean("Returning", returning);
}
@Override
protected void applyProjectileReflectionData(NBTTagCompound nbt) {
boomerang = nbt.getBoolean("Boomerang");
returning = nbt.getBoolean("Returning");
}
// ==================== NBT ====================
@Override
@@ -50,10 +50,11 @@ public class EntityAbilityDome extends EntityAbilityBarrier {
this.stepHeight = 0.0F;
this.ownerEntityId = owner.getEntityId();
this.setDomeRadius(domeRadius);
this.displayData = display;
this.lightningData = lightning;
this.barrierData = barrier;
this.currentHealth = barrier.maxHealth;
// Defensive copy: entities must never share data objects with the source ability.
this.displayData = display != null ? display.copy() : new EnergyDisplayData();
this.lightningData = lightning != null ? lightning.copy() : new EnergyLightningData();
this.barrierData = barrier != null ? barrier.copy() : new EnergyBarrierData();
this.currentHealth = this.barrierData.maxHealth;
this.setPosition(x, y, z);
}
@@ -121,6 +122,8 @@ public class EntityAbilityDome extends EntityAbilityBarrier {
if (worldObj.isRemote && barrierData.meleeEnabled && !isCharging()) {
adjustMeleeBoundingBox();
}
debugLogBarrierTick();
}
// ==================== INCOMING CHECK ====================
@@ -296,6 +299,14 @@ public class EntityAbilityDome extends EntityAbilityBarrier {
double surfaceX = posX + nx * pushDist;
double surfaceY = posY + ny * pushDist - ent.height * 0.5;
double surfaceZ = posZ + nz * pushDist;
// Prevent grounded entities from being teleported below the floor.
// The spherical surface calculation can produce a Y below ground level
// near the dome's equator, and setPosition bypasses collision detection.
if (ent.onGround && surfaceY < ent.posY) {
surfaceY = ent.posY;
}
teleportEntity(ent, surfaceX, surfaceY, surfaceZ);
// Cancel radial velocity and add corrective push
@@ -547,6 +558,14 @@ public class EntityAbilityDome extends EntityAbilityBarrier {
return surfaceDist * surfaceDist;
}
// ==================== DEBUG ====================
@Override
protected String debugLogBarrierExtra() {
return String.format("radius=%.2f targetRadius=%.2f follow=%b",
domeRadius, targetDomeRadius, followCaster);
}
// ==================== GETTERS ====================
public float getDomeRadius() {
@@ -1,5 +1,6 @@
package kamkeel.npcs.entity;
import kamkeel.npcs.controllers.data.ability.type.energy.AbilityEnergyProjectile;
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyAnchorData;
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyCombatData;
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyDisplayData;
@@ -20,8 +21,10 @@ import noppes.npcs.entity.EntityNPCInterface;
import java.util.List;
/**
* Laser projectile - fast expanding thin line.
* No homing, travels in a straight line from origin to max distance.
* Laser entity - sweeping beam that follows the caster's look vector.
* Expands from origin at expansionSpeed until reaching maxLength,
* then stays active until maxLifetime expires. Always tracks the owner's
* look direction so the beam can be swept mid-shot.
* <p>
* Design inspired by LouisXIV's energy attack system.
*/
@@ -30,23 +33,26 @@ public class EntityAbilityLaser extends EntityEnergyProjectile {
// Laser-specific properties
private float laserWidth = 0.2f;
private float expansionSpeed = 2.0f; // Blocks per tick
private int lingerTicks = 10; // How long laser stays visible after reaching max
private float maxLength = 32.0f; // Maximum beam reach in blocks
// State
private float currentLength = 0.0f;
private float desiredLength = 0.0f; // Intended beam reach (grows to maxLength)
private float currentLength = 0.0f; // Actual length after block truncation (visual + collision)
private boolean fullyExtended = false;
private int ticksSinceFullExtension = 0;
// Direction (normalized)
private double dirX, dirY, dirZ;
// Lock vertical direction after firing (only update yaw, keep pitch fixed)
private boolean lockVerticalDirection = false;
private int reflectedLockTicks = 0;
// End point for rendering
private double endX, endY, endZ;
// Block hit deduplication — prevents event/explosion spam every tick
private int lastBlockHitX = Integer.MIN_VALUE;
private int lastBlockHitY = Integer.MIN_VALUE;
private int lastBlockHitZ = Integer.MIN_VALUE;
private int lastExplosionTick = -100;
private static final int EXPLOSION_COOLDOWN = 10; // ticks between explosive impacts
public EntityAbilityLaser(World world) {
super(world);
}
@@ -59,17 +65,15 @@ public class EntityAbilityLaser extends EntityEnergyProjectile {
float laserWidth,
EnergyDisplayData display, EnergyCombatData combat,
EnergyLightningData lightning, EnergyLifespanData lifespan,
float expansionSpeed, int lingerTicks) {
float expansionSpeed, float maxLength) {
super(world);
// Initialize base properties (laser doesn't rotate)
initProjectile(owner, target, x, y, z, laserWidth, display, combat, lightning, lifespan);
this.displayData.rotationSpeed = 0.0f;
// Laser-specific properties
this.laserWidth = laserWidth;
this.expansionSpeed = expansionSpeed;
this.lingerTicks = lingerTicks;
this.maxLength = maxLength;
// Calculate direction toward target or forward
if (target != null) {
@@ -117,12 +121,12 @@ public class EntityAbilityLaser extends EntityEnergyProjectile {
@Override
protected void updateRotation() {
// Laser doesn't rotate
// Beam rotation is handled in the renderer via perpendicular vector rotation
}
@Override
protected boolean checkMaxDistance() {
// Laser handles distance differently - it expands to max then lingers
// Laser handles distance via maxLength, not max distance
return false;
}
@@ -133,66 +137,57 @@ public class EntityAbilityLaser extends EntityEnergyProjectile {
return;
}
// After reflection, hold trajectory briefly so the rebound is visible.
if (reflectedLockTicks > 0) {
reflectedLockTicks--;
} else {
// Track owner's anchor position and rotation each tick
// so the laser follows the NPC's facing direction
// Origin/direction tracking runs on BOTH sides so the client can render the beam.
// Once reflected, never resume owner-tracking — the laser flies in a fixed direction.
if (!reflected) {
updateLaserOriginAndDirection();
}
// Expand the intended reach — runs on both sides for visual sync
if (!fullyExtended) {
// Expand the laser
currentLength += expansionSpeed;
if (currentLength >= getMaxDistance()) {
currentLength = getMaxDistance();
desiredLength += expansionSpeed;
if (desiredLength >= maxLength) {
desiredLength = maxLength;
fullyExtended = true;
}
}
// Update end point
endX = startX + dirX * currentLength;
endY = startY + dirY * currentLength;
endZ = startZ + dirZ * currentLength;
// Start from full desired length, then truncate at block hits
currentLength = desiredLength;
// Check for block collision along the new segment
if (!worldObj.isRemote) {
checkBlockCollision();
// Re-evaluate barriers against the current beam segment before entity hits.
// This prevents first-tick pass-through where the pre-update barrier check
// still sees a zero-length beam.
if (checkBarrierCollisionForCurrentSegment()) {
return;
}
checkEntityCollisionAlongLine();
// Block raytrace — truncate beam at first solid block (both sides)
checkBlockCollision();
// Update end point
updateEndPoint();
// Server-only: barrier collision, entity collision, death
if (!worldObj.isRemote) {
if (checkLaserBarrierCollision()) {
return;
}
} else {
// Laser is fully extended, count down linger time
ticksSinceFullExtension++;
// Update end point during linger (origin/direction may still change)
endX = startX + dirX * currentLength;
endY = startY + dirY * currentLength;
endZ = startZ + dirZ * currentLength;
if (ticksSinceFullExtension >= lingerTicks) {
if (!worldObj.isRemote) {
EventHooks.onEnergyProjectileExpired(this);
}
this.setDead();
}
checkEntityCollisionAlongLine();
// Death handled by base class deathWorldTime (from lifespanData.maxLifetime)
}
}
/**
* Update end point from current origin + direction * length.
*/
private void updateEndPoint() {
endX = startX + dirX * currentLength;
endY = startY + dirY * currentLength;
endZ = startZ + dirZ * currentLength;
}
/**
* Update laser origin and direction from the owner's current look rotation.
* Called each tick so the laser origin stays centered on look vector.
* Called each tick so the laser follows the caster's aim (sweeping beam).
*/
private void updateLaserOriginAndDirection() {
Entity owner = getOwnerEntity();
if (owner == null || !(owner instanceof EntityLivingBase)) {
// On client, fall back to entity tracker interpolation when owner not loaded
if (worldObj != null && worldObj.isRemote) {
handleClientInterpolation();
}
@@ -200,7 +195,7 @@ public class EntityAbilityLaser extends EntityEnergyProjectile {
}
EntityLivingBase livingOwner = (EntityLivingBase) owner;
// Use owner look vector (head/eye aim) for direction updates so hit logic matches visuals.
// Get owner look vector for direction
Vec3 look = livingOwner.getLookVec();
if (look == null) {
float yaw = (float) Math.toRadians(owner.rotationYaw);
@@ -212,42 +207,28 @@ public class EntityAbilityLaser extends EntityEnergyProjectile {
);
}
if (lockVerticalDirection) {
// Keep current vertical component, but align horizontal direction to current look.
double lookHorizontalLen = Math.sqrt(look.xCoord * look.xCoord + look.zCoord * look.zCoord);
if (lookHorizontalLen > 0.0001) {
double currentHorizontalLen = Math.sqrt(dirX * dirX + dirZ * dirZ);
if (currentHorizontalLen < 0.0001) {
currentHorizontalLen = Math.sqrt(Math.max(0.0001, 1.0 - dirY * dirY));
}
dirX = (look.xCoord / lookHorizontalLen) * currentHorizontalLen;
dirZ = (look.zCoord / lookHorizontalLen) * currentHorizontalLen;
}
} else {
dirX = look.xCoord;
dirY = look.yCoord;
dirZ = look.zCoord;
}
// Set direction directly from look vector (no lockVerticalDirection split)
dirX = look.xCoord;
dirY = look.yCoord;
dirZ = look.zCoord;
// Keep direction normalized for consistent expansion and collision math.
Vec3 direction = Vec3.createVectorHelper(dirX, dirY, dirZ);
double dirLen = Math.sqrt(direction.xCoord * direction.xCoord + direction.yCoord * direction.yCoord + direction.zCoord * direction.zCoord);
// Keep direction normalized
double dirLen = Math.sqrt(dirX * dirX + dirY * dirY + dirZ * dirZ);
if (dirLen > 0.0001) {
direction = Vec3.createVectorHelper(
direction.xCoord / dirLen,
direction.yCoord / dirLen,
direction.zCoord / dirLen
);
dirX /= dirLen;
dirY /= dirLen;
dirZ /= dirLen;
} else {
direction = look != null ? look : Vec3.createVectorHelper(1.0, 0.0, 0.0);
dirX = 1.0;
dirY = 0.0;
dirZ = 0.0;
}
dirX = direction.xCoord;
dirY = direction.yCoord;
dirZ = direction.zCoord;
// Keep origin centered on look vector while active and clear owner bbox.
Vec3 direction = Vec3.createVectorHelper(dirX, dirY, dirZ);
setLookVectorLaunchPosition(livingOwner, direction, false);
// Lower beam origin slightly so it doesn't obscure the player's crosshair
posY -= 0.15;
syncStartPositionToCurrent();
syncPositionStateToCurrent(false);
}
@@ -255,22 +236,24 @@ public class EntityAbilityLaser extends EntityEnergyProjectile {
public void startMoving(EntityLivingBase target) {
beginLookVectorLaunch(false);
desiredLength = 0.0f;
currentLength = 0.0f;
fullyExtended = false;
ticksSinceFullExtension = 0;
lastBlockHitX = Integer.MIN_VALUE;
lastBlockHitY = Integer.MIN_VALUE;
lastBlockHitZ = Integer.MIN_VALUE;
lastExplosionTick = -100;
// Set initial direction: target-based when available, otherwise owner look vector.
// Set initial direction: owner look vector (sweeping beam always follows look)
Vec3 look = getOwnerLookVector();
if (!setDirectionTowardTarget(target, startX, startY, startZ)) {
if (look != null) {
dirX = look.xCoord;
dirY = look.yCoord;
dirZ = look.zCoord;
} else {
dirX = 1.0;
dirY = 0.0;
dirZ = 0.0;
}
if (look != null) {
dirX = look.xCoord;
dirY = look.yCoord;
dirZ = look.zCoord;
} else if (!setDirectionTowardTarget(target, startX, startY, startZ)) {
dirX = 1.0;
dirY = 0.0;
dirZ = 0.0;
}
// Initialize end point at start (will expand from here)
@@ -294,40 +277,69 @@ public class EntityAbilityLaser extends EntityEnergyProjectile {
}
private void checkBlockCollision() {
MovingObjectPosition blockHit = rayTraceBlocks(startX, startY, startZ, endX, endY, endZ);
if (currentLength <= 0) return;
double traceEndX = startX + dirX * currentLength;
double traceEndY = startY + dirY * currentLength;
double traceEndZ = startZ + dirZ * currentLength;
MovingObjectPosition blockHit = rayTraceBlocks(startX, startY, startZ, traceEndX, traceEndY, traceEndZ);
if (blockHit != null && blockHit.typeOfHit == MovingObjectPosition.MovingObjectType.BLOCK) {
if (!worldObj.isRemote) {
EventHooks.onEnergyProjectileBlockImpact(this, blockHit.blockX, blockHit.blockY, blockHit.blockZ);
}
// Laser stops at block
currentLength = (float) Math.sqrt(
// Truncate beam at block hit
float hitDist = (float) Math.sqrt(
(blockHit.hitVec.xCoord - startX) * (blockHit.hitVec.xCoord - startX) +
(blockHit.hitVec.yCoord - startY) * (blockHit.hitVec.yCoord - startY) +
(blockHit.hitVec.zCoord - startZ) * (blockHit.hitVec.zCoord - startZ)
);
endX = blockHit.hitVec.xCoord;
endY = blockHit.hitVec.yCoord;
endZ = blockHit.hitVec.zCoord;
fullyExtended = true;
if (isExplosive()) {
// Explode at hit point
double oldPosX = posX;
double oldPosY = posY;
double oldPosZ = posZ;
posX = endX;
posY = endY;
posZ = endZ;
doExplosion();
posX = oldPosX;
posY = oldPosY;
posZ = oldPosZ;
if (hitDist < currentLength) {
currentLength = hitDist;
endX = blockHit.hitVec.xCoord;
endY = blockHit.hitVec.yCoord;
endZ = blockHit.hitVec.zCoord;
}
// Server-only: fire event and explosion only when block changes or cooldown expires
if (!worldObj.isRemote) {
int bx = blockHit.blockX;
int by = blockHit.blockY;
int bz = blockHit.blockZ;
boolean newBlock = (bx != lastBlockHitX || by != lastBlockHitY || bz != lastBlockHitZ);
// Fire block impact event only when the beam hits a different block
if (newBlock) {
lastBlockHitX = bx;
lastBlockHitY = by;
lastBlockHitZ = bz;
EventHooks.onEnergyProjectileBlockImpact(this, bx, by, bz);
}
// Explosive: fire on cooldown so sweeping doesn't explode every tick
if (isExplosive() && (ticksExisted - lastExplosionTick) >= EXPLOSION_COOLDOWN) {
lastExplosionTick = ticksExisted;
double oldPosX = posX;
double oldPosY = posY;
double oldPosZ = posZ;
posX = endX;
posY = endY;
posZ = endZ;
doExplosion();
posX = oldPosX;
posY = oldPosY;
posZ = oldPosZ;
}
}
} else if (!worldObj.isRemote) {
// Beam is not hitting any block — reset tracking so next block contact triggers fresh
lastBlockHitX = Integer.MIN_VALUE;
lastBlockHitY = Integer.MIN_VALUE;
lastBlockHitZ = Integer.MIN_VALUE;
}
}
private void checkEntityCollisionAlongLine() {
if (currentLength <= 0) return;
// Expand search AABB by entity sizes to ensure we find all potential targets
double expand = Math.max(laserWidth, 1.0);
double minX = Math.min(startX, endX) - expand;
@@ -349,7 +361,7 @@ public class EntityAbilityLaser extends EntityEnergyProjectile {
// Check if entity's bounding box intersects with the laser line
if (isEntityOnLine(entity)) {
if (processEntityHit(entity, entity.posX, entity.posY + entity.height * 0.5, entity.posZ)) {
// Stop at impact point and terminate
// SINGLE hit or maxHits reached — truncate beam at impact and die
double dx = entity.posX - startX;
double dy = (entity.posY + entity.height * 0.5) - startY;
double dz = entity.posZ - startZ;
@@ -360,8 +372,7 @@ public class EntityAbilityLaser extends EntityEnergyProjectile {
endZ = startZ + dirZ * currentLength;
return;
}
// PIERCE/MULTI continue through remaining entities.
// MULTI/PIERCE continue through remaining entities
}
}
}
@@ -391,24 +402,22 @@ public class EntityAbilityLaser extends EntityEnergyProjectile {
// ==================== BARRIER COLLISION ====================
/**
* Override barrier collision for lasers. The base class uses projectile position/velocity
* which doesn't work for lasers (posX/Y/Z stays at origin, motionX/Y/Z is zero).
* Instead, performs line-sphere intersection against domes.
* Disable the base class barrier check — it uses position/velocity which doesn't work
* for lasers (posX/Y/Z stays at origin, motionX/Y/Z is zero). We handle barriers
* exclusively in updateProjectile() via checkLaserBarrierCollision().
*/
@Override
@SuppressWarnings("unchecked")
protected boolean checkBarrierCollision() {
return checkBarrierCollisionInternal(false);
}
private boolean checkBarrierCollisionForCurrentSegment() {
return checkBarrierCollisionInternal(true);
return false;
}
/**
* Laser-specific barrier collision using line-segment intersection.
* Called from updateProjectile() every tick.
*/
@SuppressWarnings("unchecked")
private boolean checkBarrierCollisionInternal(boolean allowWhenFullyExtended) {
private boolean checkLaserBarrierCollision() {
if (currentLength <= 0) return false;
if (fullyExtended && !allowWhenFullyExtended) return false;
List<EntityAbilityBarrier> barriers = EntityAbilityBarrier.getActiveBarriers(worldObj);
for (EntityAbilityBarrier barrier : barriers) {
@@ -441,15 +450,13 @@ public class EntityAbilityLaser extends EntityEnergyProjectile {
endZ = startZ + dirZ * currentLength;
}
if (handleBarrierHitOutcome(dome, hitOutcome)) {
if (handleBarrierHitOutcome(dome, hitOutcome, damage)) {
return true;
}
return false;
}
} else {
// For line-based lasers, use a swept segment against generic barrier checks.
// Panel/Dome logic expects previous+current positions and motion; map those
// to [start -> end] each tick.
double segMotionX = endX - startX;
double segMotionY = endY - startY;
double segMotionZ = endZ - startZ;
@@ -468,13 +475,12 @@ public class EntityAbilityLaser extends EntityEnergyProjectile {
}
if (hitOutcome.result != EntityAbilityBarrier.ProjectileHitResult.BROKEN) {
// Keep current segment length at the exact collision tick position.
endX = startX + dirX * currentLength;
endY = startY + dirY * currentLength;
endZ = startZ + dirZ * currentLength;
}
if (handleBarrierHitOutcome(barrier, hitOutcome)) {
if (handleBarrierHitOutcome(barrier, hitOutcome, damage)) {
return true;
}
return false;
@@ -539,17 +545,29 @@ public class EntityAbilityLaser extends EntityEnergyProjectile {
setPosition(startX, startY, startZ);
syncPositionStateToCurrent(true);
desiredLength = 0.0f;
currentLength = 0.0f;
fullyExtended = false;
ticksSinceFullExtension = 0;
endX = startX;
endY = startY;
endZ = startZ;
lastBlockHitX = Integer.MIN_VALUE;
lastBlockHitY = Integer.MIN_VALUE;
lastBlockHitZ = Integer.MIN_VALUE;
lastExplosionTick = -100;
// Reset lifetime so reflected laser gets a fresh maxLifetime from the base class
deathWorldTime = -1;
reflectedLockTicks = Math.max(reflectedLockTicks, 10);
barrierImpactPauseTicks = 0;
barrierImpactDestroyOnResume = false;
// Detach from source ability so the caster is freed from active-phase locks.
if (sourceAbility instanceof AbilityEnergyProjectile) {
((AbilityEnergyProjectile<?>) sourceAbility).detachEntity(this);
}
sourceAbility = null;
Entity barrierOwner = barrier.getOwnerEntity();
if (barrierOwner != null) {
setOwnerEntityId(barrierOwner.getEntityId());
@@ -566,6 +584,7 @@ public class EntityAbilityLaser extends EntityEnergyProjectile {
hitOnceEntities.clear();
lastHitTickByEntity.clear();
reflected = true;
return true;
}
@@ -588,7 +607,6 @@ public class EntityAbilityLaser extends EntityEnergyProjectile {
if (originDistSq < (double) radius * radius) return -1;
// Solve: |start + t*dir - C|^2 = R^2
// a*t^2 + b*t + c = 0
double a = dirX * dirX + dirY * dirY + dirZ * dirZ;
double b = 2.0 * (dirX * ocX + dirY * ocY + dirZ * ocZ);
double c = originDistSq - (double) radius * radius;
@@ -606,15 +624,18 @@ public class EntityAbilityLaser extends EntityEnergyProjectile {
return -1;
}
// ==================== GETTERS FOR RENDERER ====================
// ==================== DEBUG ====================
public void setLockVerticalDirection(boolean lock) {
this.lockVerticalDirection = lock;
@Override
protected String debugLogExtra() {
return String.format("dir=(%.3f,%.3f,%.3f) desired=%.2f current=%.2f max=%.2f " +
"fullyExtended=%b end=(%.2f,%.2f,%.2f) width=%.2f expSpd=%.2f origin=(%.2f,%.2f,%.2f)",
dirX, dirY, dirZ, desiredLength, currentLength, maxLength,
fullyExtended, endX, endY, endZ, laserWidth, expansionSpeed,
startX, startY, startZ);
}
public boolean isLockVerticalDirection() {
return lockVerticalDirection;
}
// ==================== GETTERS / SETTERS ====================
public float getLaserWidth() {
return laserWidth;
@@ -632,12 +653,12 @@ public class EntityAbilityLaser extends EntityEnergyProjectile {
this.expansionSpeed = speed;
}
public int getLingerTicks() {
return lingerTicks;
public float getMaxLength() {
return maxLength;
}
public void setLingerTicks(int ticks) {
this.lingerTicks = ticks;
public void setMaxLength(float maxLength) {
this.maxLength = maxLength;
}
public void setDirection(double x, double y, double z) {
@@ -690,15 +711,6 @@ public class EntityAbilityLaser extends EntityEnergyProjectile {
return dirZ;
}
/**
* Get the alpha for fade-out effect during linger.
*/
public float getLingerAlpha() {
if (!fullyExtended) return 1.0f;
if (lingerTicks <= 0) return 0.0f;
return Math.max(0.0f, 1.0f - ((float) ticksSinceFullExtension / lingerTicks));
}
@Override
protected float getLaunchClearanceRadius() {
return Math.max(0.1f, laserWidth * 0.5f);
@@ -706,17 +718,53 @@ public class EntityAbilityLaser extends EntityEnergyProjectile {
@Override
public void setupCharging(EnergyAnchorData anchor, int chargeDuration) {
// Ensure charging visuals grow from a small orb based on laser width.
this.size = laserWidth;
// Charge orb grows to half the beam width
this.size = laserWidth * 0.5f;
this.desiredLength = 0.0f;
this.currentLength = 0.0f;
this.fullyExtended = false;
this.ticksSinceFullExtension = 0;
super.setupCharging(anchor, chargeDuration);
this.endX = posX;
this.endY = posY;
this.endZ = posZ;
}
// ==================== REFLECTION SYNC ====================
@Override
protected void writeProjectileReflectionData(NBTTagCompound nbt) {
nbt.setDouble("DirX", dirX);
nbt.setDouble("DirY", dirY);
nbt.setDouble("DirZ", dirZ);
nbt.setDouble("StartX", startX);
nbt.setDouble("StartY", startY);
nbt.setDouble("StartZ", startZ);
nbt.setFloat("DesiredLength", desiredLength);
nbt.setFloat("CurrentLength", currentLength);
nbt.setDouble("EndX", endX);
nbt.setDouble("EndY", endY);
nbt.setDouble("EndZ", endZ);
nbt.setBoolean("FullyExtended", fullyExtended);
nbt.setFloat("MaxLength", maxLength);
}
@Override
protected void applyProjectileReflectionData(NBTTagCompound nbt) {
dirX = nbt.getDouble("DirX");
dirY = nbt.getDouble("DirY");
dirZ = nbt.getDouble("DirZ");
startX = nbt.getDouble("StartX");
startY = nbt.getDouble("StartY");
startZ = nbt.getDouble("StartZ");
desiredLength = nbt.getFloat("DesiredLength");
currentLength = nbt.getFloat("CurrentLength");
endX = nbt.getDouble("EndX");
endY = nbt.getDouble("EndY");
endZ = nbt.getDouble("EndZ");
fullyExtended = nbt.getBoolean("FullyExtended");
if (nbt.hasKey("MaxLength")) maxLength = nbt.getFloat("MaxLength");
}
// ==================== NBT ====================
@Override
@@ -724,13 +772,12 @@ public class EntityAbilityLaser extends EntityEnergyProjectile {
this.laserWidth = sanitize(nbt.hasKey("LaserWidth") ? nbt.getFloat("LaserWidth") : 0.2f, 0.2f, MAX_ENTITY_SIZE);
this.expansionSpeed = nbt.hasKey("ExpansionSpeed") ? nbt.getFloat("ExpansionSpeed") : 2.0f;
if (Float.isNaN(expansionSpeed) || Float.isInfinite(expansionSpeed) || expansionSpeed <= 0) expansionSpeed = 2.0f;
this.lingerTicks = nbt.hasKey("LingerTicks") ? nbt.getInteger("LingerTicks") : 10;
if (lingerTicks <= 0) lingerTicks = 1;
this.lockVerticalDirection = nbt.getBoolean("LockVerticalDir");
this.ticksSinceFullExtension = nbt.getInteger("TicksSinceExtended");
this.maxLength = nbt.hasKey("MaxLength") ? nbt.getFloat("MaxLength") : 32.0f;
if (Float.isNaN(maxLength) || Float.isInfinite(maxLength) || maxLength <= 0) maxLength = 32.0f;
this.dirX = nbt.getDouble("DirX");
this.dirY = nbt.getDouble("DirY");
this.dirZ = nbt.getDouble("DirZ");
this.desiredLength = nbt.getFloat("DesiredLength");
this.currentLength = nbt.getFloat("CurrentLength");
this.fullyExtended = nbt.getBoolean("FullyExtended");
this.endX = nbt.getDouble("EndX");
@@ -744,12 +791,11 @@ public class EntityAbilityLaser extends EntityEnergyProjectile {
protected void writeProjectileNBT(NBTTagCompound nbt) {
nbt.setFloat("LaserWidth", laserWidth);
nbt.setFloat("ExpansionSpeed", expansionSpeed);
nbt.setInteger("LingerTicks", lingerTicks);
nbt.setBoolean("LockVerticalDir", lockVerticalDirection);
nbt.setInteger("TicksSinceExtended", ticksSinceFullExtension);
nbt.setFloat("MaxLength", maxLength);
nbt.setDouble("DirX", dirX);
nbt.setDouble("DirY", dirY);
nbt.setDouble("DirZ", dirZ);
nbt.setFloat("DesiredLength", desiredLength);
nbt.setFloat("CurrentLength", currentLength);
nbt.setBoolean("FullyExtended", fullyExtended);
nbt.setDouble("EndX", endX);
@@ -765,19 +811,19 @@ public class EntityAbilityLaser extends EntityEnergyProjectile {
*/
public void setupPreview(EntityLivingBase owner, float laserWidth, EnergyDisplayData display,
EnergyLightningData lightning, EnergyAnchorData anchor, int chargeDuration,
float expansionSpeed, float maxDistance) {
float expansionSpeed, float maxLength) {
setupPreviewState(owner, display, lightning, anchor, chargeDuration);
// Laser-specific visual/behavior state.
this.laserWidth = laserWidth;
this.expansionSpeed = expansionSpeed;
this.lifespanData.maxDistance = Math.min(maxDistance, 5.0f); // Limit for GUI preview
this.maxLength = Math.min(maxLength, 5.0f); // Limit for GUI preview
this.desiredLength = 0.0f;
this.currentLength = 0.0f;
this.fullyExtended = false;
this.ticksSinceFullExtension = 0;
// Charge as an orb from tiny size to laser width while anchored.
this.targetSize = laserWidth;
// Charge as an orb from tiny size to half laser width while anchored.
this.targetSize = laserWidth * 0.5f;
setVisualSize(0.01f);
setChargeOriginFromAnchor(owner, anchorData);
clearMotion();
@@ -38,7 +38,7 @@ public class EntityAbilityOrb extends EntityEnergyProjectile {
// Initialize base properties
initProjectile(owner, target, x, y, z, orbSize, display, combat, lightning, lifespan);
this.homingData = homing;
this.homingData = homing != null ? homing.copy() : new EnergyHomingData();
// Calculate initial velocity toward target
calculateInitialVelocity(owner, target, x, y, z);
@@ -150,6 +150,16 @@ public class EntityAbilityOrb extends EntityEnergyProjectile {
processEntitiesInHitBox(hitBox, nextX, nextY, nextZ);
}
// ==================== DEBUG ====================
@Override
protected String debugLogExtra() {
return String.format("motion=(%.3f,%.3f,%.3f) homing=%b start=(%.2f,%.2f,%.2f)",
motionX, motionY, motionZ,
homingData != null && homingData.isHoming(),
startX, startY, startZ);
}
// ==================== GETTERS ====================
// Legacy getter for renderer compatibility
@@ -56,11 +56,12 @@ public class EntityAbilityPanel extends EntityAbilityBarrier {
this.panelYaw = yaw;
this.prevPanelYaw = yaw;
this.mode = mode;
this.displayData = display;
this.lightningData = lightning;
this.barrierData = barrier;
this.panelData = panel;
this.currentHealth = barrier.maxHealth;
// Defensive copy: entities must never share data objects with the source ability.
this.displayData = display != null ? display.copy() : new EnergyDisplayData();
this.lightningData = lightning != null ? lightning.copy() : new EnergyLightningData();
this.barrierData = barrier != null ? barrier.copy() : new EnergyBarrierData();
this.panelData = panel != null ? panel.copy() : new EnergyPanelData();
this.currentHealth = this.barrierData.maxHealth;
// For launched panels, set initial velocity
if (mode == PanelMode.LAUNCHED) {
@@ -142,6 +143,8 @@ public class EntityAbilityPanel extends EntityAbilityBarrier {
// Stationary, no update needed
break;
}
debugLogBarrierTick();
}
private void updateHeld() {
@@ -175,7 +178,10 @@ public class EntityAbilityPanel extends EntityAbilityBarrier {
if (worldObj != null && worldObj.isRemote && mode == PanelMode.HELD && getOwnerEntity() != null) {
return;
}
super.setPositionAndRotation2(x, y, z, yaw, pitch, posRotationIncrements);
// Bypass vanilla's lerp + push-out-of-blocks logic which causes
// large-BB entities (PLACED/LAUNCHED panels) to shift into the sky.
this.setPosition(x, y, z);
this.setRotation(yaw, pitch);
}
private void updateLaunched() {
@@ -544,6 +550,14 @@ public class EntityAbilityPanel extends EntityAbilityBarrier {
return df * df + dr * dr + du * du;
}
// ==================== DEBUG ====================
@Override
protected String debugLogBarrierExtra() {
return String.format("mode=%s yaw=%.1f w=%.2f h=%.2f",
mode.name(), panelYaw, panelData.panelWidth, panelData.panelHeight);
}
// ==================== GETTERS ====================
public PanelMode getMode() {
@@ -3,6 +3,7 @@ package kamkeel.npcs.entity;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyDisplayData;
import kamkeel.npcs.util.CNPCDebug;
import net.minecraft.entity.Entity;
import net.minecraft.entity.EntityLivingBase;
import net.minecraft.entity.player.EntityPlayer;
@@ -79,7 +80,7 @@ public class EntityAbilitySweeper extends EntityEnergyAbility {
this.beamLength = beamLength;
this.beamWidth = beamWidth;
this.beamHeight = beamHeight;
this.displayData = displayData;
this.displayData = displayData != null ? displayData.copy() : new EnergyDisplayData();
this.sweepSpeed = sweepSpeed;
this.numberOfRotations = numberOfRotations;
this.damage = damage;
@@ -168,6 +169,19 @@ public class EntityAbilitySweeper extends EntityEnergyAbility {
if (!worldObj.isRemote) {
handleDamage(owner);
}
// Debug logging
{
boolean isClient = worldObj.isRemote;
if (isClient ? CNPCDebug.isClientEnabled("energy") : CNPCDebug.isServerEnabled("energy")) {
CNPCDebug.log("energy", isClient, String.format(
"[Sweeper id=%d tick=%d] pos=(%.2f,%.2f,%.2f) angle=%.1f prevAngle=%.1f " +
"speed=%.1f rotations=%d/%d length=%.2f width=%.2f",
getEntityId(), ticksExisted, posX, posY, posZ,
currentAngle, prevAngle, sweepSpeed,
completedRotations, numberOfRotations, beamLength, beamWidth));
}
}
}
/**
@@ -4,6 +4,7 @@ import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyDisplayData;
import kamkeel.npcs.controllers.data.ability.data.energy.EnergyLightningData;
import kamkeel.npcs.util.CNPCDebug;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.MathHelper;
import net.minecraft.world.World;
@@ -57,6 +58,17 @@ public class EntityEnergyExplosion extends EntityEnergyAbility {
float eased = 1.0f - (1.0f - progress) * (1.0f - progress);
this.renderRadius = maxRadius * eased;
// Debug logging
{
boolean isClient = worldObj.isRemote;
if (isClient ? CNPCDebug.isClientEnabled("energy") : CNPCDebug.isServerEnabled("energy")) {
CNPCDebug.log("energy", isClient, String.format(
"[Explosion id=%d tick=%d] pos=(%.2f,%.2f,%.2f) renderRadius=%.2f maxRadius=%.2f progress=%.2f",
getEntityId(), ticksExisted, posX, posY, posZ,
renderRadius, maxRadius, getLifeProgress(0f)));
}
}
if (ticksExisted >= durationTicks) {
setDead();
}
@@ -3,6 +3,7 @@ package kamkeel.npcs.entity;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
import kamkeel.npcs.controllers.AbilityController;
import kamkeel.npcs.controllers.data.ability.type.energy.AbilityEnergyProjectile;
import kamkeel.npcs.controllers.data.ability.data.effect.AbilityPotionEffect;
import kamkeel.npcs.controllers.data.ability.enums.AnchorPoint;
import kamkeel.npcs.controllers.data.ability.enums.HitType;
@@ -12,8 +13,10 @@ 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.energy.EnergyLightningData;
import kamkeel.npcs.network.packets.data.energy.ProjectileReflectPacket;
import kamkeel.npcs.network.packets.data.energyexplosion.EnergyExplosionSpawnPacket;
import kamkeel.npcs.util.AnchorPointHelper;
import kamkeel.npcs.util.CNPCDebug;
import net.minecraft.block.Block;
import net.minecraft.block.material.Material;
import net.minecraft.entity.Entity;
@@ -120,6 +123,7 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility {
protected int targetEntityId = -1;
// ==================== STATE ====================
protected boolean reflected = false; // Set after barrier reflection; prevents owner-tracking on reflected projectiles
protected boolean hasHit = false;
protected int hitCount = 0;
protected final Set<Integer> hitOnceEntities = new HashSet<Integer>();
@@ -183,10 +187,13 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility {
// Visual
this.size = size;
this.displayData = display;
this.combatData = combat;
this.lifespanData = lifespan; // deathWorldTime will be set on first tick when world is available
this.lightningData = lightning;
// Defensive copy: entities must never share data objects with the source ability.
// Without copies, runtime mutations (e.g. barrier reflection changing colors/damage)
// bleed back into the ability template and corrupt future projectiles.
this.displayData = display != null ? display.copy() : new EnergyDisplayData();
this.combatData = combat != null ? combat.copy() : new EnergyCombatData();
this.lifespanData = lifespan != null ? lifespan.copy() : new EnergyLifespanData();
this.lightningData = lightning != null ? lightning.copy() : new EnergyLightningData();
syncProjectileColorWatchers();
// Initialize render size
@@ -221,6 +228,10 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility {
if (ticksExisted == 1 && !worldObj.isRemote) {
trackProjectile(this);
}
} else {
// Preview entities skip super.onUpdate() but still need ticksExisted
// for renderer pulsing effects (e.g., sin(ticksExisted + partialTicks)).
this.ticksExisted++;
}
if (!previewMode) {
@@ -230,8 +241,13 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility {
// Update rotation
updateRotation();
// Lerp render size toward actual size
this.renderCurrentSize = this.renderCurrentSize + (this.size - this.renderCurrentSize) * 0.15f;
// Update render size: during charging, track directly (size already grows smoothly).
// Post-charging, lerp for smooth visual transitions.
if (isCharging()) {
this.renderCurrentSize = this.size;
} else {
this.renderCurrentSize = this.renderCurrentSize + (this.size - this.renderCurrentSize) * 0.15f;
}
// Server-synced visual timer used for brief barrier impact lightning.
if (!previewMode && !worldObj.isRemote) {
@@ -319,6 +335,8 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility {
if (!previewMode && !worldObj.isRemote) {
EventHooks.onEnergyProjectileTick(this);
}
debugLogTick();
}
/**
@@ -334,6 +352,53 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility {
if (this.rotationValZ > 360.0f) this.rotationValZ -= 360.0f;
}
// ==================== DEBUG LOGGING ====================
/**
* Debug log called every tick. Logs position/size/charging state.
* Subclasses override debugLogExtra() for type-specific data.
*/
protected void debugLogTick() {
boolean isClient = worldObj.isRemote;
if (isClient ? !CNPCDebug.isClientEnabled("energy") : !CNPCDebug.isServerEnabled("energy"))
return;
String className = getClass().getSimpleName();
boolean dwCharging = isCharging(); // DataWatcher value (what client sees)
boolean localCharging = this.charging; // Local field (server truth)
String base = String.format("[%s id=%d tick=%d] pos=(%.2f,%.2f,%.2f) prev=(%.2f,%.2f,%.2f) " +
"size=%.3f renderSize=%.3f prevRenderSize=%.3f " +
"charging(dw)=%b charging(local)=%b chargeTick=%d/%d chargeProgress=%.3f",
className, getEntityId(), ticksExisted,
posX, posY, posZ, prevPosX, prevPosY, prevPosZ,
size, renderCurrentSize, prevRenderSize,
dwCharging, localCharging, chargeTick, chargeDuration, getChargeProgress());
// Flag mismatch between DataWatcher and local field
if (dwCharging != localCharging) {
base += " !!CHARGE_MISMATCH!!";
}
// Client interpolation state (only meaningful on client)
if (isClient) {
base += String.format(" interp(steps=%d target=(%.2f,%.2f,%.2f))",
interpSteps, interpTargetX, interpTargetY, interpTargetZ);
}
String extra = debugLogExtra();
String full = extra.isEmpty() ? base : base + " " + extra;
CNPCDebug.log("energy", isClient, full);
}
/**
* Subclass hook for type-specific debug data.
* Return empty string if nothing extra to log.
*/
protected String debugLogExtra() {
return "";
}
/**
* Check if projectile has exceeded max distance.
*
@@ -380,7 +445,7 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility {
if (hitOutcome == null || hitOutcome.result == EntityAbilityBarrier.ProjectileHitResult.PASS) {
continue;
}
if (handleBarrierHitOutcome(barrier, hitOutcome)) {
if (handleBarrierHitOutcome(barrier, hitOutcome, damage)) {
return true;
}
return false;
@@ -393,20 +458,29 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility {
/**
* Handle the result of a projectile/barrier interaction.
*
* @param fullDamage The DBC-scaled damage that was passed to the barrier (from getModifiedDamage).
* @return true if caller should stop processing this tick.
*/
protected boolean handleBarrierHitOutcome(EntityAbilityBarrier barrier, EntityAbilityBarrier.ProjectileHitOutcome hitOutcome) {
protected boolean handleBarrierHitOutcome(EntityAbilityBarrier barrier, EntityAbilityBarrier.ProjectileHitOutcome hitOutcome, float fullDamage) {
if (hitOutcome == null || hitOutcome.result == EntityAbilityBarrier.ProjectileHitResult.PASS) {
return false;
}
if (hitOutcome.result == EntityAbilityBarrier.ProjectileHitResult.BROKEN) {
if (hitOutcome.remainingProjectileDamage <= 0.0f) {
damageMultiplier = 0.0f;
setBarrierSparkTicks(Math.max(getBarrierSparkTicks(), BARRIER_BREAK_SPARK_TICKS));
hasHit = true;
return true;
}
// Accumulate barrier pass-through ratio for extender damage scaling.
// fullDamage is the DBC-scaled damage the barrier saw; remainingProjectileDamage
// is what survived. Multiply into the running ratio so multiple barriers stack.
if (fullDamage > 0.0f) {
damageMultiplier *= (hitOutcome.remainingProjectileDamage / fullDamage);
}
setCombatDamage(hitOutcome.remainingProjectileDamage);
setBarrierSparkTicks(Math.max(getBarrierSparkTicks(), BARRIER_BREAK_SPARK_TICKS));
return false;
@@ -415,6 +489,8 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility {
if (hitOutcome.shouldReflect()) {
if (!reflectFromBarrier(barrier, hitOutcome.reflectStrengthPct)) {
beginBarrierImpactPause(true, barrier);
} else {
sendReflectionSync();
}
return true;
}
@@ -486,6 +562,14 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility {
barrierImpactPauseTicks = 0;
barrierImpactDestroyOnResume = false;
// Detach from source ability BEFORE changing ownership.
// This lets the ability see the projectile as gone and free the caster
// (e.g., release movement/rotation locks on the player).
if (sourceAbility instanceof AbilityEnergyProjectile) {
((AbilityEnergyProjectile<?>) sourceAbility).detachEntity(this);
}
sourceAbility = null;
Entity barrierOwner = barrier.getOwnerEntity();
if (barrierOwner != null) {
setOwnerEntityId(barrierOwner.getEntityId());
@@ -499,15 +583,83 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility {
setOuterColor(barrier.getOuterColor());
float clampedStrength = Math.max(0.0f, Math.min(100.0f, reflectStrengthPct));
float reducedDamage = getDamage() * (1.0f - clampedStrength / 100.0f);
float reductionFactor = 1.0f - clampedStrength / 100.0f;
float reducedDamage = getDamage() * reductionFactor;
setCombatDamage(Math.max(0.0f, reducedDamage));
damageMultiplier *= reductionFactor;
hitOnceEntities.clear();
lastHitTickByEntity.clear();
hitCount = 0;
reflected = true;
return true;
}
// ==================== REFLECTION SYNC ====================
/**
* Write reflection state for client sync.
* Subclasses should override writeProjectileReflectionData for type-specific fields.
*/
public NBTTagCompound writeReflectionData() {
NBTTagCompound nbt = new NBTTagCompound();
nbt.setInteger("OwnerId", ownerEntityId);
nbt.setDouble("PosX", posX);
nbt.setDouble("PosY", posY);
nbt.setDouble("PosZ", posZ);
nbt.setDouble("MotionX", motionX);
nbt.setDouble("MotionY", motionY);
nbt.setDouble("MotionZ", motionZ);
nbt.setInteger("InnerColor", getInnerColor());
nbt.setInteger("OuterColor", getOuterColor());
nbt.setBoolean("Reflected", reflected);
writeProjectileReflectionData(nbt);
return nbt;
}
/**
* Apply reflection state from server sync.
* Subclasses should override applyProjectileReflectionData for type-specific fields.
*/
public void applyReflectionData(NBTTagCompound nbt) {
ownerEntityId = nbt.getInteger("OwnerId");
double px = nbt.getDouble("PosX");
double py = nbt.getDouble("PosY");
double pz = nbt.getDouble("PosZ");
setPosition(px, py, pz);
motionX = nbt.getDouble("MotionX");
motionY = nbt.getDouble("MotionY");
motionZ = nbt.getDouble("MotionZ");
setInnerColor(nbt.getInteger("InnerColor"));
setOuterColor(nbt.getInteger("OuterColor"));
reflected = nbt.getBoolean("Reflected");
syncPositionStateToCurrent(true);
hitOnceEntities.clear();
lastHitTickByEntity.clear();
hitCount = 0;
applyProjectileReflectionData(nbt);
}
/**
* Subclass hook: write type-specific reflection data.
*/
protected void writeProjectileReflectionData(NBTTagCompound nbt) {
}
/**
* Subclass hook: apply type-specific reflection data on client.
*/
protected void applyProjectileReflectionData(NBTTagCompound nbt) {
}
/**
* Send reflection state to all tracking clients.
*/
protected void sendReflectionSync() {
if (worldObj == null || worldObj.isRemote) return;
ProjectileReflectPacket.sendToTracking(this, writeReflectionData());
}
protected double[] getBarrierImpactNormal(EntityAbilityBarrier barrier, double velocityX, double velocityY, double velocityZ) {
if (barrier instanceof EntityAbilityDome) {
EntityAbilityDome dome = (EntityAbilityDome) barrier;
@@ -857,7 +1009,7 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility {
float kbUp = getKnockbackUp() > 0 ? getKnockbackUp() : 0.1f;
handled = AbilityController.Instance.fireOnAbilityDamage(
sourceAbility, (EntityLivingBase) owner, target,
dmg, kb, kbUp, dx, dz);
dmg, kb, kbUp, dx, dz, damageMultiplier);
}
if (!handled) {
@@ -902,7 +1054,15 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility {
* Set the effects list from the ability's configured effects.
*/
public void setEffects(List<AbilityPotionEffect> effects) {
this.effects = effects != null ? effects : new ArrayList<>();
if (effects == null || effects.isEmpty()) {
this.effects = new ArrayList<>();
} else {
// Deep copy: entity must not share the ability's effects list or its entries.
this.effects = new ArrayList<>(effects.size());
for (AbilityPotionEffect effect : effects) {
this.effects.add(effect.copy());
}
}
}
protected void doExplosion() {
@@ -1509,8 +1669,8 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility {
int chargeDuration) {
this.setPreviewMode(true);
this.setPreviewOwner(owner);
this.displayData = display;
this.lightningData = lightning;
this.displayData = display != null ? display.copy() : new EnergyDisplayData();
this.lightningData = lightning != null ? lightning.copy() : new EnergyLightningData();
setupChargingState(anchor, chargeDuration);
}
@@ -1817,6 +1977,18 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility {
// ==================== COMBAT GETTERS & SETTERS ====================
/**
* Accumulated proportional damage multiplier (0.0-1.0).
* Accumulates multiplicatively when the projectile breaks through barriers, is reflected,
* or any other system reduces its effective damage. Used by ability extenders to scale
* recalculated damage proportionally.
*/
private float damageMultiplier = 1.0f;
public float getDamageMultiplier() {
return damageMultiplier;
}
public float getDamage() {
return combatData.getDamage();
}
@@ -2167,6 +2339,7 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility {
this.startZ = nbt.getDouble("StartZ");
this.targetEntityId = nbt.getInteger("TargetId");
this.reflected = nbt.getBoolean("Reflected");
this.motionX = nbt.getDouble("MotionX");
this.motionY = nbt.getDouble("MotionY");
@@ -2223,6 +2396,7 @@ public abstract class EntityEnergyProjectile extends EntityEnergyAbility {
nbt.setDouble("StartZ", startZ);
nbt.setInteger("TargetId", targetEntityId);
nbt.setBoolean("Reflected", reflected);
nbt.setDouble("MotionX", motionX);
nbt.setDouble("MotionY", motionY);
@@ -43,7 +43,7 @@ public class EntityEnergySlicer extends EntityEnergyProjectile {
super(world);
initProjectile(owner, target, x, y, z, sliceWidth, display, combat, lightning, lifespan);
this.homingData = homing;
this.homingData = homing != null ? homing.copy() : new EnergyHomingData();
this.sliceWidth = sliceWidth;
this.sliceThickness = sliceThickness;
@@ -192,6 +192,14 @@ public class EntityEnergySlicer extends EntityEnergyProjectile {
return lastTravelPitch;
}
// ==================== DEBUG ====================
@Override
protected String debugLogExtra() {
return String.format("motion=(%.3f,%.3f,%.3f) sliceW=%.2f sliceT=%.2f",
motionX, motionY, motionZ, sliceWidth, sliceThickness);
}
// ==================== GETTERS ====================
public float getSliceWidth() {
@@ -39,6 +39,7 @@ import kamkeel.npcs.network.packets.data.script.ScriptedParticlePacket;
import kamkeel.npcs.network.packets.data.energycharge.EnergyChargeRemovePacket;
import kamkeel.npcs.network.packets.data.energycharge.EnergyChargeSpawnPacket;
import kamkeel.npcs.network.packets.data.energyexplosion.EnergyExplosionSpawnPacket;
import kamkeel.npcs.network.packets.data.energy.ProjectileReflectPacket;
import kamkeel.npcs.network.packets.data.telegraph.TelegraphRemovePacket;
import kamkeel.npcs.network.packets.data.telegraph.TelegraphSpawnPacket;
import kamkeel.npcs.network.packets.player.AuctionActionPacket;
@@ -592,6 +593,7 @@ public class PacketHandler {
DATA_PACKET.registerPacket(new EnergyChargeSpawnPacket());
DATA_PACKET.registerPacket(new EnergyChargeRemovePacket());
DATA_PACKET.registerPacket(new EnergyExplosionSpawnPacket());
DATA_PACKET.registerPacket(new ProjectileReflectPacket());
DATA_PACKET.registerPacket(new PlayerAbilitySyncPacket());
DATA_PACKET.registerPacket(new PlayerAbilityStatePacket());
DATA_PACKET.registerPacket(new AbilityHotbarSyncPacket());
@@ -76,4 +76,7 @@ public enum EnumDataPacket {
// Energy Explosion Preview
ENERGY_EXPLOSION_SPAWN,
// Energy Projectile Reflection Sync
PROJECTILE_REFLECT,
}
@@ -10,6 +10,7 @@ import kamkeel.npcs.network.enums.EnumDataPacket;
import kamkeel.npcs.util.ByteBufUtils;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.entity.player.EntityPlayerMP;
import noppes.npcs.client.ClientCacheHandler;
import noppes.npcs.controllers.PlayerDataController;
import noppes.npcs.controllers.data.PlayerData;
@@ -86,7 +87,7 @@ public final class AbilityCooldownSyncPacket extends AbstractPacket {
perDurations.put(key, duration);
}
PlayerData data = PlayerData.get(player);
PlayerData data = ClientCacheHandler.playerData;
if (data != null && data.abilityData != null) {
data.abilityData.applyCooldownSync(globalEndTime, globalDuration, perEndTimes, perDurations);
}
@@ -11,6 +11,7 @@ import kamkeel.npcs.util.ByteBufUtils;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.entity.player.EntityPlayerMP;
import net.minecraft.nbt.NBTTagCompound;
import noppes.npcs.client.ClientCacheHandler;
import noppes.npcs.controllers.PlayerDataController;
import noppes.npcs.controllers.data.PlayerData;
@@ -53,7 +54,7 @@ public final class PlayerAbilitySyncPacket extends AbstractPacket {
NBTTagCompound nbt = ByteBufUtils.readNBT(in);
if (nbt == null) return;
PlayerData data = PlayerData.get(player);
PlayerData data = ClientCacheHandler.playerData;
if (data != null) {
data.abilityData.readFromNBT(nbt);
}
@@ -0,0 +1,79 @@
package kamkeel.npcs.network.packets.data.energy;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
import io.netty.buffer.ByteBuf;
import kamkeel.npcs.entity.EntityEnergyProjectile;
import kamkeel.npcs.network.AbstractPacket;
import kamkeel.npcs.network.PacketChannel;
import kamkeel.npcs.network.PacketHandler;
import kamkeel.npcs.network.enums.EnumDataPacket;
import kamkeel.npcs.util.ByteBufUtils;
import net.minecraft.client.Minecraft;
import net.minecraft.entity.Entity;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.world.World;
import java.io.IOException;
/**
* Syncs projectile reflection state to all tracking clients.
* Sent server→client after a successful barrier reflection so the client
* can update ownerEntityId, position, motion, colors, and type-specific
* reflection data (beam mode, laser direction, disc boomerang, etc.).
*/
public final class ProjectileReflectPacket extends AbstractPacket {
public static final String packetName = "Data|ProjectileReflect";
private int entityId;
private NBTTagCompound reflectData;
public ProjectileReflectPacket() {
}
public ProjectileReflectPacket(int entityId, NBTTagCompound reflectData) {
this.entityId = entityId;
this.reflectData = reflectData;
}
@Override
public Enum getType() {
return EnumDataPacket.PROJECTILE_REFLECT;
}
@Override
public PacketChannel getChannel() {
return PacketHandler.DATA_PACKET;
}
@Override
public void sendData(ByteBuf out) throws IOException {
out.writeInt(entityId);
ByteBufUtils.writeNBT(out, reflectData);
}
@SideOnly(Side.CLIENT)
@Override
public void receiveData(ByteBuf in, EntityPlayer player) throws IOException {
int id = in.readInt();
NBTTagCompound nbt = ByteBufUtils.readNBT(in);
if (nbt == null) return;
World world = Minecraft.getMinecraft().theWorld;
if (world == null) return;
Entity entity = world.getEntityByID(id);
if (entity instanceof EntityEnergyProjectile) {
((EntityEnergyProjectile) entity).applyReflectionData(nbt);
}
}
public static void sendToTracking(EntityEnergyProjectile projectile, NBTTagCompound reflectData) {
if (projectile == null || reflectData == null) return;
PacketHandler.Instance.sendTracking(
new ProjectileReflectPacket(projectile.getEntityId(), reflectData),
projectile
);
}
}
@@ -311,6 +311,10 @@ public class AuctionActionPacket extends AbstractPacket {
ItemStack item = ItemStack.loadItemStackFromNBT(itemNBT);
if (item == null) return "Invalid item data.";
// Validate stack size
if (item.stackSize <= 0) return "Invalid item amount.";
if (item.stackSize > item.getMaxStackSize()) return "Stack size exceeds item maximum.";
// Remove item from inventory first
if (!removeItemFromInventory(player, item)) {
return "Item not found in inventory.";
@@ -366,8 +366,9 @@ public class ManageAuctionPacket extends AbstractPacket {
String result = controller.createGlobalListing(player, sellerName, item, startingPrice, buyoutPrice, durationHours);
if (result != null) {
// Failed create, return item (or drop to avoid loss).
if (!player.inventory.addItemStackToInventory(item.copy())) {
player.entityDropItem(item.copy(), 0.5f);
// Use same reference so partial add mutates stackSize, then drop only the remainder.
if (!player.inventory.addItemStackToInventory(item)) {
player.entityDropItem(item, 0.5f);
}
}
if (result == null) {
@@ -0,0 +1,163 @@
package kamkeel.npcs.util;
import java.io.File;
import java.io.FileOutputStream;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.logging.Formatter;
import java.util.logging.Handler;
import java.util.logging.Level;
import java.util.logging.LogRecord;
import java.util.logging.StreamHandler;
/**
* Debug logging system for CNPC+ entities.
* Writes to logs/cnpc_debug/[type]_[side].log (file only, no console output).
* Toggle at runtime via /cnpcdebugger [type] (client) or /kam debug [type] (server).
*/
public class CNPCDebug {
private static final String DIR = "logs/cnpc_debug";
private static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("HH:mm:ss");
// Runtime toggle state per type, separate for client and server
private static final Map<String, Boolean> serverToggles = new ConcurrentHashMap<>();
private static final Map<String, Boolean> clientToggles = new ConcurrentHashMap<>();
// Per-type file handlers (lazily initialized)
private static final Map<String, Handler> serverHandlers = new ConcurrentHashMap<>();
private static final Map<String, Handler> clientHandlers = new ConcurrentHashMap<>();
// ==================== TOGGLE API ====================
/**
* Toggle server-side debug for given type. Returns new state.
*/
public static boolean toggleServer(String type) {
type = type.toLowerCase();
boolean newState = !isServerEnabled(type);
serverToggles.put(type, newState);
if (newState) {
ensureHandler(type, false);
}
return newState;
}
/**
* Toggle client-side debug for given type. Returns new state.
*/
public static boolean toggleClient(String type) {
type = type.toLowerCase();
boolean newState = !isClientEnabled(type);
clientToggles.put(type, newState);
if (newState) {
ensureHandler(type, true);
}
return newState;
}
public static boolean isServerEnabled(String type) {
Boolean val = serverToggles.get(type.toLowerCase());
return val != null && val;
}
public static boolean isClientEnabled(String type) {
Boolean val = clientToggles.get(type.toLowerCase());
return val != null && val;
}
// ==================== LOGGING API ====================
/**
* Log a debug message for the given type on the given side.
* Callers should guard with isServerEnabled/isClientEnabled for performance.
*/
public static void log(String type, boolean client, String msg) {
type = type.toLowerCase();
Map<String, Boolean> toggles = client ? clientToggles : serverToggles;
Boolean enabled = toggles.get(type);
if (enabled == null || !enabled) return;
Handler handler = ensureHandler(type, client);
if (handler == null) return;
String side = client ? "CLIENT" : "SERVER";
String timestamp;
synchronized (DATE_FORMAT) {
timestamp = DATE_FORMAT.format(new Date());
}
String formatted = "[" + timestamp + "][" + side + "][DEBUG-" + type.toUpperCase() + "] " + msg + System.lineSeparator();
LogRecord record = new LogRecord(Level.INFO, formatted);
handler.publish(record);
handler.flush();
}
/**
* Log on server side.
*/
public static void logServer(String type, String msg) {
log(type, false, msg);
}
/**
* Log on client side.
*/
public static void logClient(String type, String msg) {
log(type, true, msg);
}
// ==================== INTERNALS ====================
private static Handler ensureHandler(String type, boolean client) {
Map<String, Handler> handlers = client ? clientHandlers : serverHandlers;
Handler existing = handlers.get(type);
if (existing != null) return existing;
synchronized (CNPCDebug.class) {
existing = handlers.get(type);
if (existing != null) return existing;
try {
File dir = new File(DIR);
if (!dir.exists()) dir.mkdirs();
String side = client ? "client" : "server";
File file = new File(dir, type + "_" + side + ".log");
// Rotate old log
if (file.exists()) {
File backup = new File(dir, type + "_" + side + "-prev.log");
if (backup.exists()) backup.delete();
file.renameTo(backup);
}
Handler handler = new StreamHandler(new FileOutputStream(file), new Formatter() {
@Override
public String format(LogRecord record) {
return record.getMessage();
}
});
handler.setLevel(Level.ALL);
handlers.put(type, handler);
// Log header
String timestamp;
synchronized (DATE_FORMAT) {
timestamp = DATE_FORMAT.format(new Date());
}
LogRecord header = new LogRecord(Level.INFO,
"[" + timestamp + "][" + (client ? "CLIENT" : "SERVER") + "][DEBUG-" + type.toUpperCase() + "] Debug logging started" + System.lineSeparator());
handler.publish(header);
handler.flush();
return handler;
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
}
+5 -2
View File
@@ -26,6 +26,7 @@ import kamkeel.npcs.controllers.AttributeController;
import kamkeel.npcs.controllers.ProfileController;
import kamkeel.npcs.controllers.SyncController;
import kamkeel.npcs.controllers.TelegraphController;
import kamkeel.npcs.controllers.data.energycharge.EnergyChargeTracker;
import kamkeel.npcs.controllers.data.profile.CNPCData;
import kamkeel.npcs.developer.Developer;
import kamkeel.npcs.entity.EntityAbilityBarrier;
@@ -37,6 +38,7 @@ import kamkeel.npcs.entity.EntityAbilityPanel;
import kamkeel.npcs.entity.EntityAbilitySweeper;
import kamkeel.npcs.entity.EntityAbilityZone;
import kamkeel.npcs.entity.EntityAbilityDome;
import kamkeel.npcs.entity.EntityEnergyProjectile;
import kamkeel.npcs.entity.EntityEnergySlicer;
import kamkeel.npcs.network.PacketHandler;
import kamkeel.npcs.util.BukkitUtil;
@@ -122,7 +124,7 @@ import java.util.HashSet;
import java.util.Set;
import java.util.function.Consumer;
@Mod(modid = "customnpcs", name = "CustomNPC+", version = "1.11-beta9")
@Mod(modid = "customnpcs", name = "CustomNPC+", version = "1.11-beta10")
public class CustomNpcs {
@SidedProxy(clientSide = "noppes.npcs.client.ClientProxy", serverSide = "noppes.npcs.CommonProxy")
@@ -430,8 +432,9 @@ public class CustomNpcs {
AuctionController.Instance.save();
}
kamkeel.npcs.entity.EntityEnergyProjectile.clearAllProjectiles();
EntityEnergyProjectile.clearAllProjectiles();
EntityAbilityBarrier.clearAllBarriers();
EnergyChargeTracker.Instance.clear();
if (FMLCommonHandler.instance().getSide().isClient())
clientJaninoCompiler = null;
@@ -147,10 +147,7 @@ public class ScriptPlayerEventHandler {
EntityPlayerMP mp = (EntityPlayerMP) player;
SyncController.syncEffects(mp);
SyncController.syncAbilityCooldowns(mp);
if (playerData.abilityData.needsSync) {
PlayerAbilitySyncPacket.sendToPlayer(mp);
playerData.abilityData.needsSync = false;
}
PlayerAbilitySyncPacket.sendToPlayer(mp);
playerData.abilityData.ensureLockStateClear();
}
@@ -4,6 +4,7 @@ import cpw.mods.fml.common.FMLCommonHandler;
import cpw.mods.fml.common.eventhandler.SubscribeEvent;
import cpw.mods.fml.relauncher.Side;
import kamkeel.npcs.controllers.SyncController;
import kamkeel.npcs.controllers.data.energycharge.EnergyChargeTracker;
import kamkeel.npcs.entity.EntityAbilityBarrier;
import kamkeel.npcs.entity.EntityAbilityDome;
import kamkeel.npcs.network.PacketHandler;
@@ -300,6 +301,7 @@ public class ServerEventsHandler {
public void invoke(LivingDeathEvent event) {
if (event.entityLiving.worldObj.isRemote)
return;
EnergyChargeTracker.Instance.removeAllForCaster(event.entityLiving.getEntityId());
if (event.source.getEntity() != null) {
if (event.source.getEntity() instanceof EntityPlayer) {
doExcalibur((EntityPlayer) event.source.getEntity(), event.entityLiving);
@@ -535,7 +537,19 @@ public class ServerEventsHandler {
@SubscribeEvent
public void playerTracking(PlayerEvent.StartTracking event) {
if (!(event.target instanceof EntityPlayerMP || event.target instanceof EntityNPCInterface) || event.target.worldObj.isRemote)
if (event.target.worldObj.isRemote) return;
// Energy charge visuals: send active charges for newly tracked entity
if (event.target instanceof EntityLivingBase
&& event.entityPlayer instanceof EntityPlayerMP) {
EnergyChargeTracker.Instance.sendToPlayer(
event.target.getEntityId(),
(EntityPlayerMP) event.entityPlayer,
(int) event.target.worldObj.getTotalWorldTime()
);
}
if (!(event.target instanceof EntityPlayerMP || event.target instanceof EntityNPCInterface))
return;
AnimationData animationData = AnimationData.getData(event.target);
@@ -25,9 +25,6 @@ import java.util.Map;
import java.util.Set;
public class FlyPathFinder extends PathFinder {
private static final float STRAIGHT_COST = 1.0F;
private static final float DIAGONAL_2D_COST = 1.4142135F;
private static final float DIAGONAL_3D_COST = 1.7320508F;
/**
* Used to find obstacles
*/
@@ -144,7 +141,7 @@ public class FlyPathFinder extends PathFinder {
private NPCPath addToPath(Entity p_75861_1_, NPCPathPoint p_75861_2_, NPCPathPoint p_75861_3_, NPCPathPoint p_75861_4_, float p_75861_5_) {
p_75861_2_.totalPathDistance = 0.0F;
p_75861_2_.distanceToNext = this.estimateCostToTarget(p_75861_2_, p_75861_3_);
p_75861_2_.distanceToNext = p_75861_2_.distanceToSquared(p_75861_3_);
p_75861_2_.distanceToTarget = p_75861_2_.distanceToNext;
this.path.clearPath();
this.path.addPoint(p_75861_2_);
@@ -164,7 +161,7 @@ public class FlyPathFinder extends PathFinder {
return this.createEntityPath(p_75861_2_, p_75861_3_);
}
if (this.estimateCostToTarget(pathpoint4, p_75861_3_) < this.estimateCostToTarget(pathpoint3, p_75861_3_)) {
if (pathpoint4.distanceToSquared(p_75861_3_) < pathpoint3.distanceToSquared(p_75861_3_)) {
pathpoint3 = pathpoint4;
}
@@ -173,13 +170,12 @@ public class FlyPathFinder extends PathFinder {
for (int j = 0; j < i; ++j) {
NPCPathPoint pathpoint5 = this.pathOptions[j];
float f1 = pathpoint4.totalPathDistance + this.getTraversalCost(pathpoint4, pathpoint5)
+ this.getDirectionalPenalty(pathpoint4, pathpoint5, p_75861_3_);
float f1 = pathpoint4.totalPathDistance + pathpoint4.distanceToSquared(pathpoint5);
if (!pathpoint5.isAssigned() || f1 < pathpoint5.totalPathDistance) {
pathpoint5.previous = pathpoint4;
pathpoint5.totalPathDistance = f1;
pathpoint5.distanceToNext = this.estimateCostToTarget(pathpoint5, p_75861_3_);
pathpoint5.distanceToNext = pathpoint5.distanceToSquared(p_75861_3_);
if (pathpoint5.isAssigned()) {
this.path.changeDistance(pathpoint5, pathpoint5.totalPathDistance + pathpoint5.distanceToNext);
@@ -198,50 +194,6 @@ public class FlyPathFinder extends PathFinder {
}
}
private float getTraversalCost(NPCPathPoint from, NPCPathPoint to) {
int dx = Math.abs(to.xCoord - from.xCoord);
int dy = Math.abs(to.yCoord - from.yCoord);
int dz = Math.abs(to.zCoord - from.zCoord);
int changedAxes = dx + dy + dz;
if (changedAxes <= 1) {
return STRAIGHT_COST;
}
if (changedAxes == 2) {
return DIAGONAL_2D_COST;
}
return DIAGONAL_3D_COST;
}
private float getDirectionalPenalty(NPCPathPoint from, NPCPathPoint to, NPCPathPoint goal) {
int stepX = to.xCoord - from.xCoord;
int stepY = to.yCoord - from.yCoord;
int stepZ = to.zCoord - from.zCoord;
int goalX = goal.xCoord - from.xCoord;
int goalY = goal.yCoord - from.yCoord;
int goalZ = goal.zCoord - from.zCoord;
int stepLen = Math.abs(stepX) + Math.abs(stepY) + Math.abs(stepZ);
int goalLen = Math.abs(goalX) + Math.abs(goalY) + Math.abs(goalZ);
if (stepLen == 0 || goalLen == 0) {
return 0.0F;
}
float alignment = (float) (stepX * goalX + stepY * goalY + stepZ * goalZ) / (float) (stepLen * goalLen);
return (1.0F - alignment) * 0.04F;
}
private float estimateCostToTarget(NPCPathPoint from, NPCPathPoint to) {
float dx = Math.abs(to.xCoord - from.xCoord);
float dy = Math.abs(to.yCoord - from.yCoord);
float dz = Math.abs(to.zCoord - from.zCoord);
float max = Math.max(dx, Math.max(dy, dz));
float min = Math.min(dx, Math.min(dy, dz));
float mid = dx + dy + dz - max - min;
return DIAGONAL_3D_COST * min + DIAGONAL_2D_COST * (mid - min) + STRAIGHT_COST * (max - mid);
}
/**
* populates pathOptions with available points and returns the number of options found (args: unused1, currentPoint,
* unused2, targetPoint, maxDistance)
@@ -7,8 +7,6 @@ import noppes.npcs.entity.EntityNPCFlying;
import noppes.npcs.entity.EntityNPCInterface;
public class FlyingMoveHelper extends EntityMoveHelper {
private static final double CLOSE_ENOUGH_SQ = 0.01D;
private static final float MAX_TURN_PER_TICK = 35.0F;
private final EntityNPCInterface entity;
private double posX;
@@ -40,24 +38,22 @@ public class FlyingMoveHelper extends EntityMoveHelper {
double d5 = MathHelper.sqrt_double(d4);
speed = Math.min(d5 / 5.0D, speed);
if (d4 > CLOSE_ENOUGH_SQ && d5 > 1.0E-4D) {
double targetMotionX = speed * (d0 / d5);
double targetMotionZ = speed * (d2 / d5);
double horizontalLerp = Math.min(1.0D, Math.max(0.12D, speed * 0.6D));
this.entity.motionX += (targetMotionX - this.entity.motionX) * horizontalLerp;
this.entity.motionZ += (targetMotionZ - this.entity.motionZ) * horizontalLerp;
// its here louis!
// This is the hurt time that stuns the NPC!!!!!!
// if (this.entity.hurtTime == 0 && d4 > 0.5D) {
if (d4 >= 0.5D) {
this.entity.motionX += (speed * (d0 / d5) - this.entity.motionX) * speed;
this.entity.motionZ += (speed * (d2 / d5) - this.entity.motionZ) * speed;
if (((EntityNPCFlying) this.entity).flyLimitAllow || !this.entity.ais.hasFlyLimit) {
double targetMotionY = verticalSpeed * (d1 / d5);
if (targetMotionY > 0.0D) {
targetMotionY += 0.05D;
this.entity.motionY = verticalSpeed * (d1 / d5);
if (this.entity.motionY > 0) {
this.entity.motionY += 0.1D;
}
double verticalLerp = Math.min(1.0D, Math.max(0.15D, verticalSpeed * 0.85D));
this.entity.motionY += (targetMotionY - this.entity.motionY) * verticalLerp;
}
this.entity.velocityChanged = true;
this.entity.rotationYaw = this.limitAngle(this.entity.rotationYaw, (float) ((Math.atan2(-d0, -d2) + Math.PI) * -(180F / Math.PI)), MAX_TURN_PER_TICK);
this.entity.rotationYaw = this.limitAngle(this.entity.rotationYaw, (float) ((Math.atan2(-d0, -d2) + Math.PI) * -(180F / Math.PI)), 20.0F);
}
}
}
@@ -21,10 +21,6 @@ import noppes.npcs.entity.EntityNPCInterface;
import javax.annotation.Nullable;
public class PathNavigateFlying extends PathNavigate {
private static final long MIN_REPATH_INTERVAL_TICKS = 8L;
private static final long MAX_REPATH_INTERVAL_TICKS = 30L;
private static final int PATH_REUSE_TARGET_DELTA_SQ = 2;
private static final int FINAL_NODE_REUSE_DELTA_SQ = 3;
private EntityNPCInterface theEntity;
private World worldObj;
@@ -87,21 +83,16 @@ public class PathNavigateFlying extends PathNavigate {
}
public void updatePath() {
if (this.targetPos == null) {
this.tryUpdatePath = false;
return;
}
long now = this.worldObj.getTotalWorldTime();
if (now - this.lastTimeUpdated < this.getAdaptiveRepathInterval()) {
if (this.worldObj.getTotalWorldTime() - this.lastTimeUpdated > 20L) {
if (this.targetPos != null) {
this.currentPath = null;
this.currentPath = this.getPathToXYZ(this.targetPos.getX(), this.targetPos.getY(), this.targetPos.getZ());
this.lastTimeUpdated = this.worldObj.getTotalWorldTime();
this.tryUpdatePath = false;
}
} else {
this.tryUpdatePath = true;
return;
}
this.currentPath = null;
this.currentPath = this.getPathToXYZ(this.targetPos.getX(), this.targetPos.getY(), this.targetPos.getZ());
this.lastTimeUpdated = now;
this.tryUpdatePath = false;
}
public void setAvoidsWater(boolean p_75491_1_) {
@@ -165,8 +156,7 @@ public class PathNavigateFlying extends PathNavigate {
BlockPos pos = new BlockPos(x, y, z);
if (!this.canNavigate()) {
return null;
} else if (this.canReuseCurrentPath(pos)) {
this.targetPos = pos;
} else if (this.currentPath != null && !this.currentPath.isFinished() && pos.equals(this.targetPos)) {
return this.currentPath;
} else {
this.targetPos = pos;
@@ -190,8 +180,7 @@ public class PathNavigateFlying extends PathNavigate {
return null;
} else {
BlockPos blockpos = new BlockPos(entity);
if (this.canReuseCurrentPath(blockpos)) {
this.targetPos = blockpos;
if (this.currentPath != null && !this.currentPath.isFinished() && blockpos.equals(this.targetPos)) {
return this.currentPath;
} else {
this.targetPos = blockpos;
@@ -226,8 +215,6 @@ public class PathNavigateFlying extends PathNavigate {
this.removeSunnyPath();
}
this.smoothCurrentPath();
if (this.currentPath.getCurrentPathLength() == 0) {
return false;
} else {
@@ -255,72 +242,6 @@ public class PathNavigateFlying extends PathNavigate {
return !this.isInLiquid() || (this.canSwim && this.isInLiquid());
}
private long getAdaptiveRepathInterval() {
if (this.targetPos == null) {
return MAX_REPATH_INTERVAL_TICKS;
}
double distance = this.theEntity.getDistance(this.targetPos.getX() + 0.5D, this.targetPos.getY(), this.targetPos.getZ() + 0.5D);
return (long) MathHelper.clamp_double(distance * 0.6D, MIN_REPATH_INTERVAL_TICKS, MAX_REPATH_INTERVAL_TICKS);
}
private boolean canReuseCurrentPath(BlockPos newTarget) {
if (this.currentPath == null || this.currentPath.isFinished() || this.targetPos == null) {
return false;
}
if (this.getBlockDistanceSq(this.targetPos, newTarget) <= PATH_REUSE_TARGET_DELTA_SQ) {
return true;
}
NPCPathPoint finalPoint = this.currentPath.getFinalPathPoint();
return finalPoint != null && this.getPointDistanceSq(finalPoint, newTarget) <= FINAL_NODE_REUSE_DELTA_SQ;
}
private int getBlockDistanceSq(BlockPos a, BlockPos b) {
int dx = a.getX() - b.getX();
int dy = a.getY() - b.getY();
int dz = a.getZ() - b.getZ();
return dx * dx + dy * dy + dz * dz;
}
private int getPointDistanceSq(NPCPathPoint point, BlockPos blockPos) {
int dx = point.xCoord - blockPos.getX();
int dy = point.yCoord - blockPos.getY();
int dz = point.zCoord - blockPos.getZ();
return dx * dx + dy * dy + dz * dz;
}
private void smoothCurrentPath() {
if (this.currentPath == null) {
return;
}
int pathLength = this.currentPath.getCurrentPathLength();
if (pathLength < 3) {
return;
}
int width = MathHelper.ceiling_double_int(this.theEntity.width);
int height = MathHelper.ceiling_double_int(this.theEntity.height + 1.0F);
NPCPathPoint[] points = this.currentPath.points;
int writeIndex = 1;
int anchorIndex = 0;
for (int readIndex = 2; readIndex < pathLength; readIndex++) {
Vec3 anchor = this.currentPath.getVectorFromIndex(this.theEntity, anchorIndex);
Vec3 probe = this.currentPath.getVectorFromIndex(this.theEntity, readIndex);
if (!this.isDirectPathBetweenPoints(anchor, probe, width, height)) {
points[writeIndex++] = points[readIndex - 1];
anchorIndex = readIndex - 1;
}
}
points[writeIndex++] = points[pathLength - 1];
this.currentPath.setCurrentPathLength(writeIndex);
if (this.currentPath.getCurrentPathIndex() >= writeIndex) {
this.currentPath.setCurrentPathIndex(Math.max(0, writeIndex - 1));
}
}
public void onUpdateNavigation() {
++this.totalTicks;
@@ -6,6 +6,7 @@ import cpw.mods.fml.client.registry.RenderingRegistry;
import cpw.mods.fml.common.FMLCommonHandler;
import cpw.mods.fml.common.ObfuscationReflectionHelper;
import kamkeel.npcs.addon.client.DBCClient;
import kamkeel.npcs.client.command.CommandCNPCDebugger;
import kamkeel.npcs.client.renderer.RenderAbilityBeam;
import kamkeel.npcs.client.renderer.RenderAbilityDisc;
import kamkeel.npcs.client.renderer.RenderAbilityLaser;
@@ -53,6 +54,7 @@ import net.minecraft.util.ChatComponentText;
import net.minecraft.util.ReportedException;
import net.minecraft.util.ResourceLocation;
import net.minecraft.world.World;
import net.minecraftforge.client.ClientCommandHandler;
import net.minecraftforge.client.MinecraftForgeClient;
import net.minecraftforge.common.MinecraftForge;
import net.minecraftforge.common.util.FakePlayer;
@@ -362,6 +364,9 @@ public class ClientProxy extends CommonProxy {
ClientTagMapController.Instance = new ClientTagMapController();
MinecraftForge.EVENT_BUS.register(new ClientEventHandler());
// Debug commands
ClientCommandHandler.instance.registerCommand(new CommandCNPCDebugger());
// Telegraph rendering system
TelegraphManager.initClient();
MinecraftForge.EVENT_BUS.register(new TelegraphRenderer());
@@ -419,7 +424,8 @@ public class ClientProxy extends CommonProxy {
@Override
public PlayerData getPlayerData(EntityPlayer player) {
if (player.getUniqueID() == Minecraft.getMinecraft().thePlayer.getUniqueID()) {
EntityPlayer local = Minecraft.getMinecraft().thePlayer;
if (local != null && player.getUniqueID().equals(local.getUniqueID())) {
if (ClientCacheHandler.playerData != null) {
if (ClientCacheHandler.playerData.player != player) {
ClientCacheHandler.playerData.player = player;
@@ -28,7 +28,7 @@ public class GuiNpcPather extends GuiNPCInterface implements IGuiData {
drawDefaultBackground = false;
xSize = 176;
title = "Npc Pather";
setBackground("smallbg.png");
setBackground("menubg.png");
ai = npc.ais;
PacketClient.sendClient(new MovingPathGetPacket());
}
@@ -13,7 +13,7 @@ public class SubGuiAnimationFrame extends SubGuiInterface implements ITextfieldL
public SubGuiAnimationFrame(Frame editingFrame) {
this.editingFrame = editingFrame;
setBackground("smallbg.png");
setBackground("menubg.png");
}
@Override
@@ -11,7 +11,7 @@ public class SubGuiAnimationOptions extends SubGuiInterface {
public SubGuiAnimationOptions(Animation animation) {
this.animation = animation;
setBackground("smallbg.png");
setBackground("menubg.png");
}
@Override
@@ -11,7 +11,7 @@ public class SubGuiEditText extends SubGuiInterface {
public SubGuiEditText(String text) {
this.text = text;
setBackground("extrasmallbg.png");
setBackground("menubg.png");
closeOnEsc = true;
xSize = 176;
ySize = 71;
@@ -220,7 +220,7 @@ public class GuiFieldBuilder {
float fVal = def.getValue() instanceof Number ? ((Number) def.getValue()).floatValue() : 0f;
GuiNpcTextField tf = new GuiNpcTextField(widgetId, parent, fontRenderer, fieldX, y, fieldW, 20, String.valueOf(fVal));
tf.setFloatsOnly();
if (def.hasRange()) tf.setMinMaxDefaultFloat(def.getMin(), def.getMax(), fVal);
tf.setMinMaxDefaultFloat(def.getMin(), def.getMax(), fVal);
if (!def.isEnabled()) tf.setEnabled(false);
if (hover != null) tf.setHoverText(hover);
sw.addTextField(tf);
@@ -233,7 +233,7 @@ public class GuiFieldBuilder {
int iVal = def.getValue() instanceof Number ? ((Number) def.getValue()).intValue() : 0;
GuiNpcTextField tf = new GuiNpcTextField(widgetId, parent, fontRenderer, fieldX, y, fieldW, 20, String.valueOf(iVal));
tf.setIntegersOnly();
if (def.hasRange()) tf.setMinMaxDefault((int) def.getMin(), (int) def.getMax(), iVal);
tf.setMinMaxDefault((int) def.getMin(), (int) def.getMax(), iVal);
if (!def.isEnabled()) tf.setEnabled(false);
if (hover != null) tf.setHoverText(hover);
sw.addTextField(tf);
@@ -411,6 +411,9 @@ public class GuiNPCManageDialogs extends GuiNPCInterface2 implements IScrollData
}
if (subgui instanceof SubGuiNpcDialog) {
saveType(true);
if (dialog != null && dialog.id >= 0) {
setPrevDialogName(dialog.title);
}
}
}
@@ -401,6 +401,9 @@ public class GuiNPCManageQuest extends GuiNPCInterface2 implements IScrollData,
}
if (subgui instanceof SubGuiNpcQuest) {
saveType(true);
if (quest != null && quest.id >= 0) {
setPrevQuestName(quest.title);
}
}
}
@@ -15,7 +15,7 @@ import org.lwjgl.opengl.GL11;
public class GuiQuestCompletion extends GuiNPCInterface implements ITopButtonListener {
private Quest quest;
private final ResourceLocation resource = new ResourceLocation("customnpcs", "textures/gui/smallbg.png");
private final ResourceLocation resource = new ResourceLocation("customnpcs", "textures/gui/menubg.png");
public GuiQuestCompletion(Quest quest) {
super();
@@ -20,7 +20,7 @@ import java.util.Vector;
public class GuiTransportSelection extends GuiNPCInterface implements ITopButtonListener, IScrollData {
private final ResourceLocation resource = new ResourceLocation("customnpcs", "textures/gui/smallbg.png");
private final ResourceLocation resource = new ResourceLocation("customnpcs", "textures/gui/menubg.png");
protected int xSize;
protected int guiLeft;
protected int guiTop;
@@ -16,7 +16,7 @@ import org.lwjgl.opengl.GL11;
import java.util.HashMap;
public class GuiScriptGlobal extends GuiNPCInterface implements IGuiData {
private final ResourceLocation resource = new ResourceLocation("customnpcs", "textures/gui/smallbg.png");
private final ResourceLocation resource = new ResourceLocation("customnpcs", "textures/gui/menubg.png");
private HashMap<Integer, Class<?>> scriptGuiClasses = new HashMap<>();
public GuiScriptGlobal() {
@@ -15,7 +15,7 @@ public class SubGuiNpcCooldownPicker extends SubGuiInterface implements ITextfie
public SubGuiNpcCooldownPicker(boolean isMCCustom, long currentCooldown) {
this.isMCCustom = isMCCustom;
this.cooldownValue = currentCooldown;
setBackground("smallbg.png");
setBackground("menubg.png");
xSize = 220;
ySize = 222;
closeOnEsc = true;
@@ -289,7 +289,15 @@ public class AuctionController implements IAuctionHandler {
// Global claims are not tied to a single player.
claim.playerUUID = null;
claim.playerName = "Global";
globalClaims.add(claim);
synchronized (globalClaims) {
// Prevent duplicate claim IDs
for (AuctionClaim existing : globalClaims) {
if (existing.id.equals(claim.id)) {
return;
}
}
globalClaims.add(claim);
}
markDirty();
}
@@ -305,19 +313,23 @@ public class AuctionController implements IAuctionHandler {
private AuctionClaim getGlobalClaimInternal(String claimId) {
if (claimId == null || claimId.isEmpty()) return null;
for (AuctionClaim claim : globalClaims) {
if (claim != null && !claim.claimed && claimId.equals(claim.id)) {
return claim;
synchronized (globalClaims) {
for (AuctionClaim claim : globalClaims) {
if (claim != null && !claim.claimed && claimId.equals(claim.id)) {
return claim;
}
}
}
return null;
}
private boolean claimAndRemoveGlobal(String claimId) {
AuctionClaim claim = getGlobalClaimInternal(claimId);
if (claim == null) return false;
claim.claimed = true;
return globalClaims.remove(claim);
synchronized (globalClaims) {
AuctionClaim claim = getGlobalClaimInternal(claimId);
if (claim == null) return false;
claim.claimed = true;
return globalClaims.remove(claim);
}
}
/**
@@ -358,15 +370,27 @@ public class AuctionController implements IAuctionHandler {
return "This item cannot be listed on the Auction House.";
}
// Validate minimum price
// Validate prices
if (startingPrice <= 0) {
return "Starting price must be positive.";
}
if (startingPrice < ConfigMarket.MinimumListingPrice) {
return "Starting price must be at least " + ConfigMarket.MinimumListingPrice + " " + ConfigMarket.CurrencyName + ".";
}
if (startingPrice > ConfigMarket.MaxBalance) {
return "Starting price exceeds maximum allowed value.";
}
// Validate buyout price if set
if (buyoutPrice < 0) {
return "Buyout price cannot be negative.";
}
if (buyoutPrice > 0 && buyoutPrice < startingPrice) {
return "Buyout price must be higher than starting price.";
}
if (buyoutPrice > ConfigMarket.MaxBalance) {
return "Buyout price exceeds maximum allowed value.";
}
UUID playerUUID = player.getUniqueID();
String playerName = player.getCommandSenderName();
@@ -386,7 +410,7 @@ public class AuctionController implements IAuctionHandler {
PlayerTradeData currency = playerData.tradeData;
long fee = ConfigMarket.ListingFee;
if (!currency.canAfford(fee)) {
if (fee > 0 && !currency.canAfford(fee)) {
return "You cannot afford the listing fee (" + fee + " " + ConfigMarket.CurrencyName + ").";
}
@@ -400,7 +424,7 @@ public class AuctionController implements IAuctionHandler {
}
// Deduct fee
if (!currency.withdraw(fee)) {
if (fee > 0 && !currency.withdraw(fee)) {
return "Failed to deduct listing fee.";
}
@@ -435,9 +459,16 @@ public class AuctionController implements IAuctionHandler {
return "No item provided.";
}
// Validate prices
if (startingPrice <= 0) {
return "Starting price must be positive.";
}
if (startingPrice < ConfigMarket.MinimumListingPrice) {
return "Starting price must be at least " + ConfigMarket.MinimumListingPrice + " " + ConfigMarket.CurrencyName + ".";
}
if (buyoutPrice < 0) {
return "Buyout price cannot be negative.";
}
if (buyoutPrice > 0 && buyoutPrice < startingPrice) {
return "Buyout price must be higher than starting price.";
}
@@ -514,7 +545,13 @@ public class AuctionController implements IAuctionHandler {
if (penalty > 0) {
PlayerData sellerData = PlayerDataController.Instance.getData(listing.sellerUUID);
if (sellerData != null) {
sellerData.tradeData.withdraw(penalty);
if (!sellerData.tradeData.withdraw(penalty)) {
// Withdraw whatever they have if they can't cover the full penalty
long available = sellerData.tradeData.getBalance();
if (available > 0) {
sellerData.tradeData.withdraw(available);
}
}
sellerData.save();
}
}
@@ -551,6 +588,11 @@ public class AuctionController implements IAuctionHandler {
return "Auction is disabled.";
}
// Validate bid amount
if (bidAmount <= 0) {
return "Bid amount must be positive.";
}
UUID playerUUID = player.getUniqueID();
String playerName = player.getCommandSenderName();
@@ -641,14 +683,14 @@ public class AuctionController implements IAuctionHandler {
listing.highBidderName = playerName;
listing.currentBid = bidAmount;
listing.bidCount++;
// Add to new bidder's active bids
addToPlayerBids(playerUUID, listing.id);
// Snipe protection (must be inside sync to prevent race with endAuction)
listing.extendForSnipeProtection(ConfigMarket.SnipeProtectionMinutes);
}
// Add to new bidder's active bids
addToPlayerBids(playerUUID, listing.id);
// Snipe protection
listing.extendForSnipeProtection(ConfigMarket.SnipeProtectionMinutes);
markDirty();
logAuction(EnumAuctionLogAction.BID, playerName, listing.item.getDisplayName(), bidAmount,
@@ -834,26 +876,36 @@ public class AuctionController implements IAuctionHandler {
}
public String claimGlobalItem(String claimId, EntityPlayer player) {
AuctionClaim claim = getGlobalClaimInternal(claimId);
if (claim == null) {
return "Global claim not found.";
}
if (!claim.type.isItem()) {
return "This global claim is not an item claim.";
}
if (claim.item == null) {
return "Item data is missing.";
}
String listingId;
String itemName;
synchronized (globalClaims) {
AuctionClaim claim = getGlobalClaimInternal(claimId);
if (claim == null) {
return "Global claim not found.";
}
if (!claim.type.isItem()) {
return "This global claim is not an item claim.";
}
if (claim.item == null) {
return "Item data is missing.";
}
if (!player.inventory.addItemStackToInventory(claim.item.copy())) {
return "Your inventory is full.";
}
// Check inventory space before adding (addItemStackToInventory mutates even on partial add)
if (!canFitInInventory(player, claim.item)) {
return "Not enough inventory space to claim this item.";
}
claimAndRemoveGlobal(claim.id);
cleanupListingIfComplete(claim.listingId);
// Add to inventory (safe - we verified space above)
player.inventory.addItemStackToInventory(claim.item.copy());
listingId = claim.listingId;
itemName = claim.item.getDisplayName();
claimAndRemoveGlobal(claim.id);
}
cleanupListingIfComplete(listingId);
markDirty();
logAuction(EnumAuctionLogAction.CLAIMED, player.getCommandSenderName(), claim.item.getDisplayName(), 0,
logAuction(EnumAuctionLogAction.CLAIMED, player.getCommandSenderName(), itemName, 0,
"Global item claim");
return null;
}
@@ -862,19 +914,25 @@ public class AuctionController implements IAuctionHandler {
* Global currency claims are acknowledged/cleared and do not alter player balances.
*/
public String claimGlobalCurrency(String claimId, EntityPlayer player) {
AuctionClaim claim = getGlobalClaimInternal(claimId);
if (claim == null) {
return "Global claim not found.";
}
if (!claim.type.isCurrency()) {
return "This global claim is not a currency claim.";
}
String listingId;
long currency;
synchronized (globalClaims) {
AuctionClaim claim = getGlobalClaimInternal(claimId);
if (claim == null) {
return "Global claim not found.";
}
if (!claim.type.isCurrency()) {
return "This global claim is not a currency claim.";
}
claimAndRemoveGlobal(claim.id);
cleanupListingIfComplete(claim.listingId);
listingId = claim.listingId;
currency = claim.currency;
claimAndRemoveGlobal(claim.id);
}
cleanupListingIfComplete(listingId);
markDirty();
logAuction(EnumAuctionLogAction.CLAIMED, player.getCommandSenderName(), ConfigMarket.CurrencyName, claim.currency,
logAuction(EnumAuctionLogAction.CLAIMED, player.getCommandSenderName(), ConfigMarket.CurrencyName, currency,
"Global currency claim acknowledged (no balance change)");
return null;
}
@@ -910,11 +968,14 @@ public class AuctionController implements IAuctionHandler {
return "Claim was prevented.";
}
// Check inventory space
if (!player.inventory.addItemStackToInventory(claim.item.copy())) {
return "Your inventory is full.";
// Check inventory space before adding (addItemStackToInventory mutates even on partial add)
if (!canFitInInventory(player, claim.item)) {
return "Not enough inventory space to claim this item.";
}
// Add to inventory (safe - we verified space above)
player.inventory.addItemStackToInventory(claim.item.copy());
// Remove claim from player's auction data
playerData.tradeData.claimAndRemove(claimId);
playerData.updateClient = true;
@@ -973,6 +1034,34 @@ public class AuctionController implements IAuctionHandler {
return null; // Success
}
/**
* Check if a player's inventory can fit the entire ItemStack without partial adds.
* Unlike addItemStackToInventory, this does NOT mutate the inventory.
*/
private boolean canFitInInventory(EntityPlayer player, ItemStack stack) {
int remaining = stack.stackSize;
int maxStack = stack.getMaxStackSize();
ItemStack[] mainInventory = player.inventory.mainInventory;
for (int i = 0; i < mainInventory.length; i++) {
if (remaining <= 0) return true;
ItemStack slot = mainInventory[i];
if (slot == null) {
remaining -= Math.min(remaining, maxStack);
} else if (slot.getItem() == stack.getItem()
&& slot.getItemDamage() == stack.getItemDamage()
&& ItemStack.areItemStackTagsEqual(slot, stack)) {
int spaceInSlot = maxStack - slot.stackSize;
if (spaceInSlot > 0) {
remaining -= Math.min(remaining, spaceInSlot);
}
}
}
return remaining <= 0;
}
/**
* Clean up a listing if it's ended and no longer needed.
* Now just removes ended listings after some delay since claims are in PlayerData.
@@ -1019,6 +1108,7 @@ public class AuctionController implements IAuctionHandler {
for (AuctionListing listing : listings.values()) {
if (listing.status != EnumAuctionStatus.ACTIVE) continue;
if (listing.isExpired()) continue;
// Apply search filter (searches item name AND seller name)
if (filter.hasSearchText()) {
@@ -1069,6 +1159,7 @@ public class AuctionController implements IAuctionHandler {
int count = 0;
for (AuctionListing listing : listings.values()) {
if (listing.status != EnumAuctionStatus.ACTIVE) continue;
if (listing.isExpired()) continue;
if (filter.hasSearchText()) {
String itemName = listing.item != null ? listing.item.getDisplayName() : "";
String sellerName = listing.sellerName != null ? listing.sellerName : "";
@@ -1755,20 +1846,12 @@ public class AuctionController implements IAuctionHandler {
String itemName = listing.item != null ? listing.item.getDisplayName() : "Unknown Item";
// Return active highest bid.
// Return active highest bid - always refund to the actual bidder
if (listing.hasBids() && listing.highBidderUUID != null) {
AuctionClaim refundClaim;
if (cancelCompletely) {
refundClaim = AuctionClaim.createRefundClaim(
null, "Global", listing.id, listing.currentBid, itemName,
listing.highBidderName, null);
addGlobalClaim(refundClaim);
} else {
refundClaim = AuctionClaim.createRefundClaim(
listing.highBidderUUID, listing.highBidderName, listing.id, listing.currentBid, itemName,
listing.sellerName, null);
addClaimToPlayer(listing.highBidderUUID, refundClaim);
}
AuctionClaim refundClaim = AuctionClaim.createRefundClaim(
listing.highBidderUUID, listing.highBidderName, listing.id, listing.currentBid, itemName,
listing.sellerName, null);
addClaimToPlayer(listing.highBidderUUID, refundClaim);
removeFromPlayerBids(listing.highBidderUUID, listing.id);
}
@@ -5,6 +5,7 @@ import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.nbt.NBTTagList;
import noppes.npcs.CustomNpcs;
import noppes.npcs.LogWriter;
import noppes.npcs.api.handler.ILinkedItemHandler;
import noppes.npcs.api.handler.data.ILinkedItem;
import noppes.npcs.api.item.IItemStack;
import noppes.npcs.controllers.data.IScriptHandler;
@@ -21,7 +22,7 @@ import java.io.IOException;
import java.util.HashMap;
import java.util.zip.GZIPInputStream;
public class LinkedItemController {
public class LinkedItemController implements ILinkedItemHandler {
private static LinkedItemController Instance;
private int lastUsedID = 0;
@@ -40,25 +41,28 @@ public class LinkedItemController {
return Instance;
}
public LinkedItem createItem(String name) {
@Override
public ILinkedItem createItem(String name) {
return new LinkedItem(name);
}
@Override
public IItemStack createItemStack(int id) {
LinkedItem linkedItem = this.get(id);
LinkedItem linkedItem = (LinkedItem) this.get(id);
if (linkedItem != null) {
return linkedItem.createStack();
}
return null;
}
public void add(LinkedItem linkedItem) {
@Override
public void add(ILinkedItem linkedItem) {
if (linkedItem != null) {
String name = linkedItem.getName();
if (name != null && !name.isEmpty()) {
int linkedItemId = linkedItem.getId();
int id = this.linkedItems.containsKey(linkedItemId) ? linkedItemId : this.getUnusedId();
this.linkedItems.put(id, linkedItem);
this.linkedItems.put(id, (LinkedItem) linkedItem);
linkedItem.setId(id);
this.addScript(id);
}
@@ -69,6 +73,7 @@ public class LinkedItemController {
this.linkedItemsScripts.put(id, new LinkedItemScript());
}
@Override
public LinkedItem remove(int id) {
this.removeScript(id);
return this.linkedItems.remove(id);
@@ -78,16 +83,19 @@ public class LinkedItemController {
this.linkedItemsScripts.remove(id);
}
@Override
public LinkedItem get(int id) {
return this.linkedItems.get(id);
return (LinkedItem) this.linkedItems.get(id);
}
@Override
public boolean contains(int id) {
return this.linkedItems.containsKey(id);
}
public boolean contains(LinkedItem linkedItem) {
return this.linkedItems.containsValue(linkedItem);
@Override
public boolean contains(ILinkedItem linkedItem) {
return this.linkedItems.containsValue((LinkedItem) linkedItem);
}
public IScriptHandler getScriptHandler(int id) {
@@ -219,7 +227,7 @@ public class LinkedItemController {
}
public void delete(int id) {
LinkedItem linkedItem = get(id);
LinkedItem linkedItem = (LinkedItem) get(id);
if (linkedItem != null) {
LinkedItem foundItem = remove(id);
if (foundItem != null && foundItem.name != null) {
@@ -91,14 +91,6 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA
*/
private transient byte lastSyncedFlags = 0;
/**
* Dirty flag: set when ability data changes (unlock/lock/select/toggle).
* Checked by the periodic 10-tick sync handler, which sends an ability
* data packet and clears the flag. Avoids scattering sync calls across
* every mutation method.
*/
public transient boolean needsSync = false;
/**
* World time when the last ability was activated (for double-press cancel detection).
*/
@@ -497,7 +489,6 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA
@Override
protected void onToggleStateChanged(String key, boolean active, int state) {
needsSync = true;
}
@Override
@@ -534,20 +525,17 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA
public void setSelectedIndex(int index) {
if (index == -1 || (index >= 0 && index < unlockedAbilities.size())) {
this.selectedIndex = index;
needsSync = true;
}
}
public void selectNext() {
if (unlockedAbilities.isEmpty()) return;
selectedIndex = (selectedIndex + 1) % unlockedAbilities.size();
needsSync = true;
}
public void selectPrevious() {
if (unlockedAbilities.isEmpty()) return;
selectedIndex = (selectedIndex - 1 + unlockedAbilities.size()) % unlockedAbilities.size();
needsSync = true;
}
public String getSelectedAbilityKey() {
@@ -576,13 +564,11 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA
if (selectedIndex >= unlockedAbilities.size()) {
selectedIndex = Math.max(0, unlockedAbilities.size() - 1);
}
needsSync = true;
}
public void unlockAbility(String key) {
if (key != null && !key.isEmpty() && !unlockedAbilities.contains(key)) {
unlockedAbilities.add(key);
needsSync = true;
}
}
@@ -591,7 +577,6 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA
if (selectedIndex >= unlockedAbilities.size()) {
selectedIndex = Math.max(0, unlockedAbilities.size() - 1);
}
needsSync = true;
}
public boolean hasUnlockedAbility(String key) {
@@ -837,7 +822,6 @@ public class PlayerAbilityData extends AbstractDataAbilities implements IPlayerA
// Reset sync tracking so next sync sends fresh state
lastSyncedFlags = 0;
needsSync = true;
}
/**
@@ -245,8 +245,14 @@ public class PlayerTradeData implements IPlayerTradeData {
* Add a claim to this player's pending claims.
* Thread-safe - can be called from any thread.
*/
public void addClaim(AuctionClaim claim) {
public synchronized void addClaim(AuctionClaim claim) {
if (claim != null && !claim.claimed) {
// Prevent duplicate claim IDs
for (AuctionClaim existing : claims) {
if (existing.id.equals(claim.id)) {
return;
}
}
claims.add(claim);
}
}
@@ -276,8 +282,9 @@ public class PlayerTradeData implements IPlayerTradeData {
/**
* Get a specific claim by ID (internal use).
* Synchronized to pair with claimAndRemove for thread safety.
*/
public AuctionClaim getClaimInternal(String claimId) {
public synchronized AuctionClaim getClaimInternal(String claimId) {
for (AuctionClaim claim : claims) {
if (claim.id.equals(claimId) && !claim.claimed) {
return claim;
@@ -296,8 +303,9 @@ public class PlayerTradeData implements IPlayerTradeData {
/**
* Mark a claim as claimed and remove it.
* Synchronized to prevent double-claim from concurrent threads.
*/
public boolean claimAndRemove(String claimId) {
public synchronized boolean claimAndRemove(String claimId) {
for (AuctionClaim claim : claims) {
if (claim.id.equals(claimId) && !claim.claimed) {
claim.claimed = true;
@@ -91,13 +91,11 @@ public abstract class EntityNPCFlying extends EntityNPCInterface {
this.prevLimbSwingAmount = this.limbSwingAmount;
double distanceX = this.posX - this.prevPosX;
double distanceZ = this.posZ - this.prevPosZ;
float distance = MathHelper.sqrt_double(distanceX * distanceX + distanceZ * distanceZ);
float swingScale = this.getNavigator().noPath() ? 0.35F : 0.9F;
distance *= swingScale;
float distance = MathHelper.sqrt_double(distanceX * distanceX + distanceZ * distanceZ) * 4.0F;
if (distance > 1.0F) {
distance = 1.0F;
}
this.limbSwingAmount += (distance - this.limbSwingAmount) * 0.18F;
this.limbSwingAmount += (distance - this.limbSwingAmount) * 0.4F;
this.limbSwing += this.limbSwingAmount;
}
@@ -33,7 +33,7 @@ public class ItemLinked extends ItemCustomizable {
IItemStack itemStack = NpcAPI.Instance().getIItemStack(stack);
if (itemStack instanceof ScriptLinkedItem) {
ScriptLinkedItem scriptLinkedItem = (ScriptLinkedItem) itemStack;
LinkedItem linkedItem = LinkedItemController.getInstance().get(scriptLinkedItem.linkedItem.getId());
LinkedItem linkedItem = (LinkedItem) LinkedItemController.getInstance().get(scriptLinkedItem.linkedItem.getId());
int prevVersion = scriptLinkedItem.linkedVersion;
if (linkedItem != null && scriptLinkedItem.linkedVersion != linkedItem.version) {
scriptLinkedItem.linkedItem = linkedItem.clone();
+6 -34
View File
@@ -71,24 +71,7 @@ import noppes.npcs.api.entity.ICustomNpc;
import noppes.npcs.api.entity.IEntity;
import noppes.npcs.api.entity.IPlayer;
import noppes.npcs.api.gui.ICustomGui;
import noppes.npcs.api.handler.IAbilityHandler;
import noppes.npcs.api.handler.IActionManager;
import noppes.npcs.api.handler.IAnimationHandler;
import noppes.npcs.api.handler.IAttributeHandler;
import noppes.npcs.api.handler.IAuctionHandler;
import noppes.npcs.api.handler.ICloneHandler;
import noppes.npcs.api.handler.ICustomEffectHandler;
import noppes.npcs.api.handler.IDialogHandler;
import noppes.npcs.api.handler.IFactionHandler;
import noppes.npcs.api.handler.IMagicHandler;
import noppes.npcs.api.handler.INaturalSpawnsHandler;
import noppes.npcs.api.handler.IPartyHandler;
import noppes.npcs.api.handler.IProfileHandler;
import noppes.npcs.api.handler.IQuestHandler;
import noppes.npcs.api.handler.IRecipeHandler;
import noppes.npcs.api.handler.IScriptHookHandler;
import noppes.npcs.api.handler.ITelegraphHandler;
import noppes.npcs.api.handler.ITransportHandler;
import noppes.npcs.api.handler.*;
import noppes.npcs.api.handler.data.IAnimation;
import noppes.npcs.api.handler.data.IFrame;
import noppes.npcs.api.handler.data.IFramePart;
@@ -99,22 +82,7 @@ import noppes.npcs.compat.PixelmonHelper;
import noppes.npcs.config.ConfigScript;
import noppes.npcs.constants.EnumAnimationPart;
import noppes.npcs.containers.ContainerNpcInterface;
import noppes.npcs.controllers.AnimationController;
import noppes.npcs.controllers.AuctionController;
import noppes.npcs.controllers.ChunkController;
import noppes.npcs.controllers.CustomEffectController;
import noppes.npcs.controllers.DialogController;
import noppes.npcs.controllers.FactionController;
import noppes.npcs.controllers.MagicController;
import noppes.npcs.controllers.PartyController;
import noppes.npcs.controllers.QuestController;
import noppes.npcs.controllers.RecipeController;
import noppes.npcs.controllers.ScriptController;
import noppes.npcs.controllers.ScriptEntityData;
import noppes.npcs.controllers.ScriptHookController;
import noppes.npcs.controllers.ServerCloneController;
import noppes.npcs.controllers.SpawnController;
import noppes.npcs.controllers.TransportController;
import noppes.npcs.controllers.*;
import noppes.npcs.controllers.data.Animation;
import noppes.npcs.controllers.data.Frame;
import noppes.npcs.controllers.data.FramePart;
@@ -383,6 +351,10 @@ public class NpcAPI extends AbstractNpcAPI {
return AnimationController.Instance;
}
public ILinkedItemHandler getLinkedItems() {
return LinkedItemController.getInstance();
}
public IScriptHookHandler getScriptHooks() {
return ScriptHookController.Instance;
}
@@ -45,12 +45,12 @@ public class ScriptEnergyLaser<T extends EntityAbilityLaser> extends ScriptEnerg
entity.setExpansionSpeed(speed);
}
public int getLingerTicks() {
return entity.getLingerTicks();
public float getMaxLength() {
return entity.getMaxLength();
}
public void setLingerTicks(int ticks) {
entity.setLingerTicks(ticks);
public void setMaxLength(float maxLength) {
entity.setMaxLength(maxLength);
}
public float getCurrentLength() {
@@ -89,18 +89,6 @@ public class ScriptEnergyLaser<T extends EntityAbilityLaser> extends ScriptEnerg
return entity.getEndZ();
}
public float getLingerAlpha() {
return entity.getLingerAlpha();
}
public boolean isLockVerticalDirection() {
return entity.isLockVerticalDirection();
}
public void setLockVerticalDirection(boolean lock) {
entity.setLockVerticalDirection(lock);
}
// ==================== FIRE ====================
@Override
@@ -18,6 +18,7 @@ import noppes.npcs.scripted.NpcAPI;
public class AbilityEvent extends NpcEvent implements IAbilityEvent {
protected final Ability ability;
protected final IEntityLivingBase target;
private transient IAbility abilityCopy;
public AbilityEvent(ICustomNpc npc, Ability ability, EntityLivingBase target) {
super(npc);
@@ -27,7 +28,10 @@ public class AbilityEvent extends NpcEvent implements IAbilityEvent {
@Override
public IAbility getAbility() {
return ability;
if (abilityCopy == null && ability != null) {
abilityCopy = ability.deepCopy();
}
return abilityCopy;
}
@Override
@@ -18,6 +18,7 @@ import noppes.npcs.scripted.NpcAPI;
public class PlayerAbilityEvent extends PlayerEvent implements IPlayerAbilityEvent {
protected final Ability ability;
protected final IEntityLivingBase target;
private transient IAbility abilityCopy;
public PlayerAbilityEvent(IPlayer player, Ability ability, EntityLivingBase target) {
super(player);
@@ -27,7 +28,10 @@ public class PlayerAbilityEvent extends PlayerEvent implements IPlayerAbilityEve
@Override
public IAbility getAbility() {
return ability;
if (abilityCopy == null && ability != null) {
abilityCopy = ability.deepCopy();
}
return abilityCopy;
}
@Override
@@ -36,7 +36,12 @@ public class ScriptDataAbilities implements IDataAbilities {
@Override
public IAbility[] getAbilities() {
return data.getAbilities().toArray(new IAbility[0]);
List<Ability> live = data.getAbilities();
IAbility[] copies = new IAbility[live.size()];
for (int i = 0; i < live.size(); i++) {
copies[i] = live.get(i).deepCopy();
}
return copies;
}
@Override
@@ -73,7 +78,8 @@ public class ScriptDataAbilities implements IDataAbilities {
@Override
public IAbility getAbility(String abilityId) {
return data.getAbility(abilityId);
Ability a = data.getAbility(abilityId);
return a != null ? a.deepCopy() : null;
}
@Override
@@ -115,6 +121,17 @@ public class ScriptDataAbilities implements IDataAbilities {
@Override
public IAbility getCurrentAbility() {
Ability a = data.getCurrentAbility();
return a != null ? a.deepCopy() : null;
}
@Override
public IAbility getSourceAbility(String abilityId) {
return data.getAbility(abilityId);
}
@Override
public IAbility getSourceCurrentAbility() {
return data.getCurrentAbility();
}
@@ -85,7 +85,8 @@ public class ScriptPlayerAbilityData implements IPlayerAbilityData {
@Override
public IAbility getCurrentAbility() {
return data.getCurrentAbility();
Ability a = data.getCurrentAbility();
return a != null ? a.deepCopy() : null;
}
@Override
@@ -1693,7 +1693,7 @@ ability.cnpc.effect=Effect
ability.cnpc.hazard=Hazard
ability.cnpc.trap=Trap
ability.cnpc.disc=Disc
ability.cnpc.laser_shot=Laser Shot
ability.cnpc.laser_shot=Laser
ability.cnpc.sweeper=Sweeper
# ═══════════════════════════════════════════════════════════════
@@ -1822,6 +1822,7 @@ ability.multiHitDelay=Multi-Hit Delay
ability.maxHits=Max Hits
ability.distance=Distance
ability.maxDistance=Max Distance
ability.maxLength=Max Length
ability.lifetime=Lifetime
ability.stunTicks=Stun Ticks
ability.hitWidth=Hit Width
@@ -1829,7 +1830,6 @@ ability.hitLength=Hit Length
ability.effects=Effects
ability.effect.burn=Burn
ability.mode=Mode
ability.duration=Duration
ability.anchorPoint=Anchor Point
ability.anchor.offsetX=Offset X
ability.anchor.offsetY=Offset Y