mirror of
https://github.com/Sneed-Group/Poodletooth-iLand
synced 2024-12-23 19:52:37 -06:00
Push TTR nametag code
This commit is contained in:
parent
5044724410
commit
84e8ad4c0d
17 changed files with 1606 additions and 3 deletions
|
@ -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'
|
||||
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
|
||||
|
|
162
otp/margins/ClickablePopup.py
Normal file
162
otp/margins/ClickablePopup.py
Normal file
|
@ -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
|
83
otp/margins/MarginCell.py
Normal file
83
otp/margins/MarginCell.py
Normal file
|
@ -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
|
83
otp/margins/MarginManager.py
Normal file
83
otp/margins/MarginManager.py
Normal file
|
@ -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)
|
52
otp/margins/MarginPopup.py
Normal file
52
otp/margins/MarginPopup.py
Normal file
|
@ -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
|
97
otp/margins/WhisperPopup.py
Normal file
97
otp/margins/WhisperPopup.py
Normal file
|
@ -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()
|
0
otp/margins/__init__.py
Normal file
0
otp/margins/__init__.py
Normal file
93
otp/nametag/ChatBalloon.py
Normal file
93
otp/nametag/ChatBalloon.py
Normal file
|
@ -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
|
156
otp/nametag/Nametag.py
Normal file
156
otp/nametag/Nametag.py
Normal file
|
@ -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)
|
113
otp/nametag/Nametag2d.py
Normal file
113
otp/nametag/Nametag2d.py
Normal file
|
@ -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
|
70
otp/nametag/Nametag3d.py
Normal file
70
otp/nametag/Nametag3d.py
Normal file
|
@ -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
|
216
otp/nametag/NametagConstants.py
Normal file
216
otp/nametag/NametagConstants.py
Normal file
|
@ -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
|
||||
}
|
7
otp/nametag/NametagFloat2d.py
Normal file
7
otp/nametag/NametagFloat2d.py
Normal file
|
@ -0,0 +1,7 @@
|
|||
from Nametag3d import *
|
||||
|
||||
class NametagFloat2d(Nametag3d):
|
||||
WANT_DYNAMIC_SCALING = False
|
||||
SCALING_FACTOR = 1.0
|
||||
SHOULD_BILLBOARD = False
|
||||
IS_3D = False
|
7
otp/nametag/NametagFloat3d.py
Normal file
7
otp/nametag/NametagFloat3d.py
Normal file
|
@ -0,0 +1,7 @@
|
|||
from Nametag3d import *
|
||||
|
||||
class NametagFloat3d(Nametag3d):
|
||||
WANT_DYNAMIC_SCALING = False
|
||||
SCALING_FACTOR = 1.0
|
||||
SHOULD_BILLBOARD = True
|
||||
IS_3D = False
|
100
otp/nametag/NametagGlobals.py
Normal file
100
otp/nametag/NametagGlobals.py
Normal file
|
@ -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
|
320
otp/nametag/NametagGroup.py
Normal file
320
otp/nametag/NametagGroup.py
Normal file
|
@ -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()
|
0
otp/nametag/__init__.py
Normal file
0
otp/nametag/__init__.py
Normal file
Loading…
Reference in a new issue