NPC Think invoked for:	NPC_4864
PerformAction called for NPC:	NPC_4864
Sending JSON Data:	{"model":"llama3.2:3b","stream":false,"prompt":"You are an NPC in Garry's Mod. You can perform the following actions: Move to player, Move away, Equip shotgun, Random move, Say hello, Dance, Jump, Give item to player, Attack player, Wave, Sit down, Follow player, Stop following, Tell joke, Compliment player, Insult player, Pickup item, Drop item. You have access to these items: weapon_pistol, weapon_shotgun, weapon_crowbar. Players nearby: joober97. Your current position: -1342.213501 122.478073 -147.908890. Your health: 100. Decide your next action and respond exactly with one of the available commands as plain text without any quotes, e.g., Say hello. Do not add any additional text."}
HTTP request sent.
[NPC Error] HTTP request failed:
Timeout was reached
NPC_4864: [Error] HTTP request failed:
Timeout was reached
[NPC Error] HTTP request failed:
Timeout was reached
NPC_4864: [Error] HTTP request failed:
Timeout was reached


-- File: lua/entities/ai_npc/init.lua

AddCSLuaFile("cl_init.lua")
AddCSLuaFile("shared.lua")

include("shared.lua")

-- Define valid actions and items as entity properties for accessibility
ENT.validActions = {
    "Move to player",
    "Move away",
    "Equip shotgun",
    "Random move",
    "Say hello",
    "Dance",
    "Jump",
    "Give item to player",
    "Attack player",
    "Wave",
    "Sit down",
    "Follow player",
    "Stop following",
    "Tell joke",
    "Compliment player",
    "Insult player",
    "Pickup item",
    "Drop item"
}

ENT.validItems = {"weapon_pistol", "weapon_shotgun", "weapon_crowbar"}

-- Initialize the entity
function ENT:Initialize()
    print("Initializing NPC:", self:GetName())

    local models = {
        "models/Humans/Group01/male_01.mdl",
        "models/Humans/Group01/male_02.mdl",
        "models/Humans/Group01/male_03.mdl",
        "models/Humans/Group01/male_04.mdl",
        "models/Humans/Group01/male_05.mdl",
        "models/Humans/Group01/male_06.mdl",
        "models/Humans/Group01/male_07.mdl",
        "models/Humans/Group01/male_08.mdl",
        "models/Humans/Group01/male_09.mdl",
        "models/Humans/Group01/female_01.mdl",
        "models/Humans/Group01/female_02.mdl",
        "models/Humans/Group01/female_03.mdl",
        "models/Humans/Group01/female_04.mdl",
        "models/Humans/Group01/female_06.mdl",
        "models/Humans/Group01/female_07.mdl"
    }

    self:SetModel(models[math.random(#models)])
    self:SetHullType(HULL_HUMAN)
    self:SetHullSizeNormal()
    self:SetNPCState(NPC_STATE_SCRIPT)
    self:SetSolid(SOLID_BBOX)
    self:CapabilitiesAdd(bit.bor(
        CAP_MOVE_GROUND,
        CAP_ANIMATEDFACE,
        CAP_USE_WEAPONS,
        CAP_WEAPON_MELEE_ATTACK1,
        CAP_INNATE_MELEE_ATTACK1
    ))
    self:SetUseType(SIMPLE_USE)
    self:DropToFloor()
    self.NextThinkTime = 0
    self:SetHealth(100)
    self:SetMaxHealth(100)
    self:SetName("NPC_" .. math.random(1000, 9999))
    self.recentMessages = {}
    self.FollowTarget = nil

    print("NPC Initialized with model:", self:GetModel())

    -- Capture 'self' in a local variable for use within the hook
    local entity = self

    -- Hook to listen to chat messages with a unique identifier using EntIndex
    hook.Add("PlayerSay", "ChatListener_" .. self:EntIndex(), function(ply, text)
        if not IsValid(ply) or not ply:IsPlayer() then return end
        table.insert(entity.recentMessages, {player = ply:Nick(), text = text})
        if #entity.recentMessages > 5 then
            table.remove(entity.recentMessages, 1)
        end
    end)

    print("Hook for PlayerSay added for NPC:", self:GetName())
end

-- Handle damage taken by the NPC
function ENT:OnTakeDamage(dmginfo)
    self:SetHealth(self:Health() - dmginfo:GetDamage())
    if self:Health() <= 0 then
        self:Remove()
    end
end

-- Main thinking loop
function ENT:Think()
    if CurTime() >= self.NextThinkTime then
        print("NPC Think invoked for:", self:GetName())

        -- Perform thinking pose
        local sequence = self:LookupSequence("idle_allthinking") or self:LookupSequence("pose_standing_02")
        if sequence and sequence >= 0 then
            self:ResetSequence(sequence)
            print("Set thinking sequence:", sequence)
        end
        self:SetNPCState(NPC_STATE_SCRIPT)
        self:PerformAction()
        self.NextThinkTime = CurTime() + math.random(15, 30)
    else
        self:NextThink(CurTime())
    end

    -- Follow target if set
    if IsValid(self.FollowTarget) then
        self:SetLastPosition(self.FollowTarget:GetPos())
        self:SetSchedule(SCHED_FORCED_GO_RUN)
        print(self:GetName() .. " is following:", self.FollowTarget:GetName())
    end

    return true
end

-- Perform an action based on AI response
function ENT:PerformAction()
    print("PerformAction called for NPC:", self:GetName())

    local players = player.GetAll()
    local playerNames = {}
    for _, ply in ipairs(players) do
        table.insert(playerNames, ply:Nick())
    end

    -- Compile recent messages
    local messagesText = ""
    for _, msg in ipairs(self.recentMessages) do
        messagesText = messagesText .. msg.player .. ": " .. msg.text .. "\n"
    end

    -- Construct the prompt with explicit instructions
    local prompt = "You are an NPC in Garry's Mod."
    prompt = prompt .. " You can perform the following actions: " .. table.concat(self.validActions, ", ") .. "."
    prompt = prompt .. " You have access to these items: " .. table.concat(self.validItems, ", ") .. "."
    prompt = prompt .. " Players nearby: " .. table.concat(playerNames, ", ") .. "."
    if messagesText ~= "" then
        prompt = prompt .. " Recent chat messages:\n" .. messagesText
    end
    prompt = prompt .. " Your current position: " .. tostring(self:GetPos()) .. "."
    prompt = prompt .. " Your health: " .. self:Health() .. "."
    prompt = prompt .. " Decide your next action and respond exactly with one of the available commands as plain text without any quotes, e.g., Say hello. Do not add any additional text."

    -- Construct the JSON payload
    local payload = {
        model = "llama3.2:3b",  -- Ensure this matches a valid model name
        prompt = prompt,
        stream = false
    }

    -- Convert the table to JSON
    local jsonData = util.TableToJSON(payload)

    -- Debugging: Print the JSON data
    print("Sending JSON Data:", jsonData)

    -- Use the custom hostname instead of 'localhost'
    local url = "http://88.141.148.196:11434/api/generate"

    -- Send the HTTP request
    http.Fetch(
        url,
        function(result, statusCode, headers)
            local success, err = pcall(function()
                if statusCode == 200 then
                    -- Parse the JSON response
                    local status, response = pcall(util.JSONToTable, result)
                    if status and response then
                        local aiResponse = response.response or ""
                        local command = string.Trim(aiResponse)

                        -- Debugging: Print the AI response
                        print("AI Response:", command)

                        if command ~= "" then
                            -- Strip any leading/trailing quotes just in case
                            command = string.Trim(command, "\"'")
                            self:ExecuteCommand(command)
                        else
                            self:HandleError("AI responded with an empty command.")
                        end
                    else
                        self:HandleError("Failed to parse AI response as JSON.\nParsing Error:\n" .. tostring(response) .. "\nResponse:\n" .. result)
                    end
                else
                    self:HandleError("HTTP request failed with status code " .. statusCode .. ".\nResponse:\n" .. result)
                end
            end)
            if not success then
                self:HandleError("Error in success callback: " .. tostring(err))
            end
        end,
        function(errorMessage)
            local success, err = pcall(function()
                -- Handle HTTP failure
                self:HandleError("HTTP request failed:\n" .. errorMessage)
            end)
            if not success then
                print("Error in failure callback:", err)
            end
        end,
        {
            method = "POST",
            headers = {
                ["Content-Type"] = "application/json"
            },
            body = jsonData
        }
    )

    print("HTTP request sent.")
end

-- Execute the received command
function ENT:ExecuteCommand(command)
    print("ExecuteCommand called with command:", command)
    
    -- Check if the command is valid
    local validCommand = false
    for _, action in ipairs(self.validActions) do
        if string.lower(action) == string.lower(command) then
            validCommand = true
            command = action -- Use the correctly cased command
            break
        end
    end

    if not validCommand then
        self:HandleError("Unknown command received: '" .. command .. "'")
        return
    end

    local players = player.GetAll()

    if command == "Move to player" then
        if #players > 0 then
            local target = players[math.random(#players)]
            self:SetLastPosition(target:GetPos())
            self:SetSchedule(SCHED_FORCED_GO_RUN)
            print(self:GetName() .. " is moving to player:", target:Nick())
        end
    elseif command == "Move away" then
        local newPos = self:GetPos() + VectorRand() * 500
        self:SetLastPosition(newPos)
        self:SetSchedule(SCHED_FORCED_GO_RUN)
        print(self:GetName() .. " is moving away to position:", newPos)
    elseif command == "Equip shotgun" then
        self:Give("weapon_shotgun")
        print(self:GetName() .. " equipped a shotgun.")
    elseif command == "Random move" then
        local newPos = self:GetPos() + VectorRand() * 300
        self:SetLastPosition(newPos)
        self:SetSchedule(SCHED_FORCED_GO)
        print(self:GetName() .. " is moving randomly to position:", newPos)
    elseif command == "Say hello" then
        self:EmitSound("vo/npc/male01/hi0" .. math.random(1, 2) .. ".wav")
        print(self:GetName() .. " says hello.")
    elseif command == "Dance" then
        local sequence = self:LookupSequence("taunt_dance")
        if sequence and sequence >= 0 then
            self:ResetSequence(sequence)
            print(self:GetName() .. " is dancing.")
        else
            self:HandleError("Cannot perform 'Dance' action: Sequence 'taunt_dance' not found.")
        end
    elseif command == "Jump" then
        self:SetVelocity(Vector(0, 0, 200))
        print(self:GetName() .. " is jumping.")
    elseif command == "Give item to player" then
        if #players > 0 then
            local target = players[math.random(#players)]
            target:Give("weapon_pistol")
            self:SayToPlayer(target, "Here you go!")
            print(self:GetName() .. " gave a pistol to player:", target:Nick())
        end
    elseif command == "Attack player" then
        if #players > 0 then
            local target = players[math.random(#players)]
            self:SetEnemy(target)
            self:SetNPCState(NPC_STATE_COMBAT)
            print(self:GetName() .. " is attacking player:", target:Nick())
        end
    elseif command == "Wave" then
        local sequence = self:LookupSequence("wave")
        if sequence and sequence >= 0 then
            self:ResetSequence(sequence)
            print(self:GetName() .. " is waving.")
        else
            self:HandleError("Cannot perform 'Wave' action: Sequence 'wave' not found.")
        end
    elseif command == "Sit down" then
        local sequence = self:LookupSequence("sit_ground")
        if sequence and sequence >= 0 then
            self:ResetSequence(sequence)
            print(self:GetName() .. " is sitting down.")
        else
            self:HandleError("Cannot perform 'Sit down' action: Sequence 'sit_ground' not found.")
        end
    elseif command == "Follow player" then
        if #players > 0 then
            self.FollowTarget = players[math.random(#players)]
            self:SayToPlayer(self.FollowTarget, "I'll follow you!")
            print(self:GetName() .. " is now following player:", self.FollowTarget:Nick())
        end
    elseif command == "Stop following" then
        if IsValid(self.FollowTarget) then
            self:SayToPlayer(self.FollowTarget, "I'll stop following you.")
            print(self:GetName() .. " has stopped following player:", self.FollowTarget:Nick())
            self.FollowTarget = nil
        end
    elseif command == "Tell joke" then
        self:BroadcastMessage("Why did the physicist cross the road? Because the chicken was on the other side!")
        print(self:GetName() .. " told a joke.")
    elseif command == "Compliment player" then
        if #players > 0 then
            local target = players[math.random(#players)]
            self:SayToPlayer(target, "You're doing great, " .. target:Nick() .. "!")
            print(self:GetName() .. " complimented player:", target:Nick())
        end
    elseif command == "Insult player" then
        if #players > 0 then
            local target = players[math.random(#players)]
            self:SayToPlayer(target, "I bet you can't even code in Lua, " .. target:Nick() .. "!")
            print(self:GetName() .. " insulted player:", target:Nick())
        end
    elseif command == "Pickup item" then
        -- Find nearby items and pick one up
        local items = ents.FindInSphere(self:GetPos(), 100)
        local pickedUp = false
        for _, item in ipairs(items) do
            if item:GetClass() == "prop_physics" then
                item:Remove()
                self:SayToWorld("I picked up an item!")
                pickedUp = true
                print(self:GetName() .. " picked up an item.")
                break
            end
        end
        if not pickedUp then
            self:HandleError("No items nearby to pick up.")
        end
    elseif command == "Drop item" then
        -- Drop an item at the NPC's position
        local ent = ents.Create("prop_physics")
        ent:SetModel("models/props_junk/wood_crate001a.mdl")
        ent:SetPos(self:GetPos() + Vector(0, 0, 50))
        ent:Spawn()
        self:SayToWorld("I dropped an item!")
        print(self:GetName() .. " dropped an item.")
    else
        self:HandleError("Unknown command received: '" .. command .. "'")
    end
end

-- Function to send a message to a specific player
function ENT:SayToPlayer(ply, message)
    if IsValid(ply) and ply:IsPlayer() then
        ply:ChatPrint(self:GetName() .. ": " .. message)
    end
end

-- Function to send a message to all players
function ENT:SayToWorld(message)
    for _, ply in ipairs(player.GetAll()) do
        ply:ChatPrint(self:GetName() .. ": " .. message)
    end
end

-- Function to broadcast a message to all players
function ENT:BroadcastMessage(message)
    self:SayToWorld(message)
end

-- Function to handle errors gracefully
function ENT:HandleError(errorMessage)
    -- Print to server console
    print("[NPC Error] " .. errorMessage)
    
    -- Send error message to all players
    local players = player.GetAll()
    for _, ply in ipairs(players) do
        if IsValid(ply) and ply:IsPlayer() then
            ply:ChatPrint(self:GetName() .. ": [Error] " .. errorMessage)
        end
    end
end

-- Function triggered when a player uses the NPC
function ENT:Use(activator, caller)
    if activator:IsPlayer() then
        self:SayToPlayer(activator, "Hello, " .. activator:Nick() .. "!")
    end
end