visual.lua 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456
  1. --[[
  2. Minetest Mod Storage Drawers - A Mod adding storage drawers
  3. Copyright (C) 2017-2019 Linus Jahn <lnj@kaidan.im>
  4. MIT License
  5. Permission is hereby granted, free of charge, to any person obtaining a copy
  6. of this software and associated documentation files (the "Software"), to deal
  7. in the Software without restriction, including without limitation the rights
  8. to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  9. copies of the Software, and to permit persons to whom the Software is
  10. furnished to do so, subject to the following conditions:
  11. The above copyright notice and this permission notice shall be included in all
  12. copies or substantial portions of the Software.
  13. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  14. IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  15. FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  16. AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  17. LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  18. OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
  19. SOFTWARE.
  20. ]]
  21. -- Load support for intllib.
  22. local MP = core.get_modpath(core.get_current_modname())
  23. local S, NS = dofile(MP.."/intllib.lua")
  24. core.register_entity("drawers:visual", {
  25. initial_properties = {
  26. hp_max = 1,
  27. physical = false,
  28. collide_with_objects = false,
  29. collisionbox = {-0.4374, -0.4374, 0, 0.4374, 0.4374, 0}, -- for param2 0, 2
  30. visual = "upright_sprite", -- "wielditem" for items without inv img?
  31. visual_size = {x = 0.6, y = 0.6},
  32. textures = {"blank.png"},
  33. spritediv = {x = 1, y = 1},
  34. initial_sprite_basepos = {x = 0, y = 0},
  35. is_visible = true,
  36. },
  37. get_staticdata = function(self)
  38. return core.serialize({
  39. drawer_posx = self.drawer_pos.x,
  40. drawer_posy = self.drawer_pos.y,
  41. drawer_posz = self.drawer_pos.z,
  42. texture = self.texture,
  43. drawerType = self.drawerType,
  44. visualId = self.visualId
  45. })
  46. end,
  47. on_activate = function(self, staticdata, dtime_s)
  48. -- Restore data
  49. local data = core.deserialize(staticdata)
  50. if data then
  51. self.drawer_pos = {
  52. x = data.drawer_posx,
  53. y = data.drawer_posy,
  54. z = data.drawer_posz,
  55. }
  56. self.texture = data.texture
  57. self.drawerType = data.drawerType or 1
  58. self.visualId = data.visualId or ""
  59. -- backwards compatibility
  60. if self.texture == "drawers_empty.png" then
  61. self.texture = "blank.png"
  62. end
  63. else
  64. self.drawer_pos = drawers.last_drawer_pos
  65. self.texture = drawers.last_texture or "blank.png"
  66. self.visualId = drawers.last_visual_id
  67. self.drawerType = drawers.last_drawer_type
  68. end
  69. local node = minetest.get_node(self.object:get_pos())
  70. if not node.name:match("^drawers:") then
  71. self.object:remove()
  72. return
  73. end
  74. -- add self to public drawer visuals
  75. -- this is needed because there is no other way to get this class
  76. -- only the underlying LuaEntitySAO
  77. -- PLEASE contact me, if this is wrong
  78. local vId = self.visualId
  79. if vId == "" then vId = 1 end
  80. local posstr = core.serialize(self.drawer_pos)
  81. if not drawers.drawer_visuals[posstr] then
  82. drawers.drawer_visuals[posstr] = {[vId] = self}
  83. else
  84. drawers.drawer_visuals[posstr][vId] = self
  85. end
  86. -- get meta
  87. self.meta = core.get_meta(self.drawer_pos)
  88. -- collisionbox
  89. node = core.get_node(self.drawer_pos)
  90. local colbox
  91. if self.drawerType ~= 2 then
  92. if node.param2 == 1 or node.param2 == 3 then
  93. colbox = {0, -0.4374, -0.4374, 0, 0.4374, 0.4374}
  94. else
  95. colbox = {-0.4374, -0.4374, 0, 0.4374, 0.4374, 0} -- for param2 = 0 or 2
  96. end
  97. -- only half the size if it's a small drawer
  98. if self.drawerType > 1 then
  99. for i,j in pairs(colbox) do
  100. colbox[i] = j * 0.5
  101. end
  102. end
  103. else
  104. if node.param2 == 1 or node.param2 == 3 then
  105. colbox = {0, -0.2187, -0.4374, 0, 0.2187, 0.4374}
  106. else
  107. colbox = {-0.4374, -0.2187, 0, 0.4374, 0.2187, 0} -- for param2 = 0 or 2
  108. end
  109. end
  110. -- visual size
  111. local visual_size = {x = 0.6, y = 0.6}
  112. if self.drawerType >= 2 then
  113. visual_size = {x = 0.3, y = 0.3}
  114. end
  115. -- drawer values
  116. local vid = self.visualId
  117. self.count = self.meta:get_int("count"..vid)
  118. self.itemName = self.meta:get_string("name"..vid)
  119. self.maxCount = self.meta:get_int("max_count"..vid)
  120. self.itemStackMax = self.meta:get_int("base_stack_max"..vid)
  121. self.stackMaxFactor = self.meta:get_int("stack_max_factor"..vid)
  122. -- infotext
  123. local infotext = self.meta:get_string("entity_infotext"..vid) .. "\n\n\n\n\n"
  124. self.object:set_properties({
  125. collisionbox = colbox,
  126. infotext = infotext,
  127. textures = {self.texture},
  128. visual_size = visual_size
  129. })
  130. -- make entity undestroyable
  131. self.object:set_armor_groups({immortal = 1})
  132. end,
  133. on_rightclick = function(self, clicker)
  134. if core.is_protected(self.drawer_pos, clicker:get_player_name()) then
  135. core.record_protection_violation(self.drawer_pos, clicker:get_player_name())
  136. return
  137. end
  138. -- used to check if we need to play a sound in the end
  139. local inventoryChanged = false
  140. -- When the player uses the drawer with their bare hand all
  141. -- stacks from the inventory will be added to the drawer.
  142. if self.itemName ~= "" and
  143. clicker:get_wielded_item():get_name() == "" and
  144. not clicker:get_player_control().sneak then
  145. -- try to insert all items from inventory
  146. local i = 0
  147. local inv = clicker:get_inventory()
  148. while i <= inv:get_size("main") do
  149. -- set current stack to leftover of insertion
  150. local leftover = self.try_insert_stack(
  151. self,
  152. inv:get_stack("main", i),
  153. true
  154. )
  155. -- check if something was added
  156. if leftover:get_count() < inv:get_stack("main", i):get_count() then
  157. inventoryChanged = true
  158. end
  159. -- set new stack
  160. inv:set_stack("main", i, leftover)
  161. i = i + 1
  162. end
  163. else
  164. -- try to insert wielded item only
  165. local leftover = self.try_insert_stack(
  166. self,
  167. clicker:get_wielded_item(),
  168. not clicker:get_player_control().sneak
  169. )
  170. -- check if something was added
  171. if clicker:get_wielded_item():get_count() > leftover:get_count() then
  172. inventoryChanged = true
  173. end
  174. -- set the leftover as new wielded item for the player
  175. clicker:set_wielded_item(leftover)
  176. end
  177. if inventoryChanged then
  178. self:play_interact_sound()
  179. end
  180. end,
  181. on_punch = function(self, puncher, time_from_last_punch, tool_capabilities, dir)
  182. local node = minetest.get_node(self.object:get_pos())
  183. if not node.name:match("^drawers:") then
  184. self.object:remove()
  185. return
  186. end
  187. local add_stack = not puncher:get_player_control().sneak
  188. if core.is_protected(self.drawer_pos, puncher:get_player_name()) then
  189. core.record_protection_violation(self.drawer_pos, puncher:get_player_name())
  190. return
  191. end
  192. local inv = puncher:get_inventory()
  193. if inv == nil then
  194. return
  195. end
  196. local spaceChecker = ItemStack(self.itemName)
  197. if add_stack then
  198. spaceChecker:set_count(spaceChecker:get_stack_max())
  199. end
  200. if not inv:room_for_item("main", spaceChecker) then
  201. return
  202. end
  203. local stack = self:take_items(add_stack)
  204. if stack ~= nil then
  205. -- add removed stack to player's inventory
  206. inv:add_item("main", stack)
  207. -- play the interact sound
  208. self:play_interact_sound()
  209. end
  210. end,
  211. take_items = function(self, take_stack)
  212. local meta = core.get_meta(self.drawer_pos)
  213. if self.count <= 0 then
  214. return
  215. end
  216. local removeCount = 1
  217. if take_stack then
  218. removeCount = ItemStack(self.itemName):get_stack_max()
  219. end
  220. if removeCount > self.count then
  221. removeCount = self.count
  222. end
  223. local stack = ItemStack(self.itemName)
  224. stack:set_count(removeCount)
  225. -- update the drawer count
  226. self.count = self.count - removeCount
  227. self:updateInfotext()
  228. self:updateTexture()
  229. self:saveMetaData()
  230. -- return the stack that was removed from the drawer
  231. return stack
  232. end,
  233. try_insert_stack = function(self, itemstack, insert_stack)
  234. local stackCount = itemstack:get_count()
  235. local stackName = itemstack:get_name()
  236. -- if nothing to be added, return
  237. if stackCount <= 0 then return itemstack end
  238. -- if no itemstring, return
  239. if stackName == "" then return itemstack end
  240. -- only add one, if player holding sneak key
  241. if not insert_stack then
  242. stackCount = 1
  243. end
  244. -- if current itemstring is not empty
  245. if self.itemName ~= "" then
  246. -- check if same item
  247. if stackName ~= self.itemName then return itemstack end
  248. else -- is empty
  249. self.itemName = stackName
  250. self.count = 0
  251. -- get new stack max
  252. self.itemStackMax = ItemStack(self.itemName):get_stack_max()
  253. self.maxCount = self.itemStackMax * self.stackMaxFactor
  254. end
  255. -- Don't add items stackable only to 1
  256. if self.itemStackMax == 1 then
  257. self.itemName = ""
  258. return itemstack
  259. end
  260. -- set new counts:
  261. -- if new count is more than max_count
  262. if (self.count + stackCount) > self.maxCount then
  263. itemstack:set_count(self.count + stackCount - self.maxCount)
  264. self.count = self.maxCount
  265. else -- new count fits
  266. self.count = self.count + stackCount
  267. -- this is for only removing one
  268. itemstack:set_count(itemstack:get_count() - stackCount)
  269. end
  270. -- update infotext, texture
  271. self:updateInfotext()
  272. self:updateTexture()
  273. self:saveMetaData()
  274. if itemstack:get_count() == 0 then itemstack = ItemStack("") end
  275. return itemstack
  276. end,
  277. updateInfotext = function(self)
  278. local itemDescription = ""
  279. if core.registered_items[self.itemName] then
  280. itemDescription = core.registered_items[self.itemName].description
  281. end
  282. if self.count <= 0 then
  283. self.itemName = ""
  284. self.meta:set_string("name"..self.visualId, self.itemName)
  285. self.texture = "blank.png"
  286. itemDescription = S("Empty")
  287. end
  288. local infotext = drawers.gen_info_text(itemDescription,
  289. self.count, self.stackMaxFactor, self.itemStackMax)
  290. self.meta:set_string("entity_infotext"..self.visualId, infotext)
  291. self.object:set_properties({
  292. infotext = infotext .. "\n\n\n\n\n"
  293. })
  294. end,
  295. updateTexture = function(self)
  296. -- texture
  297. self.texture = drawers.get_inv_image(self.itemName)
  298. self.object:set_properties({
  299. textures = {self.texture}
  300. })
  301. end,
  302. dropStack = function(self, itemStack)
  303. -- print warning if dropping higher stack counts than allowed
  304. if itemStack:get_count() > itemStack:get_stack_max() then
  305. core.log("warning", "[drawers] Dropping item stack with higher count than allowed")
  306. end
  307. -- find a position containing air
  308. local dropPos = core.find_node_near(self.drawer_pos, 1, {"air"}, false)
  309. -- if no pos found then drop on the top of the drawer
  310. if not dropPos then
  311. dropPos = self.pos
  312. dropPos.y = dropPos.y + 1
  313. end
  314. -- drop the item stack
  315. core.item_drop(itemStack, nil, dropPos)
  316. end,
  317. dropItemOverload = function(self)
  318. -- drop stacks until there are no more items than allowed
  319. while self.count > self.maxCount do
  320. -- remove the overflow
  321. local removeCount = self.count - self.maxCount
  322. -- if this is too much for a single stack, only take the
  323. -- stack limit
  324. if removeCount > self.itemStackMax then
  325. removeCount = self.itemStackMax
  326. end
  327. -- remove this count from the drawer
  328. self.count = self.count - removeCount
  329. -- create a new item stack having the size of the remove
  330. -- count
  331. local stack = ItemStack(self.itemName)
  332. stack:set_count(removeCount)
  333. print(stack:to_string())
  334. -- drop the stack
  335. self:dropStack(stack)
  336. end
  337. end,
  338. setStackMaxFactor = function(self, stackMaxFactor)
  339. self.stackMaxFactor = stackMaxFactor
  340. self.maxCount = self.stackMaxFactor * self.itemStackMax
  341. -- will drop possible overflowing items
  342. self:dropItemOverload()
  343. self:updateInfotext()
  344. self:saveMetaData()
  345. end,
  346. play_interact_sound = function(self)
  347. core.sound_play("drawers_interact", {
  348. pos = self.object:get_pos(),
  349. max_hear_distance = 6,
  350. gain = 2.0
  351. })
  352. end,
  353. saveMetaData = function(self, meta)
  354. self.meta:set_int("count"..self.visualId, self.count)
  355. self.meta:set_string("name"..self.visualId, self.itemName)
  356. self.meta:set_int("max_count"..self.visualId, self.maxCount)
  357. self.meta:set_int("base_stack_max"..self.visualId, self.itemStackMax)
  358. self.meta:set_int("stack_max_factor"..self.visualId, self.stackMaxFactor)
  359. end
  360. })
  361. core.register_lbm({
  362. name = "drawers:restore_visual",
  363. nodenames = {"group:drawer"},
  364. run_at_every_load = true,
  365. action = function(pos, node)
  366. local meta = core.get_meta(pos)
  367. -- create drawer upgrade inventory
  368. meta:get_inventory():set_size("upgrades", 5)
  369. -- set the formspec
  370. meta:set_string("formspec", drawers.drawer_formspec)
  371. -- count the drawer visuals
  372. local drawerType = core.registered_nodes[node.name].groups.drawer
  373. local foundVisuals = 0
  374. local objs = core.get_objects_inside_radius(pos, 0.59)
  375. if objs then
  376. for _, obj in pairs(objs) do
  377. if obj and obj:get_luaentity() and
  378. obj:get_luaentity().name == "drawers:visual" then
  379. foundVisuals = foundVisuals + 1
  380. end
  381. end
  382. end
  383. -- if all drawer visuals were found, return
  384. if foundVisuals == drawerType then
  385. return
  386. end
  387. -- not enough visuals found, remove existing and create new ones
  388. drawers.remove_visuals(pos)
  389. drawers.spawn_visuals(pos)
  390. end
  391. })