import ddf.minim.*; //////////// CONSTS static final int DRAW_FPS = 60; static final int PHYS_FPS = 180; static final int FRATE = 800; static final float MARCH_BPM = 35.793; static final int MARCH_FRAMES = (int) (PHYS_FPS * 60.0 / MARCH_BPM); static final int DRAW_MILLS = 1000/DRAW_FPS; static final int PHYS_MILLS = 1000/PHYS_FPS; static final int EXPLOSION_DURATION = 180; static final int EXPLOSION_SCALE = 30; static final int EXPLOSION_BONUS = 20; static final int INVADER_BLOCK_WIDTH = 11; static final int INVADER_BLOCK_HEIGHT = 5; static final int INVADER_X_SPACING = 48; static final int INVADER_Y_SPACING = 48; static final int INVADER_RADIUS = 16; static final int MARCH_DISTANCE = 16; static final int MARCH_RIGHT = 0; static final int MARCH_DOWN_LEFT = 1; static final int MARCH_LEFT = 2; static final int MARCH_DOWN_RIGHT = 3; static final int SHOT_DELAY_BASE = 180 * 18; static final int START_DIFFICULTY = 6; static final int DOOMSDAY_DIFFICULTY = 12; static final float STEER_FACTOR = 0.02; static final int NUM_CITIES = 6; static final float CITY_SPACING = 100; static final float CITY_RADIUS = 20; static final int ANNOUNCE_TIME = 3 * PHYS_FPS; static final int CITY_BONUS = 1000; static final int FLAWLESS_BONUS = 3000; static final int TIME_BONUS_SECS = 100; static final int TIME_BONUS = 200; static final int EXTRA_CITY_AT = 50000; /////////// GLOBALS static PVector[][] patterns; PFont font; int drawMark; int drawingFrame; int physicsFrame; int physMark; ArrayList missiles; ArrayList explosions; ArrayList invaders; boolean[] cities; int playerX; int playerY = 520; int playerWidth = 120; int playerHeight = 16; int marchDir; int difficulty; int shotTimer; int waveTimer; String announceString; String bonusString = ""; int score; boolean flawlessDefense; boolean doomsday = false; int cityPoints; boolean paused; boolean gameOver; boolean atTitle; boolean firstLevel; /////////// AUDIO Minim minim; AudioPlayer bgm; AudioSample paddleSound; AudioSample wallSound; AudioSample ceilSound; AudioSample hitSound; AudioSample exploSound; AudioSample popSound; void setSound() { minim = new Minim(this); paddleSound = minim.loadSample("bounce1.wav", 512); wallSound = minim.loadSample("bounce2.wav", 512); ceilSound = minim.loadSample("bounce3.wav", 512); hitSound = minim.loadSample("bounce3.wav", 512); exploSound = minim.loadSample("explo1.wav", 512); popSound = minim.loadSample("explo4.wav", 512); bgm = minim.loadFile("spaceout.mp3", 4096); } void closeSound() { bgm.close(); paddleSound.close(); wallSound.close(); ceilSound.close(); hitSound.close(); exploSound.close(); popSound.close(); minim.stop(); } /////////// UTIL boolean inRadius(PVector a, PVector b, float rad) { return rad*rad > (a.x - b.x)*(a.x - b.x) + (a.y - b.y) * (a.y - b.y); } boolean inRadius(float ax, float ay, float bx, float by, float rad) { return rad*rad > (ax - bx)*(ax - bx) + (ay - by) * (ay - by); } float cityPos(int c) { float y = height; float x = width/2 - ((NUM_CITIES-1)/2.0)*CITY_SPACING + c*CITY_SPACING; return x;//new PVector(x, y); } /////////// CLASSES class Missile { boolean friendly; PVector origin; PVector pos; PVector vel; Missile(PVector from, PVector velocity, boolean friend) { friendly = friend; origin = new PVector(from.x, from.y); pos = new PVector(from.x, from.y); vel = new PVector(velocity.x, velocity.y); } void move() { pos.add(vel); if(pos.x < 0 || pos.x > width) { if(pos.x < 0) pos.x=0; if(pos.x > width) pos.x=width; origin = new PVector(pos.x, pos.y); vel.x = - vel.x; wallSound.trigger(); } if(pos.y < 0) { origin = new PVector(pos.x, pos.y); vel.y = - vel.y; ceilSound.trigger(); } if(vel.y > 0 && pos.y > playerY && pos.y < playerY + playerHeight && pos.x > playerX - playerWidth/2 && pos.x < playerX + playerWidth/2) { vel.y = - vel.y; friendly = true; origin = new PVector(pos.x, pos.y); vel.x = (pos.x - playerX)*STEER_FACTOR; paddleSound.trigger(); } } void draw() { strokeWeight(2); if(friendly) stroke(0, 0, 255); else stroke(255, 0, 0); line(origin.x, origin.y, pos.x, pos.y); fill(255); noStroke(); ellipse(pos.x, pos.y, 5, 5); } } class Explosion { PVector pos; int level; int life; int maxlife; Explosion(PVector at, int lev) { level = lev; pos = new PVector(at.x, at.y); maxlife = life = EXPLOSION_DURATION * (level + 1); } float radius() { int magn = (EXPLOSION_SCALE + EXPLOSION_BONUS * level) * 2; float lifeLeft = ((float)life) / maxlife; return min(magn * lifeLeft, magn * (1 - lifeLeft)); } void draw() { noStroke(); fill(blastColor()); ellipse(pos.x, pos.y, radius()*2, radius()*2); } } class Invader { final int row; final int column; PVector pos; final int strength; PVector[] pattern; Invader(int c, int r, int s) { strength = min(max(s, 0), 6); row = r; column = c; pos = new PVector(column * INVADER_X_SPACING + INVADER_RADIUS, row * INVADER_Y_SPACING + INVADER_RADIUS); //temp pattern = patterns[strength]; } boolean march() { boolean hitwall = false; if(marchDir == MARCH_LEFT) { pos.x -= MARCH_DISTANCE; if(pos.x <= INVADER_RADIUS + MARCH_DISTANCE) hitwall = true; } else if(marchDir == MARCH_RIGHT) { pos.x += MARCH_DISTANCE; if(pos.x >= width - (INVADER_RADIUS + MARCH_DISTANCE)) hitwall = true; } else { pos.y += MARCH_DISTANCE; } return hitwall; } void draw() { color[] testColors = { color(255, 0, 0), color(255, 255, 0), color(0, 255, 0), color(0, 255, 255), color(0, 0, 255), color(255, 0, 255), color(255, 255, 255) }; //fill(testColors[strength]); fill(testColors[strength]); noStroke(); //ellipse(pos.x, pos.y, INVADER_RADIUS*2, INVADER_RADIUS*2); pushMatrix(); rectMode(CORNERS); translate(pos.x, pos.y); rect(pattern[0].x, pattern[0].y, pattern[1].x, pattern[1].y); rect(-pattern[0].x, pattern[0].y, -pattern[1].x, pattern[1].y); rect(pattern[2].x, pattern[2].y, pattern[3].x, pattern[3].y); rect(-pattern[2].x, pattern[2].y, -pattern[3].x, pattern[3].y); if((physicsFrame % MARCH_FRAMES) > MARCH_FRAMES/2) { rect(pattern[4].x, pattern[4].y, pattern[5].x, pattern[5].y); rect(-pattern[4].x, pattern[4].y, -pattern[5].x, pattern[5].y); } else { rect(pattern[6].x, pattern[6].y, pattern[7].x, pattern[7].y); rect(-pattern[6].x, pattern[6].y, -pattern[7].x, pattern[7].y); } rect(-INVADER_RADIUS*0.75,-INVADER_RADIUS*0.5, INVADER_RADIUS*0.75,INVADER_RADIUS*0.5); fill(0); rect(-INVADER_RADIUS*0.5,-INVADER_RADIUS*0.25, -INVADER_RADIUS*0.25,INVADER_RADIUS*0); rect(INVADER_RADIUS*0.5,-INVADER_RADIUS*0.25, INVADER_RADIUS*0.25,INVADER_RADIUS*0); rectMode(CORNER); popMatrix(); } void fireShot() { if(strength <= 0) missiles.add(new Missile(new PVector(pos.x, pos.y+INVADER_RADIUS), new PVector(0, 0.6), false)); //straight down else if(strength == 1) missiles.add(new Missile(new PVector(pos.x, pos.y+INVADER_RADIUS), new PVector(random(0.6)-0.3, 0.6), false)); //scatter else if(strength == 2) { //aim at random living or dead base int base = (int) random(NUM_CITIES); PVector origin = new PVector(pos.x, pos.y+INVADER_RADIUS); PVector target = new PVector(cityPos(base), height); PVector vel = PVector.sub(target, origin); vel.normalize(); missiles.add(new Missile(origin, vel, false)); } else if(strength == 3) { //aim at random living or dead base, banking shot off a wall int base = (int) random(NUM_CITIES); PVector origin = new PVector(pos.x, pos.y+INVADER_RADIUS); PVector target; if(random(2) < 1)target = new PVector(0 - cityPos(base), height); else target = new PVector(width * 2 - cityPos(base), height); PVector vel = PVector.sub(target, origin); vel.normalize(); missiles.add(new Missile(origin, vel, false)); } else if(strength == 4) { //aim fast shot at a random base, preferring living ones int base = (int) random(NUM_CITIES); for(int i = 0; i<5; i++) if (!cities[base]) base = (int) random(NUM_CITIES); //reroll for living city PVector origin = new PVector(pos.x, pos.y+INVADER_RADIUS); PVector target = new PVector(cityPos(base), height); PVector vel = PVector.sub(target, origin); vel.normalize(); vel.mult(1.5); missiles.add(new Missile(origin, vel, false)); } else if(strength == 5) { //aim fast bank shot at a living base int base = (int) random(NUM_CITIES); for(int i = 0; i<5; i++) if (!cities[base]) base = (int) random(NUM_CITIES); //reroll for living city PVector origin = new PVector(pos.x, pos.y+INVADER_RADIUS); PVector target; if(random(2) < 1)target = new PVector(0 - cityPos(base), height); else target = new PVector(width*2 - cityPos(base), height); PVector vel = PVector.sub(target, origin); vel.normalize(); vel.mult(1.5); missiles.add(new Missile(origin, vel, false)); } else missiles.add(new Missile(new PVector(pos.x, pos.y+INVADER_RADIUS), new PVector(random(4)-2, 1.7), false)); //fast random shot } void getHit() { explosions.add(new Explosion(pos, 0)); popSound.trigger(); } } /////////// PHYSICS void physicsStep() { int citiesRemaining = 0; for(int i=0; i < NUM_CITIES; i++) if(cities[i]) ++citiesRemaining; //playerWidth = 60 + 10*citiesRemaining; updatePlayerPos(); testExplosions(); testMissiles(); ///update explosions Iterator i = explosions.iterator(); while(i.hasNext()) { Explosion e = (Explosion) i.next(); e.life--; if(e.life <= 0) i.remove(); } i = missiles.iterator(); while(i.hasNext()) { Missile m = (Missile) i.next(); m.move(); if(m.pos.y > height) { i.remove(); explosions.add(new Explosion(m.pos, 1)); exploSound.trigger(); flawlessDefense = false; } else if (invaders.isEmpty()) { if(m.pos.y < 16) { i.remove(); explosions.add(new Explosion(m.pos, 1)); exploSound.trigger(); } } } if(physicsFrame%MARCH_FRAMES == 0) marchInvaders(); invaderThink(); if(invaders.isEmpty() && missiles.isEmpty() && explosions.isEmpty() && !gameOver) { nextLevel(); } physicsFrame++; waveTimer++; } void testExplosions() { ArrayList newExplosions = new ArrayList(); Iterator ei = explosions.iterator(); while(ei.hasNext()) { Explosion e = (Explosion) ei.next(); float r = e.radius(); Iterator vi = invaders.iterator(); while(vi.hasNext()) { Invader v = (Invader) vi.next(); if(inRadius(v.pos, e.pos, r)) { vi.remove(); score += min(100*(v.strength+1), 500); newExplosions.add(new Explosion(v.pos, 0)); popSound.trigger(); } } Iterator mi = missiles.iterator(); while(mi.hasNext()) { Missile m = (Missile) mi.next(); if(inRadius(m.pos, e.pos, r)) { mi.remove(); newExplosions.add(new Explosion(m.pos, 2)); exploSound.trigger(); } } for(int i = 0; i < NUM_CITIES; i++) { if(cities[i]) { if(inRadius(e.pos.x, e.pos.y, cityPos(i), height, r)) { cities[i] = false; newExplosions.add(new Explosion(new PVector(cityPos(i), height), 2)); exploSound.trigger(); flawlessDefense = false; } } } } explosions.addAll(newExplosions); } void testMissiles() { Iterator im = missiles.iterator(); while(im.hasNext()) { Missile m = (Missile) im.next(); for(int i = 0; i < invaders.size(); i++) { Invader test = (Invader) invaders.get(i); if(inRadius(m.pos, test.pos, INVADER_RADIUS) && m.friendly) { test.getHit(); PVector away = PVector.sub(m.pos, test.pos); away.normalize(); away.mult(m.vel.mag()); m.vel = away; m.origin = new PVector(m.pos.x, m.pos.y); PVector away2 = PVector.sub(m.pos, test.pos); away2.normalize(); away2.mult(INVADER_RADIUS); m.pos = PVector.add(away2, test.pos); } } } } void updatePlayerPos() { playerX = mouseX; } void marchInvaders() { boolean wall = false; for(int i = 0; i < invaders.size(); i++) { Invader v = (Invader) invaders.get(i); if(v.march()) wall = true; } if(wall) { if(marchDir == MARCH_LEFT) marchDir = MARCH_DOWN_RIGHT; else if(marchDir == MARCH_RIGHT) marchDir = MARCH_DOWN_LEFT; } else { if(marchDir == MARCH_DOWN_LEFT) marchDir = MARCH_LEFT; if(marchDir == MARCH_DOWN_RIGHT) marchDir = MARCH_RIGHT; } } void detonateMissiles() { boolean exploded = false; Iterator i = missiles.iterator(); while(i.hasNext()) { Missile m = (Missile) i.next(); if(m.friendly) { explosions.add(new Explosion(m.pos, 1)); exploded = true; i.remove(); } } if(exploded) exploSound.trigger(); } void invaderThink() { if(shotTimer <= 0 && !invaders.isEmpty()) { shotTimer = SHOT_DELAY_BASE / difficulty; boolean blocked; Invader randInvader; do{ int index = (int)random(invaders.size()); randInvader = (Invader) invaders.get(index); blocked = false; for(int i = index+1; i < invaders.size(); i++) { Invader test = (Invader) invaders.get(i); if(test.pos.x == randInvader.pos.x) { blocked = true; break; } } } while(blocked); if(!blocked) randInvader.fireShot(); } shotTimer--; } /////////// DRAWING void drawFrame() { if(!atTitle) { if(!paused)drawingFrame++; background(0); drawCities(); drawMissiles(); drawPlayer(); for(int i = 0; i< invaders.size(); i++) { Invader v = (Invader) invaders.get(i); v.draw(); } for(int i = 0; i< explosions.size(); i++) { Explosion e = (Explosion) explosions.get(i); e.draw(); } drawHud(); } else { background(0); fill(255); textFont(font, 32); textAlign(CENTER, TOP); text("SPACEOUT\nCOMBREAK\nVADERMAND", width/2, 96); textFont(font, 16); text("- A HISTORICAL RECONSTRUCTION -\n\n\nSELECT SIMULATION MODE:\n\nT - TRADITIONAL INTERPRETATION\nD - \"DOOMSDAY\" THEORY\n\n\nPRESS P TO PAUSE SIMULATION\nPRESS Q TO END SIMULATION", width/2, 224); } } void drawHud() { noSmooth(); textFont(font, 16); fill(255); textAlign(LEFT, TOP); text("SCORE "+score, 8, 8); int countdown = (int)max(TIME_BONUS_SECS - waveTimer/PHYS_FPS, 0); textAlign(CENTER, TOP); if(!gameOver) text(countdown, width/2, 8); if(!doomsday) { textAlign(RIGHT, TOP); text("NEXT CITY "+cityPoints, width-8, 8); } if(waveTimer < ANNOUNCE_TIME || gameOver) { textFont(font, 32); textAlign(CENTER, CENTER); text(announceString, width/2, height/2); textFont(font, 16); textAlign(CENTER, TOP); text(bonusString, width/2, height/2+24); } if(paused) { fill(255); textFont(font, 16); textAlign(CENTER, CENTER); text("PAUSED - CLICK TO RESUME", width/2, height/2-32); } } color blastColor() { int phase = drawingFrame % 4; return color(255, 255*(phase/2), 255*(phase%2)); } void drawPlayer() { fill(255); noStroke(); rect(playerX - playerWidth/2, playerY, playerWidth, playerHeight); } void drawMissiles() { Iterator i = missiles.iterator(); while(i.hasNext()) { Missile m = (Missile) i.next(); m.draw(); } } void drawCities() { noStroke(); fill(255, 255, 0); rect(0, height-8, width, 8); for(int i = 0; i < NUM_CITIES; i++) if(cities[i]) { strokeWeight(1); fill(0, 0, 255); stroke(0, 255, 255); ellipse(cityPos(i), height, CITY_RADIUS*2, CITY_RADIUS*2); } } ////////// STATE MANAGEMENT void spawnInvaders() { int strength = 0; int level = difficulty - START_DIFFICULTY; for(int r = 0; r < INVADER_BLOCK_HEIGHT; r++) { if(r == 0) strength = (level)/2+2; if(r == 1) strength = (level+1)/2+1; if(r == 2) strength = (level)/2+1; if(r == 3) strength = (level+1)/2; if(r == 4) strength = (level+1)/2-1; if(doomsday) strength = 7; for(int c = 0; c < INVADER_BLOCK_WIDTH; c++) { invaders.add(new Invader(c, r+1, strength)); } } } void rebuildCity() { for(int i = 0; i < cities.length; i++) { if(!cities[i]) { cities[i] = true; break; } } } void nextLevel() { difficulty++; // if(doomsday) difficulty = START_DIFFICULTY + 13; shotTimer = SHOT_DELAY_BASE / difficulty; int citiesRemaining = 0; for(int i=0; i < NUM_CITIES; i++) if(cities[i]) ++citiesRemaining; if(doomsday) announceString = "DOOMSDAY"; else announceString = "WAVE "+(difficulty - START_DIFFICULTY + 1); bonusString = ""; if(!firstLevel) { bonusString += "\n- BONUS -\n"; bonusString +="CITIES "+citiesRemaining+" x "+CITY_BONUS+"\n"; score += (citiesRemaining * CITY_BONUS); if(flawlessDefense) { bonusString +="FLAWLESS DEFENSE "+FLAWLESS_BONUS+"\n"; score += FLAWLESS_BONUS; } int waveSecs = waveTimer / PHYS_FPS; if(waveSecs < TIME_BONUS_SECS) { bonusString += "TIME "+(TIME_BONUS_SECS - waveSecs)+" x "+TIME_BONUS+"\n"; score += (TIME_BONUS_SECS - waveSecs)* TIME_BONUS; } while(citiesRemaining < NUM_CITIES && score >= cityPoints && !doomsday) { bonusString += cityPoints+" POINTS - CITY REBUILT\n"; cityPoints += EXTRA_CITY_AT; rebuildCity(); } } citiesRemaining = 0; for(int i=0; i < NUM_CITIES; i++) if(cities[i]) ++citiesRemaining; if(citiesRemaining == 0) { gameOver = true; announceString = "GAME OVER"; bonusString += "\n- FINAL SCORE -\n"+score; } waveTimer = 0; flawlessDefense = true; marchDir = MARCH_RIGHT; if(!gameOver) spawnInvaders(); firstLevel = false; } void titleScreen() { bgm.rewind(); bgm.pause(); atTitle = true; } void newGame() { missiles = new ArrayList(); explosions = new ArrayList(); invaders = new ArrayList(); patterns = new PVector[7][]; for(int j = 0; j < patterns.length; j++) { PVector[] pattern = new PVector[8]; for(int i = 0; i < pattern.length; i++) { PVector corner = new PVector((int)random(INVADER_RADIUS), -INVADER_RADIUS+(int)random(INVADER_RADIUS*2)); pattern[i] = corner; } patterns[j] = pattern; } cities = new boolean[NUM_CITIES]; for(int i = 0; i < NUM_CITIES; i++) cities[i]=true; drawingFrame = 0; physicsFrame = 0; score = 0; if(!doomsday) difficulty = START_DIFFICULTY - 1; else difficulty = DOOMSDAY_DIFFICULTY -1; bonusString = ""; cityPoints = EXTRA_CITY_AT; gameOver = false; atTitle = false; firstLevel = true; nextLevel(); bgm.loop(); } void pause(boolean toPause) { paused = toPause; } void setup() { font = loadFont("PressStartK-64.vlw"); size(640, 640, P2D); frameRate(FRATE); setSound(); titleScreen(); } void draw() { while(millis() > physMark + PHYS_MILLS) { if(!paused && !atTitle && !gameOver)physicsStep(); physMark += PHYS_MILLS; } if(millis() > drawMark + DRAW_MILLS) { drawFrame(); drawMark += DRAW_MILLS; } } void stop() { closeSound(); super.stop(); } /////////////// DEBUG void mousePressed() { if(!atTitle && !gameOver) { if(paused) pause(false); else detonateMissiles(); } else if (!atTitle && gameOver) { titleScreen(); } //explosions.add(new Explosion(new PVector(mouseX, mouseY), 0)); //missiles.add(new Missile(new PVector(mouseX, mouseY), new PVector(1, 1), false)); } void keyPressed() { if(!atTitle && !gameOver) { if(key == 'p') pause(true); else if(!paused && key == ' ') detonateMissiles(); else if(key == 'q') { for(int i = 0; i < cities.length; i++) cities[i] = false; invaders.clear(); missiles.clear(); explosions.clear(); gameOver = true; announceString = "GAME OVER"; bonusString = "\n- FINAL SCORE -\n"+score; } } else if (!atTitle && gameOver) { titleScreen(); } else if(atTitle) { if(key == 't') { doomsday = false; newGame(); } if(key == 'd') { doomsday = true; newGame(); } } }