from direct.gui.DirectGuiGlobals import FLAT, NORMAL
from direct.gui.OnscreenText import OnscreenText
from direct.showbase.DirectObject import DirectObject
+from ya2.p3d.gfx import P3dGfxMgr
from pmachines.items.item import ItemStrategy, FixedStrategy, StillStrategy
from pmachines.items.box import HitStrategy
from pmachines.items.domino import DownStrategy, UpStrategy
self._frm.destroy()
self.ignore('item-rototranslated')
messenger.send('editor-inspector-destroy')
+
+
+class PixelSpaceInspector(DirectObject):
+
+ def __init__(self, item, all_items):
+ super().__init__()
+ self.__item = item
+ self.__all_items = all_items
+ self._font = base.loader.load_font(
+ 'assets/fonts/Hanken-Book.ttf')
+ self._font.clear()
+ self._font.set_pixels_per_unit(60)
+ self._font.set_minfilter(Texture.FTLinearMipmapLinear)
+ self._font.set_outline((0, 0, 0, 1), .8, .2)
+ self._common = {
+ 'scale': .046,
+ 'text_font': self._font,
+ 'text_fg': (.9, .9, .9, 1),
+ 'relief': FLAT,
+ 'frameColor': (.4, .4, .4, .14),
+ 'rolloverSound': loader.load_sfx(
+ 'assets/audio/sfx/rollover.ogg'),
+ 'clickSound': loader.load_sfx(
+ 'assets/audio/sfx/click.ogg')}
+ w, h = .8, .36
+ self._frm = DirectFrame(frameColor=(.4, .4, .4, .06),
+ frameSize=(0, w, -h, 0),
+ parent=base.a2dTopRight,
+ pos=(-w, 0, 0))
+ self.__z = -.08
+ p = self.__item._np.get_pos()
+ _id = ''
+ if 'id' in self.__item.json:
+ _id = self.__item.json['id']
+ t, pos_entry = self.__add_row(_('position'), f'{round(p.x, 3)}, {round(p.z, 3)}', self.on_edit_position)
+ t, id_entry = self.__add_row(_('id'), _id, self.on_edit_id)
+ fields = ['position', 'id']
+ Entries = namedtuple('Entries', fields)
+ self.__entries = Entries(pos_entry, id_entry)
+ def load_images_btn(path, col):
+ colors = {
+ 'gray': [
+ (.6, .6, .6, 1), # ready
+ (1, 1, 1, 1), # press
+ (.8, .8, .8, 1), # rollover
+ (.4, .4, .4, .4)],
+ 'green': [
+ (.1, .68, .1, 1),
+ (.1, 1, .1, 1),
+ (.1, .84, .1, 1),
+ (.4, .1, .1, .4)]}[col]
+ return [self.__load_img_btn(path, col) for col in colors]
+ fcols = (.4, .4, .4, .14), (.3, .3, .3, .05)
+ DirectButton(
+ image=load_images_btn('exitRight', 'gray'), scale=.05,
+ pos=(.06, 1, -h + .06),
+ parent=self._frm, command=self.destroy, state=NORMAL, relief=FLAT,
+ frameColor=fcols[0],
+ rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
+ clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
+ self.accept('item-rototranslated', self.__on_item_rototranslated)
+ DirectButton(
+ image=load_images_btn('trashcan', 'gray'), scale=.05,
+ pos=(.18, 1, -h + .06),
+ parent=self._frm, command=self.__delete_item, state=NORMAL, relief=FLAT,
+ frameColor=fcols[0],
+ rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
+ clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
+
+ def __add_row(self, label, text, callback):
+ tw = 10
+ t = OnscreenText(
+ label,
+ pos=(.03, self.__z), parent=self._frm,
+ font=self._common['text_font'],
+ scale=self._common['scale'],
+ fg=self._common['text_fg'],
+ wordwrap=20, align=TextNode.ALeft)
+ e = DirectEntry(
+ scale=self._common['scale'],
+ pos=(.30, 1, self.__z),
+ entryFont=self._font,
+ width=tw,
+ cursorKeys=True,
+ frameColor=self._common['frameColor'],
+ initialText=text,
+ parent=self._frm,
+ text_fg=self._common['text_fg'],
+ command=callback)
+ self.__z -= .1
+ return t, e
+
+ def __load_img_btn(self, path, col):
+ img = OnscreenImage('assets/images/buttons/%s.dds' % path)
+ img.set_transparency(True)
+ img.set_color(col)
+ img.detach_node()
+ return img
+
+ def __on_item_rototranslated(self, np):
+ pos = P3dGfxMgr.pos2d_p2d(np)
+ self.__entries.position.set('%s %s' % (str(round(pos[0], 3)), str(round(pos[1], 3))))
+ self.__item.json['position'] = list(pos)
+
+ def __delete_item(self):
+ messenger.send('editor-inspector-delete', [self.__item])
+ self.destroy()
+
+ @property
+ def item(self):
+ return self.__item
+
+ def on_edit_position(self, txt):
+ x, z = map(float, txt.split())
+ self.__item.position = [x, 0, z]
+
+ def on_edit_id(self, txt):
+ self.__item.id = txt
+
+ def __actually_close(self, arg):
+ self.__entries.strategy.set('')
+ self.__entries.strategy_args.set('')
+ self.__dialog.cleanup()
+
+ def destroy(self):
+ self._frm.destroy()
+ self.ignore('item-rototranslated')
+ messenger.send('editor-inspector-destroy')
+
+
+class WorldSpaceInspector(DirectObject):
+
+ def __init__(self, item, all_items):
+ super().__init__()
+ self.__item = item
+ self.__all_items = all_items
+ self._font = base.loader.load_font(
+ 'assets/fonts/Hanken-Book.ttf')
+ self._font.clear()
+ self._font.set_pixels_per_unit(60)
+ self._font.set_minfilter(Texture.FTLinearMipmapLinear)
+ self._font.set_outline((0, 0, 0, 1), .8, .2)
+ self._common = {
+ 'scale': .046,
+ 'text_font': self._font,
+ 'text_fg': (.9, .9, .9, 1),
+ 'relief': FLAT,
+ 'frameColor': (.4, .4, .4, .14),
+ 'rolloverSound': loader.load_sfx(
+ 'assets/audio/sfx/rollover.ogg'),
+ 'clickSound': loader.load_sfx(
+ 'assets/audio/sfx/click.ogg')}
+ w, h = .8, .36
+ self._frm = DirectFrame(frameColor=(.4, .4, .4, .06),
+ frameSize=(0, w, -h, 0),
+ parent=base.a2dTopRight,
+ pos=(-w, 0, 0))
+ self.__z = -.08
+ p = self.__item._np.get_pos()
+ _id = ''
+ if 'id' in self.__item.json:
+ _id = self.__item.json['id']
+ t, pos_entry = self.__add_row(_('position'), f'{round(p.x, 3)}, {round(p.z, 3)}', self.on_edit_position)
+ t, id_entry = self.__add_row(_('id'), _id, self.on_edit_id)
+ fields = ['position', 'id']
+ Entries = namedtuple('Entries', fields)
+ self.__entries = Entries(pos_entry, id_entry)
+ def load_images_btn(path, col):
+ colors = {
+ 'gray': [
+ (.6, .6, .6, 1), # ready
+ (1, 1, 1, 1), # press
+ (.8, .8, .8, 1), # rollover
+ (.4, .4, .4, .4)],
+ 'green': [
+ (.1, .68, .1, 1),
+ (.1, 1, .1, 1),
+ (.1, .84, .1, 1),
+ (.4, .1, .1, .4)]}[col]
+ return [self.__load_img_btn(path, col) for col in colors]
+ fcols = (.4, .4, .4, .14), (.3, .3, .3, .05)
+ DirectButton(
+ image=load_images_btn('exitRight', 'gray'), scale=.05,
+ pos=(.06, 1, -h + .06),
+ parent=self._frm, command=self.destroy, state=NORMAL, relief=FLAT,
+ frameColor=fcols[0],
+ rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
+ clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
+ self.accept('item-rototranslated', self.__on_item_rototranslated)
+ DirectButton(
+ image=load_images_btn('trashcan', 'gray'), scale=.05,
+ pos=(.18, 1, -h + .06),
+ parent=self._frm, command=self.__delete_item, state=NORMAL, relief=FLAT,
+ frameColor=fcols[0],
+ rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
+ clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
+
+ def __add_row(self, label, text, callback):
+ tw = 10
+ t = OnscreenText(
+ label,
+ pos=(.03, self.__z), parent=self._frm,
+ font=self._common['text_font'],
+ scale=self._common['scale'],
+ fg=self._common['text_fg'],
+ wordwrap=20, align=TextNode.ALeft)
+ e = DirectEntry(
+ scale=self._common['scale'],
+ pos=(.30, 1, self.__z),
+ entryFont=self._font,
+ width=tw,
+ cursorKeys=True,
+ frameColor=self._common['frameColor'],
+ initialText=text,
+ parent=self._frm,
+ text_fg=self._common['text_fg'],
+ command=callback)
+ self.__z -= .1
+ return t, e
+
+ def __load_img_btn(self, path, col):
+ img = OnscreenImage('assets/images/buttons/%s.dds' % path)
+ img.set_transparency(True)
+ img.set_color(col)
+ img.detach_node()
+ return img
+
+ def __on_item_rototranslated(self, np):
+ pos = np.get_pos()
+ self.__entries.position.set('%s %s' % (str(round(pos.x, 3)), str(round(pos.z, 3))))
+ self.__item.json['position'] = list(pos)
+
+ def __delete_item(self):
+ messenger.send('editor-inspector-delete', [self.__item])
+ self.destroy()
+
+ @property
+ def item(self):
+ return self.__item
+
+ def on_edit_position(self, txt):
+ x, z = map(float, txt.split())
+ self.__item.position = [x, 0, z]
+
+ def on_edit_id(self, txt):
+ self.__item.id = txt
+
+ def __actually_close(self, arg):
+ self.__entries.strategy.set('')
+ self.__entries.strategy_args.set('')
+ self.__dialog.cleanup()
+
+ def destroy(self):
+ self._frm.destroy()
+ self.ignore('item-rototranslated')
+ messenger.send('editor-inspector-destroy')
from direct.gui.OnscreenText import OnscreenText
from direct.showbase.DirectObject import DirectObject
from pmachines.items.item import Item
+from pmachines.items.test_item import PixelSpaceTestItem, WorldSpaceTestItem
from pmachines.editor.scene_list import SceneList
-from pmachines.editor.inspector import Inspector
+from pmachines.editor.inspector import Inspector, PixelSpaceInspector, WorldSpaceInspector
from pmachines.editor.start_items import StartItems
+from ya2.p3d.gfx import P3dGfxMgr
class SceneEditor(DirectObject):
- def __init__(self, json, json_name, context, add_item, items):
+ def __init__(self, json, json_name, context, add_item, items, world, mouse_plane_node):
super().__init__()
self.__items = items
self.__json = json
+ self.__world = world
+ self.__mouse_plane_node = mouse_plane_node
if not json_name:
self.__json = json = {
'name': '',
frameColor=fcols[0],
rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
+ self.__test_items = []
+ self.__set_test_items()
messenger.send('editor-start')
self.accept('editor-item-click', self.__on_item_click)
self.accept('editor-inspector-destroy', self.__on_inspector_destroy)
self.accept('editor-start-items-save', self.__on_start_items_save)
self.accept('editor-start-items-destroy', self.__on_start_items_destroy)
+ def __set_test_items(self):
+ for pixel_space_item in self.__json['test_items']['pixel_space']:
+ print(pixel_space_item['id'], pixel_space_item['position'])
+ pos_pixel = pixel_space_item['position']
+ win_res = base.win.getXSize(), base.win.getYSize()
+ pos_win = (pos_pixel[0] / win_res[0] * 2 - 1,
+ 1 - pos_pixel[1] / win_res[1] * 2)
+ p_from, p_to = P3dGfxMgr.world_from_to(pos_win)
+ hits = self.__world.ray_test_all(p_from, p_to).get_hits()
+ for hit in self.__world.ray_test_all(p_from, p_to).get_hits():
+ if hit.get_node() == self.__mouse_plane_node:
+ pos = hit.get_hit_pos()
+ self.__set_test_item(pos, pixel_space_item, PixelSpaceTestItem)
+ for world_space_item in self.__json['test_items']['world_space']:
+ print(world_space_item['id'], world_space_item['position'])
+ self.__set_test_item(world_space_item['position'], world_space_item, WorldSpaceTestItem)
+
+ def __set_test_item(self, pos, json, item_class):
+ test_item = item_class(
+ self.__context.world,
+ self.__context.plane_node,
+ self.__context.cb_inst,
+ self.__context.curr_bottom,
+ self.__context.repos,
+ json,
+ pos=(pos[0], 0, pos[-1]),
+ model_scale=.2)
+ self.__test_items += [test_item]
+
def __on_close(self):
self.__json['name'] = self.__name_entry.get()
self.__json['instructions'] = self.__instructions_entry.get()
if self.__compute_hash() == self.__json['version']:
- self._frm.destroy()
+ self.destroy()
else:
self.__dialog = YesNoDialog(dialogName='Unsaved changes',
text=_('You have unsaved changes. Really quit?'),
def __on_new_item(self, item):
info(f'new {item}')
item_json = {}
+ scale = 1
+ if item in ['PixelSpaceTestItem', 'WorldSpaceTestItem']:
+ scale = .2
_item = self.__new_items[item](
self.__context.world,
self.__context.plane_node,
self.__context.cb_inst,
self.__context.curr_bottom,
self.__context.repos,
- item_json)
+ item_json,
+ model_scale=scale)
_item._Item__editing = True
self.__add_item(_item)
item_json['class'] = _item.__class__.__name__
item_json['position'] = list(_item._np.get_pos())
- self.__json['items'] += [item_json]
+ if item == 'PixelSpaceTestItem':
+ del item_json['class']
+ self.__json['test_items']['pixel_space'] += [item_json]
+ elif item == 'WorldSpaceTestItem':
+ del item_json['class']
+ self.__json['test_items']['world_space'] += [item_json]
+ else:
+ self.__json['items'] += [item_json]
def __actually_close(self, arg):
if arg:
if self.__inspector and self.__inspector.item == item: return
if self.__inspector:
self.__inspector.destroy()
- self.__inspector = Inspector(item, self.__items)
+ if item.__class__ == PixelSpaceTestItem:
+ self.__inspector = PixelSpaceInspector(item, self.__items)
+ elif item.__class__ == WorldSpaceTestItem:
+ self.__inspector = WorldSpaceInspector(item, self.__items)
+ else:
+ self.__inspector = Inspector(item, self.__items)
def __on_inspector_destroy(self):
self.__inspector = None
def __on_start_items_destroy(self):
self.__start_items = None
+ @property
+ def test_items(self):
+ return self.__test_items
+
def destroy(self):
self._frm.destroy()
if self.__inspector:
self.ignore('editor-inspector-destroy')
self.ignore('editor-start-items-save')
self.ignore('editor-start-items-destroy')
+ for t in self.__test_items: t.destroy()
+ self.__test_items = []
--- /dev/null
+from panda3d.bullet import BulletBoxShape
+from pmachines.items.item import Item
+
+
+class TestItem(Item):
+
+ def __init__(self, world, plane_node, cb_inst, curr_bottom, repos, json, mass=1, pos=(0, 0, 0), r=0, count=0, restitution=.5, friction=.8, model_scale=1):
+ super().__init__(world, plane_node, cb_inst, curr_bottom, repos, 'assets/models/bam/test_handle/test_handle.bam', json, mass=mass, pos=pos, r=r, count=count, restitution=restitution, friction=friction, model_scale=model_scale)
+ self._np.set_bin('fixed', 0)
+ self._np.set_depth_test(False)
+ self._np.set_depth_write(False)
+
+ def _set_shape(self, apply_scale=True):
+ self.node.add_shape(BulletBoxShape((.5, .5, .5)))
+
+class PixelSpaceTestItem(TestItem): pass
+
+
+class WorldSpaceTestItem(TestItem): pass
self._set_lights()
self._set_input()
self._set_mouse_plane()
- self.items = []
+ self.__items = []
self._test_items = []
self.reset()
self._state = 'init'
else:
self._set_instructions()
self._bg = Background()
- self._side_panel = SidePanel(world, self._mouse_plane_node, (-5, 4), (-3, 1), 1, self.items)
+ self._side_panel = SidePanel(world, self._mouse_plane_node, (-5, 4), (-3, 1), 1, self.__items)
self._scene_tsk = taskMgr.add(self.on_frame, 'on_frame')
if auto_start_editor:
self._set_editor()
def __process_json_escape(self, string):
return bytes(string, 'utf-8').decode('unicode-escape')
+ @property
+ def items(self):
+ items = self.__items[:]
+ if self.__scene_editor:
+ items += self.__scene_editor.test_items
+ return items
+
def _set_items(self):
if not self.__json_name: return
- self.items = []
+ self.__items = []
self._test_items = []
if not self.json:
with open(f'assets/scenes/{self.__json_name}.json') as f:
args['mass'] = item['mass']
if 'friction' in item:
args['friction'] = item['friction']
- self.items += [self.__code2class(item['class'])(**args)]
+ self.__items += [self.__code2class(item['class'])(**args)]
for item in self.json['items']:
args = {
'world': self._world,
args['restitution'] = item['restitution']
if 'friction' in item:
args['friction'] = item['friction']
- self.items += [self.__code2class(item['class'])(**args)]
+ self.__items += [self.__code2class(item['class'])(**args)]
if 'strategy' in item:
match item['strategy']:
case 'DownStrategy':
- self.items[-1].set_strategy(self.__code2class(item['strategy'])(self.items[-1]._np, *item['strategy_args']))
+ self.__items[-1].set_strategy(self.__code2class(item['strategy'])(self.__items[-1]._np, *item['strategy_args']))
case 'HitStrategy':
- self.items[-1].set_strategy(self.__code2class(item['strategy'])(self.__item_with_id(item['strategy_args'][0]), self.items[-1].node, self.items[-1]._world))
+ self.__items[-1].set_strategy(self.__code2class(item['strategy'])(self.__item_with_id(item['strategy_args'][0]), self.items[-1].node, self.__items[-1]._world))
def __code2class(self, code):
return {
}[code]
def __item_with_id(self, id):
- for item in self.items:
+ for item in self.__items:
if 'id' in item.json and item.json['id'] == id:
return item
def current_bottom(self):
curr_bottom = 1
- for item in self.items:
+ for item in self.__items:
if item.repos_done:
curr_bottom = min(curr_bottom, item.get_bottom())
return curr_bottom
def reset(self):
- [itm.destroy() for itm in self.items]
+ [itm.destroy() for itm in self.__items]
[itm.remove_node() for itm in self._test_items]
- self.items = []
+ self.__items = []
self._test_items = []
self._set_items()
self._set_test_items()
self._unset_lights()
self._unset_input()
self._unset_mouse_plane()
- [itm.destroy() for itm in self.items]
+ [itm.destroy() for itm in self.__items]
[itm.remove_node() for itm in self._test_items]
self._bg.destroy()
self._side_panel.destroy()
for item in [i for i in self.items if hit.get_node() == i.node and i.interactable]:
if not self._item_active:
self._item_active = item
+ if item not in self.__items:
+ method = 'on_click_l'
getattr(item, method)(pos)
img = 'move' if method == 'on_click_l' else 'rotate'
if not (img == 'rotate' and not item._instantiated):
# self.__next_btn['state'] = DISABLED
# self.__next_btn['frameColor'] = fcols[1]
self._item_active = None
- [item.on_release() for item in self.items]
+ [item.on_release() for item in self.__items]
self._cursor.set_image('assets/images/buttons/arrowUpLeft.dds')
def repos(self):
- for item in self.items:
+ for item in self.__items:
item.repos_done = False
- self.items = sorted(self.items, key=lambda itm: itm.__class__.__name__)
- [item.on_aspect_ratio_changed() for item in self.items]
- self._side_panel.update(self.items)
+ self.__items = sorted(self.__items, key=lambda itm: itm.__class__.__name__)
+ [item.on_aspect_ratio_changed() for item in self.__items]
+ self._side_panel.update(self.__items)
max_x = -float('inf')
- for item in self.items:
+ for item in self.__items:
if not item._instantiated:
max_x = max(item._np.get_x(), max_x)
- for item in self.items:
+ for item in self.__items:
if not item._instantiated:
item.repos_x(max_x)
self.repos()
def _win_condition(self):
- return all(itm.strategy.win_condition() for itm in self.items) and not self._paused
+ return all(itm.strategy.win_condition() for itm in self.__items) and not self._paused
def _fail_condition(self):
- return all(itm.fail_condition() for itm in self.items) and not self._paused and self._state == 'playing'
+ return all(itm.fail_condition() for itm in self.__items) and not self._paused and self._state == 'playing'
def on_frame(self, task):
hits = self._get_hits()
return task.cont
def cb_inst(self, item):
- self.items += [item]
+ self.__items += [item]
def on_play(self):
self._state = 'playing'
#self.__prev_btn['state'] = DISABLED
#self.__next_btn['state'] = DISABLED
self.__right_btn['state'] = DISABLED
- [itm.play() for itm in self.items]
+ [itm.play() for itm in self.__items]
self._start_evt_time = globalClock.getFrameTime()
def on_next(self):
self.__btn_state = [btn['state'] for btn in btns]
for btn in btns:
btn['state'] = DISABLED
- [itm.store_state() for itm in self.items]
+ [itm.store_state() for itm in self.__items]
def __restore_state(self):
btns = [
]
for btn, state in zip(btns, self.__btn_state):
btn['state'] = state
- [itm.restore_state() for itm in self.items]
+ [itm.restore_state() for itm in self.__items]
self._paused = False
def __on_close_instructions(self, frm):
self._test_items[-1].set_pos(pos[0], 0, pos[1])
def add_item(self, item):
- self.items += [item]
+ self.__items += [item]
def _set_editor(self):
fields = ['world', 'plane_node', 'cb_inst', 'curr_bottom', 'repos', 'json']
self.current_bottom,
self.repos,
{})
- self.__scene_editor = SceneEditor(self.json, self.__json_name, context, self.add_item, self.items)
+ self.__scene_editor = SceneEditor(self.json, self.__json_name, context, self.add_item, self.__items, self._world, self._mouse_plane_node)
def __on_inspector_delete(self, item):
- self.items.remove(item)
+ self.__items.remove(item)
self.json['items'].remove(item.json)
item.destroy()
- [X] new item
- [X] left/right arrow
- [X] json
-- [ ] editing of test_items in the editor for functional tests
+- [X] editing of test_items in the editor for functional tests
- [ ] define functional tests
* BACKLOG tooltips for buttons
* BACKLOG teeter-tooter with constraints (real teeter tooter)
@staticmethod
def assert_render3d():
preserve = ['camera', 'DIRECT']
- unexpected_nodes = [c for c in render.children if child.name not in preserve]
+ unexpected_nodes = [c for c in render.children if c.name not in preserve]
for node in unexpected_nodes: Assert.__process_render3d_exception(node)
@staticmethod