ya2 · news · projects · code · about

5bd16677efe3f4758c057004aa5534582609aacf
[pmachines.git] / pmachines / menu.py
1 from logging import info, debug
2 from sys import platform, exit
3 from os import environ, system
4 from glob import glob
5 from importlib import import_module
6 from inspect import isclass
7 from webbrowser import open_new_tab
8 from panda3d.core import Texture, TextNode, WindowProperties, LVector2i, \
9 TextProperties, TextPropertiesManager
10 from direct.gui.DirectGui import DirectButton, DirectCheckButton, \
11 DirectOptionMenu, DirectSlider, DirectCheckButton
12 from direct.gui.DirectGuiGlobals import FLAT
13 from direct.gui.OnscreenText import OnscreenText
14 from lib.engine.gui.cursor import MouseCursor
15 from pmachines.scene import Scene
16 from panda3d.bullet import BulletWorld
17
18
19 class Menu:
20
21 def __init__(self, fsm, lang_mgr, opt_file, music, pipeline):
22 self._fsm = fsm
23 self._lang_mgr = lang_mgr
24 self._opt_file = opt_file
25 self._music = music
26 self._pipeline = pipeline
27 self._cursor = MouseCursor(
28 'assets/buttons/arrowUpLeft.png', (.04, 1, .04), (.5, .5, .5, 1),
29 (.01, .01))
30 self._font = base.loader.load_font('assets/fonts/Hanken-Book.ttf')
31 self._font.clear()
32 self._font.set_pixels_per_unit(60)
33 self._font.set_minfilter(Texture.FTLinearMipmapLinear)
34 self._font.set_outline((0, 0, 0, 1), .8, .2)
35 self._widgets = []
36 self._common = {
37 'scale': .12,
38 'text_font': self._font,
39 'text_fg': (.9, .9, .9, 1),
40 'relief': FLAT,
41 'frameColor': (.4, .4, .4, .14),
42 'rolloverSound': loader.load_sfx('assets/audio/sfx/rollover.ogg'),
43 'clickSound': loader.load_sfx('assets/audio/sfx/click.ogg')}
44 self._common_btn = {'frameSize': (-4.8, 4.8, -.6, 1.2)} | self._common
45 hlc = self._common_btn['frameColor']
46 hlc = (hlc[0] + .2, hlc[1] + .2, hlc[2] + .2, hlc[3] + .2)
47 self._common_opt = {
48 'item_frameColor': self._common_btn['frameColor'],
49 'popupMarker_frameColor': self._common_btn['frameColor'],
50 'item_relief': self._common_btn['relief'],
51 'item_text_font': self._common_btn['text_font'],
52 'item_text_fg': self._common_btn['text_fg'],
53 'textMayChange': 1,
54 'highlightColor': hlc,
55 'text_align': TextNode.A_center,
56 } | self._common_btn
57 f_s = self._common_opt['frameSize']
58 self._common_opt['frameSize'] = f_s[0], f_s[1] - .56, f_s[2], f_s[3]
59 self._common_slider = self._common | {
60 'range': (0, 1),
61 'thumb_frameColor': (.4, .4, .4, .4),
62 'thumb_scale': 1.6,
63 'scale': .4}
64 del self._common_slider['rolloverSound']
65 del self._common_slider['clickSound']
66 self._set_main()
67
68 def _set_main(self):
69 self._widgets = []
70 self._widgets += [DirectButton(
71 text=_('Play'), pos=(0, 1, .6), command=self.on_play,
72 **self._common_btn)]
73 self._widgets += [DirectButton(
74 text=_('Options'), pos=(0, 1, .2), command=self.on_options,
75 **self._common_btn)]
76 self._widgets += [DirectButton(
77 text=_('Credits'), pos=(0, 1, -.2), command=self.on_credits,
78 **self._common_btn)]
79 self._widgets += [DirectButton(
80 text=_('Exit'), pos=(0, 1, -.6), command=lambda: exit(),
81 **self._common_btn)]
82 self._rearrange_width()
83
84 def _set_options(self):
85 self._widgets = []
86 self._lang_funcs = [lambda: _('English'), lambda: _('Italian')]
87 items = [fnc() for fnc in self._lang_funcs]
88 inititem = {
89 'en': _('English'),
90 'it': _('Italian')
91 }[self._opt_file['settings']['language']]
92 btn = DirectOptionMenu(
93 text=_('Language'), items=items, initialitem=inititem,
94 pos=(0, 1, .8), command=self.on_language, **self._common_opt)
95 btn.popupMenu['frameColor'] = self._common_btn['frameColor']
96 btn.popupMenu['relief'] = self._common_btn['relief']
97 self._widgets += [btn]
98 self._widgets += [OnscreenText(
99 _('Volume'), pos=(-.1, .55), font=self._common['text_font'],
100 scale=self._common['scale'], fg=self._common['text_fg'],
101 align=TextNode.A_right)]
102 self._widgets += [DirectSlider(
103 pos=(.5, 1, .57),
104 value=self._opt_file['settings']['volume'],
105 command=self.on_volume,
106 **self._common_slider)]
107 self._slider = self._widgets[-1]
108 self._widgets += [DirectCheckButton(
109 text=_('Fullscreen'), pos=(0, 1, .3), command=self.on_fullscreen,
110 indicator_frameColor=self._common_opt['highlightColor'],
111 indicator_relief=self._common_btn['relief'],
112 indicatorValue=self._opt_file['settings']['fullscreen'],
113 **self._common_btn)]
114 res = self._opt_file['settings']['resolution']
115 d_i = base.pipe.get_display_information()
116 def _res(idx):
117 return d_i.get_display_mode_width(idx), \
118 d_i.get_display_mode_height(idx)
119 resolutions = [
120 _res(idx) for idx in range(d_i.get_total_display_modes())]
121 resolutions = list(set(resolutions))
122 resolutions = sorted(resolutions)
123 resolutions = [(str(_res[0]), str(_res[1])) for _res in resolutions]
124 resolutions = ['x'.join(_res) for _res in resolutions]
125 if not res:
126 res = resolutions[-1]
127 btn = DirectOptionMenu(
128 text=_('Resolution'), items=resolutions, initialitem=res,
129 pos=(0, 1, .05), command=self.on_resolution, **self._common_opt)
130 btn.popupMenu['frameColor'] = self._common_btn['frameColor']
131 btn.popupMenu['relief'] = self._common_btn['relief']
132 self._widgets += [btn]
133 self._widgets += [DirectCheckButton(
134 text=_('Antialiasing'), pos=(0, 1, -.2), command=self.on_aa,
135 indicator_frameColor=self._common_opt['highlightColor'],
136 indicator_relief=self._common_btn['relief'],
137 indicatorValue=self._opt_file['settings']['antialiasing'],
138 **self._common_btn)]
139 self._widgets += [DirectCheckButton(
140 text=_('Shadows'), pos=(0, 1, -.45), command=self.on_shadows,
141 indicator_frameColor=self._common_opt['highlightColor'],
142 indicator_relief=self._common_btn['relief'],
143 indicatorValue=self._opt_file['settings']['shadows'],
144 **self._common_btn)]
145 self._widgets += [DirectButton(
146 text=_('Back'), pos=(0, 1, -.8), command=self.on_back,
147 **self._common_btn)]
148
149 def _set_credits(self):
150 self._widgets = []
151 tp_scale = TextProperties()
152 tp_scale.set_text_scale(.64)
153 TextPropertiesManager.getGlobalPtr().setProperties('scale', tp_scale)
154 self._widgets += [OnscreenText(
155 _('Code and gfx\n \1scale\1Flavio Calva\2\n\n\nMusic\n \1scale\1Stefan Grossmann\2'),
156 pos=(-.9, .55), font=self._common['text_font'],
157 scale=self._common['scale'], fg=self._common['text_fg'],
158 align=TextNode.A_left)]
159 self._widgets += [DirectButton(
160 text=_('Website'), pos=(-.6, 1, .29), command=self.on_website,
161 **self._common_btn | {'scale': .08})]
162 self._widgets += [OnscreenText(
163 _('Special thanks to:\n \1scale\1rdb\2\n \1scale\1Luisa Tenuta\2\n \1scale\1Damiana Ercolani\2'),
164 pos=(.1, .55), font=self._common['text_font'],
165 scale=self._common['scale'], fg=self._common['text_fg'],
166 align=TextNode.A_left)]
167 self._widgets += [DirectButton(
168 text=_('Back'), pos=(0, 1, -.8), command=self.on_back,
169 **self._common_btn)]
170
171 def on_play(self):
172 scenes = []
173 for _file in glob('pmachines/scenes/*.py'):
174 _fn = _file.replace('.py', '').replace('/', '.')
175 for member in import_module(_fn).__dict__.values():
176 if isclass(member) and issubclass(member, Scene) and \
177 member != Scene:
178 scenes += [member]
179 scenes = sorted(scenes, key=lambda elm: elm.sorting)
180 self.destroy()
181 self._cursor = MouseCursor(
182 'assets/buttons/arrowUpLeft.png', (.04, 1, .04), (.5, .5, .5, 1),
183 (.01, .01))
184 self._widgets = []
185 cmn = self._common_btn.copy() | {
186 'frameSize': (-2.4, 2.4, -2.4, 2.4),
187 'frameColor': (1, 1, 1, .8),
188 'text_scale': .64}
189 left = - (dx := .8) * (min(4, len(scenes)) - 1) / 2
190 for i, cls in enumerate(scenes):
191 print(i, cls)
192 top = .1 if len(scenes) < 5 else .6
193 row = 0 if i < 4 else 1
194 self._widgets += [DirectButton(
195 text=cls.name(), pos=(left + dx * (i % 4), 1, top - dx * row),
196 command=self.start, extraArgs=[cls], text_wordwrap=6,
197 frameTexture='assets/images/scenes/%s.png' % cls.__name__,
198 **cmn)]
199 for j in range(4):
200 tnode = self._widgets[-1].component('text%s' % j).textNode
201 height = - tnode.getLineHeight() / 2
202 height += (tnode.get_height() - tnode.get_line_height()) / 2
203 self._widgets[-1].component('text%s' % j).set_pos(0, 0, height)
204 self._widgets += [DirectButton(
205 text=_('Back'), pos=(0, 1, -.8), command=self.on_back,
206 **self._common_btn)]
207
208 def start(self, cls):
209 self._fsm.demand('Scene', cls)
210
211 def on_options(self):
212 self.destroy()
213 self._cursor = MouseCursor(
214 'assets/buttons/arrowUpLeft.png', (.04, 1, .04), (.5, .5, .5, 1),
215 (.01, .01))
216 self._set_options()
217
218 def on_credits(self):
219 self.destroy()
220 self._cursor = MouseCursor(
221 'assets/buttons/arrowUpLeft.png', (.04, 1, .04), (.5, .5, .5, 1),
222 (.01, .01))
223 self._set_credits()
224
225 def _rearrange_width(self):
226 max_width = 0
227 for wdg in self._widgets:
228 t_n = wdg.component('text0')
229 u_l = t_n.textNode.get_upper_left_3d()
230 l_r = t_n.textNode.get_lower_right_3d()
231 max_width = max(l_r[0] - u_l[0], max_width)
232 for wdg in self._widgets:
233 m_w = max_width / 2 + .8
234 wdg['frameSize'] = -m_w, m_w, wdg['frameSize'][2], wdg['frameSize'][3]
235
236 def on_language(self, arg):
237 lang_code = {
238 _('English'): 'en_EN',
239 _('Italian'): 'it_IT'}[arg]
240 self._lang_mgr.set_lang(lang_code)
241 self._opt_file['settings']['language'] = lang_code[:2]
242 self._opt_file.store()
243 self.on_options()
244
245 def on_volume(self):
246 self._opt_file['settings']['volume'] = self._slider['value']
247 self._music.set_volume(self._slider['value'])
248
249 def on_fullscreen(self, arg):
250 props = WindowProperties()
251 props.set_fullscreen(arg)
252 base.win.request_properties(props)
253 self._opt_file['settings']['fullscreen'] = int(arg)
254 self._opt_file.store()
255
256 def on_resolution(self, arg):
257 props = WindowProperties()
258 props.set_size(LVector2i(*[int(_res) for _res in arg.split('x')]))
259 base.win.request_properties(props)
260 self._opt_file['settings']['resolution'] = arg
261 self._opt_file.store()
262
263 def on_aa(self, arg):
264 self._pipeline.msaa_samples = 4 if arg else 1
265 debug(f'msaa: {self._pipeline.msaa_samples}')
266 self._opt_file['settings']['antialiasing'] = int(arg)
267 self._opt_file.store()
268
269 def on_shadows(self, arg):
270 self._pipeline.enable_shadows = int(arg)
271 debug(f'shadows: {self._pipeline.enable_shadows}')
272 self._opt_file['settings']['shadows'] = int(arg)
273 self._opt_file.store()
274
275 def on_website(self):
276 if platform.startswith('linux'):
277 environ['LD_LIBRARY_PATH'] = ''
278 system('xdg-open https://www.ya2.it')
279 else:
280 open_new_tab('https://www.ya2.it')
281
282 def on_back(self):
283 self._opt_file.store()
284 self.destroy()
285 self._cursor = MouseCursor(
286 'assets/buttons/arrowUpLeft.png', (.04, 1, .04), (.5, .5, .5, 1),
287 (.01, .01))
288 self._set_main()
289
290 def destroy(self):
291 [wdg.destroy() for wdg in self._widgets]
292 self._cursor.destroy()