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/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 new file mode 100644 index 0000000..ddb4816 --- /dev/null +++ b/src/Testers/PlaceTester.rbxlx @@ -0,0 +1,9157 @@ + + null + nil + + + 0.00120000006 + 0 + false + 1 + 0 + 0 + AQEABP////8HRGVmYXVsdA== + RBX89ADB630653C43609C6DBCC5098C2571 + 0 + true + true + -500 + 0 + + 0 + 0 + 0 + + 196.199997 + 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 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + yuZpQdnvvUBOTYh1jqZ2cA== + + 0 + 0 + 0 + + 0 + false + null + 1 + yuZpQdnvvUBOTYh1jqZ2cA== + + + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + + + 0 + false + 00000000000000000000000000000000 + Workspace + -1 + + 48a5f871ef9fd19708f5cbd900000002 + + + + + 23.8128319 + 57.9881363 + 4.27575684 + 0.992719769 + 0.0835686103 + -0.0867404938 + -0 + 0.720151365 + 0.693817139 + 0.120447606 + -0.688766003 + 0.714908361 + + null + 0 + 70 + 0 + + 23.9863129 + 56.600502 + 2.84594011 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + true + 1 + false + + 0 + false + 00000000000000000000000000000000 + Camera + -1 + + 48a5f871ef9fd19708f5cbd9000003a4 + + + + + 1 + 0 + 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 + true + Default + 0 + 4284177243 + + false + + true + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + true + 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 + 0 + 0 + 0 + + 0 + 0 + 0 + + + 2048 + 16 + 2048 + + + 0 + false + 00000000000000000000000000000000 + Baseplate + -1 + + 48a5f871ef9fd19708f5cbd9000003a5 + + + + 0 + 0 + 8 + 8 + + 0 + 0 + 0 + + + + + rbxassetid://6372755229 + + + 0.800000012 + + 0 + 0 + + + 1 + 1 + + 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 + 0 + 0 + -0.5 + 0.5 + 4 + 0 + + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + true + true + true + true + Default + 0 + 4288914085 + + false + + true + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + true + 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 + + + 2044 + 252 + 2044 + + + 0 + false + 00000000000000000000000000000000 + Terrain + -1 + + 48a5f871ef9fd19708f5cbd9000003a7 + + + + + false + 0 + true + true + 194 + 1 + 1 + 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 + true + Default + 0 + 4288914085 + + 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 + 0 + 0 + 0 + + 0 + 0 + 0 + + + 12 + 1 + 12 + + + 0 + false + 00000000000000000000000000000000 + SpawnLocation + -1 + + 48a5f871ef9fd19708f5cbd9000003a8 + + + + + 1 + 1 + 1 + + + + + rbxasset://textures/SpawnLocation.png + + + 0 + + 0 + 0 + + + 1 + 1 + + 1 + 1 + + 0 + false + 00000000000000000000000000000000 + Decal + -1 + + 48a5f871ef9fd19708f5cbd9000003a9 + + + + + + + 0 + false + 00000000000000000000000000000000 + ZonePlusTests + -1 + + 48a5f871ef9fd19708f5cbd900002060 + + + + 0 + + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + yuZpQdnvvUBOTYh1jqZ2cA== + + 0 + 0 + 0 + + 0 + false + null + 1 + yuZpQdnvvUBOTYh1jqZ2cA== + + + -58.1676903 + 13.5 + -19.4133339 + 0 + 0 + -1 + 0 + 1 + 0 + 1 + 0 + 0 + + + + 0 + false + 00000000000000000000000000000000 + CylinderGroupedZone + -1 + + 48a5f871ef9fd19708f5cbd900002061 + + + + 2 + 1 + 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 + true + Default + 0 + 4285230079 + + 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.5 + + 0 + 0 + 0 + + + 13.8993607 + 18.5324821 + 18.5324821 + + + 0 + false + 00000000000000000000000000000000 + Wall1 + -1 + + 48a5f871ef9fd19708f5cbd900002067 + + + + + 1 + 1 + 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 + true + Default + 0 + 4279069100 + + 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 + + 48a5f871ef9fd19708f5cbd900002068 + + + + 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 + + 48a5f871ef9fd19708f5cbd900002069 + + + + + 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 + + 48a5f871ef9fd19708f5cbd90000206a + + + + 0 + + 0 + 0 + + 0 + + 0 + 0 + 0 + + true + 0 + 0 + 25 + 0 + 1 + + 0 + false + 00000000000000000000000000000000 + UIStroke + -1 + + 48a5f871ef9fd19708f5cbd900002a9a + + + + + + + + 2 + 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 + true + Default + 0 + 4285230079 + + 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.5 + + 0 + 0 + 0 + + + 13.8993607 + 18.5324821 + 18.5324821 + + + 0 + false + 48a5f871ef9fd19708f5cbd900002067 + Wall2 + -1 + + 48a5f871ef9fd19708f5cbd900002fe6 + + + + + 2 + 1 + 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 + true + Default + 0 + 4285230079 + + 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.5 + + 0 + 0 + 0 + + + 21.2595978 + 23.065073 + 23.065073 + + + 0 + false + 48a5f871ef9fd19708f5cbd900002067 + Wall3 + -1 + + 48a5f871ef9fd19708f5cbd90000303b + + + + + + 0 + + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + yuZpQdnvvUBOTYh1jqZ2cA== + + 0 + 0 + 0 + + 0 + false + null + 1 + yuZpQdnvvUBOTYh1jqZ2cA== + + + 44 + 14.5 + 44 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + + + 0 + false + 00000000000000000000000000000000 + SphereZone + -1 + + 48a5f871ef9fd19708f5cbd90000206b + + + + 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 + true + Default + 0 + 4294923647 + + 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.5 + + 0 + 0 + 0 + + + 25 + 25 + 25 + + + 0 + false + 00000000000000000000000000000000 + SpherePart + -1 + + 48a5f871ef9fd19708f5cbd90000206c + + + + + 1 + 1 + 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 + true + Default + 0 + 4294901951 + + 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 + + + 37.1721001 + 19.0668831 + 1.85860503 + + + 0 + false + 00000000000000000000000000000000 + Label + -1 + + 48a5f871ef9fd19708f5cbd90000206d + + + + 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 + + 48a5f871ef9fd19708f5cbd90000206e + + + + + 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 + + 48a5f871ef9fd19708f5cbd90000206f + + + + 0 + + 0 + 0 + + 0 + + 0 + 0 + 0 + + true + 0 + 0 + 25 + 0 + 1 + + 0 + false + 00000000000000000000000000000000 + UIStroke + -1 + + 48a5f871ef9fd19708f5cbd900002ac4 + + + + + + + + + 0 + + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + yuZpQdnvvUBOTYh1jqZ2cA== + + 0 + 0 + 0 + + 0 + false + null + 1 + yuZpQdnvvUBOTYh1jqZ2cA== + + + -59 + 13.25 + 39 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + + + 0 + false + 00000000000000000000000000000000 + ComplexZone + -1 + + 48a5f871ef9fd19708f5cbd900002070 + + + + 1 + 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 + true + Default + 0 + 4286578517 + + 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.5 + + 0 + 0 + 0 + + + 30 + 10 + 15 + + + 0 + false + 00000000000000000000000000000000 + Base1 + -1 + + 48a5f871ef9fd19708f5cbd900002071 + + + + + 1 + 1 + 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 + true + Default + 0 + 4286578517 + + 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.5 + + 0 + 0 + 0 + + + 15 + 10 + 15 + + + 0 + false + 00000000000000000000000000000000 + Base2 + -1 + + 48a5f871ef9fd19708f5cbd900002072 + + + + + 1 + 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 + true + Default + 0 + 4286578517 + + 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 + 0 + 0 + 0.5 + + 0 + 0 + 0 + + + 15 + 10 + 15 + + + 0 + false + 00000000000000000000000000000000 + Wedge + -1 + + 48a5f871ef9fd19708f5cbd900002073 + + + + + 1 + 1 + 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 + true + Default + 0 + 4278255360 + + 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 + + + 38.1200256 + 18.3389969 + 1.90600133 + + + 0 + false + 00000000000000000000000000000000 + Label + -1 + + 48a5f871ef9fd19708f5cbd900002074 + + + + 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 + + 48a5f871ef9fd19708f5cbd900002075 + + + + + 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 + + 48a5f871ef9fd19708f5cbd900002076 + + + + 0 + + 0 + 0 + + 0 + + 0 + 0 + 0 + + true + 0 + 0 + 25 + 0 + 1 + + 0 + false + 00000000000000000000000000000000 + UIStroke + -1 + + 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 + true + Default + 0 + 4286578517 + + 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 + 0 + 0 + 0.5 + + 0 + 0 + 0 + + + 15 + 10 + 15 + + + 0 + false + 48a5f871ef9fd19708f5cbd900002073 + Wedge + -1 + + 48a5f871ef9fd19708f5cbd90000388e + + + + + + 1 + 1 + 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 + true + Default + 0 + 4284702562 + + false + + true + -0.5 + 0.5 + 0 + 0 + -0.5 + 0.5 + 0 + 0 + false + false + 816 + + + 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 + + + 120 + 1 + 60 + + + 0 + false + 00000000000000000000000000000000 + SpawnPlatform + -1 + + 48a5f871ef9fd19708f5cbd900002077 + + + + + false + 0 + true + true + 194 + 1 + 1 + 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 + true + Default + 0 + 4283144011 + + 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 + + + 10 + 1 + 10 + + + 0 + false + 00000000000000000000000000000000 + SpawnLocation + -1 + + 48a5f871ef9fd19708f5cbd90000207e + + + + + 0 + + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + yuZpQdnvvUBOTYh1jqZ2cA== + + 0 + 0 + 0 + + 0 + false + null + 1 + yuZpQdnvvUBOTYh1jqZ2cA== + + + -1 + 13.5 + 48.5000038 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + + + 0 + false + 48a5f871ef9fd19708f5cbd900002061 + BoxZone + -1 + + 48a5f871ef9fd19708f5cbd900002b4c + + + + 1 + 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 + true + Default + 0 + 4278233855 + + 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.5 + + 0 + 0 + 0 + + + 30 + 1 + 30 + + + 0 + false + 48a5f871ef9fd19708f5cbd900002062 + Bottom + -1 + + 48a5f871ef9fd19708f5cbd900002b4d + + + + + 1 + 1 + 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 + true + Default + 0 + 4278233855 + + 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.5 + + 0 + 0 + 0 + + + 30 + 1 + 30 + + + 0 + false + 48a5f871ef9fd19708f5cbd900002063 + Top + -1 + + 48a5f871ef9fd19708f5cbd900002b4e + + + + + 1 + 1 + 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 + true + Default + 0 + 4278233855 + + 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.5 + + 0 + 0 + 0 + + + 1 + 20 + 30 + + + 0 + false + 48a5f871ef9fd19708f5cbd900002064 + Wall1 + -1 + + 48a5f871ef9fd19708f5cbd900002b4f + + + + + 1 + 1 + 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 + true + Default + 0 + 4278233855 + + 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.5 + + 0 + 0 + 0 + + + 1 + 20 + 30 + + + 0 + false + 48a5f871ef9fd19708f5cbd900002065 + Wall2 + -1 + + 48a5f871ef9fd19708f5cbd900002b50 + + + + + 1 + 1 + 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 + true + Default + 0 + 4278233855 + + 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.5 + + 0 + 0 + 0 + + + 30 + 20 + 1 + + + 0 + false + 48a5f871ef9fd19708f5cbd900002066 + Wall3 + -1 + + 48a5f871ef9fd19708f5cbd900002b51 + + + + + 1 + 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 + true + Default + 0 + 4278233855 + + 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.5 + + 0 + 0 + 0 + + + 30 + 20 + 1 + + + 0 + false + 48a5f871ef9fd19708f5cbd900002067 + Wall4 + -1 + + 48a5f871ef9fd19708f5cbd900002b52 + + + + + 1 + 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 + true + Default + 0 + 4279069100 + + 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 + 48a5f871ef9fd19708f5cbd900002068 + Label + -1 + + 48a5f871ef9fd19708f5cbd900002b53 + + + + 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 + 48a5f871ef9fd19708f5cbd900002069 + SurfaceGui + -1 + + 48a5f871ef9fd19708f5cbd900002b54 + + + + + 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 + 48a5f871ef9fd19708f5cbd90000206a + TextLabel + -1 + + 48a5f871ef9fd19708f5cbd900002b55 + + + + 0 + + 0 + 0 + + 0 + + 0 + 0 + 0 + + true + 0 + 0 + 25 + 0 + 1 + + 0 + false + 48a5f871ef9fd19708f5cbd900002a9a + UIStroke + -1 + + 48a5f871ef9fd19708f5cbd900002b56 + + + + + + + + + 0 + + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + 0 + 0 + 0 + 1 + + yuZpQdnvvUBOTYh1jqZ2cA== + + 0 + 0 + 0 + + 0 + false + null + 1 + yuZpQdnvvUBOTYh1jqZ2cA== + + + -58.1676903 + 13.5 + -113.057159 + 0 + 0 + -1 + 0 + 1 + 0 + 1 + 0 + 0 + + + + 0 + false + 48a5f871ef9fd19708f5cbd900002061 + CylinderUngroupedZone + -1 + + 48a5f871ef9fd19708f5cbd9000031ff + + + + 2 + 1 + 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 + true + Default + 0 + 4286775295 + + 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.5 + + 0 + 0 + 0 + + + 13.8993607 + 18.5324821 + 18.5324821 + + + 0 + false + 48a5f871ef9fd19708f5cbd900002067 + Wall1 + -1 + + 48a5f871ef9fd19708f5cbd900003200 + + + + + 1 + 1 + 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 + true + Default + 0 + 4279069100 + + 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 + 48a5f871ef9fd19708f5cbd900002068 + Label + -1 + + 48a5f871ef9fd19708f5cbd900003201 + + + + 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 + 48a5f871ef9fd19708f5cbd900002069 + SurfaceGui + -1 + + 48a5f871ef9fd19708f5cbd900003202 + + + + + 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 + 48a5f871ef9fd19708f5cbd90000206a + TextLabel + -1 + + 48a5f871ef9fd19708f5cbd900003203 + + + + 0 + + 0 + 0 + + 0 + + 0 + 0 + 0 + + true + 0 + 0 + 25 + 0 + 1 + + 0 + false + 48a5f871ef9fd19708f5cbd900002a9a + UIStroke + -1 + + 48a5f871ef9fd19708f5cbd900003204 + + + + + + + + 2 + 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 + true + Default + 0 + 4293787512 + + 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.5 + + 0 + 0 + 0 + + + 13.8993607 + 18.5324821 + 18.5324821 + + + 0 + false + 48a5f871ef9fd19708f5cbd900002067 + Wall2 + -1 + + 48a5f871ef9fd19708f5cbd900003205 + + + + + 2 + 1 + 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 + true + Default + 0 + 4294922990 + + 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.5 + + 0 + 0 + 0 + + + 13.8993607 + 18.5324821 + 18.5324821 + + + 0 + false + 48a5f871ef9fd19708f5cbd900002067 + Wall3 + -1 + + 48a5f871ef9fd19708f5cbd900003206 + + + + + + + 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 + + + + + + 0 + false + 00000000000000000000000000000000 + CSGDictionaryService + -1 + + 48a5f871ef9fd19708f5cbd900000335 + + + + + true + false + true + + 0 + false + 00000000000000000000000000000000 + Chat + -1 + + 48a5f871ef9fd19708f5cbd90000033b + + + + + true + true + 30 + 30 + 3 + false + + 0 + false + 00000000000000000000000000000000 + Players + -1 + + 48a5f871ef9fd19708f5cbd90000033d + + + + + + 0 + false + 00000000000000000000000000000000 + ReplicatedFirst + -1 + + 48a5f871ef9fd19708f5cbd900000340 + + + + + + 0 + false + 00000000000000000000000000000000 + TweenService + -1 + + 48a5f871ef9fd19708f5cbd900000342 + + + + + Asphalt + Basalt + Brick + Cardboard + Carpet + CeramicTiles + ClayRoofTiles + Cobblestone + Concrete + CorrodedMetal + CrackedLava + DiamondPlate + Fabric + Foil + Glacier + Granite + Grass + Ground + Ice + LeafyGrass + Leather + Limestone + Marble + Metal + Mud + Pavement + Pebble + Plaster + Plastic + Rock + RoofShingles + Rubber + Salt + Sand + Sandstone + Slate + SmoothPlastic + Snow + true + Wood + WoodPlanks + + 0 + false + 00000000000000000000000000000000 + MaterialService + -1 + + 48a5f871ef9fd19708f5cbd900000343 + + + + + true + false + 1 + true + true + false + true + + 0 + false + 00000000000000000000000000000000 + TextChatService + -1 + + 48a5f871ef9fd19708f5cbd900000344 + + + + + 0.0980392173 + 0.105882354 + 0.113725491 + + 0.2999999999999999889 + true + + rbxasset://fonts/families/BuilderSans.json + 500 + + rbxasset://fonts/BuilderSans-Medium.otf + + 1 + 1 + + 1 + 1 + 1 + + 18 + + 0 + 0 + 0 + + 0.5 + 1 + 1 + + 0 + false + 00000000000000000000000000000000 + ChatWindowConfiguration + -1 + + 48a5f871ef9fd19708f5cbd9000003b3 + + + + + true + + 0.0980392173 + 0.105882354 + 0.113725491 + + 0.2000000000000000111 + true + + rbxasset://fonts/families/BuilderSans.json + 500 + + rbxasset://fonts/BuilderSans-Medium.otf + + 47 + + 0.698039234 + 0.698039234 + 0.698039234 + + null + + 1 + 1 + 1 + + 18 + + 0 + 0 + 0 + + 0.5 + + 0 + false + 00000000000000000000000000000000 + ChatInputBarConfiguration + -1 + + 48a5f871ef9fd19708f5cbd9000003b4 + + + + + HumanoidRootPart + + 0.980392158 + 0.980392158 + 0.980392158 + + 0.10000000000000000555 + 15 + 6 + true + 47 + + rbxasset://fonts/families/BuilderSans.json + 500 + + rbxasset://fonts/BuilderSans-Medium.otf + + + 0 + 0 + 0 + + 3 + 100 + 40 + true + + 0.223529413 + 0.23137255 + 0.239215687 + + 20 + 0 + + 0 + false + 00000000000000000000000000000000 + BubbleChatConfiguration + -1 + + 48a5f871ef9fd19708f5cbd9000003b5 + + + + 0 1 1 1 0 1 1 1 1 0 + false + + 0 + 0 + + 0 + 0 0 0 1 0 0 + + 0 + false + 00000000000000000000000000000000 + UIGradient + -1 + + 48a5f871ef9fd19708f5cbd9000003b6 + + + + + + + 1 + 1 + 1 + + + 0 + 0 + + + 0 + 0 + + 0 + 0 + 0 + + + 0 + 0 + + + 0 + 0 + + + 1 + + 1 + 0 + 1 + 0 + + false + + 0 + 0 + + 0 + + 1 + 1 + 1 + + 0 + + 0.105882362 + 0.164705887 + 0.207843155 + + 0 + 1 + false + false + true + 0 + null + null + null + null + + 0 + 0 + 0 + 0 + + 0 + false + null + 0 + + 0 + 100 + 0 + 100 + + 0 + true + 1 + true + null + 0 + 0 + 0 + 0 + false + + 0 + false + 00000000000000000000000000000000 + ImageLabel + -1 + + 48a5f871ef9fd19708f5cbd9000003b7 + + + + + + 0 + 12 + + + 0 + false + 00000000000000000000000000000000 + UICorner + -1 + + 48a5f871ef9fd19708f5cbd9000003b8 + + + + + + 0 + 8 + + + 0 + 8 + + + 0 + 8 + + + 0 + 8 + + + 0 + false + 00000000000000000000000000000000 + UIPadding + -1 + + 48a5f871ef9fd19708f5cbd9000003b9 + + + + + + + 0.0980392173 + 0.105882354 + 0.113725491 + + 0 + false + + rbxasset://fonts/families/BuilderSans.json + 700 + + rbxasset://fonts/BuilderSans-Bold.otf + + + 0.490196079 + 0.490196079 + 0.490196079 + + + 1 + 1 + 1 + + + 0.686274529 + 0.686274529 + 0.686274529 + + 18 + + 0 + 0 + 0 + + 1 + + 0 + false + 00000000000000000000000000000000 + ChannelTabsConfiguration + -1 + + 48a5f871ef9fd19708f5cbd9000003ba + + + + + + + 0 + false + 00000000000000000000000000000000 + PermissionsService + -1 + + 48a5f871ef9fd19708f5cbd900000346 + + + + + false + + + false + false + + 0 + + 0 + false + 00000000000000000000000000000000 + PlayerEmulatorService + -1 + + 48a5f871ef9fd19708f5cbd900000347 + + + + + false + + 0 + false + 00000000000000000000000000000000 + StudioData + -1 + + 48a5f871ef9fd19708f5cbd90000034b + + + + + true + true + 0 + 128 + 0.5 + 0 + true + 7.19999981 + 50 + 89 + false + 16 + true + true + 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 + true + 0 + 0 + 100 + true + + 0 + false + 00000000000000000000000000000000 + StarterPlayer + -1 + + 48a5f871ef9fd19708f5cbd90000034d + + + + + 0 + false + 00000000000000000000000000000000 + StarterPlayerScripts + -1 + + 48a5f871ef9fd19708f5cbd9000003ab + + + + 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") +local destroyTestContainer = testFolder:WaitForChild("DestroyTestZone") + +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) + +-- 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) }, + 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("• 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") +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 +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") +]]> + false + + 0 + {8773AB8F-BD13-4B84-9202-F703EDFF24B2} + + 0 + false + 00000000000000000000000000000000 + ZonePlusTest + -1 + + 48a5f871ef9fd19708f5cbd9000023b9 + + + + + + + 0 + false + 00000000000000000000000000000000 + StarterCharacterScripts + -1 + + 48a5f871ef9fd19708f5cbd9000003ac + + + + + + + 0 + false + 00000000000000000000000000000000 + StarterPack + -1 + + 48a5f871ef9fd19708f5cbd90000034e + + + + + true + 0 + 4 + true + null + null + 0 + + 0 + false + 00000000000000000000000000000000 + StarterGui + -1 + + 48a5f871ef9fd19708f5cbd90000034f + + + + + + 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 + + + + {96212A46-BCE5-4D1E-B489-74D65F69472A} + + 0 + false + 00000000000000000000000000000000 + InsertionHash + -1 + + 48a5f871ef9fd19708f5cbd9000003ad + + + + + + + 0 + false + 00000000000000000000000000000000 + GamePassService + -1 + + 48a5f871ef9fd19708f5cbd90000035c + + + + + 1000 + + 0 + false + 00000000000000000000000000000000 + Debris + -1 + + 48a5f871ef9fd19708f5cbd90000035d + + + + + + 0 + false + 00000000000000000000000000000000 + CookiesService + -1 + + 48a5f871ef9fd19708f5cbd90000035e + + + + + + 0 + false + 00000000000000000000000000000000 + Selection + -1 + + 48a5f871ef9fd19708f5cbd90000035f + + + + + 0 + false + 1 + true + 1 + + 0 + false + 00000000000000000000000000000000 + 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 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 +]]> + {4613DDB5-0D7C-46C2-B38C-6CAEBB6A6698} + + 0 + false + 00000000000000000000000000000000 + Zone + -1 + + 48a5f871ef9fd19708f5cbd900001fe2 + + + + + + {7B92D5D9-C70A-4471-816C-09E835685A29} + + 0 + false + 00000000000000000000000000000000 + Enum + -1 + + 48a5f871ef9fd19708f5cbd900001fe3 + + + + + + {CFB415CC-A9B9-42AE-AE9E-2D6C39EA680B} + + 0 + false + 00000000000000000000000000000000 + Accuracy + -1 + + 48a5f871ef9fd19708f5cbd900001fe4 + + + + + + + {F93DAC13-373E-4076-B182-2F7A0552B8B3} + + 0 + false + 00000000000000000000000000000000 + Detection + -1 + + 48a5f871ef9fd19708f5cbd900001fe5 + + + + + + + {4E359F04-8F97-413F-86FB-165E041A270C} + + 0 + false + 00000000000000000000000000000000 + ZoneShape + -1 + + 48a5f871ef9fd19708f5cbd90000204e + + + + + + + " +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]]> + {F78A5C6C-BF0E-4931-9671-3FF1D962E45F} + + 0 + false + 00000000000000000000000000000000 + Janitor + -1 + + 48a5f871ef9fd19708f5cbd900001fe6 + + + + + + 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]]> + {235E203C-7385-468E-A40A-147F2032E10F} + + 0 + false + 00000000000000000000000000000000 + OldSignal + -1 + + 48a5f871ef9fd19708f5cbd900001fe7 + + + + + + + {EC5B02DA-1940-45D9-8C31-48431E966C4B} + + 0 + false + 00000000000000000000000000000000 + Signal + -1 + + 48a5f871ef9fd19708f5cbd900001fe8 + + + + + + + {FB1EF9A3-F12B-4C72-BE04-25C595E5DD54} + + 0 + false + 00000000000000000000000000000000 + VERSION + -1 + + 48a5f871ef9fd19708f5cbd900001fe9 + + + + + + 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 activeZones do + if zone.activeTriggers and zone.activeTriggers[registeredTriggerType] then + local zAccuracy = zone.accuracy + if zAccuracy and (lowestAccuracy == nil or zAccuracy < lowestAccuracy) then + lowestAccuracy = zAccuracy + end + -- 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 + 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 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 + zonesToPotentiallyIgnore[zone] = newOccupants + --end + end + end + end + 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 activeZones do + if zone.activeTriggers and zone.activeTriggers[registeredTriggerType] then + local zAccuracy = zone.accuracy + local occupantsDict = zonesAndOccupants[zone] or {} + local occupantsPresent = next(occupantsDict) ~= nil + if occupantsPresent and zAccuracy and zAccuracy > highestAccuracy then + highestAccuracy = zAccuracy + end + -- 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 collectiveSignalsToFire do + local signalType = indexToSignalType[index] + local signalName = registeredTriggerType .. signalType + for zone, occupants in zoneAndOccupants do + local signal = zone[signalName] + if signal then + for _, occupant in 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 + -- 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 + end + if registeredZone.touchedConnectionActions and registeredZone.touchedConnectionActions[registeredTriggerType] then + registeredZone:_disconnectTouchedConnection(registeredTriggerType) + end +end + +function ZoneController._updateZoneDetails() + activeParts = {} + activePartToZone = {} + allParts = {} + allPartToZone = {} + activeZonesTotalVolume = 0 + 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 zone.zoneParts do + if isActive then + table.insert(activeParts, zonePart) + activePartToZone[zonePart] = zone + end + table.insert(allParts, zonePart) + allPartToZone[zonePart] = zone + end + end + end +end + +function ZoneController._getZonesAndItems( + trackerName, + zonesDictToCheck, + zoneCustomVolume, + onlyActiveZones, + recommendedDetection +) + 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 = {} + 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 tracker.items do + local touchingZones = ZoneController.getTouchingZones(item, onlyActiveZones, recommendedDetection, tracker) + 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) + 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 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 result do + local correspondingItem = tracker.partToItem[itemOrChild] + if not finalItemsDict[correspondingItem] then + finalItemsDict[correspondingItem] = true + end + end + 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 player and zone:findPlayer(player) then + fillOccupants(zonesAndOccupants, zone, player) + end + elseif zone.findItem and 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 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 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 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 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 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 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) + 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 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 +]]> + {42E46B32-946C-4E65-9DDD-910BDA354616} + + 0 + false + 00000000000000000000000000000000 + ZoneController + -1 + + 48a5f871ef9fd19708f5cbd900001fea + + + + + + {32B28DF9-63D3-4C53-95FC-1EB1351314F9} + + 0 + false + 00000000000000000000000000000000 + CollectiveWorldModel + -1 + + 48a5f871ef9fd19708f5cbd900001feb + + + + + + 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 +]]> + {896C1A93-B3F8-4465-B5C3-29DB91256B49} + + 0 + false + 00000000000000000000000000000000 + Tracker + -1 + + 48a5f871ef9fd19708f5cbd900001fec + + + + + + + + {4CA91A45-55E9-46E4-AA68-EF7FC4F1D405} + + 0 + false + 00000000000000000000000000000000 + ZonePlusReference + -1 + + 48a5f871ef9fd19708f5cbd900001fed + + + + + + + false + + 0 + false + 00000000000000000000000000000000 + ServerScriptService + -1 + + 48a5f871ef9fd19708f5cbd900000378 + + + + + + 0 + false + 00000000000000000000000000000000 + ServerStorage + -1 + + 48a5f871ef9fd19708f5cbd900000379 + + + + + AAAAAA== + AAAAAA== + + 0 + false + 00000000000000000000000000000000 + ServiceVisibilityService + -1 + + 48a5f871ef9fd19708f5cbd90000037c + + + + + false + + 0 + false + 00000000000000000000000000000000 + HttpService + -1 + + 48a5f871ef9fd19708f5cbd900000398 + + + + + true + false + + 0 + false + 00000000000000000000000000000000 + DataStoreService + -1 + + 48a5f871ef9fd19708f5cbd90000039b + + + + + + 0.274509817 + 0.274509817 + 0.274509817 + + 3 + + 0 + 0 + 0 + + + 0 + 0 + 0 + + 1 + 1 + 0 + 0 + + 0.752941251 + 0.752941251 + 0.752941251 + + 100000 + 0 + 0 + true + 1 + + 0.274509817 + 0.274509817 + 0.274509817 + + false + true + 0.200000003 + 3 + 14:30:00 + + 0 + false + 00000000000000000000000000000000 + Lighting + -1 + + 48a5f871ef9fd19708f5cbd90000039c + + + + true + 11 + rbxassetid://6444320592 + rbxassetid://6444884337 + rbxassetid://6444884785 + rbxassetid://6444884337 + rbxassetid://6444884337 + + 0 + 0 + 0 + + rbxassetid://6444884337 + rbxassetid://6412503613 + 3000 + 11 + rbxassetid://6196665106 + + 0 + false + 00000000000000000000000000000000 + Sky + 332039975 + + 48a5f871ef9fd19708f5cbd9000003ae + + + + + 0.00999999978 + 0.100000001 + true + + 0 + false + 00000000000000000000000000000000 + SunRays + -1 + + 48a5f871ef9fd19708f5cbd9000003af + + + + + + 0.78039217 + 0.78039217 + 0.78039217 + + + 0.41568628 + 0.43921569 + 0.490196079 + + 0.300000012 + 0 + 0 + 0.25 + + 0 + false + 00000000000000000000000000000000 + Atmosphere + -1 + + 48a5f871ef9fd19708f5cbd9000003b0 + + + + + 1 + 24 + 2 + true + + 0 + false + 00000000000000000000000000000000 + Bloom + -1 + + 48a5f871ef9fd19708f5cbd9000003b1 + + + + + 0.100000001 + 0.0500000007 + 30 + 0.75 + false + + 0 + false + 00000000000000000000000000000000 + DepthOfField + -1 + + 48a5f871ef9fd19708f5cbd9000003b2 + + + + + + + 0 + false + 00000000000000000000000000000000 + Instance + -1 + + 48a5f871ef9fd19708f5cbd90000039d + + + + + true + 16 + 16 + + 0 + false + 00000000000000000000000000000000 + ProximityPromptService + -1 + + 48a5f871ef9fd19708f5cbd90000039e + + + + + + 0 + false + 00000000000000000000000000000000 + Teams + -1 + + 48a5f871ef9fd19708f5cbd90000039f + + + + + true + + false + true + true + 0 + 0 + true + 10 + + 0 + false + 00000000000000000000000000000000 + TestService + -1 + + 48a5f871ef9fd19708f5cbd9000003a0 + + + + + + 0 + false + 00000000000000000000000000000000 + UGCAvatarService + -1 + + 48a5f871ef9fd19708f5cbd9000003a1 + + + + + + 0 + false + 00000000000000000000000000000000 + VirtualInputManager + -1 + + 48a5f871ef9fd19708f5cbd9000003a2 + + + + + 0 + true + 2 + + 0 + false + 00000000000000000000000000000000 + VoiceChatService + -1 + + 48a5f871ef9fd19708f5cbd9000003a3 + + + + + + 0 + false + 00000000000000000000000000000000 + VideoService + -1 + + 48a5f871ef9fd19708f5cbd9000005d0 + + + + + + 0 + false + 00000000000000000000000000000000 + SerializationService + -1 + + 48a5f871ef9fd19708f5cbd900001a99 + + + + + + \ 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..8690a91 --- /dev/null +++ b/src/Testers/ZonePlusTest.client.lua @@ -0,0 +1,372 @@ +-- 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") +local destroyTestContainer = testFolder:WaitForChild("DestroyTestZone") + +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) + +-- 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) }, + 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("• 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") +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 +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..287c023 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,12 +30,10 @@ trackers.player = Tracker.new("player") trackers.item = Tracker.new("item") 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 @@ -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 = {} @@ -66,8 +66,9 @@ local heartbeatActions = { return zonesAndOccupants end local touchingZones = ZoneController.getTouchingZones(character, true, recommendedDetection, trackers.player) - for _, zone in pairs(touchingZones) do - if zone.activeTriggers["localPlayer"] then + for _, zone in touchingZones do + -- Safety check: ensure zone still has activeTriggers + if zone.activeTriggers and zone.activeTriggers["localPlayer"] then fillOccupants(zonesAndOccupants, zone, localPlayer) end end @@ -78,17 +79,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) @@ -99,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 @@ -106,9 +112,9 @@ 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 + if registeredZone.touchedConnectionActions and registeredZone.touchedConnectionActions[registeredTriggerType] then registeredZone:_formTouchedConnection(registeredTriggerType) end if heartbeatActions[registeredTriggerType] then @@ -123,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 @@ -139,7 +145,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 @@ -152,16 +160,19 @@ function ZoneController._formHeartbeat(registeredTriggerType) if clockTime >= 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 @@ -173,66 +184,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 + 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 + 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 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 + local indexToSignalType = { "Exited", "Entered" } + for index, zoneAndOccupants in collectiveSignalsToFire do local signalType = indexToSignalType[index] - local signalName = registeredTriggerType..signalType - for zone, occupants in pairs(zoneAndOccupants) do + local signalName = registeredTriggerType .. signalType + 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 @@ -258,12 +274,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 @@ -274,27 +296,39 @@ 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 -function ZoneController._getZonesAndItems(trackerName, zonesDictToCheck, zoneCustomVolume, onlyActiveZones, recommendedDetection) - local totalZoneVolume = zoneCustomVolume - if not totalZoneVolume then - for zone, _ in pairs(zonesDictToCheck) do - totalZoneVolume += zone.volume +function ZoneController._getZonesAndItems( + trackerName, + zonesDictToCheck, + zoneCustomVolume, + onlyActiveZones, + recommendedDetection +) + 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 = {} @@ -304,10 +338,11 @@ function ZoneController._getZonesAndItems(trackerName, zonesDictToCheck, zoneCus -- 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) @@ -322,23 +357,29 @@ function ZoneController._getZonesAndItems(trackerName, zonesDictToCheck, zoneCus -- 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 - local result = CollectiveWorldModel:GetPartBoundsInBox(zone.region.CFrame, zone.region.Size, tracker.whitelistParams) + -- 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 @@ -348,12 +389,10 @@ function ZoneController._getZonesAndItems(trackerName, zonesDictToCheck, zoneCus return zonesAndOccupants 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 @@ -364,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 @@ -396,7 +435,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 +454,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 @@ -424,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 @@ -440,20 +481,19 @@ 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 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 @@ -472,11 +512,14 @@ function ZoneController.getTouchingZones(item, onlyActiveZones, recommendedDetec end end end - + 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) @@ -494,18 +537,16 @@ 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 + for k, v in properties do group[k] = v end end @@ -529,6 +570,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