#!/usr/bin/env ruby -rubygems

# OVERLY SIMPLE INFECTION SIMULATOR WITH OPENGL FOR RUBY
# by udo.schroeter@gmail.com
# license: public domain
#
# if you haven't already, you need to install the "ruby-opengl" gem
# for this example to work. on a unix system, you can use the
# following command:

# sudo gem install ruby-opengl

require "gl"
require "glu"
require "glut"
require "mathn"

include Gl
include Glu
include Glut

$window = ""

# viewport movement variables
$fXDiff = 0
$fYDiff = -45
$fZDiff = 0
$xLastIncr = 0
$yLastIncr = 0
$fXInertia = -0.0000
$fYInertia = -0.000
$fZInertia = -0.0035
$fScale = 0.15
$ftime = 0
$xLast = -1
$yLast = -1
$bmModifiers = 0
$rotate = 1

# playing field configuration
$tick = 0;
$polySize = 0.5
$polySizeW = 0.4
$entities = Array.new()
$entityIdx = 0
$fastPi = 3.141
$deg2rad = $fastPi/180
$xMax = 25
TIMER_FREQUENCY_MILLIS = 20

# create all entities
def makeEntities()
  (-7..7).each do |x|
    (-7..7).each do |y|
      entity = {}
      entity[:x] = x*2
      entity[:y] = y*2
      entity[:type] = 'human'
      entity[:direction] = 0
      $entities.push(entity)
    end
  end
end

# make the OpenGL window
def init_gl_window(width = 640, height = 480)
    glClearColor(0.0, 0.0, 0, 0)
    glClearDepth(1.0)
    glDepthFunc(GL_LEQUAL)
    glEnable(GL_DEPTH_TEST)
    glShadeModel(GL_SMOOTH)
    glMatrixMode(GL_PROJECTION)
    glLoadIdentity
    gluPerspective(45.0, width / height, 0.1, 100.0)
    glMatrixMode(GL_MODELVIEW)
    draw_gl_scene
end

# on window resize
def reshape(width, height)
    height = 1 if height == 0
    glViewport(0, 0, width, height)
    glMatrixMode(GL_PROJECTION)
    glLoadIdentity
    gluPerspective(45.0, width / height, 0.1, 100.0)
end

# turn entities around at the border (just like at the Rio Grande but more effective)
def doBoundary(entity)
  distFromZero = Math.sqrt(entity[:x]**2 + entity[:y]**2)
  if distFromZero > $xMax
    entity[:direction] = 180-180/$fastPi*Math.atan2(entity[:x], entity[:y])+20-rand(40)
    entity[:newdirection] = 180-180/$fastPi*Math.atan2(entity[:x], entity[:y])+20-rand(40)
    entity[:speed] = 0.01
  end
end

# display a single entity
def drawEntity(zpos, entity)
  if entity[:type] == 'human'
    dirmod = 0
    r = 0
    g = 0
    b = 0.7
  elsif entity[:type] == 'zombie'
    zombPhase = Math.sin($tick/10)
    dirmod = zombPhase*20+rand(4)-2
    r = 0.6+zombPhase*0.2
    g = 0
    b = 0
  end
  glPushMatrix()
  glTranslatef(+entity[:x], +entity[:y], +zpos)
  glRotatef(entity[:direction]+dirmod, 0,0,1)
  if entity[:anistate] 
    entity[:anistate] -= 0.3
    glRotatef(entity[:anistate], 0,1,0)
    r = entity[:anistate]/50+0.4
    g = r
    b = r
    if entity[:anistate] < 0
      entity.delete(:anistate)
    end
  end
  glTranslatef(-entity[:x], -entity[:y], -zpos)
  glBegin(GL_POLYGON)
    glColor3f(0.8,0.8,0.8)
    glVertex3f( 0.0+entity[:x],  $polySize+entity[:y], 0.0+zpos)
    glColor3f(r,g,b)
    glVertex3f( $polySizeW+entity[:x], -$polySize+entity[:y], 0.0+zpos)
    glColor3f(r,g,b)
    glVertex3f(-$polySizeW+entity[:x], -$polySize+entity[:y], 0.0+zpos)
  glEnd
  glPopMatrix()
end

# get the nearest entity of a certain type
def getNearest(type, mx, my)
  zombDist = 100000
  zombIndex = nil
  $entities.each do |other|
    if (other[:type] == type)
      ex = other[:x]-mx
      ey = other[:y]-my
      dist = Math.sqrt(ex*ex + ey*ey)
      if dist < zombDist
        zombIndex = other
        zombDist = dist
      end
    end
  end
  if zombIndex
   zombIndex[:pdist] = zombDist
  end
  return(zombIndex)
end

# here you can plug in whatever decision making routine you like
def DoEntityAI(entity)
  case(entity[:type])
  when 'human'
    other = getNearest('zombie', entity[:x], entity[:y])
    if other
      ex = other[:x]-entity[:x]
      ey = other[:y]-entity[:y]
      entity[:newdirection] = 180-180/$fastPi*Math.atan2(ex, ey)
      if (entity[:newdirection]-entity[:direction]).abs>180 
        entity[:newdirection] -= 360
      end
      entity[:speed] = 0.01*(1/(0.8+other[:pdist]))
    end
  when 'zombie'
    if entity[:anistate] == nil
      other = getNearest('human', entity[:x], entity[:y])
      if other
        ex = other[:x]-entity[:x]
        ey = other[:y]-entity[:y]
        entity[:newdirection] = 360-180/$fastPi*Math.atan2(ex, ey)
        if (entity[:newdirection]-entity[:direction]).abs>180 
          entity[:newdirection] -= 360
        end
        entity[:speed] = 0.0025+0.005*(1/(1+other[:pdist]))
        if other[:pdist] < 1
          other[:type] = 'zombie'
          other[:anistate] = 180
          entity[:speed] = 0
          other[:speed] = 0
        end
      end
    end
  end
end

# update the scene
def draw_gl_scene
  glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT)
  glMatrixMode(GL_MODELVIEW)
  glLoadIdentity

  glTranslatef(0.0, 0.0, -6.0)
  glScalef($fScale, $fScale, $fScale)
  $fXDiff += $fXInertia
  $fYDiff += $fYInertia
  $fZDiff += $fZInertia
  glRotatef($fYDiff, 1,0,0)
  glRotatef($fXDiff, 0,1,0)
  glRotatef($fZDiff, 0,0,1)
  $tick += 0.1
  $entities.each do |entity|
    drawEntity(0, entity)
  end    
  glutSwapBuffers
end

# draw the viewport during idle cycles of the CPU
def idle
    glutPostRedisplay
end

# handle keyboard input
keyboard = lambda do |key, x, y|
  case(key)
  when ?\e
    glutDestroyWindow($window)
    exit(0)
  when ?h
    entity = $entities[rand($entities.size)]
    entity[:anistate] = 180;
    entity[:type] = 'human'
  when ?z
    entity = $entities[rand($entities.size)]
    entity[:anistate] = 180;
    entity[:type] = 'zombie'
  when ?r
    $entities = Array.new()
    makeEntities()
  end
  glutPostRedisplay
end

# timer event that updates the position of the entities and calls decision making routine
timer = lambda do |value|
  $entities.each do |entity|
    if entity[:speed] 
      if entity[:speed] > 0.2
        entity[:speed] = 0.2
      end
      entity[:x] += 10*entity[:speed]*Math.sin(-(entity[:direction])*$deg2rad)
      entity[:y] += 10*entity[:speed]*Math.cos(-(entity[:direction])*$deg2rad)
      if entity[:speed] < 0.001
        entity.delete(:speed)
      end
    end
    if entity[:newdirection]
      if entity[:newdirection] > entity[:direction]
        entity[:direction] += 2.5
      else
        entity[:direction] -= 2.5
      end
      if (entity[:newdirection] - entity[:direction]).abs < 1
        entity.delete(:newdirection)
      end
    end
    doBoundary(entity)  
    if rand(10) == 1
     DoEntityAI(entity)
    end
  end
  glutTimerFunc(TIMER_FREQUENCY_MILLIS , timer, 0)
end

# handle mouse input (not implemented)
mouse = lambda do |button,state,x,y|
  $bmModifiers = glutGetModifiers()
  if (button == GLUT_LEFT_BUTTON)
    if (state == GLUT_UP)
      
    end
  end
end

# display console help
puts('press "z" to make a zombie')
puts('press "h" to make a human')
puts('press "r" to reload')
puts('press ESC key to quit')

# init entities
makeEntities()

# init OpenGL
glutInit
glutInitDisplayMode(GLUT_RGB | GLUT_DOUBLE | GLUT_ALPHA | GLUT_DEPTH);
glutInitWindowSize(800, 500);
glutInitWindowPosition(0, 0);
$window = glutCreateWindow('RubyZombies!0.1');
glutDisplayFunc(method(:draw_gl_scene).to_proc);
glutReshapeFunc(method(:reshape).to_proc);
glutIdleFunc(method(:idle).to_proc);
glutKeyboardFunc(keyboard);
glutMouseFunc(mouse)
init_gl_window(800, 500)
glutTimerFunc(TIMER_FREQUENCY_MILLIS , timer, 0)
glutMainLoop();