/*
* GPLv2 or 3, Copyright (c) 2010 Andrzej Zaborowski
*
* This implements the game logic.
*/
package wmsturbochallenge;
import static org.openstreetmap.josm.tools.I18n.tr;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Point;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;
import org.openstreetmap.josm.Main;
import org.openstreetmap.josm.data.ProjectionBounds;
import org.openstreetmap.josm.data.coor.EastNorth;
import org.openstreetmap.josm.data.gpx.GpxData;
import org.openstreetmap.josm.data.gpx.ImmutableGpxTrack;
import org.openstreetmap.josm.data.gpx.WayPoint;
import org.openstreetmap.josm.gui.layer.GpxLayer;
import org.openstreetmap.josm.gui.layer.Layer;
public class GameWindow extends JFrame implements ActionListener {
public GameWindow(Layer ground) {
setTitle(tr("The Ultimate WMS Super-speed Turbo Challenge II"));
setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
setUndecorated(true);
setSize(s.getScreenSize().width, s.getScreenSize().height);
setLocationRelativeTo(null);
setResizable(false);
while (s.getScreenSize().width < width * scale ||
s.getScreenSize().height < height * scale)
scale --;
add(panel);
setVisible(true);
/* TODO: "Intro" screen perhaps with "Hall of Fame" */
screen_image = new BufferedImage(width, height,
BufferedImage.TYPE_INT_RGB);
screen = screen_image.getGraphics();
this.ground = ground;
ground_view = new FakeMapView(Main.map.mapView, 0.0000001);
/* Retrieve start position */
EastNorth start = ground_view.parent.getCenter();
lat = start.north();
lon = start.east();
addKeyListener(new TAdapter());
timer = new Timer(80, this);
timer.start();
car_gps = new gps();
car_gps.start();
car_engine = new EngineSound();
car_engine.start();
for (int i = 0; i < maxsprites; i ++)
sprites[i] = new sprite_pos();
generate_sky();
}
protected EngineSound car_engine;
protected gps car_gps;
protected class gps extends Timer implements ActionListener {
public gps() {
super(1000, null);
addActionListener(this);
trackSegs = new ArrayList<>();
}
protected Collection<WayPoint> segment;
protected Collection<Collection<WayPoint>> trackSegs;
@Override
public void actionPerformed(ActionEvent e) {
/* We should count the satellites here, see if we
* have a fix and add any distortions. */
segment.add(new WayPoint(Main.getProjection().eastNorth2latlon(
new EastNorth(lon, lat))));
}
@Override
public void start() {
super.start();
/* Start recording */
segment = new ArrayList<>();
trackSegs.add(segment);
actionPerformed(null);
}
public void save_trace() {
int len = 0;
for (Collection<WayPoint> seg : trackSegs)
len += seg.size();
/* Don't save traces shorter than 5s */
if (len <= 5)
return;
GpxData data = new GpxData();
data.tracks.add(new ImmutableGpxTrack(trackSegs,
new HashMap<String, Object>()));
ground_view.parent.getLayerManager().addLayer(
new GpxLayer(data, "Car GPS trace"));
}
}
/* These are EastNorth, not actual LatLon */
protected double lat, lon;
/* Camera's altitude above surface (same units as lat/lon above) */
protected double ele = 0.000003;
/* Cut off at ~75px from bottom of the screen */
protected double horizon = 0.63;
/* Car's distance from the camera lens */
protected double cardist = ele * 3;
/* Pixels per pixel, the bigger the more oldschool :-) */
protected int scale = 5;
protected BufferedImage screen_image;
protected Graphics screen;
protected int width = 320;
protected int height = 200;
protected int centre = width / 2;
double maxdist = ele / (horizon - 0.6);
double realwidth = maxdist * width / height;
double pixelperlat = 1.0 * width / realwidth;
double sratio = 0.85;
protected int sw = (int) (2 * Math.PI * maxdist * pixelperlat * sratio);
/* TODO: figure out how to load these dynamically after splash
* screen is shown */
protected static final ImageIcon car[] = new ImageIcon[] {
new ImageIcon(Toolkit.getDefaultToolkit().createImage(
WMSRacer.class.getResource(
"/images/car0-l.png"))),
new ImageIcon(Toolkit.getDefaultToolkit().createImage(
WMSRacer.class.getResource(
"/images/car0.png"))),
new ImageIcon(Toolkit.getDefaultToolkit().createImage(
WMSRacer.class.getResource(
"/images/car0-r.png"))),
new ImageIcon(Toolkit.getDefaultToolkit().createImage(
WMSRacer.class.getResource(
"/images/car1-l.png"))),
new ImageIcon(Toolkit.getDefaultToolkit().createImage(
WMSRacer.class.getResource(
"/images/car1.png"))),
new ImageIcon(Toolkit.getDefaultToolkit().createImage(
WMSRacer.class.getResource(
"/images/car1-r.png"))),
};
protected static final ImageIcon bg[] = new ImageIcon[] {
new ImageIcon(Toolkit.getDefaultToolkit().createImage(
WMSRacer.class.getResource(
"/images/bg0.png"))),
};
protected static final ImageIcon skyline[] = new ImageIcon[] {
new ImageIcon(Toolkit.getDefaultToolkit().createImage(
WMSRacer.class.getResource(
"/images/horizon.png"))),
};
protected static final ImageIcon cactus[] = new ImageIcon[] {
new ImageIcon(Toolkit.getDefaultToolkit().createImage(
WMSRacer.class.getResource(
"/images/cactus0.png"))),
new ImageIcon(Toolkit.getDefaultToolkit().createImage(
WMSRacer.class.getResource(
"/images/cactus1.png"))),
new ImageIcon(Toolkit.getDefaultToolkit().createImage(
WMSRacer.class.getResource(
"/images/cactus2.png"))),
};
protected static final ImageIcon cloud[] = new ImageIcon[] {
new ImageIcon(Toolkit.getDefaultToolkit().createImage(
WMSRacer.class.getResource(
"/images/cloud0.png"))),
new ImageIcon(Toolkit.getDefaultToolkit().createImage(
WMSRacer.class.getResource(
"/images/cloud1.png"))),
new ImageIcon(Toolkit.getDefaultToolkit().createImage(
WMSRacer.class.getResource(
"/images/cloud2.png"))),
new ImageIcon(Toolkit.getDefaultToolkit().createImage(
WMSRacer.class.getResource(
"/images/cloud3.png"))),
new ImageIcon(Toolkit.getDefaultToolkit().createImage(
WMSRacer.class.getResource(
"/images/cloud4.png"))),
};
protected static final ImageIcon aircraft[] = new ImageIcon[] {
new ImageIcon(Toolkit.getDefaultToolkit().createImage(
WMSRacer.class.getResource(
"/images/aircraft0.png"))),
};
protected static final ImageIcon loading = new ImageIcon(
Toolkit.getDefaultToolkit().createImage(
WMSRacer.class.getResource(
"/images/loading.png")));
protected static Toolkit s = Toolkit.getDefaultToolkit();
protected int current_bg = 0;
protected int current_car = 0;
protected boolean cacti_on = true;
protected List<EastNorth> cacti = new ArrayList<>();
protected List<EastNorth> todelete = new ArrayList<>();
protected int splashframe = -1;
protected EastNorth splashcactus;
protected Layer ground;
protected double heading = 0.0;
protected double wheelangle = 0.0;
protected double speed = 0.0;
protected boolean key_down[] = new boolean[] {
false, false, false, false, };
protected void move() {
/* Left */
/* (At high speeds make more gentle turns) */
if (key_down[0])
wheelangle -= 0.1 / (1.0 + Math.abs(speed));
/* Right */
if (key_down[1])
wheelangle += 0.1 / (1.0 + Math.abs(speed));
if (wheelangle > 0.3)
wheelangle = 0.3; /* Radians */
if (wheelangle < -0.3)
wheelangle = -0.3;
wheelangle *= 0.7;
/* Up */
if (key_down[2])
speed += speed >= 0.0 ? 1.0 / (2.0 + speed) : 0.5;
/* Down */
if (key_down[3]) {
if (speed >= 0.5) /* Brake (TODO: sound) */
speed -= 0.5;
else if (speed >= 0.01) /* Brake (TODO: sound) */
speed = 0.0;
else /* Reverse */
speed -= 0.5 / (4.0 - speed);
}
speed *= 0.97;
car_engine.set_speed(speed);
if (speed > -0.1 && speed < 0.1)
speed = 0;
heading += wheelangle * speed;
boolean chop = false;
double newlat = lat + Math.cos(heading) * speed * ele * 0.2;
double newlon = lon + Math.sin(heading) * speed * ele * 0.2;
for (EastNorth pos : cacti) {
double alat = Math.abs(pos.north() - newlat);
double alon = Math.abs(pos.east() - newlon);
if (alat + alon < ele * 1.0) {
if (Math.abs(speed) < 2.0) {
if (speed > 0.0)
speed = -0.5;
else
speed = 0.3;
newlat = lat;
newlon = lon;
break;
}
chop = true;
splashframe = 0;
splashcactus = pos;
todelete.add(pos);
}
}
lat = newlat;
lon = newlon;
/* Seed a new cactus if we're moving.
* TODO: hook into data layers and avoid putting the cactus on
* the road!
*/
if (cacti_on && Math.random() * 30.0 < speed) {
double left_x = maxdist * (width - centre) / height;
double right_x = maxdist * (0 - centre) / height;
double x = left_x + Math.random() * (right_x - left_x);
double clat = lat + (maxdist - cardist) *
Math.cos(heading) - x * Math.sin(heading);
double clon = lon + (maxdist - cardist) *
Math.sin(heading) + x * Math.cos(heading);
cacti.add(new EastNorth(clon, clat));
chop = true;
}
/* Chop down any cactus far enough that it can't
* be seen. ``If a cactus falls in a forest and
* there is nobody around did it make a sound?''
*/
if (chop) {
for (EastNorth pos : cacti) {
double alat = Math.abs(pos.north() - lat);
double alon = Math.abs(pos.east() - lon);
if (alat + alon > 2 * maxdist)
todelete.add(pos);
}
cacti.removeAll(todelete);
todelete = new ArrayList<>();
}
}
int frame;
boolean downloading = false;
protected void screen_repaint() {
/* Draw background first */
sky_paint();
/* On top of it project the floor */
ground_paint();
/* Messages */
frame ++;
if ((frame & 8) == 0 && downloading)
screen.drawImage(loading.getImage(), centre -
loading.getIconWidth() / 2, 50, this);
/* Sprites */
sprites_paint();
}
static double max3(double x[]) {
return x[0] > x[1] ? x[2] > x[0] ? x[2] : x[0] :
(x[2] > x[1] ? x[2] : x[1]);
}
static double min3(double x[]) {
return x[0] < x[1] ? x[2] < x[0] ? x[2] : x[0] :
(x[2] < x[1] ? x[2] : x[1]);
}
protected void ground_paint() {
double sin = Math.sin(heading);
double cos = Math.cos(heading);
/* First calculate the bounding box for the visible area.
* The area will be (nearly) a triangle, so calculate the
* EastNorth for the three corners and make a bounding box.
*/
double left_x = maxdist * (width - centre) / height;
double right_x = maxdist * (0 - centre) / height;
double e_lat[] = new double[] {
lat + (maxdist - cardist) * cos - left_x * sin,
lat + (maxdist - cardist) * cos - right_x * sin,
lat - cardist * cos, };
double e_lon[] = new double[] {
lon + (maxdist - cardist) * sin + left_x * cos,
lon + (maxdist - cardist) * sin + right_x * cos,
lon - cardist * sin, };
ground_view.setProjectionBounds(new ProjectionBounds(
new EastNorth(min3(e_lon), min3(e_lat)),
new EastNorth(max3(e_lon), max3(e_lat))));
/* If the layer is a WMS layer, check if any tiles are
* missing */
/* FIXME: the code below is commented to fix compilation problems. Not sure if the code below is needed
if (ground instanceof WMSLayer) {
WMSLayer wms = (WMSLayer) ground;
downloading = wms.hasAutoDownload() && (
null == wms.findImage(new EastNorth(
e_lon[0], e_lat[0])) ||
null == wms.findImage(new EastNorth(
e_lon[0], e_lat[0])) ||
null == wms.findImage(new EastNorth(
e_lon[0], e_lat[0])));
}
*/
/* Request the image from ground layer */
ground.paint(ground_view.graphics, ground_view, null);
for (int y = (int) (height * horizon + 0.1); y < height; y ++) {
/* Assume a 60 deg vertical Field of View when
* calculating the distance at given pixel. */
double dist = ele / (1.0 * y / height - 0.6);
double lat_off = lat + (dist - cardist) * cos;
double lon_off = lon + (dist - cardist) * sin;
for (int x = 0; x < width; x ++) {
double p_x = dist * (x - centre) / height;
EastNorth en = new EastNorth(
lon_off + p_x * cos,
lat_off - p_x * sin);
Point pt = ground_view.getPoint(en);
int rgb = ground_view.ground_image.getRGB(
pt.x, pt.y);
screen_image.setRGB(x, y, rgb);
}
}
}
protected BufferedImage sky_image;
protected Graphics sky;
public void generate_sky() {
sky_image = new BufferedImage(sw, 70,
BufferedImage.TYPE_INT_ARGB);
sky = sky_image.getGraphics();
int n = (int) (Math.random() * sw * 0.03);
for (int i = 0; i < n; i ++) {
int t = (int) (Math.random() * 5.0);
int x = (int) (Math.random() *
(sw - cloud[t].getIconWidth()));
int y = (int) ((1 - Math.random() * Math.random()) *
(70 - cloud[t].getIconHeight()));
sky.drawImage(cloud[t].getImage(), x, y, this);
}
if (Math.random() < 0.5) {
int t = 0;
int x = (int) (300 + Math.random() * (sw - 500 -
aircraft[t].getIconWidth()));
sky.drawImage(aircraft[t].getImage(), x, 0, this);
}
}
public void sky_paint() {
/* for x -> 0, lim sin(x) / x = 1 */
int hx = (int) (-heading * maxdist * pixelperlat);
int hw = skyline[current_bg].getIconWidth();
hx = ((hx % hw) - hw) % hw;
int sx = (int) (-heading * maxdist * pixelperlat * sratio);
sx = ((sx % sw) - sw) % sw;
screen.drawImage(bg[current_bg].getImage(), 0, 0, this);
screen.drawImage(sky_image, sx, 50, this);
if (sw + sx < width)
screen.drawImage(sky_image, sx + sw, 50, this);
screen.drawImage(skyline[current_bg].getImage(), hx, 66, this);
if (hw + hx < width)
screen.drawImage(skyline[current_bg].getImage(),
hx + hw, 66, this);
}
protected static class sprite_pos implements Comparable<sprite_pos> {
double dist;
int x, y, sx, sy;
Image sprite;
public sprite_pos() {
}
@Override
public int compareTo(sprite_pos x) {
return (int) ((x.dist - this.dist) * 1000000.0);
}
}
/* sizes decides how many zoom levels the sprites have. We
* could do just normal scalling according to distance but
* that's not what old games did, they had prescaled sprites
* for the different distances and you could see the feature
* grow discretely as you approached it. */
protected final static int sizes = 8;
protected final static int maxsprites = 32;
protected sprite_pos sprites[] = new sprite_pos[maxsprites];
protected void sprites_paint() {
/* The vehicle */
int orientation = (wheelangle > -0.02 ? wheelangle < 0.02 ?
1 : 2 : 0) + current_car * 3;
sprites[0].sprite = car[orientation].getImage();
sprites[0].dist = cardist;
sprites[0].sx = car[orientation].getIconWidth();
sprites[0].x = centre - sprites[0].sx / 2;
sprites[0].sy = car[orientation].getIconHeight();
sprites[0].y = height - sprites[0].sy - 10; /* TODO */
/* The cacti */
double sin = Math.sin(-heading);
double cos = Math.cos(-heading);
int i = 1;
for (EastNorth ll : cacti) {
double clat = ll.north() - lat;
double clon = ll.east() - lon;
double dist = (clat * cos - clon * sin) + cardist;
double p_x = clat * sin + clon * cos;
if (dist * 8 <= cardist || dist > maxdist)
continue;
int x = (int) (p_x * height / dist + centre);
int y = (int) ((ele / dist + 0.6) * height);
if (i >= maxsprites)
break;
if (x < -10 || x > width + 10)
continue;
int type = (((int) (ll.north() * 10000000.0) & 31) % 3);
int sx = cactus[type].getIconWidth();
int sy = cactus[type].getIconHeight();
sprite_pos pos = sprites[i ++];
pos.dist = dist;
pos.sprite = cactus[type].getImage();
pos.sx = (int) (sx * cardist * 0.7 / dist);
pos.sy = (int) (sy * cardist * 0.7 / dist);
pos.x = x - pos.sx / 2;
pos.y = y - pos.sy;
}
Arrays.sort(sprites, 0, i);
for (sprite_pos sprite : sprites)
if (i --> 0)
screen.drawImage(sprite.sprite,
sprite.x, sprite.y,
sprite.sx, sprite.sy, this);
else
break;
if (splashframe >= 0) {
splashframe ++;
if (splashframe >= 8)
splashframe = -1;
int type = (((int) (splashcactus.north() *
10000000.0) & 31) % 3);
int sx = cactus[type].getIconWidth();
int sy = cactus[type].getIconHeight();
Image image = cactus[type].getImage();
for (i = 0; i < 50; i ++) {
int x = (int) (Math.random() * sx);
int y = (int) (Math.random() * sy);
int w = (int) (Math.random() * 20);
int h = (int) (Math.random() * 20);
int nx = centre + splashframe * (x - sx / 2);
int ny = height - splashframe * (sy - y);
int nw = w + splashframe;
int nh = h + splashframe;
screen.drawImage(image,
nx, ny, nx + nw, ny + nh,
x, y, x + w, y + h, this);
}
}
}
public boolean no_super_repaint = false;
protected class GamePanel extends JPanel {
public GamePanel() {
setBackground(Color.BLACK);
setDoubleBuffered(true);
}
@Override
public void paint(Graphics g) {
int w = (int) getSize().getWidth();
int h = (int) getSize().getHeight();
if (no_super_repaint)
no_super_repaint = false;
else
super.paint(g);
g.drawImage(screen_image, (w - width * scale) / 2,
(h - height * scale) / 2,
width * scale, height * scale, this);
Toolkit.getDefaultToolkit().sync();
}
}
JPanel panel = new GamePanel();
protected void quit() {
timer.stop();
car_engine.stop();
car_gps.stop();
car_gps.save_trace();
setVisible(false);
panel = null;
screen_image = null;
screen = null;
dispose();
}
/*
* Supposedly a thread drawing frames and sleeping in a loop is
* better than for animating than swing Timers. For the moment
* I'll use a timer because I don't want to deal with all the
* potential threading issues.
*/
protected Timer timer;
@Override
public void actionPerformed(ActionEvent e) {
move();
screen_repaint();
no_super_repaint = true;
panel.repaint();
}
protected class TAdapter extends KeyAdapter {
@Override
public void keyPressed(KeyEvent e) {
int key = e.getKeyCode();
if (key == KeyEvent.VK_LEFT && !key_down[0]) {
wheelangle -= 0.02;
key_down[0] = true;
}
if (key == KeyEvent.VK_RIGHT && !key_down[1]) {
wheelangle += 0.02;
key_down[1] = true;
}
if (key == KeyEvent.VK_UP)
key_down[2] = true;
if (key == KeyEvent.VK_DOWN)
key_down[3] = true;
if (key == KeyEvent.VK_ESCAPE)
quit();
/* Toggle sound */
if (key == KeyEvent.VK_S) {
if (car_engine.is_on())
car_engine.stop();
else
car_engine.start();
}
/* Toggle cacti */
if (key == KeyEvent.VK_C) {
cacti_on = !cacti_on;
if (!cacti_on)
cacti = new ArrayList<>();
}
/* Switch vehicle */
if (key == KeyEvent.VK_V)
if (current_car ++>= 1)
current_car = 0;
}
@Override
public void keyReleased(KeyEvent e) {
int key = e.getKeyCode();
if (key == KeyEvent.VK_LEFT)
key_down[0] = false;
if (key == KeyEvent.VK_RIGHT)
key_down[1] = false;
if (key == KeyEvent.VK_UP)
key_down[2] = false;
if (key == KeyEvent.VK_DOWN)
key_down[3] = false;
}
}
protected FakeMapView ground_view;
}