import ddf.minim.*;
/**
* Game 12: "One to Tango"
* P to pause, M to mute
* C for ultra-fast-forward
*
Go play more games at NMcCoy.net!
* 
This work by Nathan McCoy is licensed under a Creative Commons Attribution-Noncommercial-Share Alike 3.0 United States License.
*/
boolean[] keys = new boolean[256];
boolean title;
boolean paused;
boolean muted;
int drawing_ms_last;
int physics_ms_last;
int CORE_FPS = 540;
int DRAWING_FPS = 60;
int PHYSICS_FPS = 180;
int DRAW_MS = 1000 / DRAWING_FPS;
int PHYS_MS = 1000 / PHYSICS_FPS;
Minim minim;
PFont font;
AudioSample dieSound;
AudioSample goldSound;
AudioSample jumpSound;
AudioSample pastdieSound;
AudioSample pastgoldSound;
AudioSample pastjumpSound;
AudioPlayer bgm;
// level setup consts
int EMPTY = 0;
int WALL = 1;
int HAZARD = 2;
int GOLD = 3;
int RED_SWITCH = 4;
int RED_DOOR = 5;
int GREEN_SWITCH = 6;
int GREEN_DOOR = 7;
int BLUE_SWITCH = 8;
int BLUE_DOOR = 9;
int SPAWN = 10;
int WHITE_SWITCH = 11;
int WHITE_DOOR = 12;
int ERROR = 99;
int GOTTEN_GOLD = 88;
char[] TILES = {' ', '#', '%', '*', 'r', 'R', 'g', 'G', 'b', 'B', '@', 'w', 'W'};
int LEVEL_H = 30;
int LEVEL_W = 60;
int TILE_W = 720 / LEVEL_W;
float GRAVITY = 0.04;
float X_SPEED = 0.9;
float JUMP_IMPULSE = 2.0;
int TIME_LIMIT = 5000;
PVector player_pos;
float player_yvel;
PVector player_spawn;
int time_counter;
int color_cycle_frames;
int level_number;
int gold_remaining;
int[][] level_map;
String level_name;
ArrayList current_path;
ArrayList past_path;
ArrayList jump_frames;
ArrayList past_jump_frames;
ParticleList particles;
boolean stood;
boolean ground;
boolean facing_right = true;
boolean past_facing_right = true;
boolean buffered_jump = false;
boolean redswitch = false;
boolean greenswitch = false;
boolean blueswitch = false;
boolean whiteswitch = false;
class SquareSpark extends Particle
{
color col;
SquareSpark(PVector pos, color c)
{
super(pos, new PVector(random(4)-2, random(4)-2), 60);
col = c;
}
void draw()
{
fill(col, 255.0 * life / maxlife);
noStroke();
rect(pos.x, pos.y, TILE_W, TILE_W);
}
}
void loadLevel(int level)
{
gold_remaining = 0;
current_path = new ArrayList();
past_path = new ArrayList();
past_jump_frames = new ArrayList();
jump_frames = new ArrayList();
player_spawn = new PVector(TILE_W, TILE_W);
reader = createReader("" + level + ".lvl");
if(reader == null) reader = createReader("end.lvl");
String row;
try {
row = reader.readLine();
}
catch (IOException e) {
e.printStackTrace();
row = null;
}
level_name = row;
level_map = new int[LEVEL_W][LEVEL_H];
for(int y = 0; y < LEVEL_H; y++)
{
try {
row = reader.readLine();
}
catch (IOException e) {
e.printStackTrace();
row = null;
}
if (row == null) {
// Stop reading because of an error or file is empty
break;
}
else {
for(int x = 0; x < LEVEL_W; x++)
{
char c = row.charAt(x);
int tile = 0;
for(int i = 0; i < TILES.length; i++)
{
if(c == TILES[i]) tile = i;
}
level_map[x][y] = tile;
if(tile == SPAWN) player_spawn = new PVector(x * TILE_W, y * TILE_W);
if(tile == GOLD) gold_remaining++;
}
}
}
startLife();
}
void startLife()
{
past_jump_frames = jump_frames;
jump_frames = new ArrayList();
time_counter = 0;
player_pos = new PVector(player_spawn.x, player_spawn.y);
player_yvel = 0;
past_path = current_path;
current_path = new ArrayList();
stood = false;
ground = false;
facing_right = true;
past_facing_right = true;
for(int x = 0; x < LEVEL_W; x++) for(int y = 0; y < LEVEL_H; y++)
if(level_map[x][y] == GOTTEN_GOLD) {level_map[x][y] = GOLD; gold_remaining++;}
}
BufferedReader reader;
void setup()
{
size(720, 480);
frameRate(600);
minim = new Minim(this);
dieSound = minim.loadSample("die.wav", 1024);
dieSound.setGain(-5);
goldSound = minim.loadSample("gold.wav", 1024);
goldSound.setGain(-5);
jumpSound = minim.loadSample("jump.wav", 1024);
jumpSound.setGain(-10);
pastdieSound = minim.loadSample("pastdie.wav", 1024);
pastdieSound.setGain(-5);
pastgoldSound = minim.loadSample("pastgold.wav", 1024);
pastgoldSound.setGain(-5);
pastjumpSound = minim.loadSample("pastjump.wav", 1024);
pastjumpSound.setGain(-7);
bgm = minim.loadFile("timehat.mp3", 2048);
bgm.setGain(-7);
bgm.loop();
level_number = 1;
loadLevel(level_number);
startLife();
particles = new ParticleList();
font = loadFont("PressStartK-32.vlw");
}
void stop()
{
dieSound.close();
goldSound.close();
jumpSound.close();
pastdieSound.close();
pastgoldSound.close();
pastjumpSound.close();
bgm.close();
minim.stop();
super.stop();
}
void die()
{
if(time_counter > 0)
{
if(!muted) dieSound.trigger();
for(int i = 0; i < 10; i++) particles.add(new SquareSpark(player_pos, color(200, 128, 255)));
startLife();
}
}
void touch(int x, int y)
{
int tile = level_map[x][y];
if(tile == HAZARD) die();
if(tile == GOLD)
{
gold_remaining--;
if(!muted) goldSound.trigger();
for(int i = 0; i < 10; i++) particles.add(new SquareSpark(new PVector(x * TILE_W, y * TILE_W), color(255, 255, 96)));
level_map[x][y] = GOTTEN_GOLD;
}
if(tile == RED_SWITCH) redswitch = true;
if(tile == GREEN_SWITCH) greenswitch = true;
if(tile == BLUE_SWITCH) blueswitch = true;
if(tile == WHITE_SWITCH) whiteswitch = true;
}
void pastTouch(int x, int y)
{
int tile = level_map[x][y];
if(tile == GOLD)
{
gold_remaining--;
if(!muted) pastgoldSound.trigger();
for(int i = 0; i < 10; i++) particles.add(new SquareSpark(new PVector(x * TILE_W, y * TILE_W), color(255, 255, 96)));
level_map[x][y] = GOTTEN_GOLD;
}
if(tile == RED_SWITCH) redswitch = true;
if(tile == GREEN_SWITCH) greenswitch = true;
if(tile == BLUE_SWITCH) blueswitch = true;
if(tile == WHITE_SWITCH) whiteswitch = true;
}
boolean isSolid(int tile)
{
if(tile == WALL) return true;
if(tile == RED_DOOR && !redswitch) return true;
if(tile == GREEN_DOOR && !greenswitch) return true;
if(tile == BLUE_DOOR && !blueswitch) return true;
if(tile == WHITE_DOOR && whiteswitch) return true;
return false;
}
void physicsStep()
{
particles.tick();
if(time_counter > TIME_LIMIT) die();
if(keys[LEFT]) player_pos.x -= X_SPEED;
if(keys[RIGHT]) player_pos.x += X_SPEED;
if(keys['Z'] && ground && buffered_jump) {
player_yvel = -JUMP_IMPULSE;
buffered_jump = false;
if(!muted) jumpSound.trigger();
jump_frames.add(new Integer(time_counter));
}
player_yvel+= GRAVITY;
player_pos.y += player_yvel;
ground = false;
redswitch = false;
greenswitch = false;
blueswitch = false;
whiteswitch = false;
int tx = (int) (player_pos.x / TILE_W);
int ty = (int) (player_pos.y / TILE_W);
if(time_counter < past_path.size() && time_counter > 0)
{
PVector pastpos = (PVector)past_path.get(time_counter);
if(time_counter == past_path.size() - 1)
{
for(int i = 0; i < 10; i++) particles.add(new SquareSpark(pastpos, color(200, 140, 100)));
if(!muted) pastdieSound.trigger();
}
int ptx = (int) (pastpos.x / TILE_W);
int pty = (int) (pastpos.y / TILE_W);
pastTouch(ptx, pty);
pastTouch(ptx+1, pty);
pastTouch(ptx, pty+1);
pastTouch(ptx+1, pty+1);
if(stood)
{
PVector lastpastpos = (PVector)past_path.get(time_counter - 1);
player_pos.add(PVector.sub(pastpos, lastpastpos));
}
if(player_pos.x - pastpos.x < TILE_W && player_pos.x - pastpos.x > -TILE_W && player_pos.y - pastpos.y < TILE_W && player_pos.y - pastpos.y > -TILE_W)
{
//past collision
if(player_pos.y - pastpos.y < -TILE_W / 2)
{
player_pos.y = pastpos.y - TILE_W;
stood = true;
ground = true;
player_yvel = min(player_yvel, 0);
}
else if(player_pos.x < pastpos.x) player_pos.x = pastpos.x - TILE_W;
else
{
if(isSolid(level_map[tx+1][ty]) && player_pos.x - pastpos.x < 1) //prevent crushing
player_pos.x = pastpos.x - TILE_W;
else player_pos.x = pastpos.x + TILE_W;
}
}
else stood = false;
}
tx = (int) (player_pos.x / TILE_W);
ty = (int) (player_pos.y / TILE_W);
touch(tx, ty);
touch(tx+1, ty);
touch(tx, ty+1);
touch(tx+1, ty+1);
float x_fraction = player_pos.x / TILE_W - tx;
float y_fraction = player_pos.y / TILE_W - ty;
boolean hitNW = false;
boolean hitNE = false;
boolean hitSW = false;
boolean hitSE = false;
if(isSolid(level_map[tx][ty])) hitNW = true;
if(isSolid(level_map[tx + 1][ty])) hitNE = true;
if(isSolid(level_map[tx][ty + 1])) hitSW = true;
if(isSolid(level_map[tx + 1][ty + 1])) hitSE = true;
boolean resolved = false;
if(hitNW && hitSW && hitNE && hitSE) {die(); resolved = true;}
if(!resolved)
{
if(hitSW && hitSE)/*eject N*/{ground = true; player_pos.y = ty * TILE_W; player_yvel = min(player_yvel, 0); resolved = true;}
if(hitNW && hitNE)/*eject S*/{player_pos.y = (ty+1) * TILE_W; player_yvel = max(player_yvel, 0); resolved = true;}
if(hitNW && hitSW)/*eject E*/{player_pos.x = (tx+1) * TILE_W; resolved = true;}
if(hitSE && hitNE)/*eject W*/{player_pos.x = tx * TILE_W; resolved = true;}
}
if(!resolved)
{
if(hitNW)
if(x_fraction > y_fraction) {player_pos.x = (tx+1) * TILE_W;}
else {player_pos.y = (ty+1) * TILE_W; player_yvel = max(player_yvel, 0);}
if(hitSW)
if(x_fraction > 1 - y_fraction) {player_pos.x = (tx+1) * TILE_W;}
else {ground = true; player_pos.y = ty * TILE_W; player_yvel = min(player_yvel, 0);}
if(hitNE)
if(1 - x_fraction > y_fraction) {player_pos.x = (tx) * TILE_W;}
else {player_pos.y = (ty+1) * TILE_W; player_yvel = max(player_yvel, 0);}
if(hitSE)
if(1 - x_fraction > 1 - y_fraction) {player_pos.x = (tx) * TILE_W;}
else {ground = true; player_pos.y = ty * TILE_W; player_yvel = min(player_yvel, 0);}
}
current_path.add(new PVector(player_pos.x, player_pos.y));
if(past_jump_frames.size() > 0)
{
Integer pj = (Integer) past_jump_frames.get(0);
if(pj.intValue() < time_counter) past_jump_frames.remove(0);
if(pj.intValue() == time_counter) if(!muted) pastjumpSound.trigger();
}
time_counter ++;
if(gold_remaining <= 0) loadLevel(++level_number);
}
void drawingStep()
{
background(0);
if(!paused)color_cycle_frames++;
int tile_w = TILE_W;
for(int x = 0; x < LEVEL_W; x++) for(int y = 0; y < LEVEL_H; y++)
{
if(level_map[x][y] == EMPTY || level_map[x][y] == SPAWN || level_map[x][y] == GOTTEN_GOLD) fill(96, 128, 192);
else if(level_map[x][y] == WALL) fill(180, 170, 160);
else if(level_map[x][y] == HAZARD) fill(255, 32*sin((color_cycle_frames + x + y) * 0.2)+128, 64);
else if(level_map[x][y] == GOLD) fill(255, 255, 128*sin((color_cycle_frames + x + y) * 0.3)+128);
//else if(level_map[x][y] == GOTTEN_GOLD) fill(180+16, 170+16, 160+8*sin((color_cycle_frames + x + y) * 0.3)+8);
else if(level_map[x][y] == RED_DOOR) if(!redswitch) fill(190, 140, 140); else fill(116, 128, 192);
else if(level_map[x][y] == GREEN_DOOR) if(!greenswitch) fill(140, 190, 140); else fill(96, 148, 192);
else if(level_map[x][y] == BLUE_DOOR) if(!blueswitch) fill(150, 150, 200); else fill(96, 128, 212);
else if(level_map[x][y] == RED_SWITCH) fill(255, 32*sin((color_cycle_frames + x + y) * 0.3)+128, 32*sin((color_cycle_frames + x + y) * 0.3)+128);
else if(level_map[x][y] == GREEN_SWITCH) fill(32*sin((color_cycle_frames + x + y) * 0.3)+128, 255, 32*sin((color_cycle_frames + x + y) * 0.3)+128);
else if(level_map[x][y] == BLUE_SWITCH) fill(32*sin((color_cycle_frames + x + y) * 0.3)+128, 32*sin((color_cycle_frames + x + y) * 0.3)+128, 255);
else if(level_map[x][y] == WHITE_SWITCH) fill(128*sin((color_cycle_frames + x + y) * 0.3)+255, -128*sin((color_cycle_frames + x + y) * 0.3)+255, 255);
else if(level_map[x][y] == WHITE_DOOR) if(whiteswitch) {
fill(192+64*sin((color_cycle_frames -x - y) * 0.1), 192+64*sin((color_cycle_frames - x - y) * 0.1 + TWO_PI/3), 192+64*sin((color_cycle_frames - x - y) * 0.1 + TWO_PI*2.0/3));
} else fill(96, 128, 192);
else fill(255, 0, 255);
noStroke();
rect(x * tile_w, y * tile_w, tile_w, tile_w);
}
if(time_counter > 1)
{
PVector lastp = (PVector)current_path.get(time_counter - 2);
if(lastp.x > player_pos.x) facing_right = false;
if(lastp.x < player_pos.x) facing_right = true;
if(time_counter < past_path.size())
{
PVector lastpastp = (PVector)past_path.get(time_counter - 1);
PVector pastp = (PVector)past_path.get(time_counter);
if(lastpastp.x > pastp.x) past_facing_right = false;
if(lastpastp.x < pastp.x) past_facing_right = true;
}
}
if(time_counter < past_path.size()) ///draw past life
{
if(time_counter < past_path.size() - 100 || (color_cycle_frames) % 2 == 0)
{
PVector pastp = (PVector)past_path.get(time_counter);
fill(200, 140, 100);
rect(pastp.x, pastp.y, TILE_W, TILE_W);
fill(96, 80, 60);
rect(pastp.x, pastp.y, TILE_W, TILE_W / 3);
rect(pastp.x - TILE_W / 4, pastp.y + TILE_W / 3, TILE_W * 1.5, 1);
if(past_facing_right) rect(pastp.x + TILE_W * 0.75 - 1, pastp.y + TILE_W / 3, 1, TILE_W / 3);
else rect(pastp.x + TILE_W * 0.25, pastp.y + TILE_W / 3, 1, TILE_W / 3);
}
}
fill(200, 128, 255);
rect(player_pos.x, player_pos.y, TILE_W, TILE_W);
fill(64, 64, 64);
rect(player_pos.x, player_pos.y, TILE_W, TILE_W / 3);
rect(player_pos.x - TILE_W / 4, player_pos.y + TILE_W / 3, TILE_W * 1.5, 1);
if(facing_right) rect(player_pos.x + TILE_W * 0.75 - 1, player_pos.y + TILE_W / 3, 1, TILE_W / 3);
else rect(player_pos.x + TILE_W * 0.25, player_pos.y + TILE_W / 3, 1, TILE_W / 3);
textFont(font, 16);
textAlign(LEFT, TOP);
int HUD_LEFT = 8;
int HUD_TOP = LEVEL_H * TILE_W + 8;
fill(255);
text(level_name, HUD_LEFT, HUD_TOP);
text("Time: Gold Remaining: "+gold_remaining, HUD_LEFT, HUD_TOP + 20);
ellipseMode(CORNER);
float time_ratio = 1 - 1.0*time_counter / TIME_LIMIT;
arc(HUD_LEFT + 90, HUD_TOP + 20, 16, 16, -HALF_PI - time_ratio * TWO_PI, -HALF_PI);
particles.draw();
}
void draw()
{
while(physics_ms_last + PHYS_MS < millis())
{
if(!paused)
{
physicsStep();
}
physics_ms_last += PHYS_MS;
}
if(drawing_ms_last + DRAW_MS < millis())
{
drawingStep();
drawing_ms_last = millis();
}
}
void keyPressed()
{
if(!keys[keyCode])
{
keys[keyCode] = true;
down(keyCode);
}
}
void keyReleased()
{
if(keys[keyCode])
{
keys[keyCode] = false;
up(keyCode);
}
}
void down(int theKey)
{
if(theKey == 'M') {
muted = !muted;
if(muted)bgm.mute();
else bgm.unmute();
}
if(theKey == 'P') paused = !paused;
if(!paused)
{
if(theKey == 'X') die();
if(theKey == 'Z') buffered_jump = true;
if(theKey == 'C') {
ArrayList life_check = current_path;
while(life_check == current_path) physicsStep();
}//while(++time_counter < TIME_LIMIT) current_path.add(new PVector(player_pos.x, player_pos.y));
if(theKey == '=') loadLevel(++level_number);
}
}
void up(int theKey)
{
println(theKey + " up");
}