Add warnings and notes about trading implementation
[MineClone/MineClone2.git] / mods / ENTITIES / mobs_mc / villager.lua
blob364e342ee8cdbfc9a32ae195822d779e503fdc0a
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: Weird behaviour when taking single item from output stack
10 -- FIXME: Placing output on exiting item in player inventory destroys item
12 -- intllib
13 local MP = minetest.get_modpath(minetest.get_current_modname())
14 local S, NS = dofile(MP.."/intllib.lua")
16 -- playername-indexed table containing the previously used tradenum
17 local player_tradenum = {}
18 -- playername-indexed table containing the objectref of trader, if trading formspec is open
19 local player_trading_with = {}
21 --###################
22 --################### VILLAGER
23 --###################
25 -- LIST OF VILLAGER PROFESSIONS AND TRADES
26 local E1 = { "mcl_core:emerald", 1, 1 } -- one emerald
27 local professions = {
28 farmer = {
29 id = "farmer",
30 name = "Farmer",
31 trades = {
33 { { "mcl_farming:wheat_item", 18, 22, }, E1 },
34 { { "mcl_farming:potato_item", 15, 15, }, E1 },
35 { { "mcl_farming:carrot_item", 15, 19, }, E1 },
36 { E1, { "mcl_farming:bread", 2, 4 } },
40 { { "mcl_farming:pumpkin_face", 8, 13 }, E1 },
41 { E1, { "mcl_farming:pumpkin_pie", 2, 3} },
45 { { "mcl_farming:melon", 7, 12 }, E1 },
46 { E1, { "mcl_core:apple", 5, 7 }, },
50 { E1, { "mcl_farming:cookie", 6, 10 } },
51 { E1, { "mcl_cake:cake", 1, 1 } },
55 fisherman = {
56 id = "fisherman",
57 name = "Fisherman",
58 trades = {
60 { { "mcl_fishing:fish_raw", 6, 6, "mcl_core:emerald", 1, 1 }, { "mcl_fishing:fish_cooked", 6, 6 } },
61 { { "mcl_mobitems:string", 15, 20 }, E1 },
62 { { "mcl_core:coal_lump", 16, 24 }, E1 },
64 -- TODO: enchanted fishing rod
67 fletcher = {
68 id = "fletcher",
69 name = "Fletcher",
70 trades = {
72 { { "mcl_mobitems:string", 15, 20 }, E1 },
73 { E1, { "mcl_bows:arrow", 8, 12 } },
77 { { "mcl_core:gravel", 10, 10, "mcl_core:emerald", 1, 1 }, { "mcl_core:flint", 6, 10 } },
78 { { "mcl_core:emerald", 2, 3 }, { "mcl_bows:bow", 1, 1 } },
82 shepherd ={
83 id = "shepherd",
84 name = "Shepherd",
85 trades = {
87 { { "mcl_wool:white", 16, 22 }, E1 },
88 { { "mcl_core:emerald", 3, 4 }, { "mcl_tools:shears", 1, 1 } },
92 { { "mcl_core:emerald", 1, 2 }, { "mcl_wool:white", 1, 1 } },
93 { { "mcl_core:emerald", 1, 2 }, { "mcl_wool:grey", 1, 1 } },
94 { { "mcl_core:emerald", 1, 2 }, { "mcl_wool:silver", 1, 1 } },
95 { { "mcl_core:emerald", 1, 2 }, { "mcl_wool:yellow", 1, 1 } },
96 { { "mcl_core:emerald", 1, 2 }, { "mcl_wool:red", 1, 1 } },
97 { { "mcl_core:emerald", 1, 2 }, { "mcl_wool:purple", 1, 1 } },
98 { { "mcl_core:emerald", 1, 2 }, { "mcl_wool:blue", 1, 1 } },
99 { { "mcl_core:emerald", 1, 2 }, { "mcl_wool:light_blue", 1, 1 } },
100 { { "mcl_core:emerald", 1, 2 }, { "mcl_wool:brown", 1, 1 } },
101 { { "mcl_core:emerald", 1, 2 }, { "mcl_wool:lime", 1, 1 } },
102 { { "mcl_core:emerald", 1, 2 }, { "mcl_wool:green", 1, 1 } },
103 { { "mcl_core:emerald", 1, 2 }, { "mcl_wool:magenta", 1, 1 } },
104 { { "mcl_core:emerald", 1, 2 }, { "mcl_wool:black", 1, 1 } },
105 { { "mcl_core:emerald", 1, 2 }, { "mcl_wool:cyan", 1, 1 } },
106 { { "mcl_core:emerald", 1, 2 }, { "mcl_wool:pink", 1, 1 } },
110 librarian = {
111 id = "librarian",
112 name = "Librarian",
113 trades = {
115 { { "mcl_core:paper", 24, 36 }, E1 },
116 -- TODO: enchanted book
117 { { "mcl_books:book", 8, 10 }, E1 },
118 { { "mcl_core:emerald", 10, 12 }, { "mcl_compass:compass", 1 ,1 }},
119 { { "mcl_core:emerald", 3, 4 }, { "mcl_books:bookshelf", 1 ,1 }},
123 { { "mcl_books:written_book", 2, 2 }, E1 },
124 { { "mcl_core:emerald", 10, 12 }, { "mcl_clock:clock", 1, 1 } },
125 { E1, { "mcl_core:glass", 3, 5 } },
129 { E1, { "mcl_core:glass", 3, 5 } },
132 -- TODO: 2 enchanted book tiers
135 { { "mcl_core:emerald", 20, 22 }, { "mcl_mobs:nametag", 1, 1 } },
139 cartographer = {
140 id = "cartographer",
141 name = "Cartographer",
142 trades = {
144 { { "mcl_core:paper", 24, 36 }, E1 },
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
159 armorer = {
160 id = "armorer",
161 name = "Armorer",
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 trades = {
191 { { "mcl_mobitems:leather", 9, 12 }, E1 },
192 { { "mcl_core:emerald", 2, 4 }, { "3d_armor:leggings_leather", 2, 4 } },
196 -- TODO: enchant
197 { { "mcl_core:emerald", 7, 12 }, { "3d_armor:chestplate_leather", 1, 1 } },
201 { { "mcl_core:emerald", 8, 10 }, { "mcl_mobitems:saddle", 1, 1 } },
205 butcher = {
206 name = "Butcher",
207 trades = {
209 { { "mcl_mobitems:beef", 14, 18 }, E1 },
210 { { "mcl_mobitems:chicken", 14, 18 }, E1 },
214 { { "mcl_core:coal_lump", 16, 24 }, E1 },
215 { E1, { "mcl_mobitems:cooked_beef", 5, 7 } },
216 { E1, { "mcl_mobitems:cooked_chicken", 6, 8 } },
220 weapon_smith = {
221 name = "Weapon Smith",
222 trades = {
224 { { "mcl_core:coal_lump", 16, 24 }, E1 },
225 { { "mcl_core:emerald", 6, 8 }, { "mcl_tools:axe_iron", 1, 1 } },
229 { { "mcl_core:iron_ingot", 7, 9 }, E1 },
230 -- TODO: enchant
231 { { "mcl_core:emerald", 9, 10 }, { "mcl_tools:sword_iron", 1, 1 } },
235 { { "mcl_core:diamond", 3, 4 }, E1 },
236 -- TODO: enchant
237 { { "mcl_core:emerald", 12, 15 }, { "mcl_tools:sword_diamond", 1, 1 } },
238 -- TODO: enchant
239 { { "mcl_core:emerald", 9, 12 }, { "mcl_tools:axe_diamond", 1, 1 } },
243 tool_smith = {
244 name = "Tool Smith",
245 trades = {
247 { { "mcl_core:coal_lump", 16, 24 }, E1 },
248 -- TODO: enchant
249 { { "mcl_core:emerald", 5, 7 }, { "mcl_tools:shovel_iron", 1, 1 } },
253 { { "mcl_core:iron_ingot", 7, 9 }, E1 },
254 -- TODO: enchant
255 { { "mcl_core:emerald", 9, 11 }, { "mcl_tools:pick_iron", 1, 1 } },
259 { { "mcl_core:diamond", 3, 4 }, E1 },
260 -- TODO: enchant
261 { { "mcl_core:emerald", 12, 15 }, { "mcl_tools:pick_diamond", 1, 1 } },
265 cleric = {
266 name = "Cleric",
267 trades = {
269 { { "mcl_mobitems:rotten_flesh", 36, 40 }, E1 },
270 { { "mcl_core:gold_ingot", 8, 10 }, E1 },
274 { E1, { "mesecons:redstone", 1, 4 } },
275 { E1, { "mcl_dye:blue", 1, 2 } },
279 { E1, { "mcl_nether:glowstone", 1, 3 } },
280 { { "mcl_core:emerald", 4, 7 }, { "mcl_throwing:ender_pearl", 1, 1 } },
283 -- TODO: Bottle 'o enchanting
286 -- TODO: Nitwit
289 local profession_names = {}
290 for id, _ in pairs(professions) do
291 table.insert(profession_names, id)
294 local init_profession = function(self)
295 if not self._profession then
296 local p = math.random(1, #profession_names)
297 self._profession = profession_names[p]
299 if not self._max_trade_tier then
300 -- TODO: Start with tier 1
301 self._max_trade_tier = 10
305 local update_trades = function(self, inv)
306 local profession = professions[self._profession]
307 local trade_tiers = profession.trades
308 if trade_tiers == nil then
309 return
312 local max_tier = math.min(#trade_tiers, self._max_trade_tier)
313 local trades = {}
314 for tiernum=1, max_tier do
315 local tier = trade_tiers[tiernum]
316 for tradenum=1, #tier do
317 local trade = tier[tradenum]
318 local wanted1_item = trade[1][1]
319 local wanted1_count = math.random(trade[1][2], trade[1][3])
320 local offered_item = trade[2][1]
321 local offered_count = math.random(trade[2][2], trade[2][3])
323 local wanted = { wanted1_item .. " " ..wanted1_count }
324 if trade[1][4] then
325 local wanted2_item = trade[1][4]
326 local wanted2_count = math.random(trade[1][5], trade[1][6])
327 table.insert(wanted, wanted2_item .. " " ..wanted2_count)
330 table.insert(trades, {
331 wanted = wanted,
332 offered = offered_item .. " " .. offered_count,
333 tier = tiernum,
337 self._trades = minetest.serialize(trades)
340 local set_trade = function(self, player, inv, concrete_tradenum)
341 local trades = minetest.deserialize(self._trades)
342 if not trades then
343 update_trades(self)
344 trades = minetest.deserialize(self._trades)
345 if not trades then
346 minetest.log("error", "[mobs_mc] Failed to select villager trade!")
347 return
351 if concrete_tradenum > #trades then
352 concrete_tradenum = 1
353 player_tradenum[player:get_player_name()] = concrete_tradenum
354 elseif concrete_tradenum < 1 then
355 concrete_tradenum = #trades
356 player_tradenum[player:get_player_name()] = concrete_tradenum
358 local trade = trades[concrete_tradenum]
359 inv:set_stack("wanted", 1, ItemStack(trade.wanted[1]))
360 inv:set_stack("offered", 1, ItemStack(trade.offered))
361 if trade.wanted[2] then
362 local wanted2 = ItemStack(trade.wanted[2])
363 inv:set_stack("wanted", 2, wanted2)
364 else
365 inv:set_stack("wanted", 2, "")
370 local function show_trade_formspec(playername, trader)
371 local profession = professions[trader._profession].name
372 local formspec =
373 "size[9,8.75]"..
374 "background[-0.19,-0.25;9.41,9.49;mobs_mc_trading_formspec_bg.png]"..
375 mcl_vars.inventory_header..
376 "label[4,0;"..minetest.formspec_escape(profession).."]"
377 -- FIXME: Remove when trading bugs are fixed
378 .."label[0,0.5;"..minetest.formspec_escape(minetest.colorize("#FF0000", "WARNING! Trading is incomplete and might have bugs!")).."]"
379 .."list[current_player;main;0,4.5;9,3;9]"
380 .."list[current_player;main;0,7.74;9,1;]"
381 .."button[1,1;0.5,1;prev_trade;<]"
382 .."button[7.26,1;0.5,1;next_trade;>]"
383 .."list[detached:mobs_mc:trade;wanted;2,1;2,1;]"
384 .."list[detached:mobs_mc:trade;offered;5.76,1;1,1;]"
385 .."list[detached:mobs_mc:trade;input;2,2.5;2,1;]"
386 .."list[detached:mobs_mc:trade;output;5.76,2.55;1,1;]"
387 .."listring[detached:mobs_mc:trade;output]"
388 .."listring[current_player;main]"
389 .."listring[detached:mobs_mc:trade;input]"
390 .."listring[current_player;main]"
391 minetest.sound_play("mobs_mc_villager_trade", {to_player = playername})
392 minetest.show_formspec(playername, "mobs_mc:trade", formspec)
395 local update_offer = function(inv, player, sound)
396 if inv:contains_item("input", inv:get_stack("wanted", 1)) and
397 (inv:get_stack("wanted", 2):is_empty() or inv:contains_item("input", inv:get_stack("wanted", 2))) then
398 inv:set_stack("output", 1, inv:get_stack("offered", 1))
399 if sound then
400 minetest.sound_play("mobs_mc_villager_accept", {to_player = player:get_player_name()})
402 return true
403 else
404 inv:set_stack("output", 1, ItemStack(""))
405 if sound then
406 minetest.sound_play("mobs_mc_villager_deny", {to_player = player:get_player_name()})
408 return false
412 mobs:register_mob("mobs_mc:villager", {
413 type = "npc",
414 hp_min = 20,
415 hp_max = 20,
416 collisionbox = {-0.3, -0.01, -0.3, 0.3, 1.94, 0.3},
417 visual = "mesh",
418 mesh = "mobs_mc_villager.b3d",
419 textures = {
421 "mobs_mc_villager.png",
422 "mobs_mc_villager.png", --hat
425 "mobs_mc_villager_farmer.png",
426 "mobs_mc_villager_farmer.png", --hat
429 "mobs_mc_villager_priest.png",
430 "mobs_mc_villager_priest.png", --hat
433 "mobs_mc_villager_librarian.png",
434 "mobs_mc_villager_librarian.png", --hat
437 "mobs_mc_villager_butcher.png",
438 "mobs_mc_villager_butcher.png", --hat
441 "mobs_mc_villager_smith.png",
442 "mobs_mc_villager_smith.png", --hat
445 visual_size = {x=3, y=3},
446 makes_footstep_sound = true,
447 walk_velocity = 1.2,
448 run_velocity = 2.4,
449 drops = {},
450 sounds = {
451 random = "mobs_mc_villager_noise",
452 death = "mobs_mc_villager_death",
453 damage = "mobs_mc_villager_damage",
454 distance = 16,
456 animation = {
457 stand_speed = 25,
458 stand_start = 40,
459 stand_end = 59,
460 walk_speed = 25,
461 walk_start = 0,
462 walk_end = 40,
463 run_speed = 25,
464 run_start = 0,
465 run_end = 40,
466 die_speed = 15,
467 die_start = 210,
468 die_end = 220,
469 die_loop = false,
471 water_damage = 0,
472 lava_damage = 4,
473 light_damage = 0,
474 view_range = 16,
475 fear_height = 4,
476 on_rightclick = function(self, clicker)
477 local name = clicker:get_player_name()
479 player_trading_with[name] = self
481 -- TODO: Create per-player trading inventories
482 local inv = minetest.get_inventory({type="detached", name="mobs_mc:trade"})
483 if not inv then
484 inv = minetest.create_detached_inventory("mobs_mc:trade", {
485 allow_take = function(inv, listname, index, stack, player)
486 if listname == "input" or listname == "output" then
487 return stack:get_count()
488 else
489 return 0
491 end,
492 allow_move = function(inv, from_list, from_index, to_list, to_index, count, player)
493 if from_list == "wanted" or from_list == "offered" or to_list == "wanted" or to_list == "offered" then
494 return 0
495 elseif from_list == "output" and inv:get_stack(to_list, to_index):is_empty() then
496 return count
497 elseif from_list == "input" then
498 return count
499 else
500 return 0
502 end,
503 allow_put = function(inv, listname, index, stack, player)
504 if listname == "input" then
505 return stack:get_count()
506 else
507 return 0
509 end,
510 on_put = function(inv, listname, index, stack, player)
511 update_offer(inv, player, true)
512 end,
513 on_move = function(inv, from_list, from_index, to_list, to_index, count, player)
514 update_offer(inv, player, true)
515 end,
516 on_take = function(inv, listname, index, stack, player)
517 local accept
518 if listname == "output" then
519 inv:remove_item("input", inv:get_stack("wanted", 1))
520 local wanted2 = inv:get_stack("wanted", 2)
521 if not wanted2:is_empty() then
522 inv:remove_item("input", inv:get_stack("wanted", 2))
524 accept = true
526 if accept then
527 minetest.sound_play("mobs_mc_villager_accept", {to_player = player:get_player_name()})
528 else
529 minetest.sound_play("mobs_mc_villager_deny", {to_player = player:get_player_name()})
532 update_offer(inv, player, false)
533 end,
536 inv:set_size("input", 2)
537 inv:set_size("output", 1)
538 inv:set_size("wanted", 2)
539 inv:set_size("offered", 1)
541 init_profession(self)
542 if not self._trades then
543 update_trades(self)
545 player_tradenum[name] = 1
546 set_trade(self, player, inv, player_tradenum[name])
548 show_trade_formspec(name, self)
549 end,
551 on_spawn = function(self)
552 init_profession(self)
553 end,
556 -- Returns a single itemstack in the given inventory to the player's main inventory, or drop it when there's no space left
557 local function return_item(itemstack, dropper, pos, inv_p)
558 if dropper:is_player() then
559 -- Return to main inventory
560 if inv_p:room_for_item("main", itemstack) then
561 inv_p:add_item("main", itemstack)
562 else
563 -- Drop item on the ground
564 local v = dropper:get_look_dir()
565 local p = {x=pos.x, y=pos.y+1.2, z=pos.z}
566 p.x = p.x+(math.random(1,3)*0.2)
567 p.z = p.z+(math.random(1,3)*0.2)
568 local obj = minetest.add_item(p, itemstack)
569 if obj then
570 v.x = v.x*4
571 v.y = v.y*4 + 2
572 v.z = v.z*4
573 obj:setvelocity(v)
574 obj:get_luaentity()._insta_collect = false
577 else
578 -- Fallback for unexpected cases
579 minetest.add_item(pos, itemstack)
581 return itemstack
584 local return_fields = function(player)
585 local inv_t = minetest.get_inventory({type="detached", name = "mobs_mc:trade"})
586 local inv_p = player:get_inventory()
587 for i=1, inv_t:get_size("input") do
588 local stack = inv_t:get_stack("input", i)
589 return_item(stack, player, player:get_pos(), inv_p)
590 stack:clear()
591 inv_t:set_stack("input", i, stack)
593 inv_t:set_stack("output", 1, "")
596 minetest.register_on_player_receive_fields(function(player, formname, fields)
597 if formname == "mobs_mc:trade" then
598 local name = player:get_player_name()
599 if fields.quit then
600 return_fields(player)
601 player_trading_with[name] = nil
602 elseif fields.next_trade then
603 local trader = player_trading_with[name]
604 if not trader or not trader.object:get_luaentity() then
605 return
607 player_tradenum[name] = player_tradenum[name] + 1
608 local inv = minetest.get_inventory({type="detached", name="mobs_mc:trade"})
609 set_trade(trader, player, inv, player_tradenum[name])
610 update_offer(inv, player, false)
611 show_trade_formspec(name, trader)
612 elseif fields.prev_trade then
613 local trader = player_trading_with[name]
614 if not trader or not trader.object:get_luaentity() then
615 return
617 player_tradenum[name] = player_tradenum[name] - 1
618 local inv = minetest.get_inventory({type="detached", name="mobs_mc:trade"})
619 set_trade(trader, player, inv, player_tradenum[name])
620 update_offer(inv, player, false)
621 show_trade_formspec(name, trader)
624 end)
626 minetest.register_on_leaveplayer(function(player)
627 return_fields(player)
628 player_tradenum[player:get_player_name()] = nil
629 player_trading_with[player:get_player_name()] = nil
630 end)
632 minetest.register_on_joinplayer(function(player)
633 player_tradenum[player:get_player_name()] = 1
634 player_trading_with[player:get_player_name()] = nil
635 end)
637 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)
639 -- compatibility
640 mobs:alias_mob("mobs:villager", "mobs_mc:villager")
642 -- spawn eggs
643 mobs:register_egg("mobs_mc:villager", S("Villager"), "mobs_mc_spawn_icon_villager.png", 0)
645 if minetest.settings:get_bool("log_mods") then
646 minetest.log("action", "MC mobs loaded")