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