ya2 · news · projects · code · about

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