/* neonio's SpaceWar! v1.0.0 */
/* dependencies: raylib */
#include <stddef.h>             /* NULL */
#include <stdlib.h>             /* malloc */
#include <string.h>             /* memcpy */
#include "raylib.h"
#include "raymath.h"
#define screenWidth 600
enum collisiongroups { EARTH=1,VENUS=2,TORP=4,DUST=8 };
enum types { T_SHIP,T_WELL,T_TORPEDO,T_DUST };
/* Extra vector functions */
Vector2 Vector2FromAngle(float angle,float magnitude){
  return Vector2Rotate((Vector2){0.0,magnitude},angle);
}
Vector2 Vector2RandomSquare(float min,float max){
  return (Vector2){ (GetRandomValue(0,1000)/1000.0)*(max-min)+min,
                    (GetRandomValue(0,1000)/1000.0)*(max-min)+min  };
}
/* Doubly-linked "object" list */
struct bindings { int cw,ccw,thrust,fire; };
struct object {
  char type;
  Vector2 position,velocity; float angle,radius; char colmask;
  struct { char grav:1,drag:1; } flags; /* todo: change 'flags' to 'flag' */
  struct {
    void (*move)(struct object* this);
    void (*draw)(struct object* this);
    void (*collide)(struct object* this,struct object* that);
    void (*kill)(struct object* this);
  } on;
  float counter;
  struct bindings* bind;
  struct object *next, *prev;
};
struct object* AddObject(struct object* tail,const struct object* obj){
  struct object* new = malloc(sizeof(struct object));
  memcpy(new, obj, sizeof(*new));
  if (tail) new->next = tail->next; else new->next = NULL;
  new->prev = tail;
  if (new->next) new->next->prev = new;
  if (new->prev) new->prev->next = new;
  return new;
}
void RemObject(struct object* obj){
  if(obj->prev) obj->prev->next = obj->next;
  if(obj->next) obj->next->prev = obj->prev;
  free(obj);
}
/* Object list walkers */
void MoveAllObjects(struct object* head){
  for (;head;head=head->next) if (head->on.move) (*head->on.move)(head);
}
void DrawAllObjects(struct object* head){
  for (;head;head=head->next) if (head->on.draw) (*head->on.draw)(head);
}
void CollideAllObjects(struct object* head){
  for(struct object* this = head; this; this = this->next){
    if(this->on.collide)
      for(struct object* that = head; that; that = that->next){
        if(this!=that &&
           this->colmask & that->colmask &&
           CheckCollisionCircles(this->position, this->radius,
                                 that->position, that->radius))
          (*this->on.collide)(this,that);
      }
    this->position.x = Wrap(this->position.x,-1.0,1.0);
    this->position.y = Wrap(this->position.y,-1.0,1.0);
  }
}
void KillAllObjects(struct object* head){
  for (;head;head=head->next) if (head->on.kill && head->counter<=0 )
    (*head->on.kill)(head);
}
/* Move/Draw/Collide/Kill functions */
void DrawShip(struct object* this){
  Vector2 shipHalf = Vector2FromAngle(this->angle,this->radius/2.0);
  DrawPolyLines(this->position,
                (this->colmask&EARTH)?3:5,
                this->radius,this->angle*RAD2DEG+90,WHITE);
  DrawLineV(Vector2Subtract(this->position,shipHalf),
            Vector2Add(this->position,shipHalf), WHITE);
}
void DrawWell(struct object* this){
  DrawCircleLinesV(this->position,this->radius,
                   ColorLerp(BLACK,WHITE,this->counter));
  this->counter *= 0.5;
}
void DrawTorpedo(struct object* this){
  if(this->counter<=0) DrawCircleV(this->position,this->radius,WHITE);
  else DrawLineV(this->position,
                 Vector2Add(this->position,
                            Vector2FromAngle(this->angle,this->radius)),
                 WHITE);
}
void DrawDust(struct object* this){
  DrawLineV(this->position,
            Vector2Add(this->position, Vector2Negate(this->velocity)),
            ColorLerp(DARKGRAY,WHITE,this->counter));
}
void CollideWell(struct object *this,struct object *that){
  this->counter += 0.05;
  that->position = Vector2RandomSquare(-1.0,1.0);
  that->velocity = Vector2Zero();
}
void CollideTorpedo(struct object *this,struct object *that){
  if(that->type==T_SHIP){ this->counter = 0; that->counter = 0; }
  else if (that->type==T_TORPEDO){ this->counter = 0; that->counter = 0; }
}
void MoveObject(struct object *this){
  if(this->flags.grav)
    this->velocity
      = Vector2Add(this->velocity,
                   Vector2Scale
                   (Vector2Normalize(Vector2Negate(this->position)),
                    0.00002 *   /* G*m1*m2 */
                    1 / ( Vector2Length(this->position) *
                          Vector2Length(this->position))));
  if(this->flags.drag) this->velocity = Vector2Scale(this->velocity, 0.99);
  this->position = Vector2Add(this->position, this->velocity);
}
void MoveDust(struct object* this){ this->counter*=0.999; MoveObject(this); }
void MakeDust(struct object* tail,Vector2 position,int amount,float velocity){
  for(int i=amount;
       i--;
       tail = AddObject(tail,
                        &(struct object){T_DUST,
                                         position,
                                         Vector2RandomSquare
                                           (-1.0*velocity,velocity),
                                         0,0,DUST,{1,1},{&MoveDust,&DrawDust},
                                         1.0}));
}
void Thrust(struct object* this,float impulse){
  this->velocity =
    Vector2Add(this->velocity,
               Vector2FromAngle(this->angle,impulse));
}
void KillTorpedo(struct object* this){
  MakeDust(this,this->position,3,0.005), RemObject(this);
}
void MoveTorpedo(struct object* this){
  this->counter--, Thrust(this,0.0005), MoveObject(this);
}
void MoveShip(struct object* this){
  if(IsKeyDown(this->bind->cw))     this->angle += DEG2RAD* 2.5;
  if(IsKeyDown(this->bind->ccw))    this->angle += DEG2RAD*-2.5;
  if(IsKeyDown(this->bind->thrust)) Thrust(this,0.0002);
  if(IsKeyPressed(this->bind->fire)){
    struct object* torpedo =
      AddObject(this,
                &(struct object)
                {T_TORPEDO,
                 this->position,this->velocity,this->angle,0.01,0,{1,1},
                 {&MoveTorpedo,&DrawTorpedo,&CollideTorpedo,&KillTorpedo},
                 100.0});
    if(this->colmask & EARTH) torpedo->colmask |= VENUS;
    else torpedo->colmask |= EARTH;
    torpedo->colmask |= TORP;
  }
  MoveObject(this);
}
void KillShip(struct object* this){
 MakeDust(this,this->position,18,0.01), RemObject(this);
}
void DrawMenu(struct object* head){
  int shipCount = 0, winner = 0;
  for(struct object* current=head;current;current=current->next)
    if(current->type==T_SHIP){
      shipCount++;
      winner = current->colmask;
    }
  if(shipCount<=1){
    if(winner&EARTH)
      DrawText("GAME OVER\nTERRAN VICTORY",screenWidth/2,screenWidth/2,12,WHITE);
    else
      DrawText("GAME OVER\nVENUSIAN VICTORY",screenWidth/2,screenWidth/2,12,WHITE);
  }
}
int main() {
  struct bindings earthBinds = {KEY_D,KEY_A,KEY_W,KEY_S};
  struct bindings venusBinds = {KEY_L,KEY_J,KEY_I,KEY_K};
  struct object* head =
    AddObject(NULL,
              &(struct object)
              {T_WELL,
               Vector2Zero(),Vector2Zero(),0,0.1,EARTH|VENUS|TORP|DUST,{0,0},
               {NULL,&DrawWell,&CollideWell,NULL},1.0});
  struct object* tail = head;
  tail = AddObject(tail,
                   &(struct object)
                   {T_SHIP,
                    {0.5,0.5},Vector2Zero(),PI,0.03,EARTH,{1,1},
                    {&MoveShip,&DrawShip,NULL,&KillShip},
                    1.0, &earthBinds});
  struct object* earth = tail;
  tail = AddObject(tail,
                   &(struct object)
                   {T_SHIP,
                    {-0.5,-0.5},Vector2Zero(),PI,0.03,VENUS,{1,1},
                    {&MoveShip,&DrawShip,NULL,&KillShip},
                    1.0, &venusBinds});
  for(InitWindow(screenWidth,screenWidth,"spacewar"),
        SetTargetFPS(60),
        SetRandomSeed(200);
      !WindowShouldClose();
      ){
    KillAllObjects(head);
    CollideAllObjects(head);
    MoveAllObjects(head);
    BeginDrawing(),
      ClearBackground(BLACK),
      BeginMode2D((Camera2D){{screenWidth/2,screenWidth/2},Vector2Zero(),
                             0,screenWidth/2});
    DrawAllObjects(head);
    EndMode2D(),
      DrawMenu(head),
      EndDrawing();
  }
  CloseWindow();
  return 0;
}
