ya2 · news · projects · code · about

housekeeping: ya2.utils.gui
[pmachines.git] / pmachines / scene / scene.py
1 from os.path import exists
2 from os import makedirs
3 from logging import info
4 from json import loads
5 from collections import namedtuple
6 from panda3d.core import AmbientLight, Texture, TextPropertiesManager, \
7 TextNode, Spotlight, PerspectiveLens, BitMask32
8 from panda3d.bullet import BulletPlaneShape, BulletGhostNode
9 from direct.gui.OnscreenImage import OnscreenImage
10 from direct.gui.OnscreenText import OnscreenText
11 from direct.gui.DirectGui import DirectButton, DirectFrame
12 from direct.gui.DirectGuiGlobals import FLAT, DISABLED, NORMAL
13 from direct.showbase.DirectObject import DirectObject
14 from direct.interval.IntervalGlobal import Sequence, Func
15 from direct.interval.LerpInterval import LerpFunctionInterval
16 from pmachines.items.background import Background
17 from pmachines.gui.sidepanel import SidePanel
18 from pmachines.items.box import Box, HitStrategy
19 from pmachines.items.basketball import Basketball
20 from pmachines.items.domino import Domino, DownStrategy
21 from pmachines.items.shelf import Shelf
22 from pmachines.items.teetertooter import TeeterTooter
23 from pmachines.items.test_item import PixelSpaceTestItem, WorldSpaceTestItem
24 from pmachines.editor.scene import SceneEditor
25 from ya2.utils.gui.cursor import MouseCursor, MouseCursorInfo
26 from ya2.utils.gfx import GfxTools, DirectGuiMixin
27 from ya2.utils.gui.gui import GuiTools
28
29
30 class Scene(DirectObject):
31
32 json_files = {}
33 scenes_done = []
34
35 def __init__(self, world, exit_cb, auto_close_instr, dbg_items, reload_cb, scenes, pos_mgr, testing, mouse_coords, persistent, json_name, editor, auto_start_editor):
36 super().__init__()
37 self._world = world
38 self._exit_cb = exit_cb
39 self._testing = testing
40 self._mouse_coords = mouse_coords
41 self._dbg_items = dbg_items
42 self._reload_cb = reload_cb
43 self._pos_mgr = pos_mgr
44 for k in list(self._pos_mgr.keys()): del self._pos_mgr[k]
45 self._scenes = scenes
46 self._start_evt_time = None
47 self._enforce_result = ''
48 self.__persistent = persistent
49 self.__json_name = json_name
50 self.__editor = editor
51 self.__scene_editor = None
52 self.json = {}
53 self.accept('enforce_result', self.enforce_result)
54 self._set_camera()
55 c = MouseCursorInfo(
56 'assets/images/buttons/arrowUpLeft.dds', testing, (.04, 1, .04), (.5, .5, .5, 1),
57 (.01, .01))
58 self._cursor = MouseCursor(c)
59 self.__set_font()
60 self._set_gui()
61 self._set_lights()
62 self._set_input()
63 self._set_mouse_plane()
64 self.__items = []
65 self._test_items = []
66 self.reset()
67 self._state = 'init'
68 self._paused = False
69 self._item_active = None
70 if auto_close_instr:
71 self.__store_state()
72 self.__restore_state()
73 elif self.__json_name:
74 self._set_instructions()
75 self._bg = Background()
76 self._side_panel = SidePanel(world, self._mouse_plane_node, (-5, 4), (-3, 1), 1, self.__items)
77 self._scene_tsk = taskMgr.add(self.on_frame, 'scene_on_frame')
78 if auto_start_editor:
79 self._set_editor()
80 self.accept('editor-inspector-delete', self.__on_inspector_delete)
81
82 @classmethod
83 def filename(cls, scene_name):
84 return f'assets/scenes/{scene_name}.json'
85
86 @classmethod
87 def name(cls, scene_name):
88 if not scene_name in cls.json_files:
89 with open(cls.filename(scene_name)) as f:
90 cls.json_files[scene_name] = loads(f.read())
91 return _(cls.json_files[scene_name]['name'])
92
93 @classmethod
94 def version(cls, scene_name):
95 if not scene_name: return ''
96 if not scene_name in cls.json_files:
97 with open(cls.filename(scene_name)) as f:
98 cls.json_files[scene_name] = loads(f.read())
99 return cls.json_files[scene_name]['version']
100
101 @classmethod
102 def is_done(cls, scene_name):
103 if not cls.scenes_done or len(cls.scenes_done) == 1 and not cls.scenes_done[0]:
104 return False
105 return bool([(name, version) for name, version in cls.scenes_done if scene_name == name and cls.version(scene_name) == version])
106
107 def _instr_txt(self):
108 txt = _('Scene: ') + self.name(self.__json_name) + '\n\n'
109 txt += _(self.__process_json_escape(self.__class__.json_files[self.__json_name]['instructions']))
110 return txt
111
112 def __process_json_escape(self, string):
113 return bytes(string, 'utf-8').decode('unicode-escape')
114
115 @property
116 def items(self):
117 items = self.__items[:]
118 if self.__scene_editor:
119 items += self.__scene_editor.test_items
120 return items
121
122 def _set_items(self):
123 if not self.__json_name: return
124 self.__items = []
125 self._test_items = []
126 if not self.json:
127 with open(f'assets/scenes/{self.__json_name}.json') as f:
128 self.json = loads(f.read())
129 for item in self.json['start_items']:
130 args = {
131 'world': self._world,
132 'plane_node': self._mouse_plane_node,
133 'cb_inst': self.cb_inst,
134 'curr_bottom': self.current_bottom,
135 'repos': self.repos,
136 'count': item['count'],
137 'json': item}
138 if 'mass' in item:
139 args['mass'] = item['mass']
140 if 'friction' in item:
141 args['friction'] = item['friction']
142 self.__items += [self.__code2class(item['class'])(**args)]
143 for item in self.json['items']:
144 args = {
145 'world': self._world,
146 'plane_node': self._mouse_plane_node,
147 'cb_inst': self.cb_inst,
148 'curr_bottom': self.current_bottom,
149 'repos': self.repos,
150 'json': item}
151 args['pos'] = tuple(item['position'])
152 if 'mass' in item:
153 args['mass'] = item['mass']
154 if 'friction' in item:
155 args['friction'] = item['friction']
156 if 'roll' in item:
157 args['r'] = item['roll']
158 if 'model_scale' in item:
159 args['model_scale'] = item['model_scale']
160 if 'restitution' in item:
161 args['restitution'] = item['restitution']
162 if 'friction' in item:
163 args['friction'] = item['friction']
164 self.__items += [self.__code2class(item['class'])(**args)]
165 if 'strategy' in item:
166 match item['strategy']:
167 case 'DownStrategy':
168 self.__items[-1].set_strategy(self.__code2class(item['strategy'])(self.__items[-1]._np, *item['strategy_args']))
169 case 'HitStrategy':
170 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))
171
172 def __code2class(self, code):
173 return {
174 'Box': Box,
175 'Basketball': Basketball,
176 'Domino': Domino,
177 'Shelf': Shelf,
178 'TeeterTooter': TeeterTooter,
179 'DownStrategy': DownStrategy,
180 'HitStrategy': HitStrategy
181 }[code]
182
183 def __item_with_id(self, id):
184 for item in self.__items:
185 if 'id' in item.json and item.json['id'] == id:
186 return item
187
188 def screenshot(self, task=None):
189 tex = Texture('screenshot')
190 buffer = base.win.make_texture_buffer('screenshot', 512, 512, tex, True )
191 cam = base.make_camera(buffer)
192 cam.reparent_to(render)
193 cam.node().get_lens().set_fov(base.camLens.get_fov())
194 cam.set_pos(0, -20, 0)
195 cam.look_at(0, 0, 0)
196 import simplepbr
197 simplepbr.init(
198 window=buffer,
199 camera_node=cam,
200 use_normal_maps=True,
201 use_emission_maps=False,
202 use_occlusion_maps=True,
203 msaa_samples=4,
204 enable_shadows=True)
205 base.graphicsEngine.renderFrame()
206 base.graphicsEngine.renderFrame()
207 fname = self.__json_name
208 if not exists('assets/images/scenes'):
209 makedirs('assets/images/scenes')
210 buffer.save_screenshot('assets/images/scenes/%s.png' % fname)
211 # img = DirectButton(
212 # frameTexture=buffer.get_texture(), relief=FLAT,
213 # frameSize=(-.2, .2, -.2, .2))
214 return buffer.get_texture()
215
216 def current_bottom(self):
217 curr_bottom = 1
218 for item in self.__items:
219 if item.repos_done:
220 curr_bottom = min(curr_bottom, item.get_bottom())
221 return curr_bottom
222
223 def reset(self):
224 [itm.destroy() for itm in self.__items]
225 [itm.remove_node() for itm in self._test_items]
226 self.__items = []
227 self._test_items = []
228 self._set_items()
229 self._set_test_items()
230 self._state = 'init'
231 self._commands = []
232 self._command_idx = 0
233 self._start_evt_time = None
234 if hasattr(self, '_success_txt'):
235 self._success_txt.destroy()
236 del self._success_txt
237 self.__right_btn['state'] = NORMAL
238
239 def enforce_result(self, val):
240 self._enforce_result = val
241 info('enforce result: ' + val)
242
243 def destroy(self):
244 self.__intro_sequence.finish()
245 self.ignore('enforce_result')
246 self._unset_gui()
247 self._unset_lights()
248 self._unset_input()
249 self._unset_mouse_plane()
250 [itm.destroy() for itm in self.__items]
251 [itm.remove_node() for itm in self._test_items]
252 self._bg.destroy()
253 self._side_panel.destroy()
254 self._cursor.destroy()
255 taskMgr.remove(self._scene_tsk)
256 if hasattr(self, '_success_txt'):
257 self._success_txt.destroy()
258 self.ignore('editor-inspector-delete')
259 if self.__scene_editor: self.__scene_editor.destroy()
260
261 def _set_camera(self):
262 base.camera.set_pos(0, -20, 0)
263 base.camera.look_at(0, 0, 0)
264 def camera_ani(t):
265 start_v = (1, -5, 1)
266 end_v = (0, -20, 0)
267 curr_pos = (
268 start_v[0] + (end_v[0] - start_v[0]) * t,
269 start_v[1] + (end_v[1] - start_v[1]) * t,
270 start_v[2] + (end_v[2] - start_v[2]) * t)
271 base.camera.set_pos(*curr_pos)
272 self.repos()
273 camera_interval = LerpFunctionInterval(
274 camera_ani,
275 1.2,
276 0,
277 1,
278 blendType='easeInOut')
279 self.__intro_sequence = Sequence(
280 camera_interval,
281 Func(self.repos))
282 self.__intro_sequence.start()
283
284 def __load_img_btn(self, path, col):
285 img = OnscreenImage('assets/images/buttons/%s.dds' % path)
286 img.set_transparency(True)
287 img.set_color(col)
288 img.detach_node()
289 return img
290
291 def _set_gui(self):
292 def load_images_btn(path, col):
293 colors = {
294 'gray': [
295 (.6, .6, .6, 1), # ready
296 (1, 1, 1, 1), # press
297 (.8, .8, .8, 1), # rollover
298 (.4, .4, .4, .4)],
299 'green': [
300 (.1, .68, .1, 1),
301 (.1, 1, .1, 1),
302 (.1, .84, .1, 1),
303 (.4, .1, .1, .4)]}[col]
304 return [self.__load_img_btn(path, col) for col in colors]
305 abl, abr = base.a2dBottomLeft, base.a2dBottomRight
306 btn_info = [
307 ('home', self.on_home, NORMAL, abl, 'gray', _('Exit'), 'right'),
308 ('information', self._set_instructions, NORMAL, abl, 'gray', _('Instructions'), 'right'),
309 ('right', self.on_play, NORMAL, abr, 'green', _('Run'), 'left'),
310 #('next', self.on_next, DISABLED, abr, 'gray'),
311 #('previous', self.on_prev, DISABLED, abr, 'gray'),
312 #('rewind', self.reset, NORMAL, abr, 'gray')
313 ]
314 if self.__editor:
315 btn_info.insert(2, ('wrench', self._set_editor, NORMAL, abl, 'gray', _('Editor'), 'right'))
316 num_l = num_r = 0
317 btns = []
318 tooltip_args = self.__font, .05, (.93, .93, .93, 1)
319 for binfo in btn_info:
320 imgs = load_images_btn(binfo[0], binfo[4])
321 if binfo[3] == base.a2dBottomLeft:
322 sign, num = 1, num_l
323 num_l += 1
324 else:
325 sign, num = -1, num_r
326 num_r += 1
327 fcols = (.4, .4, .4, .14), (.3, .3, .3, .05)
328 btn = DirectButton(
329 image=imgs, scale=.05, pos=(sign * (.06 + .11 * num), 1, .06),
330 parent=binfo[3], command=binfo[1], state=binfo[2], relief=FLAT,
331 frameColor=fcols[0] if binfo[2] == NORMAL else fcols[1],
332 rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
333 clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
334 btn.__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
335 btn.set_transparency(True)
336 t = tooltip_args + (binfo[6],)
337 btn.set_tooltip(binfo[5], *t)
338 self._pos_mgr[binfo[0]] = btn.pos_pixel()
339 btns += [btn]
340 if self.__editor:
341 self.__home_btn, self.__info_btn, self.__editor_btn, self.__right_btn = btns
342 else:
343 self.__home_btn, self.__info_btn, self.__right_btn = btns
344 # , self.__next_btn, self.__prev_btn, self.__rewind_btn
345 if self._dbg_items:
346 self._info_txt = OnscreenText(
347 '', parent=base.a2dTopRight, scale=0.04,
348 pos=(-.03, -.06), fg=(.9, .9, .9, 1), align=TextNode.A_right)
349 if self._mouse_coords:
350 self._coords_txt = OnscreenText(
351 '', parent=base.a2dTopRight, scale=0.04,
352 pos=(-.03, -.12), fg=(.9, .9, .9, 1), align=TextNode.A_right)
353 def update_coords(task):
354 pos = None
355 for hit in self._get_hits():
356 if hit.get_node() == self._mouse_plane_node:
357 pos = hit.get_hit_pos()
358 if pos:
359 txt = '%s %s' % (round(pos.x, 3),
360 round(pos.z, 3))
361 self._coords_txt['text'] = txt
362 return task.cont
363 self._coords_tsk = taskMgr.add(update_coords, 'update_coords')
364
365 def _unset_gui(self):
366 btns = [
367 self.__home_btn, self.__info_btn, self.__right_btn
368 #self.__next_btn, self.__prev_btn, self.__rewind_btn
369 ]
370 if self.__editor: btns += [self.__editor_btn]
371 [btn.destroy() for btn in btns]
372 if self._dbg_items:
373 self._info_txt.destroy()
374 if self._mouse_coords:
375 taskMgr.remove(self._coords_tsk)
376 self._coords_txt.destroy()
377
378 def _set_spotlight(self, name, pos, look_at, color, shadows=False):
379 light = Spotlight(name)
380 if shadows:
381 light.setLens(PerspectiveLens())
382 light_np = render.attach_new_node(light)
383 light_np.set_pos(pos)
384 light_np.look_at(look_at)
385 light.set_color(color)
386 render.set_light(light_np)
387 return light_np
388
389 def _set_lights(self):
390 alight = AmbientLight('alight') # for ao
391 alight.set_color((.15, .15, .15, 1))
392 self._alnp = render.attach_new_node(alight)
393 render.set_light(self._alnp)
394 self._key_light = self._set_spotlight(
395 'key light', (-5, -80, 5), (0, 0, 0), (2.8, 2.8, 2.8, 1))
396 self._shadow_light = self._set_spotlight(
397 'key light', (-5, -80, 5), (0, 0, 0), (.58, .58, .58, 1), True)
398 self._shadow_light.node().set_shadow_caster(True, 2048, 2048)
399 self._shadow_light.node().get_lens().set_film_size(2048, 2048)
400 self._shadow_light.node().get_lens().set_near_far(1, 256)
401 self._shadow_light.node().set_camera_mask(BitMask32(0x01))
402
403 def _unset_lights(self):
404 for light in [self._alnp, self._key_light, self._shadow_light]:
405 render.clear_light(light)
406 light.remove_node()
407
408 def _set_input(self):
409 self.accept('mouse1', self.on_click_l)
410 self.accept('mouse1-up', self.on_release)
411 self.accept('mouse3', self.on_click_r)
412 self.accept('mouse3-up', self.on_release)
413
414 def _unset_input(self):
415 for evt in ['mouse1', 'mouse1-up', 'mouse3', 'mouse3-up']:
416 self.ignore(evt)
417
418 def _set_mouse_plane(self):
419 shape = BulletPlaneShape((0, -1, 0), 0)
420 #self._mouse_plane_node = BulletRigidBodyNode('mouse plane')
421 self._mouse_plane_node = BulletGhostNode('mouse plane')
422 self._mouse_plane_node.addShape(shape)
423 #np = render.attachNewNode(self._mouse_plane_node)
424 #self._world.attachRigidBody(self._mouse_plane_node)
425 self._world.attach_ghost(self._mouse_plane_node)
426
427 def _unset_mouse_plane(self):
428 self._world.remove_ghost(self._mouse_plane_node)
429
430 def _get_hits(self):
431 if not base.mouseWatcherNode.has_mouse(): return []
432 p_from, p_to = GuiTools.get_mouse().from_to_points()
433 return self._world.ray_test_all(p_from, p_to).get_hits()
434
435 def _update_info(self, item):
436 txt = ''
437 if item:
438 txt = '%.3f %.3f\n%.3f°' % (
439 item._np.get_x(), item._np.get_z(), item._np.get_r())
440 self._info_txt['text'] = txt
441
442 def _on_click(self, method):
443 if self._paused:
444 return
445 for hit in self._get_hits():
446 if hit.get_node() == self._mouse_plane_node:
447 pos = hit.get_hit_pos()
448 for hit in self._get_hits():
449 for item in [i for i in self.items if hit.get_node() == i.node and i.interactable]:
450 if not self._item_active:
451 self._item_active = item
452 if item not in self.__items:
453 method = 'on_click_l'
454 getattr(item, method)(pos)
455 img = 'move' if method == 'on_click_l' else 'rotate'
456 if not (img == 'rotate' and not item._instantiated):
457 self._cursor.set_image('assets/images/buttons/%s.dds' % img)
458
459 def on_click_l(self):
460 self._on_click('on_click_l')
461
462 def on_click_r(self):
463 self._on_click('on_click_r')
464
465 def on_release(self):
466 if self._item_active and not self._item_active._first_command:
467 self._commands = self._commands[:self._command_idx]
468 self._commands += [self._item_active]
469 self._command_idx += 1
470 #self.__prev_btn['state'] = NORMAL
471 #fcols = (.4, .4, .4, .14), (.3, .3, .3, .05)
472 #self.__prev_btn['frameColor'] = fcols[0]
473 #if self._item_active._command_idx == len(self._item_active._commands) - 1:
474 # self.__next_btn['state'] = DISABLED
475 # self.__next_btn['frameColor'] = fcols[1]
476 self._item_active = None
477 [item.on_release() for item in self.__items]
478 self._cursor.set_image('assets/images/buttons/arrowUpLeft.dds')
479
480 def repos(self):
481 for item in self.__items:
482 item.repos_done = False
483 self.__items = sorted(self.__items, key=lambda itm: itm.__class__.__name__)
484 [item.on_aspect_ratio_changed() for item in self.__items]
485 self._side_panel.update(self.__items)
486 max_x = -float('inf')
487 for item in self.__items:
488 if not item._instantiated:
489 max_x = max(item._np.get_x(), max_x)
490 for item in self.__items:
491 if not item._instantiated:
492 item.repos_x(max_x)
493
494 def on_aspect_ratio_changed(self):
495 self.repos()
496
497 def _win_condition(self):
498 return all(itm.strategy.win_condition() for itm in self.__items) and not self._paused
499
500 def _fail_condition(self):
501 return all(itm.fail_condition() for itm in self.__items) and not self._paused and self._state == 'playing'
502
503 def on_frame(self, task):
504 hits = self._get_hits()
505 pos = None
506 for hit in self._get_hits():
507 if hit.get_node() == self._mouse_plane_node:
508 pos = hit.get_hit_pos()
509 hit_nodes = [hit.get_node() for hit in hits]
510 if self._item_active:
511 items_hit = [self._item_active]
512 else:
513 items_hit = [itm for itm in self.items if itm.node in hit_nodes]
514 items_no_hit = [itm for itm in self.items if itm not in items_hit]
515 [itm.on_mouse_on() for itm in items_hit]
516 [itm.on_mouse_off() for itm in items_no_hit]
517 if pos and self._item_active:
518 self._item_active.on_mouse_move(pos)
519 if self._dbg_items:
520 self._update_info(items_hit[0] if items_hit else None)
521 if not self.__scene_editor and self.__json_name and self._win_condition():
522 self._start_evt_time = None
523 self._set_fail() if self._enforce_result == 'fail' else self._set_win()
524 elif self._state == 'playing' and self._fail_condition():
525 self._start_evt_time = None
526 self._set_win() if self._enforce_result == 'win' else self._set_fail()
527 elif self._testing and self._start_evt_time and globalClock.getFrameTime() - self._start_evt_time > 5.0:
528 self._start_evt_time = None
529 self._set_win() if self._enforce_result == 'win' else self._set_fail()
530 if any(itm._overlapping for itm in self.items):
531 self._cursor.set_color((.9, .1, .1, 1))
532 else:
533 self._cursor.set_color((.9, .9, .9, 1))
534 return task.cont
535
536 def cb_inst(self, item):
537 self.__items += [item]
538
539 def on_play(self):
540 self._state = 'playing'
541 #self.__prev_btn['state'] = DISABLED
542 #self.__next_btn['state'] = DISABLED
543 self.__right_btn['state'] = DISABLED
544 [itm.play() for itm in self.__items]
545 self._start_evt_time = globalClock.getFrameTime()
546
547 def on_next(self):
548 self._commands[self._command_idx].redo()
549 self._command_idx += 1
550 #fcols = (.4, .4, .4, .14), (.3, .3, .3, .05)
551 #self.__prev_btn['state'] = NORMAL
552 #self.__prev_btn['frameColor'] = fcols[0]
553 #more_commands = self._command_idx < len(self._commands)
554 #self.__next_btn['state'] = NORMAL if more_commands else DISABLED
555 #self.__next_btn['frameColor'] = fcols[0] if more_commands else fcols[1]
556
557 def on_prev(self):
558 self._command_idx -= 1
559 self._commands[self._command_idx].undo()
560 #fcols = (.4, .4, .4, .14), (.3, .3, .3, .05)
561 #self.__next_btn['state'] = NORMAL
562 #self.__next_btn['frameColor'] = fcols[0]
563 #self.__prev_btn['state'] = NORMAL if self._command_idx else DISABLED
564 #self.__prev_btn['frameColor'] = fcols[0] if self._command_idx else fcols[1]
565
566 def on_home(self):
567 self._exit_cb()
568
569 def __set_font(self):
570 self.__font = base.loader.load_font('assets/fonts/Hanken-Book.ttf')
571 self.__font.clear()
572 self.__font.set_pixels_per_unit(60)
573 self.__font.set_minfilter(Texture.FTLinearMipmapLinear)
574 self.__font.set_outline((0, 0, 0, 1), .8, .2)
575
576
577 def _set_instructions(self):
578 self._paused = True
579 self.__store_state()
580 mgr = TextPropertiesManager.get_global_ptr()
581 for name in ['mouse_l', 'mouse_r']:
582 graphic = OnscreenImage('assets/images/buttons/%s.dds' % name)
583 graphic.set_scale(.5)
584 graphic.get_texture().set_minfilter(Texture.FTLinearMipmapLinear)
585 graphic.get_texture().set_anisotropic_degree(2)
586 mgr.set_graphic(name, graphic)
587 graphic.set_z(-.2)
588 graphic.set_transparency(True)
589 graphic.detach_node()
590 frm = DirectFrame(frameColor=(.4, .4, .4, .06),
591 frameSize=(-.6, .6, -.3, .3))
592 self._txt = OnscreenText(
593 self._instr_txt(), parent=frm, font=self.__font, scale=0.06,
594 fg=(.9, .9, .9, 1), align=TextNode.A_left)
595 u_l = self._txt.textNode.get_upper_left_3d()
596 l_r = self._txt.textNode.get_lower_right_3d()
597 w, h = l_r[0] - u_l[0], u_l[2] - l_r[2]
598 btn_scale = .05
599 mar = .06 # margin
600 z = h / 2 - self.__font.get_line_height() * self._txt['scale'][1]
601 z += (btn_scale + 2 * mar) / 2
602 self._txt['pos'] = -w / 2, z
603 u_l = self._txt.textNode.get_upper_left_3d()
604 l_r = self._txt.textNode.get_lower_right_3d()
605 c_l_r = l_r[0], l_r[1], l_r[2] - 2 * mar - btn_scale
606 fsz = u_l[0] - mar, l_r[0] + mar, c_l_r[2] - mar, u_l[2] + mar
607 frm['frameSize'] = fsz
608 colors = [
609 (.6, .6, .6, 1), # ready
610 (1, 1, 1, 1), # press
611 (.8, .8, .8, 1), # rollover
612 (.4, .4, .4, .4)]
613 imgs = [self.__load_img_btn('exitRight', col) for col in colors]
614 btn = DirectButton(
615 image=imgs, scale=btn_scale,
616 pos=(l_r[0] - btn_scale, 1, l_r[2] - mar - btn_scale),
617 parent=frm, command=self.__on_close_instructions, extraArgs=[frm],
618 relief=FLAT, frameColor=(.6, .6, .6, .08),
619 rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
620 clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
621 btn.__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
622 btn.set_transparency(True)
623 self._pos_mgr['close_instructions'] = btn.pos_pixel()
624
625 def _set_win(self):
626 self.__persistent.save_scene(self.__json_name, self.version(self.__json_name))
627 loader.load_sfx('assets/audio/sfx/success.ogg').play()
628 self._paused = True
629 self.__store_state()
630 frm = DirectFrame(frameColor=(.4, .4, .4, .06),
631 frameSize=(-.6, .6, -.3, .3))
632 font = base.loader.load_font('assets/fonts/Hanken-Book.ttf')
633 font.clear()
634 font.set_pixels_per_unit(60)
635 font.set_minfilter(Texture.FTLinearMipmapLinear)
636 font.set_outline((0, 0, 0, 1), .8, .2)
637 self._txt = OnscreenText(
638 _('You win!'),
639 parent=frm,
640 font=font, scale=0.2,
641 fg=(.9, .9, .9, 1))
642 u_l = self._txt.textNode.get_upper_left_3d()
643 l_r = self._txt.textNode.get_lower_right_3d()
644 #w, h = l_r[0] - u_l[0], u_l[2] - l_r[2]
645 h = u_l[2] - l_r[2]
646 btn_scale = .05
647 mar = .06 # margin
648 z = h / 2 - font.get_line_height() * self._txt['scale'][1]
649 z += (btn_scale + 2 * mar) / 2
650 self._txt['pos'] = 0, z
651 u_l = self._txt.textNode.get_upper_left_3d()
652 l_r = self._txt.textNode.get_lower_right_3d()
653 c_l_r = l_r[0], l_r[1], l_r[2] - 2 * mar - btn_scale
654 fsz = u_l[0] - mar, l_r[0] + mar, c_l_r[2] - mar, u_l[2] + mar
655 frm['frameSize'] = fsz
656 colors = [
657 (.6, .6, .6, 1), # ready
658 (1, 1, 1, 1), # press
659 (.8, .8, .8, 1), # rollover
660 (.4, .4, .4, .4)]
661 imgs = [self.__load_img_btn('home', col) for col in colors]
662 btn = DirectButton(
663 image=imgs, scale=btn_scale,
664 pos=(-2.8 * btn_scale, 1, l_r[2] - mar - btn_scale),
665 parent=frm, command=self._on_end_home, extraArgs=[frm],
666 relief=FLAT, frameColor=(.6, .6, .6, .08),
667 rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
668 clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
669 btn.__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
670 btn.set_transparency(True)
671 self._pos_mgr['home_win'] = btn.pos_pixel()
672 imgs = [self.__load_img_btn('rewind', col) for col in colors]
673 btn = DirectButton(
674 image=imgs, scale=btn_scale,
675 pos=(0, 1, l_r[2] - mar - btn_scale),
676 parent=frm, command=self._on_restart, extraArgs=[frm],
677 relief=FLAT, frameColor=(.6, .6, .6, .08),
678 rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
679 clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
680 btn.__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
681 self._pos_mgr['replay'] = btn.pos_pixel()
682 btn.set_transparency(True)
683 if self.__json_name:
684 enabled = self._scenes.index(self.__json_name) < len(self._scenes) - 1
685 if enabled:
686 next_scene = self._scenes[self._scenes.index(self.__json_name) + 1]
687 else:
688 next_scene = None
689 else:
690 next_scene = None
691 enabled = False
692 imgs = [self.__load_img_btn('right', col) for col in colors]
693 btn = DirectButton(
694 image=imgs, scale=btn_scale,
695 pos=(2.8 * btn_scale, 1, l_r[2] - mar - btn_scale),
696 parent=frm, command=self._on_next_scene,
697 extraArgs=[frm, next_scene], relief=FLAT,
698 frameColor=(.6, .6, .6, .08),
699 rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
700 clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
701 btn.__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
702 btn['state'] = NORMAL if enabled else DISABLED
703 self._pos_mgr['next'] = btn.pos_pixel()
704 btn.set_transparency(True)
705
706 def _set_fail(self):
707 loader.load_sfx('assets/audio/sfx/success.ogg').play()
708 self._paused = True
709 self.__store_state()
710 frm = DirectFrame(frameColor=(.4, .4, .4, .06),
711 frameSize=(-.6, .6, -.3, .3))
712 font = base.loader.load_font('assets/fonts/Hanken-Book.ttf')
713 font.clear()
714 font.set_pixels_per_unit(60)
715 font.set_minfilter(Texture.FTLinearMipmapLinear)
716 font.set_outline((0, 0, 0, 1), .8, .2)
717 self._txt = OnscreenText(
718 _('You have failed!'),
719 parent=frm,
720 font=font, scale=0.2,
721 fg=(.9, .9, .9, 1))
722 u_l = self._txt.textNode.get_upper_left_3d()
723 l_r = self._txt.textNode.get_lower_right_3d()
724 #w, h = l_r[0] - u_l[0], u_l[2] - l_r[2]
725 h = u_l[2] - l_r[2]
726 btn_scale = .05
727 mar = .06 # margin
728 z = h / 2 - font.get_line_height() * self._txt['scale'][1]
729 z += (btn_scale + 2 * mar) / 2
730 self._txt['pos'] = 0, z
731 u_l = self._txt.textNode.get_upper_left_3d()
732 l_r = self._txt.textNode.get_lower_right_3d()
733 c_l_r = l_r[0], l_r[1], l_r[2] - 2 * mar - btn_scale
734 fsz = u_l[0] - mar, l_r[0] + mar, c_l_r[2] - mar, u_l[2] + mar
735 frm['frameSize'] = fsz
736 colors = [
737 (.6, .6, .6, 1), # ready
738 (1, 1, 1, 1), # press
739 (.8, .8, .8, 1), # rollover
740 (.4, .4, .4, .4)]
741 imgs = [self.__load_img_btn('home', col) for col in colors]
742 btn = DirectButton(
743 image=imgs, scale=btn_scale,
744 pos=(-2.8 * btn_scale, 1, l_r[2] - mar - btn_scale),
745 parent=frm, command=self._on_end_home, extraArgs=[frm],
746 relief=FLAT, frameColor=(.6, .6, .6, .08),
747 rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
748 clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
749 btn.__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
750 self._pos_mgr['home_win'] = btn.pos_pixel()
751 btn.set_transparency(True)
752 imgs = [self.__load_img_btn('rewind', col) for col in colors]
753 btn = DirectButton(
754 image=imgs, scale=btn_scale,
755 pos=(0, 1, l_r[2] - mar - btn_scale),
756 parent=frm, command=self._on_restart, extraArgs=[frm],
757 relief=FLAT, frameColor=(.6, .6, .6, .08),
758 rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
759 clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
760 btn.__class__ = type('DirectButtonMixed', (DirectButton, DirectGuiMixin), {})
761 self._pos_mgr['replay'] = btn.pos_pixel()
762 btn.set_transparency(True)
763
764 def _on_restart(self, frm):
765 self.__on_close_instructions(frm)
766 self.reset()
767
768 def _on_end_home(self, frm):
769 self.__on_close_instructions(frm)
770 self.on_home()
771
772 def _on_next_scene(self, frm, scene):
773 self.__on_close_instructions(frm)
774 self._reload_cb(scene)
775
776 def __store_state(self):
777 btns = [
778 self.__home_btn, self.__info_btn, self.__right_btn,
779 #self.__next_btn, self.__prev_btn, self.__rewind_btn
780 ]
781 if self.__editor: btns += [self.__editor_btn]
782 self.__btn_state = [btn['state'] for btn in btns]
783 for btn in btns:
784 btn['state'] = DISABLED
785 [itm.store_state() for itm in self.__items]
786
787 def __restore_state(self):
788 btns = [
789 self.__home_btn, self.__info_btn, self.__right_btn,
790 #self.__next_btn, self.__prev_btn, self.__rewind_btn
791 ]
792 if self.__editor: btns += [self.__editor_btn]
793 for btn, state in zip(btns, self.__btn_state):
794 btn['state'] = state
795 [itm.restore_state() for itm in self.__items]
796 self._paused = False
797
798 def __on_close_instructions(self, frm):
799 frm.remove_node()
800 self.__restore_state()
801
802 def _set_test_items(self):
803 def frame_after(task):
804 self._define_test_items()
805 for itm in self.items:
806 if itm.id:
807 self._pos_mgr[itm.id] = itm._model.pos2d_pixel()
808 for itm in self._test_items:
809 self._pos_mgr[itm.name] = itm.pos2d_pixel()
810 taskMgr.doMethodLater(1.4, frame_after, 'frame after') # after the intro sequence
811
812 def _define_test_items(self):
813 if not self.__json_name: return
814 if not self.__json_name in self.__class__.json_files:
815 with open(self.__class__.filename(self.__json_name)) as f:
816 self.__class__.json_files[self.__json_name] = loads(f.read())
817 for item in self.__class__.json_files[self.__json_name]['test_items']['pixel_space']:
818 self._pos_mgr[item['id']] = tuple(item['position'])
819 for item in self.__class__.json_files[self.__json_name]['test_items']['world_space']:
820 self._set_test_item(item['id'], tuple(item['position']))
821
822 def _set_test_item(self, name, pos):
823 self._test_items += [GfxTools.build_empty_node(name)]
824 self._test_items[-1].set_pos(pos[0], 0, pos[1])
825
826 def add_item(self, item):
827 self.__items += [item]
828
829 def _set_editor(self):
830 fields = ['world', 'plane_node', 'cb_inst', 'curr_bottom', 'repos', 'json']
831 SceneContext = namedtuple('SceneContext', fields)
832 context = SceneContext(
833 self._world,
834 self._mouse_plane_node,
835 self.cb_inst,
836 self.current_bottom,
837 self.repos,
838 {})
839 self.__scene_editor = SceneEditor(self.json, self.__json_name, context, self.add_item, self.__items, self._world, self._mouse_plane_node, self._pos_mgr)
840
841 def __on_inspector_delete(self, item):
842 if item.__class__ in [WorldSpaceTestItem, PixelSpaceTestItem]:
843 for i in self._test_items:
844 if item.id == i.name:
845 r = i
846 self._test_items.remove(r)
847 else:
848 self.__items.remove(item)
849 j = self.json['items']
850 if item.__class__ == PixelSpaceTestItem:
851 j = self.json['test_items']['pixel_space']
852 if item.__class__ == WorldSpaceTestItem:
853 j = self.json['test_items']['world_space']
854 j.remove(item.json)
855 item.destroy()