]> git.lizzy.rs Git - dragonfireclient.git/blob - builtin/game/falling.lua
Rewrite falling entity to make use of collision info
[dragonfireclient.git] / builtin / game / falling.lua
1 -- Minetest: builtin/item.lua
2
3 local builtin_shared = ...
4 local SCALE = 0.667
5
6 local facedir_to_euler = {
7         {y = 0, x = 0, z = 0},
8         {y = -math.pi/2, x = 0, z = 0},
9         {y = math.pi, x = 0, z = 0},
10         {y = math.pi/2, x = 0, z = 0},
11         {y = math.pi/2, x = -math.pi/2, z = math.pi/2},
12         {y = math.pi/2, x = math.pi, z = math.pi/2},
13         {y = math.pi/2, x = math.pi/2, z = math.pi/2},
14         {y = math.pi/2, x = 0, z = math.pi/2},
15         {y = -math.pi/2, x = math.pi/2, z = math.pi/2},
16         {y = -math.pi/2, x = 0, z = math.pi/2},
17         {y = -math.pi/2, x = -math.pi/2, z = math.pi/2},
18         {y = -math.pi/2, x = math.pi, z = math.pi/2},
19         {y = 0, x = 0, z = math.pi/2},
20         {y = 0, x = -math.pi/2, z = math.pi/2},
21         {y = 0, x = math.pi, z = math.pi/2},
22         {y = 0, x = math.pi/2, z = math.pi/2},
23         {y = math.pi, x = math.pi, z = math.pi/2},
24         {y = math.pi, x = math.pi/2, z = math.pi/2},
25         {y = math.pi, x = 0, z = math.pi/2},
26         {y = math.pi, x = -math.pi/2, z = math.pi/2},
27         {y = math.pi, x = math.pi, z = 0},
28         {y = -math.pi/2, x = math.pi, z = 0},
29         {y = 0, x = math.pi, z = 0},
30         {y = math.pi/2, x = math.pi, z = 0}
31 }
32
33 local gravity = tonumber(core.settings:get("movement_gravity")) or 9.81
34
35 --
36 -- Falling stuff
37 --
38
39 core.register_entity(":__builtin:falling_node", {
40         initial_properties = {
41                 visual = "item",
42                 visual_size = {x = SCALE, y = SCALE, z = SCALE},
43                 textures = {},
44                 physical = true,
45                 is_visible = false,
46                 collide_with_objects = false,
47                 collisionbox = {-0.5, -0.5, -0.5, 0.5, 0.5, 0.5},
48         },
49
50         node = {},
51         meta = {},
52         floats = false,
53
54         set_node = function(self, node, meta)
55                 self.node = node
56                 meta = meta or {}
57                 if type(meta.to_table) == "function" then
58                         meta = meta:to_table()
59                 end
60                 for _, list in pairs(meta.inventory or {}) do
61                         for i, stack in pairs(list) do
62                                 if type(stack) == "userdata" then
63                                         list[i] = stack:to_string()
64                                 end
65                         end
66                 end
67                 local def = core.registered_nodes[node.name]
68                 if not def then
69                         -- Don't allow unknown nodes to fall
70                         core.log("info",
71                                 "Unknown falling node removed at "..
72                                 core.pos_to_string(self.object:get_pos()))
73                         self.object:remove()
74                         return
75                 end
76                 self.meta = meta
77
78                 -- Cache whether we're supposed to float on water
79                 self.floats = core.get_item_group(node.name, "float") ~= 0
80
81                 -- Set entity visuals
82                 if def.drawtype == "torchlike" or def.drawtype == "signlike" then
83                         local textures
84                         if def.tiles and def.tiles[1] then
85                                 local tile = def.tiles[1]
86                                 if type(tile) == "table" then
87                                         tile = tile.name
88                                 end
89                                 if def.drawtype == "torchlike" then
90                                         textures = { "("..tile..")^[transformFX", tile }
91                                 else
92                                         textures = { tile, "("..tile..")^[transformFX" }
93                                 end
94                         end
95                         local vsize
96                         if def.visual_scale then
97                                 local s = def.visual_scale
98                                 vsize = {x = s, y = s, z = s}
99                         end
100                         self.object:set_properties({
101                                 is_visible = true,
102                                 visual = "upright_sprite",
103                                 visual_size = vsize,
104                                 textures = textures,
105                                 glow = def.light_source,
106                         })
107                 elseif def.drawtype ~= "airlike" then
108                         local itemstring = node.name
109                         if core.is_colored_paramtype(def.paramtype2) then
110                                 itemstring = core.itemstring_with_palette(itemstring, node.param2)
111                         end
112                         local vsize
113                         if def.visual_scale then
114                                 local s = def.visual_scale * SCALE
115                                 vsize = {x = s, y = s, z = s}
116                         end
117                         self.object:set_properties({
118                                 is_visible = true,
119                                 wield_item = itemstring,
120                                 visual_size = vsize,
121                                 glow = def.light_source,
122                         })
123                 end
124
125                 -- Rotate entity
126                 if def.drawtype == "torchlike" then
127                         self.object:set_yaw(math.pi*0.25)
128                 elseif (node.param2 ~= 0 and (def.wield_image == ""
129                                 or def.wield_image == nil))
130                                 or def.drawtype == "signlike"
131                                 or def.drawtype == "mesh"
132                                 or def.drawtype == "normal"
133                                 or def.drawtype == "nodebox" then
134                         if (def.paramtype2 == "facedir" or def.paramtype2 == "colorfacedir") then
135                                 local fdir = node.param2 % 32
136                                 -- Get rotation from a precalculated lookup table
137                                 local euler = facedir_to_euler[fdir + 1]
138                                 if euler then
139                                         self.object:set_rotation(euler)
140                                 end
141                         elseif (def.paramtype2 == "wallmounted" or def.paramtype2 == "colorwallmounted") then
142                                 local rot = node.param2 % 8
143                                 local pitch, yaw, roll = 0, 0, 0
144                                 if rot == 1 then
145                                         pitch, yaw = math.pi, math.pi
146                                 elseif rot == 2 then
147                                         pitch, yaw = math.pi/2, math.pi/2
148                                 elseif rot == 3 then
149                                         pitch, yaw = math.pi/2, -math.pi/2
150                                 elseif rot == 4 then
151                                         pitch, yaw = math.pi/2, math.pi
152                                 elseif rot == 5 then
153                                         pitch, yaw = math.pi/2, 0
154                                 end
155                                 if def.drawtype == "signlike" then
156                                         pitch = pitch - math.pi/2
157                                         if rot == 0 then
158                                                 yaw = yaw + math.pi/2
159                                         elseif rot == 1 then
160                                                 yaw = yaw - math.pi/2
161                                         end
162                                 elseif def.drawtype == "mesh" or def.drawtype == "normal" then
163                                         if rot >= 0 and rot <= 1 then
164                                                 roll = roll + math.pi
165                                         else
166                                                 yaw = yaw + math.pi
167                                         end
168                                 end
169                                 self.object:set_rotation({x=pitch, y=yaw, z=roll})
170                         end
171                 end
172         end,
173
174         get_staticdata = function(self)
175                 local ds = {
176                         node = self.node,
177                         meta = self.meta,
178                 }
179                 return core.serialize(ds)
180         end,
181
182         on_activate = function(self, staticdata)
183                 self.object:set_armor_groups({immortal = 1})
184                 self.object:set_acceleration({x = 0, y = -gravity, z = 0})
185
186                 local ds = core.deserialize(staticdata)
187                 if ds and ds.node then
188                         self:set_node(ds.node, ds.meta)
189                 elseif ds then
190                         self:set_node(ds)
191                 elseif staticdata ~= "" then
192                         self:set_node({name = staticdata})
193                 end
194         end,
195
196         try_place = function(self, bcp, bcn)
197                 local bcd = core.registered_nodes[bcn.name]
198                 -- Add levels if dropped on same leveled node
199                 if bcd and bcd.leveled and
200                                 bcn.name == self.node.name then
201                         local addlevel = self.node.level
202                         if not addlevel or addlevel <= 0 then
203                                 addlevel = bcd.leveled
204                         end
205                         if core.add_node_level(bcp, addlevel) == 0 then
206                                 return true
207                         end
208                 end
209
210                 -- Decide if we're replacing the node or placing on top
211                 local np = vector.new(bcp)
212                 if bcd and bcd.buildable_to and
213                                 (not self.floats or bcd.liquidtype == "none") then
214                         core.remove_node(bcp)
215                 else
216                         np.y = np.y + 1
217                 end
218
219                 -- Check what's here
220                 local n2 = core.get_node(np)
221                 local nd = core.registered_nodes[n2.name]
222                 -- If it's not air or liquid, remove node and replace it with
223                 -- it's drops
224                 if n2.name ~= "air" and (not nd or nd.liquidtype == "none") then
225                         if nd and nd.buildable_to == false then
226                                 nd.on_dig(np, n2, nil)
227                                 -- If it's still there, it might be protected
228                                 if core.get_node(np).name == n2.name then
229                                         return false
230                                 end
231                         else
232                                 core.remove_node(np)
233                         end
234                 end
235
236                 -- Create node
237                 local def = core.registered_nodes[self.node.name]
238                 if def then
239                         core.add_node(np, self.node)
240                         if self.meta then
241                                 core.get_meta(np):from_table(self.meta)
242                         end
243                         if def.sounds and def.sounds.place then
244                                 core.sound_play(def.sounds.place, {pos = np}, true)
245                         end
246                 end
247                 core.check_for_falling(np)
248                 return true
249         end,
250
251         on_step = function(self, dtime, moveresult)
252                 -- Fallback code since collision detection can't tell us
253                 -- about liquids (which do not collide)
254                 if self.floats then
255                         local pos = self.object:get_pos()
256
257                         local bcp = vector.round({x = pos.x, y = pos.y - 0.7, z = pos.z})
258                         local bcn = core.get_node(bcp)
259
260                         local bcd = core.registered_nodes[bcn.name]
261                         if bcd and bcd.liquidtype ~= "none" then
262                                 if self:try_place(bcp, bcn) then
263                                         self.object:remove()
264                                         return
265                                 end
266                         end
267                 end
268
269                 assert(moveresult)
270                 if not moveresult.collides then
271                         return -- Nothing to do :)
272                 end
273
274                 local bcp, bcn
275                 if moveresult.touching_ground then
276                         for _, info in ipairs(moveresult.collisions) do
277                                 if info.axis == "y" then
278                                         bcp = info.node_pos
279                                         bcn = core.get_node(bcp)
280                                         break
281                                 end
282                         end
283                 end
284
285                 if not bcp then
286                         -- We're colliding with something, but not the ground. Irrelevant to us.
287                         return
288                 elseif bcn.name == "ignore" then
289                         -- Delete on contact with ignore at world edges
290                         self.object:remove()
291                         return
292                 end
293
294                 local failure = false
295
296                 local pos = self.object:get_pos()
297                 local distance = vector.apply(vector.subtract(pos, bcp), math.abs)
298                 if distance.x >= 1 or distance.z >= 1 then
299                         -- We're colliding with some part of a node that's sticking out
300                         -- Since we don't want to visually teleport, drop as item
301                         failure = true
302                 elseif distance.y >= 2 then
303                         -- Doors consist of a hidden top node and a bottom node that is
304                         -- the actual door. Despite the top node being solid, the moveresult
305                         -- almost always indicates collision with the bottom node.
306                         -- Compensate for this by checking the top node
307                         bcp.y = bcp.y + 1
308                         bcn = core.get_node(bcp)
309                         local def = core.registered_nodes[bcn.name]
310                         if not (def and def.walkable) then
311                                 failure = true -- This is unexpected, fail
312                         end
313                 end
314
315                 -- Try to actually place ourselves
316                 if not failure then
317                         failure = not self:try_place(bcp, bcn)
318                 end
319
320                 if failure then
321                         local drops = core.get_node_drops(self.node, "")
322                         for _, item in pairs(drops) do
323                                 core.add_item(pos, item)
324                         end
325                 end
326                 self.object:remove()
327         end
328 })
329
330 local function convert_to_falling_node(pos, node)
331         local obj = core.add_entity(pos, "__builtin:falling_node")
332         if not obj then
333                 return false
334         end
335         node.level = core.get_node_level(pos)
336         local meta = core.get_meta(pos)
337         local metatable = meta and meta:to_table() or {}
338
339         local def = core.registered_nodes[node.name]
340         if def and def.sounds and def.sounds.fall then
341                 core.sound_play(def.sounds.fall, {pos = pos}, true)
342         end
343
344         obj:get_luaentity():set_node(node, metatable)
345         core.remove_node(pos)
346         return true
347 end
348
349 function core.spawn_falling_node(pos)
350         local node = core.get_node(pos)
351         if node.name == "air" or node.name == "ignore" then
352                 return false
353         end
354         return convert_to_falling_node(pos, node)
355 end
356
357 local function drop_attached_node(p)
358         local n = core.get_node(p)
359         local drops = core.get_node_drops(n, "")
360         local def = core.registered_items[n.name]
361         if def and def.preserve_metadata then
362                 local oldmeta = core.get_meta(p):to_table().fields
363                 -- Copy pos and node because the callback can modify them.
364                 local pos_copy = {x=p.x, y=p.y, z=p.z}
365                 local node_copy = {name=n.name, param1=n.param1, param2=n.param2}
366                 local drop_stacks = {}
367                 for k, v in pairs(drops) do
368                         drop_stacks[k] = ItemStack(v)
369                 end
370                 drops = drop_stacks
371                 def.preserve_metadata(pos_copy, node_copy, oldmeta, drops)
372         end
373         if def and def.sounds and def.sounds.fall then
374                 core.sound_play(def.sounds.fall, {pos = p}, true)
375         end
376         core.remove_node(p)
377         for _, item in pairs(drops) do
378                 local pos = {
379                         x = p.x + math.random()/2 - 0.25,
380                         y = p.y + math.random()/2 - 0.25,
381                         z = p.z + math.random()/2 - 0.25,
382                 }
383                 core.add_item(pos, item)
384         end
385 end
386
387 function builtin_shared.check_attached_node(p, n)
388         local def = core.registered_nodes[n.name]
389         local d = {x = 0, y = 0, z = 0}
390         if def.paramtype2 == "wallmounted" or
391                         def.paramtype2 == "colorwallmounted" then
392                 -- The fallback vector here is in case 'wallmounted to dir' is nil due
393                 -- to voxelmanip placing a wallmounted node without resetting a
394                 -- pre-existing param2 value that is out-of-range for wallmounted.
395                 -- The fallback vector corresponds to param2 = 0.
396                 d = core.wallmounted_to_dir(n.param2) or {x = 0, y = 1, z = 0}
397         else
398                 d.y = -1
399         end
400         local p2 = vector.add(p, d)
401         local nn = core.get_node(p2).name
402         local def2 = core.registered_nodes[nn]
403         if def2 and not def2.walkable then
404                 return false
405         end
406         return true
407 end
408
409 --
410 -- Some common functions
411 --
412
413 function core.check_single_for_falling(p)
414         local n = core.get_node(p)
415         if core.get_item_group(n.name, "falling_node") ~= 0 then
416                 local p_bottom = {x = p.x, y = p.y - 1, z = p.z}
417                 -- Only spawn falling node if node below is loaded
418                 local n_bottom = core.get_node_or_nil(p_bottom)
419                 local d_bottom = n_bottom and core.registered_nodes[n_bottom.name]
420                 if d_bottom and
421
422                                 (core.get_item_group(n.name, "float") == 0 or
423                                 d_bottom.liquidtype == "none") and
424
425                                 (n.name ~= n_bottom.name or (d_bottom.leveled and
426                                 core.get_node_level(p_bottom) <
427                                 core.get_node_max_level(p_bottom))) and
428
429                                 (not d_bottom.walkable or d_bottom.buildable_to) then
430                         convert_to_falling_node(p, n)
431                         return true
432                 end
433         end
434
435         if core.get_item_group(n.name, "attached_node") ~= 0 then
436                 if not builtin_shared.check_attached_node(p, n) then
437                         drop_attached_node(p)
438                         return true
439                 end
440         end
441
442         return false
443 end
444
445 -- This table is specifically ordered.
446 -- We don't walk diagonals, only our direct neighbors, and self.
447 -- Down first as likely case, but always before self. The same with sides.
448 -- Up must come last, so that things above self will also fall all at once.
449 local check_for_falling_neighbors = {
450         {x = -1, y = -1, z = 0},
451         {x = 1, y = -1, z = 0},
452         {x = 0, y = -1, z = -1},
453         {x = 0, y = -1, z = 1},
454         {x = 0, y = -1, z = 0},
455         {x = -1, y = 0, z = 0},
456         {x = 1, y = 0, z = 0},
457         {x = 0, y = 0, z = 1},
458         {x = 0, y = 0, z = -1},
459         {x = 0, y = 0, z = 0},
460         {x = 0, y = 1, z = 0},
461 }
462
463 function core.check_for_falling(p)
464         -- Round p to prevent falling entities to get stuck.
465         p = vector.round(p)
466
467         -- We make a stack, and manually maintain size for performance.
468         -- Stored in the stack, we will maintain tables with pos, and
469         -- last neighbor visited. This way, when we get back to each
470         -- node, we know which directions we have already walked, and
471         -- which direction is the next to walk.
472         local s = {}
473         local n = 0
474         -- The neighbor order we will visit from our table.
475         local v = 1
476
477         while true do
478                 -- Push current pos onto the stack.
479                 n = n + 1
480                 s[n] = {p = p, v = v}
481                 -- Select next node from neighbor list.
482                 p = vector.add(p, check_for_falling_neighbors[v])
483                 -- Now we check out the node. If it is in need of an update,
484                 -- it will let us know in the return value (true = updated).
485                 if not core.check_single_for_falling(p) then
486                         -- If we don't need to "recurse" (walk) to it then pop
487                         -- our previous pos off the stack and continue from there,
488                         -- with the v value we were at when we last were at that
489                         -- node
490                         repeat
491                                 local pop = s[n]
492                                 p = pop.p
493                                 v = pop.v
494                                 s[n] = nil
495                                 n = n - 1
496                                 -- If there's nothing left on the stack, and no
497                                 -- more sides to walk to, we're done and can exit
498                                 if n == 0 and v == 11 then
499                                         return
500                                 end
501                         until v < 11
502                         -- The next round walk the next neighbor in list.
503                         v = v + 1
504                 else
505                         -- If we did need to walk the neighbor, then
506                         -- start walking it from the walk order start (1),
507                         -- and not the order we just pushed up the stack.
508                         v = 1
509                 end
510         end
511 end
512
513 --
514 -- Global callbacks
515 --
516
517 local function on_placenode(p, node)
518         core.check_for_falling(p)
519 end
520 core.register_on_placenode(on_placenode)
521
522 local function on_dignode(p, node)
523         core.check_for_falling(p)
524 end
525 core.register_on_dignode(on_dignode)
526
527 local function on_punchnode(p, node)
528         core.check_for_falling(p)
529 end
530 core.register_on_punchnode(on_punchnode)