1 from copy
import deepcopy
3 from importlib
import import_module
4 from inspect
import isclass
6 from os
.path
import basename
7 from logging
import info
9 from panda3d
.core
import Texture
, TextNode
, LPoint3f
10 from direct
.gui
.OnscreenImage
import OnscreenImage
11 from direct
.gui
.DirectGui
import DirectButton
, DirectEntry
, \
12 YesNoDialog
, DirectOptionMenu
, DirectFrame
13 from direct
.gui
.DirectGuiGlobals
import FLAT
, NORMAL
14 from direct
.gui
.OnscreenText
import OnscreenText
15 from direct
.showbase
.DirectObject
import DirectObject
16 from pmachines
.items
.item
import Item
17 from pmachines
.items
.test_item
import PixelSpaceTestItem
, WorldSpaceTestItem
18 from pmachines
.editor
.scene_list
import SceneList
19 from pmachines
.editor
.inspector
import Inspector
, PixelSpaceInspector
, WorldSpaceInspector
20 from pmachines
.editor
.start_items
import StartItems
21 from ya2
.utils
.gfx
import Point
, DirectGuiMixin
22 from pmachines
.editor
.augmented_frame
import AugmentedDirectFrame
23 from ya2
.utils
.gui
.base_page
import DirectOptionMenuTestable
24 from pmachines
.items
.basketball
import Basketball
25 from pmachines
.items
.box
import Box
26 from pmachines
.items
.domino
import Domino
27 from pmachines
.items
.shelf
import Shelf
28 from pmachines
.items
.teetertooter
import TeeterTooter
29 from pmachines
.items
.item
import FixedStrategy
, StillStrategy
30 from pmachines
.items
.domino
import UpStrategy
, DownStrategy
31 from pmachines
.items
.box
import HitStrategy
34 class SceneEditor(DirectObject
):
36 def __init__(self
, json
, json_name
, context
, add_item
, items
, world
, mouse_plane_node
, pos_mgr
):
41 self
.__mouse
_plane
_node
= mouse_plane_node
42 self
.__pos
_mgr
= pos_mgr
45 self
.__json
= json
= {
54 self
.__inspector
= None
55 self
.__context
= context
56 self
.__add
_item
= add_item
57 self
._font
= base
.loader
.load_font(
58 'assets/fonts/Hanken-Book.ttf')
60 self
._font
.set_pixels_per_unit(60)
61 self
._font
.set_minfilter(Texture
.FTLinearMipmapLinear
)
62 self
._font
.set_outline((0, 0, 0, 1), .8, .2)
63 self
.__item
_classes
= [Basketball
, Box
, Domino
, Shelf
, TeeterTooter
]
64 self
.__item
_strategy
_classes
= [FixedStrategy
, StillStrategy
, HitStrategy
, UpStrategy
, DownStrategy
]
67 'text_font': self
._font
,
68 'text_fg': (.9, .9, .9, 1),
70 'frameColor': (.4, .4, .4, .14),
71 'rolloverSound': loader
.load_sfx(
72 'assets/audio/sfx/rollover.ogg'),
73 'clickSound': loader
.load_sfx(
74 'assets/audio/sfx/click.ogg')}
75 tooltip_args
= self
._common
['text_font'], self
._common
['scale'], self
._common
['text_fg']
76 w
, h
, tw
, l
= 1.8, 1, 30, .36
77 self
._frm
= AugmentedDirectFrame(frameColor
=(.4, .4, .4, .06),
78 frameSize
=(0, w
, 0, h
),
79 parent
=base
.a2dBottomCenter
,
81 delta_drag
=LPoint3f(0, 0, h
),
82 collapse_pos
=(.06, 1, .94),
83 pos_mgr
=self
.__pos
_mgr
,
86 _('Filename'), pos
=(l
- .03, h
- .1), parent
=self
._frm
,
87 font
=self
._common
['text_font'],
88 scale
=self
._common
['scale'],
89 fg
=self
._common
['text_fg'],
90 align
=TextNode
.A_right
)
91 self
.__filenamename
_entry
= DirectEntry(
92 scale
=self
._common
['scale'],
96 frameColor
=self
._common
['frameColor'],
97 initialText
=json_name
,
99 text_fg
=self
._common
['text_fg'])
100 self
.__filenamename
_entry
['text_fg'] = (1, 0, 0, 1)
101 self
.__filenamename
_entry
.__class
__ = type('DirectEntryMixed', (DirectEntry
, DirectGuiMixin
), {})
102 self
.__filenamename
_entry
.set_tooltip(_('The name of the file without the extension'), *tooltip_args
)
103 self
.__pos
_mgr
['editor_scene_filename'] = self
.__filenamename
_entry
.pos_pixel()
105 _('Name'), pos
=(l
- .03, h
- .2), parent
=self
._frm
,
106 font
=self
._common
['text_font'],
107 scale
=self
._common
['scale'],
108 fg
=self
._common
['text_fg'],
109 align
=TextNode
.A_right
)
110 self
.__name
_entry
= DirectEntry(
111 scale
=self
._common
['scale'],
113 entryFont
=self
._font
,
115 frameColor
=self
._common
['frameColor'],
116 initialText
=json
['name'],
118 text_fg
=self
._common
['text_fg'])
119 self
.__name
_entry
.__class
__ = type('DirectEntryMixed', (DirectEntry
, DirectGuiMixin
), {})
120 self
.__name
_entry
.set_tooltip(_('The title of the scene'), *tooltip_args
)
121 self
.__pos
_mgr
['editor_scene_name'] = self
.__name
_entry
.pos_pixel()
123 _('Description'), pos
=(l
- .03, h
- .3), parent
=self
._frm
,
124 font
=self
._common
['text_font'],
125 scale
=self
._common
['scale'],
126 fg
=self
._common
['text_fg'],
127 align
=TextNode
.A_right
)
128 def add_line_break(txt
, entry
):
129 curpos
= entry
.node().getCursorPosition()
130 entry
.set(txt
[:curpos
]+ "\n" + txt
[curpos
:])
131 entry
.node().setCursorPosition(curpos
+1)
133 self
.__instructions
_entry
= DirectEntry(
134 scale
=self
._common
['scale'],
136 entryFont
=self
._font
,
140 frameColor
=self
._common
['frameColor'],
141 initialText
=json
['instructions'].replace('\\n', '\n'),
143 text_fg
=self
._common
['text_fg'])
144 self
.__instructions
_entry
['command'] = add_line_break
145 self
.__instructions
_entry
['extraArgs'] = [self
.__instructions
_entry
]
146 self
.__instructions
_entry
.__class
__ = type('DirectEntryMixed', (DirectEntry
, DirectGuiMixin
), {})
147 self
.__instructions
_entry
.set_tooltip(_('The description of the scene'), *tooltip_args
)
148 self
.__pos
_mgr
['editor_scene_instructions'] = self
.__instructions
_entry
.pos_pixel()
149 def load_images_btn(path
, col
):
152 (.6, .6, .6, 1), # ready
153 (1, 1, 1, 1), # press
154 (.8, .8, .8, 1), # rollover
160 (.4, .1, .1, .4)]}[col
]
161 return [self
.__load
_img
_btn
(path
, col
) for col
in colors
]
162 fcols
= (.4, .4, .4, .14), (.3, .3, .3, .05)
164 image
=load_images_btn('exitRight', 'gray'), scale
=.05,
166 parent
=self
._frm
, command
=self
.__on
_close
, state
=NORMAL
, relief
=FLAT
,
168 rolloverSound
=loader
.load_sfx('assets/audio/sfx/rollover.ogg'),
169 clickSound
=loader
.load_sfx('assets/audio/sfx/click.ogg'))
170 b
.__class
__ = type('DirectButtonMixed', (DirectButton
, DirectGuiMixin
), {})
171 self
.__pos
_mgr
['editor_close'] = b
.pos_pixel()
172 b
.set_tooltip(_('Close the scene editor'), *tooltip_args
)
174 image
=load_images_btn('save', 'gray'), scale
=.05,
176 parent
=self
._frm
, command
=self
.__on
_save
, state
=NORMAL
, relief
=FLAT
,
178 rolloverSound
=loader
.load_sfx('assets/audio/sfx/rollover.ogg'),
179 clickSound
=loader
.load_sfx('assets/audio/sfx/click.ogg'))
180 b
.__class
__ = type('DirectButtonMixed', (DirectButton
, DirectGuiMixin
), {})
181 self
.__pos
_mgr
['editor_save'] = b
.pos_pixel()
182 b
.set_tooltip(_('Save the scene'), *tooltip_args
)
184 image
=load_images_btn('menuList', 'gray'), scale
=.05,
186 parent
=self
._frm
, command
=self
.__on
_scene
_list
, state
=NORMAL
, relief
=FLAT
,
188 rolloverSound
=loader
.load_sfx('assets/audio/sfx/rollover.ogg'),
189 clickSound
=loader
.load_sfx('assets/audio/sfx/click.ogg'))
190 b
.__class
__ = type('DirectButtonMixed', (DirectButton
, DirectGuiMixin
), {})
191 self
.__pos
_mgr
['editor_sorting'] = b
.pos_pixel()
192 b
.set_tooltip(_('Set the sorting of the scenes'), *tooltip_args
)
193 # item_modules = glob('pmachines/items/*.py')
194 # item_modules = [basename(i)[:-3] for i in item_modules]
195 self
.__new
_items
= {}
196 # for item_module in item_modules:
197 # mod_name = 'pmachines.items.' + item_module
198 # for member in import_module(mod_name).__dict__.values():
199 # if isclass(member) and issubclass(member, Item) and \
201 # self.__new_items[member.__name__] = member
202 for i
in self
.__item
_classes
: self
.__new
_items
[i
.__name
__] = i
204 _('new item'), pos
=(.02, .46), parent
=self
._frm
,
205 font
=self
._common
['text_font'],
206 scale
=self
._common
['scale'],
207 fg
=self
._common
['text_fg'],
208 align
=TextNode
.A_left
)
209 items
= list(self
.__new
_items
.keys())
210 def new_item_test_set(comps
):
211 item_labels
= [f
'new_item_{i.lower()}' for i
in items
]
212 for i
in item_labels
:
213 if i
in self
.__pos
_mgr
:
214 del self
.__pos
_mgr
[i
]
215 for l
, b
in zip(item_labels
, comps
):
216 b
.__class
__ = type('DirectFrameMixed', (DirectFrame
, DirectGuiMixin
), {})
218 self
.__pos
_mgr
[l
] = (p
[0] + 5, p
[1])
219 b
= DirectOptionMenuTestable(
221 text
=_('new item'), pos
=(.02, 1, .4), items
=items
,
222 parent
=self
._frm
, command
=self
.__on
_new
_item
, state
=NORMAL
,
223 relief
=FLAT
, item_relief
=FLAT
,
224 frameColor
=fcols
[0], item_frameColor
=fcols
[0],
225 popupMenu_frameColor
=fcols
[0],
226 popupMarker_frameColor
=fcols
[0],
227 text_font
=self
._font
, text_fg
=(.9, .9, .9, 1),
228 highlightColor
=(.9, .9, .9, .9),
229 item_text_font
=self
._font
, item_text_fg
=(.9, .9, .9, 1),
230 rolloverSound
=loader
.load_sfx('assets/audio/sfx/rollover.ogg'),
231 clickSound
=loader
.load_sfx('assets/audio/sfx/click.ogg'))
232 b
.__class
__ = type('DirectOptionMenuMixed', (DirectOptionMenu
, DirectGuiMixin
), {})
233 b
._show
_cb
= new_item_test_set
234 b
.set_tooltip(_('Create a new item'), *tooltip_args
)
235 self
.__pos
_mgr
['editor_new_item'] = b
.pos_pixel()
237 image
=load_images_btn('start_items', 'gray'), scale
=.05,
239 parent
=self
._frm
, command
=self
.__on
_start
_items
, state
=NORMAL
, relief
=FLAT
,
241 rolloverSound
=loader
.load_sfx('assets/audio/sfx/rollover.ogg'),
242 clickSound
=loader
.load_sfx('assets/audio/sfx/click.ogg'))
243 b
.__class
__ = type('DirectButtonMixed', (DirectButton
, DirectGuiMixin
), {})
244 self
.__pos
_mgr
['editor_start'] = b
.pos_pixel()
245 b
.set_tooltip(_('Initial items'), *tooltip_args
)
247 image
=load_images_btn('plus', 'gray'), scale
=.05,
249 parent
=self
._frm
, command
=self
.__on
_new
_scene
, state
=NORMAL
, relief
=FLAT
,
251 rolloverSound
=loader
.load_sfx('assets/audio/sfx/rollover.ogg'),
252 clickSound
=loader
.load_sfx('assets/audio/sfx/click.ogg'))
253 b
.__class
__ = type('DirectButtonMixed', (DirectButton
, DirectGuiMixin
), {})
254 self
.__pos
_mgr
['editor_new'] = b
.pos_pixel()
255 b
.set_tooltip(_('New scene'), *tooltip_args
)
256 self
.__test
_items
= []
257 self
.__set
_test
_items
()
258 self
.__start
_items
= self
.__scene
_list
= None
259 messenger
.send('editor-start')
260 self
.accept('editor-item-click', self
.__on
_item
_click
)
261 self
.accept('editor-inspector-destroy', self
.__on
_inspector
_destroy
)
262 self
.accept('editor-start-items-save', self
.__on
_start
_items
_save
)
263 self
.accept('editor-start-items-destroy', self
.__on
_start
_items
_destroy
)
265 def __set_test_items(self
):
266 for pixel_space_item
in self
.__json
['test_items']['pixel_space']:
267 print(pixel_space_item
['id'], pixel_space_item
['position'])
268 pos_pixel
= pixel_space_item
['position']
269 win_res
= base
.win
.getXSize(), base
.win
.getYSize()
270 pos_win
= (pos_pixel
[0] / win_res
[0] * 2 - 1,
271 1 - pos_pixel
[1] / win_res
[1] * 2)
272 p_from
, p_to
= Point(pos_win
).from_to_points()
273 for hit
in self
.__world
.ray_test_all(p_from
, p_to
).get_hits():
274 if hit
.get_node() == self
.__mouse
_plane
_node
:
275 pos
= hit
.get_hit_pos()
276 self
.__set
_test
_item
(pos
, pixel_space_item
, PixelSpaceTestItem
)
277 for world_space_item
in self
.__json
['test_items']['world_space']:
278 print(world_space_item
['id'], world_space_item
['position'])
279 self
.__set
_test
_item
(world_space_item
['position'], world_space_item
, WorldSpaceTestItem
)
281 def __set_test_item(self
, pos
, json
, item_class
):
282 test_item
= item_class(
283 self
.__context
.world
,
284 self
.__context
.plane_node
,
285 self
.__context
.cb_inst
,
286 self
.__context
.curr_bottom
,
287 self
.__context
.repos
,
289 pos
=(pos
[0], 0, pos
[-1]),
291 self
.__test
_items
+= [test_item
]
293 def __on_close(self
):
294 self
.__json
['name'] = self
.__name
_entry
.get()
295 self
.__json
['instructions'] = self
.__instructions
_entry
.get()
296 if self
.__compute
_hash
() == self
.__json
['version']:
299 self
.__dialog
= YesNoDialog(dialogName
='Unsaved changes',
300 text
=_('You have unsaved changes. Really quit?'),
301 command
=self
.__actually
_close
)
302 self
.__dialog
['frameColor'] = (.4, .4, .4, .14)
303 self
.__dialog
['relief'] = FLAT
304 self
.__dialog
.component('text0')['fg'] = (.9, .9, .9, 1)
305 self
.__dialog
.component('text0')['font'] = self
._font
306 for b
in self
.__dialog
.buttonList
:
307 b
['frameColor'] = (.4, .4, .4, .14)
308 b
.component('text0')['fg'] = (.9, .9, .9, 1)
309 b
.component('text0')['font'] = self
._font
310 b
.component('text1')['fg'] = (.9, .1, .1, 1)
311 b
.component('text1')['font'] = self
._font
312 b
.component('text2')['fg'] = (.9, .9, .1, 1)
313 b
.component('text2')['font'] = self
._font
315 def __on_new_item(self
, item
):
319 if item
in ['PixelSpaceTestItem', 'WorldSpaceTestItem']:
321 _item
= self
.__new
_items
[item
](
322 self
.__context
.world
,
323 self
.__context
.plane_node
,
324 self
.__context
.cb_inst
,
325 self
.__context
.curr_bottom
,
326 self
.__context
.repos
,
329 _item
._Item
__editing
= True
330 self
.__add
_item
(_item
)
331 item_json
['class'] = _item
.__class
__.__name
__
332 item_json
['position'] = list(_item
._np
.get_pos())
333 if item
== 'PixelSpaceTestItem':
334 del item_json
['class']
335 self
.__json
['test_items']['pixel_space'] += [item_json
]
336 elif item
== 'WorldSpaceTestItem':
337 del item_json
['class']
338 self
.__json
['test_items']['world_space'] += [item_json
]
340 self
.__json
['items'] += [item_json
]
342 def __actually_close(self
, arg
):
345 messenger
.send('editor-stop')
346 self
.__dialog
.cleanup()
350 self
.__json
['name'] = self
.__name
_entry
.get()
351 self
.__json
['instructions'] = self
.__instructions
_entry
.get()
352 self
.__json
['version'] = self
.__compute
_hash
()
353 json_name
= self
.__filenamename
_entry
.get()
354 with
open('assets/scenes/%s.json' % json_name
, 'w') as f
:
355 f
.write(dumps(self
.__json
, indent
=2, sort_keys
=True))
357 def __on_scene_list(self
):
358 self
.__scene
_list
= SceneList(self
.__pos
_mgr
)
360 def __load_img_btn(self
, path
, col
):
361 img
= OnscreenImage('assets/images/buttons/%s.dds' % path
)
362 img
.set_transparency(True)
367 def __compute_hash(self
):
368 new_dict
= deepcopy(self
.__json
)
369 del new_dict
['version']
370 new_dict_str
= str(dumps(new_dict
, indent
=2, sort_keys
=True))
371 h
= hashlib
.new('sha256')
372 h
.update(new_dict_str
.encode())
373 return h
.hexdigest()[:12]
375 def __on_item_click(self
, item
):
376 if self
.__inspector
and self
.__inspector
.item
== item
: return
378 self
.__inspector
.destroy()
379 if item
.__class
__ == PixelSpaceTestItem
:
380 self
.__inspector
= PixelSpaceInspector(item
, self
.__items
)
381 elif item
.__class
__ == WorldSpaceTestItem
:
382 self
.__inspector
= WorldSpaceInspector(item
, self
.__items
, self
.__pos
_mgr
)
384 self
.__inspector
= Inspector(item
, self
.__items
, self
.__pos
_mgr
, self
.__item
_strategy
_classes
)
386 def __on_inspector_destroy(self
):
387 self
.__inspector
= None
389 def __on_new_scene(self
):
391 messenger
.send('editor-stop')
392 messenger
.send('new_scene')
394 def __on_start_items(self
):
395 self
.__start
_items
= StartItems(self
.__json
['start_items'], self
.__pos
_mgr
, self
.__item
_classes
, self
.__item
_strategy
_classes
)
397 def __on_start_items_save(self
, start_items
):
398 self
.__json
['start_items'] = start_items
401 def __on_start_items_destroy(self
):
402 self
.__start
_items
= None
405 def test_items(self
):
406 return self
.__test
_items
411 self
.__inspector
.destroy()
412 self
.ignore('editor-item-click')
413 self
.ignore('editor-inspector-destroy')
414 self
.ignore('editor-start-items-save')
415 self
.ignore('editor-start-items-destroy')
416 if self
.__dialog
: self
.__actually
_close
(False)
417 for t
in self
.__test
_items
: t
.destroy()
418 self
.__test
_items
= []
419 if self
.__start
_items
:
420 self
.__start
_items
.destroy()
421 if self
.__scene
_list
:
422 self
.__scene
_list
.destroy()