Medieval RTS | Enemy Tactical Intelligence V1.0 Army Tracker
MEDIEVAL RTS [⚔️BIGGER ARMIES]Description
Medieval RTS | Enemy Tactical Intelligence V2 Know your enemy. Track their army. Defend your kingdom. Enemy Tactical Intelligence V1.0 is a real-time intelligence dashboard for Roblox Medieval RTS, designed for experienced players who want better battlefield awareness. Monitor enemy troop compositions, military infrastructure, unit production activity, and potential threats to your Castle and Windmills all through a clean, collapsible interface. Features Live Enemy Army Tracking Displays every opponent separately. Counts Knights, Crossbowmen, Swordmen, Builders, and other unit types. Shows total military units and total units. Displays enemy population. Expand opponent profiles to see their complete army composition. Castle & Windmill Threat Detection Detects enemy combat units approaching your Castle. Identifies enemies near your Windmills. Displays the distance of nearby enemy troops (in studs). Helps identify potential raids before your economy is destroyed. Enemy Economy & Infrastructure Intelligence Tracks enemy Windmills, Barracks, Houses, and Towers. Helps identify whether an opponent is prioritizing economic expansion or military production. Provides useful information for planning attacks and defending key locations. Production Activity Monitoring Tracks newly observed enemy units over the previous 60 seconds. Helps estimate how quickly opponents are expanding their armies. Makes it easier to recognize military buildup. Modern, Mobile-Friendly GUI Clean tactical dashboard. Collapsible and draggable interface. Expandable enemy profiles. Refreshes approximately every 3 seconds. Designed for PC and mobile environments. Why Use It? Medieval RTS rewards good decisions, positioning, and timing. This tracker brings important battlefield information into one convenient interface, reducing the need to inspect enemy armies and buildings manually. Whether you're defending your Windmills, preparing for a Knight rush, or comparing army compositions, Enemy Tactical Intelligence V2 helps you make more informed strategic decisions. Interface: PC and mobile-friendly Tested Environment: Delta
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 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")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