Making an Inventory - Part 3
Ok folks, time to wrap this up. The plan for today is to implement drag and drop capabilities, allowing us to move an item from pack space to equipped space. We will also want to display a tool tip panel on item hover to show details about the item.
The first thing I need to do is a bit of clean up. The popButton function needs to be made more flexible to support handling of different types of elements. We need to move these out of the menu document and into the common elements doc, which is a collection of general things which are commonly used by many other documents.
and so this:
REM file: guiDoc_gameMenu.dba
function _gui_gameMenu_popBtn(e as integer)
_gui_elementStyle_setProp(e, "background-image", gui_Elements(e).name + "-pop.png")
endfunction
function _gui_gameMenu_unpopBtn(e as integer)
_gui_elementStyle_setProp(e, "background-image", gui_Elements(e).name + ".png")
endfunction
function _gui_gameMenu_pushBtn(e as integer)
_gui_elementStyle_setProp(e, "background-image", gui_Elements(e).name + "-push.png")
endfunction
becomes this:
REM file: guiDoc_common.dba
function _gui_hoverElement(e as integer)
select gui_Elements(e).tag
case "button":
_gui_elementStyle_setProp(e, "background-image", gui_Elements(e).name + "-pop.png")
endcase
case "icon":
_gui_elementStyle_setProp(e, "background-image", gui_Elements(e).name + "-pop.png")
`TODO: show tooltip
endcase
case "socket":
`TODO: show drop allowed/not allowed feedback
endcase
endselect
endfunction
function _gui_unhoverElement(e as integer)
select gui_Elements(e).tag
case "button":
_gui_elementStyle_setProp(e, "background-image", gui_Elements(e).name + ".png")
endcase
case "icon":
_gui_elementStyle_setProp(e, "background-image", gui_Elements(e).name + ".png")
`TODO: hide tooltip
endcase
case "socket":
_gui_elementStyle_setProp(e, "background-image", gui_Elements(e).name + ".png")
endcase
endselect
endfunction
function _gui_pressElement(e as integer)
select gui_Elements(e).tag
case "button":
_gui_elementStyle_setProp(e, "background-image", gui_Elements(e).name + "-push.png")
endcase
case "icon":
`TODO: pickup and start drag.
endcase
endselect
endfunction
function _gui_releaseElement(e as integer)
select gui_Elements(e).tag
case "icon":
`TODO: drop draggable onto new socket if valid, else revert to original location
endcase
endselect
endfunction
Change callback references to these functions as appropriate. Would not hurt to go ahead and compile + run to verify that nothing is still trying to call the old non-existant functions.
Next, remember that private gui stuff that was sort of hacked into the character module to get things working in part 2? Let's move that back into the gui module and access it through a proper public function.
REM file: characters.dba
function chars_addToInventory(reqTarget as string, reqChar as integer)
... snip ...
`TODO: this private function should really not be called here, move this to new public functions gui_dragElement / gui_dropElement within the gui core module. These can also be used to allow the player to move other gui elements like the inventory panel, or a popup message box as they wish.
`TODO: convert gui_Elements to private, it really shouldn't be modified directly outside of the ui module and controllers.
_gui_elementStyle_setProp(guiSlot, "background-image", "media/ui/icon-" + chars_Inventories().itemBase + "-lg.png")
gui_Elements(guiSlot).name = "media/ui/icon-" + chars_Inventories().itemBase + "-lg"
gui_Elements(guiSlot).enableEvents = 1
gui_Elements(guiSlot).onMouseIn = "_gui_hoverElement"
gui_Elements(guiSlot).onMouseOut = "_gui_unhoverElement"
... snip ...
endfunction
becomes:
REM file: characters.dba
function chars_addToInventory(reqTarget as string, reqChar as integer)
... snip ...
gui_makeIcon("item", chars_Inventories().itemBase, guiSlot)
... snip ...
endfunction
REM file: guiDoc_common.dba
function gui_makeIcon(reqType as string, nameBase as string, reqSocket as integer)
select reqType
case "item":
`parameters for makeElement(tag as string, id as string, name as string, parentID as string, class as string)
e = _gui_makeElement("icon", "itemIcon-" + nameBase, "media/ui/icon-" + nameBase + "-lg", gui_Elements(reqSocket).id, gui_Elements(reqSocket).class)
_gui_elementStyle_setProp(e, "background-image", "media/ui/icon-" + nameBase + "-lg.png")
gui_Elements(e).enableEvents = 1
gui_Elements(e).onMouseIn = "_gui_hoverElement"
gui_Elements(e).onMouseOut = "_gui_unhoverElement"
gui_Elements(e).onPress = "_gui_pressElement"
gui_Elements(e).onRelease = "_gui_releaseElement"
endcase
case "ability":
`TODO: ^
endcase
endselect
endfunction
Yes, we still need to rename gui_Elements to _gui_Elements, but that is a fairly sweeping change throughout many files and is not something I will tackle today. I've also left a stub for Ability icons, obviously items won't be the only type of icon we may want to create in the future. By setting the function up as a switch statement, it becomes easily extensible for future needs. What other sorts of things may need an interactable icon? Effects perhaps. Party member selection? Who knows! Whatever we may come across can now just slide right in.
So, let's do a quick compile and check that nothing was broken in the restructuring. We should basically see that everything still works just as it did at the end of part 2: open inventory with blank slots, pickup the dagger, see slot 1 filled with the dagger's icon. dagger icon should give visual feedback on mouse hover.
Good, good. Next, let's turn our attention to the onPress event. When we click and hold an icon, we will want to pick it up and bind it's position to the mouse position. Now getting the position to continually update with the mouse position isn't tricky in and of itself, but getting it to do so within the context of the framework's gui update is a bit more so. There is currently no handling for this sort of positioning and we will need to expand the core ui module for this. As with many thing, there are any number of ways to do this, from assigning a specialized class or converting the element's type, to adding a new element property, or a new UDT, to modifying an existing property with new functionality.
I think I am going to go with a combination of a new UDT and modifying the existing enableEvents property, there are a couple of reasons for this. This property is already of type byte giving us allowable values 0-255 of which I am currently using only 0-2. Additionally, an icon's enableEvent value is currently 1 which means that it will react to an assigned hotkey press/release and to mouse events such as onMouseIn, onMouseOut, onPress, onRelease and the like. Inventory slots are currently event type 0 which means, not enabled. Under the current types, we would need to make slots type 1, the same as the icons, and which is always active. Left as is, this could cause problems for drag and drop as in order to pickup or drop an icon, the mouse will have to be over 2 event enabled elements at the same time: the icon and the drop socket. We are already going to have to add some sort of event ordering or layering to handle this.
So, to start off with, let's set socket elements which are capable of receiving a dropped element as enableEvents type 3. These sockets should only become active while an element is being dragged. Further, lets bind the actively dragging element to a new global and type as we will need some specialized data handling for this action.
REM file: guiDoc_gameHUD.xml
<element
id="inventory-slot0"
type="socket"
class="icon-inventorySlot-lg"
name="media/ui/icon-inventory-lg"
enableEvents="3"
onMouseIn="_gui_hoverElement"
onMouseOut="_gui_unhoverElement">
<element_style
top="0px">
</element_style>
</element>
... update the other slots likewise ...
REM file: user_interface.dba
type uiDragElementData
isActive as boolean
id as integer
offsetX as integer
offsetY as integer
endtype
`let this new type extend the existing UI object:
type uiData
status as uiStatusData
dragElement as uiDragElementData
endtype
GLOBAL UI as uiData
function framework_update_getInputUI()
... snip, pause and focus hold handling ...
for e = 0 to array count(gui_Elements())
`old, change this: if gui_Elements(e).enableEvents = 1 and gui_Elements(e).resolvedStyle.display <> "none"
if (gui_Elements(e).enableEvents = 1 or (gui_Elements(e).enableEvents = 3 and UI.dragElement.isActive = TRUE)) and gui_Elements(e).resolvedStyle.display <> "none"
... snip, check for and process mouse events (in, out, press, release) ...
endif
next e
... snip, check and process keyboard events ...
endfunction
Next, we need to go back to our onPress callback and capture some data on icon pickup.
REM file: guiDoc_common.dba
function _gui_pressElement(e as integer)
select gui_Elements(e).tag
case "button":
_gui_elementStyle_setProp(e, "background-image", gui_Elements(e).name + "-push.png")
endcase
case "icon":
_gui_elementStyle_setProp(e, "background-image", gui_Elements(e).name + ".png")
`pick up the icon
UI.dragElement.isActive = TRUE
UI.dragElement.id = e
UI.dragElement.offsetX = mouseX() - gui_Elements(e).resolvedStyle.finalX
UI.dragElement.offsetY = mouseY() - gui_Elements(e).resolvedStyle.finalY
endcase
endselect
endfunction
Now, when resolving the element's final position, we need to use the mouse position instead of the style position if the current update element matches the dragElement while drag isActive = TRUE
REM file: user_interface.dba
function _gui_elementStyle_resolveFinalValues(e, p)
... snip, resolve as normal, append drag check to the end ...
`handle dragging element
if UI.dragElement.isActive and UI.dragElement.id = e
finalX = mouseX() - UI.dragElement.offsetX
finalY = mouseY() - UI.dragElement.offsetY
endif
gui_Elements(e).resolvedStyle.finalX = finalX
gui_Elements(e).resolvedStyle.finalY = finalY
gui_Elements(e).resolvedStyle.resolvedState = 2
endfunction
Ok, I realize that a lot of code is getting snipped out when dealing with user_interface.dba, I suppose this may cause things to seem a little disjointed perhaps, but really it is not needed to know the exact manner in which final screen coordinate values are being resolved, only that they have been, as far as adding this drag and drop functionality is concerned. The core UI code is quite long for one thing, and is not cleaned up or polished for presentation. It is a bit beyond the scope of these Inventory sessions. Hopefully, the overall concept of what is being done is clear enough. If not, I'm happy to answer questions or give more specific details.
Anyway, we are now at a point where we should be able to click and drag an icon around the screen, if we drop it, it should revert back to its original location.
Next, as we drag over droppable sockets, we need to check if the icon is allowed to drop on this socket, and give some feed back to the user about this. This will be done in the onMouseIn event for the socket in question, which specifically is a callback to the function _gui_hoverElement(e)
For this, we are going to need to know a bit more about the type of socket the mouse is over. We can get this by parsing the element's id which is already in the form "inventory-slot0", "inventory-equipped-body" and so on. So our hoverElement function will now look like this:
REM file: guiDoc_common.dba
function _gui_hoverElement(e as integer)
select gui_Elements(e).tag
case "button":
_gui_elementStyle_setProp(e, "background-image", gui_Elements(e).name + "-pop.png")
endcase
case "icon":
_gui_elementStyle_setProp(e, "background-image", gui_Elements(e).name + "-pop.png")
`TODO: show tooltip
endcase
case "socket":
split string gui_Elements(e).id, "-"
select get split word$(1)
case "inventory":
if get split word$(2) = "equipped"
`active equipment
select get split word$(3)
case "back": : endcase
case "body": : endcase
case "neck": : endcase
case "accessory": : endcase
case "rHand": : endcase
case "lHand": : endcase
endselect
else
`general packspace
slotNum = intval(mid$(get split word$(2), 4, 0))
endif
endcase
case "ability":
endcase
endselect
endcase
endselect
endfunction
We also need to know more about the type of item the icon represents. This is a good time to go ahead and query up all the info we need for the tool tip display since we are going to be querying the database anyways. We already have a primary key reference stored as chars_Inventories().itemID for fast querying.
So, continue to expand the hoverElement function, but this time we will work with icons instead of sockets:
REM file: guiDoc_common.dba
function _gui_hoverElement(e as integer)
select gui_Elements(e).tag
case "button":
_gui_elementStyle_setProp(e, "background-image", gui_Elements(e).name + "-pop.png")
endcase
case "icon":
_gui_elementStyle_setProp(e, "background-image", gui_Elements(e).name + "-pop.png")
`get tooltip data
tDB = database_getID("data/items.db")
qry$ = "SELECT * FROM itemMaster WHERE itemID = '" + str$(chars_Inventories( ..... !!!
ah ha! we've got a problem here don't we. As our inventory array currently stands, all characters' inventories are mixed together. we can't easily iterate through it to retrieve our itemID as our hover function doesnt really know anything about the item that the icon is representing. As usual, Matrix1 provides a helpful method. Free lists! So we need to take a quick detour and rework a bit of the character module.
First, setup a new free list to handle our bag slots, and store a reference to it. We can stick it in to our handles data which stores references to various resource data. Next, block out the slots that the free list will handle, then modify the chars_addToInventory() function.
REM file: characters.dba
type charHandleData
myAnimationSet as integer `index of my animation sequence map
myInventoryList as integer `id of my inventory free list.
endtype
type inventoryData
charID as integer
itemID as integer
slotID as integer
itemBase as string
endtype
dim chars_Inventories() as inventoryData
function chars_addCharacter()
... snip, insert and other initializations ...
`reserve 10 inventory slots for this character
tList = find free freelist()
make freelist tList, array count(chars_Inventories()) + 1, 10, -1
Chars().handle.myInventoryList = tList
for s = 0 to 9
array insert at bottom chars_Inventories()
chars_Inventories().charID = cID
chars_Inventories().slotID = s
chars_Inventories().itemID = -1
next s
endfunction cID
function chars_addToInventory(reqTarget as string, reqChar as integer)
tSlot = get from freelist(Chars(reqChar).handle.myInventoryList)
if tSlot > -1
if reqTarget = "interactableTarget"
`item is the current world interactive target, add to inventory data
for s = 0 to array count(chars_Inventories())
if chars_Inventories(s).charID = reqChar and chars_Inventories(s).slotID = tSlot
chars_Inventories(s).itemID = WORLD.interactableTarget.itemID
exit
endif
next s
`to derive the item base from the base file path, we want everything to the right of the last / and before the first _ modifier
startPos = last instr(WORLD.interactableTarget.fileBase, "/")
endPos = instr(WORLD.interactableTarget.fileBase, "_", startPos) - 1
chars_Inventories(s).itemBase = mid$(WORLD.interactableTarget.fileBase, (startPos + 1), (endPos - startPos))
`exclude item from the world - no need to delete, we can just unexclude and resume updating it on equip or drop.
exclude object on WORLD.interactableTarget.obj
sc_setObjectCollisionOff WORLD.interactableTarget.obj
else
`add some other arbitrary item to some arbitrary character, we won't implement this now but it is nice to leave the option available for later.
`TODO: ^
endif
`update the inventory gui
guiSlot = gui_getElementById("inventory-slot" + str$(tSlot))
gui_makeIcon("item", chars_Inventories(s).itemBase, guiSlot)
else
`inventory is full, throw a message
gui_alert(" You can't take this now, your inventory is full! ", TRUE)
endif
endfunction
Another nice thing about this refactoring, is that we will now be able to move items around in bag space with gaps in between. You could now put something in slots 2, 7, and 9 where before this would cause problems when adding a new item.
Next, let's define our tool tip panel, this can be used to store and display our item details. For now, nothing fancy, we will just dump everything into a basic list. We can style it and art it up later.
REM file: guiDoc_gameHUD.xml
<class
name="panel-tooltip">
<class_style
width="200px"
height="150px"
top="-50px"
left="-180px"
padding="10px"
border-color="#000"
border-width="1px"
color="#fff"
background-color="#33000000"
display="none">
</class_style>
</class>
<class
name="panel-tooltip-allow">
<class_style
width="180px"
height="150px"
top="-50px"
left="-160px"
padding="10px"
border-color="#000"
border-width="1px"
color="#fff"
background-color="#550000ff">
</class_style>
</class>
<class
name="panel-tooltip-unallow">
<class_style
width="180px"
height="150px"
top="-50px"
left="-160px"
padding="10px"
border-color="#000"
border-width="1px"
color="#fff"
background-color="#55ff0000">
</class_style>
</class>
<element
id="panel-tooltip-active"
type="div"
class="panel-tooltip">
<element_style
z-index="10">
</element_style>
</element>
<element
id="panel-tooltip-compare"
type="div"
class="panel-tooltip">
<element_style
z-index="9">
</element_style>
</element>
Now we can try the hoverElement modification again. Let's also take this opportunity to move the more game-specific code back into guiDoc_gameHUD and leave this function as a more general and abstract helper, as this 'common' elements controller is meant to be non-specific and usable with little to no modification from project to project.
REM file: guiDoc_common.dba
function _gui_hoverElement(e as integer)
select gui_Elements(e).tag
case "button":
_gui_elementStyle_setProp(e, "background-image", gui_Elements(e).name + "-pop.png")
endcase
case "icon":
_gui_elementStyle_setProp(e, "background-image", gui_Elements(e).name + "-pop.png")
`get parent socket type
split string gui_Elements(gui_Elements(e).parent).id, "-"
select get split word$(1)
case "inventory":
_gui_gameHUD_setToolTip(e)
endcase
case "ability":
_gui_gameHUD_setToolTip(e)
`TODO: other processing ? check allowed to activate? updating background-image may need to be handled differently by type.
endcase
endselect
endcase
case "socket":
ttPanel = gui_getElementById("panel-tooltip-active")
if _gui_gameHUD_getDropAllowed(e)
_gui_elementStyle_setProp(e, "background-image", gui_Elements(e).name + "-allow.png")
gui_Elements(ttPanel).class = "panel-tooltip-allow"
else
_gui_elementStyle_setProp(e, "background-image", gui_Elements(e).name + "-unallow.png")
gui_Elements(ttPanel).class = "panel-tooltip-unallow"
endif
endcase
endselect
endfunction
REM file: guiDoc_gameHUD.dba
function _gui_gameHUD_setToolTip(e as integer)
split string gui_Elements(gui_Elements(e).parent).id, "-"
select get split word$(1)
case "inventory":
itemID = -1
if get split word$(2) = "equipped"
`active equipment
select get split word$(3)
case "back": itemID = Chars(MY_CHAR).equipped.back.id : endcase
case "body": itemID = Chars(MY_CHAR).equipped.body.id : endcase
case "neck": itemID = Chars(MY_CHAR).equipped.neck.id : endcase
case "accessory": itemID = Chars(MY_CHAR).equipped.accessory.id : endcase
case "rHand": itemID = Chars(MY_CHAR).equipped.rHand.id : endcase
case "lHand": itemID = Chars(MY_CHAR).equipped.lHand.id : endcase
endselect
else
`general packspace
slotNum = intval(mid$(get split word$(2), 4, 0))
for i = 0 to array count(chars_Inventories())
if chars_Inventories(i).charID = MY_CHAR and chars_Inventories(i).slotID = slotNum
itemID = chars_Inventories(i).itemID
exit
endif
next i
endif
`get item details
if itemID > -1
tDB = database_getID("data/items.db")
qry$ = "SELECT * FROM itemMaster WHERE itemID = '" + str$(itemID) + "'"
WRLOG.ui, "Execute Query: " + qry$ + NEWLINE
txt$ = " "
res = sqlite begin sql query(tDB, qry$)
totalRows = sqlite record row count(tDB)
if totalRows > 0
res = sqlite goto record row(tDB, 1)
tItemClass$ = sqlite record row string$(tDB, 1)
tItemBaseType$ = sqlite record row string$(tDB, 2)
txt$ = txt$ + sqlite record row string$(tDB, 3) + NEWLINE
txt$ = txt$ + sqlite record row string$(tDB, 4) + NEWLINE
else
gui_alert(" oops! no record found. {itemMaster}", FALSE)
endif
res = sqlite finish sql query(tDB)
select tItemClass$
case "Weapon":
qry$ = "SELECT * FROM weaponMaster WHERE weaponBaseType = '" + tItemBaseType$ + "'"
WRLOG.ui, "Execute Query: " + qry$ + NEWLINE
res = sqlite begin sql query(tDB, qry$)
totalRows = sqlite record row count(tDB)
if totalRows > 0
res = sqlite goto record row(tDB, 1)
tWeaponID = sqlite record row integer(tDB, 0)
txt$ = txt$ + "<" + sqlite record row string$(tDB, 1) + ">" + NEWLINE
txt$ = txt$ + "Mass Rating: " + str$(sqlite record row integer(tDB, 4)) + NEWLINE
txt$ = txt$ + "Size Rating: " + str$(sqlite record row integer(tDB, 5)) + NEWLINE
txt$ = txt$ + "Control Rating: " + str$(sqlite record row integer(tDB, 6)) + NEWLINE
`update tooltip
ttPanel = gui_getElementById("panel-tooltip-active")
gui_Elements(ttPanel).selectedIndex = tWeaponID
gui_Elements(ttPanel).name = tItemClass$
gui_Elements(ttPanel).value = txt$
gui_Elements(ttPanel).parent = e
_gui_elementStyle_setProp(ttPanel, "display", "inline")
else
gui_alert(" oops! no record found. {weaponMaster}", FALSE)
endif
res = sqlite finish sql query(tDB)
endcase
endselect
endif
endcase
case "ability":
endcase
endselect
endfunction
function _gui_gameHUD_getDropAllowed(e as integer)
tAllowed = FALSE
split string gui_Elements(e).id, "-"
select get split word$(1)
case "inventory":
if get split word$(2) = "equipped"
`active equipment - allowed drops are restricted by socket location. we can get the item type from the name of the active tooltip.
ttPanel = gui_getElementById("panel-tooltip-active")
tItemType$ = gui_Elements(ttPanel).name
select get split word$(3)
case "back":
endcase
case "body":
endcase
case "neck":
endcase
case "accessory":
endcase
case "rHand":
if tItemType$ = "Weapon"
tAllowed = TRUE
endif
endcase
case "lHand":
if tItemType$ = "Weapon" or tItemType$ = "Shield"
tAllowed = TRUE
endif
endcase
endselect
else
`general packspace
slotNum = intval(mid$(get split word$(2), 5, 0))
`any item is allowed in packspace, we only need to check that the slot is empty.
tAllowed = available in freelist(Chars(MY_CHAR).handle.myInventoryList, slotNum)
WRLOG.ui_event, " drop allowed " + str$(tAllowed) + " for " + gui_Elements(UI.dragElement.id).id + " on slot " + gui_Elements(e).id
exitfunction tAllowed
endif
endcase
case "ability":
endcase
endselect
WRLOG.ui_event, "drop allowed " + str$(tAllowed) + " for " + gui_Elements(UI.dragElement.id).id + " on slot " + gui_Elements(e).id
endfunction tAllowed
quick note on 'gui_Elements(ttPanel).selectedIndex = tWeaponID': selectedIndex was intended for use with drop down select boxes, I am re-purposing it here because it is both a suitable data type and a suitable name for the data I am storing, and this lets me keep the data I need without having to add any additional properties or handling. In a loose and flexible system like this, context is key, but you can get yourself in trouble if you lose track of that context...
Wow, so I'll admit this has grown more complex than I had initially *hoped*, but really no surprise there. We are in the home stretch now though. We can pick up an item, see it in inventory, mouse over it to get selection feedback and a tool tip with info about the item. We can click and hold to begin dragging and see the icon and tooltip follow the cursor around the screen. If we release our hold, the icon will revert back to its initial socket location, and when dragging it over other sockets, we determine and give visual feedback as to whether the icon is allowed to drop on this location. whew!
Just a few things left to do: drop the icon, move the item around within invnetory and equipped backend data, and update/hide the gui elements as needed. Lastly, if the item was equipped, we need to restore the 3d object to world space and bind it to the character object. Most of this will get triggered during the onRelease event of the dragging icon.
I will leave you for today with one little addition to unhoverElement to clear the tooltip for non-dragging mouse over icons. Yes, I can see that there will be a problem with the tool tip when dragging one icon over another, and each icon tries to use the tooltip panel to display info. That's why I set up 2 tooltip panels: 'active' and 'compare' in the xml. I'll add handling for this later, it's a pretty straightforward check against the active drag data and it's a non-issue until we have more than one icon in inventory anyway.
REM file: guiDoc_common.dba
function _gui_unhoverElement(e as integer)
select gui_Elements(e).tag
case "button":
_gui_elementStyle_setProp(e, "background-image", gui_Elements(e).name + ".png")
endcase
case "icon":
_gui_elementStyle_setProp(e, "background-image", gui_Elements(e).name + ".png")
ttPanel = gui_getElementById("panel-tooltip-active")
_gui_elementStyle_setProp(ttPanel, "display", "none")
gui_Elements(ttPanel).name = ""
gui_Elements(ttPanel).value = ""
gui_Elements(ttPanel).parent = 0
gui_Elements(ttPanel).selectedIndex = -1
endcase
case "socket":
_gui_elementStyle_setProp(e, "background-image", gui_Elements(e).name + ".png")
endcase
endselect
endfunction