import greenfoot.*; // (World, Actor, GreenfootImage, Greenfoot and MouseInfo) /** * Stuff that flies through the sky. Base class for both AI-controlled ships and manually controlled ones. * * @author Jannis Andrija Schnitzer, Martin Schend * @version 2011-01-22 */ public class Spaceship extends QuantumObject { protected int side; protected int level; protected int hitpoints = 0; protected int max_hitpoints = 0; /** * Variable containing the state of the blink animation that happens when the spaceship is damaged. * 1: start blink animation * 2..MAX_BLINK: continue blinking * MAX_BLINK+1: restore default sprite * * @see #doBlink() */ protected int blink = 0; /** * How many turns the blinking animations should last. */ protected int max_blink = 2; /** * Filename of the sprite to be used while blinking, i. e. of the spaceship's "blink" state. */ protected String blink_file = ""; /** * X distance between guns on one spaceship. */ public static final int GUN_DISTANCE = 10; /** * Determines the angular distance between guns when angled_guns is true. The angle between the first and the * last gun equals 2 * n_guns * GUN_ANGLE. The value is expressed in radians. * * @see #angled_guns * @see #n_guns */ public static final double GUN_ANGLE = .05; public static final int N_GUNS = 6; protected Gun[] guns; protected int n_guns; /** * Determines if guns should shoot in a straight way (angled_guns = false) or rather in a widespread way * (angled_guns = true). */ protected boolean angled_guns; public void act() { super.act(); for (int i = 0; i < N_GUNS; i++) { if (guns[i] != null) guns[i].tick(); } doBlink(); } public Spaceship(int the_level) { super(); guns = new Gun[N_GUNS]; for(int i = 0; i < N_GUNS; i++) guns[i] = null; n_guns = 0; angled_guns = false; level = the_level; // influences HP and such. Setting HP etc. should be done in a subclass's constructor. } /** * Add a gun in the given position. Doesn't check if position exceeds array bounds. This is a feature. * (Seriously: The check wasn't implemented because that way programming mistakes in MoreGuns etc. became * obvious more quickly.) * * @see MoreGuns */ public void addGun(int position, Gun new_gun) { if (guns[position] == null) { guns[position] = new_gun; guns[position].setSide(side); guns[position].setLevel(level); n_guns++; setAngledGuns(angled_guns); } } /** * Remove the gun in the given position. Doesn't check if position exceeds array bounds. * * @see #addGun(int position, Gun new_gun) */ public void removeGun(int position) { if (guns[position] != null) { guns[position] = null; n_guns--; setAngledGuns(angled_guns); } } public int getNumberOfGuns() { return n_guns; } /** * Set the guns' dx values so that the missiles are launched from appear in an neat, aligned fashion. * That is, make them launch from positions that are GUN_DISTANCE pixels apart from each other, and * aligned to the center of the spaceship. */ public void reorderGuns() { // needed because there *could* be holes in the guns array. int counter = 0; /* We *could* add i < N_GUNS in the loop test condition, but once again, programming errors are more * obvious if we don't. */ for (int i = 0; counter < n_guns; i++) { if (guns[i] == null) continue; if (n_guns % 2 == 0) guns[i].set_dx(GUN_DISTANCE*i - GUN_DISTANCE*(n_guns/2 - 1) - GUN_DISTANCE/2); else guns[i].set_dx(GUN_DISTANCE*i - GUN_DISTANCE*((n_guns-1)/2)); counter++; } } /** * Return an angle that lies between -max_angle and max_angle for x between 0 and 1 * * @param x number between 0.0 and 1.0 */ public double angle(double x, double max_angle) { double result = Math.asin(x-.5); result = max_angle/(Math.PI/6) * result; return result; } /** * Sets the ship to shoot straight or widespread (aka angled). Recalculate the guns' speed vectors based * on that value. */ public void setAngledGuns(boolean angled) { angled_guns = angled; if (angled) { for (int i = 0; i < n_guns; i++) { double abs_speed = guns[i].getAbsSpeed(); double angle; int x = 0; // used to get the correct order with both white and black ships. if (side == InvadersWorld.WHITE) x = i; else if (side == InvadersWorld.BLACK) x = n_guns-(i+1); if (n_guns == 1) angle = 0.0; else angle = angle((double)x/(double)(n_guns-1), n_guns * GUN_ANGLE); guns[i].setSpeed(abs_speed, angle); } } else { for (int i = 0; i < n_guns; i++) { double abs_speed = guns[i].getAbsSpeed(); guns[i].setSpeed(abs_speed, 0.0); // angle of 0 -> straight } } } public void setSide(int new_side) { side = new_side; } public int getSide() { return side; } public void setLevel(int new_level) { level = new_level; for (int i = 0; i < n_guns; i++) { guns[i].setLevel(level); } } public int getLevel() { return level; } /** * Try to fire (all) the guns. That is, call each gun's missile() method and check if something came out yet. * If yes, place it into the world. */ protected void fire() { int x = getX(); int y = getY(); World world = getWorld(); for (int i = 0; i < N_GUNS; i++) { if (guns[i] != null) { Missile m = guns[i].missile(); if (m != null) world.addObject(m, x + guns[i].get_dx(), y - guns[i].get_dy()); // subtracting dy here because greenfoot has 0,0 at the top left corner } } } public void addHitpoints(int n_hitpoints) { if (hitpoints + n_hitpoints <= max_hitpoints) hitpoints += n_hitpoints; else hitpoints = max_hitpoints; } public void removeHitpoints(int n_hitpoints) { blink = 1; hitpoints -= n_hitpoints; if (hitpoints <= 0) { this.beforeDestruction(); destroysOnNextTurn = true; // shouldn't survive } } public int getHitpoints() { return hitpoints; } public void setHitpoints(int hit) { hitpoints = hit; } public int getMaxHitpoints() { return max_hitpoints; } public void setMaxHitpoints(int max) { max_hitpoints = max; } /** * This method is called right before the spaceship is destroyed by (and only by) lack of hitpoints. * Empty here, to be overridden in subclasses that e. g. want to leave power-ups behind. */ public void beforeDestruction() { } /** * If the missile was shot by an enemy, subtract hitpoints. */ public boolean hit(Missile m) { if (m.getSide() == side) // the missile is ours return true; else { this.removeHitpoints(m.getDamage()); return true; } } /** * Sort-of abstract method available for overriddance [sic]. Subclasses can implement this to set their sprite * (back) to its default value. It's called whenever a reset to the default sprite is needed, e. g. after * the blinking animation. * * @see #doBlink() */ protected void setupImages() { } public void setBlinkFile(String the_blink_file) { blink_file = the_blink_file; } public String getBlinkFile() { return blink_file; } /** * Do the blinking animation. This is called whenever the ship has hitpoints subtracted. It evaluates and * changes the "blink" variable, and changes sprites if necessary. */ public void doBlink() { int max = max_blink + 1; // This way max_blink actually contains the number of turns the animation lasts. if (blink == 1) { try { this.setImage(blink_file); } catch (IllegalArgumentException ex) { // The image could not be found -- don't blink then. } } if (blink > 0 && blink < max_blink) { blink++; } else if (blink >= max_blink) // >= because you never know. { setupImages(); blink = 0; } } public void setMaxBlink(int max) { max_blink = max; } public int getMaxBlink() { return max_blink; } }