local modname = minetest.get_current_modname() local S = minetest.get_translator(modname) local has_mcl_wip = minetest.get_modpath("mcl_wip") mcl_minecarts = {} mcl_minecarts.modpath = minetest.get_modpath(modname) mcl_minecarts.speed_max = 10 mcl_minecarts.check_float_time = 15 local max_step_distance = 0.5 local friction = 0.4 dofile(mcl_minecarts.modpath.."/functions.lua") dofile(mcl_minecarts.modpath.."/rails.lua") local LOGGING_ON = minetest.settings:get_bool("mcl_logging_minecarts", false) local DEBUG = false local function mcl_log(message) if LOGGING_ON then mcl_util.mcl_log(message, "[Minecarts]", true) end end mcl_minecarts.on_enter_below = function(pos, cart, next_dir, node_def) local staticdata = cart._staticdata if (node_def.groups.hopper or 0) == 0 then return end local hopper_pulled = mcl_hoppers.pull_from_minecart( cart, pos, cart._inv_size or 0 ) if DEBUG then print( "Attempt pull_from_minecart, hopper_pulled="..tostring(hopper_pulled) ) end if hopper_pulled and next_dir ~= staticdata.dir then -- If there was an item pulled by a hopper under the rails force the cart to stay put for 1.5 seconds -- to allow redstone time to process if hopper_pulled then staticdata.delay = 1.5 end end end --[[ Array of hooks { {u,v,w}, name } Actual position is pos + u * dir + v * right + w * up ]] local enter_exit_checks = { { 0, 0, 0, "" }, { 0, 0, 1, "_above" }, { 0, 0,-1, "_below" }, { 0, 1, 0, "_side" }, { 0,-1, 0, "_side" }, } local function handle_cart_enter_exit(self, pos, next_dir, event) local staticdata = self._staticdata local dir = staticdata.dir local right = vector.new( dir.z, dir.y, -dir.x) local up = vector.new(0,1,0) for _,check in ipairs(enter_exit_checks) do local check_pos = pos + dir * check[1] + right * check[2] + up * check[3] local node = minetest.get_node(check_pos) local node_def = minetest.registered_nodes[node.name] -- node-specific hook local hook_name = "_mcl_minecarts_"..event..check[4] local hook = node_def[hook_name] if hook then hook(check_pos, self, next_dir, pos) end -- global minecart hook hook = mcl_minecarts[event..check[4]] if hook then hook(check_pos, self, next_dir, node_def) end end -- Handle cart-specific behaviors local hook = self["_mcl_minecarts_"..event] if hook then hook(self, pos) end end local function handle_cart_enter(self, pos, next_dir) handle_cart_enter_exit(self, pos, next_dir, "on_enter" ) end local function handle_cart_leave(self, pos, next_dir) handle_cart_enter_exit(self, pos, next_dir, "on_leave" ) end local function handle_cart_node_watches(self, dtime) local staticdata = self._staticdata local watches = staticdata.node_watches or {} local new_watches = {} for _,node_pos in ipairs(watches) do local node = minetest.get_node(node_pos) local node_def = minetest.registered_nodes[node.name] local hook = node_def._mcl_minecarts_node_on_step if hook and hook(node_pos, self, dtime) then new_watches[#new_watches+1] = node_pos end end staticdata.node_watches = new_watches end local function update_cart_orientation(self,staticdata) -- constants local _2_pi = math.pi * 2 local pi = math.pi local dir = staticdata.dir -- Calculate an angle from the x,z direction components local rot_y = math.atan2( dir.x, dir.z ) + ( staticdata.rot_adjust or 0 ) if rot_y < 0 then rot_y = rot_y + _2_pi end -- Check if the rotation is a 180 flip and don't change if so local rot = self.object:get_rotation() local diff = math.abs((rot_y - ( rot.y + pi ) % _2_pi) ) if diff < 0.001 or diff > _2_pi - 0.001 then -- Update rotation adjust and recalculate the rotation staticdata.rot_adjust = ( ( staticdata.rot_adjust or 0 ) + pi ) % _2_pi rot.y = math.atan2( dir.x, dir.z ) + ( staticdata.rot_adjust or 0 ) else rot.y = rot_y end -- Forward/backwards tilt (pitch) if dir.y < 0 then rot.x = -0.25 * pi elseif dir.y > 0 then rot.x = 0.25 * pi else rot.x = 0 end if ( staticdata.rot_adjust or 0 ) < 0.01 then rot.x = -rot.x end if dir.z ~= 0 then rot.x = -rot.x end self.object:set_rotation(rot) end local function direction_away_from_players(self, staticdata) local objs = minetest.get_objects_inside_radius(self.object:get_pos(), 1.1) for n=1,#objs do local obj = objs[n] local player_name = obj:get_player_name() if player_name and player_name ~= "" and not ( self._driver and self._driver == player_name ) then local diff = obj:get_pos() - self.object:get_pos() local length = vector.distance(vector.new(0,0,0),diff) local vec = diff / length local force = vector.dot( vec, vector.normalize(staticdata.dir) ) if force > 0.5 then return -length * 4 elseif force < -0.5 then return length * 4 end end end return 0 end local function calculate_acceleration(self, staticdata) local acceleration = 0 -- Apply friction if moving if staticdata.velocity > 0 then acceleration = -friction end local pos = staticdata.connected_at local node_name = minetest.get_node(pos).name local node_def = minetest.registered_nodes[node_name] local max_vel = mcl_minecarts.speed_max if self._go_forward then acceleration = 2 elseif self._brake then acceleration = -1.5 elseif self._punched then acceleration = 2 elseif self._fueltime and self._fueltime > 0 then acceleration = 0.6 elseif staticdata.velocity >= ( node_def._max_acceleration_velocity or max_vel ) then -- Standard friction else if node_def._rail_acceleration then acceleration = node_def._rail_acceleration * 4 end end -- Factor in gravity after everything else local gravity_strength = 2.45 --friction * 5 if staticdata.dir.y < 0 then acceleration = gravity_strength - friction elseif staticdata.dir.y > 0 then acceleration = -gravity_strength + friction end return acceleration end local function reverse_direction(self, staticdata) -- Complete moving thru this block into the next, reverse direction, and put us back at the same position we were at local next_dir = -staticdata.dir staticdata.connected_at = staticdata.connected_at + staticdata.dir staticdata.distance = 1 - staticdata.distance -- recalculate direction local next_dir,_ = mcl_minecarts:get_rail_direction(staticdata.connected_at, next_dir, nil, nil, staticdata.railtype) staticdata.dir = next_dir end local function do_movement_step(self, dtime) local staticdata = self._staticdata if not staticdata.connected_at then return 0 end -- Calculate timestep remaiing in this block local x_0 = staticdata.distance or 0 local remaining_in_block = 1 - x_0 local a = calculate_acceleration(self, staticdata) local v_0 = staticdata.velocity -- Repel minecarts local away = direction_away_from_players(self, staticdata) if away > 0 then v_0 = away elseif away < 0 then reverse_direction(self, staticdata) v_0 = -away end if DEBUG and ( v_0 > 0 or a ~= 0 ) then print( " cart #"..tostring(staticdata.cart_id).. ": a="..tostring(a).. ",v_0="..tostring(v_0).. ",x_0="..tostring(x_0).. ",timestep="..tostring(timestep).. ",dir="..tostring(staticdata.dir).. ",connected_at="..tostring(staticdata.connected_at).. ",distance="..tostring(staticdata.distance) ) end -- Not moving if a == 0 and v_0 == 0 then return 0 end -- Movement equation with acceleration: x_1 = x_0 + v_0 * t + 0.5 * a * t*t local timestep local stops_in_block = false local inside = v_0 * v_0 + 2 * a * remaining_in_block if inside < 0 then -- Would stop or reverse direction inside this block, calculate time to v_1 = 0 timestep = -v_0 / a stops_in_block = true elseif a ~= 0 then -- Setting x_1 = x_0 + remaining_in_block, and solving for t gives: timestep = ( math.sqrt( v_0 * v_0 + 2 * a * remaining_in_block) - v_0 ) / a else timestep = remaining_in_block / v_0 end -- Truncate timestep to remaining time delta if timestep > dtime then timestep = dtime end -- Truncate timestep to prevent v_1 from being larger that speed_max local v_max = mcl_minecarts.speed_max if v_0 + a * timestep > v_max then timestep = ( v_max - v_0 ) / a end -- Prevent infinite loops if timestep <= 0 then return 0 end -- Calculate v_1 taking v_max into account local v_1 = v_0 + a * timestep if v_1 > v_max then v_1 = v_max elseif v_1 < friction / 5 then v_1 = 0 end -- Calculate x_1 local x_1 = x_0 + timestep * v_0 + 0.5 * a * timestep * timestep -- Update position and velocity of the minecart staticdata.velocity = v_1 staticdata.distance = x_1 -- Entity movement local pos = staticdata.connected_at -- Handle movement to next block, account for loss of precision in calculations if x_1 >= 0.99 then staticdata.distance = 0 -- Anchor at the next node local old_pos = pos pos = pos + staticdata.dir staticdata.connected_at = pos -- Get the next direction local next_dir,_ = mcl_minecarts:get_rail_direction(pos, staticdata.dir, nil, nil, staticdata.railtype) if DEBUG and next_dir ~= staticdata.dir then print( "Changing direction from "..tostring(staticdata.dir).." to "..tostring(next_dir)) end -- Leave the old node handle_cart_leave(self, old_pos, next_dir ) -- Enter the new node handle_cart_enter(self, pos, next_dir) -- Handle end of track if next_dir == staticdata.dir * -1 and next_dir.y == 0 then if DEBUG then print("Stopping cart at end of track at "..tostring(pos)) end staticdata.velocity = 0 end -- Update cart direction staticdata.dir = next_dir elseif stops_in_block and v_1 < (friction/5) and a <= 0 then -- Handle direction flip due to gravity if DEBUG then print("Gravity flipped direction") end -- Velocity should be zero at this point staticdata.velocity = 0 reverse_direction(self, staticdata) -- Intermediate movement pos = staticdata.connected_at + staticdata.dir * staticdata.distance else -- Intermediate movement pos = pos + staticdata.dir * staticdata.distance end self.object:move_to(pos) -- Update cart orientation update_cart_orientation(self,staticdata) -- Debug reporting if DEBUG and ( v_0 > 0 or v_1 > 0 ) then print( " cart #"..tostring(staticdata.cart_id).. ": a="..tostring(a).. ",v_0="..tostring(v_0).. ",v_1="..tostring(v_1).. ",x_0="..tostring(x_0).. ",x_1="..tostring(x_1).. ",timestep="..tostring(timestep).. ",dir="..tostring(staticdata.dir).. ",pos="..tostring(pos).. ",connected_at="..tostring(staticdata.connected_at).. ",distance="..tostring(staticdata.distance) ) end -- Report the amount of time processed return dtime - timestep end local function do_movement( self, dtime ) local staticdata = self._staticdata -- Allow the carts to be delay for the rest of the world to react before moving again if ( staticdata.delay or 0 ) > dtime then staticdata.delay = staticdata.delay - dtime return else staticdata.delay = 0 end local initial_velocity = 2 if self._punched and statcdata.velocity < initial_velocity then staticdata.velocity = initial_velocity end -- Break long movements at block boundaries to make it -- it impossible to jump across gaps due to server lag -- causing large timesteps while dtime > 0 do local new_dtime = do_movement_step(self, dtime) -- Handle node watches here in steps to prevent server lag from changing behavior handle_cart_node_watches(self, dtime - new_dtime) dtime = new_dtime end -- Clear punched flag now that movement for this step has been completed self._punched = false end local function detach_driver(self) if not self._driver then return end mcl_player.player_attached[self._driver] = nil local player = minetest.get_player_by_name(self._driver) self._driver = nil self._start_pos = nil if player then player:set_detach() player:set_eye_offset(vector.new(0,0,0),vector.new(0,0,0)) mcl_player.player_set_animation(player, "stand" , 30) end end local function activate_tnt_minecart(self, timer) if self._boomtimer then return end self.object:set_armor_groups({immortal=1}) if timer then self._boomtimer = timer else self._boomtimer = tnt.BOOMTIMER end self.object:set_properties({textures = { "mcl_tnt_blink.png", "mcl_tnt_blink.png", "mcl_tnt_blink.png", "mcl_tnt_blink.png", "mcl_tnt_blink.png", "mcl_tnt_blink.png", "mcl_minecarts_minecart.png", }}) self._blinktimer = tnt.BLINKTIMER minetest.sound_play("tnt_ignite", {pos = self.object:get_pos(), gain = 1.0, max_hear_distance = 15}, true) end local function activate_normal_minecart(self) detach_driver(self) if self._passenger then local mob = self._passenger.object mob:set_detach() end end local function hopper_take_item(self, dtime) local pos = self.object:get_pos() if not pos then return end if not self or self.name ~= "mcl_minecarts:hopper_minecart" then return end if mcl_util.check_dtime_timer(self, dtime, "hoppermc_take", 0.15) then --minetest.log("The check timer was triggered: " .. dump(pos) .. ", name:" .. self.name) else --minetest.log("The check timer was not triggered") return end --mcl_log("self.itemstring: ".. self.itemstring) local above_pos = vector.offset(pos, 0, 0.9, 0) --mcl_log("self.itemstring: ".. minetest.pos_to_string(above_pos)) local objs = minetest.get_objects_inside_radius(above_pos, 1.25) if objs then mcl_log("there is an itemstring. Number of objs: ".. #objs) for k, v in pairs(objs) do local ent = v:get_luaentity() if ent and not ent._removed and ent.itemstring and ent.itemstring ~= "" then local taken_items = false mcl_log("ent.name: " .. tostring(ent.name)) mcl_log("ent pos: " .. tostring(ent.object:get_pos())) local inv = mcl_entity_invs.load_inv(self, 5) if not inv then return false end local current_itemstack = ItemStack(ent.itemstring) mcl_log("inv. size: " .. self._inv_size) if inv:room_for_item("main", current_itemstack) then mcl_log("Room") inv:add_item("main", current_itemstack) ent.object:get_luaentity().itemstring = "" ent.object:remove() taken_items = true else mcl_log("no Room") end if not taken_items then local items_remaining = current_itemstack:get_count() -- This will take part of a floating item stack if no slot can hold the full amount for i = 1, self._inv_size, 1 do local stack = inv:get_stack("main", i) mcl_log("i: " .. tostring(i)) mcl_log("Items remaining: " .. items_remaining) mcl_log("Name: " .. tostring(stack:get_name())) if current_itemstack:get_name() == stack:get_name() then mcl_log("We have a match. Name: " .. tostring(stack:get_name())) local room_for = stack:get_stack_max() - stack:get_count() mcl_log("Room for: " .. tostring(room_for)) if room_for == 0 then -- Do nothing mcl_log("No room") elseif room_for < items_remaining then mcl_log("We have more items remaining than space") items_remaining = items_remaining - room_for stack:set_count(stack:get_stack_max()) inv:set_stack("main", i, stack) taken_items = true else local new_stack_size = stack:get_count() + items_remaining stack:set_count(new_stack_size) mcl_log("We have more than enough space. Now holds: " .. new_stack_size) inv:set_stack("main", i, stack) items_remaining = 0 ent.object:get_luaentity().itemstring = "" ent.object:remove() taken_items = true break end mcl_log("Count: " .. tostring(stack:get_count())) mcl_log("stack max: " .. tostring(stack:get_stack_max())) --mcl_log("Is it empty: " .. stack:to_string()) end if i == self._inv_size and taken_items then mcl_log("We are on last item and still have items left. Set final stack size: " .. items_remaining) current_itemstack:set_count(items_remaining) --mcl_log("Itemstack2: " .. current_itemstack:to_string()) ent.itemstring = current_itemstack:to_string() end end end --Add in, and delete if taken_items then mcl_log("Saving") mcl_entity_invs.save_inv(ent) return taken_items else mcl_log("No need to save") end end end end return false end -- Table for item-to-entity mapping. Keys: itemstring, Values: Corresponding entity ID local entity_mapping = {} local function make_staticdata( railtype, connected_at, dir ) return { railtype = railtype, connected_at = connected_at, distance = 0, velocity = 0, dir = vector.new(dir), cart_id = math.random(1,1000000000), } end local function to_dirstring(dir) if dir.x == 0 then if dir.z == 1 then return "north" else return "south" end elseif dir.z == 0 then if dir.x == 1 then return " east" else return " west" end end end local function register_entity(entity_id, def) assert( def.drop, "drop is required parameter" ) -- Entity groups local groups = { minecart = 1 } for k,v in pairs(def.groups or {}) do groups[k] = v end local cart = { initial_properties = { physical = true, collisionbox = {-10/16., -0.5, -10/16, 10/16, 0.25, 10/16}, visual = "mesh", mesh = def.mesh, visual_size = {x=1, y=1}, textures = def.textures, }, groups = groups, on_rightclick = def.on_rightclick, on_activate_by_rail = def.on_activate_by_rail, _mcl_minecarts_on_enter = def._mcl_minecarts_on_enter, _mcl_minecarts_on_place = def._mcl_minecarts_on_place, _mcl_minecarts_on_step = def._mcl_minecarts_on_step, _driver = nil, -- player who sits in and controls the minecart (only for minecart!) _passenger = nil, -- for mobs _punched = false, -- used to re-send _velocity and position _start_pos = nil, -- Used to calculate distance for “On A Rail” achievement _last_float_check = nil, -- timestamp of last time the cart was checked to be still on a rail _fueltime = nil, -- how many seconds worth of fuel is left. Only used by minecart with furnace _boomtimer = nil, -- how many seconds are left before exploding _blinktimer = nil, -- how many seconds are left before TNT blinking _blink = false, -- is TNT blink texture active? _old_pos = nil, _staticdata = nil, } function cart:on_activate(staticdata, dtime_s) -- Initialize local data = minetest.deserialize(staticdata) if type(data) == "table" then -- Migrate old data if data._railtype then data.railtype = data._railtype data._railtype = nil end -- Fix up types data.dir = vector.new(data.dir) self._staticdata = data end self.object:set_armor_groups({immortal=1}) -- Activate cart if on activator rail if self.on_activate_by_rail then local pos = self.object:get_pos() local node = minetest.get_node(vector.floor(pos)) if node.name == "mcl_minecarts:activator_rail_on" then self:on_activate_by_rail() end end end function cart:on_punch(puncher, time_from_last_punch, tool_capabilities, direction) local staticdata = self._staticdata if not staticdata then staticdata = make_staticdata() self._staticdata = staticdata end local pos = staticdata.connected_at if not pos then pos = self.object:get_pos() -- Try to reattach local rounded_pos = vector.round(pos) if mcl_minecarts:is_rail(rounded_pos) and vector.distance(pos, rounded_pos) < 0.25 then -- Reattach staticdata.connected_at = rounded_pos pos = rounded_pos else mineclone.log("warning","TODO: handle detached cart behavior") end end -- Fix railtype field if not staticdata.railtype then local node = minetest.get_node(vector.floor(pos)).name staticdata.railtype = minetest.get_item_group(node, "connect_to_raillike") end -- Handle punches by something other than the player if not puncher or not puncher:is_player() then local cart_dir = mcl_minecarts:get_rail_direction(pos, vector.new(1,0,0), nil, nil, staticdata.railtype) if vector.equals(cart_dir, vector.new(0,0,0)) then return end staticdata.dir = cart_dir self._punched = true return end -- Punch+sneak: Pick up minecart (unless TNT was ignited) if puncher:get_player_control().sneak and not self._boomtimer then if self._driver then if self._old_pos then self.object:set_pos(self._old_pos) end detach_driver(self) end -- Disable detector rail local rou_pos = vector.round(pos) local node = minetest.get_node(rou_pos) if node.name == "mcl_minecarts:detector_rail_on" then local newnode = {name="mcl_minecarts:detector_rail", param2 = node.param2} minetest.swap_node(rou_pos, newnode) mesecon.receptor_off(rou_pos) end -- Drop items and remove cart entity local drop = def.drop if not minetest.is_creative_enabled(puncher:get_player_name()) then for d=1, #drop do minetest.add_item(self.object:get_pos(), drop[d]) end elseif puncher and puncher:is_player() then local inv = puncher:get_inventory() for d=1, #drop do if not inv:contains_item("main", drop[d]) then inv:add_item("main", drop[d]) end end end self.object:remove() return end -- Handle player punches local vel = self.object:get_velocity() if puncher:get_player_name() == self._driver then if math.abs(vel.x + vel.z) > 7 then return end end local punch_dir = mcl_minecarts:velocity_to_dir(puncher:get_look_dir()) punch_dir.y = 0 local cart_dir = mcl_minecarts:get_rail_direction(pos, punch_dir, nil, nil, self._staticdata.railtype) if vector.equals(cart_dir, vector.new(0,0,0)) then return end staticdata.dir = cart_dir time_from_last_punch = math.min(time_from_last_punch, tool_capabilities.full_punch_interval) local f = 3 * (time_from_last_punch / tool_capabilities.full_punch_interval) -- Perform acceleration here staticdata.velocity = (staticdata.velocity or 0 ) + f local max_vel = mcl_minecarts.speed_max if staticdata.velocity > max_vel then staticdata.velocity = max_vel end end local passenger_attach_position = vector.new(0, -1.75, 0) function cart:add_node_watch(pos) local staticdata = self._staticdata local watches = staticdata.node_watches or {} for _,watch in ipairs(watches) do if watch == pos then return end end watches[#watches+1] = pos staticdata.node_watches = watches end function cart:remove_node_watch(pos) local staticdata = self._staticdata local watches = staticdata.node_watches or {} local new_watches = {} for _,node_pos in ipairs(watches) do if node_pos ~= post then new_watches[#new_watches] = node_pos end end staticdata.node_watches = new_watches end function cart:on_step(dtime) -- TODO: move to _mcl_minecarts_on_step handler and on_enter handler for hopper minecart hopper_take_item(self, dtime) local staticdata = self._staticdata -- Make sure all carts have an ID to isolate them staticdata.cart_id = staticdata.cart_id or math.random(1,1000000000) if not staticdata then staticdata = make_staticdata() self._staticdata = staticdata end if (staticdata.hopper_delay or 0) > 0 then staticdata.hopper_delay = staticdata.hopper_delay - dtime end local pos, rou_pos, node = self.object:get_pos() -- Controls local ctrl, player = nil, nil if self._driver then player = minetest.get_player_by_name(self._driver) if player then ctrl = player:get_player_control() -- player detach if ctrl.sneak then detach_driver(self) return end -- Experimental controls --self._go_forward = ctrl.up --self._brake = ctrl.down end -- Give achievement when player reached a distance of 1000 nodes from the start position if vector.distance(self._start_pos, pos) >= 1000 then awards.unlock(self._driver, "mcl:onARail") end end do_movement(self, dtime) -- TODO: move this into mcl_core:cactus _mcl_minecarts_on_enter_side -- Drop minecart if it collides with a cactus node local r = 0.6 for _, node_pos in pairs({{r, 0}, {0, r}, {-r, 0}, {0, -r}}) do if minetest.get_node(vector.offset(pos, node_pos[1], 0, node_pos[2])).name == "mcl_core:cactus" then detach_driver(self) local drop = def.drop for d = 1, #drop do minetest.add_item(pos, drop[d]) end self.object:remove() return end end -- TODO: move to _mcl_minecarts_on_step handler for plain minecart -- Grab mob if math.random(1,20) > 15 and not self._passenger then if self.name == "mcl_minecarts:minecart" then local mobsnear = minetest.get_objects_inside_radius(self.object:get_pos(), 1.3) for n=1, #mobsnear do local mob = mobsnear[n] if mob then local entity = mob:get_luaentity() if entity and entity.is_mob then self._passenger = entity mob:set_attach(self.object, "", passenger_attach_position, vector.zero()) break end end end end elseif self._passenger then local passenger_pos = self._passenger.object:get_pos() if not passenger_pos then self._passenger = nil end end -- Drop minecart if it isn't on a rail anymore --[[ Remove this entirely once off-cart minecart behavior is implemented if self._last_float_check == nil then self._last_float_check = 0 else self._last_float_check = self._last_float_check + dtime end if self._last_float_check >= mcl_minecarts.check_float_time then pos = self.object:get_pos() rou_pos = vector.round(pos) node = minetest.get_node(rou_pos) local g = minetest.get_item_group(node.name, "connect_to_raillike") if g ~= self._staticdata.railtype and self._staticdata.railtype then -- Detach driver detach_driver(self) -- Explode if already ignited if self._boomtimer then self.object:remove() mcl_explosions.explode(pos, 4, { drop_chance = 1.0 }) return end -- Do not drop minecart. It goes off the rails too frequently, and anyone using them for farms won't -- notice and lose their iron and not bother. Not cool until fixed. end self._last_float_check = 0 end ]] local hook = cart._mcl_minecarts_on_step if hook then hook(cart,dtime) end -- TODO: move the below into cart-specific hooks -- TODO: move to _mcl_minecarts_on_step handler for furnace minecart -- Update furnace stuff if self._fueltime and self._fueltime > 0 then self._fueltime = self._fueltime - dtime if self._fueltime <= 0 then self.object:set_properties({textures = { "default_furnace_top.png", "default_furnace_top.png", "default_furnace_front.png", "default_furnace_side.png", "default_furnace_side.png", "default_furnace_side.png", "mcl_minecarts_minecart.png", }}) self._fueltime = 0 end end local has_fuel = self._fueltime and self._fueltime > 0 -- TODO: move to _mcl_minecarts_on_step handler for TNT minecart -- Update TNT stuff if self._boomtimer then -- Explode self._boomtimer = self._boomtimer - dtime local pos = self.object:get_pos() if self._boomtimer <= 0 then self.object:remove() mcl_explosions.explode(pos, 4, { drop_chance = 1.0 }) return else tnt.smoke_step(pos) end end if self._blinktimer then self._blinktimer = self._blinktimer - dtime if self._blinktimer <= 0 then self._blink = not self._blink if self._blink then self.object:set_properties({textures = { "default_tnt_top.png", "default_tnt_bottom.png", "default_tnt_side.png", "default_tnt_side.png", "default_tnt_side.png", "default_tnt_side.png", "mcl_minecarts_minecart.png", }}) else self.object:set_properties({textures = { "mcl_tnt_blink.png", "mcl_tnt_blink.png", "mcl_tnt_blink.png", "mcl_tnt_blink.png", "mcl_tnt_blink.png", "mcl_tnt_blink.png", "mcl_minecarts_minecart.png", }}) end self._blinktimer = tnt.BLINKTIMER end end end function cart:get_staticdata() return minetest.serialize(self._staticdata or {}) end minetest.register_entity(entity_id, cart) end -- Place a minecart at pointed_thing function mcl_minecarts.place_minecart(itemstack, pointed_thing, placer) if not pointed_thing.type == "node" then return end local railpos, node if mcl_minecarts:is_rail(pointed_thing.under) then railpos = pointed_thing.under node = minetest.get_node(pointed_thing.under) elseif mcl_minecarts:is_rail(pointed_thing.above) then railpos = pointed_thing.above node = minetest.get_node(pointed_thing.above) else return end local entity_id = entity_mapping[itemstack:get_name()] local cart = minetest.add_entity(railpos, entity_id) local railtype = minetest.get_item_group(node.name, "connect_to_raillike") local cart_dir = mcl_minecarts:get_rail_direction(railpos, vector.new(1,0,0), nil, nil, railtype) cart:set_yaw(minetest.dir_to_yaw(cart_dir)) -- Call placer if cart._mcl_minecarts_on_place then cart._mcl_minecarts_on_place(self, placer) end -- Update static data local le = cart:get_luaentity() if le then le._staticdata = make_staticdata( railtype, railpos, cart_dir ) end handle_cart_enter(le, railpos) local pname = "" if placer then pname = placer:get_player_name() end if not minetest.is_creative_enabled(pname) then itemstack:take_item() end return itemstack end local function dropper_place_minecart(dropitem, pos) local node = minetest.get_node(pos) local nodedef = minetest.registered_nodes[node.name] -- Don't try to place the minecart if pos isn't a rail if (nodedef.groups.rail or 0) == 0 then return false end mcl_minecarts.place_minecart(dropitem, { above = pos, under = vector.offset(pos,0,-1,0) }) return true end local function register_craftitem(itemstring, entity_id, description, tt_help, longdesc, usagehelp, icon, creative) entity_mapping[itemstring] = entity_id local groups = { minecart = 1, transport = 1 } if creative == false then groups.not_in_creative_inventory = 1 end local def = { stack_max = 1, _mcl_dropper_on_drop = dropper_place_minecart, on_place = function(itemstack, placer, pointed_thing) if not pointed_thing.type == "node" then return end -- Call on_rightclick if the pointed node defines it local node = minetest.get_node(pointed_thing.under) if placer and not placer:get_player_control().sneak then if minetest.registered_nodes[node.name] and minetest.registered_nodes[node.name].on_rightclick then return minetest.registered_nodes[node.name].on_rightclick(pointed_thing.under, node, placer, itemstack) or itemstack end end return mcl_minecarts.place_minecart(itemstack, pointed_thing, placer) end, _on_dispense = function(stack, pos, droppos, dropnode, dropdir) -- Place minecart as entity on rail. If there's no rail, just drop it. local placed if minetest.get_item_group(dropnode.name, "rail") ~= 0 then -- FIXME: This places minecarts even if the spot is already occupied local pointed_thing = { under = droppos, above = vector.new( droppos.x, droppos.y+1, droppos.z ) } placed = mcl_minecarts.place_minecart(stack, pointed_thing) end if placed == nil then -- Drop item minetest.add_item(droppos, stack) end end, groups = groups, } def.description = description def._tt_help = tt_help def._doc_items_longdesc = longdesc def._doc_items_usagehelp = usagehelp def.inventory_image = icon def.wield_image = icon minetest.register_craftitem(itemstring, def) end --[[ Register a minecart * itemstring: Itemstring of minecart item * entity_id: ID of minecart entity * description: Item name / description * longdesc: Long help text * usagehelp: Usage help text * mesh: Minecart mesh * textures: Minecart textures table * icon: Item icon * drop: Dropped items after destroying minecart * on_rightclick: Called after rightclick * on_activate_by_rail: Called when above activator rail * creative: If false, don't show in Creative Inventory ]] local function register_minecart(def) register_entity(def.entity_id, def) register_craftitem(def.itemstring, def.entity_id, def.description, def.tt_help, def.longdesc, def.usagehelp, def.icon, def.creative) if minetest.get_modpath("doc_identifier") then doc.sub.identifier.register_object(def.entity_id, "craftitems", itemstring) end end -- Minecart register_minecart({ itemstring = "mcl_minecarts:minecart", entity_id = "mcl_minecarts:minecart", description = S("Minecart"), tt_helop = S("Vehicle for fast travel on rails"), long_descp = S("Minecarts can be used for a quick transportion on rails.") .. "\n" .. S("Minecarts only ride on rails and always follow the tracks. At a T-junction with no straight way ahead, they turn left. The speed is affected by the rail type."), S("You can place the minecart on rails. Right-click it to enter it. Punch it to get it moving.") .. "\n" .. S("To obtain the minecart, punch it while holding down the sneak key.") .. "\n" .. S("If it moves over a powered activator rail, you'll get ejected."), mesh = "mcl_minecarts_minecart.b3d", textures = {"mcl_minecarts_minecart.png"}, icon = "mcl_minecarts_minecart_normal.png", drop = {"mcl_minecarts:minecart"}, on_rightclick = function(self, clicker) local name = clicker:get_player_name() if not clicker or not clicker:is_player() then return end local player_name = clicker:get_player_name() if self._driver and player_name == self._driver then --detach_driver(self) elseif not self._driver and not clicker:get_player_control().sneak then self._driver = player_name self._start_pos = self.object:get_pos() mcl_player.player_attached[player_name] = true clicker:set_attach(self.object, "", vector.new(1,-1.75,-2), vector.new(0,0,0)) mcl_player.player_attached[name] = true minetest.after(0.2, function(name) local player = minetest.get_player_by_name(name) if player then mcl_player.player_set_animation(player, "sit" , 30) player:set_eye_offset(vector.new(0,-5.5,0), vector.new(0,-4,0)) mcl_title.set(clicker, "actionbar", {text=S("Sneak to dismount"), color="white", stay=60}) end end, name) end end, on_activate_by_rail = activate_normal_minecart }) -- Minecart with Chest register_minecart({ itemstring = "mcl_minecarts:chest_minecart", entity_id = "mcl_minecarts:chest_minecart", description = S("Minecart with Chest"), tt_help = nil, longdesc = nil, usagehelp = nil, mesh = "mcl_minecarts_minecart_chest.b3d", textures = { "mcl_chests_normal.png", "mcl_minecarts_minecart.png" }, icon = "mcl_minecarts_minecart_chest.png", drop = {"mcl_minecarts:minecart", "mcl_chests:chest"}, groups = { container = 1 }, on_rightclick = nil, on_activate_by_rail = nil, creative = true }) mcl_entity_invs.register_inv("mcl_minecarts:chest_minecart","Minecart",27,false,true) -- Minecart with Furnace register_minecart({ itemstring = "mcl_minecarts:furnace_minecart", entity_id = "mcl_minecarts:furnace_minecart", description = S("Minecart with Furnace"), tt_help = nil, longdesc = S("A minecart with furnace is a vehicle that travels on rails. It can propel itself with fuel."), usagehelp = S("Place it on rails. If you give it some coal, the furnace will start burning for a long time and the minecart will be able to move itself. Punch it to get it moving.") .. "\n" .. S("To obtain the minecart and furnace, punch them while holding down the sneak key."), mesh = "mcl_minecarts_minecart_block.b3d", textures = { "default_furnace_top.png", "default_furnace_top.png", "default_furnace_front.png", "default_furnace_side.png", "default_furnace_side.png", "default_furnace_side.png", "mcl_minecarts_minecart.png", }, icon = "mcl_minecarts_minecart_furnace.png", drop = {"mcl_minecarts:minecart", "mcl_furnaces:furnace"}, on_rightclick = function(self, clicker) -- Feed furnace with coal if not clicker or not clicker:is_player() then return end if not self._fueltime then self._fueltime = 0 end local held = clicker:get_wielded_item() if minetest.get_item_group(held:get_name(), "coal") == 1 then self._fueltime = self._fueltime + 180 if not minetest.is_creative_enabled(clicker:get_player_name()) then held:take_item() local index = clicker:get_wield_index() local inv = clicker:get_inventory() inv:set_stack("main", index, held) end self.object:set_properties({textures = { "default_furnace_top.png", "default_furnace_top.png", "default_furnace_front_active.png", "default_furnace_side.png", "default_furnace_side.png", "default_furnace_side.png", "mcl_minecarts_minecart.png", }}) end end, on_activate_by_rail = nil, creative = true }) -- Minecart with Command Block register_minecart({ itemstring = "mcl_minecarts:command_block_minecart", entity_id = "mcl_minecarts:command_block_minecart", description = S("Minecart with Command Block"), tt_help = nil, loncdesc = nil, usagehelp = nil, mesh = "mcl_minecarts_minecart_block.b3d", textures = { "jeija_commandblock_off.png^[verticalframe:2:0", "jeija_commandblock_off.png^[verticalframe:2:0", "jeija_commandblock_off.png^[verticalframe:2:0", "jeija_commandblock_off.png^[verticalframe:2:0", "jeija_commandblock_off.png^[verticalframe:2:0", "jeija_commandblock_off.png^[verticalframe:2:0", "mcl_minecarts_minecart.png", }, icon = "mcl_minecarts_minecart_command_block.png", drop = {"mcl_minecarts:minecart"}, on_rightclick = nil, _mcl_minecarts_on_place = function(self, placer) if mesecon and mesecon.command_block then mesecons.command_block.configure_entity(self, placer) end end, on_activate_by_rail = function(self, timer) if mesecon and mesecond.command_block then mesecons.command_block.resolve_command_entity(self) end end, creative = false }) -- Minecart with Hopper register_minecart({ itemstring = "mcl_minecarts:hopper_minecart", entity_id = "mcl_minecarts:hopper_minecart", description = S("Minecart with Hopper"), tt_help = nil, longdesc = nil, usagehelp = nil, mesh = "mcl_minecarts_minecart_hopper.b3d", textures = { "mcl_hoppers_hopper_inside.png", "mcl_minecarts_minecart.png", "mcl_hoppers_hopper_outside.png", "mcl_hoppers_hopper_top.png", }, icon = "mcl_minecarts_minecart_hopper.png", drop = {"mcl_minecarts:minecart", "mcl_hoppers:hopper"}, groups = { container = 1 }, on_rightclick = nil, on_activate_by_rail = nil, _mcl_minecarts_on_enter = function(self,pos) local staticdata = self._staticdata if (staticdata.hopper_delay or 0) > 0 then return end -- try to pull from containers into our inventory local inv = mcl_entity_invs.load_inv(self,5) local above_pos = pos + vector.new(0,1,0) mcl_util.hopper_pull_to_inventory(inv, 'main', above_pos) staticdata.hopper_delay = (staticdata.hopper_delay or 0) + (1/20) end, creative = true }) mcl_entity_invs.register_inv("mcl_minecarts:hopper_minecart", "Hopper Minecart", 5, false, true) -- Minecart with TNT register_minecart({ itemstring = "mcl_minecarts:tnt_minecart", entity_id = "mcl_minecarts:tnt_minecart", description = S("Minecart with TNT"), tt_help = S("Vehicle for fast travel on rails").."\n"..S("Can be ignited by tools or powered activator rail"), longdesc = S("A minecart with TNT is an explosive vehicle that travels on rail."), usagehelp = S("Place it on rails. Punch it to move it. The TNT is ignited with a flint and steel or when the minecart is on an powered activator rail.") .. "\n" .. S("To obtain the minecart and TNT, punch them while holding down the sneak key. You can't do this if the TNT was ignited."), mesh = "mcl_minecarts_minecart_block.b3d", textures = { "default_tnt_top.png", "default_tnt_bottom.png", "default_tnt_side.png", "default_tnt_side.png", "default_tnt_side.png", "default_tnt_side.png", "mcl_minecarts_minecart.png", }, icon = "mcl_minecarts_minecart_tnt.png", drop = {"mcl_minecarts:minecart", "mcl_tnt:tnt"}, on_rightclick = function(self, clicker) -- Ingite if not clicker or not clicker:is_player() then return end if self._boomtimer then return end local held = clicker:get_wielded_item() if held:get_name() == "mcl_fire:flint_and_steel" then if not minetest.is_creative_enabled(clicker:get_player_name()) then held:add_wear(65535/65) -- 65 uses local index = clicker:get_wield_index() local inv = clicker:get_inventory() inv:set_stack("main", index, held) end activate_tnt_minecart(self) end end, on_activate_by_rail = activate_tnt_minecart, creative = true }) minetest.register_craft({ output = "mcl_minecarts:minecart", recipe = { {"mcl_core:iron_ingot", "", "mcl_core:iron_ingot"}, {"mcl_core:iron_ingot", "mcl_core:iron_ingot", "mcl_core:iron_ingot"}, }, }) minetest.register_craft({ output = "mcl_minecarts:tnt_minecart", recipe = { {"mcl_tnt:tnt"}, {"mcl_minecarts:minecart"}, }, }) minetest.register_craft({ output = "mcl_minecarts:furnace_minecart", recipe = { {"mcl_furnaces:furnace"}, {"mcl_minecarts:minecart"}, }, }) minetest.register_craft({ output = "mcl_minecarts:hopper_minecart", recipe = { {"mcl_hoppers:hopper"}, {"mcl_minecarts:minecart"}, }, }) minetest.register_craft({ output = "mcl_minecarts:chest_minecart", recipe = { {"mcl_chests:chest"}, {"mcl_minecarts:minecart"}, }, }) if has_mcl_wip then mcl_wip.register_wip_item("mcl_minecarts:chest_minecart") mcl_wip.register_wip_item("mcl_minecarts:furnace_minecart") mcl_wip.register_wip_item("mcl_minecarts:command_block_minecart") end