Nevermania: Tweaks
[neverball] / ball / game.c
index 20267f2..bef5cd0 100644 (file)
@@ -1,4 +1,4 @@
-/*   
+/*
  * Copyright (C) 2003 Robert Kooima
  *
  * NEVERBALL is  free software; you can redistribute  it and/or modify
@@ -21,6 +21,7 @@
 #include "geom.h"
 #include "back.h"
 #include "part.h"
+#include "ball.h"
 #include "image.h"
 #include "audio.h"
 #include "solid_gl.h"
@@ -35,16 +36,13 @@ static int game_state = 0;
 static struct s_file file;
 static struct s_file back;
 
-static float clock      = 0.f;          /* Clock time                        */
-static int   clock_down = 1;            /* Clock go up or down?              */
+static float timer      = 0.f;          /* Clock time                        */
+static int   timer_down = 1;            /* Timer go up or down?              */
 
-static float game_ix;                   /* Input rotation about X axis       */
-static float game_iz;                   /* Input rotation about Z axis       */
 static float game_rx;                   /* Floor rotation about X axis       */
 static float game_rz;                   /* Floor rotation about Z axis       */
 
 static float view_a;                    /* Ideal view rotation about Y axis  */
-static float view_ry;                   /* Angular velocity about Y axis     */
 static float view_dc;                   /* Ideal view distance above ball    */
 static float view_dp;                   /* Ideal view distance above ball    */
 static float view_dz;                   /* Ideal view distance behind ball   */
@@ -53,7 +51,7 @@ static float view_fov;                  /* Field of view                     */
 static float view_c[3];                 /* Current view center               */
 static float view_v[3];                 /* Current view vector               */
 static float view_p[3];                 /* Current view position             */
-static float view_e[3][3];              /* Current view orientation          */
+static float view_e[3][3];              /* Current view reference frame      */
 static float view_k;
 
 static int   coins  = 0;                /* Collected coins                   */
@@ -65,26 +63,235 @@ static float jump_dt;                   /* Jump duration                     */
 static float jump_p[3];                 /* Jump destination                  */
 static float fade_k = 0.0;              /* Fade in/out level                 */
 static float fade_d = 0.0;              /* Fade in/out direction             */
-static int   drawball = 1;              /* Should the ball be drawn?         */
-static int   ball_b = 0;                /* Is the ball a bonus ball?         */
 
 /*---------------------------------------------------------------------------*/
 
-static void view_init(void)
+/*
+ * This is an abstraction of the game's input state.  All input is
+ * encapsulated here, and all references to the input by the game are made
+ * here.  This has the effect of homogenizing input for use in replay
+ * recording and playback.
+ *
+ * x and z:
+ *     -32767 = -ANGLE_BOUND
+ *     +32767 = +ANGLE_BOUND
+ *
+ * r:
+ *     -32767 = -VIEWR_BOUND
+ *     +32767 = +VIEWR_BOUND
+ *     
+ */
+
+struct input
+{
+    short x;
+    short z;
+    short r;
+    short c;
+};
+
+static struct input input_current;
+
+static void input_init(void)
 {
-    /* Get the initial orientation angle */
-    if (file.uc > 0)
-        view_a  = file.uv->a - 90.f; /* angle is in the sol */
-    else
-        view_a  = 0.f; /* default is north :) */
+    input_current.x = 0;
+    input_current.z = 0;
+    input_current.r = 0;
+    input_current.c = 0;
+}
+
+static void input_set_x(float x)
+{
+    if (x < -ANGLE_BOUND) x = -ANGLE_BOUND;
+    if (x >  ANGLE_BOUND) x =  ANGLE_BOUND;
+
+    input_current.x = (short) (32767.0f * x / ANGLE_BOUND);
+}
+
+static void input_set_z(float z)
+{
+    if (z < -ANGLE_BOUND) z = -ANGLE_BOUND;
+    if (z >  ANGLE_BOUND) z =  ANGLE_BOUND;
+
+    input_current.z = (short) (32767.0f * z / ANGLE_BOUND);
+}
 
-    view_ry = 0.f;
+static void input_set_r(float r)
+{
+    if (r < -VIEWR_BOUND) r = -VIEWR_BOUND;
+    if (r >  VIEWR_BOUND) r =  VIEWR_BOUND;
+
+    input_current.r = (short) (32767.0f * r / VIEWR_BOUND);
+}
+
+static void input_set_c(int c)
+{
+    input_current.c = (short) c;
+}
+
+static float input_get_x(void)
+{
+    return ANGLE_BOUND * (float) input_current.x / 32767.0f;
+}
+
+static float input_get_z(void)
+{
+    return ANGLE_BOUND * (float) input_current.z / 32767.0f;
+}
+
+static float input_get_r(void)
+{
+    return VIEWR_BOUND * (float) input_current.r / 32767.0f;
+}
+
+static int input_get_c(void)
+{
+    return (int) input_current.c;
+}
+
+int input_put(FILE *fout)
+{
+    if (game_state)
+    {
+        put_short(fout, &input_current.x);
+        put_short(fout, &input_current.z);
+        put_short(fout, &input_current.r);
+        put_short(fout, &input_current.c);
+        
+        return 1;
+    }
+    return 0;
+}
+
+int input_get(FILE *fin)
+{
+    if (game_state)
+    {
+        get_short(fin, &input_current.x);
+        get_short(fin, &input_current.z);
+        get_short(fin, &input_current.r);
+        get_short(fin, &input_current.c);
+
+        return (feof(fin) ? 0 : 1);
+    }
+    return 0;
+}
+
+/*---------------------------------------------------------------------------*/
+
+static int   grow = 0;                  /* Should the ball be changing size? */
+static float grow_orig = 0;             /* the original ball size            */
+static float grow_goal = 0;             /* how big or small to get!          */
+static float grow_t = 0.0;              /* timer for the ball to grow...     */
+static float grow_strt = 0;             /* starting value for growth         */
+static int   got_orig = 0;              /* Do we know original ball size?    */
+
+#define GROW_TIME  0.5f                 /* sec for the ball to get to size.  */
+#define GROW_BIG   1.5f                 /* large factor                      */
+#define GROW_SMALL 0.5f                 /* small factor                      */
+
+static int   grow_state = 0;            /* Current state (values -1, 0, +1)  */
+
+static void grow_init(const struct s_file *fp, int type)
+{
+    if (!got_orig)
+    {
+        grow_orig  = fp->uv->r;
+        grow_goal  = grow_orig;
+        grow_strt  = grow_orig;
+
+        grow_state = 0;
+
+        got_orig   = 1;
+    }
+
+    if (type == ITEM_SHRINK)
+    {
+        audio_play(AUD_SHRINK, 1.f);
+
+        switch (grow_state)
+        {
+        case -1:
+            break;
+
+        case  0:
+            grow_goal = grow_orig * GROW_SMALL;
+            grow_state = -1;
+            grow = 1;
+            break;
+
+        case +1:
+            grow_goal = grow_orig;
+            grow_state = 0;
+            grow = 1;
+            break;
+        }
+    }
+    else if (type == ITEM_GROW)
+    {
+        audio_play(AUD_GROW, 1.f);
 
+        switch (grow_state)
+        {
+        case -1:
+            grow_goal = grow_orig;
+            grow_state = 0;
+            grow = 1;
+            break;
+
+        case  0:
+            grow_goal = grow_orig * GROW_BIG;
+            grow_state = +1;
+            grow = 1;
+            break;
+
+        case +1:
+            break;
+        }
+    }
+
+    if (grow)
+    {
+        grow_t = 0.0;
+        grow_strt = fp->uv->r;
+    }
+}
+
+static void grow_step(const struct s_file *fp, float dt)
+{
+    float dr;
+
+    if (!grow)
+        return;
+
+    /* Calculate new size based on how long since you touched the coin... */
+
+    grow_t += dt;
+
+    if (grow_t >= GROW_TIME)
+    {
+        grow = 0;
+        grow_t = GROW_TIME;
+    }
+
+    dr = grow_strt + ((grow_goal-grow_strt) * (1.0f / (GROW_TIME / grow_t)));
+
+    /* No sinking through the floor! Keeps ball's bottom constant. */
+
+    fp->uv->p[1] += (dr - fp->uv->r);
+    fp->uv->r     =  dr;
+}
+
+/*---------------------------------------------------------------------------*/
+
+static void view_init(void)
+{
     view_fov = (float) config_get_d(CONFIG_VIEW_FOV);
     view_dp  = (float) config_get_d(CONFIG_VIEW_DP) / 100.0f;
     view_dc  = (float) config_get_d(CONFIG_VIEW_DC) / 100.0f;
     view_dz  = (float) config_get_d(CONFIG_VIEW_DZ) / 100.0f;
     view_k   = 1.0f;
+    view_a   = 0.0f;
 
     view_c[0] = 0.f;
     view_c[1] = view_dc;
@@ -107,25 +314,24 @@ static void view_init(void)
 
 int game_init(const struct level *level, int t, int g)
 {
-    clock      = (float) t / 100.f;
-    clock_down = (t > 0);
+    timer      = (float) t / 100.f;
+    timer_down = (t > 0);
     coins      = 0;
 
     if (game_state)
         game_free();
 
-    if (!sol_load_gl(&file, level->file, config_get_d(CONFIG_TEXTURES),
+    if (!sol_load_gl(&file, config_data(level->file),
+                     config_get_d(CONFIG_TEXTURES),
                      config_get_d(CONFIG_SHADOW)))
         return (game_state = 0);
 
     game_state = 1;
 
-    game_ix = 0.f;
-    game_iz = 0.f;
-    game_rx = 0.f;
-    game_rz = 0.f;
+    input_init();
 
-    drawball = 1;
+    game_rx = 0.0f;
+    game_rz = 0.0f;
 
     /* Initialize jump and goal states. */
 
@@ -135,8 +341,6 @@ int game_init(const struct level *level, int t, int g)
     goal_c = g;
     goal_k = (g == 0) ? 1.0f : 0.0f;
 
-    ball_b = level->is_bonus;
-
     /* Initialise the level, background, particles, fade, and view. */
 
     fade_k =  1.0f;
@@ -149,6 +353,11 @@ int game_init(const struct level *level, int t, int g)
     sol_load_gl(&back, config_data(level->back),
                 config_get_d(CONFIG_TEXTURES), 0);
 
+    /* Initialize ball size tracking... */
+
+    got_orig = 0;
+    grow = 0;
+
     return game_state;
 }
 
@@ -167,7 +376,7 @@ void game_free(void)
 
 int curr_clock(void)
 {
-    return (int) (clock * 100.f);
+    return (int) (timer * 100.f);
 }
 
 int curr_coins(void)
@@ -182,59 +391,128 @@ int curr_goal(void)
 
 /*---------------------------------------------------------------------------*/
 
-static void game_draw_balls(const struct s_file *fp)
+static void game_draw_balls(const struct s_file *fp,
+                            const float *bill_M, float t)
 {
     float c[4] = { 1.0f, 1.0f, 1.0f, 1.0f };
-    float M[16];
 
-    m_basis(M, fp->uv[0].e[0], fp->uv[0].e[1], fp->uv[0].e[2]);
+    float ball_M[16];
+    float pend_M[16];
+
+    m_basis(ball_M, fp->uv[0].e[0], fp->uv[0].e[1], fp->uv[0].e[2]);
+    m_basis(pend_M, fp->uv[0].E[0], fp->uv[0].E[1], fp->uv[0].E[2]);
 
+    glPushAttrib(GL_LIGHTING_BIT);
     glPushMatrix();
     {
         glTranslatef(fp->uv[0].p[0],
                      fp->uv[0].p[1] + BALL_FUDGE,
                      fp->uv[0].p[2]);
-        glMultMatrixf(M);
         glScalef(fp->uv[0].r,
                  fp->uv[0].r,
                  fp->uv[0].r);
 
         glColor4fv(c);
-
-        ball_draw(ball_b);
+        ball_draw(ball_M, pend_M, bill_M, t);
     }
     glPopMatrix();
+    glPopAttrib();
 }
 
-static void game_draw_coins(const struct s_file *fp)
+static void game_draw_items(const struct s_file *fp, float t)
 {
-    float r = 360.f * SDL_GetTicks() / 1000.f;
-    int ci;
+    float r = 360.f * t;
+    int hi;
+
+    glPushAttrib(GL_LIGHTING_BIT);
+    {
+        item_push(ITEM_COIN);
+        {
+            for (hi = 0; hi < fp->hc; hi++)
+
+                if (fp->hv[hi].t == ITEM_COIN && fp->hv[hi].n > 0)
+                {
+                    glPushMatrix();
+                    {
+                        glTranslatef(fp->hv[hi].p[0],
+                                     fp->hv[hi].p[1],
+                                     fp->hv[hi].p[2]);
+                        glRotatef(r, 0.0f, 1.0f, 0.0f);
+                        item_draw(&fp->hv[hi], r);
+                    }
+                    glPopMatrix();
+                }
+        }
+        item_pull();
+
+        item_push(ITEM_SHRINK);
+        {
+            for (hi = 0; hi < fp->hc; hi++)
+
+                if (fp->hv[hi].t == ITEM_SHRINK)
+                {
+                    glPushMatrix();
+                    {
+                        glTranslatef(fp->hv[hi].p[0],
+                                     fp->hv[hi].p[1],
+                                     fp->hv[hi].p[2]);
+                        glRotatef(r, 0.0f, 1.0f, 0.0f);
+                        item_draw(&fp->hv[hi], r);
+                    }
+                    glPopMatrix();
+                }
+        }
+        item_pull();
+
+        item_push(ITEM_GROW);
+        {
+            for (hi = 0; hi < fp->hc; hi++)
 
-    coin_push();
+                if (fp->hv[hi].t == ITEM_GROW)
+                {
+                    glPushMatrix();
+                    {
+                        glTranslatef(fp->hv[hi].p[0],
+                                     fp->hv[hi].p[1],
+                                     fp->hv[hi].p[2]);
+                        glRotatef(r, 0.0f, 1.0f, 0.0f);
+                        item_draw(&fp->hv[hi], r);
+                    }
+                    glPopMatrix();
+                }
+        }
+        item_pull();
+    }
+    glPopAttrib();
+}
+
+static void game_draw_goals(const struct s_file *fp, const float *M, float t)
+{
+    if (goal_c == 0)
     {
-        for (ci = 0; ci < fp->cc; ci++)
-            if (fp->cv[ci].n > 0)
+        int zi;
+
+        /* Draw the goal particles. */
+
+        glEnable(GL_TEXTURE_2D);
+        {
+            for (zi = 0; zi < fp->zc; zi++)
             {
                 glPushMatrix();
                 {
-                    glTranslatef(fp->cv[ci].p[0],
-                                 fp->cv[ci].p[1],
-                                 fp->cv[ci].p[2]);
-                    glRotatef(r, 0.0f, 1.0f, 0.0f);
-                    coin_draw(fp->cv[ci].n, r);
+                    glTranslatef(fp->zv[zi].p[0],
+                                 fp->zv[zi].p[1],
+                                 fp->zv[zi].p[2]);
+                    
+                    part_draw_goal(M, fp->zv[zi].r, goal_k, t);
                 }
                 glPopMatrix();
             }
-    }
-    coin_pull();
-}
+        }
+        glDisable(GL_TEXTURE_2D);
 
-static void game_draw_goals(const struct s_file *fp, float rx, float ry)
-{
-    int zi;
+        /* Draw the goal column. */
 
-    if (goal_c == 0)
         for (zi = 0; zi < fp->zc; zi++)
         {
             glPushMatrix();
@@ -243,13 +521,15 @@ static void game_draw_goals(const struct s_file *fp, float rx, float ry)
                              fp->zv[zi].p[1],
                              fp->zv[zi].p[2]);
 
-                part_draw_goal(rx, ry, fp->zv[zi].r, goal_k, fp->zv[zi].c);
+                glScalef(fp->zv[zi].r,
+                         goal_k,
+                         fp->zv[zi].r);
 
-                glScalef(fp->zv[zi].r, goal_k, fp->zv[zi].r);
                 goal_draw();
             }
             glPopMatrix();
         }
+    }
 }
 
 static void game_draw_jumps(const struct s_file *fp)
@@ -263,8 +543,10 @@ static void game_draw_jumps(const struct s_file *fp)
             glTranslatef(fp->jv[ji].p[0],
                          fp->jv[ji].p[1],
                          fp->jv[ji].p[2]);
+            glScalef(fp->jv[ji].r,
+                     1.0f,
+                     fp->jv[ji].r);
 
-            glScalef(fp->jv[ji].r, 1.f, fp->jv[ji].r);
             jump_draw(!jump_e);
         }
         glPopMatrix();
@@ -279,13 +561,16 @@ static void game_draw_swchs(const struct s_file *fp)
     {
         if (fp->xv[xi].i)
             continue;
+
         glPushMatrix();
         {
             glTranslatef(fp->xv[xi].p[0],
                          fp->xv[xi].p[1],
                          fp->xv[xi].p[2]);
+            glScalef(fp->xv[xi].r,
+                     1.0f,
+                     fp->xv[xi].r);
 
-            glScalef(fp->xv[xi].r, 1.f, fp->xv[xi].r);
             swch_draw(fp->xv[xi].f, fp->xv[xi].e);
         }
         glPopMatrix();
@@ -294,18 +579,23 @@ static void game_draw_swchs(const struct s_file *fp)
 
 /*---------------------------------------------------------------------------*/
 
-static void game_refl_all(int s)
+static void game_draw_tilt(int d)
 {
     const float *ball_p = file.uv->p;
 
+    /* Rotate the environment about the position of the ball. */
+
+    glTranslatef(+ball_p[0], +ball_p[1] * d, +ball_p[2]);
+    glRotatef(-game_rz * d, view_e[2][0], view_e[2][1], view_e[2][2]);
+    glRotatef(-game_rx * d, view_e[0][0], view_e[0][1], view_e[0][2]);
+    glTranslatef(-ball_p[0], -ball_p[1] * d, -ball_p[2]);
+}
+
+static void game_refl_all(void)
+{
     glPushMatrix();
     {
-        /* Rotate the environment about the position of the ball. */
-
-        glTranslatef(+ball_p[0], +ball_p[1], +ball_p[2]);
-        glRotatef(-game_rz, view_e[2][0], view_e[2][1], view_e[2][2]);
-        glRotatef(-game_rx, view_e[0][0], view_e[0][1], view_e[0][2]);
-        glTranslatef(-ball_p[0], -ball_p[1], -ball_p[2]);
+        game_draw_tilt(1);
 
         /* Draw the floor. */
 
@@ -319,8 +609,8 @@ static void game_refl_all(int s)
 static void game_draw_light(void)
 {
     const float light_p[2][4] = {
-        { -8.0f, +32.0f, -8.0f, 1.0f },
-        { +8.0f, +32.0f, +8.0f, 1.0f },
+        { -8.0f, +32.0f, -8.0f, 0.0f },
+        { +8.0f, +32.0f, +8.0f, 0.0f },
     };
     const float light_c[2][4] = {
         { 1.0f, 0.8f, 0.8f, 1.0f },
@@ -340,11 +630,8 @@ static void game_draw_light(void)
     glLightfv(GL_LIGHT1, GL_SPECULAR, light_c[1]);
 }
 
-static void game_draw_back(int pose, int d, const float p[3])
+static void game_draw_back(int pose, int d, float t)
 {
-    float c[4] = { 1.0f, 1.0f, 1.0f, 1.0f };
-    float t = SDL_GetTicks() / 1000.f + 120.0f;
-
     glPushMatrix();
     {
         if (d < 0)
@@ -353,90 +640,151 @@ static void game_draw_back(int pose, int d, const float p[3])
             glRotatef(game_rx * 2, view_e[0][0], view_e[0][1], view_e[0][2]);
         }
 
-        glTranslatef(p[0], p[1], p[2]);
-        glColor4fv(c);
+        glTranslatef(view_p[0], view_p[1] * d, view_p[2]);
 
         if (config_get_d(CONFIG_BACKGROUND))
         {
             /* Draw all background layers back to front. */
 
-            sol_back(&back, BACK_DIST, FAR_DIST, t);
+            sol_back(&back, BACK_DIST, FAR_DIST,  t);
             back_draw(0);
-            sol_back(&back, 0, BACK_DIST, t);
-
-            /* Draw all foreground geometry in the background file. */
-
-            sol_draw(&back);
+            sol_back(&back,         0, BACK_DIST, t);
         }
         else back_draw(0);
     }
     glPopMatrix();
 }
 
-static void game_draw_fore(int pose, float rx, float ry, int d, const float p[3])
+static void game_clip_refl(int d)
+{
+    /* Fudge to eliminate the floor from reflection. */
+
+    GLdouble e[4], k = -0.00001;
+
+    e[0] = 0;
+    e[1] = 1;
+    e[2] = 0;
+    e[3] = k;
+
+    glClipPlane(GL_CLIP_PLANE0, e);
+}
+
+static void game_clip_ball(int d, const float *p)
+{
+    GLdouble r, c[3], pz[4], nz[4];
+
+    /* Compute the plane giving the front of the ball, as seen from view_p. */
+
+    c[0] = p[0];
+    c[1] = p[1] * d;
+    c[2] = p[2];
+
+    pz[0] = view_p[0] - c[0];
+    pz[1] = view_p[1] - c[1];
+    pz[2] = view_p[2] - c[2];
+
+    r = sqrt(pz[0] * pz[0] + pz[1] * pz[1] + pz[2] * pz[2]);
+
+    pz[0] /= r;
+    pz[1] /= r;
+    pz[2] /= r;
+    pz[3] = -(pz[0] * c[0] +
+              pz[1] * c[1] +
+              pz[2] * c[2]);
+
+    /* Find the plane giving the back of the ball, as seen from view_p. */
+
+    nz[0] = -pz[0];
+    nz[1] = -pz[1];
+    nz[2] = -pz[2];
+    nz[3] = -pz[3];
+
+    /* Reflect these planes as necessary, and store them in the GL state. */
+
+    pz[1] *= d;
+    nz[1] *= d;
+
+    glClipPlane(GL_CLIP_PLANE1, nz);
+    glClipPlane(GL_CLIP_PLANE2, pz);
+}
+
+static void game_draw_fore(int pose, const float *M, int d, float t)
 {
     const float *ball_p = file.uv->p;
     const float  ball_r = file.uv->r;
-    
-    glPushAttrib(GL_LIGHTING_BIT | GL_COLOR_BUFFER_BIT);
+
+    glPushMatrix();
     {
-        glPushMatrix();
-        {
-            /* Rotate the environment about the position of the ball. */
+        /* Rotate the environment about the position of the ball. */
 
-            glTranslatef(+ball_p[0], +ball_p[1] * d, +ball_p[2]);
-            glRotatef(-game_rz * d, view_e[2][0], view_e[2][1], view_e[2][2]);
-            glRotatef(-game_rx * d, view_e[0][0], view_e[0][1], view_e[0][2]);
-            glTranslatef(-ball_p[0], -ball_p[1] * d, -ball_p[2]);
+        game_draw_tilt(d);
 
-            if (d < 0)
-            {
-                GLdouble e[4];
+        /* Compute clipping planes for reflection and ball facing. */
 
-                e[0] = +0;
-                e[1] = +1;
-                e[2] = +0;
-                e[3] = -0.00001;
+        game_clip_refl(d);
+        game_clip_ball(d, ball_p);
 
-                glEnable(GL_CLIP_PLANE0);
-                glClipPlane(GL_CLIP_PLANE0, e);
-            }
+        if (d < 0)
+            glEnable(GL_CLIP_PLANE0);
+
+        if (pose)
+            sol_draw(&file, 0, 1);
+        else
+        {
+            /* Draw the coins. */
+
+            game_draw_items(&file, t);
 
             /* Draw the floor. */
 
-            sol_draw(&file);
+            sol_draw(&file, 0, 1);
+
+            /* Draw the ball shadow. */
 
-            if (config_get_d(CONFIG_SHADOW) && drawball)
+            if (d > 0 && config_get_d(CONFIG_SHADOW))
             {
                 shad_draw_set(ball_p, ball_r);
                 sol_shad(&file);
                 shad_draw_clr();
             }
 
-            /* Draw the game elements. */
+            /* Draw the ball. */
 
-            glEnable(GL_BLEND);
-            glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
+            game_draw_balls(&file, M, t);
+        }
+
+        /* Draw the particles and light columns. */
+
+        glEnable(GL_COLOR_MATERIAL);
+        glDisable(GL_LIGHTING);
+        glDepthMask(GL_FALSE);
+        {
+            glColor3f(1.0f, 1.0f, 1.0f);
+
+            sol_bill(&file, M, t);
+            part_draw_coin(M, t);
 
-            if (pose == 0)
+            glDisable(GL_TEXTURE_2D);
             {
-                part_draw_coin(-rx * d, -ry);
-                game_draw_coins(&file);
-                if (drawball)
-                    game_draw_balls(&file);
+                game_draw_goals(&file, M, t);
+                game_draw_jumps(&file);
+                game_draw_swchs(&file);
             }
-            game_draw_goals(&file, -rx * d, -ry);
-            game_draw_jumps(&file);
-            game_draw_swchs(&file);
+            glEnable(GL_TEXTURE_2D);
 
-            glDisable(GL_CLIP_PLANE0);
+            glColor3f(1.0f, 1.0f, 1.0f);
         }
-        glPopMatrix();
+        glDepthMask(GL_TRUE);
+        glEnable(GL_LIGHTING);
+        glDisable(GL_COLOR_MATERIAL);
+
+        if (d < 0)
+            glDisable(GL_CLIP_PLANE0);
     }
-    glPopAttrib();
+    glPopMatrix();
 }
 
-void game_draw(int pose, float st)
+void game_draw(int pose, float t)
 {
     float fov = view_fov;
 
@@ -447,66 +795,68 @@ void game_draw(int pose, float st)
         config_push_persp(fov, 0.1f, FAR_DIST);
         glPushMatrix();
         {
-            float v[3], rx, ry;
-            float pup[3];
-            float pdn[3];
+            float T[16], U[16], M[16], v[3];
 
-            v_cpy(pup, view_p);
-            v_cpy(pdn, view_p);
-            pdn[1] = -pdn[1];
+            /* Compute direct and reflected view bases. */
 
-            /* Compute and apply the view. */
+            v[0] = +view_p[0];
+            v[1] = -view_p[1];
+            v[2] = +view_p[2];
 
-            v_sub(v, view_c, view_p);
+            m_view(T, view_c, view_p, view_e[1]);
+            m_view(U, view_c, v,      view_e[1]);
+
+            m_xps(M, T);
 
-            rx = V_DEG(fatan2f(-v[1], fsqrtf(v[0] * v[0] + v[2] * v[2])));
-            ry = V_DEG(fatan2f(+v[0], -v[2])) + st;
+            /* Apply current the view. */
+
+            v_sub(v, view_c, view_p);
 
             glTranslatef(0.f, 0.f, -v_len(v));
-            glRotatef(rx, 1.f, 0.f, 0.f);
-            glRotatef(ry, 0.f, 1.f, 0.f);
+            glMultMatrixf(M);
             glTranslatef(-view_c[0], -view_c[1], -view_c[2]);
 
             if (config_get_d(CONFIG_REFLECTION))
             {
-                /* Draw the mirror only into the stencil buffer. */
-
-                glDisable(GL_DEPTH_TEST);
                 glEnable(GL_STENCIL_TEST);
-                glStencilFunc(GL_ALWAYS, 1, 0xFFFFFFFF);
-                glStencilOp(GL_REPLACE, GL_REPLACE, GL_REPLACE);
-                glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);
+                {
+                    /* Draw the mirrors only into the stencil buffer. */
 
-                game_refl_all(0);
+                    glStencilFunc(GL_ALWAYS, 1, 0xFFFFFFFF);
+                    glStencilOp(GL_REPLACE, GL_REPLACE, GL_REPLACE);
+                    glColorMask(GL_FALSE, GL_FALSE, GL_FALSE, GL_FALSE);
+                    glDepthMask(GL_FALSE);
 
-                /* Draw the scene reflected into color and depth buffers. */
+                    game_refl_all();
 
-                glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
-                glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
-                glStencilFunc(GL_EQUAL, 1, 0xFFFFFFFF);
-                glEnable(GL_DEPTH_TEST);
+                    glDepthMask(GL_TRUE);
+                    glColorMask(GL_TRUE, GL_TRUE, GL_TRUE, GL_TRUE);
+                    glStencilOp(GL_KEEP, GL_KEEP, GL_KEEP);
+                    glStencilFunc(GL_EQUAL, 1, 0xFFFFFFFF);
 
-                glFrontFace(GL_CW);
-                glPushMatrix();
-                {
-                    glScalef(+1.f, -1.f, +1.f);
+                    /* Draw the scene reflected into color and depth buffers. */
 
-                    game_draw_light();
-                    game_draw_back(pose,         -1, pdn);
-                    game_draw_fore(pose, rx, ry, -1, pdn);
-                }
-                glPopMatrix();
-                glFrontFace(GL_CCW);
+                    glFrontFace(GL_CW);
+                    glPushMatrix();
+                    {
+                        glScalef(+1.0f, -1.0f, +1.0f);
 
+                        game_draw_light();
+                        game_draw_back(pose,    -1, t);
+                        game_draw_fore(pose, U, -1, t);
+                    }
+                    glPopMatrix();
+                    glFrontFace(GL_CCW);
+                }
                 glDisable(GL_STENCIL_TEST);
             }
 
             /* Draw the scene normally. */
 
             game_draw_light();
-            game_refl_all(pose ? 0 : config_get_d(CONFIG_SHADOW));
-            game_draw_back(pose,         +1, pup);
-            game_draw_fore(pose, rx, ry, +1, pup);
+            game_refl_all();
+            game_draw_back(pose,    +1, t);
+            game_draw_fore(pose, T, +1, t);
         }
         glPopMatrix();
         config_pop_matrix();
@@ -521,10 +871,8 @@ void game_draw(int pose, float st)
 
 static void game_update_grav(float h[3], const float g[3])
 {
-    struct s_file *fp = &file;
-
     float x[3];
-    float y[3] = { 0.f, 1.f, 0.f };
+    float y[3] = { 0.0f, 1.0f, 0.0f };
     float z[3];
     float X[16];
     float Z[16];
@@ -532,7 +880,10 @@ static void game_update_grav(float h[3], const float g[3])
 
     /* Compute the gravity vector from the given world rotations. */
 
-    v_sub(z, view_p, fp->uv->p);
+    z[0] = fsinf(V_RAD(view_a));
+    z[1] = 0.0f;
+    z[2] = fcosf(V_RAD(view_a));
+
     v_crs(x, y, z);
     v_crs(z, x, y);
     v_nrm(x, x);
@@ -547,44 +898,42 @@ static void game_update_grav(float h[3], const float g[3])
 static void game_update_view(float dt)
 {
     float dc = view_dc * (jump_b ? 2.0f * fabsf(jump_dt - 0.5f) : 1.0f);
-    float dx = view_ry * dt * 5.0f;
+    float da = input_get_r() * dt * 90.0f;
     float k;
 
-    view_a += view_ry * dt * 90.f;
+    float M[16], v[3], Y[3] = { 0.0f, 1.0f, 0.0f };
+
+    view_a += da;
 
     /* Center the view about the ball. */
 
     v_cpy(view_c, file.uv->p);
     v_inv(view_v, file.uv->v);
 
-    switch (config_get_d(CONFIG_CAMERA))
+    view_e[2][0] = fsinf(V_RAD(view_a));
+    view_e[2][1] = 0.0;
+    view_e[2][2] = fcosf(V_RAD(view_a));
+
+    switch (input_get_c())
     {
-    case 1: /* Camera 1:  Viewpoint chases the ball position. */
+    case 1: /* Camera 1: Viewpoint chases the ball position. */
+
+        /* TODO: This camera no longer exists. */
 
-        v_sub(view_e[2], view_p, view_c);
         break;
 
     case 2: /* Camera 2: View vector is given by view angle. */
 
-        view_e[2][0] = fsinf(V_RAD(view_a));
-        view_e[2][1] = 0.f;
-        view_e[2][2] = fcosf(V_RAD(view_a));
-
-        dx = 0.0f;
-
         break;
 
     default: /* Default: View vector approaches the ball velocity vector. */
 
-        k = v_dot(view_v, view_v);
-
-        v_sub(view_e[2], view_p, view_c);
-        v_mad(view_e[2], view_e[2], view_v, k * dt / 4);
+        v_mad(view_e[2], view_e[2], view_v, v_dot(view_v, view_v) * dt / 4);
 
         break;
     }
 
-    /* Orthonormalize the basis of the view in its new position. */
+    /* Orthonormalize the new view reference frame. */
 
     v_crs(view_e[0], view_e[1], view_e[2]);
     v_crs(view_e[2], view_e[0], view_e[1]);
@@ -599,10 +948,11 @@ static void game_update_view(float dt)
 
     if (view_k < 0.5) view_k = 0.5;
 
-    v_cpy(view_p, file.uv->p);
-    v_mad(view_p, view_p, view_e[0], dx      * view_k);
-    v_mad(view_p, view_p, view_e[1], view_dp * view_k);
-    v_mad(view_p, view_p, view_e[2], view_dz * view_k);
+    v_scl(v,    view_e[1], view_dp * view_k);
+    v_mad(v, v, view_e[2], view_dz * view_k);
+    m_rot(M, Y, V_RAD(da));
+    m_vxfm(view_p, M, v);
+    v_add(view_p, view_p, file.uv->p);
 
     /* Compute the new view center. */
 
@@ -621,53 +971,64 @@ static void game_update_time(float dt, int b)
 
    /* The ticking clock. */
 
-    if (b && clock_down)
+    if (b && timer_down)
     {
-        if (clock < 600.f)
-            clock -= dt;
-        if (clock < 0.f)
-            clock = 0.f;
+        if (timer < 600.f)
+            timer -= dt;
+        if (timer < 0.f)
+            timer = 0.f;
     }
     else if (b)
     {
-        clock += dt;
+        timer += dt;
     }
 }
 
-static int game_update_state(int *state_value)
+static int game_update_state(int bt)
 {
     struct s_file *fp = &file;
+    struct s_goal *zp;
+    struct s_item *hp;
+
     float p[3];
     float c[3];
-    int bt = state_value != NULL;
-    int n;
-    struct s_goal *g;
 
-    /* Test for a coin grab. */
-    
-    if (bt && (n = sol_coin_test(fp, p, COIN_RADIUS)) > 0)
+    /* Test for an item. */
+
+    if (bt && (hp = sol_item_test(fp, p, COIN_RADIUS)))
     {
-        coin_color(c, n);
+        const char *sound = AUD_COIN;
+
+        item_color(hp, c);
         part_burst(p, c);
 
-        coins += n;
-        /* Check for goal open. */
-        if (goal_c > 0)
+        grow_init(fp, hp->t);
+
+        if (hp->t == ITEM_COIN)
         {
-            goal_c = goal_c - n;
-            if (goal_c <= 0)
+            coins += hp->n;
+
+            /* Check for goal open. */
+
+            if (goal_c > 0)
             {
-                audio_play(AUD_SWITCH, 1.f);
-                goal_c = 0;
+                goal_c -= hp->n;
+                if (goal_c <= 0)
+                {
+                    sound = AUD_SWITCH;
+                    goal_c = 0;
+                }
             }
-            else
-                audio_play(AUD_COIN, 1.f);
-        } 
-        else
-            audio_play(AUD_COIN, 1.f);
+        }
+        audio_play(sound, 1.f);
+
+        /* Reset item type. */
+
+        hp->t = ITEM_NONE;
     }
 
     /* Test for a switch. */
+
     if (sol_swch_test(fp, 0))
         audio_play(AUD_SWITCH, 1.f);
 
@@ -678,7 +1039,7 @@ static int game_update_state(int *state_value)
         jump_b  = 1;
         jump_e  = 0;
         jump_dt = 0.f;
-        
+
         audio_play(AUD_JUMP, 1.f);
     }
     if (jump_e == 0 && jump_b == 0 && sol_jump_test(fp, jump_p, 0) == 0)
@@ -686,26 +1047,16 @@ static int game_update_state(int *state_value)
 
     /* Test for a goal. */
 
-    if (bt && goal_c == 0 && (g = sol_goal_test(fp, p, 0)))
+    if (bt && goal_c == 0 && (zp = sol_goal_test(fp, p, 0)))
     {
-        *state_value = g->s;
         audio_play(AUD_GOAL, 1.0f);
-        return g->c ? GAME_SPEC : GAME_GOAL;
+        return GAME_GOAL;
     }
 
     /* Test for time-out. */
 
-    if (bt && clock_down && clock <= 0.f)
+    if (bt && timer_down && timer <= 0.f)
     {
-        const GLfloat *p = fp->uv->p;
-        const GLfloat c[5] = {1.0f, 1.0f, 0.0f, 0.0f, 1.0f};
-        part_burst(p, c);
-        part_burst(p, c+1);
-        part_burst(p, c+2);
-        part_burst(p, c);
-        part_burst(p, c+1);
-        part_burst(p, c+2);
-        drawball = 0;
         audio_play(AUD_TIME, 1.0f);
         return GAME_TIME;
     }
@@ -721,144 +1072,102 @@ static int game_update_state(int *state_value)
     return GAME_NONE;
 }
 
-/*
- * On  most  hardware, rendering  requires  much  more  computing power  than
- * physics.  Since  physics takes less time  than graphics, it  make sense to
- * detach  the physics update  time step  from the  graphics frame  rate.  By
- * performing multiple physics updates for  each graphics update, we get away
- * with higher quality physics with little impact on overall performance.
- *
- * Toward this  end, we establish a  baseline maximum physics  time step.  If
- * the measured  frame time  exceeds this  maximum, we cut  the time  step in
- * half, and  do two updates.  If THIS  time step exceeds the  maximum, we do
- * four updates.  And  so on.  In this way, the physics  system is allowed to
- * seek an optimal update rate independant of, yet in integral sync with, the
- * graphics frame rate.
- */
-
-int game_step(const float g[3], float dt, int *state_value)
+int game_step(const float g[3], float dt, int bt)
 {
-    struct s_file *fp = &file;
-
-    float h[3];
-    float d = 0.f;
-    float b = 0.f;
-    float t;
-    int i, n = 1;
-
     if (game_state)
     {
-        t = dt;
+        struct s_file *fp = &file;
+
+        float h[3];
 
         /* Smooth jittery or discontinuous input. */
 
-        if (t < RESPONSE)
-        {
-            game_rx += (game_ix - game_rx) * t / RESPONSE;
-            game_rz += (game_iz - game_rz) * t / RESPONSE;
-        }
-        else
-        {
-            game_rx = game_ix;
-            game_rz = game_iz;
-        }
+        game_rx += (input_get_x() - game_rx) * dt / RESPONSE;
+        game_rz += (input_get_z() - game_rz) * dt / RESPONSE;
+
+        grow_step(fp, dt);
 
         game_update_grav(h, g);
-        part_step(h, t);
+        part_step(h, dt);
 
-        if (!drawball)
-                /* nothing */;
-        else if (jump_b)
+        if (jump_b)
         {
-            jump_dt += t;
+            jump_dt += dt;
 
             /* Handle a jump. */
 
-            if (0.5 < jump_dt)
+            if (0.5f < jump_dt)
             {
                 fp->uv[0].p[0] = jump_p[0];
                 fp->uv[0].p[1] = jump_p[1];
                 fp->uv[0].p[2] = jump_p[2];
             }
-            if (1.f < jump_dt)
+            if (1.0f < jump_dt)
                 jump_b = 0;
         }
         else
         {
             /* Run the sim. */
 
-            while (t > MAX_DT && n < MAX_DN)
-            {
-                t /= 2;
-                n *= 2;
-            }
-
-            for (i = 0; i < n; i++)
-                if (b < (d = sol_step(fp, h, t, 0, NULL)))
-                    b = d;
+            float b = sol_step(fp, h, dt, 0, NULL);
 
             /* Mix the sound of a ball bounce. */
 
-            if (b > 0.5)
-                audio_play(AUD_BUMP, (b - 0.5f) * 2.0f);
+            if (b > 0.5f)
+            {
+                float k = (b - 0.5f) * 2.0f;
+
+                if (got_orig)
+                {
+                    if      (fp->uv->r > grow_orig) audio_play(AUD_BUMPL, k);
+                    else if (fp->uv->r < grow_orig) audio_play(AUD_BUMPS, k);
+                    else                            audio_play(AUD_BUMPM, k);
+                }
+                else audio_play(AUD_BUMPM, k);
+            }
         }
 
         game_step_fade(dt);
         game_update_view(dt);
-        game_update_time(dt, state_value != NULL);
+        game_update_time(dt, bt);
 
-        return game_update_state(state_value);
+        return game_update_state(bt);
     }
     return GAME_NONE;
 }
 
 /*---------------------------------------------------------------------------*/
 
-void game_no_aa(void)
+void game_set_x(int k)
 {
-    float max = game_ix * game_ix + game_iz * game_iz;
-    if (max > ANGLE_BOUND * ANGLE_BOUND)
-    {
-        max = ANGLE_BOUND / sqrt(max);
-        game_ix *= max;
-        game_iz *= max;
-    }
+    input_set_x(-ANGLE_BOUND * k / JOY_MAX);
 }
 
-void game_set_x(int k)
+void game_set_z(int k)
 {
-    game_ix = -(ANGLE_BOUND) * k / JOY_MAX;
-#if NO_AA
-    game_no_aa();
-#endif
+    input_set_z(+ANGLE_BOUND * k / JOY_MAX);
 }
 
-void game_set_z(int k)
+void game_set_ang(int x, int z)
 {
-    game_iz = +ANGLE_BOUND * k / JOY_MAX;
-#if NO_AA
-    game_no_aa();
-#endif
+    input_set_x(x);
+    input_set_z(z);
 }
 
 void game_set_pos(int x, int y)
 {
-    game_ix += 40.f * y / config_get_d(CONFIG_MOUSE_SENSE);
-    game_iz += 40.f * x / config_get_d(CONFIG_MOUSE_SENSE);
-    
-#if NO_AA
-    game_no_aa();
-#else
-    if (game_ix > +ANGLE_BOUND) game_ix = +ANGLE_BOUND;
-    if (game_ix < -ANGLE_BOUND) game_ix = -ANGLE_BOUND;
-    if (game_iz > +ANGLE_BOUND) game_iz = +ANGLE_BOUND;
-    if (game_iz < -ANGLE_BOUND) game_iz = -ANGLE_BOUND;
-#endif
+    input_set_x(input_get_x() + 40.0f * y / config_get_d(CONFIG_MOUSE_SENSE));
+    input_set_z(input_get_z() + 40.0f * x / config_get_d(CONFIG_MOUSE_SENSE));
+}
+
+void game_set_cam(int c)
+{
+    input_set_c(c);
 }
 
 void game_set_rot(float r)
 {
-    view_ry = r;
+    input_set_r(r);
 }
 
 /*---------------------------------------------------------------------------*/
@@ -967,45 +1276,3 @@ void game_fade(float d)
 }
 
 /*---------------------------------------------------------------------------*/
-
-int put_game_state(FILE *fout)
-{
-    if (game_state)
-    {
-        /* Write the view and tilt state. */
-
-        put_float(fout, &game_rx);
-        put_float(fout, &game_rz);
-        put_array(fout,  view_c, 3);
-        put_array(fout,  view_p, 3);
-
-        /* Write the game simulation state. */
-
-        put_file_state(fout, &file);
-
-        return 1;
-    }
-    return 0;
-}
-
-int get_game_state(FILE *fin)
-{
-    if (game_state)
-    {
-        /* Read the view and tilt state. */
-
-        get_float(fin, &game_rx);
-        get_float(fin, &game_rz);
-        get_array(fin,  view_c, 3);
-        get_array(fin,  view_p, 3);
-
-        /* Read the game simulation state. */
-
-        get_file_state(fin, &file);
-
-        return (feof(fin) ? 0 : 1);
-    }
-    return 0;
-}
-
-/*---------------------------------------------------------------------------*/