Ejemplo n.º 1
0
class MainMenu:
    """The main game menu.

    Includes starting a game, loading, saving and exiting GUI.
    """
    def __init__(self):
        self._save_but = None
        self._chosen_crew = None
        self._load_msg = None
        self._load_screen = None
        self._is_first_pause = True
        self._save_blocked_lab = None
        self._menu_music = loader.loadSfx(  # noqa: F821
            "sounds/music/Among Madness - Fever.mp3")
        self._menu_music.setVolume(0.19)

        self.tactics_wids = []
        self.save_wids = []
        self.conf_wids = []
        self.cred_wids = []

        self._hover_snd = loader.loadSfx("sounds/GUI/menu1.ogg")  # noqa: F821
        self._hover_snd.setVolume(0.1)
        self.click_snd = loader.loadSfx("sounds/GUI/menu2.ogg")  # noqa: F821
        self.click_snd.setVolume(0.1)
        self.new_enemy_snd = loader.loadSfx(
            "sounds/new_enemy.ogg")  # noqa: F821
        self.new_enemy_snd.setPlayRate(0.95)

        self._main_fr = DirectFrame(
            frameSize=(-2, 2, -1, 1),
            frameColor=(0.15, 0.15, 0.15, 1),
            state=DGG.NORMAL,
        )
        wids = self._show_authors_word()
        taskMgr.doMethodLater(  # noqa: F821
            5,
            self._hide_authors_word,
            "stop_splash_screens",
            extraArgs=[wids])

    def _build(self):
        """Build the main menu."""
        but_params = {
            "text_scale": 0.05,
            "relief": None,
            "parent": self._main_fr,
            "clickSound": self.click_snd,
            "text_font": base.main_font,  # noqa: F821
        }
        self._new_game_but = DirectButton(  # New game
            pos=(-1.12, 0, 0.5),
            text_fg=RUST_COL,
            text=base.labels.MAIN_MENU[0],  # noqa: F821
            command=self._choose_tactics,
            text_align=TextNode.ALeft,
            **but_params,
        )
        self.bind_button(self._new_game_but)

        is_save_exists = save_exists(1) or save_exists(2) or save_exists(3)
        self._load_but = DirectButton(  # Load game
            pos=(-1.12, 0, 0.4),
            text_fg=RUST_COL if is_save_exists else (0.5, 0.5, 0.5, 1),
            text=base.labels.MAIN_MENU[1],  # noqa: F821
            command=self._show_slots if is_save_exists else None,
            extraArgs=[True],
            text_align=TextNode.ALeft,
            **but_params,
        )
        self.bind_button(self._load_but)

        self.bind_button(
            DirectButton(  # Options
                pos=(-1.12, 0, 0.2),
                text_fg=RUST_COL,
                text=base.labels.MAIN_MENU[2],  # noqa: F821
                text_align=TextNode.ALeft,
                command=self._show_conf,
                **but_params,
            ))
        self.bind_button(
            DirectButton(  # Credits
                pos=(-1.12, 0, 0.1),
                text_fg=RUST_COL,
                text=base.labels.MAIN_MENU[30],  # noqa: F821
                command=self._show_credits,
                text_align=TextNode.ALeft,
                **but_params,
            ))
        self.bind_button(
            DirectButton(  # Exit
                pos=(-1.12, 0, -0.1),
                text_fg=RUST_COL,
                text=base.labels.MAIN_MENU[3],  # noqa: F821
                command=sys.exit,
                extraArgs=[1],
                text_align=TextNode.ALeft,
                **but_params,
            ))
        self._alpha_disclaimer = DirectLabel(
            parent=self._main_fr,
            pos=(0, 0, -0.9),
            text_scale=0.03,
            text_fg=SILVER_COL,
            frameColor=(0, 0, 0, 0),
            text_font=base.main_font,  # noqa: F821
            text=base.labels.MAIN_MENU[4],  # noqa: F821
        )

    def _check_can_save(self, task):
        """Check if the game can be saved right now.

        If the game can't be saved, the cause will
        be shown under the "Save game" button.
        """
        if base.world.is_on_et:  # noqa: F821
            cause = base.labels.MAIN_MENU[16]  # noqa: F821
        elif base.world.is_in_city:  # noqa: F821
            cause = base.labels.MAIN_MENU[17]  # noqa: F821
        elif base.world.is_near_fork:  # noqa: F821
            cause = base.labels.MAIN_MENU[18]  # noqa: F821
        elif base.train.ctrl.critical_damage:  # noqa: F821
            cause = base.labels.MAIN_MENU[19]  # noqa: F821
        elif base.current_block.id < 4:  # noqa: F821
            cause = base.labels.MAIN_MENU[20]  # noqa: F821
        else:
            cause = None

        if cause:
            if self._save_blocked_lab is None:
                self._save_blocked_lab = DirectLabel(
                    parent=self._main_fr,
                    pos=(-1.12, 0, 0.26),
                    frameColor=(0, 0, 0, 0),
                    text_scale=0.026,
                    text_fg=SILVER_COL,
                    text=cause,
                    text_font=base.main_font,  # noqa: F821
                    text_align=TextNode.ALeft,
                )
            self._save_blocked_lab["text"] = cause
            self._save_but["text_fg"] = SILVER_COL
            self._save_but["command"] = None
            clear_wids(self.save_wids)
        else:
            self._save_but["text_fg"] = RUST_COL
            self._save_but["command"] = self._show_slots
            if self._save_blocked_lab is not None:
                self._save_blocked_lab.destroy()
                self._save_blocked_lab = None

        return task.again

    def _choose_tactics(self):
        """Choose initial tactics before new game start."""
        if self.tactics_wids:
            return

        clear_wids(self.save_wids)
        clear_wids(self.conf_wids)
        clear_wids(self.cred_wids)

        self.tactics_wids.append(
            DirectLabel(  # Choose your crew
                parent=self._main_fr,
                text=base.labels.MAIN_MENU[5],  # noqa: F821
                text_fg=RUST_COL,
                text_scale=0.04,
                text_font=base.main_font,  # noqa: F821
                frameColor=(0, 0, 0, 0),
                pos=(0.7, 0, 0.75),
            ))
        # an image with a crew representer
        self._crew_preview = DirectFrame(
            parent=self._main_fr,
            frameSize=(-0.61, 0.61, -0.35, 0.35),
            pos=(0.7, 0, 0.3),
        )
        self._crew_preview.setTransparency(TransparencyAttrib.MAlpha)
        self.tactics_wids.append(self._crew_preview)

        but_params = {
            "parent": self._main_fr,
            "text_scale": 0.035,
            "text_fg": RUST_COL,
            "text_font": base.main_font,  # noqa: F821
            "relief": None,
            "clickSound": self.click_snd,
            "command": self._show_crew,
        }
        self._crew_buts = {
            "soldiers":
            DirectButton(
                text=base.labels.MAIN_MENU[6],  # noqa: F821
                extraArgs=["soldiers"],
                pos=(0.5, 0, -0.15),
                **but_params,
            ),
            "raiders":
            DirectButton(
                text=base.labels.MAIN_MENU[7],  # noqa: F821
                extraArgs=["raiders"],
                pos=(0.7, 0, -0.15),
                **but_params,
            ),
            "anarchists":
            DirectButton(
                text=base.labels.MAIN_MENU[8],  # noqa: F821
                extraArgs=["anarchists"],
                pos=(0.925, 0, -0.15),
                **but_params,
            ),
        }
        self.tactics_wids += self._crew_buts.values()

        for but in self._crew_buts.values():
            self.bind_button(but)

        self._team_description = DirectLabel(  # Crew description
            parent=self._main_fr,
            text=base.labels.MAIN_MENU[9],  # noqa: F821
            text_fg=SILVER_COL,
            text_scale=0.03,
            text_font=base.main_font,  # noqa: F821
            frameColor=(0, 0, 0, 0),
            pos=(0.7, 0, -0.26),
        )
        self.tactics_wids.append(self._team_description)

        start_but = DirectButton(  # Start
            parent=self._main_fr,
            text_scale=0.045,
            text_fg=RUST_COL,
            text=base.labels.MAIN_MENU[10],  # noqa: F821
            text_font=base.main_font,  # noqa: F821
            relief=None,
            command=self._start_new_game,
            pos=(0.7, 0, -0.5),
            clickSound=self.click_snd,
        )
        self.bind_button(start_but)
        self.tactics_wids.append(start_but)

        self._show_crew("soldiers")

    def _clear_temp_wids(self, task):
        """Destroy widgets from the first game screen."""
        clear_wids(self.tactics_wids)
        self._alpha_disclaimer.destroy()
        return task.done

    def _dehighlight_but(self, button, _):
        """Dehighlight the button, when mouse pointer leaved it.

        Args:
            button (panda3d.gui.DirectGui.DirectButton):
                Button to dehighlight.
        """
        if button["command"] is not None:
            button["text_scale"] = (
                button["text_scale"][0] - 0.002,
                button["text_scale"][1] - 0.003,
            )

    def _hide_authors_word(self, wids):
        """Hide author's word splashscreen and build the main menu.

        Args:
            wids (list): Widgets to destroy.
        """
        for wid in wids:
            wid.destroy()

        self._menu_music.play()
        self._build()

    def _highlight_but(self, button, _):
        """Highlight the button pointed by mouse.

        Args:
            button (panda3d.gui.DirectGui.DirectButton):
                Button to highlight.
        """
        if button["command"] is not None:
            button["text_scale"] = (
                button["text_scale"][0] + 0.002,
                button["text_scale"][1] + 0.003,
            )
            self._hover_snd.play()

    def _load_game(self, num):
        """Load previously saved game.

        Args:
            num (int): The slot number.
        """
        save_file = "saves/save{}.dat".format(num)
        if not os.path.exists(save_file):
            return

        self.show_loading()
        taskMgr.doMethodLater(  # noqa: F821
            4, self._clear_temp_wids, "clear_main_menu_temp_wids")
        base.doMethodLater(  # noqa: F821
            0.25,
            base.load_game,
            "load_game",
            extraArgs=[num]  # noqa: F821
        )

    def _read_saves(self):
        """Read all the game save slots.

        Returns:
            list: Dicts, each of which describes a save.
        """
        saves = []
        for num in range(1, 4):
            if save_exists(num):
                save = shelve.open("saves/save{}".format(str(num)))
                classes = [char["class"] for char in save["team"]]

                saves.append({
                    "save_time":
                    save["save_time"],
                    "miles":
                    save["train"]["miles"],
                    "chars": (
                        classes.count("soldier"),
                        classes.count("raider"),
                        classes.count("anarchist"),
                    ),
                })
                save.close()
            else:
                saves.append({})

        return saves

    def _save_conf_and_restart(
        self,
        res_chooser,
        tutorial_check,
        lang_chooser,
        fps_chooser,
        fps_meter,
        multi_threading,
    ):
        """Save configurations and restart the game program.

        Args:
            res_chooser (GUI.widgets.ListChooser):
                Widget to choose a screen resolution.
            tutorial_check (direct.gui.DirectCheckButton):
                Tutorial enabling check button.
            lang_chooser (GUI.widgets.ListChooser):
                Widget to choose GUI language.
            fps_chooser (GUI.widgets.ListChooser):
                Framerate chooser.
            fps_meter (direct.gui.DirectCheckButton):
                FPS meter enabling check button.
            multi_threading (direct.gui.DirectCheckButton):
                Multi threading mode enabling check button.
        """
        base.game_config.update(  # noqa: F821
            res_chooser.chosen_item,
            lang_chooser.chosen_item,
            str(bool(tutorial_check["indicatorValue"])),
            fps_chooser.chosen_item,
            str(bool(fps_meter["indicatorValue"])),
            str(bool(multi_threading["indicatorValue"])),
        )
        base.restart_game()  # noqa: F821

    def _show_authors_word(self):
        """Show author's word splashscreen.

        Returns:
            list: Splash screen widgets.
        """
        title = DirectLabel(
            pos=(0, 0, 0.2),
            text_scale=0.045,
            text_fg=SILVER_COL,
            frameColor=(0, 0, 0, 0),
            text=base.labels.SPLASHES[0],  # noqa: F821
            state=DGG.NORMAL,
            text_font=base.main_font,  # noqa: F821
        )
        msg = DirectLabel(
            pos=(0, 0, 0.05),
            text_scale=0.035,
            text_fg=SILVER_COL,
            frameColor=(0, 0, 0, 0),
            text=base.labels.SPLASHES[1],  # noqa: F821,
            state=DGG.NORMAL,
            text_font=base.main_font,  # noqa: F821
        )
        return [title, msg]

    def _show_conf(self):
        """Show game configurations GUI."""
        clear_wids(self.save_wids)
        clear_wids(self.tactics_wids)
        clear_wids(self.cred_wids)

        self.conf_wids.append(
            DirectLabel(  # Resolution:
                parent=self._main_fr,
                text=base.labels.MAIN_MENU[22],  # noqa: F821,
                text_fg=RUST_COL,
                text_scale=0.04,
                text_font=base.main_font,  # noqa: F821
                pos=(-0.3, 0, 0.5),
                frameColor=(0, 0, 0, 0),
            ))

        if base.game_config.resolution in RESOLUTIONS:  # noqa: F821
            res_ind = RESOLUTIONS.index(
                base.game_config.resolution)  # noqa: F821
        else:
            res_ind = len(RESOLUTIONS)
            RESOLUTIONS.append(base.game_config.resolution)  # noqa: F821

        res_chooser = ListChooser()
        res_chooser.prepare(self._main_fr, (0.1, 0, 0.51), RESOLUTIONS,
                            res_ind)
        self.conf_wids.append(res_chooser)

        self.conf_wids.append(
            DirectLabel(  # Tutorial:
                parent=self._main_fr,
                text=base.labels.MAIN_MENU[23],  # noqa: F821,
                text_fg=RUST_COL,
                text_scale=0.04,
                text_font=base.main_font,  # noqa: F821
                pos=(-0.3, 0, 0.4),
                frameColor=(0, 0, 0, 0),
            ))
        tutorial_check = DirectCheckButton(
            parent=self._main_fr,
            indicatorValue=base.game_config.tutorial_enabled,  # noqa: F821
            clickSound=self.click_snd,
            scale=0.02,
            pos=(0.12, 0, 0.41),
            boxBorder=0,
            boxImage=(GUI_PIC + "no_check.png", GUI_PIC + "check.png", None),
            boxRelief=None,
            relief="flat",
            frameColor=RUST_COL,
        )
        tutorial_check.setTransparency(TransparencyAttrib.MAlpha)
        self.conf_wids.append(tutorial_check)

        self.conf_wids.append(
            DirectLabel(  # Language
                parent=self._main_fr,
                text=base.labels.MAIN_MENU[24],  # noqa: F821,
                text_fg=RUST_COL,
                text_scale=0.04,
                text_font=base.main_font,  # noqa: F821
                pos=(-0.3, 0, 0.3),
                frameColor=(0, 0, 0, 0),
            ))

        lang_chooser = ListChooser()
        lang_chooser.prepare(
            self._main_fr,
            (0.1, 0, 0.31),
            LANGUAGES,
            LANGUAGES.index(base.game_config.language),  # noqa: F821,
        )
        self.conf_wids.append(lang_chooser)

        self.conf_wids.append(
            DirectLabel(  # Framerate limit
                parent=self._main_fr,
                text=base.labels.MAIN_MENU[29],  # noqa: F821,
                text_fg=RUST_COL,
                text_scale=0.04,
                text_font=base.main_font,  # noqa: F821
                pos=(-0.3, 0, 0.21),
                frameColor=(0, 0, 0, 0),
            ))

        fps_chooser = ListChooser()
        fps_chooser.prepare(
            self._main_fr,
            (0.1, 0, 0.22),
            FPS,
            FPS.index(str(base.game_config.fps_limit)),  # noqa: F821
        )
        self.conf_wids.append(fps_chooser)

        self.conf_wids.append(
            DirectLabel(  # FPS meter:
                parent=self._main_fr,
                text=base.labels.MAIN_MENU[36],  # noqa: F821,
                text_fg=RUST_COL,
                text_scale=0.04,
                text_font=base.main_font,  # noqa: F821
                pos=(-0.3, 0, 0.11),
                frameColor=(0, 0, 0, 0),
            ))

        fps_meter = DirectCheckButton(
            parent=self._main_fr,
            indicatorValue=base.game_config.fps_meter,  # noqa: F821
            clickSound=self.click_snd,
            scale=0.02,
            pos=(0.12, 0, 0.12),
            boxBorder=0,
            boxImage=(GUI_PIC + "no_check.png", GUI_PIC + "check.png", None),
            boxRelief=None,
            relief="flat",
            frameColor=RUST_COL,
        )
        fps_meter.setTransparency(TransparencyAttrib.MAlpha)
        self.conf_wids.append(fps_meter)

        self.conf_wids.append(
            DirectLabel(  # Multi threading:
                parent=self._main_fr,
                text=base.labels.MAIN_MENU[37],  # noqa: F821,
                text_fg=RUST_COL,
                text_scale=0.04,
                text_font=base.main_font,  # noqa: F821
                pos=(-0.3, 0, 0.01),
                frameColor=(0, 0, 0, 0),
            ))

        multi_threading = DirectCheckButton(
            parent=self._main_fr,
            indicatorValue=base.game_config.multi_threading,  # noqa: F821
            clickSound=self.click_snd,
            scale=0.02,
            pos=(0.12, 0, 0.02),
            boxBorder=0,
            boxImage=(GUI_PIC + "no_check.png", GUI_PIC + "check.png", None),
            boxRelief=None,
            relief="flat",
            frameColor=RUST_COL,
        )
        multi_threading.setTransparency(TransparencyAttrib.MAlpha)
        self.conf_wids.append(multi_threading)

        self.conf_wids.append(
            DirectLabel(  # Multi threading:
                parent=self._main_fr,
                text=base.labels.MAIN_MENU[38],  # noqa: F821,
                text_fg=SILVER_COL,
                text_scale=0.025,
                text_font=base.main_font,  # noqa: F821
                pos=(-0.3, 0, -0.03),
                frameColor=(0, 0, 0, 0),
            ))

        but = DirectButton(  # Save and restart
            parent=self._main_fr,
            text_scale=0.045,
            text_fg=RUST_COL,
            text=base.labels.MAIN_MENU[25],  # noqa: F821,
            text_font=base.main_font,  # noqa: F821
            relief=None,
            command=self._save_conf_and_restart,
            extraArgs=[
                res_chooser,
                tutorial_check,
                lang_chooser,
                fps_chooser,
                fps_meter,
                multi_threading,
            ],
            pos=(0.1, 0, -0.2),
            clickSound=self.click_snd,
        )
        self.bind_button(but)
        self.conf_wids.append(but)

    def _show_credits(self):
        """Show the game credits."""
        clear_wids(self.save_wids)
        clear_wids(self.conf_wids)
        clear_wids(self.tactics_wids)

        center = 0.25

        self.cred_wids.append(
            DirectLabel(
                parent=self._main_fr,
                pos=(center, 0, 0.6),
                text_scale=0.04,
                text_fg=SILVER_COL,
                text_font=base.main_font,  # noqa: F821
                frameColor=(0, 0, 0, 0),
                text=base.labels.MAIN_MENU[31],  # noqa: F821
            ))
        self.cred_wids.append(
            DirectLabel(  # Project source code
                parent=self._main_fr,
                pos=(center, 0, 0.5),
                text_scale=0.04,
                text_fg=SILVER_COL,
                text_font=base.main_font,  # noqa: F821
                frameColor=(0, 0, 0, 0),
                text=base.labels.MAIN_MENU[32],  # noqa: F821
            ))
        self.cred_wids.append(
            DirectButton(
                parent=self._main_fr,
                pos=(center, 0, 0.45),
                frameColor=(0, 0, 0, 0),
                text_scale=0.04,
                text_fg=RUST_COL,
                text="github.com/IlyaFaer/ForwardOnlyGame",
                text_font=base.main_font,  # noqa: F821
                relief=None,
                command=webbrowser.open,
                extraArgs=["https://github.com/IlyaFaer/ForwardOnlyGame"],
                clickSound=self.click_snd,
            ))
        self.cred_wids.append(
            DirectLabel(  # Subscribe
                parent=self._main_fr,
                pos=(center, 0, 0.35),
                text_scale=0.04,
                text_fg=SILVER_COL,
                text_font=base.main_font,  # noqa: F821
                frameColor=(0, 0, 0, 0),
                text=base.labels.MAIN_MENU[33],  # noqa: F821
            ))
        self.cred_wids.append(
            DirectButton(
                parent=self._main_fr,
                pos=(0.11, 0, 0.29),
                frameTexture="credits/youtube.png",
                frameSize=(-0.056, 0.056, -0.029, 0.029),
                relief="flat",
                command=webbrowser.open,
                extraArgs=[
                    "https://www.youtube.com/channel/UCKmtk9K6VkcQdOMiE7H-W9w"
                ],
                clickSound=self.click_snd,
            ))
        self.cred_wids.append(
            DirectButton(
                parent=self._main_fr,
                pos=(center, 0, 0.29),
                frameTexture="credits/indie_db.png",
                frameSize=(-0.058, 0.058, -0.029, 0.029),
                relief="flat",
                command=webbrowser.open,
                extraArgs=["https://www.indiedb.com/games/forward-only"],
                clickSound=self.click_snd,
            ))
        but = DirectButton(
            parent=self._main_fr,
            pos=(0.38, 0, 0.29),
            frameTexture="credits/discord.png",
            frameSize=(-0.045, 0.045, -0.045, 0.045),
            relief="flat",
            command=webbrowser.open,
            extraArgs=["https://discord.gg/8UgFJAWsFx"],
            clickSound=self.click_snd,
        )
        but.setTransparency(TransparencyAttrib.MAlpha)
        self.cred_wids.append(but)
        self.cred_wids.append(
            DirectLabel(  # Stack
                parent=self._main_fr,
                pos=(center, 0, 0.18),
                text_scale=0.04,
                text_fg=SILVER_COL,
                text_font=base.main_font,  # noqa: F821
                frameColor=(0, 0, 0, 0),
                text=base.labels.MAIN_MENU[34],  # noqa: F821
            ))
        but = DirectButton(
            parent=self._main_fr,
            pos=(0.05, 0, 0.11),
            frameTexture="credits/python.png",
            frameSize=(-0.05, 0.05, -0.05, 0.05),
            relief="flat",
            command=webbrowser.open,
            extraArgs=["https://www.python.org/"],
            clickSound=self.click_snd,
        )
        but.setTransparency(TransparencyAttrib.MAlpha)
        self.cred_wids.append(but)

        but = DirectButton(
            parent=self._main_fr,
            pos=(0.185, 0, 0.11),
            frameTexture="credits/panda3d.png",
            frameSize=(-0.05, 0.05, -0.05, 0.05),
            relief="flat",
            command=webbrowser.open,
            extraArgs=["https://www.panda3d.org/"],
            clickSound=self.click_snd,
        )
        but.setTransparency(TransparencyAttrib.MAlpha)
        self.cred_wids.append(but)

        but = DirectButton(
            parent=self._main_fr,
            pos=(0.315, 0, 0.11),
            frameTexture="credits/blender.png",
            frameSize=(-0.05, 0.05, -0.05, 0.05),
            relief="flat",
            command=webbrowser.open,
            extraArgs=["https://www.blender.org/"],
            clickSound=self.click_snd,
        )
        but.setTransparency(TransparencyAttrib.MAlpha)
        self.cred_wids.append(but)

        but = DirectButton(
            parent=self._main_fr,
            pos=(0.45, 0, 0.11),
            frameTexture="credits/make_human.png",
            frameSize=(-0.05, 0.05, -0.05, 0.05),
            relief="flat",
            command=webbrowser.open,
            extraArgs=["http://www.makehumancommunity.org/"],
            clickSound=self.click_snd,
        )
        but.setTransparency(TransparencyAttrib.MAlpha)
        self.cred_wids.append(but)

        self.cred_wids.append(
            DirectLabel(  # Tools
                parent=self._main_fr,
                pos=(center, 0, -0.02),
                text_scale=0.04,
                text_fg=SILVER_COL,
                text_font=base.main_font,  # noqa: F821
                frameColor=(0, 0, 0, 0),
                text=base.labels.MAIN_MENU[35],  # noqa: F821
            ))
        but = DirectButton(
            parent=self._main_fr,
            pos=(center - 0.12, 0, -0.09),
            frameTexture="credits/free_sound.png",
            frameSize=(-0.057, 0.057, -0.029, 0.029),
            relief="flat",
            command=webbrowser.open,
            extraArgs=["https://freesound.org/"],
            clickSound=self.click_snd,
        )
        but.setTransparency(TransparencyAttrib.MAlpha)
        self.cred_wids.append(but)

        but = DirectButton(
            parent=self._main_fr,
            pos=(center, 0, -0.09),
            frameTexture="credits/photopea.png",
            frameSize=(-0.03, 0.03, -0.03, 0.03),
            relief="flat",
            command=webbrowser.open,
            extraArgs=["https://www.photopea.com/"],
            clickSound=self.click_snd,
        )
        but.setTransparency(TransparencyAttrib.MAlpha)
        self.cred_wids.append(but)

        but = DirectButton(
            parent=self._main_fr,
            pos=(center + 0.09, 0, -0.09),
            frameTexture="credits/online_convert.png",
            frameSize=(-0.03, 0.03, -0.03, 0.03),
            relief="flat",
            command=webbrowser.open,
            extraArgs=["https://audio.online-convert.com/"],
            clickSound=self.click_snd,
        )
        but.setTransparency(TransparencyAttrib.MAlpha)
        self.cred_wids.append(but)

        self.cred_wids.append(
            DirectLabel(  # Music
                parent=self._main_fr,
                pos=(center, 0, -0.24),
                text_scale=0.042,
                text_fg=SILVER_COL,
                text_font=base.main_font,  # noqa: F821
                frameColor=(0, 0, 0, 0),
                text=base.labels.MAIN_MENU[39],  # noqa: F821
            ))
        but = DirectButton(
            parent=self._main_fr,
            pos=(-0.15, 0, -0.45),
            frameTexture="credits/among_madness_logo.png",
            frameSize=(-0.15, 0.15, -0.15, 0.15),
            relief="flat",
            command=webbrowser.open,
            extraArgs=[
                "https://open.spotify.com/artist/3uy4tvaLvBAsKdV52Kc2TI"
            ],
            clickSound=self.click_snd,
        )
        but.setTransparency(TransparencyAttrib.MAlpha)
        self.cred_wids.append(but)

        self.cred_wids.append(
            DirectLabel(
                parent=self._main_fr,
                pos=(-0.15, 0, -0.65),
                text_scale=0.033,
                text_fg=SILVER_COL,
                text_font=base.main_font,  # noqa: F821
                frameColor=(0, 0, 0, 0),
                text="Among Madness",
            ))

        but = DirectButton(
            parent=self._main_fr,
            pos=(0.25, 0, -0.45),
            frameTexture="credits/qualia_logo.png",
            frameSize=(-0.15, 0.15, -0.15, 0.15),
            relief="flat",
            command=webbrowser.open,
            extraArgs=["https://kvalia.net/"],
            clickSound=self.click_snd,
        )
        but.setTransparency(TransparencyAttrib.MAlpha)
        self.cred_wids.append(but)

        self.cred_wids.append(
            DirectLabel(
                parent=self._main_fr,
                pos=(0.25, 0, -0.65),
                text_scale=0.033,
                text_fg=SILVER_COL,
                text_font=base.main_font,  # noqa: F821
                frameColor=(0, 0, 0, 0),
                text="Квалиа",
            ))

        but = DirectButton(
            parent=self._main_fr,
            pos=(0.65, 0, -0.45),
            frameTexture="credits/moloken_logo.png",
            frameSize=(-0.15, 0.15, -0.15, 0.15),
            relief="flat",
            command=webbrowser.open,
            extraArgs=[
                "https://open.spotify.com/artist/3LZzdqKCEcBwhh6vd6y6Q5"
            ],
            clickSound=self.click_snd,
        )
        but.setTransparency(TransparencyAttrib.MAlpha)
        self.cred_wids.append(but)

        self.cred_wids.append(
            DirectLabel(
                parent=self._main_fr,
                pos=(0.65, 0, -0.65),
                text_scale=0.033,
                text_fg=SILVER_COL,
                text_font=base.main_font,  # noqa: F821
                frameColor=(0, 0, 0, 0),
                text="Moloken",
            ))

    def _show_crew(self, crew):
        """Show the description of the chosen tactics.

        Args:
            crew (str): The chosen tactics name.
        """
        self._chosen_crew = crew
        self._crew_preview["frameTexture"] = "gui/tex/preview/{}.png".format(
            crew)

        for key, but in self._crew_buts.items():
            but["text_fg"] = SILVER_COL if key == crew else RUST_COL

        descs = {
            "soldiers": base.labels.MAIN_MENU[11],  # noqa: F821
            "raiders": base.labels.MAIN_MENU[12],  # noqa: F821
            "anarchists": base.labels.MAIN_MENU[13],  # noqa: F821
        }
        self._team_description["text"] = descs[crew]

    def _show_slots(self, for_loading=False):
        """Show save slots GUI.

        Args:
            for_loading (bool):
                Optional. True if slots must be shown for loading.
        """
        if self.save_wids:
            return

        clear_wids(self.tactics_wids)
        clear_wids(self.conf_wids)
        clear_wids(self.cred_wids)

        if for_loading:
            is_active = True
            command = self._load_game
        else:
            is_active = not (base.train.ctrl.critical_damage  # noqa: F821
                             or base.world.is_in_city  # noqa: F821
                             or base.world.is_on_et  # noqa: F821
                             or base.current_block.id < 4  # noqa: F821
                             or base.world.is_near_fork  # noqa: F821
                             )
            command = base.save_game  # noqa: F821

        saves = self._read_saves()

        z_shift = 0.5
        x_shift = -0.2
        for num in range(1, 4):
            but = DirectButton(
                parent=self._main_fr,
                text="Slot {}".format(str(num)),
                pos=(x_shift - 0.103, 0, z_shift),
                text_scale=0.04,
                text_fg=RUST_COL,
                relief=None,
                command=command if is_active else None,
                extraArgs=[num],
                clickSound=self.click_snd,
            )
            self.bind_button(but)
            self.save_wids.append(but)

            self.save_wids.append(
                DirectLabel(
                    parent=self._main_fr,
                    pos=(x_shift, 0, z_shift - 0.05),
                    text_scale=0.03,
                    text_fg=SILVER_COL,
                    frameColor=(0, 0, 0, 0),
                    text=saves[num - 1].get("save_time", "-"),
                ))
            self.save_wids.append(
                DirectLabel(
                    parent=self._main_fr,
                    pos=(x_shift - 0.1, 0, z_shift - 0.1),
                    text_scale=0.03,
                    text_fg=SILVER_COL,
                    frameColor=(0, 0, 0, 0),
                    text=(str(saves[num - 1].get("miles") or "- ")) + " mi",
                ))
            self.save_wids.append(
                DirectLabel(
                    parent=self._main_fr,
                    pos=(x_shift + 0.35, 0, z_shift - 0.1),
                    text_scale=0.03,
                    text_fg=SILVER_COL,
                    frameColor=(0, 0, 0, 0),
                    text="{0} soldiers, {1} raiders, {2} anarchists".format(
                        *saves[num - 1].get("chars", ("-", "-", "-"))),
                ))
            z_shift -= 0.2

    def _start_new_game(self):
        """Start a new game."""
        taskMgr.doMethodLater(  # noqa: F821
            4, self._clear_temp_wids, "clear_main_menu_temp_wids")
        self.show_loading(is_game_start=True)
        taskMgr.doMethodLater(  # noqa: F821
            0.25,
            base.start_new_game,  # noqa: F821
            "start_new_game",
            extraArgs=[self._chosen_crew],
        )

    def bind_button(self, button):
        """Bind the given button to visual effects.

        Args:
            button (panda3d.gui.DirectGui.DirectButton): Button to bind.
        """
        button.bind(DGG.ENTER, self._highlight_but, extraArgs=[button])
        button.bind(DGG.EXIT, self._dehighlight_but, extraArgs=[button])

    def hide(self):
        """Hide the main menu."""
        self._main_fr.hide()
        clear_wids(self.save_wids)
        clear_wids(self.conf_wids)
        clear_wids(self.cred_wids)

        if self._load_screen is not None:
            self._load_screen.destroy()
            self._load_screen = None

        base.accept("escape", self.show)  # noqa: F821
        taskMgr.remove("check_can_save")  # noqa: F821

    def hide_loading_msg(self):
        """Hide the "Loading..." message widget."""
        self._load_msg.destroy()

    def show_credits(self, task):
        """Show the game end credits."""

        mus = loader.loadSfx("sounds/music/Moloken - 11''12.mp3")  # noqa: F821
        mus.setVolume(0.2)
        mus.play()

        goodness = 0

        inserts = {}
        for key, value in base.decisions.items():  # noqa: F821
            inserts[key] = value["decision"]
            goodness += value["goodness"]

        if goodness <= 15:
            inserts["leader_desc"] = base.labels.ROUGH_LEADER  # noqa: F821
        elif goodness <= 30:
            inserts[
                "leader_desc"] = base.labels.OPPORTUNIST_LEADER  # noqa: F821
        else:
            inserts["leader_desc"] = base.labels.EMPATHIC_LEADER  # noqa: F821

        frame = DirectFrame(
            frameSize=(-2, 2, -1, 1),
            frameColor=(0.15, 0.15, 0.15, 1),
            state=DGG.NORMAL,
        )
        lab = DirectLabel(
            parent=frame,
            text=base.labels.CREDITS.format(**inserts),  # noqa: F821
            text_font=base.main_font,  # noqa: F821
            text_scale=0.042,
            text_fg=SILVER_COL,
            frameColor=(0, 0, 0, 0),
            text_align=TextNode.ACenter,
            pos=(0, 0, -1.5),
        )
        LerpPosInterval(lab, 100, (0, 0, 10)).start()
        return task.done

    def show_loading(self, is_game_start=False):
        """Show game loading screen.

        Args:
            is_game_start (bool):
                Flag, indicating if the player just started a new game.
        """
        if is_game_start:
            self.hide()
            self._load_screen = DirectFrame(
                frameSize=(-2, 2, -1, 1),
                frameColor=(0.15, 0.15, 0.15, 1),
                state=DGG.NORMAL,
            )
            DirectLabel(
                parent=self._load_screen,
                text=base.labels.SCENARIO_LABELS[2],  # noqa: F821
                text_font=base.main_font,  # noqa: F821
                text_scale=0.056,
                text_fg=RUST_COL,
                frameColor=(0, 0, 0, 0),
                text_align=TextNode.ALeft,
                pos=(-1, 0, 0.5),
            )
            DirectLabel(
                parent=self._load_screen,
                text=base.labels.PREAMBULA,  # noqa: F821
                text_font=base.main_font,  # noqa: F821
                text_scale=0.033,
                text_fg=SILVER_COL,
                frameColor=(0, 0, 0, 0),
                text_align=TextNode.ALeft,
                pos=(-1, 0, 0.4),
            )

        self._load_msg = DirectLabel(  # Loading...
            parent=self._load_screen or self._main_fr,
            text=base.labels.MAIN_MENU[26],  # noqa: F821,
            text_font=base.main_font,  # noqa: F821
            text_fg=RUST_COL,
            frameSize=(1, 1, 1, 1),
            text_scale=0.04,
            pos=(0, 0, -0.75),
        )

    def show(self, is_game_over=False):
        """Show the main menu.

        Args:
            is_game_over (bool):
                True, if the main menu is shown on game over.
        """
        if not getattr(base, "traits_gui", None):  # noqa: F821
            return

        if base.world.rails_scheme.is_shown:  # noqa: F821
            base.world.rails_scheme.show()  # noqa: F821

        if base.traits_gui.is_shown:  # noqa: F821
            base.traits_gui.hide()  # noqa: F821

        self._main_fr.show()

        if is_game_over:
            self._new_game_but["command"] = None
            self._new_game_but["text_fg"] = SILVER_COL
            DirectLabel(
                parent=self._main_fr,
                pos=(0.85, 0, 0.3),
                frameColor=(0, 0, 0, 0),
                text_scale=0.055,
                text_fg=SILVER_COL,
                text_font=base.main_font,  # noqa: F821
                text=base.labels.MAIN_MENU[28],  # noqa: F821
            )
            base.ignore("escape")  # noqa: F821
        else:
            base.accept("escape", self.hide)  # noqa: F821

        taskMgr.doMethodLater(  # noqa: F821
            0.25, self._check_can_save, "check_can_save")
        if not self._is_first_pause:
            return

        # it is the first pause
        self._main_fr["frameColor"] = (0, 0, 0, 0.6)

        self._new_game_but["text"] = base.labels.MAIN_MENU[21]  # noqa: F821
        if not is_game_over:
            self._new_game_but["command"] = self.hide

        self._load_but.destroy()

        can_save = not (base.train.ctrl.critical_damage  # noqa: F821
                        or base.world.is_in_city  # noqa: F821
                        or base.world.is_on_et  # noqa: F821
                        or base.current_block.id < 4  # noqa: F821
                        or base.world.is_near_fork  # noqa: F821
                        )
        self._save_but = DirectButton(
            parent=self._main_fr,
            pos=(-1.12, 0, 0.3),
            text_scale=0.05,
            text_fg=RUST_COL if can_save else SILVER_COL,
            text=base.labels.MAIN_MENU[15],  # noqa: F821
            text_font=base.main_font,  # noqa: F821
            relief=None,
            text_align=TextNode.ALeft,
            command=self._show_slots if can_save else None,  # noqa: F821
            clickSound=self.click_snd,
        )
        self.bind_button(self._save_but)

        self.bind_button(
            DirectButton(  # Main menu
                parent=self._main_fr,
                pos=(-1.12, 0, 0),
                text_scale=0.05,
                text_fg=RUST_COL,
                text=base.labels.MAIN_MENU[14],  # noqa: F821
                text_font=base.main_font,  # noqa: F821
                relief=None,
                command=base.restart_game,  # noqa: F821
                text_align=TextNode.ALeft,
                clickSound=self.click_snd,
            ))
        self._is_first_pause = False

    def show_start_button(self):
        """Show a button to start a game on the loading screen."""
        parent = self._load_msg.getParent()
        self._load_msg.destroy()

        self._load_msg = DirectButton(
            parent=parent,
            text=base.labels.MAIN_MENU[27],  # noqa: F821,
            text_font=base.main_font,  # noqa: F821
            text_fg=RUST_COL,
            text_scale=0.04,
            pos=(0, 0, -0.75),
            relief=None,
            clickSound=self.click_snd,
            command=base.start_game,  # noqa: F821
        )
        self.bind_button(self._load_msg)

    def stop_music(self):
        """Stop the main menu music."""
        taskMgr.doMethodLater(  # noqa: F821
            0.3,
            drown_snd,
            "drown_menu_music",
            extraArgs=[self._menu_music],
            appendTask=True,
        )
        taskMgr.doMethodLater(  # noqa: F821
            3,
            loader.unloadSfx,  # noqa: F821
            "unload_menu_music",
            extraArgs=[self._menu_music],
            appendTask=False,
        )
Ejemplo n.º 2
0
    def __init__(self,
                 items,
                 parent=None,
                 sidePad=.0,
                 edgePos=PTop,
                 align=ALeft,
                 effect=ENone,
                 buttonThrower=None,
                 font=None,
                 baselineOffset=.0,
                 scale=.05,
                 itemHeight=1.,
                 leftPad=.0,
                 separatorHeight=.5,
                 underscoreThickness=1,
                 BGColor=(0, 0, 0, .7),
                 BGBorderColor=(1, .85, .4, 1),
                 separatorColor=(1, 1, 1, 1),
                 frameColorHover=(1, .85, .4, 1),
                 frameColorPress=(0, 1, 0, 1),
                 textColorReady=(1, 1, 1, 1),
                 textColorHover=(0, 0, 0, 1),
                 textColorPress=(0, 0, 0, 1),
                 textColorDisabled=(.5, .5, .5, 1),
                 draggable=False,
                 onMove=None):
        '''
      sidePad : additional space on the left and right of the text item
      edgePos : menu bar position on the screen,
                  use DropDownMenu.PLeft, PRight, PBottom, or PTop
      align   : menu items alignment on menu bar,
                  use DropDownMenu.ALeft, ACenter, or ARight
      effect  : the drop down appearance effect,
                  use DropDownMenu.ENone, EFade, ESlide, or EStretch
      draggable : menu bar's draggability status
      onMove : a function which will be called after changing edge position

      Read the remaining options documentation in PopupMenu class.
      '''
        self.parent = parent if parent else getattr(
            base, DropDownMenu.parents[edgePos][align])
        self.BT = buttonThrower if buttonThrower else base.buttonThrowers[
            0].node()
        self.menu = self.parent.attachNewNode('dropdownmenu-%s' % id(self))
        self.font = font if font else TextNode.getDefaultFont()
        self.baselineOffset = baselineOffset
        self.scale = scale
        self.itemHeight = itemHeight
        self.sidePad = sidePad
        self.edgePos = edgePos
        self.alignment = align
        self.effect = effect
        self.leftPad = leftPad
        self.underscoreThickness = underscoreThickness
        self.separatorHeight = separatorHeight
        self.BGColor = BGColor
        self.BGBorderColor = BGBorderColor
        self.separatorColor = separatorColor
        self.frameColorHover = frameColorHover
        self.frameColorPress = frameColorPress
        self.textColorReady = textColorReady
        self.textColorHover = textColorHover
        self.textColorPress = textColorPress
        self.textColorDisabled = textColorDisabled
        self.draggable = draggable
        self.onMove = onMove
        self.dropDownMenu = self.whoseDropDownMenu = None

        self.gapFromEdge = gapFromEdge = .008
        texMargin = self.font.getTextureMargin() * self.scale * .25
        b = DirectButton(parent=NodePath(''),
                         text='^|g_',
                         text_font=self.font,
                         scale=self.scale)
        fr = b.node().getFrame()
        b.getParent().removeNode()
        baselineToCenter = (fr[2] + fr[3]) * self.scale
        LH = (fr[3] - fr[2]) * self.itemHeight * self.scale
        baselineToTop = (fr[3] * self.itemHeight * self.scale /
                         LH) / (1. + self.baselineOffset)
        baselineToBot = LH / self.scale - baselineToTop
        self.height = LH + .01
        l, r, b, t = 0, 5, -self.height, 0
        self.menuBG = DirectFrame(parent=self.menu,
                                  frameColor=BGColor,
                                  frameSize=(l, r, b, t),
                                  state=DGG.NORMAL,
                                  suppressMouse=1)
        if self.draggable:
            self.setDraggable(1)
        LSborder = LineSegs()
        LSborder.setThickness(2)
        LSborder.setColor(0, 0, 0, 1)
        LSborder.moveTo(l, 0, b)
        LSborder.drawTo(r, 0, b)
        self.menuBG.attachNewNode(LSborder.create())
        self.itemsParent = self.menu.attachNewNode('menu items parent')

        x = sidePad * self.scale + gapFromEdge
        for t, menuItemsGenerator in items:
            underlinePos = t.find('_')
            t = t.replace('_', '')
            b = DirectButton(
                parent=self.itemsParent,
                text=t,
                text_font=self.font,
                pad=(sidePad, 0),
                scale=self.scale,
                pos=(x, 0, -baselineToTop * self.scale - gapFromEdge),
                text_fg=textColorReady,
                # text color when mouse over
                text2_fg=textColorHover,
                # text color when pressed
                text1_fg=textColorPress,
                # framecolor when pressed
                frameColor=frameColorPress,
                command=self.__createMenu,
                extraArgs=[True, menuItemsGenerator],
                text_align=TextNode.ALeft,
                relief=DGG.FLAT,
                rolloverSound=0,
                clickSound=0)
            b['extraArgs'] += [b.getName()]
            b.stateNodePath[2].setColor(
                *frameColorHover)  # framecolor when mouse over
            b.stateNodePath[0].setColor(0, 0, 0, 0)  # framecolor when ready
            fr = b.node().getFrame()
            b['frameSize'] = (fr[0], fr[1], -baselineToBot, baselineToTop)
            self.accept(DGG.ENTER + b.guiId, self.__createMenu,
                        [False, menuItemsGenerator,
                         b.getName()])
            if underlinePos > -1:
                tn = TextNode('')
                tn.setFont(self.font)
                tn.setText(t[:underlinePos + 1])
                tnp = NodePath(tn.getInternalGeom())
                underlineXend = tnp.getTightBounds()[1][0]
                tnp.removeNode()
                tn.setText(t[underlinePos])
                tnp = NodePath(tn.getInternalGeom())
                b3 = tnp.getTightBounds()
                underlineXstart = underlineXend - (b3[1] - b3[0])[0]
                tnp.removeNode()
                LSunder = LineSegs()
                LSunder.setThickness(underscoreThickness)
                LSunder.moveTo(underlineXstart + texMargin, 0,
                               -.7 * baselineToBot)
                LSunder.drawTo(underlineXend - texMargin, 0,
                               -.7 * baselineToBot)
                underline = b.stateNodePath[0].attachNewNode(LSunder.create())
                underline.setColor(Vec4(*textColorReady), 1)
                underline.copyTo(b.stateNodePath[1],
                                 10).setColor(Vec4(*textColorPress), 1)
                underline.copyTo(b.stateNodePath[2],
                                 10).setColor(Vec4(*textColorHover), 1)
                self.accept('alt-' + t[underlinePos].lower(),
                            self.__createMenu,
                            [True, menuItemsGenerator,
                             b.getName()])
            x += (fr[1] - fr[0]) * self.scale
        self.width = x - 2 * gapFromEdge
        self.align(align)
        self.setEdgePos(edgePos)
        self.minZ = base.a2dBottom + self.height if edgePos == DropDownMenu.PBottom else None
        viewPlaneNode = PlaneNode('cut menu')
        viewPlaneNode.setPlane(Plane(Vec3(0, 0, -1), Point3(0, 0, -LH)))
        self.clipPlane = self.menuBG.attachNewNode(viewPlaneNode)
Ejemplo n.º 3
0
class ScrolledButtonsList(DirectObject):
    """
       A class to display a list of selectable buttons.
       It is displayed using scrollable window (DirectScrolledFrame).
    """
    def __init__(self, parent=None, frameSize=(.8,1.2), buttonTextColor=(1,1,1,1),
                 font=None, itemScale=.045, itemTextScale=0.85, itemTextZ=0,
                 command=None, contextMenu=None, autoFocus=0,
                 colorChange=1, colorChangeDuration=1, newItemColor=globals.colors['guiblue1'],
                 rolloverColor=globals.colors['guiyellow'],
                 suppressMouseWheel=1, modifier='control'):
        self.mode = None
        self.focusButton=None
        self.command=command
        self.contextMenu=contextMenu
        self.autoFocus=autoFocus
        self.colorChange=colorChange
        self.colorChangeDuration=colorChangeDuration*.5
        self.newItemColor=newItemColor
        self.rolloverColor=rolloverColor
        self.rightClickTextColors=(Vec4(0,1,0,1),Vec4(0,35,100,1))
        self.font=font
        if font:
           self.fontHeight=font.getLineHeight()
        else:
           self.fontHeight=TextNode.getDefaultFont().getLineHeight()
        self.fontHeight*=1.2 # let's enlarge font height a little
        self.xtraSideSpace=.2*self.fontHeight
        self.itemTextScale=itemTextScale
        self.itemTextZ=itemTextZ
        self.buttonTextColor=buttonTextColor
        self.suppressMouseWheel=suppressMouseWheel
        self.modifier=modifier
        self.buttonsList=[]
        self.numItems=0
        self.__eventReceivers={}
        # DirectScrolledFrame to hold items
        self.itemScale=itemScale
        self.itemVertSpacing=self.fontHeight*self.itemScale
        self.frameWidth,self.frameHeight=frameSize
        # I set canvas' Z size smaller than the frame to avoid the auto-generated vertical slider bar
        self.childrenFrame = DirectScrolledFrame(
                     parent=parent,pos=(-self.frameWidth*.5,0,.5*self.frameHeight), relief=DGG.GROOVE,
                     state=DGG.NORMAL, # to create a mouse watcher region
                     frameSize=(0, self.frameWidth, -self.frameHeight, 0), frameColor=(0,0,0,.7),
                     canvasSize=(0, 0, -self.frameHeight*.5, 0), borderWidth=(0.01,0.01),
                     manageScrollBars=0, enableEdit=0, suppressMouse=0, sortOrder=1000 )
        # the real canvas is "self.childrenFrame.getCanvas()",
        # but if the frame is hidden since the beginning,
        # no matter how I set the canvas Z pos, the transform would be resistant,
        # so just create a new node under the canvas to be my canvas
        self.canvas=self.childrenFrame.getCanvas().attachNewNode('myCanvas')
        # slider background
        SliderBG=DirectFrame( parent=self.childrenFrame,frameSize=(-.025,.025,-self.frameHeight,0),
                     frameColor=(0,0,0,.7), pos=(-.03,0,0),enableEdit=0, suppressMouse=0)
        # slider thumb track
        sliderTrack = DirectFrame( parent=SliderBG, relief=DGG.FLAT, #state=DGG.NORMAL,
                     frameColor=(1,1,1,.2), frameSize=(-.015,.015,-self.frameHeight+.01,-.01),
                     enableEdit=0, suppressMouse=0)
        # page up
        self.pageUpRegion=DirectFrame( parent=SliderBG, relief=DGG.FLAT, state=DGG.NORMAL,
                     frameColor=(1,.8,.2,.1), frameSize=(-.015,.015,0,0),
                     enableEdit=0, suppressMouse=0)
        self.pageUpRegion.setAlphaScale(0)
        self.pageUpRegion.bind(DGG.B1PRESS,self.__startScrollPage,[-1])
        self.pageUpRegion.bind(DGG.WITHIN,self.__continueScrollUp)
        self.pageUpRegion.bind(DGG.WITHOUT,self.__suspendScrollUp)
        # page down
        self.pageDnRegion=DirectFrame( parent=SliderBG, relief=DGG.FLAT, state=DGG.NORMAL,
                     frameColor=(1,.8,.2,.1), frameSize=(-.015,.015,0,0),
                     enableEdit=0, suppressMouse=0)
        self.pageDnRegion.setAlphaScale(0)
        self.pageDnRegion.bind(DGG.B1PRESS,self.__startScrollPage,[1])
        self.pageDnRegion.bind(DGG.WITHIN,self.__continueScrollDn)
        self.pageDnRegion.bind(DGG.WITHOUT,self.__suspendScrollDn)
        self.pageUpDnSuspended=[0,0]
        # slider thumb
        self.vertSliderThumb=DirectButton(parent=SliderBG, relief=DGG.FLAT,
                     frameColor=(1,1,1,.6), frameSize=(-.015,.015,0,0),
                     enableEdit=0, suppressMouse=0, rolloverSound=None, clickSound=None)
        self.vertSliderThumb.bind(DGG.B1PRESS,self.__startdragSliderThumb)
        self.vertSliderThumb.bind(DGG.WITHIN,self.__enteringThumb)
        self.vertSliderThumb.bind(DGG.WITHOUT,self.__exitingThumb)
        self.oldPrefix=base.buttonThrowers[0].node().getPrefix()
        self.sliderThumbDragPrefix='draggingSliderThumb-'
        # GOD & I DAMN IT !!!
        # These things below don't work well if the canvas has a lot of buttons.
        # So I end up checking the mouse region every frame by myself using a continuous task.
  #       self.accept(DGG.WITHIN+self.childrenFrame.guiId,self.__enteringFrame)
  #       self.accept(DGG.WITHOUT+self.childrenFrame.guiId,self.__exitingFrame)
        self.isMouseInRegion=False
        self.mouseOutInRegionCommand=(self.__exitingFrame,self.__enteringFrame)
        taskMgr.doMethodLater(.2,self.__getFrameRegion,'getFrameRegion')
  
    def __getFrameRegion(self,t):
        for g in range(base.mouseWatcherNode.getNumGroups()):
            region=base.mouseWatcherNode.getGroup(g).findRegion(self.childrenFrame.guiId)
            if region!=None:
               self.frameRegion=region
               taskMgr.add(self.__mouseInRegionCheck,'mouseInRegionCheck')
               break
  
    def __mouseInRegionCheck(self,t):
        """
           check if the mouse is within or without the scrollable frame, and
           upon within or without, run the provided command
        """
        if not base.mouseWatcherNode.hasMouse(): return Task.cont
        m=base.mouseWatcherNode.getMouse()
        bounds=self.frameRegion.getFrame()
        inRegion=bounds[0]<m[0]<bounds[1] and bounds[2]<m[1]<bounds[3]
        if self.isMouseInRegion==inRegion: return Task.cont
        self.isMouseInRegion=inRegion
        self.mouseOutInRegionCommand[inRegion]()
        return Task.cont
  
    def __startdragSliderThumb(self,m=None):
        if self.mode != None:
            if hasattr(self.mode, 'enableMouseCamControl') == 1:
                if self.mode.enableMouseCamControl == 1:
                    self.mode.game.app.disableMouseCamControl()
        mpos=base.mouseWatcherNode.getMouse()
        parentZ=self.vertSliderThumb.getParent().getZ(render2d)
        sliderDragTask=taskMgr.add(self.__dragSliderThumb,'dragSliderThumb')
        sliderDragTask.ZposNoffset=mpos[1]-self.vertSliderThumb.getZ(render2d)+parentZ
  #       sliderDragTask.mouseX=base.winList[0].getPointer(0).getX()
        self.oldPrefix=base.buttonThrowers[0].node().getPrefix()
        base.buttonThrowers[0].node().setPrefix(self.sliderThumbDragPrefix)
        self.acceptOnce(self.sliderThumbDragPrefix+'mouse1-up',self.__stopdragSliderThumb)
  
    def __dragSliderThumb(self,t):
        if not base.mouseWatcherNode.hasMouse():
           return
        mpos=base.mouseWatcherNode.getMouse()
  #       newY=base.winList[0].getPointer(0).getY()
        self.__updateCanvasZpos((t.ZposNoffset-mpos[1])/self.canvasRatio)
  #       base.winList[0].movePointer(0, t.mouseX, newY)
        return Task.cont
  
    def __stopdragSliderThumb(self,m=None):
        if self.mode != None:
            if hasattr(self.mode, 'enableMouseCamControl') == 1:
                if self.mode.enableMouseCamControl == 1:
                    self.mode.game.app.enableMouseCamControl()
        taskMgr.remove('dragSliderThumb')
        self.__stopScrollPage()
        base.buttonThrowers[0].node().setPrefix(self.oldPrefix)
        if self.isMouseInRegion:
           self.mouseOutInRegionCommand[self.isMouseInRegion]()
  
    def __startScrollPage(self,dir,m):
        self.oldPrefix=base.buttonThrowers[0].node().getPrefix()
        base.buttonThrowers[0].node().setPrefix(self.sliderThumbDragPrefix)
        self.acceptOnce(self.sliderThumbDragPrefix+'mouse1-up',self.__stopdragSliderThumb)
        t=taskMgr.add(self.__scrollPage,'scrollPage',extraArgs=[int((dir+1)*.5),dir*.01/self.canvasRatio])
        self.pageUpDnSuspended=[0,0]
  
    def __scrollPage(self,dir,scroll):
        if not self.pageUpDnSuspended[dir]:
           self.__scrollCanvas(scroll)
        return Task.cont
  
    def __stopScrollPage(self,m=None):
        taskMgr.remove('scrollPage')
  
    def __suspendScrollUp(self,m=None):
        self.pageUpRegion.setAlphaScale(0)
        self.pageUpDnSuspended[0]=1
    def __continueScrollUp(self,m=None):
        if taskMgr.hasTaskNamed('dragSliderThumb'):
           return
        self.pageUpRegion.setAlphaScale(1)
        self.pageUpDnSuspended[0]=0
   
    def __suspendScrollDn(self,m=None):
        self.pageDnRegion.setAlphaScale(0)
        self.pageUpDnSuspended[1]=1
    def __continueScrollDn(self,m=None):
        if taskMgr.hasTaskNamed('dragSliderThumb'):
           return
        self.pageDnRegion.setAlphaScale(1)
        self.pageUpDnSuspended[1]=0
  
    def __suspendScrollPage(self,m=None):
        self.__suspendScrollUp()
        self.__suspendScrollDn()
   
    def __enteringThumb(self,m=None):
        self.vertSliderThumb['frameColor']=(1,1,1,1)
        self.__suspendScrollPage()
  
    def __exitingThumb(self,m=None):
        self.vertSliderThumb['frameColor']=(1,1,1,.6)
  
    def __scrollCanvas(self,scroll):
        if self.vertSliderThumb.isHidden() or self.buttonsList == []:
           return
        self.__updateCanvasZpos(self.canvas.getZ()+scroll)
  
    def __updateCanvasZpos(self,Zpos):
        newZ=clampScalar(Zpos, .0, self.canvasLen-self.frameHeight+.015)
        self.canvas.setZ(newZ)
        thumbZ=-newZ*self.canvasRatio
        self.vertSliderThumb.setZ(thumbZ)
        self.pageUpRegion['frameSize']=(-.015,.015,thumbZ-.01,-.01)
        self.pageDnRegion['frameSize']=(-.015,.015,-self.frameHeight+.01,thumbZ+self.vertSliderThumb['frameSize'][2])
  
    def __adjustCanvasLength(self,numItem):
        self.canvasLen=float(numItem)*self.itemVertSpacing
        self.canvasRatio=(self.frameHeight-.015)/(self.canvasLen+.01)
        if self.canvasLen<=self.frameHeight-.015:
           canvasZ=.0
           self.vertSliderThumb.hide()
           self.pageUpRegion.hide()
           self.pageDnRegion.hide()
           self.canvasLen=self.frameHeight-.015
        else:
           canvasZ=self.canvas.getZ()
           self.vertSliderThumb.show()
           self.pageUpRegion.show()
           self.pageDnRegion.show()
        self.__updateCanvasZpos(canvasZ)
        self.vertSliderThumb['frameSize']=(-.015,.015,-self.frameHeight*self.canvasRatio,-.01)
        thumbZ=self.vertSliderThumb.getZ()
        self.pageUpRegion['frameSize']=(-.015,.015,thumbZ-.01,-.01)
        self.pageDnRegion['frameSize']=(-.015,.015,-self.frameHeight+.01,thumbZ+self.vertSliderThumb['frameSize'][2])
  
    def __acceptAndIgnoreWorldEvent(self,event,command,extraArgs=[]):
        receivers=messenger.whoAccepts(event)
        if receivers is None:
           self.__eventReceivers[event]={}
        else:
           self.__eventReceivers[event]=receivers.copy()
        for r in self.__eventReceivers[event].keys():
            if type(r) != types.TupleType:
                r.ignore(event)
        self.accept(event,command,extraArgs)
  
    def __ignoreAndReAcceptWorldEvent(self,events):
        for event in events:
            self.ignore(event)
            if self.__eventReceivers.has_key(event):
               for r, method_xtraArgs_persist in self.__eventReceivers[event].items():
                   if type(r) != types.TupleType:
                       messenger.accept(event,r,*method_xtraArgs_persist)
            self.__eventReceivers[event]={}
  
    def __enteringFrame(self,m=None):
        # sometimes the WITHOUT event for page down region doesn't fired,
        # so directly suspend the page scrolling here
        self.__suspendScrollPage()
        BTprefix=base.buttonThrowers[0].node().getPrefix()
        if BTprefix==self.sliderThumbDragPrefix:
           return
        self.inOutBTprefix=BTprefix
        if self.suppressMouseWheel:
           self.__acceptAndIgnoreWorldEvent(self.inOutBTprefix+'wheel_up',
                command=self.__scrollCanvas, extraArgs=[-.07])
           self.__acceptAndIgnoreWorldEvent(self.inOutBTprefix+'wheel_down',
                command=self.__scrollCanvas, extraArgs=[.07])
        else:
           self.accept(self.inOutBTprefix+self.modifier+'-wheel_up',self.__scrollCanvas, [-.07])
           self.accept(self.inOutBTprefix+self.modifier+'-wheel_down',self.__scrollCanvas, [.07])
  
    def __exitingFrame(self,m=None):
        if not hasattr(self,'inOutBTprefix'):
           return
        if self.suppressMouseWheel:
           self.__ignoreAndReAcceptWorldEvent( (
                                               self.inOutBTprefix+'wheel_up',
                                               self.inOutBTprefix+'wheel_down',
                                               ) )
        else:
           self.ignore(self.inOutBTprefix+self.modifier+'-wheel_up')
           self.ignore(self.inOutBTprefix+self.modifier+'-wheel_down')
  
    def __setFocusButton(self,button,item):
        if self.focusButton:
           self.restoreNodeButton2Normal()
        self.focusButton=button
        self.highlightNodeButton()
        if callable(self.command) and button in self.buttonsList:
           # run user command and pass the selected item, it's index, and the button
           self.command(item,self.buttonsList.index(button),button)
  
    def __rightPressed(self,button,m):
        self.__isRightIn=True
  #       text0 : normal
  #       text1 : pressed
  #       text2 : rollover
  #       text3 : disabled
        button._DirectGuiBase__componentInfo['text2'][0].setColorScale(self.rightClickTextColors[self.focusButton==button])
        button.bind(DGG.B3RELEASE,self.__rightReleased,[button])
        button.bind(DGG.WITHIN,self.__rightIn,[button])
        button.bind(DGG.WITHOUT,self.__rightOut,[button])
  
    def __rightIn(self,button,m):
        self.__isRightIn=True
        button._DirectGuiBase__componentInfo['text2'][0].setColorScale(self.rightClickTextColors[self.focusButton==button])
    def __rightOut(self,button,m):
        self.__isRightIn=False
        button._DirectGuiBase__componentInfo['text2'][0].setColorScale(Vec4(1,1,1,1))
  
    def __rightReleased(self,button,m):
        button.unbind(DGG.B3RELEASE)
        button.unbind(DGG.WITHIN)
        button.unbind(DGG.WITHOUT)
        button._DirectGuiBase__componentInfo['text2'][0].setColorScale(self.rolloverColor)
        if not self.__isRightIn:
           return
        if callable(self.contextMenu):
           # run user command and pass the selected item, it's index, and the button
           self.contextMenu(button['extraArgs'][1],self.buttonsList.index(button),button)

    def scrollToBottom(self):
        ##for i in range(0,self.numItems):
        self.__scrollCanvas(1)

    def selectButton(self, button, item):
        self.__setFocusButton(button, item)
  
    def restoreNodeButton2Normal(self):
        """
           stop highlighting item
        """
        if self.focusButton != None:
            #self.focusButton['text_fg']=(1,1,1,1)
            self.focusButton['frameColor']=(0,0,0,0)
  
    def highlightNodeButton(self,idx=None):
        """
           highlight the item
        """
        if idx is not None:
            self.focusButton=self.buttonsList[idx]
        #self.focusButton['text_fg']=(.01,.01,.01,1)
        # nice dark blue.  don't mess with the text fg color though! we want it custom
        self.focusButton['frameColor']=(0,.3,.8,1)
  
    def clear(self):
        """
           clear the list
        """
        for c in self.buttonsList:
            c.remove()
        self.buttonsList=[]
        self.focusButton=None
        self.numItems=0
  
    def addItem(self,text,extraArgs=None,atIndex=None,textColorName=None):
        """
           add item to the list
           text : text for the button
           extraArgs : the object which will be passed to user command(s)
                       (both command and contextMenu) when the button get clicked
           atIndex : where to add the item
                     <None> : put item at the end of list
                     <integer> : put item at index <integer>
                     <button> : put item at <button>'s index
            textColorName : the color name eg. 'yellow'
        """
        textColor = self.buttonTextColor
        if textColorName != None:
            textColor = globals.colors[textColorName]
            
        button = DirectButton(parent=self.canvas,
            scale=self.itemScale,
            relief=DGG.FLAT,
            frameColor=(0,0,0,0),text_scale=self.itemTextScale,
            text=text, text_pos=(0,self.itemTextZ),text_fg=textColor,
            text_font=self.font, text_align=TextNode.ALeft,
            command=self.__setFocusButton,
            enableEdit=0, suppressMouse=0, rolloverSound=None,clickSound=None)
        #button.setMyMode(self.mode)
        l,r,b,t=button.getBounds()
        # top & bottom are blindly set without knowing where exactly the baseline is,
        # but this ratio fits most fonts
        baseline=-self.fontHeight*.25
        #button['saved_color'] = textColor
        button['frameSize']=(l-self.xtraSideSpace,r+self.xtraSideSpace,baseline,baseline+self.fontHeight)
  
  #          Zc=NodePath(button).getBounds().getCenter()[1]-self.fontHeight*.5+.25
  # #          Zc=button.getCenter()[1]-self.fontHeight*.5+.25
  #          button['frameSize']=(l-self.xtraSideSpace,r+self.xtraSideSpace,Zc,Zc+self.fontHeight)
       
        button['extraArgs']=[button,extraArgs]
        button._DirectGuiBase__componentInfo['text2'][0].setColorScale(self.rolloverColor)
        button.bind(DGG.B3PRESS,self.__rightPressed,[button])
        if isinstance(atIndex,DirectButton):
           if atIndex.isEmpty():
              atIndex=None
           else:
              index=self.buttonsList.index(atIndex)
              self.buttonsList.insert(index,button)
        if atIndex==None:
           self.buttonsList.append(button)
           index=self.numItems
        elif type(atIndex)==IntType:
           index=atIndex
           self.buttonsList.insert(index,button)
        Zpos=(-.7-index)*self.itemVertSpacing
        button.setPos(.02,0,Zpos)
        if index!=self.numItems:
           for i in range(index+1,self.numItems+1):
               self.buttonsList[i].setZ(self.buttonsList[i],-self.fontHeight)
        self.numItems+=1
        self.__adjustCanvasLength(self.numItems)
        if self.autoFocus:
           self.focusViewOnItem(index)
        if self.colorChange:
           Sequence(
              button.colorScaleInterval(self.colorChangeDuration,self.newItemColor,globals.colors['guiblue3']),
              button.colorScaleInterval(self.colorChangeDuration,Vec4(1,1,1,1),self.newItemColor)
              ).start()
  
    def focusViewOnItem(self,idx):
        """
           Scroll the window so the newly added item will be displayed
           in the middle of the window, if possible.
        """
        Zpos=(idx+.7)*self.itemVertSpacing-self.frameHeight*.5
        self.__updateCanvasZpos(Zpos)
       
    def setAutoFocus(self,b):
        """
           set auto-view-focus state of newly added item
        """
        self.autoFocus=b
  
    def index(self,button):
        """
           get the index of button
        """
        if not button in self.buttonsList:
           return None
        return self.buttonsList.index(button)
       
    def getNumItems(self):
        """
           get the current number of items on the list
        """
        return self.numItems
  
    def disableItem(self,i):
        if not 0<=i<self.numItems:
           print 'DISABLING : invalid index (%s)' %i
           return
        self.buttonsList[i]['state']=DGG.DISABLED
        self.buttonsList[i].setColorScale(.3,.3,.3,1)
   
    def enableItem(self,i):
        if not 0<=i<self.numItems:
           print 'ENABLING : invalid index (%s)' %i
           return
        self.buttonsList[i]['state']=DGG.NORMAL
        self.buttonsList[i].setColorScale(1,1,1,1)
  
    def removeItem(self,index):
        if not 0<=index<self.numItems:
           print 'REMOVAL : invalid index (%s)' %index
           return
        if self.numItems==0: return
        if self.focusButton==self.buttonsList[index]:
           self.focusButton=None
        self.buttonsList[index].removeNode()
        del self.buttonsList[index]
        self.numItems-=1
        for i in range(index,self.numItems):
            self.buttonsList[i].setZ(self.buttonsList[i],self.fontHeight)
        self.__adjustCanvasLength(self.numItems)
  
    def destroy(self):
        self.clear()
        self.__exitingFrame()
        self.ignoreAll()
        self.childrenFrame.removeNode()
        taskMgr.remove('mouseInRegionCheck')
  
    def hide(self):
        self.childrenFrame.hide()
        self.isMouseInRegion=False
        self.__exitingFrame()
        taskMgr.remove('mouseInRegionCheck')
  
    def show(self):
        self.childrenFrame.show()
        if not hasattr(self,'frameRegion'):
           taskMgr.doMethodLater(.2,self.__getFrameRegion,'getFrameRegion')
        elif not taskMgr.hasTaskNamed('mouseInRegionCheck'):
           taskMgr.add(self.__mouseInRegionCheck,'mouseInRegionCheck')
  
    def toggleVisibility(self):
        if self.childrenFrame.isHidden():
           self.show()
        else:
           self.hide()
  
    def setMyMode(self, myMode):
        self.mode = myMode
Ejemplo n.º 4
0
    def __init__(self,
                 items,
                 parent=None,
                 buttonThrower=None,
                 onDestroy=None,
                 font=None,
                 baselineOffset=.0,
                 scale=.05,
                 itemHeight=1.,
                 leftPad=.0,
                 separatorHeight=.5,
                 underscoreThickness=1,
                 BGColor=(0, 0, 0, .7),
                 BGBorderColor=(1, .85, .4, 1),
                 separatorColor=(1, 1, 1, 1),
                 frameColorHover=(1, .85, .4, 1),
                 frameColorPress=(0, 1, 0, 1),
                 textColorReady=(1, 1, 1, 1),
                 textColorHover=(0, 0, 0, 1),
                 textColorPress=(0, 0, 0, 1),
                 textColorDisabled=(.5, .5, .5, 1),
                 minZ=None,
                 useMouseZ=True):
        '''
      items : a collection of menu items
         Item format :
            ( 'Item text', 'path/to/image', command )
                        OR
            ( 'Item text', 'path/to/image', command, arg1,arg2,.... )
         If you don't want to use an image, pass 0.

         To create disabled item, pass 0 for the command :
            ( 'Item text', 'path/to/image', 0 )
         so, you can easily switch between enabled or disabled :
            ( 'Item text', 'path/to/image', command if commandEnabled else 0 )
                        OR
            ( 'Item text', 'path/to/image', (0,command)[commandEnabled] )

         To create submenu, pass a sequence of submenu items for the command.
         To create disabled submenu, pass an empty sequence for the command.

         To enable hotkey, insert an underscore before the character,
         e.g. hotkey of 'Item te_xt' is 'x' key.

         To add shortcut key text at the right side of the item, append it at the end of
         the item text, separated by "more than" sign, e.g. 'Item text>Ctrl-T'.

         To insert separator line, pass 0 for the whole item.


      parent : where to attach the menu, defaults to aspect2d

      buttonThrower : button thrower whose thrown events are blocked temporarily
                      when the menu is displayed. If not given, the default
                      button thrower is used

      onDestroy : user function which will be called after the menu is fully destroyed

      font           : text font
      baselineOffset : text's baseline Z offset

      scale       : text scale
      itemHeight  : spacing between items, defaults to 1
      leftPad     : blank space width before text
      separatorHeight : separator line height, relative to itemHeight

      underscoreThickness : underscore line thickness

      BGColor, BGBorderColor, separatorColor, frameColorHover, frameColorPress,
      textColorReady, textColorHover, textColorPress, textColorDisabled
      are some of the menu components' color

      minZ : minimum Z position to restrain menu's bottom from going offscreen (-1..1).
             If it's None, it will be set a little above the screen's bottom.
      '''
        self.parent = parent if parent else aspect2d
        self.onDestroy = onDestroy
        self.BT = buttonThrower if buttonThrower else base.buttonThrowers[
            0].node()
        self.menu = NodePath('menu-%s' % id(self))
        self.parentMenu = None
        self.submenu = None
        self.BTprefix = self.menu.getName() + '>'
        self.submenuCreationTaskName = 'createSubMenu-' + self.BTprefix
        self.submenuRemovalTaskName = 'removeSubMenu-' + self.BTprefix
        self.font = font if font else TextNode.getDefaultFont()
        self.baselineOffset = baselineOffset
        self.scale = scale
        self.itemHeight = itemHeight
        self.leftPad = leftPad
        self.separatorHeight = separatorHeight
        self.underscoreThickness = underscoreThickness
        self.BGColor = BGColor
        self.BGBorderColor = BGBorderColor
        self.separatorColor = separatorColor
        self.frameColorHover = frameColorHover
        self.frameColorPress = frameColorPress
        self.textColorReady = textColorReady
        self.textColorHover = textColorHover
        self.textColorPress = textColorPress
        self.textColorDisabled = textColorDisabled
        self.minZ = minZ
        self.mpos = Point2(base.mouseWatcherNode.getMouse())

        self.itemCommand = []
        self.hotkeys = {}
        self.numItems = 0
        self.sel = -1
        self.selByKey = False

        bgPad = self.bgPad = .0125
        texMargin = self.font.getTextureMargin() * self.scale * .25
        b = DirectButton(parent=NodePath(''),
                         text='^|g_',
                         text_font=self.font,
                         scale=self.scale)
        fr = b.node().getFrame()
        b.getParent().removeNode()
        baselineToCenter = (fr[2] + fr[3]) * self.scale
        LH = (fr[3] - fr[2]) * self.itemHeight * self.scale
        imageHalfHeight = .5 * (fr[3] - fr[2]) * self.itemHeight * .85
        arrowHalfHeight = .5 * (fr[3] - fr[2]) * self.itemHeight * .5
        baselineToTop = (fr[3] * self.itemHeight * self.scale /
                         LH) / (1. + self.baselineOffset)
        baselineToBot = LH / self.scale - baselineToTop
        itemZcenter = (baselineToTop - baselineToBot) * .5
        separatorHalfHeight = .5 * separatorHeight * LH
        LSseparator = LineSegs()
        LSseparator.setColor(.5, .5, .5, .2)

        arrowVtx = [
            (0, itemZcenter),
            (-2 * arrowHalfHeight, itemZcenter + arrowHalfHeight),
            (-arrowHalfHeight, itemZcenter),
            (-2 * arrowHalfHeight, itemZcenter - arrowHalfHeight),
        ]
        tri = Triangulator()
        vdata = GeomVertexData('trig', GeomVertexFormat.getV3(), Geom.UHStatic)
        vwriter = GeomVertexWriter(vdata, 'vertex')
        for x, z in arrowVtx:
            vi = tri.addVertex(x, z)
            vwriter.addData3f(x, 0, z)
            tri.addPolygonVertex(vi)
        tri.triangulate()
        prim = GeomTriangles(Geom.UHStatic)
        for i in range(tri.getNumTriangles()):
            prim.addVertices(tri.getTriangleV0(i), tri.getTriangleV1(i),
                             tri.getTriangleV2(i))
            prim.closePrimitive()
        geom = Geom(vdata)
        geom.addPrimitive(prim)
        geomNode = GeomNode('arrow')
        geomNode.addGeom(geom)
        realArrow = NodePath(geomNode)
        z = -baselineToTop * self.scale - bgPad
        maxWidth = .1 / self.scale
        shortcutTextMaxWidth = 0
        anyImage = False
        anyArrow = False
        anyShortcut = False
        arrows = []
        shortcutTexts = []
        loadPrcFileData('', 'text-flatten 0')
        for item in items:
            if item:
                t, imgPath, f = item[:3]
                haveSubmenu = type(f) in SEQUENCE_TYPES
                anyArrow |= haveSubmenu
                anyImage |= bool(imgPath)
                disabled = not len(f) if haveSubmenu else not callable(f)
                args = item[3:]
                underlinePos = t.find('_')
                t = t.replace('_', '')
                shortcutSepPos = t.find('>')
                if shortcutSepPos > -1:
                    if haveSubmenu:
                        print(
                            "\nA SHORTCUT KEY POINTING TO A SUBMENU IS NON-SENSE, DON'T YOU AGREE ?"
                        )
                    else:
                        shortcutText = NodePath(
                            OnscreenText(
                                parent=self.menu,
                                text=t[shortcutSepPos + 1:],
                                font=self.font,
                                scale=1,
                                fg=(1, 1, 1, 1),
                                align=TextNode.ARight,
                            ))
                        shortcutTextMaxWidth = max(
                            shortcutTextMaxWidth,
                            abs(shortcutText.getTightBounds()[0][0]))
                        anyShortcut = True
                    t = t[:shortcutSepPos]
                else:
                    shortcutText = ''
                EoLcount = t.count('\n')
                arrowZpos = -self.font.getLineHeight() * EoLcount * .5
                if disabled:
                    b = NodePath(
                        OnscreenText(
                            parent=self.menu,
                            text=t,
                            font=self.font,
                            scale=1,
                            fg=textColorDisabled,
                            align=TextNode.ALeft,
                        ))
                    # don't pass the scale and position to OnscreenText constructor,
                    # to maintain correctness between the OnscreenText and DirectButton items
                    # due to the new text generation implementation
                    b.setScale(self.scale)
                    b.setZ(z)
                    maxWidth = max(maxWidth,
                                   b.getTightBounds()[1][0] / self.scale)
                    if shortcutText:
                        shortcutText.reparentTo(b)
                        shortcutText.setColor(Vec4(*textColorDisabled), 1)
                        shortcutText.setZ(arrowZpos)
                        shortcutTexts.append(shortcutText)
                else:
                    b = DirectButton(
                        parent=self.menu,
                        text=t,
                        text_font=self.font,
                        scale=self.scale,
                        pos=(0, 0, z),
                        text_fg=textColorReady,
                        # text color when mouse over
                        text2_fg=textColorHover,
                        # text color when pressed
                        text1_fg=textColorHover
                        if haveSubmenu else textColorPress,
                        # framecolor when pressed
                        frameColor=frameColorHover
                        if haveSubmenu else frameColorPress,
                        command=(lambda: 0)
                        if haveSubmenu else self.__runCommand,
                        extraArgs=[] if haveSubmenu else [f, args],
                        text_align=TextNode.ALeft,
                        relief=DGG.FLAT,
                        rolloverSound=0,
                        clickSound=0,
                        pressEffect=0)
                    b.stateNodePath[2].setColor(
                        *frameColorHover)  # framecolor when mouse over
                    b.stateNodePath[0].setColor(0, 0, 0,
                                                0)  # framecolor when ready
                    bframe = Vec4(b.node().getFrame())
                    if EoLcount:
                        bframe.setZ(EoLcount * 10)
                        b['frameSize'] = bframe
                    maxWidth = max(maxWidth, bframe[1])
                    if shortcutText:
                        for snpi, col in ((0, textColorReady),
                                          (1, textColorPress),
                                          (2, textColorHover)):
                            sct = shortcutText.copyTo(b.stateNodePath[snpi],
                                                      sort=10)
                            sct.setColor(Vec4(*col), 1)
                            sct.setZ(arrowZpos)
                            shortcutTexts.append(sct)
                        shortcutText.removeNode()
                if imgPath:
                    img = loader.loadTexture(imgPath)
                    if disabled:
                        if imgPath in PopupMenu.grayImages:
                            img = PopupMenu.grayImages[imgPath]
                        else:
                            pnm = PNMImage()
                            img.store(pnm)
                            pnm.makeGrayscale(.2, .2, .2)
                            img = Texture()
                            img.load(pnm)
                            PopupMenu.grayImages[imgPath] = img
                    img.setMinfilter(Texture.FTLinearMipmapLinear)
                    img.setWrapU(Texture.WMClamp)
                    img.setWrapV(Texture.WMClamp)
                    CM = CardMaker('')
                    CM.setFrame(-2 * imageHalfHeight - leftPad, -leftPad,
                                itemZcenter - imageHalfHeight,
                                itemZcenter + imageHalfHeight)
                    imgCard = b.attachNewNode(CM.generate())
                    imgCard.setTexture(img)
                if underlinePos > -1:
                    oneLineText = t[:underlinePos + 1]
                    oneLineText = oneLineText[oneLineText.rfind('\n') + 1:]
                    tn = TextNode('')
                    tn.setFont(self.font)
                    tn.setText(oneLineText)
                    tnp = NodePath(tn.getInternalGeom())
                    underlineXend = tnp.getTightBounds()[1][0]
                    tnp.removeNode()
                    tn.setText(t[underlinePos])
                    tnp = NodePath(tn.getInternalGeom())
                    b3 = tnp.getTightBounds()
                    underlineXstart = underlineXend - (b3[1] - b3[0])[0]
                    tnp.removeNode()
                    underlineZpos = -.7 * baselineToBot - self.font.getLineHeight(
                    ) * t[:underlinePos].count('\n')
                    LSunder = LineSegs()
                    LSunder.setThickness(underscoreThickness)
                    LSunder.moveTo(underlineXstart + texMargin, 0,
                                   underlineZpos)
                    LSunder.drawTo(underlineXend - texMargin, 0, underlineZpos)
                    if disabled:
                        underline = b.attachNewNode(LSunder.create())
                        underline.setColor(Vec4(*textColorDisabled), 1)
                    else:
                        underline = b.stateNodePath[0].attachNewNode(
                            LSunder.create())
                        underline.setColor(Vec4(*textColorReady), 1)
                        underline.copyTo(b.stateNodePath[1], 10).setColor(
                            Vec4(*textColorHover
                                 if haveSubmenu else textColorPress), 1)
                        underline.copyTo(b.stateNodePath[2],
                                         10).setColor(Vec4(*textColorHover), 1)
                        hotkey = t[underlinePos].lower()
                        if hotkey in self.hotkeys:
                            self.hotkeys[hotkey].append(self.numItems)
                        else:
                            self.hotkeys[hotkey] = [self.numItems]
                            self.accept(self.BTprefix + hotkey,
                                        self.__processHotkey, [hotkey])
                            self.accept(self.BTprefix + 'alt-' + hotkey,
                                        self.__processHotkey, [hotkey])
                if haveSubmenu:
                    if disabled:
                        arrow = realArrow.instanceUnderNode(b, '')
                        arrow.setColor(Vec4(*textColorDisabled), 1)
                        arrow.setZ(arrowZpos)
                    else:
                        arrow = realArrow.instanceUnderNode(
                            b.stateNodePath[0], 'r')
                        arrow.setColor(Vec4(*textColorReady), 1)
                        arrow.setZ(arrowZpos)
                        arrPress = realArrow.instanceUnderNode(
                            b.stateNodePath[1], 'p')
                        arrPress.setColor(Vec4(*textColorHover), 1)
                        arrPress.setZ(arrowZpos)
                        arrHover = realArrow.instanceUnderNode(
                            b.stateNodePath[2], 'h')
                        arrHover.setColor(Vec4(*textColorHover), 1)
                        arrHover.setZ(arrowZpos)
                        # weird, if sort order is 0, it's obscured by the frame
                        for a in (arrPress, arrHover):
                            a.reparentTo(a.getParent(), sort=10)
                if not disabled:
                    extraArgs = [self.numItems, f if haveSubmenu else 0]
                    self.accept(DGG.ENTER + b.guiId, self.__hoverOnItem,
                                extraArgs)
                    self.accept(DGG.EXIT + b.guiId, self.__offItem)
                    #~ self.itemCommand.append((None,0) if haveSubmenu else (f,args))
                    self.itemCommand.append((f, args))
                    if self.numItems == 0:
                        self.firstButtonIdx = int(b.guiId[2:])
                    self.numItems += 1
                z -= LH + self.font.getLineHeight() * self.scale * EoLcount
            else:  # SEPARATOR LINE
                z += LH - separatorHalfHeight - baselineToBot * self.scale
                LSseparator.moveTo(0, 0, z)
                LSseparator.drawTo(self.scale * .5, 0, z)
                LSseparator.drawTo(self.scale, 0, z)
                z -= separatorHalfHeight + baselineToTop * self.scale
        maxWidth += 7 * arrowHalfHeight * (
            anyArrow or anyShortcut) + .2 + shortcutTextMaxWidth
        arrowXpos = maxWidth - arrowHalfHeight
        realArrow.setX(arrowXpos)
        if anyImage:
            leftPad += 2 * imageHalfHeight + leftPad
        for sct in shortcutTexts:
            sct.setX(maxWidth - 2 * (arrowHalfHeight * anyArrow + .2))
        for c in asList(self.menu.findAllMatches('**/DirectButton*')):
            numLines = c.node().getFrame()[2]
            c.node().setFrame(
                Vec4(
                    -leftPad, maxWidth, -baselineToBot -
                    (numLines * .1 * self.itemHeight if numLines >= 10 else 0),
                    baselineToTop))
        loadPrcFileData('', 'text-flatten 1')

        try:
            minZ = self.menu.getChild(0).getRelativePoint(
                b, Point3(0, 0,
                          b.node().getFrame()[2]))[2]
        except:
            minZ = self.menu.getChild(0).getRelativePoint(
                self.menu, Point3(
                    0, 0,
                    b.getTightBounds()[0][2]))[2] - baselineToBot * .5
        try:
            top = self.menu.getChild(0).node().getFrame()[3]
        except:
            top = self.menu.getChild(0).getZ() + baselineToTop
        l, r, b, t = -leftPad - bgPad / self.scale, maxWidth + bgPad / self.scale, minZ - bgPad / self.scale, top + bgPad / self.scale
        menuBG = DirectFrame(parent=self.menu.getChild(0),
                             frameSize=(l, r, b, t),
                             frameColor=BGColor,
                             state=DGG.NORMAL,
                             suppressMouse=1)
        menuBorder = self.menu.getChild(0).attachNewNode('border')
        borderVtx = (
            (l, 0, b),
            (l, 0, .5 * (b + t)),
            (l, 0, t),
            (.5 * (l + r), 0, t),
            (r, 0, t),
            (r, 0, .5 * (b + t)),
            (r, 0, b),
            (.5 * (l + r), 0, b),
            (l, 0, b),
        )
        LSborderBG = LineSegs()
        LSborderBG.setThickness(4)
        LSborderBG.setColor(0, 0, 0, .7)
        LSborderBG.moveTo(*(borderVtx[0]))
        for v in borderVtx[1:]:
            LSborderBG.drawTo(*v)
        # fills the gap at corners
        for v in range(0, 7, 2):
            LSborderBG.moveTo(*(borderVtx[v]))
        menuBorder.attachNewNode(LSborderBG.create())
        LSborder = LineSegs()
        LSborder.setThickness(2)
        LSborder.setColor(*BGBorderColor)
        LSborder.moveTo(*(borderVtx[0]))
        for v in borderVtx[1:]:
            LSborder.drawTo(*v)
        menuBorder.attachNewNode(LSborder.create())
        for v in range(1, 8, 2):
            LSborderBG.setVertexColor(v, Vec4(0, 0, 0, .1))
            LSborder.setVertexColor(v, Vec4(.3, .3, .3, .5))
        menuBorderB3 = menuBorder.getTightBounds()
        menuBorderDims = menuBorderB3[1] - menuBorderB3[0]
        menuBG.wrtReparentTo(self.menu, sort=-1)
        self.menu.reparentTo(self.parent)
        x = -menuBorderB3[0][0] * self.scale
        for c in asList(self.menu.getChildren()):
            c.setX(x)
        self.maxWidth = maxWidth = menuBorderDims[0]
        self.height = menuBorderDims[2]
        maxWidthR2D = maxWidth * self.menu.getChild(0).getSx(render2d)
        separatorLines = self.menu.attachNewNode(LSseparator.create(), 10)
        separatorLines.setSx(maxWidth)
        for v in range(1, LSseparator.getNumVertices(), 3):
            LSseparator.setVertexColor(v, Vec4(*separatorColor))
        x = clampScalar(-.98, .98 - maxWidthR2D,
                        self.mpos[0] - maxWidthR2D * .5)
        minZ = (-.98 if self.minZ is None else self.minZ)
        z = clampScalar(
            minZ +
            menuBorderDims[2] * self.scale * self.parent.getSz(render2d), .98,
            self.mpos[1] if useMouseZ else -1000)
        self.menu.setPos(render2d, x, 0, z)
        self.menu.setTransparency(1)

        self.origBTprefix = self.BT.getPrefix()
        self.BT.setPrefix(self.BTprefix)
        self.accept(self.BTprefix + 'escape', self.destroy)
        for e in ('mouse1', 'mouse3'):
            self.accept(self.BTprefix + e, self.destroy, [True])
        self.accept(self.BTprefix + 'arrow_down', self.__nextItem)
        self.accept(self.BTprefix + 'arrow_down-repeat', self.__nextItem)
        self.accept(self.BTprefix + 'arrow_up', self.__prevItem)
        self.accept(self.BTprefix + 'arrow_up-repeat', self.__prevItem)
        self.accept(self.BTprefix + 'enter', self.__runSelItemCommand)
        self.accept(self.BTprefix + 'space', self.__runSelItemCommand)