ya2 · news · projects · code · about

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