write something there

This commit is contained in:
N-Nachtigal 2025-05-04 16:01:41 +02:00
commit b4b6c08f4f
8546 changed files with 309825 additions and 0 deletions

456
mods/researcher/src/api.lua Normal file
View file

@ -0,0 +1,456 @@
-- ----------------- --
-- GROUP FUNCTIONS --
-- ----------------- --
-- Add an item to a group
function researcher.add_item_to_group(item,group)
researcher.groups[group] = researcher.groups[group] or {}
researcher.groups[group][item] = true
return true
end
-- Iterate over items in a group
function researcher.for_item_in_group(group,fn)
for item,_ in pairs(researcher.groups[group]) do
if fn(item) then
return true
end
end
return true
end
-- Iterate over groups of an item
function researcher.for_group_in_item(item,fn)
item = researcher.registered_items[item]
if not item then
return false
end
for group,_ in pairs(item.groups) do
if fn(group) then
return true
end
end
return true
end
-- Determine if item is in a specific group
function researcher.is_item_in_group(item,group)
return researcher.groups[group] and researcher.groups[group][item] and true or false
end
-- Determine if two items have any groups in common
function researcher.do_items_share_groups(item1,item2)
item1 = researcher.registered_items[item1]
item2 = researcher.registered_items[item2]
if not item1 or not item2 then
return false
end
for group,_ in pairs(item1.groups) do
if item2.groups[group] then
return true
end
end
return false
end
-- ---------------- --
-- ITEM FUNCTIONS --
-- ---------------- --
-- Register an item with Researcher
function researcher.register_item(def)
-- Do not register duplicate items
if researcher.registered_items[def.name] then
return false
end
-- Create internal item definition
local item = {
name = def.name,
groups = {},
}
-- Determine base research points per level for this item
item.points_per_level = def.points_per_level
-- Determine research point adjustments that apply to this item
item.adjustments = {}
for _,adjustment in ipairs(researcher.registered_adjustments) do
local amount = adjustment.calculate(item.name)
if amount ~= 0 then
item.points_per_level = math.max(researcher.settings.points_per_research,item.points_per_level + amount)
table.insert(item.adjustments,{
name = adjustment.name,
amount = amount,
})
end
end
-- Determine groups for this item
local groups = def.groups or {}
for _,group in ipairs(groups) do
item.groups[group] = true
researcher.add_item_to_group(def.name,group)
end
-- Register the item
researcher.registered_items[def.name] = item
return true
end
-- ---------------------- --
-- ADJUSTMENT FUNCTIONS --
-- ---------------------- --
-- Register a adjustment function that will return a adjustment amount
function researcher.register_adjustment(def)
-- Do not register duplicate adjustments
if researcher.registered_adjustments[def.name] then
return false
end
-- Register the new adjustment
local adjustment = {
name = def.name,
reason = def.reason,
calculate = def.calculate,
}
researcher.registered_adjustments[def.name] = adjustment
table.insert(researcher.registered_adjustments,adjustment)
return true
end
function researcher.get_adjustments_for_item(item)
item = researcher.registered_items[item]
if not item then
return {}
end
return item.adjustments
end
-- ----------------- --
-- BONUS FUNCTIONS --
-- ----------------- --
-- Register a bonus
function researcher.register_bonus(def)
-- Do not register duplicate bonuses
if researcher.registered_bonuses[def.name] then
return false
end
-- Register the new bonus
local bonus = {
name = def.name,
reason = def.reason,
calculate = def.calculate,
initialize_player_data = def.initialize_player_data or function() end,
}
researcher.registered_bonuses[def.name] = bonus
table.insert(researcher.registered_bonuses,bonus)
return true
end
-- ---------------- --
-- DATA FUNCTIONS --
-- ---------------- --
-- Initialize player data
function researcher.initialize_player_data(player_name)
-- Create new player data
local player_data = {
-- The player's name
name = player_name,
-- All of the player's research progress indexed by item name
research = {},
-- The current subject of the player's research
subject = {
image = nil,
description = "",
groups = "(Research an item on the left to see info)",
research = nil,
},
}
-- Initialize bonus-specific data
for _,bonus in ipairs(researcher.registered_bonuses) do
bonus.initialize_player_data(player_data)
end
-- Save initialized player data to mod storage
researcher.save_player_data(player_name)
-- Return initialized data
return player_data
end
-- Get player data
function researcher.get_player_data(player_name)
-- Attempt to load player data from cache
local pstring = "player_" .. player_name
local player_data = researcher.data[pstring]
if not player_data then
-- Attempt to load player data from mod storage
player_data = researcher.storage:get(pstring)
-- Initialize new player data if not found or parse string if found
if not player_data then
player_data = researcher.initialize_player_data(player_name)
else
player_data = minetest.deserialize(player_data)
end
-- Link research subject to actual research
if player_data.subject.research then
player_data.subject.research = player_data.research[player_data.subject.item.name]
end
-- Cache player data
researcher.data[pstring] = player_data
end
-- Return player data
return player_data
end
-- Determine the number of points to the next level
-- FIXME should be (item,level)?
function researcher.get_points_to_next_level(player,item)
item = researcher.registered_items[item]
if item then
local research = (type(player) == "string" and researcher.get_player_data(player) or player).research[item.name]
if research then
local level = research.level
if research.level <= researcher.settings.level_max then
return math.round(item.points_per_level * math.pow(research.level,researcher.settings.level_scale) / 100) * 100
end
end
else
return 0
end
return item.points_per_level
end
-- Save player data; can be called many times during the current tick but will
-- only execute once on the next tick
function researcher.save_player_data(player_name)
if not researcher.data.save[player_name] then
researcher.data.save[player_name] = true
minetest.after(0,function()
researcher.storage:set_string("player_" .. player_name,minetest.serialize(researcher.get_player_data(player_name)))
researcher.data.save[player_name] = nil
end)
end
end
-- -------------------- --
-- RESEARCH FUNCTIONS --
-- -------------------- --
-- Research an item
function researcher.research_item(player,item)
-- Initialize result
local result = {
item = item,
base = researcher.settings.points_per_research,
bonuses = {},
total = researcher.settings.points_per_research,
success = true,
}
-- Get player data
local player_data = type(player) == "string" and researcher.get_player_data(player) or player
-- Cannot research beyond max research level
if player_data.research[item] and player_data.research[item].level > researcher.settings.level_max then
result = {
item = item,
base = 0,
bonuses = {},
total = 0,
success = false,
}
return result
end
-- Calculate bonuses
for _,bonus in ipairs(researcher.registered_bonuses) do
local amount = bonus.calculate(item,player_data)
if amount ~= 0 then
result.total = result.total + amount
table.insert(result.bonuses,{
name = bonus.name,
reason = bonus.reason,
points = amount,
})
end
end
-- Get research entry
local research = player_data.research[item] or {
level = 1,
points = 0,
}
player_data.research[item] = research
player_data.subject.research = research
-- Look up item
item = researcher.registered_items[item]
if not item then
return {
item = "???",
base = 0,
bonuses = {},
total = 0,
success = false,
}
end
-- Level up research
local points_tally = research.points + result.total
local points_level = researcher.get_points_to_next_level(player_data,item.name)
while points_tally >= points_level do
research.level = math.min(researcher.settings.level_max + 1,research.level + 1)
points_tally = points_tally - points_level
points_level = researcher.get_points_to_next_level(player_data,item.name)
end
research.points = points_tally
-- Unlock award for basic research
if researcher.settings.awards then
awards.unlock(player.name,"researcher:apprentice")
end
-- Set points to 0 at max level and unlock Prodigious award
if research.level > researcher.settings.level_max then
research.points = 0
if researcher.settings.awards then
awards.unlock(player.name,"researcher:prodigious")
end
end
-- Save data
researcher.save_player_data(player_data.name)
-- Return final research result
return result
end
-- Research an ItemStack
function researcher.research_itemstack(player,itemstack)
if itemstack:is_empty() then
return {
item = itemstack:get_name(),
success = false,
remainder = ItemStack(itemstack:get_name() .. " 0"),
}
end
-- Get player data for entire stack
local player_data = type(player) == "string" and researcher.get_player_data(player) or player
-- Research each item in the stack individually
local name = itemstack:get_name()
local results = {
name = name,
success = false,
remainder = ItemStack(name .. " 0"),
}
for i = 1, itemstack:get_count() do
local result = researcher.research_item(player_data,name)
results.success = results.success or result.success
if result.success then
table.insert(results,result)
elseif player_data.research[name].level > researcher.settings.level_max then
itemstack:set_count(itemstack:get_count() - i + 1)
results.remainder = itemstack
return results
else
return results
end
end
-- Return all results
return results
end
-- Research an entire inventory list
function researcher.research_inventory(player,inventory,list)
-- Get player data
local player_data = type(player) == "string" and researcher.get_player_data(player) or player
-- Research each ItemStack in the inventory
local results = {
success = false,
}
for i = 1, inventory:get_size(list) do
local itemstack = inventory:get_stack(list,i)
local result = researcher.research_itemstack(player_data,itemstack)
results.success = results.success or result.success
if result.success then
inventory:set_stack(list,i,result.remainder)
end
table.insert(results,result)
end
-- Return full inventory results
return results
end
-- ----------------------- --
-- DUPLICATION FUNCTIONS --
-- ----------------------- --
-- Duplicate the item in the player's research inventory
function researcher.duplicate_research(player)
local inventory = player:get_inventory()
local itemstack = inventory:get_stack("research",1)
if itemstack and not itemstack:is_empty() then
local item = itemstack:get_name()
local idef = minetest.registered_items[item]
if idef then
inventory:add_item("main",ItemStack(itemstack:get_name() .. " " .. itemstack:get_stack_max()))
end
end
end
-- --------------- --
-- GUI FUNCTIONS --
-- --------------- --
-- Return a formspec that implements research
function researcher.get_formspec_data(player_name)
local player_data = researcher.get_player_data(player_name)
local subject = player_data.subject
local data = {
subject = subject,
current_points = subject.research and subject.research.points or 0,
points_to_next_level = subject.research and researcher.get_points_to_next_level(player_name,subject.item.name) or 0,
is_max_level = subject.research and (subject.research.level > researcher.settings.level_max) and true or false,
is_inventory_empty = minetest.get_inventory({ type = "player", name = player_name, }):get_stack("research",1):is_empty(),
last_result = player_data.last_result,
}
player_data.last_result = nil -- last result is only available once immediately after research
return data
end
-- -------------------- --
-- CALLBACK FUNCTIONS --
-- -------------------- --
function researcher.register_on_research(fn)
table.insert(researcher.registered_on_research,fn)
end

View file

@ -0,0 +1,43 @@
if researcher.settings.awards then
-- Use the available registration function, if any
local register_award = (function()
if awards.register_award then
return function(...)
awards.register_award(...)
end
elseif awards.register_achievement then
return function(...)
awards.register_achievement(...)
end
else
minetest.log("warn","Researcher does not support the loaded awards mod.")
return function()
-- unsupported awards mod, noop
end
end
end)()
-- Apprentice: awarded for a player's first successful research
register_award("researcher:apprentice",{
title = "Apprentice",
description = "Research any item",
difficulty = 5,
icon = "researcher_icon_bronze.png",
})
-- Eureka!: awarded for a player's first gained research level
register_award("researcher:eureka",{
title = "Eureka!",
description = "Earn a research level for any item",
difficulty = 50,
icon = "researcher_icon_silver.png",
})
-- Prodigious: awarded for a player's first max research level
register_award("researcher:prodigious",{
title = "Prodigious",
description = "Earn max research level for any item",
difficulty = 150,
icon = "researcher_icon_gold.png",
})
end

View file

@ -0,0 +1,183 @@
-- Research level group bonus
if researcher.settings.group_research_bonus > 0 and researcher.settings.group_research_bonus_max > 0 then
researcher.register_bonus({
name = "researcher:research_group_research_bonus",
reason = "Group Research",
calculate = function(item,player_data)
local bonus = 0
for subject,research in pairs(player_data.research) do
if researcher.do_items_share_groups(item,subject) then
bonus = bonus + (research.level - 1) * researcher.settings.group_research_bonus
end
end
return bonus
end,
initialize_player_data = function()
-- all bonus calculation comes from research; nothing to initialize
end,
})
else
researcher.register_bonus({
name = "researcher:research_group_research_bonus",
reason = "Group Research",
calculate = function() return 0 end,
initialize_player_data = function()
-- bonus is calculated from research; nothing to initialize
end,
})
end
-- Focused research bonus
if researcher.settings.focused_research_bonus_max > 0 and (researcher.settings.focused_research_bonus_exact > 0 or researcher.settings.focused_research_bonus_group > 0) then
researcher.register_bonus({
name = "researcher:focused_research_bonus",
reason = "Focused Research",
calculate = function(item,player_data)
-- Calculate bonus value
if item == player_data.focused_research.item then
player_data.focused_research.bonus = player_data.focused_research.bonus + researcher.settings.focused_research_bonus_exact
elseif researcher.do_items_share_groups(item,player_data.focused_research.item) then
player_data.focused_research.bonus = player_data.focused_research.bonus + researcher.settings.focused_research_bonus_group
else
player_data.focused_research.bonus = 0
end
-- Set focused item
player_data.focused_research.item = item
-- Return capped bonus value
player_data.focused_research.bonus = math.min(player_data.focused_research.bonus,researcher.settings.focused_research_bonus_max)
return player_data.focused_research.bonus
end,
initialize_player_data = function(player_data)
player_data.focused_research = {
item = "",
bonus = 0,
}
end,
})
else
researcher.register_bonus({
name = "researcher:focused_research_bonus",
reason = "Focused Research",
calculate = function() return 0 end,
initialize_player_data = function(player_data)
player_data.focused_research = {
item = "",
bonus = 0,
}
end,
})
end
-- Research table bonus
if researcher.settings.research_table_bonus_exact > 0 or researcher.settings.research_table_bonus_group > 0 or (researcher.settings.research_table_adjacency_bonus > 0 and researcher.settings.research_table_adjacency_max > 0) then
researcher.register_bonus({
name = "researcher:research_table_bonus",
reason = "Research Table",
calculate = function(item,player_data)
-- Initialize bonus and max flag
local bonus = 0
local bonusmax = false
-- Track limits when tallying bonuses
local nadj = 0
local function rtbonus(bonus,increment,adjacency)
local result = bonus
if adjacency then
local adj_bounded = math.min(adjacency,researcher.settings.research_table_adjacency_max - nadj)
nadj = nadj + adj_bounded
result = result + researcher.settings.research_table_adjacency_bonus * adj_bounded
else
result = bonus + increment
end
return math.min(result,researcher.settings.research_table_bonus_max), (result >= researcher.settings.research_table_bonus_max or nadj >= researcher.settings.research_table_adjacency_max)
end
-- Scan radius around player for research tables
local player = minetest.get_player_by_name(player_data.name)
local research_table = nil
if player then
local pos = player:get_pos()
local radius = researcher.settings.research_table_player_radius
for _,rt in ipairs(minetest.find_nodes_in_area(pos:add(-radius),pos:add(radius),"researcher:research_table")) do
-- Get research table's focus
local meta = minetest.get_meta(rt)
local inventory = meta:get_inventory()
local itemstack = inventory:get_stack("focus",1)
local name = itemstack:get_name()
-- If the focus item matches the item in question, then add to the
-- calculated bonus accordingly
if item == name then
bonus, bonusmax = rtbonus(bonus,(research_table and (researcher.settings.research_table_bonus_exact - researcher.settings.research_table_bonus_group) or researcher.settings.research_table_bonus_exact))
if bonusmax then
return bonus
end
research_table = rt
break -- cannot do better than an exact match
elseif not research_table and researcher.do_items_share_groups(item,name) then
bonus, bonusmax = rtbonus(bonus,researcher.settings.research_table_bonus_group)
if bonusmax then
return bonus
end
research_table = rt
-- keep scanning for better matches
end
end
-- Calculate adjacency bonus for research table
if research_table then
local radius = researcher.settings.research_table_adjacency_radius
local pos1 = research_table:add(-radius)
local pos2 = research_table:add(radius)
-- Check nearby node groups
bonus, bonusmax = rtbonus(bonus,researcher.settings.research_table_adjacency_bonus,#minetest.find_nodes_in_area(pos1,pos2,(function()
local groups = {}
for group,_ in pairs(researcher.registered_items[item].groups) do
table.insert(groups,"group:" .. group)
end
return groups
end)()))
if bonusmax then
return bonus
end
-- Check nearby node inventories
for _,node in ipairs(minetest.find_nodes_with_meta(pos1,pos2)) do
if not node:equals(research_table) then
local nodemeta = minetest.get_meta(node)
local nodeinventory = nodemeta:get_inventory()
if nodeinventory then
for list,stacks in pairs(nodeinventory:get_lists() or {}) do
for _,itemstack in ipairs(stacks or {}) do
if not itemstack:is_empty() and researcher.do_items_share_groups(item,itemstack:get_name()) then
bonus, bonusmax = rtbonus(bonus,researcher.settings.research_table_adjacency_bonus,itemstack:get_count())
if bonusmax then
return bonus
end
end
end
end
end
end
end
else
return 0 -- no bonus if no research table was found
end
else
return 0 -- no bonus if player is mysteriously not found
end
-- Return partial bonus total
return bonus
end,
})
else
researcher.register_bonus({
name = "researcher:research_table_bonus",
reason = "Research Table",
calculate = function() return 0 end,
})
end

View file

@ -0,0 +1,51 @@
local show_formspec = (function()
if researcher.dependencies.sfinv and sfinv.enabled then
return function(name)
local player = minetest.get_player_by_name(name)
sfinv.set_page(player,"researcher:player_research")
return "Use the inventory (default key 'i') to perform research."
end
elseif researcher.dependencies.mcl_inventory then
return function(name)
return "Use the inventory (default key 'i') to perform research."
end
elseif researcher.dependencies.unified_inventory then
return function(name)
return "Use the inventory (default key 'i') to perform research."
end
elseif researcher.dependencies.i3 then
return function(name)
local player = minetest.get_player_by_name(name)
i3.set_tab(player,"research")
return "Use the inventory (default key 'i') to perform research."
end
else
return function(name)
minetest.show_formspec(name,"researcher:player_research",researcher.get_formspec(name))
end
end
end)()
minetest.register_chatcommand("research",{
params = "<action> <value>",
description = "interact with researcher",
func = function(name,params)
if params == "reset" then
researcher.data["player_" .. name] = researcher.initialize_player_data(name)
return true, "Research has been fully reset."
end
if params:find("^reset .+$") then
local item = params:split(" ")[2]
researcher.get_player_data(name).research[item] = nil
researcher.save_player_data(name)
return true, "Research has been reset for " .. item
end
if not params or params == "" or params == "show" then
return true, show_formspec(name)
end
return false, "Unknown or incorrect researcher command"
end
})

484
mods/researcher/src/gui.lua Normal file
View file

@ -0,0 +1,484 @@
-- ----------------- --
-- LOCAL FUNCTIONS --
-- ----------------- --
local function format_formspec(formspec,...)
return string.format(string.gsub(formspec,"%-%-[^\n]*",""),...)
end
local get_ui = (function()
if researcher.dependencies.sfinv and sfinv.enabled and not researcher.dependencies.i3 then
-- Get sfinv formspec
return function(player_name)
local data = researcher.get_formspec_data(player_name)
local status_string = "hypertext[0.3,2.3;4,4;;<global halign=center>(place item above to analyze)]"
if data.last_result then
status_string = "hypertext[0.3,2.3;4,4;;<global halign=center><i>Research successful!</i>\n\n" .. data.last_result .. "]"
end
local progress_bar = data.subject.image and string.format([[
box[4.475,3.675;%f,0.625;#00ff00]
image[4.45,3.65;3.65,0.8;researcher_research_points_border.png;7]
hypertext[4.3,3.65;4,0.9;;<global valign=middle halign=center><b>Level %s</b>%s]
]],
data.is_max_level and 2.85 or (2.85 * (data.current_points / data.points_to_next_level)),
data.is_max_level and "MAX" or (data.subject.research and data.subject.research.level or 1),
data.is_max_level and "" or ("\n" .. (data.subject.research and data.subject.research.points or 0) .. " / " .. researcher.get_points_to_next_level(player_name,data.subject.item.name)))
local decor = data.is_inventory_empty and "inactive" or "active"
return format_formspec([[
-- Background boxes; research/duplication on the left, info/progress on the right
box[0,0;3.8,4.9;#00000040]
box[4,0;3.8,4.9;#00000040]
-- Player's 1x1 research inventory with optional research/duplication button
-- at the bottom
image[1.5,0.8;1,1;researcher_gui_hb_bg.png]
image[1.1,0.375;2,2;researcher_research_inventory_decor_%s.png]
list[current_player;research;1.5,0.8;1,1;0]
listring[current_player;main]
listring[current_player;research]
%s
-- Current research item image, name, and groups
item_image[5,0.2;2.2,2.2;%s]
hypertext[4.3,2.3;4,0.5;;<global halign=center size=18><b>%s</b>]
box[4.3,2.6;3.2,0.001;#00000099]
hypertext[4.3,2.7;4,1.5;;<global halign=center>%s]
-- Research level/points progress bar
%s
]],
decor,
data.is_inventory_empty and status_string or string.format("button[0.4,3.3;3.15,1.6;%s;%s]",data.is_max_level and "duplicate" or "research",data.is_max_level and "Duplicate" or "Research"),
data.subject.image or "",
data.subject.description,
data.subject.research and data.subject.groups or "(research to learn item groups)",
progress_bar or "")
end
-- Get Mineclonia/VoxelLibre formspec
elseif researcher.dependencies.mcl_inventory then
return function(player_name)
local data = researcher.get_formspec_data(player_name)
local status_string = "hypertext[1,2.75;4.5,4;;<global halign=center>(place item above to analyze)]"
if data.last_result then
status_string = "hypertext[1,2.75;4.5,4;;<global halign=center><i>Research successful!</i>\n\n" .. data.last_result .. "]"
end
local progress_bar = data.subject.image and string.format([[
box[6.55,4.41;%f,0.73;#00ff00]
image[6.5,4.4;3.725,0.75;researcher_research_points_border.png;7]
hypertext[6.1,4.325;4.5,0.9;;<global valign=middle halign=center><b>Level %s</b>%s]
]],
data.is_max_level and 3.66 or (3.66 * (data.current_points / data.points_to_next_level)),
data.is_max_level and "MAX" or (data.subject.research and data.subject.research.level or 1),
data.is_max_level and "" or ("\n" .. (data.subject.research and data.subject.research.points or 0) .. " / " .. researcher.get_points_to_next_level(player_name,data.subject.item.name)))
local decor = data.is_inventory_empty and "inactive" or "active"
return format_formspec([[
-- Background boxes; research/duplication on the left, info/progress on the right
box[1,0.2;4.5,5.1;#00000040]
box[6.1,0.2;4.5,5.1;#00000040]
-- Player's 1x1 research inventory with optional research/duplication button
-- at the bottom
image[2.75,1;1,1;researcher_gui_hb_bg.png]
image[2.25,0.5;2,2;researcher_research_inventory_decor_%s.png]
list[current_player;research;2.75,1;1,1;0]
listring[current_player;main]
listring[current_player;research]
%s
-- Current research item image, name, and groups
item_image[7.325,0.4;2,2;%s]
hypertext[6.1,2.5;4.5,0.5;;<global halign=center size=18><b>%s</b>]
box[6.5,2.8;3.75,0.001;#00000099]
hypertext[6.1,2.9;4.5,1.5;;<global halign=center>%s]
-- Research level/points progress bar
%s
]],
decor,
data.is_inventory_empty and status_string or string.format("button[1.25,4.4;4,0.75;%s;%s]",data.is_max_level and "duplicate" or "research",data.is_max_level and "Duplicate" or "Research"),
data.subject.image or "",
data.subject.description,
data.subject.research and data.subject.groups or "(research to learn item groups)",
progress_bar or "")
end
-- Get Unified Inventory formspec
elseif researcher.dependencies.unified_inventory then
return function(player_name)
local data = researcher.get_formspec_data(player_name)
local status_string = "hypertext[0.5,2.75;4.5,4;;<global halign=center>(place item above to analyze)]"
if data.last_result then
status_string = "hypertext[0.5,2.75;4.5,4;;<global halign=center><i>Research successful!</i>\n\n" .. data.last_result .. "]"
end
local progress_bar = data.subject.image and string.format([[
box[6.05,4.61;%f,0.73;#00ff00]
image[6,4.6;3.725,0.75;researcher_research_points_border.png;7]
hypertext[5.6,4.525;4.5,0.9;;<global valign=middle halign=center><b>Level %s</b>%s]
]],
data.is_max_level and 3.66 or (3.66 * (data.current_points / data.points_to_next_level)),
data.is_max_level and "MAX" or (data.subject.research and data.subject.research.level or 1),
data.is_max_level and "" or ("\n" .. (data.subject.research and data.subject.research.points or 0) .. " / " .. researcher.get_points_to_next_level(player_name,data.subject.item.name)))
local decor = data.is_inventory_empty and "inactive" or "active"
return format_formspec([[
-- Background boxes; research/duplication on the left, info/progress on the right
box[0.5,0.2;4.5,5.4;#00000040]
box[5.6,0.2;4.5,5.4;#00000040]
-- Player's 1x1 research inventory with optional research/duplication button
-- at the bottom
image[2.25,1;1,1;researcher_gui_hb_bg.png]
image[1.75,0.5;2,2;researcher_research_inventory_decor_%s.png]
list[current_player;research;2.25,1;1,1;0]
listring[current_player;main]
listring[current_player;research]
%s
-- Current research item image, name, and groups
item_image[6.825,0.4;2,2;%s]
hypertext[5.6,2.5;4.5,0.5;;<global halign=center size=18><b>%s</b>]
box[6,2.8;3.75,0.001;#00000099]
hypertext[5.6,2.9;4.5,1.5;;<global halign=center>%s]
-- Research level/points progress bar
%s
]],
decor,
data.is_inventory_empty and status_string or string.format("button[0.75,4.6;4,0.75;%s;%s]",data.is_max_level and "duplicate" or "research",data.is_max_level and "Duplicate" or "Research"),
data.subject.image or "",
data.subject.description,
data.subject.research and data.subject.groups or "(research to learn item groups)",
progress_bar or "")
end
-- Get i3 formspec
elseif researcher.dependencies.i3 then
return function(player_name)
local data = researcher.get_formspec_data(player_name)
local status_string = "hypertext[0.5,3.05;4.5,4;;<global halign=center>(place item above to analyze)]"
if data.last_result then
status_string = "hypertext[0.5,3.05;4.5,4;;<global halign=center><i>Research successful!</i>\n\n" .. data.last_result .. "]"
end
local progress_bar = data.subject.image and string.format([[
box[5.75,4.925;%f,0.715;#00ff00]
image[5.7,4.9;3.725,0.75;researcher_research_points_border.png;7]
hypertext[5.3,4.825;4.5,0.9;;<global valign=middle halign=center><b>Level %s</b>%s]
]],
data.is_max_level and 3.64 or (3.64 * (data.current_points / data.points_to_next_level)),
data.is_max_level and "MAX" or (data.subject.research and data.subject.research.level or 1),
data.is_max_level and "" or ("\n" .. (data.subject.research and data.subject.research.points or 0) .. " / " .. researcher.get_points_to_next_level(player_name,data.subject.item.name)))
local decor = data.is_inventory_empty and "inactive" or "active"
return format_formspec([[
-- Background boxes; research/duplication on the left, info/progress on the right
box[0.5,0.5;4.5,5.4;#00000040]
box[5.3,0.5;4.5,5.4;#00000040]
-- Player's 1x1 research inventory with optional research/duplication button
-- at the bottom
image[2.25,1.3;1,1;researcher_gui_hb_bg.png]
image[1.75,0.8;2,2;researcher_research_inventory_decor_%s.png]
list[current_player;research;2.25,1.3;1,1;0]
listring[current_player;main]
listring[current_player;research]
%s
-- Current research item image, name, and groups
item_image[6.525,0.7;2,2;%s]
hypertext[5.3,2.8;4.5,0.5;;<global halign=center size=18><b>%s</b>]
box[5.7,3.1;3.75,0.001;#00000099]
hypertext[5.3,3.2;4.5,1.5;;<global halign=center>%s]
-- Research level/points progress bar
%s
]],
decor,
data.is_inventory_empty and status_string or string.format("button[0.75,4.9;4,0.75;%s;%s]",data.is_max_level and "duplicate" or "research",data.is_max_level and "Duplicate" or "Research"),
data.subject.image or "",
data.subject.description,
data.subject.research and data.subject.groups or "(research to learn item groups)",
progress_bar or "")
end
-- Get universal formspec
else
return function(player_name)
local data = researcher.get_formspec_data(player_name)
local status_string = "hypertext[0.3,2.3;4,4;;<global halign=center>(place item above to analyze)]"
if data.last_result then
status_string = "hypertext[0.3,2.3;4,4;;<global halign=center><i>Research successful!</i>\n\n" .. data.last_result .. "]"
end
local progress_bar = data.subject.image and string.format([[
box[4.475,3.675;%f,0.625;#00ff00]
image[4.45,3.65;3.65,0.8;researcher_research_points_border.png;7]
hypertext[4.3,3.65;4,0.9;;<global valign=middle halign=center><b>Level %s</b>%s]
]],
data.is_max_level and 2.85 or (2.85 * (data.current_points / data.points_to_next_level)),
data.is_max_level and "MAX" or (data.subject.research and data.subject.research.level or 1),
data.is_max_level and "" or ("\n" .. (data.subject.research and data.subject.research.points or 0) .. " / " .. researcher.get_points_to_next_level(player_name,data.subject.item.name)))
local decor = data.is_inventory_empty and "inactive" or "active"
return format_formspec([[
-- sfinv size + padding
size[8,9.1]
-- Background boxes; research/duplication on the left, info/progress on the right
box[0,0;3.8,4.9;#00000040]
box[4,0;3.8,4.9;#00000040]
-- Player's 1x1 research inventory with optional research/duplication button
-- at the bottom
image[1.5,0.8;1,1;researcher_gui_hb_bg.png]
image[1.1,0.375;2,2;researcher_research_inventory_decor_%s.png]
list[current_player;research;1.5,0.8;1,1;0]
listring[current_player;main]
listring[current_player;research]
%s
-- Current research item image, name, and groups
item_image[5,0.2;2.2,2.2;%s]
hypertext[4.3,2.3;4,0.5;;<global halign=center size=18><b>%s</b>]
box[4.3,2.6;3.2,0.001;#00000099]
hypertext[4.3,2.7;4,1.5;;<global halign=center>%s]
-- Research level/points progress bar
%s
-- Player's main inventory a la sfinv
image[0,5.2;1,1;researcher_gui_hb_bg.png]
image[1,5.2;1,1;researcher_gui_hb_bg.png]
image[2,5.2;1,1;researcher_gui_hb_bg.png]
image[3,5.2;1,1;researcher_gui_hb_bg.png]
image[4,5.2;1,1;researcher_gui_hb_bg.png]
image[5,5.2;1,1;researcher_gui_hb_bg.png]
image[6,5.2;1,1;researcher_gui_hb_bg.png]
image[7,5.2;1,1;researcher_gui_hb_bg.png]
list[current_player;main;0,5.2;8,1;]
list[current_player;main;0,6.35;8,3;8]
]],
decor,
data.is_inventory_empty and status_string or string.format("button[0.4,3.3;3.15,1.6;%s;%s]",data.is_max_level and "duplicate" or "research",data.is_max_level and "Duplicate" or "Research"),
data.subject.image or "",
data.subject.description,
data.subject.research and data.subject.groups or "(research to learn item groups)",
progress_bar or "")
end
end
end)()
local refresh_ui = (function()
-- Refresh sfinv formspec
if researcher.dependencies.sfinv and sfinv.enabled and not researcher.dependencies.i3 then
return function(player)
sfinv.set_player_inventory_formspec(player,sfinv.get_or_create_context(player))
end
-- Refresh Mineclone formspec
elseif researcher.dependencies.mcl_inventory then
return function(player)
mcl_inventory.update_inventory_formspec(player)
end
-- Refresh Unified Inventory formspec
elseif researcher.dependencies.unified_inventory then
return function(player)
unified_inventory.set_inventory_formspec(player,"Research")
end
-- Refresh i3 formspec
elseif researcher.dependencies.i3 then
return function(player)
i3.set_fs(player)
end
-- Refresh universal formspec
else
return function(player)
local name = player:get_player_name()
minetest.show_formspec(name,"researcher:player_research",get_ui(name))
end
end
end)()
local do_research = function(player,fields,refresh)
if fields.research then
local player_name = player:get_player_name()
local inventory = player:get_inventory()
local item = inventory:get_stack("research",1)
if not item:is_empty() then
-- Get research level to compare with after research
local player_data = researcher.get_player_data(player_name)
local research = player_data.research[item:get_name()]
local level_before = 1
if research then
level_before = research.level
end
-- Perform research
local results = researcher.research_inventory(player_name,inventory,"research")
-- Aggregate results
local totals = {
items = 0,
base = 0,
bonuses = {},
}
for _,itemstack in ipairs(results) do
for _,i in ipairs(itemstack) do
totals.items = totals.items + 1
totals.base = totals.base + i.base
for _,bonus in ipairs(i.bonuses) do
totals.bonuses[bonus.reason] = totals.bonuses[bonus.reason] or 0
totals.bonuses[bonus.reason] = totals.bonuses[bonus.reason] + bonus.points
end
end
end
player_data.last_result = "Items researched: <style color=#0099cc>" .. totals.items .. "</style>\nBase points: <style color=#0099cc>" .. totals.base .. "</style>\n"
for reason,points in pairs(totals.bonuses) do
player_data.last_result = player_data.last_result .. reason .. ": " .. (points > 0 and "<style color=#00cc00>+" or "<style color=#cc0000>-") .. points .. "</style>\n"
end
-- Refresh UI
if refresh ~= false then
refresh_ui(player)
end
-- Determine level up progress
research = player_data.research[item:get_name()]
if research and research.level > level_before then
-- Unlock eureka award
if researcher.settings.awards then
awards.unlock(player_name,"researcher:eureka")
end
-- Play research level up sound
minetest.sound_play({
name = "researcher_level_up",
gain = 0.5,
},{ to_player = player_name },true)
else
-- Play normal research sound
minetest.sound_play({
name = "researcher_research",
gain = 0.1,
pitch = 1 + (math.random(1,4) / 10),
},{ to_player = player_name },true)
end
end
elseif fields.duplicate then
researcher.duplicate_research(player)
minetest.sound_play({
name = "researcher_duplicate",
gain = 0.5,
pitch = 1.5,
},{ to_player = player_name },true)
end
end
-- ------------------ --
-- GUI INTEGRATIONS --
-- ------------------ --
-- Configure sfinv UI
if researcher.dependencies.sfinv and sfinv.enabled and not researcher.dependencies.i3 then
sfinv.register_page("researcher:player_research",{
title = "Research",
get = function(self,player,context)
return sfinv.make_formspec(player,context,get_ui(player:get_player_name()),true)
end,
is_in_nav = function()
return true
end,
on_player_receive_fields = function(self,player,context,fields)
do_research(player,fields)
end,
})
-- Configure Mineclonia/VoxelLibre UI
elseif researcher.dependencies.mcl_inventory then
mcl_inventory.register_survival_inventory_tab({
id = "research",
description = "Research",
item_icon = "researcher:research_table",
show_inventory = true,
build = function(player)
return get_ui(player:get_player_name())
end,
handle = function(player, fields)
do_research(player,fields)
end,
})
-- Configure Unified Inventory UI
elseif researcher.dependencies.unified_inventory then
unified_inventory.register_page("Research",{
get_formspec = function(player,formspec)
return { formspec = formspec.standard_inv_bg .. get_ui(player:get_player_name()) }
end,
})
unified_inventory.register_button("Research",{
type = "image",
image = "researcher_icon_black.png",
tooltip = "Research",
hide_lite = false,
})
minetest.register_on_player_receive_fields(function(player,formname,fields)
if formname == "" and (fields.research or fields.duplicate) then
do_research(player,fields)
return
end
end)
-- Configure i3 UI
elseif researcher.dependencies.i3 then
i3.new_tab("research",{
description = "Research",
slots = true,
formspec = function(player, data, fs)
fs(get_ui(player:get_player_name()))
end,
fields = function(player, data, fields)
if fields.research or fields.duplicate then
do_research(player,fields,false)
end
end,
})
-- Configure universal/standalone UI
else
minetest.register_on_player_receive_fields(function(player,formname,fields)
if formname == "researcher:player_research" then
do_research(player,fields)
return
end
end)
end
-- Refresh UI when research inventory changes
minetest.register_on_player_inventory_action(function(player, action, inventory, inventory_info)
if inventory_info.listname == "research" or inventory_info.to_list == "research" or inventory_info.from_list == "research" then
refresh_ui(player)
return true
end
return false
end)

View file

@ -0,0 +1,62 @@
-- Initialize player research inventory
minetest.register_on_joinplayer(function(player)
player:get_inventory():set_size("research",1)
end)
-- Set the subject of the player's research based on their research inventory
minetest.register_on_player_inventory_action(function(player, action, inventory, inventory_info)
if inventory_info.listname == "research" or inventory_info.to_list == "research" or inventory_info.from_list == "research" then
-- Get item info from research inventory
local item = inventory:get_stack("research",1)
-- Get player name and data
local player_name = player:get_player_name()
local player_data = researcher.get_player_data(player_name)
-- Cache inventory empty status
player_data.subject.is_empty = item:is_empty()
-- Set the player's research subject if research inventory has an item in it
local name = item:get_name()
local subject = player_data.subject
local description, groups
if not item:is_empty() then
subject.name = name
subject.item = researcher.registered_items[name]
subject.image = name
description = minetest.registered_items[name]
if description then
subject.description = description.description:split("\n",1)[1]
else
subject.description = "???"
end
if subject.item then
subject.groups = (function()
local str = ""
local grouplist = {}
for group,_ in pairs(subject.item.groups) do
table.insert(grouplist,group)
end
table.sort(grouplist)
return table.concat(grouplist,", ")
end)()
subject.research = player_data.research[name]
else
subject.groups = "(groups unknown)"
subject.research = nil
subject.item = {
name = "???",
groups = {},
adjustments = {},
}
end
end
-- Save player data
researcher.save_player_data(player_name)
return true
end
return false
end)

View file

@ -0,0 +1,178 @@
-- Function to get formspec string
local function get_formspec(item)
-- Set item info string
local iteminfo = [[
box[1,2.75;6,0.001;#00000099]
hypertext[2.3,3.15;4,1.5;;<global halign=center>(place item above to set focus)]
]]
if item then
local description = minetest.registered_items[item] and minetest.registered_items[item].description:split("\n")[1]
local groups = researcher.registered_items[item] and (function()
local str = ""
local grouplist = {}
for group,_ in pairs(researcher.registered_items[item].groups) do
table.insert(grouplist,group)
end
table.sort(grouplist)
return table.concat(grouplist,", ")
end)()
iteminfo = string.format([[
box[1,2.75;6,0.001;#00000099]
hypertext[2.3,3;4,0.5;;<global halign=center size=24><b>%s</b>]
hypertext[2.3,3.35;4,1.5;;<global halign=center size=18>%s]
]],
description or "???",
groups or "(groups unknown)")
end
return string.format([[
size[8,9.1]
box[0,0;7.8,5;#00000040]
image[3.1,0.575;2,2;researcher_research_inventory_decor_%s.png]
list[context;focus;3.5,1;1,1;0]
listring[current_player;main]
listring[context;focus]
%s
image[0,5.2;1,1;researcher_gui_hb_bg.png]
image[1,5.2;1,1;researcher_gui_hb_bg.png]
image[2,5.2;1,1;researcher_gui_hb_bg.png]
image[3,5.2;1,1;researcher_gui_hb_bg.png]
image[4,5.2;1,1;researcher_gui_hb_bg.png]
image[5,5.2;1,1;researcher_gui_hb_bg.png]
image[6,5.2;1,1;researcher_gui_hb_bg.png]
image[7,5.2;1,1;researcher_gui_hb_bg.png]
list[current_player;main;0,5.2;8,1;]
list[current_player;main;0,6.35;8,3;8]
]],
item and "active" or "inactive",
iteminfo)
end
-- Function for refreshing infotext when node meta inventory changes
local function update_metadata(pos)
local meta = minetest.get_meta(pos)
local item = meta:get_inventory():get_stack("focus",1)
if item and not item:is_empty() then
local iname = item:get_name()
item = minetest.registered_items[iname]
description = ""
if item and item.description then
meta:set_string("infotext","Research Table: " .. item.description:split("\n")[1])
end
meta:set_string("formspec",get_formspec(iname))
else
meta:set_string("formspec",get_formspec(nil))
meta:set_string("infotext","Research Table")
end
end
-- Register research table node
minetest.register_node("researcher:research_table",{
-- Node definition fields
description = "Research Table",
short_description = "Research Table",
drawtype = "mesh",
mesh = "research_table.obj",
tiles = {
{ name = "researcher_research_table_frame.png" },
{ name = "researcher_research_table_surface.png" },
},
paramtype2 = "4dir",
stack_max = 1,
sounds = (function()
if researcher.dependencies.default then
return default.node_sound_wood_defaults()
elseif researcher.dependencies.mcl_sounds then
return mcl_sounds.node_sound_wood_defaults()
else
return nil -- no specific sounds
end
end)(),
-- Set research table groups
groups = {
oddly_breakable_by_hand = 1,
},
-- Initialize research table data
on_construct = function(pos)
-- Set inventory size
local meta = minetest.get_meta(pos)
local inventory = meta:get_inventory()
inventory:set_size("focus", 1)
-- Set infotext
meta:set_string("infotext","Research Table")
-- Set meta formspec
meta:set_string("formspec",get_formspec(nil))
end,
-- Drop inventory contents when destroyed
on_destruct = function(pos)
local item = minetest.get_meta(pos):get_inventory():get_stack("focus",1)
if item and not item:is_empty() then
minetest.add_item(pos,item)
end
end,
-- Update infotext when inventory changes
on_metadata_inventory_move = update_metadata,
on_metadata_inventory_take = update_metadata,
on_metadata_inventory_put = update_metadata,
})
-- Register research table crafting recipe
minetest.register_craft({
output = "researcher:research_table",
recipe = {
{"group:stone", "group:stone", "group:stone"},
{"group:wood", "group:wood", "group:wood"},
{"group:wood", "", "group:wood"},
},
})
-- Register ABM for activation particles
minetest.register_abm({
label = "Researcher: Research Table Activation Particles",
nodenames = {"researcher:research_table"},
interval = 4,
chance = 1,
catch_up = false,
action = function(pos)
-- Do nothing if research table is empty
if minetest.get_meta(pos):get_inventory():get_stack("focus",1):is_empty() then
return
end
-- Show item particles to nearby players
local radius = researcher.settings.research_table_player_radius
for object in minetest.objects_in_area(pos:add(-2),pos:add(2)) do
if object:is_player() then
minetest.add_particlespawner({
playername = object:get_player_name(),
amount = 16,
time = 4,
pos = {
min = pos:add(vector.new(-0.6,0,-0.6)),
max = pos:add(vector.new(0.6,0,0.6)),
},
minsize = 1,
maxsize = 1.5,
minvel = { x = 0, y = 0.05, z = 0 },
maxvel = { x = 0, y = 0.1, z = 0 },
minacc = { x = 0, y = 0.1, z = 0 },
maxacc = { x = 0, y = 0.2, z = 0 },
minexptime = 4.5,
maxexptime = 3,
texture = "plus.png^[colorize:#ffff77^[opacity:180",
glow = 14,
collisiondetection = false,
})
end
end
end,
})

View file

@ -0,0 +1,195 @@
-- Scan all items for groups
minetest.register_on_mods_loaded(function()
-- Reduce research difficulty for items not present in mapgen
local mapgen_nodes = {}
if researcher.settings.discount_mapgen < 0 then
mapgen_nodes = {
map_drop = function(self,node)
local def = minetest.registered_nodes[node]
if not def then
return "<ndef>"
end
local drop = def.drop
if not drop or drop == "" then
self[node] = true
return node
elseif type(drop) == "string" then
self[drop] = true
return drop
elseif type(drop) == "table" then
for _,item in ipairs(drop.items or {}) do
if type(item) == "table" then
for _,i in ipairs(item.items) do
self[i] = true
end
end
end
end
end,
}
for _,def in pairs(minetest.registered_biomes) do
for _,node_type in ipairs({
"node_top",
"node_filler",
"node_stone",
"node_water_top",
"node_water",
"node_river_water",
"node_riverbed",
"node_cave_liquid",
"node_dungeon",
"node_dungeon_alt",
"node_dungeon_stair",
}) do
if def[node_type] then
local node = def[node_type]
if type(node) == "string" then
mapgen_nodes[node] = true
mapgen_nodes:map_drop(node)
elseif type(node) == "table" then
for _,n in ipairs(node) do
if type(n) == "string" then
mapgen_nodes[n] = true
mapgen_nodes:map_drop(n)
end
end
end
end
end
end
local read_schematics = {}
for _,def in pairs(minetest.registered_decorations) do
if (not def.deco_type or def.deco_type == "simple") and def.decoration then
if type(def.decoration) == "string" then
mapgen_nodes[def.decoration] = true
mapgen_nodes:map_drop(def.decoration)
elseif type(def.decoration) == "table" then
for _,node in ipairs(def.decoration) do
if type(node) == "string" then
mapgen_nodes[node] = true
local to = mapgen_nodes:map_drop(node)
end
end
end
elseif def.deco_type == "schematic" and def.schematic then
local schematic
if type(def.schematic) == "string" and not read_schematics[def.schematic] then
read_schematics[def.schematic] = true
schematic = minetest.read_schematic(def.schematic,{ write_yslice_prob = "none" })
elseif type(def.schematic) == "table" then
schematic = def.schematic
end
if schematic then
for _,node in ipairs(schematic.data) do
if type(node.name) == "string" and node.name ~= "air" then
mapgen_nodes[node.name] = true
mapgen_nodes:map_drop(node.name)
end
end
end
end
end
for _,def in pairs(minetest.registered_ores) do
if type(def.ore) == "string" then
mapgen_nodes[def.ore] = true
mapgen_nodes:map_drop(def.ore)
end
end
researcher.register_adjustment({
name = "researcher:discount_mapgen",
reason = "Item not abundant in the world",
calculate = function(item)
return (not mapgen_nodes[item]) and researcher.settings.discount_mapgen or 0
end,
})
else
researcher.register_adjustment({
name = "researcher:discount_mapgen",
reason = "Item not abundant in world",
calculate = function() return 0 end,
})
end
-- Register low stack discount
local stack_max = researcher.dependencies.mcl_inventory and 64 or tonumber(minetest.settings:get("default_stack_max",99) or 99)
local low_stack = {}
if researcher.settings.discount_stack_max < 0 then
researcher.register_adjustment({
name = "researcher:discount_stack_max",
reason = "Item has a stack max less than the default max",
calculate = function(item)
return low_stack[item] and researcher.settings.discount_stack_max or 0
end,
})
else
researcher.register_adjustment({
name = "researcher:discount_stack_max",
reason = "Item has a stack max less than the default max",
calculate = function() return 0 end,
})
end
-- Reduce research difficulty for items that are not craftable
local not_craftable = {}
if researcher.settings.discount_not_craftable < 0 then
researcher.register_adjustment({
name = "researcher:discount_not_craftable",
reason = "Item is not craftable",
calculate = function(item)
return (not mapgen_nodes[item] and not_craftable[item]) and researcher.settings.discount_not_craftable or 0
end,
})
else
researcher.register_adjustment({
name = "researcher:discount_not_craftable",
reason = "Item is not craftable",
calculate = function() return 0 end,
})
end
-- Register items with Researcher
for name,def in pairs(minetest.registered_items) do
-- Get low stack data
if def.tool_capabilities or ((def.stack_max or stack_max) < stack_max) then
low_stack[name] = def.stack_max or (def.tool_capabilities and 1) or stack_max
end
-- Filter out unwanted groups
local groups = def.groups
local keep_groups = {}
if groups then
for group,value in pairs(groups) do
if value > 0 and not researcher.excluded_groups[group] then
table.insert(keep_groups,group)
end
end
end
-- Check for valid recipes
local recipes = minetest.get_all_craft_recipes(name) or {}
for _,recipe in ipairs(recipes) do
if recipe.method == "normal" or recipe.method == "cooking" then
recipes = 1
break
end
end
if recipes ~= 1 then
not_craftable[name] = true
end
-- Register item
researcher.register_item({
name = name,
points_per_level = researcher.settings.points_per_level,
groups = keep_groups,
})
end
end)