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