From 08655c3d502c041dd26c6601e8c6b09b716a3a03 Mon Sep 17 00:00:00 2001 From: haui Date: Wed, 12 Jun 2024 19:36:28 +0200 Subject: [PATCH] First commit --- enchantments.lua | 751 +++++++++++++++++++++++++++++++++++++++++++++++ engine.lua | 732 +++++++++++++++++++++++++++++++++++++++++++++ groupcaps.lua | 72 +++++ init.lua | 367 +++++++++++++++++++++++ mod.conf | 5 + 5 files changed, 1927 insertions(+) create mode 100644 enchantments.lua create mode 100644 engine.lua create mode 100644 groupcaps.lua create mode 100644 init.lua create mode 100644 mod.conf diff --git a/enchantments.lua b/enchantments.lua new file mode 100644 index 0000000..379a18e --- /dev/null +++ b/enchantments.lua @@ -0,0 +1,751 @@ +local S = minetest.get_translator(minetest.get_current_modname()) + +-- Taken from https://minecraft.gamepedia.com/Enchanting + +local function increase_damage(damage_group, factor) + return function(itemstack, level) + local tool_capabilities = itemstack:get_tool_capabilities() + tool_capabilities.damage_groups[damage_group] = (tool_capabilities.damage_groups[damage_group] or 0) + level * factor + itemstack:get_meta():set_tool_capabilities(tool_capabilities) + end +end + +-- implemented via on_enchant and additions in mobs_mc; Slowness IV part unimplemented +mcl_enchanting.enchantments.bane_of_arthropods = { + name = S("Bane of Arthropods"), + max_level = 5, + primary = {sword = true}, + secondary = {axe = true}, + disallow = {}, + incompatible = {smite = true, sharpness = true}, + weight = 5, + description = S("Increases damage and applies Slowness IV to arthropod mobs (spiders, cave spiders, silverfish and endermites)."), + curse = false, + on_enchant = increase_damage("anthropod", 2.5), + requires_tool = false, + treasure = false, + power_range_table = {{5, 25}, {13, 33}, {21, 41}, {29, 49}, {37, 57}}, + inv_combat_tab = true, + inv_tool_tab = false, +} + +-- requires missing MineClone2 feature +--[[mcl_enchanting.enchantments.channeling = { + name = S("Channeling"), + max_level = 1, + primary = {trident = true}, + secondary = {}, + disallow = {}, + incompatible = {riptide = true}, + weight = 1, + description = S("Channels a bolt of lightning toward a target. Works only during thunderstorms and if target is unobstructed with opaque blocks."), + curse = false, + on_enchant = function() end, + requires_tool = false, + treasure = false, + power_range_table = {{25, 50}}, + inv_combat_tab = true, + inv_tool_tab = false, +}]]-- + +-- implemented in mcl_death_drop +mcl_enchanting.enchantments.curse_of_vanishing = { + name = S("Curse of Vanishing"), + max_level = 1, + primary = {}, + secondary = {armor_head = true, armor_torso = true, armor_legs = true, armor_feet = true, tool = true, weapon = true}, + disallow = {}, + incompatible = {}, + weight = 1, + description = S("Item destroyed on death."), + curse = true, + on_enchant = function() end, + requires_tool = false, + treasure = true, + power_range_table = {{25, 50}}, + inv_combat_tab = true, + inv_tool_tab = true, +} + +-- implemented in mcl_playerplus +mcl_enchanting.enchantments.depth_strider = { + name = S("Depth Strider"), + max_level = 3, + primary = {}, + secondary = {armor_feet = true}, + disallow = {non_combat_armor = true}, + incompatible = {frost_walker = true}, + weight = 2, + description = S("Increases underwater movement speed."), + curse = false, + on_enchant = function() end, + requires_tool = false, + treasure = false, + power_range_table = {{10, 25}, {20, 35}, {30, 45}}, + inv_combat_tab = true, + inv_tool_tab = false, +} + +-- implemented via on_enchant +mcl_enchanting.enchantments.efficiency = { + name = S("Efficiency"), + max_level = 5, + primary = {pickaxe = true, shovel = true, axe = true, hoe = true}, + secondary = {shears = true}, + disallow = {}, + incompatible = {}, + weight = 10, + description = S("Increases mining speed."), + curse = false, + on_enchant = function() + -- Updating digging speed is handled by update_groupcaps which + -- is called from load_enchantments. + end, + requires_tool = false, + treasure = false, + power_range_table = {{1, 61}, {11, 71}, {21, 81}, {31, 91}, {41, 101}}, + inv_combat_tab = false, + inv_tool_tab = true, +} + +-- implemented in mcl_mobs and via register_on_punchplayer callback +mcl_enchanting.enchantments.fire_aspect = { + name = S("Fire Aspect"), + max_level = 2, + primary = {sword = true}, + secondary = {}, + disallow = {}, + incompatible = {}, + weight = 2, + description = S("Sets target on fire."), + curse = false, + on_enchant = function() end, + requires_tool = false, + treasure = false, + power_range_table = {{10, 61}, {30, 71}}, + inv_combat_tab = true, + inv_tool_tab = false, +} + +minetest.register_on_punchplayer(function(player, hitter, time_from_last_punch, tool_capabilities, dir, damage) + if hitter and hitter:is_player() then + local wielditem = hitter:get_wielded_item() + if wielditem then + local fire_aspect_level = mcl_enchanting.get_enchantment(wielditem, "fire_aspect") + if fire_aspect_level > 0 then + local player_pos = player:get_pos() + local hitter_pos = hitter:get_pos() + if vector.distance(hitter_pos, player_pos) <= 3 then + mcl_burning.set_on_fire(player, fire_aspect_level * 4) + end + end + end + end +end) + +mcl_enchanting.enchantments.flame = { + name = S("Flame"), + max_level = 1, + primary = {bow = true}, + secondary = {}, + disallow = {}, + incompatible = {}, + weight = 2, + description = S("Arrows set target on fire."), + curse = false, + on_enchant = function() end, + requires_tool = false, + treasure = false, + power_range_table = {{20, 50}}, + inv_combat_tab = true, + inv_tool_tab = false, +} + +-- implemented in mcl_item_entity +mcl_enchanting.enchantments.fortune = { + name = S("Fortune"), + max_level = 3, + primary = {pickaxe = true, shovel = true, axe = true, hoe = true}, + secondary = {}, + disallow = {}, + incompatible = {silk_touch = true}, + weight = 2, + description = S("Increases certain block drops."), + curse = false, + on_enchant = function() end, + requires_tool = false, + treasure = false, + power_range_table = {{15, 61}, {24, 71}, {33, 81}}, + inv_combat_tab = false, + inv_tool_tab = true, +} + +-- implemented via walkover.register_global +mcl_enchanting.enchantments.frost_walker = { + name = S("Frost Walker"), + max_level = 2, + primary = {}, + secondary = {armor_feet = true}, + disallow = {non_combat_armor = true}, + incompatible = {depth_strider = true}, + weight = 2, + description = S("Turns water beneath the player into frosted ice and prevents the damage from magma blocks."), + curse = false, + on_enchant = function() end, + requires_tool = false, + treasure = true, + power_range_table = {{10, 25}, {20, 35}}, + inv_combat_tab = true, + inv_tool_tab = false, +} + +walkover.register_global(function(pos, _, player) + local boots = player:get_inventory():get_stack("armor", 5) + local frost_walker = mcl_enchanting.get_enchantment(boots, "frost_walker") + if frost_walker <= 0 then + return + end + local radius = frost_walker + 2 + local minp = {x = pos.x - radius, y = pos.y, z = pos.z - radius} + local maxp = {x = pos.x + radius, y = pos.y, z = pos.z + radius} + local positions = minetest.find_nodes_in_area_under_air(minp, maxp, "mcl_core:water_source") + for _, p in ipairs(positions) do + if vector.distance(pos, p) <= radius then + minetest.set_node(p, {name = "mcl_core:frosted_ice_0"}) + end + end +end) + +-- requires missing MineClone2 feature +--[[mcl_enchanting.enchantments.impaling = { + name = S("Impaling"), + max_level = 5, + primary = {trident = true}, + secondary = {}, + disallow = {}, + incompatible = {}, + weight = 2, + description = S("Trident deals additional damage to ocean mobs."), + curse = false, + on_enchant = function() end, + requires_tool = false, + treasure = false, + power_range_table = {{1, 21}, {9, 29}, {17, 37}, {25, 45}, {33, 53}}, + inv_combat_tab = true, + inv_tool_tab = false, +}]]-- + +-- implemented in mcl_bows +mcl_enchanting.enchantments.infinity = { + name = S("Infinity"), + max_level = 1, + primary = {bow = true}, + secondary = {}, + disallow = {}, + incompatible = {mending = true}, + weight = 1, + description = S("Shooting consumes no regular arrows."), + curse = false, + on_enchant = function() end, + requires_tool = false, + treasure = false, + power_range_table = {{20, 50}}, + inv_combat_tab = true, + inv_tool_tab = false, +} + +-- implemented via minetest.calculate_knockback +mcl_enchanting.enchantments.knockback = { + name = S("Knockback"), + max_level = 2, + primary = {sword = true}, + secondary = {}, + disallow = {}, + incompatible = {}, + weight = 5, + description = S("Increases knockback."), + curse = false, + on_enchant = function() end, + requires_tool = false, + treasure = false, + power_range_table = {{5, 61}, {25, 71}}, + inv_combat_tab = true, + inv_tool_tab = false, +} + +local old_calculate_knockback = minetest.calculate_knockback +function minetest.calculate_knockback(player, hitter, time_from_last_punch, tool_capabilities, dir, distance, damage) + local knockback = old_calculate_knockback(player, hitter, time_from_last_punch, tool_capabilities, dir, distance, damage) + local luaentity + if hitter then + luaentity = hitter:get_luaentity() + end + if hitter and hitter:is_player() and distance <= 3 then + local wielditem = hitter:get_wielded_item() + --knockback = knockback + 3 * mcl_enchanting.get_enchantment(wielditem, "knockback") + local enchant = mcl_enchanting.get_enchantment(wielditem, "knockback") + knockback = knockback + 3.22 * enchant + -- add vertical lift to knockback + local v = player:get_velocity() + local added_v = 0 + local invul = player:get_meta():get_int("mcl_damage:invulnerable") + if v and v.y <= 0.01 and v.y >= -0.01 and invul == 0 then + local regular_v = 6.4 + local enchant_v = 7 + regular_v = regular_v * math.abs(dir.y - 1) + enchant_v = enchant_v * math.abs(dir.y - 1) + if enchant == 0 then + player:add_velocity({x = 0, y = regular_v, z = 0}) + added_v = regular_v + else + player:add_velocity({x = 0, y = enchant_v, z = 0}) + added_v = enchant_v + end + -- add minimum knockback + if knockback <= 1.5 then + knockback = knockback + 4.875 + elseif knockback <= 6.19 then + knockback = knockback + 0.609375 + end + end + -- counteract forward velocity when hit + local self_dir_dot = (v.x * dir.x) + (v.z * dir.z) + if self_dir_dot < 0 then + player:add_velocity({x = v.x * -1, y = 0, z = v.z * -1}) + end + -- add player velocity to knockback + local h_name = hitter:get_player_name() + local hv = hitter:get_velocity() + local dir_dot = (hv.x * dir.x) + (hv.z * dir.z) + local hitter_mag = math.sqrt((hv.x * hv.x) + (hv.z * hv.z)) + if dir_dot > 0 and mcl_sprint.is_sprinting(h_name) then + knockback = knockback + hitter_mag * 0.6875 + elseif dir_dot > 0 then + knockback = knockback + hitter_mag * 0.515625 + end + -- reduce floatiness + minetest.after(0.25, function() + player:add_velocity({x = 0, y = (v.y + added_v) * -0.375, z = 0}) + end) + -- reduce knockback when moving towards hitter while attacking + local self_dir_dot = (v.x * dir.x) + (v.z * dir.z) + local control = player:get_player_control() + if self_dir_dot < -4.3 and control.up and control.LMB then + knockback = knockback * 0.6 + end + -- remove knockback if invulnerable + if invul > 0 then + knockback = 0 + end + elseif hitter and hitter:is_player() and distance > 3 then + knockback = 0 + elseif luaentity and luaentity._knockback then + local kb = knockback + luaentity._knockback / 4 + local punch_dir = dir + punch_dir.y = 0 + punch_dir = vector.normalize(punch_dir) * kb + punch_dir.y = 4 + player:add_velocity(punch_dir) + knockback = 0 + end + return knockback +end + +-- implemented in mcl_mobs and mobs_mc +mcl_enchanting.enchantments.looting = { + name = S("Looting"), + max_level = 3, + primary = {sword = true}, + secondary = {}, + disallow = {}, + incompatible = {}, + weight = 2, + description = S("Increases mob loot."), + curse = false, + on_enchant = function() end, + requires_tool = false, + treasure = false, + power_range_table = {{15, 61}, {24, 71}, {33, 81}}, + inv_combat_tab = true, + inv_tool_tab = false, +} + +-- requires missing MineClone2 feature +--[[mcl_enchanting.enchantments.loyalty = { + name = S("Loyalty"), + max_level = 3, + primary = {trident = true}, + secondary = {}, + disallow = {}, + incompatible = {riptide = true}, + weight = 5, + description = S("Trident returns after being thrown. Higher levels reduce return time."), + curse = false, + on_enchant = function() end, + requires_tool = false, + treasure = false, + power_range_table = {{12, 50}, {19, 50}, {26, 50}}, + inv_combat_tab = true, + inv_tool_tab = false, +}]]-- + +-- implemented in mcl_fishing +mcl_enchanting.enchantments.luck_of_the_sea = { + name = S("Luck of the Sea"), + max_level = 3, + primary = {fishing_rod = true}, + secondary = {}, + disallow = {}, + incompatible = {}, + weight = 2, + description = S("Increases rate of good loot (enchanting books, etc.)"), + curse = false, + on_enchant = function() end, + requires_tool = false, + treasure = false, + power_range_table = {{15, 61}, {24, 71}, {33, 81}}, + inv_combat_tab = false, + inv_tool_tab = true, +} + +-- implemented in mcl_fishing +mcl_enchanting.enchantments.lure = { + name = S("Lure"), + max_level = 3, + primary = {fishing_rod = true}, + secondary = {}, + disallow = {}, + incompatible = {}, + weight = 2, + description = S("Decreases time until rod catches something."), + curse = false, + on_enchant = function() end, + requires_tool = false, + treasure = false, + power_range_table = {{15, 61}, {24, 71}, {33, 81}}, + inv_combat_tab = false, + inv_tool_tab = true, +} + +-- implemented in mcl_experience +mcl_enchanting.enchantments.mending = { + name = S("Mending"), + max_level = 1, + primary = {}, + secondary = {armor_head = true, armor_torso = true, armor_legs = true, armor_feet = true, tool = true, weapon = true}, + disallow = {}, + incompatible = {infinity = true}, + weight = 2, + description = S("Repair the item while gaining XP orbs."), + curse = false, + on_enchant = function() end, + requires_tool = true, + treasure = true, + power_range_table = {{25, 75}}, + inv_combat_tab = true, + inv_tool_tab = true, +} + +mcl_experience.register_on_add_xp(function(player, xp) + local inv = player:get_inventory() + + local candidates = { + {list = "main", index = player:get_wield_index()}, + {list = "armor", index = 2}, + {list = "armor", index = 3}, + {list = "armor", index = 4}, + {list = "armor", index = 5}, + {list = "offhand", index = 1}, + } + + local final_candidates = {} + for _, can in ipairs(candidates) do + local stack = inv:get_stack(can.list, can.index) + local wear = stack:get_wear() + if mcl_enchanting.has_enchantment(stack, "mending") and wear > 0 then + can.stack = stack + can.wear = wear + table.insert(final_candidates, can) + end + end + + if #final_candidates > 0 then + local can = final_candidates[math.random(#final_candidates)] + local stack, list, index, wear = can.stack, can.list, can.index, can.wear + local uses = mcl_util.calculate_durability(stack) + local multiplier = 2 * 65535 / uses + local repair = xp * multiplier + local new_wear = wear - repair + + if new_wear < 0 then + xp = math.floor(-new_wear / multiplier + 0.5) + new_wear = 0 + else + xp = 0 + end + + stack:set_wear(math.floor(new_wear)) + tt.reload_itemstack_description(stack) -- update tooltip + inv:set_stack(list, index, stack) + end + + return xp +end, 0) + +mcl_enchanting.enchantments.multishot = { + name = S("Multishot"), + max_level = 1, + primary = {crossbow = true}, + secondary = {}, + disallow = {}, + incompatible = {piercing = true}, + weight = 2, + description = S("Shoot 3 arrows at the cost of one."), + curse = false, + on_enchant = function() end, + requires_tool = false, + treasure = false, + power_range_table = {{20, 50}}, + inv_combat_tab = true, + inv_tool_tab = false, +} + +-- requires missing MineClone2 feature +mcl_enchanting.enchantments.piercing = { + name = S("Piercing"), + max_level = 4, + primary = {crossbow = true}, + secondary = {}, + disallow = {}, + incompatible = {multishot = true}, + weight = 10, + description = S("Arrows passes through multiple objects."), + curse = false, + on_enchant = function() end, + requires_tool = false, + treasure = false, + power_range_table = {{1, 50}, {11, 50}, {21, 50}, {31, 50}}, + inv_combat_tab = true, + inv_tool_tab = false, +} + +-- implemented in mcl_bows +mcl_enchanting.enchantments.power = { + name = S("Power"), + max_level = 5, + primary = {bow = true}, + secondary = {}, + disallow = {}, + incompatible = {}, + weight = 10, + description = S("Increases arrow damage."), + curse = false, + on_enchant = function() end, + requires_tool = false, + treasure = false, + power_range_table = {{1, 16}, {11, 26}, {21, 36}, {31, 46}, {41, 56}}, + inv_combat_tab = true, + inv_tool_tab = false, +} + +-- implemented via minetest.calculate_knockback (together with the Knockback enchantment) and mcl_bows +mcl_enchanting.enchantments.punch = { + name = S("Punch"), + max_level = 2, + primary = {}, + secondary = {bow = true}, + disallow = {}, + incompatible = {}, + weight = 2, + description = S("Increases arrow knockback."), + curse = false, + on_enchant = function() end, + requires_tool = false, + treasure = false, + power_range_table = {{12, 37}, {32, 57}}, + inv_combat_tab = true, + inv_tool_tab = false, +} + +-- requires missing MineClone2 feature +mcl_enchanting.enchantments.quick_charge = { + name = S("Quick Charge"), + max_level = 3, + primary = {crossbow = true}, + secondary = {}, + disallow = {}, + incompatible = {}, + weight = 5, + description = S("Decreases crossbow charging time."), + curse = false, + on_enchant = function() end, + requires_tool = false, + treasure = false, + power_range_table = {{12, 50}, {32, 50}, {52, 50}}, + inv_combat_tab = true, + inv_tool_tab = false, +} + +-- unimplemented +--[[mcl_enchanting.enchantments.respiration = { + name = S("Respiration"), + max_level = 3, + primary = {armor_head = true}, + secondary = {}, + disallow = {non_combat_armor = true}, + incompatible = {}, + weight = 2, + description = S("Extends underwater breathing time."), + curse = false, + on_enchant = function() end, + requires_tool = false, + treasure = false, + power_range_table = {{10, 40}, {20, 50}, {30, 60}}, + inv_combat_tab = true, + inv_tool_tab = false, +}]]-- + +-- requires missing MineClone2 feature +--[[mcl_enchanting.enchantments.riptide = { + name = S("Riptide"), + max_level = 3, + primary = {trident = true}, + secondary = {}, + disallow = {}, + incompatible = {channeling = true, loyalty = true}, + weight = 2, + description = S("Trident launches player with itself when thrown. Works only in water or rain."), + curse = false, + on_enchant = function() end, + requires_tool = false, + treasure = false, + power_range_table = {{17, 50}, {24, 50}, {31, 50}}, + inv_combat_tab = true, + inv_tool_tab = false, +}]]-- + +-- implemented via on_enchant +mcl_enchanting.enchantments.sharpness = { + name = S("Sharpness"), + max_level = 5, + primary = {sword = true}, + secondary = {axe = true}, + disallow = {}, + incompatible = {bane_of_arthropods = true, smite = true}, + weight = 5, + description = S("Increases damage."), + curse = false, + on_enchant = increase_damage("fleshy", 0.5), + requires_tool = false, + treasure = false, + power_range_table = {{1, 21}, {12, 32}, {23, 43}, {34, 54}, {45, 65}}, + inv_combat_tab = true, + inv_tool_tab = false, +} + +-- implemented in mcl_item_entity +mcl_enchanting.enchantments.silk_touch = { + name = S("Silk Touch"), + max_level = 1, + primary = {pickaxe = true, shovel = true, axe = true, hoe = true}, + secondary = {shears = true}, + disallow = {}, + incompatible = {fortune = true}, + weight = 1, + description = S("Mined blocks drop themselves."), + curse = false, + on_enchant = function() end, + requires_tool = false, + treasure = false, + power_range_table = {{15, 61}}, + inv_combat_tab = false, + inv_tool_tab = true, +} + +-- implemented via on_enchant and additions in mobs_mc +mcl_enchanting.enchantments.smite = { + name = S("Smite"), + max_level = 5, + primary = {sword = true}, + secondary = {axe = true}, + disallow = {}, + incompatible = {bane_of_arthropods = true, sharpness = true}, + weight = 5, + description = S("Increases damage to undead mobs."), + curse = false, + on_enchant = increase_damage("undead", 2.5), + requires_tool = false, + treasure = false, + power_range_table = {{5, 25}, {13, 33}, {21, 41}, {29, 49}, {37, 57}}, + inv_combat_tab = true, + inv_tool_tab = false, +} + +-- implemented in mcl_playerplus +mcl_enchanting.enchantments.soul_speed = { + name = S("Soul Speed"), + max_level = 3, + primary = {}, + secondary = {armor_feet = true}, + disallow = {non_combat_armor = true}, + incompatible = {frost_walker = true}, + weight = 2, + description = S("Increases walking speed on soul sand."), + curse = false, + on_enchant = function() end, + requires_tool = false, + treasure = true, + power_range_table = {{10, 25}, {20, 35}, {30, 45}}, + inv_combat_tab = true, + inv_tool_tab = false, +} + +-- requires missing MineClone2 feature +--[[mcl_enchanting.enchantments.sweeping_edge = { + name = S("Sweeping Edge"), + max_level = 3, + primary = {sword = true}, + secondary = {}, + disallow = {}, + incompatible = {}, + weight = 2, + description = S("Increases sweeping attack damage."), + curse = false, + on_enchant = function() end, + requires_tool = false, + treasure = false, + power_range_table = {{5, 20}, {14, 29}, {23, 38}}, + inv_combat_tab = true, + inv_tool_tab = false, +}]]-- + +-- for tools & weapons implemented via on_enchant; for bows implemented in mcl_bows; for armor implemented in mcl_armor and mcl_tt; for fishing rods implemented in mcl_fishing +mcl_enchanting.enchantments.unbreaking = { + name = S("Unbreaking"), + max_level = 3, + primary = {armor_head = true, armor_torso = true, armor_legs = true, armor_feet = true, pickaxe = true, shovel = true, axe = true, hoe = true, sword = true, fishing_rod = true, bow = true}, + secondary = {tool = true}, + disallow = {non_combat_armor = true}, + incompatible = {}, + weight = 5, + description = S("Increases item durability."), + curse = false, + on_enchant = function(itemstack, level) + local name = itemstack:get_name() + if not minetest.registered_tools[name].tool_capabilities then + return + end + + local tool_capabilities = itemstack:get_tool_capabilities() + tool_capabilities.punch_attack_uses = tool_capabilities.punch_attack_uses * (1 + level) + itemstack:get_meta():set_tool_capabilities(tool_capabilities) + + -- Updating digging durability is handled by update_groupcaps + -- which is called from load_enchantments. + end, + requires_tool = true, + treasure = false, + power_range_table = {{5, 61}, {13, 71}, {21, 81}}, + inv_combat_tab = true, + inv_tool_tab = true, +} diff --git a/engine.lua b/engine.lua new file mode 100644 index 0000000..dd01d59 --- /dev/null +++ b/engine.lua @@ -0,0 +1,732 @@ +local S = minetest.get_translator(minetest.get_current_modname()) +local F = minetest.formspec_escape + +function mcl_enchanting.is_book(itemname) + return itemname == "mcl_books:book" or itemname == "mcl_enchanting:book_enchanted" or + itemname == "mcl_books:book_enchanted" +end + +function mcl_enchanting.get_enchantments(itemstack) + if not itemstack then + return {} + end + return minetest.deserialize(itemstack:get_meta():get_string("mcl_enchanting:enchantments")) or {} +end + +function mcl_enchanting.unload_enchantments(itemstack) + local itemdef = itemstack:get_definition() + local meta = itemstack:get_meta() + if itemdef.tool_capabilities then + meta:set_tool_capabilities(nil) + meta:set_string("groupcaps_hash", "") + end + if meta:get_string("name") == "" then + meta:set_string("description", "") + meta:set_string("groupcaps_hash", "") + end +end + +function mcl_enchanting.load_enchantments(itemstack, enchantments) + if not mcl_enchanting.is_book(itemstack:get_name()) then + mcl_enchanting.unload_enchantments(itemstack) + for enchantment, level in pairs(enchantments or mcl_enchanting.get_enchantments(itemstack)) do + local enchantment_def = mcl_enchanting.enchantments[enchantment] + if enchantment_def.on_enchant then + enchantment_def.on_enchant(itemstack, level) + end + end + mcl_enchanting.update_groupcaps(itemstack) + end + tt.reload_itemstack_description(itemstack) +end + +function mcl_enchanting.set_enchantments(itemstack, enchantments) + itemstack:get_meta():set_string("mcl_enchanting:enchantments", minetest.serialize(enchantments)) + mcl_enchanting.load_enchantments(itemstack) +end + +function mcl_enchanting.get_enchantment(itemstack, enchantment) + return mcl_enchanting.get_enchantments(itemstack)[enchantment] or 0 +end + +function mcl_enchanting.has_enchantment(itemstack, enchantment) + return mcl_enchanting.get_enchantment(itemstack, enchantment) > 0 +end + +function mcl_enchanting.get_enchantment_description(enchantment, level) + local enchantment_def = mcl_enchanting.enchantments[enchantment] + return enchantment_def.name .. + (enchantment_def.max_level == 1 and "" or " " .. mcl_util.to_roman(level)) +end + +function mcl_enchanting.get_colorized_enchantment_description(enchantment, level) + return minetest.colorize(mcl_enchanting.enchantments[enchantment].curse and mcl_colors.RED or mcl_colors.GRAY, + mcl_enchanting.get_enchantment_description(enchantment, level)) +end + +function mcl_enchanting.get_enchanted_itemstring(itemname) + local def = minetest.registered_items[itemname] + return def and def._mcl_enchanting_enchanted_tool +end + +function mcl_enchanting.set_enchanted_itemstring(itemstack) + itemstack:set_name(mcl_enchanting.get_enchanted_itemstring(itemstack:get_name())) +end + +function mcl_enchanting.is_enchanted(itemname) + return minetest.get_item_group(itemname, "enchanted") > 0 +end + +function mcl_enchanting.not_enchantable_on_enchanting_table(itemname) + return mcl_enchanting.get_enchantability(itemname) == -1 +end + +function mcl_enchanting.is_enchantable(itemname) + return mcl_enchanting.get_enchantability(itemname) > 0 or + mcl_enchanting.not_enchantable_on_enchanting_table(itemname) +end + +function mcl_enchanting.can_enchant_freshly(itemname) + return mcl_enchanting.is_enchantable(itemname) and not mcl_enchanting.is_enchanted(itemname) +end + +function mcl_enchanting.get_enchantability(itemname) + return minetest.get_item_group(itemname, "enchantability") +end + +function mcl_enchanting.item_supports_enchantment(itemname, enchantment, early) + if not mcl_enchanting.is_enchantable(itemname) then + return false + end + local enchantment_def = mcl_enchanting.enchantments[enchantment] + if mcl_enchanting.is_book(itemname) then + return true, (not enchantment_def.treasure) + end + local itemdef = minetest.registered_items[itemname] + if itemdef.type ~= "tool" and enchantment_def.requires_tool then + return false + end + for disallow in pairs(enchantment_def.disallow) do + if minetest.get_item_group(itemname, disallow) > 0 then + return false + end + end + for group in pairs(enchantment_def.primary) do + if minetest.get_item_group(itemname, group) > 0 then + return true, true + end + end + for group in pairs(enchantment_def.secondary) do + if minetest.get_item_group(itemname, group) > 0 then + return true, false + end + end + return false +end + +function mcl_enchanting.can_enchant(itemstack, enchantment, level) + local enchantment_def = mcl_enchanting.enchantments[enchantment] + if not enchantment_def then + return false, "enchantment invalid" + end + local itemname = itemstack:get_name() + if itemname == "" then + return false, "item missing" + end + local supported, primary = mcl_enchanting.item_supports_enchantment(itemname, enchantment) + if not supported then + return false, "item not supported" + end + if not level then + return false, "level invalid" + end + if level > enchantment_def.max_level then + return false, "level too high", enchantment_def.max_level + elseif level < 1 then + return false, "level too small", 1 + end + local item_enchantments = mcl_enchanting.get_enchantments(itemstack) + local enchantment_level = item_enchantments[enchantment] + if enchantment_level then + return false, "incompatible", mcl_enchanting.get_enchantment_description(enchantment, enchantment_level) + end + if not mcl_enchanting.is_book(itemname) then + for incompatible in pairs(enchantment_def.incompatible) do + local incompatible_level = item_enchantments[incompatible] + if incompatible_level then + return false, "incompatible", + mcl_enchanting.get_enchantment_description(incompatible, incompatible_level) + end + end + end + return true, nil, nil, primary +end + +function mcl_enchanting.enchant(itemstack, enchantment, level) + mcl_enchanting.set_enchanted_itemstring(itemstack) + local enchantments = mcl_enchanting.get_enchantments(itemstack) + enchantments[enchantment] = level + mcl_enchanting.set_enchantments(itemstack, enchantments) + return itemstack +end + +function mcl_enchanting.combine(itemstack, combine_with) + local itemname = itemstack:get_name() + local combine_name = combine_with:get_name() + local enchanted_itemname = mcl_enchanting.get_enchanted_itemstring(itemname) + if not enchanted_itemname or + enchanted_itemname ~= mcl_enchanting.get_enchanted_itemstring(combine_name) and + not mcl_enchanting.is_book(combine_name) then + return false + end + local enchantments = mcl_enchanting.get_enchantments(itemstack) + local any_new_enchantment = false + for enchantment, combine_level in pairs(mcl_enchanting.get_enchantments(combine_with)) do + local enchantment_def = mcl_enchanting.enchantments[enchantment] + local enchantment_level = enchantments[enchantment] + if enchantment_level then -- The enchantment already exist in the provided item + if enchantment_level == combine_level then + enchantment_level = math.min(enchantment_level + 1, enchantment_def.max_level) + else + enchantment_level = math.max(enchantment_level, combine_level) + end + any_new_enchantment = any_new_enchantment or ( enchantment_level ~= enchantments[enchantment] ) + elseif mcl_enchanting.item_supports_enchantment(itemname, enchantment) then -- this is a new enchantement to try to add + local supported = true + for incompatible in pairs(enchantment_def.incompatible) do + if enchantments[incompatible] then + supported = false + break + end + end + if supported then + enchantment_level = combine_level + any_new_enchantment = true + end + end + if enchantment_level and enchantment_level > 0 then + enchantments[enchantment] = enchantment_level + end + end + if any_new_enchantment then + itemstack:set_name(enchanted_itemname) + mcl_enchanting.set_enchantments(itemstack, enchantments) + end + return any_new_enchantment +end + +function mcl_enchanting.enchantments_snippet(_, _, itemstack) + if not itemstack then + return + end + local enchantments = mcl_enchanting.get_enchantments(itemstack) + local text = "" + for enchantment, level in pairs(enchantments) do + text = text .. mcl_enchanting.get_colorized_enchantment_description(enchantment, level) .. "\n" + end + if text ~= "" then + if not itemstack:get_definition()._tt_original_description then + text = text:sub(1, text:len() - 1) + end + return text, false + end +end + +-- Returns the after_use callback function to use when registering an enchanted +-- item. The after_use callback is used to update the tool_capabilities of +-- efficiency enchanted tools with outdated digging times. +-- +-- It does this by calling apply_efficiency to reapply the efficiency +-- enchantment. That function is written to use hash values to only update the +-- tool if neccessary. +-- +-- This is neccessary for digging times of tools to be in sync when MineClone2 +-- or mods add new hardness values. +local function get_after_use_callback(itemdef) + if itemdef.after_use then + -- If the tool already has an after_use, make sure to call that + -- one too. + return function(itemstack, user, node, digparams) + itemdef.after_use(itemstack, user, node, digparams) + mcl_enchanting.update_groupcaps(itemstack) + end + end + + -- If the tool does not have after_use, add wear to the tool as if no + -- after_use was registered. + return function(itemstack, user, node, digparams) + if not minetest.is_creative_enabled(user:get_player_name()) then + itemstack:add_wear(digparams.wear) + end + + --local enchantments = mcl_enchanting.get_enchantments(itemstack) + mcl_enchanting.update_groupcaps(itemstack) + end +end + +function mcl_enchanting.initialize() + local register_tool_list = {} + local register_item_list = {} + for itemname, itemdef in pairs(minetest.registered_items) do + if mcl_enchanting.can_enchant_freshly(itemname) and not mcl_enchanting.is_book(itemname) then + local new_name = itemname .. "_enchanted" + minetest.override_item(itemname, { _mcl_enchanting_enchanted_tool = new_name }) + local new_def = table.copy(itemdef) + new_def.inventory_image = itemdef.inventory_image .. mcl_enchanting.overlay + if new_def.wield_image then + new_def.wield_image = new_def.wield_image .. mcl_enchanting.overlay + end + new_def.groups.not_in_creative_inventory = 1 + new_def.groups.not_in_craft_guide = 1 + new_def.groups.enchanted = 1 + + if new_def._mcl_armor_texture then + if type(new_def._mcl_armor_texture) == "string" then + new_def._mcl_armor_texture = new_def._mcl_armor_texture .. mcl_enchanting.overlay + end + end + + new_def._mcl_enchanting_enchanted_tool = new_name + new_def.after_use = get_after_use_callback(itemdef) + local register_list = register_item_list + if itemdef.type == "tool" then + register_list = register_tool_list + end + register_list[":" .. new_name] = new_def + end + end + for new_name, new_def in pairs(register_item_list) do + minetest.register_craftitem(new_name, new_def) + end + for new_name, new_def in pairs(register_tool_list) do + minetest.register_tool(new_name, new_def) + end +end + +function mcl_enchanting.random(pr, ...) + local r = pr and pr:next(...) or math.random(...) + + if pr and not ({ ... })[1] then + r = r / 32767 + end + + return r +end + +function mcl_enchanting.get_random_enchantment(itemstack, treasure, weighted, exclude, pr) + local possible = {} + + for enchantment, enchantment_def in pairs(mcl_enchanting.enchantments) do + local can_enchant, _, _, primary = mcl_enchanting.can_enchant(itemstack, enchantment, 1) + + if can_enchant and (primary or treasure) and (not exclude or table.indexof(exclude, enchantment) == -1) then + local weight = weighted and enchantment_def.weight or 1 + + for i = 1, weight do + table.insert(possible, enchantment) + end + end + end + + return #possible > 0 and possible[mcl_enchanting.random(pr, 1, #possible)] +end + +function mcl_enchanting.generate_random_enchantments(itemstack, enchantment_level, treasure, no_reduced_bonus_chance, + ignore_already_enchanted, pr) + local itemname = itemstack:get_name() + + if (not mcl_enchanting.can_enchant_freshly(itemname) and not ignore_already_enchanted) or + mcl_enchanting.not_enchantable_on_enchanting_table(itemname) then + return + end + + itemstack = ItemStack(itemstack) + + local enchantability = minetest.get_item_group(itemname, "enchantability") + enchantability = 1 + mcl_enchanting.random(pr, 0, math.floor(enchantability / 4)) + + mcl_enchanting.random(pr, 0, math.floor(enchantability / 4)) + + enchantment_level = enchantment_level + enchantability + enchantment_level = enchantment_level + + enchantment_level * (mcl_enchanting.random(pr) + mcl_enchanting.random(pr) - 1) * 0.15 + enchantment_level = math.max(math.floor(enchantment_level + 0.5), 1) + + local enchantments = {} + local description + + enchantment_level = enchantment_level * 2 + + repeat + enchantment_level = math.floor(enchantment_level / 2) + + if enchantment_level == 0 then + break + end + + local selected_enchantment = mcl_enchanting.get_random_enchantment(itemstack, treasure, true, nil, pr) + + if not selected_enchantment then + break + end + + local enchantment_def = mcl_enchanting.enchantments[selected_enchantment] + local power_range_table = enchantment_def.power_range_table + + local enchantment_power + + for i = enchantment_def.max_level, 1, -1 do + local power_range = power_range_table[i] + if enchantment_level >= power_range[1] and enchantment_level <= power_range[2] then + enchantment_power = i + break + end + end + + if not description then + if not enchantment_power then + return + end + + description = mcl_enchanting.get_enchantment_description(selected_enchantment, enchantment_power) + end + + if enchantment_power then + enchantments[selected_enchantment] = enchantment_power + mcl_enchanting.enchant(itemstack, selected_enchantment, enchantment_power) + end + until not no_reduced_bonus_chance and mcl_enchanting.random(pr) >= (enchantment_level + 1) / 50 + + return enchantments, description +end + +function mcl_enchanting.generate_random_enchantments_reliable(itemstack, enchantment_level, treasure, no_reduced_bonus_chance, ignore_already_enchanted, pr) + local enchantments + + repeat + enchantments = mcl_enchanting.generate_random_enchantments(itemstack, enchantment_level, treasure, + no_reduced_bonus_chance, ignore_already_enchanted, pr) + until enchantments + + return enchantments +end + +function mcl_enchanting.enchant_randomly(itemstack, enchantment_level, treasure, no_reduced_bonus_chance, + ignore_already_enchanted, pr) + local enchantments = mcl_enchanting.generate_random_enchantments_reliable(itemstack, enchantment_level, treasure, no_reduced_bonus_chance, ignore_already_enchanted, pr) + + mcl_enchanting.set_enchanted_itemstring(itemstack) + mcl_enchanting.set_enchantments(itemstack, enchantments) + + return itemstack +end + +function mcl_enchanting.enchant_uniform_randomly(stack, exclude, pr) + local enchantment = mcl_enchanting.get_random_enchantment(stack, true, false, exclude, pr) + + if enchantment then + mcl_enchanting.enchant(stack, enchantment, + mcl_enchanting.random(pr, 1, mcl_enchanting.enchantments[enchantment].max_level)) + end + + return stack +end + +function mcl_enchanting.get_random_glyph_row() + local glyphs = "" + local x = 1.3 + for i = 1, 9 do + glyphs = glyphs .. + "image[" .. x .. ",0.1;0.5,0.5;mcl_enchanting_glyph_" .. math.random(18) .. ".png^[colorize:#675D49:255]" + x = x + 0.6 + end + return glyphs +end + +function mcl_enchanting.generate_random_table_slots(itemstack, num_bookshelves) + local base = math.random(8) + math.floor(num_bookshelves / 2) + math.random(0, num_bookshelves) + local required_levels = { + math.max(base / 3, 1), + (base * 2) / 3 + 1, + math.max(base, num_bookshelves * 2) + } + local slots = {} + for i, enchantment_level in ipairs(required_levels) do + local slot = false + local enchantments, description = mcl_enchanting.generate_random_enchantments(itemstack, enchantment_level) + if enchantments then + slot = { + enchantments = enchantments, + description = description, + glyphs = mcl_enchanting.get_random_glyph_row(), + level_requirement = math.max(i, math.floor(enchantment_level)), + } + end + slots[i] = slot + end + return slots +end + +function mcl_enchanting.get_table_slots(player, itemstack, num_bookshelves) + local itemname = itemstack:get_name() + if (not mcl_enchanting.can_enchant_freshly(itemname)) or mcl_enchanting.not_enchantable_on_enchanting_table(itemname) then + return { false, false, false } + end + local meta = player:get_meta() + local player_slots = minetest.deserialize(meta:get_string("mcl_enchanting:slots")) or {} + local player_bookshelves_slots = player_slots[num_bookshelves] or {} + local player_bookshelves_item_slots = player_bookshelves_slots[itemname] + if player_bookshelves_item_slots then + return player_bookshelves_item_slots + else + player_bookshelves_item_slots = mcl_enchanting.generate_random_table_slots(itemstack, num_bookshelves) + if player_bookshelves_item_slots then + player_bookshelves_slots[itemname] = player_bookshelves_item_slots + player_slots[num_bookshelves] = player_bookshelves_slots + meta:set_string("mcl_enchanting:slots", minetest.serialize(player_slots)) + return player_bookshelves_item_slots + else + return { false, false, false } + end + end +end + +function mcl_enchanting.reset_table_slots(player) + player:get_meta():set_string("mcl_enchanting:slots", "") +end + +function mcl_enchanting.show_enchanting_formspec(player) + local C = minetest.get_color_escape_sequence + local name = player:get_player_name() + local meta = player:get_meta() + local inv = player:get_inventory() + local num_bookshelves = meta:get_int("mcl_enchanting:num_bookshelves") + local table_name = meta:get_string("mcl_enchanting:table_name") + + local formspec = table.concat({ + "formspec_version[4]", + "size[11.75,10.425]", + + "label[0.375,0.375;" .. F(C(mcl_formspec.label_color) .. table_name) .. "]", + mcl_formspec.get_itemslot_bg_v4(1, 3.25, 1, 1), + "list[current_player;enchanting_item;1,3.25;1,1]", + mcl_formspec.get_itemslot_bg_v4(2.25, 3.25, 1, 1), + "image[2.25,3.25;1,1;mcl_enchanting_lapis_background.png]", + "list[current_player;enchanting_lapis;2.25,3.25;1,1]", + "image[4.125,0.56;7.25,4.1;mcl_enchanting_button_background.png]", + "label[0.375,4.7;" .. F(C(mcl_formspec.label_color) .. S("Inventory")) .. "]", + mcl_formspec.get_itemslot_bg_v4(0.375, 5.1, 9, 3), + "list[current_player;main;0.375,5.1;9,3;9]", + + mcl_formspec.get_itemslot_bg_v4(0.375, 9.05, 9, 1), + "list[current_player;main;0.375,9.05;9,1;]", + + "listring[current_player;enchanting_item]", + "listring[current_player;main]", + "listring[current_player;enchanting]", + "listring[current_player;main]", + "listring[current_player;enchanting_lapis]", + "listring[current_player;main]", + }) + + local itemstack = inv:get_stack("enchanting_item", 1) + local player_levels = mcl_experience.get_level(player) + local y = 0.65 + local any_enchantment = false + local table_slots = mcl_enchanting.get_table_slots(player, itemstack, num_bookshelves) + for i, slot in ipairs(table_slots) do + any_enchantment = any_enchantment or slot + local enough_lapis = inv:contains_item("enchanting_lapis", ItemStack({ name = "mcl_core:lapis", count = i })) + local enough_levels = slot and slot.level_requirement <= player_levels + local can_enchant = (slot and enough_lapis and enough_levels) + local ending = (can_enchant and "" or "_off") + local hover_ending = (can_enchant and "_hovered" or "_off") + formspec = formspec + .. "container[4.125," .. y .. "]" + .. + ( + slot and + "tooltip[button_" .. + i .. + ";" .. + C("#818181") .. + ((slot.description and F(slot.description)) or "") .. + " " .. + C("#FFFFFF") .. + " . . . ?\n\n" .. + ( + enough_levels and + C(enough_lapis and "#818181" or "#FC5454") .. + F(S("@1 Lapis Lazuli", i)) .. "\n" .. C("#818181") .. F(S("@1 Enchantment Levels", i)) or + C("#FC5454") .. F(S("Level requirement: @1", slot.level_requirement))) .. "]" or "") + .. + "style[button_" .. + i .. + ";bgimg=mcl_enchanting_button" .. + ending .. + ".png;bgimg_hovered=mcl_enchanting_button" .. + hover_ending .. ".png;bgimg_pressed=mcl_enchanting_button" .. hover_ending .. ".png]" + .. "button[0,0;7.25,1.3;button_" .. i .. ";]" + .. (slot and "image[0,0;1.3,1.3;mcl_enchanting_number_" .. i .. ending .. ".png]" or "") + .. (slot and "label[6.8,1;" .. C(can_enchant and "#80FF20" or "#407F10") .. slot.level_requirement .. "]" or "") + .. (slot and slot.glyphs or "") + .. "container_end[]" + y = y + 1.3 + end + formspec = formspec + .. + "image[" .. + (any_enchantment and 1.1 or 1.67) .. + ",1.2;" .. + (any_enchantment and 2 or 0.87) .. + ",1.43;mcl_enchanting_book_" .. (any_enchantment and "open" or "closed") .. ".png]" + minetest.show_formspec(name, "mcl_enchanting:table", formspec) +end + +function mcl_enchanting.handle_formspec_fields(player, formname, fields) + if formname == "mcl_enchanting:table" then + local button_pressed + for i = 1, 3 do + if fields["button_" .. i] then + button_pressed = i + end + end + if not button_pressed then return end + local name = player:get_player_name() + local inv = player:get_inventory() + local meta = player:get_meta() + local num_bookshelfes = meta:get_int("mcl_enchanting:num_bookshelves") + local itemstack = inv:get_stack("enchanting_item", 1) + local cost = ItemStack({ name = "mcl_core:lapis", count = button_pressed }) + if not inv:contains_item("enchanting_lapis", cost) then + return + end + local slots = mcl_enchanting.get_table_slots(player, itemstack, num_bookshelfes) + local slot = slots[button_pressed] + if not slot then + return + end + local player_level = mcl_experience.get_level(player) + if player_level < slot.level_requirement then + return + end + mcl_experience.set_level(player, player_level - button_pressed) + inv:remove_item("enchanting_lapis", cost) + mcl_enchanting.set_enchanted_itemstring(itemstack) + mcl_enchanting.set_enchantments(itemstack, slot.enchantments) + inv:set_stack("enchanting_item", 1, itemstack) + minetest.sound_play("mcl_enchanting_enchant", { to_player = name, gain = 5.0 }) + mcl_enchanting.reset_table_slots(player) + mcl_enchanting.show_enchanting_formspec(player) + awards.unlock(player:get_player_name(), "mcl:enchanter") + end +end + +function mcl_enchanting.initialize_player(player) + local inv = player:get_inventory() + inv:set_size("enchanting", 1) + inv:set_size("enchanting_item", 1) + inv:set_size("enchanting_lapis", 1) +end + +function mcl_enchanting.is_enchanting_inventory_action(action, inventory, inventory_info) + if inventory:get_location().type == "player" then + local enchanting_lists = mcl_enchanting.enchanting_lists + if action == "move" then + local is_from = table.indexof(enchanting_lists, inventory_info.from_list) ~= -1 + local is_to = table.indexof(enchanting_lists, inventory_info.to_list) ~= -1 + return is_from or is_to, is_to + elseif (action == "put" or action == "take") and table.indexof(enchanting_lists, inventory_info.listname) ~= -1 then + return true + end + else + return false + end +end + +function mcl_enchanting.allow_inventory_action(player, action, inventory, inventory_info) + local is_enchanting_action, do_limit = mcl_enchanting.is_enchanting_inventory_action(action, inventory, + inventory_info) + if is_enchanting_action and do_limit then + if action == "move" then + local listname = inventory_info.to_list + local stack = inventory:get_stack(inventory_info.from_list, inventory_info.from_index) + if stack:get_name() == "mcl_core:lapis" and listname ~= "enchanting_item" then + local count = stack:get_count() + local old_stack = inventory:get_stack("enchanting_lapis", 1) + if old_stack:get_name() ~= "" then + count = math.min(count, old_stack:get_free_space()) + end + return count + elseif inventory:get_stack("enchanting_item", 1):get_count() == 0 and listname ~= "enchanting_lapis" then + return 1 + else + return 0 + end + else + return 0 + end + end +end + +function mcl_enchanting.on_inventory_action(player, action, inventory, inventory_info) + if mcl_enchanting.is_enchanting_inventory_action(action, inventory, inventory_info) then + if action == "move" and inventory_info.to_list == "enchanting" then + local stack = inventory:get_stack("enchanting", 1) + local result_list + if stack:get_name() == "mcl_core:lapis" then + result_list = "enchanting_lapis" + stack:add_item(inventory:get_stack("enchanting_lapis", 1)) + else + result_list = "enchanting_item" + end + inventory:set_stack(result_list, 1, stack) + inventory:set_stack("enchanting", 1, nil) + end + mcl_enchanting.show_enchanting_formspec(player) + end +end + +function mcl_enchanting.schedule_book_animation(self, anim) + self.scheduled_anim = { timer = self.anim_length, anim = anim } +end + +function mcl_enchanting.set_book_animation(self, anim) + local anim_index = mcl_enchanting.book_animations[anim] + local start, stop = mcl_enchanting.book_animation_steps[anim_index], + mcl_enchanting.book_animation_steps[anim_index + 1] + self.object:set_animation({ x = start, y = stop }, mcl_enchanting.book_animation_speed, 0, + mcl_enchanting.book_animation_loop[anim] or false) + self.scheduled_anim = nil + self.anim_length = (stop - start) / 40 +end + +function mcl_enchanting.check_animation_schedule(self, dtime) + local schedanim = self.scheduled_anim + if schedanim then + schedanim.timer = schedanim.timer - dtime + if schedanim.timer <= 0 then + mcl_enchanting.set_book_animation(self, schedanim.anim) + end + end +end + +function mcl_enchanting.look_at(self, pos2) + local pos1 = self.object:get_pos() + local vec = vector.subtract(pos1, pos2) + local yaw = math.atan(vec.z / vec.x) - math.pi / 2 + yaw = yaw + (pos1.x >= pos2.x and math.pi or 0) + self.object:set_yaw(yaw + math.pi) +end + +function mcl_enchanting.get_bookshelves(pos) + local absolute, relative = {}, {} + for i, rp in ipairs(mcl_enchanting.bookshelf_positions) do + local airp = vector.add(pos, mcl_enchanting.air_positions[i]) + local ap = vector.add(pos, rp) + if minetest.get_node(ap).name == "mcl_books:bookshelf" and minetest.get_node(airp).name == "air" then + table.insert(absolute, ap) + table.insert(relative, rp) + end + end + return absolute, relative +end diff --git a/groupcaps.lua b/groupcaps.lua new file mode 100644 index 0000000..2a83727 --- /dev/null +++ b/groupcaps.lua @@ -0,0 +1,72 @@ +local groupcaps_cache = {} + +-- Compute a hash value. +function compute_hash(value) + return string.sub(minetest.sha1(minetest.serialize(value)), 1, 8) +end + +-- Get the groupcaps and hash for an enchanted tool. If this function is called +-- repeatedly with the same values it will return data from a cache. +-- +-- Parameters: +-- toolname - Name of the tool +-- level - The efficiency level of the tool +-- +-- Returns a table with the following two fields: +-- values - The groupcaps table +-- hash - The hash of the groupcaps table +local function get_efficiency_groupcaps(toolname, level) + local toolcache = groupcaps_cache[toolname] + local level = level + + if not toolcache then + toolcache = {} + groupcaps_cache[toolname] = toolcache + end + + local levelcache = toolcache[level] + if not levelcache then + levelcache = {} + levelcache.values = mcl_autogroup.get_groupcaps(toolname, level) + levelcache.hash = compute_hash(levelcache.values) + toolcache[level] = levelcache + end + + return levelcache +end + +-- Update groupcaps of an enchanted tool. This function will be called +-- repeatedly to make sure the digging times stored in groupcaps stays in sync +-- when the digging times of nodes can change. +-- +-- To make it more efficient it will first check a hash value to determine if +-- the tool needs to be updated. +function mcl_enchanting.update_groupcaps(itemstack) + local name = itemstack:get_name() + if not minetest.registered_tools[name] or not minetest.registered_tools[name].tool_capabilities then + return + end + + local efficiency = mcl_enchanting.get_enchantment(itemstack, "efficiency") + local unbreaking = mcl_enchanting.get_enchantment(itemstack, "unbreaking") + if unbreaking == 0 and efficiency == 0 then + return + end + + local groupcaps = get_efficiency_groupcaps(name, efficiency) + local hash = itemstack:get_meta():get_string("groupcaps_hash") + + if not hash or hash ~= groupcaps.hash then + local tool_capabilities = itemstack:get_tool_capabilities() + tool_capabilities.groupcaps = table.copy(groupcaps.values) + + -- Increase the number of uses depending on the unbreaking level + -- of the tool. + for group, capability in pairs(tool_capabilities.groupcaps) do + capability.uses = capability.uses * (1 + unbreaking) + end + + itemstack:get_meta():set_tool_capabilities(tool_capabilities) + itemstack:get_meta():set_string("groupcaps_hash", groupcaps.hash) + end +end diff --git a/init.lua b/init.lua new file mode 100644 index 0000000..02ed1ee --- /dev/null +++ b/init.lua @@ -0,0 +1,367 @@ +local modname = minetest.get_current_modname() +local modpath = minetest.get_modpath(modname) +local S = minetest.get_translator(modname) + +local math = math +local vector = vector + +mcl_enchanting = { + book_offset = vector.new(0, 0.75, 0), + book_animations = {["close"] = 1, ["opening"] = 2, ["open"] = 3, ["closing"] = 4}, + book_animation_steps = {0, 640, 680, 700, 740}, + book_animation_loop = {["open"] = true, ["close"] = true}, + book_animation_speed = 40, + enchantments = {}, + overlay = "^[colorize:purple:50", + --overlay = "^[invert:rgb^[multiply:#4df44d:50^[invert:rgb", + enchanting_lists = {"enchanting", "enchanting_item", "enchanting_lapis"}, + bookshelf_positions = { + {x = -2, y = 0, z = -2}, {x = -2, y = 1, z = -2}, + {x = -1, y = 0, z = -2}, {x = -1, y = 1, z = -2}, + {x = 0, y = 0, z = -2}, {x = 0, y = 1, z = -2}, + {x = 1, y = 0, z = -2}, {x = 1, y = 1, z = -2}, + {x = 2, y = 0, z = -2}, {x = 2, y = 1, z = -2}, + {x = -2, y = 0, z = 2}, {x = -2, y = 1, z = 2}, + {x = -1, y = 0, z = 2}, {x = -1, y = 1, z = 2}, + {x = 0, y = 0, z = 2}, {x = 0, y = 1, z = 2}, + {x = 1, y = 0, z = 2}, {x = 1, y = 1, z = 2}, + {x = 2, y = 0, z = 2}, {x = 2, y = 1, z = 2}, + -- {x = -2, y = 0, z = -2}, {x = -2, y = 1, z = -2}, + {x = -2, y = 0, z = -1}, {x = -2, y = 1, z = -1}, + {x = -2, y = 0, z = 0}, {x = -2, y = 1, z = 0}, + {x = -2, y = 0, z = 1}, {x = -2, y = 1, z = 1}, + -- {x = -2, y = 0, z = 2}, {x = -2, y = 1, z = 2}, + -- {x = 2, y = 0, z = -2}, {x = 2, y = 1, z = -2}, + {x = 2, y = 0, z = -1}, {x = 2, y = 1, z = -1}, + {x = 2, y = 0, z = 0}, {x = 2, y = 1, z = 0}, + {x = 2, y = 0, z = 1}, {x = 2, y = 1, z = 1}, + -- {x = 2, y = 0, z = 2}, {x = 2, y = 1, z = 2}, + }, + air_positions = { + {x = -1, y = 0, z = -1}, {x = -1, y = 1, z = -1}, + {x = -1, y = 0, z = -1}, {x = -1, y = 1, z = -1}, + {x = 0, y = 0, z = -1}, {x = 0, y = 1, z = -1}, + {x = 1, y = 0, z = -1}, {x = 1, y = 1, z = -1}, + {x = 1, y = 0, z = -1}, {x = 1, y = 1, z = -1}, + {x = -1, y = 0, z = 1}, {x = -1, y = 1, z = 1}, + {x = -1, y = 0, z = 1}, {x = -1, y = 1, z = 1}, + {x = 0, y = 0, z = 1}, {x = 0, y = 1, z = 1}, + {x = 1, y = 0, z = 1}, {x = 1, y = 1, z = 1}, + {x = 1, y = 0, z = 1}, {x = 1, y = 1, z = 1}, + -- {x = -1, y = 0, z = -1}, {x = -1, y = 1, z = -1}, + {x = -1, y = 0, z = -1}, {x = -1, y = 1, z = -1}, + {x = -1, y = 0, z = 0}, {x = -1, y = 1, z = 0}, + {x = -1, y = 0, z = 1}, {x = -1, y = 1, z = 1}, + -- {x = -1, y = 0, z = 1}, {x = -1, y = 1, z = 1}, + -- {x = 1, y = 0, z = -1}, {x = 1, y = 1, z = -1}, + {x = 1, y = 0, z = -1}, {x = 1, y = 1, z = -1}, + {x = 1, y = 0, z = 0}, {x = 1, y = 1, z = 0}, + {x = 1, y = 0, z = 1}, {x = 1, y = 1, z = 1}, + -- {x = 1, y = 0, z = 1}, {x = 1, y = 1, z = 1}, + }, +} + +dofile(modpath .. "/engine.lua") +dofile(modpath .. "/groupcaps.lua") +dofile(modpath .. "/enchantments.lua") + +minetest.register_chatcommand("enchant", { + description = S("Enchant an item"), + params = S(" []"), + privs = {give = true}, + func = function(_, param) + local sparam = param:split(" ") + local target_name = sparam[1] + local enchantment = sparam[2] + local level_str = sparam[3] + local level = tonumber(level_str or "1") + if not target_name or not enchantment then + return false, S("Usage: /enchant []") + end + local target = minetest.get_player_by_name(target_name) + if not target then + return false, S("Player '@1' cannot be found.", target_name) + end + local itemstack = target:get_wielded_item() + local can_enchant, errorstring, extra_info = mcl_enchanting.can_enchant(itemstack, enchantment, level) + if not can_enchant then + if errorstring == "enchantment invalid" then + return false, S("There is no such enchantment '@1'.", enchantment) + elseif errorstring == "item missing" then + return false, S("The target doesn't hold an item.") + elseif errorstring == "item not supported" then + return false, S("The selected enchantment can't be added to the target item.") + elseif errorstring == "level invalid" then + return false, S("'@1' is not a valid number", level_str) + elseif errorstring == "level too high" then + return false, S("The number you have entered (@1) is too big, it must be at most @2.", level_str, extra_info) + elseif errorstring == "level too small" then + return false, S("The number you have entered (@1) is too small, it must be at least @2.", level_str, extra_info) + elseif errorstring == "incompatible" then + return false, S("@1 can't be combined with @2.", mcl_enchanting.get_enchantment_description(enchantment, level), extra_info) + end + else + target:set_wielded_item(mcl_enchanting.enchant(itemstack, enchantment, level)) + return true, S("Enchanting succeded.") + end + end +}) + +minetest.register_chatcommand("forceenchant", { + description = S("Forcefully enchant an item"), + params = S(" []"), + privs = {give = true}, + func = function(_, param) + local sparam = param:split(" ") + local target_name = sparam[1] + local enchantment = sparam[2] + local level_str = sparam[3] + local level = tonumber(level_str or "1") + if not target_name or not enchantment then + return false, S("Usage: /forceenchant []") + end + local target = minetest.get_player_by_name(target_name) + if not target then + return false, S("Player '@1' cannot be found.", target_name) + end + local itemstack = target:get_wielded_item() + local _, errorstring = mcl_enchanting.can_enchant(itemstack, enchantment, level) + if errorstring == "enchantment invalid" then + return false, S("There is no such enchantment '@1'.", enchantment) + elseif errorstring == "item missing" then + return false, S("The target doesn't hold an item.") + elseif errorstring == "item not supported" and not mcl_enchanting.is_enchantable(itemstack:get_name()) then + return false, S("The target item is not enchantable.") + elseif errorstring == "level invalid" then + return false, S("'@1' is not a valid number.", level_str) + else + target:set_wielded_item(mcl_enchanting.enchant(itemstack, enchantment, level)) + return true, S("Enchanting succeded.") + end + end +}) + +minetest.register_craftitem("mcl_enchanting:book_enchanted", { + description = S("Enchanted Book"), + inventory_image = "mcl_enchanting_book_enchanted.png" .. mcl_enchanting.overlay, + groups = {enchanted = 1, not_in_creative_inventory = 1, enchantability = 1}, + _mcl_enchanting_enchanted_tool = "mcl_enchanting:book_enchanted", + stack_max = 1, +}) + +minetest.register_alias("mcl_books:book_enchanted", "mcl_enchanting:book_enchanted") + +local function spawn_book_entity(pos, respawn) + if respawn then + -- Check if we already have a book + local objs = minetest.get_objects_inside_radius(pos, 1) + for o=1, #objs do + local obj = objs[o] + local lua = obj:get_luaentity() + if lua and lua.name == "mcl_enchanting:book" then + if lua._table_pos and vector.equals(pos, lua._table_pos) then + return + end + end + end + end + local obj = minetest.add_entity(vector.add(pos, mcl_enchanting.book_offset), "mcl_enchanting:book") + if obj then + local lua = obj:get_luaentity() + if lua then + lua._table_pos = table.copy(pos) + end + end +end + +minetest.register_entity("mcl_enchanting:book", { + initial_properties = { + visual = "mesh", + mesh = "mcl_enchanting_book.b3d", + visual_size = {x = 12.5, y = 12.5}, + collisionbox = {0, 0, 0}, + pointable = false, + physical = false, + textures = {"mcl_enchanting_book_entity.png", "mcl_enchanting_book_entity.png", "mcl_enchanting_book_entity.png", "mcl_enchanting_book_entity.png", "mcl_enchanting_book_entity.png"}, + static_save = false, + }, + _player_near = false, + _table_pos = nil, + on_activate = function(self, staticdata) + self.object:set_armor_groups({immortal = 1}) + mcl_enchanting.set_book_animation(self, "close") + end, + on_step = function(self, dtime) + local old_player_near = self._player_near + local player_near = false + local player + for _, obj in pairs(minetest.get_objects_inside_radius(vector.subtract(self.object:get_pos(), mcl_enchanting.book_offset), 2.5)) do + if obj:is_player() then + player_near = true + player = obj + end + end + if player_near and not old_player_near then + mcl_enchanting.set_book_animation(self, "opening") + mcl_enchanting.schedule_book_animation(self, "open") + elseif old_player_near and not player_near then + mcl_enchanting.set_book_animation(self, "closing") + mcl_enchanting.schedule_book_animation(self, "close") + end + if player then + mcl_enchanting.look_at(self, player:get_pos()) + end + self._player_near = player_near + mcl_enchanting.check_animation_schedule(self, dtime) + end, +}) + +local rotate +if minetest.get_modpath("screwdriver") then + rotate = screwdriver.rotate_simple +end + +minetest.register_node("mcl_enchanting:table", { + description = S("Enchanting Table"), + _tt_help = S("Spend experience, and lapis to enchant various items."), + _doc_items_longdesc = S("Enchanting Tables will let you enchant armors, tools, weapons, and books with various abilities. But, at the cost of some experience, and lapis lazuli."), + _doc_items_usagehelp = + S("Rightclick the Enchanting Table to open the enchanting menu.").."\n".. + S("Place a tool, armor, weapon or book into the top left slot, and then place 1-3 Lapis Lazuli in the slot to the right.").."\n".."\n".. + S("After placing your items in the slots, the enchanting options will be shown. Hover over the options to read what is available to you.").."\n".. + S("These options are randomized, and dependent on experience level; but the enchantment strength can be increased.").."\n".."\n".. + S("To increase the enchantment strength, place bookshelves around the enchanting table. However, you will need to keep 1 air node between the table, & the bookshelves to empower the enchanting table.").."\n".."\n".. + S("After finally selecting your enchantment; left-click on the selection, and you will see both the lapis lazuli and your experience levels consumed. And, an enchanted item left in its place."), + _doc_items_hidden = false, + drawtype = "nodebox", + tiles = {"mcl_enchanting_table_top.png", "mcl_enchanting_table_bottom.png", "mcl_enchanting_table_side.png", "mcl_enchanting_table_side.png", "mcl_enchanting_table_side.png", "mcl_enchanting_table_side.png"}, + use_texture_alpha = minetest.features.use_texture_alpha_string_modes and "opaque" or false, + node_box = { + type = "fixed", + fixed = {-0.5, -0.5, -0.5, 0.5, 0.25, 0.5}, + }, + sounds = mcl_sounds.node_sound_stone_defaults(), + groups = {pickaxey = 2, deco_block = 1}, + on_rotate = rotate, + on_rightclick = function(pos, node, clicker, itemstack, pointed_thing) + local player_meta = clicker:get_meta() + --local table_meta = minetest.get_meta(pos) + --local num_bookshelves = table_meta:get_int("mcl_enchanting:num_bookshelves") + local table_name = minetest.get_meta(pos):get_string("name") + if table_name == "" then + table_name = S("Enchant") + end + local bookshelves = mcl_enchanting.get_bookshelves(pos) + player_meta:set_int("mcl_enchanting:num_bookshelves", math.min(15, #bookshelves)) + player_meta:set_string("mcl_enchanting:table_name", table_name) + mcl_enchanting.show_enchanting_formspec(clicker) + -- Respawn book entity just in case it got lost + spawn_book_entity(pos, true) + end, + on_construct = function(pos) + spawn_book_entity(pos) + end, + after_dig_node = function(pos, oldnode, oldmetadata, digger) + local dname = (digger and digger:get_player_name()) or "" + if minetest.is_creative_enabled(dname) then + return + end + local itemstack = ItemStack("mcl_enchanting:table") + local meta = minetest.get_meta(pos) + local itemmeta = itemstack:get_meta() + itemmeta:set_string("name", meta:get_string("name")) + itemmeta:set_string("description", meta:get_string("description")) + minetest.add_item(pos, itemstack) + end, + after_place_node = function(pos, placer, itemstack, pointed_thing) + local meta = minetest.get_meta(pos) + local itemmeta = itemstack:get_meta() + meta:set_string("name", itemmeta:get_string("name")) + meta:set_string("description", itemmeta:get_string("description")) + end, + after_destruct = function(pos) + local objs = minetest.get_objects_inside_radius(pos, 1) + for o=1, #objs do + local obj = objs[o] + local lua = obj:get_luaentity() + if lua and lua.name == "mcl_enchanting:book" then + if lua._table_pos and vector.equals(pos, lua._table_pos) then + obj:remove() + end + end + end + end, + drop = "", + _mcl_blast_resistance = 1200, + _mcl_hardness = 5, +}) + +minetest.register_craft({ + output = "mcl_enchanting:table", + recipe = { + {"", "mcl_books:book", ""}, + {"mcl_core:diamond", "mcl_core:obsidian", "mcl_core:diamond"}, + {"mcl_core:obsidian", "mcl_core:obsidian", "mcl_core:obsidian"} + } +}) + +minetest.register_abm({ + label = "Enchanting table bookshelf particles", + interval = 1, + chance = 1, + nodenames = "mcl_enchanting:table", + action = function(pos) + local playernames = {} + for _, obj in pairs(minetest.get_objects_inside_radius(pos, 15)) do + if obj:is_player() then + table.insert(playernames, obj:get_player_name()) + end + end + if #playernames < 1 then + return + end + local absolute, relative = mcl_enchanting.get_bookshelves(pos) + for i, ap in ipairs(absolute) do + if math.random(5) == 1 then + local rp = relative[i] + local t = math.random()+1 --time + local d = {x = rp.x, y=rp.y-0.7, z=rp.z} --distance + local v = {x = -math.random()*d.x, y = math.random(), z = -math.random()*d.z} --velocity + local a = {x = 2*(-v.x*t - d.x)/t/t, y = 2*(-v.y*t - d.y)/t/t, z = 2*(-v.z*t - d.z)/t/t} --acceleration + local s = math.random()+0.9 --size + t = t - 0.1 --slightly decrease time to avoid texture overlappings + local tx = "mcl_enchanting_glyph_" .. math.random(18) .. ".png" + for _, name in pairs(playernames) do + minetest.add_particle({ + pos = ap, + velocity = v, + acceleration = a, + expirationtime = t, + size = s, + texture = tx, + collisiondetection = false, + playername = name + }) + end + end + end + end +}) + +minetest.register_lbm({ + label = "(Re-)spawn book entity above enchanting table", + name = "mcl_enchanting:spawn_book_entity", + nodenames = {"mcl_enchanting:table"}, + run_at_every_load = true, + action = function(pos) + spawn_book_entity(pos, true) + end, +}) + + +minetest.register_on_mods_loaded(mcl_enchanting.initialize) +minetest.register_on_joinplayer(mcl_enchanting.initialize_player) +minetest.register_on_player_receive_fields(mcl_enchanting.handle_formspec_fields) +minetest.register_allow_player_inventory_action(mcl_enchanting.allow_inventory_action) +minetest.register_on_player_inventory_action(mcl_enchanting.on_inventory_action) +tt.register_priority_snippet(mcl_enchanting.enchantments_snippet) diff --git a/mod.conf b/mod.conf new file mode 100644 index 0000000..d163fcd --- /dev/null +++ b/mod.conf @@ -0,0 +1,5 @@ +name = mcl_enchanting +description = Enchanting for MineClone2 +depends = tt, walkover, mcl_sounds, mcl_colors, mcl_experience, mcl_util +optional_depends = screwdriver +author = Fleckenstein