--[[ Minecart ======== Copyright (C) 2019-2021 Joachim Stolberg MIT See license.txt for more information ]]-- local P2S = function(pos) if pos then return minetest.pos_to_string(pos) end end local P2H = minetest.hash_node_position local H2P = minetest.get_position_from_hash local LEN = function(t) local c = 0; for _ in pairs(t) do c = c + 1 end; return c; end local MAX_SPEED = minecart.MAX_SPEED local dot2dir = minecart.dot2dir local get_waypoint = minecart.get_waypoint local recording_waypoints = minecart.recording_waypoints local recording_junctions = minecart.recording_junctions local set_junctions = minecart.set_junctions local player_ctrl = minecart.player_ctrl local tEntityNames = minecart.tEntityNames local function stop_cart(self, cart_pos) local player self.is_running = false self.arrival_time = 0 if self.driver then player = minetest.get_player_by_name(self.driver) if player then minecart.stop_recording(self, cart_pos) minecart.manage_attachment(player, self, false) end end if not minecart.get_buffer_pos(cart_pos, self.owner) then -- Probably somewhere in the pampas minecart.delete_cart_waypoint(cart_pos) end minecart.entity_to_node(cart_pos, self) if player then player:set_pos(cart_pos) end end local function get_ctrl(self, pos) -- Use player ctrl or junction data from recorded routes return (self.driver and self.ctrl) or (self.junctions and self.junctions[P2H(pos)]) or {} end local function new_speed(self, new_dir) self.cart_speed = self.cart_speed or 0 local rail_speed = (self.waypoint.speed or 0) / 10 if rail_speed <= 0 then rail_speed = math.max(self.cart_speed + rail_speed, 0) elseif rail_speed <= self.cart_speed then rail_speed = math.max((self.cart_speed + rail_speed) / 2, 0) end -- Speed corrections if new_dir.y == 1 then if rail_speed < 1 then rail_speed = 0 end else if rail_speed < 0.4 then rail_speed = 0 end end self.cart_speed = rail_speed -- store for next cycle return rail_speed end local function running(self) local rot = self.object:get_rotation() local dir = minetest.yaw_to_dir(rot.y) dir.y = math.floor((rot.x / (math.pi/4)) + 0.5) dir = vector.round(dir) -- If running in a 45 degree direction (extra cycle), use the old dir -- to calculate face_dir. Otherwise the junction detection will not work as expected. local facedir if self.waypoint and self.waypoint.old_dir then facedir = minetest.dir_to_facedir(self.waypoint.old_dir) else facedir = minetest.dir_to_facedir(dir) end local cart_pos, wayp_pos, is_junction if self.reenter then -- through monitoring cart_pos = H2P(self.reenter[1]) -- pos correction on slopes if not minecart.is_rail(cart_pos) then cart_pos.y = cart_pos.y - 1 end wayp_pos = cart_pos is_junction = false self.waypoint = {pos = H2P(self.reenter[2]), power = 0, dot = self.reenter[4]} self.cart_speed = self.reenter[3] self.speed_limit = MAX_SPEED self.reenter = nil elseif not self.waypoint then -- get waypoint cart_pos = vector.round(self.object:get_pos()) wayp_pos = cart_pos is_junction = false self.waypoint = get_waypoint(cart_pos, facedir, get_ctrl(self, cart_pos), true) if self.no_normal_start then -- Probably somewhere in the pampas minecart.delete_waypoint(cart_pos) self.no_normal_start = nil end self.cart_speed = 2 -- push speed self.speed_limit = MAX_SPEED else -- next waypoint cart_pos = vector.new(self.waypoint.cart_pos or self.waypoint.pos) wayp_pos = self.waypoint.pos local vel = self.object:get_velocity() self.waypoint, is_junction = get_waypoint(wayp_pos, facedir, get_ctrl(self, wayp_pos), self.cart_speed < 0.1) end if not self.waypoint then stop_cart(self, wayp_pos) return end if is_junction then if self.is_recording then set_junctions(self, wayp_pos) end self.ctrl = nil end --print("running", LEN(self.junctions)) local dist = vector.distance(cart_pos, self.waypoint.cart_pos or self.waypoint.pos) local new_dir = dot2dir(self.waypoint.dot) local new_speed = new_speed(self, new_dir) local straight_ahead = vector.equals(new_dir, dir) -- If straight_ahead, then it's probably a speed limit sign if straight_ahead then self.speed_limit = minecart.get_speedlimit(wayp_pos, facedir) or self.speed_limit end new_speed = math.min(new_speed, self.speed_limit) local new_cart_pos, extra_cycle = minecart.get_current_cart_pos_correction( wayp_pos, facedir, dir.y, self.waypoint.dot) -- TODO: Why has self.waypoint no dot? if extra_cycle and not vector.equals(cart_pos, new_cart_pos) then self.waypoint = {pos = wayp_pos, cart_pos = new_cart_pos, old_dir = vector.new(dir)} new_dir = vector.direction(cart_pos, new_cart_pos) dist = vector.distance(cart_pos, new_cart_pos) --print("extra_cycle", P2S(cart_pos), P2S(wayp_pos), P2S(new_cart_pos), new_speed) end -- Slope corrections --print("Slope corrections", P2S(new_dir), P2S(cart_pos)) if new_dir.y ~= 0 then cart_pos.y = cart_pos.y + 0.2 end -- Calc velocity, rotation and arrival_time local yaw = minetest.dir_to_yaw(new_dir) local pitch = new_dir.y * math.pi/4 --print("new_speed", new_speed / (new_dir.y ~= 0 and 1.41 or 1)) local vel = vector.multiply(new_dir, new_speed / ((new_dir.y ~= 0) and 1.41 or 1)) self.arrival_time = self.timebase + (dist / new_speed) -- needed for recording self.curr_speed = new_speed self.num_sections = (self.num_sections or 0) + 1 -- Got stuck somewhere if new_speed < 0.1 or dist < 0 then minetest.log("warning", "[Minecart] Got stuck somewhere " .. new_speed .. " " .. dist) stop_cart(self, wayp_pos) return end self.object:set_pos(cart_pos) self.object:set_rotation({x = pitch, y = yaw, z = 0}) self.object:set_velocity(vel) return end local function play_sound(self) if self.sound_handle then local handle = self.sound_handle self.sound_handle = nil minetest.after(0.2, minetest.sound_stop, handle) end if self.object then self.sound_handle = minetest.sound_play( "carts_cart_moving", { pos = self.object:get_pos(), gain = (self.curr_speed or 0) / MAX_SPEED, }) end end local function on_step(self, dtime) self.timebase = (self.timebase or 0) + dtime if self.is_running then if self.arrival_time <= self.timebase then running(self) end if (self.sound_ttl or 0) <= self.timebase then play_sound(self) self.sound_ttl = self.timebase + 1.0 end else if self.sound_handle then minetest.sound_stop(self.sound_handle) self.sound_handle = nil end end if self.driver then if self.is_recording then if self.rec_time <= self.timebase then recording_waypoints(self) self.rec_time = self.rec_time + 2.0 end if recording_junctions(self) then local pos = vector.round(self.object:get_pos()) minecart.stop_recording(self, pos, true) local player = minetest.get_player_by_name(self.driver) minecart.manage_attachment(player, self, false) minecart.entity_to_node(pos, self) end else if player_ctrl(self) then local pos = vector.round(self.object:get_pos()) local player = minetest.get_player_by_name(self.driver) minecart.manage_attachment(player, self, false) minecart.entity_to_node(pos, self) end end end end local function on_entitycard_activate(self, staticdata, dtime_s) self.object:set_armor_groups({immortal=1}) end -- Start the entity cart (or dig by shift+leftclick) local function on_entitycard_punch(self, puncher, time_from_last_punch, tool_capabilities, dir) if minecart.is_owner(puncher, self.owner) then if puncher:get_player_control().sneak then if not self.only_dig_if_empty or not next(self.cargo) then -- drop items local pos = vector.round(self.object:get_pos()) for _,item in ipairs(self.cargo or {}) do minetest.add_item(pos, ItemStack(item)) end -- Dig cart if self.driver then -- remove cart as driver minecart.stop_recording(self, pos, true) minecart.monitoring_remove_cart(self.owner, self.userID) minecart.remove_entity(self, pos, puncher) minecart.manage_attachment(puncher, self, false) else -- remove cart from outside minecart.monitoring_remove_cart(self.owner, self.userID) minecart.remove_entity(self, pos, puncher) end end elseif not self.is_running then -- start the cart local pos = vector.round(self.object:get_pos()) if puncher then local yaw = puncher:get_look_horizontal() dir = minetest.yaw_to_dir(yaw) self.object:set_rotation({x = 0, y = yaw, z = 0}) end local facedir = minetest.dir_to_facedir(dir or {x=0, y=0, z=0}) minecart.start_entitycart(self, pos, facedir or 0) minecart.start_recording(self, pos) end end end -- Player get on / off local function on_entitycard_rightclick(self, clicker) if clicker and clicker:is_player() and self.driver_allowed then -- Get on / off if self.driver then -- get off local pos = vector.round(self.object:get_pos()) minecart.stop_recording(self, pos, true) minecart.manage_attachment(clicker, self, false) minecart.entity_to_node(pos, self) else -- get on local pos = vector.round(self.object:get_pos()) minecart.stop_recording(self, pos, true) minecart.manage_attachment(clicker, self, true) end end end local function on_entitycard_detach_child(self, child) if child and child:get_player_name() == self.driver then self.driver = nil end end function minecart.get_entitycart_nearby(pos, param2, radius) local pos2 = param2 and vector.add(pos, minecart.param2_to_dir(param2)) or pos for _, object in pairs(minetest.get_objects_inside_radius(pos2, radius or 0.5)) do local entity = object:get_luaentity() if entity and entity.name and tEntityNames[entity.name] then local vel = object:get_velocity() if vector.equals(vel, {x=0, y=0, z=0}) then -- still standing? return entity end end end end function minecart.push_entitycart(self, punch_dir) --print("push_entitycart") local vel = self.object:get_velocity() punch_dir.y = 0 local yaw = minetest.dir_to_yaw(punch_dir) self.object:set_rotation({x = 0, y = yaw, z = 0}) self.is_running = true self.arrival_time = 0 end function minecart.register_cart_entity(entity_name, node_name, cart_type, entity_def) entity_def.entity_name = entity_name entity_def.node_name = node_name entity_def.on_activate = on_entitycard_activate entity_def.on_punch = on_entitycard_punch entity_def.on_step = on_step entity_def.on_rightclick = on_entitycard_rightclick entity_def.on_detach_child = on_entitycard_detach_child entity_def.owner = nil entity_def.driver = nil entity_def.cargo = {} minetest.register_entity(entity_name, entity_def) -- register node for punching minecart.register_cart_names(node_name, entity_name, cart_type) end