A recent change to Doom RL is the ability to key-bind custom lua scripts. To stop the possibility of using these scripts to cheat, the modding/engine API is not available. This means new safe APIs have to be created to increase scope of customization options.
Currently, there is only a single function in the inventory management API:
function command.use_item(STRING item_id) -> BOOLEAN
-- Searches the inventory for an item with the given id.
-- If the item is found, it is used and the function returns true.
-- Otherwise, the function returns false.
What follows is my proposal to expand the inventory management API.
Item Inspection:
A safe api cannot give players references to actual items, as the items could then be modified. I recommend returning instead a pure-lua table (created on-the-fly for each call) that contains item properties that are visible to the player. The names of the properties are generally the same as the name used by the engine so that modders won't have to relearn anything. Quite possibly even more properties could be added, but for now, I'm just including properties that aren't hidden and would be useful in keybindings. Preferably, these values would be case-insensitive.
The downside of the (obvious) implementation of this is that item reference will become invalid after the inventory
changes. This hurts the ease of persistence.
Global properties (for all items):
STRING id -- The sid of the item. (e.g. "smed" for small med packs. A full list is available on the wiki.)
STRING name -- The displayed name of the item (e.g. "modified pistol").
ITEMTYPE itype -- Indicates whether the item is boots, armor, ranged weapon, etc.
Equipment properties:
TABLE mods -- A table mapping mod numbers to quantities for mods that have been applied to the equipment. (e.g. {A = 2, T = 1})
Ranged weapon properties:
STRING ammoid -- The sid of the ammo item used by the weapon.
INTEGER ammo -- The amount of currently loaded ammo.
INTEGER ammomax -- The magazine size of the weapon.
INTEGER acc -- The accuracy bonus of the weapon.
INTEGER damage_dice -- The X of XdY.
INTEGER damage_sides -- The Y of XdY.
INTEGER blastradius -- The explosion size of the weapon. (0 for non-explosive weapons.)
INTEGER shots -- The number of shots fired in a burst (e.g. the Z of (XdY)xZ)
INTEGER shotcost -- The amount of ammo required to fire each shot. (e.g. 40 for the BFG 9000)
INTEGER reloadtime -- The number of 0.1s intervals needed to reload the weapon. (Not including trait bonuses)
INTEGER usetime -- The number of 0.1s intervals needed to fire the weapon. (Not including trait bonuses)
LOCATION location -- [technical] A special table indicating the item's location in the inventory/equipment so that functions will be able to use the table (e.g. {"inventory", index = 5}).
Armor/boots properties:
INTEGER armor -- The amount of protection the armor provides (at full durability).
INTEGER durability -- The current % durability of the armor.
INTEGER maxdurability -- The maximum % durability of the armor.
INTEGER movemod -- The movement speed bonus/penalty of the armor (in %)
INTEGER knockmod -- The knockback bonus/penalty of the armor (in %)
Ammo properties:
INTEGER ammo -- The amount of ammo in the stack.
INTEGER ammomax -- The maximum amount of ammo that would fit in the stack (ignoring Backpack)
Functions:
command.get_weapon() -> ITEM
-- Returns the player's currently equipped weapon (or nil if none is equipped).
command.get_prepared() -> ITEM
-- Returns the player's prepared item.
command.get_armor() -> ITEM
-- Returns the player's equipped armor.
command.get_boots() -> ITEM
-- Returns the player's equipped boots.
command.get_slot(EQUIP_SLOT slot) -> ITEM
-- Returns one of the player's equipped items depending on the given slot constant.
command.get_inv_item(INTEGER index) -> ITEM
-- Returns the item at the given index in the player's inventory (or nil if the slot is empty).
command.get_inv_item(STRING sid) -> ITEM, ...
-- Returns all the items in the player's inventory with the given sid (or nil if no such items exists).
command.get_inv_size() -> INTEGER
-- Returns the player's maximum inventory size (which can be different in AoLT).
command.inventory() -> ITERATOR of ITEM
-- Returns an iterator over the player's inventory items.
command.inv_is_full() -> BOOLEAN
-- Returns whether the player's inventory is full.
command.count_ammo(STRING sid) -> INTEGER
-- Counts the total amount of ammo of the given type in the player's inventory. Doesn't count ammo that
-- is loaded into weapons.
Equipment management:
There need to be functions to equip items. These functions should interact nicely with the inspection API.
Functions:
command.equip(ITEM item, EQUIP_SLOT slot) -> BOOLEAN
-- Equips the given item in the given equipment slot.
-- Slot is optional; if omitted, the default slot for the item is used.
-- The return value is true if the item can be successfully equipped in the slot.
-- An INTEGER or STRING can be used instead of ITEM. In this case, the equipped item
-- will correspond to command.get_inv_item.
command.unequip(EQIUP_SLOT slot) -> BOOLEAN
-- Unequips the item in the given slot and returns true.
-- If the slot was empty or the inventory was full or the item was cursed, returns false.
command.swap()
-- Quickswaps the player's weapon and prepared slots.
Inventory management:
Besides commands to shuffle equipment, there should be commands to manage the inventory slots.
command.use_item(ITEM item) -> BOOLEAN
-- Uses the given item; returns true whenever the item could be found and used.
-- An INTEGER or STRING can be used instead of ITEM. In this case, the used item will
-- correspond to command.get_inv_item.
-- (This means that the new version is backwards-compatible.)
-- (Given nil, this will open the use menu.)
command.drop(ITEM item) -> BOOLEAN
-- Drops the given item; returns true if the item can be found and dropped.
-- As command.use_item, an INTEGER or STRING can be used for item.
-- (Given nil, this will open the drop menu.)
command.unload(ITEM item) -> BOOLEAN
-- Unloads the given item; returns true if the item can be found and unloaded.
-- As command.use_item, an INTEGER or STRING can be used for item.
-- (Given nil, this will open the unload menu.)
Example usage:
-- This keybinding function equips a green armor from the inventory.
function()
if not command.equip("garmor") then
ui.msg("You don't have any green armor!")
end
end
-- This keybinding function equips the armor with highest protection.
function()
local function calculate_protection(item)
local protection = item.armor
if protection == 0 then
return 0
end
if item.durability == 0 then
return 0
end
if item.durability <= 25 then
protection = math.floor(protection / 4)
elseif item.durability <= 49 then
protection = math.floor(protection / 2)
end
return math.max(1, protection)
end
local best_armor = nil
local best_protection = 0
local current_armor = command.get_armor()
if current_armor then
best_protection = calculate_protection(current_armor)
end
for item in command.inventory() do
if item.itype == ITEMTYPE_ARMOR then
local protection = calculate_protection(item)
if protection > best_protection then
best_armor = item
best_protection = protection
end
end
end
if best_armor then
if not command.equip(best_armor) then
ui.msg("You can't equip your most protective armor!")
end
else
ui.msg("You are already wearing your most protective armor!")
end
end
-- This keybinding function will equip a loaded shotgun from the inventory/prepared slot.
-- (It is designed to help players who use several shotguns to bypass reload time before
-- finding a combat shotty.)
function()
local weapon = command.get_weapon()
if weapon and weapon.sid == "shotgun" and weapon.ammo == 1 then
ui.msg("You are already wielding a loaded shotgun!")
return
end
local prepared = command.get_prepared()
if prepared and prepared.sid == "shotgun" and prepared.ammo == 1 then
command.swap()
return
end
for item in command.inventory() do
if item.sid == "shotgun" and item.ammo == 1 then
command.equip(item)
return
end
end
ui.msg("You don't have any loaded shotguns!")
end
-- As a sister to the above function, this command function will help reload all the shotguns
-- after firing. (Just keep using it until nothing happens.)
function()
local weapon = command.get_weapon()
if weapon and weapon.sid == "shotgun" and weapon.ammo == 0 then
command.reload()
return
end
local prepared = command.get_prepared()
if prepared and prepared.sid == "shotgun" and prepared.ammo == 0 then
command.swap()
return
end
for item in command.inventory() do
if item.sid == "shotgun" and item.ammo == 0 then
command.equip(item)
return
end
end
ui.msg("All of your shotguns are loaded and ready for action!")
end
-- This command is designed to swap in/out ammopacks for the currently equipped weapon.
-- (Ammopacks are a new feature that is currently in beta.)
-- Swapping them out is a bit imperfect, as the command may not swap back in the exact same
-- weapon that was previously prepared if two weapons have the same sid.
do
local last_prepared_sid = nil
function()
local weapon = command.get_weapon()
if not weapon then
ui.msg("You don't have a weapon equipped!")
return
end
local prepared = command.get_prepared()
if prepared and prepared.itype == ITEMTYPE_AMMOPACK and prepared.ammoid == weapon.ammoid then
-- The appropriate ammo pack is already equipped ... swap it out!
if last_prepared_sid then
command.equip(last_prepared_sid, SLOT_PREPARED)
else
if not command.unequip(SLOT_PREPARED) then
ui.msg("You don't have enough inventory space to unequip your ammo pack!")
end
end
else
if prepared then
last_prepared_sid = prepared.sid
end
command.equip("p" .. weapon.ammoid)
end
end
end
Remarks:
It is possible some of these commands could be metatabled for simpler access. Any comments and suggestions for this API are welcome, but keep in mind that this proposal is focused on inventory and equipment; interacting with the map is an entirely different monster.
Probably the biggest concern would be the inspection design. Creating tables on the fly isn't efficient, but it is reasonably flexible and very safe. Efficiency shouldn't be too much of a concern, since the player interaction part of the code is pretty non-critical for speed purposes. The bigger concern is persistence; it would be nice to be able to save an item reference and have its properties updated seamlessly by the engine (as would be the case with a read-only reference to the real item). However, the engine's representation of items doesn't store their location (at least in terms of equipment/inventory) which would be a problem (or at least source of confusion) with this alternate possibility. Probably this could be worked around, but it would be a pain.