Repository navigation
Expand file tree
/
Copy pathscript.lua
More file actions
269 lines (197 loc) · 5.71 KB
/
Copy pathscript.lua
File metadata and controls
269 lines (197 loc) · 5.71 KB
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
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
--Services
local Players = game:GetService("Players")
local PathfindingService = game:GetService("PathfindingService")
local RunService = game:GetService("RunService")
-- Class
local NPC = {}
NPC.__index = NPC
-- Constructor
function NPC.new(model)
local self = setmetatable({}, NPC)
self.model = model
self.root = model:WaitForChild("HumanoidRootPart")
self.humanoid = model:WaitForChild("Humanoid")
-- Config
self.maxDistance = 150
self.attackRange = 6
self.viewAngle = 0.5
self.memoryTime = 4
self.attackCooldown = 1.2
self.windupTime = 0.25
-- State
self.state = "Idle"
self.target = nil
self.lastSeen = 0
self.lastAttack = 0
-- Movement
self.currentPath = nil
self.waypoints = {}
self.waypointIndex = 1
self.moving = false
return self
end
--VALIDATION
-- Ensures a target is alive and usable
function NPC:isValidTarget(target)
if not target then return false end
local hum = target:FindFirstChild("Humanoid")
if not hum or hum.Health <= 0 then return false end
local hrp = target:FindFirstChild("HumanoidRootPart")
if not hrp then return false end
return true
end
TARGETING
-- Finds closest player efficiently
function NPC:getClosestPlayer()
local closest = nil
local shortest = math.huge
for _, player in ipairs(Players:GetPlayers()) do
local char = player.Character
if not self:isValidTarget(char) then continue end
local hrp = char.HumanoidRootPart
local dist = (hrp.Position - self.root.Position).Magnitude
if dist < shortest then
shortest = dist
closest = char
end
end
return closest, shortest
end
-- Checks if target is within field of view using dot product
function NPC:canSee(target)
if not self:isValidTarget(target) then return false end
local dir = (target.HumanoidRootPart.Position - self.root.Position).Unit
local dot = self.root.CFrame.LookVector:Dot(dir)
return dot > self.viewAngle
end
-- Predicts future position based on velocity
function NPC:getPredictedPosition(target)
local hrp = target.HumanoidRootPart
return hrp.Position + (hrp.Velocity * 0.4)
end
MOVEMENT
-- Computes a path to predicted position
function NPC:computePath(target)
local path = PathfindingService:CreatePath({
AgentRadius = 2,
AgentHeight = 5,
AgentCanJump = true
})
local predicted = self:getPredictedPosition(target)
path:ComputeAsync(self.root.Position, predicted)
if path.Status ~= Enum.PathStatus.Success then return nil end
return path
end
-- Starts following a path without blocking main thread
function NPC:followPath(path)
self.currentPath = path
self.waypoints = path:GetWaypoints()
self.waypointIndex = 1
self.moving = true
task.spawn(function()
while self.moving do
local waypoint = self.waypoints[self.waypointIndex]
if not waypoint then break end
self.humanoid:MoveTo(waypoint.Position)
local reached = self.humanoid.MoveToFinished:Wait()
if not reached then break end
self.waypointIndex += 1
end
self.moving = false
end)
end
-- Stops movement safely
function NPC:stopMoving()
self.moving = false
self.currentPath = nil
self.waypoints = {}
end
--COMBAT
-- Handles attacking with cooldown and windup
function NPC:attack(target)
if tick() - self.lastAttack < self.attackCooldown then return end
if not self:isValidTarget(target) then return end
self.lastAttack = tick()
task.delay(self.windupTime, function()
if not self:isValidTarget(target) then return end
local dist = (target.HumanoidRootPart.Position - self.root.Position).Magnitude
if dist > self.attackRange then return end
target.Humanoid:TakeDamage(20)
end)
end
-- STATE LOGIC
function NPC:idle()
local target, dist = self:getClosestPlayer()
if not target then return end
if dist > self.maxDistance then return end
if self:canSee(target) then
self.target = target
self.lastSeen = tick()
self.state = "Chase"
end
end
function NPC:chase()
if not self:isValidTarget(self.target) then
self.target = nil
self.state = "Idle"
self:stopMoving()
return
end
local hrp = self.target.HumanoidRootPart
local dist = (hrp.Position - self.root.Position).Magnitude
if self:canSee(self.target) then
self.lastSeen = tick()
end
if tick() - self.lastSeen > self.memoryTime then
self.target = nil
self.state = "Idle"
self:stopMoving()
return
end
if dist <= self.attackRange then
self.state = "Attack"
self:stopMoving()
return
end
if not self.moving then
local path = self:computePath(self.target)
if path then
self:followPath(path)
end
end
end
function NPC:attackState()
if not self:isValidTarget(self.target) then
self.state = "Idle"
return
end
local dist = (self.target.HumanoidRootPart.Position - self.root.Position).Magnitude
if dist > self.attackRange then
self.state = "Chase"
return
end
self:attack(self.target)
end
--UPDATE LOOP
function NPC:update()
if self.state == "Idle" then
self:idle()
return
end
if self.state == "Chase" then
self:chase()
return
end
if self.state == "Attack" then
self:attackState()
return
end
end
-- Start system using Heartbeat
function NPC:start()
RunService.Heartbeat:Connect(function()
if not self.model.Parent then return end
self:update()
end)
end
return NPC