init.lua 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356
  1. local modpath = minetest.get_modpath("markov_macaws")
  2. local settings = Settings(modpath .. "/markov_macaws.conf")
  3. local MarkovGraph = dofile(modpath .. "/markov_chains.lua")
  4. local parrot_brain = dofile(modpath .. "/parrot_brain.lua")
  5. local function parse_num(str)
  6. return 0 + str
  7. end
  8. local HEAR_DISTANCE = parse_num(settings:get("hear_distance"))
  9. local MAX_KNOWN_WORDS = parse_num(settings:get("max_known_words"))
  10. --make a list of edible food
  11. local foods = {}
  12. do
  13. local food = settings:get("foods")
  14. for name in string.gmatch(food, "[^,%s]+")
  15. do
  16. foods[name] = true
  17. end
  18. end
  19. local parrots = {}
  20. parrots["markov_macaws:macaw"] = true
  21. --put message into markov chain
  22. --if the parrot likes the player this is more likely to happen
  23. local function hear(self, talker_name, message)
  24. local rel = self.player_relations[talker_name]
  25. if rel < 0 or math.random() < 0.5
  26. then
  27. return
  28. end
  29. self.player_relations[talker_name] = rel + math.random() * 0.5
  30. self.talker:learn_line(message)
  31. if math.random() < 0.1
  32. then
  33. self.talker:cull(MAX_KNOWN_WORDS)
  34. end
  35. end
  36. --Makes parrots in a radius around the player hear messages
  37. local function parrot_chat_listener(name, message)
  38. local player = minetest.get_player_by_name(name)
  39. if not player
  40. then
  41. return
  42. end
  43. if minetest.check_player_privs(player, "shout")
  44. then
  45. local pos = player:get_pos()
  46. local objs = minetest.get_objects_inside_radius(pos, HEAR_DISTANCE)
  47. --check all entities in a radius around the player if they are
  48. --parrots. Those that are are called in the hear function
  49. for i, obj in pairs(objs)
  50. do
  51. local luae = obj:get_luaentity()
  52. if luae
  53. then
  54. if parrots[luae.name]
  55. then
  56. hear(luae, name, message)
  57. end
  58. end
  59. end
  60. end
  61. end
  62. minetest.register_on_chat_message(parrot_chat_listener)
  63. --on_activate function. Takes care of mobkit activation and also
  64. --initializes player_relations table and talker (markov chain) object
  65. local function init_parrot(self, staticdata, dtime_s)
  66. mobkit.actfunc(self, staticdata, dtime_s)
  67. --initialize self.talker
  68. --self.talker stores a markov chain object
  69. self.talker = MarkovGraph()
  70. local words = mobkit.recall(self, "words")
  71. if not words
  72. then
  73. --initialize with new words
  74. self.talker:learn_from_file(modpath .. "/default_words.txt")
  75. mobkit.remember(self, "words", self.talker.nodes)
  76. else
  77. --initialize with loaded words
  78. local meta = getmetatable(self.talker.nodes)
  79. self.talker.nodes = words
  80. setmetatable(self.talker.nodes, meta)
  81. end
  82. --initialize self.player_relations
  83. --self.player_relations maps names of known players to numbers
  84. --the number indicates how much the parrot likes the player
  85. self.player_relations = mobkit.recall(self, "player_relations")
  86. if not self.player_relations
  87. then
  88. self.player_relations = {}
  89. mobkit.remember(self, "player_relations", self.player_relations)
  90. end
  91. do
  92. --accessing relation with unknown player returns 0 instead of nil
  93. local meta =
  94. {
  95. __index = function(self, key)
  96. return 0
  97. end,
  98. }
  99. setmetatable(self.player_relations, meta)
  100. end
  101. end
  102. local function lq_animate_for_duration(self, duration, animation)
  103. local time = 0
  104. mobkit.animate(self, animation)
  105. local function func()
  106. time = time + self.dtime
  107. if time > duration
  108. then
  109. return true
  110. end
  111. end
  112. mobkit.queue_low(self, func)
  113. end
  114. local function hq_nope(self, prty)
  115. local function func()
  116. if self.isonground
  117. then
  118. mobkit.clear_queue_low(self)
  119. mobkit.make_sound(self, "squawk")
  120. lq_animate_for_duration(self, 0.5, "flap")
  121. return true
  122. else
  123. return true
  124. end
  125. end
  126. mobkit.queue_high(self, func, prty)
  127. end
  128. --this doesn't make 100% sense because it makes
  129. --assumptions about how the player model looks
  130. local attach_positions =
  131. player_api and
  132. {
  133. {"Body", {x = 4, y = 6, z = 0}, {x = 0, y = 180, z = 0}},
  134. {"Body", {x = 4, y = 6, z = 0}, {x = 0, y = 0, z = 0}},
  135. {"Body", {x = -4, y = 6, z = 0}, {x = 0, y = 180, z = 0}},
  136. {"Body", {x = -4, y = 6, z = 0}, {x = 0, y = 0, z = 0}},
  137. } or
  138. {
  139. --alternative if player_api is not installed
  140. {"", {x = 4, y = 14, z = 0}, {x = 0, y = 0, z = 0}},
  141. {"", {x = 4, y = 14, z = 0}, {x = 0, y = 180, z = 0}},
  142. {"", {x = -4, y = 14, z = 0}, {x = 0, y = 0, z = 0}},
  143. {"", {x = -4, y = 14, z = 0}, {x = 0, y = 180, z = 0}},
  144. }
  145. --TODO: use low level queue properly
  146. local function hq_perch(self, prty, mount)
  147. --TODO: make this only work if there is no parrot on the shoulder yet
  148. local mountpos = attach_positions[math.random(#attach_positions)]
  149. self.object:set_attach(mount, mountpos[1], mountpos[2], mountpos[3])
  150. local trouble = 0
  151. local troubletimer = 0
  152. local function func()
  153. --if mount is jumping, increase trouble value
  154. if mobkit.is_alive(mount)
  155. then
  156. trouble = trouble + math.abs(mount:get_player_velocity().y)
  157. end
  158. --decay trouble value
  159. trouble = trouble * 0.9
  160. if trouble > 3
  161. then
  162. --flap if trouble value is high
  163. troubletimer = troubletimer + self.dtime
  164. mobkit.clear_queue_low(self)
  165. lq_animate_for_duration(self, 0.1, "perch_flap")
  166. else
  167. if not mobkit.is_queue_empty_low(self)
  168. then
  169. return false
  170. end
  171. --reset trouble timer
  172. troubletimer = 0
  173. --dance randomly
  174. local dance = math.random()
  175. if dance < 0.3
  176. then
  177. lq_animate_for_duration(self, 1, "perch_dance")
  178. return false
  179. end
  180. lq_animate_for_duration(self, 1, "perch")
  181. end
  182. --dismount if jumping or falling for long / fast enough
  183. if troubletimer * trouble > 300 or not self.object:get_attach()
  184. then
  185. self.object:set_detach()
  186. return true
  187. end
  188. end
  189. mobkit.queue_high(self, func, prty)
  190. end
  191. local function eat_food(self, clicker)
  192. --only eat from non-bad people
  193. local name = clicker:get_player_name()
  194. local rel = self.player_relations[name]
  195. if rel < 0 or
  196. math.random() > rel
  197. then
  198. return false
  199. end
  200. local wielditem = clicker:get_wielded_item()
  201. if foods[wielditem:get_name()]
  202. then
  203. wielditem:take_item()
  204. clicker:set_wielded_item(wielditem)
  205. self.player_relations[name] = rel + math.random()
  206. return true
  207. end
  208. end
  209. local function on_rightclick(self, clicker)
  210. if eat_food(self, clicker)
  211. then
  212. return
  213. end
  214. local clickername = clicker:get_player_name()
  215. local rel = self.player_relations[clickername]
  216. if rel > 2 and math.random() < rel
  217. then
  218. --perch on shoulder if lucky
  219. hq_perch(self, 20, clicker)
  220. return
  221. end
  222. --play flap animation
  223. mobkit.clear_queue_high(self)
  224. hq_nope(self, 10)
  225. end
  226. local parrot =
  227. {
  228. physical = true,
  229. stepheight = 0.1, --EVIL!
  230. collide_with_objects = true,
  231. collisionbox = {-0.3, -0.01, -0.3, 0.3, 0.7, 0.3},
  232. visual = "mesh",
  233. mesh = "markov_macaws_macaw.b3d",
  234. textures = {"markov_macaws_macaw.png"},
  235. static_save = true,
  236. makes_footstep_sound = true,
  237. timeout = 0,
  238. buoyancy = -1,
  239. lung_capacity = 10,
  240. max_hp = 10,
  241. jump_height = 1,
  242. max_speed = 3,
  243. view_range = 10,
  244. armor_groups = {fleshy = 3},
  245. color = "red", --TODO: make more colors
  246. animation =
  247. {
  248. walk = {range = {x = 1, y = 29}, speed = 30, loop = true},
  249. stand = {range = {x = 101, y = 129}, speed = 30, loop = true},
  250. emote =
  251. {
  252. {range = {x = 61, y = 69}, speed = 30, loop = true}, --hop
  253. {range = {x = 131, y = 139}, speed = 30, loop = true} --dance
  254. },
  255. flap = {range = {x = 171, y = 185}, speed = 30, loop = true},
  256. perch = {range = {x = 31, y = 59}, speed = 30, loop = true},
  257. perch_flap = {range = {x = 155, y = 169}, speed = 30, loop = true},
  258. perch_dance = {range = {x = 141, y = 153}, speed = 15, loop = true},
  259. },
  260. sounds =
  261. {
  262. squawk =
  263. {
  264. name = "markov_macaws_squawk",
  265. gain = {1, 1.5},
  266. pitch = {0.7, 1.5},
  267. },
  268. talk =
  269. {
  270. name = "markov_macaws_talk",
  271. pitch = {0.7, 1.3},
  272. gain = {0.5, 1.5},
  273. },
  274. playful =
  275. {
  276. {
  277. name = "markov_macaws_whistle",
  278. pitch = {0.7, 1.5},
  279. gain = {0.3, 1.5},
  280. },
  281. {
  282. name = "markov_macaws_clack",
  283. pitch = {0.9, 1.3},
  284. gain = {0.3, 1.5},
  285. },
  286. {
  287. name = "markov_macaws_squawk",
  288. pitch = {0.7, 1.3},
  289. gain = {1, 1.5},
  290. },
  291. {
  292. name = "markov_macaws_talk",
  293. pitch = {0.7, 1.3},
  294. gain = {0.5, 1.5},
  295. },
  296. }
  297. },
  298. logic = parrot_brain,
  299. on_step = mobkit.stepfunc, -- required
  300. on_activate = init_parrot,
  301. get_staticdata = mobkit.statfunc,
  302. on_rightclick = on_rightclick,
  303. on_punch = function(self, puncher, time_from_last_punch, tool_capabilities, dir)
  304. if mobkit.is_alive(self) then
  305. local hvel = vector.multiply(vector.normalize({x=dir.x,y=0,z=dir.z}),4)
  306. self.object:set_velocity({x=hvel.x,y=2,z=hvel.z})
  307. mobkit.hurt(self,tool_capabilities.damage_groups.fleshy or 1)
  308. if type(puncher)=='userdata' and puncher:is_player() then -- if hit by a player
  309. mobkit.clear_queue_high(self) -- abandon whatever they've been doing
  310. mobkit.hq_runfrom(self, 50, puncher) -- flee
  311. end
  312. end
  313. end
  314. }
  315. minetest.register_entity("markov_macaws:macaw", parrot)