Created
June 22, 2022 11:14
-
-
Save kergalym/411ddf3205a8eb81c32f46704f6d776e to your computer and use it in GitHub Desktop.
Panda3D Inventory (original code)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# -*- 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