package com.zarkonnen.airships;

import static com.zarkonnen.airships.Appearance.app;
import com.zarkonnen.airships.Particle.Type;
import com.zarkonnen.catengine.Draw;
import com.zarkonnen.catengine.Img;
import com.zarkonnen.catengine.util.Clr;
import com.zarkonnen.catengine.util.Pt;
import com.zarkonnen.catengine.util.Utils;
import static com.zarkonnen.catengine.util.Utils.*;
import com.zarkonnen.catengine.util.Utils.Pair;
import java.util.ArrayList;
import java.util.EnumSet;

public strictfp enum ModuleType {
	CORRIDOR(b("Corridor", 1, 1, 32, 15, 350, 4, app().frame(6, 0)).windows(0, 0).cost(1).upDoors(true)
			.desc("Allows crew to move through the ship rapidly.")),
	DECK(b("Deck", 1, 1, 40, 10, 200, 1,
			app().frame(26, 5).frame(27, 5).frame(28, 5).frame(29, 5).frame(30, 5).frame(31, 5).frame(32, 5).frame(33, 5).interval(5000))
			.desc("Decks let your crew move at high speeds, but offer no protection.")
			.external()
			.dontDrawDoors()
			.framesAreVariants()
			.external(app().frame(26, 4).frame(27, 4).frame(28, 4).frame(29, 4).frame(30, 4).frame(31, 4).frame(32, 4).frame(33, 4).interval(5000))
	),
	GUARD_POST(b("Guard Post", 1, 1, 40, 20, 600, 5, app(34, 5))
		.desc("An air marine keeps watch here, deterring intruders.")
		.fixedGuards(1)),
	HATCH(b("Supply Hatch", 1, 1, 60, 20, 350, 8, app(34, 4))
		.external(app(35, 4))
		.external()
		.desc("A way in and out for supplies and people. You need at least one of these.")
		.hatch(8)),
	CARGO_DOOR(b("Cargo Door", 2, 2, 180, 50, 350, 20, app().frame(36, 4, 2, 2))
		.external(app().frame(38, 4, 2, 2))
		.canOccupy(0,0, 0,1, 1,1)
		.upDoors(true, false)
		.external()
		.desc("A large door for quickly re-supplying the ship and moving troops and crew.")
		.hatch(40)),
	SMALL_KEEL(b("Small Keel", 5, 1, 200, 60, 10000, 750, app().frame(6, 1, 5, 1)).availableFor(ShipType.AIRSHIP).shipHPBonus(300).cost(30).canOccupy(/* none */)
			.desc("A solid wooden keel to reinforce the ship.")),
	LARGE_KEEL(b("Large Keel", 9, 1, 600, 100, 10000, 1400, app().frame(11, 1, 9, 1)).availableFor(ShipType.AIRSHIP).shipHPBonus(1000).cost(80).canOccupy(/* none */)
			.desc("A massive wooden keel that adds a great deal of structural integrity.")),
	GRAND_KEEL(b("Grand Keel", 14, 2, 2000, 300, 10000, 4500, app().frame(6, 2, 14, 2)).availableFor(ShipType.AIRSHIP).shipHPBonus(4500).cost(300).canOccupy(/* none */).required(Bonus.GREAT_FORESTS)
			.desc("Crafted from the trunk of a giant tree, this Grand Keel provides a massive boost to the ship's structure.")),
	RAM(b("Ram", 2, 1, 1200, 0, 10000, 500, app().frame(26, 6, 2, 1))
			.desc("A bronze ramming bow built to tear holes into enemy ships while leaving your own unharmed.")
			.availableFor(ShipType.AIRSHIP).cost(80).canOccupy(/*none*/)
			.external().external(app().frame(26, 6, 2, 1))),
	GRAND_RAM(b("Grand Ram", 4, 2, 12000, 0, 10000, 3000, app().frame(28, 6, 4, 2))
			.desc("A huge bronze ram's head to smash your enemies.")
			.availableFor(ShipType.AIRSHIP).cost(300).canOccupy(/*none*/)
			.required(Bonus.RAMMING_PROWS)
			.external().external(app().frame(28, 6, 4, 2))),
	COAL_STORE(b("Coal store", 3, 2, 240, 450, 450, 80, app().frame(0, 1, 3, 2)).coal(40)
			.canOccupy(0,1, 1,1, 2,1, 2,0).optionalCrew(3).upDoors(false, false, true)
			.desc("Coal is needed to power suspendium chambers and propellers. Unfortunately, it burns easily.")),
	SMALL_COAL_STORE(b("Small coal store", 2, 1, 80, 80, 600, 30, app().frame(3, 2, 2, 1)).coal(10)
			.optionalCrew(1)
			.desc("A small coal storage room.")),
	AMMO(b("Ammo store", 3, 2, 240, 150, 600, 100, app().frame(0, 3, 3, 2)).ammo(50).explode(100, 500)
			.canOccupy(0,1, 1,1, 1,0, 2,1, 2,0).optionalCrew(3).cost(40).upDoors(false, true, true)
			.recommendedGuards(1)
			.desc("Ammunition for the ship's weapons is stored here. Just make sure it doesn't catch fire!")),
	SMALL_AMMO(b("Small ammo store", 2, 1, 80, 45, 600, 35, app().frame(3, 4, 2, 1)).ammo(12).explode(20, 150)
			.optionalCrew(1).cost(15)
			.recommendedGuards(1)
			.desc("A small ammunition closet.")),
	SICKBAY(b("Sickbay", 3, 1, 120, 50, 600, 40, app().frame(7, 0, 3, 1)).sickbay(3).windows(0, 0, 1, 0, 2, 0).crew(1).optionalCrew(2).recommendedCrew(2).cost(20).maintenanceCost(1)
			.desc("The sickbay can restore crew members to full fighting shape again, whether they drag themselves here under their own power or are carried by a comrade.")),
	REPAIR_BAY(b("Repair bay", 3, 1, 160, 90, 600, 60, app().frame(10, 0, 3, 1)).repair(50).windows(0, 0).optionalCrew(3).recommendedCrew(2).cost(30)
			.desc("Filled with tools, replacement parts, and sturdy sections of wood and pipe.")),
	MACHINE_SHOP(b("Machine shop", 4, 3, 640, 360, 800, 250, app().frame(22, 4, 4, 3)).repair(400).windows(2, 0, 3, 1, 1, 2).optionalCrew(5).recommendedCrew(4)
			.cost(200)
			.upDoors(true, true, true, true)
			.leftDoors(true, true, true).rightDoors(true, true, true)
			.desc("The machine shop has a near-exhaustible supply of parts, carefully organized.")),
	FIRE_POINT(b("Fire point", 2, 2, 160, 15, 600, 200, app().frame(0, 5, 2, 2)).water(100).windows(1, 1)
			.canOccupy(0,1, 1,1).optionalCrew(2).recommendedCrew(2).cost(25)
			.desc("Two massive water tanks provide the means to extinguish fires.")),
	FIRE_EXTINGUISHER(b("Fire extinguisher", 1, 1, 32, 5, 400, 12, app().frame(2, 5, 1, 1)).water(8).optionalCrew(1).cost(10).upDoors(true).required(Bonus.FIRE_EXTINGUISHERS)
			.desc("A small tank of pressurized water to quickly put out nearby fires.")),
	FIRE_DOOR(b("Fire door", 1, 1, 50, 0, 500, 20, app().frame(2, 6)).windows(0, 0).cost(5).upDoors(true)
			.desc("A reinforced fire door to slow the spread of fire down the ship's corridors. It also slows crew down a little.")),
	QUARTERS(b("Quarters", 3, 1, 100, 70, 600, 40, app().frame(13, 0, 3, 1))
			.quarters(12, CrewType.SAILOR)
			.windows(2, 0)
			.cost(40)
			.maintenanceCost(8)
			.desc("Enough hammocks to sleep twelve airsailors.")),
	BERTH(b("Berth", 1, 1, 40, 20, 600, 15, app().frame(5, 2, 1, 1))
			.quarters(3, CrewType.SAILOR)
			.windows(0, 0)
			.cost(15)
			.maintenanceCost(3)
			.upDoors(true)
			.desc("A cramped room with three hammocks for crew.")),
	BARRACKS(b("Barracks", 3, 2, 200, 140, 600, 45, app().frame(12, 7, 3, 2))
			.availableFor(ShipType.AIRSHIP)
			.quarters(8, CrewType.MARINE)
			.windows(0, 1)
			.cost(120)
			.maintenanceCost(4)
			.upDoors(false, false, true)
			.leftDoors(true, true).rightDoors(true, true)
			.canOccupy(0,0, 1,0, 2,0, 0,1, 1,1, 2,1)
			.desc("Houses eight air marines ready to board enemy ships or defend your own.")),
	GRAPPLER_BARRACKS(b("Grenadier Barracks", 3, 2, 200, 140, 600, 45, app().frame(17, 7, 3, 2))
			.required(Bonus.GRAPPLING_HOOKS)
			.availableFor(ShipType.AIRSHIP)
			.quarters(4, CrewType.GRENADIER)
			.windows(0, 1)
			.cost(150)
			.maintenanceCost(4)
			.upDoors(false, false, true)
			.leftDoors(true, true).rightDoors(true, true)
			.canOccupy(0,0, 1,0, 2,0, 0,1, 1,1, 2,1)
			.desc("Houses four elite grenadiers with grappling hooks for rapidly boarding enemy ships.")),
	GUARD_BARRACKS(b("Guard Barracks", 3, 1, 100, 70, 600, 30, app().frame(15, 10, 3, 1))
			.availableFor(ShipType.BUILDING)
			.quarters(3, CrewType.GUARD)
			.windows(0, 0)
			.cost(40)
			.maintenanceCost(1)
			.upDoors(false, false, true)
			.desc("Quarters for three guards to protect the building .")),
	BRIDGE(b("Bridge", 3, 1, 200, 60, 600, 50, app().frame(16, 0, 3, 1)).command(5).windows(0, 0, 1, 0, 2, 0).crew(3)
			.recommendedGuards(3)
			.desc("The ship's command center. Without a working bridge or cockpit, you cannot give orders in battle or see the inside of the ship.")),
	COCKPIT(b("Cockpit", 1, 1, 80, 20, 600, 20, app().frame(3, 3, 1, 1).frame(4, 3, 1, 1).frame(5, 3, 1, 1).interval(3500)).command(1).windows(0, 0).crew(1)
			.recommendedGuards(1)
			.desc("Everything you need to steer a small airship. Without a working bridge or cockpit, you cannot give orders in battle or see the inside of the ship.")),
	TELESCOPE(b("Telescope", 1, 1, 30, 25, 600, 8, app().frame(31, 0, 2, 1)).windows(0, 0).cost(200).crew(1).recommendedCrew(1).accuracyBonus(0.3).frontOnly().required(Bonus.TELESCOPES)
			.desc("Equipped with a telescope, a spotter can direct the fire of nearby weapons.")),
	TARGETING_COMPUTER(b("Targeting Computer", 3, 3, 250, 150, 600, 500, app().frame(7, 4, 3, 3).frame(10, 4, 3, 3).frame(13, 4, 3, 3).frame(16, 4, 3, 3).interval(300))
			.recommendedGuards(1)
			.emit(new ModuleParticleEmitter(0.4, 1.38, Type.SMOKE, .012))
			.external(app().frame(5, 8, 1, 3))
			.canOccupy(0,2, 1,2, 2,2).windows(0, 0)
			.cost(800).crew(3).coalReload(20000).recommendedCrew(4).accuracyBonus(0.3).required(Bonus.COMPUTERS)
			.desc("A miracle of technology, this valve-based targeting computer can calculate accurate firing solutions within minutes!")),
	SUSPENDIUM_CHAMBER(b("Suspendium chamber", 3, 2, 200, 120, 800, 120,
			app().frame(0, 7, 3, 2).frame(6, 7, 3, 2).frame(9, 7, 3, 2).interval(150))
			.availableFor(ShipType.AIRSHIP)
			.lift(2200).coalReload(20000).crew(1)
			.external(app().frame(3, 7, 3, 2))
			.emit(new ModuleParticleEmitter(0.4, 0.38, Type.SMOKE, .012))
			.canOccupy(0, 1, 1, 1, 2, 1).cost(120)
			.recommendedCrew(2)
			.desc("A large crystal of Suspendium is kept chained and charged in this chamber, keeping the ship aloft.")),
	SMALL_SUSPENDIUM_CHAMBER(b("Small suspendium chamber", 1, 2, 60, 35, 600, 50,
			app().frame(3, 5, 1, 2).frame(4, 5, 1, 2).frame(5, 5, 1, 2).interval(150))
			.availableFor(ShipType.AIRSHIP)
			.lift(650).coalReload(40000).crew(1)
			.external(app().frame(6, 5, 1, 2))
			.emit(new ModuleParticleEmitter(0.25, 0.9, Type.SMOKE, .005))
			.canOccupy(0, 1).cost(40)
			.recommendedCrew(1)
			.desc("A modest crystal of Suspendium is kept chained and charged in this chamber, keeping the ship aloft.")),
	SUSPENDIUM_DUST_TANK(b("Suspendium Dust Tank", 2, 3, 50, 0, 10000, 50, app().frame(15, 7, 2, 3))
			.external().external(app().frame(15, 7, 2, 3))
			.availableFor(ShipType.AIRSHIP)
			.canOccupy(/*none*/)
			.explode(10, 25)
			.lift(250)
			.cost(15)
			.desc("A large tank filled with charged suspendium dust - a simple and low-maintenance way to keep an airship aloft.")
	),
	SAIL(b("Sail", 2, 2, 80, 60, 800, 120, app().frame(32, 6, 2, 2))
			.desc("A simple but labour-intensive way of moving your ship around.")
			.availableFor(ShipType.AIRSHIP)
			.crew(3).recommendedCrew(4)
			.propulsion(0.05)
			.canOccupy(0,0, 0,1, 1,1).windows(1,1).upDoors(true, false)
			.external(app().frame(34, 6, 3, 4)).externalOffset(-1, -1, true)),
	PROPELLER(b("Propeller", 2, 2, 160, 110, 800, 80,
			app().frame(0, 9, 3, 2).frame(9, 9, 3, 2).frame(6, 9, 3, 2).frame(12, 9, 3, 2).interval(120))
			.availableFor(ShipType.AIRSHIP)
			.recommendedGuards(1)
			.propulsion(0.55).coalReload(30000).backOnly().crew(1)
			.external(app().frame(3, 9, 3, 2))
			.emit(new ModuleParticleEmitter(1.2, 0.38, Type.SMOKE, 0.005))
			.canOccupy(0,1, 1,1)
			.recommendedCrew(2)
			.cost(60)
			.desc("A steam-powered propeller, used to move the ship forwards or back.")),
	FLIPPED_PROPELLER(),
	SMALL_PROPELLER(b("Small Propeller", 1, 1, 40, 25, 600, 25,
			app().frame(5, 11, 2, 1).frame(7, 11, 2, 1).frame(9, 11, 2, 1).frame(11, 11, 2, 1).interval(120))
			.availableFor(ShipType.AIRSHIP)
			.propulsion(0.11).coalReload(100000).backOnly().crew(1)
			.external(app().frame(5, 12, 2, 1))
			.emit(new ModuleParticleEmitter(1.6, 0.1, Type.SMOKE, 0.002))
			.recommendedCrew(1)
			.cost(30)
			.desc("A little steam propeller able to shift smaller ships.")),
	FLIPPED_SMALL_PROPELLER(),
	RIFLE(b("Rifle", 1, 1, 40, 18, 800, 8, app(19, 0)).weapon(
			/* reload */ 1000,
			/* clip */   6,
			/* jitter */ 0.25 / 300,
			/* blastD */ 0,
			/* penD */   6,
			/* fireArc */Arc.centeredDegrees(240),
			/* mzCX */   0.68,
			/* mzCY */   0.56,
			/* mzL  */   1.0,
			/* fireSnd */"rifle",
			/* sndCnt */ 2,
			/* hitSnd */ null,
			/* optRange */ 800,
			new WeaponAppearance()
					.shot(new Img("spritesheet", 48, 304, 1, 1, false))
					.barrel(new Img("spritesheet", 321, 112, 23, 6, false), new Pt(0, 6), 1)
					.recoil(2)
			)
			.crew(1).recommendedCrew(1).cost(10).isGun()
			.jitterMerge(0.4)
			.desc("Riflemen are accurate and cheap, but their weapons cannot penetrate heavy armour.")),
	FLIPPED_RIFLE(),
	GRENADES(b("Grenades", 1, 1, 32, 32, 800, 12, app(24, 0)).windows(0,0).weapon(
			/* reload */   5000,
			/* clip */     3,
			/* jitter */   2.5 / 300,
			/* blastD */   30,
			/* penD */     0,
			/* fireArc*/   Arc.centeredDegrees(240),
			/* muzzleCx */ 0.8,
			/* muzzleCy */ 0.4,
			/* mzLength */ 0,
			/* fireSnd */  null,
			/* fireSndC */ 0,
			/* hitSnd */   "smexplosion",
			/* optRange */ 80,
			new WeaponAppearance()
				.shot(new Img("spritesheet", 64, 304, 2, 2, false))
				.shotEmitter(new Particle.Emitter(Type.TRAILING_SMOKE, 0.007))
			).crew(1).recommendedCrew(2).explode(15, 15).maxRange(150, 30).isExplosive()
			.shotSpeed(0.2)
			.desc("Grenades can do a massive amount of damage at close range, especially to wooden hulls.").cost(15)),
	FLIPPED_GRENADES(),
	ROCKETS(b("Rockets", 2, 2, 80, 100, 800, 40, app().frame(25, 0, 2, 2)).weapon(
			/* reload */   2000,
			/* clip */     1,
			/* jitter */   2.7 / 300,
			/* blastD */   65,
			/* penD */     0,
			/* fireA */    Arc.centeredDegrees(180),
			/* muzzCX */   1.56,
			/* muzzCY */   1.31,
			/* muzzLen */  0,
			/* fireSnd */  "rocket",
			/* sndCnt */   6,
			/* hitSnd */   "smexplosion",
			/* optRange */ 100,
			new WeaponAppearance()
				.shot(new Img("spritesheet", 80, 305, 30, 5, false))
				.barrel(new Img("spritesheet", 80, 305, 30, 5, false), new Pt(10, 18.5), 2)
				.shotEmitter(new Particle.Emitter(Type.TRAILING_SMOKE, 0.02))
			).explode(50, 60).canOccupy(0,0, 0,1, 1,1).windows(1,1).crew(2).recommendedCrew(3).upDoors(true, false).required(Bonus.ROCKETS).isExplosive()
			.jitterMerge(0.3)
			.shotSpeed(0.6)
			.desc("Not accurate, or safe, but pretty devastating when they hit.").cost(30)),
	FLIPPED_ROCKETS(),
	GATLING_GUN(b("Gatling gun", 2, 1, 40, 18, 800, 20, app().frame(20, 0, 2, 1))
			.weapon(
			/* reload */  170,
			/* clip */    40,
			/* jitter */  1.8 / 300,
			/* blastD */  0,
			/* penD */    6,
			/* farc */    Arc.centeredDegrees(120),
			/* mzCX */    1.55,
			/* mzCY */    0.56,
			/* mzL */     0.8,
			/* sound */   "gatling",
			/* nSounds */ 9,
			/* hitSnd */  null,
			/* optRange */100,
			new WeaponAppearance()
				.shot(new Img("spritesheet", 32, 304, 2, 1, false))
				.barrel(new Img("spritesheet", 353, 113, 24, 9, false), new Pt(13, 5), 2)
				.recoil(2)
			)
			.crew(1)
			.cost(50)
			.required(Bonus.GATLING_GUNS)
			.isGun()
			.soundEvery(1)
			.jitterMerge(0.8)
			.desc("With its rapid fire rate, a gatling gun can tear apart lightly armoured targets. It's not very accurate, however.")),
	FLIPPED_GATLING_GUN(),
	CANNON(b("Cannon", 2, 1, 100, 60, 800, 70, app().frame(22, 0, 2, 1)).weapon(
			/* reload */   3000,
			/* clip */     1,
			/* jitter */   0.8 / 300,
			/* blastD */   0,
			/* penD */     40,
			/* fireA */    Arc.centeredDegrees(90),
			/* mzCX */     1,
			/* mzCY */     0.55,
			/* mzL */      1.15,
			/* fireSnd */  "cannon",
			/* sndCnt */   6,
			/* hitSnd */   null,
			/* optRange */ 400,
			new WeaponAppearance()
				.shot(new Img("spritesheet", 16, 304, 5, 5, false))
				.barrel(new Img("spritesheet", 327, 124, 37, 11, false), new Pt(-2, 4), 2)
				.recoil(6)
			).explode(30, 50).crew(1).recommendedCrew(2).cost(75).isCannon().isGun()
			.recommendedGuards(1)
			.desc("A single shot can tear through most armour. The mainstay of airship armament.")),
	FLIPPED_CANNON(),
	HV_CANNON(b("Heavy Cannon", 4, 2, 240, 150, 800, 160, app().frame(0, 11, 4, 2)).weapon(
			/* reload */    7000,
			/* clip */      1,
			/* jitter */    0.5 / 300,
			/* blastD */    0,
			/* penD */      100,
			/* fireArc */   Arc.centeredDegrees(60),
			/* mzCX */      2.8,
			/* mzCY */      1.45,
			/* mzL */       1.25,
			/* sound */     "hv_cannon",
			/* nSounds */   3,
			/* hitSound */  null,
			/* optRange */  800,
			new WeaponAppearance()
				.shot(new Img("spritesheet", 0, 304, 10, 5, false))
				.shotEmitter(new Particle.Emitter(Type.TRAILING_SMOKE, 0.007))
				.barrel(new Img("spritesheet", 151, 196, 40, 9, false), new Pt(25, 19), 4)
				.recoil(8)
			).explode(80, 100).windows(0, 0, 2, 1).crew(2).recommendedCrew(3)
			.recommendedGuards(1)
			.canOccupy(0,1, 1,1, 2,1, 3,1, 0,0)
			.cost(200).upDoors(true, false, false, false)
			.required(Bonus.HEAVY_CANNON)
			.isCannon().isGun()
			.desc("Slow-firing but devastating. Not even the strongest armour stands a chance.")),
	FLIPPED_HV_CANNON(),
	IMPERIAL_CANNON(b("Imperial Cannon", 6, 2, 350, 225, 800, 400, app().frame(38, 0, 6, 2)).weapon(
			/* reload */    16000,
			/* clip */      1,
			/* jitter */    0.9 / 300,
			/* blastD */    0,
			/* penD */      350,
			/* fireArc */   Arc.centeredDegrees(40),
			/* mzCX */      2.95,
			/* mzCY */      1.0,
			/* mzL */       3.7,
			/* sound */     "hv_cannon",
			/* nSounds */   3,
			/* hitSound */  null,
			/* optRange */  300,
			new WeaponAppearance()
				.shot(new Img("spritesheet", 720, 16, 10, 10, false))
				.shotEmitter(new Particle.Emitter(Type.TRAILING_SMOKE, 0.007))
				.barrel(new Img("spritesheet", 596, 39, 118, 18, false), new Pt(-12, 7), 6)
				.recoil(8)
			).explode(120, 200).windows(0, 0, 2, 1, 4, 1).crew(4).recommendedCrew(5)
			.recommendedGuards(2)
			.canOccupy(0,1, 1,1, 2,1, 3,1, 4,1, 5,1, 0,0, 3,0)
			.cost(600).upDoors(true, false, false, true, false, false)
			.isCannon().isGun()
			.desc("The huge Imperial Cannon may not fire very quickly, or very accurately, but its shots tear ships apart.")),
	FLIPPED_IMPERIAL_CANNON(),
	FLAMETHROWER(b("Flamethrower", 2, 1, 30, 25, 800, 30, app().frame(20, 1, 2, 1)).weapon(
			/* reload */    50,
			/* clip */      200,
			/* jitter */    2.0 / 300,
			/* blastD */    2,
			/* penD */      0,
			/* fireArc */   Arc.centeredDegrees(90),
			/* mzCX */      1.55,
			/* mzCY */      0.63,
			/* mzL */       0.75,
			/* sound */     "flamethrower",
			/* nSounds */   1,
			/* hitSound */  null,
			/* optRange */  30,
			new WeaponAppearance()
				.shot(new Img("spritesheet", 224, 304, 11, 11, false))
				.shotEmitter(new Particle.Emitter(Type.FIRE, 0.02))
				.barrel(new Img("spritesheet", 325, 134, 24, 4, false), new Pt(13, 8), 2)
			)
			.soundEvery(8)
			.crew(1)
			.recommendedCrew(2)
			.cost(60)
			.jitterMerge(0.95)
			.maxRange(250, 120)
			.shotSpeed(0.2)
			.required(Bonus.FLAMETHROWERS)
			.desc("Shooting an arc of fire, this is a powerful close-range weapon against lightly armoured targets.")),
	FLIPPED_FLAMETHROWER(),
	SUSPENDIUM_CANNON(b("Suspendium Cannon", 5, 2, 280, 100, 800, 200, app().frame(20, 2, 5, 2))
			.weapon(
			/* reload */ 3500,
			/* clip */   10,
			/* jitter */ 0.2 / 300,
			/* blastD */ 0,
			/* penD */   70,
			/* fireArc */Arc.centeredDegrees(30),
			/* mzCX */   3.0,
			/* mzCY */   1.5,
			/* mzL */    2.5,
			/* snd */    "suspendium_cannon",
			/* nSnds */  1,
			/* hitSnd */ null,
			/* optRange*/1300,
			new WeaponAppearance()
				.back(app().frame(33, 0, 5, 2))
				.shot(new Img("spritesheet", 176, 304, 11, 7, false))
				.barrel(new Img("spritesheet", 429, 49, 80, 13, false), new Pt(8, 17), 5)
				.shotEmitter(new Particle.Emitter(Type.SUSP_SPARK, 0.1))
				.recoil(1)
			)
			.crew(3).recommendedCrew(4)
			.cost(300)
			.recommendedGuards(1)
			.upDoors(true, false, true, false, false)
			.external(app().frame(3, 7, 3, 2))
			.emit(new ModuleParticleEmitter(0.4, 0.38, Type.SMOKE, .012))
			.coalReload(10000)
			.canOccupy(0,0, 2,0, 0,1, 1,1, 2,1, 3,1, 4,1)
			.shotSpeed(1.2)
			.required(Bonus.SUSPENDIUM_CANNON)
			.desc("A precise arrangement of Suspendium crystals fires small Suspendium bolts with extreme speed and accuracy.")),
	FLIPPED_SUSPENDIUM_CANNON;

	static {
		FLIPPED_PROPELLER.b.deriveFlipped(PROPELLER);
		FLIPPED_RIFLE.b.deriveFlipped(RIFLE);
		FLIPPED_GRENADES.b.deriveFlipped(GRENADES);
		FLIPPED_ROCKETS.b.deriveFlipped(ROCKETS);
		FLIPPED_GATLING_GUN.b.deriveFlipped(GATLING_GUN);
		FLIPPED_CANNON.b.deriveFlipped(CANNON);
		FLIPPED_HV_CANNON.b.deriveFlipped(HV_CANNON);
		FLIPPED_SMALL_PROPELLER.b.deriveFlipped(SMALL_PROPELLER);
		FLIPPED_FLAMETHROWER.b.deriveFlipped(FLAMETHROWER);
		FLIPPED_SUSPENDIUM_CANNON.b.deriveFlipped(SUSPENDIUM_CANNON);
		FLIPPED_IMPERIAL_CANNON.b.deriveFlipped(IMPERIAL_CANNON);
	}

	private Builder b;

	public static final int COST_PER_TILE = 5;

	public void draw(Draw d, double x, double y, int ms, boolean flipped, int variant) {
		if (b.framesAreVariants) { ms = variant * b.app.getInterval(); }
		if (flipped != isBackOnly() && getApp().width() > getW()) {
			getApp().draw(d, x - (getApp().width() - getW()) * AGame.SGS, y, ms, flipped);
		} else {
			getApp().draw(d, x, y, ms, flipped);
		}
	}

	public void draw(Draw d, double x, double y, int ms, Clr clr, boolean flipped, int variant) {
		if (b.framesAreVariants) { ms = variant * b.app.getInterval(); }
		if (flipped != isBackOnly() && getApp().width() > getW()) {
			getApp().fromX(getApp().width() - getW()).draw(d, x, y, ms, clr, flipped);
			if (getApp().width() > b.w) {
				if (b.flipped || isBackOnly()) {
					getApp().upToX(getApp().width() - getW()).draw(d, x - (getApp().width() - getW()) * AGame.SGS, y, ms, null, flipped);
				} else {
					getApp().fromX(getApp().width() - getW()).draw(d, x - (getApp().width() - getW()) * AGame.SGS, y, ms, null, flipped);
				}
			}
		} else {
			getApp().fromX(getApp().width() - getW()).draw(d, x, y, ms, clr, flipped);
			if (getApp().width() > b.w) {
				if (b.flipped || isBackOnly()) {
					getApp().upToX(getApp().width() - getW()).draw(d, x + getW() * AGame.SGS, y, ms, null, flipped);
				} else {
					getApp().fromX(getApp().width() - getW()).draw(d, x + getW() * AGame.SGS, y, ms, null, flipped);
				}
			}
		}
	}
	
	public void drawBack(Draw d, double x, double y, int ms, Clr clr, boolean flipped, int variant) {
		if (b.framesAreVariants) { ms = variant * b.app.getInterval(); }
		if (flipped != isBackOnly() && getApp().width() > getW()) {
			weaponAppearance().back.draw(d, x - (getApp().width() - getW()) * AGame.SGS, y, ms, clr, flipped);
		} else {
			weaponAppearance().back.draw(d, x, y, ms, clr, flipped);
		}
	}
	
	public void drawExternal(Draw d, double x, double y, int ms, boolean flipped, int variant) {
		if (b.framesAreVariants) { ms = variant * b.app.getInterval(); }
		if (getExternalApp() == null) { return; }
		if (flipped != isBackOnly() && getExternalApp().width() > getW()) {
			getExternalApp().draw(d, x - (getExternalApp().width() - getW()) * AGame.SGS, y, ms, flipped);
		} else {
			getExternalApp().draw(d, x, y, ms, flipped);
		}
	}

	private ModuleType(Builder b) {
		this.b = b;
	}

	private ModuleType() {
		this.b = new Builder();
	}
	
	private static Builder b(String name, int w, int h, int hp, int fireHP, int moveDelay, int weight, Appearance app) {
		return new Builder(name, w, h, hp, fireHP, moveDelay, weight, app);
	}

	public boolean canOccupy(int x, int y) {
		for (Pair<Integer, Integer> p : b.canOccupy) {
			if (p.a == x && p.b == y) { return true; }
		}
		return false;
	}
	
	public int getResourceCapacity(Resource r) {
		switch (r) {
			case AMMO:
				return b.ammo;
			case COAL:
				return b.coal;
			case REPAIR:
				return b.repair;
			case WATER:
				return b.water;
			default: return 0;
		}
	}

	public boolean isOccupable() {
		return !b.canOccupy.isEmpty();
	}

	public boolean isWeapon() {
		return getClip() > 0;
	}
	
	public double DPS() {
		return (b.blastDmg + b.penDmg) * 1000.0 / b.reload;
	}

	public int getOccupableTileCount() {
		return b.canOccupy.size();
	}
	
	private static strictfp class Builder {
		private boolean flipped;
		private String name;
		private String desc;
		private EnumSet<ShipType> availableFor = EnumSet.allOf(ShipType.class);
		private int w;
		private int h;
		private int hp;
		private int fireHP;
		private int explodeHP = -1;
		private int explodeDmg;
		private int moveDelay;
		private int weight;
		private int coal;
		private int ammo;
		private int sickbay;
		private int repair;
		private int water;
		private int quarters;
		private CrewType quartersType;
		private int command;
		private int lift;
		private double propulsion;
		private int coalReload; // in rounds
		private int reload; // distance of 1 standard deviation from target tile
		private int clip;
		private double inaccuracy;
		private int blastDmg;
		private int penDmg;
		private boolean frontOnly;
		private boolean backOnly;
		private int crew;
		private int optionalCrew;
		private int recommendedCrew;
		private int recommendedGuards;
		private int fixedGuards;
		private int cost;
		private int maintenanceCost;
		private int shipHPBonus;
		private double accuracyBonus;
		private Appearance app;
		private boolean isCannon;
		private boolean isExplosive;
		private boolean isGun;
		private Arc fireArc;
		private double muzzleCenterX;
		private double muzzleCenterY;
		private double muzzleLength;
		private WeaponAppearance weaponAppearance;
		private int maxXRange;
		private int maxUpRange;
		private String fireSound;
		private int fireSoundCount;
		private String hitSound;
		private int optimumRange = 500;
		private int soundEvery = 1;
		private Appearance externalApp;
		private ArrayList<Utils.Pair<Integer, Integer>> windows = new ArrayList<Utils.Pair<Integer, Integer>>();
		private ArrayList<Utils.Pair<Integer, Integer>> canOccupy = new ArrayList<Utils.Pair<Integer, Integer>>();
		private ArrayList<ModuleParticleEmitter> emitters = new ArrayList<ModuleParticleEmitter>();
		private boolean[] leftDoors;
		private boolean[] rightDoors;
		private boolean[] upDoors;
		private Bonus required;
		private double jitterMerge;
		private double shotSpeed = 0.85;
		private boolean external = false;
		private boolean drawDoors = true;
		private boolean framesAreVariants = false;
		private int externalXOffset, externalYOffset;
		private boolean externalDrawPriority;
		private int supplyProvided;

		public Builder(String name, int w, int h, int hp, int fireHP, int moveDelay, int weight, Appearance app) {
			this.name = name;
			this.w = w;
			this.h = h;
			this.hp = hp;
			this.fireHP = fireHP;
			this.moveDelay = moveDelay;
			this.weight = weight;
			this.app = app;
			for (int y = 0; y < h; y++) { for (int x = 0; x < w; x++) {
				canOccupy.add(p(x, y));
			}}
			cost = w * h * COST_PER_TILE;
			leftDoors = new boolean[h]; leftDoors[h - 1] = true;
			rightDoors = new boolean[h]; rightDoors[h - 1] = true;
			upDoors = new boolean[w];
		}

		public Builder() {}

		public void deriveFlipped(ModuleType src) {
			Builder b = src.b;
			flipped = !b.flipped;
			name = b.name + " (flipped)";
			desc = b.desc;
			availableFor = b.availableFor;
			w = b.w; h = b.h;
			hp = b.hp; fireHP = b.fireHP;
			explodeHP = b.explodeHP; explodeDmg = b.explodeDmg;
			moveDelay = b.moveDelay;
			weight = b.weight;
			coal = b.coal; ammo = b.ammo; sickbay = b.sickbay; repair = b.repair; water = b.water;
			quarters = b.quarters; quartersType = b.quartersType;
			command = b.command;
			lift = b.lift; propulsion = b.propulsion; coalReload = b.coalReload;
			reload = b.reload; clip = b.clip;
			inaccuracy = b.inaccuracy;
			blastDmg = b.blastDmg; penDmg = b.penDmg;
			frontOnly = b.backOnly; backOnly = b.frontOnly;
			crew = b.crew; optionalCrew = b.optionalCrew; recommendedCrew = b.recommendedCrew;
			recommendedGuards = b.recommendedGuards; fixedGuards = b.fixedGuards;
			cost = b.cost;
			maintenanceCost = b.maintenanceCost;
			app = b.app.flip();
			weaponAppearance = b.weaponAppearance == null ? null : b.weaponAppearance.flipped(w);
			fireArc = b.fireArc == null ? null : b.fireArc.flipHorizontal();
			muzzleCenterX = b.w - b.muzzleCenterX;
			muzzleCenterY = b.muzzleCenterY;
			muzzleLength = b.muzzleLength;
			maxXRange = b.maxXRange;
			maxUpRange = b.maxUpRange;
			isCannon = b.isCannon;
			isExplosive = b.isExplosive;
			isGun = b.isGun;
			fireSound = b.fireSound;
			fireSoundCount = b.fireSoundCount;
			hitSound = b.hitSound;
			optimumRange = b.optimumRange;
			soundEvery = b.soundEvery;
			shipHPBonus = b.shipHPBonus;
			accuracyBonus = b.accuracyBonus;
			jitterMerge = b.jitterMerge;
			shotSpeed = b.shotSpeed;
			drawDoors = b.drawDoors;
			framesAreVariants = b.framesAreVariants;
			externalXOffset = b.externalXOffset;
			externalYOffset = b.externalYOffset;
			externalDrawPriority = b.externalDrawPriority;
			supplyProvided = b.supplyProvided;
			if (b.externalApp != null) {
				externalApp = b.externalApp.flip();
			}
			for (Pair<Integer, Integer> win : b.windows) {
				windows.add(new Pair<Integer, Integer>(w - 1 - win.a, win.b));
			}
			for (Pair<Integer, Integer> co : b.canOccupy) {
				canOccupy.add(new Pair<Integer, Integer>(w - 1 - co.a, co.b));
			}
			for (ModuleParticleEmitter mpe : b.emitters) {
				emitters.add(new ModuleParticleEmitter(w - mpe.x, mpe.y, mpe.t, mpe.emitProbability));
			}
			leftDoors = b.rightDoors;
			rightDoors = b.leftDoors;
			upDoors = new boolean[b.upDoors.length];
			for (int i = 0; i < b.upDoors.length; i++) {
				upDoors[upDoors.length - 1 - i] = b.upDoors[i];
			}
			required = b.required;
			external = b.external;
		}

		public Builder availableFor(ShipType... ts) {
			availableFor.clear();
			for (ShipType t : ts) { availableFor.add(t); }
			return this;
		}
		
		public Builder hatch(int hatchDistance) {
			this.supplyProvided = hatchDistance;
			return this;
		}

		public Builder desc(String desc) {
			this.desc = desc;
			return this;
		}
		
		public Builder explode(int explodeHP, int explodeDmg) {
			this.explodeHP = explodeHP;
			this.explodeDmg = explodeDmg;
			return this;
		}
		
		public Builder coal(int coal) {
			this.coal = coal;
			return this;
		}
		
		public Builder ammo(int ammo) {
			this.ammo = ammo;
			return this;
		}
		
		public Builder maintenanceCost(int maintenanceCost) {
			this.maintenanceCost = maintenanceCost;
			return this;
		}
		
		public Builder sickbay(int sickbay) {
			this.sickbay = sickbay;
			return this;
		}
		
		public Builder repair(int repair) {
			this.repair = repair;
			return this;
		}
		
		public Builder water(int water) {
			this.water = water;
			return this;
		}
		
		public Builder quarters(int quarters, CrewType quartersType) {
			this.quarters = quarters;
			this.quartersType = quartersType;
			return this;
		}
		
		public Builder command(int command) {
			this.command = command;
			return this;
		}
		
		public Builder lift(int lift) {
			this.lift = lift;
			return this;
		}
		
		public Builder propulsion(double propulsion) {
			this.propulsion = propulsion;
			return this;
		}
		
		public Builder coalReload(int coalReload) {
			this.coalReload = coalReload;
			return this;
		}
		
		public Builder frontOnly() {
			this.frontOnly = true;
			return this;
		}
		
		public Builder backOnly() {
			this.backOnly = true;
			return this;
		}
		
		public Builder external(Appearance externalApp) {
			this.externalApp = externalApp;
			return this;
		}
		
		public Builder externalOffset(int externalXOffset, int externalYOffset, boolean externalDrawPriority) {
			this.externalXOffset = externalXOffset;
			this.externalYOffset = externalYOffset;
			this.externalDrawPriority = externalDrawPriority;
			return this;
		}

		public Builder shipHPBonus(int shipHPBonus) {
			this.shipHPBonus = shipHPBonus;
			return this;
		}

		public Builder accuracyBonus(double accuracyBonus) {
			this.accuracyBonus = accuracyBonus;
			return this;
		}

		public Builder leftDoors(boolean... doors) {
			if (doors.length != leftDoors.length) {
				throw new RuntimeException("Incorrect number of left doors for " + name + ". Got " + doors.length + ", expected " + leftDoors.length);
			}
			leftDoors = doors;
			return this;
		}

		public Builder rightDoors(boolean... doors) {
			if (doors.length != rightDoors.length) {
				throw new RuntimeException("Incorrect number of right doors for " + name + ". Got " + doors.length + ", expected " + rightDoors.length);
			}
			rightDoors = doors;
			return this;
		}

		public Builder upDoors(boolean... doors) {
			if (doors.length != upDoors.length) {
				throw new RuntimeException("Incorrect number of up doors for " + name + ". Got " + doors.length + ", expected " + upDoors.length);
			}
			upDoors = doors;
			return this;
		}
		
		public Builder windows(int... xy) {
			for (int i = 0; i < xy.length; i += 2) {
				windows.add(p(xy[i], xy[i + 1]));
			}
			return this;
		}
		
		public Builder canOccupy(int... xy) {
			canOccupy.clear();
			for (int i = 0; i < xy.length; i += 2) {
				canOccupy.add(p(xy[i], xy[i + 1]));
			}
			return this;
		}

		public Builder maxRange(int maxXRange, int maxUpRange) {
			this.maxXRange = maxXRange;
			this.maxUpRange = maxUpRange;
			return this;
		}
		
		public Builder crew(int crew) {
			this.crew = crew;
			return this;
		}
		
		public Builder optionalCrew(int optionalCrew) {
			this.optionalCrew = optionalCrew;
			return this;
		}

		public Builder recommendedCrew(int recommendedCrew) {
			this.recommendedCrew = recommendedCrew;
			return this;
		}
		
		public Builder recommendedGuards(int recommendedGuards) {
			this.recommendedGuards = recommendedGuards;
			return this;
		}
		
		public Builder fixedGuards(int fixedGuards) {
			this.fixedGuards = fixedGuards;
			return this;
		}

		public Builder emit(ModuleParticleEmitter em) {
			this.emitters.add(em);
			return this;
		}
		
		public Builder weapon(int reload, int clip, double inaccuracy, int blastDmg, int penDmg, Arc fireArc, double muzzleX, double muzzleY, double muzzleLength, String fireSound, int fireSoundCount, String hitSound, int optimumRange, WeaponAppearance weaponAppearance) {
			this.reload = reload;
			this.clip = clip;
			this.inaccuracy = inaccuracy;
			this.blastDmg = blastDmg;
			this.penDmg = penDmg;
			this.frontOnly = true;
			this.fireArc = fireArc;
			this.muzzleCenterX = muzzleX;
			this.muzzleCenterY = muzzleY;
			this.muzzleLength = muzzleLength;
			this.fireSound = fireSound;
			this.fireSoundCount = fireSoundCount;
			this.hitSound = hitSound;
			this.optimumRange = optimumRange;
			this.weaponAppearance = weaponAppearance;
			return this;
		}

		public Builder cost(int cost) {
			this.cost = cost;
			return this;
		}

		public Builder required(Bonus required) {
			this.required = required;
			return this;
		}

		public Builder isCannon() {
			this.isCannon = true;
			return this;
		}

		public Builder isExplosive() {
			this.isExplosive = true;
			return this;
		}

		public Builder isGun() {
			this.isGun = true;
			return this;
		}

		public Builder soundEvery(int soundEvery) {
			this.soundEvery = soundEvery;
			return this;
		}
		
		public Builder jitterMerge(double jitterMerge) {
			this.jitterMerge = jitterMerge;
			return this;
		}
		
		public Builder shotSpeed(double shotSpeed) {
			this.shotSpeed = shotSpeed;
			return this;
		}
		
		public Builder external() {
			this.external = true;
			return this;
		}
		
		public Builder dontDrawDoors() {
			this.drawDoors = false;
			return this;
		}
		
		public Builder framesAreVariants() {
			this.framesAreVariants = true;
			return this;
		}
	}
	
	public double getShotSpeed() {
		return b.shotSpeed;
	}

	public String getName() {
		return b.name;
	}

	public EnumSet<ShipType> availableFor() {
		return b.availableFor;
	}

	public boolean availableFor(ShipType t) {
		return b.availableFor.contains(t);
	}

	public int getW() {
		return b.w;
	}

	public int getH() {
		return b.h;
	}

	public int getHp() {
		return b.hp;
	}

	public Arc getFireArc() {
		return b.fireArc;
	}

	public double muzzleCenterX() {
		return b.muzzleCenterX;
	}

	public double muzzleCenterY() {
		return b.muzzleCenterY;
	}
	
	public double muzzleLength() {
		return b.muzzleLength;
	}
	
	public WeaponAppearance weaponAppearance() {
		return b.weaponAppearance;
	}

	public int getFireHP() {
		return b.fireHP;
	}

	public int getExplodeHP() {
		return b.explodeHP;
	}

	public int getExplodeDmg() {
		return b.explodeDmg;
	}

	public int getMoveDelay() {
		return b.moveDelay;
	}

	public int getWeight() {
		return b.weight;
	}

	public int getCoal() {
		return b.coal;
	}
	
	public int maintenanceCost() {
		return b.maintenanceCost;
	}

	public int getAmmo() {
		return b.ammo;
	}

	public int getSickbay() {
		return b.sickbay;
	}

	public int getRepair() {
		return b.repair;
	}

	public int getWater() {
		return b.water;
	}

	public int getQuarters() {
		return b.quarters;
	}
	
	public CrewType getQuartersType() {
		return b.quartersType;
	}

	public int getCommand() {
		return b.command;
	}

	public int getLift(EnumSet<Bonus> bonuses) {
		return bonuses.contains(Bonus.SUSPENDIUM_SPECIALISTS) ? (int) (b.lift * 1.3) : b.lift;
	}
	
	public boolean hasLift() { return b.lift > 0; }

	public double getPropulsion() {
		return b.propulsion;
	}

	public int getCoalReload() {
		return b.coalReload;
	}

	public int getReload() {
		return b.reload;
	}

	public int getClip() {
		return b.clip;
	}

	public double getInaccuracy() {
		return b.inaccuracy;
	}

	public int getBlastDmg(EnumSet<Bonus> bonuses) {
		return bonuses.contains(Bonus.POWERFUL_EXPLOSIVES) ? (int) (b.blastDmg * 1.3) : b.blastDmg;
	}

	public int getPenDmg() {
		return b.penDmg;
	}

	public boolean isFrontOnly() {
		return b.frontOnly;
	}

	public boolean isBackOnly() {
		return b.backOnly;
	}
	
	public int getCrew() {
		return b.crew;
	}
	
	public int getOptionalCrew() {
		return b.optionalCrew;
	}

	public int getRecommendedCrew() {
		return b.recommendedCrew;
	}
	
	public int getRecommendedGuards() {
		return b.recommendedGuards;
	}
	
	public int getFixedGuards() {
		return b.fixedGuards;
	}

	public int getSoundEvery() {
		return b.soundEvery;
	}

	public int getCost(EnumSet<Bonus> bonuses) {
		double c = b.cost;
		if (bonuses.contains(Bonus.CHEAP_WOOD)) {
			c *= 0.9;
		}
		if (b.isGun && bonuses.contains(Bonus.CHEAP_GUNS)) {
			c *= 0.7;
		}
		return (int) c;
	}

	public int getMaxXRange() {
		return b.maxXRange;
	}

	public int getMaxUpRange() {
		return b.maxUpRange;
	}

	public boolean[] getLeftDoors() {
		return b.leftDoors;
	}

	public boolean[] getRightDoors() {
		return b.rightDoors;
	}

	public boolean[] getUpDoors() {
		return b.upDoors;
	}

	public Appearance getApp() {
		return b.app;
	}
	
	public Appearance getExternalApp() {
		return b.externalApp;
	}

	public ArrayList<Utils.Pair<Integer, Integer>> getWindows() {
		return b.windows;
	}
	
	public ArrayList<ModuleParticleEmitter> getEmitters() {
		return b.emitters;
	}
	
	public ArrayList<Utils.Pair<Integer, Integer>> getCanOccupy() {
		return b.canOccupy;
	}

	public Bonus getRequired() {
		return b.required;
	}

	public boolean isCannon() {
		return b.isCannon;
	}

	public boolean isExplosive() {
		return b.isExplosive;
	}

	public boolean isGun() {
		return b.isGun;
	}

	public String getFireSound() {
		return b.fireSound;
	}
	
	public int getFireSoundCount() {
		return b.fireSoundCount;
	}

	public String getHitSound() {
		return b.hitSound;
	}

	public int getShipHPBonus() {
		return b.shipHPBonus;
	}

	public double getAccuracyBonus() {
		return b.accuracyBonus;
	}
	
	public double getJitterMerge() {
		return b.jitterMerge;
	}
	
	public boolean isExternal() {
		return b.external;
	}
	
	public boolean drawDoors() {
		return b.drawDoors;
	}
	
	public boolean areFramesVariants() {
		return b.framesAreVariants;
	}
	
	public int getExternalXOffset() {
		return b.externalXOffset;
	}
	
	public int getExternalYOffset() {
		return b.externalYOffset;
	}
	
	public boolean getExternalDrawPriority() {
		return b.externalDrawPriority;
	}
	
	public int getSupplyProvided() {
		return b.supplyProvided;
	}
	
	public int getOptimumRange() {
		return b.optimumRange;
	}
	
	public int getSupplyRequired() {
		return (int) Math.ceil(
				b.quarters * (b.quartersType != null ? b.quartersType.supplyCost : 0) +
				b.coal * 0.01 +
				b.water * 0.01 +
				b.ammo * 0.01 +
				b.sickbay * 0.2
		);
	}
	
	public boolean isHatch() {
		return b.supplyProvided > 0;
	}
	
	public boolean isFlipped() {
		return b.flipped;
	}
	
	public static class ModuleParticleEmitter extends Particle.Emitter {
		double x, y;

		public ModuleParticleEmitter(double x, double y, Type t, double emitProbability) {
			super(t, emitProbability);
			this.x = x;
			this.y = y;
		}
		
		public Particle emit(double moduleX, double moduleY, int ms, boolean flipped) {
			if (flipped) {
				return super.emit(moduleX - x * AGame.SGS, moduleY + y * AGame.SGS, ms);
			} else {
				return super.emit(moduleX + x * AGame.SGS, moduleY + y * AGame.SGS, ms);
			}
		}
	}
	
	public String getDescription(EnumSet<Bonus> bonuses) {
		StringBuilder sb = new StringBuilder();
		sb.append(getName().toUpperCase()).append("\n\n");
		sb.append(b.desc);
		String nameAndDesc = sb.toString();
		sb = new StringBuilder();
		if (maintenanceCost() > 0) {
			sb.append("Maintenance cost: ").append(maintenanceCost()).append("\n");
		}
		if (isWeapon()) {
			if (getBlastDmg(bonuses) > 0) {
				sb.append("Blast damage: ").append(getBlastDmg(bonuses)).append("\n");
			}
			if (getPenDmg() > 0) {
				sb.append("Penetration damage: ").append(getPenDmg()).append("\n");
			}
			if (getReload() < 1000) {
				sb.append("Rate of fire: ").append(1000 / getReload()).append(" per second").append("\n");
			} else {
				sb.append("Reload time: ").append(getReload() / 1000).append(" seconds").append("\n");
			}
			if (getClip() > 1) {
				sb.append("Clip size: ").append(getClip()).append(" rounds").append("\n");
			}
			sb.append("Fire arc: ").append((int) Math.ceil(getFireArc().sizeRadians * 180 / Math.PI)).append(" degrees").append("\n");
			if (getMaxXRange() != 0) {
				sb.append("Maximum range: ").append(getMaxXRange() / 4).append(" metres").append("\n");
			}
		}
		sb.append("Weight: ").append(getWeight()).append("\n");
		sb.append("HP: ").append(getHp()).append("\n");
		if (getCoal() > 0) {
			sb.append("Coal capacity: ").append(getCoal()).append("\n");
		}
		if (getWater() > 0) {
			sb.append("Water capacity: ").append(getWater()).append("\n");
		}
		if (getRepair() > 0) {
			sb.append("Repair supplies: ").append(getRepair()).append("\n");
		}
		if (getAmmo() > 0) {
			sb.append("Ammo storage: ").append(getAmmo()).append("\n");
		}
		if (getCommand() > 0) {
			sb.append("Provides ").append(getCommand()).append(" command").append("\n");
		}
		if (getSupplyProvided() > 0) {
			sb.append("Provides ").append(getSupplyProvided()).append(" supply\n");
		}
		if (getSupplyRequired() > 0) {
			sb.append("Requires ").append(getSupplyRequired()).append(" supply\n");
		}
		if (getQuarters() > 0) {
			sb.append("Provides quarters for ").append(getQuarters()).append(" ").append(getQuartersType().plural).append("\n");
		}
		if (getSickbay() > 0) {
			sb.append("Provides healing for up to ").append(getSickbay()).append(" crew").append("\n");
		}
		if (getLift(bonuses) > 0) {
			sb.append("Generates ").append(getLift(bonuses)).append(" lift").append("\n");
		}
		if (getPropulsion() > 0) {
			sb.append("Generates ").append((int) (getPropulsion() * 1000)).append(" propulsion").append("\n");
		}
		if (getCoalReload() > 0) {
			sb.append("Requires a unit of coal every ").append(getCoalReload() / 1000).append(" seconds").append("\n");
		}
		if (getShipHPBonus() > 0) {
			sb.append("Increases ship hit points by ").append(getShipHPBonus()).append("\n");
		}
		if (getAccuracyBonus() > 0) {
			sb.append("Increases weapon accuracy by ").append((int) (getAccuracyBonus() * 100)).append("%").append("\n");
		}
		if (getCrew() == 1) {
			sb.append("Operators: one crew member").append("\n");
		} else if (getCrew() > 1) {
			sb.append("Operators: ").append(getCrew()).append(" crew members").append("\n");
		}
		if (getRecommendedCrew() > 0) {
			sb.append("Recommended crew: ").append(getRecommendedCrew()).append("\n");
		}
		if (getFixedGuards() > 0) {
			sb.append("Guards stationed: ").append(getFixedGuards()).append("\n");
		}
		if (sb.length() == 0) {
			return nameAndDesc;
		} else {
			return nameAndDesc + "\n\n" + sb.toString();
		}
	}
}
