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!
* Creative Commons License
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"); }