public class FEMultiplayer extends Game{
	private static Stage currentStage;
	private static Client client;
	private static Player localPlayer;
	
	public static Player turn;
	public static ClientOverworldStage map;
	public static ClientLobbyStage lobby;
	public static ConnectStage connect;
	
	// For testing fightstage
	private static Session testSession;

	
	public static void main(String[] args) {
		try{
			FEMultiplayer game = new FEMultiplayer();
//			SoundTrack.enabled = false;
			game.init(480, 320, "Fire Emblem Multiplayer");
			/* Testing code */
//			game.testFightStage();
//			game.testOverworldStage();
//			game.testDraftStage();
			game.loop();
		} catch (Exception e){
			System.err.println("Exception occurred, writing to logs...");
			e.printStackTrace();
			try{
				File errLog = new File("error_log_client" + System.currentTimeMillis()%100000000 + ".log");
				PrintWriter pw = new PrintWriter(errLog);
				e.printStackTrace(pw);
				pw.close();
			}catch (IOException e2){
				e2.printStackTrace();
			}
			System.exit(0);
		}
	}
	
	
	
	public void init(int width, int height, String name) {
		super.init(width, height, name);
		Player p1 = new Player("Player", (byte) 0);
		localPlayer = p1;
		ByteBuffer icon16, icon32;
		icon16 = icon32 = null;
		try {
			icon16 = ByteBuffer.wrap(TextureLoader.getTexture("PNG", ResourceLoader.getResourceAsStream("res/gui/icon16.png")).getTextureData());
			icon32 = ByteBuffer.wrap(TextureLoader.getTexture("PNG", ResourceLoader.getResourceAsStream("res/gui/icon32.png")).getTextureData());
		} catch (IOException e) {
			e.printStackTrace();
		}
		Display.setIcon(new ByteBuffer[]{icon16, icon32});
		FEResources.loadResources();
		FEResources.loadBitmapFonts();
		WeaponFactory.loadWeapons();
		UnitFactory.loadUnits();
		p1.getParty().setColor(Party.TEAM_BLUE);
		
		/* OpenGL final setup */
		glEnable(GL_LINE_SMOOTH);
		
		UnitFactory.getUnit("Lyn");
		connect = new ConnectStage();
		setCurrentStage(new TitleStage());
		messages = new CopyOnWriteArrayList<Message>();
		SoundTrack.loop("main_theme");
		
	}
	
	public void testDraftStage() {
		Player p1 = localPlayer;
		testSession = new Session();
		testSession.setMaxUnits(6);
		Player p2 = new Player("p2", (byte) 1);
		p2.getParty().setColor(Party.TEAM_RED);
		p2.getParty().addUnit(UnitFactory.getUnit("Mia"));
		p2.getParty().addUnit(UnitFactory.getUnit("L'Arachel"));
		testSession.addPlayer(p1);
		testSession.addPlayer(p2);
		currentStage = new TeamDraftStage(testSession);
	}
	
	public void testFightStage(){
		Player p1 = localPlayer;
		testSession = new Session();
		testSession.setMap("test");
		testSession.setObjective(new Seize());
		Player p2 = new Player("p2", (byte) 1);
		p2.getParty().setColor(Party.TEAM_RED);
		testSession.addPlayer(p1);
		testSession.addPlayer(p2);
		
		map = new ClientOverworldStage(testSession);
		Unit u1 = UnitFactory.getUnit("Gilliam");
		u1.getInventory().add(WeaponFactory.getWeapon("Gradivus"));
		map.addUnit(u1, 0, 0);
		u1.equip(0);
		u1.setLevel(20);
		u1.loadMapSprites();
		p1.getParty().addUnit(u1);
		
		Unit u2 = UnitFactory.getUnit("Joshua");
		u2.getInventory().add(WeaponFactory.getWeapon("Wo Dao"));
		map.addUnit(u2, 1, 0);
		u2.equip(0);
		u2.setLevel(15);
		u2.loadMapSprites();
		p2.getParty().addUnit(u2);
		
		map.processAddStack();
		int u2Uses = u2.getWeapon().getMaxUses();
		CombatCalculator calc = new CombatCalculator(new UnitIdentifier(u1), new UnitIdentifier(u2), true);
		System.out.println(calc.getAttackQueue());
		
		u1.getInventory().add(WeaponFactory.getWeapon("Debug Noodle"));
		u1.equip(0);
		u2.getWeapon().setUsesDEBUGGING(u2Uses);
		u1.fillHp();
		u2.fillHp();
		setCurrentStage(new FightStage(new UnitIdentifier(u1), new UnitIdentifier(u2), calc.getAttackQueue()));
	}
	
	public void testOverworldStage() {
		testSession = new Session();
		testSession.setMap("test");
		testSession.setObjective(new Seize());
		testSession.addPlayer(localPlayer);
		
		Player p2 = new Player("P2", (byte)1);
		p2.getParty().setColor(Party.TEAM_RED);
		testSession.addPlayer(p2);
		
		Unit u1 = UnitFactory.getUnit("Gilliam");
		u1.addToInventory(WeaponFactory.getWeapon("Gradivus"));
		u1.equip(0);
		u1.setHp(1);
		localPlayer.getParty().addUnit(u1);
		
		Unit u3 = UnitFactory.getUnit("Joshua");
		u3.addToInventory(WeaponFactory.getWeapon("Wo Dao"));
		u3.equip(0);
		u3.setHp(1);
		p2.getParty().addUnit(u3);
		
		Unit u2 = UnitFactory.getUnit("Lute");
		u2.addToInventory(WeaponFactory.getWeapon("Physic"));
		localPlayer.getParty().addUnit(u2);
		
		currentStage = new ClientOverworldStage(testSession);

	}
	
	public static Unit getUnit(UnitIdentifier id){
		for(Player p : getPlayers().values()){
			if(!p.isSpectator() && p.getParty().getColor().equals(id.partyColor)){
				return p.getParty().search(id.name);
			}
		}
		return null;
	}
	
	public static void connect(String nickname, String ip) {
		getLocalPlayer().setName(nickname);
		client = new Client(ip, 21255);
		if(client.isOpen()) {
			lobby = new ClientLobbyStage(client.getSession());
			setCurrentStage(lobby);
			client.start();
		} else {
			currentStage.addEntity(new Notification(
					180, 120, "default_med", "ERROR: Could not connect to the server!", 5f, new Color(255, 100, 100), 0f));
		}
	}

	@Override
	public void loop() {
		while(!Display.isCloseRequested()) {
			time = System.nanoTime();
			glClear(GL_COLOR_BUFFER_BIT |
			        GL_DEPTH_BUFFER_BIT |
			        GL_STENCIL_BUFFER_BIT);
			glClearDepth(1.0f);
			getInput();
			messages.clear();
			if(client != null){
				messages.addAll(client.getMessages());
				for(Message m : messages)
					client.messages.remove(m);
			}
			SoundStore.get().poll(0);
			glPushMatrix();
			//Global resolution scale
//			Renderer.scale(scaleX, scaleY);
			if(!paused) {
				currentStage.beginStep();
				currentStage.onStep();
				currentStage.processAddStack();
				currentStage.processRemoveStack();
				currentStage.render();
//				FEResources.getBitmapFont("stat_numbers").render(
//						(int)(1.0f/getDeltaSeconds())+"", 440f, 0f, 0f);
				currentStage.endStep();
			}
			glPopMatrix();
			Display.update();
			timeDelta = System.nanoTime()-time;
		}
		AL.destroy();
		Display.destroy();
		if(client != null && client.isOpen()) client.quit();
	}
	
	public static void reportFightResults(FightStage stage){ 
		
	}
	
	public static void send(UnitIdentifier u, int moveX, int moveY, Object... cmds){
		for(Object o: cmds){
			System.out.print(o + " ");
		}
		System.out.println();
		client.sendMessage(new CommandMessage(u, moveX, moveY, null, cmds));
	}
	
	public static void goToFightStage(UnitIdentifier u, UnitIdentifier other, 
			ArrayList<AttackRecord> queue) {
			FightStage to = new FightStage(u, other, queue);
			currentStage.addEntity(new OverworldFightTransition((ClientOverworldStage)currentStage, to, u, other));
	}
	
	public static void setCurrentStage(Stage stage) {
		currentStage = stage;
		if(stage.soundTrack != null){
			SoundTrack.loop(stage.soundTrack);
		}
	}

	public static Stage getOverworldStage() {
		return map;
	}
	
	public static Client getClient() {
		return client;
	}

	public static Stage getCurrentStage() {
		return currentStage;
	}

	public static void setLocalPlayer(Player p) {
		localPlayer = p;
	}
	
	public static Player getLocalPlayer() {
		return localPlayer;
	}
	
	public static HashMap<Byte, Player> getPlayers() {
		return getSession().getPlayerMap();
	}
	
	public static Session getSession() {
		if(client == null)
			return testSession;
		return client.getSession();
	}

} public class FEResources {
	private static String[] searchFolders = 
		{"battle_anim", "battle_anim/static", "map_mugshots", "gui", "map_anim"};
	
	
	private static HashMap<String, Audio> audio;
	private static HashMap<String, AnimationData> textures;
	private static HashMap<String, BitmapFont> bitmapFonts;
	
	static {
		audio = new HashMap<String, Audio>();
		textures = new HashMap<String, AnimationData>();
		bitmapFonts = new HashMap<String, BitmapFont>();
	}

	public static Texture getTexture(String string) {
		return getTextureData(string).getTexture();
	}
	
	public static boolean hasTexture(String string){
		return textures.containsKey(string);
	}
	
	public static void loadResources() {
		try {
			//Load bitmap fonts
			loadBitmapFonts();
			
			// Textures
			textures.put("whoops", new AnimationData("res/whoops.png"));
			loadTextures();	
			//load audio
			audio.put("miss", AudioLoader.getAudio("WAV",
					ResourceLoader.getResourceAsStream("res/sfx/miss.wav")));
			
		} catch (IOException e) {
			int max = GL11.glGetInteger(GL11.GL_MAX_TEXTURE_SIZE);
			System.out.println(max);
			e.printStackTrace();
		}
		System.gc();
	}
	public static AnimationData getMapTexture(String name){
		AnimationData t = textures.get(name);
		if(t!=null) return t;
		System.out.println("Warn:" + name + " not explicitly defined");
		for(String loc: searchFolders){
			try{
				AnimationData txt = new AnimationData("res/" + loc + "/" + name + ".png", 
						96, 24, 4, 4, 4, 4, 0, null, null);
				textures.put(name, txt);
				return txt;
			} catch (Exception e){
				
			}
		}
		return textures.get("whoops");
	}

	private static void loadTextures() {
		long startTime = System.nanoTime();
		// TODO Load textures from JSON
		InputStream file = ResourceLoader.getResourceAsStream("res/resources.json");
		Scanner in = new Scanner(file);
		StringBuilder sb = new StringBuilder();
		while(in.hasNextLine()) {
			sb.append(in.nextLine());
		}
		String json = sb.toString();
		
		JSONObject resources = null;
		try {
			resources = (JSONObject) JSONValue.parseWithException(json);
		} catch (ParseException e1) {
			// TODO Auto-generated catch block
			e1.printStackTrace();
		}
		JSONArray txArray = (JSONArray) resources.get("textures");
		LoadStage.setMaximum(txArray.size());
		for(Object obj : txArray) {
			JSONObject texture = (JSONObject) obj;
			String name = (String)texture.get("name");
			String path = (String)texture.get("path");
			Number width = (Number)texture.get("width");
			Number height = (Number)texture.get("height");
			Number frames = (Number)texture.get("frames");
			Number columns = (Number)texture.get("columns");
			Number freeze = (Number)texture.get("freeze");
			Number offsetX = (Number)texture.get("offsetX");
			Number offsetY = (Number)texture.get("offsetY");
			Number speed = (Number)texture.get("speed");
			Number shakeFrames = (Number)texture.get("shakeFrames");
			Number shakeIntensity = (Number)texture.get("shakeIntensity");
			Boolean stop = (Boolean)texture.get("stop");
			JSONArray hitArray = (JSONArray) texture.get("hitframes");
			JSONArray audioArray = (JSONArray) texture.get("soundMap");
			HashMap<Integer, String> audioMap = new HashMap<Integer, String>();
			
			int[] hitframes;
			if(hitArray != null) {
				hitframes = new int[hitArray.size()];
				for(int i=0; i<hitframes.length; i++) {
					hitframes[i] = ((Number)hitArray.get(i)).intValue();
				}
			} else {
				hitframes = new int[0];
			}
			
			if(audioArray != null) {
				for(Object obj2 : audioArray) {
					JSONObject audio = (JSONObject) obj2; 
					audioMap.put(((Number)audio.get("frame")).intValue(), 
							(String)audio.get("sound"));
				}
			}
			AnimationData data;
			if(width == null) {
				data = new AnimationData(path);
			} else {
				data = new AnimationData(path,
						width.intValue(),
						height.intValue(),
						frames.intValue(),
						columns.intValue(),
						offsetX.intValue(),
						offsetY.intValue(),
						freeze.intValue(),
						hitframes,
						audioMap);
			}
			if(speed != null)
				data.speed = speed.floatValue();
			if(shakeFrames != null)
				data.shakeFrames = shakeFrames.intValue();
			if(shakeIntensity != null)
				data.shakeIntensity = shakeIntensity.intValue();
			if(stop != null)
				data.stop = stop.booleanValue();
			textures.put(name, data);
			if((System.nanoTime() - startTime)/1000000.0f > 100){
				LoadStage.update(textures.size());
				LoadStage.render();
			}
		}
		in.close();
	}

	public static BitmapFont getBitmapFont(String name) {
		return bitmapFonts.get(name);
	}
	
	public static void loadBitmapFonts() {
		Scanner in = new Scanner(ResourceLoader.getResourceAsStream("res/fonts/fonts.txt"));
		while(in.hasNextLine()) {
			String line = in.nextLine();
			if(line.startsWith("#"))
				continue;
			if(line.startsWith("define")) {
				String name = line.split(":")[1];
				String texName = in.nextLine();
				char[] chars = in.nextLine().toCharArray();
				int height = Integer.parseInt(in.nextLine());
				int spacing = Integer.parseInt(in.nextLine());
				char[] widths = in.nextLine().toCharArray();
				
				BitmapFont font = new BitmapFont(texName);
				font.setHeight(height);
				font.setSpacing(spacing);
				int pos = 0;
				for(int i=0; i<chars.length; i++) {
					int width = Integer.parseInt(widths[i]+"");
					font.put(chars[i], pos, width);
					pos += width;
				}
				bitmapFonts.put(name, font);
				System.out.println(name+"(bitmap font) loaded");
			}
		}
		in.close();
	}

	public static AnimationData getTextureData(String string) {
		AnimationData t = textures.get(string);
		if(t != null) {
			return t;
		} else {
			//try to get it, in case we forgot
			System.err.println("Warn: " + string + " not explicitly defined");
			for(String loc: searchFolders){
				if(ResourceLoader.resourceExists("res/" + loc + "/" + string + ".png")){
					AnimationData txt = new AnimationData("res/" + loc + "/" + string + ".png");
					textures.put(string, txt);
					return txt;
				}
				
			}
			return textures.get("whoops");
		}
	}

	public static Audio getAudio(String name) {
		Audio a = audio.get(name);
		if(a == null) {
//			System.err.println("Warn: " + name + " not explicitly defined");
			try{
				Audio b = AudioLoader.getAudio("WAV",
						ResourceLoader.getResourceAsStream("res/sfx/"+name+".wav"));
				audio.put(name, b);
				return b;
			} catch (Exception e){
				return null;
			}
		} else {
			return a;
		}
	}

} public class AnimationArgs {
	public String userclass;
	public String wepAnimName;
	public String classification;
	public boolean left;
	public Unit unit;
	public int range;
	
	public AnimationArgs(Unit u, boolean left, int range){
		userclass = u.functionalClassName();
		this.left = left;
		this.range = range;
		unit = u;
		Weapon w = u.getWeapon();
		if(w == null){
			Type wType = u.getTheClass().usableWeapon.get(0);
			if(wType.isMagic()){
				wepAnimName = "magic";
				classification = "magic";
			} else {
				wepAnimName = wType.toString().toLowerCase();
				classification = "normal";
			}
		}else if(w.isMagic()){
			wepAnimName = "magic";
			classification = "magic";
		} else {
			wepAnimName = w.type.toString().toLowerCase();
			classification = "normal";
			if(w.range.contains(range) && range > 1){
				if(w.type == Weapon.Type.AXE){
					wepAnimName = "handaxe";
					classification = "ranged";
				}
				if (w.type == Weapon.Type.LANCE){
					wepAnimName = "javelin";
					classification = "ranged";
				} 
				if (w.type == Weapon.Type.SWORD){
					wepAnimName = "rangedsword";
					classification = "ranged";
				}
			}
			if (w.type == Weapon.Type.BOW){
				this.classification = "ranged";
			}
		}
	}
}
 public abstract class AttackAnimation extends Animation {
	
	protected int[] hitframes;
	private int headX;
	private int headY;
	private int prevFrame;
	private boolean stop;
	protected int loopUntil;
	
	protected AnimationArgs animationArgs;
	protected FightStage stage;
	protected int freeze;
	protected HashMap<Integer, String> soundMap;
	
	public static final float NORMAL_SPEED = .055f;
	private float defaultSpeed = NORMAL_SPEED;
	
	//TODO You can't have a hit frame on the very last frame
	public AttackAnimation(AnimationData data, FightStage stage, AnimationArgs animArgs) {
		super(data.getTexture(), data.frameWidth, data.frameHeight, data.frames,
                data.columns, data.offsetX, data.offsetY, 0);
		this.hitframes = data.hitframes;
		this.stage = stage;
		this.animationArgs = animArgs;
		this.freeze = data.freeze;
		this.loopUntil = -1;
		this.prevFrame = -1;
		this.soundMap = data.soundMap;
		this.stop = data.stop;
		if(data.speed != 0) {
			defaultSpeed = data.speed;
		}
	}
	
	@Override
	public void done() {
		((FightStage)stage).setCurrentEvent(FightStage.DONE);
		setSpeed(0);
		if(stop)
			setFrame(getFrame()-1);
		else
			setFrame(0);
	}
	
	@Override
	public void update() {
		prevFrame = getFrame();
		super.update();
		if(prevFrame != getFrame()) {
			for(int j=0; j<hitframes.length; j++) {
				int i = hitframes[j];
				if(getFrame() == i) {
					if(j == hitframes.length - 1)
						onLastHit();
					else
						onHit();
				}
			}
			if(soundMap.get(getFrame()) != null) {
				AudioPlayer.playAudio(soundMap.get(getFrame()), 1, 1);
			}
		}
		if(getFrame() == loopUntil) {
			setFrame(getFrame()-freeze);
		}
	}
	
	protected void loopNextFrames(int frames){
		if(frames == 0){
			setSpeed(0);
		} else if (frames > 0){
			loopUntil = getFrame() + frames;
		}
	}

	public int getHeadX() {
		return headX;
	}
	
	public int getHeadY() {
		return headY;
	}
	
	@Override
	public void setSpeed(float speed) {
		super.setSpeed(speed);
		loopUntil = -1;
	}
	
	
	public float getDefaultSpeed() {
		return defaultSpeed;
	}
	
	public abstract void onHit();
	public abstract void onLastHit();
	
	public static AttackAnimation createAnimation(AnimationData data, 
			FightStage stage, AnimationArgs args){
		if(args.classification.equals("normal")){
			return new NormalAttack(data, stage, args);
		} else if (args.classification.equals("ranged")){
			return new ProjectileAttack(data, stage, args);
		} else if (args.classification.equals("magic")){
			return new MagicAttack(data, stage, args);
		}
		return null;
	}

	public int getNextHitFrame() {
		for(int h : hitframes) {
			if(h >= getFrame()) {
				return h;
			}
		}
		return -1;
	}

} public class DodgeAnimation extends Animation {
	
	public static final float DODGE_DURATION = 0.25f;
	public static final float NORMAL_SPEED = .06f;
	
	private float dodgeTimer;
	private boolean dodging;

	public DodgeAnimation(AnimationData data) {
		super(data.getTexture(), data.frameWidth, data.frameHeight, data.frames,
				data.columns, data.offsetX, data.offsetY,
				DodgeAnimation.NORMAL_SPEED);
		dodgeTimer = 0;
	}
	
	@Override
	public void update() {
		super.update();
		if(dodging) {
			dodgeTimer += Game.getDeltaSeconds();
			if(dodgeTimer > DODGE_DURATION) {
				dodging = false;
				setSpeed(-DodgeAnimation.NORMAL_SPEED);
			}
		}
	}
	
	@Override
	public void done() {
		if(speed > 0) {
			dodging = true;
			setSpeed(0);
			setFrame(getLength()-1);
		} else if (speed < 0) {
			getSprite().setAnimation("ATTACK");
			getSprite().setSpeed(0);
			dodging = false;
			dodgeTimer = 0;
		}
	}

} public class HUD extends Entity {
	private Unit unit;
	private int sign;
	private String hit,crit,dmg;
	private static Texture battleStats;
	
	public HUD(Unit u1, Unit u2, FightStage stage) {
		super(0, 0);
		this.unit = u1;
		sign = stage.isLeft(u1) ? -1 : 1;
		this.stage = stage;
		if(battleStats == null)
			battleStats = FEResources.getTexture("gui_battleStats");
		
		if (!CombatCalculator.shouldAttack(u1, u2, stage.getRange())) {
			hit = "  -";
			crit = "  -";
			dmg = "  -";
		} else {
			hit = String.format("%3d",
					Math.min(100, Math.max(CombatCalculator.hitRate(u1, u2), 0)));
			crit = String.format("%3d",
					Math.min(100, Math.max(u1.crit() - u2.dodge(), 0)));
			dmg = String.format("%3d", Math.min(100,
					Math.max(CombatCalculator.calculateBaseDamage(u1, u2), 0)));
		}

		renderDepth = FightStage.HUD_DEPTH;

		stage.addEntity(new ItemDisplay(FightStage.CENTRAL_AXIS + sign * 39
				- 37, FightStage.FLOOR + 13, u1.getWeapon(), false));
	}

	public void render() {
		// Main status
		Renderer.drawRectangle(FightStage.CENTRAL_AXIS + sign * 120,
				FightStage.FLOOR + 12, FightStage.CENTRAL_AXIS,
				FightStage.FLOOR + 56, renderDepth, FightStage.BORDER_DARK);
		Renderer.drawRectangle(FightStage.CENTRAL_AXIS + sign * 119,
				FightStage.FLOOR + 13, FightStage.CENTRAL_AXIS + sign,
				FightStage.FLOOR + 55, renderDepth, FightStage.BORDER_LIGHT);
		Renderer.drawRectangle(FightStage.CENTRAL_AXIS + sign * 118,
				FightStage.FLOOR + 14, FightStage.CENTRAL_AXIS + sign * 2,
				FightStage.FLOOR + 54, renderDepth, unit.getPartyColor().darker(0.5f));

		// Weapon
		Renderer.drawRectangle(FightStage.CENTRAL_AXIS + sign * 119,
				FightStage.FLOOR + 13, FightStage.CENTRAL_AXIS + sign,
				FightStage.FLOOR + 31, renderDepth, FightStage.BORDER_LIGHT);
		Renderer.drawRectangle(FightStage.CENTRAL_AXIS + sign * 118,
				FightStage.FLOOR + 14, FightStage.CENTRAL_AXIS + sign * 2,
				FightStage.FLOOR + 30, renderDepth, FightStage.NEUTRAL);

		// Attack Stats
		Renderer.drawRectangle(FightStage.CENTRAL_AXIS + sign * 120,
				FightStage.FLOOR + 2, FightStage.CENTRAL_AXIS + sign * 76,
				FightStage.FLOOR + 32, renderDepth, FightStage.BORDER_DARK);
		Renderer.drawRectangle(FightStage.CENTRAL_AXIS + sign * 119,
				FightStage.FLOOR + 3, FightStage.CENTRAL_AXIS + sign * 77,
				FightStage.FLOOR + 31, renderDepth, FightStage.BORDER_LIGHT);
		Renderer.drawRectangle(FightStage.CENTRAL_AXIS + sign * 118,
				FightStage.FLOOR + 4, FightStage.CENTRAL_AXIS + sign * 78,
				FightStage.FLOOR + 30, renderDepth, unit.getPartyColor());
		
		Renderer.render(battleStats, 0, 0, 1, 1, FightStage.CENTRAL_AXIS
				+ sign * 98 - 18, FightStage.FLOOR+5, FightStage.CENTRAL_AXIS
				+ sign * 98 - 3, FightStage.FLOOR+29, renderDepth);

		Renderer.drawString("stat_numbers", hit, FightStage.CENTRAL_AXIS + sign * 98 - 5, 
				FightStage.FLOOR + 5, renderDepth);

		Renderer.drawString("stat_numbers", dmg, FightStage.CENTRAL_AXIS + sign * 98 - 5,
				FightStage.FLOOR + 13, renderDepth);

		Renderer.drawString("stat_numbers", crit,FightStage.CENTRAL_AXIS + sign * 98 - 5, 
				FightStage.FLOOR + 21, renderDepth);

		// Name
		Renderer.drawRectangle(FightStage.CENTRAL_AXIS + sign * 120,
				FightStage.FLOOR - 99, FightStage.CENTRAL_AXIS + sign * 63,
				FightStage.FLOOR - 77, renderDepth, FightStage.BORDER_DARK);
		Renderer.drawRectangle(FightStage.CENTRAL_AXIS + sign * 120,
				FightStage.FLOOR - 98, FightStage.CENTRAL_AXIS + sign * 64,
				FightStage.FLOOR - 78, renderDepth, FightStage.BORDER_LIGHT);
		Renderer.drawRectangle(FightStage.CENTRAL_AXIS + sign * 120,
				FightStage.FLOOR - 97, FightStage.CENTRAL_AXIS + sign * 65,
				FightStage.FLOOR - 79, renderDepth, unit.getPartyColor());
		Renderer.drawString("default_med", unit.name, FightStage.CENTRAL_AXIS + sign
				* 94 - 16, 11, 0.0f);
	}
}
 public class HitEffect extends Entity {
	private boolean left;
	private float shakeLength;
	private int shakeIntensity;

	public HitEffect(String name, boolean leftAttacking, final boolean crit, boolean loadTxt, boolean primary) {
		super(0, 0);
		left = leftAttacking;
		final AnimationData data = getHitTexture(name, crit);
		if(data.shakeIntensity > 0){
			shakeIntensity = data.shakeIntensity;
		} else {
			shakeIntensity = 5;
		}
		
		if(data.shakeFrames > 0){
			shakeLength = data.shakeFrames;
		} else if(data.hitframes.length > 0){
			shakeLength = data.frames - data.hitframes[0] + (crit?0:0.8f);
		} else {
			shakeLength = data.frames + (crit?0:0.8f);
		}
		final int realHit;
		if(!primary){
			realHit = -1;
		} else if(data.hitframes.length != 0){
			realHit = data.hitframes[0];
		} else {
			realHit = 0;
		}
		if(loadTxt){
			Animation anim = new Animation(FightStage.getPreload(getHitTextureName(name, crit)), data.frameWidth,
					data.frameHeight, data.frames, data.columns, data.offsetX,
					data.offsetY, data.speed==0.0f?0.05f:data.speed) {
				HashMap<Integer, String> soundMap = new HashMap<Integer, String>(data.soundMap);
				int hitframe = realHit;
				public void update() {
					super.update();
					if(soundMap.get(0) != null){
						AudioPlayer.playAudio(soundMap.remove(0), 1, 1);
					}
					if(soundMap.get(getFrame()) != null) {
						AudioPlayer.playAudio(soundMap.remove(getFrame()), 1, 1);
					}
					if(getFrame()>hitframe && hitframe >= 0){
						hitframe = -1; 
						((FightStage) stage).setCurrentEvent(FightStage.ATTACKED);
					}
				}
				@Override
				public void done() {
					destroy();
				}
			};
			sprite.addAnimation("default", anim);
		}
		renderDepth = FightStage.EFFECT_DEPTH;
	}

	@Override
	public void render() {
		Transform t = new Transform();
		int offset = - ((FightStage) stage).distanceToHead();
		if (left) {
			t.flipHorizontal();
			offset *= -1;
		}
		sprite.render(FightStage.CENTRAL_AXIS + offset,
				FightStage.FLOOR, renderDepth, t);
	}
	
	public float getShakeLength(){
		return shakeLength;
	}
	
	public int getShakeIntensity() {
		return shakeIntensity;
	}
	
	public static String getHitTextureName(String name, boolean crit){
		String critName = name + "_critical";
		if(FEResources.hasTexture("hit_effect_" + critName) && crit){
			return "hit_effect_" + critName;
		} else {
			return "hit_effect_" + name;
		}
	}
	
	public static AnimationData getHitTexture(String name, boolean crit) {
		String critName = name + "_critical";
		if(FEResources.hasTexture("hit_effect_" + critName) && crit){
			return FEResources.getTextureData("hit_effect_" + critName);
		} else {
			return FEResources.getTextureData("hit_effect_" + name);
		}
	}

	public static List<HitEffect> getEffects(AnimationArgs animArgs,
			AttackRecord rec, boolean loadTex) {
		boolean crit = rec.animation.contains("Critical");
		boolean primary = true;
		List<HitEffect> effects = new ArrayList<HitEffect>();
		
		for(String effectName: getEffectNames(animArgs, rec)){
			effects.add(new HitEffect(effectName, animArgs.left, crit, loadTex, primary));
			primary = false;
		}

		return effects;
	}
	
	public static List<String> getEffectNames(AnimationArgs animArgs, AttackRecord rec){
		List<String> effects = new ArrayList<String>();
		if(animArgs.unit.getWeapon() == null) return effects;
		if (animArgs.unit.getWeapon().type == Weapon.Type.STAFF) {
			effects.add("heal");
		}
		
		if (animArgs.unit.getWeapon().isMagic()) {
			effects.add(animArgs.unit.getWeapon().name.replaceAll(" ", "-").toLowerCase());
		}
		
		if(animArgs.wepAnimName.equals("rangedsword")) {
			effects.add(animArgs.unit.getWeapon().name.replaceAll(" ", "-").toLowerCase());
		}
		
		for(String anim: FightStage.analyzeAnimation(rec.animation, "(a)", false)){
			if(!FEResources.hasTexture("hit_effect_" + anim.toLowerCase()) && 
					anim.matches(".*\\d")){ 
				anim = anim.substring(0, anim.length() - 1);
			}
			if(FEResources.hasTexture("hit_effect_" + anim.toLowerCase())){
				effects.add(anim.toLowerCase());
			} else {
				
			}
		}
		
		
		if (effects.size() == 0 && rec.damage != 0) { // We have nothing														// nothing.
			effects.add("attack");
		}
		
		for(String anim: FightStage.analyzeAnimation(rec.animation, "(d)", false)){
			if(anim.matches(".*\\d")) 
				anim = anim.substring(0, anim.length() - 1);
			if(FEResources.hasTexture("hit_effect_" + anim.toLowerCase())){
				effects.add(anim.toLowerCase());
			}
		}


		return effects;
	}


	
	
}
 public class MagicEffect extends Entity {
	private AnimationArgs args;

	public MagicEffect(final AnimationArgs args) {
		super(0, 0);
		this.args = args;
		final AnimationData data = getTexture(args);
		Animation anim = new Animation(FightStage.getPreload(getTextureName(args)), data.frameWidth,
				data.frameHeight, data.frames, data.columns, data.offsetX,
				data.offsetY, data.speed==0.0f?0.05f:data.speed) {
			HashMap<Integer, String> soundMap = data.soundMap;
			int prevFrame = -1;
			public void update() {
				super.update();
				if(soundMap.get(getFrame()) != null && prevFrame != getFrame()) {
					prevFrame = getFrame();
					AudioPlayer.playAudio(soundMap.get(getFrame()), 1, 1);
				}
			}
			@Override
			public void done() {
				setFrame(0);
				setSpeed(0);
				((FightStage) stage).setCurrentEvent(FightStage.HIT_EFFECT);
				((FightStage) stage).moveCamera(args.left);
				destroy();
			}
		};
		sprite.addAnimation("default", anim);
		
		renderDepth = FightStage.EFFECT_DEPTH;
	}

	public void render(){
		Transform t = new Transform();
		int offset = FightStage.rangeToHeadDistance(args.range);
		if (args.left) {
			t.flipHorizontal();
			offset *=-1;
		}
		sprite.render(FightStage.CENTRAL_AXIS + offset,				
				FightStage.FLOOR, 0, t);
		
	}

	public static AnimationData getTexture(String name) {
		return FEResources.getTextureData(name);
	}
	
	public static AnimationData getTexture(AnimationArgs args) {
		return getTexture(getTextureName(args));
	}
	
	public static String getTextureName(AnimationArgs args){
		return "magic_effect_" + args.unit.getWeapon().name.toLowerCase();
	}

}
 public class MissEffect extends Entity {

	private boolean left;
	public MissEffect(boolean left) {
		super(0, 0);
		AudioPlayer.playAudio("miss", 1, 1);
		Animation anim = new Animation(FEResources.getTexture("miss"), 38, 26, 20, 5, 0, 0, .015f) {
			@Override
			public void update() {
				super.update();
				if(getFrame() == 17)
					setSpeed(.08f);
			}
			@Override
			public void done() {
				destroy();
			}
		};
		sprite.addAnimation("default", anim);
		this.left = left;
		
		renderDepth = FightStage.EFFECT_DEPTH;
	}
	
	@Override
	public void render() {
		if(left)
			sprite.render(FightStage.CENTRAL_AXIS-60, FightStage.FLOOR-80, 0.0f);
		else
			sprite.render(FightStage.CENTRAL_AXIS+22, FightStage.FLOOR-80, 0.0f);
	}
	
	

} public class NoDamageEffect extends Entity {

	private boolean left;
	public NoDamageEffect(boolean left) {
		super(0, 0);
		AudioPlayer.playAudio("no_damage", 1, 1);
		Animation anim = new Animation(FEResources.getTexture("no_damage"), 78, 26, 20, 4, 0, 0, .015f) {
			@Override
			public void update() {
				super.update();
				if(getFrame() == 17)
					setSpeed(.08f);
			}
			@Override
			public void done() {
				destroy();
			}
		};
		sprite.addAnimation("default", anim);
		this.left = left;
		
		renderDepth = FightStage.EFFECT_DEPTH;
	}
	
	@Override
	public void render() {
		if(left)
			sprite.render(FightStage.CENTRAL_AXIS-60, FightStage.FLOOR-80, 0.0f);
		else
			sprite.render(FightStage.CENTRAL_AXIS+22, FightStage.FLOOR-80, 0.0f);
	}
	
	

}
 public class Platform extends Entity {
	private boolean left;
	private Texture texture;

	public Platform(Terrain t, boolean left, int range) {
		super(left ? 0 : FightStage.CENTRAL_AXIS, FightStage.FLOOR - 16);
		this.left = left;
		String txtName = t.name().toLowerCase();
		if(range > 1){
			txtName += "_far";
		}
		texture = FEResources.getTexture("platform_" + txtName);
		renderDepth = FightStage.PLATFORM_DEPTH;
	}

	public void render() {
		Transform t = new Transform();
		if (!left) {
			t.flipHorizontal();
		}
		Renderer.render(texture, 0, 0, 1, 1, x, y, x + 120, y + 40,
				1, t);
	}

}
 public class SkillIndicator extends Entity{
	private String message;
	private float elapsedTime;
	private boolean extended;
	private int vx;
	private int x0;
	private int xt;
	private boolean left;
	
	public static final int MSG_TIME = 1;
	public static final int WIDTH = 50;
	public static final int HEIGHT = 12;
	
	public SkillIndicator(String message, boolean left, int position){
		super(0,29 + position*(HEIGHT+2));
		this.message = message;
		elapsedTime = 0;
		extended = false;
		vx = left? 800: -800;
		x = left? -WIDTH: FightStage.CENTRAL_AXIS*2;
		xt = left? 0: FightStage.CENTRAL_AXIS*2-WIDTH;
		x0 = (int) x;
		this.left = left;
		
		renderDepth = FightStage.HUD_DEPTH;
	}
	
	public void onStep(){
		if(extended){
			elapsedTime += Game.getDeltaSeconds();
			if(elapsedTime > MSG_TIME){
				extended = false;
			}
		} else if(elapsedTime == 0){
			x += vx * Game.getDeltaSeconds();
			if(Math.abs(x - x0)+3 >= WIDTH){
				extended = true;
				x = xt;
			}
		} else {
			x -= vx * Game.getDeltaSeconds();
			if(Math.abs(x - x0) <= 0){
				destroy();
			}
		}
	}
	
	public void render(){
		if(left){
			Renderer.drawRectangle(x-10, y, x+WIDTH, y + HEIGHT, 0, FightStage.NEUTRAL);
		} else {
			Renderer.drawRectangle(x, y, x+WIDTH+10, y + HEIGHT, 0, FightStage.NEUTRAL);
		}
		Renderer.drawString("msg_text", message, x+4, y+2, 0.0f);
	}
} public class UnitIdentifier implements Serializable{
	private static final long serialVersionUID = 1L;
	public String name;
	public org.newdawn.slick.Color partyColor;
	
	public UnitIdentifier(Unit u){
		name = u.name;
		partyColor = u.getPartyColor();
	}
	
	public int hashCode(){
		return name.hashCode() + partyColor.hashCode();
	}
	
	public boolean equals(Object o){
		if(!(o instanceof UnitIdentifier)) return false;
		UnitIdentifier other = (UnitIdentifier) o;
		return name.equals(other.name) && partyColor.equals(other.partyColor);
	}
	
	public String toString(){
		return name;
	}
}
 public abstract class Game {
	
	protected static int windowWidth = 640;
	protected static int windowHeight = 480;
	protected static int scaleX = 2;
	protected static int scaleY = 2;
	protected boolean paused = false;
	protected static List<KeyboardEvent> keys;
	protected static List<MouseEvent> mouseEvents;
	protected static CopyOnWriteArrayList<Message> messages;
	protected long time;
	protected static long timeDelta;
	protected static boolean glContextExists;
	
	public void init(int width, int height, String name) {
		time = System.nanoTime();
		
		windowWidth = width*scaleX;
		windowHeight = height*scaleY;

		try {
			Display.setDisplayMode(new DisplayMode(windowWidth, windowHeight));
			Display.create();
			Display.setTitle(name);
			Keyboard.create();
			Keyboard.enableRepeatEvents(true);
			Mouse.create();
			glContextExists = true;
		} catch (LWJGLException e) {
			e.printStackTrace();
			System.exit(0);
		}
		
		//init OpenGL
		glEnable(GL_DEPTH_TEST);
		glDepthFunc(GL_LEQUAL);
		glEnable(GL_ALPHA_TEST);
		glAlphaFunc(GL_GREATER, 0.01f);
		glEnable(GL_TEXTURE_2D);
		glShadeModel(GL_SMOOTH);
		glClearDepth(1.0f);
		glClearColor(0.0f, 0.0f, 0.0f, 0.0f);
		glEnable(GL_BLEND);
		glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA);
		glViewport(0, 0, windowWidth, windowHeight);
		glMatrixMode(GL_PROJECTION);
		glLoadIdentity();
		glOrtho(0, windowWidth/scaleX, windowHeight/scaleY, 0, 1, -1);		//It's basically a camera
		glMatrixMode(GL_MODELVIEW);
		
		keys = new ArrayList<KeyboardEvent>();
		mouseEvents = new ArrayList<MouseEvent>();
	}
	
	public abstract void loop();


	public static void getInput() {
		Keyboard.poll();
		keys.clear();
		while(Keyboard.next()) {
			KeyboardEvent ke = new KeyboardEvent(
					Keyboard.getEventKey(),
					Keyboard.getEventCharacter(),
					Keyboard.isRepeatEvent(),
					Keyboard.getEventKeyState());
			keys.add(ke);
		}
		Mouse.poll();
		mouseEvents.clear();
		while(Mouse.next()) {
			MouseEvent me = new MouseEvent(
					Mouse.getEventX(),
					Mouse.getEventY(),
					Mouse.getEventDWheel(),
					Mouse.getEventButton(),
					Mouse.getEventButtonState());
			mouseEvents.add(me);
		}
	}
	
	public static List<KeyboardEvent> getKeys() {
		return keys;
	}
	
	public static List<MouseEvent> getMouseEvents() {
		return mouseEvents;
	}
	
	public static List<Message> getMessages() {
		return messages;
	}

	public static long getDelta() {
		return timeDelta;
	}
	
	public static float getDeltaMillis() {
		return timeDelta/1000000.0f;
	}
	
	public static float getDeltaSeconds() {
		return timeDelta/1000000000.0f;
	}
	
	public static int getWindowWidth() {
		return windowWidth;
	}
	
	public static int getWindowHeight() {
		return windowHeight;
	}

	public static boolean glContextExists() {
		return glContextExists;
	}
	
	public static int getScaleX() {
		return scaleX;
	}
	
	public static int getScaleY() {
		return scaleY;
	}
} public class AudioPlayer {

	static Camera camera;
	static float globalGain;

	public static void setCamera(Camera c) {
		camera = c;
		globalGain = 0f;
	}

	public static void playAudio(String name, float pitch, float gain, float x,
			float y, float z, float fade) {
		Audio audio = FEResources.getAudio(name);
		float cx, cy;
		if(camera == null) {
			cx = Game.getWindowWidth()/2;
			cy = Game.getWindowHeight()/2;
		} else {
			cx = camera.getX();
			cy = camera.getY();
		}
		audio.playAsSoundEffect(pitch, gain + globalGain, false, 
				(x - cx) / fade, (y - cy) / fade, z);
	}
	
	public static void playAudio(String name, float pitch, float gain) {
		Audio audio = FEResources.getAudio(name);
		audio.playAsSoundEffect(pitch, gain + globalGain, false);
	}
} public class Renderer {

	private static Camera camera;
	private static RectClip clip;
	private static final int SCALE_FILTER = GL_NEAREST;
	private static Color color;
	private static Stack<RendererState> stateStack;
	private static HashMap<String, Integer> programs;

	static {
		System.out.println(GL11.glGetString(GL20.GL_SHADING_LANGUAGE_VERSION));
		System.out.println(GL11.glGetInteger(GL20.GL_MAX_COMBINED_TEXTURE_IMAGE_UNITS));
		stateStack = new Stack<RendererState>();
		programs = new HashMap<String, Integer>();
		camera = new Camera(null, 0, 0);
		clip = null;
		color = Color.white;
		programs.put("default", createProgram("default", "default"));
		programs.put("greyscale", createProgram("default", "greyscale"));
		programs.put("lighten", createProgram("default", "lighten"));
		programs.put("paletteSwap", createProgram("betterSwap", "betterSwap"));
	}

	/***
	 * Draws the given subtexture at the given coordinates.
	 * 
	 * @param t
	 *            Texture to be drawn
	 * @param tx0
	 *            First texture x coord
	 * @param ty0
	 *            First texture y coord
	 * @param tx1
	 *            Second texture x coord
	 * @param ty1
	 *            Second texture y coord
	 * @param x0
	 *            First render x coord
	 * @param y0
	 *            First render y coord
	 * @param x1
	 *            Second render x coord
	 * @param y1
	 *            Second render y coord
	 */
	public static void render(Texture t, float tx0, float ty0, float tx1,
			float ty1, float x0, float y0, float x1, float y1, float depth) {
		render(t, tx0, ty0, tx1, ty1, x0, y0, x1, y1, depth, null, new ShaderArgs());
	}
	
	public static void render(Texture t, float tx0, float ty0, float tx1,
			float ty1, float x0, float y0, float x1, float y1, float depth, Transform transform) {
		render(t, tx0, ty0, tx1, ty1, x0, y0, x1, y1, depth, transform, new ShaderArgs());
	}

	public static void render(Texture t, float tx0, float ty0,
			float tx1, float ty1, float x0, float y0, float x1, float y1,
			float depth, Transform transform, ShaderArgs shader) {
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, SCALE_FILTER);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, SCALE_FILTER);
		int program = programs.get(shader.programName);
		ARBShaderObjects.glUseProgramObjectARB(program);
		shader.bindArgs(program);
		t.bind();
		glPushMatrix();
		if(transform != null) {
			Color c = transform.color.multiply(color);
			glColor4f(c.r, c.g, c.b, c.a);
			glTranslatef(x0, y0, depth);
			glTranslatef(transform.translateX, transform.translateY, 0);
			glScalef(transform.scaleX, transform.scaleY, 0);
			glTranslatef(-x0 + (x0 + x1) / 2, -y0 + (y0 + y1) / 2, 0);
			glRotatef(transform.rotation / (float) Math.PI * 180, 0, 0, 1);
			glTranslatef(-(x0 + x1) / 2, -(y0 + y1) / 2, -depth);
	
			// do flip operations
			if (transform.flipHorizontal) {
				float temp = tx0;
				tx0 = tx1;
				tx1 = temp;
			}
	
			if (transform.flipVertical) {
				float temp = ty0;
				ty0 = ty1;
				ty1 = temp;
			}
		} else {
			glColor4f(color.r, color.g, color.b, color.a);
		}
		// Compensation for non power of 2 images
		float txi = tx0*t.getImageWidth()/t.getTextureWidth();
		float tyi = ty0*t.getImageHeight()/t.getTextureHeight();
		float txf = tx1*t.getImageWidth()/t.getTextureWidth();
		float tyf = ty1*t.getImageHeight()/t.getTextureHeight();

		// draw quad
		glBegin(GL_QUADS);
		glTexCoord2f(txi, tyi);
		glVertex3f(x0, y0, depth);
		glTexCoord2f(txf, tyi);
		glVertex3f(x1, y0, depth);
		glTexCoord2f(txf, tyf);
		glVertex3f(x1, y1, depth);
		glTexCoord2f(txi, tyf);
		glVertex3f(x0, y1, depth);
		glEnd();

		glPopMatrix();
		if(clip != null && !clip.persistent) clip.destroy();
		ARBShaderObjects.glUseProgramObjectARB(0);
	}

	public static void drawSquare(float x, float y, float s, float depth,
			Color c) {
		drawRectangle(x, y, x + s, y + s, depth, c);
	}

	public static void drawRectangle(float x0, float y0, float x1, float y1,
			float depth, Color c) {
		drawRectangle(x0, y0, x1, y1, depth, c, c, c, c);
	}
	
	public static void drawBorderedRectangle(float x0, float y0, float x1, float y1, float depth, Color... c){
		for(int i = c.length - 1; i >= 0; i--){
			drawRectangle(x0 - i, y0 - i, x1 + i, y1 + i, depth, c[i]);
		}
	}

	public static void drawRectangle(float x0, float y0, float x1, float y1,
			float depth, Color c0, Color c1, Color c2, Color c3) {
		glDisable(GL_TEXTURE_2D);
		glBegin(GL_QUADS);
		c0 = c0.multiply(color);
		c1 = c1.multiply(color);
		c2 = c2.multiply(color);
		c3 = c3.multiply(color);
		glColor4f(c0.r, c0.g, c0.b, c0.a);
		glVertex3f(x0, y0, depth);
		glColor4f(c1.r, c1.g, c1.b, c1.a);
		glVertex3f(x1, y0, depth);
		glColor4f(c2.r, c2.g, c2.b, c2.a);
		glVertex3f(x1, y1, depth);
		glColor4f(c3.r, c3.g, c3.b, c3.a);
		glVertex3f(x0, y1, depth);
		glEnd();
		glEnable(GL_TEXTURE_2D);
		if(clip != null && !clip.persistent) clip.destroy();
	}

	public static void drawLine(float x0, float y0, float x, float y,
			float width, float depth, Color c1, Color c2) {
		glDisable(GL_TEXTURE_2D);
		glLineWidth(width);

		// glLoadIdentity();
		glBegin(GL_LINES);
		c1 = c1.multiply(color);
		c2 = c2.multiply(color);
		glColor4f(c1.r, c1.g, c1.b, c1.a);
		glVertex3f(x0, y0, depth);
		glColor4f(c2.r, c2.g, c2.b, c2.a);
		glVertex3f(x, y, depth);
		glEnd();
		glEnable(GL_TEXTURE_2D);
		if(clip != null && !clip.persistent) clip.destroy();
	}

	public static void drawTriangle(float x0, float y0, float x, float y,
			float x2, float y2, float depth, Color c) {
		c.bind();
		glDisable(GL_TEXTURE_2D);
		c = c.multiply(color);
		glColor4f(c.r, c.g, c.b, c.a);
		glBegin(GL_TRIANGLES);
		glVertex3f(x0, y0, depth);
		glVertex3f(x, y, depth);
		glVertex3f(x2, y2, depth);
		glEnd();
		glEnable(GL_TEXTURE_2D);
		if(clip != null && !clip.persistent) clip.destroy();
	}
	
	public static void drawString(String fontName, int num, float x, float y, float depth) {
		drawString(fontName, num+"", x, y, depth);
	}
	
	public static void drawString(String fontName, String string, float x, float y, float depth) {
		drawString(fontName, string, x, y, depth, null);
	}
	
	public static void drawString(String fontName, String string, float x, float y, float depth, Transform t) {
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, SCALE_FILTER);
		glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, SCALE_FILTER);
		FEResources.getBitmapFont(fontName).render(string, x, y, depth, t);
		if(clip != null && !clip.persistent) clip.destroy();
	}
	
	public static void translate(float x, float y) {
		glTranslatef(x, y, 0);
	}
	
	public static void scale(float x, float y) {
		glScalef(x, y, 0);
	}
	
	public static void setColor(Color c) {
		if(c == null) color = Color.white;
		else color = c;
	}
	
	public static void pushMatrix() {
		glPushMatrix();
		RendererState state = new RendererState();
		state.color = color;
		state.clip = clip;
		stateStack.push(state);
	}
	
	public static void popMatrix() {
		glPopMatrix();
		RendererState state = stateStack.pop();
		color = state.color;
		clip = state.clip;
		stateStack.push(state);
	}
	
	public static void setCamera(Camera c) {
		camera = c;
	}

	public static Camera getCamera() {
		return camera;
	}
    
    public static void addClip(float f, float g, float h, float i, boolean persistent) {
    	clip = new RectClip(f, g, h, i, persistent);
    }
    
    public static void removeClip() {
    	if(clip != null) clip.destroy();
    }
    
    /*
    * With the exception of syntax, setting up vertex and fragment shaders
    * is the same.
    * @param the name and path to the vertex shader
    */
    private static int createShader(String filename, int shaderType) throws Exception {
    	int shader = 0;
    	try {
	        shader = ARBShaderObjects.glCreateShaderObjectARB(shaderType);
	        
	        if(shader == 0)
	        	return 0;
	        
	        ARBShaderObjects.glShaderSourceARB(shader, readFileAsString(filename));
	        ARBShaderObjects.glCompileShaderARB(shader);
	        
	        if (ARBShaderObjects.glGetObjectParameteriARB(shader, ARBShaderObjects.GL_OBJECT_COMPILE_STATUS_ARB) == GL_FALSE)
	            throw new RuntimeException("Error creating shader: " + getLogInfo(shader));
	        
	        return shader;
    	}
    	catch(Exception exc) {
    		ARBShaderObjects.glDeleteObjectARB(shader);
    		throw exc;
    	}
    }
    
    private static int createProgram(String vertShader, String fragShader) {
    	// Create program object
    	int prog = ARBShaderObjects.glCreateProgramObjectARB();
    	// Create vertex shader
    	try {
			int vertexShader = createShader("shaders/"+vertShader+".vert",ARBVertexShader.GL_VERTEX_SHADER_ARB);
			if(vertexShader == 0) throw new Exception();
			ARBShaderObjects.glAttachObjectARB(prog, vertexShader);
		} catch (Exception e) {
			System.err.println("Unable to create vertex shader: "+vertShader);
			e.printStackTrace();
		}
    	// Create fragment shader
    	try {
			int fragmentShader = createShader("shaders/"+fragShader+".frag",ARBFragmentShader.GL_FRAGMENT_SHADER_ARB);
			if(fragmentShader == 0) throw new Exception();
			ARBShaderObjects.glAttachObjectARB(prog, fragmentShader);
		} catch (Exception e) {
			System.err.println("Unable to create fragment shader: "+fragShader);
			e.printStackTrace();
		}
    	// Link program
    	ARBShaderObjects.glLinkProgramARB(prog);
	    if (ARBShaderObjects.glGetObjectParameteriARB(prog, ARBShaderObjects.GL_OBJECT_LINK_STATUS_ARB) == GL_FALSE) {
	        System.err.println(getLogInfo(prog));
	        System.out.println("crap");
	        return -1;
	    }
	    
	    // Use program
	    ARBShaderObjects.glUseProgramObjectARB(prog);
        
	    // Validate program
	    ARBShaderObjects.glValidateProgramARB(prog);
	    if (ARBShaderObjects.glGetObjectParameteriARB(prog, ARBShaderObjects.GL_OBJECT_VALIDATE_STATUS_ARB) == GL_FALSE) {
	    	System.err.println(getLogInfo(prog));
	    	System.out.println("shit");
	    	return -1;
	    }
	    // Bind texture units to uniforms
	    int loc = GL20.glGetUniformLocation(prog, "texture1");
        GL20.glUniform1i(loc, 0);
        int loc2 = GL20.glGetUniformLocation(prog, "texture2");
        GL20.glUniform1i(loc2, 8);
	    
    	return prog;
    }

	private static String getLogInfo(int obj) {
        return ARBShaderObjects.glGetInfoLogARB(obj, 
        		ARBShaderObjects.glGetObjectParameteriARB(obj,
        				ARBShaderObjects.GL_OBJECT_INFO_LOG_LENGTH_ARB));
    }
	
    /**
     * I copied this method from the lwjgl website but this code makes me want
     * to kill myself. I might fix later
     * @param filename
     * @return
     * @throws Exception
     */
    private static String readFileAsString(String filename) throws Exception {
        StringBuilder source = new StringBuilder();
        InputStream in = ResourceLoader.getResourceAsStream(filename);
        Exception exception = null;
        BufferedReader reader;
        try{
            reader = new BufferedReader(new InputStreamReader(in,"UTF-8"));
            
            Exception innerExc= null;
            try {
            	String line;
                while((line = reader.readLine()) != null)
                    source.append(line).append('\n');
            }
            catch(Exception exc) {
            	exception = exc;
            }
            finally {
            	try {
            		reader.close();
            	}
            	catch(Exception exc) {
            		if(innerExc == null)
            			innerExc = exc;
            		else
            			exc.printStackTrace();
            	}
            }
            
            if(innerExc != null)
            	throw innerExc;
        }
        catch(Exception exc) {
        	exception = exc;
        }
        finally {
        	try {
        		in.close();
        	}
        	catch(Exception exc) {
        		if(exception == null)
        			exception = exc;
        		else
					exc.printStackTrace();
        	}
        	
        	if(exception != null)
        		throw exception;
        }
        
        return source.toString();
    }
    
    public static void addProgram(String name, String vertShader, String fragShader) {
    	programs.put(name, createProgram(vertShader, fragShader));
    }
    
    static class RectClip {
    	boolean persistent;
    	public RectClip(float x0, float y0, float w, float h, boolean p) {
    		persistent = p;
    		glEnable(GL_SCISSOR_TEST);
    		glScissor(
    				Game.getScaleX()*(int)x0, 
    				(int)(Game.getWindowHeight()-Game.getScaleY()*(y0+h)), 
    				Game.getScaleX()*(int)w, 
    				Game.getScaleY()*(int)h);
    	}
    	
    	public void destroy() {
    		glDisable(GL_SCISSOR_TEST);
    	}
    }

    static class RendererState {
		Color color;
		RectClip clip;
	}
    
}
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.PriorityQueue;
import net.fe.FEMultiplayer;
import net.fe.FEResources;
import net.fe.fightStage.anim.AnimationArgs;
import net.fe.fightStage.anim.AttackAnimation;
import net.fe.fightStage.anim.BackgroundEffect;
import net.fe.fightStage.anim.DodgeAnimation;
import net.fe.fightStage.anim.HUD;
import net.fe.fightStage.anim.HitEffect;
import net.fe.fightStage.anim.MagicAttack;
import net.fe.fightStage.anim.MagicEffect;
import net.fe.fightStage.anim.MissEffect;
import net.fe.fightStage.anim.NoDamageEffect;
import net.fe.fightStage.anim.Platform;
import net.fe.fightStage.anim.SkillIndicator;
import net.fe.overworldStage.Grid;
import net.fe.transition.FightOverworldTransition;
import net.fe.unit.Unit;
import net.fe.unit.UnitIdentifier;
import org.newdawn.slick.Color;
import org.newdawn.slick.opengl.Texture;
import chu.engine.Entity;
import chu.engine.Game;
import chu.engine.SortByRender;
import chu.engine.Stage;
import chu.engine.anim.AudioPlayer;
import chu.engine.anim.Renderer;

package net.fe.fightStage;





public class FightStage extends Stage {
	private Unit left, right;
	private FightUnit leftFighter, rightFighter;
	private Healthbar leftHP, rightHP;
	private int range;
	private boolean done;

	private ArrayList<AttackRecord> attackQ;

	private Texture bg;
	private int currentEvent;
	private float prevShakeTimer;
	private float shakeTimer;
	private float timer;
	private float shakeX;
	private float shakeY;
	private float cameraOffsetF;
	private float cameraOffsetT;
	private float cameraOffset;
	
	private float darknessT, darkness;
	
	public static final HashMap<String, Texture> PRELOADED_EFFECTS = new HashMap<String, Texture>();

	// Config
	public static final float SHAKE_INTERVAL = 0.05f;
	public static final float MIN_TIME = 1.5f;

	public static final int CENTRAL_AXIS = 120;
	public static final int FLOOR = 105;

	public static final Color BORDER_DARK = new Color(0x483828);
	public static final Color BORDER_LIGHT = new Color(0xf8f0c8);
	public static final Color NEUTRAL = new Color(0xb0a878);

	public static final float HP_DEPTH = 0.14f;
	public static final float HUD_DEPTH = 0.15f;
	public static final float EFFECT_DEPTH = 0.2f;
	public static final float UNIT_DEPTH = 0.5f;
	public static final float PLATFORM_DEPTH = 0.7f;
	public static final float BG_DEPTH = 1;

	public static final int START = 0;
	public static final int ATTACKING = 1;
	public static final int HIT_EFFECT = 2;
	public static final int HIT_EFFECTED = 3;
	public static final int ATTACKED = 4;
	public static final int HURTING = 5;
	public static final int HURTED = 6;
	public static final int DYING = 7;
	public static final int RETURNING = 8;
	public static final int DONE = 9;

	public FightStage(UnitIdentifier u1, UnitIdentifier u2,
			ArrayList<AttackRecord> attackQ) {
		super(u1.partyColor.equals(u2.partyColor) ? "curing" :
				u1.partyColor.equals(FEMultiplayer.getLocalPlayer().getParty().getColor()) ?
						"fight" : "defense");
		shakeTimer = 0;
		prevShakeTimer = 0;
		timer = 0;
		done = false;
		shakeX = 0;
		shakeY = 0;
		left = FEMultiplayer.getUnit(u1);
		right = FEMultiplayer.getUnit(u2);
		
//		System.out.println(left);
//		System.out.println(right);

		range = Grid.getDistance(left, right);
		cameraOffsetF = rangeToHeadDistance(range) - rangeToHeadDistance(1);
		leftFighter = new FightUnit(new AnimationArgs(left, true, range), this);
		rightFighter = new FightUnit(new AnimationArgs(right, false, range),
				this);
		leftHP = new Healthbar(left, true);
		rightHP = new Healthbar(right, false);
		addEntity(leftFighter);
		addEntity(rightFighter);
		addEntity(leftHP);
		addEntity(rightHP);
		addEntity(new Platform(left.getTerrain(), true, range));
		addEntity(new Platform(right.getTerrain(), false, range));
		addEntity(new HUD(left, right, this));
		addEntity(new HUD(right, left, this));

		bg = FEResources.getTexture(right.getTerrain().toString().toLowerCase()
