ya2 · news · projects · code · about

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