ToonTownRewritten/toontown/election/DistributedSafezoneInvasionAI.py

440 lines
18 KiB
Python

import random
from direct.directnotify import DirectNotifyGlobal
from direct.distributed.DistributedObjectAI import DistributedObjectAI
from direct.fsm.FSM import FSM
from otp.ai.MagicWordGlobal import *
from DistributedInvasionSuitAI import DistributedInvasionSuitAI
from InvasionMasterAI import InvasionMasterAI
import SafezoneInvasionGlobals
import DistributedElectionEventAI
from toontown.suit import SuitTimings
from toontown.toonbase import ToontownBattleGlobals
class DistributedSafezoneInvasionAI(DistributedObjectAI, FSM):
notify = DirectNotifyGlobal.directNotify.newCategory("DistributedSafezoneInvasionAI")
def __init__(self, air, election):
DistributedObjectAI.__init__(self, air)
FSM.__init__(self, 'InvasionFSM')
self.master = InvasionMasterAI(self)
self.election = election
self.waveNumber = 0
self.spawnPoints = []
self.suits = []
self.toons = []
self.sadToons = []
self.lastWave = (self.waveNumber == len(SafezoneInvasionGlobals.SuitWaves) - 1)
self.invasionOn = False
self.numberOfSuits = 0
def announceGenerate(self):
self.b_setInvasionStarted(True)
self.demand('BeginWave', 0)
# Kill all the butterflies in Toontown Central.
#for butterfly in self.air.hoods[0].butterflies:
#butterfly.requestDelete()
# Start up the "which Toons are in the area" tracking.
for toon in self.air.doId2do.values():
if toon.zoneId != self.zoneId:
continue # Object isn't here.
if toon.dclass != self.air.dclassesByName['DistributedToonAI']:
continue # Object isn't a Toon.
self._handleToonEnter(toon)
self.accept('toon-entered-%s' % self.zoneId, self._handleToonEnter)
self.accept('toon-left-%s' % self.zoneId, self._handleToonExit)
def b_setInvasionStarted(self, started):
self.setInvasionStarted(started)
self.d_setInvasionStarted(started)
def setInvasionStarted(self, started):
self.invasionOn = started
def d_setInvasionStarted(self, started):
self.sendUpdate('setInvasionStarted', [started])
def getInvasionStarted(self):
return self.invasionOn
def delete(self):
DistributedObjectAI.delete(self)
self.demand('Off')
self.ignoreAll()
'''
INVASION-RELATED
There are 4 types of Invasion waves: Normal, Wait, Intermission, and Finale.
- Normal Waves simply spawn cogs and move on to the next wave as soon as the cogs have landed.
- Wait Waives will wait until all cogs in the current wave are destroyed before moving to the next.
- Intermission Waves wait a certain amount of time defined in InvasionGlobals before spawning the next wave. These are used to separate the Levels of cogs.
- The Finale Wave is only used once as soon as all other waves are done. It spawns our boss, which ends the invasion upon death.
Although each set of cogs is called a "wave", this is only to prevent too many cogs from spawning.
To the players, each wave visually ends as soon as an Intermission is called.
'''
def enterBeginWave(self, waveNumber):
# In this state, Cogs rain down from the heavens. We call spawnOne at
# regular intervals until the quota for the wave is met.
self.waveNumber = waveNumber
if self.waveNumber == 12:
self.election.saySurleePhrase('Brace yourselves for impact. They\'re sending in the Movers and Shakers!', 1, True)
elif self.waveNumber == 24:
self.election.saySurleePhrase('Oh no. Nonono. We\'re destroying the Cogs faster than they can be built. Skelecogs inbound!', 1, True)
# Reset spawnpoints:
self.spawnPoints = range(len(SafezoneInvasionGlobals.SuitSpawnPoints))
# Get the suits to call:
suitsToCall = SafezoneInvasionGlobals.SuitWaves[self.waveNumber]
self.numberOfSuits = len(suitsToCall)
# How long do we have to spread out the suit calldowns?
# In case some dummkopf set WaveBeginningTime too low:
delay = max(SafezoneInvasionGlobals.WaveBeginningTime, SuitTimings.fromSky)
spread = delay - SuitTimings.fromSky
spreadPerSuit = spread/len(suitsToCall)
self._waveBeginTasks = []
for i, (suit, level) in enumerate(suitsToCall):
self._waveBeginTasks.append(
taskMgr.doMethodLater(i*spreadPerSuit, self.spawnOne,
self.uniqueName('summon-suit-%s' % i),
extraArgs=[suit, level]))
# Plus a task to switch to the 'Wave' state:
self._waveBeginTasks.append(
taskMgr.doMethodLater(delay, self.demand,
self.uniqueName('begin-wave'),
extraArgs=['Wave']))
def exitBeginWave(self):
for task in self._waveBeginTasks:
task.remove()
# Reset the spawn points for the continuous cogs
self.spawnPoints = range(len(SafezoneInvasionGlobals.SuitSpawnPoints))
def enterWave(self):
# This state is entered after all Cogs have been spawned by BeginWave.
# If the next wave isn't an Intermission or Wait Wave, it will spawn the next one.
for suit in self.suits:
suit.start()
# If the wave isn't an Intermission or Wait Wave, send another set of suits out.
# Otherwise, wait until all suits are dead and let waveWon() decide what to do.
if self.lastWave:
return
else:
if self.waveNumber not in SafezoneInvasionGlobals.SuitIntermissionWaves and self.waveNumber not in SafezoneInvasionGlobals.SuitWaitWaves:
self.spawnPoints = range(len(SafezoneInvasionGlobals.SuitSpawnPoints))
self.demand('BeginWave', self.waveNumber + 1)
self.lastWave = (self.waveNumber == len(SafezoneInvasionGlobals.SuitWaves) - 1)
# The first suit on the scene also says a faceoff taunt:
if self.suits:
self.suits[0].d_sayFaceoffTaunt()
def exitWave(self):
# Clean up any loose suits, in case the wave is being ended by MW.
if self.waveNumber in SafezoneInvasionGlobals.SuitIntermissionWaves or self.waveNumber in SafezoneInvasionGlobals.SuitWaitWaves:
self.__deleteSuits()
def waveWon(self):
if self.state != 'Wave':
return
# Was this the last wave?
if self.lastWave:
self.demand('Finale')
# Should we spawn this one immidiately?
elif self.waveNumber in SafezoneInvasionGlobals.SuitWaitWaves:
self.demand('BeginWave', self.waveNumber + 1)
self.lastWave = (self.waveNumber == len(SafezoneInvasionGlobals.SuitWaves) - 1)
# No, we'll give a short intermission.
else:
self.demand('Intermission')
def enterIntermission(self):
# This will tell us when to start the next wave
self._delay = taskMgr.doMethodLater(SafezoneInvasionGlobals.IntermissionTime,
self.__endIntermission,
self.uniqueName('intermission'))
# This state is entered after a wave is successfully over. There's a
# pause in the action until the next wave.
# Surlee keeps everyone's spirits held high during the Intermission.
if self.waveNumber == 2:
self.election.saySurleePhrase('You got them, but that\'s only the first wave. We have a short time to regroup before they come back.', 1, True)
elif self.waveNumber == 5:
self.election.saySurleePhrase('Another wave down, very nice. Get ready, more are on the way!', 1, True)
elif self.waveNumber == 8:
self.election.saySurleePhrase('They\'re getting stronger with each wave... This isn\'t good. No, not good at all.', 1, True)
elif self.waveNumber == 11:
self.election.saySurleePhrase('Stay happy, toons! We can do this! There can\'t be too many more...', 1, True)
elif self.waveNumber == 14:
self.election.saySurleePhrase('We\'re losing toons fast, but our motivation is still high. Don\'t let these metal menaces take over our town!', 1, True)
elif self.waveNumber == 17:
self.election.saySurleePhrase('One more down. I\'ve been keeping track of the wave intervals - we seem to have about 20 seconds between each fight. Hang on tight.', 1, True)
elif self.waveNumber == 20:
self.election.saySurleePhrase('These next ones are the hardest yet. Flippy, do you have any bigger pies? We\'re going to need a whole lot more as fast as possible.', 1, True)
elif self.waveNumber == 23:
self.election.saySurleePhrase('Did... Did we get them? That had to have been the last wave - their field levels don\'t go any higher. Why are the skies growing darker?', 1, True)
elif self.waveNumber == 26:
self.election.saySurleePhrase('Absolutely outstanding! I can\'t believe we made it! Brace yourselves, we have one more baddy to go...', 1, True)
def __endIntermission(self, task):
self.demand('BeginWave', self.waveNumber + 1)
self.lastWave = (self.waveNumber == len(SafezoneInvasionGlobals.SuitWaves) - 1)
def exitIntermission(self):
self._delay.remove()
def enterFinale(self):
self._delay = taskMgr.doMethodLater(20, self.spawnFinaleSuit, self.uniqueName('summon-finale-suit'))
def spawnFinaleSuit(self, task):
self.election.saySurleePhrase('This is it, toons. They\'re sending in the boss! Brace yourselves, this will be the toughest one yet!', 1, True)
suit = DistributedInvasionSuitAI(self.air, self)
suit.dna.newSuit('ls')
suit.setSpawnPoint(100) # Point 100 just tells announceGenerate that this is our boss
suit.setLevel(4) # Give it the highest level we can. Requires 200 damage for a level 12, 156 for a level 11
suit.generateWithRequired(self.zoneId)
suit.d_makeSkelecog()
suit.b_setState('FlyDown')
self.suits.append(suit)
def setFinaleSuitStunned(self, hp, kill = False):
if self.state == 'Finale' and kill:
self.sendUpdate('stopMusic')
for suit in self.suits:
hp = min(hp, suit.currHP) # Don't take more damage than we have...
suit.b_setHP(suit.currHP - hp)
suit.b_setState('Stunned')
elif self.state == 'Finale':
for suit in self.suits:
suit.takeDamage(hp)
def winFinale(self):
if self.state == 'Finale':
for suit in self.suits:
suit.b_setState('Explode')
def exitFinale(self):
self._delay.remove()
def enterVictory(self):
self.b_setInvasionStarted(False)
for toon in self.toons:
toon.toonUp(toon.getMaxHp())
for toon in self.sadToons:
toon.toonUp(toon.getMaxHp())
taskMgr.doMethodLater(65, self.wrapUp, self.uniqueName('WrapUp-Later'))
def wrapUp(self, task):
self.election.b_setState('WrapUp')
def enterOff(self):
self.__deleteSuits()
'''
TOON-RELATED
Toons, obviously, are our players. Most of them have 15-20 laff, so healing each other with pies will be a must.
Each pie gives toons +1 health, however it takes 3 pie hits to restore a toon after they went sad. Cogs won't attack sad toons.
'''
def getToon(self, toonId):
for toon in self.toons:
if toon.doId == toonId:
return toon
return None
def _handleToonEnter(self, toon):
if toon not in self.toons:
self.toons.append(toon)
self.acceptOnce(self.air.getAvatarExitEvent(toon.doId),
self._handleToonExit,
extraArgs=[toon])
# Don't want those cogs attacking us if we are sad.
# Thats just mean
self.checkToonHp()
toon.b_setHealthDisplay(2)
def _handleToonExit(self, toon):
if toon in self.toons:
self.toons.remove(toon)
self.ignore(self.air.getAvatarExitEvent(toon.doId))
if toon in self.sadToons:
self.sadToons.remove(toon)
self.ignore(self.air.getAvatarExitEvent(toon.doId))
toon.b_setHealthDisplay(0)
def takeDamage(self, damage):
# One of our cogs successfully hit a toon. Time to drain their laff.
avId = self.air.getAvatarIdFromSender()
toon = self.air.doId2do.get(avId)
if not toon:
self.air.writeServerEvent('suspicious', avId=avId, issue='Nonexistent Toon tried to get hit!')
return
# If the cog's attack is higher than the amount of laff they have, we'll only take away what they have.
# If the attack is 5 and the toon has 3 laff, we'll only take away 3 laff. This mostly prevents toons going under 0 Laff.
toonHp = toon.getHp()
if damage > toonHp and toonHp > 0:
toon.takeDamage(toonHp)
else:
toon.takeDamage(damage)
self.checkToonHp()
def pieHitToon(self, doId):
# Someone hit a toon with a pie!
avId = self.air.getAvatarIdFromSender()
toon = self.air.doId2do.get(doId)
if not toon:
self.air.writeServerEvent('suspicious', avId=avId, issue='Hit a nonexistent Toon with a pie!')
return
from toontown.toon.DistributedToonAI import DistributedToonAI
if not isinstance(toon, DistributedToonAI):
self.air.writeServerEvent('suspicious', avId=avId, issue='Hit a non-Toon with a pie through healToon()!')
return
# Just to be safe, let's check if the Toon has less than 0 laff.
# Sometimes this happens from multiple cog hits at once.
if toon.getHp() == -1:
# They do! :( Let's give them a little boost before tooning up to make it fair.
toon.setHp(0)
toon.toonUp(SafezoneInvasionGlobals.ToonHealAmount)
self.checkToonHp()
def checkToonHp(self):
# Check all the toons
for toon in self.toons:
if toon.getHp() < 0:
# We kicked the bucket
if toon not in self.sadToons:
self.sadToons.append(toon) # They got one of us!
# Make sure the toon is the invasion before removing it
if toon in self.toons:
self.toons.remove(toon) # Stop attacking us sad toons!
for toon in self.sadToons:
if toon.getHp() > 0:
# Toon now has some laff...
if toon in self.sadToons:
self.toons.append(toon) # Add the toon back into the invasion
self.sadToons.remove(toon) # Remove the sad toon
'''
SUIT-RELATED
We don't have much to do for our Suits here, as most of it takes place in DistributedInvasionSuit.
This just handles the spawning and damage taking.
'''
def spawnOne(self, suitType, levelOffset=0):
# Pick a spawnpoint:
if not self.spawnPoints:
return
pointId = random.choice(self.spawnPoints)
self.spawnPoints.remove(pointId)
# Define our suit:
suit = DistributedInvasionSuitAI(self.air, self)
suit.dna.newSuit(suitType)
suit.setSpawnPoint(pointId)
suit.setLevel(levelOffset)
suit.generateWithRequired(self.zoneId)
# Is this a skelecog wave?
if self.waveNumber in SafezoneInvasionGlobals.SuitSkelecogWaves:
suit.d_makeSkelecog()
# Now send 'em in!
suit.b_setState('FlyDown')
self.suits.append(suit)
def pieHitSuit(self, doId):
# One of those annoying toons hit one of our suits.
avId = self.air.getAvatarIdFromSender()
toon = self.air.doId2do.get(avId)
if not toon:
self.air.writeServerEvent('suspicious', avId=avId, issue='Nonexistent Toon tried to throw a pie!')
return
suit = self.air.doId2do.get(doId)
if suit not in self.suits:
# N.B. this is NOT suspicious as it can happen as a result of a race condition
return
# How much damage does this Throw gag do?
pieDamageEntry = ToontownBattleGlobals.AvPropDamage[ToontownBattleGlobals.THROW_TRACK][toon.pieType]
(pieDamage, pieGroupDamage), _ = pieDamageEntry
suit.takeDamage(pieDamage)
def __deleteSuits(self):
for suit in self.suits:
suit.requestDelete()
def suitDied(self, suit):
if self.state == 'Finale':
self.suits.remove(suit)
self.demand('Victory')
return
if suit not in self.suits:
self.notify.warning('suitDied called twice for same suit!')
return
if self.waveNumber not in SafezoneInvasionGlobals.SuitIntermissionWaves and self.numberOfSuits > 0:
self.numberOfSuits = self.numberOfSuits - 1
# Delay spawing the suits
taskMgr.doMethodLater(1, self.spawnOne, self.uniqueName('summon-suit-%s' % self.numberOfSuits), extraArgs=[suit.getStyleName(), suit.getLevel()])
self.suits.remove(suit)
if not self.suits:
self.waveWon()
@magicWord(category=CATEGORY_DEBUG, types=[str, str])
def szInvasion(cmd, arg=''):
if not config.GetBool('want-doomsday', False):
simbase.air.writeServerEvent('warning', avId=spellbook.getInvoker().doId, issue='Attempted to initiate doomsday while it is disabled.')
return 'ABOOSE! Doomsday is currently disabled. Your request has been logged.'
invasion = simbase.air.doFind('SafezoneInvasion')
if invasion is None and cmd != 'start':
return 'No invasion has been created'
if cmd == 'start':
if invasion is None:
election = simbase.air.doFind('ElectionEvent')
if election is None:
return 'No election event.'
invasion = DistributedSafezoneInvasionAI(simbase.air, election)
invasion.generateWithRequired(2000)
else:
return 'An invasion object already exists.'
elif cmd == 'stop':
invasion.b_setInvasionStarted(False)
invasion.requestDelete()
elif cmd == 'spawn':
invasion.spawnOne(arg)
elif cmd == 'wave':
invasion.demand('BeginWave', int(arg))
elif cmd == 'endWave':
invasion.waveWon()
elif cmd == 'stunFinaleSuit':
invasion.setFinaleSuitStunned(200, True)
elif cmd == 'winFinale':
invasion.winFinale()