ya2 · news · projects · code · about

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