/*
	dungeon.js: ダンジョン生成スクリプト
		version: 0.1
		published: 2008-11-07
		updated: 2009-01-09
		author: junkieta(webmaster@junkieta.net)
*/


function Dungeon(sizeX, sizeY) {
	sizeX = new Number(sizeX);
	sizeY = new Number(sizeY);
	// X, Yともに0 ～ 0xFFの範囲におさめる(各座標は8bit整数として扱う)
	if(sizeX < 0 || 0x100 < sizeX || sizeY < 0 || 0x100 < sizeY)
		throw "WRONG_SIZE_ERR";
	this.maxSizeX = sizeX;
	this.maxSizeY = sizeY;
	this.currentFloor = null;
}

// 方角オブジェクトを生成する
Dungeon.NORTH_DIR = { name : "NORTH", offsetValue : -0x200, bitMask : 1 };
Dungeon.EAST_DIR = { name : "EAST", offsetValue : 1, bitMask : 2 };
Dungeon.SOUTH_DIR = { name : "SOUTH", offsetValue : 0x200, bitMask : 4 };
Dungeon.WEST_DIR = { name : "WEST", offsetValue : -1, bitMask : 8 };
Dungeon.NORTH_DIR.toString =
Dungeon.EAST_DIR.toString =
Dungeon.SOUTH_DIR.toString =
Dungeon.WEST_DIR.toString = function() { return this.name; };
Dungeon.NORTH_DIR.valueOf =
Dungeon.EAST_DIR.valueOf =
Dungeon.SOUTH_DIR.valueOf =
Dungeon.WEST_DIR.valueOf = function() { return this.bitMask; };
// 各方角オブジェクトは固定的な位置関係にあり、"left" "right" "back"で互いに参照可能
Dungeon.NORTH_DIR.left = Dungeon.EAST_DIR.back = Dungeon.SOUTH_DIR.right = Dungeon.WEST_DIR;
Dungeon.EAST_DIR.left = Dungeon.SOUTH_DIR.back = Dungeon.WEST_DIR.right = Dungeon.NORTH_DIR;
Dungeon.SOUTH_DIR.left = Dungeon.WEST_DIR.back = Dungeon.NORTH_DIR.right = Dungeon.EAST_DIR;
Dungeon.WEST_DIR.left = Dungeon.NORTH_DIR.back = Dungeon.EAST_DIR.right = Dungeon.SOUTH_DIR;

// KeyCode <-> CommandIdentifer
Dungeon.KeycodeConverter = {
	38: 1, // 前進
	37: 2, // 向き変更(左)
	39: 3, // 向き変更(右)
	40: 4, // 向き変更(後)
	27: 5  // 終了
};

Dungeon.getCommandIdFromKeyCode = function(kc) { return this.KeycodeConverter[kc] || null; };

Dungeon.prototype = {
	
	constructor : Dungeon,
	
	// 主人公の座標情報取得関数
	getHeroX : function() { return this.heroOffset & 0xFF; },
	getHeroY : function() { return this.heroOffset >> 9 & 0xFF; },
	
	// 現在のheroOffsetからXY分ぶらした座標が移動可能かどうか
	isWalkable : function(x, y) {
		var nx = this.getHeroX();
		var ny = this.getHeroY();
		var dir = this.heroDir;
		switch(dir) {
			case Dungeon.NORTH_DIR: nx += x; ny -= y; break;
			case Dungeon.EAST_DIR: nx += y; ny += x; break;
			case Dungeon.SOUTH_DIR: nx -= x; ny += y; break;
			case Dungeon.WEST_DIR: nx -= y; ny -= x; break;
		}
		return this.currentFloor.isWalkable(nx, ny);
	},
	
	// 引数の示すコマンドを実行し、成功すれば1, 失敗すれば0を返す
	executeCommand : function(cId/* CommandIdentifer */) {
		if(!cId) return 0;
		switch(cId) {
			case 1: // ↑
				if(!this.isWalkable(0,1)) return 0;
				this.heroOffset += this.heroDir.offsetValue;
				break;
			case 2: // ←
				this.heroDir = this.heroDir.left;
				break;
			case 3: // →
				this.heroDir = this.heroDir.right;
				break;
			case 4: // ↓
				this.heroDir = this.heroDir.back;
				break;
			case 5:
				replaceView(this);
			default:
				return 0;
		}
		return 1;
	},
	
	generateRandom : function() {
		// 迷宮の生成
		this.currentFloor = new Dungeon.Maze(this);
		// 主人公の初期方角をセット
		this.heroDir = [Dungeon.NORTH_DIR, Dungeon.EAST_DIR, Dungeon.SOUTH_DIR, Dungeon.WEST_DIR][Math.floor(Math.random() * 4)];
		// 主人公の所在を確定
		this.heroOffset = this.currentFloor.getRandomOffset(false, true);
	}
};

// ダンジョンフロアのプロトタイプ
Dungeon.FloorPrototype = {
	// 指定座標が移動可能かどうかを返す
	isWalkable : function(x, y) { return this[x | y << 9] || 0; },
	
	// ランダムにフロア内の座標を生成して返す
	// noEvenフラグは偶数地点[通路座標]を排除する
	// noWallフラグは移動不能地点を排除する
	getRandomOffset : function(noEven, noWall) {
		var x = this.dungeon.maxSizeX;
		var y = this.dungeon.maxSizeY;
		var o = noEven == true
			? Math.floor(Math.random() * x / 2) * 2 + 1 | Math.floor(Math.random() * y / 2) * 2 + 1 << 9
			: Math.floor(Math.random() * x) | Math.floor(Math.random() * y) << 9;
		if(noWall == true) {
			if(noEven == true) while(!this[o])
				o = Math.floor(Math.random() * x / 2) * 2 + 1 | Math.floor(Math.random() * y / 2) * 2 + 1 << 9;
			else while(!this[o])
				o = Math.floor(Math.random() * x) | Math.floor(Math.random() * y) << 9;
		}
		return o;
	}
};

// ランダム迷路のコンストラクタ
Dungeon.Maze = function(dun) {
	if(!(dun instanceof Dungeon)) throw "WRONG OBJECT ERR";
	var y, x, offset, r, target, p, np;
	this.constructor = Dungeon.Maze;
	this.dungeon = dun;
	// 床情報の初期化
	for(y = 0; y < dun.maxSizeY; y++) for(x = 0; x < dun.maxSizeX; x++)
		// XからYへの繰り上がりを防ぐため8bit目を空けている
		this[x | y << 9] = 0;
	// 生成開始位置を範囲内の奇数offsetとして取得
	offset = this.getRandomOffset(true, false);
	x = p & 0xFF;
	y = p >> 9;
	// 配列かき混ぜ用関数(Array.sortの引数)
	r = function() { return Math.random() < 0.5 ? 1 : -1; };
	// 生成先の方角を格納、ループ内でpop & pushしていく
	target = [Dungeon.NORTH_DIR, Dungeon.EAST_DIR, Dungeon.SOUTH_DIR, Dungeon.WEST_DIR];
	target.sort(r);
	while(target.length) {
		p = target.pop();
		// 16bit整数はオフセットに代入する
		if(typeof p == "number") {
			offset = p;
			continue;
		}
		// 二歩先(分岐点)の位置を取得
		np = offset + (p.offsetValue * 2);
		// 範囲外・生成済み地点は無視
		if(!this.hasOwnProperty(np) || 0 < this[np]) continue;
		
		// 現在地・通路・次の分岐点にそれぞれ移動可能方角を書き込む
		this[offset] |= p.bitMask;
		this[offset + p.offsetValue] |= (p.bitMask | p.back.bitMask);
		this[np] = p.bitMask;
		
		// 配列に次の移動候補を追加
		p = [p, p.left, p.right];
		p.sort(r);
		target.push(offset, p[0], p[1], p[2]);
		// 現在地更新
		offset = np;
	}
};

// Floorをプロトタイプチェーンに接続
Dungeon.Maze.prototype = Dungeon.FloorPrototype;

// 迷宮の描画を更新
// いずれViewクラスとして抽象化した方がいいかも
function updateView(dun) {
	var x, y, node1, node2, classNames;
	y = x = 0;
	node1 = document.getElementById("DUNGEON");
	for(node1 = node1.lastChild; node1; node1 = node1.previousSibling) {
		// 左方向チェック
		for(node2 = node1.firstChild.firstChild; node2; node2 = node2.nextSibling) {
			classNames = [];
			if(!dun.isWalkable(x, y))
				classNames.push("wall"); // 移動不能
			else if(!dun.isWalkable(x - 1, y))
				classNames.push("stopleft"); // 左方向に壁
			// 左方向終了
			if(!x) break;
			node2.className = classNames.join(" ");
			x++;
		}
		
		classNames.unshift("front");
		node2.className = classNames.join(" ");
		// 中心部は同一X座標の要素が二つあるので0に戻す
		x = 0;
		classNames.length = 1; // == ["front"]
		// 右方向チェック
		for(node2 = node2.nextSibling; node2; node2 = node2.nextSibling) {
			if(!dun.isWalkable(x, y))
				classNames.push("wall"); // 移動不能
			else if(!dun.isWalkable(x + 1, y))
				classNames.push("stopright"); // 右方向に壁
			node2.className = classNames.join(" ");
			classNames.length = 0; // クラス名配列初期化
			x++;
		}
		y++;
		x = -y;
	}
	
	// 位置情報表示の更新
	node1 = document;
	node2 = node1.getElementById("DIRECTION").lastChild;
	node2.data = dun.heroDir.name.charAt();
	x = dun.getHeroX();
	node2 = node1.getElementById("X").lastChild;
	node2.data = (x < 16 ? "0x0" : "0x").concat(x.toString(16).toUpperCase());
	y = dun.getHeroY();
	node2 = node1.getElementById("Y").lastChild;
	node2.data = (y < 16 ? "0x0" : "0x").concat(y.toString(16).toUpperCase());
}

// ダンジョン描画用のDOMツリーを生成し、DocumentFragmentとして確保しておく
(function(){
	var doc, root, node, node2, add, elm, txt, tmp, i;
	doc = document;
	// コード量省略用変数
	add = new String("appendChild");
	elm = new String("createElement");
	txt = new String("createTextNode");
	root = doc.createDocumentFragment();
	Dungeon.prototype.bodyContent = root;
	
	// コマンドリスト
	node = root[add](doc[elm]("dl"));
	node.className = "command";
	tmp = [
		"↑", "前進",
		"→", "向き変更(右)",
		"↓", "向き変更(後)",
		"←", "向き変更(左)",
		"ESC", "終了"
	];
	i = 0;
	while(i in tmp) {
		node[add](doc[elm]("dt"))[add](doc[elm]("kbd"))[add](doc[txt](tmp[i++]));
		node[add](doc[elm]("dd"))[add](doc[txt](tmp[i++]));
	}
	
	// 3Dダンジョン用ListElement
	node = root[add](doc[elm]("ul"));
	node.id = "DUNGEON";
	tmp = doc[elm]("li");
	tmp[add](doc[elm]("code"))[add](doc[txt]("X"));
	node2 = node[add](doc[elm]("li"))[add](doc[elm]("ul"));
	node2.className = "Y2";
	for(i = 0; i < 6; i++)
		node2[add](tmp.cloneNode(true));
	node2 = node[add](doc[elm]("li"))[add](doc[elm]("ul"));
	node2.className = "Y1";
	for(i = 0; i < 4; i++)
		node2[add](tmp.cloneNode(true));
	node2 = node[add](doc[elm]("li"))[add](doc[elm]("ul"));
	node2.className = "Y0";
	node2[add](tmp.cloneNode(true));
	node2[add](tmp);
	
	// 位置情報
	node = root[add](doc[elm]("dl"));
	node.className = "geo";
	node[add](doc[elm]("dt"))[add](doc[txt]("方角"));
	tmp = node[add](doc[elm]("dd"))[add](doc[elm]("var"));
	tmp.id = "DIRECTION";
	tmp[add](doc[txt]("?"));
	node[add](doc[elm]("dt"))[add](doc[txt]("座標"));
	tmp = node[add](doc[elm]("dd"));
	tmp[add](doc[txt]("X="));
	tmp = tmp[add](doc[elm]("var"));
	tmp.id = "X";
	tmp[add](doc[txt]("----"));
	tmp = node[add](doc[elm]("dd"));
	tmp[add](doc[txt]("Y="));
	tmp = tmp[add](doc[elm]("var"));
	tmp.id = "Y";
	tmp[add](doc[txt]("----"));
})();

// 文書<->ダンジョンの切り替え
function replaceView(dun) {
	var doc, node, body, df, tmp;
	doc = document;
	// スタイルシートの有効・無効切り替え
	node = doc.styleSheets;
	tmp = node.length;
	while(tmp--)
		node[tmp].disabled = !node[tmp].disabled;
	// 現在のbody.childNodesをDocumentFragmentに格納する
	body = doc.body;
	node = body.firstChild;
	df = doc.createDocumentFragment();
	while(node) {
		node = (tmp = node).nextSibling;
		body.removeChild(tmp);
		if(tmp.nodeType == 1 && tmp.nodeName != "SCRIPT")
			df.appendChild(tmp);
	}
	// DocumentFragmentを入れ替える
	body.appendChild(dun.bodyContent);
	dun.bodyContent = df;
};


// スタイルシートの確認(Safari対策)
if(document.styleSheets.length != 2) (function(links) {
	var hash = {}, i, n;
	for(i = 0, n; i < links.length; i++) {
		n = links[i];
		if(n.rel == "alternate stylesheet") {
			n.rel = "stylesheet";
			n.disabled = false;
			hash[n.href] = true;
		}
	}
	links = document.styleSheets;
	for(i = 0; i < links.length; i++)
		if(hash[links[i].href])
			links[i].disabled = true;
})(document.getElementsByTagName("link"));


// ウインドウロードのハンドラ。Dungeonオブジェクトを生成し、キー入力に結びつける
function Init() {
	var dun, doc, node;
	// ダンジョンを生成しておく
	dun = new Dungeon(0x30,0x30);
	dun.generateRandom();
	// スタートボタンの設置
	doc = document;
	node = doc.body.firstChild;
	while(node.nodeName != "H2")
		node = node.nextSibling;
	node = node.appendChild(doc.createElement("button"));
	node.appendChild(doc.createTextNode("生成"));
	// ボタンのハンドラ
	function __replace(e) {
		if(e.type != "click" && e.keyCode != 8 && e.keyCode != 32) return;
		replaceView(dun);
		updateView(dun);
	}
	node.addEventListener("click",__replace,false);
	node.addEventListener("keydown",__replace,false);
	node = null;
	
	// キーコードをコマンドに変換→処理→成功したら再描画
	function __keydownHandler(e) {
		if(dun.closed) return;
		var cId = Dungeon.getCommandIdFromKeyCode(e.keyCode);
		if(!cId) return;
		e.preventDefault();
		e.stopPropagation();
		if(dun.executeCommand(cId)) updateView(dun);
	}
	// 一応、イベント登録はunloadで解除
	function __unloadHandler() {
		doc.removeEventListener("keydown",__keydownHandler,true);
		window.removeEventListener("unload",__unloadHandler,false);
		doc = dun = dun.bodyContent = __replace = __keydownHandler = __unloadHandler = null;
	}
	doc.addEventListener("keydown",__keydownHandler,true);
	window.addEventListener("unload",__unloadHandler,false);
}

// 振り分け。
if(window.addEventListener) {
	window.addEventListener("load",Init,false);
} else if(window.attachEvent) {
	Init = Init.toString();
	Init = Init.replace(/addEventListener\("(.*?)",(.+?),(?:true|false)/gm, "attachEvent(\"on$1\",$2");
	Init = Init.replace(/removeEventListener\("(.*?)",(.+?),(?:true|false)/gm, "detachEvent(\"on$1\",$2");
	Init = Init.replace(/\(e\)\s*?\{/gm, "(e){ e=event;");
	Init = Init.replace("e.preventDefault()", "e.returnValue=false");
	Init = Init.replace("e.stopPropagation()", "e.cancelBubble=true");
	eval("Init="+Init);
	window.attachEvent("onload",Init);
}

