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:
(https://coinsh.red/p/e7y1b-npawf.gif)
My game revolves a lot around trading items, so it's important that this is streamlined. It allows writing code like this:
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
I'm not sure I organized my code the best way, but here it is:
scripts/menus/picker.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
scripts/npc_ask_item.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)
Finally, I've just added the script to features.lua:
require("scripts/npc_ask_item") -- NPC item picker menu
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?
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?
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,
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()
)
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.
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.
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:
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
Then you can use game:get_item(item_name) to convert the item names into item objects.
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:
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.
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 (http://lua-users.org/wiki/EnvironmentsTutorial). I'm guessing Solarus runs on an older version?
Note that Solarus 1.6 will have a new feature sol.main.get_resource_ids("item").
Quote from: Christopho on October 25, 2018, 09:50:40 AM
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:
-- Polyfill for sol.main.get_resource_ids(resource_type)
-- See: http://www.solarus-games.org/doc/1.6/lua_api_main.html#lua_api_main_get_resource_ids
--
-- Note that v1.6 also allows dynamically adding new resources at runtime, so
-- parsing project_db.dat will no longer be a viable way to get all resource IDs
--
-- Originally created by llamazing, modified here
-- Source: http://forum.solarus-games.org/index.php/topic,1256.msg7443.html#msg7443
local function get_resource_ids(resource_type)
local all_resources = {}
local env = {}
env[resource_type] = function(properties)
local id = properties.id
assert(type(id)=="string", "id must be a string")
table.insert(all_resources, 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_resources
end
-- Only use this polyfill if Solarus < 1.6
if not sol.main.get_resource_ids then
sol.main.get_resource_ids = get_resource_ids
end
Added to
scripts/features.lua:
require("scripts/polyfills/get_resource_ids")
It totally works!
> 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
...
I ended up with npc:prompt_item(callback, [filter]) (https://gitlab.com/voadi/voadi/wikis/docs/lua-api#npcprompt_itemcallback-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. :)
Quote from: alexgleason on October 25, 2018, 05:16:44 PM
Nice! Using llamazing's code I created a polyfill for the feature.
scripts/get_resource_ids.lua:
-- Polyfill for sol.main.get_resource_ids(resource_type)
-- See: http://www.solarus-games.org/doc/1.6/lua_api_main.html#lua_api_main_get_resource_ids
--
-- Originally created by llamazing
-- Source: http://forum.solarus-games.org/index.php/topic,1256.msg7443.html#msg7443
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:
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 (https://gitlab.com/solarus-games/solarus/issues/630)
Quote from: llamazing on October 26, 2018, 05:19:07 AM
btw, you should change line 7 so that it doesn't overwrite the real get_resource_ids() function once v1.6 is released.
Quote from: llamazing on October 26, 2018, 05:19:07 AM
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 (https://gitlab.com/solarus-games/solarus/issues/630)
Updated my post above. Thanks!