commit 86fc1975beb0e94abea89d341465c548f646265c Author: sys4-fr Date: Sat Sep 8 14:29:29 2018 +0200 Remplissage du dépôt. diff --git a/README.md b/README.md new file mode 100755 index 0000000..5e5d8e6 --- /dev/null +++ b/README.md @@ -0,0 +1,98 @@ +Player Classes +============== + +Yet another class mod for Minetest. + +# TODO + - Flush and read system for `pclasses.datas.players` + + +# Namespaces + +## pclasses + - All our stuff + +### pclasses.api + - All functions used to declare, get, set classes + +### pclasses.api.util + - Some utility functions + +### pclasses.conf + - Some configuration values + +### pclasses.classes + - All classes and their specs + +### pclasses.data + - Miscellaneous data + +#### pclasses.data.players + - List of all players' class. Index is player's name and value is the class's name + +#### pclasses.data.hud_ids + - Surely useful in the future with a hypothetical hud to show current class + + +# Functions + +### pclasses.api.register_class + - Arguments : cname, def + - Registers a class and its specifications + - Def is a definition table that can contain many functions/values : + - `on_assigned` which is a function, receiving as argument the player name + - `on_unassigned` which is a function, receiving as argument the player name + - `on_update` which is a function, receiving as argument the player name + - `switch_params`, which is a table, containing parameters for the switch pedestal : + - `holo_item` is mandatory. It's the itemstring of the item to be put over the pedestal + - `color` is optional. Default is white. It's a RGB table. + - `tile` is optional. Default is none. It's a string of the texture to be applied over the pedestal + +### pclasses.register_class_switch + - Arguments : cname, params + - Used internally to create switch pedestals + - `params` is the `def` table given to `pclasses.api.register_class`, documented above + +### pclasses.api.get_class_by_name + - Argument : cname + - Return the class' specs (table) corresponding a class name or nil if not found + +### pclasses.api.get_player_class + - Argument : pname (player's name) + - Return the player's current class' name + +### pclasses.api.get_class_players + - Argument : cname + - Return a list (table) of all players with class cname + +### pclasses.api.set_player_class + - Arguments : pname, cname + - Assign a player the cname class + - Returns true if achieved, false if not + +### pclasses.api.util.does_wear_full_armor + - Arguments : pname, material, noshield + - Returns true if player `pname` is wearing the full armor made out of `material` + - `noshield` must be true when the full armor has no shield + +### pclasses.api.util.can_have_item + - Arguments : pname, itemname + - Returns true if player `pname` can have items `itemstring` in his main inventory, according to his class + +### pclasses.api.util.on_update + - Arguments : pname + - Update player's stats + +### pclasses.api.reserve_item + - Arguments : cname, itemstring + - Adds an entry in the reserved items' table. Players will need to belong to class `cname` in order to have items `itemstring` in their main inventory + - Note : You can reserve the same item for two classes, any player of either of both can then have the item + +### pclasses.api.create_graveyard_inventory + - Argument : player + - Creates a detached inventory dedicated to 'dead' items (confiscated reserved items) + - Used internally, should not be used outside of pclasses + +### pclasses.api.vacuum_graveyard + - Argument : player + - Check all of `player`'s graveyard inventory to get them back items they obtained to right to have diff --git a/adventurer.lua b/adventurer.lua new file mode 100755 index 0000000..0a56926 --- /dev/null +++ b/adventurer.lua @@ -0,0 +1,28 @@ +----------------------------- +-- Boilerplate class +-- + +pclasses.api.register_class("adventurer", { + switch_params = { + color = { r = 142, g = 64, b = 00}, + tile = "wool_white.png", + holo_item = "unified_inventory:bag_large" + }, + on_assigned = function(pname, inform) + if inform then + minetest.chat_send_player(pname, "You are now an adventurer") + end + end, + on_unassigned = function(pname) + end, + on_update = function(pname) + end, + informations = pclasses.api.textify("Adventurer, the casual players, or hardcore players. Whatever end of the spectrum\n" .. + "you're in, adventurer will bring you what you want : no advantages, no help. Maybe you\n" .. + "don't want that if you just began playing. If that's the case.. just pick another tab and\n" .. + "read what's in it. You'll still be able to come back to this boilerplate class whenever you\n" .. + "want to (minus actual cooldown regulation of.. an hour between two changes) if you like\n" .. + "being hurt, or hardcore gaming, which, from afar, look alike.... The pedestal has a backpack\n" .. + "over it, because, yay adventures!") .. "image[2.4,5.6;6,4;pclasses_showcase_adventurer.png]" +}) + diff --git a/api.lua b/api.lua new file mode 100755 index 0000000..58c420d --- /dev/null +++ b/api.lua @@ -0,0 +1,176 @@ +------------------ +-- PClasses' API +-- + +-- Various utility functions + +-- Register the class (basic registration) +function pclasses.api.register_class(cname, def) + if not cname then + minetest.log("error", "[PClasses] Error registering unamed class") + return + elseif not def then + minetest.log("error", "[PClasses] Error registering class " .. + cname .. ". Reason : no definition table.") + return + end + if cname == "infos" then + minetest.log("error", "[PClasses] Error registering class with reserved name : infos") + return + end + pclasses.register_class_switch(cname, def.switch_params) + + pclasses.classes[cname] = def + return true +end + +------------------------ +-- Getters and Setters +-- + +-- Get class specs by name +function pclasses.api.get_class_by_name(cname) + return pclasses.classes[cname] +end + + +-- Get single player +function pclasses.api.get_player_class(pname) + return pclasses.data.players[pname] +end + +-- Get all players for a class +function pclasses.api.get_class_players(cname) + local pnames = {} + if pclasses.api.get_class_by_name(cname) then + for p,c in ipairs(pclasses.data.players) do + if c == cname then + table.insert(pnames, table.getn(pnames)+1) + end + end + end +end + +-- Set single player +function pclasses.api.set_player_class(pname, cname, inform) + if pclasses.api.get_class_by_name(cname) then + if pclasses.api.get_player_class(pname) then + pclasses.api.get_class_by_name(pclasses.api.get_player_class(pname)).on_update(pname) + pclasses.api.get_class_by_name(pclasses.api.get_player_class(pname)).on_unassigned(pname) + end + pclasses.data.players[pname] = cname + local newclass = pclasses.api.get_class_by_name(cname) + newclass.on_assigned(pname, inform) + -- Implicit call to on_update because we don't wanna repeat it + if newclass.on_update then + newclass.on_update(pname) + else + newclass.on_update = function(pname) end -- So that it won't annoy us later + end + + local ref = minetest.get_player_by_name(pname) + local armor_inv = minetest.get_inventory({type = "detached", name = pname .. "_armor"}) + local inv = ref:get_inventory() + vacuum_inventory(pname, inv, "armor", true) + vacuum_inventory(pname, armor_inv, "armor", false) -- Don't move to the graveyard + armor:set_player_armor(ref) + armor:update_inventory(ref) + + pclasses.api.vacuum_graveyard(minetest.get_player_by_name(pname)) + return true + end + return false +end + +-- Util function(s) + +pclasses.api.util.does_wear_full_armor = function(pname, material, noshield) + local inv = minetest.get_inventory({type = "detached", name = pname .. "_armor"}) + if not inv or inv:is_empty("armor") then + return false + end + local full_armor = true + for _, piece in pairs({"chestplate", "leggings", "boots", "helmet"}) do + full_armor = full_armor and inv:contains_item("armor", "3d_armor:" .. piece .. "_" .. material) + end + return full_armor and (inv:contains_item("armor", "shields:shield_" .. material) or noshield) +end + +function pclasses.api.util.can_have_item(pname, itemname) + if not pclasses.data.reserved_items[itemname] or (pclasses.conf.superuser_class and pclasses.api.get_player_class(pname) == pclasses.conf.superuser_class) then + return true + end + for index, class in pairs(pclasses.data.reserved_items[itemname]) do + if pclasses.api.get_player_class(pname) == class then + return true + end + end + return false +end + +function pclasses.api.util.on_update(pname) + local cname = pclasses.api.get_player_class(pname) + if cname ~= nil and pclasses.api.get_class_by_name(cname) and pclasses.api.get_class_by_name(cname).on_update then + pclasses.api.get_class_by_name(cname).on_update(pname) + end +end + +-- TEMPORARY CLASS SHIFT SYSTEM +-- Used to test on local servers +-- + +minetest.register_privilege("class_shifter", "Able to shift between classes") + +minetest.register_chatcommand("switch_class", { + args = "", + privs = {class_shifter = true}, + func = function(name, param) + pclasses.api.set_player_class(name, param) + end +}) + +------------------- +-- Reserved items +-- +function pclasses.api.reserve_item(cname, itemstring) + pclasses.data.reserved_items[itemstring] = pclasses.data.reserved_items[itemstring] or {} + table.insert(pclasses.data.reserved_items[itemstring], cname) +end + + +------------------------------------------- +-- Determination and reserved items tick -- +------------------------------------------- + +function vacuum_inventory(name, inv, invname, bury) + local ref = minetest.get_player_by_name(name) + for i = 1, inv:get_size(invname) do + local stack = inv:get_stack(invname, i) + if pclasses.data.reserved_items[stack:get_name()] then + if not pclasses.api.util.can_have_item(name, stack:get_name()) then + inv:set_stack(invname, i, "") + if bury then + local grave_inv = pclasses.api.create_graveyard_inventory(ref) + if grave_inv and grave_inv:room_for_item("graveyard", stack) then + grave_inv:add_item("graveyard", stack) + inv:add_item("graveyard", stack) + -- ^ Because add_item doesn't trigger on_put, nonsense + else + minetest.add_item(ref:getpos(), stack) + end + end + end + end + end +end + +local function tick() + for id, ref in ipairs(minetest.get_connected_players()) do + local name = ref:get_player_name() + local inv = ref:get_inventory() + vacuum_inventory(name, inv, "main", true) + end + minetest.after(2, tick) +end + +tick() diff --git a/depends.txt b/depends.txt new file mode 100755 index 0000000..86d6f14 --- /dev/null +++ b/depends.txt @@ -0,0 +1,3 @@ +3d_armor +sprint +unified_inventory diff --git a/init.lua b/init.lua new file mode 100755 index 0000000..69b2e6f --- /dev/null +++ b/init.lua @@ -0,0 +1,92 @@ +------------------- +-- Player Classes +-- + +-- NOTE: This is a very simple interface for classes, more features will be +-- added depending on the various mechanisms we will need + +-- Global namespace +pclasses = {} + +-- API +pclasses.api = {} +pclasses.api.util = {} + +-- Configuration +pclasses.conf = {} +pclasses.conf.default_class = "adventurer" +pclasses.conf.superuser_class = "admin" +pclasses.conf.save_interval = 3 * 60 +pclasses.conf.datafile = minetest.get_worldpath() .. "/pclasses" +pclasses.conf.gravefile = minetest.get_worldpath() .. "/graveyards" + +-- Classes +pclasses.classes = {} + +-- Data +pclasses.data = {} +pclasses.data.players = {} +pclasses.data.reserved_items = {} +pclasses.data.hud_ids = {} -- HUD maybe? + +dofile(minetest.get_modpath("pclasses") .. "/api.lua") +dofile(minetest.get_modpath("pclasses") .. "/inventory.lua") +dofile(minetest.get_modpath("pclasses") .. "/nodes.lua") + +function pclasses.data.load() + local file = io.open(pclasses.conf.datafile, "r") + if file then + local loaded = minetest.deserialize(file:read("*all")) + file:close() + if loaded then + pclasses.data.players = loaded.players or pclasses.data.players + minetest.log("action", "[PClasses] Loaded data") + end + end +end + +function pclasses.data.save() + local file, err = io.open(pclasses.conf.datafile, "w") + if file then + file:write(minetest.serialize({ + players = pclasses.data.players, + })) + file:close() + --minetest.log("action", "[PClasses] Saved data") + else + minetest.log("error", "[PClasses] Data save failed: open failed: " .. err) + end +end + +local function data_save_loop() + pclasses.data.save() + minetest.after(pclasses.conf.save_interval, data_save_loop) +end + +pclasses.data.load() + +------------------ +-- Default class +-- + +if pclasses.conf.default_class then + dofile(minetest.get_modpath("pclasses") .. "/" .. pclasses.conf.default_class .. ".lua") +end + +minetest.register_on_joinplayer(function(player) + local pname = player:get_player_name() + pclasses.api.create_graveyard_inventory(player) --create inventory before + + local cname = pclasses.api.get_player_class(pname) + if cname ~= nil and pclasses.api.get_class_by_name(cname) then + pclasses.api.set_player_class(pname, cname) + elseif pclasses.api.get_class_by_name(pclasses.conf.default_class) then + pclasses.api.set_player_class(pname, pclasses.conf.default_class) + end +end) + +minetest.register_on_shutdown(function() + pclasses.data.save() +end) + +data_save_loop() diff --git a/inventory.lua b/inventory.lua new file mode 100755 index 0000000..8454f27 --- /dev/null +++ b/inventory.lua @@ -0,0 +1,152 @@ +------------------------ +-- PClasses' inventory +-- + +-- Inventory for 'dead' items +pclasses.api.create_graveyard_inventory = function(player) + local pname = player:get_player_name() + local grave_inv = minetest.get_inventory({type = "detached", name = pname .. "_graveyard"}) + if grave_inv then + return grave_inv + end + local player_inv = minetest.get_inventory({type = "player", name = pname}) + grave_inv = minetest.create_detached_inventory(pname .. "_graveyard", { + on_take = function(inv, listname, index, stack, player) + player_inv:set_stack(listname, index, nil) + end, + on_move = function(inv, from_list, from_index, to_list, to_index, count, player) + local stack = inv:get_stack(to_list, to_index) + player_inv:set_stack(to_list, to_index, stack) + player_inv:set_stack(from_list, from_index, nil) + end, + allow_take = function(inv, listname, index, stack, player) + player_inv:set_stack(listname, index, nil) + return stack:get_count() + end, + allow_move = function(inv, from_list, from_index, to_list, to_index, count, player) + return 0 + end, + allow_put = function(inv, listname, index, stack, player) + return 0 + end, + }) + grave_inv:set_size("graveyard", 7*8) + player_inv:set_size("graveyard", 7*8) + for i = 1,56 do + local stack = player_inv:get_stack("graveyard", i) + grave_inv:set_stack("graveyard", i, stack) + end + return grave_inv +end + +unified_inventory.register_button("graveyard", { + type = "image", + image = "pclasses_grave_button.png", + tooltip = "Item Graveyard", +}) + +unified_inventory.register_page("graveyard", { + get_formspec = function(player) + local pname = player:get_player_name() + local form = "label[0,0;Graveyard]" .. + "list[detached:" .. pname .. "_graveyard;graveyard;0.5,0.7;7,8]" + return {formspec = form, draw_inventory = false} + end +}) + +minetest.register_chatcommand("clear_graveyard", { + description = "Clear Graveyard Inventory", + privs = {}, + func = function(name, param) + local grave_inv = minetest.get_inventory({type = "detached", name = name .. "_graveyard"}) + grave_inv:set_list("graveyard", {}) + minetest.get_player_by_name(name):get_inventory():set_list("graveyard", {}) + return true, "Graveyard flushed" + end, +}) + +function pclasses.api.vacuum_graveyard(player) + local pname = player:get_player_name() + local grave_inv = minetest.get_inventory({type = "detached", name = pname .. "_graveyard"}) + local player_inv = minetest.get_inventory({type = "player", name = pname}) + + if not grave_inv then return end + + for i = 1,7*8 do + local stack = grave_inv:get_stack("graveyard", i) + if pclasses.data.reserved_items[stack:get_name()] and pclasses.api.util.can_have_item(pname, stack:get_name()) then + grave_inv:set_stack("graveyard", i, nil) + player_inv:set_stack("graveyard", i, nil) + if player_inv:room_for_item("main", stack) then + player_inv:add_item("main", stack) + else + minetest.add_item(player:getpos(), stack) + end + end + end +end + +-- Inventory description buttons +local pbutton_form = "size[10,10]" .. + "button_exit[4.5,9.5;1,0.5;pmenu_leave;Leave]" .. + "tabheader[0,0;pmenu_header;infos" + +function pclasses.api.textify(text) + return ("textarea[0.5,0.2;9.6,5.8;pmenu_data;;%s]"):format(text) +end + +local pbuttons = {} +local pforms = {} +local pinfo = pclasses.api.textify( + "PClasses (Player Classes) allows you to become a member of specific classes implemented " .. + "with abilities, advantages, and reserved items. Each one of the classes defined grants " .. + "the right to carry items, called reserved items, tied to the abilities of a class. A " .. + "hunter will be able to use arrows, whereas a warrior can own powerful weapons. Each time " .. + "you switch classes, you will lose your stats and items, the latter being transfered into " .. + "a special part of your inventory, the graveyard. Once you return to a class that allows " .. + "you to use those items, they will return in your main inventory.\n" .. + "You can use this menu to navigate between classes and read informations about what " .. + "abilities come with specific classes.\n" .. + "You can see on this man the location of all class buildings available, containing their " .. + "respective class pedestals." +) .. "image[2.4,5.6;6,4;pclasses_buildings.png]" + + +minetest.after(0, function() + for cname, cdef in pairs(pclasses.classes) do + if cname ~= pclasses.conf.superuser_class then + pbutton_form = pbutton_form .. ',' .. cname + table.insert(pbuttons, cname) + end + end + pbutton_form = pbutton_form .. ";1]" +end) + +minetest.register_on_player_receive_fields(function(player, formname, fields) + if formname ~= "" then return end + + if fields.pmenu_header then + if fields.pmenu_header + 0 == 1 then + player:set_inventory_formspec(pbutton_form .. pinfo) + else + player:set_inventory_formspec(string.sub(pbutton_form, 1, -3) .. fields.pmenu_header .. "]" .. (pclasses.classes[pbuttons[fields.pmenu_header-1]].informations or "No informations available")) + end + return + + elseif fields.pmenu_leave then + player:set_inventory_formspec(pforms[player:get_player_name()]) + pforms[player:get_player_name()] = nil + end +end) + +unified_inventory.register_button("pclasses", { + type = "image", + image = "pclasses_inv.png", + tooltip = "Player Classes Descriptions", + action = function(player) + if not pforms[player:get_player_name()] then + pforms[player:get_player_name()] = player:get_inventory_formspec() + player:set_inventory_formspec(pbutton_form .. pinfo) + end + end +}) diff --git a/nodes.lua b/nodes.lua new file mode 100755 index 0000000..ae0209b --- /dev/null +++ b/nodes.lua @@ -0,0 +1,137 @@ +minetest.register_entity("pclasses:item", { + initial_properties = { + hp_max = 1, + physical = false, + collisionbox = {-0.17,-0.17,-0.17, 0.17,0.17,0.17}, + visual = "sprite", + visual_size = {x=0.5, y=0.5}, + textures = {""}, + spritediv = {x=1, y=1}, + initial_sprite_basepos = {x=0, y=0}, + is_visible = false, + }, + itemname = '', + class = '', + set_class = function(self, class) + self.class = class + end, + set_item = function(self, itemstring) + self.itemname = itemstring + local itemname = itemstring + local item_texture = nil + local item_type = "" + if minetest.registered_items[itemname] then + item_texture = minetest.registered_items[itemname].inventory_image + item_type = minetest.registered_items[itemname].type + end + local prop = { + is_visible = true, + visual = "sprite", + textures = {"unknown_item.png"} + } + if item_texture and item_texture ~= "" then + prop.visual = "sprite" + prop.textures = {item_texture} + prop.visual_size = {x=0.50, y=0.50} + else + prop.visual = "wielditem" + prop.textures = {itemname} + prop.visual_size = {x=0.25, y=0.25} + prop.automatic_rotate = math.pi * 0.10 + end + self.object:set_properties(prop) + end, + on_rightclick = function(self, clicker) + action_timers.wrapper( + clicker:get_player_name(), + "class switch", + "class_switch_" .. clicker:get_player_name(), + 3600, + pclasses.api.set_player_class, + {clicker:get_player_name(), self.class, true} + ) + end, + on_activate = function(self, staticdata) + local tab = minetest.deserialize(staticdata) + if tab then + self.itemname = tab.itemname + self.class = tab.class + else + self.itemname = staticdata:split("|")[1] + self.class = staticdata:split("|")[2] + end + self.object:set_armor_groups({immortal=1}) + self:set_item(self.itemname) + end, + get_staticdata = function(self) + return minetest.serialize({itemname = self.itemname, class = self.class}) + end, +}) + +function pclasses.register_class_switch(cname, params) + local color = params.color or { r = 255, g = 255, b = 255 } + local txtcolor = string.format("#%02x%02x%02x", color.r, color.g, color.b) + local overlay = "pclasses_class_switch_orb_overlay.png" + local holo_item = params.holo_item or "default:diamond" + minetest.register_node(":pclasses:class_switch_" .. cname, { + description = "Class switch orb (" .. cname .. ")", + tiles = {(params.tile or overlay) .. "^[colorize:" .. txtcolor .. ":200"}, + drawtype = "nodebox", + node_box = { type = "fixed", fixed = { + {-7/16, -8/16, -7/16, 7/16, -7/16, 7/16}, -- bottom plate + {-6/16, -7/16, -6/16, 6/16, -6/16, 6/16}, -- bottom plate (upper) + {-0.25, -6/16, -0.25, 0.25, 11/16, 0.25}, -- pillar + {-7/16, 11/16, -7/16, 7/16, 12/16, 7/16}, -- top plate + }}, + can_dig = function(pos, player) return minetest.get_player_privs(player:get_player_name()).server == true end, + sunlight_propagates = true, + light_source = 10, + sounds = default.node_sound_glass_defaults(), + groups = {unbreakable = 1}, + after_place_node = function(pos) + pos.y = pos.y + 1 + + -- Clean remaining entities + for _,ref in pairs(minetest.get_objects_inside_radius(pos, 0.3)) do + local e = ref:get_luaentity() + if e and e.name == "pclasses:item" then + ref:remove() + end + end + + local obj = minetest.add_entity(pos, "pclasses:item") + if obj then + obj:get_luaentity():set_item(holo_item) + obj:get_luaentity():set_class(cname) + end + pos.y = pos.y - 1 + local timer = minetest.get_node_timer(pos) + timer:start(3) + end, + on_timer = function(pos) + pos.y = pos.y + 1 + for _,ref in pairs(minetest.get_objects_inside_radius(pos, 0.3)) do + local e = ref:get_luaentity() + if e and e.name == "pclasses:item" then + return true + end + end + + local obj = minetest.add_entity(pos, "pclasses:item") + if obj then + obj:get_luaentity():set_item(holo_item) + obj:get_luaentity():set_class(cname) + end + return true + end, + on_destruct = function(pos) + pos.y = pos.y + 1 + for _,ref in pairs(minetest.get_objects_inside_radius(pos, 0.3)) do + local e = ref:get_luaentity() + if e and e.name == "pclasses:item" then + ref:remove() + end + end + end, + }) +end diff --git a/textures/pclasses_buildings.png b/textures/pclasses_buildings.png new file mode 100644 index 0000000..84e636d Binary files /dev/null and b/textures/pclasses_buildings.png differ diff --git a/textures/pclasses_class_switch_orb_overlay.png b/textures/pclasses_class_switch_orb_overlay.png new file mode 100755 index 0000000..aee7e0f Binary files /dev/null and b/textures/pclasses_class_switch_orb_overlay.png differ diff --git a/textures/pclasses_grave_button.png b/textures/pclasses_grave_button.png new file mode 100644 index 0000000..fd1fb30 Binary files /dev/null and b/textures/pclasses_grave_button.png differ diff --git a/textures/pclasses_inv.png b/textures/pclasses_inv.png new file mode 100644 index 0000000..1de55e5 Binary files /dev/null and b/textures/pclasses_inv.png differ diff --git a/textures/pclasses_showcase_adventurer.png b/textures/pclasses_showcase_adventurer.png new file mode 100644 index 0000000..cd195ab Binary files /dev/null and b/textures/pclasses_showcase_adventurer.png differ