From 2e002cb9a15930528f0b6f390c6244e35c0d6e5f Mon Sep 17 00:00:00 2001 From: Anton Sarukhanov <code@ant.sr> Date: Sat, 5 Sep 2020 11:06:08 -0400 Subject: [PATCH] Add stock code. --- Dockerfile | 10 +- scripts/bsBomb.py | 854 +++++++++ scripts/bsGame.py | 3040 ++++++++++++++++++++++++++++++++ scripts/bsSpaz.py | 3890 +++++++++++++++++++++++++++++++++++++++++ scripts/bsTeamGame.py | 1455 +++++++++++++++ 5 files changed, 9244 insertions(+), 5 deletions(-) create mode 100644 scripts/bsBomb.py create mode 100644 scripts/bsGame.py create mode 100644 scripts/bsSpaz.py create mode 100644 scripts/bsTeamGame.py diff --git a/Dockerfile b/Dockerfile index a791035..7df6f1d 100644 --- a/Dockerfile +++ b/Dockerfile @@ -7,11 +7,11 @@ RUN wget -O BombSquad_Server_Linux_64bit_$BS_VERSION.tar.gz $BS_MIRROR/BombSquad && tar xzf BombSquad_Server_Linux_64bit_$BS_VERSION.tar.gz \ && rm BombSquad_Server_Linux_64bit_$BS_VERSION.tar.gz -COPY ./config.py /bombsquad/BombSquad_Server_Linux_64bit_$BS_VERSION/config.py -COPY ./bsTeamGame.py /bombsquad/BombSquad_Server_Linux_64bit_$BS_VERSION/data/scripts/bsTeamGame.py -COPY ./bsGame.py /bombsquad/BombSquad_Server_Linux_64bit_$BS_VERSION/data/scripts/bsGame.py -COPY ./bsBomb.py /bombsquad/BombSquad_Server_Linux_64bit_$BS_VERSION/data/scripts/bsBomb.py -COPY ./bsSpaz.py /bombsquad/BombSquad_Server_Linux_64bit_$BS_VERSION/data/scripts/bsSpaz.py +COPY ./scripts/config.py /bombsquad/BombSquad_Server_Linux_64bit_$BS_VERSION/config.py +COPY ./scripts/bsTeamGame.py /bombsquad/BombSquad_Server_Linux_64bit_$BS_VERSION/data/scripts/bsTeamGame.py +COPY ./scripts/bsGame.py /bombsquad/BombSquad_Server_Linux_64bit_$BS_VERSION/data/scripts/bsGame.py +COPY ./scripts/bsBomb.py /bombsquad/BombSquad_Server_Linux_64bit_$BS_VERSION/data/scripts/bsBomb.py +COPY ./scripts/bsSpaz.py /bombsquad/BombSquad_Server_Linux_64bit_$BS_VERSION/data/scripts/bsSpaz.py CMD /bombsquad/BombSquad_Server_Linux_64bit_$BS_VERSION/bombsquad_server EXPOSE 43212/udp diff --git a/scripts/bsBomb.py b/scripts/bsBomb.py new file mode 100644 index 0000000..e311846 --- /dev/null +++ b/scripts/bsBomb.py @@ -0,0 +1,854 @@ +import bs +import bsUtils +from bsVector import Vector +import random +import weakref + + +class BombFactory(object): + """ + category: Game Flow Classes + + Wraps up media and other resources used by bs.Bombs + A single instance of this is shared between all bombs + and can be retrieved via bs.Bomb.getFactory(). + + Attributes: + + bombModel + The bs.Model of a standard or ice bomb. + + stickyBombModel + The bs.Model of a sticky-bomb. + + impactBombModel + The bs.Model of an impact-bomb. + + landMinModel + The bs.Model of a land-mine. + + tntModel + The bs.Model of a tnt box. + + regularTex + The bs.Texture for regular bombs. + + iceTex + The bs.Texture for ice bombs. + + stickyTex + The bs.Texture for sticky bombs. + + impactTex + The bs.Texture for impact bombs. + + impactLitTex + The bs.Texture for impact bombs with lights lit. + + landMineTex + The bs.Texture for land-mines. + + landMineLitTex + The bs.Texture for land-mines with the light lit. + + tntTex + The bs.Texture for tnt boxes. + + hissSound + The bs.Sound for the hiss sound an ice bomb makes. + + debrisFallSound + The bs.Sound for random falling debris after an explosion. + + woodDebrisFallSound + A bs.Sound for random wood debris falling after an explosion. + + explodeSounds + A tuple of bs.Sounds for explosions. + + freezeSound + A bs.Sound of an ice bomb freezing something. + + fuseSound + A bs.Sound of a burning fuse. + + activateSound + A bs.Sound for an activating impact bomb. + + warnSound + A bs.Sound for an impact bomb about to explode due to time-out. + + bombMaterial + A bs.Material applied to all bombs. + + normalSoundMaterial + A bs.Material that generates standard bomb noises on impacts, etc. + + stickyMaterial + A bs.Material that makes 'splat' sounds and makes collisions softer. + + landMineNoExplodeMaterial + A bs.Material that keeps land-mines from blowing up. + Applied to land-mines when they are created to allow land-mines to + touch without exploding. + + landMineBlastMaterial + A bs.Material applied to activated land-mines that causes them to + explode on impact. + + impactBlastMaterial + A bs.Material applied to activated impact-bombs that causes them to + explode on impact. + + blastMaterial + A bs.Material applied to bomb blast geometry which triggers impact + events with what it touches. + + dinkSounds + A tuple of bs.Sounds for when bombs hit the ground. + + stickyImpactSound + The bs.Sound for a squish made by a sticky bomb hitting something. + + rollSound + bs.Sound for a rolling bomb. + """ + + def getRandomExplodeSound(self): + 'Return a random explosion bs.Sound from the factory.' + return self.explodeSounds[random.randrange(len(self.explodeSounds))] + + def __init__(self): + """ + Instantiate a BombFactory. + You shouldn't need to do this; call bs.Bomb.getFactory() to get a + shared instance. + """ + + self.bombModel = bs.getModel('bomb') + self.stickyBombModel = bs.getModel('bombSticky') + self.impactBombModel = bs.getModel('impactBomb') + self.landMineModel = bs.getModel('landMine') + self.tntModel = bs.getModel('tnt') + + self.regularTex = bs.getTexture('bombColor') + self.iceTex = bs.getTexture('bombColorIce') + self.stickyTex = bs.getTexture('bombStickyColor') + self.impactTex = bs.getTexture('impactBombColor') + self.impactLitTex = bs.getTexture('impactBombColorLit') + self.landMineTex = bs.getTexture('landMine') + self.landMineLitTex = bs.getTexture('landMineLit') + self.tntTex = bs.getTexture('tnt') + + self.hissSound = bs.getSound('hiss') + self.debrisFallSound = bs.getSound('debrisFall') + self.woodDebrisFallSound = bs.getSound('woodDebrisFall') + + self.explodeSounds = (bs.getSound('explosion01'), + bs.getSound('explosion02'), + bs.getSound('explosion03'), + bs.getSound('explosion04'), + bs.getSound('explosion05')) + + self.freezeSound = bs.getSound('freeze') + self.fuseSound = bs.getSound('fuse01') + self.activateSound = bs.getSound('activateBeep') + self.warnSound = bs.getSound('warnBeep') + + # set up our material so new bombs dont collide with objects + # that they are initially overlapping + self.bombMaterial = bs.Material() + self.normalSoundMaterial = bs.Material() + self.stickyMaterial = bs.Material() + + self.bombMaterial.addActions( + conditions=((('weAreYoungerThan',100), + 'or',('theyAreYoungerThan',100)), + 'and',('theyHaveMaterial', + bs.getSharedObject('objectMaterial'))), + actions=(('modifyNodeCollision','collide',False))) + + # we want pickup materials to always hit us even if we're currently not + # colliding with their node (generally due to the above rule) + self.bombMaterial.addActions( + conditions=('theyHaveMaterial', + bs.getSharedObject('pickupMaterial')), + actions=(('modifyPartCollision','useNodeCollide', False))) + + self.bombMaterial.addActions(actions=('modifyPartCollision', + 'friction', 0.3)) + + self.landMineNoExplodeMaterial = bs.Material() + self.landMineBlastMaterial = bs.Material() + self.landMineBlastMaterial.addActions( + conditions=( + ('weAreOlderThan',200), + 'and', ('theyAreOlderThan',200), + 'and', ('evalColliding',), + 'and', (('theyDontHaveMaterial', + self.landMineNoExplodeMaterial), + 'and', (('theyHaveMaterial', + bs.getSharedObject('objectMaterial')), + 'or',('theyHaveMaterial', + bs.getSharedObject('playerMaterial'))))), + actions=(('message', 'ourNode', 'atConnect', ImpactMessage()))) + + self.impactBlastMaterial = bs.Material() + self.impactBlastMaterial.addActions( + conditions=(('weAreOlderThan', 200), + 'and', ('theyAreOlderThan',200), + 'and', ('evalColliding',), + 'and', (('theyHaveMaterial', + bs.getSharedObject('footingMaterial')), + 'or',('theyHaveMaterial', + bs.getSharedObject('objectMaterial')))), + actions=(('message','ourNode','atConnect',ImpactMessage()))) + + self.blastMaterial = bs.Material() + self.blastMaterial.addActions( + conditions=(('theyHaveMaterial', + bs.getSharedObject('objectMaterial'))), + actions=(('modifyPartCollision','collide',True), + ('modifyPartCollision','physical',False), + ('message','ourNode','atConnect',ExplodeHitMessage()))) + + self.dinkSounds = (bs.getSound('bombDrop01'), + bs.getSound('bombDrop02')) + self.stickyImpactSound = bs.getSound('stickyImpact') + self.rollSound = bs.getSound('bombRoll01') + + # collision sounds + self.normalSoundMaterial.addActions( + conditions=('theyHaveMaterial', + bs.getSharedObject('footingMaterial')), + actions=(('impactSound',self.dinkSounds,2,0.8), + ('rollSound',self.rollSound,3,6))) + + self.stickyMaterial.addActions( + actions=(('modifyPartCollision','stiffness',0.1), + ('modifyPartCollision','damping',1.0))) + + self.stickyMaterial.addActions( + conditions=(('theyHaveMaterial', + bs.getSharedObject('playerMaterial')), + 'or', ('theyHaveMaterial', + bs.getSharedObject('footingMaterial'))), + actions=(('message','ourNode','atConnect',SplatMessage()))) + +class SplatMessage(object): + pass + +class ExplodeMessage(object): + pass + +class ImpactMessage(object): + """ impact bomb touched something """ + pass + +class ArmMessage(object): + pass + +class WarnMessage(object): + pass + +class ExplodeHitMessage(object): + "Message saying an object was hit" + def __init__(self): + pass + +class Blast(bs.Actor): + """ + category: Game Flow Classes + + An explosion, as generated by a bs.Bomb. + """ + def __init__(self, position=(0,1,0), velocity=(0,0,0), blastRadius=2.0, + blastType="normal", sourcePlayer=None, hitType='explosion', + hitSubType='normal'): + """ + Instantiate with given values. + """ + bs.Actor.__init__(self) + + factory = Bomb.getFactory() + + self.blastType = blastType + self.sourcePlayer = sourcePlayer + + self.hitType = hitType; + self.hitSubType = hitSubType; + + # blast radius + self.radius = blastRadius + + # set our position a bit lower so we throw more things upward + self.node = bs.newNode('region', delegate=self, attrs={ + 'position':(position[0], position[1]-0.1, position[2]), + 'scale':(self.radius,self.radius,self.radius), + 'type':'sphere', + 'materials':(factory.blastMaterial, + bs.getSharedObject('attackMaterial'))}) + + bs.gameTimer(50, self.node.delete) + + # throw in an explosion and flash + explosion = bs.newNode("explosion", attrs={ + 'position':position, + 'velocity':(velocity[0],max(-1.0,velocity[1]),velocity[2]), + 'radius':self.radius, + 'big':(self.blastType == 'tnt')}) + if self.blastType == "ice": + explosion.color = (0, 0.05, 0.4) + + bs.gameTimer(1000,explosion.delete) + + if self.blastType != 'ice': + bs.emitBGDynamics(position=position, velocity=velocity, + count=int(1.0+random.random()*4), + emitType='tendrils',tendrilType='thinSmoke') + bs.emitBGDynamics( + position=position, velocity=velocity, + count=int(4.0+random.random()*4), emitType='tendrils', + tendrilType='ice' if self.blastType == 'ice' else 'smoke') + bs.emitBGDynamics( + position=position, emitType='distortion', + spread=1.0 if self.blastType == 'tnt' else 2.0) + + # and emit some shrapnel.. + if self.blastType == 'ice': + def _doEmit(): + bs.emitBGDynamics(position=position, velocity=velocity, + count=30, spread=2.0, scale=0.4, + chunkType='ice', emitType='stickers'); + bs.gameTimer(50, _doEmit) # looks better if we delay a bit + + elif self.blastType == 'sticky': + def _doEmit(): + bs.emitBGDynamics(position=position, velocity=velocity, + count=int(4.0+random.random()*8), + spread=0.7,chunkType='slime'); + bs.emitBGDynamics(position=position, velocity=velocity, + count=int(4.0+random.random()*8), scale=0.5, + spread=0.7,chunkType='slime'); + bs.emitBGDynamics(position=position, velocity=velocity, + count=15, scale=0.6, chunkType='slime', + emitType='stickers'); + bs.emitBGDynamics(position=position, velocity=velocity, + count=20, scale=0.7, chunkType='spark', + emitType='stickers'); + bs.emitBGDynamics(position=position, velocity=velocity, + count=int(6.0+random.random()*12), + scale=0.8, spread=1.5,chunkType='spark'); + bs.gameTimer(50,_doEmit) # looks better if we delay a bit + + elif self.blastType == 'impact': # regular bomb shrapnel + def _doEmit(): + bs.emitBGDynamics(position=position, velocity=velocity, + count=int(4.0+random.random()*8), scale=0.8, + chunkType='metal'); + bs.emitBGDynamics(position=position, velocity=velocity, + count=int(4.0+random.random()*8), scale=0.4, + chunkType='metal'); + bs.emitBGDynamics(position=position, velocity=velocity, + count=20, scale=0.7, chunkType='spark', + emitType='stickers'); + bs.emitBGDynamics(position=position, velocity=velocity, + count=int(8.0+random.random()*15), scale=0.8, + spread=1.5, chunkType='spark'); + bs.gameTimer(50,_doEmit) # looks better if we delay a bit + + else: # regular or land mine bomb shrapnel + def _doEmit(): + if self.blastType != 'tnt': + bs.emitBGDynamics(position=position, velocity=velocity, + count=int(4.0+random.random()*8), + chunkType='rock'); + bs.emitBGDynamics(position=position, velocity=velocity, + count=int(4.0+random.random()*8), + scale=0.5,chunkType='rock'); + bs.emitBGDynamics(position=position, velocity=velocity, + count=30, + scale=1.0 if self.blastType=='tnt' else 0.7, + chunkType='spark', emitType='stickers'); + bs.emitBGDynamics(position=position, velocity=velocity, + count=int(18.0+random.random()*20), + scale=1.0 if self.blastType == 'tnt' else 0.8, + spread=1.5, chunkType='spark'); + + # tnt throws splintery chunks + if self.blastType == 'tnt': + def _emitSplinters(): + bs.emitBGDynamics(position=position, velocity=velocity, + count=int(20.0+random.random()*25), + scale=0.8, spread=1.0, + chunkType='splinter'); + bs.gameTimer(10,_emitSplinters) + + # every now and then do a sparky one + if self.blastType == 'tnt' or random.random() < 0.1: + def _emitExtraSparks(): + bs.emitBGDynamics(position=position, velocity=velocity, + count=int(10.0+random.random()*20), + scale=0.8, spread=1.5, + chunkType='spark'); + bs.gameTimer(20,_emitExtraSparks) + + bs.gameTimer(50,_doEmit) # looks better if we delay a bit + + light = bs.newNode('light', attrs={ + 'position':position, + 'volumeIntensityScale': 10.0, + 'color': ((0.6, 0.6, 1.0) if self.blastType == 'ice' + else (1, 0.3, 0.1))}) + + s = random.uniform(0.6,0.9) + scorchRadius = lightRadius = self.radius + if self.blastType == 'tnt': + lightRadius *= 1.4 + scorchRadius *= 1.15 + s *= 3.0 + + iScale = 1.6 + bsUtils.animate(light,"intensity", { + 0:2.0*iScale, int(s*20):0.1*iScale, + int(s*25):0.2*iScale, int(s*50):17.0*iScale, int(s*60):5.0*iScale, + int(s*80):4.0*iScale, int(s*200):0.6*iScale, + int(s*2000):0.00*iScale, int(s*3000):0.0}) + bsUtils.animate(light,"radius", { + 0:lightRadius*0.2, int(s*50):lightRadius*0.55, + int(s*100):lightRadius*0.3, int(s*300):lightRadius*0.15, + int(s*1000):lightRadius*0.05}) + bs.gameTimer(int(s*3000),light.delete) + + # make a scorch that fades over time + scorch = bs.newNode('scorch', attrs={ + 'position':position, + 'size':scorchRadius*0.5, + 'big':(self.blastType == 'tnt')}) + if self.blastType == 'ice': + scorch.color = (1,1,1.5) + + bsUtils.animate(scorch,"presence",{3000:1, 13000:0}) + bs.gameTimer(13000,scorch.delete) + + if self.blastType == 'ice': + bs.playSound(factory.hissSound,position=light.position) + + p = light.position + bs.playSound(factory.getRandomExplodeSound(),position=p) + bs.playSound(factory.debrisFallSound,position=p) + + bs.shakeCamera(intensity=5.0 if self.blastType == 'tnt' else 1.0) + + # tnt is more epic.. + if self.blastType == 'tnt': + bs.playSound(factory.getRandomExplodeSound(),position=p) + def _extraBoom(): + bs.playSound(factory.getRandomExplodeSound(),position=p) + bs.gameTimer(250,_extraBoom) + def _extraDebrisSound(): + bs.playSound(factory.debrisFallSound,position=p) + bs.playSound(factory.woodDebrisFallSound,position=p) + bs.gameTimer(400,_extraDebrisSound) + + def handleMessage(self, msg): + self._handleMessageSanityCheck() + + if isinstance(msg, bs.DieMessage): + self.node.delete() + + elif isinstance(msg, ExplodeHitMessage): + node = bs.getCollisionInfo("opposingNode") + if node is not None and node.exists(): + t = self.node.position + + # new + mag = 2000.0 + if self.blastType == 'ice': mag *= 0.5 + elif self.blastType == 'landMine': mag *= 2.5 + elif self.blastType == 'tnt': mag *= 2.0 + + node.handleMessage(bs.HitMessage( + pos=t, + velocity=(0,0,0), + magnitude=mag, + hitType=self.hitType, + hitSubType=self.hitSubType, + radius=self.radius, + sourcePlayer=self.sourcePlayer)) + if self.blastType == "ice": + bs.playSound(Bomb.getFactory().freezeSound, 10, position=t) + node.handleMessage(bs.FreezeMessage()) + + else: + bs.Actor.handleMessage(self, msg) + +class Bomb(bs.Actor): + """ + category: Game Flow Classes + + A bomb and its variants such as land-mines and tnt-boxes. + """ + + def __init__(self, position=(0,1,0), velocity=(0,0,0), bombType='normal', + blastRadius=2.0, sourcePlayer=None, owner=None): + """ + Create a new Bomb. + + bombType can be 'ice','impact','landMine','normal','sticky', or 'tnt'. + Note that for impact or landMine bombs you have to call arm() + before they will go off. + """ + bs.Actor.__init__(self) + + factory = self.getFactory() + + if not bombType in ('ice','impact','landMine','normal','sticky','tnt'): + raise Exception("invalid bomb type: " + bombType) + self.bombType = bombType + + self._exploded = False + + if self.bombType == 'sticky': self._lastStickySoundTime = 0 + + self.blastRadius = blastRadius + if self.bombType == 'ice': self.blastRadius *= 1.2 + elif self.bombType == 'impact': self.blastRadius *= 0.7 + elif self.bombType == 'landMine': self.blastRadius *= 0.7 + elif self.bombType == 'tnt': self.blastRadius *= 1.45 + + self._explodeCallbacks = [] + + # the player this came from + self.sourcePlayer = sourcePlayer + + # by default our hit type/subtype is our own, but we pick up types of + # whoever sets us off so we know what caused a chain reaction + self.hitType = 'explosion' + self.hitSubType = self.bombType + + # if no owner was provided, use an unconnected node ref + if owner is None: owner = bs.Node(None) + + # the node this came from + self.owner = owner + + # adding footing-materials to things can screw up jumping and flying + # since players carrying those things + # and thus touching footing objects will think they're on solid ground.. + # perhaps we don't wanna add this even in the tnt case?.. + if self.bombType == 'tnt': + materials = (factory.bombMaterial, + bs.getSharedObject('footingMaterial'), + bs.getSharedObject('objectMaterial')) + else: + materials = (factory.bombMaterial, + bs.getSharedObject('objectMaterial')) + + if self.bombType == 'impact': + materials = materials + (factory.impactBlastMaterial,) + elif self.bombType == 'landMine': + materials = materials + (factory.landMineNoExplodeMaterial,) + + if self.bombType == 'sticky': + materials = materials + (factory.stickyMaterial,) + else: + materials = materials + (factory.normalSoundMaterial,) + + if self.bombType == 'landMine': + self.node = bs.newNode('prop', delegate=self, attrs={ + 'position':position, + 'velocity':velocity, + 'model':factory.landMineModel, + 'lightModel':factory.landMineModel, + 'body':'landMine', + 'shadowSize':0.44, + 'colorTexture':factory.landMineTex, + 'reflection':'powerup', + 'reflectionScale':[1.0], + 'materials':materials}) + + elif self.bombType == 'tnt': + self.node = bs.newNode('prop', delegate=self, attrs={ + 'position':position, + 'velocity':velocity, + 'model':factory.tntModel, + 'lightModel':factory.tntModel, + 'body':'crate', + 'shadowSize':0.5, + 'colorTexture':factory.tntTex, + 'reflection':'soft', + 'reflectionScale':[0.23], + 'materials':materials}) + + elif self.bombType == 'impact': + fuseTime = 20000 + self.node = bs.newNode('prop', delegate=self, attrs={ + 'position':position, + 'velocity':velocity, + 'body':'sphere', + 'model':factory.impactBombModel, + 'shadowSize':0.3, + 'colorTexture':factory.impactTex, + 'reflection':'powerup', + 'reflectionScale':[1.5], + 'materials':materials}) + self.armTimer = bs.Timer(200, bs.WeakCall(self.handleMessage, + ArmMessage())) + self.warnTimer = bs.Timer(fuseTime-1700, + bs.WeakCall(self.handleMessage, + WarnMessage())) + + else: + fuseTime = 3000 + if self.bombType == 'sticky': + sticky = True + model = factory.stickyBombModel + rType = 'sharper' + rScale = 1.8 + else: + sticky = False + model = factory.bombModel + rType = 'sharper' + rScale = 1.8 + if self.bombType == 'ice': tex = factory.iceTex + elif self.bombType == 'sticky': tex = factory.stickyTex + else: tex = factory.regularTex + self.node = bs.newNode('bomb', delegate=self, attrs={ + 'position':position, + 'velocity':velocity, + 'model':model, + 'shadowSize':0.3, + 'colorTexture':tex, + 'sticky':sticky, + 'owner':owner, + 'reflection':rType, + 'reflectionScale':[rScale], + 'materials':materials}) + + sound = bs.newNode('sound', owner=self.node, attrs={ + 'sound':factory.fuseSound, + 'volume':0.25}) + self.node.connectAttr('position', sound, 'position') + bsUtils.animate(self.node, 'fuseLength', {0:1.0, fuseTime:0.0}) + + # light the fuse!!! + if self.bombType not in ('landMine','tnt'): + bs.gameTimer(fuseTime, + bs.WeakCall(self.handleMessage, ExplodeMessage())) + + bsUtils.animate(self.node,"modelScale",{0:0, 200:1.3, 260:1}) + + def getSourcePlayer(self): + """ + Returns a bs.Player representing the source of this bomb. + """ + if self.sourcePlayer is None: return bs.Player(None) # empty player ref + return self.sourcePlayer + + @classmethod + def getFactory(cls): + """ + Returns a shared bs.BombFactory object, creating it if necessary. + """ + activity = bs.getActivity() + try: return activity._sharedBombFactory + except Exception: + f = activity._sharedBombFactory = BombFactory() + return f + + def onFinalize(self): + bs.Actor.onFinalize(self) + # release callbacks/refs so we don't wind up with dependency loops.. + self._explodeCallbacks = [] + + def _handleDie(self,m): + self.node.delete() + + def _handleOOB(self, msg): + self.handleMessage(bs.DieMessage()) + + def _handleImpact(self,m): + node,body = bs.getCollisionInfo("opposingNode","opposingBody") + # if we're an impact bomb and we came from this node, don't explode... + # alternately if we're hitting another impact-bomb from the same source, + # don't explode... + try: nodeDelegate = node.getDelegate() + except Exception: nodeDelegate = None + if node is not None and node.exists(): + if (self.bombType == 'impact' and + (node is self.owner + or (isinstance(nodeDelegate, Bomb) + and nodeDelegate.bombType == 'impact' + and nodeDelegate.owner is self.owner))): return + else: + self.handleMessage(ExplodeMessage()) + + def _handleDropped(self,m): + if self.bombType == 'landMine': + self.armTimer = \ + bs.Timer(1250, bs.WeakCall(self.handleMessage, ArmMessage())) + + # once we've thrown a sticky bomb we can stick to it.. + elif self.bombType == 'sticky': + def _safeSetAttr(node,attr,value): + if node.exists(): setattr(node,attr,value) + bs.gameTimer( + 250, lambda: _safeSetAttr(self.node, 'stickToOwner', True)) + + def _handleSplat(self,m): + node = bs.getCollisionInfo("opposingNode") + if (node is not self.owner + and bs.getGameTime() - self._lastStickySoundTime > 1000): + self._lastStickySoundTime = bs.getGameTime() + bs.playSound(self.getFactory().stickyImpactSound, 2.0, + position=self.node.position) + + def addExplodeCallback(self,call): + """ + Add a call to be run when the bomb has exploded. + The bomb and the new blast object are passed as arguments. + """ + self._explodeCallbacks.append(call) + + def explode(self): + """ + Blows up the bomb if it has not yet done so. + """ + if self._exploded: return + self._exploded = True + activity = self.getActivity() + if activity is not None and self.node.exists(): + blast = Blast( + position=self.node.position, + velocity=self.node.velocity, + blastRadius=self.blastRadius, + blastType=self.bombType, + sourcePlayer=self.sourcePlayer, + hitType=self.hitType, + hitSubType=self.hitSubType).autoRetain() + for c in self._explodeCallbacks: c(self,blast) + + # we blew up so we need to go away + bs.gameTimer(1, bs.WeakCall(self.handleMessage, bs.DieMessage())) + + def _handleWarn(self, m): + if self.textureSequence.exists(): + self.textureSequence.rate = 30 + bs.playSound(self.getFactory().warnSound, 0.5, + position=self.node.position) + + def _addMaterial(self, material): + if not self.node.exists(): return + materials = self.node.materials + if not material in materials: + self.node.materials = materials + (material,) + + def arm(self): + """ + Arms land-mines and impact-bombs so + that they will explode on impact. + """ + if not self.node.exists(): return + factory = self.getFactory() + if self.bombType == 'landMine': + self.textureSequence = \ + bs.newNode('textureSequence', owner=self.node, attrs={ + 'rate':30, + 'inputTextures':(factory.landMineLitTex, + factory.landMineTex)}) + bs.gameTimer(500,self.textureSequence.delete) + # we now make it explodable. + bs.gameTimer(250,bs.WeakCall(self._addMaterial, + factory.landMineBlastMaterial)) + elif self.bombType == 'impact': + self.textureSequence = \ + bs.newNode('textureSequence', owner=self.node, attrs={ + 'rate':100, + 'inputTextures':(factory.impactLitTex, + factory.impactTex, + factory.impactTex)}) + bs.gameTimer(250, bs.WeakCall(self._addMaterial, + factory.landMineBlastMaterial)) + else: + raise Exception('arm() should only be called ' + 'on land-mines or impact bombs') + self.textureSequence.connectAttr('outputTexture', + self.node, 'colorTexture') + bs.playSound(factory.activateSound, 0.5, position=self.node.position) + + def _handleHit(self, msg): + isPunch = (msg.srcNode.exists() and msg.srcNode.getNodeType() == 'spaz') + + # normal bombs are triggered by non-punch impacts.. + # impact-bombs by all impacts + if (not self._exploded and not isPunch + or self.bombType in ['impact', 'landMine']): + # also lets change the owner of the bomb to whoever is setting + # us off.. (this way points for big chain reactions go to the + # person causing them) + if msg.sourcePlayer not in [None]: + self.sourcePlayer = msg.sourcePlayer + + # also inherit the hit type (if a landmine sets off by a bomb, + # the credit should go to the mine) + # the exception is TNT. TNT always gets credit. + if self.bombType != 'tnt': + self.hitType = msg.hitType + self.hitSubType = msg.hitSubType + + bs.gameTimer(100+int(random.random()*100), + bs.WeakCall(self.handleMessage, ExplodeMessage())) + self.node.handleMessage( + "impulse", msg.pos[0], msg.pos[1], msg.pos[2], + msg.velocity[0], msg.velocity[1], msg.velocity[2], + msg.magnitude, msg.velocityMagnitude, msg.radius, 0, + msg.velocity[0], msg.velocity[1], msg.velocity[2]) + + if msg.srcNode.exists(): + pass + + def handleMessage(self, msg): + if isinstance(msg, ExplodeMessage): self.explode() + elif isinstance(msg, ImpactMessage): self._handleImpact(msg) + elif isinstance(msg, bs.PickedUpMessage): + # change our source to whoever just picked us up *only* if its None + # this way we can get points for killing bots with their own bombs + # hmm would there be a downside to this?... + if self.sourcePlayer is not None: + self.sourcePlayer = msg.node.sourcePlayer + elif isinstance(msg, SplatMessage): self._handleSplat(msg) + elif isinstance(msg, bs.DroppedMessage): self._handleDropped(msg) + elif isinstance(msg, bs.HitMessage): self._handleHit(msg) + elif isinstance(msg, bs.DieMessage): self._handleDie(msg) + elif isinstance(msg, bs.OutOfBoundsMessage): self._handleOOB(msg) + elif isinstance(msg, ArmMessage): self.arm() + elif isinstance(msg, WarnMessage): self._handleWarn(msg) + else: bs.Actor.handleMessage(self, msg) + +class TNTSpawner(object): + """ + category: Game Flow Classes + + Regenerates TNT at a given point in space every now and then. + """ + def __init__(self,position,respawnTime=30000): + """ + Instantiate with a given position and respawnTime (in milliseconds). + """ + self._position = position + self._tnt = None + self._update() + self._updateTimer = bs.Timer(1000,bs.WeakCall(self._update),repeat=True) + self._respawnTime = int(random.uniform(0.8,1.2)*respawnTime) + self._waitTime = 0 + + def _update(self): + tntAlive = self._tnt is not None and self._tnt.node.exists() + if not tntAlive: + # respawn if its been long enough.. otherwise just increment our + # how-long-since-we-died value + if self._tnt is None or self._waitTime >= self._respawnTime: + self._tnt = Bomb(position=self._position,bombType='tnt') + self._waitTime = 0 + else: self._waitTime += 1000 diff --git a/scripts/bsGame.py b/scripts/bsGame.py new file mode 100644 index 0000000..96edae0 --- /dev/null +++ b/scripts/bsGame.py @@ -0,0 +1,3040 @@ +import bs +import bsInternal +from bsVector import Vector +import weakref +import random +import bsUtils +import time + + +class Team(object): + """ + category: Game Flow Classes + + A team of one or more bs.Players. + Note that a player *always* has a team; + in some cases, such as free-for-all bs.Sessions, + the teams consists of just one bs.Player each. + + Attributes: + + name + The team's name. + + color + The team's color. + + players + The list of bs.Players on the team. + + gameData + A dict for use by the current bs.Activity + for storing data associated with this team. + This gets cleared for each new bs.Activity. + + sessionData + A dict for use by the current bs.Session for + storing data associated with this team. + Unlike gameData, this perists for the duration + of the session. + """ + + def __init__(self, teamID=0, name='', color=(1, 1, 1)): + """ + Instantiate a team. In most cases teams are provided to you + automatically by the bs.Session, so calling this shouldn't be necessary. + """ + # we override __setattr__ to lock ourself down + # so we have to set attrs all funky-like + object.__setattr__(self, '_teamID', teamID) + object.__setattr__(self, 'name', name) + object.__setattr__(self, 'color', tuple(color)) + object.__setattr__(self, 'players', []) + object.__setattr__(self, 'gameData', {}) # per-game user-data + object.__setattr__(self, 'sessionData', {}) # per-session user-data + + def getID(self): + 'Returns the numeric team ID.' + return object.__getattribute__(self, '_teamID') + + def celebrate(self, duration=10000): + 'Tells all players on the team to celebrate' + for player in self.players: + try: + player.actor.node.handleMessage('celebrate', 10000) + except Exception: + pass + + def _reset(self): + self._resetGameData() + object.__setattr__(self, 'players', []) + + def _resetGameData(self): + object.__setattr__(self, 'gameData', {}) + + def _resetSessionData(self): + object.__setattr__(self, 'sessionData', {}) + + def __setattr__(self, name, value): + raise Exception("can't set attrs on bs.Team objects") + + +class OutOfBoundsMessage(object): + """ + category: Message Classes + + Tells an object that it is out of bounds. + """ + pass + + +class DieMessage(object): + """ + category: Message Classes + + Tells an object to die. + Most bs.Actors respond to this. + + Attributes: + + immediate + If this is set to True, the actor should disappear immediately. + This is for 'removing' stuff from the game moreso than 'killing' it. + If False, the actor should die a 'normal' death and can take its time + with lingering corpses, sound effects, etc. + + how + The particular reason for death; 'fall', 'impact', 'leftGame', etc. + This can be examined for scoring or other purposes. + + """ + + def __init__(self, immediate=False, how="generic"): + """ + Instantiate with the given values. + """ + self.immediate = immediate + self.how = how + + +class StandMessage(object): + """ + category: Message Classes + + Tells an object to position itself to be standing upright at the given + position. Used when teleporting players to home base, etc. + + Attributes: + + position + Where to stand. + + angle + The angle to face (in degrees) + """ + + def __init__(self, position=(0, 0, 0), angle=0): + """ + Instantiate with a given position and angle. + """ + self.position = position + self.angle = angle + + +class PickUpMessage(object): + """ + category: Message Classes + + Tells an object that it has picked something up. + + Attributes: + + node + The bs.Node that is getting picked up. + """ + + def __init__(self, node): + 'Instantiate with a given bs.Node.' + self.node = node + + +class DropMessage(object): + """ + category: Message Classes + + Tells an object that it has dropped whatever it was holding. + """ + pass + + +class PickedUpMessage(object): + """ + category: Message Classes + + Tells an object that it has been picked up by something. + + Attributes: + + node + The bs.Node doing the picking up. + """ + + def __init__(self, node): + """ + Instantiate with a given bs.Node. + """ + self.node = node + + +class DroppedMessage(object): + """ + category: Message Classes + + Tells an object that it has been dropped. + + Attributes: + + node + The bs.Node doing the dropping. + """ + + def __init__(self, node): + """ + Instantiate with a given bs.Node. + """ + self.node = node + + +class ShouldShatterMessage(object): + """ + category: Message Classes + + Tells an object that it should shatter. + """ + pass + + +class ImpactDamageMessage(object): + """ + category: Message Classes + + Tells an object that it has been jarred violently and + may want to be damaged. + + Attributes: + + intensity + The intensity of the impact. + """ + + def __init__(self, intensity): + """ + Instantiate a messages with a given intensity value. + """ + self.intensity = intensity + + +class FreezeMessage(object): + """ + category: Message Classes + + Tells an object to become frozen + (as in the effects of an ice bs.Bomb). + """ + pass + + +class ThawMessage(object): + """ + category: Message Classes + + Tells an object that was frozen by a bs.FrozenMessage + to thaw out. + """ + pass + + +class HitMessage(object): + """ + category: Message Classes + + Tells an object it has been hit in some way. + This is used by punches, explosions, etc to convey + their effect to a target. + """ + + def __init__( + self, srcNode=None, pos=Vector(0, 0, 0), + velocity=Vector(0, 0, 0), + magnitude=1.0, velocityMagnitude=0.0, radius=1.0, sourcePlayer=None, + kickBack=1.0, flatDamage=None, hitType='generic', + forceDirection=None, hitSubType='default'): + """ + Instantiate a message with various bits of information + on the type of hit that occurred. + """ + # convert None to empty node-ref/player-ref + if srcNode is None: + srcNode = bs.Node(None) + if sourcePlayer is None: + sourcePlayer = bs.Player(None) + + self.srcNode = srcNode + self.pos = pos + self.velocity = velocity + self.magnitude = magnitude + self.velocityMagnitude = velocityMagnitude + self.radius = radius + self.sourcePlayer = sourcePlayer + self.kickBack = kickBack + self.flatDamage = flatDamage + self.hitType = hitType + self.hitSubType = hitSubType + self.forceDirection = (forceDirection if forceDirection is not None + else velocity) + + +class Actor(object): + """ + category: Game Flow Classes + + Actors are high level organizational entities that generally manage one or + more bs.Nodes, bits of game media, etc, along with the associated logic to + wrangle them. + + If bs.Nodes represent cells, think of bs.Actors as organs. + Some example actors include bs.Bomb, bs.Flag, and bs.Spaz. + + One key feature of actors is that they generally 'die' + (killing off or transitioning out their nodes) when the last python + reference to them disappears, so you can use logic such as: + + # create a flag in our activity + self.flag = bs.Flag(position=(0,10,0)) + + # later, destroy the flag.. + # (provided nothing else is holding a reference to it) + self.flag = None + + This is in contrast to the behavior of the more low level bs.Nodes, + which are always explicitly created and destroyed regardless of how many + python references to them exist. + + Another key feature of bs.Actor is its handleMessage() method, which + takes a single arbitrary object as an argument. This provides a safe way + to communicate between bs.Actor, bs.Activity, bs.Session, and any other + object providing a handleMessage() method. The most universally handled + message type for actors is the bs.DieMessage. + + # another way to kill the flag from the example above: + # we can safely call this on any bombsquad type with a 'handleMessage' + # method. (though its not guaranteed to always have a meaningful effect) + self.flag.handleMessage(bs.DieMessage()) + """ + + def __init__(self): + """ + Instantiates an actor in the current bs.Activity. + """ + # we technically shouldn't need to store a ref to + # the current activity; it should just come along in our context.. + activity = bs.getActivity() + self._activity = weakref.ref(activity) + + # update; we now *always* add a weak-ref... + activity._addActorWeakRef(self) + + def __del__(self): + try: + # non-finalized actors send themselves a DieMessage when going down + if not self.isFinalized(): + self.handleMessage(DieMessage()) + except Exception: + bs.printException('exception in bs.Actor.__del__() for', self) + + def handleMessage(self, msg): + """ + General message handling; can be passed any message object. + """ + pass + + def _handleMessageSanityCheck(self): + if self.isFinalized(): + bs.printError('handleMessage called on finalized actor', self) + + def autoRetain(self): + """ + Automatically keeps this bs.Actor in existence by storing a + reference to it with the bs.Activity it was created in. The reference + is released once the actor no longer exists (see bs.Actor.exists()) + or when the Activity is finalized. This can be a convenient alternative + to storing references explicitly just to keep a bs.Actor from dying. + For convenience, this method returns the bs.Actor it is called with, + enabling chained statements such as: myFlag = bs.Flag().autoRetain() + """ + activity = self._activity() + if activity is None: + raise Exception("actor's activity not found") + activity._retainActor(self) + return self + + def onFinalize(self): + """ + onFinalize is called for each remaining bs.Actor when its bs.Activity + is shutting down. Actors can use this opportunity to clear callbacks + or other references which have the potential of keeping the + bs.Activity alive inadvertantly. + + Once an actor has been finalized (see bs.Actor.isFinalized()) it should + no longer perform any game-object manipulation (creating, modifying, + or deleting nodes, media, timers, etc.) Attempts to do so will + likely result in errors. + """ + pass + + def isFinalized(self): + """ + Returns whether the actor has been finalized. + (see bs.Actor.onFinalize()) + """ + activity = self.getActivity(exceptionOnNone=False) + return True if activity is None else activity.isFinalized() + + def exists(self): + """ + Returns True if the actor is still visible or present in some way. + Note that a dying character should still return True here as long + as their corpse exists; this is about presence, not being 'alive'. + + If this returns False, it is assumed the actor can be completely + deleted without affecting the game; this call is often used + when 'pruning' lists of actors. + + The default implementation of this method returns 'node.exists()' + if the actor has a 'node' attr; otherwise True. + """ + + # as a sensible default, return the existance of self.node + node = getattr(self, 'node', None) + if node is not None: + return node.exists() + else: + return True + + def isAlive(self): + """ + Returns whether the Actor is 'alive'. + What this means is up to the actor. + It is not a requirement for actors to be + able to die; just that they report whether + they are alive or not. + """ + return True + + def getActivity(self, exceptionOnNone=True): + """ + Return the bs.Activity this Actor is associated with. + If the activity no longer exists, returns None. + """ + a = self._activity() + if a is None and exceptionOnNone: + raise Exception("Activity not found") + return a + + +class NodeActor(Actor): + """ + category: Game Flow Classes + + A simple bs.Actor which wraps around a single bs.Node and kills + the node when told to die. This allows you to take advantage of + standard actor behavior such as dying when no longer referenced, + so you can do things like kill off a bunch of nodes simply by + clearing a python list, etc. + + Attributes: + + node + The wrapped node. + """ + + def __init__(self, node): + """ + Instantiate with a given bs.Node. + """ + Actor.__init__(self) + self.node = node + + def handleMessage(self, msg): + if isinstance(msg, DieMessage): + self.node.delete() + + +class Session(object): + """ + category: Game Flow Classes + + A Session is the highest level control structure in the game. + Types of sessions are bs.FreeForAllSession, bs.TeamsSession, and + bs.CoopSession. + + A Session is responsible for wrangling and transitioning between various + bs.Activity instances such as mini-games and score-screens, and for + maintaining state between them (players, teams, score tallies, etc). + + Attributes: + + teams + All the bs.Teams in the Session. Most things should use the team + list in bs.Activity; not this. + + players + All bs.Players in the Session. Most things should use the player + list in bs.Activity; not this. + """ + + def __init__(self, teamNames=['Good Guys'], + teamColors=[(0.6, 0.2, 1.0)], + useTeamColors=True, + minPlayers=1, + maxPlayers=8, + allowMidActivityJoins=True): + """ + Instantiate a session with the provided info about' + teams and max players. + """ + + import bsLobby + import bsScoreSet + + # first thing, generate our link to our C layer equivalent.. + self._sessionData = bsInternal._registerSession(self) + + self._useTeams = (teamNames is not None) + self._useTeamColors = useTeamColors + self._inSetActivity = False + + self._allowMidActivityJoins = allowMidActivityJoins + + self.teams = [] + self.players = [] + self._nextTeamID = 0 + self._activityRetained = None + + # hacky way to create empty weak ref; must be a better way... + class EmptyObj: + pass + self._activityWeak = weakref.ref(EmptyObj()) + if self._activityWeak() is not None: + raise Exception("error creating empty weak ref") + + self._nextActivity = None + self._wantToEnd = False + self._ending = False + self._minPlayers = minPlayers + self._maxPlayers = maxPlayers + + if self._useTeams: + for i in range(len(teamColors)): + team = bs.Team( + teamID=self._nextTeamID, + name=GameActivity.getTeamDisplayString(teamNames[i]), + color=teamColors[i]) + self.teams.append(team) + self._nextTeamID += 1 + + try: + with bs.Context(self): + self.onTeamJoin(team) + except Exception: + bs.printException('exception in onTeamJoin for', self) + + self._lobby = bsLobby.Lobby() + self.scoreSet = bsScoreSet.ScoreSet() + + # instantiates our session globals node.. (so it can apply + # default settings) + bs.getSharedObject('globals') + + def onPlayerRequest(self, player): + """ + Called when a new bs.Player wants to join; + should return True or False to accept/reject. + """ + # limit player counts based on pro purchase/etc *unless* we're in a + # stress test + if bsUtils._gStressTestResetTimer is None: + + if len(self.players) >= self._maxPlayers: + + # print a rejection message *only* to the client trying to join + # (prevents spamming everyone else in the game) + bs.playSound(bs.getSound('error')) + bs.screenMessage( + bs.Lstr( + resource='playerLimitReachedText', + subs=[('${COUNT}', str(self._maxPlayers))]), + color=(0.8, 0.0, 0.0), + clients=[player.getInputDevice().getClientID()], + transient=True) + return False + + bs.playSound(bs.getSound('dripity')) + return True + + def onPlayerLeave(self, player): + """ + Called when a previously-accepted bs.Player leaves the session. + """ + # remove them from the game rosters + if player in self.players: + + bs.playSound(bs.getSound('playerLeft')) + + # this will be None if the player is still in the chooser + team = player.getTeam() + + activity = self._activityWeak() + + # if he had no team, he's in the lobby + # if we have a current activity with a lobby, ask them to remove him + if team is None: + with bs.Context(self): + try: + self._lobby.removeChooser(player) + except Exception: + bs.printException( + 'Error: exception in Lobby.removeChooser()') + + # *if* he was actually in the game, announce his departure + if team is not None: + bs.screenMessage( + bs.Lstr( + resource='playerLeftText', + subs=[('${PLAYER}', player.getName(full=True))])) + + # remove him from his team and session lists + # (he may not be on the team list since player are re-added to + # team lists every activity) + if team is not None and player in team.players: + + # testing.. can remove this eventually + if isinstance(self, bs.FreeForAllSession): + if len(team.players) != 1: + bs.printError("expected 1 player in FFA team") + team.players.remove(player) + + # remove player from any current activity + if activity is not None and player in activity.players: + activity.players.remove(player) + + # run the activity callback unless its been finalized + if not activity.isFinalized(): + try: + with bs.Context(activity): + activity.onPlayerLeave(player) + except Exception: + bs.printException( + 'exception in onPlayerLeave for activity', + activity) + else: + bs.printError( + "finalized activity in onPlayerLeave; shouldn't happen") + + player._setActivity(None) + + # reset the player - this will remove its actor-ref and clear + # its calls/etc + try: + with bs.Context(activity): + player._reset() + except Exception: + bs.printException( + 'exception in player._reset in' + ' onPlayerLeave for player', + player) + + # if we're a non-team session, remove the player's team completely + if not self._useTeams and team is not None: + + # if the team's in an activity, call its onTeamLeave callback + if activity is not None and team in activity.teams: + activity.teams.remove(team) + + if not activity.isFinalized(): + try: + with bs.Context(activity): + activity.onTeamLeave(team) + except Exception: + bs.printException( + 'exception in onTeamLeave for activity', + activity) + else: + bs.printError( + "finalized activity in onPlayerLeave p2" + "; shouldn't happen") + + # clear the team's game-data (so dying stuff will + # have proper context) + try: + with bs.Context(activity): + team._resetGameData() + except Exception: + bs.printException( + 'exception clearing gameData for team:', + team, 'for player:', player, + 'in activity:', activity) + + # remove the team from the session + self.teams.remove(team) + try: + with bs.Context(self): + self.onTeamLeave(team) + except Exception: + bs.printException( + 'exception in onTeamLeave for session', self) + # clear the team's session-data (so dying stuff will + # have proper context) + try: + with bs.Context(self): + team._resetSessionData() + except Exception: + bs.printException( + 'exception clearing sessionData for team:', team, + 'in session:', self) + + # now remove them from the session list + self.players.remove(player) + + else: + print ('ERROR: Session.onPlayerLeave called' + ' for player not in our list.') + + def end(self): + """ + Initiates an end to the session and a return to the main menu. + Note that this happens asynchronously, allowing the + session and its activities to shut down gracefully. + """ + self._wantToEnd = True + if self._nextActivity is None: + self._launchEndActivity() + + def _launchEndActivity(self): + with bs.Context(self): + if self._ending: + # ignore repeats unless its been a while.. + sinceLast = bs.getRealTime()-self._launchEndActivityTime + if sinceLast < 3000: + return + bs.printError( + "_launchEndActivity called twice (sinceLast=" + + str(sinceLast) + ")") + self._launchEndActivityTime = bs.getRealTime() + self.setActivity(bs.newActivity(EndSessionActivity)) + self._wantToEnd = False + self._ending = True # prevents further activity-mucking + + def onTeamJoin(self, team): + 'Called when a new bs.Team joins the session.' + pass + + def onTeamLeave(self, team): + 'Called when a bs.Team is leaving the session.' + pass + + def _activityEnd(self, activity, results): + # run the subclass callback in the session context + try: + with bs.Context(self): + self.onActivityEnd(activity, results) + except Exception: + bs.printException( + 'exception in onActivityEnd() for session', self, 'activity', + activity, 'with results', results) + + def handleMessage(self, msg): + 'General message handling; can be passed any message object.' + import bsLobby + if isinstance(msg, bsLobby.PlayerReadyMessage): + self._onPlayerReady(msg.chooser) + + elif isinstance(msg, EndActivityMessage): + + # if the whole session is shutting down, ignore these.. + if self._ending: + return + + # only pay attention if this is coming from our current activity.. + if msg.activity is self._activityRetained: + + # if this activity hasn't begun yet, just make note of what to + # do after we begin it + if not msg.activity._hasBegun: + if not msg.activity._shouldEndImmediately or msg.force: + msg.activity._shouldEndImmediately = True + msg.activity._shouldEndImmediatelyResults = msg.results + msg.activity._shouldEndImmediatelyDelay = msg.delay + + # the activity has already begun; get ready to end it.. + else: + if (not msg.activity._hasEnded) or msg.force: + # set a timer to set in motion this activity's demise + msg.activity._hasEnded = True + self._activityEndTimer = bs.Timer( + msg.delay, bs.Call( + self._activityEnd, msg.activity, msg.results), + timeType='net') + + elif isinstance(msg, PlayerProfilesChangedMessage): + # if we have a current activity with a lobby, ask it to + # reload profile + with bs.Context(self): + self._lobby.reloadProfiles() + + else: + Session.handleMessage(self, msg) + + def setActivity(self, activity): + """ + Assign a new current bs.Activity for the session. + Note that this will not change the current context to the new + Activity's. Code must be run in the new activity's methods + (onTransitionIn, etc) to get it. (so you can't do + session.setActivity(foo) and then bs.newNode() to add a node to foo) + """ + + # sanity test - make sure this doesn't get called recursively + if self._inSetActivity: + raise Exception( + "Session.setActivity() cannot be called recursively.") + + if activity.getSession() != bs.getSession(): + raise Exception("provided activity's session is not current") + + # quietly ignore this if we're currently going down + if self._ending: + return + + if activity is self._activityRetained: + bs.printError("activity set to already-current activity") + return + + if self._nextActivity is not None: + raise Exception( + "Activity switch already in progress (to " + + str(self._nextActivity) + ")") + + self._inSetActivity = True + + prevActivity = self._activityRetained + + if prevActivity is not None: + with bs.Context(prevActivity): + prevGlobals = bs.getSharedObject('globals') + else: + prevGlobals = None + + with bs.Context(activity): + g = bs.getSharedObject('globals') + g.useFixedVROverlay = activity._useFixedVROverlay + g.allowKickIdlePlayers = activity._allowKickIdlePlayers + if activity._inheritsSlowMotion and prevGlobals is not None: + g.slowMotion = prevGlobals.slowMotion + else: + g.slowMotion = activity._isSlowMotion + if activity._inheritsMusic and prevGlobals is not None: + g.musicContinuous = True # prevents restarting same music + g.music = prevGlobals.music + g.musicCount += 1 + if activity._inheritsCameraVROffset and prevGlobals is not None: + g.vrCameraOffset = prevGlobals.vrCameraOffset + if activity._inheritsVROverlayCenter and prevGlobals is not None: + g.vrOverlayCenter = prevGlobals.vrOverlayCenter + g.vrOverlayCenterEnabled = prevGlobals.vrOverlayCenterEnabled + + # if they want to inherit tint from the previous activity.. + if activity._inheritsTint and prevGlobals is not None: + g.tint = prevGlobals.tint + g.vignetteOuter = prevGlobals.vignetteOuter + g.vignetteInner = prevGlobals.vignetteInner + activity._hasTransitionedIn = True + activity.onTransitionIn() + + self._nextActivity = activity + + # if we have a current activity, tell it it's transitioning out; + # the next one will become current once this one dies. + if prevActivity is not None: + prevActivity._transitioningOut = True + + # activity will be None until the next one begins + with bs.Context(prevActivity): + prevActivity.onTransitionOut() + + # setting this to None should free up the old activity to die + # which will call _beginNextActivity. + # we can still access our old activity through self._activityWeak() + # to keep it up to date on player joins/departures/etc until it dies + self._activityRetained = None + + # theres no existing activity; lets just go ahead with the begin call + else: + self._beginNextActivity() + + # tell the C layer that this new activity is now 'foregrounded' + # this means that its globals node controls global stuff and + # stuff like console operations, keyboard shortcuts, etc will run in it + activity._activityData.makeForeground() + + # we want to call _destroy() for the previous activity once it should + # tear itself down, clear out any self-refs, etc. If the new activity + # has a transition-time, set it up to be called after that passes; + # otherwise call it immediately. After this call the activity should + # have no refs left to it and should die (which will trigger the next + # activity to run) + if prevActivity is not None: + if activity._transitionTime > 0: + # fixme - we should tweak the activity to not allow + # node-creation/etc when we call _destroy (or after) + with bs.Context('UI'): + bs.realTimer(activity._transitionTime, + prevActivity._destroy) + # just run immediately + else: + prevActivity._destroy() + + self._inSetActivity = False + + def getActivity(self): + 'Returns the current foreground activity for this session.' + return self._activityWeak() + + def getCustomMenuEntries(self): + """ + Subclasses can override this to provide custom menu entries. + The returned value should be a list of dicts, each containing + a 'label' and 'call' entry, with 'label' being the text for + the entry (translated) and 'call' being the callable to trigger + if the entry is pressed. + """ + return [] + + def _requestPlayer(self, player): + + # if we're ending, allow no new players + if self._ending: + return False + + # ask the user + try: + with bs.Context(self): + result = self.onPlayerRequest(player) + except Exception: + bs.printException( + 'exception in session onPlayerRequest call for', self) + result = False + + # if the user said yes, add the player to the session list + if result: + self.players.append(player) + + activity = self._activityWeak() + + # if we have a current activity with a lobby, + # ask it to bring up a chooser for this player. + # otherwise they'll have to wait around for the next activity. + with bs.Context(self): + try: + self._lobby.addChooser(player) + except Exception: + bs.printException('exception in lobby.addChooser()') + + return result + + def onActivityEnd(self, activity, results): + """ + Called when the current bs.Activity has ended. + The session should look at the results and start + another activity. + """ + pass + + def _beginNextActivity(self): + """ + Called once the previous activity has been totally torn down; + this means we're ready to begin the next one + """ + if self._nextActivity is not None: + + # we store both a weak and a strong ref to the new activity; + # the strong is to keep it alive and the weak is so we can access + # it even after we've released the strong-ref to allow it to die + self._activityRetained = self._nextActivity + self._activityWeak = weakref.ref(self._nextActivity) + self._nextActivity = None + # lets kick out any players sitting in a chooser since + # new activities such as score screens could cover them up; + # better to have them rejoin + self._lobby.removeAllChoosersAndKickPlayers() + self._activityWeak()._begin(self) + + def _onPlayerReady(self, chooser): + 'Called when a bs.Player has chosen a character and is ready to join.' + + lobby = chooser.getLobby() + + activity = self._activityWeak() + + # in joining activities, we wait till all choosers are ready + # and then create all players at once + if activity is not None and activity._isJoiningActivity: + if lobby.checkAllReady(): + choosers = lobby.getChoosers() + minPlayers = self._minPlayers + if len(choosers) >= minPlayers: + for chooser in lobby.getChoosers(): + self._addChosenPlayer(chooser) + lobby.removeAllChoosers() + # get our next activity going.. + self._activityEnd(activity, {}) + else: + bs.screenMessage( + bs.Lstr( + resource='notEnoughPlayersText', + subs=[('${COUNT}', str(minPlayers))]), + color=(1, 1, 0)) + bs.playSound(bs.getSound('error')) + else: + return + # otherwise just add players on the fly + else: + player = self._addChosenPlayer(chooser) + lobby.removeChooser(chooser.getPlayer()) + + def _addChosenPlayer(self, chooser): + + player = chooser.getPlayer() + if not player in self.players: + bs.printError('player not found in session ' + 'player-list after chooser selection') + + activity = self._activityWeak() + + # we need to reset the player's input here, as it is currently + # referencing the chooser which could inadvertantly keep it alive + player.resetInput() + + # pass it to the current activity if it has already begun + # (otherwise it'll get passed once begin is called) + passToActivity = ( + activity + is not None and activity.hasBegun() + and not activity._isJoiningActivity) + + # if we're not allowing mid-game joins, dont' pass; just announce + # the arrival + if passToActivity: + if not self._allowMidActivityJoins: + passToActivity = False + with bs.Context(self): + bs.screenMessage( + bs.Lstr(resource='playerDelayedJoinText', subs=[ + ('${PLAYER}', player.getName(full=True))]), + color=(0, 1, 0)) + + # if we're a non-team game, each player gets their own team + # (keeps mini-game coding simpler if we can always deal with teams) + if self._useTeams: + team = chooser.getTeam() + else: + ourTeamID = self._nextTeamID + team = bs.Team( + teamID=ourTeamID, name=chooser.getPlayer().getName( + full=True, icon=False), + color=chooser.getColor()) + self.teams.append(team) + self._nextTeamID += 1 + try: + with bs.Context(self): + self.onTeamJoin(team) + except Exception: + bs.printException('exception in onTeamJoin for', self) + + if passToActivity: + if team in activity.teams: + bs.printError( + "Duplicate team ID in bs.Session._addChosenPlayer") + activity.teams.append(team) + try: + with bs.Context(activity): + activity.onTeamJoin(team) + except Exception: + bs.printException( + 'ERROR: exception in onTeamJoin for', activity) + + player._setData(team=team, + character=chooser.getCharacterName(), + color=chooser.getColor(), + highlight=chooser.getHighlight()) + + self.scoreSet.registerPlayer(player) + + if passToActivity: + + if isinstance(self, bs.FreeForAllSession): + if len(player.getTeam().players) != 0: + bs.printError("expected 0 players in FFA team") + + # dont actually add the player to their team list if we're not + # in an activity; (players get (re)added to their team lists + # when the activity begins) + player.getTeam().players.append(player) + + if player in activity.players: + bs.printException( + 'Duplicate player in bs.Session._addChosenPlayer:', player) + else: + activity.players.append(player) + player._setActivity(activity) + activity._createPlayerNode(player) + try: + with bs.Context(activity): + activity.onPlayerJoin(player) + except Exception: + bs.printException('Error on onPlayerJoin for', activity) + return player + + +class EndActivityMessage(object): + def __init__(self, activity, results=None, delay=0, force=False): + self.activity = activity + self.results = results + self.delay = delay + self.force = force + + +class PlayerProfilesChangedMessage(object): + """ + Signifies player profiles may have changed and should be reloaded if they + are being used. + """ + pass + + +class Activity(object): + """ + category: Game Flow Classes + + Units wrangled by a bs.Session. Examples of Activities include games, + score-screens, cutscenes, etc. A bs.Session has one 'current' Activity at + any time, though their existence can overlap during transitions. + + Attributes: + + settings + The settings dict passed in when the activity was made. + + teams + The list of bs.Teams in the activity. This gets populated just before + onBegin() is called and is updated automatically as players join or + leave the game. (at least in free-for-all mode where every player gets + their own team; in teams mode there are always 2 teams regardless of + the player count). + + players + The list of bs.Players in the activity. This gets populated just + before onBegin() is called and is updated automatically as players + join or leave the game. + """ + + def __init__(self, settings={}): + """ + Creates an activity in the current bs.Session. + The activity will not be actually run until bs.Session.setActivity() + is called. + 'settings' should be a dict of key/value pairs specific to the activity. + + Activities should preload as much of their media/etc as possible in + their constructor, but none of it should actually be used until they + are transitioned in. + """ + + # first thing, generate our link to our C layer equivalent. + self._activityData = bsInternal._registerActivity(self) + session = bs.getSession() + self._session = weakref.ref(session) + + if session is None: + raise Exception("No current session") + if type(settings) is not dict: + raise Exception("expected dict for settings") + if bs.getActivity(exceptionOnNone=False) is not self: + raise Exception('invalid context state') + + self.settings = settings + + self._hasTransitionedIn = False + self._hasBegun = False + self._hasEnded = False + self._shouldEndImmediately = False + + self._isWaitingForContinue = False + self._continueCost = bsInternal._getAccountMiscReadVal( + 'continueStartCost', + 25) + self._continueCostMult = bsInternal._getAccountMiscReadVal( + 'continuesMult', + 2) + self._continueCostOffset = bsInternal._getAccountMiscReadVal( + 'continuesOffset', + 0) + + self._finalized = False + + # whether to print every time a player dies. This can be pertinant + # in games such as Death-Match but can be annoying in games where it + # doesn't matter + self.announcePlayerDeaths = False + + # joining activities are for waiting for initial player joins + # they are treated slightly differently than regular activities, + # mainly in that all players are passed to the activity at once + # instead of as each joins. + self._isJoiningActivity = False + + # whether game-time should still progress when in menus/etc + self._allowPausing = False + + # whether idle players can potentially be kicked (should not happen in + # menus/etc) + self._allowKickIdlePlayers = True + + # in vr mode, this determines whether overlay nodes (text,images,etc) + # are created at a fixed position in space or one that moves based on + # the current map. generally this should be on for games and off for + # transitions/score-screens/etc + # that persist between maps. + self._useFixedVROverlay = False + + # if True, runs in slow motion and turns down sound pitch + self._isSlowMotion = False + + # set this to True to inherit slow motion setting from previous activity + # (useful for transitions to avoid hitches) + self._inheritsSlowMotion = False + + # set this to True to keep playing the music from the previous activity + # (without even restarting it) + self._inheritsMusic = False + + # set this to true to inherit VR camera offsets from the previous + # activity (useful for preventing sporadic camera movement + # during transitions) + self._inheritsCameraVROffset = False + + # set this to true to inherit (non-fixed) VR overlay positioning from + # the previous activity (useful for prevent sporadic overlay jostling + # during transitions) + self._inheritsVROverlayCenter = False + + # set this to true to inherit screen tint/vignette colors from the + # previous activity (useful to prevent sudden color changes during + # transitions) + self._inheritsTint = False + + # if the activity fades or transitions in, it should set the length of + # time here so that previous activities will be kept alive for that + # long (avoiding 'holes' in the screen) + # note that this time value is in real-time; not game-time. + self._transitionTime = 0 + + # this gets set once another activity has begun transitioning in but + # before this one is killed. (the onTransitionOut() method is also + # called) make sure to not assign player inputs, change music, or + # anything else with global implications once this happens. + self._transitioningOut = False + + # a handy place to put most actors; this list is pruned of dead + # actors regularly and these actors are insta-killed as the activity + # is dying. + self._actorRefs = [] + + self._actorWeakRefs = [] + self._ownedNodes = [] + + self._lastDeadObjectPruneTime = bs.getGameTime() + + # this stuff gets filled in just before onBegin() is called + self.teams = [] + self.players = [] + self.scoreSet = None + + self._useLobby = True + self._lobby = None + + def onFinalize(self): + """ + Called when your activity is being finalized. + If your activity has created anything explicitly that may be retaining + a strong reference to the activity and preventing it from dying, you + should tear that down here. From this point on your activity's sole + purpose in life is to hit zero references and die so the next activity + can begin. + """ + pass + + def isFinalized(self): + """ + The activity is set as finalized when shutting down. + At this point no new nodes, timers, etc should be made, + run, etc, and the activity should be considered to be a 'zombie'. + """ + return self._finalized + + def __del__(self): + + # if the activity has been run, we've explicitly cleaned it up, + # but we need to run finalize here for un-run activities + if not self._finalized: + with bs.Context('empty'): + self._finalize() + + # since we're mostly between activities at this point, lets run a cycle + # of garbage collection; hopefully it won't cause hitches here + bsUtils.garbageCollect(sessionEnd=False) + + # now that our object is officially gonna be dead, tell the session to + # fire up the next activity + if self._transitioningOut: + session = self._session() + if session is not None: + with bs.Context(session): + if getattr(self, '_canShowAdOnDeath', False): + bsUtils._callAfterAd(session._beginNextActivity) + else: + bs.pushCall(session._beginNextActivity) + + def _getPlayerIcon(self, player): + # do we want to cache these somehow?.. + info = player._getIconInfo() + return {'texture': bs.getTexture(info['texture']), + 'tintTexture': bs.getTexture(info['tintTexture']), + 'tintColor': info['tintColor'], + 'tint2Color': info['tint2Color']} + + def _destroy(self): + + # create a real-timer that watches a weak-ref of this activity + # and reports any lingering references keeping it alive.. + # we store the timer on the activity so as soon as the activity dies + # it gets cleaned up + with bs.Context('UI'): + r = weakref.ref(self) + self._activityDeathCheckTimer = bs.Timer( + 5000, bs.Call(self._checkActivityDeath, r, [0]), + repeat=True, timeType='real') + + # run _finalize in an empty context; nothing should be happening in + # there except deleting things which requires no context. + # (plus, _finalize() runs in the destructor for un-run activities + # and we can't properly provide context in that situation anyway; might + # as well be consistent) + if not self._finalized: + with bs.Context('empty'): + self._finalize() + else: + raise Exception("_destroy() called multiple times") + + def _continueChoice(self, doContinue): + self._isWaitingForContinue = False + if self.hasEnded(): + return + with bs.Context(self): + if doContinue: + bs.playSound(bs.getSound('shieldUp')) + bs.playSound(bs.getSound('cashRegister')) + bsInternal._addTransaction({'type': 'CONTINUE', + 'cost': self._continueCost}) + bsInternal._runTransactions() + self._continueCost = ( + self._continueCost * self._continueCostMult + + self._continueCostOffset) + self.onContinue() + else: + self.endGame() + + def isWaitingForContinue(self): + """Returns whether or not this activity is currently waiting for the + player to continue (or timeout)""" + return self._isWaitingForContinue + + def continueOrEndGame(self): + """If continues are allowed, prompts the player to purchase a continue + and calls either endGame or continueGame depending on the result""" + + import bsUI + + try: + if bsInternal._getAccountMiscReadVal('enableContinues', False): + + # we only support continuing in non-tournament games + try: + tournamentID = self.getSession()._tournamentID + except Exception: + tournamentID = None + if tournamentID is None: + + # we currently only support continuing in sequential + # co-op campaigns + if isinstance(self.getSession(), bs.CoopSession): + if self.getSession()._campaign.isSequential(): + g = bs.getSharedObject('globals') + + # only attempt this if we're not currently paused + # and there appears to be no UI + if (not g.paused + and bsUI.uiGlobals['mainMenuWindow'] + is None + or not bsUI.uiGlobals['mainMenuWindow'] + .exists()): + self._isWaitingForContinue = True + with bs.Context('UI'): + bs.realTimer( + 500, lambda: bsUI.ContinueWindow( + self, self._continueCost, + continueCall=bs.WeakCall( + self._continueChoice, True), + cancelCall=bs.WeakCall( + self._continueChoice, False))) + return + + except Exception: + bs.printException("error continuing game") + + self.endGame() + + @classmethod + def _checkActivityDeath(cls, activityRef, counter): + try: + import gc + import types + a = activityRef() + print 'ERROR: Activity is not dying when expected:', a,\ + '(warning '+str(counter[0]+1)+')' + print 'This means something is still strong-referencing it.' + + counter[0] += 1 + + # FIXME - running the code below shows us references but winds up + # keeping the object alive... need to figure out why. + # for now we just print refs if the count gets to 3, and then we + # kill the app at 4 so it doesn't matter anyway.. + if counter[0] == 3: + print 'Activity references for', a, ':' + refs = list(gc.get_referrers(a)) + i = 1 + for ref in refs: + if type(ref) is types.FrameType: + continue + print ' reference', i, ':', ref + i += 1 + if counter[0] == 4: + print 'Killing app due to stuck activity... :-(' + bs.quit() + + except Exception: + bs.printException('exception on _checkActivityDeath:') + + def _finalize(self): + + self._finalized = True + + # do some default cleanup + try: + + try: + self.onFinalize() + except Exception: + bs.printException( + 'Exception in onFinalize() for activity', self) + + # send finalize notices to all remaining actors + for actorRef in self._actorWeakRefs: + try: + actor = actorRef() + if actor is not None: + actor.onFinalize() + except Exception: + bs.printException('Exception on bs.Activity._finalize()' + ' in actor onFinalize():', actorRef()) + + # reset all players (releases any attached actors, clears + # game-data, etc) + for player in self.players: + if player.exists(): + try: + player._reset() + player._setActivity(None) + except Exception: + bs.printException('Exception on bs.Activity._finalize()' + ' resetting player:', player) + + # ditto with teams + for team in self.teams: + try: + team._reset() + except Exception: + bs.printException( + 'Exception on bs.Activity._finalize() resetting team:', + player) + + except Exception: + bs.printException('Exception during bs.Activity._finalize():') + + # regardless of what happened here, we want to destroy our data, as + # our activity might not go down if we don't. # This will kill all + # timers, nodes, etc, which should clear up any remaining refs to our + # actors and activity and allow us to die peacefully. + try: + self._activityData.destroy() + except Exception: + bs.printException( + 'Exception during bs.Activity._finalize() destroying data:') + + def _pruneDeadObjects(self): + try: + self._actorRefs = [a for a in self._actorRefs if a.exists()] + except Exception: + bs.printException('exc pruning '+str(self._actorRefs)) + self._actorWeakRefs = [a for a in self._actorWeakRefs + if a() is not None and a().exists()] + self._lastDeadObjectPruneTime = bs.getGameTime() + + def _retainActor(self, a): + if not isinstance(a, bs.Actor): + raise Exception("non-actor passed to _retainActor") + if (self.hasTransitionedIn() and bs.getGameTime() + - self._lastDeadObjectPruneTime > 10000): + bs.printError('it looks like nodes/actors are not' + ' being pruned in your activity;' + ' did you call Activity.onTransitionIn()' + ' from your subclass?; ' + + str(self) + ' (loc. a)') + self._actorRefs.append(a) + + def _addActorWeakRef(self, a): + if not isinstance(a, bs.Actor): + raise Exception("non-actor passed to _addActorWeakRef") + if (self.hasTransitionedIn() + and bs.getGameTime() - self._lastDeadObjectPruneTime > 10000): + bs.printError('it looks like nodes/actors are ' + 'not being pruned in your activity;' + ' did you call Activity.onTransitionIn()' + ' from your subclass?; '+ str(self) + ' (loc. b)') + self._actorWeakRefs.append(weakref.ref(a)) + + def getSession(self): + """ + Returns the bs.Session this activity belongs to. + If the session no longer exists, returns None. + """ + session = self._session() + return session + + def onPlayerJoin(self, player): + 'Called for all new bs.Players (including the initial set of them).' + pass + + def onPlayerLeave(self, player): + 'Called when a player is leaving the activity.' + pass + + def onTeamJoin(self, team): + """ + Called when a new bs.Team enters the activity + (including the initial set of them).' + """ + pass + + def onTeamLeave(self, team): + 'Called when a bs.Team leaves the activity.' + pass + + def onTransitionIn(self): + """ + Called when your activity is first becoming visible; + It should fade in backgrounds, start playing music, etc. + It does not yet have access to bs.Players or bs.Teams, however, + until bs.Activity.onBegin() is called; they are still owned + by the previous activity up until this point. + """ + + self._calledActivityOnTransitionIn = True + + # start pruning our transient actors periodically + self._pruneDeadObjectsTimer = bs.Timer( + 5000, bs.WeakCall(self._pruneDeadObjects), repeat=True) + self._pruneDeadObjects() + + # also start our low-level scene-graph running + self._activityData.start() + + def onTransitionOut(self): + """ + Called when your activity starts transitioning out and a new + activity is on the way in. Note that this may happen at any + time even if finish() has not been called. + """ + pass + + def onBegin(self): + """ + Called once the previous bs.Activity has finished transitioning out. + At this point the activity's initial players and teams are filled in + and it should begin its actual game logic. + """ + self._calledActivityOnBegin = True + + def onContinue(self): + """ + This is called if a game supports and offers a continue and the player + accepts. In this case the player should be given an extra life or + whatever is relevant to keep the game going. + """ + pass + + def handleMessage(self, msg): + 'General message handling; can be passed any message object.' + pass + + def end(self, results=None, delay=0, force=False): + """ + Commences activity shutdown and delivers results to the bs.Session. + 'delay' is the time delay before the activity actually ends + (in milliseconds). Further calls to end() will be ignored up until + this time, unless 'force' is True, in which case the new results + will replace the old. + """ + + # if results is a standard team-game-results, associate it with us + # so it can grab our score prefs + if isinstance(results, bs.TeamGameResults): + results._setGame(self) + + # if we had a standard time-limit that had not expired, stop it so + # it doesnt tick annoyingly + if (hasattr(self, '_standardTimeLimitTime') + and self._standardTimeLimitTime > 0): + self._standardTimeLimitTimer = self._standardTimeLimitText = None + + # ditto with tournament time limits + if (hasattr(self, '_tournamentTimeLimitTime') + and self._tournamentTimeLimitTime > 0): + self._tournamentTimeLimitTimer = self._tournamentTimeLimitText = \ + self._tournamentTimeLimitTitleText = None + + self.getSession()\ + .handleMessage(EndActivityMessage(self, results, delay, force)) + + def hasTransitionedIn(self): + 'Returns whether onTransitionIn() has been called for this activity.' + return self._hasTransitionedIn + + def hasBegun(self): + 'Returns whether onBegin() has been called for this activity.' + return self._hasBegun + + def hasEnded(self): + 'Returns whether end() has been called for this activity.' + return self._hasEnded + + def isTransitioningOut(self): + 'Returns whether onTransitionOut() has been called for this activity.' + return self._transitioningOut + + def _createPlayerNode(self, player): + with bs.Context(self): + player.gameData['_playerNode'] = bs.NodeActor( + bs.newNode('player', attrs={'playerID': player.getID()})) + + def _begin(self, session): + 'private call to set up onBegin' + + if self._hasBegun: + bs.printError("_begin called twice; this shouldn't happen") + return + + self.scoreSet = session.scoreSet + + # operate on the subset of session players who have passed team/char + # selection + players = [] + chooserPlayers = [] + for p in session.players: + if p.exists(): + if p.getTeam() is not None: + p.resetInput() + players.append(p) + else: + # simply ignore players sitting in a player-chooser + # this technically shouldn't happen anymore since choosers + # now get cleared when starting new activities... + chooserPlayers.append(p) + else: + bs.printError("got nonexistant player in Activity._begin()") + + # add teams in one by one and send team-joined messages for each + for team in session.teams: + if team in self.teams: + raise Exception("Duplicate Team Entry") + self.teams.append(team) + try: + with bs.Context(self): + self.onTeamJoin(team) + except Exception: + bs.printException('ERROR: exception in onTeamJoin for', self) + + # now add each player to the activity and to its team's list, + # and send player-joined messages for each + for player in players: + self.players.append(player) + + player.getTeam().players.append(player) + player._setActivity(self) + self._createPlayerNode(player) + try: + with bs.Context(self): + self.onPlayerJoin(player) + except Exception: + bs.printException('exception in onPlayerJoin for', self) + + with bs.Context(self): + # and finally tell the game to start + self._hasBegun = True + self.onBegin() + + # make sure that bs.Activity.onTransitionIn() got called at some point + if not hasattr(self, '_calledActivityOnTransitionIn'): + bs.printError( + "bs.Activity.onTransitionIn() never got called for " + + str(self) + + "; did you forget to call it in your onTransitionIn override?") + else: + del self._calledActivityOnTransitionIn + + # make sure that bs.Activity.onBegin() got called at some point + if not hasattr(self, '_calledActivityOnBegin'): + bs.printError( + "bs.Activity.onBegin() never got called for " + str(self) + + "; did you forget to call it in your onBegin override?") + else: + del self._calledActivityOnBegin + + # if the whole session wants to die and was waiting on us, can get + # that going now.. + if session._wantToEnd: + session._launchEndActivity() + else: + # otherwise, if we've already been told to die, do so now.. + if self._shouldEndImmediately: + self.end(self._shouldEndImmediatelyResults, + self._shouldEndImmediatelyDelay) + + +class EndSessionActivity(Activity): + """ Special activity to fade out and end the current session """ + + def __init__(self, settings={}): + Activity.__init__(self, settings) + self._transitionTime = 250 # keeps prev activity alive while we fadeout + self._useLobby = False + self._inheritsTint = True + self._inheritsSlowMotion = True + self._inheritsCameraVROffset = True + self._inheritsVROverlayCenter = True + + def onTransitionIn(self): + Activity.onTransitionIn(self) + bsInternal._fadeScreen(False, time=250) + bsInternal._lockAllInput() + + def onBegin(self): + import bsMainMenu + Activity.onBegin(self) + bsInternal._unlockAllInput() + bsUtils._callAfterAd(bs.Call(bsInternal._newHostSession, + bsMainMenu.MainMenuSession)) + + +class JoiningActivity(Activity): + """ + Standard joining activity that shows tips and waits for all players to join. + """ + + def __init__(self, settings={}): + Activity.__init__(self, settings) + + # this activity is a special 'joiner' activity + # - it will get shut down as soon as all players have checked ready + self._isJoiningActivity = True + + # players may be idle waiting for joiners; lets not kick them for it + self._allowKickIdlePlayers = False + + # in vr mode we dont want stuff moving around + self._useFixedVROverlay = True + + def onTransitionIn(self): + Activity.onTransitionIn(self) + self._background = bsUtils.Background( + fadeTime=500, startFaded=True, showLogo=True) + self._tipsText = bsUtils.TipsText() + bs.playMusic('CharSelect') + + self._joinInfo = self.getSession()._lobby._createJoinInfo() + + bsInternal._setAnalyticsScreen('Joining Screen') + + +class TransitionActivity(Activity): + """ + A simple overlay fade out/in; useful as a bare minimum transition + between two level based activities. + """ + + def __init__(self, settings={}): + Activity.__init__(self, settings) + self._transitionTime = 500 # keeps prev activity alive while we fade in + self._inheritsSlowMotion = True # dont change.. + self._inheritsTint = True # dont change + self._inheritsCameraVROffset = True # dont change + self._inheritsVROverlayCenter = True + self._useFixedVROverlay = True + + def onTransitionIn(self): + Activity.onTransitionIn(self) + self._background = bsUtils.Background( + fadeTime=500, startFaded=False, showLogo=False) + + def onBegin(self): + Activity.onBegin(self) + # die almost immediately + bs.gameTimer(100, self.end) + + +class ScoreScreenActivity(Activity): + """ + A standard score screen that fades in and shows stuff for a while. + After a specified delay, player input is assigned to send an + EndActivityMessage. + """ + + def __init__(self, settings={}): + Activity.__init__(self, settings) + self._transitionTime = 500 + self._birthTime = bs.getGameTime() + self._minViewTime = 5000 + self._inheritsTint = True + self._inheritsCameraVROffset = True + self._useFixedVROverlay = True + self._allowServerRestart = False + + def onPlayerJoin(self, player): + Activity.onPlayerJoin(self, player) + timeTillAssign = max( + 0, self._birthTime+self._minViewTime-bs.getGameTime()) + # if we're still kicking at the end of our assign-delay, assign this + # guy's input to trigger us + bs.gameTimer(timeTillAssign, bs.WeakCall(self._safeAssign, player)) + + def _safeAssign(self, player): + # just to be extra careful, don't assign if we're transitioning out.. + # (though theoretically that would be ok) + if not self.isTransitioningOut() and player.exists(): + player.assignInputCall( + ('jumpPress', 'punchPress', 'bombPress', 'pickUpPress'), + self._playerPress) + + def _playerPress(self): + + # If we're running in server-mode and the config is dirty, this is a + # good time to either restart or quit + if self._allowServerRestart and bsUtils._gServerConfigDirty: + if bsUtils._gServerConfig.get('quit', False): + if not getattr(self, '_kickedOffServerShutdown', False): + if bsUtils._gServerConfig.get('quitReason') == 'restarting': + # FIXME - should add a server-screen-message call + # or something. + bsInternal._chatMessage( + bs.Lstr(resource='internal.serverRestartingText') + .evaluate()) + print 'Exiting for server-restart at ' \ + + time.strftime('%c') + else: + print 'Exiting for server-shutdown at ' \ + + time.strftime('%c') + with bs.Context('UI'): + bs.realTimer(2000, bs.quit) + self._kickedOffServerShutdown = True + return + else: + if not getattr(self, '_kickedOffServerRestart', False): + print 'Running updated server config at ' \ + + time.strftime('%c') + with bs.Context('UI'): + bs.realTimer(1000, bs.Call( + bs.pushCall, bsUtils._runServer)) + self._kickedOffServerRestart = True + return + + self.end() + + def onTransitionIn(self, music='Scores', showTips=True): + Activity.onTransitionIn(self) + self._background = bsUtils.Background( + fadeTime=500, startFaded=False, showLogo=True) + if showTips: + self._tipsText = bsUtils.TipsText() + bs.playMusic(music) + + def onBegin(self, customContinueMessage=None): + Activity.onBegin(self) + # pop up a 'press any button to continue' statement after our + # min-view-time show a 'press any button to continue..' + # thing after a bit.. + if bs.getEnvironment()['interfaceType'] == 'large': + # FIXME - need a better way to determine whether we've probably + # got a keyboard + s = bs.Lstr(resource='pressAnyKeyButtonText') + else: + s = bs.Lstr(resource='pressAnyButtonText') + + bsUtils.Text(customContinueMessage if customContinueMessage else s, + vAttach='bottom', + hAlign='center', + flash=True, + vrDepth=50, + position=(0, 10), + scale=0.8, + color=(0.5, 0.7, 0.5, 0.5), + transition='inBottomSlow', + transitionDelay=self._minViewTime).autoRetain() + + +class GameActivity(Activity): + """ + category: Game Flow Classes + + Common base class for all game activities; whether of + the free-for-all, co-op, or teams variety. + """ + tips = [] + + @classmethod + def createConfigUI(cls, sessionType, config, completionCall): + """ + Launch a UI to configure settings for this game type under the given + bs.Session type. + + 'config' should be an existing config dict (specifies 'edit' mode) or + None (specifies 'add' mode). + + 'completionCall' will be called with a filled-out config dict on success + or None on cancel. + + Generally subclasses don't need to override this; if they override + bs.GameActivity.getSettings() and bs.GameActivity.getSupportedMaps() + they can just rely on the default implementation here + which calls those functions. + """ + import bsUI + bsUI.standardGameConfigUI(cls, sessionType, config, completionCall) + + @classmethod + def getScoreInfo(cls): + """ + Games should override this to provide info about their scoring setup. + They should return a dict containing any of the following (missing + values will be default): + + 'scoreName': a label shown to the user for scores; 'Score', + 'Time Survived', etc. 'Score' is the default. + + 'lowerIsBetter': a boolean telling whether lower scores are preferable + instead of higher (the default). + + 'noneIsWinner': specifies whether a score value of None is considered + better than other scores or worse. Default is False. + + 'scoreType': can be 'seconds', 'milliseconds', or 'points'. + + 'scoreVersion': to change high-score lists used by a game without + renaming the game, change this. Defaults to empty string. + """ + return {} + + @classmethod + def getResolvedScoreInfo(cls): + """ + Call this to return a game's score info with all missing values + filled in with defaults. This should not be overridden; override + getScoreInfo() instead. + """ + values = cls.getScoreInfo() + if 'scoreName' not in values: + values['scoreName'] = 'Score' + if 'lowerIsBetter' not in values: + values['lowerIsBetter'] = False + if 'noneIsWinner' not in values: + values['noneIsWinner'] = False + if 'scoreType' not in values: + values['scoreType'] = 'points' + if 'scoreVersion' not in values: + values['scoreVersion'] = '' + + if values['scoreType'] not in ['seconds', 'milliseconds', 'points']: + raise Exception( + "invalid scoreType value: '"+values['scoreType']+"'") + + # make sure they didnt misspell anything in there.. + for name in values.keys(): + if name not in ( + 'scoreName', 'lowerIsBetter', 'noneIsWinner', 'scoreType', + 'scoreVersion'): + print 'WARNING: invalid key in scoreInfo: "'+name+'"' + + return values + + @classmethod + def getName(cls): + """ + Return a name for this game type in English. + """ + try: + return cls.__module__.replace('_', ' ') + except Exception: + return 'Untitled Game' + + @classmethod + def getDisplayString(cls, settings=None): + """ + Return a descriptive name for this game/settings combo. + Subclasses should override getName(); not this. + """ + name = bs.Lstr(translate=('gameNames', cls.getName())) + + # a few substitutions for 'Epic', 'Solo' etc modes.. + # FIXME: should provide a way for game types to define filters of + # their own.. + if settings is not None: + if 'Solo Mode' in settings and settings['Solo Mode']: + name = bs.Lstr(resource='soloNameFilterText', + subs=[('${NAME}', name)]) + if 'Epic Mode' in settings and settings['Epic Mode']: + name = bs.Lstr(resource='epicNameFilterText', + subs=[('${NAME}', name)]) + + return name + + @classmethod + def getTeamDisplayString(cls, name): + """ + Given a team name, returns a localized version of it. + """ + return bs.Lstr(translate=('teamNames', name)) + + @classmethod + def getDescription(cls, sessionType): + """ + Subclasses should override this to return a description for this + activity type (in English) within the context of the given + bs.Session type. + """ + return '' + + @classmethod + def getDescriptionDisplayString(cls, sessionType): + """ + Return a translated version of getDescription(). + Sub-classes should override getDescription(); not this. + """ + description = cls.getDescription(sessionType) + return bs.Lstr(translate=('gameDescriptions', description)) + + @classmethod + def getSettings(cls, sessionType): + """ + Called by the default bs.GameActivity.createConfigUI() implementation; + should return a dict of config options to be presented + to the user for the given bs.Session type. + + The format for settings is a list of 2-member tuples consisting + of a name and a dict of options. + + Available Setting Options: + + 'default': This determines the default value as well as the + type (int, float, or bool) + + 'minValue': Minimum value for int/float settings. + + 'maxValue': Maximum value for int/float settings. + + 'choices': A list of 2-member name/value tuples which the user can + toggle through. + + 'increment': Value increment for int/float settings. + + # example getSettings() implementation for a capture-the-flag game: + @classmethod + def getSettings(cls,sessionType): + return [("Score to Win",{'default':3, + 'minValue':1}), + ("Flag Touch Return Time",{'default':0, + 'minValue':0, + 'increment':1}), + ("Flag Idle Return Time",{'default':30, + 'minValue':5, + 'increment':5}), + ("Time Limit",{'default':0, + 'choices':[('None',0), + ('1 Minute',60), + ('2 Minutes',120), + ('5 Minutes',300), + ('10 Minutes',600), + ('20 Minutes',1200)]}), + ("Respawn Times",{'default':1.0, + 'choices':[('Shorter',0.25), + ('Short',0.5), + ('Normal',1.0), + ('Long',2.0), + ('Longer',4.0)]}), + ("Epic Mode",{'default':False})] + """ + return {} + + @classmethod + def getSupportedMaps(cls, sessionType): + """ + Called by the default bs.GameActivity.createConfigUI() implementation; + should return a list of map names valid for this game-type + for the given bs.Session type. + """ + import bsMap + return bsMap.getMapsSupportingPlayType("melee") + + @classmethod + def filterGameName(cls): + """ + Given a game name, game type + """ + + @classmethod + def getConfigDisplayString(cls, config): + """ + Given a game config dict, return a short description for it. + This is used when viewing game-lists or showing what game + is up next in a series. + """ + import bsMap + name = cls.getDisplayString(config['settings']) + + # in newer configs, map is in settings; it used to be in the config root + if 'map' in config['settings']: + s = bs.Lstr( + value="${NAME} @ ${MAP}", + subs=[('${NAME}', name), + ('${MAP}', bsMap.getMapDisplayString( + bsMap.getFilteredMapName( + config['settings']['map'])))]) + elif 'map' in config: + s = bs.Lstr( + value="${NAME} @ ${MAP}", + subs=[('${NAME}', name), + ('${MAP}', bsMap.getMapDisplayString( + bsMap.getFilteredMapName(config['map'])))]) + else: + print 'invalid game config - expected map entry under settings' + s = '???' + return s + + @classmethod + def supportsSessionType(cls, sessionType): + """ + Return whether this game type can be played + in the provided bs.Session subtype. + """ + return True if issubclass(sessionType, bs.TeamsSession) else False + + def __init__(self, settings={}): + """ + Instantiates the activity and starts pre-loading the requested map. + """ + import bsMap + Activity.__init__(self, settings) + + # set some defaults.. + self._allowPausing = True + self._allowKickIdlePlayers = True + self._spawnSound = bs.getSound('spawn') + self._showKillPoints = True # whether to show points for kills + + # go ahead and get our map loading.. + if 'map' in settings: + mapName = settings['map'] + else: + # if settings doesn't specify a map, pick a random one from the + # list of supported ones + unOwnedMaps = bsMap._getUnOwnedMaps() + validMaps = [m + for m in self.getSupportedMaps( + type(self.getSession())) if m not in unOwnedMaps] + if len(validMaps) == 0: + bs.screenMessage(bs.Lstr(resource='noValidMapsErrorText')) + raise Exception("No valid maps") + mapName = validMaps[random.randrange(len(validMaps))] + + self._mapType = bsMap.getMapClass(mapName) + self._mapType.preload() + self._map = None + + # fixme - should we expose this through the player class?... + def _getPlayerNode(self, player): + return player.gameData['_playerNode'].node + + def getMap(self): + """ + Returns the bs.Map in use for this activity. + """ + if self._map is None: + raise Exception( + "getMap() cannot be called until after onTransitionIn()") + return self._map + + def getInstanceDisplayString(self): + """ + Returns a name for this particular game instance. + """ + return self.getDisplayString(self.settings) + + def getInstanceScoreBoardDisplayString(self): + """ + Returns a name for this particular game instance, in English. + This name is used above the game scoreboard in the corner + of the screen, so it should be as concise as possible. + """ + # if we're in a co-op session, use the level name + # FIXME; should clean this up.. + try: + import bsUI + if isinstance(self.getSession(), bs.CoopSession): + return self.getSession()._campaign.getLevel( + self.getSession()._campaignInfo['level']).getDisplayString() + except Exception: + bs.printError('error geting campaign level name') + return self.getInstanceDisplayString() + + def getInstanceDescription(self): + """ + Returns a description for this particular game instance, in English. + This is shown in the center of the screen below the game name at the + start of a game. It should start with a capital letter and end with a + period, and can be a bit more verbose than the version returned by + getInstanceScoreBoardDescription(). + + Note that translation is applied by looking up the specific returned + value as a key, so the number of returned variations should be limited; + ideally just one or two. To include arbitrary values in the description, + you can return a sequence of values in the following form instead of + just a string: + + # this will give us something like 'Score 3 goals.' in English + # and can properly translate to 'Anota 3 goles.' in Spanish. + # If we just returned the string 'Score 3 Goals' here, there would + # have to be a translation entry for each specific number. ew. + return ['Score ${ARG1} goals.', self.settings['Score to Win']] + + This way the first string can be consistently translated, with any arg + values then substituted into the result. ${ARG1} will be replaced with + the first value, ${ARG2} with the second, etc. + """ + return self.getDescription(type(self.getSession())) + + def getInstanceScoreBoardDescription(self): + """ + Returns a short description for this particular game instance, in + English. This description is used above the game scoreboard in the + corner of the screen, so it should be as concise as possible. + It should be lowercase and should not contain periods or other + punctuation. + + Note that translation is applied by looking up the specific returned + value as a key, so the number of returned variations should be limited; + ideally just one or two. To include arbitrary values in the description, + you can return a sequence of values in the following form instead of + just a string: + + # this will give us something like 'score 3 goals' in English + # and can properly translate to 'anota 3 goles' in Spanish. + # If we just returned the string 'score 3 goals' here, there would + # have to be a translation entry for each specific number. ew. + return ['score ${ARG1} goals', self.settings['Score to Win']] + + This way the first string can be consistently translated, with any arg + values then substituted into the result. ${ARG1} will be replaced + with the first value, ${ARG2} with the second, etc. + + """ + return '' + + def onTransitionIn(self, music=None): + """ + Method override; optionally can + be passed a 'music' string which is the suggested type of + music to play during the game. + Note that in some cases music may be overridden by + the map or other factors, which is why you should pass + it in here instead of simply playing it yourself. + """ + + Activity.onTransitionIn(self) + + # make our map + self._map = self._mapType() + + # give our map a chance to override the music + # (for happy-thoughts and other such themed maps) + overrideMusic = self._mapType.getMusicType() + if overrideMusic is not None: + music = overrideMusic + + if music is not None: + bs.playMusic(music) + + def onBegin(self): + Activity.onBegin(self) + + # report for analytics + s = self.getSession() + try: + if isinstance(s, bs.CoopSession): + import bsUI + bsInternal._setAnalyticsScreen( + 'Coop Game: '+s._campaign.getName() + +' '+s._campaign.getLevel(bsUI.gCoopSessionArgs['level']) + .getName()) + bsInternal._incrementAnalyticsCount('Co-op round start') + if len(self.players) == 1: + bsInternal._incrementAnalyticsCount( + 'Co-op round start 1 human player') + elif len(self.players) == 2: + bsInternal._incrementAnalyticsCount( + 'Co-op round start 2 human players') + elif len(self.players) == 3: + bsInternal._incrementAnalyticsCount( + 'Co-op round start 3 human players') + elif len(self.players) >= 4: + bsInternal._incrementAnalyticsCount( + 'Co-op round start 4+ human players') + elif isinstance(s, bs.TeamsSession): + bsInternal._setAnalyticsScreen('Teams Game: '+self.getName()) + bsInternal._incrementAnalyticsCount('Teams round start') + if len(self.players) == 1: + bsInternal._incrementAnalyticsCount( + 'Teams round start 1 human player') + elif len(self.players) > 1 and len(self.players) < 8: + bsInternal._incrementAnalyticsCount( + 'Teams round start ' + str(len(self.players)) + + ' human players') + elif len(self.players) >= 8: + bsInternal._incrementAnalyticsCount( + 'Teams round start 8+ human players') + elif isinstance(s, bs.FreeForAllSession): + bsInternal._setAnalyticsScreen( + 'FreeForAll Game: '+self.getName()) + bsInternal._incrementAnalyticsCount('Free-for-all round start') + if len(self.players) == 1: + bsInternal._incrementAnalyticsCount( + 'Free-for-all round start 1 human player') + elif len(self.players) > 1 and len(self.players) < 8: + bsInternal._incrementAnalyticsCount( + 'Free-for-all round start ' + str(len(self.players)) + + ' human players') + elif len(self.players) >= 8: + bsInternal._incrementAnalyticsCount( + 'Free-for-all round start 8+ human players') + except Exception: + bs.printException("error setting analytics screen") + + # for some analytics tracking on the c layer.. + bsInternal._resetGameActivityTracking() + + # we dont do this in onTransitionIn because it may depend on + # players/teams which arent available until now + bs.gameTimer(1, bs.WeakCall(self.showScoreBoardInfo)) + bs.gameTimer(1000, bs.WeakCall(self.showInfo)) + bs.gameTimer(2500, bs.WeakCall(self._showTip)) + + # store some basic info about players present at start time + self.initialPlayerInfo = [ + {'name': p.getName(full=True), + 'character': p.character} for p in self.players] + + # sort this by name so high score lists/etc will be consistent + # regardless of player join order.. + self.initialPlayerInfo.sort(key=lambda x: x['name']) + + # if this is a tournament, query info about it such as how much + # time is left + try: + tournamentID = self.getSession()._tournamentID + except Exception: + tournamentID = None + + if tournamentID is not None: + bsInternal._tournamentQuery( + args={'tournamentIDs': [tournamentID], + 'source': 'in-game time remaining query'}, + callback=bs.WeakCall(self._onTournamentQueryResponse)) + + def _onTournamentQueryResponse(self, data): + import bsUI + if data is not None: + data = data['t'] # this used to be the whole payload + # keep our cached tourney info up to date + bsUI._cacheTournamentInfo(data) + self._setupTournamentTimeLimit(max(5, data[0]['timeRemaining'])) + + def onPlayerJoin(self, player): + Activity.onPlayerJoin(self, player) + # by default, just spawn a dude + self.spawnPlayer(player) + + def onPlayerLeave(self, player): + Activity.onPlayerLeave(self, player) + + # if the player has an actor, lets send it a die-message in a timer; + # not immediately. That way the player will no longer exist once the + # message goes through, making it easier for the game to realize the + # player has left and less likely to make a respawn timer or whatnot. + actor = player.actor + if actor is not None: + # use a strong ref (Call) to make sure the actor doesnt die here + # due to no refs + bs.gameTimer(0, bs.Call(actor.handleMessage, + bs.DieMessage(how='leftGame'))) + player.setActor(None) + + def handleMessage(self, msg): + + if isinstance(msg, bs.PlayerSpazDeathMessage): + + player = msg.spaz.getPlayer() + killer = msg.killerPlayer + + # inform our score-set of the demise + self.scoreSet.playerLostSpaz( + player, killed=msg.killed, killer=killer) + + # award the killer points if he's on a different team + if killer is not None and killer.getTeam() is not player.getTeam(): + pts, importance = msg.spaz.getDeathPoints(msg.how) + if not self.hasEnded(): + self.scoreSet.playerScored( + killer, pts, kill=True, victimPlayer=player, + importance=importance, showPoints=self._showKillPoints) + + def showScoreBoardInfo(self): + """ + Creates the game info display + in the top left corner showing the name + and short description of the game. + """ + + sbName = self.getInstanceScoreBoardDisplayString() + + # the description can be either a string or a sequence with args to swap + # in post-translation + sbDesc = self.getInstanceScoreBoardDescription() + if type(sbDesc) in [unicode, str]: + sbDesc = [sbDesc] # handle simple string case + if type(sbDesc[0]) not in [unicode, str]: + raise Exception("Invalid format for instance description") + + isEmpty = (sbDesc[0] == '') + subs = [] + for i in range(len(sbDesc)-1): + subs.append(('${ARG'+str(i+1)+'}', str(sbDesc[i+1]))) + translation = bs.Lstr( + translate=('gameDescriptions', sbDesc[0]), + subs=subs) + sbDesc = translation + + vr = bs.getEnvironment()['vrMode'] + + # y = -34 if sbDesc == '' else -20 + y = -34 if isEmpty else -20 + y -= 16 + self._gameScoreBoardNameText = bs.NodeActor( + bs.newNode( + "text", + attrs={'text': sbName, 'maxWidth': 300, 'position': (15, y) + if + isinstance(self.getSession(), + bs.FreeForAllSession) else(15, y), + 'hAttach': "left", 'vrDepth': 10, 'vAttach': "top", + 'vAlign': 'bottom', 'color': (1.0, 1.0, 1.0, 1.0), + 'shadow': 1.0 if vr else 0.6, 'flatness': 1.0 + if vr else 0.5, 'scale': 1.1})) + + bsUtils.animate(self._gameScoreBoardNameText.node, + 'opacity', {0: 0.0, 1000: 1.0}) + + self._gameScoreBoardDescriptionText = bs.NodeActor( + bs.newNode( + "text", + attrs={'text': sbDesc, 'maxWidth': 480, + 'position': (17, -44 + 10) + if + isinstance(self.getSession(), + bs.FreeForAllSession) else(17, -44 + 10), + 'scale': 0.7, 'hAttach': "left", 'vAttach': "top", + 'vAlign': 'top', 'shadow': 1.0 if vr else 0.7, + 'flatness': 1.0 if vr else 0.8, 'color': (1, 1, 1, 1) + if vr else(0.9, 0.9, 0.9, 1.0)})) + + bsUtils.animate(self._gameScoreBoardDescriptionText.node, + 'opacity', {0: 0.0, 1000: 1.0}) + + def showInfo(self): + """ show the game description """ + name = self.getInstanceDisplayString() + bsUtils.ZoomText( + name, maxWidth=800, lifespan=2500, jitter=2.0, position=(0, 180), + flash=False, color=(0.93 * 1.25, 0.9 * 1.25, 1.0 * 1.25), + trailColor=(0.15, 0.05, 1.0, 0.0)).autoRetain() + bs.gameTimer(200, bs.Call(bs.playSound, bs.getSound('gong'))) + + # the description can be either a string or a sequence with args to swap + # in post-translation + desc = self.getInstanceDescription() + if type(desc) in [unicode, str]: + desc = [desc] # handle simple string case + if type(desc[0]) not in [unicode, str]: + raise Exception("Invalid format for instance description") + subs = [] + for i in range(len(desc)-1): + subs.append(('${ARG'+str(i+1)+'}', str(desc[i+1]))) + translation = bs.Lstr( + translate=('gameDescriptions', desc[0]), + subs=subs) + + # do some standard filters (epic mode, etc) + if ('Epic Mode' in self.settings and self.settings['Epic Mode']): + translation = bs.Lstr( + resource='epicDescriptionFilterText', + subs=[('${DESCRIPTION}', translation)]) + + vr = bs.getEnvironment()['vrMode'] + + d = bs.newNode('text', + attrs={'vAttach': 'center', + 'hAttach': 'center', + 'hAlign': 'center', + 'color': (1, 1, 1, 1), + 'shadow': 1.0 if vr else 0.5, + 'flatness': 1.0 if vr else 0.5, + 'vrDepth': -30, + 'position': (0, 80), + 'scale': 1.2, + 'maxWidth': 700, + 'text': translation}) + c = bs.newNode( + "combine", owner=d, + attrs={'input0': 1.0, 'input1': 1.0, 'input2': 1.0, 'size': 4}) + c.connectAttr('output', d, 'color') + keys = {500: 0, 1000: 1.0, 2500: 1.0, 4000: 0.0} + bsUtils.animate(c, "input3", keys) + bs.gameTimer(4000, d.delete) + + def _showTip(self): + + # if theres any tips left on the global list, display one.. + if len(self.tips) > 0: + tip = self.tips.pop(random.randrange(len(self.tips))) + tipTitle = bs.Lstr(value='${A}:', subs=[ + ('${A}', bs.Lstr(resource='tipText'))]) + icon = None + sound = None + if type(tip) == dict: + if 'icon' in tip: + icon = tip['icon'] + if 'sound' in tip: + sound = tip['sound'] + tip = tip['tip'] + + # a few subs.. + tip = bs.Lstr(translate=('tips', tip), subs=[ + ('${PICKUP}', bs.getSpecialChar('topButton'))]) + basePosition = (75, 50) + tipScale = 0.8 + tipTitleScale = 1.2 + vr = bs.getEnvironment()['vrMode'] + + tOffs = -350.0 + t = bs.newNode( + 'text', + attrs={'text': tip, 'scale': tipScale, 'maxWidth': 900, + 'position': (basePosition[0] + tOffs, basePosition[1]), + 'hAlign': 'left', 'vrDepth': 300, 'shadow': 1.0 + if vr else 0.5, 'flatness': 1.0 if vr else 0.5, + 'vAlign': 'center', 'vAttach': 'bottom'}) + t2 = bs.newNode( + 'text', owner=t, + attrs={'text': tipTitle, 'scale': tipTitleScale, + 'position': + (basePosition[0] + tOffs - + (20 if icon is None else 82), + basePosition[1] + 2), + 'hAlign': 'right', 'vrDepth': 300, 'shadow': 1.0 + if vr else 0.5, 'flatness': 1.0 if vr else 0.5, + 'maxWidth': 140, 'vAlign': 'center', + 'vAttach': 'bottom'}) + if icon is not None: + img = bs.newNode('image', + attrs={'texture': icon, + 'position': (basePosition[0]+tOffs-40, + basePosition[1]+1), + 'scale': (50, 50), + 'opacity': 1.0, + 'vrDepth': 315, + 'color': (1, 1, 1), + 'absoluteScale': True, + 'attach': 'bottomCenter'}) + bsUtils.animate(img, 'opacity', { + 0: 0, 1000: 1, 4000: 1, 5000: 0}) + bs.gameTimer(5000, img.delete) + if sound is not None: + bs.playSound(sound) + + c = bs.newNode( + "combine", owner=t, + attrs={'input0': 1.0, 'input1': 0.8, 'input2': 1.0, 'size': 4}) + c.connectAttr('output', t, 'color') + c.connectAttr('output', t2, 'color') + bsUtils.animate(c, 'input3', {0: 0, 1000: 1, 4000: 1, 5000: 0}) + bs.gameTimer(5000, t.delete) + + def endGame(self): + """ + Tells the game to wrap itself up and call bs.Activity.end() immediately. + This method should be overridden by subclasses. + + A game should always be prepared to end and deliver results, even if + there is no 'winner' yet; this way things like the standard time-limit + (bs.GameActivity.setupStandardTimeLimit()) will work with the game. + """ + print ('WARNING: default endGame() implementation called;' + ' your game should override this.') + + def onContinue(self): + pass + + def spawnPlayerIfExists(self, player): + """ + A utility method which calls self.spawnPlayer() *only* if the bs.Player + provided still exists; handy for use in timers and whatnot. + + There is no need to override this; just override spawnPlayer(). + """ + if player.exists(): + self.spawnPlayer(player) + + def spawnPlayer(self, player): + """ + Spawn *something* for the provided bs.Player. + The default implementation simply calls spawnPlayerSpaz(). + """ + + if not player.exists(): + bs.printError('spawnPlayer() called for nonexistant player') + return + + return self.spawnPlayerSpaz(player) + + def respawnPlayer(self, player, respawnTime=None): + """ + Given a bs.Player, sets up a standard respawn timer, + along with the standard counter display, etc. + At the end of the respawn period spawnPlayer() will + be called if the Player still exists. + An explicit 'respawnTime' can optionally be provided + (in milliseconds). + """ + + if player is None or not player.exists(): + if player is None: + bs.printError('None passed as player to respawnPlayer()') + else: + bs.printError('Nonexistant bs.Player passed to respawnPlayer();' + ' call player.exists() to make sure a player' + ' is still there.') + return + + if player.getTeam() is None: + bs.printError('player has no team in respawnPlayer()') + return + + if respawnTime is None: + if len(player.getTeam().players) == 1: + respawnTime = 3000 + elif len(player.getTeam().players) == 2: + respawnTime = 5000 + elif len(player.getTeam().players) == 3: + respawnTime = 6000 + else: + respawnTime = 7000 + + # if this standard setting is present, factor it in + if 'Respawn Times' in self.settings: + respawnTime *= self.settings['Respawn Times'] + + respawnTime = int(max(1000, respawnTime)) + if respawnTime % 1000 != 0: + respawnTime -= respawnTime % 1000 # we want whole seconds + + if player.actor and not self.hasEnded(): + import bsSpaz + player.gameData['respawnTimer'] = bs.Timer( + respawnTime, bs.WeakCall(self.spawnPlayerIfExists, player)) + player.gameData['respawnIcon'] = bsSpaz.RespawnIcon( + player, respawnTime) + + def spawnPlayerSpaz(self, player, position=(0, 0, 0), angle=None): + """ + Create and wire up a bs.PlayerSpaz for the provide bs.Player. + """ + name = player.getName() + color = player.color + highlight = player.highlight + + lightColor = bsUtils.getNormalizedColor(color) + displayColor = bs.getSafeColor(color, targetIntensity=0.75) + spaz = bs.PlayerSpaz(color=color, + highlight=highlight, + character=player.character, + player=player) + player.setActor(spaz) + + # if this is co-op and we're on Courtyard or Runaround, add the + # material that allows us to collide with the player-walls + # FIXME; need to generalize this + if isinstance( + self.getSession(), + bs.CoopSession) and self.getMap().getName() in[ + 'Courtyard', 'Tower D']: + mat = self.getMap().preloadData['collideWithWallMaterial'] + spaz.node.materials += (mat,) + spaz.node.rollerMaterials += (mat,) + + spaz.node.name = name + spaz.node.nameColor = displayColor + spaz.connectControlsToPlayer() + self.scoreSet.playerGotNewSpaz(player, spaz) + + # move to the stand position and add a flash of light + spaz.handleMessage( + bs.StandMessage( + position, angle + if angle is not None else random.uniform(0, 360))) + t = bs.getGameTime() + bs.playSound(self._spawnSound, 1, position=spaz.node.position) + light = bs.newNode('light', attrs={'color': lightColor}) + spaz.node.connectAttr('position', light, 'position') + bsUtils.animate(light, 'intensity', {0: 0, 250: 1, 500: 0}) + bs.gameTimer(500, light.delete) + return spaz + + def projectFlagStand(self, pos): + """ + Projects a flag-stand onto the ground at the given position. + Useful for games such as capture-the-flag to show where a + movable flag originated from. + """ + # need to do this in a timer for it to work.. need to look into that. + bs.gameTimer(1, bs.WeakCall(self._projectFlagStand, pos[:3])) + + def _projectFlagStand(self, pos): + bs.emitBGDynamics(position=pos, emitType='flagStand') + + def setupStandardPowerupDrops(self, enableTNT=True): + """ + Create standard powerup drops for the current map. + """ + import bsPowerup + self._powerupDropTimer = bs.Timer( + bsPowerup.defaultPowerupInterval, bs.WeakCall( + self._standardDropPowerups), + repeat=True) + self._standardDropPowerups() + if enableTNT: + self._tntObjs = {} + self._tntDropTimer = bs.Timer( + 5500, bs.WeakCall(self._standardDropTnt), + repeat=True) + self._standardDropTnt() + + def _standardDropPowerup(self, index, expire=True): + import bsPowerup + bsPowerup.Powerup( + position=self.getMap().powerupSpawnPoints[index], + powerupType=bs.Powerup.getFactory().getRandomPowerupType(), + expire=expire).autoRetain() + + def _standardDropPowerups(self): + """ + Standard powerup drop. + """ + # drop one powerup per point + pts = self.getMap().powerupSpawnPoints + for i, pt in enumerate(pts): + bs.gameTimer(i*400, bs.WeakCall(self._standardDropPowerup, i)) + + def _standardDropTnt(self): + """ + Standard tnt drop. + """ + # drop tnt on the map for any tnt location with no existing tnt box + for i, pt in enumerate(self.getMap().tntPoints): + if not i in self._tntObjs: + self._tntObjs[i] = {'absentTicks': 9999, 'obj': None} + tntObj = self._tntObjs[i] + + # respawn once its been dead for a while.. + if tntObj['obj'] is None or not tntObj['obj'].exists(): + tntObj['absentTicks'] += 1 + if tntObj['absentTicks'] > 3: + tntObj['obj'] = bs.Bomb(position=pt, bombType='tnt') + tntObj['absentTicks'] = 0 + + def setupStandardTimeLimit(self, duration): + """ + Create a standard game time-limit given the provided + duration in seconds. + This will be displayed at the top of the screen. + If the time-limit expires, endGame() will be called. + """ + + if duration <= 0: + return + self._standardTimeLimitTime = int(duration) + self._standardTimeLimitTimer = bs.Timer( + 1000, bs.WeakCall(self._standardTimeLimitTick), repeat=True) + self._standardTimeLimitText = bs.NodeActor( + bs.newNode( + 'text', + attrs={'vAttach': 'top', 'hAttach': 'center', 'hAlign': 'left', + 'color': (1.0, 1.0, 1.0, 0.5), + 'position': (-25, -30), + 'flatness': 1.0, 'scale': 0.9})) + self._standardTimeLimitTextInput = bs.NodeActor(bs.newNode( + 'timeDisplay', attrs={'time2': duration*1000, 'timeMin': 0})) + bs.getSharedObject('globals').connectAttr( + 'gameTime', self._standardTimeLimitTextInput.node, 'time1') + self._standardTimeLimitTextInput.node.connectAttr( + 'output', self._standardTimeLimitText.node, 'text') + + def _standardTimeLimitTick(self): + self._standardTimeLimitTime -= 1 + if self._standardTimeLimitTime <= 10: + if self._standardTimeLimitTime == 10: + self._standardTimeLimitText.node.scale = 1.3 + self._standardTimeLimitText.node.position = (-30, -45) + c = bs.newNode( + 'combine', owner=self._standardTimeLimitText.node, + attrs={'size': 4}) + c.connectAttr( + 'output', self._standardTimeLimitText.node, 'color') + bsUtils.animate(c, "input0", {0: 1, 150: 1}, loop=True) + bsUtils.animate(c, "input1", {0: 1, 150: 0.5}, loop=True) + bsUtils.animate(c, "input2", {0: 0.1, 150: 0.0}, loop=True) + c.input3 = 1.0 + bs.playSound(bs.getSound('tick')) + if self._standardTimeLimitTime <= 0: + self._standardTimeLimitTimer = None + self.endGame() + n = bs.newNode('text', + attrs={'vAttach': 'top', 'hAttach': 'center', + 'hAlign': 'center', 'color': (1, 0.7, 0, 1), + 'position': (0, -90), 'scale': 1.2, + 'text': bs.Lstr(resource='timeExpiredText') + }) + bs.playSound(bs.getSound('refWhistle')) + bsUtils.animate(n, "scale", {0: 0.0, 100: 1.4, 150: 1.2}) + + def _setupTournamentTimeLimit(self, duration): + """ + Create a tournament game time-limit given the provided + duration in seconds. + This will be displayed at the top of the screen. + If the time-limit expires, endGame() will be called. + """ + + if duration <= 0: + return + self._tournamentTimeLimitTime = int(duration) + # we need this timer to match the server's time as close as possible, + # so lets go with net-time.. theoretically we should do real-time but + # then we have to mess with contexts and whatnot... :-/ + self._tournamentTimeLimitTimer = bs.Timer( + 1000, bs.WeakCall(self._tournamentTimeLimitTick), + repeat=True, timeType='net') + self._tournamentTimeLimitTitleText = bs.NodeActor( + bs.newNode('text', attrs={ + 'vAttach': 'bottom', + 'hAttach': 'left', + 'hAlign': 'center', + 'vAlign': 'center', + 'vrDepth': 300, + 'maxWidth': 100, + 'color': (1.0, 1.0, 1.0, 0.5), + 'position': (60, 50), + 'flatness': 1.0, + 'scale': 0.5, + 'text': bs.Lstr(resource='tournamentText') + })) + self._tournamentTimeLimitText = bs.NodeActor(bs.newNode('text', attrs={ + 'vAttach': 'bottom', + 'hAttach': 'left', + 'hAlign': 'center', + 'vAlign': 'center', + 'vrDepth': 300, + 'maxWidth': 100, + 'color': (1.0, 1.0, 1.0, 0.5), + 'position': (60, 30), + 'flatness': 1.0, + 'scale': 0.9})) + self._tournamentTimeLimitTextInput = bs.NodeActor( + bs.newNode('timeDisplay', attrs={ + 'timeMin': 0, + 'time2': self._tournamentTimeLimitTime*1000})) + self._tournamentTimeLimitTextInput.node.connectAttr( + 'output', self._tournamentTimeLimitText.node, 'text') + + def _tournamentTimeLimitTick(self): + self._tournamentTimeLimitTime -= 1 + if self._tournamentTimeLimitTime <= 10: + if self._tournamentTimeLimitTime == 10: + self._tournamentTimeLimitTitleText.node.scale = 1.0 + self._tournamentTimeLimitText.node.scale = 1.3 + self._tournamentTimeLimitTitleText.node.position = (80, 85) + self._tournamentTimeLimitText.node.position = (80, 60) + c = bs.newNode( + 'combine', owner=self._tournamentTimeLimitText.node, + attrs={'size': 4}) + c.connectAttr( + 'output', self._tournamentTimeLimitTitleText.node, 'color') + c.connectAttr( + 'output', self._tournamentTimeLimitText.node, 'color') + bsUtils.animate(c, "input0", {0: 1, 150: 1}, loop=True) + bsUtils.animate(c, "input1", {0: 1, 150: 0.5}, loop=True) + bsUtils.animate(c, "input2", {0: 0.1, 150: 0.0}, loop=True) + c.input3 = 1.0 + bs.playSound(bs.getSound('tick')) + if self._tournamentTimeLimitTime <= 0: + self._tournamentTimeLimitTimer = None + self.endGame() + n = bs.newNode( + 'text', + attrs={'vAttach': 'top', 'hAttach': 'center', + 'hAlign': 'center', 'color': (1, 0.7, 0, 1), + 'position': (0, -200), + 'scale': 1.6, 'text': bs.Lstr( + resource='tournamentTimeExpiredText', + fallbackResource='timeExpiredText')}) + bs.playSound(bs.getSound('refWhistle')) + bsUtils.animate(n, "scale", {0: 0.0, 100: 1.4, 150: 1.2}) + + # normally we just connect this to time, but since this is a bit of a + # funky setup we just update it manually once per second.. + self._tournamentTimeLimitTextInput.node.time2 = \ + self._tournamentTimeLimitTime*1000 + + def showZoomMessage( + self, message, color=(0.9, 0.4, 0.0), + scale=0.8, duration=2000, trail=False): + """ + Show the standard zooming text used to announce + game names and winners. + """ + # reserve a spot on the screen (in case we get multiple of these so + # they dont overlap) + try: + times = self._zoomMessageTimes + except Exception: + self._zoomMessageTimes = {} + i = 0 + curTime = bs.getGameTime() + while True: + if (i not in self._zoomMessageTimes + or self._zoomMessageTimes[i] < curTime): + self._zoomMessageTimes[i] = curTime + duration + break + i += 1 + bsUtils.ZoomText(message, lifespan=duration, jitter=2.0, + position=(0, 200-i*100), scale=scale, maxWidth=800, + trail=trail, color=color).autoRetain() + + def cameraFlash(self, duration=999): + """ + Create a strobing camera flash effect + as seen when a team wins a game. + """ + xSpread = 10 + ySpread = 5 + positions = [ + [-xSpread, -ySpread], + [0, -ySpread], + [0, ySpread], + [xSpread, -ySpread], + [xSpread, ySpread], + [-xSpread, ySpread]] + times = [0, 2700, 1000, 1800, 500, 1400] + + # store this on our activity so we only have one at a time + self._cameraFlash = [] + for i in range(6): + light = bs.NodeActor(bs.newNode("light", attrs={ + 'position': (positions[i][0], 0, positions[i][1]), + 'radius': 1.0, + 'lightVolumes': False, + 'heightAttenuated': False, + 'color': (0.2, 0.2, 0.8)})) + s = 1.87 + iScale = 1.3 + tcombine = bs.newNode( + "combine", owner=light.node, + attrs={'size': 3, 'input0': positions[i][0], + 'input1': 0, 'input2': positions[i][1]}) + tcombine.connectAttr('output', light.node, 'position') + x = positions[i][0] + y = positions[i][1] + spd = 0.5 + random.random() + spd2 = 0.5 + random.random() + bsUtils.animate(tcombine, 'input0', + {0: x+0, 69*spd: x+10.0, + 143*spd: x-10.0, 201*spd: x+0}, + loop=True) + bsUtils.animate(tcombine, 'input2', + {0: y+0, 150*spd2: y+10.0, + 287*spd2: y-10.0, 398*spd2: y+0}, + loop=True) + bsUtils.animate( + light.node, "intensity", + {0: 0, 20 * s: 0, 50 * s: 0.8 * iScale, 80 * s: 0, 100 * s: 0}, + loop=True, offset=times[i]) + bs.gameTimer( + int(times[i] + random.randint(1, duration) * 40 * s), + light.node.delete) + self._cameraFlash.append(light) diff --git a/scripts/bsSpaz.py b/scripts/bsSpaz.py new file mode 100644 index 0000000..3accc30 --- /dev/null +++ b/scripts/bsSpaz.py @@ -0,0 +1,3890 @@ +import weakref +import random + +import bs +import bsUtils +import bsInternal + +# list of defined spazzes +appearances = {} + + +def getAppearances(includeLocked=False): + disallowed = [] + if not includeLocked: + # hmm yeah this'll be tough to hack... + if not bsInternal._getPurchased('characters.santa'): + disallowed.append('Santa Claus') + if not bsInternal._getPurchased('characters.frosty'): + disallowed.append('Frosty') + if not bsInternal._getPurchased('characters.bones'): + disallowed.append('Bones') + if not bsInternal._getPurchased('characters.bernard'): + disallowed.append('Bernard') + if not bsInternal._getPurchased('characters.pixie'): + disallowed.append('Pixel') + if not bsInternal._getPurchased('characters.pascal'): + disallowed.append('Pascal') + if not bsInternal._getPurchased('characters.actionhero'): + disallowed.append('Todd McBurton') + if not bsInternal._getPurchased('characters.taobaomascot'): + disallowed.append('Taobao Mascot') + if not bsInternal._getPurchased('characters.agent'): + disallowed.append('Agent Johnson') + if not bsInternal._getPurchased('characters.jumpsuit'): + disallowed.append('Lee') + if not bsInternal._getPurchased('characters.assassin'): + disallowed.append('Zola') + if not bsInternal._getPurchased('characters.wizard'): + disallowed.append('Grumbledorf') + if not bsInternal._getPurchased('characters.cowboy'): + disallowed.append('Butch') + if not bsInternal._getPurchased('characters.witch'): + disallowed.append('Witch') + if not bsInternal._getPurchased('characters.warrior'): + disallowed.append('Warrior') + if not bsInternal._getPurchased('characters.superhero'): + disallowed.append('Middle-Man') + if not bsInternal._getPurchased('characters.alien'): + disallowed.append('Alien') + if not bsInternal._getPurchased('characters.oldlady'): + disallowed.append('OldLady') + if not bsInternal._getPurchased('characters.gladiator'): + disallowed.append('Gladiator') + if not bsInternal._getPurchased('characters.wrestler'): + disallowed.append('Wrestler') + if not bsInternal._getPurchased('characters.operasinger'): + disallowed.append('Gretel') + if not bsInternal._getPurchased('characters.robot'): + disallowed.append('Robot') + if not bsInternal._getPurchased('characters.cyborg'): + disallowed.append('B-9000') + if not bsInternal._getPurchased('characters.bunny'): + disallowed.append('Easter Bunny') + if not bsInternal._getPurchased('characters.kronk'): + disallowed.append('Kronk') + if not bsInternal._getPurchased('characters.zoe'): + disallowed.append('Zoe') + if not bsInternal._getPurchased('characters.jackmorgan'): + disallowed.append('Jack Morgan') + if not bsInternal._getPurchased('characters.mel'): + disallowed.append('Mel') + if not bsInternal._getPurchased('characters.snakeshadow'): + disallowed.append('Snake Shadow') + return [s for s in appearances.keys() if s not in disallowed] + + +gPowerupWearOffTime = 20000 +gBasePunchPowerScale = 1.2 # obsolete - just used for demo guy now +gBasePunchCooldown = 400 +gLameBotColor = (1.2, 0.9, 0.2) +gLameBotHighlight = (1.0, 0.5, 0.6) +gDefaultBotColor = (0.6, 0.6, 0.6) +gDefaultBotHighlight = (0.1, 0.3, 0.1) +gProBotColor = (1.0, 0.2, 0.1) +gProBotHighlight = (0.6, 0.1, 0.05) +gLastTurboSpamWarningTime = -99999 + + +class _PickupMessage(object): + 'We wanna pick something up' + pass + + +class _PunchHitMessage(object): + 'Message saying an object was hit' + pass + + +class _CurseExplodeMessage(object): + 'We are cursed and should blow up now.' + pass + + +class _BombDiedMessage(object): + "A bomb has died and thus can be recycled" + pass + + +class Appearance(object): + """Create and fill out one of these suckers to define a spaz appearance""" + def __init__(self, name): + self.name = name + if appearances.has_key(self.name): + raise Exception("spaz appearance name \"" + + self.name + "\" already exists.") + appearances[self.name] = self + self.colorTexture = "" + self.headModel = "" + self.torsoModel = "" + self.pelvisModel = "" + self.upperArmModel = "" + self.foreArmModel = "" + self.handModel = "" + self.upperLegModel = "" + self.lowerLegModel = "" + self.toesModel = "" + self.jumpSounds = [] + self.attackSounds = [] + self.impactSounds = [] + self.deathSounds = [] + self.pickupSounds = [] + self.fallSounds = [] + self.style = 'spaz' + self.defaultColor = None + self.defaultHighlight = None + + +class SpazFactory(object): + """ + Category: Game Flow Classes + + Wraps up media and other resources used by bs.Spaz instances. + Generally one of these is created per bs.Activity and shared + between all spaz instances. Use bs.Spaz.getFactory() to return + the shared factory for the current activity. + + Attributes: + + impactSoundsMedium + A tuple of bs.Sounds for when a bs.Spaz hits something kinda hard. + + impactSoundsHard + A tuple of bs.Sounds for when a bs.Spaz hits something really hard. + + impactSoundsHarder + A tuple of bs.Sounds for when a bs.Spaz hits something really + really hard. + + singlePlayerDeathSound + The sound that plays for an 'importan' spaz death such as in + co-op games. + + punchSound + A standard punch bs.Sound. + + punchSoundsStrong + A tuple of stronger sounding punch bs.Sounds. + + punchSoundStronger + A really really strong sounding punch bs.Sound. + + swishSound + A punch swish bs.Sound. + + blockSound + A bs.Sound for when an attack is blocked by invincibility. + + shatterSound + A bs.Sound for when a frozen bs.Spaz shatters. + + splatterSound + A bs.Sound for when a bs.Spaz blows up via curse. + + spazMaterial + A bs.Material applied to all of parts of a bs.Spaz. + + rollerMaterial + A bs.Material applied to the invisible roller ball body that a bs.Spaz + uses for locomotion. + + punchMaterial + A bs.Material applied to the 'fist' of a bs.Spaz. + + pickupMaterial + A bs.Material applied to the 'grabber' body of a bs.Spaz. + + curseMaterial + A bs.Material applied to a cursed bs.Spaz that triggers an explosion. + """ + + def _preload(self, character): + """ + Preload media that will be needed for a given character. + """ + self._getMedia(character) + + def __init__(self): + """ + Instantiate a factory object. + """ + + self.impactSoundsMedium = (bs.getSound('impactMedium'), + bs.getSound('impactMedium2')) + self.impactSoundsHard = (bs.getSound('impactHard'), + bs.getSound('impactHard2'), + bs.getSound('impactHard3')) + self.impactSoundsHarder = (bs.getSound('bigImpact'), + bs.getSound('bigImpact2')) + self.singlePlayerDeathSound = bs.getSound('playerDeath') + self.punchSound = bs.getSound('punch01') + + self.punchSoundsStrong = (bs.getSound('punchStrong01'), + bs.getSound('punchStrong02')) + + self.punchSoundStronger = bs.getSound('superPunch') + + self.swishSound = bs.getSound('punchSwish') + self.blockSound = bs.getSound('block') + self.shatterSound = bs.getSound('shatter') + self.splatterSound = bs.getSound('splatter') + + self.spazMaterial = bs.Material() + self.rollerMaterial = bs.Material() + self.punchMaterial = bs.Material() + self.pickupMaterial = bs.Material() + self.curseMaterial = bs.Material() + + footingMaterial = bs.getSharedObject('footingMaterial') + objectMaterial = bs.getSharedObject('objectMaterial') + playerMaterial = bs.getSharedObject('playerMaterial') + regionMaterial = bs.getSharedObject('regionMaterial') + + # send footing messages to spazzes so they know when they're on solid + # ground. + # eww this should really just be built into the spaz node + self.rollerMaterial.addActions( + conditions=('theyHaveMaterial', footingMaterial), + actions=(('message', 'ourNode', 'atConnect', 'footing', 1), + ('message', 'ourNode', 'atDisconnect', 'footing', -1))) + + self.spazMaterial.addActions( + conditions=('theyHaveMaterial', footingMaterial), + actions=(('message', 'ourNode', 'atConnect', 'footing', 1), + ('message', 'ourNode', 'atDisconnect', 'footing', -1))) + # punches + self.punchMaterial.addActions( + conditions=('theyAreDifferentNodeThanUs',), + actions=(('modifyPartCollision', 'collide', True), + ('modifyPartCollision', 'physical', False), + ('message', 'ourNode', 'atConnect', _PunchHitMessage()))) + # pickups + self.pickupMaterial.addActions( + conditions=(('theyAreDifferentNodeThanUs',), + 'and', ('theyHaveMaterial', objectMaterial)), + actions=(('modifyPartCollision', 'collide', True), + ('modifyPartCollision', 'physical', False), + ('message', 'ourNode', 'atConnect', _PickupMessage()))) + # curse + self.curseMaterial.addActions( + conditions=(('theyAreDifferentNodeThanUs',), + 'and', ('theyHaveMaterial', playerMaterial)), + actions=('message', 'ourNode', 'atConnect', _CurseExplodeMessage())) + + self.footImpactSounds = (bs.getSound('footImpact01'), + bs.getSound('footImpact02'), + bs.getSound('footImpact03')) + + self.footSkidSound = bs.getSound('skid01') + self.footRollSound = bs.getSound('scamper01') + + self.rollerMaterial.addActions( + conditions=('theyHaveMaterial', footingMaterial), + actions=(('impactSound', self.footImpactSounds, 1, 0.2), + ('skidSound', self.footSkidSound, 20, 0.3), + ('rollSound', self.footRollSound, 20, 3.0))) + + self.skidSound = bs.getSound('gravelSkid') + + self.spazMaterial.addActions( + conditions=('theyHaveMaterial', footingMaterial), + actions=(('impactSound', self.footImpactSounds, 20, 6), + ('skidSound', self.skidSound, 2.0, 1), + ('rollSound', self.skidSound, 2.0, 1))) + + self.shieldUpSound = bs.getSound('shieldUp') + self.shieldDownSound = bs.getSound('shieldDown') + self.shieldHitSound = bs.getSound('shieldHit') + + # we dont want to collide with stuff we're initially overlapping + # (unless its marked with a special region material) + self.spazMaterial.addActions( + conditions=((('weAreYoungerThan', 51), + 'and', ('theyAreDifferentNodeThanUs',)), + 'and', ('theyDontHaveMaterial', regionMaterial)), + actions=(('modifyNodeCollision', 'collide', False))) + + self.spazMedia = {} + + # lets load some basic rules (allows them to be tweaked from the + # master server) + self.shieldDecayRate = bsInternal._getAccountMiscReadVal('rsdr', 10.0) + self.punchCooldown = bsInternal._getAccountMiscReadVal('rpc', 400) + self.punchCooldownGloves = \ + bsInternal._getAccountMiscReadVal('rpcg', 300) + self.punchPowerScale = bsInternal._getAccountMiscReadVal('rpp', 1.2) + self.punchPowerScaleGloves = \ + bsInternal._getAccountMiscReadVal('rppg', 1.4) + self.maxShieldSpilloverDamage = \ + bsInternal._getAccountMiscReadVal('rsms', 500) + + def _getStyle(self, character): + return appearances[character].style + + def _getMedia(self, character): + t = appearances[character] + if not self.spazMedia.has_key(character): + m = self.spazMedia[character] = { + 'jumpSounds':[bs.getSound(s) for s in t.jumpSounds], + 'attackSounds':[bs.getSound(s) for s in t.attackSounds], + 'impactSounds':[bs.getSound(s) for s in t.impactSounds], + 'deathSounds':[bs.getSound(s) for s in t.deathSounds], + 'pickupSounds':[bs.getSound(s) for s in t.pickupSounds], + 'fallSounds':[bs.getSound(s) for s in t.fallSounds], + 'colorTexture':bs.getTexture(t.colorTexture), + 'colorMaskTexture':bs.getTexture(t.colorMaskTexture), + 'headModel':bs.getModel(t.headModel), + 'torsoModel':bs.getModel(t.torsoModel), + 'pelvisModel':bs.getModel(t.pelvisModel), + 'upperArmModel':bs.getModel(t.upperArmModel), + 'foreArmModel':bs.getModel(t.foreArmModel), + 'handModel':bs.getModel(t.handModel), + 'upperLegModel':bs.getModel(t.upperLegModel), + 'lowerLegModel':bs.getModel(t.lowerLegModel), + 'toesModel':bs.getModel(t.toesModel) + } + else: + m = self.spazMedia[character] + return m + +class Spaz(bs.Actor): + """ + category: Game Flow Classes + + Base class for various Spazzes. + A Spaz is the standard little humanoid character in the game. + It can be controlled by a player or by AI, and can have + various different appearances. The name 'Spaz' is not to be + confused with the 'Spaz' character in the game, which is just + one of the skins available for instances of this class. + + Attributes: + + node + The 'spaz' bs.Node. + """ + pointsMult = 1 + curseTime = 5000 + defaultBombCount = 1 + defaultBombType = 'normal' + defaultBoxingGloves = False + defaultShields = False + + def __init__(self, color=(1, 1, 1), highlight=(0.5, 0.5, 0.5), + character="Spaz", sourcePlayer=None, startInvincible=True, + canAcceptPowerups=True, powerupsExpire=False, demoMode=False): + """ + Create a new spaz with the requested color, character, etc. + """ + + bs.Actor.__init__(self) + activity = self.getActivity() + + factory = self.getFactory() + + # we need to behave slightly different in the tutorial + self._demoMode = demoMode + + self.playBigDeathSound = False + + # translate None into empty player-ref + if sourcePlayer is None: sourcePlayer = bs.Player(None) + + # scales how much impacts affect us (most damage calcs) + self._impactScale = 1.0 + + self.sourcePlayer = sourcePlayer + self._dead = False + if self._demoMode: # preserve old behavior + self._punchPowerScale = gBasePunchPowerScale + else: + self._punchPowerScale = factory.punchPowerScale + self.fly = bs.getSharedObject('globals').happyThoughtsMode + self._hockey = activity._map.isHockey + self._punchedNodes = set() + self._cursed = False + self._connectedToPlayer = None + + materials = [factory.spazMaterial, + bs.getSharedObject('objectMaterial'), + bs.getSharedObject('playerMaterial')] + + rollerMaterials = [factory.rollerMaterial, + bs.getSharedObject('playerMaterial')] + + extrasMaterials = [] + + if canAcceptPowerups: + pam = bs.Powerup.getFactory().powerupAcceptMaterial + materials.append(pam) + rollerMaterials.append(pam) + extrasMaterials.append(pam) + + media = factory._getMedia(character) + self.node = bs.newNode( + type="spaz", + delegate=self, + attrs={'color':color, + 'behaviorVersion':0 if demoMode else 1, + 'demoMode':True if demoMode else False, + 'highlight':highlight, + 'jumpSounds':media['jumpSounds'], + 'attackSounds':media['attackSounds'], + 'impactSounds':media['impactSounds'], + 'deathSounds':media['deathSounds'], + 'pickupSounds':media['pickupSounds'], + 'fallSounds':media['fallSounds'], + 'colorTexture':media['colorTexture'], + 'colorMaskTexture':media['colorMaskTexture'], + 'headModel':media['headModel'], + 'torsoModel':media['torsoModel'], + 'pelvisModel':media['pelvisModel'], + 'upperArmModel':media['upperArmModel'], + 'foreArmModel':media['foreArmModel'], + 'handModel':media['handModel'], + 'upperLegModel':media['upperLegModel'], + 'lowerLegModel':media['lowerLegModel'], + 'toesModel':media['toesModel'], + 'style':factory._getStyle(character), + 'fly':self.fly, + 'hockey':self._hockey, + 'materials':materials, + 'rollerMaterials':rollerMaterials, + 'extrasMaterials':extrasMaterials, + 'punchMaterials':(factory.punchMaterial, + bs.getSharedObject('attackMaterial')), + 'pickupMaterials':(factory.pickupMaterial, + bs.getSharedObject('pickupMaterial')), + 'invincible':startInvincible, + 'sourcePlayer':sourcePlayer}) + self.shield = None + + if startInvincible: + def _safeSetAttr(node, attr, val): + if node.exists(): setattr(node, attr, val) + bs.gameTimer(1000, bs.Call(_safeSetAttr, self.node, + 'invincible', False)) + self.hitPoints = 1000 + self.hitPointsMax = 1000 + self.bombCount = self.defaultBombCount + self._maxBombCount = self.defaultBombCount + self.bombTypeDefault = self.defaultBombType + self.bombType = self.bombTypeDefault + self.landMineCount = 0 + self.blastRadius = 2.0 + self.powerupsExpire = powerupsExpire + if self._demoMode: # preserve old behavior + self._punchCooldown = gBasePunchCooldown + else: + self._punchCooldown = factory.punchCooldown + self._jumpCooldown = 250 + self._pickupCooldown = 0 + self._bombCooldown = 0 + self._hasBoxingGloves = False + if self.defaultBoxingGloves: self.equipBoxingGloves() + self.lastPunchTime = -9999 + self.lastJumpTime = -9999 + self.lastPickupTime = -9999 + self.lastRunTime = -9999 + self._lastRunValue = 0 + self.lastBombTime = -9999 + self._turboFilterTimes = {} + self._turboFilterTimeBucket = 0 + self._turboFilterCounts = {} + self.frozen = False + self.shattered = False + self._lastHitTime = None + self._numTimesHit = 0 + self._bombHeld = False + if self.defaultShields: self.equipShields() + self._droppedBombCallbacks = [] + + # deprecated stuff.. need to make these into lists + self.punchCallback = None + self.pickUpPowerupCallback = None + + def onFinalize(self): + bs.Actor.onFinalize(self) + + # release callbacks/refs so we don't wind up with dependency loops.. + self._droppedBombCallbacks = [] + self.punchCallback = None + self.pickUpPowerupCallback = None + + def addDroppedBombCallback(self, call): + """ + Add a call to be run whenever this Spaz drops a bomb. + The spaz and the newly-dropped bomb are passed as arguments. + """ + self._droppedBombCallbacks.append(call) + + def isAlive(self): + """ + Method override; returns whether ol' spaz is still kickin'. + """ + return not self._dead + + @classmethod + def getFactory(cls): + """ + Returns the shared bs.SpazFactory object, creating it if necessary. + """ + activity = bs.getActivity() + if activity is None: raise Exception("no current activity") + try: return activity._sharedSpazFactory + except Exception: + f = activity._sharedSpazFactory = SpazFactory() + return f + + def exists(self): + return self.node.exists() + + def _hideScoreText(self): + if self._scoreText.exists(): + bs.animate(self._scoreText, 'scale', + {0:self._scoreText.scale, 200:0}) + + def _turboFilterAddPress(self, source): + """ + Can pass all button presses through here; if we see an obscene number + of them in a short time let's shame/pushish this guy for using turbo + """ + t = bs.getNetTime() + tBucket = int(t/1000) + if tBucket == self._turboFilterTimeBucket: + # add only once per timestep (filter out buttons triggering + # multiple actions) + if t != self._turboFilterTimes.get(source, 0): + self._turboFilterCounts[source] = \ + self._turboFilterCounts.get(source, 0) + 1 + self._turboFilterTimes[source] = t + # (uncomment to debug; prints what this count is at) + # bs.screenMessage( str(source) + " " + # + str(self._turboFilterCounts[source])) + if self._turboFilterCounts[source] == 15: + + # knock 'em out. That'll learn 'em. + self.node.handleMessage("knockout", 500.0) + + # also issue periodic notices about who is turbo-ing + realTime = bs.getRealTime() + global gLastTurboSpamWarningTime + if realTime > gLastTurboSpamWarningTime + 30000: + gLastTurboSpamWarningTime = realTime + bs.screenMessage( + bs.Lstr(translate=('statements', + ('Warning to ${NAME}: ' + 'turbo / button-spamming knocks' + ' you out.')), + subs=[('${NAME}', self.node.name)]), + color=(1, 0.5, 0)) + bs.playSound(bs.getSound('error')) + else: + self._turboFilterTimes = {} + self._turboFilterTimeBucket = tBucket + self._turboFilterCounts = {source:1} + + def setScoreText(self, t, color=(1, 1, 0.4), flash=False): + """ + Utility func to show a message momentarily over our spaz that follows + him around; Handy for score updates and things. + """ + colorFin = bs.getSafeColor(color)[:3] + if not self.node.exists(): return + try: exists = self._scoreText.exists() + except Exception: exists = False + if not exists: + startScale = 0.0 + m = bs.newNode('math', owner=self.node, attrs={ 'input1':(0, 1.4, 0), + 'operation':'add' }) + self.node.connectAttr('torsoPosition', m, 'input2') + self._scoreText = bs.newNode('text', + owner=self.node, + attrs={'text':t, + 'inWorld':True, + 'shadow':1.0, + 'flatness':1.0, + 'color':colorFin, + 'scale':0.02, + 'hAlign':'center'}) + m.connectAttr('output', self._scoreText, 'position') + else: + self._scoreText.color = colorFin + startScale = self._scoreText.scale + self._scoreText.text = t + if flash: + combine = bs.newNode("combine", owner=self._scoreText, + attrs={'size':3}) + sc = 1.8 + offs = 0.5 + t = 300 + for i in range(3): + c1 = offs+sc*colorFin[i] + c2 = colorFin[i] + bs.animate(combine, 'input'+str(i), {0.5*t:c2, + 0.75*t:c1, + 1.0*t:c2}) + combine.connectAttr('output', self._scoreText, 'color') + + bs.animate(self._scoreText, 'scale', {0:startScale, 200:0.02}) + self._scoreTextHideTimer = bs.Timer(1000, + bs.WeakCall(self._hideScoreText)) + + def onJumpPress(self): + """ + Called to 'press jump' on this spaz; + used by player or AI connections. + """ + if not self.node.exists(): return + t = bs.getGameTime() + if t - self.lastJumpTime >= self._jumpCooldown: + self.node.jumpPressed = True + self.lastJumpTime = t + self._turboFilterAddPress('jump') + + def onJumpRelease(self): + """ + Called to 'release jump' on this spaz; + used by player or AI connections. + """ + if not self.node.exists(): return + self.node.jumpPressed = False + + def onPickUpPress(self): + """ + Called to 'press pick-up' on this spaz; + used by player or AI connections. + """ + if not self.node.exists(): return + t = bs.getGameTime() + if t - self.lastPickupTime >= self._pickupCooldown: + self.node.pickUpPressed = True + self.lastPickupTime = t + self._turboFilterAddPress('pickup') + + def onPickUpRelease(self): + """ + Called to 'release pick-up' on this spaz; + used by player or AI connections. + """ + if not self.node.exists(): return + self.node.pickUpPressed = False + + def _onHoldPositionPress(self): + """ + Called to 'press hold-position' on this spaz; + used for player or AI connections. + """ + if not self.node.exists(): return + self.node.holdPositionPressed = True + self._turboFilterAddPress('holdposition') + + def _onHoldPositionRelease(self): + """ + Called to 'release hold-position' on this spaz; + used for player or AI connections. + """ + if not self.node.exists(): return + self.node.holdPositionPressed = False + + def onPunchPress(self): + """ + Called to 'press punch' on this spaz; + used for player or AI connections. + """ + if (not self.node.exists() + or self.frozen + or self.node.knockout > 0.0): return + t = bs.getGameTime() + if t - self.lastPunchTime >= self._punchCooldown: + if self.punchCallback is not None: + self.punchCallback(self) + self._punchedNodes = set() # reset this.. + self.lastPunchTime = t + self.node.punchPressed = True + if not self.node.holdNode.exists(): + bs.gameTimer(100, bs.WeakCall(self._safePlaySound, + self.getFactory().swishSound, + 0.8)) + self._turboFilterAddPress('punch') + + def _safePlaySound(self, sound, volume): + """ + Plays a sound at our position if we exist. + """ + if self.node.exists(): + bs.playSound(sound, volume, self.node.position) + + def onPunchRelease(self): + """ + Called to 'release punch' on this spaz; + used for player or AI connections. + """ + if not self.node.exists(): return + self.node.punchPressed = False + + def onBombPress(self): + """ + Called to 'press bomb' on this spaz; + used for player or AI connections. + """ + if not self.node.exists(): return + + if self._dead or self.frozen: return + if self.node.knockout > 0.0: return + t = bs.getGameTime() + if t - self.lastBombTime >= self._bombCooldown: + self.lastBombTime = t + self.node.bombPressed = True + if not self.node.holdNode.exists(): self.dropBomb() + self._turboFilterAddPress('bomb') + + def onBombRelease(self): + """ + Called to 'release bomb' on this spaz; + used for player or AI connections. + """ + if not self.node.exists(): return + self.node.bombPressed = False + + def onRun(self, value): + """ + Called to 'press run' on this spaz; + used for player or AI connections. + """ + if not self.node.exists(): return + + t = bs.getGameTime() + self.lastRunTime = t + self.node.run = value + + # filtering these events would be tough since its an analog + # value, but lets still pass full 0-to-1 presses along to + # the turbo filter to punish players if it looks like they're turbo-ing + if self._lastRunValue < 0.01 and value > 0.99: + self._turboFilterAddPress('run') + + self._lastRunValue = value + + + def onFlyPress(self): + """ + Called to 'press fly' on this spaz; + used for player or AI connections. + """ + if not self.node.exists(): return + # not adding a cooldown time here for now; slightly worried + # input events get clustered up during net-games and we'd wind up + # killing a lot and making it hard to fly.. should look into this. + self.node.flyPressed = True + self._turboFilterAddPress('fly') + + def onFlyRelease(self): + """ + Called to 'release fly' on this spaz; + used for player or AI connections. + """ + if not self.node.exists(): return + self.node.flyPressed = False + + def onMove(self, x, y): + """ + Called to set the joystick amount for this spaz; + used for player or AI connections. + """ + if not self.node.exists(): return + self.node.handleMessage("move", x, y) + + def onMoveUpDown(self, value): + """ + Called to set the up/down joystick amount on this spaz; + used for player or AI connections. + value will be between -32768 to 32767 + WARNING: deprecated; use onMove instead. + """ + if not self.node.exists(): return + self.node.moveUpDown = value + + def onMoveLeftRight(self, value): + """ + Called to set the left/right joystick amount on this spaz; + used for player or AI connections. + value will be between -32768 to 32767 + WARNING: deprecated; use onMove instead. + """ + if not self.node.exists(): return + self.node.moveLeftRight = value + + def onPunched(self, damage): + """ + Called when this spaz gets punched. + """ + pass + + def getDeathPoints(self, how): + 'Get the points awarded for killing this spaz' + numHits = float(max(1, self._numTimesHit)) + # base points is simply 10 for 1-hit-kills and 5 otherwise + importance = 2 if numHits < 2 else 1 + return ((10 if numHits < 2 else 5) * self.pointsMult, importance) + + def curse(self): + """ + Give this poor spaz a curse; + he will explode in 5 seconds. + """ + if not self._cursed: + factory = self.getFactory() + self._cursed = True + # add the curse material.. + for attr in ['materials', 'rollerMaterials']: + materials = getattr(self.node, attr) + if not factory.curseMaterial in materials: + setattr(self.node, attr, + materials + (factory.curseMaterial,)) + + # -1 specifies no time limit + if self.curseTime == -1: + self.node.curseDeathTime = -1 + else: + self.node.curseDeathTime = bs.getGameTime()+5000 + bs.gameTimer(5000, bs.WeakCall(self.curseExplode)) + + def equipBoxingGloves(self): + """ + Give this spaz some boxing gloves. + """ + self.node.boxingGloves = 1 + if self._demoMode: # preserve old behavior + self._punchPowerScale = 1.7 + self._punchCooldown = 300 + else: + factory = self.getFactory() + self._punchPowerScale = factory.punchPowerScaleGloves + self._punchCooldown = factory.punchCooldownGloves + + def equipShields(self, decay=False): + """ + Give this spaz a nice energy shield. + """ + + if not self.node.exists(): + bs.printError('Can\'t equip shields; no node.') + return + + factory = self.getFactory() + if self.shield is None: + self.shield = bs.newNode('shield', owner=self.node, + attrs={'color':(0.3, 0.2, 2.0), + 'radius':1.3}) + self.node.connectAttr('positionCenter', self.shield, 'position') + self.shieldHitPoints = self.shieldHitPointsMax = 650 + self.shieldDecayRate = factory.shieldDecayRate if decay else 0 + self.shield.hurt = 0 + bs.playSound(factory.shieldUpSound, 1.0, position=self.node.position) + + if self.shieldDecayRate > 0: + self.shieldDecayTimer = bs.Timer(500, bs.WeakCall(self.shieldDecay), + repeat=True) + self.shield.alwaysShowHealthBar = True # so user can see the decay + + def shieldDecay(self): + 'Called repeatedly to decay shield HP over time.' + if self.shield is not None and self.shield.exists(): + self.shieldHitPoints = \ + max(0, self.shieldHitPoints - self.shieldDecayRate) + self.shield.hurt = \ + 1.0 - float(self.shieldHitPoints)/self.shieldHitPointsMax + if self.shieldHitPoints <= 0: + self.shield.delete() + self.shield = None + self.shieldDecayTimer = None + bs.playSound(self.getFactory().shieldDownSound, + 1.0, position=self.node.position) + else: + self.shieldDecayTimer = None + + def handleMessage(self, msg): + self._handleMessageSanityCheck() + + if isinstance(msg, bs.PickedUpMessage): + self.node.handleMessage("hurtSound") + self.node.handleMessage("pickedUp") + # this counts as a hit + self._numTimesHit += 1 + + elif isinstance(msg, bs.ShouldShatterMessage): + # eww; seems we have to do this in a timer or it wont work right + # (since we're getting called from within update() perhaps?..) + bs.gameTimer(1, bs.WeakCall(self.shatter)) + + elif isinstance(msg, bs.ImpactDamageMessage): + # eww; seems we have to do this in a timer or it wont work right + # (since we're getting called from within update() perhaps?..) + bs.gameTimer(1, bs.WeakCall(self._hitSelf, msg.intensity)) + + elif isinstance(msg, bs.PowerupMessage): + if self._dead: return True + if self.pickUpPowerupCallback is not None: + self.pickUpPowerupCallback(self) + + if (msg.powerupType == 'tripleBombs'): + tex = bs.Powerup.getFactory().texBomb + self._flashBillboard(tex) + self.setBombCount(3) + if self.powerupsExpire: + self.node.miniBillboard1Texture = tex + t = bs.getGameTime() + self.node.miniBillboard1StartTime = t + self.node.miniBillboard1EndTime = t+gPowerupWearOffTime + self._multiBombWearOffFlashTimer = \ + bs.Timer(gPowerupWearOffTime-2000, + bs.WeakCall(self._multiBombWearOffFlash)) + self._multiBombWearOffTimer = \ + bs.Timer(gPowerupWearOffTime, + bs.WeakCall(self._multiBombWearOff)) + elif msg.powerupType == 'landMines': + self.setLandMineCount(min(self.landMineCount+3, 3)) + elif msg.powerupType == 'impactBombs': + self.bombType = 'impact' + tex = self._getBombTypeTex() + self._flashBillboard(tex) + if self.powerupsExpire: + self.node.miniBillboard2Texture = tex + t = bs.getGameTime() + self.node.miniBillboard2StartTime = t + self.node.miniBillboard2EndTime = t+gPowerupWearOffTime + self._bombWearOffFlashTimer = \ + bs.Timer( gPowerupWearOffTime-2000, + bs.WeakCall(self._bombWearOffFlash)) + self._bombWearOffTimer = \ + bs.Timer(gPowerupWearOffTime, + bs.WeakCall(self._bombWearOff)) + elif msg.powerupType == 'stickyBombs': + self.bombType = 'sticky' + tex = self._getBombTypeTex() + self._flashBillboard(tex) + if self.powerupsExpire: + self.node.miniBillboard2Texture = tex + t = bs.getGameTime() + self.node.miniBillboard2StartTime = t + self.node.miniBillboard2EndTime = t+gPowerupWearOffTime + self._bombWearOffFlashTimer = \ + bs.Timer(gPowerupWearOffTime-2000, + bs.WeakCall(self._bombWearOffFlash)) + self._bombWearOffTimer = \ + bs.Timer(gPowerupWearOffTime, + bs.WeakCall(self._bombWearOff)) + elif msg.powerupType == 'punch': + self._hasBoxingGloves = True + tex = bs.Powerup.getFactory().texPunch + self._flashBillboard(tex) + self.equipBoxingGloves() + if self.powerupsExpire: + self.node.boxingGlovesFlashing = 0 + self.node.miniBillboard3Texture = tex + t = bs.getGameTime() + self.node.miniBillboard3StartTime = t + self.node.miniBillboard3EndTime = t+gPowerupWearOffTime + self._boxingGlovesWearOffFlashTimer = \ + bs.Timer(gPowerupWearOffTime-2000, + bs.WeakCall(self._glovesWearOffFlash)) + self._boxingGlovesWearOffTimer = \ + bs.Timer(gPowerupWearOffTime, + bs.WeakCall(self._glovesWearOff)) + elif msg.powerupType == 'shield': + factory = self.getFactory() + # let's allow powerup-equipped shields to lose hp over time + self.equipShields( + decay=True if factory.shieldDecayRate > 0 else False) + elif msg.powerupType == 'curse': + self.curse() + elif (msg.powerupType == 'iceBombs'): + self.bombType = 'ice' + tex = self._getBombTypeTex() + self._flashBillboard(tex) + if self.powerupsExpire: + self.node.miniBillboard2Texture = tex + t = bs.getGameTime() + self.node.miniBillboard2StartTime = t + self.node.miniBillboard2EndTime = t+gPowerupWearOffTime + self._bombWearOffFlashTimer = \ + bs.Timer(gPowerupWearOffTime-2000, + bs.WeakCall(self._bombWearOffFlash)) + self._bombWearOffTimer = \ + bs.Timer(gPowerupWearOffTime, + bs.WeakCall(self._bombWearOff)) + elif (msg.powerupType == 'health'): + if self._cursed: + self._cursed = False + # remove cursed material + factory = self.getFactory() + for attr in ['materials', 'rollerMaterials']: + materials = getattr(self.node, attr) + if factory.curseMaterial in materials: + setattr(self.node, attr, + tuple(m for m in materials + if m != factory.curseMaterial)) + self.node.curseDeathTime = 0 + self.hitPoints = self.hitPointsMax + self._flashBillboard(bs.Powerup.getFactory().texHealth) + self.node.hurt = 0 + self._lastHitTime = None + self._numTimesHit = 0 + + self.node.handleMessage("flash") + if msg.sourceNode.exists(): + msg.sourceNode.handleMessage(bs.PowerupAcceptMessage()) + return True + + elif isinstance(msg, bs.FreezeMessage): + if not self.node.exists(): return + if self.node.invincible == True: + bs.playSound(self.getFactory().blockSound, 1.0, + position=self.node.position) + return + if self.shield is not None: return + if not self.frozen: + self.frozen = True + self.node.frozen = 1 + bs.gameTimer(5000, bs.WeakCall(self.handleMessage, + bs.ThawMessage())) + # instantly shatter if we're already dead + # (otherwise its hard to tell we're dead) + if self.hitPoints <= 0: + self.shatter() + + elif isinstance(msg, bs.ThawMessage): + if self.frozen and not self.shattered and self.node.exists(): + self.frozen = False + self.node.frozen = 0 + + elif isinstance(msg, bs.HitMessage): + if not self.node.exists(): return + if self.node.invincible == True: + bs.playSound(self.getFactory().blockSound, + 1.0, position=self.node.position) + return True + + # if we were recently hit, don't count this as another + # (so punch flurries and bomb pileups essentially count as 1 hit) + gameTime = bs.getGameTime() + if self._lastHitTime is None or gameTime-self._lastHitTime > 1000: + self._numTimesHit += 1 + self._lastHitTime = gameTime + + mag = msg.magnitude * self._impactScale + velocityMag = msg.velocityMagnitude * self._impactScale + + damageScale = 0.22 + + # if they've got a shield, deliver it to that instead.. + if self.shield is not None: + + if msg.flatDamage: damage = msg.flatDamage * self._impactScale + else: + # hit our spaz with an impulse but tell it to only return + # theoretical damage; not apply the impulse.. + self.node.handleMessage( + "impulse", msg.pos[0], msg.pos[1], msg.pos[2], + msg.velocity[0], msg.velocity[1], msg.velocity[2], + mag , velocityMag, msg.radius, 1, + msg.forceDirection[0], msg.forceDirection[1], + msg.forceDirection[2]) + damage = damageScale * self.node.damage + + self.shieldHitPoints -= damage + + self.shield.hurt = (1.0 - float(self.shieldHitPoints) + /self.shieldHitPointsMax) + # its a cleaner event if a hit just kills the shield + # without damaging the player.. + # however, massive damage events should still be able to + # damage the player.. this hopefully gives us a happy medium. + maxSpillover = self.getFactory().maxShieldSpilloverDamage + if self.shieldHitPoints <= 0: + # fixme - transition out perhaps?.. + self.shield.delete() + self.shield = None + bs.playSound(self.getFactory().shieldDownSound, 1.0, + position=self.node.position) + # emit some cool lookin sparks when the shield dies + t = self.node.position + bs.emitBGDynamics(position=(t[0], t[1]+0.9, t[2]), + velocity=self.node.velocity, + count=random.randrange(20, 30), scale=1.0, + spread=0.6, chunkType='spark') + + else: + bs.playSound(self.getFactory().shieldHitSound, 0.5, + position=self.node.position) + + # emit some cool lookin sparks on shield hit + bs.emitBGDynamics(position=msg.pos, + velocity=(msg.forceDirection[0]*1.0, + msg.forceDirection[1]*1.0, + msg.forceDirection[2]*1.0), + count=min(30, 5+int(damage*0.005)), + scale=0.5, spread=0.3, chunkType='spark') + + # if they passed our spillover threshold, + # pass damage along to spaz + if self.shieldHitPoints <= -maxSpillover: + leftoverDamage = -maxSpillover-self.shieldHitPoints + shieldLeftoverRatio = leftoverDamage/damage + + # scale down the magnitudes applied to spaz accordingly.. + mag *= shieldLeftoverRatio + velocityMag *= shieldLeftoverRatio + else: + return True # good job shield! + else: shieldLeftoverRatio = 1.0 + + if msg.flatDamage: + damage = (msg.flatDamage * self._impactScale + * shieldLeftoverRatio) + else: + # hit it with an impulse and get the resulting damage + self.node.handleMessage( + "impulse", msg.pos[0], msg.pos[1], msg.pos[2], + msg.velocity[0], msg.velocity[1], msg.velocity[2], + mag, velocityMag, msg.radius, 0, + msg.forceDirection[0], msg.forceDirection[1], + msg.forceDirection[2]) + + damage = damageScale * self.node.damage + self.node.handleMessage("hurtSound") + + # play punch impact sound based on damage if it was a punch + if msg.hitType == 'punch': + + self.onPunched(damage) + + # if damage was significant, lets show it + if damage > 350: + bsUtils.showDamageCount('-' + str(int(damage/10)) + "%", + msg.pos, msg.forceDirection) + + # lets always add in a super-punch sound with boxing + # gloves just to differentiate them + if msg.hitSubType == 'superPunch': + bs.playSound(self.getFactory().punchSoundStronger, 1.0, + position=self.node.position) + if damage > 500: + sounds = self.getFactory().punchSoundsStrong + sound = sounds[random.randrange(len(sounds))] + else: sound = self.getFactory().punchSound + bs.playSound(sound, 1.0, position=self.node.position) + + # throw up some chunks + bs.emitBGDynamics(position=msg.pos, + velocity=(msg.forceDirection[0]*0.5, + msg.forceDirection[1]*0.5, + msg.forceDirection[2]*0.5), + count=min(10, 1+int(damage*0.0025)), + scale=0.3, spread=0.03); + + bs.emitBGDynamics(position=msg.pos, + chunkType='sweat', + velocity=(msg.forceDirection[0]*1.3, + msg.forceDirection[1]*1.3+5.0, + msg.forceDirection[2]*1.3), + count=min(30, 1+int(damage*0.04)), + scale=0.9, + spread=0.28); + # momentary flash + hurtiness = damage*0.003 + punchPos = (msg.pos[0]+msg.forceDirection[0]*0.02, + msg.pos[1]+msg.forceDirection[1]*0.02, + msg.pos[2]+msg.forceDirection[2]*0.02) + flashColor = (1.0, 0.8, 0.4) + light = bs.newNode("light", + attrs={'position':punchPos, + 'radius':0.12+hurtiness*0.12, + 'intensity':0.3*(1.0+1.0*hurtiness), + 'heightAttenuated':False, + 'color':flashColor}) + bs.gameTimer(60, light.delete) + + + flash = bs.newNode("flash", + attrs={'position':punchPos, + 'size':0.17+0.17*hurtiness, + 'color':flashColor}) + bs.gameTimer(60, flash.delete) + + if msg.hitType == 'impact': + bs.emitBGDynamics(position=msg.pos, + velocity=(msg.forceDirection[0]*2.0, + msg.forceDirection[1]*2.0, + msg.forceDirection[2]*2.0), + count=min(10, 1+int(damage*0.01)), + scale=0.4, spread=0.1); + if self.hitPoints > 0: + # its kinda crappy to die from impacts, so lets reduce + # impact damage by a reasonable amount if it'll keep us alive + if msg.hitType == 'impact' and damage > self.hitPoints: + # drop damage to whatever puts us at 10 hit points, + # or 200 less than it used to be whichever is greater + # (so it *can* still kill us if its high enough) + newDamage = max(damage-200, self.hitPoints-10) + damage = newDamage + self.node.handleMessage("flash") + # if we're holding something, drop it + if damage > 0.0 and self.node.holdNode.exists(): + self.node.holdNode = bs.Node(None) + self.hitPoints -= damage + self.node.hurt = 1.0 - float(self.hitPoints)/self.hitPointsMax + # if we're cursed, *any* damage blows us up + if self._cursed and damage > 0: + bs.gameTimer(50, bs.WeakCall(self.curseExplode, + msg.sourcePlayer)) + # if we're frozen, shatter.. otherwise die if we hit zero + if self.frozen and (damage > 200 or self.hitPoints <= 0): + self.shatter() + elif self.hitPoints <= 0: + self.node.handleMessage(bs.DieMessage(how='impact')) + + # if we're dead, take a look at the smoothed damage val + # (which gives us a smoothed average of recent damage) and shatter + # us if its grown high enough + if self.hitPoints <= 0: + damageAvg = self.node.damageSmoothed * damageScale + if damageAvg > 1000: + self.shatter() + + elif isinstance(msg, _BombDiedMessage): + self.bombCount += 1 + + elif isinstance(msg, bs.DieMessage): + wasDead = self._dead + self._dead = True + self.hitPoints = 0 + if msg.immediate: + self.node.delete() + elif self.node.exists(): + self.node.hurt = 1.0 + if self.playBigDeathSound and not wasDead: + bs.playSound(self.getFactory().singlePlayerDeathSound) + self.node.dead = True + bs.gameTimer(2000, self.node.delete) + + elif isinstance(msg, bs.OutOfBoundsMessage): + # by default we just die here + self.handleMessage(bs.DieMessage(how='fall')) + elif isinstance(msg, bs.StandMessage): + self._lastStandPos = (msg.position[0], msg.position[1], + msg.position[2]) + self.node.handleMessage("stand", msg.position[0], msg.position[1], + msg.position[2], msg.angle) + elif isinstance(msg, _CurseExplodeMessage): + self.curseExplode() + elif isinstance(msg, _PunchHitMessage): + node = bs.getCollisionInfo("opposingNode") + + # only allow one hit per node per punch + if (node is not None and node.exists() + and not node in self._punchedNodes): + + punchMomentumAngular = (self.node.punchMomentumAngular + * self._punchPowerScale) + punchPower = self.node.punchPower * self._punchPowerScale + + # ok here's the deal: we pass along our base velocity for use + # in the impulse damage calculations since that is a more + # predictable value than our fist velocity, which is rather + # erratic. ...however we want to actually apply force in the + # direction our fist is moving so it looks better.. so we still + # pass that along as a direction ..perhaps a time-averaged + # fist-velocity would work too?.. should try that. + + # if its something besides another spaz, just do a muffled punch + # sound + if node.getNodeType() != 'spaz': + sounds = self.getFactory().impactSoundsMedium + sound = sounds[random.randrange(len(sounds))] + bs.playSound(sound, 1.0, position=self.node.position) + + t = self.node.punchPosition + punchDir = self.node.punchVelocity + v = self.node.punchMomentumLinear + + self._punchedNodes.add(node) + node.handleMessage( + bs.HitMessage( + pos=t, + velocity=v, + magnitude=punchPower*punchMomentumAngular*110.0, + velocityMagnitude=punchPower*40, + radius=0, + srcNode=self.node, + sourcePlayer=self.sourcePlayer, + forceDirection = punchDir, + hitType='punch', + hitSubType=('superPunch' if self._hasBoxingGloves + else 'default'))) + + # also apply opposite to ourself for the first punch only + # ..this is given as a constant force so that it is more + # noticable for slower punches where it matters.. for fast + # awesome looking punches its ok if we punch 'through' + # the target + mag = -400.0 + if self._hockey: mag *= 0.5 + if len(self._punchedNodes) == 1: + self.node.handleMessage("kickBack", t[0], t[1], t[2], + punchDir[0], punchDir[1], + punchDir[2], mag) + + elif isinstance(msg, _PickupMessage): + opposingNode, opposingBody = bs.getCollisionInfo('opposingNode', + 'opposingBody') + + if opposingNode is None or not opposingNode.exists(): return True + + # dont allow picking up of invincible dudes + try: + if opposingNode.invincible == True: return True + except Exception: pass + + # if we're grabbing the pelvis of a non-shattered spaz, we wanna + # grab the torso instead + if (opposingNode.getNodeType() == 'spaz' + and not opposingNode.shattered and opposingBody == 4): + opposingBody = 1 + + # special case - if we're holding a flag, dont replace it + # ( hmm - should make this customizable or more low level ) + held = self.node.holdNode + if (held is not None and held.exists() + and held.getNodeType() == 'flag'): return True + self.node.holdBody = opposingBody # needs to be set before holdNode + self.node.holdNode = opposingNode + else: + bs.Actor.handleMessage(self, msg) + + def dropBomb(self): + """ + Tell the spaz to drop one of his bombs, and returns + the resulting bomb object. + If the spaz has no bombs or is otherwise unable to + drop a bomb, returns None. + """ + + if (self.landMineCount <= 0 and self.bombCount <= 0) or self.frozen: + return + p = self.node.positionForward + v = self.node.velocity + + if self.landMineCount > 0: + droppingBomb = False + self.setLandMineCount(self.landMineCount-1) + bombType = 'landMine' + else: + droppingBomb = True + bombType = self.bombType + + bomb = bs.Bomb(position=(p[0], p[1] - 0.0, p[2]), + velocity=(v[0], v[1], v[2]), + bombType=bombType, + blastRadius=self.blastRadius, + sourcePlayer=self.sourcePlayer, + owner=self.node).autoRetain() + + if droppingBomb: + self.bombCount -= 1 + bomb.node.addDeathAction(bs.WeakCall(self.handleMessage, + _BombDiedMessage())) + self._pickUp(bomb.node) + + for c in self._droppedBombCallbacks: c(self, bomb) + + return bomb + + def _pickUp(self, node): + if self.node.exists() and node.exists(): + self.node.holdBody = 0 # needs to be set before holdNode + self.node.holdNode = node + + def setLandMineCount(self, count): + """ + Set the number of land-mines this spaz is carrying. + """ + self.landMineCount = count + if self.node.exists(): + if self.landMineCount != 0: + self.node.counterText = 'x'+str(self.landMineCount) + self.node.counterTexture = bs.Powerup.getFactory().texLandMines + else: + self.node.counterText = '' + + def curseExplode(self, sourcePlayer=None): + """ + Explode the poor spaz as happens when + a curse timer runs out. + """ + + # convert None to an empty player-ref + if sourcePlayer is None: sourcePlayer = bs.Player(None) + + if self._cursed and self.node.exists(): + self.shatter(extreme=True) + self.handleMessage(bs.DieMessage()) + activity = self._activity() + if activity: + bs.Blast(position=self.node.position, + velocity=self.node.velocity, + blastRadius=3.0, blastType='normal', + sourcePlayer=(sourcePlayer if sourcePlayer.exists() + else self.sourcePlayer)).autoRetain() + self._cursed = False + + def shatter(self, extreme=False): + """ + Break the poor spaz into little bits. + """ + if self.shattered: return + self.shattered = True + if self.frozen: + # momentary flash of light + light = bs.newNode('light', + attrs={'position':self.node.position, + 'radius':0.5, + 'heightAttenuated':False, + 'color': (0.8, 0.8, 1.0)}) + + bs.animate(light, 'intensity', {0:3.0, 40:0.5, 80:0.07, 300:0}) + bs.gameTimer(300, light.delete) + # emit ice chunks.. + bs.emitBGDynamics(position=self.node.position, + velocity=self.node.velocity, + count=int(random.random()*10.0+10.0), + scale=0.6, spread=0.2, chunkType='ice'); + bs.emitBGDynamics(position=self.node.position, + velocity=self.node.velocity, + count=int(random.random()*10.0+10.0), + scale=0.3, spread=0.2, chunkType='ice'); + + bs.playSound(self.getFactory().shatterSound, 1.0, + position=self.node.position) + else: + bs.playSound(self.getFactory().splatterSound, 1.0, + position=self.node.position) + self.handleMessage(bs.DieMessage()) + self.node.shattered = 2 if extreme else 1 + + def _hitSelf(self, intensity): + + # clean exit if we're dead.. + try: pos = self.node.position + except Exception: return + + self.handleMessage(bs.HitMessage(flatDamage=50.0*intensity, + pos=pos, + forceDirection=self.node.velocity, + hitType='impact')) + self.node.handleMessage("knockout", max(0.0, 50.0*intensity)) + if intensity > 5: sounds = self.getFactory().impactSoundsHarder + elif intensity > 3: sounds = self.getFactory().impactSoundsHard + else: sounds = self.getFactory().impactSoundsMedium + s = sounds[random.randrange(len(sounds))] + bs.playSound(s, position=pos, volume=5.0) + + def _getBombTypeTex(self): + bombFactory = bs.Powerup.getFactory() + if self.bombType == 'sticky': return bombFactory.texStickyBombs + elif self.bombType == 'ice': return bombFactory.texIceBombs + elif self.bombType == 'impact': return bombFactory.texImpactBombs + else: raise Exception() + + def _flashBillboard(self, tex): + self.node.billboardTexture = tex + self.node.billboardCrossOut = False + bs.animate(self.node, "billboardOpacity", + {0:0.0, 100:1.0, 400:1.0, 500:0.0}) + + def setBombCount(self, count): + 'Sets the number of bombs this Spaz has.' + # we cant just set bombCount cuz some bombs may be laid currently + # so we have to do a relative diff based on max + diff = count - self._maxBombCount + self._maxBombCount += diff + self.bombCount += diff + + def _glovesWearOffFlash(self): + if self.node.exists(): + self.node.boxingGlovesFlashing = 1 + self.node.billboardTexture = bs.Powerup.getFactory().texPunch + self.node.billboardOpacity = 1.0 + self.node.billboardCrossOut = True + + def _glovesWearOff(self): + if self._demoMode: # preserve old behavior + self._punchPowerScale = gBasePunchPowerScale + self._punchCooldown = gBasePunchCooldown + else: + factory = self.getFactory() + self._punchPowerScale = factory.punchPowerScale + self._punchCooldown = factory.punchCooldown + self._hasBoxingGloves = False + if self.node.exists(): + bs.playSound(bs.Powerup.getFactory().powerdownSound, + position=self.node.position) + self.node.boxingGloves = 0 + self.node.billboardOpacity = 0.0 + + def _multiBombWearOffFlash(self): + if self.node.exists(): + self.node.billboardTexture = bs.Powerup.getFactory().texBomb + self.node.billboardOpacity = 1.0 + self.node.billboardCrossOut = True + + def _multiBombWearOff(self): + self.setBombCount(self.defaultBombCount) + if self.node.exists(): + bs.playSound(bs.Powerup.getFactory().powerdownSound, + position=self.node.position) + self.node.billboardOpacity = 0.0 + + def _bombWearOffFlash(self): + if self.node.exists(): + self.node.billboardTexture = self._getBombTypeTex() + self.node.billboardOpacity = 1.0 + self.node.billboardCrossOut = True + + def _bombWearOff(self): + self.bombType = self.bombTypeDefault + if self.node.exists(): + bs.playSound(bs.Powerup.getFactory().powerdownSound, + position=self.node.position) + self.node.billboardOpacity = 0.0 + + +class PlayerSpazDeathMessage(object): + """ + category: Message Classes + + A bs.PlayerSpaz has died. + + Attributes: + + spaz + The bs.PlayerSpaz that died. + + killed + If True, the spaz was killed; + If False, they left the game or the round ended. + + killerPlayer + The bs.Player that did the killing, or None. + + how + The particular type of death. + """ + def __init__(self, spaz, wasKilled, killerPlayer, how): + """ + Instantiate a message with the given values. + """ + self.spaz = spaz + self.killed = wasKilled + self.killerPlayer = killerPlayer + self.how = how + +class PlayerSpazHurtMessage(object): + """ + category: Message Classes + + A bs.PlayerSpaz was hurt. + + Attributes: + + spaz + The bs.PlayerSpaz that was hurt + """ + def __init__(self, spaz): + """ + Instantiate with the given bs.Spaz value. + """ + self.spaz = spaz + + +class PlayerSpaz(Spaz): + """ + category: Game Flow Classes + + A bs.Spaz subclass meant to be controlled by a bs.Player. + + When a PlayerSpaz dies, it delivers a bs.PlayerSpazDeathMessage + to the current bs.Activity. (unless the death was the result of the + player leaving the game, in which case no message is sent) + + When a PlayerSpaz is hurt, it delivers a bs.PlayerSpazHurtMessage + to the current bs.Activity. + """ + + + def __init__(self, color=(1, 1, 1), highlight=(0.5, 0.5, 0.5), + character="Spaz", player=None, powerupsExpire=True): + """ + Create a spaz for the provided bs.Player. + Note: this does not wire up any controls; + you must call connectControlsToPlayer() to do so. + """ + # convert None to an empty player-ref + if player is None: player = bs.Player(None) + + Spaz.__init__(self, color=color, highlight=highlight, + character=character, sourcePlayer=player, + startInvincible=True, powerupsExpire=powerupsExpire) + self.lastPlayerAttackedBy = None # FIXME - should use empty player ref + self.lastAttackedTime = 0 + self.lastAttackedType = None + self.heldCount = 0 + self.lastPlayerHeldBy = None # FIXME - should use empty player ref here + self._player = player + + # grab the node for this player and wire it to follow our spaz + # (so players' controllers know where to draw their guides, etc) + if player.exists(): + playerNode = bs.getActivity()._getPlayerNode(player) + self.node.connectAttr('torsoPosition', playerNode, 'position') + + def __superHandleMessage(self, msg): + super(PlayerSpaz, self).handleMessage(msg) + + def getPlayer(self): + """ + Return the bs.Player associated with this spaz. + Note that while a valid player object will always be + returned, there is no guarantee that the player is still + in the game. Call bs.Player.exists() on the return value + before doing anything with it. + """ + return self._player + + def connectControlsToPlayer(self, enableJump=True, enablePunch=True, + enablePickUp=True, enableBomb=True, + enableRun=True, enableFly=True): + """ + Wire this spaz up to the provided bs.Player. + Full control of the character is given by default + but can be selectively limited by passing False + to specific arguments. + """ + player = self.getPlayer() + + # reset any currently connected player and/or the player we're wiring up + if self._connectedToPlayer is not None: + if player != self._connectedToPlayer: player.resetInput() + self.disconnectControlsFromPlayer() + else: player.resetInput() + + player.assignInputCall('upDown', self.onMoveUpDown) + player.assignInputCall('leftRight', self.onMoveLeftRight) + player.assignInputCall('holdPositionPress', self._onHoldPositionPress) + player.assignInputCall('holdPositionRelease', + self._onHoldPositionRelease) + + if enableJump: + player.assignInputCall('jumpPress', self.onJumpPress) + player.assignInputCall('jumpRelease', self.onJumpRelease) + if enablePickUp: + player.assignInputCall('pickUpPress', self.onPickUpPress) + player.assignInputCall('pickUpRelease', self.onPickUpRelease) + if enablePunch: + player.assignInputCall('punchPress', self.onPunchPress) + player.assignInputCall('punchRelease', self.onPunchRelease) + if enableBomb: + player.assignInputCall('bombPress', self.onBombPress) + player.assignInputCall('bombRelease', self.onBombRelease) + if enableRun: + player.assignInputCall('run', self.onRun) + if enableFly: + player.assignInputCall('flyPress', self.onFlyPress) + player.assignInputCall('flyRelease', self.onFlyRelease) + + self._connectedToPlayer = player + + + def disconnectControlsFromPlayer(self): + """ + Completely sever any previously connected + bs.Player from control of this spaz. + """ + if self._connectedToPlayer is not None: + self._connectedToPlayer.resetInput() + self._connectedToPlayer = None + # send releases for anything in case its held.. + self.onMoveUpDown(0) + self.onMoveLeftRight(0) + self._onHoldPositionRelease() + self.onJumpRelease() + self.onPickUpRelease() + self.onPunchRelease() + self.onBombRelease() + self.onRun(0.0) + self.onFlyRelease() + else: + print ('WARNING: disconnectControlsFromPlayer() called for' + ' non-connected player') + + def handleMessage(self, msg): + self._handleMessageSanityCheck() + # keep track of if we're being held and by who most recently + if isinstance(msg, bs.PickedUpMessage): + self.__superHandleMessage(msg) # augment standard behavior + self.heldCount += 1 + pickedUpBy = msg.node.sourcePlayer + if pickedUpBy is not None and pickedUpBy.exists(): + self.lastPlayerHeldBy = pickedUpBy + elif isinstance(msg, bs.DroppedMessage): + self.__superHandleMessage(msg) # augment standard behavior + self.heldCount -= 1 + if self.heldCount < 0: + print "ERROR: spaz heldCount < 0" + # let's count someone dropping us as an attack.. + try: pickedUpBy = msg.node.sourcePlayer + except Exception: pickedUpBy = None + if pickedUpBy is not None and pickedUpBy.exists(): + self.lastPlayerAttackedBy = pickedUpBy + self.lastAttackedTime = bs.getGameTime() + self.lastAttackedType = ('pickedUp', 'default') + elif isinstance(msg, bs.DieMessage): + + # report player deaths to the game + if not self._dead: + + # immediate-mode or left-game deaths don't count as 'kills' + killed = (msg.immediate==False and msg.how!='leftGame') + + activity = self._activity() + + if not killed: + killerPlayer = None + else: + # if this player was being held at the time of death, + # the holder is the killer + if (self.heldCount > 0 + and self.lastPlayerHeldBy is not None + and self.lastPlayerHeldBy.exists()): + killerPlayer = self.lastPlayerHeldBy + else: + # otherwise, if they were attacked by someone in the + # last few seconds, that person's the killer.. + # otherwise it was a suicide. + # FIXME - currently disabling suicides in Co-Op since + # all bot kills would register as suicides; need to + # change this from lastPlayerAttackedBy to something + # like lastActorAttackedBy to fix that. + if (self.lastPlayerAttackedBy is not None + and self.lastPlayerAttackedBy.exists() + and bs.getGameTime() - self.lastAttackedTime \ + < 4000): + killerPlayer = self.lastPlayerAttackedBy + else: + # ok, call it a suicide unless we're in co-op + if (activity is not None + and not isinstance(activity.getSession(), + bs.CoopSession)): + killerPlayer = self.getPlayer() + else: + killerPlayer = None + + if killerPlayer is not None and not killerPlayer.exists(): + killerPlayer = None + + # only report if both the player and the activity still exist + if (killed and activity is not None + and self.getPlayer().exists()): + activity.handleMessage( + PlayerSpazDeathMessage(self, killed, + killerPlayer, msg.how)) + + self.__superHandleMessage(msg) # augment standard behavior + + # keep track of the player who last hit us for point rewarding + elif isinstance(msg, bs.HitMessage): + if msg.sourcePlayer is not None and msg.sourcePlayer.exists(): + self.lastPlayerAttackedBy = msg.sourcePlayer + self.lastAttackedTime = bs.getGameTime() + self.lastAttackedType = (msg.hitType, msg.hitSubType) + self.__superHandleMessage(msg) # augment standard behavior + activity = self._activity() + if activity is not None: + activity.handleMessage(PlayerSpazHurtMessage(self)) + else: + Spaz.handleMessage(self, msg) + + +class RespawnIcon(object): + """ + category: Game Flow Classes + + An icon with a countdown that appears alongside the screen; + used to indicate that a bs.Player is waiting to respawn. + """ + + def __init__(self, player, respawnTime): + """ + Instantiate with a given bs.Player and respawnTime (in milliseconds) + """ + activity = bs.getActivity() + onRight = False + self._visible = True + if isinstance(bs.getSession(), bs.TeamsSession): + onRight = player.getTeam().getID()%2==1 + # store a list of icons in the team + try: + respawnIcons = (player.getTeam() + .gameData['_spazRespawnIconsRight']) + except Exception: + respawnIcons = (player.getTeam() + .gameData['_spazRespawnIconsRight']) = {} + offsExtra = -20 + else: + onRight = False + # store a list of icons in the activity + try: respawnIcons = activity._spazRespawnIconsRight + except Exception: + respawnIcons = activity._spazRespawnIconsRight = {} + if isinstance(activity.getSession(), bs.FreeForAllSession): + offsExtra = -150 + else: offsExtra = -20 + + try: + maskTex = player.getTeam().gameData['_spazRespawnIconsMaskTex'] + except Exception: + maskTex = player.getTeam().gameData['_spazRespawnIconsMaskTex'] = \ + bs.getTexture('characterIconMask') + + # now find the first unused slot and use that + index = 0 + while (index in respawnIcons and respawnIcons[index]() is not None + and respawnIcons[index]()._visible): + index += 1 + respawnIcons[index] = weakref.ref(self) + + offs = offsExtra + index*-53 + icon = player.getIcon() + texture = icon['texture'] + hOffs = -10 + self._image = bs.NodeActor( + bs.newNode('image', + attrs={'texture':texture, + 'tintTexture':icon['tintTexture'], + 'tintColor':icon['tintColor'], + 'tint2Color':icon['tint2Color'], + 'maskTexture':maskTex, + 'position':(-40-hOffs if onRight + else 40+hOffs, -180+offs), + 'scale':(32, 32), + 'opacity':1.0, + 'absoluteScale':True, + 'attach':'topRight' if onRight else 'topLeft'})) + + bs.animate(self._image.node, 'opacity', {0:0, 200:0.7}) + + self._name = bs.NodeActor( + bs.newNode('text', + attrs={'vAttach':'top', + 'hAttach':'right' if onRight else 'left', + 'text':bs.Lstr(value=player.getName()), + 'maxWidth':100, + 'hAlign':'center', + 'vAlign':'center', + 'shadow':1.0, + 'flatness':1.0, + 'color':bs.getSafeColor(icon['tintColor']), + 'scale':0.5, + 'position':(-40-hOffs if onRight + else 40+hOffs, -205+49+offs)})) + + bs.animate(self._name.node, 'scale', {0:0, 100:0.5}) + + self._text = bs.NodeActor( + bs.newNode('text', + attrs={'position':(-60-hOffs if onRight + else 60+hOffs, -192+offs), + 'hAttach':'right' if onRight else 'left', + 'hAlign':'right' if onRight else 'left', + 'scale':0.9, + 'shadow':0.5, + 'flatness':0.5, + 'vAttach':'top', + 'color':bs.getSafeColor(icon['tintColor']), + 'text':''})) + + bs.animate(self._text.node, 'scale', {0:0, 100:0.9}) + + self._respawnTime = bs.getGameTime()+respawnTime + self._update() + self._timer = bs.Timer(1000, bs.WeakCall(self._update), repeat=True) + + def _update(self): + remaining = int(round(self._respawnTime-bs.getGameTime())/1000.0) + if remaining > 0: + if self._text.node.exists(): + self._text.node.text = str(remaining) + else: self._clear() + + def _clear(self): + self._visible = False + self._image = self._text = self._timer = self._name = None + + + +class SpazBotPunchedMessage(object): + """ + category: Message Classes + + A bs.SpazBot got punched. + + Attributes: + + badGuy + The bs.SpazBot that got punched. + + damage + How much damage was done to the bs.SpazBot. + """ + + def __init__(self, badGuy, damage): + """ + Instantiate a message with the given values. + """ + self.badGuy = badGuy + self.damage = damage + +class SpazBotDeathMessage(object): + """ + category: Message Classes + + A bs.SpazBot has died. + + Attributes: + + badGuy + The bs.SpazBot that was killed. + + killerPlayer + The bs.Player that killed it (or None). + + how + The particular type of death. + """ + + def __init__(self, badGuy, killerPlayer, how): + """ + Instantiate with given values. + """ + self.badGuy = badGuy + self.killerPlayer = killerPlayer + self.how = how + + +class SpazBot(Spaz): + """ + category: Bot Classes + + A really dumb AI version of bs.Spaz. + Add these to a bs.BotSet to use them. + + Note: currently the AI has no real ability to + navigate obstacles and so should only be used + on wide-open maps. + + When a SpazBot is killed, it delivers a bs.SpazBotDeathMessage + to the current activity. + + When a SpazBot is punched, it delivers a bs.SpazBotPunchedMessage + to the current activity. + """ + + character = 'Spaz' + punchiness = 0.5 + throwiness = 0.7 + static = False + bouncy = False + run = False + chargeDistMin = 0.0 # when we can start a new charge + chargeDistMax = 2.0 # when we can start a new charge + runDistMin = 0.0 # how close we can be to continue running + chargeSpeedMin = 0.4 + chargeSpeedMax = 1.0 + throwDistMin = 5.0 + throwDistMax = 9.0 + throwRate = 1.0 + defaultBombType = 'normal' + defaultBombCount = 3 + startCursed = False + color=gDefaultBotColor + highlight=gDefaultBotHighlight + + def __init__(self): + """ + Instantiate a spaz-bot. + """ + Spaz.__init__(self, color=self.color, highlight=self.highlight, + character=self.character, sourcePlayer=None, + startInvincible=False, canAcceptPowerups=False) + + # if you need to add custom behavior to a bot, set this to a callable + # which takes one arg (the bot) and returns False if the bot's normal + # update should be run and True if not + self.updateCallback = None + self._map = weakref.ref(bs.getActivity().getMap()) + + self.lastPlayerAttackedBy = None # FIXME - should use empty player-refs + self.lastAttackedTime = 0 + self.lastAttackedType = None + self.targetPointDefault = None + self.heldCount = 0 + self.lastPlayerHeldBy = None # FIXME - should use empty player-refs here + self.targetFlag = None + self._chargeSpeed = 0.5*(self.chargeSpeedMin+self.chargeSpeedMax) + self._leadAmount = 0.5 + self._mode = 'wait' + self._chargeClosingIn = False + self._lastChargeDist = 0.0 + self._running = False + self._lastJumpTime = 0 + + # these cooldowns didnt exist when these bots were calibrated, + # so take them out of the equation + self._jumpCooldown = 0 + self._pickupCooldown = 0 + self._flyCooldown = 0 + self._bombCooldown = 0 + + if self.startCursed: self.curse() + + def _getTargetPlayerPt(self): + """ returns the default player pt we're targeting """ + bp = bs.Vector(*self.node.position) + closestLen = None + closestVel = None + for pp, pv in self._playerPts: + + l = (pp-bp).length() + # ignore player-points that are significantly below the bot + # (keeps bots from following players off cliffs) + if (closestLen is None or l < closestLen) and (pp[1] > bp[1] - 5.0): + closestLen = l + closestVel = pv + closest = pp + if closestLen is not None: + return (bs.Vector(closest[0], closest[1], closest[2]), + bs.Vector(closestVel[0], closestVel[1], closestVel[2])) + else: + return None, None + + def _setPlayerPts(self, pts): + """ + Provide the spaz-bot with the locations of players. + """ + self._playerPts = pts + + def _updateAI(self): + """ + Should be called periodically to update the spaz' AI + """ + + if self.updateCallback is not None: + if self.updateCallback(self) == True: + return # true means bot has been handled + + t = self.node.position + ourPos = bs.Vector(t[0], 0, t[2]) + canAttack = True + + # if we're a flag-bearer, we're pretty simple-minded - just walk + # towards the flag and try to pick it up + if self.targetFlag is not None: + + if not self.targetFlag.node.exists(): + # our flag musta died :-C + self.targetFlag = None + return + if self.node.holdNode.exists(): + try: holdingFlag = (self.node.holdNode.getNodeType() == 'flag') + except Exception: holdingFlag = False + else: holdingFlag = False + # if we're holding the flag, just walk left + if holdingFlag: + # just walk left + self.node.moveLeftRight = -1.0 + self.node.moveUpDown = 0.0 + # otherwise try to go pick it up + else: + targetPtRaw = bs.Vector(*self.targetFlag.node.position) + targetVel = bs.Vector(0, 0, 0) + diff = (targetPtRaw-ourPos) + diff = bs.Vector(diff[0], 0, diff[2]) # dont care about y + dist = diff.length() + toTarget = diff.normal() + + # if we're holding some non-flag item, drop it + if self.node.holdNode.exists(): + self.node.pickUpPressed = True + self.node.pickUpPressed = False + return + + # if we're a runner, run only when not super-near the flag + if self.run and dist > 3.0: + self._running = True + self.node.run = 1.0 + else: + self._running = False + self.node.run = 0.0 + + self.node.moveLeftRight = toTarget.x() + self.node.moveUpDown = -toTarget.z() + if dist < 1.25: + self.node.pickUpPressed = True + self.node.pickUpPressed = False + return + # not a flag-bearer.. if we're holding anything but a bomb, drop it + else: + if self.node.holdNode.exists(): + try: holdingBomb = \ + (self.node.holdNode.getNodeType() in ['bomb', 'prop']) + except Exception: holdingBomb = False + if not holdingBomb: + self.node.pickUpPressed = True + self.node.pickUpPressed = False + return + + targetPtRaw, targetVel = self._getTargetPlayerPt() + + if targetPtRaw is None: + # use default target if we've got one + if self.targetPointDefault is not None: + targetPtRaw = self.targetPointDefault + targetVel = bs.Vector(0, 0, 0) + canAttack = False + # with no target, we stop moving and drop whatever we're holding + else: + self.node.moveLeftRight = 0 + self.node.moveUpDown = 0 + if self.node.holdNode.exists(): + self.node.pickUpPressed = True + self.node.pickUpPressed = False + return + + # we dont want height to come into play + targetPtRaw.data[1] = 0 + targetVel.data[1] = 0 + + distRaw = (targetPtRaw-ourPos).length() + # use a point out in front of them as real target + # (more out in front the farther from us they are) + targetPt = targetPtRaw + targetVel*distRaw*0.3*self._leadAmount + + diff = (targetPt-ourPos) + dist = diff.length() + toTarget = diff.normal() + + if self._mode == 'throw': + # we can only throw if alive and well.. + if not self._dead and not self.node.knockout: + + timeTillThrow = self._throwReleaseTime-bs.getGameTime() + + if not self.node.holdNode.exists(): + # if we havnt thrown yet, whip out the bomb + if not self._haveDroppedThrowBomb: + self.dropBomb() + self._haveDroppedThrowBomb = True + # otherwise our lack of held node means we successfully + # released our bomb.. lets retreat now + else: + self._mode = 'flee' + + # oh crap we're holding a bomb.. better throw it. + elif timeTillThrow <= 0: + # jump and throw.. + def _safePickup(node): + if node.exists(): + self.node.pickUpPressed = True + self.node.pickUpPressed = False + if dist > 5.0: + self.node.jumpPressed = True + self.node.jumpPressed = False + # throws: + bs.gameTimer(100, bs.Call(_safePickup, self.node)) + else: + # throws: + bs.gameTimer(1, bs.Call(_safePickup, self.node)) + + if self.static: + if timeTillThrow < 300: + speed = 1.0 + elif timeTillThrow < 700 and dist > 3.0: + speed = -1.0 # whiplash for long throws + else: + speed = 0.02 + else: + if timeTillThrow < 700: + # right before throw charge full speed towards target + speed = 1.0 + else: + # earlier we can hold or move backward for a whiplash + speed = 0.0125 + self.node.moveLeftRight = toTarget.x() * speed + self.node.moveUpDown = toTarget.z() * -1.0 * speed + + elif self._mode == 'charge': + if random.random() < 0.3: + self._chargeSpeed = random.uniform(self.chargeSpeedMin, + self.chargeSpeedMax) + # if we're a runner we run during charges *except when near + # an edge (otherwise we tend to fly off easily) + if self.run and distRaw > self.runDistMin: + self._leadAmount = 0.3 + self._running = True + self.node.run = 1.0 + else: + self._leadAmont = 0.01 + self._running = False + self.node.run = 0.0 + + self.node.moveLeftRight = toTarget.x() * self._chargeSpeed + self.node.moveUpDown = toTarget.z() * -1.0*self._chargeSpeed + + elif self._mode == 'wait': + # every now and then, aim towards our target.. + # other than that, just stand there + if bs.getGameTime()%1234 < 100: + self.node.moveLeftRight = toTarget.x() * (400.0/33000) + self.node.moveUpDown = toTarget.z() * (-400.0/33000) + else: + self.node.moveLeftRight = 0 + self.node.moveUpDown = 0 + + elif self._mode == 'flee': + # even if we're a runner, only run till we get away from our + # target (if we keep running we tend to run off edges) + if self.run and dist < 3.0: + self._running = True + self.node.run = 1.0 + else: + self._running = False + self.node.run = 0.0 + self.node.moveLeftRight = toTarget.x() * -1.0 + self.node.moveUpDown = toTarget.z() + + # we might wanna switch states unless we're doing a throw + # (in which case thats our sole concern) + if self._mode != 'throw': + + # if we're currently charging, keep track of how far we are + # from our target.. when this value increases it means our charge + # is over (ran by them or something) + if self._mode == 'charge': + if (self._chargeClosingIn and dist < 3.0 + and dist > self._lastChargeDist): + self._chargeClosingIn = False + self._lastChargeDist = dist + + # if we have a clean shot, throw! + if (dist >= self.throwDistMin and dist < self.throwDistMax + and random.random() < self.throwiness and canAttack): + self._mode = 'throw' + self._leadAmount = ((0.4+random.random()*0.6) if distRaw > 4.0 + else (0.1+random.random()*0.4)) + self._haveDroppedThrowBomb = False + self._throwReleaseTime = (bs.getGameTime() + + (1.0/self.throwRate) + *(800 + int(1300*random.random()))) + + # if we're static, always charge (which for us means barely move) + elif self.static: + self._mode = 'wait' + + # if we're too close to charge (and arent in the middle of an + # existing charge) run away + elif dist < self.chargeDistMin and not self._chargeClosingIn: + # ..unless we're near an edge, in which case we got no choice + # but to charge.. + if self._map()._isPointNearEdge(ourPos, self._running): + if self._mode != 'charge': + self._mode = 'charge' + self._leadAmount = 0.2 + self._chargeClosingIn = True + self._lastChargeDist = dist + else: + self._mode = 'flee' + + # we're within charging distance, backed against an edge, or farther + # than our max throw distance.. chaaarge! + elif (dist < self.chargeDistMax + or dist > self.throwDistMax + or self._map()._isPointNearEdge(ourPos, self._running)): + if self._mode != 'charge': + self._mode = 'charge' + self._leadAmount = 0.01 + self._chargeClosingIn = True + self._lastChargeDist = dist + + # we're too close to throw but too far to charge - either run + # away or just chill if we're near an edge + elif dist < self.throwDistMin: + # charge if either we're within charge range or + # cant retreat to throw + self._mode = 'flee' + + # do some awesome jumps if we're running + if ((self._running + and dist > 1.2 and dist < 2.2 + and bs.getGameTime()-self._lastJumpTime > 1000) + or (self.bouncy + and bs.getGameTime()-self._lastJumpTime > 400 + and random.random() < 0.5)): + self._lastJumpTime = bs.getGameTime() + self.node.jumpPressed = True + self.node.jumpPressed = False + + # throw punches when real close + if dist < (1.6 if self._running else 1.2) and canAttack: + if random.random() < self.punchiness: + self.onPunchPress() + self.onPunchRelease() + + def __superHandleMessage(self, msg): + super(SpazBot, self).handleMessage(msg) + + def onPunched(self, damage): + """ + Method override; sends bs.SpazBotPunchedMessage to the current activity. + """ + bs.getActivity().handleMessage(SpazBotPunchedMessage(self, damage)) + + def onFinalize(self): + Spaz.onFinalize(self) + # we're being torn down; release + # our callback(s) so there's no chance of them + # keeping activities or other things alive.. + self.updateCallback = None + + def handleMessage(self, msg): + self._handleMessageSanityCheck() + + # keep track of if we're being held and by who most recently + if isinstance(msg, bs.PickedUpMessage): + self.__superHandleMessage(msg) # augment standard behavior + self.heldCount += 1 + pickedUpBy = msg.node.sourcePlayer + if pickedUpBy is not None and pickedUpBy.exists(): + self.lastPlayerHeldBy = pickedUpBy + + elif isinstance(msg, bs.DroppedMessage): + self.__superHandleMessage(msg) # augment standard behavior + self.heldCount -= 1 + if self.heldCount < 0: + print "ERROR: spaz heldCount < 0" + # let's count someone dropping us as an attack.. + try: + if msg.node.exists(): pickedUpBy = msg.node.sourcePlayer + else: pickedUpBy = bs.Player(None) # empty player ref + except Exception as e: + print 'EXC on SpazBot DroppedMessage:', e + pickedUpBy = bs.Player(None) # empty player ref + + if pickedUpBy.exists(): + self.lastPlayerAttackedBy = pickedUpBy + self.lastAttackedTime = bs.getGameTime() + self.lastAttackedType = ('pickedUp', 'default') + + elif isinstance(msg, bs.DieMessage): + + # report normal deaths for scoring purposes + if not self._dead and not msg.immediate: + + # if this guy was being held at the time of death, the + # holder is the killer + if (self.heldCount > 0 and self.lastPlayerHeldBy is not None + and self.lastPlayerHeldBy.exists()): + killerPlayer = self.lastPlayerHeldBy + else: + # otherwise if they were attacked by someone in the + # last few seconds that person's the killer.. + # otherwise it was a suicide + if (self.lastPlayerAttackedBy is not None + and self.lastPlayerAttackedBy.exists() + and bs.getGameTime() - self.lastAttackedTime < 4000): + killerPlayer = self.lastPlayerAttackedBy + else: + killerPlayer = None + activity = self._activity() + + if killerPlayer is not None and not killerPlayer.exists(): + killerPlayer = None + if activity is not None: + activity.handleMessage( + SpazBotDeathMessage(self, killerPlayer, msg.how)) + self.__superHandleMessage(msg) # augment standard behavior + + # keep track of the player who last hit us for point rewarding + elif isinstance(msg, bs.HitMessage): + if msg.sourcePlayer is not None and msg.sourcePlayer.exists(): + self.lastPlayerAttackedBy = msg.sourcePlayer + self.lastAttackedTime = bs.getGameTime() + self.lastAttackedType = (msg.hitType, msg.hitSubType) + self.__superHandleMessage(msg) + else: + Spaz.handleMessage(self, msg) + + +class BomberBot(SpazBot): + """ + category: Bot Classes + + A bot that throws regular bombs + and occasionally punches. + """ + character='Spaz' + punchiness=0.3 + + +class BomberBotLame(BomberBot): + """ + category: Bot Classes + + A less aggressive yellow version of bs.BomberBot. + """ + color=gLameBotColor + highlight=gLameBotHighlight + punchiness = 0.2 + throwRate = 0.7 + throwiness = 0.1 + chargeSpeedMin = 0.6 + chargeSpeedMax = 0.6 + + +class BomberBotStaticLame(BomberBotLame): + """ + category: Bot Classes + + A less aggressive yellow version of bs.BomberBot + who generally stays in one place. + """ + static = True + throwDistMin = 0.0 + + +class BomberBotStatic(BomberBot): + """ + category: Bot Classes + + A version of bs.BomberBot + who generally stays in one place. + """ + static = True + throwDistMin = 0.0 + + +class BomberBotPro(BomberBot): + """ + category: Bot Classes + + A more aggressive red version of bs.BomberBot. + """ + pointsMult = 2 + color=gProBotColor + highlight = gProBotHighlight + defaultBombCount = 3 + defaultBoxingGloves = True + punchiness = 0.7 + throwRate = 1.3 + run = True + runDistMin = 6.0 + + +class BomberBotProShielded(BomberBotPro): + """ + category: Bot Classes + + A more aggressive red version of bs.BomberBot + who starts with shields. + """ + pointsMult = 3 + defaultShields = True + + +class BomberBotProStatic(BomberBotPro): + """ + category: Bot Classes + + A more aggressive red version of bs.BomberBot + who generally stays in one place. + """ + static = True + throwDistMin = 0.0 + +class BomberBotProStaticShielded(BomberBotProShielded): + """ + category: Bot Classes + + A more aggressive red version of bs.BomberBot + who starts with shields and + who generally stays in one place. + """ + static = True + throwDistMin = 0.0 + + +class ToughGuyBot(SpazBot): + """ + category: Bot Classes + + A manly bot who walks and punches things. + """ + character = 'Kronk' + punchiness = 0.9 + chargeDistMax = 9999.0 + chargeSpeedMin = 1.0 + chargeSpeedMax = 1.0 + throwDistMin = 9999 + throwDistMax = 9999 + + +class ToughGuyBotLame(ToughGuyBot): + """ + category: Bot Classes + + A less aggressive yellow version of bs.ToughGuyBot. + """ + color=gLameBotColor + highlight=gLameBotHighlight + punchiness = 0.3 + chargeSpeedMin = 0.6 + chargeSpeedMax = 0.6 + + +class ToughGuyBotPro(ToughGuyBot): + """ + category: Bot Classes + + A more aggressive red version of bs.ToughGuyBot. + """ + color=gProBotColor + highlight=gProBotHighlight + run = True + runDistMin = 4.0 + defaultBoxingGloves = True + punchiness = 0.95 + pointsMult = 2 + + +class ToughGuyBotProShielded(ToughGuyBotPro): + """ + category: Bot Classes + + A more aggressive version of bs.ToughGuyBot + who starts with shields. + """ + defaultShields = True + pointsMult = 3 + + +class NinjaBot(SpazBot): + """ + category: Bot Classes + + A speedy attacking melee bot. + """ + + character = 'Snake Shadow' + punchiness = 1.0 + run = True + chargeDistMin = 10.0 + chargeDistMax = 9999.0 + chargeSpeedMin = 1.0 + chargeSpeedMax = 1.0 + throwDistMin = 9999 + throwDistMax = 9999 + pointsMult = 2 + + +class BunnyBot(SpazBot): + """ + category: Bot Classes + + A speedy attacking melee bot. + """ + + color=(1, 1, 1) + highlight=(1.0, 0.5, 0.5) + character = 'Easter Bunny' + punchiness = 1.0 + run = True + bouncy = True + defaultBoxingGloves = True + chargeDistMin = 10.0 + chargeDistMax = 9999.0 + chargeSpeedMin = 1.0 + chargeSpeedMax = 1.0 + throwDistMin = 9999 + throwDistMax = 9999 + pointsMult = 2 + + +class NinjaBotPro(NinjaBot): + """ + category: Bot Classes + + A more aggressive red bs.NinjaBot. + """ + color=gProBotColor + highlight=gProBotHighlight + defaultShields = True + defaultBoxingGloves = True + pointsMult = 3 + + +class NinjaBotProShielded(NinjaBotPro): + """ + category: Bot Classes + + A more aggressive red bs.NinjaBot + who starts with shields. + """ + defaultShields = True + pointsMult = 4 + + +class ChickBot(SpazBot): + """ + category: Bot Classes + + A slow moving bot with impact bombs. + """ + character = 'Zoe' + punchiness = 0.75 + throwiness = 0.7 + chargeDistMax = 1.0 + chargeSpeedMin = 0.3 + chargeSpeedMax = 0.5 + throwDistMin = 3.5 + throwDistMax = 5.5 + defaultBombType = 'impact' + pointsMult = 2 + + +class ChickBotStatic(ChickBot): + """ + category: Bot Classes + + A bs.ChickBot who generally stays in one place. + """ + static = True + throwDistMin = 0.0 + + +class ChickBotPro(ChickBot): + """ + category: Bot Classes + + A more aggressive red version of bs.ChickBot. + """ + color=gProBotColor + highlight=gProBotHighlight + defaultBombCount = 3 + defaultBoxingGloves = True + chargeSpeedMin = 1.0 + chargeSpeedMax = 1.0 + punchiness = 0.9 + throwRate = 1.3 + run = True + runDistMin = 6.0 + pointsMult = 3 + + +class ChickBotProShielded(ChickBotPro): + """ + category: Bot Classes + + A more aggressive red version of bs.ChickBot + who starts with shields. + """ + defaultShields = True + pointsMult = 4 + + +class MelBot(SpazBot): + """ + category: Bot Classes + + A crazy bot who runs and throws sticky bombs. + """ + character = 'Mel' + punchiness = 0.9 + throwiness = 1.0 + run = True + chargeDistMin = 4.0 + chargeDistMax = 10.0 + chargeSpeedMin = 1.0 + chargeSpeedMax = 1.0 + throwDistMin = 0.0 + throwDistMax = 4.0 + throwRate = 2.0 + defaultBombType = 'sticky' + defaultBombCount = 3 + pointsMult = 3 + + +class MelBotStatic(MelBot): + """ + category: Bot Classes + + A crazy bot who throws sticky-bombs but generally stays in one place. + """ + static = True + + +class PirateBot(SpazBot): + """ + category: Bot Classes + + A bot who runs and explodes in 5 seconds. + """ + character = 'Jack Morgan' + run = True + chargeDistMin = 0.0 + chargeDistMax = 9999 + chargeSpeedMin = 1.0 + chargeSpeedMax = 1.0 + throwDistMin = 9999 + throwDistMax = 9999 + startCursed = True + pointsMult = 4 + + +class PirateBotNoTimeLimit(PirateBot): + """ + category: Bot Classes + + A bot who runs but does not explode on his own. + """ + curseTime = -1 + + +class PirateBotShielded(PirateBot): + """ + category: Bot Classes + + A bs.PirateBot who starts with shields. + """ + defaultShields = True + pointsMult = 5 + + +class BotSet(object): + """ + category: Bot Classes + + A container/controller for one or more bs.SpazBots. + """ + def __init__(self): + """ + Create a bot-set. + """ + # we spread our bots out over a few lists so we can update + # them in a staggered fashion + self._botListCount = 5 + self._botAddList = 0 + self._botUpdateList = 0 + self._botLists = [[] for i in range(self._botListCount)] + self._spawnSound = bs.getSound('spawn') + self._spawningCount = 0 + self.startMoving() + + def __del__(self): + self.clear() + + def spawnBot(self, botType, pos, spawnTime=3000, onSpawnCall=None): + """ + Spawn a bot from this set. + """ + bsUtils.Spawner(pt=pos, spawnTime=spawnTime, + sendSpawnMessage=False, + spawnCallback=bs.Call(self._spawnBot, botType, + pos, onSpawnCall)) + self._spawningCount += 1 + + def _spawnBot(self, botType, pos, onSpawnCall): + spaz = botType() + bs.playSound(self._spawnSound, position=pos) + spaz.node.handleMessage("flash") + spaz.node.isAreaOfInterest = 0 + spaz.handleMessage(bs.StandMessage(pos, random.uniform(0, 360))) + self.addBot(spaz) + self._spawningCount -= 1 + if onSpawnCall is not None: onSpawnCall(spaz) + + def haveLivingBots(self): + """ + Returns whether any bots in the set are alive or spawning. + """ + haveLiving = any((any((not a._dead for a in l)) + for l in self._botLists)) + haveSpawning = True if self._spawningCount > 0 else False + return (haveLiving or haveSpawning) + + + def getLivingBots(self): + """ + Returns the living bots in the set. + """ + bots = [] + for l in self._botLists: + for b in l: + if not b._dead: bots.append(b) + return bots + + def _update(self): + + # update one of our bot lists each time through.. + # first off, remove dead bots from the list + # (we check exists() here instead of dead.. we want to keep them + # around even if they're just a corpse) + try: + botList = self._botLists[self._botUpdateList] = \ + [b for b in self._botLists[self._botUpdateList] if b.exists()] + except Exception: + bs.printException("error updating bot list: " + +str(self._botLists[self._botUpdateList])) + self._botUpdateList = (self._botUpdateList+1)%self._botListCount + + # update our list of player points for the bots to use + playerPts = [] + for player in bs.getActivity().players: + try: + if player.isAlive(): + playerPts.append((bs.Vector(*player.actor.node.position), + bs.Vector(*player.actor.node.velocity))) + except Exception: + bs.printException('error on bot-set _update') + + for b in botList: + b._setPlayerPts(playerPts) + b._updateAI() + + def clear(self): + """ + Immediately clear out any bots in the set. + """ + # dont do this if the activity is shutting down or dead + activity = bs.getActivity(exceptionOnNone=False) + if activity is None or activity.isFinalized(): return + + for i in range(len(self._botLists)): + for b in self._botLists[i]: + b.handleMessage(bs.DieMessage(immediate=True)) + self._botLists[i] = [] + + def celebrate(self, duration): + """ + Tell all living bots in the set to celebrate momentarily + while continuing onward with their evil bot activities. + """ + for l in self._botLists: + for b in l: + if b.node.exists(): + b.node.handleMessage('celebrate', duration) + + def startMoving(self): + """ + Starts processing bot AI updates and let them start doing their thing. + """ + self._botUpdateTimer = bs.Timer(50, bs.WeakCall(self._update), + repeat=True) + + def stopMoving(self): + """ + Tell all bots to stop moving and stops + updating their AI. + Useful when players have won and you want the + enemy bots to just stand and look bewildered. + """ + self._botUpdateTimer = None + for l in self._botLists: + for b in l: + if b.node.exists(): + b.node.moveLeftRight = 0 + b.node.moveUpDown = 0 + + def finalCelebrate(self): + """ + Tell all bots in the set to stop what they were doing + and just jump around and celebrate. Use this when + the bots have won a game. + """ + self._botUpdateTimer = None + # at this point stop doing anything but jumping and celebrating + for l in self._botLists: + for b in l: + if b.node.exists(): + b.node.moveLeftRight = 0 + b.node.moveUpDown = 0 + bs.gameTimer(random.randrange(0, 500), + bs.Call(b.node.handleMessage, + 'celebrate', 10000)) + jumpDuration = random.randrange(400, 500) + j = random.randrange(0, 200) + for i in range(10): + b.node.jumpPressed = True + b.node.jumpPressed = False + j += jumpDuration + bs.gameTimer(random.randrange(0, 1000), + bs.Call(b.node.handleMessage, 'attackSound')) + bs.gameTimer(random.randrange(1000, 2000), + bs.Call(b.node.handleMessage, 'attackSound')) + bs.gameTimer(random.randrange(2000, 3000), + bs.Call(b.node.handleMessage, 'attackSound')) + + def addBot(self, bot): + """ + Add a bs.SpazBot instance to the set. + """ + self._botLists[self._botAddList].append(bot) + self._botAddList = (self._botAddList+1)%self._botListCount + +# define our built-in characters... + +############### SPAZ ################## +t = Appearance("Spaz") + +t.colorTexture = "neoSpazColor" +t.colorMaskTexture = "neoSpazColorMask" + +t.iconTexture = "neoSpazIcon" +t.iconMaskTexture = "neoSpazIconColorMask" + +t.headModel = "neoSpazHead" +t.torsoModel = "neoSpazTorso" +t.pelvisModel = "neoSpazPelvis" +t.upperArmModel = "neoSpazUpperArm" +t.foreArmModel = "neoSpazForeArm" +t.handModel = "neoSpazHand" +t.upperLegModel = "neoSpazUpperLeg" +t.lowerLegModel = "neoSpazLowerLeg" +t.toesModel = "neoSpazToes" + +t.jumpSounds=["spazJump01", + "spazJump02", + "spazJump03", + "spazJump04"] +t.attackSounds=["spazAttack01", + "spazAttack02", + "spazAttack03", + "spazAttack04"] +t.impactSounds=["spazImpact01", + "spazImpact02", + "spazImpact03", + "spazImpact04"] +t.deathSounds=["spazDeath01"] +t.pickupSounds=["spazPickup01"] +t.fallSounds=["spazFall01"] + +t.style = 'spaz' + + +############### Zoe ################## +t = Appearance("Zoe") + +t.colorTexture = "zoeColor" +t.colorMaskTexture = "zoeColorMask" + +t.defaultColor = (0.6, 0.6, 0.6) +t.defaultHighlight = (0, 1, 0) + +t.iconTexture = "zoeIcon" +t.iconMaskTexture = "zoeIconColorMask" + +t.headModel = "zoeHead" +t.torsoModel = "zoeTorso" +t.pelvisModel = "zoePelvis" +t.upperArmModel = "zoeUpperArm" +t.foreArmModel = "zoeForeArm" +t.handModel = "zoeHand" +t.upperLegModel = "zoeUpperLeg" +t.lowerLegModel = "zoeLowerLeg" +t.toesModel = "zoeToes" + +t.jumpSounds=["zoeJump01", + "zoeJump02", + "zoeJump03"] +t.attackSounds=["zoeAttack01", + "zoeAttack02", + "zoeAttack03", + "zoeAttack04"] +t.impactSounds=["zoeImpact01", + "zoeImpact02", + "zoeImpact03", + "zoeImpact04"] +t.deathSounds=["zoeDeath01"] +t.pickupSounds=["zoePickup01"] +t.fallSounds=["zoeFall01"] + +t.style = 'female' + + +############### Ninja ################## +t = Appearance("Snake Shadow") + +t.colorTexture = "ninjaColor" +t.colorMaskTexture = "ninjaColorMask" + +t.defaultColor = (1, 1, 1) +t.defaultHighlight = (0.55, 0.8, 0.55) + +t.iconTexture = "ninjaIcon" +t.iconMaskTexture = "ninjaIconColorMask" + +t.headModel = "ninjaHead" +t.torsoModel = "ninjaTorso" +t.pelvisModel = "ninjaPelvis" +t.upperArmModel = "ninjaUpperArm" +t.foreArmModel = "ninjaForeArm" +t.handModel = "ninjaHand" +t.upperLegModel = "ninjaUpperLeg" +t.lowerLegModel = "ninjaLowerLeg" +t.toesModel = "ninjaToes" + +ninjaAttacks = ['ninjaAttack'+str(i+1)+'' for i in range(7)] +ninjaHits = ['ninjaHit'+str(i+1)+'' for i in range(8)] +ninjaJumps = ['ninjaAttack'+str(i+1)+'' for i in range(7)] + +t.jumpSounds=ninjaJumps +t.attackSounds=ninjaAttacks +t.impactSounds=ninjaHits +t.deathSounds=["ninjaDeath1"] +t.pickupSounds=ninjaAttacks +t.fallSounds=["ninjaFall1"] + +t.style = 'ninja' + + +############### Kronk ################## +t = Appearance("Kronk") + +t.colorTexture = "kronk" +t.colorMaskTexture = "kronkColorMask" + +t.defaultColor = (0.4, 0.5, 0.4) +t.defaultHighlight = (1, 0.5, 0.3) + +t.iconTexture = "kronkIcon" +t.iconMaskTexture = "kronkIconColorMask" + +t.headModel = "kronkHead" +t.torsoModel = "kronkTorso" +t.pelvisModel = "kronkPelvis" +t.upperArmModel = "kronkUpperArm" +t.foreArmModel = "kronkForeArm" +t.handModel = "kronkHand" +t.upperLegModel = "kronkUpperLeg" +t.lowerLegModel = "kronkLowerLeg" +t.toesModel = "kronkToes" + +kronkSounds = ["kronk1", + "kronk2", + "kronk3", + "kronk4", + "kronk5", + "kronk6", + "kronk7", + "kronk8", + "kronk9", + "kronk10"] +t.jumpSounds=kronkSounds +t.attackSounds=kronkSounds +t.impactSounds=kronkSounds +t.deathSounds=["kronkDeath"] +t.pickupSounds=kronkSounds +t.fallSounds=["kronkFall"] + +t.style = 'kronk' + + +############### MEL ################## +t = Appearance("Mel") + +t.colorTexture = "melColor" +t.colorMaskTexture = "melColorMask" + +t.defaultColor = (1, 1, 1) +t.defaultHighlight = (0.1, 0.6, 0.1) + +t.iconTexture = "melIcon" +t.iconMaskTexture = "melIconColorMask" + +t.headModel = "melHead" +t.torsoModel = "melTorso" +t.pelvisModel = "kronkPelvis" +t.upperArmModel = "melUpperArm" +t.foreArmModel = "melForeArm" +t.handModel = "melHand" +t.upperLegModel = "melUpperLeg" +t.lowerLegModel = "melLowerLeg" +t.toesModel = "melToes" + +melSounds = ["mel01", + "mel02", + "mel03", + "mel04", + "mel05", + "mel06", + "mel07", + "mel08", + "mel09", + "mel10"] + +t.attackSounds = melSounds +t.jumpSounds = melSounds +t.impactSounds = melSounds +t.deathSounds=["melDeath01"] +t.pickupSounds = melSounds +t.fallSounds=["melFall01"] + +t.style = 'mel' + + +############### Jack Morgan ################## + +t = Appearance("Jack Morgan") + +t.colorTexture = "jackColor" +t.colorMaskTexture = "jackColorMask" + +t.defaultColor = (1, 0.2, 0.1) +t.defaultHighlight = (1, 1, 0) + +t.iconTexture = "jackIcon" +t.iconMaskTexture = "jackIconColorMask" + +t.headModel = "jackHead" +t.torsoModel = "jackTorso" +t.pelvisModel = "kronkPelvis" +t.upperArmModel = "jackUpperArm" +t.foreArmModel = "jackForeArm" +t.handModel = "jackHand" +t.upperLegModel = "jackUpperLeg" +t.lowerLegModel = "jackLowerLeg" +t.toesModel = "jackToes" + +hitSounds = ["jackHit01", + "jackHit02", + "jackHit03", + "jackHit04", + "jackHit05", + "jackHit06", + "jackHit07"] + +sounds = ["jack01", + "jack02", + "jack03", + "jack04", + "jack05", + "jack06"] + +t.attackSounds = sounds +t.jumpSounds = sounds +t.impactSounds = hitSounds +t.deathSounds=["jackDeath01"] +t.pickupSounds = sounds +t.fallSounds=["jackFall01"] + +t.style = 'pirate' + + +############### SANTA ################## + +t = Appearance("Santa Claus") + +t.colorTexture = "santaColor" +t.colorMaskTexture = "santaColorMask" + +t.defaultColor = (1, 0, 0) +t.defaultHighlight = (1, 1, 1) + +t.iconTexture = "santaIcon" +t.iconMaskTexture = "santaIconColorMask" + +t.headModel = "santaHead" +t.torsoModel = "santaTorso" +t.pelvisModel = "kronkPelvis" +t.upperArmModel = "santaUpperArm" +t.foreArmModel = "santaForeArm" +t.handModel = "santaHand" +t.upperLegModel = "santaUpperLeg" +t.lowerLegModel = "santaLowerLeg" +t.toesModel = "santaToes" + +hitSounds = ['santaHit01', 'santaHit02', 'santaHit03', 'santaHit04'] +sounds = ['santa01', 'santa02', 'santa03', 'santa04', 'santa05'] + +t.attackSounds = sounds +t.jumpSounds = sounds +t.impactSounds = hitSounds +t.deathSounds=["santaDeath"] +t.pickupSounds = sounds +t.fallSounds=["santaFall"] + +t.style = 'santa' + +############### FROSTY ################## + +t = Appearance("Frosty") + +t.colorTexture = "frostyColor" +t.colorMaskTexture = "frostyColorMask" + +t.defaultColor = (0.5, 0.5, 1) +t.defaultHighlight = (1, 0.5, 0) + +t.iconTexture = "frostyIcon" +t.iconMaskTexture = "frostyIconColorMask" + +t.headModel = "frostyHead" +t.torsoModel = "frostyTorso" +t.pelvisModel = "frostyPelvis" +t.upperArmModel = "frostyUpperArm" +t.foreArmModel = "frostyForeArm" +t.handModel = "frostyHand" +t.upperLegModel = "frostyUpperLeg" +t.lowerLegModel = "frostyLowerLeg" +t.toesModel = "frostyToes" + +frostySounds = ['frosty01', 'frosty02', 'frosty03', 'frosty04', 'frosty05'] +frostyHitSounds = ['frostyHit01', 'frostyHit02', 'frostyHit03'] + +t.attackSounds = frostySounds +t.jumpSounds = frostySounds +t.impactSounds = frostyHitSounds +t.deathSounds=["frostyDeath"] +t.pickupSounds = frostySounds +t.fallSounds=["frostyFall"] + +t.style = 'frosty' + +############### BONES ################## + +t = Appearance("Bones") + +t.colorTexture = "bonesColor" +t.colorMaskTexture = "bonesColorMask" + +t.defaultColor = (0.6, 0.9, 1) +t.defaultHighlight = (0.6, 0.9, 1) + +t.iconTexture = "bonesIcon" +t.iconMaskTexture = "bonesIconColorMask" + +t.headModel = "bonesHead" +t.torsoModel = "bonesTorso" +t.pelvisModel = "bonesPelvis" +t.upperArmModel = "bonesUpperArm" +t.foreArmModel = "bonesForeArm" +t.handModel = "bonesHand" +t.upperLegModel = "bonesUpperLeg" +t.lowerLegModel = "bonesLowerLeg" +t.toesModel = "bonesToes" + +bonesSounds = ['bones1', 'bones2', 'bones3'] +bonesHitSounds = ['bones1', 'bones2', 'bones3'] + +t.attackSounds = bonesSounds +t.jumpSounds = bonesSounds +t.impactSounds = bonesHitSounds +t.deathSounds=["bonesDeath"] +t.pickupSounds = bonesSounds +t.fallSounds=["bonesFall"] + +t.style = 'bones' + +# bear ################################### + +t = Appearance("Bernard") + +t.colorTexture = "bearColor" +t.colorMaskTexture = "bearColorMask" + +t.defaultColor = (0.7, 0.5, 0.0) +#t.defaultHighlight = (0.6, 0.5, 0.8) + +t.iconTexture = "bearIcon" +t.iconMaskTexture = "bearIconColorMask" + +t.headModel = "bearHead" +t.torsoModel = "bearTorso" +t.pelvisModel = "bearPelvis" +t.upperArmModel = "bearUpperArm" +t.foreArmModel = "bearForeArm" +t.handModel = "bearHand" +t.upperLegModel = "bearUpperLeg" +t.lowerLegModel = "bearLowerLeg" +t.toesModel = "bearToes" + +bearSounds = ['bear1', 'bear2', 'bear3', 'bear4'] +bearHitSounds = ['bearHit1', 'bearHit2'] + +t.attackSounds = bearSounds +t.jumpSounds = bearSounds +t.impactSounds = bearHitSounds +t.deathSounds=["bearDeath"] +t.pickupSounds = bearSounds +t.fallSounds=["bearFall"] + +t.style = 'bear' + +# Penguin ################################### + +t = Appearance("Pascal") + +t.colorTexture = "penguinColor" +t.colorMaskTexture = "penguinColorMask" + +t.defaultColor = (0.3, 0.5, 0.8) +t.defaultHighlight = (1, 0, 0) + +t.iconTexture = "penguinIcon" +t.iconMaskTexture = "penguinIconColorMask" + +t.headModel = "penguinHead" +t.torsoModel = "penguinTorso" +t.pelvisModel = "penguinPelvis" +t.upperArmModel = "penguinUpperArm" +t.foreArmModel = "penguinForeArm" +t.handModel = "penguinHand" +t.upperLegModel = "penguinUpperLeg" +t.lowerLegModel = "penguinLowerLeg" +t.toesModel = "penguinToes" + +penguinSounds = ['penguin1', 'penguin2', 'penguin3', 'penguin4'] +penguinHitSounds = ['penguinHit1', 'penguinHit2'] + +t.attackSounds = penguinSounds +t.jumpSounds = penguinSounds +t.impactSounds = penguinHitSounds +t.deathSounds=["penguinDeath"] +t.pickupSounds = penguinSounds +t.fallSounds=["penguinFall"] + +t.style = 'penguin' + + +# Ali ################################### +t = Appearance("Taobao Mascot") +t.colorTexture = "aliColor" +t.colorMaskTexture = "aliColorMask" +t.defaultColor = (1, 0.5, 0) +t.defaultHighlight = (1, 1, 1) +t.iconTexture = "aliIcon" +t.iconMaskTexture = "aliIconColorMask" +t.headModel = "aliHead" +t.torsoModel = "aliTorso" +t.pelvisModel = "aliPelvis" +t.upperArmModel = "aliUpperArm" +t.foreArmModel = "aliForeArm" +t.handModel = "aliHand" +t.upperLegModel = "aliUpperLeg" +t.lowerLegModel = "aliLowerLeg" +t.toesModel = "aliToes" +aliSounds = ['ali1', 'ali2', 'ali3', 'ali4'] +aliHitSounds = ['aliHit1', 'aliHit2'] +t.attackSounds = aliSounds +t.jumpSounds = aliSounds +t.impactSounds = aliHitSounds +t.deathSounds=["aliDeath"] +t.pickupSounds = aliSounds +t.fallSounds=["aliFall"] +t.style = 'ali' + +# cyborg ################################### +t = Appearance("B-9000") +t.colorTexture = "cyborgColor" +t.colorMaskTexture = "cyborgColorMask" +t.defaultColor = (0.5, 0.5, 0.5) +t.defaultHighlight = (1, 0, 0) +t.iconTexture = "cyborgIcon" +t.iconMaskTexture = "cyborgIconColorMask" +t.headModel = "cyborgHead" +t.torsoModel = "cyborgTorso" +t.pelvisModel = "cyborgPelvis" +t.upperArmModel = "cyborgUpperArm" +t.foreArmModel = "cyborgForeArm" +t.handModel = "cyborgHand" +t.upperLegModel = "cyborgUpperLeg" +t.lowerLegModel = "cyborgLowerLeg" +t.toesModel = "cyborgToes" +cyborgSounds = ['cyborg1', 'cyborg2', 'cyborg3', 'cyborg4'] +cyborgHitSounds = ['cyborgHit1', 'cyborgHit2'] +t.attackSounds = cyborgSounds +t.jumpSounds = cyborgSounds +t.impactSounds = cyborgHitSounds +t.deathSounds=["cyborgDeath"] +t.pickupSounds = cyborgSounds +t.fallSounds=["cyborgFall"] +t.style = 'cyborg' + +# Agent ################################### +t = Appearance("Agent Johnson") +t.colorTexture = "agentColor" +t.colorMaskTexture = "agentColorMask" +t.defaultColor = (0.3, 0.3, 0.33) +t.defaultHighlight = (1, 0.5, 0.3) +t.iconTexture = "agentIcon" +t.iconMaskTexture = "agentIconColorMask" +t.headModel = "agentHead" +t.torsoModel = "agentTorso" +t.pelvisModel = "agentPelvis" +t.upperArmModel = "agentUpperArm" +t.foreArmModel = "agentForeArm" +t.handModel = "agentHand" +t.upperLegModel = "agentUpperLeg" +t.lowerLegModel = "agentLowerLeg" +t.toesModel = "agentToes" +agentSounds = ['agent1', 'agent2', 'agent3', 'agent4'] +agentHitSounds = ['agentHit1', 'agentHit2'] +t.attackSounds = agentSounds +t.jumpSounds = agentSounds +t.impactSounds = agentHitSounds +t.deathSounds=["agentDeath"] +t.pickupSounds = agentSounds +t.fallSounds=["agentFall"] +t.style = 'agent' + +# Jumpsuit ################################### +t = Appearance("Lee") +t.colorTexture = "jumpsuitColor" +t.colorMaskTexture = "jumpsuitColorMask" +t.defaultColor = (0.3, 0.5, 0.8) +t.defaultHighlight = (1, 0, 0) +t.iconTexture = "jumpsuitIcon" +t.iconMaskTexture = "jumpsuitIconColorMask" +t.headModel = "jumpsuitHead" +t.torsoModel = "jumpsuitTorso" +t.pelvisModel = "jumpsuitPelvis" +t.upperArmModel = "jumpsuitUpperArm" +t.foreArmModel = "jumpsuitForeArm" +t.handModel = "jumpsuitHand" +t.upperLegModel = "jumpsuitUpperLeg" +t.lowerLegModel = "jumpsuitLowerLeg" +t.toesModel = "jumpsuitToes" +jumpsuitSounds = ['jumpsuit1', 'jumpsuit2', 'jumpsuit3', 'jumpsuit4'] +jumpsuitHitSounds = ['jumpsuitHit1', 'jumpsuitHit2'] +t.attackSounds = jumpsuitSounds +t.jumpSounds = jumpsuitSounds +t.impactSounds = jumpsuitHitSounds +t.deathSounds=["jumpsuitDeath"] +t.pickupSounds = jumpsuitSounds +t.fallSounds=["jumpsuitFall"] +t.style = 'spaz' + +# ActionHero ################################### +t = Appearance("Todd McBurton") +t.colorTexture = "actionHeroColor" +t.colorMaskTexture = "actionHeroColorMask" +t.defaultColor = (0.3, 0.5, 0.8) +t.defaultHighlight = (1, 0, 0) +t.iconTexture = "actionHeroIcon" +t.iconMaskTexture = "actionHeroIconColorMask" +t.headModel = "actionHeroHead" +t.torsoModel = "actionHeroTorso" +t.pelvisModel = "actionHeroPelvis" +t.upperArmModel = "actionHeroUpperArm" +t.foreArmModel = "actionHeroForeArm" +t.handModel = "actionHeroHand" +t.upperLegModel = "actionHeroUpperLeg" +t.lowerLegModel = "actionHeroLowerLeg" +t.toesModel = "actionHeroToes" +actionHeroSounds = ['actionHero1', 'actionHero2', 'actionHero3', 'actionHero4'] +actionHeroHitSounds = ['actionHeroHit1', 'actionHeroHit2'] +t.attackSounds = actionHeroSounds +t.jumpSounds = actionHeroSounds +t.impactSounds = actionHeroHitSounds +t.deathSounds=["actionHeroDeath"] +t.pickupSounds = actionHeroSounds +t.fallSounds=["actionHeroFall"] +t.style = 'spaz' + +# Assassin ################################### +t = Appearance("Zola") +t.colorTexture = "assassinColor" +t.colorMaskTexture = "assassinColorMask" +t.defaultColor = (0.3, 0.5, 0.8) +t.defaultHighlight = (1, 0, 0) +t.iconTexture = "assassinIcon" +t.iconMaskTexture = "assassinIconColorMask" +t.headModel = "assassinHead" +t.torsoModel = "assassinTorso" +t.pelvisModel = "assassinPelvis" +t.upperArmModel = "assassinUpperArm" +t.foreArmModel = "assassinForeArm" +t.handModel = "assassinHand" +t.upperLegModel = "assassinUpperLeg" +t.lowerLegModel = "assassinLowerLeg" +t.toesModel = "assassinToes" +assassinSounds = ['assassin1', 'assassin2', 'assassin3', 'assassin4'] +assassinHitSounds = ['assassinHit1', 'assassinHit2'] +t.attackSounds = assassinSounds +t.jumpSounds = assassinSounds +t.impactSounds = assassinHitSounds +t.deathSounds=["assassinDeath"] +t.pickupSounds = assassinSounds +t.fallSounds=["assassinFall"] +t.style = 'spaz' + +# Wizard ################################### +t = Appearance("Grumbledorf") +t.colorTexture = "wizardColor" +t.colorMaskTexture = "wizardColorMask" +t.defaultColor = (0.2, 0.4, 1.0) +t.defaultHighlight = (0.06, 0.15, 0.4) +t.iconTexture = "wizardIcon" +t.iconMaskTexture = "wizardIconColorMask" +t.headModel = "wizardHead" +t.torsoModel = "wizardTorso" +t.pelvisModel = "wizardPelvis" +t.upperArmModel = "wizardUpperArm" +t.foreArmModel = "wizardForeArm" +t.handModel = "wizardHand" +t.upperLegModel = "wizardUpperLeg" +t.lowerLegModel = "wizardLowerLeg" +t.toesModel = "wizardToes" +wizardSounds = ['wizard1', 'wizard2', 'wizard3', 'wizard4'] +wizardHitSounds = ['wizardHit1', 'wizardHit2'] +t.attackSounds = wizardSounds +t.jumpSounds = wizardSounds +t.impactSounds = wizardHitSounds +t.deathSounds=["wizardDeath"] +t.pickupSounds = wizardSounds +t.fallSounds=["wizardFall"] +t.style = 'spaz' + +# Cowboy ################################### +t = Appearance("Butch") +t.colorTexture = "cowboyColor" +t.colorMaskTexture = "cowboyColorMask" +t.defaultColor = (0.3, 0.5, 0.8) +t.defaultHighlight = (1, 0, 0) +t.iconTexture = "cowboyIcon" +t.iconMaskTexture = "cowboyIconColorMask" +t.headModel = "cowboyHead" +t.torsoModel = "cowboyTorso" +t.pelvisModel = "cowboyPelvis" +t.upperArmModel = "cowboyUpperArm" +t.foreArmModel = "cowboyForeArm" +t.handModel = "cowboyHand" +t.upperLegModel = "cowboyUpperLeg" +t.lowerLegModel = "cowboyLowerLeg" +t.toesModel = "cowboyToes" +cowboySounds = ['cowboy1', 'cowboy2', 'cowboy3', 'cowboy4'] +cowboyHitSounds = ['cowboyHit1', 'cowboyHit2'] +t.attackSounds = cowboySounds +t.jumpSounds = cowboySounds +t.impactSounds = cowboyHitSounds +t.deathSounds=["cowboyDeath"] +t.pickupSounds = cowboySounds +t.fallSounds=["cowboyFall"] +t.style = 'spaz' + +# Witch ################################### +t = Appearance("Witch") +t.colorTexture = "witchColor" +t.colorMaskTexture = "witchColorMask" +t.defaultColor = (0.3, 0.5, 0.8) +t.defaultHighlight = (1, 0, 0) +t.iconTexture = "witchIcon" +t.iconMaskTexture = "witchIconColorMask" +t.headModel = "witchHead" +t.torsoModel = "witchTorso" +t.pelvisModel = "witchPelvis" +t.upperArmModel = "witchUpperArm" +t.foreArmModel = "witchForeArm" +t.handModel = "witchHand" +t.upperLegModel = "witchUpperLeg" +t.lowerLegModel = "witchLowerLeg" +t.toesModel = "witchToes" +witchSounds = ['witch1', 'witch2', 'witch3', 'witch4'] +witchHitSounds = ['witchHit1', 'witchHit2'] +t.attackSounds = witchSounds +t.jumpSounds = witchSounds +t.impactSounds = witchHitSounds +t.deathSounds=["witchDeath"] +t.pickupSounds = witchSounds +t.fallSounds=["witchFall"] +t.style = 'spaz' + +# Warrior ################################### +t = Appearance("Warrior") +t.colorTexture = "warriorColor" +t.colorMaskTexture = "warriorColorMask" +t.defaultColor = (0.3, 0.5, 0.8) +t.defaultHighlight = (1, 0, 0) +t.iconTexture = "warriorIcon" +t.iconMaskTexture = "warriorIconColorMask" +t.headModel = "warriorHead" +t.torsoModel = "warriorTorso" +t.pelvisModel = "warriorPelvis" +t.upperArmModel = "warriorUpperArm" +t.foreArmModel = "warriorForeArm" +t.handModel = "warriorHand" +t.upperLegModel = "warriorUpperLeg" +t.lowerLegModel = "warriorLowerLeg" +t.toesModel = "warriorToes" +warriorSounds = ['warrior1', 'warrior2', 'warrior3', 'warrior4'] +warriorHitSounds = ['warriorHit1', 'warriorHit2'] +t.attackSounds = warriorSounds +t.jumpSounds = warriorSounds +t.impactSounds = warriorHitSounds +t.deathSounds=["warriorDeath"] +t.pickupSounds = warriorSounds +t.fallSounds=["warriorFall"] +t.style = 'spaz' + +# Superhero ################################### +t = Appearance("Middle-Man") +t.colorTexture = "superheroColor" +t.colorMaskTexture = "superheroColorMask" +t.defaultColor = (0.3, 0.5, 0.8) +t.defaultHighlight = (1, 0, 0) +t.iconTexture = "superheroIcon" +t.iconMaskTexture = "superheroIconColorMask" +t.headModel = "superheroHead" +t.torsoModel = "superheroTorso" +t.pelvisModel = "superheroPelvis" +t.upperArmModel = "superheroUpperArm" +t.foreArmModel = "superheroForeArm" +t.handModel = "superheroHand" +t.upperLegModel = "superheroUpperLeg" +t.lowerLegModel = "superheroLowerLeg" +t.toesModel = "superheroToes" +superheroSounds = ['superhero1', 'superhero2', 'superhero3', 'superhero4'] +superheroHitSounds = ['superheroHit1', 'superheroHit2'] +t.attackSounds = superheroSounds +t.jumpSounds = superheroSounds +t.impactSounds = superheroHitSounds +t.deathSounds=["superheroDeath"] +t.pickupSounds = superheroSounds +t.fallSounds=["superheroFall"] +t.style = 'spaz' + +# Alien ################################### +t = Appearance("Alien") +t.colorTexture = "alienColor" +t.colorMaskTexture = "alienColorMask" +t.defaultColor = (0.3, 0.5, 0.8) +t.defaultHighlight = (1, 0, 0) +t.iconTexture = "alienIcon" +t.iconMaskTexture = "alienIconColorMask" +t.headModel = "alienHead" +t.torsoModel = "alienTorso" +t.pelvisModel = "alienPelvis" +t.upperArmModel = "alienUpperArm" +t.foreArmModel = "alienForeArm" +t.handModel = "alienHand" +t.upperLegModel = "alienUpperLeg" +t.lowerLegModel = "alienLowerLeg" +t.toesModel = "alienToes" +alienSounds = ['alien1', 'alien2', 'alien3', 'alien4'] +alienHitSounds = ['alienHit1', 'alienHit2'] +t.attackSounds = alienSounds +t.jumpSounds = alienSounds +t.impactSounds = alienHitSounds +t.deathSounds=["alienDeath"] +t.pickupSounds = alienSounds +t.fallSounds=["alienFall"] +t.style = 'spaz' + +# OldLady ################################### +t = Appearance("OldLady") +t.colorTexture = "oldLadyColor" +t.colorMaskTexture = "oldLadyColorMask" +t.defaultColor = (0.3, 0.5, 0.8) +t.defaultHighlight = (1, 0, 0) +t.iconTexture = "oldLadyIcon" +t.iconMaskTexture = "oldLadyIconColorMask" +t.headModel = "oldLadyHead" +t.torsoModel = "oldLadyTorso" +t.pelvisModel = "oldLadyPelvis" +t.upperArmModel = "oldLadyUpperArm" +t.foreArmModel = "oldLadyForeArm" +t.handModel = "oldLadyHand" +t.upperLegModel = "oldLadyUpperLeg" +t.lowerLegModel = "oldLadyLowerLeg" +t.toesModel = "oldLadyToes" +oldLadySounds = ['oldLady1', 'oldLady2', 'oldLady3', 'oldLady4'] +oldLadyHitSounds = ['oldLadyHit1', 'oldLadyHit2'] +t.attackSounds = oldLadySounds +t.jumpSounds = oldLadySounds +t.impactSounds = oldLadyHitSounds +t.deathSounds=["oldLadyDeath"] +t.pickupSounds = oldLadySounds +t.fallSounds=["oldLadyFall"] +t.style = 'spaz' + +# Gladiator ################################### +t = Appearance("Gladiator") +t.colorTexture = "gladiatorColor" +t.colorMaskTexture = "gladiatorColorMask" +t.defaultColor = (0.3, 0.5, 0.8) +t.defaultHighlight = (1, 0, 0) +t.iconTexture = "gladiatorIcon" +t.iconMaskTexture = "gladiatorIconColorMask" +t.headModel = "gladiatorHead" +t.torsoModel = "gladiatorTorso" +t.pelvisModel = "gladiatorPelvis" +t.upperArmModel = "gladiatorUpperArm" +t.foreArmModel = "gladiatorForeArm" +t.handModel = "gladiatorHand" +t.upperLegModel = "gladiatorUpperLeg" +t.lowerLegModel = "gladiatorLowerLeg" +t.toesModel = "gladiatorToes" +gladiatorSounds = ['gladiator1', 'gladiator2', 'gladiator3', 'gladiator4'] +gladiatorHitSounds = ['gladiatorHit1', 'gladiatorHit2'] +t.attackSounds = gladiatorSounds +t.jumpSounds = gladiatorSounds +t.impactSounds = gladiatorHitSounds +t.deathSounds=["gladiatorDeath"] +t.pickupSounds = gladiatorSounds +t.fallSounds=["gladiatorFall"] +t.style = 'spaz' + +# Wrestler ################################### +t = Appearance("Wrestler") +t.colorTexture = "wrestlerColor" +t.colorMaskTexture = "wrestlerColorMask" +t.defaultColor = (0.3, 0.5, 0.8) +t.defaultHighlight = (1, 0, 0) +t.iconTexture = "wrestlerIcon" +t.iconMaskTexture = "wrestlerIconColorMask" +t.headModel = "wrestlerHead" +t.torsoModel = "wrestlerTorso" +t.pelvisModel = "wrestlerPelvis" +t.upperArmModel = "wrestlerUpperArm" +t.foreArmModel = "wrestlerForeArm" +t.handModel = "wrestlerHand" +t.upperLegModel = "wrestlerUpperLeg" +t.lowerLegModel = "wrestlerLowerLeg" +t.toesModel = "wrestlerToes" +wrestlerSounds = ['wrestler1', 'wrestler2', 'wrestler3', 'wrestler4'] +wrestlerHitSounds = ['wrestlerHit1', 'wrestlerHit2'] +t.attackSounds = wrestlerSounds +t.jumpSounds = wrestlerSounds +t.impactSounds = wrestlerHitSounds +t.deathSounds=["wrestlerDeath"] +t.pickupSounds = wrestlerSounds +t.fallSounds=["wrestlerFall"] +t.style = 'spaz' + +# OperaSinger ################################### +t = Appearance("Gretel") +t.colorTexture = "operaSingerColor" +t.colorMaskTexture = "operaSingerColorMask" +t.defaultColor = (0.3, 0.5, 0.8) +t.defaultHighlight = (1, 0, 0) +t.iconTexture = "operaSingerIcon" +t.iconMaskTexture = "operaSingerIconColorMask" +t.headModel = "operaSingerHead" +t.torsoModel = "operaSingerTorso" +t.pelvisModel = "operaSingerPelvis" +t.upperArmModel = "operaSingerUpperArm" +t.foreArmModel = "operaSingerForeArm" +t.handModel = "operaSingerHand" +t.upperLegModel = "operaSingerUpperLeg" +t.lowerLegModel = "operaSingerLowerLeg" +t.toesModel = "operaSingerToes" +operaSingerSounds = ['operaSinger1', 'operaSinger2', + 'operaSinger3', 'operaSinger4'] +operaSingerHitSounds = ['operaSingerHit1', 'operaSingerHit2'] +t.attackSounds = operaSingerSounds +t.jumpSounds = operaSingerSounds +t.impactSounds = operaSingerHitSounds +t.deathSounds=["operaSingerDeath"] +t.pickupSounds = operaSingerSounds +t.fallSounds=["operaSingerFall"] +t.style = 'spaz' + +# Pixie ################################### +t = Appearance("Pixel") +t.colorTexture = "pixieColor" +t.colorMaskTexture = "pixieColorMask" +t.defaultColor = (0, 1, 0.7) +t.defaultHighlight = (0.65, 0.35, 0.75) +t.iconTexture = "pixieIcon" +t.iconMaskTexture = "pixieIconColorMask" +t.headModel = "pixieHead" +t.torsoModel = "pixieTorso" +t.pelvisModel = "pixiePelvis" +t.upperArmModel = "pixieUpperArm" +t.foreArmModel = "pixieForeArm" +t.handModel = "pixieHand" +t.upperLegModel = "pixieUpperLeg" +t.lowerLegModel = "pixieLowerLeg" +t.toesModel = "pixieToes" +pixieSounds = ['pixie1', 'pixie2', 'pixie3', 'pixie4'] +pixieHitSounds = ['pixieHit1', 'pixieHit2'] +t.attackSounds = pixieSounds +t.jumpSounds = pixieSounds +t.impactSounds = pixieHitSounds +t.deathSounds=["pixieDeath"] +t.pickupSounds = pixieSounds +t.fallSounds=["pixieFall"] +t.style = 'pixie' + +# Robot ################################### +t = Appearance("Robot") +t.colorTexture = "robotColor" +t.colorMaskTexture = "robotColorMask" +t.defaultColor = (0.3, 0.5, 0.8) +t.defaultHighlight = (1, 0, 0) +t.iconTexture = "robotIcon" +t.iconMaskTexture = "robotIconColorMask" +t.headModel = "robotHead" +t.torsoModel = "robotTorso" +t.pelvisModel = "robotPelvis" +t.upperArmModel = "robotUpperArm" +t.foreArmModel = "robotForeArm" +t.handModel = "robotHand" +t.upperLegModel = "robotUpperLeg" +t.lowerLegModel = "robotLowerLeg" +t.toesModel = "robotToes" +robotSounds = ['robot1', 'robot2', 'robot3', 'robot4'] +robotHitSounds = ['robotHit1', 'robotHit2'] +t.attackSounds = robotSounds +t.jumpSounds = robotSounds +t.impactSounds = robotHitSounds +t.deathSounds=["robotDeath"] +t.pickupSounds = robotSounds +t.fallSounds=["robotFall"] +t.style = 'spaz' + +# Bunny ################################### +t = Appearance("Easter Bunny") +t.colorTexture = "bunnyColor" +t.colorMaskTexture = "bunnyColorMask" +t.defaultColor = (1, 1, 1) +t.defaultHighlight = (1, 0.5, 0.5) +t.iconTexture = "bunnyIcon" +t.iconMaskTexture = "bunnyIconColorMask" +t.headModel = "bunnyHead" +t.torsoModel = "bunnyTorso" +t.pelvisModel = "bunnyPelvis" +t.upperArmModel = "bunnyUpperArm" +t.foreArmModel = "bunnyForeArm" +t.handModel = "bunnyHand" +t.upperLegModel = "bunnyUpperLeg" +t.lowerLegModel = "bunnyLowerLeg" +t.toesModel = "bunnyToes" +bunnySounds = ['bunny1', 'bunny2', 'bunny3', 'bunny4'] +bunnyHitSounds = ['bunnyHit1', 'bunnyHit2'] +t.attackSounds = bunnySounds +t.jumpSounds = ['bunnyJump'] +t.impactSounds = bunnyHitSounds +t.deathSounds=["bunnyDeath"] +t.pickupSounds = bunnySounds +t.fallSounds=["bunnyFall"] +t.style = 'bunny' diff --git a/scripts/bsTeamGame.py b/scripts/bsTeamGame.py new file mode 100644 index 0000000..294594e --- /dev/null +++ b/scripts/bsTeamGame.py @@ -0,0 +1,1455 @@ +import bs +import bsGame +import bsUtils +import random +import weakref +import copy +import bsTutorial +import bsInternal + +gDefaultTeamColors = ((0.1, 0.25, 1.0), (1.0, 0.25, 0.2)) +gDefaultTeamNames = ("Blue", "Red") + +gTeamSeriesLength = 7 +gFFASeriesLength = 24 + + +class TeamGameResults(object): + """ + category: Game Flow Classes + + Results for a completed bs.TeamGameActivity. + Upon completion, a game should fill one of these out and pass it to its + bs.Activity.end() call. + """ + + def __init__(self): + """ + Instantiate a results instance. + """ + + self._gameSet = False + self._scores = {} + + def _setGame(self, game): + if self._gameSet: + raise Exception("game being set twice for TeamGameResults") + self._gameSet = True + self._teams = [weakref.ref(team) for team in game.teams] + scoreInfo = game.getResolvedScoreInfo() + self._playerInfo = copy.deepcopy(game.initialPlayerInfo) + self._lowerIsBetter = scoreInfo['lowerIsBetter'] + self._scoreName = scoreInfo['scoreName'] + self._noneIsWinner = scoreInfo['noneIsWinner'] + self._scoreType = scoreInfo['scoreType'] + + def setTeamScore(self, team, score): + """ + Set the score for a given bs.Team. + This can be a number or None. + (see the noneIsWinner arg in the constructor) + """ + self._scores[team.getID()] = (weakref.ref(team), score) + + def _getTeamScore(self, team): + 'Return the score for a given bs.Team' + for score in self._scores.values(): + if score[0]() is team: + return score[1] + # if we have no score, None is assumed + return None + + def _getTeams(self): + 'Return all bs.Teams in the results.' + if not self._gameSet: + raise Exception("cant get this until game is set") + teams = [] + for teamRef in self._teams: + team = teamRef() + if team is not None: + teams.append(team) + return teams + + def _hasScoreForTeam(self, team): + 'Return whether there is a score for a given bs.Team' + for score in self._scores.values(): + if score[0]() is team: + return True + return False + + def _getTeamScoreStr(self, team): + """ + Return a score for a bs.Team as a string, + properly formatted for the score type. + """ + if not self._gameSet: + raise Exception("cant get this until game is set") + for score in self._scores.values(): + if score[0]() is team: + if score[1] is None: + return '-' + if self._scoreType == 'seconds': + return bsUtils.getTimeString(score[1]*1000, centi=False) + elif self._scoreType == 'milliseconds': + return bsUtils.getTimeString(score[1], centi=True) + else: + return str(score[1]) + return '-' + + def _getScoreName(self): + 'Return the name associated with scores (\'points\', etc)' + if not self._gameSet: + raise Exception("cant get this until game is set") + return self._scoreName + + def _getLowerIsBetter(self): + 'Return whether lower scores are better' + if not self._gameSet: + raise Exception("cant get this until game is set") + return self._lowerIsBetter + + def _getWinningTeam(self): + 'Return the winning bs.Team if there is one; None othewise.' + if not self._gameSet: + raise Exception("cant get winners until game is set") + winners = self._getWinners() + if len(winners) > 0 and len(winners[0]['teams']) == 1: + return winners[0]['teams'][0] + else: + return None + + def _getWinners(self): + 'Return an ordered list of dicts containing score and teams' + if not self._gameSet: + raise Exception("cant get winners until game is set") + winners = {} + # filter out any dead teams + scores = [score for score in self._scores.values() + if score[0]() is not None and score[1] is not None] + for score in scores: + try: + s = winners[score[1]] + except Exception: + s = winners[score[1]] = [] + s.append(score[0]()) + results = winners.items() + results.sort(reverse=not self._lowerIsBetter) + + # tack a group with all our 'None' scores onto the end + noneTeams = [score[0]() for score in self._scores.values() + if score[0]() is not None and score[1] is None] + if len(noneTeams) > 0: + nones = [(None, noneTeams)] + if self._noneIsWinner: + results = nones + results + else: + results = results + nones + return [{'score': r[0], 'teams':r[1]} for r in results] + + +class ShuffleList(object): + """ + shuffles a set of games with some smarts + to avoid repeats in maps or game types + """ + + def __init__(self, items, shuffle=True): + self.sourceList = items + self.shuffle = shuffle + self.shuffleList = [] + self.lastGotten = None + + def pullNext(self): + + # refill our list if its empty + if len(self.shuffleList) == 0: + self.shuffleList = list(self.sourceList) + + # ok now find an index we should pull + index = 0 + + if self.shuffle: + for i in range(4): + index = random.randrange(0, len(self.shuffleList)) + testObj = self.shuffleList[index] + # if the new one is the same map or game-type as the previous, + # lets try to keep looking.. + if len(self.shuffleList) > 1 and self.lastGotten is not None: + if testObj['settings']['map'] == \ + self.lastGotten['settings']['map']: + continue + if testObj['type'] == self.lastGotten['type']: + continue + # sufficiently different.. lets go with it + break + + obj = self.shuffleList.pop(index) + self.lastGotten = obj + return obj + + +class TeamsScoreScreenActivity(bsGame.ScoreScreenActivity): + + def __init__(self, settings={}): + bsGame.ScoreScreenActivity.__init__(self, settings=settings) + self._scoreDisplaySound = bs.getSound("scoreHit01") + self._scoreDisplaySoundSmall = bs.getSound("scoreHit02") + + def onBegin(self, showUpNext=True, customContinueMessage=None): + bsGame.ScoreScreenActivity.onBegin( + self, customContinueMessage=customContinueMessage) + if showUpNext: + t = bs.Lstr(value='${A} ${B}', + subs=[('${A}', bs.Lstr( + resource='upNextText', + subs=[('${COUNT}', + str(self.getSession().getGameNumber()+1))]) + ),('${B}', self.getSession().getNextGameDescription())]) + bsUtils.Text(t, + maxWidth=900, + hAttach='center', + vAttach='bottom', + hAlign='center', + vAlign='center', + position=(0, 53), + flash=False, + color=(0.3, 0.3, 0.35, 1.0), + transition='fadeIn', + transitionDelay=2000).autoRetain() + + def showPlayerScores( + self, delay=2500, results=None, scale=1.0, xOffset=0, yOffset=0): + tsVOffset = 150.0+yOffset + tsHOffs = 80.0+xOffset + tDelay = delay + spacing = 40 + + isFreeForAll = isinstance(self.getSession(), bs.FreeForAllSession) + + def _getPlayerScore(player): + if isFreeForAll and results is not None: + return results._getTeamScore(player.getTeam()) + else: + return player.accumScore + + def _getPlayerScoreStr(player): + if isFreeForAll and results is not None: + return results._getTeamScoreStr(player.getTeam()) + else: + return str(player.accumScore) + + # getValidPlayers() can return players that are no longer in the game.. + # if we're using results we have to filter + # those out (since they're not in results and that's where we pull their + # scores from) + if results is not None: + playersSorted = [] + validPlayers = self.scoreSet.getValidPlayers().items() + + def _getPlayerScoreSetEntry(player): + for p in validPlayers: + if p[1].getPlayer() is player: + return p[1] + return None + + # results is already sorted; just convert it into a list of + # score-set-entries + for winner in results._getWinners(): + for team in winner['teams']: + if len(team.players) == 1: + playerEntry = _getPlayerScoreSetEntry(team.players[0]) + if playerEntry is not None: + playersSorted.append(playerEntry) + else: + playersSorted = [ + [_getPlayerScore(p), + name, p] for name, p in + self.scoreSet.getValidPlayers().items()] + playersSorted.sort( + reverse=( + results + is None or + not results._getLowerIsBetter())) + # just want living player entries + playersSorted = [p[2] for p in playersSorted if p[2]] + + vOffs = -140.0 + spacing*len(playersSorted)*0.5 + + def _txt( + xOffs, yOffs, text, hAlign='right', extraScale=1.0, + maxWidth=120.0): + bsUtils.Text(text, color=(0.5, 0.5, 0.6, 0.5), + position=(tsHOffs + xOffs * scale, tsVOffset + + (vOffs + yOffs + 4.0) * scale), + hAlign=hAlign, vAlign='center', scale=0.8 * scale * + extraScale, maxWidth=maxWidth, transition='inLeft', + transitionDelay=tDelay).autoRetain() + + _txt( + 180, 43, bs.Lstr( + resource='gameLeadersText', + subs=[('${COUNT}', str(self.getSession().getGameNumber()))]), + hAlign='center', extraScale=1.4, maxWidth=None) + _txt(-15, 4, bs.Lstr(resource='playerText'), hAlign='left') + _txt(180, 4, bs.Lstr(resource='killsText')) + _txt(280, 4, bs.Lstr(resource='deathsText'), maxWidth=100) + + scoreName = 'Score' if results is None else results._getScoreName() + translated = bs.Lstr(translate=('scoreNames', scoreName)) + + _txt(390, 0, translated) + + topKillCount = 0 + topKilledCount = 99999 + topScore = 0 if( + len(playersSorted) == 0) else _getPlayerScore( + playersSorted[0]) + + for p in playersSorted: + topKillCount = max(topKillCount, p.accumKillCount) + topKilledCount = min(topKilledCount, p.accumKilledCount) + + def _scoreTxt(text, xOffs, highlight, delay, maxWidth=70.0): + bsUtils.Text( + text, + position=(tsHOffs + xOffs * scale, tsVOffset + (vOffs + 15) + * scale), + scale=scale, color=(1.0, 0.9, 0.5, 1.0) + if highlight else(0.5, 0.5, 0.6, 0.5), hAlign='right', + vAlign='center', maxWidth=maxWidth, transition='inLeft', + transitionDelay=tDelay + delay).autoRetain() + + for p in playersSorted: + tDelay += 50 + vOffs -= spacing + bsUtils.Image( + p.getIcon(), + position=(tsHOffs - 12 * scale, + tsVOffset + (vOffs + 15.0) * scale), + scale=(30.0 * scale, 30.0 * scale), + transition='inLeft', transitionDelay=tDelay).autoRetain() + bsUtils.Text( + bs.Lstr(value=p.getName(full=True)), + maxWidth=160, scale=0.75 * scale, + position=(tsHOffs + 10.0 * scale, + tsVOffset + (vOffs + 15) * scale), + hAlign='left', vAlign='center', color=bs.getSafeColor( + p.getTeam().color + (1,)), + transition='inLeft', transitionDelay=tDelay).autoRetain() + _scoreTxt( + str(p.accumKillCount), + 180, p.accumKillCount == topKillCount, 100) + _scoreTxt(str(p.accumKilledCount), 280, + p.accumKilledCount == topKilledCount, 100) + _scoreTxt( + _getPlayerScoreStr(p), + 390, _getPlayerScore(p) == topScore, 200) + + +class FreeForAllVictoryScoreScreenActivity(TeamsScoreScreenActivity): + + def __init__(self, settings={}): + TeamsScoreScreenActivity.__init__(self, settings=settings) + self._transitionTime = 500 # keeps prev activity alive while we fade in + + self._cymbalSound = bs.getSound('cymbal') + + def onBegin(self): + bsInternal._setAnalyticsScreen('FreeForAll Score Screen') + TeamsScoreScreenActivity.onBegin(self) + + yBase = 100 + tsHOffs = -305 + tDelay = 1000 + scale = 1.2 + spacing = 37.0 + + # we include name and previous score in the sort to reduce the amount + # of random jumping around the list we do in cases of ties + playerOrderPrev = list(self.players) + playerOrderPrev.sort(reverse=True, key=lambda player: ( + player.getTeam().sessionData['previousScore'], + player.getName(full=True))) + playerOrder = list(self.players) + playerOrder.sort(reverse=True, + key=lambda player: ( + player.getTeam().sessionData['score'], + player.getTeam().sessionData['score'], + player.getName(full=True))) + + vOffs = -74.0 + spacing*len(playerOrderPrev)*0.5 + + delay1 = 1300+100 + delay2 = 2900+100 + delay3 = 2900+100 + + orderChange = playerOrder != playerOrderPrev + + if orderChange: + delay3 += 1500 + + bs.gameTimer(300, bs.Call(bs.playSound, self._scoreDisplaySound)) + self.showPlayerScores( + delay=1, results=self.settings['results'], + scale=1.2, xOffset=-110) + + soundTimes = set() + + def _scoreTxt( + text, xOffs, yOffs, highlight, delay, extraScale, flash=False): + return bsUtils.Text(text, + position=(tsHOffs+xOffs*scale, + yBase+(yOffs+vOffs+2.0)*scale), + scale=scale*extraScale, + color=((1.0, 0.7, 0.3, 1.0) if highlight + else (0.7, 0.7, 0.7, 0.7)), + hAlign='right', + transition='inLeft', + transitionDelay=tDelay+delay, + flash=flash).autoRetain() + vOffs -= spacing + + slideAmt = 0.0 + + transitionTime = 250 + transitionTime2 = 250 + + title = bsUtils.Text( + bs.Lstr( + resource='firstToSeriesText', + subs=[('${COUNT}', str(self.getSession()._ffaSeriesLength))]), + scale=1.05 * scale, + position=(tsHOffs - 0.0 * scale, yBase + (vOffs + 50.0) * scale), + hAlign='center', color=(0.5, 0.5, 0.5, 0.5), + transition='inLeft', transitionDelay=tDelay).autoRetain() + + vOffs -= 25 + vOffsStart = vOffs + + bs.gameTimer(tDelay+delay3, bs.WeakCall( + self._safeAnimate, + title.positionCombine, + 'input0', {0: tsHOffs-0.0*scale, + transitionTime2: tsHOffs-(0.0+slideAmt)*scale})) + + for i, player in enumerate(playerOrderPrev): + vOffs2 = vOffsStart - spacing * (playerOrder.index(player)) + + bs.gameTimer(tDelay+300, bs.Call(bs.playSound, + self._scoreDisplaySoundSmall)) + if orderChange: + bs.gameTimer(tDelay+delay2+100, + bs.Call(bs.playSound, self._cymbalSound)) + + img = bsUtils.Image( + player.getIcon(), + position=(tsHOffs - 72.0 * scale, + yBase + (vOffs + 15.0) * scale), + scale=(30.0 * scale, 30.0 * scale), + transition='inLeft', transitionDelay=tDelay).autoRetain() + bs.gameTimer( + tDelay + delay2, bs.WeakCall( + self._safeAnimate, img.positionCombine, 'input1', + {0: yBase + (vOffs + 15.0) * scale, transitionTime: yBase + + (vOffs2 + 15.0) * scale})) + bs.gameTimer( + tDelay + delay3, bs.WeakCall( + self._safeAnimate, img.positionCombine, 'input0', + {0: tsHOffs - 72.0 * scale, transitionTime2: tsHOffs - + (72.0 + slideAmt) * scale})) + txt = bsUtils.Text( + bs.Lstr(value=player.getName(full=True)), + maxWidth=130.0, scale=0.75 * scale, + position=(tsHOffs - 50.0 * scale, + yBase + (vOffs + 15.0) * scale), + hAlign='left', vAlign='center', color=bs.getSafeColor( + player.getTeam().color + (1,)), + transition='inLeft', transitionDelay=tDelay).autoRetain() + bs.gameTimer( + tDelay + delay2, bs.WeakCall( + self._safeAnimate, txt.positionCombine, 'input1', + {0: yBase + (vOffs + 15.0) * scale, transitionTime: yBase + + (vOffs2 + 15.0) * scale})) + bs.gameTimer( + tDelay + delay3, bs.WeakCall( + self._safeAnimate, txt.positionCombine, 'input0', + {0: tsHOffs - 50.0 * scale, transitionTime2: tsHOffs - + (50.0 + slideAmt) * scale})) + + txtNum = bsUtils.Text( + '#' + str(i + 1), + scale=0.55 * scale, + position=(tsHOffs - 95.0 * scale, + yBase + (vOffs + 8.0) * scale), + hAlign='right', color=(0.6, 0.6, 0.6, 0.6), + transition='inLeft', transitionDelay=tDelay).autoRetain() + bs.gameTimer( + tDelay + delay3, bs.WeakCall( + self._safeAnimate, txtNum.positionCombine, 'input0', + {0: tsHOffs - 95.0 * scale, transitionTime2: tsHOffs - + (95.0 + slideAmt) * scale})) + + sTxt = _scoreTxt( + str(player.getTeam().sessionData['previousScore']), + 80, 0, False, 0, 1.0) + bs.gameTimer( + tDelay + delay2, bs.WeakCall( + self._safeAnimate, sTxt.positionCombine, 'input1', + {0: yBase + (vOffs + 2.0) * scale, transitionTime: yBase + + (vOffs2 + 2.0) * scale})) + bs.gameTimer(tDelay+delay3, bs.WeakCall( + self._safeAnimate, sTxt.positionCombine, + 'input0', {0: tsHOffs+80*scale, + transitionTime2: tsHOffs+(80-slideAmt)*scale})) + + scoreChange = player.getTeam().sessionData['score'] \ + - player.getTeam().sessionData['previousScore'] + if scoreChange > 0: + x = 113 + y = 3.0 + sTxt2 = _scoreTxt( + '+' + str(scoreChange), + x, y, True, 0, 0.7, flash=True) + bs.gameTimer(tDelay + delay2, bs.WeakCall( + self._safeAnimate, sTxt2.positionCombine, + 'input1', + {0: yBase + (vOffs + y + 2.0) * scale, + transitionTime: yBase + (vOffs2 + y + 2.0) * + scale})) + bs.gameTimer( + tDelay + delay3, bs.WeakCall( + self._safeAnimate, sTxt2.positionCombine, 'input0', + {0: tsHOffs + x * scale, transitionTime2: tsHOffs + + (x - slideAmt) * scale})) + + def _safeSetAttr(node, attr, value): + if node.exists(): + setattr(node, attr, value) + + bs.gameTimer(tDelay+delay1, bs.Call( + _safeSetAttr, sTxt.node, 'color', (1, 1, 1, 1))) + for j in range(scoreChange): + bs.gameTimer(tDelay+delay1+150*j, bs.Call( + _safeSetAttr, sTxt.node, 'text', + str(player.getTeam().sessionData['previousScore']+j+1))) + t = tDelay+delay1+150*j + if not t in soundTimes: + soundTimes.add(t) + bs.gameTimer( + t, bs.Call( + bs.playSound, self._scoreDisplaySoundSmall)) + + vOffs -= spacing + + def _safeAnimate(self, node, attr, keys): + if node.exists(): + bsUtils.animate(node, attr, keys) + + +class DrawScoreScreenActivity(TeamsScoreScreenActivity): + + def __init__(self, settings={}): + TeamsScoreScreenActivity.__init__(self, settings=settings) + + def onTransitionIn(self): + TeamsScoreScreenActivity.onTransitionIn(self, music=None) + + def onBegin(self): + bsInternal._setAnalyticsScreen('Draw Score Screen') + TeamsScoreScreenActivity.onBegin(self) + bsUtils.ZoomText(bs.Lstr(resource='drawText'), position=(0, 0), + maxWidth=400, + shiftPosition=(-220, 0), shiftDelay=2000, + flash=False, trail=False, jitter=1.0).autoRetain() + bs.gameTimer(350, bs.Call(bs.playSound, self._scoreDisplaySound)) + + if 'results' in self.settings: + r = self.settings['results'] + else: + r = None + + self.showPlayerScores(results=r) + + +class TeamVictoryScoreScreenActivity(TeamsScoreScreenActivity): + + def __init__(self, settings={}): + TeamsScoreScreenActivity.__init__(self, settings=settings) + + def onBegin(self): + bsInternal._setAnalyticsScreen('Teams Score Screen') + TeamsScoreScreenActivity.onBegin(self) + + # call bsOnGameEnd() for any modules that define it.. + # this is intended as a simple way to upload game scores to a server or + # whatnot TODO... + + height = 130 + activeTeamCount = len(self.teams) + v = (height*activeTeamCount)/2 - height/2 + i = 0 + shiftTime = 2500 + + # usually we say 'Best of 7', but if the language prefers we can say + # 'First to 4' + if bsUtils._getResource('bestOfUseFirstToInstead'): + bestTxt = bs.Lstr( + resource='firstToSeriesText', subs=[ + ('${COUNT}', str(self.getSession()._seriesLength/2+1))]) + else: + bestTxt = bs.Lstr( + resource='bestOfSeriesText', subs=[ + ('${COUNT}', str(self.getSession()._seriesLength))]) + + bsUtils.ZoomText(bestTxt, + position=(0, 175), + shiftPosition=(-250, 175), shiftDelay=2500, + flash=False, trail=False, hAlign='center', + scale=0.25, + color=(0.5, 0.5, 0.5, 1.0), jitter=3.0).autoRetain() + for team in self.teams: + bs.gameTimer(i*150+150, bs.WeakCall( + self._showTeamName, + v-i*height, team, i*200, shiftTime-(i*150+150))) + bs.gameTimer(i*150+500, bs.Call(bs.playSound, + self._scoreDisplaySoundSmall)) + scored = (team is self.settings['winner']) + delay = 200 + if scored: + delay = 1200 + bs.gameTimer(i*150+200, bs.WeakCall( + self._showTeamOldScore, + v-i*height, team, shiftTime-(i*150+200))) + bs.gameTimer(i*150+1500, bs.Call(bs.playSound, + self._scoreDisplaySound)) + + bs.gameTimer(i*150+delay, bs.WeakCall( + self._showTeamScore, + v-i*height, team, scored, i*200+100, shiftTime-(i*150+delay))) + i += 1 + self.showPlayerScores() + + def _showTeamName(self, posV, team, killDelay, shiftDelay): + bsUtils.ZoomText(bs.Lstr(value='${A}:', subs=[('${A}', team.name)]), + # team.name+":", + position=(100, posV), + shiftPosition=(-150, posV), shiftDelay=shiftDelay, + flash=False, trail=False, hAlign='right', maxWidth=300, + color=team.color, jitter=1.0).autoRetain() + + def _showTeamOldScore(self, posV, team, shiftDelay): + bsUtils.ZoomText( + str(team.sessionData['score'] - 1), + position=(150, posV), + maxWidth=100, color=(0.6, 0.6, 0.7), + shiftPosition=(-100, posV), + shiftDelay=shiftDelay, flash=False, trail=False, lifespan=1000, + hAlign='left', jitter=1.0).autoRetain() + + def _showTeamScore(self, posV, team, scored, killDelay, shiftDelay): + bsUtils.ZoomText(str(team.sessionData['score']), position=(150, posV), + maxWidth=100, + color=(1.0, 0.9, 0.5) if scored else (0.6, 0.6, 0.7), + shiftPosition=(-100, posV), shiftDelay=shiftDelay, + flash=scored, trail=scored, + hAlign='left', jitter=1.0, + trailColor=(1, 0.8, 0.0, 0)).autoRetain() + + +class TeamSeriesVictoryScoreScreenActivity(TeamsScoreScreenActivity): + + def __init__(self, settings={}): + TeamsScoreScreenActivity.__init__(self, settings=settings) + self._minViewTime = 15000 + self._isFFA = isinstance(self.getSession(), bs.FreeForAllSession) + self._allowServerRestart = True + + def onTransitionIn(self): + # we dont yet want music and stuff.. + TeamsScoreScreenActivity.onTransitionIn( + self, music=None, showTips=False) + + def onBegin(self): + bsInternal._setAnalyticsScreen( + 'FreeForAll Series Victory Screen' + if self._isFFA else 'Teams Series Victory Screen') + + if bs.getEnvironment()['interfaceType'] == 'large': + s = bs.Lstr(resource='pressAnyKeyButtonPlayAgainText') + else: + s = bs.Lstr(resource='pressAnyButtonPlayAgainText') + + TeamsScoreScreenActivity.onBegin( + self, showUpNext=False, customContinueMessage=s) + + winningTeam = self.settings['winner'] + + # pause a moment before playing victory music + bs.gameTimer(600, bs.WeakCall(self._playVictoryMusic)) + bs.gameTimer(4400, bs.WeakCall( + self._showWinner, self.settings['winner'])) + bs.gameTimer(4400+200, bs.Call(bs.playSound, self._scoreDisplaySound)) + + # make sure to exclude players without teams (this means they're + # still doing selection) + if self._isFFA: + playersSorted = [ + [p.getTeam().sessionData['score'], + p.getName(full=True), + p] for name, p in self.scoreSet.getValidPlayers().items() + if p.getTeam() is not None] + playersSorted.sort(reverse=True) + else: + playersSorted = [ + [p.score, p.nameFull, p] + for name, p in self.scoreSet.getValidPlayers().items()] + playersSorted.sort(reverse=True) + + tsHeight = 300 + tsHOffs = -390 + t = 6400 + tIncr = 120 + + if self._isFFA: + txt = bs.Lstr(value='${A}:', + subs=[('${A}', bs.Lstr( + resource='firstToFinalText', subs=[ + ('${COUNT}', + str(self.getSession()._ffaSeriesLength))]))]) + else: + # some languages may prefer to always show 'first to X' instead of + # 'best of X' FIXME - this will affect all clients connected to + # us even if they're not using a language with this enabled.. not + # the end of the world but something to be aware of. + if bsUtils._getResource('bestOfUseFirstToInstead'): + txt = bs.Lstr(value='${A}:', subs=[ + ('${A}', bs.Lstr(resource='firstToFinalText', subs=[ + ('${COUNT}', + str(self.getSession()._seriesLength/2+1))]))]) + else: + txt = bs.Lstr(value='${A}:', subs=[ + ('${A}', bs.Lstr(resource='bestOfFinalText', subs=[ + ('${COUNT}', str(self.getSession()._seriesLength))]))]) + + bsUtils.Text(txt, + vAlign='center', + maxWidth=300, + color=(0.5, 0.5, 0.5, 1.0), position=(0, 220), + scale=1.2, transition='inTopSlow', hAlign='center', + transitionDelay=tIncr*4).autoRetain() + + winScore = (self.getSession()._seriesLength-1)/2+1 + loseScore = 0 + for team in self.teams: + if team.sessionData['score'] != winScore: + loseScore = team.sessionData['score'] + + if not self._isFFA: + bsUtils.Text( + bs.Lstr( + resource='gamesToText', + subs=[('${WINCOUNT}', str(winScore)), + ('${LOSECOUNT}', str(loseScore))]), + color=(0.5, 0.5, 0.5, 1.0), + maxWidth=160, vAlign='center', position=(0, -215), + scale=1.8, transition='inLeft', hAlign='center', + transitionDelay=4800 + tIncr * 4).autoRetain() + + if self._isFFA: + vExtra = 120 + else: + vExtra = 0 + + # show game MVP + if not self._isFFA: + mvp, mvpName = None, None + for p in playersSorted: + if p[2].getTeam() == winningTeam: + mvp = p[2] + mvpName = p[1] + break + if mvp is not None: + bsUtils.Text(bs.Lstr(resource='mostValuablePlayerText'), + color=(0.5, 0.5, 0.5, 1.0), + vAlign='center', + maxWidth=300, + position=(180, tsHeight/2+15), transition='inLeft', + hAlign='left', transitionDelay=t).autoRetain() + t += 4*tIncr + + bsUtils.Image( + mvp.getIcon(), + position=(230, tsHeight / 2 - 55 + 14 - 5), + scale=(70, 70), + transition='inLeft', transitionDelay=t).autoRetain() + bsUtils.Text( + bs.Lstr(value=mvpName), + position=(280, tsHeight / 2 - 55 + 15 - 5), + hAlign='left', vAlign='center', maxWidth=170, scale=1.3, + color=bs.getSafeColor(mvp.getTeam().color + (1,)), + transition='inLeft', transitionDelay=t).autoRetain() + t += 4*tIncr + + # most violent + mostKills = 0 + for p in playersSorted: + if p[2].killCount >= mostKills: + mvp = p[2] + mvpName = p[1] + mostKills = p[2].killCount + if mvp is not None: + bsUtils.Text(bs.Lstr(resource='mostViolentPlayerText'), + color=(0.5, 0.5, 0.5, 1.0), + vAlign='center', + maxWidth=300, + position=(180, tsHeight/2-150+vExtra+15), + transition='inLeft', + hAlign='left', transitionDelay=t).autoRetain() + bsUtils.Text( + bs.Lstr( + value='(${A})', + subs=[('${A}', bs.Lstr( + resource='killsTallyText', + subs=[('${COUNT}', str(mostKills))]))]), + position=(260, tsHeight / 2 - 150 - 15 + vExtra), + color=(0.3, 0.3, 0.3, 1.0), + scale=0.6, hAlign='left', transition='inLeft', + transitionDelay=t).autoRetain() + t += 4*tIncr + + bsUtils.Image( + mvp.getIcon(), + position=(233, tsHeight / 2 - 150 - 30 - 46 + 25 + vExtra), + scale=(50, 50), + transition='inLeft', transitionDelay=t).autoRetain() + bsUtils.Text( + bs.Lstr(value=mvpName), + position=(270, tsHeight / 2 - 150 - 30 - 36 + vExtra + 15), + hAlign='left', vAlign='center', maxWidth=180, + color=bs.getSafeColor(mvp.getTeam().color + (1,)), + transition='inLeft', transitionDelay=t).autoRetain() + t += 4*tIncr + + # most killed + mostKilled = 0 + mkp, mkpName = None, None + for p in playersSorted: + if p[2].killedCount >= mostKilled: + mkp = p[2] + mkpName = p[1] + mostKilled = p[2].killedCount + if mkp is not None: + bsUtils.Text(bs.Lstr(resource='mostViolatedPlayerText'), + color=(0.5, 0.5, 0.5, 1.0), + vAlign='center', + maxWidth=300, + position=(180, tsHeight/2-300+vExtra+15), + transition='inLeft', + hAlign='left', transitionDelay=t).autoRetain() + bsUtils.Text( + bs.Lstr( + value='(${A})', + subs=[('${A}', bs.Lstr( + resource='deathsTallyText', + subs=[('${COUNT}', str(mostKilled))]))]), + position=(260, tsHeight / 2 - 300 - 15 + vExtra), + hAlign='left', scale=0.6, color=(0.3, 0.3, 0.3, 1.0), + transition='inLeft', transitionDelay=t).autoRetain() + t += 4*tIncr + bsUtils.Image( + mkp.getIcon(), + position=(233, tsHeight / 2 - 300 - 30 - 46 + 25 + vExtra), + scale=(50, 50), + transition='inLeft', transitionDelay=t).autoRetain() + bsUtils.Text(bs.Lstr(value=mkpName), + position=(270, tsHeight/2-300-30-36+vExtra+15), + hAlign='left', vAlign='center', + color=bs.getSafeColor(mkp.getTeam().color+(1,)), + maxWidth=180, transition='inLeft', + transitionDelay=t).autoRetain() + t += 4*tIncr + + # now show individual scores + tDelay = t + bsUtils.Text(bs.Lstr(resource='finalScoresText'), + color=(0.5, 0.5, 0.5, 1.0), + position=(tsHOffs, tsHeight/2), + transition='inRight', + transitionDelay=tDelay).autoRetain() + tDelay += 4*tIncr + + vOffs = 0.0 + tDelay += len(playersSorted) * 8*tIncr + for score, name, p in playersSorted: + tDelay -= 4*tIncr + vOffs -= 40 + bsUtils.Text(str(p.getTeam().sessionData['score']) + if self._isFFA else str(p.score), + color=(0.5, 0.5, 0.5, 1.0), + position=(tsHOffs + 230, tsHeight / 2 + vOffs), + hAlign='right', transition='inRight', + transitionDelay=tDelay).autoRetain() + tDelay -= 4*tIncr + + bsUtils.Image( + p.getIcon(), + position=(tsHOffs - 72, tsHeight / 2 + vOffs + 15), + scale=(30, 30), + transition='inLeft', transitionDelay=tDelay).autoRetain() + bsUtils.Text( + bs.Lstr(value=name), + position=(tsHOffs - 50, tsHeight / 2 + vOffs + 15), + hAlign='left', vAlign='center', maxWidth=180, + color=bs.getSafeColor(p.getTeam().color + (1,)), + transition='inRight', transitionDelay=tDelay).autoRetain() + + bs.gameTimer(15000, bs.WeakCall(self._showTips)) + + def _showTips(self): + self._tipsText = bsUtils.TipsText(offsY=70) + + def _playVictoryMusic(self): + # make sure we dont stomp on the next activity's music choice + if not self.isTransitioningOut(): + bs.playMusic('Victory') + + def _showWinner(self, team): + if not self._isFFA: + offsV = 0 + bsUtils.ZoomText(team.name, position=(0, 97), + color=team.color, scale=1.15, jitter=1.0, + maxWidth=250).autoRetain() + else: + offsV = -80 + if len(team.players) == 1: + i = bsUtils.Image( + team.players[0].getIcon(), + position=(0, 143), + scale=(100, 100)).autoRetain() + bsUtils.animate(i.node, 'opacity', {0: 0.0, 250: 1.0}) + bsUtils.ZoomText(bs.Lstr( + value=team.players[0].getName( + full=True, icon=False)), + position=(0, 97 + offsV), + color=team.color, scale=1.15, jitter=1.0, + maxWidth=250).autoRetain() + + sExtra = 1.0 if self._isFFA else 1.0 + + # some languages say "FOO WINS" differently for teams vs players + if isinstance(self.getSession(), bs.FreeForAllSession): + winsResource = 'seriesWinLine1PlayerText' + else: + winsResource = 'seriesWinLine1TeamText' + winsText = bs.Lstr(resource=winsResource) + # temp - if these come up as the english default, fall-back to the + # unified old form which is more likely to be translated + + bsUtils.ZoomText(winsText, position=(0, -10 + offsV), + color=team.color, scale=0.65 * sExtra, jitter=1.0, + maxWidth=250).autoRetain() + bsUtils.ZoomText(bs.Lstr(resource='seriesWinLine2Text'), + position=(0, -110 + offsV), + scale=1.0 * sExtra, color=team.color, jitter=1.0, + maxWidth=250).autoRetain() + + +class TeamJoiningActivity(bsGame.JoiningActivity): + def __init__(self, settings={}): + bsGame.JoiningActivity.__init__(self, settings) + + def onTransitionIn(self): + bsGame.JoiningActivity.onTransitionIn(self) + + bsUtils.ControlsHelpOverlay(delay=1000).autoRetain() + + # show info about the next up game + self._nextUpText = bsUtils.Text( + bs.Lstr( + value='${1} ${2}', + subs=[('${1}', bs.Lstr(resource='upFirstText')), + ('${2}', self.getSession().getNextGameDescription())]), + hAttach='center', scale=0.7, vAttach='top', hAlign='center', + position=(0, -70), + flash=False, color=(0.5, 0.5, 0.5, 1.0), + transition='fadeIn', transitionDelay=5000) + + # in teams mode, show our two team names + # (technically should have Lobby handle this, but this works for now) + if isinstance(bs.getSession(), bs.TeamsSession): + teamNames = [team.name for team in bs.getSession().teams] + teamColors = [tuple(team.color) + (0.5,) + for team in bs.getSession().teams] + if len(teamNames) == 2: + for i in range(2): + teamColor = (1, 0, 0)+(1,) + bsUtils.Text(teamNames[i], + scale=0.7, + hAttach='center', + vAttach='top', + hAlign='center', + position=(-200+350*i, -100), + color=teamColors[i], + transition='fadeIn').autoRetain() + + bsUtils.Text(bs.Lstr( + resource='mustInviteFriendsText', + subs=[('${GATHER}', bs.Lstr( + resource='gatherWindow.titleText'))]), + hAttach='center', scale=0.8, hostOnly=True, + vAttach='center', hAlign='center', position=(0, 0), + flash=False, color=(0, 1, 0, 1.0), + transition='fadeIn', transitionDelay=2000, + transitionOutDelay=7000).autoRetain() + + +class TeamGameActivity(bs.GameActivity): + """ + category: Game Flow Classes + + Base class for teams and free-for-all mode games. + (Free-for-all is essentially just a special case where every + bs.Player has their own bs.Team) + """ + + @classmethod + def supportsSessionType(cls, sessionType): + """ + Class method override; + returns True for bs.TeamsSessions and bs.FreeForAllSessions; + False otherwise. + """ + return True if( + issubclass(sessionType, bs.TeamsSession) + or issubclass(sessionType, bs.FreeForAllSession)) else False + + def __init__(self, settings={}): + bs.GameActivity.__init__(self, settings) + # by default we don't show kill-points in free-for-all + # (there's usually some activity-specific score and we dont + # wanna confuse things) + if isinstance(bs.getSession(), bs.FreeForAllSession): + self._showKillPoints = False + + def onTransitionIn(self, music=None): + bs.GameActivity.onTransitionIn(self, music) + + # on the first game, show the controls UI momentarily + # (unless we're being run in co-op mode, in which case we leave + # it up to them) + if not isinstance(self.getSession(), bs.CoopSession): + if not self.getSession()._haveShownControlsHelpOverlay: + delay = 4000 + lifespan = 10000 + if self._isSlowMotion: + lifespan = int(lifespan*0.3) + bsUtils.ControlsHelpOverlay( + delay=delay, lifespan=lifespan, scale=0.8, + position=(380, 200), + bright=True).autoRetain() + self.getSession()._haveShownControlsHelpOverlay = True + + def onBegin(self): + bs.GameActivity.onBegin(self) + + # a few achievements... + try: + # award a few achievements.. + if issubclass(type(self.getSession()), bs.FreeForAllSession): + if len(self.players) >= 2: + import bsAchievement + bsAchievement._awardLocalAchievement('Free Loader') + elif issubclass(type(self.getSession()), bs.TeamsSession): + if len(self.players) >= 4: + import bsAchievement + bsAchievement._awardLocalAchievement('Team Player') + except Exception: + bs.printException() + + def spawnPlayerSpaz(self, player, position=None, angle=None): + """ + Method override; spawns and wires up a standard bs.PlayerSpaz for + a bs.Player. + + If position or angle is not supplied, a default will be chosen based + on the bs.Player and their bs.Team. + """ + if position is None: + # in teams-mode get our team-start-location + if isinstance(self.getSession(), bs.TeamsSession): + position = \ + self.getMap().getStartPosition(player.getTeam().getID()) + else: + # otherwise do free-for-all spawn locations + position = self.getMap().getFFAStartPosition(self.players) + + return bs.GameActivity.spawnPlayerSpaz(self, player, position, angle) + + def end( + self, results=None, announceWinningTeam=True, announceDelay=100, + force=False): + """ + Method override; announces the single winning team + unless 'announceWinningTeam' is False. (useful for games where + there is not a single most-important winner). + """ + # announce win (but only for the first finish() call) + # (also don't announce in co-op sessions; we leave that up to them) + if not isinstance(self.getSession(), bs.CoopSession): + doAnnounce = not self.hasEnded() + bs.GameActivity.end( + self, results, delay=2000+announceDelay, force=force) + # need to do this *after* end end call so that results is valid + if doAnnounce: + self.getSession().announceGameResults( + self, results, delay=announceDelay, + announceWinningTeam=announceWinningTeam) + + # for co-op we just pass this up the chain with a delay added + # (in most cases) + # (team games expect a delay for the announce portion in teams/ffa + # mode so this keeps it consistent) + else: + # dont want delay on restarts.. + if (type(results) is dict + and 'outcome' in results + and results['outcome'] == 'restart'): + delay = 0 + else: + delay = 2000 + bs.gameTimer(100, bs.Call( + bs.playSound, bs.getSound("boxingBell"))) + bs.GameActivity.end(self, results, delay=delay, force=force) + + +class TeamBaseSession(bs.Session): + """ + category: Game Flow Classes + + Common base class for bs.TeamsSession and bs.FreeForAllSession. + Free-for-all-mode is essentially just teams-mode with each bs.Player having + their own bs.Team, so there is much overlap in functionality. + """ + + def __init__(self): + """ + Sets up playlists and launches a bs.Activity to accept joiners. + """ + + mp = self.getMaxPlayers() + bsConfig = bs.getConfig() + + if self._useTeams: + teamNames = bsConfig.get('Custom Team Names', gDefaultTeamNames) + teamColors = bsConfig.get('Custom Team Colors', gDefaultTeamColors) + else: + teamNames = None + teamColors = None + + bs.Session.__init__(self, + teamNames=teamNames, teamColors=teamColors, + useTeamColors=self._useTeams, + minPlayers=1, maxPlayers=mp) + + self._seriesLength = gTeamSeriesLength + self._ffaSeriesLength = gFFASeriesLength + + showTutorial = bsConfig.get('Show Tutorial', True) + + if showTutorial: + # get this loading.. + self._tutorialActivityInstance = bs.newActivity( + bsTutorial.TutorialActivity) + else: + self._tutorialActivityInstance = None + + # TeamGameActivity uses this to display a help overlay on + # the first activity only + self._haveShownControlsHelpOverlay = False + + try: + self._playlistName = bs.getConfig()[self._playlistSelectionVar] + except Exception: + self._playlistName = '__default__' + try: + self._playlistRandomize = bs.getConfig()[self._playlistRandomizeVar] + except Exception: + self._playlistRandomize = False + + # which game activity we're on + self._gameNumber = 0 + + try: + playlists = bs.getConfig()[self._playlistsVar] + except Exception: + playlists = {} + + if (self._playlistName != '__default__' + and self._playlistName in playlists): + # (make sure to copy this, as we muck with it in place once we've + # got it and we dont want that to affect our config) + playlist = copy.deepcopy(playlists[self._playlistName]) + else: + if self._useTeams: + playlist = bsUtils._getDefaultTeamsPlaylist() + else: + playlist = bsUtils._getDefaultFreeForAllPlaylist() + + # resolve types and whatnot to get our final playlist + playlistResolved = bsUtils._filterPlaylist( + playlist, sessionType=type(self), + addResolvedType=True) + + if not playlistResolved: + raise Exception("playlist contains no valid games") + + self._playlist = ShuffleList( + playlistResolved, shuffle=self._playlistRandomize) + + # get a game on deck ready to go + self._currentGameSpec = None + self._nextGameSpec = self._playlist.pullNext() + self._nextGame = self._nextGameSpec['resolvedType'] + + # go ahead and instantiate the next game we'll + # use so it has lots of time to load + self._instantiateNextGame() + + # start in our custom join screen + self.setActivity(bs.newActivity(TeamJoiningActivity)) + + def getNextGameDescription(self): + 'Returns a description of the next game on deck' + return self._nextGameSpec['resolvedType'].getConfigDisplayString( + self._nextGameSpec) + + def getGameNumber(self): + 'Returns which game in the series is currently being played.' + return self._gameNumber + + def onTeamJoin(self, team): + team.sessionData['previousScore'] = team.sessionData['score'] = 0 + + def getMaxPlayers(self): + """ + Return the max number of bs.Players allowed to join the game at once. + """ + if self._useTeams: + try: + return bs.getConfig()['Team Game Max Players'] + except Exception: + return 8 + else: + try: + return bs.getConfig()['Free-for-All Max Players'] + except Exception: + return 8 + + def _instantiateNextGame(self): + self._nextGameInstance = bs.newActivity( + self._nextGameSpec['resolvedType'], + self._nextGameSpec['settings']) + + def onPlayerRequest(self, player): + return bs.Session.onPlayerRequest(self, player) + + def onActivityEnd(self, activity, results): + + # if we have a tutorial to show, + # thats the first thing we do no matter what + if self._tutorialActivityInstance is not None: + self.setActivity(self._tutorialActivityInstance) + self._tutorialActivityInstance = None + + # if we're leaving the tutorial activity, + # pop a transition activity to transition + # us into a round gracefully (otherwise we'd + # snap from one terrain to another instantly) + elif isinstance(activity, bsTutorial.TutorialActivity): + self.setActivity(bs.newActivity(bsGame.TransitionActivity)) + + # if we're in a between-round activity or a restart-activity, + # hop into a round + elif (isinstance(activity, bsGame.JoiningActivity) + or isinstance(activity, bsGame.TransitionActivity) + or isinstance(activity, bsGame.ScoreScreenActivity)): + + # if we're coming from a series-end activity, reset scores + if isinstance(activity, TeamSeriesVictoryScoreScreenActivity): + self.scoreSet.reset() + self._gameNumber = 0 + for team in self.teams: + team.sessionData['score'] = 0 + # otherwise just set accum (per-game) scores + else: + self.scoreSet.resetAccum() + + nextGame = self._nextGameInstance + + self._currentGameSpec = self._nextGameSpec + self._nextGameSpec = self._playlist.pullNext() + self._gameNumber += 1 + + # instantiate the next now so they have plenty of time to load + self._instantiateNextGame() + + # (re)register all players and wire the score-set to our next + # activity + for p in self.players: + # ..but only ones who have completed joining + if p.getTeam() is not None: + self.scoreSet.registerPlayer(p) + self.scoreSet.setActivity(nextGame) + + # now flip the current activity + self.setActivity(nextGame) + + # if we're leaving a round, go to the score screen + else: + # teams mode + if self._useTeams: + winners = results._getWinners() + # if everyone has the same score, call it a draw + if len(winners) < 2: + self.setActivity(bs.newActivity(DrawScoreScreenActivity)) + else: + winner = winners[0]['teams'][0] + winner.sessionData['score'] += 1 + # if a team has won, show final victory screen... + if winner.sessionData['score'] >= ( + self._seriesLength - 1) / 2 + 1: + self.setActivity( + bs.newActivity( + TeamSeriesVictoryScoreScreenActivity, + {'winner': winner})) + else: + self.setActivity( + bs.newActivity( + TeamVictoryScoreScreenActivity, + {'winner': winner})) + # free-for-all mode + else: + winners = results._getWinners() + + # if there's multiple players and everyone has the same score, + # call it a draw + if len(self.players) > 1 and len(winners) < 2: + self.setActivity(bs.newActivity( + DrawScoreScreenActivity, + {'results': results})) + else: + # award different point amounts based on number of players + pointAwards = self._getFFAPointAwards() + + for i, winner in enumerate(winners): + for team in winner['teams']: + points = pointAwards[i] if i in pointAwards else 0 + team.sessionData['previousScore'] = \ + team.sessionData['score'] + team.sessionData['score'] += points + + seriesWinners = [team for team in self.teams + if team.sessionData['score'] + >= self._ffaSeriesLength] + seriesWinners.sort( + reverse=True, key=lambda + team: (team.sessionData['score'])) + if len(seriesWinners) == 1 or ( + len(seriesWinners) > 1 + and seriesWinners[0].sessionData['score'] != + seriesWinners[1].sessionData['score']): + self.setActivity( + bs.newActivity( + TeamSeriesVictoryScoreScreenActivity, + {'winner': seriesWinners[0]})) + else: + self.setActivity( + bs.newActivity( + FreeForAllVictoryScoreScreenActivity, + {'results': results})) + + def announceGameResults(self, activity, results, delay, + announceWinningTeam=True): + """ + Show game results at the end of a game + (before transitioning to a score screen). + This will include a zoom-text of 'BLUE WINS' + or whatnot, along with a possible audio + announcement of the same. + """ + + bs.gameTimer(delay, bs.Call(bs.playSound, bs.getSound("boxingBell"))) + if announceWinningTeam: + winningTeam = results._getWinningTeam() + if winningTeam is not None: + # have all players celebrate + for player in winningTeam.players: + try: + player.actor.node.handleMessage('celebrate', 10000) + except Exception: + pass + activity.cameraFlash() + + # some languages say "FOO WINS" different for teams vs players + if isinstance(self, bs.FreeForAllSession): + winsResource = 'winsPlayerText' + else: + winsResource = 'winsTeamText' + winsText = bs.Lstr(resource=winsResource, subs=[ + ('${NAME}', winningTeam.name)]) + activity.showZoomMessage( + winsText, scale=0.85, color=bsUtils.getNormalizedColor( + winningTeam.color)) + + +class FreeForAllSession(TeamBaseSession): + """ + category: Game Flow Classes + + bs.Session type for free-for-all mode games. + """ + _useTeams = False + _playlistSelectionVar = 'Free-for-All Playlist Selection' + _playlistRandomizeVar = 'Free-for-All Playlist Randomize' + _playlistsVar = 'Free-for-All Playlists' + + def _getFFAPointAwards(self): + """ + Returns the number of points awarded for different + rankings based on the current number of players. + """ + if len(self.players) == 1: + pointAwards = {} + elif len(self.players) == 2: + pointAwards = {0: 6} + elif len(self.players) == 3: + pointAwards = {0: 6, 1: 3} + elif len(self.players) == 4: + pointAwards = {0: 8, 1: 4, 2: 2} + elif len(self.players) == 5: + pointAwards = {0: 8, 1: 4, 2: 2} + elif len(self.players) == 6: + pointAwards = {0: 8, 1: 4, 2: 2} + else: + pointAwards = {0: 8, 1: 4, 2: 2, 3: 1} + return pointAwards + + def __init__(self): + bsInternal._incrementAnalyticsCount('Free-for-all session start') + TeamBaseSession.__init__(self) + + +class TeamsSession(TeamBaseSession): + """ + category: Game Flow Classes + + bs.Session type for teams mode games. + """ + _useTeams = True + _playlistSelectionVar = 'Team Tournament Playlist Selection' + _playlistRandomizeVar = 'Team Tournament Playlist Randomize' + _playlistsVar = 'Team Tournament Playlists' + + def __init__(self): + bsInternal._incrementAnalyticsCount('Teams session start') + TeamBaseSession.__init__(self) -- GitLab