Compare commits

...

11 Commits

Author SHA1 Message Date
trans_soup afc58d0b97 replace leaves decoration with saplings.
will replace with actual trees later.
2023-10-17 15:16:11 +02:00
trans_soup c5d51e9fe5 make nuts sometimes drop from leaves.
give living & growing leaves a chance of dropping nuts, finally making
trees actually farmable.
2023-10-17 15:14:02 +02:00
trans_soup d9523dc015 remove unused mod `bg_funnel`. 2023-10-17 15:07:04 +02:00
trans_soup d0a481334a refactor stoneworking recipe code. 2023-10-17 15:05:40 +02:00
trans_soup d197e1f1e3 move `item_matches` into api.
move function `item_matches` from `bg_crafting` into `bg_api`, since
it'll be useful in many other places.
2023-10-17 15:02:39 +02:00
trans_soup 7df5d33d02 refactor: extract repeated code into api function.
move function `starts_with`, which was declared twice in separate files,
to the api, and make those files use that function instead.
2023-10-17 14:58:15 +02:00
trans_soup c31d6cf5d0 improve pummeling.
pummeling definition tables now has lists of item id:s and groups, the
same way e.g. ABM:s do, that determine what items are involved in a
recipe.

update existing (in-use) pummel recipes to make use of this.
2023-10-17 14:53:08 +02:00
trans_soup 45f59f5e03 minor refactor of recipe code.
just separated different recipe types into different files.
2023-10-17 14:28:08 +02:00
trans_soup 5305cc526d minor fix & refactors of recipe code. 2023-10-17 14:23:44 +02:00
trans_soup 17636dbf87 improve leaves decomposition.
leaves will now decompose faster depending on their surroundings.
touching dirty nodes or other decomposing leaves will make them decay
quicker.
2023-10-17 13:41:30 +02:00
trans_soup cca53e76b3 add flood fill utility to api.
add api function for walking through the nodes surrounding a given
position.
2023-10-17 13:40:04 +02:00
23 changed files with 288 additions and 362 deletions

View File

@ -7,12 +7,13 @@ local blockgame = {}
rawset(_G, "blockgame", blockgame)
load_file("util_table")
load_file("util_stringify")
load_file("util_string")
load_file("util_debug")
load_file("util_random")
load_file("util_vector")
load_file("util_node")
load_file("util_item")
load_file("wrappers")

12
mods/bg_api/util_item.lua Normal file
View File

@ -0,0 +1,12 @@
function blockgame.item_matches (name, options)
return blockgame.any(options, function (option)
if name == option then return true end
if blockgame.starts_with(option, "group:") then
local group = string.sub(option, 7)
return minetest.get_item_group(name, group) > 0
end
return false
end)
end

View File

@ -48,3 +48,37 @@ function blockgame.random_walk (data)
end
return pos
end
function blockgame.flood_fill (start, step, max_range)
max_range = max_range or 4
local queue = { { pos = start, range = 0 } }
local visited = {}
local matches = {}
local rejected = {}
while #queue > 0 do
local current = table.remove(queue, 1)
local pos = current.pos
local range = current.range
local hash = minetest.hash_node_position(pos)
if not visited[hash] and range <= max_range and step(pos, range) then
table.insert(matches, pos)
local neighbors = blockgame.vector.get_neighbors(pos)
for _, p in pairs(neighbors) do
table.insert(queue, { pos = p, range = range + 1 })
end
else
table.insert(rejected, pos)
end
visited[hash] = true
end
return matches, rejected
end

View File

@ -15,3 +15,9 @@ function blockgame.stringify (tab)
return tab
end
end
function blockgame.starts_with (str, start)
return string.sub(str, 1, string.len(start)) == start
end

View File

@ -1,140 +1,9 @@
blockgame.crafting = blockgame.crafting or {}
local api = blockgame.crafting
local recipes = {}
api.registered_recipes = recipes
function api.register_stack_recipe (top_node, bottom_node, result, consumes_top)
local consumes_top = consumes_top
if consumes_top == nil then consumes_top = true end
recipes[top_node] = recipes[top_node] or {}
table.insert(recipes[top_node], {
["type"] = "stack_two_nodes",
index_key = top_node,
top_node = top_node,
bottom_node = bottom_node,
result = result,
consumes_top = consumes_top,
})
end
function api.handle_placement (pos, new_node, placer, old_node, itemstack, pointed_thing)
if recipes[new_node.name] == nil then return false end
for _, recipe in pairs(recipes[new_node.name]) do
local result = api.handle_stack_recipe(recipe, pos, new_node)
if result then return not recipe.consumes_top end
end
return false
end
function api.handle_stack_recipe (recipe, pos, top_node)
if top_node.name ~= recipe.top_node then return false end
local down = vector.add(pos, vector.new(0, -1, 0))
local below = minetest.get_node(down)
if below.name ~= recipe.bottom_node then return false end
minetest.remove_node(pos)
minetest.set_node(down, recipe.result)
return true
end
-- pummeling is heavily based on nodecore by Warr1024 (both the mechanic itself and the code here implementing it).
local pummel_recipes = {}
function api.register_pummel_recipe (def)
local def = def or {}
def.label = def.label or "unnamed pummel recipe"
-- TODO: throw errors when these defs are invalid instead of just returning.
if not def.used_item then return false end
if not def.target_node then return false end
if not type(def.on_success) == "function" then return false end
local key = def.used_item
def.index_key = key
pummel_recipes[key] = pummel_recipes[key] or {}
table.insert(pummel_recipes[key], def)
end
-- TODO: add support for pummel recipes using groups instead of just specific node names.
function api.pummel_check (pos, used_item, target_node)
local key = used_item
if not pummel_recipes[key] then return false end
local potential_recipes = pummel_recipes[key]
for _, def in pairs(potential_recipes) do
if def.target_node == target_node.name then
if type(def.check) == "function" and not def.check(pos, used_item, target_node) then
return false
end
def.on_success(pos, used_item, target_node)
return true
end
end
end
api.registered_recipes = {}
local pummeling = {}
-- NOTE: might wanna change this to somehow store pummels per-node instead of per-player?
blockgame.register_on_dignode(function (_, _, digger)
if not (digger and digger:is_player()) then return end
pummeling[digger:get_player_name()] = nil
end)
blockgame.register_on_punchnode(function (pos, node, puncher, pointed)
if (not puncher:is_player()) or puncher:get_player_control().sneak then return end
local player_name = puncher:get_player_name()
-- if not minetest.interact(player_name) then return end
if not minetest.check_player_privs(player_name, "interact") then return end
node = node or minetest.get_node(pos)
local node_def = minetest.registered_items[node.name] or {}
if not node_def.pointable then return end
local wield = puncher:get_wielded_item()
local now = minetest.get_us_time() / 1000000
local pummel_data = {
action = "pummel",
crafter = puncher,
crafter_name = player_name,
pos = pos,
pointed = pointed,
node = node,
node_def = node_def,
start = now,
wield = wield:get_name() .. " " .. wield:get_count(),
count = 0,
}
local old_data = pummeling[player_name]
local hash = minetest.hash_node_position
if old_data and hash(old_data.pos) == hash(pummel_data.pos)
and hash(old_data.pointed.above) == hash(pummel_data.pointed.above)
and hash(old_data.pointed.under) == hash(pummel_data.pointed.under)
and old_data.wield == pummel_data.wield
and old_data.last >= (now - 3)
then pummel_data = old_data
end
pummel_data.count = pummel_data.count + 1
pummel_data.last = now
pummel_data.duration = now - pummel_data.start - 1
pummeling[player_name] = pummel_data
if pummel_data.count < 2 then return end
if api.pummel_check(pos, wield:get_name(), minetest.get_node(pointed.under)) then
pummeling[player_name] = nil
end
end)
load_file("stack")
load_file("pummel")

View File

@ -1,7 +1 @@
local modname = minetest.get_current_modname()
load_file("api")
blockgame.register_on_placenode(function (...)
return blockgame.crafting.handle_placement(...)
end)

View File

@ -0,0 +1,96 @@
local api = blockgame.crafting
-- LOCALS
local matches = blockgame.item_matches
-- pummeling is heavily based on nodecore by Warr1024 (both the mechanic itself and the code here implementing it).
local pummel_recipes = {}
function api.register_pummel_recipe (def)
local def = def or {}
-- TODO: require `def.name` to be specified and unique.
def.label = def.label or "unlabeled pummel recipe"
-- TODO: throw errors when these defs are invalid instead of just returning.
if not def.used then return false end
if not def.target then return false end
if not type(def.on_success) == "function" then return false end
def["type"] = "pummel"
table.insert(pummel_recipes, def)
table.insert(api.registered_recipes, def)
end
-- TODO: add support for pummel recipes using groups instead of just specific node names.
function api.pummel_check (pos, used, target_node)
for _, def in pairs(pummel_recipes) do
if matches(used, def.used) and matches(target_node.name, def.target) then
if type(def.check) == "function" and not def.check(pos, used, target_node) then
return false
end
def.on_success(pos, used, target_node)
return true
end
end
end
local pummeling = {}
-- NOTE: might wanna change this to somehow store pummels per-node instead of per-player?
blockgame.register_on_dignode(function (_, _, digger)
if not (digger and digger:is_player()) then return end
pummeling[digger:get_player_name()] = nil
end)
blockgame.register_on_punchnode(function (pos, node, puncher, pointed)
if (not puncher:is_player()) or puncher:get_player_control().sneak then return end
local player_name = puncher:get_player_name()
-- if not minetest.interact(player_name) then return end
if not minetest.check_player_privs(player_name, "interact") then return end
node = node or minetest.get_node(pos)
local node_def = minetest.registered_items[node.name] or {}
if not node_def.pointable then return end
local wield = puncher:get_wielded_item()
local now = minetest.get_us_time() / 1000000
local pummel_data = {
action = "pummel",
crafter = puncher,
crafter_name = player_name,
pos = pos,
pointed = pointed,
node = node,
node_def = node_def,
start = now,
wield = wield:get_name() .. " " .. wield:get_count(),
count = 0,
}
local old_data = pummeling[player_name]
local hash = minetest.hash_node_position
if old_data and hash(old_data.pos) == hash(pummel_data.pos)
and hash(old_data.pointed.above) == hash(pummel_data.pointed.above)
and hash(old_data.pointed.under) == hash(pummel_data.pointed.under)
and old_data.wield == pummel_data.wield
and old_data.last >= (now - 3)
then pummel_data = old_data
end
pummel_data.count = pummel_data.count + 1
pummel_data.last = now
pummel_data.duration = now - pummel_data.start - 1
pummeling[player_name] = pummel_data
if pummel_data.count < 2 then return end
if api.pummel_check(pos, wield:get_name(), minetest.get_node(pointed.under)) then
pummeling[player_name] = nil
end
end)

View File

@ -0,0 +1,50 @@
local api = blockgame.crafting
local stack_recipes = {}
function api.register_stack_recipe (top_node, bottom_node, result, consumes_top)
local consumes_top = consumes_top
if consumes_top == nil then consumes_top = true end
stack_recipes[top_node] = stack_recipes[top_node] or {}
local def = {
["type"] = "stack_two_nodes",
index_key = top_node,
top_node = top_node,
bottom_node = bottom_node,
result = result,
consumes_top = consumes_top,
}
table.insert(stack_recipes[top_node], def)
table.insert(api.registered_recipes, def)
end
function api.handle_stack_recipe (recipe, pos, top_node)
if top_node.name ~= recipe.top_node then return false end
local down = vector.add(pos, vector.new(0, -1, 0))
local below = minetest.get_node(down)
if below.name ~= recipe.bottom_node then return false end
minetest.remove_node(pos)
minetest.set_node(down, recipe.result)
return true
end
function api.handle_placement (pos, new_node, placer, old_node, itemstack, pointed_thing)
if stack_recipes[new_node.name] == nil then return false end
for _, recipe in pairs(stack_recipes[new_node.name]) do
local result = api.handle_stack_recipe(recipe, pos, new_node)
if result then return not recipe.consumes_top end
end
return false
end
blockgame.register_on_placenode(function (...)
return blockgame.crafting.handle_placement(...)
end)

View File

@ -1,57 +0,0 @@
local function get_inventory (pos)
local meta = minetest.get_meta(pos)
return minetest.deserialize(meta:get_string("inventory"))
end
local function set_inventory (pos, inventory)
local meta = minetest.get_meta(pos)
meta:set_string("inventory", minetest.serialize(inventory))
end
local function push (pos, value)
local inventory = get_inventory(pos)
if not inventory then return false end
table.insert(inventory, value)
set_inventory(pos, inventory)
return true
end
local function pop (pos)
local inventory = get_inventory(pos)
if not inventory then return nil end
local value = table.remove(inventory, 1)
set_inventory(pos, inventory)
return value
end
local function place_funnel (itemstack, placed, pointed)
local result = minetest.item_place(itemstack, placer, pointed)
local pos = pointed.above
local meta = minetest.get_meta(pos)
set_inventory(pos, {})
return result
end
local function dig_funnel (pos, ...)
local result = minetest.node_dig(pos, ...)
return result
end
return {
get_inventory = get_inventory,
set_inventory = set_inventory,
place_funnel = place_funnel,
dig_funnel = dig_funnel,
push = push,
pop = pop,
}

View File

@ -1,82 +0,0 @@
local modname = minetest.get_current_modname()
local api = load_file("api")
local function get_meta_table (pos)
return minetest.get_meta(pos):to_table()
end
local function set_meta (pos, meta_table)
local meta = minetest.get_meta(pos)
for key, value in pairs(meta_table) do
local fn = nil
if type(value) == "string" then fn = "set_string" end
if type(value) == "int" then fn = "set_int" end
if type(value) == "float" then fn = "set_float" end
if fn then
meta[fn](key, value)
end
end
end
local function attempt_input (pos)
-- TODO: add max limit for amount of nodes inside funnel inventory.
local above = pos + blockgame.vector.dirs.up
if minetest.get_node(above).name == "air" then return false end
local node = minetest.get_node(above)
local meta = get_meta_table(above)
local value = {
node = node,
meta = meta,
}
local success = api.push(pos, value)
if not success then return false end
minetest.remove_node(above)
return true
end
local function attempt_output (pos)
local below = pos + blockgame.vector.dirs.down
if minetest.get_node(below).name ~= "air" then return false end
local value = api.pop(pos)
if not value then return nil end
local node = value.node
local meta = value.meta
minetest.set_node(below, node)
set_meta(below, meta)
return true
end
blockgame.register_abm({
label = "funnel nodes",
nodenames = {modname .. ":funnel"},
interval = 1,
chance = 1,
catch_up = false,
action = function (pos)
-- NOTE: metadata storage might not work, and funnels might not be able to pick up other funnels successfully.
attempt_input(pos)
attempt_output(pos)
end,
})
-- NOTE: the documentation says `register_on_placenode` is "not recommended"; what else could be used for this?
minetest.register_on_placenode(function (pos)
local below = pos + blockgame.vector.dirs.down
if minetest.get_node(below).name ~= modname .. ":funnel" then return end
attempt_input(below)
end)
-- NOTE: the documentation says `register_on_dignode` is "not recommended"; what else could be used for this?
minetest.register_on_dignode(function (pos)
local above = pos + blockgame.vector.dirs.up
if minetest.get_node(above).name ~= modname .. ":funnel" then return end
attempt_output(above)
end)

View File

@ -1,6 +0,0 @@
-- this mod is disabled for now.
--[[
load_file("node")
load_file("recipe")
load_file("funnel")
]]--

View File

@ -1,3 +0,0 @@
name = bg_funnel
description = adds a funnel node to blockgame.
depends = bg_terrain, bg_api, bg_tree, bg_woodworking

View File

@ -1,27 +0,0 @@
local modname = minetest.get_current_modname()
local api = load_file("api")
blockgame.register_node(modname .. ":funnel", {
description = "Funnel",
tiles = {
modname .. "_funnel_top.png",
modname .. "_funnel_top.png",
modname .. "_funnel.png",
},
groups = {
woody = 1,
},
on_place = function (...)
return api.place_funnel(...)
end,
on_dig = function (...)
return api.dig_funnel(...)
end,
preserve_metadata = function (pos, node, meta, drops)
-- TODO: store funnel contents inside dropped item.
end,
after_place_node = function (pos, placer, item_stack)
-- TODO: recreate funnel contents from placed item.
end,
})

View File

@ -1,16 +0,0 @@
local modname = minetest.get_current_modname()
blockgame.crafting.register_pummel_recipe({
label = "pummel logs into funnel",
used_item = "woodworking:plank",
target_node = "bg_tree:log",
check = function (pos, used_node, target_node)
local below = pos + blockgame.vector.dirs.down
return minetest.get_node(below).name == "bg_tree:log"
end,
on_success = function (pos, used_node, target_node)
local below = pos + blockgame.vector.dirs.down
minetest.remove_node(pos)
minetest.set_node(below, {name = modname .. ":funnel"})
end,
})

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 766 B

View File

@ -1,13 +1,17 @@
local modname = minetest.get_current_modname()
local function is_stoney_below (pos)
local below_node = minetest.get_node(pos + blockgame.vector.dirs.down)
return blockgame.item_matches(below_node.name, {"group:stoney"})
end
blockgame.crafting.register_pummel_recipe({
name = modname .. ":craft_tile",
label = "pummel stone into tile",
used_item = "bg_terrain:cobblestone",
target_node = "bg_terrain:cobblestone",
used = {"group:stoney"},
target = {"bg_terrain:cobblestone"},
check = function (pos, used_node, target_node)
local below_node = minetest.get_node(pos + blockgame.vector.dirs.down)
local stoney_value = minetest.get_item_group(below_node.name, "stoney")
return stoney_value ~= 0 and stoney_value ~= nil
return is_stoney_below(pos)
end,
on_success = function (pos, used_node, target_node)
minetest.set_node(pos, {name = modname .. ":tile"})
@ -15,13 +19,12 @@ blockgame.crafting.register_pummel_recipe({
})
blockgame.crafting.register_pummel_recipe({
name = modname .. ":craft_bricks",
label = "pummel tile into bricks",
used_item = "bg_terrain:cobblestone",
target_node = modname .. ":tile",
used = {"group:stoney"},
target = {modname .. ":tile"},
check = function (pos, used_node, target_node)
local below_node = minetest.get_node(pos + blockgame.vector.dirs.down)
local stoney_value = minetest.get_item_group(below_node.name, "stoney")
return stoney_value ~= 0 and stoney_value ~= nil
return is_stoney_below(pos)
end,
on_success = function (pos, used_node, target_node)
minetest.set_node(pos, {name = modname .. ":bricks"})

View File

@ -1,7 +1,3 @@
local function starts_with (str, start)
return string.sub(str, 1, string.len(start)) == start
end
-- rename from bg_core to bg_terrain.
minetest.register_lbm({
name = "bg_terrain:rename_core_mod",
@ -18,8 +14,8 @@ minetest.register_lbm({
run_at_every_load = true,
action = function (pos, node)
local prefix = ""
if starts_with(node.name, "core") then prefix = "core"
elseif starts_with(node.name, "bg_core") then prefix = "bg_core" end
if blockgame.starts_with(node.name, "core") then prefix = "core"
elseif blockgame.starts_with(node.name, "bg_core") then prefix = "bg_core" end
local name = string.sub(node.name, string.len(prefix) + 1)
minetest.set_node(pos, {name = "bg_terrain" .. name})

View File

@ -27,20 +27,57 @@ blockgame.register_increasing_abm({
end,
})
local decompose_node_scores = {
[leaves_decomposing] = 30,
}
local decompose_group_scores = {
dirty = 50,
}
local decompose_cost = 2000
blockgame.register_increasing_abm({
id = modname .. ":decompose",
label = "decompose leaves",
nodenames = {leaves_decomposing},
neighbors = {"group:dirty"},
interval = 20,
chance = 8,
neighbors = {"group:dirty", leaves_decomposing},
interval = 30,
chance = 10,
rate = function (pos, node, data)
-- TODO: decompose faster depending on surrounding dirty nodes & other decomposing leaves.
-- can probably do that with a flood fill that counts up a "score" depending on the nodes it encounters.
return data.value + math.random(1, 9)
local score = 100
local source_pos = pos
blockgame.flood_fill(pos, function (pos, distance)
if pos == source_pos then return true end
local name = minetest.get_node(pos).name
local gain = 0
for group, value in pairs(decompose_group_scores) do
if minetest.get_item_group(name, group) > 0 then
gain = math.max(gain, value)
end
end
gain = math.floor(gain / distance)
if gain > 0 then
score = score + gain
return true
end
if decompose_node_scores[name] then
score = score + math.floor(decompose_node_scores[name] / distance)
return true
end
return false
end, 4)
return data.value + score
end,
check = function (pos, node, data)
return data.value >= 25
return data.value >= decompose_cost
end,
action = function (pos, node, data)
if blockgame.chance(2) then

View File

@ -14,5 +14,5 @@ minetest.register_decoration({
},
y_min = -31000,
y_max = 31000,
decoration = modname .. ":leaves",
decoration = modname .. ":sapling",
})

View File

@ -62,18 +62,36 @@ local function reg_leaves (name, def)
blockgame.register_node(modname .. ":" .. name, def)
end
local nutty_drops = {
max_items = 3,
items = {
{
rarity = 8,
items = {modname .. ":nut"},
},
{
rarity = 12,
items = {modname .. ":nut 2"},
},
{
rarity = 1,
items = {modname .. ":leaves"},
},
},
}
reg_leaves("leaves", {
description = "Leaves",
})
reg_leaves("leaves_growing", {
description = "Growing Leaves",
drop = modname .. ":leaves",
texture = "leaves_alive",
drop = nutty_drops,
fall_check = leaves_fall_check,
})
reg_leaves("leaves_alive", {
description = "Leaves",
drop = modname .. ":leaves",
drop = nutty_drops,
fall_check = leaves_fall_check,
})
reg_leaves("leaves_decomposing", {

View File

@ -1,2 +1,2 @@
load_file("node")
load_file("recipes")
load_file("recipe")

View File

@ -1,9 +1,10 @@
local modname = minetest.get_current_modname()
blockgame.crafting.register_pummel_recipe({
label = "pummel log into plank",
used_item = "bg_terrain:cobblestone",
target_node = "bg_tree:log",
name = modname .. ":chop_log",
label = "chop log into plank",
used = {"group:stoney"},
target = {"bg_tree:log"},
check = function (pos, used_node, target_node)
for _, side in pairs(blockgame.vector.sides) do
local output_pos = pos + side