File size: 7,792 Bytes
0712d5f | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 | -- 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)
|