Skip to content

Instantly share code, notes, and snippets.

@kergalym
Created June 22, 2022 11:14
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kergalym/411ddf3205a8eb81c32f46704f6d776e to your computer and use it in GitHub Desktop.
Save kergalym/411ddf3205a8eb81c32f46704f6d776e to your computer and use it in GitHub Desktop.
Panda3D Inventory (original code)
# -*- coding: utf-8 -*-
from panda3d.core import *
from direct.gui.DirectGui import *
from direct.gui.OnscreenText import OnscreenText
import random
import sys
HIDDEN = 0
VISIBLE = 1
MIDDLE = 0
LEFT = DOWN = -1
RIGHT = UP = 1
class InvSlot():
""" Inventory slot
"""
def __init__(self, data):
type, pos, info, ico = data
self.pos = pos
self.type = type
self.info = info
self.ico = ico
def get_icon(self):
return self.ico
def get_info(self):
return self.info
class Inventory:
""" Main inventory class
"""
def __init__(self, settings):
try:
self.default_slot_ico = settings['default-slot-image']
self.bg_size = settings['background-size']
self.bg_image = settings['background-image']
self.slot_half_size_x = settings['slot-size'][0] * 0.5
self.slot_half_size_y = settings['slot-size'][1] * 0.5
self.slot_margin = settings['slot-margin'] * 0.5
self.item_half_size_x = settings['item-size'][0] * 0.5
self.item_half_size_y = settings['item-size'][1] * 0.5
self.frame_slot_size = (-self.slot_half_size_x,
self.slot_half_size_x,
-self.slot_half_size_y,
self.slot_half_size_y)
self.informer_delay = settings['informer-popup-delay']
self.informer_bg = settings['informer-bg-color']
self.informer_fg = settings['informer-text-color']
self.informer_font = settings['informer-font']
self.use_transparency = settings['use-transparency']
except KeyError:
print('Incorrect inventory settings!')
sys.exit()
self.state = None # VISIBLE or HIDDEN
self.drag_item = -1 # if >=0 then we drag item with this id
self.slots = [] # list of InvSlot (data)
self._slots_vis = []
self.items = [] # list of items (data)
self._items_vis = []
self._counters = {}
self.old_mp = (0, 0)
self.slot_under_mouse = -1 # slot id under mouse cursor
self.item_under_mouse = -1 # item id under mouse cursor
self.informer_timer = 0.0 # hint popup time
self.informer = Informer(self.informer_bg, self.informer_fg, self.informer_font)
taskMgr.add(self.drag_task, 'Drag task')
taskMgr.add(self.informer_task, 'Inventory informer task')
def is_slot_busy(self, id, except_id=[]):
""" Check whether the slot is busy. """
for i, item in enumerate(self.items):
if item.slot_id == id and i not in except_id:
return True
return False
def fill_inv_slots(self, sx, sy, dx, dy, sltype='INVENTORY'):
""" Add (sx,sy) array of slots with (dx,dy) displace """
for y in range(sy):
for x in range(sx):
fx = self.slot_half_size_x * 2 + self.slot_margin * 2
fy = self.slot_half_size_y * 2 + self.slot_margin * 2
pos = (fx * x + dx, 0, -fy * y + dy)
self.slots.append(InvSlot((sltype, pos, '', self.default_slot_ico)))
def custom_inv_slots(self, slots_data):
""" Make slots from list
"""
for si in slots_data:
self.slots.append(InvSlot(si))
def find_first_empty_slot(self, sltype='INVENTORY'):
""" Find first empty slots with type of 'sltype' and return id
"""
for id, slot in enumerate(self.slots):
if slot.type == sltype:
if not self.is_slot_busy(id):
return id
return -1
def hide(self):
""" Hide all and stop tasks
"""
self.stop_drag()
self.back.hide()
taskMgr.remove('Drag task')
taskMgr.remove('Inventory informer task')
self.state = HIDDEN
def show(self):
""" Show inventory
"""
self.back.show()
taskMgr.add(self.drag_task, 'Drag task')
taskMgr.add(self.informer_task, 'Inventory informer task')
self.state = VISIBLE
def switch(self):
""" Switch inventory visibility
"""
if self.state == VISIBLE:
self.hide()
else:
self.show()
def _visualize_slots(self):
""" Make background and slots visualization from prepared data
"""
self.back = DirectFrame(frameTexture=self.bg_image,
frameSize=self.bg_size,
pos=(0, 0, 0))
if self.use_transparency:
self.back.setTransparency(TransparencyAttrib.MAlpha)
for id, slot in enumerate(self.slots):
self._slots_vis.append(DirectButton(frameTexture=slot.get_icon(),
pos=slot.pos,
frameSize=self.frame_slot_size,
pad=(0.5, 0.5),
relief=1,
rolloverSound=None,
command=self.on_slot_click,
extraArgs=[id]))
self._slots_vis[id].bind(DGG.ENTER, self.on_slot_enter, [id])
self._slots_vis[id].bind(DGG.EXIT, self.on_slot_exit, [id])
self._slots_vis[id].reparentTo(self.back)
if self.use_transparency:
self._slots_vis[id].setTransparency(TransparencyAttrib.MAlpha)
def _visualize_items(self):
""" Inventory items visualization
"""
for id, item in enumerate(self.items):
i_pos = self.slots[item.slot_id].pos
self._items_vis.append(DirectButton(frameTexture=item.get_icon(),
pos=i_pos,
frameSize=(-self.item_half_size_x,
self.item_half_size_x,
-self.item_half_size_y,
self.item_half_size_y),
pad=(0.5, 0.5), relief=1,
rolloverSound=None,
command=self.on_item_click,
extraArgs=[id]))
self._items_vis[id].bind(DGG.ENTER, self.on_item_enter, [id])
self._items_vis[id].bind(DGG.EXIT, self.on_item_exit, [id])
self._items_vis[id].reparentTo(self.back)
if self.use_transparency:
self._items_vis[id].setTransparency(TransparencyAttrib.MAlpha)
def _make_counters(self):
""" Make counter text for multiple items
"""
for id, item in enumerate(self.items):
if item.get_max_count() > 1:
self._counters[id] = OnscreenText(text=str(item.count),
pos=(0, 0, 0),
fg=(1, 0.2, 0.2, 1),
scale=(0.06),
mayChange=True)
self._counters[id].reparentTo(self._items_vis[id])
self._counters[id].setPos(self.item_half_size_x * 0.5,
-self.item_half_size_x * 0.8)
def update_counters(self):
for id, item in enumerate(self.items):
if item.get_max_count() > 1:
self._counters[id]['text'] = str(item.count)
def make(self):
self._visualize_slots()
self._visualize_items()
self._make_counters()
self.switch()
def refresh_items(self):
""" Remove old items vis. and create new
"""
for iv in self._items_vis:
iv.destroy()
for c in self._counters.values():
c.destroy()
self._items_vis = []
self._counters = {}
self._visualize_items()
self._make_counters()
def add_item(self, item, target='INVENTORY'):
""" Add new item data. To make it visible needs to call refresh_items
"""
id = self.find_first_empty_slot(target)
if id >= 0:
item.slot_id = id
self.items.append(item)
return True
return False
def drop_item_to(self, iid, target='INVENTORY'):
""" Move item to first empty slot with type of 'target' if
possible, otherwise - remove item
"""
id = self.find_first_empty_slot(target)
if id >= 0:
self.move_item_to_slot(iid, id)
else:
self.remove_item(iid)
def move_item_to_slot(self, iid, sid, force=False):
""" Move item with id 'iid' to slot with id 'sid'
"""
if not self.is_slot_busy(sid, [iid]) or force:
if self.slots[sid].type in self.items[iid].get_slots():
old_sid = self.items[iid].slot_id
self.items[iid].slot_id = sid
self._items_vis[iid].setPos(self.slots[sid].pos)
messenger.send('inventory-item-move', [iid, old_sid, sid])
return True
self.stop_drag()
return False
def remove_item(self, id):
""" Fully remove item
"""
if self._counters.has_key(id):
self._counters[id].destroy()
del self._counters[id]
if self.item_under_mouse == id:
self.item_under_mouse = -1
if self.drag_item == id:
self.stop_drag()
self._items_vis[id].destroy()
del self._items_vis[id]
del self.items[id]
self.refresh_items()
def stop_drag(self):
""" Stop item dragging and return it to the slot
"""
if self.drag_item >= 0:
item = self.items[self.drag_item]
self._items_vis[self.drag_item].setPos(self.slots[item.slot_id].pos)
self._items_vis[self.drag_item].setBin('unsorted', 1000)
self.drag_item = -1
def get_slots_by_type(self, s_type='INVENTORY'):
""" Return slot's IDs list, which type is 's_type'
"""
slots = []
for id, slot in enumerate(self.slots):
if slot.type == s_type:
slots.append(id)
return slots
def on_slot_click(self, *args):
""" Slot click callback. Try to drop item into the clicked slot.
"""
if self.drag_item >= 0:
if self.move_item_to_slot(self.drag_item, args[0]):
self.stop_drag()
def on_item_click(self, *args):
""" Item click callback. Try to capture the item or replace
item in the slot
"""
# Capture clicked item
if self.drag_item < 0:
self.drag_item = args[0]
self._items_vis[self.drag_item].setBin('gui-popup', 9999)
# Merge items if possible
elif self.items[args[0]].get_type() == self.items[self.drag_item].get_type() and \
self.items[args[0]].get_max_count() > 1 and \
self.items[self.drag_item].get_max_count() > 1:
diff = self.items[args[0]].get_max_count() - self.items[args[0]].count
if diff < self.items[self.drag_item].count:
self.items[args[0]].count += diff
self.items[self.drag_item].count -= diff
else:
self.items[args[0]].count += self.items[self.drag_item].count
self.remove_item(self.drag_item)
self.stop_drag()
self.update_counters()
# otherwise replace an item in the slot and drop or remove old item to the inventory
elif self.move_item_to_slot(self.drag_item, self.items[args[0]].slot_id, force=True):
self.drop_item_to(args[0], 'INVENTORY')
self.stop_drag()
def on_slot_enter(self, *args):
""" Slot mouse entering callback
"""
self.slot_under_mouse = args[0]
def on_slot_exit(self, *args):
""" Slot mouse exiting callback
"""
if self.slot_under_mouse == args[0]:
self.slot_under_mouse = -1
def on_item_enter(self, *args):
""" Item mouse entering callback
"""
self.item_under_mouse = args[0]
def on_item_exit(self, *args):
""" Item mouse exiting callback
"""
if self.item_under_mouse == args[0]:
self.item_under_mouse = -1
def drag_task(self, task):
""" Follow captured item to the mouse cursor
"""
if self.drag_item >= 0:
if base.mouseWatcherNode.hasMouse():
x = base.mouseWatcherNode.getMouseX() * base.getAspectRatio() + (self.item_half_size_x + 0.01)
y = base.mouseWatcherNode.getMouseY() - (self.item_half_size_y + 0.01)
self._items_vis[self.drag_item].setPos(x, 0, y)
return task.cont
def informer_task(self, task):
""" Popup hint task
"""
if base.mouseWatcherNode.hasMouse():
x = base.mouseWatcherNode.getMouseX() * base.getAspectRatio()
y = base.mouseWatcherNode.getMouseY()
if self.old_mp == (x, y):
self.informer_timer += globalClock.getDt()
else:
self.informer_timer = 0
if self.informer.state == VISIBLE:
self.informer.hide()
if self.informer_timer > self.informer_delay and \
self.informer.state == HIDDEN:
if self.item_under_mouse >= 0 and self.drag_item < 0:
txt = self.items[self.item_under_mouse].get_info()
self.informer.show(txt, (x + 0.01, 0, y + 0.01), bound=self.bg_size)
elif self.slot_under_mouse >= 0:
txt = self.slots[self.slot_under_mouse].get_info()
self.informer.show(txt, (x + 0.01, 0, y + 0.01), bound=self.bg_size)
self.old_mp = (x, y)
return task.cont
class Informer:
""" Popup hint for items and slots
"""
def __init__(self, bg_color, txt_color, font_file):
self.font = loader.loadFont(font_file)
self.back = DirectFrame(pos=(0, 0, 0),
frameColor=bg_color,
sortOrder=10000)
self.back.setScale(0.07)
self.info_text = OnscreenText(text=' ',
pos=(0, 0, 0),
fg=txt_color,
mayChange=True,
align=TextNode.ALeft,
font=self.font)
self.info_text.reparentTo(self.back)
self.info_text.setScale(1.0)
self.state = 0
self.hide()
def hide(self):
self.back.hide()
self.state = HIDDEN
def show(self, text=None, pos=None, bound=None):
""" Show hint with 'text' in 'pos' position. 'bound' is bounding
area to set where hint may be visible.
"""
if text:
self.info_text['text'] = text
sy, sx = self.info_text.textNode.getHeight() * 0.07, self.info_text.textNode.getWidth() * 0.07
card = self.info_text.textNode.getCardActual()
card = (card[0] - 0.2, card[1] + 0.2, card[2] - 0.2, card[3] + 0.2)
self.back['frameSize'] = card
if pos:
sy, sx = self.info_text.textNode.getHeight() * 0.07, self.info_text.textNode.getWidth() * 0.07
x = pos[0] - card[0] * 0.07
y = pos[2] - card[2] * 0.07
if bound:
l, r, d, u = bound
if x + card[1] * 0.07 > r:
x = x - ((x + card[1] * 0.07) - r)
if x - card[0] * 0.07 < l:
x = x + (l - (x - card[0] * 0.07))
if y + card[3] * 0.07 > u:
y = y - ((y + card[3] * 0.07) - u)
if y - card[2] * 0.07 < d:
y = y + (d - (y - card[2] * 0.07))
self.back.setPos((x, pos[1], y))
self.back.show()
self.state = VISIBLE
def get_size(self):
sy, sx = self.info_text.textNode.getHeight() * 0.1, self.info_text.textNode.getWidth() * 0.1
return (sx, sy)
class Item:
""" Implementation of inventory item data. You can change this class
to suit your needs, but it must contain next elements:
slot_id: integer ID of slot
count: integer amount of the item
get_icon: function should return string with image file name
get_info: function should return hint string
get_slots: function should return list or tuple of the slot types in
which is possible to place this item
get_max_count: function return max count of items one (this) type,
which may be in one slot
get_type: function should return arbitrary 'type' of the item. This
'type' needed to try to merge amount of items of one type in the
one slot.
"""
def __init__(self, data):
self.slot_id = -1
self.count = data[4]
self.data = data
def get_icon(self):
return self.data[2]
def get_info(self):
txt = ''
if self.data[1] == 'armor':
txt = '%s\nArmor: %i' % (self.data[3], self.data[6])
elif self.data[1] == 'weapon':
txt = '%s\nDamage: %i' % (self.data[3], self.data[7])
else:
txt = self.data[3]
return txt
def get_slots(self):
return self.data[0]
def get_max_count(self):
return self.data[5]
def get_type(self):
return self.data[1]
# -----------------------------------------------------------------------
# EXAMPLE
# -----------------------------------------------------------------------
if __name__ == '__main__':
import direct.directbase.DirectStart
# Inventory settings
inv_settings = {'background-size': (-base.getAspectRatio(),
base.getAspectRatio(),
-1, 1),
'background-image': 'res/back.png',
'slot-size': (0.16, 0.16),
'item-size': (0.14, 0.14),
'slot-margin': 0.001,
'informer-popup-delay': 0.5,
'default-slot-image': 'res/base_slot.png',
'informer-font': 'res/arial.ttf',
'informer-bg-color': (0.15, 0.067, 0.035, 0.92),
'informer-text-color': (1, 1, 1, 1),
'use-transparency': True
}
slot_info = [('HAND1', (-1.1, 0, 0.15), u'Hand', 'res/base_slot.png'),
('HAND2', (-0.4, 0, 0.05), u'Hand', 'res/base_slot.png'),
('HEAD', (-0.74, 0, 0.7), u'Head', 'res/head_slot.png'),
('BODY', (-0.74, 0, 0.32), u'Body', 'res/body_slot.png'),
('LEGS', (-0.74, 0, -0.65), u'Legs', 'res/leg_slot.png'),
('TRASH', (-0.2, 0, -0.6), u'Trash', 'res/trash_slot.png')]
items = [(('INVENTORY', 'TRASH', 'HAND1', 'HAND2'), 'weapon',
'res/glok.png', 'Glok 17', 1, 1, 0, 8),
(('INVENTORY', 'TRASH', 'BODY'), 'armor',
'res/armor.png', 'Light armor', 1, 1, 10),
(('INVENTORY', 'TRASH', 'HEAD'), 'armor',
'res/helmet.png', 'Helmet', 1, 1, 5),
(('INVENTORY', 'TRASH', 'LEGS'), 'armor',
'res/boots.png', 'Boots', 1, 1, 2),
(('INVENTORY', 'TRASH'), '9x19lu',
'res/bullets.png', '9x19 luger', 20, 30),
(('INVENTORY', 'TRASH'), '9x19lu',
'res/bullets.png', '9x19 luger', 15, 30),
(('INVENTORY', 'TRASH'), '9x19lu',
'res/bullets.png', '9x19 luger', 6, 30)]
inv = Inventory(inv_settings)
inv.custom_inv_slots(slot_info) # Custom slots init
inv.fill_inv_slots(3, 3, 0.35, 0.5) # Field of slots 3х3, left upper corner (0.35;0.5)
for item in items:
inv.add_item(Item(item)) # Add items
inv.make() # Visualisation
base.accept('mouse3-up', inv.stop_drag) # Stop item dragging on right mouse
base.accept('i', inv.switch) # key I - show/hide inventory
# 'on item move' event processing
# in this case we are delete item, which has been placed into the 'TRASH'
def on_item_move(*args):
iid, s_from, s_to = args
if inv.slots[s_to].type == 'TRASH':
inv.remove_item(iid)
base.accept('inventory-item-move', on_item_move)
# Add another item to inventory
item = (('INVENTORY', 'TRASH'), '9x19lu', 'res/bullets.png', '9x19 luger', 15, 30)
inv.add_item(Item(item))
inv.refresh_items()
base.run()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment