Add FPS game example, auto-connect plugin, and Python injection tools
- Updated RobloxMCPPlugin with HTTP polling (auto-enables HttpService) - Added 20-weapon FPS game example (CoD-style) - Added Python studio-inject.py for command bar injection via Win32 API - Added auto-connect setup scripts (VBS + PowerShell) - Updated MCP server with all FPS game tools Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
242
examples/fps-game/part1_map.lua
Normal file
242
examples/fps-game/part1_map.lua
Normal file
@@ -0,0 +1,242 @@
|
||||
-- ═══════════════════════════════════════════════════════════════════
|
||||
-- MINI CALL OF DUTY - FPS Game Setup (Part 1: Map + Infrastructure)
|
||||
-- Inject into Roblox Studio Command Bar
|
||||
-- ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
-- Clean workspace
|
||||
for _, c in ipairs(workspace:GetChildren()) do
|
||||
if not c:IsA("Terrain") and not c:IsA("Camera") then c:Destroy() end
|
||||
end
|
||||
-- Clean services
|
||||
for _, c in ipairs(game:GetService("ReplicatedStorage"):GetChildren()) do c:Destroy() end
|
||||
for _, c in ipairs(game:GetService("StarterGui"):GetChildren()) do c:Destroy() end
|
||||
for _, c in ipairs(game:GetService("ServerScriptService"):GetChildren()) do c:Destroy() end
|
||||
for _, s in ipairs({"StarterPlayerScripts", "StarterPlayer"}) do
|
||||
local f = game:GetService("StarterPlayer"):FindFirstChild(s)
|
||||
if f then for _, c in ipairs(f:GetChildren()) do c:Destroy() end end
|
||||
end
|
||||
|
||||
local Lighting = game:GetService("Lighting")
|
||||
for _, c in ipairs(Lighting:GetChildren()) do c:Destroy() end
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════
|
||||
-- FOLDERS & REMOTES
|
||||
-- ═══════════════════════════════════════════════════════════════
|
||||
local RS = game:GetService("ReplicatedStorage")
|
||||
|
||||
local events = Instance.new("Folder") events.Name = "Events" events.Parent = RS
|
||||
Instance.new("RemoteEvent", events).Name = "ShootEvent"
|
||||
Instance.new("RemoteEvent", events).Name = "HitEvent"
|
||||
Instance.new("RemoteEvent", events).Name = "KillEvent"
|
||||
Instance.new("RemoteEvent", events).Name = "DamageEvent"
|
||||
Instance.new("RemoteEvent", events).Name = "ReloadEvent"
|
||||
Instance.new("RemoteFunction", events).Name = "GetGameData"
|
||||
|
||||
local shared = Instance.new("Folder") shared.Name = "Shared" shared.Parent = RS
|
||||
local assets = Instance.new("Folder") assets.Name = "Assets" assets.Parent = RS
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════
|
||||
-- MAP BUILDING - Urban Military Zone
|
||||
-- ═══════════════════════════════════════════════════════════════
|
||||
local mapModel = Instance.new("Model") mapModel.Name = "Map" mapModel.Parent = workspace
|
||||
|
||||
local function P(props)
|
||||
local p = Instance.new("Part")
|
||||
p.Anchored = true
|
||||
p.TopSurface = Enum.SurfaceType.Smooth
|
||||
p.BottomSurface = Enum.SurfaceType.Smooth
|
||||
for k,v in pairs(props) do p[k] = v end
|
||||
p.Parent = props.Parent or mapModel
|
||||
return p
|
||||
end
|
||||
|
||||
local function W(props)
|
||||
local w = Instance.new("WedgePart")
|
||||
w.Anchored = true
|
||||
for k,v in pairs(props) do w[k] = v end
|
||||
w.Parent = props.Parent or mapModel
|
||||
return w
|
||||
end
|
||||
|
||||
-- Ground
|
||||
P({Name="Ground", Size=Vector3.new(400,2,400), Position=Vector3.new(0,-1,0),
|
||||
Color=Color3.fromRGB(80,78,70), Material=Enum.Material.Asphalt})
|
||||
|
||||
-- Spawn area
|
||||
P({Name="SpawnPad", Size=Vector3.new(20,1,20), Position=Vector3.new(0,0.5,-160),
|
||||
Color=Color3.fromRGB(30,120,30), Material=Enum.Material.SmoothPlastic})
|
||||
|
||||
local spawnLoc = Instance.new("SpawnLocation")
|
||||
spawnLoc.Name = "PlayerSpawn"
|
||||
spawnLoc.Size = Vector3.new(8,1,8)
|
||||
spawnLoc.Position = Vector3.new(0,1,-155)
|
||||
spawnLoc.Anchored = true
|
||||
spawnLoc.CanCollide = false
|
||||
spawnLoc.Transparency = 0.5
|
||||
spawnLoc.Color = Color3.fromRGB(0,255,0)
|
||||
spawnLoc.Parent = mapModel
|
||||
|
||||
-- ─── BUILDINGS ───
|
||||
local function makeBuilding(x, z, w, d, h, color)
|
||||
local bldg = Instance.new("Model") bldg.Name = "Building" bldg.Parent = mapModel
|
||||
-- Floor
|
||||
P({Name="Floor", Size=Vector3.new(w,1,d), Position=Vector3.new(x,0.5,z),
|
||||
Color=Color3.fromRGB(100,95,85), Material=Enum.Material.SmoothPlastic, Parent=bldg})
|
||||
-- Walls
|
||||
P({Name="WallN", Size=Vector3.new(w,h,1), Position=Vector3.new(x,h/2+1,z-d/2),
|
||||
Color=color, Material=Enum.Material.Brick, Parent=bldg})
|
||||
P({Name="WallS", Size=Vector3.new(w,h,1), Position=Vector3.new(x,h/2+1,z+d/2),
|
||||
Color=color, Material=Enum.Material.Brick, Parent=bldg})
|
||||
P({Name="WallE", Size=Vector3.new(1,h,d), Position=Vector3.new(x+w/2,h/2+1,z),
|
||||
Color=color, Material=Enum.Material.Brick, Parent=bldg})
|
||||
P({Name="WallW", Size=Vector3.new(1,h,d), Position=Vector3.new(x-w/2,h/2+1,z),
|
||||
Color=color, Material=Enum.Material.Brick, Parent=bldg})
|
||||
-- Roof
|
||||
P({Name="Roof", Size=Vector3.new(w+2,1,d+2), Position=Vector3.new(x,h+1,z),
|
||||
Color=Color3.fromRGB(60,55,50), Material=Enum.Material.CorrodedMetal, Parent=bldg})
|
||||
-- Door opening (destroy wall segment)
|
||||
-- Window holes
|
||||
P({Name="WinN1", Size=Vector3.new(4,3,1.2), Position=Vector3.new(x+3,h/2,z-d/2),
|
||||
Color=Color3.fromRGB(135,135,135), Material=Enum.Material.SmoothPlastic, Parent=bldg})
|
||||
P({Name="WinN2", Size=Vector3.new(4,3,1.2), Position=Vector3.new(x-3,h/2,z-d/2),
|
||||
Color=Color3.fromRGB(135,135,135), Material=Enum.Material.SmoothPlastic, Parent=bldg})
|
||||
return bldg
|
||||
end
|
||||
|
||||
-- Main buildings
|
||||
makeBuilding(-50, -60, 30, 20, 12, Color3.fromRGB(140,130,120)) -- HQ building
|
||||
makeBuilding(50, -60, 25, 25, 10, Color3.fromRGB(130,125,115)) -- Barracks
|
||||
makeBuilding(-50, 40, 20, 30, 14, Color3.fromRGB(120,115,110)) -- Tower building
|
||||
makeBuilding(50, 50, 28, 22, 10, Color3.fromRGB(125,120,110)) -- Warehouse
|
||||
makeBuilding(0, 50, 22, 18, 8, Color3.fromRGB(145,135,125)) -- Center building
|
||||
|
||||
-- Ruined building (half walls)
|
||||
P({Name="RuinedWall1", Size=Vector3.new(12,6,1), Position=Vector3.new(-20,3,0),
|
||||
Color=Color3.fromRGB(100,95,85), Material=Enum.Material.Brick})
|
||||
P({Name="RuinedWall2", Size=Vector3.new(1,4,8), Position=Vector3.new(-14,2,-4),
|
||||
Color=Color3.fromRGB(100,95,85), Material=Enum.Material.Brick})
|
||||
P({Name="RuinedFloor", Size=Vector3.new(15,1,10), Position=Vector3.new(-20,0.5,0),
|
||||
Color=Color3.fromRGB(90,85,75), Material=Enum.Material.Concrete})
|
||||
|
||||
-- ─── COVER OBJECTS ───
|
||||
local coverPositions = {
|
||||
{-30,-120, 4,3,8}, {30,-120, 4,3,8}, {-10,-100, 6,2,4}, {10,-100, 6,2,4},
|
||||
{-40,-30, 3,2,6}, {40,-30, 3,2,6}, {-25,10, 5,2,3}, {25,10, 5,2,3},
|
||||
{0,-20, 4,3,4}, {-15,30, 3,2,5}, {15,30, 3,2,5},
|
||||
{-60,0, 4,3,8}, {60,0, 4,3,8},
|
||||
{-35,80, 5,2,4}, {35,80, 5,2,4}, {0,80, 3,2,6},
|
||||
{-20,-50, 3,2,3}, {20,-50, 3,2,3},
|
||||
{-70,-60, 4,3,6}, {70,-60, 4,3,6},
|
||||
{0,120, 6,2,4}, {-40,120, 4,3,5}, {40,120, 4,3,5},
|
||||
}
|
||||
for i, pos in ipairs(coverPositions) do
|
||||
P({Name="Cover_"..i, Size=Vector3.new(pos[4],pos[5],pos[6]),
|
||||
Position=Vector3.new(pos[1],pos[5]/2+0.5,pos[2]),
|
||||
Color=Color3.fromRGB(90+i*2,85+i*2,75+i*2), Material=Enum.Material.Concrete})
|
||||
end
|
||||
|
||||
-- ─── SANDBAG WALLS ───
|
||||
for i = 1, 20 do
|
||||
local angle = (i/20) * math.pi * 2
|
||||
local r = 85
|
||||
P({Name="Sandbag_"..i, Size=Vector3.new(6,3,3),
|
||||
Position=Vector3.new(math.cos(angle)*r, 1.5, math.sin(angle)*r),
|
||||
Orientation=Vector3.new(0, math.deg(angle), 0),
|
||||
Color=Color3.fromRGB(160,145,110), Material=Enum.Material.Slate})
|
||||
end
|
||||
|
||||
-- ─── WATCHTOWER ───
|
||||
local function makeTower(x, z)
|
||||
P({Name="TowerBase_"..x, Size=Vector3.new(6,0.5,6), Position=Vector3.new(x,8,z),
|
||||
Color=Color3.fromRGB(80,70,60), Material=Enum.Material.Wood})
|
||||
-- Legs
|
||||
P({Name="Leg1", Size=Vector3.new(1,16,1), Position=Vector3.new(x-2,8,z-2),
|
||||
Color=Color3.fromRGB(70,60,50), Material=Enum.Material.Wood})
|
||||
P({Name="Leg2", Size=Vector3.new(1,16,1), Position=Vector3.new(x+2,8,z-2),
|
||||
Color=Color3.fromRGB(70,60,50), Material=Enum.Material.Wood})
|
||||
P({Name="Leg3", Size=Vector3.new(1,16,1), Position=Vector3.new(x-2,8,z+2),
|
||||
Color=Color3.fromRGB(70,60,50), Material=Enum.Material.Wood})
|
||||
P({Name="Leg4", Size=Vector3.new(1,16,1), Position=Vector3.new(x+2,8,z+2),
|
||||
Color=Color3.fromRGB(70,60,50), Material=Enum.Material.Wood})
|
||||
-- Railing
|
||||
P({Name="Rail1", Size=Vector3.new(6,2,0.3), Position=Vector3.new(x,9,z-2.85),
|
||||
Color=Color3.fromRGB(70,60,50), Material=Enum.Material.Wood})
|
||||
P({Name="Rail2", Size=Vector3.new(6,2,0.3), Position=Vector3.new(x,9,z+2.85),
|
||||
Color=Color3.fromRGB(70,60,50), Material=Enum.Material.Wood})
|
||||
end
|
||||
makeTower(-70, -80)
|
||||
makeTower(70, -80)
|
||||
makeTower(-70, 90)
|
||||
makeTower(70, 90)
|
||||
|
||||
-- ─── CRATES ───
|
||||
for i = 1, 15 do
|
||||
local cx = math.random(-80, 80)
|
||||
local cz = math.random(-140, 140)
|
||||
P({Name="Crate_"..i, Size=Vector3.new(3,3,3),
|
||||
Position=Vector3.new(cx, 1.5, cz),
|
||||
Orientation=Vector3.new(0, math.random(0,90), 0),
|
||||
Color=Color3.fromRGB(140,110,60), Material=Enum.Material.Wood})
|
||||
end
|
||||
|
||||
-- ─── BARRELS ───
|
||||
for i = 1, 10 do
|
||||
local bx = math.random(-90, 90)
|
||||
local bz = math.random(-140, 140)
|
||||
P({Name="Barrel_"..i, Shape=Enum.PartType.Cylinder, Size=Vector3.new(3,3,3),
|
||||
Position=Vector3.new(bx, 1.5, bz),
|
||||
Orientation=Vector3.new(0, math.random(0,180), 90),
|
||||
Color=Color3.fromRGB(50,60,50), Material=Enum.Material.SmoothPlastic})
|
||||
end
|
||||
|
||||
-- ─── MAP BOUNDARY WALLS ───
|
||||
P({Name="BorderN", Size=Vector3.new(200,15,3), Position=Vector3.new(0,7.5,-180),
|
||||
Color=Color3.fromRGB(60,60,60), Material=Enum.Material.Concrete})
|
||||
P({Name="BorderS", Size=Vector3.new(200,15,3), Position=Vector3.new(0,7.5,180),
|
||||
Color=Color3.fromRGB(60,60,60), Material=Enum.Material.Concrete})
|
||||
P({Name="BorderE", Size=Vector3.new(3,15,200), Position=Vector3.new(98,7.5,0),
|
||||
Color=Color3.fromRGB(60,60,60), Material=Enum.Material.Concrete})
|
||||
P({Name="BorderW", Size=Vector3.new(3,15,200), Position=Vector3.new(-98,7.5,0),
|
||||
Color=Color3.fromRGB(60,60,60), Material=Enum.Material.Concrete})
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════
|
||||
-- LIGHTING - Dusk/Battle Atmosphere
|
||||
-- ═══════════════════════════════════════════════════════════════
|
||||
Lighting.Ambient = Color3.fromRGB(80,75,70)
|
||||
Lighting.OutdoorAmbient = Color3.fromRGB(100,90,80)
|
||||
Lighting.Brightness = 1.2
|
||||
Lighting.ClockTime = 17.5 -- Dusk
|
||||
Lighting.FogEnd = 400
|
||||
Lighting.FogStart = 50
|
||||
Lighting.FogColor = Color3.fromRGB(140,130,120)
|
||||
|
||||
local atmo = Instance.new("Atmosphere")
|
||||
atmo.Density = 0.25
|
||||
atmo.Color = Color3.fromRGB(180,165,145)
|
||||
atmo.Decay = Color3.fromRGB(120,110,100)
|
||||
atmo.Glare = 0.3
|
||||
atmo.Haze = 1.5
|
||||
atmo.Parent = Lighting
|
||||
|
||||
-- Sun rays
|
||||
local sunRays = Instance.new("SunRaysEffect")
|
||||
sunRays.Intensity = 0.04
|
||||
sunRays.Spread = 0.6
|
||||
sunRays.Parent = Lighting
|
||||
|
||||
-- Bloom
|
||||
local bloom = Instance.new("BloomEffect")
|
||||
bloom.Intensity = 0.3
|
||||
bloom.Size = 24
|
||||
bloom.Threshold = 1.5
|
||||
bloom.Parent = Lighting
|
||||
|
||||
-- Color correction (warm wartime tones)
|
||||
local cc = Instance.new("ColorCorrectionEffect")
|
||||
cc.Brightness = 0.02
|
||||
cc.Contrast = 0.1
|
||||
cc.Saturation = -0.15
|
||||
cc.TintColor = Color3.fromRGB(255,240,220)
|
||||
cc.Parent = Lighting
|
||||
|
||||
print("[CoD FPS] Part 1/5 complete: Map built, lighting set.")
|
||||
383
examples/fps-game/part2_weapons.lua
Normal file
383
examples/fps-game/part2_weapons.lua
Normal file
@@ -0,0 +1,383 @@
|
||||
-- ═══════════════════════════════════════════════════════════════════
|
||||
-- MINI CALL OF DUTY - FPS Game Setup (Part 2: Weapon System)
|
||||
-- ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
local RS = game:GetService("ReplicatedStorage")
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════
|
||||
-- WEAPON DATA MODULE
|
||||
-- ═══════════════════════════════════════════════════════════════
|
||||
local weaponData = Instance.new("ModuleScript")
|
||||
weaponData.Name = "WeaponData"
|
||||
weaponData.Parent = RS:FindFirstChild("Shared")
|
||||
weaponData.Source = [[
|
||||
local Weapons = {
|
||||
M4A1 = {
|
||||
name = "M4A1",
|
||||
displayName = "M4A1 Carbine",
|
||||
damage = 25,
|
||||
fireRate = 0.09, -- seconds between shots
|
||||
reloadTime = 2.2,
|
||||
magSize = 30,
|
||||
maxAmmo = 210,
|
||||
range = 300,
|
||||
headshotMult = 2.5,
|
||||
recoil = {x = 0.8, y = 1.2},
|
||||
spread = {hip = 3, ads = 0.5},
|
||||
aimSpeed = 0.15,
|
||||
moveSpeedMult = 0.95,
|
||||
automatic = true,
|
||||
adsFOV = 50,
|
||||
},
|
||||
AK47 = {
|
||||
name = "AK-47",
|
||||
displayName = "AK-47",
|
||||
damage = 33,
|
||||
fireRate = 0.1,
|
||||
reloadTime = 2.5,
|
||||
magSize = 30,
|
||||
maxAmmo = 210,
|
||||
range = 280,
|
||||
headshotMult = 2.0,
|
||||
recoil = {x = 1.2, y = 1.8},
|
||||
spread = {hip = 4, ads = 0.8},
|
||||
aimSpeed = 0.18,
|
||||
moveSpeedMult = 0.92,
|
||||
automatic = true,
|
||||
adsFOV = 48,
|
||||
},
|
||||
Sniper = {
|
||||
name = "AWP",
|
||||
displayName = "AWP Sniper",
|
||||
damage = 95,
|
||||
fireRate = 1.2,
|
||||
reloadTime = 3.5,
|
||||
magSize = 5,
|
||||
maxAmmo = 30,
|
||||
range = 800,
|
||||
headshotMult = 3.0,
|
||||
recoil = {x = 3, y = 5},
|
||||
spread = {hip = 8, ads = 0.1},
|
||||
aimSpeed = 0.25,
|
||||
moveSpeedMult = 0.85,
|
||||
automatic = false,
|
||||
adsFOV = 20,
|
||||
},
|
||||
Shotgun = {
|
||||
name = "SPAS-12",
|
||||
displayName = "SPAS-12 Shotgun",
|
||||
damage = 15, -- per pellet (8 pellets)
|
||||
fireRate = 0.7,
|
||||
reloadTime = 3.0,
|
||||
magSize = 8,
|
||||
maxAmmo = 40,
|
||||
range = 50,
|
||||
headshotMult = 1.5,
|
||||
recoil = {x = 4, y = 6},
|
||||
spread = {hip = 12, ads = 8},
|
||||
aimSpeed = 0.15,
|
||||
moveSpeedMult = 0.88,
|
||||
automatic = false,
|
||||
pellets = 8,
|
||||
adsFOV = 55,
|
||||
},
|
||||
}
|
||||
return Weapons
|
||||
]]
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════
|
||||
-- CLIENT WEAPON CONTROLLER (LocalScript)
|
||||
-- ═══════════════════════════════════════════════════════════════
|
||||
local weaponClient = Instance.new("LocalScript")
|
||||
weaponClient.Name = "WeaponClient"
|
||||
weaponClient.Parent = game:GetService("StarterPlayer"):FindFirstChild("StarterPlayerScripts")
|
||||
|
||||
weaponClient.Source = [[
|
||||
local Players = game:GetService("Players")
|
||||
local RS = game:GetService("ReplicatedStorage")
|
||||
local UIS = game:GetService("UserInputService")
|
||||
local RunService = game:GetService("RunService")
|
||||
local Events = RS:WaitForChild("Events")
|
||||
|
||||
local player = Players.LocalPlayer
|
||||
local camera = workspace.CurrentCamera
|
||||
local Weapons = require(RS:WaitForChild("Shared"):WaitForChild("WeaponData"))
|
||||
|
||||
-- State
|
||||
local currentWeapon = "M4A1"
|
||||
local weapon = Weapons[currentWeapon]
|
||||
local ammo = weapon.magSize
|
||||
local reserveAmmo = weapon.maxAmmo
|
||||
local isReloading = false
|
||||
local isADS = false
|
||||
local isSprinting = false
|
||||
local isFiring = false
|
||||
local lastFireTime = 0
|
||||
local canShoot = true
|
||||
|
||||
-- Recoil tracking
|
||||
local recoilX = 0
|
||||
local recoilY = 0
|
||||
local recoilRecoverySpeed = 8
|
||||
|
||||
-- Functions
|
||||
local function updateHUD()
|
||||
local hud = player.PlayerGui:FindFirstChild("FPS_HUD")
|
||||
if not hud then return end
|
||||
local frame = hud:FindFirstChild("MainFrame")
|
||||
if not frame then return end
|
||||
|
||||
local ammoText = frame:FindFirstChild("AmmoDisplay")
|
||||
if ammoText then ammoText.Text = ammo .. " / " .. reserveAmmo end
|
||||
|
||||
local weaponText = frame:FindFirstChild("WeaponName")
|
||||
if weaponText then weaponText.Text = weapon.displayName end
|
||||
|
||||
local healthBar = frame:FindFirstChild("HealthBar")
|
||||
local healthFill = frame:FindFirstChild("HealthFill")
|
||||
if healthBar and healthFill then
|
||||
local char = player.Character
|
||||
local hum = char and char:FindFirstChildOfClass("Humanoid")
|
||||
if hum then
|
||||
local pct = hum.Health / hum.MaxHealth
|
||||
healthFill.Size = UDim2.new(pct * 0.18, 0, 0.025, 0)
|
||||
if pct < 0.3 then
|
||||
healthFill.BackgroundColor3 = Color3.fromRGB(200, 30, 30)
|
||||
elseif pct < 0.6 then
|
||||
healthFill.BackgroundColor3 = Color3.fromRGB(200, 180, 30)
|
||||
else
|
||||
healthFill.BackgroundColor3 = Color3.fromRGB(30, 200, 30)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local scoreText = frame:FindFirstChild("ScoreDisplay")
|
||||
if scoreText then scoreText.Text = "KILLS: " .. tostring(player:GetAttribute("Kills") or 0) end
|
||||
end
|
||||
|
||||
local function shoot()
|
||||
if isReloading or ammo <= 0 or not canShoot then return end
|
||||
if tick() - lastFireTime < weapon.fireRate then return end
|
||||
|
||||
lastFireTime = tick()
|
||||
ammo = ammo - 1
|
||||
canShoot = false
|
||||
|
||||
-- Fire raycast
|
||||
local mousePos = UIS:GetMouseLocation()
|
||||
local ray = camera:ViewportPointToRay(mousePos.X, mousePos.Y)
|
||||
|
||||
local spreadMult = isADS and weapon.spread.ads or weapon.spread.hip
|
||||
local spread = CFrame.new(
|
||||
math.random(-100, 100) / 100 * spreadMult,
|
||||
math.random(-100, 100) / 100 * spreadMult,
|
||||
math.random(-100, 100) / 100 * spreadMult
|
||||
) * 0.01
|
||||
local direction = (ray.Direction.Unit + spread.Position).Unit
|
||||
|
||||
local pellets = weapon.pellets or 1
|
||||
for _ = 1, pellets do
|
||||
local hitRay = RaycastParams.new()
|
||||
hitRay.FilterDescendantsInstances = {player.Character or {}}
|
||||
hitRay.FilterType = Enum.RaycastFilterType.Exclude
|
||||
|
||||
local result = workspace:Raycast(ray.Origin, direction * weapon.range, hitRay)
|
||||
if result then
|
||||
Events:FindFirstChild("ShootEvent"):FireServer({
|
||||
origin = ray.Origin,
|
||||
direction = direction * weapon.range,
|
||||
hit = result.Instance,
|
||||
hitPos = result.Position,
|
||||
normal = result.Normal,
|
||||
weapon = currentWeapon,
|
||||
})
|
||||
|
||||
-- Muzzle flash visual
|
||||
local flash = Instance.new("Part")
|
||||
flash.Size = Vector3.new(0.3, 0.3, 0.3)
|
||||
flash.Shape = Enum.PartType.Ball
|
||||
flash.Color = Color3.fromRGB(255, 200, 50)
|
||||
flash.Material = Enum.Material.Neon
|
||||
flash.Anchored = true
|
||||
flash.CanCollide = false
|
||||
flash.Position = camera.CFrame.Position + camera.CFrame.LookVector * 3
|
||||
flash.Parent = workspace
|
||||
game:GetService("Debris"):AddItem(flash, 0.05)
|
||||
|
||||
-- Bullet trail
|
||||
local trail = Instance.new("Part")
|
||||
trail.Size = Vector3.new(0.1, 0.1, weapon.range)
|
||||
trail.CFrame = CFrame.new(camera.CFrame.Position, result.Position) * CFrame.new(0, 0, -weapon.range/2)
|
||||
trail.Anchored = true
|
||||
trail.CanCollide = false
|
||||
trail.Color = Color3.fromRGB(255, 220, 100)
|
||||
trail.Material = Enum.Material.Neon
|
||||
trail.Transparency = 0.5
|
||||
trail.Parent = workspace
|
||||
game:GetService("Debris"):AddItem(trail, 0.03)
|
||||
|
||||
-- Impact effect
|
||||
local impact = Instance.new("Part")
|
||||
impact.Size = Vector3.new(0.5, 0.5, 0.5)
|
||||
impact.Shape = Enum.PartType.Ball
|
||||
impact.Color = Color3.fromRGB(255, 150, 50)
|
||||
impact.Material = Enum.Material.Neon
|
||||
impact.Anchored = true
|
||||
impact.CanCollide = false
|
||||
impact.Position = result.Position
|
||||
impact.Parent = workspace
|
||||
game:GetService("Debris"):AddItem(impact, 0.1)
|
||||
end
|
||||
end
|
||||
|
||||
-- Apply recoil
|
||||
if isADS then
|
||||
recoilX = recoilX - weapon.recoil.x * 0.4
|
||||
recoilY = recoilY + weapon.recoil.y * 0.4
|
||||
else
|
||||
recoilX = recoilX - weapon.recoil.x
|
||||
recoilY = recoilY + weapon.recoil.y
|
||||
end
|
||||
|
||||
-- Screen shake
|
||||
local shake = isADS and 0.002 or 0.005
|
||||
camera.CFrame = camera.CFrame * CFrame.new(
|
||||
math.random(-100,100)/100 * shake,
|
||||
math.random(-100,100)/100 * shake,
|
||||
0
|
||||
)
|
||||
|
||||
task.wait(weapon.fireRate)
|
||||
canShoot = true
|
||||
updateHUD()
|
||||
|
||||
if ammo <= 0 then
|
||||
reload()
|
||||
end
|
||||
end
|
||||
|
||||
function reload()
|
||||
if isReloading or reserveAmmo <= 0 then return end
|
||||
isReloading = true
|
||||
|
||||
Events:FindFirstChild("ReloadEvent"):FireServer()
|
||||
|
||||
task.wait(weapon.reloadTime)
|
||||
|
||||
local needed = weapon.magSize - ammo
|
||||
local available = math.min(needed, reserveAmmo)
|
||||
ammo = ammo + available
|
||||
reserveAmmo = reserveAmmo - available
|
||||
isReloading = false
|
||||
updateHUD()
|
||||
end
|
||||
|
||||
-- Input handling
|
||||
UIS.InputBegan:Connect(function(input, processed)
|
||||
if processed then return end
|
||||
|
||||
if input.UserInputType == Enum.UserInputType.MouseButton2 then
|
||||
isADS = true
|
||||
if isSprinting then isSprinting = false end
|
||||
end
|
||||
|
||||
if input.UserInputType == Enum.UserInputType.MouseButton1 then
|
||||
isFiring = true
|
||||
if isSprinting then isSprinting = false end
|
||||
end
|
||||
|
||||
if input.KeyCode == Enum.KeyCode.LeftShift then
|
||||
if not isADS then isSprinting = true end
|
||||
end
|
||||
|
||||
if input.KeyCode == Enum.KeyCode.LeftControl then
|
||||
local char = player.Character
|
||||
if char then
|
||||
local hum = char:FindFirstChildOfClass("Humanoid")
|
||||
if hum then hum.WalkSpeed = 8 end
|
||||
end
|
||||
end
|
||||
|
||||
if input.KeyCode == Enum.KeyCode.R then
|
||||
reload()
|
||||
end
|
||||
|
||||
-- Weapon switch: 1-4
|
||||
if input.KeyCode == Enum.KeyCode.One then currentWeapon = "M4A1" end
|
||||
if input.KeyCode == Enum.KeyCode.Two then currentWeapon = "AK47" end
|
||||
if input.KeyCode == Enum.KeyCode.Three then currentWeapon = "Sniper" end
|
||||
if input.KeyCode == Enum.KeyCode.Four then currentWeapon = "Shotgun" end
|
||||
|
||||
if input.KeyCode >= Enum.KeyCode.One and input.KeyCode <= Enum.KeyCode.Four then
|
||||
weapon = Weapons[currentWeapon]
|
||||
ammo = weapon.magSize
|
||||
reserveAmmo = weapon.maxAmmo
|
||||
isReloading = false
|
||||
updateHUD()
|
||||
end
|
||||
end)
|
||||
|
||||
UIS.InputEnded:Connect(function(input)
|
||||
if input.UserInputType == Enum.UserInputType.MouseButton2 then
|
||||
isADS = false
|
||||
end
|
||||
if input.UserInputType == Enum.UserInputType.MouseButton1 then
|
||||
isFiring = false
|
||||
end
|
||||
if input.KeyCode == Enum.KeyCode.LeftShift then
|
||||
isSprinting = false
|
||||
end
|
||||
if input.KeyCode == Enum.KeyCode.LeftControl then
|
||||
local char = player.Character
|
||||
if char then
|
||||
local hum = char:FindFirstChildOfClass("Humanoid")
|
||||
if hum then hum.WalkSpeed = 20 end
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
-- Main loop
|
||||
RunService.RenderStepped:Connect(function()
|
||||
-- Camera FOV for ADS
|
||||
local targetFOV = isADS and weapon.adsFOV or 70
|
||||
camera.FieldOfView = camera.FieldOfView + (targetFOV - camera.FieldOfView) * 0.2
|
||||
|
||||
-- Sprint speed
|
||||
local char = player.Character
|
||||
if char then
|
||||
local hum = char:FindFirstChildOfClass("Humanoid")
|
||||
if hum and hum.MoveDirection.Magnitude > 0 then
|
||||
if isSprinting then
|
||||
hum.WalkSpeed = 30
|
||||
elseif not UIS:IsKeyDown(Enum.KeyCode.LeftControl) then
|
||||
hum.WalkSpeed = 20
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Auto-fire
|
||||
if isFiring and weapon.automatic then
|
||||
shoot()
|
||||
end
|
||||
|
||||
-- Recoil recovery
|
||||
recoilX = recoilX + (0 - recoilX) * math.clamp(recoilRecoverySpeed * 0.01, 0, 1)
|
||||
recoilY = recoilY + (0 - recoilY) * math.clamp(recoilRecoverySpeed * 0.01, 0, 1)
|
||||
|
||||
updateHUD()
|
||||
end)
|
||||
|
||||
-- Lock mouse for FPS
|
||||
UIS.MouseIconEnabled = false
|
||||
|
||||
player.CharacterAdded:Connect(function()
|
||||
ammo = weapon.magSize
|
||||
reserveAmmo = weapon.maxAmmo
|
||||
updateHUD()
|
||||
end)
|
||||
|
||||
updateHUD()
|
||||
print("[WeaponClient] Loaded - Controls: LMB=Shoot, RMB=ADS, Shift=Sprint, Ctrl=Crouch, R=Reload, 1-4=Switch weapon")
|
||||
]]
|
||||
|
||||
print("[CoD FPS] Part 2/5 complete: Weapon system created.")
|
||||
542
examples/fps-game/part3_ai.lua
Normal file
542
examples/fps-game/part3_ai.lua
Normal file
@@ -0,0 +1,542 @@
|
||||
-- ═══════════════════════════════════════════════════════════════════
|
||||
-- MINI CALL OF DUTY - FPS Game Setup (Part 3: Enemy AI + Server Handler)
|
||||
-- ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
local SSS = game:GetService("ServerScriptService")
|
||||
local RS = game:GetService("ReplicatedStorage")
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════
|
||||
-- SERVER GAME HANDLER
|
||||
-- ═══════════════════════════════════════════════════════════════
|
||||
local serverScript = Instance.new("Script")
|
||||
serverScript.Name = "GameServer"
|
||||
serverScript.Parent = SSS
|
||||
serverScript.Source = [[
|
||||
local Players = game:GetService("Players")
|
||||
local RS = game:GetService("ReplicatedStorage")
|
||||
local Events = RS:WaitForChild("Events")
|
||||
local Shared = RS:WaitForChild("Shared")
|
||||
local WeaponData = require(Shared:WaitForChild("WeaponData"))
|
||||
|
||||
local scores = {}
|
||||
local killFeed = {}
|
||||
|
||||
-- Player setup
|
||||
Players.PlayerAdded:Connect(function(player)
|
||||
scores[player.UserId] = {kills = 0, deaths = 0, streak = 0}
|
||||
|
||||
player.CharacterAdded:Connect(function(char)
|
||||
task.wait(0.5)
|
||||
local hum = char:WaitForChild("Humanoid")
|
||||
hum.MaxHealth = 100
|
||||
hum.Health = 100
|
||||
hum.WalkSpeed = 20
|
||||
|
||||
-- Give starter weapon visual
|
||||
local tool = Instance.new("Tool")
|
||||
tool.Name = "M4A1"
|
||||
tool.RequiresHandle = true
|
||||
tool.CanBeDropped = false
|
||||
|
||||
local handle = Instance.new("Part")
|
||||
handle.Name = "Handle"
|
||||
handle.Size = Vector3.new(0.5, 0.5, 3)
|
||||
handle.Color = Color3.fromRGB(40, 40, 40)
|
||||
handle.Material = Enum.Material.SmoothPlastic
|
||||
handle.CanCollide = false
|
||||
handle.Anchored = false
|
||||
handle.Parent = tool
|
||||
tool.Parent = player.Backpack
|
||||
|
||||
-- Gun body
|
||||
local barrel = Instance.new("Part")
|
||||
barrel.Name = "Barrel"
|
||||
barrel.Size = Vector3.new(0.2, 0.2, 2)
|
||||
barrel.Color = Color3.fromRGB(30, 30, 30)
|
||||
barrel.Material = Enum.Material.Metal
|
||||
barrel.CanCollide = false
|
||||
barrel.Anchored = false
|
||||
local weld = Instance.new("WeldConstraint")
|
||||
weld.Part0 = handle
|
||||
weld.Part1 = barrel
|
||||
weld.Parent = barrel
|
||||
barrel.CFrame = handle.CFrame * CFrame.new(0, 0.1, -2)
|
||||
barrel.Parent = tool
|
||||
|
||||
-- Magazine
|
||||
local mag = Instance.new("Part")
|
||||
mag.Name = "Magazine"
|
||||
mag.Size = Vector3.new(0.3, 0.8, 0.4)
|
||||
mag.Color = Color3.fromRGB(35, 35, 35)
|
||||
mag.Material = Enum.Material.SmoothPlastic
|
||||
mag.CanCollide = false
|
||||
mag.Anchored = false
|
||||
local weld2 = Instance.new("WeldConstraint")
|
||||
weld2.Part0 = handle
|
||||
weld2.Part1 = mag
|
||||
weld2.Parent = mag
|
||||
mag.CFrame = handle.CFrame * CFrame.new(0, -0.5, -0.5)
|
||||
mag.Parent = tool
|
||||
|
||||
-- Health regeneration
|
||||
task.spawn(function()
|
||||
while hum and hum.Health > 0 do
|
||||
task.wait(3)
|
||||
if hum.Health < hum.MaxHealth and hum.Health > 0 then
|
||||
hum.Health = math.min(hum.MaxHealth, hum.Health + 5)
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
-- Death handler
|
||||
hum.Died:Connect(function()
|
||||
scores[player.UserId].deaths = scores[player.UserId].deaths + 1
|
||||
scores[player.UserId].streak = 0
|
||||
|
||||
-- Respawn after 5 seconds
|
||||
task.delay(5, function()
|
||||
if player then
|
||||
player:LoadCharacter()
|
||||
end
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
|
||||
-- Handle hit events from clients
|
||||
Events:WaitForChild("ShootEvent").OnServerEvent:Connect(function(player, data)
|
||||
-- Validate the shot
|
||||
if not data or not data.hit then return end
|
||||
|
||||
local hitObj = data.hit
|
||||
local weaponName = data.weapon or "M4A1"
|
||||
local weapon = WeaponData[weaponName]
|
||||
if not weapon then return end
|
||||
|
||||
-- Check range
|
||||
local char = player.Character
|
||||
if not char then return end
|
||||
local dist = (data.hitPos - char.Head.Position).Magnitude
|
||||
if dist > weapon.range then return end
|
||||
|
||||
-- Find the humanoid of what was hit
|
||||
local targetHum = nil
|
||||
local isHeadshot = false
|
||||
|
||||
if hitObj:IsA("Model") then
|
||||
targetHum = hitObj:FindFirstChildOfClass("Humanoid")
|
||||
elseif hitObj.Parent and hitObj.Parent:IsA("Model") then
|
||||
targetHum = hitObj.Parent:FindFirstChildOfClass("Humanoid")
|
||||
elseif hitObj.Parent and hitObj.Parent.Parent and hitObj.Parent.Parent:IsA("Model") then
|
||||
targetHum = hitObj.Parent.Parent:FindFirstChildOfClass("Humanoid")
|
||||
end
|
||||
|
||||
-- Check headshot
|
||||
if hitObj.Name == "Head" and targetHum then
|
||||
isHeadshot = true
|
||||
end
|
||||
|
||||
-- Apply damage
|
||||
if targetHum then
|
||||
local dmg = weapon.damage
|
||||
if isHeadshot then dmg = dmg * weapon.headshotMult end
|
||||
|
||||
targetHum:TakeDamage(dmg)
|
||||
|
||||
-- Check if killed
|
||||
if targetHum.Health <= 0 then
|
||||
local victim = nil
|
||||
for _, p in ipairs(Players:GetPlayers()) do
|
||||
if p.Character and p.Character:FindFirstChildOfClass("Humanoid") == targetHum then
|
||||
victim = p
|
||||
break
|
||||
end
|
||||
end
|
||||
|
||||
if victim then
|
||||
scores[player.UserId].kills = scores[player.UserId].kills + 1
|
||||
scores[player.UserId].streak = scores[player.UserId].streak + 1
|
||||
end
|
||||
|
||||
-- Fire kill event
|
||||
Events:WaitForChild("KillEvent"):FireAllClients({
|
||||
killer = player.Name,
|
||||
victim = victim and victim.Name or "Enemy",
|
||||
weapon = weaponName,
|
||||
headshot = isHeadshot,
|
||||
streak = scores[player.UserId].streak,
|
||||
})
|
||||
end
|
||||
|
||||
-- Fire damage indicator
|
||||
Events:WaitForChild("DamageEvent"):FireClient(player, {
|
||||
hit = true,
|
||||
headshot = isHeadshot,
|
||||
damage = dmg,
|
||||
})
|
||||
end
|
||||
end)
|
||||
|
||||
-- Game data request
|
||||
Events:WaitForChild("GetGameData").OnServerInvoke = function(player)
|
||||
return {
|
||||
scores = scores,
|
||||
killFeed = killFeed,
|
||||
}
|
||||
end
|
||||
|
||||
-- Kill feed relay
|
||||
Events:WaitForChild("KillEvent").OnServerEvent:Connect(function(player, data)
|
||||
table.insert(killFeed, 1, data)
|
||||
if #killFeed > 5 then table.remove(killFeed) end
|
||||
end)
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════
|
||||
-- ENEMY AI SYSTEM
|
||||
-- ═══════════════════════════════════════════════════════════════
|
||||
local enemySpawns = {
|
||||
Vector3.new(60, 2, 80), Vector3.new(-60, 2, 80),
|
||||
Vector3.new(70, 2, -80), Vector3.new(-70, 2, -80),
|
||||
Vector3.new(0, 2, 100), Vector3.new(0, 2, -80),
|
||||
Vector3.new(50, 2, 0), Vector3.new(-50, 2, 0),
|
||||
Vector3.new(-40, 2, 50), Vector3.new(40, 2, -50),
|
||||
Vector3.new(-80, 2, 30), Vector3.new(80, 2, -30),
|
||||
}
|
||||
|
||||
local enemies = {}
|
||||
local MAX_ENEMIES = 10
|
||||
local SPAWN_INTERVAL = 8
|
||||
|
||||
local function createEnemy(pos)
|
||||
local model = Instance.new("Model")
|
||||
model.Name = "Enemy_" .. tostring(#enemies + 1)
|
||||
|
||||
-- Humanoid
|
||||
local hum = Instance.new("Humanoid")
|
||||
hum.MaxHealth = 80
|
||||
hum.Health = 80
|
||||
hum.WalkSpeed = 14
|
||||
hum.Parent = model
|
||||
|
||||
-- Head
|
||||
local head = Instance.new("Part")
|
||||
head.Name = "Head"
|
||||
head.Size = Vector3.new(1.5, 1.5, 1.5)
|
||||
head.Color = Color3.fromRGB(180, 140, 110)
|
||||
head.Material = Enum.Material.SmoothPlastic
|
||||
head.CanCollide = true
|
||||
head.Parent = model
|
||||
local headMesh = Instance.new("SpecialMesh")
|
||||
headMesh.MeshType = Enum.MeshType.Head
|
||||
headMesh.Scale = Vector3.new(1.25, 1.25, 1.25)
|
||||
headMesh.Parent = head
|
||||
|
||||
-- Torso
|
||||
local torso = Instance.new("Part")
|
||||
torso.Name = "HumanoidRootPart"
|
||||
torso.Size = Vector3.new(2, 2, 1)
|
||||
torso.Color = Color3.fromRGB(60, 80, 40) -- Military green
|
||||
torso.Material = Enum.Material.SmoothPlastic
|
||||
torso.CanCollide = true
|
||||
torso.Parent = model
|
||||
|
||||
-- Legs
|
||||
local lleg = Instance.new("Part")
|
||||
lleg.Name = "Left Leg"
|
||||
lleg.Size = Vector3.new(1, 2, 1)
|
||||
lleg.Color = Color3.fromRGB(50, 55, 45)
|
||||
lleg.Material = Enum.Material.SmoothPlastic
|
||||
lleg.Parent = model
|
||||
|
||||
local rleg = Instance.new("Part")
|
||||
rleg.Name = "Right Leg"
|
||||
rleg.Size = Vector3.new(1, 2, 1)
|
||||
rleg.Color = Color3.fromRGB(50, 55, 45)
|
||||
rleg.Material = Enum.Material.SmoothPlastic
|
||||
rleg.Parent = model
|
||||
|
||||
-- Arms
|
||||
local larm = Instance.new("Part")
|
||||
larm.Name = "Left Arm"
|
||||
larm.Size = Vector3.new(1, 2, 1)
|
||||
larm.Color = Color3.fromRGB(60, 80, 40)
|
||||
larm.Material = Enum.Material.SmoothPlastic
|
||||
larm.Parent = model
|
||||
|
||||
local rarm = Instance.new("Part")
|
||||
rarm.Name = "Right Arm"
|
||||
rarm.Size = Vector3.new(1, 2, 1)
|
||||
rarm.Color = Color3.fromRGB(60, 80, 40)
|
||||
rarm.Material = Enum.Material.SmoothPlastic
|
||||
rarm.Parent = model
|
||||
|
||||
-- Motor6D connections
|
||||
local function weld(part0, part1, c0, c1)
|
||||
local m = Instance.new("Motor6D")
|
||||
m.Part0 = part0
|
||||
m.Part1 = part1
|
||||
if c0 then m.C0 = c0 end
|
||||
if c1 then m.C1 = c1 end
|
||||
m.Parent = part0
|
||||
end
|
||||
|
||||
weld(torso, head, CFrame.new(0, 1.5, 0), CFrame.new(0, 0, 0))
|
||||
weld(torso, larm, CFrame.new(-1.5, 0, 0), CFrame.new(0.5, 0, 0))
|
||||
weld(torso, rarm, CFrame.new(1.5, 0, 0), CFrame.new(-0.5, 0, 0))
|
||||
weld(torso, lleg, CFrame.new(-0.5, -2, 0), CFrame.new(0, 1, 0))
|
||||
weld(torso, rleg, CFrame.new(0.5, -2, 0), CFrame.new(0, 1, 0))
|
||||
|
||||
-- Beret/hat
|
||||
local hat = Instance.new("Part")
|
||||
hat.Name = "Hat"
|
||||
hat.Size = Vector3.new(1.8, 0.5, 1.8)
|
||||
hat.Color = Color3.fromRGB(40, 50, 30)
|
||||
hat.Material = Enum.Material.SmoothPlastic
|
||||
hat.CanCollide = false
|
||||
hat.Parent = model
|
||||
local hatWeld = Instance.new("WeldConstraint")
|
||||
hatWeld.Part0 = head
|
||||
hatWeld.Part1 = hat
|
||||
hatWeld.Parent = hat
|
||||
hat.CFrame = head.CFrame * CFrame.new(0, 0.9, 0)
|
||||
|
||||
-- Health bar above head
|
||||
local billboard = Instance.new("BillboardGui")
|
||||
billboard.Name = "HealthBar"
|
||||
billboard.Size = UDim2.new(3, 0, 0.4, 0)
|
||||
billboard.StudsOffset = Vector3.new(0, 3.5, 0)
|
||||
billboard.AlwaysOnTop = true
|
||||
billboard.Parent = head
|
||||
|
||||
local bg = Instance.new("Frame")
|
||||
bg.Size = UDim2.new(1, 0, 1, 0)
|
||||
bg.BackgroundColor3 = Color3.fromRGB(40, 40, 40)
|
||||
bg.BorderSizePixel = 0
|
||||
bg.Parent = billboard
|
||||
|
||||
local fill = Instance.new("Frame")
|
||||
fill.Name = "Fill"
|
||||
fill.Size = UDim2.new(1, 0, 1, 0)
|
||||
fill.BackgroundColor3 = Color3.fromRGB(200, 30, 30)
|
||||
fill.BorderSizePixel = 0
|
||||
fill.Parent = bg
|
||||
|
||||
-- AI Script
|
||||
local aiScript = Instance.new("Script")
|
||||
aiScript.Source = [[
|
||||
local humanoid = script.Parent:FindFirstChildOfClass("Humanoid")
|
||||
local rootPart = script.Parent:FindFirstChild("HumanoidRootPart")
|
||||
local head = script.Parent:FindFirstChild("Head")
|
||||
local healthFill = script.Parent:FindFirstChild("Head"):FindFirstChild("HealthBar"):FindFirstChild("Frame"):FindFirstChild("Fill")
|
||||
|
||||
if not humanoid or not rootPart then return end
|
||||
|
||||
local state = "patrol"
|
||||
local target = nil
|
||||
local lastShot = 0
|
||||
local fireRate = 1.2
|
||||
local damage = 12
|
||||
local detectionRange = 80
|
||||
local attackRange = 60
|
||||
local patrolPoints = {}
|
||||
local currentPatrolIndex = 1
|
||||
|
||||
-- Generate patrol points
|
||||
for i = 1, 6 do
|
||||
local angle = math.rad(math.random(360))
|
||||
local dist = math.random(15, 60)
|
||||
table.insert(patrolPoints, rootPart.Position + Vector3.new(math.cos(angle)*dist, 0, math.sin(angle)*dist))
|
||||
end
|
||||
|
||||
-- Find closest player
|
||||
local function findTarget()
|
||||
local closest = nil
|
||||
local closestDist = detectionRange
|
||||
for _, player in ipairs(game:GetService("Players"):GetPlayers()) do
|
||||
if player.Character then
|
||||
local hum = player.Character:FindFirstChildOfClass("Humanoid")
|
||||
if hum and hum.Health > 0 then
|
||||
local dist = (player.Character.Head.Position - rootPart.Position).Magnitude
|
||||
if dist < closestDist then
|
||||
closest = player
|
||||
closestDist = dist
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return closest, closestDist
|
||||
end
|
||||
|
||||
-- Update health bar
|
||||
humanoid.HealthChanged:Connect(function(health)
|
||||
local pct = health / humanoid.MaxHealth
|
||||
healthFill.Size = UDim2.new(pct, 0, 1, 0)
|
||||
if pct < 0.3 then
|
||||
healthFill.BackgroundColor3 = Color3.fromRGB(200, 30, 30)
|
||||
else
|
||||
healthFill.BackgroundColor3 = Color3.fromRGB(200, 60, 30)
|
||||
end
|
||||
end)
|
||||
|
||||
-- Main AI loop
|
||||
while humanoid and humanoid.Health > 0 do
|
||||
task.wait(0.3)
|
||||
|
||||
target, _ = findTarget()
|
||||
|
||||
if target and target.Character then
|
||||
local targetHum = target.Character:FindFirstChildOfClass("Humanoid")
|
||||
local targetHead = target.Character:FindFirstChild("Head")
|
||||
|
||||
if targetHum and targetHum.Health > 0 and targetHead then
|
||||
local dist = (targetHead.Position - rootPart.Position).Magnitude
|
||||
|
||||
-- Face target
|
||||
rootPart.CFrame = CFrame.new(rootPart.Position, Vector3.new(targetHead.Position.X, rootPart.Position.Y, targetHead.Position.Z))
|
||||
|
||||
if dist <= attackRange then
|
||||
state = "attack"
|
||||
-- Shoot at player
|
||||
if tick() - lastShot > fireRate then
|
||||
lastShot = tick()
|
||||
|
||||
-- Raycast to player
|
||||
local direction = (targetHead.Position - head.Position).Unit
|
||||
local spread = Vector3.new(math.random()-0.5, math.random()-0.5, math.random()-0.5) * 2
|
||||
local hitResult = workspace:Raycast(head.Position, (direction + spread) * attackRange)
|
||||
|
||||
if hitResult then
|
||||
-- Muzzle flash
|
||||
local flash = Instance.new("Part")
|
||||
flash.Size = Vector3.new(0.3, 0.3, 0.3)
|
||||
flash.Shape = Enum.PartType.Ball
|
||||
flash.Color = Color3.fromRGB(255, 200, 50)
|
||||
flash.Material = Enum.Material.Neon
|
||||
flash.Anchored = true
|
||||
flash.CanCollide = false
|
||||
flash.Position = head.Position + direction * 2
|
||||
flash.Parent = workspace
|
||||
game:GetService("Debris"):AddItem(flash, 0.08)
|
||||
|
||||
-- Bullet trail
|
||||
local trail = Instance.new("Part")
|
||||
trail.Size = Vector3.new(0.1, 0.1, (head.Position - hitResult.Position).Magnitude)
|
||||
trail.CFrame = CFrame.new(head.Position, hitResult.Position) * CFrame.new(0, 0, -trail.Size.Z/2)
|
||||
trail.Anchored = true
|
||||
trail.CanCollide = false
|
||||
trail.Color = Color3.fromRGB(255, 200, 100)
|
||||
trail.Material = Enum.Material.Neon
|
||||
trail.Transparency = 0.3
|
||||
trail.Parent = workspace
|
||||
game:GetService("Debris"):AddItem(trail, 0.1)
|
||||
|
||||
-- Check if hit player
|
||||
local hitChar = hitResult.Instance
|
||||
if hitChar then
|
||||
local hitHum = nil
|
||||
if hitChar.Parent and hitChar.Parent:FindFirstChildOfClass("Humanoid") then
|
||||
hitHum = hitChar.Parent:FindFirstChildOfClass("Humanoid")
|
||||
elseif hitChar.Parent and hitChar.Parent.Parent and hitChar.Parent.Parent:FindFirstChildOfClass("Humanoid") then
|
||||
hitHum = hitChar.Parent.Parent:FindFirstChildOfClass("Humanoid")
|
||||
end
|
||||
|
||||
if hitHum and hitHum ~= humanoid then
|
||||
hitHum:TakeDamage(damage)
|
||||
end
|
||||
end
|
||||
|
||||
-- Impact effect
|
||||
local impact = Instance.new("Part")
|
||||
impact.Size = Vector3.new(0.5, 0.5, 0.5)
|
||||
impact.Shape = Enum.PartType.Ball
|
||||
impact.Color = Color3.fromRGB(255, 150, 50)
|
||||
impact.Material = Enum.Material.Neon
|
||||
impact.Anchored = true
|
||||
impact.CanCollide = false
|
||||
impact.Position = hitResult.Position
|
||||
impact.Parent = workspace
|
||||
game:GetService("Debris"):AddItem(impact, 0.15)
|
||||
end
|
||||
end
|
||||
else
|
||||
-- Move toward target
|
||||
state = "chase"
|
||||
humanoid:MoveTo(targetHead.Position)
|
||||
end
|
||||
end
|
||||
else
|
||||
-- Patrol
|
||||
state = "patrol"
|
||||
if #patrolPoints > 0 then
|
||||
humanoid:MoveTo(patrolPoints[currentPatrolIndex])
|
||||
humanoid.MoveToFinished:Connect(function(reached)
|
||||
if reached then
|
||||
currentPatrolIndex = (currentPatrolIndex % #patrolPoints) + 1
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Death effect
|
||||
local root = script.Parent:FindFirstChild("HumanoidRootPart")
|
||||
if root then
|
||||
for _, v in ipairs(script.Parent:GetDescendants()) do
|
||||
if v:IsA("BasePart") then
|
||||
v.Anchored = false
|
||||
v.BrickColor = BrickColor.new("Dark stone grey")
|
||||
local bf = Instance.new("BodyForce")
|
||||
bf.Force = Vector3.new(math.random(-50,50), 100, math.random(-50,50))
|
||||
bf.Parent = v
|
||||
game:GetService("Debris"):AddItem(v, 3)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
task.delay(3, function()
|
||||
if script.Parent then script.Parent:Destroy() end
|
||||
end)
|
||||
]]
|
||||
aiScript.Parent = model
|
||||
|
||||
-- Position
|
||||
local primary = torso
|
||||
model.PrimaryPart = primary
|
||||
primary.Position = pos
|
||||
head.Position = pos + Vector3.new(0, 2.5, 0)
|
||||
lleg.Position = pos + Vector3.new(-0.5, -1, 0)
|
||||
rleg.Position = pos + Vector3.new(0.5, -1, 0)
|
||||
larm.Position = pos + Vector3.new(-1.5, 0, 0)
|
||||
rarm.Position = pos + Vector3.new(1.5, 0, 0)
|
||||
|
||||
model.Parent = workspace
|
||||
table.insert(enemies, model)
|
||||
return model
|
||||
end
|
||||
|
||||
-- Enemy spawner loop
|
||||
task.spawn(function()
|
||||
task.wait(5) -- Initial delay
|
||||
while true do
|
||||
-- Remove dead enemies
|
||||
for i = #enemies, 1, -1 do
|
||||
if not enemies[i] or not enemies[i]:FindFirstChildOfClass("Humanoid")
|
||||
or enemies[i]:FindFirstChildOfClass("Humanoid").Health <= 0 then
|
||||
table.remove(enemies, i)
|
||||
end
|
||||
end
|
||||
|
||||
-- Spawn new enemies
|
||||
if #enemies < MAX_ENEMIES then
|
||||
local pos = enemySpawns[math.random(#enemySpawns)]
|
||||
createEnemy(pos + Vector3.new(math.random(-5,5), 0, math.random(-5,5)))
|
||||
end
|
||||
|
||||
task.wait(SPAWN_INTERVAL)
|
||||
end
|
||||
end)
|
||||
|
||||
print("[GameServer] Enemy AI system active. Spawning " .. MAX_ENEMIES .. " enemies.")
|
||||
]]
|
||||
|
||||
print("[CoD FPS] Part 3/5 complete: Server handler + Enemy AI created.")
|
||||
421
examples/fps-game/part4_hud.lua
Normal file
421
examples/fps-game/part4_hud.lua
Normal file
@@ -0,0 +1,421 @@
|
||||
-- ═══════════════════════════════════════════════════════════════════
|
||||
-- MINI CALL OF DUTY - FPS Game Setup (Part 4: HUD + Player Scripts)
|
||||
-- ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
local SG = game:GetService("StarterGui")
|
||||
local SP = game:GetService("StarterPlayer")
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════
|
||||
-- HUD (ScreenGui + LocalScript)
|
||||
-- ═══════════════════════════════════════════════════════════════
|
||||
local hudGui = Instance.new("ScreenGui")
|
||||
hudGui.Name = "FPS_HUD"
|
||||
hudGui.ResetOnSpawn = false
|
||||
hudGui.IgnoreGuiInset = true
|
||||
hudGui.Parent = SG
|
||||
|
||||
-- Crosshair
|
||||
local cross = Instance.new("Frame")
|
||||
cross.Name = "Crosshair"
|
||||
cross.Size = UDim2.new(0, 20, 0, 20)
|
||||
cross.Position = UDim2.new(0.5, -10, 0.5, -10)
|
||||
cross.BackgroundTransparency = 1
|
||||
cross.Parent = hudGui
|
||||
|
||||
for _, dir in ipairs({{0,-12,0,4,"Top"},{0,4,0,12,"Bottom"},{-12,0,4,0,"Left"},{4,0,12,0,"Right"}}) do
|
||||
local line = Instance.new("Frame")
|
||||
line.Name = dir[5]
|
||||
line.Size = UDim2.new(0, dir[3] == 0 and 2 or dir[3], 0, dir[4] == 0 and 2 or dir[4])
|
||||
line.Position = UDim2.new(0, 9+dir[1], 0, 9+dir[2])
|
||||
line.BackgroundColor3 = Color3.fromRGB(255, 255, 255)
|
||||
line.BackgroundTransparency = 0.2
|
||||
line.BorderSizePixel = 0
|
||||
line.Parent = cross
|
||||
end
|
||||
|
||||
-- Center dot
|
||||
local dot = Instance.new("Frame")
|
||||
dot.Name = "CenterDot"
|
||||
dot.Size = UDim2.new(0, 3, 0, 3)
|
||||
dot.Position = UDim2.new(0, 8, 0, 8)
|
||||
dot.BackgroundColor3 = Color3.fromRGB(255, 50, 50)
|
||||
dot.BorderSizePixel = 0
|
||||
dot.Parent = cross
|
||||
|
||||
-- Hit marker (appears on hit)
|
||||
local hitMarker = Instance.new("Frame")
|
||||
hitMarker.Name = "HitMarker"
|
||||
hitMarker.Size = UDim2.new(0, 30, 0, 30)
|
||||
hitMarker.Position = UDim2.new(0.5, -15, 0.5, -15)
|
||||
hitMarker.BackgroundTransparency = 1
|
||||
hitMarker.Visible = false
|
||||
hitMarker.Parent = hudGui
|
||||
|
||||
for _, d in ipairs({{-8,-8,6,6,45},{2,-8,6,6,-45},{-8,2,6,6,-45},{2,2,6,6,45}}) do
|
||||
local mark = Instance.new("Frame")
|
||||
mark.Size = UDim2.new(0, d[3], 0, d[4])
|
||||
mark.Position = UDim2.new(0, 12+d[1], 0, 12+d[2])
|
||||
mark.BackgroundColor3 = Color3.fromRGB(255, 255, 255)
|
||||
mark.BorderSizePixel = 0
|
||||
mark.Rotation = d[5]
|
||||
mark.Parent = hitMarker
|
||||
end
|
||||
|
||||
-- Damage vignette overlay
|
||||
local dmgVignette = Instance.new("Frame")
|
||||
dmgVignette.Name = "DamageVignette"
|
||||
dmgVignette.Size = UDim2.new(1, 0, 1, 0)
|
||||
dmgVignette.BackgroundColor3 = Color3.fromRGB(200, 0, 0)
|
||||
dmgVignette.BackgroundTransparency = 1
|
||||
dmgVignette.BorderSizePixel = 0
|
||||
dmgVignette.ZIndex = 9
|
||||
dmgVignette.Parent = hudGui
|
||||
|
||||
-- Kill feed frame (top right)
|
||||
local killFeedFrame = Instance.new("Frame")
|
||||
killFeedFrame.Name = "KillFeed"
|
||||
killFeedFrame.Size = UDim2.new(0, 350, 0, 150)
|
||||
killFeedFrame.Position = UDim2.new(1, -360, 0, 10)
|
||||
killFeedFrame.BackgroundTransparency = 1
|
||||
killFeedFrame.Parent = hudGui
|
||||
|
||||
-- Score display (top center)
|
||||
local scoreFrame = Instance.new("Frame")
|
||||
scoreFrame.Name = "ScoreFrame"
|
||||
scoreFrame.Size = UDim2.new(0, 200, 0, 40)
|
||||
scoreFrame.Position = UDim2.new(0.5, -100, 0, 10)
|
||||
scoreFrame.BackgroundColor3 = Color3.fromRGB(0, 0, 0)
|
||||
scoreFrame.BackgroundTransparency = 0.5
|
||||
scoreFrame.BorderSizePixel = 0
|
||||
scoreFrame.Parent = hudGui
|
||||
|
||||
local scoreLabel = Instance.new("TextLabel")
|
||||
scoreLabel.Name = "ScoreLabel"
|
||||
scoreLabel.Size = UDim2.new(1, 0, 1, 0)
|
||||
scoreLabel.BackgroundTransparency = 1
|
||||
scoreLabel.Text = "KILLS: 0 | DEATHS: 0"
|
||||
scoreLabel.TextColor3 = Color3.fromRGB(255, 255, 255)
|
||||
scoreLabel.TextSize = 18
|
||||
scoreLabel.Font = Enum.Font.GothamBold
|
||||
scoreLabel.Parent = scoreFrame
|
||||
|
||||
-- Killstreak banner (center)
|
||||
local streakBanner = Instance.new("TextLabel")
|
||||
streakBanner.Name = "StreakBanner"
|
||||
streakBanner.Size = UDim2.new(0, 400, 0, 60)
|
||||
streakBanner.Position = UDim2.new(0.5, -200, 0.3, 0)
|
||||
streakBanner.BackgroundTransparency = 1
|
||||
streakBanner.Text = ""
|
||||
streakBanner.TextColor3 = Color3.fromRGB(255, 200, 50)
|
||||
streakBanner.TextSize = 32
|
||||
streakBanner.Font = Enum.Font.GothamBold
|
||||
streakBanner.TextStrokeTransparency = 0
|
||||
streakBanner.Visible = false
|
||||
streakBanner.ZIndex = 10
|
||||
streakBanner.Parent = hudGui
|
||||
|
||||
-- Minimap (top left)
|
||||
local minimap = Instance.new("Frame")
|
||||
minimap.Name = "Minimap"
|
||||
minimap.Size = UDim2.new(0, 150, 0, 150)
|
||||
minimap.Position = UDim2.new(0, 10, 0, 10)
|
||||
minimap.BackgroundColor3 = Color3.fromRGB(30, 40, 30)
|
||||
minimap.BackgroundTransparency = 0.3
|
||||
minimap.BorderSizePixel = 0
|
||||
minimap.Parent = hudGui
|
||||
|
||||
local mapCorner = Instance.new("UICorner")
|
||||
mapCorner.CornerRadius = UDim.new(0, 75)
|
||||
mapCorner.Parent = minimap
|
||||
|
||||
local playerDot = Instance.new("Frame")
|
||||
playerDot.Name = "PlayerDot"
|
||||
playerDot.Size = UDim2.new(0, 6, 0, 6)
|
||||
playerDot.Position = UDim2.new(0.5, -3, 0.5, -3)
|
||||
playerDot.BackgroundColor3 = Color3.fromRGB(0, 255, 0)
|
||||
playerDot.BorderSizePixel = 0
|
||||
playerDot.Parent = minimap
|
||||
|
||||
-- Weapon info panel (bottom right)
|
||||
local weaponPanel = Instance.new("Frame")
|
||||
weaponPanel.Name = "WeaponPanel"
|
||||
weaponPanel.Size = UDim2.new(0, 250, 0, 80)
|
||||
weaponPanel.Position = UDim2.new(1, -260, 1, -90)
|
||||
weaponPanel.BackgroundColor3 = Color3.fromRGB(0, 0, 0)
|
||||
weaponPanel.BackgroundTransparency = 0.4
|
||||
weaponPanel.BorderSizePixel = 0
|
||||
weaponPanel.Parent = hudGui
|
||||
|
||||
local weaponLabel = Instance.new("TextLabel")
|
||||
weaponLabel.Name = "WeaponName"
|
||||
weaponLabel.Size = UDim2.new(1, -10, 0, 25)
|
||||
weaponLabel.Position = UDim2.new(0, 5, 0, 5)
|
||||
weaponLabel.BackgroundTransparency = 1
|
||||
weaponLabel.Text = "M4A1 CARBINE"
|
||||
weaponLabel.TextColor3 = Color3.fromRGB(255, 255, 255)
|
||||
weaponLabel.TextSize = 16
|
||||
weaponLabel.Font = Enum.Font.GothamBold
|
||||
weaponLabel.TextXAlignment = Enum.TextXAlignment.Right
|
||||
weaponLabel.Parent = weaponPanel
|
||||
|
||||
local ammoLabel = Instance.new("TextLabel")
|
||||
ammoLabel.Name = "AmmoLabel"
|
||||
ammoLabel.Size = UDim2.new(1, -10, 0, 35)
|
||||
ammoLabel.Position = UDim2.new(0, 5, 0, 25)
|
||||
ammoLabel.BackgroundTransparency = 1
|
||||
ammoLabel.Text = "30 / 210"
|
||||
ammoLabel.TextColor3 = Color3.fromRGB(255, 255, 255)
|
||||
ammoLabel.TextSize = 28
|
||||
ammoLabel.Font = Enum.Font.GothamBold
|
||||
ammoLabel.TextXAlignment = Enum.TextXAlignment.Right
|
||||
ammoLabel.Parent = weaponPanel
|
||||
|
||||
local reserveLabel = Instance.new("TextLabel")
|
||||
reserveLabel.Name = "ReserveLabel"
|
||||
reserveLabel.Size = UDim2.new(1, -10, 0, 15)
|
||||
reserveLabel.Position = UDim2.new(0, 5, 0, 60)
|
||||
reserveLabel.BackgroundTransparency = 1
|
||||
reserveLabel.Text = ""
|
||||
reserveLabel.TextColor3 = Color3.fromRGB(180, 180, 180)
|
||||
reserveLabel.TextSize = 12
|
||||
reserveLabel.Font = Enum.Font.Gotham
|
||||
reserveLabel.TextXAlignment = Enum.TextXAlignment.Right
|
||||
reserveLabel.Parent = weaponPanel
|
||||
|
||||
-- Reload bar
|
||||
local reloadBar = Instance.new("Frame")
|
||||
reloadBar.Name = "ReloadBar"
|
||||
reloadBar.Size = UDim2.new(0, 200, 0, 8)
|
||||
reloadBar.Position = UDim2.new(0.5, -100, 0.6, 0)
|
||||
reloadBar.BackgroundColor3 = Color3.fromRGB(40, 40, 40)
|
||||
reloadBar.BorderSizePixel = 0
|
||||
reloadBar.Visible = false
|
||||
reloadBar.Parent = hudGui
|
||||
|
||||
local reloadFill = Instance.new("Frame")
|
||||
reloadFill.Name = "Fill"
|
||||
reloadFill.Size = UDim2.new(0, 0, 1, 0)
|
||||
reloadFill.BackgroundColor3 = Color3.fromRGB(255, 200, 50)
|
||||
reloadFill.BorderSizePixel = 0
|
||||
reloadFill.Parent = reloadBar
|
||||
|
||||
-- Controls hint (bottom center)
|
||||
local controlsHint = Instance.new("TextLabel")
|
||||
controlsHint.Name = "Controls"
|
||||
controlsHint.Size = UDim2.new(0, 600, 0, 25)
|
||||
controlsHint.Position = UDim2.new(0.5, -300, 1, -30)
|
||||
controlsHint.BackgroundTransparency = 1
|
||||
controlsHint.Text = "WASD=Move | LMB=Shoot | RMB=ADS | Shift=Sprint | Ctrl=Crouch | R=Reload | 1-4=Weapons"
|
||||
controlsHint.TextColor3 = Color3.fromRGB(150, 150, 150)
|
||||
controlsHint.TextSize = 12
|
||||
controlsHint.Font = Enum.Font.Gotham
|
||||
controlsHint.Parent = hudGui
|
||||
|
||||
-- ═══════════════════════════════════════════════════════════════
|
||||
-- PLAYER SETUP SCRIPT (LocalScript in StarterPlayerScripts)
|
||||
-- ═══════════════════════════════════════════════════════════════
|
||||
local spScripts = SP:FindFirstChild("StarterPlayerScripts")
|
||||
if not spScripts then
|
||||
spScripts = Instance.new("Folder")
|
||||
spScripts.Name = "StarterPlayerScripts"
|
||||
spScripts.Parent = SP
|
||||
end
|
||||
|
||||
local playerSetup = Instance.new("LocalScript")
|
||||
playerSetup.Name = "PlayerSetup"
|
||||
playerSetup.Parent = spScripts
|
||||
playerSetup.Source = [[
|
||||
local Players = game:GetService("Players")
|
||||
local RS = game:GetService("ReplicatedStorage")
|
||||
local RunService = game:GetService("RunService")
|
||||
local UIS = game:GetService("UserInputService")
|
||||
local Events = RS:WaitForChild("Events")
|
||||
|
||||
local player = Players.LocalPlayer
|
||||
local camera = workspace.CurrentCamera
|
||||
|
||||
-- Force first person
|
||||
player.CameraMode = Enum.CameraMode.LockFirstPerson
|
||||
player.CameraMaxZoomDistance = 0.5
|
||||
player.CameraMinZoomDistance = 0.5
|
||||
|
||||
-- Character setup on spawn
|
||||
player.CharacterAdded:Connect(function(char)
|
||||
task.wait(0.5)
|
||||
local hum = char:WaitForChild("Humanoid")
|
||||
hum.WalkSpeed = 20
|
||||
hum.JumpPower = 40
|
||||
|
||||
-- Health regen
|
||||
task.spawn(function()
|
||||
while hum and hum.Health > 0 do
|
||||
task.wait(2)
|
||||
if hum.Health < hum.MaxHealth and hum.Health > 0 then
|
||||
hum.Health = math.min(hum.MaxHealth, hum.Health + 3)
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
-- Damage vignette on hit
|
||||
hum.HealthChanged:Connect(function(health)
|
||||
local lost = hum.MaxHealth - health
|
||||
if lost > 0 then
|
||||
local gui = player:FindFirstChild("PlayerGui")
|
||||
if gui then
|
||||
local hud = gui:FindFirstChild("FPS_HUD")
|
||||
if hud then
|
||||
local vignette = hud:FindFirstChild("DamageVignette")
|
||||
if vignette then
|
||||
local intensity = math.clamp(lost / hum.MaxHealth, 0, 0.6)
|
||||
vignette.BackgroundTransparency = 1 - intensity
|
||||
task.delay(0.3, function()
|
||||
if vignette then
|
||||
vignette.BackgroundTransparency = 1
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
end)
|
||||
|
||||
-- Kill feed listener
|
||||
Events:WaitForChild("KillEvent").OnClientEvent:Connect(function(data)
|
||||
local gui = player:FindFirstChild("PlayerGui")
|
||||
if not gui then return end
|
||||
local hud = gui:FindFirstChild("FPS_HUD")
|
||||
if not hud then return end
|
||||
local feed = hud:FindFirstChild("KillFeed")
|
||||
if not feed then return end
|
||||
|
||||
-- Create kill feed entry
|
||||
local entry = Instance.new("TextLabel")
|
||||
entry.Size = UDim2.new(1, 0, 0, 25)
|
||||
entry.BackgroundTransparency = 0.4
|
||||
entry.BackgroundColor3 = Color3.fromRGB(0, 0, 0)
|
||||
entry.Text = " " .. data.killer .. " [" .. data.weapon .. "] " .. data.victim
|
||||
.. (data.headshot and " ★" or "")
|
||||
entry.TextColor3 = data.killer == player.Name and Color3.fromRGB(50, 255, 50)
|
||||
or Color3.fromRGB(255, 255, 255)
|
||||
entry.TextSize = 14
|
||||
entry.Font = Enum.Font.GothamBold
|
||||
entry.TextXAlignment = Enum.TextXAlignment.Right
|
||||
entry.BorderSizePixel = 0
|
||||
|
||||
-- Headshot indicator color
|
||||
if data.headshot then
|
||||
entry.TextColor3 = Color3.fromRGB(255, 50, 50)
|
||||
end
|
||||
|
||||
entry.Parent = feed
|
||||
|
||||
-- Shift older entries down
|
||||
for i, child in ipairs(feed:GetChildren()) do
|
||||
if child:IsA("TextLabel") then
|
||||
child.Position = UDim2.new(0, 0, 0, (i-1) * 28)
|
||||
end
|
||||
end
|
||||
|
||||
-- Remove after 5 seconds
|
||||
task.delay(5, function()
|
||||
if entry then entry:Destroy() end
|
||||
end)
|
||||
|
||||
-- Killstreak banner
|
||||
if data.killer == player.Name and data.streak then
|
||||
local banner = hud:FindFirstChild("StreakBanner")
|
||||
if banner then
|
||||
local streakNames = {
|
||||
[3] = "TRIPLE KILL!",
|
||||
[5] = "KILLING SPREE!",
|
||||
[7] = "UNSTOPPABLE!",
|
||||
[10] = "TACTICAL NUKE READY!",
|
||||
}
|
||||
local msg = streakNames[data.streak]
|
||||
if msg then
|
||||
banner.Text = msg
|
||||
banner.Visible = true
|
||||
task.delay(3, function()
|
||||
if banner then banner.Visible = false end
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
-- Hit marker listener
|
||||
Events:WaitForChild("DamageEvent").OnClientEvent:Connect(function(data)
|
||||
local gui = player:FindFirstChild("PlayerGui")
|
||||
if not gui then return end
|
||||
local hud = gui:FindFirstChild("FPS_HUD")
|
||||
if not hud then return end
|
||||
|
||||
-- Show hit marker
|
||||
local hm = hud:FindFirstChild("HitMarker")
|
||||
if hm and data.hit then
|
||||
hm.Visible = true
|
||||
-- Change color for headshots
|
||||
for _, child in ipairs(hm:GetChildren()) do
|
||||
if child:IsA("Frame") then
|
||||
child.BackgroundColor3 = data.headshot
|
||||
and Color3.fromRGB(255, 50, 50)
|
||||
or Color3.fromRGB(255, 255, 255)
|
||||
end
|
||||
end
|
||||
task.delay(0.15, function()
|
||||
if hm then hm.Visible = false end
|
||||
end)
|
||||
end
|
||||
end)
|
||||
|
||||
-- Minimap updater
|
||||
task.spawn(function()
|
||||
task.wait(3)
|
||||
while true do
|
||||
task.wait(0.5)
|
||||
local char = player.Character
|
||||
if char then
|
||||
local gui = player:FindFirstChild("PlayerGui")
|
||||
if gui then
|
||||
local hud = gui:FindFirstChild("FPS_HUD")
|
||||
if hud then
|
||||
local map = hud:FindFirstChild("Minimap")
|
||||
if map then
|
||||
-- Update enemy dots
|
||||
for _, child in ipairs(map:GetChildren()) do
|
||||
if child.Name == "EnemyDot" then child:Destroy() end
|
||||
end
|
||||
for _, obj in ipairs(workspace:GetChildren()) do
|
||||
if obj.Name:match("^Enemy_") then
|
||||
local hum = obj:FindFirstChildOfClass("Humanoid")
|
||||
if hum and hum.Health > 0 then
|
||||
local root = obj:FindFirstChild("HumanoidRootPart")
|
||||
if root and char:FindFirstChild("Head") then
|
||||
local relPos = root.Position - char.Head.Position
|
||||
local mapScale = 150 / 400 -- minimap size / map size
|
||||
local mx = math.clamp(relPos.X * mapScale + 72, 5, 145)
|
||||
local mz = math.clamp(relPos.Z * mapScale + 72, 5, 145)
|
||||
|
||||
local eDot = Instance.new("Frame")
|
||||
eDot.Name = "EnemyDot"
|
||||
eDot.Size = UDim2.new(0, 5, 0, 5)
|
||||
eDot.Position = UDim2.new(0, mx, 0, mz)
|
||||
eDot.BackgroundColor3 = Color3.fromRGB(255, 50, 50)
|
||||
eDot.BorderSizePixel = 0
|
||||
eDot.Parent = map
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
print("[PlayerSetup] FPS player configured.)
|
||||
]]
|
||||
|
||||
print("[CoD FPS] Part 4/5 complete: HUD + Player scripts created.")
|
||||
371
examples/fps-game/part5_client.lua
Normal file
371
examples/fps-game/part5_client.lua
Normal file
@@ -0,0 +1,371 @@
|
||||
-- ═══════════════════════════════════════════════════════════════════
|
||||
-- MINI CALL OF DUTY - FPS Game Setup (Part 5: Weapon Client Script)
|
||||
-- ═══════════════════════════════════════════════════════════════════
|
||||
|
||||
local SG = game:GetService("StarterGui")
|
||||
|
||||
-- Get the HUD that was already created in Part 4
|
||||
local hudGui = SG:FindFirstChild("FPS_HUD")
|
||||
|
||||
-- Add the weapon controller LocalScript
|
||||
local weaponScript = Instance.new("LocalScript")
|
||||
weaponScript.Name = "WeaponController"
|
||||
weaponScript.Parent = hudGui
|
||||
weaponScript.Source = [[
|
||||
local Players = game:GetService("Players")
|
||||
local RS = game:GetService("ReplicatedStorage")
|
||||
local RunService = game:GetService("RunService")
|
||||
local UIS = game:GetService("UserInputService")
|
||||
local Events = RS:WaitForChild("Events")
|
||||
local Shared = RS:WaitForChild("Shared")
|
||||
local WeaponData = require(Shared:WaitForChild("WeaponData"))
|
||||
|
||||
local player = Players.LocalPlayer
|
||||
local camera = workspace.CurrentCamera
|
||||
local mouse = player:GetMouse()
|
||||
|
||||
-- Weapon state
|
||||
local currentWeapon = "M4A1"
|
||||
local weapon = WeaponData[currentWeapon]
|
||||
local ammo = weapon.magSize
|
||||
local reserveAmmo = weapon.maxAmmo
|
||||
local isReloading = false
|
||||
local lastShot = 0
|
||||
local isADS = false
|
||||
local isSprinting = false
|
||||
local isFiring = false
|
||||
local recoilX = 0
|
||||
local recoilY = 0
|
||||
|
||||
-- UI references
|
||||
local scriptParent = script.Parent
|
||||
local crosshair = scriptParent:WaitForChild("Crosshair")
|
||||
local hitMarker = scriptParent:WaitForChild("HitMarker")
|
||||
local weaponPanel = scriptParent:WaitForChild("WeaponPanel")
|
||||
local ammoLabel = weaponPanel:WaitForChild("AmmoLabel")
|
||||
local weaponLabel = weaponPanel:WaitForChild("WeaponName")
|
||||
local reserveLabel = weaponPanel:WaitForChild("ReserveLabel")
|
||||
local reloadBar = scriptParent:WaitForChild("ReloadBar")
|
||||
local reloadFill = reloadBar:WaitForChild("Fill")
|
||||
local scoreLabel = scriptParent:WaitForChild("ScoreFrame"):WaitForChild("ScoreLabel")
|
||||
local damageVignette = scriptParent:WaitForChild("DamageVignette")
|
||||
|
||||
local kills = 0
|
||||
local deaths = 0
|
||||
|
||||
local function updateHUD()
|
||||
if ammoLabel then
|
||||
ammoLabel.Text = ammo .. " / " .. reserveAmmo
|
||||
if ammo <= math.floor(weapon.magSize * 0.25) then
|
||||
ammoLabel.TextColor3 = Color3.fromRGB(255, 80, 80)
|
||||
else
|
||||
ammoLabel.TextColor3 = Color3.fromRGB(255, 255, 255)
|
||||
end
|
||||
end
|
||||
if weaponLabel then
|
||||
weaponLabel.Text = weapon.displayName:upper()
|
||||
end
|
||||
if reserveLabel then
|
||||
reserveLabel.Text = isReloading and "RELOADING..." or ""
|
||||
end
|
||||
if scoreLabel then
|
||||
scoreLabel.Text = "KILLS: " .. kills .. " | DEATHS: " .. deaths
|
||||
end
|
||||
end
|
||||
|
||||
local function shoot()
|
||||
if isReloading then return end
|
||||
if ammo <= 0 then
|
||||
-- Auto reload
|
||||
if reserveAmmo > 0 then
|
||||
-- Play empty click sound via visual feedback
|
||||
end
|
||||
return
|
||||
end
|
||||
if tick() - lastShot < weapon.fireRate then return end
|
||||
|
||||
lastShot = tick()
|
||||
ammo = ammo - 1
|
||||
|
||||
-- Recoil
|
||||
recoilX = recoilX + (math.random() - 0.5) * weapon.recoil.x
|
||||
recoilY = weapon.recoil.y * 0.3
|
||||
camera.CFrame = camera.CFrame * CFrame.Angles(
|
||||
math.rad(-recoilY),
|
||||
math.rad(recoilX * 0.1),
|
||||
0
|
||||
)
|
||||
|
||||
-- Spread
|
||||
local spreadAmount = isADS and weapon.spread.ads or weapon.spread.hip
|
||||
local spread = Vector3.new(
|
||||
(math.random() - 0.5) * spreadAmount * 0.01,
|
||||
(math.random() - 0.5) * spreadAmount * 0.01,
|
||||
0
|
||||
)
|
||||
|
||||
-- Raycast
|
||||
local rayDirection = (camera.CFrame.LookVector + spread) * weapon.range
|
||||
local raycastParams = RaycastParams.new()
|
||||
raycastParams.FilterType = Enum.RaycastFilterType.Exclude
|
||||
local char = player.Character
|
||||
if char then raycastParams.FilterDescendantsInstances = {char} end
|
||||
|
||||
local result = workspace:Raycast(camera.CFrame.Position, rayDirection, raycastParams)
|
||||
|
||||
-- Muzzle flash
|
||||
local flash = Instance.new("Part")
|
||||
flash.Size = Vector3.new(0.3, 0.3, 0.3)
|
||||
flash.Shape = Enum.PartType.Ball
|
||||
flash.Color = Color3.fromRGB(255, 200, 50)
|
||||
flash.Material = Enum.Material.Neon
|
||||
flash.Anchored = true
|
||||
flash.CanCollide = false
|
||||
flash.Position = camera.CFrame.Position + camera.CFrame.LookVector * 3
|
||||
flash.Parent = workspace
|
||||
game:GetService("Debris"):AddItem(flash, 0.04)
|
||||
|
||||
if result then
|
||||
-- Bullet trail
|
||||
local trail = Instance.new("Part")
|
||||
local trailLen = (camera.CFrame.Position - result.Position).Magnitude
|
||||
trail.Size = Vector3.new(0.08, 0.08, trailLen)
|
||||
trail.CFrame = CFrame.new(camera.CFrame.Position, result.Position)
|
||||
* CFrame.new(0, 0, -trailLen / 2)
|
||||
trail.Anchored = true
|
||||
trail.CanCollide = false
|
||||
trail.Color = Color3.fromRGB(255, 220, 100)
|
||||
trail.Material = Enum.Material.Neon
|
||||
trail.Transparency = 0.4
|
||||
trail.Parent = workspace
|
||||
game:GetService("Debris"):AddItem(trail, 0.06)
|
||||
|
||||
-- Impact spark
|
||||
local spark = Instance.new("Part")
|
||||
spark.Size = Vector3.new(0.4, 0.4, 0.4)
|
||||
spark.Shape = Enum.PartType.Ball
|
||||
spark.Color = Color3.fromRGB(255, 180, 50)
|
||||
spark.Material = Enum.Material.Neon
|
||||
spark.Anchored = true
|
||||
spark.CanCollide = false
|
||||
spark.Position = result.Position
|
||||
spark.Parent = workspace
|
||||
game:GetService("Debris"):AddItem(spark, 0.12)
|
||||
|
||||
-- Smoke puff at impact
|
||||
local smoke = Instance.new("Part")
|
||||
smoke.Size = Vector3.new(1, 1, 1)
|
||||
smoke.Shape = Enum.PartType.Ball
|
||||
smoke.Color = Color3.fromRGB(120, 120, 110)
|
||||
smoke.Transparency = 0.5
|
||||
smoke.Anchored = true
|
||||
smoke.CanCollide = false
|
||||
smoke.Position = result.Position
|
||||
smoke.Parent = workspace
|
||||
game:GetService("Debris"):AddItem(smoke, 0.3)
|
||||
|
||||
-- Send to server
|
||||
Events:WaitForChild("ShootEvent"):FireServer({
|
||||
origin = camera.CFrame.Position,
|
||||
direction = rayDirection,
|
||||
hit = result.Instance,
|
||||
hitPos = result.Position,
|
||||
normal = result.Normal,
|
||||
weapon = currentWeapon,
|
||||
})
|
||||
else
|
||||
-- Shot into air - just trail to max range
|
||||
local endPoint = camera.CFrame.Position + rayDirection
|
||||
local trail = Instance.new("Part")
|
||||
local trailLen = weapon.range
|
||||
trail.Size = Vector3.new(0.06, 0.06, trailLen)
|
||||
trail.CFrame = CFrame.new(camera.CFrame.Position, endPoint)
|
||||
* CFrame.new(0, 0, -trailLen / 2)
|
||||
trail.Anchored = true
|
||||
trail.CanCollide = false
|
||||
trail.Color = Color3.fromRGB(255, 220, 100)
|
||||
trail.Material = Enum.Material.Neon
|
||||
trail.Transparency = 0.5
|
||||
trail.Parent = workspace
|
||||
game:GetService("Debris"):AddItem(trail, 0.04)
|
||||
end
|
||||
|
||||
-- Auto reload when empty
|
||||
if ammo <= 0 and reserveAmmo > 0 then
|
||||
task.delay(0.3, function() reload() end)
|
||||
end
|
||||
|
||||
updateHUD()
|
||||
end
|
||||
|
||||
local function reload()
|
||||
if isReloading then return end
|
||||
if ammo >= weapon.magSize then return end
|
||||
if reserveAmmo <= 0 then return end
|
||||
|
||||
isReloading = true
|
||||
reloadBar.Visible = true
|
||||
|
||||
local startTime = tick()
|
||||
local conn
|
||||
conn = RunService.RenderStepped:Connect(function()
|
||||
local elapsed = tick() - startTime
|
||||
local pct = math.clamp(elapsed / weapon.reloadTime, 0, 1)
|
||||
reloadFill.Size = UDim2.new(pct * 200, 0, 1, 0)
|
||||
|
||||
if pct >= 1 then
|
||||
conn:Disconnect()
|
||||
local needed = weapon.magSize - ammo
|
||||
local toLoad = math.min(needed, reserveAmmo)
|
||||
ammo = ammo + toLoad
|
||||
reserveAmmo = reserveAmmo - toLoad
|
||||
isReloading = false
|
||||
reloadBar.Visible = false
|
||||
reloadFill.Size = UDim2.new(0, 0, 1, 0)
|
||||
updateHUD()
|
||||
end
|
||||
end)
|
||||
|
||||
updateHUD()
|
||||
end
|
||||
|
||||
-- Input handling
|
||||
UIS.InputBegan:Connect(function(input, processed)
|
||||
if processed then return end
|
||||
|
||||
if input.UserInputType == Enum.UserInputType.MouseButton2 then
|
||||
isADS = true
|
||||
if isSprinting then isSprinting = false end
|
||||
end
|
||||
|
||||
if input.UserInputType == Enum.UserInputType.MouseButton1 then
|
||||
isFiring = true
|
||||
if not weapon.automatic then shoot() end
|
||||
if isSprinting then isSprinting = false end
|
||||
end
|
||||
|
||||
if input.KeyCode == Enum.KeyCode.LeftShift then
|
||||
if not isADS then isSprinting = true end
|
||||
end
|
||||
|
||||
if input.KeyCode == Enum.KeyCode.LeftControl then
|
||||
local c = player.Character
|
||||
if c then
|
||||
local h = c:FindFirstChildOfClass("Humanoid")
|
||||
if h then h.WalkSpeed = 8 end
|
||||
end
|
||||
end
|
||||
|
||||
if input.KeyCode == Enum.KeyCode.R then reload() end
|
||||
|
||||
-- Weapon switch
|
||||
if input.KeyCode == Enum.KeyCode.One then currentWeapon = "M4A1"
|
||||
elseif input.KeyCode == Enum.KeyCode.Two then currentWeapon = "AK47"
|
||||
elseif input.KeyCode == Enum.KeyCode.Three then currentWeapon = "Sniper"
|
||||
elseif input.KeyCode == Enum.KeyCode.Four then currentWeapon = "Shotgun" end
|
||||
|
||||
if input.KeyCode >= Enum.KeyCode.One and input.KeyCode <= Enum.KeyCode.Four then
|
||||
weapon = WeaponData[currentWeapon]
|
||||
ammo = weapon.magSize
|
||||
reserveAmmo = weapon.maxAmmo
|
||||
isReloading = false
|
||||
reloadBar.Visible = false
|
||||
updateHUD()
|
||||
end
|
||||
end)
|
||||
|
||||
UIS.InputEnded:Connect(function(input)
|
||||
if input.UserInputType == Enum.UserInputType.MouseButton2 then isADS = false end
|
||||
if input.UserInputType == Enum.UserInputType.MouseButton1 then isFiring = false end
|
||||
if input.KeyCode == Enum.KeyCode.LeftShift then isSprinting = false end
|
||||
if input.KeyCode == Enum.KeyCode.LeftControl then
|
||||
local c = player.Character
|
||||
if c then
|
||||
local h = c:FindFirstChildOfClass("Humanoid")
|
||||
if h then h.WalkSpeed = 20 end
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
-- Track kills/deaths from events
|
||||
Events:WaitForChild("KillEvent").OnClientEvent:Connect(function(data)
|
||||
if data.killer == player.Name then
|
||||
kills = kills + 1
|
||||
end
|
||||
if data.victim == player.Name then
|
||||
deaths = deaths + 1
|
||||
end
|
||||
updateHUD()
|
||||
end)
|
||||
|
||||
-- Main loop
|
||||
RunService.RenderStepped:Connect(function()
|
||||
-- Camera FOV for ADS
|
||||
local targetFOV = isADS and weapon.adsFOV or 70
|
||||
camera.FieldOfView = camera.FieldOfView + (targetFOV - camera.FieldOfView) * 0.2
|
||||
|
||||
-- Sprint speed
|
||||
local c = player.Character
|
||||
if c then
|
||||
local h = c:FindFirstChildOfClass("Humanoid")
|
||||
if h and h.MoveDirection.Magnitude > 0 then
|
||||
if isSprinting then
|
||||
h.WalkSpeed = 30
|
||||
elseif not UIS:IsKeyDown(Enum.KeyCode.LeftControl) then
|
||||
h.WalkSpeed = 20
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Auto-fire for automatic weapons
|
||||
if isFiring and weapon.automatic then shoot() end
|
||||
|
||||
-- Recoil recovery
|
||||
recoilX = recoilX * 0.9
|
||||
recoilY = recoilY * 0.85
|
||||
|
||||
-- Crosshair spread
|
||||
local spreadPx = isADS and 2 or (isSprinting and 15 or 6)
|
||||
if isFiring then spreadPx = spreadPx + 4 end
|
||||
for _, child in ipairs(crosshair:GetChildren()) do
|
||||
if child.Name == "Top" then child.Position = UDim2.new(0, 9, 0, 9 - spreadPx)
|
||||
elseif child.Name == "Bottom" then child.Position = UDim2.new(0, 9, 0, 9 + spreadPx)
|
||||
elseif child.Name == "Left" then child.Position = UDim2.new(0, 9 - spreadPx, 0, 9)
|
||||
elseif child.Name == "Right" then child.Position = UDim2.new(0, 9 + spreadPx, 0, 9)
|
||||
end
|
||||
end
|
||||
end)
|
||||
|
||||
-- First person lock
|
||||
UIS.MouseIconEnabled = false
|
||||
player.CameraMode = Enum.CameraMode.LockFirstPerson
|
||||
|
||||
player.CharacterAdded:Connect(function()
|
||||
ammo = weapon.magSize
|
||||
reserveAmmo = weapon.maxAmmo
|
||||
kills = 0
|
||||
deaths = 0
|
||||
isReloading = false
|
||||
reloadBar.Visible = false
|
||||
updateHUD()
|
||||
end)
|
||||
|
||||
updateHUD()
|
||||
print("═══════════════════════════════════════════")
|
||||
print(" MINI CALL OF DUTY - LOADED!")
|
||||
print(" Controls:")
|
||||
print(" WASD = Move")
|
||||
print(" LMB = Shoot")
|
||||
print(" RMB = Aim Down Sights")
|
||||
print(" Shift = Sprint")
|
||||
print(" Ctrl = Crouch")
|
||||
print(" R = Reload")
|
||||
print(" 1-4 = Switch Weapon")
|
||||
print(" Weapons: M4A1(1), AK-47(2), AWP Sniper(3), SPAS-12(4)")
|
||||
print("═══════════════════════════════════════════")
|
||||
]]
|
||||
|
||||
print("[CoD FPS] Part 5/5 complete: Weapon controller script created.")
|
||||
print("═══════════════════════════════════════════")
|
||||
print(" ALL PARTS COMPLETE! Press PLAY in Studio to start.")
|
||||
print("═══════════════════════════════════════════")
|
||||
Reference in New Issue
Block a user