Yes, I will provide better snippets
The breakpoints are indeed enabled (except there are some disabled breakpoints but those are not in use currently), the 2nd snapshots shows the config of each breakpoint. They’re all on ‘ALL CONTEXTS’ and no other conditions. I attempted to set contexts only to Client and Edit but that didn’t change anything so I changed it back to All.
To answer the last question, I just tried after you posted it. I created a 2nd module script with the same kind of layout :

I added this function call to the Villager Client Script:
Works perfectly fine. Very strange. So the problem has to be with my module script Villager AI? I cannot find the difference. I will post it now:
--!strict
local ai = {}
local mt = {}
local Types = require(workspace.Utils.Types)
local main = require(workspace.Utils.Game)
local Pathfinder = require(workspace.Utils.Pathfinder)
local Timer = require(workspace.Utils.Timer)
local model = script.Parent
local humanoid = model.Humanoid
local entityId = model:GetAttribute("EntID")
local entity : Types.Villager= main.GetEntityById(entityId)
local state = "Idle"
local target = nil
local treeHarvestSpeed = 0.2
local buildSpeed = 1
local baseWalkSpeed = 16
local taskName : string? = nil
local taskPriority : number? = nil
local taskArgs : { [string] : any }? = nil
local taskProgress : number? | string? = nil
local pathAgent : Types.Pathfinder = Pathfinder.new(entity, {
AgentRadius = 4,
AgentHeight = 9,
AgentCanJump = true,
AgentCanClimb = false,
Costs = {
Cobblestone = 0.25
}
})
local PathfindingService = game:GetService("PathfindingService")
local Players = game:GetService("Players")
local RunService = game:GetService("RunService")
local TraversedPath = Instance.new("BindableEvent")
local cleanup = {}
local timer : number? = nil
local canEat = true
local eatTimer = Timer.new(3, { OnElapsed = function() canEat = true end })
local actionTimer : {}? = nil
ai.TaskFinished = Instance.new("BindableEvent")
local function getEntryPoint(building : Types.Entity)
local size = building.Inst:GetExtentsSize()
return building.Inst:GetPivot():ToWorldSpace(CFrame.new(size.X / 2 + 2, 0, 0)).Position
end
local function findEntity(name : string) : Types.Entity?
local list = main.GetEntitiesSortedDistance(name, model:GetPivot().Position)
if list[1] then
return list[1][1]
end
return nil
end
local function targetDeltaPosition()
return (model:GetPivot().Position - target.Inst:GetPivot().Position).Magnitude
end
local function ListenEventOnce(event: BindableEvent, func : (...any) -> ())
local conn = event.Event:Once(func)
table.insert(cleanup, conn)
return conn
end
function SetState(_state : string)
state = _state
if state == "Idle" or state == "" then
if actionTimer then
actionTimer:Stop(true)
actionTimer = nil
end
end
if state == "" then
taskName = nil
taskPriority = nil
taskProgress = nil
taskArgs = nil
target = nil
timer = nil
--ResetPath()
pathAgent:CancelPath()
for _, v in ipairs(cleanup) do
v:Disconnect()
end
table.clear(cleanup)
end
end
function ai.GetState()
return state
end
function ai.GetTask()
return taskName, taskPriority, taskProgress, taskArgs
end
function ai.GetEntity()
return entity
end
function ai.GetTaskPriority()
return taskPriority
end
function ai.RunTask(name : string, priority : number, args : {}) : (boolean, string?)
if state == "Task" then
if taskPriority > priority then
return false, "LOW_PRIORITY"
else
--Cancel logic
SetState("Idle")
end
end
taskName = name
taskPriority = priority
taskProgress = 0
taskArgs = args
print("Villager " .. entityId .. " has new task " .. taskName :: string)
return true
end
local function CalculateTask()
if entity.Inventory:GetWeight() > 0 then
ai.RunTask("StoreItems", 100, {})
return
end
if canEat and entity.Hunger < 50 then
ai.RunTask("Eat", 100, {})
eatTimer:Start()
canEat = false
return
end
end
function ai.EndTask()
SetState("")
end
local function FinishTask()
ai.TaskFinished:Fire(taskName)
print("Finished " .. taskName .. " task")
SetState("")
end
function ai.OnOccupationChange(oldOccupation : string, newOccupation : string)
if entity.Inventory:GetWeight() > 0 then
ai.RunTask("StoreItems", 100, {})
else
ai.EndTask()
end
print("Villager " .. entityId .. " reassigned from " .. oldOccupation .. " to " .. newOccupation)
end
--Is the AI available/willing to work?
function ai.AvailableWork()
return state == "Idle" and not taskName
end
local function OnPathSuccess(success : boolean, status : string?)
if typeof(taskProgress) == "number" then
taskProgress += 1
end
end
local function consumeFood(name : string)
local amountFilled = 20
local amountToEat = math.min(entity.PersonalInventory:GetItemAmount(name), math.ceil((100 - entity.Hunger) / amountFilled) )
if amountToEat > 0 then
entity.PersonalInventory:RemoveItem(name, amountToEat)
entity.Hunger += math.min(100, amountToEat * amountFilled)
end
end
local function buildStructure(buildSite : Types.Entity)
local queue : Types.BuildQueueEntry = buildSite.BuildQueue
if queue.Stage == "Build" then
queue.BuildProgress += 1
if queue.BuildProgress >= queue.MaxBuildProgress then
main.buildQueue:Remove(queue)
main.DestroyEntity(buildSite)
main.BuildBuilding(queue.BuildName, queue.Origin, queue.Size)
FinishTask()
end
end
end
local function purchaseMarket(market : Types.WorkBuilding, itemName : string, quantity : number) : boolean
local ppu = main.itemSettings[itemName].Price
local totalPrice = ppu * quantity
if entity.Gold >= totalPrice then
entity.Gold -= totalPrice
main.IncreaseGold(totalPrice)
market.Inventory:TransferItem(itemName, quantity, entity.PersonalInventory)
--Adjust item stocks for market price calcs
main.itemStocks[itemName] -= quantity
print("Entity " .. entityId .. " purchased " .. itemName .. " x " .. quantity .. " for PPU: " .. ppu)
return true
end
return false
end
function ai.Run()
--print("Hello " .. villager.Name .. " ent ID: " .. entityId)
entity.Hunger = 70
local heartbeat = RunService.Heartbeat:Connect(function(deltaTime: number)
local floor = humanoid.FloorMaterial
local walkSpeedMofifier = 1.0
if floor == Enum.Material.Cobblestone then
walkSpeedMofifier += 1.0
end
--walkSpeedMofifier -= 0.5 * (entity.Inventory:GetWeight() / entity.CarryWeight)
humanoid.WalkSpeed = baseWalkSpeed * walkSpeedMofifier * main.GetGameSpeed()
local deltaHunger = if main.GetGameRunning() then 0.5 * deltaTime * main.GetGameSpeed() else 0
entity.Hunger = math.max(0, entity.Hunger - deltaHunger)
end)
while task.wait() do
if state == "" then
CalculateTask()
if taskName then
SetState("Task")
else
SetState("Idle")
end
elseif state == "Idle" then
--Try to resolve a task
if taskName then
state = "Task"
--print("Switching to Task state")
continue
end
CalculateTask()
elseif state == "Task" then
--Should always be the case. Repressing Luau type checker from crying about it
if not taskArgs or not taskProgress then continue end
if taskName == "GatherResource" then
if taskProgress == 0 then
--Walk to the resource
if not pathAgent:HasMoveCommand() then
local cframe :CFrame = taskArgs.Target.Inst:GetPivot()
local pos = cframe:ToWorldSpace(CFrame.new(taskArgs.Target.Inst:GetExtentsSize().X / 2 + 4, 0, 0)).Position
pathAgent:FollowPath(pos, function(success : boolean, status : string?)
if success then
taskProgress += 1
else
print("Could not traverse path " .. status)
end
end)
end
elseif taskProgress == 1 then
if not actionTimer then
local function OnElapsed()
--Calculate what resource we're taking
local res = nil
for n, v in pairs(taskArgs.Target.Inventory.Items) do
if v == 0 then continue end
res = n
break
end
local didTransfer = taskArgs.Target.Inventory:TransferItem(res, 1, entity.Inventory)
local invFull = entity.Inventory:GetWeight() >= entity.CarryWeight
if didTransfer then
main.itemStocks[res] += 1
end
if (not didTransfer) or (didTransfer and invFull) then
actionTimer:Stop(true)
actionTimer = nil
FinishTask()
end
end
actionTimer = Timer.new(treeHarvestSpeed * entity.WorkSpeed, { OnElapsed = OnElapsed, Looped = true})
actionTimer:Start()
end
end
elseif taskName == "StoreItems" then
if taskProgress == 0 then
if not pathAgent:HasMoveCommand() then
local _target = taskArgs.Target or findEntity("Stockyard")
if not _target then
continue
end
target = _target
pathAgent:PathToBuilding(target, function(success: boolean, status: string?)
if success then
taskProgress += 1
end
end)
end
elseif taskProgress == 1 then
--Give gold for their work
local gold = 0
for n, v in pairs(entity.Inventory.Items) do
gold += main.itemSettings[n].EqPrice * v
end
entity.Gold += gold
main.DecreaseGold(gold)
entity.Inventory:TransferAllItems(target.Inventory :: Types.Inventory)
FinishTask()
continue
end
elseif taskName == "StockBuilding" then
local workplace = entity.Workplace :: Types.WorkBuilding
if taskProgress == 0 then
if not pathAgent:HasMoveCommand() then
pathAgent:PathToBuilding(taskArgs.Target, function(success: boolean, status: string?)
if success then
taskProgress += 1
end
end)
end
elseif taskProgress == 1 then
if taskArgs.Target.Inventory:TransferItem(taskArgs.Resource, taskArgs.Quantity, entity.Inventory) then
taskProgress += 1
end
elseif taskProgress == 2 then
if not pathAgent:HasMoveCommand() then
pathAgent:PathToBuilding(workplace, function(success: boolean, status: string?)
if success then
taskProgress += 1
end
end)
end
elseif taskProgress == 3 then
if entity.Inventory:TransferItem(taskArgs.Resource, taskArgs.Quantity, workplace.Inventory) then
--Remove from overall item stocks (for correct market price calculations)
if workplace.Inst.Name ~= "Marketplace" then
main.itemStocks[taskArgs.Resource] -= taskArgs.Quantity
end
FinishTask()
continue
end
end
elseif taskName == "StockBuildSite" then
local buildSite : Types.Entity = taskArgs.BuildSite
local storage : Types.Entity = taskArgs.Storage
local resource : string = taskArgs.Resource
local quantity : number = taskArgs.Quantity
if taskProgress == 0 then
if not pathAgent:HasMoveCommand() then
pathAgent:PathToBuilding(storage, function(success: boolean, status: string?)
if success then
taskProgress += 1
end
end)
end
elseif taskProgress == 1 then
if storage.Inventory:TransferItem(resource, quantity, entity.Inventory) then
taskProgress += 1
end
elseif taskProgress == 2 then
if not pathAgent:HasMoveCommand() then
pathAgent:PathToBuilding(buildSite, function(success: boolean, status: string?)
if success then
taskProgress += 1
end
end)
end
elseif taskProgress == 3 then
if entity.Inventory:TransferItem(resource, quantity, buildSite.Inventory) then
FinishTask()
continue
end
end
elseif taskName == "ProduceProduct" then
local recipe : Types.Recipe = taskArgs.Recipe
local workplace = entity.Workplace :: Types.WorkBuilding
if taskProgress == 0 then
if entity.ResidingBuilding == workplace then
taskProgress += 1
continue
end
if not pathAgent:HasMoveCommand() then
pathAgent:PathToBuilding(workplace, function(success: boolean, status: string?)
if success then
taskProgress += 1
end
end)
end
elseif taskProgress == 1 then
local stockedWell : boolean = true
for _, v in ipairs(recipe.Ingridients) do
local myAmount = entity.Inventory:GetItemAmount(v.Item)
if myAmount < v.Quantity then
if not workplace.Inventory:TransferItem(v.Item, v.Quantity - myAmount, entity.Inventory) then
stockedWell = false
continue
end
end
end
--TODO: FinishTask() with an error
if not stockedWell then
warn("Could not finish ProduceProduct task because not enough stock!")
FinishTask()
continue
end
--Fully stocked to work
taskProgress += 1
elseif taskProgress == 2 then
if not actionTimer then
local function OnElapsed()
for _, ing in ipairs(recipe.Ingridients) do
entity.Inventory:RemoveItem(ing.Item, ing.Quantity)
end
entity.Inventory:AddItem(recipe.Product, recipe.ProductAmount)
actionTimer = nil
FinishTask()
end
actionTimer = Timer.new(treeHarvestSpeed * entity.WorkSpeed, { OnElapsed = OnElapsed })
actionTimer:Start()
end
--if not timer then
-- timer = tick()
-- continue
--elseif tick() - timer > (3) then
-- timer = nil
-- for _, ing in ipairs(recipe.Ingridients) do
-- entity.Inventory:RemoveItem(ing.Item, ing.Quantity)
-- end
-- entity.Inventory:AddItem(recipe.Product, recipe.ProductAmount)
-- FinishTask()
-- continue
--end
end
elseif taskName == "BuildStructure" then
local structure : Types.Entity? = taskArgs.Target:Resolve()
--Cancel if the build site is gone (completed by other builders or canceled)
if not structure then
print("NO STRUCT")
FinishTask()
continue
end
if taskProgress == 0 then
if not pathAgent:HasMoveCommand() then
pathAgent:FollowPath(structure.Inst:GetPivot().Position, OnPathSuccess)
end
elseif taskProgress == 1 then
if not actionTimer then
local function OnElapsed()
print("BUild taskname: " .. taskName)
buildStructure(structure)
end
actionTimer = Timer.new(buildSpeed * entity.WorkSpeed, { OnElapsed = OnElapsed, Looped = true })
actionTimer:Start()
end
end
elseif taskName == "Eat" then
if taskProgress == 0 then
if entity.PersonalInventory:GetItemAmount("Berry") > 0 then
consumeFood("Berry")
if entity.Hunger > 90 then
FinishTask()
continue
end
end
--Go to store
for _, v : Types.Entity in ipairs(main.GetEntities()) do
if v.Type == "Building" and v.Inst.Name == "Marketplace" then
local building : Types.WorkBuilding = v :: Types.WorkBuilding
if building.Inventory:GetItemAmount("Berry") > 0 then
target = building
taskProgress = "GotoMarket"
break
end
end
end
if not target then
FinishTask()
continue
end
elseif taskProgress == "GotoMarket" then
if entity.ResidingBuilding == target then
taskProgress = "ShopMarket"
continue
end
if not pathAgent:HasMoveCommand() then
pathAgent:PathToBuilding(target, function(success: boolean, status: string?)
if success then
taskProgress = "ShopMarket"
end
end)
end
elseif taskProgress == "ShopMarket" then
local stock = target.Inventory:GetItemAmount("Berry")
local amountFilled = 20
local amountToEat = math.min(stock, math.ceil((100 - entity.Hunger) / amountFilled) )
local pricePerUnit = main.itemSettings["Berry"].Price
local amountToBuy = math.min(amountToEat, math.floor(entity.Gold / pricePerUnit))
if amountToBuy > 0 then
if purchaseMarket(target, "Berry", amountToBuy) then
taskProgress = 0
end
end
end
end
end
end
--Not running the AI anymore
heartbeat:Disconnect()
end
print("Villager AI!")
return ai