mob_basics.lua 35 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894
  1. ------------------------------------------------------------------------------------------------------
  2. -- Provides basic mob functionality:
  3. ------------------------------------------------------------------------------------------------------
  4. -- * handles formspec input of a mob
  5. -- * allows configuration of the mob
  6. -- * adds spawn command
  7. -- * initializes a mob
  8. -- * helper functions (i.e. turn towards player)
  9. -- * data of *active* traders (those that stand around somewhere in the world; excluding those in
  10. -- inventories in players or chests) is stored
  11. ------------------------------------------------------------------------------------------------------
  12. minetest.register_privilege("mob_basics_spawn", { description = "allows to spawn mob_basic based mobs with a chat command (i.e. /trader)", give_to_singleplayer = false});
  13. -- reserve namespace for basic mob operations
  14. mob_basics = {}
  15. -- store information about the diffrent mobs
  16. mob_basics.mob_types = {}
  17. -- if you want to add a new texture, do it here
  18. mob_basics.TEXTURES = {'kuhhaendler.png', 'bauer_in_sonntagskleidung.png', 'baeuerin.png', 'character.png', 'wheat_farmer_by_addi.png', 'tomatenhaendler.png', 'blacksmith.png',
  19. 'holzfaeller.png' };
  20. -- further good looking skins:
  21. --mob_basics.TEXTURES = {'kuhhaendler.png', 'bauer_in_sonntagskleidung.png', 'baeuerin.png', 'character.png', 'wheat_farmer_by_addi.png',
  22. -- "pawel04z.png", "character.png", "skin_2014012302322877138.png",
  23. -- "6265356020613120.png","4535914155999232.png","6046371190669312.png","1280435.png"};
  24. -- TODO: gather textures from installed skin mods?
  25. -- keep a list of all mobs that are handled by this mod
  26. mob_basics.known_mobs = {}
  27. -- this is a list of the indices of mob_basics.known_mobs needed for the /moblist command
  28. mob_basics.mob_list = {};
  29. -----------------------------------------------------------------------------------------------------
  30. -- Logging is important for debugging
  31. -----------------------------------------------------------------------------------------------------
  32. mob_basics.log = function( msg, self, prefix )
  33. if( self==nil ) then
  34. minetest.log("action", '[mob_basics] '..tostring( msg ) );
  35. else
  36. minetest.log("action", '[mob_basics] '..tostring( msg )..
  37. ' id:'..tostring( self[ prefix..'_id'] )..
  38. ' typ:'..tostring( self[ prefix..'_typ'] or '?' )..
  39. ' prefix:'..tostring( prefix or '?' )..
  40. ' at:'..minetest.pos_to_string( self.object:get_pos() )..
  41. ' by:'..tostring( self[ prefix..'_owner'] )..'.');
  42. end
  43. end
  44. -----------------------------------------------------------------------------------------------------
  45. -- Save mob data to a file
  46. -----------------------------------------------------------------------------------------------------
  47. -- TODO: save and restore ought to be library functions and not implemented in each individual mod!
  48. mob_basics.save_data = function()
  49. local data = minetest.serialize( mob_basics.known_mobs );
  50. local path = minetest.get_worldpath().."/mod_mob_basics.data";
  51. local file = io.open( path, "w" );
  52. if( file ) then
  53. file:write( data );
  54. file:close();
  55. else
  56. print("[Mod mob_basics] Error: Savefile '"..tostring( path ).."' could not be written.");
  57. end
  58. end
  59. -----------------------------------------------------------------------------------------------------
  60. -- restore data
  61. -- Note: At first start, there will be a complaint about missing savefile. That message can be ignored.
  62. -----------------------------------------------------------------------------------------------------
  63. mob_basics.restore_data = function()
  64. local path = minetest.get_worldpath().."/mod_mob_basics.data";
  65. local file = io.open( path, "r" );
  66. if( file ) then
  67. local data = file:read("*all");
  68. mob_basics.known_mobs = minetest.deserialize( data );
  69. file:close();
  70. -- this is also a good time to create a list of all mobs which is used for /moblist
  71. mob_basics.mob_list = {};
  72. for k,v in pairs( mob_basics.known_mobs ) do
  73. table.insert( mob_basics.mob_list, k );
  74. end
  75. else
  76. print("[Mod mob_basics] Error: Savefile '"..tostring( path ).."' not found.");
  77. end
  78. end
  79. -- read information about known mob entities from a savefile
  80. -- (do this once at each startup)
  81. mob_basics.restore_data();
  82. -----------------------------------------------------------------------------------------------------
  83. -- A new mob has been added or a mob has been changed (i.e. new trade goods added)
  84. -----------------------------------------------------------------------------------------------------
  85. mob_basics.update = function( self, prefix )
  86. if( not( self )) then
  87. return;
  88. end
  89. -- make sure we save the current position
  90. self[ prefix..'_pos' ] = self.object:get_pos();
  91. local staticdata = self:get_staticdata();
  92. -- deserialize to do some tests
  93. local staticdata_table = minetest.deserialize( staticdata );
  94. if( not( staticdata_table[ prefix..'_name'] )
  95. or not( staticdata_table[ prefix..'_id' ] )
  96. or not( staticdata_table[ prefix..'_typ' ] )) then
  97. return;
  98. end
  99. -- if it is a new mob, register it in the list
  100. if( not( mob_basics.known_mobs[ staticdata_table[ prefix..'_id' ] ] )) then
  101. table.insert( mob_basics.mob_list, staticdata_table[ prefix..'_id' ] );
  102. end
  103. mob_basics.known_mobs[ staticdata_table[ prefix..'_id' ] ] = staticdata_table;
  104. -- actually store the changed data
  105. mob_basics.save_data();
  106. --minetest.chat_send_player('singleplayer','UPDATING MOB '..tostring( self[ prefix..'_id'] ));
  107. end
  108. -----------------------------------------------------------------------------------------------------
  109. -- Data about mobs that where picked up and are stored in the player's inventory is not saved here;
  110. -- Those mobs need to be forgotten.
  111. -----------------------------------------------------------------------------------------------------
  112. mob_basics.forget_mob = function( id )
  113. mob_basics.known_mobs[ id ] = nil;
  114. mob_basics.save_data();
  115. --minetest.chat_send_player('singleplayer','FORGETTING MOB '..tostring( id ));
  116. -- the mob does *not* get deleted out of mob_basics.known_mobs because that would screw up the list
  117. end
  118. -----------------------------------------------------------------------------------------------------
  119. -- return a list of all known mob types which use prefix
  120. -----------------------------------------------------------------------------------------------------
  121. mob_basics.type_list_for_prefix = function( prefix )
  122. local list = {};
  123. if( not( prefix ) or not( mob_basics.mob_types[ prefix ] )) then
  124. return list;
  125. end
  126. for k,v in pairs( mob_basics.mob_types[ prefix ] ) do
  127. table.insert( list, k );
  128. end
  129. return list;
  130. end
  131. -----------------------------------------------------------------------------------------------------
  132. -- return the mobs owned by pname
  133. -----------------------------------------------------------------------------------------------------
  134. -- if prefix is nil, then all mobs owned by the player will be returned
  135. mob_basics.mob_id_list_by_player = function( pname, search_prefix )
  136. local res = {};
  137. if( not( pname )) then
  138. return res;
  139. end
  140. for k,v in ipairs( mob_basics.mob_list ) do
  141. local data = mob_basics.known_mobs[ v ];
  142. if( data ) then
  143. local prefix = data['mob_prefix'];
  144. if( (not( search_prefix ) or search_prefix == prefix)
  145. and data[ prefix..'_owner']
  146. and data[ prefix..'_owner']==pname ) then
  147. table.insert( res, v );
  148. end
  149. end
  150. end
  151. return res;
  152. end
  153. -----------------------------------------------------------------------------------------------------
  154. -- idea taken from npcf
  155. -----------------------------------------------------------------------------------------------------
  156. mob_basics.find_mob_by_id = function( id, prefix )
  157. if( not( id )) then
  158. return;
  159. end
  160. for i, v in pairs( minetest.luaentities ) do
  161. if( v.object and v[ prefix..'_typ'] and v[ prefix..'_id'] and v[ prefix..'_id'] == id ) then
  162. return v;
  163. end
  164. end
  165. end
  166. -----------------------------------------------------------------------------------------------------
  167. -- helper function that lets the mob turn towards a target; taken from npcf
  168. -----------------------------------------------------------------------------------------------------
  169. mob_basics.get_face_direction = function(v1, v2)
  170. if v1 and v2 then
  171. if v1.x and v2.x and v1.z and v2.z then
  172. local dx = v1.x - v2.x
  173. local dz = v2.z - v1.z
  174. return math.atan2(dx, dz)
  175. end
  176. end
  177. end
  178. -----------------------------------------------------------------------------------------------------
  179. -- turn towards the player
  180. -----------------------------------------------------------------------------------------------------
  181. mob_basics.turn_towards_player = function( self, player )
  182. if( self.object and self.object.set_yaw ) then
  183. self.object:set_yaw( mob_basics.get_face_direction( self.object:get_pos(), player:get_pos() ));
  184. end
  185. end
  186. -----------------------------------------------------------------------------------------------------
  187. -- the mobs can vary in height and width
  188. -----------------------------------------------------------------------------------------------------
  189. -- create pseudoradom gaussian distributed numbers
  190. mob_basics.random_number_generator_polar = function()
  191. local u = 0;
  192. local v = 0;
  193. local q = 0;
  194. repeat
  195. u = 2 * math.random() - 1;
  196. v = 2 * math.random() - 1;
  197. q = u * u + v * v
  198. until( (0 < q) and (q < 1));
  199. local p = math.sqrt(-2 * math.log(q) / q) -- math.log returns ln(q)
  200. return {x1 = u * p, x2 = v * p };
  201. end
  202. -----------------------------------------------------------------------------------------------------
  203. -- visual_size needs to be updated whenever changed or the mob is activated
  204. -----------------------------------------------------------------------------------------------------
  205. -- called whenever changed/configured;
  206. -- called from the entity itself in on_activate;
  207. -- standard size is assumed to be 180 cm
  208. mob_basics.update_visual_size = function( self, new_size, generate, prefix )
  209. if( not( new_size ) or not( new_size.x )) then
  210. if( generate ) then
  211. local res = mob_basics.random_number_generator_polar();
  212. local width = 1.0+(res.x1/20.0);
  213. local height = 1.0+(res.x2/10.0);
  214. width = math.floor( width * 100 + 0.5 );
  215. height = math.floor( height * 100 + 0.5 );
  216. new_size = {x=(width/100.0), y=(height/100.0), z=(width/100.0)};
  217. else
  218. new_size = {x=1, y=1, z=1};
  219. end
  220. end
  221. if( not( self[ prefix..'_vsize'] ) or not(self[ prefix..'_vsize'].x)) then
  222. self[ prefix..'_vsize'] = {x=1, y=1, z=1};
  223. end
  224. self[ prefix..'_vsize'].x = new_size.x;
  225. self[ prefix..'_vsize'].y = new_size.y;
  226. self[ prefix..'_vsize'].z = new_size.z;
  227. self.object:set_properties( { visual_size = {x=self[ prefix..'_vsize'].x, y=self[ prefix..'_vsize'].y, z=self[ prefix..'_vsize'].z}});
  228. end
  229. -----------------------------------------------------------------------------------------------------
  230. -- configure a mob using a formspec menu
  231. -----------------------------------------------------------------------------------------------------
  232. mob_basics.config_mob = function( self, player, menu_path, prefix, formname, fields )
  233. local mob_changed = false; -- do we need to store the changes? There might be multiple changes in one go
  234. -- change texture
  235. if( menu_path and #menu_path>3 and menu_path[2]=='config' and menu_path[3]=='texture' ) then
  236. local nr = tonumber( menu_path[4] );
  237. -- actually set the new texture
  238. if( nr and nr > 0 and nr <= #mob_basics.TEXTURES ) then
  239. self[ prefix..'_texture'] = mob_basics.TEXTURES[ nr ];
  240. mob_basics.update_texture( self, prefix, nil );
  241. end
  242. mob_changed = true;
  243. -- change animation (i.e. sit, walk, ...)
  244. elseif( menu_path and #menu_path>3 and menu_path[2]=='config' and menu_path[3]=='anim' ) then
  245. self[ prefix..'_animation'] = menu_path[4];
  246. self.object:set_animation({x=self.animation[ self[ prefix..'_animation']..'_START'], y=self.animation[ self[ prefix..'_animation']..'_END']},
  247. self.animation_speed-5+math.random(10));
  248. mob_changed = true;
  249. end
  250. -- texture and animation are changed via buttons; the other options use input fields
  251. -- prepare variables needed for the size of the mob and the actual formspec
  252. local formspec = 'size[10,8]';
  253. fields['MOBheight'] = tonumber( fields['MOBheight']);
  254. fields['MOBwidth'] = tonumber( fields['MOBwidth']);
  255. if( not( self[ prefix..'_vsize'] ) or not( self[ prefix..'_vsize'].x )) then
  256. self[ prefix..'_vsize'] = {x=1,y=1,z=1};
  257. end
  258. -- rename a mob
  259. if( fields['MOBname'] and fields['MOBname'] ~= "" and fields['MOBname'] ~= self[ prefix..'_name'] ) then
  260. if( string.len(fields['MOBname']) < 2 or string.len(fields['MOBname']) > 40 ) then
  261. minetest.chat_send_player( player:get_player_name(),
  262. "Sorry. This name is not allowed. The name has to be between 2 and 40 letters long.");
  263. elseif( not( fields['MOBname']:match("^[A-Za-z0-9%_%-% ]+$"))) then
  264. minetest.chat_send_player( player:get_player_name(),
  265. "Sorry. The name may only contain letters, numbers, _, - and blanks.");
  266. elseif( minetest.check_player_privs( fields['MOBname'], {shout=true})) then
  267. minetest.chat_send_player( player:get_player_name(),
  268. "Sorry. The name may not be the same as that one of a player.");
  269. else
  270. minetest.chat_send_player( player:get_player_name(),
  271. 'Your mob has been renamed from \"'..tostring( self[ prefix..'_name'] )..'\" to \"'..
  272. fields['MOBname']..'\".');
  273. self[ prefix..'_name'] = fields['MOBname'];
  274. formspec = formspec..'label[3.0,1.5;Renamed successfully.]';
  275. mob_changed = true;
  276. end
  277. -- height has to be at least halfway reasonable
  278. elseif( fields['MOBheight'] and fields['MOBheight']>20 and fields['MOBheight']<300
  279. and (fields['MOBheight']/180.0)~=self[ prefix..'_vsize'].y ) then
  280. local new_height = math.floor((fields['MOBheight']/1.8) +0.5)/100.0;
  281. mob_basics.update_visual_size( self, {x=self[ prefix..'_vsize'].x, y=new_height, z=self[ prefix..'_vsize'].z}, false, prefix );
  282. formspec = formspec..'label[3.0,1.5;Height changed to '..tostring( self[ prefix..'_vsize'].y*180)..' cm.]';
  283. mob_changed = true;
  284. -- width (x and z direction) has to be at least halfway reasonable
  285. elseif( fields['MOBwidth'] and fields['MOBwidth']>50 and fields['MOBwidth']<150
  286. and (fields['MOBwidth']/100.0)~=self[ prefix..'_vsize'].x ) then
  287. local new_width = math.floor(fields['MOBwidth'] +0.5)/100.0;
  288. mob_basics.update_visual_size( self, {x=new_width, y=self[ prefix..'_vsize'].y, z=new_width}, false, prefix );
  289. formspec = formspec..'label[3.0,1.5;Width changed to '..tostring( self[ prefix..'_vsize'].x*100)..'%.]';
  290. mob_changed = true;
  291. end
  292. -- save only if there where any actual changes
  293. if( mob_changed ) then
  294. mob_basics.update( self, prefix );
  295. end
  296. local npc_id = self[ prefix..'_id'];
  297. formspec = formspec..
  298. 'label[3.0,0.0;Configure your mob]'..
  299. 'label[0.0,0.5;Activity:]'..
  300. 'button[1.5,0.6;1,0.5;'..npc_id..'_config_anim_stand;*stand*]'..
  301. 'button[2.5,0.6;1,0.5;'..npc_id..'_config_anim_sit;*sit*]'..
  302. 'button[3.5,0.6;1,0.5;'..npc_id..'_config_anim_sleep;*sleep*]'..
  303. 'button[4.5,0.6;1,0.5;'..npc_id..'_config_anim_walk;*walk*]'..
  304. 'button[5.5,0.6;1,0.5;'..npc_id..'_config_anim_mine;*mine*]'..
  305. 'button[6.5,0.6;1,0.5;'..npc_id..'_config_anim_walkmine;*w&m*]'..
  306. 'label[0.0,1.0;Name of the mob:]'..
  307. 'field[3.0,1.5;3.0,0.5;MOBname;;'..( self[ prefix..'_name'] or '?' )..']'..
  308. 'label[5.8,1.0;Height:]'..
  309. 'field[6.8,1.5;0.9,0.5;MOBheight;;'..( self[ prefix..'_vsize'].y*180)..']'..
  310. 'label[7.2,1.0;cm]'..
  311. 'label[5.8,1.5;Width:]'..
  312. 'field[6.8,2.0;0.9,0.5;MOBwidth;;'..( (self[ prefix..'_vsize'].x*100) or '100' )..']'..
  313. 'label[7.2,1.5;%]'..
  314. 'label[0.0,1.6;Select a texture:]'..
  315. 'button_exit[7.5,0.2;2,0.5;'..npc_id..'_take;Take]'..
  316. 'button[7.5,0.7;2,0.5;'..npc_id..'_main;Back]'..
  317. 'button[7.5,1.2;2,0.5;'..npc_id..'_config_store;Store]';
  318. -- list available textures and mark the currently selected one
  319. for i,v in ipairs( mob_basics.TEXTURES ) do
  320. local label = '';
  321. if( v==self[ prefix..'_texture'] ) then
  322. label = 'current';
  323. end
  324. formspec = formspec..
  325. 'image_button['..tostring(((i-1)%8)*1.1-1.0)..','..tostring(math.ceil((i-1)/8)*1.1+1.2)..
  326. ';1.0,1.0;'..v..';'..npc_id..'_config_texture_'..tostring(i)..';'..label..']';
  327. end
  328. -- show the resulting formspec to the player
  329. minetest.show_formspec( player:get_player_name(), formname, formspec );
  330. end
  331. -----------------------------------------------------------------------------------------------------
  332. -- formspec input received
  333. -----------------------------------------------------------------------------------------------------
  334. mob_basics.form_input_handler = function( player, formname, fields)
  335. if( formname and formname == "mob_basics:mob_list" ) then
  336. mob_basics.mob_list_formspec( player, formname, fields );
  337. return;
  338. end
  339. -- are we responsible to handle this input?
  340. if( not( formname ) or formname ~= "mob_trading:trader" ) then -- TODO
  341. return false;
  342. end
  343. -- TODO: determine prefix from formname
  344. local prefix = 'trader';
  345. -- all the relevant information is contained in the name of the button that has
  346. -- been clicked on: npc-id, selections
  347. for k,v in pairs( fields ) do
  348. if( k == 'quit' and #fields==1) then
  349. return true;
  350. end
  351. -- all values are seperated by _
  352. local menu_path = k:split( '_');
  353. if( menu_path and #menu_path > 0 ) then
  354. -- find the mob object
  355. local self = mob_basics.find_mob_by_id( menu_path[1], prefix );
  356. if( self ) then
  357. if( #menu_path == 1 ) then
  358. menu_path = nil;
  359. end
  360. -- pick the mob up
  361. if( v=='Take' ) then
  362. if( mob_pickup and mob_pickup.pick_mob_up ) then
  363. -- all these mobs do have a unique id and are personalized, so the parameter before the last one is true
  364. -- we really want to pick up the mob - so the very last parameter is nil (not only a copy)
  365. mob_pickup.pick_mob_up( self, player, menu_path, prefix, true, nil);
  366. end
  367. return true;
  368. -- configure mob (the mob turns towards the player and shows a formspec)
  369. elseif( v=='Config' or (#menu_path>1 and menu_path[2]=='config')) then
  370. mob_basics.turn_towards_player( self, player );
  371. mob_basics.config_mob( self, player, menu_path, prefix, formname, fields );
  372. return true;
  373. -- trade with the mob (who turns towards the player and shows a formspec)
  374. else
  375. mob_basics.turn_towards_player( self, player );
  376. mob_trading.show_trader_formspec( self, player, menu_path, fields,
  377. mob_basics.mob_types[ prefix ][ self.trader_typ ].goods ); -- this is handled in mob_trading.lua
  378. return true;
  379. end
  380. return true;
  381. end
  382. end
  383. end
  384. return true;
  385. end
  386. -- make sure we receive the input
  387. minetest.register_on_player_receive_fields( mob_basics.form_input_handler );
  388. -----------------------------------------------------------------------------------------------------
  389. -- initialize a newly created mob
  390. -----------------------------------------------------------------------------------------------------
  391. mob_basics.initialize_mob = function( self, mob_name, mob_typ, mob_owner, mob_home_pos, prefix)
  392. local typ_data = mob_basics.mob_types[ prefix ];
  393. -- does this typ of mob actually exist?
  394. if( not( mob_typ ) or not( typ_data ) or not( typ_data[ mob_typ ] )) then
  395. mob_typ = 'default'; -- a default mob
  396. end
  397. -- each mob may have an individual name
  398. if( not( mob_name )) then
  399. local i = math.random( 1, #typ_data[ mob_typ ].names );
  400. self[ prefix..'_name'] = typ_data[ mob_typ ].names[ i ];
  401. else
  402. self[ prefix..'_name'] = mob_name;
  403. end
  404. if( typ_data[ mob_typ ].description ) then
  405. self.description = typ_data[ mob_typ ].description;
  406. else
  407. self.description = prefix..' '..self[ prefix..'_name'];
  408. end
  409. self[ prefix..'_typ'] = mob_typ; -- the type of the mob
  410. self[ prefix..'_owner'] = mob_owner; -- who spawned this guy?
  411. self[ prefix..'_home_pos'] = mob_home_pos; -- position of a control object (build chest, sign?)
  412. self[ prefix..'_pos'] = self.object:get_pos(); -- the place where the mob was "born"
  413. self[ prefix..'_birthtime'] = os.time(); -- when was the npc first called into existence?
  414. self[ prefix..'_sold'] = {}; -- the trader is new and had no time to sell anything yet (only makes sense for traders)
  415. -- select a random texture for the mob depending on the mob type
  416. if( typ_data[ mob_typ ].textures ) then
  417. local texture = typ_data[ mob_typ ].textures[ math.random( 1, #typ_data[ mob_typ ].textures )];
  418. self[ prefix..'_texture'] = texture;
  419. mob_basics.update_texture( self, prefix, nil );
  420. end
  421. mob_basics.update_visual_size( self, nil, true, prefix ); -- generate random visual size
  422. -- create unique ID for the mob
  423. -- uniq_id: time in seconds, _, adress of entitty data, _, prefix
  424. local uniq_id = os.time()..'.'..string.sub( tostring(self), 8 )..'.'..prefix;
  425. -- does a mob with that id exist already?
  426. if( mob_basics.known_mobs[ uniq_id ] ) then
  427. return false;
  428. end
  429. -- mobs flying in the air would be odd
  430. self.object:set_velocity( {x=0, y= 0, z=0});
  431. self.object:set_acceleration({x=0, y=-10, z=0});
  432. -- if there is already a mob with the same id, remove this one here in order to avoid duplicates
  433. if( mob_basics.find_mob_by_id( uniq_id, prefix )) then
  434. self.object:remove();
  435. return false;
  436. else
  437. -- if the mob was already known under a temporary id
  438. if( self[ prefix..'_id'] ) then
  439. mob_basics.forget_mob( self[ prefix..'_id'] )
  440. end
  441. self[ prefix..'_id'] = uniq_id;
  442. mob_basics.update( self, prefix ); -- store the newly created mob
  443. return true;
  444. end
  445. end
  446. -----------------------------------------------------------------------------------------------------
  447. -- spawn a mob
  448. -----------------------------------------------------------------------------------------------------
  449. mob_basics.spawn_mob = function( pos, mob_typ, player_name, mob_entity_name, prefix, initialize, no_messages )
  450. if( not( no_messages )) then
  451. mob_basics.log('Trying to spawn '..tostring( mob_entity_name )..' of type '..tostring( mob_typ )..' at '..minetest.pos_to_string( pos ));
  452. end
  453. -- spawning from random_buildings
  454. if( not( mob_entity_name ) and not( prefix )) then
  455. mob_entity_name = 'mobf_trader:trader';
  456. prefix = 'trader';
  457. initialize = true;
  458. end
  459. -- slightly above the position of the player so that it does not end up in a solid block
  460. local object = minetest.add_entity( {x=pos.x, y=(pos.y+1.5), z=pos.z}, mob_entity_name );
  461. if( not( initialize )) then
  462. if( object ~= nil ) then
  463. local self = object:get_luaentity();
  464. mob_basics.update( self, prefix ); -- a mob has been added
  465. end
  466. return self;
  467. end
  468. if object ~= nil then
  469. object:set_yaw( -1.14 );
  470. local self = object:get_luaentity();
  471. -- initialize_mob does a mob_basics.update() already
  472. if( mob_basics.initialize_mob( self, nil, mob_typ, player_name, pos, prefix )) then
  473. if( not( no_messages )) then
  474. mob_basics.log( 'Spawned mob', self, prefix );
  475. end
  476. self[ prefix..'_texture'] = mob_basics.TEXTURES[ math.random( 1, #mob_basics.TEXTURES )];
  477. self.object:set_properties( { textures = { self[ prefix..'_texture'] }});
  478. elseif( not( no_messages )) then
  479. mob_basics.log( 'Error: ID already taken. Can not spawn mob.', nil, prefix );
  480. end
  481. return self;
  482. end
  483. end
  484. -- compatibility function for random_buildings
  485. mobf_trader_spawn_trader = mob_basics.spawn_mob;
  486. if( minetest.get_modpath( "mobf_trader" ) and mobf_trader ) then
  487. mobf_trader.spawn_trader = mob_basics.spawn_mob;
  488. end
  489. -----------------------------------------------------------------------------------------------------
  490. -- handle input from a chat command to spawn a mob
  491. -----------------------------------------------------------------------------------------------------
  492. mob_basics.handle_chat_command = function( name, param, prefix, mob_entity_name )
  493. if( param == "" or param==nil) then
  494. minetest.chat_send_player(name,
  495. "Please supply the type of "..prefix.."! Supported: "..
  496. table.concat( mob_basics.type_list_for_prefix( prefix ), ', ')..'.' );
  497. return;
  498. end
  499. if( not( mob_basics.mob_types[ prefix ] ) or not( mob_basics.mob_types[ prefix ][ param ] )) then
  500. minetest.chat_send_player(name,
  501. "A mob "..prefix.." of type \""..tostring( param )..
  502. "\" does not exist. Supported: "..
  503. table.concat( mob_basics.type_list_for_prefix( prefix ), ', ')..'.' );
  504. return;
  505. end
  506. -- the actual spawning requires a priv; the type list as such may be seen by anyone
  507. if( not( minetest.check_player_privs(name, {mob_basics_spawn=true}))) then
  508. minetest.chat_send_player(name,
  509. "You need the mob_basics_spawn priv in order to spawn "..prefix..".");
  510. return;
  511. end
  512. local player = minetest.get_player_by_name(name);
  513. local pos = player:get_pos();
  514. minetest.chat_send_player(name,
  515. "Placing "..prefix.." \'"..tostring( param )..
  516. "\' at your position: "..minetest.pos_to_string( pos )..".");
  517. mob_basics.spawn_mob( pos, param, name, mob_entity_name, prefix, true );
  518. end
  519. -----------------------------------------------------------------------------------------------------
  520. -- It is sometimes helpful to be able to figure out where the traders and other mobs actually are
  521. -- and where the nearest one can be found.
  522. -- This is also useful for restoring mobs after a /clearallobjects.
  523. -----------------------------------------------------------------------------------------------------
  524. minetest.register_chatcommand( 'moblist', {
  525. params = "<trader type>",
  526. description = "Shows a list of all mobs known to mob_basics.",
  527. privs = {},
  528. func = function(name, param)
  529. -- this function handles the sanity checks and the actual spawning
  530. return mob_basics.mob_list_formspec( minetest.get_player_by_name( name ), "mob_basics:mob_list", {});
  531. end
  532. });
  533. -----------------------------------------------------------------------------------------------------
  534. -- show a list of existing mobs and their positions
  535. -----------------------------------------------------------------------------------------------------
  536. mob_basics.mob_list_formspec = function( player, formname, fields )
  537. if( not( player ) or fields.quit) then
  538. return
  539. end
  540. local pname = player:get_player_name();
  541. local ppos = player:get_pos();
  542. local search_for = nil;
  543. local id_found = nil;
  544. local col = 0;
  545. local text = '';
  546. if( fields and fields.mob_list and not( fields.back )) then
  547. local row = 1;
  548. local selection = minetest.explode_table_event( fields.mob_list );
  549. if( selection ) then
  550. row = selection.row;
  551. col = selection.column;
  552. end
  553. if( not( row ) or row > #mob_basics.mob_list or row < 1 or not( mob_basics.known_mobs[ mob_basics.mob_list[ row ]])) then
  554. row = 1; -- default to first row
  555. end
  556. if( not( col ) or col < 1 or col > 10 ) then
  557. col = 0; -- default to no limitation
  558. end
  559. local data = mob_basics.known_mobs[ mob_basics.mob_list[ row ]];
  560. if( not( data )) then
  561. data = {};
  562. end
  563. local prefix = data['mob_prefix'];
  564. if( not( prefix )) then
  565. prefix = 'trader';
  566. end
  567. local mpos = data[ prefix..'_pos'];
  568. if( not( prefix )) then
  569. prefix = 'trader';
  570. end
  571. if( col == 1 ) then
  572. search_for = math.ceil( math.sqrt(((ppos.x-mpos.x)*(ppos.x-mpos.x))
  573. + ((ppos.y-mpos.y)*(ppos.y-mpos.y))
  574. + ((ppos.z-mpos.z)*(ppos.z-mpos.z))));
  575. text = 'Mobs that are less than '..tostring( search_for )..' m away from you';
  576. elseif( col == 2 ) then -- same prefix
  577. search_for = prefix;
  578. text = 'Mobs of typ \''..tostring( search_for )..'\'';
  579. elseif( col == 3 ) then
  580. search_for = data[ prefix..'_typ'];
  581. text = 'Mobs of the subtyp \''..tostring( search_for )..'\'';
  582. -- 4, 5 and 6 store the mobs position
  583. elseif( col == 7 ) then
  584. search_for = data[ prefix..'_name'];
  585. text = 'Mobs with the name \''..tostring( search_for )..'\'';
  586. elseif( col == 8 ) then
  587. search_for = data[ prefix..'_owner'];
  588. text = 'Mobs belonging to player '..tostring(search_for);
  589. -- create a copy of the mob data and store that as a placeable item in the player's inventory
  590. elseif( col == 9 ) then
  591. -- actually create a copy of the mob
  592. if( mob_pickup and mob_pickup.pick_mob_up ) then
  593. local mobself = {};
  594. mobself.name = 'mobf_trader:trader'; -- TODO: there may be further types in the future
  595. mobself[ prefix..'_owner' ] = data[ prefix..'_owner'];
  596. -- all these mobs do have a unique id and are personalized, so the parameter before the last one is true
  597. -- the mob as such is not affected - we only want a copy (thus, last parameter is data)
  598. mob_pickup.pick_mob_up(mobself, player, menu_path, prefix, true, minetest.serialize( data ));
  599. end
  600. id_found = data[ prefix..'_id']; -- show details about this particular mob
  601. search_for = nil;
  602. col = 0;
  603. -- visit the mob
  604. elseif( col == 10 ) then
  605. if( not( minetest.check_player_privs(pname, {teleport=true}))) then
  606. search_for = nil;
  607. col = 0;
  608. minetest.chat_send_player( pname, 'You do not have the teleport priv. Please walk there manually.');
  609. elseif( mpos and mpos.x and mpos.y and mpos.z ) then
  610. player:moveto( mpos, false ); -- teleport the player to the mob
  611. -- TODO: check if the mob is there; if not: restore it
  612. return;
  613. end
  614. -- else no search
  615. else
  616. search_for = nil;
  617. col = 0;
  618. end
  619. end
  620. -- selections in that list lead back to the main list
  621. local input_form_name = 'mob_list';
  622. if( search_for ) then
  623. input_form_name = 'mob_list_searched';
  624. end
  625. local formspec = 'size[12,12]'..
  626. 'button_exit[4.0,0.5;2,0.5;quit;Quit]'..
  627. 'tablecolumns[' ..
  628. -- 'text,align=left;'..
  629. 'text,align=right;'..
  630. 'text,align=center;'..
  631. 'text,align=center;'..
  632. 'text,align=right;'..
  633. 'text,align=right;'..
  634. 'text,align=right;'..
  635. 'text,align=left;'..
  636. 'text,align=left;'..
  637. 'text,align=center;'..
  638. 'text,align=center]'..
  639. 'table[0.1,2.7;11.4,8.8;'..input_form_name..';';
  640. -- the list mob_basic.mob_list contains the ids of all known mobs; they act as indices for mob_basics.known_mobs;
  641. -- important part: mob_basics.mob_list only gets extended but not shortened during the runtime of a server
  642. for k,v in ipairs( mob_basics.mob_list ) do
  643. local data = mob_basics.known_mobs[ v ];
  644. if( data ) then
  645. local prefix = data['mob_prefix'];
  646. if( not( prefix )) then
  647. prefix = 'trader';
  648. end
  649. local mpos = data[ prefix..'_pos'];
  650. local distance = math.sqrt(((ppos.x-mpos.x)*(ppos.x-mpos.x))
  651. + ((ppos.y-mpos.y)*(ppos.y-mpos.y))
  652. + ((ppos.z-mpos.z)*(ppos.z-mpos.z)));
  653. if( not( search_for ) -- list all mobs
  654. or( col==1 and search_for and search_for >= distance ) -- list all mobs less than this many m away
  655. or( col==2 and search_for and search_for==data['mob_prefix'] ) -- list all mobs with the same prefix
  656. or( col==3 and search_for and search_for==data[ prefix..'_typ' ] ) -- " " " typ (i.e. fruit traders)
  657. or( col==7 and search_for and search_for==data[ prefix..'_name' ] ) -- " " " name
  658. or( col==8 and search_for and search_for==data[ prefix..'_owner' ] ) -- " " " owner
  659. ) then
  660. formspec = formspec..
  661. -- tostring( data[ prefix..'_id' ])..','.. -- left aligned
  662. tostring( math.floor( distance ) )..','.. -- right-aligned
  663. tostring( data[ 'mob_prefix' ] or '')..','.. -- centered
  664. tostring( data[ prefix..'_typ' ] or '')..','..
  665. tostring( math.floor(data[ prefix..'_pos'].x ))..','.. -- right-aligned
  666. tostring( math.floor(data[ prefix..'_pos'].y ))..','..
  667. tostring( math.floor(data[ prefix..'_pos'].z ))..','..
  668. tostring( data[ prefix..'_name' ] or '')..','..
  669. tostring( data[ prefix..'_owner' ] or '')..','; -- left aligned
  670. if( data[ prefix..'_owner' ] and data[ prefix..'_owner'] == pname ) then
  671. formspec = formspec..'Copy';
  672. elseif( minetest.check_player_privs( pname, {mob_pickup=true})) then
  673. formspec = formspec..'Admin-Copy';
  674. end
  675. formspec = formspec..',Visit MOB,';
  676. -- Note: The fields _sold, _goods and _limit are specific to the trader; they cannot be displayed here.
  677. -- The values animation and vsize are of no intrest here (only when watching the trader).
  678. -- The values home_pos, birthtime, and id could be of intrest to a limited degree (at least for admins).
  679. -- texture is of intrest.
  680. end
  681. end
  682. end
  683. formspec = formspec..';]'..
  684. 'tabheader[0.1,2.2;spalte;Dist,Type,Subtype,X,Y,Z,Name of Mob,Owner;;true;true]';
  685. if( search_for and text ) then
  686. formspec = formspec..
  687. 'label[1.0,1.0;'..minetest.formspec_escape( text )..':]'..
  688. 'button[7.0,1.5;2,0.5;back;Back]';
  689. end
  690. -- display the formspec
  691. minetest.show_formspec( pname, "mob_basics:mob_list", formspec );
  692. end
  693. -----------------------------------------------------------------------------------------------------
  694. -- traders may have diffrent textures; if 3d_armor is installed, they show what they sell
  695. -----------------------------------------------------------------------------------------------------
  696. -- TODO: add an option for mobs to statically wield something
  697. mob_basics.update_texture = function( self, prefix, trader_goods )
  698. -- set a default fallback texture
  699. if( not( self[ prefix..'_texture'] )) then
  700. self[ prefix..'_texture'] = "character.png";
  701. end
  702. -- normal model
  703. if( mobf_trader.mesh ~= "3d_armor_character.b3d" ) then
  704. self.object:set_properties( { textures = { self[ prefix..'_texture'] }});
  705. -- we are done; no way to show the player what the mob is trying to sell
  706. return;
  707. end
  708. -- we are dealing with wieldview now
  709. -- fallback in case we find no image for the trade good
  710. local wield_texture = "3d_armor_trans.png";
  711. -- get the goods the trader has to offer
  712. if( not( trader_goods )) then
  713. trader_goods = mob_trading.get_trader_goods( self, nil, nil);
  714. end
  715. if( not( trader_goods )) then
  716. trader_goods = {};
  717. end
  718. local wield_offer = trader_goods[1];
  719. if( type(trader_goods[1])== 'table' ) then
  720. wield_offer = trader_goods[1][1];
  721. end
  722. -- update what the trader wields
  723. if( wield_offer
  724. and trader_goods
  725. and type(wield_offer)=='string' ) then
  726. local stack = ItemStack( wield_offer );
  727. local stack_name = stack:get_name();
  728. if( stack_name and minetest.registered_items[ stack_name ] ) then
  729. wield_texture = minetest.registered_items[ stack_name ].wield_image;
  730. if( not( wield_texture ) or wield_texture=="") then
  731. wield_texture = minetest.registered_items[ stack_name ].inventory_image;
  732. end
  733. if( not( wield_texture ) or wield_texture=="") then
  734. wield_texture = minetest.registered_items[ stack_name ].tiles;
  735. if( type(wield_texture)=='table' ) then
  736. wield_texture = wield_texture[1];
  737. end
  738. end
  739. end
  740. end
  741. -- actually update the textures
  742. self.object:set_properties( { textures = {
  743. self[ prefix..'_texture'],
  744. "3d_armor_trans.png",
  745. wield_texture
  746. }});
  747. end
  748. -- TODO: show additional data
  749. -- trader_home_pos = self.trader_home_pos,
  750. -- trader_birthtime = self.trader_birthtime,
  751. -- trader_id = self.trader_id,
  752. -- trader_texture = self.trader_texture,