From c80bd9dc7561ded272cb382e90210e2132257ae8 Mon Sep 17 00:00:00 2001 From: 3944Realms Date: Wed, 8 Apr 2026 16:05:55 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20nPC=20AI=20=E7=BC=96=E5=86=99=EF=BC=88?= =?UTF-8?q?=E6=9C=AA=E5=AE=8C=E6=88=90=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../content/entity/npc/INPCPlayer.java | 11 +- .../content/entity/npc/NPCServerPlayer.java | 271 +++++++-- .../ai/control/NPCBodyRotationControl.java | 83 +++ .../entity/npc/ai/control/NPCJumpControl.java | 37 ++ .../entity/npc/ai/control/NPCLookControl.java | 145 +++++ .../entity/npc/ai/control/NPCMoveControl.java | 193 ++++++ .../entity/npc/ai/goal/NPCFindTargetGoal.java | 150 +++++ .../entity/npc/ai/goal/NPCFollowGoal.java | 61 ++ .../NPCGroupGroundPathNavigation.java | 165 +++++ .../npc/ai/navigation/NPCPathNavigation.java | 463 ++++++++++++++ .../NPCWaterBoundPathNavigation.java | 68 +++ .../pathfinder/NPCFlyNodeEvaluator.java | 325 ++++++++++ .../pathfinder/NPCNodeEvaluator.java | 117 ++++ .../navigation/pathfinder/NPCPathFinder.java | 159 +++++ .../pathfinder/NPCSwimNodeEvaluator.java | 147 +++++ .../pathfinder/NPCWalkNodeEvaluator.java | 574 ++++++++++++++++++ .../core/event/CommonHandler.java | 2 + .../player_animation/x_corss_pose_01.json | 64 -- 18 files changed, 2926 insertions(+), 109 deletions(-) create mode 100644 src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/ai/control/NPCBodyRotationControl.java create mode 100644 src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/ai/control/NPCJumpControl.java create mode 100644 src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/ai/control/NPCLookControl.java create mode 100644 src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/ai/control/NPCMoveControl.java create mode 100644 src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/ai/goal/NPCFindTargetGoal.java create mode 100644 src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/ai/goal/NPCFollowGoal.java create mode 100644 src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/ai/navigation/NPCGroupGroundPathNavigation.java create mode 100644 src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/ai/navigation/NPCPathNavigation.java create mode 100644 src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/ai/navigation/NPCWaterBoundPathNavigation.java create mode 100644 src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/ai/navigation/pathfinder/NPCFlyNodeEvaluator.java create mode 100644 src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/ai/navigation/pathfinder/NPCNodeEvaluator.java create mode 100644 src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/ai/navigation/pathfinder/NPCPathFinder.java create mode 100644 src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/ai/navigation/pathfinder/NPCSwimNodeEvaluator.java create mode 100644 src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/ai/navigation/pathfinder/NPCWalkNodeEvaluator.java delete mode 100644 src/main/resources/assets/eroticdungeongame/player_animation/x_corss_pose_01.json diff --git a/src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/INPCPlayer.java b/src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/INPCPlayer.java index 69c25a9b..674ca723 100644 --- a/src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/INPCPlayer.java +++ b/src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/INPCPlayer.java @@ -16,5 +16,14 @@ package top.r3944realms.eroticdungeongame.content.entity.npc; -public interface INPCPlayer { +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.Targeting; +import org.jetbrains.annotations.Nullable; + +public interface INPCPlayer extends Targeting { + @Nullable + @Override + default LivingEntity getTarget() { + return null; + } } diff --git a/src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/NPCServerPlayer.java b/src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/NPCServerPlayer.java index 4d9c309c..c98e7eea 100644 --- a/src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/NPCServerPlayer.java +++ b/src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/NPCServerPlayer.java @@ -16,6 +16,7 @@ package top.r3944realms.eroticdungeongame.content.entity.npc; +import com.google.common.collect.Maps; import com.mojang.authlib.GameProfile; import dev.dubhe.curtain.utils.Messenger; import net.minecraft.core.BlockPos; @@ -35,20 +36,35 @@ import net.minecraft.server.players.GameProfileCache; import net.minecraft.world.damagesource.DamageSource; import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.EquipmentSlot; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.Mob; +import net.minecraft.world.entity.ai.goal.Goal; +import net.minecraft.world.entity.ai.goal.GoalSelector; import net.minecraft.world.entity.player.Player; +import net.minecraft.world.entity.vehicle.Boat; import net.minecraft.world.food.FoodData; import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.GameType; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.entity.SkullBlockEntity; import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.pathfinder.BlockPathTypes; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import top.r3944realms.eroticdungeongame.EroticDungeon; +import top.r3944realms.eroticdungeongame.content.entity.npc.ai.control.NPCBodyRotationControl; +import top.r3944realms.eroticdungeongame.content.entity.npc.ai.control.NPCJumpControl; +import top.r3944realms.eroticdungeongame.content.entity.npc.ai.control.NPCLookControl; +import top.r3944realms.eroticdungeongame.content.entity.npc.ai.control.NPCMoveControl; +import top.r3944realms.eroticdungeongame.content.entity.npc.ai.goal.NPCFindTargetGoal; +import top.r3944realms.eroticdungeongame.content.entity.npc.ai.goal.NPCFollowGoal; +import top.r3944realms.eroticdungeongame.content.entity.npc.ai.navigation.NPCGroupGroundPathNavigation; +import top.r3944realms.eroticdungeongame.content.entity.npc.ai.navigation.NPCPathNavigation; import top.r3944realms.eroticdungeongame.core.network.NPCEmptyClientConnection; import top.r3944realms.eroticdungeongame.util.IEDGMinecraftServer; import top.r3944realms.lib39.util.nbt.NBTWriter; +import java.util.Map; import java.util.Objects; import java.util.concurrent.atomic.AtomicReference; @@ -56,6 +72,20 @@ public class NPCServerPlayer extends ServerPlayer implements INPCPlayer { public static final String PREFIX = "[NPC]"; public String npcNameWithoutPrefix; + @Nullable + private LivingEntity target; + private final GoalSelector goalSelector; + private final GoalSelector targetSelector; + + protected NPCLookControl lookControl; + protected NPCMoveControl moveControl; + protected NPCJumpControl jumpControl; + private final NPCBodyRotationControl bodyRotationControl; + + private final Map pathfindingMalus = Maps.newEnumMap(BlockPathTypes.class); + + protected NPCPathNavigation navigation; + public String getNpcNameWithoutPrefix() { return npcNameWithoutPrefix; } @@ -66,55 +96,115 @@ public class NPCServerPlayer extends ServerPlayer implements INPCPlayer { public Runnable fixStartingPosition = () -> { }; + public NPCServerPlayer(MinecraftServer server, ServerLevel level, GameProfile gameProfile) { super(server, level, gameProfile); + this.goalSelector = new GoalSelector(server::getProfiler); + this.targetSelector = new GoalSelector(server::getProfiler); + this.navigation = this.createNavigation(level); + this.lookControl = new NPCLookControl(this); + this.moveControl = new NPCMoveControl(this); + this.jumpControl = new NPCJumpControl(this); + this.bodyRotationControl = this.createBodyControl(); getAdvancements().stopListening(); + registerGoals(); } + protected NPCPathNavigation createNavigation(Level level) { + return new NPCGroupGroundPathNavigation(this, level); + } + + protected boolean shouldPassengersInheritMalus() { + return false; + } + + public float getPathfindingMalus(BlockPathTypes nodeType) { + Float f = this.pathfindingMalus.get(nodeType); + return f == null ? nodeType.getMalus() : f; + } + + public void setPathfindingMalus(BlockPathTypes nodeType, float malus) { + this.pathfindingMalus.put(nodeType, malus); + } + + public void onPathfindingStart() { + } + + public void onPathfindingDone() { + } + + + protected void registerGoals() { + // 优先级 0 最高 + this.goalSelector.addGoal(1, new NPCFindTargetGoal(this, 16, 100)); + this.goalSelector.addGoal(2, new NPCFollowGoal(this, 1.0, 3, 2)); + } + + protected NPCBodyRotationControl createBodyControl() { + return new NPCBodyRotationControl(this); + } + + public double getFollowDistance() { + return 2; + } + + public int getMaxHeadXRot() { + return 40; + } + + public int getMaxHeadYRot() { + return 75; + } + + public int getHeadRotSpeed() { + return 10; + } + + @SuppressWarnings("UnusedReturnValue") public static @Nullable NPCServerPlayer createNPC(String username, @NotNull MinecraftServer server, double x, double y, double z, double yaw, double pitch, ResourceKey dimensionId, GameType gamemode, boolean isflying) { ServerLevel worldIn = server.getLevel(dimensionId); - if (worldIn != null) { - GameProfileCache.setUsesAuthentication(false); - GameProfile gameProfile = null; - try { - GameProfileCache profileCache = server.getProfileCache(); - if (profileCache != null) { - gameProfile = profileCache.get(username).orElse(null); - } - } finally { - GameProfileCache.setUsesAuthentication(server.isDedicatedServer() && server.usesAuthentication()); + if (worldIn != null) { + GameProfileCache.setUsesAuthentication(false); + GameProfile gameProfile = null; + try { + GameProfileCache profileCache = server.getProfileCache(); + if (profileCache != null) { + gameProfile = profileCache.get(username).orElse(null); + } + } finally { + GameProfileCache.setUsesAuthentication(server.isDedicatedServer() && server.usesAuthentication()); + } + try { + if (gameProfile == null) { + gameProfile = new GameProfile(UUIDUtil.createOfflinePlayerUUID(username), username); } - try { - if (gameProfile == null) { - gameProfile = new GameProfile(UUIDUtil.createOfflinePlayerUUID(username), username); - } - if (gameProfile.getProperties().containsKey("textures")) { - AtomicReference result = new AtomicReference<>(); - Objects.requireNonNull(result); - SkullBlockEntity.updateGameprofile(gameProfile, result::set); - gameProfile = result.get(); - } - NPCServerPlayer instance = new NPCServerPlayer(server, worldIn, gameProfile); - instance.fixStartingPosition = () -> { - instance.moveTo(x, y, z, (float) yaw, (float) pitch); - }; - ((IEDGMinecraftServer)server).getNPCPlayerList().placeNewNPC(new NPCEmptyClientConnection(PacketFlow.SERVERBOUND), instance); - instance.teleportTo(worldIn, x, y, z, (float) yaw, (float) pitch); - instance.setHealth(20.0F); - instance.unsetRemoved(); - instance.setMaxUpStep(0.6F); - instance.gameMode.changeGameModeForPlayer(gamemode); - ((IEDGMinecraftServer)server).getNPCPlayerList().broadcastAll(new ClientboundRotateHeadPacket(instance, (byte)((int)(instance.yHeadRot * 256.0F / 360.0F))), dimensionId); - ((IEDGMinecraftServer)server).getNPCPlayerList().broadcastAll(new ClientboundTeleportEntityPacket(instance), dimensionId); - instance.entityData.set(DATA_PLAYER_MODE_CUSTOMISATION, (byte)127); - instance.getAbilities().flying = isflying; - return instance; - } catch (Exception e) { - EroticDungeon.getLogger().error("Failed to create NPC", e); + if (gameProfile.getProperties().containsKey("textures")) { + AtomicReference result = new AtomicReference<>(); + Objects.requireNonNull(result); + SkullBlockEntity.updateGameprofile(gameProfile, result::set); + gameProfile = result.get(); } - } else EroticDungeon.getLogger().error("Failed to create NPC because server({}) is null!", dimensionId); - return null; + NPCServerPlayer instance = new NPCServerPlayer(server, worldIn, gameProfile); + instance.fixStartingPosition = () -> { + instance.moveTo(x, y, z, (float) yaw, (float) pitch); + }; + ((IEDGMinecraftServer) server).getNPCPlayerList().placeNewNPC(new NPCEmptyClientConnection(PacketFlow.SERVERBOUND), instance); + instance.teleportTo(worldIn, x, y, z, (float) yaw, (float) pitch); + instance.setHealth(20.0F); + instance.unsetRemoved(); + instance.setMaxUpStep(0.6F); + instance.gameMode.changeGameModeForPlayer(gamemode); + ((IEDGMinecraftServer) server).getNPCPlayerList().broadcastAll(new ClientboundRotateHeadPacket(instance, (byte) ((int) (instance.yHeadRot * 256.0F / 360.0F))), dimensionId); + ((IEDGMinecraftServer) server).getNPCPlayerList().broadcastAll(new ClientboundTeleportEntityPacket(instance), dimensionId); + instance.entityData.set(DATA_PLAYER_MODE_CUSTOMISATION, (byte) 127); + instance.getAbilities().flying = isflying; + return instance; + } catch (Exception e) { + EroticDungeon.getLogger().error("Failed to create NPC", e); + } + } else EroticDungeon.getLogger().error("Failed to create NPC because server({}) is null!", dimensionId); + return null; } @Override @@ -125,6 +215,70 @@ public class NPCServerPlayer extends ServerPlayer implements INPCPlayer { } + @Override + protected void serverAiStep() { + super.serverAiStep(); + int i = Objects.requireNonNull(this.level().getServer()).getTickCount() + this.getId(); + if (i % 2 != 0 && this.tickCount > 1) { + this.level().getProfiler().push("targetSelector"); + this.targetSelector.tickRunningGoals(false); + this.level().getProfiler().pop(); + this.level().getProfiler().push("goalSelector"); + this.goalSelector.tickRunningGoals(false); + this.level().getProfiler().pop(); + } else { + this.level().getProfiler().push("targetSelector"); + this.targetSelector.tick(); + this.level().getProfiler().pop(); + this.level().getProfiler().push("goalSelector"); + this.goalSelector.tick(); + this.level().getProfiler().pop(); + } + this.level().getProfiler().push("navigation"); + this.navigation.tick(); + this.level().getProfiler().pop(); + this.level().getProfiler().push("mob tick"); + + this.level().getProfiler().pop(); + this.level().getProfiler().push("controls"); + this.level().getProfiler().push("move"); + this.moveControl.tick(); + this.level().getProfiler().popPush("look"); + this.lookControl.tick(); + this.level().getProfiler().popPush("jump"); + this.jumpControl.tick(); + this.level().getProfiler().pop(); + this.level().getProfiler().pop(); + + } + + @Override + protected float tickHeadTurn(float yRot, float animStep) { + this.bodyRotationControl.clientTick(); + return animStep; + } + + + protected void updateControlFlags() { + boolean flag = !(this.getControllingPassenger() instanceof Mob); + boolean flag1 = !(this.getVehicle() instanceof Boat); + this.goalSelector.setControlFlag(Goal.Flag.MOVE, flag); + this.goalSelector.setControlFlag(Goal.Flag.JUMP, flag && flag1); + this.goalSelector.setControlFlag(Goal.Flag.LOOK, flag); + } + + public void setZza(float amount) { + this.zza = amount; + } + + public void setYya(float amount) { + this.yya = amount; + } + + public void setXxa(float amount) { + this.xxa = amount; + } + @Override public void kill() { this.kill(Messenger.s("Killed")); @@ -166,9 +320,7 @@ public class NPCServerPlayer extends ServerPlayer implements INPCPlayer { super.addAdditionalSaveData(compound); String npcName = getName().getString(); String npcNameWithoutPrefix = npcName.substring(PREFIX.length()); - NBTWriter.of(compound) - .string("NpcName", npcNameWithoutPrefix) - .compound("NpcGameProfile", NbtUtils.writeGameProfile(new CompoundTag(), this.getGameProfile())); + NBTWriter.of(compound).string("NpcName", npcNameWithoutPrefix).compound("NpcGameProfile", NbtUtils.writeGameProfile(new CompoundTag(), this.getGameProfile())); } @Override @@ -191,9 +343,13 @@ public class NPCServerPlayer extends ServerPlayer implements INPCPlayer { public void tick() { if (this.getServer() != null && this.getServer().getTickCount() % 10 == 0) { this.connection.resetPosition(); - ((ServerLevel)this.level()).getChunkSource().move(this); + ((ServerLevel) this.level()).getChunkSource().move(this); + } + if (!this.level().isClientSide) { + if (this.tickCount % 5 == 0) { + this.updateControlFlags(); + } } - try { super.tick(); this.doTick(); @@ -229,4 +385,31 @@ public class NPCServerPlayer extends ServerPlayer implements INPCPlayer { return clientboundAddPlayerPacket; } + @Nullable + @Override + public LivingEntity getTarget() { + return target; + } + + public void setTarget(@Nullable LivingEntity entity) { + this.target = entity; + } + + public NPCLookControl getLookControl() { + return lookControl; + } + + public NPCMoveControl getMoveControl() { + return moveControl; + } + + public NPCJumpControl getJumpControl() { + return jumpControl; + } + + public NPCPathNavigation getNavigation() { + return navigation; + } + + } diff --git a/src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/ai/control/NPCBodyRotationControl.java b/src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/ai/control/NPCBodyRotationControl.java new file mode 100644 index 00000000..af10f370 --- /dev/null +++ b/src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/ai/control/NPCBodyRotationControl.java @@ -0,0 +1,83 @@ +/* + * Copyright 2025-2026 R3944Realms + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package top.r3944realms.eroticdungeongame.content.entity.npc.ai.control; + +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Mob; +import net.minecraft.world.entity.ai.control.Control; +import top.r3944realms.eroticdungeongame.content.entity.npc.NPCServerPlayer; + +public class NPCBodyRotationControl implements Control { + private final NPCServerPlayer npc; + private static final int HEAD_STABLE_ANGLE = 15; + private static final int DELAY_UNTIL_STARTING_TO_FACE_FORWARD = 10; + private static final int HOW_LONG_IT_TAKES_TO_FACE_FORWARD = 10; + private int headStableTime; + private float lastStableYHeadRot; + + public NPCBodyRotationControl(NPCServerPlayer npc) { + this.npc = npc; + } + public void clientTick() { + if (this.isMoving()) { + this.npc.yBodyRot = this.npc.getYRot(); + this.rotateHeadIfNecessary(); + this.lastStableYHeadRot = this.npc.yHeadRot; + this.headStableTime = 0; + } else { + if (notCarryingMobPassengers()) { + if (Math.abs(this.npc.yHeadRot - this.lastStableYHeadRot) > 15.0F) { + this.headStableTime = 0; + this.lastStableYHeadRot = this.npc.yHeadRot; + this.rotateBodyIfNecessary(); + } else { + ++this.headStableTime; + if (this.headStableTime > 10) { + this.rotateHeadTowardsFront(); + } + } + } + + } + } + + private void rotateBodyIfNecessary() { + this.npc.yBodyRot = Mth.rotateIfNecessary(this.npc.yBodyRot, this.npc.yHeadRot, (float)this.npc.getMaxHeadYRot()); + } + + private void rotateHeadIfNecessary() { + this.npc.yHeadRot = Mth.rotateIfNecessary(this.npc.yHeadRot, this.npc.yBodyRot, (float)this.npc.getMaxHeadYRot()); + } + + private void rotateHeadTowardsFront() { + int i = this.headStableTime - 10; + float f = Mth.clamp((float)i / 10.0F, 0.0F, 1.0F); + float f1 = (float)this.npc.getMaxHeadYRot() * (1.0F - f); + this.npc.yBodyRot = Mth.rotateIfNecessary(this.npc.yBodyRot, this.npc.yHeadRot, f1); + } + + private boolean notCarryingMobPassengers() { + return !(this.npc.getFirstPassenger() instanceof Mob); + } + + private boolean isMoving() { + double d0 = this.npc.getX() - this.npc.xo; + double d1 = this.npc.getZ() - this.npc.zo; + return d0 * d0 + d1 * d1 > (double)2.5000003E-7F; + } + +} diff --git a/src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/ai/control/NPCJumpControl.java b/src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/ai/control/NPCJumpControl.java new file mode 100644 index 00000000..c444eed1 --- /dev/null +++ b/src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/ai/control/NPCJumpControl.java @@ -0,0 +1,37 @@ +/* + * Copyright 2025-2026 R3944Realms + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package top.r3944realms.eroticdungeongame.content.entity.npc.ai.control; + +import top.r3944realms.eroticdungeongame.content.entity.npc.NPCServerPlayer; + +public class NPCJumpControl { + private final NPCServerPlayer npc; + protected boolean jump; + + public NPCJumpControl(NPCServerPlayer npc) { + this.npc = npc; + } + + public void jump() { + this.jump = true; + } + + public void tick() { + this.npc.setJumping(this.jump); + this.jump = false; + } +} diff --git a/src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/ai/control/NPCLookControl.java b/src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/ai/control/NPCLookControl.java new file mode 100644 index 00000000..8ebe3368 --- /dev/null +++ b/src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/ai/control/NPCLookControl.java @@ -0,0 +1,145 @@ +/* + * Copyright 2025-2026 R3944Realms + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package top.r3944realms.eroticdungeongame.content.entity.npc.ai.control; + +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.ai.control.Control; +import net.minecraft.world.phys.Vec3; +import top.r3944realms.eroticdungeongame.content.entity.npc.NPCServerPlayer; + +import java.util.Optional; + +public class NPCLookControl implements Control { + protected final NPCServerPlayer npc; + protected float yMaxRotSpeed; + protected float xMaxRotAngle; + protected int lookAtCooldown; + protected double wantedX; + protected double wantedY; + protected double wantedZ; + + public NPCLookControl(NPCServerPlayer npc) { + this.npc = npc; + } + + + public void setLookAt(Vec3 lookVector) { + this.setLookAt(lookVector.x, lookVector.y, lookVector.z); + } + + + public void setLookAt(Entity entity) { + this.setLookAt(entity.getX(), getWantedY(entity), entity.getZ()); + } + + + public void setLookAt(Entity entity, float deltaYaw, float deltaPitch) { + this.setLookAt(entity.getX(), getWantedY(entity), entity.getZ(), deltaYaw, deltaPitch); + } + + public void setLookAt(double x, double y, double z) { + this.setLookAt(x, y, z, (float)this.npc.getHeadRotSpeed(), (float)this.npc.getMaxHeadXRot()); + } + + /** + * Sets position to look at + */ + public void setLookAt(double x, double y, double z, float deltaYaw, float deltaPitch) { + this.wantedX = x; + this.wantedY = y; + this.wantedZ = z; + this.yMaxRotSpeed = deltaYaw; + this.xMaxRotAngle = deltaPitch; + this.lookAtCooldown = 2; + } + + public void tick() { + if (this.resetXRotOnTick()) { + this.npc.setXRot(0.0F); + } + + if (this.lookAtCooldown > 0) { + --this.lookAtCooldown; + this.getYRotD().ifPresent((p_287447_) -> { + this.npc.yHeadRot = this.rotateTowards(this.npc.yHeadRot, p_287447_, this.yMaxRotSpeed); + }); + this.getXRotD().ifPresent((p_289400_) -> { + this.npc.setXRot(this.rotateTowards(this.npc.getXRot(), p_289400_, this.xMaxRotAngle)); + }); + } else { + this.npc.yHeadRot = this.rotateTowards(this.npc.yHeadRot, this.npc.yBodyRot, 10.0F); + } + + this.clampHeadRotationToBody(); + } + + protected void clampHeadRotationToBody() { + if (!this.npc.getNavigation().isDone()) { + this.npc.yHeadRot = Mth.rotateIfNecessary(this.npc.yHeadRot, this.npc.yBodyRot, (float)this.npc.getMaxHeadYRot()); + } + + } + + protected boolean resetXRotOnTick() { + return true; + } + + public boolean isLookingAtTarget() { + return this.lookAtCooldown > 0; + } + + public double getWantedX() { + return this.wantedX; + } + + public double getWantedY() { + return this.wantedY; + } + + public double getWantedZ() { + return this.wantedZ; + } + + protected Optional getXRotD() { + double d0 = this.wantedX - this.npc.getX(); + double d1 = this.wantedY - this.npc.getEyeY(); + double d2 = this.wantedZ - this.npc.getZ(); + double d3 = Math.sqrt(d0 * d0 + d2 * d2); + return !(Math.abs(d1) > (double)1.0E-5F) && !(Math.abs(d3) > (double)1.0E-5F) ? Optional.empty() : Optional.of((float)(-(Mth.atan2(d1, d3) * (double)(180F / (float)Math.PI)))); + } + + protected Optional getYRotD() { + double d0 = this.wantedX - this.npc.getX(); + double d1 = this.wantedZ - this.npc.getZ(); + return !(Math.abs(d1) > (double)1.0E-5F) && !(Math.abs(d0) > (double)1.0E-5F) ? Optional.empty() : Optional.of((float)(Mth.atan2(d1, d0) * (double)(180F / (float)Math.PI)) - 90.0F); + } + + /** + * Rotate as much as possible from {@code from} to {@code to} within the bounds of {@code maxDelta} + */ + protected float rotateTowards(float from, float to, float maxDelta) { + float f = Mth.degreesDifference(from, to); + float f1 = Mth.clamp(f, -maxDelta, maxDelta); + return from + f1; + } + + private static double getWantedY(Entity entity) { + return entity instanceof LivingEntity ? entity.getEyeY() : (entity.getBoundingBox().minY + entity.getBoundingBox().maxY) / 2.0D; + } +} diff --git a/src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/ai/control/NPCMoveControl.java b/src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/ai/control/NPCMoveControl.java new file mode 100644 index 00000000..88b2b995 --- /dev/null +++ b/src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/ai/control/NPCMoveControl.java @@ -0,0 +1,193 @@ +/* + * Copyright 2025-2026 R3944Realms + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package top.r3944realms.eroticdungeongame.content.entity.npc.ai.control; + +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.tags.BlockTags; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.ai.attributes.Attributes; +import net.minecraft.world.entity.ai.control.Control; +import net.minecraft.world.entity.ai.navigation.PathNavigation; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.pathfinder.BlockPathTypes; +import net.minecraft.world.level.pathfinder.NodeEvaluator; +import net.minecraft.world.phys.shapes.VoxelShape; +import top.r3944realms.eroticdungeongame.content.entity.npc.NPCServerPlayer; +import top.r3944realms.eroticdungeongame.content.entity.npc.ai.navigation.NPCPathNavigation; +import top.r3944realms.eroticdungeongame.content.entity.npc.ai.navigation.pathfinder.NPCNodeEvaluator; + +public class NPCMoveControl implements Control { + public static final float MIN_SPEED = 5.0E-4F; + public static final float MIN_SPEED_SQR = 2.5000003E-7F; + protected static final int MAX_TURN = 90; + protected final NPCServerPlayer npc; + protected double wantedX; + protected double wantedY; + protected double wantedZ; + protected double speedModifier; + protected float strafeForwards; + protected float strafeRight; + protected Operation operation = Operation.WAIT; + + public NPCMoveControl(NPCServerPlayer npc) { + this.npc = npc; + } + + public boolean hasWanted() { + return this.operation == Operation.MOVE_TO; + } + + public double getSpeedModifier() { + return this.speedModifier; + } + + /** + * Sets the speed and location to move to + */ + public void setWantedPosition(double x, double y, double z, double speed) { + this.wantedX = x; + this.wantedY = y; + this.wantedZ = z; + this.speedModifier = speed; + if (this.operation != Operation.JUMPING) { + this.operation = Operation.MOVE_TO; + } + + } + + public void strafe(float forward, float strafe) { + this.operation = Operation.STRAFE; + this.strafeForwards = forward; + this.strafeRight = strafe; + this.speedModifier = 0.25D; + } + + public void tick() { + if (this.operation == Operation.STRAFE) { + float f = (float)this.npc.getAttributeValue(Attributes.MOVEMENT_SPEED); + float f1 = (float)this.speedModifier * f; + float f2 = this.strafeForwards; + float f3 = this.strafeRight; + float f4 = Mth.sqrt(f2 * f2 + f3 * f3); + if (f4 < 1.0F) { + f4 = 1.0F; + } + + f4 = f1 / f4; + f2 *= f4; + f3 *= f4; + float f5 = Mth.sin(this.npc.getYRot() * ((float)Math.PI / 180F)); + float f6 = Mth.cos(this.npc.getYRot() * ((float)Math.PI / 180F)); + float f7 = f2 * f6 - f3 * f5; + float f8 = f3 * f6 + f2 * f5; + if (!this.isWalkable(f7, f8)) { + this.strafeForwards = 1.0F; + this.strafeRight = 0.0F; + } + + this.npc.setSpeed(f1); + this.npc.setZza(this.strafeForwards); + this.npc.setXxa(this.strafeRight); + this.operation = Operation.WAIT; + } else if (this.operation == Operation.MOVE_TO) { + this.operation = Operation.WAIT; + double d0 = this.wantedX - this.npc.getX(); + double d1 = this.wantedZ - this.npc.getZ(); + double d2 = this.wantedY - this.npc.getY(); + double d3 = d0 * d0 + d2 * d2 + d1 * d1; + if (d3 < (double)2.5000003E-7F) { + this.npc.setZza(0.0F); + return; + } + + float f9 = (float)(Mth.atan2(d1, d0) * (double)(180F / (float)Math.PI)) - 90.0F; + this.npc.setYRot(this.rotlerp(this.npc.getYRot(), f9, 90.0F)); + this.npc.setSpeed((float)(this.speedModifier * this.npc.getAttributeValue(Attributes.MOVEMENT_SPEED))); + BlockPos blockpos = this.npc.blockPosition(); + BlockState blockstate = this.npc.level().getBlockState(blockpos); + VoxelShape voxelshape = blockstate.getCollisionShape(this.npc.level(), blockpos); + if (d2 > (double)this.npc.getStepHeight() && d0 * d0 + d1 * d1 < (double)Math.max(1.0F, this.npc.getBbWidth()) || !voxelshape.isEmpty() && this.npc.getY() < voxelshape.max(Direction.Axis.Y) + (double)blockpos.getY() && !blockstate.is(BlockTags.DOORS) && !blockstate.is(BlockTags.FENCES)) { + this.npc.getJumpControl().jump(); + this.operation = Operation.JUMPING; + } + } else if (this.operation == Operation.JUMPING) { + this.npc.setSpeed((float)(this.speedModifier * this.npc.getAttributeValue(Attributes.MOVEMENT_SPEED))); + if (this.npc.onGround()) { + this.operation = Operation.WAIT; + } + } else { + this.npc.setZza(0.0F); + } + + } + + /** + * @return true if the mob can walk successfully to a given X and Z + */ + private boolean isWalkable(float relativeX, float relativeZ) { + NPCPathNavigation pathnavigation = this.npc.getNavigation(); + if (pathnavigation != null) { + NPCNodeEvaluator nodeevaluator = pathnavigation.getNodeEvaluator(); + return nodeevaluator == null || nodeevaluator.getBlockPathType(this.npc.level(), Mth.floor(this.npc.getX() + (double) relativeX), this.npc.getBlockY(), Mth.floor(this.npc.getZ() + (double) relativeZ)) == BlockPathTypes.WALKABLE; + } + + return true; + } + + /** + * Attempt to rotate the first angle to become the second angle, but only allow overall direction change to at max be third parameter + */ + protected float rotlerp(float sourceAngle, float targetAngle, float maximumChange) { + float f = Mth.wrapDegrees(targetAngle - sourceAngle); + if (f > maximumChange) { + f = maximumChange; + } + + if (f < -maximumChange) { + f = -maximumChange; + } + + float f1 = sourceAngle + f; + if (f1 < 0.0F) { + f1 += 360.0F; + } else if (f1 > 360.0F) { + f1 -= 360.0F; + } + + return f1; + } + + public double getWantedX() { + return this.wantedX; + } + + public double getWantedY() { + return this.wantedY; + } + + public double getWantedZ() { + return this.wantedZ; + } + + protected enum Operation { + WAIT, + MOVE_TO, + STRAFE, + JUMPING; + } +} diff --git a/src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/ai/goal/NPCFindTargetGoal.java b/src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/ai/goal/NPCFindTargetGoal.java new file mode 100644 index 00000000..894a05ee --- /dev/null +++ b/src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/ai/goal/NPCFindTargetGoal.java @@ -0,0 +1,150 @@ +/* + * Copyright 2025-2026 R3944Realms + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package top.r3944realms.eroticdungeongame.content.entity.npc.ai.goal; + +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.ai.goal.Goal; +import net.minecraft.world.entity.player.Player; +import top.r3944realms.eroticdungeongame.content.entity.npc.NPCServerPlayer; + +import java.util.Comparator; +import java.util.EnumSet; +import java.util.function.Predicate; + +public class NPCFindTargetGoal extends Goal { + private final NPCServerPlayer npc; + private LivingEntity target; + private final double searchRadius; + private final int cooldown; + private int cooldownTicks; + + // 目标过滤条件 + private Predicate targetFilter; + + // 优先级排序器 + private Comparator targetSorter; + + public NPCFindTargetGoal(NPCServerPlayer npc, double searchRadius, int cooldown) { + this.npc = npc; + this.searchRadius = searchRadius; + this.cooldown = cooldown; + this.setFlags(EnumSet.of(Flag.LOOK)); + + // 默认过滤器:排除自己、死亡、创造模式玩家 + this.targetFilter = entity -> + entity != npc && + entity.isAlive() && + entity instanceof Player player && !player.isCreative(); + + // 默认排序:最近优先 + this.targetSorter = Comparator.comparingDouble(npc::distanceToSqr); + } + + // 允许自定义过滤和排序 + public NPCFindTargetGoal withFilter(Predicate filter) { + this.targetFilter = filter; + return this; + } + + public NPCFindTargetGoal withSorter(Comparator sorter) { + this.targetSorter = sorter; + return this; + } + + @Override + public boolean canUse() { + if (npc.getTarget() != null) return false; + if (cooldownTicks > 0) return false; + + // 寻找有效目标 + this.target = findNearestTarget(); + return this.target != null; + } + + @Override + public boolean canContinueToUse() { + // 继续条件:目标存在且存活,且没有更紧急的目标 + return target != null && + target.isAlive() && + npc.getTarget() == target && + !hasHigherPriorityTarget(); + } + + @Override + public void start() { + if (target != null) { + npc.setTarget(target); + cooldownTicks = cooldown; + } + } + + @Override + public void stop() { + // 不清除 target,让其他 Goal 决定是否保留 + cooldownTicks = cooldown; + } + + @Override + public void tick() { + if (cooldownTicks > 0) { + cooldownTicks--; + } + + // 定期验证目标是否仍然有效 + if (npc.tickCount % 20 == 0 && target != null && !isValidTarget(target)) { + npc.setTarget(null); + target = null; + } + } + + private LivingEntity findNearestTarget() { + return npc.level().getEntitiesOfClass( + LivingEntity.class, + npc.getBoundingBox().inflate(searchRadius), + this::isValidTarget + ).stream() + .min(targetSorter) + .orElse(null); + } + + private boolean isValidTarget(LivingEntity entity) { + // 基础检查 + if (entity == npc) return false; + if (!entity.isAlive()) return false; + if (entity.isRemoved()) return false; + + // 视线检查(性能消耗较大) + if (requiresLineOfSight() && !npc.hasLineOfSight(entity)) { + return false; + } + + // 应用过滤器 + return targetFilter.test(entity); + } + + private boolean hasHigherPriorityTarget() { + // 检查是否有更高优先级 + return npc.getLastHurtByMob() != null && + npc.getLastHurtByMob().isAlive() && + npc.distanceToSqr(npc.getLastHurtByMob()) < searchRadius * searchRadius; + } + + private boolean requiresLineOfSight() { + return true; // 可配置 + } + +} diff --git a/src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/ai/goal/NPCFollowGoal.java b/src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/ai/goal/NPCFollowGoal.java new file mode 100644 index 00000000..3a4a1c07 --- /dev/null +++ b/src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/ai/goal/NPCFollowGoal.java @@ -0,0 +1,61 @@ +/* + * Copyright 2025-2026 R3944Realms + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package top.r3944realms.eroticdungeongame.content.entity.npc.ai.goal; + +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.entity.ai.goal.Goal; +import top.r3944realms.eroticdungeongame.content.entity.npc.NPCServerPlayer; + +import java.util.EnumSet; + +public class NPCFollowGoal extends Goal { + private final NPCServerPlayer npc; + private LivingEntity target; + private final double speedModifier; + private final int followDistance; + private final int stopDistance; + public NPCFollowGoal(NPCServerPlayer npc, double speed, int followDist, int stopDist) { + this.npc = npc; + this.speedModifier = speed; + this.followDistance = followDist; + this.stopDistance = stopDist; + this.setFlags(EnumSet.of(Flag.MOVE, Flag.LOOK)); + } + @Override + public boolean canUse() { + this.target = npc.getTarget(); + return target != null && npc.distanceToSqr(target) > stopDistance; + } + @Override + public boolean canContinueToUse() { + return target != null && npc.distanceToSqr(target) > followDistance; + } + + @Override + public void start() { + npc.getNavigation().moveTo(target, speedModifier); + } + @Override + public void tick() { + npc.getLookControl().setLookAt(target, 30.0f, 30.0f); + if (npc.distanceToSqr(target) > stopDistance) { + npc.getNavigation().moveTo(target, speedModifier); + } else { + npc.getNavigation().stop(); + } + } +} diff --git a/src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/ai/navigation/NPCGroupGroundPathNavigation.java b/src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/ai/navigation/NPCGroupGroundPathNavigation.java new file mode 100644 index 00000000..9c34b365 --- /dev/null +++ b/src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/ai/navigation/NPCGroupGroundPathNavigation.java @@ -0,0 +1,165 @@ +/* + * Copyright 2025-2026 R3944Realms + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package top.r3944realms.eroticdungeongame.content.entity.npc.ai.navigation; + +import net.minecraft.core.BlockPos; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.pathfinder.*; +import net.minecraft.world.phys.Vec3; +import top.r3944realms.eroticdungeongame.content.entity.npc.NPCServerPlayer; +import top.r3944realms.eroticdungeongame.content.entity.npc.ai.navigation.pathfinder.NPCPathFinder; +import top.r3944realms.eroticdungeongame.content.entity.npc.ai.navigation.pathfinder.NPCWalkNodeEvaluator; + +@SuppressWarnings("deprecation") +public class NPCGroupGroundPathNavigation extends NPCPathNavigation { + private boolean avoidSun; + + public NPCGroupGroundPathNavigation(NPCServerPlayer npc, Level level) { + super(npc, level); + } + + protected NPCPathFinder createPathFinder(int maxVisitedNodes) { + this.nodeEvaluator = new NPCWalkNodeEvaluator(); + this.nodeEvaluator.setCanPassDoors(true); + return new NPCPathFinder(this.nodeEvaluator, maxVisitedNodes); + } + + protected boolean canUpdatePath() { + return this.npc.onGround() || this.isInLiquid() || this.npc.isPassenger(); + } + + protected Vec3 getTempMobPos() { + return new Vec3(this.npc.getX(), this.getSurfaceY(), this.npc.getZ()); + } + + /** + * Returns path to given BlockPos + */ + public Path createPath(BlockPos pos, int accuracy) { + if (this.level.getBlockState(pos).isAir()) { + BlockPos blockpos; + for(blockpos = pos.below(); blockpos.getY() > this.level.getMinBuildHeight() && this.level.getBlockState(blockpos).isAir(); blockpos = blockpos.below()) { + } + + if (blockpos.getY() > this.level.getMinBuildHeight()) { + return super.createPath(blockpos.above(), accuracy); + } + + while(blockpos.getY() < this.level.getMaxBuildHeight() && this.level.getBlockState(blockpos).isAir()) { + blockpos = blockpos.above(); + } + + pos = blockpos; + } + + if (!this.level.getBlockState(pos).isSolid()) { + return super.createPath(pos, accuracy); + } else { + BlockPos blockpos1; + for(blockpos1 = pos.above(); blockpos1.getY() < this.level.getMaxBuildHeight() && this.level.getBlockState(blockpos1).isSolid(); blockpos1 = blockpos1.above()) { + } + + return super.createPath(blockpos1, accuracy); + } + } + + /** + * Returns a path to the given entity or null + */ + public Path createPath(Entity entity, int accuracy) { + return this.createPath(entity.blockPosition(), accuracy); + } + + private int getSurfaceY() { + if (this.npc.isInWater() && this.canFloat()) { + int i = this.npc.getBlockY(); + BlockState blockstate = this.level.getBlockState(BlockPos.containing(this.npc.getX(), i, this.npc.getZ())); + int j = 0; + + while(blockstate.is(Blocks.WATER)) { + ++i; + blockstate = this.level.getBlockState(BlockPos.containing(this.npc.getX(), i, this.npc.getZ())); + ++j; + if (j > 16) { + return this.npc.getBlockY(); + } + } + + return i; + } else { + return Mth.floor(this.npc.getY() + 0.5D); + } + } + + protected void trimPath() { + super.trimPath(); + if (this.avoidSun) { + if (this.level.canSeeSky(BlockPos.containing(this.npc.getX(), this.npc.getY() + 0.5D, this.npc.getZ()))) { + return; + } + + if (this.path != null) { + for(int i = 0; i < this.path.getNodeCount(); ++i) { + Node node = this.path.getNode(i); + if (this.level.canSeeSky(new BlockPos(node.x, node.y, node.z))) { + this.path.truncateNodes(i); + return; + } + } + } + } + + } + + protected boolean hasValidPathType(BlockPathTypes pathType) { + if (pathType == BlockPathTypes.WATER) { + return false; + } else if (pathType == BlockPathTypes.LAVA) { + return false; + } else { + return pathType != BlockPathTypes.OPEN; + } + } + + public void setCanOpenDoors(boolean canOpenDoors) { + this.nodeEvaluator.setCanOpenDoors(canOpenDoors); + } + + public boolean canPassDoors() { + return this.nodeEvaluator.canPassDoors(); + } + + public void setCanPassDoors(boolean canPassDoors) { + this.nodeEvaluator.setCanPassDoors(canPassDoors); + } + + public boolean canOpenDoors() { + return this.nodeEvaluator.canPassDoors(); + } + + public void setAvoidSun(boolean avoidSun) { + this.avoidSun = avoidSun; + } + + public void setCanWalkOverFences(boolean canWalkOverFences) { + this.nodeEvaluator.setCanWalkOverFences(canWalkOverFences); + } +} diff --git a/src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/ai/navigation/NPCPathNavigation.java b/src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/ai/navigation/NPCPathNavigation.java new file mode 100644 index 00000000..59ea1923 --- /dev/null +++ b/src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/ai/navigation/NPCPathNavigation.java @@ -0,0 +1,463 @@ +/* + * Copyright 2025-2026 R3944Realms + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package top.r3944realms.eroticdungeongame.content.entity.npc.ai.navigation; + +import com.google.common.collect.ImmutableSet; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Vec3i; +import net.minecraft.tags.BlockTags; +import net.minecraft.util.Mth; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.ClipContext; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.PathNavigationRegion; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.pathfinder.*; +import net.minecraft.world.phys.HitResult; +import net.minecraft.world.phys.Vec3; +import org.jetbrains.annotations.Nullable; +import top.r3944realms.eroticdungeongame.content.entity.npc.NPCServerPlayer; +import top.r3944realms.eroticdungeongame.content.entity.npc.ai.navigation.pathfinder.NPCNodeEvaluator; +import top.r3944realms.eroticdungeongame.content.entity.npc.ai.navigation.pathfinder.NPCPathFinder; + +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public abstract class NPCPathNavigation { + private static final int MAX_TIME_RECOMPUTE = 20; + private static final int STUCK_CHECK_INTERVAL = 100; + private static final float STUCK_THRESHOLD_DISTANCE_FACTOR = 0.25F; + protected final NPCServerPlayer npc; + protected final Level level; + @Nullable + protected Path path; + protected double speedModifier; + protected int tick; + protected int lastStuckCheck; + protected Vec3 lastStuckCheckPos = Vec3.ZERO; + protected Vec3i timeoutCachedNode = Vec3i.ZERO; + protected long timeoutTimer; + protected long lastTimeoutCheck; + protected double timeoutLimit; + protected float maxDistanceToWaypoint = 0.5F; + + protected boolean hasDelayedRecomputation; + protected long timeLastRecompute; + protected NPCNodeEvaluator nodeEvaluator; + @Nullable + private BlockPos targetPos; + /** + * Distance in which a path point counts as target-reaching + */ + private int reachRange; + private float maxVisitedNodesMultiplier = 1.0F; + private final NPCPathFinder pathFinder; + private boolean isStuck; + + public NPCPathNavigation(NPCServerPlayer npc, Level level) { + this.npc = npc; + this.level = level; +// int i = Mth.floor(npc.getAttributeValue(Attributes.FOLLOW_RANGE) * 16.0D); + // 不要使用属性 + int i = Mth.floor(npc.getFollowDistance() * 16.0D); + this.pathFinder = this.createPathFinder(i); + } + + public void resetMaxVisitedNodesMultiplier() { + this.maxVisitedNodesMultiplier = 1.0F; + } + + public void setMaxVisitedNodesMultiplier(float multiplier) { + this.maxVisitedNodesMultiplier = multiplier; + } + + @Nullable + public BlockPos getTargetPos() { + return this.targetPos; + } + + protected abstract NPCPathFinder createPathFinder(int maxVisitedNodes); + + /** + * Sets the speed + */ + public void setSpeedModifier(double speed) { + this.speedModifier = speed; + } + + public void recomputePath() { + if (this.level.getGameTime() - this.timeLastRecompute > 20L) { + if (this.targetPos != null) { + this.path = null; + this.path = this.createPath(this.targetPos, this.reachRange); + this.timeLastRecompute = this.level.getGameTime(); + this.hasDelayedRecomputation = false; + } + } else { + this.hasDelayedRecomputation = true; + } + + } + + /** + * Returns path to given BlockPos + */ + @Nullable + public final Path createPath(double x, double y, double z, int accuracy) { + return this.createPath(BlockPos.containing(x, y, z), accuracy); + } + + /** + * Returns a path to one of the elements of the stream or null + */ + @Nullable + public Path createPath(Stream targets, int accuracy) { + return this.createPath(targets.collect(Collectors.toSet()), 8, false, accuracy); + } + + @Nullable + public Path createPath(Set positions, int distance) { + return this.createPath(positions, 8, false, distance); + } + + /** + * Returns path to given BlockPos + */ + @Nullable + public Path createPath(BlockPos pos, int accuracy) { + return this.createPath(ImmutableSet.of(pos), 8, false, accuracy); + } + + @Nullable + public Path createPath(BlockPos pos, int regionOffset, int accuracy) { + return this.createPath(ImmutableSet.of(pos), 8, false, regionOffset, (float)accuracy); + } + + /** + * Returns a path to the given entity or null + */ + @Nullable + public Path createPath(Entity entity, int accuracy) { + return this.createPath(ImmutableSet.of(entity.blockPosition()), 16, true, accuracy); + } + + /** + * Returns a path to one of the given targets or null + */ + @Nullable + protected Path createPath(Set targets, int regionOffset, boolean offsetUpward, int accuracy) { + return this.createPath(targets, regionOffset, offsetUpward, accuracy, (float)this.npc.getFollowDistance()); + } + + @Nullable + protected Path createPath(Set targets, int regionOffset, boolean offsetUpward, int accuracy, float followRange) { + if (targets.isEmpty()) { + return null; + } else if (this.npc.getY() < (double)this.level.getMinBuildHeight()) { + return null; + } else if (!this.canUpdatePath()) { + return null; + } else if (this.path != null && !this.path.isDone() && targets.contains(this.targetPos)) { + return this.path; + } else { + this.level.getProfiler().push("pathfind"); + BlockPos blockpos = offsetUpward ? this.npc.blockPosition().above() : this.npc.blockPosition(); + int i = (int)(followRange + (float)regionOffset); + PathNavigationRegion pathnavigationregion = new PathNavigationRegion(this.level, blockpos.offset(-i, -i, -i), blockpos.offset(i, i, i)); + Path path = this.pathFinder.findPath(pathnavigationregion, this.npc, targets, followRange, accuracy, this.maxVisitedNodesMultiplier); + this.level.getProfiler().pop(); + if (path != null && path.getTarget() != null) { + this.targetPos = path.getTarget(); + this.reachRange = accuracy; + this.resetStuckTimeout(); + } + + return path; + } + } + + /** + * Try to find and set a path to XYZ. Returns {@code true} if successful. + */ + public boolean moveTo(double x, double y, double z, double speed) { + return this.moveTo(this.createPath(x, y, z, 1), speed); + } + + /** + * Try to find and set a path to EntityLiving. Returns {@code true} if successful. + */ + public boolean moveTo(Entity entity, double speed) { + Path path = this.createPath(entity, 1); + return path != null && this.moveTo(path, speed); + } + + /** + * Sets a new path. If it's different from the old path. Checks to adjust path for sun avoiding, and stores start coords. + */ + public boolean moveTo(@Nullable Path pathentity, double speed) { + if (pathentity == null) { + this.path = null; + return false; + } else { + if (!pathentity.sameAs(this.path)) { + this.path = pathentity; + } + + if (this.isDone()) { + return false; + } else { + this.trimPath(); + if (this.path.getNodeCount() <= 0) { + return false; + } else { + this.speedModifier = speed; + Vec3 vec3 = this.getTempMobPos(); + this.lastStuckCheck = this.tick; + this.lastStuckCheckPos = vec3; + return true; + } + } + } + } + + @Nullable + public Path getPath() { + return this.path; + } + + public void tick() { + ++this.tick; + if (this.hasDelayedRecomputation) { + this.recomputePath(); + } + + if (!this.isDone()) { + if (this.canUpdatePath()) { + this.followThePath(); + } else if (this.path != null && !this.path.isDone()) { + Vec3 vec3 = this.getTempMobPos(); + Vec3 vec31 = this.path.getNextEntityPos(this.npc); + if (vec3.y > vec31.y && !this.npc.onGround() && Mth.floor(vec3.x) == Mth.floor(vec31.x) && Mth.floor(vec3.z) == Mth.floor(vec31.z)) { + this.path.advance(); + } + } + if (!this.isDone()) { + assert this.path != null; + Vec3 vec32 = this.path.getNextEntityPos(this.npc); + this.npc.getMoveControl().setWantedPosition(vec32.x, this.getGroundY(vec32), vec32.z, this.speedModifier); + } + } + } + + protected double getGroundY(Vec3 vec) { + BlockPos blockpos = BlockPos.containing(vec); + return this.level.getBlockState(blockpos.below()).isAir() ? vec.y : WalkNodeEvaluator.getFloorLevel(this.level, blockpos); + } + + protected void followThePath() { + Vec3 vec3 = this.getTempMobPos(); + this.maxDistanceToWaypoint = this.npc.getBbWidth() > 0.75F ? this.npc.getBbWidth() / 2.0F : 0.75F - this.npc.getBbWidth() / 2.0F; + boolean flag = isFlag(); + if (flag || this.canCutCorner(this.path != null ? this.path.getNextNode().type : null) && this.shouldTargetNextNodeInDirection(vec3)) { + assert this.path != null; + this.path.advance(); + } + + this.doStuckDetection(vec3); + } + + private boolean isFlag() { + assert this.path != null; + Vec3i vec3i = this.path.getNextNodePos(); + double d0 = Math.abs(this.npc.getX() - ((double)vec3i.getX() + (this.npc.getBbWidth() + 1) / 2D)); //Forge: Fix MC-94054 + double d1 = Math.abs(this.npc.getY() - (double)vec3i.getY()); + double d2 = Math.abs(this.npc.getZ() - ((double)vec3i.getZ() + (this.npc.getBbWidth() + 1) / 2D)); //Forge: Fix MC-94054 + boolean flag = d0 <= (double)this.maxDistanceToWaypoint && d2 <= (double)this.maxDistanceToWaypoint && d1 < 1.0D; //Forge: Fix MC-94054 + return flag; + } + + private boolean shouldTargetNextNodeInDirection(Vec3 vec) { + assert this.path != null; + if (this.path.getNextNodeIndex() + 1 >= this.path.getNodeCount()) { + return false; + } else { + Vec3 vec3 = Vec3.atBottomCenterOf(this.path.getNextNodePos()); + if (!vec.closerThan(vec3, 2.0D)) { + return false; + } else if (this.canMoveDirectly(vec, this.path.getNextEntityPos(this.npc))) { + return true; + } else { + Vec3 vec31 = Vec3.atBottomCenterOf(this.path.getNodePos(this.path.getNextNodeIndex() + 1)); + Vec3 vec32 = vec3.subtract(vec); + Vec3 vec33 = vec31.subtract(vec); + double d0 = vec32.lengthSqr(); + double d1 = vec33.lengthSqr(); + boolean flag = d1 < d0; + boolean flag1 = d0 < 0.5D; + if (!flag && !flag1) { + return false; + } else { + Vec3 vec34 = vec32.normalize(); + Vec3 vec35 = vec33.normalize(); + return vec35.dot(vec34) < 0.0D; + } + } + } + } + + /** + * Checks if entity haven't been moved when last checked and if so, stops the current navigation. + */ + protected void doStuckDetection(Vec3 positionVec3) { + if (this.tick - this.lastStuckCheck > 100) { + float f = this.npc.getSpeed() >= 1.0F ? this.npc.getSpeed() : this.npc.getSpeed() * this.npc.getSpeed(); + float f1 = f * 100.0F * 0.25F; + if (positionVec3.distanceToSqr(this.lastStuckCheckPos) < (double)(f1 * f1)) { + this.isStuck = true; + this.stop(); + } else { + this.isStuck = false; + } + + this.lastStuckCheck = this.tick; + this.lastStuckCheckPos = positionVec3; + } + + if (this.path != null && !this.path.isDone()) { + Vec3i vec3i = this.path.getNextNodePos(); + long i = this.level.getGameTime(); + if (vec3i.equals(this.timeoutCachedNode)) { + this.timeoutTimer += i - this.lastTimeoutCheck; + } else { + this.timeoutCachedNode = vec3i; + double d0 = positionVec3.distanceTo(Vec3.atBottomCenterOf(this.timeoutCachedNode)); + this.timeoutLimit = this.npc.getSpeed() > 0.0F ? d0 / (double)this.npc.getSpeed() * 20.0D : 0.0D; + } + + if (this.timeoutLimit > 0.0D && (double)this.timeoutTimer > this.timeoutLimit * 3.0D) { + this.timeoutPath(); + } + + this.lastTimeoutCheck = i; + } + + } + + private void timeoutPath() { + this.resetStuckTimeout(); + this.stop(); + } + + private void resetStuckTimeout() { + this.timeoutCachedNode = Vec3i.ZERO; + this.timeoutTimer = 0L; + this.timeoutLimit = 0.0D; + this.isStuck = false; + } + + public boolean isDone() { + return this.path == null || this.path.isDone(); + } + + public boolean isInProgress() { + return !this.isDone(); + } + + public void stop() { + this.path = null; + } + + protected abstract Vec3 getTempMobPos(); + + protected abstract boolean canUpdatePath(); + + protected boolean isInLiquid() { + return this.npc.isInWaterOrBubble() || this.npc.isInLava(); + } + + protected void trimPath() { + if (this.path != null) { + for(int i = 0; i < this.path.getNodeCount(); ++i) { + Node node = this.path.getNode(i); + Node node1 = i + 1 < this.path.getNodeCount() ? this.path.getNode(i + 1) : null; + BlockState blockstate = this.level.getBlockState(new BlockPos(node.x, node.y, node.z)); + if (blockstate.is(BlockTags.CAULDRONS)) { + this.path.replaceNode(i, node.cloneAndMove(node.x, node.y + 1, node.z)); + if (node1 != null && node.y >= node1.y) { + this.path.replaceNode(i + 1, node.cloneAndMove(node1.x, node.y + 1, node1.z)); + } + } + } + + } + } + + /** + * Checks if the specified entity can safely walk to the specified location. + */ + protected boolean canMoveDirectly(Vec3 posVec31, Vec3 posVec32) { + return false; + } + + public boolean canCutCorner(BlockPathTypes pathType) { + return pathType != BlockPathTypes.DANGER_FIRE && pathType != BlockPathTypes.DANGER_OTHER && pathType != BlockPathTypes.WALKABLE_DOOR; + } + + protected static boolean isClearForMovementBetween(NPCServerPlayer npc, Vec3 pos1, Vec3 pos2, boolean allowSwimming) { + Vec3 vec3 = new Vec3(pos2.x, pos2.y + (double)npc.getBbHeight() * 0.5D, pos2.z); + return npc.level().clip(new ClipContext(pos1, vec3, ClipContext.Block.COLLIDER, allowSwimming ? ClipContext.Fluid.ANY : ClipContext.Fluid.NONE, npc)).getType() == HitResult.Type.MISS; + } + + public boolean isStableDestination(BlockPos pos) { + BlockPos blockpos = pos.below(); + return this.level.getBlockState(blockpos).isSolidRender(this.level, blockpos); + } + + public NPCNodeEvaluator getNodeEvaluator() { + return this.nodeEvaluator; + } + + public void setCanFloat(boolean canSwim) { + this.nodeEvaluator.setCanFloat(canSwim); + } + + public boolean canFloat() { + return this.nodeEvaluator.canFloat(); + } + + public boolean shouldRecomputePath(BlockPos pos) { + if (this.hasDelayedRecomputation) { + return false; + } else if (this.path != null && !this.path.isDone() && this.path.getNodeCount() != 0) { + Node node = this.path.getEndNode(); + assert node != null; + Vec3 vec3 = new Vec3(((double)node.x + this.npc.getX()) / 2.0D, ((double)node.y + this.npc.getY()) / 2.0D, ((double)node.z + this.npc.getZ()) / 2.0D); + return pos.closerToCenterThan(vec3, this.path.getNodeCount() - this.path.getNextNodeIndex()); + } else { + return false; + } + } + + public float getMaxDistanceToWaypoint() { + return this.maxDistanceToWaypoint; + } + + public boolean isStuck() { + return this.isStuck; + } +} diff --git a/src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/ai/navigation/NPCWaterBoundPathNavigation.java b/src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/ai/navigation/NPCWaterBoundPathNavigation.java new file mode 100644 index 00000000..4b0b529c --- /dev/null +++ b/src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/ai/navigation/NPCWaterBoundPathNavigation.java @@ -0,0 +1,68 @@ +/* + * Copyright 2025-2026 R3944Realms + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package top.r3944realms.eroticdungeongame.content.entity.npc.ai.navigation; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.entity.EntityType; +import net.minecraft.world.entity.Mob; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.pathfinder.PathFinder; +import net.minecraft.world.level.pathfinder.SwimNodeEvaluator; +import net.minecraft.world.phys.Vec3; +import top.r3944realms.eroticdungeongame.content.entity.npc.NPCServerPlayer; +import top.r3944realms.eroticdungeongame.content.entity.npc.ai.navigation.pathfinder.NPCPathFinder; +import top.r3944realms.eroticdungeongame.content.entity.npc.ai.navigation.pathfinder.NPCSwimNodeEvaluator; + +public class NPCWaterBoundPathNavigation extends NPCPathNavigation { + private boolean allowBreaching; + + public NPCWaterBoundPathNavigation(NPCServerPlayer npc, Level level) { + super(npc, level); + } + + protected NPCPathFinder createPathFinder(int maxVisitedNodes) { + this.allowBreaching = false; //todo 考虑下这里修改适配NPC + this.nodeEvaluator = new NPCSwimNodeEvaluator(false); + return new NPCPathFinder(this.nodeEvaluator, maxVisitedNodes); + } + + protected boolean canUpdatePath() { + return this.allowBreaching || this.isInLiquid(); + } + + protected Vec3 getTempMobPos() { + return new Vec3(this.npc.getX(), this.npc.getY(0.5D), this.npc.getZ()); + } + + protected double getGroundY(Vec3 vec) { + return vec.y; + } + + /** + * Checks if the specified entity can safely walk to the specified location. + */ + protected boolean canMoveDirectly(Vec3 posVec31, Vec3 posVec32) { + return isClearForMovementBetween(this.npc, posVec31, posVec32, false); + } + + public boolean isStableDestination(BlockPos pos) { + return !this.level.getBlockState(pos).isSolidRender(this.level, pos); + } + + public void setCanFloat(boolean canSwim) { + } +} diff --git a/src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/ai/navigation/pathfinder/NPCFlyNodeEvaluator.java b/src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/ai/navigation/pathfinder/NPCFlyNodeEvaluator.java new file mode 100644 index 00000000..486f5a81 --- /dev/null +++ b/src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/ai/navigation/pathfinder/NPCFlyNodeEvaluator.java @@ -0,0 +1,325 @@ +/* + * Copyright 2025-2026 R3944Realms + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package top.r3944realms.eroticdungeongame.content.entity.npc.ai.navigation.pathfinder; + +import it.unimi.dsi.fastutil.longs.Long2ObjectMap; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import net.minecraft.core.BlockPos; +import net.minecraft.util.Mth; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.PathNavigationRegion; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.pathfinder.BlockPathTypes; +import net.minecraft.world.level.pathfinder.Node; +import net.minecraft.world.level.pathfinder.Target; +import net.minecraft.world.phys.AABB; +import org.jetbrains.annotations.Nullable; +import top.r3944realms.eroticdungeongame.content.entity.npc.NPCServerPlayer; + +import java.util.EnumSet; +import java.util.List; + +public class NPCFlyNodeEvaluator extends NPCWalkNodeEvaluator { + private final Long2ObjectMap pathTypeByPosCache = new Long2ObjectOpenHashMap<>(); + private static final float SMALL_MOB_INFLATED_START_NODE_BOUNDING_BOX = 1.5F; + private static final int MAX_START_NODE_CANDIDATES = 10; + + public void prepare(PathNavigationRegion level, NPCServerPlayer npc) { + super.prepare(level, npc); + this.pathTypeByPosCache.clear(); + npc.onPathfindingStart(); + } + + public void done() { + this.npc.onPathfindingDone(); + this.pathTypeByPosCache.clear(); + super.done(); + } + + public Node getStart() { + int i; + if (this.canFloat() && this.npc.isInWater()) { + i = this.npc.getBlockY(); + BlockPos.MutableBlockPos blockpos$mutableblockpos = new BlockPos.MutableBlockPos(this.npc.getX(), (double)i, this.npc.getZ()); + + for(BlockState blockstate = this.level.getBlockState(blockpos$mutableblockpos); blockstate.is(Blocks.WATER); blockstate = this.level.getBlockState(blockpos$mutableblockpos)) { + ++i; + blockpos$mutableblockpos.set(this.npc.getX(), (double)i, this.npc.getZ()); + } + } else { + i = Mth.floor(this.npc.getY() + 0.5D); + } + + BlockPos blockpos1 = BlockPos.containing(this.npc.getX(), (double)i, this.npc.getZ()); + if (!this.canStartAt(blockpos1)) { + for(BlockPos blockpos : this.iteratePathfindingStartNodeCandidatePositions(this.npc)) { + if (this.canStartAt(blockpos)) { + return super.getStartNode(blockpos); + } + } + } + + return super.getStartNode(blockpos1); + } + + protected boolean canStartAt(BlockPos pos) { + BlockPathTypes blockpathtypes = this.getBlockPathType(this.npc, pos); + return this.npc.getPathfindingMalus(blockpathtypes) >= 0.0F; + } + + public Target getGoal(double x, double y, double z) { + return this.getTargetFromNode(this.getNode(Mth.floor(x), Mth.floor(y), Mth.floor(z))); + } + + public int getNeighbors(Node[] outputArray, Node p_node) { + int i = 0; + Node node = this.findAcceptedNode(p_node.x, p_node.y, p_node.z + 1); + if (this.isOpen(node)) { + outputArray[i++] = node; + } + + Node node1 = this.findAcceptedNode(p_node.x - 1, p_node.y, p_node.z); + if (this.isOpen(node1)) { + outputArray[i++] = node1; + } + + Node node2 = this.findAcceptedNode(p_node.x + 1, p_node.y, p_node.z); + if (this.isOpen(node2)) { + outputArray[i++] = node2; + } + + Node node3 = this.findAcceptedNode(p_node.x, p_node.y, p_node.z - 1); + if (this.isOpen(node3)) { + outputArray[i++] = node3; + } + + Node node4 = this.findAcceptedNode(p_node.x, p_node.y + 1, p_node.z); + if (this.isOpen(node4)) { + outputArray[i++] = node4; + } + + Node node5 = this.findAcceptedNode(p_node.x, p_node.y - 1, p_node.z); + if (this.isOpen(node5)) { + outputArray[i++] = node5; + } + + Node node6 = this.findAcceptedNode(p_node.x, p_node.y + 1, p_node.z + 1); + if (this.isOpen(node6) && this.hasMalus(node) && this.hasMalus(node4)) { + outputArray[i++] = node6; + } + + Node node7 = this.findAcceptedNode(p_node.x - 1, p_node.y + 1, p_node.z); + if (this.isOpen(node7) && this.hasMalus(node1) && this.hasMalus(node4)) { + outputArray[i++] = node7; + } + + Node node8 = this.findAcceptedNode(p_node.x + 1, p_node.y + 1, p_node.z); + if (this.isOpen(node8) && this.hasMalus(node2) && this.hasMalus(node4)) { + outputArray[i++] = node8; + } + + Node node9 = this.findAcceptedNode(p_node.x, p_node.y + 1, p_node.z - 1); + if (this.isOpen(node9) && this.hasMalus(node3) && this.hasMalus(node4)) { + outputArray[i++] = node9; + } + + Node node10 = this.findAcceptedNode(p_node.x, p_node.y - 1, p_node.z + 1); + if (this.isOpen(node10) && this.hasMalus(node) && this.hasMalus(node5)) { + outputArray[i++] = node10; + } + + Node node11 = this.findAcceptedNode(p_node.x - 1, p_node.y - 1, p_node.z); + if (this.isOpen(node11) && this.hasMalus(node1) && this.hasMalus(node5)) { + outputArray[i++] = node11; + } + + Node node12 = this.findAcceptedNode(p_node.x + 1, p_node.y - 1, p_node.z); + if (this.isOpen(node12) && this.hasMalus(node2) && this.hasMalus(node5)) { + outputArray[i++] = node12; + } + + Node node13 = this.findAcceptedNode(p_node.x, p_node.y - 1, p_node.z - 1); + if (this.isOpen(node13) && this.hasMalus(node3) && this.hasMalus(node5)) { + outputArray[i++] = node13; + } + + Node node14 = this.findAcceptedNode(p_node.x + 1, p_node.y, p_node.z - 1); + if (this.isOpen(node14) && this.hasMalus(node3) && this.hasMalus(node2)) { + outputArray[i++] = node14; + } + + Node node15 = this.findAcceptedNode(p_node.x + 1, p_node.y, p_node.z + 1); + if (this.isOpen(node15) && this.hasMalus(node) && this.hasMalus(node2)) { + outputArray[i++] = node15; + } + + Node node16 = this.findAcceptedNode(p_node.x - 1, p_node.y, p_node.z - 1); + if (this.isOpen(node16) && this.hasMalus(node3) && this.hasMalus(node1)) { + outputArray[i++] = node16; + } + + Node node17 = this.findAcceptedNode(p_node.x - 1, p_node.y, p_node.z + 1); + if (this.isOpen(node17) && this.hasMalus(node) && this.hasMalus(node1)) { + outputArray[i++] = node17; + } + + Node node18 = this.findAcceptedNode(p_node.x + 1, p_node.y + 1, p_node.z - 1); + if (this.isOpen(node18) && this.hasMalus(node14) && this.hasMalus(node3) && this.hasMalus(node2) && this.hasMalus(node4) && this.hasMalus(node9) && this.hasMalus(node8)) { + outputArray[i++] = node18; + } + + Node node19 = this.findAcceptedNode(p_node.x + 1, p_node.y + 1, p_node.z + 1); + if (this.isOpen(node19) && this.hasMalus(node15) && this.hasMalus(node) && this.hasMalus(node2) && this.hasMalus(node4) && this.hasMalus(node6) && this.hasMalus(node8)) { + outputArray[i++] = node19; + } + + Node node20 = this.findAcceptedNode(p_node.x - 1, p_node.y + 1, p_node.z - 1); + if (this.isOpen(node20) && this.hasMalus(node16) && this.hasMalus(node3) && this.hasMalus(node1) && this.hasMalus(node4) && this.hasMalus(node9) && this.hasMalus(node7)) { + outputArray[i++] = node20; + } + + Node node21 = this.findAcceptedNode(p_node.x - 1, p_node.y + 1, p_node.z + 1); + if (this.isOpen(node21) && this.hasMalus(node17) && this.hasMalus(node) && this.hasMalus(node1) && this.hasMalus(node4) && this.hasMalus(node6) && this.hasMalus(node7)) { + outputArray[i++] = node21; + } + + Node node22 = this.findAcceptedNode(p_node.x + 1, p_node.y - 1, p_node.z - 1); + if (this.isOpen(node22) && this.hasMalus(node14) && this.hasMalus(node3) && this.hasMalus(node2) && this.hasMalus(node5) && this.hasMalus(node13) && this.hasMalus(node12)) { + outputArray[i++] = node22; + } + + Node node23 = this.findAcceptedNode(p_node.x + 1, p_node.y - 1, p_node.z + 1); + if (this.isOpen(node23) && this.hasMalus(node15) && this.hasMalus(node) && this.hasMalus(node2) && this.hasMalus(node5) && this.hasMalus(node10) && this.hasMalus(node12)) { + outputArray[i++] = node23; + } + + Node node24 = this.findAcceptedNode(p_node.x - 1, p_node.y - 1, p_node.z - 1); + if (this.isOpen(node24) && this.hasMalus(node16) && this.hasMalus(node3) && this.hasMalus(node1) && this.hasMalus(node5) && this.hasMalus(node13) && this.hasMalus(node11)) { + outputArray[i++] = node24; + } + + Node node25 = this.findAcceptedNode(p_node.x - 1, p_node.y - 1, p_node.z + 1); + if (this.isOpen(node25) && this.hasMalus(node17) && this.hasMalus(node) && this.hasMalus(node1) && this.hasMalus(node5) && this.hasMalus(node10) && this.hasMalus(node11)) { + outputArray[i++] = node25; + } + + return i; + } + + private boolean hasMalus(@Nullable Node node) { + return node != null && node.costMalus >= 0.0F; + } + + private boolean isOpen(@Nullable Node node) { + return node != null && !node.closed; + } + + @Nullable + protected Node findAcceptedNode(int x, int y, int z) { + Node node = null; + BlockPathTypes blockpathtypes = this.getCachedBlockPathType(x, y, z); + float f = this.npc.getPathfindingMalus(blockpathtypes); + if (f >= 0.0F) { + node = this.getNode(x, y, z); + node.type = blockpathtypes; + node.costMalus = Math.max(node.costMalus, f); + if (blockpathtypes == BlockPathTypes.WALKABLE) { + ++node.costMalus; + } + } + + return node; + } + + private BlockPathTypes getCachedBlockPathType(int x, int y, int z) { + return this.pathTypeByPosCache.computeIfAbsent(BlockPos.asLong(x, y, z), (p_265010_) -> { + return this.getBlockPathType(this.level, x, y, z, this.npc); + }); + } + + public BlockPathTypes getBlockPathType(BlockGetter level, int x, int y, int z, NPCServerPlayer npc) { + EnumSet enumset = EnumSet.noneOf(BlockPathTypes.class); + BlockPathTypes blockpathtypes = BlockPathTypes.BLOCKED; + BlockPos blockpos = npc.blockPosition(); + blockpathtypes = super.getBlockPathTypes(level, x, y, z, enumset, blockpathtypes, blockpos); + if (enumset.contains(BlockPathTypes.FENCE)) { + return BlockPathTypes.FENCE; + } else { + BlockPathTypes blockpathtypes1 = BlockPathTypes.BLOCKED; + + for(BlockPathTypes blockpathtypes2 : enumset) { + if (npc.getPathfindingMalus(blockpathtypes2) < 0.0F) { + return blockpathtypes2; + } + + if (npc.getPathfindingMalus(blockpathtypes2) >= npc.getPathfindingMalus(blockpathtypes1)) { + blockpathtypes1 = blockpathtypes2; + } + } + + return blockpathtypes == BlockPathTypes.OPEN && npc.getPathfindingMalus(blockpathtypes1) == 0.0F ? BlockPathTypes.OPEN : blockpathtypes1; + } + } + + /** + * Returns the node type at the specified postion taking the block below into account + */ + public BlockPathTypes getBlockPathType(BlockGetter level, int x, int y, int z) { + BlockPos.MutableBlockPos blockpos$mutableblockpos = new BlockPos.MutableBlockPos(); + BlockPathTypes blockpathtypes = getBlockPathTypeRaw(level, blockpos$mutableblockpos.set(x, y, z)); + if (blockpathtypes == BlockPathTypes.OPEN && y >= level.getMinBuildHeight() + 1) { + BlockPathTypes blockpathtypes1 = getBlockPathTypeRaw(level, blockpos$mutableblockpos.set(x, y - 1, z)); + if (blockpathtypes1 != BlockPathTypes.DAMAGE_FIRE && blockpathtypes1 != BlockPathTypes.LAVA) { + if (blockpathtypes1 == BlockPathTypes.DAMAGE_OTHER) { + blockpathtypes = BlockPathTypes.DAMAGE_OTHER; + } else if (blockpathtypes1 == BlockPathTypes.COCOA) { + blockpathtypes = BlockPathTypes.COCOA; + } else if (blockpathtypes1 == BlockPathTypes.FENCE) { + if (!blockpos$mutableblockpos.equals(this.npc.blockPosition())) { + blockpathtypes = BlockPathTypes.FENCE; + } + } else { + blockpathtypes = blockpathtypes1 != BlockPathTypes.WALKABLE && blockpathtypes1 != BlockPathTypes.OPEN && blockpathtypes1 != BlockPathTypes.WATER ? BlockPathTypes.WALKABLE : BlockPathTypes.OPEN; + } + } else { + blockpathtypes = BlockPathTypes.DAMAGE_FIRE; + } + } + + if (blockpathtypes == BlockPathTypes.WALKABLE || blockpathtypes == BlockPathTypes.OPEN) { + blockpathtypes = checkNeighbourBlocks(level, blockpos$mutableblockpos.set(x, y, z), blockpathtypes); + } + + return blockpathtypes; + } + + private Iterable iteratePathfindingStartNodeCandidatePositions(NPCServerPlayer npc) { + float f = 1.0F; + AABB aabb = npc.getBoundingBox(); + boolean flag = aabb.getSize() < 1.0D; + if (!flag) { + return List.of(BlockPos.containing(aabb.minX, (double)npc.getBlockY(), aabb.minZ), BlockPos.containing(aabb.minX, (double)npc.getBlockY(), aabb.maxZ), BlockPos.containing(aabb.maxX, (double)npc.getBlockY(), aabb.minZ), BlockPos.containing(aabb.maxX, (double)npc.getBlockY(), aabb.maxZ)); + } else { + double d0 = Math.max(0.0D, (1.5D - aabb.getZsize()) / 2.0D); + double d1 = Math.max(0.0D, (1.5D - aabb.getXsize()) / 2.0D); + double d2 = Math.max(0.0D, (1.5D - aabb.getYsize()) / 2.0D); + AABB aabb1 = aabb.inflate(d1, d2, d0); + return BlockPos.randomBetweenClosed(npc.getRandom(), 10, Mth.floor(aabb1.minX), Mth.floor(aabb1.minY), Mth.floor(aabb1.minZ), Mth.floor(aabb1.maxX), Mth.floor(aabb1.maxY), Mth.floor(aabb1.maxZ)); + } + } +} diff --git a/src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/ai/navigation/pathfinder/NPCNodeEvaluator.java b/src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/ai/navigation/pathfinder/NPCNodeEvaluator.java new file mode 100644 index 00000000..707a4338 --- /dev/null +++ b/src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/ai/navigation/pathfinder/NPCNodeEvaluator.java @@ -0,0 +1,117 @@ +/* + * Copyright 2025-2026 R3944Realms + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package top.r3944realms.eroticdungeongame.content.entity.npc.ai.navigation.pathfinder; + +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap; +import net.minecraft.core.BlockPos; +import net.minecraft.util.Mth; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.PathNavigationRegion; +import net.minecraft.world.level.pathfinder.BlockPathTypes; +import net.minecraft.world.level.pathfinder.Node; +import net.minecraft.world.level.pathfinder.Target; +import top.r3944realms.eroticdungeongame.content.entity.npc.NPCServerPlayer; + +public abstract class NPCNodeEvaluator { + protected PathNavigationRegion level; + protected NPCServerPlayer npc; + protected final Int2ObjectMap nodes = new Int2ObjectOpenHashMap<>(); + protected int entityWidth; + protected int entityHeight; + protected int entityDepth; + protected boolean canPassDoors; + protected boolean canOpenDoors; + protected boolean canFloat; + protected boolean canWalkOverFences; + + public void prepare(PathNavigationRegion level, NPCServerPlayer npc) { + this.level = level; + this.npc = npc; + this.nodes.clear(); + this.entityWidth = Mth.floor(npc.getBbWidth() + 1.0F); + this.entityHeight = Mth.floor(npc.getBbHeight() + 1.0F); + this.entityDepth = Mth.floor(npc.getBbWidth() + 1.0F); + } + + public void done() { + this.level = null; + this.npc = null; + } + + protected Node getNode(BlockPos pos) { + return this.getNode(pos.getX(), pos.getY(), pos.getZ()); + } + + /** + * Returns a mapped point or creates and adds one + */ + protected Node getNode(int x, int y, int z) { + return this.nodes.computeIfAbsent(Node.createHash(x, y, z), (p_77332_) -> { + return new Node(x, y, z); + }); + } + + public abstract Node getStart(); + + public abstract Target getGoal(double x, double y, double z); + + protected Target getTargetFromNode(Node node) { + return new Target(node); + } + + public abstract int getNeighbors(Node[] outputArray, Node node); + + public abstract BlockPathTypes getBlockPathType(BlockGetter level, int x, int y, int z, NPCServerPlayer npc); + + /** + * Returns the node type at the specified postion taking the block below into account + */ + public abstract BlockPathTypes getBlockPathType(BlockGetter level, int x, int y, int z); + + public void setCanPassDoors(boolean canEnterDoors) { + this.canPassDoors = canEnterDoors; + } + + public void setCanOpenDoors(boolean canOpenDoors) { + this.canOpenDoors = canOpenDoors; + } + + public void setCanFloat(boolean canFloat) { + this.canFloat = canFloat; + } + + public void setCanWalkOverFences(boolean canWalkOverFences) { + this.canWalkOverFences = canWalkOverFences; + } + + public boolean canPassDoors() { + return this.canPassDoors; + } + + public boolean canOpenDoors() { + return this.canOpenDoors; + } + + public boolean canFloat() { + return this.canFloat; + } + + public boolean canWalkOverFences() { + return this.canWalkOverFences; + } +} \ No newline at end of file diff --git a/src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/ai/navigation/pathfinder/NPCPathFinder.java b/src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/ai/navigation/pathfinder/NPCPathFinder.java new file mode 100644 index 00000000..ac858490 --- /dev/null +++ b/src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/ai/navigation/pathfinder/NPCPathFinder.java @@ -0,0 +1,159 @@ +/* + * Copyright 2025-2026 R3944Realms + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package top.r3944realms.eroticdungeongame.content.entity.npc.ai.navigation.pathfinder; + +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; +import net.minecraft.core.BlockPos; +import net.minecraft.util.profiling.ProfilerFiller; +import net.minecraft.util.profiling.metrics.MetricCategory; +import net.minecraft.world.level.PathNavigationRegion; +import net.minecraft.world.level.pathfinder.*; +import org.jetbrains.annotations.Nullable; +import top.r3944realms.eroticdungeongame.content.entity.npc.NPCServerPlayer; + +import java.util.*; +import java.util.function.Function; +import java.util.stream.Collectors; + +public class NPCPathFinder{ + private static final float FUDGING = 1.5F; + private final Node[] neighbors = new Node[32]; + private final int maxVisitedNodes; + private final NPCNodeEvaluator nodeEvaluator; + private static final boolean DEBUG = false; + private final BinaryHeap openSet = new BinaryHeap(); + + public NPCPathFinder(NPCNodeEvaluator nodeEvaluator, int maxVisitedNodes) { + this.nodeEvaluator = nodeEvaluator; + this.maxVisitedNodes = maxVisitedNodes; + } + + /** + * Finds a path to one of the specified positions and post-processes it or returns null if no path could be found within given accuracy + */ + @Nullable + public Path findPath(PathNavigationRegion region, NPCServerPlayer npc, Set targetPositions, float maxRange, int accuracy, float searchDepthMultiplier) { + this.openSet.clear(); + this.nodeEvaluator.prepare(region, npc); + Node node = this.nodeEvaluator.getStart(); + if (node == null) { + return null; + } else { + Map map = targetPositions.stream().collect(Collectors.toMap((pos) -> this.nodeEvaluator.getGoal(pos.getX(), pos.getY(), pos.getZ()), Function.identity())); + Path path = this.findPath(region.getProfiler(), node, map, maxRange, accuracy, searchDepthMultiplier); + this.nodeEvaluator.done(); + return path; + } + } + + @Nullable + private Path findPath(ProfilerFiller profiler, Node p_node, Map targetPos, float maxRange, int accuracy, float searchDepthMultiplier) { + profiler.push("find_path"); + profiler.markForCharting(MetricCategory.PATH_FINDING); + Set set = targetPos.keySet(); + p_node.g = 0.0F; + p_node.h = this.getBestH(p_node, set); + p_node.f = p_node.h; + this.openSet.clear(); + this.openSet.insert(p_node); + Set set1 = ImmutableSet.of(); + int i = 0; + Set set2 = Sets.newHashSetWithExpectedSize(set.size()); + int j = (int)((float)this.maxVisitedNodes * searchDepthMultiplier); + + while(!this.openSet.isEmpty()) { + ++i; + if (i >= j) { + break; + } + + Node node = this.openSet.pop(); + node.closed = true; + + for(Target target : set) { + if (node.distanceManhattan(target) <= (float)accuracy) { + target.setReached(); + set2.add(target); + } + } + + if (!set2.isEmpty()) { + break; + } + + if (!(node.distanceTo(p_node) >= maxRange)) { + int k = this.nodeEvaluator.getNeighbors(this.neighbors, node); + + for(int l = 0; l < k; ++l) { + Node node1 = this.neighbors[l]; + float f = this.distance(node, node1); + node1.walkedDistance = node.walkedDistance + f; + float f1 = node.g + f + node1.costMalus; + if (node1.walkedDistance < maxRange && (!node1.inOpenSet() || f1 < node1.g)) { + node1.cameFrom = node; + node1.g = f1; + node1.h = this.getBestH(node1, set) * 1.5F; + if (node1.inOpenSet()) { + this.openSet.changeCost(node1, node1.g + node1.h); + } else { + node1.f = node1.g + node1.h; + this.openSet.insert(node1); + } + } + } + } + } + + Optional optional = !set2.isEmpty() ? set2.stream().map((p_77454_) -> this.reconstructPath(p_77454_.getBestNode(), targetPos.get(p_77454_), true)).min(Comparator.comparingInt(Path::getNodeCount)) : set.stream().map((p_77451_) -> this.reconstructPath(p_77451_.getBestNode(), targetPos.get(p_77451_), false)).min(Comparator.comparingDouble(Path::getDistToTarget).thenComparingInt(Path::getNodeCount)); + profiler.pop(); + return optional.orElse(null); + } + + protected float distance(Node first, Node second) { + return first.distanceTo(second); + } + + private float getBestH(Node node, Set targets) { + float f = Float.MAX_VALUE; + + for(Target target : targets) { + float f1 = node.distanceTo(target); + target.updateBest(f1, node); + f = Math.min(f1, f); + } + + return f; + } + + /** + * Converts a recursive path point structure into a path + */ + private Path reconstructPath(Node point, BlockPos targetPos, boolean reachesTarget) { + List list = Lists.newArrayList(); + Node node = point; + list.add(0, point); + + while(node.cameFrom != null) { + node = node.cameFrom; + list.add(0, node); + } + + return new Path(list, targetPos, reachesTarget); + } +} diff --git a/src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/ai/navigation/pathfinder/NPCSwimNodeEvaluator.java b/src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/ai/navigation/pathfinder/NPCSwimNodeEvaluator.java new file mode 100644 index 00000000..ccc68787 --- /dev/null +++ b/src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/ai/navigation/pathfinder/NPCSwimNodeEvaluator.java @@ -0,0 +1,147 @@ +/* + * Copyright 2025-2026 R3944Realms + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package top.r3944realms.eroticdungeongame.content.entity.npc.ai.navigation.pathfinder; + +import com.google.common.collect.Maps; +import it.unimi.dsi.fastutil.longs.Long2ObjectMap; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.tags.FluidTags; +import net.minecraft.util.Mth; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.PathNavigationRegion; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.material.FluidState; +import net.minecraft.world.level.pathfinder.*; +import org.jetbrains.annotations.Nullable; +import top.r3944realms.eroticdungeongame.content.entity.npc.NPCServerPlayer; + +import java.util.Map; + +public class NPCSwimNodeEvaluator extends NPCNodeEvaluator { + private final boolean allowBreaching; + private final Long2ObjectMap pathTypesByPosCache = new Long2ObjectOpenHashMap<>(); + + public NPCSwimNodeEvaluator(boolean allowBreaching) { + this.allowBreaching = allowBreaching; + } + + public void prepare(PathNavigationRegion level, NPCServerPlayer npc) { + super.prepare(level, npc); + this.pathTypesByPosCache.clear(); + } + + public void done() { + super.done(); + this.pathTypesByPosCache.clear(); + } + + public Node getStart() { + return this.getNode(Mth.floor(this.npc.getBoundingBox().minX), Mth.floor(this.npc.getBoundingBox().minY + 0.5D), Mth.floor(this.npc.getBoundingBox().minZ)); + } + + public Target getGoal(double x, double y, double z) { + return this.getTargetFromNode(this.getNode(Mth.floor(x), Mth.floor(y), Mth.floor(z))); + } + + public int getNeighbors(Node[] outputArray, Node p_node) { + int i = 0; + Map map = Maps.newEnumMap(Direction.class); + + for(Direction direction : Direction.values()) { + Node node = this.findAcceptedNode(p_node.x + direction.getStepX(), p_node.y + direction.getStepY(), p_node.z + direction.getStepZ()); + map.put(direction, node); + if (this.isNodeValid(node)) { + outputArray[i++] = node; + } + } + + for(Direction direction1 : Direction.Plane.HORIZONTAL) { + Direction direction2 = direction1.getClockWise(); + Node node1 = this.findAcceptedNode(p_node.x + direction1.getStepX() + direction2.getStepX(), p_node.y, p_node.z + direction1.getStepZ() + direction2.getStepZ()); + if (this.isDiagonalNodeValid(node1, map.get(direction1), map.get(direction2))) { + outputArray[i++] = node1; + } + } + + return i; + } + + protected boolean isNodeValid(@Nullable Node node) { + return node != null && !node.closed; + } + + protected boolean isDiagonalNodeValid(@Nullable Node root, @Nullable Node horizontal, @Nullable Node clockwise) { + return this.isNodeValid(root) && horizontal != null && horizontal.costMalus >= 0.0F && clockwise != null && clockwise.costMalus >= 0.0F; + } + + @Nullable + protected Node findAcceptedNode(int x, int y, int z) { + Node node = null; + BlockPathTypes blockpathtypes = this.getCachedBlockType(x, y, z); + if (this.allowBreaching && blockpathtypes == BlockPathTypes.BREACH || blockpathtypes == BlockPathTypes.WATER) { + float f = this.npc.getPathfindingMalus(blockpathtypes); + if (f >= 0.0F) { + node = this.getNode(x, y, z); + node.type = blockpathtypes; + node.costMalus = Math.max(node.costMalus, f); + if (this.level.getFluidState(new BlockPos(x, y, z)).isEmpty()) { + node.costMalus += 8.0F; + } + } + } + + return node; + } + + protected BlockPathTypes getCachedBlockType(int x, int y, int z) { + return this.pathTypesByPosCache.computeIfAbsent(BlockPos.asLong(x, y, z), (p_192957_) -> { + return this.getBlockPathType(this.level, x, y, z); + }); + } + + /** + * Returns the node type at the specified postion taking the block below into account + */ + public BlockPathTypes getBlockPathType(BlockGetter level, int x, int y, int z) { + return this.getBlockPathType(level, x, y, z, this.npc); + } + + public BlockPathTypes getBlockPathType(BlockGetter level, int x, int y, int z, NPCServerPlayer mob) { + BlockPos.MutableBlockPos blockpos$mutableblockpos = new BlockPos.MutableBlockPos(); + + for(int i = x; i < x + this.entityWidth; ++i) { + for(int j = y; j < y + this.entityHeight; ++j) { + for(int k = z; k < z + this.entityDepth; ++k) { + FluidState fluidstate = level.getFluidState(blockpos$mutableblockpos.set(i, j, k)); + BlockState blockstate = level.getBlockState(blockpos$mutableblockpos.set(i, j, k)); + if (fluidstate.isEmpty() && blockstate.isPathfindable(level, blockpos$mutableblockpos.below(), PathComputationType.WATER) && blockstate.isAir()) { + return BlockPathTypes.BREACH; + } + + if (!fluidstate.is(FluidTags.WATER)) { + return BlockPathTypes.BLOCKED; + } + } + } + } + + BlockState blockstate1 = level.getBlockState(blockpos$mutableblockpos); + return blockstate1.isPathfindable(level, blockpos$mutableblockpos, PathComputationType.WATER) ? BlockPathTypes.WATER : BlockPathTypes.BLOCKED; + } +} diff --git a/src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/ai/navigation/pathfinder/NPCWalkNodeEvaluator.java b/src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/ai/navigation/pathfinder/NPCWalkNodeEvaluator.java new file mode 100644 index 00000000..c823629b --- /dev/null +++ b/src/main/java/top/r3944realms/eroticdungeongame/content/entity/npc/ai/navigation/pathfinder/NPCWalkNodeEvaluator.java @@ -0,0 +1,574 @@ +/* + * Copyright 2025-2026 R3944Realms + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package top.r3944realms.eroticdungeongame.content.entity.npc.ai.navigation.pathfinder; + +import it.unimi.dsi.fastutil.longs.Long2ObjectMap; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import it.unimi.dsi.fastutil.objects.Object2BooleanMap; +import it.unimi.dsi.fastutil.objects.Object2BooleanOpenHashMap; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.tags.BlockTags; +import net.minecraft.tags.FluidTags; +import net.minecraft.util.Mth; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.PathNavigationRegion; +import net.minecraft.world.level.block.*; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.material.FluidState; +import net.minecraft.world.level.material.Fluids; +import net.minecraft.world.level.pathfinder.BlockPathTypes; +import net.minecraft.world.level.pathfinder.Node; +import net.minecraft.world.level.pathfinder.PathComputationType; +import net.minecraft.world.level.pathfinder.Target; +import net.minecraft.world.phys.AABB; +import net.minecraft.world.phys.Vec3; +import net.minecraft.world.phys.shapes.VoxelShape; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; +import top.r3944realms.eroticdungeongame.content.entity.npc.NPCServerPlayer; + +import java.util.EnumSet; + +public class NPCWalkNodeEvaluator extends NPCNodeEvaluator { + public static final double SPACE_BETWEEN_WALL_POSTS = 0.5D; + private static final double DEFAULT_npc_JUMP_HEIGHT = 1.125D; + private final Long2ObjectMap pathTypesByPosCache = new Long2ObjectOpenHashMap<>(); + private final Object2BooleanMap collisionCache = new Object2BooleanOpenHashMap<>(); + + public void prepare(PathNavigationRegion level, NPCServerPlayer npc) { + super.prepare(level, npc); + npc.onPathfindingStart(); + } + + public void done() { + this.npc.onPathfindingDone(); + this.pathTypesByPosCache.clear(); + this.collisionCache.clear(); + super.done(); + } + + public Node getStart() { + BlockPos.MutableBlockPos blockpos$mutableblockpos = new BlockPos.MutableBlockPos(); + int i = this.npc.getBlockY(); + BlockState blockstate = this.level.getBlockState(blockpos$mutableblockpos.set(this.npc.getX(), i, this.npc.getZ())); + if (!this.npc.canStandOnFluid(blockstate.getFluidState())) { + if (this.canFloat() && this.npc.isInWater()) { + while(true) { + if (!blockstate.is(Blocks.WATER) && blockstate.getFluidState() != Fluids.WATER.getSource(false)) { + --i; + break; + } + + ++i; + blockstate = this.level.getBlockState(blockpos$mutableblockpos.set(this.npc.getX(), i, this.npc.getZ())); + } + } else if (this.npc.onGround()) { + i = Mth.floor(this.npc.getY() + 0.5D); + } else { + BlockPos blockpos; + for(blockpos = this.npc.blockPosition(); (this.level.getBlockState(blockpos).isAir() || this.level.getBlockState(blockpos).isPathfindable(this.level, blockpos, PathComputationType.LAND)) && blockpos.getY() > this.npc.level().getMinBuildHeight(); blockpos = blockpos.below()) { + } + + i = blockpos.above().getY(); + } + } else { + while(this.npc.canStandOnFluid(blockstate.getFluidState())) { + ++i; + blockstate = this.level.getBlockState(blockpos$mutableblockpos.set(this.npc.getX(), i, this.npc.getZ())); + } + + --i; + } + + BlockPos blockpos1 = this.npc.blockPosition(); + if (!this.canStartAt(blockpos$mutableblockpos.set(blockpos1.getX(), i, blockpos1.getZ()))) { + AABB aabb = this.npc.getBoundingBox(); + if (this.canStartAt(blockpos$mutableblockpos.set(aabb.minX, i, aabb.minZ)) || this.canStartAt(blockpos$mutableblockpos.set(aabb.minX, i, aabb.maxZ)) || this.canStartAt(blockpos$mutableblockpos.set(aabb.maxX, i, aabb.minZ)) || this.canStartAt(blockpos$mutableblockpos.set(aabb.maxX, i, aabb.maxZ))) { + return this.getStartNode(blockpos$mutableblockpos); + } + } + + return this.getStartNode(new BlockPos(blockpos1.getX(), i, blockpos1.getZ())); + } + + protected Node getStartNode(BlockPos pos) { + Node node = this.getNode(pos); + node.type = this.getBlockPathType(this.npc, node.asBlockPos()); + node.costMalus = this.npc.getPathfindingMalus(node.type); + return node; + } + + protected boolean canStartAt(BlockPos pos) { + BlockPathTypes blockpathtypes = this.getBlockPathType(this.npc, pos); + return blockpathtypes != BlockPathTypes.OPEN && this.npc.getPathfindingMalus(blockpathtypes) >= 0.0F; + } + + public Target getGoal(double x, double y, double z) { + return this.getTargetFromNode(this.getNode(Mth.floor(x), Mth.floor(y), Mth.floor(z))); + } + + public int getNeighbors(Node[] outputArray, Node p_node) { + int i = 0; + int j = 0; + BlockPathTypes blockpathtypes = this.getCachedBlockType(this.npc, p_node.x, p_node.y + 1, p_node.z); + BlockPathTypes blockpathtypes1 = this.getCachedBlockType(this.npc, p_node.x, p_node.y, p_node.z); + if (this.npc.getPathfindingMalus(blockpathtypes) >= 0.0F && blockpathtypes1 != BlockPathTypes.STICKY_HONEY) { + j = Mth.floor(Math.max(1.0F, this.npc.getStepHeight())); + } + + double d0 = this.getFloorLevel(new BlockPos(p_node.x, p_node.y, p_node.z)); + Node node = this.findAcceptedNode(p_node.x, p_node.y, p_node.z + 1, j, d0, Direction.SOUTH, blockpathtypes1); + if (this.isNeighborValid(node, p_node)) { + outputArray[i++] = node; + } + + Node node1 = this.findAcceptedNode(p_node.x - 1, p_node.y, p_node.z, j, d0, Direction.WEST, blockpathtypes1); + if (this.isNeighborValid(node1, p_node)) { + outputArray[i++] = node1; + } + + Node node2 = this.findAcceptedNode(p_node.x + 1, p_node.y, p_node.z, j, d0, Direction.EAST, blockpathtypes1); + if (this.isNeighborValid(node2, p_node)) { + outputArray[i++] = node2; + } + + Node node3 = this.findAcceptedNode(p_node.x, p_node.y, p_node.z - 1, j, d0, Direction.NORTH, blockpathtypes1); + if (this.isNeighborValid(node3, p_node)) { + outputArray[i++] = node3; + } + + Node node4 = this.findAcceptedNode(p_node.x - 1, p_node.y, p_node.z - 1, j, d0, Direction.NORTH, blockpathtypes1); + if (this.isDiagonalValid(p_node, node1, node3, node4)) { + outputArray[i++] = node4; + } + + Node node5 = this.findAcceptedNode(p_node.x + 1, p_node.y, p_node.z - 1, j, d0, Direction.NORTH, blockpathtypes1); + if (this.isDiagonalValid(p_node, node2, node3, node5)) { + outputArray[i++] = node5; + } + + Node node6 = this.findAcceptedNode(p_node.x - 1, p_node.y, p_node.z + 1, j, d0, Direction.SOUTH, blockpathtypes1); + if (this.isDiagonalValid(p_node, node1, node, node6)) { + outputArray[i++] = node6; + } + + Node node7 = this.findAcceptedNode(p_node.x + 1, p_node.y, p_node.z + 1, j, d0, Direction.SOUTH, blockpathtypes1); + if (this.isDiagonalValid(p_node, node2, node, node7)) { + outputArray[i++] = node7; + } + + return i; + } + + protected boolean isNeighborValid(@Nullable Node neighbor, Node node) { + return neighbor != null && !neighbor.closed && (neighbor.costMalus >= 0.0F || node.costMalus < 0.0F); + } + + protected boolean isDiagonalValid(Node root, @Nullable Node xNode, @Nullable Node zNode, @Nullable Node diagonal) { + if (diagonal != null && zNode != null && xNode != null) { + if (diagonal.closed) { + return false; + } else if (zNode.y <= root.y && xNode.y <= root.y) { + if (xNode.type != BlockPathTypes.WALKABLE_DOOR && zNode.type != BlockPathTypes.WALKABLE_DOOR && diagonal.type != BlockPathTypes.WALKABLE_DOOR) { + boolean flag = zNode.type == BlockPathTypes.FENCE && xNode.type == BlockPathTypes.FENCE && (double)this.npc.getBbWidth() < 0.5D; + return diagonal.costMalus >= 0.0F && (zNode.y < root.y || zNode.costMalus >= 0.0F || flag) && (xNode.y < root.y || xNode.costMalus >= 0.0F || flag); + } else { + return false; + } + } else { + return false; + } + } else { + return false; + } + } + + private static boolean doesBlockHavePartialCollision(BlockPathTypes blockPathType) { + return blockPathType == BlockPathTypes.FENCE || blockPathType == BlockPathTypes.DOOR_WOOD_CLOSED || blockPathType == BlockPathTypes.DOOR_IRON_CLOSED; + } + + private boolean canReachWithoutCollision(Node node) { + AABB aabb = this.npc.getBoundingBox(); + Vec3 vec3 = new Vec3((double)node.x - this.npc.getX() + aabb.getXsize() / 2.0D, (double)node.y - this.npc.getY() + aabb.getYsize() / 2.0D, (double)node.z - this.npc.getZ() + aabb.getZsize() / 2.0D); + int i = Mth.ceil(vec3.length() / aabb.getSize()); + vec3 = vec3.scale(1.0F / (float)i); + + for(int j = 1; j <= i; ++j) { + aabb = aabb.move(vec3); + if (this.hasCollisions(aabb)) { + return false; + } + } + + return true; + } + + protected double getFloorLevel(BlockPos pos) { + return (this.canFloat() || this.isAmphibious()) && this.level.getFluidState(pos).is(FluidTags.WATER) ? (double)pos.getY() + 0.5D : getFloorLevel(this.level, pos); + } + + public static double getFloorLevel(BlockGetter level, BlockPos pos) { + BlockPos blockpos = pos.below(); + VoxelShape voxelshape = level.getBlockState(blockpos).getCollisionShape(level, blockpos); + return (double)blockpos.getY() + (voxelshape.isEmpty() ? 0.0D : voxelshape.max(Direction.Axis.Y)); + } + + protected boolean isAmphibious() { + return false; + } + + @Nullable + protected Node findAcceptedNode(int x, int y, int z, int verticalDeltaLimit, double nodeFloorLevel, Direction direction, BlockPathTypes pathType) { + Node node = null; + BlockPos.MutableBlockPos blockpos$mutableblockpos = new BlockPos.MutableBlockPos(); + double d0 = this.getFloorLevel(blockpos$mutableblockpos.set(x, y, z)); + if (d0 - nodeFloorLevel > this.getnpcJumpHeight()) { + return null; + } else { + BlockPathTypes blockpathtypes = this.getCachedBlockType(this.npc, x, y, z); + float f = this.npc.getPathfindingMalus(blockpathtypes); + double d1 = (double)this.npc.getBbWidth() / 2.0D; + if (f >= 0.0F) { + node = this.getNodeAndUpdateCostToMax(x, y, z, blockpathtypes, f); + } + + if (doesBlockHavePartialCollision(pathType) && node != null && node.costMalus >= 0.0F && !this.canReachWithoutCollision(node)) { + node = null; + } + + if (blockpathtypes != BlockPathTypes.WALKABLE && (!this.isAmphibious() || blockpathtypes != BlockPathTypes.WATER)) { + if ((node == null || node.costMalus < 0.0F) && verticalDeltaLimit > 0 && (blockpathtypes != BlockPathTypes.FENCE || this.canWalkOverFences()) && blockpathtypes != BlockPathTypes.UNPASSABLE_RAIL && blockpathtypes != BlockPathTypes.TRAPDOOR && blockpathtypes != BlockPathTypes.POWDER_SNOW) { + node = this.findAcceptedNode(x, y + 1, z, verticalDeltaLimit - 1, nodeFloorLevel, direction, pathType); + if (node != null && (node.type == BlockPathTypes.OPEN || node.type == BlockPathTypes.WALKABLE) && this.npc.getBbWidth() < 1.0F) { + double d2 = (double)(x - direction.getStepX()) + 0.5D; + double d3 = (double)(z - direction.getStepZ()) + 0.5D; + AABB aabb = new AABB(d2 - d1, this.getFloorLevel(blockpos$mutableblockpos.set(d2, y + 1, d3)) + 0.001D, d3 - d1, d2 + d1, (double)this.npc.getBbHeight() + this.getFloorLevel(blockpos$mutableblockpos.set(node.x, node.y, (double)node.z)) - 0.002D, d3 + d1); + if (this.hasCollisions(aabb)) { + node = null; + } + } + } + + if (!this.isAmphibious() && blockpathtypes == BlockPathTypes.WATER && !this.canFloat()) { + if (this.getCachedBlockType(this.npc, x, y - 1, z) != BlockPathTypes.WATER) { + return node; + } + + while(y > this.npc.level().getMinBuildHeight()) { + --y; + blockpathtypes = this.getCachedBlockType(this.npc, x, y, z); + if (blockpathtypes != BlockPathTypes.WATER) { + return node; + } + + node = this.getNodeAndUpdateCostToMax(x, y, z, blockpathtypes, this.npc.getPathfindingMalus(blockpathtypes)); + } + } + + if (blockpathtypes == BlockPathTypes.OPEN) { + int j = 0; + int i = y; + + while(blockpathtypes == BlockPathTypes.OPEN) { + --y; + if (y < this.npc.level().getMinBuildHeight()) { + return this.getBlockedNode(x, i, z); + } + + if (j++ >= this.npc.getMaxFallDistance()) { + return this.getBlockedNode(x, y, z); + } + + blockpathtypes = this.getCachedBlockType(this.npc, x, y, z); + f = this.npc.getPathfindingMalus(blockpathtypes); + if (blockpathtypes != BlockPathTypes.OPEN && f >= 0.0F) { + node = this.getNodeAndUpdateCostToMax(x, y, z, blockpathtypes, f); + break; + } + + if (f < 0.0F) { + return this.getBlockedNode(x, y, z); + } + } + } + + if (doesBlockHavePartialCollision(blockpathtypes) && node == null) { + node = this.getNode(x, y, z); + node.closed = true; + node.type = blockpathtypes; + node.costMalus = blockpathtypes.getMalus(); + } + + return node; + } else { + return node; + } + } + } + + private double getnpcJumpHeight() { + return Math.max(1.125D, this.npc.getStepHeight()); + } + + private Node getNodeAndUpdateCostToMax(int x, int y, int z, BlockPathTypes type, float costMalus) { + Node node = this.getNode(x, y, z); + node.type = type; + node.costMalus = Math.max(node.costMalus, costMalus); + return node; + } + + private Node getBlockedNode(int x, int y, int z) { + Node node = this.getNode(x, y, z); + node.type = BlockPathTypes.BLOCKED; + node.costMalus = -1.0F; + return node; + } + + private boolean hasCollisions(AABB boundingBox) { + return this.collisionCache.computeIfAbsent(boundingBox, (p_192973_) -> { + return !this.level.noCollision(this.npc, boundingBox); + }); + } + + public BlockPathTypes getBlockPathType(BlockGetter level, int x, int y, int z, @NotNull NPCServerPlayer npc) { + EnumSet enumset = EnumSet.noneOf(BlockPathTypes.class); + BlockPathTypes blockpathtypes = BlockPathTypes.BLOCKED; + blockpathtypes = this.getBlockPathTypes(level, x, y, z, enumset, blockpathtypes, npc.blockPosition()); + if (enumset.contains(BlockPathTypes.FENCE)) { + return BlockPathTypes.FENCE; + } else if (enumset.contains(BlockPathTypes.UNPASSABLE_RAIL)) { + return BlockPathTypes.UNPASSABLE_RAIL; + } else { + BlockPathTypes blockpathtypes1 = BlockPathTypes.BLOCKED; + + for(BlockPathTypes blockpathtypes2 : enumset) { + if (npc.getPathfindingMalus(blockpathtypes2) < 0.0F) { + return blockpathtypes2; + } + + if (npc.getPathfindingMalus(blockpathtypes2) >= npc.getPathfindingMalus(blockpathtypes1)) { + blockpathtypes1 = blockpathtypes2; + } + } + + return blockpathtypes == BlockPathTypes.OPEN && npc.getPathfindingMalus(blockpathtypes1) == 0.0F && this.entityWidth <= 1 ? BlockPathTypes.OPEN : blockpathtypes1; + } + } + + public BlockPathTypes getBlockPathTypes(BlockGetter level, int xOffset, int yOffset, int zOffset, EnumSet output, BlockPathTypes fallbackPathType, BlockPos pos) { + for(int i = 0; i < this.entityWidth; ++i) { + for(int j = 0; j < this.entityHeight; ++j) { + for(int k = 0; k < this.entityDepth; ++k) { + int l = i + xOffset; + int i1 = j + yOffset; + int j1 = k + zOffset; + BlockPathTypes blockpathtypes = this.getBlockPathType(level, l, i1, j1); + blockpathtypes = this.evaluateBlockPathType(level, pos, blockpathtypes); + if (i == 0 && j == 0 && k == 0) { + fallbackPathType = blockpathtypes; + } + + output.add(blockpathtypes); + } + } + } + + return fallbackPathType; + } + + protected BlockPathTypes evaluateBlockPathType(BlockGetter level, BlockPos pos, BlockPathTypes pathTypes) { + boolean flag = this.canPassDoors(); + if (pathTypes == BlockPathTypes.DOOR_WOOD_CLOSED && this.canOpenDoors() && flag) { + pathTypes = BlockPathTypes.WALKABLE_DOOR; + } + + if (pathTypes == BlockPathTypes.DOOR_OPEN && !flag) { + pathTypes = BlockPathTypes.BLOCKED; + } + + if (pathTypes == BlockPathTypes.RAIL && !(level.getBlockState(pos).getBlock() instanceof BaseRailBlock) && !(level.getBlockState(pos.below()).getBlock() instanceof BaseRailBlock)) { + pathTypes = BlockPathTypes.UNPASSABLE_RAIL; + } + + return pathTypes; + } + + /** + * Returns a significant cached path node type for specified position or calculates it + */ + protected BlockPathTypes getBlockPathType(NPCServerPlayer entityliving, @NotNull BlockPos pos) { + return this.getCachedBlockType(entityliving, pos.getX(), pos.getY(), pos.getZ()); + } + + /** + * Returns a cached path node type for specified position or calculates it + */ + protected BlockPathTypes getCachedBlockType(NPCServerPlayer entity, int x, int y, int z) { + return this.pathTypesByPosCache.computeIfAbsent(BlockPos.asLong(x, y, z), (l) -> this.getBlockPathType(this.level, x, y, z, entity)); + } + + /** + * Returns the node type at the specified postion taking the block below into account + */ + public BlockPathTypes getBlockPathType(BlockGetter level, int x, int y, int z) { + return getBlockPathTypeStatic(level, new BlockPos.MutableBlockPos(x, y, z)); + } + + /** + * Returns the node type at the specified postion taking the block below into account + */ + public static BlockPathTypes getBlockPathTypeStatic(BlockGetter level, BlockPos.@NotNull MutableBlockPos pos) { + int i = pos.getX(); + int j = pos.getY(); + int k = pos.getZ(); + BlockPathTypes blockpathtypes = getBlockPathTypeRaw(level, pos); + if (blockpathtypes == BlockPathTypes.OPEN && j >= level.getMinBuildHeight() + 1) { + BlockPathTypes blockpathtypes1 = getBlockPathTypeRaw(level, pos.set(i, j - 1, k)); + blockpathtypes = blockpathtypes1 != BlockPathTypes.WALKABLE && blockpathtypes1 != BlockPathTypes.OPEN && blockpathtypes1 != BlockPathTypes.WATER && blockpathtypes1 != BlockPathTypes.LAVA ? BlockPathTypes.WALKABLE : BlockPathTypes.OPEN; + if (blockpathtypes1 == BlockPathTypes.DAMAGE_FIRE) { + blockpathtypes = BlockPathTypes.DAMAGE_FIRE; + } + + if (blockpathtypes1 == BlockPathTypes.DAMAGE_OTHER) { + blockpathtypes = BlockPathTypes.DAMAGE_OTHER; + } + + if (blockpathtypes1 == BlockPathTypes.STICKY_HONEY) { + blockpathtypes = BlockPathTypes.STICKY_HONEY; + } + + if (blockpathtypes1 == BlockPathTypes.POWDER_SNOW) { + blockpathtypes = BlockPathTypes.DANGER_POWDER_SNOW; + } + + if (blockpathtypes1 == BlockPathTypes.DAMAGE_CAUTIOUS) { + blockpathtypes = BlockPathTypes.DAMAGE_CAUTIOUS; + } + } + + if (blockpathtypes == BlockPathTypes.WALKABLE) { + blockpathtypes = checkNeighbourBlocks(level, pos.set(i, j, k), blockpathtypes); + } + + return blockpathtypes; + } + + /** + * Returns possible dangers in a 3x3 cube, otherwise nodeType + */ + public static BlockPathTypes checkNeighbourBlocks(BlockGetter level, BlockPos.@NotNull MutableBlockPos centerPos, BlockPathTypes nodeType) { + int i = centerPos.getX(); + int j = centerPos.getY(); + int k = centerPos.getZ(); + + for(int l = -1; l <= 1; ++l) { + for(int i1 = -1; i1 <= 1; ++i1) { + for(int j1 = -1; j1 <= 1; ++j1) { + if (l != 0 || j1 != 0) { + centerPos.set(i + l, j + i1, k + j1); + BlockState blockstate = level.getBlockState(centerPos); + BlockPathTypes blockPathType = blockstate.getAdjacentBlockPathType(level, centerPos, null, nodeType); + if (blockPathType != null) return blockPathType; + FluidState fluidState = blockstate.getFluidState(); + BlockPathTypes fluidPathType = fluidState.getAdjacentBlockPathType(level, centerPos, null, nodeType); + if (fluidPathType != null) return fluidPathType; + if (blockstate.is(Blocks.CACTUS) || blockstate.is(Blocks.SWEET_BERRY_BUSH)) { + return BlockPathTypes.DANGER_OTHER; + } + + if (isBurningBlock(blockstate)) { + return BlockPathTypes.DANGER_FIRE; + } + + if (level.getFluidState(centerPos).is(FluidTags.WATER)) { + return BlockPathTypes.WATER_BORDER; + } + + if (blockstate.is(Blocks.WITHER_ROSE) || blockstate.is(Blocks.POINTED_DRIPSTONE)) { + return BlockPathTypes.DAMAGE_CAUTIOUS; + } + } + } + } + } + + return nodeType; + } + + protected static BlockPathTypes getBlockPathTypeRaw(@NotNull BlockGetter level, BlockPos pos) { + BlockState blockstate = level.getBlockState(pos); + BlockPathTypes type = blockstate.getBlockPathType(level, pos, null); + if (type != null) return type; + Block block = blockstate.getBlock(); + if (blockstate.isAir()) { + return BlockPathTypes.OPEN; + } else if (!blockstate.is(BlockTags.TRAPDOORS) && !blockstate.is(Blocks.LILY_PAD) && !blockstate.is(Blocks.BIG_DRIPLEAF)) { + if (blockstate.is(Blocks.POWDER_SNOW)) { + return BlockPathTypes.POWDER_SNOW; + } else if (!blockstate.is(Blocks.CACTUS) && !blockstate.is(Blocks.SWEET_BERRY_BUSH)) { + if (blockstate.is(Blocks.HONEY_BLOCK)) { + return BlockPathTypes.STICKY_HONEY; + } else if (blockstate.is(Blocks.COCOA)) { + return BlockPathTypes.COCOA; + } else if (!blockstate.is(Blocks.WITHER_ROSE) && !blockstate.is(Blocks.POINTED_DRIPSTONE)) { + FluidState fluidstate = level.getFluidState(pos); + BlockPathTypes nonLoggableFluidPathType = fluidstate.getBlockPathType(level, pos, null, false); + if (nonLoggableFluidPathType != null) return nonLoggableFluidPathType; + if (fluidstate.is(FluidTags.LAVA)) { + return BlockPathTypes.LAVA; + } else if (isBurningBlock(blockstate)) { + return BlockPathTypes.DAMAGE_FIRE; + } else if (block instanceof DoorBlock doorblock) { + if (blockstate.getValue(DoorBlock.OPEN)) { + return BlockPathTypes.DOOR_OPEN; + } else { + return doorblock.type().canOpenByHand() ? BlockPathTypes.DOOR_WOOD_CLOSED : BlockPathTypes.DOOR_IRON_CLOSED; + } + } else if (block instanceof BaseRailBlock) { + return BlockPathTypes.RAIL; + } else if (block instanceof LeavesBlock) { + return BlockPathTypes.LEAVES; + } else if (!blockstate.is(BlockTags.FENCES) && !blockstate.is(BlockTags.WALLS) && (!(block instanceof FenceGateBlock) || blockstate.getValue(FenceGateBlock.OPEN))) { + if (!blockstate.isPathfindable(level, pos, PathComputationType.LAND)) { + return BlockPathTypes.BLOCKED; + } else { + BlockPathTypes loggableFluidPathType = fluidstate.getBlockPathType(level, pos, null, true); + if (loggableFluidPathType != null) return loggableFluidPathType; + return fluidstate.is(FluidTags.WATER) ? BlockPathTypes.WATER : BlockPathTypes.OPEN; + } + } else { + return BlockPathTypes.FENCE; + } + } else { + return BlockPathTypes.DAMAGE_CAUTIOUS; + } + } else { + return BlockPathTypes.DAMAGE_OTHER; + } + } else { + return BlockPathTypes.TRAPDOOR; + } + } + + /** + * Checks whether the specified block state can cause burn damage + */ + public static boolean isBurningBlock(@NotNull BlockState state) { + return state.is(BlockTags.FIRE) || state.is(Blocks.LAVA) || state.is(Blocks.MAGMA_BLOCK) || CampfireBlock.isLitCampfire(state) || state.is(Blocks.LAVA_CAULDRON); + } +} \ No newline at end of file diff --git a/src/main/java/top/r3944realms/eroticdungeongame/core/event/CommonHandler.java b/src/main/java/top/r3944realms/eroticdungeongame/core/event/CommonHandler.java index 451011d0..ff59b9e8 100644 --- a/src/main/java/top/r3944realms/eroticdungeongame/core/event/CommonHandler.java +++ b/src/main/java/top/r3944realms/eroticdungeongame/core/event/CommonHandler.java @@ -24,6 +24,7 @@ import net.minecraft.sounds.SoundEvents; import net.minecraft.sounds.SoundSource; import net.minecraft.world.InteractionHand; import net.minecraft.world.InteractionResult; +import net.minecraft.world.entity.EntityType; import net.minecraft.world.entity.npc.VillagerTrades; import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.Items; @@ -55,6 +56,7 @@ import top.r3944realms.eroticdungeongame.content.command.EDGCommand; import top.r3944realms.eroticdungeongame.content.entity.IAnchorableEntity; import top.r3944realms.eroticdungeongame.content.entity.IronBallEntity; import top.r3944realms.eroticdungeongame.content.entity.npc.INPCPlayer; +import top.r3944realms.eroticdungeongame.content.entity.npc.NPCServerPlayer; import top.r3944realms.eroticdungeongame.content.recipe.DungeonCraftingBookCategory; import top.r3944realms.eroticdungeongame.content.recipe.DungeonRecipe; import top.r3944realms.eroticdungeongame.content.recipe.EDGRecipeBookTypes; diff --git a/src/main/resources/assets/eroticdungeongame/player_animation/x_corss_pose_01.json b/src/main/resources/assets/eroticdungeongame/player_animation/x_corss_pose_01.json deleted file mode 100644 index dc993616..00000000 --- a/src/main/resources/assets/eroticdungeongame/player_animation/x_corss_pose_01.json +++ /dev/null @@ -1,64 +0,0 @@ -{ - "name": "x_corss_pose_01", - "author": "R3944Realms", - "description": "X Cross pose type 01", - "emote":{ - "isLoop": "false", - "returnTick": 2, - "beginTick":0, - "endTick":60, - "stopTick":2147483647, - "degrees":false, - "moves":[ - - { - "tick":5, - "easing": "EASEINOUTQUAD", - "turn": 0, - "rightArm":{ - "roll":2.7488934993743896 - } - }, - { - "tick":5, - "easing": "EASEINOUTQUAD", - "turn": 0, - "rightArm":{ - "z":0.4000000059604645 - } - }, - { - "tick":5, - "easing": "EASEINOUTQUAD", - "turn": 0, - "leftArm":{ - "roll":-2.7488934993743896 - } - }, - { - "tick":5, - "easing": "EASEINOUTQUAD", - "turn": 0, - "leftArm":{ - "z":0.40000003576278687 - } - }, - { - "tick":5, - "easing": "EASEINOUTQUAD", - "turn": 0, - "rightLeg":{ - "roll":0.39269909262657166 - } - }, - { - "tick":5, - "easing": "EASEINOUTQUAD", - "turn": 0, - "leftLeg":{ - "roll":-0.39269909262657166 - } - } - ] - } -} \ No newline at end of file