ya2 · news · projects · code · about

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