Spaces:
Running
Running
function Map(display, __game) { | |
/* private variables */ | |
var __player; | |
var __grid; | |
var __dynamicObjects = []; | |
var __objectDefinitions; | |
var __lines; | |
var __dom; | |
var __domCSS = ''; | |
var __allowOverwrite; | |
var __keyDelay; | |
var __refreshRate; | |
var __intervals = []; | |
var __chapterHideTimeout; | |
/* unexposed variables */ | |
this._properties = {}; | |
this._display = display; | |
this._dummy = false; // overridden by dummyMap in validate.js | |
this._status = ''; | |
/* wrapper */ | |
function wrapExposedMethod(f, map) { | |
return function () { | |
var args = arguments; | |
return __game._callUnexposedMethod(function () { | |
return f.apply(map, args); | |
}); | |
}; | |
}; | |
/* unexposed getters */ | |
this._getObjectDefinition = function(objName) { | |
if (__game._isPlayerCodeRunning()) { throw 'Forbidden method call: map._getObjectDefinition()';} | |
return __objectDefinitions[objName]; | |
}; | |
this._getObjectDefinitions = function() { | |
if (__game._isPlayerCodeRunning()) { throw 'Forbidden method call: map._getObjectDefinitions()';} | |
return __objectDefinitions; | |
}; | |
this._getGrid = function () { | |
if (__game._isPlayerCodeRunning()) { throw 'Forbidden method call: map._getGrid()';} | |
return __grid; | |
}; | |
this._getLines = function() { | |
if (__game._isPlayerCodeRunning()) { throw 'Forbidden method call: map._getLines()';} | |
return __lines; | |
}; | |
/* exposed getters */ | |
this.getDynamicObjects = function () { | |
// copy dynamic object list to fix issue#166 | |
var copy = []; | |
for (var i = 0; i < __dynamicObjects.length; i++) { | |
copy[i] = __dynamicObjects[i]; | |
} | |
return copy; | |
}; | |
this.getPlayer = function () { return __player; }; | |
this.getWidth = function () { return __game._dimensions.width; }; | |
this.getHeight = function () { return __game._dimensions.height; }; | |
/* unexposed methods */ | |
this._reset = function () { | |
if (__game._isPlayerCodeRunning()) { throw 'Forbidden method call: map._reset()';} | |
__objectDefinitions = __game.getListOfObjects(); | |
this._display.clear(); | |
__grid = new Array(__game._dimensions.width); | |
for (var x = 0; x < __game._dimensions.width; x++) { | |
__grid[x] = new Array(__game._dimensions.height); | |
for (var y = 0; y < __game._dimensions.height; y++) { | |
__grid[x][y] = {type: 'empty'}; | |
} | |
} | |
this.getDynamicObjects().forEach(function (obj) { | |
obj._destroy(true); | |
}); | |
__dynamicObjects = []; | |
__player = null; | |
this._clearIntervals(); | |
__lines = []; | |
__dom = ''; | |
this._overrideKeys = {}; | |
// preload stylesheet for DOM level | |
$.get('styles/dom.css', function (css) { | |
__domCSS = css; | |
}); | |
this.finalLevel = false; | |
this._callbackValidationFailed = false; | |
}; | |
this._clearIntervals = function() { | |
if (__game._isPlayerCodeRunning()) { throw 'Forbidden method call: map._clearIntervals()';} | |
for (var i = 0; i < __intervals.length; i++) { | |
clearInterval(__intervals[i]); | |
} | |
__intervals = []; | |
}; | |
this._ready = function () { | |
if (__game._isPlayerCodeRunning()) { throw 'Forbidden method call: map._ready()';} | |
// set refresh rate if one is specified | |
if (__refreshRate) { | |
// wrapExposedMethod is necessary here to prevent the `_moveToNextLevel` | |
// call from breaking | |
this.startTimer(wrapExposedMethod(function () { | |
// refresh the map | |
this.refresh(); | |
// check for nonstandard victory condition | |
__game._checkObjective() | |
}, this), __refreshRate); | |
} | |
}; | |
this._setProperties = function (mapProperties) { | |
if (__game._isPlayerCodeRunning()) { throw 'Forbidden method call: map._setProperties()';} | |
// set defaults | |
this._properties = {}; | |
__allowOverwrite = false; | |
__keyDelay = 0; | |
__refreshRate = null; | |
// now set any properties that were passed in | |
if (mapProperties) { | |
this._properties = mapProperties; | |
if (mapProperties.allowOverwrite === true) { | |
__allowOverwrite = true; | |
} | |
if (mapProperties.keyDelay) { | |
__keyDelay = mapProperties.keyDelay; | |
} | |
if (mapProperties.refreshRate) { | |
__refreshRate = mapProperties.refreshRate; | |
} | |
} | |
}; | |
this._canMoveTo = function (x, y, myType) { | |
if (__game._isPlayerCodeRunning()) { throw 'Forbidden method call: map._canMoveTo()';} | |
var x = Math.floor(x); var y = Math.floor(y); | |
if (x < 0 || x >= __game._dimensions.width || y < 0 || y >= __game._dimensions.height) { | |
return false; | |
} | |
// look for static objects that can serve as obstacles | |
var objType = __grid[x][y].type; | |
var object = __objectDefinitions[objType]; | |
if (object.impassable) { | |
if (myType && object.passableFor && object.passableFor.indexOf(myType) > -1) { | |
// this object is of a type that can pass the obstacle | |
return true; | |
} else if (typeof object.impassable === 'function') { | |
// the obstacle is impassable only in certain circumstances | |
return this._validateCallback(function () { | |
return !object.impassable(__player, object); | |
}); | |
} else { | |
// the obstacle is always impassable | |
return false; | |
} | |
} else if (myType && object.impassableFor && object.impassableFor.indexOf(myType) > -1) { | |
// this object is of a type that cannot pass the obstacle | |
return false; | |
} else { | |
// no obstacle | |
return true; | |
} | |
}; | |
// Returns the object of the given type closest to target coordinates | |
this._findNearestToPoint = function (type, targetX, targetY) { | |
if (__game._isPlayerCodeRunning()) { throw 'Forbidden method call: map._findNearestToPoint()';} | |
var foundObjects = []; | |
// look for static objects | |
for (var x = 0; x < this.getWidth(); x++) { | |
for (var y = 0; y < this.getHeight(); y++) { | |
if (__grid[x][y].type === type) { | |
foundObjects.push({x: x, y: y}); | |
} | |
} | |
} | |
// look for dynamic objects | |
for (var i = 0; i < __dynamicObjects.length; i++) { | |
var object = __dynamicObjects[i]; | |
if (object.getType() === type) { | |
foundObjects.push({x: object.getX(), y: object.getY()}); | |
} | |
} | |
// look for player | |
if (type === 'player') { | |
foundObjects.push({x: __player.getX(), y: __player.getY()}); | |
} | |
var dists = []; | |
for (var i = 0; i < foundObjects.length; i++) { | |
var obj = foundObjects[i]; | |
dists[i] = Math.sqrt(Math.pow(targetX - obj.x, 2) + Math.pow(targetY - obj.y, 2)); | |
// We want to find objects distinct from ourselves | |
if (dists[i] === 0) { | |
dists[i] = 999; | |
} | |
} | |
var minDist = Math.min.apply(Math, dists); | |
var closestTarget = foundObjects[dists.indexOf(minDist)]; | |
return closestTarget; | |
}; | |
this._isPointOccupiedByDynamicObject = function (x, y) { | |
if (__game._isPlayerCodeRunning()) { throw 'Forbidden method call: map._isPointOccupiedByDynamicObject()';} | |
var x = Math.floor(x); var y = Math.floor(y); | |
for (var i = 0; i < __dynamicObjects.length; i++) { | |
var object = __dynamicObjects[i]; | |
if (object.getX() === x && object.getY() === y) { | |
return true; | |
} | |
} | |
return false; | |
}; | |
this._findDynamicObjectAtPoint = function (x, y) { | |
if (__game._isPlayerCodeRunning()) { throw 'Forbidden method call: map._findDynamicObjectAtPoint()';} | |
var x = Math.floor(x); var y = Math.floor(y); | |
for (var i = 0; i < __dynamicObjects.length; i++) { | |
var object = __dynamicObjects[i]; | |
if (object.getX() === x && object.getY() === y) { | |
return object; | |
} | |
} | |
return false; | |
}; | |
this._moveAllDynamicObjects = function () { | |
if (__game._isPlayerCodeRunning()) { throw 'Forbidden method call: map._moveAllDynamicObjects()';} | |
// the way things work right now, teleporters must take precedence | |
// over all other objects -- otherwise, pointers.jsx will not work | |
// correctly. | |
// TODO: make this not be the case | |
// "move" teleporters | |
__dynamicObjects.filter(function (object) { | |
return (object.getType() === 'teleporter'); | |
}).forEach(function(object) { | |
object._onTurn(); | |
}); | |
// move everything else | |
__dynamicObjects.filter(function (object) { | |
return (object.getType() !== 'teleporter'); | |
}).forEach(function(object) { | |
object._onTurn(); | |
}); | |
// refresh only at the end | |
this.refresh(); | |
}; | |
this._removeItemFromMap = function (x, y, klass) { | |
if (__game._isPlayerCodeRunning()) { throw 'Forbidden method call: map._removeItemFromMap()';} | |
var x = Math.floor(x); var y = Math.floor(y); | |
if (__grid[x][y].type === klass) { | |
__grid[x][y].type = 'empty'; | |
} | |
}; | |
this._reenableMovementForPlayer = function (player) { | |
if (__game._isPlayerCodeRunning()) { throw 'Forbidden method call: map._reenableMovementForPlayer()';} | |
if (!this._callbackValidationFailed) { | |
setTimeout(function () { | |
player._canMove = true; | |
}, __keyDelay); | |
} | |
}; | |
this._hideChapter = function() { | |
if (__game._isPlayerCodeRunning()) { throw 'Forbidden method call: map._hideChapter()';} | |
// start fading out chapter immediately | |
// unless it's a death message, in which case wait 2.5 sec | |
clearInterval(__chapterHideTimeout); | |
__chapterHideTimeout = setTimeout(function () { | |
$('#chapter').fadeOut(1000); | |
}, $('#chapter').hasClass('death') ? 2500 : 0); | |
}; | |
this._refreshDynamicObjects = function() { | |
if (__game._isPlayerCodeRunning()) { throw 'Forbidden method call: map._refreshDynamicObjects()';} | |
__dynamicObjects = __dynamicObjects.filter(function (obj) { return !obj.isDestroyed(); }); | |
}; | |
this._countTimers = function() { | |
if (__game._isPlayerCodeRunning()) { throw 'Forbidden method call: map._countTimers()';} | |
return __intervals.length; | |
} | |
/* (unexposed) wrappers for game methods */ | |
this._startOfStartLevelReached = function() { | |
__game._startOfStartLevelReached = true; | |
}; | |
this._endOfStartLevelReached = function() { | |
__game._endOfStartLevelReached = true; | |
}; | |
this._playSound = function (sound) { | |
if (__game._isPlayerCodeRunning()) { throw 'Forbidden method call: map._playSound()';} | |
__game.sound.playSound(sound); | |
}; | |
this._validateCallback = function (callback) { | |
if (__game._isPlayerCodeRunning()) { throw 'Forbidden method call: map._validateCallback()';} | |
return __game.validateCallback(callback); | |
}; | |
/* exposed methods */ | |
this.refresh = wrapExposedMethod(function () { | |
if (__dom) { | |
this._display.clear(); | |
var domHTML = __dom[0].outerHTML | |
.replace(/"/g, "'") | |
.replace(/<hr([^>]*)>/g, '<hr $1 />') | |
.replace(/<img([^>]*)>/g, '<img $1 />'); | |
this._display.renderDom(domHTML, __domCSS); | |
} else { | |
this._display.drawAll(this); | |
} | |
// rewrite any status messages | |
if (this._status) { | |
this._display.writeStatus(this._status); | |
} | |
__game.drawInventory(); | |
}, this); | |
this.countObjects = wrapExposedMethod(function (type) { | |
var count = 0; | |
// count static objects | |
for (var x = 0; x < this.getWidth(); x++) { | |
for (var y = 0; y < this.getHeight(); y++) { | |
if (__grid[x][y].type === type) { | |
count++; | |
} | |
} | |
} | |
// count dynamic objects | |
__dynamicObjects.forEach(function (obj) { | |
if (obj.getType() === type) { | |
count++; | |
} | |
}) | |
return count; | |
}, this); | |
this.placeObject = wrapExposedMethod(function (x, y, type) { | |
var x = Math.floor(x); var y = Math.floor(y); | |
if (!__objectDefinitions[type]) { | |
throw "There is no type of object named " + type + "!"; | |
} | |
var minLevel = __objectDefinitions[type].minimumLevel | |
if (minLevel && __game._currentLevel < minLevel) { | |
throw type.capitalize() + "s are not available until level " + minLevel; | |
} | |
if (__player && x == __player.getX() && y == __player.getY()) { | |
throw "Can't place object on top of player!"; | |
} | |
if (typeof(__grid[x]) === 'undefined' || typeof(__grid[x][y]) === 'undefined') { | |
return; | |
// throw "Not a valid location to place an object!"; | |
} | |
if (__objectDefinitions[type].type === 'dynamic') { | |
// dynamic object | |
__dynamicObjects.push(new DynamicObject(this, type, x, y, __game)); | |
} else { | |
// static object | |
if (__grid[x][y].type === 'empty' || __grid[x][y].type === type || __allowOverwrite) { | |
__grid[x][y].type = type; | |
} else { | |
throw "There is already an object at (" + x + ", " + y + ")!"; | |
} | |
} | |
}, this); | |
this.placePlayer = wrapExposedMethod(function (x, y) { | |
var x = Math.floor(x); var y = Math.floor(y); | |
if (__player) { | |
throw "Can't place player twice!"; | |
} | |
__player = new __game._playerPrototype(x, y, this, __game); | |
this._display.drawAll(this); | |
}, this); | |
this.createFromGrid = wrapExposedMethod(function (grid, tiles, xOffset, yOffset) { | |
for (var y = 0; y < grid.length; y++) { | |
var line = grid[y]; | |
for (var x = 0; x < line.length; x++) { | |
var tile = line[x]; | |
var type = tiles[tile]; | |
if (type === 'player') { | |
this.placePlayer(x + xOffset, y + yOffset); | |
} else if (type) { | |
this.placeObject(x + xOffset, y + yOffset, type); | |
} | |
} | |
} | |
}, this); | |
this.setSquareColor = wrapExposedMethod(function (x, y, bgColor) { | |
var x = Math.floor(x); var y = Math.floor(y); | |
__grid[x][y].bgColor = bgColor; | |
}, this); | |
this.defineObject = wrapExposedMethod(function (name, properties) { | |
if (__objectDefinitions[name]) { | |
throw "There is already a type of object named " + name + "!"; | |
} | |
if (properties.interval && properties.interval < 100) { | |
throw "defineObject(): minimum interval is 100 milliseconds"; | |
} | |
__objectDefinitions[name] = properties; | |
}, this); | |
this.getObjectTypeAt = wrapExposedMethod(function (x, y) { | |
var x = Math.floor(x); var y = Math.floor(y); | |
// Bazek: We should always check, if the coordinates are inside of map! | |
if (x >= 0 && x < this.getWidth() && y >= 0 && y < this.getHeight()) | |
return __grid[x][y].type; | |
else | |
return ''; | |
}, this); | |
this.getAdjacentEmptyCells = wrapExposedMethod(function (x, y) { | |
var x = Math.floor(x); var y = Math.floor(y); | |
var map = this; | |
var actions = ['right', 'down', 'left', 'up']; | |
var adjacentEmptyCells = []; | |
$.each(actions, function (i, action) { | |
switch (actions[i]) { | |
case 'right': | |
var child = [x+1, y]; | |
break; | |
case 'left': | |
var child = [x-1, y]; | |
break; | |
case 'down': | |
var child = [x, y+1]; | |
break; | |
case 'up': | |
var child = [x, y-1]; | |
break; | |
} | |
// Bazek: We need to check, if child is inside of map! | |
var childInsideMap = child[0] >= 0 && child[0] < map.getWidth() && child[1] >= 0 && child[1] < map.getHeight(); | |
if (childInsideMap && map.getObjectTypeAt(child[0], child[1]) === 'empty') { | |
adjacentEmptyCells.push([child, action]); | |
} | |
}); | |
return adjacentEmptyCells; | |
}, this); | |
this.startTimer = wrapExposedMethod(function(timer, delay) { | |
if (!delay) { | |
throw "startTimer(): delay not specified" | |
} else if (delay < 25) { | |
throw "startTimer(): minimum delay is 25 milliseconds"; | |
} | |
var validate = this._validateCallback; | |
__intervals.push(setInterval(function(){validate(timer)}, delay)); | |
}, this); | |
this.timeout = wrapExposedMethod(function(timer, delay) { | |
if (!delay) { | |
throw "timeout(): delay not specified" | |
} else if (delay < 25) { | |
throw "timeout(): minimum delay is 25 milliseconds"; | |
} | |
var validate = this._validateCallback; | |
__intervals.push(setTimeout(function(){validate(timer)}, delay)); | |
}, this); | |
this.displayChapter = wrapExposedMethod(function(chapterName, cssClass) { | |
if (__game._displayedChapters.indexOf(chapterName) === -1) { | |
$('#chapter').html(chapterName.replace("\n","<br>")); | |
$('#chapter').removeClass().show(); | |
if (cssClass) { | |
$('#chapter').addClass(cssClass); | |
} else { | |
__game._displayedChapters.push(chapterName); | |
} | |
setTimeout(function () { | |
$('#chapter').fadeOut(); | |
}, 5 * 1000); | |
} | |
}, this); | |
this.writeStatus = wrapExposedMethod(function(status) { | |
if (this._status) { | |
// refresh to hide the old status message | |
this._status = ""; | |
this.refresh(); | |
} | |
this._status = status; | |
this._display.writeStatus(status); | |
}, this); | |
// used by validators | |
// returns true iff called at the start of the level (that is, on DummyMap) | |
// returns false iff called by validateCallback (that is, on the actual map) | |
this.isStartOfLevel = wrapExposedMethod(function () { | |
return this._dummy; | |
}, this); | |
/* canvas-related stuff */ | |
this.getCanvasContext = wrapExposedMethod(function() { | |
var ctx = $('#drawingCanvas')[0].getContext('2d'); | |
if(!this._dummy) { | |
var opts = this._display.getOptions(); | |
ctx.font = opts.fontSize+"px " +opts.fontFamily; | |
} | |
return ctx; | |
}, this); | |
this.getCanvasCoords = wrapExposedMethod(function() { | |
var x, y; | |
if(arguments.length == 1) { | |
var obj = arguments[0]; | |
x = obj.getX(); | |
y = obj.getY(); | |
} else { | |
x = arguments[0]; | |
y = arguments[1]; | |
} | |
var canvas = $('#drawingCanvas')[0]; | |
return { | |
x: (x + 0.5) * canvas.width / __game._dimensions.width, | |
y: (y + 0.5) * canvas.height / __game._dimensions.height | |
}; | |
}, this); | |
this.getRandomColor = wrapExposedMethod(function(start, end) { | |
var mean = [ | |
Math.floor((start[0] + end[0]) / 2), | |
Math.floor((start[1] + end[1]) / 2), | |
Math.floor((start[2] + end[2]) / 2) | |
]; | |
var std = [ | |
Math.floor((end[0] - start[0]) / 2), | |
Math.floor((end[1] - start[1]) / 2), | |
Math.floor((end[2] - start[2]) / 2) | |
]; | |
return ROT.Color.toHex(ROT.Color.randomize(mean, std)); | |
}, this); | |
this.createLine = wrapExposedMethod(function(start, end, callback) { | |
__lines.push({'start': start, 'end': end, 'callback': callback}); | |
}, this); | |
this.testLineCollisions = wrapExposedMethod(function(player) { | |
var threshold = 7; | |
var playerCoords = this.getCanvasCoords(player); | |
__lines.forEach(function (line) { | |
if (pDistance(playerCoords.x, playerCoords.y, | |
line.start[0], line.start[1], | |
line.end[0], line.end[1]) < threshold) { | |
__game.validateCallback(function() { | |
line.callback(__player); | |
}); | |
} | |
}) | |
}, this); | |
/* for DOM manipulation level */ | |
this.getDOM = wrapExposedMethod(function () { | |
return __dom; | |
}) | |
this.createFromDOM = wrapExposedMethod(function(dom) { | |
__dom = dom; | |
}, this); | |
this.updateDOM = wrapExposedMethod(function(dom) { | |
__dom = dom; | |
}, this); | |
this.overrideKey = wrapExposedMethod(function(keyName, callback) { | |
this._overrideKeys[keyName] = callback; | |
}, this); | |
/* validators */ | |
this.validateAtLeastXObjects = wrapExposedMethod(function(num, type) { | |
var count = this.countObjects(type); | |
if (count < num) { | |
throw 'Not enough ' + type + 's on the map! Expected: ' + num + ', found: ' + count; | |
} | |
}, this); | |
this.validateAtMostXObjects = wrapExposedMethod(function(num, type) { | |
var count = this.countObjects(type); | |
if (count > num) { | |
throw 'Too many ' + type + 's on the map! Expected: ' + num + ', found: ' + count; | |
} | |
}, this); | |
this.validateExactlyXManyObjects = wrapExposedMethod(function(num, type) { | |
var count = this.countObjects(type); | |
if (count != num) { | |
throw 'Wrong number of ' + type + 's on the map! Expected: ' + num + ', found: ' + count; | |
} | |
}, this); | |
this.validateAtMostXDynamicObjects = wrapExposedMethod(function(num) { | |
var count = this.getDynamicObjects().length; | |
if (count > num) { | |
throw 'Too many dynamic objects on the map! Expected: ' + num + ', found: ' + count; | |
} | |
}, this); | |
this.validateNoTimers = wrapExposedMethod(function() { | |
var count = this._countTimers(); | |
if (count > 0) { | |
throw 'Too many timers set on the map! Expected: 0, found: ' + count; | |
} | |
}, this); | |
this.validateAtLeastXLines = wrapExposedMethod(function(num) { | |
var count = this._getLines().length; | |
if (count < num) { | |
throw 'Not enough lines on the map! Expected: ' + num + ', found: ' + count; | |
} | |
}, this); | |
/* initialization */ | |
this._reset(); | |
// call secureObject to prevent user code from tampering with private attributes | |
__game.secureObject(this, "map"); | |
} | |