1
0
mirror of https://bitbucket.org/minetest_gamers/x_enchanting.git synced 2025-06-28 22:06:17 +02:00

Compare commits

...

5 Commits

9 changed files with 332 additions and 40 deletions

View File

@ -8,6 +8,7 @@ Adds Enchanting Mechanics and API.
* adds enchanting table
* supports all registered tools with known tool groups: pickaxe, shovel, axe, sword, e.g. `groups = {pickaxe = 1}`
* support all bows with group `{bow = 1}`
* supports `default:bookshelf` or anything with `group:bookshelf`
* enchanting trade is for `default:mese_crystal` or anything with `groups = {enchanting_trade = 1}`
* adds enchantability for all MT default tools, for custom tools the enchantability can be set in the item group, e.g. `groups = {enchantability = 15}`
@ -50,30 +51,123 @@ Items enchantibility from worst to best:
Increases melee damage.
groups: sword
#### Fortune
Increases the number and/or chances of specific item drops. This value is not used in the engine; it is the responsibility of the game/mod code to implement this.
Increases the number and/or chances of specific item drops. Works with groups: stone, soil, sand, snowy, slippery, tree, leaves and all registered ores.
Incompatible: Silk Touch
groups: pickaxe, shovel, axe
#### Unbreaking
Increases the item's durability.
groups: any
#### Efficiency
Increases the player's mining speed. Also adds mining groupcaps to item, e.g. enchanted wood pickaxe can mine level 1 nodes (e.g. obsidian) after enchantment.
groups: pickaxe, shovel, axe
#### Silk Touch
Causes certain blocks to drop themselves as items instead of their usual drops when mined. Mods can prevent this behaviour with adding group `{ no_silktouch = 1 }` to the nodes.
Incompatible: Fortune
groups: pickaxe, shovel, axe
#### Curse of Vanishing
Causes the item to disappear on death.
### Knockback
groups: any
#### Knockback
Increases knockback (players only).
groups: sword
#### Looting
Cause mobs to drop more items. This value is not used in the engine; it is the responsibility of the game/mod code to implement this.
groups: sword
#### Power
Increases arrow damage.
Damage has to be calculated in the MOD where the bow comes from!
groups: bow
#### Punch
Increases arrow knockback.
Knockback has to be calculated in the MOD where the bow comes from!
This can be obtained from tool meta:
groups: bow
#### Infinity
Prevents regular arrows from being consumed when shot.
One arrow is needed INSIDE QUIVER to use a bow enchanted with Infinity.
Fired arrows cannot be retrieved even if they are not fired from Quiver.
Only set in item meta, logic for this has to be in the MOD where the bow comes from!
groups: bow
## API
`ItemStackMetaRef`
* `get_float(key)`: Returns `0` if key not present. `key` can be enchantment id prefixed with `is_`,
e.g. enchantment `punch` would have stored meta as `is_punch`. If returned value is bigger than zero
then the value represents enchantment level bonus. See below fields for bow:
* `power` Increase percentage
* `punch` Multiplier
* `infinity` If `1` then it is infinity enchanted
* `get_string(key)`: Returns `""` if key not present. See below fields for all enchantments:
* `x_enchanting` Serialized table with key/value pairs where: `key` is enchantment `id` and `value` is `Enchantment` definition
`Enchantment` definition
* `value` number, Value of the enchantment based on level, e.g. multiplier, percentage/number increase...
example:
```lua
-- For simplicity assuming that all meta are present (biggger than zero or not "")
-- MODs have to add those checks individually
local itemstack_meta = itemstack:get_meta()
local power_value = itemstack_meta:get_float('is_power')
local punch_value = itemstack_meta:get_float('is_punch')
local infinity_value = itemstack_meta:get_float('is_infinity')
-- Or for list of all enchantments
local x_enchanting = minetest.deserialize(itemstack_meta:get_string('x_enchanting')) or {}
local power_enchantment = enchantments.power
local punch_enchantment = enchantments.punch
local infinity_enchantment = enchantments.infinity
-- Custom logic
local new_damage = damage + damage * (punch_enchantment.value / 100)
local new_knockback = knockback * punch_enchantment.value
if enchantments.infinity.value > 0 then
-- Some logic for infinity
end
```
## Dependencies
- none

193
api.lua
View File

@ -62,7 +62,29 @@ XEnchanting = {
[4] = 5,
[5] = 6.25,
},
weight = 10
weight = 10,
groups = {
'sword'
}
},
looting = {
name = S('Looting'),
-- what level should be taken, `level = min/max values`
final_level_range = {
[1] = { 15, 65 },
[2] = { 24, 74 },
[3] = { 33, 83 }
},
-- level definition, `level = number to add`
level_def = {
[1] = 1,
[2] = 2,
[3] = 3
},
weight = 2,
groups = {
'sword'
}
},
fortune = {
name = S('Fortune'),
@ -78,7 +100,13 @@ XEnchanting = {
[2] = 2,
[3] = 3
},
weight = 2
weight = 2,
groups = {
'pickaxe',
'shovel',
'axe'
},
incompatible = { 'silk_touch' }
},
unbreaking = {
name = S('Unbreaking'),
@ -94,7 +122,9 @@ XEnchanting = {
[2] = 200,
[3] = 300
},
weight = 5
weight = 5,
-- all applicable
groups = nil
},
efficiency = {
name = S('Efficiency'),
@ -114,7 +144,12 @@ XEnchanting = {
[4] = 40,
[5] = 45,
},
weight = 10
weight = 10,
groups = {
'pickaxe',
'shovel',
'axe'
}
},
silk_touch = {
name = S('Silk Touch'),
@ -125,7 +160,13 @@ XEnchanting = {
[1] = 1
},
weight = 1,
secondary = true
secondary = true,
groups = {
'pickaxe',
'shovel',
'axe'
},
incompatible = { 'fortune' }
},
curse_of_vanishing = {
name = S('Curse of Vanishing'),
@ -136,7 +177,9 @@ XEnchanting = {
[1] = 1
},
weight = 1,
secondary = true
secondary = true,
-- all applicable
groups = nil
},
knockback = {
name = S('Knockback'),
@ -149,7 +192,70 @@ XEnchanting = {
[1] = 105,
[2] = 190
},
weight = 5
weight = 5,
groups = {
'sword'
}
},
power = {
-- Increases arrow damage.
-- Damage has to be calculated in the MOD where the bow comes from!
name = S('Power'),
final_level_range = {
[1] = { 1, 16 },
[2] = { 11, 26 },
[3] = { 21, 36 },
[4] = { 31, 46 },
[5] = { 41, 56 }
},
-- increase %
level_def = {
[1] = 50,
[2] = 75,
[3] = 100,
[4] = 125,
[5] = 150
},
weight = 10,
groups = {
'bow'
}
},
punch = {
-- Increases arrow knockback.
-- Knockback has to be calculated in the MOD where the bow comes from!
name = S('Punch'),
final_level_range = {
[1] = { 12, 37 },
[2] = { 32, 57 }
},
-- multiplier
level_def = {
[1] = 3,
[2] = 6
},
weight = 2,
groups = {
'bow'
}
},
infinity = {
-- Prevents regular arrows from being consumed when shot.
-- One arrow is needed to use a bow enchanted with Infinity.
-- Only set in item meta, logic for this has to be in the MOD where the bow comes from!
name = S('Infinity'),
final_level_range = {
[1] = { 20, 50 }
},
-- will be set in meta as float
level_def = {
[1] = 1
},
weight = 1,
secondary = true,
groups = {
'bow'
}
},
},
form_context = {},
@ -159,7 +265,8 @@ XEnchanting = {
scroll_close = { { x = 45, y = 84 }, 80, 0, false },
scroll_open_idle = { { x = 41, y = 42 }, 0, 0, false },
scroll_closed_idle = { { x = 43, y = 44 }, 0, 0, false }
}
},
registered_ores = {}
}
---Merge two tables with key/value pair
@ -192,17 +299,22 @@ function XEnchanting.has_tool_group(self, name)
return 'axe'
elseif minetest.get_item_group(name, 'sword') > 0 then
return 'sword'
elseif minetest.get_item_group(name, 'bow') > 0 then
return 'bow'
end
return false
end
function XEnchanting.set_tool_enchantability(self, tool_def)
if minetest.get_item_group(tool_def.name, 'enchantability') > 0 then
-- enchantability is already set, we dont need to override the item
return
end
local _enchantability = 1
if minetest.get_item_group(tool_def.name, 'enchantability') > 0 then
_enchantability = minetest.get_item_group(tool_def.name, 'enchantability')
elseif self.tools_enchantability[tool_def.name] then
if self.tools_enchantability[tool_def.name] then
_enchantability = self.tools_enchantability[tool_def.name]
end
@ -318,7 +430,7 @@ function XEnchanting.get_enchanted_tool_capabilities(self, tool_def, enchantment
end
-- Fortune
if enchantment.id == 'fortune' and tool_capabilities.max_drop_level then
if enchantment.id == 'fortune' or enchantment.id == 'looting' and tool_capabilities.max_drop_level then
local old_max_drop_level = tool_capabilities.max_drop_level
local new_max_drop_level = old_max_drop_level + enchantment.value
@ -390,16 +502,22 @@ function XEnchanting.set_enchanted_tool(self, pos, itemstack, level, player_name
end
local stack_meta = itemstack:get_meta()
---@type table<string, {["value"]: number}>
local final_enchantments_meta = {}
for i, enchantment in ipairs(final_enchantments) do
stack_meta:set_float('is_' .. enchantment.id, enchantment.value)
-- store only necessary data, keeping the meta optimized
final_enchantments_meta[enchantment.id] = {
value = enchantment.value
}
end
stack_meta:set_tool_capabilities(capabilities)
stack_meta:set_string('description', itemstack:get_description() .. '\n' .. description)
stack_meta:set_string('short_description', S('Enchanted') .. ' ' .. itemstack:get_short_description())
stack_meta:set_int('is_enchanted', 1)
stack_meta:set_string('x_enchanting', minetest.serialize({ enchantments = final_enchantments }))
stack_meta:set_string('x_enchanting', minetest.serialize(final_enchantments_meta))
inv:set_stack('item', 1, itemstack)
@ -433,6 +551,25 @@ function XEnchanting.get_enchantment_data(self, player, nr_of_bookshelfs, tool_d
_nr_of_bookshelfs = 15
end
----
-- Filter out enchantments compatible for this item group
----
local group_enchantments = {}
for enchantment_name, enchantment_def in pairs(self.enchantment_defs) do
if not enchantment_def.groups then
group_enchantments[enchantment_name] = enchantment_def
else
for i, group in ipairs(enchantment_def.groups) do
if minetest.get_item_group(tool_def.name, group) > 0 then
group_enchantments[enchantment_name] = enchantment_def
break
end
end
end
end
----
-- 0 Show slots in formspec
----
@ -474,7 +611,7 @@ function XEnchanting.get_enchantment_data(self, player, nr_of_bookshelfs, tool_d
-- Get level
-- If the modified level is within two overlapping ranges for the same
-- enchantment type, the higher power value is used.
for enchantment_name, enchantment_def in pairs(self.enchantment_defs) do
for enchantment_name, enchantment_def in pairs(group_enchantments) do
local levels = {}
-- find matching levels
@ -495,7 +632,8 @@ function XEnchanting.get_enchantment_data(self, player, nr_of_bookshelfs, tool_d
id = enchantment_name,
value = enchantment_def.level_def[level],
level = level,
secondary = enchantment_def.secondary
secondary = enchantment_def.secondary,
incompatible = enchantment_def.incompatible
})
end
end
@ -513,10 +651,11 @@ function XEnchanting.get_enchantment_data(self, player, nr_of_bookshelfs, tool_d
total_weight = total_weight + self.enchantment_defs[enchantment.id].weight
end
-- Pick a random integer in the half range [0; total_weight] as a number `rand_weight`
-- Pick a random integer in the half range [0; total_weight / 2] as a number `rand_weight`
local rand_weight = math.random(0, total_weight / 2)
-- local probability = (final_level + 1) / 50
local probability_level = final_level
---@type Enchantment[]
local possible_enchantments_excl_secodnary = {}
for _, enchantment in pairs(possible_enchantments) do
@ -533,22 +672,40 @@ function XEnchanting.get_enchantment_data(self, player, nr_of_bookshelfs, tool_d
local rand_ench = possible_enchantments[rand_ench_idx]
if j == 1 then
-- First pick
-- Dont add cursed/secondary enchantment as first pick
rand_ench_idx = math.random(1, #possible_enchantments_excl_secodnary)
rand_ench = possible_enchantments_excl_secodnary[rand_ench_idx]
table.insert(final_enchantments, rand_ench)
for idx, value in pairs(possible_enchantments) do
if rand_ench.id == value.id then
table.remove(possible_enchantments, idx)
end
-- remove incomaptible enchantments
if rand_ench.incompatible
and table.indexof(rand_ench.incompatible, value.id) ~= -1
then
table.remove(possible_enchantments, idx)
end
end
table.insert(final_enchantments, rand_ench)
else
local probability = (probability_level + 1) / 50
table.remove(possible_enchantments, rand_ench_idx)
table.insert(final_enchantments, rand_ench)
table.remove(possible_enchantments, rand_ench_idx)
for idx, value in pairs(possible_enchantments) do
-- remove incomaptible enchantments
if rand_ench.incompatible
and table.indexof(rand_ench.incompatible, value.id) ~= -1
then
table.remove(possible_enchantments, idx)
end
end
-- With probability (`final_level` + 1) / 50, keep going. Otherwise, stop picking bonus enchantments.
local rand_probability = math.random()

View File

@ -13,6 +13,12 @@ minetest.register_on_mods_loaded(function()
XEnchanting:set_tool_enchantability(tool_def)
end
end
for _, def in pairs(minetest.registered_ores) do
if not XEnchanting.registered_ores[def.ore] then
XEnchanting.registered_ores[def.ore] = true
end
end
end)
minetest.register_on_joinplayer(function(player, last_login)
@ -27,7 +33,6 @@ minetest.register_on_leaveplayer(function(player, timed_out)
XEnchanting.form_context[player:get_player_name()] = nil
end)
-- Silk Touch
local old_handle_node_drops = minetest.handle_node_drops
function minetest.handle_node_drops(pos, drops, digger)
@ -37,29 +42,64 @@ function minetest.handle_node_drops(pos, drops, digger)
return old_handle_node_drops(pos, drops, digger)
end
local node = minetest.get_node(pos)
local wield_stack = digger:get_wielded_item()
local wield_stack_meta = wield_stack:get_meta()
if wield_stack_meta:get_float('is_silk_touch') == 0 then
-- Fortune
local fortune = wield_stack_meta:get_float('is_fortune')
if fortune > 0 then
local new_drops = {}
for _, itemstring in ipairs(drops) do
if XEnchanting.registered_ores[node.name]
or minetest.get_item_group(node.name, 'stone') > 0
or minetest.get_item_group(node.name, 'soil') > 0
or minetest.get_item_group(node.name, 'sand') > 0
or minetest.get_item_group(node.name, 'snowy') > 0
or minetest.get_item_group(node.name, 'slippery') > 0
or minetest.get_item_group(node.name, 'tree') > 0
or minetest.get_item_group(node.name, 'leaves') > 0
then
local tool_capabilities = wield_stack:get_tool_capabilities()
local stack = ItemStack(itemstring)
local chance = math.random(1, tool_capabilities.max_drop_level)
stack:set_count(stack:get_count() * chance)
if stack:get_count() > 0 then
table.insert(new_drops, stack)
end
end
end
if #new_drops > 0 then
return old_handle_node_drops(pos, new_drops, digger)
end
return old_handle_node_drops(pos, drops, digger)
end
local node = minetest.get_node(pos)
-- Silk Touch
local silk_touch = wield_stack_meta:get_float('is_silk_touch')
if minetest.get_item_group(node.name, 'no_silktouch') == 1 then
return old_handle_node_drops(pos, drops, digger)
if silk_touch > 0
and minetest.get_item_group(node.name, 'no_silktouch') == 0
then
-- drop raw item/node
return old_handle_node_drops(pos, { ItemStack(node.name) }, digger)
end
-- drop raw item/node
return old_handle_node_drops(pos, { ItemStack(node.name) }, digger)
return old_handle_node_drops(pos, drops, digger)
end
minetest.register_on_player_hpchange(function(player, hp_change, reason)
-- Curse of Vanishing
if (player:get_hp() + hp_change) <= 0 then
-- Going to die
local player_inv = player:get_inventory() --[[@as InvRef]]
-- Curse of Vanishing
local player_inventory_lists = { 'main', 'craft' }
for _, list_name in ipairs(player_inventory_lists) do

View File

@ -15,6 +15,7 @@
---@field add_entity fun(pos: Vector, name: string, staticdata?: string): ObjectRef|nil Spawn Lua-defined entity at position. Returns `ObjectRef`, or `nil` if failed.
---@field get_node fun(pos: Vector): NodeDef Returns the node at the given position as table in the format `{name="node_name", param1=0, param2=0}`, returns `{name="ignore", param1=0, param2=0}` for unloaded areas.
---@field registered_nodes table<string, NodeDef|ItemDef> Map of registered node definitions, indexed by name
---@field registered_ores table<string, table> Map of registered ore definitions, indexed by name
---@field after fun(time: number|integer, func: fun(...), ...): JobTable Call the function `func` after `time` seconds, may be fractional. Optional: Variable number of arguments that are passed to `func`.
---@field sound_play fun(spec: SimpleSoundSpec|string, parameters: SoundParamDef, ephemeral?: boolean): any Returns a `handle`. Ephemeral sounds will not return a handle and can't be stopped or faded. It is recommend to use this for short sounds that happen in response to player actions (e.g. door closing).
---@field add_particlespawner fun(particlespawner_definition: ParticlespawnerDef): number|integer Add a `ParticleSpawner`, an object that spawns an amount of particles over `time` seconds. Returns an `id`, and -1 if adding didn't succeed.

View File

@ -5,6 +5,8 @@
---@class MtgFarming
---@field hoe_on_use fun(itemstack: ItemStack, user: ObjectRef, pointed_thing: PointedThingDef, uses: number): ItemStack | nil
---@field place_seed fun(itemstack: ItemStack, placer: ObjectRef, pointed_thing: PointedThingDef, plantname: string): ItemStack Seed placement
---@field grow_plant fun(pos: Vector, elapsed: number): nil
---@field register_plant fun(name: string, def: table): nil
----Node definition. Used by `minetest.register_node`.
---@class NodeDefMtgFarming

View File

@ -27,6 +27,7 @@
---@field on_destruct fun(pos: Vector) Node destructor; called before removing node. Not called for bulk node placement. default: nil
---@field on_blast fun(pos: Vector, intensity?: number): nil intensity: 1.0 = mid range of regular TNT. If defined, called when an explosion touches the node, instead of removing the node.
---@field on_timer fun(pos: Vector, elapsed: number): boolean | nil default: nil, called by NodeTimers, see minetest.get_node_timer and NodeTimerRef. elapsed is the total time passed since the timer was started. return true to run the timer for another cycle with the same timeout value.
---@field _next_state string Only for x_farming composter
---Textures of node; +Y, -Y, +X, -X, +Z, -Z. List can be shortened to needed length.
---@class NodeTilesDef

View File

@ -6,3 +6,4 @@
---Table helpers
---@class TableAbstract
---@field copy fun(table: table): table returns a deep copy of `table`
---@field indexof fun(list: table, value: any): number returns the smallest numerical index containing the value `val` in the table `list`. Non-numerical indices are ignored. If `val` could not be found, `-1` is returned. `list` must not have negative indices.

View File

@ -1,8 +0,0 @@
---@diagnostic disable: codestyle-check
---Base class Unified Inventory
---@class UnifiedInventory
---@field set_inventory_formspec fun(player: ObjectRef, formspecname: string): nil
---@field register_button fun(name: string, def: table): nil
---@field single_slot fun(x: number, y: number): nil
---@field register_page fun(name: string, def: table): nil
---@field style_full table

View File

@ -14,6 +14,7 @@
---@field form_context table<string, FormContextDef> Additional form data for player.
---@field scroll_animations table<string, table> Parameters for `ObjectRef` `set_animation` method
---@field player_seeds table<string, number | integer>
---@field registered_ores table<string, boolean> Table with registered ores, `key` ore name
---Enchantment definition
@ -23,6 +24,8 @@
---@field level_def table<number, number> Level bonus. Value will be set in item meta as float.
---@field weight number Enchantment weight
---@field secondary boolean Will not appear in masked description and can be applied only as additional enchantment by probability chance.
---@field groups string[] | nil List of groups for items what are compatible with this enchantment. If `nil` then all groups are compatible.
---@field incompatible string[] | nil List of incompatible enchantments
---Form context
@ -36,10 +39,11 @@
---@class Enchantment
---@field id string
---@field value number
---@field level number
---@field secondary boolean Will not appear in masked description and can be applied only as additional enchantment by probability chance.
---@field id string Unique ID of the enchantment
---@field value number Value of the enchantment based on level
---@field level number Level of the enchantment
---@field secondary boolean | nil Will not appear in masked description and can be applied only as additional enchantment by probability chance.
---@field incompatible string[] | nil List of incompatible enchantments
---@class EnchantmentDataSlot