ya2 · news · projects · code · about

second level
[pmachines.git] / pmachines / scene.py
CommitLineData
31237524 1from os.path import exists
2aaa10d3 2from panda3d.core import AmbientLight, DirectionalLight, Point3, Texture, \
bdfa6dda 3 TextPropertiesManager, TextNode, Spotlight, PerspectiveLens, BitMask32
36099535
FC
4from panda3d.bullet import BulletPlaneShape, BulletGhostNode
5from direct.gui.OnscreenImage import OnscreenImage
2aaa10d3
FC
6from direct.gui.OnscreenText import OnscreenText
7from direct.gui.DirectGui import DirectButton, DirectFrame
36099535 8from direct.gui.DirectGuiGlobals import FLAT, DISABLED, NORMAL
1be87278
FC
9from direct.showbase.DirectObject import DirectObject
10from pmachines.items.background import Background
a5dc83f4 11from pmachines.sidepanel import SidePanel
79f81d48 12from lib.engine.gui.cursor import MouseCursor
13263131 13from lib.lib.p3d.gfx import P3dGfxMgr
1be87278
FC
14
15
16class Scene(DirectObject):
17
31237524 18 def __init__(self, world, exit_cb, auto_close_instr, dbg_items):
1be87278
FC
19 super().__init__()
20 self._world = world
5964572b 21 self._exit_cb = exit_cb
31237524 22 self._dbg_items = dbg_items
1be87278 23 self._set_camera()
79f81d48
FC
24 self._cursor = MouseCursor(
25 'assets/buttons/arrowUpLeft.png', (.04, 1, .04), (.5, .5, .5, 1),
26 (.01, .01))
36099535 27 self._set_gui()
1be87278
FC
28 self._set_lights()
29 self._set_input()
c8d8653f 30 self._set_mouse_plane()
d5932612
FC
31 self.items = []
32 self.reset()
9830561d 33 self._paused = False
7c0a81ae 34 self._item_active = None
e669403e 35 if auto_close_instr:
32aa4dae 36 self.__store_state()
e669403e
FC
37 self.__restore_state()
38 else:
39 self._set_instructions()
5964572b
FC
40 self._bg = Background()
41 self._side_panel = SidePanel(world, self._mouse_plane_node, (-5, 4), (-3, 1), 1, self.items)
42 self._scene_tsk = taskMgr.add(self.on_frame, 'on_frame')
43
8c9bf90e
FC
44 @staticmethod
45 def name():
46 return ''
47
0eff64a3
FC
48 def _instr_txt(self):
49 return ''
50
51 def _set_items(self):
52 self.items = []
53
0d5a5427 54 def screenshot(self, task=None):
ecdf8933
FC
55 tex = Texture('screenshot')
56 buffer = base.win.make_texture_buffer('screenshot', 512, 512, tex, True )
57 cam = base.make_camera(buffer)
58 cam.reparent_to(render)
59 cam.node().get_lens().set_fov(base.camLens.get_fov())
60 cam.set_pos(0, -20, 0)
61 cam.look_at(0, 0, 0)
62 import simplepbr
63 simplepbr.init(
64 window=buffer,
65 camera_node=cam,
66 use_normal_maps=True,
67 use_emission_maps=False,
68 use_occlusion_maps=True,
69 msaa_samples=4,
70 enable_shadows=True)
71 base.graphicsEngine.renderFrame()
72 base.graphicsEngine.renderFrame()
73 # buffer.save_screenshot('assets/scene.png')
74 # img = DirectButton(
75 # frameTexture=buffer.get_texture(), relief=FLAT,
76 # frameSize=(-.2, .2, -.2, .2))
0d5a5427 77 return buffer.get_texture()
ecdf8933 78
d5932612
FC
79 def current_bottom(self):
80 curr_bottom = 1
81 for item in self.items:
82 if item.repos_done:
83 curr_bottom = min(curr_bottom, item.get_bottom())
84 return curr_bottom
85
05fd23ec 86 def reset(self):
d5932612 87 [itm.destroy() for itm in self.items]
0eff64a3 88 self._set_items()
32aa4dae
FC
89 self._commands = []
90 self._command_idx = 0
b41381b2
FC
91 if hasattr(self, '_success_txt'):
92 self._success_txt.destroy()
93 del self._success_txt
94 self.__right_btn['state'] = NORMAL
05fd23ec 95
5964572b
FC
96 def destroy(self):
97 self._unset_gui()
98 self._unset_lights()
99 self._unset_input()
100 self._unset_mouse_plane()
101 [itm.destroy() for itm in self.items]
102 self._bg.destroy()
103 self._side_panel.destroy()
407412a5 104 self._cursor.destroy()
5964572b 105 taskMgr.remove(self._scene_tsk)
9fc7f6fb
FC
106 if hasattr(self, '_success_txt'):
107 self._success_txt.destroy()
1be87278
FC
108
109 def _set_camera(self):
110 base.camera.set_pos(0, -20, 0)
111 base.camera.look_at(0, 0, 0)
112
a0acba9a
FC
113 def __load_img_btn(self, path, col):
114 img = OnscreenImage('assets/buttons/%s.png' % path)
115 img.set_transparency(True)
116 img.set_color(col)
117 img.detach_node()
118 return img
119
36099535 120 def _set_gui(self):
01b221a6
FC
121 def load_images_btn(path, col):
122 colors = {
123 'gray': [
124 (.6, .6, .6, 1), # ready
125 (1, 1, 1, 1), # press
126 (.8, .8, .8, 1), # rollover
127 (.4, .4, .4, .4)],
128 'green': [
129 (.1, .68, .1, 1),
130 (.1, 1, .1, 1),
131 (.1, .84, .1, 1),
132 (.4, .1, .1, .4)]}[col]
a0acba9a 133 return [self.__load_img_btn(path, col) for col in colors]
36099535
FC
134 abl, abr = base.a2dBottomLeft, base.a2dBottomRight
135 btn_info = [
5964572b 136 ('home', self.on_home, NORMAL, abl, 'gray'),
9830561d 137 ('information', self._set_instructions, NORMAL, abl, 'gray'),
01b221a6
FC
138 ('right', self.on_play, NORMAL, abr, 'green'),
139 ('next', self.on_next, DISABLED, abr, 'gray'),
140 ('previous', self.on_prev, DISABLED, abr, 'gray'),
05fd23ec 141 ('rewind', self.reset, NORMAL, abr, 'gray')]
36099535 142 num_l = num_r = 0
a0acba9a 143 btns = []
36099535 144 for binfo in btn_info:
01b221a6 145 imgs = load_images_btn(binfo[0], binfo[4])
36099535
FC
146 if binfo[3] == base.a2dBottomLeft:
147 sign, num = 1, num_l
148 num_l += 1
149 else:
150 sign, num = -1, num_r
151 num_r += 1
152 fcols = (.4, .4, .4, .14), (.3, .3, .3, .05)
153 btn = DirectButton(
154 image=imgs, scale=.05, pos=(sign * (.06 + .11 * num), 1, .06),
155 parent=binfo[3], command=binfo[1], state=binfo[2], relief=FLAT,
54a1397e
FC
156 frameColor=fcols[0] if binfo[2] == NORMAL else fcols[1],
157 rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
158 clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
36099535 159 btn.set_transparency(True)
a0acba9a
FC
160 btns += [btn]
161 self.__home_btn, self.__info_btn, self.__right_btn, self.__next_btn, \
162 self.__prev_btn, self.__rewind_btn = btns
31237524
FC
163 if self._dbg_items:
164 self._info_txt = OnscreenText(
165 '', parent=base.a2dTopRight, scale=0.04,
166 pos=(-.03, -.06), fg=(.9, .9, .9, 1), align=TextNode.A_right)
36099535 167
5964572b
FC
168 def _unset_gui(self):
169 btns = [
170 self.__home_btn, self.__info_btn, self.__right_btn,
171 self.__next_btn, self.__prev_btn, self.__rewind_btn]
172 [btn.destroy() for btn in btns]
31237524
FC
173 if self._dbg_items:
174 self._info_txt.destroy()
5964572b 175
64eae9c7
FC
176 def _set_spotlight(self, name, pos, look_at, color, shadows=False):
177 light = Spotlight(name)
178 if shadows:
179 light.setLens(PerspectiveLens())
1be87278 180 light_np = render.attach_new_node(light)
64eae9c7
FC
181 light_np.set_pos(pos)
182 light_np.look_at(look_at)
1be87278
FC
183 light.set_color(color)
184 render.set_light(light_np)
5964572b 185 return light_np
1be87278
FC
186
187 def _set_lights(self):
188 alight = AmbientLight('alight') # for ao
64eae9c7
FC
189 alight.set_color((.15, .15, .15, 1))
190 self._alnp = render.attach_new_node(alight)
191 render.set_light(self._alnp)
192 self._key_light = self._set_spotlight(
193 'key light', (-5, -80, 5), (0, 0, 0), (2.8, 2.8, 2.8, 1))
194 self._shadow_light = self._set_spotlight(
195 'key light', (-5, -80, 5), (0, 0, 0), (.58, .58, .58, 1), True)
196 self._shadow_light.node().set_shadow_caster(True, 2048, 2048)
197 self._shadow_light.node().get_lens().set_film_size(2048, 2048)
198 self._shadow_light.node().get_lens().set_near_far(1, 256)
199 self._shadow_light.node().set_camera_mask(BitMask32(0x01))
5964572b
FC
200
201 def _unset_lights(self):
64eae9c7 202 for light in [self._alnp, self._key_light, self._shadow_light]:
5964572b
FC
203 render.clear_light(light)
204 light.remove_node()
1be87278
FC
205
206 def _set_input(self):
49c79300 207 self.accept('mouse1', self.on_click_l)
c8d8653f 208 self.accept('mouse1-up', self.on_release)
49c79300
FC
209 self.accept('mouse3', self.on_click_r)
210 self.accept('mouse3-up', self.on_release)
c8d8653f 211
5964572b
FC
212 def _unset_input(self):
213 for evt in ['mouse1', 'mouse1-up', 'mouse3', 'mouse3-up']:
214 self.ignore(evt)
215
c8d8653f 216 def _set_mouse_plane(self):
651713a9 217 shape = BulletPlaneShape((0, -1, 0), 0)
c8d8653f
FC
218 #self._mouse_plane_node = BulletRigidBodyNode('mouse plane')
219 self._mouse_plane_node = BulletGhostNode('mouse plane')
220 self._mouse_plane_node.addShape(shape)
221 #np = render.attachNewNode(self._mouse_plane_node)
222 #self._world.attachRigidBody(self._mouse_plane_node)
06af3aa9 223 self._world.attach_ghost(self._mouse_plane_node)
1be87278 224
5964572b
FC
225 def _unset_mouse_plane(self):
226 self._world.remove_ghost(self._mouse_plane_node)
227
37ba71d8
FC
228 def _get_hits(self):
229 if not base.mouseWatcherNode.has_mouse(): return []
13263131 230 p_from, p_to = P3dGfxMgr.world_from_to(base.mouseWatcherNode.get_mouse())
37ba71d8
FC
231 return self._world.ray_test_all(p_from, p_to).get_hits()
232
31237524
FC
233 def _update_info(self, item):
234 txt = ''
235 if item:
236 txt = '%.3f %.3f\n%.3f°' % (
237 item._np.get_x(), item._np.get_z(), item._np.get_r())
238 self._info_txt['text'] = txt
239
49c79300 240 def _on_click(self, method):
a0acba9a
FC
241 if self._paused:
242 return
c8d8653f
FC
243 for hit in self._get_hits():
244 if hit.get_node() == self._mouse_plane_node:
245 pos = hit.get_hit_pos()
37ba71d8 246 for hit in self._get_hits():
ef3c36bf 247 for item in [i for i in self.items if hit.get_node() == i.node and i.interactable]:
7c0a81ae
FC
248 if not self._item_active:
249 self._item_active = item
49c79300 250 getattr(item, method)(pos)
79f81d48 251 img = 'move' if method == 'on_click_l' else 'rotate'
a6843832
FC
252 if not (img == 'rotate' and not item._instantiated):
253 self._cursor.set_image('assets/buttons/%s.png' % img)
49c79300
FC
254
255 def on_click_l(self):
256 self._on_click('on_click_l')
257
258 def on_click_r(self):
259 self._on_click('on_click_r')
c8d8653f
FC
260
261 def on_release(self):
32aa4dae
FC
262 if self._item_active and not self._item_active._first_command:
263 self._commands = self._commands[:self._command_idx]
264 self._commands += [self._item_active]
265 self._command_idx += 1
266 self.__prev_btn['state'] = NORMAL
267 fcols = (.4, .4, .4, .14), (.3, .3, .3, .05)
268 self.__prev_btn['frameColor'] = fcols[0]
269 if self._item_active._command_idx == len(self._item_active._commands) - 1:
270 self.__next_btn['state'] = DISABLED
271 self.__next_btn['frameColor'] = fcols[1]
7c0a81ae 272 self._item_active = None
49c79300 273 [item.on_release() for item in self.items]
79f81d48 274 self._cursor.set_image('assets/buttons/arrowUpLeft.png')
37ba71d8 275
e1438449 276 def repos(self):
d5932612
FC
277 for item in self.items:
278 item.repos_done = False
e1438449 279 self.items = sorted(self.items, key=lambda itm: itm.__class__.__name__)
651713a9 280 [item.on_aspect_ratio_changed() for item in self.items]
a5dc83f4 281 self._side_panel.update(self.items)
7790323d 282 max_x = -float('inf')
e1438449
FC
283 for item in self.items:
284 if not item._instantiated:
285 max_x = max(item._np.get_x(), max_x)
286 for item in self.items:
287 if not item._instantiated:
288 item.repos_x(max_x)
289
290 def on_aspect_ratio_changed(self):
291 self.repos()
651713a9 292
37ba71d8 293 def on_frame(self, task):
c8d8653f
FC
294 hits = self._get_hits()
295 pos = None
296 for hit in self._get_hits():
297 if hit.get_node() == self._mouse_plane_node:
298 pos = hit.get_hit_pos()
299 hit_nodes = [hit.get_node() for hit in hits]
7c0a81ae
FC
300 if self._item_active:
301 items_hit = [self._item_active]
302 else:
303 items_hit = [itm for itm in self.items if itm.node in hit_nodes]
37ba71d8
FC
304 items_no_hit = [itm for itm in self.items if itm not in items_hit]
305 [itm.on_mouse_on() for itm in items_hit]
306 [itm.on_mouse_off() for itm in items_no_hit]
7c0a81ae
FC
307 if pos and self._item_active:
308 self._item_active.on_mouse_move(pos)
31237524
FC
309 if self._dbg_items:
310 self._update_info(items_hit[0] if items_hit else None)
b41381b2
FC
311 if all(itm.end_condition() for itm in self.items) and not hasattr(self, '_success_txt'):
312 loader.load_sfx('assets/audio/sfx/success.ogg').play()
313 font = base.loader.load_font('assets/fonts/Hanken-Book.ttf')
314 font.clear()
315 font.set_pixels_per_unit(60)
316 font.set_minfilter(Texture.FTLinearMipmapLinear)
317 font.set_outline((0, 0, 0, 1), .8, .2)
318 self._success_txt = OnscreenText(
319 _('You win!'), font=font, scale=0.2, fg=(.9, .9, .9, 1))
37ba71d8 320 return task.cont
c8d8653f 321
7850ccc4
FC
322 def cb_inst(self, item):
323 self.items += [item]
324
c8d8653f 325 def on_play(self):
b41381b2
FC
326 self.__prev_btn['state'] = DISABLED
327 self.__next_btn['state'] = DISABLED
328 self.__right_btn['state'] = DISABLED
c8d8653f 329 [itm.play() for itm in self.items]
36099535
FC
330
331 def on_next(self):
32aa4dae
FC
332 self._commands[self._command_idx].redo()
333 self._command_idx += 1
334 fcols = (.4, .4, .4, .14), (.3, .3, .3, .05)
335 self.__prev_btn['state'] = NORMAL
336 self.__prev_btn['frameColor'] = fcols[0]
337 more_commands = self._command_idx < len(self._commands)
338 self.__next_btn['state'] = NORMAL if more_commands else DISABLED
339 self.__next_btn['frameColor'] = fcols[0] if more_commands else fcols[1]
36099535
FC
340
341 def on_prev(self):
32aa4dae
FC
342 self._command_idx -= 1
343 self._commands[self._command_idx].undo()
344 fcols = (.4, .4, .4, .14), (.3, .3, .3, .05)
345 self.__next_btn['state'] = NORMAL
346 self.__next_btn['frameColor'] = fcols[0]
347 self.__prev_btn['state'] = NORMAL if self._command_idx else DISABLED
348 self.__prev_btn['frameColor'] = fcols[0] if self._command_idx else fcols[1]
36099535 349
36099535 350 def on_home(self):
5964572b 351 self._exit_cb()
36099535 352
2aaa10d3 353 def _set_instructions(self):
9830561d
FC
354 self._paused = True
355 self.__store_state()
2aaa10d3
FC
356 mgr = TextPropertiesManager.get_global_ptr()
357 for name in ['mouse_l', 'mouse_r']:
358 graphic = OnscreenImage('assets/buttons/%s.png' % name)
359 graphic.set_scale(.5)
360 graphic.get_texture().set_minfilter(Texture.FTLinearMipmapLinear)
361 graphic.get_texture().set_anisotropic_degree(2)
362 mgr.set_graphic(name, graphic)
363 graphic.set_z(-.2)
364 graphic.set_transparency(True)
365 graphic.detach_node()
366 frm = DirectFrame(frameColor=(.4, .4, .4, .06),
367 frameSize=(-.6, .6, -.3, .3))
368 font = base.loader.load_font('assets/fonts/Hanken-Book.ttf')
369 font.clear()
370 font.set_pixels_per_unit(60)
371 font.set_minfilter(Texture.FTLinearMipmapLinear)
372 font.set_outline((0, 0, 0, 1), .8, .2)
2aaa10d3 373 self._txt = OnscreenText(
0eff64a3
FC
374 self._instr_txt(), parent=frm, font=font, scale=0.06,
375 fg=(.9, .9, .9, 1), align=TextNode.A_left)
2aaa10d3
FC
376 u_l = self._txt.textNode.get_upper_left_3d()
377 l_r = self._txt.textNode.get_lower_right_3d()
378 w, h = l_r[0] - u_l[0], u_l[2] - l_r[2]
a0acba9a
FC
379 btn_scale = .05
380 mar = .06 # margin
2aaa10d3 381 z = h / 2 - font.get_line_height() * self._txt['scale'][1]
a0acba9a 382 z += (btn_scale + 2 * mar) / 2
2aaa10d3
FC
383 self._txt['pos'] = -w / 2, z
384 u_l = self._txt.textNode.get_upper_left_3d()
385 l_r = self._txt.textNode.get_lower_right_3d()
a0acba9a
FC
386 c_l_r = l_r[0], l_r[1], l_r[2] - 2 * mar - btn_scale
387 fsz = u_l[0] - mar, l_r[0] + mar, c_l_r[2] - mar, u_l[2] + mar
2aaa10d3 388 frm['frameSize'] = fsz
a0acba9a
FC
389 colors = [
390 (.6, .6, .6, 1), # ready
391 (1, 1, 1, 1), # press
392 (.8, .8, .8, 1), # rollover
393 (.4, .4, .4, .4)]
394 imgs = [self.__load_img_btn('exitRight', col) for col in colors]
395 btn = DirectButton(
396 image=imgs, scale=btn_scale,
397 pos=(l_r[0] - btn_scale, 1, l_r[2] - mar - btn_scale),
398 parent=frm, command=self.__on_close_instructions, extraArgs=[frm],
399 relief=FLAT, frameColor=(.6, .6, .6, .08),
400 rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
401 clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
402 btn.set_transparency(True)
403
404 def __store_state(self):
405 btns = [
406 self.__home_btn, self.__info_btn, self.__right_btn,
407 self.__next_btn, self.__prev_btn, self.__rewind_btn]
408 self.__btn_state = [btn['state'] for btn in btns]
409 for btn in btns:
410 btn['state'] = DISABLED
411 [itm.store_state() for itm in self.items]
412
413 def __restore_state(self):
414 btns = [
415 self.__home_btn, self.__info_btn, self.__right_btn,
416 self.__next_btn, self.__prev_btn, self.__rewind_btn]
417 for btn, state in zip(btns, self.__btn_state):
418 btn['state'] = state
419 [itm.restore_state() for itm in self.items]
420 self._paused = False
421
422 def __on_close_instructions(self, frm):
423 frm.remove_node()
424 self.__restore_state()