AUTO-FARM+ARMY, uses AI Intelligence [READ DESCRIPTION V1.0]
MEDIEVAL RTS [⚔️BIGGER ARMIES]Description
Kingdom Commander Version 1.0 (Open-Source code) Run this immediately at the start of a match and let it do its thing, this is version 1.0 so it has a lot of issues but if you let it run, it solo-wins against regular Medieval RTS AI in under 15 minutes. If the AI makes a mistake, like not putting a house down, or a windmill down, it will figure it out in a little bit. This is not optimized to work well on maps involving using transport ships (islands) yet. Automate your kingdom. Build your economy. Command your army. Kingdom Commander V9 is an automated strategy assistant for Roblox Medieval RTS, designed to manage your kingdom's economy, construction, unit recruitment, and military commands. The script includes a clean, collapsible interface with simple controls, making it convenient to use on mobile and PC. Features Automated Economy Automatically recruits and manages Builders. Sends Builders to unoccupied farms across the map. Constructs Windmills to expand your economy. Attempts alternative construction positions when placement is obstructed. Continues expanding your economy throughout the match. Smart Construction Prioritizes Barracks after 3 completed Windmills. Automatically constructs Houses as population grows. Attempts to place Houses near your existing military infrastructure. Manages construction alongside army recruitment. Automatic Army Recruitment Trains Knights and Crossbowers. Maintains a target composition of 60% Knights and 40% Crossbowers. Continues producing troops as resources and population allow. Army Commands ATTACK CASTLE: Orders your army toward the enemy Castle. DEFEND: Recalls your army toward your own Castle. AUTO ATTACK: Automatically initiates an attack on the enemy castle when your population reaches 88 units (unless you press STOP). STOP: Stops further automation. Clean Interface Collapsible GUI with minimize and expand controls. Compact status display. Mobile-friendly buttons. Designed for straightforward operation.
Script source Luau
-- [[ BloxScripts Risk Notice ]]
-- This script has not been verified by BloxScripts.fun. Deal with caution.
--
-- Stay safe:
-- • Never log in on unofficial Roblox sites or lookalike domains.
-- • Real Roblox links use roblox.com (check the .com ending).
-- • Treat fake Roblox login / "claim reward" pages as phishing.
-- [[ End BloxScripts Risk Notice ]]
local Players = game:GetService("Players")
local RS = game:GetService("ReplicatedStorage")
local RunService = game:GetService("RunService")
local player = Players.LocalPlayer
local mine = workspace.Game.PlayerFolder[player.Name]
local units = mine.Units
local buildings = mine.Buildings
local supplies = workspace.Game.Map.Map.Supplies
local stats = mine.Stats
local Spawn = RS:WaitForChild("Spawn")
local Move = RS:WaitForChild("SendUnitGoals")
local Place = RS:WaitForChild("PlacementEvent")
local Cash = RS:WaitForChild("GetInfoCash")
local Info = require(RS.Utilities.ObjectStats)
local CFG = {
Builders = 3,
FarmWorkers = 3,
OpeningWindmills = 3,
HouseFreeSpaceBuffer = 12,
HomeRadiusMin = 30,
HomeRadiusMax = 85,
FarmFoundationWait = 7,
FarmPositionRetries = 9,
StuckTravelSeconds = 50,
FarmTravelTimeout = 240,
ConstructionTimeout = 240,
AppearTimeout = 28,
FarmRetry = 90,
HomeRetry = 4,
FarmFundsReserve = 0,
ArmyFundsReserve = 60,
MinimumThreatMargin = 18,
CastleDangerRadius = 95,
TowerDangerRadius = 68,
RallyBackoff = 25,
DefenseRadius = 95,
ForwardDefenseRadius = 60,
EngagementDistance = 48,
OrderCooldown = 15,
HousesLimit = 20,
HousePlacementAttempts = 96,
HomeStationLimit = 7,
HomeProbeWait = 2.0,
HomeProbeRadius = 6.0,
HomeTravelTimeout = 21,
HomeImmediateRadius = 92,
CastleHomeRadius = 84,
GroundHeightTolerance = 2.5,
FirstHouses = 3,
FirstHouseAtPopulation = 25,
HouseBarracksRadius = 52,
AutoAttackPopulation = 88,
HutsLimit = 9,
UpdateSeconds = 5,
}
local active = true
local reservedFarms = {}
local farmRetryAt = {}
local farmerTasks = {}
local farmStatus = {}
local farmBusy = {}
local homeWorker = nil
local homeRequested = false
local pendingHome = {}
local homeQueue = {}
local homeBusy = false
local homeCooldown = {}
local performHomeJob
local pendingRecruit = {Builder = 0, Knight = 0, Crossbower = 0}
local recentCounts = {}
local recentQueueTime = os.clock()
local lastRequest = 0
local localPlacementStep = 0
local defenderOrders = {}
local defenderHealth = {}
local routedOrders = {}
local errors = {}
local assaultMode = false
local manualDefense = false
local nextDefenseRecall = 0
local defenderReturnOrders = {}
local assaultTarget = nil
local assaultOrdered = {}
local lastAssaultSweep = 0
local autoAttackTriggered = false
local previous = player.PlayerGui:FindFirstChild("MedievalAutoCommander")
if previous then previous:Destroy() end
local gui = Instance.new("ScreenGui")
gui.Name = "MedievalAutoCommander"
gui.ResetOnSpawn = false
gui.Parent = player.PlayerGui
local panel = Instance.new("Frame")
panel.Name = "CommanderPanel"
panel.Size = UDim2.new(0, 340, 0, 210)
panel.Position = UDim2.new(0.03, 0, 0.10, 0)
panel.BackgroundColor3 = Color3.fromRGB(22, 27, 37)
panel.BorderSizePixel = 0
panel.ClipsDescendants = true
panel.Parent = gui
local corner = Instance.new("UICorner")
corner.CornerRadius = UDim.new(0, 12)
corner.Parent = panel
local border = Instance.new("UIStroke")
border.Color = Color3.fromRGB(65, 76, 96)
border.Thickness = 1
border.Transparency = 0.2
border.Parent = panel
local header = Instance.new("Frame")
header.Name = "Header"
header.Size = UDim2.new(1, 0, 0, 43)
header.BackgroundColor3 = Color3.fromRGB(32, 42, 57)
header.BorderSizePixel = 0
header.Parent = panel
local headerCorner = Instance.new("UICorner")
headerCorner.CornerRadius = UDim.new(0, 12)
headerCorner.Parent = header
local brand = Instance.new("TextLabel")
brand.Size = UDim2.new(1, -110, 0, 23)
brand.Position = UDim2.new(0, 12, 0, 5)
brand.BackgroundTransparency = 1
brand.Text = "MEDIEVAL COMMANDER"
brand.TextColor3 = Color3.fromRGB(235, 240, 250)
brand.TextSize = 16
brand.Font = Enum.Font.GothamBold
brand.TextXAlignment = Enum.TextXAlignment.Left
brand.Parent = header
local subtitle = Instance.new("TextLabel")
subtitle.Size = UDim2.new(1, -110, 0, 14)
subtitle.Position = UDim2.new(0, 12, 0, 26)
subtitle.BackgroundTransparency = 1
subtitle.Text = "V9 / AUTO DEFENSE"
subtitle.TextColor3 = Color3.fromRGB(130, 195, 175)
subtitle.TextSize = 10
subtitle.Font = Enum.Font.GothamMedium
subtitle.TextXAlignment = Enum.TextXAlignment.Left
subtitle.Parent = header
local minimize = Instance.new("TextButton")
minimize.Name = "Minimize"
minimize.Size = UDim2.new(0, 37, 0, 31)
minimize.Position = UDim2.new(1, -44, 0, 6)
minimize.BackgroundColor3 = Color3.fromRGB(58, 69, 88)
minimize.TextColor3 = Color3.new(1, 1, 1)
minimize.TextSize = 24
minimize.Font = Enum.Font.GothamBold
minimize.Text = "−"
minimize.Parent = header
local minCorner = Instance.new("UICorner")
minCorner.CornerRadius = UDim.new(0, 7)
minCorner.Parent = minimize
local content = Instance.new("Frame")
content.Name = "Content"
content.Position = UDim2.new(0, 0, 0, 43)
content.Size = UDim2.new(1, 0, 1, -43)
content.BackgroundTransparency = 1
content.Parent = panel
local title = Instance.new("TextLabel")
title.Name = "CommanderStatus"
title.Position = UDim2.new(0, 12, 0, 8)
title.Size = UDim2.new(1, -24, 0, 29)
title.BackgroundTransparency = 1
title.TextColor3 = Color3.fromRGB(244, 247, 252)
title.TextSize = 13
title.Font = Enum.Font.GothamSemibold
title.TextWrapped = true
title.TextXAlignment = Enum.TextXAlignment.Left
title.Text = "DEFENSIVE COMMANDER V9"
title.Parent = content
local detail = Instance.new("TextLabel")
detail.Name = "EmpireStats"
detail.Position = UDim2.new(0, 12, 0, 39)
detail.Size = UDim2.new(1, -24, 0, 35)
detail.BackgroundColor3 = Color3.fromRGB(32, 41, 55)
detail.TextColor3 = Color3.fromRGB(159, 231, 202)
detail.TextSize = 11
detail.Font = Enum.Font.GothamMedium
detail.TextWrapped = true
detail.TextXAlignment = Enum.TextXAlignment.Center
detail.Text = "Starting..."
detail.Parent = content
local statsCorner = Instance.new("UICorner")
statsCorner.CornerRadius = UDim.new(0, 6)
statsCorner.Parent = detail
local workersLabel = Instance.new("TextLabel")
workersLabel.Name = "WorkerTasks"
workersLabel.Position = UDim2.new(0, 12, 0, 78)
workersLabel.Size = UDim2.new(1, -24, 0, 29)
workersLabel.BackgroundTransparency = 1
workersLabel.TextColor3 = Color3.fromRGB(238, 205, 137)
workersLabel.TextSize = 11
workersLabel.Font = Enum.Font.Gotham
workersLabel.TextWrapped = true
workersLabel.TextXAlignment = Enum.TextXAlignment.Left
workersLabel.Text = "BUILDER TASKS STARTING"
workersLabel.Parent = content
local function styleButton(button, color)
button.BackgroundColor3 = color
button.TextColor3 = Color3.fromRGB(255, 255, 255)
button.TextSize = 12
button.Font = Enum.Font.GothamBold
button.BorderSizePixel = 0
local radius = Instance.new("UICorner")
radius.CornerRadius = UDim.new(0, 7)
radius.Parent = button
end
local attackButton = Instance.new("TextButton")
attackButton.Name = "AttackCastle"
attackButton.Position = UDim2.new(0, 12, 0, 119)
attackButton.Size = UDim2.new(0.42, -9, 0, 37)
attackButton.Text = "ATTACK CASTLE"
styleButton(attackButton, Color3.fromRGB(164, 85, 51))
attackButton.Parent = content
local defendButton = Instance.new("TextButton")
defendButton.Name = "Defend"
defendButton.Position = UDim2.new(0.42, 7, 0, 119)
defendButton.Size = UDim2.new(0.32, -7, 0, 37)
defendButton.Text = "DEFEND"
styleButton(defendButton, Color3.fromRGB(39, 133, 103))
defendButton.Parent = content
local stop = Instance.new("TextButton")
stop.Name = "Stop"
stop.Position = UDim2.new(0.74, 4, 0, 119)
stop.Size = UDim2.new(0.26, -16, 0, 37)
stop.Text = "STOP"
styleButton(stop, Color3.fromRGB(134, 49, 62))
stop.Parent = content
local minimized = false
minimize.MouseButton1Click:Connect(function()
minimized = not minimized
content.Visible = not minimized
panel.Size = minimized and UDim2.new(0, 340, 0, 43)
or UDim2.new(0, 340, 0, 210)
minimize.Text = minimized and "+" or "−"
end)
local function status(message)
if active and gui.Parent then title.Text = message end
print("[DEFENSIVE V9]", message)
end
local function reportError(taskName, err)
errors[taskName] = tostring(err)
warn("[DEFENSIVE V9] " .. taskName, err)
status(taskName .. " ERROR: " .. tostring(err))
end
stop.MouseButton1Click:Connect(function()
active = false
title.Text = "COMMANDER STOPPED"
detail.Text = "No further automated commands"
end)
gui.Destroying:Connect(function() active = false end)
local function alive(obj)
if not obj or not obj.Parent or obj:GetAttribute("Destroyed") == true then return false end
local hp = obj:GetAttribute("Health")
return type(hp) ~= "number" or hp > 0
end
local function pos(obj)
if not obj or not obj.Parent then return nil end
local hit = obj:FindFirstChild("Hitbox", true)
if hit and hit:IsA("BasePart") then return hit.Position end
if obj:IsA("BasePart") then return obj.Position end
if obj:IsA("Model") then return obj:GetPivot().Position end
return nil
end
local function flatDistance(a, b)
local x, z = a.X - b.X, a.Z - b.Z
return math.sqrt(x * x + z * z)
end
local function primary(obj)
if not obj then return nil end
if obj:IsA("BasePart") then return obj end
return obj.PrimaryPart or obj:FindFirstChild("Hitbox", true)
or obj:FindFirstChildWhichIsA("BasePart", true)
end
local function cash()
local ok, n = pcall(function() return Cash:Invoke() end)
return ok and tonumber(n) or nil
end
local function cost(name)
return Info[name] and tonumber(Info[name].Cost) or math.huge
end
local function space(name)
return Info[name] and tonumber(Info[name].Space) or 0
end
local function models(folder, name)
local result = {}
if not folder then return result end
for _, obj in ipairs(folder:GetChildren()) do
if obj:IsA("Model") and alive(obj) and (not name or obj.Name == name) then
table.insert(result, obj)
end
end
return result
end
local function built(obj)
if not alive(obj) then return false end
local value = obj:GetAttribute("Built")
if type(value) == "boolean" then return value end
local flag = obj:FindFirstChild("Built", true)
if flag and flag:IsA("BoolValue") then return flag.Value end
return nil
end
local function completedCount(name)
local n = 0
for _, b in ipairs(models(buildings, name)) do
if built(b) == true then n = n + 1 end
end
return n
end
local function findBuilding(name, requireBuilt)
for _, b in ipairs(models(buildings, name)) do
if not requireBuilt or built(b) == true then return b end
end
return nil
end
local castle = findBuilding("Castle")
if not castle then status("CASTLE NOT FOUND"); return end
local castlePart = primary(castle)
local castleAt = pos(castle)
if not castlePart or not castleAt then status("CASTLE POSITION MISSING"); return end
local function builders()
return models(units, "Builder")
end
local function friendlyWindmills()
local owned = {}
-- Ownership is established by parent folder, never by a farm's Occupied attribute.
for _, mill in ipairs(models(buildings, "Windmill")) do
if mill:IsDescendantOf(buildings) and built(mill) == true then
table.insert(owned, mill)
end
end
return owned
end
local function sendMovement(batch)
if not active or #batch == 0 then return false end
local ok, result = pcall(function()
return Move:InvokeServer(batch, false, true)
end)
if not ok then warn("Move failed", result); return false end
return result ~= false
end
local function moveOne(unit, destination)
if not alive(unit) or not destination then return false end
return sendMovement({{unit, destination}})
end
local function arrive(worker, point, timeout, check, interrupt)
local since = os.clock()
local lastOrder = -math.huge
local lastProgress = os.clock()
local best = math.huge
while active and alive(worker) and os.clock() - since < timeout do
if check and not check() then return false end
if interrupt and interrupt() then return false end
local here = pos(worker)
if here then
local range = tonumber(worker:GetAttribute("Range")) or 8
local distance = flatDistance(here, point)
if distance <= math.max(2.5, range - 1.5) then
return true
end
if distance < best - 3 then
best = distance
lastProgress = os.clock()
end
if os.clock() - lastProgress > CFG.StuckTravelSeconds then
return false
end
end
if os.clock() - lastOrder >= 15 then
moveOne(worker, point)
lastOrder = os.clock()
end
task.wait(0.7)
end
return false
end
local ray = RaycastParams.new()
ray.FilterType = Enum.RaycastFilterType.Exclude
ray.FilterDescendantsInstances = {workspace.Game.PlayerFolder, supplies}
ray.IgnoreWater = true
local function groundPoint(point, buildingName)
local templates = RS.Buildings:FindFirstChild("Default")
local template = templates and templates:FindFirstChild(buildingName)
if not template or not template:IsA("Model") then return nil end
local root = primary(template)
if not root then return nil end
local hit = workspace:Raycast(
Vector3.new(point.X, point.Y + 180, point.Z),
Vector3.new(0, -450, 0), ray)
if not hit then return nil end
return Vector3.new(hit.Position.X, hit.Position.Y + root.Size.Y / 2 + 0.02, hit.Position.Z)
end
local function buildingNear(name, where, radius, except)
local best, bestDist = nil, radius
for _, b in ipairs(models(buildings, name)) do
local at = pos(b)
if at and (not except or not except[b]) then
local dist = flatDistance(at, where)
if dist < bestDist then best, bestDist = b, dist end
end
end
return best
end
local function waitUntilBuilt(constructed, worker, label, initialCapacity)
local started = os.clock()
local lastReposition = 0
while active and alive(worker) and alive(constructed)
and os.clock() - started < CFG.ConstructionTimeout do
if built(constructed) == true then
return true, constructed, "BUILT"
end
-- Some buildings may be marked complete by the capacity increase
-- even if they have no replicated Built flag.
if label == "House" and initialCapacity
and stats.MaxUnits.Value > initialCapacity then
return true, constructed, "POPULATION_CAP_INCREASED"
end
if label == "Builder Hut" and initialCapacity
and stats.MaxBuildings.Value > initialCapacity then
return true, constructed, "BUILDING_CAP_INCREASED"
end
-- If the worker is displaced during construction, return it to
-- the unfinished foundation. Never switch to another farm midway.
local workerAt = pos(worker)
local buildingAt = pos(constructed)
if workerAt and buildingAt then
local range = tonumber(worker:GetAttribute("Range")) or 8
if flatDistance(workerAt, buildingAt) > math.max(3, range - 1)
and os.clock() - lastReposition > 12 then
moveOne(worker, buildingAt)
lastReposition = os.clock()
end
end
task.wait(1)
end
status(label .. " | FOUNDATION STILL INCOMPLETE")
return false, constructed, "INCOMPLETE"
end
local function placeAndComplete(name, point, farm, worker, label, appearanceWait)
if not active or not alive(worker) then return false, nil, "UNAVAILABLE" end
if farm and (not farm.Parent or farm:GetAttribute("Occupied") ~= false) then
return false, nil, "FARM_OCCUPIED"
end
local money = cash()
if money and money < cost(name) then return false, nil, "FUNDS" end
local before = {}
for _, b in ipairs(models(buildings, name)) do before[b] = true end
local initialCapacity = nil
if name == "House" then
initialCapacity = stats.MaxUnits.Value
elseif name == "Builder Hut" then
initialCapacity = stats.MaxBuildings.Value
end
local ok, err = pcall(function()
Place:FireServer(name, point, 0, nil, farm)
end)
if not ok then
warn("Placement error", err)
return false, nil, "REQUEST_ERROR"
end
local constructed = nil
local started = os.clock()
local window = appearanceWait or CFG.AppearTimeout
while active and os.clock() - started < window do
-- A server-confirmed new model in *our* Buildings folder is
-- necessary. Farm.Occupied alone never means we own it.
constructed = buildingNear(name, point, farm and 14 or 36, before)
if constructed then break end
if farm and (not farm.Parent or farm:GetAttribute("Occupied") == true) then
-- Give replication a brief grace period in case it is our model.
task.wait(1)
constructed = buildingNear(name, point, 14, before)
if not constructed then return false, nil, "CONTESTED" end
break
end
task.wait(0.5)
end
if not constructed then return false, nil, "NO_FOUNDATION" end
status(label .. " | FOUNDATION OWNED, WAITING FOR BUILT")
return waitUntilBuilt(constructed, worker, label, initialCapacity)
end
-- Try nearby points INSIDE the farm area if the default position is
-- obstructed. Do not issue another request once our foundation appears.
local farmOffsets = {
Vector3.new(0, 0, 0),
Vector3.new(3, 0, 0), Vector3.new(-3, 0, 0),
Vector3.new(0, 0, 3), Vector3.new(0, 0, -3),
Vector3.new(4, 0, 4), Vector3.new(-4, 0, 4),
Vector3.new(4, 0, -4), Vector3.new(-4, 0, -4),
}
local function tryFarmPositions(worker, farm, center, label)
for index = 1, math.min(#farmOffsets, CFG.FarmPositionRetries) do
if not active or not alive(worker) or not farm.Parent
or farm:GetAttribute("Occupied") ~= false then
return false, "CONTESTED"
end
local wanted = center + farmOffsets[index]
local point = groundPoint(wanted, "Windmill")
if point and flatDistance(point, center) <= 7.5 then
local here = pos(worker)
local range = tonumber(worker:GetAttribute("Range")) or 8
if here and flatDistance(here, point) > math.max(2.5, range - 1.5) then
if not arrive(worker, point, 42, function()
return farm.Parent ~= nil and farm:GetAttribute("Occupied") == false
end) then
return false, "CANNOT_APPROACH"
end
end
farmStatus[worker] = "FARM SITE " .. index .. "/" .. CFG.FarmPositionRetries
local success, constructed, reason = placeAndComplete(
"Windmill", point, farm, worker, label, CFG.FarmFoundationWait)
if success then return true, "BUILT" end
if constructed or reason == "INCOMPLETE" then
-- Never abandon or duplicate a partially built Windmill.
return false, "UNFINISHED_FOUNDATION"
end
if reason ~= "NO_FOUNDATION" then
return false, reason
end
-- Rejected/obstructed placement; try a different nearby point.
task.wait(0.7)
end
end
return false, "ALL_POSITIONS_BLOCKED"
end
local function nearestFreeFarm(worker)
local here = pos(worker)
if not here then return nil end
local options = {}
for _, farm in ipairs(supplies:GetChildren()) do
if farm:GetAttribute("Occupied") == false
and not reservedFarms[farm]
and (farmRetryAt[farm] or 0) <= os.clock() then
local point = pos(farm)
if point then
table.insert(options, {farm = farm, point = point,
dist = flatDistance(here, point)})
end
end
end
table.sort(options, function(a, b) return a.dist < b.dist end)
return options[1]
end
-- Each of the three workers runs independently. Home construction is
-- one temporary job taken between farms, never a global halt.
local function claimHomeJob(worker)
if homeBusy or #homeQueue == 0 then return nil end
local name = table.remove(homeQueue, 1)
if not name then return nil end
homeBusy = true
homeWorker = worker
return name
end
local function farmWorker(worker, index)
while active and alive(worker) do
local homeJob = claimHomeJob(worker)
if homeJob then
farmStatus[worker] = "HOME: " .. homeJob
-- performHomeJob is assigned below after construction helpers.
local ok, result = pcall(function() return performHomeJob(homeJob, worker) end)
if not ok then reportError("HOME " .. homeJob, result) end
pendingHome[homeJob] = nil
homeCooldown[homeJob] = os.clock() + ((ok and result) and 2 or 15)
homeBusy = false
homeWorker = nil
farmStatus[worker] = "HOME FINISHED"
task.wait(0.5)
else
farmStatus[worker] = "FINDING FARM"
local selection = nearestFreeFarm(worker)
if not selection then
farmStatus[worker] = "NO FREE FARMS"
task.wait(3)
else
local money = cash()
local buildingReserve = CFG.FarmFundsReserve
for needed in pairs(pendingHome) do
if needed == "House" or needed == "Builder Hut" or needed == "Barracks" then
buildingReserve = math.max(buildingReserve, cost(needed))
end
end
if money and money < cost("Windmill") + buildingReserve then
farmStatus[worker] = "SAVING MONEY FOR HOME BUILDING"
task.wait(3)
else
local farm = selection.farm
if not reservedFarms[farm] and farm:GetAttribute("Occupied") == false then
reservedFarms[farm] = worker
farmBusy[worker] = true
farmStatus[worker] = "TRAVELING TO FARM"
local function free()
return farm.Parent ~= nil
and farm:GetAttribute("Occupied") == false
end
local homeInterrupt = function()
-- Only peel off one farmer during TRAVEL when
-- an urgent home job is waiting. Never interrupt
-- a started Windmill foundation.
if #homeQueue == 0 or homeBusy then return false end
-- A completed three-farm opening needs a Barracks
-- immediately. Any traveling Builder may return;
-- never abandon an in-progress Windmill.
if pendingHome["Barracks"] then return true end
if index == 3 then return true end
-- At a full population cap, release a traveling
-- worker rather than waiting for a distant farm.
local freePop = stats.MaxUnits.Value
- stats.CurrentUnits.Value
return freePop <= 2
end
local reached = arrive(worker, selection.point,
CFG.FarmTravelTimeout, free, homeInterrupt)
local success, reason = false, "NOT_REACHED"
if not reached and homeInterrupt() then
reason = "HOME_PRIORITY"
end
if reached and active and alive(worker) and free() then
farmStatus[worker] = "TRYING WINDMILL SITES"
success, reason = tryFarmPositions(worker, farm,
selection.point, "FARMER " .. index)
end
if not success and reason ~= "HOME_PRIORITY" then
farmRetryAt[farm] = os.clock() + CFG.FarmRetry
end
reservedFarms[farm] = nil
farmBusy[worker] = nil
farmStatus[worker] = success and "WINDMILL COMPLETE"
or ("FARM SKIPPED: " .. tostring(reason))
task.wait(success and 0.5 or 1.5)
else
task.wait(0.5)
end
end
end
end
end
farmBusy[worker] = nil
farmerTasks[worker] = nil
if homeWorker == worker then homeWorker = nil; homeBusy = false end
for farm, assigned in pairs(reservedFarms) do
if assigned == worker then reservedFarms[farm] = nil end
end
end
local function assignFarmers()
local count = 0
for b in pairs(farmerTasks) do
if alive(b) then count = count + 1 else farmerTasks[b] = nil end
end
for _, b in ipairs(builders()) do
if count >= CFG.FarmWorkers then break end
if not farmerTasks[b] then
farmerTasks[b] = true
count = count + 1
local index = count
task.spawn(function()
local ok, err = pcall(farmWorker, b, index)
if not ok then reportError("FARMER " .. index, err) end
farmerTasks[b] = nil
end)
end
end
end
local function tryRecruit(name, building, reserve)
if not active or not alive(building) then return false end
if not Info[name] or os.clock() - lastRequest < 4.5 then return false end
local funds = cash()
if funds and funds < cost(name) + (reserve or 0) then return false end
local reservedPop = 0
for n, amount in pairs(pendingRecruit) do
reservedPop = reservedPop + amount * space(n)
end
if stats.CurrentUnits.Value + reservedPop + space(name) > stats.MaxUnits.Value then
return false
end
local root = primary(building)
if not root then return false end
local ok, err = pcall(function()
Spawn:FireServer(name, root)
end)
if not ok then warn("Recruitment error", err); return false end
pendingRecruit[name] = pendingRecruit[name] + 1
recentQueueTime = os.clock()
lastRequest = os.clock()
return true
end
local function updateRecruitQueue()
for name in pairs(pendingRecruit) do
local current = #models(units, name)
local previousCount = recentCounts[name] or current
local added = math.max(0, current - previousCount)
pendingRecruit[name] = math.max(0, pendingRecruit[name] - added)
recentCounts[name] = current
end
if os.clock() - recentQueueTime > 105 then
for name in pairs(pendingRecruit) do pendingRecruit[name] = 0 end
recentQueueTime = os.clock()
end
end
local function builderManager()
while active do
assignFarmers() -- First available Builder starts immediately.
updateRecruitQueue()
local count = #builders()
if count + pendingRecruit.Builder < CFG.Builders then
if tryRecruit("Builder", castle, 0) then
status("RECRUITING BUILDER " .. (count + pendingRecruit.Builder)
.. "/" .. CFG.Builders)
end
end
task.wait(2)
end
end
-- Barracks and houses are placed on flat ground near the home base.
-- Start with available space around the BUILDER; only move if its
-- immediate area fails, so one cliff corner cannot stall construction.
local function homeSites(name, worker)
local anchors = {}
local here = pos(worker)
local barracks = findBuilding("Barracks", false)
local barracksAt = barracks and pos(barracks)
local placingHouse = name == "House" and barracksAt ~= nil
if placingHouse then
-- For houses, build a compact neighborhood around the Barracks.
-- Do not prefer a distant builder's current position.
table.insert(anchors, {point = barracksAt, area = "BARRACKS", low = 13, high = 43})
if here and flatDistance(here, barracksAt) <= 25 then
table.insert(anchors, {point = here, area = "BUILDER", low = 0, high = 12})
end
if flatDistance(castleAt, barracksAt) < 48 then
table.insert(anchors, {point = castleAt, area = "CASTLE", low = 17, high = 45})
end
else
if here and flatDistance(here, castleAt) <= CFG.HomeImmediateRadius then
table.insert(anchors, {point = here, area = "BUILDER", low = 0, high = 9})
end
if barracksAt then
table.insert(anchors, {point = barracksAt, area = "BARRACKS", low = 13, high = 36})
end
table.insert(anchors, {point = castleAt, area = "CASTLE", low = 16, high = 57})
for _, mill in ipairs(friendlyWindmills()) do
local at = pos(mill)
if at and flatDistance(at, castleAt) < 60 then
table.insert(anchors, {point = at, area = "WINDMILL", low = 11, high = 23})
end
end
end
local buildingPositions = {}
for _, b in ipairs(models(buildings)) do
local at = pos(b)
if at then
local size = Vector3.new(12, 8, 12)
local ok, ext = pcall(function() return b:GetExtentsSize() end)
if ok and ext then size = ext end
table.insert(buildingPositions, {
point = at,
clearance = math.min(42, math.max(size.X, size.Z) * 0.45 + 3),
})
end
end
local sites, seen = {}, {}
for _, anchor in ipairs(anchors) do
for r = anchor.low, anchor.high, 5 do
for step = 0, 15 do
local angle = step * math.pi / 8 + (r % 3) * 0.12
local at = anchor.point + Vector3.new(
math.cos(angle) * r, 0, math.sin(angle) * r)
local withinZone = placingHouse
and flatDistance(at, barracksAt) <= CFG.HouseBarracksRadius
or (not placingHouse
and flatDistance(at, castleAt) <= CFG.CastleHomeRadius)
if withinZone then
local blocked = false
for _, b in ipairs(buildingPositions) do
if flatDistance(at, b.point) < b.clearance then
blocked = true
break
end
end
if not blocked then
local key = math.floor(at.X / 3) .. ':' .. math.floor(at.Z / 3)
if not seen[key] then
seen[key] = true
table.insert(sites, {
point = at,
area = anchor.area,
homeDist = flatDistance(at, castleAt),
barracksDist = barracksAt and flatDistance(at, barracksAt) or 0,
})
end
end
end
end
end
end
local priority = placingHouse
and {BARRACKS = 1, BUILDER = 2, CASTLE = 3}
or {BUILDER = 1, BARRACKS = 2, CASTLE = 3, WINDMILL = 4}
table.sort(sites, function(a, b)
local pa, pb = priority[a.area] or 10, priority[b.area] or 10
if pa ~= pb then return pa < pb end
local da = here and flatDistance(here, a.point) or 999
local db = here and flatDistance(here, b.point) or 999
if placingHouse then
return da * 0.45 + a.barracksDist * 0.55
< db * 0.45 + b.barracksDist * 0.55
end
return da + a.homeDist * 0.08 < db + b.homeDist * 0.08
end)
return sites
end
-- Include the builder's exact location plus short circles around it.
local homeProbeOffsets = {Vector3.new(0, 0, 0)}
for _, radius in ipairs({2.5, 4.5, 6.5, 8.5}) do
for step = 0, 11 do
local a = step * math.pi / 6
table.insert(homeProbeOffsets, Vector3.new(
math.cos(a) * radius, 0, math.sin(a) * radius))
end
end
local function flatBuildGround(point, name)
local candidate = groundPoint(point, name)
if not candidate then return nil end
local templateFolder = RS.Buildings:FindFirstChild("Default")
local template = templateFolder and templateFolder:FindFirstChild(name)
local width, depth = 12, 12
if template and template:IsA("Model") then
local ok, ext = pcall(function() return template:GetExtentsSize() end)
if ok and ext then
width = ext.X
depth = ext.Z
end
end
-- A very basic approximation of Roblox's green/red placement preview.
-- Reject cliff edges and tilted terrain before sending a placement.
local spreadX = math.max(2.5, math.min(7, width * 0.26))
local spreadZ = math.max(2.5, math.min(7, depth * 0.26))
local coords = {
Vector3.new(0,0,0),
Vector3.new(spreadX,0,spreadZ),
Vector3.new(-spreadX,0,spreadZ),
Vector3.new(spreadX,0,-spreadZ),
Vector3.new(-spreadX,0,-spreadZ),
}
local centerY = nil
for _, offset in ipairs(coords) do
local start = Vector3.new(candidate.X + offset.X,
candidate.Y + 110, candidate.Z + offset.Z)
local hit = workspace:Raycast(start, Vector3.new(0,-250,0), ray)
if not hit or hit.Normal.Y < 0.88 then return nil end
if not centerY then centerY = hit.Position.Y end
if math.abs(hit.Position.Y - centerY) > CFG.GroundHeightTolerance then
return nil
end
end
return candidate
end
local function enqueueHome(name, priority)
if pendingHome[name] or os.clock() < (homeCooldown[name] or 0) then
return false
end
local money = cash()
if money and money < cost(name) then
if name == "House" then
status("HOUSE NEEDED - WAITING FOR FUNDS")
end
return false
end
pendingHome[name] = true
if priority then
table.insert(homeQueue, 1, name)
else
table.insert(homeQueue, name)
end
status("QUEUED HOME BUILDING: " .. name)
return true
end
performHomeJob = function(name, worker)
local sites = homeSites(name, worker)
local attempts, stations = 0, 0
local checked = {}
local attemptedPoints = {}
local barracksForHouse = name == "House" and findBuilding("Barracks", false) or nil
local barracksHouseAt = barracksForHouse and pos(barracksForHouse)
local function withinHomeZone(point)
if name == "House" and barracksHouseAt then
return flatDistance(point, barracksHouseAt) <= CFG.HouseBarracksRadius
end
return flatDistance(point, castleAt) <= CFG.CastleHomeRadius
end
local function probeAround(center, label)
for _, offset in ipairs(homeProbeOffsets) do
if not active or not alive(worker)
or attempts >= CFG.HousePlacementAttempts then
return false, "STOPPED"
end
local candidate = flatBuildGround(center + offset, name)
local here = pos(worker)
local range = tonumber(worker:GetAttribute("Range")) or 8
if candidate and here
and flatDistance(here, candidate) <= math.max(3, range - 1.0)
and withinHomeZone(candidate) then
local key = math.floor(candidate.X * 2) .. ':' .. math.floor(candidate.Z * 2)
if not attemptedPoints[key] then
attemptedPoints[key] = true
attempts = attempts + 1
farmStatus[worker] = "HOME " .. name .. " TRY " .. attempts
status(name .. " | TRY " .. attempts .. " NEAR " .. label)
local ok, foundation, reason = placeAndComplete(
name, candidate, nil, worker, name, CFG.HomeProbeWait)
if ok then return true, "BUILT" end
if foundation or reason == "INCOMPLETE" then
-- Don't place duplicates once the server accepted a foundation.
return false, "FOUNDATION_UNFINISHED"
end
if reason == "FUNDS" or reason == "UNAVAILABLE" then
return false, reason
end
task.wait(0.1)
end
end
end
return false, "NO_VALID_POSITION"
end
-- First probe all reachable spots around the Builder, while stationary.
local here = pos(worker)
if here and (name ~= "House"
and flatDistance(here, castleAt) <= CFG.HomeImmediateRadius
or name == "House" and barracksHouseAt
and flatDistance(here, barracksHouseAt) <= 22) then
status(name .. " | SCANNING AROUND BUILDER")
local ok, reason = probeAround(here, "BUILDER")
if ok then return true end
if reason ~= "NO_VALID_POSITION" then return false end
end
-- If needed, move briefly to another flat site around the home base,
-- not to distant cliffs or fields. Avoid long 72s waits per area.
if #sites == 0 then
status(name .. " | NO ALTERNATIVE HOME GROUND")
return false
end
for i, site in ipairs(sites) do
if not active or not alive(worker)
or attempts >= CFG.HousePlacementAttempts
or stations >= CFG.HomeStationLimit then break end
if not checked[i] then
checked[i] = true
local goal = flatBuildGround(site.point, name)
if goal then
stations = stations + 1
farmStatus[worker] = "HOME " .. name .. " AREA " .. stations
local current = pos(worker)
local range = tonumber(worker:GetAttribute("Range")) or 8
local reached = current
and flatDistance(current, goal) <= math.max(3, range - 1)
if not reached then
status(name .. " | NEARBY AREA " .. stations .. " (" .. site.area .. ")")
reached = arrive(worker, goal, CFG.HomeTravelTimeout)
end
if reached then
-- Use the builder's NEW position as the placement center.
local arrivedAt = pos(worker)
if arrivedAt then
local ok, reason = probeAround(arrivedAt, site.area)
if ok then return true end
if reason ~= "NO_VALID_POSITION" then return false end
end
end
end
end
end
status(name .. " | " .. attempts .. " GROUND PROBES; RETRYING")
return false
end
local function homeManager()
while active do
local mills = completedCount("Windmill")
local usedPop = stats.CurrentUnits.Value
local maxPop = stats.MaxUnits.Value
local queuedPop = 0
for name, number in pairs(pendingRecruit) do
queuedPop = queuedPop + number * space(name)
end
local freeSpace = maxPop - usedPop - queuedPop
local houseCount = completedCount("House")
local hasBarracks = findBuilding("Barracks", false) ~= nil
local barracksReady = findBuilding("Barracks", true) ~= nil
local unfinishedHouse = false
local unfinishedHut = false
for _, house in ipairs(models(buildings, "House")) do
if built(house) ~= true then unfinishedHouse = true; break end
end
for _, hut in ipairs(models(buildings, "Builder Hut")) do
if built(hut) ~= true then unfinishedHut = true; break end
end
-- Always prioritize the Barracks immediately after THREE completed
-- Windmills (no wait for a fourth and no enemy-Barracks condition).
if mills >= 3 and not hasBarracks then
enqueueHome("Barracks", true)
end
local hutNeeded = stats.CurrentBuildings.Value
>= stats.MaxBuildings.Value - 2
and #models(buildings, "Builder Hut") < CFG.HutsLimit
if hutNeeded and not unfinishedHut then
local barracksBlockedByCap = stats.CurrentBuildings.Value
>= stats.MaxBuildings.Value
enqueueHome("Builder Hut", barracksBlockedByCap)
end
-- Wait for the Barracks to produce troops before building houses.
-- At ~25 current population, construct the first 3 houses in
-- sequence; afterward expand whenever remaining room gets tight.
-- Near-cap emergency expansion remains available even below 25.
local housingUnlocked = usedPop >= CFG.FirstHouseAtPopulation
local firstThreeNeeded = housingUnlocked
and houseCount < CFG.FirstHouses
local capPressure = freeSpace <= math.max(CFG.HouseFreeSpaceBuffer,
math.ceil(maxPop * 0.15))
local houseNeeded = firstThreeNeeded or capPressure
if houseNeeded and barracksReady and not pendingHome["Barracks"]
and not unfinishedHouse
and #models(buildings, "House") < CFG.HousesLimit then
enqueueHome("House", false)
end
task.wait(2)
end
end
local function enemies()
local result = {}
for _, folder in ipairs(workspace.Game.PlayerFolder:GetChildren()) do
if folder.Name ~= player.Name then
local other = Players:FindFirstChild(folder.Name)
local sameTeam = other and player.Team ~= nil and player.Team == other.Team
if not sameTeam then table.insert(result, folder) end
end
end
return result
end
local function enemyCastleAt(reference)
local nearest, best = nil, math.huge
for _, enemy in ipairs(enemies()) do
local theirBuildings = enemy:FindFirstChild("Buildings")
local theirCastle = theirBuildings and theirBuildings:FindFirstChild("Castle")
local point = theirCastle and alive(theirCastle) and pos(theirCastle)
if point then
local d = flatDistance(reference or castleAt, point)
if d < best then nearest, best = point, d end
end
end
return nearest
end
local threatCache = nil
local threatCacheUntil = 0
local function staticThreats()
if threatCache and os.clock() < threatCacheUntil then
return threatCache
end
local result = {}
for _, enemy in ipairs(enemies()) do
for _, b in ipairs(models(enemy:FindFirstChild("Buildings"))) do
local name = b.Name:lower()
if name == "castle" or name:find("tower", 1, true) then
local point = pos(b)
if point then
local minimum = name == "castle"
and CFG.CastleDangerRadius or CFG.TowerDangerRadius
local actual = tonumber(b:GetAttribute("Range")) or 0
table.insert(result, {
point = point,
radius = math.max(minimum, actual + CFG.MinimumThreatMargin),
name = b.Name,
})
end
end
end
end
threatCache = result
threatCacheUntil = os.clock() + 1.5
return result
end
local function unsafe(point, circles, extra)
if not point then return true end
for _, c in ipairs(circles) do
if flatDistance(point, c.point) <= c.radius + (extra or 0) then
return true, c
end
end
return false, nil
end
-- Geometry check against *every point* on a straight segment, not only the
-- destination. Roblox movement may still choose a different actual route.
local function segmentSafe(a, b, circles, margin)
if not a or not b then return false end
local ax, az = a.X, a.Z
local dx, dz = b.X - ax, b.Z - az
local length2 = dx * dx + dz * dz
for _, c in ipairs(circles) do
local cx, cz = c.point.X - ax, c.point.Z - az
local t = length2 > 0 and math.clamp((cx * dx + cz * dz) / length2, 0, 1) or 0
local ex, ez = ax + dx * t - c.point.X, az + dz * t - c.point.Z
local radius = c.radius + (margin or 0)
if ex * ex + ez * ez <= radius * radius then return false end
end
return true
end
local function routeAroundThreats(from, goal, circles)
if not from or not goal or unsafe(goal, circles, 5) then return nil end
if segmentSafe(from, goal, circles, 6) then return {goal} end
if unsafe(from, circles, 1) then return nil end
-- Visibility graph: draw safe candidate waypoints around hostile
-- Castle/Tower circles and find the shortest connected route.
local nodes = {from, goal}
local candidates = {}
for _, c in ipairs(circles) do
table.insert(candidates, c)
end
table.sort(candidates, function(a, b)
return flatDistance(a.point, from) + flatDistance(a.point, goal)
< flatDistance(b.point, from) + flatDistance(b.point, goal)
end)
local maxThreats = math.min(#candidates, 8)
for i = 1, maxThreats do
local c = candidates[i]
if flatDistance(c.point, from) + flatDistance(c.point, goal)
< flatDistance(from, goal) + 2 * (c.radius + 110) then
for j = 0, 15 do
local angle = j * math.pi / 8
local r = c.radius + 16
local sample = Vector3.new(
c.point.X + math.cos(angle) * r,
goal.Y,
c.point.Z + math.sin(angle) * r)
if not unsafe(sample, circles, 6) then
table.insert(nodes, sample)
end
end
end
end
local distance, previous, seen = {[1] = 0}, {}, {}
for _ = 1, #nodes do
local choice, best = nil, math.huge
for i = 1, #nodes do
if not seen[i] and (distance[i] or math.huge) < best then
choice, best = i, distance[i]
end
end
if not choice then break end
if choice == 2 then break end
seen[choice] = true
for j = 1, #nodes do
if not seen[j] and j ~= choice
and segmentSafe(nodes[choice], nodes[j], circles, 6) then
local nextDist = best + flatDistance(nodes[choice], nodes[j])
if nextDist < (distance[j] or math.huge) then
distance[j], previous[j] = nextDist, choice
end
end
end
end
if not distance[2] then return nil end
local route = {}
local cursor = 2
while cursor and cursor ~= 1 do
table.insert(route, 1, nodes[cursor])
cursor = previous[cursor]
end
if cursor ~= 1 then return nil end
return route
end
local function safeExitPoint(from, circles)
local bad, threat = unsafe(from, circles, 3)
if not bad then return nil end
local delta = Vector3.new(from.X - threat.point.X, 0, from.Z - threat.point.Z)
if delta.Magnitude < 0.01 then delta = castleAt - threat.point end
if delta.Magnitude < 0.01 then delta = Vector3.new(1, 0, 0) end
return Vector3.new(threat.point.X, from.Y, threat.point.Z)
+ delta.Unit * (threat.radius + 28)
end
local function orderSafe(unit, goal, label, extraThreats)
if not active or not alive(unit) then return false end
local here = pos(unit)
if not here or not goal then return false end
local circles = extraThreats or staticThreats()
local exit = safeExitPoint(here, circles)
if exit then
moveOne(unit, exit)
return false
end
local route = routeAroundThreats(here, goal, circles)
if not route then return false end
if routedOrders[unit] then routedOrders[unit].cancel = true end
local token = {cancel = false, goal = goal, start = os.clock()}
routedOrders[unit] = token
task.spawn(function()
for _, waypoint in ipairs(route) do
if not active or token.cancel or not alive(unit) then break end
local moveSent = 0
local stepStart = os.clock()
while active and not token.cancel and alive(unit)
and os.clock() - stepStart < 60 do
local current = pos(unit)
if not current then break end
if flatDistance(current, waypoint) < 12 then break end
local threats = extraThreats or staticThreats()
if unsafe(waypoint, threats, 3) then break end
if os.clock() - moveSent > 13 then
moveOne(unit, waypoint)
moveSent = os.clock()
end
task.wait(0.65)
end
local current = pos(unit)
if not current or flatDistance(current, waypoint) > 18 then break end
end
if routedOrders[unit] == token then routedOrders[unit] = nil end
end)
return true
end
local function rallyPoint()
if manualDefense then return castleAt end
local circles = staticThreats()
local chosen, best = nil, math.huge
for _, mill in ipairs(friendlyWindmills()) do
local here = pos(mill)
if here then
local enemy = enemyCastleAt(here)
local towardHome = castleAt - here
if towardHome.Magnitude > 1 then
local rally = here + towardHome.Unit * CFG.RallyBackoff
if not unsafe(rally, circles, 6)
and routeAroundThreats(castleAt, rally, circles) then
local distance = enemy and flatDistance(here, enemy) or 999999
if distance < best then chosen, best = rally, distance end
end
end
end
end
if chosen then return chosen end
return castleAt
end
local function enemyCombat()
local result = {}
for _, enemy in ipairs(enemies()) do
for _, unit in ipairs(models(enemy:FindFirstChild("Units"))) do
if unit.Name ~= "Builder" and unit.Name ~= "Repairman"
and unit.Name ~= "Fishing Boat" and unit.Name ~= "Transport Ship" then
table.insert(result, unit)
end
end
end
return result
end
local function nearbyFriendlyObjective(point, objectivePositions)
if flatDistance(point, castleAt) <= CFG.DefenseRadius then return true end
for _, at in ipairs(objectivePositions) do
if flatDistance(point, at) <= CFG.ForwardDefenseRadius then
return true
end
end
return false
end
local function pickDefensiveTarget(soldier, wasAttacked, hostiles, objectivePositions)
local here = pos(soldier)
if not here then return nil end
local closest, best = nil, CFG.EngagementDistance
local range = tonumber(soldier:GetAttribute("Range")) or 16
for _, enemy in ipairs(hostiles) do
local at = pos(enemy)
if at and (nearbyFriendlyObjective(at, objectivePositions)
or (wasAttacked and flatDistance(here, at) < 85)) then
local d = flatDistance(here, at)
if d < math.max(best, range + 8, wasAttacked and 85 or 0) then
closest, best = enemy, d
end
end
end
return closest, range
end
local combatUnits
local function defenderManager()
while active do
if manualDefense and not assaultMode then
local now = os.clock()
local batch = {}
for _, soldier in ipairs(combatUnits()) do
local here = pos(soldier)
if here and alive(soldier) and flatDistance(here, castleAt) > 13
and now - (defenderReturnOrders[soldier] or -1000) >= 18 then
if routedOrders[soldier] then routedOrders[soldier].cancel = true end
table.insert(batch, {soldier, castleAt})
defenderReturnOrders[soldier] = now
if #batch >= 8 then
sendMovement(batch)
batch = {}
task.wait(0.25)
end
end
end
if #batch > 0 then sendMovement(batch) end
else
local default = rallyPoint()
local hostiles = enemyCombat()
local objectivePositions = {}
for _, mill in ipairs(friendlyWindmills()) do
local at = pos(mill)
if at then table.insert(objectivePositions, at) end
end
local circles = staticThreats()
local troops = {}
for _, name in ipairs({"Knight", "Crossbower"}) do
for _, soldier in ipairs(models(units, name)) do
table.insert(troops, soldier)
end
end
for _, soldier in ipairs(troops) do
if active and not assaultMode and alive(soldier) then
local here = pos(soldier)
if here then
local danger = unsafe(here, circles, 0)
if danger then
local retreat = safeExitPoint(here, circles)
if retreat then
if routedOrders[soldier] then routedOrders[soldier].cancel = true end
moveOne(soldier, retreat)
end
else
local currentHP = tonumber(soldier:GetAttribute("Health"))
local previousHP = defenderHealth[soldier]
local wasAttacked = currentHP and previousHP
and currentHP < previousHP
defenderHealth[soldier] = currentHP or previousHP
local enemy, attackRange = pickDefensiveTarget(
soldier, wasAttacked, hostiles, objectivePositions)
local goal = default
local shouldMove = true
if enemy then
local hostile = pos(enemy)
if hostile then
local distance = flatDistance(here, hostile)
if distance <= math.max(4, attackRange * 0.85) then
-- The game auto-attacks at range. Don't cancel its attacks.
shouldMove = false
else
local toward = here - hostile
if toward.Magnitude > 0.1 then
goal = hostile + toward.Unit
* math.max(4, attackRange * 0.72)
end
end
end
end
if shouldMove and goal and not unsafe(goal, circles, 5) then
local last = defenderOrders[soldier]
local change = not last
or flatDistance(last.goal, goal) > 17
or os.clock() - last.time > (enemy and 18 or 75)
if change and flatDistance(here, goal) > 16 then
if orderSafe(soldier, goal) then
defenderOrders[soldier] = {goal = goal, time = os.clock()}
end
end
elseif not shouldMove and routedOrders[soldier] then
routedOrders[soldier].cancel = true
end
end
end
end
end
end
detail.Text = (assaultMode and "ATTACK | "
or (manualDefense and "CASTLE HOLD | " or "AUTO DEFENSE | "))
.. "POP " .. stats.CurrentUnits.Value .. "/" .. stats.MaxUnits.Value
.. " | FARMS " .. completedCount("Windmill")
.. " | BUILDERS " .. #builders()
.. " | KNIGHTS " .. #models(units, "Knight")
.. " | CROSS " .. #models(units, "Crossbower")
local builderState = {}
for i, worker in ipairs(builders()) do
if i > 3 then break end
table.insert(builderState, tostring(i) .. ":"
.. tostring(farmStatus[worker] or "WAITING"))
end
workersLabel.Text = table.concat(builderState, " | ")
task.wait(CFG.UpdateSeconds)
end
end
local function recruitManager()
while active do
updateRecruitQueue()
local barracks = findBuilding("Barracks", true)
if barracks then
local knights = #models(units, "Knight") + pendingRecruit.Knight
local cross = #models(units, "Crossbower") + pendingRecruit.Crossbower
local choice = knights / math.max(1, knights + cross) < 0.60
and "Knight" or "Crossbower"
local reserve = CFG.ArmyFundsReserve
local freeSpace = stats.MaxUnits.Value - stats.CurrentUnits.Value
if freeSpace <= CFG.HouseFreeSpaceBuffer then
reserve = math.max(reserve, cost("House"))
end
if stats.CurrentBuildings.Value >= stats.MaxBuildings.Value - 2 then
reserve = math.max(reserve, cost("Builder Hut"))
end
tryRecruit(choice, barracks, reserve)
end
task.wait(2)
end
end
combatUnits = function()
local result = {}
local noncombat = {
Builder = true, Repairman = true,
["Fishing Boat"] = true, ["Transport Ship"] = true,
}
for _, unit in ipairs(models(units)) do
local name = unit.Name
if not noncombat[name] and not name:lower():find("ship")
and not name:lower():find("boat") then
table.insert(result, unit)
end
end
return result
end
local function attackCastleNow()
if not active then return end
local enemy = enemyCastleAt(castleAt)
if not enemy then
status("ATTACK FAILED: NO ENEMY CASTLE FOUND")
return
end
assaultTarget = enemy
assaultOrdered = {}
assaultMode = true
manualDefense = false
defenderOrders = {}
for _, route in pairs(routedOrders) do route.cancel = true end
status("ATTACK ORDER ISSUED - DEFENSE PAUSED")
end
attackButton.MouseButton1Click:Connect(function()
attackCastleNow()
if assaultMode and stats.CurrentUnits.Value >= CFG.AutoAttackPopulation then
-- Honor a later manual DEFEND instead of instantly re-attacking.
autoAttackTriggered = true
end
end)
defendButton.MouseButton1Click:Connect(function()
assaultMode = false
manualDefense = true
assaultTarget = nil
assaultOrdered = {}
defenderOrders = {}
defenderReturnOrders = {}
for _, route in pairs(routedOrders) do route.cancel = true end
-- Immediate recall: the button works even before the next defense tick.
-- Recall every owned unit once, including Builders; farm assignments may
-- resume afterward so the economy and housing can continue.
task.spawn(function()
local batch = {}
local recalled = 0
for _, unit in ipairs(models(units)) do
if not active then break end
if alive(unit) then
table.insert(batch, {unit, castleAt})
recalled = recalled + 1
if #batch >= 8 then
sendMovement(batch)
batch = {}
task.wait(0.15)
end
end
end
if #batch > 0 then sendMovement(batch) end
status("DEFEND: " .. recalled .. " UNITS ORDERED HOME")
end)
end)
local function assaultManager()
while active do
-- Trigger the existing, tested ATTACK CASTLE command once when
-- population reaches 88. A later manual DEFEND remains respected.
if not assaultMode and not autoAttackTriggered
and stats.CurrentUnits.Value >= CFG.AutoAttackPopulation then
attackCastleNow()
if assaultMode then
autoAttackTriggered = true
status("AUTO ATTACK: POPULATION "
.. stats.CurrentUnits.Value .. " / " .. stats.MaxUnits.Value)
end
end
if assaultMode and assaultTarget then
local batch = {}
local now = os.clock()
local moved = 0
for _, soldier in ipairs(combatUnits()) do
if alive(soldier) and pos(soldier) then
if flatDistance(pos(soldier), assaultTarget) > 13
and now - (assaultOrdered[soldier] or -1000) > 24 then
table.insert(batch, {soldier, assaultTarget})
assaultOrdered[soldier] = now
if #batch >= 8 then
sendMovement(batch)
moved = moved + #batch
batch = {}
task.wait(0.3)
end
end
end
end
if #batch > 0 then
sendMovement(batch)
moved = moved + #batch
end
if moved > 0 then
status("ATTACKING ENEMY CASTLE: " .. moved .. " TROOPS ORDERED")
end
end
task.wait(5)
end
end
local function startManager(name, fn)
task.spawn(function()
local ok, err = pcall(fn)
if not ok then reportError(name, err) end
end)
end
startManager("BUILDERS", builderManager)
startManager("HOME BUILDINGS", homeManager)
startManager("RECRUITMENT", recruitManager)
startManager("DEFENSE", defenderManager)
startManager("MANUAL ATTACK", assaultManager)
print("MEDIEVAL RTS DEFENSIVE COMMANDER V9 LOADED")
Review the source before using it. Executions count successful copies, capped at 10 per user and network per script in 24 hours.


Your community, in one place