Author Topic: NPC item picker script  (Read 262 times)

alexgleason

  • Jr. Member
  • **
  • Posts: 72
  • Vegan on a Desert Island
    • View Profile
    • Vegan on a Desert Island
NPC item picker script
« on: October 24, 2018, 11:24:07 pm »
This is sorta hard to explain, but basically I wanted the ability for any NPC to prompt the hero for an item, then the hero has the option to select an item in their inventory to give. It works like Paper Mario if you've ever played it. Demo:



My game revolves a lot around trading items, so it's important that this is streamlined. It allows writing code like this:

Code: Lua
  1. local bunny = map:get_entity("bunny1")
  2.  
  3. function bunny:on_interaction()
  4.   game:start_dialog("bunny.want_carrot", function()
  5.     bunny:ask_item(function(item)
  6.       if item and item:get_name() == "carrot"
  7.         item:set_variant(0)
  8.         game:start_dialog("bunny.thanks_for_carrot")
  9.       elseif item then
  10.         -- a non-carrot item is given
  11.         game:start_dialog("bunny.no_thanks")
  12.       else
  13.         -- The action was cancelled (item == nil). Do nothing.
  14.       end
  15.     end)
  16.   end)
  17. end

I'm not sure I organized my code the best way, but here it is:

scripts/menus/picker.lua:
Code: Lua
  1. -- ♡ Copying is an act of love. Please copy and share.
  2.  
  3. -- This menu ultimately exists to let you call npc:ask_item()
  4. -- It's designed to be called from npc:ask_item() and really cannot stand alone
  5.  
  6. require("scripts/multi_events")
  7.  
  8. -- Items that could appear in this menu
  9. local key_items = {
  10.   'trade',
  11.   'whiskey',
  12. }
  13. local player_items = {} -- Items the player actually has
  14.  
  15.  
  16. -- Get the XY for an item sprite based on its position
  17. local function get_slot_xy(i)
  18.   local mx, my = 24, 32 -- offset from menu origin
  19.  
  20.   local r = math.ceil(i/4) -- row of this slot
  21.   local ix = ((i-1)%4)*24
  22.   local iy = (r-1)*24
  23.  
  24.   local x = mx + ix
  25.   local y = my + iy
  26.  
  27.   return x, y
  28. end
  29.  
  30.  
  31. local picker = {} -- item picker menu
  32.  
  33.  
  34. function picker:initialize()
  35.   local font, font_size = "Comicoro", 16
  36.  
  37.   -- Graphics
  38.   self.menu = sol.surface.create(144, 104)
  39.   self.menu_bg = sol.sprite.create("menus/menu_bg")
  40.   self.item_sprite = sol.sprite.create("entities/items")
  41.   self.cursor_sprite = sol.sprite.create("menus/cursor")
  42.  
  43.   -- Player sprite
  44.   self.player_sprite = sol.sprite.create("main_heroes/rachel")
  45.   self.player_sprite:set_animation("walking")
  46.   self.player_sprite:set_direction(3)
  47.  
  48.   -- NPC sprite
  49.   self.npc_sprite = nil
  50.  
  51.   -- Question text
  52.   self.title = sol.text_surface.create({text_key="menus.picker.question", font=font, font_size=font_size})
  53.   self.title:set_color({0, 0, 0})
  54.  
  55.   -- Nope button
  56.   self.nope_button = sol.surface.create(32, 16)
  57.   self.nope_button:fill_color({198, 34, 0})
  58.   local button_text = sol.text_surface.create({text_key="menus.picker.nope", font=font, font_size=font_size})
  59.   button_text:draw(self.nope_button, 4, 8)
  60. end
  61.  
  62.  
  63. -- Called when the picker menu starts
  64. function picker:on_started()
  65.   sol.main.game:get_hero():freeze()
  66.   sol.main.game._picker_enabled = true
  67.  
  68.   -- Set items the player has
  69.   player_items = {}
  70.   for i, v in ipairs(key_items) do
  71.     if sol.main.game:has_item(v) then
  72.       table.insert(player_items, v)
  73.     end
  74.   end
  75.  
  76.   -- Graphics
  77.   self.npc_sprite = sol.sprite.create(self._npc:get_sprite():get_animation_set())
  78.   self.npc_sprite:set_direction(3)
  79.  
  80.   -- Initialize properties
  81.   self.cursor = 1
  82.   self._selection = nil
  83. end
  84.  
  85.  
  86. -- Set/get the selected item (when "action" is pressed)
  87. function picker:set_selection(item)
  88.   self._selection = item
  89. end
  90.  
  91. function picker:get_selection()
  92.   return self._selection
  93. end
  94.  
  95.  
  96. -- Called every frame
  97. function picker:on_draw(dst_surface)
  98.  
  99.   -- Draw BG graphics
  100.   self.menu:draw(dst_surface, 56, 16)
  101.   self.menu_bg:draw(self.menu)
  102.  
  103.   -- Draw characters
  104.   local x, y
  105.   x, y = self.player_sprite:get_origin()
  106.   self.player_sprite:draw(self.menu, 24+x, 8+y) -- draw player sprite
  107.   x, y = self.npc_sprite:get_origin()
  108.   self.npc_sprite:draw(self.menu, 104+x, 8+y) -- draw NPC sprite
  109.  
  110.   -- Draw question
  111.   self.title:draw(self.menu, 49, 16)
  112.  
  113.   -- Draw items, loop through inventory
  114.   for i, item in ipairs(player_items) do
  115.     if sol.main.game:has_item(item) then
  116.       self.item_sprite:set_animation(item) -- all items are in one sheet
  117.       local x, y = get_slot_xy(i) -- item slot XY
  118.       local ox, oy = self.item_sprite:get_origin() -- origin offset
  119.       self.item_sprite:draw(self.menu, x+ox, y+oy) -- draw item
  120.       if self.cursor == i then
  121.         self.cursor_sprite:draw(self.menu, x, y) -- draw cursor
  122.       end
  123.     end
  124.   end
  125.  
  126.   -- Draw cancel button
  127.   self.nope_button:draw(self.menu, 56, 80)
  128.   if self.cursor == #player_items+1 then
  129.     self.cursor_sprite:draw(self.menu, 64, 80) -- cancel button cursor
  130.   end
  131. end
  132.  
  133.  
  134. -- Called when a button is pressed
  135. function picker:on_command_pressed(command)
  136.  
  137.   -- D-Pad controls
  138.   if command == "up" then
  139.     self.cursor = self.cursor - 4 -- up and down navigate between rows
  140.   elseif command == "down" then
  141.     self.cursor = self.cursor + 4
  142.   elseif command == "left" then
  143.     self.cursor = self.cursor - 1
  144.   elseif command == "right" then
  145.     self.cursor = self.cursor + 1
  146.   end
  147.  
  148.   -- Cursor must be between 1 and #player_items+1 (cancel)
  149.   self.cursor = math.max(1, self.cursor)
  150.   self.cursor = math.min(self.cursor, #player_items+1)
  151.  
  152.   -- Handle selection
  153.   if command == "action" then
  154.     if self.cursor ~= #player_items+1 then
  155.       local item = sol.main.game:get_item(player_items[self.cursor])
  156.       self:set_selection(item)
  157.     end
  158.     sol.menu.stop(self)
  159.     sol.main.game:get_hero():unfreeze()
  160.     sol.main.game._picker_enabled = false
  161.   end
  162.  
  163.   return true
  164. end
  165.  
  166.  
  167. -- Initialize picker when the game starts
  168. local game_meta = sol.main.get_metatable("game")
  169. game_meta:register_event("on_started", function(self)
  170.   picker:initialize()
  171. end)
  172.  
  173.  
  174. return picker
  175.  

scripts/npc_ask_item.lua:
Code: Lua
  1. -- ♡ Copying is an act of love. Please copy and share.
  2.  
  3. require("scripts/multi_events")
  4. local picker = require("scripts/menus/picker")
  5.  
  6.  
  7. -- Add npc:ask_item(callback)
  8.  
  9. local npc_meta =  sol.main.get_metatable("npc")
  10.  
  11. -- Start a picker menu for the NPC
  12. function npc_meta:ask_item(callback)
  13.   picker._npc = self
  14.   sol.menu.start(self:get_map(), picker)
  15.   function picker:on_finished()
  16.     callback(self:get_selection())
  17.     picker._npc = nil
  18.     self.on_finished = nil -- destroy this function between calls
  19.   end
  20. end
  21.  
  22.  
  23. -- Initialize game with picker menu
  24.  
  25. local game_meta = sol.main.get_metatable("game")
  26.  
  27. -- Check whether a picker is already active
  28. function game_meta:is_picker_enabled()
  29.   return self._picker_enabled
  30. end
  31.  
  32. -- Initialize game with _picker_enabled attribute
  33. game_meta:register_event("on_started", function(self)
  34.   self._picker_enabled = false
  35. end)
  36.  

Finally, I've just added the script to features.lua:
Code: Lua
  1. require("scripts/npc_ask_item") -- NPC item picker menu
« Last Edit: October 24, 2018, 11:26:13 pm by alexgleason »
RIP Aaron Swartz

llamazing

  • Full Member
  • ***
  • Posts: 139
    • View Profile
Re: NPC item picker script
« Reply #1 on: October 25, 2018, 02:15:11 am »
That's nifty! Excellent work.

You should consider defining the items eligible to appear in the picker as part of the item scripts with some sort of custom property rather than inside the picker script itself. Perhaps add an is_giftable() method to the item metatable?

alexgleason

  • Jr. Member
  • **
  • Posts: 72
  • Vegan on a Desert Island
    • View Profile
    • Vegan on a Desert Island
Re: NPC item picker script
« Reply #2 on: October 25, 2018, 03:15:55 am »
That's nifty! Excellent work.

You should consider defining the items eligible to appear in the picker as part of the item scripts with some sort of custom property rather than inside the picker script itself. Perhaps add an is_giftable() method to the item metatable?

Thanks!

In general, items you trade will always be non-equipment items (as with most Zelda games). So I'm planning to add something like item:set_key_item(boolean), where "true" means a non-equipment item.

I'm not sure how fancy I want to get yet, but I've been thinking about npc:ask_item(filter, callback) where "filter" is a function like filter(item) that returns true if the given item should be listed.

Eg,

Code: Lua
  1. local function filter(item)
  2.   if item:get_name() == "bad_item" then
  3.     return false
  4.   end
  5.   return item:is_key_item()
  6. end
  7.  
  8. npc:ask_item(filter, function(item)
  9.   print(item:get_name()
  10. )


The only thing blocking me is that Solarus doesn't seem to have a way to list all the possible items in the quest, so I can't loop through them.
RIP Aaron Swartz

llamazing

  • Full Member
  • ***
  • Posts: 139
    • View Profile
Re: NPC item picker script
« Reply #3 on: October 25, 2018, 04:32:32 am »
The only thing blocking me is that Solarus doesn't seem to have a way to list all the possible items in the quest, so I can't loop through them.

Here's a bit of a hack, but you can get the names of all items in your quest by reading the contents of the project_db.dat file:
Code: Lua
  1. local function get_all_items()
  2.     local all_items = {}
  3.  
  4.     local env = {}
  5.     function env.item(properties)
  6.         local id = properties.id
  7.         assert(type(id)=="string", "item id must be a string")
  8.  
  9.         table.insert(all_items, id)
  10.     end
  11.  
  12.     setmetatable(env, {__index = function() return function() end end})
  13.  
  14.     local chunk = sol.main.load_file("project_db.dat")
  15.     setfenv(chunk, env)
  16.     chunk()
  17.  
  18.     return all_items
  19. end

Then you can use game:get_item(item_name) to convert the item names into item objects.

alexgleason

  • Jr. Member
  • **
  • Posts: 72
  • Vegan on a Desert Island
    • View Profile
    • Vegan on a Desert Island
Re: NPC item picker script
« Reply #4 on: October 25, 2018, 04:41:04 am »
Here's a bit of a hack, but you can get the names of all items in your quest by reading the contents of the project_db.dat file:

Woaahh, thank you!! I considered doing something like this but I didn't know how. It's great to have help from a Lua wizard. ;D Thank you.
RIP Aaron Swartz

alexgleason

  • Jr. Member
  • **
  • Posts: 72
  • Vegan on a Desert Island
    • View Profile
    • Vegan on a Desert Island
Re: NPC item picker script
« Reply #5 on: October 25, 2018, 05:27:37 am »
I had to stare at your code, read about Lua environments, and stare some more for like 30 minutes, until it finally clicked. quest_db is a lua file that makes function calls (item()), so we can simply define item(), load quest_db, and set its environment to our custom function(s). Awesome.

I did notice that Lua 5.2 drops setfenv() apparently. I'm guessing Solarus runs on an older version?
RIP Aaron Swartz

llamazing

  • Full Member
  • ***
  • Posts: 139
    • View Profile
Re: NPC item picker script
« Reply #6 on: October 25, 2018, 07:12:20 am »
I'm guessing Solarus runs on an older version?
5.1

Christopho

  • Administrator
  • Hero Member
  • *****
  • Posts: 1144
    • View Profile
Re: NPC item picker script
« Reply #7 on: October 25, 2018, 09:50:40 am »
Note that Solarus 1.6 will have a new feature sol.main.get_resource_ids("item").

alexgleason

  • Jr. Member
  • **
  • Posts: 72
  • Vegan on a Desert Island
    • View Profile
    • Vegan on a Desert Island
Re: NPC item picker script
« Reply #8 on: October 25, 2018, 05:16:44 pm »
Note that Solarus 1.6 will have a new feature sol.main.get_resource_ids("item").

Nice! Using llamazing's code I created a polyfill for the feature.

scripts/polyfills/get_resource_ids.lua:
Code: Lua
  1. -- Polyfill for sol.main.get_resource_ids(resource_type)
  2. --   See: http://www.solarus-games.org/doc/1.6/lua_api_main.html#lua_api_main_get_resource_ids
  3. --
  4. -- Note that v1.6 also allows dynamically adding new resources at runtime, so
  5. -- parsing project_db.dat will no longer be a viable way to get all resource IDs
  6. --
  7. -- Originally created by llamazing, modified here
  8. --   Source: http://forum.solarus-games.org/index.php/topic,1256.msg7443.html#msg7443
  9.  
  10. local function get_resource_ids(resource_type)
  11.   local all_resources = {}
  12.  
  13.   local env = {}
  14.   env[resource_type] = function(properties)
  15.       local id = properties.id
  16.       assert(type(id)=="string", "id must be a string")
  17.  
  18.       table.insert(all_resources, id)
  19.   end
  20.  
  21.   setmetatable(env, {__index = function() return function() end end})
  22.  
  23.   local chunk = sol.main.load_file("project_db.dat")
  24.   setfenv(chunk, env)
  25.   chunk()
  26.  
  27.   return all_resources
  28. end
  29.  
  30. -- Only use this polyfill if Solarus < 1.6
  31. if not sol.main.get_resource_ids then
  32.   sol.main.get_resource_ids = get_resource_ids
  33. end
  34.  

Added to scripts/features.lua:
Code: Lua
  1. require("scripts/polyfills/get_resource_ids")

It totally works!

Code: [Select]
> for _, item in ipairs(sol.main.get_resource_ids("item")) do print(item) end
stick
trade
vacuum
whiskey
> for _, map in ipairs(sol.main.get_resource_ids("map")) do print(map) end
0000
Water_Dungeon_final
bar
beach
beach_house_1
beach_house_2
beach_house_3
...
« Last Edit: October 26, 2018, 05:29:37 pm by alexgleason »
RIP Aaron Swartz

alexgleason

  • Jr. Member
  • **
  • Posts: 72
  • Vegan on a Desert Island
    • View Profile
    • Vegan on a Desert Island
Re: NPC item picker script
« Reply #9 on: October 25, 2018, 09:59:58 pm »
I ended up with npc:prompt_item(callback, [filter])

scripts/npc_prompt_item.lua
https://gitlab.com/voadi/voadi/blob/07cfbfd037ba12545cc46fcada04c18b1deb0c88/data/scripts/npc_prompt_item.lua

scripts/menus/picker.lua
https://gitlab.com/voadi/voadi/blob/07cfbfd037ba12545cc46fcada04c18b1deb0c88/data/scripts/menus/picker.lua

When the menu starts, it loops through all the game's items and applies the filter (if given). If a filter isn't given, it uses a default filter which checks that item:is_key_item() == true (a quest-specific, custom property).

Thanks again llamazing (and Christopho) for your help. :)
« Last Edit: October 28, 2018, 12:48:50 am by alexgleason »
RIP Aaron Swartz

llamazing

  • Full Member
  • ***
  • Posts: 139
    • View Profile
Re: NPC item picker script
« Reply #10 on: October 26, 2018, 05:19:07 am »
Nice! Using llamazing's code I created a polyfill for the feature.

scripts/get_resource_ids.lua:
Code: Lua
  1. -- Polyfill for sol.main.get_resource_ids(resource_type)
  2. --   See: http://www.solarus-games.org/doc/1.6/lua_api_main.html#lua_api_main_get_resource_ids
  3. --
  4. -- Originally created by llamazing
  5. --   Source: http://forum.solarus-games.org/index.php/topic,1256.msg7443.html#msg7443
  6.  
  7. function sol.main.get_resource_ids(resource_type)

btw, you should change line 7 so that it doesn't overwrite the real get_resource_ids() function once v1.6 is released. Something like the following:
Code: Lua
  1. sol.main.get_resource_ids = sol.main.get_resource_ids or function(resource_type)

That way if sol.main.get_resource_ids is already defined (as a function) then the first half of the conditional evaluates to true and the second half of the conditional won't be evaluated (thus your custom function is ignored and the actual API function won't be overwritten).

Note that v1.6 also allows dynamically adding new resources at runtime, so parsing the project_db.dat will no longer be a viable way to get all resource ids: issue #630

alexgleason

  • Jr. Member
  • **
  • Posts: 72
  • Vegan on a Desert Island
    • View Profile
    • Vegan on a Desert Island
Re: NPC item picker script
« Reply #11 on: October 26, 2018, 05:30:42 pm »
btw, you should change line 7 so that it doesn't overwrite the real get_resource_ids() function once v1.6 is released.
Note that v1.6 also allows dynamically adding new resources at runtime, so parsing the project_db.dat will no longer be a viable way to get all resource ids: issue #630

Updated my post above. Thanks!
RIP Aaron Swartz