示例#1
0
class Menu(object):
    '''
    '''
    def __init__(self, parent):
        
        self._notify = DirectNotify().newCategory("Menu")
        self._notify.info("New Menu-Object created: %s" %(self))
        
        #Font
        self.font = DynamicTextFont(FONT)
        self.font.setRenderMode(TextFont.RMSolid)
        
        self.KEY_DELAY = 0.15
        self.player_buttonpressed = []
        
        self._parent = parent
        self._players = parent.players
        self._devices = parent.devices
        
        taskMgr.add(self.fetchAnyKey, "fetchAnyKey")
        
    def showStartScreen(self):
        '''
        the first screen with "press any Key"
        the device with the first key press will be the first player
        '''
        self._notify.info("Initializing StartScreen")
        
        self.wii = []
        
        #StartScreen Node
        self.startNode = NodePath("StartNode")
        self.startNode.reparentTo(render)
        self.startNode.setPos(-5,15,3)

        #Headline model
        headline = loader.loadModel("data/models/logo.egg")
        headline.setX(4.7)
        headline.setY(-4)
        headline.setZ(-2)
        headline.setP(90)
        headline.reparentTo(self.startNode)
        
        #Press any key text
        presskey = TextNode("PressAnyKey")
        presskey.setFont(self.font)
        presskey.setText(_("Press any key!!"))
        textNodePath = NodePath("PressAnyNode")
        textNodePath.attachNewNode(presskey)
        textNodePath.setPos(0,10,-9.5)
        textNodePath.reparentTo(self.startNode)
        
        #Show the start screen 
        self.startNode.show()
        
        #LICHT
        plight = PointLight('plight')
        plight.setColor(VBase4(10, 10, 10, 1))
        plnp = self.startNode.attachNewNode(plight)
        plnp.setPos(20, -800, 30)
        self.startNode.setLight(plnp)
        
        #Camera
        self.camera = base.makeCamera(base.win)
        
        self._notify.info("StarScreen initialized")
        
    # -----------------------------------------------------------------
    
    def fetchAnyKey(self, task):
        '''
        Return the first device with the first key stroke
        '''
        for i in xrange(len(self._devices.devices)):
            if self._devices.devices[i].boost:
                #Kill Camera
                self.camera.node().setActive(False)
                #Kill Node
                self.startNode.removeNode()
                
                #Start the menu
                self.menu = MainMenu(self._parent, self.newGame, self._devices.devices[i], self._devices)
                self.menu.menuMain()
                return task.done
        return task.cont



        # -----------------------------------------------------------------

    def newGame(self):
        '''
        The select vehicle screen
        '''
        base.enableParticles()
        self.countdown = COUNTDOWN_START #the countdown, when its over the game can be started
        self._notify.info("Initializing new game")
        #GlobPattern if we need a Panda Class
        self.vehicle_list = glob.glob("data/models/vehicles/*.egg")
        for index in range(len(self.vehicle_list)):
            self.vehicle_list[index] = Filename.fromOsSpecific(self.vehicle_list[index]).getFullpath()
        self._notify.debug("Vehicle list: %s" %(self.vehicle_list))
        self.platform = loader.loadModel("data/models/platform.egg")
        
        #Loading-Text that gets displayed when loading a model
        text = TextNode("Loading")
        text.setFont(self.font)
        text.setText(_("Loading..."))
        text.setAlign(TextProperties.ACenter)
        self.loading = NodePath("LoadingNode")
        self.loading.attachNewNode(text)
        self.loading.setPos(0,0,2)
        
        #The countdown, its possible to start the game when its 0
        text = TextNode("Countdown")
        text.setFont(self.font)
        text.setAlign(TextProperties.ACenter)
        text.setText(_(str(COUNTDOWN_START)))
        self.countdown_node = NodePath("Countdown")
        self.countdown_node.attachNewNode(text)
        self.countdown_node.setPos(0,0,4)
        
        #PreLoad the description that gets displayed when loading a model
        text = TextNode("name")
        text.setFont(self.font)
        text.setText(_("Loading..."))
        text.setAlign(TextProperties.ACenter)
        self.attributes = NodePath("AttributeNode")
        self.attributes.attachNewNode(text)
        
        self.unusedDevices = self._devices.devices[:]
        taskMgr.add(self.collectPlayer, "collectPlayer")
        #taskMgr.add(self.collectWii, "collectWii")
        self.screens = []
        taskMgr.add(self.selectVehicle, "selectVehicle")
        
        self.color_red = Vec4(1,0,0,0)
        self.color_green = Vec4(0,1,0,0)
        
        self._notify.info("New game initialized")
        
    # -----------------------------------------------------------------
        
    def selectVehicle(self, task):
        '''
        The vehicle select and rotate task
        '''
        #Set the countdown and hide, if > 3
        self.countdown -= globalClock.getDt()
        if self.countdown <=0:
            self.countdown_node.getChild(0).node().setText(_("Go"))
            self.countdown_node.setColor(self.color_green)
        elif self.countdown <=3: 
            self.countdown_node.getChild(0).node().setText(str(int(round((self.countdown+0.5)))))
            self.countdown_node.setColor(self.color_red)           
            self.countdown_node.show()
        else: self.countdown_node.hide()
        
        heading = self.loading.getH() -(30 * globalClock.getDt())
        self.loading.setH(heading)
        for player in self._players:
            if player.vehicle.model != None:
                player.vehicle.model.setH(heading)
                
            if self.player_buttonpressed[self._players.index(player)] < task.time and not player.vehicle.model_loading:
                if player.device.directions[0] < -0.8:
                    self._parent.menuselect.play()
                    self.countdown = COUNTDOWN_START
                    self.player_buttonpressed[self._players.index(player)] = task.time + self.KEY_DELAY
                    index = self.vehicle_list.index("data/models/vehicles/%s" %(player.vehicle.model.getName()))-1
                    self._notify.debug("Previous vehicle selected: %s" %(index))
                    player.vehicle.model_loading = True
                    player.vehicle.model.hide()
                    player.camera.camera.getParent().find("AttributeNode").hide()#Hide the attributes
                    self.loading.instanceTo(player.camera.camera.getParent())
                    loader.loadModel(self.vehicle_list[index], callback = player.setVehicle)
                elif player.device.directions[0] > 0.8:
                    self._parent.menuselect.play()
                    self.countdown = COUNTDOWN_START
                    self.player_buttonpressed[self._players.index(player)] = task.time + self.KEY_DELAY
                    index = self.vehicle_list.index("data/models/vehicles/%s" %(player.vehicle.model.getName()))+1
                    self._notify.debug("Next vehicle selected: %s" %(index))
                    if index >= len(self.vehicle_list): index = 0
                    player.vehicle.model_loading = True
                    player.vehicle.model.hide()
                    player.camera.camera.getParent().find("AttributeNode").hide()#Hide the attributes
                    self.loading.instanceTo(player.camera.camera.getParent())
                    loader.loadModel(self.vehicle_list[index], callback = player.setVehicle)
                    
                if player.device.directions[1] > 0.8:
                    self.countdown = COUNTDOWN_START
                    self._parent.menuselect.play()
                    self.player_buttonpressed[self._players.index(player)] = task.time + self.KEY_DELAY
                 
                    self._notify.debug("Next color selected")
                    a = player.vehicle.model.findAllTextures()
                    tex = a.findTexture(player.vehicle.model.getName()[:-4])
                    self.rotateHue(tex, 0.1)
                    
                elif player.device.directions[1] < -0.8:
                    self._parent.menuselect.play()
                    self.countdown = COUNTDOWN_START
                    self.player_buttonpressed[self._players.index(player)] = task.time + self.KEY_DELAY
                 
                    self._notify.debug("Next color selected")
                    a = player.vehicle.model.findAllTextures()
                    tex = a.findTexture(player.vehicle.model.getName()[:-4])
                    self.rotateHue(tex, -0.1)
        return task.cont
    
    # -----------------------------------------------------------------

    def rotateHue(self, tex, value=0.1):
        '''
        '''
        img = PNMImage()
        tex.store(img)
        for y in xrange(img.getReadYSize()):
            for x in xrange(img.getReadXSize()):
                r, g, b = img.getXel(x,y)
                h, s, v = colorsys.rgb_to_hsv(r, g, b)
                h += value
                if h < 0:
                    h += 360
                r, g, b = colorsys.hsv_to_rgb(h, s, v)
                img.setXel(x,y,r,g,b)
        tex.load(img)
    
    # -----------------------------------------------------------------

    def collectWii(self, task):
        '''
        Collect wiimotes task
        '''
        try:
            print 'Put Wiimote in discoverable mode now (press 1+2)...'
            wiimote = cwiid.Wiimote()
            self.wiimoteX.append(wiimote)
            print len(self.wiimoteX)
        except:
            pass
        return task.cont
        
    # -----------------------------------------------------------------
     
    def collectPlayer(self, task):
        '''
        Wait until all players are ready
        '''
        if len(self._players) > 0 and self.player_buttonpressed[0] < task.time:
            if self._players[0].device.boost and self.countdown <= 0:
                loading = False
                for player in self._players: 
                    if player.vehicle.model_loading: 
                        loading = True
                        break
                self._notify.debug("Loading vehicle: %s" %(loading))
                if not loading:
                    taskMgr.remove("selectVehicle")
                    self.track =  trackgen3d.Track3d(1000, 1800, 1600, 1200, 5)#len(self._players))
                    self.streetPath = render.attachNewNode(self.track.createRoadMesh())
                    #self.borderleftPath = render.attachNewNode(self.track.createBorderLeftMesh())
                    self.borderleftPath = render.attachNewNode(self.track.createBorderLeftMesh())
                    self.borderrightPath = render.attachNewNode(self.track.createBorderRightMesh())
                    self.borderleftcollisionPath = NodePath(self.track.createBorderLeftCollisionMesh())
                    self.borderrightcollisionPath = NodePath(self.track.createBorderRightCollisionMesh())
                    ##self.borderPath = render.attachNewNode(self.track.createBorderMesh())
                    
                    textures = ["tube", "tube2", "street"]
                    tex = textures[random.randint(0, len(textures)-1)]
                    roadtex = loader.loadTexture('data/textures/'+tex+'.png')
                    bordertex = loader.loadTexture('data/textures/border.png')
                    self.streetPath.setTexture(roadtex)
                    self.borderleftPath.setTexture(bordertex)
                    self.borderrightPath.setTexture(bordertex)

                    #self.streetPath = loader.loadModel('data/models/Street.egg')
                    ##self.streetPath = loader.loadModel('data/models/Street.egg')
                    #tex = loader.loadTexture('data/models/StreetTex.png')
                    #self.nodePath.setTexture(tex)
                    
                    self._parent.startGame(self.streetPath,self.borderleftPath,self.borderrightPath, self.track.trackpoints, self.borderleftcollisionPath, self.borderrightcollisionPath)
                    return task.done

        for device in self.unusedDevices:
                if device.boost:
                    self.countdown = COUNTDOWN_START
                    self.player_buttonpressed.append(0)
                    self._parent.addPlayer(device)
                    
                    #Set the PlayerCam to the Vehicle select menu Node        
                    vehicleSelectNode = NodePath("VehicleSelectNode")
                    self._players[-1].camera.camera.reparentTo(vehicleSelectNode)
                    
                    #Light, that casts shadows
                    plight = Spotlight('plight')
                    plight.setColor(VBase4(10.0, 10.0, 10.0, 1))
                    if (base.win.getGsg().getSupportsBasicShaders() != 0):
                        pass
                        ##plight.setShadowCaster(True, 2048, 2048)#enable shadows for this light ##TODO wegen Linux
                        
                    #Light
                    plight.getLens().setFov(80)
                    plnp = vehicleSelectNode.attachNewNode(plight)
                    plnp.setPos(2, -10, 10)
                    plnp.lookAt(0,0,0)
                    vehicleSelectNode.setLight(plnp)
##                    vehicleSelectNode.setShaderAuto()#enable autoshader so we can use shadows
                    
                    #Light
                    ambilight = AmbientLight('ambilight')
                    ambilight.setColor(VBase4(0.2, 0.2, 0.2, 1))
                    vehicleSelectNode.setLight(vehicleSelectNode.attachNewNode(ambilight))
                    self.platform.instanceTo(vehicleSelectNode) #Load the platform
                    
                    #instance shown text
                    self.countdown_node.instanceTo(vehicleSelectNode) #Instance the Countdown
                    self.loading.instanceTo(vehicleSelectNode) #Show the Loading-Text
                    self.attributes.copyTo(vehicleSelectNode).hide()
                    self._players[-1].vehicle.model_loading = True
                    
                    #start loading the model
                    loader.loadModel(self.vehicle_list[0], callback = self._players[-1].setVehicle)
                    self._notify.debug("Loading initial vehicle: %s" %(self.vehicle_list[0]))
                    self.unusedDevices.remove(device) 
                    self.player_buttonpressed[-1] = task.time + self.KEY_DELAY
                    
                    #Add the Skybox
                    skybox = loader.loadModel("data/models/skybox.egg")
                    t = Texture()
                    t.load(PNMImage("data/textures/skybox_hangar.png"))
                    skybox.setTexture(t)
                    skybox.setBin("background", 1)
                    skybox.setDepthWrite(0)
                    skybox.setDepthTest(0)
                    skybox.setLightOff()
                    skybox.setScale(10000)
                    skybox.reparentTo(vehicleSelectNode)

        for player in self._players:
            if self.player_buttonpressed[self._players.index(player)] < task.time:
                if player.device.use_item:
                    self.countdown = COUNTDOWN_START
                    self._notify.debug("Removing player: %s" %(player))
                    self.unusedDevices.append(player.device)
                    self.player_buttonpressed.pop(self._players.index(player))
                    self._parent.removePlayer(player) 
        return task.cont
    def traverse(self, nodePath, dnaStorage):
        root = NodePath('signroot')
        head_root = NodePath('root')
        wantDecalTest = base.config.GetBool('want-sign-decal-test', False)
        x = 0
        for i in range(len(self.text)):
            tn = TextNode("text")
            tn.setText(self.text[i])
            tn.setTextColor(self.color)
            font = dnaStorage.findFont(self.code)

            if font == None:
                raise DNAError.DNAError('Font code %s not found.' % self.code)

            tn.setFont(font)

            if i == 0 and 'b' in self.flags:
                tn.setTextScale(1.5)
            np = root.attachNewNode(tn)
            np.setScale(self.scale)
            np.setDepthWrite(0)

            if i % 2:
                np.setPos(x + self.stumble, 0, self.stomp)
                np.setR(-self.wiggle)
            else:
                np.setPos(x - self.stumble, 0, self.stomp)
                np.setR(self.wiggle)

            x += tn.getWidth() * np.getSx() + self.kern

        for i in range(root.getNumChildren()):
            c = root.getChild(i)
            c.setX(c.getX() - x / 2.)

        if self.width and self.height:
            for i in range(root.getNumChildren()):
                node = root.getChild(i)

                A = (node.getX() / (self.height / 2.))
                B = (self.indent * math.pi / 180.)

                theta = A + B
                d = node.getY()
                x = math.sin(theta) * (self.height / 2.)
                y = (math.cos(theta) - 1) * (self.height / 2.)
                radius = math.hypot(x, y)

                if radius != 0:
                    j = (radius + d) / radius
                    x *= j
                    y *= j
                node.setPos(x, 0, y)
                node.setR(node, (theta * 180.) / math.pi)

        collection = root.findAllMatches("**/+TextNode")
        for i in range(collection.getNumPaths()):
            xnp = collection.getPath(i)
            np2 = xnp.getParent().attachNewNode(xnp.node().generate())
            np2.setTransform(xnp.getTransform())
            xnp.removeNode()

        _np = nodePath.attachNewNode(root.node())
        _np.setPosHpr(self.pos, self.hpr)

        if wantDecalTest:
            root.setEffect(DecalEffect.make())
        else:
            _np.setDepthOffset(50)

        self.traverseChildren(_np, dnaStorage)

        _np.flattenStrong()
示例#3
0
class StaticGeometricModel(object):
    """
    load an object as a static geometric model -> changing pos, rot, color, etc. are not allowed
    there is no extra elements for this model, thus is much faster
    author: weiwei
    date: 20190312
    """
    def __init__(self,
                 initor=None,
                 name="defaultname",
                 btransparency=True,
                 btwosided=False):
        """
        :param initor: path type defined by os.path or trimesh or nodepath
        :param btransparency
        :param name
        """
        if isinstance(initor, StaticGeometricModel):
            self._objpath = copy.deepcopy(initor.objpath)
            self._objtrm = copy.deepcopy(initor.objtrm)
            self._objpdnp = copy.deepcopy(initor.objpdnp)
            self._name = copy.deepcopy(initor.name)
            self._localframe = copy.deepcopy(initor.localframe)
        else:
            # make a grandma nodepath to separate decorations (-autoshader) and raw nodepath (+autoshader)
            self._name = name
            self._objpdnp = NodePath(name)
            if isinstance(initor, str):
                self._objpath = initor
                self._objtrm = da.trm.load(self._objpath)
                objpdnp_raw = da.trimesh_to_nodepath(self._objtrm,
                                                     name='pdnp_raw')
                objpdnp_raw.reparentTo(self._objpdnp)
            elif isinstance(initor, da.trm.Trimesh):
                self._objpath = None
                self._objtrm = initor
                objpdnp_raw = da.trimesh_to_nodepath(self._objtrm)
                objpdnp_raw.reparentTo(self._objpdnp)
            elif isinstance(initor, o3d.geometry.PointCloud
                            ):  # TODO should pointcloud be pdnp or pdnp_raw
                self._objpath = None
                self._objtrm = da.trm.Trimesh(np.asarray(initor.points))
                objpdnp_raw = da.nodepath_from_points(self._objtrm.vertices,
                                                      name='pdnp_raw')
                objpdnp_raw.reparentTo(self._objpdnp)
            elif isinstance(
                    initor,
                    np.ndarray):  # TODO should pointcloud be pdnp or pdnp_raw
                self._objpath = None
                if initor.shape[1] == 3:
                    self._objtrm = da.trm.Trimesh(initor)
                    objpdnp_raw = da.nodepath_from_points(
                        self._objtrm.vertices)
                elif initor.shape[1] == 7:
                    self._objtrm = da.trm.Trimesh(initor[:, :3])
                    objpdnp_raw = da.nodepath_from_points(
                        self._objtrm.vertices, initor[:, 3:].tolist())
                    objpdnp_raw.setRenderMode(RenderModeAttrib.MPoint, 3)
                else:
                    # TODO depth UV?
                    raise NotImplementedError
                objpdnp_raw.reparentTo(self._objpdnp)
            elif isinstance(initor, o3d.geometry.TriangleMesh):
                self._objpath = None
                self._objtrm = da.trm.Trimesh(
                    vertices=initor.vertices,
                    faces=initor.triangles,
                    face_normals=initor.triangle_normals)
                objpdnp_raw = da.trimesh_to_nodepath(self._objtrm,
                                                     name='pdnp_raw')
                objpdnp_raw.reparentTo(self._objpdnp)
            elif isinstance(initor, NodePath):
                self._objpath = None
                self._objtrm = None  # TODO nodepath to trimesh?
                objpdnp_raw = initor
                objpdnp_raw.reparentTo(self._objpdnp)
            else:
                self._objpath = None
                self._objtrm = None
                objpdnp_raw = NodePath("pdnp_raw")
                objpdnp_raw.reparentTo(self._objpdnp)
            if btransparency:
                self._objpdnp.setTransparency(TransparencyAttrib.MDual)
            if btwosided:
                self._objpdnp.getChild(0).setTwoSided(True)
            self._localframe = None

    @property
    def name(self):
        # read-only property
        return self._name

    @property
    def objpath(self):
        # read-only property
        return self._objpath

    @property
    def objpdnp(self):
        # read-only property
        return self._objpdnp

    @property
    def objpdnp_raw(self):
        # read-only property
        return self._objpdnp.getChild(0)

    @property
    def objtrm(self):
        # read-only property
        # 20210328 comment out, allow None
        # if self._objtrm is None:
        #     raise ValueError("Only applicable to models with a trimesh!")
        return self._objtrm

    @property
    def localframe(self):
        # read-only property
        return self._localframe

    @property
    def volume(self):
        # read-only property
        if self._objtrm is None:
            raise ValueError("Only applicable to models with a trimesh!")
        return self._objtrm.volume

    def set_rgba(self, rgba):
        self._objpdnp.setColor(rgba[0], rgba[1], rgba[2], rgba[3])

    def get_rgba(self):
        return da.pdv4_to_npv4(
            self._objpdnp.getColor())  # panda3d.core.LColor -> LBase4F

    def clear_rgba(self):
        self._objpdnp.clearColor()

    def set_scale(self, scale=[1, 1, 1]):
        self._objpdnp.setScale(scale[0], scale[1], scale[2])
        self._objtrm.apply_scale(scale)

    def get_scale(self):
        return da.pdv3_to_npv3(self._objpdnp.getScale())

    def set_vert_size(self, size=.005):
        self.objpdnp_raw.setRenderModeThickness(size * 1000)

    def attach_to(self, obj):
        if isinstance(obj, ShowBase):
            # for rendering to base.render
            self._objpdnp.reparentTo(obj.render)
        elif isinstance(obj, StaticGeometricModel
                        ):  # prepared for decorations like local frames
            self._objpdnp.reparentTo(obj.objpdnp)
        elif isinstance(obj, mc.ModelCollection):
            obj.add_gm(self)
        else:
            print(
                "Must be ShowBase, modeling.StaticGeometricModel, GeometricModel, CollisionModel, or CollisionModelCollection!"
            )

    def detach(self):
        self._objpdnp.detachNode()

    def remove(self):
        self._objpdnp.removeNode()

    def show_localframe(self):
        self._localframe = gen_frame()
        self._localframe.attach_to(self)

    def unshow_localframe(self):
        if self._localframe is not None:
            self._localframe.remove()
            self._localframe = None

    def copy(self):
        return copy.deepcopy(self)
示例#4
0
class Player(DirectObject):
    
    #using this to be our player
    #define tehings like health in here
    #have to tie the camera to this
    #game manager ->player ->camera as far as instantiating goes
    
    def __init__(self):
        
        #Color values
        self.red = 0
        self.green = 1
        self.blue = 1
        self.oRed = 0
        self.oGreen = 1
        self.oBlue = 1
        
        #Limits use of weapons on menus
        self.canUseWeapons = True
        #Current overheat value
        self.overHeat = 0
        #Amount of overheat reduced per cycle
        self.overHeatCount = .1
        #Flag for overheated weapons
        self.isOverHeated = False

        #Flag for ccritical health 
        self.down = True

        #Create player node
        self.playerNode = NodePath('player')
        self.playerNode.reparentTo(render)
        self.playerNode.setPos(0,-30,30)
        self.playerNode.setScale(1.0)
        
        #Create player model and reparent it to playerNode
        self.playerModel = loader.loadModel("./resources/player")
        self.playerModel.reparentTo(self.playerNode)
        self.playerModel.setPos(0,0,2)

        #Create weapons and store them in a map
        self.rRifle = RecursionRifle(base.camera, len(base.projectileList))
        self.mhBlunder = MHB(base.camera, len(base.projectileList))
        self.kvDuals = KeyValue(base.camera, len(base.projectileList))
        self.weaponMap = {1:self.rRifle, 2:self.mhBlunder, 3:self.kvDuals}
        self.curWeapon = 1

        #Names of weapons for switcching images
        self.weaponNameMap = {1:"./resources/rrName.png", 2:"./resources/mhbName.png", 3:"./resources/kvdName.png"}
        
        #Load all weaponName images in so it doesn't stutter on swap
        self.weaponName = OnscreenImage(self.weaponNameMap[3])
        self.weaponName.setTransparency(True)
        self.weaponName.setImage(self.weaponNameMap[2])
        self.weaponName.setTransparency(True)
        self.weaponName.setImage(self.weaponNameMap[1])
        self.weaponName.setTransparency(True)
        self.weaponName.reparentTo(render2d)
       
        #Hide weapons currently not in use
        self.mhBlunder.hide()
        self.kvDuals.hide()

        #Weapon controls
        self.accept("mouse1", self.fireWeapon)
        self.accept("mouse3", self.swapWeapon)
        
        #Display HUD
        self.hud = OnscreenImage("resources/hud.png")
        self.hud.setTransparency(True)
        self.hud.reparentTo(render2d)
        
        # define player health here
        # try not to re-create the player object, will alter reset these values
        # alernatively, dump player stats off in save file before recreating
        print "Mod:",base.damageMod
        self.maxEnergy = 100/base.damageMod
        self.curEnergy = self.maxEnergy
        self.accept("cnode", self.hit)
        self.accept("pickuphealth", self.energyUpgrade)
        
        #Load font for text nodes
        font = loader.loadFont("./resources/ni7seg.ttf")

        #Text node for health bar
        self.healthLable = TextNode('health field name')
        self.healthLable.setFont(font)
        self.healthLable.setText("Abstraction")
        self.textNodePath = aspect2d.attachNewNode(self.healthLable)
        self.textNodePath.setScale(0.05)
        self.healthLable.setAlign(TextNode.ACenter)
        self.textNodePath.setPos(0, 0, .68)
        self.healthLable.setTextColor(self.red, self.green, self.blue, 1)

        #TextNode for enemy counter
        self.enemiesLeft = TextNode('monsters to kill')
        self.enemiesLeft.setFont(font)
        self.enemiesLeft.setText(str(len(base.enemyList)))
        self.texnp = aspect2d.attachNewNode(self.enemiesLeft)
        self.texnp.setScale(.1)
        self.texnp.setPos(-1.68, 0, -.75)
        self.enemiesLeft.setTextColor(1, 1, 0, 1)
        

        #Health bar
        self.bar = DirectWaitBar(text = "", value = self.curEnergy, range = self.maxEnergy, pos = (0,.4,.95), barColor = (self.red, self.green, self.blue, 1))
        self.bar.setScale(0.5)
        #Usage bar       
        self.usageBar = DirectWaitBar(text = "", value = self.overHeat, range = 100,  pos = (1.25, -.4, -.95), barColor =(1, 0, 0,1))
        self.usageBar.setScale(0.5)

        #Initailize player collision
        self.createColision()
        
        #Add player tasks
        base.taskMgr.add(self.updateUsage, "usagePaint", taskChain='Gametasks')
        base.taskMgr.add(self.hFlicker, "hflicker", taskChain='GameTasks')
        base.taskMgr.add(self.updateCount, "Ecount", taskChain='GameTasks')
        base.taskMgr.add(CameraMovement(self.playerModel).cameraControl, "cameraControl", taskChain='GameTasks')
        base.taskMgr.add(self.overHeatTask, "overHeatTask")
        base.taskMgr.add(self.killFloor, "Kill Floor") 

    #Deducts health and updates health bar
    def hit(self, damage):

        self.curEnergy = self.curEnergy-damage
        self.bar['value'] = self.curEnergy
        
        #If the player dies
        if self.curEnergy <= 0:

            #Hide player HUD elements
            self.hide()

            #Player can't use weapons while dead
            self.canUseWeapons = False

            #Request game over menu from fsm
            base.fsm.request('GameOver', 1)
    
    #Hides all visable elements attached to the player
    def hide(self):

        self.weaponMap[self.curWeapon].reticle.setScale(0) 
        self.weaponMap[self.curWeapon].curScale = 0
        self.weaponMap[self.curWeapon].step = False

        self.textNodePath.hide()
        self.texnp.hide()
        self.hud.setScale(0)
        self.weaponName.setScale(0)
        self.usageBar.hide()
        self.bar.hide()

    #Display all visable elements attached to the player
    def show(self):

        self.textNodePath.show()
        self.texnp.show()
        self.hud.setScale(1)
        self.weaponName.setScale(1)
        self.usageBar.show()
        self.bar.show()
        
        #Reset the reticle for the current weapon   
        if self.curWeapon == 1:
            
            self.weaponMap[1].reticle.setScale(.025)
            self.weaponMap[1].curScale = .025

        elif self.curWeapon == 2:
            
            self.weaponMap[2].reticle.setScale(.075)
            self.weaponMap[2].curScale = .075
        else:

            self.weaponMap[3].reticle.setScale(.025)
            self.weaponMap[3].curScale = .025


    # set the player health to a specific value        
    def adjustHealth(self, value):

        self.curEnergy = value
        self.bar['value'] = self.curEnergy
    
    #Update enemy counter
    def updateCount(self, task):
        self.enemiesLeft.setText(str(len(base.enemyList)))
        return task.cont 
    
    #Update usage bar and color
    def updateUsage(self, task):

        if self.curEnergy > 0:
            if self.overHeat < 50:
                self.usageBar['barColor'] = (.2, 1, .5, 1)
            elif self.overHeat >=50 and self.overHeat <70:
                self.usageBar['barColor'] = (1, 1, .2, 1)
            elif self.overHeat >= 70:
                self.usageBar['barColor'] = (1, 0, 0, 1)
            self.usageBar['value'] = self.overHeat
            if self.isOverHeated:
                self.usageBar['barColor'] = (1, 0, 0, 1)
            
        return task.cont

    #Swaps to next weapon in the map
    def swapWeapon(self):

        #If you're not in a menu
        if self.canUseWeapons:
            
            #Reset weapon delay
            self.weaponMap[self.curWeapon].resetWeapon
            
            #Change to next weapon and hide reticles
            if  self.curWeapon == 1:
            
                self.weaponName.setImage(self.weaponNameMap[2])
                self.weaponName.setTransparency(True)
                self.weaponMap[1].reticle.setScale(0)
                self.weaponMap[1].curScale = 0
                self.weaponMap[1].step = False
          
                self.rRifle.hide()
                self.mhBlunder.show()
            
                self.curWeapon = 2
                self.weaponMap[2].reticle.setScale(.075)
                self.weaponMap[2].curScale = .075
            elif self.curWeapon == 2:
            
                self.weaponName.setImage(self.weaponNameMap[3])
                self.weaponName.setTransparency(True)
                self.weaponMap[2].reticle.setScale(0)
                self.weaponMap[2].curScale = 0
                self.weaponMap[2].step = False
            
                self.mhBlunder.hide()
                self.kvDuals.show()
            
                self.curWeapon = 3
                self.weaponMap[3].reticle.setScale(.025)
                self.weaponMap[3].curScale = .025
            elif self.curWeapon == 3:

                self.weaponName.setImage(self.weaponNameMap[1])
                self.weaponName.setTransparency(True)
                self.weaponMap[3].reticle.setScale(0)
                self.weaponMap[3].curScale = 0
                self.weaponMap[3].step = False
            
                self.kvDuals.hide()
                self.rRifle.show()
           
                self.curWeapon = 1
                self.weaponMap[1].reticle.setScale(.025)
                self.weaponMap[1].curScale = .025
         
            base.taskMgr.remove("weaponDelay")
    
    #Fires current weapon
    def fireWeapon(self):

        #If the player is not in a menu
        if self.canUseWeapons:

            #If there isn't a weapon delay task add one
            if base.taskMgr.hasTaskNamed("weaponDelay") == False:
                
                #If your not overheated
                if not self.isOverHeated:

                    base.taskMgr.add(self.weaponMap[self.curWeapon].fire, "fire")

            #If you can shoot as defined by weapon delay
            elif self.weaponMap[self.curWeapon].canShoot() == True:

                #and if your not overheated
                if not self.isOverHeated:
                    
                    base.taskMgr.remove("weaponDelay")
                    base.taskMgr.add(self.weaponMap[self.curWeapon].fire, "fire")

    #Handles weapon overheat
    def overHeatTask(self, task):
        
        #Every cycle decrement your overheat by the specified amount
        self.overHeat -= self.overHeatCount
        
        #If you exceed the limit
        if self.overHeat >= 100:
            
            #Increase cooldown amount and set overheated flag
            self.overHeatCount = .5
            self.isOverHeated = True

        #If your are not overheated anymore reset not default cooldown values
        elif self.overHeat < 0:
            
            self.overHeatCount = .1
            self.overHeat = 0
            self.isOverHeated = False

        return task.cont

    #Initialize player collision
    def createColision(self):
        
        #Set up floor collision handler
        base.floor = CollisionHandlerGravity()
        base.floor.setGravity(9.8)

        #Create player collision node and add to traverser
        self.playerCollNodePath = self.initCollisionSphere(self.playerNode.getChild(0))
        base.cTrav.addCollider(self.playerCollNodePath, base.pusher)
        base.pusher.addCollider(self.playerCollNodePath, self.playerNode)
        
        # Create Floor Ray - for gravity / floor
        floorCollRayPath = self.initCollisionRay(1,-1) 
        base.floor.addCollider(floorCollRayPath, self.playerNode)
        base.cTrav.addCollider(floorCollRayPath, base.floor)
        floorCollRayPath.reparentTo(self.playerModel)

    #Initialize player collision sphere
    def initCollisionSphere(self, obj):
        
        # Create sphere and attach to player
        cNode = CollisionNode('player')

        cs = CollisionSphere(0, 0, 4, 2)
        cNodePath = obj.attachNewNode(CollisionNode('cnode'))
        cNodePath.node().addSolid(cs)
        cNodePath.show()
        
        return cNodePath
    
    #Attach player to a collision ray
    def initCollisionRay(self, originZ, dirZ):
        ray = CollisionRay(0,0,originZ,0,0,dirZ)
        collNode = CollisionNode('playerRay')
        collNode.addSolid(ray)
        collNode.setFromCollideMask(BitMask32.bit(1))
        collNode.setIntoCollideMask(BitMask32.allOff())
        collRayNP = self.playerNode.attachNewNode(collNode)
        collRayNP.show()
        return collRayNP

    #call this method when collide with a health upgrade
    def energyUpgrade(self):

        self.maxEnergy +=(10/base.damageMod)
        self.curEnergy = self.curEnergy+(10/base.damageMod)
        self.bar['range'] = self.maxEnergy
        self.adjustHealth(self.curEnergy)
    
    #Hurts player and resets the to the levels origin upon falliong in a pit.
    def killFloor(self, task):
        
	    z = int(self.playerNode.getPos()[2])

	    if(z < -7):

		    self.playerNode.setPos(0, 0, 6) #resets height
		    self.playerModel.setPos(base.xPos, base.yPos, base.zPos) #resets position
		    self.hit(10)
	    return task.cont

    #Makes the health bar flicker when your health is critical
    def hFlicker(self, task):

        if self.curEnergy <=30:
            if self.down:
                self.red = self.red+0.1
                self.blue = self.blue-.01
                self.green = self.green-.01
            else:
                self.red = self.red-0.1
                self.blue = self.green+0.1
                self.green = self.green+0.1
        else:
            self.red = self.oRed
            self.blue = self.oBlue
            self.green = self.oGreen
        if self.red >=1:
            self.down = False
        if self.red <=0:
            self.down = True
        self.healthLable.setTextColor(self.red, self.green, self.blue, 1)
        self.bar['barColor']=(self.red, self.green, self.blue, 1)
        return task.cont
        
    #Resets players health and signifies they have restarted level after death
    def resetEnergy(self):

        self.canUseWeapons = True
        self.curEnergy = self.maxEnergy
        self.adjustHealth(self.curEnergy)
示例#5
0
class CollisionChecker(object):
    """
    A fast collision checker that allows maximum 32 collision pairs
    author: weiwei
    date: 20201214osaka
    """
    def __init__(self, name="auto"):
        self.ctrav = CollisionTraverser()
        self.chan = CollisionHandlerQueue()
        self.np = NodePath(name)
        self.bitmask_list = [BitMask32(2**n) for n in range(31)]
        self._bitmask_ext = BitMask32(
            2**31)  # 31 is prepared for cd with external non-active objects
        self.all_cdelements = [
        ]  # a list of cdlnks or cdobjs for quick accessing the cd elements (cdlnks/cdobjs)

    def add_cdlnks(self, jlcobj, lnk_idlist):
        """
        The collision node of the given links will be attached to self.np, but their collision bitmask will be cleared
        When the a robot_s is treated as an obstacle by another robot_s, the IntoCollideMask of its all_cdelements will be
        set to BitMask32(2**31), so that the other robot_s can compare its active_cdelements with the all_cdelements.
        :param jlcobj:
        :param lnk_idlist:
        :return:
        author: weiwei
        date: 20201216toyonaka
        """
        for id in lnk_idlist:
            if jlcobj.lnks[id]['cdprimit_childid'] == -1:  # first time add
                cdnp = jlcobj.lnks[id]['collision_model'].copy_cdnp_to(
                    self.np, clearmask=True)
                self.ctrav.addCollider(cdnp, self.chan)
                self.all_cdelements.append(jlcobj.lnks[id])
                jlcobj.lnks[id]['cdprimit_childid'] = len(
                    self.all_cdelements) - 1
            else:
                raise ValueError("The link is already added!")

    def set_active_cdlnks(self, activelist):
        """
        The specified collision links will be used for collision detection with external obstacles
        :param activelist: essentially a from list like [jlchain.lnk0, jlchain.lnk1...]
                           the correspondent tolist will be set online in cd functions
                           TODO use all elements in self.all_cdnlks if None
        :return:
        author: weiwei
        date: 20201216toyonaka
        """
        for cdlnk in activelist:
            if cdlnk['cdprimit_childid'] == -1:
                raise ValueError(
                    "The link needs to be added to collider using the add_cdlnks function first!"
                )
            cdnp = self.np.getChild(cdlnk['cdprimit_childid'])
            cdnp.node().setFromCollideMask(self._bitmask_ext)

    def set_cdpair(self, fromlist, intolist):
        """
        The given collision pair will be used for self collision detection
        :param fromlist: [[bool, cdprimit_cache], ...]
        :param intolist: [[bool, cdprimit_cache], ...]
        :return:
        author: weiwei
        date: 20201215
        """
        if len(self.bitmask_list) == 0:
            raise ValueError("Too many collision pairs! Maximum: 29")
        allocated_bitmask = self.bitmask_list.pop()
        for cdlnk in fromlist:
            if cdlnk['cdprimit_childid'] == -1:
                raise ValueError(
                    "The link needs to be added to collider using the addjlcobj function first!"
                )
            cdnp = self.np.getChild(cdlnk['cdprimit_childid'])
            current_from_cdmask = cdnp.node().getFromCollideMask()
            new_from_cdmask = current_from_cdmask | allocated_bitmask
            cdnp.node().setFromCollideMask(new_from_cdmask)
        for cdlnk in intolist:
            if cdlnk['cdprimit_childid'] == -1:
                raise ValueError(
                    "The link needs to be added to collider using the addjlcobj function first!"
                )
            cdnp = self.np.getChild(cdlnk['cdprimit_childid'])
            current_into_cdmask = cdnp.node().getIntoCollideMask()
            new_into_cdmask = current_into_cdmask | allocated_bitmask
            cdnp.node().setIntoCollideMask(new_into_cdmask)

    def add_cdobj(self, objcm, rel_pos, rel_rotmat, into_list):
        """
        :return: cdobj_info, a dictionary that mimics a joint link; Besides that, there is an additional 'into_list'
                 key to hold into_list to easily toggle off the bitmasks.
        """
        cdobj_info = {}
        cdobj_info['collision_model'] = objcm  # for reversed lookup
        cdobj_info['gl_pos'] = objcm.get_pos()
        cdobj_info['gl_rotmat'] = objcm.get_rotmat()
        cdobj_info['rel_pos'] = rel_pos
        cdobj_info['rel_rotmat'] = rel_rotmat
        cdobj_info['into_list'] = into_list
        cdnp = objcm.copy_cdnp_to(self.np, clearmask=True)
        cdnp.node().setFromCollideMask(self._bitmask_ext)  # set active
        self.ctrav.addCollider(cdnp, self.chan)
        self.all_cdelements.append(cdobj_info)
        cdobj_info['cdprimit_childid'] = len(self.all_cdelements) - 1
        self.set_cdpair([cdobj_info], into_list)
        return cdobj_info

    def delete_cdobj(self, cdobj_info):
        """
        :param cdobj_info: an lnk-like object generated by self.add_objinhnd
        :param objcm:
        :return:
        """
        self.all_cdelements.remove(cdobj_info)
        cdnp_to_delete = self.np.getChild(cdobj_info['cdprimit_childid'])
        self.ctrav.removeCollider(cdnp_to_delete)
        this_cdmask = cdnp_to_delete.node().getFromCollideMask()
        for cdlnk in cdobj_info['into_list']:
            cdnp = self.np.getChild(cdlnk['cdprimit_childid'])
            current_into_cdmask = cdnp.node().getIntoCollideMask()
            new_into_cdmask = current_into_cdmask & ~this_cdmask
            cdnp.node().setIntoCollideMask(new_into_cdmask)
        cdnp_to_delete.detachNode()
        self.bitmask_list.append(this_cdmask)

    def is_collided(self,
                    obstacle_list=[],
                    otherrobot_list=[],
                    toggle_contact_points=False):
        """
        :param obstacle_list: staticgeometricmodel
        :param otherrobot_list:
        :return:
        """
        for cdelement in self.all_cdelements:
            pos = cdelement['gl_pos']
            rotmat = cdelement['gl_rotmat']
            cdnp = self.np.getChild(cdelement['cdprimit_childid'])
            cdnp.setPosQuat(da.npv3_to_pdv3(pos), da.npmat3_to_pdquat(rotmat))
            # print(da.npv3mat3_to_pdmat4(pos, rotmat))
            # print("From", cdnp.node().getFromCollideMask())
            # print("Into", cdnp.node().getIntoCollideMask())
        # print("xxxx colliders xxxx")
        # for collider in self.ctrav.getColliders():
        #     print(collider.getMat())
        #     print("From", collider.node().getFromCollideMask())
        #     print("Into", collider.node().getIntoCollideMask())
        # attach obstacles
        obstacle_parent_list = []
        for obstacle in obstacle_list:
            obstacle_parent_list.append(obstacle.objpdnp.getParent())
            obstacle.objpdnp.reparentTo(self.np)
        # attach other robots
        for robot in otherrobot_list:
            for cdnp in robot.cc.np.getChildren():
                current_into_cdmask = cdnp.node().getIntoCollideMask()
                new_into_cdmask = current_into_cdmask | self._bitmask_ext
                cdnp.node().setIntoCollideMask(new_into_cdmask)
            robot.cc.np.reparentTo(self.np)
        # collision check
        self.ctrav.traverse(self.np)
        # clear obstacles
        for i, obstacle in enumerate(obstacle_list):
            obstacle.objpdnp.reparentTo(obstacle_parent_list[i])
        # clear other robots
        for robot in otherrobot_list:
            for cdnp in robot.cc.np.getChildren():
                current_into_cdmask = cdnp.node().getIntoCollideMask()
                new_into_cdmask = current_into_cdmask & ~self._bitmask_ext
                cdnp.node().setIntoCollideMask(new_into_cdmask)
            robot.cc.np.detachNode()
        if self.chan.getNumEntries() > 0:
            collision_result = True
        else:
            collision_result = False
        if toggle_contact_points:
            contact_points = [
                da.pdv3_to_npv3(cd_entry.getSurfacePoint(base.render))
                for cd_entry in self.chan.getEntries()
            ]
            return collision_result, contact_points
        else:
            return collision_result

    def show_cdprimit(self):
        """
        Copy the current nodepath to base.render to show collision states
        TODO: maintain a list to allow unshow
        :return:
        author: weiwei
        date: 20220404
        """
        # print("call show_cdprimit")
        snp_cpy = self.np.copyTo(base.render)
        for cdelement in self.all_cdelements:
            pos = cdelement['gl_pos']
            rotmat = cdelement['gl_rotmat']
            cdnp = snp_cpy.getChild(cdelement['cdprimit_childid'])
            cdnp.setPosQuat(da.npv3_to_pdv3(pos), da.npmat3_to_pdquat(rotmat))
            cdnp.show()

    def disable(self):
        """
        clear pairs and nodepath
        :return:
        """
        for cdelement in self.all_cdelements:
            cdelement['cdprimit_childid'] = -1
        self.all_cdelements = []
        for child in self.np.getChildren():
            child.removeNode()
        self.bitmask_list = list(range(31))
示例#6
0
class PopupMenu(DirectObject):
    '''
  A class to create a popup or context menu.
  Features :
    [1] it's destroyed by pressing ESCAPE, or LMB/RMB click outside of it
    [2] menu item's command is executed by pressing ENTER/RETURN or SPACE when
        it's hilighted
    [3] you can use arrow UP/DOWN to navigate
    [4] separator lines
    [5] menu item image
    [6] menu item hotkey
        If there are more than 1 item using the same hotkey,
        those items will be hilighted in cycle when the hotkey is pressed.
    [7] shortcut key text at the right side of menu item
    [8] multiple lines item text
    [9] menu item can have sub menus
    [10] it's offscreen-proof, try to put your pointer next to screen edge or corner
        before creating it
  '''
    grayImages = {}  # storage of grayed images,

    # so the same image will be converted to grayscale only once

    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
        if isinstance(scale, (float, int)):
            scale = (scale, 1.0, scale)
        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[0] * .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[0]
        LH = (fr[3] - fr[2]) * self.itemHeight * self.scale[2]
        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[2] /
                         LH) / (1. + self.baselineOffset)
        baselineToBot = LH / self.scale[2] - 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[2] - bgPad
        maxWidth = .1 / self.scale[0]
        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 |= isinstance(imgPath, bool) or 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[0])
                    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,
                        commandButtons=[DGG.LMB, DGG.RMB],
                        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 isinstance(imgPath, bool):
                    if imgPath:
                        if disabled:
                            fg = textColorDisabled
                        else:
                            fg = textColorReady
                        tick = NodePath(
                            OnscreenText(
                                parent=b,
                                text=u"\u2714",
                                font=self.font,
                                scale=1,
                                fg=fg,
                                align=TextNode.ALeft,
                            ))
                        tick.setX(-2 * imageHalfHeight - leftPad)
                elif imgPath:
                    img = loader.loadTexture(imgPath, okMissing=True)
                    if img is not None:
                        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[2] * EoLcount
            else:  # SEPARATOR LINE
                z += LH - separatorHalfHeight - baselineToBot * self.scale[2]
                LSseparator.moveTo(0, 0, z)
                LSseparator.drawTo(self.scale[0] * .5, 0, z)
                LSseparator.drawTo(self.scale[0], 0, z)
                z -= separatorHalfHeight + baselineToTop * self.scale[2]
        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[
            0], maxWidth + bgPad / self.scale[0], minZ - bgPad / self.scale[
                2], top + bgPad / self.scale[2]
        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[0]
        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 = clamp(-.98, .98 - maxWidthR2D, self.mpos[0] - maxWidthR2D * .5)
        minZ = (-.98 if self.minZ is None else self.minZ)
        z = clamp(
            minZ +
            menuBorderDims[2] * self.scale[2] * 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)

    def __offItem(self, crap):
        self.sel = -1
        self.__cancelSubmenuCreation()

    def __hoverOnItem(self, idx, menu, crap):
        self.sel = idx
        self.__cancelSubmenuCreation()
        if self.BT.getPrefix()==self.BTprefix or \
             (self.submenu and self.submenuIdx==idx):
            self.__cancelSubmenuRemoval()
        if menu:
            if not (self.submenu and self.submenuIdx == idx):
                #~ if self.selByKey:
                #~ self.selByKey=False
                #~ self.__createSubmenu(idx,menu)
                #~ else:
                taskMgr.doMethodLater(.3,
                                      self.__createSubmenu,
                                      self.submenuCreationTaskName,
                                      extraArgs=[idx, menu])
        else:
            taskMgr.doMethodLater(.5,
                                  self.__removeSubmenu,
                                  self.submenuRemovalTaskName,
                                  extraArgs=[])

    def __cancelSubmenuCreation(self):
        taskMgr.removeTasksMatching('createSubMenu-*')

    def __createSubmenu(self, idx, menu):
        self.__cancelSubmenuCreation()
        self.__removeSubmenu()
        self.submenu = PopupMenu(items=menu,
                                 parent=self.parent,
                                 buttonThrower=self.BT,
                                 font=self.font,
                                 baselineOffset=self.baselineOffset,
                                 scale=self.scale,
                                 itemHeight=self.itemHeight,
                                 leftPad=self.leftPad,
                                 separatorHeight=self.separatorHeight,
                                 underscoreThickness=self.underscoreThickness,
                                 BGColor=self.BGColor,
                                 BGBorderColor=self.BGBorderColor,
                                 separatorColor=self.separatorColor,
                                 frameColorHover=self.frameColorHover,
                                 frameColorPress=self.frameColorPress,
                                 textColorReady=self.textColorReady,
                                 textColorHover=self.textColorHover,
                                 textColorPress=self.textColorPress,
                                 textColorDisabled=self.textColorDisabled,
                                 minZ=self.minZ,
                                 useMouseZ=False)
        self.submenuIdx = idx
        self.submenu.parentMenu = self
        if self.menu.getBinName():
            self.submenu.menu.setBin(self.menu.getBinName(),
                                     self.menu.getBinDrawOrder() + 1)
        sb3 = self.submenu.menu.getTightBounds()
        sb = sb3[1] - sb3[0]
        b3 = self.menu.getTightBounds()
        x = b3[1][0]
        if render2d.getRelativePoint(self.parent, Point3(x + sb[0], 0,
                                                         0))[0] > .98:
            x = b3[0][0] - sb[0]
        if render2d.getRelativePoint(self.parent, Point3(x, 0, 0))[0] < -.98:
            x = self.parent.getRelativePoint(render2d, Point3(-.98, 0, 0))[0]
        item = self.menu.find('**/*-pg%s' % (self.firstButtonIdx + idx))
        z = self.parent.getRelativePoint(
            item, Point3(0, 0,
                         item.node().getFrame()[3]))[2] + self.bgPad
        self.submenu.menu.setPos(x, 0, max(z, self.submenu.menu.getZ()))


#       self.submenu.menu.setPos(x,0,z)

    def __nextItem(self):
        if self.numItems:
            self.sel = clamp(0, self.numItems - 1, self.sel + 1)
            self.__putPointerAtItem()
            self.selByKey = True

    def __prevItem(self):
        if self.numItems:
            self.sel = clamp(0, self.numItems - 1,
                             (self.sel -
                              1) if self.sel > -1 else self.numItems - 1)
            self.__putPointerAtItem()
            self.selByKey = True

    def __putPointerAtItem(self):
        item = self.menu.find('**/*-pg%s' % (self.firstButtonIdx + self.sel))
        fr = item.node().getFrame()
        c = Point3(.5 * (fr[0] + fr[1]), 0, .5 * (fr[2] + fr[3]))
        cR2D = render2d.getRelativePoint(item, c)
        x, y = int(base.win.getXSize() * .5 * (cR2D[0] + 1)), int(
            base.win.getYSize() * .5 * (-cR2D[2] + 1))
        if '__origmovePointer' in base.win.DtoolClassDict:
            base.win.DtoolClassDict['__origmovePointer'](base.win, 0, x, y)
        else:
            base.win.movePointer(0, x, y)

    def __processHotkey(self, hotkey):
        itemsIdx = self.hotkeys[hotkey]
        if len(itemsIdx) == 1 and type(
                self.itemCommand[itemsIdx[0]][0]) not in SEQUENCE_TYPES:
            self.__runCommand(*self.itemCommand[itemsIdx[0]])
        else:
            if self.sel in itemsIdx:
                idx = itemsIdx.index(self.sel) + 1
                idx %= len(itemsIdx)
                self.sel = itemsIdx[idx]
            else:
                self.sel = itemsIdx[0]
            self.selByKey = True
            # if it's already there, putting the pointer doesn't trigger the 'enter'
            # event, so just bypass it
            if not (self.submenu and self.submenuIdx==self.sel) and\
                 type(self.itemCommand[itemsIdx[0]][0]) in SEQUENCE_TYPES:
                self.__createSubmenu(self.sel,
                                     self.itemCommand[itemsIdx[0]][0])
            self.__putPointerAtItem()

    def __doRunCommand(self, f, args):
        self.destroy(delParents=True)
        f(*args)

    def __runCommand(self, f, args):
        if callable(f):
            # must be done at next frame, so shortcut key event won't bleed to the scene
            taskMgr.doMethodLater(.01,
                                  self.__doRunCommand,
                                  'run menu command',
                                  extraArgs=[f, args])

    def __runSelItemCommand(self):
        if self.sel == -1: return
        self.__runCommand(*self.itemCommand[self.sel])

    def __cancelSubmenuRemoval(self):
        taskMgr.removeTasksMatching('removeSubMenu-*')

    def __removeSubmenu(self):
        self.__cancelSubmenuRemoval()
        if self.submenu:
            self.submenu.destroy()

    def destroy(self, delParents=False):
        self.__cancelSubmenuCreation()
        self.__removeSubmenu()
        self.subMenu = None
        self.ignoreAll()
        self.menu.removeNode()
        #       if self.origBTprefix.find('menu-')==-1:
        #          taskMgr.step()
        self.BT.setPrefix(self.origBTprefix)
        messenger.send(self.BTprefix + 'destroyed')
        if delParents and self.parentMenu:
            parent = self.parentMenu
            while parent.parentMenu:
                parent = parent.parentMenu
            parent.destroy()
        if self.parentMenu:
            self.parentMenu.submenuIdx = None
            self.parentMenu = None
        if callable(self.onDestroy):
            self.onDestroy()