# The 200 line 3d networked shooter
# Copyright 2000 by Sebastien Loisel
# Permission to use, modify and redistribute this file is granted to all
# without restrictions.
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

from libgtc import *  # Get GTC
from whrandom import *# Random number generator
from time import *    # To keep track of time
from math import *    # For the linear algebra

scene=Scene()         # Make a new scene
scene.context=defaultContext() # Give it a default context
table=Table("",2)     # Load a table of primitives (eg, the ship)
seed()                # Seed the random number generator

# A duck is one ship in the universe
class Duck:
    speed=0.0         # Default speed is zero
    trigger=0         # Trigger is when the player wants to shoot
    up=0              # Up is when you press cursor up
    down=0            # Cursor down
    left=0            # Cursor left
    right=0           # Cursor right
    die=0             # This is a timer. If it reaches zero and diesoon==1
                      # then this duck is dead.
    diesoon=0         # Set to one if you want to use the timeout thing (die)
    autosteer=0       # This is for computer controlled players
    acks=0            # And this is for human players
    ackmax=1          # same
    def __init__(self, HP, S, z):
        self.hp=HP    # Set initial hit points
        self.size=S   # And size (radius) for collision detection
        self.pos=Xform() # Position is a transform node
        self.pos.child=table.index(z) # Child of position is ship or bullet
        self.rep=scene.add(self.pos) # Add self to the scene

# Create a new player
def player():
    ret=Duck(10,1,2)  # 10 hit points, radius=1, table[2] is ship
    (x,y,z)=(random()*20,random()*20,random()*20)
    X=xlat(x,y,z)     # Get a random position in the cube [0,20]x[0,20]x[0,20]
                      # Also get a random rotation
    R=rot(random()-.5,random()-.5,random()-.5,
          random()*6.29)
    # Set the orientation to the rotation and translation we've got
    ret.pos.A=mul(X,R)
    return ret

# Create a computer controlled ship
def computer():
    ret=player()      # Get a ship as normal
    ret.autosteer=1   # Set autosteer
    return ret

def vecadd(a,b):      # Add two 4-vectors but set the fourth component to 1
    return (a[0]+b[0],a[1]+b[1],a[2]+b[2],1.0)

def vecsub(a,b):      # Sub two 4-vectors but set the fourth component to 1
    return (a[0]-b[0],a[1]-b[1],a[2]-b[2],1.0)

def norm(a):          # Norm of a 4-vector, ignoring the fourth component
    return sqrt(a[0]*a[0]+a[1]*a[1]+a[2]*a[2])

def scale(s,v):       # Scale a 4-vector but set the fourth component to 1
    return (s*v[0],s*v[1],s*v[2],1.0)

def shoot(pl):        # Ship shoots a bullet
    ret=Duck(1,.1,1)  # Get a bullet (hp=1, size=0.1, table[1] is a bullet)
    ret.speed=5.0     # Make it fast
                      # Make its position same as that of pl, but move it
                      # forward a bit
    ret.pos.A=(pl.pos.A[0],pl.pos.A[1],pl.pos.A[2],
               vecadd(pl.pos.A[3],scale(1.5,pl.pos.A[2])))
    ret.diesoon=1     # Make bullets only last a little while
    ret.die=2.0       # Two seconds to be precise
    return ret

def collide(world):   # Check for collisions
    zoo=world[:]      # Make a copy of the world to iterate over
    for x in zoo:     # Check all pairs (x,y) for collision
        for y in zoo:
            if x==y:  # If x==y, ignore
                continue
                      # Else, calculate the distance between x and y
            foo=norm(vecsub(x.pos.A[3],y.pos.A[3]))
                      # If x and y are closer than they should
                      # and they are still alive, cause damage
            if foo<x.size+y.size and x.hp>0 and y.hp>0 :
                temp=x.hp
                x.hp=x.hp-y.hp
                y.hp=y.hp-temp
    for x in zoo:     # Now for every ship in the world
        if x.hp<=0:   # If it's dead
            x.rep.dead=1 # Remove it
            world.remove(x)

# Move all ships according to speed and such
def animate(world,dt):
    foo=world[:]      # Duplicate world
    for x in foo:     # For each ship
        # Remove it if it's dead
        if x.rep.dead==1:
            world.remove(x)
            continue
        # Advance the ship
        x.pos.A=(x.pos.A[0],x.pos.A[1],x.pos.A[2],
                 vecadd(x.pos.A[3],scale(x.speed*dt,x.pos.A[2])))
        # Steer (up/down, left/right)
        A=rot(1,0,0,(x.down-x.up)*dt)
        B=rot(0,1,0,(x.left-x.right)*dt)
        x.pos.A=mul(x.pos.A,mul(A,B))
        # Shoot
        if x.trigger==1:
            x.trigger=0
            world.append(shoot(x))
        # If this is a bullet, need to count time
        if x.diesoon==1:
            if x.die<=0: # If it's timed out
                x.rep.dead=1 # kill it
                world.remove(x)
            else:
                x.die=x.die-dt # Decrease time to live

# This handles keypresses and keyreleases
# I apologize for the weird hexadecimal numbers. You can find them
# in /usr/X11R6/include/X11/keysymdef.h, I should make these more reasonable.
def setkey(duck,k,state):
    if k==0xff52:        # up
        duck.up=state
    elif k==0xff53:      # right
        duck.right=state
    elif k==0xff54:      # down
        duck.down=state
    elif k==0xff51:      # left
        duck.left=state
    elif k>=0x31 and k<=0x39: # 0x31 is 1, 0x39 is 9, this is for speed
        duck.speed=(k-0x31)/3.0
    elif k==0x20:        # 0x20 is space
        duck.trigger=state

# Do everything
def mainloop():
    people=[]
    scene.add(table.index(5)) # weird animated ship is client-side script
    world=[]             # make an empty world
    for x in range(10):  # Add 10 computer controlled opponents
        world.append(computer())
    host=Host(4000)      # Create a new host
    t=time()             # Get the current time
    while 1:             # Then, repeat forever the main loop
        host.process(.0) # Process any communication from clients
        event=host.get_event()             # If there are new events
        while event != None:               # Process them
            who=event["who"]               # This is the source of the event
            if event["type"]==3:           # type 3 means new client connected
                who.data=player()          # Make a new ship for this client
                who.req_ack()              # Client should send an ack
                who.data.acks=who.data.acks+1
                who.table("")              # Tell the client which table to use
                who.end()                  # End this communication
                world.append(who.data)     # Add it to the world
                people.append(who)
            else:                          # It's not a new client
                if event["type"]==4:       # Client disconnected, clean up
                    print "poor client dead"
                    who.data.rep.dead=1
                    people.remove(who)
                if who.data.rep.dead==1:   # If its ship is dead
                    who.dead=1             # Kill the client
                else:                      # Otherwise, the ship is not dead
                    if event["type"]==1:   # If this is a keypress, process it
                        setkey(who.data,event["key"],1)
                    elif event["type"]==2: # Keyrelease, process it
                        setkey(who.data,event["key"],0)
                    elif event["type"]==5: # Client sent an ack, send new frame
                        who.data.acks=who.data.acks-1
                        if who.data.ackmax<3:
                            who.data.ackmax=who.data.ackmax+1
            for who in people:
                if who.data.acks<who.data.ackmax:
                    who.scene(scene,table)       # send current scene
                    who.remove(scene,who.data.rep)# remove client's ship
                    who.camera(who.data.pos.A)   # specify camera
                    who.swapbuffers()            # display picture
                    who.req_ack()                # request an ack
                    who.data.acks=who.data.acks+1
                    who.end()                    # end communication
            event=host.get_event()         # Get next event
        who=None
        scene.sweep()    # Remove dead ships from the scene
        collide(world)   # Do collision detection
        t0=time()        # Get current time
        animate(world,t0-t) # Move ships, given time delta
        t=t0             # This is so next time we have the correct time delta

mainloop()               # Start her up captain
