ya2 · news · projects · code · about

new scene
[pmachines.git] / pmachines / scene.py
CommitLineData
31237524 1from os.path import exists
a0462193 2from os import makedirs
c991401b 3from logging import info
3fe18d73 4from json import loads
2aeb9f68 5from collections import namedtuple
c991401b
FC
6from panda3d.core import AmbientLight, Texture, TextPropertiesManager, \
7 TextNode, Spotlight, PerspectiveLens, BitMask32, NodePath
36099535
FC
8from panda3d.bullet import BulletPlaneShape, BulletGhostNode
9from direct.gui.OnscreenImage import OnscreenImage
2aaa10d3
FC
10from direct.gui.OnscreenText import OnscreenText
11from direct.gui.DirectGui import DirectButton, DirectFrame
36099535 12from direct.gui.DirectGuiGlobals import FLAT, DISABLED, NORMAL
1be87278 13from direct.showbase.DirectObject import DirectObject
fe0a68a0
FC
14from direct.interval.IntervalGlobal import Sequence, Func
15from direct.interval.LerpInterval import LerpFunctionInterval
4586cbf6
FC
16from pmachines.items.background import Background
17from pmachines.gui.sidepanel import SidePanel
98741d67 18from pmachines.items.box import Box, HitStrategy
25c59f4a 19from pmachines.items.basketball import Basketball
98741d67 20from pmachines.items.domino import Domino, DownStrategy
1f76fd96
FC
21from pmachines.items.shelf import Shelf
22from pmachines.items.teetertooter import TeeterTooter
d3a2e50a 23from pmachines.editor.scene import SceneEditor
b35b1f62
FC
24from ya2.utils.cursor import MouseCursor
25from ya2.p3d.gfx import P3dGfxMgr
2d1773b1 26from ya2.p3d.p3d import LibP3d
1be87278
FC
27
28
29class Scene(DirectObject):
30
f7eade0f 31 json_files = {}
92c29685
FC
32 scenes_done = []
33
d3a2e50a 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):
1be87278
FC
35 super().__init__()
36 self._world = world
5964572b 37 self._exit_cb = exit_cb
ce302b41 38 self._testing = testing
7fa58640 39 self._mouse_coords = mouse_coords
31237524 40 self._dbg_items = dbg_items
9914cfc9 41 self._reload_cb = reload_cb
2d1773b1
FC
42 self._pos_mgr = pos_mgr
43 self._pos_mgr.reset()
6168d0c2 44 self._scenes = scenes
ce302b41 45 self._start_evt_time = None
fa3662a6 46 self._enforce_res = ''
92c29685 47 self.__persistent = persistent
f7eade0f 48 self.__json_name = json_name
d3a2e50a 49 self.__editor = editor
ee65fee0 50 self.__scene_editor = None
f7eade0f 51 self.json = {}
fa3662a6 52 self.accept('enforce_res', self.enforce_res)
1be87278 53 self._set_camera()
79f81d48 54 self._cursor = MouseCursor(
7e487769 55 'assets/images/buttons/arrowUpLeft.dds', (.04, 1, .04), (.5, .5, .5, 1),
79f81d48 56 (.01, .01))
36099535 57 self._set_gui()
1be87278
FC
58 self._set_lights()
59 self._set_input()
c8d8653f 60 self._set_mouse_plane()
d5932612 61 self.items = []
067a36db 62 self._test_items = []
d5932612 63 self.reset()
0a0994e4 64 self._state = 'init'
9830561d 65 self._paused = False
7c0a81ae 66 self._item_active = None
e669403e 67 if auto_close_instr:
32aa4dae 68 self.__store_state()
e669403e
FC
69 self.__restore_state()
70 else:
71 self._set_instructions()
5964572b
FC
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')
d3a2e50a
FC
75 if auto_start_editor:
76 self._set_editor()
2aeb9f68 77 self.accept('editor-inspector-delete', self.__on_inspector_delete)
5964572b 78
aa577aeb 79 @classmethod
f7eade0f
FC
80 def filename(cls, scene_name):
81 return f'assets/scenes/{scene_name}.json'
8c9bf90e 82
3fe18d73 83 @classmethod
f7eade0f
FC
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'])
3fe18d73 89
92c29685 90 @classmethod
f7eade0f 91 def version(cls, scene_name):
3e3c4caf 92 if not scene_name: return ''
f7eade0f
FC
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):
9209a23b
FC
100 if not cls.scenes_done or len(cls.scenes_done) == 1 and not cls.scenes_done[0]:
101 return False
f7eade0f 102 return bool([(name, version) for name, version in cls.scenes_done if scene_name == name and cls.version(scene_name) == version])
92c29685 103
0eff64a3 104 def _instr_txt(self):
f7eade0f 105 txt = _('Scene: ') + self.name(self.__json_name) + '\n\n'
d3a2e50a 106 txt += _(self.__process_json_escape(self.__class__.json_files[self.__json_name]['instructions']))
aa577aeb
FC
107 return txt
108
109 def __process_json_escape(self, string):
110 return bytes(string, 'utf-8').decode('unicode-escape')
0eff64a3
FC
111
112 def _set_items(self):
3e3c4caf 113 if not self.__json_name: return
0eff64a3 114 self.items = []
067a36db 115 self._test_items = []
f7eade0f
FC
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']:
25c59f4a
FC
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,
98741d67
FC
126 'count': item['count'],
127 'json': item}
25c59f4a
FC
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)]
2aeb9f68 133 for item in self.json['items']:
98741d67
FC
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']
2aeb9f68
FC
150 if 'restitution' in item:
151 args['restitution'] = item['restitution']
152 if 'friction' in item:
153 args['friction'] = item['friction']
98741d67
FC
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))
25c59f4a
FC
161
162 def __code2class(self, code):
163 return {
164 'Box': Box,
165 'Basketball': Basketball,
1f76fd96
FC
166 'Domino': Domino,
167 'Shelf': Shelf,
98741d67
FC
168 'TeeterTooter': TeeterTooter,
169 'DownStrategy': DownStrategy,
170 'HitStrategy': HitStrategy
25c59f4a 171 }[code]
0eff64a3 172
98741d67
FC
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
0d5a5427 178 def screenshot(self, task=None):
ecdf8933
FC
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()
f7eade0f 197 fname = self.__json_name
40cc6e36
FC
198 if not exists('assets/images/scenes'):
199 makedirs('assets/images/scenes')
63e7aeb2 200 buffer.save_screenshot('assets/images/scenes/%s.png' % fname)
ecdf8933
FC
201 # img = DirectButton(
202 # frameTexture=buffer.get_texture(), relief=FLAT,
203 # frameSize=(-.2, .2, -.2, .2))
0d5a5427 204 return buffer.get_texture()
ecdf8933 205
d5932612
FC
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
05fd23ec 213 def reset(self):
d5932612 214 [itm.destroy() for itm in self.items]
067a36db
FC
215 [itm.remove_node() for itm in self._test_items]
216 self.items = []
217 self._test_items = []
0eff64a3 218 self._set_items()
067a36db 219 self._set_test_items()
0a0994e4 220 self._state = 'init'
32aa4dae
FC
221 self._commands = []
222 self._command_idx = 0
ce302b41 223 self._start_evt_time = None
b41381b2
FC
224 if hasattr(self, '_success_txt'):
225 self._success_txt.destroy()
226 del self._success_txt
227 self.__right_btn['state'] = NORMAL
05fd23ec 228
fa3662a6
FC
229 def enforce_res(self, val):
230 self._enforce_res = val
74ed9732 231 info('enforce res: ' + val)
fa3662a6 232
5964572b 233 def destroy(self):
fe0a68a0 234 self.__intro_sequence.finish()
fa3662a6 235 self.ignore('enforce_res')
5964572b
FC
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]
067a36db 241 [itm.remove_node() for itm in self._test_items]
5964572b
FC
242 self._bg.destroy()
243 self._side_panel.destroy()
407412a5 244 self._cursor.destroy()
5964572b 245 taskMgr.remove(self._scene_tsk)
9fc7f6fb
FC
246 if hasattr(self, '_success_txt'):
247 self._success_txt.destroy()
3e3c4caf 248 self.ignore('editor-inspector-delete')
1be87278
FC
249
250 def _set_camera(self):
251 base.camera.set_pos(0, -20, 0)
252 base.camera.look_at(0, 0, 0)
fe0a68a0
FC
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()
1be87278 272
a0acba9a 273 def __load_img_btn(self, path, col):
7e487769 274 img = OnscreenImage('assets/images/buttons/%s.dds' % path)
a0acba9a
FC
275 img.set_transparency(True)
276 img.set_color(col)
277 img.detach_node()
278 return img
279
36099535 280 def _set_gui(self):
01b221a6
FC
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]
a0acba9a 293 return [self.__load_img_btn(path, col) for col in colors]
36099535
FC
294 abl, abr = base.a2dBottomLeft, base.a2dBottomRight
295 btn_info = [
5964572b 296 ('home', self.on_home, NORMAL, abl, 'gray'),
9830561d 297 ('information', self._set_instructions, NORMAL, abl, 'gray'),
01b221a6 298 ('right', self.on_play, NORMAL, abr, 'green'),
f26497a5
FC
299 #('next', self.on_next, DISABLED, abr, 'gray'),
300 #('previous', self.on_prev, DISABLED, abr, 'gray'),
301 #('rewind', self.reset, NORMAL, abr, 'gray')
302 ]
d3a2e50a
FC
303 if self.__editor:
304 btn_info.insert(2, ('wrench', self._set_editor, NORMAL, abl, 'gray'))
36099535 305 num_l = num_r = 0
a0acba9a 306 btns = []
36099535 307 for binfo in btn_info:
01b221a6 308 imgs = load_images_btn(binfo[0], binfo[4])
36099535
FC
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,
54a1397e
FC
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'))
36099535 322 btn.set_transparency(True)
2d1773b1 323 self._pos_mgr.register(binfo[0], LibP3d.wdg_pos(btn))
a0acba9a 324 btns += [btn]
d3a2e50a
FC
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
f26497a5 329 # , self.__next_btn, self.__prev_btn, self.__rewind_btn
31237524
FC
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)
7fa58640
FC
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')
36099535 349
5964572b
FC
350 def _unset_gui(self):
351 btns = [
352 self.__home_btn, self.__info_btn, self.__right_btn,
f26497a5
FC
353 #self.__next_btn, self.__prev_btn, self.__rewind_btn
354 ]
5964572b 355 [btn.destroy() for btn in btns]
31237524
FC
356 if self._dbg_items:
357 self._info_txt.destroy()
7fa58640
FC
358 if self._mouse_coords:
359 taskMgr.remove(self._coords_tsk)
360 self._coords_txt.destroy()
5964572b 361
64eae9c7
FC
362 def _set_spotlight(self, name, pos, look_at, color, shadows=False):
363 light = Spotlight(name)
364 if shadows:
365 light.setLens(PerspectiveLens())
1be87278 366 light_np = render.attach_new_node(light)
64eae9c7
FC
367 light_np.set_pos(pos)
368 light_np.look_at(look_at)
1be87278
FC
369 light.set_color(color)
370 render.set_light(light_np)
5964572b 371 return light_np
1be87278
FC
372
373 def _set_lights(self):
374 alight = AmbientLight('alight') # for ao
64eae9c7
FC
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))
5964572b
FC
386
387 def _unset_lights(self):
64eae9c7 388 for light in [self._alnp, self._key_light, self._shadow_light]:
5964572b
FC
389 render.clear_light(light)
390 light.remove_node()
1be87278
FC
391
392 def _set_input(self):
49c79300 393 self.accept('mouse1', self.on_click_l)
c8d8653f 394 self.accept('mouse1-up', self.on_release)
49c79300
FC
395 self.accept('mouse3', self.on_click_r)
396 self.accept('mouse3-up', self.on_release)
c8d8653f 397
5964572b
FC
398 def _unset_input(self):
399 for evt in ['mouse1', 'mouse1-up', 'mouse3', 'mouse3-up']:
400 self.ignore(evt)
401
c8d8653f 402 def _set_mouse_plane(self):
651713a9 403 shape = BulletPlaneShape((0, -1, 0), 0)
c8d8653f
FC
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)
06af3aa9 409 self._world.attach_ghost(self._mouse_plane_node)
1be87278 410
5964572b
FC
411 def _unset_mouse_plane(self):
412 self._world.remove_ghost(self._mouse_plane_node)
413
37ba71d8
FC
414 def _get_hits(self):
415 if not base.mouseWatcherNode.has_mouse(): return []
13263131 416 p_from, p_to = P3dGfxMgr.world_from_to(base.mouseWatcherNode.get_mouse())
37ba71d8
FC
417 return self._world.ray_test_all(p_from, p_to).get_hits()
418
31237524
FC
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
49c79300 426 def _on_click(self, method):
a0acba9a
FC
427 if self._paused:
428 return
c8d8653f
FC
429 for hit in self._get_hits():
430 if hit.get_node() == self._mouse_plane_node:
431 pos = hit.get_hit_pos()
37ba71d8 432 for hit in self._get_hits():
ef3c36bf 433 for item in [i for i in self.items if hit.get_node() == i.node and i.interactable]:
7c0a81ae
FC
434 if not self._item_active:
435 self._item_active = item
49c79300 436 getattr(item, method)(pos)
79f81d48 437 img = 'move' if method == 'on_click_l' else 'rotate'
a6843832 438 if not (img == 'rotate' and not item._instantiated):
7e487769 439 self._cursor.set_image('assets/images/buttons/%s.dds' % img)
49c79300
FC
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')
c8d8653f
FC
446
447 def on_release(self):
32aa4dae
FC
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
f26497a5
FC
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]
7c0a81ae 458 self._item_active = None
49c79300 459 [item.on_release() for item in self.items]
7e487769 460 self._cursor.set_image('assets/images/buttons/arrowUpLeft.dds')
37ba71d8 461
e1438449 462 def repos(self):
d5932612
FC
463 for item in self.items:
464 item.repos_done = False
e1438449 465 self.items = sorted(self.items, key=lambda itm: itm.__class__.__name__)
651713a9 466 [item.on_aspect_ratio_changed() for item in self.items]
a5dc83f4 467 self._side_panel.update(self.items)
7790323d 468 max_x = -float('inf')
e1438449
FC
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()
651713a9 478
0a0994e4 479 def _win_condition(self):
b8161d21 480 return all(itm.strategy.win_condition() for itm in self.items) and not self._paused
0e86689f 481
0a0994e4
FC
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
37ba71d8 485 def on_frame(self, task):
c8d8653f
FC
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]
7c0a81ae
FC
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]
37ba71d8
FC
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]
7c0a81ae
FC
499 if pos and self._item_active:
500 self._item_active.on_mouse_move(pos)
31237524
FC
501 if self._dbg_items:
502 self._update_info(items_hit[0] if items_hit else None)
3e3c4caf 503 if not self.__scene_editor and self._win_condition():
ce302b41 504 self._start_evt_time = None
fa3662a6 505 self._set_fail() if self._enforce_res == 'fail' else self._set_win()
0a0994e4 506 elif self._state == 'playing' and self._fail_condition():
ce302b41
FC
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
fa3662a6 511 self._set_win() if self._enforce_res == 'win' else self._set_fail()
93cf86b2
FC
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)
37ba71d8 516 return task.cont
c8d8653f 517
7850ccc4
FC
518 def cb_inst(self, item):
519 self.items += [item]
520
c8d8653f 521 def on_play(self):
0a0994e4 522 self._state = 'playing'
f26497a5
FC
523 #self.__prev_btn['state'] = DISABLED
524 #self.__next_btn['state'] = DISABLED
b41381b2 525 self.__right_btn['state'] = DISABLED
c8d8653f 526 [itm.play() for itm in self.items]
ce302b41 527 self._start_evt_time = globalClock.getFrameTime()
36099535
FC
528
529 def on_next(self):
32aa4dae
FC
530 self._commands[self._command_idx].redo()
531 self._command_idx += 1
c991401b 532 #fcols = (.4, .4, .4, .14), (.3, .3, .3, .05)
f26497a5
FC
533 #self.__prev_btn['state'] = NORMAL
534 #self.__prev_btn['frameColor'] = fcols[0]
c991401b 535 #more_commands = self._command_idx < len(self._commands)
f26497a5
FC
536 #self.__next_btn['state'] = NORMAL if more_commands else DISABLED
537 #self.__next_btn['frameColor'] = fcols[0] if more_commands else fcols[1]
36099535
FC
538
539 def on_prev(self):
32aa4dae
FC
540 self._command_idx -= 1
541 self._commands[self._command_idx].undo()
c991401b 542 #fcols = (.4, .4, .4, .14), (.3, .3, .3, .05)
f26497a5
FC
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]
36099535 547
36099535 548 def on_home(self):
5964572b 549 self._exit_cb()
36099535 550
2aaa10d3 551 def _set_instructions(self):
9830561d
FC
552 self._paused = True
553 self.__store_state()
2aaa10d3
FC
554 mgr = TextPropertiesManager.get_global_ptr()
555 for name in ['mouse_l', 'mouse_r']:
7e487769 556 graphic = OnscreenImage('assets/images/buttons/%s.dds' % name)
2aaa10d3
FC
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)
2aaa10d3 571 self._txt = OnscreenText(
0eff64a3
FC
572 self._instr_txt(), parent=frm, font=font, scale=0.06,
573 fg=(.9, .9, .9, 1), align=TextNode.A_left)
2aaa10d3
FC
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]
a0acba9a
FC
577 btn_scale = .05
578 mar = .06 # margin
2aaa10d3 579 z = h / 2 - font.get_line_height() * self._txt['scale'][1]
a0acba9a 580 z += (btn_scale + 2 * mar) / 2
2aaa10d3
FC
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()
a0acba9a
FC
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
2aaa10d3 586 frm['frameSize'] = fsz
a0acba9a
FC
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)
2d1773b1 601 self._pos_mgr.register('close_instructions', LibP3d.wdg_pos(btn))
a0acba9a 602
0a0994e4 603 def _set_win(self):
f7eade0f 604 self.__persistent.save_scene(self.__json_name, self.version(self.__json_name))
9914cfc9
FC
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()
c991401b
FC
622 #w, h = l_r[0] - u_l[0], u_l[2] - l_r[2]
623 h = u_l[2] - l_r[2]
9914cfc9
FC
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)
2d1773b1 648 self._pos_mgr.register('home_win', LibP3d.wdg_pos(btn))
9914cfc9
FC
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'))
2d1773b1 657 self._pos_mgr.register('replay', LibP3d.wdg_pos(btn))
9914cfc9 658 btn.set_transparency(True)
3e3c4caf
FC
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
9914cfc9
FC
665 else:
666 next_scene = None
3e3c4caf 667 enabled = False
9914cfc9
FC
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
2d1773b1 678 self._pos_mgr.register('next', LibP3d.wdg_pos(btn))
9914cfc9
FC
679 btn.set_transparency(True)
680
0a0994e4
FC
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()
c991401b
FC
699 #w, h = l_r[0] - u_l[0], u_l[2] - l_r[2]
700 h = u_l[2] - l_r[2]
0a0994e4
FC
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'))
2d1773b1 724 self._pos_mgr.register('home_win', LibP3d.wdg_pos(btn))
0a0994e4
FC
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'))
2d1773b1 734 self._pos_mgr.register('replay', LibP3d.wdg_pos(btn))
0a0994e4
FC
735 btn.set_transparency(True)
736
9914cfc9
FC
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
a0acba9a
FC
749 def __store_state(self):
750 btns = [
751 self.__home_btn, self.__info_btn, self.__right_btn,
f26497a5
FC
752 #self.__next_btn, self.__prev_btn, self.__rewind_btn
753 ]
a0acba9a
FC
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,
f26497a5
FC
762 #self.__next_btn, self.__prev_btn, self.__rewind_btn
763 ]
a0acba9a
FC
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()
067a36db
FC
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))
9209a23b 778 taskMgr.doMethodLater(1.4, frame_after, 'frame after') # after the intro sequence
067a36db 779
da03f030 780 def _define_test_items(self):
3e3c4caf 781 if not self.__json_name: return
f7eade0f
FC
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']:
da03f030 786 self._pos_mgr.register(item['id'], tuple(item['position']))
f7eade0f 787 for item in self.__class__.json_files[self.__json_name]['test_items']['world_space']:
da03f030
FC
788 self._set_test_item(item['id'], tuple(item['position']))
789
067a36db
FC
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])
d3a2e50a 793
2aeb9f68
FC
794 def add_item(self, item):
795 self.items += [item]
796
d3a2e50a 797 def _set_editor(self):
2aeb9f68
FC
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()