/*
* Maze
* Copyright (C) 2000 Paul Davis, pdavis@lpccomp.bc.ca
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
*/
import java.awt.*;
import java.awt.event.*;
/**
* Display a 3D maze using a 2D projection as an AWT component.
* Line drawings are used to represent walls and corners.
* The orientation of the view is variable: the player can stand on the
* walls or ceiling.
* The player can click on the left or right side to pivot left or right,
* near the top or bottom to pivot backward or forward or near the
* center to move forward.
* The following keystrokes are also accepted:
* <DL>
* <LI> SPACE - move forward
* <LI> L - pivot left
* <LI> R - pivot right
* <LI> B - roll back
* <LI> F - roll forward
* <LI> G - roll right
* <LI> H - roll left
* <LI> S - spin around
* <LI> U - slide up
* <LI> D - slide down
* <LI> I - forward until a choice
* <LI> W - forward until a wall
* <LI> Z - follow until a choice
* <LI> ? - show 2D maze
* </DL>
*/
public class Maze3D extends Component {
/**
* The maze model.
* @see Maze
*/
private MazeModel model;
private Coordinate3D size,current;
private byte front,right,up;
private boolean showMarks;
private int myWidth,myHeight;
private Frame mapFrame;
/**
* Create a 3D maze display. Defaults to a size of 300x300.
* @param model The MazeModel to use for the maze contents.
*/
public Maze3D(MazeModel model) {
this(model,300,300);
}
/**
* Create a 3D maze display.
* @param model The MazeModel to use for the maze contents.
* @param width The display width of the Component.
* @param height The display width of the Component.
*/
public Maze3D(MazeModel model, int width, int height) {
this.model = model;
myWidth = width;
myHeight = height;
size = model.getSize();
current = new Coordinate3D(0,size.y-1,0);
model.setCurrent(current);
front = MazeModel.YMI;
right = MazeModel.XPL;
up = MazeModel.ZPL;
showMarks = false;
setSize(width,height);
addMouseListener(new MouseListener() {
public void mouseClicked(MouseEvent e) {
//System.out.println("Mouse at " +
// String.valueOf(e.getX()) +
// "," + String.valueOf(e.getY()));
handleClick(e.getX(),e.getY());
}
public void mouseEntered(MouseEvent e) {
}
public void mouseExited(MouseEvent e) {
}
public void mousePressed(MouseEvent e) {
}
public void mouseReleased(MouseEvent e) {
}
} );
addKeyListener(new KeyListener() {
public void keyPressed(KeyEvent e) {
//System.out.println("key pressed");
}
public void keyReleased(KeyEvent e) {
handleKeyCode(e.getKeyCode());
//System.out.println("key released");
}
public void keyTyped(KeyEvent e) {
//System.out.println("key typed");
}
} );
}
/**
* From Component, this component can get keyboard input.
*/
public boolean isFocusTraversable() {
return true;
}
/**
* Handle a mouse click.
* @param x The component x co-ordinate of the click.
* @param y The component y co-ordinate of the click.
*/
private void handleClick(int x,int y) {
Dimension s = getSize();
if ( x < s.width/3 ) { // Left
turnLeft();
repaint();
}
else if ( x > s.width*2/3 ) { // Right
turnRight();
repaint();
}
else if ( y < s.height/3 ) { // Up
if ( goUp() )
repaint();
}
else if ( y > s.height*2/3 ) { // Down
if ( goDown() )
repaint();
}
else { // Forward
if ( goForward() )
repaint();
}
//System.out.println("Now at " + String.valueOf(current.x) +
// "," + String.valueOf(current.y) +
// "," + String.valueOf(current.z) +
// " " + Maze.directionString(front));
}
/**
* Handle a keyboard key.
* @param keyCode The keycode from the keyboard.
*/
private void handleKeyCode(int keyCode) {
//System.out.println("key code " + String.valueOf(keyCode));
switch(keyCode) {
case KeyEvent.VK_SPACE: // SPACE - move forward
if( goForward() )
repaint();
break;
case KeyEvent.VK_L: // L - pivot left
turnLeft();
repaint();
break;
case KeyEvent.VK_R: // R - pivot right
turnRight();
repaint();
break;
case KeyEvent.VK_B: // B - roll back
rollBack();
repaint();
break;
case KeyEvent.VK_F: // F - roll forward
rollForward();
repaint();
break;
case KeyEvent.VK_G: // G - roll right
rollRight();
repaint();
break;
case KeyEvent.VK_H: // H - roll left
rollLeft();
repaint();
break;
case KeyEvent.VK_S: // S - spin around
front = Maze.invert(front);
right = Maze.invert(right);
repaint();
break;
case KeyEvent.VK_U: // U - slide up
if ( goUp() )
repaint();
break;
case KeyEvent.VK_D: // D - slide down
if ( goDown() )
repaint();
break;
case KeyEvent.VK_I: // I - forward until a choice
if ( ! goForward() )
break;
while ( !isOpen(right) &&
!isOpen(Maze.invert(right)) &&
!isOpen(up) &&
!isOpen(Maze.invert(up)) &&
isOpen(front) )
Maze.forward(current,front);
model.setCurrent(current);
repaint();
break;
case KeyEvent.VK_W: // W - forward until a wall
while ( isOpen(front) )
Maze.forward(current,front);
model.setCurrent(current);
repaint();
break;
case KeyEvent.VK_Z: // Z - follow until a choice
if ( ! goForward() )
break;
while ( Maze.count(model.grid(current)) == 2 ) {
if ( isOpen(front) )
Maze.forward(current,front);
else if ( isOpen(right) ) {
turnRight();
Maze.forward(current,front);
}
else if ( isOpen(Maze.invert(right)) ) {
turnLeft();
Maze.forward(current,front);
}
else if ( isOpen(up) ) {
rollBack();
Maze.forward(current,front);
}
else if ( isOpen(Maze.invert(up)) ) {
rollBack();
Maze.forward(current,front);
}
}
model.setCurrent(current);
repaint();
break;
case KeyEvent.VK_SLASH: // ? - show 2D maze
mapFrame = new Frame();
Dialog d = new Dialog(mapFrame,"Map");
int w = 600 / size.x;
if ( w > 20 )
w = 20;
int h = 450 / size.y;
if ( h > 20 )
h = 20;
if ( w>h )
d.add(new Maze2D(model,w,15,15));
else
d.add(new Maze2D(model,h,15,15));
d.addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent e) {
//System.out.println("close: " +
// e.getSource().toString());
Window win = (Window)e.getSource();
win.dispose();
}
});
d.pack();
d.show();
break;
}
}
/**
* From Component.getPreferredSize.
*/
public Dimension getPreferredSize() {
return new Dimension(myWidth,myHeight);
}
/*
public Dimension getSize() {
return new Dimension(myWidth,myHeight);
}
*/
/*
public void setPosition(Coordinate3D current) {
this.current = current;
}
*/
/**
* Set the orientation of the view.
* The parameters determine which direction is viewed and
* which "floor" or "wall" the player is standing on.
* @param front The dimension to the front of the player: XPL etc.
* @param right The dimension to the right of the player.
* @param up The dimension above the player.
*/
public void setOrientation(byte front, byte right, byte up) {
this.front = front;
this.right = right;
this.up = up;
}
/*
public Coordinate3D getCurrent() {
return current;
}
*/
/**
* Change the view orientation to pivot to the left.
*/
private void turnLeft() {
byte temp = front;
front = Maze.invert(right);
right = temp;
}
/**
* Change the view orientation to pivot to the right.
*/
private void turnRight() {
byte temp = front;
front = right;
right = Maze.invert(temp);
}
/**
* Change the view orientation to pivot backwards.
*/
private void rollBack() {
byte temp = front;
front = up;
up = Maze.invert(temp);
}
/**
* Change the view orientation to pivot frontwards.
*/
private void rollForward() {
byte temp = front;
front = Maze.invert(up);
up = temp;
}
/**
* Change the view orientation to roll onto your right side.
*/
private void rollRight() {
byte temp = up;
up = right;
right = Maze.invert(temp);
}
/**
* Change the view orientation to roll onto your left side.
*/
private void rollLeft() {
byte temp = up;
up = Maze.invert(right);
right = temp;
}
/**
* Move forward in the maze. Keep the same orientation.
* @return True if movement possible, false if blocked by a wall.
*/
private boolean goForward() {
if ( isOpen(model.grid(current),front) ) {
Maze.forward(current,front);
model.setCurrent(current);
return true;
}
return false;
}
/**
* Move backwards in maze. Keep the same orientation.
* @return True if movement possible, false if blocked by a wall.
*/
private boolean goBackward() {
if ( isOpen(model.grid(current),Maze.invert(front)) ) {
Maze.forward(current,Maze.invert(front));
model.setCurrent(current);
return true;
}
return false;
}
/**
* Move up a level in the maze. Keep the same orientation.
* @return True if movement possible, false if blocked by a wall.
*/
private boolean goUp() {
if ( isOpen(model.grid(current),up) ) {
Maze.forward(current,up);
model.setCurrent(current);
return true;
}
return false;
}
/**
* Move down a level in the maze. Keep the same orientation.
* @return True if movement possible, false if blocked by a wall.
*/
private boolean goDown() {
if ( isOpen(model.grid(current),Maze.invert(up)) ) {
Maze.forward(current,Maze.invert(up));
model.setCurrent(current);
return true;
}
return false;
}
/**
* Check if the given direction is open from the given cell value.
* @param value The cell value to use.
* @param direction The direction: XPL etc.
* @return True if there is no wall in that direction.
*/
private boolean isOpen(byte value,byte direction) {
return (value & direction) != 0;
}
/**
* Check if the given direction is open from the current cell.
* @param direction The direction: XPL etc.
* @return True if there is no wall in that direction.
*/
private boolean isOpen(byte direction) {
return isOpen(model.grid(current),direction);
}
/**
* From Component, paint the 2D projection of the maze based
* on the current position and view orientation.
*/
public void paint(Graphics g) {
Dimension s = getSize();
//System.out.println("Maze3D size: " + s.toString());
int x,y;
g.setColor(Color.white);
g.fillRect(0,0,s.width,s.height);
int depth=0;
Coordinate3D cr = new Coordinate3D(current.x,current.y,current.z);
g.setColor(Color.black);
while (true) {
drawLayer(g,cr,depth++);
if ( !isOpen(model.grid(cr),front) || depth>50 )
break;
Maze.forward(cr,front);
}
requestFocus();
}
/**
* Draw one layer of the projection.
* Each layer is one cell further away from the current view point.
* @param g The Graphics context to use.
* @param cr The position of the cell to draw.
* @param depth The number of cells away from the viewpoint.
*/
private void drawLayer(Graphics g, Coordinate3D cr, int depth) {
Dimension s = getSize();
byte value;
int x1,x2,y1,y2,x15,y15;
byte left,down;
int limx=s.width-1,limy=s.height-1;
double factor = 0.7;
double fdepth = (double)depth-factor;
double denom1=(fdepth+1.71)*2;
double denom2=(fdepth+2.71)*2;
/*
if ( depth>3 )
return;
*/
left = Maze.invert(right);
down = Maze.invert(up);
value = model.grid(cr);
x1 = (int)Math.floor(fdepth*s.width/denom1+0.5);
x2 = (int)Math.floor((fdepth+1)*s.width/denom2+0.5);
y1 = (int)Math.floor(fdepth*s.height/denom1+0.5);
y2 = (int)Math.floor((fdepth+1)*s.height/denom2+0.5);
x15 = (x1+x2)/2;
y15 = (y1+y2)/2;
//fprintf(fp,"depth: %d at %d,%d,%d : %d,%d %d,%d\n",
// depth,cr.x,cr.y,cr.z,y1,x1,y2,x2);
/*
xpos1 = ax[depth];
xpos2 = ax[depth+1];
ypos1 = ay[depth];
ypos2 = ay[depth+1];
len0 = 0;
len1 = len[depth];
len2 = len[depth+1];
len3 = (len1-len2)/2;
*/
if ( isOpen(value,up) ) {
g.drawLine(x1,y1,limx-x1,y1);
g.drawLine(x2,y1,x2,y2);
g.drawLine(limx-x2,y1,limx-x2,y2);
}
if ( isOpen(value,right) ) {
g.drawLine(limx-x1,y1,limx-x1,limy-y1);
g.drawLine(limx-x1,limy-y2,limx-x2,limy-y2);
g.drawLine(limx-x1,y2,limx-x2,y2);
}
if ( isOpen(value,down) ) {
g.drawLine(x1,limy-y1,limx-x1,limy-y1);
g.drawLine(x2,limy-y1,x2,limy-y2);
g.drawLine(limx-x2,limy-y1,limx-x2,limy-y2);
}
if ( isOpen(value,left) ) {
g.drawLine(x1,y1,x1,limy-y1);
g.drawLine(x1,limy-y2,x2,limy-y2);
g.drawLine(x1,y2,x2,y2);
}
if ( !( isOpen(value,up) ^ isOpen(value,front)) )
g.drawLine(x2,y2,limx-x2,y2);
if ( !( isOpen(value,right) ^ isOpen(value,front)) )
g.drawLine(limx-x2,y2,limx-x2,limy-y2);
if ( !( isOpen(value,down) ^ isOpen(value,front)) )
g.drawLine(x2,limy-y2,limx-x2,limy-y2);
if ( !( isOpen(value,left) ^ isOpen(value,front)) )
g.drawLine(x2,y2,x2,limy-y2);
if ( !( isOpen(value,up) ^ isOpen(value,left)))
g.drawLine(x1,y1,x2,y2);
if ( !( isOpen(value,up) ^ isOpen(value,right)))
g.drawLine(limx-x1,y1,limx-x2,y2);
if ( !( isOpen(value,right) ^ isOpen(value,down)))
g.drawLine(limx-x1,limy-y1,limx-x2,limy-y2);
if ( !( isOpen(value,down) ^ isOpen(value,left)))
g.drawLine(x1,limy-y1,x2,limy-y2);
/* Floor markers
if ( up==ZMI && !(value&up))
g.drawLine(limx/2,y15,limx/2,y15);
if ( right==ZMI && !(value&right))
g.drawLine(limx-x15,limy/2,limx-x15,limy/2);
if ( down==ZMI && !(value&down))
g.drawLine(limx/2,limy-y15,limx/2,limy-y15);
if ( left==ZMI && !(value&left))
g.drawLine(x15,limy/2,x15,limy/2);
if ( front==ZMI && !(value&front))
g.drawLine(limx/2,limy/2,limx/2,limy/2);
*/
Coordinate3D finish = model.getFinish();
if ( finish!=null && cr.x==finish.x && cr.y==finish.y && cr.z==finish.z )
chest(g,x1,y1,x2,y2);
if ( depth>0 && model.isMarked(cr) ) {
if ( up==MazeModel.ZMI )
block(g,limx/2,y15,limx/2,y15,x2-x1);
if ( right==MazeModel.ZMI )
block(g,limx-x15,limy/2,limx-x15,limy/2,x2-x1);
if ( down==MazeModel.ZMI )
block(g,limx/2,limy-y15,limx/2,limy-y15,x2-x1);
if ( left==MazeModel.ZMI )
block(g,x15,limy/2,x15,limy/2,x2-x1);
if ( front==MazeModel.ZMI )
block(g,limx/2,limy/2,limx/2,limy/2,x2-x1);
}
}
/**
* Draw the "treasure chest", a floating box at the maze finish.
* @param g The Graphics context.
*/
private void chest(Graphics g,int x1,int y1,int x2,int y2)
{
Dimension s = getSize();
int sy1=(s.height/2-y1)/5;
int sx1=(s.width/2-x1)/5;
int sy2=(s.height/2-y2)/5;
int sx2=(s.width/2-x2)/5;
int x=s.width/2;
int y=s.height/2;
g.drawLine(x-sx1,y-sy1,x+sx1,y-sy1);
g.drawLine(x+sx1,y-sy1,x+sx1,y+sy1);
g.drawLine(x+sx1,y+sy1,x-sx1,y+sy1);
g.drawLine(x-sx1,y+sy1,x-sx1,y-sy1);
g.drawLine(x-sx1,y-sy1,x-sx2,y-sy2);
g.drawLine(x+sx1,y-sy1,x+sx2,y-sy2);
g.drawLine(x+sx1,y+sy1,x+sx2,y+sy2);
g.drawLine(x-sx1,y+sy1,x-sx2,y+sy2);
g.drawLine(x-sx2,y-sy2,x+sx2,y-sy2);
g.drawLine(x+sx2,y-sy2,x+sx2,y+sy2);
g.drawLine(x+sx2,y+sy2,x-sx2,y+sy2);
g.drawLine(x-sx2,y+sy2,x-sx2,y-sy2);
}
/**
* Draw a marker on the floor for the cells that have been visited.
*/
private void block(Graphics g, int x1, int y1, int x2, int y2, int d) {
d /= 10;
if ( d < 1 )
d = 1;
int e = 0;
g.drawLine(x1-d+e,y1-d,x1-d,y1+d); // Left
g.drawLine(x1-d,y1+d,x1+d,y1+d); // Bottom
g.drawLine(x1+d,y1+d,x1+d-e,y1-d); // Right
g.drawLine(x1+d-e,y1-d,x1-d+e,y1-d); // Top
}
}