ya2 · news · projects · code · about

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