Trader: Close trade formspec + return items on die
[MineClone/MineClone2.git] / mods / ENTITIES / mobs_mc / villager.lua
blob44c0dbd4758f40fb91f10423245864e60aa578b5
1 --MCmobs v0.4
2 --maikerumine
3 --made for MC like Survival game
4 --License for code WTFPL and otherwise stated in readmes
6 --###################
7 --################### VILLAGER
8 --###################
9 -- Summary: Villagers are complex NPCs, their main feature allows players to trade with them.
11 -- TODO: Particles
12 -- TODO: 4s Regeneration I after trade unlock
14 -- intllib
15 local MP = minetest.get_modpath(minetest.get_current_modname())
16 local S, NS = dofile(MP.."/intllib.lua")
18 -- playername-indexed table containing the previously used tradenum
19 local player_tradenum = {}
20 -- playername-indexed table containing the objectref of trader, if trading formspec is open
21 local player_trading_with = {}
23 local DEFAULT_WALK_CHANCE = 33 -- chance to walk in percent, if no player nearby
24 local PLAYER_SCAN_INTERVAL = 5 -- every X seconds, villager looks for players nearby
25 local PLAYER_SCAN_RADIUS = 4 -- scan radius for looking for nearby players
27 --[=======[ TRADING ]=======]
29 -- LIST OF VILLAGER PROFESSIONS AND TRADES
31 -- TECHNICAL RESTRICTIONS (FIXME):
32 -- * You can't use a clock as requested item
33 -- * You can't use a compass as requested item if its stack size > 1
34 -- * You can't use a compass in the second requested slot
35 -- This is a problem in the mcl_compass and mcl_clock mods,
36 -- these items should be implemented as single items, then everything
37 -- will be much easier.
39 local COMPASS = "mcl_compass:compass"
40 if minetest.registered_aliases[COMPASS] then
41 COMPASS = minetest.registered_aliases[COMPASS]
42 end
44 local E1 = { "mcl_core:emerald", 1, 1 } -- one emerald
46 -- Special trades for v6 only
47 local TRADE_V6_RED_SANDSTONE, TRADE_V6_DARK_OAK_SAPLING, TRADE_V6_ACACIA_SAPLING, TRADE_V6_BIRCH_SAPLING
48 if minetest.get_mapgen_setting("mg_name") == "v6" then
49 TRADE_V6_RED_SANDSTONE = { E1, { "mcl_core:redsandstone", 12, 16 } }
50 TRADE_V6_DARK_OAK_SAPLING = { { "mcl_core:emerald", 6, 9 }, { "mcl_core:darksapling", 1, 1 } }
51 TRADE_V6_ACACIA_SAPLING = { { "mcl_core:emerald", 14, 17 }, { "mcl_core:acaciasapling", 1, 1 } }
52 TRADE_V6_BIRCH_SAPLING = { { "mcl_core:emerald", 8, 11 }, { "mcl_core:birchsapling", 1, 1 } }
53 end
55 local professions = {
56 farmer = {
57 name = "Farmer",
58 texture = "mobs_mc_villager_farmer.png",
59 trades = {
61 { { "mcl_farming:wheat_item", 18, 22, }, E1 },
62 { { "mcl_farming:potato_item", 15, 19, }, E1 },
63 { { "mcl_farming:carrot_item", 15, 19, }, E1 },
64 { E1, { "mcl_farming:bread", 2, 4 } },
68 { { "mcl_farming:pumpkin_face", 8, 13 }, E1 },
69 { E1, { "mcl_farming:pumpkin_pie", 2, 3} },
73 { { "mcl_farming:melon", 7, 12 }, E1 },
74 { E1, { "mcl_core:apple", 5, 7 }, },
78 { E1, { "mcl_farming:cookie", 6, 10 } },
79 { E1, { "mcl_cake:cake", 1, 1 } },
80 TRADE_V6_BIRCH_SAPLING,
81 TRADE_V6_DARK_OAK_SAPLING,
82 TRADE_V6_ACACIA_SAPLING,
86 fisherman = {
87 name = "Fisherman",
88 texture = "mobs_mc_villager_farmer.png",
89 trades = {
91 { { "mcl_fishing:fish_raw", 6, 6, "mcl_core:emerald", 1, 1 }, { "mcl_fishing:fish_cooked", 6, 6 } },
92 { { "mcl_mobitems:string", 15, 20 }, E1 },
93 { { "mcl_core:coal_lump", 16, 24 }, E1 },
95 -- TODO: enchanted fishing rod
98 fletcher = {
99 name = "Fletcher",
100 texture = "mobs_mc_villager_farmer.png",
101 trades = {
103 { { "mcl_mobitems:string", 15, 20 }, E1 },
104 { E1, { "mcl_bows:arrow", 8, 12 } },
108 { { "mcl_core:gravel", 10, 10, "mcl_core:emerald", 1, 1 }, { "mcl_core:flint", 6, 10 } },
109 { { "mcl_core:emerald", 2, 3 }, { "mcl_bows:bow", 1, 1 } },
113 shepherd ={
114 name = "Shepherd",
115 texture = "mobs_mc_villager_farmer.png",
116 trades = {
118 { { "mcl_wool:white", 16, 22 }, E1 },
119 { { "mcl_core:emerald", 3, 4 }, { "mcl_tools:shears", 1, 1 } },
123 { { "mcl_core:emerald", 1, 2 }, { "mcl_wool:white", 1, 1 } },
124 { { "mcl_core:emerald", 1, 2 }, { "mcl_wool:grey", 1, 1 } },
125 { { "mcl_core:emerald", 1, 2 }, { "mcl_wool:silver", 1, 1 } },
126 { { "mcl_core:emerald", 1, 2 }, { "mcl_wool:black", 1, 1 } },
127 { { "mcl_core:emerald", 1, 2 }, { "mcl_wool:yellow", 1, 1 } },
128 { { "mcl_core:emerald", 1, 2 }, { "mcl_wool:orange", 1, 1 } },
129 { { "mcl_core:emerald", 1, 2 }, { "mcl_wool:red", 1, 1 } },
130 { { "mcl_core:emerald", 1, 2 }, { "mcl_wool:magenta", 1, 1 } },
131 { { "mcl_core:emerald", 1, 2 }, { "mcl_wool:purple", 1, 1 } },
132 { { "mcl_core:emerald", 1, 2 }, { "mcl_wool:blue", 1, 1 } },
133 { { "mcl_core:emerald", 1, 2 }, { "mcl_wool:cyan", 1, 1 } },
134 { { "mcl_core:emerald", 1, 2 }, { "mcl_wool:lime", 1, 1 } },
135 { { "mcl_core:emerald", 1, 2 }, { "mcl_wool:green", 1, 1 } },
136 { { "mcl_core:emerald", 1, 2 }, { "mcl_wool:pink", 1, 1 } },
137 { { "mcl_core:emerald", 1, 2 }, { "mcl_wool:light_blue", 1, 1 } },
138 { { "mcl_core:emerald", 1, 2 }, { "mcl_wool:brown", 1, 1 } },
142 librarian = {
143 name = "Librarian",
144 texture = "mobs_mc_villager_librarian.png",
145 trades = {
147 { { "mcl_core:paper", 24, 36 }, E1 },
148 -- TODO: enchanted book
149 { { "mcl_books:book", 8, 10 }, E1 },
150 { { "mcl_core:emerald", 10, 12 }, { "mcl_compass:compass", 1 ,1 }},
151 { { "mcl_core:emerald", 3, 4 }, { "mcl_books:bookshelf", 1 ,1 }},
155 { { "mcl_books:written_book", 2, 2 }, E1 },
156 { { "mcl_core:emerald", 10, 12 }, { "mcl_clock:clock", 1, 1 } },
157 { E1, { "mcl_core:glass", 3, 5 } },
161 { E1, { "mcl_core:glass", 3, 5 } },
164 -- TODO: 2 enchanted book tiers
167 { { "mcl_core:emerald", 20, 22 }, { "mcl_mobs:nametag", 1, 1 } },
171 cartographer = {
172 name = "Cartographer",
173 texture = "mobs_mc_villager_librarian.png",
174 trades = {
176 { { "mcl_core:paper", 24, 36 }, E1 },
180 -- subject to special checks
181 { { "mcl_compass:compass", 1, 1 }, E1 },
185 -- TODO: replace with empty map
186 { { "mcl_core:emerald", 7, 11}, { "mcl_maps:filled_map", 1, 1 } },
189 -- TODO: special maps
192 armorer = {
193 name = "Armorer",
194 texture = "mobs_mc_villager_smith.png",
195 trades = {
197 { { "mcl_core:coal_lump", 16, 24 }, E1 },
198 { { "mcl_core:emerald", 6, 8 }, { "3d_armor:helmet_iron", 1, 1 } },
202 { { "mcl_core:iron_ingot", 7, 9 }, E1 },
203 { { "mcl_core:emerald", 10, 14 }, { "3d_armor:chestplate_iron", 1, 1 } },
207 { { "mcl_core:diamond", 3, 4 }, E1 },
208 -- TODO: enchant
209 { { "mcl_core:emerald", 16, 19 }, { "3d_armor:chestplate_diamond", 1, 1 } },
213 { { "mcl_core:emerald", 5, 7 }, { "3d_armor:boots_chain", 1, 1 } },
214 { { "mcl_core:emerald", 9, 11 }, { "3d_armor:leggings_chain", 1, 1 } },
215 { { "mcl_core:emerald", 5, 7 }, { "3d_armor:helmet_chain", 1, 1 } },
216 { { "mcl_core:emerald", 11, 15 }, { "3d_armor:chestplate_chain", 1, 1 } },
220 leatherworker = {
221 name = "Leatherworker",
222 texture = "mobs_mc_villager_butcher.png",
223 trades = {
225 { { "mcl_mobitems:leather", 9, 12 }, E1 },
226 { { "mcl_core:emerald", 2, 4 }, { "3d_armor:leggings_leather", 2, 4 } },
230 -- TODO: enchant
231 { { "mcl_core:emerald", 7, 12 }, { "3d_armor:chestplate_leather", 1, 1 } },
235 { { "mcl_core:emerald", 8, 10 }, { "mcl_mobitems:saddle", 1, 1 } },
239 butcher = {
240 name = "Butcher",
241 texture = "mobs_mc_villager_butcher.png",
242 trades = {
244 { { "mcl_mobitems:beef", 14, 18 }, E1 },
245 { { "mcl_mobitems:chicken", 14, 18 }, E1 },
249 { { "mcl_core:coal_lump", 16, 24 }, E1 },
250 { E1, { "mcl_mobitems:cooked_beef", 5, 7 } },
251 { E1, { "mcl_mobitems:cooked_chicken", 6, 8 } },
255 weapon_smith = {
256 name = "Weapon Smith",
257 texture = "mobs_mc_villager_smith.png",
258 trades = {
260 { { "mcl_core:coal_lump", 16, 24 }, E1 },
261 { { "mcl_core:emerald", 6, 8 }, { "mcl_tools:axe_iron", 1, 1 } },
265 { { "mcl_core:iron_ingot", 7, 9 }, E1 },
266 -- TODO: enchant
267 { { "mcl_core:emerald", 9, 10 }, { "mcl_tools:sword_iron", 1, 1 } },
271 { { "mcl_core:diamond", 3, 4 }, E1 },
272 -- TODO: enchant
273 { { "mcl_core:emerald", 12, 15 }, { "mcl_tools:sword_diamond", 1, 1 } },
274 -- TODO: enchant
275 { { "mcl_core:emerald", 9, 12 }, { "mcl_tools:axe_diamond", 1, 1 } },
279 tool_smith = {
280 name = "Tool Smith",
281 texture = "mobs_mc_villager_smith.png",
282 trades = {
284 { { "mcl_core:coal_lump", 16, 24 }, E1 },
285 -- TODO: enchant
286 { { "mcl_core:emerald", 5, 7 }, { "mcl_tools:shovel_iron", 1, 1 } },
290 { { "mcl_core:iron_ingot", 7, 9 }, E1 },
291 -- TODO: enchant
292 { { "mcl_core:emerald", 9, 11 }, { "mcl_tools:pick_iron", 1, 1 } },
296 { { "mcl_core:diamond", 3, 4 }, E1 },
297 -- TODO: enchant
298 { { "mcl_core:emerald", 12, 15 }, { "mcl_tools:pick_diamond", 1, 1 } },
302 cleric = {
303 name = "Cleric",
304 texture = "mobs_mc_villager_priest.png",
305 trades = {
307 { { "mcl_mobitems:rotten_flesh", 36, 40 }, E1 },
308 { { "mcl_core:gold_ingot", 8, 10 }, E1 },
312 { E1, { "mesecons:redstone", 1, 4 } },
313 { E1, { "mcl_dye:blue", 1, 2 } },
317 TRADE_V6_RED_SANDSTONE,
318 { E1, { "mcl_nether:glowstone", 1, 3 } },
319 { { "mcl_core:emerald", 4, 7 }, { "mcl_throwing:ender_pearl", 1, 1 } },
322 -- TODO: Bottle 'o enchanting
325 nitwit = {
326 name = "Nitwit",
327 texture = "mobs_mc_villager.png",
328 -- No trades for nitwit
329 trades = nil,
333 local profession_names = {}
334 for id, _ in pairs(professions) do
335 table.insert(profession_names, id)
338 local stand_still = function(self)
339 self.walk_chance = 0
340 self.jump = false
343 local update_max_tradenum = function(self)
344 if not self._trades then
345 return
347 local trades = minetest.deserialize(self._trades)
348 for t=1, #trades do
349 local trade = trades[t]
350 if trade.tier > self._max_trade_tier then
351 self._max_tradenum = t - 1
352 return
355 self._max_tradenum = #trades
358 local init_trader_vars = function(self)
359 if not self._profession then
360 -- Select random profession from all professions with matching clothing
361 local texture = self.base_texture[1]
362 local matches = {}
363 for prof_id, prof in pairs(professions) do
364 if texture == prof.texture then
365 table.insert(matches, prof_id)
368 local p = math.random(1, #matches)
369 self._profession = matches[p]
371 if not self._max_trade_tier then
372 self._max_trade_tier = 1
374 if not self._locked_trades then
375 self._locked_trades = 0
377 if not self._trading_players then
378 self._trading_players = {}
382 local init_trades = function(self, inv)
383 local profession = professions[self._profession]
384 local trade_tiers = profession.trades
385 if trade_tiers == nil then
386 -- Empty trades
387 self._trades = false
388 return
391 local max_tier = #trade_tiers
392 local trades = {}
393 for tiernum=1, max_tier do
394 local tier = trade_tiers[tiernum]
395 for tradenum=1, #tier do
396 local trade = tier[tradenum]
397 local wanted1_item = trade[1][1]
398 local wanted1_count = math.random(trade[1][2], trade[1][3])
399 local offered_item = trade[2][1]
400 local offered_count = math.random(trade[2][2], trade[2][3])
402 local wanted = { wanted1_item .. " " ..wanted1_count }
403 if trade[1][4] then
404 local wanted2_item = trade[1][4]
405 local wanted2_count = math.random(trade[1][5], trade[1][6])
406 table.insert(wanted, wanted2_item .. " " ..wanted2_count)
409 table.insert(trades, {
410 wanted = wanted,
411 offered = offered_item .. " " .. offered_count,
412 tier = tiernum, -- tier of this trade
413 traded_once = false, -- true if trade was traded at least once
414 trade_counter = 0, -- how often the this trade was mate after the last time it got unlocked
415 locked = false, -- if this trade is locked. Locked trades can't be used
419 self._trades = minetest.serialize(trades)
422 local set_trade = function(trader, player, inv, concrete_tradenum)
423 local trades = minetest.deserialize(trader._trades)
424 if not trades then
425 init_trades(trader)
426 trades = minetest.deserialize(trader._trades)
427 if not trades then
428 minetest.log("error", "[mobs_mc] Failed to select villager trade!")
429 return
432 local name = player:get_player_name()
434 -- Stop tradenum from advancing into locked tiers or out-of-range areas
435 if concrete_tradenum > trader._max_tradenum then
436 concrete_tradenum = trader._max_tradenum
437 elseif concrete_tradenum < 1 then
438 concrete_tradenum = 1
440 player_tradenum[name] = concrete_tradenum
441 local trade = trades[concrete_tradenum]
442 inv:set_stack("wanted", 1, ItemStack(trade.wanted[1]))
443 inv:set_stack("offered", 1, ItemStack(trade.offered))
444 if trade.wanted[2] then
445 local wanted2 = ItemStack(trade.wanted[2])
446 inv:set_stack("wanted", 2, wanted2)
447 else
448 inv:set_stack("wanted", 2, "")
453 local function show_trade_formspec(playername, trader, tradenum)
454 if not trader._trades then
455 return
457 if not tradenum then
458 tradenum = 1
460 local trades = minetest.deserialize(trader._trades)
461 local trade = trades[tradenum]
462 local profession = professions[trader._profession].name
463 local disabled_img = ""
464 if trade.locked then
465 disabled_img = "image[4.3,2.52;1,1;mobs_mc_trading_formspec_disabled.png]"..
466 "image[4.3,1.1;1,1;mobs_mc_trading_formspec_disabled.png]"
468 local tradeinv_name = "mobs_mc:trade_"..playername
469 local tradeinv = minetest.formspec_escape("detached:"..tradeinv_name)
471 local b_prev, b_next = "", ""
472 if #trades > 1 then
473 if tradenum > 1 then
474 b_prev = "button[1,1;0.5,1;prev_trade;<]"
476 if tradenum < trader._max_tradenum then
477 b_next = "button[7.26,1;0.5,1;next_trade;>]"
481 local formspec =
482 "size[9,8.75]"
483 .."background[-0.19,-0.25;9.41,9.49;mobs_mc_trading_formspec_bg.png]"
484 ..disabled_img
485 ..mcl_vars.inventory_header
486 .."label[4,0;"..minetest.formspec_escape(profession).."]"
487 .."list[current_player;main;0,4.5;9,3;9]"
488 .."list[current_player;main;0,7.74;9,1;]"
489 ..b_prev..b_next
490 .."list["..tradeinv..";wanted;2,1;2,1;]"
491 .."list["..tradeinv..";offered;5.76,1;1,1;]"
492 .."list["..tradeinv..";input;2,2.5;2,1;]"
493 .."list["..tradeinv..";output;5.76,2.55;1,1;]"
494 .."listring["..tradeinv..";output]"
495 .."listring[current_player;main]"
496 .."listring["..tradeinv..";input]"
497 .."listring[current_player;main]"
498 minetest.sound_play("mobs_mc_villager_trade", {to_player = playername})
499 minetest.show_formspec(playername, tradeinv_name, formspec)
502 local update_offer = function(inv, player, sound)
503 local name = player:get_player_name()
504 local trader = player_trading_with[name]
505 local tradenum = player_tradenum[name]
506 if not trader or not tradenum then
507 return false
509 local trades = minetest.deserialize(trader._trades)
510 if not trades then
511 return false
513 local trade = trades[tradenum]
514 if not trade then
515 return false
517 local wanted1, wanted2 = inv:get_stack("wanted", 1), inv:get_stack("wanted", 2)
518 local input1, input2 = inv:get_stack("input", 1), inv:get_stack("input", 2)
520 -- BEGIN OF SPECIAL HANDLING OF COMPASS
521 -- These 2 functions are a complicated check to check if the input contains a
522 -- special item which we cannot check directly against their name, like
523 -- compass.
524 -- TODO: Remove these check functions when compass and clock are implemented
525 -- as single items.
526 local check_special = function(special_item, group, wanted1, wanted2, input1, input2)
527 if minetest.registered_aliases[special_item] then
528 special_item = minetest.registered_aliases[special_item]
530 if wanted1:get_name() == special_item then
531 local check_input = function(input, wanted, group)
532 return minetest.get_item_group(input:get_name(), group) ~= 0 and input:get_count() >= wanted:get_count()
534 if check_input(input1, wanted1, group) then
535 return true
536 elseif check_input(input2, wanted1, group) then
537 return true
538 else
539 return false
542 return false
544 -- Apply above function to all items which we consider special.
545 -- This function succeeds if ANY item check succeeds.
546 local check_specials = function(wanted1, wanted2, input1, input2)
547 return check_special(COMPASS, "compass", wanted1, wanted2, input1, input2)
549 -- END OF SPECIAL HANDLING OF COMPASS
551 if (
552 ((inv:contains_item("input", wanted1) and
553 (wanted2:is_empty() or inv:contains_item("input", wanted2))) or
554 -- BEGIN OF SPECIAL HANDLING OF COMPASS
555 check_specials(wanted1, wanted2, input1, input2)) and
556 -- END OF SPECIAL HANDLING OF COMPASS
557 (trade.locked == false)) then
558 inv:set_stack("output", 1, inv:get_stack("offered", 1))
559 if sound then
560 minetest.sound_play("mobs_mc_villager_accept", {to_player = name})
562 return true
563 else
564 inv:set_stack("output", 1, ItemStack(""))
565 if sound then
566 minetest.sound_play("mobs_mc_villager_deny", {to_player = name})
568 return false
572 -- Returns a single itemstack in the given inventory to the player's main inventory, or drop it when there's no space left
573 local function return_item(itemstack, dropper, pos, inv_p)
574 if dropper:is_player() then
575 -- Return to main inventory
576 if inv_p:room_for_item("main", itemstack) then
577 inv_p:add_item("main", itemstack)
578 else
579 -- Drop item on the ground
580 local v = dropper:get_look_dir()
581 local p = {x=pos.x, y=pos.y+1.2, z=pos.z}
582 p.x = p.x+(math.random(1,3)*0.2)
583 p.z = p.z+(math.random(1,3)*0.2)
584 local obj = minetest.add_item(p, itemstack)
585 if obj then
586 v.x = v.x*4
587 v.y = v.y*4 + 2
588 v.z = v.z*4
589 obj:setvelocity(v)
590 obj:get_luaentity()._insta_collect = false
593 else
594 -- Fallback for unexpected cases
595 minetest.add_item(pos, itemstack)
597 return itemstack
600 local return_fields = function(player)
601 local name = player:get_player_name()
602 local inv_t = minetest.get_inventory({type="detached", name = "mobs_mc:trade_"..name})
603 local inv_p = player:get_inventory()
604 for i=1, inv_t:get_size("input") do
605 local stack = inv_t:get_stack("input", i)
606 return_item(stack, player, player:get_pos(), inv_p)
607 stack:clear()
608 inv_t:set_stack("input", i, stack)
610 inv_t:set_stack("output", 1, "")
613 minetest.register_on_player_receive_fields(function(player, formname, fields)
614 if string.sub(formname, 1, 14) == "mobs_mc:trade_" then
615 local name = player:get_player_name()
616 if fields.quit then
617 -- Get input items back
618 return_fields(player)
619 -- Reset internal "trading with" state
620 local trader = player_trading_with[name]
621 if trader then
622 trader._trading_players[name] = nil
624 player_trading_with[name] = nil
625 elseif fields.next_trade or fields.prev_trade then
626 local trader = player_trading_with[name]
627 if not trader or not trader.object:get_luaentity() then
628 return
630 local trades = trader._trades
631 if not trades then
632 return
634 local dir = 1
635 if fields.prev_trade then
636 dir = -1
638 local tradenum = player_tradenum[name] + dir
639 local inv = minetest.get_inventory({type="detached", name="mobs_mc:trade_"..name})
640 set_trade(trader, player, inv, tradenum)
641 update_offer(inv, player, false)
642 show_trade_formspec(name, trader, player_tradenum[name])
645 end)
647 minetest.register_on_leaveplayer(function(player)
648 return_fields(player)
649 player_tradenum[player:get_player_name()] = nil
650 local trader = player_trading_with[name]
651 if trader then
652 trader._trading_players[name] = nil
654 player_trading_with[player:get_player_name()] = nil
656 end)
658 -- Return true if player is trading with villager, and the villager entity exists
659 local trader_exists = function(playername)
660 local trader = player_trading_with[playername]
661 return trader ~= nil and trader.object:get_luaentity() ~= nil
664 local trade_inventory = {
665 allow_take = function(inv, listname, index, stack, player)
666 if listname == "input" then
667 return stack:get_count()
668 elseif listname == "output" then
669 if not trader_exists(player:get_player_name()) then
670 return 0
672 -- Only allow taking full stack
673 local count = stack:get_count()
674 if count == inv:get_stack(listname, index):get_count() then
675 -- Also update output stack again.
676 -- If input has double the wanted items, the
677 -- output will stay because there will be still
678 -- enough items in input after the trade
679 local wanted1 = inv:get_stack("wanted", 1)
680 local wanted2 = inv:get_stack("wanted", 2)
681 local input1 = inv:get_stack("input", 1)
682 local input2 = inv:get_stack("input", 2)
683 wanted1:set_count(wanted1:get_count()*2)
684 wanted2:set_count(wanted2:get_count()*2)
685 -- BEGIN OF SPECIAL HANDLING FOR COMPASS
686 local special_checks = function(wanted1, input1, input2)
687 if wanted1:get_name() == COMPASS then
688 local compasses = 0
689 if (minetest.get_item_group(input1:get_name(), "compass") ~= 0) then
690 compasses = compasses + input1:get_count()
692 if (minetest.get_item_group(input2:get_name(), "compass") ~= 0) then
693 compasses = compasses + input2:get_count()
695 return compasses >= wanted1:get_count()
697 return false
699 -- END OF SPECIAL HANDLING FOR COMPASS
700 if (inv:contains_item("input", wanted1) and
701 (wanted2:is_empty() or inv:contains_item("input", wanted2)))
702 -- BEGIN OF SPECIAL HANDLING FOR COMPASS
703 or special_checks(wanted1, input1, input2) then
704 -- END OF SPECIAL HANDLING FOR COMPASS
705 return -1
706 else
707 -- If less than double the wanted items,
708 -- remove items from output (final trade,
709 -- input runs empty)
710 return count
712 else
713 return 0
715 else
716 return 0
718 end,
719 allow_move = function(inv, from_list, from_index, to_list, to_index, count, player)
720 if from_list == "input" and to_list == "input" then
721 return count
722 elseif from_list == "output" and to_list == "input" then
723 if not trader_exists(player:get_player_name()) then
724 return 0
726 local move_stack = inv:get_stack(from_list, from_index)
727 if inv:get_stack(to_list, to_index):item_fits(move_stack) then
728 return count
731 return 0
732 end,
733 allow_put = function(inv, listname, index, stack, player)
734 if listname == "input" then
735 if not trader_exists(player:get_player_name()) then
736 return 0
737 else
738 return stack:get_count()
740 else
741 return 0
743 end,
744 on_put = function(inv, listname, index, stack, player)
745 update_offer(inv, player, true)
746 end,
747 on_move = function(inv, from_list, from_index, to_list, to_index, count, player)
748 if from_list == "output" and to_list == "input" then
749 inv:remove_item("input", inv:get_stack("wanted", 1))
750 local wanted2 = inv:get_stack("wanted", 2)
751 if not wanted2:is_empty() then
752 inv:remove_item("input", inv:get_stack("wanted", 2))
754 minetest.sound_play("mobs_mc_villager_accept", {to_player = player:get_player_name()})
756 update_offer(inv, player, true)
757 end,
758 on_take = function(inv, listname, index, stack, player)
759 local accept
760 local name = player:get_player_name()
761 if listname == "output" then
762 local wanted1 = inv:get_stack("wanted", 1)
763 inv:remove_item("input", wanted1)
764 local wanted2 = inv:get_stack("wanted", 2)
765 if not wanted2:is_empty() then
766 inv:remove_item("input", inv:get_stack("wanted", 2))
768 -- BEGIN OF SPECIAL HANDLING FOR COMPASS
769 if wanted1:get_name() == COMPASS then
770 for n=1, 2 do
771 local input = inv:get_stack("input", n)
772 if minetest.get_item_group(input:get_name(), "compass") ~= 0 then
773 input:set_count(input:get_count() - wanted1:get_count())
774 inv:set_stack("input", n, input)
775 break
779 -- END OF SPECIAL HANDLING FOR COMPASS
780 local trader = player_trading_with[name]
781 local tradenum = player_tradenum[name]
782 local trades
783 if trader and trader._trades then
784 trades = minetest.deserialize(trader._trades)
786 if trades then
787 local trade = trades[tradenum]
788 local unlock_stuff = false
789 if not trade.traded_once then
790 -- Unlock all the things if something was traded
791 -- for the first time ever
792 unlock_stuff = true
793 trade.traded_once = true
794 elseif trade.trade_counter == 0 and math.random(1,5) == 1 then
795 -- Otherwise, 20% chance to unlock if used freshly reset trade
796 unlock_stuff = true
798 local update_formspec = false
799 if unlock_stuff then
800 -- First-time trade unlock all trades and unlock next trade tier
801 if trade.tier + 1 > trader._max_trade_tier then
802 trader._max_trade_tier = trader._max_trade_tier + 1
803 update_max_tradenum(trader)
804 update_formspec = true
806 for t=1, #trades do
807 trades[t].locked = false
808 trades[t].trade_counter = 0
810 trader._locked_trades = 0
811 -- Also heal trader for unlocking stuff
812 -- TODO: Replace by Regeneration I
813 trader.health = math.min(trader.hp_max, trader.health + 4)
815 trade.trade_counter = trade.trade_counter + 1
816 -- Semi-randomly lock trade for repeated trade (not if there's only 1 trade)
817 if trader._max_tradenum > 1 then
818 if trade.trade_counter >= 12 then
819 trade.locked = true
820 elseif trade.trade_counter >= 2 then
821 local r = math.random(1, math.random(4, 10))
822 if r == 1 then
823 trade.locked = true
828 if trade.locked then
829 inv:set_stack("output", 1, "")
830 update_formspec = true
831 trader._locked_trades = trader._locked_trades + 1
832 -- Check if we managed to lock ALL available trades. Rare but possible.
833 if trader._locked_trades >= trader._max_tradenum then
834 -- Emergency unlock! Unlock all other trades except the current one
835 for t=1, #trades do
836 if t ~= tradenum then
837 trades[t].locked = false
838 trades[t].trade_counter = 0
841 trader._locked_trades = 1
842 -- Also heal trader for unlocking stuff
843 -- TODO: Replace by Regeneration I
844 trader.health = math.min(trader.hp_max, trader.health + 4)
847 trader._trades = minetest.serialize(trades)
848 if update_formspec then
849 show_trade_formspec(name, trader, tradenum)
851 else
852 minetest.log("error", "[mobs_mc] Player took item from trader output but player_trading_with or player_tradenum is nil!")
855 accept = true
856 elseif listname == "input" then
857 update_offer(inv, player, false)
859 if accept then
860 minetest.sound_play("mobs_mc_villager_accept", {to_player = name})
861 else
862 minetest.sound_play("mobs_mc_villager_deny", {to_player = name})
864 end,
867 minetest.register_on_joinplayer(function(player)
868 local name = player:get_player_name()
869 player_tradenum[name] = 1
870 player_trading_with[name] = nil
872 -- Create or get player-specific trading inventory
873 local inv = minetest.get_inventory({type="detached", name="mobs_mc:trade_"..name})
874 if not inv then
875 inv = minetest.create_detached_inventory("mobs_mc:trade_"..name, trade_inventory, name)
877 inv:set_size("input", 2)
878 inv:set_size("output", 1)
879 inv:set_size("wanted", 2)
880 inv:set_size("offered", 1)
881 end)
883 --[=======[ MOB REGISTRATION AND SPAWNING ]=======]
885 mobs:register_mob("mobs_mc:villager", {
886 type = "npc",
887 hp_min = 20,
888 hp_max = 20,
889 collisionbox = {-0.3, -0.01, -0.3, 0.3, 1.94, 0.3},
890 visual = "mesh",
891 mesh = "mobs_mc_villager.b3d",
892 textures = {
894 "mobs_mc_villager.png",
895 "mobs_mc_villager.png", --hat
898 "mobs_mc_villager_farmer.png",
899 "mobs_mc_villager_farmer.png", --hat
902 "mobs_mc_villager_priest.png",
903 "mobs_mc_villager_priest.png", --hat
906 "mobs_mc_villager_librarian.png",
907 "mobs_mc_villager_librarian.png", --hat
910 "mobs_mc_villager_butcher.png",
911 "mobs_mc_villager_butcher.png", --hat
914 "mobs_mc_villager_smith.png",
915 "mobs_mc_villager_smith.png", --hat
918 visual_size = {x=3, y=3},
919 makes_footstep_sound = true,
920 walk_velocity = 1.2,
921 run_velocity = 2.4,
922 drops = {},
923 sounds = {
924 random = "mobs_mc_villager_noise",
925 death = "mobs_mc_villager_death",
926 damage = "mobs_mc_villager_damage",
927 distance = 16,
929 animation = {
930 stand_speed = 25,
931 stand_start = 40,
932 stand_end = 59,
933 walk_speed = 25,
934 walk_start = 0,
935 walk_end = 40,
936 run_speed = 25,
937 run_start = 0,
938 run_end = 40,
939 die_speed = 15,
940 die_start = 210,
941 die_end = 220,
942 die_loop = false,
944 water_damage = 0,
945 lava_damage = 4,
946 light_damage = 0,
947 view_range = 16,
948 fear_height = 4,
949 jump = true,
950 walk_chance = DEFAULT_WALK_CHANCE,
951 on_rightclick = function(self, clicker)
952 -- Initiate trading
953 local name = clicker:get_player_name()
954 self._trading_players[name] = true
956 init_trader_vars(self)
957 if self._trades == nil then
958 init_trades(self)
960 update_max_tradenum(self)
961 if self._trades == false then
962 -- Villager has no trades, rightclick is a no-op
963 return
966 player_trading_with[name] = self
968 local inv = minetest.get_inventory({type="detached", name="mobs_mc:trade_"..name})
970 set_trade(self, clicker, inv, 1)
972 show_trade_formspec(name, self)
974 -- Behaviour stuff:
975 -- Make villager look at player and stand still
976 local selfpos = self.object:get_pos()
977 local clickerpos = clicker:get_pos()
978 local dir = vector.direction(selfpos, clickerpos)
979 self.object:set_yaw(minetest.dir_to_yaw(dir))
980 stand_still(self)
981 end,
983 _player_scan_timer = 0,
984 _trading_players = {}, -- list of playernames currently trading with villager (open formspec)
985 do_custom = function(self, dtime)
986 -- Stand still if player is nearby.
987 if not self._player_scan_timer then
988 self._player_scan_timer = 0
990 self._player_scan_timer = self._player_scan_timer + dtime
991 -- Check infrequently to keep CPU load low
992 if self._player_scan_timer > PLAYER_SCAN_INTERVAL then
993 self._player_scan_timer = 0
994 local selfpos = self.object:get_pos()
995 local objects = minetest.get_objects_inside_radius(selfpos, PLAYER_SCAN_RADIUS)
996 local has_player = false
997 for o, obj in pairs(objects) do
998 if obj:is_player() then
999 has_player = true
1000 break
1003 if has_player then
1004 minetest.log("verbose", "[mobs_mc] Player near villager found!")
1005 stand_still(self)
1006 else
1007 minetest.log("verbose", "[mobs_mc] No player near villager found!")
1008 self.walk_chance = DEFAULT_WALK_CHANCE
1009 self.jump = true
1012 end,
1014 on_spawn = function(self)
1015 init_trader_vars(self)
1016 end,
1017 on_die = function(self, pos)
1018 -- Close open trade formspecs and give input back to players
1019 local trading_players = self._trading_players
1020 for name, _ in pairs(trading_players) do
1021 minetest.close_formspec(name, "mobs_mc:trade_"..name)
1022 local player = minetest.get_player_by_name(name)
1023 if player then
1024 return_fields(player)
1027 end,
1032 mobs:spawn_specific("mobs_mc:villager", mobs_mc.spawn.village, {"air"}, 0, minetest.LIGHT_MAX+1, 30, 8000, 4, mobs_mc.spawn_height.water+1, mobs_mc.spawn_height.overworld_max)
1034 -- compatibility
1035 mobs:alias_mob("mobs:villager", "mobs_mc:villager")
1037 -- spawn eggs
1038 mobs:register_egg("mobs_mc:villager", S("Villager"), "mobs_mc_spawn_icon_villager.png", 0)
1040 if minetest.settings:get_bool("log_mods") then
1041 minetest.log("action", "MC mobs loaded")