Villager: Prevent taking partial track from output
[MineClone/MineClone2.git] / mods / ENTITIES / mobs_mc / villager.lua
blob9c67f36f68075df2abdc65257069e2628876c5af
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
9 -- FIXME: Placing output on exiting item in player inventory destroys item
11 -- intllib
12 local MP = minetest.get_modpath(minetest.get_current_modname())
13 local S, NS = dofile(MP.."/intllib.lua")
15 -- playername-indexed table containing the previously used tradenum
16 local player_tradenum = {}
17 -- playername-indexed table containing the objectref of trader, if trading formspec is open
18 local player_trading_with = {}
20 --###################
21 --################### VILLAGER
22 --###################
24 -- LIST OF VILLAGER PROFESSIONS AND TRADES
25 local E1 = { "mcl_core:emerald", 1, 1 } -- one emerald
26 local professions = {
27 farmer = {
28 id = "farmer",
29 name = "Farmer",
30 trades = {
32 { { "mcl_farming:wheat_item", 18, 22, }, E1 },
33 { { "mcl_farming:potato_item", 15, 15, }, E1 },
34 { { "mcl_farming:carrot_item", 15, 19, }, E1 },
35 { E1, { "mcl_farming:bread", 2, 4 } },
39 { { "mcl_farming:pumpkin_face", 8, 13 }, E1 },
40 { E1, { "mcl_farming:pumpkin_pie", 2, 3} },
44 { { "mcl_farming:melon", 7, 12 }, E1 },
45 { E1, { "mcl_core:apple", 5, 7 }, },
49 { E1, { "mcl_farming:cookie", 6, 10 } },
50 { E1, { "mcl_cake:cake", 1, 1 } },
54 fisherman = {
55 id = "fisherman",
56 name = "Fisherman",
57 trades = {
59 { { "mcl_fishing:fish_raw", 6, 6, "mcl_core:emerald", 1, 1 }, { "mcl_fishing:fish_cooked", 6, 6 } },
60 { { "mcl_mobitems:string", 15, 20 }, E1 },
61 { { "mcl_core:coal_lump", 16, 24 }, E1 },
63 -- TODO: enchanted fishing rod
66 fletcher = {
67 id = "fletcher",
68 name = "Fletcher",
69 trades = {
71 { { "mcl_mobitems:string", 15, 20 }, E1 },
72 { E1, { "mcl_bows:arrow", 8, 12 } },
76 { { "mcl_core:gravel", 10, 10, "mcl_core:emerald", 1, 1 }, { "mcl_core:flint", 6, 10 } },
77 { { "mcl_core:emerald", 2, 3 }, { "mcl_bows:bow", 1, 1 } },
81 shepherd ={
82 id = "shepherd",
83 name = "Shepherd",
84 trades = {
86 { { "mcl_wool:white", 16, 22 }, E1 },
87 { { "mcl_core:emerald", 3, 4 }, { "mcl_tools:shears", 1, 1 } },
91 { { "mcl_core:emerald", 1, 2 }, { "mcl_wool:white", 1, 1 } },
92 { { "mcl_core:emerald", 1, 2 }, { "mcl_wool:grey", 1, 1 } },
93 { { "mcl_core:emerald", 1, 2 }, { "mcl_wool:silver", 1, 1 } },
94 { { "mcl_core:emerald", 1, 2 }, { "mcl_wool:yellow", 1, 1 } },
95 { { "mcl_core:emerald", 1, 2 }, { "mcl_wool:red", 1, 1 } },
96 { { "mcl_core:emerald", 1, 2 }, { "mcl_wool:purple", 1, 1 } },
97 { { "mcl_core:emerald", 1, 2 }, { "mcl_wool:blue", 1, 1 } },
98 { { "mcl_core:emerald", 1, 2 }, { "mcl_wool:light_blue", 1, 1 } },
99 { { "mcl_core:emerald", 1, 2 }, { "mcl_wool:brown", 1, 1 } },
100 { { "mcl_core:emerald", 1, 2 }, { "mcl_wool:lime", 1, 1 } },
101 { { "mcl_core:emerald", 1, 2 }, { "mcl_wool:green", 1, 1 } },
102 { { "mcl_core:emerald", 1, 2 }, { "mcl_wool:magenta", 1, 1 } },
103 { { "mcl_core:emerald", 1, 2 }, { "mcl_wool:black", 1, 1 } },
104 { { "mcl_core:emerald", 1, 2 }, { "mcl_wool:cyan", 1, 1 } },
105 { { "mcl_core:emerald", 1, 2 }, { "mcl_wool:pink", 1, 1 } },
109 librarian = {
110 id = "librarian",
111 name = "Librarian",
112 trades = {
114 { { "mcl_core:paper", 24, 36 }, E1 },
115 -- TODO: enchanted book
116 { { "mcl_books:book", 8, 10 }, E1 },
117 { { "mcl_core:emerald", 10, 12 }, { "mcl_compass:compass", 1 ,1 }},
118 { { "mcl_core:emerald", 3, 4 }, { "mcl_books:bookshelf", 1 ,1 }},
122 { { "mcl_books:written_book", 2, 2 }, E1 },
123 { { "mcl_core:emerald", 10, 12 }, { "mcl_clock:clock", 1, 1 } },
124 { E1, { "mcl_core:glass", 3, 5 } },
128 { E1, { "mcl_core:glass", 3, 5 } },
131 -- TODO: 2 enchanted book tiers
134 { { "mcl_core:emerald", 20, 22 }, { "mcl_mobs:nametag", 1, 1 } },
138 cartographer = {
139 id = "cartographer",
140 name = "Cartographer",
141 trades = {
143 { { "mcl_core:paper", 24, 36 }, E1 },
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
158 armorer = {
159 id = "armorer",
160 name = "Armorer",
161 trades = {
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 },
174 -- TODO: enchant
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 } },
186 leatherworker = {
187 name = "Leatherworker",
188 trades = {
190 { { "mcl_mobitems:leather", 9, 12 }, E1 },
191 { { "mcl_core:emerald", 2, 4 }, { "3d_armor:leggings_leather", 2, 4 } },
195 -- TODO: enchant
196 { { "mcl_core:emerald", 7, 12 }, { "3d_armor:chestplate_leather", 1, 1 } },
200 { { "mcl_core:emerald", 8, 10 }, { "mcl_mobitems:saddle", 1, 1 } },
204 butcher = {
205 name = "Butcher",
206 trades = {
208 { { "mcl_mobitems:beef", 14, 18 }, E1 },
209 { { "mcl_mobitems:chicken", 14, 18 }, E1 },
213 { { "mcl_core:coal_lump", 16, 24 }, E1 },
214 { E1, { "mcl_mobitems:cooked_beef", 5, 7 } },
215 { E1, { "mcl_mobitems:cooked_chicken", 6, 8 } },
219 weapon_smith = {
220 name = "Weapon Smith",
221 trades = {
223 { { "mcl_core:coal_lump", 16, 24 }, E1 },
224 { { "mcl_core:emerald", 6, 8 }, { "mcl_tools:axe_iron", 1, 1 } },
228 { { "mcl_core:iron_ingot", 7, 9 }, E1 },
229 -- TODO: enchant
230 { { "mcl_core:emerald", 9, 10 }, { "mcl_tools:sword_iron", 1, 1 } },
234 { { "mcl_core:diamond", 3, 4 }, E1 },
235 -- TODO: enchant
236 { { "mcl_core:emerald", 12, 15 }, { "mcl_tools:sword_diamond", 1, 1 } },
237 -- TODO: enchant
238 { { "mcl_core:emerald", 9, 12 }, { "mcl_tools:axe_diamond", 1, 1 } },
242 tool_smith = {
243 name = "Tool Smith",
244 trades = {
246 { { "mcl_core:coal_lump", 16, 24 }, E1 },
247 -- TODO: enchant
248 { { "mcl_core:emerald", 5, 7 }, { "mcl_tools:shovel_iron", 1, 1 } },
252 { { "mcl_core:iron_ingot", 7, 9 }, E1 },
253 -- TODO: enchant
254 { { "mcl_core:emerald", 9, 11 }, { "mcl_tools:pick_iron", 1, 1 } },
258 { { "mcl_core:diamond", 3, 4 }, E1 },
259 -- TODO: enchant
260 { { "mcl_core:emerald", 12, 15 }, { "mcl_tools:pick_diamond", 1, 1 } },
264 cleric = {
265 name = "Cleric",
266 trades = {
268 { { "mcl_mobitems:rotten_flesh", 36, 40 }, E1 },
269 { { "mcl_core:gold_ingot", 8, 10 }, E1 },
273 { E1, { "mesecons:redstone", 1, 4 } },
274 { E1, { "mcl_dye:blue", 1, 2 } },
278 { E1, { "mcl_nether:glowstone", 1, 3 } },
279 { { "mcl_core:emerald", 4, 7 }, { "mcl_throwing:ender_pearl", 1, 1 } },
282 -- TODO: Bottle 'o enchanting
285 -- TODO: Nitwit
288 local profession_names = {}
289 for id, _ in pairs(professions) do
290 table.insert(profession_names, id)
293 local init_profession = function(self)
294 if not self._profession then
295 local p = math.random(1, #profession_names)
296 self._profession = profession_names[p]
298 if not self._max_trade_tier then
299 -- TODO: Start with tier 1
300 self._max_trade_tier = 10
304 local update_trades = function(self, inv)
305 local profession = professions[self._profession]
306 local trade_tiers = profession.trades
307 if trade_tiers == nil then
308 return
311 local max_tier = math.min(#trade_tiers, self._max_trade_tier)
312 local trades = {}
313 for tiernum=1, max_tier do
314 local tier = trade_tiers[tiernum]
315 for tradenum=1, #tier do
316 local trade = tier[tradenum]
317 local wanted1_item = trade[1][1]
318 local wanted1_count = math.random(trade[1][2], trade[1][3])
319 local offered_item = trade[2][1]
320 local offered_count = math.random(trade[2][2], trade[2][3])
322 local wanted = { wanted1_item .. " " ..wanted1_count }
323 if trade[1][4] then
324 local wanted2_item = trade[1][4]
325 local wanted2_count = math.random(trade[1][5], trade[1][6])
326 table.insert(wanted, wanted2_item .. " " ..wanted2_count)
329 table.insert(trades, {
330 wanted = wanted,
331 offered = offered_item .. " " .. offered_count,
332 tier = tiernum,
336 self._trades = minetest.serialize(trades)
339 local set_trade = function(self, player, inv, concrete_tradenum)
340 local trades = minetest.deserialize(self._trades)
341 if not trades then
342 update_trades(self)
343 trades = minetest.deserialize(self._trades)
344 if not trades then
345 minetest.log("error", "[mobs_mc] Failed to select villager trade!")
346 return
350 if concrete_tradenum > #trades then
351 concrete_tradenum = 1
352 player_tradenum[player:get_player_name()] = concrete_tradenum
353 elseif concrete_tradenum < 1 then
354 concrete_tradenum = #trades
355 player_tradenum[player:get_player_name()] = concrete_tradenum
357 local trade = trades[concrete_tradenum]
358 inv:set_stack("wanted", 1, ItemStack(trade.wanted[1]))
359 inv:set_stack("offered", 1, ItemStack(trade.offered))
360 if trade.wanted[2] then
361 local wanted2 = ItemStack(trade.wanted[2])
362 inv:set_stack("wanted", 2, wanted2)
363 else
364 inv:set_stack("wanted", 2, "")
369 local function show_trade_formspec(playername, trader)
370 local profession = professions[trader._profession].name
371 local formspec =
372 "size[9,8.75]"..
373 "background[-0.19,-0.25;9.41,9.49;mobs_mc_trading_formspec_bg.png]"..
374 mcl_vars.inventory_header..
375 "label[4,0;"..minetest.formspec_escape(profession).."]"
376 -- FIXME: Remove when trading bugs are fixed
377 .."label[0,0.5;"..minetest.formspec_escape(minetest.colorize("#FF3333", "WARNING! Trading is incomplete and has bugs!")).."]"
378 .."list[current_player;main;0,4.5;9,3;9]"
379 .."list[current_player;main;0,7.74;9,1;]"
380 .."button[1,1;0.5,1;prev_trade;<]"
381 .."button[7.26,1;0.5,1;next_trade;>]"
382 .."list[detached:mobs_mc:trade;wanted;2,1;2,1;]"
383 .."list[detached:mobs_mc:trade;offered;5.76,1;1,1;]"
384 .."list[detached:mobs_mc:trade;input;2,2.5;2,1;]"
385 .."list[detached:mobs_mc:trade;output;5.76,2.55;1,1;]"
386 .."listring[detached:mobs_mc:trade;output]"
387 .."listring[current_player;main]"
388 .."listring[detached:mobs_mc:trade;input]"
389 .."listring[current_player;main]"
390 minetest.sound_play("mobs_mc_villager_trade", {to_player = playername})
391 minetest.show_formspec(playername, "mobs_mc:trade", formspec)
394 local update_offer = function(inv, player, sound)
395 if inv:contains_item("input", inv:get_stack("wanted", 1)) and
396 (inv:get_stack("wanted", 2):is_empty() or inv:contains_item("input", inv:get_stack("wanted", 2))) then
397 inv:set_stack("output", 1, inv:get_stack("offered", 1))
398 if sound then
399 minetest.sound_play("mobs_mc_villager_accept", {to_player = player:get_player_name()})
401 return true
402 else
403 inv:set_stack("output", 1, ItemStack(""))
404 if sound then
405 minetest.sound_play("mobs_mc_villager_deny", {to_player = player:get_player_name()})
407 return false
411 mobs:register_mob("mobs_mc:villager", {
412 type = "npc",
413 hp_min = 20,
414 hp_max = 20,
415 collisionbox = {-0.3, -0.01, -0.3, 0.3, 1.94, 0.3},
416 visual = "mesh",
417 mesh = "mobs_mc_villager.b3d",
418 textures = {
420 "mobs_mc_villager.png",
421 "mobs_mc_villager.png", --hat
424 "mobs_mc_villager_farmer.png",
425 "mobs_mc_villager_farmer.png", --hat
428 "mobs_mc_villager_priest.png",
429 "mobs_mc_villager_priest.png", --hat
432 "mobs_mc_villager_librarian.png",
433 "mobs_mc_villager_librarian.png", --hat
436 "mobs_mc_villager_butcher.png",
437 "mobs_mc_villager_butcher.png", --hat
440 "mobs_mc_villager_smith.png",
441 "mobs_mc_villager_smith.png", --hat
444 visual_size = {x=3, y=3},
445 makes_footstep_sound = true,
446 walk_velocity = 1.2,
447 run_velocity = 2.4,
448 drops = {},
449 sounds = {
450 random = "mobs_mc_villager_noise",
451 death = "mobs_mc_villager_death",
452 damage = "mobs_mc_villager_damage",
453 distance = 16,
455 animation = {
456 stand_speed = 25,
457 stand_start = 40,
458 stand_end = 59,
459 walk_speed = 25,
460 walk_start = 0,
461 walk_end = 40,
462 run_speed = 25,
463 run_start = 0,
464 run_end = 40,
465 die_speed = 15,
466 die_start = 210,
467 die_end = 220,
468 die_loop = false,
470 water_damage = 0,
471 lava_damage = 4,
472 light_damage = 0,
473 view_range = 16,
474 fear_height = 4,
475 on_rightclick = function(self, clicker)
476 local name = clicker:get_player_name()
478 player_trading_with[name] = self
480 -- TODO: Create per-player trading inventories
481 local inv = minetest.get_inventory({type="detached", name="mobs_mc:trade"})
482 if not inv then
483 inv = minetest.create_detached_inventory("mobs_mc:trade", {
484 allow_take = function(inv, listname, index, stack, player)
485 if listname == "input" then
486 return stack:get_count()
487 elseif listname == "output" then
488 -- Only allow taking full stack
489 local count = stack:get_count()
490 if count == inv:get_stack(listname, index):get_count() then
491 return count
492 else
493 return 0
495 else
496 return 0
498 end,
499 allow_move = function(inv, from_list, from_index, to_list, to_index, count, player)
500 if from_list == "input" and to_list == "input" then
501 return count
502 elseif from_list == "output" and to_list == "input" then
503 local move_stack = inv:get_stack(from_list, from_index)
504 if inv:get_stack(to_list, to_index):item_fits(move_stack) then
505 return count
508 return 0
509 end,
510 allow_put = function(inv, listname, index, stack, player)
511 if listname == "input" then
512 return stack:get_count()
513 else
514 return 0
516 end,
517 on_put = function(inv, listname, index, stack, player)
518 update_offer(inv, player, true)
519 end,
520 on_move = function(inv, from_list, from_index, to_list, to_index, count, player)
521 if from_list == "output" and to_list == "input" then
522 inv:remove_item("input", inv:get_stack("wanted", 1))
523 local wanted2 = inv:get_stack("wanted", 2)
524 if not wanted2:is_empty() then
525 inv:remove_item("input", inv:get_stack("wanted", 2))
527 minetest.sound_play("mobs_mc_villager_accept", {to_player = player:get_player_name()})
529 update_offer(inv, player, true)
530 end,
531 on_take = function(inv, listname, index, stack, player)
532 local accept
533 if listname == "output" then
534 inv:remove_item("input", inv:get_stack("wanted", 1))
535 local wanted2 = inv:get_stack("wanted", 2)
536 if not wanted2:is_empty() then
537 inv:remove_item("input", inv:get_stack("wanted", 2))
539 accept = true
541 if accept then
542 minetest.sound_play("mobs_mc_villager_accept", {to_player = player:get_player_name()})
543 else
544 minetest.sound_play("mobs_mc_villager_deny", {to_player = player:get_player_name()})
546 update_offer(inv, player, false)
547 end,
550 inv:set_size("input", 2)
551 inv:set_size("output", 1)
552 inv:set_size("wanted", 2)
553 inv:set_size("offered", 1)
555 init_profession(self)
556 if not self._trades then
557 update_trades(self)
559 player_tradenum[name] = 1
560 set_trade(self, player, inv, player_tradenum[name])
562 show_trade_formspec(name, self)
563 end,
565 on_spawn = function(self)
566 init_profession(self)
567 end,
570 -- Returns a single itemstack in the given inventory to the player's main inventory, or drop it when there's no space left
571 local function return_item(itemstack, dropper, pos, inv_p)
572 if dropper:is_player() then
573 -- Return to main inventory
574 if inv_p:room_for_item("main", itemstack) then
575 inv_p:add_item("main", itemstack)
576 else
577 -- Drop item on the ground
578 local v = dropper:get_look_dir()
579 local p = {x=pos.x, y=pos.y+1.2, z=pos.z}
580 p.x = p.x+(math.random(1,3)*0.2)
581 p.z = p.z+(math.random(1,3)*0.2)
582 local obj = minetest.add_item(p, itemstack)
583 if obj then
584 v.x = v.x*4
585 v.y = v.y*4 + 2
586 v.z = v.z*4
587 obj:setvelocity(v)
588 obj:get_luaentity()._insta_collect = false
591 else
592 -- Fallback for unexpected cases
593 minetest.add_item(pos, itemstack)
595 return itemstack
598 local return_fields = function(player)
599 local inv_t = minetest.get_inventory({type="detached", name = "mobs_mc:trade"})
600 local inv_p = player:get_inventory()
601 for i=1, inv_t:get_size("input") do
602 local stack = inv_t:get_stack("input", i)
603 return_item(stack, player, player:get_pos(), inv_p)
604 stack:clear()
605 inv_t:set_stack("input", i, stack)
607 inv_t:set_stack("output", 1, "")
610 minetest.register_on_player_receive_fields(function(player, formname, fields)
611 if formname == "mobs_mc:trade" then
612 local name = player:get_player_name()
613 if fields.quit then
614 return_fields(player)
615 player_trading_with[name] = nil
616 elseif fields.next_trade then
617 local trader = player_trading_with[name]
618 if not trader or not trader.object:get_luaentity() then
619 return
621 player_tradenum[name] = player_tradenum[name] + 1
622 local inv = minetest.get_inventory({type="detached", name="mobs_mc:trade"})
623 set_trade(trader, player, inv, player_tradenum[name])
624 update_offer(inv, player, false)
625 show_trade_formspec(name, trader)
626 elseif fields.prev_trade then
627 local trader = player_trading_with[name]
628 if not trader or not trader.object:get_luaentity() then
629 return
631 player_tradenum[name] = player_tradenum[name] - 1
632 local inv = minetest.get_inventory({type="detached", name="mobs_mc:trade"})
633 set_trade(trader, player, inv, player_tradenum[name])
634 update_offer(inv, player, false)
635 show_trade_formspec(name, trader)
638 end)
640 minetest.register_on_leaveplayer(function(player)
641 return_fields(player)
642 player_tradenum[player:get_player_name()] = nil
643 player_trading_with[player:get_player_name()] = nil
644 end)
646 minetest.register_on_joinplayer(function(player)
647 player_tradenum[player:get_player_name()] = 1
648 player_trading_with[player:get_player_name()] = nil
649 end)
651 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)
653 -- compatibility
654 mobs:alias_mob("mobs:villager", "mobs_mc:villager")
656 -- spawn eggs
657 mobs:register_egg("mobs_mc:villager", S("Villager"), "mobs_mc_spawn_icon_villager.png", 0)
659 if minetest.settings:get_bool("log_mods") then
660 minetest.log("action", "MC mobs loaded")