class Menu(object): ''' @param name the caption of this menu @param items a sequence of MenuItem objects to be displayed in this menu ''' def __init__(self, name, items=(), listener=None): self.name = name self.items = list(items) self.onAction = Event() # (action) if listener is not None: self.onAction.addListener(listener) for item in items: item.onAction.addListener(self.onAction.execute) def popItem(self, index): item = self.items.pop(index) item.onAction.removeListener(self.onAction.execute) return item def removeItem(self, item): self.popItem(self.items.index(item)) def insertItem(self, pos, item): self.items.insert(pos, item) item.onAction.addListener(self.onAction.execute) def appendItem(self, item): self.insertItem(len(self.items), item)
class GameVoteMenu(CompoundElement): def __init__(self, app, world, onChange=None): CompoundElement.__init__(self, app) self.world = world self.playerId = NO_PLAYER self.readyMenu = ReadyMenu(self, 0) self.levelMenu = LevelMenu(self, MENU_WIDTH) self.teamMenu = TeamMenu(self, 2 * MENU_WIDTH) self.sizeMenu = SizeMenu(self, 3 * MENU_WIDTH) self.durationMenu = DurationMenu(self, 4 * MENU_WIDTH) self.onChange = Event() if onChange is not None: self.onChange.addListener(onChange) self.subMenu = GameVoteSubMenu(app) font = app.screenManager.fonts.ingameMenuFont self.elements = [ SolidRect( app, (0, 64, 192), None, Region(topleft=Abs(0, 0), width=Abs(4 * MENU_WIDTH), height=Abs(font.getHeight(app) + 2))), self.subMenu, self.readyMenu.button, self.levelMenu.button, self.teamMenu.button, self.sizeMenu.button, self.durationMenu.button, ] def update(self, player): self.playerId = player.id self.readyMenu.updateTitle(player) self.levelMenu.updateTitle(player) self.teamMenu.updateTitle(player) self.sizeMenu.updateTitle(player) self.durationMenu.updateTitle(player) if self.subMenu.manager.menu is not None: self.subMenu.manager.menu.update() def showSubMenu(self, menu): menu.update() if self.subMenu.manager.menu is menu or self.subMenu.hidden: self.subMenu.hide() self.subMenu.manager.setDefaultMenu(menu) self.subMenu.location = menu.button.pos def hideSubMenu(self): if not self.subMenu.hidden: self.subMenu.hide()
class SettingsMenu(framework.CompoundElement): def __init__(self, app, onClose=None, onRestart=None, showThemes=False, showSound=True, showDisplay=True, showKeymap=True): super(SettingsMenu, self).__init__(app) self.onClose = Event() if onClose is not None: self.onClose.addListener(onClose) self.onRestart = Event() if onRestart is not None: self.onRestart.addListener(onRestart) area = ScaledArea(50, 140, 924, 570) bg = pygame.Surface((924, 500)) bg.fill(app.theme.colours.settingsMenu) if app.displaySettings.alphaOverlays: bg.set_alpha(192) font = app.screenManager.fonts.bigMenuFont self.tabContainer = TabContainer(self.app, area, font, app.theme.colours.settingsTabBorder) bp = elements.SizedPicture( app, bg, Location(AttachedPoint((0, 0), self.tabContainer._getTabRect)), TabSize(self.tabContainer)) if showDisplay: displayTab = DisplaySettingsTab(app, onClose=self.onClose.execute) self.tabContainer.addTab(displayTab) if showKeymap: keymapTab = KeymapTab(app, onClose=self.onClose.execute) self.tabContainer.addTab(keymapTab) if showSound: soundTab = SoundSettingsTab(app, onClose=self.onClose.execute) self.tabContainer.addTab(soundTab) if showThemes: themeTab = ThemeTab(app, onClose=self.onClose.execute, onRestart=self.onRestart.execute) self.tabContainer.addTab(themeTab) self.elements = [bp, self.tabContainer]
class ConnectionFailedDialog(DialogBox): def __init__(self, app, reason): width = app.screenManager.scaleFactor * absoluteWidth font = app.fonts.connectionFailedFont boundary = app.screenManager.scaleFactor * 20 lines = wrapWords(app, reason, font, width - 2 * boundary) x = boundary elements = [] colours = app.theme.colours for text in lines: elements.append( TextElement(app, text, font, Location( DialogBoxAttachedPoint(self, (0, x), 'midtop'), 'midtop'), colour=colours.errorColour)) x += font.getHeight(app) # Boundary * 3 for top, bottom, and middle height = boundary * 3 + font.getHeight(app) * (len(lines) + 1) size = ScaledSize(absoluteWidth, height) super(ConnectionFailedDialog, self).__init__(app, size, 'Connection Failed') self.elements = elements self.elements.append( TextButton(app, Location( DialogBoxAttachedPoint(self, (0, -boundary), 'midbottom'), 'midbottom'), 'OK', font, colours.dialogButtonColour, colours.radioMouseover, onClick=lambda sender: self.close())) self.onEnter = Event() self.onEnter.addListener(self.close) def processEvent(self, event): if event.type == pygame.KEYDOWN and event.key in (pygame.K_KP_ENTER, pygame.K_RETURN): self.onEnter.execute() return None else: return super(ConnectionFailedDialog, self).processEvent(event)
class MenuItem(object): ''' @param name the caption of the menu item @param action a callable for when the item is selected ''' def __init__(self, name, action=None, listener=None, enabled=True): self.name = name self.onClick = Event() # (MenuItem) self.onAction = Event() # (action) self.action = action self.enabled = enabled if listener is not None: self.onClick.addListener(listener) def execute(self): self.onClick.execute(self) if self.action is not None: self.onAction.execute(self.action)
class PractiseScreen(framework.CompoundElement): def __init__(self, app, onClose=None, onStart=None): super(PractiseScreen, self).__init__(app) self.onClose = Event() if onClose is not None: self.onClose.addListener(onClose) self.onStart = Event() if onStart is not None: self.onStart.addListener(onStart) if app.displaySettings.alphaOverlays: alpha = 192 else: alpha = None bg = SolidRect(app, app.theme.colours.playMenu, alpha, Region(centre=Canvas(512, 384), size=Canvas(924, 500))) #colour = app.theme.colours.mainMenuColour #font = app.screenManager.fonts.consoleFont font = app.screenManager.fonts.bigMenuFont cancel = TextButton(app, Location(Canvas(512, 624), 'midbottom'), 'Cancel', font, app.theme.colours.secondMenuColour, app.theme.colours.white, onClick=self.cancel) self.elements = [bg, cancel] def cancel(self, element): self.onClose.execute() def startGame(self): db = self.app.layoutDatabase game = LocalGame(db, SIZE[0], SIZE[1], onceOnly=True) for i in range(AICOUNT): game.addBot('ranger') self.onStart.execute(game)
class Hotkeys(Element): ''' Traps any keys which occur in the mapping and carries out some action on them. Note that this observes only, but does not stop the keypress events from passing through. ''' def __init__(self, app, mapping, onActivated=None): super(Hotkeys, self).__init__(app) self.mapping = mapping self.onActivated = Event() if onActivated is not None: self.onActivated.addListener(onActivated) def processEvent(self, event): if event.type == pygame.KEYDOWN: try: action = self.mapping[event.key] except KeyError: return event # Process this hotkey. self.onActivated.execute(action) return event def __repr__(self): if isinstance(self.key, int): key = self.key else: # Calculate the physical key. for key, value in self.mapping.iteritems(): if value == self.key: break else: return '??' return keyboard.shortcutName(key, 0)
class GameInterface(framework.CompoundElement, ConcreteAgent): '''Interface for when we are connected to a game.''' achievementDefs = availableAchievements def __init__(self, app, game, onDisconnectRequest=None, onConnectionLost=None, replay=False, authTag=0): super(GameInterface, self).__init__(app, game=game) self.localState.onShoxwave.addListener(self.localShoxwaveFired) self.localState.onGameInfoChanged.addListener(self.gameInfoChanged) self.world.onOpenChatReceived.addListener(self.openChat) self.world.onTeamChatReceived.addListener(self.teamChat) self.world.onReset.addListener(self.worldReset) self.world.onGrenadeExplosion.addListener(self.grenadeExploded) self.world.onTrosballExplosion.addListener(self.trosballExploded) self.world.onBomberExplosion.addListener(self.trosballExploded) self.world.uiOptions.onChange.addListener(self.uiOptionsChanged) self.authTag = authTag self.subscribedPlayers = set() self.onDisconnectRequest = Event() if onDisconnectRequest is not None: self.onDisconnectRequest.addListener(onDisconnectRequest) self.onConnectionLost = Event() if onConnectionLost is not None: self.onConnectionLost.addListener(onConnectionLost) self.game = game self.joinDialogReason = None self.keyMapping = keyboard.KeyboardMapping(keymap.default_game_keys) self.runningPlayerInterface = None self.updateKeyMapping() self.gameViewer = viewManager.GameViewer(self.app, self, game, replay) if replay: self.joinController = None else: self.joinController = JoinGameController(self.app, self, self.game) self.detailsInterface = DetailsInterface(self.app, self) self.winnerMsg = WinnerMsg(app) self.timingInfo = TimingInfo( app, self, Location(Canvas(307, 768), 'midbottom'), app.screenManager.fonts.consoleFont, ) self.gameInfoDisplay = GameInfoDisplay( app, self, Region(topleft=Screen(0.01, 0.05), size=Canvas(330, 200))) self.hotkeys = hotkey.Hotkeys(self.app, self.keyMapping, self.detailsInterface.doAction) self.terminal = None self.joinInProgress = False self.vcInterface = None if replay: self.vcInterface = ViewControlInterface(self.app, self) self.ready = False defer.maybeDeferred(game.addAgent, self).addCallback(self.addedAgent) self.setElements() def gameInfoChanged(self): self.gameInfoDisplay.refreshInfo() def addedAgent(self, result): self.ready = True if self.joinController: self.joinDialogReason = 'automatic' self.joinController.start() def spectatorWantsToJoin(self): if self.runningPlayerInterface or not self.joinController: return self.joinDialogReason = 'from menu' self.joinController.maybeShowJoinDialog(autoJoin=True) def sendRequest(self, msg): if not self.ready: # Not yet completely connected to game return super(GameInterface, self).sendRequest(msg) def worldReset(self, *args, **kwarsg): self.winnerMsg.hide() if self.ready and self.joinController: self.joinController.gotWorldReset() self.gameViewer.reset() def updateKeyMapping(self): # Set up the keyboard mapping. try: # Try to load keyboard mappings from the user's personal settings. config = open(getPath(user, 'keymap'), 'rU').read() self.keyMapping.load(config) if self.runningPlayerInterface: self.runningPlayerInterface.keyMappingUpdated() except IOError: pass @ConnectionLostMsg.handler def connectionLost(self, msg): self.cleanUp() if self.joinController: self.joinController.hide() self.onConnectionLost.execute() def joined(self, player): '''Called when joining of game is successful.''' pygame.key.set_repeat() self.gameViewer.worldgui.overridePlayer(self.localState.player) self.runningPlayerInterface = pi = PlayerInterface(self.app, self) self.detailsInterface.setPlayer(pi.player) self.setElements() self.joinController.hide() self.gameViewer.leaderboard.update() def spectate(self): ''' Called by join controller if user selects to only spectate. ''' self.vcInterface = ViewControlInterface(self.app, self) self.setElements() self.joinController.hide() def joinDialogCancelled(self): if self.joinDialogReason == 'automatic': self.disconnect() else: self.spectate() def stop(self): super(GameInterface, self).stop() self.localState.onShoxwave.removeListener(self.localShoxwaveFired) self.localState.onGameInfoChanged.removeListener(self.gameInfoChanged) self.world.onOpenChatReceived.removeListener(self.openChat) self.world.onTeamChatReceived.removeListener(self.teamChat) self.world.onReset.removeListener(self.worldReset) self.world.onGrenadeExplosion.removeListener(self.grenadeExploded) self.world.onTrosballExplosion.removeListener(self.trosballExploded) self.world.onBomberExplosion.removeListener(self.trosballExploded) self.world.uiOptions.onChange.removeListener(self.uiOptionsChanged) self.gameViewer.stop() if self.runningPlayerInterface is not None: self.runningPlayerInterface.stop() def setElements(self): spectate = replay = False if self.runningPlayerInterface: self.elements = [ self.gameViewer, self.runningPlayerInterface, self.gameInfoDisplay, self.hotkeys, self.detailsInterface, self.winnerMsg, self.timingInfo ] else: self.elements = [ self.gameViewer, self.gameInfoDisplay, self.hotkeys, self.detailsInterface, self.winnerMsg, self.timingInfo ] if self.vcInterface is not None: self.elements.insert(2, self.vcInterface) if self.joinController: spectate = True else: replay = True self.detailsInterface.menuManager.setMode(spectate=spectate, replay=replay) def toggleTerminal(self): if self.terminal is None: locs = {'app': self.app} if hasattr(self.app, 'getConsoleLocals'): locs.update(self.app.getConsoleLocals()) self.terminal = console.TrosnothInteractiveConsole( self.app, self.app.screenManager.fonts.consoleFont, Region(size=Screen(1, 0.4), bottomright=Screen(1, 1)), locals=locs) self.terminal.interact().addCallback(self._terminalQuit) from trosnoth.utils.utils import timeNow if self.terminal in self.elements: if timeNow() > self._termWaitTime: self.elements.remove(self.terminal) else: self._termWaitTime = timeNow() + 0.1 self.elements.append(self.terminal) self.setFocus(self.terminal) def _terminalQuit(self, result): if self.terminal in self.elements: self.elements.remove(self.terminal) self.terminal = None def disconnect(self): self.cleanUp() self.onDisconnectRequest.execute() def joinGame(self, nick, team, timeout=10): if self.joinInProgress: return if team is None: teamId = NEUTRAL_TEAM_ID else: teamId = team.id self.joinInProgress = True self.sendJoinRequest(teamId, nick, authTag=self.authTag) WeakCallLater(timeout, self, '_joinTimedOut') def setPlayer(self, player): if not player: self.gameViewer.worldgui.removeOverride() self.lostPlayer() super(GameInterface, self).setPlayer(player) if player: if __debug__ and globaldebug.enabled: globaldebug.localPlayerId = player.id self.joinInProgress = False self.joined(player) @CannotJoinMsg.handler def joinFailed(self, msg): self.joinInProgress = False self.joinController.joinFailed(msg.reasonId) def _joinTimedOut(self): if self.player or not self.joinInProgress: return self.joinInProgress = False self.joinController.joinFailed('timeout') def cleanUp(self): if self.gameViewer.timerBar is not None: self.gameViewer.timerBar = None pygame.key.set_repeat(300, 30) def uiOptionsChanged(self): if self.world.uiOptions.showGameOver: winner = self.world.uiOptions.winningTeam if winner: self.winnerMsg.show( '{} win'.format(winner), self.app.theme.colours.chatColour(winner), ) else: self.winnerMsg.show('Game drawn', (128, 128, 128)) else: self.winnerMsg.hide() @PlayerCoinsSpentMsg.handler def discard(self, msg): pass @AwardPlayerCoinMsg.handler def playerAwardedCoin(self, msg): if not self.localState.player: return if msg.sound and msg.playerId == self.localState.player.id: self.playSound('gotCoin') @PlayerHasElephantMsg.handler def gotElephant(self, msg, _lastElephantPlayer=[None]): player = self.world.getPlayer(msg.playerId) if player and player != _lastElephantPlayer[0]: message = '%s now has Jerakeen!' % (player.nick, ) self.detailsInterface.newMessage(message) _lastElephantPlayer[0] = player @PlayerHasTrosballMsg.handler def gotTrosball(self, msg, _lastTrosballPlayer=[None]): player = self.world.playerWithId.get(msg.playerId) if player != _lastTrosballPlayer[0]: _lastTrosballPlayer[0] = player if player is None: message = 'The ball has been dropped!' else: message = '%s has the ball!' % (player.nick, ) self.detailsInterface.newMessage(message) @AddPlayerMsg.handler def addPlayer(self, msg): player = self.world.getPlayer(msg.playerId) if player and player not in self.subscribedPlayers: self.subscribedPlayers.add(player) team = player.team if player.team else self.world.rogueTeamName message = '%s has joined %s' % (player.nick, team) self.detailsInterface.newMessage(message) player.onDied.addListener(partial(self.playerDied, player)) @SetPlayerTeamMsg.handler def changeTeam(self, msg): self.defaultHandler(msg) # Make sure the local player changes team player = self.world.getPlayer(msg.playerId) if player: message = '%s has joined %s' % (player.nick, self.world.getTeamName(msg.teamId)) self.detailsInterface.newMessage(message) @RemovePlayerMsg.handler def handle_RemovePlayerMsg(self, msg): player = self.world.getPlayer(msg.playerId) if player: message = '%s has left the game' % (player.nick, ) self.detailsInterface.newMessage(message) self.subscribedPlayers.discard(player) def lostPlayer(self): self.runningPlayerInterface.stop() self.runningPlayerInterface = None self.setElements() @CannotBuyUpgradeMsg.handler def notEnoughCoins(self, msg): if msg.reasonId == NOT_ENOUGH_COINS_REASON: text = 'Your team does not have enough coins.' elif msg.reasonId == CANNOT_REACTIVATE_REASON: text = 'You already have that item.' elif msg.reasonId == PLAYER_DEAD_REASON: text = 'You cannot buy an upgrade while dead.' elif msg.reasonId == GAME_NOT_STARTED_REASON: text = 'Upgrades can' 't be bought at this time.' elif msg.reasonId == ALREADY_TURRET_REASON: text = 'There is already a turret in this zone.' elif msg.reasonId == TOO_CLOSE_TO_EDGE_REASON: text = 'You are too close to the zone edge.' elif msg.reasonId == TOO_CLOSE_TO_ORB_REASON: text = 'You are too close to the orb.' elif msg.reasonId == NOT_IN_DARK_ZONE_REASON: text = 'You are not in a dark friendly zone.' elif msg.reasonId == INVALID_UPGRADE_REASON: text = 'Upgrade not recognised by server.' elif msg.reasonId == DISABLED_UPGRADE_REASON: text = 'That upgrade is currently disabled.' else: text = 'You cannot buy that item at this time.' self.detailsInterface.newMessage(text) self.defaultHandler(msg) @PlayerHasUpgradeMsg.handler def gotUpgrade(self, msg): player = self.world.getPlayer(msg.playerId) if player: self.detailsInterface.upgradeUsed(player, msg.upgradeType) upgradeClass = self.world.getUpgradeType(msg.upgradeType) existing = player.items.get(upgradeClass) if not existing: if (self.detailsInterface.player is None or self.detailsInterface.player.isFriendsWith(player)): if upgradeClass == Turret: self.playSound('turret') else: self.playSound('buyUpgrade') self.defaultHandler(msg) @ChatFromServerMsg.handler def gotChatFromServer(self, msg): self.detailsInterface.newMessage(msg.text.decode(), error=msg.error) @TaggingZoneMsg.handler def zoneTagged(self, msg): try: zone = self.world.zoneWithId[msg.zoneId] zoneLabel = zone.defn.label except KeyError: zoneLabel = '<?>' if msg.playerId != NO_PLAYER: try: player = self.world.playerWithId[msg.playerId] except KeyError: nick = '<?>' else: nick = player.nick message = '%s tagged zone %s' % (nick, zoneLabel) self.detailsInterface.newMessage(message) def playerDied(self, target, killer, deathType): if deathType == OFF_MAP_DEATH: messages = [ 'fell into the void', 'looked into the abyss', 'dug too greedily and too deep' ] message = '%s %s' % (target.nick, random.choice(messages)) elif deathType == TROSBALL_DEATH: message = '%s was killed by the Trosball' % (target.nick, ) elif deathType == BOMBER_DEATH: message = '%s head asplode' % (target.nick, ) thisPlayer = self.detailsInterface.player if thisPlayer and target.id == thisPlayer.id: self.detailsInterface.doAction('no upgrade') else: if killer is None: message = '%s was killed' % (target.nick, ) self.detailsInterface.newMessage(message) else: message = '%s killed %s' % (killer.nick, target.nick) self.detailsInterface.newMessage(message) @RespawnMsg.handler def playerRespawn(self, msg): player = self.world.getPlayer(msg.playerId) if player: message = '%s is back in the game' % (player.nick, ) self.detailsInterface.newMessage(message) @CannotRespawnMsg.handler def respawnFailed(self, msg): if msg.reasonId == GAME_NOT_STARTED_REASON: message = 'The game has not started yet.' elif msg.reasonId == ALREADY_ALIVE_REASON: message = 'You are already alive.' elif msg.reasonId == BE_PATIENT_REASON: message = 'You cannot respawn yet.' elif msg.reasonId == ENEMY_ZONE_REASON: message = 'Cannot respawn outside friendly zone.' elif msg.reasonId == FROZEN_ZONE_REASON: message = 'That zone has been frozen!' else: message = 'You cannot respawn here.' self.detailsInterface.newMessage( message, self.app.theme.colours.errorMessageColour) def sendPrivateChat(self, player, targetId, text): self.sendRequest(ChatMsg(PRIVATE_CHAT, targetId, text=text.encode())) def sendTeamChat(self, player, text): self.sendRequest(ChatMsg(TEAM_CHAT, player.teamId, text=text.encode())) def sendPublicChat(self, player, text): self.sendRequest(ChatMsg(OPEN_CHAT, text=text.encode())) def openChat(self, text, sender): text = ': ' + text self.detailsInterface.newChat(text, sender) def teamChat(self, team, text, sender): player = self.detailsInterface.player if player and player.isFriendsWithTeam(team): text = " (team): " + text self.detailsInterface.newChat(text, sender) @AchievementUnlockedMsg.handler def achievementUnlocked(self, msg): player = self.world.getPlayer(msg.playerId) if not player: return achievementName = self.achievementDefs.getAchievementDetails( msg.achievementId)[0] self.detailsInterface.newMessage( '%s has unlocked "%s"!' % (player.nick, achievementName), self.app.theme.colours.achievementMessageColour) focusPlayer = self.detailsInterface.player if (focusPlayer is not None and focusPlayer.id == msg.playerId): self.detailsInterface.localAchievement(msg.achievementId) @ShotFiredMsg.handler def shotFired(self, msg): self.defaultHandler(msg) try: shot = self.world.getShot(msg.shotId) except KeyError: return pos = shot.pos dist = self.distance(pos) self.playSound('shoot', self.getSoundVolume(dist)) def grenadeExploded(self, pos, radius): self.gameViewer.worldgui.addExplosion(pos) dist = self.distance(pos) self.playSound('explodeGrenade', self.getSoundVolume(dist)) def trosballExploded(self, player): self.gameViewer.worldgui.addTrosballExplosion(player.pos) dist = self.distance(player.pos) self.playSound('explodeGrenade', self.getSoundVolume(dist)) @FireShoxwaveMsg.handler def shoxwaveExplosion(self, msg): localPlayer = self.localState.player if localPlayer and msg.playerId == localPlayer.id: return self.gameViewer.worldgui.addShoxwaveExplosion((msg.xpos, msg.ypos)) def localShoxwaveFired(self): localPlayer = self.localState.player self.gameViewer.worldgui.addShoxwaveExplosion(localPlayer.pos) @UpgradeChangedMsg.handler def upgradeChanged(self, msg): self.detailsInterface.upgradeDisplay.refresh() def distance(self, pos): return distance(self.gameViewer.viewManager.getTargetPoint(), pos) def getSoundVolume(self, distance): 'The volume for something that far away from the player' # Up to 500px away is within the "full sound zone" - full sound distFromScreen = max(0, distance - 500) # 1000px away from "full sound zone" is 0 volume: return 1 - min(1, (distFromScreen / 1000.)) def playSound(self, action, volume=1): self.app.soundPlayer.play(action, volume) @PlaySoundMsg.handler def playSoundFromServerCommand(self, msg): self.app.soundPlayer.playFromServerCommand( msg.filename.decode('utf-8')) @TickMsg.handler def handle_TickMsg(self, msg): super(GameInterface, self).handle_TickMsg(msg) self.timingInfo.ticksSeen += 1
class HoverButton(Element): '''A button which changes image when the mouse hovers over it. @param pos should be a trosnoth.ui.common.Location instance ''' def __init__(self, app, pos, stdImage, hvrImage, hotkey=None, onClick=None): super(HoverButton, self).__init__(app) self.stdImage = stdImage self.hvrImage = hvrImage self.pos = pos self.onClick = Event() self.hotkey = hotkey self.mouseOver = False if onClick is not None: self.onClick.addListener(onClick) def _getSurfaceToBlit(self): img = self._getImageToUse() if hasattr(img, 'getImage'): return img.getImage(self.app) else: return img def _getImageToUse(self): if self.mouseOver: return self.hvrImage else: return self.stdImage def _getSize(self): img = self._getImageToUse() if hasattr(img, 'getSize'): return img.getSize(self.app) else: # pygame.surface.Surface return img.get_size() def _getPt(self): if hasattr(self.pos, 'apply'): rect = pygame.Rect((0, 0), self._getSize()) self.pos.apply(self.app, rect) return rect.topleft else: return self.pos def _getRect(self): if hasattr(self.pos, 'apply'): rect = pygame.Rect((0, 0), self._getSize()) self.pos.apply(self.app, rect) return rect return pygame.Rect(self.pos, self._getSize()) def processEvent(self, event): rect = self._getRect() # Passive events. if event.type == pygame.MOUSEMOTION: self.mouseOver = rect.collidepoint(event.pos) # Active events. if (event.type == pygame.MOUSEBUTTONDOWN and event.button == 1 and rect.collidepoint(event.pos)): self.onClick.execute(self) elif event.type == pygame.KEYDOWN and event.key == self.hotkey: self.onClick.execute(self) else: return event return None def draw(self, screen): # Draw the button. screen.blit(self._getSurfaceToBlit(), self._getPt())
class ThemeTab(Tab, framework.TabFriendlyCompoundElement): def __init__(self, app, onClose=None, onRestart=None): super(ThemeTab, self).__init__(app, 'Themes') self.onClose = Event() if onClose is not None: self.onClose.addListener(onClose) self.onRestart = Event() if onRestart is not None: self.onRestart.addListener(onRestart) font = self.app.screenManager.fonts.menuFont colours = self.app.theme.colours self.inactiveTheme = False self.originalTheme = app.theme.name # Static text self.staticText = [ TextElement(self.app, 'theme information:', font, ScaledLocation(960, 250, 'topright'), colours.headingColour), TextElement(self.app, 'theme contents:', font, ScaledLocation(960, 390, 'topright'), colours.headingColour) ] # Dynamic text self.feedbackText = TextElement( self.app, 'Your current theme: %s' % (app.theme.name, ), font, ScaledLocation(512, 200, 'midtop'), colours.startButton) self.listHeaderText = TextElement(self.app, 'available themes:', font, ScaledLocation(70, 250), colours.headingColour) self.themeNameText = TextElement(self.app, 'Default Theme', font, ScaledLocation(960, 290, 'topright'), colours.startButton) self.themeAuthorText = TextElement( self.app, 'created by: Trosnoth Team', font, ScaledLocation(960, 330, 'topright'), colours.startButton) self.contents = [] numContents = 4 for yPos in range(430, 430 + numContents * 40 + 1, 40): self.contents.append( TextElement(self.app, '', font, ScaledLocation(960, yPos, 'topright'), colours.startButton)) self.dynamicText = [ self.feedbackText, self.listHeaderText, self.themeNameText, self.themeAuthorText ] + self.contents # Theme list self.themeList = ListBox(self.app, ScaledArea(70, 290, 400, 290), [], font, colours.listboxButtons) self.themeList.onValueChanged.addListener(self.updateSidebar) # Text buttons self.useThemeButton = button(app, 'use selected theme', self.applyTheme, (0, -125), 'midbottom') self.refreshButton = button(app, 'refresh', self.populateList, (-100, -75), 'midbottom') self.cancelButton = button(app, 'cancel', self.backToMain, (100, -75), 'midbottom') self.restartButton = button(app, 'restart Trosnoth', self.restart, (0, -125), 'midbottom') self.buttons = [ self.useThemeButton, self.refreshButton, self.cancelButton, self.restartButton ] # Combine the elements self.elements = self.staticText + self.dynamicText + self.buttons + [ self.themeList ] self.contentTypes = { "sprites": "sprite", "blocks": "map block", "fonts": "font", "startupMenu": "backdrop" } # Populate the list of replays self.populateList() def populateList(self): defaultTheme = { "name": "Default Theme", "filename": "default", "author": "Trosnoth Team", "content": None, "source": "internal" } # Clear out the sidebar self.themeNameText.setText('') self.themeAuthorText.setText('') for element in self.contents: element.setText('') self.useThemeButton.setText('') self.restartButton.setText('') self.listHeaderText.setText('available themes:') self.themeList.index = -1 userThemeDir = getPath(user, 'themes') internalThemeDir = getPath(themes) makeDirs(userThemeDir) themeList = [] # Get a list of internal themes for dirName in os.listdir(internalThemeDir): if os.path.isdir(os.path.join(internalThemeDir, dirName)): themeList.append("i/%s" % dirName) # Get a list of user-defined themes for dirName in os.listdir(userThemeDir): if os.path.isdir(os.path.join(userThemeDir, dirName)): # Internal themes overrule user-defined themes if "i/" + dirName not in themeList and dirName != "default": themeList.append("u/%s" % dirName) # Assume all themes are valid for now validThemes = themeList[:] self.themeInfo = {} for themeName in themeList: themeInfo = {"content": {}} if themeName.startswith("i/"): themeInfo['source'] = 'internal' directory = internalThemeDir else: themeInfo['source'] = 'user-defined' directory = userThemeDir themeNameList = themeName themeName = themeName[2:] themeInfo['filename'] = themeName[2:] anyContent = False for contentType in list(self.contentTypes.keys()): if themeInfo['source'] == 'internal': contentDir = os.path.join(directory, themeName, contentType) else: contentDir = os.path.join(directory, themeName, contentType) if not os.path.isdir(contentDir): continue else: fileCount = len([ f for f in os.listdir(contentDir) if os.path.isfile(os.path.join(contentDir, f)) ]) if fileCount > 0: anyContent = True themeInfo["content"][contentType] = fileCount if not anyContent: validThemes.remove(themeNameList) continue infoFile = os.path.join(directory, themeName, "info.txt") if os.path.isfile(infoFile): infoFile = open(infoFile) infoContents = infoFile.readlines() else: infoContents = [] if len(infoContents) >= 2: themeInfo["author"] = infoContents[1].strip() else: themeInfo["author"] = None if len(infoContents) >= 1: themeInfo["name"] = infoContents[0].strip() else: themeInfo["name"] = themeName self.themeInfo[themeName] = themeInfo self.themeInfo["default"] = defaultTheme # Sort the themes alphabetically items = [(v['filename'], n) for n, v in self.themeInfo.items()] items.sort() items = [n for v, n in items] self.themeList.setItems(items) if len(self.themeInfo) == 1: self.listHeaderText.setText("1 available theme:") self.themeList.index = 0 self.updateSidebar(0) else: self.listHeaderText.setText("%d available themes:" % len(self.themeInfo)) def updateSidebar(self, listID): # Update the details on the sidebar displayName = self.themeList.getItem(listID) themeInfo = self.themeInfo[displayName] if themeInfo['source'] == "internal": self.themeNameText.setText(themeInfo['name'] + " (built-in)") else: self.themeNameText.setText(themeInfo['name']) if themeInfo['author'] is not None: self.themeAuthorText.setText("by " + themeInfo['author']) else: self.themeAuthorText.setText("(creator unknown)") if themeInfo['content'] is None: for element in self.contents: element.setText('') self.contents[0].setText('N/A') else: count = 0 for k, v in themeInfo['content'].items(): suffix = "" if v != 1: suffix = "s" text = "%d %s%s" % (v, self.contentTypes[k], suffix) self.contents[count].setText(text) count += 1 self.restartButton.setText("") if self.app.displaySettings.theme != displayName: if displayName == self.originalTheme: self.useThemeButton.setText("revert back to this theme") else: self.useThemeButton.setText("use this theme") else: self.useThemeButton.setText("") if self.inactiveTheme: self.restartButton.setText('restart Trosnoth') def applyTheme(self): themeName = self.themeList.getItem(self.themeList.index) self.app.displaySettings.theme = themeName self.app.displaySettings.save() self.useThemeButton.setText("") if themeName != self.originalTheme: self.feedbackText.setText("You must restart Trosnoth for the " "'%s' theme to take effect." % themeName) self.restartButton.setText('restart Trosnoth') self.inactiveTheme = True else: self.feedbackText.setText('Your current theme: %s' % (self.app.theme.name, )) self.restartButton.setText("") self.inactiveTheme = False def restart(self): self.onRestart.execute() def backToMain(self): if self.inactiveTheme: self.feedbackText.setText("Your current theme: %s " "(restart required)" % self.app.displaySettings.theme) self.onClose.execute() def draw(self, surface): super(ThemeTab, self).draw(surface) width = max(int(3 * self.app.screenManager.scaleFactor), 1) scalePoint = self.app.screenManager.placePoint pygame.draw.line(surface, self.app.theme.colours.mainMenuColour, scalePoint((512, 260)), scalePoint((512, 580)), width)
class SoundSettingsTab(Tab, framework.CompoundElement): def __init__(self, app, onClose=None): super(SoundSettingsTab, self).__init__(app, 'Sounds') self.onClose = Event() if onClose is not None: self.onClose.addListener(onClose) font = self.app.screenManager.fonts.bigMenuFont colours = app.theme.colours text = [ TextElement(self.app, 'Music Volume', font, ScaledLocation(400, 285, 'topright'), colours.headingColour), TextElement(self.app, 'Enable Music', font, ScaledLocation(400, 355, 'topright'), colours.headingColour), TextElement(self.app, 'Sound Volume', font, ScaledLocation(400, 425, 'topright'), colours.headingColour), TextElement(self.app, 'Enable Sound', font, ScaledLocation(400, 495, 'topright'), colours.headingColour), ] initVolume = app.soundSettings.musicVolume musicVolumeLabel = TextElement(self.app, '%d' % (initVolume, ), font, ScaledLocation(870, 280, 'topleft'), colours.headingColour) self.musicVolumeSlider = Slider(self.app, ScaledArea(450, 280, 400, 40)) onSlide = lambda volume: musicVolumeLabel.setText('%d' % volume) self.musicVolumeSlider.onSlide.addListener(onSlide) self.musicVolumeSlider.onValueChanged.addListener(onSlide) self.musicVolumeSlider.setVal(initVolume) self.musicBox = CheckBox(self.app, ScaledLocation(450, 360), text='', font=font, colour=(192, 192, 192), initValue=app.soundSettings.musicEnabled) initSndVolume = app.soundSettings.soundVolume soundVolumeLabel = TextElement(self.app, '%d' % (initSndVolume, ), font, ScaledLocation(870, 420, 'topleft'), colours.headingColour) self.soundVolumeSlider = Slider(self.app, ScaledArea(450, 420, 400, 40)) onSlide = lambda volume: soundVolumeLabel.setText('%d' % volume) self.soundVolumeSlider.onSlide.addListener(onSlide) self.soundVolumeSlider.onValueChanged.addListener(onSlide) self.soundVolumeSlider.setVal(initSndVolume) self.soundBox = CheckBox(self.app, ScaledLocation(450, 500), text='', font=font, colour=(192, 192, 192), initValue=app.soundSettings.soundEnabled) self.buttons = [ button(app, 'save', self.saveSettings, (-100, -75), 'midbottom', secondColour=app.theme.colours.white), button(app, 'cancel', self.onClose.execute, (100, -75), 'midbottom', secondColour=app.theme.colours.white) ] self.elements = text + [ musicVolumeLabel, self.musicVolumeSlider, self.musicBox, soundVolumeLabel, self.soundVolumeSlider, self.soundBox ] + self.buttons def saveSettings(self, sender=None): playMusic, volume, playSound, sndVolume = self.getValues() ss = self.app.soundSettings ss.musicEnabled = playMusic ss.musicVolume = volume ss.soundEnabled = playSound ss.soundVolume = sndVolume ss.save() ss.apply() self.onClose.execute() def getValues(self): playMusic = self.musicBox.value volume = self.musicVolumeSlider.getVal() playSound = self.soundBox.value sndVolume = self.soundVolumeSlider.getVal() return [playMusic, volume, playSound, sndVolume]
class KeymapTab(Tab): def __init__(self, app, onClose=None): super(KeymapTab, self).__init__(app, 'Controls') self.font = app.screenManager.fonts.bigMenuFont self.onClose = Event() if onClose is not None: self.onClose.addListener(onClose) # Break things up into categories movement = ['jump', 'down', 'left', 'right'] menus = ['menu', 'more actions'] actions = [ 'respawn', 'select upgrade', 'activate upgrade', 'change nickname', 'ready' ] misc = ['chat', 'follow'] upgrades = [ upgradeClass.action for upgradeClass in sorted( allUpgrades, key=lambda upgradeClass: upgradeClass.order) ] upgrades.append('no upgrade') display = ['leaderboard', 'toggle interface', 'toggle terminal'] actionNames = { 'select upgrade': 'Select upgrade', 'activate upgrade': 'Activate upgrade', 'change nickname': 'Change nickname', 'chat': 'Chat', 'down': 'Drop down', 'follow': 'Auto pan (replay)', 'jump': 'Jump', 'leaderboard': 'Show leaderboard', 'left': 'Move left', 'menu': 'Main menu', 'more actions': 'Advanced', 'no upgrade': 'Deselect upgrade', 'ready': 'Toggle ready', 'respawn': 'Respawn', 'right': 'Move right', 'status bar': 'Status bar', 'timer': 'Show timer', 'toggle interface': 'Toggle HUD', 'toggle terminal': 'Toggle terminal', 'zone progress': 'Show zone bar', } actionNames.update((upgradeClass.action, upgradeClass.name) for upgradeClass in allUpgrades) # Organise the categories by column self.layout = [ [movement, menus], [actions, display], [upgrades, misc], ] self.errorInfo = TextElement(self.app, '', self.font, ScaledLocation(512, 580, 'center')) self.text = [self.errorInfo] self.inputLookup = {} xPos = 190 # Lay everything out automatically. keymapFont = self.app.screenManager.fonts.keymapFont keymapInputFont = self.app.screenManager.fonts.keymapInputFont for column in self.layout: # Each column yPos = 200 for category in column: # Each category for action in category: # Each action # Draw action name (eg. Respawn) self.text.append( TextElement(self.app, actionNames[action], keymapFont, ScaledLocation(xPos, yPos + 6, 'topright'), self.app.theme.colours.headingColour)) # Create input box box = prompt.KeycodeBox(self.app, ScaledArea(xPos + 10, yPos, 100, 30), font=keymapInputFont) box.onClick.addListener(self.setFocus) box.onChange.addListener(self.inputChanged) box.__action = action self.inputLookup[action] = box yPos += 35 # Between items yPos += 35 # Between categories xPos += 320 # Between columns self.elements = self.text + self.inputLookup.values() + [ button(app, 'restore default controls', self.restoreDefaults, (0, -125), 'midbottom', secondColour=app.theme.colours.white), button(app, 'save', self.saveSettings, (-100, -75), 'midbottom', secondColour=app.theme.colours.white), button(app, 'cancel', self.cancel, (100, -75), 'midbottom', secondColour=app.theme.colours.white), ] self.populateInputs() def inputChanged(self, box): # Remove the old key. try: oldKey = self.keyMapping.getkey(box.__action) except KeyError: pass else: del self.keyMapping[oldKey] # Set the new key. self.keyMapping[box.value] = box.__action # Refresh the display. self.refreshInputs() def populateInputs(self): # Set up the keyboard mapping. self.keyMapping = keyboard.KeyboardMapping(keymap.default_game_keys) try: # Try to load keyboard mappings from the user's personal settings. config = open(getPath(user, 'keymap'), 'rU').read() self.keyMapping.load(config) except IOError: pass # Refresh the display. self.refreshInputs() def refreshInputs(self): for column in self.layout: for category in column: for action in category: # Get the current key and put it in the box. try: key = self.keyMapping.getkey(action) except KeyError: key = None self.inputLookup[action].value = key # Make the box white self.inputLookup[action].backColour = (255, 255, 255) def restoreDefaults(self): self.keyMapping = keyboard.KeyboardMapping(keymap.default_game_keys) self.refreshInputs() self.incorrectInput( "Default controls restored: press 'save' to " "confirm", (0, 128, 0)) def clearBackgrounds(self): for action in self.inputLookup: self.inputLookup[action].backColour = (255, 255, 255) self.setFocus(None) def saveSettings(self): # Perform the save. open(getPath(user, 'keymap'), 'w').write(self.keyMapping.save()) emptyBoxes = [] for box in self.inputLookup.itervalues(): if box.value is None: emptyBoxes.append(box) if len(emptyBoxes) > 0: self.populateInputs() for box in emptyBoxes: box.backColour = self.app.theme.colours.invalidDataColour self.incorrectInput('Warning: some actions have no key', (192, 0, 0)) else: self.mainMenu() def incorrectInput(self, string, colour): self.errorInfo.setColour(colour) self.errorInfo.setText(string) self.errorInfo.setFont(self.font) def cancel(self): self.populateInputs() self.mainMenu() def mainMenu(self): self.incorrectInput('', (0, 0, 0)) self.clearBackgrounds() self.onClose.execute()
class AIAgent(ConcreteAgent): ''' Base class for an AI agent. ''' def __init__(self, game, aiClass, fromLevel, nick=None, *args, **kwargs): super(AIAgent, self).__init__(game=game, *args, **kwargs) self.aiClass = aiClass self.fromLevel = fromLevel self._initialisationNick = nick self.ai = None self.team = None self.requestedNick = None self._onBotSet = Event() self._loop = WeakLoopingCall(self, '_tick') def start(self, team=None): self.team = team self._loop.start(2) def stop(self): super(AIAgent, self).stop() self._loop.stop() if self.ai: self.ai.disable() self.ai = None def _tick(self): if self.ai is not None: return if self.fromLevel and self._initialisationNick: nick = self._initialisationNick else: nick = self.aiClass.nick if self.team is None: teamId = NEUTRAL_TEAM_ID else: teamId = self.team.id self._joinGame(nick, teamId) def _joinGame(self, nick, teamId): self.requestedNick = nick self.sendJoinRequest(teamId, nick, bot=True, fromLevel=self.fromLevel) @CannotJoinMsg.handler def _joinFailed(self, msg): r = msg.reasonId nick = self.requestedNick if r == GAME_FULL_REASON: message = 'full' elif r == UNAUTHORISED_REASON: message = 'not authenticated' elif r == NICK_USED_REASON: message = 'nick in use' elif r == USER_IN_GAME_REASON: message = 'user already in game' # Should never happen elif r == ALREADY_JOINED_REASON: message = 'tried to join twice' # Should never happen else: message = repr(r) log.error('Join failed for AI %r (%s)', nick, message) self.stop() def setPlayer(self, player): if player is None and self.ai: self.ai.disable() self.ai = None super(AIAgent, self).setPlayer(player) if player: self.requestedNick = None self.ai = self.aiClass(self.world, self.localState.player, self) self._onBotSet(self.ai) self._onBotSet.clear() def getBot(self): ''' @return: a Deferred which fires with this agent's Bot object, as soon as it has one. ''' d = defer.Deferred() if self.ai is not None: d.callback(self.ai) else: self._onBotSet.addListener(d.callback, weak=False) return d @TickMsg.handler def handle_TickMsg(self, msg): super(AIAgent, self).handle_TickMsg(msg) if self.ai: self.ai.consumeMsg(msg) @ResyncPlayerMsg.handler def handle_ResyncPlayerMsg(self, msg): super(AIAgent, self).handle_ResyncPlayerMsg(msg) if self.ai: self.ai.playerResynced() def defaultHandler(self, msg): super(AIAgent, self).defaultHandler(msg) if self.ai: self.ai.consumeMsg(msg)
class DisplaySettingsTab(Tab, framework.TabFriendlyCompoundElement): def __init__(self, app, onClose=None): super(DisplaySettingsTab, self).__init__(app, 'Display') self.onClose = Event() if onClose is not None: self.onClose.addListener(onClose) font = self.app.screenManager.fonts.bigMenuFont smallNoteFont = self.app.screenManager.fonts.smallNoteFont colour = self.app.theme.colours.headingColour def mkText(text, x, y, textFont=font, anchor='topright'): return TextElement(self.app, text, textFont, ScaledLocation(x, y, anchor), colour) self.text = [ mkText('X', 640, 280), mkText('Screen resolution', 430, 280), mkText('Fullscreen mode', 430, 360), mkText('Graphics detail', 430, 440), mkText('low', 460, 475, textFont=smallNoteFont, anchor='midtop'), mkText('high', 845, 475, textFont=smallNoteFont, anchor='midtop'), mkText('Show timings', 430, 525), ] self.invalidInputText = TextElement(self.app, '', font, ScaledLocation(512, 230, 'midtop'), (192, 0, 0)) self.widthInput = prompt.InputBox(self.app, ScaledArea(460, 265, 150, 60), initValue=str( self.app.screenManager.size[0]), font=font, maxLength=4, validator=prompt.intValidator) self.widthInput.onEnter.addListener(lambda sender: self.saveSettings()) self.widthInput.onClick.addListener(self.setFocus) self.widthInput.onTab.addListener(self.tabNext) self.heightInput = prompt.InputBox(self.app, ScaledArea(652, 265, 150, 60), initValue=str( self.app.screenManager.size[1]), font=font, maxLength=4, validator=prompt.intValidator) self.heightInput.onEnter.addListener( lambda sender: self.saveSettings()) self.heightInput.onClick.addListener(self.setFocus) self.heightInput.onTab.addListener(self.tabNext) self.tabOrder = [self.widthInput, self.heightInput] self.fullscreenBox = CheckBox( self.app, ScaledLocation(460, 365), text='', font=font, colour=(192, 192, 192), initValue=self.app.screenManager.isFullScreen(), ) self.fullscreenBox.onValueChanged.addListener(self.fullscreenChanged) displaySettings = app.displaySettings self.detailSlider = Slider( self.app, ScaledArea(460, 430, 390, 40), bounds=(0, len(displaySettings.DETAIL_LEVELS) - 1), snap=True) self.detailSlider.setVal( displaySettings.DETAIL_LEVELS.index(displaySettings.detailLevel)) self.detailSlider.onValueChanged.addListener(self.detailChanged) self.showTimingsBox = CheckBox( self.app, ScaledLocation(460, 530), text='', font=font, colour=(192, 192, 192), initValue=displaySettings.showTimings, ) self.input = [ self.widthInput, self.heightInput, self.widthInput, self.fullscreenBox, self.detailSlider, self.showTimingsBox ] self.elements = self.text + self.input + [ self.invalidInputText, button(app, 'save', self.saveSettings, (-100, -75), 'midbottom', secondColour=app.theme.colours.white), button(app, 'cancel', self.cancelMenu, (100, -75), 'midbottom', secondColour=app.theme.colours.white), ] self.setFocus(self.widthInput) def detailChanged(self, newLevel): pass def cancelMenu(self): self.fullscreenBox.setValue(self.app.screenManager.isFullScreen()) self.showTimingsBox.setValue(self.app.displaySettings.showTimings) self.heightInput.setValue(str(self.app.screenManager.size[1])) self.widthInput.setValue(str(self.app.screenManager.size[0])) self.onClose.execute() def saveSettings(self): displaySettings = self.app.displaySettings height = self.getInt(self.heightInput.value) width = self.getInt(self.widthInput.value) fullScreen = self.fullscreenBox.value detailLevel = displaySettings.DETAIL_LEVELS[self.detailSlider.getVal()] showTimings = self.showTimingsBox.value # The resolutionList is used when fullScreen is true. resolutionList = pygame.display.list_modes() resolutionList.sort() minResolution = resolutionList[0] maxResolution = resolutionList[-1] if not fullScreen: minResolution = (320, 240) # These values are used when fullScreen is false. widthRange = (minResolution[0], maxResolution[0]) heightRange = (minResolution[1], maxResolution[1]) if not widthRange[0] <= width <= widthRange[1]: self.incorrectInput('Screen width must be between %d and %d' % (widthRange[0], widthRange[1])) width = None return if not heightRange[0] <= height <= heightRange[1]: self.incorrectInput('Screen height must be between %d and %d' % (heightRange[0], heightRange[1])) height = None return if fullScreen: selectedResolution = (width, height) if selectedResolution not in resolutionList: self.incorrectInput('Selected resolution is not valid for ' 'this display') height = width = None return self.incorrectInput('') # Save these values. displaySettings.fullScreen = fullScreen displaySettings.detailLevel = detailLevel displaySettings.showTimings = showTimings if fullScreen: displaySettings.fsSize = (width, height) else: displaySettings.size = (width, height) # Write to file and apply. displaySettings.save() displaySettings.apply() self.onClose.execute() def getInt(self, value): if value == '': return 0 return int(value) def incorrectInput(self, string): self.invalidInputText.setText(string) self.invalidInputText.setFont(self.app.screenManager.fonts.bigMenuFont) def fullscreenChanged(self, element): # If the resolution boxes haven't been touched, swap their values to # the appropriate resolution for the new mode. height = self.getInt(self.heightInput.value) width = self.getInt(self.widthInput.value) fullScreen = self.fullscreenBox.value if fullScreen: # Going to full screen mode. if (width, height) != self.app.displaySettings.size: return width, height = self.app.displaySettings.fsSize else: # Going from full screen mode. if (width, height) != self.app.displaySettings.fsSize: return width, height = self.app.displaySettings.size self.heightInput.setValue(str(height)) self.widthInput.setValue(str(width))
class CreditsScreen(framework.CompoundElement): def __init__(self, app, colour, onCancel=None, speed=None, loop=True, startOff=False, backText='back to main menu', highlight=(255, 255, 255)): super(CreditsScreen, self).__init__(app) self.colour = colour self.highlight = highlight self.onCancel = Event() if onCancel is not None: self.onCancel.addListener(onCancel) f = open(getPath(startupMenu, 'credits.txt'), 'rU', encoding='utf-8') area = ScaledArea(50, 130, 900, 540) fonts = { 'body': self.app.fonts.creditsFont, 'h1': self.app.fonts.creditsH1, 'h2': self.app.fonts.creditsH2 } text = f.read() if not startOff: # Make at least some room on top and bottom text = '\n' * 3 + text + '\n' * 2 self.credits = scrollingText.ScrollingText(self.app, area, text, self.colour, fonts, textAlign='middle', loop=loop, startOff=startOff) self.credits.setShadowColour((192, 192, 192)) self.credits.setAutoscroll(True) if speed is None: speed = 80 self.credits.setSpeed(ScalingSpeed(speed)) self.credits.setBorder(False) self.backText = backText cancelButton = self.button(backText, self.onCancel.execute, (-50, -30), 'bottomright') self.elements = [self.credits, cancelButton] @property def finished(self): return self.credits.reachedEnd def setColour(self, colour): self.colour = colour self.elements[-1] = self.button(self.backText, self.onCancel.execute, (-50, -30), 'bottomright') self.credits.setColour(colour) def button(self, text, onClick, pos, anchor='topleft', hugeFont=False): pos = Location( ScaledScreenAttachedPoint(ScaledSize(pos[0], pos[1]), anchor), anchor) if hugeFont: font = self.app.screenManager.fonts.hugeMenuFont else: font = self.app.screenManager.fonts.bigMenuFont result = elements.TextButton(self.app, pos, text, font, self.colour, self.highlight) result.onClick.addListener(lambda sender: onClick()) return result def restart(self): self.credits.returnToTop()
class InputBox(framework.Element): pxFromLeft = 2 pxFromRight = 5 pxToSkipWhenCursorGoesToTheRight = 90 cursorFlashTime = 0.6 def __init__(self, app, area, initValue='', validator=None, font=None, colour=(255, 255, 255), maxLength=None, onClick=None, onEnter=None, onTab=None, onEsc=None, onEdit=None): super(InputBox, self).__init__(app) self.area = area self.onClick = Event() if onClick is not None: self.onClick.addListener(onClick) self.onEnter = Event() if onEnter is not None: self.onEnter.addListener(onEnter) self.onTab = Event() if onTab is not None: self.onTab.addListener(onTab) self.onEsc = Event() if onEsc is not None: self.onEsc.addListener(onEsc) self.onEdit = Event() if onEdit is not None: self.onEdit.addListener(onEdit) if validator: self.validator = validator else: self.validator = lambda x: True if font: self.font = font else: self.font = self.app.screenManager.fonts.defaultTextBoxFont self.value = initValue self.maxLength = maxLength self.fontColour = (0, 0, 0) self.backColour = colour self.cursorColour = (255, 0, 0) self._cursorVisible = True self._timeToFlash = self.cursorFlashTime # Calculate the white part of the input area self.cursorIndex = len(self.value) self.offset = 0 def clear(self): self.setValue('') def setValue(self, value): self.value = value self.offset = 0 self.cursorIndex = len(self.value) def getValue(self): return self.value def setFont(self, font): self.font = font def setValidator(self, validator): self.validator = validator def setMaxLength(self, length): self.maxLength = length def _getSize(self): return self._getRect().size def _getCursorImage(self): s = pygame.Surface((2, min(int(self.font.getHeight(self.app)), self._getSize()[1] * 0.8))) s.fill(self.cursorColour) return s def _getRect(self): return self.area.getRect(self.app) def _getPt(self): return self._getRect().topleft def setFontColour(self, fontColour): self.fontColour = fontColour def setBackColour(self, backColour): self.backColour = backColour def setCursorColour(self, cursorColour): self.cursorColour = cursorColour def gotFocus(self): self._cursorVisible = True self._timeToFlash = self.cursorFlashTime def _renderText(self): ''' Provided so that PasswordBox can override it. ''' return self.font.render(self.app, self.value, True, self.fontColour, self.backColour) def draw(self, surface): rect = self._getRect() size = rect.size pos = rect.topleft # Fill the input area with the specified colour surface.fill(self.backColour, rect) # Put what's currently inputted into the input area inputText = self._renderText() # Adjust offset cursorPos = self._getCursorPos() if cursorPos < self.offset: self.offset = max( 0, self.offset - self.pxToSkipWhenCursorGoesToTheRight) else: minOffset = cursorPos - size[0] + self.pxFromRight + self.pxFromLeft if self.offset < minOffset: self.offset = minOffset text_rect = inputText.get_rect() text_rect.centery = rect.centery diff = (text_rect.height - rect.height) / 2 # Find the currently-displaying text (based on where the cursor is): area = pygame.Rect(self.offset, diff, size[0] - self.pxFromRight, rect.height) # Put the text on the screen surface.blit(inputText, (pos[0] + self.pxFromLeft, rect.top), area) # Add the cursor where it is. if self.hasFocus and self._cursorVisible: cs = self._getCursorImage() cursor_rect = cs.get_rect() cursor_rect.centery = rect.centery surface.blit( cs, (pos[0] + self._getCursorPos() - self.offset, cursor_rect.top)) def _getCursorPos(self): return self._getFontSize(self.value[:self.cursorIndex])[0] + 1 def _getFontSize(self, text): ''' Provided so that PasswordBox can override it. ''' return self.font.size(self.app, text) # Get the index of the position clicked def __getCursorIndex(self, clickOffset): totalOffset = clickOffset + self.offset + self.pxFromLeft i = 1 fontOffset = 0 last = 0 while fontOffset < totalOffset and i <= len(self.value): last = fontOffset fontOffset = self._getFontSize(self.value[:i])[0] i += 1 if (fontOffset - totalOffset) <= (totalOffset - last): return i - 1 else: return i - 2 return i def processEvent(self, event): '''Processes the key press. If we use the event, return nothing. If we do not use it, return the event so something else can use it.''' if self.hasFocus and event.type == pygame.KEYDOWN: self._cursorVisible = True self._timeToFlash = self.cursorFlashTime if event.key in (pygame.K_RETURN, pygame.K_KP_ENTER): self.onEnter.execute(self) return None if event.key == pygame.K_ESCAPE: self.onEsc.execute(self) return None if event.key == pygame.K_TAB: self.onTab.execute(self) return None # Delete the letter behind the cursor if event.key == pygame.K_BACKSPACE: # Make sure there's something in the string behind the cursor # first, don't want to kill what's not there if self.cursorIndex > 0: self.value = (self.value[:self.cursorIndex - 1] + self.value[self.cursorIndex:]) self.cursorIndex -= 1 self.onEdit.execute(self) elif event.key == pygame.K_DELETE: # Make sure there's something in the string in front of the # cursor first, don't want to kill what's not there if self.cursorIndex < len(self.value): self.value = (self.value[:self.cursorIndex] + self.value[self.cursorIndex + 1:]) self.onEdit.execute(self) elif event.key == pygame.K_LEFT: if self.cursorIndex > 0: self.cursorIndex -= 1 elif event.key == pygame.K_RIGHT: if self.cursorIndex < len(self.value): self.cursorIndex += 1 elif event.key == pygame.K_END: self.cursorIndex = len(self.value) elif event.key == pygame.K_HOME: self.cursorIndex = 0 self.offset = 0 # Add the character to our string elif event.unicode == '': return event else: # Validate the input. if not self.validator(event.unicode): return event # Check the maxLength if self.maxLength is not None: if len(self.value) >= self.maxLength: return event # Add the typed letter to the string self.value = (self.value[:self.cursorIndex] + event.unicode + self.value[self.cursorIndex:]) self.cursorIndex += len(event.unicode) self.onEdit.execute(self) else: rect = self._getRect() if (event.type == pygame.MOUSEBUTTONDOWN and rect.collidepoint(event.pos) and event.button == 1): self.onClick.execute(self) xOffset = event.pos[0] - rect.left self.cursorIndex = self.__getCursorIndex(xOffset) self._timeToFlash = self.cursorFlashTime self._cursorVisible = True else: # It's not a keydown event. Pass it on. return event def tick(self, deltaT): self._timeToFlash -= deltaT while self._timeToFlash < 0: self._timeToFlash += self.cursorFlashTime self._cursorVisible = not self._cursorVisible
class KeymapTab(Tab): def __init__(self, app, onClose=None): super(KeymapTab, self).__init__(app, 'Controls') self.font = app.screenManager.fonts.bigMenuFont self.onClose = Event() if onClose is not None: self.onClose.addListener(onClose) # Break things up into categories movement = [ ACTION_JUMP, ACTION_DOWN, ACTION_LEFT, ACTION_RIGHT, ACTION_HOOK ] menus = [ACTION_MAIN_MENU, ACTION_MORE_MENU] actions = [ ACTION_UPGRADE_MENU, ACTION_USE_UPGRADE, ACTION_EDIT_PLAYER_INFO, ACTION_READY, ACTION_SHOW_TRAJECTORY, ACTION_EMOTE ] misc = [ACTION_CHAT, ACTION_FOLLOW] upgrades = [ upgradeClass.action for upgradeClass in sorted( allUpgrades, key=lambda upgradeClass: upgradeClass.order) ] upgrades.append(ACTION_CLEAR_UPGRADE) display = [ ACTION_LEADERBOARD_TOGGLE, ACTION_HUD_TOGGLE, ACTION_TERMINAL_TOGGLE ] actionNames = { ACTION_EDIT_PLAYER_INFO: 'Change nick / hat', ACTION_CHAT: 'Chat', ACTION_CLEAR_UPGRADE: 'Deselect upgrade', ACTION_DOWN: 'Drop down', ACTION_FOLLOW: 'Auto pan (replay)', ACTION_HOOK: 'Grappling hook', ACTION_HUD_TOGGLE: 'Toggle HUD', ACTION_JUMP: 'Jump', ACTION_LEADERBOARD_TOGGLE: 'Show leaderboard', ACTION_LEFT: 'Move left', ACTION_MAIN_MENU: 'Main menu', ACTION_MORE_MENU: 'Advanced', ACTION_READY: 'Toggle ready', ACTION_RIGHT: 'Move right', ACTION_TERMINAL_TOGGLE: 'Toggle terminal', ACTION_UPGRADE_MENU: 'Select upgrade', ACTION_USE_UPGRADE: 'Activate upgrade', ACTION_SHOW_TRAJECTORY: 'Show trajectory', ACTION_EMOTE: 'Emote', } actionNames.update((upgradeClass.action, upgradeClass.name) for upgradeClass in allUpgrades) # Organise the categories by column self.layout = [ [movement, menus], [actions, display], [upgrades, misc], ] self.errorInfo = TextElement(self.app, '', self.font, ScaledLocation(512, 580, 'center')) self.text = [self.errorInfo] self.inputLookup = {} xPos = 210 # Lay everything out automatically. keymapFont = self.app.screenManager.fonts.keymapFont keymapInputFont = self.app.screenManager.fonts.keymapInputFont for column in self.layout: # Each column yPos = 200 for category in column: # Each category for action in category: # Each action # Draw action name (eg. Respawn) self.text.append( TextElement(self.app, actionNames[action], keymapFont, ScaledLocation(xPos, yPos + 6, 'topright'), self.app.theme.colours.headingColour)) # Create input box box = prompt.KeycodeBox(self.app, ScaledArea(xPos + 10, yPos, 100, 30), font=keymapInputFont, acceptMouse=True) box.onClick.addListener(self.setFocus) box.onChange.addListener(self.inputChanged) box.__action = action self.inputLookup[action] = box yPos += 35 # Between items yPos += 35 # Between categories xPos += 310 # Between columns self.elements = self.text + list(self.inputLookup.values()) + [ button(app, 'restore default controls', self.restoreDefaults, (0, -125), 'midbottom', secondColour=app.theme.colours.white), button(app, 'save', self.saveSettings, (-100, -75), 'midbottom', secondColour=app.theme.colours.white), button(app, 'cancel', self.cancel, (100, -75), 'midbottom', secondColour=app.theme.colours.white), ] self.populateInputs() def inputChanged(self, box): # Remove the old key. try: oldKey = self.keyMapping.getkey(box.__action) except KeyError: pass else: del self.keyMapping[oldKey] # Set the new key. self.keyMapping[box.value] = box.__action # Refresh the display. self.refreshInputs() def populateInputs(self): # Set up the keyboard mapping. self.keyMapping = keyboard.KeyboardMapping(keymap.default_game_keys) try: # Try to load keyboard mappings from the user's personal settings. with open(getPath(user, 'keymap'), 'r') as f: config = f.read() self.keyMapping.load(config) except IOError: pass # Refresh the display. self.refreshInputs() def refreshInputs(self): for column in self.layout: for category in column: for action in category: # Get the current key and put it in the box. try: key = self.keyMapping.getkey(action) except KeyError: key = None self.inputLookup[action].value = key # Make the box white self.inputLookup[action].backColour = (255, 255, 255) def restoreDefaults(self): self.keyMapping = keyboard.KeyboardMapping(keymap.default_game_keys) self.refreshInputs() self.showMessage( "Default controls restored: press 'save' to " "confirm", (0, 128, 0)) def clearBackgrounds(self): for action in self.inputLookup: self.inputLookup[action].backColour = (255, 255, 255) self.setFocus(None) def saveSettings(self): # Perform the save. open(getPath(user, 'keymap'), 'w').write(self.keyMapping.save()) self.mainMenu() def showMessage(self, string, colour): self.errorInfo.setColour(colour) self.errorInfo.setText(string) self.errorInfo.setFont(self.font) def cancel(self): self.populateInputs() self.mainMenu() def mainMenu(self): self.showMessage('', (0, 0, 0)) self.clearBackgrounds() self.onClose.execute()