ya2 · news · projects · code · about

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