def enableMixing(self): if self.__mixer != self.animationMixer: self.disableMixing() self.__mixer = self.animationMixer if self.__mixer: Actor.enableBlend(self) self.__mixer.cleanup()
def enableMixing(self): if self._UsesAnimationMixer__mixer != self.animationMixer: self.disableMixing() self._UsesAnimationMixer__mixer = self.animationMixer if self._UsesAnimationMixer__mixer: Actor.enableBlend(self) self._UsesAnimationMixer__mixer.cleanup()
class World: def __init__(self): base.disableMouse() base.camera.setPos(0, -5, 1) self.setupLight() self.kid = Actor( "../Models/Kid.egg", { "Walk": "../Animations/Walk.egg", "Thoughtful": "../Animations/Thoughtful.egg" }) self.kid.reparentTo(render) self.kid.enableBlend() self.kid.setControlEffect("Walk", 1) self.kid.setControlEffect("Thoughtful", 1) self.kid.loop("Thoughtful") self.kid.loop("Walk") self.kid.setH(180) def setupLight(self): primeL = DirectionalLight("prime") primeL.setColor(VBase4(.6, .6, .6, 1)) # Creates the primary directional light and sets it's color to 60%. self.dirLight = render.attachNewNode(primeL) self.dirLight.setHpr(45, -60, 0) # Assigns the light to a nodePath and rotates that nodePath to aim the light. render.setLight(self.dirLight) # Sets the directional light to illuminate everything attached to the render node. ambL = AmbientLight("amb") ambL.setColor(VBase4(.2, .2, .2, 1)) self.ambLight = render.attachNewNode(ambL) # Creates an ambient light to fill in the shadows and sets it's color to 20%. # also places it in a NodePath. render.setLight(self.ambLight) # Sets the ambient light to illuminate the scene. return
class Person(DirectObject): person = None personActor = None state_key = None def __init__(self): self.load_char() self.catch_events() def catch_events(self): pass def load_char(self): self.person = render.attachNewNode('persona') self.personActor = Actor('resources/eggs/personagem.egg', {'idle':'resources/eggs/personagem-parado', 'run' :'resources/eggs/personagem-correr', 'jump':'resources/eggs/personagem-pular'} ) #self.personActor.setScale(.3) self.personActor.loop('idle') self.personActor.reparentTo(self.person) self.personActor.setPos(0,0,1.5) self.state_key = { 'right':0, 'left':0,'jump': False, 'fall':0, 'speed_walk' : 0.8, 'speed_side' : 0.8,'up' : 0, 'down': 0, 'moving' : False, 'walk_right' : 0, 'walk_left' : 0, 'crouch' : 0,'crouching' : False, 'aim' : 0 } self.personActor.enableBlend() self.personActor.loop('idle') self.personActor.loop('run') self.personActor.loop('jump') self.person.setPos(0,30,0) self.person.setH(0) taskMgr.add( self.walk, 'movePerson' ) taskMgr.add( self.crouch, 'crouchPerson') def walk(self,task): elapsed = globalClock.getDt() #roda o sprite em seu propio eixo if (self.state_key['left']!=0): self.person.setH(self.person.getH() + ((elapsed*300) * self.state_key['speed_side'] )) if (self.state_key['right']!=0): self.person.setH(self.person.getH() - ((elapsed*300) * self.state_key['speed_side'] )) if (self.state_key['up']!=0): self.person.setY(self.person, -((elapsed*25) * self.state_key['speed_walk'] )) if (self.state_key['down']!=0): self.person.setY(self.person, +((elapsed*25) * self.state_key['speed_walk'] )) if (self.state_key['walk_right'] != 0): self.person.setX(self.person, - ((elapsed*25) * self.state_key['speed_walk'] )) if (self.state_key['walk_left'] != 0): self.person.setX(self.person, + ((elapsed*25) * self.state_key['speed_walk'] )) if ((self.state_key['up'] == 1) or (self.state_key['down'] == 1)): if (not self.state_key['moving']): self.state_key['moving'] = True else: if (self.state_key['moving']): self.state_key['moving'] = False self.animation() return task.cont def animation(self): if (self.state_key['crouching'] == True): blendAnim = [0, 0, 0, 1.0] elif (self.state_key['moving'] == False) and (self.state_key['jump'] == False): blendAnim = [1.0, 0.0, 0.0, 0.0] elif (self.state_key['moving'] == True) and (self.state_key['jump'] == False): blendAnim = [0.0, 1.0, 0.0, 0.0] elif (self.state_key['jump'] == True): blendAnim = [0.0, 0.0, 1.0, 0.0] else: blendAnim = [1.0, 0.0, 0.0, 0.0] self.personActor.setControlEffect( 'idle', blendAnim[0] ) self.personActor.setControlEffect( 'run', blendAnim[1] ) self.personActor.setControlEffect( 'jump', blendAnim[2] ) self.personActor.setControlEffect( 'jump', blendAnim[3] ) def attack(self): pass def jump(self): pass def aim(self): pass def shoot(self): pass def crouch(self, task): if self.state_key['crouch'] == 1: self.state_key['crouching'] = True else: self.state_key['crouching'] = False self.animation() return task.cont
class World(DirectObject): #necessary to accept events def __init__(self): print "START THE GAAAAAME!" self.accept("space", self.StartGame) self.titleScreen = OnscreenImage(image = '../images/Title.png', scale = (1.3333333,1,1)) self.titleScreen.setTransparency(TransparencyAttrib.MAlpha) self.loadSound() def StartGame(self): self.titleScreen.destroy() #turn off default mouse control base.disableMouse() #add update tasks to taskManager taskMgr.add(self.keyEvents, "keyEventTask") taskMgr.add(self.checkPipes2, "checkPipesTask") taskMgr.add(self.loopMusic, "loopMusicTask") taskMgr.add(self.updateTimer, "timeTask") #Enables particle effects base.enableParticles() camera.setPosHpr(0, -18, 3, 0, 0, 0) self.keyMap = {"moveLeft":0, "moveRight":0, "moveUp":0, "moveDown":0, "drop":0, \ "actionLeft":0, "actionRight":0, "actionDown":0, "actionUp":0} self.prevTime = 0 #Sets initial collision state, will change name later self.numCollisions = 0 self.currentPipe = False self.loadModels() self.setupLights() #self.loadSound() self.setupCollisions() self.accept("escape", sys.exit) #message name, function to call, (optional) list of arguments to that function #useful interval methods: # loop, pause, resume, finish # start can optionally take arguments: starttime, endtime, playrate #for "continuous" control self.accept("space", self.setKey, ["drop", 0]) #disables debug stopping self.accept("space-up", self.setKey, ["drop", 0]) self.accept("arrow_up", self.setKey, ["actionUp", 1]) self.accept("arrow_down", self.setKey, ["actionDown", 1]) self.accept("arrow_left", self.setKey, ["actionLeft", 1]) self.accept("arrow_right", self.setKey, ["actionRight", 1]) self.accept("arrow_up-up", self.setKey, ["actionUp", 0]) self.accept("arrow_down-up", self.setKey, ["actionDown", 0]) self.accept("arrow_left-up", self.setKey, ["actionLeft", 0]) self.accept("arrow_right-up", self.setKey, ["actionRight", 0]) self.accept("w", self.setKey, ["moveUp", 1]) self.accept("s", self.setKey, ["moveDown", 1]) self.accept("a", self.setKey, ["moveLeft", 1]) self.accept("d", self.setKey, ["moveRight", 1]) self.accept("w-up", self.setKey, ["moveUp", 0]) self.accept("s-up", self.setKey, ["moveDown", 0]) self.accept("a-up", self.setKey, ["moveLeft", 0]) self.accept("d-up", self.setKey, ["moveRight", 0]) self.accept("spider-and-tube_collision", self.pipeCollide) self.DefaultTime = 1#.8 self.TimeLeft = self.DefaultTime self.TimerGoing = False #Set game over thing self.GameOver = False #Set gameplay variables to keep track of self.gameScore = 0 self.playerStability = 100 self.currentActionCommand = ActionCommand(0,"") self.currentActionCommand.isEmpty() #Create HUD and add it to task thing self.Hud = GameHUD() taskMgr.add(self.update_game_Hud, "updateHudTask") #Fog and changing background myFog = Fog("Fog Name") f = 0.05 myFog.setColor(f,f,f) myFog.setExpDensity(.01) render.setFog(myFog) base.setBackgroundColor(f,f,f) def updateTimer(self,task): if self.TimerGoing == True: self.TimeLeft = self.TimeLeft - globalClock.getDt() if self.TimeLeft <= 0: print"-----------TIME UP!!!!!!!!!---------" print self.currentActionCommand.getCommand() if self.currentActionCommand.isEmpty() == False: self.playerStability = self.playerStability - 10 if self.playerStability <= 0: if self.GameOver == False: #REPLACE TITLE.PNG WITH THE GAME OVER IMAGE!!!!!! self.gameOverScreen = OnscreenImage(image = '../images/GameOver.png', scale = (1.3333333,1,1)) self.gameOverScreen.setTransparency(TransparencyAttrib.MAlpha) self.GameOver = True self.Hud.hide = True print "***Blanked Action Command***" self.currentActionCommand = ActionCommand(0,"") self.TimeLeft = self.DefaultTime self.TimerGoing = False return Task.cont def setKey(self, key, value): self.keyMap[key] = value def loadModels(self): """loads initial models into the world""" #load pipes self.numPipes = 6 #number appearing on the stage at any given time self.numGenericTypes = 5 #number of normal pipe models self.numSpecialTypes = 4 #number of 'broken' pipe models self.pipeGenericBag = GrabBag(self.numGenericTypes) self.pipeSpecialBag = GrabBag(self.numSpecialTypes) self.pipeList = [] self.pipeInterval = 20.25*3.05#*.98 #length*timesLonger*overlapConstant self.pipeDepth = 0 self.pipeCycle = 1 #create initial pipes for i in range(self.numPipes): self.createPipe2(i) #print self.pipeList[i].model.getY() #Enable initial shaders self.pipeList[0].addShader() self.pipeList[1].addShader() #load spider #self.spider = loader.loadModel("../models/spider.egg") self.spider = Actor("../models/spider.egg", {"aniFall":"../models/animation_fall cycle.egg", "aniFix1":"../models/animation_fixattack1.egg",} ) #setup animation self.spider.enableBlend() self.spider.setControlEffect('aniFall', 1) self.spider.loop("aniFall") self.spider.reparentTo(render) self.spider.setShaderAuto() self.spider.setScale(.045) self.spider.setZ(4.25) self.spider.setH(180) self.spider.setP(-65) #load back panal self.backPanal = loader.loadModel("../models/infinity.egg") self.backPanal.reparentTo(render) self.backPanal.setZ(4.25) self.backPanal.setY(self.pipeInterval*5*.90) def loadSound(self): self.openingMusic = loader.loadSfx("../audio/opening.wav") self.mainLoopMusic = loader.loadSfx("../audio/mainLoop.wav") SoundInterval(self.openingMusic).start() #repair sound effects self.fixSound1 = loader.loadSfx("../audio/repair1.wav") self.fixSound2 = loader.loadSfx("../audio/repair2.wav") self.fixSound3 = loader.loadSfx("../audio/repair3.wav") self.fixSound4 = loader.loadSfx("../audio/repair4.wav") self.explosionSound = loader.loadSfx("../audio/explosion.wav") def setupLights(self): """loads initial lighting""" self.ambientLight = AmbientLight("ambientLight") #for setting colors, alpha is largely irrelevant self.ambientLight.setColor((.25, .25, .35, 1.0)) #self.ambientLight.setColor((.25, .25, .25, 1.0)) #create a NodePath, and attach it directly into the scene self.ambientLightNP = render.attachNewNode(self.ambientLight) #the node that calls setLight is what's illuminated by the given light #you can use clearLight() to turn it off render.setLight(self.ambientLightNP) self.dirLight = DirectionalLight("dirLight") self.dirLight.setColor((.7, .5, .5, 1)) #self.dirLight.setColor((.7, .7, 1, 1)) self.dirLightNP = render.attachNewNode(self.dirLight) self.dirLightNP.setHpr(0, -25, 0) render.setLight(self.dirLightNP) def setupCollisions(self): #make a collision traverser, set it to default base.cTrav = CollisionTraverser() self.cHandler = CollisionHandlerEvent() #set the pattern for the event sent on collision # "%in" is substituted with the name of the into object self.cHandler.setInPattern("%fn-and-%in") cSphere = CollisionSphere((0,-5,0), 30) cNode = CollisionNode("spider") cNode.addSolid(cSphere) #spider is *only* a from object cNode.setIntoCollideMask(BitMask32.allOff()) self.spiderCollisionNode = self.spider.attachNewNode(cNode) #self.spiderCollisionNode.show() base.cTrav.addCollider(self.spiderCollisionNode, self.cHandler) def pipeCollide(self, cEntry): self.numCollisions += 1 print self.numCollisions #print cEntry.getIntoNodePath().getParent().getParent().getName() #print "\n\n\n" modelKey = cEntry.getIntoNodePath().getParent().getParent().getKey() self.currentPipe = self.getPipe(modelKey) if self.currentPipe.actionCommand.isEmpty() == False: print "Pipe Action Command = " + str(self.currentPipe.actionCommand.getCommand()) self.currentActionCommand = ActionCommand(2,"",self.currentPipe.actionCommand.getCommand()) print "Player Action Command = " + str(self.currentActionCommand.getCommand()) self.currentPipe.actionCommand.blankCommand() print "Player Action Command = " + str(self.currentActionCommand.getCommand()) print "------!!!!!!!!!!!!!------" if self.TimerGoing == False: self.TimeLeft = self.DefaultTime self.TimerGoing = True def getPipe(self, model): modelPath = model print modelPath print "CollideKey: " + str(modelPath) #KeyTestDebug for i in range(self.pipeList.__len__()): print "Key "+ str(i) +":" + str(self.pipeList[i].key) for i in range(self.pipeList.__len__()): print "Testing Key: " + str(self.pipeList[i].key) if self.pipeList[i].key == modelPath: return(self.pipeList[i]) def update_game_Hud(self,task): self.gameScore = self.gameScore + globalClock.getDt() tempScore = int(self.gameScore * 100) self.Hud.updateHud(self.playerStability,tempScore,self.currentActionCommand.getOriginal()) return Task.cont
class Tricker(object): def __init__(self): ## tricker-model taken from tutorial files from # https://www.digitaltutors.com/tutorial/1478-Animating-an-Acrobatic-Fight-Scene-in-Maya self.actor = Actor( "tp/models/tricker-model", { "gainer": "tp/anims/tricker-gainer", "gainer_bad": "tp/anims/tricker-gainer-bad", "gswitch": "tp/anims/tricker-gswitch", "gswitch_bad": "tp/anims/tricker-gswitch-bad", "btwist": "tp/anims/tricker-btwist", "btwist_bad": "tp/anims/tricker-btwist", "cork": "tp/anims/tricker-cork", "cork_bad": "tp/anims/tricker-cork-bad", "doublecork": "tp/anims/tricker-dubcork", "doublecork_bad": "tp/anims/tricker-dubcork", "fall_swing": "tp/anims/tricker-fall-left", "initial_swing": "tp/anims/tricker-initial-swing", "end_swing": "tp/anims/tricker-end-swing", "fall_reversal": "tp/anims/tricker-fall-right", "raiz": "tp/anims/tricker-raiz", "raiz_bad": "tp/anims/tricker-raiz-bad", "fiveForty": "tp/anims/tricker-540", "fiveForty_bad": "tp/anims/tricker-540-bad" }) #saveDict contains all info to be saved to json self.saveDict = { 'name': '', 'level': 0, 'totalStam': 100, 'skills': { "gainer": 1, "gswitch": 1, "btwist": 1, "cork": 1, "doublecork": 1, "raiz": 1, "fiveForty": 1 } } self.updateAttributes() # trickMap is different - this shit maps trick names to their classes # in order to get class data just given an animation name self.trickMap = { 'gainer': self.gainer, 'gswitch': self.gswitch, 'btwist': self.btwist, 'cork': self.cork, 'doublecork': self.doublecork, 'raiz': self.raiz, 'fiveForty': self.fiveForty } # runtime traits, to be reset with reset function # NOTE: You MUST add any vals here to the reset function below self.prevTrick = None self.stamina = self.totalStam self.grade = '' self.timing = '' self.score = 0 self.comboLength = 0 self.comboEnded = False self.comboContinuing = False self.falling = False self.midTrick = False def comboHasEnded(self): return self.comboEnded def hasName(self): return self.name != '' def isFalling(self): return self.falling def isMidTrick(self): return self.midTrick def getName(self): if self.hasName(): return self.name else: return "NewPlayer" def getLevel(self): return str(self.saveDict['level']) def getTotalStam(self): return str(self.totalStam) def getSaveDict(self): return self.saveDict def getSkillDict(self): return self.skillDict def getComboLength(self): return str(int(self.comboLength)) def getScore(self): return str(int(self.score)) def getTiming(self): return self.timing def updateStamina(self, stamCost): self.stamina -= stamCost def updateComboLength(self): self.comboLength += 1 def updateScore(self, trick, goodPercentage, comboLength): b = trick.getDifficulty() * 100 score = b + (b * goodPercentage) + (b * (comboLength / 5)) self.score += score def getTimingBarPercentage(self): currAnim = self.actor.getCurrentAnim() if currAnim and 'fall' not in currAnim and 'initial' not in currAnim and 'end' not in currAnim: trick = self.trickMap[currAnim.split('_')[0]] sweetspot = trick.getSweetSpot() currFrame = self.actor.getCurrentFrame() dist = abs(sweetspot - currFrame) eMargin = trick.getDuration() * .2 / trick.getDifficulty() if dist > eMargin: gp = 0 else: gp = 1 - (dist / eMargin) return gp else: return 0 def getTrueStam(self): return self.stamina def stamPercentage(self): sp = self.stamina / self.totalStam if sp < 0: sp = 0 return sp def getGrade(self): return self.grade def increaseSkill(self, trick, grade): if grade == 0: b = 2 elif grade == 1: b = 1.66 elif grade == 2: b = 1.33 elif grade == 3: b = 1 elif grade == 4: b = 1 # This line makes the exp curve and prevents leveling over 100 exp = b - math.log(self.saveDict['skills'][str(trick)]) if exp < 0: exp = 0 self.saveDict['skills'][str(trick)] += exp self.updateAttributes() """ debug animation procedure: comment out: return under if self.comboEnded in self.tryTrick both moveInterval.start() in self.doTrickTask """ def tryTrick(self, trick, taskMgr): if self.midTrick: print("already received an input") return self.midTrick = True if self.comboEnded: print("combo ended - no more tricking 4 u") return if self.falling: print("can't trick once you've fallen boi") return if self.stamina <= 0: self.comboEnded = True print("no stamina to throw trick!! ending combo") return self.comboContinuing = True distance = (0, 0, 0) self.grade = 0 currAnim = self.actor.getCurrentAnim() goodPercentage = trick.getGoodPercentage(self.grade) if self.prevTrick: distance = self.prevTrick.getDistance() if currAnim and self.prevTrick: if (self.prevTrick.getExitTransition() != trick.getEntryTransition()): self.comboEnded = True print("invalid transition - ended combo") return distance = self.prevTrick.getDistance() currFrame = self.actor.getCurrentFrame(currAnim) numFrames = self.actor.getNumFrames(currAnim) framesLeft = numFrames - currFrame (self.grade, self.timing) = self.prevTrick.getGrade(currFrame) if self.grade == 4: self.falling = True print("Combo failed - falling") self.prevTrick = None stamCost = trick.getStamCost(self.grade) self.updateStamina(stamCost) goodPercentage = trick.getGoodPercentage(self.grade) # 0.06 is the time it takes for 2 frames - smooth blending delayTime = framesLeft / 30 - 0.09 taskMgr.doMethodLater( delayTime, self.doTrickTask, 'doTrick', extraArgs=[str(trick), goodPercentage, distance, taskMgr], appendTask=True) else: stamCost = trick.getStamCost(self.grade) self.updateStamina(stamCost) if trick.entryTransition == 'swing': self.actor.play('initial_swing') distance = (-2, 2, 0) taskMgr.doMethodLater( 21 / 30, self.doTrickTask, 'doTrick', extraArgs=[str(trick), goodPercentage, distance, taskMgr], appendTask=True) else: taskMgr.add( self.doTrickTask, 'doTrick', extraArgs=[str(trick), goodPercentage, distance, taskMgr], appendTask=True) if self.getTrueStam() < 0: self.falling = True return ("Ran out of stamina mid-trick - falling!") if not self.falling: self.updateScore(trick, goodPercentage, self.comboLength) self.updateComboLength() self.prevTrick = trick # this is tricking - you still learn from your falls! self.increaseSkill(trick, self.grade) def doTrickTask(self, animation, goodPercentage, distance, taskMgr, task): print("goodp:", goodPercentage) self.actor.setPos(self.actor, distance) badAnim = str(animation + "_bad") if not self.isFalling(): self.actor.enableBlend() self.actor.setControlEffect(badAnim, 1 - goodPercentage) self.actor.setControlEffect(animation, goodPercentage) self.actor.play(badAnim) self.actor.play(animation) self.actor.disableBlend() self.comboContinuing = False self.midTrick = False taskMgr.add(self.checkTrickStateTask, 'checkTrickState', extraArgs=[animation], appendTask=True) elif self.isFalling(): trick = self.trickMap[animation] fallDist = trick.getDistance() exitTrans = trick.getExitTransition() fallingAnim = "fall_" + exitTrans fallStartFrame = trick.getDuration() - trick.getSweetSpot() regFrames = self.actor.getNumFrames(animation) - fallStartFrame fallSeq = Sequence( self.actor.actorInterval(badAnim, endFrame=regFrames), Func(self.playFall, fallingAnim, fallDist, exitTrans)) fallSeq.start() # if moveInterval: moveInterval.start() return Task.done def playFall(self, fallingAnim, distance, exitTrans): # if fallingAnim == 'fall_swing': # self.actor.enableBlend() # self.actor.setControlEffect(badAnim, .5) # self.actor.setControlEffect(fallingAnim, .5) # self.actor.play(badAnim, fromFrame=startFrame) # self.actor.play(fallingAnim) # self.actor.disableBlend() # elif fallingAnim == 'fall_reversal': if exitTrans == 'swing': self.actor.setPos(self.actor, distance) self.actor.play(fallingAnim) def playSwingExit(self, distance): self.actor.setPos(self.actor, distance) self.actor.play('end_swing') def checkTrickStateTask(self, animation, task): # has to be -1 otherwise the currFrame never gets to the last frame. IDK why totalFrames = self.actor.getNumFrames(animation) - 1 currFrame = self.actor.getCurrentFrame(animation) if self.comboContinuing or self.falling: print("comboContinuing or falling") return Task.done self.comboContinuing = False if currFrame == totalFrames: self.comboEnded = True print("no input received - combo ended") trick = self.trickMap[animation] distance = trick.getDistance() if trick.getExitTransition() == 'swing': self.playSwingExit(distance) return Task.done return Task.cont def reset(self): self.prevTrick = None self.stamina = 100 self.grade = None self.timing = '' self.score = 0 self.comboLength = 0 self.comboEnded = False self.comboContinuing = False self.falling = False self.midTrick = False def setName(self, name): self.saveDict['name'] = name self.updateAttributes() def loadToSaveDict(self, indata): self.saveDict = indata self.updateAttributes() def updateAttributes(self): self.name = self.saveDict['name'] self.totalStam = self.saveDict['totalStam'] self.skillDict = self.saveDict['skills'] # set tricker's level based on proficiency in all skills numTricks = totalSP = 0 for trick in self.skillDict: numTricks += 1 totalSP += self.skillDict[trick] self.level = int(totalSP / numTricks) self.saveDict['level'] = self.level # Load tricks self.gainer = Gainer(self) self.gswitch = Gswitch(self) self.btwist = Btwist(self) self.cork = Cork(self) self.doublecork = DoubleCork(self) self.raiz = Raiz(self) self.fiveForty = FiveForty(self)
class Vehicle: def __init__(self, app, model_name): model_file_name = 'assets/cars/{}/{}.bam'.format( model_name, model_name) self.app = app def animation_path(model, animation): base_path = 'assets/cars/animations/{}/{}-{}.bam' return base_path.format(model, model, animation) self.model = Actor( model_file_name, { # AIRBRAKE: 'assets/cars/animations/{}-{}.bam'.format(model_name, AIRBRAKE), # AIRBRAKE: animation_path(model_name, AIRBRAKE), # STABILIZER_FINS: animation_path(model_name, STABILIZER_FINS), }) self.model.enableBlend() self.model.setControlEffect(AIRBRAKE, 1) self.model.setControlEffect(STABILIZER_FINS, 1) # FIXME: This code fails due to a bug in Actor # airbrake_joints = [joint.name # for joint in self.model.getJoints() # if joint.name.startswith(AIRBRAKE) # ] # self.model.makeSubpart(AIRBRAKE, airbrake_joints) # stabilizer_joints = [joint.name # for joint in self.model.getJoints() # if joint.name.startswith(STABILIZER_FINS) # ] # self.model.makeSubpart(STABILIZER_FINS, stabilizer_joints) puppet = self.app.loader.load_model(model_file_name) puppet.find("**/armature").hide() puppet.reparentTo(self.model) # Get the vehicle data self.vehicle_data = VehicleData(puppet, model_name, 'cars') # Configure the physics node self.physics_node = BulletRigidBodyNode('vehicle') self.physics_node.set_friction(self.vehicle_data.friction) self.physics_node.set_linear_sleep_threshold(0) self.physics_node.set_angular_sleep_threshold(0) self.physics_node.setCcdMotionThreshold(1e-7) self.physics_node.setCcdSweptSphereRadius(0.5) self.physics_node.setMass(self.vehicle_data.mass) shape = BulletConvexHullShape() for geom_node in self.model.find_all_matches("**/+GeomNode"): for geom in geom_node.node().get_geoms(): vertices = GeomVertexReader(geom.get_vertex_data(), 'vertex') while not vertices.is_at_end(): v_geom = vertices.getData3f() v_model = self.model.get_relative_point(geom_node, v_geom) shape.add_point(v_model) self.physics_node.add_shape(shape) self.vehicle = NodePath(self.physics_node) self.vehicle.set_collide_mask(CM_VEHICLE | CM_COLLIDE) self.model.reparent_to(self.vehicle) # Navigational aids self.target_node = self.app.loader.load_model('models/zup-axis') self.target_node.reparent_to(self.model) self.target_node.set_scale(1) self.target_node.set_render_mode_wireframe() self.target_node.hide() self.delta_node = self.app.loader.load_model('models/smiley') self.delta_node.set_pos(1, 10, 1) self.delta_node.reparent_to(base.cam) self.delta_node.hide() self.airbrake_state = 0.0 self.airbrake_factor = 0.5 self.airbrake_speed = 1 / self.vehicle_data.airbrake_duration self.stabilizer_fins_state = 0.0 self.stabilizer_fins_speed = 1 / self.vehicle_data.stabilizer_fins_duration self.centroid = base.loader.load_model('models/smiley') self.centroid.reparent_to(self.vehicle) self.centroid.hide() # Gyro sound sound_file = 'assets/cars/{}/{}.wav'.format( model_name, GYROSCOPE_SOUND, ) sound = base.audio3d.load_sfx(sound_file) self.model.set_python_tag(GYROSCOPE_SOUND, sound) base.audio3d.attach_sound_to_object(sound, self.model) sound.set_volume(0) sound.set_play_rate(0) sound.set_loop(True) sound.play() # Thruster limiting self.thruster_state = 0.0 self.thruster_heat = 0.0 for repulsor in self.vehicle_data.repulsor_nodes: self.add_repulsor(repulsor, model_name) for thruster in self.vehicle_data.thruster_nodes: self.add_thruster(thruster, model_name) # ECU data storage from frame to frame self.last_flight_height = None # FIXME: Move into a default controller self.inputs = { # Repulsors REPULSOR_ACTIVATION: 0.0, ACCELERATE: 0.0, TURN: 0.0, STRAFE: 0.0, HOVER: 0.0, FULL_REPULSORS: False, # Gyro ACTIVE_STABILIZATION_ON_GROUND: PASSIVE, ACTIVE_STABILIZATION_CUTOFF_ANGLE: PASSIVE, ACTIVE_STABILIZATION_IN_AIR: PASSIVE, TARGET_ORIENTATION: Vec3(0, 0, 0), # Thrust THRUST: 0.0, # Air foils AIRBRAKE: 0.0, STABILIZER_FINS: 0.0, } self.sensors = {} self.commands = {} def np(self): return self.vehicle def place(self, spawn_point): # FIXME: Pass a root node to __init__ instead self.vehicle.reparent_to(self.app.environment.model) connector = self.model.find("**/" + SPAWN_POINT_CONNECTOR) self.vehicle.set_hpr(-connector.get_hpr(spawn_point)) self.vehicle.set_pos(-connector.get_pos(spawn_point)) self.app.environment.add_physics_node(self.physics_node) def set_inputs(self, inputs): self.inputs = inputs def add_repulsor(self, node, model_name): ground_contact = self.app.loader.load_model( "assets/effect/repulsorhit.egg") ground_contact.set_scale(1) ground_contact.reparent_to(self.app.render) node.set_python_tag('ray_end', ground_contact) sound_file = 'assets/cars/{}/{}.wav'.format( model_name, REPULSOR_SOUND, ) sound = base.audio3d.load_sfx(sound_file) node.set_python_tag(REPULSOR_SOUND, sound) base.audio3d.attach_sound_to_object(sound, node) sound.set_volume(0) sound.set_play_rate(0) sound.set_loop(True) sound.play() def add_thruster(self, node, model_name): node.set_python_tag(THRUSTER_POWER, 0.0) node.set_python_tag(THRUSTER_OVERHEATED, False) # Basic jet sound sound_file = 'assets/cars/{}/{}.wav'.format( model_name, THRUSTER_SOUND, ) sound = base.audio3d.load_sfx(sound_file) node.set_python_tag(THRUSTER_SOUND, sound) base.audio3d.attach_sound_to_object(sound, node) sound.set_volume(0) sound.set_play_rate(0) sound.set_loop(True) sound.play() # Overheating sound sound_file = 'assets/cars/{}/{}.wav'.format( model_name, THRUSTER_OVERHEAT_SOUND, ) sound = base.audio3d.load_sfx(sound_file) node.set_python_tag(THRUSTER_OVERHEAT_SOUND, sound) base.audio3d.attach_sound_to_object(sound, node) def game_loop(self): self.gather_sensors() self.ecu() self.apply_air_drag() self.apply_repulsors() self.apply_gyroscope() self.apply_thrusters() self.apply_airbrake() self.apply_stabilizer_fins() def gather_sensors(self): # Gather data repulsor ray collisions with ground repulsor_data = [] for node in self.vehicle_data.repulsor_nodes: data = RepulsorData() repulsor_data.append(data) max_distance = node.get_python_tag(ACTIVATION_DISTANCE) data.position = node.get_pos(self.vehicle) data.direction = self.app.render.get_relative_vector( node, Vec3(0, 0, -max_distance), ) # FIXME: `self.app.environment.physics_world` is ugly. feeler = self.app.environment.physics_world.ray_test_closest( base.render.get_relative_point(self.vehicle, data.position), base.render.get_relative_point(self.vehicle, data.position + data.direction), CM_TERRAIN, ) if feeler.has_hit(): data.active = True data.fraction = feeler.get_hit_fraction() data.contact = self.vehicle.get_relative_point( self.app.render, feeler.get_hit_pos(), ) else: data.active = False data.fraction = 1.0 # Find the local ground's normal vector local_up = False contacts = [ data.contact for data in repulsor_data if data.active and self.inputs[REPULSOR_ACTIVATION] ] if len(contacts) > 2: local_up = True target_mode = self.inputs[ACTIVE_STABILIZATION_ON_GROUND] # We can calculate a local up as the smallest base vector of the # point cloud of contacts. centroid = reduce(lambda a, b: a + b, contacts) / len(contacts) covariance = [ [c.x, c.y, c.z] for c in [contact - centroid for contact in contacts] ][0:3] # We need exactly 3, no PCA yet. :( eigenvalues, eigenvectors = numpy.linalg.eig( numpy.array(covariance)) # FIXME: These few lines look baaaad... indexed_eigenvalues = enumerate(eigenvalues) def get_magnitude(indexed_element): index, value = indexed_element return abs(value) sorted_indexed_eigenvalues = sorted( indexed_eigenvalues, key=get_magnitude, ) # The smallest eigenvalue leads to the ground plane's normal up_vec_idx, _ = sorted_indexed_eigenvalues[0] up_vec = VBase3(*eigenvectors[:, up_vec_idx]) # Point into the upper half-space if up_vec.z < 0: up_vec *= -1 # Calculate the forward of the centroid centroid_forward = Vec3(0, 1, 0) - up_vec * (Vec3(0, 1, 0).dot(up_vec)) # FIXME: Huh? forward_planar = centroid_forward - up_vec * ( centroid_forward.dot(up_vec)) # Now let's orient and place the centroid self.centroid.set_pos(self.vehicle, (0, 0, 0)) self.centroid.heads_up(forward_planar, up_vec) self.centroid.set_pos(self.vehicle, centroid) # Flight height for repulsor attenuation flight_height = -self.centroid.get_z(self.vehicle) if self.last_flight_height is not None: climb_speed = (flight_height - self.last_flight_height) / globalClock.dt else: climb_speed = 0 self.last_flight_height = flight_height else: local_up = False self.last_flight_height = None flight_height = 0.0 climb_speed = 0.0 # Air drag movement = self.physics_node.get_linear_velocity() drag_coefficient = self.vehicle_data.drag_coefficient drag_area = self.vehicle_data.drag_area + \ self.vehicle_data.airbrake_area * self.airbrake_state + \ self.vehicle_data.stabilizer_fins_area * self.stabilizer_fins_state air_density = self.app.environment.env_data.air_density air_speed = -self.vehicle.get_relative_vector( base.render, movement, ) # drag_force = 1/2*drag_coefficient*mass_density*area*flow_speed**2 result = Vec3(air_speed) result = (result**3) / result.length() result.componentwise_mult(drag_area) result.componentwise_mult(self.vehicle_data.drag_coefficient) drag_force = result * 1 / 2 * air_density self.sensors = { CURRENT_ORIENTATION: self.vehicle.get_hpr(self.app.render), CURRENT_MOVEMENT: movement, CURRENT_ROTATION: self.physics_node.get_angular_velocity(), LOCAL_DRAG_FORCE: drag_force, REPULSOR_DATA: repulsor_data, LOCAL_UP: local_up, FLIGHT_HEIGHT: flight_height, CLIMB_SPEED: climb_speed, } def ecu(self): repulsor_target_angles = self.ecu_repulsor_reorientation() repulsor_activation, delta_height, projected_delta_height, \ power_frac_needed = self.ecu_repulsor_activation() gyro_rotation = self.ecu_gyro_stabilization() thruster_activation = [ self.inputs[THRUST] for _ in self.vehicle_data.thruster_nodes ] airbrake = self.inputs[AIRBRAKE] stabilizer_fins = self.inputs[STABILIZER_FINS] self.commands = { # Steering commands REPULSOR_TARGET_ORIENTATIONS: repulsor_target_angles, REPULSOR_ACTIVATION: repulsor_activation, GYRO_ROTATION: gyro_rotation, THRUSTER_ACTIVATION: thruster_activation, AIRBRAKE: airbrake, STABILIZER_FINS: stabilizer_fins, # ECU data output; Interesting numbers we found along the way. HEIGHT_OVER_TARGET: delta_height, HEIGHT_OVER_TARGET_PROJECTED: projected_delta_height, REPULSOR_POWER_FRACTION_NEEDED: power_frac_needed, } def ecu_repulsor_reorientation(self): # Calculate effective repulsor motion blend values accelerate = self.inputs[ACCELERATE] turn = self.inputs[TURN] strafe = self.inputs[STRAFE] hover = self.inputs[HOVER] length = sum([abs(accelerate), abs(turn), abs(strafe), hover]) if length > 1: accelerate /= length turn /= length strafe /= length hover /= length # Split the turn signal into animation blend factors if turn > 0.0: turn_left = 0.0 turn_right = turn else: turn_left = -turn turn_right = 0.0 # Blend the repulsor angle repulsor_target_angles = [] for node in self.vehicle_data.repulsor_nodes: acc_angle = -(node.get_python_tag(ACCELERATE)) * accelerate turn_left_angle = node.get_python_tag(TURN_LEFT) * turn_left turn_right_angle = node.get_python_tag(TURN_RIGHT) * turn_right strafe_angle = node.get_python_tag(STRAFE) * strafe hover_angle = node.get_python_tag(HOVER) * hover angle = acc_angle + turn_left_angle + turn_right_angle + \ strafe_angle + hover_angle repulsor_target_angles.append(angle) return repulsor_target_angles def ecu_repulsor_activation(self): # Do we know how high we are flying? if self.sensors[LOCAL_UP]: tau = self.inputs[TARGET_FLIGHT_HEIGHT_TAU] # What would we be at in tau seconds if we weren't using repulsors? flight_height = self.sensors[FLIGHT_HEIGHT] target_flight_height = self.inputs[TARGET_FLIGHT_HEIGHT] delta_height = flight_height - target_flight_height gravity_z = self.centroid.get_relative_vector( self.app.render, self.app.environment.physics_world.get_gravity(), ).get_z() # Since gravity is an acceleration gravity_h = 1 / 2 * gravity_z * tau**2 climb_rate = self.sensors[CLIMB_SPEED] * tau projected_delta_height = delta_height + gravity_h + climb_rate # Are we sinking? if projected_delta_height <= 0: # Our projected height will be under our target height, so we # will need to apply the repulsors to make up the difference. # How much climb can each repulsor provide at 100% power right # now? max_powers = [ node.get_python_tag(FORCE) for node in self.vehicle_data.repulsor_nodes ] transferrable_powers = [ max_power * cos(0.5 * pi * data.fraction) for max_power, data in zip( max_powers, self.sensors[REPULSOR_DATA], ) ] angle_ratios = [ cos(node.get_quat(self.vehicle).get_angle_rad()) for node in self.vehicle_data.repulsor_nodes ] angled_powers = [ power * ratio for power, ratio in zip(transferrable_powers, angle_ratios) ] # We don't want to activate the repulsors unevenly, so we'll # have to go by the weakest link. total_angled_power = min(angled_powers) * len(angled_powers) # How high can we climb under 100% repulsor power? max_climb = 1 / 2 * total_angled_power * tau**2 / self.vehicle_data.mass # The fraction of power needed to achieve the desired climb power_frac_needed = -projected_delta_height / max_climb # ...and store it. repulsor_activation = [ power_frac_needed for _ in self.vehicle_data.repulsor_nodes ] else: # We're not sinking. repulsor_activation = [ 0.0 for _ in self.vehicle_data.repulsor_nodes ] power_frac_needed = 0.0 else: # We do not have ground contact. repulsor_activation = [ 0.0 for _ in self.vehicle_data.repulsor_nodes ] delta_height = 0.0 projected_delta_height = 0.0 power_frac_needed = 0.0 # The driver gives 100% repulsor power, no matter how high we are, or # whether we even have ground contact. if self.inputs[FULL_REPULSORS]: repulsor_activation = [ self.inputs[REPULSOR_ACTIVATION] for _ in self.vehicle_data.repulsor_nodes ] return repulsor_activation, delta_height, projected_delta_height,\ power_frac_needed def ecu_gyro_stabilization(self): # Active stabilization and angular dampening # FIXME: Get from self.inputs tau = 0.2 # Seconds until target orientation is reached if self.sensors[LOCAL_UP]: target_mode = self.inputs[ACTIVE_STABILIZATION_ON_GROUND] else: target_mode = self.inputs[ACTIVE_STABILIZATION_IN_AIR] if target_mode == TO_HORIZON: # Stabilize to the current heading, but in a horizontal # orientation self.target_node.set_hpr( self.app.render, self.vehicle.get_h(), 0, 0, ) elif target_mode == TO_GROUND: # Stabilize towards the local up self.target_node.set_hpr(self.centroid, (0, 0, 0)) if target_mode != PASSIVE: xyz_driver_modification = self.inputs[TARGET_ORIENTATION] hpr_driver_modification = VBase3( xyz_driver_modification.z, xyz_driver_modification.x, xyz_driver_modification.y, ) self.target_node.set_hpr( self.target_node, hpr_driver_modification, ) # Now comes the math. orientation = self.vehicle.get_quat(self.app.render) target_orientation = self.target_node.get_quat(self.app.render) delta_orientation = target_orientation * invert(orientation) self.delta_node.set_quat(invert(delta_orientation)) delta_angle = delta_orientation.get_angle_rad() if abs(delta_angle) < (pi / 360 * 0.1) or isnan(delta_angle): delta_angle = 0 axis_of_torque = VBase3(0, 0, 0) else: axis_of_torque = delta_orientation.get_axis() axis_of_torque.normalize() axis_of_torque = self.app.render.get_relative_vector( self.vehicle, axis_of_torque, ) if delta_angle > pi: delta_angle -= 2 * pi # If the mass was standing still, this would be the velocity that # has to be reached to achieve the targeted orientation in tau # seconds. target_angular_velocity = axis_of_torque * delta_angle / tau else: # `elif target_mode == PASSIVE:`, since we might want an OFF mode # Passive stabilization, so this is the pure commanded impulse target_angular_velocity = self.app.render.get_relative_vector( self.vehicle, self.inputs[TARGET_ORIENTATION] * tau / pi, ) # But we also have to cancel out the current velocity for that. angular_velocity = self.physics_node.get_angular_velocity() countering_velocity = -angular_velocity # An impulse of 1 causes an angular velocity of 2.5 rad on a unit mass, # so we have to adjust accordingly. target_impulse = target_angular_velocity / 2.5 * self.vehicle_data.mass countering_impulse = countering_velocity / 2.5 * self.vehicle_data.mass # Now just sum those up, and we have the impulse that needs to be # applied to steer towards target. impulse = target_impulse + countering_impulse return impulse def apply_air_drag(self): drag_force = self.sensors[LOCAL_DRAG_FORCE] global_drag_force = base.render.get_relative_vector( self.vehicle, drag_force, ) if not isnan(global_drag_force.x): self.physics_node.apply_central_impulse( global_drag_force * globalClock.dt, ) def apply_repulsors(self): dt = globalClock.dt repulsor_data = zip(self.vehicle_data.repulsor_nodes, self.sensors[REPULSOR_DATA], self.commands[REPULSOR_ACTIVATION], self.commands[REPULSOR_TARGET_ORIENTATIONS]) for node, data, activation, angle in repulsor_data: active = data.active frac = data.fraction # Repulse in current orientation if active and activation: if activation > 1.0: activation = 1.0 if activation < 0.0: activation = 0.0 # Repulsor power at zero distance base_strength = node.get_python_tag(FORCE) # Effective fraction of repulsors force transfer_frac = cos(0.5 * pi * frac) # Effective repulsor force strength = base_strength * activation * transfer_frac # Resulting impulse impulse_dir = Vec3(0, 0, 1) impulse_dir_world = self.app.render.get_relative_vector( node, impulse_dir, ) impulse = impulse_dir_world * strength # Apply repulsor_pos = node.get_pos(self.vehicle) # FIXME! The position at which an impulse is applied seems to be # centered around node it is applied to, but offset in the world # orientation. So, right distance, wrong angle. This is likely a # bug in Panda3D's Bullet wrapper. Or an idiosyncracy of Bullet. self.physics_node.apply_impulse( impulse * dt, self.app.render.get_relative_vector( self.vehicle, repulsor_pos, ), ) # Contact visualization node max_distance = node.get_python_tag(ACTIVATION_DISTANCE) contact_distance = -impulse_dir_world * max_distance * frac contact_node = node.get_python_tag('ray_end') contact_node.set_pos( node.get_pos(self.app.render) + contact_distance, ) #contact_node.set_hpr(node, 0, -90, 0) # Look towards repulsor #contact_node.set_hpr(0, -90, 0) # Look up contact_node.set_hpr(0, -90, contact_node.getR() + 4) # Look up and rotate contact_node.set_scale(1 - frac) contact_node.show() # Sound sound = node.get_python_tag(REPULSOR_SOUND) sound.set_volume(activation * 20) sound.set_play_rate((1 + activation / 2) * 2) else: node.get_python_tag('ray_end').hide() sound = node.get_python_tag(REPULSOR_SOUND) sound.set_volume(0.0) sound.set_play_rate(0.0) # Reorient old_hpr = node.get_python_tag(REPULSOR_OLD_ORIENTATION) want_hpr = VBase3(angle.z, angle.x, angle.y) delta_hpr = want_hpr - old_hpr max_angle = node.get_python_tag(REPULSOR_TURNING_ANGLE) * dt if delta_hpr.length() > max_angle: delta_hpr = delta_hpr / delta_hpr.length() * max_angle new_hpr = old_hpr + delta_hpr node.set_hpr(new_hpr) node.set_python_tag(REPULSOR_OLD_ORIENTATION, new_hpr) def apply_gyroscope(self): impulse = self.commands[GYRO_ROTATION] # Clamp the impulse to what the "motor" can produce. max_impulse = self.vehicle_data.max_gyro_torque if impulse.length() > max_impulse: clamped_impulse = impulse / impulse.length() * max_impulse else: clamped_impulse = impulse self.physics_node.apply_torque_impulse(clamped_impulse) impulse_ratio = clamped_impulse.length() / max_impulse sound = self.model.get_python_tag(GYROSCOPE_SOUND) sound.set_volume(0.5 * impulse_ratio) sound.set_play_rate(0.9 + 0.1 * impulse_ratio) def apply_thrusters(self): dt = globalClock.dt thruster_data = zip( self.vehicle_data.thruster_nodes, self.commands[THRUSTER_ACTIVATION], ) for node, thrust in thruster_data: if self.thruster_heat >= 1.0: thrust = 0.0 current_power = node.get_python_tag(THRUSTER_POWER) # Adjust thrust to be within bounds of thruster's ability to adjust # thrust. ramp_time = node.get_python_tag(THRUSTER_RAMP_TIME) ramp_step = (1 / ramp_time) * globalClock.dt thrust = adjust_within_limit(thrust, current_power, ramp_step) node.set_python_tag(THRUSTER_POWER, thrust) max_force = node.get_python_tag(FORCE) real_force = max_force * thrust # FIXME: See repulsors above for the shortcoming that this suffers thruster_pos = base.render.get_relative_vector( self.vehicle, node.get_pos(self.vehicle), ) thrust_direction = self.app.render.get_relative_vector( node, Vec3(0, 0, 1)) self.physics_node.apply_impulse( thrust_direction * real_force * dt, thruster_pos, ) heating = node.get_python_tag(THRUSTER_HEATING) cooling = node.get_python_tag(THRUSTER_COOLING) effective_heating = -cooling + (heating - cooling) * thrust self.thruster_heat += effective_heating * dt if self.thruster_heat < 0.0: self.thruster_heat = 0.0 # Sound sound = node.get_python_tag(THRUSTER_SOUND) sound.set_volume(thrust * 5) sound.set_play_rate((1 + thrust / 20) / 3) was_overheated = node.get_python_tag(THRUSTER_OVERHEATED) is_overheated = self.thruster_heat > 1.0 if is_overheated and not was_overheated: sound = node.get_python_tag(THRUSTER_OVERHEAT_SOUND) sound.set_volume(5) sound.play() node.set_python_tag(THRUSTER_OVERHEATED, True) if not is_overheated: node.set_python_tag(THRUSTER_OVERHEATED, False) def apply_airbrake(self): # Animation and state update only, since the effect is in air drag dt = globalClock.dt target = self.commands[AIRBRAKE] max_movement = self.airbrake_speed * dt # Clamp change to available speed delta = target - self.airbrake_state if abs(delta) > max_movement: self.airbrake_state += copysign(max_movement, delta) else: self.airbrake_state = target if self.airbrake_state > 1.0: self.airbrake_state = 1.0 if self.airbrake_state < 0.0: self.airbrake_state = 0.0 #self.model.pose(AIRBRAKE, self.airbrake_state, partName=AIRBRAKE) self.model.pose(AIRBRAKE, self.airbrake_state) def apply_stabilizer_fins(self): # Animation and state update only, since the effect is in air drag dt = globalClock.dt target = self.commands[STABILIZER_FINS] max_movement = self.stabilizer_fins_speed * dt # Clamp change to available speed delta = target - self.stabilizer_fins_state if abs(delta) > max_movement: self.stabilizer_fins_state += copysign(max_movement, delta) else: self.stabilizer_fins_state = target if self.stabilizer_fins_state > 1.0: self.stabilizer_fins_state = 1.0 if self.stabilizer_fins_state < 0.0: self.stabilizer_fins_state = 0.0 self.model.pose( STABILIZER_FINS, self.stabilizer_fins_state, #partName=STABILIZER_FINS, ) # FIXME: Implement stabilizing effect def shock(self, x=0, y=0, z=0): self.physics_node.apply_impulse( Vec3(0, 0, 0), Vec3(random(), random(), random()) * 10, ) self.physics_node.apply_torque_impulse(Vec3(x, y, z))
class Character(Shooter, Unit): """A game character. Represents a unit controlled by a player. Args: id_ (int): This character unique id. name (str): This character name. class_ (str): This character class. sex (str): This character gender. team (crew.Crew): The Crew object. desc (dict): Optional. This character description (used for loading character from a saved game). """ def __init__(self, id_, name, class_, sex, team, desc=None): Unit.__init__( self, "character_" + str(id_), class_, CLASSES[sex + "_" + class_] ) Shooter.__init__(self) self._team = team self._current_pos = None self._current_anim = None self._idle_seq = None self._cohesion_ball = None self._is_stunned = False self._is_stimulated = False self._health_bar = None self.inhale = 15 self.damage_range = [5, 8] self.clear_damage = [5, 8] self.effects = {} self._yeah_snds = [] self.name = name self.sex = sex if sex == "male": self.heshe = base.labels.PRONOUNS[0] # noqa: F821 self.hisher = base.labels.PRONOUNS[1] # noqa: F821 self.himher = base.labels.PRONOUNS[2] # noqa: F821 else: self.heshe = base.labels.PRONOUNS[3] # noqa: F821 self.hisher = base.labels.PRONOUNS[4] # noqa: F821 self.himher = base.labels.PRONOUNS[5] # noqa: F821 if desc: self._energy = desc["energy"] self.is_diseased = desc["is_diseased"] self.get_well_score = desc["get_well_score"] self.traits = desc["traits"] self.disabled_traits = desc["disabled_traits"] if self.is_diseased: taskMgr.doMethodLater( # noqa: F821 60, self.get_well, self.id + "_get_well" ) else: self._energy = 100 self.is_diseased = False self.get_well_score = 0 self.disabled_traits = [] self.traits = [] traits = copy.copy(base.labels.TRAITS) # noqa: F821 for _ in range(random.randint(0, 2)): self.traits.append(random.choice(take_random(traits))) @property def clear_delay(self): """ Delay between this character death and clearing the character object. Returns: float: Seconds to wait before clearing. """ return 3.5 @property def damage(self): """This character one-time calculated damage. Returns: float: Damage one-time made by this character. """ return random.uniform(*self.damage_range) * self.damage_factor @property def damage_factor(self): """This character damage factor. The damage factor depends on cohesion with another character on the same part of the Train. Returns: float: Damage one-time made by this character. """ factor = self._team.calc_cohesion_factor(self.current_part.chars, self) if ( # Loner base.labels.TRAITS[4][1] in self.traits # noqa: F821 and len(self.current_part.chars) == 1 ): return factor * 1.3 return factor @property def description(self): """This character state in saveable form. Used for saving the character. Returns: dict: This character description. """ return { "id": int(self.id.split("_")[1]), "name": self.name, "sex": self.sex, "class": self.class_, "health": self.health, "energy": self.energy, "place": self.current_part.name, "traits": self.traits, "disabled_traits": self.disabled_traits, "is_diseased": self.is_diseased, "get_well_score": self.get_well_score, } @property def energy(self): """This character energy. Returns: int: This character energy points. """ return self._energy @energy.setter def energy(self, value): """This character energy setter. Args: value (int): New energy value. """ self._energy = min(max(value, 0), 80 if self.is_diseased else 100) @property def shooting_speed(self): """Delay between shots of this unit. Returns: float: Delay between shots in seconds. """ if base.labels.TRAITS[0][0] in self.traits: # noqa: F821 # Fast hands return 1.1 + random.uniform(0.1, 0.8) if base.labels.TRAITS[0][1] in self.traits: # noqa: F821 # Snail return 2 + random.uniform(0.1, 1.1) return 1.7 + random.uniform(0.1, 0.9) @property def statuses(self): """Return strings describing the current character status. Returns: list: Status description lines. """ statuses = [] if base.world.sun.is_dark: # noqa: F821 if base.labels.TRAITS[1][0] in self.traits: # noqa: F821 statuses.append(base.labels.STATUSES[0]) # noqa: F821 elif base.train.lights_on: # noqa: F821 if "Floodlights" not in base.train.upgrades: # noqa: F821 statuses.append(base.labels.STATUSES[1]) # noqa: F821 else: statuses.append(base.labels.STATUSES[2]) # noqa: F821 if self.energy <= 95: statuses.append( base.labels.STATUSES[3].format((100 - self.energy) // 5) # noqa: F821 ) if not base.world.is_in_city and self.current_part is not None: # noqa: F821 factor = round(self.damage_factor, 2) if factor != 1: statuses.append(base.labels.STATUSES[4].format(factor)) # noqa: F821 if self.health < 50 and base.labels.TRAITS[2][1] in self.traits: # noqa: F821 statuses.append(base.labels.STATUSES[5]) # noqa: F821 if self.is_diseased: statuses.append(base.labels.STATUSES[6]) # noqa: F821 if ( base.labels.TRAITS[6][1] in self.traits # noqa: F821 and base.train.ctrl.current_speed > 0.75 # noqa: F821 ): statuses.append(base.labels.STATUSES[7]) # noqa: F821 return statuses[:4] @property def tooltip(self): """Tooltip to show on this character mouse pointing. Returns: str: This character name. """ return self.name def attack(self, enemy_unit): """Make the given enemy unit this character's target. Args: enemy_unit (units.enemy.EnemyUnit): Enemy unit to attack. """ if enemy_unit in self.current_part.enemies: self._target = enemy_unit def prepare(self): """Load the character model and positionate it. Tweak collision solid and sounds. """ animations = { name: address(self.class_ + "-" + name) for name in ( "die", "gun_up", "incline1", "release_gun", "stand_and_aim", "stand", "surrender", "tread1", "turn_head1", "stunned", "cough", "celebrate", ) } self.model = Actor(address(self.sex + "_" + self.class_), animations) self.model.enableBlend() self.model.setControlEffect("stand", 1) self.model.setPlayRate(0.8, "stand_and_aim") self.model.setPlayRate(0.6, "stand") self.model.loop("stand") self._current_anim = "stand" self._health_bar = HealthBar(self) base.team.init_relations(self) # noqa: F821 taskMgr.doMethodLater( # noqa: F821 random.randint(40, 60), self._idle_animation, self.id + "_idle_anim" ) self._col_node = self._init_col_node( NO_MASK, MOUSE_MASK, CollisionCapsule(0, 0, 0, 0, 0, 0.035, 0.035) ) self.shot_snd = self._set_shoot_snd(self.class_data["shot_snd"]) self._cough_snd = base.sound_mgr.loadSfx( # noqa: F821 "sounds/{sex}_cough.ogg".format(sex=self.sex) ) base.sound_mgr.attachSoundToObject(self._cough_snd, self.model) # noqa: F821 self._die_snd = base.sound_mgr.loadSfx( # noqa: F821 "sounds/combat/{sex}_death.ogg".format(sex=self.sex) ) base.sound_mgr.attachSoundToObject(self._die_snd, self.model) # noqa: F821 for i in range(1, 4): yeah_snd1 = base.sound_mgr.loadSfx( # noqa: F821 "sounds/{sex}_yes{num}.ogg".format(sex=self.sex, num=str(i)) ) base.sound_mgr.attachSoundToObject(yeah_snd1, self.model) # noqa: F821 self._yeah_snds.append(yeah_snd1) if self.class_ == "soldier": z = 0.064 if self.sex == "male" else 0.062 elif self.class_ == "raider": z = 0.047 elif self.class_ == "anarchist": z = 0.06 if self.sex == "male" else 0.057 self._shoot_anim = self._set_shoot_anim( (0.004, 0.045, z), 97, self.class_data["shots_num"] ) taskMgr.doMethodLater( # noqa: F821 self.class_data["energy_spend"], self._reduce_energy, self.id + "_reduce_energy", ) self._prepare_cohesion_particles() self._prepare_auras() def _prepare_cohesion_particles(self): """Prepare cohesion skills particle effects.""" effect = ParticleEffect() effect.loadConfig("effects/recall_the_past.ptf") self.effects["recall_the_past"] = {"length": 0.7, "effect": effect} effect = ParticleEffect() effect.loadConfig("effects/not_leaving_ours.ptf") self.effects["not_leaving_ours"] = {"length": 2, "effect": effect} effect = ParticleEffect() effect.loadConfig("effects/common_rage.ptf") self.effects["common_rage"] = {"length": 90, "effect": effect} def _prepare_auras(self): """Prepare cohesion skills aura effects.""" aura = loader.loadModel(address("cover_fire_aura")) # noqa: F821 aura.hide() aura.reparentTo(self.model) aura.setY(0.007) self.effects["cover_fire"] = { "model": aura, "seq": Sequence( LerpScaleInterval(aura, 1.5, (2, 2, 2), (1, 1, 1)), LerpScaleInterval(aura, 1.5, (1, 1, 1), (2, 2, 2)), ), } aura = loader.loadModel(address("common_rage_aura")) # noqa: F821 aura.hide() aura.reparentTo(self.model) aura.setY(0.007) self.effects["common_rage_aura"] = { "model": aura, "seq": Sequence( LerpScaleInterval(aura, 1.5, (2, 2, 2), (1, 1, 1)), LerpScaleInterval(aura, 1.5, (1, 1, 1), (2, 2, 2)), ), } aura = loader.loadModel(address("hold_together_aura")) # noqa: F821 aura.hide() aura.reparentTo(self.model) aura.setY(0.007) self.effects["hold_together"] = { "model": aura, "seq": Sequence( LerpScaleInterval(aura, 1.5, (2, 2, 2), (1, 1, 1)), LerpScaleInterval(aura, 1.5, (1, 1, 1), (2, 2, 2)), ), } def play_cohesion_effect(self, name): """Play the given cohesion skill effect. Args: name (str): Name of the skill. """ self.effects[name]["effect"].start(self.model, self.model) self.effects[name]["effect"].softStart() taskMgr.doMethodLater( # noqa: F821 self.effects[name]["length"], self.effects[name]["effect"].softStop, "stop_cohesion_effect", extraArgs=[], ) def play_cohesion_aura(self, name): """Play the given aura effect. Args: name (str): The effect name. """ self.effects[name]["model"].show() self.effects[name]["seq"].loop() def play_yes(self): """Play a voice sound, when the character is chosen.""" random.choice(self._yeah_snds).play() def stop_aura_effect(self, name): """Stop the given aura effect. Args: name (str): The effect name. """ self.effects[name]["model"].hide() self.effects[name]["seq"].finish() def celebrate(self, task): """Play victory celebration animation.""" if self.current_part.name == "part_rest": return task.done LerpAnimInterval(self.model, 0.1, self._current_anim, "celebrate").start() self._current_anim = "celebrate" self.model.play("celebrate") return task.done def move_to(self, part, to_pos=None): """Move this Character to the given Train part. If the movement is done with a positions exchange between two characters, the method doesn't take or release characters cells, just replaces one character with another one. Args: part (train.part.TrainPart): Train part to move this Character to. to_pos (dict): Position to move this Character to. Returns: bool: True, if the character was moved. """ if to_pos: part.chars.append(self) pos = to_pos else: pos = part.give_cell(self) if not pos: # no free cells on the chosen part return if self.current_part is not None: if to_pos: self.current_part.chars.remove(self) else: self.current_part.release_cell(self._current_pos, self) if self.current_part.name == "part_rest": self.stop_rest() if part.name == "part_rest": self.rest() self.model.wrtReparentTo(part.parent) self.model.setPos(pos) self.model.setH(part.angle) self.current_part = part self._current_pos = pos if part.name == "part_rest" and base.char_gui.rest_list_shown: # noqa: F821 base.char_gui.update_resting_chars(part) # noqa: F821 return True def do_effects(self, effects): """Do outing effects to this character. Effects are: adding traits, changing health and energy. Args: effects (dict): Effects and their values. """ if effects is None: return effects = copy.deepcopy(effects) self.get_damage(-effects.pop("health", 0)) if "add_trait" in effects and len(self.traits + self.disabled_traits) < 3: ind1, ind2 = effects["add_trait"] trait = base.labels.TRAITS[ind1][ind2] # noqa: F821 if trait and trait not in self.traits + self.disabled_traits: self.traits.append(trait) base.char_gui.move_status_label(-1) # noqa: F821 effects.pop("add_trait") for key, value in effects.items(): if hasattr(self, key): setattr(self, key, getattr(self, key) + value) def prepare_to_fight(self): """Prepare this character for a fight. Switch animations and run a task to choose a target. """ self._stop_tasks("_idle_anim") if self._idle_seq is not None: self._idle_seq.finish() self.model.loop("stand_and_aim") LerpAnimInterval(self.model, 0.8, self._current_anim, "stand_and_aim").start() LerpAnimInterval(self.model, 0.8, "stand", "stand_and_aim").start() self._current_anim = "stand_and_aim" taskMgr.doMethodLater( # noqa: F821 0.5, self._choose_target, self.id + "_choose_target" ) self._health_bar.show_health() def rest(self): """Make this character rest. Stops all the active tasks and starts energy regaining/healing. """ self._stop_tasks( "_reduce_energy", "_shoot", "_aim", "_choose_target", "_idle_anim" ) self.model.hide() self._col_node.stash() taskMgr.doMethodLater( # noqa: F821 0.05, self._calm_down, self.id + "_calm_down", extraArgs=[True], ) taskMgr.doMethodLater( # noqa: F821 self.class_data["energy_gain"], self._gain_energy, self.id + "_gain_energy" ) if self.health < self.class_data["health"]: taskMgr.doMethodLater( # noqa: F821 self.class_data["healing"], self._heal, self.id + "_heal" ) def surrender(self): """Stop fighting, surrender.""" self._stop_tasks("_shoot", "_aim", "_choose_target") self._col_node.removeNode() self._shoot_anim.finish() LerpAnimInterval(self.model, 0.5, "stand_and_aim", "surrender").start() self.model.play("surrender") def stop_rest(self): """Stop this character rest.""" self.model.show() self._col_node.unstash() base.char_gui.destroy_char_button(self.id) # noqa: F821 self._stop_tasks("_gain_energy", "_heal") taskMgr.doMethodLater( # noqa: F821 self.class_data["energy_spend"], self._reduce_energy, self.id + "_reduce_energy", ) if base.world.enemy.active_units: # noqa: F821 self.prepare_to_fight() def _reduce_energy(self, task): """ Reduce the character energy according to day part and status: fighting or not. """ if base.world.sun.is_dark: # noqa: F821 if not base.train.lights_on: # noqa: F821 task.delayTime = 15 elif "Floodlights" in base.train.upgrades: # noqa: F821 task.delayTime = self.class_data["energy_spend"] else: task.delayTime = 20 if base.labels.TRAITS[1][1] in self.traits: # noqa: F821 # Fear of dark task.delayTime /= 2 elif self._target: # Nervousness task.delayTime = ( 15 if base.labels.TRAITS[5][1] in self.traits else 20 # noqa: F821 ) else: task.delayTime = self.class_data["energy_spend"] if ( base.labels.TRAITS[2][1] in self.traits # noqa: F821 and self.health < self.class_data["health"] / 2 ): # Hemophobia task.delayTime *= 0.75 self.energy -= 1 if base.labels.TRAITS[7][0] in self.traits: # noqa: F821 # Mechanic base.train.get_damage(-3) # noqa: F821 return task.again def _gain_energy(self, task): """Regain this character energy. Used as a timed task while the character is resting. """ if ( base.labels.TRAITS[6][1] in self.traits # noqa: F821 and base.train.ctrl.current_speed > 0.75 # noqa: F821 ): # Motion sickness return task.again self.energy += 3 for char in self.current_part.chars: if char.sex == "female" and self.id != char.id: task.delayTime = self.class_data["energy_gain"] - 5 return task.again return task.again def _heal(self, task): """Regain this character health. Used as a timed task while the character is resting. """ if ( base.labels.TRAITS[6][1] in self.traits # noqa: F821 and base.train.ctrl.current_speed > 0.75 # noqa: F821 ): # Motion sickness return task.again if base.labels.TRAITS[7][1] in self.traits and chance(40): # noqa: F821 # Pharmacophobia return task.again if self.health < self.class_data["health"]: self.health += 1 return task.again return task.done def _choose_target(self, task): """Choose an enemy to shoot. Only an enemy from the Train part shooting range can be chosen as a target. """ if self.current_part and self.current_part.enemies: self._target = random.choice(self.current_part.enemies) taskMgr.doMethodLater(0.1, self._aim, self.id + "_aim") # noqa: F821 taskMgr.doMethodLater(1, self._shoot, self.id + "_shoot") # noqa: F821 return task.done # enemies retreated - return to passive state if not base.world.enemy.active_units: # noqa: F821 taskMgr.doMethodLater( # noqa: F821 7, self._calm_down, self.id + "_calm_down", extraArgs=[False], ) return task.done return task.again def _aim(self, task): """Rotate the character to aim on enemy.""" if ( self._target and self._target.is_dead and base.labels.TRAITS[5][0] in self.traits # noqa: F821 ): # Bloodthirsty self.health += 7 if self.current_part and self._target in self.current_part.enemies: self.model.headsUp(self._target.model) return task.again self._stop_tasks("_shoot") self._target = None if base.world.enemy.active_units: # noqa: F821 taskMgr.doMethodLater( # noqa: F821 0.5, self._choose_target, self.id + "_choose_target" ) return task.done taskMgr.doMethodLater( # noqa: F821 7, self._calm_down, self.id + "_calm_down", extraArgs=[False] ) return task.done def _calm_down(self, force): """Return to passive state on a combat over. Args: force (bool): If True, change to passive animation fast. Change animation in usual speed otherwise. """ self._stop_tasks("_shoot", "_calm_down") if self.current_part: self.model.hprInterval( 0.1 if force else 2, (self.current_part.angle, 0, 0) ).start() if force: for anim in self.model.get_anim_names(): self.model.setControlEffect(anim, 0) self.model.stop() self.model.setControlEffect("stand", 1) self.model.loop("stand") else: LerpAnimInterval(self.model, 2, self._current_anim, "stand").start() self._current_anim = "stand" taskMgr.doMethodLater( # noqa: F821 random.randint(40, 60), self._idle_animation, self.id + "_idle_anim" ) self._health_bar.hide_health() def _idle_animation(self, task): """Play one of the idle animations. Args: task (panda3d.core.PythonTask): Task object. """ if self.is_diseased and chance(80): self._current_anim = "cough" self._cough_snd.play() else: self._current_anim = random.choice( ("incline1", "gun_up", "release_gun", "tread1", "turn_head1") ) LerpAnimInterval(self.model, 0.2, "stand", self._current_anim).start() self._idle_seq = Sequence( self.model.actorInterval(self._current_anim, playRate=0.75), Func(self._stand), ) self._idle_seq.start() task.delayTime = random.randint(40, 60) return task.again def _stand(self): """Return to the idle stand animation.""" LerpAnimInterval(self.model, 0.2, self._current_anim, "stand").start() self._current_anim = "stand" def _die(self): """Character death sequence. Stop all the character's tasks, play death animation and plan the character object clearing. """ if self._team.hold_together: self.health = 1 return False if not Shooter._die(self): return False Unit._die(self) if base.common_ctrl.chosen_char == self: # noqa: F821 base.common_ctrl.deselect(clear_resting=False) # noqa: F821 self._health_bar.hide_health() self._stop_tasks( "_reduce_energy", "_get_well", "_infect", "_stop_stimul", "_gain_energy" ) base.char_gui.destroy_char_button(self.id) # noqa: F821 self._team.delete_relations(self.id) LerpAnimInterval(self.model, 0.3, "stand_and_aim", "die").start() LerpAnimInterval(self.model, 0.3, self._current_anim, "die").start() self.model.hprInterval(1, (self.current_part.angle, 0, 0)).start() self.model.play("die") self._die_snd.play() taskMgr.doMethodLater(3, self._hide, self.id + "_hide") # noqa: F821 def _hide(self, task): """Hide the main model.""" self.model.hide() return task.done def leave(self): """Make this character leave, plan clearing. Used only when sending a character away in a city. """ self._stop_tasks("_calm_down", "_gain_energy", "_heal", "_infect", "_get_well") taskMgr.doMethodLater(0.05, self.clear, self.id + "_clear") # noqa: F821 def clear(self, task): """Clear this character. Release models and sounds memory, release the part cell and delete the character from the team list. """ self.model.cleanup() self._health_bar.removeNode() self.model.removeNode() base.sound_mgr.detach_sound(self.shot_snd) # noqa: F821 base.sound_mgr.detach_sound(self._cough_snd) # noqa: F821 base.sound_mgr.detach_sound(self._die_snd) # noqa: F821 for snd in self._yeah_snds: base.sound_mgr.detach_sound(snd) # noqa: F821 self._team.chars.pop(self.id) base.res_gui.update_chars() # noqa: F821 self.current_part.release_cell(self._current_pos, self) self.current_part = None return task.done def exchange_pos(self, char): """Exchange positions of this Character and the given one. Args: char (units.character.Character): Character to exchange the positions with. """ o_part, o_pos = self.current_part, self._current_pos self.move_to(char.current_part, char._current_pos) char.move_to(o_part, o_pos) def hide_relations_ball(self): """Hide the relations ball of this character.""" if self._cohesion_ball is not None: self._cohesion_ball.removeNode() self._cohesion_ball = None def show_relation(self, cohesion): """Show a relations ball above this character. The color of the ball shows the strength of cohesion between the chosen character and this one. Args: cohesion (float): Value of the cohesion between the chosen character and this one. """ if self._cohesion_ball is None: self._cohesion_ball = loader.loadModel( # noqa: F821 address("relation_ball") ) self._cohesion_ball.clearLight() base.world.sun.ignore_shadows(self._cohesion_ball) # noqa: F821 self._cohesion_ball.reparentTo(self.model) self._cohesion_ball.setZ(0.14) self._cohesion_ball.setColorScale(*RELATION_COLORS[cohesion // 20]) def _missed_shot(self): """Calculate if character missed the current shot. Returns: bool: True if character missed, False otherwise. """ miss_chance = 0 if self.class_ == "soldier": if ( abs(self._target.node.getX()) < 0.56 and abs(self._target.node.getY()) < 0.95 ): miss_chance += 20 elif self.class_ == "raider": if ( abs(self._target.node.getX()) > 0.56 and abs(self._target.node.getY()) > 0.95 ): miss_chance += 20 if base.world.sun.is_dark: # noqa: F821 if base.labels.TRAITS[1][0] in self.traits: # noqa: F821 # Cat eyes miss_chance -= 5 elif base.train.lights_on: # noqa: F821 if "Floodlights" not in base.train.upgrades: # noqa: F821 miss_chance += 10 else: miss_chance += 20 miss_chance += (100 - self.energy) // 5 if self._team.cover_fire: miss_chance = min(100, max(0, miss_chance - 25)) return chance(miss_chance) def get_stunned(self): """Make this character stunned for some time. Stunned unit can't shoot. """ if self._is_stunned or self._is_stimulated: return self._is_stunned = True self._stop_tasks("_aim", "_shoot") self._current_anim = "stunned" self.model.play("stunned") LerpAnimInterval(self.model, 0.05, "stand_and_aim", "stunned").start() taskMgr.doMethodLater( # noqa: F821 6, self._stop_stunning, self.id + "_stop_stunning" ) def get_sick(self, is_infect=False): """Calculations to get this character sick. The worse condition the character has the higher is the chance for him to get sick. Sick character has lower energy maximum, and all his positive traits are disabled until getting well. Args: is_infect (bool): True if the disease came from a character on the same Train part. """ if self.is_diseased: return cond_percent = (self.energy + self.health) / (100 + self.class_data["health"]) percent = (1 - cond_percent) * 30 sick_chance = max( 0, # not less than zero ( percent + (20 if is_infect else 0) # Immunity - (40 if base.labels.TRAITS[3][0] in self.traits else 0) # noqa: F821 # Weak immunity + (20 if base.labels.TRAITS[3][1] in self.traits else 0) # noqa: F821 ), ) if chance(sick_chance): self.is_diseased = True self.get_well_score = 0 for traits_pair in base.labels.TRAITS: # noqa: F821 if traits_pair[0] in self.traits: self.traits.remove(traits_pair[0]) self.disabled_traits.append(traits_pair[0]) taskMgr.doMethodLater( # noqa: F821 60, self.get_well, self.id + "_get_well" ) taskMgr.doMethodLater(240, self.infect, self.id + "_infect") # noqa: F821 self.energy = min(self.energy, 80) def get_stimulated(self): """Use a stimulator on this character. Disables all the negative traits for some time. """ for traits_pair in base.labels.TRAITS: # noqa: F821 if traits_pair[1] in self.traits: self.traits.remove(traits_pair[1]) self.disabled_traits.append(traits_pair[1]) self._is_stimulated = True taskMgr.doMethodLater( # noqa: F821 300, self._stop_stimul, self.id + "_stop_stimul" ) def _stop_stimul(self, task): """Stop stimulation of this character. Returns back all the disabled negative traits. """ for trait_pair in base.labels.TRAITS: # noqa: F821 if trait_pair[1] in self.disabled_traits: self.traits.append(trait_pair[1]) self.disabled_traits.remove(trait_pair[1]) self._is_stimulated = False return task.done def get_well(self, task): """Get this character well. When the character got well, his energy maximum is restored to the default value, and his positive traits are back operational. """ self.get_well_score += 1 if self.get_well_score < 20: return task.again self.is_diseased = False for trait_pair in base.labels.TRAITS: # noqa: F821 if trait_pair[0] in self.disabled_traits: self.traits.append(trait_pair[0]) self.disabled_traits.remove(trait_pair[0]) self._stop_tasks("_infect") return task.done def infect(self, task): """Infect other characters on the same Train part.""" for char in self.current_part.chars: if char.id != self.id: char.get_sick(is_infect=True) return task.again def _shoot(self, task): """Play shooting animation and sound, make damage.""" if self.current_part and not self.current_part.is_covered: return Shooter._shoot(self, task) return task.again def _stop_stunning(self, task): """Stop this character stun and continue fighting.""" self._is_stunned = False LerpAnimInterval(self.model, 0.8, self._current_anim, "stand_and_aim").start() self._current_anim = "stand_and_aim" self.model.loop("stand_and_aim") taskMgr.doMethodLater(0.1, self._aim, self.id + "_aim") # noqa: F821 taskMgr.doMethodLater(1, self._shoot, self.id + "_shoot") # noqa: F821 return task.done def get_damage(self, damage): """Getting damage. Args: damage (int): Damage points to get. """ if base.labels.TRAITS[2][0] in self.traits: # noqa: F821 # Masochism self.energy += 1 Unit.get_damage(self, damage) def get_stench_damage(self): """Get damage from the Stench.""" if base.labels.TRAITS[6][0] in self.traits and self.inhale > 0: # noqa: F821 # Deep breath self.inhale -= 1 return if ( self.current_part and self.current_part.name == "part_rest" and "Window Frames" in base.train.upgrades # noqa: F821 ): return self.get_damage(1)
class Player(DirectObject): #---------------- # Initialization #---------------- def __init__(self): self.pNode = render.attachNewNode('playerRoot') # The root node of the player self.eyeHeight = 1.5 # The z height of the camera self.selectedBlock = 0 self.adjacentBlock = 0 self.selectedPlayer = None self.playerState = PlayerState(self) self.inputBuffer = InputBuffer() self.currentItem = None self.itemModels = {} self.playerModel = None # The bounding box of the player for collision with environment geometry self.boundingBoxCenterNode = self.pNode.attachNewNode('bboxCenter') self.boundingBoxCenterNode.setPos(0, 0, 0.9) self.boundingBox = BoundingBox(self.boundingBoxCenterNode, [0.2, 0.2, 0.9]) self.animFSM = PlayerAnimationFSM(self) self.animFSM.request('Idle') self.camNode = self.pNode.attachNewNode('cam') # The position of the player's eyes self.camNode.setPos(0, 0, self.eyeHeight) self.playerParent = self.pNode.attachNewNode('playerParent') # Change this back so items follow player turning #self.itemNode = self.playerParent.attachNewNode('3rdPersonItem') self.itemNode = self.pNode.attachNewNode('3rdPersonItem') self.itemNode.setPos(0, 0, 1.2) self.camItemNode = self.camNode.attachNewNode('1stPersonItem') # Node for name text self.nameText = None self.nameNode = self.pNode.attachNewNode('NameNode') self.nameNode.setPos(0, 0, 1.97) self.lookingRayNode = self.pNode.attachNewNode('lookingRayNode') self.lookingRayNode.setPos(0, 0, self.eyeHeight) lookingSeg = CollisionSegment(0, 0, 0, 0, 100, 0) self.lookingRay = self.lookingRayNode.attachNewNode(CollisionNode('lookingRay')) self.lookingRay.node().addSolid(lookingSeg) self.lookingRay.node().setFromCollideMask(Globals.BLOCK_PICKER_BITMASK | Globals.PLAYER_BITMASK) self.lookingRay.node().setIntoCollideMask(BitMask32.allOff()) self.lookingRayCollisionEntry = None self.pickerCollisionHandler = CollisionHandlerQueue() self.pickerTraverser = CollisionTraverser('pickerTraverser') self.pickerTraverser.addCollider(self.lookingRay, self.pickerCollisionHandler) if(not Settings.IS_SERVER): self.selectionGeom = SelectionGeom() else: self.selectionGeom = None self.CreateCollisionGeoms() self.LoadModels() def CreateCollisionGeoms(self): self.collisionNode = self.pNode.attachNewNode('CollisionNode') collisionGeom = self.collisionNode.attachNewNode(CollisionNode("legsPlayer")) collisionGeom.node().addSolid(CollisionSphere(0, 0, 0.35, 0.3)) collisionGeom.node().setFromCollideMask(Globals.ITEM_BITMASK) collisionGeom.node().setIntoCollideMask(Globals.PLAYER_BITMASK) #collisionGeom.show() collisionGeom.setPythonTag(Globals.TAG_COLLISION, Globals.COLLISION_LEG) collisionGeom.setPythonTag(Globals.TAG_PLAYER, self) collisionGeom = self.collisionNode.attachNewNode(CollisionNode("bodyPlayer")) collisionGeom.node().addSolid(CollisionSphere(0, 0, 0.9, 0.3)) collisionGeom.node().setFromCollideMask(BitMask32.allOff()) collisionGeom.node().setIntoCollideMask(Globals.PLAYER_BITMASK) #collisionGeom.show() collisionGeom.setPythonTag(Globals.TAG_COLLISION, Globals.COLLISION_BODY) collisionGeom.setPythonTag(Globals.TAG_PLAYER, self) collisionGeom = self.collisionNode.attachNewNode(CollisionNode("headPlayer")) collisionGeom.node().addSolid(CollisionSphere(0, 0, 1.45, 0.35)) collisionGeom.node().setFromCollideMask(BitMask32.allOff()) collisionGeom.node().setIntoCollideMask(Globals.PLAYER_BITMASK) collisionGeom.setPythonTag(Globals.TAG_COLLISION, Globals.COLLISION_HEAD) collisionGeom.setPythonTag(Globals.TAG_PLAYER, self) def CreateNameTextNode(self, name): self.nameText = PlayerName(name, self.nameNode) def RemoveSelectionGeom(self): if(self.selectionGeom is not None): self.selectionGeom.Destroy() self.selectionGeom = None def LoadModels(self, teamId = Game.SPECTATE): if(self.playerModel is not None): self.playerModel.cleanup() if(teamId == Game.TEAM_1): self.playerModel = Actor('Assets/Models/Players/ralph', {"run":"Assets/Models/Players/ralph-run", "walk":"Assets/Models/Players/ralph-walk"}) #elif(teamId == Game.TEAM_2): else: self.playerModel = Actor('Assets/Models/Players/ralph2', {"run":"Assets/Models/Players/ralph-run", "walk":"Assets/Models/Players/ralph-walk"}) self.playerModel.setScale(0.36) self.playerModel.reparentTo(self.playerParent) self.playerModel.setH(180) #self.playerModel.hide() self.playerModel.enableBlend() self.playerModel.loop('run') self.playerModel.pose('walk', 5) # self.outlineFilter = CommonFilters(base.win, base.cam) # if(not self.outlineFilter.setCartoonInk(0.5)): # print 'ERROR CARTOON FILTER' #-------------------- # Controlling models #-------------------- def HidePlayerModel(self): self.playerModel.hide() def ShowPlayerModel(self): self.playerModel.show() def LoopAnimation(self, animation): self.playerModel.setPlayRate(1.0, animation) self.playerModel.loop(animation) def RequestFSMTransition(self, animFSMState): self.animFSM.request(animFSMState) #-------------------- # Updating #-------------------- def Update(self, deltaTime): if(self.currentItem): self.currentItem.IncreaseTimeSinceLastUse(deltaTime) def UpdateLookingDirection(self, lookingDirection): if(not self.playerModel.isHidden() and self.GetPlayerState().GetValue(PlayerState.PLAYING_STATE) == PlayerState.PS_PLAYING): facingDirection = Vec3(lookingDirection.getX(), lookingDirection.getY(), 0) facingDirection.normalize() self.playerParent.lookAt(Point3(facingDirection)) self.itemNode.lookAt(Point3(self.itemNode.getPos() + lookingDirection)) def UpdateLookingRayDirection(self, lookingDirection): self.lookingRayNode.lookAt(Point3(lookingDirection.getX(), lookingDirection.getY(), lookingDirection.getZ() + self.eyeHeight)) def MovePosToPriorSnapshot(self, numFramesBack): self.pNode.setPos(self.GetPlayerState().GetValue(PlayerState.POSITION_HISTORY).GetPosition(numFramesBack)) def ReturnToCurrentPosition(self): self.pNode.setPos(self.currentPosition) #----------------------- # Weapons / items #----------------------- def OnSelectedItemChangeEvent(self, event): if(event.GetItemStack()): self.ChangeItem(event.GetItemStack().GetItem()) else: self.ChangeItem(None) def ChangeItem(self, item): print 'CHANGE ITEM curr', self.currentItem, 'new', item, self.GetPlayerState().GetValue(PlayerState.PID) # if(self.currentItem): # self.playerState.UpdateValue(PlayerState.CURRENT_ITEM, self.currentItem.GetItemId()) # else: # self.playerState.UpdateValue(PlayerState.CURRENT_ITEM, ItemId.NoItem) if(item != self.currentItem): oldItem = self.currentItem self.currentItem = item if(self.currentItem): self.playerState.UpdateValue(PlayerState.CURRENT_ITEM, self.currentItem.GetItemId()) else: self.playerState.UpdateValue(PlayerState.CURRENT_ITEM, ItemId.NoItem) #If both are item objects, equip dequip if(self.currentItem and oldItem): if(not Settings.IS_SERVER): self.currentItem.LoadContent() self.currentItem.ReparentTo(self.camItemNode) self.currentItem.EquipDequip(oldItem) # If just the old item is an obj, dequip it elif(oldItem): oldItem.Dequip() # If just the new item is an obj, equip it elif(self.currentItem): if(not Settings.IS_SERVER): self.currentItem.LoadContent() self.currentItem.ReparentTo(self.camItemNode) self.currentItem.Equip() # Change the item as though this is ourself, but then # reparent it so that it shows up properly in 3rd person def ThirdPersonChangeItem(self, item): self.ChangeItem(item) if(item and not Settings.IS_SERVER): self.currentItem.ReparentTo(self.itemNode) def ChangeBlockToPlace(self, direction): if(isinstance(self.currentItem, Builder)): if(direction > 0): self.currentItem.IncreaseBlock() else: self.currentItem.DecreaseBlock() def Reload(self): if(isinstance(self.currentItem, Firearm)): self.currentItem.Reload() # If we reloaded, tell fire an event if(self.GetPlayerState().GetValue(PlayerState.PID) == Globals.MY_PID): PlayerReloadEvent(self).Fire() def OnTeamChange(self, teamId): self.GetPlayerState().UpdateValue(PlayerState.TEAM, teamId) self.LoadModels(teamId) if(self.GetPlayerState().GetValue(PlayerState.PID) == Globals.MY_PID): self.HidePlayerModel() else: if(teamId != Globals.MY_TEAM): self.nameText.reshowOnInView = False self.nameText.Hide() else: self.nameText.reshowOnInView = True self.ShowNameAboveHead() self.nameText.SetColor(Globals.TEAM_COLORS[teamId]) #self.nameTextNode.setTextColor(Globals.TEAM_COLORS[teamId]) #------------------ # Death / Respawn #------------------- def OnDeath(self): self.collisionNode.stash() LerpHprInterval(self.playerParent, 0.7, (self.playerParent.getH(), 90, self.playerParent.getR())).start() self.itemNode.hide() self.camItemNode.hide() self.GetPlayerState().UpdateValue(PlayerState.PLAYING_STATE, PlayerState.PS_PLAYING_DEAD) # If this is the actual player if(self.GetPlayerState().GetValue(PlayerState.PID) == Globals.MY_PID): self.ShowPlayerModel() if(isinstance(self.currentItem, Firearm) and self.currentItem.IsADS()): self.currentItem.ToggleADS() def OnRespawn(self): self.collisionNode.unstash() self.GetInputBuffer().Clear() self.itemNode.show() self.camItemNode.show() self.GetPlayerState().UpdateValue(PlayerState.PLAYING_STATE, PlayerState.PS_PLAYING) if(self.GetPlayerState().GetValue(PlayerState.PID) == Globals.MY_PID): print 'Respawn event for me' self.HidePlayerModel() SelectedItemChangeEvent(None, ItemStack(self.currentItem)).Fire() else: self.ShowPlayerModel() #---------------------- # Accessor / Mutators #---------------------- def GetPos(self): return self.pNode.getPos() def SetPos(self, value): self.pNode.setPos(value) def GetZ(self): return self.pNode.getZ() def LerpTo(self, pos): LerpPosInterval(self.pNode, 0.1, pos).start() #--------------------------------------- # Playernames on screen #--------------------------------------- def ShowNameAboveHead(self): self.nameText.Show() def FadeOutNameAboveHead(self, task = None): self.nameText.FadeOut() #--------------------------------------- # Picking things with looking direction #---------------------------------------- #@pstat def PerfromLookingRayCollision(self, isSelf = True): self.pickerTraverser.traverse(render) entries = [] for i in range(self.pickerCollisionHandler.getNumEntries()): entry = self.pickerCollisionHandler.getEntry(i) entries.append(entry) numEntries = len(entries) if(numEntries > 0): entries.sort(lambda x,y: cmp(self.DistanceFromCam(x.getSurfacePoint(render)), self.DistanceFromCam(y.getSurfacePoint(render)))) self.lookingRayCollisionEntry = None playerEntry = None blockCollisionEntry = None for i in xrange(numEntries): if(entries[i].getIntoNodePath().getParent() != self.collisionNode): target = entries[i].getIntoNodePath().__str__() if(target.endswith('blockCollision')): blockCollisionEntry = entries[i] self.lookingRayCollisionEntry = blockCollisionEntry break elif(target.endswith('Player')): playerEntry = entries[i] self.lookingRayCollisionEntry = playerEntry break if(blockCollisionEntry and isinstance(self.currentItem, Builder)): point = blockCollisionEntry.getSurfacePoint(render) if(self.DistanceFromCam(point) <= Globals.MAX_SELECT_DISTANCE): norm = blockCollisionEntry.getSurfaceNormal(render) self.adjacentBlock = point + norm * 0.2 point -= norm * 0.2 x = point.getX() y = point.getY() z = point.getZ() if(self.selectionGeom): self.selectionGeom.SetPos(int(x), int(y), int(z)) self.selectionGeom.Show() self.selectedBlock = self.selectionGeom.GetPos() else: self.selectedBlock = VBase3(int(x), int(y), int(z)) else: self.selectedBlock = None self.adjacentBlock = None if(self.selectionGeom): self.selectionGeom.Hide() else: self.selectedBlock = None self.adjacentBlock = None if(self.selectionGeom): self.selectionGeom.Hide() if(playerEntry): player = playerEntry.getIntoNodePath().getPythonTag(Globals.TAG_PLAYER) if(player): # Each frame, if the crosshairs are hovering a player, fire the event # so the HUD can respond. PlayerSelectedEvent(player).Fire() self.selectedPlayer = player # # Before, we were only firing on changes # if(self.selectedPlayer != player): # self.selectedPlayer = player # if(not Settings.IS_SERVER and isSelf): # PlayerSelectedEvent(player).Fire() else: if(self.selectedPlayer != None): self.selectedPlayer = None if(not Settings.IS_SERVER and isSelf): PlayerSelectedEvent(None).Fire() def DistanceFromCam(self, otherPoint): eye = self.camNode.getPos(render) return (otherPoint - eye).length() def GetSelectedBlock(self): return self.selectedBlock def GetAdjacentBlock(self): return self.adjacentBlock def GetPlayerState(self): return self.playerState def GetInputBuffer(self): return self.inputBuffer def GetPlayerOverheadName(self): return self.nameText def GetCurrentItem(self): return self.currentItem #--------------- # Cleanup #--------------- def Destroy(self): for child in self.pNode.getChildren(): child.removeNode() self.pNode.removeNode() if(self.playerModel): self.playerModel.delete()