-- src/ServerScriptService/TreeManager.server.lua local Players = game:GetService("Players") local ReplicatedStorage = game:GetService("ReplicatedStorage") local CollectionService = game:GetService("CollectionService") local ChoppingConfig = require(ReplicatedStorage:WaitForChild("Shared"):WaitForChild("ChoppingConfig")) local CraftingConfig = require(ReplicatedStorage:WaitForChild("Shared"):WaitForChild("CraftingConfig")) local ChopEvent = ReplicatedStorage:WaitForChild("Events"):WaitForChild("ChopEvent") local NotificationEvent = ReplicatedStorage:WaitForChild("Events"):WaitForChild("NotificationEvent") local SoundEvent = ReplicatedStorage:WaitForChild("Events"):WaitForChild("SoundEvent") local function getEquippedAxeOrFist(player) local axeType = player:GetAttribute("EquippedAxe") -- No axe equipped or "None" = use fists if not axeType or axeType == "" or axeType == "None" then return CraftingConfig.Fist, "Fist" end local axeStats = ChoppingConfig.AxeTypes[axeType] if axeStats then return axeStats, axeType end -- Fallback to fists return CraftingConfig.Fist, "Fist" end local function createChopParticles(position, color) local attachment = Instance.new("Attachment") attachment.WorldPosition = position attachment.Parent = workspace.Terrain local emitter = Instance.new("ParticleEmitter") emitter.Color = ColorSequence.new(color or Color3.fromRGB(180, 140, 100)) emitter.Size = NumberSequence.new({ NumberSequenceKeypoint.new(0, 0.5), NumberSequenceKeypoint.new(1, 0), }) emitter.Lifetime = NumberRange.new(0.3, 0.8) emitter.Rate = 0 emitter.Speed = NumberRange.new(5, 15) emitter.SpreadAngle = Vector2.new(180, 180) emitter.Parent = attachment emitter:Emit(12) task.delay(1, function() attachment:Destroy() end) end -- Drop a collectible wood resource near the tree local function dropWoodResource(position, treeType, player) if math.random() > CraftingConfig.WoodDropChance then return end local amount = math.random(CraftingConfig.WoodDropAmount[1], CraftingConfig.WoodDropAmount[2]) local drop = Instance.new("Part") drop.Name = "WoodDrop" drop.Size = Vector3.new(1, 1, 1) drop.Position = position + Vector3.new(math.random(-3, 3), 2, math.random(-3, 3)) drop.Shape = Enum.PartType.Block drop.Material = Enum.Material.Wood drop.Color = CraftingConfig.Resources.Wood.Color drop.Anchored = false drop.CanCollide = true drop.CustomPhysicalProperties = PhysicalProperties.new(0.5, 0.3, 0.5) drop:SetAttribute("ResourceType", "Wood") drop:SetAttribute("Amount", amount) drop:SetAttribute("OwnerId", player.UserId) CollectionService:AddTag(drop, "ResourceDrop") drop.Parent = workspace -- Billboard showing amount local bb = Instance.new("BillboardGui") bb.Size = UDim2.new(0, 40, 0, 20) bb.StudsOffset = Vector3.new(0, 1.5, 0) bb.AlwaysOnTop = true bb.Parent = drop local label = Instance.new("TextLabel") label.Size = UDim2.new(1, 0, 1, 0) label.BackgroundTransparency = 1 label.Text = "+" .. tostring(amount) .. " Wood" label.TextColor3 = Color3.fromRGB(200, 160, 80) label.TextScaled = true label.Font = Enum.Font.GothamBold label.TextStrokeTransparency = 0.3 label.Parent = bb -- Auto-collect when player touches it drop.Touched:Connect(function(hit) local touchedPlayer = Players:GetPlayerFromCharacter(hit.Parent) if touchedPlayer and touchedPlayer.UserId == player.UserId then -- Add resources to player data local data = _G.GetPlayerData(touchedPlayer) if data then if not data.Resources then data.Resources = {} end data.Resources.Wood = (data.Resources.Wood or 0) + amount NotificationEvent:FireClient(touchedPlayer, "Resource", "+" .. tostring(amount) .. " Wood (Total: " .. tostring(data.Resources.Wood) .. ")") end drop:Destroy() end end) -- Auto-despawn after 30 seconds task.delay(30, function() if drop.Parent then drop:Destroy() end end) end local function applyDamage(player, segment, hitPos, damage, weaponType) if not CollectionService:HasTag(segment, "TreeSegment") then return end local treeType = segment:GetAttribute("TreeType") if not treeType or not ChoppingConfig.TreeTypes[treeType] then return end local maxHealth = ChoppingConfig.TreeTypes[treeType].HealthPerSegment local health = segment:GetAttribute("Health") if not health then health = maxHealth end health = health - damage segment:SetAttribute("Health", health) -- Visual feedback: chop particles createChopParticles(hitPos, ChoppingConfig.TreeTypes[treeType].LogColor) -- Sound feedback (different for fist vs axe) if weaponType == "Fist" then SoundEvent:FireAllClients("AxeSwing", hitPos) -- lighter sound else SoundEvent:FireAllClients("AxeHitWood", hitPos) end -- Drop wood resources on each hit dropWoodResource(hitPos, treeType, player) if health <= 0 then local segmentSizeY = segment.Size.Y -- Track stat if _G.IncrementStat then _G.IncrementStat(player, "WoodChopped", 1) end -- Progress quests if _G.ProgressQuest then _G.ProgressQuest(player, "Chop", 1) end -- Check achievements if _G.CheckAchievements then task.defer(function() _G.CheckAchievements(player) end) end -- Set ownership on the log pieces segment:SetAttribute("OwnerId", player.UserId) if segmentSizeY < ChoppingConfig.MinSegmentSizeY then -- Drop bonus resources when segment destroyed dropWoodResource(segment.Position, treeType, player) segment:Destroy() return end local localHitPos = segment.CFrame:ToObjectSpace(CFrame.new(hitPos)).Position local yPos = localHitPos.Y local totalHeight = segment.Size.Y local bottomHeight = (totalHeight / 2) + yPos local topHeight = totalHeight - bottomHeight if bottomHeight < 0.2 or topHeight < 0.2 then return end local bottomPiece = segment:Clone() local topPiece = segment:Clone() bottomPiece.Parent = segment.Parent topPiece.Parent = segment.Parent bottomPiece.Size = Vector3.new(segment.Size.X, bottomHeight, segment.Size.Z) topPiece.Size = Vector3.new(segment.Size.X, topHeight, segment.Size.Z) local offsetBottom = CFrame.new(0, -topHeight / 2, 0) local offsetTop = CFrame.new(0, bottomHeight / 2, 0) bottomPiece.CFrame = segment.CFrame * offsetBottom topPiece.CFrame = segment.CFrame * offsetTop bottomPiece:SetAttribute("Health", maxHealth) topPiece:SetAttribute("Health", maxHealth) bottomPiece:SetAttribute("OwnerId", player.UserId) topPiece:SetAttribute("OwnerId", player.UserId) bottomPiece.Anchored = false topPiece.Anchored = false bottomPiece.CustomPhysicalProperties = PhysicalProperties.new(ChoppingConfig.TreeTypes[treeType].Density, 0.3, 0.5) topPiece.CustomPhysicalProperties = PhysicalProperties.new(ChoppingConfig.TreeTypes[treeType].Density, 0.3, 0.5) -- Add health bars to split pieces if _G.AddHealthBar then _G.AddHealthBar(bottomPiece) _G.AddHealthBar(topPiece) end -- Sound: tree fall SoundEvent:FireAllClients("TreeFall", segment.Position) segment:Destroy() end end local function onChop(player, hitPart, hitPos) -- Anti-cheat: rate limit if _G.IsRateLimited and _G.IsRateLimited(player, "Chop") then return end local char = player.Character if not char or not char:FindFirstChild("Head") then return end local axeStats, weaponType = getEquippedAxeOrFist(player) local distance = (char.Head.Position - hitPos).Magnitude if distance > axeStats.Range + 2 then return end applyDamage(player, hitPart, hitPos, axeStats.Damage, weaponType) end ChopEvent.OnServerEvent:Connect(onChop)