# -*-Python-*- # Copyright (c) 2002 Sean R. Lynch # # This file is part of PythonVerse. # # PythonVerse is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by # the Free Software Foundation; either version 2 of the License, or # (at your option) any later version. # # PythonVerse is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU General Public License for more details. # # You should have received a copy of the GNU General Public License # along with PythonVerse; if not, write to the Free Software # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA # # vim:syntax=python import sys, bisect, string, random, webbrowser import pygame, pygame.font, pygame.image, pygame.time, pygame.draw from math import * from pygame.locals import * from types import * import transutil, color def wrap_lines(lines, width, font): r = [] for text in lines: r.extend(wrap(text, width, font)) for t in range(len(r)): if t < len(r): # in case any lines have been removed if r[t] == "": del r[t] return r def wrap(text, width, font): """Wrap a line of text, returning a list of lines.""" lines = [] while text: if font.size(text)[0] <= width: return lines + [text] try: i = string.rindex(text, ' ') while font.size(text[:i])[0] > width: i = string.rindex(text, ' ', 0, i) except ValueError: i = len(text)-1 while font.size(text[:i])[0] > width and i: i = i - 1 if not i: raise ValueError, 'width %d too narrow' % width lines.append(text[:i]) text = string.lstrip(text[i:]) return lines def progress(fraction, size=(20, 50), fgcolor=(0,255,0), bgcolor=(255,255,255)): w, h = size y = h * fraction s = pygame.Surface(size) s.fill(bgcolor, (0, 0, w, h-y)) s.fill(fgcolor, (0, h-y, w, y)) pygame.draw.rect(s, fgcolor, (0, 0, w-1, h-1), 1) s.set_alpha(127) return s def invertrect(initrect, rects, min_width=50, min_height=10): left, top = initrect.topleft right, bottom = initrect.bottomright width, height = initrect.size irects = [] rects = rects + [Rect(left-1, top, 0, height), Rect(left, top-1, width, 0), Rect(right, top, 0, height), Rect(left, bottom+1, width, 0)] for a in rects: # Pick a left side l = a.right+1 if l < left: continue # Pick a top for b in rects: # Make sure the rect can actually border ours if b.right < l or b.bottom > a.bottom: continue # Pick a top t = b.bottom+1 # Pick a right side for c in rects: # Make sure this rect can border ours if c.left <= l or c.bottom < t or c.top > bottom or \ c.left <= b.left: continue r = c.left-1 w = r - l h = bottom - t if w < min_width or h < min_height: continue # There can be only one rect with these three sides rect = Rect(l, t, w, h) # Now find the bottom for d in rects: if d.colliderect(rect): rect.height = d.top-t-1 # Make sure the rect is still sane and still borders # on the original border rects if rect.height < min_height or rect.bottom < a.top or \ rect.bottom < c.top: break else: irects.append(rect) return irects class Group: def __init__(self): self.list = [] self.pollers = [] self.active = None def add(self, sprite): self.list.append(sprite) def remove(self, sprite): self.list.remove(sprite) def above(self, sprite): """Move to the top of the stacking order""" # The way the group is implemented, just reinserting will do it. self.list.remove(sprite) self.list.append(sprite) def mouse(self, mousepos): """If a sprite is active, send events only to that sprite until it deactivates. Otherwise, send to all sprites in order until one activates.""" dirtyrects = [] # Send the event to all sprites until one activates sprites = self.list i = len(sprites) while i: i = i - 1 sprite = sprites[i] if sprite.mouseme and sprite.rect.collidepoint(mousepos): if self.active is sprite: return dirtyrects if self.active is not None: dirtyrects.extend(self.active.mouseoff()) self.active = sprite dirtyrects.extend(sprite.mouseon()) return dirtyrects if self.active is not None: dirtyrects.extend(self.active.mouseoff()) self.active = None return dirtyrects def update(self): """Update the positions of all the sprites that are moving""" # Move any avatars that need moving # FIXME: probably needs optimizing dirtyrects = [] ticks = pygame.time.get_ticks() pollers = self.list i = len(pollers) while i: i = i - 1 sprite = pollers[i] if sprite.pollme: dirtyrects.extend(sprite.poll(ticks)) return dirtyrects def draw(self, surface, rect=None): if rect is None: for sprite in self.list: surface.blit(sprite.image, sprite.rect) else: for sprite in self.list: # Redraw sprites whose rects overlap this rect, # but only in the overlapping area if rect.colliderect(sprite.rect): r = sprite.rect surface.blit(sprite.image, rect, rect.move((-r[0], -r[1]))) class Sprite: mouseme = 0 pollme = 0 def __init__(self, position, image=None): self.dead = 0 # self.group = group self.image = image if len(position) == 4: self.rect = position else: if image is None: w, h = 0, 0 else: w, h = image.get_size() x, y = position self.rect = Rect(x-w/2, y-h/2, w, h) # group.add(self) def __repr__(self): return '<%s(%s, %s)>' % (self.__class__, self.image, self.rect) def poll(self, t, delta): return [] def move(self, pos): rect = self.rect oldrect = Rect(rect) rect.center = pos if oldrect.colliderect(rect): return [oldrect.union(rect)] else: return [oldrect, rect] ## def die(self): ## self.group.remove(self) ## self.dead = 1 ## return [self.rect] def set_image(self, image): """Change the image without moving the center""" self.image = image oldrect = self.rect x, y = oldrect.center w, h = image.get_size() self.rect = Rect((x-w/2, y-h/2, w, h)) # Return the affected area return oldrect.union(self.rect) class ClampingSprite(Sprite): def __init__(self, pos, image, clamprect): Sprite.__init__(self, pos, image) self.pos = pos self.clamprect = clamprect self.clamp() def move(self, pos): self.pos = pos oldrect = Rect(self.rect) self.rect.center = pos self.clamp() if oldrect.colliderect(self.rect): return [oldrect.union(self.rect)] else: return [oldrect, self.rect] def clamp(self): self.rect = self.rect.clamp(self.clamprect) class Mouseover(Sprite): mouseme = 1 def __init__(self, server, name, pos, image1, image2): Sprite.__init__(self, pos, image1) self.server = server self.name = name self.image1 = image1 self.image2 = image2 def mouseon(self): self.server.whichmouseover(self.name) # for EXIT_OBJs return [self.set_image(self.image2)] def mouseoff(self): self.server.whichmouseover('') # for EXIT_OBJs return [self.set_image(self.image1)] def set_image1(self, image): self.image1 = image return self.set_image(image) def set_image2(self, image): self.image2 = image class Avatar(Sprite): mouseme = 1 def __init__(self, server, group, position, image, nick, noffset, boffset): Sprite.__init__(self, position, image) self.server = server self.rect = self.rect.clamp((0, 0, 640, 480)) self.group = group self.nick = nick self.destpos = None self.speed = None self.balloon = None self.label = None self.effects = [] self.effect_count = -1 self.effect_step = 0 self.pollme = 0 self.stoptime = 0 self.lastpoll = 0 self.moving = 0 self.set(noffset, boffset) def set(self, noffset, boffset): self.boffset = boffset nx, ny = noffset if nx < -self.rect.width/2-10: nx = -self.rect.width/2-10 elif nx > self.rect.width/2+10: nx = self.rect.width/2+10 if ny < -self.rect.height/2-10: ny = -self.rect.height/2-10 elif ny > self.rect.height/2+10: ny = self.rect.height/2+10 self.noffset = nx, ny def mouseon(self): # we want the ORT to behave like an exit if self.nick == 'ORT_Number_1': self.server.whichmouseover('ov_tram_exit') if self.label is not None: print self.nick, 'on' self.label.die() # Strip color codes from the nick for display nicklist = list(self.nick) nick = '' avoid = list(string.digits) avoid.extend([',', '\x03']) found = 0 for n in nicklist: if n == '\x03': found = 1 if found == 1: try: test = avoid.index(n) except: found = 0 if found == 0: nick = nick + n sx, sy = _font.size(nick) s = _font.render(nick, 0, (255, 255, 255)) image = pygame.Surface((sx+2, sy+2)) image.set_colorkey((255,165,0)) image.fill((255,165,0)) for x in range(3): for y in range(3): image.blit(s, (x, y)) image.blit(_font.render(nick, 1, (0,0,0)), (1, 1)) image.set_alpha(127) x, y = self.rect.center nx, ny = self.noffset self.label = ClampingSprite((x+nx, y+ny), image, Rect(0, 0, 640, 480)) self.group.add(self.label) return [self.label.rect] def mouseoff(self): if self.label is None: print self.nick, 'off' return [] else: label = self.label self.group.remove(label) self.label = None return [label.rect] def move(self, position, speed): """Changes the location of the avatar's center to the new position""" self.moving = 1 if position[0] >= 640 or position[1] > 480: print >> sys.stderr, 'Attempt to move outside the screen' return if speed == 0: speed = 1 self.speed = speed pos = self.rect.center self.startpos = pos self.starttime = pygame.time.get_ticks() distance = dist(pos, position) if distance == 0.0: return self.dx = 3.0 * (position[0]-pos[0]) * self.speed / distance / 10.0 self.dy = 3.0 * (position[1]-pos[1]) * self.speed / distance / 10.0 self.stoptime = self.starttime + distance * 10.0 / 3.0 / speed self.destpos = position self.pollme = 1 def effect(self, action): if action == 'jump' or action == 'shiver': self.effects.append(action) self.pollme = 1 def poll(self, t): """Move the avatar if necessary""" dirtyrects = [] oldrect = Rect(self.rect) if self.stoptime != 0: if t >= self.stoptime: self.rect.center = self.destpos self.pollme = 0 if self.balloon is not None and not self.balloon.dead \ and self.moving == 1: bx, by = self.boffset x, y = self.destpos dirtyrects.extend(self.balloon.move((x+bx, y+by))) self.moving = 0 else: delta_t = t - self.starttime x, y = self.startpos self.rect.center = x+int(round(self.dx*delta_t)), \ y+int(round(self.dy*delta_t)) # apply effects, if any if self.stoptime == 0: pos = (320, 200) else: pos = self.rect.center if self.effects != []: effect = self.effects[0] self.pollme = 1 x, y = pos if self.effect_count == -1: if effect == 'shiver': self.effect_step = 3 self.effect_count = 100 if effect == 'jump': self.effect_step = 5 self.effect_count = 200 if effect == 'shiver' and self.effect_count >= 0: if self.effect_step == 3: x = x - 15 if self.effect_step == 1: x = x + 15 if self.effect_step == 0: if self.effect_count > 0: self.effect_step = 4 if effect == 'jump' and self.effect_count > 0: if self.effect_step == 5: y = y - 15 if self.effect_step == 4: y = y - 30 if self.effect_step == 3: y = y - 45 if self.effect_step == 2: y = y - 30 if self.effect_step == 1: y = y - 15 if self.effect_step == 0: if self.effect_count > 0: self.effect_step = 6 self.rect.center = (x, y) now = pygame.time.get_ticks() if self.lastpoll == 0: self.lastpoll = pygame.time.get_ticks() delay = 30 - (now - self.lastpoll) self.lastpoll = now if delay > 0: pygame.time.delay(delay) self.effect_step = self.effect_step -1 self.effect_count = self.effect_count -1 if self.effect_count == -1: self.rect.center = pos del self.effects[0] if self.effects == []: self.pollme = 0 return dirtyrects + [oldrect.union(self.rect)] def chat(self, group, text): x, y = self.rect.center xoff, yoff = self.boffset if self.balloon is None or self.balloon.dead: self.balloon = Balloon(group, self.group, (x+xoff, y+yoff), text, self) group.add(self.balloon) return [self.balloon.rect] else: return self.balloon.add_text(text) def arc(radius, center, start_angle, stop_angle, n): x, y = center step = (stop_angle - start_angle) / n points = [0] * (n+1) for i in range(n+1): angle = start_angle + i*step points[i] = (x + int(round(radius*sin(angle))), y - int(round(radius*cos(angle)))) return tuple(points) def closest(rect, point): """Find the closest point on a rect to a given point""" return min(rect.right, max(point[0], rect.left)), \ min(rect.bottom, max(point[1], rect.top)) def dist(point1, point2): x1, y1 = point1 x2, y2 = point2 return sqrt((x1-x2)**2 + (y1-y2)**2) def rectdist(rect, point): return dist(closest(rect, point), point) class Balloon(Sprite): """A speech balloon""" pollme = 1 def __init__(self, group, avgroup, pos, text, avatar, timeout=10000, fgcolor=(0, 0, 0), bgcolor=(255, 255, 255)): self.timeouts = [pygame.time.get_ticks() + timeout] self.group = group self.avgroup = avgroup Sprite.__init__(self, pos) self.pos = pos self.timeout = timeout self.fgcolor = fgcolor self.bgcolor = bgcolor self.avatar = avatar self.text = [text] self.rect = None self.render() def move(self, pos): self.pos = pos oldrect = self.rect rect = self.render() if oldrect.colliderect(rect): return [oldrect.union(rect)] else: return [oldrect, rect] def nearer(self, rect1, rect2): """Compare two rects based on their distance from our position""" dist1 = rectdist(rect1, self.pos) dist2 = rectdist(rect2, self.pos) if dist1 == dist2: return cmp(rect2.width, rect1.width) return cmp(dist1, dist2) def render(self): """Render the text, returning the affected rect.""" pad = 3 maxdist = 200 screen_rect = Rect(0, 0, 640, 480 - (linesize(_font)+2)) size = linesize(_font) # Try not overlapping anything balloonrects = map(lambda s: s.rect, self.group.list) try: balloonrects.remove(self.rect) except ValueError: pass # First, try to avoid everything rects1 = invertrect(screen_rect, balloonrects + map(lambda s: s.rect, self.avgroup.list)) rects1.sort(self.nearer) # Next, try to avoid just balloons and my av rects2 = invertrect(screen_rect, balloonrects + [self.avatar.rect]) rects2.sort(self.nearer) # Finally, just avoid my own av rects3 = invertrect(screen_rect, [self.avatar.rect]) rects3.sort(self.nearer) rects = filter(lambda r,p=self.pos,m=maxdist: rectdist(r,p) < m, rects1 + rects2 + rects3) + [screen_rect] notdone = 1 while notdone: # Loop until we can fit the balloon into *some* rect for r in rects: try: lines = wrap_lines(self.text, r.width-pad*2, _font) except ValueError: continue if len(lines) * size + pad*2 < r.height: notdone = 0 break else: # Couldn't render the balloon, delete some lines del self.text[0] del self.timeouts[0] # Count the number of lines in each sequence of text surfaces = map(lambda t,f=_font,c=self.fgcolor: f.render(t, 1, c), lines) width = max(map(lambda l: _font.size(l)[0], lines)) + (pad * 2) height = len(surfaces) * size + (pad * 2) x, y = self.pos rect = Rect(x-width/2, y-height/2, width, height).clamp(r) bigrect = rect.union((self.pos, (1, 1))) image = pygame.Surface(bigrect.size) image.set_colorkey((255,165,0)) image.fill((255,165,0)) cx, cy = closest(rect, self.pos) cx = cx - bigrect.left cy = cy - bigrect.top x = x - bigrect.left y = y - bigrect.top left = rect.left - bigrect.left right = rect.right - bigrect.left - 1 top = rect.top - bigrect.top bottom = rect.bottom - bigrect.top - 1 # Calculate arcs for corners nwarc = arc(pad, (left+pad, top+pad), pi*1.5, pi*2, 5) nearc = arc(pad, (right-pad, top+pad), 0, pi*0.5, 5) searc = arc(pad, (right-pad, bottom-pad), pi*0.5, pi, 5) swarc = arc(pad, (left+pad, bottom-pad), pi, pi*1.5, 5) # Go in a clockwise direction if x > right-pad*2: # Drawing to the east if y > bottom-pad*2: # Draw the arrow to the southeast points = ((right, bottom-pad), (x, y), (right-pad, bottom)) + swarc + nwarc + nearc elif y < top+pad*2: # Draw the arrow to the northeast points = ((right-pad, top), (x, y), (right, top+pad)) + searc + swarc + nwarc elif x > right: # Due east points = ((right, cy-pad), (x, y), (right, cy+pad)) + \ searc + swarc + nwarc + nearc else: # Arrow is inside balloon points = searc + swarc + nwarc + nearc elif x < left+pad*2: # Drawing to the west if y > bottom-pad*2: # Southwest points = ((left+pad, bottom), (x, y), (left, bottom-pad)) + \ nwarc + nearc + searc elif y < top+pad*2: # Northwest points = ((left, top+pad), (x, y), (left+pad, top)) + \ nearc + searc + swarc elif x < left: # Due west points = ((left, cy+pad), (x, y), (left, cy-pad)) + \ nwarc + nearc + searc + swarc else: # Arrow is inside balloon points = nwarc + nearc + searc + swarc elif y < top: # Due north points = ((cx-pad, top), (x, y), (cx+pad, top)) + \ nearc + searc + swarc + nwarc elif y > bottom: # Due south points = ((cx+pad, bottom), (x, y), (cx-pad, bottom)) + \ swarc + nwarc + nearc + searc else: print >> sys.stderr, 'Oops, arrow point is inside balloon!' points = swarc + nwarc + nearc + searc pygame.draw.polygon(image, (255,255,255), points, 0) pygame.draw.polygon(image, (0,0,0), points, 1) y = rect.top - bigrect.top + pad x = rect.left - bigrect.left for s in surfaces: image.blit(s, (((width - s.get_width())/2+x), y)) y = y + size self.image = image #rect = rect.clamp((0, 0, 640, 480)) self.rect = bigrect return bigrect def add_text(self, text): """Add text to the balloon, scrolling it if necessary.""" self.text.append(text) self.timeouts.append(pygame.time.get_ticks() + self.timeout) return [self.rect.union(self.render())] def poll(self, ticks): if ticks >= self.timeouts[0]: del self.timeouts[0] del self.text[0] if self.text: return [self.rect.union(self.render())] else: self.group.remove(self) self.dead = 1 return [self.rect] else: return [] class Entry(Sprite): def __init__(self, pos, width=640, histlength=100, color=(255,255,255), bgcolor=(0,0,0)): Sprite.__init__(self, pos, pygame.Surface((0, 0))) self.pos = pos self.font = _font self.text = u'' self.color = color self.bgcolor = bgcolor self.width = width self.cursor = 0 self.buffer = [] self.index = 0 self.histlength = histlength def __len__(self): return len(self.text) def render(self): oldrect = self.rect cursorsize = 1 start = 0 while self.font.size(self.text[start:self.cursor+5])[0] > self.width: start = start + 1 if self.text: image = self.font.render(self.text[start:], 1, self.color, self.bgcolor) self.image = pygame.Surface((image.get_width() + cursorsize, image.get_height())) self.image.blit(image, (0, 0)) # Render the cursor cursor_pos = self.font.size(self.text[start:self.cursor])[0] cursor_height = self.image.get_height() self.image.fill((0,255,0), (cursor_pos, 0, cursorsize, cursor_height)) else: self.image = pygame.Surface((0, 0)) self.rect = Rect(self.pos, self.image.get_size()) return oldrect.union(self.rect) def insert(self, c): self.text = self.text[:self.cursor] + c + self.text[self.cursor:] self.cursor = self.cursor + len(c) return self.render() def backspace(self, n=1): """Delete one character at the end""" if self.cursor == 0: return self.text = self.text[:self.cursor-n] + self.text[self.cursor:] self.cursor = self.cursor - n return self.render() def delete(self, n=1): if self.cursor == len(self.text): return self.text = self.text[:self.cursor] + self.text[self.cursor+n:] return self.render() def home(self): self.cursor = 0 return self.render() def end(self): self.cursor = len(self.text) return self.render() def move_cursor(self, n): self.cursor = self.cursor + n if self.cursor > len(self.text): self.cursor = len(self.text) if self.cursor < 0: self.cursor = 0 return self.render() def history(self, n): if not self.buffer: return if self.text and self.index < len(self.buffer) and \ self.text != self.buffer[self.index]: self.add_history() self.index = self.index + n if self.index >= len(self.buffer): self.index = len(self.buffer) - 1 if self.index < 0: self.index = 0 self.text = self.buffer[self.index] self.cursor = len(self.text) return self.render() def add_history(self): self.buffer.append(self.text) if len(self.buffer) > self.histlength: del self.buffer[:len(self.buffer) - self.histlength] def clear(self): if self.text: self.add_history() self.index = len(self.buffer) self.text = u'' self.cursor = 0 return self.render() class Text: def __init__(self, text, font, width, fgcolor, bgcolor=None): self.text = text self.font = font self.fgcolor = fgcolor self.bgcolor = bgcolor if bgcolor is None: self.image = font.render(text, 1, fgcolor) self.shadow = font.render(text, 1, (0, 0, 0)) else: self.image = font.render(text, 1, fgcolor, bgcolor) self.shadow = None def draw(self, pos, rect=None): """Pos is the position of the upper left of my rect""" myrect = self.image.get_rect().move(pos) if rect is None: if self.shadow is not None: surface.blit(self.shadow, (pos[0]+1,pos[1]+1)) return surface.blit(self.image, pos) rect = myrect.clip(rect) if self.shadow is not None: surface.blit(self.shadow, rect.move(1, 1), ((rect.left-myrect.left-1, rect.top-myrect.top-1), rect.size)) return surface.blit(self.image, rect, ((rect.left-myrect.left, rect.top-myrect.top), rect.size)) class Console(Sprite): """Transparent scrollable text widget.""" def __init__(self, rect, font, scrollback=1000, bg=(255, 165, 0)): image = pygame.Surface(rect.size) image.set_colorkey(bg) image.set_alpha(127) image.fill(bg) Sprite.__init__(self, rect, image) self.scrollback = scrollback self.buffer = [''] * scrollback self.font = font self.bg = bg self.linesize = linesize(font) self.numlines = int(self.rect.height / self.linesize) + 1 self.currentcolor = (255, 255, 255) self.offset = 0 def add_lines(self, paragraphs): for paragraph in paragraphs: del self.buffer[0] self.buffer.append(paragraph) if self.offset > 0: self.offset = self.offset + 1 if self.offset > self.scrollback - self.numlines: self.offset = self.scrollback - self.numlines return self.render() def scroll_up(self, lines): self.offset = self.offset + lines if self.offset > self.scrollback - self.numlines: self.offset = self.scrollback - self.numlines return self.render() def scroll_down(self, lines): self.offset = self.offset - lines if self.offset < 0: self.offset = 0 return self.render() def render(self): """Update my image to match the current buffer""" self.image.fill(self.bg) lines = self.buffer[(self.scrollback - (self.numlines + self.offset)): (self.scrollback - (self.numlines + self.offset)) + self.numlines] newlines = [] for line in lines: if line != '': newlines.extend(wrap(line, self.rect.width - 2, self.font)) else: newlines.append('') lines = newlines[len(newlines) - self.numlines:] if self.offset > 0: bgcolor = (127, 0, 0) else: bgcolor = (0, 0, 0) x = 1 y = self.rect.height - ((self.numlines * self.linesize) + 1) for line in lines: if line != '': if line[0:1] == '<': self.currentcolor = (255, 255, 255) if line[0:1] == '*': self.currentcolor = (0, 255, 255) if line[0:1] == '!': if line[1:14] == 'Whispering to': self.currentcolor = (255, 191, 127) else: self.currentcolor = (255, 255, 0) if line[0:1] == '[': self.currentcolor = (0, 255, 0) if line[0:2] == '->': self.currentcolor = (0, 0, 255) ss = self.font.render(line, 0, bgcolor) for xx in range(3): for yy in range(3): self.image.blit(ss, (x + (xx - 1), y + (yy - 1))) self.image.blit(self.font.render(line, 1, self.currentcolor), (x, y)) y = y + self.linesize return [self.rect] class Client: """Callbacks for the server connection""" def __init__(self): global _clients, _active self.width, self.height = 640, 480 self.background = progress(0, (self.width, self.height)) self.server = None self.sobjects = Group() self.sprites = Group() self.balloons = Group() self.text = Group() self.avatars = {} self.exits = {} self.urls = [] self.whichmouseover = '' self.console_height = self.height - (linesize(_font) + 2) self.entry = Entry((0, self.console_height + 1)) self.text.add(self.entry) self.console = Console(Rect(0, 0, self.width, self.console_height), _font) self.console_active = 0 self.handler = transutil.InputHandler( (('connect', self.cmd_connect, '(\S+) (\d+)', (str, int)), ('reconnect', self.cmd_reconnect, '', ()), ('nick', self.cmd_nick, '(\S+)', (str,)), ('ignore', self.cmd_ignore, '(\S+) (\S+)', (str, str)), ('unignore', self.cmd_unignore, '(\S+) (\S+)', (str, str)), ('msg', self.cmd_msg, '(\S+) (.*)', (str, str)), ('url', self.cmd_url, '(\S+) (\S+)', (str, str)), ('quote', self.cmd_quote, '(.*)', (str,)), ('avatar', self.cmd_avatar, '(\S+)', (str,)), ('push', self.cmd_push, '', ()), ('effect', self.cmd_effect, '(\S+)', (str,)), ('whois', self.cmd_whois, '(\S+)', (str,)))) _clients.append(self) _active = self # Utility functions def set_server(self, server): """Set the server connection that the client talks to""" self.server = server def redraw(self, rects=None): """Redraw entire screen. Unfortunately required for scrolling text""" if self is not _active: return if rects is None: _display.blit(self.background, (0, 0)) self.sobjects.draw(_display) self.sprites.draw(_display) self.text.draw(_display) self.balloons.draw(_display) pygame.display.update() else: if not type(rects) is ListType: rects = [rects] for rect in rects: _display.blit(self.background, rect, rect) self.sobjects.draw(_display, rect) self.sprites.draw(_display, rect) self.text.draw(_display, rect) self.balloons.draw(_display, rect) pygame.display.update(rects) def debug(self, s): """Handle debug messages""" print >> sys.stderr, "DEBUG: %s" % s def write(self, s): dirtyrects = self.console.add_lines(string.split(s, '\n')) if self.console_active: return dirtyrects else: return [] # Command handlers def cmd_connect(self, host, port): self.write('-> Changing rooms to %s:%d' % (host, port)) # delete all objects/avatars here self.sprites = Group() self.avatars = {} self.exits = {} self.urls = [] self.set_title('PythonVerse') self.redraw() self.server.new_connect(host, port) def cmd_reconnect(self): host, port = self.server.gethostport() self.cmd_connect(host, port) def cmd_nick(self, nick): self.server.set_nick(nick) def cmd_ignore(self, nick, what): self.server.ignore(what, nick) def cmd_unignore(self, nick, what): self.server.unignore(what, nick) def cmd_quote(self, text): self.server.quote(text) def cmd_avatar(self, avatar): self.server.set_avatar(avatar) def cmd_msg(self, nicks, text): dirtyrects = self.write('!Whispering to %s! %s' % (nicks, text)) nicks = string.split(nicks, ',') nick = self.server.privmsg(nicks, text) avatar = self.avatars[nick] dirtyrects.extend(avatar.chat(self.balloons, text)) dirtyrects.append(avatar.rect) self.redraw(dirtyrects) def cmd_url(self, nicks, url): nicks = string.split(nicks, ',') self.server.url(nicks, url) def cmd_push(self): self.server.push() def cmd_effect(self, action): self.server.effect(action) def cmd_whois(self, nick): self.server.whois(nick) # UI functions def test_rect(self): rects = invertrect(Rect(0, 0, 640, 480 - (linesize(_font) + 2)), map(lambda s: s.rect, self.sprites.list + self.balloons.list)) for r in rects: pygame.draw.rect(_display, (0, 0, 0), r, 1) pygame.display.update() def poll(self): """Update moving stuff, clean up old balloons, etc""" dirtyrects = self.sprites.update() dirtyrects.extend(self.balloons.update()) # purge old URLs if self.urls != []: timetodie, url = self.urls[0] if pygame.time.get_ticks() >= timetodie: del self.urls[0] try: urlmo = self.avatars[url] except KeyError: self.debug('No URL %s' % url) else: dirtyrects.append(urlmo.rect) del self.avatars[url] try: del self.exits['{' + url + '}_link'] except: print >> sys.stderr, url, 'is not an exit.' self.sprites.remove(urlmo) if dirtyrects: self.redraw(dirtyrects) def mouse(self, event): """Check for mouse movement""" dirtyrects = self.sprites.mouse(event.pos) if dirtyrects: self.redraw(dirtyrects) def toggle_console(self): if self.console_active: self.console_active = 0 self.text.remove(self.console) else: self.console_active = 1 self.text.add(self.console) self.redraw(self.console.rect) def handle_event(self, event): if event.type == KEYDOWN: if event.mod & KMOD_CTRL: if event.key == K_u: self.redraw(self.entry.clear()) if event.key == K_UP: self.redraw(self.console.scroll_up(1)) if event.key == K_DOWN: self.redraw(self.console.scroll_down(1)) if event.key == K_PAGEUP: self.redraw(self.console.scroll_up(self.console.numlines)) if event.key == K_PAGEDOWN: self.redraw(self.console.scroll_down(self.console.numlines)) elif event.key == K_BACKSPACE: self.redraw(self.entry.backspace()) elif event.key == K_DELETE: self.redraw(self.entry.delete()) elif event.key == K_LEFT: self.redraw(self.entry.move_cursor(-1)) elif event.key == K_RIGHT: self.redraw(self.entry.move_cursor(1)) elif event.key == K_UP: self.redraw(self.entry.history(-1)) elif event.key == K_DOWN: self.redraw(self.entry.history(1)) elif event.key == K_HOME: self.redraw(self.entry.home()) elif event.key == K_END: self.redraw(self.entry.end()) elif event.key == K_RETURN: text = self.entry.text self.redraw(self.entry.clear()) if text and text[0] == '/': text = text[1:] if text and text[0] != '/': try: self.handler.handle(text) except transutil.HandlerError, info: self.debug(info) return self.server.chat(text.encode('utf-8')) else: self.redraw(self.entry.insert(event.unicode)) elif event.type == MOUSEBUTTONDOWN: if self.whichmouseover == '': self.server.move(event.pos) else: if self.whichmouseover != 'ov_tram_exit': self.whichmouseover = '{' + self.whichmouseover + '}_link' try: host, port = self.exits[self.whichmouseover] except: self.server.move(event.pos) else: if port == -1: if os.fork() == -1: webbrowser.open(host) if port == -2: realhost, realport, filename, filesize = host # TODO: file transfers if port > 0: self.cmd_connect(host, port) # Transport-called functions def background_image(self, image): """Change the background""" self.background = image self.redraw() def background_progress(self, length, filename, size): self.background = progress(float(length)/float(size), (640, 480), (0, 0, 255), (0, 0, 0)) self.redraw() def set_title(self, title): """Change the room's name""" pygame.display.set_caption(title) def raise_object(self, name): """Raise the named object to the top of the stacking order.""" try: avatar = self.avatars[name] except: self.debug('No avatar called %s' % name) else: self.sprites.above(avatar) self.redraw(avatar.rect) def mouseover(self, name, pos, image1, image2): """Create a mouseover object""" mo = Mouseover(self.server, name, pos, image1, image2) self.sprites.add(mo) self.avatars[name] = mo self.redraw(mo.rect) def newimage(self, filename=None): """Load an image from a file object""" #self.debug('newimage %s' % repr(filename)) if filename is None: return progress(0) image = pygame.image.load(filename).convert_alpha() return image def new_avatar(self, nick, pos, image, noffset, boffset): # Fixme: need a real default image avatar = Avatar(self.server, self.sprites, pos, image, nick, noffset, boffset) self.sprites.add(avatar) self.avatars[nick] = avatar dirtyrects = self.write('*%s* entered the room.' % nick) # dirtyrects.extend(avatar.chat(self.balloons, # '*%s* entered the room.' % nick)) dirtyrects.append(avatar.rect) self.redraw(dirtyrects) def del_avatar(self, nick): try: avatar = self.avatars[nick] except KeyError: self.debug('No avatar called %s' % nick) else: dirtyrects = self.write('*%s* left the room.' % nick) # dirtyrects.extend(avatar.chat(self.balloons, # '*%s* left the room' % nick)) dirtyrects.append(avatar.rect) del self.avatars[nick] self.sprites.remove(avatar) self.redraw(dirtyrects) def exit_obj(self, name, host, port): if host != 'dummyhost': self.exits[name] = (host, port) else: del self.exits[name] def avatar(self, nick, image, noffset, boffset): """Change the avatar for a nick""" # FIXME: need to handle other parameters (bubble position, nametag) try: avatar = self.avatars[nick] except KeyError: self.debug('No avatar called %s' % nick) else: self.redraw(avatar.set_image(image)) avatar.set(noffset, boffset) def mouseover_image1(self, name, image): """Set the unactivated image for a mouseover""" self.redraw(self.avatars[name].set_image1(image)) def mouseover_image2(self, name, image): """Set the activated image for a mouseover""" self.avatars[name].set_image2(image) def avatar_image(self, nick, image): """Set the image for an avatar""" try: avatar = self.avatars[nick] except KeyError: self.debug('No avatar called %s' % nick) else: self.redraw(avatar.set_image(image)) def avatar_progress(self, nick, length, filename, size): """Set an avatar's image to a progress bar""" try: avatar = self.avatars[nick] except KeyError: self.debug('No avatar called %s' % nick) else: rect = avatar.set_image(progress(float(length)/float(size))) self.redraw(rect) def move_avatar(self, nick, x, y, speed): try: avatar = self.avatars[nick] except: self.debug('No avatar called %s' % nick) else: avatar.move((x, y), speed) def effect(self, nick, action): action.lower() try: avatar = self.avatars[nick] except: self.debug('No avatar called %s' % nick) else: avatar.effect(action) def privmsg(self, nick, s): dirtyrects = self.write('!%s whispers! %s' % (nick, s)) try: avatar = self.avatars[nick] except KeyError: self.debug('No avatar called %s' % nick) else: self.redraw(dirtyrects + avatar.chat(self.balloons, s)) def url(self, nick, url): try: test = self.avatars[url] except: dirtyrects = self.write('[URL from %s] %s' % (nick, url)) # TODO: the 30000 should be replaced with URL timeout from Setup self.urls.append((pygame.time.get_ticks() + 30000, url)) # parse URL to find out what we want to do with it text = 'Bad' tagcolor = (255, 0, 0) head = url[0:3] head.lower() if head == 'ope': offset = url.find('://') + 3 offset2 = url.find(':', offset) host = url[offset:offset2] port = int(url[offset2+1:]) self.exit_obj('{' + url + '}_link', host, port) text = 'OV:' tagcolor = (255, 255, 0) if head == 'fil': offset = url.find('://') + 3 offset2 = url.find(':', offset) offset3 = url.find('/', offset2) offset4 = url.find(':', offset3) host = url[offset:offset2] port = url[offset2+1:offset3] filename = url[offset3+1:offset4] filesize = url[offset4:] text = 'File' tagcolor = (0, 255, 255) self.exit_obj('{' + url + '}_link' , (host, port, filename, filesize), -2) if head == 'htt' or head == 'ftp' or head == 'mai': text = 'URL' tagcolor = (0, 255, 0) self.exit_obj('{' + url + '}_link', url, -1) # generate images for URL links image1 = pygame.Surface((40, 16)) image2 = pygame.Surface((40, 16)) image1.set_colorkey((255,165,0)) image1.fill((255,165,0)) image2.set_colorkey((255,165,0)) image2.fill((255,165,0)) pygame.draw.polygon(image2, (0, 0, 0), ((1,1), (1, 15), (39, 15), (39, 6), (34, 1), (1,1)), 0) pygame.draw.polygon(image2, tagcolor, ((0,0), (0, 14), (38, 14), (38, 5), (33, 0), (0,0)), 0) pygame.draw.polygon(image2, (0, 0, 255), ((0,0), (0, 14), (28, 14), (28, 5), (23, 0), (0,0)), 0) image2.blit(_font.render(text, 1, (255, 255, 255)), (2, 0)) image1.blit(image2, (0, 0)) image1.set_alpha(127) pos = (int((random.random()*600)+20), int((random.random()*445)+10)) urlmo = Mouseover(self.server, url, pos, image1, image2) self.sprites.add(urlmo) self.avatars[url] = urlmo dirtyrects.append(urlmo.rect) self.redraw(dirtyrects) def chat(self, nick, s): dirtyrects = self.write('<%s> %s' % (nick, s)) try: avatar = self.avatars[nick] except KeyError: self.debug('No avatar called %s' % nick) else: dirtyrects.extend(avatar.chat(self.balloons, s)) self.redraw(dirtyrects) def setmouseover(self, name): if name == 'ov_tram_exit': try: test = self.exits[name] # is this really the ORT? except: self.whichmouseover = '' else: self.whichmouseover = name else: self.whichmouseover = name # Exported functions def linesize(font): # Bug in pygame or SDL_ttf? size = font.get_linesize() if size == 0: size = font.get_height() return size _clients = [] _display = None _active = None def init(): global _font, _display, _lastpoll pygame.init() _display = pygame.display.set_mode((640, 480)) # display startup image s = pygame.display.get_surface() i = pygame.image.load('pvstart.jpg') s.blit(i, (0, 0)) # other settings pygame.display.set_caption('PythonVerse') _font = pygame.font.Font('geneva.ttf', 12) pygame.key.set_repeat(300, 30) pygame.event.set_blocked((ACTIVEEVENT, KEYUP, MOUSEBUTTONUP, JOYAXISMOTION, JOYBALLMOTION, JOYHATMOTION, JOYBUTTONUP, JOYBUTTONDOWN, VIDEORESIZE, VIDEOEXPOSE)) _lastpoll = pygame.time.get_ticks() def poll(): global _lastpoll # Handle mouseevents events = pygame.event.get(MOUSEMOTION) if events: event = events[-1] _active.mouse(event) events = pygame.event.get((KEYDOWN, MOUSEBUTTONDOWN, QUIT)) pygame.event.pump() for event in events: if event.type == KEYDOWN: if event.key == K_ESCAPE: pygame.event.post(pygame.event.Event(QUIT)) elif event.mod & KMOD_ALT: if event.key == K_f: pygame.display.toggle_fullscreen() elif event.key == K_c: _active.toggle_console() elif event.key == K_t: _active.test_rect() else: _active.handle_event(event) elif event.type == QUIT: return -1 else: _active.handle_event(event) i = len(_clients) while i: i = i - 1 done = _clients[i].poll() if done: del _clients[i] now = pygame.time.get_ticks() delay = 15 - (now - _lastpoll) _lastpoll = now if delay < 0: return 0 return delay/1000.0