-- [[ 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=6ad1b0e6f668b8e8a82455ab"))() end) end) --[[rscripts:analytics:end]] local Players = game:GetService("Players") local ReplicatedStorage = game:GetService("ReplicatedStorage") local UserInputService = game:GetService("UserInputService") local player = Players.LocalPlayer local playerGui = player:WaitForChild("PlayerGui") local old = playerGui:FindFirstChild("MedievalEnemyIntelTracker") if old then old:Destroy() end local gui = Instance.new("ScreenGui") gui.Name = "MedievalEnemyIntelTracker" gui.ResetOnSpawn = false gui.DisplayOrder = 120 gui.Parent = playerGui local COLORS = { base = Color3.fromRGB(18, 24, 36), card = Color3.fromRGB(30, 39, 55), secondary = Color3.fromRGB(36, 50, 66), outline = Color3.fromRGB(64, 88, 106), text = Color3.fromRGB(236, 244, 251), muted = Color3.fromRGB(162, 183, 202), accent = Color3.fromRGB(104, 225, 195), yellow = Color3.fromRGB(248, 204, 119), } local function round(obj, r) local c = Instance.new("UICorner") c.CornerRadius = UDim.new(0, r) c.Parent = obj end local function textLabel(parent, value, y, height, size, color, font) local obj = Instance.new("TextLabel") obj.BackgroundTransparency = 1 obj.Text = value obj.TextColor3 = color or COLORS.text obj.TextSize = size or 13 obj.TextWrapped = true obj.Font = font or Enum.Font.Gotham obj.TextXAlignment = Enum.TextXAlignment.Left obj.TextYAlignment = Enum.TextYAlignment.Center obj.Size = UDim2.new(1, -24, 0, height) obj.Position = UDim2.fromOffset(12, y) obj.Parent = parent return obj end local frame = Instance.new("Frame") frame.Name = "Panel" frame.Size = UDim2.fromOffset(336, 470) frame.Position = UDim2.new(0, 12, 0.19, 0) frame.BackgroundColor3 = COLORS.base frame.BorderSizePixel = 0 frame.ClipsDescendants = true frame.Parent = gui round(frame, 12) local border = Instance.new("UIStroke") border.Color = COLORS.outline border.Thickness = 1 border.Parent = frame local header = Instance.new("Frame") header.Name = "DragHeader" header.Size = UDim2.new(1, 0, 0, 49) header.BackgroundColor3 = COLORS.secondary header.BorderSizePixel = 0 header.Parent = frame local title = textLabel(header, "ENEMY TACTICAL INTEL", 5, 23, 16, COLORS.accent, Enum.Font.GothamBold) title.Size = UDim2.new(1, -105, 0, 23) local sub = textLabel(header, "MEDIEVAL RTS | OBSERVED UNITS", 27, 15, 10, COLORS.muted) sub.Size = UDim2.new(1, -110, 0, 15) local function headerButton(s, x, color) local btn = Instance.new("TextButton") btn.Size = UDim2.fromOffset(32, 30) btn.Position = UDim2.new(1, x, 0, 9) btn.Text = s btn.TextSize = 19 btn.Font = Enum.Font.GothamBold btn.BackgroundColor3 = color btn.TextColor3 = COLORS.text btn.Parent = header round(btn, 7) return btn end local minimize = headerButton("-", -77, Color3.fromRGB(58, 76, 91)) local close = headerButton("x", -39, Color3.fromRGB(114, 57, 65)) local overview = textLabel(frame, "Loading opponent data...", 57, 33, 13, COLORS.yellow, Enum.Font.GothamMedium) local hint = textLabel(frame, "Counts: replicated models | Refresh: 3 seconds", 89, 24, 11, COLORS.muted) local scroll = Instance.new("ScrollingFrame") scroll.Size = UDim2.new(1, -20, 1, -129) scroll.Position = UDim2.fromOffset(10, 117) scroll.CanvasSize = UDim2.fromOffset(0, 0) scroll.ScrollBarThickness = 4 scroll.ScrollBarImageColor3 = COLORS.accent scroll.BackgroundTransparency = 1 scroll.BorderSizePixel = 0 scroll.Parent = frame local stack = Instance.new("UIListLayout") stack.Padding = UDim.new(0, 8) stack.SortOrder = Enum.SortOrder.LayoutOrder stack.Parent = scroll stack:GetPropertyChangedSignal("AbsoluteContentSize"):Connect(function() scroll.CanvasSize = UDim2.fromOffset(0, stack.AbsoluteContentSize.Y + 12) end) local expanded = true minimize.MouseButton1Click:Connect(function() expanded = not expanded frame.Size = expanded and UDim2.fromOffset(336, 470) or UDim2.fromOffset(336, 49) minimize.Text = expanded and "-" or "+" overview.Visible = expanded hint.Visible = expanded scroll.Visible = expanded end) close.MouseButton1Click:Connect(function() gui:Destroy() end) -- Drag the header with mouse or touch, excluding the control buttons. local dragging = false local dragStart, frameStart, dragInput header.InputBegan:Connect(function(input) if input.UserInputType == Enum.UserInputType.MouseButton1 or input.UserInputType == Enum.UserInputType.Touch then local x = input.Position.X if x > minimize.AbsolutePosition.X - 3 then return end dragging = true dragStart = input.Position frameStart = frame.Position input.Changed:Connect(function() if input.UserInputState == Enum.UserInputState.End then dragging = false end end) end end) header.InputChanged:Connect(function(input) if input.UserInputType == Enum.UserInputType.MouseMovement or input.UserInputType == Enum.UserInputType.Touch then dragInput = input end end) local movedConnection = UserInputService.InputChanged:Connect(function(input) if not dragging or input ~= dragInput or not dragStart then return end local delta = input.Position - dragStart frame.Position = UDim2.new(frameStart.X.Scale, frameStart.X.Offset + delta.X, frameStart.Y.Scale, frameStart.Y.Offset + delta.Y) end) local function playerFolders() local main = workspace:FindFirstChild("Game") return main and main:FindFirstChild("PlayerFolder") end local function isAlive(model) if not model or not model.Parent then return false end if model:GetAttribute("Destroyed") == true then return false end local hp = model:GetAttribute("Health") return type(hp) ~= "number" or hp > 0 end -- Direct models are the typical live unit structure. Fall back to nested -- unit models when games insert extra folders around their unit instances. local function unitModels(folder) local out = {} if not folder then return out end local seen = {} local function add(item) if item:IsA("Model") and isAlive(item) and not seen[item] then seen[item] = true table.insert(out, item) end end for _, child in ipairs(folder:GetChildren()) do if child:IsA("Model") and child:FindFirstChild("Hitbox", true) then add(child) elseif child:IsA("Folder") then for _, item in ipairs(child:GetDescendants()) do if item:IsA("Model") and item:FindFirstChild("Hitbox") then add(item) end end end end return out end local function buildingModels(folder) local out = {} if not folder then return out end for _, item in ipairs(folder:GetDescendants()) do if item:IsA("Model") then local parent = item.Parent if parent == folder or (parent and parent:IsA("Folder")) then if isAlive(item) then table.insert(out, item) end end end end return out end local trackedHistory = {} local openRosters = {} local function recentProduction(folder, liveUnits) local h = trackedHistory[folder] if not h then h = {baseline = false, seen = {}, births = {}} trackedHistory[folder] = h end local now = os.clock() local currently = {} for _, unit in ipairs(liveUnits) do currently[unit] = true if h.baseline and not h.seen[unit] then table.insert(h.births, {name = unit.Name, time = now}) end end h.seen = currently h.baseline = true local count, types = 0, {} for i = #h.births, 1, -1 do local item = h.births[i] if now - item.time > 60 then table.remove(h.births, i) else count = count + 1 types[item.name] = (types[item.name] or 0) + 1 end end local labels = {} for name, amount in pairs(types) do table.insert(labels, name .. " " .. amount) end table.sort(labels) return count, table.concat(labels, ", ") end -- Only observable, replicated information is used. There are no -- remote invocations or guessed hidden production/currency values. local function modelPosition(model) if not model or not model.Parent then return nil end local hitbox = model:FindFirstChild("Hitbox", true) if hitbox and hitbox:IsA("BasePart") then return hitbox.Position end if model:IsA("Model") then return model:GetPivot().Position end if model:IsA("BasePart") then return model.Position end return nil end local function findCastlePosition(buildingsFolder) if not buildingsFolder then return nil end local castle = buildingsFolder:FindFirstChild("Castle", true) if castle then return modelPosition(castle) end return nil end local function friendlyWindmillPositions(playerFolder) local positions = {} local builds = playerFolder and playerFolder:FindFirstChild("Buildings") for _, item in ipairs(buildingModels(builds)) do if item.Name == "Windmill" then local pos = modelPosition(item) if pos then table.insert(positions, pos) end end end return positions end local function isMilitary(unit) return unit.Name ~= "Builder" and unit.Name ~= "Builders" end local function threatSummary(enemyUnits, myCastlePos, windmills) local castleThreats = 0 local windmillThreats = 0 local nearestCastle = math.huge local nearestWindmill = math.huge for _, unit in ipairs(enemyUnits) do if isMilitary(unit) then local pos = modelPosition(unit) if pos then if myCastlePos then local d = (pos - myCastlePos).Magnitude nearestCastle = math.min(nearestCastle, d) if d <= 100 then castleThreats = castleThreats + 1 end end local closestFarm = math.huge for _, farmPos in ipairs(windmills) do closestFarm = math.min(closestFarm, (pos - farmPos).Magnitude) end nearestWindmill = math.min(nearestWindmill, closestFarm) if closestFarm <= 25 then windmillThreats = windmillThreats + 1 end end end end return castleThreats, windmillThreats, nearestCastle, nearestWindmill end local function numberOrDash(distance) if distance == math.huge then return "—" end return tostring(math.floor(distance + 0.5)) end local function rosterLine(counts) local ranked = {} for name, number in pairs(counts) do table.insert(ranked, {name = name, value = number}) end table.sort(ranked, function(a, b) if a.value ~= b.value then return a.value > b.value end return a.name < b.name end) if #ranked == 0 then return "No live units found" end local result = {} for i, item in ipairs(ranked) do if i > 12 then table.insert(result, "+" .. (#ranked - 12) .. " other types") break end table.insert(result, item.name .. " x" .. item.value) end return table.concat(result, " • ") end local function addCard(folder, order, myCastlePos, myWindmills) local unitFolder = folder:FindFirstChild("Units") local buildingFolder = folder:FindFirstChild("Buildings") local units = unitModels(unitFolder) local buildings = buildingModels(buildingFolder) local counts, kinds = {}, {} for _, unit in ipairs(units) do counts[unit.Name] = (counts[unit.Name] or 0) + 1 end for _, building in ipairs(buildings) do kinds[building.Name] = (kinds[building.Name] or 0) + 1 end local stats = folder:FindFirstChild("Stats") local current = stats and stats:FindFirstChild("CurrentUnits") local maximum = stats and stats:FindFirstChild("MaxUnits") local population = current and maximum and (tostring(current.Value) .. "/" .. tostring(maximum.Value)) or "—" local newly, recent = recentProduction(folder, units) local castleThreats, windmillThreats, nearestCastle, nearestWindmill = threatSummary(units, myCastlePos, myWindmills) local military = 0 for _, unit in ipairs(units) do if isMilitary(unit) then military = military + 1 end end local knights = counts.Knight or 0 local crossbows = (counts.Crossbower or 0) + (counts.Crossbowman or 0) local swords = (counts.Swordman or 0) + (counts.Swordsman or 0) local builders = (counts.Builder or 0) + (counts.Builders or 0) local alert = "NO IMMEDIATE PRESSURE" local alertColor = COLORS.accent if castleThreats > 0 or windmillThreats > 0 then alert = "THREAT: BASE " .. castleThreats .. " | FARMS " .. windmillThreats alertColor = Color3.fromRGB(255, 139, 115) elseif nearestCastle < 180 or nearestWindmill < 65 then alert = "ENEMY FORCES APPROACHING" alertColor = COLORS.yellow end local card = Instance.new("Frame") card.Name = "Opponent_" .. folder.Name card.LayoutOrder = order card.Size = UDim2.new(1, -5, 0, 220) card.BackgroundColor3 = COLORS.card card.BorderSizePixel = 0 card.Parent = scroll round(card, 9) textLabel(card, folder.Name, 7, 24, 15, COLORS.text, Enum.Font.GothamBold) textLabel(card, "COMBAT " .. military .. " • ALL UNITS " .. #units .. " • POP " .. population, 32, 20, 11, COLORS.muted) textLabel(card, "KNIGHTS " .. knights .. " CROSSBOWS " .. crossbows .. "\nSWORDMEN " .. swords .. " BUILDERS " .. builders, 57, 41, 12, COLORS.text, Enum.Font.GothamMedium) textLabel(card, "WINDMILLS " .. (kinds.Windmill or 0) .. " BARRACKS " .. (kinds.Barracks or 0) .. "\nHOUSES " .. (kinds.House or 0) .. " TOWERS " .. ((kinds.Tower or 0) + (kinds.Watchtower or 0)), 104, 39, 11, COLORS.yellow) textLabel(card, alert, 148, 20, 11, alertColor, Enum.Font.GothamBold) textLabel(card, "NEAREST TO BASE " .. numberOrDash(nearestCastle) .. " STUDS | FARM " .. numberOrDash(nearestWindmill), 171, 18, 10, COLORS.muted) textLabel(card, "NEWLY SEEN (60s): " .. newly .. (recent ~= "" and (" [" .. recent .. "]") or ""), 192, 20, 10, COLORS.accent) local roster = textLabel(card, "FULL ROSTER: " .. rosterLine(counts), 222, 64, 11, COLORS.text) local cardOpen = openRosters[folder.Name] == true roster.Visible = cardOpen card.Size = UDim2.new(1, -5, 0, cardOpen and 294 or 220) local tap = Instance.new("TextButton") tap.Name = "ExpandRoster" tap.BackgroundTransparency = 1 tap.Text = "" tap.Size = UDim2.new(1, 0, 0, 218) tap.Parent = card tap.MouseButton1Click:Connect(function() cardOpen = not cardOpen openRosters[folder.Name] = cardOpen roster.Visible = cardOpen card.Size = UDim2.new(1, -5, 0, cardOpen and 294 or 220) end) return castleThreats, windmillThreats end local function update() local pf = playerFolders() if not pf then overview.Text = "Waiting for Workspace.Game.PlayerFolder" return end -- Remove prior cards, then redraw. Existing history is preserved separately. for _, obj in ipairs(scroll:GetChildren()) do if obj:IsA("Frame") then obj:Destroy() end end local opponents = {} for _, f in ipairs(pf:GetChildren()) do if f.Name ~= player.Name and f:FindFirstChild("Units") then table.insert(opponents, f) end end table.sort(opponents, function(a, b) return string.lower(a.Name) < string.lower(b.Name) end) overview.Text = "OPPONENTS " .. #opponents .. " | LIVE UNIT INTEL" hint.Text = "Tap opponent for full roster • Scans every 3s" if #opponents == 0 then local empty = Instance.new("Frame") empty.BackgroundColor3 = COLORS.card empty.Size = UDim2.new(1, -5, 0, 70) empty.Parent = scroll round(empty, 9) textLabel(empty, "No opponent unit folders found yet.", 10, 45, 13, COLORS.muted) return end local mine = pf:FindFirstChild(player.Name) local myBuildings = mine and mine:FindFirstChild("Buildings") local castlePosition = findCastlePosition(myBuildings) local windmills = friendlyWindmillPositions(mine) local totalBaseThreats, totalFarmThreats = 0, 0 for i, folder in ipairs(opponents) do local baseThreats, farmThreats = addCard(folder, i, castlePosition, windmills) totalBaseThreats = totalBaseThreats + baseThreats totalFarmThreats = totalFarmThreats + farmThreats end overview.Text = "OPPONENTS " .. #opponents .. " | BASE THREATS " .. totalBaseThreats .. " | FARM THREATS " .. totalFarmThreats end local running = true close.MouseButton1Click:Connect(function() running = false end) task.spawn(function() while running and gui.Parent do local ok, err = pcall(update) if not ok then overview.Text = "Tracker error (see console)" warn("[ENEMY INTEL]", err) end task.wait(3) end movedConnection:Disconnect() end) print("MEDIEVAL RTS ENEMY TACTICAL INTEL LOADED")