ya2 · news · projects · code · about

support for custom backgrounds
[pmachines.git] / pmachines / editor / scene.py
CommitLineData
d3a2e50a
FC
1from copy import deepcopy
2from json import dumps
2aeb9f68 3from logging import info
d3a2e50a 4import hashlib
c48bfe58 5from panda3d.core import Texture, TextNode, LPoint3f
d3a2e50a 6from direct.gui.OnscreenImage import OnscreenImage
c48bfe58 7from direct.gui.DirectGui import DirectButton, DirectEntry, \
3466af49 8 YesNoDialog, DirectOptionMenu, DirectFrame
d3a2e50a
FC
9from direct.gui.DirectGuiGlobals import FLAT, NORMAL
10from direct.gui.OnscreenText import OnscreenText
ee65fee0 11from direct.showbase.DirectObject import DirectObject
3133e30f 12from pmachines.items.test_item import PixelSpaceTestItem, WorldSpaceTestItem
012cf969 13from pmachines.editor.scene_list import SceneList
3133e30f 14from pmachines.editor.inspector import Inspector, PixelSpaceInspector, WorldSpaceInspector
9981d472 15from pmachines.editor.start_items import StartItems
a810a9ed 16from ya2.utils.gfx import Point, DirectGuiMixin
c617fd93 17from pmachines.editor.augmented_frame import AugmentedDirectFrame
9199c6aa 18from ya2.utils.gui.base_page import DirectOptionMenuTestable
fd2a4e5d
FC
19from pmachines.items.basketball import Basketball
20from pmachines.items.box import Box
21from pmachines.items.domino import Domino
22from pmachines.items.shelf import Shelf
23from pmachines.items.teetertooter import TeeterTooter
24from pmachines.items.item import FixedStrategy, StillStrategy
25from pmachines.items.domino import UpStrategy, DownStrategy
26from pmachines.items.box import HitStrategy
d3a2e50a
FC
27
28
ee65fee0 29class SceneEditor(DirectObject):
d3a2e50a 30
3466af49 31 def __init__(self, json, json_name, context, add_item, items, world, mouse_plane_node, pos_mgr):
ee65fee0 32 super().__init__()
2aeb9f68 33 self.__items = items
d3a2e50a 34 self.__json = json
3133e30f
FC
35 self.__world = world
36 self.__mouse_plane_node = mouse_plane_node
3466af49
FC
37 self.__pos_mgr = pos_mgr
38 self.__dialog = None
3e3c4caf
FC
39 if not json_name:
40 self.__json = json = {
41 'name': '',
42 'instructions': '',
43 'version': '',
44 'items': [],
45 'start_items': [],
46 'test_items': {
47 'pixel_space': [],
48 'world_space': []}}
ee65fee0 49 self.__inspector = None
2aeb9f68
FC
50 self.__context = context
51 self.__add_item = add_item
d3a2e50a
FC
52 self._font = base.loader.load_font(
53 'assets/fonts/Hanken-Book.ttf')
54 self._font.clear()
55 self._font.set_pixels_per_unit(60)
56 self._font.set_minfilter(Texture.FTLinearMipmapLinear)
57 self._font.set_outline((0, 0, 0, 1), .8, .2)
fd2a4e5d
FC
58 self.__item_classes = [Basketball, Box, Domino, Shelf, TeeterTooter]
59 self.__item_strategy_classes = [FixedStrategy, StillStrategy, HitStrategy, UpStrategy, DownStrategy]
d3a2e50a
FC
60 self._common = {
61 'scale': .046,
62 'text_font': self._font,
63 'text_fg': (.9, .9, .9, 1),
64 'relief': FLAT,
65 'frameColor': (.4, .4, .4, .14),
66 'rolloverSound': loader.load_sfx(
67 'assets/audio/sfx/rollover.ogg'),
68 'clickSound': loader.load_sfx(
69 'assets/audio/sfx/click.ogg')}
a810a9ed 70 tooltip_args = self._common['text_font'], self._common['scale'], self._common['text_fg']
2c2198af 71 w, h, tw, l = 1.8, 1.1, 30, .36
c617fd93 72 self._frm = AugmentedDirectFrame(frameColor=(.4, .4, .4, .06),
c48bfe58
FC
73 frameSize=(0, w, 0, h),
74 parent=base.a2dBottomCenter,
75 pos=(-w/2, 0, 0),
c617fd93 76 delta_drag=LPoint3f(0, 0, h),
3466af49
FC
77 collapse_pos=(.06, 1, .94),
78 pos_mgr=self.__pos_mgr,
79 frame_name='scene')
d3a2e50a 80 OnscreenText(
3e3c4caf 81 _('Filename'), pos=(l - .03, h - .1), parent=self._frm,
d3a2e50a
FC
82 font=self._common['text_font'],
83 scale=self._common['scale'],
84 fg=self._common['text_fg'],
85 align=TextNode.A_right)
3e3c4caf 86 self.__filenamename_entry = DirectEntry(
d3a2e50a
FC
87 scale=self._common['scale'],
88 pos=(l, 1, h - .1),
89 entryFont=self._font,
90 width=tw,
91 frameColor=self._common['frameColor'],
3e3c4caf
FC
92 initialText=json_name,
93 parent=self._frm,
94 text_fg=self._common['text_fg'])
9199c6aa 95 self.__filenamename_entry['text_fg'] = (1, 0, 0, 1)
a810a9ed
FC
96 self.__filenamename_entry.__class__ = type('DirectEntryMixed', (DirectEntry, DirectGuiMixin), {})
97 self.__filenamename_entry.set_tooltip(_('The name of the file without the extension'), *tooltip_args)
3466af49 98 self.__pos_mgr['editor_scene_filename'] = self.__filenamename_entry.pos_pixel()
3e3c4caf
FC
99 OnscreenText(
100 _('Name'), pos=(l - .03, h - .2), parent=self._frm,
101 font=self._common['text_font'],
102 scale=self._common['scale'],
103 fg=self._common['text_fg'],
104 align=TextNode.A_right)
105 self.__name_entry = DirectEntry(
106 scale=self._common['scale'],
107 pos=(l, 1, h - .2),
108 entryFont=self._font,
109 width=tw,
110 frameColor=self._common['frameColor'],
d3a2e50a
FC
111 initialText=json['name'],
112 parent=self._frm,
113 text_fg=self._common['text_fg'])
a810a9ed
FC
114 self.__name_entry.__class__ = type('DirectEntryMixed', (DirectEntry, DirectGuiMixin), {})
115 self.__name_entry.set_tooltip(_('The title of the scene'), *tooltip_args)
3466af49 116 self.__pos_mgr['editor_scene_name'] = self.__name_entry.pos_pixel()
d3a2e50a 117 OnscreenText(
2c2198af
FC
118 _('Background'), pos=(l - .03, h - .3), parent=self._frm,
119 font=self._common['text_font'],
120 scale=self._common['scale'],
121 fg=self._common['text_fg'],
122 align=TextNode.A_right)
123 self.__background_entry = DirectEntry(
124 scale=self._common['scale'],
125 pos=(l, 1, h - .3),
126 entryFont=self._font,
127 width=tw,
128 frameColor=self._common['frameColor'],
129 initialText=json['background'],
130 parent=self._frm,
131 text_fg=self._common['text_fg'])
132 self.__background_entry.__class__ = type('DirectEntryMixed', (DirectEntry, DirectGuiMixin), {})
133 self.__background_entry.set_tooltip(_('The name of the background'), *tooltip_args)
134 self.__pos_mgr['editor_scene_background'] = self.__background_entry.pos_pixel()
135 OnscreenText(
136 _('Description'), pos=(l - .03, h - .4), parent=self._frm,
d3a2e50a
FC
137 font=self._common['text_font'],
138 scale=self._common['scale'],
139 fg=self._common['text_fg'],
140 align=TextNode.A_right)
141 def add_line_break(txt, entry):
142 curpos = entry.node().getCursorPosition()
143 entry.set(txt[:curpos]+ "\n" + txt[curpos:])
144 entry.node().setCursorPosition(curpos+1)
145 entry['focus']=1
146 self.__instructions_entry = DirectEntry(
147 scale=self._common['scale'],
2c2198af 148 pos=(l, 1, h - .4),
d3a2e50a
FC
149 entryFont=self._font,
150 width=tw,
151 numLines=12,
152 cursorKeys=True,
153 frameColor=self._common['frameColor'],
154 initialText=json['instructions'].replace('\\n', '\n'),
155 parent=self._frm,
156 text_fg=self._common['text_fg'])
157 self.__instructions_entry['command'] = add_line_break
158 self.__instructions_entry['extraArgs'] = [self.__instructions_entry]
a810a9ed
FC
159 self.__instructions_entry.__class__ = type('DirectEntryMixed', (DirectEntry, DirectGuiMixin), {})
160 self.__instructions_entry.set_tooltip(_('The description of the scene'), *tooltip_args)
3466af49 161 self.__pos_mgr['editor_scene_instructions'] = self.__instructions_entry.pos_pixel()
d3a2e50a
FC
162 def load_images_btn(path, col):
163 colors = {
164 'gray': [
165 (.6, .6, .6, 1), # ready
166 (1, 1, 1, 1), # press
167 (.8, .8, .8, 1), # rollover
168 (.4, .4, .4, .4)],
169 'green': [
170 (.1, .68, .1, 1),
171 (.1, 1, .1, 1),
172 (.1, .84, .1, 1),
173 (.4, .1, .1, .4)]}[col]
174 return [self.__load_img_btn(path, col) for col in colors]
175 fcols = (.4, .4, .4, .14), (.3, .3, .3, .05)
a810a9ed 176 b = DirectButton(
d3a2e50a
FC
177 image=load_images_btn('exitRight', 'gray'), scale=.05,
178 pos=(.06, 1, .06),
179 parent=self._frm, command=self.__on_close, state=NORMAL, relief=FLAT,
180 frameColor=fcols[0],
181 rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
182 clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
a810a9ed 183 b.__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
3466af49 184 self.__pos_mgr['editor_close'] = b.pos_pixel()
a810a9ed
FC
185 b.set_tooltip(_('Close the scene editor'), *tooltip_args)
186 b = DirectButton(
d3a2e50a
FC
187 image=load_images_btn('save', 'gray'), scale=.05,
188 pos=(.06, 1, .18),
189 parent=self._frm, command=self.__on_save, state=NORMAL, relief=FLAT,
190 frameColor=fcols[0],
191 rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
192 clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
a810a9ed 193 b.__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
3466af49 194 self.__pos_mgr['editor_save'] = b.pos_pixel()
a810a9ed
FC
195 b.set_tooltip(_('Save the scene'), *tooltip_args)
196 b = DirectButton(
012cf969
FC
197 image=load_images_btn('menuList', 'gray'), scale=.05,
198 pos=(.06, 1, .30),
199 parent=self._frm, command=self.__on_scene_list, state=NORMAL, relief=FLAT,
200 frameColor=fcols[0],
201 rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
202 clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
a810a9ed 203 b.__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
3466af49 204 self.__pos_mgr['editor_sorting'] = b.pos_pixel()
a810a9ed 205 b.set_tooltip(_('Set the sorting of the scenes'), *tooltip_args)
fd2a4e5d
FC
206 # item_modules = glob('pmachines/items/*.py')
207 # item_modules = [basename(i)[:-3] for i in item_modules]
2aeb9f68 208 self.__new_items = {}
fd2a4e5d
FC
209 # for item_module in item_modules:
210 # mod_name = 'pmachines.items.' + item_module
211 # for member in import_module(mod_name).__dict__.values():
212 # if isclass(member) and issubclass(member, Item) and \
213 # member != Item:
214 # self.__new_items[member.__name__] = member
215 for i in self.__item_classes: self.__new_items[i.__name__] = i
2aeb9f68
FC
216 OnscreenText(
217 _('new item'), pos=(.02, .46), parent=self._frm,
218 font=self._common['text_font'],
219 scale=self._common['scale'],
220 fg=self._common['text_fg'],
221 align=TextNode.A_left)
3466af49
FC
222 items = list(self.__new_items.keys())
223 def new_item_test_set(comps):
224 item_labels = [f'new_item_{i.lower()}' for i in items]
225 for i in item_labels:
226 if i in self.__pos_mgr:
227 del self.__pos_mgr[i]
228 for l, b in zip(item_labels, comps):
229 b.__class__ = type('DirectFrameMixed', (DirectFrame, DirectGuiMixin), {})
230 p = b.pos_pixel()
231 self.__pos_mgr[l] = (p[0] + 5, p[1])
232 b = DirectOptionMenuTestable(
2aeb9f68 233 scale=.05,
3466af49 234 text=_('new item'), pos=(.02, 1, .4), items=items,
2aeb9f68
FC
235 parent=self._frm, command=self.__on_new_item, state=NORMAL,
236 relief=FLAT, item_relief=FLAT,
237 frameColor=fcols[0], item_frameColor=fcols[0],
238 popupMenu_frameColor=fcols[0],
239 popupMarker_frameColor=fcols[0],
240 text_font=self._font, text_fg=(.9, .9, .9, 1),
241 highlightColor=(.9, .9, .9, .9),
242 item_text_font=self._font, item_text_fg=(.9, .9, .9, 1),
243 rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
244 clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
a810a9ed 245 b.__class__ = type('DirectOptionMenuMixed', (DirectOptionMenu, DirectGuiMixin), {})
3466af49 246 b._show_cb = new_item_test_set
a810a9ed 247 b.set_tooltip(_('Create a new item'), *tooltip_args)
3466af49 248 self.__pos_mgr['editor_new_item'] = b.pos_pixel()
a810a9ed 249 b = DirectButton(
9981d472 250 image=load_images_btn('start_items', 'gray'), scale=.05,
3e3c4caf 251 pos=(.06, 1, .58),
9981d472
FC
252 parent=self._frm, command=self.__on_start_items, state=NORMAL, relief=FLAT,
253 frameColor=fcols[0],
254 rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
255 clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
a810a9ed 256 b.__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
3466af49 257 self.__pos_mgr['editor_start'] = b.pos_pixel()
a810a9ed
FC
258 b.set_tooltip(_('Initial items'), *tooltip_args)
259 b = DirectButton(
9981d472
FC
260 image=load_images_btn('plus', 'gray'), scale=.05,
261 pos=(.06, 1, .7),
3e3c4caf
FC
262 parent=self._frm, command=self.__on_new_scene, state=NORMAL, relief=FLAT,
263 frameColor=fcols[0],
264 rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
265 clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
a810a9ed 266 b.__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
3466af49 267 self.__pos_mgr['editor_new'] = b.pos_pixel()
a810a9ed 268 b.set_tooltip(_('New scene'), *tooltip_args)
3133e30f
FC
269 self.__test_items = []
270 self.__set_test_items()
4a71e3e4 271 self.__start_items = self.__scene_list = None
ee65fee0 272 messenger.send('editor-start')
2aeb9f68 273 self.accept('editor-item-click', self.__on_item_click)
ee65fee0 274 self.accept('editor-inspector-destroy', self.__on_inspector_destroy)
9981d472
FC
275 self.accept('editor-start-items-save', self.__on_start_items_save)
276 self.accept('editor-start-items-destroy', self.__on_start_items_destroy)
d3a2e50a 277
3133e30f
FC
278 def __set_test_items(self):
279 for pixel_space_item in self.__json['test_items']['pixel_space']:
280 print(pixel_space_item['id'], pixel_space_item['position'])
281 pos_pixel = pixel_space_item['position']
282 win_res = base.win.getXSize(), base.win.getYSize()
283 pos_win = (pos_pixel[0] / win_res[0] * 2 - 1,
284 1 - pos_pixel[1] / win_res[1] * 2)
bf77b5d5 285 p_from, p_to = Point(pos_win).from_to_points()
3133e30f
FC
286 for hit in self.__world.ray_test_all(p_from, p_to).get_hits():
287 if hit.get_node() == self.__mouse_plane_node:
288 pos = hit.get_hit_pos()
289 self.__set_test_item(pos, pixel_space_item, PixelSpaceTestItem)
290 for world_space_item in self.__json['test_items']['world_space']:
291 print(world_space_item['id'], world_space_item['position'])
292 self.__set_test_item(world_space_item['position'], world_space_item, WorldSpaceTestItem)
293
294 def __set_test_item(self, pos, json, item_class):
295 test_item = item_class(
296 self.__context.world,
297 self.__context.plane_node,
298 self.__context.cb_inst,
299 self.__context.curr_bottom,
300 self.__context.repos,
301 json,
302 pos=(pos[0], 0, pos[-1]),
303 model_scale=.2)
304 self.__test_items += [test_item]
305
d3a2e50a
FC
306 def __on_close(self):
307 self.__json['name'] = self.__name_entry.get()
2c2198af 308 self.__json['background'] = self.__background_entry.get()
d3a2e50a
FC
309 self.__json['instructions'] = self.__instructions_entry.get()
310 if self.__compute_hash() == self.__json['version']:
3133e30f 311 self.destroy()
d3a2e50a
FC
312 else:
313 self.__dialog = YesNoDialog(dialogName='Unsaved changes',
314 text=_('You have unsaved changes. Really quit?'),
315 command=self.__actually_close)
012cf969
FC
316 self.__dialog['frameColor'] = (.4, .4, .4, .14)
317 self.__dialog['relief'] = FLAT
318 self.__dialog.component('text0')['fg'] = (.9, .9, .9, 1)
319 self.__dialog.component('text0')['font'] = self._font
320 for b in self.__dialog.buttonList:
321 b['frameColor'] = (.4, .4, .4, .14)
322 b.component('text0')['fg'] = (.9, .9, .9, 1)
323 b.component('text0')['font'] = self._font
324 b.component('text1')['fg'] = (.9, .1, .1, 1)
325 b.component('text1')['font'] = self._font
326 b.component('text2')['fg'] = (.9, .9, .1, 1)
327 b.component('text2')['font'] = self._font
d3a2e50a 328
2aeb9f68
FC
329 def __on_new_item(self, item):
330 info(f'new {item}')
331 item_json = {}
3133e30f
FC
332 scale = 1
333 if item in ['PixelSpaceTestItem', 'WorldSpaceTestItem']:
334 scale = .2
2aeb9f68
FC
335 _item = self.__new_items[item](
336 self.__context.world,
337 self.__context.plane_node,
338 self.__context.cb_inst,
339 self.__context.curr_bottom,
340 self.__context.repos,
3133e30f
FC
341 item_json,
342 model_scale=scale)
2aeb9f68
FC
343 _item._Item__editing = True
344 self.__add_item(_item)
345 item_json['class'] = _item.__class__.__name__
346 item_json['position'] = list(_item._np.get_pos())
3133e30f
FC
347 if item == 'PixelSpaceTestItem':
348 del item_json['class']
349 self.__json['test_items']['pixel_space'] += [item_json]
350 elif item == 'WorldSpaceTestItem':
351 del item_json['class']
352 self.__json['test_items']['world_space'] += [item_json]
353 else:
354 self.__json['items'] += [item_json]
2aeb9f68 355
d3a2e50a
FC
356 def __actually_close(self, arg):
357 if arg:
3e3c4caf 358 self.destroy()
ee65fee0 359 messenger.send('editor-stop')
d3a2e50a 360 self.__dialog.cleanup()
3466af49 361 self.__dialog = None
d3a2e50a
FC
362
363 def __on_save(self):
364 self.__json['name'] = self.__name_entry.get()
2c2198af 365 self.__json['background'] = self.__background_entry.get()
d3a2e50a
FC
366 self.__json['instructions'] = self.__instructions_entry.get()
367 self.__json['version'] = self.__compute_hash()
3e3c4caf
FC
368 json_name = self.__filenamename_entry.get()
369 with open('assets/scenes/%s.json' % json_name, 'w') as f:
d3a2e50a
FC
370 f.write(dumps(self.__json, indent=2, sort_keys=True))
371
012cf969 372 def __on_scene_list(self):
3466af49 373 self.__scene_list = SceneList(self.__pos_mgr)
012cf969 374
d3a2e50a
FC
375 def __load_img_btn(self, path, col):
376 img = OnscreenImage('assets/images/buttons/%s.dds' % path)
377 img.set_transparency(True)
378 img.set_color(col)
379 img.detach_node()
380 return img
381
382 def __compute_hash(self):
383 new_dict = deepcopy(self.__json)
384 del new_dict['version']
385 new_dict_str = str(dumps(new_dict, indent=2, sort_keys=True))
386 h = hashlib.new('sha256')
387 h.update(new_dict_str.encode())
388 return h.hexdigest()[:12]
ee65fee0 389
2aeb9f68 390 def __on_item_click(self, item):
ee65fee0
FC
391 if self.__inspector and self.__inspector.item == item: return
392 if self.__inspector:
393 self.__inspector.destroy()
3133e30f
FC
394 if item.__class__ == PixelSpaceTestItem:
395 self.__inspector = PixelSpaceInspector(item, self.__items)
396 elif item.__class__ == WorldSpaceTestItem:
3466af49 397 self.__inspector = WorldSpaceInspector(item, self.__items, self.__pos_mgr)
3133e30f 398 else:
fd2a4e5d 399 self.__inspector = Inspector(item, self.__items, self.__pos_mgr, self.__item_strategy_classes)
ee65fee0
FC
400
401 def __on_inspector_destroy(self):
402 self.__inspector = None
3e3c4caf
FC
403
404 def __on_new_scene(self):
405 self.destroy()
406 messenger.send('editor-stop')
407 messenger.send('new_scene')
408
9981d472 409 def __on_start_items(self):
fd2a4e5d 410 self.__start_items = StartItems(self.__json['start_items'], self.__pos_mgr, self.__item_classes, self.__item_strategy_classes)
9981d472
FC
411
412 def __on_start_items_save(self, start_items):
413 self.__json['start_items'] = start_items
414 self.__on_save()
415
416 def __on_start_items_destroy(self):
417 self.__start_items = None
418
3133e30f
FC
419 @property
420 def test_items(self):
421 return self.__test_items
422
3e3c4caf
FC
423 def destroy(self):
424 self._frm.destroy()
425 if self.__inspector:
426 self.__inspector.destroy()
427 self.ignore('editor-item-click')
428 self.ignore('editor-inspector-destroy')
9981d472
FC
429 self.ignore('editor-start-items-save')
430 self.ignore('editor-start-items-destroy')
3466af49 431 if self.__dialog: self.__actually_close(False)
3133e30f
FC
432 for t in self.__test_items: t.destroy()
433 self.__test_items = []
4a71e3e4
FC
434 if self.__start_items:
435 self.__start_items.destroy()
436 if self.__scene_list:
437 self.__scene_list.destroy()