--[[ Helpers ]]-- -- Skin mod detection local skin_mod local skin_mods = {"skinsdb", "skins", "u_skins", "simple_skins", "wardrobe"} for _, mod in pairs(skin_mods) do local path = minetest.get_modpath(mod) if path then skin_mod = mod end end local function get_player_skin(player) local name = player:get_player_name() local armor_tex = "" if minetest.global_exists("armor") then -- Filter out helmet (for bike helmet) and boots/shield (to not mess up UV mapping) local function filter(str, find) for _,f in pairs(find) do str = str:gsub("%^"..f.."_(.-.png)", "") end return str end armor_tex = filter("^"..armor.textures[name].armor, {"shields_shield", "3d_armor_boots", "3d_armor_helmet"}) end -- Return the skin with armor (if applicable) if skin_mod == "skinsdb" then return "[combine:64x32:0,0="..skins.get_player_skin(minetest.get_player_by_name(name))["_texture"]..armor_tex elseif (skin_mod == "skins" or skin_mod == "simple_skins") and skins.skins[name] then return skins.skins[name]..".png"..armor_tex elseif skin_mod == "u_skins" and u_skins.u_skins[name] then return u_skins.u_skins[name]..".png"..armor_tex elseif skin_mod == "wardrobe" and wardrobe.playerSkins and wardrobe.playerSkins[name] then return wardrobe.playerSkins[name]..armor_tex end local skin = player:get_properties().textures[1] -- If we just have 3d_armor enabled make sure we get the player skin properly if minetest.global_exists("armor") then skin = armor:get_player_skin(name) end return skin..armor_tex end -- Bike metal texture handling local function is_hex(color) return color:match("#%x%x%x%x%x%x") end local function colormetal(color, alpha) return "metal_base.png^[colorize:"..(color)..":"..tostring(alpha) end -- Keep track of attached players (for leaveplayer) local attached = {} -- Terrain checkers local function is_water(pos) local nn = minetest.get_node(pos).name return minetest.get_item_group(nn, "liquid") ~= 0 end local function is_bike_friendly(pos) local nn = minetest.get_node(pos).name return minetest.get_item_group(nn, "crumbly") == 0 or minetest.get_item_group(nn, "bike_friendly") ~= 0 end -- Maths local function get_sign(i) if i == 0 then return 0 else return i / math.abs(i) end end local function get_velocity(v, yaw, y) local x = -math.sin(yaw) * v local z = math.cos(yaw) * v return {x = x, y = y, z = z} end local function get_v(v) return math.sqrt(v.x ^ 2 + v.z ^ 2) end -- Custom hand minetest.register_node("bike:hand", { description = "", -- No interaction on a bike :) range = 0, on_place = function(itemstack, placer, pointed_thing) return ItemStack("bike:hand "..itemstack:get_count()) end, -- Copy default:hand looks so it doesnt look as weird when the hands are switched wield_image = minetest.registered_items[""].wield_image, wield_scale = minetest.registered_items[""].wield_scale, node_placement_prediction = "", }) --[[ Bike ]]-- -- Default textures (overidden when mounted or colored) local function default_tex(metaltex, alpha) return { "metal_grey.png", "gear.png", colormetal(metaltex, alpha), "leather.png", "chain.png", "metal_grey.png", "leather.png", "metal_black.png", "metal_black.png", "blank.png", "tread.png", "gear.png", "spokes.png", "tread.png", "spokes.png", } end -- Entity local bike = { physical = true, -- Warning: Do not change the position of the collisionbox top surface, -- lowering it causes the bike to fall through the world if underwater collisionbox = {-0.5, -0.4, -0.5, 0.5, 0.8, 0.5}, collide_with_objects = false, visual = "mesh", mesh = "bike.b3d", textures = default_tex("#FFFFFF", 150), stepheight = 0.6, driver = nil, color = "#FFFFFF", alpha = 150, old_driver = {}, v = 0, -- Current velocity last_v = 0, -- Last velocity max_v = 6.9, -- Max velocity fast_v = 0, -- Fast adder f_speed = 30, -- Frame speed last_y = 0, -- Last height up = false, -- Are we going up? timer = 0, removed = false } -- Dismont the player local function dismount_player(bike, exit) bike.object:set_velocity({x = 0, y = 0, z = 0}) -- Make the bike empty again bike.object:set_properties({textures = default_tex(bike.color, bike.alpha)}) bike.v = 0 if bike.driver then attached[bike.driver:get_player_name()] = nil bike.driver:set_detach() -- Reset original player properties bike.driver:set_properties({visual_size=bike.old_driver["vsize"]}) bike.driver:set_eye_offset(bike.old_driver["eye_offset"].offset_first, bike.old_driver["eye_offset"].offset_third) bike.driver:hud_set_flags(bike.old_driver["hud"]) bike.driver:get_inventory():set_stack("hand", 1, bike.driver:get_inventory():get_stack("old_hand", 1)) -- Is the player leaving? If so, dont do this stuff or Minetest will have a fit if not exit then local pos = bike.driver:get_pos() pos = {x = pos.x, y = pos.y + 0.2, z = pos.z} bike.driver:set_pos(pos) end bike.driver = nil end end -- Mounting function bike.on_rightclick(self, clicker) if not clicker or not clicker:is_player() then return end if not self.driver then attached[clicker:get_player_name()] = true -- Make integrated player appear self.object:set_properties({ textures = { "metal_grey.png", "gear.png", colormetal(self.color, self.alpha), "leather.png", "chain.png", "metal_grey.png", "leather.png", "metal_black.png", "metal_black.png", get_player_skin(clicker).."^helmet.png", "tread.png", "gear.png", "spokes.png", "tread.png", "spokes.png", }, }) -- Save the player's properties that we need to change self.old_driver["vsize"] = clicker:get_properties().visual_size self.old_driver["eye_offset"] = clicker:get_eye_offset() self.old_driver["hud"] = clicker:hud_get_flags() clicker:get_inventory():set_stack("old_hand", 1, clicker:get_inventory():get_stack("hand", 1)) -- Change the hand clicker:get_inventory():set_stack("hand", 1, "bike:hand") local attach = clicker:get_attach() if attach and attach:get_luaentity() then local luaentity = attach:get_luaentity() if luaentity.driver then luaentity.driver = nil end clicker:set_detach() end self.driver = clicker -- Set new properties and hide HUD clicker:set_properties({visual_size = {x=0,y=0}}) clicker:set_attach(self.object, "body", {x = 0, y = 10, z = 5}, {x = 0, y = 0, z = 0}) clicker:set_eye_offset({x=0,y=-3,z=10},{x=0,y=0,z=5}) clicker:hud_set_flags({ hotbar = false, wielditem = false, }) -- Look forward initially clicker:set_look_horizontal(self.object:get_yaw()) end end function bike.on_activate(self, staticdata, dtime_s) self.object:set_acceleration({x = 0, y = -9.8, z = 0}) self.object:set_armor_groups({immortal = 1}) if staticdata ~= "" then local data = minetest.deserialize(staticdata) if data ~= nil then self.v = data.v self.color = data.color self.alpha = data.alpha end end self.object:set_properties({textures=default_tex(self.color, self.alpha)}) self.last_v = self.v end -- Save velocity and color data for reload function bike.get_staticdata(self) local data = {v=self.v,color=self.color,alpha=self.alpha} return minetest.serialize(data) end -- Pick up/color function bike.on_punch(self, puncher) local itemstack = puncher:get_wielded_item() -- Bike painting if itemstack:get_name() == "bike:painter" then -- No painting while someone is riding :P if self.driver then return end -- Get color data local meta = itemstack:get_meta() self.color = meta:get_string("paint_color") self.alpha = meta:get_string("alpha") self.object:set_properties({ textures = { "metal_grey.png", "gear.png", colormetal(self.color, self.alpha), "leather.png", "chain.png", "metal_grey.png", "leather.png", "metal_black.png", "metal_black.png", "blank.png", "tread.png", "gear.png", "spokes.png", "tread.png", "spokes.png", }, }) return end if not puncher or not puncher:is_player() or self.removed then return end -- Make sure no one is riding if not self.driver then local inv = puncher:get_inventory() -- We can only carry one bike if not inv:contains_item("main", "bike:bike") then local stack = ItemStack({name="bike:bike", count=1, wear=0}) local meta = stack:get_meta() -- Set the stack to the bike color meta:set_string("color", self.color) meta:set_string("alpha", self.alpha) local leftover = inv:add_item("main", stack) -- If no room in inventory add the bike to the world if not leftover:is_empty() then minetest.add_item(self.object:get_pos(), leftover) end else -- Turn it into raw materials if not (creative and creative.is_enabled_for(puncher:get_player_name())) then local ctrl = puncher:get_player_control() if not ctrl.sneak then minetest.chat_send_player(puncher:get_player_name(), "Warning: Destroying the bike gives you only some resources back. If you are sure, hold sneak while destroying the bike.") return end local leftover = inv:add_item("main", "default:steel_ingot 6") -- If no room in inventory add the iron to the world if not leftover:is_empty() then minetest.add_item(self.object:get_pos(), leftover) end end end self.removed = true -- Delay remove to ensure player is detached minetest.after(0.1, function() self.object:remove() end) end end -- Animations local function bike_anim(self) -- The `self.object:get_animation().y ~= ` is to check if the animation is already running if self.driver then local ctrl = self.driver:get_player_control() -- Wheely if ctrl.jump then -- We are moving if self.v > 0 then if self.object:get_animation().y ~= 79 then self.object:set_animation({x=59,y=79}, self.f_speed + self.fast_v, 0, true) end return -- Else we are not else if self.object:get_animation().y ~= 59 then self.object:set_animation({x=59,y=59}, self.f_speed + self.fast_v, 0, true) end return end end -- Left or right tilt, but only if we arent doing a wheely if ctrl.left then if self.object:get_animation().y ~= 58 then self.object:set_animation({x=39,y=58}, self.f_speed + self.fast_v, 0, true) end return elseif ctrl.right then if self.object:get_animation().y ~= 38 then self.object:set_animation({x=19,y=38}, self.f_speed + self.fast_v, 0, true) end return end end -- If none of that, then we are just moving forward if self.v > 0 then if self.object:get_animation().y ~= 18 then self.object:set_animation({x=0,y=18}, 30, 0, true) end return -- Or not else if self.object:get_animation().y ~= 0 then self.object:set_animation({x=0,y=0}, 0, 0, false) end end end -- Run every tick function bike.on_step(self, dtime) -- Player checks if self.driver then -- Is the actual player somehow still visible? if self.driver:get_properties().visual_size ~= {x=0,y=0} then self.driver:set_properties({visual_size = {x=0,y=0}}) end -- Has the player left? if not attached[self.driver:get_player_name()] then dismount_player(self, true) end end -- Have we come to a sudden stop? if math.abs(self.last_v - self.v) > 3 then -- And is Minetest not being dumb if not self.up then self.v = 0 -- If so, dismount if self.driver then dismount_player(self) end end end self.last_v = self.v self.timer = self.timer + dtime; if self.timer >= 0.5 then -- Recording y values to check if we are going up self.last_y = self.object:get_pos().y self.timer = 0 end -- Are we going up? if self.last_y < self.object:get_pos().y then self.up = true else self.up = false end -- Run animations bike_anim(self) -- Are we falling? if self.object:get_velocity().y < -10 and self.driver ~= nil then -- If so, dismount dismount_player(self) return end local current_v = get_v(self.object:get_velocity()) * get_sign(self.v) self.v = (current_v + self.v*3) / 4 if self.driver then local ctrl = self.driver:get_player_control() local yaw = self.object:get_yaw() local agility = 0 -- Sneak dismount if ctrl.sneak then dismount_player(self) end if self.v > 0.4 then agility = 1/math.sqrt(self.v) else agility = 1.58 end -- Forward if ctrl.up then -- Are we going fast? if ctrl.aux1 then if self.fast_v ~= 5 then self.fast_v = 5 end else if self.fast_v > 0 then self.fast_v = self.fast_v - 0.05 * agility end end self.v = self.v + 0.2 + (self.fast_v*0.1) * agility -- Brakes elseif ctrl.down then self.v = self.v - 0.5 * agility if self.fast_v > 0 then self.fast_v = self.fast_v - 0.05 * agility end -- Nothin' else self.v = self.v - 0.05 * agility if self.fast_v > 0 then self.fast_v = self.fast_v - 0.05 * agility end end -- Wheely will change turning speed local turn_speed = 1 -- Are we doing a wheely? if ctrl.jump then turn_speed = 2 else turn_speed = 1 end -- Turning if ctrl.left then self.object:set_yaw(yaw + (turn_speed + dtime) * 0.06 * agility) elseif ctrl.right then self.object:set_yaw(yaw - (turn_speed + dtime) * 0.06 * agility) end end -- Movement local velo = self.object:get_velocity() if self.v == 0 and velo.x == 0 and velo.y == 0 and velo.z == 0 then self.object:move_to(self.object:get_pos()) return end local s = get_sign(self.v) if s ~= get_sign(self.v) then self.object:set_velocity({x = 0, y = 0, z = 0}) self.v = 0 return end if self.v > self.max_v + self.fast_v then self.v = self.max_v + self.fast_v elseif self.v < 0 then self.v = 0 end local p = self.object:get_pos() if is_water(p) then self.v = self.v / 1.3 end -- Can we ride good here? if not is_bike_friendly({x=p.x, y=p.y + self.collisionbox[2] - 0.05, z=p.z}) then self.v = self.v / 1.05 end local new_velo new_velo = get_velocity(self.v, self.object:get_yaw(), self.object:get_velocity().y) self.object:move_to(self.object:get_pos()) self.object:set_velocity(new_velo) end -- Check for stray bike hand minetest.register_on_joinplayer(function(player) local inv = player:get_inventory() if inv:get_stack("hand", 1):get_name() == "bike:hand" then inv:set_stack("hand", 1, inv:get_stack("old_hand", 1)) end end) -- Player is leaving (doesn't matter if they are on a bike or not) minetest.register_on_leaveplayer(function(player) attached[player:get_player_name()] = nil end) -- Dismount all players on server shutdown minetest.register_on_shutdown(function() for _, e in pairs(minetest.luaentities) do if (e.driver ~= nil) then dismount_player(e, true) end end end) -- Automatically dismount corpses minetest.register_on_dieplayer(function(player) attached[player:get_player_name()] = nil end) -- Register the entity minetest.register_entity("bike:bike", bike) -- Bike craftitem minetest.register_craftitem("bike:bike", { description = "Bike", inventory_image = "bike_inventory.png", wield_scale = {x = 3, y = 3, z = 2}, groups = {flammable = 2}, stack_max = 1, on_place = function(itemstack, placer, pointed_thing) local under = pointed_thing.under local node = minetest.get_node(under) local udef = minetest.registered_nodes[node.name] if udef and udef.on_rightclick and not (placer and placer:is_player() and placer:get_player_control().sneak) then return udef.on_rightclick(under, node, placer, itemstack, pointed_thing) or itemstack end if pointed_thing.type ~= "node" then return itemstack end -- Place bike with saved color local meta = itemstack:get_meta() local color = meta:get_string("color") local alpha = tonumber(meta:get_string("alpha")) -- If it's a new bike, give it default colors if alpha == nil then color, alpha = "#FFFFFF", 150 end bike_pos = placer:get_pos() bike_pos.y = bike_pos.y + 0.5 -- Use the saved color data and place the bike bike = minetest.add_entity(bike_pos, "bike:bike", minetest.serialize({v=0,color=color,alpha=alpha})) -- Point it the right direction if bike then if placer then bike:set_yaw(placer:get_look_horizontal()) end local player_name = placer and placer:get_player_name() or "" if not (creative and creative.is_enabled_for and creative.is_enabled_for(player_name)) then itemstack:take_item() end end return itemstack end, }) --[[ Painter ]]-- -- Helpers local function rgb_to_hex(r, g, b) return string.format("#%02X%02X%02X", r, g, b) end local function hex_to_rgb(hex) hex = hex:gsub("#","") local rgb = { r = tonumber("0x"..hex:sub(1,2)), g = tonumber("0x"..hex:sub(3,4)), b = tonumber("0x"..hex:sub(5,6)), } return rgb end -- Need to convert between 1000 units and 256 local function from_slider_rgb(value) value = tonumber(value) return math.floor((255/1000*value)+0.5) end -- ...and back local function to_slider_rgb(value) return 1000/255*value end -- Painter formspec local function show_painter_form(itemstack, player) local meta = itemstack:get_meta() local color = meta:get_string("paint_color") local alpha = tonumber(meta:get_string("alpha")) if alpha == nil then color, alpha = "#FFFFFF", 128 end local rgba = hex_to_rgb(color) rgba.a = alpha minetest.show_formspec(player:get_player_name(), "bike:painter", -- Init formspec "size[6,6;true]".. "position[0.5, 0.45]".. -- Hex/Alpha fields "button[1.6,5.5;2,1;set;Set paint color]".. "field[0.9,5;2,0.8;hex;Hex Color;"..color.."]".. "field[2.9,5;2,0.8;alpha;Alpha (0-255);"..tostring(alpha).."]".. -- RGBA sliders "scrollbar[0,2;5,0.3;horizontal;r;"..tostring(to_slider_rgb(rgba.r)).."]".. "label[5.1,1.9;R: "..tostring(rgba.r).."]".. "scrollbar[0,2.6;5,0.3;horizontal;g;"..tostring(to_slider_rgb(rgba.g)).."]".. "label[5.1,2.5;G: "..tostring(rgba.g).."]".. "scrollbar[0,3.2;5,0.3;horizontal;b;"..tostring(to_slider_rgb(rgba.b)).."]".. "label[5.1,3.1;B: "..tostring(rgba.b).."]".. "scrollbar[0,3.8;5,0.3;horizontal;a;"..tostring(to_slider_rgb(rgba.a)).."]".. "label[5.1,3.7;A: "..tostring(rgba.a).."]".. -- Preview "label[1,0;Preview:]".. "image[2,0;2,2;metal_base.png^[colorize:"..color..":"..tostring(rgba.a).."]" ) end minetest.register_on_player_receive_fields(function(player, formname, fields) if formname == "bike:painter" then local itemstack = player:get_wielded_item() if fields.set then if itemstack:get_name() == "bike:painter" then local meta = itemstack:get_meta() local hex = fields.hex local alpha = tonumber(fields.alpha) if is_hex(hex) == nil then hex = "#FFFFFF" end if alpha < 0 or alpha > 255 then alpha = 128 end -- Save color data to painter (rgba sliders will adjust to hex/alpha too!) meta:set_string("paint_color", hex) meta:set_string("alpha", tostring(alpha)) meta:set_string("description", "Bike Painter ("..hex:upper()..", A: "..tostring(alpha)..")") player:set_wielded_item(itemstack) show_painter_form(itemstack, player) return end end if fields.r or fields.g or fields.b or fields.a then if itemstack:get_name() == "bike:painter" then -- Save on slider adjustment (hex/alpha will adjust to match the rgba!) local meta = itemstack:get_meta() local function sval(value) return from_slider_rgb(value:gsub(".*:", "")) end meta:set_string("paint_color", rgb_to_hex(sval(fields.r),sval(fields.g),sval(fields.b))) meta:set_string("alpha", sval(fields.a)) -- Keep track of what this painter is painting meta:set_string("description", "Bike Painter ("..meta:get_string("paint_color"):upper()..", A: "..meta:get_string("alpha")..")") player:set_wielded_item(itemstack) show_painter_form(itemstack, player) end end end end) -- Make the actual thingy minetest.register_tool("bike:painter", { description = "Bike Painter", inventory_image = "bike_painter.png", wield_scale = {x = 2, y = 2, z = 1}, on_place = show_painter_form, on_secondary_use = show_painter_form, }) --[[ Crafts ]]-- minetest.register_craftitem("bike:wheel", { description = "Bike Wheel", inventory_image = "bike_wheel.png", }) minetest.register_craftitem("bike:handles", { description = "Bike Handles", inventory_image = "bike_handles.png", }) -- To rubber, or not to rubber. That is the question. local rubber if minetest.get_modpath("technic") ~= nil then rubber = "technic:rubber" else rubber = "group:wood" end minetest.register_craft({ output = "bike:wheel 2", recipe = { {"", rubber, ""}, {rubber, "default:steel_ingot", rubber}, {"", rubber, ""}, }, }) minetest.register_craft({ output = "bike:handles", recipe = { {"default:steel_ingot", "default:steel_ingot", "default:steel_ingot"}, {rubber, "", rubber}, }, }) minetest.register_craft({ output = "bike:bike", recipe = { {"bike:handles", "", rubber}, {"default:steel_ingot", "default:steel_ingot", "default:steel_ingot"}, {"bike:wheel", "", "bike:wheel"}, }, }) -- Because not everyone likes vessels local container if minetest.get_modpath("vessels") ~= nil then container = "vessels:glass_bottle" else container = "default:glass" end minetest.register_craft({ output = "bike:painter", recipe = { {"", container, ""}, {"default:steel_ingot", "dye:red", "dye:green"}, {"", rubber, "dye:blue"}, }, })