Community > Your scripts

NPC item picker script

(1/3) > >>

alexgleason:
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) ---local bunny = map:get_entity("bunny1")

function bunny:on_interaction()
  game:start_dialog("bunny.want_carrot", function()
    bunny:ask_item(function(item)
      if item and item:get_name() == "carrot"
        item:set_variant(0)
        game:start_dialog("bunny.thanks_for_carrot")
      elseif item then
        -- a non-carrot item is given
        game:start_dialog("bunny.no_thanks")
      else
        -- The action was cancelled (item == nil). Do nothing.
      end
    end)
  end)
end
--- End code ---

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

scripts/menus/picker.lua:

--- Code: ( lua) ----- ♡ Copying is an act of love. Please copy and share.

-- This menu ultimately exists to let you call npc:ask_item()
-- It's designed to be called from npc:ask_item() and really cannot stand alone

require("scripts/multi_events")

-- Items that could appear in this menu
local key_items = {
  'trade',
  'whiskey',
}
local player_items = {} -- Items the player actually has


-- Get the XY for an item sprite based on its position
local function get_slot_xy(i)
  local mx, my = 24, 32 -- offset from menu origin

  local r = math.ceil(i/4) -- row of this slot
  local ix = ((i-1)%4)*24
  local iy = (r-1)*24

  local x = mx + ix
  local y = my + iy

  return x, y
end


local picker = {} -- item picker menu


function picker:initialize()
  local font, font_size = "Comicoro", 16

  -- Graphics
  self.menu = sol.surface.create(144, 104)
  self.menu_bg = sol.sprite.create("menus/menu_bg")
  self.item_sprite = sol.sprite.create("entities/items")
  self.cursor_sprite = sol.sprite.create("menus/cursor")

  -- Player sprite
  self.player_sprite = sol.sprite.create("main_heroes/rachel")
  self.player_sprite:set_animation("walking")
  self.player_sprite:set_direction(3)

  -- NPC sprite
  self.npc_sprite = nil

  -- Question text
  self.title = sol.text_surface.create({text_key="menus.picker.question", font=font, font_size=font_size})
  self.title:set_color({0, 0, 0})

  -- Nope button
  self.nope_button = sol.surface.create(32, 16)
  self.nope_button:fill_color({198, 34, 0})
  local button_text = sol.text_surface.create({text_key="menus.picker.nope", font=font, font_size=font_size})
  button_text:draw(self.nope_button, 4, 8)
end


-- Called when the picker menu starts
function picker:on_started()
  sol.main.game:get_hero():freeze()
  sol.main.game._picker_enabled = true

  -- Set items the player has
  player_items = {}
  for i, v in ipairs(key_items) do
    if sol.main.game:has_item(v) then
      table.insert(player_items, v)
    end
  end

  -- Graphics
  self.npc_sprite = sol.sprite.create(self._npc:get_sprite():get_animation_set())
  self.npc_sprite:set_direction(3)

  -- Initialize properties
  self.cursor = 1
  self._selection = nil
end


-- Set/get the selected item (when "action" is pressed)
function picker:set_selection(item)
  self._selection = item
end

function picker:get_selection()
  return self._selection
end


-- Called every frame
function picker:on_draw(dst_surface)

  -- Draw BG graphics
  self.menu:draw(dst_surface, 56, 16)
  self.menu_bg:draw(self.menu)

  -- Draw characters
  local x, y
  x, y = self.player_sprite:get_origin()
  self.player_sprite:draw(self.menu, 24+x, 8+y) -- draw player sprite
  x, y = self.npc_sprite:get_origin()
  self.npc_sprite:draw(self.menu, 104+x, 8+y) -- draw NPC sprite

  -- Draw question
  self.title:draw(self.menu, 49, 16)

  -- Draw items, loop through inventory
  for i, item in ipairs(player_items) do
    if sol.main.game:has_item(item) then
      self.item_sprite:set_animation(item) -- all items are in one sheet
      local x, y = get_slot_xy(i) -- item slot XY
      local ox, oy = self.item_sprite:get_origin() -- origin offset
      self.item_sprite:draw(self.menu, x+ox, y+oy) -- draw item
      if self.cursor == i then
        self.cursor_sprite:draw(self.menu, x, y) -- draw cursor
      end
    end
  end

  -- Draw cancel button
  self.nope_button:draw(self.menu, 56, 80)
  if self.cursor == #player_items+1 then
    self.cursor_sprite:draw(self.menu, 64, 80) -- cancel button cursor
  end
end


-- Called when a button is pressed
function picker:on_command_pressed(command)

  -- D-Pad controls
  if command == "up" then
    self.cursor = self.cursor - 4 -- up and down navigate between rows
  elseif command == "down" then
    self.cursor = self.cursor + 4
  elseif command == "left" then
    self.cursor = self.cursor - 1
  elseif command == "right" then
    self.cursor = self.cursor + 1
  end

  -- Cursor must be between 1 and #player_items+1 (cancel)
  self.cursor = math.max(1, self.cursor)
  self.cursor = math.min(self.cursor, #player_items+1)

  -- Handle selection
  if command == "action" then
    if self.cursor ~= #player_items+1 then
      local item = sol.main.game:get_item(player_items[self.cursor])
      self:set_selection(item)
    end
    sol.menu.stop(self)
    sol.main.game:get_hero():unfreeze()
    sol.main.game._picker_enabled = false
  end

  return true
end


-- Initialize picker when the game starts
local game_meta = sol.main.get_metatable("game")
game_meta:register_event("on_started", function(self)
  picker:initialize()
end)


return picker

--- End code ---

scripts/npc_ask_item.lua:

--- Code: ( lua) ----- ♡ Copying is an act of love. Please copy and share.

require("scripts/multi_events")
local picker = require("scripts/menus/picker")


-- Add npc:ask_item(callback)

local npc_meta =  sol.main.get_metatable("npc")

-- Start a picker menu for the NPC
function npc_meta:ask_item(callback)
  picker._npc = self
  sol.menu.start(self:get_map(), picker)
  function picker:on_finished()
    callback(self:get_selection())
    picker._npc = nil
    self.on_finished = nil -- destroy this function between calls
  end
end


-- Initialize game with picker menu

local game_meta = sol.main.get_metatable("game")

-- Check whether a picker is already active
function game_meta:is_picker_enabled()
  return self._picker_enabled
end

-- Initialize game with _picker_enabled attribute
game_meta:register_event("on_started", function(self)
  self._picker_enabled = false
end)

--- End code ---

Finally, I've just added the script to features.lua:

--- Code: ( lua) ---require("scripts/npc_ask_item") -- NPC item picker menu
--- End code ---

llamazing:
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:

--- Quote from: llamazing 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?

--- End quote ---

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) ---local function filter(item)
  if item:get_name() == "bad_item" then
    return false
  end
  return item:is_key_item()
end

npc:ask_item(filter, function(item)
  print(item:get_name()
)
--- End code ---


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.

llamazing:

--- Quote from: alexgleason on October 25, 2018, 03:15:55 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.

--- End quote ---

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) ---local function get_all_items()
    local all_items = {}

    local env = {}
    function env.item(properties)
        local id = properties.id
        assert(type(id)=="string", "item id must be a string")

        table.insert(all_items, id)
    end

    setmetatable(env, {__index = function() return function() end end})

    local chunk = sol.main.load_file("project_db.dat")
    setfenv(chunk, env)
    chunk()

    return all_items
end
--- End code ---

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

alexgleason:

--- Quote from: llamazing on October 25, 2018, 04:32:32 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:

--- End quote ---

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.

Navigation

[0] Message Index

[#] Next page

Go to full version