diff --git a/etc/Configrc.prc b/etc/Configrc.prc index 32c048c..522e1ba 100644 --- a/etc/Configrc.prc +++ b/etc/Configrc.prc @@ -97,3 +97,7 @@ accept-clock-skew 1 text-minfilter linear_mipmap_linear gc-save-all 0 server-data-folder data + +# TEMPORARY +skip-friend-quest true +skip-phone-quest true diff --git a/etc/toon.dc b/etc/toon.dc index 4f09c30..e972695 100755 --- a/etc/toon.dc +++ b/etc/toon.dc @@ -467,7 +467,7 @@ dclass DistributedToon : DistributedPlayer { setHoodsVisited(uint32[] = [2000]) required ownrecv db; setInterface(blob = []) required ownrecv db; setLastHood(uint32 = 0) required ownrecv db; - setTutorialAck(uint8 = 1) required ownrecv db; + setTutorialAck(uint8 = 0) required ownrecv db; setMaxClothes(uint32 = 10) required ownrecv db; setClothesTopsList(uint8[] = []) required ownrecv db; setClothesBottomsList(uint8[] = []) required ownrecv db; diff --git a/otp/otpbase/OTPGlobals.py b/otp/otpbase/OTPGlobals.py index d65d447..b9b59e5 100644 --- a/otp/otpbase/OTPGlobals.py +++ b/otp/otpbase/OTPGlobals.py @@ -1,4 +1,5 @@ from panda3d.core import * +from panda3d.otp import * QuietZone = 1 UberZone = 2 WallBitmask = BitMask32(1) diff --git a/toontown/ai/BlackCatHolidayMgrAI.py b/toontown/ai/BlackCatHolidayMgrAI.py new file mode 100644 index 0000000..41018c5 --- /dev/null +++ b/toontown/ai/BlackCatHolidayMgrAI.py @@ -0,0 +1,18 @@ +from direct.directnotify import DirectNotifyGlobal +from toontown.ai import HolidayBaseAI +from toontown.toonbase import ToontownGlobals + +class BlackCatHolidayMgrAI(HolidayBaseAI.HolidayBaseAI): + notify = DirectNotifyGlobal.directNotify.newCategory( + 'BlackCatHolidayMgrAI') + + PostName = 'blackCatHoliday' + + def __init__(self, air, holidayId): + HolidayBaseAI.HolidayBaseAI.__init__(self, air, holidayId) + + def start(self): + bboard.post(BlackCatHolidayMgrAI.PostName) + + def stop(self): + bboard.remove(BlackCatHolidayMgrAI.PostName) \ No newline at end of file diff --git a/toontown/ai/ToontownAIRepository.py b/toontown/ai/ToontownAIRepository.py index c6d95bb..340edea 100644 --- a/toontown/ai/ToontownAIRepository.py +++ b/toontown/ai/ToontownAIRepository.py @@ -48,6 +48,7 @@ from toontown.spellbook.ToontownMagicWordManagerAI import ToontownMagicWordManag from toontown.suit.SuitInvasionManagerAI import SuitInvasionManagerAI from toontown.toon import NPCToons from toontown.toonbase import ToontownGlobals +from toontown.tutorial.TutorialManagerAI import TutorialManagerAI from toontown.uberdog.DistributedInGameNewsMgrAI import DistributedInGameNewsMgrAI import os @@ -220,6 +221,10 @@ class ToontownAIRepository(ToontownInternalRepository): self.magicWordManager = ToontownMagicWordManagerAI(self) self.magicWordManager.generateWithRequired(OTP_ZONE_ID_MANAGEMENT) + # Generate our Tutorial manager... + self.tutorialManager = TutorialManagerAI(self) + self.tutorialManager.generateWithRequired(OTP_ZONE_ID_MANAGEMENT) + def generateHood(self, hoodConstructor, zoneId): # Bossbot HQ doesn't use DNA, so we skip over that. if zoneId != ToontownGlobals.BossbotHQ: diff --git a/toontown/building/DistributedTutorialInterior.py b/toontown/building/DistributedTutorialInterior.py index 8fa3ed3..6c92ee1 100644 --- a/toontown/building/DistributedTutorialInterior.py +++ b/toontown/building/DistributedTutorialInterior.py @@ -1,5 +1,6 @@ from toontown.toonbase.ToonBaseGlobal import * from panda3d.core import * +from panda3d.toontown import * from direct.interval.IntervalGlobal import * from direct.distributed.ClockDelta import * from toontown.toonbase import ToontownGlobals diff --git a/toontown/building/DistributedTutorialInteriorAI.py b/toontown/building/DistributedTutorialInteriorAI.py index a1f8a2d..49274a6 100644 --- a/toontown/building/DistributedTutorialInteriorAI.py +++ b/toontown/building/DistributedTutorialInteriorAI.py @@ -1,5 +1,40 @@ -from direct.directnotify import DirectNotifyGlobal -from direct.distributed.DistributedObjectAI import DistributedObjectAI +from toontown.toonbase.ToontownGlobals import * +from otp.ai.AIBaseGlobal import * +from direct.distributed.ClockDelta import * -class DistributedTutorialInteriorAI(DistributedObjectAI): - notify = DirectNotifyGlobal.directNotify.newCategory('DistributedTutorialInteriorAI') +from direct.directnotify import DirectNotifyGlobal +from direct.distributed import DistributedObjectAI +from toontown.toon import NPCToons + +class DistributedTutorialInteriorAI(DistributedObjectAI.DistributedObjectAI): + + if __debug__: + notify = DirectNotifyGlobal.directNotify.newCategory('DistributedTutorialInteriorAI') + + def __init__(self, block, air, zoneId, building, npcId): + """blockNumber: the landmark building number (from the name)""" + #self.air=air + DistributedObjectAI.DistributedObjectAI.__init__(self, air) + self.block=block + self.zoneId=zoneId + self.building=building + self.tutorialNpcId = npcId + + + # Make any npcs that may be in this interior zone + # If there are none specified, this will just be an empty list + self.npcs = NPCToons.createNpcsInZone(air, zoneId) + + def delete(self): + self.ignoreAll() + for npc in self.npcs: + npc.requestDelete() + del self.npcs + del self.building + DistributedObjectAI.DistributedObjectAI.delete(self) + + def getZoneIdAndBlock(self): + return [self.zoneId, self.block] + + def getTutorialNpcId(self): + return self.tutorialNpcId \ No newline at end of file diff --git a/toontown/building/ToonInterior.py b/toontown/building/ToonInterior.py index 5037f60..c39acd4 100644 --- a/toontown/building/ToonInterior.py +++ b/toontown/building/ToonInterior.py @@ -61,7 +61,7 @@ class ToonInterior(Place.Place): State.State('HFA', self.enterHFA, self.exitHFA, ['HFAReject', 'teleportOut', 'tunnelOut']), State.State('HFAReject', self.enterHFAReject, self.exitHFAReject, ['walk']), State.State('doorIn', self.enterDoorIn, self.exitDoorIn, ['walk']), - State.State('doorOut', self.enterDoorOut, self.exitDoorOut, ['walk']), + State.State('doorOut', self.enterDoorOut, self.exitDoorOut, ['walk', 'stopped']), State.State('teleportIn', self.enterTeleportIn, self.exitTeleportIn, ['walk']), State.State('teleportOut', self.enterTeleportOut, self.exitTeleportOut, ['teleportIn']), State.State('quest', self.enterQuest, self.exitQuest, ['walk', 'doorOut']), diff --git a/toontown/building/TutorialBuildingAI.py b/toontown/building/TutorialBuildingAI.py new file mode 100644 index 0000000..552cb9e --- /dev/null +++ b/toontown/building/TutorialBuildingAI.py @@ -0,0 +1,91 @@ +from pandac.PandaModules import * +from direct.directnotify import DirectNotifyGlobal +from . import DistributedDoorAI +from . import DistributedTutorialInteriorAI +from . import FADoorCodes +from . import DoorTypes +from toontown.toon import NPCToons +from toontown.toonbase import TTLocalizer + +# This is not a distributed class... It just owns and manages some distributed +# classes. + +class TutorialBuildingAI: + def __init__(self, air, exteriorZone, interiorZone, blockNumber): + # While this is not a distributed object, it needs to know about + # the repository. + self.air = air + self.exteriorZone = exteriorZone + self.interiorZone = interiorZone + + # This is because we are "pretending" to be a DistributedBuilding. + # The DistributedTutorialInterior takes a peek at savedBy. It really + # should make a function call. Perhaps TutorialBuildingAI and + # DistributedBuildingAI should inherit from each other somehow, + # but I can't see an easy way to do that. + self.savedBy = None + + self.setup(blockNumber) + + def cleanup(self): + self.interior.requestDelete() + del self.interior + self.door.requestDelete() + del self.door + self.insideDoor.requestDelete() + del self.insideDoor + self.gagShopNPC.requestDelete() + del self.gagShopNPC + return + + def setup(self, blockNumber): + # Put an NPC in here. Give him id# 20000. When he has assigned + # his quest, he will unlock the interior door. + self.gagShopNPC = NPCToons.createNPC( + self.air, 20000, + (self.interiorZone, + TTLocalizer.NPCToonNames[20000], + ("dll" ,"ms" ,"m" ,"m" ,7 ,0 ,7 ,7 ,2 ,6 ,2 ,6 ,2 ,16), "m", 1, NPCToons.NPC_REGULAR), + self.interiorZone, + questCallback=self.unlockInteriorDoor) + # Flag him as being part of tutorial + self.gagShopNPC.setTutorial(1) + npcId = self.gagShopNPC.getDoId() + # Toon interior (with tutorial flag set to 1) + self.interior=DistributedTutorialInteriorAI.DistributedTutorialInteriorAI( + blockNumber, self.air, self.interiorZone, self, npcId) + self.interior.generateWithRequired(self.interiorZone) + # Outside door: + door=DistributedDoorAI.DistributedDoorAI(self.air, blockNumber, + DoorTypes.EXT_STANDARD, + lockValue=FADoorCodes.DEFEAT_FLUNKY_TOM) + # Inside door. Locked until you get your gags. + insideDoor=DistributedDoorAI.DistributedDoorAI( + self.air, + blockNumber, + DoorTypes.INT_STANDARD, + lockValue=FADoorCodes.TALK_TO_TOM) + # Tell them about each other: + door.setOtherDoor(insideDoor) + insideDoor.setOtherDoor(door) + door.zoneId=self.exteriorZone + insideDoor.zoneId=self.interiorZone + # Now that they both now about each other, generate them: + door.generateWithRequired(self.exteriorZone) + #door.sendUpdate("setDoorIndex", [door.getDoorIndex()]) + insideDoor.generateWithRequired(self.interiorZone) + #insideDoor.sendUpdate("setDoorIndex", [door.getDoorIndex()]) + # keep track of them: + self.door=door + self.insideDoor=insideDoor + return + + def unlockInteriorDoor(self): + self.insideDoor.setDoorLock(FADoorCodes.UNLOCKED) + + def battleOverCallback(self): + # There is an if statement here because it is possible for + # the callback to get called after cleanup has already taken + # place. + if hasattr(self, "door"): + self.door.setDoorLock(FADoorCodes.TALK_TO_HQ_TOM) \ No newline at end of file diff --git a/toontown/building/TutorialHQBuildingAI.py b/toontown/building/TutorialHQBuildingAI.py new file mode 100644 index 0000000..ffb6704 --- /dev/null +++ b/toontown/building/TutorialHQBuildingAI.py @@ -0,0 +1,128 @@ +from panda3d.core import * +from direct.directnotify import DirectNotifyGlobal +from . import DistributedDoorAI +from . import DistributedHQInteriorAI +from . import FADoorCodes +from . import DoorTypes +from toontown.toon import NPCToons +from toontown.quest import Quests +from toontown.toonbase import TTLocalizer + +# This is not a distributed class... It just owns and manages some distributed +# classes. + +class TutorialHQBuildingAI: + def __init__(self, air, exteriorZone, interiorZone, blockNumber): + # While this is not a distributed object, it needs to know about + # the repository. + self.air = air + self.exteriorZone = exteriorZone + self.interiorZone = interiorZone + + self.setup(blockNumber) + + def cleanup(self): + self.interior.requestDelete() + del self.interior + self.npc.requestDelete() + del self.npc + self.door0.requestDelete() + del self.door0 + self.door1.requestDelete() + del self.door1 + self.insideDoor0.requestDelete() + del self.insideDoor0 + self.insideDoor1.requestDelete() + del self.insideDoor1 + return + + def setup(self, blockNumber): + # The interior + self.interior=DistributedHQInteriorAI.DistributedHQInteriorAI( + blockNumber, self.air, self.interiorZone) + + # We do not use a standard npc toon here becuase these npcs are created on + # the fly for as many tutorials as we need. The interior zone is not known + # until the ai allocates a zone, so we fabricate the description here. + desc = (self.interiorZone, TTLocalizer.TutorialHQOfficerName, ('dls', 'ms', 'm', 'm', 6,0,6,6,0,10,0,10,2,9), "m", 1, 0) + self.npc = NPCToons.createNPC(self.air, Quests.ToonHQ, desc, + self.interiorZone, + questCallback=self.unlockInsideDoor1) + # Flag npc as part of tutorial + self.npc.setTutorial(1) + + self.interior.generateWithRequired(self.interiorZone) + # Outside door 0. Locked til you defeat the Flunky: + door0=DistributedDoorAI.DistributedDoorAI( + self.air, blockNumber, DoorTypes.EXT_HQ, + doorIndex=0, + lockValue=FADoorCodes.DEFEAT_FLUNKY_HQ) + # Outside door 1. Always locked. + door1=DistributedDoorAI.DistributedDoorAI( + self.air, blockNumber, DoorTypes.EXT_HQ, + doorIndex=1, + lockValue=FADoorCodes.GO_TO_PLAYGROUND) + # Inside door 0. Always locked, but the message will change. + insideDoor0=DistributedDoorAI.DistributedDoorAI( + self.air, + blockNumber, + DoorTypes.INT_HQ, + doorIndex=0, + lockValue=FADoorCodes.TALK_TO_HQ) + # Inside door 1. Locked til you get your HQ reward. + insideDoor1=DistributedDoorAI.DistributedDoorAI( + self.air, + blockNumber, + DoorTypes.INT_HQ, + doorIndex=1, + lockValue=FADoorCodes.TALK_TO_HQ) + # Tell them about each other: + door0.setOtherDoor(insideDoor0) + insideDoor0.setOtherDoor(door0) + door1.setOtherDoor(insideDoor1) + insideDoor1.setOtherDoor(door1) + # Put them in the right zones + door0.zoneId=self.exteriorZone + door1.zoneId=self.exteriorZone + insideDoor0.zoneId=self.interiorZone + insideDoor1.zoneId=self.interiorZone + # Now that they both now about each other, generate them: + door0.generateWithRequired(self.exteriorZone) + door1.generateWithRequired(self.exteriorZone) + door0.sendUpdate("setDoorIndex", [door0.getDoorIndex()]) + door1.sendUpdate("setDoorIndex", [door1.getDoorIndex()]) + insideDoor0.generateWithRequired(self.interiorZone) + insideDoor1.generateWithRequired(self.interiorZone) + insideDoor0.sendUpdate("setDoorIndex", [insideDoor0.getDoorIndex()]) + insideDoor1.sendUpdate("setDoorIndex", [insideDoor1.getDoorIndex()]) + # keep track of them: + self.door0=door0 + self.door1=door1 + self.insideDoor0=insideDoor0 + self.insideDoor1=insideDoor1 + # hide the periscope + self.interior.setTutorial(1) + return + + def unlockDoor(self, door): + door.setDoorLock(FADoorCodes.UNLOCKED) + + def battleOverCallback(self): + # There is an if statement here because it is possible for + # the callback to get called after cleanup has already taken + # place. + if hasattr(self, "door0"): + self.unlockDoor(self.door0) + + # This callback type happens to give zoneId. We don't need it. + def unlockInsideDoor1(self): + # There is an if statement here because it is possible for + # the callback to get called after cleanup has already taken + # place. + if hasattr(self, "insideDoor1"): + self.unlockDoor(self.insideDoor1) + # Change the message on this locked door to tell you to go + # through the other door. Maybe this door should not be + # here at all? + if hasattr(self, "insideDoor0"): + self.insideDoor0.setDoorLock(FADoorCodes.WRONG_DOOR_HQ) \ No newline at end of file diff --git a/toontown/quest/QuestManagerAI.py b/toontown/quest/QuestManagerAI.py index 9d7ecd7..8a7d8b7 100644 --- a/toontown/quest/QuestManagerAI.py +++ b/toontown/quest/QuestManagerAI.py @@ -1,41 +1,993 @@ +from otp.ai.AIBaseGlobal import * +from direct.task import Task from direct.directnotify import DirectNotifyGlobal +from . import Quests +from toontown.toon import NPCToons +import random +""" +TODO: (done, tested) + + + Jellybean reward + + + Max Jellybean reward + + + Quest combos + + + Required and optional reward pool + + + Quest page class + + + Integrate chat next buttons + + + Quest multiple choice AI + + + Quest multiple choice gui + + + Quest multiple choice choosing + + + clean up NPC Toon dicts + + + Stolen item list + + + Stolen item quests + + + Stolen item integration into reward panel + + + Stolen item integration into battle + + + Trolley quest + + + Make friend quest + + + sticker book reward + + + non-rejectable quest tiers + + + customize how many quests to choose from + + + gui movies in quest movies + + + Choose track access quest + + + Track access partial rewards + + + Toon HQ integration + + + Multiple NPCs indoors + + + Integrate quest page artwork + + + Cog logos for posters + + + Trolley reward in playground + - - Dynamic timeout lengths based on movie + + New Track order: + Choice 1: Sound or Heal + Choice 2: Drop or Lure + Choice 3: 1' or Trap + Choice 4: 2' or 3' +""" class QuestManagerAI: - notify = DirectNotifyGlobal.directNotify.newCategory('QuestManagerAI') + + notify = DirectNotifyGlobal.directNotify.newCategory("QuestManagerAI") + + # Immediately complete all quests and all visits are to ToonHQ + QuestCheat = simbase.config.GetBool("quest-cheat", 0) + + # table of requests for quests from specific avatars + NextQuestDict = {} def __init__(self, air): self.air = air - def recoverItems(self, toon, suitsKilled, zoneId): - return [], [] # TODO - - def toonKilledCogs(self, toon, suitsKilled, zoneId, activeToonList): - pass # TODO - def requestInteract(self, avId, npc): - npc.rejectAvatar(avId) # TODO + self.notify.debug("requestInteract: avId: %s npcId: %s" % (avId, npc.getNpcId())) + av = self.air.doId2do.get(avId) - def hasTailorClothingTicket(self, toon, npc): - return 0 # TODO + # Sanity check + if av is None: + self.notify.warning("some toon did a requestInteract but is not here: %s" % (avId)) + return - def toonKilledBuilding(self, toon, track, difficulty, numFloors, zoneId, activeToons): - pass # TODO + # If this NPC is busy, free the avatar + if npc.isBusy(): + self.notify.debug("freeing avatar %s because NPC is busy" % (avId)) + npc.freeAvatar(avId) + return - def toonKilledCogdo(self, toon, difficulty, numFloors, zoneId, activeToons): - pass # TODO + # handle unusual cases such as NPC specific quests + # interactionComplete = self.handleSpecialCases(avId, npc) - def toonPlayedMinigame(self, toon, toons): - pass # TODO + # if interactionComplete: + # return - def toonRecoveredCogSuitPart(self, av, zoneId, avList): - pass # TODO + # First, see if any quests are completed before checking for incomplete + # Since the ToonHQ could match multiple quests on the av list, we need + # to prioritize what they give their attention to first. I think it + # makes sense for them to clear complete quests first + for questDesc in av.quests: - def toonDefeatedFactory(self, toon, factoryId, activeVictors): - pass # TODO + # Sanity check for rogue quests + if not Quests.questExists(questDesc[0]): + av.removeAllTracesOfQuest(questDesc[0], questDesc[3]) + self.rejectAvatar(av, npc) + return - def toonDefeatedMint(self, toon, mintId, activeVictors): - pass # TODO + if (self.isQuestComplete(av, npc, questDesc) == Quests.COMPLETE): + self.completeQuest(av, npc, questDesc[0]) + return - def toonDefeatedStage(self, toon, stageId, activeVictors): - pass # TODO + needsQuestButNoneLeft = 0 + if (self.needsQuest(av) and npc.getGivesQuests()): + # bestQuests is a nested list of [questId, rewardId, toNpcId] lists + quests = self.getNextQuestIds(npc, av) + if quests: + if (Quests.getNumChoices(av.getRewardTier()) == 0): + assert(len(quests) == 1) # There should only be one + if npc.getHq(): + fromNpcId = Quests.ToonHQ + else: + fromNpcId = npc.getNpcId() + self.assignQuest(avId, fromNpcId, *quests[0]) + npc.assignQuest(av.getDoId(), *quests[0]) + else: + # if this avatar requested a quest, include it + if avId in self.NextQuestDict: + questId = self.NextQuestDict[avId] + # if it's already in the list of quests, + # we're all set + ids = [] + for q in quests: + ids.append(q[0]) + if questId not in ids: + # add the quest as the first choice + questDesc = Quests.QuestDict[questId] + reward = questDesc[Quests.QuestDictRewardIndex] + toNpcId = questDesc[Quests.QuestDictToNpcIndex] + if reward is Quests.Any: + reward = 604 # some jbs + if toNpcId is Quests.Any: + toNpcId = Quests.ToonHQ + quests[0] = [questId, reward, toNpcId] + + npc.presentQuestChoice(av.getDoId(), quests) + return + else: + needsQuestButNoneLeft = 1 + + # Now see if this npc has any incomplete quests with av + questDesc = self.hasQuest(av, npc) + if questDesc: + completeStatus = self.isQuestComplete(av, npc, questDesc) + questId, fromNpcId, toNpcId, rewardId, toonProgress = questDesc + self.incompleteQuest(av, npc, questId, completeStatus, toNpcId) + return + else: + if needsQuestButNoneLeft: + # If they have quests, tell them to finish their tier + if av.quests: + self.rejectAvatarTierNotDone(av, npc) + # If they do not have any quests, advance their tier + else: + if self.incrementReward(av): + # quests is a nested list of [questId, rewardId, toNpcId] lists + quests = self.getNextQuestIds(npc, av) + if quests: + if (Quests.getNumChoices(av.getRewardTier()) == 0): + assert(len(quests) == 1) # There should only be one + if npc.getHq(): + fromNpcId = Quests.ToonHQ + else: + fromNpcId = npc.getNpcId() + self.assignQuest(avId, fromNpcId, *quests[0]) + npc.assignQuest(av.getDoId(), *quests[0]) + else: + npc.presentQuestChoice(av.getDoId(), quests) + return + else: + # No more quests, sorry + # TODO: put some more meaningful dialog here + self.rejectAvatar(av, npc) + return + else: + # Avatar does not need a quest, goodbye + self.rejectAvatar(av, npc) + return + + def handleSpecialCases(self, avId, npc): + """ handle unusual cases such as NPC specific quests""" + + av = self.air.doId2do.get(avId) + + if npc.getNpcId() == 2018: + # See if this npc has the TIP quest + for questDesc in av.quests: + # Do not use doId, use the NpcId because the doId is different across shards + questId = questDesc[0] + if (questId == 103): + completeStatus = self.isQuestComplete(av, npc, questDesc) + questId, fromNpcId, toNpcId, rewardId, toonProgress = questDesc + self.incompleteQuest(av, npc, questId, completeStatus, toNpcId) + return 1 + + if self.needsQuest(av): + self.assignQuest(avId, npc.npcId, 103, Quests.QuestDict[103][5], Quests.QuestDict[103][4]) + npc.assignQuest(avId, 103, Quests.QuestDict[103][5], Quests.QuestDict[103][4]) + return 1 + + return 0 + + def rejectAvatar(self, av, npc): + self.notify.debug("rejecting avatar: avId: %s" % (av.getDoId())) + npc.rejectAvatar(av.getDoId()) + return + + def rejectAvatarTierNotDone(self, av, npc): + self.notify.debug("rejecting avatar because tier not done: avId: %s" % (av.getDoId())) + npc.rejectAvatarTierNotDone(av.getDoId()) + return + + def hasQuest(self, av, npc): + # Check if this avId has a quest on this npcId + for questDesc in av.quests: + # Do not use doId, use the NpcId because the doId is different across shards + questId = questDesc[0] + fromNpcId = questDesc[1] + toNpcId = questDesc[2] + # If the fromNpc that gave you the quest is involved, or + # if you have a toNpc then return this questId + if (fromNpcId == npc.getNpcId()): + self.notify.debug("hasQuest: found quest: %s avId: %s fromNpcId: %s" % + (questId, av.getDoId(), fromNpcId)) + return questDesc + elif (toNpcId == npc.getNpcId()): + # If the quest has this npc as the toNpc, then we are done + self.notify.debug("hasQuest: found quest with toNpc: %s avId: %s toNpcId: %s" % + (questId, av.getDoId(), toNpcId)) + return questDesc + elif (toNpcId == Quests.Any): + # If the quest has "any" as the toNpc, than this guy will do + self.notify.debug("hasQuest: found quest with any toNpc: %s avId: %s toNpcId: %s" % + (questId, av.getDoId(), toNpcId)) + return questDesc + elif ((toNpcId == Quests.ToonHQ) and (npc.getHq())): + # If the quest is for the HQ, and this toon has HQ powers, its a match + self.notify.debug("hasQuest: found quest with HQ toNpc: %s avId: %s toNpcId: %s" % + (questId, av.getDoId(), toNpcId)) + return questDesc + elif ((toNpcId == Quests.ToonTailor) and (npc.getTailor())): + # If the quest is for a tailor, and this toon is a tailor, its a match + self.notify.debug("hasQuest: found quest with Tailor toNpc: %s avId: %s toNpcId: %s" % + (questId, av.getDoId(), toNpcId)) + return questDesc + self.notify.debug("hasQuest: did not find quest for avId: %s npcId: %s" % + (av.getDoId(), npc.getNpcId())) + return None + + def isQuestComplete(self, av, npc, questDesc): + # The quest in question + quest = Quests.getQuest(questDesc[0]) + if quest == None: + return 0 + self.notify.debug("isQuestComplete: avId: %s, quest: %s" % + (av.getDoId(), quest)) + return quest.getCompletionStatus(av, questDesc, npc) + + def completeQuest(self, av, npc, questId): + self.notify.info("completeQuest: avId: %s, npcId: %s, questId: %s" % + (av.getDoId(), npc.getNpcId(), questId)) + + # If this is a track choice, we do not actually complete the quest, + # We present the track choice gui. This can be cancelled which will + # not complete the quest. + questClass = Quests.getQuestClass(questId) + if questClass == Quests.TrackChoiceQuest: + self.notify.debug("completeQuest: presentTrackChoice avId: %s, npcId: %s, questId: %s" % + (av.getDoId(), npc.getNpcId(), questId)) + quest = Quests.getQuest(questId) + tracks = quest.getChoices() + npc.presentTrackChoice(av.getDoId(), questId, tracks) + # Do not increment reward until avatar has chosen track + # This happens in avatarChoseTrack + return + + # If this is a deliver gag quest, we need to actually remove the + # gags delivered from the player's inventory + if questClass == Quests.DeliverGagQuest: + self.notify.debug("completeQuest: presentTrackChoice avId: %s, npcId: %s, questId: %s" % + (av.getDoId(), npc.getNpcId(), questId)) + # Use the items from the inventory now + quest = Quests.getQuest(questId) + track, level = quest.getGagType() + for i in range(0, quest.getNumGags()): + av.inventory.useItem(track, level) + av.d_setInventory(av.inventory.makeNetString()) + + + # See if this quest is part of a multiquest. If it is, we assign + # the next part of the multiquest. + nextQuestId, nextToNpcId = Quests.getNextQuest(questId, npc, av) + eventLogMessage = "%s|%s|%s|%s" % ( + questId, npc.getNpcId(), questClass.__name__, nextQuestId) + + if nextQuestId == Quests.NA: + rewardId = Quests.getAvatarRewardId(av, questId) + # Update the toon with the reward + reward = Quests.getReward(rewardId) + + # Clothing quests should have been handled by the Tailor. + # Just to make sure + if (reward.getType() == Quests.ClothingTicketReward): + self.notify.warning("completeQuest: rogue ClothingTicketReward avId: %s, npcId: %s, questId: %s" % + (av.getDoId(), npc.getNpcId(), questId)) + npc.freeAvatar(av.getDoId()) + return + + # Nope, this is the end, dish out the reward + av.removeQuest(questId) + # TODO: put this in the movie + reward.sendRewardAI(av) + # Full heal for completing a quest + av.toonUp(av.maxHp) + # Tell the npc to deliver the movie which will + # complete the quest, display the reward, and do nothing else + npc.completeQuest(av.getDoId(), questId, rewardId) + # Bump the reward + self.incrementReward(av) + + eventLogMessage += "|%s|%s" % ( + reward.__class__.__name__, reward.getAmount()) + + else: + # Full heal for completing part of a multistage quest + av.toonUp(av.maxHp) + # The user is not presented with a choice here + av.removeQuest(questId) + nextRewardId = Quests.getQuestReward(nextQuestId, av) + if npc.getHq(): + fromNpcId = Quests.ToonHQ + else: + fromNpcId = npc.getNpcId() + self.assignQuest(av.getDoId(), fromNpcId, nextQuestId, nextRewardId, nextToNpcId, startingQuest = 0) + npc.assignQuest(av.getDoId(), nextQuestId, nextRewardId, nextToNpcId) + eventLogMessage += "|next %s" % (nextQuestId) + + self.air.writeServerEvent('questComplete', av.getDoId(), eventLogMessage) + + def incompleteQuest(self, av, npc, questId, completeStatus, toNpcId): + self.notify.debug("incompleteQuest: avId: %s questId: %s" % + (av.getDoId(), questId)) + npc.incompleteQuest(av.getDoId(), questId, completeStatus, toNpcId) + return + + def needsQuest(self, av): + # Return 0 if this avatar does not need a new quest, 1 if he does + quests = av.quests + carryLimit = av.getQuestCarryLimit() + if (len(quests) >= carryLimit): + self.notify.debug("needsQuest: avId: %s is already full with %s/%s quest(s)" % + (av.getDoId(), len(quests), carryLimit)) + return 0 + else: + self.notify.debug("needsQuest: avId: %s only has %s/%s quest(s), needs another" % + (av.getDoId(), len(quests), carryLimit)) + return 1 + + def getNextQuestIds(self, npc, av): + # Return the quest id, reward id for the next quest + # Return None, None if the search fails for some reason + return Quests.chooseBestQuests(av.getRewardTier(), npc, av) + + def incrementReward(self, av): + # See if we finished a tier + rewardTier = av.getRewardTier() + # Make sure all the rewards have been handed out and + # Make sure we have completed them all + # First, make sure that the list is at least as big as the number of rewards + # Then, make sure we have completed them all + # Then, make sure all the rewards in the tier are in our history + rewardHistory = av.getRewardHistory()[1] + if ( + # We cannot do this short-circuit test anymore because having + # cog suit parts counts as a reward in cashbot + # HQ. Unfortunately we are losing a pretty nice optimization + # here. TODO: revisit and optimize. + # (len(rewardHistory) >= Quests.getNumRewardsInTier(rewardTier)) and + + # We cannot do this because they might still be working on a few + # optional quests from the old tier. + # (len(av.quests) == 0) and + + # Make sure they have all the required rewards + (Quests.avatarHasAllRequiredRewards(av, rewardTier)) and + + # Make sure they are not still working on required rewards + (not Quests.avatarWorkingOnRequiredRewards(av)) + ): + + if not Quests.rewardTierExists(rewardTier+1): + self.notify.info("incrementReward: avId %s, at end of rewards" % + (av.getDoId())) + return 0 + + rewardTier += 1 + self.notify.info("incrementReward: avId %s, new rewardTier: %s" % + (av.getDoId(), rewardTier)) + + # If we have just moved on to the next tier, blow away the + # old history, which is no longer needed. + av.b_setQuestHistory([]) + av.b_setRewardHistory(rewardTier, []) + + # The above will clear the quest history the *first* time + # we cross into the next tier. There may still be some + # quest id's hiding behind visit quests that belong to the + # previous tier; these will find their way onto the quest + # history when we eventually reveal them, but they will + # still be associated with the previous tier. This does + # no harm, so we won't worry about it; but it does mean + # that the questHistory list is not guaranteed to only + # list quests on the current tier. It is simply + # guaranteed to list all the completed and in-progress + # quests on the current tier, with maybe one or two others + # thrown in. + return 1 + else: + self.notify.debug("incrementReward: avId %s, not ready for new tier" % + (av.getDoId())) + return 0 + + + def avatarCancelled(self, avId): + # This is a message that came from the client, through the NPCToonAI. + # It is in response to the avatar picking from a multiple choice menu + self.notify.debug("avatarCancelled: avId: %s" % (avId)) + return + + def avatarChoseTrack(self, avId, npc, questId, trackId): + # This is a message that came from the client, through the NPCToonAI. + # It is in response to the avatar picking from a multiple choice menu + # of track options, along with a cancel option + self.notify.info("avatarChoseTrack: avId: %s trackId: %s" % (avId, trackId)) + av = self.air.doId2do.get(avId) + if av: + # Remove the track choice quest + av.removeQuest(questId) + # Update the toon with the reward + rewardId = Quests.getRewardIdFromTrackId(trackId) + reward = Quests.getReward(rewardId) + reward.sendRewardAI(av) + # Tell the npc to deliver the movie which will + # complete the quest, display the reward, and do nothing else + npc.completeQuest(av.getDoId(), questId, rewardId) + self.incrementReward(av) + else: + self.notify.warning("avatarChoseTrack: av is gone.") + + def avatarChoseQuest(self, avId, npc, questId, rewardId, toNpcId): + # This is a message that came from the client, through the NPCToonAI. + # It is in response to the avatar picking from a multiple choice menu + # of quest options, along with a cancel option + self.notify.debug("avatarChooseQuest: avId: %s questId: %s" % (avId, questId)) + av = self.air.doId2do.get(avId) + if av: + if npc.getHq(): + fromNpcId = Quests.ToonHQ + else: + fromNpcId = npc.getNpcId() + self.assignQuest(avId, fromNpcId, questId, rewardId, toNpcId) + npc.assignQuest(avId, questId, rewardId, toNpcId) + # Do not increment the reward until the quest is completed + else: + self.notify.warning("avatarChoseQuest: av is gone.") + + def assignQuest(self, avId, npcId, questId, rewardId, toNpcId, startingQuest = 1): + self.notify.info("assignQuest: avId: %s npcId: %s questId: %s rewardId: %s toNpcId: %s startingQuest: %s" % + (avId, npcId, questId, rewardId, toNpcId, startingQuest)) + # assign quest to avatar + # A quest is a list with (questId, npcId, toNpcId, rewardId, progress) + av = self.air.doId2do.get(avId) + if av: + if startingQuest: + # Since the first parts or multipart quests have NA for their + # rewardIds, we need to get the final reward of this quest by searching + # down the chain. If this questId is not the start of a multipart + # quest, finalRewardId will come back None, and addQuest will handle it + if rewardId == Quests.NA: + finalRewardId = Quests.getFinalRewardId(questId) + else: + # Do not count the end of multipart quests, even though they + # have a valid rewardId. That rewardId would have been counted + # when the initial quest was given out + if not Quests.isStartingQuest(questId): + finalRewardId = None + else: + finalRewardId = rewardId + # If this was not handed out as a starting quest, make sure you do not + # count the reward twice + else: + finalRewardId = None + # 0 for initial progress + initialProgress = 0 + # To make it easy for testing purposes. + # This should never be on in production + if self.QuestCheat: + # Quest is already compelte + initialProgress = 1000 + # Clothing quests must be handled by the Tailor. + if ((rewardId == Quests.NA) or + (Quests.getRewardClass(rewardId) != Quests.ClothingTicketReward)): + # Visit npc is the HQ + toNpcId = Quests.ToonHQ + if Quests.isLoopingFinalTier(av.getRewardTier()): + # Do not record the history if this is the final looping tier + recordHistory = 0 + else: + recordHistory = 1 + av.addQuest((questId, npcId, toNpcId, rewardId, initialProgress), finalRewardId, recordHistory) + # if this was a requested quest, clear it + if self.NextQuestDict.get(avId) == questId: + del self.NextQuestDict[avId] + else: + self.notify.warning("assignQuest: avatar not found: avId: %s" % (avId)) + return + + + def toonDefeatedFactory(self, av, location, avList): + # factory is telling us that this avatar just defeated it. + # see if this toon has a quest on this factory. If so, + # update the progress. + avQuests = av.quests + avId = av.getDoId() + changed = 0 + + for questDesc in avQuests: + quest = Quests.getQuest(questDesc[0]) + num = quest.doesFactoryCount(avId, location, avList) + if num > 0: + questDesc[4] += num + changed = 1 + + # Now send the quests back to the avatar if the status changed + if changed: + self.notify.debug("toonDefeatedFactory: av made progress") + av.b_setQuests(avQuests) + else: + self.notify.debug("toonDefeatedFactory: av made NO progress") + + def toonDefeatedStage(self, av, location, avList): + self.notify.debug("toonDefeatedStage: av made NO progress") + + def toonRecoveredCogSuitPart(self, av, location, avList): + avQuests = av.quests + avId = av.getDoId() + changed = 0 + + for questDesc in avQuests: + quest = Quests.getQuest(questDesc[0]) + num = quest.doesCogPartCount(avId, location, avList) + if num > 0: + questDesc[4] += num + changed = 1 + + # Now send the quests back to the avatar if the status changed + if changed: + self.notify.debug("toonRecoveredCogSuitPart: av made progress") + av.b_setQuests(avQuests) + else: + self.notify.debug("toonRecoveredCogSuitPart: av made NO progress") + + def toonDefeatedMint(self, av, mintId, avList): + # mint is telling us that this avatar just defeated it. + # see if this toon has a quest on this mint. If so, + # update the progress. + avQuests = av.quests + avId = av.getDoId() + changed = 0 + + for questDesc in avQuests: + quest = Quests.getQuest(questDesc[0]) + num = quest.doesMintCount(avId, mintId, avList) + if num > 0: + questDesc[4] += num + changed = 1 + + # Now send the quests back to the avatar if the status changed + if changed: + self.notify.debug("toonDefeatedMint: av made progress") + av.b_setQuests(avQuests) + else: + self.notify.debug("toonDefeatedMint: av made NO progress") + + def toonDefeatedStage(self, av, stageId, avList): + self.notify.debug("toonDefeatedStage") + pass + + def toonKilledBuilding(self, av, track, difficulty, numFloors, zoneId, avList): + # This is the battle notifying us that a toon has defeated a + # building. See if this toon has a quest on this building. + # If so, update the progress. + avQuests = av.quests + avId = av.getDoId() + changed = 0 + + #self.notify.debug("toonKilledBuilding: avId: %s, track: %s, diff: %s, numFloors: %s, zoneId: %s" % + # (avId, track, difficulty, numFloors, zoneId)) + for questDesc in avQuests: + questClass = Quests.getQuestClass(questDesc[0]) + if ((questClass == Quests.BuildingQuest) or + (questClass == Quests.BuildingNewbieQuest)): + quest = Quests.getQuest(questDesc[0]) + matchedTrack = ((quest.getBuildingTrack() == Quests.Any) or (quest.getBuildingTrack() == track)) + matchedNumFloors = (quest.getNumFloors() <= numFloors) + matchedLocation = quest.isLocationMatch(zoneId) + if matchedTrack and matchedNumFloors and matchedLocation: + num = quest.doesBuildingCount(avId, avList) + if (num > 0): + questDesc[4] += num + changed = 1 + else: + # Do not care about this quest here + continue + + # Now send the quests back to the avatar if the status changed + if changed: + self.notify.debug("toonKilledBuilding: av made progress") + av.b_setQuests(avQuests) + else: + self.notify.debug("toonKilledBuilding: av made NO progress") + return + + def toonKilledCogdo(self, av, difficulty, numFloors, zoneId, avList): + # This is the battle notifying us that a toon has defeated a + # cogdo. See if this toon has a quest on this cogdo. + # If so, update the progress. + avQuests = av.quests + avId = av.getDoId() + changed = 0 + + #self.notify.debug("toonKilledBuilding: avId: %s, track: %s, diff: %s, numFloors: %s, zoneId: %s" % + # (avId, track, difficulty, numFloors, zoneId)) + for questDesc in avQuests: + questClass = Quests.getQuestClass(questDesc[0]) + """ TODO + if ((questClass == Quests.BuildingQuest) or + (questClass == Quests.BuildingNewbieQuest)): + quest = Quests.getQuest(questDesc[0]) + matchedTrack = ((quest.getBuildingTrack() == Quests.Any) or (quest.getBuildingTrack() == track)) + matchedNumFloors = (quest.getNumFloors() <= numFloors) + matchedLocation = quest.isLocationMatch(zoneId) + if matchedTrack and matchedNumFloors and matchedLocation: + num = quest.doesBuildingCount(avId, avList) + if (num > 0): + questDesc[4] += num + changed = 1 + else: + # Do not care about this quest here + continue + """ + + # Now send the quests back to the avatar if the status changed + if changed: + self.notify.debug("toonKilledCogdo: av made progress") + av.b_setQuests(avQuests) + else: + self.notify.debug("toonKilledCogdo: av made NO progress") + return + + def toonKilledCogs(self, av, cogList, zoneId, avList): + # This is the battle notifying us that a toon killed some cogs + # See if this toon has a quest on these cogs. If so, update the progress. + avQuests = av.quests + avId = av.getDoId() + changed = 0 + + self.notify.debug("toonKilledCogs: avId: %s, avQuests: %s, cogList: %s, zoneId: %s" % + (avId, avQuests, cogList, zoneId)) + + for questDesc in avQuests: + quest = Quests.getQuest(questDesc[0]) + if quest != None: + for cogDict in cogList: + if cogDict['isVP']: + num = quest.doesVPCount(avId, cogDict, zoneId, avList) + elif cogDict['isCFO']: + num = quest.doesCFOCount(avId, cogDict, zoneId, avList) + else: + num = quest.doesCogCount(avId, cogDict, zoneId, avList) + if (num > 0): + questDesc[4] += num + changed = 1 + + # Now send the quests back to the avatar if the status changed + if changed: + self.notify.debug("toonKilledCogs: av %s made progress" % (avId)) + av.b_setQuests(avQuests) + else: + self.notify.debug("toonKilledCogs: av %s made NO progress" % (avId)) + return + + def toonRodeTrolleyFirstTime(self, av): + # This is notifying us that a toon has gotten on the + # trolley for the first time. See if this toon has a + # trolley quest. If so, update the progress. + avQuests = av.quests + avId = av.getDoId() + changed = 0 + + for questDesc in avQuests: + questClass = Quests.getQuestClass(questDesc[0]) + if (questClass == Quests.TrolleyQuest): + # Set progress + questDesc[4] = 1 + changed = 1 + else: + # Do not care about this quest here + pass + + # Now send the quests back to the avatar if the status changed + if changed: + self.notify.debug("toonRodeTrolleyFirstTime: av %s made progress" % (avId)) + av.b_setQuests(avQuests) + # log this event + self.air.writeServerEvent('firstTrolleyGame', avId, '') + else: + self.notify.debug("toonRodeTrolleyFirstTime: av %s made NO progress" % (avId)) + return + + def toonPlayedMinigame(self, av, avList): + # This is notifying us that a toon has entered a minigame. + # See if this toon has a minigame quest. If so, update the progress. + avQuests = av.quests + avId = av.getDoId() + changed = 0 + + for questDesc in avQuests: + questClass = Quests.getQuestClass(questDesc[0]) + if (questClass == Quests.MinigameNewbieQuest): + quest = Quests.getQuest(questDesc[0]) + num = quest.doesMinigameCount(av, avList) + if (num > 0): + # Set progress + questDesc[4] += num + changed = 1 + + # Now send the quests back to the avatar if the status changed + if changed: + self.notify.debug("toonPlayedMinigame: av %s made progress" % (avId)) + av.b_setQuests(avQuests) + else: + self.notify.debug("toonPlayedMinigame: av %s made NO progress" % (avId)) + return + + def toonOpenedMailbox(self, av): + # This is notifying us that a toon has opened his mailbox + # See if this toon has a mailbox quest. If so, update the progress. + avQuests = av.quests + avId = av.getDoId() + changed = 0 + for questDesc in avQuests: + questClass = Quests.getQuestClass(questDesc[0]) + if (questClass == Quests.MailboxQuest): + # Set progress + questDesc[4] = 1 + changed = 1 + # Now send the quests back to the avatar if the status changed + if changed: + self.notify.debug("toonOpenedMailbox: av %s made progress" % (avId)) + av.b_setQuests(avQuests) + else: + self.notify.debug("toonOpenedMailbox: av %s made NO progress" % (avId)) + return + + def toonUsedPhone(self, av): + # This is notifying us that a toon used his phone + # See if this toon has a phone quest. If so, update the progress. + avQuests = av.quests + avId = av.getDoId() + changed = 0 + for questDesc in avQuests: + questClass = Quests.getQuestClass(questDesc[0]) + if (questClass == Quests.PhoneQuest): + # Set progress + questDesc[4] = 1 + changed = 1 + # Now send the quests back to the avatar if the status changed + if changed: + self.notify.debug("toonUsedPhone: av %s made progress" % (avId)) + av.b_setQuests(avQuests) + else: + self.notify.debug("toonUsedPhone: av %s made NO progress" % (avId)) + return + + def recoverItems(self, av, cogList, zoneId): + avQuests = av.quests + avId = av.getDoId() + itemsRecovered = [] + itemsNotRecovered = [] + changed = 0 + + for questDesc in avQuests: + questClass = Quests.getQuestClass(questDesc[0]) + if (questClass == Quests.RecoverItemQuest): + quest = Quests.getQuest(questDesc[0]) + # See if the cog that stole the item is in the cogList + questCogType = quest.getHolder() + qualifier = quest.getHolderType() + for cogDict in cogList: + # If the cogType is Quests.Any, that means any cog + # Ok, now check to see if we recovered the item based + # on the percent chance of finding it stored in the quest + # Only find items if we still need them + self.notify.debug("recoverItems: checking against cogDict: %s" % (cogDict)) + if ((questCogType == Quests.Any) or + (questCogType == cogDict[qualifier]) or + # If it is level based, count those higher too + ((qualifier == 'level') and (questCogType <= cogDict[qualifier])) + ): + if avId in cogDict['activeToons']: + if not quest.testDone(questDesc[4]):#if questDesc[4] < quest.getNumItems(): + if quest.isLocationMatch(zoneId): + #rand = random.random() * 100 + #if rand <= quest.getPercentChance(): + check, count = quest.testRecover(questDesc[4]) + if check: + # FOUND IT! Increment progress by one item + #questDesc[4] += 1 + # Keep track of all the items recovered + itemsRecovered.append(quest.getItem()) + #changed = 1 + self.notify.debug("recoverItems: av %s made progress: %s" % (avId, questDesc[4])) + else: + self.notify.debug("recoverItems: av %s made NO progress (item not found) [%s > %s])" % (avId, check, quest.getPercentChance())) + itemsNotRecovered.append(quest.getItem()) + #keeping track of missed items + changed = 1 + questDesc[4] = count + else: + self.notify.debug("recoverItems: av %s made NO progress (wrong location)" % (avId)) + else: + self.notify.debug("recoverItems: av %s made NO progress (have enough already)" % (avId)) + else: + self.notify.debug("recoverItems: av %s made NO progress (av not active)" % (avId)) + else: + self.notify.debug("recoverItems: av %s made NO progress (wrong cog type)" % (avId)) + else: + # Do not care about this quest here + continue + + # Now send the quests back to the avatar if the status changed + + # Note: this means that an avatar will immediately get credit + # for finding an item, even if the item is found in the middle + # floor of a building and the avatar later is killed on a + # later floor, thus failing the building. + if changed: + av.b_setQuests(avQuests) + + return (itemsRecovered, itemsNotRecovered) + + def findItemInWater(self, av, zoneId): + # Similar to recoverItems, but this is called from the + # DistributedFishingSpot to see if there are any quest items + # in the water. No cogs are involved; hence, the only valid + # questCogType is Quests.AnyFish. + + # Only one item at a time is returned by this function; the + # function either returns the item found, or None. + # Note: this does not support two quests with same item + avQuests = av.quests + avId = av.getDoId() + + for questDesc in avQuests: + questClass = Quests.getQuestClass(questDesc[0]) + if (questClass == Quests.RecoverItemQuest): + quest = Quests.getQuest(questDesc[0]) + if ((quest.getType() == Quests.RecoverItemQuest) and + (quest.getHolder() == Quests.AnyFish) and + ((random.random() * 100) <= quest.getPercentChance()) and + (questDesc[4] < quest.getNumItems()) and + quest.isLocationMatch(zoneId) + ): + # FOUND IT! Increment progress by one item + questDesc[4] += 1 + self.notify.debug("findItemInWater: av %s made progress" % (avId)) + av.b_setQuests(avQuests) + # Return the item recovered + return quest.getItem() + else: + # Do not care about this quest here + continue + + self.notify.debug("findItemInWater: av %s made NO progress" % (avId)) + return None + + def completeAllQuestsMagically(self, av): + avQuests = av.quests + for quest in avQuests: + # Make sure the progress is really high so the quest will seem completed + quest[4] = 1000 + av.b_setQuests(avQuests) + return 1 + + def completeQuestMagically(self, av, index): + avQuests = av.quests + # Make sure we are in range + if index < len(av.quests): + # Make sure the progress is really high so the quest will seem completed + avQuests[index][4] = 1000 + av.b_setQuests(avQuests) + return 1 + else: + return 0 + + def toonMadeFriend(self, av, otherAv): + # This is notifying us that a toon has made a friend. + # See if this toon has a friend quest. + # If so, update the progress. + avQuests = av.quests + avId = av.getDoId() + changed = 0 + for questDesc in avQuests: + questClass = Quests.getQuestClass(questDesc[0]) + if ((questClass == Quests.FriendQuest) or + (questClass == Quests.FriendNewbieQuest)): + quest = Quests.getQuest(questDesc[0]) + if (quest.doesFriendCount(av, otherAv)): + # Set progress + questDesc[4] += 1 + changed = 1 + else: + # Do not care about this quest here + continue + # Now send the quests back to the avatar if the status changed + if changed: + self.notify.debug("toonMadeFriend: av %s made progress" % (avId)) + av.b_setQuests(avQuests) + else: + self.notify.debug("toonMadeFriend: av %s made NO progress" % (avId)) + + def hasTailorClothingTicket(self, av, npc): + for questDesc in av.quests: + questId, fromNpcId, toNpcId, rewardId, toonProgress = questDesc + questType = Quests.getQuestClass(questId) + # See if this NPC is the one we are supposed to deliver to + # You by definition have the item + if (questType == Quests.DeliverItemQuest): + if Quests.npcMatches(toNpcId, npc): + rewardId = Quests.getAvatarRewardId(av, questId) + rewardType = Quests.getRewardClass(rewardId) + if (rewardType == Quests.ClothingTicketReward): + return 1 + elif(rewardType == Quests.TIPClothingTicketReward): + return 2 + else: + # Reward was not a clothing ticket + continue + else: + # NPC does not match + continue + else: + # Not a deliver item quest + continue + # Did not find it, avId does not have clothing ticket on this tailor + return 0 + + def removeClothingTicket(self, av, npc): + for questDesc in av.quests: + questId, fromNpcId, toNpcId, rewardId, toonProgress = questDesc + questClass = Quests.getQuestClass(questId) + # See if this NPC is the one we are supposed to deliver to + # You by definition have the item + if (questClass == Quests.DeliverItemQuest): + if Quests.npcMatches(toNpcId, npc): + rewardId = Quests.getAvatarRewardId(av, questId) + rewardClass = Quests.getRewardClass(rewardId) + if (rewardClass == Quests.ClothingTicketReward or rewardClass == Quests.TIPClothingTicketReward): + # This section is much like completeQuest() + av.removeQuest(questId) + # Update the toon with the reward. This reward does nothing right + # now but it may in the future, so it is the right thing to do + reward = Quests.getReward(rewardId) + reward.sendRewardAI(av) + # Bump the reward + self.incrementReward(av) + return 1 + else: + # Reward was not a clothing ticket + continue + else: + # NPC does not match + continue + else: + # Not a deliver item quest + continue + # Did not find it, avId does not have clothing ticket on this tailor + return 0 + + def setNextQuest(self, avId, questId): + # for ~nextQuest: queue up a quest for this avatar + self.NextQuestDict[avId] = questId + + def cancelNextQuest(self, avId): + # cancel any pending quest for this avatar + oldQuest = self.NextQuestDict.get(avId) + if oldQuest: + del self.NextQuestDict[avId] + return oldQuest diff --git a/toontown/quest/QuestParser.py b/toontown/quest/QuestParser.py index fa3a797..493052b 100644 --- a/toontown/quest/QuestParser.py +++ b/toontown/quest/QuestParser.py @@ -5,6 +5,7 @@ import copy from direct.interval.IntervalGlobal import * from direct.directnotify import DirectNotifyGlobal from panda3d.core import * +from panda3d.otp import * from direct.showbase import DirectObject from . import BlinkingArrows from toontown.toon import ToonHeadFrame diff --git a/toontown/quest/Quests.py b/toontown/quest/Quests.py index 7f06e2d..655a25b 100644 --- a/toontown/quest/Quests.py +++ b/toontown/quest/Quests.py @@ -1743,11 +1743,10 @@ class TrackChoiceQuest(Quest): class FriendQuest(Quest): def filterFunc(avatar): - if len(avatar.getFriendsList()) == 0: + if not config.GetBool('skip-friend-quest', False) and len(avatar.getFriendsList()) == 0: return 1 else: return 0 - filterFunc = staticmethod(filterFunc) def __init__(self, id, quest): @@ -1889,6 +1888,13 @@ class MailboxQuest(Quest): class PhoneQuest(Quest): + def filterFunc(avatar): + if not config.GetBool('skip-phone-quest', False): + return 1 + else: + return 0 + filterFunc = staticmethod(filterFunc) + def __init__(self, id, quest): Quest.__init__(self, id, quest) @@ -2115,7 +2121,7 @@ QuestDict = { 'type'), ToonHQ, ToonHQ, - NA, + 100, 150, DefaultDialog), 150: (TT_TIER, @@ -2123,7 +2129,7 @@ QuestDict = { (FriendQuest,), Same, Same, - NA, + 100, 175, DefaultDialog), 160: (TT_TIER, diff --git a/toontown/spellbook/MagicWordIndex.py b/toontown/spellbook/MagicWordIndex.py index 773d52d..d0f9cd5 100644 --- a/toontown/spellbook/MagicWordIndex.py +++ b/toontown/spellbook/MagicWordIndex.py @@ -315,6 +315,30 @@ class RequestMinigame(MagicWord): retStr += f" with difficulty {mgDiff}" return retStr + "." +class Quests(MagicWord): + aliases = ["quest", "tasks", "task", "toontasks"] + desc = "Quest manupliation" + execLocation = MagicWordConfig.EXEC_LOC_SERVER + arguments = [("command", str, True), ("index", int, False, -1)] + + def handleWord(self, invoker, avId, toon, *args): + command = args[0] + index = args[1] + """ + Commands: + - "finish": Finish a task (sets the progress to 1000), finishes all by default + """ + if command == "finish": + if index == -1: + self.air.questManager.completeAllQuestsMagically(toon) + return "Finished all quests." + else: + if self.air.questManager.completeQuestMagically(toon, index): + return f"Finished quest {index}." + return f"Quest {index} not found. (Hint: Quest indexes start at 0)" + else: + return "Valid commands: \"finish\"" + # Instantiate all classes defined here to register them. # A bit hacky, but better than the old system for item in list(globals().values()): diff --git a/toontown/spellbook/ToontownMagicWordManagerAI.py b/toontown/spellbook/ToontownMagicWordManagerAI.py index 0973ff2..881caaf 100644 --- a/toontown/spellbook/ToontownMagicWordManagerAI.py +++ b/toontown/spellbook/ToontownMagicWordManagerAI.py @@ -77,11 +77,12 @@ class ToontownMagicWordManagerAI(DistributedObjectAI.DistributedObjectAI): self.notify.warning('requestExecuteMagicWord: Magic Word use requested but invoker avatar is non-existent!') return + # FIXME: # Same thing with the Toontorial. Magic Words are strictly forbidden here # Tell the user they can't use it because they're in the Toontorial - if hasattr(self.air, 'tutorialManager') and avId in list(self.air.tutorialManager.avId2fsm.keys()): - self.generateResponse(avId=avId, responseType="Tutorial") - return + # if hasattr(self.air, 'tutorialManager') and avId in list(self.air.tutorialManager.avId2fsm.keys()): + # self.generateResponse(avId=avId, responseType="Tutorial") + # return # Our Magic Word affectRange is either SELF (the invoker) or BOTH (invoker and a target) # Because of this, we should add the invoker to the target list diff --git a/toontown/suit/DistributedTutorialSuitAI.py b/toontown/suit/DistributedTutorialSuitAI.py index ead3ad0..c92d64d 100644 --- a/toontown/suit/DistributedTutorialSuitAI.py +++ b/toontown/suit/DistributedTutorialSuitAI.py @@ -1,5 +1,55 @@ -from direct.directnotify import DirectNotifyGlobal -from direct.distributed.DistributedObjectAI import DistributedObjectAI +from otp.ai.AIBaseGlobal import * -class DistributedTutorialSuitAI(DistributedObjectAI): - notify = DirectNotifyGlobal.directNotify.newCategory('DistributedTutorialSuitAI') +from direct.directnotify import DirectNotifyGlobal +from toontown.battle import SuitBattleGlobals +from . import DistributedSuitBaseAI + + +class DistributedTutorialSuitAI(DistributedSuitBaseAI.DistributedSuitBaseAI): + + notify = DirectNotifyGlobal.directNotify.newCategory( + 'DistributedTutorialSuitAI') + + def __init__(self, air, suitPlanner): + """__init__(air, suitPlanner)""" + DistributedSuitBaseAI.DistributedSuitBaseAI.__init__(self, air, + suitPlanner) + + def delete(self): + DistributedSuitBaseAI.DistributedSuitBaseAI.delete(self) + self.ignoreAll() + + def requestBattle(self, x, y, z, h, p, r): + """requestBattle(x, y, z, h, p, r) + """ + toonId = self.air.getAvatarIdFromSender() + + if self.notify.getDebug(): + self.notify.debug( str( self.getDoId() ) + \ + str( self.zoneId ) + \ + ': request battle with toon: %d' % toonId ) + + # Store the suit's actual pos and hpr on the client + self.confrontPos = Point3(x, y, z) + self.confrontHpr = Vec3(h, p, r) + + # Request a battle from the suit planner + if (self.sp.requestBattle(self.zoneId, self, toonId)): + self.acceptOnce(self.getDeathEvent(), self._logDeath, [toonId]) + if self.notify.getDebug(): + self.notify.debug( "Suit %d requesting battle in zone %d" % + (self.getDoId(), self.zoneId) ) + else: + # Suit tells toon to get lost + if self.notify.getDebug(): + self.notify.debug('requestBattle from suit %d - denied by battle manager' % (self.getDoId())) + self.b_setBrushOff(SuitDialog.getBrushOffIndex(self.getStyleName())) + self.d_denyBattle( toonId ) + + def getConfrontPosHpr(self): + """ getConfrontPosHpr() + """ + return (self.confrontPos, self.confrontHpr) + + def _logDeath(self, toonId): + self.air.writeServerEvent('beatFirstCog', toonId, '') \ No newline at end of file diff --git a/toontown/toon/DistributedToonAI.py b/toontown/toon/DistributedToonAI.py index bee965d..cf78958 100644 --- a/toontown/toon/DistributedToonAI.py +++ b/toontown/toon/DistributedToonAI.py @@ -3781,7 +3781,7 @@ class DistributedToonAI(DistributedPlayerAI.DistributedPlayerAI, DistributedSmoo paidStatus = simbase.config.GetString('force-paid-status', 'none') if paidStatus == 'unpaid': access = 1 - print('Setting Access %s' % access) + self.notify.debug('Setting Access %s' % access) if access == OTPGlobals.AccessInvalid: if not __dev__: self.air.writeServerEvent('Setting Access', self.doId, 'setAccess not being sent by the OTP Server, changing access to unpaid') diff --git a/toontown/town/Street.py b/toontown/town/Street.py index 743bcd1..cf2853e 100644 --- a/toontown/town/Street.py +++ b/toontown/town/Street.py @@ -64,7 +64,7 @@ class Street(BattlePlace.BattlePlace): State.State('WaitForBattle', self.enterWaitForBattle, self.exitWaitForBattle, ['battle', 'walk']), State.State('battle', self.enterBattle, self.exitBattle, ['walk', 'teleportOut', 'died']), State.State('doorIn', self.enterDoorIn, self.exitDoorIn, ['walk']), - State.State('doorOut', self.enterDoorOut, self.exitDoorOut, ['walk']), + State.State('doorOut', self.enterDoorOut, self.exitDoorOut, ['walk', 'stopped']), State.State('elevatorIn', self.enterElevatorIn, self.exitElevatorIn, ['walk']), State.State('elevator', self.enterElevator, self.exitElevator, ['walk']), State.State('trialerFA', self.enterTrialerFA, self.exitTrialerFA, ['trialerFAReject', 'DFA']), diff --git a/toontown/tutorial/DistributedBattleTutorialAI.py b/toontown/tutorial/DistributedBattleTutorialAI.py index 7d1b31f..8b38b35 100644 --- a/toontown/tutorial/DistributedBattleTutorialAI.py +++ b/toontown/tutorial/DistributedBattleTutorialAI.py @@ -1,5 +1,21 @@ +from toontown.battle import DistributedBattleAI from direct.directnotify import DirectNotifyGlobal -from direct.distributed.DistributedObjectAI import DistributedObjectAI -class DistributedBattleTutorialAI(DistributedObjectAI): +class DistributedBattleTutorialAI(DistributedBattleAI.DistributedBattleAI): notify = DirectNotifyGlobal.directNotify.newCategory('DistributedBattleTutorialAI') + + def __init__(self, air, battleMgr, pos, suit, toonId, zoneId, + finishCallback=None, maxSuits=4, interactivePropTrackBonus = -1): + """__init__(air, battleMgr, pos, suit, toonId, zoneId, + finishCallback, maxSuits) + """ + DistributedBattleAI.DistributedBattleAI.__init__( + self, air, battleMgr, pos, suit, toonId, zoneId, + finishCallback, maxSuits, tutorialFlag=1) + + # There is no timer in the tutorial... The reward movie is random length. + def startRewardTimer(self): + pass + + #def handleRewardDone(self): + # DistributedBattleAI.DistributedBattleAI.handleRewardDone(self) diff --git a/toontown/tutorial/SuitPlannerTutorialAI.py b/toontown/tutorial/SuitPlannerTutorialAI.py new file mode 100644 index 0000000..a5232a2 --- /dev/null +++ b/toontown/tutorial/SuitPlannerTutorialAI.py @@ -0,0 +1,74 @@ +""" SuitPlannerTutorial module: contains the SuitPlannerTutorial class + which handles management of the suit you will fight during the + tutorial.""" + +from otp.ai.AIBaseGlobal import * + +from direct.directnotify import DirectNotifyGlobal +from toontown.suit import DistributedTutorialSuitAI +from . import TutorialBattleManagerAI + +class SuitPlannerTutorialAI: + """ + SuitPlannerTutorialAI: manages the single suit that you fight during + the tutorial. + """ + + notify = DirectNotifyGlobal.directNotify.newCategory( + 'SuitPlannerTutorialAI') + + def __init__(self, air, zoneId, battleOverCallback): + # Store these things + self.zoneId = zoneId + self.air = air + self.battle = None + # This callback will be used to open the HQ doors when the + # battle is over. + self.battleOverCallback = battleOverCallback + + # Create a battle manager + self.battleMgr = TutorialBattleManagerAI.TutorialBattleManagerAI( + self.air) + + # Create a flunky + newSuit = DistributedTutorialSuitAI.DistributedTutorialSuitAI(self.air, self) + newSuit.setupSuitDNA(1, 1, "c") + # This is a special tutorial path state + newSuit.generateWithRequired(self.zoneId) + self.suit = newSuit + + def cleanup(self): + self.zoneId = None + self.air = None + if self.suit: + self.suit.requestDelete() + self.suit = None + if self.battle: + #self.battle.requestDelete() + #RAU made to kill the mem leak when you close the window in the middle of the battle tutorial + cellId = self.battle.battleCellId + battleMgr = self.battle.battleMgr + if cellId in battleMgr.cellId2battle: + battleMgr.destroy(self.battle) + + self.battle = None + + def getDoId(self): + # This is here because the suit expects the suit planner to be + # a distributed object, if it has a suit planner. We want it to + # have a suit planner, but not a distributed one, so we return + # 0 when asked what our DoId is. Kind of hackful, I guess. + return 0 + + def requestBattle(self, zoneId, suit, toonId): + # 70, 20, 0 is a battle cell position that I just made up. + self.battle = self.battleMgr.newBattle( + zoneId, zoneId, Vec3(35, 20, 0), + suit, toonId, + finishCallback=self.battleOverCallback) + return 1 + + def removeSuit(self, suit): + # Get rid of the suit. + suit.requestDelete() + self.suit = None diff --git a/toontown/tutorial/TutorialBattleManagerAI.py b/toontown/tutorial/TutorialBattleManagerAI.py new file mode 100644 index 0000000..b19f20e --- /dev/null +++ b/toontown/tutorial/TutorialBattleManagerAI.py @@ -0,0 +1,11 @@ +from toontown.battle import BattleManagerAI +from direct.directnotify import DirectNotifyGlobal +from . import DistributedBattleTutorialAI + +class TutorialBattleManagerAI(BattleManagerAI.BattleManagerAI): + + notify = DirectNotifyGlobal.directNotify.newCategory('TutorialBattleManagerAI') + + def __init__(self, air): + BattleManagerAI.BattleManagerAI.__init__(self, air) + self.battleConstructor = DistributedBattleTutorialAI.DistributedBattleTutorialAI diff --git a/toontown/tutorial/TutorialManagerAI.py b/toontown/tutorial/TutorialManagerAI.py index 0df746c..ae89ac1 100644 --- a/toontown/tutorial/TutorialManagerAI.py +++ b/toontown/tutorial/TutorialManagerAI.py @@ -1,5 +1,322 @@ +from otp.ai.AIBaseGlobal import * +from panda3d.core import * +from panda3d.toontown import * +from direct.distributed import DistributedObjectAI from direct.directnotify import DirectNotifyGlobal -from direct.distributed.DistributedObjectAI import DistributedObjectAI +from toontown.building import TutorialBuildingAI +from toontown.building import TutorialHQBuildingAI +from . import SuitPlannerTutorialAI +from toontown.toonbase import ToontownBattleGlobals +from toontown.toon import NPCToons +from toontown.ai import BlackCatHolidayMgrAI +from toontown.ai import DistributedBlackCatMgrAI -class TutorialManagerAI(DistributedObjectAI): - notify = DirectNotifyGlobal.directNotify.newCategory('TutorialManagerAI') +class TutorialManagerAI(DistributedObjectAI.DistributedObjectAI): + notify = DirectNotifyGlobal.directNotify.newCategory("TutorialManagerAI") + + # how many seconds do we wait for the toon to appear on AI before we + # nuke his skip tutorial request + WaitTimeForSkipTutorial = 5.0 + + def __init__(self, air): + DistributedObjectAI.DistributedObjectAI.__init__(self, air) + # This is a dictionary of all the players who are currently in + # tutorials. We need to create things when someone requests + # a tutorial, and destroy them when they leave. + self.playerDict = {} + + # There are only two blocks in the tutorial. One for the gag shop + # building, and one for the Toon HQ. If there aren't, something + # is wrong. + self.dnaStore = DNAStorage() + + dnaFile = simbase.air.lookupDNAFileName("tutorial_street.dna") + self.air.loadDNAFileAI(self.dnaStore, dnaFile) + numBlocks = self.dnaStore.getNumBlockNumbers() + assert numBlocks == 2 + # Assumption: the only block that isn't an HQ is the gag shop block. + self.hqBlock = None + self.gagBlock = None + for blockIndex in range (0, numBlocks): + blockNumber = self.dnaStore.getBlockNumberAt(blockIndex) + buildingType = self.dnaStore.getBlockBuildingType(blockNumber) + if (buildingType == 'hq'): + self.hqBlock = blockNumber + else: + self.gagBlock = blockNumber + + assert self.hqBlock and self.gagBlock + + # key is avId, value is real time when the request was made + self.avIdsRequestingSkip = {} + self.accept("avatarEntered", self.waitingToonEntered ) + + return None + + def requestTutorial(self): + # TODO: possible security breach: what if client is repeatedly + # requesting tutorial? can client request tutorial from playground? + # can client request tutorial if hp is at least 16? How do we + # handle these cases? + avId = self.air.getAvatarIdFromSender() + # Handle unexpected exits + self.acceptOnce(self.air.getAvatarExitEvent(avId), + self.__handleUnexpectedExit, + extraArgs=[avId]) + # allocate tutorial objects and zones + zoneDict = self.__createTutorial(avId) + # Tell the player to enter the zone + self.d_enterTutorial(avId, + zoneDict["branchZone"], + zoneDict["streetZone"], + zoneDict["shopZone"], + zoneDict["hqZone"] + ) + self.air.writeServerEvent('startedTutorial', avId, '') + + def toonArrived(self): + avId = self.air.getAvatarIdFromSender() + # Make sure the avatar exists + av = self.air.doId2do.get(avId) + # Clear out the avatar's quests, hp, inventory, and everything else in case + # he made it half way through the tutorial last time. + if av: + # No quests + av.b_setQuests([]) + av.b_setQuestHistory([]) + av.b_setRewardHistory(0, []) + av.b_setQuestCarryLimit(1) + # Starting HP + av.b_setMaxHp(15) + av.b_setHp(15) + # No exp + av.experience.zeroOutExp() + av.d_setExperience(av.experience.makeNetString()) + # One cupcake and one squirting flower + av.inventory.zeroInv() + av.inventory.addItem(ToontownBattleGlobals.THROW_TRACK, 0) + av.inventory.addItem(ToontownBattleGlobals.SQUIRT_TRACK, 0) + av.d_setInventory(av.inventory.makeNetString()) + # No cogs defeated + av.b_setCogStatus([1] * 32) + av.b_setCogCount([0] * 32) + return + + def allDone(self): + avId = self.air.getAvatarIdFromSender() + # No need to worry further about unexpected exits + self.ignore(self.air.getAvatarExitEvent(avId)) + # Make sure the avatar exists + av = self.air.doId2do.get(avId) + if av: + self.air.writeServerEvent('finishedTutorial', avId, '') + av.b_setTutorialAck(1) + self.__destroyTutorial(avId) + else: + self.notify.warning( + "Toon " + + str(avId) + + " isn't here, but just finished a tutorial. " + + "I will ignore this." + ) + return + + def __createTutorial(self, avId): + if self.playerDict.get(avId): + self.notify.warning(str(avId) + " is already in the playerDict!") + + branchZone = self.air.allocateZone() + streetZone = self.air.allocateZone() + shopZone = self.air.allocateZone() + hqZone = self.air.allocateZone() + # Create a building object + building = TutorialBuildingAI.TutorialBuildingAI(self.air, + streetZone, + shopZone, + self.gagBlock) + # Create an HQ object + hqBuilding = TutorialHQBuildingAI.TutorialHQBuildingAI(self.air, + streetZone, + hqZone, + self.hqBlock) + + def battleOverCallback(zoneId): + hqBuilding.battleOverCallback() + building.battleOverCallback() + + # Create a suit planner + suitPlanner = SuitPlannerTutorialAI.SuitPlannerTutorialAI( + self.air, + streetZone, + battleOverCallback) + + # Create the NPC blocking the tunnel to the playground + blockerNPC = NPCToons.createNPC(self.air, 20001, NPCToons.NPCToonDict[20001], streetZone, + questCallback=self.__handleBlockDone) + blockerNPC.setTutorial(1) + + # is the black cat holiday enabled? + blackCatMgr = None + if bboard.has(BlackCatHolidayMgrAI.BlackCatHolidayMgrAI.PostName): + blackCatMgr = DistributedBlackCatMgrAI.DistributedBlackCatMgrAI( + self.air, avId) + blackCatMgr.generateWithRequired(streetZone) + + zoneDict={"branchZone" : branchZone, + "streetZone" : streetZone, + "shopZone" : shopZone, + "hqZone" : hqZone, + "building" : building, + "hqBuilding" : hqBuilding, + "suitPlanner" : suitPlanner, + "blockerNPC" : blockerNPC, + "blackCatMgr" : blackCatMgr, + } + self.playerDict[avId] = zoneDict + return zoneDict + + def __handleBlockDone(self): + return None + + def __destroyTutorial(self, avId): + zoneDict = self.playerDict.get(avId) + if zoneDict: + zoneDict["building"].cleanup() + zoneDict["hqBuilding"].cleanup() + zoneDict["blockerNPC"].requestDelete() + if zoneDict["blackCatMgr"]: + zoneDict["blackCatMgr"].requestDelete() + self.air.deallocateZone(zoneDict["branchZone"]) + self.air.deallocateZone(zoneDict["streetZone"]) + self.air.deallocateZone(zoneDict["shopZone"]) + self.air.deallocateZone(zoneDict["hqZone"]) + zoneDict["suitPlanner"].cleanup() + del self.playerDict[avId] + else: + self.notify.warning("Tried to deallocate zones for " + + str(avId) + + " but none were present in playerDict.") + + def rejectTutorial(self): + avId = self.air.getAvatarIdFromSender() + # Make sure the avatar exists + av = self.air.doId2do.get(avId) + if av: + # Acknowlege that the player has seen a tutorial + self.air.writeServerEvent('finishedTutorial', avId, '') + av.b_setTutorialAck(1) + + self.sendUpdateToAvatarId(avId, "skipTutorialResponse", [1]) + else: + self.notify.warning( + "Toon " + + str(avId) + + " isn't here, but just rejected a tutorial. " + + "I will ignore this." + ) + return + + def respondToSkipTutorial(self, avId, av): + """Reply to the client if we let him skip the tutorial.""" + self.notify.debugStateCall(self) + assert avId + assert av + response = 1 + if av: + if av.tutorialAck: + self.air.writeServerEvent('suspicious', avId, 'requesting skip tutorial, but tutorialAck is 1') + response = 0 + + if av and response: + # Acknowlege that the player has seen a tutorial + self.air.writeServerEvent('skippedTutorial', avId, '') + av.b_setTutorialAck(1) + # these values were taken by running a real tutorial + self.air.questManager.assignQuest(avId, + 20000, + 101, + 100, + 1000, + 1 + ) + + self.air.questManager.completeAllQuestsMagically(av) + av.removeQuest(101) + self.air.questManager.assignQuest(avId, + 1000, + 110, + 2, + 1000, + 0 + ) + self.air.questManager.completeAllQuestsMagically(av) + + # do whatever needs to be done to make his quest state good + elif av: + self.notify.debug("%s requestedSkipTutorial, but tutorialAck is 1") + else: + response = 0 + self.notify.warning( + "Toon " + + str(avId) + + " isn't here, but requested to skip tutorial. " + + "I will ignore this." + ) + self.sendUpdateToAvatarId(avId, "skipTutorialResponse", [response]) + return + + def waitingToonEntered(self, av): + """Check if the avatar is someone who's requested to skip, then proceed accordingly.""" + avId = av.doId + if avId in self.avIdsRequestingSkip: + requestTime = self.avIdsRequestingSkip[avId] + + curTime = globalClock.getFrameTime() + if (curTime - requestTime) <= self.WaitTimeForSkipTutorial: + self.respondToSkipTutorial(avId, av) + else: + self.notify.warning("waited too long for toon %d responding no to skip tutorial request" % avId) + self.sendUpdateToAvatarId(avId, "skipTutorialResponse", [0]) + del self.avIdsRequestingSkip[avId] + self.removeTask("skipTutorialToon-%d" % avId) + + + def waitForToonToEnter(self,avId): + """Mark our toon as requesting to skip, and start a task to timeout for it.""" + self.notify.debugStateCall(self) + self.avIdsRequestingSkip[avId] = globalClock.getFrameTime() + self.doMethodLater(self.WaitTimeForSkipTutorial, self.didNotGetToon, "skipTutorialToon-%d" % avId, [avId]) + + def didNotGetToon(self, avId): + """Just say no since the AI didn't get it.""" + self.notify.debugStateCall(self) + if avId in self.avIdsRequestingSkip: + del self.avIdsRequestingSkip[avId] + self.sendUpdateToAvatarId(avId, "skipTutorialResponse", [0]) + return Task.done + + def requestSkipTutorial(self): + """We are requesting to skip tutorial, add other quest history to be consistent.""" + self.notify.debugStateCall(self) + avId = self.air.getAvatarIdFromSender() + # Make sure the avatar exists + av = self.air.doId2do.get(avId) + if av: + self.respondToSkipTutorial(avId,av) + else: + self.waitForToonToEnter(avId) + + def d_enterTutorial(self, avId, branchZone, streetZone, shopZone, hqZone): + self.sendUpdateToAvatarId(avId, "enterTutorial", [branchZone, + streetZone, + shopZone, + hqZone]) + return + + def __handleUnexpectedExit(self, avId): + self.notify.warning("Avatar: " + str(avId) + + " has exited unexpectedly") + self.__destroyTutorial(avId) + return + + \ No newline at end of file