| 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.dds', (.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.dds', (.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 | top = .1 if len(scenes) < 5 else .6 |
| 192 | row = 0 if i < 4 else 1 |
| 193 | self._widgets += [DirectButton( |
| 194 | text=cls.name(), pos=(left + dx * (i % 4), 1, top - dx * row), |
| 195 | command=self.start, extraArgs=[cls], text_wordwrap=6, |
| 196 | frameTexture='assets/images/scenes/%s.dds' % cls.__name__, |
| 197 | **cmn)] |
| 198 | for j in range(4): |
| 199 | tnode = self._widgets[-1].component('text%s' % j).textNode |
| 200 | height = - tnode.getLineHeight() / 2 |
| 201 | height += (tnode.get_height() - tnode.get_line_height()) / 2 |
| 202 | self._widgets[-1].component('text%s' % j).set_pos(0, 0, height) |
| 203 | self._widgets += [DirectButton( |
| 204 | text=_('Back'), pos=(0, 1, -.8), command=self.on_back, |
| 205 | **self._common_btn)] |
| 206 | |
| 207 | def start(self, cls): |
| 208 | self._fsm.demand('Scene', cls) |
| 209 | |
| 210 | def on_options(self): |
| 211 | self.destroy() |
| 212 | self._cursor = MouseCursor( |
| 213 | 'assets/buttons/arrowUpLeft.dds', (.04, 1, .04), (.5, .5, .5, 1), |
| 214 | (.01, .01)) |
| 215 | self._set_options() |
| 216 | |
| 217 | def on_credits(self): |
| 218 | self.destroy() |
| 219 | self._cursor = MouseCursor( |
| 220 | 'assets/buttons/arrowUpLeft.dds', (.04, 1, .04), (.5, .5, .5, 1), |
| 221 | (.01, .01)) |
| 222 | self._set_credits() |
| 223 | |
| 224 | def _rearrange_width(self): |
| 225 | max_width = 0 |
| 226 | for wdg in self._widgets: |
| 227 | t_n = wdg.component('text0') |
| 228 | u_l = t_n.textNode.get_upper_left_3d() |
| 229 | l_r = t_n.textNode.get_lower_right_3d() |
| 230 | max_width = max(l_r[0] - u_l[0], max_width) |
| 231 | for wdg in self._widgets: |
| 232 | m_w = max_width / 2 + .8 |
| 233 | wdg['frameSize'] = -m_w, m_w, wdg['frameSize'][2], wdg['frameSize'][3] |
| 234 | |
| 235 | def on_language(self, arg): |
| 236 | lang_code = { |
| 237 | _('English'): 'en_EN', |
| 238 | _('Italian'): 'it_IT'}[arg] |
| 239 | self._lang_mgr.set_lang(lang_code) |
| 240 | self._opt_file['settings']['language'] = lang_code[:2] |
| 241 | self._opt_file.store() |
| 242 | self.on_options() |
| 243 | |
| 244 | def on_volume(self): |
| 245 | self._opt_file['settings']['volume'] = self._slider['value'] |
| 246 | self._music.set_volume(self._slider['value']) |
| 247 | |
| 248 | def on_fullscreen(self, arg): |
| 249 | props = WindowProperties() |
| 250 | props.set_fullscreen(arg) |
| 251 | base.win.request_properties(props) |
| 252 | self._opt_file['settings']['fullscreen'] = int(arg) |
| 253 | self._opt_file.store() |
| 254 | |
| 255 | def on_resolution(self, arg): |
| 256 | props = WindowProperties() |
| 257 | props.set_size(LVector2i(*[int(_res) for _res in arg.split('x')])) |
| 258 | base.win.request_properties(props) |
| 259 | self._opt_file['settings']['resolution'] = arg |
| 260 | self._opt_file.store() |
| 261 | |
| 262 | def on_aa(self, arg): |
| 263 | self._pipeline.msaa_samples = 4 if arg else 1 |
| 264 | debug(f'msaa: {self._pipeline.msaa_samples}') |
| 265 | self._opt_file['settings']['antialiasing'] = int(arg) |
| 266 | self._opt_file.store() |
| 267 | |
| 268 | def on_shadows(self, arg): |
| 269 | self._pipeline.enable_shadows = int(arg) |
| 270 | debug(f'shadows: {self._pipeline.enable_shadows}') |
| 271 | self._opt_file['settings']['shadows'] = int(arg) |
| 272 | self._opt_file.store() |
| 273 | |
| 274 | def on_website(self): |
| 275 | if platform.startswith('linux'): |
| 276 | environ['LD_LIBRARY_PATH'] = '' |
| 277 | system('xdg-open https://www.ya2.it') |
| 278 | else: |
| 279 | open_new_tab('https://www.ya2.it') |
| 280 | |
| 281 | def on_back(self): |
| 282 | self._opt_file.store() |
| 283 | self.destroy() |
| 284 | self._cursor = MouseCursor( |
| 285 | 'assets/buttons/arrowUpLeft.dds', (.04, 1, .04), (.5, .5, .5, 1), |
| 286 | (.01, .01)) |
| 287 | self._set_main() |
| 288 | |
| 289 | def destroy(self): |
| 290 | [wdg.destroy() for wdg in self._widgets] |
| 291 | self._cursor.destroy() |