3 --made for MC like Survival game
4 --License for code WTFPL and otherwise stated in readmes
6 -- TODO: Per-player trading inventories
10 local MP
= minetest
.get_modpath(minetest
.get_current_modname())
11 local S
, NS
= dofile(MP
.."/intllib.lua")
13 -- playername-indexed table containing the previously used tradenum
14 local player_tradenum
= {}
15 -- playername-indexed table containing the objectref of trader, if trading formspec is open
16 local player_trading_with
= {}
19 --################### VILLAGER
22 -- LIST OF VILLAGER PROFESSIONS AND TRADES
23 local E1
= { "mcl_core:emerald", 1, 1 } -- one emerald
27 texture
= "mobs_mc_villager_farmer.png",
30 { { "mcl_farming:wheat_item", 18, 22, }, E1
},
31 { { "mcl_farming:potato_item", 15, 15, }, E1
},
32 { { "mcl_farming:carrot_item", 15, 19, }, E1
},
33 { E1
, { "mcl_farming:bread", 2, 4 } },
37 { { "mcl_farming:pumpkin_face", 8, 13 }, E1
},
38 { E1
, { "mcl_farming:pumpkin_pie", 2, 3} },
42 { { "mcl_farming:melon", 7, 12 }, E1
},
43 { E1
, { "mcl_core:apple", 5, 7 }, },
47 { E1
, { "mcl_farming:cookie", 6, 10 } },
48 { E1
, { "mcl_cake:cake", 1, 1 } },
54 texture
= "mobs_mc_villager_farmer.png",
57 { { "mcl_fishing:fish_raw", 6, 6, "mcl_core:emerald", 1, 1 }, { "mcl_fishing:fish_cooked", 6, 6 } },
58 { { "mcl_mobitems:string", 15, 20 }, E1
},
59 { { "mcl_core:coal_lump", 16, 24 }, E1
},
61 -- TODO: enchanted fishing rod
66 texture
= "mobs_mc_villager_farmer.png",
69 { { "mcl_mobitems:string", 15, 20 }, E1
},
70 { E1
, { "mcl_bows:arrow", 8, 12 } },
74 { { "mcl_core:gravel", 10, 10, "mcl_core:emerald", 1, 1 }, { "mcl_core:flint", 6, 10 } },
75 { { "mcl_core:emerald", 2, 3 }, { "mcl_bows:bow", 1, 1 } },
81 texture
= "mobs_mc_villager_farmer.png",
84 { { "mcl_wool:white", 16, 22 }, E1
},
85 { { "mcl_core:emerald", 3, 4 }, { "mcl_tools:shears", 1, 1 } },
89 { { "mcl_core:emerald", 1, 2 }, { "mcl_wool:white", 1, 1 } },
90 { { "mcl_core:emerald", 1, 2 }, { "mcl_wool:grey", 1, 1 } },
91 { { "mcl_core:emerald", 1, 2 }, { "mcl_wool:silver", 1, 1 } },
92 { { "mcl_core:emerald", 1, 2 }, { "mcl_wool:yellow", 1, 1 } },
93 { { "mcl_core:emerald", 1, 2 }, { "mcl_wool:red", 1, 1 } },
94 { { "mcl_core:emerald", 1, 2 }, { "mcl_wool:purple", 1, 1 } },
95 { { "mcl_core:emerald", 1, 2 }, { "mcl_wool:blue", 1, 1 } },
96 { { "mcl_core:emerald", 1, 2 }, { "mcl_wool:light_blue", 1, 1 } },
97 { { "mcl_core:emerald", 1, 2 }, { "mcl_wool:brown", 1, 1 } },
98 { { "mcl_core:emerald", 1, 2 }, { "mcl_wool:lime", 1, 1 } },
99 { { "mcl_core:emerald", 1, 2 }, { "mcl_wool:green", 1, 1 } },
100 { { "mcl_core:emerald", 1, 2 }, { "mcl_wool:magenta", 1, 1 } },
101 { { "mcl_core:emerald", 1, 2 }, { "mcl_wool:black", 1, 1 } },
102 { { "mcl_core:emerald", 1, 2 }, { "mcl_wool:cyan", 1, 1 } },
103 { { "mcl_core:emerald", 1, 2 }, { "mcl_wool:pink", 1, 1 } },
109 texture
= "mobs_mc_villager_librarian.png",
112 { { "mcl_core:paper", 24, 36 }, E1
},
113 -- TODO: enchanted book
114 { { "mcl_books:book", 8, 10 }, E1
},
115 { { "mcl_core:emerald", 10, 12 }, { "mcl_compass:compass", 1 ,1 }},
116 { { "mcl_core:emerald", 3, 4 }, { "mcl_books:bookshelf", 1 ,1 }},
120 { { "mcl_books:written_book", 2, 2 }, E1
},
121 { { "mcl_core:emerald", 10, 12 }, { "mcl_clock:clock", 1, 1 } },
122 { E1
, { "mcl_core:glass", 3, 5 } },
126 { E1
, { "mcl_core:glass", 3, 5 } },
129 -- TODO: 2 enchanted book tiers
132 { { "mcl_core:emerald", 20, 22 }, { "mcl_mobs:nametag", 1, 1 } },
137 name
= "Cartographer",
138 texture
= "mobs_mc_villager_librarian.png",
141 { { "mcl_core:paper", 24, 36 }, E1
},
146 -- the difficulty lies in supporting the compass group, not the concrete item
147 -- { { "mcl_compass:compass", 1, 1 }, E1 },
151 -- TODO: replace with empty map
152 { { "mcl_core:emerald", 7, 11}, { "mcl_maps:filled_map", 1, 1 } },
155 -- TODO: special maps
160 texture
= "mobs_mc_villager_smith.png",
163 { { "mcl_core:coal_lump", 16, 24 }, E1
},
164 { { "mcl_core:emerald", 6, 8 }, { "3d_armor:helmet_iron", 1, 1 } },
168 { { "mcl_core:iron_ingot", 7, 9 }, E1
},
169 { { "mcl_core:emerald", 10, 14 }, { "3d_armor:chestplate_iron", 1, 1 } },
173 { { "mcl_core:diamond", 3, 4 }, E1
},
175 { { "mcl_core:emerald", 16, 19 }, { "3d_armor:chestplate_diamond", 1, 1 } },
179 { { "mcl_core:emerald", 5, 7 }, { "3d_armor:boots_chain", 1, 1 } },
180 { { "mcl_core:emerald", 9, 11 }, { "3d_armor:leggings_chain", 1, 1 } },
181 { { "mcl_core:emerald", 5, 7 }, { "3d_armor:helmet_chain", 1, 1 } },
182 { { "mcl_core:emerald", 11, 15 }, { "3d_armor:chestplate_chain", 1, 1 } },
187 name
= "Leatherworker",
188 texture
= "mobs_mc_villager_butcher.png",
191 { { "mcl_mobitems:leather", 9, 12 }, E1
},
192 { { "mcl_core:emerald", 2, 4 }, { "3d_armor:leggings_leather", 2, 4 } },
197 { { "mcl_core:emerald", 7, 12 }, { "3d_armor:chestplate_leather", 1, 1 } },
201 { { "mcl_core:emerald", 8, 10 }, { "mcl_mobitems:saddle", 1, 1 } },
207 texture
= "mobs_mc_villager_butcher.png",
210 { { "mcl_mobitems:beef", 14, 18 }, E1
},
211 { { "mcl_mobitems:chicken", 14, 18 }, E1
},
215 { { "mcl_core:coal_lump", 16, 24 }, E1
},
216 { E1
, { "mcl_mobitems:cooked_beef", 5, 7 } },
217 { E1
, { "mcl_mobitems:cooked_chicken", 6, 8 } },
222 name
= "Weapon Smith",
223 texture
= "mobs_mc_villager_smith.png",
226 { { "mcl_core:coal_lump", 16, 24 }, E1
},
227 { { "mcl_core:emerald", 6, 8 }, { "mcl_tools:axe_iron", 1, 1 } },
231 { { "mcl_core:iron_ingot", 7, 9 }, E1
},
233 { { "mcl_core:emerald", 9, 10 }, { "mcl_tools:sword_iron", 1, 1 } },
237 { { "mcl_core:diamond", 3, 4 }, E1
},
239 { { "mcl_core:emerald", 12, 15 }, { "mcl_tools:sword_diamond", 1, 1 } },
241 { { "mcl_core:emerald", 9, 12 }, { "mcl_tools:axe_diamond", 1, 1 } },
247 texture
= "mobs_mc_villager_smith.png",
250 { { "mcl_core:coal_lump", 16, 24 }, E1
},
252 { { "mcl_core:emerald", 5, 7 }, { "mcl_tools:shovel_iron", 1, 1 } },
256 { { "mcl_core:iron_ingot", 7, 9 }, E1
},
258 { { "mcl_core:emerald", 9, 11 }, { "mcl_tools:pick_iron", 1, 1 } },
262 { { "mcl_core:diamond", 3, 4 }, E1
},
264 { { "mcl_core:emerald", 12, 15 }, { "mcl_tools:pick_diamond", 1, 1 } },
270 texture
= "mobs_mc_villager_priest.png",
273 { { "mcl_mobitems:rotten_flesh", 36, 40 }, E1
},
274 { { "mcl_core:gold_ingot", 8, 10 }, E1
},
278 { E1
, { "mesecons:redstone", 1, 4 } },
279 { E1
, { "mcl_dye:blue", 1, 2 } },
283 { E1
, { "mcl_nether:glowstone", 1, 3 } },
284 { { "mcl_core:emerald", 4, 7 }, { "mcl_throwing:ender_pearl", 1, 1 } },
287 -- TODO: Bottle 'o enchanting
292 texture
= "mobs_mc_villager.png",
293 -- No trades for nitwit
298 local profession_names
= {}
299 for id
, _
in pairs(professions
) do
300 table.insert(profession_names
, id
)
303 local init_profession
= function(self
)
304 if not self
._profession
then
305 -- Select random profession from all professions with matching clothing
306 local texture
= self
.base_texture
[1]
308 for prof_id
, prof
in pairs(professions
) do
309 if texture
== prof
.texture
then
310 table.insert(matches
, prof_id
)
313 local p
= math
.random(1, #matches
)
314 self
._profession
= matches
[p
]
316 if not self
._max_trade_tier
then
317 self
._max_trade_tier
= 1
321 local init_trades
= function(self
, inv
)
322 local profession
= professions
[self
._profession
]
323 local trade_tiers
= profession
.trades
324 if trade_tiers
== nil then
330 local max_tier
= #trade_tiers
332 for tiernum
=1, max_tier
do
333 local tier
= trade_tiers
[tiernum
]
334 for tradenum
=1, #tier
do
335 local trade
= tier
[tradenum
]
336 local wanted1_item
= trade
[1][1]
337 local wanted1_count
= math
.random(trade
[1][2], trade
[1][3])
338 local offered_item
= trade
[2][1]
339 local offered_count
= math
.random(trade
[2][2], trade
[2][3])
341 local wanted
= { wanted1_item
.. " " ..wanted1_count
}
343 local wanted2_item
= trade
[1][4]
344 local wanted2_count
= math
.random(trade
[1][5], trade
[1][6])
345 table.insert(wanted
, wanted2_item
.. " " ..wanted2_count
)
348 table.insert(trades
, {
350 offered
= offered_item
.. " " .. offered_count
,
351 tier
= tiernum
, -- tier of this trade
352 traded_once
= false, -- true if trade was traded at least once
353 trade_counter
= 0, -- how often the this trade was mate after the last time it got unlocked
354 locked
= false, -- if this trade is locked. Locked trades can't be used
358 self
._trades
= minetest
.serialize(trades
)
361 local set_trade
= function(trader
, player
, inv
, concrete_tradenum
)
362 local trades
= minetest
.deserialize(trader
._trades
)
365 trades
= minetest
.deserialize(trader
._trades
)
367 minetest
.log("error", "[mobs_mc] Failed to select villager trade!")
372 if concrete_tradenum
> #trades
then
373 concrete_tradenum
= 1
374 player_tradenum
[player
:get_player_name()] = concrete_tradenum
375 elseif concrete_tradenum
< 1 then
376 concrete_tradenum
= #trades
377 player_tradenum
[player
:get_player_name()] = concrete_tradenum
379 local trade
= trades
[concrete_tradenum
]
380 if trader
._max_trade_tier
< trade
.tier
then
381 concrete_tradenum
= 1
382 player_tradenum
[player
:get_player_name()] = concrete_tradenum
383 trade
= trades
[concrete_tradenum
]
385 inv
:set_stack("wanted", 1, ItemStack(trade
.wanted
[1]))
386 inv
:set_stack("offered", 1, ItemStack(trade
.offered
))
387 if trade
.wanted
[2] then
388 local wanted2
= ItemStack(trade
.wanted
[2])
389 inv
:set_stack("wanted", 2, wanted2
)
391 inv
:set_stack("wanted", 2, "")
396 local function show_trade_formspec(playername
, trader
, is_disabled
)
397 local profession
= professions
[trader
._profession
].name
400 disabled
= "image[4.3,2.52;1,1;mobs_mc_trading_formspec_disabled.png]"
404 "background[-0.19,-0.25;9.41,9.49;mobs_mc_trading_formspec_bg.png]"..
406 mcl_vars
.inventory_header
..
407 "label[4,0;"..minetest
.formspec_escape(profession
).."]"
408 .."list[current_player;main;0,4.5;9,3;9]"
409 .."list[current_player;main;0,7.74;9,1;]"
410 .."button[1,1;0.5,1;prev_trade;<]"
411 .."button[7.26,1;0.5,1;next_trade;>]"
412 .."list[detached:mobs_mc:trade;wanted;2,1;2,1;]"
413 .."list[detached:mobs_mc:trade;offered;5.76,1;1,1;]"
414 .."list[detached:mobs_mc:trade;input;2,2.5;2,1;]"
415 .."list[detached:mobs_mc:trade;output;5.76,2.55;1,1;]"
416 .."listring[detached:mobs_mc:trade;output]"
417 .."listring[current_player;main]"
418 .."listring[detached:mobs_mc:trade;input]"
419 .."listring[current_player;main]"
420 minetest
.sound_play("mobs_mc_villager_trade", {to_player
= playername
})
421 minetest
.show_formspec(playername
, "mobs_mc:trade", formspec
)
424 local update_offer
= function(inv
, player
, sound
)
425 if inv
:contains_item("input", inv
:get_stack("wanted", 1)) and
426 (inv
:get_stack("wanted", 2):is_empty() or inv
:contains_item("input", inv
:get_stack("wanted", 2))) then
427 inv
:set_stack("output", 1, inv
:get_stack("offered", 1))
429 minetest
.sound_play("mobs_mc_villager_accept", {to_player
= player
:get_player_name()})
433 inv
:set_stack("output", 1, ItemStack(""))
435 minetest
.sound_play("mobs_mc_villager_deny", {to_player
= player
:get_player_name()})
441 mobs
:register_mob("mobs_mc:villager", {
445 collisionbox
= {-0.3, -0.01, -0.3, 0.3, 1.94, 0.3},
447 mesh
= "mobs_mc_villager.b3d",
450 "mobs_mc_villager.png",
451 "mobs_mc_villager.png", --hat
454 "mobs_mc_villager_farmer.png",
455 "mobs_mc_villager_farmer.png", --hat
458 "mobs_mc_villager_priest.png",
459 "mobs_mc_villager_priest.png", --hat
462 "mobs_mc_villager_librarian.png",
463 "mobs_mc_villager_librarian.png", --hat
466 "mobs_mc_villager_butcher.png",
467 "mobs_mc_villager_butcher.png", --hat
470 "mobs_mc_villager_smith.png",
471 "mobs_mc_villager_smith.png", --hat
474 visual_size
= {x
=3, y
=3},
475 makes_footstep_sound
= true,
480 random = "mobs_mc_villager_noise",
481 death
= "mobs_mc_villager_death",
482 damage
= "mobs_mc_villager_damage",
505 on_rightclick
= function(self
, clicker
)
506 local name
= clicker
:get_player_name()
508 init_profession(self
)
509 if self
._trades
== nil then
512 if self
._trades
== false then
513 -- Villager has no trades, rightclick is a no-op
517 player_trading_with
[name
] = self
519 -- TODO: Create per-player trading inventories
520 local inv
= minetest
.get_inventory({type="detached", name
="mobs_mc:trade"})
522 inv
= minetest
.create_detached_inventory("mobs_mc:trade", {
523 allow_take
= function(inv
, listname
, index
, stack
, player
)
524 if listname
== "input" then
525 return stack
:get_count()
526 elseif listname
== "output" then
527 -- Only allow taking full stack
528 local count
= stack
:get_count()
529 if count
== inv
:get_stack(listname
, index
):get_count() then
530 -- Also update output stack again.
531 -- If input has double the wanted items, the
532 -- output will stay because there will be still
533 -- enough items in input after the trade
534 local wanted1
= inv
:get_stack("wanted", 1)
535 local wanted2
= inv
:get_stack("wanted", 2)
536 wanted1
:set_count(wanted1
:get_count()*2)
537 wanted2
:set_count(wanted2
:get_count()*2)
538 if inv
:contains_item("input", wanted1
) and
539 (wanted2
:is_empty() or inv
:contains_item("input", wanted2
)) then
542 -- If less than double the wanted items,
543 -- remove items from output (final trade,
554 allow_move
= function(inv
, from_list
, from_index
, to_list
, to_index
, count
, player
)
555 if from_list
== "input" and to_list
== "input" then
557 elseif from_list
== "output" and to_list
== "input" then
558 local move_stack
= inv
:get_stack(from_list
, from_index
)
559 if inv
:get_stack(to_list
, to_index
):item_fits(move_stack
) then
565 allow_put
= function(inv
, listname
, index
, stack
, player
)
566 if listname
== "input" then
567 return stack
:get_count()
572 on_put
= function(inv
, listname
, index
, stack
, player
)
573 update_offer(inv
, player
, true)
575 on_move
= function(inv
, from_list
, from_index
, to_list
, to_index
, count
, player
)
576 if from_list
== "output" and to_list
== "input" then
577 inv
:remove_item("input", inv
:get_stack("wanted", 1))
578 local wanted2
= inv
:get_stack("wanted", 2)
579 if not wanted2
:is_empty() then
580 inv
:remove_item("input", inv
:get_stack("wanted", 2))
582 minetest
.sound_play("mobs_mc_villager_accept", {to_player
= player
:get_player_name()})
584 update_offer(inv
, player
, true)
586 on_take
= function(inv
, listname
, index
, stack
, player
)
588 local name
= player
:get_player_name()
589 if listname
== "output" then
590 inv
:remove_item("input", inv
:get_stack("wanted", 1))
591 local wanted2
= inv
:get_stack("wanted", 2)
592 if not wanted2
:is_empty() then
593 inv
:remove_item("input", inv
:get_stack("wanted", 2))
595 local trader
= player_trading_with
[name
]
596 local tradenum
= player_tradenum
[name
]
598 if trader
and trader
._trades
then
599 trades
= minetest
.deserialize(trader
._trades
)
602 local trade
= trades
[tradenum
]
603 local unlock_stuff
= false
604 if not trade
.traded_once
then
606 trade
.traded_once
= true
607 elseif math
.random(1,5) then
608 -- Otherwise, 20% chance to unlock all trades
612 -- First-time trade unlock all trades and unlock next trade tier
613 if trade
.tier
+ 1 > trader
._max_trade_tier
then
614 trader
._max_trade_tier
= trader
._max_trade_tier
+ 1
617 trades
[t
].locked
= false
618 trades
[t
].trade_counter
= 0
621 trade
.trade_counter
= trade
.trade_counter
+ 1
622 if trade
.trade_counter
>= 12 then
624 elseif trade
.trade_counter
>= 2 then
625 math
.random(1, math
.random(1, 20))
629 minetest
.log("error", "[mobs_mc] Player took item from trader output but player_trading_with or player_tradenum is nil!")
632 elseif listname
== "input" then
633 update_offer(inv
, player
, false)
636 minetest
.sound_play("mobs_mc_villager_accept", {to_player
= name
})
638 minetest
.sound_play("mobs_mc_villager_deny", {to_player
= name
})
643 inv
:set_size("input", 2)
644 inv
:set_size("output", 1)
645 inv
:set_size("wanted", 2)
646 inv
:set_size("offered", 1)
648 player_tradenum
[name
] = 1
649 set_trade(self
, clicker
, inv
, player_tradenum
[name
])
651 show_trade_formspec(name
, self
)
654 on_spawn
= function(self
)
655 init_profession(self
)
659 -- Returns a single itemstack in the given inventory to the player's main inventory, or drop it when there's no space left
660 local function return_item(itemstack
, dropper
, pos
, inv_p
)
661 if dropper
:is_player() then
662 -- Return to main inventory
663 if inv_p
:room_for_item("main", itemstack
) then
664 inv_p
:add_item("main", itemstack
)
666 -- Drop item on the ground
667 local v
= dropper
:get_look_dir()
668 local p
= {x
=pos
.x
, y
=pos
.y
+1.2, z
=pos
.z
}
669 p
.x
= p
.x
+(math
.random(1,3)*0.2)
670 p
.z
= p
.z
+(math
.random(1,3)*0.2)
671 local obj
= minetest
.add_item(p
, itemstack
)
677 obj
:get_luaentity()._insta_collect
= false
681 -- Fallback for unexpected cases
682 minetest
.add_item(pos
, itemstack
)
687 local return_fields
= function(player
)
688 local inv_t
= minetest
.get_inventory({type="detached", name
= "mobs_mc:trade"})
689 local inv_p
= player
:get_inventory()
690 for i
=1, inv_t
:get_size("input") do
691 local stack
= inv_t
:get_stack("input", i
)
692 return_item(stack
, player
, player
:get_pos(), inv_p
)
694 inv_t
:set_stack("input", i
, stack
)
696 inv_t
:set_stack("output", 1, "")
699 minetest
.register_on_player_receive_fields(function(player
, formname
, fields
)
700 if formname
== "mobs_mc:trade" then
701 local name
= player
:get_player_name()
703 return_fields(player
)
704 player_trading_with
[name
] = nil
705 elseif fields
.next_trade
then
706 local trader
= player_trading_with
[name
]
707 if not trader
or not trader
.object
:get_luaentity() then
710 local trades
= trader
._trades
714 player_tradenum
[name
] = player_tradenum
[name
] + 1
715 local inv
= minetest
.get_inventory({type="detached", name
="mobs_mc:trade"})
716 set_trade(trader
, player
, inv
, player_tradenum
[name
])
717 update_offer(inv
, player
, false)
718 show_trade_formspec(name
, trader
)
719 elseif fields
.prev_trade
then
720 local trader
= player_trading_with
[name
]
721 if not trader
or not trader
.object
:get_luaentity() then
724 local trades
= trader
._trades
728 player_tradenum
[name
] = player_tradenum
[name
] - 1
729 local inv
= minetest
.get_inventory({type="detached", name
="mobs_mc:trade"})
730 set_trade(trader
, player
, inv
, player_tradenum
[name
])
731 update_offer(inv
, player
, false)
732 show_trade_formspec(name
, trader
)
737 minetest
.register_on_leaveplayer(function(player
)
738 return_fields(player
)
739 player_tradenum
[player
:get_player_name()] = nil
740 player_trading_with
[player
:get_player_name()] = nil
743 minetest
.register_on_joinplayer(function(player
)
744 player_tradenum
[player
:get_player_name()] = 1
745 player_trading_with
[player
:get_player_name()] = nil
748 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
)
751 mobs
:alias_mob("mobs:villager", "mobs_mc:villager")
754 mobs
:register_egg("mobs_mc:villager", S("Villager"), "mobs_mc_spawn_icon_villager.png", 0)
756 if minetest
.settings
:get_bool("log_mods") then
757 minetest
.log("action", "MC mobs loaded")