class DistributedSmoothNode(DistributedNode.DistributedNode, DistributedSmoothNodeBase.DistributedSmoothNodeBase): """ This specializes DistributedNode to add functionality to smooth motion over time, via the SmoothMover C++ object defined in DIRECT. """ def __init__(self, cr): if not hasattr(self, 'DistributedSmoothNode_initialized'): self.DistributedSmoothNode_initialized = 1 DistributedNode.DistributedNode.__init__(self, cr) DistributedSmoothNodeBase.DistributedSmoothNodeBase.__init__(self) self.smoothStarted = 0 # Set this True to assert that the local process has # complete authority over the position of this object when # smoothing is not in effect. When this is True, position # reports received over the wire will not be applied to # this node's position, unless those position reports are # received between startSmooth() and endSmooth(). self.localControl = False # flag set when we receive a stop message self.stopped = False def generate(self): self.smoother = SmoothMover() self.smoothStarted = 0 self.lastSuggestResync = 0 self._smoothWrtReparents = False DistributedNode.DistributedNode.generate(self) DistributedSmoothNodeBase.DistributedSmoothNodeBase.generate(self) self.cnode.setRepository(self.cr, 0, 0) self.activateSmoothing(GlobalSmoothing, GlobalPrediction) # clear stopped flag for re-generate self.stopped = False def disable(self): DistributedSmoothNodeBase.DistributedSmoothNodeBase.disable(self) DistributedNode.DistributedNode.disable(self) del self.smoother def delete(self): DistributedSmoothNodeBase.DistributedSmoothNodeBase.delete(self) DistributedNode.DistributedNode.delete(self) ### Methods to handle computing and updating of the smoothed ### position. def smoothPosition(self): """ This function updates the position of the node to its computed smoothed position. This may be overridden by a derived class to specialize the behavior. """ self.smoother.computeAndApplySmoothPosHpr(self, self) def doSmoothTask(self, task): self.smoothPosition() return cont def wantsSmoothing(self): # Override this function to return 0 if this particular kind # of smooth node doesn't really want to be smoothed. return 1 def startSmooth(self): """ This function starts the task that ensures the node is positioned correctly every frame. However, while the task is running, you won't be able to lerp the node or directly position it. """ if not self.wantsSmoothing() or self.isDisabled() or self.isLocal(): return if not self.smoothStarted: taskName = self.taskName("smooth") taskMgr.remove(taskName) self.reloadPosition() taskMgr.add(self.doSmoothTask, taskName) self.smoothStarted = 1 def stopSmooth(self): """ This function stops the task spawned by startSmooth(), and allows show code to move the node around directly. """ if self.smoothStarted: taskName = self.taskName("smooth") taskMgr.remove(taskName) self.forceToTruePosition() self.smoothStarted = 0 def setSmoothWrtReparents(self, flag): self._smoothWrtReparents = flag def getSmoothWrtReparents(self): return self._smoothWrtReparents def forceToTruePosition(self): """ This forces the node to reposition itself to its latest known position. This may result in a pop as the node skips the last of its lerp points. """ #printStack() if (not self.isLocal()) and \ self.smoother.getLatestPosition(): self.smoother.applySmoothPosHpr(self, self) self.smoother.clearPositions(1) def reloadPosition(self): """ This function re-reads the position from the node itself and clears any old position reports for the node. This should be used whenever show code bangs on the node position and expects it to stick. """ self.smoother.clearPositions(0) self.smoother.setPosHpr(self.getPos(), self.getHpr()) self.smoother.setPhonyTimestamp() self.smoother.markPosition() def _checkResume(self, timestamp): """ Determine if we were previously stopped and now need to resume movement by making sure any old stored positions reflect the node's current position """ if self.stopped: currTime = ClockObject.getGlobalClock().getFrameTime() now = currTime - self.smoother.getExpectedBroadcastPeriod() last = self.smoother.getMostRecentTimestamp() if now > last: # only set a new timestamp postion if we still have # a position being smoothed to (so we don't interrupt # any current smoothing and only do this if the object # is actually locally stopped) if timestamp is None: # no timestamp, use current time local = 0.0 else: local = globalClockDelta.networkToLocalTime( timestamp, currTime) self.smoother.setPhonyTimestamp(local,True) self.smoother.markPosition() self.stopped = False # distributed set pos and hpr functions # 'send' versions are inherited from DistributedSmoothNodeBase def setSmStop(self, timestamp=None): self.setComponentTLive(timestamp) self.stopped = True def setSmH(self, h, timestamp=None): self._checkResume(timestamp) self.setComponentH(h) self.setComponentTLive(timestamp) def setSmZ(self, z, timestamp=None): self._checkResume(timestamp) self.setComponentZ(z) self.setComponentTLive(timestamp) def setSmXY(self, x, y, timestamp=None): self._checkResume(timestamp) self.setComponentX(x) self.setComponentY(y) self.setComponentTLive(timestamp) def setSmXZ(self, x, z, timestamp=None): self._checkResume(timestamp) self.setComponentX(x) self.setComponentZ(z) self.setComponentTLive(timestamp) def setSmPos(self, x, y, z, timestamp=None): self._checkResume(timestamp) self.setComponentX(x) self.setComponentY(y) self.setComponentZ(z) self.setComponentTLive(timestamp) def setSmHpr(self, h, p, r, timestamp=None): self._checkResume(timestamp) self.setComponentH(h) self.setComponentP(p) self.setComponentR(r) self.setComponentTLive(timestamp) def setSmXYH(self, x, y, h, timestamp): self._checkResume(timestamp) self.setComponentX(x) self.setComponentY(y) self.setComponentH(h) self.setComponentTLive(timestamp) def setSmXYZH(self, x, y, z, h, timestamp=None): self._checkResume(timestamp) self.setComponentX(x) self.setComponentY(y) self.setComponentZ(z) self.setComponentH(h) self.setComponentTLive(timestamp) def setSmPosHpr(self, x, y, z, h, p, r, timestamp=None): self._checkResume(timestamp) self.setComponentX(x) self.setComponentY(y) self.setComponentZ(z) self.setComponentH(h) self.setComponentP(p) self.setComponentR(r) self.setComponentTLive(timestamp) def setSmPosHprL(self, l, x, y, z, h, p, r, timestamp=None): self._checkResume(timestamp) self.setComponentL(l) self.setComponentX(x) self.setComponentY(y) self.setComponentZ(z) self.setComponentH(h) self.setComponentP(p) self.setComponentR(r) self.setComponentTLive(timestamp) ### component set pos and hpr functions ### ### These are the component functions that are invoked ### remotely by the above composite functions. @report(types = ['args'], dConfigParam = 'smoothnode') def setComponentX(self, x): self.smoother.setX(x) @report(types = ['args'], dConfigParam = 'smoothnode') def setComponentY(self, y): self.smoother.setY(y) @report(types = ['args'], dConfigParam = 'smoothnode') def setComponentZ(self, z): self.smoother.setZ(z) @report(types = ['args'], dConfigParam = 'smoothnode') def setComponentH(self, h): self.smoother.setH(h) @report(types = ['args'], dConfigParam = 'smoothnode') def setComponentP(self, p): self.smoother.setP(p) @report(types = ['args'], dConfigParam = 'smoothnode') def setComponentR(self, r): self.smoother.setR(r) @report(types = ['args'], dConfigParam = 'smoothnode') def setComponentL(self, l): if l != self.zoneId: # only perform set location if location is different self.setLocation(self.parentId,l) @report(types = ['args'], dConfigParam = 'smoothnode') def setComponentT(self, timestamp): # This is a little bit hacky. If *this* function is called, # it must have been called directly by the server, for # instance to update the values previously set for some avatar # that was already into the zone as we entered. (A live # update would have gone through the function called # setComponentTLive, below.) # Since we know this update came through the server, it may # reflect very old data. Thus, we can't accurately decode the # network timestamp (since the network time encoding can only # represent a time up to about 5 minutes in the past), but we # don't really need to know the timestamp anyway. We'll just # arbitrarily place it at right now. self.smoother.setPhonyTimestamp() self.smoother.clearPositions(1) self.smoother.markPosition() # mark position only takes most recent position sent over the wire # and applies it to the smoother's sample points, but we still # need to make sure and apply that position to the actual node # path self.forceToTruePosition() @report(types = ['args'], dConfigParam = 'smoothnode') def setComponentTLive(self, timestamp): # This is the variant of setComponentT() that will be called # whenever we receive a live update directly from the other # client. This is because the component functions, above, # call this function explicitly instead of setComponentT(). #print 'setComponentTLive: %s' % timestamp if timestamp is None: # if no timestamp, re-use the most recent timestamp to keep things # from getting out of order if self.smoother.hasMostRecentTimestamp(): self.smoother.setTimestamp(self.smoother.getMostRecentTimestamp()) else: # no most-recent timestamp, use current time self.smoother.setPhonyTimestamp() self.smoother.markPosition() else: globalClock = ClockObject.getGlobalClock() now = globalClock.getFrameTime() local = globalClockDelta.networkToLocalTime(timestamp, now) realTime = globalClock.getRealTime() chug = realTime - now # Sanity check the timestamp from the other avatar. It should # be just slightly in the past, but it might be off by as much # as this frame's amount of time forward or back. howFarFuture = local - now if howFarFuture - chug >= MaxFuture.value: # Too far off; advise the other client of our clock information. if globalClockDelta.getUncertainty() is not None and \ realTime - self.lastSuggestResync >= MinSuggestResync.value and \ hasattr(self.cr, 'localAvatarDoId'): self.lastSuggestResync = realTime timestampB = globalClockDelta.localToNetworkTime(realTime) serverTime = realTime - globalClockDelta.getDelta() assert self.notify.info( "Suggesting resync for %s, with discrepency %s; local time is %s and server time is %s." % ( self.doId, howFarFuture - chug, realTime, serverTime)) self.d_suggestResync( self.cr.localAvatarDoId, timestamp, timestampB, serverTime, globalClockDelta.getUncertainty()) self.smoother.setTimestamp(local) self.smoother.markPosition() if not self.localControl and not self.smoothStarted and \ self.smoother.getLatestPosition(): self.smoother.applySmoothPosHpr(self, self) # These are all required by the CMU server, which requires get* to # match set* in more cases than the Disney server does. def getComponentL(self): return self.zoneId def getComponentX(self): return self.getX() def getComponentY(self): return self.getY() def getComponentZ(self): return self.getZ() def getComponentH(self): return self.getH() def getComponentP(self): return self.getP() def getComponentR(self): return self.getR() def getComponentT(self): return 0 @report(types = ['args'], dConfigParam = 'smoothnode') def clearSmoothing(self, bogus = None): # Call this to invalidate all the old position reports # (e.g. just before popping to a new position). #printStack() self.smoother.clearPositions(1) @report(types = ['args'], dConfigParam = 'smoothnode') def wrtReparentTo(self, parent): # We override this NodePath method to force it to # automatically reset the smoothing position when we call it. if self.smoothStarted: if self._smoothWrtReparents: #print self.getParent(), parent, self.getParent().getPos(parent) self.smoother.handleWrtReparent(self.getParent(), parent) NodePath.wrtReparentTo(self, parent) else: self.forceToTruePosition() NodePath.wrtReparentTo(self, parent) self.reloadPosition() else: NodePath.wrtReparentTo(self, parent) @report(types = ['args'], dConfigParam = 'smoothnode') def d_setParent(self, parentToken): # We override this DistributedNode method to force a full position # update immediately after the distributed setParent is sent. # See ParentMgr.py for an explanation. DistributedNode.DistributedNode.d_setParent(self, parentToken) self.forceToTruePosition() self.sendCurrentPosition() ### Monitor clock sync ### def d_suggestResync(self, avId, timestampA, timestampB, serverTime, uncertainty): serverTimeSec = math.floor(serverTime) serverTimeUSec = (serverTime - serverTimeSec) * 10000.0 self.sendUpdate("suggestResync", [avId, timestampA, timestampB, serverTimeSec, serverTimeUSec, uncertainty]) def suggestResync(self, avId, timestampA, timestampB, serverTimeSec, serverTimeUSec, uncertainty): """ This message is sent from one client to another when the other client receives a timestamp from this client that is so far out of date as to suggest that one or both clients needs to resynchronize their clock information. """ serverTime = float(serverTimeSec) + float(serverTimeUSec) / 10000.0 result = self.peerToPeerResync( avId, timestampA, serverTime, uncertainty) if result >= 0 and \ globalClockDelta.getUncertainty() is not None: other = self.cr.doId2do.get(avId) if not other: assert self.notify.info( "Warning: couldn't find the avatar %d" % (avId)) elif hasattr(other, "d_returnResync") and \ hasattr(self.cr, 'localAvatarDoId'): realTime = globalClock.getRealTime() serverTime = realTime - globalClockDelta.getDelta() assert self.notify.info( "Returning resync for %s; local time is %s and server time is %s." % ( self.doId, realTime, serverTime)) other.d_returnResync( self.cr.localAvatarDoId, timestampB, serverTime, globalClockDelta.getUncertainty()) def d_returnResync(self, avId, timestampB, serverTime, uncertainty): serverTimeSec = math.floor(serverTime) serverTimeUSec = (serverTime - serverTimeSec) * 10000.0 self.sendUpdate("returnResync", [ avId, timestampB, serverTimeSec, serverTimeUSec, uncertainty]) def returnResync(self, avId, timestampB, serverTimeSec, serverTimeUSec, uncertainty): """ A reply sent by a client whom we recently sent suggestResync to, this reports the client's new delta information so we can adjust our clock as well. """ serverTime = float(serverTimeSec) + float(serverTimeUSec) / 10000.0 self.peerToPeerResync(avId, timestampB, serverTime, uncertainty) def peerToPeerResync(self, avId, timestamp, serverTime, uncertainty): gotSync = globalClockDelta.peerToPeerResync( avId, timestamp, serverTime, uncertainty) # If we didn't get anything useful from the other client, # maybe our clock is just completely hosed. Go ask the AI. if not gotSync: if self.cr.timeManager is not None: self.cr.timeManager.synchronize("suggested by %d" % (avId)) return gotSync def activateSmoothing(self, smoothing, prediction): """ Enables or disables the smoothing of other avatars' motion. This used to be a global flag, but now it is specific to each avatar instance. However, see globalActivateSmoothing() in this module. If smoothing is off, no kind of smoothing will be performed, regardless of the setting of prediction. This is not necessarily predictive smoothing; if predictive smoothing is off, avatars will be lagged by a certain factor to achieve smooth motion. Otherwise, if predictive smoothing is on, avatars will be drawn as nearly as possible in their current position, by extrapolating from old position reports. This assumes you have a client repository that knows its localAvatarDoId -- stored in self.cr.localAvatarDoId """ if smoothing and EnableSmoothing: if prediction and EnablePrediction: # Prediction and smoothing. self.smoother.setSmoothMode(SmoothMover.SMOn) self.smoother.setPredictionMode(SmoothMover.PMOn) self.smoother.setDelay(PredictionLag.value) else: # Smoothing, but no prediction. self.smoother.setSmoothMode(SmoothMover.SMOn) self.smoother.setPredictionMode(SmoothMover.PMOff) self.smoother.setDelay(Lag.value) else: # No smoothing, no prediction. self.smoother.setSmoothMode(SmoothMover.SMOff) self.smoother.setPredictionMode(SmoothMover.PMOff) self.smoother.setDelay(0.0)
class DistributedCashbotBossCrane(DistributedObject.DistributedObject, FSM.FSM): notify = DirectNotifyGlobal.directNotify.newCategory( 'DistributedCashbotBossCrane') firstMagnetBit = 21 craneMinY = 8 craneMaxY = 25 armMinH = -45 armMaxH = 45 shadowOffset = 1 emptyFrictionCoef = 0.1 emptySlideSpeed = 10 emptyRotateSpeed = 20 lookAtPoint = Point3(0.3, 0, 0.1) lookAtUp = Vec3(0, -1, 0) neutralStickHinge = VBase3(0, 90, 0) def __init__(self, cr): DistributedObject.DistributedObject.__init__(self, cr) FSM.FSM.__init__(self, 'DistributedCashbotBossCrane') self.boss = None self.index = None self.avId = 0 self.cableLength = 20 self.numLinks = 3 self.initialArmPosition = (0, 20, 0) self.slideSpeed = self.emptySlideSpeed self.rotateSpeed = self.emptyRotateSpeed self.changeSeq = 0 self.lastChangeSeq = 0 self.moveSound = None self.links = [] self.activeLinks = [] self.collisions = NodePathCollection() self.physicsActivated = 0 self.snifferActivated = 0 self.magnetOn = 0 self.root = NodePath('root') self.hinge = self.root.attachNewNode('hinge') self.hinge.setPos(0, -17.6, 38.5) self.controls = self.root.attachNewNode('controls') self.controls.setPos(0, -4.9, 0) self.arm = self.hinge.attachNewNode('arm') self.crane = self.arm.attachNewNode('crane') self.cable = self.hinge.attachNewNode('cable') self.topLink = self.crane.attachNewNode('topLink') self.topLink.setPos(0, 0, -1) self.shadow = None self.p0 = Point3(0, 0, 0) self.v1 = Vec3(1, 1, 1) self.armSmoother = SmoothMover() self.armSmoother.setSmoothMode(SmoothMover.SMOn) self.linkSmoothers = [] self.smoothStarted = 0 self.__broadcastPeriod = 0.2 self.cable.node().setFinal(1) self.crane.setPos(*self.initialArmPosition) self.heldObject = None self.closeButton = None self.craneAdviceLabel = None self.magnetAdviceLabel = None self.atLimitSfx = base.loadSfx( 'phase_4/audio/sfx/MG_cannon_adjust.mp3') self.magnetOnSfx = base.loadSfx( 'phase_10/audio/sfx/CBHQ_CFO_magnet_on.mp3') self.magnetLoopSfx = base.loadSfx( 'phase_10/audio/sfx/CBHQ_CFO_magnet_loop.wav') self.magnetSoundInterval = Parallel( SoundInterval(self.magnetOnSfx), Sequence(Wait(0.5), Func(base.playSfx, self.magnetLoopSfx, looping=1))) self.craneMoveSfx = base.loadSfx( 'phase_9/audio/sfx/CHQ_FACT_elevator_up_down.mp3') self.fadeTrack = None return def announceGenerate(self): DistributedObject.DistributedObject.announceGenerate(self) self.name = 'crane-%s' % self.doId self.root.setName(self.name) self.root.setPosHpr( *ToontownGlobals.CashbotBossCranePosHprs[self.index]) self.rotateLinkName = self.uniqueName('rotateLink') self.snifferEvent = self.uniqueName('sniffer') self.triggerName = self.uniqueName('trigger') self.triggerEvent = 'enter%s' % self.triggerName self.shadowName = self.uniqueName('shadow') self.flickerName = self.uniqueName('flicker') self.smoothName = self.uniqueName('craneSmooth') self.posHprBroadcastName = self.uniqueName('craneBroadcast') self.craneAdviceName = self.uniqueName('craneAdvice') self.magnetAdviceName = self.uniqueName('magnetAdvice') self.controlModel = self.boss.controls.copyTo(self.controls) self.cc = NodePath('cc') column = self.controlModel.find('**/column') column.getChildren().reparentTo(self.cc) self.cc.reparentTo(column) self.stickHinge = self.cc.attachNewNode('stickHinge') self.stick = self.boss.stick.copyTo(self.stickHinge) self.stickHinge.setHpr(self.neutralStickHinge) self.stick.setHpr(0, -90, 0) self.stick.flattenLight() self.bottom = self.controlModel.find('**/bottom') self.bottom.wrtReparentTo(self.cc) self.bottomPos = self.bottom.getPos() cs = CollisionSphere(0, -5, -2, 3) cs.setTangible(0) cn = CollisionNode(self.triggerName) cn.addSolid(cs) cn.setIntoCollideMask(OTPGlobals.WallBitmask) self.trigger = self.root.attachNewNode(cn) self.trigger.stash() cs = CollisionTube(0, 2.7, 0, 0, 2.7, 3, 1.2) cn = CollisionNode('tube') cn.addSolid(cs) cn.setIntoCollideMask(OTPGlobals.WallBitmask) self.tube = self.controlModel.attachNewNode(cn) cs = CollisionSphere(0, 0, 2, 3) cn = CollisionNode('safetyBubble') cn.addSolid(cs) cn.setIntoCollideMask(ToontownGlobals.PieBitmask) self.controls.attachNewNode(cn) arm = self.boss.craneArm.copyTo(self.crane) self.boss.cranes[self.index] = self def disable(self): DistributedObject.DistributedObject.disable(self) del self.boss.cranes[self.index] self.cleanup() def cleanup(self): if self.state != 'Off': self.demand('Off') self.boss = None return def accomodateToon(self, toon): origScale = self.controlModel.getSz() origCcPos = self.cc.getPos() origBottomPos = self.bottom.getPos() origStickHingeHpr = self.stickHinge.getHpr() scale = toon.getGeomNode().getChild(0).getSz(render) self.controlModel.setScale(scale) self.cc.setPos(0, 0, 0) toon.setPosHpr(self.controls, 0, 0, 0, 0, 0, 0) toon.pose('leverNeutral', 0) toon.update() pos = toon.rightHand.getPos(self.cc) self.cc.setPos(pos[0], pos[1], pos[2] - 1) self.bottom.setZ(toon, 0.0) self.bottom.setPos(self.bottomPos[0], self.bottomPos[1], self.bottom.getZ()) self.stickHinge.lookAt(toon.rightHand, self.lookAtPoint, self.lookAtUp) lerpTime = 0.5 return Parallel( self.controlModel.scaleInterval(lerpTime, scale, origScale, blendType='easeInOut'), self.cc.posInterval(lerpTime, self.cc.getPos(), origCcPos, blendType='easeInOut'), self.bottom.posInterval(lerpTime, self.bottom.getPos(), origBottomPos, blendType='easeInOut'), self.stickHinge.quatInterval(lerpTime, self.stickHinge.getHpr(), origStickHingeHpr, blendType='easeInOut')) def getRestoreScaleInterval(self): lerpTime = 1 return Parallel( self.controlModel.scaleInterval(lerpTime, 1, blendType='easeInOut'), self.cc.posInterval(lerpTime, Point3(0, 0, 0), blendType='easeInOut'), self.bottom.posInterval(lerpTime, self.bottomPos, blendType='easeInOut'), self.stickHinge.quatInterval(lerpTime, self.neutralStickHinge, blendType='easeInOut')) def makeToonGrabInterval(self, toon): origPos = toon.getPos() origHpr = toon.getHpr() a = self.accomodateToon(toon) newPos = toon.getPos() newHpr = toon.getHpr() origHpr.setX(PythonUtil.fitSrcAngle2Dest(origHpr[0], newHpr[0])) toon.setPosHpr(origPos, origHpr) walkTime = 0.2 reach = ActorInterval(toon, 'leverReach') if reach.getDuration() < walkTime: reach = Sequence( ActorInterval(toon, 'walk', loop=1, duration=walkTime - reach.getDuration()), reach) i = Sequence( Parallel(toon.posInterval(walkTime, newPos, origPos), toon.hprInterval(walkTime, newHpr, origHpr), reach), Func(self.startWatchJoystick, toon)) i = Parallel(i, a) return i def __toonPlayWithCallback(self, animName, numFrames): duration = numFrames / 24.0 self.toon.play(animName) taskMgr.doMethodLater(duration, self.__toonPlayCallback, self.uniqueName('toonPlay')) def __toonPlayCallback(self, task): if self.changeSeq == self.lastChangeSeq: self.__toonPlayWithCallback('leverNeutral', 40) else: self.__toonPlayWithCallback('leverPull', 40) self.lastChangeSeq = self.changeSeq def startWatchJoystick(self, toon): self.toon = toon taskMgr.add(self.__watchJoystick, self.uniqueName('watchJoystick')) self.__toonPlayWithCallback('leverNeutral', 40) self.accept(toon.uniqueName('disable'), self.__handleUnexpectedExit, extraArgs=[toon.doId]) def stopWatchJoystick(self): taskMgr.remove(self.uniqueName('toonPlay')) taskMgr.remove(self.uniqueName('watchJoystick')) if self.toon: self.ignore(self.toon.uniqueName('disable')) self.toon = None return def __watchJoystick(self, task): self.toon.setPosHpr(self.controls, 0, 0, 0, 0, 0, 0) self.toon.update() self.stickHinge.lookAt(self.toon.rightHand, self.lookAtPoint, self.lookAtUp) return Task.cont def __handleUnexpectedExit(self, toonId): self.notify.warning('%s: unexpected exit for %s' % (self.doId, toonId)) if self.toon and self.toon.doId == toonId: self.stopWatchJoystick() def __activatePhysics(self): if not self.physicsActivated: for an, anp, cnp in self.activeLinks: self.boss.physicsMgr.attachPhysicalNode(an) base.cTrav.addCollider(cnp, self.handler) self.collisions.unstash() self.physicsActivated = 1 def __deactivatePhysics(self): if self.physicsActivated: for an, anp, cnp in self.activeLinks: self.boss.physicsMgr.removePhysicalNode(an) base.cTrav.removeCollider(cnp) self.collisions.stash() self.physicsActivated = 0 def __straightenCable(self): for linkNum in range(self.numLinks): an, anp, cnp = self.activeLinks[linkNum] an.getPhysicsObject().setVelocity(0, 0, 0) z = float(linkNum + 1) / float(self.numLinks) * self.cableLength anp.setPos(self.crane.getPos(self.cable)) anp.setZ(-z) def setCableLength(self, length): self.cableLength = length linkWidth = float(length) / float(self.numLinks) self.shell.setRadius(linkWidth + 1) def setupCable(self): activated = self.physicsActivated self.clearCable() self.handler = PhysicsCollisionHandler() self.handler.setStaticFrictionCoef(0.1) self.handler.setDynamicFrictionCoef(self.emptyFrictionCoef) linkWidth = float(self.cableLength) / float(self.numLinks) self.shell = CollisionInvSphere(0, 0, 0, linkWidth + 1) self.links = [] self.links.append((self.topLink, Point3(0, 0, 0))) anchor = self.topLink for linkNum in range(self.numLinks): anchor = self.__makeLink(anchor, linkNum) self.collisions.stash() self.bottomLink = self.links[-1][0] self.middleLink = self.links[-2][0] self.magnet = self.bottomLink.attachNewNode('magnet') self.wiggleMagnet = self.magnet.attachNewNode('wiggleMagnet') taskMgr.add(self.__rotateMagnet, self.rotateLinkName) magnetModel = self.boss.magnet.copyTo(self.wiggleMagnet) magnetModel.setHpr(90, 45, 90) self.gripper = magnetModel.attachNewNode('gripper') self.gripper.setPos(0, 0, -4) cn = CollisionNode('sniffer') self.sniffer = magnetModel.attachNewNode(cn) self.sniffer.stash() cs = CollisionSphere(0, 0, -10, 6) cs.setTangible(0) cn.addSolid(cs) cn.setIntoCollideMask(BitMask32(0)) cn.setFromCollideMask(ToontownGlobals.CashbotBossObjectBitmask) self.snifferHandler = CollisionHandlerEvent() self.snifferHandler.addInPattern(self.snifferEvent) self.snifferHandler.addAgainPattern(self.snifferEvent) rope = self.makeSpline() rope.reparentTo(self.cable) rope.setTexture(self.boss.cableTex) ts = TextureStage.getDefault() rope.setTexScale(ts, 0.15, 0.13) rope.setTexOffset(ts, 0.83, 0.01) if activated: self.__activatePhysics() def clearCable(self): self.__deactivatePhysics() taskMgr.remove(self.rotateLinkName) self.links = [] self.activeLinks = [] self.linkSmoothers = [] self.collisions.clear() self.cable.getChildren().detach() self.topLink.getChildren().detach() self.gripper = None return def makeSpline(self): rope = Rope.Rope() rope.setup(min(len(self.links), 4), self.links) rope.curve.normalizeKnots() rn = rope.ropeNode rn.setRenderMode(RopeNode.RMTube) rn.setNumSlices(3) rn.setTubeUp(Vec3(0, -1, 0)) rn.setUvMode(RopeNode.UVParametric) rn.setUvDirection(1) rn.setThickness(0.5) return rope def startShadow(self): self.shadow = self.boss.geom.attachNewNode('%s-shadow' % self.name) self.shadow.setColor(1, 1, 1, 0.3) self.shadow.setDepthWrite(0) self.shadow.setTransparency(1) self.shadow.setBin('shadow', 0) self.shadow.node().setFinal(1) self.magnetShadow = loader.loadModel( 'phase_3/models/props/drop_shadow') self.magnetShadow.reparentTo(self.shadow) self.craneShadow = loader.loadModel( 'phase_3/models/props/square_drop_shadow') self.craneShadow.setScale(0.5, 4, 1) self.craneShadow.setPos(0, -12, 0) self.craneShadow.flattenLight() self.craneShadow.reparentTo(self.shadow) taskMgr.add(self.__followShadow, self.shadowName) rope = self.makeSpline() rope.reparentTo(self.shadow) rope.setColor(1, 1, 1, 0.2) tex = self.craneShadow.findTexture('*') rope.setTexture(tex) rn = rope.ropeNode rn.setRenderMode(RopeNode.RMTape) rn.setNumSubdiv(6) rn.setThickness(0.8) rn.setTubeUp(Vec3(0, 0, 1)) rn.setMatrix( Mat4.translateMat(0, 0, self.shadowOffset) * Mat4.scaleMat(1, 1, 0.01)) def stopShadow(self): if self.shadow: self.shadow.removeNode() self.shadow = None self.magnetShadow = None self.craneShadow = None taskMgr.remove(self.shadowName) return def __followShadow(self, task): p = self.magnet.getPos(self.boss.geom) self.magnetShadow.setPos(p[0], p[1], self.shadowOffset) self.craneShadow.setPosHpr(self.crane, 0, 0, 0, 0, 0, 0) self.craneShadow.setZ(self.shadowOffset) return Task.cont def __makeLink(self, anchor, linkNum): an = ActorNode('link%s' % linkNum) anp = NodePath(an) cn = CollisionNode('cn') sphere = CollisionSphere(0, 0, 0, 1) cn.addSolid(sphere) cnp = anp.attachNewNode(cn) self.handler.addCollider(cnp, anp) self.activeLinks.append((an, anp, cnp)) self.linkSmoothers.append(SmoothMover()) anp.reparentTo(self.cable) z = float(linkNum + 1) / float(self.numLinks) * self.cableLength anp.setPos(self.crane.getPos()) anp.setZ(-z) mask = BitMask32.bit(self.firstMagnetBit + linkNum) cn.setFromCollideMask(mask) cn.setIntoCollideMask(BitMask32(0)) shellNode = CollisionNode('shell%s' % linkNum) shellNode.addSolid(self.shell) shellNP = anchor.attachNewNode(shellNode) shellNode.setIntoCollideMask(mask) self.collisions.addPath(shellNP) self.collisions.addPath(cnp) self.links.append((anp, Point3(0, 0, 0))) return anp def __rotateMagnet(self, task): self.magnet.lookAt(self.middleLink, self.p0, self.v1) return Task.cont def __enableControlInterface(self): gui = loader.loadModel('phase_3.5/models/gui/avatar_panel_gui') self.closeButton = DirectButton(image=(gui.find('**/CloseBtn_UP'), gui.find('**/CloseBtn_DN'), gui.find('**/CloseBtn_Rllvr'), gui.find('**/CloseBtn_UP')), relief=None, scale=2, text=TTLocalizer.CashbotCraneLeave, text_scale=0.04, text_pos=(0, -0.07), text_fg=VBase4(1, 1, 1, 1), pos=(1.05, 0, -0.82), command=self.__exitCrane) self.accept('escape', self.__exitCrane) self.accept('control', self.__controlPressed) self.accept('control-up', self.__controlReleased) self.accept('InputState-forward', self.__upArrow) self.accept('InputState-reverse', self.__downArrow) self.accept('InputState-turnLeft', self.__leftArrow) self.accept('InputState-turnRight', self.__rightArrow) taskMgr.add(self.__watchControls, 'watchCraneControls') taskMgr.doMethodLater(5, self.__displayCraneAdvice, self.craneAdviceName) taskMgr.doMethodLater(10, self.__displayMagnetAdvice, self.magnetAdviceName) NametagGlobals.setOnscreenChatForced(1) self.arrowVert = 0 self.arrowHorz = 0 return def __disableControlInterface(self): self.__turnOffMagnet() if self.closeButton: self.closeButton.destroy() self.closeButton = None self.__cleanupCraneAdvice() self.__cleanupMagnetAdvice() self.ignore('escape') self.ignore('control') self.ignore('control-up') self.ignore('InputState-forward') self.ignore('InputState-reverse') self.ignore('InputState-turnLeft') self.ignore('InputState-turnRight') self.arrowVert = 0 self.arrowHorz = 0 NametagGlobals.setOnscreenChatForced(0) taskMgr.remove('watchCraneControls') self.__setMoveSound(None) return def __displayCraneAdvice(self, task): if self.craneAdviceLabel == None: self.craneAdviceLabel = DirectLabel( text=TTLocalizer.CashbotCraneAdvice, text_fg=VBase4(1, 1, 1, 1), text_align=TextNode.ACenter, relief=None, pos=(0, 0, 0.69), scale=0.1) return def __cleanupCraneAdvice(self): if self.craneAdviceLabel: self.craneAdviceLabel.destroy() self.craneAdviceLabel = None taskMgr.remove(self.craneAdviceName) return def __displayMagnetAdvice(self, task): if self.magnetAdviceLabel == None: self.magnetAdviceLabel = DirectLabel( text=TTLocalizer.CashbotMagnetAdvice, text_fg=VBase4(1, 1, 1, 1), text_align=TextNode.ACenter, relief=None, pos=(0, 0, 0.55), scale=0.1) return def __cleanupMagnetAdvice(self): if self.magnetAdviceLabel: self.magnetAdviceLabel.destroy() self.magnetAdviceLabel = None taskMgr.remove(self.magnetAdviceName) return def __watchControls(self, task): if self.arrowHorz or self.arrowVert: self.__moveCraneArcHinge(self.arrowHorz, self.arrowVert) else: self.__setMoveSound(None) return Task.cont def __exitCrane(self): if self.closeButton: self.closeButton.destroy() self.closeButton = DirectLabel( relief=None, text=TTLocalizer.CashbotCraneLeaving, pos=(1.05, 0, -0.88), text_pos=(0, 0), text_scale=0.06, text_fg=VBase4(1, 1, 1, 1)) self.__cleanupCraneAdvice() self.__cleanupMagnetAdvice() self.d_requestFree() return def __incrementChangeSeq(self): self.changeSeq = self.changeSeq + 1 & 255 def __controlPressed(self): self.__cleanupMagnetAdvice() self.__turnOnMagnet() def __controlReleased(self): self.__turnOffMagnet() def __turnOnMagnet(self): if not self.magnetOn: self.__incrementChangeSeq() self.magnetOn = 1 if not self.heldObject: self.__activateSniffer() def __turnOffMagnet(self): if self.magnetOn: self.magnetOn = 0 self.__deactivateSniffer() self.releaseObject() def __upArrow(self, pressed): self.__incrementChangeSeq() self.__cleanupCraneAdvice() if pressed: self.arrowVert = 1 elif self.arrowVert > 0: self.arrowVert = 0 def __downArrow(self, pressed): self.__incrementChangeSeq() self.__cleanupCraneAdvice() if pressed: self.arrowVert = -1 elif self.arrowVert < 0: self.arrowVert = 0 def __rightArrow(self, pressed): self.__incrementChangeSeq() self.__cleanupCraneAdvice() if pressed: self.arrowHorz = 1 elif self.arrowHorz > 0: self.arrowHorz = 0 def __leftArrow(self, pressed): self.__incrementChangeSeq() self.__cleanupCraneAdvice() if pressed: self.arrowHorz = -1 elif self.arrowHorz < 0: self.arrowHorz = 0 def __moveCraneArcHinge(self, xd, yd): dt = globalClock.getDt() h = self.arm.getH() - xd * self.rotateSpeed * dt limitH = max(min(h, self.armMaxH), self.armMinH) self.arm.setH(limitH) y = self.crane.getY() + yd * self.slideSpeed * dt limitY = max(min(y, self.craneMaxY), self.craneMinY) atLimit = limitH != h or limitY != y if atLimit: now = globalClock.getFrameTime() x = math.sin(now * 79) * 0.05 z = math.sin(now * 70) * 0.02 self.crane.setPos(x, limitY, z) self.__setMoveSound(self.atLimitSfx) else: self.crane.setPos(0, limitY, 0) self.__setMoveSound(self.craneMoveSfx) def __setMoveSound(self, sfx): if sfx != self.moveSound: if self.moveSound: self.moveSound.stop() self.moveSound = sfx if self.moveSound: base.playSfx(self.moveSound, looping=1, volume=0.5) def __activateSniffer(self): if not self.snifferActivated: self.sniffer.unstash() base.cTrav.addCollider(self.sniffer, self.snifferHandler) self.accept(self.snifferEvent, self.__sniffedSomething) self.startFlicker() self.snifferActivated = 1 def __deactivateSniffer(self): if self.snifferActivated: base.cTrav.removeCollider(self.sniffer) self.sniffer.stash() self.ignore(self.snifferEvent) self.stopFlicker() self.snifferActivated = 0 def startFlicker(self): self.magnetSoundInterval.start() self.lightning = [] for i in range(4): t = float(i) / 3.0 - 0.5 l = self.boss.lightning.copyTo(self.gripper) l.setScale(random.choice([1, -1]), 1, 5) l.setZ(random.uniform(-5, -5.5)) l.flattenLight() l.setTwoSided(1) l.setBillboardAxis() l.setScale(random.uniform(0.5, 1.0)) if t < 0: l.setX(t - 0.7) else: l.setX(t + 0.7) l.setR(-20 * t) l.setP(random.uniform(-20, 20)) self.lightning.append(l) taskMgr.add(self.__flickerLightning, self.flickerName) def stopFlicker(self): self.magnetSoundInterval.finish() self.magnetLoopSfx.stop() taskMgr.remove(self.flickerName) for l in self.lightning: l.detachNode() self.lightning = None return def __flickerLightning(self, task): for l in self.lightning: if random.random() < 0.5: l.hide() else: l.show() return Task.cont def __sniffedSomething(self, entry): np = entry.getIntoNodePath() if np.hasNetTag('object'): doId = int(np.getNetTag('object')) else: self.notify.warning("%s missing 'object' tag" % np) return self.notify.debug('__sniffedSomething %d' % doId) obj = base.cr.doId2do.get(doId) if obj and obj.state != 'LocalDropped' and (obj.state != 'Dropped' or obj.craneId != self.doId): obj.d_requestGrab() obj.demand('LocalGrabbed', localAvatar.doId, self.doId) def grabObject(self, obj): if self.state == 'Off': return if self.heldObject != None: self.releaseObject() self.__deactivateSniffer() obj.wrtReparentTo(self.gripper) if obj.lerpInterval: obj.lerpInterval.finish() obj.lerpInterval = Parallel( obj.posInterval(ToontownGlobals.CashbotBossToMagnetTime, Point3(*obj.grabPos)), obj.quatInterval(ToontownGlobals.CashbotBossToMagnetTime, VBase3(obj.getH(), 0, 0)), obj.toMagnetSoundInterval) obj.lerpInterval.start() self.heldObject = obj self.handler.setDynamicFrictionCoef(obj.craneFrictionCoef) self.slideSpeed = obj.craneSlideSpeed self.rotateSpeed = obj.craneRotateSpeed if self.avId == localAvatar.doId and not self.magnetOn: self.releaseObject() return def dropObject(self, obj): if obj.lerpInterval: obj.lerpInterval.finish() obj.wrtReparentTo(render) obj.lerpInterval = Parallel( obj.quatInterval(ToontownGlobals.CashbotBossFromMagnetTime, VBase3(obj.getH(), 0, 0), blendType='easeOut')) obj.lerpInterval.start() p1 = self.bottomLink.node().getPhysicsObject() v = render.getRelativeVector(self.bottomLink, p1.getVelocity()) obj.physicsObject.setVelocity(v * 1.5) if self.heldObject == obj: self.heldObject = None self.handler.setDynamicFrictionCoef(self.emptyFrictionCoef) self.slideSpeed = self.emptySlideSpeed self.rotateSpeed = self.emptyRotateSpeed return def releaseObject(self): if self.heldObject: obj = self.heldObject obj.d_requestDrop() if obj.state == 'Grabbed': obj.demand('LocalDropped', localAvatar.doId, self.doId) def __hitTrigger(self, event): self.d_requestControl() def setBossCogId(self, bossCogId): self.bossCogId = bossCogId self.boss = base.cr.doId2do[bossCogId] def setIndex(self, index): self.index = index def setState(self, state, avId): if state == 'C': self.demand('Controlled', avId) elif state == 'F': self.demand('Free') else: self.notify.error('Invalid state from AI: %s' % state) def d_requestControl(self): self.sendUpdate('requestControl') def d_requestFree(self): self.sendUpdate('requestFree') def b_clearSmoothing(self): self.d_clearSmoothing() self.clearSmoothing() def d_clearSmoothing(self): self.sendUpdate('clearSmoothing', [0]) def clearSmoothing(self, bogus=None): self.armSmoother.clearPositions(1) for smoother in self.linkSmoothers: smoother.clearPositions(1) def reloadPosition(self): self.armSmoother.clearPositions(0) self.armSmoother.setPos(self.crane.getPos()) self.armSmoother.setHpr(self.arm.getHpr()) self.armSmoother.setPhonyTimestamp() for linkNum in range(self.numLinks): smoother = self.linkSmoothers[linkNum] an, anp, cnp = self.activeLinks[linkNum] smoother.clearPositions(0) smoother.setPos(anp.getPos()) smoother.setPhonyTimestamp() def doSmoothTask(self, task): self.armSmoother.computeAndApplySmoothPosHpr(self.crane, self.arm) for linkNum in range(self.numLinks): smoother = self.linkSmoothers[linkNum] anp = self.activeLinks[linkNum][1] smoother.computeAndApplySmoothPos(anp) return Task.cont def startSmooth(self): if not self.smoothStarted: taskName = self.smoothName taskMgr.remove(taskName) self.reloadPosition() taskMgr.add(self.doSmoothTask, taskName) self.smoothStarted = 1 def stopSmooth(self): if self.smoothStarted: taskName = self.smoothName taskMgr.remove(taskName) self.forceToTruePosition() self.smoothStarted = 0 def forceToTruePosition(self): if self.armSmoother.getLatestPosition(): self.armSmoother.applySmoothPos(self.crane) self.armSmoother.applySmoothHpr(self.arm) self.armSmoother.clearPositions(1) for linkNum in range(self.numLinks): smoother = self.linkSmoothers[linkNum] an, anp, cnp = self.activeLinks[linkNum] if smoother.getLatestPosition(): smoother.applySmoothPos(anp) smoother.clearPositions(1) def setCablePos(self, changeSeq, y, h, links, timestamp): self.changeSeq = changeSeq if self.smoothStarted: if len(links) > self.numLinks: self.notify.warning( 'Links passed in is greater than total number of links') return now = globalClock.getFrameTime() local = globalClockDelta.networkToLocalTime(timestamp, now) self.armSmoother.setY(y) self.armSmoother.setH(h) self.armSmoother.setTimestamp(local) self.armSmoother.markPosition() for linkNum in range(self.numLinks): smoother = self.linkSmoothers[linkNum] lp = links[linkNum] smoother.setPos(*lp) smoother.setTimestamp(local) smoother.markPosition() else: self.crane.setY(y) self.arm.setH(h) def d_sendCablePos(self): timestamp = globalClockDelta.getFrameNetworkTime() links = [] for linkNum in range(self.numLinks): an, anp, cnp = self.activeLinks[linkNum] p = anp.getPos() links.append((p[0], p[1], p[2])) self.sendUpdate('setCablePos', [ self.changeSeq, self.crane.getY(), self.arm.getH(), links, timestamp ]) def stopPosHprBroadcast(self): taskName = self.posHprBroadcastName taskMgr.remove(taskName) def startPosHprBroadcast(self): taskName = self.posHprBroadcastName self.b_clearSmoothing() self.d_sendCablePos() taskMgr.remove(taskName) taskMgr.doMethodLater(self.__broadcastPeriod, self.__posHprBroadcast, taskName) def __posHprBroadcast(self, task): self.d_sendCablePos() taskName = self.posHprBroadcastName taskMgr.doMethodLater(self.__broadcastPeriod, self.__posHprBroadcast, taskName) return Task.done def enterOff(self): self.clearCable() self.root.detachNode() def exitOff(self): if self.boss: self.setupCable() self.root.reparentTo(render) def enterControlled(self, avId): self.avId = avId toon = base.cr.doId2do.get(avId) if not toon: return self.grabTrack = self.makeToonGrabInterval(toon) if avId == localAvatar.doId: self.boss.toCraneMode() camera.reparentTo(self.hinge) camera.setPosHpr(0, -20, -5, 0, -20, 0) self.tube.stash() localAvatar.setPosHpr(self.controls, 0, 0, 0, 0, 0, 0) localAvatar.sendCurrentPosition() self.__activatePhysics() self.__enableControlInterface() self.startPosHprBroadcast() self.startShadow() self.accept('exitCrane', self.__exitCrane) else: self.startSmooth() toon.stopSmooth() self.grabTrack = Sequence(self.grabTrack, Func(toon.startSmooth)) self.grabTrack.start() def exitControlled(self): self.ignore('exitCrane') self.grabTrack.finish() del self.grabTrack if self.toon and not self.toon.isDisabled(): self.toon.loop('neutral') self.toon.startSmooth() self.stopWatchJoystick() self.stopPosHprBroadcast() self.stopShadow() self.stopSmooth() if self.avId == localAvatar.doId: self.__disableControlInterface() self.__deactivatePhysics() self.tube.unstash() camera.reparentTo(base.localAvatar) camera.setPos(base.localAvatar.cameraPositions[0][0]) camera.setHpr(0, 0, 0) if self.cr: place = self.cr.playGame.getPlace() if place and hasattr(place, 'fsm'): if place.fsm.getCurrentState().getName() == 'crane': place.setState('finalBattle') self.boss.toFinalBattleMode() self.__straightenCable() def enterFree(self): if self.fadeTrack: self.fadeTrack.finish() self.fadeTrack = None self.restoreScaleTrack = Sequence(Wait(6), self.getRestoreScaleInterval()) self.restoreScaleTrack.start() if self.avId == localAvatar.doId: self.controlModel.setAlphaScale(0.3) self.controlModel.setTransparency(1) taskMgr.doMethodLater(5, self.__allowDetect, self.triggerName) self.fadeTrack = Sequence( Func(self.controlModel.setTransparency, 1), self.controlModel.colorScaleInterval(0.2, VBase4(1, 1, 1, 0.3))) self.fadeTrack.start() else: self.trigger.unstash() self.accept(self.triggerEvent, self.__hitTrigger) self.avId = 0 return def __allowDetect(self, task): if self.fadeTrack: self.fadeTrack.finish() self.fadeTrack = Sequence( self.controlModel.colorScaleInterval(0.2, VBase4(1, 1, 1, 1)), Func(self.controlModel.clearColorScale), Func(self.controlModel.clearTransparency)) self.fadeTrack.start() self.trigger.unstash() self.accept(self.triggerEvent, self.__hitTrigger) def exitFree(self): if self.fadeTrack: self.fadeTrack.finish() self.fadeTrack = None self.restoreScaleTrack.pause() del self.restoreScaleTrack taskMgr.remove(self.triggerName) self.controlModel.clearColorScale() self.controlModel.clearTransparency() self.trigger.stash() self.ignore(self.triggerEvent) return def enterMovie(self): self.__activatePhysics() def exitMovie(self): self.__deactivatePhysics() self.__straightenCable()