3 --made for MC like Survival game
4 --License for code WTFPL and otherwise stated in readmes
6 -- TODO: Per-player trading inventories
11 local MP
= minetest
.get_modpath(minetest
.get_current_modname())
12 local S
, NS
= dofile(MP
.."/intllib.lua")
14 -- playername-indexed table containing the previously used tradenum
15 local player_tradenum
= {}
16 -- playername-indexed table containing the objectref of trader, if trading formspec is open
17 local player_trading_with
= {}
20 --################### VILLAGER
23 -- LIST OF VILLAGER PROFESSIONS AND TRADES
24 local E1
= { "mcl_core:emerald", 1, 1 } -- one emerald
28 texture
= "mobs_mc_villager_farmer.png",
31 { { "mcl_farming:wheat_item", 18, 22, }, E1
},
32 { { "mcl_farming:potato_item", 15, 15, }, E1
},
33 { { "mcl_farming:carrot_item", 15, 19, }, E1
},
34 { E1
, { "mcl_farming:bread", 2, 4 } },
38 { { "mcl_farming:pumpkin_face", 8, 13 }, E1
},
39 { E1
, { "mcl_farming:pumpkin_pie", 2, 3} },
43 { { "mcl_farming:melon", 7, 12 }, E1
},
44 { E1
, { "mcl_core:apple", 5, 7 }, },
48 { E1
, { "mcl_farming:cookie", 6, 10 } },
49 { E1
, { "mcl_cake:cake", 1, 1 } },
55 texture
= "mobs_mc_villager_farmer.png",
58 { { "mcl_fishing:fish_raw", 6, 6, "mcl_core:emerald", 1, 1 }, { "mcl_fishing:fish_cooked", 6, 6 } },
59 { { "mcl_mobitems:string", 15, 20 }, E1
},
60 { { "mcl_core:coal_lump", 16, 24 }, E1
},
62 -- TODO: enchanted fishing rod
67 texture
= "mobs_mc_villager_farmer.png",
70 { { "mcl_mobitems:string", 15, 20 }, E1
},
71 { E1
, { "mcl_bows:arrow", 8, 12 } },
75 { { "mcl_core:gravel", 10, 10, "mcl_core:emerald", 1, 1 }, { "mcl_core:flint", 6, 10 } },
76 { { "mcl_core:emerald", 2, 3 }, { "mcl_bows:bow", 1, 1 } },
82 texture
= "mobs_mc_villager_farmer.png",
85 { { "mcl_wool:white", 16, 22 }, E1
},
86 { { "mcl_core:emerald", 3, 4 }, { "mcl_tools:shears", 1, 1 } },
90 { { "mcl_core:emerald", 1, 2 }, { "mcl_wool:white", 1, 1 } },
91 { { "mcl_core:emerald", 1, 2 }, { "mcl_wool:grey", 1, 1 } },
92 { { "mcl_core:emerald", 1, 2 }, { "mcl_wool:silver", 1, 1 } },
93 { { "mcl_core:emerald", 1, 2 }, { "mcl_wool:yellow", 1, 1 } },
94 { { "mcl_core:emerald", 1, 2 }, { "mcl_wool:red", 1, 1 } },
95 { { "mcl_core:emerald", 1, 2 }, { "mcl_wool:purple", 1, 1 } },
96 { { "mcl_core:emerald", 1, 2 }, { "mcl_wool:blue", 1, 1 } },
97 { { "mcl_core:emerald", 1, 2 }, { "mcl_wool:light_blue", 1, 1 } },
98 { { "mcl_core:emerald", 1, 2 }, { "mcl_wool:brown", 1, 1 } },
99 { { "mcl_core:emerald", 1, 2 }, { "mcl_wool:lime", 1, 1 } },
100 { { "mcl_core:emerald", 1, 2 }, { "mcl_wool:green", 1, 1 } },
101 { { "mcl_core:emerald", 1, 2 }, { "mcl_wool:magenta", 1, 1 } },
102 { { "mcl_core:emerald", 1, 2 }, { "mcl_wool:black", 1, 1 } },
103 { { "mcl_core:emerald", 1, 2 }, { "mcl_wool:cyan", 1, 1 } },
104 { { "mcl_core:emerald", 1, 2 }, { "mcl_wool:pink", 1, 1 } },
110 texture
= "mobs_mc_villager_librarian.png",
113 { { "mcl_core:paper", 24, 36 }, E1
},
114 -- TODO: enchanted book
115 { { "mcl_books:book", 8, 10 }, E1
},
116 { { "mcl_core:emerald", 10, 12 }, { "mcl_compass:compass", 1 ,1 }},
117 { { "mcl_core:emerald", 3, 4 }, { "mcl_books:bookshelf", 1 ,1 }},
121 { { "mcl_books:written_book", 2, 2 }, E1
},
122 { { "mcl_core:emerald", 10, 12 }, { "mcl_clock:clock", 1, 1 } },
123 { E1
, { "mcl_core:glass", 3, 5 } },
127 { E1
, { "mcl_core:glass", 3, 5 } },
130 -- TODO: 2 enchanted book tiers
133 { { "mcl_core:emerald", 20, 22 }, { "mcl_mobs:nametag", 1, 1 } },
138 name
= "Cartographer",
139 texture
= "mobs_mc_villager_librarian.png",
142 { { "mcl_core:paper", 24, 36 }, E1
},
147 -- the difficulty lies in supporting the compass group, not the concrete item
148 -- { { "mcl_compass:compass", 1, 1 }, E1 },
152 -- TODO: replace with empty map
153 { { "mcl_core:emerald", 7, 11}, { "mcl_maps:filled_map", 1, 1 } },
156 -- TODO: special maps
161 texture
= "mobs_mc_villager_smith.png",
164 { { "mcl_core:coal_lump", 16, 24 }, E1
},
165 { { "mcl_core:emerald", 6, 8 }, { "3d_armor:helmet_iron", 1, 1 } },
169 { { "mcl_core:iron_ingot", 7, 9 }, E1
},
170 { { "mcl_core:emerald", 10, 14 }, { "3d_armor:chestplate_iron", 1, 1 } },
174 { { "mcl_core:diamond", 3, 4 }, E1
},
176 { { "mcl_core:emerald", 16, 19 }, { "3d_armor:chestplate_diamond", 1, 1 } },
180 { { "mcl_core:emerald", 5, 7 }, { "3d_armor:boots_chain", 1, 1 } },
181 { { "mcl_core:emerald", 9, 11 }, { "3d_armor:leggings_chain", 1, 1 } },
182 { { "mcl_core:emerald", 5, 7 }, { "3d_armor:helmet_chain", 1, 1 } },
183 { { "mcl_core:emerald", 11, 15 }, { "3d_armor:chestplate_chain", 1, 1 } },
188 name
= "Leatherworker",
189 texture
= "mobs_mc_villager_butcher.png",
192 { { "mcl_mobitems:leather", 9, 12 }, E1
},
193 { { "mcl_core:emerald", 2, 4 }, { "3d_armor:leggings_leather", 2, 4 } },
198 { { "mcl_core:emerald", 7, 12 }, { "3d_armor:chestplate_leather", 1, 1 } },
202 { { "mcl_core:emerald", 8, 10 }, { "mcl_mobitems:saddle", 1, 1 } },
208 texture
= "mobs_mc_villager_butcher.png",
211 { { "mcl_mobitems:beef", 14, 18 }, E1
},
212 { { "mcl_mobitems:chicken", 14, 18 }, E1
},
216 { { "mcl_core:coal_lump", 16, 24 }, E1
},
217 { E1
, { "mcl_mobitems:cooked_beef", 5, 7 } },
218 { E1
, { "mcl_mobitems:cooked_chicken", 6, 8 } },
223 name
= "Weapon Smith",
224 texture
= "mobs_mc_villager_smith.png",
227 { { "mcl_core:coal_lump", 16, 24 }, E1
},
228 { { "mcl_core:emerald", 6, 8 }, { "mcl_tools:axe_iron", 1, 1 } },
232 { { "mcl_core:iron_ingot", 7, 9 }, E1
},
234 { { "mcl_core:emerald", 9, 10 }, { "mcl_tools:sword_iron", 1, 1 } },
238 { { "mcl_core:diamond", 3, 4 }, E1
},
240 { { "mcl_core:emerald", 12, 15 }, { "mcl_tools:sword_diamond", 1, 1 } },
242 { { "mcl_core:emerald", 9, 12 }, { "mcl_tools:axe_diamond", 1, 1 } },
248 texture
= "mobs_mc_villager_smith.png",
251 { { "mcl_core:coal_lump", 16, 24 }, E1
},
253 { { "mcl_core:emerald", 5, 7 }, { "mcl_tools:shovel_iron", 1, 1 } },
257 { { "mcl_core:iron_ingot", 7, 9 }, E1
},
259 { { "mcl_core:emerald", 9, 11 }, { "mcl_tools:pick_iron", 1, 1 } },
263 { { "mcl_core:diamond", 3, 4 }, E1
},
265 { { "mcl_core:emerald", 12, 15 }, { "mcl_tools:pick_diamond", 1, 1 } },
271 texture
= "mobs_mc_villager_priest.png",
274 { { "mcl_mobitems:rotten_flesh", 36, 40 }, E1
},
275 { { "mcl_core:gold_ingot", 8, 10 }, E1
},
279 { E1
, { "mesecons:redstone", 1, 4 } },
280 { E1
, { "mcl_dye:blue", 1, 2 } },
284 { E1
, { "mcl_nether:glowstone", 1, 3 } },
285 { { "mcl_core:emerald", 4, 7 }, { "mcl_throwing:ender_pearl", 1, 1 } },
288 -- TODO: Bottle 'o enchanting
293 texture
= "mobs_mc_villager.png",
294 -- No trades for nitwit
299 local profession_names
= {}
300 for id
, _
in pairs(professions
) do
301 table.insert(profession_names
, id
)
304 local init_profession
= function(self
)
305 if not self
._profession
then
306 -- Select random profession from all professions with matching clothing
307 local texture
= self
.base_texture
[1]
309 for prof_id
, prof
in pairs(professions
) do
310 if texture
== prof
.texture
then
311 table.insert(matches
, prof_id
)
314 local p
= math
.random(1, #matches
)
315 self
._profession
= matches
[p
]
317 if not self
._max_trade_tier
then
318 -- TODO: Start with tier 1
319 self
._max_trade_tier
= 10
323 local update_trades
= function(self
, inv
)
324 local profession
= professions
[self
._profession
]
325 local trade_tiers
= profession
.trades
326 if trade_tiers
== nil then
332 local max_tier
= math
.min(#trade_tiers
, self
._max_trade_tier
)
334 for tiernum
=1, max_tier
do
335 local tier
= trade_tiers
[tiernum
]
336 for tradenum
=1, #tier
do
337 local trade
= tier
[tradenum
]
338 local wanted1_item
= trade
[1][1]
339 local wanted1_count
= math
.random(trade
[1][2], trade
[1][3])
340 local offered_item
= trade
[2][1]
341 local offered_count
= math
.random(trade
[2][2], trade
[2][3])
343 local wanted
= { wanted1_item
.. " " ..wanted1_count
}
345 local wanted2_item
= trade
[1][4]
346 local wanted2_count
= math
.random(trade
[1][5], trade
[1][6])
347 table.insert(wanted
, wanted2_item
.. " " ..wanted2_count
)
350 table.insert(trades
, {
352 offered
= offered_item
.. " " .. offered_count
,
357 self
._trades
= minetest
.serialize(trades
)
360 local set_trade
= function(self
, player
, inv
, concrete_tradenum
)
361 local trades
= minetest
.deserialize(self
._trades
)
364 trades
= minetest
.deserialize(self
._trades
)
366 minetest
.log("error", "[mobs_mc] Failed to select villager trade!")
371 if concrete_tradenum
> #trades
then
372 concrete_tradenum
= 1
373 player_tradenum
[player
:get_player_name()] = concrete_tradenum
374 elseif concrete_tradenum
< 1 then
375 concrete_tradenum
= #trades
376 player_tradenum
[player
:get_player_name()] = concrete_tradenum
378 local trade
= trades
[concrete_tradenum
]
379 inv
:set_stack("wanted", 1, ItemStack(trade
.wanted
[1]))
380 inv
:set_stack("offered", 1, ItemStack(trade
.offered
))
381 if trade
.wanted
[2] then
382 local wanted2
= ItemStack(trade
.wanted
[2])
383 inv
:set_stack("wanted", 2, wanted2
)
385 inv
:set_stack("wanted", 2, "")
390 local function show_trade_formspec(playername
, trader
)
391 local profession
= professions
[trader
._profession
].name
394 "background[-0.19,-0.25;9.41,9.49;mobs_mc_trading_formspec_bg.png]"..
395 mcl_vars
.inventory_header
..
396 "label[4,0;"..minetest
.formspec_escape(profession
).."]"
397 .."list[current_player;main;0,4.5;9,3;9]"
398 .."list[current_player;main;0,7.74;9,1;]"
399 .."button[1,1;0.5,1;prev_trade;<]"
400 .."button[7.26,1;0.5,1;next_trade;>]"
401 .."list[detached:mobs_mc:trade;wanted;2,1;2,1;]"
402 .."list[detached:mobs_mc:trade;offered;5.76,1;1,1;]"
403 .."list[detached:mobs_mc:trade;input;2,2.5;2,1;]"
404 .."list[detached:mobs_mc:trade;output;5.76,2.55;1,1;]"
405 .."listring[detached:mobs_mc:trade;output]"
406 .."listring[current_player;main]"
407 .."listring[detached:mobs_mc:trade;input]"
408 .."listring[current_player;main]"
409 minetest
.sound_play("mobs_mc_villager_trade", {to_player
= playername
})
410 minetest
.show_formspec(playername
, "mobs_mc:trade", formspec
)
413 local update_offer
= function(inv
, player
, sound
)
414 if inv
:contains_item("input", inv
:get_stack("wanted", 1)) and
415 (inv
:get_stack("wanted", 2):is_empty() or inv
:contains_item("input", inv
:get_stack("wanted", 2))) then
416 inv
:set_stack("output", 1, inv
:get_stack("offered", 1))
418 minetest
.sound_play("mobs_mc_villager_accept", {to_player
= player
:get_player_name()})
422 inv
:set_stack("output", 1, ItemStack(""))
424 minetest
.sound_play("mobs_mc_villager_deny", {to_player
= player
:get_player_name()})
430 mobs
:register_mob("mobs_mc:villager", {
434 collisionbox
= {-0.3, -0.01, -0.3, 0.3, 1.94, 0.3},
436 mesh
= "mobs_mc_villager.b3d",
439 "mobs_mc_villager.png",
440 "mobs_mc_villager.png", --hat
443 "mobs_mc_villager_farmer.png",
444 "mobs_mc_villager_farmer.png", --hat
447 "mobs_mc_villager_priest.png",
448 "mobs_mc_villager_priest.png", --hat
451 "mobs_mc_villager_librarian.png",
452 "mobs_mc_villager_librarian.png", --hat
455 "mobs_mc_villager_butcher.png",
456 "mobs_mc_villager_butcher.png", --hat
459 "mobs_mc_villager_smith.png",
460 "mobs_mc_villager_smith.png", --hat
463 visual_size
= {x
=3, y
=3},
464 makes_footstep_sound
= true,
469 random = "mobs_mc_villager_noise",
470 death
= "mobs_mc_villager_death",
471 damage
= "mobs_mc_villager_damage",
494 on_rightclick
= function(self
, clicker
)
495 local name
= clicker
:get_player_name()
497 init_profession(self
)
498 if self
._trades
== nil then
501 if self
._trades
== false then
502 -- Villager has no trades, rightclick is a no-op
506 player_trading_with
[name
] = self
508 -- TODO: Create per-player trading inventories
509 local inv
= minetest
.get_inventory({type="detached", name
="mobs_mc:trade"})
511 inv
= minetest
.create_detached_inventory("mobs_mc:trade", {
512 allow_take
= function(inv
, listname
, index
, stack
, player
)
513 if listname
== "input" then
514 return stack
:get_count()
515 elseif listname
== "output" then
516 -- Only allow taking full stack
517 local count
= stack
:get_count()
518 if count
== inv
:get_stack(listname
, index
):get_count() then
519 -- Also update output stack again.
520 -- If input has double the wanted items, the
521 -- output will stay because there will be still
522 -- enough items in input after the trade
523 local wanted1
= inv
:get_stack("wanted", 1)
524 local wanted2
= inv
:get_stack("wanted", 2)
525 wanted1
:set_count(wanted1
:get_count()*2)
526 wanted2
:set_count(wanted2
:get_count()*2)
527 if inv
:contains_item("input", wanted1
) and
528 (wanted2
:is_empty() or inv
:contains_item("input", wanted2
)) then
531 -- If less than double the wanted items,
532 -- remove items from output (final trade,
543 allow_move
= function(inv
, from_list
, from_index
, to_list
, to_index
, count
, player
)
544 if from_list
== "input" and to_list
== "input" then
546 elseif from_list
== "output" and to_list
== "input" then
547 local move_stack
= inv
:get_stack(from_list
, from_index
)
548 if inv
:get_stack(to_list
, to_index
):item_fits(move_stack
) then
554 allow_put
= function(inv
, listname
, index
, stack
, player
)
555 if listname
== "input" then
556 return stack
:get_count()
561 on_put
= function(inv
, listname
, index
, stack
, player
)
562 update_offer(inv
, player
, true)
564 on_move
= function(inv
, from_list
, from_index
, to_list
, to_index
, count
, player
)
565 if from_list
== "output" and to_list
== "input" then
566 inv
:remove_item("input", inv
:get_stack("wanted", 1))
567 local wanted2
= inv
:get_stack("wanted", 2)
568 if not wanted2
:is_empty() then
569 inv
:remove_item("input", inv
:get_stack("wanted", 2))
571 minetest
.sound_play("mobs_mc_villager_accept", {to_player
= player
:get_player_name()})
573 update_offer(inv
, player
, true)
575 on_take
= function(inv
, listname
, index
, stack
, player
)
577 if listname
== "output" then
578 inv
:remove_item("input", inv
:get_stack("wanted", 1))
579 local wanted2
= inv
:get_stack("wanted", 2)
580 if not wanted2
:is_empty() then
581 inv
:remove_item("input", inv
:get_stack("wanted", 2))
584 elseif listname
== "input" then
585 update_offer(inv
, player
, false)
588 minetest
.sound_play("mobs_mc_villager_accept", {to_player
= player
:get_player_name()})
590 minetest
.sound_play("mobs_mc_villager_deny", {to_player
= player
:get_player_name()})
595 inv
:set_size("input", 2)
596 inv
:set_size("output", 1)
597 inv
:set_size("wanted", 2)
598 inv
:set_size("offered", 1)
600 player_tradenum
[name
] = 1
601 set_trade(self
, player
, inv
, player_tradenum
[name
])
603 show_trade_formspec(name
, self
)
606 on_spawn
= function(self
)
607 init_profession(self
)
611 -- Returns a single itemstack in the given inventory to the player's main inventory, or drop it when there's no space left
612 local function return_item(itemstack
, dropper
, pos
, inv_p
)
613 if dropper
:is_player() then
614 -- Return to main inventory
615 if inv_p
:room_for_item("main", itemstack
) then
616 inv_p
:add_item("main", itemstack
)
618 -- Drop item on the ground
619 local v
= dropper
:get_look_dir()
620 local p
= {x
=pos
.x
, y
=pos
.y
+1.2, z
=pos
.z
}
621 p
.x
= p
.x
+(math
.random(1,3)*0.2)
622 p
.z
= p
.z
+(math
.random(1,3)*0.2)
623 local obj
= minetest
.add_item(p
, itemstack
)
629 obj
:get_luaentity()._insta_collect
= false
633 -- Fallback for unexpected cases
634 minetest
.add_item(pos
, itemstack
)
639 local return_fields
= function(player
)
640 local inv_t
= minetest
.get_inventory({type="detached", name
= "mobs_mc:trade"})
641 local inv_p
= player
:get_inventory()
642 for i
=1, inv_t
:get_size("input") do
643 local stack
= inv_t
:get_stack("input", i
)
644 return_item(stack
, player
, player
:get_pos(), inv_p
)
646 inv_t
:set_stack("input", i
, stack
)
648 inv_t
:set_stack("output", 1, "")
651 minetest
.register_on_player_receive_fields(function(player
, formname
, fields
)
652 if formname
== "mobs_mc:trade" then
653 local name
= player
:get_player_name()
655 return_fields(player
)
656 player_trading_with
[name
] = nil
657 elseif fields
.next_trade
then
658 local trader
= player_trading_with
[name
]
659 if not trader
or not trader
.object
:get_luaentity() then
662 player_tradenum
[name
] = player_tradenum
[name
] + 1
663 local inv
= minetest
.get_inventory({type="detached", name
="mobs_mc:trade"})
664 set_trade(trader
, player
, inv
, player_tradenum
[name
])
665 update_offer(inv
, player
, false)
666 show_trade_formspec(name
, trader
)
667 elseif fields
.prev_trade
then
668 local trader
= player_trading_with
[name
]
669 if not trader
or not trader
.object
:get_luaentity() then
672 player_tradenum
[name
] = player_tradenum
[name
] - 1
673 local inv
= minetest
.get_inventory({type="detached", name
="mobs_mc:trade"})
674 set_trade(trader
, player
, inv
, player_tradenum
[name
])
675 update_offer(inv
, player
, false)
676 show_trade_formspec(name
, trader
)
681 minetest
.register_on_leaveplayer(function(player
)
682 return_fields(player
)
683 player_tradenum
[player
:get_player_name()] = nil
684 player_trading_with
[player
:get_player_name()] = nil
687 minetest
.register_on_joinplayer(function(player
)
688 player_tradenum
[player
:get_player_name()] = 1
689 player_trading_with
[player
:get_player_name()] = nil
692 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
)
695 mobs
:alias_mob("mobs:villager", "mobs_mc:villager")
698 mobs
:register_egg("mobs_mc:villager", S("Villager"), "mobs_mc_spawn_icon_villager.png", 0)
700 if minetest
.settings
:get_bool("log_mods") then
701 minetest
.log("action", "MC mobs loaded")