From 84e8ad4c0d3439321e294e607238bc99ac10c98b Mon Sep 17 00:00:00 2001 From: John Cote Date: Wed, 24 Jun 2015 11:28:44 -0400 Subject: [PATCH] Push TTR nametag code --- otp/chat/ChatGlobals.py | 50 ++++- otp/margins/ClickablePopup.py | 162 ++++++++++++++++ otp/margins/MarginCell.py | 83 +++++++++ otp/margins/MarginManager.py | 83 +++++++++ otp/margins/MarginPopup.py | 52 ++++++ otp/margins/WhisperPopup.py | 97 ++++++++++ otp/margins/__init__.py | 0 otp/nametag/ChatBalloon.py | 93 ++++++++++ otp/nametag/Nametag.py | 156 ++++++++++++++++ otp/nametag/Nametag2d.py | 113 +++++++++++ otp/nametag/Nametag3d.py | 70 +++++++ otp/nametag/NametagConstants.py | 216 +++++++++++++++++++++ otp/nametag/NametagFloat2d.py | 7 + otp/nametag/NametagFloat3d.py | 7 + otp/nametag/NametagGlobals.py | 100 ++++++++++ otp/nametag/NametagGroup.py | 320 ++++++++++++++++++++++++++++++++ otp/nametag/__init__.py | 0 17 files changed, 1606 insertions(+), 3 deletions(-) create mode 100644 otp/margins/ClickablePopup.py create mode 100644 otp/margins/MarginCell.py create mode 100644 otp/margins/MarginManager.py create mode 100644 otp/margins/MarginPopup.py create mode 100644 otp/margins/WhisperPopup.py create mode 100644 otp/margins/__init__.py create mode 100644 otp/nametag/ChatBalloon.py create mode 100644 otp/nametag/Nametag.py create mode 100644 otp/nametag/Nametag2d.py create mode 100644 otp/nametag/Nametag3d.py create mode 100644 otp/nametag/NametagConstants.py create mode 100644 otp/nametag/NametagFloat2d.py create mode 100644 otp/nametag/NametagFloat3d.py create mode 100644 otp/nametag/NametagGlobals.py create mode 100644 otp/nametag/NametagGroup.py create mode 100644 otp/nametag/__init__.py diff --git a/otp/chat/ChatGlobals.py b/otp/chat/ChatGlobals.py index 096905f4..94203eeb 100755 --- a/otp/chat/ChatGlobals.py +++ b/otp/chat/ChatGlobals.py @@ -1,9 +1,53 @@ +import string +NORMAL_CHAT = 1 +WHISPER_CHAT = 2 +GUILD_CHAT = 3 +CREW_CHAT = 4 +SHIPPVP_CHAT = 5 +ERROR_NONE = None +ERROR_NO_OPEN_CHAT = 1 +ERROR_NOT_FRIENDS = 2 +ERROR_NO_RECEIVER = 3 +ERROR_NO_GUILD_CHAT = 4 +ERROR_NO_CREW_CHAT = 5 +ERROR_NO_SHIPPVP_CHAT = 6 +TYPEDCHAT = 0 SPEEDCHAT_NORMAL = 1 SPEEDCHAT_EMOTE = 2 SPEEDCHAT_CUSTOM = 3 -SPEEDCHAT_QUEST = 4 - +SYSTEMCHAT = 4 +GAMECHAT = 5 +GUILDCHAT = 6 +PARTYCHAT = 7 +SPEEDCHAT_QUEST = 8 +FRIEND_UPDATE = 9 +CREW_UPDATE = 10 +GUILD_UPDATE = 11 +AVATAR_UNAVAILABLE = 12 +SHIPPVPCHAT = 13 +GMCHAT = 14 +ChatEvent = 'ChatEvent' NormalChatEvent = 'NormalChatEvent' SCChatEvent = 'SCChatEvent' SCCustomChatEvent = 'SCCustomChatEvent' -SCEmoteChatEvent = 'SCEmoteChatEvent' \ No newline at end of file +SCEmoteChatEvent = 'SCEmoteChatEvent' +SCQuestEvent = 'SCQuestEvent' +OnScreen = 0 +OffScreen = 1 +Thought = 2 +ThoughtPrefix = '.' + +def isThought(message): + if len(message) == 0: + return 0 + elif message.find(ThoughtPrefix, 0, len(ThoughtPrefix)) >= 0: + return 1 + else: + return 0 + + +def removeThoughtPrefix(message): + if isThought(message): + return message[len(ThoughtPrefix):] + else: + return message diff --git a/otp/margins/ClickablePopup.py b/otp/margins/ClickablePopup.py new file mode 100644 index 00000000..ed9b8aec --- /dev/null +++ b/otp/margins/ClickablePopup.py @@ -0,0 +1,162 @@ +from pandac.PandaModules import * +from direct.showbase.DirectObject import DirectObject +from otp.nametag import NametagGlobals + +class ClickablePopup(PandaNode, DirectObject): + CS_NORMAL = 0 + CS_CLICK = 1 + CS_HOVER = 2 + CS_DISABLED = 3 + + def __init__(self, cam=None): + PandaNode.__init__(self, 'popup') + DirectObject.__init__(self) + + self.__mwn = NametagGlobals.mouseWatcher + self.__name = 'clickregion-%d' % id(self) + + self.__cam = cam + self.__region = MouseWatcherRegion(self.__name, 0, 0, 0, 0) + self.__mwn.addRegion(self.__region) + + self.__disabled = False + self.__clicked = False + self.__hovered = False + self.__onscreen = False + self.__clickState = 0 + self.__clickArgs = [] + + self.__clickEvent = '' + + self.accept(self.__getEvent(self.__mwn.getEnterPattern()), self.__mouseEnter) + self.accept(self.__getEvent(self.__mwn.getLeavePattern()), self.__mouseLeave) + self.accept(self.__getEvent(self.__mwn.getButtonDownPattern()), self.__buttonDown) + self.accept(self.__getEvent(self.__mwn.getButtonUpPattern()), self.__buttonUp) + + def destroy(self): + self.__mwn.removeRegion(self.__region) + self.ignoreAll() + + def setClickRegionEvent(self, event, clickArgs=[]): + if event is None: + # The caller is disabling us, so instead: + self.__disabled = True + self.__region.setActive(False) + self.__updateClickState() + else: + self.__clickEvent = event + self.__clickArgs = clickArgs + self.__disabled = False + self.__region.setActive(True) + self.__updateClickState() + + def getClickState(self): + return self.__clickState + + def clickStateChanged(self): + pass # Intended for subclasses. + + def __getEvent(self, pattern): + return pattern.replace('%r', self.__name) + + def __mouseEnter(self, region, extra): + self.__hovered = True + self.__updateClickState() + + def __mouseLeave(self, region, extra): + self.__hovered = False + self.__updateClickState() + + def __buttonDown(self, region, button): + if button == 'mouse1': + self.__clicked = True + self.__updateClickState() + + def __buttonUp(self, region, button): + if button == 'mouse1': + self.__clicked = False + self.__updateClickState() + + def __updateClickState(self): + if self.__disabled: + state = self.CS_DISABLED + elif self.__clicked: + state = self.CS_CLICK + elif self.__hovered: + state = self.CS_HOVER + else: + state = self.CS_NORMAL + + if self.__clickState == state: return + oldState = self.__clickState + self.__clickState = state + + if oldState == self.CS_NORMAL and state == self.CS_HOVER: + # Play rollover sound: + base.playSfx(NametagGlobals.rolloverSound) + elif state == self.CS_CLICK: + # Play click sound: + base.playSfx(NametagGlobals.clickSound) + elif oldState == self.CS_CLICK and state == self.CS_HOVER: + # Fire click event: + messenger.send(self.__clickEvent, self.__clickArgs) + + self.clickStateChanged() + + def updateClickRegion(self, left, right, bottom, top, offset=0): + transform = NodePath.anyPath(self).getNetTransform() + + if self.__cam: + # We have a camera, so get its transform and move our net transform + # into the coordinate space of the camera: + camTransform = self.__cam.getNetTransform() + transform = camTransform.invertCompose(transform) + + # We must discard the rotational component on our transform, thus: + transform = transform.setQuat(Quat()) + + # Next, we'll transform the frame into camspace: + mat = transform.getMat() + cTopLeft = mat.xformPoint(Point3(left, 0, top)) + cBottomRight = mat.xformPoint(Point3(right, 0, bottom)) + + # Shift along the offset while in camspace, not worldspace. + if offset: + mid = mat.xformPoint(Point3(0,0,0)) + length = mid.length() + shift = mid*(length - offset)/length - mid + cTopLeft += shift + cBottomRight += shift + + if self.__cam: + # We must go further and project to screenspace: + lens = self.__cam.node().getLens() + + sTopLeft = Point2() + sBottomRight = Point2() + + if not (lens.project(Point3(cTopLeft), sTopLeft) and + lens.project(Point3(cBottomRight), sBottomRight)): + # Not on-screen! Disable the click region: + self.__region.setActive(False) + self.__onscreen = False + return + else: + # No cam; the "camspace" (actually just net transform) IS the + # screenspace transform. + sTopLeft = Point2(cTopLeft[0], cTopLeft[2]) + sBottomRight = Point2(cBottomRight[0], cBottomRight[2]) + + sLeft, sTop = sTopLeft + sRight, sBottom = sBottomRight + + self.__region.setFrame(sLeft, sRight, sBottom, sTop) + self.__region.setActive(not self.__disabled) + self.__onscreen = True + + def stashClickRegion(self): + self.__region.setActive(False) + self.__onscreen = False + + def isOnScreen(self): + return self.__onscreen diff --git a/otp/margins/MarginCell.py b/otp/margins/MarginCell.py new file mode 100644 index 00000000..5fc13874 --- /dev/null +++ b/otp/margins/MarginCell.py @@ -0,0 +1,83 @@ +from pandac.PandaModules import * + +class MarginCell(NodePath): + def __init__(self, manager): + NodePath.__init__(self, 'cell') + + self.manager = manager + + self.content = None + self.available = False + + self.debugSquare = None + self.debugMode = False + + self.setDebug(config.GetBool('want-cell-debug', False)) + + def setAvailable(self, available): + if not available and self.hasContent(): + self.setContent(None) + + self.available = available + + self.updateDebug() + + def setContent(self, content): + if self.content: + self.content._assignedCell = None + self.contentNP.removeNode() + self.content.marginVisibilityChanged() + + if content: + content._assignedCell = self + content._lastCell = self + self.contentNP = self.attachNewNode(content) + content.marginVisibilityChanged() + + self.content = content + + self.updateDebug() + + def hasContent(self): + return self.content is not None + + def getContent(self): + return self.content + + def isAvailable(self): + return self.available + + def isFree(self): + return self.isAvailable() and not self.hasContent() + + def setDebugColor(self, color): + if not self.debugSquare: + cm = CardMaker('debugSquare') + cm.setFrameFullscreenQuad() + self.debugSquare = self.attachNewNode(cm.generate()) + self.debugSquare.setTransparency(1) + self.debugSquare.setY(1) + + self.debugSquare.setColor(color) + + def updateDebug(self): + if not self.debugMode: return + + if self.hasContent(): + self.setDebugColor(VBase4(0.0, 0.8, 0.0, 0.5)) + elif self.isAvailable(): + self.setDebugColor(VBase4(0.0, 0.0, 0.8, 0.5)) + else: + self.setDebugColor(VBase4(0.8, 0.0, 0.0, 0.5)) + + def setDebug(self, status): + if bool(status) == self.debugMode: + return + + self.debugMode = status + + if self.debugMode: + self.updateDebug() + elif self.debugSquare: + self.debugSquare.removeNode() + self.debugSquare = None diff --git a/otp/margins/MarginManager.py b/otp/margins/MarginManager.py new file mode 100644 index 00000000..dbbbb978 --- /dev/null +++ b/otp/margins/MarginManager.py @@ -0,0 +1,83 @@ +from pandac.PandaModules import * +from MarginCell import MarginCell +import random + +class MarginManager(PandaNode): + def __init__(self): + PandaNode.__init__(self, 'margins') + + self.cells = set() + self.visiblePopups = set() + + def addGridCell(self, x, y, a2d): + # Yucky! + nodePath = NodePath.anyPath(self) + a2d.reparentTo(nodePath) + cell = MarginCell(self) + cell.reparentTo(a2d) + cell.setScale(0.2) + cell.setPos(x, 0, y) + cell.setAvailable(True) + cell.setPythonTag('MarginCell', cell) + + self.cells.add(cell) + self.reorganize() + + return cell + + def setCellAvailable(self, cell, available): + cell = cell.getPythonTag('MarginCell') + cell.setAvailable(available) + self.reorganize() + + def addVisiblePopup(self, popup): + self.visiblePopups.add(popup) + self.reorganize() + + def removeVisiblePopup(self, popup): + if popup not in self.visiblePopups: return + self.visiblePopups.remove(popup) + self.reorganize() + + def reorganize(self): + # First, get all active cells: + activeCells = [cell for cell in self.cells if cell.isAvailable()] + + # Next, get all visible popups, sorted by priority: + popups = list(self.visiblePopups) + popups.sort(key=lambda x: -x.getPriority()) + + # We can only display so many popups, so truncate to the number of active + # margin cells: + popups = popups[:len(activeCells)] + + # Now, we need to build up a list of free cells: + freeCells = [] + for cell in activeCells: + if not cell.hasContent(): + freeCells.append(cell) + elif cell.getContent() in popups: + # It's already displaying something we want to show, so we can + # safely ignore this cell/popup pair: + popups.remove(cell.getContent()) + else: + # It's not displaying something we want to see, evict the old + # popup: + cell.setContent(None) + freeCells.append(cell) + + # At this point, there should be enough cells to show the popups: + assert len(freeCells) >= len(popups) + + # Now we assign the popups: + for popup in popups: + if popup._lastCell in freeCells and popup._lastCell.isFree(): + # The last cell it had assigned is available, so let's assign it + # again: + popup._lastCell.setContent(popup) + freeCells.remove(popup._lastCell) + else: + # We assign a cell at random. + cell = random.choice(freeCells) + cell.setContent(popup) + freeCells.remove(cell) diff --git a/otp/margins/MarginPopup.py b/otp/margins/MarginPopup.py new file mode 100644 index 00000000..69ead38a --- /dev/null +++ b/otp/margins/MarginPopup.py @@ -0,0 +1,52 @@ +from pandac.PandaModules import * + +class MarginPopup: + def __init__(self): + self.__manager = None + self.__visible = False + + self.__priority = 0 + + # The margin management system uses these: + self._assignedCell = None + self._lastCell = None + + def setVisible(self, visibility): + visibility = bool(visibility) + if self.__visible == visibility: return + + self.__visible = visibility + + if self.__manager is not None: + if visibility: + self.__manager.addVisiblePopup(self) + else: + self.__manager.removeVisiblePopup(self) + + def getPriority(self): + return self.__priority + + def setPriority(self, priority): + self.__priority = priority + if self.__manager is not None: + self.__manager.reorganize() + + def isDisplayed(self): + return self._assignedCell is not None + + def marginVisibilityChanged(self): + pass # Fired externally when the result of isDisplayed changes. For subclasses. + + def manage(self, manager): + if self.__manager: + self.unmanage(self.__manager) + self.__manager = manager + + if self.__visible: + manager.addVisiblePopup(self) + + def unmanage(self, manager): + if self.__manager is not None: + if self.__visible: + self.__manager.removeVisiblePopup(self) + self.__manager = None diff --git a/otp/margins/WhisperPopup.py b/otp/margins/WhisperPopup.py new file mode 100644 index 00000000..4b2bd260 --- /dev/null +++ b/otp/margins/WhisperPopup.py @@ -0,0 +1,97 @@ +from MarginPopup import * +from ClickablePopup import * +from otp.nametag import NametagGlobals +from otp.nametag.NametagConstants import * + +class WhisperPopup(MarginPopup, ClickablePopup): + WTNormal = WTNormal + WTQuickTalker = WTQuickTalker + WTSystem = WTSystem + WTBattleSOS = WTBattleSOS + WTEmote = WTEmote + WTToontownBoardingGroup = WTToontownBoardingGroup + + WORDWRAP = 7.5 + SCALE_2D = 0.25 + + def __init__(self, text, font, whisperType, timeout=10.0): + ClickablePopup.__init__(self) + MarginPopup.__init__(self) + + self.innerNP = NodePath.anyPath(self).attachNewNode('innerNP') + self.innerNP.setScale(self.SCALE_2D) + + self.text = text + self.font = font + self.whisperType = whisperType + self.timeout = timeout + + self.active = False + self.fromId = 0 + + self.left = 0.0 + self.right = 0.0 + self.top = 0.0 + self.bottom = 0.0 + + self.updateContents() + + self.setPriority(2) + self.setVisible(True) + + def updateContents(self): + if self.whisperType in WHISPER_COLORS: + cc = self.whisperType + else: + cc = WTSystem + + fgColor, bgColor = WHISPER_COLORS[cc][self.getClickState()] + self.innerNP.node().removeAllChildren() + + balloon, frame = NametagGlobals.speechBalloon2d.generate( + self.text, self.font, textColor=fgColor, balloonColor=bgColor, + wordWrap=self.WORDWRAP) + balloon.reparentTo(self.innerNP) + + # Calculate the center of the TextNode. + text = balloon.find('**/+TextNode') + t = text.node() + self.left, self.right, self.bottom, self.top = t.getFrameActual() + center = self.innerNP.getRelativePoint(text, ((self.left + self.right) / 2., 0, (self.bottom + self.top) / 2.)) + + # Next translate the balloon along the inverse. + balloon.setPos(balloon, -center) + + if self.active and self.fromId: + self.setClickRegionEvent('clickedWhisper', clickArgs=[self.fromId]) + + def setClickable(self, senderName, fromId, todo=0): + self.active = True + self.fromId = fromId + + self.updateContents() + self.__updateClickRegion() + + def marginVisibilityChanged(self): + self.__updateClickRegion() + + def __updateClickRegion(self): + if self.isDisplayed() and self.active: + self.updateClickRegion(-1, 1, self.bottom, self.top) + else: + self.stashClickRegion() + + def clickStateChanged(self): + self.updateContents() + + def manage(self, manager): + MarginPopup.manage(self, manager) + + taskMgr.doMethodLater(self.timeout, self.unmanage, 'whisper-timeout-%d' % id(self), [manager]) + + # Manually Clean up + def unmanage(self, manager): + MarginPopup.unmanage(self, manager) + + ClickablePopup.destroy(self) + self.innerNP.removeNode() diff --git a/otp/margins/__init__.py b/otp/margins/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/otp/nametag/ChatBalloon.py b/otp/nametag/ChatBalloon.py new file mode 100644 index 00000000..598eb47b --- /dev/null +++ b/otp/nametag/ChatBalloon.py @@ -0,0 +1,93 @@ +from pandac.PandaModules import * + +class ChatBalloon: + TEXT_SHIFT = (0.1, -0.05, 1.1) + TEXT_SHIFT_REVERSED = -0.05 + TEXT_SHIFT_PROP = 0.08 + NATIVE_WIDTH = 10.0 + MIN_WIDTH = 2.5 + MIN_HEIGHT = 1 + BUBBLE_PADDING = 0.3 + BUBBLE_PADDING_PROP = 0.05 + BUTTON_SCALE = 6 + BUTTON_SHIFT = (-0.2, 0, 0.6) + FRAME_SHIFT = (0.2, 1.4) + + def __init__(self, model): + self.model = model + + def generate(self, text, font, textColor=(0,0,0,1), balloonColor=(1,1,1,1), + wordWrap = 10.0, button=None, reversed=False): + root = NodePath('balloon') + + # Add balloon geometry: + balloon = self.model.copyTo(root) + top = balloon.find('**/top') + middle = balloon.find('**/middle') + bottom = balloon.find('**/bottom') + + balloon.setColor(balloonColor) + if balloonColor[3] < 1.0: + balloon.setTransparency(1) + + # Render the text into a TextNode, using the font: + t = root.attachNewNode(TextNode('text')) + t.node().setFont(font) + t.node().setWordwrap(wordWrap) + t.node().setText(text) + t.node().setTextColor(textColor) + + width, height = t.node().getWidth(), t.node().getHeight() + + # Turn off depth write for the text: The place in the depth buffer is + # held by the chat bubble anyway, and the text renders after the bubble + # so there's no risk of the bubble overwriting the text's pixels. + t.setAttrib(DepthWriteAttrib.make(0)) + t.setPos(self.TEXT_SHIFT) + t.setX(t, self.TEXT_SHIFT_PROP*width) + t.setZ(t, height) + + if reversed: + # The nametag code wants the text on the left side of the axis, + # rather than on the right side. Therefore, we move the text to the + # opposite side: + t.setX(self.TEXT_SHIFT_REVERSED - self.TEXT_SHIFT_PROP*width - width) + + # Give the chat bubble a button, if one is requested: + if button: + np = button.copyTo(root) + np.setPos(t, width, 0, -height) + np.setPos(np, self.BUTTON_SHIFT) + np.setScale(self.BUTTON_SCALE) + + # Set a minimum width and height for short or empty messages + if width < self.MIN_WIDTH: + width = self.MIN_WIDTH + if reversed: + t.setX(t, -width/2.0) + else: + t.setX(t, width/2.0) + t.node().setAlign(TextNode.ACenter) + + if height < self.MIN_HEIGHT: + height = self.MIN_HEIGHT + t.setX(t, height/2.0) + t.node().setAlign(TextNode.ACenter) + + # Set the balloon's size: + width *= 1+self.BUBBLE_PADDING_PROP + width += self.BUBBLE_PADDING + balloon.setSx(width/self.NATIVE_WIDTH) + if reversed: + balloon.setSx(-balloon.getSx()) + balloon.setTwoSided(True) # Render the backface of the balloon + middle.setSz(height) + top.setZ(top, height-1) + + # Calculate the frame occupied by the balloon: + left, bottom = self.FRAME_SHIFT + if reversed: + left = -left - width + frame = (left, left+width, bottom, bottom+height+1) + + return root, frame diff --git a/otp/nametag/Nametag.py b/otp/nametag/Nametag.py new file mode 100644 index 00000000..ac1bf1e6 --- /dev/null +++ b/otp/nametag/Nametag.py @@ -0,0 +1,156 @@ +from NametagConstants import * +import NametagGlobals +from otp.margins.ClickablePopup import ClickablePopup +from otp.otpbase import OTPGlobals +from pandac.PandaModules import * + +class Nametag(ClickablePopup): + CName = 1 + CSpeech = 2 + CThought = 4 + + NAME_PADDING = 0.2 + CHAT_ALPHA = 1.0 + + DEFAULT_CHAT_WORDWRAP = 10.0 + + IS_3D = False # 3D variants will set this to True. + + def __init__(self): + if self.IS_3D: + ClickablePopup.__init__(self, NametagGlobals.camera) + else: + ClickablePopup.__init__(self) + + self.contents = 0 # To be set by subclass. + + self.innerNP = NodePath.anyPath(self).attachNewNode('nametag_contents') + + self.wordWrap = 7.5 + self.chatWordWrap = None + + self.font = None + self.speechFont = None + self.name = '' + self.displayName = '' + self.qtColor = VBase4(1,1,1,1) + self.colorCode = CCNormal + self.avatar = None + self.icon = NodePath('icon') + + self.frame = (0, 0, 0, 0) + + self.nameFg = (0,0,0,1) + self.nameBg = (1,1,1,1) + self.chatFg = (0,0,0,1) + self.chatBg = (1,1,1,1) + + self.chatString = '' + self.chatFlags = 0 + + def destroy(self): + ClickablePopup.destroy(self) + + def setContents(self, contents): + self.contents = contents + self.update() + + def setAvatar(self, avatar): + self.avatar = avatar + + def setChatWordwrap(self, chatWordWrap): + self.chatWordWrap = chatWordWrap + + def tick(self): + pass # Does nothing by default. + + def clickStateChanged(self): + self.update() + + def getButton(self): + cs = self.getClickState() + if self.buttons is None: + return None + elif cs in self.buttons: + return self.buttons[cs] + else: + return self.buttons.get(0) + + def update(self): + if self.colorCode in NAMETAG_COLORS: + cc = self.colorCode + else: + cc = CCNormal + + self.nameFg, self.nameBg, self.chatFg, self.chatBg = NAMETAG_COLORS[cc][self.getClickState()] + + self.innerNP.node().removeAllChildren() + if self.contents & self.CThought and self.chatFlags & CFThought: + self.showThought() + elif self.contents & self.CSpeech and self.chatFlags&CFSpeech: + self.showSpeech() + elif self.contents & self.CName and self.displayName: + self.showName() + + def showBalloon(self, balloon, text): + if not self.speechFont: + # If no font is set, we can't display anything yet... + return + color = self.qtColor if (self.chatFlags&CFQuicktalker) else self.chatBg + if color[3] > self.CHAT_ALPHA: + color = (color[0], color[1], color[2], self.CHAT_ALPHA) + + reversed = (self.IS_3D and (self.chatFlags&CFReversed)) + + balloon, frame = balloon.generate(text, self.speechFont, textColor=self.chatFg, + balloonColor=color, + wordWrap=self.chatWordWrap or \ + self.DEFAULT_CHAT_WORDWRAP, + button=self.getButton(), + reversed=reversed) + balloon.reparentTo(self.innerNP) + self.frame = frame + + def showThought(self): + self.showBalloon(self.getThoughtBalloon(), self.chatString) + + def showSpeech(self): + self.showBalloon(self.getSpeechBalloon(), self.chatString) + + def showName(self): + if not self.font: + # If no font is set, we can't actually display a name yet... + return + + # Create text node: + self.innerNP.attachNewNode(self.icon) + t = self.innerNP.attachNewNode(TextNode('name'), 1) + t.node().setFont(self.font) + t.node().setAlign(TextNode.ACenter) + t.node().setWordwrap(self.wordWrap) + t.node().setText(self.displayName) + t.setColor(self.nameFg) + t.setTransparency(self.nameFg[3] < 1.0) + + width, height = t.node().getWidth(), t.node().getHeight() + + # Put the actual written name a little in front of the nametag and + # disable depth write so the text appears nice and clear, free from + # z-fighting and bizarre artifacts. The text renders *after* the tag + # behind it, due to both being in the transparency bin, + # so there's really no problem with doing this. + t.setY(-0.05) + t.setAttrib(DepthWriteAttrib.make(0)) + + # Apply panel behind the text: + panel = NametagGlobals.nametagCardModel.copyTo(self.innerNP, 0) + panel.setPos((t.node().getLeft()+t.node().getRight())/2.0, 0, + (t.node().getTop()+t.node().getBottom())/2.0) + panel.setScale(width + self.NAME_PADDING, 1, height + self.NAME_PADDING) + panel.setColor(self.nameBg) + panel.setTransparency(self.nameBg[3] < 1.0) + + self.frame = (t.node().getLeft()-self.NAME_PADDING/2.0, + t.node().getRight()+self.NAME_PADDING/2.0, + t.node().getBottom()-self.NAME_PADDING/2.0, + t.node().getTop()+self.NAME_PADDING/2.0) diff --git a/otp/nametag/Nametag2d.py b/otp/nametag/Nametag2d.py new file mode 100644 index 00000000..0622fed5 --- /dev/null +++ b/otp/nametag/Nametag2d.py @@ -0,0 +1,113 @@ +from Nametag import * +from otp.margins.MarginPopup import * +from pandac.PandaModules import * +import math + +class Nametag2d(Nametag, MarginPopup): + SCALE_2D = 0.25 + CHAT_ALPHA = 0.5 + ARROW_OFFSET = -1.0 + ARROW_SCALE = 1.5 + + DEFAULT_CHAT_WORDWRAP = 8.0 + + def __init__(self): + Nametag.__init__(self) + MarginPopup.__init__(self) + + self.contents = self.CName|self.CSpeech + self.chatWordWrap = 7.5 + + self.arrow = None + + self.innerNP.setScale(self.SCALE_2D) + + def showBalloon(self, balloon, text): + text = '%s: %s' % (self.name, text) + Nametag.showBalloon(self, balloon, text) + + # Next, center the balloon in the cell: + balloon = NodePath.anyPath(self).find('*/balloon') + + # Calculate the center of the TextNode. + text = balloon.find('**/+TextNode') + t = text.node() + left, right, bottom, top = t.getFrameActual() + center = self.innerNP.getRelativePoint(text, + ((left+right)/2., 0, (bottom+top)/2.)) + + # Next translate the balloon along the inverse. + balloon.setPos(balloon, -center) + # Also translate the frame: + left, right, bottom, top = self.frame + self.frame = (left-center.getX(), right-center.getX(), + bottom-center.getZ(), top-center.getZ()) + + # When a balloon is active, we need to be somewhat higher-priority in the + # popup system: + self.setPriority(1) + + # Remove our pointer arrow: + if self.arrow is not None: + self.arrow.removeNode() + self.arrow = None + + def showName(self): + Nametag.showName(self) + + # Revert our priority back to basic: + self.setPriority(0) + + # Tack on an arrow: + t = self.innerNP.find('**/+TextNode') + arrowZ = self.ARROW_OFFSET + t.node().getBottom() + + self.arrow = NametagGlobals.arrowModel.copyTo(self.innerNP) + self.arrow.setZ(arrowZ) + self.arrow.setScale(self.ARROW_SCALE) + self.arrow.setColor(ARROW_COLORS.get(self.colorCode, self.nameFg)) + + def update(self): + Nametag.update(self) + self.considerUpdateClickRegion() + + def marginVisibilityChanged(self): + self.considerUpdateClickRegion() + + def considerUpdateClickRegion(self): + # If we are onscreen, we update our click region: + if self.isDisplayed(): + left, right, bottom, top = self.frame + self.updateClickRegion(left*self.SCALE_2D, right*self.SCALE_2D, + bottom*self.SCALE_2D, top*self.SCALE_2D) + else: + self.stashClickRegion() + + def tick(self): + # Update the arrow's pointing. + if not self.isDisplayed() or self.arrow is None: + return # No arrow or not onscreen. + + if self.avatar is None or self.avatar.isEmpty(): + return # No avatar, can't be done. + + # Get points needed in calculation: + cam = NametagGlobals.camera or base.cam + toon = NametagGlobals.toon or cam + + # libotp calculates this using the offset from localToon->avatar, but + # the orientation from cam. Therefore, we duplicate it like so: + location = self.avatar.getPos(toon) + rotation = toon.getQuat(cam) + + camSpacePos = rotation.xform(location) + arrowRadians = math.atan2(camSpacePos[0], camSpacePos[1]) + arrowDegrees = arrowRadians/math.pi*180 + + self.arrow.setR(arrowDegrees - 90) + + def getSpeechBalloon(self): + return NametagGlobals.speechBalloon2d + + def getThoughtBalloon(self): + return NametagGlobals.thoughtBalloon2d diff --git a/otp/nametag/Nametag3d.py b/otp/nametag/Nametag3d.py new file mode 100644 index 00000000..63ad5bab --- /dev/null +++ b/otp/nametag/Nametag3d.py @@ -0,0 +1,70 @@ +from Nametag import * +import NametagGlobals +from NametagConstants import * +from pandac.PandaModules import * +import math + +class Nametag3d(Nametag): + WANT_DYNAMIC_SCALING = True + SCALING_FACTOR = 0.055 + SCALING_MINDIST = 1 + SCALING_MAXDIST = 50 + + BILLBOARD_OFFSET = 3.0 + SHOULD_BILLBOARD = True + + IS_3D = True + + def __init__(self): + Nametag.__init__(self) + + self.contents = self.CName|self.CSpeech|self.CThought + + self.bbOffset = self.BILLBOARD_OFFSET + self._doBillboard() + + def _doBillboard(self): + if self.SHOULD_BILLBOARD: + self.innerNP.setEffect(BillboardEffect.make( + Vec3(0,0,1), + True, + False, + self.bbOffset, + NodePath(), # Empty; look at scene camera + Point3(0,0,0))) + else: + self.bbOffset = 0.0 + + def setBillboardOffset(self, bbOffset): + self.bbOffset = bbOffset + self._doBillboard() + + def tick(self): + if not self.WANT_DYNAMIC_SCALING: + scale = self.SCALING_FACTOR + else: + # Attempt to maintain the same on-screen size. + distance = self.innerNP.getPos(NametagGlobals.camera).length() + distance = max(min(distance, self.SCALING_MAXDIST), self.SCALING_MINDIST) + + scale = math.sqrt(distance)*self.SCALING_FACTOR + + self.innerNP.setScale(scale) + + # As 3D nametags can move around on their own, we need to update the + # click frame constantly: + path = NodePath.anyPath(self) + if path.isHidden() or (path.getTop() != NametagGlobals.camera.getTop() and + path.getTop() != render2d): + self.stashClickRegion() + else: + left, right, bottom, top = self.frame + self.updateClickRegion(left*scale, right*scale, + bottom*scale, top*scale, + self.bbOffset) + + def getSpeechBalloon(self): + return NametagGlobals.speechBalloon3d + + def getThoughtBalloon(self): + return NametagGlobals.thoughtBalloon3d diff --git a/otp/nametag/NametagConstants.py b/otp/nametag/NametagConstants.py new file mode 100644 index 00000000..39cd3067 --- /dev/null +++ b/otp/nametag/NametagConstants.py @@ -0,0 +1,216 @@ +CFNoQuitButton=256 +CFPageButton=16 +CFQuicktalker=4 +CFQuitButton=32 +CFReversed=64 +CFSndOpenchat=128 +CFSpeech=1 +CFThought=2 +CFTimeout=8 + +CCNormal = 0 +CCNoChat = 1 +CCNonPlayer = 2 +CCSuit = 3 +CCToonBuilding = 4 +CCSuitBuilding = 5 +CCHouseBuilding = 6 +CCSpeedChat = 7 +CCFreeChat = 8 + +NAMETAG_COLORS = { + CCNormal: ( + # Normal FG BG + ((0.3, 0.3, 0.7, 1.0), (0.8, 0.8, 0.8, 0.5), # Name + (0.0, 0.0, 0.0, 1.0), (1.0, 1.0, 1.0, 1.0)), # Chat + # Click FG BG + ((0.3, 0.3, 0.7, 1.0), (0.2, 0.2, 0.2, 0.6), # Name + (1.0, 0.5, 0.5, 1.0), (1.0, 1.0, 1.0, 1.0)), # Chat + # Hover FG BG + ((0.5, 0.5, 1.0, 1.0), (1.0, 1.0, 1.0, 1.0), # Name + (0.0, 0.6, 0.6, 1.0), (1.0, 1.0, 1.0, 1.0)), # Chat + # Disable FG BG + ((0.3, 0.3, 0.7, 1.0), (0.8, 0.8, 0.8, 0.5), # Name + (0.0, 0.0, 0.0, 1.0), (1.0, 1.0, 1.0, 1.0)), # Chat + ), + CCNoChat: ( + # Normal FG BG + ((0.8, 0.4, 0.0, 1.0), (0.8, 0.8, 0.8, 0.5), # Name + (0.0, 0.0, 0.0, 1.0), (1.0, 1.0, 1.0, 1.0)), # Chat + # Click FG BG + ((1.0, 0.5, 0.5, 1.0), (0.2, 0.2, 0.2, 0.6), # Name + (1.0, 0.5, 0.5, 1.0), (1.0, 1.0, 1.0, 1.0)), # Chat + # Hover FG BG + ((1.0, 0.5, 0.0, 1.0), (1.0, 1.0, 1.0, 1.0), # Name + (0.0, 0.6, 0.6, 1.0), (1.0, 1.0, 1.0, 1.0)), # Chat + # Disable FG BG + ((0.8, 0.4, 0.0, 1.0), (0.8, 0.8, 0.8, 0.5), # Name + (0.0, 0.0, 0.0, 1.0), (1.0, 1.0, 1.0, 1.0)), # Chat + ), + CCNonPlayer: ( + # Normal FG BG + ((0.8, 0.4, 0.0, 1.0), (0.8, 0.8, 0.8, 0.5), # Name + (0.0, 0.0, 0.0, 1.0), (1.0, 1.0, 1.0, 1.0)), # Chat + # Click FG BG + ((0.8, 0.4, 0.0, 1.0), (0.8, 0.8, 0.8, 0.5), # Name + (0.0, 0.0, 0.0, 1.0), (1.0, 1.0, 1.0, 1.0)), # Chat + # Hover FG BG + ((0.8, 0.4, 0.0, 1.0), (0.8, 0.8, 0.8, 0.5), # Name + (0.0, 0.0, 0.0, 1.0), (1.0, 1.0, 1.0, 1.0)), # Chat + # Disable FG BG + ((0.8, 0.4, 0.0, 1.0), (0.8, 0.8, 0.8, 0.5), # Name + (0.0, 0.0, 0.0, 1.0), (1.0, 1.0, 1.0, 1.0)), # Chat + ), + CCSuit: ( + # Normal FG BG + ((0.2, 0.2, 0.2, 1.0), (0.8, 0.8, 0.8, 0.5), # Name + (0.0, 0.0, 0.0, 1.0), (1.0, 1.0, 1.0, 1.0)), # Chat + # Click FG BG + ((0.2, 0.2, 0.2, 1.0), (0.2, 0.2, 0.2, 0.6), # Name + (1.0, 0.5, 0.5, 1.0), (1.0, 1.0, 1.0, 1.0)), # Chat + # Hover FG BG + ((0.4, 0.4, 0.4, 1.0), (1.0, 1.0, 1.0, 0.7), # Name + (0.0, 0.6, 0.6, 1.0), (1.0, 1.0, 1.0, 1.0)), # Chat + # Disable FG BG + ((0.2, 0.2, 0.2, 1.0), (0.8, 0.8, 0.8, 0.5), # Name + (0.0, 0.0, 0.0, 1.0), (1.0, 1.0, 1.0, 1.0)), # Chat + ), + CCSuitBuilding: ( + # Normal FG BG + ((0.5, 0.5, 0.5, 1.0), (0.8, 0.8, 0.8, 0.5), # Name + (0.0, 0.0, 0.0, 1.0), (1.0, 1.0, 1.0, 1.0)), # Chat + # Click FG BG + ((0.5, 0.5, 0.5, 1.0), (0.2, 0.2, 0.2, 0.6), # Name + (1.0, 0.5, 0.5, 1.0), (1.0, 1.0, 1.0, 1.0)), # Chat + # Hover FG BG + ((0.7, 0.7, 0.7, 1.0), (1.0, 1.0, 1.0, 0.7), # Name + (0.0, 0.6, 0.6, 1.0), (1.0, 1.0, 1.0, 1.0)), # Chat + # Disable FG BG + ((0.5, 0.5, 0.5, 1.0), (0.8, 0.8, 0.8, 0.5), # Name + (0.0, 0.0, 0.0, 1.0), (1.0, 1.0, 1.0, 1.0)), # Chat + ), + CCToonBuilding: ( + # Normal FG BG + ((0.2, 0.6, 0.9, 1.0), (0.8, 0.8, 0.8, 0.5), # Name + (0.0, 0.0, 0.0, 1.0), (1.0, 1.0, 1.0, 1.0)), # Chat + # Click FG BG + ((0.2, 0.6, 0.9, 1.0), (0.8, 0.8, 0.8, 0.5), # Name + (0.0, 0.0, 0.0, 1.0), (1.0, 1.0, 1.0, 1.0)), # Chat + # Hover FG BG + ((0.2, 0.6, 0.9, 1.0), (0.8, 0.8, 0.8, 0.5), # Name + (0.0, 0.0, 0.0, 1.0), (1.0, 1.0, 1.0, 1.0)), # Chat + # Disable FG BG + ((0.2, 0.6, 0.9, 1.0), (0.8, 0.8, 0.8, 0.5), # Name + (0.0, 0.0, 0.0, 1.0), (1.0, 1.0, 1.0, 1.0)), # Chat + ), + CCHouseBuilding: ( + # Normal FG BG + ((0.2, 0.6, 0.9, 1.0), (0.8, 0.8, 0.8, 0.5), # Name + (0.0, 0.0, 0.0, 1.0), (1.0, 1.0, 1.0, 1.0)), # Chat + # Click FG BG + ((0.2, 0.2, 0.5, 1.0), (0.2, 0.2, 0.2, 0.6), # Name + (1.0, 0.5, 0.5, 1.0), (1.0, 1.0, 1.0, 1.0)), # Chat + # Hover FG BG + ((0.5, 0.5, 1.0, 1.0), (1.0, 1.0, 1.0, 1.0), # Name + (0.0, 0.6, 0.6, 1.0), (1.0, 1.0, 1.0, 1.0)), # Chat + # Disable FG BG + ((0.0, 0.6, 0.2, 1.0), (0.8, 0.8, 0.8, 0.5), # Name + (0.0, 0.0, 0.0, 1.0), (1.0, 1.0, 1.0, 1.0)), # Chat + ), + CCSpeedChat: ( + # Normal FG BG + ((0.0, 0.6, 0.2, 1.0), (0.8, 0.8, 0.8, 0.5), # Name + (0.0, 0.0, 0.0, 1.0), (1.0, 1.0, 1.0, 1.0)), # Chat + # Click FG BG + ((0.0, 0.5, 0.0, 1.0), (0.5, 0.5, 0.5, 0.6), # Name + (1.0, 0.5, 0.5, 1.0), (1.0, 1.0, 1.0, 1.0)), # Chat + # Hover FG BG + ((0.0, 0.7, 0.2, 1.0), (1.0, 1.0, 1.0, 0.7), # Name + (0.0, 0.6, 0.6, 1.0), (1.0, 1.0, 1.0, 1.0)), # Chat + # Disable FG BG + ((0.0, 0.6, 0.2, 1.0), (0.8, 0.8, 0.8, 0.5), # Name + (0.0, 0.0, 0.0, 1.0), (1.0, 1.0, 1.0, 1.0)), # Chat + ), + CCFreeChat: ( + # Normal FG BG + ((0.3, 0.3, 0.7, 1.0), (0.8, 0.8, 0.8, 0.5), # Name + (0.0, 0.0, 0.0, 1.0), (1.0, 1.0, 1.0, 1.0)), # Chat + # Click FG BG + ((0.2, 0.2, 0.5, 1.0), (0.2, 0.2, 0.2, 0.6), # Name + (1.0, 0.5, 0.5, 1.0), (1.0, 1.0, 1.0, 1.0)), # Chat + # Hover FG BG + ((0.5, 0.5, 1.0, 1.0), (1.0, 1.0, 1.0, 1.0), # Name + (0.0, 0.6, 0.6, 1.0), (1.0, 1.0, 1.0, 1.0)), # Chat + # Disable FG BG + ((0.3, 0.3, 0.7, 1.0), (0.8, 0.8, 0.8, 0.5), # Name + (0.0, 0.0, 0.0, 1.0), (1.0, 1.0, 1.0, 1.0)), # Chat + ), +} + +ARROW_COLORS = { + CCSuit: (0.8, 0.4, 0.0, 1.0), +} + +DEFAULT_WORDWRAPS = { + CCNormal: 7.5, + CCNoChat: 7.5, + CCNonPlayer: 7.5, + CCSuit: 7.5, + CCToonBuilding: 8.5, + CCSuitBuilding: 8.5, + CCHouseBuilding: 10.0, + CCSpeedChat: 7.5, + CCFreeChat: 7.5 +} + +WTNormal = 0 +WTQuickTalker = 1 +WTSystem = 2 +WTBattleSOS = 3 +WTEmote = 4 +WTToontownBoardingGroup = 5 + +WHISPER_COLORS = { + WTNormal: ( + # Normal FG BG + ((0.0, 0.0, 0.0, 1.0), (0.2, 0.6, 0.8, 0.6)), + # Click FG BG + ((1.0, 0.5, 0.5, 1.0), (1.0, 1.0, 1.0, 0.8)), + # Hover FG BG + ((0.0, 0.0, 0.0, 1.0), (0.2, 0.7, 0.9, 0.6)), + # Disable FG BG + ((0.0, 0.0, 0.0, 1.0), (0.2, 0.7, 0.8, 0.6)), + ), + WTQuickTalker: ( + # Normal FG BG + ((0.0, 0.0, 0.0, 1.0), (0.2, 0.6, 0.8, 0.6)), + # Click FG BG + ((1.0, 0.5, 0.5, 1.0), (1.0, 1.0, 1.0, 0.8)), + # Hover FG BG + ((0.0, 0.0, 0.0, 1.0), (0.2, 0.7, 0.9, 0.6)), + # Disable FG BG + ((0.0, 0.0, 0.0, 1.0), (0.2, 0.7, 0.8, 0.6)), + ), + WTSystem: ( + # Normal FG BG + ((0.0, 0.0, 0.0, 1.0), (0.8, 0.3, 0.6, 0.6)), + # Click FG BG + ((1.0, 0.5, 0.5, 1.0), (1.0, 1.0, 1.0, 0.8)), + # Hover FG BG + ((0.0, 0.0, 0.0, 1.0), (0.8, 0.4, 1.0, 0.6)), + # Disable FG BG + ((0.0, 0.0, 0.0, 1.0), (0.8, 0.3, 0.6, 0.6)), + ), + # TODO: WTBattleSOS + WTEmote: ( + # Normal FG BG + ((0.0, 0.0, 0.0, 1.0), (0.9, 0.5, 0.1, 0.6)), + # Click FG BG + ((1.0, 0.5, 0.5, 1.0), (1.0, 1.0, 1.0, 0.8)), + # Hover FG BG + ((0.0, 0.0, 0.0, 1.0), (0.9, 0.6, 0.2, 0.6)), + # Disable FG BG + ((0.0, 0.0, 0.0, 1.0), (0.9, 0.6, 0.1, 0.6)), + ), + # TODO: WTToontownBoardingGroup +} diff --git a/otp/nametag/NametagFloat2d.py b/otp/nametag/NametagFloat2d.py new file mode 100644 index 00000000..258e2e97 --- /dev/null +++ b/otp/nametag/NametagFloat2d.py @@ -0,0 +1,7 @@ +from Nametag3d import * + +class NametagFloat2d(Nametag3d): + WANT_DYNAMIC_SCALING = False + SCALING_FACTOR = 1.0 + SHOULD_BILLBOARD = False + IS_3D = False diff --git a/otp/nametag/NametagFloat3d.py b/otp/nametag/NametagFloat3d.py new file mode 100644 index 00000000..0d331f58 --- /dev/null +++ b/otp/nametag/NametagFloat3d.py @@ -0,0 +1,7 @@ +from Nametag3d import * + +class NametagFloat3d(Nametag3d): + WANT_DYNAMIC_SCALING = False + SCALING_FACTOR = 1.0 + SHOULD_BILLBOARD = True + IS_3D = False diff --git a/otp/nametag/NametagGlobals.py b/otp/nametag/NametagGlobals.py new file mode 100644 index 00000000..d6c972b6 --- /dev/null +++ b/otp/nametag/NametagGlobals.py @@ -0,0 +1,100 @@ +camera = None +def setCamera(cam): + global camera + camera = cam + +arrowModel = None +def setArrowModel(am): + global arrowModel + arrowModel = am + +nametagCardModel = None +nametagCardDimensions = None +def setNametagCard(model, dimensions): + global nametagCardModel, nametagCardDimensions + nametagCardModel = model + nametagCardDimensions = dimensions + +mouseWatcher = None +def setMouseWatcher(mw): + global mouseWatcher + mouseWatcher = mw + +speechBalloon3d = None +def setSpeechBalloon3d(sb3d): + global speechBalloon3d + speechBalloon3d = sb3d + +thoughtBalloon3d = None +def setThoughtBalloon3d(tb3d): + global thoughtBalloon3d + thoughtBalloon3d = tb3d + +speechBalloon2d = None +def setSpeechBalloon2d(sb2d): + global speechBalloon2d + speechBalloon2d = sb2d + +thoughtBalloon2d = None +def setThoughtBalloon2d(tb2d): + global thoughtBalloon2d + thoughtBalloon2d = tb2d + +pageButtons = {} +def setPageButton(state, model): + pageButtons[state] = model + +quitButtons = {} +def setQuitButton(state, model): + quitButtons[state] = model + +rolloverSound = None +def setRolloverSound(ros): + global rolloverSound + rolloverSound = ros + +clickSound = None +def setClickSound(cs): + global clickSound + clickSound = cs + +toon = None +def setToon(t): + global toon + toon = t + +masterArrowsOn = 0 +def setMasterArrowsOn(mao): + global masterArrowsOn + masterArrowsOn = mao + +masterNametagsActive = 0 +def setMasterNametagsActive(mna): + global masterNametagsActive + masterNametagsActive = mna + +min2dAlpha = 0.0 +def setMin2dAlpha(m2a): + global min2dAlpha + min2dAlpha = m2a + +def getMin2dAlpha(): + global min2dAlpha + return min2dAlpha + +max2dAlpha = 0.0 +def setMax2dAlpha(m2a): + global max2dAlpha + max2dAlpha = m2a + +def getMax2dAlpha(): + global max2dAlpha + return max2dAlpha + +onscreenChatForced = 0 +def setOnscreenChatForced(ocf): + global onscreenChatForced + onscreenChatForced = ocf + +def setGlobalNametagScale(s): + pass \ No newline at end of file diff --git a/otp/nametag/NametagGroup.py b/otp/nametag/NametagGroup.py new file mode 100644 index 00000000..ea7af978 --- /dev/null +++ b/otp/nametag/NametagGroup.py @@ -0,0 +1,320 @@ +from pandac.PandaModules import * +from NametagConstants import * +from Nametag3d import * +from Nametag2d import * + +class NametagGroup: + CCNormal = CCNormal + CCNoChat = CCNoChat + CCNonPlayer = CCNonPlayer + CCSuit = CCSuit + CCToonBuilding = CCToonBuilding + CCSuitBuilding = CCSuitBuilding + CCHouseBuilding = CCHouseBuilding + CCSpeedChat = CCSpeedChat + CCFreeChat = CCFreeChat + + CHAT_TIMEOUT_MAX = 12.0 + CHAT_TIMEOUT_MIN = 4.0 + CHAT_TIMEOUT_PROP = 0.5 + + def __init__(self): + self.nametag2d = Nametag2d() + self.nametag3d = Nametag3d() + self.icon = PandaNode('icon') + + self.chatTimeoutTask = None + + self.font = None + self.speechFont = None + self.name = '' + self.displayName = '' + self.wordWrap = None + self.qtColor = VBase4(1,1,1,1) + self.colorCode = CCNormal + self.avatar = None + self.active = True + + self.chatPages = [] + self.chatPage = 0 + self.chatFlags = 0 + + self.objectCode = None + + self.manager = None + + self.nametags = [] + self.addNametag(self.nametag2d) + self.addNametag(self.nametag3d) + + self.visible3d = True # Is a 3D nametag visible, or do we need a 2D popup? + + self.tickTask = taskMgr.add(self.__tickTask, self.getUniqueId(), sort=45) + + self.stompTask = None + self.stompText = None + self.stompFlags = 0 + + def destroy(self): + taskMgr.remove(self.tickTask) + if self.manager is not None: + self.unmanage(self.manager) + for nametag in list(self.nametags): + self.removeNametag(nametag) + if self.stompTask: + self.stompTask.remove() + + def getNametag2d(self): + return self.nametag2d + + def getNametag3d(self): + return self.nametag3d + + def getNameIcon(self): + return self.icon + + def getNumChatPages(self): + if not self.chatFlags & (CFSpeech|CFThought): + return 0 + + return len(self.chatPages) + + def setPageNumber(self, page): + self.chatPage = page + self.updateTags() + + def getChatStomp(self): + return bool(self.stompTask) + + def getChat(self): + if self.chatPage >= len(self.chatPages): + return '' + else: + return self.chatPages[self.chatPage] + + def getStompText(self): + return self.stompText + + def getStompDelay(self): + return 0.2 + + def getUniqueId(self): + return 'Nametag-%d' % id(self) + + def hasButton(self): + return bool(self.getButtons()) + + def getButtons(self): + if self.getNumChatPages() < 2: + # Either only one page or no pages displayed. This means no button, + # unless the game code specifically requests one. + if self.chatFlags & CFQuitButton: + return NametagGlobals.quitButtons + elif self.chatFlags & CFPageButton: + return NametagGlobals.pageButtons + else: + return None + elif self.chatPage == self.getNumChatPages()-1: + # Last page of a multiple-page chat. This calls for a quit button, + # unless the game says otherwise. + if not self.chatFlags & CFNoQuitButton: + return NametagGlobals.quitButtons + else: + return None + else: + # Non-last page of a multiple-page chat. This calls for a page + # button, but only if the game requests it: + if self.chatFlags & CFPageButton: + return NametagGlobals.pageButtons + else: + return None + + def setActive(self, active): + self.active = active + + def isActive(self): + return self.active + + def setAvatar(self, avatar): + self.avatar = avatar + + def setFont(self, font): + self.font = font + self.updateTags() + + def setSpeechFont(self, font): + self.speechFont = font + self.updateTags() + + def setWordwrap(self, wrap): + self.wordWrap = wrap + self.updateTags() + + def setColorCode(self, cc): + self.colorCode = cc + self.updateTags() + + def setName(self, name): + self.name = name + self.updateTags() + + def setDisplayName(self, name): + self.displayName = name + self.updateTags() + + def setQtColor(self, color): + self.qtColor = color + self.updateTags() + + def setChat(self, chatString, chatFlags): + if not self.chatFlags&CFSpeech: + # We aren't already displaying some chat. Therefore, we don't have + # to stomp. + self._setChat(chatString, chatFlags) + else: + # Stomp! + self.clearChat() + self.stompText = chatString + self.stompFlags = chatFlags + self.stompTask = taskMgr.doMethodLater(self.getStompDelay(), self.__updateStomp, + 'ChatStomp-' + self.getUniqueId()) + + def _setChat(self, chatString, chatFlags): + if chatString: + self.chatPages = chatString.split('\x07') + self.chatFlags = chatFlags + else: + self.chatPages = [] + self.chatFlags = 0 + self.setPageNumber(0) # Calls updateTags() for us. + + self._stopChatTimeout() + if chatFlags&CFTimeout: + self._startChatTimeout() + + def __updateStomp(self, task): + self._setChat(self.stompText, self.stompFlags) + self.stompTask = None + + def setContents(self, contents): + # This function is a little unique, it's meant to override contents on + # EXISTING nametags only: + for tag in self.nametags: + tag.setContents(contents) + + def setObjectCode(self, objectCode): + self.objectCode = objectCode + + def getObjectCode(self): + return self.objectCode + + def _startChatTimeout(self): + length = len(self.getChat()) + timeout = min(max(length*self.CHAT_TIMEOUT_PROP, self.CHAT_TIMEOUT_MIN), self.CHAT_TIMEOUT_MAX) + self.chatTimeoutTask = taskMgr.doMethodLater(timeout, self.__doChatTimeout, + 'ChatTimeout-' + self.getUniqueId()) + + def __doChatTimeout(self, task): + self._setChat('', 0) + return task.done + + def _stopChatTimeout(self): + if self.chatTimeoutTask: + taskMgr.remove(self.chatTimeoutTask) + + def clearShadow(self): + pass + + def clearChat(self): + self._setChat('', 0) + if self.stompTask: + self.stompTask.remove() + + def updateNametag(self, tag): + tag.font = self.font + tag.speechFont = self.speechFont + tag.name = self.name + tag.wordWrap = self.wordWrap or DEFAULT_WORDWRAPS[self.colorCode] + tag.displayName = self.displayName or self.name + tag.qtColor = self.qtColor + tag.colorCode = self.colorCode + tag.chatString = self.getChat() + tag.buttons = self.getButtons() + tag.chatFlags = self.chatFlags + tag.avatar = self.avatar + tag.icon = self.icon + + tag.update() + + def __testVisible3D(self): + # We must determine if a 3D nametag is visible or not, since this + # affects the visibility state of 2D nametags. + + # Next, we iterate over all of our nametags until we find a visible + # one: + for nametag in self.nametags: + if not isinstance(nametag, Nametag3d): + continue # It's not in the 3D system, disqualified. + + if nametag.isOnScreen(): + return True + + # If we got here, none of the tags were a match... + return False + + + def __tickTask(self, task): + for nametag in self.nametags: + nametag.tick() + if (NametagGlobals.masterNametagsActive and self.active) or self.hasButton(): + nametag.setClickRegionEvent(self.getUniqueId()) + else: + nametag.setClickRegionEvent(None) + + if NametagGlobals.onscreenChatForced and self.chatFlags & CFSpeech: + # Because we're *forcing* chat onscreen, we skip the visible3d test + # and go ahead and display it anyway. + visible3d = False + elif not NametagGlobals.masterArrowsOn and not self.chatFlags: + # We're forcing margins offscreen; therefore, we should pretend + # that the 3D nametag is always visible. + visible3d = True + else: + visible3d = self.__testVisible3D() + + if visible3d ^ self.visible3d: + self.visible3d = visible3d + for nametag in self.nametags: + if isinstance(nametag, MarginPopup): + nametag.setVisible(not visible3d) + + return task.cont + + def updateTags(self): + for nametag in self.nametags: + self.updateNametag(nametag) + + def addNametag(self, nametag): + self.nametags.append(nametag) + self.updateNametag(nametag) + if self.manager is not None and isinstance(nametag, MarginPopup): + nametag.manage(manager) + + def removeNametag(self, nametag): + self.nametags.remove(nametag) + if self.manager is not None and isinstance(nametag, MarginPopup): + nametag.unmanage(manager) + nametag.destroy() + + def manage(self, manager): + self.manager = manager + for tag in self.nametags: + if isinstance(tag, MarginPopup): + tag.manage(manager) + + def unmanage(self, manager): + self.manager = None + for tag in self.nametags: + if isinstance(tag, MarginPopup): + tag.unmanage(manager) + tag.destroy() diff --git a/otp/nametag/__init__.py b/otp/nametag/__init__.py new file mode 100644 index 00000000..e69de29b