Spaces:
Running
Running
/* | |
This is rot.js, the ROguelike Toolkit in JavaScript. | |
Version 0.4~dev, generated on Fri Mar 8 11:07:22 CET 2013. | |
*/ | |
/** | |
* @namespace Top-level ROT namespace | |
*/ | |
var ROT = { | |
/** | |
* @returns {bool} Is rot.js supported by this browser? | |
*/ | |
isSupported: function() { | |
return !!(document.createElement("canvas").getContext && Function.prototype.bind); | |
}, | |
/** Default with for display and map generators */ | |
DEFAULT_WIDTH: 80, | |
/** Default height for display and map generators */ | |
DEFAULT_HEIGHT: 25, | |
/** Directional constants. Ordering is important! */ | |
DIRS: { | |
"4": [ | |
[ 0, -1], | |
[ 1, 0], | |
[ 0, 1], | |
[-1, 0] | |
], | |
"8": [ | |
[ 0, -1], | |
[ 1, -1], | |
[ 1, 0], | |
[ 1, 1], | |
[ 0, 1], | |
[-1, 1], | |
[-1, 0], | |
[-1, -1] | |
], | |
"6": [ | |
[-1, -1], | |
[ 1, -1], | |
[ 2, 0], | |
[ 1, 1], | |
[-1, 1], | |
[-2, 0] | |
] | |
}, | |
/** Cancel key. */ | |
VK_CANCEL: 3, | |
/** Help key. */ | |
VK_HELP: 6, | |
/** Backspace key. */ | |
VK_BACK_SPACE: 8, | |
/** Tab key. */ | |
VK_TAB: 9, | |
/** 5 key on Numpad when NumLock is unlocked. Or on Mac, clear key which is positioned at NumLock key. */ | |
VK_CLEAR: 12, | |
/** Return/enter key on the main keyboard. */ | |
VK_RETURN: 13, | |
/** Reserved, but not used. */ | |
VK_ENTER: 14, | |
/** Shift key. */ | |
VK_SHIFT: 16, | |
/** Control key. */ | |
VK_CONTROL: 17, | |
/** Alt (Option on Mac) key. */ | |
VK_ALT: 18, | |
/** Pause key. */ | |
VK_PAUSE: 19, | |
/** Caps lock. */ | |
VK_CAPS_LOCK: 20, | |
/** Escape key. */ | |
VK_ESCAPE: 27, | |
/** Space bar. */ | |
VK_SPACE: 32, | |
/** Page Up key. */ | |
VK_PAGE_UP: 33, | |
/** Page Down key. */ | |
VK_PAGE_DOWN: 34, | |
/** End key. */ | |
VK_END: 35, | |
/** Home key. */ | |
VK_HOME: 36, | |
/** Left arrow. */ | |
VK_LEFT: 37, | |
/** Up arrow. */ | |
VK_UP: 38, | |
/** Right arrow. */ | |
VK_RIGHT: 39, | |
/** Down arrow. */ | |
VK_DOWN: 40, | |
/** Print Screen key. */ | |
VK_PRINTSCREEN: 44, | |
/** Ins(ert) key. */ | |
VK_INSERT: 45, | |
/** Del(ete) key. */ | |
VK_DELETE: 46, | |
/***/ | |
VK_0: 48, | |
/***/ | |
VK_1: 49, | |
/***/ | |
VK_2: 50, | |
/***/ | |
VK_3: 51, | |
/***/ | |
VK_4: 52, | |
/***/ | |
VK_5: 53, | |
/***/ | |
VK_6: 54, | |
/***/ | |
VK_7: 55, | |
/***/ | |
VK_8: 56, | |
/***/ | |
VK_9: 57, | |
/** Colon (:) key. Requires Gecko 15.0 */ | |
VK_COLON: 58, | |
/** Semicolon (;) key. */ | |
VK_SEMICOLON: 59, | |
/** Less-than (<) key. Requires Gecko 15.0 */ | |
VK_LESS_THAN: 60, | |
/** Equals (=) key. */ | |
VK_EQUALS: 61, | |
/** Greater-than (>) key. Requires Gecko 15.0 */ | |
VK_GREATER_THAN: 62, | |
/** Question mark (?) key. Requires Gecko 15.0 */ | |
VK_QUESTION_MARK: 63, | |
/** Atmark (@) key. Requires Gecko 15.0 */ | |
VK_AT: 64, | |
/***/ | |
VK_A: 65, | |
/***/ | |
VK_B: 66, | |
/***/ | |
VK_C: 67, | |
/***/ | |
VK_D: 68, | |
/***/ | |
VK_E: 69, | |
/***/ | |
VK_F: 70, | |
/***/ | |
VK_G: 71, | |
/***/ | |
VK_H: 72, | |
/***/ | |
VK_I: 73, | |
/***/ | |
VK_J: 74, | |
/***/ | |
VK_K: 75, | |
/***/ | |
VK_L: 76, | |
/***/ | |
VK_M: 77, | |
/***/ | |
VK_N: 78, | |
/***/ | |
VK_O: 79, | |
/***/ | |
VK_P: 80, | |
/***/ | |
VK_Q: 81, | |
/***/ | |
VK_R: 82, | |
/***/ | |
VK_S: 83, | |
/***/ | |
VK_T: 84, | |
/***/ | |
VK_U: 85, | |
/***/ | |
VK_V: 86, | |
/***/ | |
VK_W: 87, | |
/***/ | |
VK_X: 88, | |
/***/ | |
VK_Y: 89, | |
/***/ | |
VK_Z: 90, | |
/***/ | |
VK_CONTEXT_MENU: 93, | |
/** 0 on the numeric keypad. */ | |
VK_NUMPAD0: 96, | |
/** 1 on the numeric keypad. */ | |
VK_NUMPAD1: 97, | |
/** 2 on the numeric keypad. */ | |
VK_NUMPAD2: 98, | |
/** 3 on the numeric keypad. */ | |
VK_NUMPAD3: 99, | |
/** 4 on the numeric keypad. */ | |
VK_NUMPAD4: 100, | |
/** 5 on the numeric keypad. */ | |
VK_NUMPAD5: 101, | |
/** 6 on the numeric keypad. */ | |
VK_NUMPAD6: 102, | |
/** 7 on the numeric keypad. */ | |
VK_NUMPAD7: 103, | |
/** 8 on the numeric keypad. */ | |
VK_NUMPAD8: 104, | |
/** 9 on the numeric keypad. */ | |
VK_NUMPAD9: 105, | |
/** * on the numeric keypad. */ | |
VK_MULTIPLY: 106, | |
/** + on the numeric keypad. */ | |
VK_ADD: 107, | |
/***/ | |
VK_SEPARATOR: 108, | |
/** - on the numeric keypad. */ | |
VK_SUBTRACT: 109, | |
/** Decimal point on the numeric keypad. */ | |
VK_DECIMAL: 110, | |
/** / on the numeric keypad. */ | |
VK_DIVIDE: 111, | |
/** F1 key. */ | |
VK_F1: 112, | |
/** F2 key. */ | |
VK_F2: 113, | |
/** F3 key. */ | |
VK_F3: 114, | |
/** F4 key. */ | |
VK_F4: 115, | |
/** F5 key. */ | |
VK_F5: 116, | |
/** F6 key. */ | |
VK_F6: 117, | |
/** F7 key. */ | |
VK_F7: 118, | |
/** F8 key. */ | |
VK_F8: 119, | |
/** F9 key. */ | |
VK_F9: 120, | |
/** F10 key. */ | |
VK_F10: 121, | |
/** F11 key. */ | |
VK_F11: 122, | |
/** F12 key. */ | |
VK_F12: 123, | |
/** F13 key. */ | |
VK_F13: 124, | |
/** F14 key. */ | |
VK_F14: 125, | |
/** F15 key. */ | |
VK_F15: 126, | |
/** F16 key. */ | |
VK_F16: 127, | |
/** F17 key. */ | |
VK_F17: 128, | |
/** F18 key. */ | |
VK_F18: 129, | |
/** F19 key. */ | |
VK_F19: 130, | |
/** F20 key. */ | |
VK_F20: 131, | |
/** F21 key. */ | |
VK_F21: 132, | |
/** F22 key. */ | |
VK_F22: 133, | |
/** F23 key. */ | |
VK_F23: 134, | |
/** F24 key. */ | |
VK_F24: 135, | |
/** Num Lock key. */ | |
VK_NUM_LOCK: 144, | |
/** Scroll Lock key. */ | |
VK_SCROLL_LOCK: 145, | |
/** Circumflex (^) key. Requires Gecko 15.0 */ | |
VK_CIRCUMFLEX: 160, | |
/** Exclamation (!) key. Requires Gecko 15.0 */ | |
VK_EXCLAMATION: 161, | |
/** Double quote () key. Requires Gecko 15.0 */ | |
VK_DOUBLE_QUOTE: 162, | |
/** Hash (#) key. Requires Gecko 15.0 */ | |
VK_HASH: 163, | |
/** Dollar sign ($) key. Requires Gecko 15.0 */ | |
VK_DOLLAR: 164, | |
/** Percent (%) key. Requires Gecko 15.0 */ | |
VK_PERCENT: 165, | |
/** Ampersand (&) key. Requires Gecko 15.0 */ | |
VK_AMPERSAND: 166, | |
/** Underscore (_) key. Requires Gecko 15.0 */ | |
VK_UNDERSCORE: 167, | |
/** Open parenthesis (() key. Requires Gecko 15.0 */ | |
VK_OPEN_PAREN: 168, | |
/** Close parenthesis ()) key. Requires Gecko 15.0 */ | |
VK_CLOSE_PAREN: 169, | |
/* Asterisk (*) key. Requires Gecko 15.0 */ | |
VK_ASTERISK: 170, | |
/** Plus (+) key. Requires Gecko 15.0 */ | |
VK_PLUS: 171, | |
/** Pipe (|) key. Requires Gecko 15.0 */ | |
VK_PIPE: 172, | |
/** Hyphen-US/docs/Minus (-) key. Requires Gecko 15.0 */ | |
VK_HYPHEN_MINUS: 173, | |
/** Open curly bracket ({) key. Requires Gecko 15.0 */ | |
VK_OPEN_CURLY_BRACKET: 174, | |
/** Close curly bracket (}) key. Requires Gecko 15.0 */ | |
VK_CLOSE_CURLY_BRACKET: 175, | |
/** Tilde (~) key. Requires Gecko 15.0 */ | |
VK_TILDE: 176, | |
/** Comma (,) key. */ | |
VK_COMMA: 188, | |
/** Period (.) key. */ | |
VK_PERIOD: 190, | |
/** Slash (/) key. */ | |
VK_SLASH: 191, | |
/** Back tick (`) key. */ | |
VK_BACK_QUOTE: 192, | |
/** Open square bracket ([) key. */ | |
VK_OPEN_BRACKET: 219, | |
/** Back slash (\) key. */ | |
VK_BACK_SLASH: 220, | |
/** Close square bracket (]) key. */ | |
VK_CLOSE_BRACKET: 221, | |
/** Quote (''') key. */ | |
VK_QUOTE: 222, | |
/** Meta key on Linux, Command key on Mac. */ | |
VK_META: 224, | |
/** AltGr key on Linux. Requires Gecko 15.0 */ | |
VK_ALTGR: 225, | |
/** Windows logo key on Windows. Or Super or Hyper key on Linux. Requires Gecko 15.0 */ | |
VK_WIN: 91, | |
/** Linux support for this keycode was added in Gecko 4.0. */ | |
VK_KANA: 21, | |
/** Linux support for this keycode was added in Gecko 4.0. */ | |
VK_HANGUL: 21, | |
/** 英数 key on Japanese Mac keyboard. Requires Gecko 15.0 */ | |
VK_EISU: 22, | |
/** Linux support for this keycode was added in Gecko 4.0. */ | |
VK_JUNJA: 23, | |
/** Linux support for this keycode was added in Gecko 4.0. */ | |
VK_FINAL: 24, | |
/** Linux support for this keycode was added in Gecko 4.0. */ | |
VK_HANJA: 25, | |
/** Linux support for this keycode was added in Gecko 4.0. */ | |
VK_KANJI: 25, | |
/** Linux support for this keycode was added in Gecko 4.0. */ | |
VK_CONVERT: 28, | |
/** Linux support for this keycode was added in Gecko 4.0. */ | |
VK_NONCONVERT: 29, | |
/** Linux support for this keycode was added in Gecko 4.0. */ | |
VK_ACCEPT: 30, | |
/** Linux support for this keycode was added in Gecko 4.0. */ | |
VK_MODECHANGE: 31, | |
/** Linux support for this keycode was added in Gecko 4.0. */ | |
VK_SELECT: 41, | |
/** Linux support for this keycode was added in Gecko 4.0. */ | |
VK_PRINT: 42, | |
/** Linux support for this keycode was added in Gecko 4.0. */ | |
VK_EXECUTE: 43, | |
/** Linux support for this keycode was added in Gecko 4.0. */ | |
VK_SLEEP: 95 | |
}; | |
/** | |
* @namespace | |
* Contains text tokenization and breaking routines | |
*/ | |
ROT.Text = { | |
RE_COLORS: /%([bc]){([^}]*)}/g, | |
/* token types */ | |
TYPE_TEXT: 0, | |
TYPE_NEWLINE: 1, | |
TYPE_FG: 2, | |
TYPE_BG: 3, | |
/** | |
* Measure size of a resulting text block | |
*/ | |
measure: function(str, maxWidth) { | |
var result = {width:0, height:1}; | |
var tokens = this.tokenize(str, maxWidth); | |
var lineWidth = 0; | |
for (var i=0;i<tokens.length;i++) { | |
var token = tokens[i]; | |
switch (token.type) { | |
case this.TYPE_TEXT: | |
lineWidth += token.value.length; | |
break; | |
case this.TYPE_NEWLINE: | |
result.height++; | |
result.width = Math.max(result.width, lineWidth); | |
lineWidth = 0; | |
break; | |
} | |
} | |
result.width = Math.max(result.width, lineWidth); | |
return result; | |
}, | |
/** | |
* Convert string to a series of a formatting commands | |
*/ | |
tokenize: function(str, maxWidth) { | |
var result = []; | |
/* first tokenization pass - split texts and color formatting commands */ | |
var offset = 0; | |
str.replace(this.RE_COLORS, function(match, type, name, index) { | |
/* string before */ | |
var part = str.substring(offset, index); | |
if (part.length) { | |
result.push({ | |
type: ROT.Text.TYPE_TEXT, | |
value: part | |
}); | |
} | |
/* color command */ | |
result.push({ | |
type: (type == "c" ? ROT.Text.TYPE_FG : ROT.Text.TYPE_BG), | |
value: name.trim() | |
}); | |
offset = index + match.length; | |
return ""; | |
}); | |
/* last remaining part */ | |
var part = str.substring(offset); | |
if (part.length) { | |
result.push({ | |
type: ROT.Text.TYPE_TEXT, | |
value: part | |
}); | |
} | |
return this._breakLines(result, maxWidth); | |
}, | |
/* insert line breaks into first-pass tokenized data */ | |
_breakLines: function(tokens, maxWidth) { | |
if (!maxWidth) { maxWidth = Infinity; }; | |
var i = 0; | |
var lineLength = 0; | |
var lastTokenWithSpace = -1; | |
while (i < tokens.length) { /* take all text tokens, remove space, apply linebreaks */ | |
var token = tokens[i]; | |
if (token.type == ROT.Text.TYPE_NEWLINE) { /* reset */ | |
lineLength = 0; | |
lastTokenWithSpace = -1; | |
} | |
if (token.type != ROT.Text.TYPE_TEXT) { /* skip non-text tokens */ | |
i++; | |
continue; | |
} | |
/* remove spaces at the beginning of line */ | |
while (lineLength == 0 && token.value.charAt(0) == " ") { token.value = token.value.substring(1); } | |
/* forced newline? insert two new tokens after this one */ | |
var index = token.value.indexOf("\n"); | |
if (index != -1) { | |
token.value = this._breakInsideToken(tokens, i, index, true); | |
/* if there are spaces at the end, we must remove them (we do not want the line too long) */ | |
var arr = token.value.split(""); | |
while (arr[arr.length-1] == " ") { arr.pop(); } | |
token.value = arr.join(""); | |
} | |
/* token degenerated? */ | |
if (!token.value.length) { | |
tokens.splice(i, 1); | |
continue; | |
} | |
if (lineLength + token.value.length > maxWidth) { /* line too long, find a suitable breaking spot */ | |
/* is it possible to break within this token? */ | |
var index = -1; | |
while (1) { | |
var nextIndex = token.value.indexOf(" ", index+1); | |
if (nextIndex == -1) { break; } | |
if (lineLength + nextIndex > maxWidth) { break; } | |
index = nextIndex; | |
} | |
if (index != -1) { /* break at space within this one */ | |
token.value = this._breakInsideToken(tokens, i, index, true); | |
} else if (lastTokenWithSpace != -1) { /* is there a previous token where a break can occur? */ | |
var token = tokens[lastTokenWithSpace]; | |
var breakIndex = token.value.lastIndexOf(" "); | |
token.value = this._breakInsideToken(tokens, lastTokenWithSpace, breakIndex, true); | |
i = lastTokenWithSpace; | |
} else { /* force break in this token */ | |
token.value = this._breakInsideToken(tokens, i, maxWidth-lineLength, false); | |
} | |
} else { /* line not long, continue */ | |
lineLength += token.value.length; | |
if (token.value.indexOf(" ") != -1) { lastTokenWithSpace = i; } | |
} | |
i++; /* advance to next token */ | |
} | |
tokens.push({type: ROT.Text.TYPE_NEWLINE}); /* insert fake newline to fix the last text line */ | |
/* remove trailing space from text tokens before newlines */ | |
var lastTextToken = null; | |
for (var i=0;i<tokens.length;i++) { | |
var token = tokens[i]; | |
switch (token.type) { | |
case ROT.Text.TYPE_TEXT: lastTextToken = token; break; | |
case ROT.Text.TYPE_NEWLINE: | |
if (lastTextToken) { /* remove trailing space */ | |
var arr = lastTextToken.value.split(""); | |
while (arr[arr.length-1] == " ") { arr.pop(); } | |
lastTextToken.value = arr.join(""); | |
} | |
lastTextToken = null; | |
break; | |
} | |
} | |
tokens.pop(); /* remove fake token */ | |
return tokens; | |
}, | |
/** | |
* Create new tokens and insert them into the stream | |
* @param {object[]} tokens | |
* @param {int} tokenIndex Token being processed | |
* @param {int} breakIndex Index within current token's value | |
* @param {bool} removeBreakChar Do we want to remove the breaking character? | |
* @returns {string} remaining unbroken token value | |
*/ | |
_breakInsideToken: function(tokens, tokenIndex, breakIndex, removeBreakChar) { | |
var newBreakToken = { | |
type: ROT.Text.TYPE_NEWLINE | |
} | |
var newTextToken = { | |
type: ROT.Text.TYPE_TEXT, | |
value: tokens[tokenIndex].value.substring(breakIndex + (removeBreakChar ? 1 : 0)) | |
} | |
tokens.splice(tokenIndex+1, 0, newBreakToken, newTextToken); | |
return tokens[tokenIndex].value.substring(0, breakIndex); | |
} | |
} | |
/** | |
* @class Speed-based scheduler | |
*/ | |
ROT.Scheduler = function() { | |
this._items = []; | |
} | |
/** | |
* @param {object} item anything with "getSpeed" method | |
*/ | |
ROT.Scheduler.prototype.add = function(item) { | |
var o = { | |
item: item, | |
bucket: 1/item.getSpeed() | |
} | |
this._items.push(o); | |
return this; | |
} | |
/** | |
* Clear all actors | |
*/ | |
ROT.Scheduler.prototype.clear = function() { | |
this._items = []; | |
return this; | |
} | |
/** | |
* Remove a previously added item | |
* @param {object} item anything with "getSpeed" method | |
*/ | |
ROT.Scheduler.prototype.remove = function(item) { | |
var it = null; | |
for (var i=0;i<this._items.length;i++) { | |
it = this._items[i]; | |
if (it.item == item) { | |
this._items.splice(i, 1); | |
break; | |
} | |
} | |
return this; | |
} | |
/** | |
* Schedule next actor | |
* @returns {object} | |
*/ | |
ROT.Scheduler.prototype.next = function() { | |
if (!this._items.length) { return null; } | |
var minBucket = Infinity; | |
var minItem = null; | |
for (var i=0;i<this._items.length;i++) { | |
var item = this._items[i]; | |
if (item.bucket < minBucket) { | |
minBucket = item.bucket; | |
minItem = item; | |
} else if (item.bucket == minBucket && item.item.getSpeed() > minItem.item.getSpeed()) { | |
minItem = item; | |
} | |
} | |
if (minBucket) { /* non-zero value; subtract from all buckets */ | |
for (var i=0;i<this._items.length;i++) { | |
var item = this._items[i]; | |
item.bucket = Math.max(0, item.bucket - minBucket); | |
} | |
} | |
minItem.bucket += 1/minItem.item.getSpeed(); | |
return minItem.item; | |
} | |
/** | |
* @class Asynchronous main loop | |
*/ | |
ROT.Engine = function() { | |
this._scheduler = new ROT.Scheduler(); | |
this._lock = 1; | |
} | |
/** | |
* @param {object} actor Anything with "getSpeed" and "act" methods | |
*/ | |
ROT.Engine.prototype.addActor = function(actor) { | |
this._scheduler.add(actor); | |
return this; | |
} | |
/** | |
* Remove a previously added actor | |
* @param {object} actor | |
*/ | |
ROT.Engine.prototype.removeActor = function(actor) { | |
this._scheduler.remove(actor); | |
return this; | |
} | |
/** | |
* Remove all actors | |
*/ | |
ROT.Engine.prototype.clear = function() { | |
this._scheduler.clear(); | |
return this; | |
} | |
/** | |
* Start the main loop. When this call returns, the loop is locked. | |
*/ | |
ROT.Engine.prototype.start = function() { | |
return this.unlock(); | |
} | |
/** | |
* Interrupt the engine by an asynchronous action | |
*/ | |
ROT.Engine.prototype.lock = function() { | |
this._lock++; | |
} | |
/** | |
* Resume execution (paused by a previous lock) | |
*/ | |
ROT.Engine.prototype.unlock = function() { | |
if (!this._lock) { throw new Error("Cannot unlock unlocked engine"); } | |
this._lock--; | |
while (!this._lock) { | |
var actor = this._scheduler.next(); | |
if (!actor) { return this.lock(); } /* no actors */ | |
actor.act(); | |
} | |
return this; | |
} | |
/** | |
* @returns {any} Randomly picked item, null when length=0 | |
*/ | |
Array.prototype.random = function() { | |
if (!this.length) { return null; } | |
return this[Math.floor(ROT.RNG.getUniform() * this.length)]; | |
} | |
/** | |
* @returns {array} New array with randomized items | |
* FIXME destroys this! | |
*/ | |
Array.prototype.randomize = function() { | |
var result = []; | |
while (this.length) { | |
var index = this.indexOf(this.random()); | |
result.push(this.splice(index, 1)[0]); | |
} | |
return result; | |
} | |
if (!Date.now) { | |
/** | |
* @returns {int} Current timestamp (msec) | |
*/ | |
Date.now = function() { return +(new Date); } | |
} | |
/** | |
* Always positive modulus | |
* @param {int} n Modulus | |
* @returns {int} this modulo n | |
*/ | |
Number.prototype.mod = function(n) { | |
return ((this%n)+n)%n; | |
} | |
/** | |
* @returns {string} First letter capitalized | |
*/ | |
String.prototype.capitalize = function() { | |
return this.charAt(0).toUpperCase() + this.substring(1); | |
} | |
/** | |
* Left pad | |
* @param {string} [character="0"] | |
* @param {int} [count=2] | |
*/ | |
String.prototype.lpad = function(character, count) { | |
var ch = character || "0"; | |
var cnt = count || 2; | |
var s = ""; | |
while (s.length < (cnt - this.length)) { s += ch; } | |
s = s.substring(0, cnt-this.length); | |
return s+this; | |
} | |
/** | |
* Right pad | |
* @param {string} [character="0"] | |
* @param {int} [count=2] | |
*/ | |
String.prototype.rpad = function(character, count) { | |
var ch = character || "0"; | |
var cnt = count || 2; | |
var s = ""; | |
while (s.length < (cnt - this.length)) { s += ch; } | |
s = s.substring(0, cnt-this.length); | |
return this+s; | |
} | |
/** | |
* Format a string in a flexible way. Scans for %s strings and replaces them with arguments. List of patterns is modifiable via String.format.map. | |
* @param {string} template | |
* @param {any} [argv] | |
*/ | |
String.format = function(template) { | |
var map = String.format.map; | |
var args = Array.prototype.slice.call(arguments, 1); | |
var replacer = function(match, group1, group2, index) { | |
if (template.charAt(index-1) == "%") { return match.substring(1); } | |
if (!args.length) { return match; } | |
var obj = args[0]; | |
var group = group1 || group2; | |
var parts = group.split(","); | |
var name = parts.shift(); | |
var method = map[name.toLowerCase()]; | |
if (!method) { return match; } | |
var obj = args.shift(); | |
var replaced = obj[method].apply(obj, parts); | |
var first = name.charAt(0); | |
if (first != first.toLowerCase()) { replaced = replaced.capitalize(); } | |
return replaced; | |
} | |
return template.replace(/%(?:([a-z]+)|(?:{([^}]+)}))/gi, replacer); | |
} | |
String.format.map = { | |
"s": "toString" | |
} | |
/** | |
* Convenience shortcut to String.format(this) | |
*/ | |
String.prototype.format = function() { | |
var args = Array.prototype.slice.call(arguments); | |
args.unshift(this); | |
return String.format.apply(String, args); | |
} | |
if (!Object.create) { | |
/** | |
* ES5 Object.create | |
*/ | |
Object.create = function(o) { | |
var tmp = function() {}; | |
tmp.prototype = o; | |
return new tmp(); | |
}; | |
} | |
/** | |
* Sets prototype of this function to an instance of parent function | |
* @param {function} parent | |
*/ | |
Function.prototype.extend = function(parent) { | |
this.prototype = Object.create(parent.prototype); | |
this.prototype.constructor = this; | |
return this; | |
} | |
/** | |
* @class Visual map display | |
* @param {object} [options] | |
* @param {int} [options.width=ROT.DEFAULT_WIDTH] | |
* @param {int} [options.height=ROT.DEFAULT_HEIGHT] | |
* @param {int} [options.fontSize=15] | |
* @param {string} [options.fontFamily="monospace"] | |
* @param {string} [options.fontStyle=""] bold/italic/none/both | |
* @param {string} [options.fg="#ccc"] | |
* @param {string} [options.bg="#000"] | |
* @param {int} [options.fps=25] | |
* @param {float} [options.spacing=1] | |
* @param {float} [options.border=0] | |
* @param {string} [options.layout="rect"] | |
*/ | |
ROT.Display = function(options) { | |
var canvas = document.createElement("canvas"); | |
this._context = canvas.getContext("2d"); | |
this._data = {}; | |
this._dirty = false; /* false = nothing, true = all, object = dirty cells */ | |
this._options = {}; | |
this._backend = null; | |
var defaultOptions = { | |
width: ROT.DEFAULT_WIDTH, | |
height: ROT.DEFAULT_HEIGHT, | |
layout: "rect", | |
fontSize: 15, | |
fps: 25, | |
spacing: 1, | |
border: 0, | |
fontFamily: "monospace", | |
fontStyle: "", | |
fg: "#ccc", | |
bg: "#000" | |
}; | |
for (var p in options) { defaultOptions[p] = options[p]; } | |
this.setOptions(defaultOptions); | |
this.DEBUG = this.DEBUG.bind(this); | |
this._interval = setInterval(this._tick.bind(this), 1000/this._options.fps); | |
} | |
/** | |
* Debug helper, ideal as a map generator callback. Always bound to this. | |
* @param {int} x | |
* @param {int} y | |
* @param {int} what | |
*/ | |
ROT.Display.prototype.DEBUG = function(x, y, what) { | |
var colors = [this._options.bg, this._options.fg]; | |
this.draw(x, y, null, null, colors[what % colors.length]); | |
} | |
/** | |
* Clear the whole display (cover it with background color) | |
*/ | |
ROT.Display.prototype.clear = function() { | |
this._data = {}; | |
this._dirty = true; | |
} | |
/** | |
* @see ROT.Display | |
*/ | |
ROT.Display.prototype.setOptions = function(options) { | |
for (var p in options) { this._options[p] = options[p]; } | |
if (options.width || options.height || options.fontSize || options.fontFamily || options.spacing || options.layout) { | |
if (options.layout) { | |
this._backend = new ROT.Display[options.layout.capitalize()](this._context); | |
} | |
var font = (this._options.fontStyle ? this._options.fontStyle + " " : "") + this._options.fontSize + "px " + this._options.fontFamily; | |
this._context.font = font; | |
this._backend.compute(this._options); | |
this._context.font = font; | |
this._context.textAlign = "center"; | |
this._context.textBaseline = "middle"; | |
this._dirty = true; | |
} | |
return this; | |
} | |
/** | |
* Returns currently set options | |
* @returns {object} Current options object | |
*/ | |
ROT.Display.prototype.getOptions = function() { | |
return this._options; | |
} | |
/** | |
* Returns the DOM node of this display | |
* @returns {node} DOM node | |
*/ | |
ROT.Display.prototype.getContainer = function() { | |
return this._context.canvas; | |
} | |
/** | |
* Compute the maximum width/height to fit into a set of given constraints | |
* @param {int} availWidth Maximum allowed pixel width | |
* @param {int} availHeight Maximum allowed pixel height | |
* @returns {int[2]} cellWidth,cellHeight | |
*/ | |
ROT.Display.prototype.computeSize = function(availWidth, availHeight) { | |
return this._backend.computeSize(availWidth, availHeight, this._options); | |
} | |
/** | |
* Compute the maximum font size to fit into a set of given constraints | |
* @param {int} availWidth Maximum allowed pixel width | |
* @param {int} availHeight Maximum allowed pixel height | |
* @returns {int} fontSize | |
*/ | |
ROT.Display.prototype.computeFontSize = function(availWidth, availHeight) { | |
return this._backend.computeFontSize(availWidth, availHeight, this._options); | |
} | |
/** | |
* @param {int} x | |
* @param {int} y | |
* @param {string} ch | |
* @param {string} [fg] foreground color | |
* @param {string} [bg] background color | |
*/ | |
ROT.Display.prototype.draw = function(x, y, ch, fg, bg) { | |
if (!fg) { fg = this._options.fg; } | |
if (!bg) { bg = this._options.bg; } | |
this._data[x+","+y] = [x, y, ch, fg, bg]; | |
if (this._dirty === true) { return; } /* will already redraw everything */ | |
if (!this._dirty) { this._dirty = {}; } /* first! */ | |
this._dirty[x+","+y] = true; | |
} | |
/** | |
* Draws a text at given position. Optionally wraps at a maximum length. Currently does not work with hex layout. | |
* @param {int} x | |
* @param {int} y | |
* @param {string} text May contain color/background format specifiers, %c{name}/%b{name}, both optional. %c{}/%b{} resets to default. | |
* @param {int} [maxWidth] wrap at what width? | |
* @returns {int} lines drawn | |
*/ | |
ROT.Display.prototype.drawText = function(x, y, text, maxWidth) { | |
var fg = null; | |
var bg = null; | |
var cx = x; | |
var cy = y; | |
var lines = 1; | |
if (!maxWidth) { maxWidth = this._options.width-x; } | |
var tokens = ROT.Text.tokenize(text, maxWidth); | |
while (tokens.length) { /* interpret tokenized opcode stream */ | |
var token = tokens.shift(); | |
switch (token.type) { | |
case ROT.Text.TYPE_TEXT: | |
for (var i=0;i<token.value.length;i++) { | |
this.draw(cx++, cy, token.value.charAt(i), fg, bg); | |
} | |
break; | |
case ROT.Text.TYPE_FG: | |
fg = token.value || null; | |
break; | |
case ROT.Text.TYPE_BG: | |
bg = token.value || null; | |
break; | |
case ROT.Text.TYPE_NEWLINE: | |
cx = x; | |
cy++; | |
lines++ | |
break; | |
} | |
} | |
return lines; | |
} | |
/** | |
* Timer tick: update dirty parts | |
*/ | |
ROT.Display.prototype._tick = function() { | |
if (!this._dirty) { return; } | |
if (this._dirty === true) { /* draw all */ | |
this._context.fillStyle = this._options.bg; | |
this._context.fillRect(0, 0, this._context.canvas.width, this._context.canvas.height); | |
for (var id in this._data) { /* redraw cached data */ | |
this._draw(id, false); | |
} | |
} else { /* draw only dirty */ | |
for (var key in this._dirty) { | |
this._draw(key, true); | |
} | |
} | |
this._dirty = false; | |
} | |
/** | |
* @param {string} key What to draw | |
* @param {bool} clearBefore Is it necessary to clean before? | |
*/ | |
ROT.Display.prototype._draw = function(key, clearBefore) { | |
var data = this._data[key]; | |
if (data[4] != this._options.bg) { clearBefore = true; } | |
this._backend.draw(data, clearBefore); | |
} | |
/** | |
* @class Abstract display backend module | |
* @private | |
*/ | |
ROT.Display.Backend = function(context) { | |
this._context = context; | |
} | |
ROT.Display.Backend.prototype.compute = function(options) { | |
} | |
ROT.Display.Backend.prototype.draw = function(data, clearBefore) { | |
} | |
ROT.Display.Backend.prototype.computeSize = function(availWidth, availHeight) { | |
} | |
ROT.Display.Backend.prototype.computeFontSize = function(availWidth, availHeight) { | |
} | |
/** | |
* @class Rectangular backend | |
* @private | |
*/ | |
ROT.Display.Rect = function(context) { | |
ROT.Display.Backend.call(this, context); | |
this._spacingX = 0; | |
this._spacingY = 0; | |
this._canvasCache = {}; | |
this._options = {}; | |
} | |
ROT.Display.Rect.extend(ROT.Display.Backend); | |
ROT.Display.Rect.cache = false; | |
ROT.Display.Rect.prototype.compute = function(options) { | |
this._canvasCache = {}; | |
this._options = options; | |
var charWidth = Math.ceil(this._context.measureText("W").width); | |
this._spacingX = Math.ceil(options.spacing * charWidth); | |
this._spacingY = Math.ceil(options.spacing * options.fontSize); | |
this._context.canvas.width = options.width * this._spacingX; | |
this._context.canvas.height = options.height * this._spacingY; | |
} | |
ROT.Display.Rect.prototype.draw = function(data, clearBefore) { | |
if (this.constructor.cache) { | |
this._drawWithCache(data, clearBefore); | |
} else { | |
this._drawNoCache(data, clearBefore); | |
} | |
} | |
ROT.Display.Rect.prototype._drawWithCache = function(data, clearBefore) { | |
var x = data[0]; | |
var y = data[1]; | |
var ch = data[2]; | |
var fg = data[3]; | |
var bg = data[4]; | |
var hash = ""+ch+fg+bg; | |
if (hash in this._canvasCache) { | |
var canvas = this._canvasCache[hash]; | |
} else { | |
var b = this._options.border; | |
var canvas = document.createElement("canvas"); | |
var ctx = canvas.getContext("2d"); | |
canvas.width = this._spacingX; | |
canvas.height = this._spacingY; | |
ctx.fillStyle = bg; | |
ctx.fillRect(b, b, canvas.width-b, canvas.height-b); | |
if (ch) { | |
ctx.fillStyle = fg; | |
ctx.font = this._context.font; | |
ctx.textAlign = "center"; | |
ctx.textBaseline = "middle"; | |
ctx.fillText(ch, this._spacingX/2, this._spacingY/2); | |
} | |
this._canvasCache[hash] = canvas; | |
} | |
this._context.drawImage(canvas, x*this._spacingX, y*this._spacingY); | |
} | |
ROT.Display.Rect.prototype._drawNoCache = function(data, clearBefore) { | |
var x = data[0]; | |
var y = data[1]; | |
var ch = data[2]; | |
var fg = data[3]; | |
var bg = data[4]; | |
if (clearBefore) { | |
var b = this._options.border; | |
this._context.fillStyle = bg; | |
this._context.fillRect(x*this._spacingX + b, y*this._spacingY + b, this._spacingX - b, this._spacingY - b); | |
} | |
if (!ch) { return; } | |
this._context.fillStyle = fg; | |
this._context.fillText(ch, (x+0.5) * this._spacingX, (y+0.5) * this._spacingY); | |
} | |
ROT.Display.Rect.prototype.computeSize = function(availWidth, availHeight) { | |
var width = Math.floor(availWidth / this._spacingX); | |
var height = Math.floor(availHeight / this._spacingY); | |
return [width, height]; | |
} | |
ROT.Display.Rect.prototype.computeFontSize = function(availWidth, availHeight) { | |
var boxWidth = Math.floor(availWidth / this._options.width); | |
var boxHeight = Math.floor(availHeight / this._options.height); | |
/* compute char ratio */ | |
var oldFont = this._context.font; | |
this._context.font = "100px " + this._options.fontFamily; | |
var width = Math.ceil(this._context.measureText("W").width); | |
this._context.font = oldFont; | |
var ratio = width / 100; | |
var widthFraction = ratio * boxHeight / boxWidth; | |
if (widthFraction > 1) { /* too wide with current aspect ratio */ | |
boxHeight = Math.floor(boxHeight / widthFraction); | |
} | |
return Math.floor(boxHeight / this._options.spacing); | |
} | |
/** | |
* @class Hexagonal backend | |
* @private | |
*/ | |
ROT.Display.Hex = function(context) { | |
ROT.Display.Backend.call(this, context); | |
this._spacingX = 0; | |
this._spacingY = 0; | |
this._hexSize = 0; | |
this._options = {}; | |
} | |
ROT.Display.Hex.extend(ROT.Display.Backend); | |
ROT.Display.Hex.prototype.compute = function(options) { | |
this._options = options; | |
var charWidth = Math.ceil(this._context.measureText("W").width); | |
this._hexSize = Math.floor(options.spacing * (options.fontSize + charWidth/Math.sqrt(3)) / 2); | |
this._spacingX = this._hexSize * Math.sqrt(3) / 2; | |
this._spacingY = this._hexSize * 1.5; | |
this._context.canvas.width = Math.ceil( (options.width + 1) * this._spacingX ); | |
this._context.canvas.height = Math.ceil( (options.height - 1) * this._spacingY + 2*this._hexSize ); | |
} | |
ROT.Display.Hex.prototype.draw = function(data, clearBefore) { | |
var x = data[0]; | |
var y = data[1]; | |
var ch = data[2]; | |
var fg = data[3]; | |
var bg = data[4]; | |
var cx = (x+1) * this._spacingX; | |
var cy = y * this._spacingY + this._hexSize; | |
if (clearBefore) { | |
this._context.fillStyle = bg; | |
this._fill(cx, cy); | |
} | |
if (!ch) { return; } | |
this._context.fillStyle = fg; | |
this._context.fillText(ch, cx, cy); | |
} | |
ROT.Display.Hex.prototype.computeSize = function(availWidth, availHeight) { | |
var width = Math.floor(availWidth / this._spacingX) - 1; | |
var height = Math.floor((availHeight - 2*this._hexSize) / this._spacingY + 1); | |
return [width, height]; | |
} | |
ROT.Display.Hex.prototype.computeFontSize = function(availWidth, availHeight) { | |
var hexSizeWidth = 2*availWidth / ((this._options.width+1) * Math.sqrt(3)) - 1; | |
var hexSizeHeight = availHeight / (2 + 1.5*(this._options.height-1)); | |
var hexSize = Math.min(hexSizeWidth, hexSizeHeight); | |
/* compute char ratio */ | |
var oldFont = this._context.font; | |
this._context.font = "100px " + this._options.fontFamily; | |
var width = Math.ceil(this._context.measureText("W").width); | |
this._context.font = oldFont; | |
var ratio = width / 100; | |
hexSize = Math.floor(hexSize)+1; /* closest larger hexSize */ | |
var fontSize = 2*hexSize / (this._options.spacing * (1 + ratio / Math.sqrt(3))); | |
/* closest smaller fontSize */ | |
return Math.ceil(fontSize)-1; | |
} | |
ROT.Display.Hex.prototype._fill = function(cx, cy) { | |
var a = this._hexSize; | |
var b = this._options.border; | |
this._context.beginPath(); | |
this._context.moveTo(cx, cy-a+b); | |
this._context.lineTo(cx + this._spacingX - b, cy-a/2+b); | |
this._context.lineTo(cx + this._spacingX - b, cy+a/2-b); | |
this._context.lineTo(cx, cy+a-b); | |
this._context.lineTo(cx - this._spacingX + b, cy+a/2-b); | |
this._context.lineTo(cx - this._spacingX + b, cy-a/2+b); | |
this._context.lineTo(cx, cy-a+b); | |
this._context.fill(); | |
} | |
/** | |
* @namespace | |
* This code is an implementation of Alea algorithm; (C) 2010 Johannes Baagøe. | |
* Alea is licensed according to the http://en.wikipedia.org/wiki/MIT_License. | |
*/ | |
ROT.RNG = { | |
/** | |
* @returns {number} | |
*/ | |
getSeed: function() { | |
return this._seed; | |
}, | |
/** | |
* @param {number} seed Seed the number generator | |
*/ | |
setSeed: function(seed) { | |
seed = (seed < 1 ? 1/seed : seed); | |
this._seed = seed; | |
this._s0 = (seed >>> 0) * this._frac; | |
seed = (seed*69069 + 1) >>> 0; | |
this._s1 = seed * this._frac; | |
seed = (seed*69069 + 1) >>> 0; | |
this._s2 = seed * this._frac; | |
this._c = 1; | |
return this; | |
}, | |
/** | |
* @returns {float} Pseudorandom value [0,1), uniformly distributed | |
*/ | |
getUniform: function() { | |
var t = 2091639 * this._s0 + this._c * this._frac; | |
this._s0 = this._s1; | |
this._s1 = this._s2; | |
this._c = t | 0; | |
this._s2 = t - this._c; | |
return this._s2; | |
}, | |
/** | |
* @param {float} [mean=0] Mean value | |
* @param {float} [stddev=1] Standard deviation. ~95% of the absolute values will be lower than 2*stddev. | |
* @returns {float} A normally distributed pseudorandom value | |
*/ | |
getNormal: function(mean, stddev) { | |
do { | |
var u = 2*this.getUniform()-1; | |
var v = 2*this.getUniform()-1; | |
var r = u*u + v*v; | |
} while (r > 1 || r == 0); | |
var gauss = u * Math.sqrt(-2*Math.log(r)/r); | |
return (mean || 0) + gauss*(stddev || 1); | |
}, | |
/** | |
* @returns {int} Pseudorandom value [1,100] inclusive, uniformly distributed | |
*/ | |
getPercentage: function() { | |
return 1 + Math.floor(this.getUniform()*100); | |
}, | |
/** | |
* @param {object} data key=whatever, value=weight (relative probability) | |
* @returns {string} whatever | |
*/ | |
getWeightedValue: function(data) { | |
var avail = []; | |
var total = 0; | |
for (var id in data) { | |
total += data[id]; | |
} | |
var random = Math.floor(this.getUniform()*total); | |
var part = 0; | |
for (var id in data) { | |
part += data[id]; | |
if (random < part) { return id; } | |
} | |
return null; | |
}, | |
/** | |
* Get RNG state. Useful for storing the state and re-setting it via setState. | |
* @returns {?} Internal state | |
*/ | |
getState: function() { | |
return [this._s0, this._s1, this._s2, this._c]; | |
}, | |
/** | |
* Set a previously retrieved state. | |
* @param {?} state | |
*/ | |
setState: function(state) { | |
this._s0 = state[0]; | |
this._s1 = state[1]; | |
this._s2 = state[2]; | |
this._c = state[3]; | |
return this; | |
}, | |
_s0: 0, | |
_s1: 0, | |
_s2: 0, | |
_c: 0, | |
_frac: 2.3283064365386963e-10 /* 2^-32 */ | |
} | |
ROT.RNG.setSeed(Date.now()); | |
/** | |
* @class (Markov process)-based string generator. | |
* Copied from a <a href="http://www.roguebasin.roguelikedevelopment.org/index.php?title=Names_from_a_high_order_Markov_Process_and_a_simplified_Katz_back-off_scheme">RogueBasin article</a>. | |
* Offers configurable order and prior. | |
* @param {object} [options] | |
* @param {bool} [options.words=false] Use word mode? | |
* @param {int} [options.order=3] | |
* @param {float} [options.prior=0.001] | |
*/ | |
ROT.StringGenerator = function(options) { | |
this._options = { | |
words: false, | |
order: 3, | |
prior: 0.001 | |
} | |
for (var p in options) { this._options[p] = options[p]; } | |
this._boundary = String.fromCharCode(0); | |
this._suffix = this._boundary; | |
this._prefix = []; | |
for (var i=0;i<this._options.order;i++) { this._prefix.push(this._boundary); } | |
this._priorValues = {}; | |
this._priorValues[this._boundary] = this._options.prior; | |
this._data = {}; | |
} | |
/** | |
* Remove all learning data | |
*/ | |
ROT.StringGenerator.prototype.clear = function() { | |
this._data = {}; | |
this._priorValues = {}; | |
} | |
/** | |
* @returns {string} Generated string | |
*/ | |
ROT.StringGenerator.prototype.generate = function() { | |
var result = [this._sample(this._prefix)]; | |
while (result[result.length-1] != this._boundary) { | |
result.push(this._sample(result)); | |
} | |
return this._join(result.slice(0, -1)); | |
} | |
/** | |
* Observe (learn) a string from a training set | |
*/ | |
ROT.StringGenerator.prototype.observe = function(string) { | |
var tokens = this._split(string); | |
for (var i=0; i<tokens.length; i++) { | |
this._priorValues[tokens[i]] = this._options.prior; | |
} | |
tokens = this._prefix.concat(tokens).concat(this._suffix); /* add boundary symbols */ | |
for (var i=this._options.order; i<tokens.length; i++) { | |
var context = tokens.slice(i-this._options.order, i); | |
var event = tokens[i]; | |
for (var j=0; j<context.length; j++) { | |
var subcontext = context.slice(j); | |
this._observeEvent(subcontext, event); | |
} | |
} | |
} | |
ROT.StringGenerator.prototype.getStats = function() { | |
var parts = []; | |
var priorCount = 0; | |
for (var p in this._priorValues) { priorCount++; } | |
priorCount--; /* boundary */ | |
parts.push("distinct samples: " + priorCount); | |
var dataCount = 0; | |
var eventCount = 0; | |
for (var p in this._data) { | |
dataCount++; | |
for (var key in this._data[p]) { | |
eventCount++; | |
} | |
} | |
parts.push("dictionary size (contexts): " + dataCount); | |
parts.push("dictionary size (events): " + eventCount); | |
return parts.join(", "); | |
} | |
/** | |
* @param {string} | |
* @returns {string[]} | |
*/ | |
ROT.StringGenerator.prototype._split = function(str) { | |
return str.split(this._options.words ? /\s+/ : ""); | |
} | |
/** | |
* @param {string[]} | |
* @returns {string} | |
*/ | |
ROT.StringGenerator.prototype._join = function(arr) { | |
return arr.join(this._options.words ? " " : ""); | |
} | |
/** | |
* @param {string[]} context | |
* @param {string} event | |
*/ | |
ROT.StringGenerator.prototype._observeEvent = function(context, event) { | |
var key = this._join(context); | |
if (!(key in this._data)) { this._data[key] = {}; } | |
var data = this._data[key]; | |
if (!(event in data)) { data[event] = 0; } | |
data[event]++; | |
} | |
/** | |
* @param {string[]} | |
* @returns {string} | |
*/ | |
ROT.StringGenerator.prototype._sample = function(context) { | |
context = this._backoff(context); | |
var key = this._join(context); | |
var data = this._data[key]; | |
var available = {}; | |
if (this._options.prior) { | |
for (var event in this._priorValues) { available[event] = this._priorValues[event]; } | |
for (var event in data) { available[event] += data[event]; } | |
} else { | |
available = data; | |
} | |
return this._pickRandom(available); | |
} | |
/** | |
* @param {string[]} | |
* @returns {string[]} | |
*/ | |
ROT.StringGenerator.prototype._backoff = function(context) { | |
if (context.length > this._options.order) { | |
context = context.slice(-this._options.order); | |
} else if (context.length < this._options.order) { | |
context = this._prefix.slice(0, this._options.order - context.length).concat(context); | |
} | |
while (!(this._join(context) in this._data) && context.length > 0) { context = context.slice(1); } | |
return context; | |
} | |
ROT.StringGenerator.prototype._pickRandom = function(data) { | |
var total = 0; | |
for (var id in data) { | |
total += data[id]; | |
} | |
var random = ROT.RNG.getUniform()*total; | |
var part = 0; | |
for (var id in data) { | |
part += data[id]; | |
if (random < part) { return id; } | |
} | |
} | |
/** | |
* @class Base map generator | |
* @param {int} [width=ROT.DEFAULT_WIDTH] | |
* @param {int} [height=ROT.DEFAULT_HEIGHT] | |
*/ | |
ROT.Map = function(width, height) { | |
this._width = width || ROT.DEFAULT_WIDTH; | |
this._height = height || ROT.DEFAULT_HEIGHT; | |
}; | |
ROT.Map.prototype.create = function(callback) {} | |
ROT.Map.prototype._fillMap = function(value) { | |
var map = []; | |
for (var i=0;i<this._width;i++) { | |
map.push([]); | |
for (var j=0;j<this._height;j++) { map[i].push(value); } | |
} | |
return map; | |
} | |
/** | |
* @class Simple empty rectangular room | |
* @augments ROT.Map | |
*/ | |
ROT.Map.Arena = function(width, height) { | |
ROT.Map.call(this, width, height); | |
} | |
ROT.Map.Arena.extend(ROT.Map); | |
ROT.Map.Arena.prototype.create = function(callback) { | |
var w = this._width-1; | |
var h = this._height-1; | |
for (var i=0;i<=w;i++) { | |
for (var j=0;j<=h;j++) { | |
var empty = (i && j && i<w && j<h); | |
callback(i, j, empty ? 0 : 1); | |
} | |
} | |
return this; | |
} | |
/** | |
* @class Recursively divided maze, http://en.wikipedia.org/wiki/Maze_generation_algorithm#Recursive_division_method | |
* @augments ROT.Map | |
*/ | |
ROT.Map.DividedMaze = function(width, height) { | |
ROT.Map.call(this, width, height); | |
this._stack = []; | |
} | |
ROT.Map.DividedMaze.extend(ROT.Map); | |
ROT.Map.DividedMaze.prototype.create = function(callback) { | |
var w = this._width; | |
var h = this._height; | |
this._map = []; | |
for (var i=0;i<w;i++) { | |
this._map.push([]); | |
for (var j=0;j<h;j++) { | |
var border = (i == 0 || j == 0 || i+1 == w || j+1 == h); | |
this._map[i].push(border ? 1 : 0); | |
} | |
} | |
this._stack = [ | |
[1, 1, w-2, h-2] | |
]; | |
this._process(); | |
for (var i=0;i<w;i++) { | |
for (var j=0;j<h;j++) { | |
callback(i, j, this._map[i][j]); | |
} | |
} | |
this._map = null; | |
return this; | |
} | |
ROT.Map.DividedMaze.prototype._process = function() { | |
while (this._stack.length) { | |
var room = this._stack.shift(); /* [left, top, right, bottom] */ | |
this._partitionRoom(room); | |
} | |
} | |
ROT.Map.DividedMaze.prototype._partitionRoom = function(room) { | |
var availX = []; | |
var availY = []; | |
for (var i=room[0]+1;i<room[2];i++) { | |
var top = this._map[i][room[1]-1]; | |
var bottom = this._map[i][room[3]+1]; | |
if (top && bottom && !(i % 2)) { availX.push(i); } | |
} | |
for (var j=room[1]+1;j<room[3];j++) { | |
var left = this._map[room[0]-1][j]; | |
var right = this._map[room[2]+1][j]; | |
if (left && right && !(j % 2)) { availY.push(j); } | |
} | |
if (!availX.length || !availY.length) { return; } | |
var x = availX.random(); | |
var y = availY.random(); | |
this._map[x][y] = 1; | |
var walls = []; | |
var w = []; walls.push(w); /* left part */ | |
for (var i=room[0]; i<x; i++) { | |
this._map[i][y] = 1; | |
w.push([i, y]); | |
} | |
var w = []; walls.push(w); /* right part */ | |
for (var i=x+1; i<=room[2]; i++) { | |
this._map[i][y] = 1; | |
w.push([i, y]); | |
} | |
var w = []; walls.push(w); /* top part */ | |
for (var j=room[1]; j<y; j++) { | |
this._map[x][j] = 1; | |
w.push([x, j]); | |
} | |
var w = []; walls.push(w); /* bottom part */ | |
for (var j=y+1; j<=room[3]; j++) { | |
this._map[x][j] = 1; | |
w.push([x, j]); | |
} | |
var solid = walls.random(); | |
for (var i=0;i<walls.length;i++) { | |
var w = walls[i]; | |
if (w == solid) { continue; } | |
var hole = w.random(); | |
this._map[hole[0]][hole[1]] = 0; | |
} | |
this._stack.push([room[0], room[1], x-1, y-1]); /* left top */ | |
this._stack.push([x+1, room[1], room[2], y-1]); /* right top */ | |
this._stack.push([room[0], y+1, x-1, room[3]]); /* left bottom */ | |
this._stack.push([x+1, y+1, room[2], room[3]]); /* right bottom */ | |
} | |
/** | |
* @class Icey's Maze generator | |
* See http://www.roguebasin.roguelikedevelopment.org/index.php?title=Simple_maze for explanation | |
* @augments ROT.Map | |
*/ | |
ROT.Map.IceyMaze = function(width, height, regularity) { | |
ROT.Map.call(this, width, height); | |
this._regularity = regularity || 0; | |
} | |
ROT.Map.IceyMaze.extend(ROT.Map); | |
ROT.Map.IceyMaze.prototype.create = function(callback) { | |
var width = this._width; | |
var height = this._height; | |
var map = this._fillMap(1); | |
width -= (width % 2 ? 1 : 2); | |
height -= (height % 2 ? 1 : 2); | |
var cx = 0; | |
var cy = 0; | |
var nx = 0; | |
var ny = 0; | |
var done = 0; | |
var blocked = false; | |
var dirs = [ | |
[0, 0], | |
[0, 0], | |
[0, 0], | |
[0, 0] | |
]; | |
do { | |
cx = 1 + 2*Math.floor(ROT.RNG.getUniform()*(width-1) / 2); | |
cy = 1 + 2*Math.floor(ROT.RNG.getUniform()*(height-1) / 2); | |
if (!done) { map[cx][cy] = 0; } | |
if (!map[cx][cy]) { | |
this._randomize(dirs); | |
do { | |
if (Math.floor(ROT.RNG.getUniform()*(this._regularity+1)) == 0) { this._randomize(dirs); } | |
blocked = true; | |
for (var i=0;i<4;i++) { | |
nx = cx + dirs[i][0]*2; | |
ny = cy + dirs[i][1]*2; | |
if (this._isFree(map, nx, ny, width, height)) { | |
map[nx][ny] = 0; | |
map[cx + dirs[i][0]][cy + dirs[i][1]] = 0; | |
cx = nx; | |
cy = ny; | |
blocked = false; | |
done++; | |
break; | |
} | |
} | |
} while (!blocked); | |
} | |
} while (done+1 < width*height/4); | |
for (var i=0;i<this._width;i++) { | |
for (var j=0;j<this._height;j++) { | |
callback(i, j, map[i][j]); | |
} | |
} | |
this._map = null; | |
return this; | |
} | |
ROT.Map.IceyMaze.prototype._randomize = function(dirs) { | |
for (var i=0;i<4;i++) { | |
dirs[i][0] = 0; | |
dirs[i][1] = 0; | |
} | |
switch (Math.floor(ROT.RNG.getUniform()*4)) { | |
case 0: | |
dirs[0][0] = -1; dirs[1][0] = 1; | |
dirs[2][1] = -1; dirs[3][1] = 1; | |
break; | |
case 1: | |
dirs[3][0] = -1; dirs[2][0] = 1; | |
dirs[1][1] = -1; dirs[0][1] = 1; | |
break; | |
case 2: | |
dirs[2][0] = -1; dirs[3][0] = 1; | |
dirs[0][1] = -1; dirs[1][1] = 1; | |
break; | |
case 3: | |
dirs[1][0] = -1; dirs[0][0] = 1; | |
dirs[3][1] = -1; dirs[2][1] = 1; | |
break; | |
} | |
} | |
ROT.Map.IceyMaze.prototype._isFree = function(map, x, y, width, height) { | |
if (x < 1 || y < 1 || x >= width || y >= height) { return false; } | |
return map[x][y]; | |
} | |
/** | |
* @class Maze generator - Eller's algorithm | |
* See http://homepages.cwi.nl/~tromp/maze.html for explanation | |
* @augments ROT.Map | |
*/ | |
ROT.Map.EllerMaze = function(width, height) { | |
ROT.Map.call(this, width, height); | |
} | |
ROT.Map.EllerMaze.extend(ROT.Map); | |
ROT.Map.EllerMaze.prototype.create = function(callback) { | |
var map = this._fillMap(1); | |
var w = Math.ceil((this._width-2)/2); | |
var rand = 9/24; | |
var L = []; | |
var R = []; | |
for (var i=0;i<w;i++) { | |
L.push(i); | |
R.push(i); | |
} | |
L.push(w-1); /* fake stop-block at the right side */ | |
for (var j=1;j+3<this._height;j+=2) { | |
/* one row */ | |
for (var i=0;i<w;i++) { | |
/* cell coords (will be always empty) */ | |
var x = 2*i+1; | |
var y = j; | |
map[x][y] = 0; | |
/* right connection */ | |
if (i != L[i+1] && ROT.RNG.getUniform() > rand) { | |
this._addToList(i, L, R); | |
map[x+1][y] = 0; | |
} | |
/* bottom connection */ | |
if (i != L[i] && ROT.RNG.getUniform() > rand) { | |
/* remove connection */ | |
this._removeFromList(i, L, R); | |
} else { | |
/* create connection */ | |
map[x][y+1] = 0; | |
} | |
} | |
} | |
/* last row */ | |
for (var i=0;i<w;i++) { | |
/* cell coords (will be always empty) */ | |
var x = 2*i+1; | |
var y = j; | |
map[x][y] = 0; | |
/* right connection */ | |
if (i != L[i+1] && (i == L[i] || ROT.RNG.getUniform() > rand)) { | |
/* dig right also if the cell is separated, so it gets connected to the rest of maze */ | |
this._addToList(i, L, R); | |
map[x+1][y] = 0; | |
} | |
this._removeFromList(i, L, R); | |
} | |
for (var i=0;i<this._width;i++) { | |
for (var j=0;j<this._height;j++) { | |
callback(i, j, map[i][j]); | |
} | |
} | |
return this; | |
} | |
/** | |
* Remove "i" from its list | |
*/ | |
ROT.Map.EllerMaze.prototype._removeFromList = function(i, L, R) { | |
R[L[i]] = R[i]; | |
L[R[i]] = L[i]; | |
R[i] = i; | |
L[i] = i; | |
} | |
/** | |
* Join lists with "i" and "i+1" | |
*/ | |
ROT.Map.EllerMaze.prototype._addToList = function(i, L, R) { | |
R[L[i+1]] = R[i]; | |
L[R[i]] = L[i+1]; | |
R[i] = i+1; | |
L[i+1] = i; | |
} | |
/** | |
* @class Cellular automaton map generator | |
* @augments ROT.Map | |
* @param {int} [width=ROT.DEFAULT_WIDTH] | |
* @param {int} [height=ROT.DEFAULT_HEIGHT] | |
* @param {object} [options] Options | |
* @param {int[]} [options.born] List of neighbor counts for a new cell to be born in empty space | |
* @param {int[]} [options.survive] List of neighbor counts for an existing cell to survive | |
* @param {int} [options.topology] Topology 4 or 6 or 8 | |
*/ | |
ROT.Map.Cellular = function(width, height, options) { | |
ROT.Map.call(this, width, height); | |
this._options = { | |
born: [5, 6, 7, 8], | |
survive: [4, 5, 6, 7, 8], | |
topology: 8 | |
}; | |
for (var p in options) { this._options[p] = options[p]; } | |
this._dirs = ROT.DIRS[this._options.topology]; | |
this._map = this._fillMap(0); | |
} | |
ROT.Map.Cellular.extend(ROT.Map); | |
/** | |
* Fill the map with random values | |
* @param {float} probability Probability for a cell to become alive; 0 = all empty, 1 = all full | |
*/ | |
ROT.Map.Cellular.prototype.randomize = function(probability) { | |
for (var i=0;i<this._width;i++) { | |
for (var j=0;j<this._height;j++) { | |
this._map[i][j] = (ROT.RNG.getUniform() < probability ? 1 : 0); | |
} | |
} | |
return this; | |
} | |
ROT.Map.Cellular.prototype.set = function(x, y, value) { | |
this._map[x][y] = value; | |
} | |
ROT.Map.Cellular.prototype.create = function(callback) { | |
var newMap = this._fillMap(0); | |
var born = this._options.born; | |
var survive = this._options.survive; | |
for (var j=0;j<this._height;j++) { | |
var widthStep = 1; | |
var widthStart = 0; | |
if (this._options.topology == 6) { | |
widthStep = 2; | |
widthStart = j%2; | |
} | |
for (var i=widthStart; i<this._width; i+=widthStep) { | |
var cur = this._map[i][j]; | |
var ncount = this._getNeighbors(i, j); | |
if (cur && survive.indexOf(ncount) != -1) { /* survive */ | |
newMap[i][j] = 1; | |
} else if (!cur && born.indexOf(ncount) != -1) { /* born */ | |
newMap[i][j] = 1; | |
} | |
if (callback) { callback(i, j, newMap[i][j]); } | |
} | |
} | |
this._map = newMap; | |
} | |
/** | |
* Get neighbor count at [i,j] in this._map | |
*/ | |
ROT.Map.Cellular.prototype._getNeighbors = function(cx, cy) { | |
var result = 0; | |
for (var i=0;i<this._dirs.length;i++) { | |
var dir = this._dirs[i]; | |
var x = cx + dir[0]; | |
var y = cy + dir[1]; | |
if (x < 0 || x >= this._width || x < 0 || y >= this._width) { continue; } | |
result += (this._map[x][y] == 1 ? 1 : 0); | |
} | |
return result; | |
} | |
/** | |
* @class Dungeon map: has rooms and corridors | |
* @augments ROT.Map | |
*/ | |
ROT.Map.Dungeon = function(width, height) { | |
ROT.Map.call(this, width, height); | |
this._rooms = []; /* list of all rooms */ | |
this._corridors = []; | |
} | |
ROT.Map.Dungeon.extend(ROT.Map); | |
/** | |
* Get all generated rooms | |
* @returns {ROT.Map.Feature.Room[]} | |
*/ | |
ROT.Map.Dungeon.prototype.getRooms = function() { | |
return this._rooms; | |
} | |
/** | |
* Get all generated corridors | |
* @returns {ROT.Map.Feature.Corridor[]} | |
*/ | |
ROT.Map.Dungeon.prototype.getCorridors = function() { | |
return this._corridors; | |
} | |
/** | |
* @class Random dungeon generator using human-like digging patterns. | |
* Heavily based on Mike Anderson's ideas from the "Tyrant" algo, mentioned at | |
* http://www.roguebasin.roguelikedevelopment.org/index.php?title=Dungeon-Building_Algorithm. | |
* @augments ROT.Map.Dungeon | |
*/ | |
ROT.Map.Digger = function(width, height, options) { | |
ROT.Map.Dungeon.call(this, width, height); | |
this._options = { | |
roomWidth: [3, 9], /* room minimum and maximum width */ | |
roomHeight: [3, 5], /* room minimum and maximum height */ | |
corridorLength: [3, 10], /* corridor minimum and maximum length */ | |
dugPercentage: 0.2, /* we stop after this percentage of level area has been dug out */ | |
timeLimit: 1000 /* we stop after this much time has passed (msec) */ | |
} | |
for (var p in options) { this._options[p] = options[p]; } | |
this._features = { | |
"Room": 4, | |
"Corridor": 4 | |
} | |
this._featureAttempts = 20; /* how many times do we try to create a feature on a suitable wall */ | |
this._walls = {}; /* these are available for digging */ | |
this._digCallback = this._digCallback.bind(this); | |
this._canBeDugCallback = this._canBeDugCallback.bind(this); | |
this._isWallCallback = this._isWallCallback.bind(this); | |
this._priorityWallCallback = this._priorityWallCallback.bind(this); | |
} | |
ROT.Map.Digger.extend(ROT.Map.Dungeon); | |
/** | |
* Create a map | |
* @see ROT.Map#create | |
*/ | |
ROT.Map.Digger.prototype.create = function(callback) { | |
this._rooms = []; | |
this._corridors = []; | |
this._map = this._fillMap(1); | |
this._walls = {}; | |
this._dug = 0; | |
var area = (this._width-2) * (this._height-2); | |
this._firstRoom(); | |
var t1 = Date.now(); | |
do { | |
var t2 = Date.now(); | |
if (t2 - t1 > this._options.timeLimit) { break; } | |
/* find a good wall */ | |
var wall = this._findWall(); | |
if (!wall) { break; } /* no more walls */ | |
var parts = wall.split(","); | |
var x = parseInt(parts[0]); | |
var y = parseInt(parts[1]); | |
var dir = this._getDiggingDirection(x, y); | |
if (!dir) { continue; } /* this wall is not suitable */ | |
// console.log("wall", x, y); | |
/* try adding a feature */ | |
var featureAttempts = 0; | |
do { | |
featureAttempts++; | |
if (this._tryFeature(x, y, dir[0], dir[1])) { /* feature added */ | |
if (this._rooms.length + this._corridors.length == 2) { this._rooms[0].addDoor(x, y); } /* first room oficially has doors */ | |
this._removeSurroundingWalls(x, y); | |
this._removeSurroundingWalls(x-dir[0], y-dir[1]); | |
break; | |
} | |
} while (featureAttempts < this._featureAttempts); | |
var priorityWalls = 0; | |
for (var id in this._walls) { | |
if (this._walls[id] > 1) { priorityWalls++; } | |
} | |
} while (this._dug/area < this._options.dugPercentage || priorityWalls); /* fixme number of priority walls */ | |
if (callback) { | |
for (var i=0;i<this._width;i++) { | |
for (var j=0;j<this._height;j++) { | |
callback(i, j, this._map[i][j]); | |
} | |
} | |
} | |
this._walls = {}; | |
this._map = null; | |
return this; | |
} | |
ROT.Map.Digger.prototype._digCallback = function(x, y, value) { | |
if (value == 0 || value == 2) { /* empty */ | |
this._map[x][y] = 0; | |
this._dug++; | |
} else { /* wall */ | |
this._walls[x+","+y] = 1; | |
} | |
} | |
ROT.Map.Digger.prototype._isWallCallback = function(x, y) { | |
if (x < 0 || y < 0 || x >= this._width || y >= this._height) { return false; } | |
return (this._map[x][y] == 1); | |
} | |
ROT.Map.Digger.prototype._canBeDugCallback = function(x, y) { | |
if (x < 1 || y < 1 || x+1 >= this._width || y+1 >= this._height) { return false; } | |
return (this._map[x][y] == 1); | |
} | |
ROT.Map.Digger.prototype._priorityWallCallback = function(x, y) { | |
this._walls[x+","+y] = 2; | |
} | |
ROT.Map.Digger.prototype._firstRoom = function() { | |
var cx = Math.floor(this._width/2); | |
var cy = Math.floor(this._height/2); | |
var room = ROT.Map.Feature.Room.createRandomCenter(cx, cy, this._options); | |
this._rooms.push(room); | |
room.create(this._digCallback); | |
} | |
/** | |
* Get a suitable wall | |
*/ | |
ROT.Map.Digger.prototype._findWall = function() { | |
var prio1 = []; | |
var prio2 = []; | |
for (var id in this._walls) { | |
var prio = this._walls[id]; | |
if (prio == 2) { | |
prio2.push(id); | |
} else { | |
prio1.push(id); | |
} | |
} | |
var arr = (prio2.length ? prio2 : prio1); | |
if (!arr.length) { return null; } /* no walls :/ */ | |
var id = arr.random(); | |
delete this._walls[id]; | |
return id; | |
} | |
/** | |
* Tries adding a feature | |
* @returns {bool} was this a successful try? | |
*/ | |
ROT.Map.Digger.prototype._tryFeature = function(x, y, dx, dy) { | |
var feature = null; | |
var total = 0; | |
for (var p in this._features) { total += this._features[p]; } | |
var random = Math.floor(ROT.RNG.getUniform()*total); | |
var sub = 0; | |
for (var p in this._features) { | |
sub += this._features[p]; | |
if (random < sub) { | |
feature = ROT.Map.Feature[p]; | |
break; | |
} | |
} | |
feature = feature.createRandomAt(x, y, dx, dy, this._options); | |
if (!feature.isValid(this._isWallCallback, this._canBeDugCallback)) { | |
// console.log("not valid"); | |
// feature.debug(); | |
return false; | |
} | |
feature.create(this._digCallback); | |
// feature.debug(); | |
if (feature instanceof ROT.Map.Feature.Room) { this._rooms.push(feature); } | |
if (feature instanceof ROT.Map.Feature.Corridor) { | |
feature.createPriorityWalls(this._priorityWallCallback); | |
this._corridors.push(feature); | |
} | |
return true; | |
} | |
ROT.Map.Digger.prototype._removeSurroundingWalls = function(cx, cy) { | |
var deltas = ROT.DIRS[4]; | |
for (var i=0;i<deltas.length;i++) { | |
var delta = deltas[i]; | |
var x = cx + delta[0]; | |
var y = cy + delta[1]; | |
delete this._walls[x+","+y]; | |
var x = cx + 2*delta[0]; | |
var y = cy + 2*delta[1]; | |
delete this._walls[x+","+y]; | |
} | |
} | |
/** | |
* Returns vector in "digging" direction, or false, if this does not exist (or is not unique) | |
*/ | |
ROT.Map.Digger.prototype._getDiggingDirection = function(cx, cy) { | |
var result = null; | |
var deltas = ROT.DIRS[4]; | |
for (var i=0;i<deltas.length;i++) { | |
var delta = deltas[i]; | |
var x = cx + delta[0]; | |
var y = cy + delta[1]; | |
if (x < 0 || y < 0 || x >= this._width || y >= this._width) { return null; } | |
if (!this._map[x][y]) { /* there already is another empty neighbor! */ | |
if (result) { return null; } | |
result = delta; | |
} | |
} | |
/* no empty neighbor */ | |
if (!result) { return null; } | |
return [-result[0], -result[1]]; | |
} | |
/** | |
* @class Dungeon generator which tries to fill the space evenly. Generates independent rooms and tries to connect them. | |
* @augments ROT.Map.Dungeon | |
*/ | |
ROT.Map.Uniform = function(width, height, options) { | |
ROT.Map.Dungeon.call(this, width, height); | |
this._options = { | |
roomWidth: [3, 9], /* room minimum and maximum width */ | |
roomHeight: [3, 5], /* room minimum and maximum height */ | |
roomDugPercentage: 0.1, /* we stop after this percentage of level area has been dug out by rooms */ | |
timeLimit: 1000 /* we stop after this much time has passed (msec) */ | |
} | |
for (var p in options) { this._options[p] = options[p]; } | |
this._roomAttempts = 20; /* new room is created N-times until is considered as impossible to generate */ | |
this._corridorAttempts = 20; /* corridors are tried N-times until the level is considered as impossible to connect */ | |
this._connected = []; /* list of already connected rooms */ | |
this._unconnected = []; /* list of remaining unconnected rooms */ | |
this._digCallback = this._digCallback.bind(this); | |
this._canBeDugCallback = this._canBeDugCallback.bind(this); | |
this._isWallCallback = this._isWallCallback.bind(this); | |
} | |
ROT.Map.Uniform.extend(ROT.Map.Dungeon); | |
/** | |
* Create a map. If the time limit has been hit, returns null. | |
* @see ROT.Map#create | |
*/ | |
ROT.Map.Uniform.prototype.create = function(callback) { | |
var t1 = Date.now(); | |
while (1) { | |
var t2 = Date.now(); | |
if (t2 - t1 > this._options.timeLimit) { return null; } /* time limit! */ | |
this._map = this._fillMap(1); | |
this._dug = 0; | |
this._rooms = []; | |
this._unconnected = []; | |
this._generateRooms(); | |
if (this._generateCorridors()) { break; } | |
} | |
if (callback) { | |
for (var i=0;i<this._width;i++) { | |
for (var j=0;j<this._height;j++) { | |
callback(i, j, this._map[i][j]); | |
} | |
} | |
} | |
return this; | |
} | |
/** | |
* Generates a suitable amount of rooms | |
*/ | |
ROT.Map.Uniform.prototype._generateRooms = function() { | |
var w = this._width-2; | |
var h = this._height-2; | |
do { | |
var room = this._generateRoom(); | |
if (this._dug/(w*h) > this._options.roomDugPercentage) { break; } /* achieved requested amount of free space */ | |
} while (room); | |
/* either enough rooms, or not able to generate more of them :) */ | |
} | |
/** | |
* Try to generate one room | |
*/ | |
ROT.Map.Uniform.prototype._generateRoom = function() { | |
var count = 0; | |
while (count < this._roomAttempts) { | |
count++; | |
var room = ROT.Map.Feature.Room.createRandom(this._width, this._height, this._options); | |
if (!room.isValid(this._isWallCallback, this._canBeDugCallback)) { continue; } | |
room.create(this._digCallback); | |
this._rooms.push(room); | |
return room; | |
} | |
/* no room was generated in a given number of attempts */ | |
return null; | |
} | |
/** | |
* Generates connectors beween rooms | |
* @returns {bool} success Was this attempt successfull? | |
*/ | |
ROT.Map.Uniform.prototype._generateCorridors = function() { | |
var cnt = 0; | |
while (cnt < this._corridorAttempts) { | |
cnt++; | |
this._corridors = []; | |
/* dig rooms into a clear map */ | |
this._map = this._fillMap(1); | |
for (var i=0;i<this._rooms.length;i++) { | |
var room = this._rooms[i]; | |
room.clearDoors(); | |
room.create(this._digCallback); | |
} | |
this._unconnected = this._rooms.slice().randomize(); | |
this._connected = []; | |
if (this._unconnected.length) { this._connected.push(this._unconnected.pop()); } /* first one is always connected */ | |
while (1) { | |
/* 1. pick random connected room */ | |
var connected = this._connected.random(); | |
/* 2. find closest unconnected */ | |
var room1 = this._closestRoom(this._unconnected, connected); | |
/* 3. connect it to closest connected */ | |
var room2 = this._closestRoom(this._connected, room1); | |
var ok = this._connectRooms(room1, room2); | |
if (!ok) { break; } /* stop connecting, re-shuffle */ | |
if (!this._unconnected.length) { return true; } /* done; no rooms remain */ | |
} | |
} | |
return false; | |
} | |
/** | |
* For a given room, find the closest one from the list | |
*/ | |
ROT.Map.Uniform.prototype._closestRoom = function(rooms, room) { | |
var dist = Infinity; | |
var center = room.getCenter(); | |
var result = null; | |
for (var i=0;i<rooms.length;i++) { | |
var r = rooms[i]; | |
var c = r.getCenter(); | |
var dx = c[0]-center[0]; | |
var dy = c[1]-center[1]; | |
var d = dx*dx+dy*dy; | |
if (d < dist) { | |
dist = d; | |
result = r; | |
} | |
} | |
return result; | |
} | |
ROT.Map.Uniform.prototype._connectRooms = function(room1, room2) { | |
/* | |
room1.debug(); | |
room2.debug(); | |
*/ | |
var center1 = room1.getCenter(); | |
var center2 = room2.getCenter(); | |
var diffX = center2[0] - center1[0]; | |
var diffY = center2[1] - center1[1]; | |
if (Math.abs(diffX) < Math.abs(diffY)) { /* first try connecting north-south walls */ | |
var dirIndex1 = (diffY > 0 ? 2 : 0); | |
var dirIndex2 = (dirIndex1 + 2) % 4; | |
var min = room2.getLeft(); | |
var max = room2.getRight(); | |
var index = 0; | |
} else { /* first try connecting east-west walls */ | |
var dirIndex1 = (diffX > 0 ? 1 : 3); | |
var dirIndex2 = (dirIndex1 + 2) % 4; | |
var min = room2.getTop(); | |
var max = room2.getBottom(); | |
var index = 1; | |
} | |
var start = this._placeInWall(room1, dirIndex1); /* corridor will start here */ | |
if (!start) { return false; } | |
if (start[index] >= min && start[index] <= max) { /* possible to connect with straight line (I-like) */ | |
var end = start.slice(); | |
var value = null; | |
switch (dirIndex2) { | |
case 0: value = room2.getTop()-1; break; | |
case 1: value = room2.getRight()+1; break; | |
case 2: value = room2.getBottom()+1; break; | |
case 3: value = room2.getLeft()-1; break; | |
} | |
end[(index+1)%2] = value; | |
this._digLine([start, end]); | |
} else if (start[index] < min-1 || start[index] > max+1) { /* need to switch target wall (L-like) */ | |
var diff = start[index] - center2[index]; | |
switch (dirIndex2) { | |
case 0: | |
case 1: var rotation = (diff < 0 ? 3 : 1); break; | |
case 2: | |
case 3: var rotation = (diff < 0 ? 1 : 3); break; | |
} | |
dirIndex2 = (dirIndex2 + rotation) % 4; | |
var end = this._placeInWall(room2, dirIndex2); | |
if (!end) { return false; } | |
var mid = [0, 0]; | |
mid[index] = start[index]; | |
var index2 = (index+1)%2; | |
mid[index2] = end[index2]; | |
this._digLine([start, mid, end]); | |
} else { /* use current wall pair, but adjust the line in the middle (S-like) */ | |
var index2 = (index+1)%2; | |
var end = this._placeInWall(room2, dirIndex2); | |
if (!end) { return; } | |
var mid = Math.round((end[index2] + start[index2])/2); | |
var mid1 = [0, 0]; | |
var mid2 = [0, 0]; | |
mid1[index] = start[index]; | |
mid1[index2] = mid; | |
mid2[index] = end[index]; | |
mid2[index2] = mid; | |
this._digLine([start, mid1, mid2, end]); | |
} | |
room1.addDoor(start[0], start[1]); | |
room2.addDoor(end[0], end[1]); | |
var index = this._unconnected.indexOf(room1); | |
if (index != -1) { | |
this._unconnected.splice(index, 1); | |
this._connected.push(room1); | |
} | |
var index = this._unconnected.indexOf(room2); | |
if (index != -1) { | |
this._unconnected.splice(index, 1); | |
this._connected.push(room2); | |
} | |
return true; | |
} | |
ROT.Map.Uniform.prototype._placeInWall = function(room, dirIndex) { | |
var start = [0, 0]; | |
var dir = [0, 0]; | |
var length = 0; | |
switch (dirIndex) { | |
case 0: | |
dir = [1, 0]; | |
start = [room.getLeft(), room.getTop()-1]; | |
length = room.getRight()-room.getLeft()+1; | |
break; | |
case 1: | |
dir = [0, 1]; | |
start = [room.getRight()+1, room.getTop()]; | |
length = room.getBottom()-room.getTop()+1; | |
break; | |
case 2: | |
dir = [1, 0]; | |
start = [room.getLeft(), room.getBottom()+1]; | |
length = room.getRight()-room.getLeft()+1; | |
break; | |
case 3: | |
dir = [0, 1]; | |
start = [room.getLeft()-1, room.getTop()]; | |
length = room.getBottom()-room.getTop()+1; | |
break; | |
} | |
var avail = []; | |
var lastBadIndex = -2; | |
for (var i=0;i<length;i++) { | |
var x = start[0] + i*dir[0]; | |
var y = start[1] + i*dir[1]; | |
avail.push(null); | |
var isWall = (this._map[x][y] == 1); | |
if (isWall) { | |
if (lastBadIndex != i-1) { avail[i] = [x, y]; } | |
} else { | |
lastBadIndex = i; | |
if (i) { avail[i-1] = null; } | |
} | |
} | |
for (var i=avail.length-1; i>=0; i--) { | |
if (!avail[i]) { avail.splice(i, 1); } | |
} | |
return (avail.length ? avail.random() : null); | |
} | |
/** | |
* Dig a polyline. | |
*/ | |
ROT.Map.Uniform.prototype._digLine = function(points) { | |
for (var i=1;i<points.length;i++) { | |
var start = points[i-1]; | |
var end = points[i]; | |
var corridor = new ROT.Map.Feature.Corridor(start[0], start[1], end[0], end[1]); | |
corridor.create(this._digCallback); | |
this._corridors.push(corridor); | |
} | |
} | |
ROT.Map.Uniform.prototype._digCallback = function(x, y, value) { | |
this._map[x][y] = value; | |
if (value == 0) { this._dug++; } | |
} | |
ROT.Map.Uniform.prototype._isWallCallback = function(x, y) { | |
if (x < 0 || y < 0 || x >= this._width || y >= this._height) { return false; } | |
return (this._map[x][y] == 1); | |
} | |
ROT.Map.Uniform.prototype._canBeDugCallback = function(x, y) { | |
if (x < 1 || y < 1 || x+1 >= this._width || y+1 >= this._height) { return false; } | |
return (this._map[x][y] == 1); | |
} | |
/** | |
* @class | |
* Dungeon feature; has own .create() method | |
*/ | |
ROT.Map.Feature = function() {} | |
ROT.Map.Feature.prototype.isValid = function(canBeDugCallback) {} | |
ROT.Map.Feature.prototype.create = function(digCallback) {} | |
ROT.Map.Feature.prototype.debug = function() {} | |
ROT.Map.Feature.createRandomAt = function(x, y, dx, dy, options) {} | |
/** | |
* @class Room | |
* @augments ROT.Map.Feature | |
* @param {int} x1 | |
* @param {int} y1 | |
* @param {int} x2 | |
* @param {int} y2 | |
* @param {int} [doorX] | |
* @param {int} [doorY] | |
*/ | |
ROT.Map.Feature.Room = function(x1, y1, x2, y2, doorX, doorY) { | |
this._x1 = x1; | |
this._y1 = y1; | |
this._x2 = x2; | |
this._y2 = y2; | |
this._doors = {}; | |
if (arguments.length > 4) { this.addDoor(doorX, doorY); } | |
} | |
ROT.Map.Feature.Room.extend(ROT.Map.Feature); | |
/** | |
* Room of random size, with a given doors and direction | |
*/ | |
ROT.Map.Feature.Room.createRandomAt = function(x, y, dx, dy, options) { | |
var min = options.roomWidth[0]; | |
var max = options.roomWidth[1]; | |
var width = min + Math.floor(ROT.RNG.getUniform()*(max-min+1)); | |
var min = options.roomHeight[0]; | |
var max = options.roomHeight[1]; | |
var height = min + Math.floor(ROT.RNG.getUniform()*(max-min+1)); | |
if (dx == 1) { /* to the right */ | |
var y2 = y - Math.floor(ROT.RNG.getUniform() * height); | |
return new this(x+1, y2, x+width, y2+height-1, x, y); | |
} | |
if (dx == -1) { /* to the left */ | |
var y2 = y - Math.floor(ROT.RNG.getUniform() * height); | |
return new this(x-width, y2, x-1, y2+height-1, x, y); | |
} | |
if (dy == 1) { /* to the bottom */ | |
var x2 = x - Math.floor(ROT.RNG.getUniform() * width); | |
return new this(x2, y+1, x2+width-1, y+height, x, y); | |
} | |
if (dy == -1) { /* to the top */ | |
var x2 = x - Math.floor(ROT.RNG.getUniform() * width); | |
return new this(x2, y-height, x2+width-1, y-1, x, y); | |
} | |
} | |
/** | |
* Room of random size, positioned around center coords | |
*/ | |
ROT.Map.Feature.Room.createRandomCenter = function(cx, cy, options) { | |
var min = options.roomWidth[0]; | |
var max = options.roomWidth[1]; | |
var width = min + Math.floor(ROT.RNG.getUniform()*(max-min+1)); | |
var min = options.roomHeight[0]; | |
var max = options.roomHeight[1]; | |
var height = min + Math.floor(ROT.RNG.getUniform()*(max-min+1)); | |
var x1 = cx - Math.floor(ROT.RNG.getUniform()*width); | |
var y1 = cy - Math.floor(ROT.RNG.getUniform()*height); | |
var x2 = x1 + width - 1; | |
var y2 = y1 + height - 1; | |
return new this(x1, y1, x2, y2); | |
} | |
/** | |
* Room of random size within a given dimensions | |
*/ | |
ROT.Map.Feature.Room.createRandom = function(availWidth, availHeight, options) { | |
var min = options.roomWidth[0]; | |
var max = options.roomWidth[1]; | |
var width = min + Math.floor(ROT.RNG.getUniform()*(max-min+1)); | |
var min = options.roomHeight[0]; | |
var max = options.roomHeight[1]; | |
var height = min + Math.floor(ROT.RNG.getUniform()*(max-min+1)); | |
var left = availWidth - width - 1; | |
var top = availHeight - height - 1; | |
var x1 = 1 + Math.floor(ROT.RNG.getUniform()*left); | |
var y1 = 1 + Math.floor(ROT.RNG.getUniform()*top); | |
var x2 = x1 + width - 1; | |
var y2 = y1 + height - 1; | |
return new this(x1, y1, x2, y2); | |
} | |
ROT.Map.Feature.Room.prototype.addDoor = function(x, y) { | |
this._doors[x+","+y] = 1; | |
} | |
/** | |
* @param {function} | |
*/ | |
ROT.Map.Feature.Room.prototype.getDoors = function(callback) { | |
for (var key in this._doors) { | |
var parts = key.split(","); | |
callback(parseInt(parts[0]), parseInt(parts[1])); | |
} | |
} | |
ROT.Map.Feature.Room.prototype.clearDoors = function() { | |
this._doors = {}; | |
return this; | |
} | |
ROT.Map.Feature.Room.prototype.debug = function() { | |
console.log("room", this._x1, this._y1, this._x2, this._y2); | |
} | |
ROT.Map.Feature.Room.prototype.isValid = function(isWallCallback, canBeDugCallback) { | |
var left = this._x1-1; | |
var right = this._x2+1; | |
var top = this._y1-1; | |
var bottom = this._y2+1; | |
for (var x=left; x<=right; x++) { | |
for (var y=top; y<=bottom; y++) { | |
if (x == left || x == right || y == top || y == bottom) { | |
if (!isWallCallback(x, y)) { return false; } | |
} else { | |
if (!canBeDugCallback(x, y)) { return false; } | |
} | |
} | |
} | |
return true; | |
} | |
/** | |
* @param {function} digCallback Dig callback with a signature (x, y, value). Values: 0 = empty, 1 = wall, 2 = door. Multiple doors are allowed. | |
*/ | |
ROT.Map.Feature.Room.prototype.create = function(digCallback) { | |
var left = this._x1-1; | |
var right = this._x2+1; | |
var top = this._y1-1; | |
var bottom = this._y2+1; | |
var value = 0; | |
for (var x=left; x<=right; x++) { | |
for (var y=top; y<=bottom; y++) { | |
if (x+","+y in this._doors) { | |
value = 2; | |
} else if (x == left || x == right || y == top || y == bottom) { | |
value = 1; | |
} else { | |
value = 0; | |
} | |
digCallback(x, y, value); | |
} | |
} | |
} | |
ROT.Map.Feature.Room.prototype.getCenter = function() { | |
return [Math.round((this._x1 + this._x2)/2), Math.round((this._y1 + this._y2)/2)]; | |
} | |
ROT.Map.Feature.Room.prototype.getLeft = function() { | |
return this._x1; | |
} | |
ROT.Map.Feature.Room.prototype.getRight = function() { | |
return this._x2; | |
} | |
ROT.Map.Feature.Room.prototype.getTop = function() { | |
return this._y1; | |
} | |
ROT.Map.Feature.Room.prototype.getBottom = function() { | |
return this._y2; | |
} | |
/** | |
* @class Corridor | |
* @augments ROT.Map.Feature | |
* @param {int} startX | |
* @param {int} startY | |
* @param {int} endX | |
* @param {int} endY | |
*/ | |
ROT.Map.Feature.Corridor = function(startX, startY, endX, endY) { | |
this._startX = startX; | |
this._startY = startY; | |
this._endX = endX; | |
this._endY = endY; | |
this._endsWithAWall = true; | |
} | |
ROT.Map.Feature.Corridor.extend(ROT.Map.Feature); | |
ROT.Map.Feature.Corridor.createRandomAt = function(x, y, dx, dy, options) { | |
var min = options.corridorLength[0]; | |
var max = options.corridorLength[1]; | |
var length = min + Math.floor(ROT.RNG.getUniform()*(max-min+1)); | |
return new this(x, y, x + dx*length, y + dy*length); | |
} | |
ROT.Map.Feature.Corridor.prototype.debug = function() { | |
console.log("corridor", this._startX, this._startY, this._endX, this._endY); | |
} | |
ROT.Map.Feature.Corridor.prototype.isValid = function(isWallCallback, canBeDugCallback){ | |
var sx = this._startX; | |
var sy = this._startY; | |
var dx = this._endX-sx; | |
var dy = this._endY-sy; | |
var length = 1 + Math.max(Math.abs(dx), Math.abs(dy)); | |
if (dx) { dx = dx/Math.abs(dx); } | |
if (dy) { dy = dy/Math.abs(dy); } | |
var nx = dy; | |
var ny = -dx; | |
var ok = true; | |
for (var i=0; i<length; i++) { | |
var x = sx + i*dx; | |
var y = sy + i*dy; | |
if (!canBeDugCallback( x, y)) { ok = false; } | |
if (!isWallCallback (x + nx, y + ny)) { ok = false; } | |
if (!isWallCallback (x - nx, y - ny)) { ok = false; } | |
if (!ok) { | |
length = i; | |
this._endX = x-dx; | |
this._endY = y-dy; | |
break; | |
} | |
} | |
/** | |
* If the length degenerated, this corridor might be invalid | |
*/ | |
/* not supported */ | |
if (length == 0) { return false; } | |
/* length 1 allowed only if the next space is empty */ | |
if (length == 1 && isWallCallback(this._endX + dx, this._endY + dy)) { return false; } | |
/** | |
* We do not want the corridor to crash into a corner of a room; | |
* if any of the ending corners is empty, the N+1th cell of this corridor must be empty too. | |
* | |
* Situation: | |
* #######1 | |
* .......? | |
* #######2 | |
* | |
* The corridor was dug from left to right. | |
* 1, 2 - problematic corners, ? = N+1th cell (not dug) | |
*/ | |
var firstCornerBad = !isWallCallback(this._endX + dx + nx, this._endY + dy + ny); | |
var secondCornerBad = !isWallCallback(this._endX + dx - nx, this._endY + dy - ny); | |
this._endsWithAWall = isWallCallback(this._endX + dx, this._endY + dy); | |
if ((firstCornerBad || secondCornerBad) && this._endsWithAWall) { return false; } | |
return true; | |
} | |
/** | |
* @param {function} digCallback Dig callback with a signature (x, y, value). Values: 0 = empty. | |
*/ | |
ROT.Map.Feature.Corridor.prototype.create = function(digCallback) { | |
var sx = this._startX; | |
var sy = this._startY; | |
var dx = this._endX-sx; | |
var dy = this._endY-sy; | |
var length = 1+Math.max(Math.abs(dx), Math.abs(dy)); | |
if (dx) { dx = dx/Math.abs(dx); } | |
if (dy) { dy = dy/Math.abs(dy); } | |
var nx = dy; | |
var ny = -dx; | |
for (var i=0; i<length; i++) { | |
var x = sx + i*dx; | |
var y = sy + i*dy; | |
digCallback(x, y, 0); | |
} | |
return true; | |
} | |
ROT.Map.Feature.Corridor.prototype.createPriorityWalls = function(priorityWallCallback) { | |
if (!this._endsWithAWall) { return; } | |
var sx = this._startX; | |
var sy = this._startY; | |
var dx = this._endX-sx; | |
var dy = this._endY-sy; | |
if (dx) { dx = dx/Math.abs(dx); } | |
if (dy) { dy = dy/Math.abs(dy); } | |
var nx = dy; | |
var ny = -dx; | |
priorityWallCallback(this._endX + dx, this._endY + dy); | |
priorityWallCallback(this._endX + nx, this._endY + ny); | |
priorityWallCallback(this._endX - nx, this._endY - ny); | |
}/** | |
* @class Base noise generator | |
*/ | |
ROT.Noise = function() { | |
}; | |
ROT.Noise.prototype.get = function(x, y) {} | |
/** | |
* A simple 2d implementation of simplex noise by Ondrej Zara | |
* | |
* Based on a speed-improved simplex noise algorithm for 2D, 3D and 4D in Java. | |
* Which is based on example code by Stefan Gustavson (stegu@itn.liu.se). | |
* With Optimisations by Peter Eastman (peastman@drizzle.stanford.edu). | |
* Better rank ordering method by Stefan Gustavson in 2012. | |
*/ | |
/** | |
* @class 2D simplex noise generator | |
* @param {int} [gradients=256] Random gradients | |
*/ | |
ROT.Noise.Simplex = function(gradients) { | |
ROT.Noise.call(this); | |
this._F2 = 0.5 * (Math.sqrt(3) - 1); | |
this._G2 = (3 - Math.sqrt(3)) / 6; | |
this._gradients = [ | |
[ 0, -1], | |
[ 1, -1], | |
[ 1, 0], | |
[ 1, 1], | |
[ 0, 1], | |
[-1, 1], | |
[-1, 0], | |
[-1, -1] | |
]; | |
var permutations = []; | |
var count = gradients || 256; | |
for (var i=0;i<count;i++) { permutations.push(i); } | |
permutations = permutations.randomize(); | |
this._perms = []; | |
this._indexes = []; | |
for (var i=0;i<2*count;i++) { | |
this._perms.push(permutations[i % count]); | |
this._indexes.push(this._perms[i] % this._gradients.length); | |
} | |
}; | |
ROT.Noise.Simplex.extend(ROT.Noise); | |
ROT.Noise.Simplex.prototype.get = function(xin, yin) { | |
var perms = this._perms; | |
var indexes = this._indexes; | |
var count = perms.length/2; | |
var G2 = this._G2; | |
var n0 =0, n1 = 0, n2 = 0, gi; // Noise contributions from the three corners | |
// Skew the input space to determine which simplex cell we're in | |
var s = (xin + yin) * this._F2; // Hairy factor for 2D | |
var i = Math.floor(xin + s); | |
var j = Math.floor(yin + s); | |
var t = (i + j) * G2; | |
var X0 = i - t; // Unskew the cell origin back to (x,y) space | |
var Y0 = j - t; | |
var x0 = xin - X0; // The x,y distances from the cell origin | |
var y0 = yin - Y0; | |
// For the 2D case, the simplex shape is an equilateral triangle. | |
// Determine which simplex we are in. | |
var i1, j1; // Offsets for second (middle) corner of simplex in (i,j) coords | |
if (x0 > y0) { | |
i1 = 1; | |
j1 = 0; | |
} else { // lower triangle, XY order: (0,0)->(1,0)->(1,1) | |
i1 = 0; | |
j1 = 1; | |
} // upper triangle, YX order: (0,0)->(0,1)->(1,1) | |
// A step of (1,0) in (i,j) means a step of (1-c,-c) in (x,y), and | |
// a step of (0,1) in (i,j) means a step of (-c,1-c) in (x,y), where | |
// c = (3-sqrt(3))/6 | |
var x1 = x0 - i1 + G2; // Offsets for middle corner in (x,y) unskewed coords | |
var y1 = y0 - j1 + G2; | |
var x2 = x0 - 1 + 2*G2; // Offsets for last corner in (x,y) unskewed coords | |
var y2 = y0 - 1 + 2*G2; | |
// Work out the hashed gradient indices of the three simplex corners | |
var ii = i.mod(count); | |
var jj = j.mod(count); | |
// Calculate the contribution from the three corners | |
var t0 = 0.5 - x0*x0 - y0*y0; | |
if (t0 >= 0) { | |
t0 *= t0; | |
gi = indexes[ii+perms[jj]]; | |
var grad = this._gradients[gi]; | |
n0 = t0 * t0 * (grad[0] * x0 + grad[1] * y0); | |
} | |
var t1 = 0.5 - x1*x1 - y1*y1; | |
if (t1 >= 0) { | |
t1 *= t1; | |
gi = indexes[ii+i1+perms[jj+j1]]; | |
var grad = this._gradients[gi]; | |
n1 = t1 * t1 * (grad[0] * x1 + grad[1] * y1); | |
} | |
var t2 = 0.5 - x2*x2 - y2*y2; | |
if (t2 >= 0) { | |
t2 *= t2; | |
gi = indexes[ii+1+perms[jj+1]]; | |
var grad = this._gradients[gi]; | |
n2 = t2 * t2 * (grad[0] * x2 + grad[1] * y2); | |
} | |
// Add contributions from each corner to get the final noise value. | |
// The result is scaled to return values in the interval [-1,1]. | |
return 70 * (n0 + n1 + n2); | |
} | |
/** | |
* @class Abstract FOV algorithm | |
* @param {function} lightPassesCallback Does the light pass through x,y? | |
* @param {object} [options] | |
* @param {int} [options.topology=8] 4/6/8 | |
*/ | |
ROT.FOV = function(lightPassesCallback, options) { | |
this._lightPasses = lightPassesCallback; | |
this._options = { | |
topology: 8 | |
} | |
for (var p in options) { this._options[p] = options[p]; } | |
}; | |
/** | |
* Compute visibility | |
* @param {int} x | |
* @param {int} y | |
* @param {int} R Maximum visibility radius | |
* @param {function} callback | |
*/ | |
ROT.FOV.prototype.compute = function(x, y, R, callback) {} | |
/** | |
* Return all neighbors in a concentric ring | |
* @param {int} cx center-x | |
* @param {int} cy center-y | |
* @param {int} r range | |
*/ | |
ROT.FOV.prototype._getCircle = function(cx, cy, r) { | |
var result = []; | |
var dirs, countFactor, startOffset; | |
switch (this._options.topology) { | |
case 4: | |
countFactor = 1; | |
startOffset = [0, 1]; | |
dirs = [ | |
ROT.DIRS[8][7], | |
ROT.DIRS[8][1], | |
ROT.DIRS[8][3], | |
ROT.DIRS[8][5] | |
] | |
break; | |
case 6: | |
dirs = ROT.DIRS[6]; | |
countFactor = 1; | |
startOffset = [-1, 1]; | |
break; | |
case 8: | |
dirs = ROT.DIRS[4]; | |
countFactor = 2; | |
startOffset = [-1, 1]; | |
break; | |
} | |
/* starting neighbor */ | |
var x = cx + startOffset[0]*r; | |
var y = cy + startOffset[1]*r; | |
/* circle */ | |
for (var i=0;i<dirs.length;i++) { | |
for (var j=0;j<r*countFactor;j++) { | |
result.push([x, y]); | |
x += dirs[i][0]; | |
y += dirs[i][1]; | |
} | |
} | |
return result; | |
} | |
/** | |
* @class Discrete shadowcasting algorithm | |
* @augments ROT.FOV | |
*/ | |
ROT.FOV.DiscreteShadowcasting = function(lightPassesCallback, options) { | |
ROT.FOV.call(this, lightPassesCallback, options); | |
} | |
ROT.FOV.DiscreteShadowcasting.extend(ROT.FOV); | |
/** | |
* @see ROT.FOV#compute | |
*/ | |
ROT.FOV.DiscreteShadowcasting.prototype.compute = function(x, y, R, callback) { | |
var center = this._coords; | |
var map = this._map; | |
/* this place is always visible */ | |
callback(x, y, 0); | |
/* standing in a dark place. FIXME is this a good idea? */ | |
if (!this._lightPasses(x, y)) { return; } | |
/* start and end angles */ | |
var DATA = []; | |
var A, B, cx, cy, blocks; | |
/* analyze surrounding cells in concentric rings, starting from the center */ | |
for (var r=1; r<=R; r++) { | |
var neighbors = this._getCircle(x, y, r); | |
var angle = 360 / neighbors.length; | |
for (var i=0;i<neighbors.length;i++) { | |
cx = neighbors[i][0]; | |
cy = neighbors[i][1]; | |
A = angle * (i - 0.5); | |
B = A + angle; | |
blocks = !this._lightPasses(cx, cy); | |
if (this._visibleCoords(Math.floor(A), Math.ceil(B), blocks, DATA)) { callback(cx, cy, r, 1); } | |
if (DATA.length == 2 && DATA[0] == 0 && DATA[1] == 360) { return; } /* cutoff? */ | |
} /* for all cells in this ring */ | |
} /* for all rings */ | |
} | |
/** | |
* @param {int} A start angle | |
* @param {int} B end angle | |
* @param {bool} blocks Does current cell block visibility? | |
* @param {int[][]} DATA shadowed angle pairs | |
*/ | |
ROT.FOV.DiscreteShadowcasting.prototype._visibleCoords = function(A, B, blocks, DATA) { | |
if (A < 0) { | |
var v1 = arguments.callee(0, B, blocks, DATA); | |
var v2 = arguments.callee(360+A, 360, blocks, DATA); | |
return v1 || v2; | |
} | |
var index = 0; | |
while (index < DATA.length && DATA[index] < A) { index++; } | |
if (index == DATA.length) { /* completely new shadow */ | |
if (blocks) { DATA.push(A, B); } | |
return true; | |
} | |
var count = 0; | |
if (index % 2) { /* this shadow starts in an existing shadow, or within its ending boundary */ | |
while (index < DATA.length && DATA[index] < B) { | |
index++; | |
count++; | |
} | |
if (count == 0) { return false; } | |
if (blocks) { | |
if (count % 2) { | |
DATA.splice(index-count, count, B); | |
} else { | |
DATA.splice(index-count, count); | |
} | |
} | |
return true; | |
} else { /* this shadow starts outside an existing shadow, or within a starting boundary */ | |
while (index < DATA.length && DATA[index] < B) { | |
index++; | |
count++; | |
} | |
/* visible when outside an existing shadow, or when overlapping */ | |
if (A == DATA[index-count] && count == 1) { return false; } | |
if (blocks) { | |
if (count % 2) { | |
DATA.splice(index-count, count, A); | |
} else { | |
DATA.splice(index-count, count, A, B); | |
} | |
} | |
return true; | |
} | |
} | |
/** | |
* @class Precise shadowcasting algorithm | |
* @augments ROT.FOV | |
*/ | |
ROT.FOV.PreciseShadowcasting = function(lightPassesCallback, options) { | |
ROT.FOV.call(this, lightPassesCallback, options); | |
} | |
ROT.FOV.PreciseShadowcasting.extend(ROT.FOV); | |
/** | |
* @see ROT.FOV#compute | |
*/ | |
ROT.FOV.PreciseShadowcasting.prototype.compute = function(x, y, R, callback) { | |
/* this place is always visible */ | |
callback(x, y, 0, 1); | |
/* standing in a dark place. FIXME is this a good idea? */ | |
if (!this._lightPasses(x, y)) { return; } | |
/* list of all shadows */ | |
var SHADOWS = []; | |
var cx, cy, blocks, A1, A2, visibility; | |
/* analyze surrounding cells in concentric rings, starting from the center */ | |
for (var r=1; r<=R; r++) { | |
var neighbors = this._getCircle(x, y, r); | |
var neighborCount = neighbors.length; | |
for (var i=0;i<neighborCount;i++) { | |
cx = neighbors[i][0]; | |
cy = neighbors[i][1]; | |
/* shift half-an-angle backwards to maintain consistency of 0-th cells */ | |
A1 = [i ? 2*i-1 : 2*neighborCount-1, 2*neighborCount]; | |
A2 = [2*i+1, 2*neighborCount]; | |
blocks = !this._lightPasses(cx, cy); | |
visibility = this._checkVisibility(A1, A2, blocks, SHADOWS); | |
if (visibility) { callback(cx, cy, r, visibility); } | |
if (SHADOWS.length == 2 && SHADOWS[0][0] == 0 && SHADOWS[1][0] == SHADOWS[1][1]) { return; } /* cutoff? */ | |
} /* for all cells in this ring */ | |
} /* for all rings */ | |
} | |
/** | |
* @param {int[2]} A1 arc start | |
* @param {int[2]} A2 arc end | |
* @param {bool} blocks Does current arc block visibility? | |
* @param {int[][]} SHADOWS list of active shadows | |
*/ | |
ROT.FOV.PreciseShadowcasting.prototype._checkVisibility = function(A1, A2, blocks, SHADOWS) { | |
if (A1[0] > A2[0]) { /* split into two sub-arcs */ | |
var v1 = this._checkVisibility(A1, [A1[1], A1[1]], blocks, SHADOWS); | |
var v2 = this._checkVisibility([0, 1], A2, blocks, SHADOWS); | |
return (v1+v2)/2; | |
} | |
/* index1: first shadow >= A1 */ | |
var index1 = 0, edge1 = false; | |
while (index1 < SHADOWS.length) { | |
var old = SHADOWS[index1]; | |
var diff = old[0]*A1[1] - A1[0]*old[1]; | |
if (diff >= 0) { /* old >= A1 */ | |
if (diff == 0 && !(index1 % 2)) { edge1 = true; } | |
break; | |
} | |
index1++; | |
} | |
/* index2: last shadow <= A2 */ | |
var index2 = SHADOWS.length, edge2 = false; | |
while (index2--) { | |
var old = SHADOWS[index2]; | |
var diff = A2[0]*old[1] - old[0]*A2[1]; | |
if (diff >= 0) { /* old <= A2 */ | |
if (diff == 0 && (index2 % 2)) { edge2 = true; } | |
break; | |
} | |
} | |
var visible = true; | |
if (index1 == index2 && (edge1 || edge2)) { /* subset of existing shadow, one of the edges match */ | |
visible = false; | |
} else if (edge1 && edge2 && index1+1==index2 && (index2 % 2)) { /* completely equivalent with existing shadow */ | |
visible = false; | |
} else if (index1 > index2 && (index1 % 2)) { /* subset of existing shadow, not touching */ | |
visible = false; | |
} | |
if (!visible) { return 0; } /* fast case: not visible */ | |
var visibleLength, P; | |
/* compute the length of visible arc, adjust list of shadows (if blocking) */ | |
var remove = index2-index1+1; | |
if (remove % 2) { | |
if (index1 % 2) { /* first edge within existing shadow, second outside */ | |
var P = SHADOWS[index1]; | |
visibleLength = (A2[0]*P[1] - P[0]*A2[1]) / (P[1] * A2[1]); | |
if (blocks) { SHADOWS.splice(index1, remove, A2); } | |
} else { /* second edge within existing shadow, first outside */ | |
var P = SHADOWS[index2]; | |
visibleLength = (P[0]*A1[1] - A1[0]*P[1]) / (A1[1] * P[1]); | |
if (blocks) { SHADOWS.splice(index1, remove, A1); } | |
} | |
} else { | |
if (index1 % 2) { /* both edges within existing shadows */ | |
var P1 = SHADOWS[index1]; | |
var P2 = SHADOWS[index2]; | |
visibleLength = (P2[0]*P1[1] - P1[0]*P2[1]) / (P1[1] * P2[1]); | |
if (blocks) { SHADOWS.splice(index1, remove); } | |
} else { /* both edges outside existing shadows */ | |
if (blocks) { SHADOWS.splice(index1, remove, A1, A2); } | |
return 1; /* whole arc visible! */ | |
} | |
} | |
var arcLength = (A2[0]*A1[1] - A1[0]*A2[1]) / (A1[1] * A2[1]); | |
return visibleLength/arcLength; | |
} | |
/** | |
* @namespace Color operations | |
*/ | |
ROT.Color = { | |
fromString: function(str) { | |
var cached, r; | |
if (str in this._cache) { | |
cached = this._cache[str]; | |
} else { | |
if (str.charAt(0) == "#") { /* hex rgb */ | |
var values = str.match(/[0-9a-f]/gi).map(function(x) { return parseInt(x, 16); }); | |
if (values.length == 3) { | |
cached = values.map(function(x) { return x*17; }); | |
} else { | |
for (var i=0;i<3;i++) { | |
values[i+1] += 16*values[i]; | |
values.splice(i, 1); | |
} | |
cached = values; | |
} | |
} else if (r = str.match(/rgb\(([0-9, ]+)\)/i)) { /* decimal rgb */ | |
cached = r[1].split(/\s*,\s*/).map(function(x) { return parseInt(x); }); | |
} else { /* html name */ | |
cached = [0, 0, 0]; | |
} | |
this._cache[str] = cached; | |
} | |
return cached.slice(); | |
}, | |
/** | |
* Add two or more colors | |
* @param {number[]} color1 | |
* @param {number[]} color2 | |
* @returns {number[]} | |
*/ | |
add: function(color1, color2) { | |
var result = color1.slice(); | |
for (var i=0;i<3;i++) { | |
for (var j=1;j<arguments.length;j++) { | |
result[i] += arguments[j][i]; | |
} | |
} | |
return result; | |
}, | |
/** | |
* Add two or more colors, MODIFIES FIRST ARGUMENT | |
* @param {number[]} color1 | |
* @param {number[]} color2 | |
* @returns {number[]} | |
*/ | |
add_: function(color1, color2) { | |
for (var i=0;i<3;i++) { | |
for (var j=1;j<arguments.length;j++) { | |
color1[i] += arguments[j][i]; | |
} | |
} | |
return color1; | |
}, | |
/** | |
* Multiply (mix) two or more colors | |
* @param {number[]} color1 | |
* @param {number[]} color2 | |
* @returns {number[]} | |
*/ | |
multiply: function(color1, color2) { | |
var result = color1.slice(); | |
for (var i=0;i<3;i++) { | |
for (var j=1;j<arguments.length;j++) { | |
result[i] *= arguments[j][i] / 255; | |
} | |
result[i] = Math.round(result[i]); | |
} | |
return result; | |
}, | |
/** | |
* Multiply (mix) two or more colors, MODIFIES FIRST ARGUMENT | |
* @param {number[]} color1 | |
* @param {number[]} color2 | |
* @returns {number[]} | |
*/ | |
multiply_: function(color1, color2) { | |
for (var i=0;i<3;i++) { | |
for (var j=1;j<arguments.length;j++) { | |
color1[i] *= arguments[j][i] / 255; | |
} | |
color1[i] = Math.round(color1[i]); | |
} | |
return color1; | |
}, | |
/** | |
* Interpolate (blend) two colors with a given factor | |
* @param {number[]} color1 | |
* @param {number[]} color2 | |
* @param {float} [factor=0.5] 0..1 | |
* @returns {number[]} | |
*/ | |
interpolate: function(color1, color2, factor) { | |
if (arguments.length < 3) { factor = 0.5; } | |
var result = color1.slice(); | |
for (var i=0;i<3;i++) { | |
result[i] = Math.round(result[i] + factor*(color2[i]-color1[i])); | |
} | |
return result; | |
}, | |
/** | |
* Interpolate (blend) two colors with a given factor in HSL mode | |
* @param {number[]} color1 | |
* @param {number[]} color2 | |
* @param {float} [factor=0.5] 0..1 | |
* @returns {number[]} | |
*/ | |
interpolateHSL: function(color1, color2, factor) { | |
if (arguments.length < 3) { factor = 0.5; } | |
var hsl1 = this.rgb2hsl(color1); | |
var hsl2 = this.rgb2hsl(color2); | |
for (var i=0;i<3;i++) { | |
hsl1[i] += factor*(hsl2[i]-hsl1[i]); | |
} | |
return this.hsl2rgb(hsl1); | |
}, | |
/** | |
* Create a new random color based on this one | |
* @param {number[]} color | |
* @param {number[]} diff Set of standard deviations | |
* @returns {number[]} | |
*/ | |
randomize: function(color, diff) { | |
if (!(diff instanceof Array)) { diff = ROT.RNG.getNormal(0, diff); } | |
var result = color.slice(); | |
for (var i=0;i<3;i++) { | |
result[i] += (diff instanceof Array ? Math.round(ROT.RNG.getNormal(0, diff[i])) : diff); | |
} | |
return result; | |
}, | |
/** | |
* Converts an RGB color value to HSL. Expects 0..255 inputs, produces 0..1 outputs. | |
* @param {number[]} color | |
* @returns {number[]} | |
*/ | |
rgb2hsl: function(color) { | |
var r = color[0]/255; | |
var g = color[1]/255; | |
var b = color[2]/255; | |
var max = Math.max(r, g, b), min = Math.min(r, g, b); | |
var h, s, l = (max + min) / 2; | |
if (max == min) { | |
h = s = 0; // achromatic | |
} else { | |
var d = max - min; | |
s = (l > 0.5 ? d / (2 - max - min) : d / (max + min)); | |
switch(max) { | |
case r: h = (g - b) / d + (g < b ? 6 : 0); break; | |
case g: h = (b - r) / d + 2; break; | |
case b: h = (r - g) / d + 4; break; | |
} | |
h /= 6; | |
} | |
return [h, s, l]; | |
}, | |
/** | |
* Converts an HSL color value to RGB. Expects 0..1 inputs, produces 0..255 outputs. | |
* @param {number[]} color | |
* @returns {number[]} | |
*/ | |
hsl2rgb: function(color) { | |
var l = color[2]; | |
if (color[1] == 0) { | |
l *= 255; | |
return [l, l, l]; | |
} else { | |
function hue2rgb(p, q, t) { | |
if (t < 0) t += 1; | |
if (t > 1) t -= 1; | |
if (t < 1/6) return p + (q - p) * 6 * t; | |
if (t < 1/2) return q; | |
if (t < 2/3) return p + (q - p) * (2/3 - t) * 6; | |
return p; | |
} | |
var s = color[1]; | |
var q = (l < 0.5 ? l * (1 + s) : l + s - l * s); | |
var p = 2 * l - q; | |
var r = hue2rgb(p, q, color[0] + 1/3); | |
var g = hue2rgb(p, q, color[0]); | |
var b = hue2rgb(p, q, color[0] - 1/3); | |
return [Math.round(r*255), Math.round(g*255), Math.round(b*255)]; | |
} | |
}, | |
toRGB: function(color) { | |
return "rgb(" + this._clamp(color[0]) + "," + this._clamp(color[1]) + "," + this._clamp(color[2]) + ")"; | |
}, | |
toHex: function(color) { | |
var parts = []; | |
for (var i=0;i<3;i++) { | |
parts.push(this._clamp(color[i]).toString(16).lpad("0", 2)); | |
} | |
return "#" + parts.join(""); | |
}, | |
_clamp: function(num) { | |
if (num < 0) { | |
return 0; | |
} else if (num > 255) { | |
return 255; | |
} else { | |
return num; | |
} | |
}, | |
_cache: { | |
"black": [0,0,0], | |
"navy": [0,0,128], | |
"darkblue": [0,0,139], | |
"mediumblue": [0,0,205], | |
"blue": [0,0,255], | |
"darkgreen": [0,100,0], | |
"green": [0,128,0], | |
"teal": [0,128,128], | |
"darkcyan": [0,139,139], | |
"deepskyblue": [0,191,255], | |
"darkturquoise": [0,206,209], | |
"mediumspringgreen": [0,250,154], | |
"lime": [0,255,0], | |
"springgreen": [0,255,127], | |
"aqua": [0,255,255], | |
"cyan": [0,255,255], | |
"midnightblue": [25,25,112], | |
"dodgerblue": [30,144,255], | |
"forestgreen": [34,139,34], | |
"seagreen": [46,139,87], | |
"darkslategray": [47,79,79], | |
"darkslategrey": [47,79,79], | |
"limegreen": [50,205,50], | |
"mediumseagreen": [60,179,113], | |
"turquoise": [64,224,208], | |
"royalblue": [65,105,225], | |
"steelblue": [70,130,180], | |
"darkslateblue": [72,61,139], | |
"mediumturquoise": [72,209,204], | |
"indigo": [75,0,130], | |
"darkolivegreen": [85,107,47], | |
"cadetblue": [95,158,160], | |
"cornflowerblue": [100,149,237], | |
"mediumaquamarine": [102,205,170], | |
"dimgray": [105,105,105], | |
"dimgrey": [105,105,105], | |
"slateblue": [106,90,205], | |
"olivedrab": [107,142,35], | |
"slategray": [112,128,144], | |
"slategrey": [112,128,144], | |
"lightslategray": [119,136,153], | |
"lightslategrey": [119,136,153], | |
"mediumslateblue": [123,104,238], | |
"lawngreen": [124,252,0], | |
"chartreuse": [127,255,0], | |
"aquamarine": [127,255,212], | |
"maroon": [128,0,0], | |
"purple": [128,0,128], | |
"olive": [128,128,0], | |
"gray": [128,128,128], | |
"grey": [128,128,128], | |
"skyblue": [135,206,235], | |
"lightskyblue": [135,206,250], | |
"blueviolet": [138,43,226], | |
"darkred": [139,0,0], | |
"darkmagenta": [139,0,139], | |
"saddlebrown": [139,69,19], | |
"darkseagreen": [143,188,143], | |
"lightgreen": [144,238,144], | |
"mediumpurple": [147,112,216], | |
"darkviolet": [148,0,211], | |
"palegreen": [152,251,152], | |
"darkorchid": [153,50,204], | |
"yellowgreen": [154,205,50], | |
"sienna": [160,82,45], | |
"brown": [165,42,42], | |
"darkgray": [169,169,169], | |
"darkgrey": [169,169,169], | |
"lightblue": [173,216,230], | |
"greenyellow": [173,255,47], | |
"paleturquoise": [175,238,238], | |
"lightsteelblue": [176,196,222], | |
"powderblue": [176,224,230], | |
"firebrick": [178,34,34], | |
"darkgoldenrod": [184,134,11], | |
"mediumorchid": [186,85,211], | |
"rosybrown": [188,143,143], | |
"darkkhaki": [189,183,107], | |
"silver": [192,192,192], | |
"mediumvioletred": [199,21,133], | |
"indianred": [205,92,92], | |
"peru": [205,133,63], | |
"chocolate": [210,105,30], | |
"tan": [210,180,140], | |
"lightgray": [211,211,211], | |
"lightgrey": [211,211,211], | |
"palevioletred": [216,112,147], | |
"thistle": [216,191,216], | |
"orchid": [218,112,214], | |
"goldenrod": [218,165,32], | |
"crimson": [220,20,60], | |
"gainsboro": [220,220,220], | |
"plum": [221,160,221], | |
"burlywood": [222,184,135], | |
"lightcyan": [224,255,255], | |
"lavender": [230,230,250], | |
"darksalmon": [233,150,122], | |
"violet": [238,130,238], | |
"palegoldenrod": [238,232,170], | |
"lightcoral": [240,128,128], | |
"khaki": [240,230,140], | |
"aliceblue": [240,248,255], | |
"honeydew": [240,255,240], | |
"azure": [240,255,255], | |
"sandybrown": [244,164,96], | |
"wheat": [245,222,179], | |
"beige": [245,245,220], | |
"whitesmoke": [245,245,245], | |
"mintcream": [245,255,250], | |
"ghostwhite": [248,248,255], | |
"salmon": [250,128,114], | |
"antiquewhite": [250,235,215], | |
"linen": [250,240,230], | |
"lightgoldenrodyellow": [250,250,210], | |
"oldlace": [253,245,230], | |
"red": [255,0,0], | |
"fuchsia": [255,0,255], | |
"magenta": [255,0,255], | |
"deeppink": [255,20,147], | |
"orangered": [255,69,0], | |
"tomato": [255,99,71], | |
"hotpink": [255,105,180], | |
"coral": [255,127,80], | |
"darkorange": [255,140,0], | |
"lightsalmon": [255,160,122], | |
"orange": [255,165,0], | |
"lightpink": [255,182,193], | |
"pink": [255,192,203], | |
"gold": [255,215,0], | |
"peachpuff": [255,218,185], | |
"navajowhite": [255,222,173], | |
"moccasin": [255,228,181], | |
"bisque": [255,228,196], | |
"mistyrose": [255,228,225], | |
"blanchedalmond": [255,235,205], | |
"papayawhip": [255,239,213], | |
"lavenderblush": [255,240,245], | |
"seashell": [255,245,238], | |
"cornsilk": [255,248,220], | |
"lemonchiffon": [255,250,205], | |
"floralwhite": [255,250,240], | |
"snow": [255,250,250], | |
"yellow": [255,255,0], | |
"lightyellow": [255,255,224], | |
"ivory": [255,255,240], | |
"white": [255,255,255] | |
} | |
} | |
/** | |
* @class Lighting computation, based on a traditional FOV for multiple light sources and multiple passes. | |
* @param {function} reflectivityCallback Callback to retrieve cell reflectivity (0..1) | |
* @param {object} [options] | |
* @param {int} [options.passes=1] Number of passes. 1 equals to simple FOV of all light sources, >1 means a *highly simplified* radiosity-like algorithm. | |
* @param {int} [options.emissionThreshold=100] Cells with emissivity > threshold will be treated as light source in the next pass. | |
* @param {int} [options.range=10] Max light range | |
*/ | |
ROT.Lighting = function(reflectivityCallback, options) { | |
this._reflectivityCallback = reflectivityCallback; | |
this._options = { | |
passes: 1, | |
emissionThreshold: 100, | |
range: 10 | |
}; | |
this._fov = null; | |
this._lights = {}; | |
this._reflectivityCache = {}; | |
this._fovCache = {}; | |
this.setOptions(options); | |
} | |
/** | |
* Adjust options at runtime | |
* @see ROT.Lighting | |
* @param {object} [options] | |
*/ | |
ROT.Lighting.prototype.setOptions = function(options) { | |
for (var p in options) { this._options[p] = options[p]; } | |
if (options.range) { this.reset(); } | |
return this; | |
} | |
/** | |
* Set the used Field-Of-View algo | |
* @param {ROT.FOV} fov | |
*/ | |
ROT.Lighting.prototype.setFOV = function(fov) { | |
this._fov = fov; | |
this._fovCache = {}; | |
return this; | |
} | |
/** | |
* Set (or remove) a light source | |
* @param {int} x | |
* @param {int} y | |
* @param {null || string || number[3]} color | |
*/ | |
ROT.Lighting.prototype.setLight = function(x, y, color) { | |
var key = x+","+y; | |
if (color) { | |
this._lights[key] = (typeof(color) == "string" ? ROT.Color.fromString(color) : color); | |
} else { | |
delete this._lights[key]; | |
} | |
return this; | |
} | |
/** | |
* Reset the pre-computed topology values. Call whenever the underlying map changes its light-passability. | |
*/ | |
ROT.Lighting.prototype.reset = function() { | |
this._reflectivityCache = {}; | |
this._fovCache = {}; | |
return this; | |
} | |
/** | |
* Compute the lighting | |
* @param {function} lightingCallback Will be called with (x, y, color) for every lit cell | |
*/ | |
ROT.Lighting.prototype.compute = function(lightingCallback) { | |
var doneCells = {}; | |
var emittingCells = {}; | |
var litCells = {}; | |
for (var key in this._lights) { /* prepare emitters for first pass */ | |
var light = this._lights[key]; | |
if (!(key in emittingCells)) { emittingCells[key] = [0, 0, 0]; } | |
ROT.Color.add_(emittingCells[key], light); | |
} | |
for (var i=0;i<this._options.passes;i++) { /* main loop */ | |
this._emitLight(emittingCells, litCells, doneCells); | |
if (i+1 == this._options.passes) { continue; } /* not for the last pass */ | |
emittingCells = this._computeEmitters(litCells, doneCells); | |
} | |
for (var litKey in litCells) { /* let the user know what and how is lit */ | |
var parts = litKey.split(","); | |
var x = parseInt(parts[0]); | |
var y = parseInt(parts[1]); | |
lightingCallback(x, y, litCells[litKey]); | |
} | |
return this; | |
} | |
/** | |
* Compute one iteration from all emitting cells | |
* @param {object} emittingCells These emit light | |
* @param {object} litCells Add projected light to these | |
* @param {object} doneCells These already emitted, forbid them from further calculations | |
*/ | |
ROT.Lighting.prototype._emitLight = function(emittingCells, litCells, doneCells) { | |
for (var key in emittingCells) { | |
var parts = key.split(","); | |
var x = parseInt(parts[0]); | |
var y = parseInt(parts[1]); | |
this._emitLightFromCell(x, y, emittingCells[key], litCells); | |
doneCells[key] = 1; | |
} | |
return this; | |
} | |
/** | |
* Prepare a list of emitters for next pass | |
* @param {object} litCells | |
* @param {object} doneCells | |
* @returns {object} | |
*/ | |
ROT.Lighting.prototype._computeEmitters = function(litCells, doneCells) { | |
var result = {}; | |
for (var key in litCells) { | |
if (key in doneCells) { continue; } /* already emitted */ | |
var color = litCells[key]; | |
if (key in this._reflectivityCache) { | |
var reflectivity = this._reflectivityCache[key]; | |
} else { | |
var parts = key.split(","); | |
var x = parseInt(parts[0]); | |
var y = parseInt(parts[1]); | |
var reflectivity = this._reflectivityCallback(x, y); | |
this._reflectivityCache[key] = reflectivity; | |
} | |
if (reflectivity == 0) { continue; } /* will not reflect at all */ | |
/* compute emission color */ | |
var emission = []; | |
var intensity = 0; | |
for (var i=0;i<3;i++) { | |
var part = Math.round(color[i]*reflectivity); | |
emission[i] = part; | |
intensity += part; | |
} | |
if (intensity > this._options.emissionThreshold) { result[key] = emission; } | |
} | |
return result; | |
} | |
/** | |
* Compute one iteration from one cell | |
* @param {int} x | |
* @param {int} y | |
* @param {number[]} color | |
* @param {object} litCells Cell data to by updated | |
*/ | |
ROT.Lighting.prototype._emitLightFromCell = function(x, y, color, litCells) { | |
var key = x+","+y; | |
if (key in this._fovCache) { | |
var fov = this._fovCache[key]; | |
} else { | |
var fov = this._updateFOV(x, y); | |
} | |
for (var fovKey in fov) { | |
var formFactor = fov[fovKey]; | |
if (fovKey in litCells) { /* already lit */ | |
var result = litCells[fovKey]; | |
} else { /* newly lit */ | |
var result = [0, 0, 0]; | |
litCells[fovKey] = result; | |
} | |
for (var i=0;i<3;i++) { result[i] += Math.round(color[i]*formFactor); } /* add light color */ | |
} | |
return this; | |
} | |
/** | |
* Compute FOV ("form factor") for a potential light source at [x,y] | |
* @param {int} x | |
* @param {int} y | |
* @returns {object} | |
*/ | |
ROT.Lighting.prototype._updateFOV = function(x, y) { | |
var key1 = x+","+y; | |
var cache = {}; | |
this._fovCache[key1] = cache; | |
var range = this._options.range; | |
var cb = function(x, y, r, vis) { | |
var key2 = x+","+y; | |
var formFactor = vis * (1-r/range); | |
if (formFactor == 0) { return; } | |
cache[key2] = formFactor; | |
} | |
this._fov.compute(x, y, range, cb.bind(this)); | |
return cache; | |
} | |
/** | |
* @class Abstract pathfinder | |
* @param {int} toX Target X coord | |
* @param {int} toY Target Y coord | |
* @param {function} passableCallback Callback to determine map passability | |
* @param {object} [options] | |
* @param {int} [options.topology=8] | |
*/ | |
ROT.Path = function(toX, toY, passableCallback, options) { | |
this._toX = toX; | |
this._toY = toY; | |
this._fromX = null; | |
this._fromY = null; | |
this._passableCallback = passableCallback; | |
this._options = { | |
topology: 8 | |
} | |
for (var p in options) { this._options[p] = options[p]; } | |
this._dirs = ROT.DIRS[this._options.topology]; | |
} | |
/** | |
* Compute a path from a given point | |
* @param {int} fromX | |
* @param {int} fromY | |
* @param {function} callback Will be called for every path item with arguments "x" and "y" | |
*/ | |
ROT.Path.prototype.compute = function(fromX, fromY, callback) { | |
} | |
ROT.Path.prototype._getNeighbors = function(cx, cy) { | |
var result = []; | |
for (var i=0;i<this._dirs.length;i++) { | |
var dir = this._dirs[i]; | |
var x = cx + dir[0]; | |
var y = cy + dir[1]; | |
if (!this._passableCallback(x, y)) { continue; } | |
result.push([x, y]); | |
} | |
return result; | |
} | |
/** | |
* @class Simplified Dijkstra's algorithm: all edges have a value of 1 | |
* @augments ROT.Path | |
* @see ROT.Path | |
*/ | |
ROT.Path.Dijkstra = function(toX, toY, passableCallback, options) { | |
ROT.Path.call(this, toX, toY, passableCallback, options); | |
this._computed = {}; | |
this._todo = []; | |
this._add(toX, toY, null); | |
} | |
ROT.Path.Dijkstra.extend(ROT.Path); | |
/** | |
* Compute a path from a given point | |
* @see ROT.Path#compute | |
*/ | |
ROT.Path.Dijkstra.prototype.compute = function(fromX, fromY, callback) { | |
var key = fromX+","+fromY; | |
if (!(key in this._computed)) { this._compute(fromX, fromY); } | |
if (!(key in this._computed)) { return; } | |
var item = this._computed[key]; | |
while (item) { | |
callback(item.x, item.y); | |
item = item.prev; | |
} | |
} | |
/** | |
* Compute a non-cached value | |
*/ | |
ROT.Path.Dijkstra.prototype._compute = function(fromX, fromY) { | |
while (this._todo.length) { | |
var item = this._todo.shift(); | |
if (item.x == fromX && item.y == fromY) { return; } | |
var neighbors = this._getNeighbors(item.x, item.y); | |
for (var i=0;i<neighbors.length;i++) { | |
var neighbor = neighbors[i]; | |
var x = neighbor[0]; | |
var y = neighbor[1]; | |
var id = x+","+y; | |
if (id in this._computed) { continue; } /* already done */ | |
this._add(x, y, item); | |
} | |
} | |
} | |
ROT.Path.Dijkstra.prototype._add = function(x, y, prev) { | |
var obj = { | |
x: x, | |
y: y, | |
prev: prev | |
} | |
this._computed[x+","+y] = obj; | |
this._todo.push(obj); | |
} | |
/** | |
* @class Simplified A* algorithm: all edges have a value of 1 | |
* @augments ROT.Path | |
* @see ROT.Path | |
*/ | |
ROT.Path.AStar = function(toX, toY, passableCallback, options) { | |
ROT.Path.call(this, toX, toY, passableCallback, options); | |
this._todo = []; | |
this._done = {}; | |
this._fromX = null; | |
this._fromY = null; | |
} | |
ROT.Path.AStar.extend(ROT.Path); | |
/** | |
* Compute a path from a given point | |
* @see ROT.Path#compute | |
*/ | |
ROT.Path.AStar.prototype.compute = function(fromX, fromY, callback) { | |
this._todo = []; | |
this._done = {}; | |
this._fromX = fromX; | |
this._fromY = fromY; | |
this._add(this._toX, this._toY, null); | |
while (this._todo.length) { | |
var item = this._todo.shift(); | |
if (item.x == fromX && item.y == fromY) { break; } | |
var neighbors = this._getNeighbors(item.x, item.y); | |
for (var i=0;i<neighbors.length;i++) { | |
var neighbor = neighbors[i]; | |
var x = neighbor[0]; | |
var y = neighbor[1]; | |
var id = x+","+y; | |
if (id in this._done) { continue; } | |
this._add(x, y, item); | |
} | |
} | |
var item = this._done[fromX+","+fromY]; | |
if (!item) { return; } | |
while (item) { | |
callback(item.x, item.y); | |
item = item.prev; | |
} | |
} | |
ROT.Path.AStar.prototype._add = function(x, y, prev) { | |
var obj = { | |
x: x, | |
y: y, | |
prev: prev, | |
g: (prev ? prev.g+1 : 0), | |
h: this._distance(x, y) | |
} | |
this._done[x+","+y] = obj; | |
/* insert into priority queue */ | |
var f = obj.g + obj.h; | |
for (var i=0;i<this._todo.length;i++) { | |
var item = this._todo[i]; | |
if (f < item.g + item.h) { | |
this._todo.splice(i, 0, obj); | |
return; | |
} | |
} | |
this._todo.push(obj); | |
} | |
ROT.Path.AStar.prototype._distance = function(x, y) { | |
switch (this._options.topology) { | |
case 4: | |
return (Math.abs(x-this._fromX) + Math.abs(y-this._fromY)); | |
break; | |
case 6: | |
var dx = Math.abs(x - this._fromX); | |
var dy = Math.abs(y - this._fromY); | |
return dy + Math.max(0, (dx-dy)/2); | |
break; | |
case 8: | |
return Math.max(Math.abs(x-this._fromX), Math.abs(y-this._fromY)); | |
break; | |
} | |
} | |