1 from os
.path
import exists
2 from os
import makedirs
3 from logging
import info
5 from collections
import namedtuple
6 from panda3d
.core
import AmbientLight
, Texture
, TextPropertiesManager
, \
7 TextNode
, Spotlight
, PerspectiveLens
, BitMask32
8 from panda3d
.bullet
import BulletPlaneShape
, BulletGhostNode
9 from direct
.gui
.OnscreenImage
import OnscreenImage
10 from direct
.gui
.OnscreenText
import OnscreenText
11 from direct
.gui
.DirectGui
import DirectButton
, DirectFrame
12 from direct
.gui
.DirectGuiGlobals
import FLAT
, DISABLED
, NORMAL
13 from direct
.showbase
.DirectObject
import DirectObject
14 from direct
.interval
.IntervalGlobal
import Sequence
, Func
15 from direct
.interval
.LerpInterval
import LerpFunctionInterval
16 from pmachines
.items
.background
import Background
17 from pmachines
.gui
.sidepanel
import SidePanel
18 from pmachines
.items
.box
import Box
, HitStrategy
19 from pmachines
.items
.basketball
import Basketball
20 from pmachines
.items
.domino
import Domino
, DownStrategy
21 from pmachines
.items
.shelf
import Shelf
22 from pmachines
.items
.teetertooter
import TeeterTooter
23 from pmachines
.items
.test_item
import PixelSpaceTestItem
, WorldSpaceTestItem
24 from pmachines
.editor
.scene
import SceneEditor
25 from ya2
.utils
.cursor
import MouseCursor
26 from ya2
.utils
.gfx
import GfxTools
, DirectGuiMixin
27 from ya2
.utils
.gui
import GuiTools
30 class Scene(DirectObject
):
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
):
38 self
._exit
_cb
= exit_cb
39 self
._testing
= testing
40 self
._mouse
_coords
= mouse_coords
41 self
._dbg
_items
= dbg_items
42 self
._reload
_cb
= reload_cb
43 self
._pos
_mgr
= pos_mgr
44 for k
in list(self
._pos
_mgr
.keys()): del self
._pos
_mgr
[k
]
46 self
._start
_evt
_time
= None
47 self
._enforce
_result
= ''
48 self
.__persistent
= persistent
49 self
.__json
_name
= json_name
50 self
.__editor
= editor
51 self
.__scene
_editor
= None
53 self
.accept('enforce_result', self
.enforce_result
)
55 self
._cursor
= MouseCursor(
56 'assets/images/buttons/arrowUpLeft.dds', (.04, 1, .04), (.5, .5, .5, 1),
62 self
._set
_mouse
_plane
()
68 self
._item
_active
= None
71 self
.__restore
_state
()
72 elif self
.__json
_name
:
73 self
._set
_instructions
()
74 self
._bg
= Background()
75 self
._side
_panel
= SidePanel(world
, self
._mouse
_plane
_node
, (-5, 4), (-3, 1), 1, self
.__items
)
76 self
._scene
_tsk
= taskMgr
.add(self
.on_frame
, 'scene_on_frame')
79 self
.accept('editor-inspector-delete', self
.__on
_inspector
_delete
)
82 def filename(cls
, scene_name
):
83 return f
'assets/scenes/{scene_name}.json'
86 def name(cls
, scene_name
):
87 if not scene_name
in cls
.json_files
:
88 with
open(cls
.filename(scene_name
)) as f
:
89 cls
.json_files
[scene_name
] = loads(f
.read())
90 return _(cls
.json_files
[scene_name
]['name'])
93 def version(cls
, scene_name
):
94 if not scene_name
: return ''
95 if not scene_name
in cls
.json_files
:
96 with
open(cls
.filename(scene_name
)) as f
:
97 cls
.json_files
[scene_name
] = loads(f
.read())
98 return cls
.json_files
[scene_name
]['version']
101 def is_done(cls
, scene_name
):
102 if not cls
.scenes_done
or len(cls
.scenes_done
) == 1 and not cls
.scenes_done
[0]:
104 return bool([(name
, version
) for name
, version
in cls
.scenes_done
if scene_name
== name
and cls
.version(scene_name
) == version
])
106 def _instr_txt(self
):
107 txt
= _('Scene: ') + self
.name(self
.__json
_name
) + '\n\n'
108 txt
+= _(self
.__process
_json
_escape
(self
.__class
__.json_files
[self
.__json
_name
]['instructions']))
111 def __process_json_escape(self
, string
):
112 return bytes(string
, 'utf-8').decode('unicode-escape')
116 items
= self
.__items
[:]
117 if self
.__scene
_editor
:
118 items
+= self
.__scene
_editor
.test_items
121 def _set_items(self
):
122 if not self
.__json
_name
: return
124 self
._test
_items
= []
126 with
open(f
'assets/scenes/{self.__json_name}.json') as f
:
127 self
.json
= loads(f
.read())
128 for item
in self
.json
['start_items']:
130 'world': self
._world
,
131 'plane_node': self
._mouse
_plane
_node
,
132 'cb_inst': self
.cb_inst
,
133 'curr_bottom': self
.current_bottom
,
135 'count': item
['count'],
138 args
['mass'] = item
['mass']
139 if 'friction' in item
:
140 args
['friction'] = item
['friction']
141 self
.__items
+= [self
.__code
2class
(item
['class'])(**args
)]
142 for item
in self
.json
['items']:
144 'world': self
._world
,
145 'plane_node': self
._mouse
_plane
_node
,
146 'cb_inst': self
.cb_inst
,
147 'curr_bottom': self
.current_bottom
,
150 args
['pos'] = tuple(item
['position'])
152 args
['mass'] = item
['mass']
153 if 'friction' in item
:
154 args
['friction'] = item
['friction']
156 args
['r'] = item
['roll']
157 if 'model_scale' in item
:
158 args
['model_scale'] = item
['model_scale']
159 if 'restitution' in item
:
160 args
['restitution'] = item
['restitution']
161 if 'friction' in item
:
162 args
['friction'] = item
['friction']
163 self
.__items
+= [self
.__code
2class
(item
['class'])(**args
)]
164 if 'strategy' in item
:
165 match item
['strategy']:
167 self
.__items
[-1].set_strategy(self
.__code
2class
(item
['strategy'])(self
.__items
[-1]._np
, *item
['strategy_args']))
169 self
.__items
[-1].set_strategy(self
.__code
2class
(item
['strategy'])(self
.__item
_with
_id
(item
['strategy_args'][0]), self
.items
[-1].node
, self
.__items
[-1]._world
))
171 def __code2class(self
, code
):
174 'Basketball': Basketball
,
177 'TeeterTooter': TeeterTooter
,
178 'DownStrategy': DownStrategy
,
179 'HitStrategy': HitStrategy
182 def __item_with_id(self
, id):
183 for item
in self
.__items
:
184 if 'id' in item
.json
and item
.json
['id'] == id:
187 def screenshot(self
, task
=None):
188 tex
= Texture('screenshot')
189 buffer = base
.win
.make_texture_buffer('screenshot', 512, 512, tex
, True )
190 cam
= base
.make_camera(buffer)
191 cam
.reparent_to(render
)
192 cam
.node().get_lens().set_fov(base
.camLens
.get_fov())
193 cam
.set_pos(0, -20, 0)
199 use_normal_maps
=True,
200 use_emission_maps
=False,
201 use_occlusion_maps
=True,
204 base
.graphicsEngine
.renderFrame()
205 base
.graphicsEngine
.renderFrame()
206 fname
= self
.__json
_name
207 if not exists('assets/images/scenes'):
208 makedirs('assets/images/scenes')
209 buffer.save_screenshot('assets/images/scenes/%s.png' % fname
)
210 # img = DirectButton(
211 # frameTexture=buffer.get_texture(), relief=FLAT,
212 # frameSize=(-.2, .2, -.2, .2))
213 return buffer.get_texture()
215 def current_bottom(self
):
217 for item
in self
.__items
:
219 curr_bottom
= min(curr_bottom
, item
.get_bottom())
223 [itm
.destroy() for itm
in self
.__items
]
224 [itm
.remove_node() for itm
in self
._test
_items
]
226 self
._test
_items
= []
228 self
._set
_test
_items
()
231 self
._command
_idx
= 0
232 self
._start
_evt
_time
= None
233 if hasattr(self
, '_success_txt'):
234 self
._success
_txt
.destroy()
235 del self
._success
_txt
236 self
.__right
_btn
['state'] = NORMAL
238 def enforce_result(self
, val
):
239 self
._enforce
_result
= val
240 info('enforce result: ' + val
)
243 self
.__intro
_sequence
.finish()
244 self
.ignore('enforce_result')
248 self
._unset
_mouse
_plane
()
249 [itm
.destroy() for itm
in self
.__items
]
250 [itm
.remove_node() for itm
in self
._test
_items
]
252 self
._side
_panel
.destroy()
253 self
._cursor
.destroy()
254 taskMgr
.remove(self
._scene
_tsk
)
255 if hasattr(self
, '_success_txt'):
256 self
._success
_txt
.destroy()
257 self
.ignore('editor-inspector-delete')
258 if self
.__scene
_editor
: self
.__scene
_editor
.destroy()
260 def _set_camera(self
):
261 base
.camera
.set_pos(0, -20, 0)
262 base
.camera
.look_at(0, 0, 0)
267 start_v
[0] + (end_v
[0] - start_v
[0]) * t
,
268 start_v
[1] + (end_v
[1] - start_v
[1]) * t
,
269 start_v
[2] + (end_v
[2] - start_v
[2]) * t
)
270 base
.camera
.set_pos(*curr_pos
)
272 camera_interval
= LerpFunctionInterval(
277 blendType
='easeInOut')
278 self
.__intro
_sequence
= Sequence(
281 self
.__intro
_sequence
.start()
283 def __load_img_btn(self
, path
, col
):
284 img
= OnscreenImage('assets/images/buttons/%s.dds' % path
)
285 img
.set_transparency(True)
291 def load_images_btn(path
, col
):
294 (.6, .6, .6, 1), # ready
295 (1, 1, 1, 1), # press
296 (.8, .8, .8, 1), # rollover
302 (.4, .1, .1, .4)]}[col
]
303 return [self
.__load
_img
_btn
(path
, col
) for col
in colors
]
304 abl
, abr
= base
.a2dBottomLeft
, base
.a2dBottomRight
306 ('home', self
.on_home
, NORMAL
, abl
, 'gray', _('Exit'), 'right'),
307 ('information', self
._set
_instructions
, NORMAL
, abl
, 'gray', _('Instructions'), 'right'),
308 ('right', self
.on_play
, NORMAL
, abr
, 'green', _('Run'), 'left'),
309 #('next', self.on_next, DISABLED, abr, 'gray'),
310 #('previous', self.on_prev, DISABLED, abr, 'gray'),
311 #('rewind', self.reset, NORMAL, abr, 'gray')
314 btn_info
.insert(2, ('wrench', self
._set
_editor
, NORMAL
, abl
, 'gray', _('Editor'), 'right'))
317 tooltip_args
= self
.__font
, .05, (.93, .93, .93, 1)
318 for binfo
in btn_info
:
319 imgs
= load_images_btn(binfo
[0], binfo
[4])
320 if binfo
[3] == base
.a2dBottomLeft
:
324 sign
, num
= -1, num_r
326 fcols
= (.4, .4, .4, .14), (.3, .3, .3, .05)
328 image
=imgs
, scale
=.05, pos
=(sign
* (.06 + .11 * num
), 1, .06),
329 parent
=binfo
[3], command
=binfo
[1], state
=binfo
[2], relief
=FLAT
,
330 frameColor
=fcols
[0] if binfo
[2] == NORMAL
else fcols
[1],
331 rolloverSound
=loader
.load_sfx('assets/audio/sfx/rollover.ogg'),
332 clickSound
=loader
.load_sfx('assets/audio/sfx/click.ogg'))
333 btn
.__class
__ = type('DirectButtonMixed', (DirectButton
, DirectGuiMixin
), {})
334 btn
.set_transparency(True)
335 t
= tooltip_args
+ (binfo
[6],)
336 btn
.set_tooltip(binfo
[5], *t
)
337 self
._pos
_mgr
[binfo
[0]] = btn
.pos_pixel()
340 self
.__home
_btn
, self
.__info
_btn
, self
.__editor
_btn
, self
.__right
_btn
= btns
342 self
.__home
_btn
, self
.__info
_btn
, self
.__right
_btn
= btns
343 # , self.__next_btn, self.__prev_btn, self.__rewind_btn
345 self
._info
_txt
= OnscreenText(
346 '', parent
=base
.a2dTopRight
, scale
=0.04,
347 pos
=(-.03, -.06), fg
=(.9, .9, .9, 1), align
=TextNode
.A_right
)
348 if self
._mouse
_coords
:
349 self
._coords
_txt
= OnscreenText(
350 '', parent
=base
.a2dTopRight
, scale
=0.04,
351 pos
=(-.03, -.12), fg
=(.9, .9, .9, 1), align
=TextNode
.A_right
)
352 def update_coords(task
):
354 for hit
in self
._get
_hits
():
355 if hit
.get_node() == self
._mouse
_plane
_node
:
356 pos
= hit
.get_hit_pos()
358 txt
= '%s %s' % (round(pos
.x
, 3),
360 self
._coords
_txt
['text'] = txt
362 self
._coords
_tsk
= taskMgr
.add(update_coords
, 'update_coords')
364 def _unset_gui(self
):
366 self
.__home
_btn
, self
.__info
_btn
, self
.__right
_btn
367 #self.__next_btn, self.__prev_btn, self.__rewind_btn
369 if self
.__editor
: btns
+= [self
.__editor
_btn
]
370 [btn
.destroy() for btn
in btns
]
372 self
._info
_txt
.destroy()
373 if self
._mouse
_coords
:
374 taskMgr
.remove(self
._coords
_tsk
)
375 self
._coords
_txt
.destroy()
377 def _set_spotlight(self
, name
, pos
, look_at
, color
, shadows
=False):
378 light
= Spotlight(name
)
380 light
.setLens(PerspectiveLens())
381 light_np
= render
.attach_new_node(light
)
382 light_np
.set_pos(pos
)
383 light_np
.look_at(look_at
)
384 light
.set_color(color
)
385 render
.set_light(light_np
)
388 def _set_lights(self
):
389 alight
= AmbientLight('alight') # for ao
390 alight
.set_color((.15, .15, .15, 1))
391 self
._alnp
= render
.attach_new_node(alight
)
392 render
.set_light(self
._alnp
)
393 self
._key
_light
= self
._set
_spotlight
(
394 'key light', (-5, -80, 5), (0, 0, 0), (2.8, 2.8, 2.8, 1))
395 self
._shadow
_light
= self
._set
_spotlight
(
396 'key light', (-5, -80, 5), (0, 0, 0), (.58, .58, .58, 1), True)
397 self
._shadow
_light
.node().set_shadow_caster(True, 2048, 2048)
398 self
._shadow
_light
.node().get_lens().set_film_size(2048, 2048)
399 self
._shadow
_light
.node().get_lens().set_near_far(1, 256)
400 self
._shadow
_light
.node().set_camera_mask(BitMask32(0x01))
402 def _unset_lights(self
):
403 for light
in [self
._alnp
, self
._key
_light
, self
._shadow
_light
]:
404 render
.clear_light(light
)
407 def _set_input(self
):
408 self
.accept('mouse1', self
.on_click_l
)
409 self
.accept('mouse1-up', self
.on_release
)
410 self
.accept('mouse3', self
.on_click_r
)
411 self
.accept('mouse3-up', self
.on_release
)
413 def _unset_input(self
):
414 for evt
in ['mouse1', 'mouse1-up', 'mouse3', 'mouse3-up']:
417 def _set_mouse_plane(self
):
418 shape
= BulletPlaneShape((0, -1, 0), 0)
419 #self._mouse_plane_node = BulletRigidBodyNode('mouse plane')
420 self
._mouse
_plane
_node
= BulletGhostNode('mouse plane')
421 self
._mouse
_plane
_node
.addShape(shape
)
422 #np = render.attachNewNode(self._mouse_plane_node)
423 #self._world.attachRigidBody(self._mouse_plane_node)
424 self
._world
.attach_ghost(self
._mouse
_plane
_node
)
426 def _unset_mouse_plane(self
):
427 self
._world
.remove_ghost(self
._mouse
_plane
_node
)
430 if not base
.mouseWatcherNode
.has_mouse(): return []
431 p_from
, p_to
= GuiTools
.get_mouse().from_to_points()
432 return self
._world
.ray_test_all(p_from
, p_to
).get_hits()
434 def _update_info(self
, item
):
437 txt
= '%.3f %.3f\n%.3f°' % (
438 item
._np
.get_x(), item
._np
.get_z(), item
._np
.get_r())
439 self
._info
_txt
['text'] = txt
441 def _on_click(self
, method
):
444 for hit
in self
._get
_hits
():
445 if hit
.get_node() == self
._mouse
_plane
_node
:
446 pos
= hit
.get_hit_pos()
447 for hit
in self
._get
_hits
():
448 for item
in [i
for i
in self
.items
if hit
.get_node() == i
.node
and i
.interactable
]:
449 if not self
._item
_active
:
450 self
._item
_active
= item
451 if item
not in self
.__items
:
452 method
= 'on_click_l'
453 getattr(item
, method
)(pos
)
454 img
= 'move' if method
== 'on_click_l' else 'rotate'
455 if not (img
== 'rotate' and not item
._instantiated
):
456 self
._cursor
.set_image('assets/images/buttons/%s.dds' % img
)
458 def on_click_l(self
):
459 self
._on
_click
('on_click_l')
461 def on_click_r(self
):
462 self
._on
_click
('on_click_r')
464 def on_release(self
):
465 if self
._item
_active
and not self
._item
_active
._first
_command
:
466 self
._commands
= self
._commands
[:self
._command
_idx
]
467 self
._commands
+= [self
._item
_active
]
468 self
._command
_idx
+= 1
469 #self.__prev_btn['state'] = NORMAL
470 #fcols = (.4, .4, .4, .14), (.3, .3, .3, .05)
471 #self.__prev_btn['frameColor'] = fcols[0]
472 #if self._item_active._command_idx == len(self._item_active._commands) - 1:
473 # self.__next_btn['state'] = DISABLED
474 # self.__next_btn['frameColor'] = fcols[1]
475 self
._item
_active
= None
476 [item
.on_release() for item
in self
.__items
]
477 self
._cursor
.set_image('assets/images/buttons/arrowUpLeft.dds')
480 for item
in self
.__items
:
481 item
.repos_done
= False
482 self
.__items
= sorted(self
.__items
, key
=lambda itm
: itm
.__class
__.__name
__)
483 [item
.on_aspect_ratio_changed() for item
in self
.__items
]
484 self
._side
_panel
.update(self
.__items
)
485 max_x
= -float('inf')
486 for item
in self
.__items
:
487 if not item
._instantiated
:
488 max_x
= max(item
._np
.get_x(), max_x
)
489 for item
in self
.__items
:
490 if not item
._instantiated
:
493 def on_aspect_ratio_changed(self
):
496 def _win_condition(self
):
497 return all(itm
.strategy
.win_condition() for itm
in self
.__items
) and not self
._paused
499 def _fail_condition(self
):
500 return all(itm
.fail_condition() for itm
in self
.__items
) and not self
._paused
and self
._state
== 'playing'
502 def on_frame(self
, task
):
503 hits
= self
._get
_hits
()
505 for hit
in self
._get
_hits
():
506 if hit
.get_node() == self
._mouse
_plane
_node
:
507 pos
= hit
.get_hit_pos()
508 hit_nodes
= [hit
.get_node() for hit
in hits
]
509 if self
._item
_active
:
510 items_hit
= [self
._item
_active
]
512 items_hit
= [itm
for itm
in self
.items
if itm
.node
in hit_nodes
]
513 items_no_hit
= [itm
for itm
in self
.items
if itm
not in items_hit
]
514 [itm
.on_mouse_on() for itm
in items_hit
]
515 [itm
.on_mouse_off() for itm
in items_no_hit
]
516 if pos
and self
._item
_active
:
517 self
._item
_active
.on_mouse_move(pos
)
519 self
._update
_info
(items_hit
[0] if items_hit
else None)
520 if not self
.__scene
_editor
and self
.__json
_name
and self
._win
_condition
():
521 self
._start
_evt
_time
= None
522 self
._set
_fail
() if self
._enforce
_result
== 'fail' else self
._set
_win
()
523 elif self
._state
== 'playing' and self
._fail
_condition
():
524 self
._start
_evt
_time
= None
525 self
._set
_win
() if self
._enforce
_result
== 'win' else self
._set
_fail
()
526 elif self
._testing
and self
._start
_evt
_time
and globalClock
.getFrameTime() - self
._start
_evt
_time
> 5.0:
527 self
._start
_evt
_time
= None
528 self
._set
_win
() if self
._enforce
_result
== 'win' else self
._set
_fail
()
529 if any(itm
._overlapping
for itm
in self
.items
):
530 self
._cursor
.set_color((.9, .1, .1, 1))
532 self
._cursor
.set_color((.9, .9, .9, 1))
535 def cb_inst(self
, item
):
536 self
.__items
+= [item
]
539 self
._state
= 'playing'
540 #self.__prev_btn['state'] = DISABLED
541 #self.__next_btn['state'] = DISABLED
542 self
.__right
_btn
['state'] = DISABLED
543 [itm
.play() for itm
in self
.__items
]
544 self
._start
_evt
_time
= globalClock
.getFrameTime()
547 self
._commands
[self
._command
_idx
].redo()
548 self
._command
_idx
+= 1
549 #fcols = (.4, .4, .4, .14), (.3, .3, .3, .05)
550 #self.__prev_btn['state'] = NORMAL
551 #self.__prev_btn['frameColor'] = fcols[0]
552 #more_commands = self._command_idx < len(self._commands)
553 #self.__next_btn['state'] = NORMAL if more_commands else DISABLED
554 #self.__next_btn['frameColor'] = fcols[0] if more_commands else fcols[1]
557 self
._command
_idx
-= 1
558 self
._commands
[self
._command
_idx
].undo()
559 #fcols = (.4, .4, .4, .14), (.3, .3, .3, .05)
560 #self.__next_btn['state'] = NORMAL
561 #self.__next_btn['frameColor'] = fcols[0]
562 #self.__prev_btn['state'] = NORMAL if self._command_idx else DISABLED
563 #self.__prev_btn['frameColor'] = fcols[0] if self._command_idx else fcols[1]
568 def __set_font(self
):
569 self
.__font
= base
.loader
.load_font('assets/fonts/Hanken-Book.ttf')
571 self
.__font
.set_pixels_per_unit(60)
572 self
.__font
.set_minfilter(Texture
.FTLinearMipmapLinear
)
573 self
.__font
.set_outline((0, 0, 0, 1), .8, .2)
576 def _set_instructions(self
):
579 mgr
= TextPropertiesManager
.get_global_ptr()
580 for name
in ['mouse_l', 'mouse_r']:
581 graphic
= OnscreenImage('assets/images/buttons/%s.dds' % name
)
582 graphic
.set_scale(.5)
583 graphic
.get_texture().set_minfilter(Texture
.FTLinearMipmapLinear
)
584 graphic
.get_texture().set_anisotropic_degree(2)
585 mgr
.set_graphic(name
, graphic
)
587 graphic
.set_transparency(True)
588 graphic
.detach_node()
589 frm
= DirectFrame(frameColor
=(.4, .4, .4, .06),
590 frameSize
=(-.6, .6, -.3, .3))
591 self
._txt
= OnscreenText(
592 self
._instr
_txt
(), parent
=frm
, font
=self
.__font
, scale
=0.06,
593 fg
=(.9, .9, .9, 1), align
=TextNode
.A_left
)
594 u_l
= self
._txt
.textNode
.get_upper_left_3d()
595 l_r
= self
._txt
.textNode
.get_lower_right_3d()
596 w
, h
= l_r
[0] - u_l
[0], u_l
[2] - l_r
[2]
599 z
= h
/ 2 - self
.__font
.get_line_height() * self
._txt
['scale'][1]
600 z
+= (btn_scale
+ 2 * mar
) / 2
601 self
._txt
['pos'] = -w
/ 2, z
602 u_l
= self
._txt
.textNode
.get_upper_left_3d()
603 l_r
= self
._txt
.textNode
.get_lower_right_3d()
604 c_l_r
= l_r
[0], l_r
[1], l_r
[2] - 2 * mar
- btn_scale
605 fsz
= u_l
[0] - mar
, l_r
[0] + mar
, c_l_r
[2] - mar
, u_l
[2] + mar
606 frm
['frameSize'] = fsz
608 (.6, .6, .6, 1), # ready
609 (1, 1, 1, 1), # press
610 (.8, .8, .8, 1), # rollover
612 imgs
= [self
.__load
_img
_btn
('exitRight', col
) for col
in colors
]
614 image
=imgs
, scale
=btn_scale
,
615 pos
=(l_r
[0] - btn_scale
, 1, l_r
[2] - mar
- btn_scale
),
616 parent
=frm
, command
=self
.__on
_close
_instructions
, extraArgs
=[frm
],
617 relief
=FLAT
, frameColor
=(.6, .6, .6, .08),
618 rolloverSound
=loader
.load_sfx('assets/audio/sfx/rollover.ogg'),
619 clickSound
=loader
.load_sfx('assets/audio/sfx/click.ogg'))
620 btn
.__class
__ = type('DirectButtonMixed', (DirectButton
, DirectGuiMixin
), {})
621 btn
.set_transparency(True)
622 self
._pos
_mgr
['close_instructions'] = btn
.pos_pixel()
625 self
.__persistent
.save_scene(self
.__json
_name
, self
.version(self
.__json
_name
))
626 loader
.load_sfx('assets/audio/sfx/success.ogg').play()
629 frm
= DirectFrame(frameColor
=(.4, .4, .4, .06),
630 frameSize
=(-.6, .6, -.3, .3))
631 font
= base
.loader
.load_font('assets/fonts/Hanken-Book.ttf')
633 font
.set_pixels_per_unit(60)
634 font
.set_minfilter(Texture
.FTLinearMipmapLinear
)
635 font
.set_outline((0, 0, 0, 1), .8, .2)
636 self
._txt
= OnscreenText(
639 font
=font
, scale
=0.2,
641 u_l
= self
._txt
.textNode
.get_upper_left_3d()
642 l_r
= self
._txt
.textNode
.get_lower_right_3d()
643 #w, h = l_r[0] - u_l[0], u_l[2] - l_r[2]
647 z
= h
/ 2 - font
.get_line_height() * self
._txt
['scale'][1]
648 z
+= (btn_scale
+ 2 * mar
) / 2
649 self
._txt
['pos'] = 0, z
650 u_l
= self
._txt
.textNode
.get_upper_left_3d()
651 l_r
= self
._txt
.textNode
.get_lower_right_3d()
652 c_l_r
= l_r
[0], l_r
[1], l_r
[2] - 2 * mar
- btn_scale
653 fsz
= u_l
[0] - mar
, l_r
[0] + mar
, c_l_r
[2] - mar
, u_l
[2] + mar
654 frm
['frameSize'] = fsz
656 (.6, .6, .6, 1), # ready
657 (1, 1, 1, 1), # press
658 (.8, .8, .8, 1), # rollover
660 imgs
= [self
.__load
_img
_btn
('home', col
) for col
in colors
]
662 image
=imgs
, scale
=btn_scale
,
663 pos
=(-2.8 * btn_scale
, 1, l_r
[2] - mar
- btn_scale
),
664 parent
=frm
, command
=self
._on
_end
_home
, extraArgs
=[frm
],
665 relief
=FLAT
, frameColor
=(.6, .6, .6, .08),
666 rolloverSound
=loader
.load_sfx('assets/audio/sfx/rollover.ogg'),
667 clickSound
=loader
.load_sfx('assets/audio/sfx/click.ogg'))
668 btn
.__class
__ = type('DirectButtonMixed', (DirectButton
, DirectGuiMixin
), {})
669 btn
.set_transparency(True)
670 self
._pos
_mgr
['home_win'] = btn
.pos_pixel()
671 imgs
= [self
.__load
_img
_btn
('rewind', col
) for col
in colors
]
673 image
=imgs
, scale
=btn_scale
,
674 pos
=(0, 1, l_r
[2] - mar
- btn_scale
),
675 parent
=frm
, command
=self
._on
_restart
, extraArgs
=[frm
],
676 relief
=FLAT
, frameColor
=(.6, .6, .6, .08),
677 rolloverSound
=loader
.load_sfx('assets/audio/sfx/rollover.ogg'),
678 clickSound
=loader
.load_sfx('assets/audio/sfx/click.ogg'))
679 btn
.__class
__ = type('DirectButtonMixed', (DirectButton
, DirectGuiMixin
), {})
680 self
._pos
_mgr
['replay'] = btn
.pos_pixel()
681 btn
.set_transparency(True)
683 enabled
= self
._scenes
.index(self
.__json
_name
) < len(self
._scenes
) - 1
685 next_scene
= self
._scenes
[self
._scenes
.index(self
.__json
_name
) + 1]
691 imgs
= [self
.__load
_img
_btn
('right', col
) for col
in colors
]
693 image
=imgs
, scale
=btn_scale
,
694 pos
=(2.8 * btn_scale
, 1, l_r
[2] - mar
- btn_scale
),
695 parent
=frm
, command
=self
._on
_next
_scene
,
696 extraArgs
=[frm
, next_scene
], relief
=FLAT
,
697 frameColor
=(.6, .6, .6, .08),
698 rolloverSound
=loader
.load_sfx('assets/audio/sfx/rollover.ogg'),
699 clickSound
=loader
.load_sfx('assets/audio/sfx/click.ogg'))
700 btn
.__class
__ = type('DirectButtonMixed', (DirectButton
, DirectGuiMixin
), {})
701 btn
['state'] = NORMAL
if enabled
else DISABLED
702 self
._pos
_mgr
['next'] = btn
.pos_pixel()
703 btn
.set_transparency(True)
706 loader
.load_sfx('assets/audio/sfx/success.ogg').play()
709 frm
= DirectFrame(frameColor
=(.4, .4, .4, .06),
710 frameSize
=(-.6, .6, -.3, .3))
711 font
= base
.loader
.load_font('assets/fonts/Hanken-Book.ttf')
713 font
.set_pixels_per_unit(60)
714 font
.set_minfilter(Texture
.FTLinearMipmapLinear
)
715 font
.set_outline((0, 0, 0, 1), .8, .2)
716 self
._txt
= OnscreenText(
717 _('You have failed!'),
719 font
=font
, scale
=0.2,
721 u_l
= self
._txt
.textNode
.get_upper_left_3d()
722 l_r
= self
._txt
.textNode
.get_lower_right_3d()
723 #w, h = l_r[0] - u_l[0], u_l[2] - l_r[2]
727 z
= h
/ 2 - font
.get_line_height() * self
._txt
['scale'][1]
728 z
+= (btn_scale
+ 2 * mar
) / 2
729 self
._txt
['pos'] = 0, z
730 u_l
= self
._txt
.textNode
.get_upper_left_3d()
731 l_r
= self
._txt
.textNode
.get_lower_right_3d()
732 c_l_r
= l_r
[0], l_r
[1], l_r
[2] - 2 * mar
- btn_scale
733 fsz
= u_l
[0] - mar
, l_r
[0] + mar
, c_l_r
[2] - mar
, u_l
[2] + mar
734 frm
['frameSize'] = fsz
736 (.6, .6, .6, 1), # ready
737 (1, 1, 1, 1), # press
738 (.8, .8, .8, 1), # rollover
740 imgs
= [self
.__load
_img
_btn
('home', col
) for col
in colors
]
742 image
=imgs
, scale
=btn_scale
,
743 pos
=(-2.8 * btn_scale
, 1, l_r
[2] - mar
- btn_scale
),
744 parent
=frm
, command
=self
._on
_end
_home
, extraArgs
=[frm
],
745 relief
=FLAT
, frameColor
=(.6, .6, .6, .08),
746 rolloverSound
=loader
.load_sfx('assets/audio/sfx/rollover.ogg'),
747 clickSound
=loader
.load_sfx('assets/audio/sfx/click.ogg'))
748 btn
.__class
__ = type('DirectButtonMixed', (DirectButton
, DirectGuiMixin
), {})
749 self
._pos
_mgr
['home_win'] = btn
.pos_pixel()
750 btn
.set_transparency(True)
751 imgs
= [self
.__load
_img
_btn
('rewind', col
) for col
in colors
]
753 image
=imgs
, scale
=btn_scale
,
754 pos
=(0, 1, l_r
[2] - mar
- btn_scale
),
755 parent
=frm
, command
=self
._on
_restart
, extraArgs
=[frm
],
756 relief
=FLAT
, frameColor
=(.6, .6, .6, .08),
757 rolloverSound
=loader
.load_sfx('assets/audio/sfx/rollover.ogg'),
758 clickSound
=loader
.load_sfx('assets/audio/sfx/click.ogg'))
759 btn
.__class
__ = type('DirectButtonMixed', (DirectButton
, DirectGuiMixin
), {})
760 self
._pos
_mgr
['replay'] = btn
.pos_pixel()
761 btn
.set_transparency(True)
763 def _on_restart(self
, frm
):
764 self
.__on
_close
_instructions
(frm
)
767 def _on_end_home(self
, frm
):
768 self
.__on
_close
_instructions
(frm
)
771 def _on_next_scene(self
, frm
, scene
):
772 self
.__on
_close
_instructions
(frm
)
773 self
._reload
_cb
(scene
)
775 def __store_state(self
):
777 self
.__home
_btn
, self
.__info
_btn
, self
.__right
_btn
,
778 #self.__next_btn, self.__prev_btn, self.__rewind_btn
780 if self
.__editor
: btns
+= [self
.__editor
_btn
]
781 self
.__btn
_state
= [btn
['state'] for btn
in btns
]
783 btn
['state'] = DISABLED
784 [itm
.store_state() for itm
in self
.__items
]
786 def __restore_state(self
):
788 self
.__home
_btn
, self
.__info
_btn
, self
.__right
_btn
,
789 #self.__next_btn, self.__prev_btn, self.__rewind_btn
791 if self
.__editor
: btns
+= [self
.__editor
_btn
]
792 for btn
, state
in zip(btns
, self
.__btn
_state
):
794 [itm
.restore_state() for itm
in self
.__items
]
797 def __on_close_instructions(self
, frm
):
799 self
.__restore
_state
()
801 def _set_test_items(self
):
802 def frame_after(task
):
803 self
._define
_test
_items
()
804 for itm
in self
.items
:
806 self
._pos
_mgr
[itm
.id] = itm
._model
.pos2d_pixel()
807 for itm
in self
._test
_items
:
808 self
._pos
_mgr
[itm
.name
] = itm
.pos2d_pixel()
809 taskMgr
.doMethodLater(1.4, frame_after
, 'frame after') # after the intro sequence
811 def _define_test_items(self
):
812 if not self
.__json
_name
: return
813 if not self
.__json
_name
in self
.__class
__.json_files
:
814 with
open(self
.__class
__.filename(self
.__json
_name
)) as f
:
815 self
.__class
__.json_files
[self
.__json
_name
] = loads(f
.read())
816 for item
in self
.__class
__.json_files
[self
.__json
_name
]['test_items']['pixel_space']:
817 self
._pos
_mgr
[item
['id']] = tuple(item
['position'])
818 for item
in self
.__class
__.json_files
[self
.__json
_name
]['test_items']['world_space']:
819 self
._set
_test
_item
(item
['id'], tuple(item
['position']))
821 def _set_test_item(self
, name
, pos
):
822 self
._test
_items
+= [GfxTools
.build_empty_node(name
)]
823 self
._test
_items
[-1].set_pos(pos
[0], 0, pos
[1])
825 def add_item(self
, item
):
826 self
.__items
+= [item
]
828 def _set_editor(self
):
829 fields
= ['world', 'plane_node', 'cb_inst', 'curr_bottom', 'repos', 'json']
830 SceneContext
= namedtuple('SceneContext', fields
)
831 context
= SceneContext(
833 self
._mouse
_plane
_node
,
838 self
.__scene
_editor
= SceneEditor(self
.json
, self
.__json
_name
, context
, self
.add_item
, self
.__items
, self
._world
, self
._mouse
_plane
_node
, self
._pos
_mgr
)
840 def __on_inspector_delete(self
, item
):
841 if item
.__class
__ in [WorldSpaceTestItem
, PixelSpaceTestItem
]:
842 for i
in self
._test
_items
:
843 if item
.id == i
.name
:
845 self
._test
_items
.remove(r
)
847 self
.__items
.remove(item
)
848 j
= self
.json
['items']
849 if item
.__class
__ == PixelSpaceTestItem
:
850 j
= self
.json
['test_items']['pixel_space']
851 if item
.__class
__ == WorldSpaceTestItem
:
852 j
= self
.json
['test_items']['world_space']