diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..69a5a82ba4c2484f91571f85559cd7d510ae0104 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 callmerem + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/STRUCTURE.md b/STRUCTURE.md new file mode 100644 index 0000000000000000000000000000000000000000..dd42a44d2a417cbc4b609cd70405ec869b3472df --- /dev/null +++ b/STRUCTURE.md @@ -0,0 +1,101 @@ +## Project Structure + +```text +TimberWoods/ +├── references/ +├── src/ +│ ├── ReplicatedStorage/ +│ │ ├── Events/ +│ │ │ ├── ChopEvent.model.json +│ │ │ ├── ClaimPlot.model.json +│ │ │ ├── CraftEvent.model.json +│ │ │ ├── DialogueEvent.model.json +│ │ │ ├── DragEvent.model.json +│ │ │ ├── DropEvent.model.json +│ │ │ ├── EquipToolEvent.model.json +│ │ │ ├── FillBlueprintEvent.model.json +│ │ │ ├── MarketUpdateEvent.model.json +│ │ │ ├── NotificationEvent.model.json +│ │ │ ├── PlaceBlueprintEvent.model.json +│ │ │ ├── PurchaseEvent.model.json +│ │ │ ├── QuestUpdateEvent.model.json +│ │ │ ├── SettingsEvent.model.json +│ │ │ ├── ShopEvent.model.json +│ │ │ ├── SoundEvent.model.json +│ │ │ └── WireConnectEvent.model.json +│ │ └── Shared/ +│ │ ├── AutomationConfig.lua +│ │ ├── BiomeConfig.lua +│ │ ├── BuildingConfig.lua +│ │ ├── ChoppingConfig.lua +│ │ ├── Constants.lua +│ │ ├── CraftingConfig.lua +│ │ ├── DraggingConfig.lua +│ │ ├── EconomyConfig.lua +│ │ ├── ExplorationConfig.lua +│ │ ├── GameConfig.lua +│ │ ├── MutationConfig.lua +│ │ ├── NPCConfig.lua +│ │ ├── PlotConfig.lua +│ │ ├── QuestConfig.lua +│ │ ├── ShopConfig.lua +│ │ ├── SoundConfig.lua +│ │ ├── TreeModelConfig.lua +│ │ ├── Utility.lua +│ │ ├── VehicleConfig.lua +│ │ ├── WeatherConfig.lua +│ │ └── WireLogicConfig.lua +│ ├── ServerScriptService/ +│ │ ├── AchievementManager.server.lua +│ │ ├── AntiCheatManager.server.lua +│ │ ├── BiomeGeneratorManager.server.lua +│ │ ├── ConstructionManager.server.lua +│ │ ├── CraftingManager.server.lua +│ │ ├── DatastoreManager.server.lua +│ │ ├── DayNightManager.server.lua +│ │ ├── DragManager.server.lua +│ │ ├── DroneAutomationManager.server.lua +│ │ ├── EnvironmentalPuzzleManager.server.lua +│ │ ├── InventoryManager.server.lua +│ │ ├── LeaderboardManager.server.lua +│ │ ├── MapManager.server.lua +│ │ ├── MarketManager.server.lua +│ │ ├── MutationManager.server.lua +│ │ ├── NPCManager.server.lua +│ │ ├── PlayerSetupManager.server.lua +│ │ ├── PlotManager.server.lua +│ │ ├── QuestManager.server.lua +│ │ ├── RespawnManager.server.lua +│ │ ├── SawmillManager.server.lua +│ │ ├── ShopManager.server.lua +│ │ ├── SoundscapeManager.server.lua +│ │ ├── TreeManager.server.lua +│ │ ├── TreeSpawnerManager.server.lua +│ │ ├── VehicleLogisticsManager.server.lua +│ │ ├── WeatherManager.server.lua +│ │ └── WireLogicManager.server.lua +│ ├── StarterGui/ +│ │ ├── CraftingGUI.client.lua +│ │ ├── DialogueGUI.client.lua +│ │ ├── InventoryGUI.client.lua +│ │ ├── MainHUD.client.lua +│ │ ├── MarketGUI.client.lua +│ │ ├── QuestGUI.client.lua +│ │ ├── SettingsGUI.client.lua +│ │ └── ShopGUI.client.lua +│ └── StarterPlayer/ +│ └── StarterCharacterScripts/ +│ ├── AxeController.client.lua +│ ├── BuildController.client.lua +│ ├── DragController.client.lua +│ └── SoundController.client.lua +├── aftman.toml +├── default.project.json +├── LICENSE +├── mcp.json +├── roblox-studio-antigravity-mcp-guide.md +├── robloxstudio-mcp-system-mechanics.md +├── studio-rust-mcp-server-system-mechanics.md +├── studio_setup_guide.md +└── TECHSTACK.md +``` diff --git a/TECHSTACK.md b/TECHSTACK.md new file mode 100644 index 0000000000000000000000000000000000000000..61b89248806dd4e5b10adfd70c22c2d4585bc748 --- /dev/null +++ b/TECHSTACK.md @@ -0,0 +1,12 @@ +## Techstack + +Audit of **TimberWoods** project files (excluding environment and cache): + +| File Type | Count | Size (KB) | +| :--- | :--- | :--- | +| Lua (.lua) | 61 | 212.2 | +| JSON (.json) | 19 | 1.8 | +| Markdown (.md) | 4 | 17.8 | +| (no extension) | 1 | 1.1 | +| TOML (.toml) | 1 | 0.3 | +| **Total** | **86** | **233.1** | diff --git a/aftman.toml b/aftman.toml new file mode 100644 index 0000000000000000000000000000000000000000..dc49c6e80ad2a605381d1b2a463c205465189827 --- /dev/null +++ b/aftman.toml @@ -0,0 +1,7 @@ +# This file lists tools managed by Aftman, a cross-platform toolchain manager. +# For more information, see https://github.com/LPGhatguy/aftman + +# To add a new tool, add an entry to this table. +[tools] +rojo = "rojo-rbx/rojo@7.7.0-rc.1" +# rojo = "rojo-rbx/rojo@6.2.0" \ No newline at end of file diff --git a/default.project.json b/default.project.json new file mode 100644 index 0000000000000000000000000000000000000000..41e377890148da342f6526182564eb3831751e25 --- /dev/null +++ b/default.project.json @@ -0,0 +1,36 @@ +{ + "name": "TimberboundExpeditions", + "tree": { + "$className": "DataModel", + "ReplicatedStorage": { + "$className": "ReplicatedStorage", + "Shared": { + "$path": "src/ReplicatedStorage/Shared" + }, + "Events": { + "$path": "src/ReplicatedStorage/Events" + } + }, + "ServerScriptService": { + "$className": "ServerScriptService", + "TimberboundServer": { + "$path": "src/ServerScriptService" + } + }, + "StarterPlayer": { + "$className": "StarterPlayer", + "StarterCharacterScripts": { + "$className": "StarterCharacterScripts", + "TimberboundClient": { + "$path": "src/StarterPlayer/StarterCharacterScripts" + } + } + }, + "StarterGui": { + "$className": "StarterGui", + "TimberboundGUI": { + "$path": "src/StarterGui" + } + } + } +} \ No newline at end of file diff --git a/mcp.json b/mcp.json new file mode 100644 index 0000000000000000000000000000000000000000..2c8f1a003f83ba96a6438ac768703ad9996e2ce2 --- /dev/null +++ b/mcp.json @@ -0,0 +1,10 @@ +{ + "mcpServers": { + "Roblox Studio": { + "command": "C:\\Users\\User\\Desktop\\VSCode2\\RobloxStudio-MCP-GoogleAntigravity\\references\\studio-rust-mcp-server-main\\target\\release\\rbx-studio-mcp.exe", + "args": [ + "--stdio" + ] + } + } +} \ No newline at end of file diff --git a/roblox-studio-antigravity-mcp-guide.md b/roblox-studio-antigravity-mcp-guide.md new file mode 100644 index 0000000000000000000000000000000000000000..05bd199a736d57bb9cc149a1ba01eb2f0c20795c --- /dev/null +++ b/roblox-studio-antigravity-mcp-guide.md @@ -0,0 +1,74 @@ +# Integrating Google Antigravity with Roblox Studio via Rust MCP + +This guide outlines exactly how to connect the Google Antigravity system (running within VS Code) natively to your Roblox Studio instance utilizing the high-performance **Rust Model Context Protocol (MCP)** implementation. + +--- + +## Step 1: Prepare the Rust Environment + +The `studio-rust-mcp-server-main` reference must first be built into a local binary. + +1. Open your terminal inside the `studio-rust-mcp-server-main` repository directory. +2. Ensure you have Rust and Cargo installed via rustup. +3. Build the server for release: + ```bash + cargo build --release + ``` +4. This will output a binary executable located at `target\release\rbx-studio-mcp.exe`. + +## Step 2: Configure Google Antigravity (VS Code) + +Google Antigravity leverages a configuration file named `mcp.json` inside your VS Code user data directory to identify connected AI bridging servers. + +1. Navigate to your VS Code data folder. On Windows, this is typically located at: + `C:\Users\User\AppData\Roaming\Code\User\mcp.json` + *(If the file does not exist, create it).* +2. Add the `Roblox Studio` entry to the `mcpServers` object, directing it to your compiled executable and passing the required `--stdio` flag. The file should look like this: + + ```json + { + "mcpServers": { + "Roblox Studio": { + "command": "C:\\Users\\User\\Desktop\\VSCode2\\RobloxStudio-MCP-GoogleAntigravity\\references\\studio-rust-mcp-server-main\\target\\release\\rbx-studio-mcp.exe", + "args": [ + "--stdio" + ] + } + } + } + ``` + +## Step 3: Install the Studio Component Plugin + +The Rust MCP server utilizes a native Roblox Studio plugin (`MCPStudioPlugin.rbxm`) to communicate over local ports via long polling. The compiled server binary can install this plugin automatically. + +1. Run the `.exe` directly **without any arguments** from your command line: + ```bash + .\target\release\rbx-studio-mcp.exe + ``` +2. The installer will automatically extract and inject the plugin directly into your local Roblox plugins directory (`%LOCALAPPDATA%\Roblox\Plugins`). + +## Step 4: Finalizing the Integration + +You must make both systems aware of the new bridging connections. + +1. **Reload VS Code**: Because you just created/modified `mcp.json`, the Antigravity system hasn't loaded it yet. Run the VS Code command `Developer: Reload Window` (Ctrl+Shift+P) or simply restart VS Code entirely. +2. **Restart Roblox Studio**: Open any local place or project file to load your plugins. +3. **Activate the Plugin**: In the Studio `Plugins` toolbar, click the MCP icon to toggle it ON. Look at your Studio `Output` window—you should see the message: + > `"The MCP Studio plugin is ready for prompts."` + +--- + +## Step 5: Test the Workspace Link! + +The setup is complete! Since Google Antigravity relies on an internal network of autonomous tools (and doesn't require jumping through an external Claude/Cursor GUI), you can simply begin prompting directly in the Antigravity chat: + +- *"List the child elements of my Workspace."* +- *"Create a red part at the world spawn."* +- *"Find and report any LocalScripts currently running in ReplicatedStorage."* + +Antigravity will instantly pipe instructions sequentially through `rbx-studio-mcp.exe`, execute natively inside Roblox Studio's DataModel, and retrieve responses securely back into your agent interface. + +# others + +cat C:\Users\User\AppData\Roaming\Code\User\mcp.json \ No newline at end of file diff --git a/robloxstudio-mcp-system-mechanics.md b/robloxstudio-mcp-system-mechanics.md new file mode 100644 index 0000000000000000000000000000000000000000..e8a3df1e5a1871d475f6788582d51aadb8dcabbc --- /dev/null +++ b/robloxstudio-mcp-system-mechanics.md @@ -0,0 +1,78 @@ +# NOTE: This is from 'C:\Users\User\Desktop\VSCode2\RobloxStudio-MCP-GoogleAntigravity\references\robloxstudio-mcp-main'. + +# Roblox Studio MCP Server (Node.js/TypeScript) System Mechanics + +This document provides a detailed breakdown of the system architecture and mechanics for the `robloxstudio-mcp` project, which facilitates communication between an LLM Client (like Claude or Cursor) and Roblox Studio. + +## Architecture Diagram + +Below is a detailed Mermaid sequence diagram illustrating the lifecycle of a tool request in this environment. + +```mermaid +sequenceDiagram + participant LLM as LLM Client (Claude/Cursor) + participant MCP as Node.js MCP Server (stdio) + participant Bridge as BridgeService (Memory) + participant HTTP as Express HTTP Server (:port) + participant Plugin as Roblox Studio Plugin (Luau) + + note over LLM,Plugin: 1. Setup Phase + HTTP-->>Plugin: Setup Endpoints (/health, /poll, /response) + Plugin->>HTTP: POST /ready (Initialize Connection) + HTTP->>Bridge: bridge.clearAllPendingRequests() + + note over LLM,Plugin: 2. Long Polling Loop + loop Every few seconds or immediately after a response + Plugin->>HTTP: GET /poll + HTTP->>Bridge: getPendingRequest() + alt No pending request + HTTP-->>Plugin: empty response (after delay/timeout) + else Has pending request + HTTP-->>Plugin: JSON { request, requestId } + end + end + + note over LLM,Plugin: 3. Tool Execution Lifecycle + LLM->>MCP: CallTool (e.g., get_file_tree) + MCP->>Bridge: sendRequest(endpoint, data) + Bridge->>Bridge: generate UUID & store in pendingRequests Map + + note right of HTTP: The next long poll from Plugin hits... + Plugin->>HTTP: GET /poll + HTTP->>Bridge: getPendingRequest() + Bridge-->>HTTP: oldest pending request + HTTP-->>Plugin: Return Request Payload (requestId, tool data) + + note over Plugin: Evaluates request & interacts with Roblox DataModel + + Plugin->>HTTP: POST /response { requestId, response, error } + HTTP->>Bridge: resolveRequest(requestId, response) + Bridge->>Bridge: Match UUID, trigger promise resolve + Bridge-->>MCP: Return result + MCP-->>LLM: Return CallToolResult +``` + +## Detailed Discussion + +### Components Overview + +1. **MCP Server over stdio (`src/index.ts`)**: + The core entry point utilizes the `@modelcontextprotocol/sdk` to expose Roblox Studio capabilities to AI assistants via Standard AI standard input/output streams (`stdio`). It initializes various available tools (like `get_file_tree`, `search_files`, `edit_script_lines`, etc.) and maps them to functions in `RobloxStudioTools`. + +2. **BridgeService (`src/bridge-service.ts`)**: + Since standard LLMs cannot execute HTTP requests directly into the Roblox Studio execution context, a "Bridge" is required. The `BridgeService` stores requests originating from the LLM in memory utilizing a `Map`. It assigns a `uuidv4` identifier to each request, resolving or rejecting Javascript Promises when Roblox Studio responds or naturally timing out after 30 seconds. + +3. **Express HTTP Server (`src/http-server.ts`)**: + Exposed locally, this server creates standard REST API endpoints mapping to the bridge context: + - `/ready` and `/disconnect`: Manage lifecycle awareness for the connected plugin. + - `/poll`: The crucial endpoint for the long-polling architecture. The Roblox Studio plugin continuously sends GET requests here. If a request exists, it returns immediately; otherwise, it waits/cycles to keep the network open without overwhelming the LLM client. + - `/response`: Where the plugin submits the results of executed queries natively run inside Studio. + - Various `/mcp/*` endpoints exist natively simulating the tools routing. + +4. **Roblox Studio Plugin (`studio-plugin/src/...`)**: + Written in Luau. Because Roblox restricts incoming connections for security, the plugin utilizes `HttpService` to make outgoing requests to the local Express server. It intercepts the HTTP Polling results, runs internal Studio SDK functions (e.g., getting hierarchical Place structures, updating script source code), and uses `/response` to fire the feedback back up the chain. + +### Core Mechanics + +- **Asynchronous Execution Gap Check**: The Node.js application is cleanly divided between the immediate `stdio` processing required by AI environments and the delayed networking constraints of `HttpService` in Roblox. The Promise architecture in the `BridgeService` forces the LLM to patiently await a complete trip through the local REST API and back. +- **Robust Toolset**: Uniquely among implementations, the TS-based server opts to fully define over 30 distinct granular tools (e.g., `insert_script_lines`, `mass_create_objects`) rather than primarily depending on evaluating raw Luau injected as strings. This gives the AI safer, strictly structured constraints to work within instead of writing custom engine scripts consistently over REST. diff --git a/src/ReplicatedStorage/Events/ChopEvent.model.json b/src/ReplicatedStorage/Events/ChopEvent.model.json new file mode 100644 index 0000000000000000000000000000000000000000..3f959ea00ace8546af01befe9f4656e4672c32df --- /dev/null +++ b/src/ReplicatedStorage/Events/ChopEvent.model.json @@ -0,0 +1,3 @@ +{ + "ClassName": "RemoteEvent" +} \ No newline at end of file diff --git a/src/ReplicatedStorage/Events/ClaimPlot.model.json b/src/ReplicatedStorage/Events/ClaimPlot.model.json new file mode 100644 index 0000000000000000000000000000000000000000..3f959ea00ace8546af01befe9f4656e4672c32df --- /dev/null +++ b/src/ReplicatedStorage/Events/ClaimPlot.model.json @@ -0,0 +1,3 @@ +{ + "ClassName": "RemoteEvent" +} \ No newline at end of file diff --git a/src/ReplicatedStorage/Events/CraftEvent.model.json b/src/ReplicatedStorage/Events/CraftEvent.model.json new file mode 100644 index 0000000000000000000000000000000000000000..3f959ea00ace8546af01befe9f4656e4672c32df --- /dev/null +++ b/src/ReplicatedStorage/Events/CraftEvent.model.json @@ -0,0 +1,3 @@ +{ + "ClassName": "RemoteEvent" +} \ No newline at end of file diff --git a/src/ReplicatedStorage/Events/DialogueEvent.model.json b/src/ReplicatedStorage/Events/DialogueEvent.model.json new file mode 100644 index 0000000000000000000000000000000000000000..3f959ea00ace8546af01befe9f4656e4672c32df --- /dev/null +++ b/src/ReplicatedStorage/Events/DialogueEvent.model.json @@ -0,0 +1,3 @@ +{ + "ClassName": "RemoteEvent" +} \ No newline at end of file diff --git a/src/ReplicatedStorage/Events/DragEvent.model.json b/src/ReplicatedStorage/Events/DragEvent.model.json new file mode 100644 index 0000000000000000000000000000000000000000..3f959ea00ace8546af01befe9f4656e4672c32df --- /dev/null +++ b/src/ReplicatedStorage/Events/DragEvent.model.json @@ -0,0 +1,3 @@ +{ + "ClassName": "RemoteEvent" +} \ No newline at end of file diff --git a/src/ReplicatedStorage/Events/DropEvent.model.json b/src/ReplicatedStorage/Events/DropEvent.model.json new file mode 100644 index 0000000000000000000000000000000000000000..3f959ea00ace8546af01befe9f4656e4672c32df --- /dev/null +++ b/src/ReplicatedStorage/Events/DropEvent.model.json @@ -0,0 +1,3 @@ +{ + "ClassName": "RemoteEvent" +} \ No newline at end of file diff --git a/src/ReplicatedStorage/Events/EquipToolEvent.model.json b/src/ReplicatedStorage/Events/EquipToolEvent.model.json new file mode 100644 index 0000000000000000000000000000000000000000..3f959ea00ace8546af01befe9f4656e4672c32df --- /dev/null +++ b/src/ReplicatedStorage/Events/EquipToolEvent.model.json @@ -0,0 +1,3 @@ +{ + "ClassName": "RemoteEvent" +} \ No newline at end of file diff --git a/src/ReplicatedStorage/Events/FillBlueprintEvent.model.json b/src/ReplicatedStorage/Events/FillBlueprintEvent.model.json new file mode 100644 index 0000000000000000000000000000000000000000..3f959ea00ace8546af01befe9f4656e4672c32df --- /dev/null +++ b/src/ReplicatedStorage/Events/FillBlueprintEvent.model.json @@ -0,0 +1,3 @@ +{ + "ClassName": "RemoteEvent" +} \ No newline at end of file diff --git a/src/ReplicatedStorage/Events/MarketUpdateEvent.model.json b/src/ReplicatedStorage/Events/MarketUpdateEvent.model.json new file mode 100644 index 0000000000000000000000000000000000000000..3f959ea00ace8546af01befe9f4656e4672c32df --- /dev/null +++ b/src/ReplicatedStorage/Events/MarketUpdateEvent.model.json @@ -0,0 +1,3 @@ +{ + "ClassName": "RemoteEvent" +} \ No newline at end of file diff --git a/src/ReplicatedStorage/Events/NotificationEvent.model.json b/src/ReplicatedStorage/Events/NotificationEvent.model.json new file mode 100644 index 0000000000000000000000000000000000000000..3f959ea00ace8546af01befe9f4656e4672c32df --- /dev/null +++ b/src/ReplicatedStorage/Events/NotificationEvent.model.json @@ -0,0 +1,3 @@ +{ + "ClassName": "RemoteEvent" +} \ No newline at end of file diff --git a/src/ReplicatedStorage/Events/PlaceBlueprintEvent.model.json b/src/ReplicatedStorage/Events/PlaceBlueprintEvent.model.json new file mode 100644 index 0000000000000000000000000000000000000000..3f959ea00ace8546af01befe9f4656e4672c32df --- /dev/null +++ b/src/ReplicatedStorage/Events/PlaceBlueprintEvent.model.json @@ -0,0 +1,3 @@ +{ + "ClassName": "RemoteEvent" +} \ No newline at end of file diff --git a/src/ReplicatedStorage/Events/PurchaseEvent.model.json b/src/ReplicatedStorage/Events/PurchaseEvent.model.json new file mode 100644 index 0000000000000000000000000000000000000000..3f959ea00ace8546af01befe9f4656e4672c32df --- /dev/null +++ b/src/ReplicatedStorage/Events/PurchaseEvent.model.json @@ -0,0 +1,3 @@ +{ + "ClassName": "RemoteEvent" +} \ No newline at end of file diff --git a/src/ReplicatedStorage/Events/QuestUpdateEvent.model.json b/src/ReplicatedStorage/Events/QuestUpdateEvent.model.json new file mode 100644 index 0000000000000000000000000000000000000000..3f959ea00ace8546af01befe9f4656e4672c32df --- /dev/null +++ b/src/ReplicatedStorage/Events/QuestUpdateEvent.model.json @@ -0,0 +1,3 @@ +{ + "ClassName": "RemoteEvent" +} \ No newline at end of file diff --git a/src/ReplicatedStorage/Events/SettingsEvent.model.json b/src/ReplicatedStorage/Events/SettingsEvent.model.json new file mode 100644 index 0000000000000000000000000000000000000000..3f959ea00ace8546af01befe9f4656e4672c32df --- /dev/null +++ b/src/ReplicatedStorage/Events/SettingsEvent.model.json @@ -0,0 +1,3 @@ +{ + "ClassName": "RemoteEvent" +} \ No newline at end of file diff --git a/src/ReplicatedStorage/Events/ShopEvent.model.json b/src/ReplicatedStorage/Events/ShopEvent.model.json new file mode 100644 index 0000000000000000000000000000000000000000..3f959ea00ace8546af01befe9f4656e4672c32df --- /dev/null +++ b/src/ReplicatedStorage/Events/ShopEvent.model.json @@ -0,0 +1,3 @@ +{ + "ClassName": "RemoteEvent" +} \ No newline at end of file diff --git a/src/ReplicatedStorage/Events/SoundEvent.model.json b/src/ReplicatedStorage/Events/SoundEvent.model.json new file mode 100644 index 0000000000000000000000000000000000000000..3f959ea00ace8546af01befe9f4656e4672c32df --- /dev/null +++ b/src/ReplicatedStorage/Events/SoundEvent.model.json @@ -0,0 +1,3 @@ +{ + "ClassName": "RemoteEvent" +} \ No newline at end of file diff --git a/src/ReplicatedStorage/Events/WireConnectEvent.model.json b/src/ReplicatedStorage/Events/WireConnectEvent.model.json new file mode 100644 index 0000000000000000000000000000000000000000..3f959ea00ace8546af01befe9f4656e4672c32df --- /dev/null +++ b/src/ReplicatedStorage/Events/WireConnectEvent.model.json @@ -0,0 +1,3 @@ +{ + "ClassName": "RemoteEvent" +} \ No newline at end of file diff --git a/src/ReplicatedStorage/Shared/AutomationConfig.lua b/src/ReplicatedStorage/Shared/AutomationConfig.lua new file mode 100644 index 0000000000000000000000000000000000000000..05b1c07e2cace0ef29d7ce86b8ed7e8ba9fbc470 --- /dev/null +++ b/src/ReplicatedStorage/Shared/AutomationConfig.lua @@ -0,0 +1,20 @@ +-- src/ReplicatedStorage/Shared/AutomationConfig.lua + +local AutomationConfig = { + Drones = { + BasicDrone = { + Speed = 20, + MaxCarryWeight = 50, -- Volume limit + EnergyCapacity = 100, + EnergyDrainPerStud = 0.1 + }, + AdvancedDrone = { + Speed = 40, + MaxCarryWeight = 150, + EnergyCapacity = 300, + EnergyDrainPerStud = 0.05 + } + } +} + +return AutomationConfig diff --git a/src/ReplicatedStorage/Shared/BiomeConfig.lua b/src/ReplicatedStorage/Shared/BiomeConfig.lua new file mode 100644 index 0000000000000000000000000000000000000000..af9eef6ef04cab532ace2b1c4f0e0d7a3e5ba894 --- /dev/null +++ b/src/ReplicatedStorage/Shared/BiomeConfig.lua @@ -0,0 +1,105 @@ +-- src/ReplicatedStorage/Shared/BiomeConfig.lua + +local BiomeConfig = { + Biomes = { + Forest = { + TerrainMaterial = Enum.Material.Grass, + GroundColor = Color3.fromRGB(85, 120, 60), + TreeTypes = {"Oak", "Birch", "Elm"}, + TreeDensity = 0.08, -- trees per stud^2 (in spawn region) + ElevationRange = {0, 5}, + HazardType = nil, + FogEnd = 1500, + FogColor = Color3.fromRGB(180, 200, 180), + Region = {MinX = -500, MaxX = -1, MinZ = 1, MaxZ = 500}, + }, + PineWoods = { + TerrainMaterial = Enum.Material.Grass, + GroundColor = Color3.fromRGB(60, 95, 45), + TreeTypes = {"Pine", "Birch"}, + TreeDensity = 0.10, + ElevationRange = {5, 20}, + HazardType = nil, + FogEnd = 1200, + FogColor = Color3.fromRGB(160, 180, 160), + Region = {MinX = 1, MaxX = 500, MinZ = 1, MaxZ = 500}, + }, + Swamp = { + TerrainMaterial = Enum.Material.Mud, + GroundColor = Color3.fromRGB(70, 80, 50), + TreeTypes = {"Oak", "Elm"}, + TreeDensity = 0.04, + ElevationRange = {-3, 0}, + HazardType = "SwampZone", + FogEnd = 600, + FogColor = Color3.fromRGB(120, 140, 100), + Region = {MinX = -500, MaxX = -1, MinZ = -500, MaxZ = -1}, + }, + Desert = { + TerrainMaterial = Enum.Material.Sand, + GroundColor = Color3.fromRGB(210, 190, 140), + TreeTypes = {"Elm"}, + TreeDensity = 0.01, + ElevationRange = {0, 10}, + HazardType = nil, + FogEnd = 2000, + FogColor = Color3.fromRGB(230, 220, 180), + Region = {MinX = 501, MaxX = 1000, MinZ = -500, MaxZ = -1}, + }, + Volcanic = { + TerrainMaterial = Enum.Material.Basalt, + GroundColor = Color3.fromRGB(60, 40, 35), + TreeTypes = {"LavaWood"}, + TreeDensity = 0.02, + ElevationRange = {10, 40}, + HazardType = "HazardZone", + HazardAttribute = "Lava", + FogEnd = 800, + FogColor = Color3.fromRGB(180, 100, 60), + Region = {MinX = 501, MaxX = 1000, MinZ = 1, MaxZ = 500}, + }, + IcePeak = { + TerrainMaterial = Enum.Material.Glacier, + GroundColor = Color3.fromRGB(200, 220, 240), + TreeTypes = {"Pine", "Birch"}, + TreeDensity = 0.03, + ElevationRange = {20, 60}, + HazardType = nil, + FogEnd = 900, + FogColor = Color3.fromRGB(210, 220, 240), + Region = {MinX = -1000, MaxX = -501, MinZ = -500, MaxZ = -1}, + }, + TropicalRainforest = { + TerrainMaterial = Enum.Material.LeafyGrass, + GroundColor = Color3.fromRGB(40, 100, 35), + TreeTypes = {"Mahogany", "Walnut", "GlowWood"}, + TreeDensity = 0.12, + ElevationRange = {0, 8}, + HazardType = nil, + FogEnd = 500, + FogColor = Color3.fromRGB(100, 150, 100), + Region = {MinX = -1000, MaxX = -501, MinZ = 1, MaxZ = 500}, + }, + Meadow = { + TerrainMaterial = Enum.Material.Grass, + GroundColor = Color3.fromRGB(130, 170, 80), + TreeTypes = {"Oak", "Birch"}, + TreeDensity = 0.02, + ElevationRange = {0, 3}, + HazardType = nil, + FogEnd = 2000, + FogColor = Color3.fromRGB(200, 210, 200), + Region = {MinX = 1, MaxX = 500, MinZ = -500, MaxZ = -1}, + }, + }, + + -- Terrain generation + TerrainChunkSize = 64, -- studs per terrain block + TerrainResolution = 4, -- voxel resolution + WaterLevel = -2, + + -- Tree spacing + MinTreeSpacing = 12, -- minimum studs between trees +} + +return BiomeConfig diff --git a/src/ReplicatedStorage/Shared/BuildingConfig.lua b/src/ReplicatedStorage/Shared/BuildingConfig.lua new file mode 100644 index 0000000000000000000000000000000000000000..eeed8a939dc83f4b8df37166a72c64014a75756c --- /dev/null +++ b/src/ReplicatedStorage/Shared/BuildingConfig.lua @@ -0,0 +1,98 @@ +-- src/ReplicatedStorage/Shared/BuildingConfig.lua + +local BuildingConfig = { + GridSnap = 2, + MaxPlacementDistance = 30, + + Structures = { + WoodWall = { + Name = "Wooden Wall", + Size = Vector3.new(10, 10, 1), + Cost = { + WoodVolume = 20, + SpecificMaterial = "Plank", + }, + }, + WoodFloor = { + Name = "Wooden Floor", + Size = Vector3.new(10, 1, 10), + Cost = { + WoodVolume = 25, + SpecificMaterial = "Plank", + }, + }, + WoodRoof = { + Name = "Wooden Roof", + Size = Vector3.new(12, 1, 12), + Cost = { + WoodVolume = 15, + SpecificMaterial = "Plank", + }, + }, + WoodDoor = { + Name = "Wooden Door", + Size = Vector3.new(4, 8, 1), + Cost = { + WoodVolume = 10, + SpecificMaterial = "Plank", + }, + }, + WoodStairs = { + Name = "Wooden Stairs", + Size = Vector3.new(4, 10, 10), + Cost = { + WoodVolume = 30, + SpecificMaterial = "Plank", + }, + }, + WoodWindow = { + Name = "Wooden Window Frame", + Size = Vector3.new(6, 6, 1), + Cost = { + WoodVolume = 12, + SpecificMaterial = "Plank", + }, + }, + WoodFence = { + Name = "Wooden Fence", + Size = Vector3.new(10, 4, 1), + Cost = { + WoodVolume = 8, + SpecificMaterial = "Stripped", + }, + }, + WoodBridge = { + Name = "Wooden Bridge", + Size = Vector3.new(6, 1, 16), + Cost = { + WoodVolume = 40, + SpecificMaterial = "Plank", + }, + }, + StonePillar = { + Name = "Stone Pillar", + Size = Vector3.new(3, 12, 3), + Cost = { + WoodVolume = 5, -- Mortar binding + SpecificMaterial = "Raw", + }, + }, + WoodShelter = { + Name = "Rain Shelter", + Size = Vector3.new(14, 8, 14), + Cost = { + WoodVolume = 50, + SpecificMaterial = "Plank", + }, + }, + }, + + -- Structure type cycle order for BuildController + StructureOrder = { + "WoodWall", "WoodFloor", "WoodRoof", "WoodDoor", + "WoodStairs", "WoodWindow", "WoodFence", "WoodBridge", + "StonePillar", "WoodShelter", + }, +} + +return BuildingConfig diff --git a/src/ReplicatedStorage/Shared/ChoppingConfig.lua b/src/ReplicatedStorage/Shared/ChoppingConfig.lua new file mode 100644 index 0000000000000000000000000000000000000000..fbdda21f464da16e6eaa3f39195f75df17465db9 --- /dev/null +++ b/src/ReplicatedStorage/Shared/ChoppingConfig.lua @@ -0,0 +1,115 @@ +-- src/ReplicatedStorage/Shared/ChoppingConfig.lua +local ChoppingConfig = { + -- Tree configuration + TreeTypes = { + Oak = { + HealthPerSegment = 100, + LogColor = Color3.fromRGB(133, 94, 66), + Density = 0.7, + ValueMult = 1.0, + }, + Pine = { + HealthPerSegment = 80, + LogColor = Color3.fromRGB(158, 127, 98), + Density = 0.5, + ValueMult = 0.8, + }, + Birch = { + HealthPerSegment = 60, + LogColor = Color3.fromRGB(220, 215, 200), + Density = 0.4, + ValueMult = 1.2, + }, + Walnut = { + HealthPerSegment = 120, + LogColor = Color3.fromRGB(80, 50, 30), + Density = 0.8, + ValueMult = 1.8, + }, + Mahogany = { + HealthPerSegment = 150, + LogColor = Color3.fromRGB(100, 40, 20), + Density = 0.9, + ValueMult = 2.5, + }, + Elm = { + HealthPerSegment = 90, + LogColor = Color3.fromRGB(110, 80, 55), + Density = 0.6, + ValueMult = 1.0, + }, + Redwood = { + HealthPerSegment = 200, + LogColor = Color3.fromRGB(120, 50, 30), + Density = 1.0, + ValueMult = 3.0, + }, + GlowWood = { + HealthPerSegment = 70, + LogColor = Color3.fromRGB(60, 180, 80), + Density = 0.3, + ValueMult = 4.0, + }, + LavaWood = { + HealthPerSegment = 130, + LogColor = Color3.fromRGB(50, 30, 20), + Density = 1.2, + ValueMult = 5.0, + }, + -- Mutation types + Fireproof = { + HealthPerSegment = 180, + LogColor = Color3.fromRGB(255, 100, 0), + Density = 1.1, + ValueMult = 6.0, + }, + Glowing = { + HealthPerSegment = 90, + LogColor = Color3.fromRGB(0, 255, 100), + Density = 0.5, + ValueMult = 7.0, + }, + Frozen = { + HealthPerSegment = 50, + LogColor = Color3.fromRGB(150, 200, 255), + Density = 0.9, + ValueMult = 4.5, + }, + Crystalline = { + HealthPerSegment = 250, + LogColor = Color3.fromRGB(200, 160, 255), + Density = 1.5, + ValueMult = 10.0, + }, + }, + + -- Axe configuration + AxeTypes = { + BasicAxe = { + Damage = 25, + SwingCooldown = 0.8, + Range = 6, + }, + SteelAxe = { + Damage = 50, + SwingCooldown = 0.6, + Range = 7, + }, + GoldAxe = { + Damage = 80, + SwingCooldown = 0.5, + Range = 8, + }, + DiamondAxe = { + Damage = 120, + SwingCooldown = 0.4, + Range = 9, + }, + }, + + -- General Physics logic limits + MinSegmentSizeY = 1.5, + MaxSegmentSizeY = 8, +} + +return ChoppingConfig diff --git a/src/ReplicatedStorage/Shared/Constants.lua b/src/ReplicatedStorage/Shared/Constants.lua new file mode 100644 index 0000000000000000000000000000000000000000..9d67e26c179f9a1de8782a8479e6a14e02c3f903 --- /dev/null +++ b/src/ReplicatedStorage/Shared/Constants.lua @@ -0,0 +1,63 @@ +-- src/ReplicatedStorage/Shared/Constants.lua + +local Constants = {} + +-- CollectionService Tags +Constants.Tags = { + TreeSegment = "TreeSegment", + TreeModel = "TreeModel", + Draggable = "Draggable", + Blueprint = "Blueprint", + ConstructedPart = "ConstructedPart", + EmptyPlot = "EmptyPlot", + ClaimedPlot = "ClaimedPlot", + MarketDropoff = "MarketDropoff", + ShopCounter = "ShopCounter", + BoxedItem = "BoxedItem", + ProcessingMachine = "ProcessingMachine", + Vehicle = "Vehicle", + DronePad = "DronePad", + HazardZone = "HazardZone", + SwampZone = "SwampZone", + WeightSwitch = "WeightSwitch", + LogicSource = "LogicSource", + SwampTires = "SwampTires", + NPC = "NPC", + BiomeZone = "BiomeZone", + WorldBoundary = "WorldBoundary", + SpawnPlatform = "SpawnPlatform", +} + +-- Attributes +Constants.Attributes = { + TreeType = "TreeType", + Health = "Health", + ProcessState = "ProcessState", + OwnerId = "OwnerId", + ItemId = "ItemId", + FillLevel = "FillLevel", + WoodFilled = "WoodFilled", + WoodRequired = "WoodRequired", + StructureType = "StructureType", + MaterialRequired = "MaterialRequired", + MachineType = "MachineType", + DegradedMult = "DegradedMult", + Mutated = "Mutated", + HazardType = "HazardType", + ComponentType = "ComponentType", + SourceType = "SourceType", + TargetFunction = "TargetFunction", + ConveyorSpeed = "ConveyorSpeed", + Purchased = "Purchased", + EquippedAxe = "EquippedAxe", + BiomeName = "BiomeName", +} + +-- Process States +Constants.ProcessStates = { + Raw = "Raw", + Stripped = "Stripped", + Plank = "Plank", +} + +return Constants diff --git a/src/ReplicatedStorage/Shared/CraftingConfig.lua b/src/ReplicatedStorage/Shared/CraftingConfig.lua new file mode 100644 index 0000000000000000000000000000000000000000..af1b3531d9fa18a7e823b52c83e95e31de18bfe4 --- /dev/null +++ b/src/ReplicatedStorage/Shared/CraftingConfig.lua @@ -0,0 +1,74 @@ +-- src/ReplicatedStorage/Shared/CraftingConfig.lua + +local CraftingConfig = { + -- Punch (fist) stats + Fist = { + Damage = 5, + SwingCooldown = 1.2, + Range = 5, + }, + + -- Resources dropped per chop hit + WoodDropChance = 0.6, -- 60% chance to drop resource per hit + WoodDropAmount = {1, 3}, -- random amount per drop + + -- Crafting recipes + Recipes = { + BasicAxe = { + Name = "Basic Axe", + Materials = { + Wood = 10, + }, + CraftTime = 2, + }, + SteelAxe = { + Name = "Steel Axe", + Materials = { + Wood = 30, + Stone = 5, + }, + CraftTime = 4, + }, + GoldAxe = { + Name = "Gold Axe", + Materials = { + Wood = 50, + Stone = 15, + GoldOre = 10, + }, + CraftTime = 6, + }, + DiamondAxe = { + Name = "Diamond Axe", + Materials = { + Wood = 80, + Stone = 30, + GoldOre = 20, + Diamond = 5, + }, + CraftTime = 10, + }, + }, + + -- Resource types and how they are gathered + Resources = { + Wood = { + Source = "TreeSegment", + Color = Color3.fromRGB(160, 120, 80), + }, + Stone = { + Source = "Rock", + Color = Color3.fromRGB(140, 140, 140), + }, + GoldOre = { + Source = "GoldVein", + Color = Color3.fromRGB(255, 200, 50), + }, + Diamond = { + Source = "DiamondVein", + Color = Color3.fromRGB(100, 200, 255), + }, + }, +} + +return CraftingConfig diff --git a/src/ReplicatedStorage/Shared/DraggingConfig.lua b/src/ReplicatedStorage/Shared/DraggingConfig.lua new file mode 100644 index 0000000000000000000000000000000000000000..4b4c60b460cb201c0c738c66834b5d7deb4b9f0e --- /dev/null +++ b/src/ReplicatedStorage/Shared/DraggingConfig.lua @@ -0,0 +1,15 @@ +-- src/ReplicatedStorage/Shared/DraggingConfig.lua +local DraggingConfig = { + MaxGrabDistance = 15, -- How far away the player can grab an object + HoldDistance = 6, -- The distance at which the object is held in front of the player + + -- AlignPosition Properties + AlignPositionMaxForce = 20000, + AlignPositionResponsiveness = 15, + + -- AlignOrientation Properties + AlignOrientationMaxTorque = 20000, + AlignOrientationResponsiveness = 20, +} + +return DraggingConfig diff --git a/src/ReplicatedStorage/Shared/EconomyConfig.lua b/src/ReplicatedStorage/Shared/EconomyConfig.lua new file mode 100644 index 0000000000000000000000000000000000000000..9895612536c29909ebf06e0a6e2aed619a64360e --- /dev/null +++ b/src/ReplicatedStorage/Shared/EconomyConfig.lua @@ -0,0 +1,40 @@ +-- src/ReplicatedStorage/Shared/EconomyConfig.lua + +local EconomyConfig = { + -- Base values per unit (Stud^3 roughly) of raw wood + WoodBaseValues = { + Oak = 15, + Pine = 10, + Birch = 18, + Walnut = 25, + Mahogany = 35, + Elm = 12, + Redwood = 45, + GlowWood = 60, + LavaWood = 80, + Fireproof = 100, + Glowing = 120, + Frozen = 70, + Crystalline = 200, + }, + + -- Processing multipliers + ProcessingMultipliers = { + Raw = 1.0, + Stripped = 1.5, + Plank = 2.5, + }, + + -- Market fluctuation settings + MarketFluctuations = { + Enabled = true, + UpdateInterval = 300, + MaxIncrease = 1.3, + MaxDecrease = 0.7, + }, + + -- Starter cash amount + StarterCash = 500, +} + +return EconomyConfig diff --git a/src/ReplicatedStorage/Shared/ExplorationConfig.lua b/src/ReplicatedStorage/Shared/ExplorationConfig.lua new file mode 100644 index 0000000000000000000000000000000000000000..2982b883ef8f7784eaf21508481ddefd0473361e --- /dev/null +++ b/src/ReplicatedStorage/Shared/ExplorationConfig.lua @@ -0,0 +1,42 @@ +-- src/ReplicatedStorage/Shared/ExplorationConfig.lua + +local ExplorationConfig = { + Puzzles = { + WeightSwitch = { + RequiredWeight = 500, + ActiveTime = 0, + }, + LightReceptor = { + RequiredIntensity = 0.8, + }, + }, + + Biomes = { + Swamp = { + SinkingSpeed = 2, + DamagePerSecond = 5, + }, + IcePeak = { + SlipFriction = 0.05, + FreezeDamage = 2, + }, + Desert = { + HeatDamagePerSecond = 1, + StaminaDrainMult = 1.5, + }, + Volcanic = { + LavaDamagePerSecond = 20, + SafeDistance = 10, + }, + TropicalRainforest = { + VisibilityRange = 100, + PoisonChance = 0.1, + }, + Tundra = { + FreezeDamage = 3, + SlowMult = 0.7, + }, + }, +} + +return ExplorationConfig diff --git a/src/ReplicatedStorage/Shared/GameConfig.lua b/src/ReplicatedStorage/Shared/GameConfig.lua new file mode 100644 index 0000000000000000000000000000000000000000..fb1cc28303833106255b5cd94c99cf4459fa111d --- /dev/null +++ b/src/ReplicatedStorage/Shared/GameConfig.lua @@ -0,0 +1,57 @@ +-- src/ReplicatedStorage/Shared/GameConfig.lua + +local GameConfig = { + -- Starting values + StarterCash = 500, + StarterAxe = "BasicAxe", + + -- Leaderstats + Stats = { + Cash = "Cash", + WoodChopped = "WoodChopped", + }, + + -- World boundaries + WorldBounds = { + MinX = -1500, + MaxX = 1500, + MinZ = -1500, + MaxZ = 1500, + }, + + -- Spawn area + SpawnPosition = Vector3.new(0, 5, 0), + SpawnPlatformSize = Vector3.new(60, 1, 60), + + -- Auto-save + AutoSaveInterval = 120, -- seconds + + -- Max players per server + MaxPlayers = 12, + + -- Plot spawn positions (evenly spaced) + PlotPositions = { + Vector3.new(120, 0.5, 0), + Vector3.new(240, 0.5, 0), + Vector3.new(120, 0.5, 220), + Vector3.new(240, 0.5, 220), + Vector3.new(-120, 0.5, 0), + Vector3.new(-240, 0.5, 0), + Vector3.new(-120, 0.5, 220), + Vector3.new(-240, 0.5, 220), + Vector3.new(120, 0.5, -220), + Vector3.new(240, 0.5, -220), + Vector3.new(-120, 0.5, -220), + Vector3.new(-240, 0.5, -220), + }, + + -- Market area + MarketPosition = Vector3.new(0, 0.5, -100), + MarketSize = Vector3.new(40, 10, 40), + + -- Shop area + ShopPosition = Vector3.new(60, 0.5, -100), + ShopSize = Vector3.new(30, 12, 30), +} + +return GameConfig diff --git a/src/ReplicatedStorage/Shared/MutationConfig.lua b/src/ReplicatedStorage/Shared/MutationConfig.lua new file mode 100644 index 0000000000000000000000000000000000000000..0f3c2ff5b5155c0d70aed7ddce4db552d67fa98e --- /dev/null +++ b/src/ReplicatedStorage/Shared/MutationConfig.lua @@ -0,0 +1,40 @@ +-- src/ReplicatedStorage/Shared/MutationConfig.lua + +local MutationConfig = { + Hazards = { + Lava = { + Radius = 50, + MutationChance = 0.5, + ModifiedType = "Fireproof", + ValueMult = 2.5, + Material = Enum.Material.Neon, + Color = Color3.fromRGB(255, 100, 0), + }, + Toxic = { + Radius = 60, + MutationChance = 0.3, + ModifiedType = "Glowing", + ValueMult = 3.0, + Material = Enum.Material.Neon, + Color = Color3.fromRGB(0, 255, 100), + }, + Frozen = { + Radius = 45, + MutationChance = 0.4, + ModifiedType = "Frozen", + ValueMult = 2.0, + Material = Enum.Material.Ice, + Color = Color3.fromRGB(150, 200, 255), + }, + Crystalline = { + Radius = 30, + MutationChance = 0.15, + ModifiedType = "Crystalline", + ValueMult = 5.0, + Material = Enum.Material.Glass, + Color = Color3.fromRGB(200, 160, 255), + }, + }, +} + +return MutationConfig diff --git a/src/ReplicatedStorage/Shared/NPCConfig.lua b/src/ReplicatedStorage/Shared/NPCConfig.lua new file mode 100644 index 0000000000000000000000000000000000000000..0227e929127ad11b7998ad1b5fb7d59100bbca03 --- /dev/null +++ b/src/ReplicatedStorage/Shared/NPCConfig.lua @@ -0,0 +1,86 @@ +-- src/ReplicatedStorage/Shared/NPCConfig.lua + +local NPCConfig = { + NPCs = { + Lumberjack_Larry = { + DisplayName = "Larry the Lumberjack", + Position = Vector3.new(10, 3, -80), + BodyColors = { + HeadColor = Color3.fromRGB(234, 184, 146), + TorsoColor = Color3.fromRGB(180, 40, 40), + LeftArmColor = Color3.fromRGB(234, 184, 146), + RightArmColor = Color3.fromRGB(234, 184, 146), + LeftLegColor = Color3.fromRGB(50, 50, 130), + RightLegColor = Color3.fromRGB(50, 50, 130), + }, + Dialogue = { + "Welcome to Timberbound Expeditions, partner!", + "Grab your axe and start choppin' trees!", + "Process logs at the sawmill for better prices.", + "Watch out for the rain -- it ruins unprotected wood!", + }, + QuestIds = {"ChopFirstTree", "Chop10Trees"}, + Role = "Guide", + }, + Shopkeeper_Sue = { + DisplayName = "Sue's Supply Shop", + Position = Vector3.new(60, 3, -90), + BodyColors = { + HeadColor = Color3.fromRGB(234, 184, 146), + TorsoColor = Color3.fromRGB(50, 150, 50), + LeftArmColor = Color3.fromRGB(234, 184, 146), + RightArmColor = Color3.fromRGB(234, 184, 146), + LeftLegColor = Color3.fromRGB(90, 70, 50), + RightLegColor = Color3.fromRGB(90, 70, 50), + }, + Dialogue = { + "Welcome to my shop! Take a look around.", + "I've got axes, sawmills, and more!", + "Drag items to the counter and talk to me to buy.", + }, + QuestIds = {"BuyFirstItem"}, + Role = "Shopkeeper", + }, + Market_Mike = { + DisplayName = "Market Mike", + Position = Vector3.new(0, 3, -110), + BodyColors = { + HeadColor = Color3.fromRGB(180, 140, 100), + TorsoColor = Color3.fromRGB(200, 180, 50), + LeftArmColor = Color3.fromRGB(180, 140, 100), + RightArmColor = Color3.fromRGB(180, 140, 100), + LeftLegColor = Color3.fromRGB(40, 40, 40), + RightLegColor = Color3.fromRGB(40, 40, 40), + }, + Dialogue = { + "Bring your wood here and I'll buy it!", + "Planks are worth more than raw logs.", + "Market prices shift every few minutes. Sell smart!", + }, + QuestIds = {"SellFirstWood", "Earn5000"}, + Role = "Buyer", + }, + Explorer_Eva = { + DisplayName = "Explorer Eva", + Position = Vector3.new(-30, 3, -80), + BodyColors = { + HeadColor = Color3.fromRGB(210, 170, 130), + TorsoColor = Color3.fromRGB(100, 70, 40), + LeftArmColor = Color3.fromRGB(210, 170, 130), + RightArmColor = Color3.fromRGB(210, 170, 130), + LeftLegColor = Color3.fromRGB(60, 80, 50), + RightLegColor = Color3.fromRGB(60, 80, 50), + }, + Dialogue = { + "This world is full of secrets!", + "Different biomes have different tree types.", + "Some rare woods glow in the dark... and they're worth a fortune!", + "Have you found the volcanic region yet?", + }, + QuestIds = {"ExploreAllBiomes"}, + Role = "Explorer", + }, + }, +} + +return NPCConfig diff --git a/src/ReplicatedStorage/Shared/PlotConfig.lua b/src/ReplicatedStorage/Shared/PlotConfig.lua new file mode 100644 index 0000000000000000000000000000000000000000..e924f34571417eb1e530d40ff0a2b5cafeb0dbdd --- /dev/null +++ b/src/ReplicatedStorage/Shared/PlotConfig.lua @@ -0,0 +1,10 @@ +-- src/ReplicatedStorage/Shared/PlotConfig.lua +local PlotConfig = { + GridSize = 2, -- Grid snapping increment in studs + MaxItemsPerPlot = 1000, -- Limit for performance + + -- Dimensions of the claimable land + PlotDimensions = Vector3.new(200, 1, 200), +} + +return PlotConfig diff --git a/src/ReplicatedStorage/Shared/QuestConfig.lua b/src/ReplicatedStorage/Shared/QuestConfig.lua new file mode 100644 index 0000000000000000000000000000000000000000..1b0bf0e10385dededafc29d5365f8282a88a5e91 --- /dev/null +++ b/src/ReplicatedStorage/Shared/QuestConfig.lua @@ -0,0 +1,107 @@ +-- src/ReplicatedStorage/Shared/QuestConfig.lua + +local QuestConfig = { + Quests = { + ChopFirstTree = { + Title = "First Timber", + Description = "Chop down your first tree segment.", + Type = "Chop", + Target = 1, + Reward = {Cash = 100}, + Icon = "rbxassetid://0", -- placeholder + }, + Chop10Trees = { + Title = "Getting Started", + Description = "Chop 10 tree segments.", + Type = "Chop", + Target = 10, + Reward = {Cash = 500}, + Icon = "rbxassetid://0", + }, + Chop50Trees = { + Title = "Lumberjack", + Description = "Chop 50 tree segments.", + Type = "Chop", + Target = 50, + Reward = {Cash = 2500}, + Icon = "rbxassetid://0", + }, + Chop200Trees = { + Title = "Master Forester", + Description = "Chop 200 tree segments.", + Type = "Chop", + Target = 200, + Reward = {Cash = 10000}, + Icon = "rbxassetid://0", + }, + SellFirstWood = { + Title = "First Sale", + Description = "Sell wood at the market for the first time.", + Type = "Sell", + Target = 1, + Reward = {Cash = 200}, + Icon = "rbxassetid://0", + }, + Earn5000 = { + Title = "Money Maker", + Description = "Earn a total of $5,000 from selling wood.", + Type = "EarnCash", + Target = 5000, + Reward = {Cash = 1000}, + Icon = "rbxassetid://0", + }, + Earn50000 = { + Title = "Timber Tycoon", + Description = "Earn a total of $50,000.", + Type = "EarnCash", + Target = 50000, + Reward = {Cash = 10000}, + Icon = "rbxassetid://0", + }, + BuildFirstStructure = { + Title = "Builder", + Description = "Construct your first building.", + Type = "Build", + Target = 1, + Reward = {Cash = 300}, + Icon = "rbxassetid://0", + }, + Build10Structures = { + Title = "Architect", + Description = "Construct 10 buildings.", + Type = "Build", + Target = 10, + Reward = {Cash = 3000}, + Icon = "rbxassetid://0", + }, + ExploreAllBiomes = { + Title = "Explorer", + Description = "Visit every biome in the world.", + Type = "Explore", + Target = 8, -- number of biomes + Reward = {Cash = 5000}, + Icon = "rbxassetid://0", + }, + ProcessFirstPlank = { + Title = "Mill Worker", + Description = "Process a log into a plank at the sawmill.", + Type = "Process", + Target = 1, + Reward = {Cash = 150}, + Icon = "rbxassetid://0", + }, + BuyFirstItem = { + Title = "First Purchase", + Description = "Buy an item from the shop.", + Type = "Purchase", + Target = 1, + Reward = {Cash = 100}, + Icon = "rbxassetid://0", + }, + }, + + -- Quest ordering (which quests unlock first) + StarterQuests = {"ChopFirstTree", "SellFirstWood", "BuildFirstStructure", "BuyFirstItem"}, +} + +return QuestConfig diff --git a/src/ReplicatedStorage/Shared/ShopConfig.lua b/src/ReplicatedStorage/Shared/ShopConfig.lua new file mode 100644 index 0000000000000000000000000000000000000000..8e489c37186961b664446def06318e7cb831d110 --- /dev/null +++ b/src/ReplicatedStorage/Shared/ShopConfig.lua @@ -0,0 +1,112 @@ +-- src/ReplicatedStorage/Shared/ShopConfig.lua + +local ShopConfig = { + Items = { + BasicAxe = { + Price = 0, -- Free starter tool + Name = "Basic Axe", + Type = "Tool", + Description = "A simple axe for chopping trees.", + }, + SteelAxe = { + Price = 1200, + Name = "Steel Axe", + Type = "Tool", + Description = "Stronger than basic. Faster swings.", + }, + GoldAxe = { + Price = 5000, + Name = "Gold Axe", + Type = "Tool", + Description = "High damage with quick recovery.", + }, + DiamondAxe = { + Price = 25000, + Name = "Diamond Axe", + Type = "Tool", + Description = "The ultimate axe. Cuts anything fast.", + }, + BarkStripper = { + Price = 800, + Name = "Bark Stripper Machine", + Type = "Machine", + Description = "Strips bark from raw logs.", + }, + Sawmill = { + Price = 3500, + Name = "Basic Sawmill", + Type = "Machine", + Description = "Converts logs into valuable planks.", + }, + AdvancedSawmill = { + Price = 15000, + Name = "Advanced Sawmill", + Type = "Machine", + Description = "Processes logs faster and cleaner.", + }, + Conveyor = { + Price = 500, + Name = "Conveyor Belt", + Type = "Machine", + Description = "Moves items automatically.", + }, + Flatbed = { + Price = 8000, + Name = "Flatbed Truck", + Type = "Vehicle", + Description = "Transport large loads of lumber.", + }, + BasicDrone = { + Price = 12000, + Name = "Basic Delivery Drone", + Type = "Automation", + Description = "Automatically carries small loads.", + }, + AdvancedDrone = { + Price = 35000, + Name = "Advanced Delivery Drone", + Type = "Automation", + Description = "Carries heavy loads at high speed.", + }, + WoodShelter = { + Price = 2000, + Name = "Rain Shelter Kit", + Type = "Structure", + Description = "Protects wood from rain degradation.", + }, + WireKit = { + Price = 300, + Name = "Wire Kit", + Type = "Utility", + Description = "Connect components with wires.", + }, + Button = { + Price = 200, + Name = "Logic Button", + Type = "Component", + Description = "Wire source: sends pulse signal.", + }, + Lever = { + Price = 250, + Name = "Logic Lever", + Type = "Component", + Description = "Wire source: toggles on/off.", + }, + }, + + -- Category ordering for ship GUI + Categories = {"Tool", "Machine", "Vehicle", "Automation", "Structure", "Utility", "Component"}, + + -- Items per category (for display ordering) + CategoryItems = { + Tool = {"BasicAxe", "SteelAxe", "GoldAxe", "DiamondAxe"}, + Machine = {"BarkStripper", "Sawmill", "AdvancedSawmill", "Conveyor"}, + Vehicle = {"Flatbed"}, + Automation = {"BasicDrone", "AdvancedDrone"}, + Structure = {"WoodShelter"}, + Utility = {"WireKit"}, + Component = {"Button", "Lever"}, + }, +} + +return ShopConfig diff --git a/src/ReplicatedStorage/Shared/SoundConfig.lua b/src/ReplicatedStorage/Shared/SoundConfig.lua new file mode 100644 index 0000000000000000000000000000000000000000..79b3262b19df3ddc4224b20b967e744c27b7b988 --- /dev/null +++ b/src/ReplicatedStorage/Shared/SoundConfig.lua @@ -0,0 +1,39 @@ +-- src/ReplicatedStorage/Shared/SoundConfig.lua +-- Sound asset IDs (using Roblox default content where available) + +local SoundConfig = { + -- Tool Sounds + AxeSwing = "rbxasset://sounds/snap.mp3", + AxeHitWood = "rbxasset://sounds/impact_wood.mp3", + TreeFall = "rbxasset://sounds/bass.mp3", + + -- Economy + CashRegister = "rbxasset://sounds/electronicpingshort.wav", + PurchaseSuccess = "rbxasset://sounds/electronicpingshort.wav", + PurchaseFail = "rbxasset://sounds/bass.mp3", + + -- Building + PlaceBlueprint = "rbxasset://sounds/snap.mp3", + ConstructComplete = "rbxasset://sounds/electronicpingshort.wav", + + -- UI + ButtonClick = "rbxasset://sounds/snap.mp3", + QuestComplete = "rbxasset://sounds/electronicpingshort.wav", + Notification = "rbxasset://sounds/electronicpingshort.wav", + + -- Weather ambient + RainLoop = "rbxasset://sounds/bass.mp3", + WindLoop = "rbxasset://sounds/snap.mp3", + + -- General ambient + ForestAmbient = "rbxasset://sounds/snap.mp3", + SwampAmbient = "rbxasset://sounds/bass.mp3", + + -- Volume levels + MasterVolume = 0.8, + SFXVolume = 1.0, + MusicVolume = 0.5, + AmbientVolume = 0.3, +} + +return SoundConfig diff --git a/src/ReplicatedStorage/Shared/TreeModelConfig.lua b/src/ReplicatedStorage/Shared/TreeModelConfig.lua new file mode 100644 index 0000000000000000000000000000000000000000..e89a4bb150502f0b44575a674ad4f862a64591f4 --- /dev/null +++ b/src/ReplicatedStorage/Shared/TreeModelConfig.lua @@ -0,0 +1,119 @@ +-- src/ReplicatedStorage/Shared/TreeModelConfig.lua + +local TreeModelConfig = { + Templates = { + Oak = { + TrunkDiameter = {2.5, 3.5}, -- min, max + TrunkHeight = {12, 20}, + SegmentCount = {3, 5}, + CanopyRadius = {6, 10}, + CanopySegments = 5, + LeafColor = Color3.fromRGB(50, 120, 40), + LeafMaterial = Enum.Material.LeafyGrass, + BarkColor = Color3.fromRGB(133, 94, 66), + BarkMaterial = Enum.Material.Wood, + HasLeaves = true, + }, + Pine = { + TrunkDiameter = {1.5, 2.5}, + TrunkHeight = {15, 25}, + SegmentCount = {4, 6}, + CanopyRadius = {3, 5}, + CanopySegments = 6, + LeafColor = Color3.fromRGB(30, 80, 30), + LeafMaterial = Enum.Material.LeafyGrass, + BarkColor = Color3.fromRGB(158, 127, 98), + BarkMaterial = Enum.Material.Wood, + HasLeaves = true, + ConicalCanopy = true, + }, + Birch = { + TrunkDiameter = {1.5, 2.0}, + TrunkHeight = {14, 22}, + SegmentCount = {4, 6}, + CanopyRadius = {4, 7}, + CanopySegments = 4, + LeafColor = Color3.fromRGB(100, 170, 60), + LeafMaterial = Enum.Material.LeafyGrass, + BarkColor = Color3.fromRGB(220, 215, 200), + BarkMaterial = Enum.Material.Marble, + HasLeaves = true, + }, + Walnut = { + TrunkDiameter = {2.0, 3.0}, + TrunkHeight = {10, 16}, + SegmentCount = {3, 4}, + CanopyRadius = {5, 8}, + CanopySegments = 5, + LeafColor = Color3.fromRGB(60, 100, 30), + LeafMaterial = Enum.Material.LeafyGrass, + BarkColor = Color3.fromRGB(80, 50, 30), + BarkMaterial = Enum.Material.Wood, + HasLeaves = true, + }, + Mahogany = { + TrunkDiameter = {3.0, 4.5}, + TrunkHeight = {18, 28}, + SegmentCount = {5, 7}, + CanopyRadius = {8, 12}, + CanopySegments = 6, + LeafColor = Color3.fromRGB(40, 90, 25), + LeafMaterial = Enum.Material.LeafyGrass, + BarkColor = Color3.fromRGB(100, 40, 20), + BarkMaterial = Enum.Material.Wood, + HasLeaves = true, + }, + Elm = { + TrunkDiameter = {2.0, 3.0}, + TrunkHeight = {12, 18}, + SegmentCount = {3, 5}, + CanopyRadius = {5, 9}, + CanopySegments = 5, + LeafColor = Color3.fromRGB(80, 140, 50), + LeafMaterial = Enum.Material.LeafyGrass, + BarkColor = Color3.fromRGB(110, 80, 55), + BarkMaterial = Enum.Material.Wood, + HasLeaves = true, + }, + Redwood = { + TrunkDiameter = {4.0, 6.0}, + TrunkHeight = {30, 50}, + SegmentCount = {6, 10}, + CanopyRadius = {6, 10}, + CanopySegments = 4, + LeafColor = Color3.fromRGB(35, 70, 30), + LeafMaterial = Enum.Material.LeafyGrass, + BarkColor = Color3.fromRGB(120, 50, 30), + BarkMaterial = Enum.Material.Wood, + HasLeaves = true, + }, + GlowWood = { + TrunkDiameter = {1.5, 2.5}, + TrunkHeight = {8, 14}, + SegmentCount = {3, 4}, + CanopyRadius = {4, 6}, + CanopySegments = 4, + LeafColor = Color3.fromRGB(0, 255, 100), + LeafMaterial = Enum.Material.Neon, + BarkColor = Color3.fromRGB(60, 180, 80), + BarkMaterial = Enum.Material.SmoothPlastic, + HasLeaves = true, + GlowTrunk = true, + }, + LavaWood = { + TrunkDiameter = {2.0, 3.5}, + TrunkHeight = {8, 14}, + SegmentCount = {3, 4}, + CanopyRadius = {3, 5}, + CanopySegments = 3, + LeafColor = Color3.fromRGB(255, 120, 0), + LeafMaterial = Enum.Material.Neon, + BarkColor = Color3.fromRGB(50, 30, 20), + BarkMaterial = Enum.Material.CrackedLava, + HasLeaves = true, + GlowTrunk = true, + }, + }, +} + +return TreeModelConfig diff --git a/src/ReplicatedStorage/Shared/Utility.lua b/src/ReplicatedStorage/Shared/Utility.lua new file mode 100644 index 0000000000000000000000000000000000000000..7f9b98e6c350ab0aaf5aad68d1c29b0502494cb8 --- /dev/null +++ b/src/ReplicatedStorage/Shared/Utility.lua @@ -0,0 +1,85 @@ +-- src/ReplicatedStorage/Shared/Utility.lua + +local Utility = {} + +function Utility.formatCash(amount) + if amount >= 1000000 then + return string.format("$%.1fM", amount / 1000000) + elseif amount >= 1000 then + return string.format("$%.1fK", amount / 1000) + else + return "$" .. tostring(math.floor(amount)) + end +end + +function Utility.formatTime(seconds) + local min = math.floor(seconds / 60) + local sec = math.floor(seconds % 60) + return string.format("%d:%02d", min, sec) +end + +function Utility.lerp(a, b, t) + return a + (b - a) * t +end + +function Utility.getPartVolume(part) + if not part or not part:IsA("BasePart") then return 0 end + return part.Size.X * part.Size.Y * part.Size.Z +end + +function Utility.randomInRange(min, max) + return min + math.random() * (max - min) +end + +function Utility.randomIntInRange(min, max) + return math.random(min, max) +end + +function Utility.clampVector3(vec, minBounds, maxBounds) + return Vector3.new( + math.clamp(vec.X, minBounds.X, maxBounds.X), + math.clamp(vec.Y, minBounds.Y, maxBounds.Y), + math.clamp(vec.Z, minBounds.Z, maxBounds.Z) + ) +end + +function Utility.shuffleTable(tbl) + local shuffled = {} + for _, v in ipairs(tbl) do + table.insert(shuffled, v) + end + for i = #shuffled, 2, -1 do + local j = math.random(1, i) + shuffled[i], shuffled[j] = shuffled[j], shuffled[i] + end + return shuffled +end + +function Utility.tableContains(tbl, value) + for _, v in ipairs(tbl) do + if v == value then return true end + end + return false +end + +function Utility.tableKeys(tbl) + local keys = {} + for k, _ in pairs(tbl) do + table.insert(keys, k) + end + return keys +end + +function Utility.deepCopy(original) + local copy = {} + for k, v in pairs(original) do + if type(v) == "table" then + copy[k] = Utility.deepCopy(v) + else + copy[k] = v + end + end + return copy +end + +return Utility diff --git a/src/ReplicatedStorage/Shared/VehicleConfig.lua b/src/ReplicatedStorage/Shared/VehicleConfig.lua new file mode 100644 index 0000000000000000000000000000000000000000..8bd91cb8fa7d8f86c84c4c44488b9fda38404618 --- /dev/null +++ b/src/ReplicatedStorage/Shared/VehicleConfig.lua @@ -0,0 +1,14 @@ +-- src/ReplicatedStorage/Shared/VehicleConfig.lua +local VehicleConfig = { + -- Physical properties for the truck bed to prevent logs from sliding off easily while driving + -- Density, Friction, Elasticity, FrictionWeight, ElasticityWeight + -- Extremely high Friction and FrictionWeight to lock the logs to the bed + FlatbedPhysicalProperties = PhysicalProperties.new(1.0, 2.0, 0, 100, 100), + + CollisionGroups = { + Logs = "Logs", + Vehicles = "Vehicles", + } +} + +return VehicleConfig diff --git a/src/ReplicatedStorage/Shared/WeatherConfig.lua b/src/ReplicatedStorage/Shared/WeatherConfig.lua new file mode 100644 index 0000000000000000000000000000000000000000..ad398c95f54f64ca6b5f5600e6ab4bdf813f8947 --- /dev/null +++ b/src/ReplicatedStorage/Shared/WeatherConfig.lua @@ -0,0 +1,17 @@ +-- src/ReplicatedStorage/Shared/WeatherConfig.lua + +local WeatherConfig = { + CycleDurationMin = 300, -- 5 mins of clear weather + CycleDurationMax = 600, -- 10 mins + + RainDurationMin = 60, -- 1 minute of rain + RainDurationMax = 180, -- 3 minutes of rain + + DegradationConfig = { + WoodValueLossPerTick = 0.05, -- 5% loss per tick + TickInterval = 3, -- Every 3 seconds in rain + MaxValueLoss = 0.5, -- Logs won't lose more than 50% of value + } +} + +return WeatherConfig diff --git a/src/ReplicatedStorage/Shared/WireLogicConfig.lua b/src/ReplicatedStorage/Shared/WireLogicConfig.lua new file mode 100644 index 0000000000000000000000000000000000000000..64b367607bc16594cf5132a82c0f3835d377d416 --- /dev/null +++ b/src/ReplicatedStorage/Shared/WireLogicConfig.lua @@ -0,0 +1,14 @@ +-- src/ReplicatedStorage/Shared/WireLogicConfig.lua + +local WireLogicConfig = { + MaxWireLength = 50, -- Maximum distance between two connected nodes + + -- Defining the kinds of components available + ComponentTypes = { + Source = {"Button", "Lever", "PressurePlate"}, + Logic = {"AND", "OR", "NOT", "Delay", "Timer"}, + Target = {"Door", "Sawmill", "Conveyor", "Light"} + } +} + +return WireLogicConfig diff --git a/src/ServerScriptService/AchievementManager.server.lua b/src/ServerScriptService/AchievementManager.server.lua new file mode 100644 index 0000000000000000000000000000000000000000..99a9909a0e1805de0d4510917c2325acf651b770 --- /dev/null +++ b/src/ServerScriptService/AchievementManager.server.lua @@ -0,0 +1,82 @@ +-- src/ServerScriptService/AchievementManager.server.lua + +local Players = game:GetService("Players") +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local NotificationEvent = ReplicatedStorage:WaitForChild("Events"):WaitForChild("NotificationEvent") + +-- Achievement definitions +local Achievements = { + {id = "first_chop", title = "First Swing", description = "Chop your first tree segment", stat = "WoodChopped", target = 1}, + {id = "chop_100", title = "Century Cutter", description = "Chop 100 tree segments", stat = "WoodChopped", target = 100}, + {id = "chop_500", title = "Lumber Legend", description = "Chop 500 tree segments", stat = "WoodChopped", target = 500}, + {id = "chop_1000", title = "Timber Titan", description = "Chop 1000 tree segments", stat = "WoodChopped", target = 1000}, + {id = "earn_1000", title = "First Thousand", description = "Earn $1,000 total", stat = "TotalEarned", target = 1000}, + {id = "earn_10000", title = "Money Bags", description = "Earn $10,000 total", stat = "TotalEarned", target = 10000}, + {id = "earn_100000", title = "Timber Mogul", description = "Earn $100,000 total", stat = "TotalEarned", target = 100000}, + {id = "build_1", title = "First Foundation", description = "Build your first structure", stat = "TotalBuilt", target = 1}, + {id = "build_25", title = "Master Builder", description = "Build 25 structures", stat = "TotalBuilt", target = 25}, +} + +-- Track awarded achievements per player +local playerAchievements = {} -- [userId] = {achievementId = true, ...} + +local function checkAchievements(player) + local data = _G.GetPlayerData(player) + if not data then return end + + if not playerAchievements[player.UserId] then + playerAchievements[player.UserId] = {} + end + + local leaderstats = player:FindFirstChild("leaderstats") + + for _, achievement in ipairs(Achievements) do + if playerAchievements[player.UserId][achievement.id] then continue end + + local currentValue = 0 + + -- Check leaderstats + if leaderstats then + local stat = leaderstats:FindFirstChild(achievement.stat) + if stat then + currentValue = stat.Value + end + end + + -- Check player data + if data[achievement.stat] then + currentValue = data[achievement.stat] + end + + if currentValue >= achievement.target then + playerAchievements[player.UserId][achievement.id] = true + NotificationEvent:FireClient(player, "Achievement", achievement.title .. " - " .. achievement.description) + end + end +end + +-- Expose for other managers +_G.CheckAchievements = function(player) + checkAchievements(player) +end + +_G.GetPlayerAchievements = function(player) + return playerAchievements[player.UserId] or {} +end + +-- Periodic check +task.spawn(function() + while true do + task.wait(10) + for _, player in ipairs(Players:GetPlayers()) do + pcall(function() + checkAchievements(player) + end) + end + end +end) + +Players.PlayerRemoving:Connect(function(player) + playerAchievements[player.UserId] = nil +end) diff --git a/src/ServerScriptService/AntiCheatManager.server.lua b/src/ServerScriptService/AntiCheatManager.server.lua new file mode 100644 index 0000000000000000000000000000000000000000..18a11e1307db9e2ab51791e446871b2283b1e81e --- /dev/null +++ b/src/ServerScriptService/AntiCheatManager.server.lua @@ -0,0 +1,73 @@ +-- src/ServerScriptService/AntiCheatManager.server.lua + +local Players = game:GetService("Players") +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +-- Rate limiting tracker +local rateLimits = {} -- [userId] = { [eventName] = { count, lastReset } } + +local MAX_EVENTS_PER_SECOND = 10 +local RATE_WINDOW = 1 -- seconds + +local function getRateData(player, eventName) + if not rateLimits[player.UserId] then + rateLimits[player.UserId] = {} + end + if not rateLimits[player.UserId][eventName] then + rateLimits[player.UserId][eventName] = { + count = 0, + lastReset = tick(), + } + end + return rateLimits[player.UserId][eventName] +end + +-- Check if a player is rate limited +_G.IsRateLimited = function(player, eventName) + local data = getRateData(player, eventName) + local now = tick() + + if now - data.lastReset > RATE_WINDOW then + data.count = 0 + data.lastReset = now + end + + data.count = data.count + 1 + + if data.count > MAX_EVENTS_PER_SECOND then + warn("Rate limit exceeded for " .. player.Name .. " on event " .. eventName) + return true + end + + return false +end + +-- Validate distance between player and target +_G.ValidateDistance = function(player, targetPosition, maxDistance) + local character = player.Character + if not character then return false end + + local rootPart = character:FindFirstChild("HumanoidRootPart") + if not rootPart then return false end + + local distance = (rootPart.Position - targetPosition).Magnitude + return distance <= maxDistance +end + +-- Validate player has enough cash +_G.ValidateCash = function(player, amount) + local leaderstats = player:FindFirstChild("leaderstats") + if not leaderstats then return false end + + local cash = leaderstats:FindFirstChild("Cash") + if not cash then return false end + + return cash.Value >= amount +end + +-- Cleanup on player leave +Players.PlayerRemoving:Connect(function(player) + rateLimits[player.UserId] = nil +end) + +print("Anti-Cheat Manager initialized") diff --git a/src/ServerScriptService/BiomeGeneratorManager.server.lua b/src/ServerScriptService/BiomeGeneratorManager.server.lua new file mode 100644 index 0000000000000000000000000000000000000000..534ce631fe3f6098addc37fe8c4516bb1aaaa2cd --- /dev/null +++ b/src/ServerScriptService/BiomeGeneratorManager.server.lua @@ -0,0 +1,195 @@ +-- src/ServerScriptService/BiomeGeneratorManager.server.lua +-- Simple flat baseplate-style world with colored biome regions + +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local CollectionService = game:GetService("CollectionService") + +local BiomeConfig = require(ReplicatedStorage:WaitForChild("Shared"):WaitForChild("BiomeConfig")) +local GameConfig = require(ReplicatedStorage:WaitForChild("Shared"):WaitForChild("GameConfig")) + +-- Create folders +local biomeZonesFolder = Instance.new("Folder") +biomeZonesFolder.Name = "BiomeZones" +biomeZonesFolder.Parent = workspace + +local worldFolder = Instance.new("Folder") +worldFolder.Name = "WorldStructures" +worldFolder.Parent = workspace + +-- Biome color map (distinct flat colors, no grass) +local BiomeColors = { + Forest = Color3.fromRGB(80, 130, 60), + PineWoods = Color3.fromRGB(50, 100, 45), + Swamp = Color3.fromRGB(65, 85, 50), + Desert = Color3.fromRGB(210, 190, 130), + Volcanic = Color3.fromRGB(70, 40, 35), + IcePeak = Color3.fromRGB(190, 210, 230), + TropicalRainforest = Color3.fromRGB(35, 110, 40), + Meadow = Color3.fromRGB(140, 180, 80), +} + +local function createBiomePlate(biomeName, biomeData) + local region = biomeData.Region + local regionWidth = region.MaxX - region.MinX + local regionDepth = region.MaxZ - region.MinZ + local centerX = (region.MinX + region.MaxX) / 2 + local centerZ = (region.MinZ + region.MaxZ) / 2 + + -- Flat baseplate for this biome + local plate = Instance.new("Part") + plate.Name = biomeName .. "_Plate" + plate.Size = Vector3.new(regionWidth, 1, regionDepth) + plate.Position = Vector3.new(centerX, -0.5, centerZ) -- Top surface at Y=0 + plate.Anchored = true + plate.Material = Enum.Material.SmoothPlastic + plate.Color = BiomeColors[biomeName] or Color3.fromRGB(100, 100, 100) + plate.TopSurface = Enum.SurfaceType.Smooth + plate.BottomSurface = Enum.SurfaceType.Smooth + plate.Parent = biomeZonesFolder + + -- Invisible detection zone (taller, for biome detection) + local zonePart = Instance.new("Part") + zonePart.Name = biomeName .. "_Zone" + zonePart.Size = Vector3.new(regionWidth, 50, regionDepth) + zonePart.Position = Vector3.new(centerX, 25, centerZ) + zonePart.Anchored = true + zonePart.CanCollide = false + zonePart.Transparency = 1 + zonePart:SetAttribute("BiomeName", biomeName) + CollectionService:AddTag(zonePart, "BiomeZone") + zonePart.Parent = biomeZonesFolder + + -- Hazard zone if applicable + if biomeData.HazardType then + local hazardZone = Instance.new("Part") + hazardZone.Name = biomeName .. "_Hazard" + hazardZone.Size = Vector3.new(regionWidth * 0.8, 20, regionDepth * 0.8) + hazardZone.Position = Vector3.new(centerX, 10, centerZ) + hazardZone.Anchored = true + hazardZone.CanCollide = false + hazardZone.Transparency = 0.95 + hazardZone.BrickColor = BrickColor.new("Bright red") + + if biomeData.HazardAttribute then + hazardZone:SetAttribute("HazardType", biomeData.HazardAttribute) + end + + CollectionService:AddTag(hazardZone, biomeData.HazardType) + hazardZone.Parent = biomeZonesFolder + end + + -- Biome label sign at center + local signPart = Instance.new("Part") + signPart.Name = biomeName .. "_Sign" + signPart.Size = Vector3.new(8, 4, 0.5) + signPart.Position = Vector3.new(centerX, 4, centerZ) + signPart.Anchored = true + signPart.Material = Enum.Material.Wood + signPart.BrickColor = BrickColor.new("Reddish brown") + signPart.Parent = biomeZonesFolder + + local signGui = Instance.new("SurfaceGui") + signGui.Face = Enum.NormalId.Front + signGui.Parent = signPart + + local signLabel = Instance.new("TextLabel") + signLabel.Size = UDim2.new(1, 0, 1, 0) + signLabel.BackgroundTransparency = 1 + signLabel.Text = biomeName + signLabel.TextColor3 = Color3.new(1, 1, 1) + signLabel.TextScaled = true + signLabel.Font = Enum.Font.GothamBold + signLabel.Parent = signGui + + print("Biome plate created:", biomeName) +end + +-- Create spawn platform (elevated slightly so it is distinct) +local function createSpawnPlatform() + local platform = Instance.new("Part") + platform.Name = "SpawnPlatform" + platform.Size = Vector3.new(GameConfig.SpawnPlatformSize.X, 0.2, GameConfig.SpawnPlatformSize.Z) + platform.Position = Vector3.new(GameConfig.SpawnPosition.X, 0.1, GameConfig.SpawnPosition.Z) -- rests slightly above biome plate + platform.Anchored = true + platform.Material = Enum.Material.SmoothPlastic + platform.Color = Color3.fromRGB(160, 160, 165) + platform.TopSurface = Enum.SurfaceType.Smooth + platform.BottomSurface = Enum.SurfaceType.Smooth + CollectionService:AddTag(platform, "SpawnPlatform") + platform.Parent = worldFolder + + local spawn = Instance.new("SpawnLocation") + spawn.Size = Vector3.new(6, 1, 6) + spawn.Position = Vector3.new(GameConfig.SpawnPosition.X, 0.5, GameConfig.SpawnPosition.Z) + spawn.Anchored = true + spawn.CanCollide = false + spawn.Transparency = 1 + spawn.Parent = worldFolder +end + +-- World boundaries +local function createWorldBoundaries() + local bounds = GameConfig.WorldBounds + local height = 200 + local thickness = 5 + + local walls = { + {pos = Vector3.new(0, height/2, bounds.MinZ), size = Vector3.new(bounds.MaxX - bounds.MinX, height, thickness)}, + {pos = Vector3.new(0, height/2, bounds.MaxZ), size = Vector3.new(bounds.MaxX - bounds.MinX, height, thickness)}, + {pos = Vector3.new(bounds.MinX, height/2, 0), size = Vector3.new(thickness, height, bounds.MaxZ - bounds.MinZ)}, + {pos = Vector3.new(bounds.MaxX, height/2, 0), size = Vector3.new(thickness, height, bounds.MaxZ - bounds.MinZ)}, + } + + for i, wallData in ipairs(walls) do + local wall = Instance.new("Part") + wall.Name = "WorldBoundary_" .. i + wall.Size = wallData.size + wall.Position = wallData.pos + wall.Anchored = true + wall.Transparency = 1 + wall.CanCollide = true + CollectionService:AddTag(wall, "WorldBoundary") + wall.Parent = worldFolder + end +end + +-- Plot areas +local function createPlots() + local plotsFolder = Instance.new("Folder") + plotsFolder.Name = "Plots" + plotsFolder.Parent = worldFolder + + for i, pos in ipairs(GameConfig.PlotPositions) do + local plot = Instance.new("Part") + plot.Name = "Plot_" .. i + plot.Size = Vector3.new(200, 0.2, 200) + plot.Position = Vector3.new(pos.X, 0.1, pos.Z) -- elevate slightly to prevent z-fighting + plot.Anchored = true + plot.Material = Enum.Material.SmoothPlastic + plot.Color = Color3.fromRGB(90, 90, 95) + plot.TopSurface = Enum.SurfaceType.Smooth + plot.BottomSurface = Enum.SurfaceType.Smooth + plot.Transparency = 1 -- fully invisible as requested + CollectionService:AddTag(plot, "EmptyPlot") + plot.Parent = plotsFolder + + + end +end + +-- Execute world generation +task.spawn(function() + print("=== Timberbound Expeditions: World Generation Starting ===") + + createSpawnPlatform() + createWorldBoundaries() + createPlots() + + -- Create flat biome plates (no terrain, no elevation) + for biomeName, biomeData in pairs(BiomeConfig.Biomes) do + createBiomePlate(biomeName, biomeData) + end + + print("=== World Generation Complete ===") + _G.BiomeGenerationComplete = true +end) diff --git a/src/ServerScriptService/ConstructionManager.server.lua b/src/ServerScriptService/ConstructionManager.server.lua new file mode 100644 index 0000000000000000000000000000000000000000..9505eb2ec33c0894b6dc4f44203948c966cefdb5 --- /dev/null +++ b/src/ServerScriptService/ConstructionManager.server.lua @@ -0,0 +1,88 @@ +-- src/ServerScriptService/ConstructionManager.server.lua + +local CollectionService = game:GetService("CollectionService") +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local Players = game:GetService("Players") + +local BuildingConfig = require(ReplicatedStorage:WaitForChild("Shared"):WaitForChild("BuildingConfig")) +local PlaceBlueprintEvent = ReplicatedStorage:WaitForChild("Events"):WaitForChild("PlaceBlueprintEvent") +local FillBlueprintEvent = ReplicatedStorage:WaitForChild("Events"):WaitForChild("FillBlueprintEvent") + +local function getPartVolume(part) + return part.Size.X * part.Size.Y * part.Size.Z +end + +-- Validate and place a blueprint +local function onPlaceBlueprint(player, structureType, cframe) + local config = BuildingConfig.Structures[structureType] + if not config then return end + + -- In a real game, you would verify if `cframe` is inside the player's Claimed Plot + -- Verification skipped here for brevity + + local blueprintModel = Instance.new("Part") + blueprintModel.Size = Vector3.new(10, 10, 1) -- Example dimensions + blueprintModel.CFrame = cframe + + -- Visual Blueprint state + blueprintModel.Transparency = 0.5 + blueprintModel.BrickColor = BrickColor.new("Neon blue") + blueprintModel.Material = Enum.Material.ForceField + blueprintModel.Anchored = true + + -- Attributes for tracking + blueprintModel:SetAttribute("StructureType", structureType) + blueprintModel:SetAttribute("WoodFilled", 0) + blueprintModel:SetAttribute("WoodRequired", config.Cost.WoodVolume) + blueprintModel:SetAttribute("MaterialRequired", config.Cost.SpecificMaterial) + blueprintModel:SetAttribute("OwnerId", player.UserId) + + CollectionService:AddTag(blueprintModel, "Blueprint") + + blueprintModel.Parent = workspace.Terrain + + -- Bind touched event so players can "throw" wood into the blueprint to fill it + local debounce = {} + blueprintModel.Touched:Connect(function(hit) + -- Make sure it's fully processed wood touching it + if CollectionService:HasTag(hit, "TreeSegment") and not debounce[hit] then + debounce[hit] = true + + local requiredMaterial = blueprintModel:GetAttribute("MaterialRequired") + local hitMaterial = hit:GetAttribute("ProcessState") or "Raw" + + if hitMaterial == requiredMaterial then + local volume = getPartVolume(hit) + + local currentFill = blueprintModel:GetAttribute("WoodFilled") + local required = blueprintModel:GetAttribute("WoodRequired") + + local newFill = currentFill + volume + blueprintModel:SetAttribute("WoodFilled", newFill) + + -- Visual feedback: change transparency based on fill % + local fillPercent = math.clamp(newFill / required, 0, 1) + blueprintModel.Transparency = 0.5 - (0.5 * fillPercent) + + hit:Destroy() + + if newFill >= required then + -- Construct! + blueprintModel.Transparency = 0 + blueprintModel.BrickColor = BrickColor.new("Burlap") + blueprintModel.Material = Enum.Material.WoodPlanks + blueprintModel.CanCollide = true + + CollectionService:RemoveTag(blueprintModel, "Blueprint") + CollectionService:AddTag(blueprintModel, "ConstructedPart") + end + end + + task.delay(0.5, function() + debounce[hit] = nil + end) + end + end) +end + +PlaceBlueprintEvent.OnServerEvent:Connect(onPlaceBlueprint) diff --git a/src/ServerScriptService/CraftingManager.server.lua b/src/ServerScriptService/CraftingManager.server.lua new file mode 100644 index 0000000000000000000000000000000000000000..1aa22fc4a1eb8ceb0cc26c627e83d659524bd1d6 --- /dev/null +++ b/src/ServerScriptService/CraftingManager.server.lua @@ -0,0 +1,102 @@ +-- src/ServerScriptService/CraftingManager.server.lua + +local Players = game:GetService("Players") +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local CraftingConfig = require(ReplicatedStorage:WaitForChild("Shared"):WaitForChild("CraftingConfig")) +local ChoppingConfig = require(ReplicatedStorage:WaitForChild("Shared"):WaitForChild("ChoppingConfig")) +local CraftEvent = ReplicatedStorage:WaitForChild("Events"):WaitForChild("CraftEvent") +local NotificationEvent = ReplicatedStorage:WaitForChild("Events"):WaitForChild("NotificationEvent") +local ShopEvent = ReplicatedStorage:WaitForChild("Events"):WaitForChild("ShopEvent") +local SoundEvent = ReplicatedStorage:WaitForChild("Events"):WaitForChild("SoundEvent") + +local function getPlayerResources(player) + local data = _G.GetPlayerData(player) + if not data then return {} end + if not data.Resources then + data.Resources = {} + end + return data.Resources +end + +local function canCraft(player, recipeId) + local recipe = CraftingConfig.Recipes[recipeId] + if not recipe then return false, "Unknown recipe" end + + local resources = getPlayerResources(player) + + for material, requiredAmount in pairs(recipe.Materials) do + local have = resources[material] or 0 + if have < requiredAmount then + return false, "Need " .. tostring(requiredAmount) .. " " .. material .. " (have " .. tostring(have) .. ")" + end + end + + -- Check if player already owns this tool + if ChoppingConfig.AxeTypes[recipeId] and _G.HasInInventory and _G.HasInInventory(player, "Tools", recipeId) then + return false, "You already own " .. (recipe.Name or recipeId) + end + + return true, "OK" +end + +local function craftItem(player, recipeId) + -- Anti-cheat + if _G.IsRateLimited and _G.IsRateLimited(player, "Craft") then return end + + local canDo, reason = canCraft(player, recipeId) + if not canDo then + NotificationEvent:FireClient(player, "Error", reason) + SoundEvent:FireClient(player, "PurchaseFail", nil) + return + end + + local recipe = CraftingConfig.Recipes[recipeId] + local resources = getPlayerResources(player) + + -- Deduct materials + for material, requiredAmount in pairs(recipe.Materials) do + resources[material] = resources[material] - requiredAmount + end + + -- Add tool to inventory + if _G.AddToInventory then + _G.AddToInventory(player, "Tools", recipeId) + end + + -- Auto-equip if it is an axe + if ChoppingConfig.AxeTypes[recipeId] then + local data = _G.GetPlayerData(player) + if data then + data.EquippedAxe = recipeId + player:SetAttribute("EquippedAxe", recipeId) + if player.Character then + player.Character:SetAttribute("EquippedAxe", recipeId) + end + end + end + + NotificationEvent:FireClient(player, "Crafted", "Crafted " .. (recipe.Name or recipeId) .. "!") + SoundEvent:FireClient(player, "ConstructComplete", nil) + + -- Send updated inventory to client + local data = _G.GetPlayerData(player) + if data then + ShopEvent:FireClient(player, "InventoryUpdate", data.Inventory) + CraftEvent:FireClient(player, "ResourceUpdate", data.Resources) + end +end + +-- Handle resource query from client +local function onCraftEvent(player, action, data) + if action == "Craft" then + craftItem(player, data) + elseif action == "QueryResources" then + local resources = getPlayerResources(player) + CraftEvent:FireClient(player, "ResourceUpdate", resources) + end +end + +CraftEvent.OnServerEvent:Connect(onCraftEvent) + +print("Crafting Manager initialized") diff --git a/src/ServerScriptService/DatastoreManager.server.lua b/src/ServerScriptService/DatastoreManager.server.lua new file mode 100644 index 0000000000000000000000000000000000000000..6504de66ec6d801a7ca3616e6a11efb526b9b622 --- /dev/null +++ b/src/ServerScriptService/DatastoreManager.server.lua @@ -0,0 +1,276 @@ +-- src/ServerScriptService/DatastoreManager.server.lua + +local DataStoreService = game:GetService("DataStoreService") +local HttpService = game:GetService("HttpService") +local Players = game:GetService("Players") + +local PlotDataStore +local datastoreAvailable = false + +local success, err = pcall(function() + PlotDataStore = DataStoreService:GetDataStore("Timberbound_PlayerData_v2") +end) + +if success then + datastoreAvailable = true +else + warn("DataStore not available (place may not be published): " .. tostring(err)) +end + +local AUTO_SAVE_INTERVAL = 120 -- seconds +local MAX_RETRIES = 3 + +local function serializePlotItems(plotPart) + local dataToSave = {} + + for _, item in pairs(plotPart:GetChildren()) do + if not (item:IsA("Model") or item:IsA("BasePart")) then continue end + + local itemId = item:GetAttribute("ItemId") or item.Name + local relativeCFrame = plotPart.CFrame:ToObjectSpace(item:GetPivot()) + local px, py, pz, r00, r01, r02, r10, r11, r12, r20, r21, r22 = relativeCFrame:GetComponents() + + local stateData = {} + if item:GetAttribute("FillLevel") then + stateData.FillLevel = item:GetAttribute("FillLevel") + end + if item:GetAttribute("WoodFilled") then + stateData.WoodFilled = item:GetAttribute("WoodFilled") + end + if item:GetAttribute("ProcessState") then + stateData.ProcessState = item:GetAttribute("ProcessState") + end + + table.insert(dataToSave, { + id = itemId, + cframe = {px, py, pz, r00, r01, r02, r10, r11, r12, r20, r21, r22}, + state = stateData, + }) + end + + return dataToSave +end + +local function buildSaveData(player, plotPart) + local data = _G.GetPlayerData(player) + local leaderstats = player:FindFirstChild("leaderstats") + + local saveData = { + version = 2, + cash = 0, + woodChopped = 0, + inventory = {}, + questProgress = {}, + achievements = {}, + biomesVisited = {}, + totalEarned = 0, + totalWoodSold = 0, + totalBuilt = 0, + plotItems = {}, + } + + -- Cash and stats from leaderstats + if leaderstats then + local cash = leaderstats:FindFirstChild("Cash") + if cash then saveData.cash = cash.Value end + local wood = leaderstats:FindFirstChild("WoodChopped") + if wood then saveData.woodChopped = wood.Value end + end + + -- Player data + if data then + saveData.inventory = data.Inventory or {} + saveData.questProgress = data.QuestProgress or {} + saveData.biomesVisited = data.BiomesVisited or {} + saveData.totalEarned = data.TotalEarned or 0 + saveData.totalWoodSold = data.TotalWoodSold or 0 + saveData.totalBuilt = data.TotalBuilt or 0 + end + + -- Quest data + local quests = _G.GetPlayerQuests and _G.GetPlayerQuests(player) + if quests then + saveData.questProgress = quests + end + + -- Achievement data + local achievements = _G.GetPlayerAchievements and _G.GetPlayerAchievements(player) + if achievements then + saveData.achievements = achievements + end + + -- Plot items + if plotPart then + saveData.plotItems = serializePlotItems(plotPart) + end + + return saveData +end + +-- Save with retry logic +local function saveWithRetry(userId, saveData) + if not datastoreAvailable then return false end + for attempt = 1, MAX_RETRIES do + local success, err = pcall(function() + local jsonString = HttpService:JSONEncode(saveData) + PlotDataStore:SetAsync(tostring(userId), jsonString) + end) + + if success then + return true + else + warn("Save attempt " .. attempt .. " failed for userId " .. userId .. ": " .. tostring(err)) + if attempt < MAX_RETRIES then + task.wait(2 ^ attempt) -- Exponential backoff + end + end + end + return false +end + +-- Save Player Data (globally accessible) +_G.SavePlayerData = function(player, plotPart) + if not datastoreAvailable then + print("DataStore not available, skipping save for " .. player.Name) + return false + end + local saveData = buildSaveData(player, plotPart) + local success = saveWithRetry(player.UserId, saveData) + + if success then + print("Successfully saved data for " .. player.Name) + else + warn("Failed to save data for " .. player.Name .. " after " .. MAX_RETRIES .. " retries") + end + + return success +end + +-- Load Player Data +_G.LoadPlayerData = function(player, plotPart) + if not datastoreAvailable then + print("DataStore not available, skipping load for " .. player.Name) + return + end + local success, result = pcall(function() + return PlotDataStore:GetAsync(tostring(player.UserId)) + end) + + if not success then + warn("Failed to load data for " .. player.Name .. ": " .. tostring(result)) + return + end + + if not result then + print("No saved data found for " .. player.Name .. " (new player)") + return + end + + local savedData = HttpService:JSONDecode(result) + + -- Restore cash + local leaderstats = player:FindFirstChild("leaderstats") + if leaderstats and savedData.cash then + local cash = leaderstats:FindFirstChild("Cash") + if cash then cash.Value = savedData.cash end + local wood = leaderstats:FindFirstChild("WoodChopped") + if wood and savedData.woodChopped then wood.Value = savedData.woodChopped end + end + + -- Restore player data cache + local data = _G.GetPlayerData(player) + if data then + if savedData.inventory then data.Inventory = savedData.inventory end + if savedData.biomesVisited then data.BiomesVisited = savedData.biomesVisited end + if savedData.totalEarned then data.TotalEarned = savedData.totalEarned end + if savedData.totalWoodSold then data.TotalWoodSold = savedData.totalWoodSold end + if savedData.totalBuilt then data.TotalBuilt = savedData.totalBuilt end + + -- Restore equipped axe + if savedData.inventory and savedData.inventory.Tools and #savedData.inventory.Tools > 0 then + local lastTool = savedData.inventory.Tools[#savedData.inventory.Tools] + data.EquippedAxe = lastTool + player:SetAttribute("EquippedAxe", lastTool) + end + end + + -- Restore plot items + if plotPart and savedData.plotItems then + for _, itemData in pairs(savedData.plotItems) do + -- Create parts from saved data + local newPart = Instance.new("Part") + newPart.Name = itemData.id + newPart:SetAttribute("ItemId", itemData.id) + + -- Reconstruct CFrame + if itemData.cframe and #itemData.cframe >= 12 then + local cf = CFrame.new(unpack(itemData.cframe)) + newPart:PivotTo(plotPart.CFrame * cf) + end + + -- Restore state + if itemData.state then + for key, value in pairs(itemData.state) do + newPart:SetAttribute(key, value) + end + end + + newPart.Anchored = true + newPart.Size = Vector3.new(4, 4, 4) + newPart.Material = Enum.Material.WoodPlanks + newPart.Parent = plotPart + end + end + + print("Successfully loaded data for " .. player.Name) +end + +-- Auto-save loop +task.spawn(function() + while true do + task.wait(AUTO_SAVE_INTERVAL) + for _, player in ipairs(Players:GetPlayers()) do + task.spawn(function() + pcall(function() + -- Find their plot + local plotPart = nil + local plotsFolder = workspace:FindFirstChild("WorldStructures") + if plotsFolder then + local plots = plotsFolder:FindFirstChild("Plots") + if plots then + for _, plot in pairs(plots:GetChildren()) do + if plot:GetAttribute("OwnerId") == player.UserId then + plotPart = plot + break + end + end + end + end + _G.SavePlayerData(player, plotPart) + end) + end) + end + end +end) + +-- BindToClose for server shutdown +game:BindToClose(function() + for _, player in ipairs(Players:GetPlayers()) do + pcall(function() + local plotPart = nil + local plotsFolder = workspace:FindFirstChild("WorldStructures") + if plotsFolder then + local plots = plotsFolder:FindFirstChild("Plots") + if plots then + for _, plot in pairs(plots:GetChildren()) do + if plot:GetAttribute("OwnerId") == player.UserId then + plotPart = plot + break + end + end + end + end + _G.SavePlayerData(player, plotPart) + end) + end +end) diff --git a/src/ServerScriptService/DayNightManager.server.lua b/src/ServerScriptService/DayNightManager.server.lua new file mode 100644 index 0000000000000000000000000000000000000000..7d8473ff7443298823c7eb7dd6d1b5fc29401014 --- /dev/null +++ b/src/ServerScriptService/DayNightManager.server.lua @@ -0,0 +1,122 @@ +-- src/ServerScriptService/DayNightManager.server.lua + +local Lighting = game:GetService("Lighting") +local TweenService = game:GetService("TweenService") + +-- Day/Night cycle settings +local CYCLE_SPEED = 1 -- 1 = real-time (1 min real = 1 min ingame), higher = faster +local MINUTES_PER_CYCLE = 20 -- Full day-night cycle in real minutes + +-- Lighting presets +local Presets = { + Dawn = { + ClockTime = 6, + Ambient = Color3.fromRGB(140, 120, 100), + OutdoorAmbient = Color3.fromRGB(120, 100, 80), + Brightness = 1, + FogColor = Color3.fromRGB(200, 170, 140), + FogEnd = 1500, + }, + Day = { + ClockTime = 12, + Ambient = Color3.fromRGB(150, 150, 150), + OutdoorAmbient = Color3.fromRGB(140, 140, 140), + Brightness = 2, + FogColor = Color3.fromRGB(200, 210, 220), + FogEnd = 2000, + }, + Dusk = { + ClockTime = 18, + Ambient = Color3.fromRGB(150, 100, 80), + OutdoorAmbient = Color3.fromRGB(130, 90, 70), + Brightness = 1, + FogColor = Color3.fromRGB(200, 140, 100), + FogEnd = 1200, + }, + Night = { + ClockTime = 0, + Ambient = Color3.fromRGB(50, 50, 70), + OutdoorAmbient = Color3.fromRGB(30, 30, 50), + Brightness = 0, + FogColor = Color3.fromRGB(40, 40, 60), + FogEnd = 800, + }, +} + +-- Initialize lighting +Lighting.ClockTime = 6 +Lighting.GeographicLatitude = 40 +Lighting.GlobalShadows = true + +-- Add atmosphere +local atmosphere = Instance.new("Atmosphere") +atmosphere.Density = 0.3 +atmosphere.Offset = 0.25 +atmosphere.Color = Color3.fromRGB(199, 199, 199) +atmosphere.Decay = Color3.fromRGB(92, 60, 13) +atmosphere.Glare = 0 +atmosphere.Haze = 1 +atmosphere.Parent = Lighting + +-- Add sky +local sky = Instance.new("Sky") +sky.SunAngularSize = 11 +sky.MoonAngularSize = 9 +sky.Parent = Lighting + +-- Add bloom effect +local bloom = Instance.new("BloomEffect") +bloom.Intensity = 0.2 +bloom.Size = 24 +bloom.Threshold = 0.9 +bloom.Parent = Lighting + +-- Add color correction +local colorCorrection = Instance.new("ColorCorrectionEffect") +colorCorrection.Brightness = 0 +colorCorrection.Contrast = 0.1 +colorCorrection.Saturation = 0.15 +colorCorrection.TintColor = Color3.new(1, 1, 1) +colorCorrection.Parent = Lighting + +-- Add sun rays +local sunRays = Instance.new("SunRaysEffect") +sunRays.Intensity = 0.05 +sunRays.Spread = 0.5 +sunRays.Parent = Lighting + +-- Smooth day/night cycle +local cycleStep = (24 / (MINUTES_PER_CYCLE * 60)) * CYCLE_SPEED + +task.spawn(function() + while true do + Lighting.ClockTime = (Lighting.ClockTime + cycleStep) % 24 + + -- Adjust lighting properties based on time of day + local time = Lighting.ClockTime + + if time >= 5 and time < 7 then + -- Dawn transition + local t = (time - 5) / 2 + colorCorrection.TintColor = Color3.new(1, 0.9 + t * 0.1, 0.85 + t * 0.15) + sunRays.Intensity = 0.02 + t * 0.06 + elseif time >= 7 and time < 17 then + -- Full day + colorCorrection.TintColor = Color3.new(1, 1, 1) + sunRays.Intensity = 0.05 + elseif time >= 17 and time < 19 then + -- Dusk transition + local t = (time - 17) / 2 + colorCorrection.TintColor = Color3.new(1, 0.95 - t * 0.1, 0.9 - t * 0.2) + sunRays.Intensity = 0.08 - t * 0.06 + else + -- Night + colorCorrection.TintColor = Color3.new(0.8, 0.8, 0.95) + sunRays.Intensity = 0 + end + + task.wait(1) + end +end) + +print("Day/Night cycle started") diff --git a/src/ServerScriptService/DragManager.server.lua b/src/ServerScriptService/DragManager.server.lua new file mode 100644 index 0000000000000000000000000000000000000000..b3aedec7053eba1a3dac8ee434b65ad773a6f321 --- /dev/null +++ b/src/ServerScriptService/DragManager.server.lua @@ -0,0 +1,118 @@ +-- src/ServerScriptService/DragManager.server.lua + +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local CollectionService = game:GetService("CollectionService") +local Players = game:GetService("Players") +local RunService = game:GetService("RunService") + +local DraggingConfig = require(ReplicatedStorage:WaitForChild("Shared"):WaitForChild("DraggingConfig")) +local DragEvent = ReplicatedStorage:WaitForChild("Events"):WaitForChild("DragEvent") +local DropEvent = ReplicatedStorage:WaitForChild("Events"):WaitForChild("DropEvent") + +-- Store active dragging states +local activeDrags = {} + +-- Validate if the object can be dragged (e.g. TreeSegments, Logs, Furniture) +local function canBeDragged(part) + return CollectionService:HasTag(part, "Draggable") or CollectionService:HasTag(part, "TreeSegment") +end + +-- Cleanup function when a player drops an object or disconnects +local function dropObject(player) + local dragData = activeDrags[player.UserId] + if dragData then + -- Clean up attachments and constraints + if dragData.dragAttachment then dragData.dragAttachment:Destroy() end + if dragData.targetAttachment then dragData.targetAttachment:Destroy() end + if dragData.alignPos then dragData.alignPos:Destroy() end + if dragData.alignOri then dragData.alignOri:Destroy() end + + -- Reset Network Ownership to Server (or Auto) + if dragData.part and dragData.part:IsDescendantOf(workspace) then + -- Optional: Add a small impulse so it doesn't just freeze mid-air + pcall(function() + dragData.part:SetNetworkOwner(nil) + end) + end + + activeDrags[player.UserId] = nil + end +end + +-- Handle Drag Event Request +local function onDragRequest(player, targetPart, hitPoint) + if not targetPart or not targetPart:IsA("BasePart") then return end + if not canBeDragged(targetPart) then return end + + local char = player.Character + if not char or not char:FindFirstChild("HumanoidRootPart") then return end + + -- Distance verification + local distance = (char.HumanoidRootPart.Position - targetPart.Position).Magnitude + if distance > DraggingConfig.MaxGrabDistance then return end + + -- Ensure player isn't already dragging something + if activeDrags[player.UserId] then dropObject(player) end + + -- Verify the part isn't already being dragged by someone else + for _, dragData in pairs(activeDrags) do + if dragData.part == targetPart then return end + end + + -- Setup Attachments and Constraints + local partAttachment = Instance.new("Attachment") + partAttachment.Name = "DragAttachment" + partAttachment.CFrame = targetPart.CFrame:ToObjectSpace(CFrame.new(hitPoint)) + partAttachment.Parent = targetPart + + -- We create a proxy attachment in Terrain, managed by the client later, or just update it via Server + -- For Smoothest results, we give the Client network ownership and let local physics do the dragging, but the Constraints exist Server-Side. + + local targetAttachment = Instance.new("Attachment") + targetAttachment.Name = "TargetAttachment_Player" .. player.UserId + + -- Setting it to character's Head/Camera relative position on the server initially + targetAttachment.WorldCFrame = CFrame.new(char.Head.Position + char.Head.CFrame.LookVector * DraggingConfig.HoldDistance) + targetAttachment.Parent = workspace.Terrain + + local alignPos = Instance.new("AlignPosition") + alignPos.Attachment0 = partAttachment + alignPos.Attachment1 = targetAttachment + alignPos.MaxForce = DraggingConfig.AlignPositionMaxForce + alignPos.Responsiveness = DraggingConfig.AlignPositionResponsiveness + alignPos.Parent = targetPart + + local alignOri = Instance.new("AlignOrientation") + alignOri.Attachment0 = partAttachment + alignOri.Attachment1 = targetAttachment + alignOri.MaxTorque = DraggingConfig.AlignOrientationMaxTorque + alignOri.Responsiveness = DraggingConfig.AlignOrientationResponsiveness + alignOri.Parent = targetPart + + -- Grant Network Ownership to the Player to eliminate latency rubber-banding + pcall(function() + targetPart:SetNetworkOwner(player) + end) + + -- Save the state + activeDrags[player.UserId] = { + part = targetPart, + dragAttachment = partAttachment, + targetAttachment = targetAttachment, + alignPos = alignPos, + alignOri = alignOri + } +end + +-- Handle Drop Event Request +local function onDropRequest(player) + dropObject(player) +end + +-- Players leaving cleanup +Players.PlayerRemoving:Connect(function(player) + dropObject(player) +end) + +DragEvent.OnServerEvent:Connect(onDragRequest) +DropEvent.OnServerEvent:Connect(onDropRequest) diff --git a/src/ServerScriptService/DroneAutomationManager.server.lua b/src/ServerScriptService/DroneAutomationManager.server.lua new file mode 100644 index 0000000000000000000000000000000000000000..63f4eadc8bc8be653f611773ba3bad6a9758591c --- /dev/null +++ b/src/ServerScriptService/DroneAutomationManager.server.lua @@ -0,0 +1,111 @@ +-- src/ServerScriptService/DroneAutomationManager.server.lua + +local CollectionService = game:GetService("CollectionService") +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local RunService = game:GetService("RunService") + +local AutomationConfig = require(ReplicatedStorage:WaitForChild("Shared"):WaitForChild("AutomationConfig")) + +local activeDrones = {} + +local function getPartVolume(part) + return part.Size.X * part.Size.Y * part.Size.Z +end + +local function spawnDrone(droneType, startZone, targetZone) + local config = AutomationConfig.Drones[droneType] + if not config then return end + + -- In real game, clone from ReplicatedStorage + local droneModel = Instance.new("Part") + droneModel.Name = "DeliveryDrone" + droneModel.Size = Vector3.new(4, 1, 4) + droneModel.BrickColor = BrickColor.new("Dark stone grey") + droneModel.Anchored = true + droneModel.CFrame = startZone.CFrame + Vector3.new(0, 5, 0) + + droneModel:SetAttribute("TargetZone", targetZone.Name) + droneModel.Parent = workspace + + local droneData = { + model = droneModel, + config = config, + state = "Idle", -- Idle, Loading, Traveling, Unloading + startPos = droneModel.Position, + targetPos = targetZone.Position + Vector3.new(0, 10, 0), + carriedItem = nil + } + + table.insert(activeDrones, droneData) + return droneData +end + +-- Simplified Drone AI Loop +RunService.Heartbeat:Connect(function(dt) + for i, drone in ipairs(activeDrones) do + local model = drone.model + if not model or not model.Parent then + table.remove(activeDrones, i) + continue + end + + if drone.state == "Traveling" then + local direction = (drone.targetPos - model.Position).Unit + local distance = (drone.targetPos - model.Position).Magnitude + + local moveStep = math.min(drone.config.Speed * dt, distance) + model.Position = model.Position + (direction * moveStep) + + if drone.carriedItem and drone.carriedItem.Parent then + drone.carriedItem.Position = model.Position - Vector3.new(0, 3, 0) + end + + if distance < 1 then + drone.state = "Unloading" + -- Drop item + if drone.carriedItem then + drone.carriedItem.Anchored = false + drone.carriedItem = nil + end + + -- Return Home + local temp = drone.startPos + drone.startPos = drone.targetPos + drone.targetPos = temp + drone.state = "Traveling" + end + end + end +end) + +-- Drone Pickup Pad Logic +local function bindDronePad(pad) + local debounce = {} + + pad.Touched:Connect(function(hit) + if CollectionService:HasTag(hit, "TreeSegment") and not debounce[hit] then + debounce[hit] = true + + local volume = getPartVolume(hit) + + -- Find an available idle drone assigned to this pad + for _, drone in ipairs(activeDrones) do + if drone.state == "Idle" and volume <= drone.config.MaxCarryWeight then + drone.state = "Traveling" + drone.carriedItem = hit + hit.Anchored = true + break + end + end + + task.delay(1, function() + debounce[hit] = nil + end) + end + end) +end + +CollectionService:GetInstanceAddedSignal("DronePad"):Connect(bindDronePad) +for _, pad in pairs(CollectionService:GetTagged("DronePad")) do + bindDronePad(pad) +end diff --git a/src/ServerScriptService/EnvironmentalPuzzleManager.server.lua b/src/ServerScriptService/EnvironmentalPuzzleManager.server.lua new file mode 100644 index 0000000000000000000000000000000000000000..828ad413d1a926f34904707226beb643778d22d0 --- /dev/null +++ b/src/ServerScriptService/EnvironmentalPuzzleManager.server.lua @@ -0,0 +1,79 @@ +-- src/ServerScriptService/EnvironmentalPuzzleManager.server.lua + +local CollectionService = game:GetService("CollectionService") +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local RunService = game:GetService("RunService") + +local ExplorationConfig = require(ReplicatedStorage:WaitForChild("Shared"):WaitForChild("ExplorationConfig")) + +-- Manage Weight Switches (Pressure Plates that require heavy logs) +local function bindWeightSwitch(switchModel) + local platePart = switchModel:FindFirstChild("Plate") + local targetDoor = switchModel:FindFirstChild("LinkedDoor") or switchModel.Parent:FindFirstChild("Door") + + if not platePart or not targetDoor then return end + + local config = ExplorationConfig.Puzzles.WeightSwitch + local isActive = false + + RunService.Heartbeat:Connect(function() + -- Calculate total mass resting on the plate + local touchingParts = workspace:GetPartsInPart(platePart) + local totalMass = 0 + + for _, part in ipairs(touchingParts) do + if part:IsA("BasePart") and not part.Anchored then + totalMass = totalMass + part.AssemblyMass + end + end + + if totalMass >= config.RequiredWeight and not isActive then + isActive = true + platePart.BrickColor = BrickColor.new("Bright green") + -- Open Door Tween + targetDoor.Transparency = 0.5 + targetDoor.CanCollide = false + print("Puzzle Solved: Door Opened") + + elseif totalMass < config.RequiredWeight and isActive then + isActive = false + platePart.BrickColor = BrickColor.new("Bright red") + -- Close Door Tween + targetDoor.Transparency = 0 + targetDoor.CanCollide = true + print("Puzzle Reset: Door Closed") + end + end) +end + +CollectionService:GetInstanceAddedSignal("WeightSwitch"):Connect(bindWeightSwitch) +for _, switch in pairs(CollectionService:GetTagged("WeightSwitch")) do + bindWeightSwitch(switch) +end + +-- Manage Biome Hazards (e.g., Sinking in Swamps) +local function bindSwampZone(zonePart) + local config = ExplorationConfig.Biomes.Swamp + + RunService.Heartbeat:Connect(function(dt) + local touchingParts = workspace:GetPartsInPart(zonePart) + for _, part in ipairs(touchingParts) do + -- Sink objects that aren't specifically built for swamps + if part:IsA("BasePart") and not part.Anchored and not CollectionService:HasTag(part, "SwampTires") then + -- Apply a downward velocity to simulate sinking + part.AssemblyLinearVelocity = Vector3.new( + part.AssemblyLinearVelocity.X, + -config.SinkingSpeed, + part.AssemblyLinearVelocity.Z + ) + end + + -- Can add Player damage logic here if character parts touch it + end + end) +end + +CollectionService:GetInstanceAddedSignal("SwampZone"):Connect(bindSwampZone) +for _, zone in pairs(CollectionService:GetTagged("SwampZone")) do + bindSwampZone(zone) +end diff --git a/src/ServerScriptService/InventoryManager.server.lua b/src/ServerScriptService/InventoryManager.server.lua new file mode 100644 index 0000000000000000000000000000000000000000..df5502b7bb0d8461bf80afffd207447068a2b4df --- /dev/null +++ b/src/ServerScriptService/InventoryManager.server.lua @@ -0,0 +1,40 @@ +-- src/ServerScriptService/InventoryManager.server.lua + +local Players = game:GetService("Players") +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local ShopConfig = require(ReplicatedStorage:WaitForChild("Shared"):WaitForChild("ShopConfig")) +local ChoppingConfig = require(ReplicatedStorage:WaitForChild("Shared"):WaitForChild("ChoppingConfig")) +local EquipToolEvent = ReplicatedStorage:WaitForChild("Events"):WaitForChild("EquipToolEvent") +local NotificationEvent = ReplicatedStorage:WaitForChild("Events"):WaitForChild("NotificationEvent") + +-- Handle equip tool requests from client +local function onEquipTool(player, toolId) + if not toolId or type(toolId) ~= "string" then return end + + -- Validate the tool exists in configs + if not ChoppingConfig.AxeTypes[toolId] then return end + + -- Validate the player owns the tool + if not _G.HasInInventory(player, "Tools", toolId) then + NotificationEvent:FireClient(player, "Error", "You don't own this tool!") + return + end + + -- Update the equipped axe + local data = _G.GetPlayerData(player) + if data then + data.EquippedAxe = toolId + player:SetAttribute("EquippedAxe", toolId) + + -- Update character attribute + local character = player.Character + if character then + character:SetAttribute("EquippedAxe", toolId) + end + + NotificationEvent:FireClient(player, "Equipped", "Equipped " .. toolId) + end +end + +EquipToolEvent.OnServerEvent:Connect(onEquipTool) diff --git a/src/ServerScriptService/LeaderboardManager.server.lua b/src/ServerScriptService/LeaderboardManager.server.lua new file mode 100644 index 0000000000000000000000000000000000000000..8d13d34d14e4f9293339e6c2551b692368187e83 --- /dev/null +++ b/src/ServerScriptService/LeaderboardManager.server.lua @@ -0,0 +1,114 @@ +-- src/ServerScriptService/LeaderboardManager.server.lua + +local Players = game:GetService("Players") +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local GameConfig = require(ReplicatedStorage:WaitForChild("Shared"):WaitForChild("GameConfig")) + +-- Create global leaderboard SurfaceGui on a part in the world +local leaderboardFolder = Instance.new("Folder") +leaderboardFolder.Name = "Leaderboard" +leaderboardFolder.Parent = workspace + +local function createLeaderboardDisplay() + local board = Instance.new("Part") + board.Name = "LeaderboardBoard" + board.Size = Vector3.new(12, 8, 0.5) + board.Position = Vector3.new(-10, 8, -80) + board.Anchored = true + board.Material = Enum.Material.SmoothPlastic + board.BrickColor = BrickColor.new("Really black") + board.Parent = leaderboardFolder + + local surfaceGui = Instance.new("SurfaceGui") + surfaceGui.Name = "LeaderboardGui" + surfaceGui.Face = Enum.NormalId.Front + surfaceGui.SizingMode = Enum.SurfaceGuiSizingMode.PixelsPerStud + surfaceGui.PixelsPerStud = 50 + surfaceGui.Parent = board + + -- Title + local title = Instance.new("TextLabel") + title.Name = "Title" + title.Size = UDim2.new(1, 0, 0.15, 0) + title.Position = UDim2.new(0, 0, 0, 0) + title.BackgroundColor3 = Color3.fromRGB(40, 80, 40) + title.BorderSizePixel = 0 + title.Text = "TOP LUMBERJACKS" + title.TextColor3 = Color3.new(1, 1, 1) + title.TextScaled = true + title.Font = Enum.Font.GothamBold + title.Parent = surfaceGui + + -- Entries container + local entriesFrame = Instance.new("Frame") + entriesFrame.Name = "Entries" + entriesFrame.Size = UDim2.new(1, 0, 0.85, 0) + entriesFrame.Position = UDim2.new(0, 0, 0.15, 0) + entriesFrame.BackgroundColor3 = Color3.fromRGB(20, 20, 20) + entriesFrame.BorderSizePixel = 0 + entriesFrame.Parent = surfaceGui + + local listLayout = Instance.new("UIListLayout") + listLayout.SortOrder = Enum.SortOrder.LayoutOrder + listLayout.Parent = entriesFrame + + return entriesFrame +end + +local entriesFrame = createLeaderboardDisplay() + +local function updateLeaderboard() + -- Clear existing entries + for _, child in pairs(entriesFrame:GetChildren()) do + if child:IsA("TextLabel") then + child:Destroy() + end + end + + -- Collect player data + local playerStats = {} + for _, player in ipairs(Players:GetPlayers()) do + local leaderstats = player:FindFirstChild("leaderstats") + if leaderstats then + local cash = leaderstats:FindFirstChild("Cash") + if cash then + table.insert(playerStats, { + name = player.Name, + cash = cash.Value, + }) + end + end + end + + -- Sort by cash descending + table.sort(playerStats, function(a, b) return a.cash > b.cash end) + + -- Display top 10 + local maxEntries = math.min(#playerStats, 10) + for i = 1, maxEntries do + local entry = Instance.new("TextLabel") + entry.Name = "Entry_" .. i + entry.Size = UDim2.new(1, 0, 0, 30) + entry.LayoutOrder = i + entry.BackgroundTransparency = 0.5 + entry.BackgroundColor3 = i % 2 == 0 and Color3.fromRGB(30, 30, 30) or Color3.fromRGB(40, 40, 40) + entry.BorderSizePixel = 0 + entry.Text = string.format(" #%d %s - $%s", i, playerStats[i].name, tostring(playerStats[i].cash)) + entry.TextColor3 = i == 1 and Color3.fromRGB(255, 215, 0) or Color3.new(1, 1, 1) + entry.TextXAlignment = Enum.TextXAlignment.Left + entry.TextScaled = true + entry.Font = Enum.Font.GothamMedium + entry.Parent = entriesFrame + end +end + +-- Update periodically +task.spawn(function() + while true do + updateLeaderboard() + task.wait(15) + end +end) + +print("Leaderboard Manager initialized") diff --git a/src/ServerScriptService/MapManager.server.lua b/src/ServerScriptService/MapManager.server.lua new file mode 100644 index 0000000000000000000000000000000000000000..5df6a49a4ccb7251b6a8b054c7a5e018e72fca49 --- /dev/null +++ b/src/ServerScriptService/MapManager.server.lua @@ -0,0 +1,227 @@ +-- src/ServerScriptService/MapManager.server.lua + +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local CollectionService = game:GetService("CollectionService") + +local GameConfig = require(ReplicatedStorage:WaitForChild("Shared"):WaitForChild("GameConfig")) + +local worldFolder = workspace:WaitForChild("WorldStructures") + +-- Create market area +local function createMarketArea() + local marketFolder = Instance.new("Folder") + marketFolder.Name = "Market" + marketFolder.Parent = worldFolder + + -- Market building floor + local floor = Instance.new("Part") + floor.Name = "MarketFloor" + floor.Size = GameConfig.MarketSize + floor.Position = GameConfig.MarketPosition + floor.Anchored = true + floor.Material = Enum.Material.Cobblestone + floor.BrickColor = BrickColor.new("Dark stone grey") + floor.TopSurface = Enum.SurfaceType.Smooth + floor.Parent = marketFolder + + -- Market walls (3 walls, open front) + local wallHeight = 10 + local wallThickness = 1 + local marketPos = GameConfig.MarketPosition + local mSize = GameConfig.MarketSize + + local walls = { + {name = "BackWall", pos = Vector3.new(marketPos.X, marketPos.Y + wallHeight/2, marketPos.Z - mSize.Z/2), size = Vector3.new(mSize.X, wallHeight, wallThickness)}, + {name = "LeftWall", pos = Vector3.new(marketPos.X - mSize.X/2, marketPos.Y + wallHeight/2, marketPos.Z), size = Vector3.new(wallThickness, wallHeight, mSize.Z)}, + {name = "RightWall", pos = Vector3.new(marketPos.X + mSize.X/2, marketPos.Y + wallHeight/2, marketPos.Z), size = Vector3.new(wallThickness, wallHeight, mSize.Z)}, + } + + for _, wallData in ipairs(walls) do + local wall = Instance.new("Part") + wall.Name = wallData.name + wall.Size = wallData.size + wall.Position = wallData.pos + wall.Anchored = true + wall.Material = Enum.Material.WoodPlanks + wall.BrickColor = BrickColor.new("Burlap") + wall.Parent = marketFolder + end + + -- Roof + local roof = Instance.new("Part") + roof.Name = "MarketRoof" + roof.Size = Vector3.new(mSize.X + 4, 1, mSize.Z + 4) + roof.Position = Vector3.new(marketPos.X, marketPos.Y + wallHeight, marketPos.Z) + roof.Anchored = true + roof.Material = Enum.Material.WoodPlanks + roof.BrickColor = BrickColor.new("Reddish brown") + roof.Parent = marketFolder + + -- Market dropoff zone + local dropoff = Instance.new("Part") + dropoff.Name = "MarketDropoffZone" + dropoff.Size = Vector3.new(15, 3, 15) + dropoff.Position = GameConfig.MarketPosition + Vector3.new(0, 2, 10) + dropoff.Anchored = true + dropoff.CanCollide = false + dropoff.Transparency = 0.7 + dropoff.BrickColor = BrickColor.new("Bright green") + dropoff.Material = Enum.Material.ForceField + CollectionService:AddTag(dropoff, "MarketDropoff") + dropoff.Parent = marketFolder + + -- Market sign + local signPart = Instance.new("Part") + signPart.Name = "MarketSign" + signPart.Size = Vector3.new(12, 3, 0.5) + signPart.Position = Vector3.new(marketPos.X, marketPos.Y + wallHeight + 2.5, marketPos.Z + mSize.Z/2) + signPart.Anchored = true + signPart.Material = Enum.Material.Wood + signPart.BrickColor = BrickColor.new("Dark orange") + signPart.Parent = marketFolder + + local signGui = Instance.new("SurfaceGui") + signGui.Face = Enum.NormalId.Front + signGui.Parent = signPart + + local signLabel = Instance.new("TextLabel") + signLabel.Size = UDim2.new(1, 0, 1, 0) + signLabel.BackgroundTransparency = 1 + signLabel.Text = "LUMBER MARKET" + signLabel.TextColor3 = Color3.new(1, 1, 1) + signLabel.TextScaled = true + signLabel.Font = Enum.Font.GothamBold + signLabel.Parent = signGui + + print("Market area created") +end + +-- Create shop area +local function createShopArea() + local shopFolder = Instance.new("Folder") + shopFolder.Name = "Shop" + shopFolder.Parent = worldFolder + + local shopPos = GameConfig.ShopPosition + local sSize = GameConfig.ShopSize + + -- Shop floor + local floor = Instance.new("Part") + floor.Name = "ShopFloor" + floor.Size = sSize + floor.Position = shopPos + floor.Anchored = true + floor.Material = Enum.Material.WoodPlanks + floor.BrickColor = BrickColor.new("Burlap") + floor.TopSurface = Enum.SurfaceType.Smooth + floor.Parent = shopFolder + + -- Shop walls + local wallHeight = 12 + local wallThickness = 1 + + local walls = { + {name = "BackWall", pos = Vector3.new(shopPos.X, shopPos.Y + wallHeight/2, shopPos.Z - sSize.Z/2), size = Vector3.new(sSize.X, wallHeight, wallThickness)}, + {name = "LeftWall", pos = Vector3.new(shopPos.X - sSize.X/2, shopPos.Y + wallHeight/2, shopPos.Z), size = Vector3.new(wallThickness, wallHeight, sSize.Z)}, + {name = "RightWall", pos = Vector3.new(shopPos.X + sSize.X/2, shopPos.Y + wallHeight/2, shopPos.Z), size = Vector3.new(wallThickness, wallHeight, sSize.Z)}, + } + + for _, wallData in ipairs(walls) do + local wall = Instance.new("Part") + wall.Name = wallData.name + wall.Size = wallData.size + wall.Position = wallData.pos + wall.Anchored = true + wall.Material = Enum.Material.Brick + wall.BrickColor = BrickColor.new("Reddish brown") + wall.Parent = shopFolder + end + + -- Roof + local roof = Instance.new("Part") + roof.Name = "ShopRoof" + roof.Size = Vector3.new(sSize.X + 4, 1, sSize.Z + 4) + roof.Position = Vector3.new(shopPos.X, shopPos.Y + wallHeight, shopPos.Z) + roof.Anchored = true + roof.Material = Enum.Material.Slate + roof.BrickColor = BrickColor.new("Dark stone grey") + roof.Parent = shopFolder + + -- Shop counter + local counter = Instance.new("Part") + counter.Name = "ShopCounter" + counter.Size = Vector3.new(10, 3, 3) + counter.Position = shopPos + Vector3.new(0, 1.5, -5) + counter.Anchored = true + counter.Material = Enum.Material.WoodPlanks + counter.BrickColor = BrickColor.new("Pine Cone") + CollectionService:AddTag(counter, "ShopCounter") + counter.Parent = shopFolder + + -- Shop sign + local signPart = Instance.new("Part") + signPart.Name = "ShopSign" + signPart.Size = Vector3.new(10, 3, 0.5) + signPart.Position = Vector3.new(shopPos.X, shopPos.Y + wallHeight + 2.5, shopPos.Z + sSize.Z/2) + signPart.Anchored = true + signPart.Material = Enum.Material.Wood + signPart.BrickColor = BrickColor.new("Bright blue") + signPart.Parent = shopFolder + + local signGui = Instance.new("SurfaceGui") + signGui.Face = Enum.NormalId.Front + signGui.Parent = signPart + + local signLabel = Instance.new("TextLabel") + signLabel.Size = UDim2.new(1, 0, 1, 0) + signLabel.BackgroundTransparency = 1 + signLabel.Text = "SUE'S SUPPLY SHOP" + signLabel.TextColor3 = Color3.new(1, 1, 1) + signLabel.TextScaled = true + signLabel.Font = Enum.Font.GothamBold + signLabel.Parent = signGui + + print("Shop area created") +end + +-- Create roads connecting major areas +local function createRoads() + local roadFolder = Instance.new("Folder") + roadFolder.Name = "Roads" + roadFolder.Parent = worldFolder + + -- Main road from spawn toward market + local roads = { + {from = GameConfig.SpawnPosition, to = GameConfig.MarketPosition, width = 8}, + {from = GameConfig.MarketPosition, to = GameConfig.ShopPosition, width = 6}, + } + + for i, roadData in ipairs(roads) do + local midpoint = (roadData.from + roadData.to) / 2 + local direction = (roadData.to - roadData.from) + local length = direction.Magnitude + + local roadPart = Instance.new("Part") + roadPart.Name = "Road_" .. i + roadPart.Size = Vector3.new(roadData.width, 0.3, length) + roadPart.CFrame = CFrame.lookAt(midpoint, roadData.to) + Vector3.new(0, 0.2, 0) + roadPart.Anchored = true + roadPart.Material = Enum.Material.Cobblestone + roadPart.Color = Color3.fromRGB(120, 110, 100) + roadPart.TopSurface = Enum.SurfaceType.Smooth + roadPart.Parent = roadFolder + end + + print("Roads created") +end + +-- Wait for world folder to be ready then build structures +task.spawn(function() + task.wait(1) -- Brief delay to ensure BiomeGeneratorManager creates the folder + + createMarketArea() + createShopArea() + createRoads() + + print("=== Map Structures Complete ===") +end) diff --git a/src/ServerScriptService/MarketManager.server.lua b/src/ServerScriptService/MarketManager.server.lua new file mode 100644 index 0000000000000000000000000000000000000000..cb33471ac519ca5e9748132d5d4863d9f3bcb85f --- /dev/null +++ b/src/ServerScriptService/MarketManager.server.lua @@ -0,0 +1,148 @@ +-- src/ServerScriptService/MarketManager.server.lua + +local CollectionService = game:GetService("CollectionService") +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local Players = game:GetService("Players") + +local EconomyConfig = require(ReplicatedStorage:WaitForChild("Shared"):WaitForChild("EconomyConfig")) +local NotificationEvent = ReplicatedStorage:WaitForChild("Events"):WaitForChild("NotificationEvent") +local MarketUpdateEvent = ReplicatedStorage:WaitForChild("Events"):WaitForChild("MarketUpdateEvent") +local SoundEvent = ReplicatedStorage:WaitForChild("Events"):WaitForChild("SoundEvent") + +-- Global current market multipliers +local currentMarketRates = {} + +-- Initialize base rates for all wood types +for woodType, _ in pairs(EconomyConfig.WoodBaseValues) do + currentMarketRates[woodType] = 1.0 +end + +local function updateMarketRates() + for woodType, _ in pairs(currentMarketRates) do + local randShift = (math.random(-10, 10) / 100) + local newRate = currentMarketRates[woodType] + randShift + + currentMarketRates[woodType] = math.clamp( + newRate, + EconomyConfig.MarketFluctuations.MaxDecrease, + EconomyConfig.MarketFluctuations.MaxIncrease + ) + end + + -- Broadcast updated rates to all clients + for _, player in ipairs(Players:GetPlayers()) do + MarketUpdateEvent:FireClient(player, currentMarketRates) + end +end + +-- Start Market Cycle +task.spawn(function() + if EconomyConfig.MarketFluctuations.Enabled then + while true do + task.wait(EconomyConfig.MarketFluctuations.UpdateInterval) + updateMarketRates() + end + end +end) + +local function getPartVolume(part) + return part.Size.X * part.Size.Y * part.Size.Z +end + +local function calculateWoodValue(logPart) + local treeType = logPart:GetAttribute("TreeType") + local state = logPart:GetAttribute("ProcessState") or "Raw" + + if not treeType or not EconomyConfig.WoodBaseValues[treeType] then return 0 end + + local baseValue = EconomyConfig.WoodBaseValues[treeType] + local marketMult = currentMarketRates[treeType] or 1.0 + local processMult = EconomyConfig.ProcessingMultipliers[state] or 1.0 + local degradedMult = logPart:GetAttribute("DegradedMult") or 1.0 + local volume = getPartVolume(logPart) + + -- Formula: Volume * BaseValue * ProcessingState * MarketDemand * Degradation + return math.floor(volume * baseValue * processMult * marketMult * degradedMult) +end + +local function rewardPlayer(player, amount) + if not player then return end + local leaderstats = player:FindFirstChild("leaderstats") + if leaderstats then + local cash = leaderstats:FindFirstChild("Cash") + if cash then + cash.Value = cash.Value + amount + end + end + + -- Track total earned + if _G.IncrementStat then + _G.IncrementStat(player, "TotalEarned", amount) + end + + -- Progress quests + if _G.ProgressQuest then + _G.ProgressQuest(player, "Sell", 1) + _G.ProgressQuest(player, "EarnCash", amount) + end + + -- Check achievements + if _G.CheckAchievements then + task.defer(function() _G.CheckAchievements(player) end) + end +end + +local function bindDropoffZone(zonePart) + local debounce = {} + + zonePart.Touched:Connect(function(hit) + if CollectionService:HasTag(hit, "TreeSegment") then + if not debounce[hit] then + debounce[hit] = true + + local woodValue = calculateWoodValue(hit) + local ownerId = hit:GetAttribute("OwnerId") + local playerToReward = nil + + if ownerId then + playerToReward = Players:GetPlayerByUserId(ownerId) + else + pcall(function() + playerToReward = hit:GetNetworkOwner() + end) + end + + if playerToReward and woodValue > 0 then + rewardPlayer(playerToReward, woodValue) + + -- Notification with value + local treeType = hit:GetAttribute("TreeType") or "Unknown" + local state = hit:GetAttribute("ProcessState") or "Raw" + NotificationEvent:FireClient(playerToReward, "Sale", "Sold " .. state .. " " .. treeType .. " for $" .. tostring(woodValue)) + SoundEvent:FireClient(playerToReward, "CashRegister", nil) + end + + hit:Destroy() + end + end + end) +end + +CollectionService:GetInstanceAddedSignal("MarketDropoff"):Connect(bindDropoffZone) +for _, zone in pairs(CollectionService:GetTagged("MarketDropoff")) do + bindDropoffZone(zone) +end + +-- Expose market rates for billboard GUI +_G.GetMarketRates = function() + return currentMarketRates +end + +-- Send initial market rates to new players +Players.PlayerAdded:Connect(function(player) + task.delay(5, function() + if player.Parent then + MarketUpdateEvent:FireClient(player, currentMarketRates) + end + end) +end) diff --git a/src/ServerScriptService/MutationManager.server.lua b/src/ServerScriptService/MutationManager.server.lua new file mode 100644 index 0000000000000000000000000000000000000000..a569b04727b4920b9fc38829878d6f88e445dce0 --- /dev/null +++ b/src/ServerScriptService/MutationManager.server.lua @@ -0,0 +1,56 @@ +-- src/ServerScriptService/MutationManager.server.lua + +local CollectionService = game:GetService("CollectionService") +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local MutationConfig = require(ReplicatedStorage:WaitForChild("Shared"):WaitForChild("MutationConfig")) + +local function applyMutation(treeModel, hazardType) + local config = MutationConfig.Hazards[hazardType] + if not config then return end + + -- In Lumber Tycoon style, trees are models containing segments + treeModel:SetAttribute("Mutated", hazardType) + + for _, part in pairs(treeModel:GetDescendants()) do + if part:IsA("BasePart") and CollectionService:HasTag(part, "TreeSegment") then + part:SetAttribute("TreeType", config.ModifiedType) + part.Material = config.Material + part.Color = config.Color + + -- Optional: Add a ParticleEmitter + if hazardType == "Toxic" then + local particles = Instance.new("ParticleEmitter") + particles.Color = ColorSequence.new(config.Color) + particles.Size = NumberSequence.new(0.5, 0) + particles.Rate = 5 + particles.Parent = part + end + end + end +end + +-- Scan all hazard zones at startup (and potentially periodically) +local function scanForMutations() + for _, hazardZone in pairs(CollectionService:GetTagged("HazardZone")) do + local hazardType = hazardZone:GetAttribute("HazardType") + local config = MutationConfig.Hazards[hazardType] + + if config then + -- Find all trees near the hazard + for _, tree in pairs(CollectionService:GetTagged("TreeModel")) do + if not tree:GetAttribute("Mutated") then + local distance = (tree.PrimaryPart.Position - hazardZone.Position).Magnitude + if distance <= config.Radius then + if math.random() <= config.MutationChance then + applyMutation(tree, hazardType) + end + end + end + end + end + end +end + +-- Run scan after a brief delay to ensure map has loaded +task.delay(5, scanForMutations) diff --git a/src/ServerScriptService/NPCManager.server.lua b/src/ServerScriptService/NPCManager.server.lua new file mode 100644 index 0000000000000000000000000000000000000000..80effb30b101f2383973a499b6c2d27d88cd9dad --- /dev/null +++ b/src/ServerScriptService/NPCManager.server.lua @@ -0,0 +1,152 @@ +-- src/ServerScriptService/NPCManager.server.lua + +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local CollectionService = game:GetService("CollectionService") + +local NPCConfig = require(ReplicatedStorage:WaitForChild("Shared"):WaitForChild("NPCConfig")) +local DialogueEvent = ReplicatedStorage:WaitForChild("Events"):WaitForChild("DialogueEvent") +local ShopEvent = ReplicatedStorage:WaitForChild("Events"):WaitForChild("ShopEvent") + +local npcFolder = Instance.new("Folder") +npcFolder.Name = "NPCs" +npcFolder.Parent = workspace + +local function createNPCModel(npcId, npcData) + local model = Instance.new("Model") + model.Name = npcId + + -- Create humanoid R6-style body parts + local torso = Instance.new("Part") + torso.Name = "HumanoidRootPart" + torso.Size = Vector3.new(2, 2, 1) + torso.Position = npcData.Position + torso.Anchored = true + torso.Color = npcData.BodyColors.TorsoColor + torso.Material = Enum.Material.SmoothPlastic + torso.Parent = model + + local head = Instance.new("Part") + head.Name = "Head" + head.Shape = Enum.PartType.Ball + head.Size = Vector3.new(1.6, 1.6, 1.6) + head.Position = npcData.Position + Vector3.new(0, 1.8, 0) + head.Anchored = true + head.Color = npcData.BodyColors.HeadColor + head.Material = Enum.Material.SmoothPlastic + head.Parent = model + + -- Face decal + local face = Instance.new("Decal") + face.Face = Enum.NormalId.Front + face.Texture = "rbxasset://textures/face.png" + face.Parent = head + + -- Left Arm + local leftArm = Instance.new("Part") + leftArm.Name = "LeftArm" + leftArm.Size = Vector3.new(1, 2, 1) + leftArm.Position = npcData.Position + Vector3.new(-1.5, 0, 0) + leftArm.Anchored = true + leftArm.Color = npcData.BodyColors.LeftArmColor + leftArm.Material = Enum.Material.SmoothPlastic + leftArm.Parent = model + + -- Right Arm + local rightArm = Instance.new("Part") + rightArm.Name = "RightArm" + rightArm.Size = Vector3.new(1, 2, 1) + rightArm.Position = npcData.Position + Vector3.new(1.5, 0, 0) + rightArm.Anchored = true + rightArm.Color = npcData.BodyColors.RightArmColor + rightArm.Material = Enum.Material.SmoothPlastic + rightArm.Parent = model + + -- Left Leg + local leftLeg = Instance.new("Part") + leftLeg.Name = "LeftLeg" + leftLeg.Size = Vector3.new(1, 2, 1) + leftLeg.Position = npcData.Position + Vector3.new(-0.5, -2, 0) + leftLeg.Anchored = true + leftLeg.Color = npcData.BodyColors.LeftLegColor + leftLeg.Material = Enum.Material.SmoothPlastic + leftLeg.Parent = model + + -- Right Leg + local rightLeg = Instance.new("Part") + rightLeg.Name = "RightLeg" + rightLeg.Size = Vector3.new(1, 2, 1) + rightLeg.Position = npcData.Position + Vector3.new(0.5, -2, 0) + rightLeg.Anchored = true + rightLeg.Color = npcData.BodyColors.RightLegColor + rightLeg.Material = Enum.Material.SmoothPlastic + rightLeg.Parent = model + + model.PrimaryPart = torso + + -- Nameplate + local billboard = Instance.new("BillboardGui") + billboard.Size = UDim2.new(0, 200, 0, 50) + billboard.StudsOffset = Vector3.new(0, 3.5, 0) + billboard.AlwaysOnTop = true + billboard.Parent = head + + local nameLabel = Instance.new("TextLabel") + nameLabel.Size = UDim2.new(1, 0, 1, 0) + nameLabel.BackgroundTransparency = 1 + nameLabel.Text = npcData.DisplayName + nameLabel.TextColor3 = Color3.new(1, 1, 0.7) + nameLabel.TextScaled = true + nameLabel.Font = Enum.Font.GothamBold + nameLabel.TextStrokeTransparency = 0.5 + nameLabel.Parent = billboard + + -- ProximityPrompt for interaction + local prompt = Instance.new("ProximityPrompt") + prompt.ActionText = "Talk" + prompt.ObjectText = npcData.DisplayName + prompt.MaxActivationDistance = 10 + prompt.HoldDuration = 0 + prompt.Parent = torso + + -- Track dialogue state per player + local dialogueIndex = {} -- [userId] = current line index + + prompt.Triggered:Connect(function(player) + if not dialogueIndex[player.UserId] then + dialogueIndex[player.UserId] = 1 + end + + local lineIndex = dialogueIndex[player.UserId] + local dialogue = npcData.Dialogue + + if lineIndex <= #dialogue then + DialogueEvent:FireClient(player, npcData.DisplayName, dialogue[lineIndex], lineIndex < #dialogue) + dialogueIndex[player.UserId] = lineIndex + 1 + else + -- Reset dialogue + dialogueIndex[player.UserId] = 1 + DialogueEvent:FireClient(player, npcData.DisplayName, dialogue[1], #dialogue > 1) + + -- If shopkeeper, open shop UI + if npcData.Role == "Shopkeeper" then + ShopEvent:FireClient(player, "OpenShop") + end + end + end) + + CollectionService:AddTag(model, "NPC") + model.Parent = npcFolder + + return model +end + +-- Spawn all NPCs +task.spawn(function() + task.wait(2) -- Wait for world structures + + for npcId, npcData in pairs(NPCConfig.NPCs) do + createNPCModel(npcId, npcData) + end + + print("All NPCs spawned") +end) diff --git a/src/ServerScriptService/PlayerSetupManager.server.lua b/src/ServerScriptService/PlayerSetupManager.server.lua new file mode 100644 index 0000000000000000000000000000000000000000..8a9743448a703e663b3e31ec5adaed24c40b1883 --- /dev/null +++ b/src/ServerScriptService/PlayerSetupManager.server.lua @@ -0,0 +1,136 @@ +-- src/ServerScriptService/PlayerSetupManager.server.lua + +local Players = game:GetService("Players") +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local GameConfig = require(ReplicatedStorage:WaitForChild("Shared"):WaitForChild("GameConfig")) +local EconomyConfig = require(ReplicatedStorage:WaitForChild("Shared"):WaitForChild("EconomyConfig")) +local ShopConfig = require(ReplicatedStorage:WaitForChild("Shared"):WaitForChild("ShopConfig")) +local NotificationEvent = ReplicatedStorage:WaitForChild("Events"):WaitForChild("NotificationEvent") + +-- Player data cache for this session +local playerData = {} + +local function createLeaderstats(player) + local leaderstats = Instance.new("Folder") + leaderstats.Name = "leaderstats" + leaderstats.Parent = player + + local cash = Instance.new("IntValue") + cash.Name = GameConfig.Stats.Cash + cash.Value = EconomyConfig.StarterCash + cash.Parent = leaderstats + + local woodChopped = Instance.new("IntValue") + woodChopped.Name = GameConfig.Stats.WoodChopped + woodChopped.Value = 0 + woodChopped.Parent = leaderstats +end + +local function initializeInventory(player) + -- Start with empty inventory -- players must craft their tools + local defaultInventory = { + Tools = {}, + Machines = {}, + Vehicles = {}, + Automation = {}, + } + + -- Store in player data cache + playerData[player.UserId] = { + Inventory = defaultInventory, + EquippedAxe = "", -- empty = fists + QuestProgress = {}, + Achievements = {}, + BiomesVisited = {}, + Resources = { Wood = 0, Stone = 0, GoldOre = 0, Diamond = 0 }, + TotalEarned = 0, + TotalWoodSold = 0, + TotalBuilt = 0, + } + + -- Empty string means fists -- client will default to fist stats + player:SetAttribute("EquippedAxe", "") +end + +local function onPlayerAdded(player) + createLeaderstats(player) + initializeInventory(player) + + -- Wait for character to load then set attributes + player.CharacterAdded:Connect(function(character) + character:SetAttribute("EquippedAxe", playerData[player.UserId].EquippedAxe) + end) + + -- Welcome notification + task.delay(3, function() + if player.Parent then + NotificationEvent:FireClient(player, "Welcome", "Welcome to Timberbound Expeditions! Punch trees to gather wood, then press C to craft your first axe!") + end + end) +end + +local function onPlayerRemoving(player) + playerData[player.UserId] = nil +end + +-- Expose playerData globally for other server scripts +_G.GetPlayerData = function(player) + return playerData[player.UserId] +end + +_G.SetPlayerData = function(player, key, value) + if playerData[player.UserId] then + playerData[player.UserId][key] = value + end +end + +_G.AddToInventory = function(player, category, itemId) + local data = playerData[player.UserId] + if data and data.Inventory[category] then + table.insert(data.Inventory[category], itemId) + return true + end + return false +end + +_G.HasInInventory = function(player, category, itemId) + local data = playerData[player.UserId] + if data and data.Inventory[category] then + for _, id in ipairs(data.Inventory[category]) do + if id == itemId then return true end + end + end + return false +end + +_G.IncrementStat = function(player, statName, amount) + local data = playerData[player.UserId] + if not data then return end + + if statName == "WoodChopped" then + local leaderstats = player:FindFirstChild("leaderstats") + if leaderstats then + local stat = leaderstats:FindFirstChild(statName) + if stat then + stat.Value = stat.Value + amount + end + end + elseif statName == "TotalEarned" then + data.TotalEarned = (data.TotalEarned or 0) + amount + elseif statName == "TotalWoodSold" then + data.TotalWoodSold = (data.TotalWoodSold or 0) + amount + elseif statName == "TotalBuilt" then + data.TotalBuilt = (data.TotalBuilt or 0) + amount + end +end + +Players.PlayerAdded:Connect(onPlayerAdded) +Players.PlayerRemoving:Connect(onPlayerRemoving) + +-- Handle players who joined before this script loaded +for _, player in ipairs(Players:GetPlayers()) do + task.spawn(function() + onPlayerAdded(player) + end) +end diff --git a/src/ServerScriptService/PlotManager.server.lua b/src/ServerScriptService/PlotManager.server.lua new file mode 100644 index 0000000000000000000000000000000000000000..0a2853efa8b0f191f326fe2a180f5de39e389295 --- /dev/null +++ b/src/ServerScriptService/PlotManager.server.lua @@ -0,0 +1,73 @@ +-- src/ServerScriptService/PlotManager.server.lua + +local CollectionService = game:GetService("CollectionService") +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local Players = game:GetService("Players") + +local PlotConfig = require(ReplicatedStorage:WaitForChild("Shared"):WaitForChild("PlotConfig")) +local ClaimPlotEvent = ReplicatedStorage:WaitForChild("Events"):WaitForChild("ClaimPlot") + +-- Keeps track of which player owns which plot +local activePlots = {} -- [player.UserId] = plotPart + +local function claimPlot(player, targetPlot) + -- Validate requested plot + if not targetPlot or not targetPlot:IsA("BasePart") then return end + if not CollectionService:HasTag(targetPlot, "EmptyPlot") then return end + + -- Ensure player doesn't already own a plot + if activePlots[player.UserId] then + print(player.Name .. " already owns a plot.") + return + end + + -- Ensure plot isn't already claimed + for _, ownedPlot in pairs(activePlots) do + if ownedPlot == targetPlot then + print("Plot already claimed.") + return + end + end + + -- Grant Ownership + activePlots[player.UserId] = targetPlot + + -- Change Plot Visuals + targetPlot.BrickColor = BrickColor.new("Bright green") + CollectionService:RemoveTag(targetPlot, "EmptyPlot") + CollectionService:AddTag(targetPlot, "ClaimedPlot") + + targetPlot:SetAttribute("OwnerId", player.UserId) + + print(player.Name .. " successfully claimed a plot.") + + -- Load their Datastore Data onto this plot (Deferred to DatastoreManager) + _G.LoadPlayerData(player, targetPlot) +end + +-- Handle Claim Event +ClaimPlotEvent.OnServerEvent:Connect(claimPlot) + +-- Cleanup when player leaves +Players.PlayerRemoving:Connect(function(player) + local plot = activePlots[player.UserId] + if plot then + -- Save their data (Deferred to DatastoreManager) + _G.SavePlayerData(player, plot) + + -- Reset Plot + plot.BrickColor = BrickColor.new("Dark stone grey") + CollectionService:RemoveTag(plot, "ClaimedPlot") + CollectionService:AddTag(plot, "EmptyPlot") + plot:SetAttribute("OwnerId", nil) + + -- Delete all objects on the plot + for _, obj in pairs(plot:GetChildren()) do + if obj:IsA("Model") or obj:IsA("BasePart") then + obj:Destroy() + end + end + + activePlots[player.UserId] = nil + end +end) diff --git a/src/ServerScriptService/QuestManager.server.lua b/src/ServerScriptService/QuestManager.server.lua new file mode 100644 index 0000000000000000000000000000000000000000..7f91ff7153286d0caf6d8fb92655c3d81454228c --- /dev/null +++ b/src/ServerScriptService/QuestManager.server.lua @@ -0,0 +1,135 @@ +-- src/ServerScriptService/QuestManager.server.lua + +local Players = game:GetService("Players") +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local QuestConfig = require(ReplicatedStorage:WaitForChild("Shared"):WaitForChild("QuestConfig")) +local QuestUpdateEvent = ReplicatedStorage:WaitForChild("Events"):WaitForChild("QuestUpdateEvent") +local NotificationEvent = ReplicatedStorage:WaitForChild("Events"):WaitForChild("NotificationEvent") + +-- Player quest state: [userId] = { [questId] = { progress, completed } } +local playerQuests = {} + +local function initPlayerQuests(player) + playerQuests[player.UserId] = {} + + -- Assign starter quests + for _, questId in ipairs(QuestConfig.StarterQuests) do + playerQuests[player.UserId][questId] = { + progress = 0, + completed = false, + } + end + + -- Send initial quest data to client + task.delay(4, function() + if player.Parent then + QuestUpdateEvent:FireClient(player, "Init", playerQuests[player.UserId]) + end + end) +end + +local function isQuestActive(player, questId) + local quests = playerQuests[player.UserId] + if not quests then return false end + local quest = quests[questId] + if not quest then return false end + return not quest.completed +end + +local function progressQuest(player, questType, amount) + local quests = playerQuests[player.UserId] + if not quests then return end + + for questId, questState in pairs(quests) do + if questState.completed then continue end + + local questDef = QuestConfig.Quests[questId] + if not questDef then continue end + + if questDef.Type == questType then + questState.progress = questState.progress + amount + + if questState.progress >= questDef.Target then + questState.progress = questDef.Target + questState.completed = true + + -- Grant rewards + if questDef.Reward.Cash then + local leaderstats = player:FindFirstChild("leaderstats") + if leaderstats then + local cash = leaderstats:FindFirstChild("Cash") + if cash then + cash.Value = cash.Value + questDef.Reward.Cash + end + end + end + + NotificationEvent:FireClient(player, "Quest Complete", questDef.Title .. " completed! +" .. tostring(questDef.Reward.Cash or 0) .. " Cash") + + -- Unlock follow-up quests + unlockNextQuests(player, questId) + end + + -- Update client + QuestUpdateEvent:FireClient(player, "Update", { + questId = questId, + progress = questState.progress, + target = questDef.Target, + completed = questState.completed, + }) + end + end +end + +function unlockNextQuests(player, completedQuestId) + -- Simple linear progression: unlock quests based on what was completed + local unlockMap = { + ChopFirstTree = {"Chop10Trees"}, + Chop10Trees = {"Chop50Trees"}, + Chop50Trees = {"Chop200Trees"}, + SellFirstWood = {"Earn5000"}, + Earn5000 = {"Earn50000"}, + BuildFirstStructure = {"Build10Structures"}, + BuyFirstItem = {"ProcessFirstPlank"}, + ProcessFirstPlank = {"ExploreAllBiomes"}, + } + + local nextQuests = unlockMap[completedQuestId] + if nextQuests then + for _, nextId in ipairs(nextQuests) do + if not playerQuests[player.UserId][nextId] then + playerQuests[player.UserId][nextId] = { + progress = 0, + completed = false, + } + + local questDef = QuestConfig.Quests[nextId] + if questDef then + NotificationEvent:FireClient(player, "New Quest", "New quest unlocked: " .. questDef.Title) + end + end + end + end +end + +-- Expose globally for other managers to trigger quest progress +_G.ProgressQuest = function(player, questType, amount) + progressQuest(player, questType, amount or 1) +end + +_G.GetPlayerQuests = function(player) + return playerQuests[player.UserId] +end + +Players.PlayerAdded:Connect(initPlayerQuests) +Players.PlayerRemoving:Connect(function(player) + playerQuests[player.UserId] = nil +end) + +-- Handle players already in game +for _, player in ipairs(Players:GetPlayers()) do + task.spawn(function() + initPlayerQuests(player) + end) +end diff --git a/src/ServerScriptService/RespawnManager.server.lua b/src/ServerScriptService/RespawnManager.server.lua new file mode 100644 index 0000000000000000000000000000000000000000..1328bd5682070bbcde18c2b2f6d345235daf27de --- /dev/null +++ b/src/ServerScriptService/RespawnManager.server.lua @@ -0,0 +1,93 @@ +-- src/ServerScriptService/RespawnManager.server.lua + +local CollectionService = game:GetService("CollectionService") +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local BiomeConfig = require(ReplicatedStorage:WaitForChild("Shared"):WaitForChild("BiomeConfig")) + +-- Respawn delay in seconds +local RESPAWN_DELAY = 120 + +-- Track destroyed tree positions for respawning +local respawnQueue = {} -- { {treeType, position, delay} } + +-- Monitor tree destruction +local function onTreeSegmentRemoved(segment) + -- When a segment is destroyed (chopped to nothing), track it for respawn + -- We only respawn if the segment was the bottom piece (or original) + local treeType = segment:GetAttribute("TreeType") + if not treeType then return end + + local position = segment.Position + -- Only queue if near ground level (don't respawn floating segments) + if position.Y < 20 then + table.insert(respawnQueue, { + treeType = treeType, + position = Vector3.new(position.X, 0, position.Z), + timer = RESPAWN_DELAY, + }) + end +end + +-- Bind to existing and new tree segments +CollectionService:GetInstanceRemovedSignal("TreeSegment"):Connect(function(segment) + -- We need to capture data before it's fully gone + task.defer(function() + -- The segment data was already captured via the signal + end) +end) + +-- Alternative: listen for Destroying event on tagged parts +local function bindSegmentTracking(segment) + segment.Destroying:Connect(function() + onTreeSegmentRemoved(segment) + end) +end + +CollectionService:GetInstanceAddedSignal("TreeSegment"):Connect(bindSegmentTracking) +for _, segment in pairs(CollectionService:GetTagged("TreeSegment")) do + bindSegmentTracking(segment) +end + +-- Process respawn queue +task.spawn(function() + while not _G.TreeSpawningComplete do + task.wait(1) + end + + while true do + task.wait(5) + + local toRemove = {} + + for i, entry in ipairs(respawnQueue) do + entry.timer = entry.timer - 5 + + if entry.timer <= 0 then + -- Respawn tree + if _G.CreateTree then + local rayResult = workspace:Raycast( + Vector3.new(entry.position.X, 200, entry.position.Z), + Vector3.new(0, -400, 0) + ) + + local groundY = 0 + if rayResult then + groundY = rayResult.Position.Y + end + + _G.CreateTree(entry.treeType, Vector3.new(entry.position.X, groundY, entry.position.Z)) + end + + table.insert(toRemove, i) + end + end + + -- Remove processed entries (reverse order to preserve indices) + for i = #toRemove, 1, -1 do + table.remove(respawnQueue, toRemove[i]) + end + end +end) + +print("Respawn Manager initialized") diff --git a/src/ServerScriptService/SawmillManager.server.lua b/src/ServerScriptService/SawmillManager.server.lua new file mode 100644 index 0000000000000000000000000000000000000000..eeeae97df9070ed888a76174eb22f886d9352eb4 --- /dev/null +++ b/src/ServerScriptService/SawmillManager.server.lua @@ -0,0 +1,75 @@ +-- src/ServerScriptService/SawmillManager.server.lua + +local CollectionService = game:GetService("CollectionService") + +-- A generic processing function that converts a log into a plank +local function processLog(logPart, machineType) + local state = logPart:GetAttribute("ProcessState") or "Raw" + local treeType = logPart:GetAttribute("TreeType") + + if machineType == "Stripper" and state == "Raw" then + -- Remove bark (Visual change: change material or color) + logPart:SetAttribute("ProcessState", "Stripped") + + -- In a real game, you might tween the size down slightly to represent bark loss + local newSize = logPart.Size * 0.9 + logPart.Size = newSize + logPart.Material = Enum.Material.Wood + + elseif machineType == "Sawmill" and (state == "Raw" or state == "Stripped") then + -- Convert to a clean plank + logPart:SetAttribute("ProcessState", "Plank") + + -- Change shape + local volume = logPart.Size.X * logPart.Size.Y * logPart.Size.Z + -- Make it rectangular and flat (like a plank) while preserving some volume proportionality + local plankThickness = 0.5 + local length = logPart.Size.Y + local width = (volume / length) / plankThickness -- maintain roughly same mass/volume for physics sanity, minus some waste + + -- Prevent extremely thin or extremely wide planks + width = math.clamp(width, 1, 4) + + logPart.Size = Vector3.new(width, length, plankThickness) + logPart.Material = Enum.Material.WoodPlanks + logPart.BrickColor = BrickColor.new("Burlap") -- Typical plank color, or utilize TreeType specific clean color + + -- Update collision and physics bounds instantly + logPart.Anchored = false + end +end + +-- Monitor for touch events on Sawmill Triggers +local function bindMachine(machineModel) + local triggerPart = machineModel:FindFirstChild("ProcessTrigger") + if not triggerPart then return end + + local machineType = machineModel:GetAttribute("MachineType") or "Sawmill" + + local debounce = {} + + triggerPart.Touched:Connect(function(hit) + -- Ensure it's a tree segment + if CollectionService:HasTag(hit, "TreeSegment") then + -- Prevent rapid firing + if not debounce[hit] then + debounce[hit] = true + + -- Process it + processLog(hit, machineType) + + -- Cleanup debounce + task.delay(1, function() + debounce[hit] = nil + end) + end + end + end) +end + +-- Initialize existing and future machines +CollectionService:GetInstanceAddedSignal("ProcessingMachine"):Connect(bindMachine) + +for _, machine in pairs(CollectionService:GetTagged("ProcessingMachine")) do + bindMachine(machine) +end diff --git a/src/ServerScriptService/ShopManager.server.lua b/src/ServerScriptService/ShopManager.server.lua new file mode 100644 index 0000000000000000000000000000000000000000..be1670003e3380659a1d68ab8da34987ffe7c3dd --- /dev/null +++ b/src/ServerScriptService/ShopManager.server.lua @@ -0,0 +1,161 @@ +-- src/ServerScriptService/ShopManager.server.lua + +local CollectionService = game:GetService("CollectionService") +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local Players = game:GetService("Players") + +local ShopConfig = require(ReplicatedStorage:WaitForChild("Shared"):WaitForChild("ShopConfig")) +local PurchaseEvent = ReplicatedStorage:WaitForChild("Events"):WaitForChild("PurchaseEvent") +local ShopEvent = ReplicatedStorage:WaitForChild("Events"):WaitForChild("ShopEvent") +local NotificationEvent = ReplicatedStorage:WaitForChild("Events"):WaitForChild("NotificationEvent") +local SoundEvent = ReplicatedStorage:WaitForChild("Events"):WaitForChild("SoundEvent") + +local counters = {} + +local function getPlayerCash(player) + local leaderstats = player:FindFirstChild("leaderstats") + if leaderstats then + local cash = leaderstats:FindFirstChild("Cash") + if cash then + return cash.Value, cash + end + end + return 0, nil +end + +-- Direct purchase handler (from GUI) +local function handleDirectPurchase(player, itemId) + if not itemId or type(itemId) ~= "string" then return end + if not ShopConfig.Items[itemId] then return end + + -- Anti-cheat + if _G.IsRateLimited and _G.IsRateLimited(player, "Purchase") then return end + + local item = ShopConfig.Items[itemId] + local currentCash, cashObj = getPlayerCash(player) + + if currentCash < item.Price then + NotificationEvent:FireClient(player, "Error", "Not enough cash! Need $" .. tostring(item.Price)) + SoundEvent:FireClient(player, "PurchaseFail", nil) + return + end + + -- Check if already owned (for tools) + if item.Type == "Tool" and _G.HasInInventory and _G.HasInInventory(player, "Tools", itemId) then + NotificationEvent:FireClient(player, "Info", "You already own this tool!") + return + end + + -- Deduct cash + cashObj.Value = cashObj.Value - item.Price + + -- Add to inventory + local categoryMap = { + Tool = "Tools", + Machine = "Machines", + Vehicle = "Vehicles", + Automation = "Automation", + Structure = "Machines", + Utility = "Machines", + Component = "Machines", + } + + local category = categoryMap[item.Type] or "Machines" + if _G.AddToInventory then + _G.AddToInventory(player, category, itemId) + end + + -- Progress quests + if _G.ProgressQuest then + _G.ProgressQuest(player, "Purchase", 1) + end + + NotificationEvent:FireClient(player, "Purchase", "Purchased " .. item.Name .. " for $" .. tostring(item.Price)) + SoundEvent:FireClient(player, "PurchaseSuccess", nil) + + -- Send updated inventory to client + local data = _G.GetPlayerData and _G.GetPlayerData(player) + if data then + ShopEvent:FireClient(player, "InventoryUpdate", data.Inventory) + end +end + +-- Counter-based purchase (legacy physical shop) +local function handleCounterPurchase(player, counterZone) + if not counters[counterZone] then return end + + local totalCost = 0 + local itemsToBuy = {} + + for boxedItem, _ in pairs(counters[counterZone]) do + if boxedItem.Parent then + local itemId = boxedItem:GetAttribute("ItemId") + if itemId and ShopConfig.Items[itemId] then + totalCost = totalCost + ShopConfig.Items[itemId].Price + table.insert(itemsToBuy, {item = boxedItem, id = itemId}) + end + else + counters[counterZone][boxedItem] = nil + end + end + + if #itemsToBuy == 0 then return end + + local currentCash, cashObj = getPlayerCash(player) + + if currentCash >= totalCost then + cashObj.Value = cashObj.Value - totalCost + + for _, buyData in ipairs(itemsToBuy) do + buyData.item:SetAttribute("Purchased", true) + buyData.item.BrickColor = BrickColor.new("Bright green") + counters[counterZone][buyData.item] = nil + + if _G.AddToInventory then + _G.AddToInventory(player, "Machines", buyData.id) + end + end + + NotificationEvent:FireClient(player, "Purchase", "Transaction complete! Total: $" .. tostring(totalCost)) + SoundEvent:FireClient(player, "PurchaseSuccess", nil) + + if _G.ProgressQuest then + _G.ProgressQuest(player, "Purchase", #itemsToBuy) + end + else + NotificationEvent:FireClient(player, "Error", "Not enough cash! Need $" .. tostring(totalCost)) + SoundEvent:FireClient(player, "PurchaseFail", nil) + end +end + +local function bindCounterZone(zonePart) + counters[zonePart] = {} + + zonePart.Touched:Connect(function(hit) + if CollectionService:HasTag(hit, "BoxedItem") and not hit:GetAttribute("Purchased") then + counters[zonePart][hit] = true + end + end) + + zonePart.TouchEnded:Connect(function(hit) + if counters[zonePart][hit] then + counters[zonePart][hit] = nil + end + end) +end + +CollectionService:GetInstanceAddedSignal("ShopCounter"):Connect(bindCounterZone) +for _, zone in pairs(CollectionService:GetTagged("ShopCounter")) do + bindCounterZone(zone) +end + +-- Handle both types of purchase events +PurchaseEvent.OnServerEvent:Connect(function(player, target) + if type(target) == "string" then + -- Direct purchase by item ID (from GUI) + handleDirectPurchase(player, target) + elseif typeof(target) == "Instance" and CollectionService:HasTag(target, "ShopCounter") then + -- Counter-based purchase + handleCounterPurchase(player, target) + end +end) diff --git a/src/ServerScriptService/SoundscapeManager.server.lua b/src/ServerScriptService/SoundscapeManager.server.lua new file mode 100644 index 0000000000000000000000000000000000000000..dfbe2b269202644ae23dd7e2f0b3e8e71514fa13 --- /dev/null +++ b/src/ServerScriptService/SoundscapeManager.server.lua @@ -0,0 +1,51 @@ +-- src/ServerScriptService/SoundscapeManager.server.lua + +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local SoundService = game:GetService("SoundService") + +local SoundConfig = require(ReplicatedStorage:WaitForChild("Shared"):WaitForChild("SoundConfig")) +local SoundEvent = ReplicatedStorage:WaitForChild("Events"):WaitForChild("SoundEvent") + +-- Create ambient sound groups +local ambientGroup = Instance.new("SoundGroup") +ambientGroup.Name = "Ambient" +ambientGroup.Volume = SoundConfig.AmbientVolume +ambientGroup.Parent = SoundService + +local sfxGroup = Instance.new("SoundGroup") +sfxGroup.Name = "SFX" +sfxGroup.Volume = SoundConfig.SFXVolume +sfxGroup.Parent = SoundService + +-- Create ambient forest sound (looping) +local forestSound = Instance.new("Sound") +forestSound.Name = "ForestAmbient" +forestSound.SoundId = SoundConfig.ForestAmbient +forestSound.Looped = true +forestSound.Volume = 0.3 +forestSound.SoundGroup = ambientGroup +forestSound.Parent = workspace + +-- Start ambient +forestSound:Play() + +-- Handle client SFX requests +SoundEvent.OnServerEvent:Connect(function(player, soundName, position) + -- Relay to all nearby clients + if SoundConfig[soundName] then + for _, otherPlayer in ipairs(game:GetService("Players"):GetPlayers()) do + if otherPlayer ~= player then + SoundEvent:FireClient(otherPlayer, soundName, position) + end + end + end +end) + +-- Expose for server-side sound triggering +_G.PlayWorldSound = function(soundName, position) + for _, player in ipairs(game:GetService("Players"):GetPlayers()) do + SoundEvent:FireClient(player, soundName, position) + end +end + +print("Soundscape Manager initialized") diff --git a/src/ServerScriptService/TreeManager.server.lua b/src/ServerScriptService/TreeManager.server.lua new file mode 100644 index 0000000000000000000000000000000000000000..b1cc0e7753648b71150d00fa50274a3274c995ff --- /dev/null +++ b/src/ServerScriptService/TreeManager.server.lua @@ -0,0 +1,238 @@ +-- src/ServerScriptService/TreeManager.server.lua + +local Players = game:GetService("Players") +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local CollectionService = game:GetService("CollectionService") + +local ChoppingConfig = require(ReplicatedStorage:WaitForChild("Shared"):WaitForChild("ChoppingConfig")) +local CraftingConfig = require(ReplicatedStorage:WaitForChild("Shared"):WaitForChild("CraftingConfig")) +local ChopEvent = ReplicatedStorage:WaitForChild("Events"):WaitForChild("ChopEvent") +local NotificationEvent = ReplicatedStorage:WaitForChild("Events"):WaitForChild("NotificationEvent") +local SoundEvent = ReplicatedStorage:WaitForChild("Events"):WaitForChild("SoundEvent") + +local function getEquippedAxeOrFist(player) + local axeType = player:GetAttribute("EquippedAxe") + + -- No axe equipped or "None" = use fists + if not axeType or axeType == "" or axeType == "None" then + return CraftingConfig.Fist, "Fist" + end + + local axeStats = ChoppingConfig.AxeTypes[axeType] + if axeStats then + return axeStats, axeType + end + + -- Fallback to fists + return CraftingConfig.Fist, "Fist" +end + +local function createChopParticles(position, color) + local attachment = Instance.new("Attachment") + attachment.WorldPosition = position + attachment.Parent = workspace.Terrain + + local emitter = Instance.new("ParticleEmitter") + emitter.Color = ColorSequence.new(color or Color3.fromRGB(180, 140, 100)) + emitter.Size = NumberSequence.new({ + NumberSequenceKeypoint.new(0, 0.5), + NumberSequenceKeypoint.new(1, 0), + }) + emitter.Lifetime = NumberRange.new(0.3, 0.8) + emitter.Rate = 0 + emitter.Speed = NumberRange.new(5, 15) + emitter.SpreadAngle = Vector2.new(180, 180) + emitter.Parent = attachment + + emitter:Emit(12) + + task.delay(1, function() + attachment:Destroy() + end) +end + +-- Drop a collectible wood resource near the tree +local function dropWoodResource(position, treeType, player) + if math.random() > CraftingConfig.WoodDropChance then return end + + local amount = math.random(CraftingConfig.WoodDropAmount[1], CraftingConfig.WoodDropAmount[2]) + + local drop = Instance.new("Part") + drop.Name = "WoodDrop" + drop.Size = Vector3.new(1, 1, 1) + drop.Position = position + Vector3.new(math.random(-3, 3), 2, math.random(-3, 3)) + drop.Shape = Enum.PartType.Block + drop.Material = Enum.Material.Wood + drop.Color = CraftingConfig.Resources.Wood.Color + drop.Anchored = false + drop.CanCollide = true + drop.CustomPhysicalProperties = PhysicalProperties.new(0.5, 0.3, 0.5) + drop:SetAttribute("ResourceType", "Wood") + drop:SetAttribute("Amount", amount) + drop:SetAttribute("OwnerId", player.UserId) + CollectionService:AddTag(drop, "ResourceDrop") + drop.Parent = workspace + + -- Billboard showing amount + local bb = Instance.new("BillboardGui") + bb.Size = UDim2.new(0, 40, 0, 20) + bb.StudsOffset = Vector3.new(0, 1.5, 0) + bb.AlwaysOnTop = true + bb.Parent = drop + + local label = Instance.new("TextLabel") + label.Size = UDim2.new(1, 0, 1, 0) + label.BackgroundTransparency = 1 + label.Text = "+" .. tostring(amount) .. " Wood" + label.TextColor3 = Color3.fromRGB(200, 160, 80) + label.TextScaled = true + label.Font = Enum.Font.GothamBold + label.TextStrokeTransparency = 0.3 + label.Parent = bb + + -- Auto-collect when player touches it + drop.Touched:Connect(function(hit) + local touchedPlayer = Players:GetPlayerFromCharacter(hit.Parent) + if touchedPlayer and touchedPlayer.UserId == player.UserId then + -- Add resources to player data + local data = _G.GetPlayerData(touchedPlayer) + if data then + if not data.Resources then + data.Resources = {} + end + data.Resources.Wood = (data.Resources.Wood or 0) + amount + NotificationEvent:FireClient(touchedPlayer, "Resource", "+" .. tostring(amount) .. " Wood (Total: " .. tostring(data.Resources.Wood) .. ")") + end + drop:Destroy() + end + end) + + -- Auto-despawn after 30 seconds + task.delay(30, function() + if drop.Parent then drop:Destroy() end + end) +end + +local function applyDamage(player, segment, hitPos, damage, weaponType) + if not CollectionService:HasTag(segment, "TreeSegment") then return end + + local treeType = segment:GetAttribute("TreeType") + if not treeType or not ChoppingConfig.TreeTypes[treeType] then return end + + local maxHealth = ChoppingConfig.TreeTypes[treeType].HealthPerSegment + local health = segment:GetAttribute("Health") + if not health then + health = maxHealth + end + + health = health - damage + segment:SetAttribute("Health", health) + + -- Visual feedback: chop particles + createChopParticles(hitPos, ChoppingConfig.TreeTypes[treeType].LogColor) + + -- Sound feedback (different for fist vs axe) + if weaponType == "Fist" then + SoundEvent:FireAllClients("AxeSwing", hitPos) -- lighter sound + else + SoundEvent:FireAllClients("AxeHitWood", hitPos) + end + + -- Drop wood resources on each hit + dropWoodResource(hitPos, treeType, player) + + if health <= 0 then + local segmentSizeY = segment.Size.Y + + -- Track stat + if _G.IncrementStat then + _G.IncrementStat(player, "WoodChopped", 1) + end + + -- Progress quests + if _G.ProgressQuest then + _G.ProgressQuest(player, "Chop", 1) + end + + -- Check achievements + if _G.CheckAchievements then + task.defer(function() _G.CheckAchievements(player) end) + end + + -- Set ownership on the log pieces + segment:SetAttribute("OwnerId", player.UserId) + + if segmentSizeY < ChoppingConfig.MinSegmentSizeY then + -- Drop bonus resources when segment destroyed + dropWoodResource(segment.Position, treeType, player) + segment:Destroy() + return + end + + local localHitPos = segment.CFrame:ToObjectSpace(CFrame.new(hitPos)).Position + local yPos = localHitPos.Y + local totalHeight = segment.Size.Y + local bottomHeight = (totalHeight / 2) + yPos + local topHeight = totalHeight - bottomHeight + + if bottomHeight < 0.2 or topHeight < 0.2 then + return + end + + local bottomPiece = segment:Clone() + local topPiece = segment:Clone() + + bottomPiece.Parent = segment.Parent + topPiece.Parent = segment.Parent + + bottomPiece.Size = Vector3.new(segment.Size.X, bottomHeight, segment.Size.Z) + topPiece.Size = Vector3.new(segment.Size.X, topHeight, segment.Size.Z) + + local offsetBottom = CFrame.new(0, -topHeight / 2, 0) + local offsetTop = CFrame.new(0, bottomHeight / 2, 0) + + bottomPiece.CFrame = segment.CFrame * offsetBottom + topPiece.CFrame = segment.CFrame * offsetTop + + bottomPiece:SetAttribute("Health", maxHealth) + topPiece:SetAttribute("Health", maxHealth) + bottomPiece:SetAttribute("OwnerId", player.UserId) + topPiece:SetAttribute("OwnerId", player.UserId) + + bottomPiece.Anchored = false + topPiece.Anchored = false + + bottomPiece.CustomPhysicalProperties = PhysicalProperties.new(ChoppingConfig.TreeTypes[treeType].Density, 0.3, 0.5) + topPiece.CustomPhysicalProperties = PhysicalProperties.new(ChoppingConfig.TreeTypes[treeType].Density, 0.3, 0.5) + + -- Add health bars to split pieces + if _G.AddHealthBar then + _G.AddHealthBar(bottomPiece) + _G.AddHealthBar(topPiece) + end + + -- Sound: tree fall + SoundEvent:FireAllClients("TreeFall", segment.Position) + + segment:Destroy() + end +end + +local function onChop(player, hitPart, hitPos) + -- Anti-cheat: rate limit + if _G.IsRateLimited and _G.IsRateLimited(player, "Chop") then return end + + local char = player.Character + if not char or not char:FindFirstChild("Head") then return end + + local axeStats, weaponType = getEquippedAxeOrFist(player) + + local distance = (char.Head.Position - hitPos).Magnitude + if distance > axeStats.Range + 2 then + return + end + + applyDamage(player, hitPart, hitPos, axeStats.Damage, weaponType) +end + +ChopEvent.OnServerEvent:Connect(onChop) diff --git a/src/ServerScriptService/TreeSpawnerManager.server.lua b/src/ServerScriptService/TreeSpawnerManager.server.lua new file mode 100644 index 0000000000000000000000000000000000000000..052eb361cba4c4c5366b73aa8fed70290de86ac5 --- /dev/null +++ b/src/ServerScriptService/TreeSpawnerManager.server.lua @@ -0,0 +1,279 @@ +-- src/ServerScriptService/TreeSpawnerManager.server.lua +-- Spawns trees on flat biome baseplates at Y=0 + +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local CollectionService = game:GetService("CollectionService") + +local BiomeConfig = require(ReplicatedStorage:WaitForChild("Shared"):WaitForChild("BiomeConfig")) +local TreeModelConfig = require(ReplicatedStorage:WaitForChild("Shared"):WaitForChild("TreeModelConfig")) +local ChoppingConfig = require(ReplicatedStorage:WaitForChild("Shared"):WaitForChild("ChoppingConfig")) + +local treesFolder = Instance.new("Folder") +treesFolder.Name = "Trees" +treesFolder.Parent = workspace + +local treePositions = {} + +local function randomInRange(min, max) + return min + math.random() * (max - min) +end + +local function isPositionValid(pos, minSpacing) + for _, existingPos in ipairs(treePositions) do + local dx = pos.X - existingPos.X + local dz = pos.Z - existingPos.Z + if (dx * dx + dz * dz) < (minSpacing * minSpacing) then + return false + end + end + return true +end + +-- Add a health bar to a tree segment +local function addHealthBar(segment) + local treeType = segment:GetAttribute("TreeType") + local config = ChoppingConfig.TreeTypes[treeType] + if not config then return end + + local billboard = Instance.new("BillboardGui") + billboard.Name = "HealthBarGui" + billboard.Size = UDim2.new(0, 60, 0, 8) + billboard.StudsOffset = Vector3.new(0, 1, 0) + billboard.AlwaysOnTop = false + billboard.MaxDistance = 30 + billboard.Parent = segment + + local bg = Instance.new("Frame") + bg.Name = "Background" + bg.Size = UDim2.new(1, 0, 1, 0) + bg.BackgroundColor3 = Color3.fromRGB(30, 30, 30) + bg.BorderSizePixel = 0 + bg.Parent = billboard + + local bgCorner = Instance.new("UICorner") + bgCorner.CornerRadius = UDim.new(0, 3) + bgCorner.Parent = bg + + local fill = Instance.new("Frame") + fill.Name = "Fill" + fill.Size = UDim2.new(1, 0, 1, 0) + fill.BackgroundColor3 = Color3.fromRGB(50, 200, 50) + fill.BorderSizePixel = 0 + fill.Parent = bg + + local fillCorner = Instance.new("UICorner") + fillCorner.CornerRadius = UDim.new(0, 3) + fillCorner.Parent = fill + + local label = Instance.new("TextLabel") + label.Name = "HealthText" + label.Size = UDim2.new(1, 0, 1, 0) + label.BackgroundTransparency = 1 + label.Text = tostring(config.HealthPerSegment) + label.TextColor3 = Color3.new(1, 1, 1) + label.TextScaled = true + label.Font = Enum.Font.GothamBold + label.TextStrokeTransparency = 0.5 + label.Parent = bg + + segment:GetAttributeChangedSignal("Health"):Connect(function() + local maxHP = config.HealthPerSegment + local currentHP = segment:GetAttribute("Health") or maxHP + local ratio = math.clamp(currentHP / maxHP, 0, 1) + + fill.Size = UDim2.new(ratio, 0, 1, 0) + label.Text = tostring(math.ceil(currentHP)) + + if ratio > 0.5 then + fill.BackgroundColor3 = Color3.fromRGB(math.floor(255 * (1 - ratio) * 2), 200, 50) + else + fill.BackgroundColor3 = Color3.fromRGB(255, math.floor(200 * ratio * 2), 50) + end + end) +end + +-- Build a tree model at a position (Y=0 is the ground) +local function createTreeModel(treeType, position) + local template = TreeModelConfig.Templates[treeType] + if not template then return nil end + + local config = ChoppingConfig.TreeTypes[treeType] + if not config then return nil end + + local model = Instance.new("Model") + model.Name = treeType .. "_Tree" + + local trunkDiameter = randomInRange(template.TrunkDiameter[1], template.TrunkDiameter[2]) + local totalHeight = randomInRange(template.TrunkHeight[1], template.TrunkHeight[2]) + local segmentCount = math.random(template.SegmentCount[1], template.SegmentCount[2]) + local segmentHeight = totalHeight / segmentCount + + -- Ground level is 0 -- trees sit directly on the baseplate + local groundY = 0 + local currentY = groundY + + for i = 1, segmentCount do + local segment = Instance.new("Part") + segment.Name = "TrunkSegment_" .. i + segment.Shape = Enum.PartType.Cylinder + + local taperMult = 1 - ((i - 1) / segmentCount) * 0.4 + local diameter = trunkDiameter * taperMult + + segment.Size = Vector3.new(segmentHeight, diameter, diameter) + segment.CFrame = CFrame.new(position.X, currentY + segmentHeight / 2, position.Z) * CFrame.Angles(0, 0, math.rad(90)) + + segment.Color = template.BarkColor + segment.Material = template.BarkMaterial + segment.Anchored = true + segment.TopSurface = Enum.SurfaceType.Smooth + segment.BottomSurface = Enum.SurfaceType.Smooth + + segment:SetAttribute("TreeType", treeType) + segment:SetAttribute("Health", config.HealthPerSegment) + segment.CustomPhysicalProperties = PhysicalProperties.new(config.Density, 0.3, 0.5) + + CollectionService:AddTag(segment, "TreeSegment") + CollectionService:AddTag(segment, "Draggable") + + if template.GlowTrunk then + local light = Instance.new("PointLight") + light.Color = template.BarkColor + light.Brightness = 0.5 + light.Range = 8 + light.Parent = segment + end + + addHealthBar(segment) + + segment.Parent = model + currentY = currentY + segmentHeight + + if i > 1 then + local weld = Instance.new("WeldConstraint") + weld.Part0 = model:FindFirstChild("TrunkSegment_" .. (i - 1)) + weld.Part1 = segment + weld.Parent = segment + end + end + + -- Build canopy + if template.HasLeaves then + local canopyRadius = randomInRange(template.CanopyRadius[1], template.CanopyRadius[2]) + local canopyY = currentY + + if template.ConicalCanopy then + for layer = 1, template.CanopySegments do + local layerRadius = canopyRadius * (1 - (layer - 1) / template.CanopySegments) + local canopy = Instance.new("Part") + canopy.Name = "Canopy_" .. layer + canopy.Shape = Enum.PartType.Ball + canopy.Size = Vector3.new(layerRadius * 2, 2, layerRadius * 2) + canopy.Position = Vector3.new(position.X, canopyY + (layer - 1) * 1.5, position.Z) + canopy.Color = template.LeafColor + canopy.Material = template.LeafMaterial + canopy.Anchored = true + canopy.CanCollide = false + canopy.CastShadow = true + canopy.Parent = model + end + else + for seg = 1, template.CanopySegments do + local angle = (seg / template.CanopySegments) * math.pi * 2 + local offsetX = math.cos(angle) * canopyRadius * 0.4 + local offsetZ = math.sin(angle) * canopyRadius * 0.4 + local size = randomInRange(canopyRadius * 0.6, canopyRadius * 1.0) + + local canopy = Instance.new("Part") + canopy.Name = "Canopy_" .. seg + canopy.Shape = Enum.PartType.Ball + canopy.Size = Vector3.new(size * 2, size * 1.2, size * 2) + canopy.Position = Vector3.new( + position.X + offsetX, + canopyY + randomInRange(-1, 2), + position.Z + offsetZ + ) + canopy.Color = template.LeafColor + canopy.Material = template.LeafMaterial + canopy.Anchored = true + canopy.CanCollide = false + canopy.CastShadow = true + canopy.Parent = model + end + end + end + + local primaryPart = model:FindFirstChild("TrunkSegment_1") + if primaryPart then + model.PrimaryPart = primaryPart + end + + CollectionService:AddTag(model, "TreeModel") + model.Parent = treesFolder + + return model +end + +-- Spawn trees in a biome region +local function spawnTreesInBiome(biomeName, biomeData) + local region = biomeData.Region + local treeTypes = biomeData.TreeTypes + if #treeTypes == 0 then return end + + local regionWidth = region.MaxX - region.MinX + local regionDepth = region.MaxZ - region.MinZ + local area = regionWidth * regionDepth + + local numTrees = math.floor(area * biomeData.TreeDensity) + numTrees = math.min(numTrees, 200) + + local spawned = 0 + local attempts = 0 + local maxAttempts = numTrees * 5 + + while spawned < numTrees and attempts < maxAttempts do + attempts = attempts + 1 + + local x = region.MinX + math.random() * regionWidth + local z = region.MinZ + math.random() * regionDepth + + if isPositionValid(Vector3.new(x, 0, z), BiomeConfig.MinTreeSpacing) then + local treeType = treeTypes[math.random(1, #treeTypes)] + -- All trees at Y=0 (flat baseplate surface) + local treePos = Vector3.new(x, 0, z) + + local tree = createTreeModel(treeType, treePos) + if tree then + table.insert(treePositions, treePos) + spawned = spawned + 1 + end + end + end + + print("Spawned " .. spawned .. " trees in " .. biomeName) +end + +-- Wait for biome generation then spawn trees +task.spawn(function() + while not _G.BiomeGenerationComplete do + task.wait(0.5) + end + + print("=== Tree Spawning Starting ===") + + for biomeName, biomeData in pairs(BiomeConfig.Biomes) do + spawnTreesInBiome(biomeName, biomeData) + task.wait() + end + + print("=== Tree Spawning Complete ===") + _G.TreeSpawningComplete = true +end) + +_G.CreateTree = function(treeType, position) + return createTreeModel(treeType, position) +end + +_G.AddHealthBar = function(segment) + addHealthBar(segment) +end diff --git a/src/ServerScriptService/VehicleLogisticsManager.server.lua b/src/ServerScriptService/VehicleLogisticsManager.server.lua new file mode 100644 index 0000000000000000000000000000000000000000..67d0cf02bb4510b9baaa8c5e71845ffdde69300a --- /dev/null +++ b/src/ServerScriptService/VehicleLogisticsManager.server.lua @@ -0,0 +1,65 @@ +-- src/ServerScriptService/VehicleLogisticsManager.server.lua + +local CollectionService = game:GetService("CollectionService") +local PhysicsService = game:GetService("PhysicsService") +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local VehicleConfig = require(ReplicatedStorage:WaitForChild("Shared"):WaitForChild("VehicleConfig")) + +-- Setup collision groups initially to prevent chaotic interactions if needed later +pcall(function() + PhysicsService:RegisterCollisionGroup(VehicleConfig.CollisionGroups.Logs) + PhysicsService:RegisterCollisionGroup(VehicleConfig.CollisionGroups.Vehicles) + + -- Ensure they are fully collidable so logs can rest on the vehicle bed + PhysicsService:CollisionGroupSetCollidable(VehicleConfig.CollisionGroups.Logs, VehicleConfig.CollisionGroups.Vehicles, true) +end) + +-- Function to initialize a new vehicle's physics to handle the heavy logs safely +local function setupVehicle(vehicleModel) + -- Typically flatbeds are made of several parts grouped under a 'Flatbed' model or have a specific tag. + -- We will check for parts named "FlatbedPart" or tagged "Flatbed" within the vehicle. + local function applyToPart(part) + if part:IsA("BasePart") then + part.CustomPhysicalProperties = VehicleConfig.FlatbedPhysicalProperties + part.CollisionGroup = VehicleConfig.CollisionGroups.Vehicles + end + end + + for _, descendant in pairs(vehicleModel:GetDescendants()) do + if descendant.Name == "FlatbedPart" or CollectionService:HasTag(descendant, "Flatbed") then + applyToPart(descendant) + end + end +end + +-- Function to apply Log physics specifically for stability +local function setupLog(logSegment) + if logSegment:IsA("BasePart") then + logSegment.CollisionGroup = VehicleConfig.CollisionGroups.Logs + + -- Override elasticity so logs don't bounce out of the truck on rough terrain + local currentProps = logSegment.CustomPhysicalProperties or PhysicalProperties.new(logSegment.Material) + logSegment.CustomPhysicalProperties = PhysicalProperties.new( + currentProps.Density, + 0.8, -- Moderate friction against other logs + 0, -- ZERO elasticity + 100, -- Friction weight + 100 -- Elasticity weight (forcing the zero elasticity to win against the truck) + ) + end +end + + +-- Listen for new vehicles and logs added to the game +CollectionService:GetInstanceAddedSignal("Vehicle"):Connect(setupVehicle) +CollectionService:GetInstanceAddedSignal("TreeSegment"):Connect(setupLog) + +-- Setup existing elements that might have spawned before this script loaded +for _, vehicle in pairs(CollectionService:GetTagged("Vehicle")) do + setupVehicle(vehicle) +end + +for _, log in pairs(CollectionService:GetTagged("TreeSegment")) do + setupLog(log) +end diff --git a/src/ServerScriptService/WeatherManager.server.lua b/src/ServerScriptService/WeatherManager.server.lua new file mode 100644 index 0000000000000000000000000000000000000000..e6c5f160bf9ae7febd550e5d03bd96e60a794d0d --- /dev/null +++ b/src/ServerScriptService/WeatherManager.server.lua @@ -0,0 +1,126 @@ +-- src/ServerScriptService/WeatherManager.server.lua + +local CollectionService = game:GetService("CollectionService") +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local Lighting = game:GetService("Lighting") +local Players = game:GetService("Players") + +local WeatherConfig = require(ReplicatedStorage:WaitForChild("Shared"):WaitForChild("WeatherConfig")) +local NotificationEvent = ReplicatedStorage:WaitForChild("Events"):WaitForChild("NotificationEvent") +local SoundEvent = ReplicatedStorage:WaitForChild("Events"):WaitForChild("SoundEvent") + +-- Weather states +local WeatherStates = {"Clear", "Rain", "Fog", "Storm"} +local currentWeather = "Clear" +local isRaining = false + +-- Determine if a log is under a roof +local function isLogProtected(log) + local origin = log.Position + local direction = Vector3.new(0, 100, 0) + + local raycastParams = RaycastParams.new() + raycastParams.FilterDescendantsInstances = {log} + raycastParams.FilterType = Enum.RaycastFilterType.Exclude + + local result = workspace:Raycast(origin, direction, raycastParams) + + if result and result.Instance then + return true + end + + return false +end + +-- Process Wood Degradation +local function processDegradation() + if not isRaining then return end + + for _, log in pairs(CollectionService:GetTagged("TreeSegment")) do + if not isLogProtected(log) then + local currentDegradation = log:GetAttribute("DegradedMult") or 1.0 + + if currentDegradation > WeatherConfig.DegradationConfig.MaxValueLoss then + local newMult = currentDegradation - WeatherConfig.DegradationConfig.WoodValueLossPerTick + newMult = math.clamp(newMult, WeatherConfig.DegradationConfig.MaxValueLoss, 1.0) + + log:SetAttribute("DegradedMult", newMult) + + local r, g, b = log.Color.R, log.Color.G, log.Color.B + log.Color = Color3.new(r * 0.95, g * 0.95, b * 0.95) + end + end + end +end + +local function setWeather(state) + currentWeather = state + + if state == "Rain" or state == "Storm" then + isRaining = true + + -- Darken lighting + Lighting.Ambient = Color3.fromRGB(100, 100, 110) + Lighting.OutdoorAmbient = Color3.fromRGB(80, 80, 90) + Lighting.FogEnd = state == "Storm" and 400 or 800 + Lighting.FogColor = Color3.fromRGB(140, 140, 150) + + -- Notify players + for _, player in ipairs(Players:GetPlayers()) do + NotificationEvent:FireClient(player, "Weather", state == "Storm" and "A storm is rolling in! Protect your wood!" or "It's starting to rain. Wood left outside will degrade.") + SoundEvent:FireClient(player, "RainLoop", nil) + end + + -- Start degradation loop + task.spawn(function() + while isRaining do + processDegradation() + task.wait(WeatherConfig.DegradationConfig.TickInterval) + end + end) + + elseif state == "Fog" then + isRaining = false + Lighting.Ambient = Color3.fromRGB(120, 120, 125) + Lighting.OutdoorAmbient = Color3.fromRGB(100, 100, 105) + Lighting.FogEnd = 300 + Lighting.FogColor = Color3.fromRGB(180, 180, 190) + + else -- Clear + isRaining = false + Lighting.Ambient = Color3.fromRGB(150, 150, 150) + Lighting.OutdoorAmbient = Color3.fromRGB(140, 140, 140) + Lighting.FogEnd = 2000 + Lighting.FogColor = Color3.fromRGB(200, 210, 220) + end + + print("Weather changed to:", state) +end + +-- Weather Cycle Loop +task.spawn(function() + while true do + -- Clear period + setWeather("Clear") + local clearTime = math.random(WeatherConfig.CycleDurationMin, WeatherConfig.CycleDurationMax) + task.wait(clearTime) + + -- Pick weather event + local roll = math.random() + if roll < 0.5 then + setWeather("Rain") + elseif roll < 0.75 then + setWeather("Fog") + else + setWeather("Storm") + end + + local eventTime = math.random(WeatherConfig.RainDurationMin, WeatherConfig.RainDurationMax) + task.wait(eventTime) + end +end) + +-- Expose current weather state +_G.GetCurrentWeather = function() + return currentWeather +end diff --git a/src/ServerScriptService/WireLogicManager.server.lua b/src/ServerScriptService/WireLogicManager.server.lua new file mode 100644 index 0000000000000000000000000000000000000000..e5cb52cff2b077eaab412af43417055306b1935f --- /dev/null +++ b/src/ServerScriptService/WireLogicManager.server.lua @@ -0,0 +1,133 @@ +-- src/ServerScriptService/WireLogicManager.server.lua + +local CollectionService = game:GetService("CollectionService") +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local WireLogicConfig = require(ReplicatedStorage:WaitForChild("Shared"):WaitForChild("WireLogicConfig")) +local WireConnectEvent = ReplicatedStorage:WaitForChild("Events"):WaitForChild("WireConnectEvent") + +-- Store connections globally +local connections = {} -- [sourcePart] = {targetPart1, targetPart2...} +local componentStates = {} -- [part] = true/false (Power state) + +-- Update the state of a specific component +local function updateState(part, newState) + if componentStates[part] == newState then return end + componentStates[part] = newState + + -- Visual changes mapping + if newState then + if part:IsA("BasePart") then + -- Optional visual effect (glow) + part.Material = Enum.Material.Neon + end + else + if part:IsA("BasePart") then + part.Material = Enum.Material.SmoothPlastic + end + end + + -- Trigger logic if it's a Target component + local compType = part:GetAttribute("ComponentType") + if compType == "Target" then + local targetFunc = part:GetAttribute("TargetFunction") + if targetFunc == "ToggleDoor" then + -- Basic door tweening logic here + -- if newState then openDoor() else closeDoor() end + print("Door toggled to " .. tostring(newState)) + elseif targetFunc == "ToggleConveyor" then + -- Turn on/off conveyor velocity + local conveyorVelocity = part:GetAttribute("ConveyorSpeed") or 5 + part.AssemblyLinearVelocity = newState and (part.CFrame.LookVector * conveyorVelocity) or Vector3.new() + end + end + + -- Propagate signal to connected children + local targets = connections[part] + if targets then + for _, targetPart in pairs(targets) do + -- Basic signal pass. Real implementation requires Logic Gate (AND/OR) processing here. + updateState(targetPart, newState) + end + end +end + +-- Hook up interactions for Source components (Buttons, Levers) +local function bindSource(sourcePart) + local sourceType = sourcePart:GetAttribute("SourceType") + + if sourceType == "Button" then + -- We'll use a ProximityPrompt for interaction + local prompt = sourcePart:FindFirstChild("ProximityPrompt") + if not prompt then + prompt = Instance.new("ProximityPrompt") + prompt.ActionText = "Press" + prompt.Parent = sourcePart + end + + prompt.Triggered:Connect(function(player) + -- Buttons pulse signal for 1 second + updateState(sourcePart, true) + task.delay(1, function() + updateState(sourcePart, false) + end) + end) + + elseif sourceType == "Lever" then + local prompt = sourcePart:FindFirstChild("ProximityPrompt") + if not prompt then + prompt = Instance.new("ProximityPrompt") + prompt.ActionText = "Toggle" + prompt.Parent = sourcePart + end + + prompt.Triggered:Connect(function(player) + -- Levers toggle exact state + local currentState = componentStates[sourcePart] or false + updateState(sourcePart, not currentState) + end) + end +end + +-- Initialize Sources +CollectionService:GetInstanceAddedSignal("LogicSource"):Connect(bindSource) +for _, src in pairs(CollectionService:GetTagged("LogicSource")) do + bindSource(src) +end + +-- Handle Players wiring things together +WireConnectEvent.OnServerEvent:Connect(function(player, sourcePart, targetPart) + -- Security checks + if not sourcePart or not targetPart then return end + + -- Distance logic validation + if (sourcePart.Position - targetPart.Position).Magnitude > WireLogicConfig.MaxWireLength then + print("Wire too long!") + return + end + + -- Register connection + if not connections[sourcePart] then + connections[sourcePart] = {} + end + + table.insert(connections[sourcePart], targetPart) + + -- Draw physical wire (Visual) using Beam or RopeConstraint + local attachment0 = sourcePart:FindFirstChild("WireAtt") or Instance.new("Attachment", sourcePart) + attachment0.Name = "WireAtt" + + local attachment1 = targetPart:FindFirstChild("WireAtt") or Instance.new("Attachment", targetPart) + attachment1.Name = "WireAtt" + + local beam = Instance.new("Beam") + beam.Attachment0 = attachment0 + beam.Attachment1 = attachment1 + beam.FaceCamera = true + beam.Width0 = 0.1 + beam.Width1 = 0.1 + beam.Color = ColorSequence.new(Color3.new(0,0,0)) + beam.Parent = sourcePart + + print("Wire connected from", sourcePart.Name, "to", targetPart.Name) +end) diff --git a/src/StarterGui/CraftingGUI.client.lua b/src/StarterGui/CraftingGUI.client.lua new file mode 100644 index 0000000000000000000000000000000000000000..245b9103fe382dcc55b951cfd782f0dbeaa1d906 --- /dev/null +++ b/src/StarterGui/CraftingGUI.client.lua @@ -0,0 +1,233 @@ +-- src/StarterGui/CraftingGUI.client.lua +-- Crafting panel showing recipes and resource counts, opened with C key + +local Players = game:GetService("Players") +local UserInputService = game:GetService("UserInputService") +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local player = Players.LocalPlayer +local CraftingConfig = require(ReplicatedStorage:WaitForChild("Shared"):WaitForChild("CraftingConfig")) +local CraftEvent = ReplicatedStorage:WaitForChild("Events"):WaitForChild("CraftEvent") + +local screenGui = Instance.new("ScreenGui") +screenGui.Name = "CraftingGUI" +screenGui.ResetOnSpawn = false +screenGui.Parent = player:WaitForChild("PlayerGui") + +local mainFrame = Instance.new("Frame") +mainFrame.Name = "CraftingFrame" +mainFrame.Size = UDim2.new(0, 380, 0, 420) +mainFrame.Position = UDim2.new(0.5, -190, 0.5, -210) +mainFrame.BackgroundColor3 = Color3.fromRGB(30, 25, 20) +mainFrame.BackgroundTransparency = 0.05 +mainFrame.BorderSizePixel = 0 +mainFrame.Visible = false +mainFrame.Parent = screenGui + +local corner = Instance.new("UICorner") +corner.CornerRadius = UDim.new(0, 12) +corner.Parent = mainFrame + +local stroke = Instance.new("UIStroke") +stroke.Color = Color3.fromRGB(180, 120, 50) +stroke.Thickness = 2 +stroke.Parent = mainFrame + +-- Title +local titleBar = Instance.new("Frame") +titleBar.Size = UDim2.new(1, 0, 0, 40) +titleBar.BackgroundColor3 = Color3.fromRGB(140, 90, 30) +titleBar.BorderSizePixel = 0 +titleBar.Parent = mainFrame + +local titleCorner = Instance.new("UICorner") +titleCorner.CornerRadius = UDim.new(0, 12) +titleCorner.Parent = titleBar + +local titleLabel = Instance.new("TextLabel") +titleLabel.Size = UDim2.new(1, -50, 1, 0) +titleLabel.Position = UDim2.new(0, 15, 0, 0) +titleLabel.BackgroundTransparency = 1 +titleLabel.Text = "Crafting Table" +titleLabel.TextColor3 = Color3.new(1, 1, 1) +titleLabel.TextScaled = true +titleLabel.Font = Enum.Font.GothamBold +titleLabel.TextXAlignment = Enum.TextXAlignment.Left +titleLabel.Parent = titleBar + +local closeBtn = Instance.new("TextButton") +closeBtn.Size = UDim2.new(0, 30, 0, 30) +closeBtn.Position = UDim2.new(1, -35, 0, 5) +closeBtn.BackgroundColor3 = Color3.fromRGB(180, 50, 50) +closeBtn.Text = "X" +closeBtn.TextColor3 = Color3.new(1, 1, 1) +closeBtn.TextScaled = true +closeBtn.Font = Enum.Font.GothamBold +closeBtn.Parent = titleBar + +local closeCorner = Instance.new("UICorner") +closeCorner.CornerRadius = UDim.new(0, 6) +closeCorner.Parent = closeBtn + +closeBtn.MouseButton1Click:Connect(function() + mainFrame.Visible = false +end) + +-- Resources bar +local resourceBar = Instance.new("Frame") +resourceBar.Size = UDim2.new(1, -20, 0, 30) +resourceBar.Position = UDim2.new(0, 10, 0, 45) +resourceBar.BackgroundColor3 = Color3.fromRGB(20, 18, 15) +resourceBar.BorderSizePixel = 0 +resourceBar.Parent = mainFrame + +local resourceCorner = Instance.new("UICorner") +resourceCorner.CornerRadius = UDim.new(0, 6) +resourceCorner.Parent = resourceBar + +local resourceLabel = Instance.new("TextLabel") +resourceLabel.Name = "ResourceLabel" +resourceLabel.Size = UDim2.new(1, -10, 1, 0) +resourceLabel.Position = UDim2.new(0, 5, 0, 0) +resourceLabel.BackgroundTransparency = 1 +resourceLabel.Text = "Wood: 0 | Stone: 0 | Gold: 0 | Diamond: 0" +resourceLabel.TextColor3 = Color3.fromRGB(200, 180, 140) +resourceLabel.TextScaled = true +resourceLabel.Font = Enum.Font.GothamMedium +resourceLabel.TextXAlignment = Enum.TextXAlignment.Left +resourceLabel.Parent = resourceBar + +-- Recipe list +local scrollFrame = Instance.new("ScrollingFrame") +scrollFrame.Size = UDim2.new(1, -20, 1, -90) +scrollFrame.Position = UDim2.new(0, 10, 0, 80) +scrollFrame.BackgroundColor3 = Color3.fromRGB(18, 15, 12) +scrollFrame.BorderSizePixel = 0 +scrollFrame.ScrollBarThickness = 5 +scrollFrame.CanvasSize = UDim2.new(0, 0, 0, 0) +scrollFrame.Parent = mainFrame + +local scrollCorner = Instance.new("UICorner") +scrollCorner.CornerRadius = UDim.new(0, 8) +scrollCorner.Parent = scrollFrame + +local listLayout = Instance.new("UIListLayout") +listLayout.SortOrder = Enum.SortOrder.LayoutOrder +listLayout.Padding = UDim.new(0, 6) +listLayout.Parent = scrollFrame + +local scrollPad = Instance.new("UIPadding") +scrollPad.PaddingTop = UDim.new(0, 5) +scrollPad.PaddingLeft = UDim.new(0, 5) +scrollPad.PaddingRight = UDim.new(0, 5) +scrollPad.Parent = scrollFrame + +local currentResources = {} + +local function updateResourceDisplay() + local wood = currentResources.Wood or 0 + local stone = currentResources.Stone or 0 + local gold = currentResources.GoldOre or 0 + local diamond = currentResources.Diamond or 0 + resourceLabel.Text = "Wood: " .. wood .. " | Stone: " .. stone .. " | Gold: " .. gold .. " | Diamond: " .. diamond +end + +local function populateRecipes() + for _, child in pairs(scrollFrame:GetChildren()) do + if child:IsA("Frame") then child:Destroy() end + end + + local idx = 0 + for recipeId, recipe in pairs(CraftingConfig.Recipes) do + idx = idx + 1 + + local card = Instance.new("Frame") + card.Name = recipeId + card.Size = UDim2.new(1, 0, 0, 80) + card.LayoutOrder = idx + card.BackgroundColor3 = Color3.fromRGB(40, 35, 28) + card.BorderSizePixel = 0 + card.Parent = scrollFrame + + local cardCorner = Instance.new("UICorner") + cardCorner.CornerRadius = UDim.new(0, 8) + cardCorner.Parent = card + + -- Name + local nameLabel = Instance.new("TextLabel") + nameLabel.Size = UDim2.new(0.6, 0, 0, 22) + nameLabel.Position = UDim2.new(0, 8, 0, 5) + nameLabel.BackgroundTransparency = 1 + nameLabel.Text = recipe.Name + nameLabel.TextColor3 = Color3.fromRGB(255, 220, 150) + nameLabel.TextScaled = true + nameLabel.Font = Enum.Font.GothamBold + nameLabel.TextXAlignment = Enum.TextXAlignment.Left + nameLabel.Parent = card + + -- Materials list + local matText = "" + for mat, amount in pairs(recipe.Materials) do + local have = currentResources[mat] or 0 + local color = have >= amount and "OK" or "NEED" + matText = matText .. mat .. ": " .. have .. "/" .. amount .. " " + end + + local matLabel = Instance.new("TextLabel") + matLabel.Name = "Materials" + matLabel.Size = UDim2.new(1, -16, 0, 20) + matLabel.Position = UDim2.new(0, 8, 0, 28) + matLabel.BackgroundTransparency = 1 + matLabel.Text = matText + matLabel.TextColor3 = Color3.fromRGB(180, 170, 150) + matLabel.TextScaled = true + matLabel.Font = Enum.Font.Gotham + matLabel.TextXAlignment = Enum.TextXAlignment.Left + matLabel.Parent = card + + -- Craft button + local craftBtn = Instance.new("TextButton") + craftBtn.Size = UDim2.new(0, 80, 0, 25) + craftBtn.Position = UDim2.new(1, -88, 1, -30) + craftBtn.BackgroundColor3 = Color3.fromRGB(140, 100, 30) + craftBtn.Text = "CRAFT" + craftBtn.TextColor3 = Color3.new(1, 1, 1) + craftBtn.TextScaled = true + craftBtn.Font = Enum.Font.GothamBold + craftBtn.Parent = card + + local btnCorner = Instance.new("UICorner") + btnCorner.CornerRadius = UDim.new(0, 6) + btnCorner.Parent = craftBtn + + craftBtn.MouseButton1Click:Connect(function() + CraftEvent:FireServer("Craft", recipeId) + end) + end + + task.wait() + scrollFrame.CanvasSize = UDim2.new(0, 0, 0, listLayout.AbsoluteContentSize.Y + 15) +end + +-- Listen for resource updates +CraftEvent.OnClientEvent:Connect(function(action, data) + if action == "ResourceUpdate" then + currentResources = data or {} + updateResourceDisplay() + populateRecipes() + end +end) + +-- Toggle with C key +UserInputService.InputBegan:Connect(function(input, gameProcessed) + if gameProcessed then return end + if input.KeyCode == Enum.KeyCode.C then + mainFrame.Visible = not mainFrame.Visible + if mainFrame.Visible then + CraftEvent:FireServer("QueryResources") + end + end +end) + +-- Initial populate +populateRecipes() diff --git a/src/StarterGui/DialogueGUI.client.lua b/src/StarterGui/DialogueGUI.client.lua new file mode 100644 index 0000000000000000000000000000000000000000..3baf1d7280739d7f22b1ef394e1baa3c6fbbf7a7 --- /dev/null +++ b/src/StarterGui/DialogueGUI.client.lua @@ -0,0 +1,90 @@ +-- src/StarterGui/DialogueGUI.client.lua +local Players = game:GetService("Players") +local TweenService = game:GetService("TweenService") +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local player = Players.LocalPlayer +local DialogueEvent = ReplicatedStorage:WaitForChild("Events"):WaitForChild("DialogueEvent") + +local screenGui = Instance.new("ScreenGui") +screenGui.Name = "DialogueGUI" +screenGui.ResetOnSpawn = false +screenGui.Parent = player:WaitForChild("PlayerGui") + +local mainFrame = Instance.new("Frame") +mainFrame.Name = "DialogueFrame" +mainFrame.Size = UDim2.new(0, 500, 0, 120) +mainFrame.Position = UDim2.new(0.5, -250, 1, -140) +mainFrame.BackgroundColor3 = Color3.fromRGB(20, 22, 28) +mainFrame.BackgroundTransparency = 0.1 +mainFrame.BorderSizePixel = 0 +mainFrame.Visible = false +mainFrame.Parent = screenGui + +local corner = Instance.new("UICorner") +corner.CornerRadius = UDim.new(0, 12) +corner.Parent = mainFrame + +local stroke = Instance.new("UIStroke") +stroke.Color = Color3.fromRGB(180, 160, 100) +stroke.Thickness = 2 +stroke.Parent = mainFrame + +local nameLabel = Instance.new("TextLabel") +nameLabel.Size = UDim2.new(0, 200, 0, 25) +nameLabel.Position = UDim2.new(0, 10, 0, 5) +nameLabel.BackgroundColor3 = Color3.fromRGB(120, 100, 50) +nameLabel.Text = "NPC" +nameLabel.TextColor3 = Color3.new(1, 1, 1) +nameLabel.TextScaled = true +nameLabel.Font = Enum.Font.GothamBold +nameLabel.Parent = mainFrame + +local nameCorner = Instance.new("UICorner") +nameCorner.CornerRadius = UDim.new(0, 6) +nameCorner.Parent = nameLabel + +local textLabel = Instance.new("TextLabel") +textLabel.Name = "DialogueText" +textLabel.Size = UDim2.new(1, -20, 0, 50) +textLabel.Position = UDim2.new(0, 10, 0, 35) +textLabel.BackgroundTransparency = 1 +textLabel.Text = "" +textLabel.TextColor3 = Color3.fromRGB(220, 220, 220) +textLabel.TextScaled = true +textLabel.Font = Enum.Font.Gotham +textLabel.TextXAlignment = Enum.TextXAlignment.Left +textLabel.TextWrapped = true +textLabel.Parent = mainFrame + +local continueLabel = Instance.new("TextLabel") +continueLabel.Size = UDim2.new(1, -10, 0, 20) +continueLabel.Position = UDim2.new(0, 5, 1, -25) +continueLabel.BackgroundTransparency = 1 +continueLabel.Text = "Click NPC again to continue..." +continueLabel.TextColor3 = Color3.fromRGB(140, 140, 140) +continueLabel.TextScaled = true +continueLabel.Font = Enum.Font.Gotham +continueLabel.Parent = mainFrame + +-- Typewriter effect +local function typewrite(label, text, speed) + label.Text = "" + for i = 1, #text do + label.Text = string.sub(text, 1, i) + task.wait(speed or 0.03) + end +end + +DialogueEvent.OnClientEvent:Connect(function(npcName, line, hasMore) + nameLabel.Text = " " .. npcName .. " " + mainFrame.Visible = true + continueLabel.Text = hasMore and "Click NPC again to continue..." or "End of conversation" + typewrite(textLabel, line, 0.025) + + if not hasMore then + task.delay(3, function() + mainFrame.Visible = false + end) + end +end) diff --git a/src/StarterGui/InventoryGUI.client.lua b/src/StarterGui/InventoryGUI.client.lua new file mode 100644 index 0000000000000000000000000000000000000000..115440ce1930788992e5088afb2e97bafdc5610a --- /dev/null +++ b/src/StarterGui/InventoryGUI.client.lua @@ -0,0 +1,192 @@ +-- src/StarterGui/InventoryGUI.client.lua +-- Grid-based inventory display with tool equip buttons + +local Players = game:GetService("Players") +local UserInputService = game:GetService("UserInputService") +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local player = Players.LocalPlayer +local ShopConfig = require(ReplicatedStorage:WaitForChild("Shared"):WaitForChild("ShopConfig")) +local EquipToolEvent = ReplicatedStorage:WaitForChild("Events"):WaitForChild("EquipToolEvent") +local ShopEvent = ReplicatedStorage:WaitForChild("Events"):WaitForChild("ShopEvent") + +local screenGui = Instance.new("ScreenGui") +screenGui.Name = "InventoryGUI" +screenGui.ResetOnSpawn = false +screenGui.Parent = player:WaitForChild("PlayerGui") + +local mainFrame = Instance.new("Frame") +mainFrame.Name = "InventoryFrame" +mainFrame.Size = UDim2.new(0, 400, 0, 350) +mainFrame.Position = UDim2.new(0.5, -200, 0.5, -175) +mainFrame.BackgroundColor3 = Color3.fromRGB(25, 28, 35) +mainFrame.BackgroundTransparency = 0.05 +mainFrame.BorderSizePixel = 0 +mainFrame.Visible = false +mainFrame.Parent = screenGui + +local corner = Instance.new("UICorner") +corner.CornerRadius = UDim.new(0, 12) +corner.Parent = mainFrame + +local stroke = Instance.new("UIStroke") +stroke.Color = Color3.fromRGB(100, 130, 200) +stroke.Thickness = 2 +stroke.Parent = mainFrame + +-- Title +local titleBar = Instance.new("Frame") +titleBar.Size = UDim2.new(1, 0, 0, 40) +titleBar.BackgroundColor3 = Color3.fromRGB(50, 70, 120) +titleBar.BorderSizePixel = 0 +titleBar.Parent = mainFrame + +local titleCorner = Instance.new("UICorner") +titleCorner.CornerRadius = UDim.new(0, 12) +titleCorner.Parent = titleBar + +local titleLabel = Instance.new("TextLabel") +titleLabel.Size = UDim2.new(1, -50, 1, 0) +titleLabel.Position = UDim2.new(0, 15, 0, 0) +titleLabel.BackgroundTransparency = 1 +titleLabel.Text = "Inventory" +titleLabel.TextColor3 = Color3.new(1, 1, 1) +titleLabel.TextScaled = true +titleLabel.Font = Enum.Font.GothamBold +titleLabel.TextXAlignment = Enum.TextXAlignment.Left +titleLabel.Parent = titleBar + +local closeBtn = Instance.new("TextButton") +closeBtn.Size = UDim2.new(0, 30, 0, 30) +closeBtn.Position = UDim2.new(1, -35, 0, 5) +closeBtn.BackgroundColor3 = Color3.fromRGB(180, 50, 50) +closeBtn.Text = "X" +closeBtn.TextColor3 = Color3.new(1, 1, 1) +closeBtn.TextScaled = true +closeBtn.Font = Enum.Font.GothamBold +closeBtn.Parent = titleBar + +local closeCorner = Instance.new("UICorner") +closeCorner.CornerRadius = UDim.new(0, 6) +closeCorner.Parent = closeBtn + +closeBtn.MouseButton1Click:Connect(function() + mainFrame.Visible = false +end) + +-- Scroll frame for items +local scrollFrame = Instance.new("ScrollingFrame") +scrollFrame.Size = UDim2.new(1, -20, 1, -55) +scrollFrame.Position = UDim2.new(0, 10, 0, 45) +scrollFrame.BackgroundColor3 = Color3.fromRGB(15, 18, 22) +scrollFrame.BorderSizePixel = 0 +scrollFrame.ScrollBarThickness = 6 +scrollFrame.CanvasSize = UDim2.new(0, 0, 0, 0) +scrollFrame.Parent = mainFrame + +local scrollCorner = Instance.new("UICorner") +scrollCorner.CornerRadius = UDim.new(0, 8) +scrollCorner.Parent = scrollFrame + +local gridLayout = Instance.new("UIGridLayout") +gridLayout.CellSize = UDim2.new(0, 110, 0, 70) +gridLayout.CellPadding = UDim2.new(0, 8, 0, 8) +gridLayout.SortOrder = Enum.SortOrder.LayoutOrder +gridLayout.Parent = scrollFrame + +local padding = Instance.new("UIPadding") +padding.PaddingTop = UDim.new(0, 5) +padding.PaddingLeft = UDim.new(0, 5) +padding.Parent = scrollFrame + +-- Cache current inventory +local currentInventory = nil + +local function refreshInventory(inventory) + currentInventory = inventory + + for _, child in pairs(scrollFrame:GetChildren()) do + if child:IsA("Frame") then child:Destroy() end + end + + if not inventory then return end + + local idx = 0 + for category, items in pairs(inventory) do + for _, itemId in ipairs(items) do + idx = idx + 1 + local itemData = ShopConfig.Items[itemId] + local itemName = itemData and itemData.Name or itemId + + local card = Instance.new("Frame") + card.Name = itemId .. "_" .. idx + card.LayoutOrder = idx + card.BackgroundColor3 = Color3.fromRGB(40, 45, 55) + card.BorderSizePixel = 0 + card.Parent = scrollFrame + + local cardCorner = Instance.new("UICorner") + cardCorner.CornerRadius = UDim.new(0, 6) + cardCorner.Parent = card + + local nameLabel = Instance.new("TextLabel") + nameLabel.Size = UDim2.new(1, -6, 0, 22) + nameLabel.Position = UDim2.new(0, 3, 0, 3) + nameLabel.BackgroundTransparency = 1 + nameLabel.Text = itemName + nameLabel.TextColor3 = Color3.new(1, 1, 1) + nameLabel.TextScaled = true + nameLabel.Font = Enum.Font.GothamBold + nameLabel.Parent = card + + local typeLabel = Instance.new("TextLabel") + typeLabel.Size = UDim2.new(1, -6, 0, 15) + typeLabel.Position = UDim2.new(0, 3, 0, 25) + typeLabel.BackgroundTransparency = 1 + typeLabel.Text = category + typeLabel.TextColor3 = Color3.fromRGB(150, 150, 180) + typeLabel.TextScaled = true + typeLabel.Font = Enum.Font.Gotham + typeLabel.Parent = card + + -- Equip button (only for tools) + if category == "Tools" then + local equipBtn = Instance.new("TextButton") + equipBtn.Size = UDim2.new(0.8, 0, 0, 20) + equipBtn.Position = UDim2.new(0.1, 0, 1, -23) + equipBtn.BackgroundColor3 = Color3.fromRGB(60, 120, 200) + equipBtn.Text = "Equip" + equipBtn.TextColor3 = Color3.new(1, 1, 1) + equipBtn.TextScaled = true + equipBtn.Font = Enum.Font.GothamBold + equipBtn.Parent = card + + local eqCorner = Instance.new("UICorner") + eqCorner.CornerRadius = UDim.new(0, 4) + eqCorner.Parent = equipBtn + + equipBtn.MouseButton1Click:Connect(function() + EquipToolEvent:FireServer(itemId) + end) + end + end + end + + task.wait() + scrollFrame.CanvasSize = UDim2.new(0, 0, 0, gridLayout.AbsoluteContentSize.Y + 15) +end + +-- Listen for inventory updates from server +ShopEvent.OnClientEvent:Connect(function(action, data) + if action == "InventoryUpdate" then + refreshInventory(data) + end +end) + +-- Toggle with I key +UserInputService.InputBegan:Connect(function(input, gameProcessed) + if gameProcessed then return end + if input.KeyCode == Enum.KeyCode.I then + mainFrame.Visible = not mainFrame.Visible + end +end) diff --git a/src/StarterGui/MainHUD.client.lua b/src/StarterGui/MainHUD.client.lua new file mode 100644 index 0000000000000000000000000000000000000000..cc5b12ca134a2509ad07215281be494c5773ffd7 --- /dev/null +++ b/src/StarterGui/MainHUD.client.lua @@ -0,0 +1,190 @@ +-- src/StarterGui/MainHUD.client.lua +-- Full in-game HUD: cash display, equipped tool, minimap frame, hotbar, weather indicator + +local Players = game:GetService("Players") +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local RunService = game:GetService("RunService") + +local player = Players.LocalPlayer +local Utility = require(ReplicatedStorage:WaitForChild("Shared"):WaitForChild("Utility")) + +-- Create ScreenGui +local screenGui = Instance.new("ScreenGui") +screenGui.Name = "MainHUD" +screenGui.ResetOnSpawn = false +screenGui.ZIndexBehavior = Enum.ZIndexBehavior.Sibling +screenGui.Parent = player:WaitForChild("PlayerGui") + +-- ========== TOP BAR ========== +local topBar = Instance.new("Frame") +topBar.Name = "TopBar" +topBar.Size = UDim2.new(1, 0, 0, 50) +topBar.Position = UDim2.new(0, 0, 0, 0) +topBar.BackgroundColor3 = Color3.fromRGB(20, 25, 30) +topBar.BackgroundTransparency = 0.3 +topBar.BorderSizePixel = 0 +topBar.Parent = screenGui + +local topCorner = Instance.new("UICorner") +topCorner.CornerRadius = UDim.new(0, 0) +topCorner.Parent = topBar + +-- Cash Display +local cashFrame = Instance.new("Frame") +cashFrame.Name = "CashFrame" +cashFrame.Size = UDim2.new(0, 200, 0, 40) +cashFrame.Position = UDim2.new(0, 10, 0, 5) +cashFrame.BackgroundColor3 = Color3.fromRGB(40, 120, 40) +cashFrame.BackgroundTransparency = 0.3 +cashFrame.BorderSizePixel = 0 +cashFrame.Parent = topBar + +local cashCorner = Instance.new("UICorner") +cashCorner.CornerRadius = UDim.new(0, 8) +cashCorner.Parent = cashFrame + +local cashIcon = Instance.new("TextLabel") +cashIcon.Name = "CashIcon" +cashIcon.Size = UDim2.new(0, 30, 1, 0) +cashIcon.Position = UDim2.new(0, 5, 0, 0) +cashIcon.BackgroundTransparency = 1 +cashIcon.Text = "$" +cashIcon.TextColor3 = Color3.fromRGB(255, 215, 0) +cashIcon.TextScaled = true +cashIcon.Font = Enum.Font.GothamBold +cashIcon.Parent = cashFrame + +local cashLabel = Instance.new("TextLabel") +cashLabel.Name = "CashLabel" +cashLabel.Size = UDim2.new(1, -40, 1, 0) +cashLabel.Position = UDim2.new(0, 38, 0, 0) +cashLabel.BackgroundTransparency = 1 +cashLabel.Text = "0" +cashLabel.TextColor3 = Color3.new(1, 1, 1) +cashLabel.TextScaled = true +cashLabel.Font = Enum.Font.GothamBold +cashLabel.TextXAlignment = Enum.TextXAlignment.Left +cashLabel.Parent = cashFrame + +-- Wood Chopped Display +local woodFrame = Instance.new("Frame") +woodFrame.Name = "WoodFrame" +woodFrame.Size = UDim2.new(0, 180, 0, 40) +woodFrame.Position = UDim2.new(0, 220, 0, 5) +woodFrame.BackgroundColor3 = Color3.fromRGB(120, 80, 40) +woodFrame.BackgroundTransparency = 0.3 +woodFrame.BorderSizePixel = 0 +woodFrame.Parent = topBar + +local woodCorner = Instance.new("UICorner") +woodCorner.CornerRadius = UDim.new(0, 8) +woodCorner.Parent = woodFrame + +local woodLabel = Instance.new("TextLabel") +woodLabel.Name = "WoodLabel" +woodLabel.Size = UDim2.new(1, -10, 1, 0) +woodLabel.Position = UDim2.new(0, 10, 0, 0) +woodLabel.BackgroundTransparency = 1 +woodLabel.Text = "Wood: 0" +woodLabel.TextColor3 = Color3.new(1, 1, 1) +woodLabel.TextScaled = true +woodLabel.Font = Enum.Font.GothamMedium +woodLabel.TextXAlignment = Enum.TextXAlignment.Left +woodLabel.Parent = woodFrame + +-- Equipped Tool Display +local toolFrame = Instance.new("Frame") +toolFrame.Name = "ToolFrame" +toolFrame.Size = UDim2.new(0, 180, 0, 40) +toolFrame.Position = UDim2.new(0.5, -90, 0, 5) +toolFrame.AnchorPoint = Vector2.new(0, 0) +toolFrame.BackgroundColor3 = Color3.fromRGB(60, 60, 80) +toolFrame.BackgroundTransparency = 0.3 +toolFrame.BorderSizePixel = 0 +toolFrame.Parent = topBar + +local toolCorner = Instance.new("UICorner") +toolCorner.CornerRadius = UDim.new(0, 8) +toolCorner.Parent = toolFrame + +local toolLabel = Instance.new("TextLabel") +toolLabel.Name = "ToolLabel" +toolLabel.Size = UDim2.new(1, -10, 1, 0) +toolLabel.Position = UDim2.new(0, 10, 0, 0) +toolLabel.BackgroundTransparency = 1 +toolLabel.Text = "Axe: BasicAxe" +toolLabel.TextColor3 = Color3.new(1, 1, 1) +toolLabel.TextScaled = true +toolLabel.Font = Enum.Font.GothamMedium +toolLabel.TextXAlignment = Enum.TextXAlignment.Center +toolLabel.Parent = toolFrame + +-- Weather Display +local weatherFrame = Instance.new("Frame") +weatherFrame.Name = "WeatherFrame" +weatherFrame.Size = UDim2.new(0, 140, 0, 40) +weatherFrame.Position = UDim2.new(1, -150, 0, 5) +weatherFrame.BackgroundColor3 = Color3.fromRGB(50, 70, 100) +weatherFrame.BackgroundTransparency = 0.3 +weatherFrame.BorderSizePixel = 0 +weatherFrame.Parent = topBar + +local weatherCorner = Instance.new("UICorner") +weatherCorner.CornerRadius = UDim.new(0, 8) +weatherCorner.Parent = weatherFrame + +local weatherLabel = Instance.new("TextLabel") +weatherLabel.Name = "WeatherLabel" +weatherLabel.Size = UDim2.new(1, -10, 1, 0) +weatherLabel.Position = UDim2.new(0, 5, 0, 0) +weatherLabel.BackgroundTransparency = 1 +weatherLabel.Text = "Clear" +weatherLabel.TextColor3 = Color3.new(1, 1, 1) +weatherLabel.TextScaled = true +weatherLabel.Font = Enum.Font.GothamMedium +weatherLabel.TextXAlignment = Enum.TextXAlignment.Center +weatherLabel.Parent = weatherFrame + +-- ========== HOTBAR HINTS ========== +local hintsFrame = Instance.new("Frame") +hintsFrame.Name = "ControlHints" +hintsFrame.Size = UDim2.new(0, 400, 0, 30) +hintsFrame.Position = UDim2.new(0.5, -200, 1, -40) +hintsFrame.BackgroundColor3 = Color3.fromRGB(20, 20, 20) +hintsFrame.BackgroundTransparency = 0.5 +hintsFrame.BorderSizePixel = 0 +hintsFrame.Parent = screenGui + +local hintsCorner = Instance.new("UICorner") +hintsCorner.CornerRadius = UDim.new(0, 6) +hintsCorner.Parent = hintsFrame + +local hintsLabel = Instance.new("TextLabel") +hintsLabel.Size = UDim2.new(1, -10, 1, 0) +hintsLabel.Position = UDim2.new(0, 5, 0, 0) +hintsLabel.BackgroundTransparency = 1 +hintsLabel.Text = "[Click] Punch/Chop [B] Build [C] Craft [I] Inventory [P] Shop [L] Quests [M] Market [O] Settings" +hintsLabel.TextColor3 = Color3.fromRGB(200, 200, 200) +hintsLabel.TextScaled = true +hintsLabel.Font = Enum.Font.Gotham +hintsLabel.Parent = hintsFrame + +-- ========== UPDATE LOOP ========== +RunService.Heartbeat:Connect(function() + local leaderstats = player:FindFirstChild("leaderstats") + if leaderstats then + local cash = leaderstats:FindFirstChild("Cash") + if cash then + cashLabel.Text = Utility.formatCash(cash.Value) + end + + local wood = leaderstats:FindFirstChild("WoodChopped") + if wood then + woodLabel.Text = "Wood: " .. tostring(wood.Value) + end + end + + -- Update equipped tool display + local axeType = player:GetAttribute("EquippedAxe") or "BasicAxe" + toolLabel.Text = "Axe: " .. axeType +end) diff --git a/src/StarterGui/MarketGUI.client.lua b/src/StarterGui/MarketGUI.client.lua new file mode 100644 index 0000000000000000000000000000000000000000..d8385df7b35ba5030c4875b1c6d42435750a2102 --- /dev/null +++ b/src/StarterGui/MarketGUI.client.lua @@ -0,0 +1,179 @@ +-- src/StarterGui/MarketGUI.client.lua +-- Live market prices display with trend indicators + +local Players = game:GetService("Players") +local UserInputService = game:GetService("UserInputService") +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local player = Players.LocalPlayer +local EconomyConfig = require(ReplicatedStorage:WaitForChild("Shared"):WaitForChild("EconomyConfig")) +local Utility = require(ReplicatedStorage:WaitForChild("Shared"):WaitForChild("Utility")) +local MarketUpdateEvent = ReplicatedStorage:WaitForChild("Events"):WaitForChild("MarketUpdateEvent") + +local screenGui = Instance.new("ScreenGui") +screenGui.Name = "MarketGUI" +screenGui.ResetOnSpawn = false +screenGui.Parent = player:WaitForChild("PlayerGui") + +local mainFrame = Instance.new("Frame") +mainFrame.Name = "MarketFrame" +mainFrame.Size = UDim2.new(0, 300, 0, 380) +mainFrame.Position = UDim2.new(0, 10, 0.5, -190) +mainFrame.BackgroundColor3 = Color3.fromRGB(20, 25, 30) +mainFrame.BackgroundTransparency = 0.1 +mainFrame.BorderSizePixel = 0 +mainFrame.Visible = false +mainFrame.Parent = screenGui + +local frameCorner = Instance.new("UICorner") +frameCorner.CornerRadius = UDim.new(0, 12) +frameCorner.Parent = mainFrame + +local frameStroke = Instance.new("UIStroke") +frameStroke.Color = Color3.fromRGB(200, 160, 60) +frameStroke.Thickness = 2 +frameStroke.Parent = mainFrame + +local titleBar = Instance.new("Frame") +titleBar.Size = UDim2.new(1, 0, 0, 35) +titleBar.BackgroundColor3 = Color3.fromRGB(150, 120, 30) +titleBar.BorderSizePixel = 0 +titleBar.Parent = mainFrame + +local titleCorner = Instance.new("UICorner") +titleCorner.CornerRadius = UDim.new(0, 12) +titleCorner.Parent = titleBar + +local titleLabel = Instance.new("TextLabel") +titleLabel.Size = UDim2.new(1, -50, 1, 0) +titleLabel.Position = UDim2.new(0, 10, 0, 0) +titleLabel.BackgroundTransparency = 1 +titleLabel.Text = "Market Prices" +titleLabel.TextColor3 = Color3.new(1, 1, 1) +titleLabel.TextScaled = true +titleLabel.Font = Enum.Font.GothamBold +titleLabel.TextXAlignment = Enum.TextXAlignment.Left +titleLabel.Parent = titleBar + +local closeBtn = Instance.new("TextButton") +closeBtn.Size = UDim2.new(0, 28, 0, 28) +closeBtn.Position = UDim2.new(1, -32, 0, 3) +closeBtn.BackgroundColor3 = Color3.fromRGB(180, 50, 50) +closeBtn.Text = "X" +closeBtn.TextColor3 = Color3.new(1, 1, 1) +closeBtn.TextScaled = true +closeBtn.Font = Enum.Font.GothamBold +closeBtn.Parent = titleBar + +local closeCorner = Instance.new("UICorner") +closeCorner.CornerRadius = UDim.new(0, 6) +closeCorner.Parent = closeBtn + +closeBtn.MouseButton1Click:Connect(function() + mainFrame.Visible = false +end) + +local scrollFrame = Instance.new("ScrollingFrame") +scrollFrame.Size = UDim2.new(1, -16, 1, -45) +scrollFrame.Position = UDim2.new(0, 8, 0, 38) +scrollFrame.BackgroundTransparency = 1 +scrollFrame.BorderSizePixel = 0 +scrollFrame.ScrollBarThickness = 5 +scrollFrame.CanvasSize = UDim2.new(0, 0, 0, 0) +scrollFrame.Parent = mainFrame + +local listLayout = Instance.new("UIListLayout") +listLayout.SortOrder = Enum.SortOrder.LayoutOrder +listLayout.Padding = UDim.new(0, 3) +listLayout.Parent = scrollFrame + +-- Previous rates for trend arrows +local previousRates = {} + +local function updateMarketDisplay(rates) + -- Clear old entries + for _, child in pairs(scrollFrame:GetChildren()) do + if child:IsA("Frame") then child:Destroy() end + end + + local idx = 0 + for woodType, rate in pairs(rates) do + idx = idx + 1 + local baseValue = EconomyConfig.WoodBaseValues[woodType] or 0 + local currentPrice = math.floor(baseValue * rate) + + local prevRate = previousRates[woodType] or 1.0 + local trend = "" + local trendColor = Color3.new(1, 1, 1) + if rate > prevRate + 0.01 then + trend = " ^" + trendColor = Color3.fromRGB(100, 255, 100) + elseif rate < prevRate - 0.01 then + trend = " v" + trendColor = Color3.fromRGB(255, 100, 100) + else + trend = " =" + trendColor = Color3.fromRGB(200, 200, 200) + end + + local row = Instance.new("Frame") + row.Size = UDim2.new(1, 0, 0, 22) + row.LayoutOrder = idx + row.BackgroundColor3 = idx % 2 == 0 and Color3.fromRGB(30, 33, 40) or Color3.fromRGB(25, 28, 35) + row.BorderSizePixel = 0 + row.Parent = scrollFrame + + local rowCorner = Instance.new("UICorner") + rowCorner.CornerRadius = UDim.new(0, 4) + rowCorner.Parent = row + + local nameLabel = Instance.new("TextLabel") + nameLabel.Size = UDim2.new(0.45, 0, 1, 0) + nameLabel.Position = UDim2.new(0, 5, 0, 0) + nameLabel.BackgroundTransparency = 1 + nameLabel.Text = woodType + nameLabel.TextColor3 = Color3.new(1, 1, 1) + nameLabel.TextScaled = true + nameLabel.Font = Enum.Font.GothamMedium + nameLabel.TextXAlignment = Enum.TextXAlignment.Left + nameLabel.Parent = row + + local priceLabel = Instance.new("TextLabel") + priceLabel.Size = UDim2.new(0.3, 0, 1, 0) + priceLabel.Position = UDim2.new(0.45, 0, 0, 0) + priceLabel.BackgroundTransparency = 1 + priceLabel.Text = "$" .. tostring(currentPrice) .. "/vol" + priceLabel.TextColor3 = Color3.fromRGB(255, 215, 0) + priceLabel.TextScaled = true + priceLabel.Font = Enum.Font.GothamMedium + priceLabel.Parent = row + + local trendLabel = Instance.new("TextLabel") + trendLabel.Size = UDim2.new(0.2, 0, 1, 0) + trendLabel.Position = UDim2.new(0.78, 0, 0, 0) + trendLabel.BackgroundTransparency = 1 + trendLabel.Text = string.format("%.0f%%", rate * 100) .. trend + trendLabel.TextColor3 = trendColor + trendLabel.TextScaled = true + trendLabel.Font = Enum.Font.GothamBold + trendLabel.Parent = row + end + + previousRates = rates + + task.wait() + scrollFrame.CanvasSize = UDim2.new(0, 0, 0, listLayout.AbsoluteContentSize.Y + 10) +end + +-- Listen for market updates +MarketUpdateEvent.OnClientEvent:Connect(function(rates) + updateMarketDisplay(rates) +end) + +-- Toggle with M key +UserInputService.InputBegan:Connect(function(input, gameProcessed) + if gameProcessed then return end + if input.KeyCode == Enum.KeyCode.M then + mainFrame.Visible = not mainFrame.Visible + end +end) diff --git a/src/StarterGui/QuestGUI.client.lua b/src/StarterGui/QuestGUI.client.lua new file mode 100644 index 0000000000000000000000000000000000000000..918db0430c318ec1bf59d440a6c0553b2c07b7ac --- /dev/null +++ b/src/StarterGui/QuestGUI.client.lua @@ -0,0 +1,217 @@ +-- src/StarterGui/QuestGUI.client.lua +-- Quest log panel with progress bars + +local Players = game:GetService("Players") +local UserInputService = game:GetService("UserInputService") +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local player = Players.LocalPlayer +local QuestConfig = require(ReplicatedStorage:WaitForChild("Shared"):WaitForChild("QuestConfig")) +local QuestUpdateEvent = ReplicatedStorage:WaitForChild("Events"):WaitForChild("QuestUpdateEvent") + +local screenGui = Instance.new("ScreenGui") +screenGui.Name = "QuestGUI" +screenGui.ResetOnSpawn = false +screenGui.Parent = player:WaitForChild("PlayerGui") + +local mainFrame = Instance.new("Frame") +mainFrame.Name = "QuestFrame" +mainFrame.Size = UDim2.new(0, 350, 0, 400) +mainFrame.Position = UDim2.new(1, -360, 0, 60) +mainFrame.BackgroundColor3 = Color3.fromRGB(25, 25, 35) +mainFrame.BackgroundTransparency = 0.05 +mainFrame.BorderSizePixel = 0 +mainFrame.Visible = false +mainFrame.Parent = screenGui + +local corner = Instance.new("UICorner") +corner.CornerRadius = UDim.new(0, 12) +corner.Parent = mainFrame + +local stroke = Instance.new("UIStroke") +stroke.Color = Color3.fromRGB(200, 180, 60) +stroke.Thickness = 2 +stroke.Parent = mainFrame + +local titleBar = Instance.new("Frame") +titleBar.Size = UDim2.new(1, 0, 0, 40) +titleBar.BackgroundColor3 = Color3.fromRGB(120, 100, 30) +titleBar.BorderSizePixel = 0 +titleBar.Parent = mainFrame + +local titleCorner = Instance.new("UICorner") +titleCorner.CornerRadius = UDim.new(0, 12) +titleCorner.Parent = titleBar + +local titleLabel = Instance.new("TextLabel") +titleLabel.Size = UDim2.new(1, -50, 1, 0) +titleLabel.Position = UDim2.new(0, 15, 0, 0) +titleLabel.BackgroundTransparency = 1 +titleLabel.Text = "Quest Log" +titleLabel.TextColor3 = Color3.new(1, 1, 1) +titleLabel.TextScaled = true +titleLabel.Font = Enum.Font.GothamBold +titleLabel.TextXAlignment = Enum.TextXAlignment.Left +titleLabel.Parent = titleBar + +local closeBtn = Instance.new("TextButton") +closeBtn.Size = UDim2.new(0, 30, 0, 30) +closeBtn.Position = UDim2.new(1, -35, 0, 5) +closeBtn.BackgroundColor3 = Color3.fromRGB(180, 50, 50) +closeBtn.Text = "X" +closeBtn.TextColor3 = Color3.new(1, 1, 1) +closeBtn.TextScaled = true +closeBtn.Font = Enum.Font.GothamBold +closeBtn.Parent = titleBar + +local closeCorner = Instance.new("UICorner") +closeCorner.CornerRadius = UDim.new(0, 6) +closeCorner.Parent = closeBtn + +closeBtn.MouseButton1Click:Connect(function() + mainFrame.Visible = false +end) + +local scrollFrame = Instance.new("ScrollingFrame") +scrollFrame.Size = UDim2.new(1, -20, 1, -55) +scrollFrame.Position = UDim2.new(0, 10, 0, 45) +scrollFrame.BackgroundColor3 = Color3.fromRGB(15, 15, 22) +scrollFrame.BorderSizePixel = 0 +scrollFrame.ScrollBarThickness = 6 +scrollFrame.CanvasSize = UDim2.new(0, 0, 0, 0) +scrollFrame.Parent = mainFrame + +local scrollCorner = Instance.new("UICorner") +scrollCorner.CornerRadius = UDim.new(0, 8) +scrollCorner.Parent = scrollFrame + +local listLayout = Instance.new("UIListLayout") +listLayout.SortOrder = Enum.SortOrder.LayoutOrder +listLayout.Padding = UDim.new(0, 6) +listLayout.Parent = scrollFrame + +local scrollPadding = Instance.new("UIPadding") +scrollPadding.PaddingTop = UDim.new(0, 5) +scrollPadding.PaddingLeft = UDim.new(0, 5) +scrollPadding.PaddingRight = UDim.new(0, 5) +scrollPadding.Parent = scrollFrame + +local questCards = {} -- [questId] = cardFrame + +local function createQuestCard(questId, questDef, progress, target, completed) + if questCards[questId] then + -- Update existing + local card = questCards[questId] + local progressBar = card:FindFirstChild("ProgressFill") + local progressLabel = card:FindFirstChild("ProgressLabel") + + if progressBar then + progressBar.Size = UDim2.new(math.clamp(progress / target, 0, 1), 0, 1, 0) + progressBar.BackgroundColor3 = completed and Color3.fromRGB(60, 180, 60) or Color3.fromRGB(60, 120, 200) + end + if progressLabel then + progressLabel.Text = completed and "COMPLETE" or (tostring(progress) .. " / " .. tostring(target)) + end + return + end + + local card = Instance.new("Frame") + card.Name = questId + card.Size = UDim2.new(1, 0, 0, 70) + card.BackgroundColor3 = completed and Color3.fromRGB(30, 50, 30) or Color3.fromRGB(35, 38, 48) + card.BorderSizePixel = 0 + card.Parent = scrollFrame + + local cardCorner = Instance.new("UICorner") + cardCorner.CornerRadius = UDim.new(0, 8) + cardCorner.Parent = card + + local nameLabel = Instance.new("TextLabel") + nameLabel.Size = UDim2.new(1, -10, 0, 20) + nameLabel.Position = UDim2.new(0, 5, 0, 5) + nameLabel.BackgroundTransparency = 1 + nameLabel.Text = questDef.Title + nameLabel.TextColor3 = completed and Color3.fromRGB(100, 255, 100) or Color3.new(1, 1, 1) + nameLabel.TextScaled = true + nameLabel.Font = Enum.Font.GothamBold + nameLabel.TextXAlignment = Enum.TextXAlignment.Left + nameLabel.Parent = card + + local descLabel = Instance.new("TextLabel") + descLabel.Size = UDim2.new(1, -10, 0, 15) + descLabel.Position = UDim2.new(0, 5, 0, 25) + descLabel.BackgroundTransparency = 1 + descLabel.Text = questDef.Description + descLabel.TextColor3 = Color3.fromRGB(170, 170, 180) + descLabel.TextScaled = true + descLabel.Font = Enum.Font.Gotham + descLabel.TextXAlignment = Enum.TextXAlignment.Left + descLabel.Parent = card + + -- Progress bar background + local progressBg = Instance.new("Frame") + progressBg.Name = "ProgressBg" + progressBg.Size = UDim2.new(0.7, 0, 0, 12) + progressBg.Position = UDim2.new(0, 5, 1, -18) + progressBg.BackgroundColor3 = Color3.fromRGB(20, 20, 25) + progressBg.BorderSizePixel = 0 + progressBg.Parent = card + + local bgCorner = Instance.new("UICorner") + bgCorner.CornerRadius = UDim.new(0, 4) + bgCorner.Parent = progressBg + + local progressFill = Instance.new("Frame") + progressFill.Name = "ProgressFill" + progressFill.Size = UDim2.new(math.clamp(progress / target, 0, 1), 0, 1, 0) + progressFill.BackgroundColor3 = completed and Color3.fromRGB(60, 180, 60) or Color3.fromRGB(60, 120, 200) + progressFill.BorderSizePixel = 0 + progressFill.Parent = progressBg + + local fillCorner = Instance.new("UICorner") + fillCorner.CornerRadius = UDim.new(0, 4) + fillCorner.Parent = progressFill + + -- Progress text + local progressLabel = Instance.new("TextLabel") + progressLabel.Name = "ProgressLabel" + progressLabel.Size = UDim2.new(0.25, 0, 0, 12) + progressLabel.Position = UDim2.new(0.75, 0, 1, -18) + progressLabel.BackgroundTransparency = 1 + progressLabel.Text = completed and "COMPLETE" or (tostring(progress) .. " / " .. tostring(target)) + progressLabel.TextColor3 = completed and Color3.fromRGB(100, 255, 100) or Color3.fromRGB(180, 180, 180) + progressLabel.TextScaled = true + progressLabel.Font = Enum.Font.GothamMedium + progressLabel.TextXAlignment = Enum.TextXAlignment.Right + progressLabel.Parent = card + + questCards[questId] = card + + task.wait() + scrollFrame.CanvasSize = UDim2.new(0, 0, 0, listLayout.AbsoluteContentSize.Y + 15) +end + +-- Handle quest events from server +QuestUpdateEvent.OnClientEvent:Connect(function(action, data) + if action == "Init" then + for questId, state in pairs(data) do + local questDef = QuestConfig.Quests[questId] + if questDef then + createQuestCard(questId, questDef, state.progress, questDef.Target, state.completed) + end + end + elseif action == "Update" then + local questDef = QuestConfig.Quests[data.questId] + if questDef then + createQuestCard(data.questId, questDef, data.progress, data.target, data.completed) + end + end +end) + +-- Toggle with L key +UserInputService.InputBegan:Connect(function(input, gameProcessed) + if gameProcessed then return end + if input.KeyCode == Enum.KeyCode.L then + mainFrame.Visible = not mainFrame.Visible + end +end) diff --git a/src/StarterGui/SettingsGUI.client.lua b/src/StarterGui/SettingsGUI.client.lua new file mode 100644 index 0000000000000000000000000000000000000000..2892e27419bdc9d38f99a33a10b006cfb2c4614f --- /dev/null +++ b/src/StarterGui/SettingsGUI.client.lua @@ -0,0 +1,177 @@ +-- src/StarterGui/SettingsGUI.client.lua +local Players = game:GetService("Players") +local UserInputService = game:GetService("UserInputService") +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local SoundService = game:GetService("SoundService") + +local player = Players.LocalPlayer + +local screenGui = Instance.new("ScreenGui") +screenGui.Name = "SettingsGUI" +screenGui.ResetOnSpawn = false +screenGui.Parent = player:WaitForChild("PlayerGui") + +local mainFrame = Instance.new("Frame") +mainFrame.Name = "SettingsFrame" +mainFrame.Size = UDim2.new(0, 350, 0, 300) +mainFrame.Position = UDim2.new(0.5, -175, 0.5, -150) +mainFrame.BackgroundColor3 = Color3.fromRGB(25, 28, 35) +mainFrame.BackgroundTransparency = 0.05 +mainFrame.BorderSizePixel = 0 +mainFrame.Visible = false +mainFrame.Parent = screenGui + +local corner = Instance.new("UICorner") +corner.CornerRadius = UDim.new(0, 12) +corner.Parent = mainFrame + +local stroke = Instance.new("UIStroke") +stroke.Color = Color3.fromRGB(120, 120, 140) +stroke.Thickness = 2 +stroke.Parent = mainFrame + +local titleBar = Instance.new("Frame") +titleBar.Size = UDim2.new(1, 0, 0, 38) +titleBar.BackgroundColor3 = Color3.fromRGB(60, 60, 80) +titleBar.BorderSizePixel = 0 +titleBar.Parent = mainFrame + +local titleCorner = Instance.new("UICorner") +titleCorner.CornerRadius = UDim.new(0, 12) +titleCorner.Parent = titleBar + +local titleLabel = Instance.new("TextLabel") +titleLabel.Size = UDim2.new(1, -50, 1, 0) +titleLabel.Position = UDim2.new(0, 15, 0, 0) +titleLabel.BackgroundTransparency = 1 +titleLabel.Text = "Settings" +titleLabel.TextColor3 = Color3.new(1, 1, 1) +titleLabel.TextScaled = true +titleLabel.Font = Enum.Font.GothamBold +titleLabel.TextXAlignment = Enum.TextXAlignment.Left +titleLabel.Parent = titleBar + +local closeBtn = Instance.new("TextButton") +closeBtn.Size = UDim2.new(0, 28, 0, 28) +closeBtn.Position = UDim2.new(1, -33, 0, 5) +closeBtn.BackgroundColor3 = Color3.fromRGB(180, 50, 50) +closeBtn.Text = "X" +closeBtn.TextColor3 = Color3.new(1, 1, 1) +closeBtn.TextScaled = true +closeBtn.Font = Enum.Font.GothamBold +closeBtn.Parent = titleBar + +local closeCorner = Instance.new("UICorner") +closeCorner.CornerRadius = UDim.new(0, 6) +closeCorner.Parent = closeBtn + +closeBtn.MouseButton1Click:Connect(function() + mainFrame.Visible = false +end) + +-- Slider builder +local function createSlider(parent, label, yPos, initial, callback) + local sliderFrame = Instance.new("Frame") + sliderFrame.Size = UDim2.new(1, -30, 0, 40) + sliderFrame.Position = UDim2.new(0, 15, 0, yPos) + sliderFrame.BackgroundTransparency = 1 + sliderFrame.Parent = parent + + local nameLabel = Instance.new("TextLabel") + nameLabel.Size = UDim2.new(0.4, 0, 0, 20) + nameLabel.BackgroundTransparency = 1 + nameLabel.Text = label + nameLabel.TextColor3 = Color3.new(1, 1, 1) + nameLabel.TextScaled = true + nameLabel.Font = Enum.Font.GothamMedium + nameLabel.TextXAlignment = Enum.TextXAlignment.Left + nameLabel.Parent = sliderFrame + + local trackBg = Instance.new("Frame") + trackBg.Size = UDim2.new(0.55, 0, 0, 8) + trackBg.Position = UDim2.new(0.4, 0, 0.5, -4) + trackBg.BackgroundColor3 = Color3.fromRGB(40, 40, 50) + trackBg.BorderSizePixel = 0 + trackBg.Parent = sliderFrame + + local trackCorner = Instance.new("UICorner") + trackCorner.CornerRadius = UDim.new(0, 4) + trackCorner.Parent = trackBg + + local fill = Instance.new("Frame") + fill.Size = UDim2.new(initial, 0, 1, 0) + fill.BackgroundColor3 = Color3.fromRGB(80, 140, 220) + fill.BorderSizePixel = 0 + fill.Parent = trackBg + + local fillCorner = Instance.new("UICorner") + fillCorner.CornerRadius = UDim.new(0, 4) + fillCorner.Parent = fill + + local valueLabel = Instance.new("TextLabel") + valueLabel.Size = UDim2.new(0, 30, 0, 20) + valueLabel.Position = UDim2.new(0.96, 0, 0.5, -10) + valueLabel.BackgroundTransparency = 1 + valueLabel.Text = tostring(math.floor(initial * 100)) .. "%" + valueLabel.TextColor3 = Color3.fromRGB(180, 180, 200) + valueLabel.TextScaled = true + valueLabel.Font = Enum.Font.Gotham + valueLabel.Parent = sliderFrame + + local dragging = false + trackBg.InputBegan:Connect(function(input) + if input.UserInputType == Enum.UserInputType.MouseButton1 then + dragging = true + end + end) + trackBg.InputEnded:Connect(function(input) + if input.UserInputType == Enum.UserInputType.MouseButton1 then + dragging = false + end + end) + + UserInputService.InputChanged:Connect(function(input) + if dragging and input.UserInputType == Enum.UserInputType.MouseMovement then + local absPos = trackBg.AbsolutePosition.X + local absSize = trackBg.AbsoluteSize.X + local relative = math.clamp((input.Position.X - absPos) / absSize, 0, 1) + fill.Size = UDim2.new(relative, 0, 1, 0) + valueLabel.Text = tostring(math.floor(relative * 100)) .. "%" + callback(relative) + end + end) +end + +-- Volume sliders +createSlider(mainFrame, "Master Volume", 50, 0.8, function(v) + SoundService.AmbientReverb = Enum.ReverbType.NoReverb + -- Adjust all sound groups + for _, group in pairs(SoundService:GetChildren()) do + if group:IsA("SoundGroup") then + group.Volume = v + end + end +end) + +createSlider(mainFrame, "SFX Volume", 100, 1.0, function(v) + local sfx = SoundService:FindFirstChild("SFX") + if sfx then sfx.Volume = v end +end) + +createSlider(mainFrame, "Ambient Volume", 150, 0.3, function(v) + local ambient = SoundService:FindFirstChild("Ambient") + if ambient then ambient.Volume = v end +end) + +createSlider(mainFrame, "Graphics Quality", 200, 0.8, function(v) + local qualityLevel = math.clamp(math.floor(v * 10) + 1, 1, 10) + settings().Rendering.QualityLevel = Enum.QualityLevel:GetEnumItems()[qualityLevel] or Enum.QualityLevel.Automatic +end) + +-- Toggle with ESC/O key +UserInputService.InputBegan:Connect(function(input, gameProcessed) + if gameProcessed then return end + if input.KeyCode == Enum.KeyCode.O then + mainFrame.Visible = not mainFrame.Visible + end +end) diff --git a/src/StarterGui/ShopGUI.client.lua b/src/StarterGui/ShopGUI.client.lua new file mode 100644 index 0000000000000000000000000000000000000000..debaae16ed1344db8283b9d70e6b8b920b3abbd1 --- /dev/null +++ b/src/StarterGui/ShopGUI.client.lua @@ -0,0 +1,246 @@ +-- src/StarterGui/ShopGUI.client.lua +-- Scrollable shop interface opened via P key or NPC interaction + +local Players = game:GetService("Players") +local UserInputService = game:GetService("UserInputService") +local ReplicatedStorage = game:GetService("ReplicatedStorage") + +local player = Players.LocalPlayer +local ShopConfig = require(ReplicatedStorage:WaitForChild("Shared"):WaitForChild("ShopConfig")) +local Utility = require(ReplicatedStorage:WaitForChild("Shared"):WaitForChild("Utility")) +local PurchaseEvent = ReplicatedStorage:WaitForChild("Events"):WaitForChild("PurchaseEvent") +local ShopEvent = ReplicatedStorage:WaitForChild("Events"):WaitForChild("ShopEvent") + +-- Create ScreenGui +local screenGui = Instance.new("ScreenGui") +screenGui.Name = "ShopGUI" +screenGui.ResetOnSpawn = false +screenGui.Parent = player:WaitForChild("PlayerGui") + +-- Main Frame (hidden by default) +local mainFrame = Instance.new("Frame") +mainFrame.Name = "ShopFrame" +mainFrame.Size = UDim2.new(0, 500, 0, 450) +mainFrame.Position = UDim2.new(0.5, -250, 0.5, -225) +mainFrame.BackgroundColor3 = Color3.fromRGB(25, 30, 35) +mainFrame.BackgroundTransparency = 0.05 +mainFrame.BorderSizePixel = 0 +mainFrame.Visible = false +mainFrame.Parent = screenGui + +local frameCorner = Instance.new("UICorner") +frameCorner.CornerRadius = UDim.new(0, 12) +frameCorner.Parent = mainFrame + +local frameStroke = Instance.new("UIStroke") +frameStroke.Color = Color3.fromRGB(80, 160, 80) +frameStroke.Thickness = 2 +frameStroke.Parent = mainFrame + +-- Title bar +local titleBar = Instance.new("Frame") +titleBar.Size = UDim2.new(1, 0, 0, 45) +titleBar.BackgroundColor3 = Color3.fromRGB(40, 100, 40) +titleBar.BorderSizePixel = 0 +titleBar.Parent = mainFrame + +local titleCorner = Instance.new("UICorner") +titleCorner.CornerRadius = UDim.new(0, 12) +titleCorner.Parent = titleBar + +local titleLabel = Instance.new("TextLabel") +titleLabel.Size = UDim2.new(1, -50, 1, 0) +titleLabel.Position = UDim2.new(0, 15, 0, 0) +titleLabel.BackgroundTransparency = 1 +titleLabel.Text = "Sue's Supply Shop" +titleLabel.TextColor3 = Color3.new(1, 1, 1) +titleLabel.TextScaled = true +titleLabel.Font = Enum.Font.GothamBold +titleLabel.TextXAlignment = Enum.TextXAlignment.Left +titleLabel.Parent = titleBar + +-- Close button +local closeBtn = Instance.new("TextButton") +closeBtn.Size = UDim2.new(0, 35, 0, 35) +closeBtn.Position = UDim2.new(1, -40, 0, 5) +closeBtn.BackgroundColor3 = Color3.fromRGB(180, 50, 50) +closeBtn.Text = "X" +closeBtn.TextColor3 = Color3.new(1, 1, 1) +closeBtn.TextScaled = true +closeBtn.Font = Enum.Font.GothamBold +closeBtn.Parent = titleBar + +local closeBtnCorner = Instance.new("UICorner") +closeBtnCorner.CornerRadius = UDim.new(0, 6) +closeBtnCorner.Parent = closeBtn + +closeBtn.MouseButton1Click:Connect(function() + mainFrame.Visible = false +end) + +-- Category tabs +local tabFrame = Instance.new("Frame") +tabFrame.Size = UDim2.new(1, -20, 0, 30) +tabFrame.Position = UDim2.new(0, 10, 0, 50) +tabFrame.BackgroundTransparency = 1 +tabFrame.Parent = mainFrame + +local tabLayout = Instance.new("UIListLayout") +tabLayout.FillDirection = Enum.FillDirection.Horizontal +tabLayout.SortOrder = Enum.SortOrder.LayoutOrder +tabLayout.Padding = UDim.new(0, 4) +tabLayout.Parent = tabFrame + +-- Scroll frame for items +local scrollFrame = Instance.new("ScrollingFrame") +scrollFrame.Name = "ItemsScroll" +scrollFrame.Size = UDim2.new(1, -20, 1, -100) +scrollFrame.Position = UDim2.new(0, 10, 0, 85) +scrollFrame.BackgroundColor3 = Color3.fromRGB(15, 18, 22) +scrollFrame.BorderSizePixel = 0 +scrollFrame.ScrollBarThickness = 6 +scrollFrame.CanvasSize = UDim2.new(0, 0, 0, 0) +scrollFrame.Parent = mainFrame + +local scrollCorner = Instance.new("UICorner") +scrollCorner.CornerRadius = UDim.new(0, 8) +scrollCorner.Parent = scrollFrame + +local gridLayout = Instance.new("UIGridLayout") +gridLayout.CellSize = UDim2.new(0, 220, 0, 80) +gridLayout.CellPadding = UDim2.new(0, 8, 0, 8) +gridLayout.SortOrder = Enum.SortOrder.LayoutOrder +gridLayout.Parent = scrollFrame + +local scrollPadding = Instance.new("UIPadding") +scrollPadding.PaddingTop = UDim.new(0, 5) +scrollPadding.PaddingLeft = UDim.new(0, 5) +scrollPadding.Parent = scrollFrame + +local currentCategory = "Tool" + +local function createItemCard(itemId, itemData, layoutOrder) + local card = Instance.new("Frame") + card.Name = itemId + card.LayoutOrder = layoutOrder + card.BackgroundColor3 = Color3.fromRGB(35, 40, 48) + card.BorderSizePixel = 0 + + local cardCorner = Instance.new("UICorner") + cardCorner.CornerRadius = UDim.new(0, 8) + cardCorner.Parent = card + + local nameLabel = Instance.new("TextLabel") + nameLabel.Size = UDim2.new(1, -10, 0, 20) + nameLabel.Position = UDim2.new(0, 5, 0, 5) + nameLabel.BackgroundTransparency = 1 + nameLabel.Text = itemData.Name + nameLabel.TextColor3 = Color3.new(1, 1, 1) + nameLabel.TextScaled = true + nameLabel.Font = Enum.Font.GothamBold + nameLabel.TextXAlignment = Enum.TextXAlignment.Left + nameLabel.Parent = card + + local descLabel = Instance.new("TextLabel") + descLabel.Size = UDim2.new(1, -10, 0, 15) + descLabel.Position = UDim2.new(0, 5, 0, 25) + descLabel.BackgroundTransparency = 1 + descLabel.Text = itemData.Description or "" + descLabel.TextColor3 = Color3.fromRGB(180, 180, 180) + descLabel.TextScaled = true + descLabel.Font = Enum.Font.Gotham + descLabel.TextXAlignment = Enum.TextXAlignment.Left + descLabel.Parent = card + + local buyBtn = Instance.new("TextButton") + buyBtn.Size = UDim2.new(0, 90, 0, 25) + buyBtn.Position = UDim2.new(1, -95, 1, -30) + buyBtn.BackgroundColor3 = itemData.Price == 0 and Color3.fromRGB(100, 100, 100) or Color3.fromRGB(60, 140, 60) + buyBtn.Text = itemData.Price == 0 and "FREE" or Utility.formatCash(itemData.Price) + buyBtn.TextColor3 = Color3.new(1, 1, 1) + buyBtn.TextScaled = true + buyBtn.Font = Enum.Font.GothamBold + buyBtn.Parent = card + + local btnCorner = Instance.new("UICorner") + btnCorner.CornerRadius = UDim.new(0, 6) + btnCorner.Parent = buyBtn + + buyBtn.MouseButton1Click:Connect(function() + PurchaseEvent:FireServer(itemId) + end) + + card.Parent = scrollFrame +end + +local function populateCategory(category) + currentCategory = category + + -- Clear existing items + for _, child in pairs(scrollFrame:GetChildren()) do + if child:IsA("Frame") then + child:Destroy() + end + end + + -- Populate items for this category + local items = ShopConfig.CategoryItems[category] or {} + for i, itemId in ipairs(items) do + local itemData = ShopConfig.Items[itemId] + if itemData then + createItemCard(itemId, itemData, i) + end + end + + -- Update canvas size + task.wait() + scrollFrame.CanvasSize = UDim2.new(0, 0, 0, gridLayout.AbsoluteContentSize.Y + 15) +end + +-- Create category tabs +for i, category in ipairs(ShopConfig.Categories) do + local tab = Instance.new("TextButton") + tab.Name = category + tab.Size = UDim2.new(0, 65, 1, 0) + tab.LayoutOrder = i + tab.BackgroundColor3 = Color3.fromRGB(50, 55, 65) + tab.Text = category + tab.TextColor3 = Color3.new(1, 1, 1) + tab.TextScaled = true + tab.Font = Enum.Font.GothamMedium + tab.Parent = tabFrame + + local tabCorner = Instance.new("UICorner") + tabCorner.CornerRadius = UDim.new(0, 6) + tabCorner.Parent = tab + + tab.MouseButton1Click:Connect(function() + -- Highlight active tab + for _, sibling in pairs(tabFrame:GetChildren()) do + if sibling:IsA("TextButton") then + sibling.BackgroundColor3 = Color3.fromRGB(50, 55, 65) + end + end + tab.BackgroundColor3 = Color3.fromRGB(40, 100, 40) + + populateCategory(category) + end) +end + +-- Initialize with first category +populateCategory("Tool") + +-- Toggle shop with P key +UserInputService.InputBegan:Connect(function(input, gameProcessed) + if gameProcessed then return end + if input.KeyCode == Enum.KeyCode.P then + mainFrame.Visible = not mainFrame.Visible + end +end) + +-- Handle NPC shop open event +ShopEvent.OnClientEvent:Connect(function(action, data) + if action == "OpenShop" then + mainFrame.Visible = true + end +end) diff --git a/src/StarterPlayer/StarterCharacterScripts/AxeController.client.lua b/src/StarterPlayer/StarterCharacterScripts/AxeController.client.lua new file mode 100644 index 0000000000000000000000000000000000000000..ad9af46105786e8926bfc3e4d30027a2071d1f0d --- /dev/null +++ b/src/StarterPlayer/StarterCharacterScripts/AxeController.client.lua @@ -0,0 +1,123 @@ +-- src/StarterPlayer/StarterCharacterScripts/AxeController.client.lua +-- Handles both fist punching and axe chopping + +local Players = game:GetService("Players") +local UserInputService = game:GetService("UserInputService") +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local CollectionService = game:GetService("CollectionService") + +local player = Players.LocalPlayer +local mouse = player:GetMouse() +local character = player.Character or player.CharacterAdded:Wait() + +local ChoppingConfig = require(ReplicatedStorage:WaitForChild("Shared"):WaitForChild("ChoppingConfig")) +local CraftingConfig = require(ReplicatedStorage:WaitForChild("Shared"):WaitForChild("CraftingConfig")) +local ChopEvent = ReplicatedStorage:WaitForChild("Events"):WaitForChild("ChopEvent") + +local canSwing = true + +local function getCurrentWeapon() + local axeType = nil + if character then + axeType = character:GetAttribute("EquippedAxe") + end + if not axeType or axeType == "" then + axeType = player:GetAttribute("EquippedAxe") + end + + -- No axe = use fists + if not axeType or axeType == "" or axeType == "None" then + return CraftingConfig.Fist, "Fist" + end + + local stats = ChoppingConfig.AxeTypes[axeType] + if stats then + return stats, axeType + end + + return CraftingConfig.Fist, "Fist" +end + +local function playSwingAnimation(weaponType) + local humanoid = character:FindFirstChildOfClass("Humanoid") + if not humanoid then return end + + local camera = workspace.CurrentCamera + if camera then + task.spawn(function() + local shake = weaponType == "Fist" and 1 or 2 + for i = 1, 3 do + camera.CFrame = camera.CFrame * CFrame.Angles(0, 0, math.rad(-shake + i * (shake * 0.6))) + task.wait(0.03) + end + end) + end +end + +local function createHitEffect(position, weaponType) + local part = Instance.new("Part") + part.Size = Vector3.new(0.4, 0.4, 0.4) + part.Position = position + part.Anchored = true + part.CanCollide = false + part.Material = Enum.Material.Neon + part.Shape = Enum.PartType.Ball + part.Parent = workspace.Terrain + + if weaponType == "Fist" then + part.BrickColor = BrickColor.new("Bright orange") + else + part.BrickColor = BrickColor.new("Bright yellow") + end + + task.spawn(function() + for i = 1, 5 do + part.Size = part.Size * 1.3 + part.Transparency = i / 5 + task.wait(0.03) + end + part:Destroy() + end) +end + +local function onInputBegan(input, gameProcessed) + if gameProcessed then return end + + if input.UserInputType == Enum.UserInputType.MouseButton1 then + if not canSwing then return end + if not character or not character:FindFirstChild("Head") then return end + + local weaponStats, weaponType = getCurrentWeapon() + + local rayOrigin = character.Head.Position + local rayDirection = (mouse.Hit.Position - rayOrigin).Unit * weaponStats.Range + + local raycastParams = RaycastParams.new() + raycastParams.FilterDescendantsInstances = {character} + raycastParams.FilterType = Enum.RaycastFilterType.Exclude + + local raycastResult = workspace:Raycast(rayOrigin, rayDirection, raycastParams) + + if raycastResult and raycastResult.Instance then + local hitPart = raycastResult.Instance + + -- Only chop tree segments + if CollectionService:HasTag(hitPart, "TreeSegment") then + playSwingAnimation(weaponType) + createHitEffect(raycastResult.Position, weaponType) + ChopEvent:FireServer(hitPart, raycastResult.Position) + end + end + + -- Cooldown applies even on miss (animation commitment) + canSwing = false + task.wait(weaponStats.SwingCooldown) + canSwing = true + end +end + +player.CharacterAdded:Connect(function(char) + character = char +end) + +UserInputService.InputBegan:Connect(onInputBegan) diff --git a/src/StarterPlayer/StarterCharacterScripts/BuildController.client.lua b/src/StarterPlayer/StarterCharacterScripts/BuildController.client.lua new file mode 100644 index 0000000000000000000000000000000000000000..9666b48a00543160e731da1915e377413a31f659 --- /dev/null +++ b/src/StarterPlayer/StarterCharacterScripts/BuildController.client.lua @@ -0,0 +1,149 @@ +-- src/StarterPlayer/StarterCharacterScripts/BuildController.client.lua + +local Players = game:GetService("Players") +local UserInputService = game:GetService("UserInputService") +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local RunService = game:GetService("RunService") + +local BuildingConfig = require(ReplicatedStorage:WaitForChild("Shared"):WaitForChild("BuildingConfig")) +local PlaceBlueprintEvent = ReplicatedStorage:WaitForChild("Events"):WaitForChild("PlaceBlueprintEvent") + +local player = Players.LocalPlayer +local mouse = player:GetMouse() +local character = player.Character or player.CharacterAdded:Wait() + +local buildModeActive = false +local currentStructureIndex = 1 +local currentStructure = BuildingConfig.StructureOrder[1] +local currentRotation = 0 +local previewModel = nil + +local function snapToGrid(position, gridSize) + return Vector3.new( + math.floor((position.X + gridSize/2) / gridSize) * gridSize, + position.Y, + math.floor((position.Z + gridSize/2) / gridSize) * gridSize + ) +end + +local function getStructureSize() + local config = BuildingConfig.Structures[currentStructure] + if config and config.Size then + return config.Size + end + return Vector3.new(10, 10, 1) +end + +local function cleanupPreview() + if previewModel then + previewModel:Destroy() + previewModel = nil + end +end + +local function createPreview() + cleanupPreview() + local size = getStructureSize() + + previewModel = Instance.new("Part") + previewModel.Size = size + previewModel.Transparency = 0.5 + previewModel.BrickColor = BrickColor.new("Neon blue") + previewModel.CanCollide = false + previewModel.Anchored = true + previewModel.Material = Enum.Material.ForceField + previewModel.Parent = workspace.Terrain + + -- Add label + local billboard = Instance.new("BillboardGui") + billboard.Size = UDim2.new(0, 200, 0, 50) + billboard.StudsOffset = Vector3.new(0, size.Y/2 + 2, 0) + billboard.AlwaysOnTop = true + billboard.Parent = previewModel + + local label = Instance.new("TextLabel") + label.Name = "StructureLabel" + label.Size = UDim2.new(1, 0, 1, 0) + label.BackgroundTransparency = 0.3 + label.BackgroundColor3 = Color3.new(0, 0, 0) + label.TextColor3 = Color3.new(1, 1, 1) + label.TextScaled = true + label.Font = Enum.Font.GothamBold + + local config = BuildingConfig.Structures[currentStructure] + label.Text = (config and config.Name or currentStructure) .. " [Q] Cycle [R] Rotate" + label.Parent = billboard +end + +local function toggleBuildMode() + buildModeActive = not buildModeActive + if not buildModeActive then + cleanupPreview() + else + createPreview() + end +end + +local function cycleStructure() + currentStructureIndex = currentStructureIndex + 1 + if currentStructureIndex > #BuildingConfig.StructureOrder then + currentStructureIndex = 1 + end + currentStructure = BuildingConfig.StructureOrder[currentStructureIndex] + if buildModeActive then + createPreview() + end +end + +local function updatePreview() + if not buildModeActive or not previewModel then return end + + local char = player.Character + if char and char:FindFirstChild("HumanoidRootPart") then + local distance = (char.HumanoidRootPart.Position - mouse.Hit.Position).Magnitude + if distance <= BuildingConfig.MaxPlacementDistance then + local snappedPos = snapToGrid(mouse.Hit.Position, BuildingConfig.GridSnap) + snappedPos = Vector3.new(snappedPos.X, mouse.Hit.Position.Y + (previewModel.Size.Y / 2), snappedPos.Z) + + previewModel.CFrame = CFrame.new(snappedPos) * CFrame.Angles(0, math.rad(currentRotation), 0) + previewModel.BrickColor = BrickColor.new("Neon blue") + else + previewModel.Position = mouse.Hit.Position + previewModel.BrickColor = BrickColor.new("Really red") + end + end +end + +local function onInputBegan(input, gameProcessed) + if gameProcessed then return end + + if input.KeyCode == Enum.KeyCode.B then + toggleBuildMode() + end + + if input.KeyCode == Enum.KeyCode.Q and buildModeActive then + cycleStructure() + end + + if input.KeyCode == Enum.KeyCode.R and buildModeActive then + currentRotation = (currentRotation + 90) % 360 + end + + if input.UserInputType == Enum.UserInputType.MouseButton1 and buildModeActive then + if previewModel and previewModel.BrickColor.Name == "Neon blue" then + PlaceBlueprintEvent:FireServer(currentStructure, previewModel.CFrame) + end + end +end + +player.CharacterAdded:Connect(function(char) + character = char +end) + +UserInputService.InputBegan:Connect(onInputBegan) + +RunService.RenderStepped:Connect(function() + if buildModeActive then + updatePreview() + end +end) diff --git a/src/StarterPlayer/StarterCharacterScripts/DragController.client.lua b/src/StarterPlayer/StarterCharacterScripts/DragController.client.lua new file mode 100644 index 0000000000000000000000000000000000000000..d7fb71befe07a56404f0caae2e2db495567a08ca --- /dev/null +++ b/src/StarterPlayer/StarterCharacterScripts/DragController.client.lua @@ -0,0 +1,177 @@ +-- src/StarterPlayer/StarterCharacterScripts/DragController.client.lua +local Players = game:GetService("Players") +local UserInputService = game:GetService("UserInputService") +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local RunService = game:GetService("RunService") +local CollectionService = game:GetService("CollectionService") + +local player = Players.LocalPlayer +local mouse = player:GetMouse() +local camera = workspace.CurrentCamera +local character = player.Character or player.CharacterAdded:Wait() + +local DraggingConfig = require(ReplicatedStorage:WaitForChild("Shared"):WaitForChild("DraggingConfig")) +local DragEvent = ReplicatedStorage:WaitForChild("Events"):WaitForChild("DragEvent") +local DropEvent = ReplicatedStorage:WaitForChild("Events"):WaitForChild("DropEvent") + +local isDragging = false +local draggedPart = nil +local targetAttachment = nil +local currentHoldDistance = DraggingConfig.HoldDistance +local highlightedPart = nil +local highlight = nil + +local renderConnection = nil +local hoverConnection = nil + +-- Create highlight effect +local function createHighlight(part) + removeHighlight() + if not part then return end + + highlight = Instance.new("SelectionBox") + highlight.Adornee = part + highlight.Color3 = Color3.fromRGB(100, 200, 255) + highlight.LineThickness = 0.03 + highlight.SurfaceTransparency = 0.8 + highlight.SurfaceColor3 = Color3.fromRGB(100, 200, 255) + highlight.Parent = part + + highlightedPart = part +end + +function removeHighlight() + if highlight then + highlight:Destroy() + highlight = nil + end + highlightedPart = nil +end + +-- Hover detection for draggable parts +local function updateHover() + if isDragging then return end + + local target = mouse.Target + if target and (CollectionService:HasTag(target, "Draggable") or CollectionService:HasTag(target, "TreeSegment")) then + if target ~= highlightedPart then + createHighlight(target) + end + else + removeHighlight() + end +end + +local function updateDraggingPosition() + if not isDragging or not draggedPart or not targetAttachment then return end + + local char = player.Character + if not char then return end + + local head = char:FindFirstChild("Head") + if not head then return end + + local rayOrigin = head.Position + local rayDirection = camera.CFrame.LookVector * currentHoldDistance + + local raycastParams = RaycastParams.new() + raycastParams.FilterDescendantsInstances = {char, draggedPart} + raycastParams.FilterType = Enum.RaycastFilterType.Exclude + + local raycastResult = workspace:Raycast(rayOrigin, rayDirection, raycastParams) + + local targetPos = rayOrigin + rayDirection + if raycastResult then + targetPos = raycastResult.Position + end + + targetAttachment.WorldCFrame = CFrame.new(targetPos) * CFrame.Angles(0, camera.CFrame.Rotation.Y, 0) +end + +local function stopDragging() + if not isDragging then return end + isDragging = false + draggedPart = nil + targetAttachment = nil + currentHoldDistance = DraggingConfig.HoldDistance + + if renderConnection then + renderConnection:Disconnect() + renderConnection = nil + end + + DropEvent:FireServer() +end + +local function onInputBegan(input, gameProcessed) + if gameProcessed then return end + + if input.UserInputType == Enum.UserInputType.MouseButton1 then + if isDragging then return end + + local targetPart = mouse.Target + if not targetPart then return end + + if not CollectionService:HasTag(targetPart, "Draggable") and not CollectionService:HasTag(targetPart, "TreeSegment") then return end + + local char = player.Character + if not char or not char:FindFirstChild("HumanoidRootPart") then return end + + local distance = (char.HumanoidRootPart.Position - targetPart.Position).Magnitude + if distance > DraggingConfig.MaxGrabDistance then return end + + removeHighlight() + + DragEvent:FireServer(targetPart, mouse.Hit.Position) + + local attachmentName = "TargetAttachment_Player" .. player.UserId + + local t = 0 + while not workspace.Terrain:FindFirstChild(attachmentName) and t < 1 do + t = t + task.wait() + end + + targetAttachment = workspace.Terrain:FindFirstChild(attachmentName) + + if targetAttachment then + isDragging = true + draggedPart = targetPart + currentHoldDistance = DraggingConfig.HoldDistance + + renderConnection = RunService.RenderStepped:Connect(updateDraggingPosition) + end + end +end + +local function onInputEnded(input, gameProcessed) + if input.UserInputType == Enum.UserInputType.MouseButton1 then + stopDragging() + end +end + +-- Scroll wheel for hold distance adjustment +local function onInputChanged(input) + if not isDragging then return end + + if input.UserInputType == Enum.UserInputType.MouseWheel then + currentHoldDistance = math.clamp( + currentHoldDistance + input.Position.Z * 2, + 3, + DraggingConfig.MaxGrabDistance + ) + end +end + +-- Respawn handling +player.CharacterAdded:Connect(function(char) + character = char + camera = workspace.CurrentCamera + stopDragging() +end) + +-- Hover detection loop +hoverConnection = RunService.RenderStepped:Connect(updateHover) + +UserInputService.InputBegan:Connect(onInputBegan) +UserInputService.InputEnded:Connect(onInputEnded) +UserInputService.InputChanged:Connect(onInputChanged) diff --git a/src/StarterPlayer/StarterCharacterScripts/SoundController.client.lua b/src/StarterPlayer/StarterCharacterScripts/SoundController.client.lua new file mode 100644 index 0000000000000000000000000000000000000000..72a1c0a306a047860d2ffa46bfd734e9f8229c2b --- /dev/null +++ b/src/StarterPlayer/StarterCharacterScripts/SoundController.client.lua @@ -0,0 +1,31 @@ +-- src/StarterPlayer/StarterCharacterScripts/SoundController.client.lua +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local SoundConfig = require(ReplicatedStorage:WaitForChild("Shared"):WaitForChild("SoundConfig")) +local SoundEvent = ReplicatedStorage:WaitForChild("Events"):WaitForChild("SoundEvent") + +local function playSound(soundName, position) + local soundId = SoundConfig[soundName] + if not soundId then return end + + local sound = Instance.new("Sound") + sound.SoundId = soundId + sound.Volume = SoundConfig.SFXVolume or 1 + sound.RollOffMaxDistance = 100 + + if position then + local att = Instance.new("Attachment") + att.WorldPosition = position + att.Parent = workspace.Terrain + sound.Parent = att + sound:Play() + sound.Ended:Once(function() att:Destroy() end) + else + sound.Parent = workspace + sound:Play() + sound.Ended:Once(function() sound:Destroy() end) + end +end + +SoundEvent.OnClientEvent:Connect(function(soundName, position) + playSound(soundName, position) +end) diff --git a/studio-rust-mcp-server-system-mechanics.md b/studio-rust-mcp-server-system-mechanics.md new file mode 100644 index 0000000000000000000000000000000000000000..5831963aabd4af18599ba230e890ac476b887327 --- /dev/null +++ b/studio-rust-mcp-server-system-mechanics.md @@ -0,0 +1,78 @@ +# NOTE: This document is from 'C:\Users\User\Desktop\VSCode2\RobloxStudio-MCP-GoogleAntigravity\references\studio-rust-mcp-server-main' + +# Studio Rust MCP Server System Mechanics + +This document provides a detailed analysis of the architecture and control flow of the `studio-rust-mcp-server` integration, an alternative implementation built leveraging the performance and strong safety guarantees of Rust and Tokio. + +## Architecture Diagram + +Below is a Mermaid sequence diagram showing how LLM requests are proxied down into the Roblox Studio instance. + +```mermaid +sequenceDiagram + participant LLM as LLM Client (Claude/Cursor) + participant MCP as Rust rmcp Server + participant Axum as Axum HTTP Server (Port 44755) + participant State as AppState (Arc) + participant Plugin as Roblox Studio Plugin (Luau) + + note over LLM,Plugin: 1. Setup & Background Loop + Plugin->>Axum: GET /request (Long Polling) + Axum->>State: Wait for task via tokio::sync::watch + + note over LLM,Plugin: 2. Incoming AI Request + LLM->>MCP: CallTool (e.g., run_code) + MCP->>State: Lock Mutex -> push_back(ToolArguments) to process_queue + MCP->>State: Store output channel in output_map with UUID + MCP->>State: Trigger watch channel + + note over Axum,Plugin: 3. Processing Polling State + State-->>Axum: Watch channel unblocks! + Axum->>State: pop_front() from process_queue + Axum-->>Plugin: Return ToolArguments payload (JSON with ID) + + note over Plugin: 4. Plugin Action + Plugin->>Plugin: Execute raw code / action inside Studio DataModel + Plugin->>Axum: POST /response (with UUID and string outcome) + + note over Axum,LLM: 5. Response Routing + Axum->>State: Lock Mutex -> remove channel from output_map + Axum->>MCP: Pass outcome via mpsc channel + Axum-->>Plugin: HTTP 200 OK + MCP-->>LLM: Return CallToolResult + + note over Plugin: 6. Polling Resumes + Plugin->>Axum: GET /request (starts long polling again) +``` + +## Detailed Discussion + +### Components Overview + +1. **`rmcp` Server (`src/rbx_studio_server.rs`)**: + Instead of using Node SDKs, this project opts for `rmcp` to implement the Model Context Protocol in Rust. The `RBXStudioServer` struct acts as the core handler, registering tools. Unlike the Node implementation which relies extensively on granular specific tools, this server relies on a handful of very powerful primitives: + - `run_code`: Executes raw arbitrary Luau in Studio. + - `insert_model`: Wrapper around asset fetching. + - `start_stop_play`: Manipulating playtest environments directly. + - `run_script_in_play_mode`: Playtesting code safely. + +2. **AppState Wrapper (Shared State Management)**: + Rust safely handles concurrency via a struct `AppState` wrapped in an `Arc>`. It contains: + - `process_queue`: A double-ended queue (`VecDeque`) holding incoming tool arguments. + - `output_map`: A hash map linking specific request UUIDs to Multi-Producer Single-Consumer (`mpsc`) transmission channels. + - `trigger` & `waiter`: Utilizing `tokio::sync::watch` to notify the polling endpoint instantly when new data arrives queueing up. + +3. **Axum Web Server (`src/main.rs` & `request_handler`)**: + The `axum` routing library manages local standard REST routes: + - `/request`: An endpoint heavily utilizing Tokio. Uses `tokio::time::timeout` locked to ~15 seconds to simulate a long poll. When the `/request` endpoint is hit, it waits asynchronously until the watch channel (`waiter`) is triggered by the `mcp` server. If it pops a task from the queue, it returns safely; otherwise, it closes the request to avoid stale connections. + - `/response`: Simply captures the resulting output payload, maps it heavily guarded against `state.output_map`, and triggers the matching channel to allow the caller to yield. + - `/proxy`: Acts as a relay loop back mechanism if addressing ports directly fails. + +4. **Studio Plugin Component**: + Utilizes standard long polling mechanisms available in Roblox, waiting on the HTTP GET boundary limits and posting resolutions rapidly upon execution completion. + +### Core Mechanics + +- **Asynchronous Channels**: By relying deeply on Tokio execution primitives (`mpsc` and `watch` channels), this implementation is incredibly efficient and avoids potential Node.JS event-loop blocking overheads. +- **Simplicity via Primitives**: Instead of building abstractions for every potential Studio action (like changing colors or getting tags explicitly), the Rust server relies on the LLM generating the correct Luau code and sending it raw via `run_code`. This decreases the size of the server greatly but increases the dependency on the LLM outputting perfectly formatted, syntactically correct engine scripts. +- **Graceful Timeouts Management**: Leveraging `tokio::time::timeout` ensures threads and HTTP connections are cleanly released precisely every 15 seconds, preventing the Roblox Studio client memory from lagging over time with trapped networking resources. diff --git a/studio_setup_guide.md b/studio_setup_guide.md new file mode 100644 index 0000000000000000000000000000000000000000..9ee1e17994bbfcc7469f15e00519bb021c9b6109 --- /dev/null +++ b/studio_setup_guide.md @@ -0,0 +1,88 @@ +# Timberbound Expeditions: Studio Setup & Tagging Guide + +To bring the overarching logic to life, your physical objects in Roblox Studio need to be identified by the engine scripts. We use Roblox's **CollectionService (Tags)** and **Attributes** to do this securely and efficiently, avoiding thousands of `.Touched` connections on generic parts. + +## How to Apply Tags & Attributes +- **Tags:** Use the built-in **Tag Editor** in Roblox Studio (View -> Tag Editor). Select your part/model, and tick the box for the specific tag name. +- **Attributes:** Select an object, scroll to the bottom of the **Properties** panel, and click "Add Attribute" (usually a String, Number, or Boolean type). + +--- + +## 1. Core Mechanics: Trees & Dragging + +### `TreeSegment` (Tag) +* **What it goes on:** Any physical cylinder/block that represents a piece of a choppable tree or a cut log. +* **Required Attributes:** + * `TreeType` (String): e.g., `"Oak"`, `"Pine"`. This maps exactly to the names in [ChoppingConfig.lua](file:///c:/Users/User/Desktop/VSCode2/RobloxStudio-MCP-GoogleAntigravity/src/ReplicatedStorage/Shared/ChoppingConfig.lua). + * `Health` (Number - *Optional*): The server will auto-assign MaxHealth from the config if you don't provide this. + +### `Draggable` (Tag) +* **What it goes on:** Furniture, tools, or boxes that the player can pick up and move around using the physics dragger. +* *(Note: Anything tagged `TreeSegment` is automatically draggable, you do not need both tags).* + +--- + +## 2. Economy & Processing + +### `ProcessingMachine` (Tag) +* **What it goes on:** The main `Model` grouping your sawmill or stripper. +* **Required Parts Inside:** + * `ProcessTrigger` (Part): An invisible, non-colliding block inside the machine where the sawblade/stripper is. When a log touches this, it gets processed. +* **Required Attributes (on the Model):** + * `MachineType` (String): `"Stripper"` or `"Sawmill"`. + +### `MarketDropoff` (Tag) +* **What it goes on:** A large, invisible, non-colliding `Part` at your market location. When processed wood touches it, the wood is sold and cash is given. + +### `ShopCounter` (Tag) +* **What it goes on:** An invisible `Part` resting just above the checkout counter in your shop. +* **How it works:** Players drop boxed items onto this part. The script counts their prices when the player interacts with the NPC. + +### `BoxedItem` (Tag) +* **What it goes on:** The physical cardboard box mesh sitting on the shop shelves. +* **Required Attributes:** + * `ItemId` (String): e.g., `"BasicAxe"`, `"Sawmill"`. Must match the IDs in [ShopConfig.lua](file:///c:/Users/User/Desktop/VSCode2/RobloxStudio-MCP-GoogleAntigravity/src/ReplicatedStorage/Shared/ShopConfig.lua). + +--- + +## 3. Base Building & Plots + +### `EmptyPlot` (Tag) +* **What it goes on:** The massive, flat baseplate `Part` representing a claimable piece of land. +* **How it works:** When a player without a plot touches this part, the server assigns it to them and loads their Datastore objects onto it. + +--- + +## 4. Vehicle Logistics + +### `Vehicle` (Tag) +* **What it goes on:** The root `Model` of whatever vehicle chassis you are using (A-Chassis, Constraint Chassis, etc.). +* **Required Parts Inside:** + * Either name the bed of the truck `"FlatbedPart"` OR tag that single part `"Flatbed"`. The script will apply massive friction to this specific part so logs don't slide off. + +--- + +## 5. Wiring & Logic + +### `LogicSource` (Tag) +* **What it goes on:** The interactable object like a button or lever `Part`. +* **Required Attributes:** + * `SourceType` (String): `"Button"` or `"Lever"`. A button pulses power for 1 second; a lever toggles it indefinitely. + +--- + +## 6. Adventure & Hazards + +### `HazardZone` (Tag) +* **What it goes on:** A large invisible part representing an area affecting nearby flora. +* **Required Attributes:** + * `HazardType` (String): `"Lava"` or `"Toxic"`. Mutates nearby trees into Fireproof or Glowing variants. + +### `WeightSwitch` (Tag) +* **What it goes on:** A `Model` representing an environmental puzzle switch. +* **Required Parts Inside:** + * `Plate` (Part): The physical button that gets stepped on. + * `LinkedDoor` (Part): The wall/door that disappears when the puzzle is solved. + +### `SwampZone` (Tag) +* **What it goes on:** A large invisible part representing deep mud or swamp liquid. Heavy items sinking into it are physically forced downwards unless they possess the `SwampTires` tag.