module vs.character.kanako;



private import std.math;
private import gedo.audio.sound;
private import gedo.video.ggl;
private import gedo.video.color;
private import gedo.video.texture;
private import gedo.video.polyline;
private import gedo.video.staticmesh;
private import gedo.video.skinnedmesh;
private import gedo.video.alphameshlist;
private import gedo.input.joystick;
private import gedo.system.task;
private import vs.action.task;
private import vs.action.bullet;
private import vs.action.button;
private import vs.action.effect;
private import vs.action.character;
private import vs.bullet.fist;



private enum Motion
{
	//aiw begin ../../../lib/vs/kanako/body.gsa
	ATTACKRUN0,
	ATTACKRUN1,
	CHARGEHIT0,
	CHARGEHIT1,
	CHARGERUN,
	CHARGESHOT,
	COUNTER0,
	DASH0,
	DASH1,
	DASH2,
	DOWN,
	FALL0,
	FALL1,
	GUARD0,
	GUARD1,
	GUARD2,
	HIT0,
	HIT1,
	HOMINGSHOT,
	JUMP,
	KNOCKBACK,
	KO,
	LAND,
	O_DRIVER0,
	O_DRIVER1,
	RISE,
	RUN0,
	RUN1,
	RUN2,
	SINGLESHOT,
	SPECIAL0,
	SPELL,
	STAND,
	//aiw end
	INVALID
}



private enum TextureID
{
	DEFAULT,
	G_DEFAULT,
	ATTACK,
	G_ATTACK,
	DAMAGE,
	G_DAMAGE,
	BUNSHA,
	G_BUNSHA,
	ZETTAI,
	G_ZETTAI,
	SINGLESHOT,
	CHARGESHOT,
	MISSILEBOOST,
}



private enum MeshID
{
	BODY,
	ONBASHIRA,
}



private enum SoundID
{
	HIT0,
	HIT1,
	CHARGEHIT0,
	CHARGEHIT1A,
	CHARGEHIT1B,
	COUNTER0,
	O_DRIVER0,
	O_DRIVER1,
	SINGLESHOT,
	CHARGESHOT,
	HOMINGSHOT,
	SPELL,
}



private enum FistID
{
	HIT0,
	HIT1,
	CHARGEHIT0,
	CHARGEHIT1A,
	CHARGEHIT1B,
	COUNTER0,
	O_DRIVER0,
	O_DRIVER1,
	SPECIAL0,
	SPECIAL1,
	SPELL,
}



public class KanakoFactory : CharacterFactory
{
public:
	this()
	{
		_textures[TextureID.DEFAULT] = Texture("$LIB/kanako/kanako_t.png");
		_textures[TextureID.ATTACK ] = Texture("$LIB/kanako/kanako_t.png");
		_textures[TextureID.DAMAGE ] = Texture("$LIB/kanako/kanako_d.png");
		_textures[TextureID.BUNSHA] = Texture("$LIB/kanako/kanako_b.png");
		_textures[TextureID.ZETTAI] = Texture("$LIB/kanako/kanako_z.png");
		_textures[TextureID.G_DEFAULT] = Texture("$LIB/kanako/g_kanako_t.png");
		_textures[TextureID.G_ATTACK ] = Texture("$LIB/kanako/g_kanako_t.png");
		_textures[TextureID.G_DAMAGE ] = Texture("$LIB/kanako/g_kanako_d.png");
		_textures[TextureID.G_BUNSHA] = Texture("$LIB/kanako/kanako_b.png");
		_textures[TextureID.G_ZETTAI] = Texture("$LIB/kanako/g_kanako_z.png");
		_textures[TextureID.SINGLESHOT] = Texture("$LIB/kanako/singleshot.png");
		_textures[TextureID.CHARGESHOT] = Texture("$LIB/kanako/chargeshot.png");
		_textures[TextureID.MISSILEBOOST] = Texture("$LIB/kanako/missileboost.png");

		_meshes[MeshID.ONBASHIRA] = SkinnedMesh("$LIB/kanako/onbashira.gsa");
		_meshes[MeshID.BODY     ] = SkinnedMesh("$LIB/kanako/body.gsa");

		_missileMesh = StaticMesh("$LIB/kanako/o_missile.gsa");
		_bunshaMesh = StaticMesh("$LIB/kanako/bunsya.gsa");

		_hanabi = new Hanabi();
	}


	Character create(CharacterSystem sys)
	{
		return new Kanako(sys);
	}


private:
	SkinnedMesh[MeshID.max+1] _meshes;
	Texture[TextureID.max+1]  _textures;
	Sound[SoundID.max+1]      _sounds;
	StaticMesh                _missileMesh;
	StaticMesh                _bunshaMesh;
	Hanabi                    _hanabi;



public class Kanako: Character
{
public:
	static this()
	{
		getDefaultAccelerations(_accelerations);

		_accelerations[Movement.RUN][1]     *= 0.625;
		_accelerations[Movement.FALL][1]    *= 0.625;
		_accelerations[Movement.STEP][0]    *= 0.875;
		_accelerations[Movement.STEP][1]    *= 0.875;
	}


	this(CharacterSystem sys)
	{
		super(_meshes[MeshID.BODY], sys);

		auto sound = sys.sharedSound(SharedSoundID.HITM);
		foreach(inout se; _sounds)
		{
			se = sound;
		}
		_sounds[SoundID.HIT1] = sys.sharedSound(SharedSoundID.HITH);
		_sounds[SoundID.CHARGEHIT1B] = sys.sharedSound(SharedSoundID.FINISH);
		_sounds[SoundID.COUNTER0] = sys.sharedSound(SharedSoundID.FINISH);
		_sounds[SoundID.SPELL] = sys.sharedSound(SharedSoundID.FINISH);

		auto ve = sys.sharedEffect(SharedEffectID.HITMARK);

		_fists[FistID.HIT0]        = new Fist(new Power(16, 1.0, [0.0f, 1.0f, 0.0f]), _sounds[SoundID.HIT0], ve, _timer);
		_fists[FistID.HIT1]        = new Fist(new Power(24, 2.5, [0.0f, 1.0f, 0.5f]), _sounds[SoundID.HIT1], ve, _timer);
		_fists[FistID.CHARGEHIT0]  = new Fist(new Power( 8, 0.0, [0.0f, 1.0f, 0.0f]), _sounds[SoundID.CHARGEHIT0], ve, _timer);
		_fists[FistID.CHARGEHIT1A] = new Fist(new Power( 0, 0.0, [0.0f, 1.0f, 0.0f]), _sounds[SoundID.CHARGEHIT1A], ve, _timer);
		_fists[FistID.CHARGEHIT1A].getHoldPosition = &getHoldPosition;
		_fists[FistID.CHARGEHIT1B] = new Fist(new Power(32, 2.5, [0.0f, 1.0f, 1.0f]), _sounds[SoundID.CHARGEHIT1B], _hanabi, _timer);
		_fists[FistID.COUNTER0]    = new Fist(new Power(40, 2.5, [0.0f, 1.0f, 0.0f]), _sounds[SoundID.COUNTER0], ve, _timer);
		_fists[FistID.O_DRIVER0]   = new Fist(new Power( 0, 0.0                    ), _sounds[SoundID.O_DRIVER0], ve, _timer, f2t(16));
		_fists[FistID.O_DRIVER0].getHoldPosition = &getHoldPosition;
		_fists[FistID.O_DRIVER0].onCollideOwner = &onDriverHit;
		_fists[FistID.O_DRIVER1]   = new Fist(new Power(50, 2.5, [0.0f, 1.0f, 1.0f]), _sounds[SoundID.CHARGEHIT1B], ve, _timer);
		_fists[FistID.SPELL]       = new SpellFist(new Power(40, 2.5, [0.0f, 0.25f, 1.0f]), _sounds[SoundID.SPELL], ve, _timer);
		_fists[FistID.SPELL].onCollideOwner = &onSpellHit;
		_fists[FistID.SPECIAL0]    = new Fist(new Power(24, 0.0, [0.0f, 1.0f, 0.0f]), _sounds[SoundID.HIT1], ve, _timer);
		_fists[FistID.SPECIAL1]    = new Fist(new Power(40, 2.5, [0.0f, 1.0f, 1.0f]), _sounds[SoundID.SPELL], ve, _timer);

		auto power = new Power(12, 2.0);
		foreach(inout bullet; _singleShots)
		{
			bullet = new SingleShot(power);
		}

		power = new Power(10, 1.5);
		foreach(inout bullet; _chargeShots)
		{
			bullet = new ChargeShot(power);
		}

		power = new Power(2, 1.5);
		foreach(inout bullet; _missiles)
		{
			bullet = new Missile(power);
		}

		_spell = new Spell(new Power(32, 4.0, [0.0f, 1.0f, 0.5f]));
	}


	override float[] acceleration(Movement m)
	{
		return _accelerations[m];
	}


	override void draw()
	{
		super.draw();

		foreach(pos; _bunshaPositions[0 .. _faith])
		{
			GGL_pushMatrix();

			translate(pos);

			_bunshaMesh.draw();

			GGL_popMatrix();
		}
	}


	override void onDraw()
	{
		uint t;
		uint i;

		_timer.get(i, t);

		Texture tex = null;
		if(_counts[Count.SAFETY] & 2)
		{
			tex = Texture.white;

		}
		else
		{
			auto id = TextureID.DEFAULT;

			switch(state)
			{
			case State.KNOCKBACK:
			case State.DOWN:
			case State.KO:
				id = TextureID.DAMAGE;
				break;

			case State.SINGLESHOT:
			case State.CHARGESHOT:
			case State.HOMINGSHOT:
			case State.HIT:
			case State.CHARGEHIT:
				id = TextureID.ATTACK;
				break;

			case State.SPECIAL:
				id = TextureID.BUNSHA;
				break;

			case State.SPELL:
				if(t >= f2t(47) && t <= f2t(76))
				{
					id = TextureID.ZETTAI;
				}
				else
				{
					id = TextureID.ATTACK;
				}
				break;

			default:
				id = TextureID.DEFAULT;
				break;
			}
			if(_faith >= 3)
			{
				++id;
			}
			tex = _textures[id];
		}

		foreach(mesh; _meshes)
		{
			mesh.draw(i, t, tex);
		}
	}


	override void onUpdate(float axisLength, Vector axisDirection, Button[] buttons)
	{
		uint i, t;

		_timer.get(i, t);

		switch(i)
		{
		case Motion.STAND:
			if(state != State.STAND)
			{
				changeState(State.STAND);
			}
			break;

		case Motion.SINGLESHOT:
			if(t == f2t(5))
			{
				turnToTarget(.std.math.PI/4.0);

				Vector r;
				direction.getRight(r);
				.gedo.math.vector.multiply(r, 2.0);

				if(auto b = cast(Bullet)findFreeTask(_singleShots))
				{
					Vector p = r[];
					.gedo.math.vector.add(p, position.current);

					shot(b, p, direction);
				}

				.gedo.math.vector.invert(r);
				if(auto b = cast(Bullet)findFreeTask(_singleShots))
				{
					Vector p = r[];
					.gedo.math.vector.add(p, position.current);

					shot(b, p, direction);
				}
				Source().play(_system.sharedSound(SharedSoundID.SHOTM));
			}
			else if(t < f2t(5))
			{
				turnToTarget(.std.math.PI/4.0);
			}
			break;

		case Motion.CHARGESHOT:
			if(t == f2t(14))
			{
				if(auto b = cast(Bullet)findFreeTask(_chargeShots))
				{
					shot(b);
					Source().play(_system.sharedSound(SharedSoundID.SHOTM));
				}

				Vector r;
				Vector u;
				direction.getRight(r);
				direction.getUp(u);
				for(auto id=0; id<6; ++id)
				{
					if(auto b = cast(Bullet)findFreeTask(_chargeShots))
					{
						shot(b);

						Vector va = r[];
						.gedo.math.vector.multiply(va, .std.math.cos(.std.math.PI / 3.0 * id));

						Vector vb = u[];
						.gedo.math.vector.multiply(vb, .std.math.sin(.std.math.PI / 3.0 * id));

						.gedo.math.vector.add(va, vb);

						b.direction.homing(va, .std.math.PI/64.0, true);
					}
					else
					{
						break;
					}
				}
			}
			else if(t < f2t(14))
			{
				turnToTarget(.std.math.PI/16.0);
			}
			break;

		case Motion.HOMINGSHOT:
			if(t >= f2t(14) && t <= f2t(26))
			{
				if((t&3) == 0)
				{
					if(auto bullet = cast(Bullet)findFreeTask(_missiles))
					{
						Vector back;

						direction.getFront(back);
						.gedo.math.vector.multiply(back, -2.0);
						.gedo.math.vector.add(back, position.current);

						shot(bullet, back, direction);
						Source().play(_system.sharedSound(SharedSoundID.SHOTL));

						Vector vv;
						direction.getUp(vv);

						Vector vh;
						direction.getRight(vh);

						t = (t - f2t(14))>>2;

						auto angle = .std.math.PI / 6.0 * (t>>1);
						if(t & 1)
						{
							angle = .std.math.PI - angle;
						}

						.gedo.math.vector.multiply(vv, .std.math.sin(angle));
						.gedo.math.vector.multiply(vh, .std.math.cos(angle));

						.gedo.math.vector.add(vv, vh);

						bullet.direction.homing(vv, .std.math.PI, true);
					}
				}
			}
			break;

		case Motion.HIT0:
			if(t < f2t(8))
			{
				follow();
			}
			else if(t == f2t(8))
			{
				velocity[] = 0.0;
				shotFist(_fists[FistID.HIT0]);
			}
			else if(t >= f2t(12) && _command.isReceived(Command.HIT))
			{
				_timer.set(Motion.HIT1, Motion.STAND);
			}
			break;

		case Motion.HIT1:
			if(t < f2t(6))
			{
				follow();
			}
			else if(t == f2t(6))
			{
				velocity[] = 0.0;
				shotFist(_fists[FistID.HIT1]);
			}
			break;

		case Motion.COUNTER0:
			if(t < f2t(9))
			{
				follow();
			}
			else if(t == f2t(9))
			{
				velocity[] = 0.0;
				shotFist(_fists[FistID.COUNTER0]);
			}
			break;

		case Motion.CHARGERUN:
			if(isHitRange() || _command.isReceived(Command.SINGLESHOT))
			{
				_timer.set(Motion.CHARGEHIT0, Motion.STAND);
			}
			follow();
			break;

		case Motion.CHARGEHIT0:
			switch(t)
			{
			case f2t(8):
			case f2t(15):
			case f2t(22):
			case f2t(29):
				shotFist(_fists[FistID.CHARGEHIT0]);
				velocity[] = 0.0;
				break;

			default:
				if(t >= f2t(34) && _command.isReceived(Command.HIT))
				{
					_timer.set(Motion.CHARGEHIT1, Motion.STAND);
				}
				else if(t < f2t(29))
				{
					follow();
				}
				break;
			}
			break;

		case Motion.CHARGEHIT1:
			if(t < f2t(9))
			{
				follow();
			}
			else if(t == f2t(9))
			{
				velocity[] = 0.0;
				shotFist(_fists[FistID.CHARGEHIT1A]);
			}
			else if(t == f2t(19))
			{
				shotFist(_fists[FistID.CHARGEHIT1B], [0.0f, 5.0f, 8.0f]);
			}
			break;

		case Motion.O_DRIVER0:
			if(t == f2t(2))
			{
				shotFist(_fists[FistID.O_DRIVER0]);
			}
			if(t <= f2t(31))
			{
				follow();
			}
			else
			{
				brake();
			}
			break;

		case Motion.O_DRIVER1:
			if(t >= f2t(5))
			{
				velocity[2] = (40.0 - (t - f2t(5))) / 8.0;
				if(velocity[2] < -3.0)
				{
					velocity[2] = -3.0;
				}

				Vector r;
				direction.getRight(r);
				direction.homing(r, .std.math.PI/4.0);
			}
			break;

		case Motion.RISE:
			if(state == State.SPECIAL)
			{
				shotFist(_fists[FistID.O_DRIVER1]);

				direction.getFront(velocity);
				.gedo.math.vector.multiply(velocity, -0.5);
				velocity[2] = 0.5;

				changeState(State.STAND);
			}
			break;

		case Motion.SPECIAL0:
			if(t == f2t(8))
			{
				direction.getFront(_bunshaPositions[_faith]);
				.gedo.math.vector.multiply(_bunshaPositions[_faith], 6.0);
				.gedo.math.vector.add(_bunshaPositions[_faith], position.current);

				++_faith;

				shotFist(_fists[FistID.SPECIAL0]);
			}
			else if(t == f2t(25))
			{
				shotFist(_fists[FistID.SPECIAL1]);
			}

			turnToHorizon();
			brake();
			break;

		case Motion.SPELL:
			if(t < f2t(6))
			{
				follow();
			}
			else
			{
				brake();
				turnToHorizon();
			}

			if(t == f2t(12))
			{
				shotFist(_fists[FistID.SPELL]);
			}
			else if(_spell.parent is this)
			{
				_counts[Count.SAFETY] = 4;
			}
			break;

		default:
			break;
		}
	}


	override void onChangeState(State from, State to)
	{
		Motion[2] motion;

		motion[1] = Motion.INVALID;

		switch(to)
		{
		case State.RESSURECT:
			motion[0] = Motion.FALL0;
			motion[1] = Motion.FALL1;
			break;

		case State.STAND:
			motion[0] = Motion.STAND;
			break;

		case State.FALL:
			if(velocity[2] > 0.0)
			{
				motion[0] = Motion.RISE;
			}
			else
			{
				motion[0] = Motion.FALL0;
				motion[1] = Motion.FALL1;
			}
			break;

		case State.RECOVER:
			switch(_timer.index)
			{
			default:
			case Motion.GUARD1:
				motion[0] = Motion.GUARD2;
				break;

			case Motion.RUN1:
				motion[0] = Motion.RUN2;
				break;

			case Motion.FALL1:
				motion[0] = Motion.LAND;
				break;

			case Motion.DASH1:
				motion[0] = Motion.DASH2;
				break;
			}
			motion[1] = motion[0];
			break;

		case State.RUN:
			motion[0] = Motion.RUN0;
			motion[1] = Motion.RUN1;
			break;

		case State.STEP:
			motion[0] = Motion.DASH0;
			motion[1] = Motion.DASH1;
			break;

		case State.JUMP:
			motion[0] = Motion.JUMP;
			motion[1] = Motion.STAND;
			break;

		case State.LAND:
			motion[0] = Motion.LAND;
			motion[1] = Motion.STAND;
			break;

		case State.GUARD:
			motion[0] = Motion.GUARD0;
			motion[1] = Motion.GUARD1;
			break;

		case State.SINGLESHOT:
			motion[0] = Motion.SINGLESHOT;
			motion[1] = Motion.STAND;
			break;

		case State.CHARGESHOT:
			motion[0] = Motion.CHARGESHOT;
			motion[1] = Motion.STAND;
			break;

		case State.HOMINGSHOT:
			motion[0] = Motion.HOMINGSHOT;
			motion[1] = Motion.STAND;
			break;

		case State.SPECIAL:
			if(_faith < 3)
			{
				motion[0] = Motion.SPECIAL0;
				motion[1] = Motion.STAND;
			}
			else
			{
				_faith = 0;
				motion[0] = Motion.O_DRIVER0;
				motion[1] = Motion.STAND;
			}
			break;

		case State.SPELL:
			motion[0] = Motion.SPELL;
			motion[1] = Motion.STAND;
			break;

		case State.FOLLOW:
			motion[0] = Motion.ATTACKRUN0;
			motion[1] = Motion.ATTACKRUN1;
			break;

		case State.HIT:
			if(from == State.GUARD)
			{
				motion[0] = Motion.COUNTER0;
				motion[1] = Motion.STAND;
			}
			else
			{
				motion[0] = Motion.HIT0;
				motion[1] = Motion.STAND;
			}
			break;

		case State.CHARGEHIT:
			motion[0] = Motion.CHARGERUN;
			motion[1] = Motion.STAND;
			break;

		case State.KNOCKBACK:
			motion[0] = Motion.KNOCKBACK;
			motion[1] = Motion.STAND;
			break;

		case State.DOWN:
			motion[0] = Motion.DOWN;
			motion[1] = Motion.STAND;
			break;

		case State.KO:
			motion[0] = Motion.KO;
			break;
		}

		if(motion[1] == Motion.INVALID)
		{
			_timer.set(motion[0]);
		}
		else
		{
			_timer.set(motion[0], motion[1]);
		}
	}


	override void getEyeVector(Vector d)
	{
		switch(_timer.index)
		{
		case Motion.HIT0:
		case Motion.HIT1:
		case Motion.CHARGEHIT0:
		case Motion.CHARGEHIT1:
		case Motion.COUNTER0:
			direction.getRight(d);
			break;

		case Motion.O_DRIVER1:
			d[] = Direction.FRONT[];
			break;

		case Motion.SPELL:
			if(_timer.time <= f2t(44))
			{
				super.getEyeVector(d);
			}
			else
			{
				direction.getFront(d);
			}
			break;
		default:
			super.getEyeVector(d);
			return;
		}
	}


	override void onLand()
	{
		switch(_timer.index)
		{
		case Motion.O_DRIVER1:
			if(_timer.time > f2t(10))
			{
				_timer.set(Motion.RISE, Motion.STAND);
			}
			break;

		default:
			break;
		}
	}


	override string voiceDir()
	{
		return "$LIB/kanako/voice";
	}


private:
	static float[2][Movement.max+1] _accelerations;
	Bullet[FistID.max+1]            _fists;
	SingleShot[8]                   _singleShots;
	ChargeShot[7]                   _chargeShots;
	Missile[8]                      _missiles;
	Vector[3]                       _bunshaPositions;
	Spell                           _spell;
	uint                            _faith;


	bool getHoldPosition(Vector p)
	{
		uint index, time;

		_timer.get(index, time);

		if(index == Motion.CHARGEHIT1 && time >= f2t(8) && time <= f2t(18))
		{
			Vector p0;
			Vector p1;

			direction.getFront(p0);
			.gedo.math.vector.multiply(p0, 7.0);

			direction.toVector(0.0, 5.0, 8.0, p1);

			float k = (time - f2t(8)) / 18.0;

			.gedo.math.vector.blend(p, 1.0-k, p0, k, p1);

			.gedo.math.vector.add(p, position.current);

			return true;
		}
		else if(index == Motion.O_DRIVER0 || index == Motion.O_DRIVER1)
		{
			direction.getFront(p);
			.gedo.math.vector.multiply(p, 3.0);
			.gedo.math.vector.add(p, position.current);

			return true;
		}

		return false;
	}


	void onDriverHit(VisualTask t, Vector p)
	{
		_timer.set(Motion.O_DRIVER1, Motion.RISE);

		turnToHorizon();

		velocity[] = 0.0;

		_isGhost = true;
	}


	void onSpellHit(VisualTask t, Vector p)
	{
		if(!_spell.parent)
		{
			Vector pos;

			direction.getFront(pos);
			.gedo.math.vector.multiply(pos, 5.0 + _spell.radius);
			.gedo.math.vector.add(pos, position.current);
			pos[2] += 128.0;

			shot(_spell, pos, direction);

			_spell.direction.homing([0.0f, 0.0f, -1.0f], .std.math.PI, true);
		}
	}
}



private class SingleShot : Bullet
{
public:
	this(Power p)
	{
		_power = p;
	}


	override void onUpdate()
	{
		homing(.std.math.PI/2048.0);

		Vector v;

		direction.getFront(v);

		.gedo.math.vector.multiply(v, 4.0);

		.gedo.math.vector.add(position.current, v);

		if(_time >= 128)
		{
			stop();
		}
		else if((cast(Character)parent).system.field.isSinked(position.current))
		{
			_time = 128;
		}
	}


	override void draw()
	{
		if(auto am = (cast(Character)parent).system.alphaMeshList().get())
		{
			am.set(position.current, &draw2);
		}
	}


	override void onCollide(Vector center)
	{
		_time = 128;
	}


	override BulletType type()
	{
		return BulletType.SHOT;
	}


	override Power power()
	{
		return _power;
	}


	override Sound hitSE()
	{
		return _sounds[SoundID.SINGLESHOT];
	}


	override EffectProgram hitEffect()
	{
		return _hanabi;
	}


private:
	Power _power;


	void draw2()
	{
		translate(position.current);

		GGL_enterBillBoardMode();

		GGL_setBlendMode(GGL_BlendMode.MIX);

		GGL_rotate(_time * .std.math.PI / 3.0, 0, 0, 1);

		_textures[TextureID.SINGLESHOT].draw(4.0, 4.0);
	}
}



private class ChargeShot : Bullet
{
public:
	this(Power p)
	{
		_power    = p;
		_polyLine = new PolyLine(2);

		_polyLine.count  = 2;
		_polyLine.radius = 1.0;
	}


	override float radius()
	{
		return _polyLine.radius;
	}


	override Vector[] points()
	{
		return _polyLine.points[length-1 .. length];
	}


	override void onStart()
	{
		foreach(inout point; _polyLine.points)
		{
			point[] = position.current[];
		}
	}


	override void onUpdate()
	{
		if(_time >= LIFETIME)
		{
			stop();
		}
		else
		{
			Vector v;

			direction.getFront(v);

			.gedo.math.vector.multiply(v, 4.0);

			.gedo.math.vector.add(position.current, v);

			_polyLine.points[length-1][] = position.current[];

			Vector d;

			.gedo.math.vector.subtract(d, _polyLine.points[0], _polyLine.points[1]);

			auto l = .gedo.math.vector.magnitude(d);
			if(l > 32.0)
			{
				.gedo.math.vector.multiply(d, 32.0/l);

				.gedo.math.vector.add(_polyLine.points[0], _polyLine.points[1], d);
			}

			if((cast(Character)parent).system.field.isSinked(position.current))
			{
				_time = LIFETIME;
			}
		}
	}


	override void draw()
	{
		if(auto am = (cast(Character)parent).system.alphaMeshList().get())
		{
			am.set(_polyLine.vertices, _textures[TextureID.CHARGESHOT], GGL_BlendMode.MIX);
		}
	}


	override void onCollide(Vector center)
	{
		_time = LIFETIME;
	}


	override BulletType type()
	{
		return BulletType.SHOT;
	}


	override Power power()
	{
		return _power;
	}


	override Sound hitSE()
	{
		return _sounds[SoundID.CHARGESHOT];
	}


	override EffectProgram hitEffect()
	{
		return _hanabi;
	}


private:
	const int LIFETIME = 64;

	PolyLine _polyLine;
	Power    _power;
}



private class Missile : Bullet
{
public:
	this(Power p)
	{
		_power = p;

		_polyLine = new PolyLine(4);

		_polyLine.count  = 4;
		_polyLine.radius = 1.0;
	}


	override void onStart()
	{
		foreach(inout point; _polyLine.points)
		{
			point[] = position.current[];
		}
	}


	override void onUpdate()
	{
		if(_time >= 32)
		{
			homing(.std.math.PI/32.0);

			Vector v;

			direction.getFront(v);

			.gedo.math.vector.multiply(v, 2.0);

			.gedo.math.vector.add(position.current, v);
		}
		else
		{
			Vector v;

			direction.getFront(v);

			.gedo.math.vector.multiply(v, 0.25);

			.gedo.math.vector.add(position.current, v);
		}

		if(_time >= LIFETIME)
		{
			stop();
		}
		else if((cast(Character)parent).system.field.isSinked(position.current))
		{
			_time = LIFETIME;
		}
		else
		{
			auto master = cast(Character)parent;

			if(master && master.getBalance() != 0)
			{
				stop();
			}
			else
			{
				foreach(i, inout p; _polyLine.points[0 .. $-1])
				{
					p[] = _polyLine.points[i+1][];
				}
				_polyLine.points[$-1][] = position.current[];
				if(_time & 4)
				{
					_polyLine.radius = 0.25;
				}
				else
				{
					_polyLine.radius = 1.0;
				}
			}
		}
	}


	override void draw()
	{
		GGL_pushMatrix();

		Vector pos;

		direction.getFront(pos);
		.gedo.math.vector.multiply(pos, 1.8);
		.gedo.math.vector.add(pos, position.current);

		direction.transform(pos);

		GGL_rotate(_time * .std.math.PI / 16.0, 0, 1, 0);

		_missileMesh.draw();

		GGL_popMatrix();

		if(auto am = (cast(Character)parent).system.alphaMeshList().get())
		{
			am.set(_polyLine.vertices, _textures[TextureID.MISSILEBOOST], GGL_BlendMode.ADD);
		}
	}


	override void onCollide(Vector center)
	{
		_time = LIFETIME;
	}


	override BulletType type()
	{
		return BulletType.SHOT;
	}


	override Power power()
	{
		return _power;
	}


	override Sound hitSE()
	{
		return _sounds[SoundID.SINGLESHOT];
	}


	override EffectProgram hitEffect()
	{
		return _hanabi;
	}


private:
	const int LIFETIME = 192;

	PolyLine _polyLine;
	Power    _power;
}



private class Spell : Bullet
{
public:
	this(Power p)
	{
		_power = p;
	}


	override float radius()
	{
		return 16.0;
	}


	override void onUpdate()
	{
		if(_time >= LIFETIME)
		{
			stop();
		}
		else
		{
			position.current[2] -= 128.0 / LIFETIME;
		}
	}


	override void draw()
	{
		GGL_pushMatrix();

		direction.transform(position.current);

		GGL_rotate(_time * .std.math.PI / 16.0, 0, 1, 0);

		auto s = radius / 0.4;
		GGL_scale(s, s, s);

		_missileMesh.draw();

		GGL_popMatrix();
	}


	override BulletType type()
	{
		return BulletType.HOLD;
	}


	override Power power()
	{
		return _power;
	}


	override Sound hitSE()
	{
		return _sounds[SoundID.SPELL];
	}


	override EffectProgram hitEffect()
	{
		return (cast(Character)parent).system.sharedEffect(SharedEffectID.HITMARK);
	}


private:
	const int LIFETIME = 128;

	Power _power;
}



private class SpellFist : Fist
{
public:
	this(Power pow, Sound se, EffectProgram ve, Timer t)
	{
		super(pow, se, ve, t);
	}


	override float radius()
	{
		return 4.0;
	}


	override BulletType type()
	{
		return BulletType.HIT;
	}
}



private class Hanabi : EffectProgram
{
public:
	this()
	{
		foreach(i, inout hanabi; _hanabis)
		{
			hanabi = SkinnedMesh("$LIB/kanako/hanabi_" ~ cast(char)(i + '0') ~ ".gsa");
		}
	}


	void update(EffectTask task)
	{
		if(!isLiving(task))
		{
			task.stop();
		}
	}


	void draw(EffectTask task)
	{
		if(auto am = task.list.get())
		{
			am.set(task.position.current, &task.drawAlpha);
		}
	}


	void drawAlpha(EffectTask task)
	{
		GGL_pushMatrix();
		scope(exit)
		{
			GGL_popMatrix();
		}

		translate(task.position.current);

		GGL_enterBillBoardMode();

		GGL_setBlendMode(GGL_BlendMode.MIX);

		foreach(i, hanabi; _hanabis)
		{
			auto color = rgba(255, 255, 255, alpha(i, task.time));

			if(task.time < hanabi.animationLength(0))
			{
				hanabi.draw(0, task.time, hanabi.texture, color);
			}
		}
	}


	bool isLiving(EffectTask task)
	{
		return (task.time < _hanabis[0].animationLength(0));
	}


private:
	SkinnedMesh[5] _hanabis;


	int alpha(int i, int t)
	{
		const int[5] FADETIMES = [f2t(5), f2t(5), f2t(7), f2t(9), f2t(15)];

		if(t <= FADETIMES[i])
		{
			return 255;
		}
		else
		{
			auto ta = _hanabis[i].animationLength(0);

			return cast(int)(256.0*cast(double)(ta - t)/(ta - FADETIMES[i]));
		}
	}
}
}
