26 local function coupling_particles(pos,truth)
32 minetest.add_particlespawner({
37 minvel = vector.new(-10,-10,-10),
38 maxvel = vector.new(10,10,10),
39 minacc = {x=0, y=0, z=0},
40 maxacc = {x=0, y=0, z=0},
45 collisiondetection = false,
46 collision_removal = false,
48 texture = "couple_particle.png^[colorize:"..color..":200",
53 local function data_injection(pos,data)
55 pool[minetest.hash_node_position(pos)] = true
57 pool[minetest.hash_node_position(pos)] = nil
61 local function speed_limiter(self,speed)
62 local test = self.object:get_velocity()--vector.multiply(self.velocity,new_vel)
64 if test.x > speed then
66 elseif test.x < -speed then
69 if test.z > speed then
71 elseif test.z < -speed then
74 self.object:set_velocity(test)
77 local function create_axis(pos)
78 local possible_dirs = {}
79 for _,dir in pairs(dirs) do
80 local pos2 = vector.add(pos,dir)
81 if pool[minetest.hash_node_position(pos2)] then
82 table.insert(possible_dirs,dir)
88 local function collision_detect(self)
89 if not self.axis_lock then return end
90 local pos = self.object:get_pos()
91 for _,object in ipairs(minetest.get_objects_inside_radius(pos, 1)) do
92 if object:is_player() then
93 local pos2 = object:get_pos()
94 if self.axis_lock == "x" then
96 local velocity = (1-vector.distance(vector.new(pos.x,0,0),vector.new(pos2.x,0,0)))*5
97 local dir = vector.direction(vector.new(pos2.x,0,0),vector.new(pos.x,0,0))
98 local new_vel = vector.multiply(dir,velocity)
99 self.object:add_velocity(new_vel)
101 elseif self.axis_lock == "z" then
102 local velocity = (1-vector.distance(vector.new(0,0,pos.z),vector.new(0,0,pos2.z)))*5
103 local dir = vector.direction(vector.new(0,0,pos2.z),vector.new(0,0,pos.z))
104 local new_vel = vector.multiply(dir,velocity)
105 self.object:add_velocity(new_vel)
113 local function direction_snap(self)
116 if dir.y == 1 then pitch = math.pi/4 end
117 if dir.y == -1 then pitch = -math.pi/4 end
119 local yaw = minetest.dir_to_yaw(dir)
122 self.driver:set_look_vertical(-pitch)
123 self.driver:set_look_horizontal(yaw)
125 self.object:set_rotation(vector.new(pitch,yaw,0))
130 local function turn_snap(pos,self,dir,dir2)
131 if self.axis_lock == "x" then
132 if dir.x ~= 0 and dir2.z ~= 0 then
133 local velocity = self.object:get_velocity()
134 local inertia = math.abs(velocity.x)
135 self.object:set_velocity(vector.multiply(dir2,inertia))
138 self.object:set_pos(pos)
143 if self.axis_lock == "z" then
144 if dir.z ~= 0 and dir2.x ~= 0 then
145 local velocity = self.object:get_velocity()
146 local inertia = math.abs(velocity.z)
147 self.object:set_velocity(vector.multiply(dir2,inertia))
150 self.object:set_pos(pos)
158 local function climb_snap(pos,self,dir,dir2)
159 if self.axis_lock == "x" then
160 if dir.x == dir2.x and dir2.y ~= 0 then
161 local velocity = self.object:get_velocity()
162 local inertia = math.abs(velocity.x)
163 self.object:set_velocity(vector.multiply(dir2,inertia))
166 self.object:set_pos(pos)
171 if self.axis_lock == "z" then
172 if dir.z == dir2.z and dir2.y ~= 0 then
173 local velocity = self.object:get_velocity()
174 local inertia = math.abs(velocity.z)
175 self.object:set_velocity(vector.multiply(dir2,inertia))
178 self.object:set_pos(pos)
186 local function straight_snap(pos,self,dir)
187 if self.axis_lock == "x" then
188 if dir.x ~= 0 and pool[minetest.hash_node_position(vector.add(pos,vector.new(dir.x,0,0)))] then
189 local velocity = self.object:get_velocity()
190 self.object:set_velocity(vector.new(velocity.x,0,0))
191 self.dir = vector.new(dir.x,0,0)
193 self.object:set_pos(pos)
198 if self.axis_lock == "z" then
199 if dir.z ~= 0 and pool[minetest.hash_node_position(vector.add(pos,vector.new(0,0,dir.z)))] then
200 local velocity = self.object:get_velocity()
201 self.object:set_velocity(vector.new(0,0,velocity.z))
202 self.dir = vector.new(0,0,dir.z)
204 self.object:set_pos(pos)
213 local function coupling_logic(self)
215 if not self.axis_lock then return end
217 if not self.coupler1 then return end
219 if self.dir.y ~= 0 then return end
221 local pos = self.object:get_pos()
223 local pos2 = self.coupler1:get_pos()
225 local coupler_goal = self.coupler1:get_luaentity().coupler_distance
227 local coupler_velocity = self.coupler1:get_velocity()
229 if self.axis_lock == "x" then
230 local velocity_real = self.object:get_velocity()
231 local distance = vector.distance(pos,pos2)
232 local new_vel = vector.new(0,0,0)
233 if distance > coupler_goal then
234 local velocity = (distance-coupler_goal)*5
235 local dir = vector.direction(vector.new(pos.x,0,0),vector.new(pos2.x,0,0))
237 new_vel = vector.multiply(dir,velocity)
239 --if vector.equals(coupler_velocity,vector.new(0,0,0)) then
240 --new_vel = vector.multiply(velocity_real,-1)
241 if distance > coupler_goal-0.2 then
242 local c_vel = vector.distance(vector.new(0,0,0),coupler_velocity)
243 local a_vel = vector.distance(vector.new(0,0,0),velocity_real)
244 local d_vel = a_vel-c_vel
248 new_vel = vector.multiply(self.dir,d_vel)
250 new_vel = vector.multiply(velocity_real,-1)
253 self.object:add_velocity(new_vel)
254 elseif self.axis_lock == "z" then
255 local velocity_real = self.object:get_velocity()
256 local distance = vector.distance(pos,pos2)
257 local new_vel = vector.new(0,0,0)
258 if distance > coupler_goal then
259 local velocity = (distance-coupler_goal)*5
260 local dir = vector.direction(vector.new(0,0,pos.z),vector.new(0,0,pos2.z))
262 new_vel = vector.multiply(dir,velocity)
264 --if vector.equals(coupler_velocity,vector.new(0,0,0)) then
265 --new_vel = vector.multiply(velocity_real,-1)
266 if distance > coupler_goal-0.2 then
267 local c_vel = vector.distance(vector.new(0,0,0),coupler_velocity)
268 local a_vel = vector.distance(vector.new(0,0,0),velocity_real)
269 local d_vel = a_vel-c_vel
273 new_vel = vector.multiply(self.dir,d_vel)
275 new_vel = vector.multiply(velocity_real,-1)
278 self.object:add_velocity(new_vel)
285 local function rail_brain(self,pos)
287 if not self.dir then self.dir = vector.new(0,0,0) end
289 local pos2 = self.object:get_pos()
293 speed_limiter(self,6)
295 if not pool[minetest.hash_node_position(vector.add(pos,dir))] then
297 if straight_snap(pos,self,dir) then
301 local possible_dirs = create_axis(pos)
303 if table.getn(possible_dirs) == 0 then
304 --stop slow down become physical
306 for _,dir2 in pairs(possible_dirs) do
307 if turn_snap(pos,self,dir,dir2) then
310 if climb_snap(pos,self,dir,dir2) then
325 █████╗ ██████╗ ██╗ ██████╗ ███████╗ ██████╗ ██╗███╗ ██╗
326 ██╔══██╗██╔══██╗██║ ██╔══██╗██╔════╝██╔════╝ ██║████╗ ██║
327 ███████║██████╔╝██║ ██████╔╝█████╗ ██║ ███╗██║██╔██╗ ██║
328 ██╔══██║██╔═══╝ ██║ ██╔══██╗██╔══╝ ██║ ██║██║██║╚██╗██║
329 ██║ ██║██║ ██║ ██████╔╝███████╗╚██████╔╝██║██║ ╚████║
330 ╚═╝ ╚═╝╚═╝ ╚═╝ ╚═════╝ ╚══════╝ ╚═════╝ ╚═╝╚═╝ ╚═══╝
334 function register_train(name,data)
337 train.power = data.power
338 train.coupler_distance = data.coupler_distance
339 train.is_car = data.is_car
340 train.is_engine = data.is_engine
341 train.max_speed = data.max_speed
344 train.initial_properties = {
345 physical = false, -- otherwise going uphill breaks
346 collisionbox = {-0.4, -0.5, -0.4, 0.4, 0.45, 0.4},
349 visual_size = {x=1, y=1},
350 textures = {data.texture},
354 train.on_step = function(self,dtime)
356 self.object:set_pos(self.old_pos)
358 local pos = vector.round(self.object:get_pos())
359 if not self.axis_lock then
360 local possible_dirs = create_axis(pos)
361 for _,dir in pairs(possible_dirs) do
367 elseif dir.z ~= 0 then
376 --collision_detect(self)
378 self.old_pos = self.object:get_pos()
384 train.on_punch = function(self, puncher)
385 if not puncher:get_wielded_item():get_name() == "train:wrench" then
389 if self.is_engine and puncher:get_player_control().sneak then
390 if vector.equals(self.object:get_velocity(),vector.new(0,0,0)) then
391 if self.dir.y == 0 then
392 self.dir = vector.multiply(self.dir,-1)
394 minetest.sound_play("wrench",{
395 object = self.object,
397 max_hear_distance = 64,
404 if self.is_engine then
405 self.object:set_velocity(vector.multiply(self.dir,self.max_speed))
409 if self.coupler1 then
410 self.coupler1:get_luaentity().coupler2 = nil
414 if self.coupler2 then
415 self.coupler2:get_luaentity().coupler1 = nil
422 train.on_rightclick = function(self,clicker)
424 if clicker:get_wielded_item():get_name() == "utility:furnace" then
425 local obj = minetest.add_entity(pos, "train:furnace")
426 obj:set_attach(self.object,"",vector.new(0,0,0),vector.new(0,0,0))
427 minetest.sound_play("wrench",{
428 object = self.object,
430 max_hear_distance = 64,
432 coupling_particles(pos,true)
438 if clicker:get_wielded_item():get_name() ~= "train:wrench" then
439 if self.is_engine then
440 if not self.driver then
442 clicker:set_attach(self.object, "", data.body_pos, data.body_rotation)
443 clicker:set_eye_offset(data.eye_offset,{x=0,y=0,z=0})
444 player_is_attached(clicker,true)
445 set_player_animation(clicker,"stand",0)
446 local rotation = self.object:get_rotation()
447 clicker:set_look_vertical(0)
448 clicker:set_look_horizontal(rotation.y)
449 self.object:set_velocity(vector.multiply(self.dir,self.max_speed))
450 self.driver = clicker
451 elseif clicker == self.driver then
454 clicker:set_eye_offset({x=0,y=0,z=0},{x=0,y=0,z=0})
455 player_is_attached(clicker,false)
456 set_player_animation(clicker,"stand",0)
457 self.object:set_velocity(vector.new(0,0,0))
465 local pos = self.object:get_pos()
467 local name = clicker:get_player_name()
468 if not pool[name] then
469 if not self.coupler2 then
470 pool[name] = self.object
471 minetest.sound_play("wrench",{
472 object = self.object,
474 max_hear_distance = 64,
476 coupling_particles(pos,true)
478 minetest.sound_play("wrench",{
479 object = self.object,
481 max_hear_distance = 64,
484 coupling_particles(pos,false)
487 if not self.is_engine and pool[name] ~= self.object and not (pool[name]:get_luaentity().coupler1 and pool[name]:get_luaentity().coupler1 == self.object or self.coupler2) then
488 self.coupler1 = pool[name]
489 pool[name]:get_luaentity().coupler2 = self.object
490 minetest.sound_play("wrench",{
491 object = self.object,
493 max_hear_distance = 64,
495 coupling_particles(pos,true)
497 minetest.sound_play("wrench",{
498 object = self.object,
500 max_hear_distance = 64,
503 coupling_particles(pos,false)
510 train.on_activate = function(self,staticdata, dtime_s)
511 self.object:set_armor_groups({immortal=1})
512 if string.sub(staticdata, 1, string.len("return")) ~= "return" then
515 local data = minetest.deserialize(staticdata)
516 if type(data) ~= "table" then
519 self.old_pos = self.object:get_pos()
520 self.velocity = vector.new(0,0,0)
523 train.get_staticdata = function(self)
524 return minetest.serialize({
528 minetest.register_entity(name, train)
533 ███████╗███╗ ██╗██████╗
534 ██╔════╝████╗ ██║██╔══██╗
535 █████╗ ██╔██╗ ██║██║ ██║
536 ██╔══╝ ██║╚██╗██║██║ ██║
537 ███████╗██║ ╚████║██████╔╝
538 ╚══════╝╚═╝ ╚═══╝╚═════╝
544 register_train("train:steam_train",{
545 mesh = "steam_train.b3d",
546 texture = "steam_train.png",
550 coupler_distance = 3,
551 body_pos = vector.new(0,0,-15),
552 body_rotation = vector.new(0,0,0),
553 eye_offset = vector.new(6,-1,-10)
556 register_train("train:steam_train_small",{
557 mesh = "steam_train_small.b3d",
558 texture = "steam_train_small.png",
562 coupler_distance = 3,
563 body_pos = vector.new(0,0,-15),
564 body_rotation = vector.new(0,0,0),
565 eye_offset = vector.new(6,-1,-10)
569 register_train("train:minecart",{
571 texture = "minecart.png",
576 coupler_distance = 1.3,
577 --body_pos = vector.new(0,0,-15),
578 --body_rotation = vector.new(0,0,0),
579 --eye_offset = vector.new(6,-1,-10)
584 minetest.register_craftitem("train:train", {
585 description = "Steam Train",
586 inventory_image = "minecartitem.png",
587 wield_image = "minecartitem.png",
588 on_place = function(itemstack, placer, pointed_thing)
589 if not pointed_thing.type == "node" then
593 local sneak = placer:get_player_control().sneak
594 local noddef = minetest.registered_nodes[minetest.get_node(pointed_thing.under).name]
595 if not sneak and noddef.on_rightclick then
596 minetest.item_place(itemstack, placer, pointed_thing)
600 if minetest.get_item_group(minetest.get_node(pointed_thing.under).name, "rail")>0 then
601 minetest.add_entity(pointed_thing.under, "train:steam_train")
606 itemstack:take_item()
612 minetest.register_craft({
613 output = "train:minecart",
615 {"main:iron", "main:iron", "main:iron"},
616 {"main:iron", "main:iron", "main:iron"},
621 minetest.register_craftitem("train:minecart", {
622 description = "Minecart",
623 inventory_image = "minecartitem.png",
624 wield_image = "minecartitem.png",
625 on_place = function(itemstack, placer, pointed_thing)
626 if not pointed_thing.type == "node" then
630 local sneak = placer:get_player_control().sneak
631 local noddef = minetest.registered_nodes[minetest.get_node(pointed_thing.under).name]
632 if not sneak and noddef.on_rightclick then
633 minetest.item_place(itemstack, placer, pointed_thing)
637 if minetest.get_item_group(minetest.get_node(pointed_thing.under).name, "rail")>0 then
638 minetest.add_entity(pointed_thing.under, "train:minecart")
643 itemstack:take_item()
649 minetest.register_craft({
650 output = "train:train",
652 {"main:iron", "", "main:iron"},
653 {"main:iron", "main:iron", "main:iron"},
659 minetest.register_node("train:rail",{
660 description = "Rail",
661 wield_image = "rail.png",
663 "rail.png", "railcurve.png",
664 "railt.png", "railcross.png"
666 drawtype = "raillike",
668 sunlight_propagates = true,
669 is_ground_content = false,
671 node_placement_prediction = "",
674 fixed = {-1/2, -1/2, -1/2, 1/2, -1/2+1/16, 1/2},
676 sounds = main.stoneSound(),
677 after_place_node = function(pos)
678 data_injection(pos,true)
680 after_destruct = function(pos)
683 groups={stone=1,wood=1,rail=1,attached_node=1},
687 minetest.register_lbm({
689 nodenames = {"train:rail"},
690 run_at_every_load = true,
691 action = function(pos)
692 data_injection(pos,true)
696 minetest.register_craft({
697 output = "train:rail 32",
699 {"main:iron","","main:iron"},
700 {"main:iron","main:stick","main:iron"},
701 {"main:iron","","main:iron"}
706 minetest.register_food("train:wrench",{
707 description = "Train Wrench",
708 texture = "wrench.png",
711 minetest.register_craft({
712 output = "train:wrench",
714 {"main:iron", "", "main:iron"},
715 {"main:iron", "main:lapis", "main:iron"},
716 {"", "main:lapis", ""}
722 minetest.register_entity("train:furnace", {
723 initial_properties = {
724 visual = "wielditem",
725 visual_size = {x = 0.6, y = 0.6},
729 collide_with_objects = false,
731 collisionbox = {-0.5, -0.5, -0.5, 0.5, 0.5, 0.5},
733 set_node = function(self)
734 self.object:set_properties({
736 textures = {"utility:furnace"},
741 on_activate = function(self, staticdata)
742 self.object:set_armor_groups({immortal = 1})
748 local steam_check_dirs = {
754 local buffer_pool = {}
755 local function do_craft_effects(pos)
756 local hash_pos = minetest.hash_node_position(pos)
758 if buffer_pool[hash_pos] then return end
760 buffer_pool[hash_pos] = true
762 minetest.sound_play("steam_whistle_1",{pos=pos,gain=3,max_hear_distance=128})
763 minetest.add_particlespawner({
766 minpos = vector.new(pos.x-0.1,pos.y+0.5,pos.z-0.1),
767 maxpos = vector.new(pos.x+0.1,pos.y+0.5,pos.z+0.1),
768 minvel = vector.new(-0.5,3,-0.5),
769 maxvel = vector.new(0.5,5,0.5),
770 minacc = {x=0, y=3, z=0},
771 maxacc = {x=0, y=5, z=0},
776 collisiondetection = false,
777 collision_removal = false,
779 texture = "smoke.png",
782 minetest.after(1.3, function()
783 for _,dir in pairs(steam_check_dirs) do
784 local n_pos = vector.add(pos,dir)
785 local node2 = minetest.get_node(n_pos).name
786 if not minetest.get_nodedef(node2, "walkable") then
787 local dir_mod = vector.multiply(dir,0.5)
797 elseif dir.x == 0 then
804 local p_min = vector.new(pos.x+x_min,pos.y-0.2,pos.z+z_min)
805 local p_max = vector.new(pos.x+x_max,pos.y+0.2,pos.z+z_max)
807 local v_min = vector.new(dir_mod.x,0.2,dir_mod.z)
808 local v_max = vector.new(dir_mod.x*2,0.3,dir_mod.z*2)
810 minetest.add_particlespawner({
817 minacc = vector.new(0,1,0),
818 maxacc = vector.new(0,3,0),
823 collisiondetection = false,
824 collision_removal = false,
826 texture = "smoke.png^[colorize:white:255",
831 minetest.sound_play("steam_release",{pos=pos,gain=1,max_hear_distance=128})
832 minetest.after(1, function()
833 buffer_pool[hash_pos] = nil
838 minetest.register_on_craft(function(itemstack, player, old_craft_grid, craft_inv)
839 if minetest.registered_items[itemstack:get_name()].mod_origin == "train" then
840 local pos = player:get_pos()
841 pos.y = pos.y + 1.625
842 local look_dir = player:get_look_dir()
843 look_dir = vector.multiply(look_dir,4)
844 local pos2 = vector.add(pos,look_dir)
845 local ray = minetest.raycast(pos, pos2, false, true)
847 for pointed_thing in ray do
848 if pointed_thing then
849 if minetest.get_node(pointed_thing.under).name == "craftingtable:craftingtable" then
850 do_craft_effects(pointed_thing.under)