Created
November 20, 2012 03:29
-
-
Save GuyCarver/4115763 to your computer and use it in GitHub Desktop.
Pythonista version of vector graphics ripoff arcade game.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#---------------------------------------------------------------------- | |
# Copyright (c) 2012, Guy Carver | |
# All rights reserved. | |
# | |
# Redistribution and use in source and binary forms, with or without modification, | |
# are permitted provided that the following conditions are met: | |
# | |
# * Redistributions of source code must retain the above copyright notice, | |
# this list of conditions and the following disclaimer. | |
# | |
# * Redistributions in binary form must reproduce the above copyright notice, | |
# this list of conditions and the following disclaimer in the documentation | |
# and/or other materials provided with the distribution. | |
# | |
# * The name of Guy Carver may not be used to endorse or promote products # derived# | |
# from # this software without specific prior written permission.# | |
# | |
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND | |
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED | |
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE | |
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR | |
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES | |
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; | |
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON | |
# ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS | |
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
# | |
# FILE ripoffv3.py | |
# BY Guy Carver | |
# DATE 11/19/2012 06:16 PM | |
#---------------------------------------------------------------------- | |
from time import clock | |
from scene import * | |
from sound import * | |
from threading import Thread, Event | |
from math import sin, cos, pi, sqrt, acos, hypot, modf | |
from random import random, randint, shuffle, uniform, choice | |
pi2 = pi * 2 #360 deg rotation in radians. | |
hpi = pi / 2 #90 deg rotation in radians. | |
g_scale = 16 #global scale value for mobs. | |
numcrates = 9 #Number of crates to start with. | |
cratespacing = 45 #Space between crates in rows and columns. | |
numbullets = 5 #Number of bullets players may shoot at a time. | |
aibulletspeed = 500 #Speed of AI bullets in pixels/second. | |
aibulletlife = .5 #Life time of AI bullets in seconds. | |
bulletspeed = 1000 #Player bullet speed in pixles/second. | |
movesense = 2 | |
movescale = 2 | |
turnscale = .05 #convert linear movement to angular radians/second. | |
uadj = pi / 16 #maximum random amount to adjust angular velocity by when unstable (on a crate) | |
expv = 32 #Maximum explosion velocity in pixles/second. | |
expav = pi #Maximum explosion angular velocity in radians/second. | |
expdur = .75 #Amount to reduce explosion alpha by per second. | |
blastdur = 2.5 #Amound to reduce blast alph by per second. | |
blastexp = 64 #Blast expansion rate in pixels/second. | |
deadtime = 5 #Seconds player stays dead. | |
screenrad = 100 #Distance from center of screen to a corner (calculated in Scene.setup()) | |
tetherlen = 32 #Length of tether from robber to crate. | |
maxkillers = 4 #Maximum number of killer AI mobs. | |
firedelay = .5 #Delay between shots by killers when zeroed in. | |
robbercount = 6 #Number of robbers. | |
killerinterval = 4 #Waves between killer addition. | |
killerdowntime = 5 #Seconds killer remains dead before re-spawning. | |
killervel = 1.25 #Velocity scalar. | |
velbase = 100.0 #Base velocity for all AI. | |
velscale = 1.0 / 100.0 #Velocity increase per wave for robbers and killers. | |
mt = False #True = multi-thread AI update. Actually slows things down a bit. | |
debug = False #Set to true to render waypoints and such. | |
crateverts = [Point(-.5, 0), Point(0, .75), Point(.5, 0), Point(0, -.75)] | |
cratesegs = [(0,1), (1,2), (2,3), (3,0), (0,2)] | |
cratemesh = (crateverts, cratesegs) | |
playerverts = [Point(0, .75), Point(1, 0), Point(.75, -.5), Point(.5, -.25), | |
Point(-.5,-.25), Point(-.75,-.5), Point(-1, 0)] | |
playersegs = [(0,1),(1,2),(2,3),(3,4),(4,5),(5,6),(6,0)] | |
playermesh = (playerverts, playersegs) | |
robberverts = [Point(0, 1), Point(.5, .25), Point(.5, -.25), Point(0, -.75), | |
Point(-.5, -.25), Point(-.5, .25)] | |
robbersegs = [(0,1), (1,2), (2,3), (3,4), (4,5), (5,0), (0, 3)] #(1,5), (2,4)] | |
robbermesh = (robberverts, robbersegs) | |
killerverts = [Point(0,1), Point(.35,0), Point(.5, -.75), Point(0,0), | |
Point(-.5,-.75), Point(-.35,0)] | |
killersegs = [(0,1), (1,2), (2,3), (3,4), (4,5), (5,0)] | |
killermesh = (killerverts, killersegs) | |
sounds = ['Laser_6', 'Laser_3', 'Hit_3', 'Explosion_5','Clank','Ding_2'] | |
#Waypoint list for robbers. | |
#tuple of distance from screen center, angle relative to current position in radians, min,max % of velbase. | |
pathrange = [(.85, pi, Point(.8, .6)), (.5, hpi, Point(.6, .5)), (.25, hpi / 2, Point(.5, .3)), (0, 0, Point(.3, .02)), (1.5, pi, Point(.6, .3))] | |
exitwp = len(pathrange) - 1 #Index for the exit waypoint. | |
pickupwp = exitwp - 1 #Index of the crate pickup waypoint. | |
wpskipchance = 0.1 #% chance of skipping an approach waypoint. | |
wpskipfactor = 0.01 #Amount to add to wpskipchance per wave. | |
playerfilter = 1 #Player bullet collision ID. | |
aifilter = 2 #AI bullet collision ID. | |
###-1 if < 0 otherwise 1 | |
def sgn(val): return 1 if val >= 0 else -1 | |
###squared length of given vector. | |
def lensq(vec): return (vec.x * vec.x + vec.y * vec.y) | |
###Dot product of p1, p2. | |
def dot(p1, p2): return p1.x * p2.x + p1.y * p2.y | |
###Center of segment p1-p2. | |
def center(p1, p2): return Point(p1.x + p2.x / 2, p1.y + p2.y / 2) | |
###Draw line between 2 points. | |
def drawline(p1, p2): line(p1.x, p1.y, p2.x, p2.y) | |
def normalize(pnt): | |
###Normalize given Point in place and return the length. | |
len = hypot(pnt.x, pnt.y) | |
pnt.x /= len | |
pnt.y /= len | |
return len | |
def segvcircle(p1, p2, circle): | |
###Check intersection of segment p1-p2 with circle. | |
###circle is a Vector3, z = radius. | |
segv = Point(p2.x - p1.x, p2.y - p1.y) | |
#Get vector from p1 to circle. | |
cp1v = Point(circle.x - p1.x, circle.y - p1.y) | |
segvn = Point(*segv) #Make copy of vector from p1 to p2. | |
l = normalize(segvn) # and normalize. | |
sl = dot(cp1v, segvn) #Get distance from between line and circle. | |
c = Point(*p1) #Start and segment point 1. | |
#If off the end of the segment use the end point of the segment. | |
if sl >= l: c = Point(*p2) | |
elif sl > 0: #Move point on segment until segment of this point to circle | |
# is perpendicular to p1-p2 segment. | |
c.x += segvn.x * sl | |
c.y += segvn.y * sl | |
#Return true if distnce from this point to circle is < radius of circle. | |
return (c.distance(circle) < circle.z) | |
def addangle(a1, a2): | |
###return a1 + a2 making sure the result is within 0-2pi. | |
a1 += a2 | |
while a1 < 0: a1 += pi2 | |
while a1 > pi2: a1 -= pi2 | |
return a1 | |
def deltaangle(a0, a1): | |
###Return shortest angle in radians between a1 and a0. | |
### Result will -pi < delta < pi. | |
a = a1 - a0 | |
b = abs(a) | |
c = pi2 - b #Get rotation in oppisite direction. | |
if b < c: return a #if 1st rotation is shorter then return it. | |
else: return c * -sgn(a) #Otherwise return 2nd rotation setting the sign to opposite of 1st rotation. | |
def anglefromvector(vect): | |
#Return Tuple (radian rotation, vect length) represented by given vector. | |
len = hypot(vect.x, vect.y) #sqrt(lensq(vect)) | |
if len > 0: #If not 0 length. | |
a = acos(vect.y / len) #Get angle from cos. | |
#If x negative then angle is between pi-2pi | |
if vect.x < 0: a = pi2 - a | |
else: a = 0 | |
return (a, len) | |
def dampen(val, d): | |
###Return given velocity dampened by given amount not letting velocity change signs. | |
s = sgn(val) | |
val -= s * d | |
if sgn(val) != s: val = 0 #If sign changed clamp at 0. | |
return val | |
def rotpoint(a, p): | |
###Rotate point in place by angle. | |
c = cos(a) | |
s = sin(a) | |
x = p.x * c + p.y * s | |
p.y = p.y * c - p.x * s | |
p.x = x | |
def clippoint(pnt, bound): | |
###Clip pnt in place to given bound. | |
l = bound.left() | |
r = bound.right() | |
t = bound.top() | |
b = bound.bottom() | |
pnt.x = max(l, min(r, pnt.x)) | |
pnt.y = max(b, min(t, pnt.y)) | |
class mob(object): | |
###Base class representing a transformable vector graphics image. | |
def __init__(self, pos, scn, mesh): | |
object.__init__(self) | |
self.scene = scn #Scene to which mob belogs. | |
self.pos = Point(*pos) | |
self.filter = 0 #Bullet ID. | |
self.scale = g_scale #Scale of the mesh. | |
self.color = Color(.4, .8, 1) #Color of the mesh. | |
self.mesh = mesh | |
self.points = [Point(*p) for p in mesh[0]] #Make copy of points for transform. | |
self.angle = 0 | |
self.dotrans = True #If true transform the points on update. | |
self.on = False #Start turned off. | |
def reset(self): | |
self.angle = 0 #Reset the heading. | |
self.on = True #Turn on. | |
def boundcheck(self, bound): | |
###Check circle representing mob bound against given bound. | |
if not self.on: return False | |
x = bound.x - self.pos.x | |
y = bound.y - self.pos.y | |
dsq = x * x + y * y | |
r = self.scale + bound.z #Radius of mob is scale * 1. | |
return dsq <= r * r #Collide if distance squared < square of radius sum. | |
def offscreen(self): | |
###Check if mob is off screen. | |
s = self.scale | |
s2 = 2 * s | |
#Make rectangle around mob. | |
r = Rect(self.pos.x - s, self.pos.y - s, s2, s2) | |
return not self.scene.bounds.intersects(r) | |
def transformpoints(self): | |
###Transform points | |
c = cos(self.angle) | |
s = sin(self.angle) | |
#Local function to transform point. | |
def trans(p, d): | |
d.x = (p.x * c + p.y * s) * self.scale + self.pos.x | |
d.y = (p.y * c - p.x * s) * self.scale + self.pos.y | |
#Loop through points and transform into destination of self.points | |
for i, p in enumerate(self.mesh[0]): | |
trans(p, self.points[i]) | |
def draw(self): | |
###If on draw the mob. | |
if self.on: | |
stroke(*self.color) | |
stroke_weight(1) | |
if self.dotrans: self.transformpoints() | |
for p0, p1 in self.mesh[1]: #Draw each segment. | |
drawline(self.points[p0], self.points[p1]) | |
###Turn off. | |
def kill(self): self.on = False | |
class explosion(object): | |
###Object to represent an explosion consisting of the exploded | |
### mesh segments and a blast circle. | |
def __init__(self, mob): | |
object.__init__(self) | |
c = mob.color | |
self.pos = Point(*mob.pos) | |
self.alpha = 1 | |
self.color = Color(c.r, c.g, c.b, 1) | |
self.angle = mob.angle | |
self.blast = 4 #Stroke weight of blast circle. | |
numsegs = len(mob.mesh[1]) | |
###Make and explosion segment from the given segment index. | |
###Returns a Tuple of Vector3 for point and angle, vector of segment, Vector3 of velocities). | |
def make(index): | |
xv = uniform(-expv, expv) #Set random x,y velocities. | |
yv = uniform(-expv, expv) #random() * expv * 2 - expv | |
av = uniform(-expav, expav) #Set random angular velocity. | |
i0, i1 = mob.mesh[1][index] #Get point indices for this segment. | |
p0 = mob.points[i0] #Get point 0. | |
p1 = Point(*mob.points[i1]) #Get copy of point 1. | |
p1.x -= p0.x #Convert point 1 into vector to point1 from point0. | |
p1.y -= p0.y | |
return (Vector3(p0.x,p0.y,0), Point(p1.x, p1.y), Vector3(xv, yv, av)) | |
#Make list of segments representing exploded mesh. | |
self.segs = [make(i) for i in xrange(numsegs)] | |
set_volume(0.5) | |
play_effect(sounds[3]) #Play explosion mesh. | |
def update(self, dt): | |
###Update explosion and return true when off. | |
on = self.color.a > 0 #On as long as alpha isn't 0. | |
if on: | |
self.color.a = max(0, self.color.a - expdur * dt) | |
self.alpha = max(0, self.alpha - blastdur * dt) | |
self.blast += blastexp * dt | |
for p0, _, v in self.segs: | |
p0.x += v.x * dt | |
p0.y += v.y * dt | |
p0.z = addangle(p0.z, v.z * dt) | |
return not on | |
def draw(self): | |
###Draw the explosion. | |
fill(0,0,0,0) #No fill color. | |
if self.alpha: | |
width = self.alpha * 32 #The stroke width is % of 32 pixels. | |
asq = self.alpha * self.alpha | |
#Red alwasy 1, green = 1.5 * alpha, blue = 1 * alpha squared. | |
#This will make the color animate from white to yellow to orange. | |
stroke(1,1.5 * self.alpha,1 * asq, self.alpha) | |
stroke_weight(width) | |
x = self.pos.x - self.blast #Get lower left corner of circle. | |
y = self.pos.y - self.blast | |
wh = self.blast * 2 #Diameter. | |
ellipse(x, y, wh, wh) #Draw the explosion circle. | |
stroke(*self.color) | |
stroke_weight(1) | |
#Now draw the segments. | |
for p0, p1d, v in self.segs: | |
p1 = Point(p1d.x, p1d.y) | |
rotpoint(p0.z, p1) #Rotate the direction by the angle. | |
p1.x += p0.x #p1 = p0 + direction. | |
p1.y += p0.y | |
drawline(p0, p1) | |
class crate(mob): | |
###Mob rebresenting a crate the robbers will attempt to steal. | |
def __init__(self, pos, scn): | |
mob.__init__(self, pos, scn, cratemesh) | |
self.color = Color(0.80, 0.80, 0.20) | |
self.reset() | |
def reset(self): | |
mob.reset(self) | |
self.targeted = 0 #No robbers are targeting. | |
self.tethered = None #No robber is tethered to the crate. | |
self.dotrans = False #Don't transform the points each frame. | |
self.transformpoints() #Transform the points 1 time. | |
def kill(self): | |
###Kill the crate. | |
mob.kill(self) | |
self.scene.killcrate(self) #Remove the create from the scene. | |
set_volume(0.5) | |
play_effect(sounds[4]) | |
class bullet(object): | |
###Object representing a shot bullet. | |
def __init__(self, owner): | |
object.__init__(self) | |
self.owner = owner #Owner of this bullet. | |
self.pos = Point(0,0) | |
self.vel = Point(0,0) | |
self.color = Color(1, 0.7, 0.7) | |
self.life = 0 #Current life of the bullet. | |
self.speed = bulletspeed | |
self.lifespan = 1 #Maximum life span of bullet in seconds. | |
###Return filter ID of the owner of this bullet. | |
def getfilter(self): return self.owner.filter | |
def turnon(self, pos, vel): | |
###Turn the bullet on. | |
self.life = self.lifespan #Reset the life span. | |
self.pos = pos | |
self.vel.x = vel.x * self.speed | |
self.vel.y = vel.y * self.speed | |
def update(self, scn, dt): | |
###Update bullet and return true when turned off. | |
if self.life: #If any life left. | |
self.life = (max(0, self.life - dt)) #Reduce life. | |
if self.life: #If still on. | |
prev = Point(*self.pos) | |
self.pos.x += self.vel.x * dt | |
self.pos.y += self.vel.y * dt | |
#Check segment representing bullet traversal against mobs in scene. | |
if scn.checkbullet(prev, self.pos, self.owner): | |
self.life = 0 #Hit something so turn off. | |
if not self.life: | |
self.owner.shotcount -= 1 #reduce shot count on owner if bullet turned off. | |
return True | |
return False | |
def draw(self): | |
###Draw bullet if on. | |
if self.life: | |
stroke(*self.color) | |
stroke_weight(4) | |
x1 = self.pos.x | |
x2 = x1 + 2 | |
y1 = self.pos.y | |
y2 = y1 + 2 | |
line(x1, y1, x2, y2) | |
class machine(mob): | |
###Base class for player and AI machines. You will note some | |
### features that are not used by all machines. They are here | |
### just in case. For instance making robbers shoot. | |
def __init__(self, pos, filter, scn, mesh, bulletcount): | |
mob.__init__(self, pos, scn, mesh) | |
self.filter = filter #Set bullet ID. | |
self.brk = 200 #Break amount in pixels/second. | |
self.brka = pi * 2 #Angular break amount int rads/second. | |
self.shotcount = 0 #Number of shots. | |
#Create bullets. | |
self.bullets = [bullet(self) for i in xrange(bulletcount)] | |
self.shoot = sounds[0] #Set shooting sound. | |
self.shootv = 0.6 #Shot sound volume. | |
###Reset the mob. | |
def reset(self): | |
mob.reset(self) | |
self.avel = 0 | |
self.vel = Point(0, 0) | |
self.wrap = True #Wrap mob around on screen as opposed to cliping on edges. | |
###Apply breaks to liner velocity. | |
def slowdown(self, dt): self.vel.y = dampen(self.vel.y, self.brk * dt) | |
###Get screen position and shot direction. | |
def shotpos(self): | |
p = Point(*self.points[0]) #Shot comes out of point 0. | |
v = Point((p.x - self.pos.x) / self.scale, (p.y - self.pos.y) / self.scale) | |
return (p, v) | |
###Shoot a bullet. | |
def fire(self): | |
if self.on and self.shotcount < len(self.bullets): | |
for b in self.bullets: | |
if not b.life: #Find unused bullet. | |
b.turnon(*self.shotpos()) | |
self.shotcount += 1 | |
set_volume(self.shootv) | |
play_effect(self.shoot) | |
self.scene.activebullets.append(b) #Add bullet to scene. | |
return | |
###Update machine. | |
def update(self, dt): | |
if self.on: | |
v = Point(*self.vel) | |
rotpoint(self.angle, v) | |
self.pos.x += v.x * dt #Add velocity to position. | |
self.pos.y += v.y * dt | |
if self.wrap: #If wraping on screen then do so. | |
sz = self.scene.size | |
if self.pos.x > sz.w: | |
self.pos.x -= sz.w | |
elif self.pos.x < 0: | |
self.pos.x += sz.w | |
if self.pos.y > sz.h: | |
self.pos.y -= sz.h | |
elif self.pos.y < 0: | |
self.pos.y += sz.h | |
#Adjust direction by angular velocity. | |
self.angle = addangle(self.angle, self.avel * dt) | |
class aimachine(machine): | |
###Base class for the AI controlled machines. | |
def __init__(self, scn, mesh, numbullets): | |
machine.__init__(self, Point(0,0), aifilter, scn, mesh, numbullets) | |
self.on = False #Start turned off. | |
self.brka = 0 #No angular velocity break. | |
def reset(self, pos, angle): | |
machine.reset(self) | |
self.wrap = False #Don't wrap AI mobs on screen. | |
self.pos = pos | |
self.angle = angle | |
self.minvel = 0 | |
self.maxvel = 0 | |
self.wp = Point(0,0) | |
self.wpn = Point(0,0) | |
self.nextwaypoint() #Set 1st waypoint. | |
###Return base velocity adjust by the wave count. | |
def basevel(self): return velbase + (velbase * float(self.scene.wave) * velscale) | |
def updatevels(self, dt): | |
###Update the linear and angular velocities. | |
vect = Point(*self.wp) | |
vect.x -= self.pos.x | |
vect.y -= self.pos.y | |
a, l = anglefromvector(vect) #Get angle, distance to target. | |
self.avel = deltaangle(self.angle, a) #Get delta angle to target. | |
self.vel = Point(0, max(self.minvel, min(self.maxvel, l))) #Set velocity based on target distance. | |
def checkdest(self): | |
###Check to see if we have reached target waypoint. | |
delta = Point(self.pos.x - self.wp.x, self.pos.y - self.wp.y) | |
l = lensq(delta) | |
#If distance < minimum velocity. | |
if l < (self.minvel * self.minvel): | |
d = dot(delta, self.wpn) #See if we are on other side of waypoint normal. | |
return d <= 0 | |
return False | |
def update(self, dt): | |
###Update the AI machine if on. return 1 if updated else 0. | |
if self.on: | |
self.state(dt) #Call the state function. | |
machine.update(self, dt) #Call base update method. | |
return 1 | |
return 0 | |
class robber(aimachine): | |
###Robber AI machine. | |
def __init__(self, scn): | |
aimachine.__init__(self, scn, robbermesh, 0) | |
def reset(self, pos, angle, tgt): | |
self.state = self.approachstate #Start with target approach state. | |
self.wpindex = -1 #Start with waypoint index -1 so 1st index will be 0 when NextWaypoint is called. | |
self.approacha = 0 #Approach angle of 0. | |
self.target = tgt #Target crate. | |
if tgt: tgt.targeted += 1 | |
aimachine.reset(self, pos, angle) | |
def setexit(self) : | |
###Set exit state and tether to target crate. | |
self.state = self.exitstate | |
self.wrap = False | |
tgt = self.target | |
if not tgt.tethered: #If target not tethered then tether to it. | |
b = Vector3(self.pos.x, self.pos.y, self.scale) | |
if tgt.boundcheck(b): #Make sure in range of target before tethering. | |
tgt.tethered = self | |
tgt.dotrans = True #start crate updating transform as we are going to move it. | |
return | |
#if didn't tether then follow crate (Another robber is tethered to it). | |
self.wp = tgt.pos #Reference target position and follow that. | |
# If target moves so will our way point. | |
self.state = self.followstate | |
def done(self): | |
###The robber has exited the screen so turn off. | |
self.on = False | |
#If tethered to a crate the kill the crate. | |
if self.target and self.target.tethered is self: | |
self.target.kill() | |
def kill(self): | |
###Kill the robber. | |
mob.kill(self) | |
tgt = self.target | |
if tgt: #If targeting a create stop targeting. | |
self.target = None | |
tgt.targeted -= 1 | |
if tgt.tethered is self: #If pulling a crate then stop pulling. | |
tgt.tethered = None | |
tgt.dotrans = False #Make crate not update it's transforms as it is no longer moving. | |
def pullcrate(self, dt): | |
###Pull the crate. | |
tgt = self.target | |
if tgt and tgt.tethered is self: | |
if tgt.offscreen(): #If target off screen then kill robber/crate. | |
self.done() | |
else: #Move crate. | |
p = tgt.pos | |
d = Point(self.pos.x - p.x, self.pos.y - p.y) | |
dm = Point(abs(d.x) - tetherlen, abs(d.y) - tetherlen) | |
if dm.x > 0: | |
p.x += dm.x * sgn(d.x) | |
if dm.y > 0: | |
p.y += dm.y * sgn(d.y) | |
elif self.offscreen(): #If robber off screen then stop it. | |
self.done() | |
#Get screen location of tether point on mesh. | |
def tetherpos(self): return self.points[3] #Point 3 is tether point. | |
def nextwaypoint(self): | |
###Set next waypoint. | |
self.wpindex += 1 | |
#Run chance of skipping waypoint but only up to pickup waypoint. | |
while self.wpindex < pickupwp and (random() < self.scene.wpskipchance): | |
self.wpindex += 1 | |
self.setwaypoint() #Set the waypoint position and normal. | |
if self.wpindex == exitwp: #If we are at the exit waypoint change states. | |
self.setexit() | |
def setwaypoint(self): | |
###Set the waypoint. | |
if self.target: | |
rad, da, vels = pathrange[self.wpindex] | |
v = self.basevel() | |
self.maxvel = vels.x * v #Set maximum/minimum velcoties as % of base velocity. | |
self.minvel = vels.y * v | |
#Adjust current approach angle by delta angle from next waypoint. | |
self.approacha = addangle(self.approacha, uniform(-da, da)) | |
self.wp = Point(0, rad * screenrad) | |
rotpoint(self.approacha, self.wp) | |
self.wp.x += self.target.pos.x | |
self.wp.y += self.target.pos.y | |
if self.wpindex < exitwp: #don't clip exit waypoint as it takes us off screen. | |
clippoint(self.wp, self.scene.bounds) #Clip point to screen. | |
#Waypoint normal from vector of pos-wp. | |
self.wpn = Point(self.pos.x - self.wp.x, self.pos.y - self.wp.y) | |
normalize(self.wpn) | |
def followstate(self, dt): | |
###Watch target and if no longer tethered then tether to it. | |
if not self.target.tethered: #If not tethered. | |
self.wpindex = pickupwp #Re-run pickup waypoint. | |
self.state = self.approachstate #Switch back to approach state. | |
self.setwaypoint() #Set pickup waypoint. | |
else: | |
self.exitstate(dt) #Run exit state. | |
def approachstate(self, dt): | |
###Approach target through series of waypoins. | |
if self.checkdest(): #Check if reached waypoint. | |
self.nextwaypoint() #Next waypoint. | |
self.updatevels(dt) #Update the velocities to get to waypoint. | |
machine.update(self, dt) | |
def exitstate(self, dt): | |
self.updatevels(dt) #Update velocities to get to waypoint. | |
self.pullcrate(dt) #Pull the crate if tethered. NOTE: We could make a | |
# exitpull state and not have to check if tethered in pullcrate(). | |
def draw(self): | |
###Draw the robber if on. | |
if self.on: | |
mob.draw(self) | |
tgt = self.target | |
#If tethered then draw tether line. | |
if tgt and tgt.tethered is self: | |
stroke(1,1,1,.5) | |
stroke_weight(1) | |
tp = self.tetherpos() | |
line(tp.x, tp.y, tgt.pos.x, tgt.pos.y) | |
if debug: #If debug then draw the current waypoint. | |
v = Point(self.wpn.x * 16, self.wpn.y * 16) | |
v.x += self.wp.x | |
v.y += self.wp.y | |
stroke(1,0,0,1) | |
stroke_weight(2) | |
line(self.wp.x, self.wp.y, v.x, v.y) | |
stroke(1,1,1,1) | |
stroke_weight(1) | |
line(self.wp.x, self.wp.y, self.wp.x + 1, self.wp.y + 1) | |
tint(1,1,0,1) | |
class killer(aimachine): | |
###Hunter Killer AI machine. | |
def __init__(self, scn): | |
aimachine.__init__(self, scn, killermesh, 2) | |
self.color = Color(1.00, 0.00, 1.00) | |
self.shoot = sounds[2] | |
self.downtime = killerdowntime | |
self.state = self.down | |
def reset(self, pos, angle): | |
aimachine.reset(self, pos, angle) | |
v = killervel * self.basevel() | |
self.minvel = v | |
self.maxvel = v | |
self.firedelay = 0 #Reset the fire delay timer. | |
self.state = self.hunt #Start hunding. | |
#Set bullet speed and lifespan again just in case they have changed. | |
for b in self.bullets: | |
b.speed = aibulletspeed | |
b.lifespan = aibulletlife | |
def nextwaypoint(self): | |
###Randomly create a new waypoint on screen. | |
self.wp = Point(randint(0, self.scene.size.w), randint(0, self.scene.size.h)) | |
self.wpn = Point(self.pos.x - self.wp.x, self.pos.y - self.wp.y) | |
normalize(self.wpn) | |
def kill(self): | |
self.downtime = killerdowntime #Reset the down time countdown timer. | |
aimachine.kill(self) | |
self.state = self.down | |
def down(self, dt): | |
###Down state, remain so until downtime is up. | |
self.downtime -= dt | |
if self.downtime <= 0: | |
p, a = self.scene.startpos() | |
self.reset(p, a) | |
def checkfire(self, dt): | |
#Check to see if we should fire. | |
if self.firedelay > 0: #Wait until delay timer is up. | |
self.firedelay = max(0, self.firedelay - dt) | |
return | |
for p in self.scene.pl: #Check each player to see if in front of killer. | |
v = Point(p.pos.x - self.pos.x, p.pos.y - self.pos.y) | |
a1, _ = anglefromvector(v) | |
da = deltaangle(self.angle, a1) | |
if abs(da) < 0.07: #If delta angle is within range shoot. | |
self.firedelay = firedelay | |
self.fire() | |
def hunt(self, dt): | |
###State to keep going to random waypoints and checking for fire opportunities. | |
if self.checkdest(): | |
self.nextwaypoint() | |
self.updatevels(dt) | |
self.checkfire(dt) | |
machine.update(self, dt) | |
def update(self, dt): self.state(dt) | |
###Machine to represent a player. | |
class player(machine): | |
def __init__(self, pos, scn, dir): | |
machine.__init__(self, pos, playerfilter, scn, playermesh, numbullets) | |
self.dir = dir #+ or - 1. | |
self.startpos = Point(*pos) | |
self.control = 0 | |
self.unstable = False | |
self.reset() | |
###Reset the player to original position. | |
def reset(self): | |
machine.reset(self) | |
self.dead = 0 | |
self.pos.x = self.startpos.x | |
self.pos.y = self.startpos.y | |
self.vel.y = self.scale * 8 #Start out with a velocity to move onto screen. | |
self.angle = -hpi * self.dir | |
self.destangle = self.angle | |
###Kill the player. | |
def kill(self): | |
mob.kill(self) | |
self.dead = deadtime #Start dead timer. | |
###Move the player. | |
def move(self, dt): | |
if self.control: | |
da = deltaangle(self.angle, self.destangle) | |
if da: #If delta angle adjust angular velocity. | |
av = self.avel + da | |
s = sgn(av) | |
self.avel = min(pi, abs(av)) * s | |
def update(self, dt): | |
if self.on: #If on then update. | |
self.move(dt) | |
self.unstable = False | |
b = Vector3(self.pos.x, self.pos.y, self.scale * .75) | |
#Check if colliding with crate, if so add instability to turning. | |
for m in self.scene.crates: | |
if m.boundcheck(b): | |
self.unstable = True | |
break | |
#Dampen angular velocity. | |
self.avel = dampen(self.avel, self.brka * dt) | |
#If not controlling linear velocity dampen that as well. | |
if not self.control: | |
self.slowdown(dt) | |
machine.update(self, dt) | |
else: #Death of player is different from killer which uses state functions | |
# simply to show another way of doing it. I prefer states. | |
self.dead -= dt | |
if self.dead <= 0: #As soon as dead time up reset. | |
self.reset() | |
###Calculate direction/velocity from movement button touch. | |
def movetouch(self, touch): | |
deltax = (touch.location.x - self.movepos.x) | |
deltay = (touch.location.y - self.movepos.y) | |
a, l = anglefromvector(Point(deltax, deltay)) | |
self.destangle = a | |
if l > movesense: | |
self.vel.y = l * movescale | |
###Check left/right button pressed, return true if handled. | |
def touch_began(self, touch): | |
if self.on: | |
tl = touch.location | |
if tl in self.moverect: #If movement rectangle touched. | |
self.movetouch(touch) | |
self.control += 1 | |
return True | |
if tl in self.shootrect: #If fire rectangle touched. | |
self.fire() | |
return True | |
return False | |
def touch_moved(self, touch): | |
if self.on: | |
tl = touch.location | |
if tl in self.moverect: #If movement rectangle touched. | |
self.movetouch(touch) | |
return True | |
tpl = touch.prev_location | |
if tpl in self.moverect: #If previous location was in movement rectangle then stop movement. | |
self.control = max(0, self.control - 1) | |
return True | |
if tl in self.shootrect: #If fire rectangle touched calculate anguler velocity. | |
#NOTE: We don't check if previous location was in button, we just assume so. | |
delta = touch.location.y - touch.prev_location.y | |
av = delta * turnscale * self.dir | |
if self.unstable and av: #If unstable (on crate) add random angular velocity. | |
r = uniform(-uadj, uadj) | |
self.angle = addangle(self.angle, r) | |
self.avel += av | |
return True | |
return False | |
###Touch ends. | |
def touch_ended(self, touch): | |
if self.on: | |
tl = touch.location | |
if tl in self.moverect: #If no longer touching movement button decrement control count. | |
self.control = max(0, self.control - 1) | |
return True | |
return tl in self.shootrect #Return handled if intersects fire rectangle. | |
return False | |
class MyScene(Scene): | |
###Main scene. | |
def setup(self): | |
global screenrad | |
# This will be called before the first frame is drawn. | |
pos = self.bounds.center() | |
cp = Point(*pos) | |
pos.x = 0 | |
w = self.size.w | |
h = self.size.h | |
w3 = w / 5 #1/5th Screen width. | |
w6 = w3 / 2 #1/10th Screen width. | |
screenrad = hypot(w, h) * .5 #Set screen radius. | |
self.pl = [] | |
plr = player(pos, self, 1.0) #Create player 1. | |
self.pl.append(plr) | |
plr.color = Color(1.00, 0.50, 0.00) | |
plr.moverect = Rect(w - w3, 0, w3, w3) #Set movement button rectangle. | |
plr.movepos = plr.moverect.center() | |
plr.shootrect = Rect(w - w6, w3 * 2.25, w6, w3) #Set fire button rectangle. | |
pos.x = w | |
plr = player(pos, self, -1.0) #Create player 2. | |
self.pl.append(plr) | |
plr.color = Color(0.40, 1.00, 0.40) | |
plr.shoot = sounds[1] #Change fire sound for player 2. | |
plr.shootv = 0.3 #Set lower volume. | |
plr.moverect = Rect(0, h - w3, w3, w3) #Set movement button rectangle. | |
plr.movepos = plr.moverect.center() | |
plr.shootrect = Rect(0, h - w3 * 2.25 - w3, w6, w3) #Set fire button rectangle. | |
self.activebullets = [] #Array of active bullets. | |
self.explosions = [] #Array of active explosions. | |
self.controlalpha = 0.45 #Control rectangle alpha. | |
self.wave = 0 #Wave counter. | |
self.wpskipchance = wpskipchance | |
self.killerinterval = killerinterval | |
self.state = self.run | |
self.gameovertxt = render_text('You\'ve been Ripped Off!', 'Copperplate', 32) | |
self.pausetxt = render_text('Pause', 'Copperplate', 28) | |
c = self.bounds.center() | |
hh = self.pausetxt[1].w / 2 | |
self.pauserect = Rect(c.x - hh, 0, hh * 2, self.pausetxt[1].h) | |
if mt: | |
self.udstart = Event() | |
self.uddone = Event() | |
self.udthread = Thread(target=self.udthread) | |
self.udthread.start() | |
for s in sounds: load_effect(s) #pre-load sound effects. | |
def makecrate(i): | |
###Local function to create a crate mob. | |
x = (int(i / 3) - 1) * cratespacing | |
y = ((i % 3) - 1) * cratespacing | |
cr = crate(Point(cp.x + x, cp.y + y), self) | |
return cr | |
self.crates = [makecrate(i) for i in xrange(numcrates)] | |
self.robbers = [robber(self) for i in xrange(robbercount)] | |
self.killers = [] #Start with 0 killers. | |
self.numrobbers = 0 | |
###Remove crate from the crates array. | |
def killcrate(self, cr): self.crates.remove(cr) | |
def adjustdifficulty(self): | |
###Adjust difficulty level based on # of waves. | |
self.wpskipchance = wpskipchance + (self.wave * wpskipfactor) | |
nk = min(self.wave / self.killerinterval, maxkillers) | |
nk -= len(self.killers) | |
while nk > 0: #Add killers. | |
self.killerinterval += 2 #Adjust wave interval for next killer. | |
nk -= 1 | |
k = killer(self) | |
self.killers.append(k) | |
def checkwave(self, dt): | |
###Check to see if wave is complete. | |
if not self.numrobbers: #If no more live robbers. | |
if len(self.crates): #If still some live crates. | |
self.wave += 1 #New wave. | |
self.scoretxt = render_text('wave: ' + str(self.wave), 'Copperplate', 28) | |
self.adjustdifficulty() | |
self.startrobbers() #Restart all of the robbers. | |
else: | |
self.state = self.gameover | |
def checkbullet(self, p1, p2, owner): | |
###Check bullet collision, return true if hit something. | |
hit = False | |
def checkcol(amob): | |
###Local function to | |
if amob.on: | |
circle = Vector3(amob.pos.x, amob.pos.y, amob.scale) | |
if segvcircle(p1, p2, circle): #If circle hit then create an explosion. | |
self.explosions.append(explosion(amob)) | |
amob.kill() #Kill the hit object. | |
return True | |
return False | |
#If not a player ID then check player collisions. | |
if owner.filter != playerfilter: | |
for p in self.pl: | |
hit |= checkcol(p) | |
elif owner.filter != aifilter: #If not AI ID. | |
for k in self.killers: #Check killers 1st. | |
if checkcol(k): | |
return True #If killer took hit exit. | |
for r in self.robbers: #Check robbers, 1 bullet can hit many. | |
hit |= checkcol(r) | |
return hit | |
def startpos(self): | |
###Get a random start position just off screen. | |
a = uniform(0, pi2) #Get angle from 0 to 2pi. | |
p = Point(0, screenrad) | |
rotpoint(a, p) | |
c = self.bounds.center() | |
p.x += c.x | |
p.y += c.y | |
return p, a | |
def startrobbers(self): | |
###Start all robbers. | |
for r in self.robbers: | |
if not r.on: | |
c = choice(self.crates) #pick a target. | |
p, a = self.startpos() | |
r.reset(p, a, c) | |
def udrobbers(self): | |
###Update robbers and set # of live robbers. | |
self.numrobbers = 0 | |
for r in self.robbers: | |
self.numrobbers += r.update(self.dt) | |
def udthread(self): | |
###Mutli-threaded update. | |
while True: | |
self.udstart.wait() #Wait for main thread to signal update. | |
self.udstart.clear() #Clear signal. | |
self.udrobbers() #Update robbers. | |
self.uddone.set() #Signal update done. | |
def update(self, dt): | |
###Update the scene. | |
if debug: | |
self.t0 = clock() | |
if mt: self.udstart.set() #If multi-threaded signal update start. | |
#if no screen touches reset control counters for safety (they can get out of sync). | |
if len(self.touches) == 0: | |
for p in self.pl: | |
p.control = 0 | |
for p in self.pl: #Update players. | |
p.update(dt) | |
for k in self.killers: #Update killers. | |
k.update(dt) | |
#If not multi-threaded update robbers here. | |
if not mt: self.udrobbers() | |
#Update active bullets. | |
for b in self.activebullets: | |
if b.update(self, self.dt): | |
self.activebullets.remove(b) | |
#Update active explosions. | |
for e in self.explosions: | |
if e.update(dt): | |
self.explosions.remove(e) | |
if debug: | |
self.t1 = clock() | |
###Pause state does nothing. | |
def paused(self): pass | |
def gameover(self): | |
###Game over state. | |
tint(0.00, 1.00, 0.00) | |
c = self.bounds.center() | |
s = self.gameovertxt[1] | |
image(self.gameovertxt[0], c.x - (s.w / 2), c.y, *s) | |
self.update(self.dt / 4) | |
def run(self): | |
###Main state. | |
dt = min(0.1, self.dt) | |
self.checkwave(dt) | |
self.update(dt) | |
def drawcontrols(self): | |
###Draw control boxes. | |
if self.controlalpha: | |
fill(0,0,0,0) | |
stroke(0, 0, .5, self.controlalpha) | |
stroke_weight(2) | |
for p in self.pl: | |
rect(*p.moverect) | |
stroke(.5, 0, .5, self.controlalpha) | |
for p in self.pl: | |
rect(*p.shootrect) | |
def drawscore(self): | |
###Draw the score and pause text. | |
tint(1,0,0) | |
c = self.bounds.center() | |
s = self.scoretxt[1] | |
image(self.scoretxt[0], c.x - (s.w / 2), self.size.h - s.h, *s) | |
clr = Color(1,1,0) if self.state == self.paused else Color(0.80, 0.40, 1) | |
if debug: #In debug draw timing text. | |
tmg = int((self.t1 - self.t0) * 1000.0) | |
text(str(tmg), x=20, y=20, alignment=9) | |
tint(*clr) | |
s = self.pausetxt[1] | |
image(self.pausetxt[0], c.x - (s.w / 2), 0, *s) | |
def draw(self): | |
###Draw all objects in the scene. | |
background(0, 0, 0) | |
self.state() #Update state. | |
self.drawscore() | |
self.drawcontrols() | |
if mt and self.state == self.run: #Wait for other thread to finish. | |
self.uddone.wait() | |
self.uddone.clear() | |
for cr in self.crates: cr.draw() | |
for p in self.pl: p.draw() | |
for k in self.killers: k.draw() | |
for b in self.activebullets: b.draw() | |
for r in self.robbers: r.draw() | |
for e in self.explosions: e.draw() | |
def checkpause(self, loc): | |
###Check to see if pause button pressed. | |
if loc in self.pauserect: | |
play_effect(sounds[5]) | |
if self.state == self.paused: | |
self.state = self.prevstate | |
else: | |
self.prevstate = self.state | |
self.state = self.paused | |
def touch_began(self, touch): | |
###Handle touch events. | |
for p in self.pl: | |
if p.touch_began(touch): | |
return | |
def touch_moved(self, touch): | |
###Handle touch move events. | |
for p in self.pl: | |
if p.touch_moved(touch): | |
return | |
def touch_ended(self, touch): | |
###Handle touch end events. | |
for p in self.pl: | |
if p.touch_ended(touch): | |
return | |
self.checkpause(touch.location) #Check for pause button press. | |
run(MyScene(), LANDSCAPE) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I made a javascript version of this game (with the help of Grok3).
The repository is here: RichardPotthoff/ripoff
And the game is here: ripoff