module vs.scene.action;



private import std.math;
private import std.cstream;

private import gedo.gui.label;
private import gedo.gui.table;
private import gedo.math.direction;
private import gedo.math.quaternion;
private import gedo.audio.sound;
private import gedo.audio.music;
private import gedo.video.ggl;
private import gedo.video.rect;
private import gedo.video.color;
private import gedo.video.texture;
private import gedo.video.staticmesh;
private import gedo.video.alphameshlist;
private import gedo.input.joystick;
private import gedo.system.task;
private import gedo.system.scene;
private import gedo.text.parser;

private import vs.action.task;
private import vs.action.field;
private import vs.action.camera;
private import vs.action.button;
private import vs.action.bullet;
private import vs.action.character;
private import vs.action.inputdevice;
private import vs.effect.program;

static private import vs.action.effect;



public enum Player2is
{
	NONE,
	FRIEND,
	ENEMY,
}



private enum Result
{
	NONE = -1,
	WIN,  // 1P win
	LOSE, // 1P lose
	DRAW,
}



alias Scene delegate(Action) OnQuit;
/*
root
	camera3D(0, 1)
		field
			teams
				team0
				team1
			effects
			lockMark
	camera2D
		0
			resultGuide
			lifeMeter
			buttonMeter
		1
			resultGuide
			lifeMeter
			buttonMeter
		common
			readyGuide
*/
public class Action : Scene, CharacterSystem
{
public:
	this(
			OnQuit               dg,
			Field                field,
			CharacterFactory[][] factoryArrays,
			Joystick[]           joysticks,
			Player2is            p2is,
			uint                 cplevel,
			uint                 arrowSize,
			bool                 isHitCameraEnable,
			float                memberLife = 1.0,
			int                  teamLife = 3
		)
	{
		_effects = new Effects(&alphaMeshList);

		_sounds[SharedSoundID.FINISH] = Sound("$LIB/action/finish.ogg");
		_sounds[SharedSoundID.JUMP]   = Sound("$LIB/action/jump.ogg");
		_sounds[SharedSoundID.HITL]   = Sound("$LIB/action/hitl.ogg");
		_sounds[SharedSoundID.HITM]   = Sound("$LIB/action/hitm.ogg");
		_sounds[SharedSoundID.HITH]   = Sound("$LIB/action/hith.ogg");
		_sounds[SharedSoundID.CUT]    = Sound("$LIB/action/cut.ogg");
		_sounds[SharedSoundID.SWINGL] = Sound("$LIB/action/swingl.ogg");
		_sounds[SharedSoundID.SWINGH] = Sound("$LIB/action/swingh.ogg");
		_sounds[SharedSoundID.SHOTBEAM] = Sound("$LIB/action/shotbeam.ogg");
		_sounds[SharedSoundID.SHOTL] = Sound("$LIB/action/shotl.ogg");
		_sounds[SharedSoundID.SHOTM] = Sound("$LIB/action/shotm.ogg");

		Character[][] characters;

		characters.length = 2;
		characters[0].length = 2;
		characters[1].length = 2;
		foreach(i, factories; factoryArrays)
		{
			foreach(j, factory; factories)
			{
				characters[i][j] = factory.create(this);
			}
		}

		foreach(member; characters[1])
		{
			member.life.max = cast(int)(member.life.max * memberLife);
		}

		_root = new RootTask(this);

		Character p2 = null;
		switch(p2is)
		{
		case Player2is.NONE:
			p2 = null;
			break;

		case Player2is.FRIEND:
			p2 = characters[0][1];
			break;

		case Player2is.ENEMY:
			p2 = characters[1][0];
			break;
		}

		_players[0] = characters[0][0];
		_players[1] = p2;
		if(p2)
		{
			if(isHitCameraEnable)
			{
				_cameras3D = new Cameras3D(new Camera(&characters[0][0].getEye), new Camera(&p2.getEye));
			}
			else
			{
				_cameras3D = new Cameras3D(new Camera(&characters[0][0].getEyeBasic), new Camera(&p2.getEyeBasic));
			}
		}
		else
		{
			if(isHitCameraEnable)
			{
				_cameras3D = new Cameras3D(new Camera(&characters[0][0].getEye));
			}
			else
			{
				_cameras3D = new Cameras3D(new Camera(&characters[0][0].getEyeBasic));
			}
		}
		_root.add(_cameras3D);

		_field = field;
		_cameras3D.add(_field);

		auto teams = new Teams(characters, &gameSet, teamLife);
		_field.add(teams);

		_field.add(_effects);

		{
			float as = arrowSize/25.0;

			auto lockmark = new LockMark(characters[0][0], as);
			_field.add(lockmark);
			_field.add(new LockMark(characters[0][1], as, lockmark.texture));
			_field.add(new LockMark(characters[1][0], as, lockmark.texture));
			_field.add(new LockMark(characters[1][1], as, lockmark.texture));
		}

		_field.add(new LifeViewer3D(characters[0][0]));
		_field.add(new LifeViewer3D(characters[0][1]));
		_field.add(new LifeViewer3D(characters[1][0]));
		_field.add(new LifeViewer3D(characters[1][1]));

		_playerSticks[] = joysticks[];
		_player2is      = p2is;

		if(_playerSticks[0])
		{
			characters[0][0].inputDevice = new ActionJoystick(_playerSticks[0], _cameras3D.current);
		}
		if(p2)
		{
			_cameras3D.swap();
			p2.inputDevice = new ActionJoystick(_playerSticks[1], _cameras3D.current);
			_cameras3D.swap();
			_quitCount = 1;
		}
		else
		{
			_quitCount = 0;
		}

		characters[0][0].ressurect(-10, -170);
		characters[0][1].ressurect( 10, -170);
		characters[1][0].ressurect(-10,  170);
		characters[1][1].ressurect( 10,  170);

		foreach(cs; characters)
		{
			foreach(c; cs)
			{
				c.cplevel = cplevel;
			}
		}

		_cameras2D = new Cameras2D();
		_root.add(_cameras2D);

		_readyGuide = new ReadyGuide();
		_cameras2D.add(_readyGuide);

		auto texTeamLife0 = Texture("$LIB/action/teamlife0.png", false);
		auto texTeamLife1 = Texture("$LIB/action/teamlife1.png", false);
		_cameras2D.current.add(new TeamLifeViewer(teams.items[0], texTeamLife0, true));
		_cameras2D.current.add(new TeamLifeViewer(teams.items[1], texTeamLife1, false));

		_cameras2D.current.add(new LifeViewer2D(characters[0][0]));
		if(p2)
		{
			_cameras2D.swap();

			_cameras2D.current.add(new LifeViewer2D(p2));
			if(p2is == Player2is.FRIEND)
			{
				_cameras2D.current.add(new TeamLifeViewer(teams.items[0], texTeamLife0, true));
				_cameras2D.current.add(new TeamLifeViewer(teams.items[1], texTeamLife1, false));
			}
			else
			{
				_cameras2D.current.add(new TeamLifeViewer(teams.items[0], texTeamLife0, false));
				_cameras2D.current.add(new TeamLifeViewer(teams.items[1], texTeamLife1, true));
			}

			_cameras2D.swap();

			GGL_getViewport(&_viewport[0], &_viewport[1], &_viewport[2], &_viewport[3]);
		}

		_resultGuide = new ResultGuide();

		{
			Color[1] pixels;
			pixels[0] = rgba(0, 0, 0, 128);
			_shadow = StaticMesh.createCircle(1.0, new Texture(pixels, 1, 1));
		}
		_onQuit = dg;
	}


	override void update()
	{
		if(!_resultGuide.parent)
		{
			_root.scanBottomUp((Task task){(cast(ActionTask)task).update();});
			_root.update();
		}
		else
		{
			_resultGuide.update();
		}

		if(_playerSticks[0])
		{
			if(_playerSticks[0].buttons[ActionButton.PAUSE].isPushed(0))
			{
				if(!_nextScene)
				{
					_nextScene = new PauseScene(this, &onPause);
				}
			}
		}
		else if(Joysticks().detectPressed())
		{
			quitScene();
		}
	}


	override void draw()
	{
		GGL_clearScreen(0.0, 0.5, 1.0);

		if(_player2is == Player2is.NONE)
		{
			alphaMeshList().clear();
			_root.scanTopDown((Task task){(cast(ActionTask)task).draw();});
		}
		else
		{
			GGL_setViewport(_viewport[0], _viewport[1], _viewport[2]>>1, _viewport[3]);
			alphaMeshList().clear();
			_root.scanTopDown((Task task){(cast(ActionTask)task).draw();});

			GGL_setViewport(_viewport[0]+(_viewport[2]>>1), _viewport[1], _viewport[2]>>1, _viewport[3]);
			_cameras3D.swap();
			_cameras2D.swap();
			alphaMeshList().clear();
			_root.scanTopDown((Task task){(cast(ActionTask)task).draw();});

			_cameras3D.swap();
			_cameras2D.swap();
		}
	}


	override void end()
	{
		if(_player2is != Player2is.NONE)
		{
			GGL_setViewport(_viewport[0], _viewport[1], _viewport[2], _viewport[3]);
		}

		super.end();
	}


	AlphaMeshList alphaMeshList()
	{
		return _cameras2D.current.alphaMeshList();
	}


	EffectProgram sharedEffect(SharedEffectID id)
	{
		return _effects[id];
	}


	EffectTask getEffectTask()
	{
		return _effects.getTask();
	}


	Sound sharedSound(SharedSoundID id)
	{
		return _sounds[id];
	}


	Field field()
	{
		return _field;
	}


	bool isReady()
	{
		return (_readyGuide.parent !is null);
	}


	void gameSet(Result r)
	{
		if(_resultGuide.result == Result.NONE)
		{
			_resultGuide.result = r;

			if(_player2is != Player2is.NONE)
			{
				auto r2 = r;

				if(_player2is == Player2is.ENEMY)
				{
					switch(r)
					{
					case Result.WIN:
						r2 = Result.LOSE;
						break;

					case Result.LOSE:
						r2 = Result.WIN;
						break;

					default:
						break;
					}
				}

				_resultGuide.result2 = r2;
			}

			_cameras2D.add(_resultGuide);
		}
	}


	void quitScene()
	{
		if(_quitCount == 0)
		{
			_nextScene = _onQuit(this);
		}
		else
		{
			--_quitCount;
		}
	}


	bool isClicked(Character p)
	{
		foreach(i, player; _players)
		{
			if(p is player)
			{
				if(_playerSticks[i])
				{
					return _playerSticks[i].buttons[0].isPushed();
				}
				else
				{
					return false;
				}
			}
		}

		return false;
	}


	bool playerWin()
	{
		return (_resultGuide.result == Result.WIN);
	}


	StaticMesh shadow()
	{
		return _shadow;
	}


	void scores(uint[] a)
	{
		foreach(i, player; _players)
		{
			if(player)
			{
				player.scoreTotal = a[i];
			}
		}
	}


	uint score(uint i)
	{
		if(_players[i])
		{
			return _players[i].scoreTotal;
		}
		else
		{
			return 0;
		}
	}


	void times(uint[] a)
	{
		foreach(i, player; _players)
		{
			if(player)
			{
				player.timeTotal = a[i];
			}
		}
	}


	uint time(uint i)
	{
		if(_players[i])
		{
			return _players[i].timeTotal;
		}
		else
		{
			return 0;
		}
	}


private:
	Sound[SharedSoundID.max+1] _sounds;
	Character[2]  _players;
	Joystick[2]   _playerSticks;
	ResultGuide   _resultGuide;
	ReadyGuide    _readyGuide;
	StaticMesh    _shadow;
	Cameras3D     _cameras3D;
	Cameras2D     _cameras2D;
	Player2is     _player2is;
	RootTask      _root;
	Effects       _effects;
	OnQuit        _onQuit;
	Field         _field;
	int[4]        _viewport;
	uint          _quitCount;


	void onPause(PauseScene pauseScene)
	{
		if(_playerSticks[0].buttons[ActionButton.PAUSE].isPushed(0))
		{
			pauseScene.resume();
		}
		/*
		else if(_playerSticks[0].axis.asButtons[Axis.UP].isPushed(0))
		{
			if(_playerSticks[0].buttons[ActionButton.SEARCH].isPushed)
			{
				_cameras3D.current.DZ += 0.25;
			}
			else if(_playerSticks[0].buttons[ActionButton.DEFEND].isPushed)
			{
				_cameras3D.current.DY += 0.25;
			}
			else
			{
				_cameras3D.current.FOV -= 15.0;
			}
		}
		else if(_playerSticks[0].axis.asButtons[Axis.DOWN].isPushed(0))
		{
			if(_playerSticks[0].buttons[ActionButton.SEARCH].isPushed)
			{
				_cameras3D.current.DZ -= 0.25;
			}
			else if(_playerSticks[0].buttons[ActionButton.DEFEND].isPushed)
			{
				_cameras3D.current.DY -= 0.25;
			}
			else
			{
				_cameras3D.current.FOV += 15.0;
			}
		}
		_cameras3D.current.update();
		*/
	}


	void drawScore(Camera c)
	{
		auto p = _cameras3D.position(c);
		_cameras2D.camera(p).add(new ScoreViewer(_players[p]));
		c.focus(&_players[p].getEye);
	}
}



private class RootTask : ActionTask
{
public:
	this(Action s)
	{
		_scene = s;
		_time  = 0;

		_scoreSE = Sound("$LIB/action/score.ogg");
	}


	void quitScene()
	{
		_scene.quitScene();
	}


	bool isClicked(Character p)
	{
		return _scene.isClicked(p);
	}


	void drawScore(Camera c)
	{
		_scene.drawScore(c);
	}


	uint scoreTime()
	{
		const uint LIMIT = 180*60;
		if(_time < LIMIT)
		{
			return LIMIT - _time;
		}
		else
		{
			return 0;
		}
	}


	uint time()
	{
		return _time;
	}


	override void update()
	{
		++_time;
	}


	Sound scoreSE()
	{
		return _scoreSE;
	}


private:
	Action _scene;
	Sound  _scoreSE;
	uint   _time;
}



private class Camera2D : ActionTask
{
public:
	this()
	{
		_list = new AlphaMeshList();
	}


	AlphaMeshList alphaMeshList()
	{
		return _list;
	}


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

		GGL_enableDepthBuffer(0);

		GGL_enableAlphaBlend(1);

		GGL_enter2DMode(640, 480);
	}


private:
	AlphaMeshList _list;
}



private class ReadyGuide : ActionTask
{
public:
	this()
	{
		_meshes[0][0] = StaticMesh("$LIB/action/ready.gsa");
		_meshes[0][1] = StaticMesh("$LIB/action/ready1.gsa");
		_meshes[0][2] = StaticMesh("$LIB/action/ready2.gsa");
		_meshes[1][0] = StaticMesh("$LIB/action/go.gsa");
		_meshes[1][1] = StaticMesh("$LIB/action/go1.gsa");
		_meshes[1][2] = StaticMesh("$LIB/action/go2.gsa");
		_time         = 256;
	}


	override void update()
	{
		if(_time == 0)
		{
			stop();
		}
		else
		{
			--_time;
		}
	}


	override void draw()
	{
		GGL_pushMatrix();

		GGL_translate(0.0, 120.0, 0.0);

		StaticMesh[] meshes = null;
		if(_time <= 60)
		{
			if(_time > 44)
			{
				auto k = 1.0 - (_time - 44) / 16.0;
				GGL_scale(k, k, 1);
			}
			meshes = _meshes[1];
		}
		else
		{
			if(_time <= 60 + 120)
			{
				auto k = 1.0 - (_time - 60) / 120.0;
				GGL_scale(k, 1, 1);

				meshes = _meshes[0];
			}
		}

		if(meshes)
		{
			GGL_scale(48.0, 48.0, 1);

			meshes[2].draw();
			meshes[1].draw();
			meshes[0].draw();
		}

		GGL_popMatrix();
	}


private:
	StaticMesh[3][2] _meshes;
	uint             _time;
}



private class ScoreViewer : ActionTask
{
private:
	enum Item
	{
		TIME_STAGE,
		TIME_TOTAL,
		ATTACK,
		DAMAGE,
		COMBO,
		TIME,
		LIFE,
		STAGE_TOTAL,
		PLAY_TOTAL,
	}


public:
	this(Character player)
	{
		struct Score
		{
			string path;
			Color  color;
			uint   value;
		}

		_player = player;
		_root   = (cast(RootTask)player.root);

		Score[Item.max+1] scores;

		scores[Item.TIME_STAGE].path  ="$LIB/action/timestage.png";
		scores[Item.TIME_STAGE].value = _root.time;
		scores[Item.TIME_STAGE].color = rgba(0, 128, 255, 255);

		scores[Item.TIME_TOTAL].path  ="$LIB/action/timetotal.png";
		scores[Item.TIME_TOTAL].value = _root.time + player.timeTotal;
		scores[Item.TIME_TOTAL].color = rgba(0, 255, 255, 255);

		scores[Item.ATTACK].path  ="$LIB/action/scoreattack.png";
		scores[Item.ATTACK].value = player.scoreAttack*20;
		scores[Item.ATTACK].color = WHITE;

		scores[Item.DAMAGE].path  ="$LIB/action/scoredamage.png";
		scores[Item.DAMAGE].value = player.scoreDamage*20;
		scores[Item.DAMAGE].color = RED;

		scores[Item.COMBO ].path  ="$LIB/action/scorecombo.png";
		scores[Item.COMBO ].value = player.scoreCombo*5000;
		scores[Item.COMBO ].color = WHITE;

		scores[Item.TIME  ].path  ="$LIB/action/scoretime.png";
		scores[Item.TIME  ].value = _root.scoreTime;
		scores[Item.TIME  ].color = WHITE;

		scores[Item.LIFE  ].path  ="$LIB/action/scorelife.png";
		scores[Item.LIFE  ].value = (cast(Team)player.parent).life*1000;
		scores[Item.LIFE  ].color = WHITE;

		scores[Item.STAGE_TOTAL].path ="$LIB/action/scorestagetotal.png";
		scores[Item.STAGE_TOTAL].color = YELLOW;
		scores[Item.STAGE_TOTAL].value = scores[Item.COMBO].value + scores[Item.TIME].value + scores[Item.LIFE].value;
		if(scores[Item.ATTACK].value > scores[Item.DAMAGE].value)
		{
			scores[Item.STAGE_TOTAL].value += scores[Item.ATTACK].value - scores[Item.DAMAGE].value;
		}

		scores[Item.PLAY_TOTAL ].path ="$LIB/action/scoreplaytotal.png";
		scores[Item.PLAY_TOTAL ].color = GREEN;
		scores[Item.PLAY_TOTAL ].value = player.scoreTotal + scores[Item.STAGE_TOTAL].value;

		player.scoreTotal = scores[Item.PLAY_TOTAL].value;
		player.timeTotal  = scores[Item.TIME_TOTAL].value;

		auto dottex = Texture("$LIB/action/dot.png", false);

		_table = new Table(2, scores.length);
		foreach(j, score; scores)
		{
			auto keyLabel = new ImageLabel(Texture(score.path, false));
			keyLabel.color = score.color;
			_table.set(0, j, keyLabel);

			if(j <= Item.TIME_TOTAL)
			{
				auto subtable = new Table(5, 1);

				subtable.fill(rgba(0, 0, 0, 0));
				subtable.frame(rgba(0, 0, 0, 0));

				auto valueLabel = new DigitLabel(score.value/3600);
				valueLabel.color = score.color;
				subtable.set(0, 0, valueLabel);

				subtable.set(1, 0, new ImageLabel(dottex));

				valueLabel = new DigitLabel((score.value/60)%60);
				valueLabel.color = score.color;
				subtable.set(2, 0, valueLabel);

				subtable.set(3, 0, new ImageLabel(dottex));

				valueLabel = new DigitLabel(((score.value%60)*100)/60);
				valueLabel.color = score.color;
				subtable.set(4, 0, valueLabel);

				_table.set(1, j, subtable);
			}
			else
			{
				auto valueLabel = new DigitLabel(score.value);
				valueLabel.color = score.color;
				_table.set(1, j, valueLabel);
			}
		}
		_table.update();

		_time = 0;

		Source().play(_root.scoreSE());
	}


	override void update()
	{
		if(_time >= 60*10 || _root.isClicked(_player))
		{
			_root.quitScene();
			stop();
		}
		else
		{
			++_time;
		}
	}


	override void draw()
	{
		_table.drawRecursive();
	}


private:
	Character _player;
	RootTask  _root;
	Table     _table;
	uint      _time;
}



private class ResultGuide : ActionTask
{
private:
	enum FinishScedule
	{
		GUIDE_FADEIN = 32,
		GUIDE_SHOW   = GUIDE_FADEIN + 96,
	}


public:
	this()
	{
		_resultMarks[Result.WIN][0]  = StaticMesh("$LIB/action/win.gsa");
		_resultMarks[Result.WIN][1]  = StaticMesh("$LIB/action/win1.gsa");
		_resultMarks[Result.WIN][2]  = StaticMesh("$LIB/action/win2.gsa");
		_resultMarks[Result.LOSE][0] = StaticMesh("$LIB/action/lose.gsa");
		_resultMarks[Result.LOSE][1] = StaticMesh("$LIB/action/lose1.gsa");
		_resultMarks[Result.LOSE][2] = StaticMesh("$LIB/action/lose2.gsa");
		_resultMarks[Result.DRAW][0] = StaticMesh("$LIB/action/draw.gsa");
		_resultMarks[Result.DRAW][1] = StaticMesh("$LIB/action/draw1.gsa");
		_resultMarks[Result.DRAW][2] = StaticMesh("$LIB/action/draw2.gsa");

		_time = 0;
	}


	void result(Result r)
	{
		_result[0] = r;
		_result[1] = r;
	}


	void result2(Result r)
	{
		_result[1] = r;
	}


	Result result()
	{
		return _result[0];
	}


	override void update()
	{
		if(_time >= FinishScedule.max)
		{
			stop();
		}
		else
		{
			++_time;
		}
	}


	override void draw()
	{
		GGL_pushMatrix();

		if(_time < FinishScedule.GUIDE_FADEIN)
		{
			float k = 1.0 - (FinishScedule.GUIDE_FADEIN - _time) / cast(float)FinishScedule.GUIDE_FADEIN;

			GGL_rotate(.std.math.PI * 2.0 * k, 0, 0, 1);

			GGL_scale(k, k, 1);
		}
		GGL_scale(64.0, 64.0, 1);

		auto meshes = _resultMarks[_result[(cast(Cameras2D)parent).position]];

		meshes[2].draw();
		meshes[1].draw();
		meshes[0].draw();

		GGL_popMatrix();
	}


private:
	StaticMesh[3][Result.max+1] _resultMarks;
	Result[2]                   _result;
	uint                        _time;
}



alias void delegate(Result result) OnGameSet;
alias void delegate() OnQuitScene;



private class Teams : ActionTask, BulletSystem
{
public:
	this(Character[][] characters, OnGameSet dg, int life2 = 3)
	{
		_teams[0] = new Team(0);
		_teams[1] = new Team(1);
		_teams[1].life = life2;

		add(_teams[0]);
		add(_teams[1]);

		foreach(i, team; _teams)
		{
			foreach(c; characters[i])
			{
				team.add(c);
			}
		}

		_onGameSet   = dg;
	}


	override void update()
	{
		// move
		foreach(team; _teams)
		{
			foreach(task; team)
			{
				auto c = cast(Character)task;
				if(!c.isPaused && isRunning(c))
				{
					c.move();
					foreach(b; c)
					{
						(cast(Bullet)b).updateRelativePosition();
					}
				}
			}
		}

		// push & attack
		if(_teams[0].life != 0 && _teams[1].life != 0)
		{
			collision();
		}

		// clip
		auto field = cast(Field)parent;
		foreach(team; _teams)
		{
			foreach(task; team)
			{
				auto c = cast(Character)task;

				Vector p = c.position.current[];

				Vector s;

				s[0] = s[1] = c.radius;
				s[2] = -c.groundPosition;


				field.clip(c.position.previous, p, s);

				c.move(p);
			}
		}
	}


	Team[] items()
	{
		return _teams;
	}


	bool isRunning(VisualTask t)
	{
		return (_dio is null || _dio is t);
	}


	void pause(VisualTask t)
	{
		_dio = t;
		Music.mute();
	}


	void resume()
	{
		_dio = null;
		Music.unmute();
	}


private:
	VisualTask  _dio = null;
	OnGameSet   _onGameSet;
	CameraMan   _cameraMan = null;
	Team[2]     _teams;


	void collision()
	{
		Character winner = null;
		Character loser  = null;

		foreach(task0; _teams[0])
		{
			auto c0 = cast(Character)task0;
			if(c0.life.current > 0)
			{
				foreach(task1; _teams[1])
				{
					auto c1 = cast(Character)task1;

					if(c1.life.current > 0)
					{
						c0.push(c1);

						if(_teams[1].life > 0)
						{
							c0.attack(c1);

							if(c1.life.current <= 0)
							{
								_teams[1].life -= 1;
								if(_teams[1].life == 0)
								{
									winner = c0;
									loser  = c1;
								}
							}
						}

						if(_teams[0].life > 0)
						{
							c1.attack(c0);
							if(c0.life.current <= 0)
							{
								_teams[0].life -= 1;
								if(_teams[0].life == 0)
								{
									winner = c1;
									loser  = c0;
								}
								break;
							}
						}
					}
				}
			}
		}

		if(winner && !_cameraMan)
		{
			onGameSet(winner, loser);
		}
	}


	void onGameSet(Character winner, Character loser)
	{
		if(_teams[0].life == 0 && _teams[1].life == 0)
		{
			_onGameSet(Result.DRAW);
		}
		else if(_teams[1].life == 0)
		{
			_onGameSet(Result.WIN);
		}
		else
		{
			_onGameSet(Result.LOSE);
		}

		auto cameras = cast(Cameras3D)parent.parent;
		auto camera  = cameras.current;

		_cameraMan = new CameraMan(camera, winner, loser);

		camera.add(_cameraMan);

		cameras.swap();

		if(cameras.current)
		{
			cameras.current.add(new CameraMan(cameras.current, winner, loser));
		}

		cameras.swap();
	}
}



private class Team : ActionTask
{
public:
	int life = 3;


	this(uint teamNo)
	{
		_no = teamNo;
	}


private:
	uint _no;
}



private class TeamLifeViewer : ActionTask
{
public:
	this(Team team, Texture mark, bool isMine)
	{
		_texture = mark;
		_team    = team;

		if(isMine)
		{
			_y = -240.0 + _texture.height*0.5;
		}
		else
		{
			_y = 240.0 - _texture.height*0.5;
		}

		_previousLife = _team.life;
		_time         = 0;
	}


	override void update()
	{
		if(_previousLife != _team.life)
		{
			_time         = MAX_TIME;
			_previousLife = _team.life;
		}
		else if(_time > 0)
		{
			--_time;
		}
	}


	override void draw()
	{
		GGL_pushMatrix();

		auto x = 320.0 - _texture.width*0.5;
		if(_time > 0)
		{
			auto t = _time>>1;
			if(t&1)
			{
				x -= t;
			}
			else
			{
				x += t;
			}
		}

		GGL_translate(x, _y, 0);

		auto dy = INTERVAL;
		if(_y > 0)
		{
			dy = -INTERVAL;
		}
		for(auto i=0;i<_team.life;++i)
		{
			_texture.draw();

			GGL_translate(0.0, dy, 0.0);
		}
		if(_time > 0)
		{
			GGL_translate(-(MAX_TIME - _time)*128/MAX_TIME, 0.0, 0.0);

			_texture.draw(rgba(255, 255, 255, 255*_time/MAX_TIME));
		}

		GGL_popMatrix();
	}


private:
	Texture _texture;
	float   _y;
	Team    _team;
	int     _time;
	int     _previousLife;

	static const float INTERVAL = 32.0;
	static const int   MAX_TIME = 64;
}



private class Effects : ActionTask
{
public:
	this(GetAlphaMeshList dg)
	{
		foreach(inout e; _machines)
		{
			e = new EffectTask(dg);
		}

		_programs[SharedEffectID.HITMARK]   = new HitMark();
		_programs[SharedEffectID.GUARDMARK] = new GuardMark();
		_programs[SharedEffectID.LANDMARK] = new LandMark();
		_programs[SharedEffectID.DASHMARK] = new DashMark();
		_programs[SharedEffectID.SETSPELLMARK] = new SetSpellMark();
		_programs[SharedEffectID.FLASH4] = new Flash4();
	}


	EffectProgram opIndex(size_t i)
	{
		return _programs[i];
	}


	EffectTask getTask()
	{
		if(auto e = cast(EffectTask)findFreeTask(_machines))
		{
			super.add(e);

			return e;
		}

		return null;
	}


private:
	EffectProgram[SharedEffectID.max+1] _programs;
	EffectTask[64]                      _machines;
}



private class LockMark : ActionTask
{
public:
	this(Character owner, float scale, Texture tex = null)
	{
		_target = owner.target;
		_owner  = owner;
		_time   = 0;
		_isRed  = false;
		_scale  = scale;

		if(tex)
		{
			_texture = tex;
		}
		else
		{
			Color[8] pixels;

			pixels[0] = rgba(255, 255,   0, 255);
			pixels[1] = rgba(  0, 255, 255, 255);
			pixels[2] = rgba(255,   0,   0, 255);
			pixels[3] = rgba(  0,   0, 255, 255);
			foreach(i, p; pixels[0 .. 4])
			{
				pixels[i+4] = setA(p, 0);
			}

			_texture = new Texture(pixels, pixels.length/2, 2);
		}

		_lockMark = StaticMesh("$LIB/action/lockmark.gsa");
	}


	override void update()
	{
		if(_owner.target !is _target)
		{
			_target = _owner.target;
			_isRed  = false;
			_time   = 0;
		}

		if(_owner.hasNewBullets())
		{
			_isRed = true;
			_time  = 0;
		}

		++_time;
	}


	override void draw()
	{
		if(auto am = _owner.system.alphaMeshList().get())
		{
			am.set(_target.position.current, &drawAlpha);
		}
	}


	void drawAlpha()
	{
		GGL_setBlendMode(GGL_BlendMode.MIX);

		GGL_enableDepthBuffer(0);
		scope(exit)
		{
			GGL_enableDepthBuffer(1);
		}
		auto scale = _scale;
		auto cameras = cast(Cameras3D)parent.parent;
		if(cameras.current.isEye(&_target.getEye) || cameras.current.isEye(&_target.getEyeBasic))
		{
			scale *= 2.0;
		}
		else if(cameras.current.isEye(&_owner.getEye) || cameras.current.isEye(&_owner.getEyeBasic))
		{
			translate(_target.position.current);

			GGL_enterBillBoardMode();

			GGL_rotate(_time*PI/128.0, 0, 0, 1);

			if(_target.isSafety())
			{
				_lockMark.draw(rgba(255, 255, 64, 224));
			}
			else
			{
				_lockMark.draw(rgba(255, 64, 0, 224));
			}

			return;
		}

		if(_time < HIGHLIGHT_TIME && (_time&4) != 0)
		{
			scale *= 2.0;
		}

		Vector d;
		
		.gedo.math.vector.subtract(d, _owner.position.current, _target.position.current);

		.gedo.math.vector.normalize(d);

		Vector head;

		.gedo.math.vector.multiply(head, d, 12.0 - (_time&15)*0.5);
		.gedo.math.vector.add(head, _target.position.current);

		Vector tail;

		.gedo.math.vector.multiply(tail, d, scale);
		.gedo.math.vector.add(tail, head);

		Vector r;
		{
			Vector campos;

			GGL_getCameraPosition(campos.ptr);

			Vector a;
			.gedo.math.vector.subtract(a, tail, campos);

			cross(r, a, d);

			auto l = .gedo.math.vector.magnitude(r);
			if(l >= 1.0/256.0)
			{
				.gedo.math.vector.multiply(r, scale/l);
			}
			else
			{
				return;
			}
		}

		float tx = 1.0/8.0;
		if(_target.target !is _owner)
		{
			tx += 2.0/8.0;
		}
		if(_time < HIGHLIGHT_TIME && _isRed)
		{
			tx += 4.0/8.0;
		}

		.gedo.math.vector.add(_vertices[0].position, tail, r);
		_vertices[0].texCoord[0] = tx;
		_vertices[0].texCoord[1] = 0.75;

		_vertices[1].position[]  = head[];
		_vertices[1].texCoord[0] = tx;
		_vertices[1].texCoord[1] = 0.25;

		.gedo.math.vector.subtract(_vertices[2].position, tail, r);
		_vertices[2].texCoord[0] = tx;
		_vertices[2].texCoord[1] = 0.75;

		_texture.draw(_vertices);
	}


	Texture texture()
	{
		return _texture;
	}


private:
	GGL_Vertex[3] _vertices;
	StaticMesh    _lockMark;
	Character     _owner;
	Character     _target;
	Texture       _texture;
	float         _scale;
	bool          _isRed;
	int           _time;

	static const int HIGHLIGHT_TIME = 32;
}



private class CameraMan : ActionTask
{
public:
	this(Camera camera, Character winner, Character loser)
	{
		_camera = camera;
		_winner = winner;
		_loser  = loser;
		_time   = 0;
	}


	override void update()
	{
		++_time;
		switch(_time)
		{
		case 2:
			_camera.focus(&_loser.getEye);
			break;

		case 121:
			_camera.focus(&_winner.getEye);
			break;

		case 241:
			(cast(RootTask)_winner.root).drawScore(_camera);
			stop();
			break;

		default:
			break;
		}
	}


private:
	Character _winner;
	Character _loser;
	Camera    _camera;
	uint      _time;
}



private class LifeViewer2D : ActionTask
{
public:
	this(Character c)
	{
		_character = c;
	}


	override void draw()
	{
		_character.drawLife2D();
	}


private:
	Character _character;
}



private class LifeViewer3D : ActionTask
{
public:
	this(Character c)
	{
		_character = c;
	}


	override void draw()
	{
		auto cameras = cast(Cameras3D)parent.parent;

		if((!cameras.current.isEye(&_character.getEye)) && (!cameras.current.isEye(&_character.getEyeBasic)))
		{
			_character.drawLife3D();
		}
	}


private:
	Character _character;
}



private class Cameras3D : ActionTask
{
public:
	this(Camera c0, Camera c1 = null)
	{
		_cameras[0] = c0;
		_cameras[1] = c1;
		_position   = 0;
	}


	Camera current()
	{
		return _cameras[_position];
	}


	int position(Camera c)
	{
		foreach(i, camera; _cameras)
		{
			if(c is camera)
			{
				return i;
			}
		}
	}


	void swap()
	{
		_position ^= 1;
	}


	override void update()
	{
		foreach(camera; _cameras)
		{
			if(camera)
			{
				foreach(task; camera)
				{
					(cast(ActionTask)task).update();
				}
				camera.update();
			}
		}
	}


	override void draw()
	{
		current.draw();
	}


private:
	Camera[2] _cameras;
	int       _position;
}



private class Cameras2D : ActionTask
{
public:
	this()
	{
		foreach(inout camera; _cameras)
		{
			camera = new Camera2D();
		}

		_position = 0;
	}


	Camera2D current()
	{
		return _cameras[_position];
	}


	Camera2D camera(uint p)
	{
		return _cameras[p];
	}


	int position()
	{
		return _position;
	}


	void swap()
	{
		_position ^= 1;
	}


	override void update()
	{
		foreach(camera; _cameras)
		{
			camera.scanBottomUp((Task task){(cast(ActionTask)task).update();});
			camera.update();
		}
	}


	override void draw()
	{
		current.draw();
		current.scanTopDown((Task task){(cast(ActionTask)task).draw();});
	}


private:
	Camera2D[2] _cameras;
	int         _position;
}



private class ActionJoystick : ActionInputDevice
{
public:
	this(Joystick joystick, Camera camera)
	{
		foreach(i, inout button; _buttons)
		{
			if(i < joystick.buttons.length)
			{
				button = joystick.buttons[i];
			}
			else
			{
				button = new Button();
			}
		}

		_joystick = joystick;
		_camera   = camera;
	}


	override void update()
	{
		Vector2D axis2D;

		axis2D[0] = _joystick.axis.x / 32768.0;
		axis2D[1] = _joystick.axis.y / 32768.0;

		_axis.length = .gedo.math.vector.magnitude(axis2D);

		if(_axis.length >= 0.5)
		{
			_camera.toWorld(axis2D, _axis.direction);
		}
		else
		{
			_axis.length      = 0.0;
			_axis.direction[] = Direction.FRONT[];
		}
	}


	override void getLocalAxis(Vector a)
	{
		a[0] = _joystick.axis.x / 32768.0;
		a[1] = _joystick.axis.y / 32768.0;
		a[2] = 0.0;
	}


private:
	Joystick _joystick;
	Camera   _camera;
}
