ya2 · news · projects · code · about

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