@el4cteo/rbx-studio-mcp 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +203 -0
- package/dist/bridge/rpc.js +243 -0
- package/dist/bridge/rpc.js.map +1 -0
- package/dist/bridge/server.js +281 -0
- package/dist/bridge/server.js.map +1 -0
- package/dist/index.js +104 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/apidump.js +269 -0
- package/dist/lib/apidump.js.map +1 -0
- package/dist/lib/errors.js +38 -0
- package/dist/lib/errors.js.map +1 -0
- package/dist/lib/format.js +191 -0
- package/dist/lib/format.js.map +1 -0
- package/dist/lib/pluginbuild.js +83 -0
- package/dist/lib/pluginbuild.js.map +1 -0
- package/dist/lib/png.js +84 -0
- package/dist/lib/png.js.map +1 -0
- package/dist/lib/protocol.js +22 -0
- package/dist/lib/protocol.js.map +1 -0
- package/dist/lib/tool.js +27 -0
- package/dist/lib/tool.js.map +1 -0
- package/dist/resources.js +70 -0
- package/dist/resources.js.map +1 -0
- package/dist/tools/api.js +78 -0
- package/dist/tools/api.js.map +1 -0
- package/dist/tools/character.js +94 -0
- package/dist/tools/character.js.map +1 -0
- package/dist/tools/debug.js +211 -0
- package/dist/tools/debug.js.map +1 -0
- package/dist/tools/device.js +74 -0
- package/dist/tools/device.js.map +1 -0
- package/dist/tools/discover.js +217 -0
- package/dist/tools/discover.js.map +1 -0
- package/dist/tools/exec.js +191 -0
- package/dist/tools/exec.js.map +1 -0
- package/dist/tools/input.js +96 -0
- package/dist/tools/input.js.map +1 -0
- package/dist/tools/instances.js +261 -0
- package/dist/tools/instances.js.map +1 -0
- package/dist/tools/perf.js +367 -0
- package/dist/tools/perf.js.map +1 -0
- package/dist/tools/playtest.js +153 -0
- package/dist/tools/playtest.js.map +1 -0
- package/dist/tools/screenshot.js +75 -0
- package/dist/tools/screenshot.js.map +1 -0
- package/dist/tools/scripts.js +316 -0
- package/dist/tools/scripts.js.map +1 -0
- package/dist/tools/session.js +152 -0
- package/dist/tools/session.js.map +1 -0
- package/dist/tools/world.js +281 -0
- package/dist/tools/world.js.map +1 -0
- package/package.json +62 -0
- package/plugin/default.project.json +6 -0
- package/plugin/src/Config.luau +59 -0
- package/plugin/src/Console.luau +657 -0
- package/plugin/src/Context.luau +35 -0
- package/plugin/src/Dispatch.luau +90 -0
- package/plugin/src/Editor.luau +142 -0
- package/plugin/src/Emulation.luau +151 -0
- package/plugin/src/LogBuffer.luau +277 -0
- package/plugin/src/Net.luau +102 -0
- package/plugin/src/Paths.luau +255 -0
- package/plugin/src/Phrase.luau +465 -0
- package/plugin/src/Png.luau +238 -0
- package/plugin/src/Scope.luau +78 -0
- package/plugin/src/ScriptEdit.luau +100 -0
- package/plugin/src/Serialize.luau +287 -0
- package/plugin/src/TextEdit.luau +296 -0
- package/plugin/src/Transport.luau +328 -0
- package/plugin/src/Undo.luau +72 -0
- package/plugin/src/Visuals.luau +710 -0
- package/plugin/src/handlers/Api.luau +242 -0
- package/plugin/src/handlers/Assets.luau +145 -0
- package/plugin/src/handlers/Capture.luau +187 -0
- package/plugin/src/handlers/Character.luau +361 -0
- package/plugin/src/handlers/Debug.luau +391 -0
- package/plugin/src/handlers/Device.luau +119 -0
- package/plugin/src/handlers/Discover.luau +289 -0
- package/plugin/src/handlers/Exec.luau +270 -0
- package/plugin/src/handlers/Geometry.luau +261 -0
- package/plugin/src/handlers/Input.luau +287 -0
- package/plugin/src/handlers/Instances.luau +389 -0
- package/plugin/src/handlers/Perf.luau +645 -0
- package/plugin/src/handlers/Playtest.luau +205 -0
- package/plugin/src/handlers/Scripts.luau +387 -0
- package/plugin/src/handlers/Session.luau +168 -0
- package/plugin/src/handlers/Viewport.luau +302 -0
- package/plugin/src/handlers/World.luau +176 -0
- package/plugin/src/init.server.luau +317 -0
- package/scripts/build-plugin.mjs +157 -0
- package/scripts/check-plugin.mjs +97 -0
- package/scripts/install-plugin.mjs +39 -0
- package/scripts/latency.mjs +201 -0
- package/scripts/locate-luau.mjs +51 -0
- package/scripts/sourcemap.mjs +58 -0
- package/scripts/test-plugin.mjs +82 -0
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
--!strict
|
|
2
|
+
--[[
|
|
3
|
+
Driving the player character during a playtest.
|
|
4
|
+
|
|
5
|
+
This exists because the obvious approach is closed. `VirtualInputManager`
|
|
6
|
+
wants the RobloxScript capability and `VirtualUser` wants LocalUser, and both
|
|
7
|
+
refuse a plugin outright -- measured, not assumed:
|
|
8
|
+
|
|
9
|
+
The current thread cannot call 'SendKeyEvent' (lacking capability RobloxScript)
|
|
10
|
+
The current thread cannot call 'SetKeyDown' (lacking capability LocalUser)
|
|
11
|
+
|
|
12
|
+
Roblox's own MCP server simulates input because it is first-party and runs
|
|
13
|
+
with privileges a plugin is never granted. So this drives the Humanoid
|
|
14
|
+
directly instead.
|
|
15
|
+
|
|
16
|
+
That turns out to be the better instrument anyway. Synthetic keystrokes test
|
|
17
|
+
the input stack; what anyone actually wants to know is whether the character
|
|
18
|
+
can reach the door, whether the trap fires, whether the checkpoint saves --
|
|
19
|
+
all of which are answered more precisely by asking the Humanoid to walk there
|
|
20
|
+
and reporting whether it arrived.
|
|
21
|
+
|
|
22
|
+
Pathfinding is what makes "go there" mean something. `Humanoid:MoveTo` walks
|
|
23
|
+
in a straight line and stops dead against the first wall; a computed path
|
|
24
|
+
routes around geometry, which is the difference between testing a route and
|
|
25
|
+
testing a collision.
|
|
26
|
+
|
|
27
|
+
Everything here needs a running playtest, and the character only exists in
|
|
28
|
+
the playtest's data model, so these must be addressed to that session.
|
|
29
|
+
]]
|
|
30
|
+
|
|
31
|
+
local PathfindingService = game:GetService("PathfindingService")
|
|
32
|
+
local Players = game:GetService("Players")
|
|
33
|
+
local RunService = game:GetService("RunService")
|
|
34
|
+
|
|
35
|
+
local Dispatch = require(script.Parent.Parent.Dispatch)
|
|
36
|
+
local Paths = require(script.Parent.Parent.Paths)
|
|
37
|
+
|
|
38
|
+
local Character = {}
|
|
39
|
+
|
|
40
|
+
-- How long to wait for a walk before calling it stuck. Long enough to cross a
|
|
41
|
+
-- large room, short enough that a failed route does not hold the request open.
|
|
42
|
+
local MOVE_TIMEOUT = 30
|
|
43
|
+
|
|
44
|
+
-- How close counts as arrived. `MoveToFinished` fires on its own timeout as
|
|
45
|
+
-- well as on arrival, so distance is what actually decides success.
|
|
46
|
+
local ARRIVAL_RADIUS = 6
|
|
47
|
+
|
|
48
|
+
local function parseVector(value: any, what: string): Vector3
|
|
49
|
+
if typeof(value) ~= "string" then
|
|
50
|
+
Dispatch.fail("BAD_PARAMS", string.format("%s must be a string like \"10, 5, 0\".", what))
|
|
51
|
+
end
|
|
52
|
+
local x, y, z = string.match(value, "^%s*(-?[%d%.]+)%s*,%s*(-?[%d%.]+)%s*,%s*(-?[%d%.]+)%s*$")
|
|
53
|
+
if x == nil then
|
|
54
|
+
Dispatch.fail("BAD_PARAMS", string.format("%s must look like \"10, 5, 0\", got %q.", what, value))
|
|
55
|
+
end
|
|
56
|
+
return Vector3.new(tonumber(x) :: number, tonumber(y) :: number, tonumber(z) :: number)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
--[[
|
|
60
|
+
The character to drive, and the Humanoid inside it.
|
|
61
|
+
|
|
62
|
+
Named lookup comes first so a multiplayer test can address one player; with
|
|
63
|
+
one player the name is unnecessary and omitting it is the normal case.
|
|
64
|
+
]]
|
|
65
|
+
local function humanoidOf(params: { [string]: any }): (Model, Humanoid, Player)
|
|
66
|
+
if RunService:IsEdit() then
|
|
67
|
+
Dispatch.fail(
|
|
68
|
+
"NOT_RUNNING",
|
|
69
|
+
"There is no character in an edit session.",
|
|
70
|
+
"Start a playtest with `playtest op=play`, then address this to the playtest's studioId."
|
|
71
|
+
)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
local player: Player? = nil
|
|
75
|
+
if typeof(params.player) == "string" and params.player ~= "" then
|
|
76
|
+
player = Players:FindFirstChild(params.player) :: Player?
|
|
77
|
+
if player == nil then
|
|
78
|
+
Dispatch.fail("NO_PLAYER", string.format("No player named %q is in this test.", params.player))
|
|
79
|
+
end
|
|
80
|
+
else
|
|
81
|
+
player = Players:GetPlayers()[1]
|
|
82
|
+
if player == nil then
|
|
83
|
+
Dispatch.fail(
|
|
84
|
+
"NO_PLAYER",
|
|
85
|
+
"No players are in this test.",
|
|
86
|
+
"Run mode has no player at all -- use `playtest op=play` for a character."
|
|
87
|
+
)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
local model = (player :: Player).Character
|
|
92
|
+
if model == nil then
|
|
93
|
+
Dispatch.fail("NO_CHARACTER", "The player has no character right now (probably respawning).")
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
local humanoid = (model :: Model):FindFirstChildOfClass("Humanoid")
|
|
97
|
+
if humanoid == nil then
|
|
98
|
+
Dispatch.fail("NO_HUMANOID", "The character has no Humanoid.")
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
return model :: Model, humanoid :: Humanoid, player :: Player
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
local function positionOf(model: Model): Vector3
|
|
105
|
+
local root = model:FindFirstChild("HumanoidRootPart")
|
|
106
|
+
if root and root:IsA("BasePart") then
|
|
107
|
+
return root.Position
|
|
108
|
+
end
|
|
109
|
+
return model:GetPivot().Position
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
--[[
|
|
113
|
+
Takes physics authority over the character so the server can actually move it.
|
|
114
|
+
|
|
115
|
+
Without this the character does not move at all, and says nothing about why.
|
|
116
|
+
A player's character is network-owned by their client, so a `MoveTo` issued
|
|
117
|
+
from the server is overridden by the client's own controller on the next
|
|
118
|
+
frame -- the humanoid accepts the request, `MoveToFinished` fires, waypoints
|
|
119
|
+
are reported reached, and the character has not travelled a single stud.
|
|
120
|
+
Measured: identical start and end positions across a three-waypoint path that
|
|
121
|
+
reported Success.
|
|
122
|
+
|
|
123
|
+
`SetNetworkOwner(nil)` hands authority to the server, after which the same
|
|
124
|
+
call moved it 7.11 studs. This is what Studio's own "server-side character
|
|
125
|
+
control" does, and it is the difference between driving the character and
|
|
126
|
+
politely asking the client to.
|
|
127
|
+
]]
|
|
128
|
+
local function takeControl(model: Model): boolean
|
|
129
|
+
local root = model:FindFirstChild("HumanoidRootPart")
|
|
130
|
+
if root == nil or not root:IsA("BasePart") then
|
|
131
|
+
return false
|
|
132
|
+
end
|
|
133
|
+
-- Guarded: an anchored root, or a session where the part is not
|
|
134
|
+
-- network-owned at all, throws rather than returning false.
|
|
135
|
+
local ok = pcall(function()
|
|
136
|
+
(root :: BasePart):SetNetworkOwner(nil)
|
|
137
|
+
end)
|
|
138
|
+
return ok
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
--[[
|
|
142
|
+
Walks to a point, following a computed path around obstacles.
|
|
143
|
+
|
|
144
|
+
Reports where it ended up rather than only whether the call returned, because
|
|
145
|
+
`MoveTo` succeeds at being asked and says nothing about arriving. A route
|
|
146
|
+
blocked by a wall the agent did not know about looks identical to a
|
|
147
|
+
successful walk unless the final distance is measured.
|
|
148
|
+
]]
|
|
149
|
+
function Character.moveTo(params: { [string]: any }): { [string]: any }
|
|
150
|
+
local model, humanoid = humanoidOf(params)
|
|
151
|
+
|
|
152
|
+
local goal: Vector3
|
|
153
|
+
if typeof(params.path) == "string" and params.path ~= "" then
|
|
154
|
+
local target = Paths.resolve(params.path)
|
|
155
|
+
if target:IsA("BasePart") then
|
|
156
|
+
goal = (target :: BasePart).Position
|
|
157
|
+
elseif target:IsA("Model") then
|
|
158
|
+
goal = (target :: Model):GetPivot().Position
|
|
159
|
+
else
|
|
160
|
+
Dispatch.fail("NOT_POSITIONED", string.format("%s has no position.", params.path))
|
|
161
|
+
end
|
|
162
|
+
else
|
|
163
|
+
goal = parseVector(params.to, "`to`")
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
local start = positionOf(model)
|
|
167
|
+
local controlled = takeControl(model)
|
|
168
|
+
|
|
169
|
+
--[[
|
|
170
|
+
Straight-line movement is available on request, because pathfinding
|
|
171
|
+
deliberately refuses routes it considers unreachable -- and "walk at it
|
|
172
|
+
anyway and tell me what happens" is a legitimate thing to test.
|
|
173
|
+
]]
|
|
174
|
+
local waypoints: { Vector3 } = {}
|
|
175
|
+
local pathStatus = "direct"
|
|
176
|
+
if params.direct ~= true then
|
|
177
|
+
local path = PathfindingService:CreatePath({
|
|
178
|
+
AgentRadius = 2,
|
|
179
|
+
AgentHeight = 5,
|
|
180
|
+
AgentCanJump = params.canJump ~= false,
|
|
181
|
+
})
|
|
182
|
+
local ok, err = pcall(function()
|
|
183
|
+
path:ComputeAsync(start, goal)
|
|
184
|
+
end)
|
|
185
|
+
if not ok then
|
|
186
|
+
Dispatch.fail("PATH_FAILED", string.format("Could not compute a path: %s", tostring(err)))
|
|
187
|
+
end
|
|
188
|
+
pathStatus = tostring(path.Status)
|
|
189
|
+
if path.Status == Enum.PathStatus.Success then
|
|
190
|
+
for _, waypoint in path:GetWaypoints() do
|
|
191
|
+
table.insert(waypoints, waypoint.Position)
|
|
192
|
+
end
|
|
193
|
+
else
|
|
194
|
+
return {
|
|
195
|
+
arrived = false,
|
|
196
|
+
pathStatus = pathStatus,
|
|
197
|
+
from = tostring(start),
|
|
198
|
+
goal = tostring(goal),
|
|
199
|
+
note = "No route exists. Pass direct=true to walk straight at it regardless.",
|
|
200
|
+
}
|
|
201
|
+
end
|
|
202
|
+
else
|
|
203
|
+
waypoints = { goal }
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
local deadline = os.clock() + MOVE_TIMEOUT
|
|
207
|
+
local reached = 0
|
|
208
|
+
for index, waypoint in waypoints do
|
|
209
|
+
if os.clock() > deadline then
|
|
210
|
+
break
|
|
211
|
+
end
|
|
212
|
+
humanoid:MoveTo(waypoint)
|
|
213
|
+
-- Jumping is per-waypoint: a path that crosses a gap marks the waypoint
|
|
214
|
+
-- before it as a jump, and walking it without jumping falls short.
|
|
215
|
+
if index > 1 and waypoint.Y - waypoints[index - 1].Y > 1.5 then
|
|
216
|
+
humanoid.Jump = true
|
|
217
|
+
end
|
|
218
|
+
local finished = humanoid.MoveToFinished:Wait()
|
|
219
|
+
reached += 1
|
|
220
|
+
if not finished then
|
|
221
|
+
-- MoveToFinished(false) means its own 8-second timeout elapsed, which
|
|
222
|
+
-- in practice means something is in the way.
|
|
223
|
+
break
|
|
224
|
+
end
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
local final = positionOf(model)
|
|
228
|
+
local distance = (final - goal).Magnitude
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
arrived = distance <= ARRIVAL_RADIUS,
|
|
232
|
+
serverControlled = controlled,
|
|
233
|
+
distance = math.floor(distance * 10 + 0.5) / 10,
|
|
234
|
+
waypoints = #waypoints,
|
|
235
|
+
waypointsReached = reached,
|
|
236
|
+
pathStatus = pathStatus,
|
|
237
|
+
from = tostring(start),
|
|
238
|
+
to = tostring(final),
|
|
239
|
+
goal = tostring(goal),
|
|
240
|
+
}
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
--[[
|
|
244
|
+
One-shot character actions: jump, sit, respawn, and the state changes worth
|
|
245
|
+
testing directly.
|
|
246
|
+
]]
|
|
247
|
+
function Character.act(params: { [string]: any }): { [string]: any }
|
|
248
|
+
local model, humanoid, player = humanoidOf(params)
|
|
249
|
+
local action = tostring(params.action or "jump")
|
|
250
|
+
|
|
251
|
+
-- Jumping and sitting are driven from the server too, and are overridden by
|
|
252
|
+
-- the owning client for exactly the same reason walking was.
|
|
253
|
+
takeControl(model)
|
|
254
|
+
|
|
255
|
+
if action == "jump" then
|
|
256
|
+
humanoid.Jump = true
|
|
257
|
+
return { action = action, at = tostring(positionOf(model)) }
|
|
258
|
+
elseif action == "stop" then
|
|
259
|
+
humanoid:MoveTo(positionOf(model))
|
|
260
|
+
return { action = action, at = tostring(positionOf(model)) }
|
|
261
|
+
elseif action == "sit" then
|
|
262
|
+
humanoid.Sit = true
|
|
263
|
+
return { action = action, sitting = humanoid.Sit }
|
|
264
|
+
elseif action == "stand" then
|
|
265
|
+
humanoid.Sit = false
|
|
266
|
+
return { action = action, sitting = humanoid.Sit }
|
|
267
|
+
elseif action == "respawn" then
|
|
268
|
+
player:LoadCharacter()
|
|
269
|
+
return { action = action, note = "The character was rebuilt; anything holding the old one is stale." }
|
|
270
|
+
elseif action == "kill" then
|
|
271
|
+
humanoid.Health = 0
|
|
272
|
+
return { action = action, note = "Killed, so the death and respawn path runs." }
|
|
273
|
+
elseif action == "equip" then
|
|
274
|
+
--[[
|
|
275
|
+
Tools are the other half of gameplay input, and the half that is
|
|
276
|
+
actually reachable. Keys and clicks are closed to plugins, but a
|
|
277
|
+
Tool's Activate is a plain server-side call, so "equip the sword and
|
|
278
|
+
swing it" -- which is what most combat tests come down to -- works
|
|
279
|
+
without simulating a single input event.
|
|
280
|
+
]]
|
|
281
|
+
local name = params.tool
|
|
282
|
+
if typeof(name) ~= "string" or name == "" then
|
|
283
|
+
Dispatch.fail("BAD_PARAMS", "equip needs a `tool` name.")
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
-- Backpack first, then StarterPack, because a tool the player already
|
|
287
|
+
-- holds is what a caller usually means.
|
|
288
|
+
local backpack = player:FindFirstChildOfClass("Backpack")
|
|
289
|
+
local tool = if backpack then backpack:FindFirstChild(name) else nil
|
|
290
|
+
if tool == nil then
|
|
291
|
+
local starter = game:GetService("StarterPack"):FindFirstChild(name)
|
|
292
|
+
if starter then
|
|
293
|
+
tool = starter:Clone()
|
|
294
|
+
;(tool :: Instance).Parent = backpack
|
|
295
|
+
end
|
|
296
|
+
end
|
|
297
|
+
if tool == nil or not tool:IsA("Tool") then
|
|
298
|
+
Dispatch.fail(
|
|
299
|
+
"NO_TOOL",
|
|
300
|
+
string.format("No Tool named %q in the player's Backpack or StarterPack.", name)
|
|
301
|
+
)
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
humanoid:EquipTool(tool :: Tool)
|
|
305
|
+
return { action = action, tool = name, equipped = (tool :: Tool).Parent == model }
|
|
306
|
+
elseif action == "activate" then
|
|
307
|
+
local equipped = model:FindFirstChildOfClass("Tool")
|
|
308
|
+
if equipped == nil then
|
|
309
|
+
Dispatch.fail(
|
|
310
|
+
"NO_TOOL",
|
|
311
|
+
"The character is not holding a tool.",
|
|
312
|
+
'Equip one first with action "equip".'
|
|
313
|
+
)
|
|
314
|
+
end
|
|
315
|
+
-- Activate is what a mouse click triggers, so this is the closest thing
|
|
316
|
+
-- to "use it" that exists without input injection.
|
|
317
|
+
equipped:Activate()
|
|
318
|
+
return { action = action, tool = equipped.Name }
|
|
319
|
+
elseif action == "unequip" then
|
|
320
|
+
humanoid:UnequipTools()
|
|
321
|
+
return { action = action, holding = model:FindFirstChildOfClass("Tool") ~= nil }
|
|
322
|
+
elseif action == "teleport" then
|
|
323
|
+
local target = parseVector(params.to, "`to`")
|
|
324
|
+
model:PivotTo(CFrame.new(target))
|
|
325
|
+
return { action = action, at = tostring(positionOf(model)), note = "Moved without walking -- collisions and triggers along the way did not fire." }
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
Dispatch.fail("BAD_PARAMS", string.format("unknown character action %q", action))
|
|
329
|
+
return {}
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
--[[
|
|
333
|
+
Where the character is and what state it is in. Cheap, and the thing to call
|
|
334
|
+
before and after anything else here.
|
|
335
|
+
]]
|
|
336
|
+
function Character.state(params: { [string]: any }): { [string]: any }
|
|
337
|
+
local model, humanoid, player = humanoidOf(params)
|
|
338
|
+
return {
|
|
339
|
+
player = player.Name,
|
|
340
|
+
position = tostring(positionOf(model)),
|
|
341
|
+
health = humanoid.Health,
|
|
342
|
+
maxHealth = humanoid.MaxHealth,
|
|
343
|
+
walkSpeed = humanoid.WalkSpeed,
|
|
344
|
+
jumpPower = humanoid.JumpPower,
|
|
345
|
+
state = tostring(humanoid:GetState()),
|
|
346
|
+
sitting = humanoid.Sit,
|
|
347
|
+
floor = if humanoid.FloorMaterial ~= Enum.Material.Air
|
|
348
|
+
then tostring(humanoid.FloorMaterial)
|
|
349
|
+
else nil,
|
|
350
|
+
}
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
function Character.register()
|
|
354
|
+
Dispatch.registerAll("character", {
|
|
355
|
+
moveTo = Character.moveTo,
|
|
356
|
+
act = Character.act,
|
|
357
|
+
state = Character.state,
|
|
358
|
+
})
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
return Character
|