ya2 · news · projects · code · about

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