Removed commented out OpenGL code.
[tower.git] / tower.py
blob3bcac2b1e4bf3530418b137ce8a5ba5309d121ca
1 # tower.py {{{1
2 # Andy Hammerlindl 2008/02/09
4 # Entry for the 2008 Game Making Deathmatch. Build a tower that can send deadly
5 # viruses into dimension N+1.
7 # Imports {{{1
8 from __future__ import with_statement
10 import os
11 import sys
12 import socket
13 import errno
14 import pickle as pickle
15 import struct
16 import shelve
17 import math
18 import random
19 from math import sqrt, exp, log, sin, cos, tan, asin, acos, atan, atan2
20 from operator import add, concat
22 from OpenGL.GL import *
23 from OpenGL.GLU import *
24 from OpenGL.GL.ARB.texture_rectangle import *
25 import pygame
26 from pygame.locals import *
28 # Constants {{{1
29 fullscreen = True
31 width = 1024
32 height = 768
33 #width = 1680
34 #height = 1050
36 # Update the world atmost every 50 ms.
37 updateThreshold = 50
39 # How fast partially construced buildings fall into disrepair.
40 disrepairFactor = 1.0/3.0
42 # How long a building is highlighted when it dies.
43 rubbleDuration = 500
45 # The dimensions of each level of the tower is.
46 levelHeight = 120
47 baseHeight = levelHeight - 64
48 baseLength = 400
49 levelStep = 40
51 # Where the pillars holding up the girders are placed. This is needed by the
52 # Girder class to draw the pillars and by the Level class to know where to place
53 # building between pillars.
54 pillarWidth = 16
55 pillarInset = 0.15
57 mountLength = 40
58 mountHeight = 40
60 # How long it take a heated gun to cool down.
61 coolDownTime = 15000
63 # The acceleration of gravity, in pixels per ticks^2
64 gravity = 5 * 10 ** -4
66 # Colors for drawing buildings.
67 blueprintColor = (0,0,0,0.2)
68 buildingColor = (0,0,0,0.5)
69 builtColor = (0,0,0,1)
70 damagedColor = (0.4, 0, 0, 1)
71 rubbleColor = (2.0, 0.5, 0, 1)
72 hotColor = (1.0, 0.5, 0.0, 1)
74 missileColor = (1,1,1,1)
76 # Virus have different levels, each with their own color.
77 virusColors = [ (1.0, 0.6, 1.0, 1.0), (1.0, 1.0, 0.5, 1.0),
78 (0.6, 1.0, 0.6, 1.0), (1.0, 0.8, 0.6, 1.0) ]
79 foreignVirusColor = (0.6, 0.85, 1.0, 1.0)
80 gatewayLoadTime = 4000
81 mutatorHalfLife = 5000
83 virusTravelTime = 8000
84 virusDecayTime = 80
86 virusOverPercent = float(virusDecayTime) / float(virusTravelTime)
89 # The rate at which clouds rise
90 lift = 0.01
92 # Draw Layer - To draw all parts of the scene in the right order. Lower
93 # numbers are drawn first.
94 BACKDROP_LAYER = -99
95 SPLINE_LAYER = -3
96 VIRUS_LAYER = -2
97 CONTRAIL_LAYER = -1
98 BUILDING_LAYER = 0
99 FLAME_LAYER = 1
100 AURA_LAYER = 2
101 MISSILE_LAYER = 3
102 CLOUD_LAYER = 4
103 LABEL_LAYER = 99
105 # Events
106 PURGE_EVENT = USEREVENT + 1
107 MUSIC_EVENT = USEREVENT + 2
109 # Stats {{{1
111 # buildingStats holds the stats that are shared for each building of a certain
112 # type, such as the time to build it, and it's maximum hit points.
113 buildingStats = {
114 'factory': { 'hp': 1000, 'buildtime': 8000,
115 'desc': 'Improves building times.' },
116 'plant': { 'hp': 1000, 'buildtime': 7000,
117 'desc': 'Improves shield effectiveness.' },
118 'munitions': { 'hp': 1000, 'buildtime': 7000,
119 'desc': 'Improves missile accuracy and yield.' },
120 'gateway': { 'hp': 800, 'buildtime': 9000,
121 'desc': 'Sends viruses through dimension N+1.' },
122 'mutator': { 'hp': 800, 'buildtime': 7000,
123 'desc': 'Mutates viruses to counter immunity.' },
124 'girder': { 'hp': 2000, 'buildtime': 1000 }, # Shouldn't be used.
125 'gun': { 'hp': 400, 'buildtime': 3000,
126 'desc': 'Fires missiles.' },
127 'shield': { 'hp': 400, 'buildtime': 3000,
128 'desc': 'Deflects missiles.' }
131 # Stats for each level:
132 levelStats = [
133 { 'girder': { 'buildtime': 10000, 'hp': 5000 },
134 'buildings': [ 'Factory', 'Factory' ] },
135 { 'girder': { 'buildtime': 12000, 'hp': 4000 },
136 'buildings': [ 'Factory', 'Plant' ] },
137 { 'girder': { 'buildtime': 16000, 'hp': 2800 },
138 'buildings': [ 'Factory', 'Munitions' ] },
139 { 'girder': { 'buildtime': 32000, 'hp': 1400 },
140 'buildings': [ 'Gateway', 'Plant' ] },
141 { 'girder': { 'buildtime': 45000, 'hp': 1200 },
142 'buildings': [ 'Mutator', 'Munitions' ] },
143 { 'girder': { 'buildtime': 90000, 'hp': 1000 },
144 'buildings': [ 'Plant', 'Munitions' ] } ]
146 # Factory boost - rate that building is increased by for a tower with factories.
147 factoryBoost = [ 1.0, 1.35, 1.70, 1.95 ]
149 # Missile Stats - stats are improved by building munitions.
150 # For accuracy, lower is better.
151 baseRange = width - 1024 + 400
152 missileStats = [
153 { 'range': (baseRange,150), 'angle': 30.0, 'accuracy': 0.07,
154 'radius': 120, 'damage': 200, 'scale': 1.0,
155 'loadTime': 1000, 'heat': 0.5 },
156 { 'range': (baseRange+50,200), 'angle': 27.0, 'accuracy': 0.06,
157 'radius': 135, 'damage': 300, 'scale': 1.15,
158 'loadTime': 750, 'heat': 0.4 },
159 { 'range': (baseRange+100,300), 'angle': 25.0, 'accuracy': 0.04,
160 'radius': 150, 'damage': 400, 'scale': 1.25,
161 'loadTime': 600, 'heat': 0.27 },
162 { 'range': (baseRange+200,400), 'angle': 20.0, 'accuracy': 0.02,
163 'radius': 165, 'damage': 500, 'scale': 1.4,
164 'loadTime': 400, 'heat': 0.2 } ]
166 shieldStats = [
167 { 'radius': 200, 'strength': 700, 'loadTime': 3000, 'width': 15.0 },
168 { 'radius': 220, 'strength': 900, 'loadTime': 2500, 'width': 20.0 },
169 { 'radius': 240, 'strength': 1150, 'loadTime': 2000, 'width': 28.0 },
170 { 'radius': 250, 'strength': 1400, 'loadTime': 1500, 'width': 35.0 } ]
172 # Shelf {{{1
173 shelf = shelve.open("settings")
174 def defaultSetting(name, value):
175 if name not in shelf:
176 shelf[name] = value
178 defaultSetting('graphicsLevel', 2)
179 defaultSetting('address', '127.0.1.1')
180 defaultSetting('handicap', 1.0)
181 defaultSetting('aiHandicap', 1.0)
182 defaultSetting('lastPlayed', None)
183 defaultSetting('fullscreen', True)
185 def graphicsLevel():
186 return shelf['graphicsLevel']
188 def toggleGraphicsLevel():
189 level = graphicsLevel()
190 if level < 3:
191 shelf['graphicsLevel'] = level+1
192 else:
193 shelf['graphicsLevel'] = 1
196 def graphicsDependent(*array):
197 return array[graphicsLevel() - 1]
199 # Util Functions {{{1
200 def interp(t, a, b):
201 if type(a) == tuple:
202 assert type(b) == tuple and len(a) == len(b)
203 return tuple([interp(t, a[i], b[i]) for i in range(len(a))])
204 else:
205 return a + t * (b-a)
207 def insort(a, x, key, lo=0, hi=None):
208 """Insert x into the list a, using key to determine the order. This assumes
209 that a is already sorted. x goes to the right-most position. This is based
210 on the bisect module (which doesn't support keys)."""
211 if hi is None:
212 hi = len(a)
213 while lo < hi:
214 mid = (lo+hi)//2
215 if key(x) < key(a[mid]): hi = mid
216 else: lo = mid+1
217 a.insert(lo, x)
219 def argmin(sequence, fn):
220 """Return the element x of sequence so that fn(x) is minimal. Based on
221 Peter Norvig's elementations:
222 http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/389640"""
223 return min((fn(x), x) for x in sequence)[1]
225 def solveQuadratic(a, b, c):
226 desc = b**2 - 4*a*c
228 if desc < 0.0:
229 return []
231 root = sqrt(desc)
232 return [(-b-root) / (2*a), (-b+root) / (2*a)]
234 def invertPos(pos):
235 x,y = pos
236 return (x, height-y-1)
238 def mousePos():
239 return invertPos(pygame.mouse.get_pos())
241 def bboxCenter(bbox):
242 l,b,r,t = bbox
243 return ((r+l)/2, (t+b)/2)
245 def dist2(loc1, loc2):
246 x1,y1 = loc1; x2,y2 = loc2
247 return (x1-x2)**2 + (y1-y2)**2
249 def dist(loc1, loc2):
250 return sqrt(dist2(loc1, loc2))
252 def length2(vector):
253 x,y = vector
254 return x**2 + y**2
256 def length(vector):
257 x,y = vector
258 return sqrt(x**2 + y**2)
260 def unit(vector):
261 x,y = vector
262 if x == 0.0 and y == 0.0:
263 return (0,0)
264 else:
265 l = length(vector)
266 return (x/l, y/l)
268 def dot(u, v):
269 return u[0]*v[0] + u[1]*v[1]
271 def locationInBBox(location, bbox):
272 x,y = location
273 l,b,r,t = bbox
274 return l <= x and x < r and b <= y and y < t
276 def bboxInBBox(box1, box2):
277 return locationInBBox((box1[0], box1[1]), box2) and \
278 locationInBBox((box1[2], box1[3]), box2)
280 def distToRange(x, lo, hi):
281 if x < lo:
282 return lo - x
283 elif x > hi:
284 return x - hi
285 else:
286 return 0
288 def distToBBox(location, bbox):
289 x,y = location; l,b,r,t = bbox
290 return length((distToRange(x,l,r), distToRange(y,b,t)))
292 def sideOfScreen(location):
293 if location[0] < width/2:
294 return 'left'
295 else:
296 return 'right'
298 class NoTexture():
299 def __enter__(self):
300 glDisable(textureFlag)
301 def __exit__(self, type, value, tb):
302 glEnable(textureFlag)
304 class PushedMatrix():
305 def __enter__(self):
306 glPushMatrix()
307 def __exit__(self, type, value, tb):
308 glPopMatrix()
310 class glGroup():
311 def __init__(self, mode):
312 self.mode = mode
313 def __enter__(self):
314 glBegin(self.mode)
315 def __exit__(self, type, value, tb):
316 glEnd()
318 class Perspective():
319 def __init__(self, center=(width/2,height/2), stop=width/2):
320 self.center = center
321 self.stop = stop
322 def __enter__(self):
323 x0, y0 = self.center
324 l = self.stop
325 glMatrixMode(GL_PROJECTION)
326 glPushMatrix()
328 # Matrices are given in a weird order in OpenGL, so it is actually the
329 # transpose of this matrix.
330 glMultMatrixf([ l, 0, 0, 0,
331 0, l, 0, 0,
332 x0, y0, 0, 1,
333 -x0*l, -y0*l, 0, 0])
335 glMatrixMode(GL_MODELVIEW)
337 def __exit__(self, type, value, tb):
338 glMatrixMode(GL_PROJECTION)
339 glPopMatrix()
340 glMatrixMode(GL_MODELVIEW)
342 def drawRectangle(bbox, color):
343 with NoTexture():
344 glColor4fv(color)
346 l,b,r,t = bbox
347 glBegin(GL_QUADS)
348 glVertex2f(l,b)
349 glVertex2f(r,b)
350 glVertex2f(r,t)
351 glVertex2f(l,t)
352 glEnd()
354 def drawRoundedRectangle(bbox, color, border=8, segments = 24):
355 segments /= 4
357 def arc(x,y, angle1, angle2):
358 for i in range(segments+1):
359 angle = angle1 + float(i)/float(segments) * (angle2 - angle1)
360 glVertex2f(x + border*cos(angle), y + border*sin(angle))
362 with NoTexture():
363 glColor4fv(color)
364 l,b,r,t = bbox
365 l += border/2; r -= border/2
366 b += border/2; t -= border/2
368 glBegin(GL_POLYGON)
369 arc(l, b, -math.pi, -0.5*math.pi)
370 arc(r, b, -0.5*math.pi, 0.0)
371 arc(r, t, 0.0, 0.5*math.pi)
372 arc(l, t, 0.5*math.pi, math.pi)
373 glEnd()
375 def otherSide(side):
376 if side == 'left':
377 return 'right'
378 else:
379 assert side == 'right'
380 return 'left'
382 # A list of functions to be called after OpenGL is initialized.
383 postGLInits = []
385 # Network {{{1
386 PORT = 6729
387 class Network:
388 """This classes encapsulates the sockets used to send messages send between
389 computers. Use the methods sendMessage(msg) and getMessages() to send and
390 receive messages. Messages are simply dictionaries. getMessages() never
391 blocks and returns either a complete message or None."""
393 def __init__(self):
394 self.sizeBuf = ''
395 self.size = None
396 self.buf = ''
397 self.LSize = struct.calcsize("l")
398 self.name = 'offline'
400 def reset(self):
401 if 'server' in self.__dict__:
402 self.server.close()
403 del self.server
404 if 'client' in self.__dict__:
405 self.client.close()
406 del self.client
407 if 'sock' in self.__dict__:
408 self.sock.close()
409 del self.sock
410 self.name = 'offline'
412 # Should we reset the message state?
414 def startServer(self):
415 """Starts the server and returns."""
416 assert 'server' not in self.__dict__
417 assert 'sock' not in self.__dict__
418 assert 'client' not in self.__dict__
420 self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
421 self.server.setblocking(0)
422 self.server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
423 self.server.bind(('', PORT))
424 #self.server.bind((socket.gethostname(), PORT))
425 self.server.listen(5)
427 def connectToClient(self):
428 """Once the server is started, listen for a client. This never blocks
429 and returns a True if a client is connected."""
430 assert 'server' in self.__dict__
431 assert 'sock' not in self.__dict__
432 assert 'client' not in self.__dict__
434 try:
435 (sock, address) = self.server.accept()
436 sock.setblocking(0)
437 self.sock = sock
438 self.name = 'server'
440 self.server.close();
441 del self.server
442 return True
443 except socket.error, e:
444 # Only handle blocking errors. Rethrow anything else.
445 if e.args[0] == errno.EWOULDBLOCK:
446 return False
447 else:
448 raise
450 def connectToServer(self, address):
451 """Connect to the server. This doesn't block and return True if a
452 connection is established. Can be called repeatedly (but with the same
453 address)."""
454 assert 'server' not in self.__dict__
455 assert 'sock' not in self.__dict__
457 if 'client' not in self.__dict__:
458 self.client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
459 self.client.setblocking(0)
461 # Based on code in the asyncore module.
462 err = self.client.connect_ex((address, PORT))
463 if err in (errno.EINPROGRESS, errno.EALREADY, errno.EWOULDBLOCK):
464 return False
465 if err in (0, errno.EISCONN):
466 self.sock = self.client
467 self.name = 'client'
469 del self.client
470 return True
471 else:
472 raise socket.error, (err, os.strerror(err))
474 def sendMessage(self, obj):
475 """Send an object through the network. Based on the example:
476 http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/457669/index_txt
478 assert 'sock' in self.__dict__
480 #print "sending:", obj
481 buf = pickle.dumps(obj)
482 size = struct.pack("l", socket.htonl(len(buf)))
483 self.sock.send(size)
484 self.sock.send(buf)
486 def getMessage(self):
487 """Returns the next message in the queue that has been completely sent
488 through the network. Returns None if no such message is available."""
490 try:
491 if self.size == None:
492 # Read in the size until it is completely read.
493 while len(self.sizeBuf) < self.LSize:
494 feed = self.sock.recv(self.LSize - len(self.sizeBuf))
495 if feed == '':
496 return None
497 else:
498 self.sizeBuf += feed
500 # Decode the integer size from the sizeBuf string.
501 assert len(self.sizeBuf) == self.LSize
502 self.size = socket.ntohl(struct.unpack("l", self.sizeBuf)[0])
503 self.sizeBuf = ''
505 # If we've reached here, we have a size.
506 assert self.size != None
508 # Read in the buffer until it is completely read.
509 while len(self.buf) < self.size:
510 feed = self.sock.recv(self.size - len(self.buf))
511 if feed == '':
512 return None
513 else:
514 self.buf += feed
516 # Unpack the object from the buffer.
517 assert len(self.buf) == self.size
518 obj = pickle.loads(self.buf)
519 self.buf = ''
520 self.size = None
521 #print "received:", obj
522 return obj
523 except socket.error, e:
524 if e[0] in [errno.EAGAIN, errno.EWOULDBLOCK]:
525 return None
526 else:
527 raise
529 # The global instance of the network.
530 net = Network()
532 def closeSocket():
533 if 'server' in net.__dict__ and net.server:
534 net.server.close()
535 if 'client' in net.__dict__ and net.client:
536 net.client.close()
537 if 'sock' in net.__dict__ and net.sock:
538 net.sock.close()
540 sys.exitfunc = closeSocket
542 # Entities {{{1
543 class Entity:
544 def local(self):
545 """Returns true if this computer is manager the player who owns this
546 entity. Uses the side field."""
547 return self.side in towers
549 def tower(self):
550 return towers[self.side]
552 def playing(self):
553 return self.local() and self.tower().playing
555 def broadcast(self, method, **msg):
556 """Broadcast sends a message out to every computer, including the local
557 one."""
558 assert 'name' not in msg or msg['name'] == self.name
559 assert 'method' not in msg or msg['method'] == method
561 msg['name'] = self.name
562 msg['method'] = method
564 broadcastMessage(msg)
566 def emit(self, method, **msg):
567 """Emit sends a message to the other computer, but does not process it
568 locally."""
569 assert 'name' not in msg or msg['name'] == self.name
570 assert 'method' not in msg or msg['method'] == method
572 msg['name'] = self.name
573 msg['method'] = method
575 emitMessage(msg)
577 # Global repository of all entities.
578 entities = {}
580 nameCounter = 0
581 def newName():
582 global nameCounter
583 nameCounter += 1
584 return (net.name, nameCounter)
586 def entityFromMessage(msg):
587 assert msg['method'] == 'create'
588 assert msg['name'] not in entities
591 # Get the class object of the entity we are creating.
592 cls = globals()[msg['type']]
593 name = msg['name']
595 entity = cls(**msg)
596 entity.name = name
597 entities[name] = entity
598 return entity
600 def nameof(cls):
601 """Replace a class object with it's name. Leave anything else unchanged."""
602 return getattr(cls, '__name__', cls)
604 def createEntity(cls, **msg):
605 type = nameof(cls)
607 assert 'method' not in msg or msg['method'] == 'create'
608 assert 'type' not in msg or msg['type'] == type
609 assert 'name' not in msg
611 msg['name'] = newName()
612 msg['method'] = 'create'
613 msg['type'] = type
615 entity = entityFromMessage(msg)
616 if net.name != 'offline':
617 net.sendMessage(msg)
618 return entity
620 def processMessage(msg):
621 if msg['method'] == 'create':
622 entityFromMessage(msg)
623 else:
624 entity = entities[msg['name']]
625 method = getattr(entity.__class__, msg['method'])
626 method(entity, **msg)
628 def processPendingMessages():
629 if net.name != 'offline':
630 while 1:
631 msg = net.getMessage()
632 if msg == None:
633 break
634 processMessage(msg)
636 def emitMessage(msg):
637 if net.name != 'offline':
638 net.sendMessage(msg)
640 def broadcastMessage(msg):
641 emitMessage(msg)
642 processMessage(msg)
645 # PriorityList {{{1
646 class PriorityList:
647 """PriorityList maintains a list of objects in an ordering prescribed by the
648 level given when the object is added. The objects are ordered by lowest
649 level first. The list can be accessed by the objs field."""
650 def __init__(self):
651 self.objs = []
653 def add(self, obj, level):
654 """Add a drawable object at a specified level."""
655 insort(self.objs, (obj, level), key = lambda x: x[1])
657 def remove(self, target):
658 for i in range(len(self.objs)):
659 obj, level = self.objs[i]
660 if obj == target:
661 del self.objs[i]
662 return
664 # Updater {{{1
665 class Updater(PriorityList):
666 def __init__(self):
667 PriorityList.__init__(self)
668 self.paused = False
669 self.pauseTime = None
670 self.downTime = 0 # Number of ticks paused since the beginning of time.
672 self.pauseLabel = None
674 def update(self, ticks, duration):
675 """Update every object that needs to be updated."""
676 if not self.paused:
677 # Use a copy so objects can be removed while updating.
678 for (obj, level) in self.objs[:]:
679 obj.update(ticks - self.downTime, duration)
681 def duration(self, t1, t2):
682 """Returns the duration of an interval where times is given in pygame
683 ticks. This accounts for pausing."""
684 assert t1 <= t2
685 if self.paused:
686 if t1 > self.pauseTime:
687 return 0
688 elif t2 > self.pauseTime:
689 return t2 - self.pauseTime
690 return t2 - t1
692 def convert(self, ticks):
693 if self.paused and ticks > self.pauseTime:
694 ticks = self.pauseTime
695 return ticks - self.downTime
697 def get_ticks(self):
698 return self.convert(pygame.time.get_ticks())
700 def pause(self):
701 if self.paused:
702 return
704 self.paused = True
705 self.pauseTime = pygame.time.get_ticks()
707 if self.pauseLabel == None:
708 self.pauseLabel = Label('Paused', (width/2,height/2), 'center')
709 else:
710 self.pauseLabel.text = 'Paused'
711 title.show()
713 def unpause(self):
714 if not self.paused:
715 return
717 self.downTime += pygame.time.get_ticks() - self.pauseTime
718 self.pauseTime = None
720 if self.pauseLabel:
721 self.pauseLabel.text=''
723 self.paused = False
724 title.hide()
726 def togglePause(self):
727 if self.paused:
728 self.unpause()
729 else:
730 self.pause()
731 updater = Updater()
733 class PauseMessage(Entity):
734 def __init__(self, pause, **msg):
735 if pause:
736 updater.pause()
737 else:
738 updater.unpause()
740 # Scene {{{1
741 class Scene(PriorityList):
742 """To make it easy to have special effects such as explosions, all drawing
743 is managed in a central place. The scene maintains a list of drawable
744 objects (sorted by level). To draw everything, the scene calls the draw
745 method of every object in its list, which draws them in increasing order.
747 Draw objects must support a draw() method (this will be passed timing info
748 eventually), and a doneDrawing() method which returns a boolean telling the
749 drawManager if it can remove the object from its list."""
751 def draw(self):
752 """Draw the entire scene."""
753 for (obj, level) in self.objs:
754 obj.draw()
756 def purge(self):
757 """Remove any animations or objects that no longer need to be drawn."""
758 self.objs = [pair for pair in self.objs if not pair[0].doneDrawing()]
760 scene=Scene()
762 # Mixer {{{1
763 soundList = [ 'building', 'built', 'missile', 'loading', 'loaded',
764 'loading_shield', 'loaded_shield', 'nogo',
765 'loading_virus', 'loaded_virus', 'launch_virus', 'virus',
766 'mutator', 'mutated', 'click', 'victory'
768 explodeList= [ 'explode'+str(i) for i in range(5) ]
769 destroyList = [ 'destroy'+str(i) for i in range(2) ]
770 soundList += explodeList + destroyList
772 musicList = (['music'+str(i)+'.ogg' for i in range(3) ])
774 class Mixer:
775 def __init__(self):
776 self.sounds = {}
777 for name in soundList:
778 self.load(name)
780 self.sounds['click'].set_volume(0.15)
782 random.shuffle(musicList)
784 pygame.mixer.init(44100)
785 pygame.mixer.music.set_volume(0.7)
786 pygame.mixer.music.set_endevent(MUSIC_EVENT)
787 self.paused = False
788 if musicList[0] == shelf['lastPlayed']:
789 self.advanceMusicList()
790 self.newSong()
792 def advanceMusicList(self):
793 global musicList
794 # Put the first track at the end of the list.
795 musicList = musicList[1:] + musicList[0:1]
797 def newSong(self):
798 music = pygame.mixer.music
799 music.load(os.path.join('data', musicList[0]))
800 music.play()
802 shelf['lastPlayed'] = musicList[0]
803 self.advanceMusicList()
805 def toggleMusic(self):
806 music = pygame.mixer.music
807 if self.paused:
808 music.unpause()
809 self.paused = False
810 else:
811 music.pause()
812 self.paused = True
814 def load(self, name, ext="wav"):
815 sound = pygame.mixer.Sound(os.path.join('data', name+'.'+ext))
816 sound.set_volume(0.2)
817 self.sounds[name] = sound
819 def get(self, name):
820 return self.sounds[name]
822 def play(self, name, loops=0, maxtime=0):
823 return self.sounds[name].play(loops, maxtime)
825 def stop(self, name):
826 self.sounds[name].stop()
828 def initMixer():
829 global mixer
830 mixer = Mixer()
832 postGLInits.append(initMixer)
834 # Circle - a simple entity for testing {{{1
835 class Circle(Entity):
836 def __init__(self, radius, **msg):
837 self.radius = radius
839 def setRadius(self, radius, **msg):
840 self.radius = radius
842 # Square - a drawable entity {{{1
843 class Square(Entity):
844 def __init__(self, size, location, color, **msg):
845 self.size = size
846 self.location = location
847 self.color = color
849 scene.add(self, 0)
851 def setSize(self, size, **msg):
852 self.size = size
854 def setLocation(self, location, **msg):
855 self.location = location
857 def setColor(self, color, **msg):
858 self.color = color
860 def draw(self):
861 loc = self.location; size = self.size
863 glColor3fv(self.color)
864 glBegin(GL_QUADS)
865 glVertex3f(loc[0]-size, loc[1]+size, loc[2])
866 glVertex3f(loc[0]+size, loc[1]+size, loc[2])
867 glVertex3f(loc[0]+size, loc[1]-size, loc[2])
868 glVertex3f(loc[0]-size, loc[1]-size, loc[2])
869 glEnd()
871 def doneDrawing(self):
872 return False
874 # Sprite {{{1
875 # Mode for drawing textures - set by init() below.
876 textureFlag = None
878 # Try to avoid gl state changes by remembering what options are enabled
879 boundTexture = None
880 def lazyBindTexture(texture):
881 global boundTexture
882 if texture != boundTexture:
883 glBindTexture(textureFlag, texture)
884 glDisable(textureFlag)
885 glEnable(textureFlag)
886 boundTexture = texture
888 def nextPowerOfTwo(n):
890 while p<n:
891 p *= 2
892 return p
894 def padSprite(imageData, oldWidth, oldHeight, newWidth, newHeight):
895 """Given imageData representing a sprite of a certain dimensions, add pixels
896 consisting of RGBA(0,0,0,0) values to make data for a sprite of new
897 dimensions. The old sprite is in the bottom left corner of the new
898 sprite."""
899 assert oldWidth <= newWidth
900 assert oldHeight <= newHeight
901 assert len(imageData) == 4 * oldWidth * oldHeight
903 newData = []
904 topPad = '\x00' * (4 * newWidth)
905 for i in range(oldHeight):
906 newData.append(imageData[4 * oldWidth * i : 4 * oldWidth * (i+1)])
907 rightPad = '\x00' * (4 * (newWidth-oldWidth))
908 newData.append(rightPad)
909 for i in range(oldHeight, newHeight):
910 newData.append(topPad)
911 return ''.join(newData)
913 class AbstractSprite:
914 def __init__(self, name, numFrames=1, width=None, height=None, columns=1):
915 """Create a sprite from an image file. If the name of the sprite is
916 'gandalf' then the file will be data/gandalf.png. numFrames gives the
917 number of frames of animation of the sprite. width and height give the
918 dimensions of each frame. columns specifies the numbers of columns in
919 which the sprite is layed out in the file, ie. if columns=4 then frames
920 0-3 are in the top row of the image file, frame 4-7 are in the next row,
921 etc."""
922 image = pygame.image.load(os.path.join('data', name+'.png'))
924 if width==None: width = image.get_width()
925 if height==None: height = image.get_height()
927 assert image.get_width() >= width * columns
928 assert image.get_height() >= height * ((numFrames-1)/columns+1)
930 self.name = name
931 self.numFrames = numFrames
932 self.width = width
933 self.height = height
934 self.columns = columns
936 self.texture = glGenTextures(1)
938 self.textureFromImage(image)
940 def draw(self, frame, location, flipped=False):
941 assert 0 <= frame and frame < self.numFrames
942 lazyBindTexture(self.texture)
944 (x,y) = location
945 width = self.width; height = self.height
946 u = (frame % self.columns) * width
947 v = (frame / self.columns) * height
949 if flipped:
950 u0 = u + width; u1 = u
951 else:
952 u0 = u; u1 = u + width
953 v0 = v+height; v1 = v
955 assert glIsEnabled(textureFlag)
957 glBegin(GL_QUADS)
958 self.texCoord(u0, v0)
959 glVertex2f(x, y)
960 self.texCoord(u1, v0)
961 glVertex2f(x+width, y)
962 self.texCoord(u1, v1)
963 glVertex2f(x+width, y+height)
964 self.texCoord(u0, v1)
965 glVertex2f(x, y+height)
966 glEnd()
969 def drawCentered(self, frame, flipped=False):
970 self.draw(frame, (-self.width/2.0, -self.height/2.0), flipped)
972 def drawPartial(self, frame, location, bbox, flipped=False):
973 """This draws part of the sprite as determined by the bounding box,
974 whose coordinates are given in the range [0,1]. For example,
975 bbox=(0,0,1,0.5) will draw the bottom half of the sprite."""
976 assert 0 <= frame and frame < self.numFrames
977 lazyBindTexture(self.texture)
979 (x,y) = location
980 width = self.width; height = self.height
981 u = (frame % self.columns) * width
982 v = (frame / self.columns) * height
984 (l,b,r,t) = bbox
986 xmin = width * l; xmax = width * r
987 ymin = height * b; ymax = height * t
989 if flipped:
990 u0 = u + width - xmin; u1 = u + width - xmax
991 else:
992 u0 = u + xmin; u1 = u + xmax
993 v0 = v+height-ymin; v1 = v+height-ymax
995 assert glIsEnabled(textureFlag)
997 glBegin(GL_QUADS)
998 self.texCoord(u0, v0)
999 glVertex2f(x+xmin, y+ymin)
1000 self.texCoord(u1, v0)
1001 glVertex2f(x+xmax, y+ymin)
1002 self.texCoord(u1, v1)
1003 glVertex2f(x+xmax, y+ymax)
1004 self.texCoord(u0, v1)
1005 glVertex2f(x+xmin, y+ymax)
1006 glEnd()
1009 class SquareSprite(AbstractSprite):
1010 def textureFromImage(self, image):
1011 imageData = pygame.image.tostring(image, "RGBA", False)
1013 self.size = nextPowerOfTwo(max(image.get_width(),image.get_height()))
1015 assert glIsEnabled(textureFlag)
1017 lazyBindTexture(self.texture)
1018 glTexParameteri(textureFlag,
1019 GL_TEXTURE_MAG_FILTER, GL_LINEAR)
1020 glTexParameteri(textureFlag,
1021 GL_TEXTURE_MIN_FILTER, GL_LINEAR)
1022 glTexImage2D(textureFlag,
1024 GL_RGBA,
1025 self.size, self.size,
1027 GL_RGBA, GL_UNSIGNED_BYTE,
1028 padSprite(imageData,
1029 image.get_width(), image.get_height(),
1030 self.size, self.size))
1033 def texCoord(self, u, v):
1034 glTexCoord2d(float(u)/float(self.size), float(v)/float(self.size))
1037 class RectangularSprite(AbstractSprite):
1038 def textureFromImage(self, image):
1039 imageData = pygame.image.tostring(image, "RGBA", False)
1041 assert glIsEnabled(textureFlag)
1043 lazyBindTexture(self.texture)
1044 glTexParameteri(textureFlag,
1045 GL_TEXTURE_MAG_FILTER, GL_LINEAR)
1046 glTexParameteri(textureFlag,
1047 GL_TEXTURE_MIN_FILTER, GL_LINEAR)
1048 glTexImage2D(textureFlag,
1050 GL_RGBA,
1051 image.get_width(), image.get_height(),
1053 GL_RGBA, GL_UNSIGNED_BYTE,
1054 imageData)
1057 def texCoord(self, u, v):
1058 glTexCoord2d(u, v)
1060 spriteClassByMode = { GL_TEXTURE_2D : SquareSprite,
1061 GL_TEXTURE_RECTANGLE_ARB: RectangularSprite }
1063 spriteNames = [
1064 'factory', 'plant', 'munitions', 'gateway', 'mutator',
1065 'gun', 'shield', 'shield_flipped',
1066 'upper', 'upper_shield', 'upper_shield_flipped',
1067 'girder',
1068 'missile', 'virus',
1069 'cloud', 'title' ]
1070 class SpriteFactory:
1071 """This loads all of the sprites in at the start, so that they can be
1072 accessed by name."""
1073 def __init__(self):
1074 self.db = {}
1076 self.spriteClass = spriteClassByMode[textureFlag]
1078 for name in spriteNames:
1079 self.add(name)
1081 self.add('font', 256, 16, 16, 16)
1083 def add(self, name, numFrames=1, width=None, height=None, columns=1):
1084 self.db[name] = \
1085 self.spriteClass(name, numFrames, width, height, columns)
1087 def get(self, name):
1088 return self.db[name]
1090 def initSprites():
1091 global sprites
1092 sprites = SpriteFactory()
1094 postGLInits.append(initSprites)
1096 # Star {{{1
1097 class Star(Entity):
1098 """Simple test class for entities with sprites."""
1099 def __init__(self, location, **msg):
1100 self.location = location
1101 self.sprite = sprites.get('star')
1103 def draw(self):
1104 glColor4f(1,1,1,1)
1105 #self.sprite.draw(0, self.location)
1106 self.sprite.drawPartial(0, self.location, (0,0,1,0.5))
1107 glColor4f(1,1,1,0.5)
1108 self.sprite.drawPartial(0, self.location, (0,0.5,1,1))
1110 def doneDrawing(self):
1111 return False
1113 # Label {{{1
1114 def drawString(str, location, justification='center', size=None):
1115 sprite = sprites.get('font')
1116 if size == None:
1117 size = sprite.width
1118 if hasattr(str, '__iter__'):
1119 for s in str:
1120 drawString(s, location, justification, size)
1121 location = (location[0], location[1] - size)
1122 else:
1123 width = len(str)*size
1124 x,y = location
1125 if justification == 'right':
1126 x -= width
1127 elif justification in ['center', 'centre']:
1128 x -= width/2
1129 else:
1130 assert justification == 'left'
1132 for i in range(len(str)):
1133 frame = ord(str[i])-32
1134 sprite.draw(frame, (x+size*i, y))
1136 class Label:
1137 def __init__(self, text, location, justification='center', size=None):
1138 self.text = text
1139 self.location = location
1140 assert justification in ['left', 'center', 'centre', 'right']
1141 self.justification = justification
1142 self.size = size
1143 self.done = False
1144 self.color = (1,1,1,0.5)
1146 self.show()
1148 def show(self):
1149 scene.add(self, LABEL_LAYER)
1150 self.shown = True
1151 def hide(self):
1152 scene.remove(self)
1153 self.shown = False
1154 def toggle(self):
1155 if self.shown:
1156 self.hide()
1157 else:
1158 self.show()
1160 def draw(self):
1161 glColor4fv(self.color)
1162 drawString(self.text, self.location, self.justification, self.size)
1164 def doneDrawing(self):
1165 return self.done
1167 messageBar = Label([],
1168 (width/2, height-32), 'center')
1170 clock = pygame.time.Clock()
1171 fpsBox = Label('', (width-10,height-26), 'right')
1172 fpsBox.hide()
1173 def showFPS():
1174 fpsBox.text = ["FPS:%3.0f" % (clock.get_fps()),
1175 "Objects:%4d" % len(scene.objs)]
1176 if 'right' in towers:
1177 girder = towers['right'].weakestGirder()
1178 if girder:
1179 fpsBox.text.append("Protection:%4d" % girder.protection())
1181 def toggleFPS():
1182 fpsBox.toggle()
1184 # Button {{{1
1185 class Widget(object):
1186 def show(self):
1187 scene.add(self, LABEL_LAYER)
1188 clickables.add(self, self.bbox)
1190 def hide(self):
1191 scene.remove(self)
1192 clickables.remove(self)
1194 class Button(Widget):
1195 def __init__(self, text, location, onClick):
1196 """A text button with onClick as a callback function."""
1197 self.font = sprites.get('font')
1198 self.border = 4
1200 self.location = location
1201 self.clickNoise = True
1202 self.onClick = onClick
1203 self.text = text
1204 self.resize()
1206 scene.add(self, LABEL_LAYER)
1208 def resize(self):
1209 x,y = self.location; border = self.border
1210 width = len(self.text)*self.font.width
1211 height = self.font.height
1213 self.bbox = (x - width/2 - border, y - border,
1214 x + width/2 + border, y + height + border)
1216 clickables.add(self, self.bbox)
1218 def draw(self):
1219 if clickables.selected == self:
1220 # Clicked colors
1221 fgColor = (0.9, 0.9, 1.0, 0.5)
1222 bgColor = (0.0, 0.0, 0.0, 0.6)
1223 elif clickables.focus == self:
1224 # Mouse over colors
1225 fgColor = (1.0, 1.0, 1.0, 0.7)
1226 bgColor = (0.0, 0.0, 0.0, 0.5)
1227 else:
1228 # Normal colors
1229 fgColor = (1.0, 1.0, 0.9, 0.5)
1230 bgColor = (0.0, 0.0, 0.0, 0.2)
1232 drawRoundedRectangle(self.bbox, bgColor)
1234 glColor4fv(fgColor)
1235 drawString(self.text, self.location, 'center')
1237 def doneDrawing(self):
1238 return False
1240 def click(self, location):
1241 pass
1242 def hold(self, location, duration):
1243 pass
1244 def release(self, location):
1245 self.onClick()
1246 return True
1248 def hit(self, damage):
1249 pass
1251 #def mom():
1252 # print "hi mom!"
1254 #def initButton():
1255 # global button
1256 # button = Button("Say Hi", (width/2, 100), mom)
1258 #postGLInits.append(initButton)
1260 # TextBox {{{1
1261 class TextBox(Widget):
1262 def __init__(self, location, capacity, text='', onEnter=None):
1263 """A text button with onClick as a callback function."""
1264 self.font = sprites.get('font')
1265 self.border = 4
1267 self.location = location
1268 self.capacity = capacity
1269 self.text = text
1270 self.onEnter = onEnter
1272 scene.add(self, LABEL_LAYER)
1274 x,y = self.location; border = self.border
1275 width = self.capacity*self.font.width
1276 height = self.font.height
1278 self.bbox = (x - width/2 - border, y - border,
1279 x + width/2 + border, y + height + border)
1281 self.clickNoise = True
1282 clickables.add(self, self.bbox)
1284 def drawCarat(self, color):
1285 if pygame.time.get_ticks() / 500 % 2 == 0:
1286 x,y = self.location
1287 width = len(self.text)*self.font.width
1288 height = self.font.height
1290 drawRectangle((x + width/2, y, x + width/2 + 2, y + height), color)
1292 def drawBackground(self, bgColor):
1293 r,g,b,a = bgColor
1294 x,y = self.location
1295 width = len(self.text)*self.font.width
1296 height = self.font.height
1297 padding = 2*self.font.width
1298 border = self.border
1300 with NoTexture():
1301 glBegin(GL_QUAD_STRIP)
1302 glColor4f(r,g,b,0)
1303 glVertex2f(x - width/2 - padding, y - border)
1304 glVertex2f(x - width/2 - padding, y + height + border)
1305 glColor4f(r,g,b,a)
1306 glVertex2f(x - width/2, y - border)
1307 glVertex2f(x - width/2, y + height + border)
1308 glVertex2f(x + width/2, y - border)
1309 glVertex2f(x + width/2, y + height + border)
1310 glColor4f(r,g,b,0)
1311 glVertex2f(x + width/2 + padding, y - border)
1312 glVertex2f(x + width/2 + padding, y + height + border)
1313 glEnd()
1315 def draw(self):
1316 if clickables.selected == self:
1317 # Clicked colors
1318 fgColor = (1.0, 1.0, 1.0, 0.5)
1319 bgColor = (0.0, 0.0, 0.0, 0.6)
1320 elif clickables.focus == self:
1321 # Mouse over colors
1322 fgColor = (1.0, 1.0, 1.0, 0.7)
1323 bgColor = (0.0, 0.0, 0.0, 0.5)
1324 else:
1325 # Normal colors
1326 fgColor = (1.0, 1.0, 1.0, 0.5)
1327 bgColor = (0.0, 0.0, 0.0, 0.2)
1329 #drawRectangle(self.bbox, bgColor)
1330 self.drawBackground(bgColor)
1332 glColor4fv(fgColor)
1333 drawString(self.text, self.location, 'center')
1335 if clickables.selected == self:
1336 self.drawCarat(fgColor)
1338 def doneDrawing(self):
1339 return False
1341 def unselect(self):
1342 clickables.unselect()
1343 pygame.key.set_repeat()
1345 def click(self, location):
1346 if locationInBBox(location, self.bbox):
1347 pygame.key.set_repeat(500, 50)
1348 else:
1349 self.unselect()
1351 def hold(self, location, duration):
1352 pass
1353 def release(self, location):
1354 return False
1355 def key(self, unicode, key, mod):
1356 if key == K_RETURN:
1357 self.unselect()
1358 if self.onEnter:
1359 self.onEnter(self.text)
1360 return True
1361 elif key in [K_BACKSPACE, K_DELETE]:
1362 if len(self.text) > 0:
1363 self.text = self.text[:-1]
1364 return True
1365 else:
1366 if len(self.text) < self.capacity:
1367 self.text += unicode
1368 return True
1370 def hit(self, damage):
1371 self.text = "Don't hit me!"
1373 #def entered(text):
1374 # print "entered:", text
1375 #def initTextBox():
1376 # global boxers
1377 # boxers = TextBox((width/2, 150), 40, 'TextBoxers', entered)
1378 #postGLInits.append(initTextBox)
1380 # Title {{{1
1381 class Title:
1382 def __init__(self, location, color):
1383 self.location = location
1384 self.color = color
1385 self.sprite = sprites.get('title')
1387 self.show()
1389 def show(self):
1390 scene.add(self, LABEL_LAYER)
1391 def hide(self):
1392 scene.remove(self)
1394 def draw(self):
1395 glColor4fv(self.color)
1396 with PushedMatrix():
1397 glTranslatef(self.location[0], self.location[1], 0.0)
1398 self.sprite.drawCentered(0)
1400 def doneDrawing(self):
1401 return False
1403 def initTitle():
1404 global title
1405 title = Title((width/2, height - 70), (1,1,1,0.6))
1406 postGLInits.append(initTitle)
1408 # Backdrop {{{1
1409 class Backdrop:
1410 def draw(self):
1411 if updater.paused:
1412 top = (0.22, 0.24, 0.38); bottom = (0.27, 0.40, 0.51)
1413 elif victor:
1414 top = (0.75, 0.13, 0.01); bottom = (0.98, 0.51 , 0.19)
1415 else:
1416 #top = (0.02, 0.09, 0.56); bottom = (0.0, 0.42, 0.77)
1417 #top = (0.10, 0.20, 0.66); bottom = (0.0, 0.42, 0.77)
1418 top = (0.05, 0.15, 0.66); bottom = (0.0, 0.42, 0.77)
1419 with NoTexture():
1420 glBegin(GL_QUADS)
1421 glColor3fv(bottom)
1422 glVertex2i(0,0); glVertex2i(width,0)
1423 glColor3fv(top)
1424 glVertex2i(width,height); glVertex2i(0, height)
1425 glEnd()
1427 def doneDrawing(self):
1428 return False
1430 scene.add(Backdrop(), BACKDROP_LAYER)
1432 # Cube {{{1
1433 class Cube:
1434 """Test for the perspective code."""
1435 def draw(self):
1436 with Perspective((width/2, height/2)):
1437 with NoTexture():
1438 glColor3f(1,1,1)
1439 with glGroup(GL_LINE_LOOP):
1440 glVertex3f(width/3,height/3,2)
1441 glVertex3f(2*width/3,height/3,2)
1442 glVertex3f(2*width/3,height/3,4)
1443 glVertex3f(width/3,height/3,4)
1445 def doneDrawing(self):
1446 return False
1448 #scene.add(Cube(), -50)
1449 # TargetManager {{{1
1450 class TargetManager:
1451 """This translates action of the mouse to selections of object in the level.
1452 Each entity has to register itself with a bounding box (left, bottom, right,
1453 top) to the target manager. Then, when the bounding box is clicked, the
1454 click method of the entity is called."""
1455 def __init__(self):
1456 self.objs = {}
1457 self.focus = None
1458 self.selected = None
1459 self.pressed = False
1460 self.lastTicks = None
1462 def add(self, obj, bbox):
1463 self.objs[obj] = bbox
1465 def remove(self, obj):
1466 if self.selected == obj:
1467 self.unselect()
1468 if self.focus == obj:
1469 self.focus = None
1470 if obj in self.objs:
1471 del self.objs[obj]
1473 def minimalBBoxes(self, pairs):
1474 if len(pairs) <= 1:
1475 return pairs
1477 minimal = []
1479 def isMinimal(pair):
1480 for m in minimal:
1481 if bboxInBBox(m[1], pair[1]):
1482 return False
1483 return True
1484 def addMinimal(pair):
1485 for i in range(len(minimal)):
1486 if bboxInBBox(pair[1], minimal[i][1]):
1487 del minimal[i]
1488 minimal.append(pair)
1490 for pair in pairs:
1491 if isMinimal(pair):
1492 addMinimal(pair)
1494 return minimal
1496 def closestObject(self, location, pairs):
1497 if len(pairs) == 0:
1498 return None
1499 if len(pairs) == 1:
1500 return pairs[0][0]
1502 def dist(location, pair):
1503 return dist2(location, bboxCenter(pair[1]))
1505 best = pairs[0]; bestDist = dist(location, pairs[0])
1506 for pair in pairs[1:]:
1507 distance = dist(location, pair)
1508 if distance < bestDist:
1509 best = pair; bestDist = distance
1510 return best[0]
1512 def objectAtLocation(self, location):
1513 candidates = [pair for pair in self.objs.items() \
1514 if locationInBBox(location, pair[1]) ]
1515 mins = self.minimalBBoxes(candidates)
1516 winner = self.closestObject(location, mins)
1517 return winner
1519 def updateMessageBar(self):
1520 if self.focus and hasattr(self.focus, 'desc'):
1521 messageBar.text = [self.focus.__class__.__name__, self.focus.desc()]
1522 else:
1523 messageBar.text = []
1525 def focusOnLocation(self, location):
1526 oldFocus = self.focus
1527 self.focus = self.objectAtLocation(location)
1528 self.updateMessageBar()
1530 if self.focus != None and self.focus != oldFocus and \
1531 hasattr(self.focus, 'clickNoise'):
1532 mixer.play('click')
1534 def unselect(self):
1535 self.focus = None
1536 self.selected = None
1538 def click(self, location, ticks):
1539 assert not self.pressed
1540 self.pressed = True
1541 self.lastTicks = ticks
1543 if not updater.paused:
1544 if self.selected:
1545 self.selected.click(location)
1546 else:
1547 self.focusOnLocation(location)
1548 if self.focus:
1549 self.selected = self.focus
1550 self.selected.click(location)
1552 def drag(self, location, ticks, minDuration = 50):
1553 if not updater.paused:
1554 duration = ticks - self.lastTicks
1555 if self.selected and duration >= minDuration:
1556 # Give the duration of the hold to the object.
1557 self.selected.hold(location, duration)
1558 self.lastTicks = ticks
1560 def move(self, location, ticks):
1561 if self.pressed:
1562 self.drag(location, ticks)
1563 elif self.selected == None:
1564 self.focusOnLocation(location)
1566 def release(self, location, ticks):
1567 """Tell the selected object that the mouse is released. If the object's
1568 release method returns true, it means that the object is done being
1569 selected. Otherwise, it will continue to receive new clicks."""
1570 assert self.pressed
1571 self.pressed = False
1573 # First finish the holding
1574 self.drag(location, ticks, minDuration = 0)
1576 if self.selected:
1577 if self.selected.release(location):
1578 self.selected = None
1579 self.focusOnLocation(location)
1582 def key(self, unicode, key, mod):
1583 """Receive a key event."""
1584 if self.selected and hasattr(self.selected, 'key'):
1585 return self.selected.key(unicode, key, mod)
1586 else:
1587 return False
1589 def update(self, ticks, duration):
1590 if self.pressed:
1591 self.drag(mousePos(), ticks)
1593 def hit(self, location, radius, damage):
1594 for obj, bbox in self.objs.copy().iteritems():
1595 dist = distToBBox(location, bbox)
1596 if dist < radius:
1597 factor = (1.0 - float(dist) / float(radius))**2
1598 #print "hit", obj.model, "for damage", factor*damage
1599 obj.hit(factor*damage)
1601 clickables = TargetManager()
1602 updater.add(clickables, 0)
1604 targets = TargetManager()
1605 #updater.add(targets, 0)
1608 # Victor {{{1
1609 victor = None
1611 class VictoryMessage(Entity):
1612 """Create this entity to send out news that one of the sides has one."""
1613 def __init__(self, side, **msg):
1614 global victor
1615 if victor == None:
1616 victor = side
1617 Label('The '+side+' is victorious!', (width/2, height/2), 'center')
1619 # Building {{{1
1620 class Building(Entity):
1621 """This is the base class for a building, an object that is built by holding
1622 the mouse down on its blueprint, and once built can be used for various
1623 purposes."""
1625 def __init__(self, name, model, side, bbox, **msg):
1626 self.name = name
1627 self.model = model
1628 self.side = side
1629 self.bbox = bbox
1631 self.state = 'blueprint'
1632 self.stats = buildingStats[model]
1634 self.hp = 0
1635 if 'location' not in self.__dict__:
1636 self.location = (bbox[0], bbox[1])
1638 self.buildPressed = False
1639 self.rubbleTime = None
1641 self.alternate = None
1642 self.show()
1644 self.persistentSoundChannel = None
1646 def desc(self):
1647 if 'desc' in self.stats:
1648 return self.stats['desc']
1649 else:
1650 return ''
1652 def mouseOver(self):
1653 return clickables.focus == self
1655 def active(self):
1656 """Returns True if the building is finished construction and can be
1657 used."""
1658 return self.state == 'built'
1660 def color(self):
1661 if self.state == 'blueprint':
1662 if self.mouseOver():
1663 return buildingColor
1664 elif self.playing():
1665 return blueprintColor
1666 else:
1667 return None
1668 elif self.state in ['rubble', 'floating']:
1669 t = updater.get_ticks() - self.rubbleTime
1670 if t < rubbleDuration:
1671 if self.state == 'rubble' and self.playing():
1672 endColor = blueprintColor
1673 else:
1674 endColor = (0,0,0,0)
1675 return interp(float(t)/float(rubbleDuration),
1676 rubbleColor, endColor)
1677 elif self.state == 'rubble' and self.playing():
1678 return blueprintColor
1679 else:
1680 # Building no longer exists, don't draw.
1681 return None
1682 elif self.state == 'building':
1683 return None
1684 else:
1685 if self.state != 'built':
1686 print 'state:', self.state
1687 assert self.state == 'built'
1688 return interp(float(self.hp)/float(self.maxhp()),
1689 damagedColor, builtColor)
1691 def playPersistentSound(self, name, loops=0, maxtime=0):
1692 """Play a sound that runs continuously, such as the construction sound,
1693 or shield charging. This routine ensures that such sounds don't
1694 continue after the building is destroyed."""
1695 self.stopPersistentSound()
1696 self.persistentSoundChannel = mixer.play(name, loops=-1)
1698 def stopPersistentSound(self):
1699 if self.persistentSoundChannel:
1700 self.persistentSoundChannel.stop()
1701 self.persistentSoundChannel = None
1703 def maxhp(self):
1704 return self.stats['hp']
1706 def percentComplete(self):
1707 return float(self.hp) / float(self.maxhp())
1709 def complete(self, sound='built', **msg):
1710 """Put into a completed state."""
1711 self.hp = self.maxhp()
1712 self.state = 'built'
1713 if sound:
1714 mixer.play(sound)
1716 if self.local():
1717 if 'towers' in globals():
1718 self.tower().audit()
1719 if self.alternate:
1720 self.alternate.broadcast('hide')
1723 def hit(self, damage):
1724 newhp = max(self.hp - damage, 0.0)
1725 if newhp == 0.0 and self.state == 'built':
1726 self.broadcast('destroy')
1727 else:
1728 self.broadcast('damage', newhp = newhp)
1730 def infect(self, level):
1731 if self.state == 'built':
1732 if self.tower().infect(level):
1733 self.broadcast('destroy')
1735 def destroy(self, **msg):
1736 self.hp = 0
1737 self.state = 'rubble'
1738 self.rubbleTime = updater.get_ticks()
1740 # Stop any sound (shield charging, gun loading, etc.) that was playing
1741 # when the building was destroyed.
1742 self.stopPersistentSound()
1744 mixer.play(random.choice(destroyList))
1746 if self.local():
1747 if 'towers' in globals() and self.local():
1748 self.tower().audit()
1749 if self.alternate:
1750 self.alternate.show()
1752 def float(self, **msg):
1753 """The level supporting this building has been destroyed. Draw a death
1754 sequence for the building, then eliminate it."""
1755 if self.active():
1756 self.hp = 0
1757 self.state = 'floating'
1758 self.rubbleTime = updater.get_ticks()
1759 else:
1760 self.state = 'blueprint'
1761 scene.remove(self)
1763 if self.local():
1764 updater.remove(self)
1765 targets.remove(self)
1766 if self.playing():
1767 clickables.remove(self)
1769 def hide(self, **msg):
1770 assert not self.active()
1771 self.shown = False
1772 scene.remove(self)
1773 if self.local():
1774 updater.remove(self)
1775 targets.remove(self)
1776 if self.playing():
1777 clickables.remove(self)
1779 def show(self, **msg):
1780 assert not self.active()
1781 if self.state != 'floating':
1782 self.shown = True
1783 scene.add(self, BUILDING_LAYER)
1784 if self.local():
1785 updater.add(self, 0)
1786 targets.add(self, self.bbox)
1787 if self.playing():
1788 clickables.add(self, self.bbox)
1790 def done(self):
1791 """Floating buildings no longer exist after their animation finishes."""
1792 if self.state == 'floating':
1793 return updater.get_ticks() > self.rubbleTime + rubbleDuration
1794 else:
1795 return False
1797 def damage(self, newhp, **msg):
1798 self.hp = newhp
1800 def activeUpdate(self, ticks, duration):
1801 pass
1802 def activeClick(self, location):
1803 pass
1804 def activeHold(self, location, duration):
1805 pass
1806 def activeRelease(self, location):
1807 return True
1809 def update(self, ticks, duration):
1810 # Neglected buildings under construction slowly decline.
1811 if self.active():
1812 self.activeUpdate(ticks, duration)
1813 elif self.state == 'building' and not self.buildPressed:
1814 maxhp = self.stats['hp']
1815 self.hp -= disrepairFactor * float(maxhp) * float(duration) / \
1816 float(self.stats['buildtime'])
1817 if self.hp < 0:
1818 self.hp = 0
1819 self.state = 'blueprint'
1820 elif self.state in 'rubble':
1821 t = updater.get_ticks() - self.rubbleTime
1822 if t > rubbleDuration:
1823 self.state = 'blueprint'
1824 self.deathTime = None
1825 elif self.done():
1826 updater.remove(self)
1827 targets.remove(self)
1828 clickables.remove(self)
1830 def click(self, location):
1831 if self.active():
1832 self.activeClick(location)
1833 else:
1834 if self.state == 'blueprint':
1835 self.state = 'building'
1837 if not self.buildPressed:
1838 if self.playing():
1839 self.playPersistentSound('building')
1840 self.buildPressed = True
1842 def hold(self, location, duration):
1843 if not self.buildPressed and self.active():
1844 self.activeHold(location, duration)
1845 elif self.state == 'building':
1846 maxhp = self.stats['hp']
1847 self.hp += self.tower().factoryBoost() * \
1848 self.tower().handicap * \
1849 float(maxhp) * float(duration) / \
1850 float(self.stats['buildtime'])
1851 if self.hp >= maxhp:
1852 if self.playing():
1853 self.stopPersistentSound()
1854 self.broadcast('complete')
1856 def release(self, location):
1857 if not self.buildPressed and self.active():
1858 return self.activeRelease(location)
1859 else:
1860 self.buildPressed = False
1861 if self.playing():
1862 self.stopPersistentSound()
1863 return True
1865 def doneDrawing(self):
1866 return self.done()
1869 # SpriteBuilding {{{1
1870 class SpriteBuilding(Building):
1871 def __init__(self, name, model, side, location, **msg):
1872 self.sprite = sprites.get(model)
1874 x,y = location
1875 bbox = (x, y, x+self.sprite.width, y+self.sprite.height)
1877 Building.__init__(self, name, model, side, bbox)
1879 def destroy(self, **msg):
1880 createDebris(self.bbox)
1881 Building.destroy(self, **msg)
1884 def draw(self):
1885 if self.state == 'building':
1886 percent = self.percentComplete()
1887 glColor4fv(builtColor)
1888 self.sprite.drawPartial(0, self.location, (0,0,1,percent))
1889 glColor4fv(buildingColor)
1890 self.sprite.drawPartial(0, self.location, (0,percent,1,1))
1891 else:
1892 color = self.color()
1893 if color != None:
1894 glColor4fv(color)
1895 self.sprite.draw(0, self.location)
1897 def buildingWidth(building):
1898 building = nameof(building).lower()
1899 return sprites.get(building).width
1901 # Some self-indulgent metaprogramming.
1902 def addBuildingModel(model):
1903 # Capitalize name.
1904 model = model.lower()
1905 Model = model[0].upper() + model[1:]
1906 class XBuilding(SpriteBuilding):
1907 def __init__(self, name, side, location, **msg):
1908 SpriteBuilding.__init__(self, name, model, side, location, **msg)
1909 XBuilding.__name__ = Model
1910 globals()[Model] = XBuilding
1912 for model in ['factory', 'munitions']:
1913 addBuildingModel(model)
1915 # Plant {{{1
1916 class Plant(SpriteBuilding):
1917 def __init__(self, name, side, location, **msg):
1918 SpriteBuilding.__init__(self, name, 'plant', side, location, **msg)
1920 def complete(self, **msg):
1921 SpriteBuilding.complete(self, **msg)
1922 if self.local():
1923 self.tower().reshield()
1924 def destroy(self, **msg):
1925 SpriteBuilding.destroy(self, **msg)
1926 if self.local():
1927 self.tower().reshield()
1928 def float(self, **msg):
1929 SpriteBuilding.float(self, **msg)
1930 if self.local():
1931 self.tower().reshield()
1933 # Missile {{{1
1935 def radians(angle): return angle * math.pi / 180.0
1936 def degrees(angle): return angle / math.pi * 180.0
1938 def maxSpeedFromRange(range):
1939 """Determine the minimum velocity needed just to reach the target."""
1940 dx, dy = range
1942 if dx == 0.0:
1943 assert dy > 0.0
1944 return sqrt(2.0 * gravity * dy)
1946 l = length(range)
1948 # Through the magic of Lagrange multipliers, we figure out the slope has to
1949 # be:
1950 # slope = (l + dy) / dx
1951 # Then using the vxAtAngle formulae:
1952 v2 = 0.5 * gravity * (dx**2 + (l + dy)**2) / l
1953 assert v2 > 0.0
1955 return sqrt(v2)
1957 def vxAtAngle(dx, dy, angle):
1958 """Calculate vx, the horizontal component of the velocity, if the missile is
1959 restricted to fire at the given angle. Returns None if it is impossible at
1960 that angle."""
1961 if dx == 0.0:
1962 return None
1963 denom = tan(radians(angle)) * abs(dx) - dy
1964 if denom <= 0.0:
1965 return None
1967 vx2 = 0.5 * (gravity * dx**2) / denom
1968 assert vx2 > 0.0
1970 if dx >= 0.0:
1971 return sqrt(vx2)
1972 else:
1973 return -sqrt(vx2)
1975 def trajectoryTimes(dx, dy, speed):
1976 a = gravity**2 / 4.0
1977 b = dy * gravity - speed**2
1978 c = dx**2 + dy**2
1980 solns = solveQuadratic(a,b,c)
1981 return [sqrt(soln) for soln in solns if soln > 0.0]
1983 def trajectoryVelocity(dx, dy, speed, T):
1984 assert T > 0
1985 vx = dx/T
1986 vy = sqrt(speed**2 - vx**2)
1987 return (vx, vy)
1989 def velocityTimeOfMissile(source, target, maxspeed, minAngle):
1990 dx = target[0] - source[0]
1991 dy = target[1] - source[1]
1993 # First try to fire at the minimum angle.
1994 vx = vxAtAngle(dx, dy, minAngle)
1995 if vx != None:
1996 v = (vx, abs(vx) * tan(radians(minAngle)))
1997 if length(v) <= maxspeed:
1998 return v, dx/vx
2000 times = trajectoryTimes(dx, dy, maxspeed)
2001 for T in times:
2002 v = trajectoryVelocity(dx, dy, maxspeed, T)
2003 angle = atan2(v[1], abs(v[0]))
2004 if degrees(angle) >= minAngle:
2005 return v, T
2006 return None, None
2008 def dudVelocity(source, target, speed):
2009 dx = target[0] - source[0]
2010 dy = target[1] - source[1]
2012 rise = dy + length((dx, dy)); run = dx; hyp = length((rise,run))
2013 return (run/hyp * speed, rise/hyp * speed)
2015 def dudTime(source, target, velocity):
2016 dx = target[0] - source[0]
2017 dy = target[1] - source[1]
2018 vx, vy = velocity
2020 if vx == 0.0:
2021 return max(vy / gravity, 0.0)
2023 # Calculate the time for the missile to be directly under the target. Then
2024 # use a linear approximation to the trajectory at that point, and move to
2025 # the closest point on that line.
2026 t = dx / vx
2028 y = - 0.5 * gravity * t**2 + vy * t
2029 vyt = - gravity * t + vy
2030 assert y < dy
2031 y_error = dy - y
2033 x_error = y_error / (vyt/vx + vx/vyt)
2035 return t + x_error / vx
2038 class Missile(Entity):
2039 def __init__(self, side, gun, source, target, stats, **msg):
2040 """Launch a missile from the source location to the target location.
2041 The stats field gives yield, """
2042 self.side = side
2043 self.sprite = sprites.get('missile')
2045 self.gun = gun
2046 self.source = source; self.target = target
2047 self.stats = stats
2049 maxSpeed = maxSpeedFromRange(stats['range'])
2050 minAngle = stats['angle']
2051 v, t = velocityTimeOfMissile(source, target, maxSpeed, minAngle)
2052 if v == None:
2053 v = dudVelocity(source, target, maxSpeed)
2054 t = dudTime(source, target, v)
2056 acc = stats['accuracy']
2057 self.startVelocity = (v[0] * random.gauss(1.0, acc),
2058 v[1] * random.gauss(1.0, acc))
2059 self.time = t
2061 # print "maxspeed:", maxspeed, "speed:", length(self.startVelocity)
2062 # print "startAngle:", \
2063 # atan2(self.startVelocity[1], self.startVelocity[0]) * 180.0 / \
2064 # math.pi
2066 self.startTicks = updater.get_ticks()
2067 self.exploded = False
2069 # Add a contrail (stream of cloud in a line), a flame off of the back of
2070 # the missile, and a cloud of smoke on the launch pad.
2071 if graphicsLevel() > 1:
2072 Contrail(self)
2073 createPlume((self.source[0], self.source[1]-self.sprite.height))
2074 Flame(self)
2076 scene.add(self, MISSILE_LAYER)
2077 if self.local():
2078 updater.add(self, 0)
2079 entities[gun].broadcast('fire')
2081 mixer.play('missile')
2083 def position(self, t):
2084 x = self.source[0] + self.startVelocity[0] * t
2085 y = self.source[1] + self.startVelocity[1] * t - 0.5 * gravity * t**2
2086 return (x,y)
2088 def velocity(self, t):
2089 vx = self.startVelocity[0]
2090 vy = self.startVelocity[1] - gravity * t
2091 return (vx, vy)
2093 def angle(self, t):
2094 vx, vy = self.velocity(t)
2095 return atan2(vy, vx) * 180.0 / math.pi
2097 def explode(self, time, **msg):
2098 self.time = time
2099 position = self.position(time)
2100 vx, vy = self.velocity(time)
2101 mixer.play(random.choice(explodeList))
2102 for i in range(graphicsDependent(10, 35, 70)):
2103 Cloud('fire', position,
2104 (random.gauss(0.05*vx, 0.02), random.gauss(0.05*vy+lift,0.02)))
2105 for i in range(graphicsDependent(5, 10, 15)):
2106 Cloud('smoke', position,
2107 (random.gauss(0.05*vx, 0.02), random.gauss(0.05*vy+lift,0.02)))
2109 def detonate(self, time):
2110 self.broadcast('explode', time = time)
2111 self.exploded = True
2113 # Cause damage.
2114 targets.hit(self.position(time),
2115 self.stats['radius'],
2116 self.stats['damage'])
2118 def detonateOnShield(self, shield, time):
2119 p = self.position(time); v = self.velocity(time)
2120 q = shield.center(); r = shield.radius()
2122 disp = (p[0] - q[0], p[1] - q[1])
2124 # Use a linear approximation of the time, and solve for the time when it
2125 # first hits the shield.
2126 solns = solveQuadratic(length2(v),
2127 2 * dot(disp,v),
2128 length2(disp) - r**2)
2129 if solns == []:
2130 t = time
2131 else:
2132 t = solns[0]
2134 self.detonate(time + solns[0])
2135 shield.absorb(self.stats['damage'])
2137 def update(self, ticks, duration):
2138 if not self.exploded:
2139 t = ticks - self.startTicks
2140 shield = self.tower().shieldingAtLocation(self.position(t))
2141 if shield:
2142 self.detonateOnShield(shield, t)
2143 elif t >= self.time:
2144 self.detonate(self.time)
2145 self.tower().hitShield(self.position(self.time),
2146 self.stats['radius'],
2147 self.stats['damage'])
2149 def draw(self):
2150 # TODO: send ticks with draw command
2151 t = updater.get_ticks() - self.startTicks
2153 if t < self.time:
2154 with PushedMatrix():
2155 x,y = self.position(t)
2156 glTranslatef(x, y, 0)
2157 glRotatef(self.angle(t)-90.0, 0, 0, 1)
2158 scale = self.stats['scale']
2159 glScalef(scale, scale, scale)
2160 glColor4fv(missileColor)
2161 self.sprite.draw(0,
2162 (-self.sprite.width/2,-self.sprite.height/2))
2163 #glPopMatrix()
2165 def doneDrawing(self):
2166 t = updater.get_ticks() - self.startTicks
2167 return t > self.time
2169 def vertices(self, t, width, offset=(0,0)):
2170 """Compute three vertices centered at the point the missile is at time t
2171 and in a line perpendicular to the missile with a width of width."""
2172 x,y = self.position(t)
2173 vx,vy = unit(self.velocity(t))
2174 w = float(width) / 2.0
2176 # Rotate 90 degrees.
2177 ux = vy; uy = -vx
2179 x += offset[0]; y += offset[1]
2181 return [(x-w*ux,y-w*uy), (x,y), (x+w*ux, y+w*uy)]
2183 def drawFlame(self):
2184 t0 = updater.get_ticks() - self.startTicks
2186 segments = 10; flameTime = 200
2187 flameColor = (1.0, 0.5, 0.0, 0.75)
2188 flameWidth = 8
2190 if t0 < self.time:
2191 glDisable(textureFlag)
2192 glColor4fv(flameColor)
2193 glBegin(GL_QUAD_STRIP)
2194 for i in range(segments):
2195 r = float(i) / float(segments)
2196 t = t0 - flameTime * r
2197 verts = self.vertices(t, (1.0 - r**2)*flameWidth,
2198 (0,lift*(t0-t)))
2200 glVertex2fv(verts[0])
2201 glVertex2fv(verts[2])
2202 glEnd()
2203 glEnable(textureFlag)
2206 def drawContrail(self):
2207 T = updater.get_ticks() - self.startTicks
2208 t0 = min(T, self.time)
2210 duration = 10000
2211 if T - t0 > duration:
2212 return
2214 segments = graphicsDependent(10, 17, 20)
2215 startAlpha = 0.1
2216 endAlpha = 0.0
2218 startWidth = 12
2219 endWidth = 80
2221 with NoTexture():
2222 glBegin(GL_QUAD_STRIP)
2223 for i in range(segments):
2224 r = float(i) / float(segments-1)
2226 t = t0 * r
2228 # c=0 corresponds to a fresh part of the contrail
2229 # c=1 corresponds to a part at the end of its life
2230 c = min(float(T - t) / float(duration), 1.0)
2232 verts = self.vertices(t, interp(c, startWidth, endWidth),
2233 (0, lift*(T-t)))
2235 if i == 0 or i == segments - 1:
2236 midAlpha = 0.0
2237 else:
2238 midAlpha = interp(c, startAlpha, endAlpha)
2240 glColor4f(1,1,1,0)
2241 glVertex2fv(verts[0])
2242 glColor4f(1,1,1, midAlpha)
2243 glVertex2fv(verts[1])
2244 glEnd()
2245 glBegin(GL_QUAD_STRIP)
2246 for i in range(segments):
2247 r = float(i) / float(segments-1)
2248 t = t0 * r
2249 c = min(float(T - t) / float(duration), 1.0)
2251 verts = self.vertices(t, interp(c, startWidth, endWidth),
2252 (0, lift*(T-t)))
2254 if i == 0 or i == segments - 1:
2255 midAlpha = 0.0
2256 else:
2257 midAlpha = interp(c, startAlpha, endAlpha)
2259 glColor4f(1,1,1,midAlpha)
2260 glVertex2fv(verts[1])
2261 glColor4f(1,1,1,0)
2262 glVertex2fv(verts[2])
2263 glEnd()
2265 def doneDrawingContrail(self):
2266 T = updater.get_ticks() - self.startTicks
2267 t0 = min(T, self.time)
2269 duration = 10000
2270 return T - t0 > duration
2274 class Contrail():
2275 def __init__(self, missile):
2276 self.missile = missile
2277 scene.add(self, CONTRAIL_LAYER)
2279 def draw(self):
2280 self.missile.drawContrail()
2282 def doneDrawing(self):
2283 return self.missile.doneDrawingContrail()
2285 class Flame():
2286 def __init__(self, missile):
2287 self.missile = missile
2288 scene.add(self, FLAME_LAYER)
2290 def draw(self):
2291 self.missile.drawFlame()
2293 def doneDrawing(self):
2294 return self.missile.doneDrawing()
2297 # Cloud {{{1
2299 clouds = { 'smoke': { 'duration': [2500, 6000, 8000], 'offset': 500,
2300 'startScale': 1.0, 'endScale': 5.0,
2301 'color': (1,1,1, 0.2),
2302 'colorVariance': (0.1, 0.1, 0.1, 0.1) },
2303 'fire': { 'duration': [1000, 3000, 4000], 'offset': 100,
2304 'startScale': 0.5, 'endScale': 3.0,
2305 'color': (1.0, 0.5, 0.0, 0.6),
2306 'colorVariance': (0.1, 0.1, 0.1, 0.1) },
2307 'dust': { 'duration': [2000, 4000, 4000], 'offset': 1000,
2308 'startScale': 2.0, 'endScale': 10.0,
2309 'color': (0.8, 0.8, 0.8, 0.4),
2310 'colorVariance': (0.0, 0.0, 0.0, 0.1) }}
2311 class Cloud:
2312 def __init__(self, model, position, velocity):
2313 self.stats = clouds[model]
2314 self.position = position
2315 self.velocity = velocity
2316 self.angle = random.uniform(-math.pi,math.pi)
2317 self.startTicks = updater.get_ticks()
2318 self.sprite = sprites.get('cloud')
2320 r,g,b,a = self.stats['color']
2321 rv,gv,bv,av = self.stats['colorVariance']
2322 self.color = (random.gauss(r,rv),
2323 random.gauss(g,gv),
2324 random.gauss(b,bv),
2325 random.gauss(a,av))
2327 scene.add(self, CLOUD_LAYER)
2329 def draw(self):
2330 t = updater.get_ticks() - self.startTicks + self.stats['offset']
2331 duration = graphicsDependent(*self.stats['duration'])
2332 if t > duration:
2333 return
2335 startScale = self.stats['startScale']
2336 endScale = self.stats['endScale']
2338 c = float(t) / float(duration)
2340 x = self.position[0] + t * self.velocity[0] * exp(-c)
2341 y = self.position[1] + t * self.velocity[1]
2343 scale = interp(c, startScale, endScale)
2345 r,g,b,a = self.color
2346 glColor4f(r, g, b, a * (1.0 - c)**2)
2347 with PushedMatrix():
2348 glTranslatef(x, y, 0)
2349 glRotatef(self.angle, 0,0,1)
2350 glScalef(scale, scale, scale)
2351 self.sprite.draw(0, (-self.sprite.width/2, -self.sprite.height/2))
2353 def doneDrawing(self):
2354 t = updater.get_ticks() - self.startTicks + self.stats['offset']
2355 duration = graphicsDependent(*self.stats['duration'])
2356 return t > duration
2358 def createPlume(location, model = 'smoke'):
2359 for i in range(graphicsDependent(0, 8, 10)):
2360 Cloud(model, location,
2361 (random.gauss(0, 0.02), random.gauss(lift,0.004)))
2362 for i in range(graphicsDependent(0, 3, 3)):
2363 Cloud(model, location,
2364 (random.gauss(0, 0.02), random.gauss(lift,0.02)))
2366 def createDebris(bbox, model = 'dust'):
2367 l,b,r,t = bbox
2368 for i in range(graphicsDependent(0, 10, 10)):
2369 Cloud(model, (random.uniform(l,r), random.uniform(b,t)),
2370 (random.gauss(0, 0.02), random.gauss(0,0.002)))
2372 # Mount {{{1
2373 class Mount(SpriteBuilding):
2374 def __init__(self, name, model, side, location, upper, **msg):
2375 SpriteBuilding.__init__(self, name, model, side, location, **msg)
2377 self.upper = upper
2378 if upper:
2379 self.upperSprite = sprites.get('upper')
2381 def complete(self, **msg):
2382 SpriteBuilding.complete(self)
2383 if self.upper:
2384 x,y = self.location
2385 self.bbox = (x, y - self.upperSprite.height,
2386 x + self.sprite.width, y + self.sprite.height)
2387 if self.local():
2388 targets.add(self, self.bbox)
2389 if self.playing():
2390 clickables.add(self, self.bbox)
2392 def destroy(self, **msg):
2393 if self.upper:
2394 x,y = self.location
2395 self.bbox = (x, y,
2396 x + self.sprite.width, y + self.sprite.height)
2397 if self.local():
2398 targets.add(self, self.bbox)
2399 if self.playing():
2400 clickables.add(self, self.bbox)
2401 SpriteBuilding.destroy(self)
2403 def draw(self):
2404 SpriteBuilding.draw(self)
2406 x,y = self.location
2407 if self.upper and self.state not in ['building', 'blueprint']:
2408 color = self.color()
2409 if color != None:
2410 glColor4fv(color)
2411 self.upperSprite.draw(0, (x, y-self.upperSprite.height))
2413 # Gun {{{1
2414 class Gun(Mount):
2415 def __init__(self, name, side, location, upper, **msg):
2416 Mount.__init__(self, name, 'gun', side, location, upper)
2418 self.missileSprite = sprites.get('missile')
2420 self.gunState = 'loading'
2421 self.heat = 0.0
2422 self.firePressed = False
2423 self.loadPercent = 0.0
2424 self.scale = 1.0
2426 def desc(self):
2427 if self.gunState == 'aiming':
2428 return "Click on target to fire."
2429 else:
2430 return Mount.desc(self)
2432 def missileStats(self):
2433 return self.tower().missileStats()
2435 def color(self):
2436 if self.active():
2437 return interp(self.heat, Mount.color(self), hotColor)
2438 else:
2439 return Mount.color(self)
2441 def resize(self):
2442 """Compute a bbox that will include the loading missile."""
2443 x,y = self.location
2444 missileHeight = self.missileSprite.height * self.loadPercent
2445 if self.upper and self.active():
2446 upperHeight = self.upperSprite.height
2447 else:
2448 upperHeight = 0.0
2450 self.bbox = (x,
2451 y - upperHeight,
2452 x + self.sprite.width,
2453 y + self.sprite.height*self.scale + missileHeight)
2455 if self.local():
2456 targets.add(self, self.bbox)
2457 if self.playing():
2458 clickables.add(self, self.bbox)
2460 def draw(self):
2461 Mount.draw(self)
2463 x,y = self.location
2464 xoffset = float(self.sprite.width) / 2.0
2466 if self.active():
2467 if self.gunState == 'loading':
2468 t = 1.0 - self.loadPercent
2469 color = buildingColor
2470 elif self.gunState in ['loaded', 'aiming']:
2471 t = 0.0
2472 color = missileColor
2473 else:
2474 return
2476 glColor4fv(color)
2477 with PushedMatrix():
2478 glTranslatef(x + xoffset, y + self.sprite.height, 0.0)
2479 scale = self.scale
2480 glScalef(scale, scale, scale)
2481 self.missileSprite.drawPartial(0,
2482 (-self.missileSprite.width/2,
2483 -t*self.missileSprite.height),
2484 (0,t,1,1))
2486 def complete(self, **msg):
2487 Mount.complete(self)
2488 self.heat = 0.0
2489 self.loadPercent = 0.0
2490 self.resize()
2492 # Start loading a missile as soon as construction is complete.
2493 self.buildPressed = False
2494 if self.local():
2495 self.playPersistentSound('loading')
2497 def destroy(self, **msg):
2498 if self.gunState == 'aiming' and self.playing():
2499 pygame.mouse.set_cursor(*pygame.cursors.arrow)
2500 clickables.unselect()
2501 self.heat = 0.0
2502 self.loadPercent = 0.0
2503 self.resize()
2504 self.gunState = 'loading'
2506 Mount.destroy(self)
2508 def load(self, percent, scale, **msg):
2509 self.loadPercent = percent
2510 self.scale = scale
2511 self.resize()
2512 if self.loadPercent >= 1.0:
2513 self.loadPercent = 1.0
2514 self.gunState = 'loaded'
2515 if self.local():
2516 self.stopPersistentSound()
2517 mixer.play('loaded')
2519 def fire(self, **msg):
2520 self.gunState = 'loading'
2521 self.loadPercent = 0.0
2522 self.resize()
2524 if self.local():
2525 self.heat += self.missileStats()['heat']
2527 def activeUpdate(self, ticks, duration):
2528 if self.heat >= 1.0:
2529 self.broadcast('destroy')
2530 self.heat -= float(duration) / float(coolDownTime)
2531 if self.heat < 0.0:
2532 self.heat = 0.0
2534 def activeClick(self, location):
2535 if self.gunState == 'aiming':
2536 if sideOfScreen(location) == self.side:
2537 # Don't fire on yourself.
2538 mixer.play('nogo')
2539 else:
2540 self.firePressed = True
2541 if self.playing():
2542 pygame.mouse.set_cursor(*pygame.cursors.arrow)
2543 source = (self.location[0]+float(self.sprite.width)/2,
2544 self.location[1]+self.sprite.height
2545 +float(self.missileSprite.height)/2)
2546 createEntity(Missile,
2547 side = otherSide(self.side),
2548 gun = self.name,
2549 source = source, # TODO: adjust for angle?
2550 target = location,
2551 stats = self.missileStats())
2552 # The created entity will send a 'fire' message back to this gun
2553 # when it is initialized.
2554 elif self.gunState == 'loading':
2555 self.playPersistentSound('loading')
2557 def activeHold(self, location, duration):
2558 gunLoadTime = self.missileStats()['loadTime']
2559 if self.gunState == 'loading' and not self.firePressed:
2560 newPercent = self.loadPercent + float(duration) / gunLoadTime
2561 self.broadcast('load', percent = newPercent,
2562 scale = self.missileStats()['scale'])
2564 def release(self, location):
2565 if self.firePressed:
2566 self.firePressed = False
2567 return Mount.release(self, location)
2569 def activeRelease(self, location):
2570 if self.gunState == 'loaded':
2571 self.gunState = 'aiming'
2572 if self.playing():
2573 pygame.mouse.set_cursor(*pygame.cursors.broken_x)
2574 clickables.updateMessageBar()
2575 return False
2576 elif self.gunState == 'loading':
2577 self.stopPersistentSound()
2578 return True
2579 else:
2580 assert self.gunState == 'aiming'
2581 return self.firePressed
2583 # Shield {{{1
2584 class Aura():
2585 def __init__(self, shield):
2586 self.shield = shield
2587 scene.add(self, AURA_LAYER)
2589 def draw(self):
2590 self.shield.drawAura()
2592 def doneDrawing(self):
2593 return self.shield.doneDrawing()
2595 shieldAngle = radians(40.0)
2596 shieldAngleSpan = radians(150.0)
2598 class Shield(Mount):
2599 def __init__(self, name, side, location, upper, **msg):
2600 Mount.__init__(self, name, 'shield', side, location, upper)
2602 sourceOffset = (29,30)
2603 if side == 'right':
2604 self.sprite = sprites.get('shield_flipped')
2605 self.upperSprite = sprites.get('upper_shield_flipped')
2606 self.source = \
2607 (self.location[0] + self.sprite.width - sourceOffset[0],
2608 self.location[1] + sourceOffset[1])
2609 else:
2610 self.upperSprite = sprites.get('upper_shield')
2611 self.source = \
2612 (self.location[0] + sourceOffset[0],
2613 self.location[1] + sourceOffset[1])
2615 # Strength of the shield. 1.0 is fully charged.
2616 self.loadPercent = 0.0
2618 # True if the beep beep denoting full charge has been played.
2619 self.loadedPlayed = False
2621 # If the shield is not local to this computer, it has no way of
2622 # computing the radius, so it is passed in charge messages and stored.
2623 self.proxyRadius = 0.0
2624 self.proxyWidth = 10.0
2626 self.auraDisplayList = glGenLists(1)
2627 self.compileList()
2628 Aura(self)
2630 def shieldStats(self):
2631 return self.tower().shieldStats()
2633 def maxRadius(self):
2634 return self.shieldStats()['radius']
2636 def maxStrength(self):
2637 return self.shieldStats()['strength']
2639 def strength(self):
2640 return self.loadPercent * self.maxStrength()
2642 def center(self):
2643 x,y = self.source
2644 offset = 0.5*self.radius()
2645 if self.side == 'left':
2646 return (x - cos(shieldAngle)*offset, y - sin(shieldAngle)*offset)
2647 else:
2648 return (x + cos(shieldAngle)*offset, y - sin(shieldAngle)*offset)
2650 def radius(self):
2651 if self.local():
2652 return self.loadPercent * self.maxRadius()
2653 else:
2654 return self.proxyRadius
2656 def width(self):
2657 if self.local():
2658 return self.shieldStats()['width']
2659 else:
2660 return self.proxyWidth
2663 def loadTime(self):
2664 return self.shieldStats()['loadTime']
2666 def complete(self, **msg):
2667 Mount.complete(self)
2668 self.loadPercent = 0.0
2670 # Start charging immediately.
2671 if self.local():
2672 self.playPersistentSound('loading_shield')
2673 self.buildPressed = False
2674 self.compileList()
2676 def destroy(self, **msg):
2677 self.loadPercent = 0.0
2678 Mount.destroy(self)
2679 self.compileList()
2681 def charge(self, percent, radius, width, **msg):
2682 self.loadPercent = percent
2683 if not self.local():
2684 self.proxyRadius = radius
2685 self.proxyWidth = width
2686 self.compileList()
2688 def reshield(self):
2689 if self.active():
2690 self.broadcast('charge', percent = self.loadPercent,
2691 radius = self.radius(),
2692 width = self.width())
2694 def absorb(self, hp):
2695 loss = float(hp) / float(self.maxStrength())
2696 percent = max(self.loadPercent - loss, 0.0)
2697 self.broadcast('charge', percent = percent,
2698 radius = self.maxRadius() * percent,
2699 width = self.width())
2701 def shieldingAtLocation(self, location):
2702 if dist2(location, self.center()) < self.radius()**2:
2703 return self
2704 else:
2705 return None
2707 def hitShield(self, location, radius, damage):
2708 d = dist(location, self.center()) - self.radius()
2709 if d < radius:
2710 self.absorb(damage * (1.0 - float(d)/float(radius))**2)
2711 return True
2712 else:
2713 return False
2715 def activeClick(self, location):
2716 if self.loadPercent < 1.0:
2717 self.playPersistentSound('loading_shield')
2718 self.loadedPlayed = False
2720 def activeHold(self, location, duration):
2721 percent = min(self.loadPercent + float(duration) / self.loadTime(), 1.0)
2722 self.broadcast('charge', percent = percent,
2723 radius = self.maxRadius() * percent,
2724 width = self.width())
2725 if percent >= 1.0:
2726 self.stopPersistentSound()
2727 if not self.loadedPlayed:
2728 mixer.play('loaded_shield')
2729 self.loadedPlayed = True
2731 def activeRelease(self, location):
2732 self.stopPersistentSound()
2733 return True
2735 def compileList(self):
2736 glNewList(self.auraDisplayList, GL_COMPILE)
2737 segments = graphicsDependent(15, 20, 25)
2739 percent = self.loadPercent
2740 radius = self.radius()
2741 width = min(self.width(), radius)
2743 startAngle = shieldAngle - 0.5*shieldAngleSpan
2744 endAngle = shieldAngle + 0.5*shieldAngleSpan
2746 glPushMatrix()
2747 x,y = self.center()
2748 glTranslatef(x,y,0)
2749 if self.side == 'right':
2750 glScalef(-1.0, 1.0, 1.0)
2752 with NoTexture():
2753 glBegin(GL_QUAD_STRIP)
2754 for i in range(segments):
2755 t = float(i) / float(segments-1)
2756 angle = interp(t, startAngle, endAngle)
2757 c, s = cos(angle), sin(angle)
2759 midAlpha = 1.5 * t * (1.0 - t)
2761 glColor4f(1,1,1,0)
2762 glVertex2f((radius - width) * c, (radius - width) * s)
2763 glColor4f(1,1,1, midAlpha)
2764 glVertex2f(radius * c, radius * s)
2765 glEnd()
2767 glPopMatrix()
2768 glEndList()
2770 def drawAura(self):
2771 if self.active():
2772 glCallList(self.auraDisplayList)
2775 # Spline {{{1
2776 class Spline():
2777 def __init__(self, gateway, a, b, c, d):
2778 self.gateway = gateway
2779 self.a = a; self.b = b; self.c = c; self.d = d
2780 self.segments = 30
2781 self.color = gateway.virusColor()
2782 self.width = 5
2784 self.compileList()
2786 scene.add(self, SPLINE_LAYER)
2788 def tOfTheWay(self, t):
2789 a = self.a; b = self.b; c = self.c; d = self.d
2790 ab = interp(t,a,b); bc = interp(t,b,c); cd = interp(t,c,d)
2791 abc = interp(t,ab,bc); bcd = interp(t,bc,cd)
2792 return interp(t, abc, bcd)
2794 def direction(self, t):
2795 def sub(u, v):
2796 return (u[0]-v[0], u[1]-v[1], u[2]-v[2])
2797 a = self.a; b = self.b; c = self.c; d = self.d
2798 ab = sub(b,a); bc = sub(b, c); cd = sub(c, d)
2799 abc = interp(t,ab,bc); bcd = interp(t,bc,cd)
2800 dir = interp(t, abc, bcd)
2801 norm = sqrt(dir[0]**2 + dir[1]**2 + dir[2]**2)
2802 return (dir[0]/norm, dir[1]/norm, dir[2]/norm)
2804 def drawVirus(self, t, color):
2805 if t < 1.0:
2806 alpha = 0.5
2807 else:
2808 alpha = 0.5 * (1.0 - (t - 1.0) / virusOverPercent)
2809 glColor4f(color[0], color[1], color[2], color[3]*alpha)
2811 size = 20
2812 with PushedMatrix():
2813 with Perspective((width/2, height/2)):
2814 glTranslatef(*self.tOfTheWay(t))
2815 u = (0,-1,0)
2816 v = self.direction(t)
2818 def uv(s, t):
2819 return (s*u[0]+t*v[0], s*u[1]+t*v[1], 0)
2821 # Note that we do not use the usual sprite drawing facilities
2822 # here.
2823 sprite = sprites.get('virus')
2824 lazyBindTexture(sprite.texture)
2825 glBegin(GL_QUADS)
2826 sprite.texCoord(0,sprite.height)
2827 glVertex3fv(uv(-size,-size))
2828 sprite.texCoord(sprite.width,sprite.height)
2829 glVertex3fv(uv(size,-size))
2830 sprite.texCoord(sprite.width,0)
2831 glVertex3fv(uv(size,size))
2832 sprite.texCoord(0,0)
2833 glVertex3fv(uv(-size,size))
2834 glEnd()
2836 def compileList(self):
2837 self.displayList = glGenLists(1)
2838 glNewList(self.displayList, GL_COMPILE)
2839 segments = self.segments
2840 r,g,b,a = self.color
2841 halfwidth = self.width / 2.0
2842 with Perspective((width/2, height/2)):
2843 with NoTexture():
2844 glBegin(GL_QUAD_STRIP)
2845 for i in range(segments):
2846 t = float(i) / float(segments-1)
2848 x,y,z = self.tOfTheWay(t)
2850 glColor4f(r,g,b,0)
2851 glVertex3f(x,y-halfwidth,z)
2852 glColor4f(r,g,b,a*t*(1.0-t))
2853 glVertex3f(x,y,z)
2854 glEnd()
2855 glBegin(GL_QUAD_STRIP)
2856 for i in range(segments):
2857 t = float(i) / float(segments-1)
2859 x,y,z = self.tOfTheWay(t)
2861 glColor4f(r,g,b,a*t*(1.0-t))
2862 glVertex3f(x,y,z)
2863 glColor4f(r,g,b,0)
2864 glVertex3f(x,y+halfwidth,z)
2865 glEnd()
2866 glEndList()
2868 def draw(self):
2869 if self.gateway.active():
2870 color = self.gateway.virusColor()
2871 if self.color != color:
2872 self.color = color
2873 self.compileList()
2875 glCallList(self.displayList)
2877 def doneDrawing(self):
2878 return self.gateway.doneDrawing()
2880 # Virus {{{1
2881 class Virus(Entity):
2882 def __init__(self, side, level, gateway, target, **msg):
2883 self.side = side
2884 self.level = level
2885 self.gateway = gateway
2886 self.target = target
2888 self.startTicks = updater.get_ticks()
2889 self.exploded = False
2891 scene.add(self, VIRUS_LAYER)
2892 if self.local():
2893 updater.add(self, 0)
2894 entities[gateway].broadcast('fire')
2896 self.splines = entities[gateway].splines
2897 mixer.play('virus')
2899 def percent(self):
2900 p = float(updater.get_ticks() - self.startTicks) / virusTravelTime
2901 if p < 0.0:
2902 return 0.0
2903 else:
2904 return p
2906 def virusColor(self):
2907 if otherSide(self.side) in towers:
2908 return virusColors[self.level]
2909 else:
2910 return foreignVirusColor
2912 def draw(self):
2913 p = self.percent()
2914 if p < 1.0 + virusOverPercent:
2915 for spline in self.splines:
2916 spline.drawVirus(p, self.virusColor())
2918 def doneDrawing(self):
2919 return self.percent() > 1.0 + virusOverPercent
2921 def update(self, ticks, duration):
2922 if self.percent() >= 1.0 and not self.exploded:
2923 self.exploded = True
2925 # Get the building at the target, and tell it has a virus
2926 obj = targets.objectAtLocation(self.target)
2927 if obj and obj.side == self.side:
2928 # Don't broadcast a message. The building will broadcast a
2929 # destroy message on a successsful infection.
2930 obj.infect(self.level) # Add level of virus.
2932 # Gateway {{{1
2933 class Gateway(SpriteBuilding):
2934 def __init__(self, name, side, location, **msg):
2935 SpriteBuilding.__init__(self, name, 'gateway', side, location, **msg)
2936 self.createSplines()
2938 self.virusSprite = sprites.get('virus')
2940 # Note some of this code could be shared with Gun, n'est pas?
2941 self.gatewayState = 'loading'
2942 self.firePressed = False
2943 self.loadPercent = 0.0
2945 def createSplines(self):
2946 self.splines = []
2948 spreadx = 50.0; spready = 150.0
2949 sw = self.sprite.width; sh = self.sprite.height
2950 source = (self.location[0]+sw/2.0, self.location[1] + sh/2.0)
2951 self.source = source
2953 if self.side == 'left':
2954 target = (width - 2*spreadx, 2*spready)
2955 else:
2956 target = (2*spreadx, 2*spready)
2958 for i in range(graphicsDependent(6,8,10)):
2959 randx = random.gauss(0.0, 1.0)
2960 randy = random.gauss(0.0, 1.0)
2961 self.addSpline((source[0] + 0.1*randx*sw,
2962 source[1] + 0.1*randy*sh),
2963 (target[0] + randx*spreadx,
2964 target[1] + randy*spready))
2966 def addSpline(self, source, target):
2967 x0, y0 = source
2968 x3, y3 = target
2970 a = (x0, y0, width/2)
2971 b = (interp(0.2, x0, x3), interp(0.2, y0, y3) + 500, width)
2972 c = (interp(0.7, x0, x3), interp(0.7, y0, y3) - 200, 0.75*width)
2973 d = (x3, y3, width/2)
2974 self.splines.append(Spline(self, a, b, c, d))
2976 def desc(self):
2977 if self.gatewayState == 'aiming':
2978 return "Click on target to infect."
2979 else:
2980 return SpriteBuilding.desc(self)
2982 def virusColor(self):
2983 if self.local():
2984 return self.tower().virusColor()
2985 else:
2986 return foreignVirusColor
2988 def draw(self):
2989 SpriteBuilding.draw(self)
2991 # Draw the loading of the virus.
2992 if self.active():
2993 percent = self.loadPercent
2994 if percent > 0.0:
2995 with PushedMatrix():
2996 glTranslatef(self.source[0], self.source[1], 0)
2997 glScalef(percent, percent, percent)
2999 r,g,b,a = self.virusColor()
3000 if percent < 1.0:
3001 glColor4f(r,g,b,0.3)
3002 else:
3003 glColor4f(r,g,b,0.5)
3005 self.virusSprite.drawCentered(0)
3007 def complete(self, **msg):
3008 SpriteBuilding.complete(self)
3010 self.buildPressed = False
3011 if self.local():
3012 self.playPersistentSound('loading_virus')
3014 def destroy(self, **msg):
3015 if self.gatewayState == 'aiming' and self.playing():
3016 pygame.mouse.set_cursor(*pygame.cursors.arrow)
3017 clickables.unselect()
3018 self.loadPercent = 0.0
3019 self.gatewayState = 'loading'
3021 SpriteBuilding.destroy(self)
3023 def fire(self, **msg):
3024 self.gatewayState = 'loading'
3025 self.loadPercent = 0.0
3027 def activeClick(self, location):
3028 if self.gatewayState == 'aiming' and \
3029 sideOfScreen(location) != self.side:
3030 self.firePressed = True
3031 if self.playing():
3032 pygame.mouse.set_cursor(*pygame.cursors.arrow)
3033 createEntity(Virus,
3034 side = otherSide(self.side),
3035 level = self.tower().virusLevel,
3036 gateway = self.name,
3037 target = location) # To add, level of virus.
3038 elif self.gatewayState == 'loading':
3039 self.playPersistentSound('loading_virus')
3041 def activeHold(self, location, duration):
3042 if self.gatewayState == 'loading' and not self.firePressed:
3043 self.loadPercent += float(duration) / gatewayLoadTime
3044 if self.loadPercent >= 1.0:
3045 self.loadPercent = 1.0
3046 self.gatewayState = 'loaded'
3047 self.stopPersistentSound()
3048 mixer.play('loaded_virus')
3050 def release(self, location):
3051 if self.firePressed == True:
3052 self.firePressed = False
3053 return SpriteBuilding.release(self, location)
3055 def activeRelease(self, location):
3056 if self.gatewayState == 'loaded':
3057 self.gatewayState = 'aiming'
3058 if self.playing():
3059 pygame.mouse.set_cursor(*pygame.cursors.broken_x)
3060 clickables.updateMessageBar()
3061 return False
3062 elif self.gatewayState == 'loading':
3063 self.stopPersistentSound()
3064 return True
3065 else:
3066 assert self.gatewayState == 'aiming'
3067 return self.firePressed
3069 # Mutator {{{1
3070 class Mutator(SpriteBuilding):
3071 def __init__(self, name, side, location, **msg):
3072 SpriteBuilding.__init__(self, name, 'mutator', side, location, **msg)
3073 self.mutating = False
3074 self.mutated = False
3075 self.waitTime = None
3077 def color(self):
3078 if self.active():
3079 if self.mutating:
3080 return (random.uniform(0.0, 1.0),
3081 random.uniform(0.0, 1.0),
3082 random.uniform(0.0, 1.0),
3083 1.0)
3084 elif self.mutated:
3085 return self.tower().virusColor()
3086 return SpriteBuilding.color(self)
3088 def activeClick(self, location):
3089 self.mutating = True
3090 self.waitTime = random.expovariate(log(2) / mutatorHalfLife)
3091 self.playPersistentSound('mutator')
3093 def activeHold(self, location, duration):
3094 if self.mutating and duration > 0:
3095 self.waitTime -= duration
3096 if self.waitTime <= 0:
3097 # We have achieved mutation
3098 self.mutating = False
3099 self.waitTime = None
3100 self.mutated = True
3101 self.tower().evolveVirus()
3102 self.stopPersistentSound()
3103 mixer.play('mutated')
3105 def activeRelease(self, location):
3106 self.mutating = False
3107 self.waitTime = None
3108 self.mutated = False
3109 self.stopPersistentSound()
3110 return True
3112 # Girder {{{1
3113 class Girder(Building):
3114 """Levels make up the tower that we are building, and each level is
3115 supported by a girder, a building built on one level that allows us to use
3116 the next level."""
3118 def __init__(self, name, tier, side, y, length, **msg):
3119 self.tier = tier
3120 self.length = length
3121 self.girder = sprites.get('girder')
3123 if side == 'left':
3124 x = 0
3125 else:
3126 assert side == 'right'
3127 x = width - length # This is the global width of the screen.
3129 self.location = (x,y)
3130 bbox = (x, y+levelHeight-self.girder.height,
3131 x+length, y+levelHeight)
3132 Building.__init__(self, name, 'girder', side, bbox)
3134 def infect(self, level):
3135 # Girders are immune to virii.
3136 pass
3138 def complete(self, sound = 'built', **msg):
3139 if self.tier == len(levelStats)-1:
3140 sound = 'victory'
3141 Building.complete(self, sound, **msg)
3143 def protection(self):
3144 """Return the number of hit points of protection this level has before
3145 it will collapse."""
3146 t,l,b,r = self.bbox
3147 shield = self.tower().shieldingAtLocation((t,l))
3148 return (shield.strength() if shield else 0) + self.hp
3150 def drawPillar(self, location, percent):
3151 """Draw a pillar based at location. percent is the amount of the pillar
3152 that has been built (from 0 to 1)."""
3153 strutHeight = 24; gap = 0
3154 x,y = location
3156 box = (x-pillarWidth/2, y+gap,
3157 x+pillarWidth/2, y+levelHeight-strutHeight-gap)
3159 if self.state == 'building':
3160 if percent <= 0.0:
3161 drawRectangle(box, buildingColor)
3162 elif percent >= 1.0:
3163 drawRectangle(box, builtColor)
3164 else:
3165 l,b,r,t = box
3166 y = b + percent * (t-b)
3167 drawRectangle((l,b,r,y), builtColor)
3168 drawRectangle((l,y,r,t), buildingColor)
3169 else:
3170 color = self.color()
3171 if color:
3172 drawRectangle(box, color)
3174 def drawStrut(self, percent):
3175 x,y = self.location; length = self.length
3176 if self.side == 'left':
3177 xx = x+length-self.girder.width
3178 flipped = False
3179 else:
3180 xx = x
3181 flipped = True
3182 loc = (xx, y+levelHeight-self.girder.height)
3184 if self.state == 'building':
3185 if percent <= 0.0:
3186 glColor4fv(buildingColor)
3187 self.girder.draw(0, loc, flipped)
3188 elif percent >= 1.0:
3189 glColor4fv(builtColor)
3190 self.girder.draw(0, loc, flipped)
3191 else:
3192 # Adjust the percentage for the portion shown on screen.
3193 sp = 1.0 - (1.0 - percent) * float(length) / \
3194 float(self.girder.width)
3195 if self.side == 'left':
3196 glColor4fv(builtColor)
3197 self.girder.drawPartial(0, loc, (0,0,sp,1))
3198 if self.local():
3199 glColor4fv(buildingColor)
3200 self.girder.drawPartial(0, loc, (sp,0,1,1))
3201 else:
3202 glColor4fv(builtColor)
3203 self.girder.drawPartial(0, loc, (1.0-sp,0,1,1), True)
3204 if self.local():
3205 glColor4fv(buildingColor)
3206 self.girder.drawPartial(0, loc, (0,0,1.0-sp,1), True)
3207 else:
3208 color = self.color()
3209 if color:
3210 glColor4fv(color)
3211 self.girder.draw(0, loc, flipped)
3213 def draw(self):
3214 x,y = self.location; length = self.length
3215 percent = self.percentComplete()
3217 if self.side == 'left':
3218 p1 = percent*2
3219 p2 = (percent-0.365)*2
3220 else:
3221 p1 = (percent-0.365)*2
3222 p2 = percent*2
3225 self.drawPillar((x + pillarInset*self.length, y), p1)
3226 self.drawPillar((x + (1.0-pillarInset)*self.length, y), p2)
3228 self.drawStrut((percent-0.4)/0.6)
3230 # Level {{{1
3232 class Level:
3233 """Containter for all of the buildings on one level of the tower."""
3234 def __init__(self, side, tier, location, length):
3235 self.side = side
3236 self.tier = tier
3237 self.location = location
3238 self.length = length
3240 self.buildings = []
3242 self.createGirder()
3243 self.createStrategicBuildings()
3244 self.createMounts()
3246 def addBuilding(self, building):
3247 self.buildings.append(building)
3249 def numberOf(self, model):
3250 num = 0
3251 for building in self.buildings:
3252 if building.model == model and building.active():
3253 num += 1
3254 return num
3256 def buildingsByModel(self, model, state=None):
3257 return [building for building in self.buildings \
3258 if building.shown and \
3259 (model==None or building.model == model) and \
3260 (state==None or building.state == state)]
3262 def strategicBuildings(self, state=None):
3263 return [building for building in [self.strat1, self.strat2] \
3264 if building.shown and \
3265 (state==None or building.state == state)]
3267 def shieldingAtLocation(self, location):
3268 if self.lowerShield.active() and \
3269 self.lowerShield.shieldingAtLocation(location):
3270 return self.lowerShield
3271 # elif self.upperShield.active() and \
3272 # self.upperShield.shieldingAtLocation(location):
3273 # return self.upperShield
3274 else:
3275 return None
3277 def hitShield(self, location, radius, damage):
3278 if self.lowerShield.active() and \
3279 self.lowerShield.hitShield(location, radius, damage):
3280 return True
3281 # elif self.upperShield.active() and \
3282 # self.upperShield.hitShield(location, radius, damage):
3283 # return True
3284 else:
3285 return False
3287 def reshield(self):
3288 """Recompute the radii of shields, due to a change in the number of
3289 power plants."""
3290 self.lowerShield.reshield()
3291 # self.upperShield.reshield()
3293 def createGirder(self):
3294 x,y = self.location
3295 strutLength = self.length - levelStep
3296 self.girder = createEntity(Girder,
3297 side = self.side,
3298 tier = self.tier,
3299 y = y,
3300 length = strutLength)
3301 self.girder.stats = levelStats[self.tier]['girder']
3302 self.addBuilding(self.girder)
3305 def createStrategicBuildings(self):
3306 x,y = self.location
3307 strutLength = self.length - levelStep
3308 center = int(strutLength / 2)
3309 if self.side == 'right':
3310 center += width - strutLength
3312 buildingNames = levelStats[self.tier]['buildings']
3313 bw = [buildingWidth(name) for name in buildingNames]
3315 overlap = 32
3316 left = center - (bw[0] + bw[1] - overlap) / 2
3317 right = left + bw[0] - overlap
3319 building = createEntity(buildingNames[0],
3320 side = self.side,
3321 location = (left, y))
3322 self.addBuilding(building)
3323 self.strat1 = building
3325 building = createEntity(buildingNames[1],
3326 side = self.side,
3327 location =
3328 (right, y))
3329 self.addBuilding(building)
3330 self.strat2 = building
3332 self.strat1.alternate = self.strat2
3333 self.strat2.alternate = self.strat1
3335 def createMounts(self):
3336 self.createLowerMount()
3337 self.createUpperMount()
3339 self.lowerShield.alternate = self.upperGun
3340 self.upperGun.alternate = self.lowerShield
3342 def createLowerMount(self):
3343 x,y = self.location
3344 if self.side == 'left':
3345 lowerMount = (self.length - mountLength, y)
3346 else:
3347 lowerMount = (width - self.length, y)
3349 # self.lowerGun = createEntity(Gun,
3350 # side = self.side,
3351 # location = lowerMount,
3352 # upper = False)
3353 # self.addBuilding(self.lowerGun)
3354 self.lowerShield = createEntity(Shield,
3355 side = self.side,
3356 location = lowerMount,
3357 upper = False)
3358 self.addBuilding(self.lowerShield)
3360 # self.lowerGun.alternate = self.lowerShield
3361 # self.lowerShield.alternate = self.lowerGun
3363 def createUpperMount(self):
3364 x,y = self.location
3365 if self.side == 'left':
3366 upperMount = (self.length - 2*mountLength, y + mountHeight)
3367 else:
3368 upperMount = (width - self.length + mountLength, y + mountHeight)
3370 self.upperGun = createEntity(Gun,
3371 side = self.side,
3372 location = upperMount,
3373 upper = True)
3374 self.addBuilding(self.upperGun)
3375 # self.upperShield = createEntity(Shield,
3376 # side = self.side,
3377 # location = upperMount,
3378 # upper = True)
3379 # self.addBuilding(self.upperShield)
3381 # self.upperGun.alternate = self.upperShield
3382 # self.upperShield.alternate = self.upperGun
3384 def float(self):
3385 for building in self.buildings:
3386 building.broadcast('float')
3388 def erase(self, building):
3389 self.buildings.remove(building)
3390 building.broadcast('erase')
3392 # Tower {{{1
3393 class Tower:
3394 def __init__(self, side):
3395 self.side = side
3396 self.playing = True
3397 self.levels = []
3398 self.auditing = False
3400 self.handicap = shelf['handicap']
3402 # How immune is this tower to virii?
3403 self.immunityLevel = -1
3405 # How evolve are it's virii?
3406 self.virusLevel = 0
3408 def startup(self):
3409 # Draw an indestructable base girder. Buildings can't be created before
3410 # the towers dictionary is created, which is why this is not in
3411 # __init__.
3412 self.base = createEntity(Girder,
3413 side = self.side,
3414 tier = -1,
3415 y = baseHeight-levelHeight,
3416 length = baseLength)
3417 self.base.broadcast('complete', sound = None)
3418 targets.remove(self.base)
3419 clickables.remove(self.base)
3420 updater.remove(self.base)
3422 self.audit()
3424 def addLevel(self):
3425 n = len(self.levels)
3426 self.levels.append(Level(self.side, n,
3427 (0, baseHeight + n * levelHeight),
3428 baseLength - n * levelStep))
3430 def numberOf(self, model):
3431 return reduce(add, [level.numberOf(model) for level in self.levels], 0)
3433 def buildingsByModel(self, model, state=None):
3434 return reduce(concat,
3435 [level.buildingsByModel(model, state) \
3436 for level in self.levels],
3439 def strategicBuildings(self, state=None):
3440 return reduce(concat,
3441 [level.strategicBuildings(state) \
3442 for level in self.levels],
3445 def weakestGirder(self):
3446 girders = self.buildingsByModel('girder', 'built')
3447 if girders:
3448 return argmin(girders, Girder.protection)
3449 else:
3450 return None
3452 def shieldingAtLocation(self, location):
3453 for level in self.levels:
3454 shield = level.shieldingAtLocation(location)
3455 if shield:
3456 return shield
3457 return None
3459 def hitShield(self, location, radius, damage):
3460 for level in self.levels:
3461 if level.hitShield(location, radius, damage):
3462 return True
3463 return False
3465 def reshield(self):
3466 for level in self.levels:
3467 level.reshield()
3469 def factoryBoost(self):
3470 return factoryBoost[self.numberOf('factory')]
3472 def missileStats(self):
3473 return missileStats[self.numberOf('munitions')]
3475 def shieldStats(self):
3476 return shieldStats[self.numberOf('plant')]
3477 def virusColor(self):
3478 return virusColors[self.virusLevel]
3480 def evolveVirus(self):
3481 self.virusLevel += 1
3483 # Invent virus colors if we have run out of pre-defined colors.
3484 while self.virusLevel >= len(virusColors):
3485 virusColors.append((random.uniform(0.5, 1.0),
3486 random.uniform(0.5, 1.0),
3487 random.uniform(0.5, 1.0),
3488 1.0))
3489 def infect(self, level):
3490 """Updates the immunity level of the tower, and returns True if the
3491 infection is destructive, and will destroy the infected building."""
3492 if level <= self.immunityLevel:
3493 return False
3494 else:
3495 # The infection is destructive with a certain probability.
3496 if random.uniform(0.0, 1.0) < 0.75:
3497 # And is destructive infection builds an immunity with a certain
3498 # probability.
3499 if random.uniform(0.0, 1.0) < 0.40:
3500 self.immunityLevel = level
3501 return True
3502 else:
3503 return False
3505 def audit(self):
3506 """When a girder is completed, this is called to create new
3507 blueprints for building on the new level. When a girder is destroyed,
3508 this is called to remove unsupported buildings."""
3509 if not self.auditing:
3510 self.auditing = True
3511 try:
3512 if self.levels == []:
3513 self.levels.append(Level(self.side, 0,
3514 (0, baseHeight),baseLength))
3515 else:
3516 levels = self.levels
3517 for i in range(len(levels)-1):
3518 if not levels[i].girder.active():
3519 # Remove and float the above levels.
3520 self.levels = levels[:i+1]
3521 for j in range(i+1,len(levels)):
3522 levels[j].float()
3523 return
3525 if levels[-1].girder.active():
3526 if len(levels) >= len(levelStats):
3527 # We have built the highest level, and therefore
3528 # won.
3529 createEntity(VictoryMessage, side = self.side)
3530 else:
3531 # More work to go.
3532 self.addLevel()
3534 finally:
3535 self.auditing = False
3537 towers = {}
3538 def createTower(side):
3539 assert side not in towers
3540 towers[side] = Tower(side)
3541 towers[side].startup()
3543 # Commands {{{1
3544 class Command:
3545 def __init__(self, tower):
3546 self.tower = tower
3547 def update(self, duration):
3548 pass
3550 nullLocation = (0,0)
3552 class BuildCommand(Command):
3553 def __init__(self, tower, buildings):
3554 """Buildings is a list of possible buildings to build. One is chosen at
3555 random."""
3556 Command.__init__(self, tower)
3557 if buildings == []:
3558 self.building = None
3559 else:
3560 self.building = random.choice(buildings)
3562 def possible(self):
3563 return self.building != None
3564 def start(self):
3565 self.building.click(nullLocation)
3566 def update(self, duration):
3567 self.building.hold(nullLocation, duration)
3568 def done(self):
3569 return self.building.state != 'building'
3570 def complete(self):
3571 self.building.release(nullLocation)
3573 class BuildGirderCommand(BuildCommand):
3574 def __init__(self, tower):
3575 BuildCommand.__init__(self, tower, [tower.levels[-1].girder])
3576 def possible(self):
3577 return len(self.tower.levels) <= len(levelStats) and \
3578 BuildCommand.possible(self)
3579 def done(self):
3580 if BuildCommand.done(self):
3581 return True
3582 # Return premateurly if the rest of the tower is in danger.
3583 girder = self.tower.weakestGirder()
3584 if girder and girder.protection() < 800:
3585 return True
3586 return False
3588 class BuildGunCommand(BuildCommand):
3589 def __init__(self, tower):
3590 BuildCommand.__init__(self, tower,
3591 tower.buildingsByModel('gun', 'blueprint'))
3593 class BuildShieldCommand(BuildCommand):
3594 def __init__(self, tower):
3595 BuildCommand.__init__(self, tower,
3596 tower.buildingsByModel('shield', 'blueprint'))
3598 class BuildStrategicBuildingCommand(BuildCommand):
3599 def __init__(self, tower):
3600 BuildCommand.__init__(self, tower,
3601 tower.strategicBuildings('blueprint'))
3603 class FireCommand(Command):
3604 def targetBuildings(self):
3605 return self.tower.enemy().buildingsByModel(None, 'built')
3607 def target(self):
3608 buildings = self.targetBuildings()
3609 if buildings == []:
3610 return (100,200)
3611 else:
3612 building = random.choice(buildings)
3613 return bboxCenter(building.bbox)
3615 def possible(self):
3616 return self.gun != None
3618 def start(self):
3619 self.gun.click(nullLocation)
3620 def update(self, duration):
3621 if self.gun.loadPercent < 1.0:
3622 self.gun.hold(nullLocation, duration)
3623 elif not self.fired:
3624 self.gun.release(nullLocation)
3625 self.gun.click(self.target())
3626 self.gun.release(nullLocation)
3627 self.fired = True
3628 def done(self):
3629 return self.gun.state != 'built' or self.fired
3630 def complete(self):
3631 pass
3633 class FireGunCommand(FireCommand):
3634 def __init__(self, tower):
3635 FireCommand.__init__(self, tower)
3636 self.fired = False
3638 guns = tower.buildingsByModel('gun', 'built')
3639 if guns == []:
3640 self.gun = None
3641 else:
3642 self.gun = random.choice(guns)
3644 class FireVirusCommand(FireCommand):
3645 def __init__(self, tower):
3646 FireCommand.__init__(self, tower)
3647 self.fired = False
3649 guns = tower.buildingsByModel('gateway', 'built')
3650 if guns == []:
3651 self.gun = None
3652 else:
3653 self.gun = random.choice(guns)
3655 def possible(self):
3656 return FireCommand.possible(self) and self.tower.virusUsage < 3
3658 def targetBuildings(self):
3659 return [b for b in FireCommand.targetBuildings(self) \
3660 if b.model != 'girder']
3662 def complete(self):
3663 FireCommand.complete(self)
3664 self.tower.virusUsage += 1
3666 class ChargeShieldCommand(Command):
3667 def __init__(self, tower):
3668 Command.__init__(self, tower)
3669 self.timeout = 5000
3670 shields = tower.buildingsByModel('shield', 'built')
3671 if shields == []:
3672 self.shield = None
3673 else:
3674 self.shield = random.choice(shields)
3676 def possible(self):
3677 return self.shield != None
3679 def start(self):
3680 self.shield.click(nullLocation)
3681 def update(self, duration):
3682 self.shield.hold(nullLocation, duration)
3683 self.timeout -= duration
3684 def done(self):
3685 return self.shield.loadPercent >= 1.0 or self.timeout < 0
3686 def complete(self):
3687 self.shield.release(nullLocation)
3689 class MutateCommand(Command):
3690 def __init__(self, tower):
3691 Command.__init__(self, tower)
3692 mutators = tower.buildingsByModel('mutator', 'built')
3693 if mutators == []:
3694 self.mutator = None
3695 else:
3696 self.mutator = random.choice(mutators)
3697 self.timeout = 5000
3698 self.level = self.tower.virusLevel
3700 def possible(self):
3701 return self.mutator != None and self.tower.virusUsage > 0
3703 def start(self):
3704 self.mutator.click(nullLocation)
3705 def update(self, duration):
3706 self.mutator.hold(nullLocation, duration)
3707 self.timeout -= duration
3708 def done(self):
3709 return self.timeout < 0 or self.tower.virusLevel != self.level
3710 def complete(self):
3711 self.mutator.release(nullLocation)
3713 commandList = [BuildGirderCommand] + \
3714 [BuildGunCommand] * 4 + \
3715 [BuildShieldCommand] * 2 + \
3716 [BuildStrategicBuildingCommand] * 2 + \
3717 [FireGunCommand] * 4 + \
3718 [ChargeShieldCommand] * 4 + \
3719 [FireVirusCommand] * 6 + \
3720 [MutateCommand] * 4
3722 # IntelligentTower {{{1
3723 class IntelligentTower(Tower):
3724 def __init__(self, side):
3725 Tower.__init__(self, side)
3726 self.playing = False
3727 self.handicap = shelf['aiHandicap']
3729 updater.add(self, 0)
3731 self.command = None
3733 # Number of times a virus of a particular strain has been used. Used
3734 # to stop the AI from sending out the same virus again and again.
3735 self.virusUsage = 0
3737 def enemy(self):
3738 return towers[otherSide(self.side)]
3740 def evolveVirus(self):
3741 Tower.evolveVirus(self)
3742 self.virusUsage = 0
3744 def chooseCommand(self):
3745 if self.command:
3746 self.command.complete()
3747 self.command = None
3749 for i in range(10):
3750 cmd = random.choice(commandList)
3751 self.command = cmd(self)
3752 if self.command.possible():
3753 self.command.start()
3754 return
3756 self.command = None
3758 def update(self, ticks, duration):
3759 if (self.command == None or self.command.done()) and victor == None:
3760 self.chooseCommand()
3761 if self.command:
3762 self.command.update(duration)
3765 # Menu {{{1
3766 def startGame(sides):
3767 title.hide()
3768 for side in sides:
3769 createTower(side)
3771 def menuLocation(row, justification='center'):
3772 x = width/2
3773 j = justification
3774 if j == 'left':
3775 x -= 100
3776 elif j == 'right':
3777 x += 100
3778 else:
3779 assert j in ['center', 'centre']
3781 y = height - 200 - 40 * row
3783 return (x,y)
3785 def hideMenu(menu):
3786 for widget in menu:
3787 widget.hide()
3789 def showMenu(menu):
3790 for widget in menu:
3791 widget.show()
3793 def singlePlayerCallback():
3794 hideMenu(mainMenu)
3795 title.hide()
3796 towers['left'] = Tower('left')
3797 towers['left'].startup()
3798 towers['right'] = IntelligentTower('right')
3799 towers['right'].startup()
3801 def startDemo():
3802 hideMenu(mainMenu)
3803 title.hide()
3804 towers['left'] = IntelligentTower('left')
3805 towers['left'].startup()
3806 towers['right'] = IntelligentTower('right')
3807 towers['right'].startup()
3809 class WaitForClient():
3810 def update(self, ticks, duration):
3811 if net.connectToClient():
3812 updater.remove(self)
3813 hideMenu(serverMenu)
3815 startGame(['left'])
3816 waitForClient = WaitForClient()
3818 def hostGameCallback():
3819 hideMenu(mainMenu)
3820 showMenu(serverMenu)
3822 # TODO: Add exception catching.
3823 net.startServer()
3825 serverStatusLabel.text = "Hosting game at "+net.server.getsockname()[0]
3827 updater.add(waitForClient, -99)
3829 def cancelServerCallback():
3830 hideMenu(serverMenu)
3831 showMenu(mainMenu)
3833 updater.remove(waitForClient)
3834 net.reset()
3836 class WaitForServer():
3837 def __init(self):
3838 self.pooched = False
3840 def update(self, ticks, duration):
3841 assert 'address' in self.__dict__
3842 try:
3843 if net.connectToServer(self.address):
3844 updater.remove(self)
3845 hideMenu(clientMenu)
3847 # This starts the action.
3848 startGame(['right'])
3849 except socket.error, e:
3850 clientStatusLabel.text = "Error: "+e[1]
3851 waitForServer = WaitForServer()
3853 def connectToCallback(address):
3854 # Store address between runs.
3855 shelf['address'] = address
3857 hideMenu(mainMenu)
3858 showMenu(clientMenu)
3860 waitForServer.address = address
3861 updater.add(waitForServer, -99)
3863 def cancelClientCallback():
3864 hideMenu(clientMenu)
3865 showMenu(mainMenu)
3867 updater.remove(waitForServer)
3868 waitForServer.pooched = False
3869 del waitForServer.address
3870 net.reset()
3872 def playgroundCallback():
3873 hideMenu(mainMenu)
3874 title.hide()
3875 startGame(['left', 'right'])
3877 def settingsCallback():
3878 hideMenu(mainMenu)
3879 showMenu(settingsMenu)
3881 def handicapCallback(value):
3882 try:
3883 handicap = float(value)
3884 shelf['handicap'] = handicap
3885 except ValueError:
3886 handicapBox.text = str(shelf['handicap'])
3888 def aiHandicapCallback(value):
3889 try:
3890 handicap = float(value)
3891 shelf['aiHandicap'] = handicap
3892 except ValueError:
3893 aiHandicapBox.text = str(shelf['aiHandicap'])
3895 def graphicsLevelCallback(value):
3896 try:
3897 level = int(value)
3898 shelf['graphicsLevel'] = max(1, min(3, level))
3899 except ValueError:
3900 pass
3901 graphicsLevelBox.text = str(shelf['graphicsLevel'])
3903 def settingsReturnCallback():
3904 hideMenu(settingsMenu)
3905 showMenu(mainMenu)
3907 def initMenus():
3908 global mainMenu, serverMenu, clientMenu, settingsMenu
3909 global singlePlayerButton, hostGameButton, connectToLabel, addressBox, \
3910 playgroundButton, settingsButton
3912 global serverStatusLabel, cancelServerButton
3913 global clientStatusLabel, cancelClientButton
3914 global handicapLabel, handicapBox, graphicsLevelLabel, \
3915 aiHandicapLevel, aiHandicapBox, \
3916 graphicsLevelBox, settingsReturnButton
3918 singlePlayerButton = Button('Single Player', menuLocation(0),
3919 singlePlayerCallback)
3920 hostGameButton = Button('Host Game', menuLocation(2), hostGameCallback)
3921 connectToLabel = Label('Connect to:', menuLocation(3))
3922 addressBox = TextBox(menuLocation(4), 40, '127.0.1.1', connectToCallback)
3923 if 'address' in shelf:
3924 addressBox.text = shelf['address']
3925 playgroundButton = Button('Playground', menuLocation(6), playgroundCallback)
3926 settingsButton = Button('Settings', menuLocation(8), settingsCallback)
3928 mainMenu = [singlePlayerButton, hostGameButton, connectToLabel, addressBox,
3929 playgroundButton, settingsButton]
3931 serverStatusLabel = Label('Started server.', menuLocation(0))
3932 cancelServerButton = Button('Cancel', menuLocation(1, 'right'),
3933 cancelServerCallback)
3935 serverMenu = [serverStatusLabel, cancelServerButton]
3937 clientStatusLabel = Label('Connecting to server.', menuLocation(0))
3938 cancelClientButton = Button('Cancel', menuLocation(1, 'right'),
3939 cancelClientCallback)
3941 clientMenu = [clientStatusLabel, cancelClientButton]
3943 graphicsLevelLabel = Label('Graphics Level:', menuLocation(0))
3944 graphicsLevelBox = TextBox(menuLocation(1), 12,
3945 str(shelf['graphicsLevel']), graphicsLevelCallback)
3946 handicapLabel = Label('Player Handicap:', menuLocation(2))
3947 handicapBox = TextBox(menuLocation(3), 12,
3948 str(shelf['handicap']), handicapCallback)
3949 aiHandicapLabel = Label('AI Handicap:', menuLocation(4))
3950 aiHandicapBox = TextBox(menuLocation(5), 12,
3951 str(shelf['aiHandicap']), aiHandicapCallback)
3952 settingsReturnButton = Button('Return', menuLocation(6),
3953 settingsReturnCallback)
3955 settingsMenu = [handicapLabel, handicapBox, graphicsLevelLabel,
3956 graphicsLevelBox, aiHandicapLabel, aiHandicapBox,
3957 settingsReturnButton]
3959 hideMenu(mainMenu)
3960 hideMenu(serverMenu)
3961 hideMenu(clientMenu)
3962 hideMenu(settingsMenu)
3964 postGLInits.append(initMenus)
3966 def showMainMenu():
3967 showMenu(mainMenu)
3969 # Basic GL Functions {{{1
3970 def resize((width, height)):
3971 if height == 0:
3972 height = 1
3973 glViewport(0, 0, width, height)
3974 glMatrixMode(GL_PROJECTION)
3975 glLoadIdentity()
3976 glOrtho(0.0, width, 0.0, height, -1.0, 1.0)
3977 glMatrixMode(GL_MODELVIEW)
3978 glLoadIdentity()
3980 def init():
3981 global textureFlag
3982 if glInitTextureRectangleARB():
3983 textureFlag = GL_TEXTURE_RECTANGLE_ARB
3984 else:
3985 textureFlag = GL_TEXTURE_2D
3986 glEnable(textureFlag)
3987 glShadeModel(GL_SMOOTH)
3988 glClearColor(0.0, 0.0, 0.0, 0.0)
3989 glHint(GL_PERSPECTIVE_CORRECTION_HINT, GL_NICEST)
3991 glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
3992 glEnable(GL_BLEND)
3994 # Call all of the init functions that have been waiting for OpenGL to be
3995 # initialized.
3996 for func in postGLInits:
3997 func()
3999 def draw():
4000 glClear(GL_COLOR_BUFFER_BIT)
4001 glLoadIdentity()
4003 scene.draw()
4005 def resetDisplay():
4006 video_flags = OPENGL | DOUBLEBUF
4007 if shelf['fullscreen']:
4008 video_flags |= FULLSCREEN | HWSURFACE
4010 pygame.display.set_mode((width,height), video_flags)
4012 resize((width,height))
4014 def toggleFullscreen():
4015 shelf['fullscreen'] = not shelf['fullscreen']
4016 resetDisplay()
4018 # Event Handling Code {{{1
4019 def onKeyDown(event):
4020 global square
4021 if event.key == K_ESCAPE:
4022 return 0
4023 elif clickables.key(event.unicode, event.key, event.mod):
4024 return 1
4025 elif event.key == K_SPACE:
4026 createEntity(PauseMessage, pause = not updater.paused)
4027 #updater.togglePause()
4028 return 1
4029 elif event.key == K_f:
4030 toggleFPS()
4031 return 1
4032 elif event.key == K_m:
4033 mixer.toggleMusic()
4034 return 1
4035 elif event.key == K_d:
4036 if len(towers) == 0:
4037 startDemo()
4038 return 1
4039 elif event.key == K_w:
4040 toggleFullscreen()
4041 return 1
4042 else:
4043 return 1
4045 def onMouseDown(event, ticks):
4046 if event.button == 1:
4047 clickables.click(invertPos(event.pos), updater.convert(ticks))
4049 def onMouseMotion(event, ticks):
4050 clickables.move(invertPos(event.pos), updater.convert(ticks))
4052 def onMouseUp(event, ticks):
4053 if event.button == 1:
4054 clickables.release(invertPos(event.pos), updater.convert(ticks))
4056 def onUserEvent(event, ticks):
4057 if event.type == PURGE_EVENT:
4058 scene.purge()
4059 showFPS()
4060 else:
4061 assert event.type == MUSIC_EVENT
4062 mixer.newSong()
4064 # Main {{{1
4065 def main():
4066 pygame.init()
4067 pygame.time.set_timer(PURGE_EVENT, 2000)
4069 resetDisplay()
4071 resize((width,height))
4072 init()
4074 showMainMenu()
4076 frames = 0
4077 startTicks = pygame.time.get_ticks()
4078 lastTicks = startTicks
4079 while 1:
4080 event = pygame.event.poll()
4082 ticks = pygame.time.get_ticks()
4083 clock.tick()
4085 if event.type == QUIT:
4086 break
4087 if event.type == KEYDOWN:
4088 if onKeyDown(event) == 0:
4089 break
4090 if event.type == MOUSEBUTTONDOWN:
4091 onMouseDown(event, ticks)
4092 if event.type == MOUSEMOTION:
4093 onMouseMotion(event, ticks)
4094 if event.type == MOUSEBUTTONUP:
4095 onMouseUp(event, ticks)
4096 if event.type >= USEREVENT:
4097 onUserEvent(event, ticks)
4099 duration = ticks - lastTicks
4100 if duration > updateThreshold:
4101 updater.update(ticks, duration)
4102 lastTicks = ticks
4104 processPendingMessages()
4106 draw()
4107 pygame.display.flip()
4108 frames += 1
4110 #print "fps: %d" % ((frames*1000)/(pygame.time.get_ticks()-startTicks))
4112 if __name__ == '__main__': main()