ya2 · news · projects · code · about

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