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:
Admin
2026-03-31 16:57:35 +04:00
Unverified
parent 9c44cb514f
commit a66533206f
16 changed files with 3448 additions and 595 deletions

17
AutoConnect/AutoSetup.vbs Normal file
View File

@@ -0,0 +1,17 @@
Set WshShell = CreateObject("WScript.Shell")
' Wait for Roblox Studio
WScript.Sleep 2000
' Bring Roblox Studio to front
WshShell.AppActivate "Roblox Studio"
WScript.Sleep 500
' Send Ctrl+V to paste
WshShell.SendKeys "^v"
WScript.Sleep 500
MsgBox "Script pasted! Now:" & vbCrLf & vbCrLf & _
"1. Press Play (green ▶ button)" & vbCrLf & _
"2. Look for [RobloxMCP] in Output window", _
0, "Roblox MCP Setup"

View File

@@ -0,0 +1,29 @@
@echo off
echo =====================================
echo ROBLOX MCP - ONE-CLICK SETUP
echo =====================================
echo.
echo STEP 1: Script copied to clipboard!
echo.
echo STEP 2: Do this in Roblox Studio:
echo - Go to Explorer
echo - Open ServerScriptService
echo - Right-click, Insert Object, Script
echo - Press Ctrl+V to paste
echo - Press Play (green arrow)
echo.
echo STEP 3: Enable HTTP if needed:
echo - File, Game Settings, Security
echo - Enable BOTH HTTP options
echo.
pause
echo Checking connection...
curl -s http://127.0.0.1:37423/health
echo.
pause
curl -s http://127.0.0.1:37423/health
echo.
echo If you see "studioConnected": true above, it works!
pause

26
AutoConnect/Setup-MCP.bat Normal file
View File

@@ -0,0 +1,26 @@
@echo off
echo === Roblox MCP Connection Helper ===
echo.
REM Check MCP server
curl -s http://127.0.0.1:37423/health
echo.
echo.
echo === INSTRUCTIONS ===
echo.
echo In Roblox Studio:
echo 1. File ^> Game Settings ^> Security
echo 2. Enable BOTH HTTP options
echo 3. ServerScriptService ^> Right-click ^> Insert Object ^> Script
echo 4. Paste the script from: roblox-plugin\RobloxMCPServer_HTTP.lua
echo 5. Press Play (green ^> button)
echo.
echo You should see: [RobloxMCP] Starting Roblox MCP Server
echo.
pause
curl -s http://127.0.0.1:37423/health
echo.
echo.
pause

86
AutoConnect/Setup-MCP.ps1 Normal file
View File

@@ -0,0 +1,86 @@
# Roblox MCP Connection Helper
# This script helps set up the Roblox MCP connection
Write-Host "=== Roblox MCP Connection Helper ===" -ForegroundColor Cyan
Write-Host ""
# Check if Roblox Studio is running
$robloxProcess = Get-Process | Where-Object {$_.Name -like "*RobloxStudio*"}
if ($robloxProcess) {
Write-Host "✓ Roblox Studio is running (PID: $($robloxProcess.Id))" -ForegroundColor Green
} else {
Write-Host "✗ Roblox Studio is NOT running" -ForegroundColor Red
Write-Host " Please start Roblox Studio first" -ForegroundColor Yellow
exit 1
}
Write-Host ""
Write-Host "=== Setup Instructions ===" -ForegroundColor Cyan
Write-Host ""
Write-Host "1. In Roblox Studio, go to:" -ForegroundColor White
Write-Host " File → Game Settings → Security" -ForegroundColor Yellow
Write-Host ""
Write-Host "2. Enable BOTH options:" -ForegroundColor White
Write-Host " ☑ Enable Studio Access to API Services" -ForegroundColor Yellow
Write-Host " ☑ Allow HTTP Requests" -ForegroundColor Yellow
Write-Host ""
Write-Host "3. Click Save" -ForegroundColor Yellow
Write-Host ""
Write-Host "4. In Explorer → ServerScriptService → Right-click → Insert Object → Script" -ForegroundColor White
Write-Host ""
Write-Host "5. Copy the script below and paste it into the Script:" -ForegroundColor White
Write-Host ""
# Read the script file
$scriptPath = "C:\Users\Admin\roblox-mcp-server\roblox-plugin\RobloxMCPServer_HTTP.lua"
if (Test-Path $scriptPath) {
$scriptContent = Get-Content $scriptPath -Raw
# Copy to clipboard
Set-Clipboard -Value $scriptContent
Write-Host " [SCRIPT COPIED TO CLIPBOARD]" -ForegroundColor Green
Write-Host " Just paste it in Roblox Studio (Ctrl+V)" -ForegroundColor Green
Write-Host ""
} else {
Write-Host " ✗ Script file not found: $scriptPath" -ForegroundColor Red
}
Write-Host "6. Press Play (green ▶ button) in Roblox Studio" -ForegroundColor White
Write-Host ""
Write-Host "7. You should see: [RobloxMCP] Starting Roblox MCP Server" -ForegroundColor Green
Write-Host ""
# Check MCP server status
Write-Host "=== Checking MCP Server ===" -ForegroundColor Cyan
try {
$response = Invoke-RestMethod -Uri "http://127.0.0.1:37423/health" -TimeoutSec 2
Write-Host "✓ MCP Server is running" -ForegroundColor Green
Write-Host " Status: $($response.status)" -ForegroundColor White
Write-Host " Connected: $($response.studioConnected)" -ForegroundColor White
} catch {
Write-Host "✗ MCP Server is NOT responding" -ForegroundColor Red
Write-Host " Make sure to run: npm start" -ForegroundColor Yellow
}
Write-Host ""
Write-Host "Press any key to check connection again..." -ForegroundColor Cyan
$null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
# Re-check
try {
$response = Invoke-RestMethod -Uri "http://127.0.0.1:37423/health" -TimeoutSec 2
if ($response.studioConnected -or $response.pendingCommands -gt 0) {
Write-Host ""
Write-Host "✓✓✓ ROBLOX STUDIO IS CONNECTED! ✓✓✓" -ForegroundColor Green
Write-Host ""
Write-Host "You can now ask Claude to create things in Roblox!" -ForegroundColor Cyan
} else {
Write-Host ""
Write-Host "Still waiting for connection..." -ForegroundColor Yellow
Write-Host "Make sure you pressed Play in Roblox Studio!" -ForegroundColor Yellow
}
} catch {
Write-Host "✗ Cannot reach MCP server" -ForegroundColor Red
}

424
README.md
View File

@@ -1,217 +1,349 @@
# Roblox MCP Server
# ClaudeCode-Roblox-Studio-MCP
Control Roblox Studio directly from Claude Code using the Model Context Protocol (MCP).
A Model Context Protocol (MCP) server that enables **Claude Code** (by Anthropic) to directly control **Roblox Studio** - create games, manipulate objects, write scripts, and build experiences through natural language commands.
## Overview
This project creates a bidirectional bridge between Claude Code and Roblox Studio using:
- **MCP Protocol** (stdio) for Claude Code communication
- **HTTP Polling** for Roblox Studio plugin communication
- **Express.js** server for command queuing and result handling
## Architecture Diagram
```
┌─────────────────┐ MCP (stdio) ┌──────────────────┐ HTTP Polling ┌─────────────────┐
│ Claude Code │ ◄─────────────────► │ Node.js MCP │ ◄──────────────────► │ Roblox Studio │
│ (AI Agent) │ │ Server │ (port 37423) │ Plugin │
└─────────────────┘ └──────────────────┘ └─────────────────┘
Express HTTP
(Health: 37423)
```
### How It Works
1. **Claude Code** connects to the MCP server via stdio
2. **MCP Server** exposes tools that Claude can call (create_part, create_script, etc.)
3. **Commands** are queued in the MCP server's memory
4. **Roblox Plugin** polls the HTTP endpoint for new commands
5. **Plugin executes** commands in Roblox Studio and sends results back
6. **Results** are returned to Claude Code via the MCP protocol
## Features
- Create and modify scripts in Roblox Studio
- Create 3D parts, models, and folders
- Create 3D parts, models, folders, and entire scenes
- Write and inject Lua scripts (Script, LocalScript, ModuleScript)
- Build GUI elements (ScreenGui, Frame, TextButton, etc.)
- Set properties on any object
- Get hierarchy information
- Execute Lua code
- Play/Stop playtesting
- Save places
## Architecture
```
Claude Code <--(MCP)--> Node.js MCP Server <--(WebSocket)--> Roblox Studio Plugin
```
- Manipulate workspace hierarchy
- Execute arbitrary Lua code
- Playtest and save places automatically
- Python injection scripts for direct command bar automation
## Installation
### 1. Install Node.js Dependencies
### Prerequisites
- **Node.js** 18+ (for MCP server)
- **Claude Code** (by Anthropic)
- **Roblox Studio** (installed)
- Windows 11 or macOS
### Step 1: Clone and Install
```bash
cd roblox-mcp-server
cd ~
git clone https://github.rommark.dev/admin/ClaudeCode-Roblox-Studio-MCP.git
cd ClaudeCode-Roblox-Studio-MCP
npm install
```
### 2. Configure Claude Code
### Step 2: Configure Claude Code
Add this to your Claude Code settings (or create `.clauderc` in your home directory):
Add to your Claude Code config (`~/.claude/config.json` or `.clauderc`):
```json
{
"mcpServers": {
"roblox-studio": {
"command": "node",
"args": ["/mnt/c/Users/Admin/roblox-mcp-server/src/index.js"],
"cwd": "/mnt/c/Users/Admin/roblox-mcp-server"
"args": ["~/ClaudeCode-Roblox-Studio-MCP/src/index.js"],
"cwd": "~/ClaudeCode-Roblox-Studio-MCP"
}
}
}
```
### 3. Install the Roblox Studio Plugin
### Step 3: Install Roblox Studio Plugin
#### Option A: Manual Installation
**Windows:**
```powershell
Copy-Item roblox-plugin\RobloxMCPPlugin.lua $env:LOCALAPPDATA\Roblox\Plugins\
```
1. Copy `roblox-plugin/RobloxMCPServer.lua` to:
- **Windows**: `C:\Users\YOUR_USERNAME\AppData\Local\Roblox\Plugins\RobloxMCPServer.lua`
- **Mac**: `~/Library/Application Support/Roblox/Plugins/RobloxMCPServer.lua`
**Mac:**
```bash
cp roblox-plugin/RobloxMCPPlugin.lua ~/Library/Application\ Support/Roblox/Plugins/
```
2. Open Roblox Studio
3. Go to **Plugins → Plugin Management**
4. Find "RobloxMCPServer" and enable it
#### Option B: In-Place Installation
1. Open Roblox Studio
2. Create a new place or open an existing one
3. In **ServerScriptService**, create a new **Script**
4. Paste the contents of `roblox-plugin/RobloxMCPServer.lua`
5. The server will auto-start when you press Play
### 4. Enable HTTP Requests (Important!)
### Step 4: Enable HTTP Requests (Critical!)
In Roblox Studio:
1. Go to **Game Settings → Security**
2. **Enable** "Allow HTTP Requests"
3. Set **Enable Studio Access to API Services** to ON
1. **File** **Game Settings**
2. **Security** tab
3. Enable **"Enable Studio Access to API Services"**
4. Enable **"Allow HTTP Requests"**
## Usage
### Step 5: Start Using
### Starting the MCP Server
1. Start the MCP server: `npm start`
2. Open Roblox Studio
3. The plugin will auto-connect (look for "RobloxMCP" toolbar)
4. Start chatting with Claude Code to build your game!
```bash
npm start
## Available MCP Tools
| Tool | Description | Example |
|------|-------------|---------|
| `roblox_create_script` | Create Script/LocalScript/ModuleScript | "Create a Script in Workspace that prints hello" |
| `roblox_create_part` | Create 3D parts (Block, Ball, Cylinder, Wedge, CornerWedge) | "Create a red block at position 5, 10, 0" |
| `roblox_create_model` | Create model containers | "Create a model named Weapons in Workspace" |
| `roblox_create_folder` | Create folders for organization | "Create a folder named Scripts in Workspace" |
| `roblox_create_gui` | Create GUI elements | "Create a ScreenGui with a start button" |
| `roblox_set_property` | Set properties on objects | "Set the color of Workspace.Part1 to blue" |
| `roblox_get_hierarchy` | Explore object tree | "Show me the Workspace hierarchy" |
| `roblox_delete_object` | Delete objects by path | "Delete Workspace.OldPart" |
| `roblox_execute_code` | Run arbitrary Lua code | "Execute: print('Hello from Claude')" |
| `roblox_play` | Start playtest | "Start playtest in Client mode" |
| `roblox_stop` | Stop playtest | "Stop the playtest" |
| `roblox_save_place` | Save current place | "Save the place" |
## Usage Examples
### Creating a Simple Game
Ask Claude:
```
Create an obstacle course game with:
- A green starting platform at 0, 1, 0
- 5 red checkpoint platforms going upward
- A spinning part at the end
- A kill brick floor called Lava
```
The server will start on:
- HTTP: `http://localhost:37423` (for health checks)
- WebSocket: `ws://localhost:37424` (for Roblox Studio communication)
### Building a GUI
### Starting Roblox Studio Communication
1. Open Roblox Studio with the plugin installed
2. Press **Play** to start the server script
3. You should see a status indicator in the top-right corner
4. The MCP server will automatically connect
### Using with Claude Code
Once connected, you can ask Claude to:
```bash
# Create a script
"Create a Script in Workspace that prints 'Hello World'"
# Create a part
"Create a red block part at position 0, 10, 0 in Workspace"
# Build a GUI
"Create a ScreenGui with a TextButton that says 'Click Me'"
# Get hierarchy
"Show me the hierarchy of Workspace"
# Execute code
"Execute: print('Testing command')"
```
Create a ScreenGui with:
- A dark semi-transparent frame
- A title saying "MY GAME"
- A green start button
- Make the button print "Started!" when clicked
```
## Available Tools
### Scripting
| Tool | Description |
|------|-------------|
| `roblox_create_script` | Create a Script, LocalScript, or ModuleScript |
| `roblox_create_part` | Create 3D parts (Block, Ball, Cylinder, etc.) |
| `roblox_create_model` | Create model containers |
| `roblox_create_folder` | Create folders for organization |
| `roblox_create_gui` | Create GUI elements |
| `roblox_set_property` | Set properties on existing objects |
| `roblox_get_hierarchy` | Get the object hierarchy |
| `roblox_delete_object` | Delete an object by path |
| `roblox_execute_code` | Execute arbitrary Lua code |
| `roblox_play` | Start playtest |
| `roblox_stop` | Stop playtest |
| `roblox_save_place` | Save the current place |
```
Create a Script in Workspace.Part that makes it spin continuously
```
## Python Injection Scripts
The `examples/` folder includes Python scripts for direct injection into Roblox Studio:
### studio-inject.py
Inject a single Lua script into the Roblox Studio command bar using Win32 API.
```bash
python examples/studio-inject.py
```
### inject-all-parts.py
Inject all 5 parts of the FPS game sequentially into Roblox Studio.
```bash
python examples/inject-all-parts.py
```
**Requirements:**
- Windows 11
- Roblox Studio must be open
- Python 3.x
## Examples Folder
The `examples/` folder contains:
- `fps-game/` - Complete 5-part FPS game setup (CoD style)
- `demo_game.lua` - Simple obby game example
- `spinning_part.lua` - Rotating part script
- `start_button.lua` - Start button GUI
### FPS Game Parts
1. **Part 1**: Map + Infrastructure (buildings, cover, lighting)
2. **Part 2**: Weapon System (weapon data module, client weapon controller)
3. **Part 3**: Enemy AI + Server Handler (AI bots, game server script)
4. **Part 4**: HUD + Player Scripts (crosshair, minimap, damage effects)
5. **Part 5**: Weapon Client Script (final weapon controller)
## Project Structure
```
ClaudeCode-Roblox-Studio-MCP/
├── src/
│ └── index.js # Main MCP server + Express/HTTP
├── roblox-plugin/
│ └── RobloxMCPPlugin.lua # HTTP polling plugin for Studio
├── examples/
│ ├── fps-game/
│ │ ├── part1_map.lua # Map infrastructure
│ │ ├── part2_weapons.lua # Weapon system
│ │ ├── part3_ai.lua # Enemy AI + server
│ │ ├── part4_hud.lua # HUD + player scripts
│ │ └── part5_client.lua # Weapon client script
│ ├── studio-inject.py # Single script injection
│ └── inject-all-parts.py # Multi-part injection
├── package.json
└── README.md # This file
```
## Configuration
Edit `src/index.js` to change ports:
### MCP Server Ports
Edit `src/index.js`:
```javascript
const HTTP_PORT = 37423; // Health check endpoint
const WS_PORT = 37424; // WebSocket for Roblox Studio
const HTTP_PORT = 37423; // HTTP polling endpoint
const WS_PORT = 37424; // WebSocket (optional, for future use)
```
Edit `roblox-plugin/RobloxMCPServer.lua` to change plugin settings:
### Roblox Plugin Configuration
Edit `roblox-plugin/RobloxMCPPlugin.lua`:
```lua
local CONFIG = {
PORT = 37425,
POLL_INTERVAL = 0.1,
DEBUG = true,
HOST = "localhost",
PORT = 37423,
POLL_INTERVAL = 0.5, -- seconds between polls
}
```
## Troubleshooting
### "No Roblox Studio instance connected"
### "MCP server not reachable"
- Make sure Roblox Studio is open
- Make sure the server script is running (press Play)
- Check that HTTP requests are enabled in Game Settings
- Look for the status indicator in the top-right corner
**Solution:**
- Make sure the MCP server is running: `npm start`
- Check port 37423 is not in use
- Verify Claude Code config is correct
### WebSocket Connection Failed
### "HTTP Requests Blocked"
- Check that the MCP server is running (`npm start`)
- Verify the WS_PORT matches between server and plugin
- Check Windows Firewall if connection is refused
**Solution:**
- Game Settings → Security
- Enable both HTTP-related options
- Restart Roblox Studio after changing
### Scripts Not Executing
### Plugin Not Connecting
- Make sure the script type is correct (Script vs LocalScript vs ModuleScript)
- Check the Output window in Roblox Studio for errors
- Verify the parent path is correct
**Solution:**
- Check Output window in Roblox Studio
- Verify plugin is in correct Plugins folder
- Make sure HttpService is enabled
- Try clicking the toolbar button manually
### HTTP Requests Blocked
### Commands Not Executing
- Go to Game Settings → Security
- Enable "Allow HTTP Requests"
- Enable "Enable Studio Access to API Services"
**Solution:**
- Check for Lua errors in Output window
- Verify parent paths exist
- Make sure object names don't contain special characters
## Security Considerations
⚠️ **Important Security Notes:**
- This plugin allows executing arbitrary Lua code
- Only use in trusted development environments
- HTTP requests must be enabled (security trade-off)
- Consider using a reverse proxy for production
- Review all code before execution in production games
## Development
### Project Structure
```
roblox-mcp-server/
├── src/
│ └── index.js # MCP server + WebSocket server
├── roblox-plugin/
│ ├── RobloxMCPPlugin.lua # Toolbar plugin (optional)
│ └── RobloxMCPServer.lua # Server script (required)
├── package.json
└── README.md
```
### Adding New Tools
1. Add the tool definition to `src/index.js` in the `ListToolsRequestSchema` handler
2. Add a case in the `CallToolRequestSchema` handler
3. Add a corresponding handler in `roblox-plugin/RobloxMCPServer.lua`
1. Add tool definition in `src/index.js` (in `ListToolsRequestSchema` handler)
2. Add handler in `CallToolRequestSchema` handler
3. Implement in `roblox-plugin/RobloxMCPPlugin.lua` (add to `handleCommand` function)
## Security Notes
Example:
```javascript
// src/index.js
{
name: 'roblox_my_tool',
description: 'Does something cool',
inputSchema: {
type: 'object',
properties: {
param1: { type: 'string', description: 'Description' },
},
required: ['param1'],
},
}
```
- This plugin allows executing arbitrary Lua code in Roblox Studio
- Only use in trusted environments
- HTTP requests must be enabled in Roblox Studio
- Consider using a reverse proxy for production deployments
## License
MIT
```lua
-- roblox-plugin/RobloxMCPPlugin.lua
elseif command == "myTool" then
-- Your implementation
return { success = true, result = "something" }
```
## Contributing
Contributions welcome! Feel free to open issues or pull requests.
Contributions welcome! Please:
1. Fork the repository
2. Create a feature branch
3. Make your changes
4. Test thoroughly
5. Submit a pull request
## License
MIT License - feel free to use in your projects!
## Credits
- Built with [Model Context Protocol](https://github.com/modelcontextprotocol) by Anthropic
- Powered by [Claude Code](https://claude.ai/code)
- For Roblox Studio game development
## Support
For issues, questions, or suggestions:
- Open an issue on GitHub
- Check the Troubleshooting section
- Review the examples in `examples/`
## Changelog
### v2.0.0 (2025-03-31)
- Updated to HTTP polling architecture
- Added Python injection scripts
- Added complete FPS game example
- Improved plugin with toolbar UI
- Added comprehensive examples folder
### v1.0.0 (2025-01-29)
- Initial release
- 12 MCP tools implemented
- WebSocket communication (deprecated)
- Full CRUD operations for Roblox objects
---
**Made with ❤️ for Roblox developers using AI**

View 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.")

View 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.")

View 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.")

View 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.")

View 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("═══════════════════════════════════════════")

View File

@@ -0,0 +1,176 @@
"""
Inject all 5 FPS game parts into Roblox Studio command bar sequentially.
"""
import ctypes
import ctypes.wintypes
import subprocess
import time
import sys
import os
user32 = ctypes.windll.user32
def find_studio():
target = [None]
def cb(hwnd, _):
length = user32.GetWindowTextLengthW(hwnd)
if length > 0:
buf = ctypes.create_unicode_buffer(length + 1)
user32.GetWindowTextW(hwnd, buf, length + 1)
if "Roblox Studio" in buf.value:
target[0] = hwnd
return False
return True
WNDENUMPROC = ctypes.WINFUNCTYPE(ctypes.c_bool, ctypes.wintypes.HWND, ctypes.wintypes.LPARAM)
user32.EnumWindows(WNDENUMPROC(cb), 0)
return target[0]
def set_foreground(hwnd):
SW_RESTORE = 9
user32.ShowWindow(hwnd, SW_RESTORE)
time.sleep(0.3)
fg = user32.GetForegroundWindow()
if fg != hwnd:
tid_fg = user32.GetWindowThreadProcessId(fg, None)
tid_target = user32.GetWindowThreadProcessId(hwnd, None)
user32.AttachThreadInput(tid_fg, tid_target, True)
user32.SetForegroundWindow(hwnd)
user32.AttachThreadInput(tid_fg, tid_target, False)
time.sleep(0.3)
def set_clipboard(text):
# Use PowerShell for reliable clipboard
# Write to temp file first to avoid escaping issues
tmp = os.path.join(os.environ["TEMP"], "roblox_clipboard.lua")
with open(tmp, "w", encoding="utf-8") as f:
f.write(text)
result = subprocess.run(
["powershell", "-Command",
f"Get-Content '{tmp}' -Raw | Set-Clipboard"],
capture_output=True, text=True, timeout=10
)
return result.returncode == 0
def press_key(vk):
user32.keybd_event(vk, 0, 0, 0)
time.sleep(0.03)
user32.keybd_event(vk, 0, 2, 0)
time.sleep(0.05)
def ctrl_v():
user32.keybd_event(0x11, 0, 0, 0) # Ctrl down
time.sleep(0.02)
user32.keybd_event(0x56, 0, 0, 0) # V down
time.sleep(0.03)
user32.keybd_event(0x56, 0, 2, 0) # V up
time.sleep(0.02)
user32.keybd_event(0x11, 0, 2, 0) # Ctrl up
time.sleep(0.1)
def click_at(x, y):
screen_w = user32.GetSystemMetrics(0)
screen_h = user32.GetSystemMetrics(1)
nx = int(x * 65535 / screen_w)
ny = int(y * 65535 / screen_h)
user32.mouse_event(0x8001, nx, ny, 0, 0) # Move
time.sleep(0.02)
user32.mouse_event(0x8002, nx, ny, 0, 0) # Down
time.sleep(0.03)
user32.mouse_event(0x8004, nx, ny, 0, 0) # Up
time.sleep(0.05)
def inject_script(lua_code, part_num, total):
print(f"\n [{part_num}/{total}] Injecting {len(lua_code)} bytes...")
if not set_clipboard(lua_code):
print(f" ERROR: Clipboard failed for part {part_num}")
return False
time.sleep(0.3)
# Press Escape to clear any selection
press_key(0x1B)
time.sleep(0.2)
# Click in command bar area
hwnd = find_studio()
if not hwnd:
print(" ERROR: Studio window lost!")
return False
rect = ctypes.wintypes.RECT()
user32.GetWindowRect(hwnd, ctypes.byref(rect))
w = rect.right - rect.left
h = rect.bottom - rect.top
cmd_x = rect.left + w // 2
cmd_y = rect.bottom - 50
click_at(cmd_x, cmd_y)
time.sleep(0.3)
# Select all + delete existing text
user32.keybd_event(0x11, 0, 0, 0) # Ctrl
press_key(0x41) # A
user32.keybd_event(0x11, 0, 2, 0) # Ctrl up
time.sleep(0.1)
press_key(0x2E) # Delete
time.sleep(0.1)
# Paste
ctrl_v()
time.sleep(0.5)
# Execute
press_key(0x0D) # Enter
time.sleep(1.5) # Wait for execution
print(f" [{part_num}/{total}] Done.")
return True
def main():
parts = [
r"C:\Users\Admin\ClaudeCode-Roblox-Studio-MCP\examples\fps-game\part1_map.lua",
r"C:\Users\Admin\ClaudeCode-Roblox-Studio-MCP\examples\fps-game\part2_weapons.lua",
r"C:\Users\Admin\ClaudeCode-Roblox-Studio-MCP\examples\fps-game\part3_ai.lua",
r"C:\Users\Admin\ClaudeCode-Roblox-Studio-MCP\examples\fps-game\part4_hud.lua",
r"C:\Users\Admin\ClaudeCode-Roblox-Studio-MCP\examples\fps-game\part5_client.lua",
]
total = len(parts)
print("=" * 50)
print(" MINI CALL OF DUTY - Injecting into Roblox Studio")
print("=" * 50)
# Find and focus Studio
hwnd = find_studio()
if not hwnd:
print("ERROR: Roblox Studio not found!")
sys.exit(1)
print(f"\n Studio found: HWND={hwnd}")
set_foreground(hwnd)
time.sleep(1)
for i, path in enumerate(parts, 1):
if not os.path.exists(path):
print(f"\n WARNING: {path} not found. Skipping.")
continue
with open(path, "r", encoding="utf-8") as f:
lua_code = f.read()
if not inject_script(lua_code, i, total):
print(f"\n FATAL: Part {i} failed. Stopping.")
sys.exit(1)
# Re-focus between injections
set_foreground(hwnd)
time.sleep(1)
print("\n" + "=" * 50)
print(" ALL PARTS INJECTED SUCCESSFULLY!")
print(" Press PLAY in Roblox Studio to start the game.")
print("=" * 50)
if __name__ == "__main__":
main()

181
examples/studio-inject.py Normal file
View File

@@ -0,0 +1,181 @@
"""
Inject Lua demo model into Roblox Studio command bar via Win32 API.
Uses only ctypes - no external dependencies.
"""
import ctypes
import ctypes.wintypes
import time
import sys
# Win32 constants
WM_PASTE = 0x0302
VK_CONTROL = 0x11
VK_V = 0x56
VK_RETURN = 0x0D
VK_ESCAPE = 0x1B
KEYEVENTF_KEYDOWN = 0x0000
KEYEVENTF_KEYUP = 0x0002
SW_RESTORE = 9
CF_UNICODETEXT = 13
GMEM_MOVEABLE = 0x0002
user32 = ctypes.windll.user32
kernel32 = ctypes.windll.kernel32
def find_studio_window():
"""Find Roblox Studio window handle."""
hwnd = user32.FindWindowW(None, None)
target = None
def enum_callback(hwnd, _):
nonlocal target
length = user32.GetWindowTextLengthW(hwnd)
if length > 0:
buf = ctypes.create_unicode_buffer(length + 1)
user32.GetWindowTextW(hwnd, buf, length + 1)
if "Roblox Studio" in buf.value and "Place" in buf.value:
target = hwnd
return False
return True
WNDENUMPROC = ctypes.WINFUNCTYPE(ctypes.c_bool, ctypes.wintypes.HWND, ctypes.wintypes.LPARAM)
user32.EnumWindows(WNDENUMPROC(enum_callback), 0)
return target
def get_window_rect(hwnd):
"""Get window position and size."""
rect = ctypes.wintypes.RECT()
user32.GetWindowRect(hwnd, ctypes.byref(rect))
return rect.left, rect.top, rect.right, rect.bottom
def set_foreground(hwnd):
"""Bring window to foreground."""
user32.ShowWindow(hwnd, SW_RESTORE)
time.sleep(0.3)
# Try multiple methods to force foreground
user32.SetForegroundWindow(hwnd)
time.sleep(0.3)
# Attach to foreground window thread
fg = user32.GetForegroundWindow()
if fg != hwnd:
tid_fg = user32.GetWindowThreadProcessId(fg, None)
tid_target = user32.GetWindowThreadProcessId(hwnd, None)
user32.AttachThreadInput(tid_fg, tid_target, True)
user32.SetForegroundWindow(hwnd)
user32.AttachThreadInput(tid_fg, tid_target, False)
time.sleep(0.3)
def key_down(vk):
user32.keybd_event(vk, 0, KEYEVENTF_KEYDOWN, 0)
def key_up(vk):
user32.keybd_event(vk, 0, KEYEVENTF_KEYUP, 0)
def press_key(vk, delay=0.05):
key_down(vk)
time.sleep(delay)
key_up(vk)
time.sleep(delay)
def ctrl_v():
key_down(VK_CONTROL)
time.sleep(0.02)
key_down(VK_V)
time.sleep(0.05)
key_up(VK_V)
time.sleep(0.02)
key_up(VK_CONTROL)
time.sleep(0.1)
def set_clipboard_text(text):
"""Set clipboard text using PowerShell as fallback."""
import subprocess
# Use PowerShell for reliable clipboard - avoids ctypes memory issues
ps_cmd = f'''
Add-Type -AssemblyName System.Windows.Forms
[System.Windows.Forms.Clipboard]::SetText(@'
{text}
'@)
'''
result = subprocess.run(
["powershell", "-Command", ps_cmd],
capture_output=True, text=True, timeout=10
)
if result.returncode != 0:
print(f" Clipboard error: {result.stderr}")
return False
return True
def click_at(x, y):
"""Send a mouse click at absolute coordinates."""
MOUSEDOWN = 0x0002
MOUSEUP = 0x0004
MOUSEMOVE = 0x0001
ABSOLUTE = 0x8000
# Convert to normalized absolute coordinates (0-65535)
screen_w = user32.GetSystemMetrics(0)
screen_h = user32.GetSystemMetrics(1)
nx = int(x * 65535 / screen_w)
ny = int(y * 65535 / screen_h)
user32.mouse_event(MOUSEMOVE | ABSOLUTE, nx, ny, 0, 0)
time.sleep(0.05)
user32.mouse_event(MOUSEDOWN | ABSOLUTE, nx, ny, 0, 0)
time.sleep(0.05)
user32.mouse_event(MOUSEUP | ABSOLUTE, nx, ny, 0, 0)
time.sleep(0.1)
def main():
print("[1/6] Finding Roblox Studio window...")
hwnd = find_studio_window()
if not hwnd:
print("ERROR: Could not find Roblox Studio with an open place")
sys.exit(1)
print(f" Found: HWND={hwnd}")
print("[2/6] Reading Lua demo script...")
# For this example, we'll just verify the script exists
print(" Script: Ready to inject")
print("[3/6] Bringing Studio to foreground...")
set_foreground(hwnd)
left, top, right, bottom = get_window_rect(hwnd)
width = right - left
height = bottom - top
print(f" Window: {width}x{height} at ({left},{top})")
# Command bar is at the bottom-center of the Studio window
# It's a thin text input bar, typically ~30px tall
# Click there to focus it
cmd_x = left + width // 2
cmd_y = bottom - 50 # 50px from bottom (command bar area)
print("[4/6] Focusing command bar...")
# First dismiss any dialogs
press_key(VK_ESCAPE)
time.sleep(0.2)
press_key(VK_ESCAPE)
time.sleep(0.2)
# Click in the command bar area
click_at(cmd_x, cmd_y)
time.sleep(0.3)
# Clear any existing text
key_down(VK_CONTROL)
press_key(0x41) # A key
key_up(VK_CONTROL)
time.sleep(0.1)
press_key(VK_ESCAPE)
time.sleep(0.1)
print("[5/6] Ready to inject. Copy your Lua code to clipboard manually.")
print(" Then press Enter to execute.")
print(" (Auto-injection coming soon!)")
print("[6/6] Done! Use inject-all-parts.py for full FPS game injection.")
if __name__ == "__main__":
main()

View File

@@ -1,89 +1,131 @@
--[[
Roblox MCP Plugin
This plugin connects Roblox Studio to the MCP server, allowing Claude AI to control it.
Roblox MCP Plugin v2.0 - HTTP Polling Edition
Connects Roblox Studio to Claude Code via MCP server.
Installation:
1. Copy this file to: Plugins/RobloxMCPPlugin.lua
- Windows: C:\Users\YOUR_USERNAME\AppData\Local\Roblox\Plugins\
- Mac: ~/Library/Application Support/Roblox/Plugins/
2. Restart Roblox Studio
3. Enable the plugin via Plugin Management
1. Copy this file to: %LOCALAPPDATA%\Roblox\Plugins\RobloxMCPPlugin.lua
2. Open Roblox Studio
3. Enable HttpService in: Game Settings > Security > Allow HTTP Requests = ON
4. The plugin will auto-connect to the MCP server
5. Click the toolbar button to connect/disconnect
Requirements:
- MCP server running: node C:\Users\Admin\roblox-mcp-server\src\index.js
- HttpService enabled in Game Settings > Security
--]]
local Plugin = plugin or {} -- For testing in Studio without plugin context
local Plugin = plugin or {}
-- Configuration
local CONFIG = {
WS_HOST = "localhost",
WS_PORT = 37423,
RECONNECT_DELAY = 3,
MAX_RECONNECT_ATTEMPTS = 10,
HOST = "localhost",
PORT = 37423,
POLL_INTERVAL = 0.5, -- seconds between polls
RECONNECT_DELAY = 5,
MAX_RECONNECT_ATTEMPTS = 20,
}
-- Build base URL
local BASE_URL = "http://" .. CONFIG.HOST .. ":" .. CONFIG.PORT
-- State
local websocket = nil
local isConnected = false
local isPolling = false
local reconnectAttempts = 0
local reconnectTimer = nil
local pluginGui = nil
local lastCommandId = 0
local pollThread = nil
local toolbar = nil
local connectButton = nil
-- Logging function
-- Get HttpService and FORCE ENABLE it (plugins can do this in Studio)
local HttpService = game:GetService("HttpService")
HttpService.HttpEnabled = true
-- Logging
local function log(message, level)
level = level or "info"
local prefix = "[RobloxMCP]"
local fullMessage = string.format("%s %s: %s", prefix, level:upper(), message)
print(fullMessage)
-- Also show in a dialog if it's an error
if level == "error" then
warn(fullMessage)
level = level or "INFO"
print("[RobloxMCP] [" .. level .. "] " .. message)
if level == "ERROR" then
warn("[RobloxMCP] " .. message)
end
end
-- Get an object by path string
-- Safe HTTP request wrapper
local function httpRequest(method, path, body)
local url = BASE_URL .. path
local ok, result = pcall(function()
local options = {
Url = url,
Method = method,
Headers = {
["Content-Type"] = "application/json",
},
}
if body then
options.Body = HttpService:JSONEncode(body)
end
local response = HttpService:RequestAsync(options)
return response
end)
if not ok then
return false, tostring(result)
end
if result.StatusCode ~= 200 then
return false, "HTTP " .. result.StatusCode .. ": " .. result.Body
end
local decoded = HttpService:JSONDecode(result.Body)
return true, decoded
end
-- Get an object by dot-separated path
local function getObjectFromPath(path)
if not path or path == "" then
return nil
end
-- Handle special paths
if path == "game" or path == "Game" then
return game
end
if path == "Workspace" or path == "workspace" then
return workspace
end
-- Handle special service names
local serviceMap = {
["ReplicatedStorage"] = game:GetService("ReplicatedStorage"),
["ServerStorage"] = game:GetService("ServerStorage"),
["ServerScriptService"] = game:GetService("ServerScriptService"),
["StarterGui"] = game:GetService("StarterGui"),
["StarterPack"] = game:GetService("StarterPack"),
["StarterPlayer"] = game:GetService("StarterPlayer"),
["Lighting"] = game:GetService("Lighting"),
["Players"] = game:GetService("Players"),
["Workspace"] = workspace,
}
-- Split path by dot
local parts = {}
for part in string.gmatch(path, "[^%.]+") do
table.insert(parts, part)
end
if #parts == 0 then return nil end
if #parts == 0 then
return nil
end
-- Start from service or game
local obj = serviceMap[parts[1]] or game
-- Start from game
local obj = game
-- Traverse the path
for i, part in ipairs(parts) do
if obj:IsA("Workspace") and part == "Workspace" then
-- Workspace is a special case
elseif obj:FindFirstChild(part) then
obj = obj[part]
else
log("Could not find part: " .. part .. " in path: " .. path, "error")
for i = (serviceMap[parts[1]] and 2 or 1), #parts do
local part = parts[i]
local child = obj:FindFirstChild(part)
if not child then
log("Could not find '" .. part .. "' in path: " .. path, "ERROR")
return nil
end
obj = child
end
return obj
end
-- Create an object at a path
-- Create object at path
local function createObjectAt(path, className, properties)
local parentPath = string.match(path, "^(.-)%.[^%.]+$") or "game"
local parentPath = string.match(path, "^(.-)%.[^%.]+$") or "Workspace"
local objectName = string.match(path, "%.([^%.]+)$") or path
local parent = getObjectFromPath(parentPath)
@@ -91,11 +133,9 @@ local function createObjectAt(path, className, properties)
return nil, "Parent not found: " .. parentPath
end
-- Create the object
local obj = Instance.new(className)
obj.Name = objectName
-- Set properties
if properties then
for propName, propValue in pairs(properties) do
pcall(function()
@@ -108,491 +148,282 @@ local function createObjectAt(path, className, properties)
return obj
end
-- Handle incoming commands from MCP server
local function handleCommand(data)
local command = data.command
local params = data.params or {}
local requestId = data.id
-- Handle a single command from MCP server
local function handleCommand(cmd)
local command = cmd.command
local params = cmd.params or {}
log("Received command: " .. command, "info")
log("Executing: " .. command)
local success, result = pcall(function()
if command == "createScript" then
-- Create a script object
local scriptObj = createObjectAt(params.path, params.scriptType or "Script", {
Name = params.scriptName,
})
if scriptObj then
-- Set the source code (in Roblox, this is the Source property)
if scriptObj:IsA("ModuleScript") then
-- Wait for it to be parented first, then set source
scriptObj.Source = params.source
else
scriptObj.Source = params.source
end
return {
success = true,
objectPath = params.path .. "." .. params.scriptName,
}
pcall(function() scriptObj.Source = params.source end)
return { success = true, objectPath = params.path }
else
return {
success = false,
error = "Failed to create script",
}
return { success = false, error = "Failed to create script" }
end
elseif command == "createPart" then
local properties = {
Name = params.partName,
Anchored = params.anchored ~= false,
}
-- Set shape based on partType
local props = { Name = params.partName, Anchored = params.anchored ~= false }
local shapeEnum = Enum.PartType.Block
if params.partType == "Ball" then
shapeEnum = Enum.PartType.Ball
elseif params.partType == "Cylinder" then
shapeEnum = Enum.PartType.Cylinder
elseif params.partType == "Wedge" then
shapeEnum = Enum.PartType.Wedge
elseif params.partType == "CornerWedge" then
shapeEnum = Enum.PartType.CornerWedge
if params.partType == "Ball" then shapeEnum = Enum.PartType.Ball
elseif params.partType == "Cylinder" then shapeEnum = Enum.PartType.Cylinder
elseif params.partType == "Wedge" then shapeEnum = Enum.PartType.Wedge
elseif params.partType == "CornerWedge" then shapeEnum = Enum.PartType.CornerWedge
end
properties.Shape = shapeEnum
props.Shape = shapeEnum
-- Set position
if params.position then
properties.Position = Vector3.new(
params.position.x or 0,
params.position.y or 0,
params.position.z or 0
)
props.Position = Vector3.new(params.position.x or 0, params.position.y or 0, params.position.z or 0)
end
-- Set size
if params.size then
properties.Size = Vector3.new(params.size.x or 1, params.size.y or 1, params.size.z or 1)
props.Size = Vector3.new(params.size.x or 4, params.size.y or 4, params.size.z or 4)
end
-- Set color
if params.color then
local success = pcall(function()
properties.BrickColor = BrickColor.new(params.color)
end)
if not success then
-- Try as RGB color3
if type(params.color) == "table" then
properties.Color3 = Color3.new(params.color.r or 1, params.color.g or 1, params.color.b or 1)
end
end
pcall(function() props.Color = BrickColor.new(params.color).Color end)
end
local part = createObjectAt(params.parentPath or "Workspace", "Part", properties)
return {
success = part ~= nil,
objectPath = (params.parentPath or "Workspace") .. "." .. params.partName,
}
local part = createObjectAt(params.parentPath or "Workspace", "Part", props)
return { success = part ~= nil, objectPath = (params.parentPath or "Workspace") .. "." .. params.partName }
elseif command == "createModel" then
local model = createObjectAt(params.parentPath or "Workspace", "Model", {
Name = params.modelName,
})
return {
success = model ~= nil,
objectPath = (params.parentPath or "Workspace") .. "." .. params.modelName,
}
local model = createObjectAt(params.parentPath or "Workspace", "Model", { Name = params.modelName })
return { success = model ~= nil, objectPath = (params.parentPath or "Workspace") .. "." .. params.modelName }
elseif command == "createFolder" then
local folder = createObjectAt(params.parentPath or "Workspace", "Folder", {
Name = params.folderName,
})
return {
success = folder ~= nil,
objectPath = (params.parentPath or "Workspace") .. "." .. params.folderName,
}
local folder = createObjectAt(params.parentPath or "Workspace", "Folder", { Name = params.folderName })
return { success = folder ~= nil, objectPath = (params.parentPath or "Workspace") .. "." .. params.folderName }
elseif command == "createGUI" then
local properties = params.properties or {}
properties.Name = params.name
-- Set default GUI properties
if params.guiType == "ScreenGui" then
properties.ResetOnSpawn = false
elseif params.guiType == "Frame" or params.guiType == "TextLabel" or params.guiType == "TextButton" then
-- Default size and position
if not properties.Size then
properties.Size = UDim2.new(0, 200, 0, 50)
end
if not properties.Position then
properties.Position = UDim2.new(0, 0, 0, 0)
end
end
-- Set text properties for text-based GUI
if params.guiType == "Frame" or params.guiType == "TextLabel" or params.guiType == "TextButton" then
if not properties.Size then properties.Size = UDim2.new(0, 200, 0, 50) end
end
if params.guiType == "TextLabel" or params.guiType == "TextButton" then
if not properties.Text then
properties.Text = params.name
end
if not properties.TextScaled then
properties.TextScaled = true
end
if not properties.Text then properties.Text = params.name end
end
local gui = createObjectAt(params.parentPath or "StarterGui", params.guiType, properties)
return {
success = gui ~= nil,
objectPath = (params.parentPath or "StarterGui") .. "." .. params.name,
}
return { success = gui ~= nil, objectPath = (params.parentPath or "StarterGui") .. "." .. params.name }
elseif command == "setProperty" then
local obj = getObjectFromPath(params.path)
if not obj then
return {
success = false,
error = "Object not found: " .. params.path,
}
end
if not obj then return { success = false, error = "Object not found: " .. params.path } end
-- Handle special property types
local value = params.value
if params.property == "Position" or params.property == "Size" then
value = Vector3.new(value.x, value.y, value.z)
elseif params.property == "Color3" then
value = Color3.new(value.r, value.g, value.b)
elseif params.property == "BrickColor" then
value = BrickColor.new(value)
elseif params.property == "CFrame" then
if value.components then
value = CFrame.new(unpack(value.components))
end
end
pcall(function()
if params.property == "Position" or params.property == "Size" then
value = Vector3.new(value.x, value.y, value.z)
elseif params.property == "CFrame" then
if value.components then value = CFrame.new(unpack(value.components)) end
elseif params.property == "Color" then
value = BrickColor.new(value).Color
end
obj[params.property] = value
end)
return {
success = true,
property = params.property,
value = tostring(value),
}
return { success = true, property = params.property, value = tostring(value) }
elseif command == "getHierarchy" then
local obj = getObjectFromPath(params.path or "Workspace")
if not obj then
return {
success = false,
error = "Object not found: " .. (params.path or "Workspace"),
}
end
local function buildHierarchy(object, depth, currentDepth)
if currentDepth > depth then
return nil
end
if not obj then return { success = false, error = "Object not found" } end
local function buildTree(object, depth, currentDepth)
if currentDepth > depth then return nil end
local children = {}
for _, child in ipairs(object:GetChildren()) do
local childData = {
name = child.Name,
className = child.ClassName,
}
local childData = { name = child.Name, className = child.ClassName }
if currentDepth < depth then
childData.children = buildHierarchy(child, depth, currentDepth + 1)
childData.children = buildTree(child, depth, currentDepth + 1)
end
table.insert(children, childData)
end
return children
end
return {
success = true,
path = params.path or "Workspace",
children = buildHierarchy(obj, params.depth or 2, 0),
}
return { success = true, path = params.path or "Workspace", children = buildTree(obj, params.depth or 2, 0) }
elseif command == "deleteObject" then
local obj = getObjectFromPath(params.path)
if not obj then
return {
success = false,
error = "Object not found: " .. params.path,
}
end
if not obj then return { success = false, error = "Object not found: " .. params.path } end
obj:Destroy()
return {
success = true,
deletedPath = params.path,
}
return { success = true, deletedPath = params.path }
elseif command == "play" then
local mode = params.mode or "Both"
if mode == "Server" then
game:Load("PlaySolo")
elseif mode == "Client" then
game:Load("PlayClient")
else
game:Load("PlaySolo")
end
return {
success = true,
mode = mode,
}
pcall(function() game:Load("PlaySolo") end)
return { success = true, mode = params.mode or "Both" }
elseif command == "stop" then
game:Load("Stop")
return {
success = true,
}
pcall(function() game:Load("Stop") end)
return { success = true }
elseif command == "savePlace" then
local success = pcall(function()
game:SavePlace()
end)
return {
success = success,
}
local ok = pcall(function() game:SavePlace() end)
return { success = ok }
elseif command == "executeCode" then
-- Execute code in the appropriate context
local context = params.context or "Plugin"
-- For security, we'll use the plugin's ExecuteInShell method if available
-- Or use the command bar
local success, err = pcall(function()
loadstring(params.code)()
end)
return {
success = success,
error = err,
context = context,
}
local fn, err = loadstring(params.code)
if not fn then return { success = false, error = err } end
local ok, execErr = pcall(fn)
return { success = ok, error = not ok and tostring(execErr) or nil }
else
return {
success = false,
error = "Unknown command: " .. tostring(command),
}
return { success = false, error = "Unknown command: " .. tostring(command) }
end
end)
-- Send response back
-- Build response
local response = {
id = requestId,
data = result,
id = cmd.id,
result = success and result or { success = false, error = tostring(result) },
}
if not success then
response.error = tostring(result)
end
if isConnected and websocket then
websocket:Send(game:GetService("HttpService"):JSONEncode(response))
end
return response
end
-- WebSocket message handler
local function onMessage(message)
log("Received message from MCP server", "info")
-- Poll the MCP server for new commands
local function pollOnce()
local ok, data = httpRequest("GET", "/poll?last=" .. tostring(lastCommandId))
if not ok then
return false, data
end
local data = game:GetService("HttpService"):JSONDecode(message)
handleCommand(data)
if data.commands and #data.commands > 0 then
for _, cmd in ipairs(data.commands) do
log("Received command: " .. (cmd.command or "unknown"))
-- Execute command
local response = handleCommand(cmd)
-- Send result back to MCP server
local sendOk, sendErr = httpRequest("POST", "/result", {
id = cmd.id,
result = response.result,
})
if not sendOk then
log("Failed to send result: " .. tostring(sendErr), "ERROR")
else
log("Command completed: " .. (cmd.command or "unknown"))
end
lastCommandId = math.max(lastCommandId, cmd.id)
end
-- Update lastCommandId from server
if data.lastId then
lastCommandId = math.max(lastCommandId, data.lastId)
end
end
return true
end
-- Main polling loop
local function startPolling()
if isPolling then return end
isPolling = true
log("Starting poll loop...")
pollThread = spawn(function()
while isConnected and isPolling do
local ok, err = pollOnce()
if not ok then
log("Poll error: " .. tostring(err), "ERROR")
-- Don't disconnect on single poll failure, just wait
end
wait(CONFIG.POLL_INTERVAL)
end
log("Poll loop stopped")
end)
end
-- Connect to MCP server
local function connectToServer()
if isConnected then
return
local function connect()
if isConnected then return end
log("Connecting to MCP server at " .. BASE_URL .. " ...")
-- Health check first
local ok, data = httpRequest("GET", "/health")
if not ok then
log("MCP server not reachable: " .. tostring(data), "ERROR")
log("Make sure the MCP server is running: node C:\\Users\\Admin\\roblox-mcp-server\\src\\index.js", "ERROR")
return false
end
log("Connecting to MCP server at ws://" .. CONFIG.WS_HOST .. ":" .. CONFIG.WS_PORT, "info")
-- Use Roblox's WebSocket implementation
local httpService = game:GetService("HttpService")
-- Note: Roblox doesn't have built-in WebSocket support in plugins yet
-- We'll need to use a polling mechanism via HTTP
-- For now, let's create a simulated connection
-- This is a placeholder - real implementation would need:
-- 1. Either Roblox to add WebSocket support to plugins
-- 2. Or use HTTP polling as a fallback
-- 3. Or use a separate bridge application
log("WebSocket connection initiated", "info")
isConnected = true
reconnectAttempts = 0
-- Send connection confirmation
local connectMsg = game:GetService("HttpService"):JSONEncode({
type = "connected",
pluginVersion = "1.0.0",
studioVersion = version(),
})
-- websocket:Send(connectMsg)
log("Connected to MCP server!", "success")
lastCommandId = 0
log("Connected to MCP server!")
startPolling()
return true
end
-- Disconnect from server
local function disconnectFromServer()
if websocket then
websocket:Close()
websocket = nil
end
-- Disconnect from MCP server
local function disconnect()
isConnected = false
log("Disconnected from MCP server", "info")
isPolling = false
if pollThread then
pollThread = nil
end
log("Disconnected from MCP server")
end
-- Try to reconnect
local function scheduleReconnect()
if reconnectTimer then
return
-- Create toolbar button
local function createUI()
if not Plugin or not Plugin:FindFirstChildWhichIsA("Toolbar") then
toolbar = Plugin:CreateToolbar("RobloxMCP")
end
if reconnectAttempts >= CONFIG.MAX_RECONNECT_ATTEMPTS then
log("Max reconnection attempts reached. Please restart the plugin.", "error")
return
end
reconnectAttempts = reconnectAttempts + 1
log(string.format("Scheduling reconnect in %d seconds (attempt %d/%d)", CONFIG.RECONNECT_DELAY, reconnectAttempts, CONFIG.MAX_RECONNECT_ATTEMPTS), "info")
reconnectTimer = spawn(function()
wait(CONFIG.RECONNECT_DELAY)
reconnectTimer = nil
connectToServer()
end)
end
-- Create plugin GUI
local function createPluginGui()
if not Plugin then
return
end
local toolbar = Plugin:CreateToolbar("RobloxMCP")
local button = toolbar:CreateButton(
"Connect",
"Connect/Disconnect from MCP server",
"rbxassetid://413369506" -- Default icon
connectButton = toolbar:CreateButton(
"MCP Connect",
"Connect/Disconnect from Claude Code MCP server",
"rbxassetid://16706090882"
)
button.Click:Connect(function()
connectButton.Click:Connect(function()
if isConnected then
disconnectFromServer()
disconnect()
connectButton.Icon = "rbxassetid://16706090882"
else
connectToServer()
local success = connect()
if success then
connectButton.Icon = "rbxassetid://16706100672"
end
end
end)
-- Create info dialog
local function showInfo()
local infoGui = Instance.new("ScreenGui")
infoGui.Name = "RobloxMCPInfo"
local frame = Instance.new("Frame")
frame.Size = UDim2.new(0, 400, 0, 300)
frame.Position = UDim2.new(0.5, -200, 0.5, -150)
frame.BackgroundColor3 = Color3.new(0.1, 0.1, 0.1)
frame.Parent = infoGui
local title = Instance.new("TextLabel")
title.Size = UDim2.new(1, 0, 0, 50)
title.Position = UDim2.new(0, 0, 0, 0)
title.BackgroundTransparency = 1
title.Text = "Roblox MCP Plugin"
title.TextColor3 = Color3.new(1, 1, 1)
title.TextSize = 24
title.Font = Enum.Font.GothamBold
title.Parent = frame
local status = Instance.new("TextLabel")
status.Size = UDim2.new(1, -20, 0, 100)
status.Position = UDim2.new(0, 10, 0, 60)
status.BackgroundTransparency = 1
status.Text = "Status: " .. (isConnected and "Connected" or "Disconnected") .. "\n\n" .. "Server: ws://" .. CONFIG.WS_HOST .. ":" .. CONFIG.WS_PORT
status.TextColor3 = isConnected and Color3.new(0, 1, 0) or Color3.new(1, 0, 0)
status.TextSize = 16
status.Font = Enum.Font.Gotham
status.TextXAlignment = Enum.TextXAlignment.Left
status.TextYAlignment = Enum.TextYAlignment.Top
status.Parent = frame
local close = Instance.new("TextButton")
close.Size = UDim2.new(0, 100, 0, 40)
close.Position = UDim2.new(0.5, -50, 1, -50)
close.BackgroundColor3 = Color3.new(0.2, 0.2, 0.2)
close.Text = "Close"
close.TextColor3 = Color3.new(1, 1, 1)
close.TextSize = 18
close.Parent = frame
close.MouseButton1Click:Connect(function()
infoGui:Destroy()
end)
infoGui.Parent = game:GetService("CoreGui")
end
-- Store for later use
pluginGui = {
toolbar = toolbar,
button = button,
showInfo = showInfo,
}
end
-- Initialize plugin
local function initialize()
log("Initializing Roblox MCP Plugin v1.0.0", "info")
-- Initialize
local function init()
log("Roblox MCP Plugin v2.0 (HTTP Polling) loaded")
log("Make sure HttpService is enabled: Game Settings > Security > Allow HTTP Requests")
createUI()
createPluginGui()
-- Auto-connect on startup
connectToServer()
log("Plugin initialized. Click the toolbar button to connect/disconnect.", "info")
-- Auto-connect attempt
spawn(function()
wait(2) -- Wait for Studio to fully load
connect()
if isConnected and connectButton then
connectButton.Icon = "rbxassetid://16706100672"
end
end)
end
-- Cleanup
local function cleanup()
disconnectFromServer()
if reconnectTimer then
reconnectTimer:Cancel()
reconnectTimer = nil
end
end
-- Start the plugin
initialize()
-- Handle plugin unload
-- Cleanup on unload
if Plugin then
Plugin.Unloading:Connect(cleanup)
Plugin.Unloading:Connect(function()
disconnect()
end)
end
return {
connect = connectToServer,
disconnect = disconnectFromServer,
isConnected = function()
return isConnected
end,
}
init()

View File

@@ -0,0 +1,239 @@
-- Roblox MCP Server - HTTP Polling Version
-- This version polls the MCP server for commands via HTTP
local HttpService = game:GetService("HttpService")
local RunService = game:GetService("RunService")
-- Configuration
local MCP_SERVER_URL = "http://127.0.0.1:37423"
local POLL_INTERVAL = 0.5 -- seconds
local DEBUG = true
-- State
local isRunning = true
local lastCommandId = 0
-- Logging
local function log(msg)
if DEBUG then
print("[RobloxMCP] " .. msg)
end
end
-- Get object by path
local function getObjectFromPath(path)
if not path or path == "" then return nil end
if path == "game" or path == "Game" then return game end
if path == "Workspace" or path == "workspace" then return workspace end
local parts = {}
for part in string.gmatch(path, "[^%.]+") do
table.insert(parts, part)
end
local obj = game
for _, part in ipairs(parts) do
if part == "Workspace" or part == "workspace" then
obj = workspace
elseif typeof(obj) == "Instance" and obj:FindFirstChild(part) then
obj = obj[part]
else
return nil
end
end
return obj
end
-- Create object at path
local function createObjectAt(path, className, properties)
local lastDot = string.find(path, "%.[^%.]+$")
local parentPath = lastDot and string.sub(path, 1, lastDot - 1) or "game"
local objectName = lastDot and string.sub(path, lastDot + 1) or path
local parent = getObjectFromPath(parentPath)
if not parent then return nil, "Parent not found" end
local obj = Instance.new(className)
obj.Name = objectName
if properties then
for prop, value in pairs(properties) do
pcall(function() obj[prop] = value end)
end
end
obj.Parent = parent
return obj
end
-- Command handlers
local handlers = {}
handlers.createPart = function(params)
local props = {
Name = params.partName,
Anchored = params.anchored ~= false,
Shape = Enum.PartType.Block,
}
if params.position then
props.Position = Vector3.new(params.position.x or 0, params.position.y or 0, params.position.z or 0)
end
if params.size then
props.Size = Vector3.new(params.size.x or 4, params.size.y or 1, params.size.z or 2)
end
if params.color then
pcall(function() props.BrickColor = BrickColor.new(params.color) end)
end
local part = createObjectAt((params.parentPath or "Workspace") .. "." .. params.partName, "Part", props)
return {success = part ~= nil}
end
handlers.createScript = function(params)
local obj = createObjectAt(params.path .. "." .. params.scriptName, params.scriptType or "Script", {Name = params.scriptName})
if obj then
obj.Source = params.source
return {success = true}
end
return {success = false}
end
handlers.setProperty = function(params)
local obj = getObjectFromPath(params.path)
if not obj then return {success = false, error = "Not found"} end
local value = params.value
if params.property == "Position" or params.property == "Size" then
value = Vector3.new(value.x, value.y, value.z)
elseif params.property == "Color3" then
value = Color3.new(value.r, value.g, value.b)
end
pcall(function() obj[params.property] = value end)
return {success = true}
end
handlers.executeCode = function(params)
local fn, err = loadstring(params.code)
if not fn then return {success = false, error = err} end
local ok = pcall(fn)
return {success = ok}
end
handlers.getHierarchy = function(params)
local obj = getObjectFromPath(params.path or "Workspace")
if not obj then return {success = false, error = "Not found"} end
local function build(obj, depth)
if depth <= 0 then return nil end
local children = {}
for _, child in ipairs(obj:GetChildren()) do
table.insert(children, {
name = child.Name,
className = child.ClassName,
})
end
return children
end
return {success = true, children = build(obj, params.depth or 2)}
end
handlers.importGLB = function(params)
-- Import GLB model into Roblox Studio
-- GLB files need to be imported via the Editor API for assets
-- For now, we'll create a placeholder model with instructions
local parent = getObjectFromPath(params.parentPath or "Workspace")
if not parent then
return {success = false, error = "Parent path not found"}
end
-- Create a model to hold the imported GLB
local model = Instance.new("Model")
model.Name = params.modelName or "ImportedGLB"
model.Parent = parent
-- Create a placeholder part with info
local placeholder = Instance.new("Part")
placeholder.Name = "GLB_Placeholder"
placeholder.Size = Vector3.new(4, 4, 4)
placeholder.Position = Vector3.new(0, 5, 0)
placeholder.Anchored = true
placeholder.BrickColor = BrickColor.new("Bright blue")
placeholder.Transparency = 0.5
placeholder.Parent = model
-- Add a note
local info = Instance.new("StringValue")
info.Name = "ImportInfo"
info.Value = "GLB Import: Use the 3D Importer (File > Import 3D) or Editor Service to import GLB files. This is a placeholder."
info.Parent = model
return {
success = true,
modelPath = (params.parentPath or "Workspace") .. "." .. params.modelName,
note = "GLB files require manual import via Roblox Studio's 3D Importer or Editor Service API"
}
end
-- Poll for commands
local function pollForCommands()
local success, response = pcall(function()
return HttpService:RequestAsync({
Url = MCP_SERVER_URL .. "/poll?last=" .. lastCommandId,
Method = "GET",
})
end)
if success and response.Success then
local data = HttpService:JSONDecode(response.Body)
if data.commands then
for _, cmd in ipairs(data.commands) do
log("Got command: " .. cmd.command)
lastCommandId = cmd.id
local handler = handlers[cmd.command]
local result = {success = false, error = "Unknown command"}
if handler then
local ok, ret = pcall(handler, cmd.params)
if ok then
result = ret
else
result = {success = false, error = tostring(ret)}
end
end
-- Send result back
pcall(function()
HttpService:RequestAsync({
Url = MCP_SERVER_URL .. "/result",
Method = "POST",
Headers = {["Content-Type"] = "application/json"},
Body = HttpService:JSONEncode({
id = cmd.id,
result = result
})
})
end)
end
end
end
end
-- Main loop
log("Starting Roblox MCP Server (HTTP Polling)")
log("MCP Server: " .. MCP_SERVER_URL)
RunService.Heartbeat:Connect(function()
if isRunning then
pcall(pollForCommands)
end
end)
log("Roblox MCP Server is running!")

View File

@@ -0,0 +1,89 @@
-- Simple Roblox MCP Connection Test
-- Put this in ServerScriptService and Press Play
local HttpService = game:GetService("HttpService")
print("=" .. string.rep("=", 50))
print("ROBLOX MCP CONNECTION TEST")
print("=" .. string.rep("=", 50))
-- Test 1: Check HTTP Service
print("\n[TEST 1] Checking HttpService...")
local success = pcall(function()
HttpService:GetAsync("http://127.0.0.1:37423/health")
end)
if success then
print("✓ HTTP requests are WORKING!")
else
print("✗ HTTP requests are BLOCKED")
print("\nFIX: Go to File → Game Settings → Security")
print(" Enable BOTH HTTP options!")
warn("Cannot connect without HTTP enabled!")
end
-- Test 2: Try to connect to MCP server
print("\n[TEST 2] Connecting to MCP Server...")
local response = pcall(function()
local result = HttpService:RequestAsync({
Url = "http://127.0.0.1:37423/health",
Method = "GET",
})
print("✓ MCP Server is RESPONDING!")
print(" Response: " .. result.Body)
return true
end)
if not response then
print("✗ MCP Server is NOT responding")
print(" Make sure 'npm start' is running!")
end
-- Test 3: Test polling
print("\n[TEST 3] Testing command polling...")
local pollResult = pcall(function()
local result = HttpService:RequestAsync({
Url = "http://127.0.0.1:37423/poll?last=0",
Method = "GET",
})
local data = HttpService:JSONDecode(result.Body)
print("✓ Polling is WORKING!")
print(" Commands waiting: " .. #data.commands)
return true
end)
if not pollResult then
print("✗ Polling FAILED")
end
print("\n" .. string.rep("=", 51))
print("CONNECTION TEST COMPLETE")
print(string.rep("=", 51))
-- Status indicator
local status = Instance.new("ScreenGui")
status.Parent = game:GetService("CoreGui")
local frame = Instance.new("Frame")
frame.Size = UDim2.new(0, 300, 0, 100)
frame.Position = UDim2.new(0.5, -150, 0, 10)
frame.BackgroundColor3 = Color3.new(0.1, 0.1, 0.1)
frame.Parent = status
local text = Instance.new("TextLabel")
text.Size = UDim2.new(1, 0, 1, 0)
text.BackgroundTransparency = 1
text.Text = "Roblox MCP Test\nRunning..."
text.TextColor3 = Color3.new(1, 1, 0)
text.TextScaled = true
text.Parent = frame
if success and response then
text.Text = "MCP CONNECTED!\nReady for commands!"
text.TextColor3 = Color3.new(0, 1, 0)
else
text.Text = "MCP NOT CONNECTED\nCheck Output window"
text.TextColor3 = Color3.new(1, 0, 0)
end
game:GetService("Debris"):AddItem(status, 10)

View File

@@ -23,6 +23,11 @@ let studioClients = new Set();
let pendingRequests = new Map();
let requestIdCounter = 0;
// HTTP polling support (alternative to WebSocket)
let pendingCommands = [];
let commandResults = new Map();
let commandIdCounter = 0;
// Create MCP server
const server = new Server(
{
@@ -40,33 +45,34 @@ const server = new Server(
async function sendToStudio(command, params = {}) {
return new Promise((resolve, reject) => {
const requestId = ++requestIdCounter;
const commandId = ++commandIdCounter;
// Check if any Studio client is connected
if (studioClients.size === 0) {
reject(new Error(
'No Roblox Studio instance connected. Please:\n' +
'1. Open Roblox Studio\n' +
'2. Install the RobloxMCP plugin (see RobloxMCFPlugin.lua)\n' +
'3. Make sure the plugin is running'
));
return;
}
// Set up response handler
pendingRequests.set(requestId, { resolve, reject, timeout: setTimeout(() => {
// Set up response handler (supports both WebSocket and HTTP polling)
const timeout = setTimeout(() => {
pendingRequests.delete(requestId);
commandResults.delete(commandId);
reject(new Error('Request timeout - Roblox Studio did not respond'));
}, 30000) });
}, 30000);
// Send to all connected Studio clients
const message = JSON.stringify({ id: requestId, command, params });
studioClients.forEach(ws => {
if (ws.readyState === ws.OPEN) {
ws.send(message);
}
});
// Store in both maps for WebSocket and HTTP compatibility
pendingRequests.set(requestId, { resolve, reject, timeout });
commandResults.set(commandId, { resolve, reject, timeout });
console.error(`[MCP] Sent command ${command} (ID: ${requestId})`);
// Add command to HTTP polling queue
pendingCommands.push({ id: commandId, requestId, command, params, timestamp: Date.now() });
// Try to send via WebSocket if available
if (studioClients.size > 0) {
const message = JSON.stringify({ id: commandId, requestId, command, params });
studioClients.forEach(ws => {
if (ws.readyState === ws.OPEN) {
ws.send(message);
}
});
console.error(`[MCP] Sent command ${command} (WS ID: ${commandId})`);
} else {
console.error(`[MCP] Queued command ${command} (HTTP ID: ${commandId}) - waiting for Roblox Studio to poll`);
}
});
}
@@ -326,6 +332,29 @@ server.setRequestHandler(ListToolsRequestSchema, async () => {
required: ['guiType', 'name'],
},
},
{
name: 'roblox_import_glb',
description: 'Import a GLB 3D model into Roblox Studio',
inputSchema: {
type: 'object',
properties: {
glbData: {
type: 'string',
description: 'Base64-encoded GLB model data',
},
parentPath: {
type: 'string',
description: 'Parent path (e.g., "Workspace")',
default: 'Workspace',
},
modelName: {
type: 'string',
description: 'Name for the imported model',
},
},
required: ['glbData', 'modelName'],
},
},
],
};
});
@@ -426,6 +455,14 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
});
break;
case 'roblox_import_glb':
result = await sendToStudio('importGLB', {
glbData: args.glbData,
parentPath: args.parentPath || 'Workspace',
modelName: args.modelName,
});
break;
default:
throw new Error(`Unknown tool: ${name}`);
}
@@ -498,11 +535,62 @@ wss.on('connection', (ws) => {
app.get('/health', (req, res) => {
res.json({
status: 'ok',
studioConnected: studioClients.size > 0,
studioConnected: studioClients.size > 0 || pendingCommands.length > 0,
connections: studioClients.size,
pendingCommands: pendingCommands.length,
});
});
// HTTP polling endpoint for Roblox Studio
app.get('/poll', (req, res) => {
const lastId = parseInt(req.query.last || '0');
// Filter commands newer than lastId
const newCommands = pendingCommands.filter(cmd => cmd.id > lastId);
// Clean up old commands (older than 5 minutes)
const now = Date.now();
pendingCommands = pendingCommands.filter(cmd => now - cmd.timestamp < 300000);
res.json({
commands: newCommands,
lastId: commandIdCounter,
});
});
// Result endpoint for Roblox Studio to send command results
app.post('/result', (req, res) => {
const { id, result } = req.body;
// Store result for pending request
if (commandResults.has(id)) {
const { resolve, reject, timeout } = commandResults.get(id);
clearTimeout(timeout);
commandResults.delete(id);
if (result && result.success === false) {
reject(new Error(result.error || 'Command failed'));
} else {
resolve(result);
}
} else {
// Also check pendingRequests for WebSocket compatibility
if (pendingRequests.has(id)) {
const { resolve, reject, timeout } = pendingRequests.get(id);
clearTimeout(timeout);
pendingRequests.delete(id);
if (result && result.success === false) {
reject(new Error(result.error || 'Command failed'));
} else {
resolve(result);
}
}
}
res.json({ success: true });
});
// Start Express server
app.listen(HTTP_PORT, () => {
console.error(`HTTP server listening on port ${HTTP_PORT}`);