ya2 · news · projects · code · about

editor: start items
[pmachines.git] / pmachines / editor / scene.py
CommitLineData
d3a2e50a
FC
1from copy import deepcopy
2from json import dumps
2aeb9f68
FC
3from importlib import import_module
4from inspect import isclass
5from glob import glob
6from os.path import basename
7from logging import info
d3a2e50a
FC
8import hashlib
9from panda3d.core import Texture, TextNode
10from direct.gui.OnscreenImage import OnscreenImage
2aeb9f68
FC
11from direct.gui.DirectGui import DirectButton, DirectFrame, DirectEntry, \
12 YesNoDialog, DirectOptionMenu
d3a2e50a
FC
13from direct.gui.DirectGuiGlobals import FLAT, NORMAL
14from direct.gui.OnscreenText import OnscreenText
ee65fee0 15from direct.showbase.DirectObject import DirectObject
2aeb9f68 16from pmachines.items.item import Item
012cf969 17from pmachines.editor.scene_list import SceneList
ee65fee0 18from pmachines.editor.inspector import Inspector
9981d472 19from pmachines.editor.start_items import StartItems
d3a2e50a
FC
20
21
ee65fee0 22class SceneEditor(DirectObject):
d3a2e50a 23
2aeb9f68 24 def __init__(self, json, json_name, context, add_item, items):
ee65fee0 25 super().__init__()
2aeb9f68 26 self.__items = items
d3a2e50a 27 self.__json = json
3e3c4caf
FC
28 if not json_name:
29 self.__json = json = {
30 'name': '',
31 'instructions': '',
32 'version': '',
33 'items': [],
34 'start_items': [],
35 'test_items': {
36 'pixel_space': [],
37 'world_space': []}}
ee65fee0 38 self.__inspector = None
2aeb9f68
FC
39 self.__context = context
40 self.__add_item = add_item
d3a2e50a
FC
41 self._font = base.loader.load_font(
42 'assets/fonts/Hanken-Book.ttf')
43 self._font.clear()
44 self._font.set_pixels_per_unit(60)
45 self._font.set_minfilter(Texture.FTLinearMipmapLinear)
46 self._font.set_outline((0, 0, 0, 1), .8, .2)
47 self._common = {
48 'scale': .046,
49 'text_font': self._font,
50 'text_fg': (.9, .9, .9, 1),
51 'relief': FLAT,
52 'frameColor': (.4, .4, .4, .14),
53 'rolloverSound': loader.load_sfx(
54 'assets/audio/sfx/rollover.ogg'),
55 'clickSound': loader.load_sfx(
56 'assets/audio/sfx/click.ogg')}
3e3c4caf 57 w, h, tw, l = 1.8, 1, 30, .36
d3a2e50a
FC
58 self._frm = DirectFrame(frameColor=(.4, .4, .4, .06),
59 frameSize=(0, w, 0, h),
60 parent=base.a2dBottomCenter,
61 pos=(-w/2, 0, 0))
62 OnscreenText(
3e3c4caf 63 _('Filename'), pos=(l - .03, h - .1), parent=self._frm,
d3a2e50a
FC
64 font=self._common['text_font'],
65 scale=self._common['scale'],
66 fg=self._common['text_fg'],
67 align=TextNode.A_right)
3e3c4caf 68 self.__filenamename_entry = DirectEntry(
d3a2e50a
FC
69 scale=self._common['scale'],
70 pos=(l, 1, h - .1),
71 entryFont=self._font,
72 width=tw,
73 frameColor=self._common['frameColor'],
3e3c4caf
FC
74 initialText=json_name,
75 parent=self._frm,
76 text_fg=self._common['text_fg'])
77 OnscreenText(
78 _('Name'), pos=(l - .03, h - .2), parent=self._frm,
79 font=self._common['text_font'],
80 scale=self._common['scale'],
81 fg=self._common['text_fg'],
82 align=TextNode.A_right)
83 self.__name_entry = DirectEntry(
84 scale=self._common['scale'],
85 pos=(l, 1, h - .2),
86 entryFont=self._font,
87 width=tw,
88 frameColor=self._common['frameColor'],
d3a2e50a
FC
89 initialText=json['name'],
90 parent=self._frm,
91 text_fg=self._common['text_fg'])
92 OnscreenText(
3e3c4caf 93 _('Description'), pos=(l - .03, h - .3), parent=self._frm,
d3a2e50a
FC
94 font=self._common['text_font'],
95 scale=self._common['scale'],
96 fg=self._common['text_fg'],
97 align=TextNode.A_right)
98 def add_line_break(txt, entry):
99 curpos = entry.node().getCursorPosition()
100 entry.set(txt[:curpos]+ "\n" + txt[curpos:])
101 entry.node().setCursorPosition(curpos+1)
102 entry['focus']=1
103 self.__instructions_entry = DirectEntry(
104 scale=self._common['scale'],
3e3c4caf 105 pos=(l, 1, h - .3),
d3a2e50a
FC
106 entryFont=self._font,
107 width=tw,
108 numLines=12,
109 cursorKeys=True,
110 frameColor=self._common['frameColor'],
111 initialText=json['instructions'].replace('\\n', '\n'),
112 parent=self._frm,
113 text_fg=self._common['text_fg'])
114 self.__instructions_entry['command'] = add_line_break
115 self.__instructions_entry['extraArgs'] = [self.__instructions_entry]
116 def load_images_btn(path, col):
117 colors = {
118 'gray': [
119 (.6, .6, .6, 1), # ready
120 (1, 1, 1, 1), # press
121 (.8, .8, .8, 1), # rollover
122 (.4, .4, .4, .4)],
123 'green': [
124 (.1, .68, .1, 1),
125 (.1, 1, .1, 1),
126 (.1, .84, .1, 1),
127 (.4, .1, .1, .4)]}[col]
128 return [self.__load_img_btn(path, col) for col in colors]
129 fcols = (.4, .4, .4, .14), (.3, .3, .3, .05)
012cf969 130 DirectButton(
d3a2e50a
FC
131 image=load_images_btn('exitRight', 'gray'), scale=.05,
132 pos=(.06, 1, .06),
133 parent=self._frm, command=self.__on_close, state=NORMAL, relief=FLAT,
134 frameColor=fcols[0],
135 rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
136 clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
012cf969 137 DirectButton(
d3a2e50a
FC
138 image=load_images_btn('save', 'gray'), scale=.05,
139 pos=(.06, 1, .18),
140 parent=self._frm, command=self.__on_save, state=NORMAL, relief=FLAT,
141 frameColor=fcols[0],
142 rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
143 clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
012cf969
FC
144 DirectButton(
145 image=load_images_btn('menuList', 'gray'), scale=.05,
146 pos=(.06, 1, .30),
147 parent=self._frm, command=self.__on_scene_list, state=NORMAL, relief=FLAT,
148 frameColor=fcols[0],
149 rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
150 clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
2aeb9f68
FC
151 item_modules = glob('pmachines/items/*.py')
152 item_modules = [basename(i)[:-3] for i in item_modules]
153 self.__new_items = {}
154 for item_module in item_modules:
155 mod_name = 'pmachines.items.' + item_module
156 for member in import_module(mod_name).__dict__.values():
157 if isclass(member) and issubclass(member, Item) and \
158 member != Item:
159 self.__new_items[member.__name__] = member
160 OnscreenText(
161 _('new item'), pos=(.02, .46), parent=self._frm,
162 font=self._common['text_font'],
163 scale=self._common['scale'],
164 fg=self._common['text_fg'],
165 align=TextNode.A_left)
166 DirectOptionMenu(
167 scale=.05,
168 text=_('new item'), pos=(.02, 1, .4), items=list(self.__new_items.keys()),
169 parent=self._frm, command=self.__on_new_item, state=NORMAL,
170 relief=FLAT, item_relief=FLAT,
171 frameColor=fcols[0], item_frameColor=fcols[0],
172 popupMenu_frameColor=fcols[0],
173 popupMarker_frameColor=fcols[0],
174 text_font=self._font, text_fg=(.9, .9, .9, 1),
175 highlightColor=(.9, .9, .9, .9),
176 item_text_font=self._font, item_text_fg=(.9, .9, .9, 1),
177 rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
178 clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
3e3c4caf 179 DirectButton(
9981d472 180 image=load_images_btn('start_items', 'gray'), scale=.05,
3e3c4caf 181 pos=(.06, 1, .58),
9981d472
FC
182 parent=self._frm, command=self.__on_start_items, state=NORMAL, relief=FLAT,
183 frameColor=fcols[0],
184 rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
185 clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
186 DirectButton(
187 image=load_images_btn('plus', 'gray'), scale=.05,
188 pos=(.06, 1, .7),
3e3c4caf
FC
189 parent=self._frm, command=self.__on_new_scene, state=NORMAL, relief=FLAT,
190 frameColor=fcols[0],
191 rolloverSound=loader.load_sfx('assets/audio/sfx/rollover.ogg'),
192 clickSound=loader.load_sfx('assets/audio/sfx/click.ogg'))
ee65fee0 193 messenger.send('editor-start')
2aeb9f68 194 self.accept('editor-item-click', self.__on_item_click)
ee65fee0 195 self.accept('editor-inspector-destroy', self.__on_inspector_destroy)
9981d472
FC
196 self.accept('editor-start-items-save', self.__on_start_items_save)
197 self.accept('editor-start-items-destroy', self.__on_start_items_destroy)
d3a2e50a
FC
198
199 def __on_close(self):
200 self.__json['name'] = self.__name_entry.get()
201 self.__json['instructions'] = self.__instructions_entry.get()
202 if self.__compute_hash() == self.__json['version']:
203 self._frm.destroy()
204 else:
205 self.__dialog = YesNoDialog(dialogName='Unsaved changes',
206 text=_('You have unsaved changes. Really quit?'),
207 command=self.__actually_close)
012cf969
FC
208 self.__dialog['frameColor'] = (.4, .4, .4, .14)
209 self.__dialog['relief'] = FLAT
210 self.__dialog.component('text0')['fg'] = (.9, .9, .9, 1)
211 self.__dialog.component('text0')['font'] = self._font
212 for b in self.__dialog.buttonList:
213 b['frameColor'] = (.4, .4, .4, .14)
214 b.component('text0')['fg'] = (.9, .9, .9, 1)
215 b.component('text0')['font'] = self._font
216 b.component('text1')['fg'] = (.9, .1, .1, 1)
217 b.component('text1')['font'] = self._font
218 b.component('text2')['fg'] = (.9, .9, .1, 1)
219 b.component('text2')['font'] = self._font
d3a2e50a 220
2aeb9f68
FC
221 def __on_new_item(self, item):
222 info(f'new {item}')
223 item_json = {}
224 _item = self.__new_items[item](
225 self.__context.world,
226 self.__context.plane_node,
227 self.__context.cb_inst,
228 self.__context.curr_bottom,
229 self.__context.repos,
230 item_json)
231 _item._Item__editing = True
232 self.__add_item(_item)
233 item_json['class'] = _item.__class__.__name__
234 item_json['position'] = list(_item._np.get_pos())
235 self.__json['items'] += [item_json]
236
d3a2e50a
FC
237 def __actually_close(self, arg):
238 if arg:
3e3c4caf 239 self.destroy()
ee65fee0 240 messenger.send('editor-stop')
d3a2e50a
FC
241 self.__dialog.cleanup()
242
243 def __on_save(self):
244 self.__json['name'] = self.__name_entry.get()
245 self.__json['instructions'] = self.__instructions_entry.get()
246 self.__json['version'] = self.__compute_hash()
3e3c4caf
FC
247 json_name = self.__filenamename_entry.get()
248 with open('assets/scenes/%s.json' % json_name, 'w') as f:
d3a2e50a
FC
249 f.write(dumps(self.__json, indent=2, sort_keys=True))
250
012cf969
FC
251 def __on_scene_list(self):
252 SceneList()
253
d3a2e50a
FC
254 def __load_img_btn(self, path, col):
255 img = OnscreenImage('assets/images/buttons/%s.dds' % path)
256 img.set_transparency(True)
257 img.set_color(col)
258 img.detach_node()
259 return img
260
261 def __compute_hash(self):
262 new_dict = deepcopy(self.__json)
263 del new_dict['version']
264 new_dict_str = str(dumps(new_dict, indent=2, sort_keys=True))
265 h = hashlib.new('sha256')
266 h.update(new_dict_str.encode())
267 return h.hexdigest()[:12]
ee65fee0 268
2aeb9f68 269 def __on_item_click(self, item):
ee65fee0
FC
270 if self.__inspector and self.__inspector.item == item: return
271 if self.__inspector:
272 self.__inspector.destroy()
2aeb9f68 273 self.__inspector = Inspector(item, self.__items)
ee65fee0
FC
274
275 def __on_inspector_destroy(self):
276 self.__inspector = None
3e3c4caf
FC
277
278 def __on_new_scene(self):
279 self.destroy()
280 messenger.send('editor-stop')
281 messenger.send('new_scene')
282
9981d472
FC
283 def __on_start_items(self):
284 self.__start_items = StartItems(self.__json['start_items'])
285
286 def __on_start_items_save(self, start_items):
287 self.__json['start_items'] = start_items
288 self.__on_save()
289
290 def __on_start_items_destroy(self):
291 self.__start_items = None
292
3e3c4caf
FC
293 def destroy(self):
294 self._frm.destroy()
295 if self.__inspector:
296 self.__inspector.destroy()
297 self.ignore('editor-item-click')
298 self.ignore('editor-inspector-destroy')
9981d472
FC
299 self.ignore('editor-start-items-save')
300 self.ignore('editor-start-items-destroy')