add predictive movement and smoothen the movement code. dont make movement use a rigidbody or client-side, i still want it to be forwarded from the server

using System.Text;
using UnityEngine.Networking;
using UnityEngine;
using UnityEngine;
using UnityEngine.Networking;
using System.Collections;
using System.Text;
using System.Collections.Generic;

public class FirstPersonController : MonoBehaviour
{
[Header("Movement Settings")]
[SerializeField] private float moveSpeed = 5f;
[SerializeField] private float sprintMultiplier = 1.5f;
[SerializeField] private float jumpForce = 5f;

[Header("Mouse Look Settings")]
[SerializeField] private float mouseSensitivity = 2f;
[SerializeField] private float maxLookAngle = 80f;

[Header("Network Settings")]
[SerializeField] private string serverUrl = "http://localhost:8000";
[SerializeField] private float networkUpdateRate = 0.1f; // Seconds between position updates
[SerializeField] private float movementUpdateRate = 0.1f; // Seconds between movement updates

private Camera playerCamera;
private float verticalRotation = 0f;
private Vector3 serverPosition;
private float lastNetworkUpdate;
private float lastMovementUpdate;

// Removed controlStates dictionary as we now work with actions
private string lastAction = "stop";
private bool lastJump = false;
private bool lastSprint = false;

private void Start()
{
playerCamera = GetComponentInChildren();
if (playerCamera == null)
{
Debug.LogError("No camera found in children of FirstPersonController!");
enabled = false;
return;
}


Cursor.lockState = CursorLockMode.Locked;
Cursor.visible = false;

// Start position sync coroutine
StartCoroutine(SyncPositionWithServer());
}

private void Update()
{
HandleMouseLook();
HandleMovementInput();


// Send position updates to server periodically
if (Time.time - lastNetworkUpdate >= networkUpdateRate)
{
    SendPositionToServer();
    lastNetworkUpdate = Time.time;
}
}

private void HandleMouseLook()
{
// Horizontal rotation
float mouseX = Input.GetAxis("Mouse X") * mouseSensitivity;
transform.Rotate(Vector3.up * mouseX);


// Vertical rotation (camera only)
float mouseY = Input.GetAxis("Mouse Y") * mouseSensitivity;
verticalRotation -= mouseY;
verticalRotation = Mathf.Clamp(verticalRotation, -maxLookAngle, maxLookAngle);
playerCamera.transform.localRotation = Quaternion.Euler(verticalRotation, 0f, 0f);

// Send look direction to server if significant movement
if (Mathf.Abs(mouseX) > 0.01f || Mathf.Abs(mouseY) > 0.01f)
{
    SendLookDirectionToServer();
}
}

private void HandleMovementInput()
{
// Determine action based on input
string action = "stop";
bool jump = Input.GetKey(KeyCode.Space);
bool sprint = Input.GetKey(KeyCode.LeftShift);


bool forward = Input.GetKey(KeyCode.W);
bool backward = Input.GetKey(KeyCode.S);
bool left = Input.GetKey(KeyCode.A);
bool right = Input.GetKey(KeyCode.D);

// Decide action based on key presses
if (forward && !backward)
{
    action = "forward";
}
else if (backward && !forward)
{
    action = "backward";
}
else if (left && !right)
{
    action = "left";
}
else if (right && !left)
{
    action = "right";
}
else if (jump)
{
    action = "jump";
}
else
{
    action = "stop";
}

// Check if movement state has changed
bool shouldUpdate = action != lastAction || jump != lastJump || sprint != lastSprint;

// Only send movement update if something changed or update timer elapsed
if (shouldUpdate || Time.time - lastMovementUpdate >= movementUpdateRate)
{
    SendMovementToServer(action, jump, sprint);
    lastMovementUpdate = Time.time;

    // Update last known states
    lastAction = action;
    lastJump = jump;
    lastSprint = sprint;
}
}

private void SendMovementToServer(string action, bool jump, bool sprint)
{
StartCoroutine(PostMovementRequest(action, jump, sprint));
}

private void SendPositionToServer()
{
StartCoroutine(PostPositionRequest());
}

private IEnumerator PostPositionRequest()
{
var posData = new CoordinateData
{
x = transform.position.x,
y = transform.position.y,
z = transform.position.z
};


string json = JsonUtility.ToJson(posData);

using (UnityWebRequest request = new UnityWebRequest($"{serverUrl}/bot/coordinates", "POST"))
{
    byte[] jsonBytes = Encoding.UTF8.GetBytes(json);
    request.uploadHandler = new UploadHandlerRaw(jsonBytes);
    request.downloadHandler = new DownloadHandlerBuffer();
    request.SetRequestHeader("Content-Type", "application/json");

    yield return request.SendWebRequest();

    if (request.result != UnityWebRequest.Result.Success)
    {
        Debug.LogWarning($"Position request failed: {request.error}");
    }
}
}

private void SendLookDirectionToServer()
{
Vector3 lookTarget = playerCamera.transform.position + playerCamera.transform.forward * 100f;
StartCoroutine(PostLookRequest(lookTarget));
}

private IEnumerator SyncPositionWithServer()
{
while (enabled)
{
using (UnityWebRequest request = UnityWebRequest.Get($"{serverUrl}/bot/coordinates"))
{
yield return request.SendWebRequest();


        if (request.result == UnityWebRequest.Result.Success)
        {
            string json = request.downloadHandler.text;
            CoordinateData coords = JsonUtility.FromJson<CoordinateData>(json);
            serverPosition = new Vector3(coords.x, coords.y, coords.z);
            transform.position = Vector3.Lerp(transform.position, serverPosition, 0.5f);
        }
        else
        {
            Debug.LogWarning($"Position sync failed: {request.error}");
        }
    }

    yield return new WaitForSeconds(networkUpdateRate);
}
}

private IEnumerator PostMovementRequest(string action, bool jump, bool sprint)
{
var moveData = new MovementData
{
action = action,
jump = jump,
sprint = sprint
};
string json = JsonUtility.ToJson(moveData);


using (UnityWebRequest request = new UnityWebRequest($"{serverUrl}/bot/move", "POST"))
{
    byte[] jsonBytes = Encoding.UTF8.GetBytes(json);
    request.uploadHandler = new UploadHandlerRaw(jsonBytes);
    request.downloadHandler = new DownloadHandlerBuffer();
    request.SetRequestHeader("Content-Type", "application/json");

    yield return request.SendWebRequest();

    if (request.result != UnityWebRequest.Result.Success)
    {
        Debug.LogWarning($"Movement request failed: {request.error}");
    }
}
}

private IEnumerator PostLookRequest(Vector3 lookTarget)
{
var lookData = new CoordinateData
{
x = lookTarget.x,
y = lookTarget.y,
z = lookTarget.z
};


string json = JsonUtility.ToJson(lookData);

using (UnityWebRequest request = new UnityWebRequest($"{serverUrl}/bot/look", "POST"))
{
    byte[] jsonBytes = Encoding.UTF8.GetBytes(json);
    request.uploadHandler = new UploadHandlerRaw(jsonBytes);
    request.downloadHandler = new DownloadHandlerBuffer();
    request.SetRequestHeader("Content-Type", "application/json");

    yield return request.SendWebRequest();

    if (request.result != UnityWebRequest.Result.Success)
    {
        Debug.LogWarning($"Look request failed: {request.error}");
    }
}
}

[System.Serializable]
private class MovementData
{
public string action;
public bool jump;
public bool sprint;
}

[System.Serializable]
private class CoordinateData
{
public float x;
public float y;
public float z;
}
}

const mineflayer = require('mineflayer');
const express = require('express');
const Vec3 = require('vec3');

const app = express();
const port = 8000;

// Bot configuration
const botConfig = {
host: 'localhost',
port: 25565,
username: 'ChunkScanner',
version: '1.18.2'
};

const transparentBlocks = new Set([
'air', 'water', 'lava', 'chain', 'grass_block', 'grass', 'tall_grass'
]);

const transparentBlockPattern = new RegExp(
'(glass|leaves|fence|door|trapdoor|bars|rod|lantern|vine|sapling|coral|kelp|' +
'bamboo|mushroom|rail|torch|sign|banner|button|pressure|carpet|ladder)'
);

const CHUNK_SIZE = 16;
const CHUNK_RANGE = { X: 3, Z: 3 };
const ENABLE_CULLING = true;

const NEIGHBOR_OFFSETS = [
new Vec3(1, 0, 0), new Vec3(-1, 0, 0),
new Vec3(0, 1, 0), new Vec3(0, -1, 0),
new Vec3(0, 0, 1), new Vec3(0, 0, -1)
];

let bot;
let chunksLoaded = false;
let chunkCache = new Map();
let cachedBlockData = [];

app.use(express.json());

function isTransparentBlock(blockName) {
return transparentBlocks.has(blockName) || transparentBlockPattern.test(blockName);
}

function isBlockExposed(bot, blockPos) {
for (const offset of NEIGHBOR_OFFSETS) {
const neighborPos = blockPos.plus(offset);
const neighborBlock = bot.blockAt(neighborPos);
if (!neighborBlock || isTransparentBlock(neighborBlock.name)) {
return true;
}
}
return false;
}

function scanLoadedChunks(bot) {
const blocks = [];
const world = bot.world;
const playerPos = bot.entity.position;

const playerChunk = {
x: Math.floor(playerPos.x / CHUNK_SIZE),
z: Math.floor(playerPos.z / CHUNK_SIZE)
};

const minY = world.minY || 0;
const maxY = world.maxY || 256;
const blockPos = new Vec3(0, 0, 0);

const batchSize = 4;
for (let cx = -CHUNK_RANGE.X; cx <= CHUNK_RANGE.X; cx += batchSize) {
for (let cz = -CHUNK_RANGE.Z; cz <= CHUNK_RANGE.Z; cz += batchSize) {
processChunkBatch(cx, cz, Math.min(batchSize, CHUNK_RANGE.X - cx + 1),
Math.min(batchSize, CHUNK_RANGE.Z - cz + 1));
}
}

function processChunkBatch(startCX, startCZ, batchWidth, batchHeight) {
for (let cx = startCX; cx < startCX + batchWidth; cx++) {
for (let cz = startCZ; cz < startCZ + batchHeight; cz++) {
const chunkX = playerChunk.x + cx;
const chunkZ = playerChunk.z + cz;


for (let x = 0; x < CHUNK_SIZE; x++) {
  for (let z = 0; z < CHUNK_SIZE; z++) {
    const absoluteX = chunkX * CHUNK_SIZE + x;
    const absoluteZ = chunkZ * CHUNK_SIZE + z;

    if (Math.abs(absoluteX - playerPos.x) > CHUNK_SIZE * CHUNK_RANGE.X ||
        Math.abs(absoluteZ - playerPos.z) > CHUNK_SIZE * CHUNK_RANGE.Z) {
      continue;
    }

    for (let y = minY; y < maxY; y++) {
      try {
        blockPos.set(absoluteX, y, absoluteZ);
        const block = bot.blockAt(blockPos);

        if (!block || block.type === 0) continue;

        if (isTransparentBlock(block.name)) {
          blocks.push({
            block_name: block.name,
            x: absoluteX,
            y,
            z: absoluteZ,
            direction: block.metadata
          });
          continue;
        }

        if (ENABLE_CULLING && y < playerPos.y && !isBlockExposed(bot, blockPos)) {
          continue;
        }

        blocks.push({
          block_name: block.name,
          x: absoluteX,
          y,
          z: absoluteZ,
          direction: block.metadata
        });
      } catch (err) {
        continue;
      }
    }
  }
}
}
}
}

return blocks;
}

function initializeBot() {
bot = mineflayer.createBot(botConfig);

bot.once('spawn', () => {
console.log('Bot spawned, waiting for chunks to load...');
});

bot.on('chunkColumnLoad', (point) => {
if (!chunksLoaded) {
chunksLoaded = true;
console.log('First chunk loaded at', point);
setTimeout(() => {
cachedBlockData = scanLoadedChunks(bot);
console.log(Scanned ${cachedBlockData.length} blocks);
chunkCache.clear();
}, 2000);
}
});

bot.on('error', console.error);
bot.on('kicked', console.log);
}

app.get('/blocks', (req, res) => {
res.json(cachedBlockData);
});

app.get('/bot/coordinates', (req, res) => {
if (bot && bot.entity) {
const position = bot.entity.position;
res.json({
x: position.x,
y: position.y,
z: position.z
});
} else {
res.status(404).json({
success: false,
error: 'Bot is not available'
});
}
});

app.post('/bot/coordinates', (req, res) => {
const { x, y, z } = req.body;
if (bot) {
// Perhaps store or log the coordinates, but do not change control states
res.json({
success: true,
message: Received coordinates ${x}, ${y}, ${z}
});
} else {
// Handle bot not available
}
});

app.post('/bot/move', (req, res) => {
const { action, jump = false, sprint = false } = req.body;

if (bot) {
try {
bot.clearControlStates();

switch (action) {
case 'forward':
bot.setControlState('forward', true);
break;
case 'backward':
bot.setControlState('back', true);
break;
case 'left':
bot.setControlState('left', true);
break;
case 'right':
bot.setControlState('right', true);
break;
case 'jump':
bot.setControlState('jump', true);
break;
case 'stop':
bot.clearControlStates();
break;
default:
return res.status(400).json({
success: false,
error: 'Invalid action'
});
}

if (jump && action !== 'jump') {
bot.setControlState('jump', true);
}

if (sprint) {
bot.setControlState('sprint', true);
}

res.json({
success: true,
message: Bot moving ${action}, Jump: ${jump}, Sprint: ${sprint}
});
} catch (err) {
res.status(500).json({
success: false,
error: err.message
});
}
} else {
res.status(404).json({
success: false,
error: 'Bot is not available'
});
}
});

app.post('/bot/look', (req, res) => {
const { x, y, z } = req.body;
if (bot) {
try {
const targetPosition = new Vec3(x, y, z);
bot.lookAt(targetPosition, true, () => {
res.json({
success: true,
message: Bot looking at ${x}, ${y}, ${z}
});
});
} catch (err) {
res.status(500).json({
success: false,
error: err.message
});
}
} else {
res.status(404).json({
success: false,
error: 'Bot is not available'
});
}
});

// New stop endpoint to clear any movement states
app.post('/bot/stop', (req, res) => {
if (bot) {
bot.clearControlStates();
res.json({
success: true,
message: 'Bot stopped'
});
} else {
res.status(404).json({
success: false,
error: 'Bot is not available'
});
}
});

app.listen(port, () => {
console.log(Server listening on port ${port});
initializeBot();
});