3 --made for MC like Survival game
4 --License for code WTFPL and otherwise stated in readmes
6 -- TODO: Per-player trading inventories
9 -- FIXME: Placing output on exiting item in player inventory destroys item
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
= {}
21 --################### VILLAGER
24 -- LIST OF VILLAGER PROFESSIONS AND TRADES
25 local E1
= { "mcl_core:emerald", 1, 1 } -- one emerald
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 } },
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
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 } },
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 } },
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 } },
140 name
= "Cartographer",
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
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",
190 { { "mcl_mobitems:leather", 9, 12 }, E1
},
191 { { "mcl_core:emerald", 2, 4 }, { "3d_armor:leggings_leather", 2, 4 } },
196 { { "mcl_core:emerald", 7, 12 }, { "3d_armor:chestplate_leather", 1, 1 } },
200 { { "mcl_core:emerald", 8, 10 }, { "mcl_mobitems:saddle", 1, 1 } },
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 } },
220 name
= "Weapon Smith",
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
},
230 { { "mcl_core:emerald", 9, 10 }, { "mcl_tools:sword_iron", 1, 1 } },
234 { { "mcl_core:diamond", 3, 4 }, E1
},
236 { { "mcl_core:emerald", 12, 15 }, { "mcl_tools:sword_diamond", 1, 1 } },
238 { { "mcl_core:emerald", 9, 12 }, { "mcl_tools:axe_diamond", 1, 1 } },
246 { { "mcl_core:coal_lump", 16, 24 }, E1
},
248 { { "mcl_core:emerald", 5, 7 }, { "mcl_tools:shovel_iron", 1, 1 } },
252 { { "mcl_core:iron_ingot", 7, 9 }, E1
},
254 { { "mcl_core:emerald", 9, 11 }, { "mcl_tools:pick_iron", 1, 1 } },
258 { { "mcl_core:diamond", 3, 4 }, E1
},
260 { { "mcl_core:emerald", 12, 15 }, { "mcl_tools:pick_diamond", 1, 1 } },
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
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
311 local max_tier
= math
.min(#trade_tiers
, self
._max_trade_tier
)
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
}
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
, {
331 offered
= offered_item
.. " " .. offered_count
,
336 self
._trades
= minetest
.serialize(trades
)
339 local set_trade
= function(self
, player
, inv
, concrete_tradenum
)
340 local trades
= minetest
.deserialize(self
._trades
)
343 trades
= minetest
.deserialize(self
._trades
)
345 minetest
.log("error", "[mobs_mc] Failed to select villager trade!")
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
)
364 inv
:set_stack("wanted", 2, "")
369 local function show_trade_formspec(playername
, trader
)
370 local profession
= professions
[trader
._profession
].name
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))
399 minetest
.sound_play("mobs_mc_villager_accept", {to_player
= player
:get_player_name()})
403 inv
:set_stack("output", 1, ItemStack(""))
405 minetest
.sound_play("mobs_mc_villager_deny", {to_player
= player
:get_player_name()})
411 mobs
:register_mob("mobs_mc:villager", {
415 collisionbox
= {-0.3, -0.01, -0.3, 0.3, 1.94, 0.3},
417 mesh
= "mobs_mc_villager.b3d",
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,
450 random = "mobs_mc_villager_noise",
451 death
= "mobs_mc_villager_death",
452 damage
= "mobs_mc_villager_damage",
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"})
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
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
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
510 allow_put
= function(inv
, listname
, index
, stack
, player
)
511 if listname
== "input" then
512 return stack
:get_count()
517 on_put
= function(inv
, listname
, index
, stack
, player
)
518 update_offer(inv
, player
, true)
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)
531 on_take
= function(inv
, listname
, index
, stack
, player
)
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))
542 minetest
.sound_play("mobs_mc_villager_accept", {to_player
= player
:get_player_name()})
544 minetest
.sound_play("mobs_mc_villager_deny", {to_player
= player
:get_player_name()})
546 update_offer(inv
, player
, false)
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
559 player_tradenum
[name
] = 1
560 set_trade(self
, player
, inv
, player_tradenum
[name
])
562 show_trade_formspec(name
, self
)
565 on_spawn
= function(self
)
566 init_profession(self
)
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
)
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
)
588 obj
:get_luaentity()._insta_collect
= false
592 -- Fallback for unexpected cases
593 minetest
.add_item(pos
, 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
)
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()
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
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
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
)
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
646 minetest
.register_on_joinplayer(function(player
)
647 player_tradenum
[player
:get_player_name()] = 1
648 player_trading_with
[player
:get_player_name()] = nil
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
)
654 mobs
:alias_mob("mobs:villager", "mobs_mc:villager")
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")