Villager profession depends on clothes, add nitwit
[MineClone/MineClone2.git] / mods / ENTITIES / mobs_mc / villager.lua
blob4711b96f572e96b0a71354581831187f6adf48b9
1 --MCmobs v0.4
2 --maikerumine
3 --made for MC like Survival game
4 --License for code WTFPL and otherwise stated in readmes
6 -- TODO: Per-player trading inventories
7 -- TODO: Trading tiers
8 -- TODO: Trade locking
10 -- intllib
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 = {}
19 --###################
20 --################### VILLAGER
21 --###################
23 -- LIST OF VILLAGER PROFESSIONS AND TRADES
24 local E1 = { "mcl_core:emerald", 1, 1 } -- one emerald
25 local professions = {
26 farmer = {
27 name = "Farmer",
28 texture = "mobs_mc_villager_farmer.png",
29 trades = {
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 } },
53 fisherman = {
54 name = "Fisherman",
55 texture = "mobs_mc_villager_farmer.png",
56 trades = {
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
65 fletcher = {
66 name = "Fletcher",
67 texture = "mobs_mc_villager_farmer.png",
68 trades = {
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 } },
80 shepherd ={
81 name = "Shepherd",
82 texture = "mobs_mc_villager_farmer.png",
83 trades = {
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 } },
108 librarian = {
109 name = "Librarian",
110 texture = "mobs_mc_villager_librarian.png",
111 trades = {
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 } },
137 cartographer = {
138 name = "Cartographer",
139 texture = "mobs_mc_villager_librarian.png",
140 trades = {
142 { { "mcl_core:paper", 24, 36 }, E1 },
145 -- {
146 -- TODO: compass
147 -- the difficulty lies in supporting the compass group, not the concrete item
148 -- { { "mcl_compass:compass", 1, 1 }, E1 },
149 -- },
152 -- TODO: replace with empty map
153 { { "mcl_core:emerald", 7, 11}, { "mcl_maps:filled_map", 1, 1 } },
156 -- TODO: special maps
159 armorer = {
160 name = "Armorer",
161 texture = "mobs_mc_villager_smith.png",
162 trades = {
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 },
175 -- TODO: enchant
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 } },
187 leatherworker = {
188 name = "Leatherworker",
189 texture = "mobs_mc_villager_butcher.png",
190 trades = {
192 { { "mcl_mobitems:leather", 9, 12 }, E1 },
193 { { "mcl_core:emerald", 2, 4 }, { "3d_armor:leggings_leather", 2, 4 } },
197 -- TODO: enchant
198 { { "mcl_core:emerald", 7, 12 }, { "3d_armor:chestplate_leather", 1, 1 } },
202 { { "mcl_core:emerald", 8, 10 }, { "mcl_mobitems:saddle", 1, 1 } },
206 butcher = {
207 name = "Butcher",
208 texture = "mobs_mc_villager_butcher.png",
209 trades = {
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 } },
222 weapon_smith = {
223 name = "Weapon Smith",
224 texture = "mobs_mc_villager_smith.png",
225 trades = {
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 },
233 -- TODO: enchant
234 { { "mcl_core:emerald", 9, 10 }, { "mcl_tools:sword_iron", 1, 1 } },
238 { { "mcl_core:diamond", 3, 4 }, E1 },
239 -- TODO: enchant
240 { { "mcl_core:emerald", 12, 15 }, { "mcl_tools:sword_diamond", 1, 1 } },
241 -- TODO: enchant
242 { { "mcl_core:emerald", 9, 12 }, { "mcl_tools:axe_diamond", 1, 1 } },
246 tool_smith = {
247 name = "Tool Smith",
248 texture = "mobs_mc_villager_smith.png",
249 trades = {
251 { { "mcl_core:coal_lump", 16, 24 }, E1 },
252 -- TODO: enchant
253 { { "mcl_core:emerald", 5, 7 }, { "mcl_tools:shovel_iron", 1, 1 } },
257 { { "mcl_core:iron_ingot", 7, 9 }, E1 },
258 -- TODO: enchant
259 { { "mcl_core:emerald", 9, 11 }, { "mcl_tools:pick_iron", 1, 1 } },
263 { { "mcl_core:diamond", 3, 4 }, E1 },
264 -- TODO: enchant
265 { { "mcl_core:emerald", 12, 15 }, { "mcl_tools:pick_diamond", 1, 1 } },
269 cleric = {
270 name = "Cleric",
271 texture = "mobs_mc_villager_priest.png",
272 trades = {
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
291 nitwit = {
292 name = "Nitwit",
293 texture = "mobs_mc_villager.png",
294 -- No trades for nitwit
295 trades = nil,
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]
308 local matches = {}
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
327 -- Empty trades
328 self._trades = false
329 return
332 local max_tier = math.min(#trade_tiers, self._max_trade_tier)
333 local trades = {}
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 }
344 if trade[1][4] then
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, {
351 wanted = wanted,
352 offered = offered_item .. " " .. offered_count,
353 tier = tiernum,
357 self._trades = minetest.serialize(trades)
360 local set_trade = function(self, player, inv, concrete_tradenum)
361 local trades = minetest.deserialize(self._trades)
362 if not trades then
363 update_trades(self)
364 trades = minetest.deserialize(self._trades)
365 if not trades then
366 minetest.log("error", "[mobs_mc] Failed to select villager trade!")
367 return
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)
384 else
385 inv:set_stack("wanted", 2, "")
390 local function show_trade_formspec(playername, trader)
391 local profession = professions[trader._profession].name
392 local formspec =
393 "size[9,8.75]"..
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))
417 if sound then
418 minetest.sound_play("mobs_mc_villager_accept", {to_player = player:get_player_name()})
420 return true
421 else
422 inv:set_stack("output", 1, ItemStack(""))
423 if sound then
424 minetest.sound_play("mobs_mc_villager_deny", {to_player = player:get_player_name()})
426 return false
430 mobs:register_mob("mobs_mc:villager", {
431 type = "npc",
432 hp_min = 20,
433 hp_max = 20,
434 collisionbox = {-0.3, -0.01, -0.3, 0.3, 1.94, 0.3},
435 visual = "mesh",
436 mesh = "mobs_mc_villager.b3d",
437 textures = {
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,
465 walk_velocity = 1.2,
466 run_velocity = 2.4,
467 drops = {},
468 sounds = {
469 random = "mobs_mc_villager_noise",
470 death = "mobs_mc_villager_death",
471 damage = "mobs_mc_villager_damage",
472 distance = 16,
474 animation = {
475 stand_speed = 25,
476 stand_start = 40,
477 stand_end = 59,
478 walk_speed = 25,
479 walk_start = 0,
480 walk_end = 40,
481 run_speed = 25,
482 run_start = 0,
483 run_end = 40,
484 die_speed = 15,
485 die_start = 210,
486 die_end = 220,
487 die_loop = false,
489 water_damage = 0,
490 lava_damage = 4,
491 light_damage = 0,
492 view_range = 16,
493 fear_height = 4,
494 on_rightclick = function(self, clicker)
495 local name = clicker:get_player_name()
497 init_profession(self)
498 if self._trades == nil then
499 update_trades(self)
501 if self._trades == false then
502 -- Villager has no trades, rightclick is a no-op
503 return
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"})
510 if not inv then
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
529 return -1
530 else
531 -- If less than double the wanted items,
532 -- remove items from output (final trade,
533 -- input runs empty)
534 return count
536 else
537 return 0
539 else
540 return 0
542 end,
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
545 return count
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
549 return count
552 return 0
553 end,
554 allow_put = function(inv, listname, index, stack, player)
555 if listname == "input" then
556 return stack:get_count()
557 else
558 return 0
560 end,
561 on_put = function(inv, listname, index, stack, player)
562 update_offer(inv, player, true)
563 end,
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)
574 end,
575 on_take = function(inv, listname, index, stack, player)
576 local accept
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))
583 accept = true
584 elseif listname == "input" then
585 update_offer(inv, player, false)
587 if accept then
588 minetest.sound_play("mobs_mc_villager_accept", {to_player = player:get_player_name()})
589 else
590 minetest.sound_play("mobs_mc_villager_deny", {to_player = player:get_player_name()})
592 end,
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)
604 end,
606 on_spawn = function(self)
607 init_profession(self)
608 end,
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)
617 else
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)
624 if obj then
625 v.x = v.x*4
626 v.y = v.y*4 + 2
627 v.z = v.z*4
628 obj:setvelocity(v)
629 obj:get_luaentity()._insta_collect = false
632 else
633 -- Fallback for unexpected cases
634 minetest.add_item(pos, itemstack)
636 return 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)
645 stack:clear()
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()
654 if fields.quit then
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
660 return
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
670 return
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)
679 end)
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
685 end)
687 minetest.register_on_joinplayer(function(player)
688 player_tradenum[player:get_player_name()] = 1
689 player_trading_with[player:get_player_name()] = nil
690 end)
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)
694 -- compatibility
695 mobs:alias_mob("mobs:villager", "mobs_mc:villager")
697 -- spawn eggs
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")