ya2 · news · projects · code · about

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