Creative: Make the placenode registration check for non-player placers
[oweals/minetest_game.git] / mods / carts / cart_entity.lua
1 local cart_entity = {
2         physical = false, -- otherwise going uphill breaks
3         collisionbox = {-0.5, -0.5, -0.5, 0.5, 0.5, 0.5},
4         visual = "mesh",
5         mesh = "carts_cart.b3d",
6         visual_size = {x=1, y=1},
7         textures = {"carts_cart.png"},
8
9         driver = nil,
10         punched = false, -- used to re-send velocity and position
11         velocity = {x=0, y=0, z=0}, -- only used on punch
12         old_dir = {x=1, y=0, z=0}, -- random value to start the cart on punch
13         old_pos = nil,
14         old_switch = 0,
15         railtype = nil,
16         attached_items = {}
17 }
18
19 function cart_entity:on_rightclick(clicker)
20         if not clicker or not clicker:is_player() then
21                 return
22         end
23         local player_name = clicker:get_player_name()
24         if self.driver and player_name == self.driver then
25                 self.driver = nil
26                 carts:manage_attachment(clicker, nil)
27         elseif not self.driver then
28                 self.driver = player_name
29                 carts:manage_attachment(clicker, self.object)
30         end
31 end
32
33 function cart_entity:on_activate(staticdata, dtime_s)
34         self.object:set_armor_groups({immortal=1})
35         if string.sub(staticdata, 1, string.len("return")) ~= "return" then
36                 return
37         end
38         local data = minetest.deserialize(staticdata)
39         if not data or type(data) ~= "table" then
40                 return
41         end
42         self.railtype = data.railtype
43         if data.old_dir then
44                 self.old_dir = data.old_dir
45         end
46         if data.old_vel then
47                 self.old_vel = data.old_vel
48         end
49 end
50
51 function cart_entity:get_staticdata()
52         return minetest.serialize({
53                 railtype = self.railtype,
54                 old_dir = self.old_dir,
55                 old_vel = self.old_vel
56         })
57 end
58
59 function cart_entity:on_punch(puncher, time_from_last_punch, tool_capabilities, direction)
60         local pos = self.object:getpos()
61         local vel = self.object:getvelocity()
62         if not self.railtype or vector.equals(vel, {x=0, y=0, z=0}) then
63                 local node = minetest.get_node(pos).name
64                 self.railtype = minetest.get_item_group(node, "connect_to_raillike")
65         end
66         -- Punched by non-player
67         if not puncher or not puncher:is_player() then
68                 local cart_dir = carts:get_rail_direction(pos, self.old_dir, nil, nil, self.railtype)
69                 if vector.equals(cart_dir, {x=0, y=0, z=0}) then
70                         return
71                 end
72                 self.velocity = vector.multiply(cart_dir, 2)
73                 self.punched = true
74                 return
75         end
76         -- Player digs cart by sneak-punch
77         if puncher:get_player_control().sneak then
78                 if self.sound_handle then
79                         minetest.sound_stop(self.sound_handle)
80                 end
81                 -- Detach driver and items
82                 if self.driver then
83                         if self.old_pos then
84                                 self.object:setpos(self.old_pos)
85                         end
86                         local player = minetest.get_player_by_name(self.driver)
87                         carts:manage_attachment(player, nil)
88                 end
89                 for _,obj_ in ipairs(self.attached_items) do
90                         if obj_ then
91                                 obj_:set_detach()
92                         end
93                 end
94                 -- Pick up cart
95                 local inv = puncher:get_inventory()
96                 if not (creative and creative.is_enabled_for
97                                 and creative.is_enabled_for(puncher:get_player_name()))
98                                 or not inv:contains_item("main", "carts:cart") then
99                         local leftover = inv:add_item("main", "carts:cart")
100                         -- If no room in inventory add a replacement cart to the world
101                         if not leftover:is_empty() then
102                                 minetest.add_item(self.object:getpos(), leftover)
103                         end
104                 end
105                 self.object:remove()
106                 return
107         end
108         -- Player punches cart to alter velocity
109         if puncher:get_player_name() == self.driver then
110                 if math.abs(vel.x + vel.z) > carts.punch_speed_max then
111                         return
112                 end
113         end
114
115         local punch_dir = carts:velocity_to_dir(puncher:get_look_dir())
116         punch_dir.y = 0
117         local cart_dir = carts:get_rail_direction(pos, punch_dir, nil, nil, self.railtype)
118         if vector.equals(cart_dir, {x=0, y=0, z=0}) then
119                 return
120         end
121
122         local punch_interval = 1
123         if tool_capabilities and tool_capabilities.full_punch_interval then
124                 punch_interval = tool_capabilities.full_punch_interval
125         end
126         time_from_last_punch = math.min(time_from_last_punch or punch_interval, punch_interval)
127         local f = 2 * (time_from_last_punch / punch_interval)
128
129         self.velocity = vector.multiply(cart_dir, f)
130         self.old_dir = cart_dir
131         self.punched = true
132 end
133
134 local function rail_on_step_event(handler, obj, dtime)
135         if handler then
136                 handler(obj, dtime)
137         end
138 end
139
140 -- sound refresh interval = 1.0sec
141 local function rail_sound(self, dtime)
142         if not self.sound_ttl then
143                 self.sound_ttl = 1.0
144                 return
145         elseif self.sound_ttl > 0 then
146                 self.sound_ttl = self.sound_ttl - dtime
147                 return
148         end
149         self.sound_ttl = 1.0
150         if self.sound_handle then
151                 local handle = self.sound_handle
152                 self.sound_handle = nil
153                 minetest.after(0.2, minetest.sound_stop, handle)
154         end
155         local vel = self.object:getvelocity()
156         local speed = vector.length(vel)
157         if speed > 0 then
158                 self.sound_handle = minetest.sound_play(
159                         "carts_cart_moving", {
160                         object = self.object,
161                         gain = (speed / carts.speed_max) / 2,
162                         loop = true,
163                 })
164         end
165 end
166
167 local function get_railparams(pos)
168         local node = minetest.get_node(pos)
169         return carts.railparams[node.name] or {}
170 end
171
172 local function rail_on_step(self, dtime)
173         local vel = self.object:getvelocity()
174         if self.punched then
175                 vel = vector.add(vel, self.velocity)
176                 self.object:setvelocity(vel)
177                 self.old_dir.y = 0
178         elseif vector.equals(vel, {x=0, y=0, z=0}) then
179                 return
180         end
181
182         local pos = self.object:getpos()
183         local update = {}
184
185         -- stop cart if velocity vector flips
186         if self.old_vel and self.old_vel.y == 0 and
187                         (self.old_vel.x * vel.x < 0 or self.old_vel.z * vel.z < 0) then
188                 self.old_vel = {x = 0, y = 0, z = 0}
189                 self.old_pos = pos
190                 self.object:setvelocity(vector.new())
191                 self.object:setacceleration(vector.new())
192                 rail_on_step_event(get_railparams(pos).on_step, self, dtime)
193                 return
194         end
195         self.old_vel = vector.new(vel)
196
197         if self.old_pos and not self.punched then
198                 local flo_pos = vector.round(pos)
199                 local flo_old = vector.round(self.old_pos)
200                 if vector.equals(flo_pos, flo_old) then
201                         -- Do not check one node multiple times
202                         return
203                 end
204         end
205
206         local ctrl, player
207
208         -- Get player controls
209         if self.driver then
210                 player = minetest.get_player_by_name(self.driver)
211                 if player then
212                         ctrl = player:get_player_control()
213                 end
214         end
215
216         if self.old_pos then
217                 -- Detection for "skipping" nodes
218                 local found_path = carts:pathfinder(
219                         pos, self.old_pos, self.old_dir, ctrl, self.old_switch, self.railtype
220                 )
221
222                 if not found_path then
223                         -- No rail found: reset back to the expected position
224                         pos = vector.new(self.old_pos)
225                         update.pos = true
226                 end
227         end
228
229         local cart_dir = carts:velocity_to_dir(vel)
230         local railparams
231
232         -- dir:         New moving direction of the cart
233         -- switch_keys: Currently pressed L/R key, used to ignore the key on the next rail node
234         local dir, switch_keys = carts:get_rail_direction(
235                 pos, cart_dir, ctrl, self.old_switch, self.railtype
236         )
237
238         local new_acc = {x=0, y=0, z=0}
239         if vector.equals(dir, {x=0, y=0, z=0}) then
240                 vel = {x = 0, y = 0, z = 0}
241                 pos = vector.round(pos)
242                 update.pos = true
243                 update.vel = true
244         else
245                 -- Direction change detected
246                 if not vector.equals(dir, self.old_dir) then
247                         vel = vector.multiply(dir, math.abs(vel.x + vel.z))
248                         update.vel = true
249                         if dir.y ~= self.old_dir.y then
250                                 pos = vector.round(pos)
251                                 update.pos = true
252                         end
253                 end
254                 -- Center on the rail
255                 if dir.z ~= 0 and math.floor(pos.x + 0.5) ~= pos.x then
256                         pos.x = math.floor(pos.x + 0.5)
257                         update.pos = true
258                 end
259                 if dir.x ~= 0 and math.floor(pos.z + 0.5) ~= pos.z then
260                         pos.z = math.floor(pos.z + 0.5)
261                         update.pos = true
262                 end
263
264                 -- Slow down or speed up..
265                 local acc = dir.y * -4.0
266
267                 -- Get rail for corrected position
268                 railparams = get_railparams(pos)
269
270                 -- no need to check for railparams == nil since we always make it exist.
271                 local speed_mod = railparams.acceleration
272                 if speed_mod and speed_mod ~= 0 then
273                         -- Try to make it similar to the original carts mod
274                         acc = acc + speed_mod
275                 else
276                         -- Handbrake or coast
277                         if ctrl and ctrl.down then
278                                 acc = acc - 3
279                         else
280                                 acc = acc - 0.4
281                         end
282                 end
283
284                 new_acc = vector.multiply(dir, acc)
285         end
286
287         -- Limits
288         local max_vel = carts.speed_max
289         for _, v in pairs({"x","y","z"}) do
290                 if math.abs(vel[v]) > max_vel then
291                         vel[v] = carts:get_sign(vel[v]) * max_vel
292                         new_acc[v] = 0
293                         update.vel = true
294                 end
295         end
296
297         self.object:setacceleration(new_acc)
298         self.old_pos = vector.new(pos)
299         if not vector.equals(dir, {x=0, y=0, z=0}) then
300                 self.old_dir = vector.new(dir)
301         end
302         self.old_switch = switch_keys
303
304         if self.punched then
305                 -- Collect dropped items
306                 for _, obj_ in pairs(minetest.get_objects_inside_radius(pos, 1)) do
307                         if not obj_:is_player() and
308                                         obj_:get_luaentity() and
309                                         not obj_:get_luaentity().physical_state and
310                                         obj_:get_luaentity().name == "__builtin:item" then
311
312                                 obj_:set_attach(self.object, "", {x=0, y=0, z=0}, {x=0, y=0, z=0})
313                                 self.attached_items[#self.attached_items + 1] = obj_
314                         end
315                 end
316                 self.punched = false
317                 update.vel = true
318         end
319
320         railparams = railparams or get_railparams(pos)
321
322         if not (update.vel or update.pos) then
323                 rail_on_step_event(railparams.on_step, self, dtime)
324                 return
325         end
326
327         local yaw = 0
328         if self.old_dir.x < 0 then
329                 yaw = 0.5
330         elseif self.old_dir.x > 0 then
331                 yaw = 1.5
332         elseif self.old_dir.z < 0 then
333                 yaw = 1
334         end
335         self.object:setyaw(yaw * math.pi)
336
337         local anim = {x=0, y=0}
338         if dir.y == -1 then
339                 anim = {x=1, y=1}
340         elseif dir.y == 1 then
341                 anim = {x=2, y=2}
342         end
343         self.object:set_animation(anim, 1, 0)
344
345         self.object:setvelocity(vel)
346         if update.pos then
347                 self.object:setpos(pos)
348         end
349
350         -- call event handler
351         rail_on_step_event(railparams.on_step, self, dtime)
352 end
353
354 function cart_entity:on_step(dtime)
355         rail_on_step(self, dtime)
356         rail_sound(self, dtime)
357 end
358
359 minetest.register_entity("carts:cart", cart_entity)
360
361 minetest.register_craftitem("carts:cart", {
362         description = "Cart (Sneak+Click to pick up)",
363         inventory_image = minetest.inventorycube("carts_cart_top.png", "carts_cart_side.png", "carts_cart_side.png"),
364         wield_image = "carts_cart_side.png",
365         on_place = function(itemstack, placer, pointed_thing)
366                 local under = pointed_thing.under
367                 local node = minetest.get_node(under)
368                 local udef = minetest.registered_nodes[node.name]
369                 if udef and udef.on_rightclick and
370                                 not (placer and placer:get_player_control().sneak) then
371                         return udef.on_rightclick(under, node, placer, itemstack,
372                                 pointed_thing) or itemstack
373                 end
374
375                 if not pointed_thing.type == "node" then
376                         return
377                 end
378                 if carts:is_rail(pointed_thing.under) then
379                         minetest.add_entity(pointed_thing.under, "carts:cart")
380                 elseif carts:is_rail(pointed_thing.above) then
381                         minetest.add_entity(pointed_thing.above, "carts:cart")
382                 else
383                         return
384                 end
385
386                 minetest.sound_play({name = "default_place_node_metal", gain = 0.5},
387                         {pos = pointed_thing.above})
388
389                 if not (creative and creative.is_enabled_for
390                                 and creative.is_enabled_for(placer:get_player_name())) then
391                         itemstack:take_item()
392                 end
393                 return itemstack
394         end,
395 })
396
397 minetest.register_craft({
398         output = "carts:cart",
399         recipe = {
400                 {"default:steel_ingot", "", "default:steel_ingot"},
401                 {"default:steel_ingot", "default:steel_ingot", "default:steel_ingot"},
402         },
403 })