From 7948818405765ca60d04cbd8d07559652d9a0d0c Mon Sep 17 00:00:00 2001 From: Froredion Date: Tue, 7 Oct 2025 05:45:43 +0800 Subject: [PATCH 1/4] v4.0.0 --- README.md | 147 +++++++++++++- docs/changelog.md | 224 +++++++++++++-------- src/Testers/ZonePlusTest.client.lua | 297 ++++++++++++++++++++++++++++ src/Zone/Enum/ZoneShape.lua | 8 + src/Zone/VERSION.lua | 3 +- src/Zone/ZoneController/Tracker.lua | 65 +++--- src/Zone/ZoneController/init.lua | 95 ++++----- src/Zone/init.lua | 285 ++++++++++++++++---------- 8 files changed, 851 insertions(+), 273 deletions(-) create mode 100644 src/Testers/ZonePlusTest.client.lua create mode 100644 src/Zone/Enum/ZoneShape.lua diff --git a/README.md b/README.md index 8670420..5f81086 100644 --- a/README.md +++ b/README.md @@ -1 +1,146 @@ -https://devforum.roblox.com/t/zone/1017701 +# ZonePlus - Modern Spatial Query Edition + +[![Documentation](https://img.shields.io/badge/docs-site-blue)](https://devforum.roblox.com/t/zone/1017701) + +ZonePlus is a powerful and efficient zone detection library for Roblox, now fully modernized to leverage Roblox's latest spatial query APIs. + +## šŸš€ What's New - Modern Spatial Query Update (v4.0.0) + +ZonePlus has been completely modernized to use Roblox's current spatial query APIs: + +### Key Improvements + +āœ… **Modern Spatial Query APIs** + +- Replaced deprecated `Region3` with `CFrame + Size` approach +- Full integration with `WorldRoot:GetPartBoundsInBox` +- Full integration with `WorldRoot:GetPartBoundsInRadius` for spherical zones +- Full integration with `WorldRoot:GetPartsInPart` for precise geometry checks + +āœ… **Updated FilterType Enums** + +- Migrated from deprecated `Whitelist/Blacklist` to modern `Include/Exclude` +- Optimized `OverlapParams` reuse for better performance + +āœ… **New Zone Shape Support** + +- `Zone.fromBox(cframe, size)` - Optimized box-shaped zones using `GetPartBoundsInBox` +- `Zone.fromSphere(position, radius)` - Optimized spherical zones using `GetPartBoundsInRadius` +- Auto-detection for optimal spatial query method based on zone geometry + +āœ… **Performance Optimizations** + +- Reusable `OverlapParams` objects to reduce garbage collection +- Smart spatial query method selection based on zone shape +- Efficient filtering using modern collision detection + +## šŸ“š Spatial Query API Comparison + +### Roblox Spatial Query Methods Used + +| API Method | Use Case | Performance | +| ----------------------- | ---------------------------------- | --------------------------- | +| `GetPartBoundsInBox` | Box-shaped zones (rotated/aligned) | ⚔ Very Fast (bounding box) | +| `GetPartBoundsInRadius` | Spherical/radial zones | ⚔ Very Fast (bounding box) | +| `GetPartsInPart` | Precise geometry checks | āš ļø Slower (precise overlap) | + +ZonePlus automatically selects the best method based on your zone configuration. + +## šŸŽÆ Quick Start + +```lua +local Zone = require(game.ReplicatedStorage.Zone) + +-- Create a traditional zone from a container +local container = workspace.SafeZone +local zone = Zone.new(container) + +-- Or create an optimized box zone +local boxZone = Zone.fromBox( + CFrame.new(0, 10, 0), + Vector3.new(50, 20, 50) +) + +-- Or create an optimized spherical zone +local sphereZone = Zone.fromSphere( + Vector3.new(0, 10, 0), + 25 -- radius +) + +-- Connect to events +zone.playerEntered:Connect(function(player) + print(player.Name .. " entered the zone!") +end) + +zone.playerExited:Connect(function(player) + print(player.Name .. " left the zone!") +end) +``` + +## šŸ”§ Advanced Configuration + +### Zone Shape Optimization + +```lua +-- Manually set the spatial query method +zone:setZoneShape("Box") -- Use GetPartBoundsInBox +zone:setZoneShape("Sphere") -- Use GetPartBoundsInRadius +zone:setZoneShape("Auto") -- Auto-detect (default) +``` + +### Detection Modes + +```lua +-- Set detection precision +zone:setAccuracy("High") -- 0.1 second checks +zone:setAccuracy("Medium") -- 0.5 second checks +zone:setAccuracy("Low") -- 1.0 second checks +zone:setAccuracy("Precise") -- Every frame (0.0) + +-- Set detection method +zone:setDetection("Centre") -- Check HumanoidRootPart only (faster) +zone:setDetection("WholeBody") -- Check entire character (more accurate) +``` + +## šŸ“– Documentation + +For comprehensive documentation including: + +- API reference +- Advanced examples +- Best practices +- Migration guides + +Visit the [ZonePlus Documentation Site](https://devforum.roblox.com/t/zone/1017701) + +## šŸ’” Performance Tips + +1. **Reuse OverlapParams**: ZonePlus now automatically reuses OverlapParams objects +2. **Choose the right shape**: Use `fromSphere()` for radial zones, `fromBox()` for rectangular zones +3. **Optimize accuracy**: Use lower accuracy settings when possible +4. **Use Centre detection**: For large zones with many players, Centre detection is much faster + +## šŸ”„ Migration from Old Region3 Code + +If you were using older ZonePlus versions, your existing code will continue to work! The modernization is backward-compatible. However, we recommend: + +1. Using the new `fromBox()` and `fromSphere()` constructors for new zones +2. Checking that zones work as expected (internal representation changed from Region3 to CFrame+Size) + +## šŸ› Known Limitations + +- Collision group edge cases: Parts in the same collision group may not always be detected (Roblox engine limitation) +- Performance with distant parts: Many parts far from query regions can cause performance degradation (Roblox engine limitation) + +## šŸ“ License + +See LICENSE file for details. + +## šŸ™ Credits + +Original ZonePlus by nanoblox +Modern Spatial Query Update - 2025 + +--- + +For support and discussions, visit the [DevForum thread](https://devforum.roblox.com/t/zone/1017701) diff --git a/docs/changelog.md b/docs/changelog.md index be7b37b..c36dbaa 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -1,172 +1,228 @@ +## [4.0.0] - October 6 2025 - Modern Spatial Query Edition + +### Major Update + +This release fully modernizes ZonePlus to leverage Roblox's latest spatial query APIs for better performance and maintainability. + +### Added + +- `Zone.fromBox(cframe, size)` - Creates optimized box-shaped zones using GetPartBoundsInBox +- `Zone.fromSphere(position, radius)` - Creates optimized spherical zones using GetPartBoundsInRadius +- `Zone:setZoneShape(enumIdOrName)` - Allows manual control over spatial query method +- `ZoneShape` enum with options: Auto, Box, Sphere +- Modern OverlapParams management with automatic reuse for better performance +- Comprehensive documentation on spatial query API usage + +### Changed + +- **BREAKING**: Replaced deprecated `Region3` with modern `CFrame + Size` approach + - `zone.region` is now split into `zone.regionCFrame` and `zone.regionSize` + - `zone.exactRegion` is now `zone.exactRegionCFrame` and `zone.exactRegionSize` +- Updated all `FilterType.Whitelist/Blacklist` to modern `FilterType.Include/Exclude` +- Optimized OverlapParams reuse throughout the codebase +- Enhanced spatial query method selection based on zone geometry +- Improved README with modern API comparison and performance tips + +### Migration Guide + +If you're upgrading from 3.x: + +1. If you directly accessed `zone.region`, update to use `zone.regionCFrame` and `zone.regionSize` +2. All existing zone creation methods (`Zone.new`, `Zone.fromRegion`) continue to work +3. Consider using new `Zone.fromBox()` and `Zone.fromSphere()` for new zones + +### Performance Improvements + +- Better OverlapParams reuse reduces garbage collection +- Smart spatial query method selection improves detection speed +- Modern API usage provides better engine-level optimizations + +--- + ## [3.2.0] - September 7 2021 + ### Added -- ``Zone:onItemEnter(characterOrBasePart, callbackFunction)`` -- ``Zone:onItemExit(characterOrBasePart, callbackFunction)`` + +- `Zone:onItemEnter(characterOrBasePart, callbackFunction)` +- `Zone:onItemExit(characterOrBasePart, callbackFunction)` - An error warning when a zone is constructed using parts that don't belong to the Default collision group - Support for non-basepart HeadParts ### Changed + - Reorganised checker parts ### Fixed -- A bug preventing the disconnection of tracked character parts which resulted in a slight memory leak whenever a player reset or changed bodyparts +- A bug preventing the disconnection of tracked character parts which resulted in a slight memory leak whenever a player reset or changed bodyparts +--- --------- ## [3.1.0] - August 28 2021 + ### Added -- ``Zone.fromRegion(cframe, size)`` -- ``zone:relocate()`` - Non-workspace zones are finally a possibility! Simply call this and the zones container will be moved into a WorldModel outside of Workspace. + +- `Zone.fromRegion(cframe, size)` +- `zone:relocate()` - Non-workspace zones are finally a possibility! Simply call this and the zones container will be moved into a WorldModel outside of Workspace. - CollectiveWorldModel module -- ``zone.hasRelocated`` property -- ``zone.worldModel`` property -- ``zone.relocationContainer`` property -- ``CollectiveWorldModel.setupWorldModel(zone)`` -- ``CollectiveWorldModel:GetPartBoundsInBox(cframe, size, overlapParams)`` -- ``CollectiveWorldModel:GetPartBoundsInRadius(position, radius, overlapParams)`` +- `zone.hasRelocated` property +- `zone.worldModel` property +- `zone.relocationContainer` property +- `CollectiveWorldModel.setupWorldModel(zone)` +- `CollectiveWorldModel:GetPartBoundsInBox(cframe, size, overlapParams)` +- `CollectiveWorldModel:GetPartBoundsInRadius(position, radius, overlapParams)` - ``CollectiveWorldModel:GetPartsInPart(part, overlapParams)` ### Changed -- ``Zone.new(zoneGroup)`` to ``Zone.new(container)`` -- ``zone.group`` property to ``zone.container`` + +- `Zone.new(zoneGroup)` to `Zone.new(container)` +- `zone.group` property to `zone.container` ### Fixed -- "ZoneController hrp is nil" bug +- "ZoneController hrp is nil" bug +--- --------- ## [3.0.0] - August 27 2021 + ### Added -- ``Zone:trackItem(characterOrBasePart)`` -- ``Zone:untrackItem(characterOrBasePart)`` -- ``Zone.itemEntered`` event -- ``Zone.itemExited`` event -- ``Zone:findItem(characterOrBasePart)`` -- ``ZoneController.setGroup(settingsGroupName, properties)`` -- ``ZoneController.getGroup(settingsGroupName)`` -- ``SettingsGroup.onlyEnterOnceExitedAll`` property -- ``Zone:bindToGroup(settingsGroupName)`` -- ``Zone:unbindFromGroup(settingsGroupName)`` -- ``Zone.settingsGroupName`` property -- ``Zone:findPoint(position)`` -- ``ZoneController.getCharacterSize(character)`` + +- `Zone:trackItem(characterOrBasePart)` +- `Zone:untrackItem(characterOrBasePart)` +- `Zone.itemEntered` event +- `Zone.itemExited` event +- `Zone:findItem(characterOrBasePart)` +- `ZoneController.setGroup(settingsGroupName, properties)` +- `ZoneController.getGroup(settingsGroupName)` +- `SettingsGroup.onlyEnterOnceExitedAll` property +- `Zone:bindToGroup(settingsGroupName)` +- `Zone:unbindFromGroup(settingsGroupName)` +- `Zone.settingsGroupName` property +- `Zone:findPoint(position)` +- `ZoneController.getCharacterSize(character)` ### Changed + - Internal behaviour to use the new Spatial [Query API](https://devforum.roblox.com/t/introducing-overlapparams-new-spatial-query-api/1435720) instead of the Region3 API. -- The default Detection from ``Automatic`` to ``Centre``. -- The behaviour of Detection ``Centre`` to include the whole HumanoidRootPart instead of a singular Vector within (this was required due to the new Spatial Query API). -- ``Zone:findPart`` now returns array ``touchingZoneParts`` as its second value. -- ``Maid`` to [``Janitor``](https://github.com/howmanysmall/Janitor) by howmanysmall. -- ``Signal`` to [``GoodSignal``](https://devforum.roblox.com/t/lua-signal-class-comparison-optimal-goodsignal-class/1387063) by stravant. -- ``ZoneController.getTouchingZones(player)`` to ``ZoneController.getTouchingZones(characterOrBasePart)``. +- The default Detection from `Automatic` to `Centre`. +- The behaviour of Detection `Centre` to include the whole HumanoidRootPart instead of a singular Vector within (this was required due to the new Spatial Query API). +- `Zone:findPart` now returns array `touchingZoneParts` as its second value. +- `Maid` to [`Janitor`](https://github.com/howmanysmall/Janitor) by howmanysmall. +- `Signal` to [`GoodSignal`](https://devforum.roblox.com/t/lua-signal-class-comparison-optimal-goodsignal-class/1387063) by stravant. +- `ZoneController.getTouchingZones(player)` to `ZoneController.getTouchingZones(characterOrBasePart)`. ### Removed -- RotatedRegion3 -- ``ZoneController.getCharacterRegion`` -- ``ZoneController.verifyTouchingParts`` -- ``ZoneController.vectorIsBetweenYBounds`` -- ``ZoneController.getHeightOfParts`` -- ``Automatic`` Detection Enum. +- RotatedRegion3 +- `ZoneController.getCharacterRegion` +- `ZoneController.verifyTouchingParts` +- `ZoneController.vectorIsBetweenYBounds` +- `ZoneController.getHeightOfParts` +- `Automatic` Detection Enum. +--- --------- ## [2.2.3] - June 17 2021 + ### Fixed -- The incorrect disabling of Seats and VehicleSeats within Part Zones. +- The incorrect disabling of Seats and VehicleSeats within Part Zones. +--- --------- ## [2.2.2] - June 4 2021 + ### Improved -- The accounting of character parts when removed/added via systems like HumanoidDescriptions. +- The accounting of character parts when removed/added via systems like HumanoidDescriptions. +--- --------- ## [2.2.1] - May 21 2021 + ### Added -- Compatibility for Deferred Events +- Compatibility for Deferred Events +--- --------- ## [2.1.3] - May 7 2021 + ### Fixed -- A bug that occured when disconnecting localPlayer events +- A bug that occured when disconnecting localPlayer events +--- --------- ## [2.1.2] - April 15 2021 -### Fixed -- ``playerExiting`` not firing when the player dies and respawns immidately within the zone. -- A rare nil checking bug within ``getTouchingZones`` in ``ZoneController``. +### Fixed +- `playerExiting` not firing when the player dies and respawns immidately within the zone. +- A rare nil checking bug within `getTouchingZones` in `ZoneController`. --------- +--- ## [2.1.1] - April 7 2021 -### Fixed -- nil comparison within ZoneController getTouchingZones line 450 +### Fixed +- nil comparison within ZoneController getTouchingZones line 450 --------- +--- ## [2.1.0] - March 5 2021 + ### Added + - Detection Enum -- ``zone.enterDetection`` -- ``zone.exitDetection`` -- ``zone:setDetection(enumItemName)`` +- `zone.enterDetection` +- `zone.exitDetection` +- `zone:setDetection(enumItemName)` - An Optimisation section to Introduction - - --------- +--- ## [2.0.0] - January 19 2021 + ### Added + - Non-player part checking! (see methods below) - Infinite zone volume, zero change in performance - zones can now be as large as you like with no additional impact to performance assuming characters/parts entering the zone remain their normal size or relatively small - Zones now support MeshParts and UnionOperations (however it's recommended to use simple parts where possible as the former require additional raycast checks) - **Methods** - - ``findLocalPlayer()`` - - ``findPlayer(player)`` - - ``findPart(basePart)`` - - ``getPlayers()`` - - ``getParts()`` - - ``setAccuracy(enumIdOrName)`` -- this enables you to customise the frequency of checks with enums 'Precise', 'High', 'Medium' and 'Low' - - 'Destroy' alias of 'destroy' + - `findLocalPlayer()` + - `findPlayer(player)` + - `findPart(basePart)` + - `getPlayers()` + - `getParts()` + - `setAccuracy(enumIdOrName)` -- this enables you to customise the frequency of checks with enums 'Precise', 'High', 'Medium' and 'Low' + - 'Destroy' alias of 'destroy' - **Events** - - ``localPlayerEntered`` - - ``localPlayerExited`` - - ``playerEntered`` - - ``playerExited`` - - ``partEntered`` - - ``partExited`` + - `localPlayerEntered` + - `localPlayerExited` + - `playerEntered` + - `playerExited` + - `partEntered` + - `partExited` ### Changed + - A players whole body is now considered as apposed to just their central position - Region checking significantly optimised (e.g. the zones region now rest on the voxel grid) - Zones now act as a 'collective' which has significantly improved and optimised player and localplayer detection -- Removed all original aliases and events, including ``:initLoop()`` which no longer has to be called (connections are detected and handled internally automatically) +- Removed all original aliases and events, including `:initLoop()` which no longer has to be called (connections are detected and handled internally automatically) - Replaced frustrating require() dependencies with static modules - Made Zone the parent module and others as descendants -- Removed the ``additonalHeight`` constructor argument - this caused confusion and added additional complexities to support -- ``:getRandomPoint()`` now returns ``randomVector, touchingGroupParts`` instead of ``randomCFrame, hitPart, hitIntersection`` -- ``zone.groupParts`` to ``zone.zoneParts`` +- Removed the `additonalHeight` constructor argument - this caused confusion and added additional complexities to support +- `:getRandomPoint()` now returns `randomVector, touchingGroupParts` instead of `randomCFrame, hitPart, hitIntersection` +- `zone.groupParts` to `zone.zoneParts` ### Fixed -- Rotational and complex geometry detection -- ``getRandomPoints()`` inaccuracies - +- Rotational and complex geometry detection +- `getRandomPoints()` inaccuracies ``` -- This constructs a zone based upon a group of parts in Workspace and listens for when a player enters and exits this group @@ -195,4 +251,4 @@ end) zone.itemExited:Connect(function(item) print(("%s exited the zone!"):format(item.Name)) end) -``` \ No newline at end of file +``` diff --git a/src/Testers/ZonePlusTest.client.lua b/src/Testers/ZonePlusTest.client.lua new file mode 100644 index 0000000..eb9cd97 --- /dev/null +++ b/src/Testers/ZonePlusTest.client.lua @@ -0,0 +1,297 @@ +-- ZonePlus Modern API Test Script +-- Place this in StarterPlayer > StarterPlayerScripts or StarterGui + +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local Players = game:GetService("Players") +local RunService = game:GetService("RunService") + +-- Wait for Zone module (adjust path as needed) +local Zone = require(ReplicatedStorage:WaitForChild("Zone")) + +local player = Players.LocalPlayer +local character = player.Character or player.CharacterAdded:Wait() +local humanoidRootPart = character:WaitForChild("HumanoidRootPart") + +-- Wait for test environment +local testFolder = workspace:WaitForChild("ZonePlusTests") +local boxZoneContainer = testFolder:WaitForChild("BoxZone") +local sphereZoneContainer = testFolder:WaitForChild("SphereZone") +local complexZoneContainer = testFolder:WaitForChild("ComplexZone") +local cylinderGroupedContainer = testFolder:WaitForChild("CylinderGroupedZone") +local cylinderUngroupedContainer = testFolder:WaitForChild("CylinderUngroupedZone") + +print("šŸš€ ZonePlus v4.0.0 - Modern Spatial Query Edition Test") +print("=" .. string.rep("=", 60)) + +-- Test 1: Traditional Zone from Container (Modern APIs under the hood) +print("šŸ“¦ Creating Box Zone from container...") +local boxZone = Zone.new(boxZoneContainer) +boxZone:setAccuracy("High") +boxZone:setDetection("Centre") +print("āœ… Box Zone created with traditional method (Uses modern GetPartBoundsInBox internally)") + +-- Test 2: New Optimized Box Zone +print("\nšŸ“¦ Creating optimized Box Zone...") +local optimizedBoxZone = Zone.fromBox(CFrame.new(0, 10, 0), Vector3.new(30, 20, 30)) +optimizedBoxZone:setAccuracy("High") +print("āœ… Optimized Box Zone created with Zone.fromBox()") + +-- Test 3: Sphere Zone from existing container +print("\nšŸ”µ Creating Sphere Zone from container...") +local sphereZone = Zone.new(sphereZoneContainer) +sphereZone:setAccuracy("High") +sphereZone:setDetection("Centre") +print("āœ… Sphere Zone created (Ball shape auto-detected)") + +-- Test 4: Complex Zone (will use GetPartsInPart for precision) +print("\nšŸ”§ Creating Complex Zone...") +local complexZone = Zone.new(complexZoneContainer) +complexZone:setAccuracy("High") +complexZone:setDetection("Centre") +print("āœ… Complex Zone created (Uses GetPartsInPart for precision)") + +-- Test 5: Cylinder Grouped Zone +print("\nšŸ›¢ļø Creating Cylinder Grouped Zone...") +local cylinderGroupedZone = Zone.new(cylinderGroupedContainer) +cylinderGroupedZone:setAccuracy("High") +cylinderGroupedZone:setDetection("Centre") +print("āœ… Cylinder Grouped Zone created (Full cylinder with top/bottom)") + +-- Test 6: Cylinder Ungrouped Zones (Each part gets its own zone with different color) +print("\nšŸ›¢ļø Creating Cylinder Ungrouped Zones...") +local ungroupedZones = {} +local ungroupedColors = { + Color3.fromRGB(170, 0, 255), -- Purple + Color3.fromRGB(255, 0, 170), -- Magenta + Color3.fromRGB(0, 255, 170), -- Cyan + Color3.fromRGB(255, 170, 0), -- Orange-Yellow + Color3.fromRGB(170, 255, 0), -- Lime +} + +local partIndex = 1 +local children = cylinderUngroupedContainer:GetChildren() +print("šŸ” Found " .. #children .. " children in CylinderUngroupedContainer") + +for _, part in children do + print(" Checking child: " .. part.Name .. " (Type: " .. part.ClassName .. ")") + if part:IsA("BasePart") then + print(" Part details - Position: " .. tostring(part.Position) .. ", Size: " .. tostring(part.Size)) + print( + " Part properties - CanCollide: " + .. tostring(part.CanCollide) + .. ", Anchored: " + .. tostring(part.Anchored) + ) + + local zone = Zone.new(part) + zone:setAccuracy("High") + zone:setDetection("Centre") + + -- Verify zone was created properly + print(" Zone created - ZoneID: " .. zone.zoneId) + print(" Zone parts count: " .. #zone.zoneParts) + if #zone.zoneParts > 0 then + print(" Zone part[1]: " .. zone.zoneParts[1].Name) + end + + local color = ungroupedColors[((partIndex - 1) % #ungroupedColors) + 1] + table.insert(ungroupedZones, { + zone = zone, + part = part, -- Store reference to the part for debugging + name = "Ungrouped Part " .. partIndex .. " (" .. part.Name .. ")", + inZone = false, + color = color, + }) + print( + "āœ… Created zone #" + .. partIndex + .. " for part: " + .. part.Name + .. " with color RGB(" + .. math.floor(color.R * 255) + .. ", " + .. math.floor(color.G * 255) + .. ", " + .. math.floor(color.B * 255) + .. ")" + ) + partIndex = partIndex + 1 + end +end + +print("šŸ“Š Total ungrouped zones created: " .. #ungroupedZones) + +-- Track zone status +local zoneStatus = { + boxZone = { name = "Box Zone (Container)", inZone = false, color = Color3.fromRGB(0, 170, 255) }, + sphereZone = { name = "Sphere Zone", inZone = false, color = Color3.fromRGB(255, 85, 127) }, + complexZone = { name = "Complex Zone", inZone = false, color = Color3.fromRGB(127, 255, 85) }, + cylinderGroupedZone = { name = "Cylinder Grouped Zone", inZone = false, color = Color3.fromRGB(255, 170, 0) }, +} + +-- Connect to zone events +boxZone.localPlayerEntered:Connect(function() + zoneStatus.boxZone.inZone = true + print("\n🟢 ENTERED:", zoneStatus.boxZone.name) +end) + +boxZone.localPlayerExited:Connect(function() + zoneStatus.boxZone.inZone = false + print("\nšŸ”“ EXITED:", zoneStatus.boxZone.name) +end) + +sphereZone.localPlayerEntered:Connect(function() + zoneStatus.sphereZone.inZone = true + print("\n🟢 ENTERED:", zoneStatus.sphereZone.name) +end) + +sphereZone.localPlayerExited:Connect(function() + zoneStatus.sphereZone.inZone = false + print("\nšŸ”“ EXITED:", zoneStatus.sphereZone.name) +end) + +complexZone.localPlayerEntered:Connect(function() + zoneStatus.complexZone.inZone = true + print("\n🟢 ENTERED:", zoneStatus.complexZone.name) +end) + +complexZone.localPlayerExited:Connect(function() + zoneStatus.complexZone.inZone = false + print("\nšŸ”“ EXITED:", zoneStatus.complexZone.name) +end) + +cylinderGroupedZone.localPlayerEntered:Connect(function() + zoneStatus.cylinderGroupedZone.inZone = true + print("\n🟢 ENTERED:", zoneStatus.cylinderGroupedZone.name) +end) + +cylinderGroupedZone.localPlayerExited:Connect(function() + zoneStatus.cylinderGroupedZone.inZone = false + print("\nšŸ”“ EXITED:", zoneStatus.cylinderGroupedZone.name) +end) + +-- Connect events for each ungrouped zone +print("\nšŸ”— Connecting events for " .. #ungroupedZones .. " ungrouped zones...") +for index, zoneData in ungroupedZones do + print( + " Connecting events for zone #" + .. index + .. ": " + .. zoneData.name + .. " (ZoneID: " + .. zoneData.zone.zoneId + .. ")" + ) + + zoneData.zone.localPlayerEntered:Connect(function() + zoneData.inZone = true + print("\n🟢 ENTERED:", zoneData.name, "| Part:", zoneData.part.Name, "| Zone:", zoneData.zone.zoneId) + end) + + zoneData.zone.localPlayerExited:Connect(function() + zoneData.inZone = false + print("\nšŸ”“ EXITED:", zoneData.name, "| Part:", zoneData.part.Name, "| Zone:", zoneData.zone.zoneId) + end) +end +print("āœ… All ungrouped zone events connected!") + +boxZone.itemEntered:Connect(function(item) + print("šŸ“¦ Item entered Box Zone:", item.Name) +end) + +boxZone.itemExited:Connect(function(item) + print("šŸ“¦ Item exited Box Zone:", item.Name) +end) + +-- Visual feedback - highlight character when in zones +local highlight = Instance.new("Highlight") +highlight.FillTransparency = 0.5 +highlight.OutlineTransparency = 0 +highlight.Parent = character + +RunService.Heartbeat:Connect(function() + -- Update highlight color based on which zone player is in + local inAnyZone = false + local currentColor = Color3.new(1, 1, 1) + + -- Check main zones + for _, status in zoneStatus do + if status.inZone then + inAnyZone = true + currentColor = status.color + break + end + end + + -- Check ungrouped zones + if not inAnyZone then + for _, zoneData in ungroupedZones do + if zoneData.inZone then + inAnyZone = true + currentColor = zoneData.color + break + end + end + end + + highlight.Enabled = inAnyZone + if inAnyZone then + highlight.FillColor = currentColor + highlight.OutlineColor = currentColor + end +end) + +-- Print test summary +task.wait(1) +print("\n" .. string.rep("-", 50)) +print("\nšŸŽÆ Test Summary:") +print("• Box Zone: Using modern GetPartBoundsInBox") +print("• Sphere Zone: Auto-detected Ball shape") +print("• Complex Zone: Using GetPartsInPart for precision") +print("• Cylinder Grouped Zone: Full cylinder with top/bottom") +print("• Cylinder Ungrouped Zones: " .. #ungroupedZones .. " separate zones with unique colors") +print("\nšŸ’” Walk into zones to test detection!") +print("šŸ’” Your character will highlight in zone colors:") +print(" šŸ”µ Blue = Box Zone") +print(" šŸ’— Pink = Sphere Zone") +print(" šŸ’š Green = Complex Zone") +print(" 🟠 Orange = Cylinder Grouped Zone") +print(" 🌈 Various Colors = Ungrouped Zones (Purple, Magenta, Cyan, Orange-Yellow, Lime)") +print("šŸ’” Watch the Output for enter/exit events") + +-- Performance monitoring and zone status debugging +local lastUpdate = tick() +local lastStatusCheck = tick() +local frameCount = 0 +RunService.Heartbeat:Connect(function() + frameCount = frameCount + 1 + if tick() - lastUpdate >= 5 then + local fps = math.floor(frameCount / (tick() - lastUpdate)) + print(string.format("šŸ“Š Performance: %d FPS", fps)) + lastUpdate = tick() + frameCount = 0 + end + + -- Debug: Check zone status every 3 seconds + if tick() - lastStatusCheck >= 3 then + local playerPos = humanoidRootPart.Position + print("\nšŸ” DEBUG: Player position: " .. tostring(playerPos)) + print(" Ungrouped zones status:") + for index, zoneData in ungroupedZones do + local distance = (zoneData.part.Position - playerPos).Magnitude + print( + string.format( + " Zone #%d (%s): inZone=%s, distance=%.2f", + index, + zoneData.part.Name, + tostring(zoneData.inZone), + distance + ) + ) + end + lastStatusCheck = tick() + end +end) + +print("\nāœ… ZonePlus test script initialized successfully!") +print("šŸ“ Watch the Output window for zone status updates") diff --git a/src/Zone/Enum/ZoneShape.lua b/src/Zone/Enum/ZoneShape.lua new file mode 100644 index 0000000..3c671de --- /dev/null +++ b/src/Zone/Enum/ZoneShape.lua @@ -0,0 +1,8 @@ +-- ZoneShape enum for optimized spatial query method selection +-- This allows zones to use different spatial query APIs based on their shape +-- enumName, enumValue, additionalProperty (query method) +return { + { "Box", 1, "GetPartBoundsInBox" }, -- Optimal for box-shaped zones (aligned or rotated) + { "Sphere", 2, "GetPartBoundsInRadius" }, -- Optimal for spherical/circular zones + { "Auto", 3 }, -- Automatically determines best method based on zone parts +} diff --git a/src/Zone/VERSION.lua b/src/Zone/VERSION.lua index 7081d1f..81ae663 100644 --- a/src/Zone/VERSION.lua +++ b/src/Zone/VERSION.lua @@ -1 +1,2 @@ --- v3.2.0 \ No newline at end of file +-- v4.0.0 +return "4.0.0" diff --git a/src/Zone/ZoneController/Tracker.lua b/src/Zone/ZoneController/Tracker.lua index d3749d9..ca94bb9 100644 --- a/src/Zone/ZoneController/Tracker.lua +++ b/src/Zone/ZoneController/Tracker.lua @@ -7,8 +7,6 @@ local heartbeat = runService.Heartbeat local Signal = require(script.Parent.Parent.Signal) local Janitor = require(script.Parent.Parent.Janitor) - - -- PUBLIC local Tracker = {} Tracker.__index = Tracker @@ -29,8 +27,6 @@ Tracker.bodyPartsToIgnore = { RightFoot = true, } - - -- FUNCTIONS function Tracker.getCombinedTotalVolumes() local combinedVolume = 0 @@ -43,24 +39,24 @@ end function Tracker.getCharacterSize(character) local head = character and character:FindFirstChild("Head") local hrp = character and character:FindFirstChild("HumanoidRootPart") - if not(hrp and head) then return nil end + if not (hrp and head) then + return nil + end if not head:IsA("BasePart") then head = hrp end local headY = head.Size.Y local hrpSize = hrp.Size local charSize = (hrpSize * Vector3.new(2, 2, 1)) + Vector3.new(0, headY, 0) - local charCFrame = hrp.CFrame * CFrame.new(0, headY/2 - hrpSize.Y/2, 0) + local charCFrame = hrp.CFrame * CFrame.new(0, headY / 2 - hrpSize.Y / 2, 0) return charSize, charCFrame end - - -- CONSTRUCTOR function Tracker.new(name) local self = {} setmetatable(self, Tracker) - + self.name = name self.totalVolume = 0 self.parts = {} @@ -83,7 +79,7 @@ function Tracker.new(name) end self.characters = characters end - + local function playerAdded(player) local function charAdded(character) local humanoid = character:WaitForChild("Humanoid", 3) @@ -107,18 +103,16 @@ function Tracker.new(name) self.exitDetections[removingCharacter] = nil end) end - + players.PlayerAdded:Connect(playerAdded) for _, player in pairs(players:GetPlayers()) do playerAdded(player) end - + players.PlayerRemoving:Connect(function(player) updatePlayerCharacters() self:update() end) - - elseif name == "item" then local function updateItem(itemDetail, newValue) if itemDetail.isCharacter then @@ -142,8 +136,6 @@ function Tracker.new(name) return self end - - -- METHODS function Tracker:_preventMultiFrameUpdates(methodName, ...) -- This prevents the funtion being called twice within a single frame @@ -178,12 +170,12 @@ function Tracker:update() if self:_preventMultiFrameUpdates("update") then return end - + self.totalVolume = 0 self.parts = {} self.partToItem = {} self.items = {} - + -- This tracks the bodyparts of a character for character, _ in pairs(self.characters) do local charSize = Tracker.getCharacterSize(character) @@ -191,20 +183,23 @@ function Tracker:update() continue end local rSize = charSize - local charVolume = rSize.X*rSize.Y*rSize.Z + local charVolume = rSize.X * rSize.Y * rSize.Z self.totalVolume += charVolume - - local characterJanitor = self.janitor:add(Janitor.new(), "destroy", "trackCharacterParts-"..self.name) + + local characterJanitor = self.janitor:add(Janitor.new(), "destroy", "trackCharacterParts-" .. self.name) local function updateTrackerOnParentChanged(instance) - characterJanitor:add(instance.AncestryChanged:Connect(function() - if not instance:IsDescendantOf(game) then - if instance.Parent == nil and characterJanitor ~= nil then - characterJanitor:destroy() - characterJanitor = nil - self:update() + characterJanitor:add( + instance.AncestryChanged:Connect(function() + if not instance:IsDescendantOf(game) then + if instance.Parent == nil and characterJanitor ~= nil then + characterJanitor:destroy() + characterJanitor = nil + self:update() + end end - end - end), "Disconnect") + end), + "Disconnect" + ) end for _, part in pairs(character:GetChildren()) do @@ -221,20 +216,18 @@ function Tracker:update() -- This tracks any additional baseParts for additionalPart, _ in pairs(self.baseParts) do local rSize = additionalPart.Size - local partVolume = rSize.X*rSize.Y*rSize.Z + local partVolume = rSize.X * rSize.Y * rSize.Z self.totalVolume += partVolume self.partToItem[additionalPart] = additionalPart table.insert(self.parts, additionalPart) table.insert(self.items, additionalPart) end - - -- This creates the whitelist so that + + -- This creates the include filter params to optimize spatial queries self.whitelistParams = OverlapParams.new() - self.whitelistParams.FilterType = Enum.RaycastFilterType.Whitelist + self.whitelistParams.FilterType = Enum.RaycastFilterType.Include self.whitelistParams.MaxParts = #self.parts self.whitelistParams.FilterDescendantsInstances = self.parts end - - -return Tracker \ No newline at end of file +return Tracker diff --git a/src/Zone/ZoneController/init.lua b/src/Zone/ZoneController/init.lua index 57be585..3c9182b 100644 --- a/src/Zone/ZoneController/init.lua +++ b/src/Zone/ZoneController/init.lua @@ -1,8 +1,6 @@ -- CONFIG local WHOLE_BODY_DETECTION_LIMIT = 729000 -- This is roughly the volume where Region3 checks begin to exceed 0.5% in Script Performance - - -- LOCAL local Janitor = require(script.Parent.Janitor) local Enum_ = require(script.Parent.Enum) @@ -25,8 +23,6 @@ local heartbeat = runService.Heartbeat local heartbeatConnections = {} local localPlayer = runService:IsClient() and players.LocalPlayer - - -- PUBLIC local ZoneController = {} local trackers = {} @@ -34,8 +30,6 @@ trackers.player = Tracker.new("player") trackers.item = Tracker.new("item") ZoneController.trackers = trackers - - -- LOCAL FUNCTIONS local function dictLength(dictionary) local count = 0 @@ -57,7 +51,13 @@ end local heartbeatActions = { ["player"] = function(recommendedDetection) - return ZoneController._getZonesAndItems("player", activeZones, activeZonesTotalVolume, true, recommendedDetection) + return ZoneController._getZonesAndItems( + "player", + activeZones, + activeZonesTotalVolume, + true, + recommendedDetection + ) end, ["localPlayer"] = function(recommendedDetection) local zonesAndOccupants = {} @@ -78,17 +78,18 @@ local heartbeatActions = { end, } - - -- PRIVATE FUNCTIONS function ZoneController._registerZone(zone) - registeredZones[zone] = true + registeredZones[zone] = true local registeredJanitor = zone.janitor:add(Janitor.new(), "destroy") zone._registeredJanitor = registeredJanitor - registeredJanitor:add(zone.updated:Connect(function() - ZoneController._updateZoneDetails() - end), "Disconnect") - ZoneController._updateZoneDetails() + registeredJanitor:add( + zone.updated:Connect(function() + ZoneController._updateZoneDetails() + end), + "Disconnect" + ) + ZoneController._updateZoneDetails() end function ZoneController._deregisterZone(zone) @@ -106,7 +107,7 @@ function ZoneController._registerConnection(registeredZone, registeredTriggerTyp ZoneController._updateZoneDetails() end local currentTriggerCount = activeTriggers[registeredTriggerType] - activeTriggers[registeredTriggerType] = (currentTriggerCount and currentTriggerCount+1) or 1 + activeTriggers[registeredTriggerType] = (currentTriggerCount and currentTriggerCount + 1) or 1 registeredZone.activeTriggers[registeredTriggerType] = true if registeredZone.touchedConnectionActions[registeredTriggerType] then registeredZone:_formTouchedConnection(registeredTriggerType) @@ -139,7 +140,9 @@ end function ZoneController._formHeartbeat(registeredTriggerType) local heartbeatConnection = heartbeatConnections[registeredTriggerType] - if heartbeatConnection then return end + if heartbeatConnection then + return + end -- This will only ever connect once per triggerType per server -- This means instead of initiating a loop per-zone we can handle everything within -- a singular connection. This is particularly beneficial for player/item-orinetated @@ -178,17 +181,17 @@ function ZoneController._formHeartbeat(registeredTriggerType) if settingsGroup and settingsGroup.onlyEnterOnceExitedAll == true then --local currentOccupants = zone.occupants[registeredTriggerType] --if currentOccupants then - for newOccupant, _ in pairs(newOccupants) do - --if currentOccupants[newOccupant] then - local groupDetail = occupantsToBlock[zone.settingsGroupName] - if not groupDetail then - groupDetail = {} - occupantsToBlock[zone.settingsGroupName] = groupDetail - end - groupDetail[newOccupant] = zone - --end + for newOccupant, _ in pairs(newOccupants) do + --if currentOccupants[newOccupant] then + local groupDetail = occupantsToBlock[zone.settingsGroupName] + if not groupDetail then + groupDetail = {} + occupantsToBlock[zone.settingsGroupName] = groupDetail end - zonesToPotentiallyIgnore[zone] = newOccupants + groupDetail[newOccupant] = zone + --end + end + zonesToPotentiallyIgnore[zone] = newOccupants --end end end @@ -205,13 +208,13 @@ function ZoneController._formHeartbeat(registeredTriggerType) end -- This deduces what signals should be fired - local collectiveSignalsToFire = {{}, {}} + local collectiveSignalsToFire = { {}, {} } for zone, _ in pairs(activeZones) do if zone.activeTriggers[registeredTriggerType] then local zAccuracy = zone.accuracy local occupantsDict = zonesAndOccupants[zone] or {} local occupantsPresent = false - for k,v in pairs(occupantsDict) do + for k, v in pairs(occupantsDict) do occupantsPresent = true break end @@ -225,10 +228,10 @@ function ZoneController._formHeartbeat(registeredTriggerType) end -- This ensures all exited signals and called before entered signals - local indexToSignalType = {"Exited", "Entered"} + local indexToSignalType = { "Exited", "Entered" } for index, zoneAndOccupants in pairs(collectiveSignalsToFire) do local signalType = indexToSignalType[index] - local signalName = registeredTriggerType..signalType + local signalName = registeredTriggerType .. signalType for zone, occupants in pairs(zoneAndOccupants) do local signal = zone[signalName] if signal then @@ -290,7 +293,13 @@ function ZoneController._updateZoneDetails() end end -function ZoneController._getZonesAndItems(trackerName, zonesDictToCheck, zoneCustomVolume, onlyActiveZones, recommendedDetection) +function ZoneController._getZonesAndItems( + trackerName, + zonesDictToCheck, + zoneCustomVolume, + onlyActiveZones, + recommendedDetection +) local totalZoneVolume = zoneCustomVolume if not totalZoneVolume then for zone, _ in pairs(zonesDictToCheck) do @@ -324,7 +333,8 @@ function ZoneController._getZonesAndItems(trackerName, zonesDictToCheck, zoneCus -- checks directly within each zone to determine players inside for zone, _ in pairs(zonesDictToCheck) do if not onlyActiveZones or zone.activeTriggers[trackerName] then - local result = CollectiveWorldModel:GetPartBoundsInBox(zone.region.CFrame, zone.region.Size, tracker.whitelistParams) + local result = + CollectiveWorldModel:GetPartBoundsInBox(zone.regionCFrame, zone.regionSize, tracker.whitelistParams) local finalItemsDict = {} for _, itemOrChild in pairs(result) do local correspondingItem = tracker.partToItem[itemOrChild] @@ -348,8 +358,6 @@ function ZoneController._getZonesAndItems(trackerName, zonesDictToCheck, zoneCus return zonesAndOccupants end - - -- PUBLIC FUNCTIONS function ZoneController.getZones() local registeredZonesArray = {} @@ -396,7 +404,9 @@ function ZoneController.getTouchingZones(item, onlyActiveZones, recommendedDetec table.insert(bodyPartsToCheck, hrp) end end - if not itemSize or not itemCFrame then return {} end + if not itemSize or not itemCFrame then + return {} + end --[[ local part = Instance.new("Part") @@ -413,7 +423,7 @@ function ZoneController.getTouchingZones(item, onlyActiveZones, recommendedDetec local partToZoneDict = (onlyActiveZones and activePartToZone) or allPartToZone local boundParams = OverlapParams.new() - boundParams.FilterType = Enum.RaycastFilterType.Whitelist + boundParams.FilterType = Enum.RaycastFilterType.Include boundParams.MaxParts = #partsTable boundParams.FilterDescendantsInstances = partsTable @@ -440,9 +450,8 @@ function ZoneController.getTouchingZones(item, onlyActiveZones, recommendedDetec local totalRemainingBoundParts = #boundPartsThatRequirePreciseChecks local precisePartsCount = 0 if totalRemainingBoundParts > 0 then - local preciseParams = OverlapParams.new() - preciseParams.FilterType = Enum.RaycastFilterType.Whitelist + preciseParams.FilterType = Enum.RaycastFilterType.Include preciseParams.MaxParts = totalRemainingBoundParts preciseParams.FilterDescendantsInstances = boundPartsThatRequirePreciseChecks @@ -472,7 +481,7 @@ function ZoneController.getTouchingZones(item, onlyActiveZones, recommendedDetec end end end - + local touchingZonesArray = {} local newExitDetection for zone, _ in pairs(zonesDict) do @@ -494,16 +503,14 @@ function ZoneController.setGroup(settingsGroupName, properties) group = {} settingsGroups[settingsGroupName] = group end - -- PUBLIC PROPERTIES -- group.onlyEnterOnceExitedAll = true - + -- PRIVATE PROPERTIES -- group._name = settingsGroupName group._memberZones = {} - if typeof(properties) == "table" then for k, v in pairs(properties) do group[k] = v @@ -529,6 +536,4 @@ function ZoneController.getWorkspaceContainer() return container end - - -return ZoneController \ No newline at end of file +return ZoneController diff --git a/src/Zone/init.lua b/src/Zone/init.lua index 62de930..c44668d 100644 --- a/src/Zone/init.lua +++ b/src/Zone/init.lua @@ -28,17 +28,15 @@ if not referencePresent then end Zone.enum = enum - - -- CONSTRUCTORS function Zone.new(container) local self = {} setmetatable(self, Zone) - + -- Validate container local INVALID_TYPE_WARNING = "The zone container must be a model, folder, basepart or table!" local containerType = typeof(container) - if not(containerType == "table" or containerType == "Instance") then + if not (containerType == "table" or containerType == "Instance") then error(INVALID_TYPE_WARNING) end @@ -46,6 +44,7 @@ function Zone.new(container) self.accuracy = enum.Accuracy.High self.autoUpdate = true self.respectUpdateQueue = true + self.zoneShape = enum.ZoneShape.Auto -- Determines optimal spatial query method --self.maxPartsAddition = 20 --self.ignoreRecommendedMaxParts = false @@ -56,7 +55,8 @@ function Zone.new(container) self.container = container self.zoneParts = {} self.overlapParams = {} - self.region = nil + self.regionCFrame = nil + self.regionSize = nil self.volume = nil self.boundMin = nil self.boundMax = nil @@ -86,7 +86,7 @@ function Zone.new(container) "player", "part", "localPlayer", - "item" + "item", } local triggerEvents = { "entered", @@ -99,8 +99,8 @@ function Zone.new(container) -- this enables us to determine when a developer connects to an event -- so that we can act accoridngly (i.e. begin or end a checker loop) local signal = janitor:add(Signal.new(true), "destroy") - local triggerEventUpper = triggerEvent:sub(1,1):upper()..triggerEvent:sub(2) - local signalName = triggerType..triggerEventUpper + local triggerEventUpper = triggerEvent:sub(1, 1):upper() .. triggerEvent:sub(2) + local signalName = triggerType .. triggerEventUpper self[signalName] = signal signal.connectionsChanged:Connect(function(increment) if triggerType == "localPlayer" and not localPlayer and increment == 1 then @@ -140,11 +140,11 @@ function Zone.new(container) janitor:add(function() ZoneController._deregisterZone(self) end, true) - + return self end -function Zone.fromRegion(cframe, size) +function Zone.fromRegion(cframe, size, zoneShape) local MAX_PART_SIZE = 2024 local container = Instance.new("Model") local function createCube(cubeCFrame, cubeSize) @@ -169,15 +169,43 @@ function Zone.fromRegion(cframe, size) end createCube(cframe, size) local zone = Zone.new(container) + if zoneShape then + zone.zoneShape = zoneShape + end zone:relocate() return zone end +function Zone.fromBox(cframe, size) + -- Creates an optimized box-shaped zone using GetPartBoundsInBox + -- Note: This calls fromRegion which auto-relocates the zone + local zone = Zone.fromRegion(cframe, size, enum.ZoneShape.Box) + return zone +end +function Zone.fromSphere(position, radius) + -- Creates an optimized spherical zone using GetPartBoundsInRadius + -- Creates a physical ball part for the zone + local part = Instance.new("Part") + part.Shape = Enum.PartType.Ball + part.Size = Vector3.new(radius * 2, radius * 2, radius * 2) + part.CFrame = CFrame.new(position) + part.Anchored = true + part.CanCollide = false + part.Transparency = 1 + part.Parent = workspace + + local zone = Zone.new(part) + zone.zoneShape = enum.ZoneShape.Sphere + zone.sphereRadius = radius + zone.spherePosition = position + -- Note: Call zone:relocate() manually if you want to move it to a WorldModel + return zone +end -- PRIVATE METHODS function Zone:_calculateRegion(tableOfParts, dontRound) - local bounds = {["Min"] = {}, ["Max"] = {}} + local bounds = { ["Min"] = {}, ["Max"] = {} } for boundType, details in pairs(bounds) do details.Values = {} function details.parseCheck(v, currentValue) @@ -188,7 +216,7 @@ function Zone:_calculateRegion(tableOfParts, dontRound) end end function details:parse(valuesToParse) - for i,v in pairs(valuesToParse) do + for i, v in pairs(valuesToParse) do local currentValue = self.Values[i] or v if self.parseCheck(v, currentValue) then self.Values[i] = v @@ -210,7 +238,7 @@ function Zone:_calculateRegion(tableOfParts, dontRound) } for _, cornerCFrame in pairs(corners) do local x, y, z = cornerCFrame:GetComponents() - local values = {x, y, z} + local values = { x, y, z } bounds.Min:parse(values) bounds.Max:parse(values) end @@ -221,7 +249,7 @@ function Zone:_calculateRegion(tableOfParts, dontRound) -- by ensuring it aligns on the voxel grid local function roundToFour(to_round) local ROUND_TO = 4 - local divided = (to_round+ROUND_TO/2) / ROUND_TO + local divided = (to_round + ROUND_TO / 2) / ROUND_TO local rounded = ROUND_TO * math.floor(divided) return rounded end @@ -231,28 +259,30 @@ function Zone:_calculateRegion(tableOfParts, dontRound) local newV = v if not dontRound then local roundOffset = (boundName == "Min" and -2) or 2 - newV = roundToFour(v+roundOffset) -- +-2 to ensures the zones region is not rounded down/up + newV = roundToFour(v + roundOffset) -- +-2 to ensures the zones region is not rounded down/up end table.insert(newTable, newV) end end local boundMin = Vector3.new(unpack(minBound)) local boundMax = Vector3.new(unpack(maxBound)) - local region = Region3.new(boundMin, boundMax) - return region, boundMin, boundMax + -- Convert bounds to CFrame + Size (modern approach instead of deprecated Region3) + local regionSize = boundMax - boundMin + local regionCFrame = CFrame.new((boundMin + boundMax) / 2) + return regionCFrame, regionSize, boundMin, boundMax end function Zone:_displayBounds() if not self.displayBoundParts then self.displayBoundParts = true - local boundParts = {BoundMin = self.boundMin, BoundMax = self.boundMax} + local boundParts = { BoundMin = self.boundMin, BoundMax = self.boundMax } for boundName, boundCFrame in pairs(boundParts) do local part = Instance.new("Part") part.Anchored = true part.CanCollide = false part.Transparency = 0.5 - part.Size = Vector3.new(1,1,1) - part.Color = Color3.fromRGB(255,0,0) + part.Size = Vector3.new(1, 1, 1) + part.Color = Color3.fromRGB(255, 0, 0) part.CFrame = CFrame.new(boundCFrame) part.Name = boundName part.Parent = workspace @@ -292,16 +322,18 @@ function Zone:_update() end self.zoneParts = zoneParts self.overlapParams = {} - + local allZonePartsAreBlocksNew = true for _, zonePart in pairs(zoneParts) do - local success, shapeName = pcall(function() return zonePart.Shape.Name end) + local success, shapeName = pcall(function() + return zonePart.Shape.Name + end) if shapeName ~= "Block" then allZonePartsAreBlocksNew = false end end self.allZonePartsAreBlocks = allZonePartsAreBlocksNew - + local zonePartsWhitelist = OverlapParams.new() zonePartsWhitelist.FilterType = Enum.RaycastFilterType.Include zonePartsWhitelist.MaxParts = #zoneParts @@ -312,7 +344,7 @@ function Zone:_update() zonePartsIgnorelist.FilterType = Enum.RaycastFilterType.Exclude zonePartsIgnorelist.FilterDescendantsInstances = zoneParts self.overlapParams.zonePartsIgnorelist = zonePartsIgnorelist - + -- this will call update on the zone when the container parts size or position changes, and when a -- child is removed or added from a holder (anything which isn't a basepart) local function update() @@ -336,10 +368,12 @@ function Zone:_update() end) end end - local partProperties = {"Size", "Position"} + local partProperties = { "Size", "Position" } local function verifyDefaultCollision(instance) if instance.CollisionGroupId ~= 0 then - error("Zone parts must belong to the 'Default' (0) CollisionGroup! Consider using zone:relocate() if you wish to move zones outside of workspace to prevent them interacting with other parts.") + error( + "Zone parts must belong to the 'Default' (0) CollisionGroup! Consider using zone:relocate() if you wish to move zones outside of workspace to prevent them interacting with other parts." + ) end end for _, part in pairs(zoneParts) do @@ -347,30 +381,37 @@ function Zone:_update() self._updateConnections:add(part:GetPropertyChangedSignal(prop):Connect(update), "Disconnect") end verifyDefaultCollision(part) - self._updateConnections:add(part:GetPropertyChangedSignal("CollisionGroupId"):Connect(function() - verifyDefaultCollision(part) - end), "Disconnect") - end - local containerEvents = {"ChildAdded", "ChildRemoved"} + self._updateConnections:add( + part:GetPropertyChangedSignal("CollisionGroupId"):Connect(function() + verifyDefaultCollision(part) + end), + "Disconnect" + ) + end + local containerEvents = { "ChildAdded", "ChildRemoved" } for _, holder in pairs(holders) do for _, event in pairs(containerEvents) do - self._updateConnections:add(self.container[event]:Connect(function(child) - if child:IsA("BasePart") then - update() - end - end), "Disconnect") + self._updateConnections:add( + self.container[event]:Connect(function(child) + if child:IsA("BasePart") then + update() + end + end), + "Disconnect" + ) end end - - local region, boundMin, boundMax = self:_calculateRegion(zoneParts) - local exactRegion, _, _ = self:_calculateRegion(zoneParts, true) - self.region = region - self.exactRegion = exactRegion + + local regionCFrame, regionSize, boundMin, boundMax = self:_calculateRegion(zoneParts) + local exactRegionCFrame, exactRegionSize, _, _ = self:_calculateRegion(zoneParts, true) + self.regionCFrame = regionCFrame + self.regionSize = regionSize + self.exactRegionCFrame = exactRegionCFrame + self.exactRegionSize = exactRegionSize self.boundMin = boundMin self.boundMax = boundMax - local rSize = region.Size - self.volume = rSize.X*rSize.Y*rSize.Z - + self.volume = regionSize.X * regionSize.Y * regionSize.Z + -- Update: I was going to use this for the old part detection until the CanTouch property was released -- everything below is now irrelevant however I'll keep just in case I use again for future ------------------------------------------------------------------------------------------------- @@ -384,9 +425,9 @@ function Zone:_update() local maxPartsBaseline = #result self.recommendedMaxParts = maxPartsBaseline + self.maxPartsAddition --]] - + self:_updateTouchedConnections() - + self.updated:Fire() end @@ -416,12 +457,12 @@ function Zone:_updateOccupants(trackerName, newOccupants) end table.insert(signalsToFire.entered, occupant) end - end + end return signalsToFire end function Zone:_formTouchedConnection(triggerType) - local touchedJanitorName = "_touchedJanitor"..triggerType + local touchedJanitorName = "_touchedJanitor" .. triggerType local touchedJanitor = self[touchedJanitorName] if touchedJanitor then touchedJanitor:clean() @@ -433,9 +474,11 @@ function Zone:_formTouchedConnection(triggerType) end function Zone:_updateTouchedConnection(triggerType) - local touchedJanitorName = "_touchedJanitor"..triggerType + local touchedJanitorName = "_touchedJanitor" .. triggerType local touchedJanitor = self[touchedJanitorName] - if not touchedJanitor then return end + if not touchedJanitor then + return + end for _, basePart in pairs(self.zoneParts) do touchedJanitor:add(basePart.Touched:Connect(self.touchedConnectionActions[triggerType], self), "Disconnect") end @@ -443,7 +486,7 @@ end function Zone:_updateTouchedConnections() for triggerType, _ in pairs(self.touchedConnectionActions) do - local touchedJanitorName = "_touchedJanitor"..triggerType + local touchedJanitorName = "_touchedJanitor" .. triggerType local touchedJanitor = self[touchedJanitorName] if touchedJanitor then touchedJanitor:cleanup() @@ -453,7 +496,7 @@ function Zone:_updateTouchedConnections() end function Zone:_disconnectTouchedConnection(triggerType) - local touchedJanitorName = "_touchedJanitor"..triggerType + local touchedJanitorName = "_touchedJanitor" .. triggerType local touchedJanitor = self[touchedJanitorName] if touchedJanitor then touchedJanitor:cleanup() @@ -462,57 +505,62 @@ function Zone:_disconnectTouchedConnection(triggerType) end local function round(number, decimalPlaces) - return math.round(number * 10^decimalPlaces) * 10^-decimalPlaces + return math.round(number * 10 ^ decimalPlaces) * 10 ^ -decimalPlaces end function Zone:_partTouchedZone(part) local trackingDict = self.trackingTouchedTriggers["part"] - if trackingDict[part] then return end + if trackingDict[part] then + return + end local nextCheck = 0 local verifiedEntrance = false local enterPosition = part.Position local enterTime = os.clock() local partJanitor = self.janitor:add(Janitor.new(), "destroy") trackingDict[part] = partJanitor - local instanceClassesToIgnore = {Seat = true, VehicleSeat = true} - local instanceNamesToIgnore = {HumanoidRootPart = true} - if not (instanceClassesToIgnore[part.ClassName] or not instanceNamesToIgnore[part.Name]) then + local instanceClassesToIgnore = { Seat = true, VehicleSeat = true } + local instanceNamesToIgnore = { HumanoidRootPart = true } + if not (instanceClassesToIgnore[part.ClassName] or not instanceNamesToIgnore[part.Name]) then part.CanTouch = false end -- local partVolume = round((part.Size.X * part.Size.Y * part.Size.Z), 5) self.totalPartVolume += partVolume -- - partJanitor:add(heartbeat:Connect(function() - local clockTime = os.clock() - if clockTime >= nextCheck then - ---- - local cooldown = enum.Accuracy.getProperty(self.accuracy) - nextCheck = clockTime + cooldown - ---- - - -- We initially perform a singular point check as this is vastly more lightweight than a large part check - -- If the former returns false, perform a whole part check in case the part is on the outer bounds. - local withinZone = self:findPoint(part.CFrame) - if not withinZone then - withinZone = self:findPart(part) - end - if not verifiedEntrance then - if withinZone then - verifiedEntrance = true - self.partEntered:Fire(part) - elseif (part.Position - enterPosition).Magnitude > 1.5 and clockTime - enterTime >= cooldown then - -- Even after the part has exited the zone, we track it for a brief period of time based upon the criteria - -- in the line above to ensure the .touched behaviours are not abused - partJanitor:cleanup() + partJanitor:add( + heartbeat:Connect(function() + local clockTime = os.clock() + if clockTime >= nextCheck then + ---- + local cooldown = enum.Accuracy.getProperty(self.accuracy) + nextCheck = clockTime + cooldown + ---- + + -- We initially perform a singular point check as this is vastly more lightweight than a large part check + -- If the former returns false, perform a whole part check in case the part is on the outer bounds. + local withinZone = self:findPoint(part.CFrame) + if not withinZone then + withinZone = self:findPart(part) + end + if not verifiedEntrance then + if withinZone then + verifiedEntrance = true + self.partEntered:Fire(part) + elseif (part.Position - enterPosition).Magnitude > 1.5 and clockTime - enterTime >= cooldown then + -- Even after the part has exited the zone, we track it for a brief period of time based upon the criteria + -- in the line above to ensure the .touched behaviours are not abused + partJanitor:cleanup() + end + elseif not withinZone then + verifiedEntrance = false + enterPosition = part.Position + enterTime = os.clock() + self.partExited:Fire(part) end - elseif not withinZone then - verifiedEntrance = false - enterPosition = part.Position - enterTime = os.clock() - self.partExited:Fire(part) end - end - end), "Disconnect") + end), + "Disconnect" + ) partJanitor:add(function() trackingDict[part] = nil part.CanTouch = true @@ -522,17 +570,19 @@ end local partShapeActions = { ["Ball"] = function(part) - return "GetPartBoundsInRadius", {part.Position, part.Size.X} + return "GetPartBoundsInRadius", { part.Position, part.Size.X } end, ["Block"] = function(part) - return "GetPartBoundsInBox", {part.CFrame, part.Size} + return "GetPartBoundsInBox", { part.CFrame, part.Size } end, ["Other"] = function(part) - return "GetPartsInPart", {part} + return "GetPartsInPart", { part } end, } function Zone:_getRegionConstructor(part, overlapParams) - local success, shapeName = pcall(function() return part.Shape.Name end) + local success, shapeName = pcall(function() + return part.Shape.Name + end) local methodName, args if success and self.allZonePartsAreBlocks then local action = partShapeActions[shapeName] @@ -549,8 +599,6 @@ function Zone:_getRegionConstructor(part, overlapParams) return methodName, args end - - -- PUBLIC METHODS function Zone:findLocalPlayer() if not localPlayer then @@ -635,7 +683,8 @@ end function Zone:_getAll(trackerName) ZoneController.updateDetection(self) local itemsArray = {} - local zonesAndOccupants = ZoneController._getZonesAndItems(trackerName, {self = true}, self.volume, false, self._currentEnterDetection) + local zonesAndOccupants = + ZoneController._getZonesAndItems(trackerName, { self = true }, self.volume, false, self._currentEnterDetection) local occupantsDict = zonesAndOccupants[self] if occupantsDict then for item, _ in pairs(occupantsDict) do @@ -665,7 +714,8 @@ function Zone:getParts() end return partsArray end - local partsInRegion = self.worldModel:GetPartBoundsInBox(self.region.CFrame, self.region.Size, self.overlapParams.zonePartsIgnorelist) + local partsInRegion = + self.worldModel:GetPartBoundsInBox(self.regionCFrame, self.regionSize, self.overlapParams.zonePartsIgnorelist) for _, part in pairs(partsInRegion) do if self:findPart(part) then table.insert(partsArray, part) @@ -675,15 +725,19 @@ function Zone:getParts() end function Zone:getRandomPoint() - local region = self.exactRegion - local size = region.Size - local cframe = region.CFrame + local cframe = self.exactRegionCFrame + local size = self.exactRegionSize local random = Random.new() local randomCFrame local success, touchingZoneParts local pointIsWithinZone repeat - randomCFrame = cframe * CFrame.new(random:NextNumber(-size.X/2,size.X/2), random:NextNumber(-size.Y/2,size.Y/2), random:NextNumber(-size.Z/2,size.Z/2)) + randomCFrame = cframe + * CFrame.new( + random:NextNumber(-size.X / 2, size.X / 2), + random:NextNumber(-size.Y / 2, size.Y / 2), + random:NextNumber(-size.Z / 2, size.Z / 2) + ) success, touchingZoneParts = self:findPoint(randomCFrame) if success then pointIsWithinZone = true @@ -709,6 +763,24 @@ function Zone:setAccuracy(enumIdOrName) self.accuracy = enumId end +function Zone:setZoneShape(enumIdOrName) + -- Allows changing the spatial query method used for this zone + -- Options: "Auto", "Box", "Sphere" + local enumId = tonumber(enumIdOrName) + if not enumId then + enumId = enum.ZoneShape[enumIdOrName] + if not enumId then + error(("'%s' is an invalid enumName!"):format(enumIdOrName)) + end + else + local enumName = enum.ZoneShape.getName(enumId) + if not enumName then + error(("%s is an invalid enumId!"):format(enumId)) + end + end + self.zoneShape = enumId +end + function Zone:setDetection(enumIdOrName) local enumId = tonumber(enumIdOrName) if not enumId then @@ -751,11 +823,14 @@ function Zone:trackItem(instance) } self.trackedItems[instance] = itemDetail - itemJanitor:add(instance.AncestryChanged:Connect(function() - if not instance:IsDescendantOf(game) then - self:untrackItem(instance) - end - end), "Disconnect") + itemJanitor:add( + instance.AncestryChanged:Connect(function() + if not instance:IsDescendantOf(game) then + self:untrackItem(instance) + end + end), + "Disconnect" + ) local Tracker = require(trackerModule) Tracker.itemAdded:Fire(itemDetail) @@ -798,7 +873,7 @@ function Zone:relocate() local worldModel = CollectiveWorldModel.setupWorldModel(self) self.worldModel = worldModel self.hasRelocated = true - + local relocationContainer = self.container if typeof(relocationContainer) == "table" then relocationContainer = Instance.new("Folder") @@ -871,6 +946,4 @@ function Zone:destroy() end Zone.Destroy = Zone.destroy - - return Zone From cdbad419bc7a4317d9d688491690ac837bc3e018 Mon Sep 17 00:00:00 2001 From: Froredion Date: Tue, 7 Oct 2025 05:47:46 +0800 Subject: [PATCH 2/4] added place tester rbxlx for the maintainer to easily test --- src/Testers/PlaceTester.rbxlx | 8388 +++++++++++++++++++++++++++++++++ 1 file changed, 8388 insertions(+) create mode 100644 src/Testers/PlaceTester.rbxlx diff --git a/src/Testers/PlaceTester.rbxlx b/src/Testers/PlaceTester.rbxlx new file mode 100644 index 0000000..74aa9ef --- /dev/null +++ b/src/Testers/PlaceTester.rbxlx @@ -0,0 +1,8388 @@ + + null + nil + + + 0.00120000006 + 0 + false + + 1 + 0 + 0 + 0 + AQEABP////8HRGVmYXVsdA== + RBX89ADB630653C43609C6DBCC5098C2571 + false + 0 + true + true + -500 + 0 + + 0 + 0 + 0 + + 196.199997 + 00000000000000000000000000000000 + 0 + 0 + 0 + 0 + + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + yuZpQdnvvUBOTYh1jqZ2cA== + + 0 + 0 + 0 + + 0 + 0 + 0 + Workspace + false + 0 + 0 + 0 + 0 + 0 + null + 0 + 0 + 0 + 0 + 0 + 1 + 2 + -1 + 2 + true + 3 + 64 + 1024 + + true + 0 + false + 48a5f871ef9fd19708f5cbd900000002 + 0 + 0 + + + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + + + + + + + -25.376812 + 24.1568317 + -122.04718 + 0.203131557 + -0.215827301 + 0.955068707 + -0 + 0.975404561 + 0.220422775 + -0.979151547 + -0.0447748229 + 0.198135406 + + null + 0 + 0 + false + 70 + 0 + + -27.2869492 + 23.7159863 + -122.443451 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + true + 1 + 00000000000000000000000000000000 + Camera + -1 + + 48a5f871ef9fd19708f5cbd9000003a4 + false + + + + + true + + true + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + -8 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + true + true + true + 0 + true + Default + 0 + 4284177243 + + false + + false + true + -0.5 + 0.5 + 0 + 0 + 00000000000000000000000000000000 + -0.5 + 0.5 + 0 + 0 + true + false + 256 + + Baseplate + + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + 0 + -0.5 + 0.5 + 0 + 0 + 0 + + 0 + 0 + 0 + + -1 + + -0.5 + 0.5 + 0 + 0 + 0 + 48a5f871ef9fd19708f5cbd9000003a5 + + 0 + 0 + 0 + + 0 + 1 + + 2048 + 16 + 2048 + + + + + + 0 + + 0 + 0 + 0 + + false + 1 + 00000000000000000000000000000000 + + Texture + + 0 + 0 + + -1 + 8 + 8 + + rbxassetid://6372755229 + + + 0.800000012 + + 0 + 0 + + + 1 + 1 + + 48a5f871ef9fd19708f5cbd9000003a6 + 1 + + + + + + 0 + true + + true + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + true + true + true + 0 + true + Default + 0 + 4288914085 + + false + + true + false + true + -0.5 + 0.5 + 0 + 0 + 0.699999988 + 00000000000000000000000000000000 + -0.5 + 0.5 + 0 + 0 + true + false + 256 + + + Terrain + AgMAAAAAAAAAAAAAAAA= + + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + 0 + -0.5 + 0.5 + 0 + 0 + 0 + + 0 + 0 + 0 + + AQU= + false + -1 + + -0.5 + 0.5 + 3 + 0 + 0 + 48a5f871ef9fd19708f5cbd9000003a7 + + 0 + 0 + 0 + + + 0.0470588282 + 0.329411775 + 0.360784322 + + 1 + 0.300000012 + 0.150000006 + 10 + + 2044 + 252 + 2044 + + + + + + false + true + + true + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + + 0 + 0.5 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + true + true + true + 0 + true + Default + 0 + 4288914085 + + false + + false + 0 + true + true + -0.5 + 0.5 + 0 + 0 + 00000000000000000000000000000000 + -0.5 + 0.5 + 0 + 0 + false + false + 256 + + SpawnLocation + true + + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + 0 + -0.5 + 0.5 + 0 + 0 + 0 + + 0 + 0 + 0 + + -1 + + 194 + -0.5 + 0.5 + 0 + 0 + 0 + 48a5f871ef9fd19708f5cbd9000003a8 + + 0 + 0 + 0 + + 1 + 1 + + 12 + 1 + 12 + + + + + + 0 + + 1 + 1 + 1 + + false + 1 + 00000000000000000000000000000000 + + Decal + + + -1 + + rbxasset://textures/SpawnLocation.png + + + 0 + + 0 + 0 + + + 1 + 1 + + 48a5f871ef9fd19708f5cbd9000003a9 + 1 + + + + + + + 0 + false + 00000000000000000000000000000000 + ZonePlusTests + -1 + + 48a5f871ef9fd19708f5cbd900002060 + + + + + 0 + false + 00000000000000000000000000000000 + 0 + + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + yuZpQdnvvUBOTYh1jqZ2cA== + + 0 + 0 + 0 + + 0 + CylinderGroupedZone + false + null + 1 + -1 + + 48a5f871ef9fd19708f5cbd900002061 + + + -58.1676903 + 13.5 + -19.4133339 + 0 + 0 + -1 + 0 + 1 + 0 + 1 + 0 + 0 + + + + + + true + + true + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + + -57.3036423 + 2.44967842 + -3.80394077 + 0 + 0 + -1 + 1 + 0 + 0 + 0 + -1 + 0 + + false + true + true + 0 + true + Default + 0 + 4285230079 + + false + + false + true + -0.5 + 0.5 + 0 + 0 + 00000000000000000000000000000000 + -0.5 + 0.5 + 0 + 0 + false + false + 288 + + Wall1 + + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + 0 + -0.5 + 0.5 + 0 + 0 + 0 + + 0 + 0 + 0 + + -1 + + -0.5 + 0.5 + 3 + 0 + 0.5 + 48a5f871ef9fd19708f5cbd900002067 + + 0 + 0 + 0 + + 1 + 2 + + 13.8993607 + 18.5324821 + 18.5324821 + + + + + + true + + true + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + + -58.1676903 + 41.5382118 + -31.8316231 + 0 + 0 + -1 + 0 + 1 + 0 + 1 + 0 + 0 + + false + true + true + 0 + true + Default + 0 + 4279069100 + + false + + false + true + -0.5 + 0.5 + 0 + 0 + 00000000000000000000000000000000 + -0.5 + 0.5 + 0 + 0 + false + false + 256 + + Label + + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + 0 + -0.5 + 0.5 + 0 + 0 + 0 + + 0 + 0 + 0 + + -1 + + -0.5 + 0.5 + 3 + 0 + 0 + 48a5f871ef9fd19708f5cbd900002068 + + 0 + 0 + 0 + + 1 + 1 + + 47.1774368 + 22.7054386 + 2.35887194 + + + + + true + null + false + + true + 1 + + 800 + 600 + + 0 + false + false + true + 5 + 00000000000000000000000000000000 + 0 + 0 + SurfaceGui + 50 + true + null + 0 + 0 + 0 + 0 + false + 0 + -1 + + 0 + 48a5f871ef9fd19708f5cbd900002069 + 0 + 0 + + + + false + + 0 + 0 + + + true + 0 + + 0.639215708 + 0.635294139 + 0.647058845 + + 1 + + 0.105882354 + 0.164705887 + 0.20784314 + + 0 + 1 + 0 + false + false + false + + rbxasset://fonts/families/LegacyArial.json + 400 + + rbxasset://fonts/Arimo-Regular.ttf + + 00000000000000000000000000000000 + true + 0 + 1 + + + -1 + TextLabel + null + null + null + null + + + 0 + 0 + 0 + 0 + + false + null + 0 + false + 0 + 0 + 0 + 0 + false + null + 0 + + 1 + 0 + 1 + 0 + + 0 + -1 + + + + 1 + 1 + 1 + + 0 + true + 8 + + 0 + 0 + 0 + + 1 + 0 + 0 + true + 2 + 1 + 48a5f871ef9fd19708f5cbd90000206a + true + 1 + + + + 0 + + + 0 + 0 + + 0 + 0 + + 0 + 0 + 0 + + false + true + 00000000000000000000000000000000 + 0 + UIStroke + -1 + 0 + + 25 + 0 + 48a5f871ef9fd19708f5cbd900002a9a + 1 + + + + + + + + true + + true + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + + -57.3036423 + 2.44967842 + -27.48666 + 0 + 0 + -1 + 1 + 0 + 0 + 0 + -1 + 0 + + false + true + true + 0 + true + Default + 0 + 4285230079 + + false + + false + true + -0.5 + 0.5 + 0 + 0 + 48a5f871ef9fd19708f5cbd900002067 + -0.5 + 0.5 + 0 + 0 + false + false + 288 + + Wall2 + + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + 0 + -0.5 + 0.5 + 0 + 0 + 0 + + 0 + 0 + 0 + + -1 + + -0.5 + 0.5 + 3 + 0 + 0.5 + 48a5f871ef9fd19708f5cbd900002fe6 + + 0 + 0 + 0 + + 1 + 2 + + 13.8993607 + 18.5324821 + 18.5324821 + + + + + + true + + true + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + + -57.3036423 + 6.12979698 + -35.2144547 + 0 + 0 + -1 + 1 + 0 + 0 + 0 + -1 + 0 + + false + true + true + 0 + true + Default + 0 + 4285230079 + + false + + false + true + -0.5 + 0.5 + 0 + 0 + 48a5f871ef9fd19708f5cbd900002067 + -0.5 + 0.5 + 0 + 0 + false + false + 288 + + Wall3 + + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + 0 + -0.5 + 0.5 + 0 + 0 + 0 + + 0 + 0 + 0 + + -1 + + -0.5 + 0.5 + 3 + 0 + 0.5 + 48a5f871ef9fd19708f5cbd90000303b + + 0 + 0 + 0 + + 1 + 2 + + 21.2595978 + 23.065073 + 23.065073 + + + + + + + + 0 + false + 00000000000000000000000000000000 + 0 + + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + yuZpQdnvvUBOTYh1jqZ2cA== + + 0 + 0 + 0 + + 0 + SphereZone + false + null + 1 + -1 + + 48a5f871ef9fd19708f5cbd90000206b + + + 44 + 14.5 + 44 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + + + + + true + + true + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + + 44 + 12.5 + 44 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + false + true + true + 0 + true + Default + 0 + 4294923647 + + false + + false + true + -0.5 + 0.5 + 0 + 0 + 00000000000000000000000000000000 + -0.5 + 0.5 + 0 + 0 + false + false + 288 + + SpherePart + + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + 0 + -0.5 + 0.5 + 0 + 0 + 0 + + 0 + 0 + 0 + + -1 + + -0.5 + 0.5 + 3 + 0 + 0.5 + 48a5f871ef9fd19708f5cbd90000206c + + 0 + 0 + 0 + + 1 + 0 + + 25 + 25 + 25 + + + + + + true + + true + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + + 44 + 42.3558807 + 44 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + false + true + true + 0 + true + Default + 0 + 4294901951 + + false + + false + true + -0.5 + 0.5 + 0 + 0 + 00000000000000000000000000000000 + -0.5 + 0.5 + 0 + 0 + false + false + 256 + + Label + + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + 0 + -0.5 + 0.5 + 0 + 0 + 0 + + 0 + 0 + 0 + + -1 + + -0.5 + 0.5 + 3 + 0 + 0 + 48a5f871ef9fd19708f5cbd90000206d + + 0 + 0 + 0 + + 1 + 1 + + 37.1721001 + 19.0668831 + 1.85860503 + + + + + true + null + false + + true + 1 + + 800 + 600 + + 0 + false + false + true + 5 + 00000000000000000000000000000000 + 0 + 0 + SurfaceGui + 50 + true + null + 0 + 0 + 0 + 0 + false + 0 + -1 + + 0 + 48a5f871ef9fd19708f5cbd90000206e + 0 + 0 + + + + false + + 0 + 0 + + + true + 0 + + 0.639215708 + 0.635294139 + 0.647058845 + + 1 + + 0.105882354 + 0.164705887 + 0.20784314 + + 0 + 1 + 0 + false + false + false + + rbxasset://fonts/families/LegacyArial.json + 400 + + rbxasset://fonts/Arimo-Regular.ttf + + 00000000000000000000000000000000 + true + 0 + 1 + + + -1 + TextLabel + null + null + null + null + + + 0 + 0 + 0 + 0 + + false + null + 0 + false + 0 + 0 + 0 + 0 + false + null + 0 + + 1 + 0 + 1 + 0 + + 0 + -1 + + + + 1 + 1 + 1 + + 0 + true + 8 + + 0 + 0 + 0 + + 1 + 0 + 0 + true + 2 + 1 + 48a5f871ef9fd19708f5cbd90000206f + true + 1 + + + + 0 + + + 0 + 0 + + 0 + 0 + + 0 + 0 + 0 + + false + true + 00000000000000000000000000000000 + 0 + UIStroke + -1 + 0 + + 25 + 0 + 48a5f871ef9fd19708f5cbd900002ac4 + 1 + + + + + + + + + + 0 + false + 00000000000000000000000000000000 + 0 + + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + yuZpQdnvvUBOTYh1jqZ2cA== + + 0 + 0 + 0 + + 0 + ComplexZone + false + null + 1 + -1 + + 48a5f871ef9fd19708f5cbd900002070 + + + -59 + 13.25 + 39 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + + + + + true + + true + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + + -59 + 5 + 31.5 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + false + true + true + 0 + true + Default + 0 + 4286578517 + + false + + false + true + -0.5 + 0.5 + 0 + 0 + 00000000000000000000000000000000 + -0.5 + 0.5 + 0 + 0 + false + false + 288 + + Base1 + + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + 0 + -0.5 + 0.5 + 0 + 0 + 0 + + 0 + 0 + 0 + + -1 + + -0.5 + 0.5 + 3 + 0 + 0.5 + 48a5f871ef9fd19708f5cbd900002071 + + 0 + 0 + 0 + + 1 + 1 + + 30 + 10 + 15 + + + + + + true + + true + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + + -66.5 + 5 + 46.5 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + false + true + true + 0 + true + Default + 0 + 4286578517 + + false + + false + true + -0.5 + 0.5 + 0 + 0 + 00000000000000000000000000000000 + -0.5 + 0.5 + 0 + 0 + false + false + 288 + + Base2 + + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + 0 + -0.5 + 0.5 + 0 + 0 + 0 + + 0 + 0 + 0 + + -1 + + -0.5 + 0.5 + 3 + 0 + 0.5 + 48a5f871ef9fd19708f5cbd900002072 + + 0 + 0 + 0 + + 1 + 1 + + 15 + 10 + 15 + + + + + + true + + true + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + + -75.7824554 + 1.08638477 + 24.8914452 + 0.707106769 + 0 + 0.707106769 + 0 + 1 + 0 + -0.707106769 + 0 + 0.707106769 + + false + true + true + 0 + true + Default + 0 + 4286578517 + + false + + false + true + -0.5 + 0.5 + 0 + 0 + 00000000000000000000000000000000 + -0.5 + 0.5 + 0 + 0 + false + false + 288 + + Wedge + + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + 0 + -0.5 + 0.5 + 0 + 0 + 0 + + 0 + 0 + 0 + + -1 + + -0.5 + 0.5 + 0 + 0 + 0.5 + 48a5f871ef9fd19708f5cbd900002073 + + 0 + 0 + 0 + + 1 + + 15 + 10 + 15 + + + + + + true + + true + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + + -59 + 40.5042038 + 31.5 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + false + true + true + 0 + true + Default + 0 + 4278255360 + + false + + false + true + -0.5 + 0.5 + 0 + 0 + 00000000000000000000000000000000 + -0.5 + 0.5 + 0 + 0 + false + false + 256 + + Label + + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + 0 + -0.5 + 0.5 + 0 + 0 + 0 + + 0 + 0 + 0 + + -1 + + -0.5 + 0.5 + 3 + 0 + 0 + 48a5f871ef9fd19708f5cbd900002074 + + 0 + 0 + 0 + + 1 + 1 + + 38.1200256 + 18.3389969 + 1.90600133 + + + + + true + null + false + + true + 1 + + 800 + 600 + + 0 + false + false + true + 5 + 00000000000000000000000000000000 + 0 + 0 + SurfaceGui + 50 + true + null + 0 + 0 + 0 + 0 + false + 0 + -1 + + 0 + 48a5f871ef9fd19708f5cbd900002075 + 0 + 0 + + + + false + + 0 + 0 + + + true + 0 + + 0.639215708 + 0.635294139 + 0.647058845 + + 1 + + 0.105882354 + 0.164705887 + 0.20784314 + + 0 + 1 + 0 + false + false + false + + rbxasset://fonts/families/LegacyArial.json + 400 + + rbxasset://fonts/Arimo-Regular.ttf + + 00000000000000000000000000000000 + true + 0 + 1 + + + -1 + TextLabel + null + null + null + null + + + 0 + 0 + 0 + 0 + + false + null + 0 + false + 0 + 0 + 0 + 0 + false + null + 0 + + 1 + 0 + 1 + 0 + + 0 + -1 + + + + 1 + 1 + 1 + + 0 + true + 8 + + 0 + 0 + 0 + + 1 + 0 + 0 + true + 2 + 1 + 48a5f871ef9fd19708f5cbd900002076 + true + 1 + + + + 0 + + + 0 + 0 + + 0 + 0 + + 0 + 0 + 0 + + false + true + 00000000000000000000000000000000 + 0 + UIStroke + -1 + 0 + + 25 + 0 + 48a5f871ef9fd19708f5cbd9000028a0 + 1 + + + + + + + + true + + true + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + + -59 + 2.33250523 + 39 + 0 + 0 + 1 + 0 + 1 + 0 + -1 + 0 + 0 + + false + true + true + 0 + true + Default + 0 + 4286578517 + + false + + false + true + -0.5 + 0.5 + 0 + 0 + 48a5f871ef9fd19708f5cbd900002073 + -0.5 + 0.5 + 0 + 0 + false + false + 288 + + Wedge + + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + 0 + -0.5 + 0.5 + 0 + 0 + 0 + + 0 + 0 + 0 + + -1 + + -0.5 + 0.5 + 0 + 0 + 0.5 + 48a5f871ef9fd19708f5cbd90000388e + + 0 + 0 + 0 + + 1 + + 15 + 10 + 15 + + + + + + + true + + true + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + + 0 + -5 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + true + true + true + 0 + true + Default + 0 + 4284702562 + + false + + false + true + -0.5 + 0.5 + 0 + 0 + 00000000000000000000000000000000 + -0.5 + 0.5 + 0 + 0 + false + false + 816 + + SpawnPlatform + + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + 0 + -0.5 + 0.5 + 0 + 0 + 0 + + 0 + 0 + 0 + + -1 + + -0.5 + 0.5 + 3 + 0 + 0 + 48a5f871ef9fd19708f5cbd900002077 + + 0 + 0 + 0 + + 1 + 1 + + 120 + 1 + 60 + + + + + + false + true + + true + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + + 0 + -3.5 + -20 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + true + true + true + 0 + true + Default + 0 + 4283144011 + + false + + false + 0 + true + true + -0.5 + 0.5 + 0 + 0 + 00000000000000000000000000000000 + -0.5 + 0.5 + 0 + 0 + false + false + 256 + + SpawnLocation + true + + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + 0 + -0.5 + 0.5 + 0 + 0 + 0 + + 0 + 0 + 0 + + -1 + + 194 + -0.5 + 0.5 + 3 + 0 + 0 + 48a5f871ef9fd19708f5cbd90000207e + + 0 + 0 + 0 + + 1 + 1 + + 10 + 1 + 10 + + + + + + + 0 + false + 48a5f871ef9fd19708f5cbd900002061 + 0 + + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + yuZpQdnvvUBOTYh1jqZ2cA== + + 0 + 0 + 0 + + 0 + BoxZone + false + null + 1 + -1 + + 48a5f871ef9fd19708f5cbd900002b4c + + + -1 + 13.5 + 48.5000038 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + + + + + true + + true + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + + -1 + 0.5 + 48.5000038 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + false + true + true + 0 + true + Default + 0 + 4278233855 + + false + + false + true + -0.5 + 0.5 + 0 + 0 + 48a5f871ef9fd19708f5cbd900002062 + -0.5 + 0.5 + 0 + 0 + false + false + 288 + + Bottom + + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + 0 + -0.5 + 0.5 + 0 + 0 + 0 + + 0 + 0 + 0 + + -1 + + -0.5 + 0.5 + 3 + 0 + 0.5 + 48a5f871ef9fd19708f5cbd900002b4d + + 0 + 0 + 0 + + 1 + 1 + + 30 + 1 + 30 + + + + + + true + + true + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + + -1 + 20.5 + 48.5000038 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + false + true + true + 0 + true + Default + 0 + 4278233855 + + false + + false + true + -0.5 + 0.5 + 0 + 0 + 48a5f871ef9fd19708f5cbd900002063 + -0.5 + 0.5 + 0 + 0 + false + false + 288 + + Top + + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + 0 + -0.5 + 0.5 + 0 + 0 + 0 + + 0 + 0 + 0 + + -1 + + -0.5 + 0.5 + 3 + 0 + 0.5 + 48a5f871ef9fd19708f5cbd900002b4e + + 0 + 0 + 0 + + 1 + 1 + + 30 + 1 + 30 + + + + + + true + + true + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + + 14 + 10.5 + 48.5000038 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + false + true + true + 0 + true + Default + 0 + 4278233855 + + false + + false + true + -0.5 + 0.5 + 0 + 0 + 48a5f871ef9fd19708f5cbd900002064 + -0.5 + 0.5 + 0 + 0 + false + false + 288 + + Wall1 + + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + 0 + -0.5 + 0.5 + 0 + 0 + 0 + + 0 + 0 + 0 + + -1 + + -0.5 + 0.5 + 3 + 0 + 0.5 + 48a5f871ef9fd19708f5cbd900002b4f + + 0 + 0 + 0 + + 1 + 1 + + 1 + 20 + 30 + + + + + + true + + true + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + + -16 + 10.5 + 48.5000038 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + false + true + true + 0 + true + Default + 0 + 4278233855 + + false + + false + true + -0.5 + 0.5 + 0 + 0 + 48a5f871ef9fd19708f5cbd900002065 + -0.5 + 0.5 + 0 + 0 + false + false + 288 + + Wall2 + + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + 0 + -0.5 + 0.5 + 0 + 0 + 0 + + 0 + 0 + 0 + + -1 + + -0.5 + 0.5 + 3 + 0 + 0.5 + 48a5f871ef9fd19708f5cbd900002b50 + + 0 + 0 + 0 + + 1 + 1 + + 1 + 20 + 30 + + + + + + true + + true + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + + -1 + 10.5 + 63.5000038 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + false + true + true + 0 + true + Default + 0 + 4278233855 + + false + + false + true + -0.5 + 0.5 + 0 + 0 + 48a5f871ef9fd19708f5cbd900002066 + -0.5 + 0.5 + 0 + 0 + false + false + 288 + + Wall3 + + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + 0 + -0.5 + 0.5 + 0 + 0 + 0 + + 0 + 0 + 0 + + -1 + + -0.5 + 0.5 + 3 + 0 + 0.5 + 48a5f871ef9fd19708f5cbd900002b51 + + 0 + 0 + 0 + + 1 + 1 + + 30 + 20 + 1 + + + + + + true + + true + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + + -1 + 10.5 + 33.5000038 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + false + true + true + 0 + true + Default + 0 + 4278233855 + + false + + false + true + -0.5 + 0.5 + 0 + 0 + 48a5f871ef9fd19708f5cbd900002067 + -0.5 + 0.5 + 0 + 0 + false + false + 288 + + Wall4 + + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + 0 + -0.5 + 0.5 + 0 + 0 + 0 + + 0 + 0 + 0 + + -1 + + -0.5 + 0.5 + 3 + 0 + 0.5 + 48a5f871ef9fd19708f5cbd900002b52 + + 0 + 0 + 0 + + 1 + 1 + + 30 + 20 + 1 + + + + + + true + + true + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + + -1 + 41.5382118 + 48.5000038 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + false + true + true + 0 + true + Default + 0 + 4279069100 + + false + + false + true + -0.5 + 0.5 + 0 + 0 + 48a5f871ef9fd19708f5cbd900002068 + -0.5 + 0.5 + 0 + 0 + false + false + 256 + + Label + + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + 0 + -0.5 + 0.5 + 0 + 0 + 0 + + 0 + 0 + 0 + + -1 + + -0.5 + 0.5 + 3 + 0 + 0 + 48a5f871ef9fd19708f5cbd900002b53 + + 0 + 0 + 0 + + 1 + 1 + + 47.1774368 + 22.7054386 + 2.35887194 + + + + + true + null + false + + true + 1 + + 800 + 600 + + 0 + false + false + true + 5 + 48a5f871ef9fd19708f5cbd900002069 + 0 + 0 + SurfaceGui + 50 + true + null + 0 + 0 + 0 + 0 + false + 0 + -1 + + 0 + 48a5f871ef9fd19708f5cbd900002b54 + 0 + 0 + + + + false + + 0 + 0 + + + true + 0 + + 0.639215708 + 0.635294139 + 0.647058845 + + 1 + + 0.105882354 + 0.164705887 + 0.20784314 + + 0 + 1 + 0 + false + false + false + + rbxasset://fonts/families/LegacyArial.json + 400 + + rbxasset://fonts/Arimo-Regular.ttf + + 48a5f871ef9fd19708f5cbd90000206a + true + 0 + 1 + + + -1 + TextLabel + null + null + null + null + + + 0 + 0 + 0 + 0 + + false + null + 0 + false + 0 + 0 + 0 + 0 + false + null + 0 + + 1 + 0 + 1 + 0 + + 0 + -1 + + + + 1 + 1 + 1 + + 0 + true + 8 + + 0 + 0 + 0 + + 1 + 0 + 0 + true + 2 + 1 + 48a5f871ef9fd19708f5cbd900002b55 + true + 1 + + + + 0 + + + 0 + 0 + + 0 + 0 + + 0 + 0 + 0 + + false + true + 48a5f871ef9fd19708f5cbd900002a9a + 0 + UIStroke + -1 + 0 + + 25 + 0 + 48a5f871ef9fd19708f5cbd900002b56 + 1 + + + + + + + + + + 0 + false + 48a5f871ef9fd19708f5cbd900002061 + 0 + + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + yuZpQdnvvUBOTYh1jqZ2cA== + + 0 + 0 + 0 + + 0 + CylinderUngroupedZone + false + null + 1 + -1 + + 48a5f871ef9fd19708f5cbd9000031ff + + + -58.1676903 + 13.5 + -113.057159 + 0 + 0 + -1 + 0 + 1 + 0 + 1 + 0 + 0 + + + + + + true + + true + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + + -57.3036423 + 2.44967842 + -102.476059 + 0 + 0 + -1 + 1 + 0 + 0 + 0 + -1 + 0 + + false + true + true + 0 + true + Default + 0 + 4286775295 + + false + + false + true + -0.5 + 0.5 + 0 + 0 + 48a5f871ef9fd19708f5cbd900002067 + -0.5 + 0.5 + 0 + 0 + false + false + 288 + + Wall1 + + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + 0 + -0.5 + 0.5 + 0 + 0 + 0 + + 0 + 0 + 0 + + -1 + + -0.5 + 0.5 + 3 + 0 + 0.5 + 48a5f871ef9fd19708f5cbd900003200 + + 0 + 0 + 0 + + 1 + 2 + + 13.8993607 + 18.5324821 + 18.5324821 + + + + + + true + + true + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + + -58.1676903 + 41.5382118 + -116.809776 + 0 + 0 + -1 + 0 + 1 + 0 + 1 + 0 + 0 + + false + true + true + 0 + true + Default + 0 + 4279069100 + + false + + false + true + -0.5 + 0.5 + 0 + 0 + 48a5f871ef9fd19708f5cbd900002068 + -0.5 + 0.5 + 0 + 0 + false + false + 256 + + Label + + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + 0 + -0.5 + 0.5 + 0 + 0 + 0 + + 0 + 0 + 0 + + -1 + + -0.5 + 0.5 + 3 + 0 + 0 + 48a5f871ef9fd19708f5cbd900003201 + + 0 + 0 + 0 + + 1 + 1 + + 47.1774368 + 22.7054386 + 2.35887194 + + + + + true + null + false + + true + 1 + + 800 + 600 + + 0 + false + false + true + 5 + 48a5f871ef9fd19708f5cbd900002069 + 0 + 0 + SurfaceGui + 50 + true + null + 0 + 0 + 0 + 0 + false + 0 + -1 + + 0 + 48a5f871ef9fd19708f5cbd900003202 + 0 + 0 + + + + false + + 0 + 0 + + + true + 0 + + 0.639215708 + 0.635294139 + 0.647058845 + + 1 + + 0.105882354 + 0.164705887 + 0.20784314 + + 0 + 1 + 0 + false + false + false + + rbxasset://fonts/families/LegacyArial.json + 400 + + rbxasset://fonts/Arimo-Regular.ttf + + 48a5f871ef9fd19708f5cbd90000206a + true + 0 + 1 + + + -1 + TextLabel + null + null + null + null + + + 0 + 0 + 0 + 0 + + false + null + 0 + false + 0 + 0 + 0 + 0 + false + null + 0 + + 1 + 0 + 1 + 0 + + 0 + -1 + + + + 1 + 1 + 1 + + 0 + true + 8 + + 0 + 0 + 0 + + 1 + 0 + 0 + true + 2 + 1 + 48a5f871ef9fd19708f5cbd900003203 + true + 1 + + + + 0 + + + 0 + 0 + + 0 + 0 + + 0 + 0 + 0 + + false + true + 48a5f871ef9fd19708f5cbd900002a9a + 0 + UIStroke + -1 + 0 + + 25 + 0 + 48a5f871ef9fd19708f5cbd900003204 + 1 + + + + + + + + true + + true + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + + -57.3036423 + 2.44967842 + -118.117416 + 0 + 0 + -1 + 1 + 0 + 0 + 0 + -1 + 0 + + false + true + true + 0 + true + Default + 0 + 4293787512 + + false + + false + true + -0.5 + 0.5 + 0 + 0 + 48a5f871ef9fd19708f5cbd900002067 + -0.5 + 0.5 + 0 + 0 + false + false + 288 + + Wall2 + + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + 0 + -0.5 + 0.5 + 0 + 0 + 0 + + 0 + 0 + 0 + + -1 + + -0.5 + 0.5 + 3 + 0 + 0.5 + 48a5f871ef9fd19708f5cbd900003205 + + 0 + 0 + 0 + + 1 + 2 + + 13.8993607 + 18.5324821 + 18.5324821 + + + + + + true + + true + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + + -57.3036423 + 2.44967842 + -131.043518 + 0 + 0 + -1 + 1 + 0 + 0 + 0 + -1 + 0 + + false + true + true + 0 + true + Default + 0 + 4294922990 + + false + + false + true + -0.5 + 0.5 + 0 + 0 + 48a5f871ef9fd19708f5cbd900002067 + -0.5 + 0.5 + 0 + 0 + false + false + 288 + + Wall3 + + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + 0 + -0.5 + 0.5 + 0 + 0 + 0 + + 0 + 0 + 0 + + -1 + + -0.5 + 0.5 + 3 + 0 + 0.5 + 48a5f871ef9fd19708f5cbd900003206 + + 0 + 0 + 0 + + 1 + 2 + + 13.8993607 + 18.5324821 + 18.5324821 + + + + + + + + + + 0 + false + 00000000000000000000000000000000 + Instance + -1 + + 48a5f871ef9fd19708f5cbd900000326 + + + + + false + 0 + + 0 + 0 + 0 + 0 + 3 + false + 3.32999992 + 1 + 00000000000000000000000000000000 + false + SoundService + true + 1 + -1 + + 48a5f871ef9fd19708f5cbd900000327 + 1 + + + + + + 0 + false + 00000000000000000000000000000000 + VideoCaptureService + -1 + + 48a5f871ef9fd19708f5cbd900000333 + + + + + + 0 + false + 00000000000000000000000000000000 + NonReplicatedCSGDictionaryService + -1 + + 48a5f871ef9fd19708f5cbd900000334 + + + + + + 0 + false + 00000000000000000000000000000000 + CSGDictionaryService + -1 + + 48a5f871ef9fd19708f5cbd900000335 + + + + + + true + 0 + false + 00000000000000000000000000000000 + false + true + Chat + -1 + + 48a5f871ef9fd19708f5cbd90000033b + + + + + + true + 0 + true + false + 00000000000000000000000000000000 + 30 + Players + 30 + 3 + -1 + + 48a5f871ef9fd19708f5cbd90000033d + false + + + + + + 0 + false + 00000000000000000000000000000000 + ReplicatedFirst + -1 + + 48a5f871ef9fd19708f5cbd900000340 + + + + + + 0 + false + 00000000000000000000000000000000 + TweenService + -1 + + 48a5f871ef9fd19708f5cbd900000342 + + + + + Asphalt + + Basalt + Brick + 0 + Cardboard + Carpet + CeramicTiles + ClayRoofTiles + Cobblestone + Concrete + CorrodedMetal + CrackedLava + false + DiamondPlate + Fabric + Foil + Glacier + Granite + Grass + Ground + 00000000000000000000000000000000 + Ice + LeafyGrass + Leather + Limestone + Marble + Metal + Mud + MaterialService + Pavement + Pebble + Plaster + Plastic + Rock + RoofShingles + Rubber + Salt + Sand + Sandstone + Slate + SmoothPlastic + Snow + -1 + + 48a5f871ef9fd19708f5cbd900000343 + true + Wood + WoodPlanks + + + + + + 0 + true + false + 1 + true + true + false + false + 00000000000000000000000000000000 + true + TextChatService + -1 + + 48a5f871ef9fd19708f5cbd900000344 + + + + + + 0.0980392173 + 0.105882354 + 0.113725491 + + 0.2999999999999999889 + 0 + false + true + + rbxasset://fonts/families/BuilderSans.json + 500 + + rbxasset://fonts/BuilderSans-Medium.otf + + 1 + 00000000000000000000000000000000 + 1 + ChatWindowConfiguration + -1 + + + 1 + 1 + 1 + + 18 + + 0 + 0 + 0 + + 0.5 + 48a5f871ef9fd19708f5cbd9000003b3 + 1 + 1 + + + + + + true + + 0.0980392173 + 0.105882354 + 0.113725491 + + 0.2000000000000000111 + 0 + false + true + + rbxasset://fonts/families/BuilderSans.json + 500 + + rbxasset://fonts/BuilderSans-Medium.otf + + 00000000000000000000000000000000 + 47 + ChatInputBarConfiguration + + 0.698039234 + 0.698039234 + 0.698039234 + + -1 + + null + + 1 + 1 + 1 + + 18 + + 0 + 0 + 0 + + 0.5 + 48a5f871ef9fd19708f5cbd9000003b4 + + + + + HumanoidRootPart + + + 0.980392158 + 0.980392158 + 0.980392158 + + 0.10000000000000000555 + 15 + 6 + 0 + false + true + 47 + + rbxasset://fonts/families/BuilderSans.json + 500 + + rbxasset://fonts/BuilderSans-Medium.otf + + 00000000000000000000000000000000 + + 0 + 0 + 0 + + 3 + 100 + 40 + BubbleChatConfiguration + -1 + + true + + 0.223529413 + 0.23137255 + 0.239215687 + + 20 + 48a5f871ef9fd19708f5cbd9000003b5 + 0 + + + + + 0 + 0 1 1 1 0 1 1 1 1 0 + false + false + 00000000000000000000000000000000 + UIGradient + + 0 + 0 + + 0 + -1 + + 0 0 0 1 0 0 + 48a5f871ef9fd19708f5cbd9000003b6 + + + + + false + + 0 + 0 + + + true + 0 + + 1 + 1 + 1 + + 0 + + 0.105882362 + 0.164705887 + 0.207843155 + + 0 + 1 + 0 + false + false + false + 00000000000000000000000000000000 + + + 1 + 1 + 1 + + + 0 + 0 + + + 0 + 0 + + 0 + true + 0 + ImageLabel + null + null + null + null + + 0 + 0 + 0 + 0 + + 0 + null + 0 + 0 + false + 0 + 0 + 0 + 0 + false + null + 0 + + 0 + 100 + 0 + 100 + + 0 + + + 0 + 0 + + + 0 + 0 + + + 1 + -1 + + + 1 + 0 + 1 + 0 + + 48a5f871ef9fd19708f5cbd9000003b7 + true + 1 + + + + + + 0 + + 0 + 12 + + false + 00000000000000000000000000000000 + UICorner + -1 + + 48a5f871ef9fd19708f5cbd9000003b8 + + + + + + 0 + false + 00000000000000000000000000000000 + UIPadding + + 0 + 8 + + + 0 + 8 + + + 0 + 8 + + + 0 + 8 + + -1 + + 48a5f871ef9fd19708f5cbd9000003b9 + + + + + + + + 0.0980392173 + 0.105882354 + 0.113725491 + + 0 + 0 + false + false + + rbxasset://fonts/families/BuilderSans.json + 700 + + rbxasset://fonts/BuilderSans-Bold.otf + + 00000000000000000000000000000000 + + 0.490196079 + 0.490196079 + 0.490196079 + + ChannelTabsConfiguration + + 1 + 1 + 1 + + -1 + + + 0.686274529 + 0.686274529 + 0.686274529 + + 18 + + 0 + 0 + 0 + + 1 + 48a5f871ef9fd19708f5cbd9000003ba + + + + + + + 0 + false + 00000000000000000000000000000000 + PermissionsService + -1 + + 48a5f871ef9fd19708f5cbd900000346 + + + + + + 0 + false + false + + + 00000000000000000000000000000000 + PlayerEmulatorService + false + false + + -1 + + 0 + 48a5f871ef9fd19708f5cbd900000347 + + + + + + 0 + false + false + 00000000000000000000000000000000 + StudioData + -1 + + 48a5f871ef9fd19708f5cbd90000034b + + + + + true + + true + 0 + 128 + 0.5 + 0 + 0 + 7.19999981 + 50 + 89 + false + 16 + true + false + 0 + 0 + 0 + 0 + 0 + 0 + true + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 1 + 0 + 0 1 + 0.95 1 + 0.9 1.05 + 0 1 + 0.7 1 + 100 + 00000000000000000000000000000000 + true + 0 + 0 + StarterPlayer + 100 + true + -1 + + 48a5f871ef9fd19708f5cbd90000034d + true + + + + + 0 + false + 00000000000000000000000000000000 + StarterPlayerScripts + -1 + + 48a5f871ef9fd19708f5cbd9000003ab + + + + + 0 + false + false + 00000000000000000000000000000000 + + ZonePlusTest + 0 + {8773AB8F-BD13-4B84-9202-F703EDFF24B2} + StarterPlayerScripts or StarterGui + +local ReplicatedStorage = game:GetService("ReplicatedStorage") +local Players = game:GetService("Players") +local RunService = game:GetService("RunService") + +-- Wait for Zone module (adjust path as needed) +local Zone = require(ReplicatedStorage:WaitForChild("Zone")) + +local player = Players.LocalPlayer +local character = player.Character or player.CharacterAdded:Wait() +local humanoidRootPart = character:WaitForChild("HumanoidRootPart") + +-- Wait for test environment +local testFolder = workspace:WaitForChild("ZonePlusTests") +local boxZoneContainer = testFolder:WaitForChild("BoxZone") +local sphereZoneContainer = testFolder:WaitForChild("SphereZone") +local complexZoneContainer = testFolder:WaitForChild("ComplexZone") +local cylinderGroupedContainer = testFolder:WaitForChild("CylinderGroupedZone") +local cylinderUngroupedContainer = testFolder:WaitForChild("CylinderUngroupedZone") + +print("šŸš€ ZonePlus v4.0.0 - Modern Spatial Query Edition Test") +print("=" .. string.rep("=", 60)) + +-- Test 1: Traditional Zone from Container (Modern APIs under the hood) +print("šŸ“¦ Creating Box Zone from container...") +local boxZone = Zone.new(boxZoneContainer) +boxZone:setAccuracy("High") +boxZone:setDetection("Centre") +print("āœ… Box Zone created with traditional method (Uses modern GetPartBoundsInBox internally)") + +-- Test 2: New Optimized Box Zone +print("\nšŸ“¦ Creating optimized Box Zone...") +local optimizedBoxZone = Zone.fromBox(CFrame.new(0, 10, 0), Vector3.new(30, 20, 30)) +optimizedBoxZone:setAccuracy("High") +print("āœ… Optimized Box Zone created with Zone.fromBox()") + +-- Test 3: Sphere Zone from existing container +print("\nšŸ”µ Creating Sphere Zone from container...") +local sphereZone = Zone.new(sphereZoneContainer) +sphereZone:setAccuracy("High") +sphereZone:setDetection("Centre") +print("āœ… Sphere Zone created (Ball shape auto-detected)") + +-- Test 4: Complex Zone (will use GetPartsInPart for precision) +print("\nšŸ”§ Creating Complex Zone...") +local complexZone = Zone.new(complexZoneContainer) +complexZone:setAccuracy("High") +complexZone:setDetection("Centre") +print("āœ… Complex Zone created (Uses GetPartsInPart for precision)") + +-- Test 5: Cylinder Grouped Zone +print("\nšŸ›¢ļø Creating Cylinder Grouped Zone...") +local cylinderGroupedZone = Zone.new(cylinderGroupedContainer) +cylinderGroupedZone:setAccuracy("High") +cylinderGroupedZone:setDetection("Centre") +print("āœ… Cylinder Grouped Zone created (Full cylinder with top/bottom)") + +-- Test 6: Cylinder Ungrouped Zones (Each part gets its own zone with different color) +print("\nšŸ›¢ļø Creating Cylinder Ungrouped Zones...") +local ungroupedZones = {} +local ungroupedColors = { + Color3.fromRGB(170, 0, 255), -- Purple + Color3.fromRGB(255, 0, 170), -- Magenta + Color3.fromRGB(0, 255, 170), -- Cyan + Color3.fromRGB(255, 170, 0), -- Orange-Yellow + Color3.fromRGB(170, 255, 0), -- Lime +} + +local partIndex = 1 +local children = cylinderUngroupedContainer:GetChildren() +print("šŸ” Found " .. #children .. " children in CylinderUngroupedContainer") + +for _, part in children do + print(" Checking child: " .. part.Name .. " (Type: " .. part.ClassName .. ")") + if part:IsA("BasePart") then + print(" Part details - Position: " .. tostring(part.Position) .. ", Size: " .. tostring(part.Size)) + print(" Part properties - CanCollide: " .. tostring(part.CanCollide) .. ", Anchored: " .. tostring(part.Anchored)) + + local zone = Zone.new(part) + zone:setAccuracy("High") + zone:setDetection("Centre") + + -- Verify zone was created properly + print(" Zone created - ZoneID: " .. zone.zoneId) + print(" Zone parts count: " .. #zone.zoneParts) + if #zone.zoneParts > 0 then + print(" Zone part[1]: " .. zone.zoneParts[1].Name) + end + + local color = ungroupedColors[((partIndex - 1) % #ungroupedColors) + 1] + table.insert(ungroupedZones, { + zone = zone, + part = part, -- Store reference to the part for debugging + name = "Ungrouped Part " .. partIndex .. " (" .. part.Name .. ")", + inZone = false, + color = color, + }) + print( + "āœ… Created zone #" + .. partIndex + .. " for part: " + .. part.Name + .. " with color RGB(" + .. math.floor(color.R * 255) + .. ", " + .. math.floor(color.G * 255) + .. ", " + .. math.floor(color.B * 255) + .. ")" + ) + partIndex = partIndex + 1 + end +end + +print("šŸ“Š Total ungrouped zones created: " .. #ungroupedZones) + +-- Track zone status +local zoneStatus = { + boxZone = { name = "Box Zone (Container)", inZone = false, color = Color3.fromRGB(0, 170, 255) }, + sphereZone = { name = "Sphere Zone", inZone = false, color = Color3.fromRGB(255, 85, 127) }, + complexZone = { name = "Complex Zone", inZone = false, color = Color3.fromRGB(127, 255, 85) }, + cylinderGroupedZone = { name = "Cylinder Grouped Zone", inZone = false, color = Color3.fromRGB(255, 170, 0) }, +} + +-- Connect to zone events +boxZone.localPlayerEntered:Connect(function() + zoneStatus.boxZone.inZone = true + print("\n🟢 ENTERED:", zoneStatus.boxZone.name) +end) + +boxZone.localPlayerExited:Connect(function() + zoneStatus.boxZone.inZone = false + print("\nšŸ”“ EXITED:", zoneStatus.boxZone.name) +end) + +sphereZone.localPlayerEntered:Connect(function() + zoneStatus.sphereZone.inZone = true + print("\n🟢 ENTERED:", zoneStatus.sphereZone.name) +end) + +sphereZone.localPlayerExited:Connect(function() + zoneStatus.sphereZone.inZone = false + print("\nšŸ”“ EXITED:", zoneStatus.sphereZone.name) +end) + +complexZone.localPlayerEntered:Connect(function() + zoneStatus.complexZone.inZone = true + print("\n🟢 ENTERED:", zoneStatus.complexZone.name) +end) + +complexZone.localPlayerExited:Connect(function() + zoneStatus.complexZone.inZone = false + print("\nšŸ”“ EXITED:", zoneStatus.complexZone.name) +end) + +cylinderGroupedZone.localPlayerEntered:Connect(function() + zoneStatus.cylinderGroupedZone.inZone = true + print("\n🟢 ENTERED:", zoneStatus.cylinderGroupedZone.name) +end) + +cylinderGroupedZone.localPlayerExited:Connect(function() + zoneStatus.cylinderGroupedZone.inZone = false + print("\nšŸ”“ EXITED:", zoneStatus.cylinderGroupedZone.name) +end) + +-- Connect events for each ungrouped zone +for _, zoneData in ungroupedZones do + zoneData.zone.localPlayerEntered:Connect(function() + zoneData.inZone = true + print("\n🟢 ENTERED:", zoneData.name) + end) + + zoneData.zone.localPlayerExited:Connect(function() + zoneData.inZone = false + print("\nšŸ”“ EXITED:", zoneData.name) + end) +end + +boxZone.itemEntered:Connect(function(item) + print("šŸ“¦ Item entered Box Zone:", item.Name) +end) + +boxZone.itemExited:Connect(function(item) + print("šŸ“¦ Item exited Box Zone:", item.Name) +end) + +-- Visual feedback - highlight character when in zones +local highlight = Instance.new("Highlight") +highlight.FillTransparency = 0.5 +highlight.OutlineTransparency = 0 +highlight.Parent = character + +RunService.Heartbeat:Connect(function() + -- Update highlight color based on which zone player is in + local inAnyZone = false + local currentColor = Color3.new(1, 1, 1) + + -- Check main zones + for _, status in zoneStatus do + if status.inZone then + inAnyZone = true + currentColor = status.color + break + end + end + + -- Check ungrouped zones + if not inAnyZone then + for _, zoneData in ungroupedZones do + if zoneData.inZone then + inAnyZone = true + currentColor = zoneData.color + break + end + end + end + + highlight.Enabled = inAnyZone + if inAnyZone then + highlight.FillColor = currentColor + highlight.OutlineColor = currentColor + end +end) + +-- Print test summary +task.wait(1) +print("\n" .. string.rep("-", 50)) +print("\nšŸŽÆ Test Summary:") +print("• Box Zone: Using modern GetPartBoundsInBox") +print("• Sphere Zone: Auto-detected Ball shape") +print("• Complex Zone: Using GetPartsInPart for precision") +print("• Cylinder Grouped Zone: Full cylinder with top/bottom") +print("• Cylinder Ungrouped Zones: " .. #ungroupedZones .. " separate zones with unique colors") +print("\nšŸ’” Walk into zones to test detection!") +print("šŸ’” Your character will highlight in zone colors:") +print(" šŸ”µ Blue = Box Zone") +print(" šŸ’— Pink = Sphere Zone") +print(" šŸ’š Green = Complex Zone") +print(" 🟠 Orange = Cylinder Grouped Zone") +print(" 🌈 Various Colors = Ungrouped Zones (Purple, Magenta, Cyan, Orange-Yellow, Lime)") +print("šŸ’” Watch the Output for enter/exit events") + +-- Performance monitoring +local lastUpdate = tick() +local frameCount = 0 +RunService.Heartbeat:Connect(function() + frameCount = frameCount + 1 + if tick() - lastUpdate >= 5 then + local fps = math.floor(frameCount / (tick() - lastUpdate)) + print(string.format("šŸ“Š Performance: %d FPS", fps)) + lastUpdate = tick() + frameCount = 0 + end +end) + +print("\nāœ… ZonePlus test script initialized successfully!") +print("šŸ“ Watch the Output window for zone status updates") +]]> + -1 + + 48a5f871ef9fd19708f5cbd9000023b9 + + + + + + + 0 + false + 00000000000000000000000000000000 + StarterCharacterScripts + -1 + + 48a5f871ef9fd19708f5cbd9000003ac + + + + + + + 0 + false + 00000000000000000000000000000000 + StarterPack + -1 + + 48a5f871ef9fd19708f5cbd90000034e + + + + + + 0 + false + 00000000000000000000000000000000 + StarterGui + true + 0 + 4 + true + -1 + null + null + + 48a5f871ef9fd19708f5cbd90000034f + 0 + + + + + + 0 + false + 00000000000000000000000000000000 + LocalizationService + -1 + + 48a5f871ef9fd19708f5cbd900000351 + + + + + + 0 + false + 00000000000000000000000000000000 + Teleport Service + -1 + + 48a5f871ef9fd19708f5cbd900000355 + + + + + + 0 + false + 00000000000000000000000000000000 + CollectionService + -1 + + 48a5f871ef9fd19708f5cbd900000357 + + + + + + 0 + false + 00000000000000000000000000000000 + PhysicsService + -1 + + 48a5f871ef9fd19708f5cbd900000358 + + + + + false + false + + 0 + false + 00000000000000000000000000000000 + InsertService + -1 + + 48a5f871ef9fd19708f5cbd90000035b + + + + + 0 + false + 00000000000000000000000000000000 + InsertionHash + -1 + + 48a5f871ef9fd19708f5cbd9000003ad + {96212A46-BCE5-4D1E-B489-74D65F69472A} + + + + + + + 0 + false + 00000000000000000000000000000000 + GamePassService + -1 + + 48a5f871ef9fd19708f5cbd90000035c + + + + + + 0 + false + 00000000000000000000000000000000 + 1000 + Debris + -1 + + 48a5f871ef9fd19708f5cbd90000035d + + + + + + 0 + false + 00000000000000000000000000000000 + CookiesService + -1 + + 48a5f871ef9fd19708f5cbd90000035e + + + + + + 0 + false + 00000000000000000000000000000000 + Selection + -1 + + 48a5f871ef9fd19708f5cbd90000035f + + + + + + 0 + false + 0 + 1 + false + true + 00000000000000000000000000000000 + 1 + VRService + -1 + + 48a5f871ef9fd19708f5cbd900000363 + + + + + + 0 + false + 00000000000000000000000000000000 + ContextActionService + -1 + + 48a5f871ef9fd19708f5cbd900000364 + + + + + + 0 + false + 00000000000000000000000000000000 + Instance + -1 + + 48a5f871ef9fd19708f5cbd900000365 + + + + + false + + 0 + false + 00000000000000000000000000000000 + AssetService + -1 + + 48a5f871ef9fd19708f5cbd900000366 + + + + + + 0 + false + 00000000000000000000000000000000 + TouchInputService + -1 + + 48a5f871ef9fd19708f5cbd900000367 + + + + + + 0 + false + 00000000000000000000000000000000 + AvatarSettings + -1 + + 48a5f871ef9fd19708f5cbd90000036d + + + + + + 0 + false + 00000000000000000000000000000000 + LuaWebService + -1 + + 48a5f871ef9fd19708f5cbd900000375 + + + + + + 0 + false + 00000000000000000000000000000000 + ProcessInstancePhysicsService + -1 + + 48a5f871ef9fd19708f5cbd900000376 + + + + + + 0 + false + 00000000000000000000000000000000 + ReplicatedStorage + -1 + + 48a5f871ef9fd19708f5cbd900000377 + + + + + 0 + false + 00000000000000000000000000000000 + + Zone + {4613DDB5-0D7C-46C2-B38C-6CAEBB6A6698} + 0 then + -- At least 1 connection active, begin loop + ZoneController._registerConnection(self, triggerType, triggerEventUpper) + elseif previousActiveConnections > 0 and activeConnections == 0 then + -- All connections have disconnected, end loop + ZoneController._deregisterConnection(self, triggerType) + end + end) + end + end + + -- Setup touched receiver functions where applicable + Zone.touchedConnectionActions = {} + for _, triggerType in pairs(triggerTypes) do + local methodName = ("_%sTouchedZone"):format(triggerType) + local correspondingMethod = self[methodName] + if correspondingMethod then + self.trackingTouchedTriggers[triggerType] = {} + Zone.touchedConnectionActions[triggerType] = function(touchedItem) + correspondingMethod(self, touchedItem) + end + end + end + + -- This constructs the zones boundaries, region, etc + self:_update() + + -- Register/deregister zone + ZoneController._registerZone(self) + janitor:add(function() + ZoneController._deregisterZone(self) + end, true) + + return self +end + +function Zone.fromRegion(cframe, size, zoneShape) + local MAX_PART_SIZE = 2024 + local container = Instance.new("Model") + local function createCube(cubeCFrame, cubeSize) + if cubeSize.X > MAX_PART_SIZE or cubeSize.Y > MAX_PART_SIZE or cubeSize.Z > MAX_PART_SIZE then + local quarterSize = cubeSize * 0.25 + local halfSize = cubeSize * 0.5 + createCube(cubeCFrame * CFrame.new(-quarterSize.X, -quarterSize.Y, -quarterSize.Z), halfSize) + createCube(cubeCFrame * CFrame.new(-quarterSize.X, -quarterSize.Y, quarterSize.Z), halfSize) + createCube(cubeCFrame * CFrame.new(-quarterSize.X, quarterSize.Y, -quarterSize.Z), halfSize) + createCube(cubeCFrame * CFrame.new(-quarterSize.X, quarterSize.Y, quarterSize.Z), halfSize) + createCube(cubeCFrame * CFrame.new(quarterSize.X, -quarterSize.Y, -quarterSize.Z), halfSize) + createCube(cubeCFrame * CFrame.new(quarterSize.X, -quarterSize.Y, quarterSize.Z), halfSize) + createCube(cubeCFrame * CFrame.new(quarterSize.X, quarterSize.Y, -quarterSize.Z), halfSize) + createCube(cubeCFrame * CFrame.new(quarterSize.X, quarterSize.Y, quarterSize.Z), halfSize) + else + local part = Instance.new("Part") + part.CFrame = cubeCFrame + part.Size = cubeSize + part.Anchored = true + part.Parent = container + end + end + createCube(cframe, size) + local zone = Zone.new(container) + if zoneShape then + zone.zoneShape = zoneShape + end + zone:relocate() + return zone +end + +function Zone.fromBox(cframe, size) + -- Creates an optimized box-shaped zone using GetPartBoundsInBox + -- Note: This calls fromRegion which auto-relocates the zone + local zone = Zone.fromRegion(cframe, size, enum.ZoneShape.Box) + return zone +end + +function Zone.fromSphere(position, radius) + -- Creates an optimized spherical zone using GetPartBoundsInRadius + -- Creates a physical ball part for the zone + local part = Instance.new("Part") + part.Shape = Enum.PartType.Ball + part.Size = Vector3.new(radius * 2, radius * 2, radius * 2) + part.CFrame = CFrame.new(position) + part.Anchored = true + part.CanCollide = false + part.Transparency = 1 + part.Parent = workspace + + local zone = Zone.new(part) + zone.zoneShape = enum.ZoneShape.Sphere + zone.sphereRadius = radius + zone.spherePosition = position + -- Note: Call zone:relocate() manually if you want to move it to a WorldModel + return zone +end + +-- PRIVATE METHODS +function Zone:_calculateRegion(tableOfParts, dontRound) + local bounds = { ["Min"] = {}, ["Max"] = {} } + for boundType, details in pairs(bounds) do + details.Values = {} + function details.parseCheck(v, currentValue) + if boundType == "Min" then + return (v <= currentValue) + elseif boundType == "Max" then + return (v >= currentValue) + end + end + function details:parse(valuesToParse) + for i, v in pairs(valuesToParse) do + local currentValue = self.Values[i] or v + if self.parseCheck(v, currentValue) then + self.Values[i] = v + end + end + end + end + for _, part in pairs(tableOfParts) do + local sizeHalf = part.Size * 0.5 + local corners = { + part.CFrame * CFrame.new(-sizeHalf.X, -sizeHalf.Y, -sizeHalf.Z), + part.CFrame * CFrame.new(-sizeHalf.X, -sizeHalf.Y, sizeHalf.Z), + part.CFrame * CFrame.new(-sizeHalf.X, sizeHalf.Y, -sizeHalf.Z), + part.CFrame * CFrame.new(-sizeHalf.X, sizeHalf.Y, sizeHalf.Z), + part.CFrame * CFrame.new(sizeHalf.X, -sizeHalf.Y, -sizeHalf.Z), + part.CFrame * CFrame.new(sizeHalf.X, -sizeHalf.Y, sizeHalf.Z), + part.CFrame * CFrame.new(sizeHalf.X, sizeHalf.Y, -sizeHalf.Z), + part.CFrame * CFrame.new(sizeHalf.X, sizeHalf.Y, sizeHalf.Z), + } + for _, cornerCFrame in pairs(corners) do + local x, y, z = cornerCFrame:GetComponents() + local values = { x, y, z } + bounds.Min:parse(values) + bounds.Max:parse(values) + end + end + local minBound = {} + local maxBound = {} + -- Rounding a regions coordinates to multiples of 4 ensures the region optimises the region + -- by ensuring it aligns on the voxel grid + local function roundToFour(to_round) + local ROUND_TO = 4 + local divided = (to_round + ROUND_TO / 2) / ROUND_TO + local rounded = ROUND_TO * math.floor(divided) + return rounded + end + for boundName, boundDetail in pairs(bounds) do + for _, v in pairs(boundDetail.Values) do + local newTable = (boundName == "Min" and minBound) or maxBound + local newV = v + if not dontRound then + local roundOffset = (boundName == "Min" and -2) or 2 + newV = roundToFour(v + roundOffset) -- +-2 to ensures the zones region is not rounded down/up + end + table.insert(newTable, newV) + end + end + local boundMin = Vector3.new(unpack(minBound)) + local boundMax = Vector3.new(unpack(maxBound)) + -- Convert bounds to CFrame + Size (modern approach instead of deprecated Region3) + local regionSize = boundMax - boundMin + local regionCFrame = CFrame.new((boundMin + boundMax) / 2) + return regionCFrame, regionSize, boundMin, boundMax +end + +function Zone:_displayBounds() + if not self.displayBoundParts then + self.displayBoundParts = true + local boundParts = { BoundMin = self.boundMin, BoundMax = self.boundMax } + for boundName, boundCFrame in pairs(boundParts) do + local part = Instance.new("Part") + part.Anchored = true + part.CanCollide = false + part.Transparency = 0.5 + part.Size = Vector3.new(1, 1, 1) + part.Color = Color3.fromRGB(255, 0, 0) + part.CFrame = CFrame.new(boundCFrame) + part.Name = boundName + part.Parent = workspace + self.janitor:add(part, "Destroy") + end + end +end + +function Zone:_update() + local container = self.container + local zoneParts = {} + local updateQueue = 0 + self._updateConnections:clean() + + local containerType = typeof(container) + local holders = {} + local INVALID_TYPE_WARNING = "The zone container must be a model, folder, basepart or table!" + if containerType == "table" then + for _, part in pairs(container) do + if part:IsA("BasePart") then + table.insert(zoneParts, part) + end + end + elseif containerType == "Instance" then + if container:IsA("BasePart") then + table.insert(zoneParts, container) + else + table.insert(holders, container) + for _, part in pairs(container:GetDescendants()) do + if part:IsA("BasePart") then + table.insert(zoneParts, part) + else + table.insert(holders, part) + end + end + end + end + self.zoneParts = zoneParts + self.overlapParams = {} + + local allZonePartsAreBlocksNew = true + for _, zonePart in pairs(zoneParts) do + local success, shapeName = pcall(function() + return zonePart.Shape.Name + end) + if shapeName ~= "Block" then + allZonePartsAreBlocksNew = false + end + end + self.allZonePartsAreBlocks = allZonePartsAreBlocksNew + + local zonePartsWhitelist = OverlapParams.new() + zonePartsWhitelist.FilterType = Enum.RaycastFilterType.Include + zonePartsWhitelist.MaxParts = #zoneParts + zonePartsWhitelist.FilterDescendantsInstances = zoneParts + self.overlapParams.zonePartsWhitelist = zonePartsWhitelist + + local zonePartsIgnorelist = OverlapParams.new() + zonePartsIgnorelist.FilterType = Enum.RaycastFilterType.Exclude + zonePartsIgnorelist.FilterDescendantsInstances = zoneParts + self.overlapParams.zonePartsIgnorelist = zonePartsIgnorelist + + -- this will call update on the zone when the container parts size or position changes, and when a + -- child is removed or added from a holder (anything which isn't a basepart) + local function update() + if self.autoUpdate then + local executeTime = os.clock() + if self.respectUpdateQueue then + updateQueue += 1 + executeTime += 0.1 + end + local updateConnection + updateConnection = runService.Heartbeat:Connect(function() + if os.clock() >= executeTime then + updateConnection:Disconnect() + if self.respectUpdateQueue then + updateQueue -= 1 + end + if updateQueue == 0 and self.zoneId then + self:_update() + end + end + end) + end + end + local partProperties = { "Size", "Position" } + local function verifyDefaultCollision(instance) + if instance.CollisionGroupId ~= 0 then + error( + "Zone parts must belong to the 'Default' (0) CollisionGroup! Consider using zone:relocate() if you wish to move zones outside of workspace to prevent them interacting with other parts." + ) + end + end + for _, part in pairs(zoneParts) do + for _, prop in pairs(partProperties) do + self._updateConnections:add(part:GetPropertyChangedSignal(prop):Connect(update), "Disconnect") + end + verifyDefaultCollision(part) + self._updateConnections:add( + part:GetPropertyChangedSignal("CollisionGroupId"):Connect(function() + verifyDefaultCollision(part) + end), + "Disconnect" + ) + end + local containerEvents = { "ChildAdded", "ChildRemoved" } + for _, holder in pairs(holders) do + for _, event in pairs(containerEvents) do + self._updateConnections:add( + self.container[event]:Connect(function(child) + if child:IsA("BasePart") then + update() + end + end), + "Disconnect" + ) + end + end + + local regionCFrame, regionSize, boundMin, boundMax = self:_calculateRegion(zoneParts) + local exactRegionCFrame, exactRegionSize, _, _ = self:_calculateRegion(zoneParts, true) + self.regionCFrame = regionCFrame + self.regionSize = regionSize + self.exactRegionCFrame = exactRegionCFrame + self.exactRegionSize = exactRegionSize + self.boundMin = boundMin + self.boundMax = boundMax + self.volume = regionSize.X * regionSize.Y * regionSize.Z + + -- Update: I was going to use this for the old part detection until the CanTouch property was released + -- everything below is now irrelevant however I'll keep just in case I use again for future + ------------------------------------------------------------------------------------------------- + -- When a zones region is determined, we also check for parts already existing within the zone + -- these parts are likely never to move or interact with the zone, so we set the number of these + -- to the baseline MaxParts value. 'recommendMaxParts' is then determined through the sum of this + -- and maxPartsAddition. This ultimately optimises region checks as they can be generated with + -- minimal MaxParts (i.e. recommendedMaxParts can be used instead of math.huge every time) + --[[ + local result = self.worldModel:FindPartsInRegion3(region, nil, math.huge) + local maxPartsBaseline = #result + self.recommendedMaxParts = maxPartsBaseline + self.maxPartsAddition + --]] + + self:_updateTouchedConnections() + + self.updated:Fire() +end + +function Zone:_updateOccupants(trackerName, newOccupants) + local previousOccupants = self.occupants[trackerName] + if not previousOccupants then + previousOccupants = {} + self.occupants[trackerName] = previousOccupants + end + local signalsToFire = {} + for occupant, prevItem in pairs(previousOccupants) do + local newItem = newOccupants[occupant] + if newItem == nil or newItem ~= prevItem then + previousOccupants[occupant] = nil + if not signalsToFire.exited then + signalsToFire.exited = {} + end + table.insert(signalsToFire.exited, occupant) + end + end + for occupant, _ in pairs(newOccupants) do + if previousOccupants[occupant] == nil then + local isAPlayer = occupant:IsA("Player") + previousOccupants[occupant] = (isAPlayer and occupant.Character) or true + if not signalsToFire.entered then + signalsToFire.entered = {} + end + table.insert(signalsToFire.entered, occupant) + end + end + return signalsToFire +end + +function Zone:_formTouchedConnection(triggerType) + local touchedJanitorName = "_touchedJanitor" .. triggerType + local touchedJanitor = self[touchedJanitorName] + if touchedJanitor then + touchedJanitor:clean() + else + touchedJanitor = self.janitor:add(Janitor.new(), "destroy") + self[touchedJanitorName] = touchedJanitor + end + self:_updateTouchedConnection(triggerType) +end + +function Zone:_updateTouchedConnection(triggerType) + local touchedJanitorName = "_touchedJanitor" .. triggerType + local touchedJanitor = self[touchedJanitorName] + if not touchedJanitor then + return + end + for _, basePart in pairs(self.zoneParts) do + touchedJanitor:add(basePart.Touched:Connect(self.touchedConnectionActions[triggerType], self), "Disconnect") + end +end + +function Zone:_updateTouchedConnections() + for triggerType, _ in pairs(self.touchedConnectionActions) do + local touchedJanitorName = "_touchedJanitor" .. triggerType + local touchedJanitor = self[touchedJanitorName] + if touchedJanitor then + touchedJanitor:cleanup() + self:_updateTouchedConnection(triggerType) + end + end +end + +function Zone:_disconnectTouchedConnection(triggerType) + local touchedJanitorName = "_touchedJanitor" .. triggerType + local touchedJanitor = self[touchedJanitorName] + if touchedJanitor then + touchedJanitor:cleanup() + self[touchedJanitorName] = nil + end +end + +local function round(number, decimalPlaces) + return math.round(number * 10 ^ decimalPlaces) * 10 ^ -decimalPlaces +end +function Zone:_partTouchedZone(part) + local trackingDict = self.trackingTouchedTriggers["part"] + if trackingDict[part] then + return + end + local nextCheck = 0 + local verifiedEntrance = false + local enterPosition = part.Position + local enterTime = os.clock() + local partJanitor = self.janitor:add(Janitor.new(), "destroy") + trackingDict[part] = partJanitor + local instanceClassesToIgnore = { Seat = true, VehicleSeat = true } + local instanceNamesToIgnore = { HumanoidRootPart = true } + if not (instanceClassesToIgnore[part.ClassName] or not instanceNamesToIgnore[part.Name]) then + part.CanTouch = false + end + -- + local partVolume = round((part.Size.X * part.Size.Y * part.Size.Z), 5) + self.totalPartVolume += partVolume + -- + partJanitor:add( + heartbeat:Connect(function() + local clockTime = os.clock() + if clockTime >= nextCheck then + ---- + local cooldown = enum.Accuracy.getProperty(self.accuracy) + nextCheck = clockTime + cooldown + ---- + + -- We initially perform a singular point check as this is vastly more lightweight than a large part check + -- If the former returns false, perform a whole part check in case the part is on the outer bounds. + local withinZone = self:findPoint(part.CFrame) + if not withinZone then + withinZone = self:findPart(part) + end + if not verifiedEntrance then + if withinZone then + verifiedEntrance = true + self.partEntered:Fire(part) + elseif (part.Position - enterPosition).Magnitude > 1.5 and clockTime - enterTime >= cooldown then + -- Even after the part has exited the zone, we track it for a brief period of time based upon the criteria + -- in the line above to ensure the .touched behaviours are not abused + partJanitor:cleanup() + end + elseif not withinZone then + verifiedEntrance = false + enterPosition = part.Position + enterTime = os.clock() + self.partExited:Fire(part) + end + end + end), + "Disconnect" + ) + partJanitor:add(function() + trackingDict[part] = nil + part.CanTouch = true + self.totalPartVolume = round((self.totalPartVolume - partVolume), 5) + end, true) +end + +local partShapeActions = { + ["Ball"] = function(part) + return "GetPartBoundsInRadius", { part.Position, part.Size.X } + end, + ["Block"] = function(part) + return "GetPartBoundsInBox", { part.CFrame, part.Size } + end, + ["Other"] = function(part) + return "GetPartsInPart", { part } + end, +} +function Zone:_getRegionConstructor(part, overlapParams) + local success, shapeName = pcall(function() + return part.Shape.Name + end) + local methodName, args + if success and self.allZonePartsAreBlocks then + local action = partShapeActions[shapeName] + if action then + methodName, args = action(part) + end + end + if not methodName then + methodName, args = partShapeActions.Other(part) + end + if overlapParams then + table.insert(args, overlapParams) + end + return methodName, args +end + +-- PUBLIC METHODS +function Zone:findLocalPlayer() + if not localPlayer then + error("Can only call 'findLocalPlayer' on the client!") + end + return self:findPlayer(localPlayer) +end + +function Zone:_find(trackerName, item) + ZoneController.updateDetection(self) + local tracker = ZoneController.trackers[trackerName] + local touchingZones = ZoneController.getTouchingZones(item, false, self._currentEnterDetection, tracker) + for _, zone in pairs(touchingZones) do + if zone == self then + return true + end + end + return false +end + +function Zone:findPlayer(player) + local character = player.Character + local humanoid = character and character:FindFirstChildOfClass("Humanoid") + if not humanoid then + return false + end + return self:_find("player", player.Character) +end + +function Zone:findItem(item) + return self:_find("item", item) +end + +function Zone:findPart(part) + local methodName, args = self:_getRegionConstructor(part, self.overlapParams.zonePartsWhitelist) + local touchingZoneParts = self.worldModel[methodName](self.worldModel, unpack(args)) + --local touchingZoneParts = self.worldModel:GetPartsInPart(part, self.overlapParams.zonePartsWhitelist) + if #touchingZoneParts > 0 then + return true, touchingZoneParts + end + return false +end + +function Zone:getCheckerPart() + local checkerPart = self.checkerPart + if not checkerPart then + checkerPart = self.janitor:add(Instance.new("Part"), "Destroy") + checkerPart.Size = Vector3.new(0.1, 0.1, 0.1) + checkerPart.Name = "ZonePlusCheckerPart" + checkerPart.Anchored = true + checkerPart.Transparency = 1 + checkerPart.CanCollide = false + self.checkerPart = checkerPart + end + local checkerParent = self.worldModel + if checkerParent == workspace then + checkerParent = ZoneController.getWorkspaceContainer() + end + if checkerPart.Parent ~= checkerParent then + checkerPart.Parent = checkerParent + end + return checkerPart +end + +function Zone:findPoint(positionOrCFrame) + local cframe = positionOrCFrame + if typeof(positionOrCFrame) == "Vector3" then + cframe = CFrame.new(positionOrCFrame) + end + local checkerPart = self:getCheckerPart() + checkerPart.CFrame = cframe + --checkerPart.Parent = self.worldModel + local methodName, args = self:_getRegionConstructor(checkerPart, self.overlapParams.zonePartsWhitelist) + local touchingZoneParts = self.worldModel[methodName](self.worldModel, unpack(args)) + --local touchingZoneParts = self.worldModel:GetPartsInPart(self.checkerPart, self.overlapParams.zonePartsWhitelist) + if #touchingZoneParts > 0 then + return true, touchingZoneParts + end + return false +end + +function Zone:_getAll(trackerName) + ZoneController.updateDetection(self) + local itemsArray = {} + local zonesAndOccupants = + ZoneController._getZonesAndItems(trackerName, { self = true }, self.volume, false, self._currentEnterDetection) + local occupantsDict = zonesAndOccupants[self] + if occupantsDict then + for item, _ in pairs(occupantsDict) do + table.insert(itemsArray, item) + end + end + return itemsArray +end + +function Zone:getPlayers() + return self:_getAll("player") +end + +function Zone:getItems() + return self:_getAll("item") +end + +function Zone:getParts() + -- This is designed for infrequent 'one off' use + -- If you plan on checking for parts within a zone frequently, it's recommended you + -- use the .partEntered and .partExited events instead. + local partsArray = {} + if self.activeTriggers["part"] then + local trackingDict = self.trackingTouchedTriggers["part"] + for part, _ in pairs(trackingDict) do + table.insert(partsArray, part) + end + return partsArray + end + local partsInRegion = + self.worldModel:GetPartBoundsInBox(self.regionCFrame, self.regionSize, self.overlapParams.zonePartsIgnorelist) + for _, part in pairs(partsInRegion) do + if self:findPart(part) then + table.insert(partsArray, part) + end + end + return partsArray +end + +function Zone:getRandomPoint() + local cframe = self.exactRegionCFrame + local size = self.exactRegionSize + local random = Random.new() + local randomCFrame + local success, touchingZoneParts + local pointIsWithinZone + repeat + randomCFrame = cframe + * CFrame.new( + random:NextNumber(-size.X / 2, size.X / 2), + random:NextNumber(-size.Y / 2, size.Y / 2), + random:NextNumber(-size.Z / 2, size.Z / 2) + ) + success, touchingZoneParts = self:findPoint(randomCFrame) + if success then + pointIsWithinZone = true + end + until pointIsWithinZone + local randomVector = randomCFrame.Position + return randomVector, touchingZoneParts +end + +function Zone:setAccuracy(enumIdOrName) + local enumId = tonumber(enumIdOrName) + if not enumId then + enumId = enum.Accuracy[enumIdOrName] + if not enumId then + error(("'%s' is an invalid enumName!"):format(enumIdOrName)) + end + else + local enumName = enum.Accuracy.getName(enumId) + if not enumName then + error(("%s is an invalid enumId!"):format(enumId)) + end + end + self.accuracy = enumId +end + +function Zone:setZoneShape(enumIdOrName) + -- Allows changing the spatial query method used for this zone + -- Options: "Auto", "Box", "Sphere" + local enumId = tonumber(enumIdOrName) + if not enumId then + enumId = enum.ZoneShape[enumIdOrName] + if not enumId then + error(("'%s' is an invalid enumName!"):format(enumIdOrName)) + end + else + local enumName = enum.ZoneShape.getName(enumId) + if not enumName then + error(("%s is an invalid enumId!"):format(enumId)) + end + end + self.zoneShape = enumId +end + +function Zone:setDetection(enumIdOrName) + local enumId = tonumber(enumIdOrName) + if not enumId then + enumId = enum.Detection[enumIdOrName] + if not enumId then + error(("'%s' is an invalid enumName!"):format(enumIdOrName)) + end + else + local enumName = enum.Detection.getName(enumId) + if not enumName then + error(("%s is an invalid enumId!"):format(enumId)) + end + end + self.enterDetection = enumId + self.exitDetection = enumId +end + +function Zone:trackItem(instance) + local isBasePart = instance:IsA("BasePart") + local isCharacter = false + if not isBasePart then + isCharacter = instance:FindFirstChildOfClass("Humanoid") and instance:FindFirstChild("HumanoidRootPart") + end + + assert(isBasePart or isCharacter, "Only BaseParts or Characters/NPCs can be tracked!") + + if self.trackedItems[instance] then + return + end + if self.itemsToUntrack[instance] then + self.itemsToUntrack[instance] = nil + end + + local itemJanitor = self.janitor:add(Janitor.new(), "destroy") + local itemDetail = { + janitor = itemJanitor, + item = instance, + isBasePart = isBasePart, + isCharacter = isCharacter, + } + self.trackedItems[instance] = itemDetail + + itemJanitor:add( + instance.AncestryChanged:Connect(function() + if not instance:IsDescendantOf(game) then + self:untrackItem(instance) + end + end), + "Disconnect" + ) + + local Tracker = require(trackerModule) + Tracker.itemAdded:Fire(itemDetail) +end + +function Zone:untrackItem(instance) + local itemDetail = self.trackedItems[instance] + if itemDetail then + itemDetail.janitor:destroy() + end + self.trackedItems[instance] = nil + + local Tracker = require(trackerModule) + Tracker.itemRemoved:Fire(itemDetail) +end + +function Zone:bindToGroup(settingsGroupName) + self:unbindFromGroup() + local group = ZoneController.getGroup(settingsGroupName) or ZoneController.setGroup(settingsGroupName) + group._memberZones[self.zoneId] = self + self.settingsGroupName = settingsGroupName +end + +function Zone:unbindFromGroup() + if self.settingsGroupName then + local group = ZoneController.getGroup(self.settingsGroupName) + if group then + group._memberZones[self.zoneId] = nil + end + self.settingsGroupName = nil + end +end + +function Zone:relocate() + if self.hasRelocated then + return + end + + local CollectiveWorldModel = require(collectiveWorldModelModule) + local worldModel = CollectiveWorldModel.setupWorldModel(self) + self.worldModel = worldModel + self.hasRelocated = true + + local relocationContainer = self.container + if typeof(relocationContainer) == "table" then + relocationContainer = Instance.new("Folder") + for _, zonePart in pairs(self.zoneParts) do + zonePart.Parent = relocationContainer + end + end + self.relocationContainer = self.janitor:add(relocationContainer, "Destroy", "RelocationContainer") + relocationContainer.Parent = worldModel +end + +function Zone:_onItemCallback(eventName, desiredValue, instance, callbackFunction) + local detail = self.onItemDetails[instance] + if not detail then + detail = {} + self.onItemDetails[instance] = detail + end + if #detail == 0 then + self.itemsToUntrack[instance] = true + end + table.insert(detail, instance) + self:trackItem(instance) + + local function triggerCallback() + callbackFunction() + if self.itemsToUntrack[instance] then + self.itemsToUntrack[instance] = nil + self:untrackItem(instance) + end + end + + local inZoneAlready = self:findItem(instance) + if inZoneAlready == desiredValue then + triggerCallback() + else + local connection + connection = self[eventName]:Connect(function(item) + if connection and item == instance then + connection:Disconnect() + connection = nil + triggerCallback() + end + end) + --[[ + if typeof(expireAfterSeconds) == "number" then + task.delay(expireAfterSeconds, function() + if connection ~= nil then + print("EXPIRE!") + connection:Disconnect() + connection = nil + triggerCallback() + end + end) + end + --]] + end +end + +function Zone:onItemEnter(...) + self:_onItemCallback("itemEntered", true, ...) +end + +function Zone:onItemExit(...) + self:_onItemCallback("itemExited", false, ...) +end + +function Zone:destroy() + self:unbindFromGroup() + self.janitor:destroy() +end +Zone.Destroy = Zone.destroy + +return Zone +]]> + -1 + + 48a5f871ef9fd19708f5cbd900001fe2 + + + + + 0 + false + 00000000000000000000000000000000 + + Enum + {7B92D5D9-C70A-4471-816C-09E835685A29} + + -1 + + 48a5f871ef9fd19708f5cbd900001fe3 + + + + + 0 + false + 00000000000000000000000000000000 + + Accuracy + {CFB415CC-A9B9-42AE-AE9E-2D6C39EA680B} + + -1 + + 48a5f871ef9fd19708f5cbd900001fe4 + + + + + + 0 + false + 00000000000000000000000000000000 + + Detection + {F93DAC13-373E-4076-B182-2F7A0552B8B3} + + -1 + + 48a5f871ef9fd19708f5cbd900001fe5 + + + + + + 0 + false + 00000000000000000000000000000000 + + ZoneShape + {4E359F04-8F97-413F-86FB-165E041A270C} + + -1 + + 48a5f871ef9fd19708f5cbd90000204e + + + + + + + 0 + false + 00000000000000000000000000000000 + + Janitor + {F78A5C6C-BF0E-4931-9671-3FF1D962E45F} + " +end + +--[[** + "Links" this Janitor to an Instance, such that the Janitor will `Cleanup` when the Instance is `Destroyed()` and garbage collected. A Janitor may only be linked to one instance at a time, unless `AllowMultiple` is true. When called with a truthy `AllowMultiple` parameter, the Janitor will "link" the Instance without overwriting any previous links, and will also not be overwritable. When called with a falsy `AllowMultiple` parameter, the Janitor will overwrite the previous link which was also called with a falsy `AllowMultiple` parameter, if applicable. + @param [t:Instance] Object The instance you want to link the Janitor to. + @param [t:boolean?] AllowMultiple Whether or not to allow multiple links on the same Janitor. + @returns [t:RbxScriptConnection] A pseudo RBXScriptConnection that can be disconnected. +**--]] +function Janitor.__index:LinkToInstance(Object, AllowMultiple) + local Connection + local IndexToUse = AllowMultiple and newproxy(false) or LinkToInstanceIndex + local IsNilParented = Object.Parent == nil + local ManualDisconnect = setmetatable({}, Disconnect) + + local function ChangedFunction(_DoNotUse, NewParent) + if ManualDisconnect.Connected then + _DoNotUse = nil + IsNilParented = NewParent == nil + + if IsNilParented then + coroutine.wrap(function() + Heartbeat:Wait() + if not ManualDisconnect.Connected then + return + elseif not Connection.Connected then + self:Cleanup() + else + while IsNilParented and Connection.Connected and ManualDisconnect.Connected do + Heartbeat:Wait() + end + + if ManualDisconnect.Connected and IsNilParented then + self:Cleanup() + end + end + end)() + end + end + end + + Connection = Object.AncestryChanged:Connect(ChangedFunction) + ManualDisconnect.Connection = Connection + + if IsNilParented then + ChangedFunction(nil, Object.Parent) + end + + Object = nil + return self:Add(ManualDisconnect, "Disconnect", IndexToUse) +end + +--[[** + Links several instances to a janitor, which is then returned. + @param [t:...Instance] ... All the instances you want linked. + @returns [t:Janitor] A janitor that can be used to manually disconnect all LinkToInstances. +**--]] +function Janitor.__index:LinkToInstances(...) + local ManualCleanup = Janitor.new() + for _, Object in ipairs({...}) do + ManualCleanup:Add(self:LinkToInstance(Object, true), "Disconnect") + end + + return ManualCleanup +end + +for FunctionName, Function in next, Janitor.__index do + local NewFunctionName = string.sub(string.lower(FunctionName), 1, 1) .. string.sub(FunctionName, 2) + Janitor.__index[NewFunctionName] = Function +end + +return Janitor]]> + -1 + + 48a5f871ef9fd19708f5cbd900001fe6 + + + + + + 0 + false + 00000000000000000000000000000000 + + OldSignal + {235E203C-7385-468E-A40A-147F2032E10F} + 0 then + local packedArgs = table.pack(...) + for waitingId, _ in pairs(self.waiting) do + self.waiting[waitingId] = packedArgs + end + end +end +Signal.fire = Signal.Fire + +function Signal:Connect(handler) + if not (type(handler) == "function") then + error(("connect(%s)"):format(typeof(handler)), 2) + end + + local signal = self + local connectionId = HttpService:GenerateGUID(false) + local connection = {} + connection.Connected = true + connection.ConnectionId = connectionId + connection.Handler = handler + self.connections[connectionId] = connection + + function connection:Disconnect() + signal.connections[connectionId] = nil + connection.Connected = false + signal.totalConnections -= 1 + if signal.connectionsChanged then + signal.connectionsChanged:Fire(-1) + end + end + connection.Destroy = connection.Disconnect + connection.destroy = connection.Disconnect + connection.disconnect = connection.Disconnect + self.totalConnections += 1 + if self.connectionsChanged then + self.connectionsChanged:Fire(1) + end + + return connection +end +Signal.connect = Signal.Connect + +function Signal:Wait() + local waitingId = HttpService:GenerateGUID(false) + self.waiting[waitingId] = true + self.totalWaiting += 1 + repeat heartbeat:Wait() until self.waiting[waitingId] ~= true + self.totalWaiting -= 1 + local args = self.waiting[waitingId] + self.waiting[waitingId] = nil + return unpack(args) +end +Signal.wait = Signal.Wait + +function Signal:Destroy() + if self.bindableEvent then + self.bindableEvent:Destroy() + self.bindableEvent = nil + end + if self.connectionsChanged then + self.connectionsChanged:Fire(-self.totalConnections) + self.connectionsChanged:Destroy() + self.connectionsChanged = nil + end + self.totalConnections = 0 + for connectionId, connection in pairs(self.connections) do + self.connections[connectionId] = nil + end +end +Signal.destroy = Signal.Destroy +Signal.Disconnect = Signal.Destroy +Signal.disconnect = Signal.Destroy + + + +return Signal]]> + -1 + + 48a5f871ef9fd19708f5cbd900001fe7 + + + + + + 0 + false + 00000000000000000000000000000000 + + Signal + {EC5B02DA-1940-45D9-8C31-48431E966C4B} + + -1 + + 48a5f871ef9fd19708f5cbd900001fe8 + + + + + + 0 + false + 00000000000000000000000000000000 + + VERSION + {FB1EF9A3-F12B-4C72-BE04-25C595E5DD54} + + -1 + + 48a5f871ef9fd19708f5cbd900001fe9 + + + + + + 0 + false + 00000000000000000000000000000000 + + ZoneController + {42E46B32-946C-4E65-9DDD-910BDA354616} + WHOLE_BODY_DETECTION_LIMIT then + detection = enum.Detection.Centre + else + detection = enum.Detection.WholeBody + end + end + zone[currentDetectionName] = detection + end +end + +function ZoneController._formHeartbeat(registeredTriggerType) + local heartbeatConnection = heartbeatConnections[registeredTriggerType] + if heartbeatConnection then + return + end + -- This will only ever connect once per triggerType per server + -- This means instead of initiating a loop per-zone we can handle everything within + -- a singular connection. This is particularly beneficial for player/item-orinetated + -- checking, where a check only needs to be cast once per interval, as apposed + -- to every zone per interval + -- I utilise heartbeat with os.clock() to provide precision (where needed) and flexibility + local nextCheck = 0 + heartbeatConnection = heartbeat:Connect(function() + local clockTime = os.clock() + if clockTime >= nextCheck then + local lowestAccuracy + local lowestDetection + for zone, _ in pairs(activeZones) do + if zone.activeTriggers[registeredTriggerType] then + local zAccuracy = zone.accuracy + if lowestAccuracy == nil or zAccuracy < lowestAccuracy then + lowestAccuracy = zAccuracy + end + ZoneController.updateDetection(zone) + local zDetection = zone._currentEnterDetection + if lowestDetection == nil or zDetection < lowestDetection then + lowestDetection = zDetection + end + end + end + local highestAccuracy = lowestAccuracy + local zonesAndOccupants = heartbeatActions[registeredTriggerType](lowestDetection) + + -- If a zone belongs to a settingsGroup with 'onlyEnterOnceExitedAll = true' , and the occupant already exists in a member group, then + -- ignore all incoming occupants for the other zones (preventing the enteredSignal from being fired until the occupant has left + -- all other zones within the same settingGroup) + local occupantsToBlock = {} + local zonesToPotentiallyIgnore = {} + for zone, newOccupants in pairs(zonesAndOccupants) do + local settingsGroup = (zone.settingsGroupName and ZoneController.getGroup(zone.settingsGroupName)) + if settingsGroup and settingsGroup.onlyEnterOnceExitedAll == true then + --local currentOccupants = zone.occupants[registeredTriggerType] + --if currentOccupants then + for newOccupant, _ in pairs(newOccupants) do + --if currentOccupants[newOccupant] then + local groupDetail = occupantsToBlock[zone.settingsGroupName] + if not groupDetail then + groupDetail = {} + occupantsToBlock[zone.settingsGroupName] = groupDetail + end + groupDetail[newOccupant] = zone + --end + end + zonesToPotentiallyIgnore[zone] = newOccupants + --end + end + end + for zone, newOccupants in pairs(zonesToPotentiallyIgnore) do + local groupDetail = occupantsToBlock[zone.settingsGroupName] + if groupDetail then + for newOccupant, _ in pairs(newOccupants) do + local occupantToKeepZone = groupDetail[newOccupant] + if occupantToKeepZone and occupantToKeepZone ~= zone then + newOccupants[newOccupant] = nil + end + end + end + end + + -- This deduces what signals should be fired + local collectiveSignalsToFire = { {}, {} } + for zone, _ in pairs(activeZones) do + if zone.activeTriggers[registeredTriggerType] then + local zAccuracy = zone.accuracy + local occupantsDict = zonesAndOccupants[zone] or {} + local occupantsPresent = false + for k, v in pairs(occupantsDict) do + occupantsPresent = true + break + end + if occupantsPresent and zAccuracy > highestAccuracy then + highestAccuracy = zAccuracy + end + local signalsToFire = zone:_updateOccupants(registeredTriggerType, occupantsDict) + collectiveSignalsToFire[1][zone] = signalsToFire.exited + collectiveSignalsToFire[2][zone] = signalsToFire.entered + end + end + + -- This ensures all exited signals and called before entered signals + local indexToSignalType = { "Exited", "Entered" } + for index, zoneAndOccupants in pairs(collectiveSignalsToFire) do + local signalType = indexToSignalType[index] + local signalName = registeredTriggerType .. signalType + for zone, occupants in pairs(zoneAndOccupants) do + local signal = zone[signalName] + if signal then + for _, occupant in pairs(occupants) do + signal:Fire(occupant) + end + end + end + end + + local cooldown = enum.Accuracy.getProperty(highestAccuracy) + nextCheck = clockTime + cooldown + end + end) + heartbeatConnections[registeredTriggerType] = heartbeatConnection +end + +function ZoneController._deregisterConnection(registeredZone, registeredTriggerType) + activeConnections -= 1 + if activeTriggers[registeredTriggerType] == 1 then + activeTriggers[registeredTriggerType] = nil + local heartbeatConnection = heartbeatConnections[registeredTriggerType] + if heartbeatConnection then + heartbeatConnections[registeredTriggerType] = nil + heartbeatConnection:Disconnect() + end + else + activeTriggers[registeredTriggerType] -= 1 + end + registeredZone.activeTriggers[registeredTriggerType] = nil + if dictLength(registeredZone.activeTriggers) == 0 then + activeZones[registeredZone] = nil + ZoneController._updateZoneDetails() + end + if registeredZone.touchedConnectionActions[registeredTriggerType] then + registeredZone:_disconnectTouchedConnection(registeredTriggerType) + end +end + +function ZoneController._updateZoneDetails() + activeParts = {} + activePartToZone = {} + allParts = {} + allPartToZone = {} + activeZonesTotalVolume = 0 + for zone, _ in pairs(registeredZones) do + local isActive = activeZones[zone] + if isActive then + activeZonesTotalVolume += zone.volume + end + for _, zonePart in pairs(zone.zoneParts) do + if isActive then + table.insert(activeParts, zonePart) + activePartToZone[zonePart] = zone + end + table.insert(allParts, zonePart) + allPartToZone[zonePart] = zone + end + end +end + +function ZoneController._getZonesAndItems( + trackerName, + zonesDictToCheck, + zoneCustomVolume, + onlyActiveZones, + recommendedDetection +) + local totalZoneVolume = zoneCustomVolume + if not totalZoneVolume then + for zone, _ in pairs(zonesDictToCheck) do + totalZoneVolume += zone.volume + end + end + local zonesAndOccupants = {} + local tracker = trackers[trackerName] + if tracker.totalVolume < totalZoneVolume then + -- If the volume of all *characters/items* within the server is *less than* the total + -- volume of all active zones (i.e. zones which listen for .playerEntered) + -- then it's more efficient cast checks within each character and + -- then determine the zones they belong to + for _, item in pairs(tracker.items) do + local touchingZones = ZoneController.getTouchingZones(item, onlyActiveZones, recommendedDetection, tracker) + for _, zone in pairs(touchingZones) do + if not onlyActiveZones or zone.activeTriggers[trackerName] then + local finalItem = item + if trackerName == "player" then + finalItem = players:GetPlayerFromCharacter(item) + end + if finalItem then + fillOccupants(zonesAndOccupants, zone, finalItem) + end + end + end + end + else + -- If the volume of all *active zones* within the server is *less than* the total + -- volume of all characters/items, then it's more efficient to perform the + -- checks directly within each zone to determine players inside + for zone, _ in pairs(zonesDictToCheck) do + if not onlyActiveZones or zone.activeTriggers[trackerName] then + local result = + CollectiveWorldModel:GetPartBoundsInBox(zone.regionCFrame, zone.regionSize, tracker.whitelistParams) + local finalItemsDict = {} + for _, itemOrChild in pairs(result) do + local correspondingItem = tracker.partToItem[itemOrChild] + if not finalItemsDict[correspondingItem] then + finalItemsDict[correspondingItem] = true + end + end + for item, _ in pairs(finalItemsDict) do + if trackerName == "player" then + local player = players:GetPlayerFromCharacter(item) + if zone:findPlayer(player) then + fillOccupants(zonesAndOccupants, zone, player) + end + elseif zone:findItem(item) then + fillOccupants(zonesAndOccupants, zone, item) + end + end + end + end + end + return zonesAndOccupants +end + +-- PUBLIC FUNCTIONS +function ZoneController.getZones() + local registeredZonesArray = {} + for zone, _ in pairs(registeredZones) do + table.insert(registeredZonesArray, zone) + end + return registeredZonesArray +end + +--[[ +-- the player touched events which utilise active zones at the moment may change to the new CanTouch method for parts in the future +-- hence im disabling this as it may be depreciated quite soon +function ZoneController.getActiveZones() + local zonesArray = {} + for zone, _ in pairs(activeZones) do + table.insert(zonesArray, zone) + end + return zonesArray +end +--]] + +function ZoneController.getTouchingZones(item, onlyActiveZones, recommendedDetection, tracker) + local exitDetection, finalDetection + if tracker then + exitDetection = tracker.exitDetections[item] + tracker.exitDetections[item] = nil + end + finalDetection = exitDetection or recommendedDetection + + local itemSize, itemCFrame + local itemIsBasePart = item:IsA("BasePart") + local itemIsCharacter = not itemIsBasePart + local bodyPartsToCheck = {} + if itemIsBasePart then + itemSize, itemCFrame = item.Size, item.CFrame + table.insert(bodyPartsToCheck, item) + elseif finalDetection == enum.Detection.WholeBody then + itemSize, itemCFrame = Tracker.getCharacterSize(item) + bodyPartsToCheck = item:GetChildren() + else + local hrp = item:FindFirstChild("HumanoidRootPart") + if hrp then + itemSize, itemCFrame = hrp.Size, hrp.CFrame + table.insert(bodyPartsToCheck, hrp) + end + end + if not itemSize or not itemCFrame then + return {} + end + + --[[ + local part = Instance.new("Part") + part.Size = itemSize + part.CFrame = itemCFrame + part.Anchored = true + part.CanCollide = false + part.Color = Color3.fromRGB(255, 0, 0) + part.Transparency = 0.4 + part.Parent = workspace + game:GetService("Debris"):AddItem(part, 2) + --]] + local partsTable = (onlyActiveZones and activeParts) or allParts + local partToZoneDict = (onlyActiveZones and activePartToZone) or allPartToZone + + local boundParams = OverlapParams.new() + boundParams.FilterType = Enum.RaycastFilterType.Include + boundParams.MaxParts = #partsTable + boundParams.FilterDescendantsInstances = partsTable + + -- This retrieves the bounds (the rough shape) of all parts touching the item/character + -- If the corresponding zone is made up of *entirely* blocks then the bound will + -- be the actual shape of the part. + local touchingPartsDictionary = {} + local zonesDict = {} + local boundParts = CollectiveWorldModel:GetPartBoundsInBox(itemCFrame, itemSize, boundParams) + local boundPartsThatRequirePreciseChecks = {} + for _, boundPart in pairs(boundParts) do + local correspondingZone = partToZoneDict[boundPart] + if correspondingZone and correspondingZone.allZonePartsAreBlocks then + zonesDict[correspondingZone] = true + touchingPartsDictionary[boundPart] = correspondingZone + else + table.insert(boundPartsThatRequirePreciseChecks, boundPart) + end + end + + -- If the bound parts belong to a zone that isn't entirely made up of blocks, then + -- we peform additional checks using GetPartsInPart which enables shape + -- geometries to be precisely determined for non-block baseparts. + local totalRemainingBoundParts = #boundPartsThatRequirePreciseChecks + local precisePartsCount = 0 + if totalRemainingBoundParts > 0 then + local preciseParams = OverlapParams.new() + preciseParams.FilterType = Enum.RaycastFilterType.Include + preciseParams.MaxParts = totalRemainingBoundParts + preciseParams.FilterDescendantsInstances = boundPartsThatRequirePreciseChecks + + local character = item + for _, bodyPart in pairs(bodyPartsToCheck) do + local endCheck = false + if not bodyPart:IsA("BasePart") or (itemIsCharacter and Tracker.bodyPartsToIgnore[bodyPart.Name]) then + continue + end + local preciseParts = CollectiveWorldModel:GetPartsInPart(bodyPart, preciseParams) + for _, precisePart in pairs(preciseParts) do + if not touchingPartsDictionary[precisePart] then + local correspondingZone = partToZoneDict[precisePart] + if correspondingZone then + zonesDict[correspondingZone] = true + touchingPartsDictionary[precisePart] = correspondingZone + precisePartsCount += 1 + end + if precisePartsCount == totalRemainingBoundParts then + endCheck = true + break + end + end + end + if endCheck then + break + end + end + end + + local touchingZonesArray = {} + local newExitDetection + for zone, _ in pairs(zonesDict) do + if newExitDetection == nil or zone._currentExitDetection < newExitDetection then + newExitDetection = zone._currentExitDetection + end + table.insert(touchingZonesArray, zone) + end + if newExitDetection and tracker then + tracker.exitDetections[item] = newExitDetection + end + return touchingZonesArray, touchingPartsDictionary +end + +local settingsGroups = {} +function ZoneController.setGroup(settingsGroupName, properties) + local group = settingsGroups[settingsGroupName] + if not group then + group = {} + settingsGroups[settingsGroupName] = group + end + + -- PUBLIC PROPERTIES -- + group.onlyEnterOnceExitedAll = true + + -- PRIVATE PROPERTIES -- + group._name = settingsGroupName + group._memberZones = {} + + if typeof(properties) == "table" then + for k, v in pairs(properties) do + group[k] = v + end + end + return group +end + +function ZoneController.getGroup(settingsGroupName) + return settingsGroups[settingsGroupName] +end + +local workspaceContainer +local workspaceContainerName = string.format("ZonePlus%sContainer", (runService:IsClient() and "Client") or "Server") +function ZoneController.getWorkspaceContainer() + local container = workspaceContainer or workspace:FindFirstChild(workspaceContainerName) + if not container then + container = Instance.new("Folder") + container.Name = workspaceContainerName + container.Parent = workspace + workspaceContainer = container + end + return container +end + +return ZoneController +]]> + -1 + + 48a5f871ef9fd19708f5cbd900001fea + + + + + 0 + false + 00000000000000000000000000000000 + + CollectiveWorldModel + {32B28DF9-63D3-4C53-95FC-1EB1351314F9} + + -1 + + 48a5f871ef9fd19708f5cbd900001feb + + + + + + 0 + false + 00000000000000000000000000000000 + + Tracker + {896C1A93-B3F8-4465-B5C3-29DB91256B49} + 1 then + self[methodName](self, unpack(args)) + end + end) + return false + end + return true +end + +function Tracker:update() + if self:_preventMultiFrameUpdates("update") then + return + end + + self.totalVolume = 0 + self.parts = {} + self.partToItem = {} + self.items = {} + + -- This tracks the bodyparts of a character + for character, _ in pairs(self.characters) do + local charSize = Tracker.getCharacterSize(character) + if not charSize then + continue + end + local rSize = charSize + local charVolume = rSize.X * rSize.Y * rSize.Z + self.totalVolume += charVolume + + local characterJanitor = self.janitor:add(Janitor.new(), "destroy", "trackCharacterParts-" .. self.name) + local function updateTrackerOnParentChanged(instance) + characterJanitor:add( + instance.AncestryChanged:Connect(function() + if not instance:IsDescendantOf(game) then + if instance.Parent == nil and characterJanitor ~= nil then + characterJanitor:destroy() + characterJanitor = nil + self:update() + end + end + end), + "Disconnect" + ) + end + + for _, part in pairs(character:GetChildren()) do + if part:IsA("BasePart") and not Tracker.bodyPartsToIgnore[part.Name] then + self.partToItem[part] = character + table.insert(self.parts, part) + updateTrackerOnParentChanged(part) + end + end + updateTrackerOnParentChanged(character) + table.insert(self.items, character) + end + + -- This tracks any additional baseParts + for additionalPart, _ in pairs(self.baseParts) do + local rSize = additionalPart.Size + local partVolume = rSize.X * rSize.Y * rSize.Z + self.totalVolume += partVolume + self.partToItem[additionalPart] = additionalPart + table.insert(self.parts, additionalPart) + table.insert(self.items, additionalPart) + end + + -- This creates the include filter params to optimize spatial queries + self.whitelistParams = OverlapParams.new() + self.whitelistParams.FilterType = Enum.RaycastFilterType.Include + self.whitelistParams.MaxParts = #self.parts + self.whitelistParams.FilterDescendantsInstances = self.parts +end + +return Tracker +]]> + -1 + + 48a5f871ef9fd19708f5cbd900001fec + + + + + + + 0 + false + 00000000000000000000000000000000 + + ZonePlusReference + {4CA91A45-55E9-46E4-AA68-EF7FC4F1D405} + + -1 + + 48a5f871ef9fd19708f5cbd900001fed + + + + + + + + 0 + false + 00000000000000000000000000000000 + false + ServerScriptService + -1 + + 48a5f871ef9fd19708f5cbd900000378 + + + + + + 0 + false + 00000000000000000000000000000000 + ServerStorage + -1 + + 48a5f871ef9fd19708f5cbd900000379 + + + + + + 0 + false + AAAAAA== + 00000000000000000000000000000000 + ServiceVisibilityService + -1 + + 48a5f871ef9fd19708f5cbd90000037c + AAAAAA== + + + + + + 0 + false + 00000000000000000000000000000000 + false + HttpService + -1 + + 48a5f871ef9fd19708f5cbd900000398 + + + + + + true + 0 + false + 00000000000000000000000000000000 + false + DataStoreService + -1 + + 48a5f871ef9fd19708f5cbd90000039b + + + + + + 0.274509817 + 0.274509817 + 0.274509817 + + + 3 + 0 + + 0 + 0 + 0 + + + 0 + 0 + 0 + + false + 1 + 1 + 0 + 0 + + 0.752941251 + 0.752941251 + 0.752941251 + + 100000 + 0 + 0 + true + 00000000000000000000000000000000 + 1 + Lighting + + 0.274509817 + 0.274509817 + 0.274509817 + + false + true + 0.200000003 + -1 + + 3 + 14:30:00 + 48a5f871ef9fd19708f5cbd90000039c + + + + + 0 + true + false + 00000000000000000000000000000000 + 11 + rbxassetid://6444320592 + Sky + rbxassetid://6444884337 + rbxassetid://6444884785 + rbxassetid://6444884337 + rbxassetid://6444884337 + + 0 + 0 + 0 + + rbxassetid://6444884337 + rbxassetid://6412503613 + 332039975 + 3000 + 11 + rbxassetid://6196665106 + + 48a5f871ef9fd19708f5cbd9000003ae + + + + + + 0 + false + true + 00000000000000000000000000000000 + 0.00999999978 + SunRays + -1 + 0.100000001 + + 48a5f871ef9fd19708f5cbd9000003af + + + + + + 0 + + 0.78039217 + 0.78039217 + 0.78039217 + + + 0.41568628 + 0.43921569 + 0.490196079 + + false + 0.300000012 + 0 + 0 + 00000000000000000000000000000000 + Atmosphere + 0.25 + -1 + + 48a5f871ef9fd19708f5cbd9000003b0 + + + + + + 0 + false + true + 00000000000000000000000000000000 + 1 + Bloom + 24 + -1 + + 2 + 48a5f871ef9fd19708f5cbd9000003b1 + + + + + + 0 + false + false + 0.100000001 + 0.0500000007 + 00000000000000000000000000000000 + 30 + DepthOfField + 0.75 + -1 + + 48a5f871ef9fd19708f5cbd9000003b2 + + + + + + + 0 + false + 00000000000000000000000000000000 + Instance + -1 + + 48a5f871ef9fd19708f5cbd90000039d + + + + + + 0 + false + true + 00000000000000000000000000000000 + 16 + 16 + ProximityPromptService + -1 + + 48a5f871ef9fd19708f5cbd90000039e + + + + + + 0 + false + 00000000000000000000000000000000 + Teams + -1 + + 48a5f871ef9fd19708f5cbd90000039f + + + + + + true + 0 + false + + false + 00000000000000000000000000000000 + true + true + TestService + 0 + 0 + -1 + + true + 10 + 48a5f871ef9fd19708f5cbd9000003a0 + + + + + + 0 + false + 00000000000000000000000000000000 + UGCAvatarService + -1 + + 48a5f871ef9fd19708f5cbd9000003a1 + + + + + + 0 + false + 00000000000000000000000000000000 + VirtualInputManager + -1 + + 48a5f871ef9fd19708f5cbd9000003a2 + + + + + + 0 + 0 + false + true + 00000000000000000000000000000000 + VoiceChatService + -1 + + 48a5f871ef9fd19708f5cbd9000003a3 + 2 + + + + + + 0 + false + 00000000000000000000000000000000 + VideoService + -1 + + 48a5f871ef9fd19708f5cbd9000005d0 + + + + + + 0 + false + 00000000000000000000000000000000 + SerializationService + -1 + + 48a5f871ef9fd19708f5cbd900001a99 + + + + + + \ No newline at end of file From 68996df2bec7cd1cce5ca26de017f0cd2e41a95e Mon Sep 17 00:00:00 2001 From: Froredion Date: Sun, 19 Oct 2025 16:32:16 -0700 Subject: [PATCH 3/4] (untested) solution: https://github.com/1ForeverHD/ZonePlus/pull/84#issuecomment-3419374487 revert if this did not fix the issue. --- src/Zone/ZoneController/init.lua | 144 +++++++++++++++++++------------ 1 file changed, 91 insertions(+), 53 deletions(-) diff --git a/src/Zone/ZoneController/init.lua b/src/Zone/ZoneController/init.lua index 3c9182b..609a082 100644 --- a/src/Zone/ZoneController/init.lua +++ b/src/Zone/ZoneController/init.lua @@ -67,7 +67,8 @@ local heartbeatActions = { end local touchingZones = ZoneController.getTouchingZones(character, true, recommendedDetection, trackers.player) for _, zone in pairs(touchingZones) do - if zone.activeTriggers["localPlayer"] then + -- Safety check: ensure zone still has activeTriggers + if zone.activeTriggers and zone.activeTriggers["localPlayer"] then fillOccupants(zonesAndOccupants, zone, localPlayer) end end @@ -100,6 +101,10 @@ function ZoneController._deregisterZone(zone) end function ZoneController._registerConnection(registeredZone, registeredTriggerType) + -- Safety check: ensure zone still has activeTriggers property + if not registeredZone.activeTriggers then + return + end local originalItems = dictLength(registeredZone.activeTriggers) activeConnections += 1 if originalItems == 0 then @@ -109,7 +114,7 @@ function ZoneController._registerConnection(registeredZone, registeredTriggerTyp local currentTriggerCount = activeTriggers[registeredTriggerType] activeTriggers[registeredTriggerType] = (currentTriggerCount and currentTriggerCount + 1) or 1 registeredZone.activeTriggers[registeredTriggerType] = true - if registeredZone.touchedConnectionActions[registeredTriggerType] then + if registeredZone.touchedConnectionActions and registeredZone.touchedConnectionActions[registeredTriggerType] then registeredZone:_formTouchedConnection(registeredTriggerType) end if heartbeatActions[registeredTriggerType] then @@ -156,15 +161,18 @@ function ZoneController._formHeartbeat(registeredTriggerType) local lowestAccuracy local lowestDetection for zone, _ in pairs(activeZones) do - if zone.activeTriggers[registeredTriggerType] then + if zone.activeTriggers and zone.activeTriggers[registeredTriggerType] then local zAccuracy = zone.accuracy - if lowestAccuracy == nil or zAccuracy < lowestAccuracy then + if zAccuracy and (lowestAccuracy == nil or zAccuracy < lowestAccuracy) then lowestAccuracy = zAccuracy end - ZoneController.updateDetection(zone) - local zDetection = zone._currentEnterDetection - if lowestDetection == nil or zDetection < lowestDetection then - lowestDetection = zDetection + -- Safety check: ensure zone still has detection properties + if zone.enterDetection and zone.exitDetection then + ZoneController.updateDetection(zone) + local zDetection = zone._currentEnterDetection + if zDetection and (lowestDetection == nil or zDetection < lowestDetection) then + lowestDetection = zDetection + end end end end @@ -177,31 +185,37 @@ function ZoneController._formHeartbeat(registeredTriggerType) local occupantsToBlock = {} local zonesToPotentiallyIgnore = {} for zone, newOccupants in pairs(zonesAndOccupants) do - local settingsGroup = (zone.settingsGroupName and ZoneController.getGroup(zone.settingsGroupName)) - if settingsGroup and settingsGroup.onlyEnterOnceExitedAll == true then - --local currentOccupants = zone.occupants[registeredTriggerType] - --if currentOccupants then - for newOccupant, _ in pairs(newOccupants) do - --if currentOccupants[newOccupant] then - local groupDetail = occupantsToBlock[zone.settingsGroupName] - if not groupDetail then - groupDetail = {} - occupantsToBlock[zone.settingsGroupName] = groupDetail + -- Safety check: ensure zone still has settingsGroupName property + if zone.settingsGroupName then + local settingsGroup = ZoneController.getGroup(zone.settingsGroupName) + if settingsGroup and settingsGroup.onlyEnterOnceExitedAll == true then + --local currentOccupants = zone.occupants[registeredTriggerType] + --if currentOccupants then + for newOccupant, _ in pairs(newOccupants) do + --if currentOccupants[newOccupant] then + local groupDetail = occupantsToBlock[zone.settingsGroupName] + if not groupDetail then + groupDetail = {} + occupantsToBlock[zone.settingsGroupName] = groupDetail + end + groupDetail[newOccupant] = zone + --end end - groupDetail[newOccupant] = zone + zonesToPotentiallyIgnore[zone] = newOccupants --end end - zonesToPotentiallyIgnore[zone] = newOccupants - --end end end for zone, newOccupants in pairs(zonesToPotentiallyIgnore) do - local groupDetail = occupantsToBlock[zone.settingsGroupName] - if groupDetail then - for newOccupant, _ in pairs(newOccupants) do - local occupantToKeepZone = groupDetail[newOccupant] - if occupantToKeepZone and occupantToKeepZone ~= zone then - newOccupants[newOccupant] = nil + -- Safety check: ensure zone still has settingsGroupName property + if zone.settingsGroupName then + local groupDetail = occupantsToBlock[zone.settingsGroupName] + if groupDetail then + for newOccupant, _ in pairs(newOccupants) do + local occupantToKeepZone = groupDetail[newOccupant] + if occupantToKeepZone and occupantToKeepZone ~= zone then + newOccupants[newOccupant] = nil + end end end end @@ -210,7 +224,7 @@ function ZoneController._formHeartbeat(registeredTriggerType) -- This deduces what signals should be fired local collectiveSignalsToFire = { {}, {} } for zone, _ in pairs(activeZones) do - if zone.activeTriggers[registeredTriggerType] then + if zone.activeTriggers and zone.activeTriggers[registeredTriggerType] then local zAccuracy = zone.accuracy local occupantsDict = zonesAndOccupants[zone] or {} local occupantsPresent = false @@ -218,12 +232,15 @@ function ZoneController._formHeartbeat(registeredTriggerType) occupantsPresent = true break end - if occupantsPresent and zAccuracy > highestAccuracy then + if occupantsPresent and zAccuracy and zAccuracy > highestAccuracy then highestAccuracy = zAccuracy end - local signalsToFire = zone:_updateOccupants(registeredTriggerType, occupantsDict) - collectiveSignalsToFire[1][zone] = signalsToFire.exited - collectiveSignalsToFire[2][zone] = signalsToFire.entered + -- Safety check: ensure zone still has necessary methods + if zone._updateOccupants then + local signalsToFire = zone:_updateOccupants(registeredTriggerType, occupantsDict) + collectiveSignalsToFire[1][zone] = signalsToFire.exited + collectiveSignalsToFire[2][zone] = signalsToFire.entered + end end end @@ -261,12 +278,18 @@ function ZoneController._deregisterConnection(registeredZone, registeredTriggerT else activeTriggers[registeredTriggerType] -= 1 end - registeredZone.activeTriggers[registeredTriggerType] = nil - if dictLength(registeredZone.activeTriggers) == 0 then + -- Safety check: ensure zone still has activeTriggers property + if registeredZone.activeTriggers then + registeredZone.activeTriggers[registeredTriggerType] = nil + if dictLength(registeredZone.activeTriggers) == 0 then + activeZones[registeredZone] = nil + ZoneController._updateZoneDetails() + end + else + -- If activeTriggers is already nil, ensure zone is removed from activeZones activeZones[registeredZone] = nil - ZoneController._updateZoneDetails() end - if registeredZone.touchedConnectionActions[registeredTriggerType] then + if registeredZone.touchedConnectionActions and registeredZone.touchedConnectionActions[registeredTriggerType] then registeredZone:_disconnectTouchedConnection(registeredTriggerType) end end @@ -278,17 +301,20 @@ function ZoneController._updateZoneDetails() allPartToZone = {} activeZonesTotalVolume = 0 for zone, _ in pairs(registeredZones) do - local isActive = activeZones[zone] - if isActive then - activeZonesTotalVolume += zone.volume - end - for _, zonePart in pairs(zone.zoneParts) do + -- Safety check: ensure zone still has required properties + if zone.volume and zone.zoneParts then + local isActive = activeZones[zone] if isActive then - table.insert(activeParts, zonePart) - activePartToZone[zonePart] = zone + activeZonesTotalVolume += zone.volume + end + for _, zonePart in pairs(zone.zoneParts) do + if isActive then + table.insert(activeParts, zonePart) + activePartToZone[zonePart] = zone + end + table.insert(allParts, zonePart) + allPartToZone[zonePart] = zone end - table.insert(allParts, zonePart) - allPartToZone[zonePart] = zone end end end @@ -300,10 +326,13 @@ function ZoneController._getZonesAndItems( onlyActiveZones, recommendedDetection ) - local totalZoneVolume = zoneCustomVolume - if not totalZoneVolume then + local totalZoneVolume = zoneCustomVolume or 0 + if not zoneCustomVolume then for zone, _ in pairs(zonesDictToCheck) do - totalZoneVolume += zone.volume + -- Safety check: ensure zone still has volume property + if zone.volume then + totalZoneVolume += zone.volume + end end end local zonesAndOccupants = {} @@ -316,7 +345,8 @@ function ZoneController._getZonesAndItems( for _, item in pairs(tracker.items) do local touchingZones = ZoneController.getTouchingZones(item, onlyActiveZones, recommendedDetection, tracker) for _, zone in pairs(touchingZones) do - if not onlyActiveZones or zone.activeTriggers[trackerName] then + -- Safety check: ensure zone still has activeTriggers + if zone.activeTriggers and (not onlyActiveZones or zone.activeTriggers[trackerName]) then local finalItem = item if trackerName == "player" then finalItem = players:GetPlayerFromCharacter(item) @@ -333,6 +363,10 @@ function ZoneController._getZonesAndItems( -- checks directly within each zone to determine players inside for zone, _ in pairs(zonesDictToCheck) do if not onlyActiveZones or zone.activeTriggers[trackerName] then + -- Safety check: ensure zone properties still exist (zone might be destroying) + if not zone.regionCFrame or not zone.regionSize or not zone.activeTriggers then + continue + end local result = CollectiveWorldModel:GetPartBoundsInBox(zone.regionCFrame, zone.regionSize, tracker.whitelistParams) local finalItemsDict = {} @@ -343,12 +377,13 @@ function ZoneController._getZonesAndItems( end end for item, _ in pairs(finalItemsDict) do - if trackerName == "player" then + -- Safety check: ensure zone methods still exist (zone might be destroying) + if trackerName == "player" and zone.findPlayer then local player = players:GetPlayerFromCharacter(item) - if zone:findPlayer(player) then + if player and zone:findPlayer(player) then fillOccupants(zonesAndOccupants, zone, player) end - elseif zone:findItem(item) then + elseif zone.findItem and zone:findItem(item) then fillOccupants(zonesAndOccupants, zone, item) end end @@ -485,7 +520,10 @@ function ZoneController.getTouchingZones(item, onlyActiveZones, recommendedDetec local touchingZonesArray = {} local newExitDetection for zone, _ in pairs(zonesDict) do - if newExitDetection == nil or zone._currentExitDetection < newExitDetection then + -- Safety check: ensure zone still has _currentExitDetection property + if + zone._currentExitDetection and (newExitDetection == nil or zone._currentExitDetection < newExitDetection) + then newExitDetection = zone._currentExitDetection end table.insert(touchingZonesArray, zone) From 62a5ea07fd3e843b79701350c86d5be1fa062825 Mon Sep 17 00:00:00 2001 From: Froredion Date: Wed, 19 Nov 2025 20:16:09 -0800 Subject: [PATCH 4/4] fully optimized and tested: (see desc) - added a tester for zone destroying - improved code maintainability - added selene.toml for people who wants to contribute and has selene installed --- selene.toml | 1 + src/Testers/PlaceTester.rbxlx | 3123 +++++++++++++++++---------- src/Testers/ZonePlusTest.client.lua | 75 + src/Zone/ZoneController/init.lua | 64 +- 4 files changed, 2052 insertions(+), 1211 deletions(-) create mode 100644 selene.toml diff --git a/selene.toml b/selene.toml new file mode 100644 index 0000000..c4ddb46 --- /dev/null +++ b/selene.toml @@ -0,0 +1 @@ +std = "roblox" diff --git a/src/Testers/PlaceTester.rbxlx b/src/Testers/PlaceTester.rbxlx index 74aa9ef..ddb4816 100644 --- a/src/Testers/PlaceTester.rbxlx +++ b/src/Testers/PlaceTester.rbxlx @@ -6,14 +6,11 @@ 0.00120000006 0 false - 1 0 - 0 0 AQEABP////8HRGVmYXVsdA== RBX89ADB630653C43609C6DBCC5098C2571 - false 0 true true @@ -25,11 +22,32 @@ 0 196.199997 - 00000000000000000000000000000000 0 - 0 0 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 0 + 2 + 2 + true + 3 + 64 + 1024 + true + 0 + false + 0 + 0 0 0 @@ -50,37 +68,11 @@ 0 0 - 0 0 - 0 - Workspace false - 0 - 0 - 0 - 0 - 0 null - 0 - 0 - 0 - 0 - 0 1 - 2 - -1 - 2 - true - 3 - 64 - 1024 - - true - 0 - false - 48a5f871ef9fd19708f5cbd900000002 - 0 - 0 + yuZpQdnvvUBOTYh1jqZ2cA== 0 @@ -97,34 +89,39 @@ 1 + + 0 + false + 00000000000000000000000000000000 + Workspace + -1 + + 48a5f871ef9fd19708f5cbd900000002 - - -25.376812 - 24.1568317 - -122.04718 - 0.203131557 - -0.215827301 - 0.955068707 + 23.8128319 + 57.9881363 + 4.27575684 + 0.992719769 + 0.0835686103 + -0.0867404938 -0 - 0.975404561 - 0.220422775 - -0.979151547 - -0.0447748229 - 0.198135406 + 0.720151365 + 0.693817139 + 0.120447606 + -0.688766003 + 0.714908361 null 0 - 0 - false 70 0 - -27.2869492 - 23.7159863 - -122.443451 + 23.9863129 + 56.600502 + 2.84594011 1 0 0 @@ -137,18 +134,22 @@ true 1 + false + + 0 + false 00000000000000000000000000000000 Camera -1 48a5f871ef9fd19708f5cbd9000003a4 - false + 1 + 0 true - true -0.5 0.5 @@ -175,7 +176,6 @@ true true true - 0 true Default 0 @@ -183,13 +183,11 @@ false - false true -0.5 0.5 0 0 - 00000000000000000000000000000000 -0.5 0.5 0 @@ -198,7 +196,6 @@ false 256 - Baseplate 0 0 @@ -224,49 +221,44 @@ 0 0 - -1 - -0.5 0.5 0 0 0 - 48a5f871ef9fd19708f5cbd9000003a5 0 0 0 - 0 - 1 2048 16 2048 + + 0 + false + 00000000000000000000000000000000 + Baseplate + -1 + + 48a5f871ef9fd19708f5cbd9000003a5 - - 0 + 0 + 0 + 8 + 8 0 0 0 - false - 1 - 00000000000000000000000000000000 - Texture - 0 - 0 - -1 - 8 - 8 - rbxassetid://6372755229 @@ -279,16 +271,39 @@ 1 1 - 48a5f871ef9fd19708f5cbd9000003a6 1 + 1 + + 0 + false + 00000000000000000000000000000000 + Texture + -1 + + 48a5f871ef9fd19708f5cbd9000003a6 0 + true + 0.699999988 + + AgMAAAAAAAAAAAAAAAA= + AQU= + false + + 0.0470588282 + 0.329411775 + 0.360784322 + + 1 + 0.300000012 + 0.150000006 + 10 true - true -0.5 0.5 @@ -315,7 +330,6 @@ true true true - 0 true Default 0 @@ -323,15 +337,11 @@ false - true - false true -0.5 0.5 0 0 - 0.699999988 - 00000000000000000000000000000000 -0.5 0.5 0 @@ -339,11 +349,7 @@ true false 256 - - Terrain - AgMAAAAAAAAAAAAAAAA= 0 0 @@ -369,42 +375,41 @@ zNLfaoZA///+//PAj5CH]]> 0 0 - AQU= - false - -1 - -0.5 0.5 3 0 0 - 48a5f871ef9fd19708f5cbd9000003a7 0 0 0 - - 0.0470588282 - 0.329411775 - 0.360784322 - - 1 - 0.300000012 - 0.150000006 - 10 2044 252 2044 + + 0 + false + 00000000000000000000000000000000 + Terrain + -1 + + 48a5f871ef9fd19708f5cbd9000003a7 false + 0 + true + true + 194 + 1 + 1 true - true -0.5 0.5 @@ -431,7 +436,6 @@ zNLfaoZA///+//PAj5CH]]> true true true - 0 true Default 0 @@ -439,15 +443,11 @@ zNLfaoZA///+//PAj5CH]]> false - false - 0 true - true -0.5 0.5 0 0 - 00000000000000000000000000000000 -0.5 0.5 0 @@ -456,8 +456,6 @@ zNLfaoZA///+//PAj5CH]]> false 256 - SpawnLocation - true 0 0 @@ -483,46 +481,40 @@ zNLfaoZA///+//PAj5CH]]> 0 0 - -1 - - 194 -0.5 0.5 0 0 0 - 48a5f871ef9fd19708f5cbd9000003a8 0 0 0 - 1 - 1 12 1 12 + + 0 + false + 00000000000000000000000000000000 + SpawnLocation + -1 + + 48a5f871ef9fd19708f5cbd9000003a8 - - 0 1 1 1 - false - 1 - 00000000000000000000000000000000 - Decal - -1 - rbxasset://textures/SpawnLocation.png @@ -535,8 +527,16 @@ zNLfaoZA///+//PAj5CH]]> 1 1 - 48a5f871ef9fd19708f5cbd9000003a9 1 + 1 + + 0 + false + 00000000000000000000000000000000 + Decal + -1 + + 48a5f871ef9fd19708f5cbd9000003a9 @@ -553,10 +553,6 @@ zNLfaoZA///+//PAj5CH]]> - - 0 - false - 00000000000000000000000000000000 0 0 @@ -579,13 +575,10 @@ zNLfaoZA///+//PAj5CH]]> 0 0 - CylinderGroupedZone false null 1 - -1 - - 48a5f871ef9fd19708f5cbd900002061 + yuZpQdnvvUBOTYh1jqZ2cA== -58.1676903 @@ -602,11 +595,20 @@ zNLfaoZA///+//PAj5CH]]> 0 + + 0 + false + 00000000000000000000000000000000 + CylinderGroupedZone + -1 + + 48a5f871ef9fd19708f5cbd900002061 + 2 + 1 true - true -0.5 0.5 @@ -633,7 +635,6 @@ zNLfaoZA///+//PAj5CH]]> false true true - 0 true Default 0 @@ -641,13 +642,11 @@ zNLfaoZA///+//PAj5CH]]> false - false true -0.5 0.5 0 0 - 00000000000000000000000000000000 -0.5 0.5 0 @@ -656,7 +655,6 @@ zNLfaoZA///+//PAj5CH]]> false 288 - Wall1 0 0 @@ -682,32 +680,36 @@ zNLfaoZA///+//PAj5CH]]> 0 0 - -1 - -0.5 0.5 3 0 0.5 - 48a5f871ef9fd19708f5cbd900002067 0 0 0 - 1 - 2 13.8993607 18.5324821 18.5324821 + + 0 + false + 00000000000000000000000000000000 + Wall1 + -1 + + 48a5f871ef9fd19708f5cbd900002067 + 1 + 1 true - true -0.5 0.5 @@ -734,7 +736,6 @@ zNLfaoZA///+//PAj5CH]]> false true true - 0 true Default 0 @@ -742,13 +743,11 @@ zNLfaoZA///+//PAj5CH]]> false - false true -0.5 0.5 0 0 - 00000000000000000000000000000000 -0.5 0.5 0 @@ -757,7 +756,6 @@ zNLfaoZA///+//PAj5CH]]> false 256 - Label 0 0 @@ -783,73 +781,107 @@ zNLfaoZA///+//PAj5CH]]> 0 0 - -1 - -0.5 0.5 3 0 0 - 48a5f871ef9fd19708f5cbd900002068 0 0 0 - 1 - 1 47.1774368 22.7054386 2.35887194 + + 0 + false + 00000000000000000000000000000000 + Label + -1 + + 48a5f871ef9fd19708f5cbd900002068 - true - null false - - true 1 800 600 - 0 false - false - true - 5 - 00000000000000000000000000000000 0 0 - SurfaceGui 50 + 0 + 0 + 0 + true + null + 5 + true true + 0 + true null 0 0 0 0 false - 0 + + 0 + false + 00000000000000000000000000000000 + SurfaceGui -1 - 0 48a5f871ef9fd19708f5cbd900002069 - 0 - 0 + + rbxasset://fonts/families/LegacyArial.json + 400 + + rbxasset://fonts/Arimo-Regular.ttf + + 1 + + + -1 + + false + + + 1 + 1 + 1 + + 0 + true + 8 + + 0 + 0 + 0 + + 1 + 0 + 0 + true + 2 + 1 false 0 0 - - true 0 0.639215708 @@ -864,44 +896,22 @@ zNLfaoZA///+//PAj5CH]]> 0 1 - 0 false - false false - - rbxasset://fonts/families/LegacyArial.json - 400 - - rbxasset://fonts/Arimo-Regular.ttf - - 00000000000000000000000000000000 true 0 - 1 - - - -1 - TextLabel null null null null - 0 0 0 0 - false - null 0 false - 0 - 0 - 0 - 0 - false null 0 @@ -911,60 +921,51 @@ zNLfaoZA///+//PAj5CH]]> 0 0 + true + 1 + true + null + 0 + 0 + 0 + 0 + false + + 0 + false + 00000000000000000000000000000000 + TextLabel -1 - - - 1 - 1 - 1 - - 0 - true - 8 - - 0 - 0 - 0 - - 1 - 0 - 0 - true - 2 - 1 48a5f871ef9fd19708f5cbd90000206a - true - 1 0 - 0 0 0 - 0 0 0 0 - false true - 00000000000000000000000000000000 0 - UIStroke - -1 0 - 25 0 - 48a5f871ef9fd19708f5cbd900002a9a 1 + + 0 + false + 00000000000000000000000000000000 + UIStroke + -1 + + 48a5f871ef9fd19708f5cbd900002a9a @@ -972,8 +973,9 @@ zNLfaoZA///+//PAj5CH]]> + 2 + 1 true - true -0.5 0.5 @@ -1000,7 +1002,6 @@ zNLfaoZA///+//PAj5CH]]> false true true - 0 true Default 0 @@ -1008,13 +1009,11 @@ zNLfaoZA///+//PAj5CH]]> false - false true -0.5 0.5 0 0 - 48a5f871ef9fd19708f5cbd900002067 -0.5 0.5 0 @@ -1023,7 +1022,6 @@ zNLfaoZA///+//PAj5CH]]> false 288 - Wall2 0 0 @@ -1049,32 +1047,36 @@ zNLfaoZA///+//PAj5CH]]> 0 0 - -1 - -0.5 0.5 3 0 0.5 - 48a5f871ef9fd19708f5cbd900002fe6 0 0 0 - 1 - 2 13.8993607 18.5324821 18.5324821 + + 0 + false + 48a5f871ef9fd19708f5cbd900002067 + Wall2 + -1 + + 48a5f871ef9fd19708f5cbd900002fe6 + 2 + 1 true - true -0.5 0.5 @@ -1101,7 +1103,6 @@ zNLfaoZA///+//PAj5CH]]> false true true - 0 true Default 0 @@ -1109,13 +1110,11 @@ zNLfaoZA///+//PAj5CH]]> false - false true -0.5 0.5 0 0 - 48a5f871ef9fd19708f5cbd900002067 -0.5 0.5 0 @@ -1124,7 +1123,6 @@ zNLfaoZA///+//PAj5CH]]> false 288 - Wall3 0 0 @@ -1150,35 +1148,34 @@ zNLfaoZA///+//PAj5CH]]> 0 0 - -1 - -0.5 0.5 3 0 0.5 - 48a5f871ef9fd19708f5cbd90000303b 0 0 0 - 1 - 2 21.2595978 23.065073 23.065073 + + 0 + false + 48a5f871ef9fd19708f5cbd900002067 + Wall3 + -1 + + 48a5f871ef9fd19708f5cbd90000303b - - 0 - false - 00000000000000000000000000000000 0 0 @@ -1201,13 +1198,10 @@ zNLfaoZA///+//PAj5CH]]> 0 0 - SphereZone false null 1 - -1 - - 48a5f871ef9fd19708f5cbd90000206b + yuZpQdnvvUBOTYh1jqZ2cA== 44 @@ -1224,11 +1218,20 @@ zNLfaoZA///+//PAj5CH]]> 1 + + 0 + false + 00000000000000000000000000000000 + SphereZone + -1 + + 48a5f871ef9fd19708f5cbd90000206b + 0 + 1 true - true -0.5 0.5 @@ -1255,7 +1258,6 @@ zNLfaoZA///+//PAj5CH]]> false true true - 0 true Default 0 @@ -1263,13 +1265,11 @@ zNLfaoZA///+//PAj5CH]]> false - false true -0.5 0.5 0 0 - 00000000000000000000000000000000 -0.5 0.5 0 @@ -1278,7 +1278,6 @@ zNLfaoZA///+//PAj5CH]]> false 288 - SpherePart 0 0 @@ -1304,32 +1303,36 @@ zNLfaoZA///+//PAj5CH]]> 0 0 - -1 - -0.5 0.5 3 0 0.5 - 48a5f871ef9fd19708f5cbd90000206c 0 0 0 - 1 - 0 25 25 25 + + 0 + false + 00000000000000000000000000000000 + SpherePart + -1 + + 48a5f871ef9fd19708f5cbd90000206c + 1 + 1 true - true -0.5 0.5 @@ -1356,7 +1359,6 @@ zNLfaoZA///+//PAj5CH]]> false true true - 0 true Default 0 @@ -1364,13 +1366,11 @@ zNLfaoZA///+//PAj5CH]]> false - false true -0.5 0.5 0 0 - 00000000000000000000000000000000 -0.5 0.5 0 @@ -1379,7 +1379,6 @@ zNLfaoZA///+//PAj5CH]]> false 256 - Label 0 0 @@ -1405,73 +1404,107 @@ zNLfaoZA///+//PAj5CH]]> 0 0 - -1 - -0.5 0.5 3 0 0 - 48a5f871ef9fd19708f5cbd90000206d 0 0 0 - 1 - 1 37.1721001 19.0668831 1.85860503 + + 0 + false + 00000000000000000000000000000000 + Label + -1 + + 48a5f871ef9fd19708f5cbd90000206d - true - null false - - true 1 800 600 - 0 false - false - true - 5 - 00000000000000000000000000000000 0 0 - SurfaceGui 50 - true - null + 0 + 0 + 0 + true + null + 5 + true + true + 0 + true + null 0 0 0 0 false - 0 + + 0 + false + 00000000000000000000000000000000 + SurfaceGui -1 - 0 48a5f871ef9fd19708f5cbd90000206e - 0 - 0 + + rbxasset://fonts/families/LegacyArial.json + 400 + + rbxasset://fonts/Arimo-Regular.ttf + + 1 + + + -1 + + false + + + 1 + 1 + 1 + + 0 + true + 8 + + 0 + 0 + 0 + + 1 + 0 + 0 + true + 2 + 1 false 0 0 - - true 0 0.639215708 @@ -1486,44 +1519,22 @@ zNLfaoZA///+//PAj5CH]]> 0 1 - 0 false - false false - - rbxasset://fonts/families/LegacyArial.json - 400 - - rbxasset://fonts/Arimo-Regular.ttf - - 00000000000000000000000000000000 true 0 - 1 - - - -1 - TextLabel null null null null - 0 0 0 0 - false - null 0 false - 0 - 0 - 0 - 0 - false null 0 @@ -1533,60 +1544,51 @@ zNLfaoZA///+//PAj5CH]]> 0 0 + true + 1 + true + null + 0 + 0 + 0 + 0 + false + + 0 + false + 00000000000000000000000000000000 + TextLabel -1 - - - 1 - 1 - 1 - - 0 - true - 8 - - 0 - 0 - 0 - - 1 - 0 - 0 - true - 2 - 1 48a5f871ef9fd19708f5cbd90000206f - true - 1 0 - 0 0 0 - 0 0 0 0 - false true - 00000000000000000000000000000000 0 - UIStroke - -1 0 - 25 0 - 48a5f871ef9fd19708f5cbd900002ac4 1 + + 0 + false + 00000000000000000000000000000000 + UIStroke + -1 + + 48a5f871ef9fd19708f5cbd900002ac4 @@ -1595,10 +1597,6 @@ zNLfaoZA///+//PAj5CH]]> - - 0 - false - 00000000000000000000000000000000 0 0 @@ -1621,13 +1619,10 @@ zNLfaoZA///+//PAj5CH]]> 0 0 - ComplexZone false null 1 - -1 - - 48a5f871ef9fd19708f5cbd900002070 + yuZpQdnvvUBOTYh1jqZ2cA== -59 @@ -1644,11 +1639,20 @@ zNLfaoZA///+//PAj5CH]]> 1 + + 0 + false + 00000000000000000000000000000000 + ComplexZone + -1 + + 48a5f871ef9fd19708f5cbd900002070 + 1 + 1 true - true -0.5 0.5 @@ -1675,7 +1679,6 @@ zNLfaoZA///+//PAj5CH]]> false true true - 0 true Default 0 @@ -1683,13 +1686,11 @@ zNLfaoZA///+//PAj5CH]]> false - false true -0.5 0.5 0 0 - 00000000000000000000000000000000 -0.5 0.5 0 @@ -1698,7 +1699,6 @@ zNLfaoZA///+//PAj5CH]]> false 288 - Base1 0 0 @@ -1724,32 +1724,36 @@ zNLfaoZA///+//PAj5CH]]> 0 0 - -1 - -0.5 0.5 3 0 0.5 - 48a5f871ef9fd19708f5cbd900002071 0 0 0 - 1 - 1 30 10 15 + + 0 + false + 00000000000000000000000000000000 + Base1 + -1 + + 48a5f871ef9fd19708f5cbd900002071 + 1 + 1 true - true -0.5 0.5 @@ -1776,7 +1780,6 @@ zNLfaoZA///+//PAj5CH]]> false true true - 0 true Default 0 @@ -1784,13 +1787,11 @@ zNLfaoZA///+//PAj5CH]]> false - false true -0.5 0.5 0 0 - 00000000000000000000000000000000 -0.5 0.5 0 @@ -1799,7 +1800,6 @@ zNLfaoZA///+//PAj5CH]]> false 288 - Base2 0 0 @@ -1825,32 +1825,35 @@ zNLfaoZA///+//PAj5CH]]> 0 0 - -1 - -0.5 0.5 3 0 0.5 - 48a5f871ef9fd19708f5cbd900002072 0 0 0 - 1 - 1 15 10 15 + + 0 + false + 00000000000000000000000000000000 + Base2 + -1 + + 48a5f871ef9fd19708f5cbd900002072 + 1 true - true -0.5 0.5 @@ -1877,7 +1880,6 @@ zNLfaoZA///+//PAj5CH]]> false true true - 0 true Default 0 @@ -1885,13 +1887,11 @@ zNLfaoZA///+//PAj5CH]]> false - false true -0.5 0.5 0 0 - 00000000000000000000000000000000 -0.5 0.5 0 @@ -1900,7 +1900,6 @@ zNLfaoZA///+//PAj5CH]]> false 288 - Wedge 0 0 @@ -1926,31 +1925,36 @@ zNLfaoZA///+//PAj5CH]]> 0 0 - -1 - -0.5 0.5 0 0 0.5 - 48a5f871ef9fd19708f5cbd900002073 0 0 0 - 1 15 10 15 + + 0 + false + 00000000000000000000000000000000 + Wedge + -1 + + 48a5f871ef9fd19708f5cbd900002073 + 1 + 1 true - true -0.5 0.5 @@ -1977,7 +1981,6 @@ zNLfaoZA///+//PAj5CH]]> false true true - 0 true Default 0 @@ -1985,13 +1988,11 @@ zNLfaoZA///+//PAj5CH]]> false - false true -0.5 0.5 0 0 - 00000000000000000000000000000000 -0.5 0.5 0 @@ -2000,7 +2001,6 @@ zNLfaoZA///+//PAj5CH]]> false 256 - Label 0 0 @@ -2026,73 +2026,107 @@ zNLfaoZA///+//PAj5CH]]> 0 0 - -1 - -0.5 0.5 3 0 0 - 48a5f871ef9fd19708f5cbd900002074 0 0 0 - 1 - 1 38.1200256 18.3389969 1.90600133 + + 0 + false + 00000000000000000000000000000000 + Label + -1 + + 48a5f871ef9fd19708f5cbd900002074 - true - null false - - true 1 800 600 - 0 false - false - true - 5 - 00000000000000000000000000000000 0 0 - SurfaceGui 50 + 0 + 0 + 0 + true + null + 5 + true true + 0 + true null 0 0 0 0 false - 0 + + 0 + false + 00000000000000000000000000000000 + SurfaceGui -1 - 0 48a5f871ef9fd19708f5cbd900002075 - 0 - 0 - false - - 0 - 0 + + rbxasset://fonts/families/LegacyArial.json + 400 + + rbxasset://fonts/Arimo-Regular.ttf + + 1 + + + -1 + + false + + + 1 + 1 + 1 + + 0 + true + 8 + + 0 + 0 + 0 + + 1 + 0 + 0 + true + 2 + 1 + false + + 0 + 0 - - true 0 0.639215708 @@ -2107,44 +2141,22 @@ zNLfaoZA///+//PAj5CH]]> 0 1 - 0 false - false false - - rbxasset://fonts/families/LegacyArial.json - 400 - - rbxasset://fonts/Arimo-Regular.ttf - - 00000000000000000000000000000000 true 0 - 1 - - - -1 - TextLabel null null null null - 0 0 0 0 - false - null 0 false - 0 - 0 - 0 - 0 - false null 0 @@ -2154,60 +2166,51 @@ zNLfaoZA///+//PAj5CH]]> 0 0 + true + 1 + true + null + 0 + 0 + 0 + 0 + false + + 0 + false + 00000000000000000000000000000000 + TextLabel -1 - - - 1 - 1 - 1 - - 0 - true - 8 - - 0 - 0 - 0 - - 1 - 0 - 0 - true - 2 - 1 48a5f871ef9fd19708f5cbd900002076 - true - 1 0 - 0 0 0 - 0 0 0 0 - false true - 00000000000000000000000000000000 0 - UIStroke - -1 0 - 25 0 - 48a5f871ef9fd19708f5cbd9000028a0 1 + + 0 + false + 00000000000000000000000000000000 + UIStroke + -1 + + 48a5f871ef9fd19708f5cbd9000028a0 @@ -2215,8 +2218,8 @@ zNLfaoZA///+//PAj5CH]]> + 1 true - true -0.5 0.5 @@ -2243,7 +2246,6 @@ zNLfaoZA///+//PAj5CH]]> false true true - 0 true Default 0 @@ -2251,13 +2253,11 @@ zNLfaoZA///+//PAj5CH]]> false - false true -0.5 0.5 0 0 - 48a5f871ef9fd19708f5cbd900002073 -0.5 0.5 0 @@ -2266,7 +2266,6 @@ zNLfaoZA///+//PAj5CH]]> false 288 - Wedge 0 0 @@ -2292,32 +2291,37 @@ zNLfaoZA///+//PAj5CH]]> 0 0 - -1 - -0.5 0.5 0 0 0.5 - 48a5f871ef9fd19708f5cbd90000388e 0 0 0 - 1 15 10 15 + + 0 + false + 48a5f871ef9fd19708f5cbd900002073 + Wedge + -1 + + 48a5f871ef9fd19708f5cbd90000388e + 1 + 1 true - true -0.5 0.5 @@ -2344,7 +2348,6 @@ zNLfaoZA///+//PAj5CH]]> true true true - 0 true Default 0 @@ -2352,13 +2355,11 @@ zNLfaoZA///+//PAj5CH]]> false - false true -0.5 0.5 0 0 - 00000000000000000000000000000000 -0.5 0.5 0 @@ -2367,7 +2368,6 @@ zNLfaoZA///+//PAj5CH]]> false 816 - SpawnPlatform 0 0 @@ -2393,33 +2393,41 @@ zNLfaoZA///+//PAj5CH]]> 0 0 - -1 - -0.5 0.5 3 0 0 - 48a5f871ef9fd19708f5cbd900002077 0 0 0 - 1 - 1 120 1 60 + + 0 + false + 00000000000000000000000000000000 + SpawnPlatform + -1 + + 48a5f871ef9fd19708f5cbd900002077 false + 0 + true + true + 194 + 1 + 1 true - true -0.5 0.5 @@ -2446,7 +2454,6 @@ zNLfaoZA///+//PAj5CH]]> true true true - 0 true Default 0 @@ -2454,15 +2461,11 @@ zNLfaoZA///+//PAj5CH]]> false - false - 0 true - true -0.5 0.5 0 0 - 00000000000000000000000000000000 -0.5 0.5 0 @@ -2471,8 +2474,6 @@ zNLfaoZA///+//PAj5CH]]> false 256 - SpawnLocation - true 0 0 @@ -2498,35 +2499,33 @@ zNLfaoZA///+//PAj5CH]]> 0 0 - -1 - - 194 -0.5 0.5 3 0 0 - 48a5f871ef9fd19708f5cbd90000207e 0 0 0 - 1 - 1 10 1 10 + + 0 + false + 00000000000000000000000000000000 + SpawnLocation + -1 + + 48a5f871ef9fd19708f5cbd90000207e - - 0 - false - 48a5f871ef9fd19708f5cbd900002061 0 0 @@ -2549,13 +2548,10 @@ zNLfaoZA///+//PAj5CH]]> 0 0 - BoxZone false null 1 - -1 - - 48a5f871ef9fd19708f5cbd900002b4c + yuZpQdnvvUBOTYh1jqZ2cA== -1 @@ -2572,11 +2568,20 @@ zNLfaoZA///+//PAj5CH]]> 1 + + 0 + false + 48a5f871ef9fd19708f5cbd900002061 + BoxZone + -1 + + 48a5f871ef9fd19708f5cbd900002b4c + 1 + 1 true - true -0.5 0.5 @@ -2603,7 +2608,6 @@ zNLfaoZA///+//PAj5CH]]> false true true - 0 true Default 0 @@ -2611,13 +2615,11 @@ zNLfaoZA///+//PAj5CH]]> false - false true -0.5 0.5 0 0 - 48a5f871ef9fd19708f5cbd900002062 -0.5 0.5 0 @@ -2626,7 +2628,6 @@ zNLfaoZA///+//PAj5CH]]> false 288 - Bottom 0 0 @@ -2652,32 +2653,36 @@ zNLfaoZA///+//PAj5CH]]> 0 0 - -1 - -0.5 0.5 3 0 0.5 - 48a5f871ef9fd19708f5cbd900002b4d 0 0 0 - 1 - 1 30 1 30 + + 0 + false + 48a5f871ef9fd19708f5cbd900002062 + Bottom + -1 + + 48a5f871ef9fd19708f5cbd900002b4d + 1 + 1 true - true -0.5 0.5 @@ -2704,7 +2709,6 @@ zNLfaoZA///+//PAj5CH]]> false true true - 0 true Default 0 @@ -2712,13 +2716,11 @@ zNLfaoZA///+//PAj5CH]]> false - false true -0.5 0.5 0 0 - 48a5f871ef9fd19708f5cbd900002063 -0.5 0.5 0 @@ -2727,7 +2729,6 @@ zNLfaoZA///+//PAj5CH]]> false 288 - Top 0 0 @@ -2753,32 +2754,36 @@ zNLfaoZA///+//PAj5CH]]> 0 0 - -1 - -0.5 0.5 3 0 0.5 - 48a5f871ef9fd19708f5cbd900002b4e 0 0 0 - 1 - 1 30 1 30 + + 0 + false + 48a5f871ef9fd19708f5cbd900002063 + Top + -1 + + 48a5f871ef9fd19708f5cbd900002b4e + 1 + 1 true - true -0.5 0.5 @@ -2805,7 +2810,6 @@ zNLfaoZA///+//PAj5CH]]> false true true - 0 true Default 0 @@ -2813,13 +2817,11 @@ zNLfaoZA///+//PAj5CH]]> false - false true -0.5 0.5 0 0 - 48a5f871ef9fd19708f5cbd900002064 -0.5 0.5 0 @@ -2828,7 +2830,6 @@ zNLfaoZA///+//PAj5CH]]> false 288 - Wall1 0 0 @@ -2854,32 +2855,36 @@ zNLfaoZA///+//PAj5CH]]> 0 0 - -1 - -0.5 0.5 3 0 0.5 - 48a5f871ef9fd19708f5cbd900002b4f 0 0 0 - 1 - 1 1 20 30 + + 0 + false + 48a5f871ef9fd19708f5cbd900002064 + Wall1 + -1 + + 48a5f871ef9fd19708f5cbd900002b4f + 1 + 1 true - true -0.5 0.5 @@ -2906,7 +2911,6 @@ zNLfaoZA///+//PAj5CH]]> false true true - 0 true Default 0 @@ -2914,13 +2918,11 @@ zNLfaoZA///+//PAj5CH]]> false - false true -0.5 0.5 0 0 - 48a5f871ef9fd19708f5cbd900002065 -0.5 0.5 0 @@ -2929,7 +2931,6 @@ zNLfaoZA///+//PAj5CH]]> false 288 - Wall2 0 0 @@ -2955,32 +2956,36 @@ zNLfaoZA///+//PAj5CH]]> 0 0 - -1 - -0.5 0.5 3 0 0.5 - 48a5f871ef9fd19708f5cbd900002b50 0 0 0 - 1 - 1 1 20 30 + + 0 + false + 48a5f871ef9fd19708f5cbd900002065 + Wall2 + -1 + + 48a5f871ef9fd19708f5cbd900002b50 + 1 + 1 true - true -0.5 0.5 @@ -3007,7 +3012,6 @@ zNLfaoZA///+//PAj5CH]]> false true true - 0 true Default 0 @@ -3015,13 +3019,11 @@ zNLfaoZA///+//PAj5CH]]> false - false true -0.5 0.5 0 0 - 48a5f871ef9fd19708f5cbd900002066 -0.5 0.5 0 @@ -3030,7 +3032,6 @@ zNLfaoZA///+//PAj5CH]]> false 288 - Wall3 0 0 @@ -3056,32 +3057,36 @@ zNLfaoZA///+//PAj5CH]]> 0 0 - -1 - -0.5 0.5 3 0 0.5 - 48a5f871ef9fd19708f5cbd900002b51 0 0 0 - 1 - 1 30 20 1 + + 0 + false + 48a5f871ef9fd19708f5cbd900002066 + Wall3 + -1 + + 48a5f871ef9fd19708f5cbd900002b51 + 1 + 1 true - true -0.5 0.5 @@ -3108,7 +3113,6 @@ zNLfaoZA///+//PAj5CH]]> false true true - 0 true Default 0 @@ -3116,13 +3120,11 @@ zNLfaoZA///+//PAj5CH]]> false - false true -0.5 0.5 0 0 - 48a5f871ef9fd19708f5cbd900002067 -0.5 0.5 0 @@ -3131,7 +3133,6 @@ zNLfaoZA///+//PAj5CH]]> false 288 - Wall4 0 0 @@ -3157,32 +3158,36 @@ zNLfaoZA///+//PAj5CH]]> 0 0 - -1 - -0.5 0.5 3 0 0.5 - 48a5f871ef9fd19708f5cbd900002b52 0 0 0 - 1 - 1 30 20 1 + + 0 + false + 48a5f871ef9fd19708f5cbd900002067 + Wall4 + -1 + + 48a5f871ef9fd19708f5cbd900002b52 + 1 + 1 true - true -0.5 0.5 @@ -3209,7 +3214,6 @@ zNLfaoZA///+//PAj5CH]]> false true true - 0 true Default 0 @@ -3217,13 +3221,11 @@ zNLfaoZA///+//PAj5CH]]> false - false true -0.5 0.5 0 0 - 48a5f871ef9fd19708f5cbd900002068 -0.5 0.5 0 @@ -3232,7 +3234,6 @@ zNLfaoZA///+//PAj5CH]]> false 256 - Label 0 0 @@ -3258,73 +3259,107 @@ zNLfaoZA///+//PAj5CH]]> 0 0 - -1 - -0.5 0.5 3 0 0 - 48a5f871ef9fd19708f5cbd900002b53 0 0 0 - 1 - 1 47.1774368 22.7054386 2.35887194 + + 0 + false + 48a5f871ef9fd19708f5cbd900002068 + Label + -1 + + 48a5f871ef9fd19708f5cbd900002b53 - true - null false - - true 1 800 600 - 0 false - false - true - 5 - 48a5f871ef9fd19708f5cbd900002069 0 0 - SurfaceGui 50 + 0 + 0 + 0 + true + null + 5 + true true + 0 + true null 0 0 0 0 false - 0 + + 0 + false + 48a5f871ef9fd19708f5cbd900002069 + SurfaceGui -1 - 0 48a5f871ef9fd19708f5cbd900002b54 - 0 - 0 + + rbxasset://fonts/families/LegacyArial.json + 400 + + rbxasset://fonts/Arimo-Regular.ttf + + 1 + + + -1 + + false + + + 1 + 1 + 1 + + 0 + true + 8 + + 0 + 0 + 0 + + 1 + 0 + 0 + true + 2 + 1 false 0 0 - - true 0 0.639215708 @@ -3339,44 +3374,22 @@ zNLfaoZA///+//PAj5CH]]> 0 1 - 0 false - false false - - rbxasset://fonts/families/LegacyArial.json - 400 - - rbxasset://fonts/Arimo-Regular.ttf - - 48a5f871ef9fd19708f5cbd90000206a true 0 - 1 - - - -1 - TextLabel null null null null - 0 0 0 0 - false - null 0 false - 0 - 0 - 0 - 0 - false null 0 @@ -3386,60 +3399,51 @@ zNLfaoZA///+//PAj5CH]]> 0 0 + true + 1 + true + null + 0 + 0 + 0 + 0 + false + + 0 + false + 48a5f871ef9fd19708f5cbd90000206a + TextLabel -1 - - - 1 - 1 - 1 - - 0 - true - 8 - - 0 - 0 - 0 - - 1 - 0 - 0 - true - 2 - 1 48a5f871ef9fd19708f5cbd900002b55 - true - 1 0 - 0 0 0 - 0 0 0 0 - false true - 48a5f871ef9fd19708f5cbd900002a9a 0 - UIStroke - -1 0 - 25 0 - 48a5f871ef9fd19708f5cbd900002b56 1 + + 0 + false + 48a5f871ef9fd19708f5cbd900002a9a + UIStroke + -1 + + 48a5f871ef9fd19708f5cbd900002b56 @@ -3448,10 +3452,6 @@ zNLfaoZA///+//PAj5CH]]> - - 0 - false - 48a5f871ef9fd19708f5cbd900002061 0 0 @@ -3474,13 +3474,10 @@ zNLfaoZA///+//PAj5CH]]> 0 0 - CylinderUngroupedZone false null 1 - -1 - - 48a5f871ef9fd19708f5cbd9000031ff + yuZpQdnvvUBOTYh1jqZ2cA== -58.1676903 @@ -3497,11 +3494,20 @@ zNLfaoZA///+//PAj5CH]]> 0 + + 0 + false + 48a5f871ef9fd19708f5cbd900002061 + CylinderUngroupedZone + -1 + + 48a5f871ef9fd19708f5cbd9000031ff + 2 + 1 true - true -0.5 0.5 @@ -3528,7 +3534,6 @@ zNLfaoZA///+//PAj5CH]]> false true true - 0 true Default 0 @@ -3536,13 +3541,11 @@ zNLfaoZA///+//PAj5CH]]> false - false true -0.5 0.5 0 0 - 48a5f871ef9fd19708f5cbd900002067 -0.5 0.5 0 @@ -3551,7 +3554,6 @@ zNLfaoZA///+//PAj5CH]]> false 288 - Wall1 0 0 @@ -3577,32 +3579,36 @@ zNLfaoZA///+//PAj5CH]]> 0 0 - -1 - -0.5 0.5 3 0 0.5 - 48a5f871ef9fd19708f5cbd900003200 0 0 0 - 1 - 2 13.8993607 18.5324821 18.5324821 + + 0 + false + 48a5f871ef9fd19708f5cbd900002067 + Wall1 + -1 + + 48a5f871ef9fd19708f5cbd900003200 + 1 + 1 true - true -0.5 0.5 @@ -3629,7 +3635,6 @@ zNLfaoZA///+//PAj5CH]]> false true true - 0 true Default 0 @@ -3637,13 +3642,11 @@ zNLfaoZA///+//PAj5CH]]> false - false true -0.5 0.5 0 0 - 48a5f871ef9fd19708f5cbd900002068 -0.5 0.5 0 @@ -3652,7 +3655,6 @@ zNLfaoZA///+//PAj5CH]]> false 256 - Label 0 0 @@ -3678,73 +3680,107 @@ zNLfaoZA///+//PAj5CH]]> 0 0 - -1 - -0.5 0.5 3 0 0 - 48a5f871ef9fd19708f5cbd900003201 0 0 0 - 1 - 1 47.1774368 22.7054386 2.35887194 + + 0 + false + 48a5f871ef9fd19708f5cbd900002068 + Label + -1 + + 48a5f871ef9fd19708f5cbd900003201 - true - null false - - true 1 800 600 - 0 false - false - true - 5 - 48a5f871ef9fd19708f5cbd900002069 0 0 - SurfaceGui 50 + 0 + 0 + 0 + true + null + 5 + true true + 0 + true null 0 0 0 0 false - 0 + + 0 + false + 48a5f871ef9fd19708f5cbd900002069 + SurfaceGui -1 - 0 48a5f871ef9fd19708f5cbd900003202 - 0 - 0 + + rbxasset://fonts/families/LegacyArial.json + 400 + + rbxasset://fonts/Arimo-Regular.ttf + + 1 + + + -1 + + false + + + 1 + 1 + 1 + + 0 + true + 8 + + 0 + 0 + 0 + + 1 + 0 + 0 + true + 2 + 1 false 0 0 - - true 0 0.639215708 @@ -3759,44 +3795,22 @@ zNLfaoZA///+//PAj5CH]]> 0 1 - 0 false - false false - - rbxasset://fonts/families/LegacyArial.json - 400 - - rbxasset://fonts/Arimo-Regular.ttf - - 48a5f871ef9fd19708f5cbd90000206a true 0 - 1 - - - -1 - TextLabel null null null null - 0 0 0 0 - false - null 0 false - 0 - 0 - 0 - 0 - false null 0 @@ -3806,60 +3820,51 @@ zNLfaoZA///+//PAj5CH]]> 0 0 + true + 1 + true + null + 0 + 0 + 0 + 0 + false + + 0 + false + 48a5f871ef9fd19708f5cbd90000206a + TextLabel -1 - - - 1 - 1 - 1 - - 0 - true - 8 - - 0 - 0 - 0 - - 1 - 0 - 0 - true - 2 - 1 48a5f871ef9fd19708f5cbd900003203 - true - 1 0 - 0 0 0 - 0 0 0 0 - false true - 48a5f871ef9fd19708f5cbd900002a9a 0 - UIStroke - -1 0 - 25 0 - 48a5f871ef9fd19708f5cbd900003204 1 + + 0 + false + 48a5f871ef9fd19708f5cbd900002a9a + UIStroke + -1 + + 48a5f871ef9fd19708f5cbd900003204 @@ -3867,8 +3872,9 @@ zNLfaoZA///+//PAj5CH]]> + 2 + 1 true - true -0.5 0.5 @@ -3895,7 +3901,6 @@ zNLfaoZA///+//PAj5CH]]> false true true - 0 true Default 0 @@ -3903,13 +3908,11 @@ zNLfaoZA///+//PAj5CH]]> false - false true -0.5 0.5 0 0 - 48a5f871ef9fd19708f5cbd900002067 -0.5 0.5 0 @@ -3918,7 +3921,6 @@ zNLfaoZA///+//PAj5CH]]> false 288 - Wall2 0 0 @@ -3944,32 +3946,36 @@ zNLfaoZA///+//PAj5CH]]> 0 0 - -1 - -0.5 0.5 3 0 0.5 - 48a5f871ef9fd19708f5cbd900003205 0 0 0 - 1 - 2 13.8993607 18.5324821 18.5324821 + + 0 + false + 48a5f871ef9fd19708f5cbd900002067 + Wall2 + -1 + + 48a5f871ef9fd19708f5cbd900003205 + 2 + 1 true - true -0.5 0.5 @@ -3996,7 +4002,6 @@ zNLfaoZA///+//PAj5CH]]> false true true - 0 true Default 0 @@ -4004,13 +4009,11 @@ zNLfaoZA///+//PAj5CH]]> false - false true -0.5 0.5 0 0 - 48a5f871ef9fd19708f5cbd900002067 -0.5 0.5 0 @@ -4019,7 +4022,6 @@ zNLfaoZA///+//PAj5CH]]> false 288 - Wall3 0 0 @@ -4045,90 +4047,709 @@ zNLfaoZA///+//PAj5CH]]> 0 0 - -1 - -0.5 0.5 3 0 0.5 - 48a5f871ef9fd19708f5cbd900003206 0 0 0 - 1 - 2 13.8993607 18.5324821 18.5324821 + + 0 + false + 48a5f871ef9fd19708f5cbd900002067 + Wall3 + -1 + + 48a5f871ef9fd19708f5cbd900003206 - - - - - - 0 - false - 00000000000000000000000000000000 - Instance - -1 - - 48a5f871ef9fd19708f5cbd900000326 - - - - - false - 0 - - 0 - 0 - 0 - 0 - 3 - false - 3.32999992 - 1 - 00000000000000000000000000000000 - false - SoundService - true - 1 - -1 - - 48a5f871ef9fd19708f5cbd900000327 - 1 - - - - - - 0 - false - 00000000000000000000000000000000 - VideoCaptureService - -1 - - 48a5f871ef9fd19708f5cbd900000333 - - - - - - 0 - false - 00000000000000000000000000000000 - NonReplicatedCSGDictionaryService - -1 - - 48a5f871ef9fd19708f5cbd900000334 - + + + + 0 + false + 00000000000000000000000000000000 + DestroyTestZone + -1 + + 4b8dae190c032fa509302c8a000022b9 + + + + 1 + 1 + true + true + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + + 41.6962395 + 10 + -56.3148651 + -0.878907979 + 0 + 0.476990849 + 0 + 1 + 0 + -0.476990849 + 0 + -0.878907979 + + false + true + true + true + Default + 0 + 4294953010 + + false + + true + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + false + false + 288 + + + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + 0 + -0.5 + 0.5 + 0 + 0 + 0 + + 0 + 0 + 0 + + -0.5 + 0.5 + 3 + 0 + 0.699999988 + + 0 + 0 + 0 + + + 20 + 15 + 20 + + + 0 + false + 00000000000000000000000000000000 + DestroyableZonePart + -1 + + 4b8dae190c032fa509302c8a000022ba + + + + + 1 + 1 + true + true + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + + 32.156414 + 1 + -38.7367096 + -0.878907979 + 0 + 0.476990849 + 0 + 1 + 0 + -0.476990849 + 0 + -0.878907979 + + false + true + true + true + Default + 0 + 4294914610 + + false + + true + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + false + false + 272 + + + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + 0 + -0.5 + 0.5 + 0 + 0 + 0 + + 0 + 0 + 0 + + -0.5 + 0.5 + 3 + 0 + 0 + + 0 + 0 + 0 + + + 8 + 1 + 8 + + + 0 + false + 00000000000000000000000000000000 + DestroyButton + -1 + + 4b8dae190c032fa509302c8a000022bb + + + + false + 1 + + 800 + 600 + + false + 0 + 0 + 50 + 0 + 0 + 0 + true + null + 1 + true + true + 0 + true + null + 0 + 0 + 0 + 0 + false + + 0 + false + 00000000000000000000000000000000 + SurfaceGui + -1 + + 4b8dae190c032fa509302c8a000022bc + + + + + rbxasset://fonts/families/GothamSSm.json + 700 + + rbxasset://fonts/Montserrat-Bold.ttf + + 1 + + + -1 + + false + + + 1 + 1 + 1 + + 0 + true + 8 + + 0 + 0 + 0 + + 1 + 0 + 0 + true + 2 + 1 + false + + 0 + 0 + + 0 + + 0.639215708 + 0.635294139 + 0.647058845 + + 1 + + 0.105882354 + 0.164705887 + 0.20784314 + + 0 + 1 + false + false + true + 0 + null + null + null + null + + 0 + 0 + 0 + 0 + + 0 + false + null + 0 + + 1 + 0 + 1 + 0 + + 0 + true + 1 + true + null + 0 + 0 + 0 + 0 + false + + 0 + false + 00000000000000000000000000000000 + TextLabel + -1 + + 4b8dae190c032fa509302c8a000022bd + + + + + + + 1 + 1 + true + true + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + + 46.565731 + 40.8582458 + -65.8494415 + -0.890576065 + 0 + 0.454834402 + 0 + 1 + 0 + -0.454834402 + 0 + -0.890576065 + + false + true + true + true + Default + 0 + 4288042325 + + false + + true + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + false + false + 256 + + + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + 0 + -0.5 + 0.5 + 0 + 0 + 0 + + 0 + 0 + 0 + + -0.5 + 0.5 + 3 + 0 + 0 + + 0 + 0 + 0 + + + 47.1774368 + 22.7054386 + 2.35887194 + + + 0 + false + 00000000000000000000000000000000 + Label + -1 + + 4b8dae190c032fa509302c8a000026e9 + + + + false + 1 + + 800 + 600 + + false + 0 + 0 + 50 + 0 + 0 + 0 + true + null + 5 + true + true + 0 + true + null + 0 + 0 + 0 + 0 + false + + 0 + false + 00000000000000000000000000000000 + SurfaceGui + -1 + + 4b8dae190c032fa509302c8a000026ea + + + + + rbxasset://fonts/families/LegacyArial.json + 400 + + rbxasset://fonts/Arimo-Regular.ttf + + 1 + + + -1 + + false + + + 1 + 1 + 1 + + 0 + true + 8 + + 0 + 0 + 0 + + 1 + 0 + 0 + true + 2 + 1 + false + + 0 + 0 + + 0 + + 0.639215708 + 0.635294139 + 0.647058845 + + 1 + + 0.105882354 + 0.164705887 + 0.20784314 + + 0 + 1 + false + false + true + 0 + null + null + null + null + + 0 + 0 + 0 + 0 + + 0 + false + null + 0 + + 1 + 0 + 1 + 0 + + 0 + true + 1 + true + null + 0 + 0 + 0 + 0 + false + + 0 + false + 00000000000000000000000000000000 + TextLabel + -1 + + 4b8dae190c032fa509302c8a000026eb + + + + 0 + + 0 + 0 + + 0 + + 0 + 0 + 0 + + true + 0 + 0 + 25 + 0 + 1 + + 0 + false + 00000000000000000000000000000000 + UIStroke + -1 + + 4b8dae190c032fa509302c8a000026ec + + + + + + + + + + + + 0 + false + 00000000000000000000000000000000 + Instance + -1 + + 48a5f871ef9fd19708f5cbd900000326 + + + + + false + 0 + 0 + 0 + 3 + 3.32999992 + 1 + false + true + 1 + 1 + + 0 + false + 00000000000000000000000000000000 + SoundService + -1 + + 48a5f871ef9fd19708f5cbd900000327 + + + + + + 0 + false + 00000000000000000000000000000000 + VideoCaptureService + -1 + + 48a5f871ef9fd19708f5cbd900000333 + + + + + + 0 + false + 00000000000000000000000000000000 + NonReplicatedCSGDictionaryService + -1 + + 48a5f871ef9fd19708f5cbd900000334 + @@ -4144,13 +4765,13 @@ zNLfaoZA///+//PAj5CH]]> - true + false + true + 0 false 00000000000000000000000000000000 - false - true Chat -1 @@ -4159,20 +4780,20 @@ zNLfaoZA///+//PAj5CH]]> - true - 0 true - false - 00000000000000000000000000000000 30 - Players 30 3 + false + + 0 + false + 00000000000000000000000000000000 + Players -1 48a5f871ef9fd19708f5cbd90000033d - false @@ -4202,10 +4823,8 @@ zNLfaoZA///+//PAj5CH]]> Asphalt - Basalt Brick - 0 Cardboard Carpet CeramicTiles @@ -4214,7 +4833,6 @@ zNLfaoZA///+//PAj5CH]]> Concrete CorrodedMetal CrackedLava - false DiamondPlate Fabric Foil @@ -4222,7 +4840,6 @@ zNLfaoZA///+//PAj5CH]]> Granite Grass Ground - 00000000000000000000000000000000 Ice LeafyGrass Leather @@ -4230,7 +4847,6 @@ zNLfaoZA///+//PAj5CH]]> Marble Metal Mud - MaterialService Pavement Pebble Plaster @@ -4244,27 +4860,32 @@ zNLfaoZA///+//PAj5CH]]> Slate SmoothPlastic Snow - -1 - - 48a5f871ef9fd19708f5cbd900000343 true Wood WoodPlanks + + 0 + false + 00000000000000000000000000000000 + MaterialService + -1 + + 48a5f871ef9fd19708f5cbd900000343 - - 0 true false 1 true true - false false - 00000000000000000000000000000000 true + + 0 + false + 00000000000000000000000000000000 TextChatService -1 @@ -4272,15 +4893,12 @@ zNLfaoZA///+//PAj5CH]]> - 0.0980392173 0.105882354 0.113725491 0.2999999999999999889 - 0 - false true rbxasset://fonts/families/BuilderSans.json @@ -4289,11 +4907,7 @@ zNLfaoZA///+//PAj5CH]]> rbxasset://fonts/BuilderSans-Medium.otf 1 - 00000000000000000000000000000000 1 - ChatWindowConfiguration - -1 - 1 1 @@ -4306,14 +4920,20 @@ zNLfaoZA///+//PAj5CH]]> 0 0.5 - 48a5f871ef9fd19708f5cbd9000003b3 1 1 + + 0 + false + 00000000000000000000000000000000 + ChatWindowConfiguration + -1 + + 48a5f871ef9fd19708f5cbd9000003b3 - true 0.0980392173 @@ -4321,8 +4941,6 @@ zNLfaoZA///+//PAj5CH]]> 0.113725491 0.2000000000000000111 - 0 - false true rbxasset://fonts/families/BuilderSans.json @@ -4330,16 +4948,12 @@ zNLfaoZA///+//PAj5CH]]> rbxasset://fonts/BuilderSans-Medium.otf - 00000000000000000000000000000000 47 - ChatInputBarConfiguration 0.698039234 0.698039234 0.698039234 - -1 - null 1 @@ -4353,13 +4967,19 @@ zNLfaoZA///+//PAj5CH]]> 0 0.5 + + 0 + false + 00000000000000000000000000000000 + ChatInputBarConfiguration + -1 + 48a5f871ef9fd19708f5cbd9000003b4 HumanoidRootPart - 0.980392158 0.980392158 @@ -4368,8 +4988,6 @@ zNLfaoZA///+//PAj5CH]]> 0.10000000000000000555 15 6 - 0 - false true 47 @@ -4378,7 +4996,6 @@ zNLfaoZA///+//PAj5CH]]> rbxasset://fonts/BuilderSans-Medium.otf - 00000000000000000000000000000000 0 0 @@ -4387,9 +5004,6 @@ zNLfaoZA///+//PAj5CH]]> 3 100 40 - BubbleChatConfiguration - -1 - true 0.223529413 @@ -4397,38 +5011,77 @@ zNLfaoZA///+//PAj5CH]]> 0.239215687 20 - 48a5f871ef9fd19708f5cbd9000003b5 0 + + 0 + false + 00000000000000000000000000000000 + BubbleChatConfiguration + -1 + + 48a5f871ef9fd19708f5cbd9000003b5 - - 0 0 1 1 1 0 1 1 1 1 0 - false false - 00000000000000000000000000000000 - UIGradient 0 0 0 + 0 0 0 1 0 0 + + 0 + false + 00000000000000000000000000000000 + UIGradient -1 - 0 0 0 1 0 0 48a5f871ef9fd19708f5cbd9000003b6 + + + 1 + 1 + 1 + + + 0 + 0 + + + 0 + 0 + + 0 + 0 + 0 + + + 0 + 0 + + + 0 + 0 + + + 1 + + 1 + 0 + 1 + 0 + false 0 0 - - true 0 1 @@ -4443,29 +5096,10 @@ zNLfaoZA///+//PAj5CH]]> 0 1 - 0 false - false false - 00000000000000000000000000000000 - - - 1 - 1 - 1 - - - 0 - 0 - - - 0 - 0 - - 0 true 0 - ImageLabel null null null @@ -4476,16 +5110,8 @@ zNLfaoZA///+//PAj5CH]]> 0 0 - 0 - null 0 - 0 false - 0 - 0 - 0 - 0 - false null 0 @@ -4495,38 +5121,33 @@ zNLfaoZA///+//PAj5CH]]> 100 0 - - - 0 - 0 - - - 0 - 0 - - - 1 + true + 1 + true + null + 0 + 0 + 0 + 0 + false + + 0 + false + 00000000000000000000000000000000 + ImageLabel -1 - - 1 - 0 - 1 - 0 - 48a5f871ef9fd19708f5cbd9000003b7 - true - 1 - - 0 0 12 + + 0 false 00000000000000000000000000000000 UICorner @@ -4537,11 +5158,6 @@ zNLfaoZA///+//PAj5CH]]> - - 0 - false - 00000000000000000000000000000000 - UIPadding 0 8 @@ -4558,6 +5174,11 @@ zNLfaoZA///+//PAj5CH]]> 0 8 + + 0 + false + 00000000000000000000000000000000 + UIPadding -1 48a5f871ef9fd19708f5cbd9000003b9 @@ -4566,15 +5187,12 @@ zNLfaoZA///+//PAj5CH]]> - 0.0980392173 0.105882354 0.113725491 0 - 0 - false false rbxasset://fonts/families/BuilderSans.json @@ -4582,20 +5200,16 @@ zNLfaoZA///+//PAj5CH]]> rbxasset://fonts/BuilderSans-Bold.otf - 00000000000000000000000000000000 0.490196079 0.490196079 0.490196079 - ChannelTabsConfiguration 1 1 1 - -1 - 0.686274529 0.686274529 @@ -4608,6 +5222,13 @@ zNLfaoZA///+//PAj5CH]]> 0 1 + + 0 + false + 00000000000000000000000000000000 + ChannelTabsConfiguration + -1 + 48a5f871ef9fd19708f5cbd9000003ba @@ -4626,29 +5247,29 @@ zNLfaoZA///+//PAj5CH]]> - - 0 false - false - 00000000000000000000000000000000 - PlayerEmulatorService false false + 0 + + 0 + false + 00000000000000000000000000000000 + PlayerEmulatorService -1 - 0 48a5f871ef9fd19708f5cbd900000347 + false 0 false - false 00000000000000000000000000000000 StudioData -1 @@ -4659,20 +5280,19 @@ zNLfaoZA///+//PAj5CH]]> true - true 0 128 0.5 0 - 0 + true 7.19999981 50 89 false 16 true - false + true 0 0 0 @@ -4698,17 +5318,19 @@ zNLfaoZA///+//PAj5CH]]> 0 1 0.7 1 100 - 00000000000000000000000000000000 true 0 0 - StarterPlayer 100 - true + true + + 0 + false + 00000000000000000000000000000000 + StarterPlayer -1 48a5f871ef9fd19708f5cbd90000034d - true @@ -4723,15 +5345,6 @@ zNLfaoZA///+//PAj5CH]]> - - 0 - false - false - 00000000000000000000000000000000 - - ZonePlusTest - 0 - {8773AB8F-BD13-4B84-9202-F703EDFF24B2} StarterPlayerScripts or StarterGui @@ -4753,6 +5366,7 @@ local sphereZoneContainer = testFolder:WaitForChild("SphereZone") local complexZoneContainer = testFolder:WaitForChild("ComplexZone") local cylinderGroupedContainer = testFolder:WaitForChild("CylinderGroupedZone") local cylinderUngroupedContainer = testFolder:WaitForChild("CylinderUngroupedZone") +local destroyTestContainer = testFolder:WaitForChild("DestroyTestZone") print("šŸš€ ZonePlus v4.0.0 - Modern Spatial Query Edition Test") print("=" .. string.rep("=", 60)) @@ -4810,7 +5424,12 @@ for _, part in children do print(" Checking child: " .. part.Name .. " (Type: " .. part.ClassName .. ")") if part:IsA("BasePart") then print(" Part details - Position: " .. tostring(part.Position) .. ", Size: " .. tostring(part.Size)) - print(" Part properties - CanCollide: " .. tostring(part.CanCollide) .. ", Anchored: " .. tostring(part.Anchored)) + print( + " Part properties - CanCollide: " + .. tostring(part.CanCollide) + .. ", Anchored: " + .. tostring(part.Anchored) + ) local zone = Zone.new(part) zone:setAccuracy("High") @@ -4850,6 +5469,75 @@ end print("šŸ“Š Total ungrouped zones created: " .. #ungroupedZones) +-- Test 7: Destroy & Recreate Zone Test +print("\nšŸ’„ Setting up Destroy & Recreate Zone Test...") +local destroyZonePart = destroyTestContainer:WaitForChild("DestroyableZonePart") +local destroyButton = destroyTestContainer:WaitForChild("DestroyButton") + +-- Create initial zone +local destroyTestZone = Zone.new(destroyZonePart) +destroyTestZone:setAccuracy("Medium") +destroyTestZone:setDetection("Centre") +print("āœ… Destroyable Zone created initially") + +-- Track destroy count +local destroyCount = 0 +local isDestroying = false + +-- Handle button touch to destroy and recreate zone +local function onButtonTouched(otherPart) + if isDestroying then + return + end + + local humanoid = otherPart.Parent:FindFirstChild("Humanoid") + if humanoid then + isDestroying = true + destroyCount = destroyCount + 1 + + print("\nšŸ’„ [Destroy Test #" .. destroyCount .. "] Destroying zone...") + print(" Zone ID before destroy: " .. tostring(destroyTestZone.zoneId)) + + -- Destroy the zone + destroyTestZone:destroy() + print(" āœ… Zone destroyed (table.clear + setmetatable called)") + + -- Wait a moment + task.wait(0.5) + + -- Recreate the zone + print(" šŸ”„ Recreating zone...") + destroyTestZone = Zone.new(destroyZonePart) + destroyTestZone:setAccuracy("Medium") + destroyTestZone:setDetection("Centre") + print(" āœ… Zone recreated with new ID: " .. tostring(destroyTestZone.zoneId)) + + -- Reconnect enter/exit events + destroyTestZone.localPlayerEntered:Connect(function() + print("\n🟢 ENTERED: Destroy Test Zone (Cycle #" .. destroyCount .. ")") + destroyZonePart.Color = Color3.fromRGB(50, 255, 50) + end) + + destroyTestZone.localPlayerExited:Connect(function() + print("\nšŸ”“ EXITED: Destroy Test Zone (Cycle #" .. destroyCount .. ")") + destroyZonePart.Color = Color3.fromRGB(255, 200, 50) + end) + + -- Flash the button to indicate success + for i = 1, 3 do + destroyButton.Color = Color3.fromRGB(50, 255, 50) + task.wait(0.1) + destroyButton.Color = Color3.fromRGB(255, 50, 50) + task.wait(0.1) + end + + isDestroying = false + end +end + +destroyButton.Touched:Connect(onButtonTouched) +print("āœ… Destroy & Recreate test initialized - Touch the red button to test!") + -- Track zone status local zoneStatus = { boxZone = { name = "Box Zone (Container)", inZone = false, color = Color3.fromRGB(0, 170, 255) }, @@ -4900,17 +5588,29 @@ cylinderGroupedZone.localPlayerExited:Connect(function() end) -- Connect events for each ungrouped zone -for _, zoneData in ungroupedZones do +print("\nšŸ”— Connecting events for " .. #ungroupedZones .. " ungrouped zones...") +for index, zoneData in ungroupedZones do + print( + " Connecting events for zone #" + .. index + .. ": " + .. zoneData.name + .. " (ZoneID: " + .. zoneData.zone.zoneId + .. ")" + ) + zoneData.zone.localPlayerEntered:Connect(function() zoneData.inZone = true - print("\n🟢 ENTERED:", zoneData.name) + print("\n🟢 ENTERED:", zoneData.name, "| Part:", zoneData.part.Name, "| Zone:", zoneData.zone.zoneId) end) zoneData.zone.localPlayerExited:Connect(function() zoneData.inZone = false - print("\nšŸ”“ EXITED:", zoneData.name) + print("\nšŸ”“ EXITED:", zoneData.name, "| Part:", zoneData.part.Name, "| Zone:", zoneData.zone.zoneId) end) end +print("āœ… All ungrouped zone events connected!") boxZone.itemEntered:Connect(function(item) print("šŸ“¦ Item entered Box Zone:", item.Name) @@ -4967,6 +5667,7 @@ print("• Sphere Zone: Auto-detected Ball shape") print("• Complex Zone: Using GetPartsInPart for precision") print("• Cylinder Grouped Zone: Full cylinder with top/bottom") print("• Cylinder Ungrouped Zones: " .. #ungroupedZones .. " separate zones with unique colors") +print("• Destroy Test Zone: Touch red button to destroy & recreate zone") print("\nšŸ’” Walk into zones to test detection!") print("šŸ’” Your character will highlight in zone colors:") print(" šŸ”µ Blue = Box Zone") @@ -4974,10 +5675,15 @@ print(" šŸ’— Pink = Sphere Zone") print(" šŸ’š Green = Complex Zone") print(" 🟠 Orange = Cylinder Grouped Zone") print(" 🌈 Various Colors = Ungrouped Zones (Purple, Magenta, Cyan, Orange-Yellow, Lime)") +print("\nšŸ’„ Touch the RED BUTTON to test zone destroy/recreate!") +print(" • Zone will be destroyed (table.clear + setmetatable)") +print(" • New zone will be created with fresh ID") +print(" • Events will be reconnected") print("šŸ’” Watch the Output for enter/exit events") --- Performance monitoring +-- Performance monitoring and zone status debugging local lastUpdate = tick() +local lastStatusCheck = tick() local frameCount = 0 RunService.Heartbeat:Connect(function() frameCount = frameCount + 1 @@ -4987,11 +5693,40 @@ RunService.Heartbeat:Connect(function() lastUpdate = tick() frameCount = 0 end + + -- Debug: Check zone status every 3 seconds + if tick() - lastStatusCheck >= 3 then + local playerPos = humanoidRootPart.Position + print("\nšŸ” DEBUG: Player position: " .. tostring(playerPos)) + print(" Ungrouped zones status:") + for index, zoneData in ungroupedZones do + local distance = (zoneData.part.Position - playerPos).Magnitude + print( + string.format( + " Zone #%d (%s): inZone=%s, distance=%.2f", + index, + zoneData.part.Name, + tostring(zoneData.inZone), + distance + ) + ) + end + lastStatusCheck = tick() + end end) print("\nāœ… ZonePlus test script initialized successfully!") print("šŸ“ Watch the Output window for zone status updates") ]]> + false + + 0 + {8773AB8F-BD13-4B84-9202-F703EDFF24B2} + + 0 + false + 00000000000000000000000000000000 + ZonePlusTest -1 48a5f871ef9fd19708f5cbd9000023b9 @@ -5025,21 +5760,21 @@ print("šŸ“ Watch the Output window for zone status updates") - - 0 - false - 00000000000000000000000000000000 - StarterGui true 0 4 true - -1 null null + 0 + + 0 + false + 00000000000000000000000000000000 + StarterGui + -1 48a5f871ef9fd19708f5cbd90000034f - 0 @@ -5105,6 +5840,7 @@ print("šŸ“ Watch the Output window for zone status updates") + {96212A46-BCE5-4D1E-B489-74D65F69472A} 0 false @@ -5113,7 +5849,6 @@ print("šŸ“ Watch the Output window for zone status updates") -1 48a5f871ef9fd19708f5cbd9000003ad - {96212A46-BCE5-4D1E-B489-74D65F69472A} @@ -5131,11 +5866,11 @@ print("šŸ“ Watch the Output window for zone status updates") + 1000 0 false 00000000000000000000000000000000 - 1000 Debris -1 @@ -5168,15 +5903,15 @@ print("šŸ“ Watch the Output window for zone status updates") - 0 false - 0 1 - false true - 00000000000000000000000000000000 1 + + 0 + false + 00000000000000000000000000000000 VRService -1 @@ -5281,13 +6016,7 @@ print("šŸ“ Watch the Output window for zone status updates") - - 0 - false - 00000000000000000000000000000000 - Zone - {4613DDB5-0D7C-46C2-B38C-6CAEBB6A6698} + {4613DDB5-0D7C-46C2-B38C-6CAEBB6A6698} + + 0 + false + 00000000000000000000000000000000 + Zone -1 48a5f871ef9fd19708f5cbd900001fe2 - - 0 - false - 00000000000000000000000000000000 - Enum - {7B92D5D9-C70A-4471-816C-09E835685A29} + {7B92D5D9-C70A-4471-816C-09E835685A29} + + 0 + false + 00000000000000000000000000000000 + Enum -1 48a5f871ef9fd19708f5cbd900001fe3 - - 0 - false - 00000000000000000000000000000000 - Accuracy - {CFB415CC-A9B9-42AE-AE9E-2D6C39EA680B} + {CFB415CC-A9B9-42AE-AE9E-2D6C39EA680B} + + 0 + false + 00000000000000000000000000000000 + Accuracy -1 48a5f871ef9fd19708f5cbd900001fe4 @@ -6395,13 +7130,7 @@ return { - - 0 - false - 00000000000000000000000000000000 - Detection - {F93DAC13-373E-4076-B182-2F7A0552B8B3} + {F93DAC13-373E-4076-B182-2F7A0552B8B3} + + 0 + false + 00000000000000000000000000000000 + Detection -1 48a5f871ef9fd19708f5cbd900001fe5 @@ -6417,13 +7152,7 @@ return { - - 0 - false - 00000000000000000000000000000000 - ZoneShape - {4E359F04-8F97-413F-86FB-165E041A270C} + {4E359F04-8F97-413F-86FB-165E041A270C} + + 0 + false + 00000000000000000000000000000000 + ZoneShape -1 48a5f871ef9fd19708f5cbd90000204e @@ -6441,13 +7176,7 @@ return { - - 0 - false - 00000000000000000000000000000000 - Janitor - {F78A5C6C-BF0E-4931-9671-3FF1D962E45F} + {F78A5C6C-BF0E-4931-9671-3FF1D962E45F} + + 0 + false + 00000000000000000000000000000000 + Janitor -1 48a5f871ef9fd19708f5cbd900001fe6 @@ -6794,13 +7529,7 @@ return Janitor]]> - - 0 - false - 00000000000000000000000000000000 - OldSignal - {235E203C-7385-468E-A40A-147F2032E10F} + {235E203C-7385-468E-A40A-147F2032E10F} + + 0 + false + 00000000000000000000000000000000 + OldSignal -1 48a5f871ef9fd19708f5cbd900001fe7 @@ -6918,13 +7653,7 @@ return Signal]]> - - 0 - false - 00000000000000000000000000000000 - Signal - {EC5B02DA-1940-45D9-8C31-48431E966C4B} + {EC5B02DA-1940-45D9-8C31-48431E966C4B} + + 0 + false + 00000000000000000000000000000000 + Signal -1 48a5f871ef9fd19708f5cbd900001fe8 @@ -7110,16 +7845,16 @@ return Signal]]> + + + {FB1EF9A3-F12B-4C72-BE04-25C595E5DD54} 0 false 00000000000000000000000000000000 - VERSION - {FB1EF9A3-F12B-4C72-BE04-25C595E5DD54} - -1 48a5f871ef9fd19708f5cbd900001fe9 @@ -7127,13 +7862,7 @@ return "4.0.0" - - 0 - false - 00000000000000000000000000000000 - ZoneController - {42E46B32-946C-4E65-9DDD-910BDA354616} = nextCheck then local lowestAccuracy local lowestDetection - for zone, _ in pairs(activeZones) do - if zone.activeTriggers[registeredTriggerType] then + for zone, _ in activeZones do + if zone.activeTriggers and zone.activeTriggers[registeredTriggerType] then local zAccuracy = zone.accuracy - if lowestAccuracy == nil or zAccuracy < lowestAccuracy then + if zAccuracy and (lowestAccuracy == nil or zAccuracy < lowestAccuracy) then lowestAccuracy = zAccuracy end - ZoneController.updateDetection(zone) - local zDetection = zone._currentEnterDetection - if lowestDetection == nil or zDetection < lowestDetection then - lowestDetection = zDetection + -- Safety check: ensure zone still has detection properties + if zone.enterDetection and zone.exitDetection then + ZoneController.updateDetection(zone) + local zDetection = zone._currentEnterDetection + if zDetection and (lowestDetection == nil or zDetection < lowestDetection) then + lowestDetection = zDetection + end end end end @@ -7312,66 +8049,71 @@ function ZoneController._formHeartbeat(registeredTriggerType) -- all other zones within the same settingGroup) local occupantsToBlock = {} local zonesToPotentiallyIgnore = {} - for zone, newOccupants in pairs(zonesAndOccupants) do - local settingsGroup = (zone.settingsGroupName and ZoneController.getGroup(zone.settingsGroupName)) - if settingsGroup and settingsGroup.onlyEnterOnceExitedAll == true then - --local currentOccupants = zone.occupants[registeredTriggerType] - --if currentOccupants then - for newOccupant, _ in pairs(newOccupants) do - --if currentOccupants[newOccupant] then - local groupDetail = occupantsToBlock[zone.settingsGroupName] - if not groupDetail then - groupDetail = {} - occupantsToBlock[zone.settingsGroupName] = groupDetail + for zone, newOccupants in zonesAndOccupants do + -- Safety check: ensure zone still has settingsGroupName property + if zone.settingsGroupName then + local settingsGroup = ZoneController.getGroup(zone.settingsGroupName) + if settingsGroup and settingsGroup.onlyEnterOnceExitedAll == true then + --local currentOccupants = zone.occupants[registeredTriggerType] + --if currentOccupants then + for newOccupant, _ in newOccupants do + --if currentOccupants[newOccupant] then + local groupDetail = occupantsToBlock[zone.settingsGroupName] + if not groupDetail then + groupDetail = {} + occupantsToBlock[zone.settingsGroupName] = groupDetail + end + groupDetail[newOccupant] = zone + --end end - groupDetail[newOccupant] = zone + zonesToPotentiallyIgnore[zone] = newOccupants --end end - zonesToPotentiallyIgnore[zone] = newOccupants - --end end end - for zone, newOccupants in pairs(zonesToPotentiallyIgnore) do - local groupDetail = occupantsToBlock[zone.settingsGroupName] - if groupDetail then - for newOccupant, _ in pairs(newOccupants) do - local occupantToKeepZone = groupDetail[newOccupant] - if occupantToKeepZone and occupantToKeepZone ~= zone then - newOccupants[newOccupant] = nil + for zone, newOccupants in zonesToPotentiallyIgnore do + -- Safety check: ensure zone still has settingsGroupName property + if zone.settingsGroupName then + local groupDetail = occupantsToBlock[zone.settingsGroupName] + if groupDetail then + for newOccupant, _ in newOccupants do + local occupantToKeepZone = groupDetail[newOccupant] + if occupantToKeepZone and occupantToKeepZone ~= zone then + newOccupants[newOccupant] = nil + end end end end end -- This deduces what signals should be fired - local collectiveSignalsToFire = { {}, {} } - for zone, _ in pairs(activeZones) do - if zone.activeTriggers[registeredTriggerType] then + local collectiveSignalsToFire = { {}, {} } + for zone, _ in activeZones do + if zone.activeTriggers and zone.activeTriggers[registeredTriggerType] then local zAccuracy = zone.accuracy - local occupantsDict = zonesAndOccupants[zone] or {} - local occupantsPresent = false - for k, v in pairs(occupantsDict) do - occupantsPresent = true - break - end - if occupantsPresent and zAccuracy > highestAccuracy then + local occupantsDict = zonesAndOccupants[zone] or {} + local occupantsPresent = next(occupantsDict) ~= nil + if occupantsPresent and zAccuracy and zAccuracy > highestAccuracy then highestAccuracy = zAccuracy end - local signalsToFire = zone:_updateOccupants(registeredTriggerType, occupantsDict) - collectiveSignalsToFire[1][zone] = signalsToFire.exited - collectiveSignalsToFire[2][zone] = signalsToFire.entered + -- Safety check: ensure zone still has necessary methods + if zone._updateOccupants then + local signalsToFire = zone:_updateOccupants(registeredTriggerType, occupantsDict) + collectiveSignalsToFire[1][zone] = signalsToFire.exited + collectiveSignalsToFire[2][zone] = signalsToFire.entered + end end end -- This ensures all exited signals and called before entered signals local indexToSignalType = { "Exited", "Entered" } - for index, zoneAndOccupants in pairs(collectiveSignalsToFire) do + for index, zoneAndOccupants in collectiveSignalsToFire do local signalType = indexToSignalType[index] local signalName = registeredTriggerType .. signalType - for zone, occupants in pairs(zoneAndOccupants) do + for zone, occupants in zoneAndOccupants do local signal = zone[signalName] if signal then - for _, occupant in pairs(occupants) do + for _, occupant in occupants do signal:Fire(occupant) end end @@ -7397,12 +8139,18 @@ function ZoneController._deregisterConnection(registeredZone, registeredTriggerT else activeTriggers[registeredTriggerType] -= 1 end - registeredZone.activeTriggers[registeredTriggerType] = nil - if dictLength(registeredZone.activeTriggers) == 0 then + -- Safety check: ensure zone still has activeTriggers property + if registeredZone.activeTriggers then + registeredZone.activeTriggers[registeredTriggerType] = nil + if dictLength(registeredZone.activeTriggers) == 0 then + activeZones[registeredZone] = nil + ZoneController._updateZoneDetails() + end + else + -- If activeTriggers is already nil, ensure zone is removed from activeZones activeZones[registeredZone] = nil - ZoneController._updateZoneDetails() end - if registeredZone.touchedConnectionActions[registeredTriggerType] then + if registeredZone.touchedConnectionActions and registeredZone.touchedConnectionActions[registeredTriggerType] then registeredZone:_disconnectTouchedConnection(registeredTriggerType) end end @@ -7413,18 +8161,21 @@ function ZoneController._updateZoneDetails() allParts = {} allPartToZone = {} activeZonesTotalVolume = 0 - for zone, _ in pairs(registeredZones) do - local isActive = activeZones[zone] - if isActive then - activeZonesTotalVolume += zone.volume - end - for _, zonePart in pairs(zone.zoneParts) do + for zone, _ in registeredZones do + -- Safety check: ensure zone still has required properties + if zone.volume and zone.zoneParts then + local isActive = activeZones[zone] if isActive then - table.insert(activeParts, zonePart) - activePartToZone[zonePart] = zone + activeZonesTotalVolume += zone.volume + end + for _, zonePart in zone.zoneParts do + if isActive then + table.insert(activeParts, zonePart) + activePartToZone[zonePart] = zone + end + table.insert(allParts, zonePart) + allPartToZone[zonePart] = zone end - table.insert(allParts, zonePart) - allPartToZone[zonePart] = zone end end end @@ -7436,10 +8187,13 @@ function ZoneController._getZonesAndItems( onlyActiveZones, recommendedDetection ) - local totalZoneVolume = zoneCustomVolume - if not totalZoneVolume then - for zone, _ in pairs(zonesDictToCheck) do - totalZoneVolume += zone.volume + local totalZoneVolume = zoneCustomVolume or 0 + if not zoneCustomVolume then + for zone, _ in zonesDictToCheck do + -- Safety check: ensure zone still has volume property + if zone.volume then + totalZoneVolume += zone.volume + end end end local zonesAndOccupants = {} @@ -7449,10 +8203,11 @@ function ZoneController._getZonesAndItems( -- volume of all active zones (i.e. zones which listen for .playerEntered) -- then it's more efficient cast checks within each character and -- then determine the zones they belong to - for _, item in pairs(tracker.items) do + for _, item in tracker.items do local touchingZones = ZoneController.getTouchingZones(item, onlyActiveZones, recommendedDetection, tracker) - for _, zone in pairs(touchingZones) do - if not onlyActiveZones or zone.activeTriggers[trackerName] then + for _, zone in touchingZones do + -- Safety check: ensure zone still has activeTriggers + if zone.activeTriggers and (not onlyActiveZones or zone.activeTriggers[trackerName]) then local finalItem = item if trackerName == "player" then finalItem = players:GetPlayerFromCharacter(item) @@ -7467,24 +8222,29 @@ function ZoneController._getZonesAndItems( -- If the volume of all *active zones* within the server is *less than* the total -- volume of all characters/items, then it's more efficient to perform the -- checks directly within each zone to determine players inside - for zone, _ in pairs(zonesDictToCheck) do + for zone, _ in zonesDictToCheck do if not onlyActiveZones or zone.activeTriggers[trackerName] then + -- Safety check: ensure zone properties still exist (zone might be destroying) + if not zone.regionCFrame or not zone.regionSize or not zone.activeTriggers then + continue + end local result = CollectiveWorldModel:GetPartBoundsInBox(zone.regionCFrame, zone.regionSize, tracker.whitelistParams) local finalItemsDict = {} - for _, itemOrChild in pairs(result) do + for _, itemOrChild in result do local correspondingItem = tracker.partToItem[itemOrChild] if not finalItemsDict[correspondingItem] then finalItemsDict[correspondingItem] = true end end - for item, _ in pairs(finalItemsDict) do - if trackerName == "player" then + for item, _ in finalItemsDict do + -- Safety check: ensure zone methods still exist (zone might be destroying) + if trackerName == "player" and zone.findPlayer then local player = players:GetPlayerFromCharacter(item) - if zone:findPlayer(player) then + if player and zone:findPlayer(player) then fillOccupants(zonesAndOccupants, zone, player) end - elseif zone:findItem(item) then + elseif zone.findItem and zone:findItem(item) then fillOccupants(zonesAndOccupants, zone, item) end end @@ -7497,7 +8257,7 @@ end -- PUBLIC FUNCTIONS function ZoneController.getZones() local registeredZonesArray = {} - for zone, _ in pairs(registeredZones) do + for zone, _ in registeredZones do table.insert(registeredZonesArray, zone) end return registeredZonesArray @@ -7508,7 +8268,7 @@ end -- hence im disabling this as it may be depreciated quite soon function ZoneController.getActiveZones() local zonesArray = {} - for zone, _ in pairs(activeZones) do + for zone, _ in activeZones do table.insert(zonesArray, zone) end return zonesArray @@ -7570,7 +8330,7 @@ function ZoneController.getTouchingZones(item, onlyActiveZones, recommendedDetec local zonesDict = {} local boundParts = CollectiveWorldModel:GetPartBoundsInBox(itemCFrame, itemSize, boundParams) local boundPartsThatRequirePreciseChecks = {} - for _, boundPart in pairs(boundParts) do + for _, boundPart in boundParts do local correspondingZone = partToZoneDict[boundPart] if correspondingZone and correspondingZone.allZonePartsAreBlocks then zonesDict[correspondingZone] = true @@ -7592,13 +8352,13 @@ function ZoneController.getTouchingZones(item, onlyActiveZones, recommendedDetec preciseParams.FilterDescendantsInstances = boundPartsThatRequirePreciseChecks local character = item - for _, bodyPart in pairs(bodyPartsToCheck) do + for _, bodyPart in bodyPartsToCheck do local endCheck = false if not bodyPart:IsA("BasePart") or (itemIsCharacter and Tracker.bodyPartsToIgnore[bodyPart.Name]) then continue end local preciseParts = CollectiveWorldModel:GetPartsInPart(bodyPart, preciseParams) - for _, precisePart in pairs(preciseParts) do + for _, precisePart in preciseParts do if not touchingPartsDictionary[precisePart] then local correspondingZone = partToZoneDict[precisePart] if correspondingZone then @@ -7620,8 +8380,11 @@ function ZoneController.getTouchingZones(item, onlyActiveZones, recommendedDetec local touchingZonesArray = {} local newExitDetection - for zone, _ in pairs(zonesDict) do - if newExitDetection == nil or zone._currentExitDetection < newExitDetection then + for zone, _ in zonesDict do + -- Safety check: ensure zone still has _currentExitDetection property + if + zone._currentExitDetection and (newExitDetection == nil or zone._currentExitDetection < newExitDetection) + then newExitDetection = zone._currentExitDetection end table.insert(touchingZonesArray, zone) @@ -7648,7 +8411,7 @@ function ZoneController.setGroup(settingsGroupName, properties) group._memberZones = {} if typeof(properties) == "table" then - for k, v in pairs(properties) do + for k, v in properties do group[k] = v end end @@ -7674,19 +8437,19 @@ end return ZoneController ]]> + {42E46B32-946C-4E65-9DDD-910BDA354616} + + 0 + false + 00000000000000000000000000000000 + ZoneController -1 48a5f871ef9fd19708f5cbd900001fea - - 0 - false - 00000000000000000000000000000000 - CollectiveWorldModel - {32B28DF9-63D3-4C53-95FC-1EB1351314F9} + {32B28DF9-63D3-4C53-95FC-1EB1351314F9} + + 0 + false + 00000000000000000000000000000000 + CollectiveWorldModel -1 48a5f871ef9fd19708f5cbd900001feb @@ -7741,13 +8510,7 @@ return CollectiveWorldModel]]> - - 0 - false - 00000000000000000000000000000000 - Tracker - {896C1A93-B3F8-4465-B5C3-29DB91256B49} + {896C1A93-B3F8-4465-B5C3-29DB91256B49} + + 0 + false + 00000000000000000000000000000000 + Tracker -1 48a5f871ef9fd19708f5cbd900001fec @@ -7990,13 +8759,7 @@ return Tracker - - 0 - false - 00000000000000000000000000000000 - ZonePlusReference - {4CA91A45-55E9-46E4-AA68-EF7FC4F1D405} + {4CA91A45-55E9-46E4-AA68-EF7FC4F1D405} + + 0 + false + 00000000000000000000000000000000 + ZonePlusReference -1 48a5f871ef9fd19708f5cbd900001fed @@ -8039,11 +8808,11 @@ return ZonePlusReference]]> + false 0 false 00000000000000000000000000000000 - false ServerScriptService -1 @@ -8064,25 +8833,25 @@ return ZonePlusReference]]> + AAAAAA== + AAAAAA== 0 false - AAAAAA== 00000000000000000000000000000000 ServiceVisibilityService -1 48a5f871ef9fd19708f5cbd90000037c - AAAAAA== + false 0 false 00000000000000000000000000000000 - false HttpService -1 @@ -8091,12 +8860,12 @@ return ZonePlusReference]]> - true + false + 0 false 00000000000000000000000000000000 - false DataStoreService -1 @@ -8110,10 +8879,7 @@ return ZonePlusReference]]> 0.274509817 0.274509817 - 3 - 0 0 0 @@ -8124,7 +8890,6 @@ WF9PcmlnaW5hbFRlY2hub2xvZ3lPbkZpbGVMb2FkBAMAAAA=]]> 0 0 - false 1 1 0 @@ -8138,9 +8903,7 @@ WF9PcmlnaW5hbFRlY2hub2xvZ3lPbkZpbGVMb2FkBAMAAAA=]]> 0 0 true - 00000000000000000000000000000000 1 - Lighting 0.274509817 0.274509817 @@ -8149,22 +8912,23 @@ WF9PcmlnaW5hbFRlY2hub2xvZ3lPbkZpbGVMb2FkBAMAAAA=]]> false true 0.200000003 - -1 - 3 14:30:00 + + 0 + false + 00000000000000000000000000000000 + Lighting + -1 + 48a5f871ef9fd19708f5cbd90000039c - - 0 true - false - 00000000000000000000000000000000 11 rbxassetid://6444320592 - Sky rbxassetid://6444884337 rbxassetid://6444884785 rbxassetid://6444884337 @@ -8176,33 +8940,36 @@ WF9PcmlnaW5hbFRlY2hub2xvZ3lPbkZpbGVMb2FkBAMAAAA=]]> rbxassetid://6444884337 rbxassetid://6412503613 - 332039975 3000 11 rbxassetid://6196665106 + + 0 + false + 00000000000000000000000000000000 + Sky + 332039975 48a5f871ef9fd19708f5cbd9000003ae + 0.00999999978 + 0.100000001 + true 0 false - true 00000000000000000000000000000000 - 0.00999999978 SunRays -1 - 0.100000001 48a5f871ef9fd19708f5cbd9000003af - - 0 0.78039217 0.78039217 @@ -8213,13 +8980,15 @@ WF9PcmlnaW5hbFRlY2hub2xvZ3lPbkZpbGVMb2FkBAMAAAA=]]> 0.43921569 0.490196079 - false 0.300000012 0 0 + 0.25 + + 0 + false 00000000000000000000000000000000 Atmosphere - 0.25 -1 48a5f871ef9fd19708f5cbd9000003b0 @@ -8227,32 +8996,32 @@ WF9PcmlnaW5hbFRlY2hub2xvZ3lPbkZpbGVMb2FkBAMAAAA=]]> + 1 + 24 + 2 + true 0 false - true 00000000000000000000000000000000 - 1 Bloom - 24 -1 - 2 48a5f871ef9fd19708f5cbd9000003b1 + 0.100000001 + 0.0500000007 + 30 + 0.75 + false 0 false - false - 0.100000001 - 0.0500000007 00000000000000000000000000000000 - 30 DepthOfField - 0.75 -1 48a5f871ef9fd19708f5cbd9000003b2 @@ -8273,13 +9042,13 @@ WF9PcmlnaW5hbFRlY2hub2xvZ3lPbkZpbGVMb2FkBAMAAAA=]]> + true + 16 + 16 0 false - true 00000000000000000000000000000000 - 16 - 16 ProximityPromptService -1 @@ -8300,22 +9069,22 @@ WF9PcmlnaW5hbFRlY2hub2xvZ3lPbkZpbGVMb2FkBAMAAAA=]]> - true - 0 - false false - 00000000000000000000000000000000 true true - TestService 0 0 - -1 - true 10 + + 0 + false + 00000000000000000000000000000000 + TestService + -1 + 48a5f871ef9fd19708f5cbd9000003a0 @@ -8345,17 +9114,17 @@ WF9PcmlnaW5hbFRlY2hub2xvZ3lPbkZpbGVMb2FkBAMAAAA=]]> + 0 + true + 2 0 - 0 false - true 00000000000000000000000000000000 VoiceChatService -1 48a5f871ef9fd19708f5cbd9000003a3 - 2 diff --git a/src/Testers/ZonePlusTest.client.lua b/src/Testers/ZonePlusTest.client.lua index eb9cd97..8690a91 100644 --- a/src/Testers/ZonePlusTest.client.lua +++ b/src/Testers/ZonePlusTest.client.lua @@ -19,6 +19,7 @@ local sphereZoneContainer = testFolder:WaitForChild("SphereZone") local complexZoneContainer = testFolder:WaitForChild("ComplexZone") local cylinderGroupedContainer = testFolder:WaitForChild("CylinderGroupedZone") local cylinderUngroupedContainer = testFolder:WaitForChild("CylinderUngroupedZone") +local destroyTestContainer = testFolder:WaitForChild("DestroyTestZone") print("šŸš€ ZonePlus v4.0.0 - Modern Spatial Query Edition Test") print("=" .. string.rep("=", 60)) @@ -121,6 +122,75 @@ end print("šŸ“Š Total ungrouped zones created: " .. #ungroupedZones) +-- Test 7: Destroy & Recreate Zone Test +print("\nšŸ’„ Setting up Destroy & Recreate Zone Test...") +local destroyZonePart = destroyTestContainer:WaitForChild("DestroyableZonePart") +local destroyButton = destroyTestContainer:WaitForChild("DestroyButton") + +-- Create initial zone +local destroyTestZone = Zone.new(destroyZonePart) +destroyTestZone:setAccuracy("Medium") +destroyTestZone:setDetection("Centre") +print("āœ… Destroyable Zone created initially") + +-- Track destroy count +local destroyCount = 0 +local isDestroying = false + +-- Handle button touch to destroy and recreate zone +local function onButtonTouched(otherPart) + if isDestroying then + return + end + + local humanoid = otherPart.Parent:FindFirstChild("Humanoid") + if humanoid then + isDestroying = true + destroyCount = destroyCount + 1 + + print("\nšŸ’„ [Destroy Test #" .. destroyCount .. "] Destroying zone...") + print(" Zone ID before destroy: " .. tostring(destroyTestZone.zoneId)) + + -- Destroy the zone + destroyTestZone:destroy() + print(" āœ… Zone destroyed (table.clear + setmetatable called)") + + -- Wait a moment + task.wait(0.5) + + -- Recreate the zone + print(" šŸ”„ Recreating zone...") + destroyTestZone = Zone.new(destroyZonePart) + destroyTestZone:setAccuracy("Medium") + destroyTestZone:setDetection("Centre") + print(" āœ… Zone recreated with new ID: " .. tostring(destroyTestZone.zoneId)) + + -- Reconnect enter/exit events + destroyTestZone.localPlayerEntered:Connect(function() + print("\n🟢 ENTERED: Destroy Test Zone (Cycle #" .. destroyCount .. ")") + destroyZonePart.Color = Color3.fromRGB(50, 255, 50) + end) + + destroyTestZone.localPlayerExited:Connect(function() + print("\nšŸ”“ EXITED: Destroy Test Zone (Cycle #" .. destroyCount .. ")") + destroyZonePart.Color = Color3.fromRGB(255, 200, 50) + end) + + -- Flash the button to indicate success + for i = 1, 3 do + destroyButton.Color = Color3.fromRGB(50, 255, 50) + task.wait(0.1) + destroyButton.Color = Color3.fromRGB(255, 50, 50) + task.wait(0.1) + end + + isDestroying = false + end +end + +destroyButton.Touched:Connect(onButtonTouched) +print("āœ… Destroy & Recreate test initialized - Touch the red button to test!") + -- Track zone status local zoneStatus = { boxZone = { name = "Box Zone (Container)", inZone = false, color = Color3.fromRGB(0, 170, 255) }, @@ -250,6 +320,7 @@ print("• Sphere Zone: Auto-detected Ball shape") print("• Complex Zone: Using GetPartsInPart for precision") print("• Cylinder Grouped Zone: Full cylinder with top/bottom") print("• Cylinder Ungrouped Zones: " .. #ungroupedZones .. " separate zones with unique colors") +print("• Destroy Test Zone: Touch red button to destroy & recreate zone") print("\nšŸ’” Walk into zones to test detection!") print("šŸ’” Your character will highlight in zone colors:") print(" šŸ”µ Blue = Box Zone") @@ -257,6 +328,10 @@ print(" šŸ’— Pink = Sphere Zone") print(" šŸ’š Green = Complex Zone") print(" 🟠 Orange = Cylinder Grouped Zone") print(" 🌈 Various Colors = Ungrouped Zones (Purple, Magenta, Cyan, Orange-Yellow, Lime)") +print("\nšŸ’„ Touch the RED BUTTON to test zone destroy/recreate!") +print(" • Zone will be destroyed (table.clear + setmetatable)") +print(" • New zone will be created with fresh ID") +print(" • Events will be reconnected") print("šŸ’” Watch the Output for enter/exit events") -- Performance monitoring and zone status debugging diff --git a/src/Zone/ZoneController/init.lua b/src/Zone/ZoneController/init.lua index 609a082..287c023 100644 --- a/src/Zone/ZoneController/init.lua +++ b/src/Zone/ZoneController/init.lua @@ -33,7 +33,7 @@ ZoneController.trackers = trackers -- LOCAL FUNCTIONS local function dictLength(dictionary) local count = 0 - for _, _ in pairs(dictionary) do + for _, _ in dictionary do count += 1 end return count @@ -66,7 +66,7 @@ local heartbeatActions = { return zonesAndOccupants end local touchingZones = ZoneController.getTouchingZones(character, true, recommendedDetection, trackers.player) - for _, zone in pairs(touchingZones) do + for _, zone in touchingZones do -- Safety check: ensure zone still has activeTriggers if zone.activeTriggers and zone.activeTriggers["localPlayer"] then fillOccupants(zonesAndOccupants, zone, localPlayer) @@ -129,7 +129,7 @@ function ZoneController.updateDetection(zone) ["enterDetection"] = "_currentEnterDetection", ["exitDetection"] = "_currentExitDetection", } - for detectionType, currentDetectionName in pairs(detectionTypes) do + for detectionType, currentDetectionName in detectionTypes do local detection = zone[detectionType] local combinedTotalVolume = Tracker.getCombinedTotalVolumes() if detection == enum.Detection.Automatic then @@ -160,7 +160,7 @@ function ZoneController._formHeartbeat(registeredTriggerType) if clockTime >= nextCheck then local lowestAccuracy local lowestDetection - for zone, _ in pairs(activeZones) do + for zone, _ in activeZones do if zone.activeTriggers and zone.activeTriggers[registeredTriggerType] then local zAccuracy = zone.accuracy if zAccuracy and (lowestAccuracy == nil or zAccuracy < lowestAccuracy) then @@ -184,14 +184,14 @@ function ZoneController._formHeartbeat(registeredTriggerType) -- all other zones within the same settingGroup) local occupantsToBlock = {} local zonesToPotentiallyIgnore = {} - for zone, newOccupants in pairs(zonesAndOccupants) do + for zone, newOccupants in zonesAndOccupants do -- Safety check: ensure zone still has settingsGroupName property if zone.settingsGroupName then local settingsGroup = ZoneController.getGroup(zone.settingsGroupName) if settingsGroup and settingsGroup.onlyEnterOnceExitedAll == true then --local currentOccupants = zone.occupants[registeredTriggerType] --if currentOccupants then - for newOccupant, _ in pairs(newOccupants) do + for newOccupant, _ in newOccupants do --if currentOccupants[newOccupant] then local groupDetail = occupantsToBlock[zone.settingsGroupName] if not groupDetail then @@ -206,12 +206,12 @@ function ZoneController._formHeartbeat(registeredTriggerType) end end end - for zone, newOccupants in pairs(zonesToPotentiallyIgnore) do + for zone, newOccupants in zonesToPotentiallyIgnore do -- Safety check: ensure zone still has settingsGroupName property if zone.settingsGroupName then local groupDetail = occupantsToBlock[zone.settingsGroupName] if groupDetail then - for newOccupant, _ in pairs(newOccupants) do + for newOccupant, _ in newOccupants do local occupantToKeepZone = groupDetail[newOccupant] if occupantToKeepZone and occupantToKeepZone ~= zone then newOccupants[newOccupant] = nil @@ -222,16 +222,12 @@ function ZoneController._formHeartbeat(registeredTriggerType) end -- This deduces what signals should be fired - local collectiveSignalsToFire = { {}, {} } - for zone, _ in pairs(activeZones) do + local collectiveSignalsToFire = { {}, {} } + for zone, _ in activeZones do if zone.activeTriggers and zone.activeTriggers[registeredTriggerType] then local zAccuracy = zone.accuracy - local occupantsDict = zonesAndOccupants[zone] or {} - local occupantsPresent = false - for k, v in pairs(occupantsDict) do - occupantsPresent = true - break - end + local occupantsDict = zonesAndOccupants[zone] or {} + local occupantsPresent = next(occupantsDict) ~= nil if occupantsPresent and zAccuracy and zAccuracy > highestAccuracy then highestAccuracy = zAccuracy end @@ -246,13 +242,13 @@ function ZoneController._formHeartbeat(registeredTriggerType) -- This ensures all exited signals and called before entered signals local indexToSignalType = { "Exited", "Entered" } - for index, zoneAndOccupants in pairs(collectiveSignalsToFire) do + for index, zoneAndOccupants in collectiveSignalsToFire do local signalType = indexToSignalType[index] local signalName = registeredTriggerType .. signalType - for zone, occupants in pairs(zoneAndOccupants) do + for zone, occupants in zoneAndOccupants do local signal = zone[signalName] if signal then - for _, occupant in pairs(occupants) do + for _, occupant in occupants do signal:Fire(occupant) end end @@ -300,14 +296,14 @@ function ZoneController._updateZoneDetails() allParts = {} allPartToZone = {} activeZonesTotalVolume = 0 - for zone, _ in pairs(registeredZones) do + for zone, _ in registeredZones do -- Safety check: ensure zone still has required properties if zone.volume and zone.zoneParts then local isActive = activeZones[zone] if isActive then activeZonesTotalVolume += zone.volume end - for _, zonePart in pairs(zone.zoneParts) do + for _, zonePart in zone.zoneParts do if isActive then table.insert(activeParts, zonePart) activePartToZone[zonePart] = zone @@ -328,7 +324,7 @@ function ZoneController._getZonesAndItems( ) local totalZoneVolume = zoneCustomVolume or 0 if not zoneCustomVolume then - for zone, _ in pairs(zonesDictToCheck) do + for zone, _ in zonesDictToCheck do -- Safety check: ensure zone still has volume property if zone.volume then totalZoneVolume += zone.volume @@ -342,9 +338,9 @@ function ZoneController._getZonesAndItems( -- volume of all active zones (i.e. zones which listen for .playerEntered) -- then it's more efficient cast checks within each character and -- then determine the zones they belong to - for _, item in pairs(tracker.items) do + for _, item in tracker.items do local touchingZones = ZoneController.getTouchingZones(item, onlyActiveZones, recommendedDetection, tracker) - for _, zone in pairs(touchingZones) do + for _, zone in touchingZones do -- Safety check: ensure zone still has activeTriggers if zone.activeTriggers and (not onlyActiveZones or zone.activeTriggers[trackerName]) then local finalItem = item @@ -361,7 +357,7 @@ function ZoneController._getZonesAndItems( -- If the volume of all *active zones* within the server is *less than* the total -- volume of all characters/items, then it's more efficient to perform the -- checks directly within each zone to determine players inside - for zone, _ in pairs(zonesDictToCheck) do + for zone, _ in zonesDictToCheck do if not onlyActiveZones or zone.activeTriggers[trackerName] then -- Safety check: ensure zone properties still exist (zone might be destroying) if not zone.regionCFrame or not zone.regionSize or not zone.activeTriggers then @@ -370,13 +366,13 @@ function ZoneController._getZonesAndItems( local result = CollectiveWorldModel:GetPartBoundsInBox(zone.regionCFrame, zone.regionSize, tracker.whitelistParams) local finalItemsDict = {} - for _, itemOrChild in pairs(result) do + for _, itemOrChild in result do local correspondingItem = tracker.partToItem[itemOrChild] if not finalItemsDict[correspondingItem] then finalItemsDict[correspondingItem] = true end end - for item, _ in pairs(finalItemsDict) do + for item, _ in finalItemsDict do -- Safety check: ensure zone methods still exist (zone might be destroying) if trackerName == "player" and zone.findPlayer then local player = players:GetPlayerFromCharacter(item) @@ -396,7 +392,7 @@ end -- PUBLIC FUNCTIONS function ZoneController.getZones() local registeredZonesArray = {} - for zone, _ in pairs(registeredZones) do + for zone, _ in registeredZones do table.insert(registeredZonesArray, zone) end return registeredZonesArray @@ -407,7 +403,7 @@ end -- hence im disabling this as it may be depreciated quite soon function ZoneController.getActiveZones() local zonesArray = {} - for zone, _ in pairs(activeZones) do + for zone, _ in activeZones do table.insert(zonesArray, zone) end return zonesArray @@ -469,7 +465,7 @@ function ZoneController.getTouchingZones(item, onlyActiveZones, recommendedDetec local zonesDict = {} local boundParts = CollectiveWorldModel:GetPartBoundsInBox(itemCFrame, itemSize, boundParams) local boundPartsThatRequirePreciseChecks = {} - for _, boundPart in pairs(boundParts) do + for _, boundPart in boundParts do local correspondingZone = partToZoneDict[boundPart] if correspondingZone and correspondingZone.allZonePartsAreBlocks then zonesDict[correspondingZone] = true @@ -491,13 +487,13 @@ function ZoneController.getTouchingZones(item, onlyActiveZones, recommendedDetec preciseParams.FilterDescendantsInstances = boundPartsThatRequirePreciseChecks local character = item - for _, bodyPart in pairs(bodyPartsToCheck) do + for _, bodyPart in bodyPartsToCheck do local endCheck = false if not bodyPart:IsA("BasePart") or (itemIsCharacter and Tracker.bodyPartsToIgnore[bodyPart.Name]) then continue end local preciseParts = CollectiveWorldModel:GetPartsInPart(bodyPart, preciseParams) - for _, precisePart in pairs(preciseParts) do + for _, precisePart in preciseParts do if not touchingPartsDictionary[precisePart] then local correspondingZone = partToZoneDict[precisePart] if correspondingZone then @@ -519,7 +515,7 @@ function ZoneController.getTouchingZones(item, onlyActiveZones, recommendedDetec local touchingZonesArray = {} local newExitDetection - for zone, _ in pairs(zonesDict) do + for zone, _ in zonesDict do -- Safety check: ensure zone still has _currentExitDetection property if zone._currentExitDetection and (newExitDetection == nil or zone._currentExitDetection < newExitDetection) @@ -550,7 +546,7 @@ function ZoneController.setGroup(settingsGroupName, properties) group._memberZones = {} if typeof(properties) == "table" then - for k, v in pairs(properties) do + for k, v in properties do group[k] = v end end