using System.Collections; using UnityEngine; public partial class EntityAliveSDXV4 { // ========================================================================= // Position / movement // ========================================================================= public override void OnUpdatePosition(float _partialTicks) { if (isHirable && position.y <= 0) { // Use frame cache leader if available; otherwise fall back to a direct lookup. var leader = _frameCache.HasLeader ? _frameCache.Leader : EntityUtilities.GetLeaderOrOwner(entityId) as EntityAlive; if (leader != null) { TeleportToPlayer(leader); return; } } base.OnUpdatePosition(_partialTicks); } // ========================================================================= // Movement // ========================================================================= /// /// EntityTrader overrides MoveEntityHeaded and does NOT call base, which leaves the NPC /// standing in a T-pose with no physics response. This override restores the full /// EntityAlive movement pipeline, including root-motion blending, jump handling, and /// animation speed state. /// public override void MoveEntityHeaded(Vector3 _direction, bool _isDirAbsolute) { var isBusy = false; if (emodel != null && emodel.avatarController != null) emodel.avatarController.TryGetBool("IsBusy", out isBusy); if (isBusy) return; if (walkType == 21 && Jumping) { motion = accumulatedRootMotion; accumulatedRootMotion = Vector3.zero; IsRotateToGroundFlat = true; if (moveHelper != null) { var delta = moveHelper.JumpToPos - position; if (Utils.FastAbs(delta.y) < 0.2f) motion.y = delta.y * 0.2f; if (Utils.FastAbs(delta.x) < 0.3f) motion.x = delta.x * 0.2f; if (Utils.FastAbs(delta.z) < 0.3f) motion.z = delta.z * 0.2f; if (delta.sqrMagnitude < 0.010000001f) { if (emodel && emodel.avatarController) emodel.avatarController.StartAnimationJump(AnimJumpMode.Land); Jumping = false; } } entityCollision(motion); return; } // EntityTrader does not call base here — we replicate EntityAlive's logic directly. if (AttachedToEntity != null) return; if (jumpIsMoving) { JumpMove(); return; } if (RootMotion) { if (isEntityRemote && bodyDamage.CurrentStun == EnumEntityStunType.None && !IsDead() && (emodel == null || emodel.avatarController == null || !emodel.avatarController.IsAnimationHitRunning())) { accumulatedRootMotion = Vector3.zero; return; } bool ragdoll = emodel && emodel.IsRagdollActive; if (isSwimming && !ragdoll) { motion += accumulatedRootMotion * 0.001f; } else if (onGround || jumpTicks > 0) { if (ragdoll) { motion.x = 0f; motion.z = 0f; } else { float y = motion.y; motion = accumulatedRootMotion; motion.y += y; } } accumulatedRootMotion = Vector3.zero; } if (IsFlyMode.Value) { EntityPlayerLocal primary = GameManager.Instance.World.GetPrimaryPlayer(); float godMod = primary != null ? primary.GodModeSpeedModifier : 1f; float speed = 2f * (MovementRunning ? 0.35f : 0.12f) * godMod; if (!RootMotion) Move(_direction, _isDirAbsolute, GetPassiveEffectSpeedModifier() * speed, GetPassiveEffectSpeedModifier() * speed); if (!IsNoCollisionMode.Value) { entityCollision(motion); motion *= ConditionalScalePhysicsMulConstant(0.546f); } else { SetPosition(position + motion, true); motion = Vector3.zero; } } else { DefaultMoveEntity(_direction, _isDirAbsolute); } if (!isEntityRemote && RootMotion) { float factor = landMovementFactor * 2.5f; if (inWaterPercent > 0.3f) { if (factor > 0.01f) { float t = (inWaterPercent - 0.3f) * 1.4285715f; factor = Mathf.Lerp(factor, 0.01f + (factor - 0.01f) * 0.1f, t); } if (isSwimming) factor = landMovementFactor * 5f; } float mag = _direction.magnitude; if (mag > 1f) factor /= mag; float fwd = _direction.z * factor; if (lerpForwardSpeed) { if (Utils.FastAbs(speedForwardTarget - fwd) > 0.05f) speedForwardTargetStep = Utils.FastAbs(fwd - speedForward) / 0.18f; speedForwardTarget = fwd; } else { speedForward = fwd; } speedStrafe = _direction.x * factor; SetMovementState(); ReplicateSpeeds(); } } /// /// Returns the Y coordinate one block above the highest solid block at (x, z), /// scanning upward from terrain height through any player-placed blocks (e.g. farm plots). /// Unlike GetHeightAt, this accounts for structures built on top of terrain. /// public static int GetSurfaceY(float x, float z) { var world = GameManager.Instance.World; int y = (int)world.GetHeightAt(x, z); int bx = (int)x; int bz = (int)z; int cap = y + 20; while (y < cap && world.GetBlock(bx, y + 1, bz).Block.shape.IsSolidSpace) y++; return y + 1; } public void TeleportToPlayer(EntityAlive target, bool randomPosition = false) { if (target == null) return; if (EntityUtilities.GetCurrentOrder(entityId) == EntityUtilities.Orders.Stay) return; if (EntityUtilities.GetCurrentOrder(entityId) == EntityUtilities.Orders.Guard) return; var dist2D = Vector2.Distance( new Vector2(target.position.x, target.position.z), new Vector2(position.x, position.z)); if (dist2D < 20f) return; if (isTeleporting) return; var myPosition = target.position + Vector3.back; var player = target as EntityPlayer; if (player != null) { myPosition = player.GetBreadcrumbPos(3f * rand.RandomFloat); if (Vector3.Distance(myPosition, player.position) > 40f) randomPosition = true; if (randomPosition) { Vector3 dir = target.position - position; myPosition = RandomPositionGenerator.CalcPositionInDirection(target, target.position, dir, 5, 80f); } // Find the actual surface, including player-placed blocks (e.g. farm plots). myPosition.y = GetSurfaceY(myPosition.x, myPosition.z); } motion = Vector3.zero; navigator?.clearPath(); SphereCache.RemovePaths(entityId); SetPosition(myPosition, true); StartCoroutine(ValidateTeleport(target, randomPosition)); } private IEnumerator ValidateTeleport(EntityAlive target, bool randomPosition = false) { yield return new WaitForSeconds(1f); var y = GetSurfaceY(position.x, position.z); if (position.y < y) { var myPosition = position; var player = target as EntityPlayer; if (player != null) myPosition = player.GetBreadcrumbPos(3f * rand.RandomFloat); if (randomPosition) { Vector3 dir = target.position - position; myPosition = RandomPositionGenerator.CalcPositionInDirection(target, target.position, dir, 5, 80f); } // Find the actual surface, including player-placed blocks (e.g. farm plots). myPosition.y = GetSurfaceY(myPosition.x, myPosition.z) + 1; motion = Vector3.zero; navigator?.clearPath(); SetPosition(myPosition, true); } isTeleporting = false; } public override void updateStepSound(float distX, float distZ, float rotDelta) { var leader = EntityUtilities.GetLeaderOrOwner(entityId) as EntityAlive; if (leader == null) { base.updateStepSound(distX, distZ, rotDelta); return; } if (leader.Buffs.GetCustomVar("quietNPC") > 0) return; if (IsCrouching) return; base.updateStepSound(distX, distZ, rotDelta); } public override float GetMoveSpeed() { var speed = EffectManager.GetValue(PassiveEffects.WalkSpeed, null, moveSpeed); if (IsCrouching) speed = EffectManager.GetValue(PassiveEffects.CrouchSpeed, null, moveSpeed); return speed; } public override float GetMoveSpeedAggro() => EffectManager.GetValue(PassiveEffects.RunSpeed, null, moveSpeedPanic); public new float GetMoveSpeedPanic() => EffectManager.GetValue(PassiveEffects.RunSpeed, null, moveSpeedPanic); public override float GetEyeHeight() { if (walkType == 21) return 0.15f; if (walkType == 22) return 0.6f; return IsCrouching ? base.height * 0.5f : base.height * 0.8f; } public override Ray GetLookRay() => new Ray(position + new Vector3(0f, GetEyeHeight() * eyeHeightHackMod, 0f), GetLookVector()); public override bool CanBePushed() => true; // ========================================================================= // Boundary box // ========================================================================= public void ConfigureBoundaryBox(Vector3 newSize, Vector3 center) { var col = gameObject.GetComponent(); if (!col) return; col.size = newSize; scaledExtent = new Vector3(col.size.x / 2f * transform.localScale.x, col.size.y / 2f * transform.localScale.y, col.size.z / 2f * transform.localScale.z); var offset = new Vector3(col.center.x * transform.localScale.x, col.center.y * transform.localScale.y, col.center.z * transform.localScale.z); boundingBox = BoundsUtils.BoundsForMinMax(-scaledExtent.x, -scaledExtent.y, -scaledExtent.z, scaledExtent.x, scaledExtent.y, scaledExtent.z); boundingBox.center += offset; if (center != Vector3.zero) boundingBox.center = center; } public override void updateSpeedForwardAndStrafe(Vector3 _dist, float _partialTicks) { if (isEntityRemote && _partialTicks > 1f) _dist /= _partialTicks; speedForward *= 0.5f; speedStrafe *= 0.5f; speedVertical *= 0.5f; if (Mathf.Abs(_dist.x) > 0.001f || Mathf.Abs(_dist.z) > 0.001f) { float s = Mathf.Sin(-rotation.y * Mathf.PI / 180f); float c = Mathf.Cos(-rotation.y * Mathf.PI / 180f); speedForward += c * _dist.z - s * _dist.x; speedStrafe += c * _dist.x + s * _dist.z; } if (Mathf.Abs(_dist.y) > 0.001f) speedVertical += _dist.y; SetMovementState(); } public void RestoreSpeed() { moveSpeed = EntityUtilities.GetFloatValue(entityId, "MoveSpeed"); var v = new Vector2(moveSpeed, moveSpeed); EntityClass.list[entityClass].Properties.ParseVec(EntityClass.PropMoveSpeedAggro, ref v); moveSpeedAggro = v.x; moveSpeedAggroMax = v.y; } // ========================================================================= // Stuck resolution (unchanged from EntityAliveSDX) // ========================================================================= public virtual void CheckStuck() { IsStuck = false; if (IsFlyMode.Value) return; // Throttle block lookups to every 5 ticks (~83 ms). Responsiveness is sufficient // since the push applied when stuck persists in `motion` across skipped frames. if (++_stuckCheckCounter % 5 != 0) return; // Skip when airborne or jumping — horizontal block stuck cannot occur mid-air. if (!onGround && !isSwimming) return; // Skip when already moving — a moving NPC is not stuck. // sqrMagnitude avoids the sqrt; threshold ~0.2 units/tick. if (motion.sqrMagnitude > 0.04f) return; // Cache position/dimension locals — avoids repeated property access across the four corner calls. var px = position.x; var pz = position.z; var yMin = boundingBox.min.y + 0.5f; var hw = width * 0.3f; var hd = depth * 0.3f; // Short-circuit after the first stuck corner — once a push direction is set // there is no benefit in checking the remaining corners. IsStuck = PushOutOfBlocks(px - hw, yMin, pz + hd) || PushOutOfBlocks(px - hw, yMin, pz - hd) || PushOutOfBlocks(px + hw, yMin, pz - hd) || PushOutOfBlocks(px + hw, yMin, pz + hd); if (IsStuck) return; var bx = Utils.Fastfloor(px); var by = Utils.Fastfloor(yMin); var bz = Utils.Fastfloor(pz); if (!ShouldPushOutOfBlock(bx, by, bz, true) || !CheckNonSolidVertical(new Vector3i(bx, by + 1, bz), 4, 2)) return; IsStuck = true; motion = new Vector3(0f, 1.6f, 0f); Log.Warning($"{EntityName} ({entityId}) is stuck. Unsticking."); } private bool PushOutOfBlocks(float _x, float _y, float _z) { var nx = Utils.Fastfloor(_x); var ny = Utils.Fastfloor(_y); var nz = Utils.Fastfloor(_z); var fx = _x - nx; var fz = _z - nz; if (!ShouldPushOutOfBlock(nx, ny, nz, false) && !ShouldPushOutOfBlock(nx, ny + 1, nz, false)) return false; bool l = !ShouldPushOutOfBlock(nx-1,ny,nz,true)&&!ShouldPushOutOfBlock(nx-1,ny+1,nz,true); bool r = !ShouldPushOutOfBlock(nx+1,ny,nz,true)&&!ShouldPushOutOfBlock(nx+1,ny+1,nz,true); bool b = !ShouldPushOutOfBlock(nx,ny,nz-1,true)&&!ShouldPushOutOfBlock(nx,ny+1,nz-1,true); bool f = !ShouldPushOutOfBlock(nx,ny,nz+1,true)&&!ShouldPushOutOfBlock(nx,ny+1,nz+1,true); byte dir = 255; float best = 9999f; if (l && fx < best) { best = fx; dir = 0; } if (r && 1f - fx < best) { best = 1f - fx; dir = 1; } if (b && fz < best) { best = fz; dir = 4; } if (f && 1f - fz < best) { dir = 5; } const float push = 0.1f; if (dir == 0) motion.x = -push; else if (dir == 1) motion.x = push; else if (dir == 4) motion.z = -push; else if (dir == 5) motion.z = push; return dir != 255; } private bool ShouldPushOutOfBlock(int x, int y, int z, bool checkTerrain) { var shape = world.GetBlock(x, y, z).Block.shape; if (shape.IsSolidSpace && !shape.IsTerrain()) return true; if (!checkTerrain || !shape.IsSolidSpace || !shape.IsTerrain()) return false; var above = world.GetBlock(x, y + 1, z).Block.shape; return above.IsSolidSpace && above.IsTerrain(); } private bool CheckNonSolidVertical(Vector3i pos, int maxY, int space) { for (int i = 0; i < maxY; i++) { if (world.GetBlock(pos.x, pos.y + i + 1, pos.z).Block.shape.IsSolidSpace) continue; bool clear = true; for (int j = 1; j < space; j++) { if (world.GetBlock(pos.x, pos.y + i + 1 + j, pos.z).Block.shape.IsSolidSpace) { clear = false; break; } } if (clear) return true; } return false; } }