#include <vectrex.h>
#define PREFER_ACCURATE 1 /* don't define, to avoid longs and take some shortcuts in the arithmetic */
#define SLOWMO 1 /* change to 20 to slow down rotation for debugging */
/*
* The graphics here are an approximation to the missing game Cube Quest. It was
* not one of the original GCE games but did come from around that time. The
* developer, Paul Allen Newell <pnewell@cs.cmu.edu>, prototyped a game for a
* laserdisc system using the Vectrex, but the Vectrex version was far from
* complete and not really very playable.
* Cube Quest was later realized in 1983/84 as a hybrid vector/raster arcade
* game by Simutrek, but the author was not entirely happy with it, as it was
* rushed out before it had been fully developed.
* There are videos of the Vectrex version of Cube Quest such as:
* https://www.youtube.com/watch?v=n8vRh4XAsDg
* From the latter video it's clear that the intention was to make it a multigame,
* with each movement across the cube being permitted only if you complete
* another mission, with the subgames being like Black Widow, Terror Tubes
* etc - in fact also looking a lot like some of ClockworkRobot's smaller games.
* Several Youtube videos of the laser version of Cube Quest are available, such as:
* https://www.youtube.com/watch?v=nNGBFEeEvKM and https://www.youtube.com/watch?v=xsfq2PXTxSA
* Also worth a read:
* http://www.ataricompendium.com/archives/interviews/paul_allen_newell/interview_paul_allen_newell.html
* This rewrite is currently very much a Work-In-Progress (WIP) - there is no game logic yet.
* As discussed on the Facebook Vectrex forum, it would be nice to finish this rewrite to be
* compatible with the Vectrex Cube Quest (which was partially released at a retro gaming show,
* but has never been dumped and released to the public), and then to go on and add new original
* minigames in the spirit of the arcade Cube Quest.
* This must be compiled in the Vide environment using gcc's -O option in order to acheive acceptable
* frame rates.
* The first version of this code used pre-computed lookup tables for the animation, but
* after some experimentation, it was found that the benefit in speed did not justify the
* added complexity or amount of RAM used. Table-driving the animations may still be an
* option but if we go that route it needs to be done differently from before.
*
*/
#define MAX_BRIGHTNESS 0x7fU
#define BRIGHT 0x7fU
#define NORMAL 0x3fU
#define DIM 0x28U // adjusted for my vectrex, not for the emulator
#define MIN_SCALE 0x0fU
#define PREFERRED_SCALE 0x78U
#define MAX_SCALE 0xf0U
#define SCREEN_X_ORIGIN (-80) /* where to place the lower left corner of the cube */
static int beam_x = 0, beam_y = 0;
static long int target_x = 0L, target_y = 0L;
static int vectors_drawn = 0;
static unsigned int Old_Intensity = 0;
// wrap move and draw procedures to allow automatic insertion of Reset0Ref
// in order to keep image stable.
static inline void Move(int y, int x) {
target_x += (long)x; target_y += (long)y;
}
static void Line(long SuppressMask, long BelongsIn, unsigned int Intensity, int y, int x) {
// If the line belongs in a particular plane, and the suppression mask is set for
// that plane, then the line is not drawn. Planes are defined perpendicular to
// the given axis.
long int dx, dy;
if (Old_Intensity != Intensity) {
Intensity_a(Old_Intensity = Intensity); // repeated procedure call overhead is worth avoiding
}
if ((beam_x != target_x) || (beam_y != target_y)) {
if (vectors_drawn >= 2) {// 150 cycles saved per increment. optimum appearance is 2
Reset0Ref(); beam_x = 0; beam_y = 0; vectors_drawn = 0;
}
dy = target_y-(long)beam_y; dx = target_x-(long)beam_x;
if (dx > 127L || dx < -128L || dy > 127L || dy < -128L) {
// Too big for a single move. So halve the distance and move twice.
Moveto_d((int)(dy>>1), (int)(dx>>1)); dx -= dx>>1; dy -= dy>>1; // bring the move back into range
}
Moveto_d((int)dy, (int)dx);
beam_x = (int)target_x; beam_y = (int)target_y;
}
if ((SuppressMask&BelongsIn) == 0) Draw_Line_d(y, x); else Moveto_d(y, x); // might be able to optimise this by calling Move()
vectors_drawn += 1;
beam_x = (int)(target_x)+x; beam_y = (int)target_y+y;
target_x = (long)beam_x; target_y = (long)beam_y;
}
// The rot sets in...
#define ROTATION_STEPS 32
int rotation = (0==0);
// This is a dimetric projection. If we want a trimetric projection,
// to copy the slight tilt of GCE's Cube Quest, then we'll need a Z parameter
// as well; but for now this is good enough...
#ifdef PREFER_ACCURATE
#define ONE 16384
typedef long POINT;
typedef long long WPOINT; /* Wide point */
#define S 0
const long sine[ROTATION_STEPS+1] = {
#define fpsin(theta) sine[theta]
#define fpcos(theta) sine[(int)ROTATION_STEPS-theta]
#define DIV 14 /* sin/cos now in range 0..16k-1 */
#else
#define ONE 16383
typedef int POINT; // in case we need to switch to long ...
typedef long WPOINT;
#define S 7
const int sine[ROTATION_STEPS+1] = {
#define fpsin(theta) sine[theta]
#define fpcos(theta) sine[(int)ROTATION_STEPS-theta]
#define DIV 7 /* sin/cos now in range 0..63 */
#endif
0>>S, 803>>S, 1605>>S, 2404>>S, 3196>>S, 3980>>S, 4756>>S, 5519>>S,
6269>>S, 7005>>S, 7723>>S, 8423>>S, 9102>>S, 9759>>S, 10393>>S, 11002>>S,
11585>>S, 12139>>S, 12665>>S, 13159>>S, 13622>>S, 14053>>S, 14449>>S, 14810>>S,
15136>>S, 15426>>S, 15678>>S, 15892>>S, 16069>>S, 16206>>S, 16305>>S, 16364>>S, ONE>>S /* scaled 0:1 -> 0:16384 */
};
// because of the way that the sin table was created, it's possible that the calculated cos
// value is off by 1/64th of 90 degrees! A mismatch between sin and cos for the same angle
// could explain the 'wobble' seen during rotations. Need to re-calculate this table...
static inline void map2dYX_to_dimetric(POINT x, POINT y, POINT *screeny, POINT *screenx) {
//for every 20 x added, X' increases by 50.
//for every 20 y added, X' increases by 30
//for every 20 y added, Y' increases by 20
//for every 20 x added, Y' increases by -10
// X' = x*50/20 + y*30/20
// Y' = y*20/20 - x*10/20
// X' = x*5/2 + y*3/2
// Y' = y - x/2
POINT tmp = x>>1L; // accept 1 pt of rounding error for now...
#ifdef PREFER_ACCURATE
*screenx = (int)(((long)x*5L)>>1L) + y + (y>>1); *screeny = y - tmp;
#else
*screenx = tmp*5 + y + (y>>1); *screeny = y - tmp;
#endif
}
// generated similarly...
static inline void map2dZY_to_dimetric(POINT z, POINT y, POINT *screeny, POINT *screenx) {
*screenx = (y>>1)+y;
*screeny = (z<<1)+y;
}
static inline void map2dZX_to_dimetric(POINT z, POINT x, POINT *screeny, POINT *screenx) {
*screeny = (z<<1) - (x>>1);
//*screenx = (x*5)>>1;
#ifdef PREFER_ACCURATE
*screenx = (int)(((long)x*5L)>>1L);
#else
*screenx = (x<<1) + (x>>1); //I think that's right. Untested so far.
#endif
}
#define BASE_Y (0)
#define BASE_X (0)
#define FORTY_Y (40)
#define SIXTY_X (60)
#define MINUSTWENTY_Y (-20)
#define HUNDRED_X (100)
#define X0 1L
#define X1 2L
#define X2 4L
#define Y0 8L
#define Y1 16L
#define Y2 32L
#define Z0 64L
#define Z1 128L
#define Z2 256L
static inline long Suppress(int axis, int plane) {
int planebit = 1<<plane; // group of 3 bits: 210
return (long)planebit << ((long)(axis-'X')*3L); // 3 groups: ZYX
}
static void draw_cube(long SuppressMask) {
// The suppress mask tells the cube drawing code to not draw specific planes,
// It defines an axis and a plane within that axis. Every line-drawing call
// in the cube drawing procedure checks to see if it is disabled, which it
// will be if the corresponding bit is set. A line may belong to multiple
// planes and faces, so it will act on the OR of three separate tests, which
// it can do efficiently with a single bitmask.
// The face rotation will now be done separately from the cube drawing.
// Calling 'draw cube' with Suppress == 0 will draw the entire cube.
// X Y Z
// Suppress mask: 111 111 111
// 012 012 012
int screen_x_origin = SCREEN_X_ORIGIN;
int z = 0;
// precompute commonly used values for speed...
// although doing these on each plane is a bit wasteful! Could be done
// once and stored in tables for each rotation angle...
#define Hidden DIM
#define Visible NORMAL
// I know. At one point these were variables. I'll fix it uo soon...
#define forty_y FORTY_Y
#define sixty_x SIXTY_X
#define minustwenty_y MINUSTWENTY_Y
#define hundred_x HUNDRED_X
#define minusforty_y (-forty_y)
#define minussixty_x (-sixty_x)
#define twenty_y (-minustwenty_y)
#define minushundred_x (-hundred_x)
#define half_of_forty_y (forty_y>>1)
#define half_of_sixty_x (sixty_x>>1)
#define half_of_minustwenty_y (minustwenty_y>>1)
#define half_of_hundred_x (hundred_x>>1)
#define half_of_minusforty_y (minusforty_y>>1)
#define half_of_minussixty_x (minussixty_x>>1)
#define half_of_twenty_y (twenty_y>>1)
#define half_of_minushundred_x (minushundred_x>>1)
/////////////////////////////////////////////////////////////////// Z = 0
z = 0; Reset0Ref(); target_x = beam_x = 0; target_y = beam_y = 0; vectors_drawn = 0;
Move(40,screen_x_origin);
Move(BASE_Y, BASE_X);
// Key: 'Z0' means the plane that can rotate around the Z axis, i.e. the X/Y plane, level 0 (top)
Line(SuppressMask, X0|Z0, Visible, forty_y, sixty_x); // horizontal front left corner to rear left corner
Line(SuppressMask, Y2|Z0, Visible, minustwenty_y, hundred_x); // horizontal rear left corner to rear right corner
Move(half_of_twenty_y, half_of_minushundred_x);
Line(SuppressMask, X1|Z0, Visible, minusforty_y, minussixty_x);
Move(forty_y,sixty_x); // back to front horizontal cross-member
Move(half_of_minustwenty_y, half_of_hundred_x);
Line(SuppressMask, X2|Z0, Visible, minusforty_y, minussixty_x); // horizontal rear right corner to front right corner
Move(half_of_forty_y, half_of_sixty_x);
Line(SuppressMask, Y1|Z0, Visible, twenty_y, minushundred_x);
Move(minustwenty_y,hundred_x); //right to left cross-member
Move(half_of_minusforty_y, half_of_minussixty_x);
Line(SuppressMask, Y0|Z0, Visible, twenty_y, minushundred_x); // horizontal front (right to left)
Move(minustwenty_y, hundred_x);
/////////////////////////////////////////////////////////////////// Z = 1
z = 1; Reset0Ref(); target_x = beam_x = 0; target_y = beam_y = 0; vectors_drawn = 0;
Move(0,screen_x_origin);
Move(BASE_Y, BASE_X);
Line(SuppressMask, X0|Z1, Hidden, forty_y, sixty_x); // horizontal front left corner to rear left corner
Line(SuppressMask, Y2|Z1, Hidden, minustwenty_y, hundred_x); // horizontal rear left corner to rear right corner
Move(half_of_twenty_y, half_of_minushundred_x);
Line(SuppressMask, X1|Z1, Hidden, minusforty_y, minussixty_x); Move(forty_y,sixty_x); // back to front horizontal cross-member
Move(half_of_minustwenty_y, half_of_hundred_x);
Line(SuppressMask, X2|Z1, Visible, minusforty_y, minussixty_x); // horizontal rear right corner to front right corner
Move(half_of_forty_y, half_of_sixty_x);
Line(SuppressMask, Y1|Z1, Hidden, twenty_y, minushundred_x);
Move(minustwenty_y,hundred_x); //right to left cross-member
Move(half_of_minusforty_y, half_of_minussixty_x);
Line(SuppressMask, Y0|Z1, Visible, twenty_y, minushundred_x); // horizontal front (right to left)
Move(minustwenty_y, hundred_x);
/////////////////////////////////////////////////////////////////// Z = 2
z = 2; Reset0Ref(); target_x = beam_x = 0; target_y = beam_y = 0; vectors_drawn = 0;
Move(-40,screen_x_origin);
Move(BASE_Y, BASE_X);
{ // VERTICALS
Line(SuppressMask, X0|Y0, Visible, 80, 0); Move(-80, 0); // Leftmost pillar
}
Line(SuppressMask, X0|Z2, Hidden, forty_y, sixty_x); // horizontal front left corner to rear left corner
{ // VERTICALS
Line(SuppressMask, X0|Y2, Hidden, 80, 0); Move(-80, 0); // back-left corner vertical
Move(-20, -30);
Line(SuppressMask, X0|Y1, Hidden, 80, 0); Move(-80, 0); // hidden left face center vertical
Move(20, 30);
}
Line(SuppressMask, Y2|Z2, Hidden, minustwenty_y, hundred_x); // horizontal rear left corner to rear right corner
Move(half_of_twenty_y, half_of_minushundred_x);
Line(SuppressMask, X1|Z2, Hidden, minusforty_y, minussixty_x); Move(forty_y,sixty_x); // back to front horizontal cross-member
{ // VERTICALS
Line(SuppressMask, X1|Y2, Hidden, 80, 0); Move(-80, 0); // rear face center vertical
Move(-20,-30); Line(SuppressMask, X1|Y1, Hidden, 80, 0); Move(-80, 0); Move(20,30); // center vertical
}
Move(half_of_minustwenty_y, half_of_hundred_x);
{ // VERTICALS
Line(SuppressMask, Y2|X2, Visible, 80, 0); Move(-80, 0); // back right corner vertical
}
Line(SuppressMask, X2|Z2, Visible, minusforty_y, minussixty_x); // horizontal rear right corner to front right corner
Move(half_of_forty_y, half_of_sixty_x);
Line(SuppressMask, Y1|Z2, Hidden, twenty_y, minushundred_x);
Move(minustwenty_y,hundred_x); //right to left cross-member
if (z == 2) {Line(SuppressMask, X2|Y1, Visible, 80, 0); Move(-80, 0);} // right face center vertical
Move(half_of_minusforty_y, half_of_minussixty_x);
if (z == 2) {Line(SuppressMask, X2|Y0, Visible, 80, 0); Move(-80, 0);} // front right corner vertical
Line(SuppressMask, Y0|Z2, Visible, twenty_y, minushundred_x); // horizontal front (right to left)
Move(half_of_minustwenty_y, half_of_hundred_x);
if (z == 2) {Line(SuppressMask, X1|Y0, Visible, 80, 0); Move(-80, 0);} // front center vertical
Move(half_of_minustwenty_y, half_of_hundred_x);
/////////////////////////////////////////////////////////////////////////////// end
}
static int delay=0;
static int theta = 0;
static int turn_direction = 1;
void draw_rotating_plane(int axis, int plane) {
int screen_x_origin = SCREEN_X_ORIGIN;
int X, Y, X_ll = -20, Y_ll = -20, X_ul = -20, Y_ul = 20, X_ur = 20, Y_ur = 20;
POINT p0x,p0y,p1x,p1y,p2x,p2y;
Reset0Ref(); target_x = beam_x = 0; target_y = beam_y = 0; vectors_drawn = 0;
if (axis == 'X') {
Move(plane*-10-40, screen_x_origin+plane*50);
} else if (axis == 'Y') {
Move(-40+plane*20, screen_x_origin+plane*30);
} else {
Move(40-plane*40, -80);
}
// calculating this on the fly is a bit more expensive than the pre-computed version
// (42K cycles per frame) but I'm tempted to live with it. It only reduces the frame
// rate below optimum during a short animation sequence which in the game will not run
// very often...
// (and if you cheat by doing the rotation only during the scaling-up, it always
// keeps to around 30K cycles per frame - if necessary, design that into the game...)
// (X_ll,Y_ll) ->
X = (int)((((WPOINT)X_ll * (WPOINT)fpcos(theta)) >> DIV) - (((WPOINT)Y_ll * (WPOINT)fpsin(theta)) >> DIV));
Y = (int)((((WPOINT)Y_ll * (WPOINT)fpcos(theta)) >> DIV) + (((WPOINT)X_ll * (WPOINT)fpsin(theta)) >> DIV));
X += 20; Y += 20;
// and map to dimetric projection...
if (axis == 'X') {
map2dZY_to_dimetric(Y,X, &p0y,&p0x);
} else if (axis == 'Y') {
map2dZX_to_dimetric(Y,X, &p0y,&p0x);
} else {
map2dYX_to_dimetric(Y,X, &p0y,&p0x);
}
// (X_ul,Y_ul) ->
X = (int)((((WPOINT)X_ul * (WPOINT)fpcos(theta)) >> DIV) - (((WPOINT)Y_ul * (WPOINT)fpsin(theta)) >> DIV));
Y = (int)((((WPOINT)Y_ul * (WPOINT)fpcos(theta)) >> DIV) + (((WPOINT)X_ul * (WPOINT)fpsin(theta)) >> DIV));
X += 20; Y += 20;
if (axis == 'X') {
map2dZY_to_dimetric(Y,X, &p1y,&p1x);
} else if (axis == 'Y') {
map2dZX_to_dimetric(Y,X, &p1y,&p1x);
} else {
map2dYX_to_dimetric(Y,X, &p1y,&p1x);
}
// (X_ur,Y_ur) ->
X = (int)((((WPOINT)X_ur * (WPOINT)fpcos(theta)) >> DIV) - (((WPOINT)Y_ur * (WPOINT)fpsin(theta)) >> DIV));
Y = (int)((((WPOINT)Y_ur * (WPOINT)fpcos(theta)) >> DIV) + (((WPOINT)X_ur * (WPOINT)fpsin(theta)) >> DIV));
X += 20; Y += 20;
if (axis == 'X') {
map2dZY_to_dimetric(Y,X, &p2y,&p2x);
} else if (axis == 'Y') {
map2dZX_to_dimetric(Y,X, &p2y,&p2x);
} else {
map2dYX_to_dimetric(Y,X, &p2y,&p2x);
}
Move((int)p0y, (int)p0x);
Line(0, 0, BRIGHT, (int)(p1y-p0y), (int)(p1x-p0x)); // p0 to p1
Move((int)(p0y-p1y)>>1, (int)(p0x-p1x)>>1);
Line(0, 0, BRIGHT, (int)(p2y-p1y), (int)(p2x-p1x)); Move((int)(p1y-p2y), (int)(p1x-p2x));
Move((int)(p1y-p0y)>>1, (int)(p1x-p0x)>>1);
Line(0, 0, BRIGHT, (int)(p2y-p1y), (int)(p2x-p1x)); // p1 to p2
Move((int)(p1y-p2y)>>1, (int)(p1x-p2x)>>1);
Line(0, 0, BRIGHT, (int)(p0y-p1y), (int)(p0x-p1x)); Move((int)(p1y-p0y), (int)(p1x-p0x));
Move((int)(p2y-p1y)>>1, (int)(p2x-p1x)>>1);
Line(0, 0, BRIGHT, (int)(p0y-p1y), (int)(p0x-p1x)); // p3 (-p1) to p0
Line(0, 0, BRIGHT, (int)(p1y-p2y), (int)(p1x-p2x)); // p2 to p3 (-p1)
delay = delay+1;
if (delay == SLOWMO) { // set delay to something like 20 to see the details of the rotation wobble
theta = (theta + turn_direction) & 31;
if (theta == 0) {
rotation = (0 != 0); // tell the level above that we've finished a 90 degree rotation
turn_direction = 0 - turn_direction;
}
delay = 0;
}
}
int main(void)
{
static long countdown = 0L;
static char *message = "X AXIS PLANE 0\x80";
int demo_axis = 'Z', demo_plane = 0;
int rotate_level;
unsigned int scale = MIN_SCALE;
int delta_scale = 2;
// Precompute the corners of the square under rotation and fill the table with the delta-moves.
// from one corner to the next.
rotate_level = 0; rotation = (0==0);
theta = 0;
for (;;) {
Wait_Recal();
Intensity_a(Old_Intensity = BRIGHT); /* set some brightness */
message[0] = (char)demo_axis; message[14] = (char)demo_plane + '0';
Print_Str_d(-80, -84, message);
VIA_t1_cnt_lo = scale; /* set scale factor */
if (rotation) {
draw_cube(Suppress(demo_axis, demo_plane));
draw_rotating_plane(demo_axis, demo_plane);
} else {
draw_cube(0); // 0 means no suppression
}
scale = scale + (unsigned int)delta_scale;
if (scale >= PREFERRED_SCALE) {
if (countdown < 0L) {
//delta_scale = 0-delta_scale; // for zooming in and zooming out...
rotate_level = ((rotate_level + 1) % 3) - 10; // 0 1 2
delta_scale = 0;
rotation = (0==0);
countdown = 96L;
}
} else if (scale <= MIN_SCALE) {
delta_scale = 0-delta_scale;
}
countdown -= 1L;
if (countdown == 0L) {
// this is just a trivial animation until the game logic is added...
rotate_level += 10; delta_scale = 1; scale = MIN_SCALE; rotation = (0==0);
demo_plane = (demo_plane + 1) % 3;
if (demo_plane == 0) demo_axis = ((demo_axis-'X' + 1) % 3) + 'X'; // 3 for x y z
}
};
return 0;
}