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.
8 from __future__
import with_statement
14 import pickle
as pickle
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 *
26 from pygame
.locals import *
36 # Update the world atmost every 50 ms.
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.
45 # The dimensions of each level of the tower is.
47 baseHeight
= levelHeight
- 64
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.
60 # How long it take a heated gun to cool down.
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
86 virusOverPercent
= float(virusDecayTime
) / float(virusTravelTime
)
89 # The rate at which clouds rise
92 # Draw Layer - To draw all parts of the scene in the right order. Lower
93 # numbers are drawn first.
106 PURGE_EVENT
= USEREVENT
+ 1
107 MUSIC_EVENT
= USEREVENT
+ 2
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.
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:
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
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 } ]
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 } ]
173 shelf
= shelve
.open("settings")
174 def defaultSetting(name
, value
):
175 if name
not in shelf
:
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)
186 return shelf
['graphicsLevel']
188 def toggleGraphicsLevel():
189 level
= graphicsLevel()
191 shelf
['graphicsLevel'] = level
+1
193 shelf
['graphicsLevel'] = 1
196 def graphicsDependent(*array
):
197 return array
[graphicsLevel() - 1]
199 # Util Functions {{{1
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
))])
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)."""
215 if key(x
) < key(a
[mid
]): hi
= mid
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
):
232 return [(-b
-root
) / (2*a
), (-b
+root
) / (2*a
)]
236 return (x
, height
-y
-1)
239 return invertPos(pygame
.mouse
.get_pos())
241 def bboxCenter(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
))
258 return sqrt(x
**2 + y
**2)
262 if x
== 0.0 and y
== 0.0:
269 return u
[0]*v
[0] + u
[1]*v
[1]
271 def locationInBBox(location
, 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
):
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:
300 glDisable(textureFlag
)
301 def __exit__(self
, type, value
, tb
):
302 glEnable(textureFlag
)
304 class PushedMatrix():
307 def __exit__(self
, type, value
, tb
):
311 def __init__(self
, mode
):
315 def __exit__(self
, type, value
, tb
):
319 def __init__(self
, center
=(width
/2,height
/2), stop
=width
/2):
325 glMatrixMode(GL_PROJECTION
)
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,
335 glMatrixMode(GL_MODELVIEW
)
337 def __exit__(self
, type, value
, tb
):
338 glMatrixMode(GL_PROJECTION
)
340 glMatrixMode(GL_MODELVIEW
)
342 def drawRectangle(bbox
, color
):
354 def drawRoundedRectangle(bbox
, color
, border
=8, segments
= 24):
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
))
365 l
+= border
/2; r
-= border
/2
366 b
+= border
/2; t
-= border
/2
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
)
379 assert side
== 'right'
382 # A list of functions to be called after OpenGL is initialized.
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."""
397 self
.LSize
= struct
.calcsize("l")
398 self
.name
= 'offline'
401 if 'server' in self
.__dict
__:
404 if 'client' in self
.__dict
__:
407 if 'sock' in self
.__dict
__:
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
__
435 (sock
, address
) = self
.server
.accept()
443 except socket
.error
, e
:
444 # Only handle blocking errors. Rethrow anything else.
445 if e
.args
[0] == errno
.EWOULDBLOCK
:
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
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
):
465 if err
in (0, errno
.EISCONN
):
466 self
.sock
= self
.client
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
)))
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."""
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
))
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])
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
))
516 # Unpack the object from the buffer.
517 assert len(self
.buf
) == self
.size
518 obj
= pickle
.loads(self
.buf
)
521 #print "received:", obj
523 except socket
.error
, e
:
524 if e
[0] in [errno
.EAGAIN
, errno
.EWOULDBLOCK
]:
529 # The global instance of the network.
533 if 'server' in net
.__dict
__ and net
.server
:
535 if 'client' in net
.__dict
__ and net
.client
:
537 if 'sock' in net
.__dict
__ and net
.sock
:
540 sys
.exitfunc
= closeSocket
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
550 return towers
[self
.side
]
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
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
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
577 # Global repository of all entities.
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']]
597 entities
[name
] = entity
601 """Replace a class object with it's name. Leave anything else unchanged."""
602 return getattr(cls
, '__name__', cls
)
604 def createEntity(cls
, **msg
):
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'
615 entity
= entityFromMessage(msg
)
616 if net
.name
!= 'offline':
620 def processMessage(msg
):
621 if msg
['method'] == 'create':
622 entityFromMessage(msg
)
624 entity
= entities
[msg
['name']]
625 method
= getattr(entity
.__class
__, msg
['method'])
626 method(entity
, **msg
)
628 def processPendingMessages():
629 if net
.name
!= 'offline':
631 msg
= net
.getMessage()
636 def emitMessage(msg
):
637 if net
.name
!= 'offline':
640 def broadcastMessage(msg
):
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."""
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
]
665 class Updater(PriorityList
):
667 PriorityList
.__init
__(self
)
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."""
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."""
686 if t1
> self
.pauseTime
:
688 elif t2
> self
.pauseTime
:
689 return t2
- self
.pauseTime
692 def convert(self
, ticks
):
693 if self
.paused
and ticks
> self
.pauseTime
:
694 ticks
= self
.pauseTime
695 return ticks
- self
.downTime
698 return self
.convert(pygame
.time
.get_ticks())
705 self
.pauseTime
= pygame
.time
.get_ticks()
707 if self
.pauseLabel
== None:
708 self
.pauseLabel
= Label('Paused', (width
/2,height
/2), 'center')
710 self
.pauseLabel
.text
= 'Paused'
717 self
.downTime
+= pygame
.time
.get_ticks() - self
.pauseTime
718 self
.pauseTime
= None
721 self
.pauseLabel
.text
=''
726 def togglePause(self
):
733 class PauseMessage(Entity
):
734 def __init__(self
, pause
, **msg
):
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."""
752 """Draw the entire scene."""
753 for (obj
, level
) in self
.objs
:
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()]
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) ])
777 for name
in soundList
:
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
)
788 if musicList
[0] == shelf
['lastPlayed']:
789 self
.advanceMusicList()
792 def advanceMusicList(self
):
794 # Put the first track at the end of the list.
795 musicList
= musicList
[1:] + musicList
[0:1]
798 music
= pygame
.mixer
.music
799 music
.load(os
.path
.join('data', musicList
[0]))
802 shelf
['lastPlayed'] = musicList
[0]
803 self
.advanceMusicList()
805 def toggleMusic(self
):
806 music
= pygame
.mixer
.music
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
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()
832 postGLInits
.append(initMixer
)
834 # Circle - a simple entity for testing {{{1
835 class Circle(Entity
):
836 def __init__(self
, radius
, **msg
):
839 def setRadius(self
, radius
, **msg
):
842 # Square - a drawable entity {{{1
843 class Square(Entity
):
844 def __init__(self
, size
, location
, color
, **msg
):
846 self
.location
= location
851 def setSize(self
, size
, **msg
):
854 def setLocation(self
, location
, **msg
):
855 self
.location
= location
857 def setColor(self
, color
, **msg
):
861 loc
= self
.location
; size
= self
.size
863 glColor3fv(self
.color
)
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])
871 def doneDrawing(self
):
875 # Mode for drawing textures - set by init() below.
878 # Try to avoid gl state changes by remembering what options are enabled
880 def lazyBindTexture(texture
):
882 if texture
!= boundTexture
:
883 glBindTexture(textureFlag
, texture
)
884 glDisable(textureFlag
)
885 glEnable(textureFlag
)
886 boundTexture
= texture
888 def nextPowerOfTwo(n
):
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
899 assert oldWidth
<= newWidth
900 assert oldHeight
<= newHeight
901 assert len(imageData
) == 4 * oldWidth
* oldHeight
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,
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)
931 self
.numFrames
= numFrames
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
)
945 width
= self
.width
; height
= self
.height
946 u
= (frame
% self
.columns
) * width
947 v
= (frame
/ self
.columns
) * height
950 u0
= u
+ width
; u1
= u
952 u0
= u
; u1
= u
+ width
953 v0
= v
+height
; v1
= v
955 assert glIsEnabled(textureFlag
)
958 self
.texCoord(u0
, v0
)
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
)
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
)
980 width
= self
.width
; height
= self
.height
981 u
= (frame
% self
.columns
) * width
982 v
= (frame
/ self
.columns
) * height
986 xmin
= width
* l
; xmax
= width
* r
987 ymin
= height
* b
; ymax
= height
* t
990 u0
= u
+ width
- xmin
; u1
= u
+ width
- xmax
992 u0
= u
+ xmin
; u1
= u
+ xmax
993 v0
= v
+height
-ymin
; v1
= v
+height
-ymax
995 assert glIsEnabled(textureFlag
)
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
)
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
,
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
,
1051 image
.get_width(), image
.get_height(),
1053 GL_RGBA
, GL_UNSIGNED_BYTE
,
1057 def texCoord(self
, u
, v
):
1060 spriteClassByMode
= { GL_TEXTURE_2D
: SquareSprite
,
1061 GL_TEXTURE_RECTANGLE_ARB
: RectangularSprite
}
1064 'factory', 'plant', 'munitions', 'gateway', 'mutator',
1065 'gun', 'shield', 'shield_flipped',
1066 'upper', 'upper_shield', 'upper_shield_flipped',
1070 class SpriteFactory
:
1071 """This loads all of the sprites in at the start, so that they can be
1072 accessed by name."""
1076 self
.spriteClass
= spriteClassByMode
[textureFlag
]
1078 for name
in spriteNames
:
1081 self
.add('font', 256, 16, 16, 16)
1083 def add(self
, name
, numFrames
=1, width
=None, height
=None, columns
=1):
1085 self
.spriteClass(name
, numFrames
, width
, height
, columns
)
1087 def get(self
, name
):
1088 return self
.db
[name
]
1092 sprites
= SpriteFactory()
1094 postGLInits
.append(initSprites
)
1098 """Simple test class for entities with sprites."""
1099 def __init__(self
, location
, **msg
):
1100 self
.location
= location
1101 self
.sprite
= sprites
.get('star')
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
):
1114 def drawString(str, location
, justification
='center', size
=None):
1115 sprite
= sprites
.get('font')
1118 if hasattr(str, '__iter__'):
1120 drawString(s
, location
, justification
, size
)
1121 location
= (location
[0], location
[1] - size
)
1123 width
= len(str)*size
1125 if justification
== 'right':
1127 elif justification
in ['center', 'centre']:
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
))
1137 def __init__(self
, text
, location
, justification
='center', size
=None):
1139 self
.location
= location
1140 assert justification
in ['left', 'center', 'centre', 'right']
1141 self
.justification
= justification
1144 self
.color
= (1,1,1,0.5)
1149 scene
.add(self
, LABEL_LAYER
)
1161 glColor4fv(self
.color
)
1162 drawString(self
.text
, self
.location
, self
.justification
, self
.size
)
1164 def doneDrawing(self
):
1167 messageBar
= Label([],
1168 (width
/2, height
-32), 'center')
1170 clock
= pygame
.time
.Clock()
1171 fpsBox
= Label('', (width
-10,height
-26), 'right')
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()
1179 fpsBox
.text
.append("Protection:%4d" % girder
.protection())
1185 class Widget(object):
1187 scene
.add(self
, LABEL_LAYER
)
1188 clickables
.add(self
, self
.bbox
)
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')
1200 self
.location
= location
1201 self
.clickNoise
= True
1202 self
.onClick
= onClick
1206 scene
.add(self
, LABEL_LAYER
)
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
)
1219 if clickables
.selected
== self
:
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
:
1225 fgColor
= (1.0, 1.0, 1.0, 0.7)
1226 bgColor
= (0.0, 0.0, 0.0, 0.5)
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
)
1235 drawString(self
.text
, self
.location
, 'center')
1237 def doneDrawing(self
):
1240 def click(self
, location
):
1242 def hold(self
, location
, duration
):
1244 def release(self
, location
):
1248 def hit(self
, damage
):
1256 # button = Button("Say Hi", (width/2, 100), mom)
1258 #postGLInits.append(initButton)
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')
1267 self
.location
= location
1268 self
.capacity
= capacity
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:
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
):
1295 width
= len(self
.text
)*self
.font
.width
1296 height
= self
.font
.height
1297 padding
= 2*self
.font
.width
1298 border
= self
.border
1301 glBegin(GL_QUAD_STRIP
)
1303 glVertex2f(x
- width
/2 - padding
, y
- border
)
1304 glVertex2f(x
- width
/2 - padding
, y
+ height
+ border
)
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
)
1311 glVertex2f(x
+ width
/2 + padding
, y
- border
)
1312 glVertex2f(x
+ width
/2 + padding
, y
+ height
+ border
)
1316 if clickables
.selected
== self
:
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
:
1322 fgColor
= (1.0, 1.0, 1.0, 0.7)
1323 bgColor
= (0.0, 0.0, 0.0, 0.5)
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
)
1333 drawString(self
.text
, self
.location
, 'center')
1335 if clickables
.selected
== self
:
1336 self
.drawCarat(fgColor
)
1338 def doneDrawing(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)
1351 def hold(self
, location
, duration
):
1353 def release(self
, location
):
1355 def key(self
, unicode, key
, mod
):
1359 self
.onEnter(self
.text
)
1361 elif key
in [K_BACKSPACE
, K_DELETE
]:
1362 if len(self
.text
) > 0:
1363 self
.text
= self
.text
[:-1]
1366 if len(self
.text
) < self
.capacity
:
1367 self
.text
+= unicode
1370 def hit(self
, damage
):
1371 self
.text
= "Don't hit me!"
1374 # print "entered:", text
1377 # boxers = TextBox((width/2, 150), 40, 'TextBoxers', entered)
1378 #postGLInits.append(initTextBox)
1382 def __init__(self
, location
, color
):
1383 self
.location
= location
1385 self
.sprite
= sprites
.get('title')
1390 scene
.add(self
, LABEL_LAYER
)
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
):
1405 title
= Title((width
/2, height
- 70), (1,1,1,0.6))
1406 postGLInits
.append(initTitle
)
1412 top
= (0.22, 0.24, 0.38); bottom
= (0.27, 0.40, 0.51)
1414 top
= (0.75, 0.13, 0.01); bottom
= (0.98, 0.51 , 0.19)
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)
1422 glVertex2i(0,0); glVertex2i(width
,0)
1424 glVertex2i(width
,height
); glVertex2i(0, height
)
1427 def doneDrawing(self
):
1430 scene
.add(Backdrop(), BACKDROP_LAYER
)
1434 """Test for the perspective code."""
1436 with
Perspective((width
/2, height
/2)):
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
):
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."""
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
:
1468 if self
.focus
== obj
:
1470 if obj
in self
.objs
:
1473 def minimalBBoxes(self
, pairs
):
1479 def isMinimal(pair
):
1481 if bboxInBBox(m
[1], pair
[1]):
1484 def addMinimal(pair
):
1485 for i
in range(len(minimal
)):
1486 if bboxInBBox(pair
[1], minimal
[i
][1]):
1488 minimal
.append(pair
)
1496 def closestObject(self
, location
, pairs
):
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
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
)
1519 def updateMessageBar(self
):
1520 if self
.focus
and hasattr(self
.focus
, 'desc'):
1521 messageBar
.text
= [self
.focus
.__class
__.__name
__, self
.focus
.desc()]
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'):
1536 self
.selected
= None
1538 def click(self
, location
, ticks
):
1539 assert not self
.pressed
1541 self
.lastTicks
= ticks
1543 if not updater
.paused
:
1545 self
.selected
.click(location
)
1547 self
.focusOnLocation(location
)
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
):
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."""
1571 self
.pressed
= False
1573 # First finish the holding
1574 self
.drag(location
, ticks
, minDuration
= 0)
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
)
1589 def update(self
, ticks
, duration
):
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
)
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)
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
):
1617 Label('The '+side
+' is victorious!', (width
/2, height
/2), 'center')
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
1625 def __init__(self
, name
, model
, side
, bbox
, **msg
):
1631 self
.state
= 'blueprint'
1632 self
.stats
= buildingStats
[model
]
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
1644 self
.persistentSoundChannel
= None
1647 if 'desc' in self
.stats
:
1648 return self
.stats
['desc']
1652 def mouseOver(self
):
1653 return clickables
.focus
== self
1656 """Returns True if the building is finished construction and can be
1658 return self
.state
== 'built'
1661 if self
.state
== 'blueprint':
1662 if self
.mouseOver():
1663 return buildingColor
1664 elif self
.playing():
1665 return blueprintColor
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
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
1680 # Building no longer exists, don't draw.
1682 elif self
.state
== 'building':
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
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'
1717 if 'towers' in globals():
1718 self
.tower().audit()
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')
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
):
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
))
1747 if 'towers' in globals() and self
.local():
1748 self
.tower().audit()
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."""
1757 self
.state
= 'floating'
1758 self
.rubbleTime
= updater
.get_ticks()
1760 self
.state
= 'blueprint'
1764 updater
.remove(self
)
1765 targets
.remove(self
)
1767 clickables
.remove(self
)
1769 def hide(self
, **msg
):
1770 assert not self
.active()
1774 updater
.remove(self
)
1775 targets
.remove(self
)
1777 clickables
.remove(self
)
1779 def show(self
, **msg
):
1780 assert not self
.active()
1781 if self
.state
!= 'floating':
1783 scene
.add(self
, BUILDING_LAYER
)
1785 updater
.add(self
, 0)
1786 targets
.add(self
, self
.bbox
)
1788 clickables
.add(self
, self
.bbox
)
1791 """Floating buildings no longer exist after their animation finishes."""
1792 if self
.state
== 'floating':
1793 return updater
.get_ticks() > self
.rubbleTime
+ rubbleDuration
1797 def damage(self
, newhp
, **msg
):
1800 def activeUpdate(self
, ticks
, duration
):
1802 def activeClick(self
, location
):
1804 def activeHold(self
, location
, duration
):
1806 def activeRelease(self
, location
):
1809 def update(self
, ticks
, duration
):
1810 # Neglected buildings under construction slowly decline.
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'])
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
1826 updater
.remove(self
)
1827 targets
.remove(self
)
1828 clickables
.remove(self
)
1830 def click(self
, location
):
1832 self
.activeClick(location
)
1834 if self
.state
== 'blueprint':
1835 self
.state
= 'building'
1837 if not self
.buildPressed
:
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
:
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
)
1860 self
.buildPressed
= False
1862 self
.stopPersistentSound()
1865 def doneDrawing(self
):
1869 # SpriteBuilding {{{1
1870 class SpriteBuilding(Building
):
1871 def __init__(self
, name
, model
, side
, location
, **msg
):
1872 self
.sprite
= sprites
.get(model
)
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
)
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))
1892 color
= self
.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
):
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
)
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
)
1923 self
.tower().reshield()
1924 def destroy(self
, **msg
):
1925 SpriteBuilding
.destroy(self
, **msg
)
1927 self
.tower().reshield()
1928 def float(self
, **msg
):
1929 SpriteBuilding
.float(self
, **msg
)
1931 self
.tower().reshield()
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."""
1944 return sqrt(2.0 * gravity
* dy
)
1948 # Through the magic of Lagrange multipliers, we figure out the slope has to
1950 # slope = (l + dy) / dx
1951 # Then using the vxAtAngle formulae:
1952 v2
= 0.5 * gravity
* (dx
**2 + (l
+ dy
)**2) / l
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
1963 denom
= tan(radians(angle
)) * abs(dx
) - dy
1967 vx2
= 0.5 * (gravity
* dx
**2) / denom
1975 def trajectoryTimes(dx
, dy
, speed
):
1976 a
= gravity
**2 / 4.0
1977 b
= dy
* gravity
- speed
**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
):
1986 vy
= sqrt(speed
**2 - vx
**2)
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
)
1996 v
= (vx
, abs(vx
) * tan(radians(minAngle
)))
1997 if length(v
) <= maxspeed
:
2000 times
= trajectoryTimes(dx
, dy
, maxspeed
)
2002 v
= trajectoryVelocity(dx
, dy
, maxspeed
, T
)
2003 angle
= atan2(v
[1], abs(v
[0]))
2004 if degrees(angle
) >= minAngle
:
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]
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.
2028 y
= - 0.5 * gravity
* t
**2 + vy
* t
2029 vyt
= - gravity
* t
+ vy
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, """
2043 self
.sprite
= sprites
.get('missile')
2046 self
.source
= source
; self
.target
= target
2049 maxSpeed
= maxSpeedFromRange(stats
['range'])
2050 minAngle
= stats
['angle']
2051 v
, t
= velocityTimeOfMissile(source
, target
, maxSpeed
, minAngle
)
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
))
2061 # print "maxspeed:", maxspeed, "speed:", length(self.startVelocity)
2062 # print "startAngle:", \
2063 # atan2(self.startVelocity[1], self.startVelocity[0]) * 180.0 / \
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:
2073 createPlume((self
.source
[0], self
.source
[1]-self
.sprite
.height
))
2076 scene
.add(self
, MISSILE_LAYER
)
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
2088 def velocity(self
, t
):
2089 vx
= self
.startVelocity
[0]
2090 vy
= self
.startVelocity
[1] - gravity
* t
2094 vx
, vy
= self
.velocity(t
)
2095 return atan2(vy
, vx
) * 180.0 / math
.pi
2097 def explode(self
, time
, **msg
):
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
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
),
2128 length2(disp
) - r
**2)
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
))
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'])
2150 # TODO: send ticks with draw command
2151 t
= updater
.get_ticks() - self
.startTicks
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
)
2162 (-self
.sprite
.width
/2,-self
.sprite
.height
/2))
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.
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)
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
,
2200 glVertex2fv(verts
[0])
2201 glVertex2fv(verts
[2])
2203 glEnable(textureFlag
)
2206 def drawContrail(self
):
2207 T
= updater
.get_ticks() - self
.startTicks
2208 t0
= min(T
, self
.time
)
2211 if T
- t0
> duration
:
2214 segments
= graphicsDependent(10, 17, 20)
2222 glBegin(GL_QUAD_STRIP
)
2223 for i
in range(segments
):
2224 r
= float(i
) / float(segments
-1)
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
),
2235 if i
== 0 or i
== segments
- 1:
2238 midAlpha
= interp(c
, startAlpha
, endAlpha
)
2241 glVertex2fv(verts
[0])
2242 glColor4f(1,1,1, midAlpha
)
2243 glVertex2fv(verts
[1])
2245 glBegin(GL_QUAD_STRIP
)
2246 for i
in range(segments
):
2247 r
= float(i
) / float(segments
-1)
2249 c
= min(float(T
- t
) / float(duration
), 1.0)
2251 verts
= self
.vertices(t
, interp(c
, startWidth
, endWidth
),
2254 if i
== 0 or i
== segments
- 1:
2257 midAlpha
= interp(c
, startAlpha
, endAlpha
)
2259 glColor4f(1,1,1,midAlpha
)
2260 glVertex2fv(verts
[1])
2262 glVertex2fv(verts
[2])
2265 def doneDrawingContrail(self
):
2266 T
= updater
.get_ticks() - self
.startTicks
2267 t0
= min(T
, self
.time
)
2270 return T
- t0
> duration
2275 def __init__(self
, missile
):
2276 self
.missile
= missile
2277 scene
.add(self
, CONTRAIL_LAYER
)
2280 self
.missile
.drawContrail()
2282 def doneDrawing(self
):
2283 return self
.missile
.doneDrawingContrail()
2286 def __init__(self
, missile
):
2287 self
.missile
= missile
2288 scene
.add(self
, FLAME_LAYER
)
2291 self
.missile
.drawFlame()
2293 def doneDrawing(self
):
2294 return self
.missile
.doneDrawing()
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) }}
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
),
2327 scene
.add(self
, CLOUD_LAYER
)
2330 t
= updater
.get_ticks() - self
.startTicks
+ self
.stats
['offset']
2331 duration
= graphicsDependent(*self
.stats
['duration'])
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'])
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'):
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)))
2373 class Mount(SpriteBuilding
):
2374 def __init__(self
, name
, model
, side
, location
, upper
, **msg
):
2375 SpriteBuilding
.__init
__(self
, name
, model
, side
, location
, **msg
)
2379 self
.upperSprite
= sprites
.get('upper')
2381 def complete(self
, **msg
):
2382 SpriteBuilding
.complete(self
)
2385 self
.bbox
= (x
, y
- self
.upperSprite
.height
,
2386 x
+ self
.sprite
.width
, y
+ self
.sprite
.height
)
2388 targets
.add(self
, self
.bbox
)
2390 clickables
.add(self
, self
.bbox
)
2392 def destroy(self
, **msg
):
2396 x
+ self
.sprite
.width
, y
+ self
.sprite
.height
)
2398 targets
.add(self
, self
.bbox
)
2400 clickables
.add(self
, self
.bbox
)
2401 SpriteBuilding
.destroy(self
)
2404 SpriteBuilding
.draw(self
)
2407 if self
.upper
and self
.state
not in ['building', 'blueprint']:
2408 color
= self
.color()
2411 self
.upperSprite
.draw(0, (x
, y
-self
.upperSprite
.height
))
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'
2422 self
.firePressed
= False
2423 self
.loadPercent
= 0.0
2427 if self
.gunState
== 'aiming':
2428 return "Click on target to fire."
2430 return Mount
.desc(self
)
2432 def missileStats(self
):
2433 return self
.tower().missileStats()
2437 return interp(self
.heat
, Mount
.color(self
), hotColor
)
2439 return Mount
.color(self
)
2442 """Compute a bbox that will include the loading missile."""
2444 missileHeight
= self
.missileSprite
.height
* self
.loadPercent
2445 if self
.upper
and self
.active():
2446 upperHeight
= self
.upperSprite
.height
2452 x
+ self
.sprite
.width
,
2453 y
+ self
.sprite
.height
*self
.scale
+ missileHeight
)
2456 targets
.add(self
, self
.bbox
)
2458 clickables
.add(self
, self
.bbox
)
2464 xoffset
= float(self
.sprite
.width
) / 2.0
2467 if self
.gunState
== 'loading':
2468 t
= 1.0 - self
.loadPercent
2469 color
= buildingColor
2470 elif self
.gunState
in ['loaded', 'aiming']:
2472 color
= missileColor
2477 with
PushedMatrix():
2478 glTranslatef(x
+ xoffset
, y
+ self
.sprite
.height
, 0.0)
2480 glScalef(scale
, scale
, scale
)
2481 self
.missileSprite
.drawPartial(0,
2482 (-self
.missileSprite
.width
/2,
2483 -t
*self
.missileSprite
.height
),
2486 def complete(self
, **msg
):
2487 Mount
.complete(self
)
2489 self
.loadPercent
= 0.0
2492 # Start loading a missile as soon as construction is complete.
2493 self
.buildPressed
= False
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()
2502 self
.loadPercent
= 0.0
2504 self
.gunState
= 'loading'
2508 def load(self
, percent
, scale
, **msg
):
2509 self
.loadPercent
= percent
2512 if self
.loadPercent
>= 1.0:
2513 self
.loadPercent
= 1.0
2514 self
.gunState
= 'loaded'
2516 self
.stopPersistentSound()
2517 mixer
.play('loaded')
2519 def fire(self
, **msg
):
2520 self
.gunState
= 'loading'
2521 self
.loadPercent
= 0.0
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
)
2534 def activeClick(self
, location
):
2535 if self
.gunState
== 'aiming':
2536 if sideOfScreen(location
) == self
.side
:
2537 # Don't fire on yourself.
2540 self
.firePressed
= True
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
),
2549 source
= source
, # TODO: adjust for angle?
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'
2573 pygame
.mouse
.set_cursor(*pygame
.cursors
.broken_x
)
2574 clickables
.updateMessageBar()
2576 elif self
.gunState
== 'loading':
2577 self
.stopPersistentSound()
2580 assert self
.gunState
== 'aiming'
2581 return self
.firePressed
2585 def __init__(self
, shield
):
2586 self
.shield
= shield
2587 scene
.add(self
, AURA_LAYER
)
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)
2604 self
.sprite
= sprites
.get('shield_flipped')
2605 self
.upperSprite
= sprites
.get('upper_shield_flipped')
2607 (self
.location
[0] + self
.sprite
.width
- sourceOffset
[0],
2608 self
.location
[1] + sourceOffset
[1])
2610 self
.upperSprite
= sprites
.get('upper_shield')
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)
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']
2640 return self
.loadPercent
* self
.maxStrength()
2644 offset
= 0.5*self
.radius()
2645 if self
.side
== 'left':
2646 return (x
- cos(shieldAngle
)*offset
, y
- sin(shieldAngle
)*offset
)
2648 return (x
+ cos(shieldAngle
)*offset
, y
- sin(shieldAngle
)*offset
)
2652 return self
.loadPercent
* self
.maxRadius()
2654 return self
.proxyRadius
2658 return self
.shieldStats()['width']
2660 return self
.proxyWidth
2664 return self
.shieldStats()['loadTime']
2666 def complete(self
, **msg
):
2667 Mount
.complete(self
)
2668 self
.loadPercent
= 0.0
2670 # Start charging immediately.
2672 self
.playPersistentSound('loading_shield')
2673 self
.buildPressed
= False
2676 def destroy(self
, **msg
):
2677 self
.loadPercent
= 0.0
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
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:
2707 def hitShield(self
, location
, radius
, damage
):
2708 d
= dist(location
, self
.center()) - self
.radius()
2710 self
.absorb(damage
* (1.0 - float(d
)/float(radius
))**2)
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())
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()
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
2749 if self
.side
== 'right':
2750 glScalef(-1.0, 1.0, 1.0)
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
)
2762 glVertex2f((radius
- width
) * c
, (radius
- width
) * s
)
2763 glColor4f(1,1,1, midAlpha
)
2764 glVertex2f(radius
* c
, radius
* s
)
2772 glCallList(self
.auraDisplayList
)
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
2781 self
.color
= gateway
.virusColor()
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
):
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
):
2808 alpha
= 0.5 * (1.0 - (t
- 1.0) / virusOverPercent
)
2809 glColor4f(color
[0], color
[1], color
[2], color
[3]*alpha
)
2812 with
PushedMatrix():
2813 with
Perspective((width
/2, height
/2)):
2814 glTranslatef(*self
.tOfTheWay(t
))
2816 v
= self
.direction(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
2823 sprite
= sprites
.get('virus')
2824 lazyBindTexture(sprite
.texture
)
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
))
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)):
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
)
2851 glVertex3f(x
,y
-halfwidth
,z
)
2852 glColor4f(r
,g
,b
,a
*t
*(1.0-t
))
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
))
2864 glVertex3f(x
,y
+halfwidth
,z
)
2869 if self
.gateway
.active():
2870 color
= self
.gateway
.virusColor()
2871 if self
.color
!= color
:
2875 glCallList(self
.displayList
)
2877 def doneDrawing(self
):
2878 return self
.gateway
.doneDrawing()
2881 class Virus(Entity
):
2882 def __init__(self
, side
, level
, gateway
, target
, **msg
):
2885 self
.gateway
= gateway
2886 self
.target
= target
2888 self
.startTicks
= updater
.get_ticks()
2889 self
.exploded
= False
2891 scene
.add(self
, VIRUS_LAYER
)
2893 updater
.add(self
, 0)
2894 entities
[gateway
].broadcast('fire')
2896 self
.splines
= entities
[gateway
].splines
2900 p
= float(updater
.get_ticks() - self
.startTicks
) / virusTravelTime
2906 def virusColor(self
):
2907 if otherSide(self
.side
) in towers
:
2908 return virusColors
[self
.level
]
2910 return foreignVirusColor
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.
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
):
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
)
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
):
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
))
2977 if self
.gatewayState
== 'aiming':
2978 return "Click on target to infect."
2980 return SpriteBuilding
.desc(self
)
2982 def virusColor(self
):
2984 return self
.tower().virusColor()
2986 return foreignVirusColor
2989 SpriteBuilding
.draw(self
)
2991 # Draw the loading of the virus.
2993 percent
= self
.loadPercent
2995 with
PushedMatrix():
2996 glTranslatef(self
.source
[0], self
.source
[1], 0)
2997 glScalef(percent
, percent
, percent
)
2999 r
,g
,b
,a
= self
.virusColor()
3001 glColor4f(r
,g
,b
,0.3)
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
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
3032 pygame
.mouse
.set_cursor(*pygame
.cursors
.arrow
)
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'
3059 pygame
.mouse
.set_cursor(*pygame
.cursors
.broken_x
)
3060 clickables
.updateMessageBar()
3062 elif self
.gatewayState
== 'loading':
3063 self
.stopPersistentSound()
3066 assert self
.gatewayState
== 'aiming'
3067 return self
.firePressed
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
3080 return (random
.uniform(0.0, 1.0),
3081 random
.uniform(0.0, 1.0),
3082 random
.uniform(0.0, 1.0),
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
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()
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
3118 def __init__(self
, name
, tier
, side
, y
, length
, **msg
):
3120 self
.length
= length
3121 self
.girder
= sprites
.get('girder')
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.
3138 def complete(self
, sound
= 'built', **msg
):
3139 if self
.tier
== len(levelStats
)-1:
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."""
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
3156 box
= (x
-pillarWidth
/2, y
+gap
,
3157 x
+pillarWidth
/2, y
+levelHeight
-strutHeight
-gap
)
3159 if self
.state
== 'building':
3161 drawRectangle(box
, buildingColor
)
3162 elif percent
>= 1.0:
3163 drawRectangle(box
, builtColor
)
3166 y
= b
+ percent
* (t
-b
)
3167 drawRectangle((l
,b
,r
,y
), builtColor
)
3168 drawRectangle((l
,y
,r
,t
), buildingColor
)
3170 color
= self
.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
3182 loc
= (xx
, y
+levelHeight
-self
.girder
.height
)
3184 if self
.state
== 'building':
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
)
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))
3199 glColor4fv(buildingColor
)
3200 self
.girder
.drawPartial(0, loc
, (sp
,0,1,1))
3202 glColor4fv(builtColor
)
3203 self
.girder
.drawPartial(0, loc
, (1.0-sp
,0,1,1), True)
3205 glColor4fv(buildingColor
)
3206 self
.girder
.drawPartial(0, loc
, (0,0,1.0-sp
,1), True)
3208 color
= self
.color()
3211 self
.girder
.draw(0, loc
, flipped
)
3214 x
,y
= self
.location
; length
= self
.length
3215 percent
= self
.percentComplete()
3217 if self
.side
== 'left':
3219 p2
= (percent
-0.365)*2
3221 p1
= (percent
-0.365)*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)
3233 """Containter for all of the buildings on one level of the tower."""
3234 def __init__(self
, side
, tier
, location
, length
):
3237 self
.location
= location
3238 self
.length
= length
3243 self
.createStrategicBuildings()
3246 def addBuilding(self
, building
):
3247 self
.buildings
.append(building
)
3249 def numberOf(self
, model
):
3251 for building
in self
.buildings
:
3252 if building
.model
== model
and building
.active():
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
3277 def hitShield(self
, location
, radius
, damage
):
3278 if self
.lowerShield
.active() and \
3279 self
.lowerShield
.hitShield(location
, radius
, damage
):
3281 # elif self.upperShield.active() and \
3282 # self.upperShield.hitShield(location, radius, damage):
3288 """Recompute the radii of shields, due to a change in the number of
3290 self
.lowerShield
.reshield()
3291 # self.upperShield.reshield()
3293 def createGirder(self
):
3295 strutLength
= self
.length
- levelStep
3296 self
.girder
= createEntity(Girder
,
3300 length
= strutLength
)
3301 self
.girder
.stats
= levelStats
[self
.tier
]['girder']
3302 self
.addBuilding(self
.girder
)
3305 def createStrategicBuildings(self
):
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
]
3316 left
= center
- (bw
[0] + bw
[1] - overlap
) / 2
3317 right
= left
+ bw
[0] - overlap
3319 building
= createEntity(buildingNames
[0],
3321 location
= (left
, y
))
3322 self
.addBuilding(building
)
3323 self
.strat1
= building
3325 building
= createEntity(buildingNames
[1],
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
):
3344 if self
.side
== 'left':
3345 lowerMount
= (self
.length
- mountLength
, y
)
3347 lowerMount
= (width
- self
.length
, y
)
3349 # self.lowerGun = createEntity(Gun,
3351 # location = lowerMount,
3353 # self.addBuilding(self.lowerGun)
3354 self
.lowerShield
= createEntity(Shield
,
3356 location
= lowerMount
,
3358 self
.addBuilding(self
.lowerShield
)
3360 # self.lowerGun.alternate = self.lowerShield
3361 # self.lowerShield.alternate = self.lowerGun
3363 def createUpperMount(self
):
3365 if self
.side
== 'left':
3366 upperMount
= (self
.length
- 2*mountLength
, y
+ mountHeight
)
3368 upperMount
= (width
- self
.length
+ mountLength
, y
+ mountHeight
)
3370 self
.upperGun
= createEntity(Gun
,
3372 location
= upperMount
,
3374 self
.addBuilding(self
.upperGun
)
3375 # self.upperShield = createEntity(Shield,
3377 # location = upperMount,
3379 # self.addBuilding(self.upperShield)
3381 # self.upperGun.alternate = self.upperShield
3382 # self.upperShield.alternate = self.upperGun
3385 for building
in self
.buildings
:
3386 building
.broadcast('float')
3388 def erase(self
, building
):
3389 self
.buildings
.remove(building
)
3390 building
.broadcast('erase')
3394 def __init__(self
, side
):
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?
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
3412 self
.base
= createEntity(Girder
,
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
)
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')
3448 return argmin(girders
, Girder
.protection
)
3452 def shieldingAtLocation(self
, location
):
3453 for level
in self
.levels
:
3454 shield
= level
.shieldingAtLocation(location
)
3459 def hitShield(self
, location
, radius
, damage
):
3460 for level
in self
.levels
:
3461 if level
.hitShield(location
, radius
, damage
):
3466 for level
in self
.levels
:
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),
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
:
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
3499 if random
.uniform(0.0, 1.0) < 0.40:
3500 self
.immunityLevel
= level
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
3512 if self
.levels
== []:
3513 self
.levels
.append(Level(self
.side
, 0,
3514 (0, baseHeight
),baseLength
))
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
)):
3525 if levels
[-1].girder
.active():
3526 if len(levels
) >= len(levelStats
):
3527 # We have built the highest level, and therefore
3529 createEntity(VictoryMessage
, side
= self
.side
)
3535 self
.auditing
= False
3538 def createTower(side
):
3539 assert side
not in towers
3540 towers
[side
] = Tower(side
)
3541 towers
[side
].startup()
3545 def __init__(self
, tower
):
3547 def update(self
, duration
):
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
3556 Command
.__init
__(self
, tower
)
3558 self
.building
= None
3560 self
.building
= random
.choice(buildings
)
3563 return self
.building
!= None
3565 self
.building
.click(nullLocation
)
3566 def update(self
, duration
):
3567 self
.building
.hold(nullLocation
, duration
)
3569 return self
.building
.state
!= 'building'
3571 self
.building
.release(nullLocation
)
3573 class BuildGirderCommand(BuildCommand
):
3574 def __init__(self
, tower
):
3575 BuildCommand
.__init
__(self
, tower
, [tower
.levels
[-1].girder
])
3577 return len(self
.tower
.levels
) <= len(levelStats
) and \
3578 BuildCommand
.possible(self
)
3580 if BuildCommand
.done(self
):
3582 # Return premateurly if the rest of the tower is in danger.
3583 girder
= self
.tower
.weakestGirder()
3584 if girder
and girder
.protection() < 800:
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')
3608 buildings
= self
.targetBuildings()
3612 building
= random
.choice(buildings
)
3613 return bboxCenter(building
.bbox
)
3616 return self
.gun
!= None
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
)
3629 return self
.gun
.state
!= 'built' or self
.fired
3633 class FireGunCommand(FireCommand
):
3634 def __init__(self
, tower
):
3635 FireCommand
.__init
__(self
, tower
)
3638 guns
= tower
.buildingsByModel('gun', 'built')
3642 self
.gun
= random
.choice(guns
)
3644 class FireVirusCommand(FireCommand
):
3645 def __init__(self
, tower
):
3646 FireCommand
.__init
__(self
, tower
)
3649 guns
= tower
.buildingsByModel('gateway', 'built')
3653 self
.gun
= random
.choice(guns
)
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']
3663 FireCommand
.complete(self
)
3664 self
.tower
.virusUsage
+= 1
3666 class ChargeShieldCommand(Command
):
3667 def __init__(self
, tower
):
3668 Command
.__init
__(self
, tower
)
3670 shields
= tower
.buildingsByModel('shield', 'built')
3674 self
.shield
= random
.choice(shields
)
3677 return self
.shield
!= None
3680 self
.shield
.click(nullLocation
)
3681 def update(self
, duration
):
3682 self
.shield
.hold(nullLocation
, duration
)
3683 self
.timeout
-= duration
3685 return self
.shield
.loadPercent
>= 1.0 or self
.timeout
< 0
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')
3696 self
.mutator
= random
.choice(mutators
)
3698 self
.level
= self
.tower
.virusLevel
3701 return self
.mutator
!= None and self
.tower
.virusUsage
> 0
3704 self
.mutator
.click(nullLocation
)
3705 def update(self
, duration
):
3706 self
.mutator
.hold(nullLocation
, duration
)
3707 self
.timeout
-= duration
3709 return self
.timeout
< 0 or self
.tower
.virusLevel
!= self
.level
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 + \
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)
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.
3738 return towers
[otherSide(self
.side
)]
3740 def evolveVirus(self
):
3741 Tower
.evolveVirus(self
)
3744 def chooseCommand(self
):
3746 self
.command
.complete()
3750 cmd
= random
.choice(commandList
)
3751 self
.command
= cmd(self
)
3752 if self
.command
.possible():
3753 self
.command
.start()
3758 def update(self
, ticks
, duration
):
3759 if (self
.command
== None or self
.command
.done()) and victor
== None:
3760 self
.chooseCommand()
3762 self
.command
.update(duration
)
3766 def startGame(sides
):
3771 def menuLocation(row
, justification
='center'):
3779 assert j
in ['center', 'centre']
3781 y
= height
- 200 - 40 * row
3793 def singlePlayerCallback():
3796 towers
['left'] = Tower('left')
3797 towers
['left'].startup()
3798 towers
['right'] = IntelligentTower('right')
3799 towers
['right'].startup()
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
)
3816 waitForClient
= WaitForClient()
3818 def hostGameCallback():
3820 showMenu(serverMenu
)
3822 # TODO: Add exception catching.
3825 serverStatusLabel
.text
= "Hosting game at "+net
.server
.getsockname()[0]
3827 updater
.add(waitForClient
, -99)
3829 def cancelServerCallback():
3830 hideMenu(serverMenu
)
3833 updater
.remove(waitForClient
)
3836 class WaitForServer():
3838 self
.pooched
= False
3840 def update(self
, ticks
, duration
):
3841 assert 'address' in self
.__dict
__
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
3858 showMenu(clientMenu
)
3860 waitForServer
.address
= address
3861 updater
.add(waitForServer
, -99)
3863 def cancelClientCallback():
3864 hideMenu(clientMenu
)
3867 updater
.remove(waitForServer
)
3868 waitForServer
.pooched
= False
3869 del waitForServer
.address
3872 def playgroundCallback():
3875 startGame(['left', 'right'])
3877 def settingsCallback():
3879 showMenu(settingsMenu
)
3881 def handicapCallback(value
):
3883 handicap
= float(value
)
3884 shelf
['handicap'] = handicap
3886 handicapBox
.text
= str(shelf
['handicap'])
3888 def aiHandicapCallback(value
):
3890 handicap
= float(value
)
3891 shelf
['aiHandicap'] = handicap
3893 aiHandicapBox
.text
= str(shelf
['aiHandicap'])
3895 def graphicsLevelCallback(value
):
3898 shelf
['graphicsLevel'] = max(1, min(3, level
))
3901 graphicsLevelBox
.text
= str(shelf
['graphicsLevel'])
3903 def settingsReturnCallback():
3904 hideMenu(settingsMenu
)
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
]
3960 hideMenu(serverMenu
)
3961 hideMenu(clientMenu
)
3962 hideMenu(settingsMenu
)
3964 postGLInits
.append(initMenus
)
3969 # Basic GL Functions {{{1
3970 def resize((width
, height
)):
3973 glViewport(0, 0, width
, height
)
3974 glMatrixMode(GL_PROJECTION
)
3976 glOrtho(0.0, width
, 0.0, height
, -1.0, 1.0)
3977 glMatrixMode(GL_MODELVIEW
)
3982 if glInitTextureRectangleARB():
3983 textureFlag
= GL_TEXTURE_RECTANGLE_ARB
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
)
3994 # Call all of the init functions that have been waiting for OpenGL to be
3996 for func
in postGLInits
:
4000 glClear(GL_COLOR_BUFFER_BIT
)
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']
4018 # Event Handling Code {{{1
4019 def onKeyDown(event
):
4021 if event
.key
== K_ESCAPE
:
4023 elif clickables
.key(event
.unicode, event
.key
, event
.mod
):
4025 elif event
.key
== K_SPACE
:
4026 createEntity(PauseMessage
, pause
= not updater
.paused
)
4027 #updater.togglePause()
4029 elif event
.key
== K_f
:
4032 elif event
.key
== K_m
:
4035 elif event
.key
== K_d
:
4036 if len(towers
) == 0:
4039 elif event
.key
== K_w
:
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
:
4061 assert event
.type == MUSIC_EVENT
4067 pygame
.time
.set_timer(PURGE_EVENT
, 2000)
4071 resize((width
,height
))
4077 startTicks
= pygame
.time
.get_ticks()
4078 lastTicks
= startTicks
4080 event
= pygame
.event
.poll()
4082 ticks
= pygame
.time
.get_ticks()
4085 if event
.type == QUIT
:
4087 if event
.type == KEYDOWN
:
4088 if onKeyDown(event
) == 0:
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
)
4104 processPendingMessages()
4107 pygame
.display
.flip()
4110 #print "fps: %d" % ((frames*1000)/(pygame.time.get_ticks()-startTicks))
4112 if __name__
== '__main__': main()