Permalink
Find file
Fetching contributors…
Cannot retrieve contributors at this time
1382 lines (1193 sloc) 45 KB
# -*-Python-*-
# Copyright (c) 2002 Sean R. Lynch <seanl@chaosring.org>
#
# 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)
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 push(self, x, y, speed): pass
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