-- src/ServerScriptService/TreeSpawnerManager.server.lua -- Spawns trees on flat biome baseplates at Y=0 local ReplicatedStorage = game:GetService("ReplicatedStorage") local CollectionService = game:GetService("CollectionService") local BiomeConfig = require(ReplicatedStorage:WaitForChild("Shared"):WaitForChild("BiomeConfig")) local TreeModelConfig = require(ReplicatedStorage:WaitForChild("Shared"):WaitForChild("TreeModelConfig")) local ChoppingConfig = require(ReplicatedStorage:WaitForChild("Shared"):WaitForChild("ChoppingConfig")) local treesFolder = Instance.new("Folder") treesFolder.Name = "Trees" treesFolder.Parent = workspace local treePositions = {} local function randomInRange(min, max) return min + math.random() * (max - min) end local function isPositionValid(pos, minSpacing) for _, existingPos in ipairs(treePositions) do local dx = pos.X - existingPos.X local dz = pos.Z - existingPos.Z if (dx * dx + dz * dz) < (minSpacing * minSpacing) then return false end end return true end -- Add a health bar to a tree segment local function addHealthBar(segment) local treeType = segment:GetAttribute("TreeType") local config = ChoppingConfig.TreeTypes[treeType] if not config then return end local billboard = Instance.new("BillboardGui") billboard.Name = "HealthBarGui" billboard.Size = UDim2.new(0, 60, 0, 8) billboard.StudsOffset = Vector3.new(0, 1, 0) billboard.AlwaysOnTop = false billboard.MaxDistance = 30 billboard.Parent = segment local bg = Instance.new("Frame") bg.Name = "Background" bg.Size = UDim2.new(1, 0, 1, 0) bg.BackgroundColor3 = Color3.fromRGB(30, 30, 30) bg.BorderSizePixel = 0 bg.Parent = billboard local bgCorner = Instance.new("UICorner") bgCorner.CornerRadius = UDim.new(0, 3) bgCorner.Parent = bg local fill = Instance.new("Frame") fill.Name = "Fill" fill.Size = UDim2.new(1, 0, 1, 0) fill.BackgroundColor3 = Color3.fromRGB(50, 200, 50) fill.BorderSizePixel = 0 fill.Parent = bg local fillCorner = Instance.new("UICorner") fillCorner.CornerRadius = UDim.new(0, 3) fillCorner.Parent = fill local label = Instance.new("TextLabel") label.Name = "HealthText" label.Size = UDim2.new(1, 0, 1, 0) label.BackgroundTransparency = 1 label.Text = tostring(config.HealthPerSegment) label.TextColor3 = Color3.new(1, 1, 1) label.TextScaled = true label.Font = Enum.Font.GothamBold label.TextStrokeTransparency = 0.5 label.Parent = bg segment:GetAttributeChangedSignal("Health"):Connect(function() local maxHP = config.HealthPerSegment local currentHP = segment:GetAttribute("Health") or maxHP local ratio = math.clamp(currentHP / maxHP, 0, 1) fill.Size = UDim2.new(ratio, 0, 1, 0) label.Text = tostring(math.ceil(currentHP)) if ratio > 0.5 then fill.BackgroundColor3 = Color3.fromRGB(math.floor(255 * (1 - ratio) * 2), 200, 50) else fill.BackgroundColor3 = Color3.fromRGB(255, math.floor(200 * ratio * 2), 50) end end) end -- Build a tree model at a position (Y=0 is the ground) local function createTreeModel(treeType, position) local template = TreeModelConfig.Templates[treeType] if not template then return nil end local config = ChoppingConfig.TreeTypes[treeType] if not config then return nil end local model = Instance.new("Model") model.Name = treeType .. "_Tree" local trunkDiameter = randomInRange(template.TrunkDiameter[1], template.TrunkDiameter[2]) local totalHeight = randomInRange(template.TrunkHeight[1], template.TrunkHeight[2]) local segmentCount = math.random(template.SegmentCount[1], template.SegmentCount[2]) local segmentHeight = totalHeight / segmentCount -- Ground level is 0 -- trees sit directly on the baseplate local groundY = 0 local currentY = groundY for i = 1, segmentCount do local segment = Instance.new("Part") segment.Name = "TrunkSegment_" .. i segment.Shape = Enum.PartType.Cylinder local taperMult = 1 - ((i - 1) / segmentCount) * 0.4 local diameter = trunkDiameter * taperMult segment.Size = Vector3.new(segmentHeight, diameter, diameter) segment.CFrame = CFrame.new(position.X, currentY + segmentHeight / 2, position.Z) * CFrame.Angles(0, 0, math.rad(90)) segment.Color = template.BarkColor segment.Material = template.BarkMaterial segment.Anchored = true segment.TopSurface = Enum.SurfaceType.Smooth segment.BottomSurface = Enum.SurfaceType.Smooth segment:SetAttribute("TreeType", treeType) segment:SetAttribute("Health", config.HealthPerSegment) segment.CustomPhysicalProperties = PhysicalProperties.new(config.Density, 0.3, 0.5) CollectionService:AddTag(segment, "TreeSegment") CollectionService:AddTag(segment, "Draggable") if template.GlowTrunk then local light = Instance.new("PointLight") light.Color = template.BarkColor light.Brightness = 0.5 light.Range = 8 light.Parent = segment end addHealthBar(segment) segment.Parent = model currentY = currentY + segmentHeight if i > 1 then local weld = Instance.new("WeldConstraint") weld.Part0 = model:FindFirstChild("TrunkSegment_" .. (i - 1)) weld.Part1 = segment weld.Parent = segment end end -- Build canopy if template.HasLeaves then local canopyRadius = randomInRange(template.CanopyRadius[1], template.CanopyRadius[2]) local canopyY = currentY if template.ConicalCanopy then for layer = 1, template.CanopySegments do local layerRadius = canopyRadius * (1 - (layer - 1) / template.CanopySegments) local canopy = Instance.new("Part") canopy.Name = "Canopy_" .. layer canopy.Shape = Enum.PartType.Ball canopy.Size = Vector3.new(layerRadius * 2, 2, layerRadius * 2) canopy.Position = Vector3.new(position.X, canopyY + (layer - 1) * 1.5, position.Z) canopy.Color = template.LeafColor canopy.Material = template.LeafMaterial canopy.Anchored = true canopy.CanCollide = false canopy.CastShadow = true canopy.Parent = model end else for seg = 1, template.CanopySegments do local angle = (seg / template.CanopySegments) * math.pi * 2 local offsetX = math.cos(angle) * canopyRadius * 0.4 local offsetZ = math.sin(angle) * canopyRadius * 0.4 local size = randomInRange(canopyRadius * 0.6, canopyRadius * 1.0) local canopy = Instance.new("Part") canopy.Name = "Canopy_" .. seg canopy.Shape = Enum.PartType.Ball canopy.Size = Vector3.new(size * 2, size * 1.2, size * 2) canopy.Position = Vector3.new( position.X + offsetX, canopyY + randomInRange(-1, 2), position.Z + offsetZ ) canopy.Color = template.LeafColor canopy.Material = template.LeafMaterial canopy.Anchored = true canopy.CanCollide = false canopy.CastShadow = true canopy.Parent = model end end end local primaryPart = model:FindFirstChild("TrunkSegment_1") if primaryPart then model.PrimaryPart = primaryPart end CollectionService:AddTag(model, "TreeModel") model.Parent = treesFolder return model end -- Spawn trees in a biome region local function spawnTreesInBiome(biomeName, biomeData) local region = biomeData.Region local treeTypes = biomeData.TreeTypes if #treeTypes == 0 then return end local regionWidth = region.MaxX - region.MinX local regionDepth = region.MaxZ - region.MinZ local area = regionWidth * regionDepth local numTrees = math.floor(area * biomeData.TreeDensity) numTrees = math.min(numTrees, 200) local spawned = 0 local attempts = 0 local maxAttempts = numTrees * 5 while spawned < numTrees and attempts < maxAttempts do attempts = attempts + 1 local x = region.MinX + math.random() * regionWidth local z = region.MinZ + math.random() * regionDepth if isPositionValid(Vector3.new(x, 0, z), BiomeConfig.MinTreeSpacing) then local treeType = treeTypes[math.random(1, #treeTypes)] -- All trees at Y=0 (flat baseplate surface) local treePos = Vector3.new(x, 0, z) local tree = createTreeModel(treeType, treePos) if tree then table.insert(treePositions, treePos) spawned = spawned + 1 end end end print("Spawned " .. spawned .. " trees in " .. biomeName) end -- Wait for biome generation then spawn trees task.spawn(function() while not _G.BiomeGenerationComplete do task.wait(0.5) end print("=== Tree Spawning Starting ===") for biomeName, biomeData in pairs(BiomeConfig.Biomes) do spawnTreesInBiome(biomeName, biomeData) task.wait() end print("=== Tree Spawning Complete ===") _G.TreeSpawningComplete = true end) _G.CreateTree = function(treeType, position) return createTreeModel(treeType, position) end _G.AddHealthBar = function(segment) addHealthBar(segment) end