var Maze = (function (my) {
/**
* Ray caster class. Casts a cone of rays from the player to the closest obstacle and uses the distance to draw a block based 3D maze.
* @constructor
* @memberOf Maze
*/
var Maze_Raycaster = my.Maze_Raycaster = function () {
this.initialize.apply(this, arguments);
};
Maze_Raycaster.prototype = Object.create(Object.prototype);
Maze_Raycaster.prototype.constructor = Maze_Raycaster;
/**
* Initializes the rayscaster.
* @param screenSprite Bitmap on which the scene will be drawn.
* @param scaleFactor Scale factor for framerate adjustement.
* @param auto If true, the scale factor will automatically change to keep an high framerate.
*/
Maze_Raycaster.prototype.initialize = function (screenSprite, scaleFactor, auto) {
this.screenSprite = screenSprite;
this.scaleFactor = scaleFactor;
this.auto = auto;
this.tileWidth = $gameMap.tileWidth();
this.tileHeight = $gameMap.tileHeight();
this.zBuffer = Array(this.screenSprite.width);
};
Maze_Raycaster.prototype.setQuality = function (scaleFactor, auto) {
this.scaleFactor = scaleFactor;
this.auto = auto;
};
/**
* Adjusts the rendering framerate by drawing smaller images scaled up.
* A quality decrease is 10 times faster than increase and happens immediately, while a quality increase will wait for
* the average FPS over a long period.
*/
Maze_Raycaster.prototype.adjustFrameRate = function () {
this.smoothFps = (this.smoothFps * 0.95 + Graphics._fpsMeter.fps * 0.05) || Graphics._fpsMeter.fps;
// Decrease must be immediate, so it uses the unsmoothed value.
if (Graphics._fpsMeter.fps < 20 && this.scaleFactor + 0.1 <= 4) {
this.scaleFactor += 0.1;
}
// Increase should be slow, so it uses the smoothed value.
else if (this.smoothFps > 59 && this.scaleFactor - 0.01 >= 1) {
this.scaleFactor -= 0.01;
// Reset the moving average
this.smoothFps = 30;
}
};
/**
* Retrieves the wall texture for a tile.
* @param x X coordinate of the tile on map.
* @param y Y coordinate of the tile on map.
* @returns {{bitmap: *, x: number, y: number}} Structure containing the bitmap and the offset for the chosen tile.
*/
Maze_Raycaster.prototype.getTexture = function (x, y) {
var ret;
var tileId = $gameMap.tileId(x, y, 0);
// If it's an autotile.
if (Tilemap.isAutotile(tileId)) {
var kind = Tilemap.getAutotileKind(tileId);
var shape = Tilemap.getAutotileShape(tileId);
var tx = kind % 8;
var ty = Math.floor(kind / 8);
var bx = 0;
var by = 0;
var setNumber = 0;
if (Tilemap.isTileA1(tileId)) {
setNumber = 0;
if (kind === 0) {
bx = 0;
by = 0;
} else if (kind === 1) {
bx = 0;
by = 3;
} else if (kind === 2) {
bx = 6;
by = 0;
} else if (kind === 3) {
bx = 6;
by = 3;
} else {
bx = Math.floor(tx / 4) * 8;
by = ty * 6 + Math.floor(tx / 2) % 2 * 3;
bx += 6;
by += this.animationFrame % 3;
}
} else if (Tilemap.isTileA2(tileId)) {
setNumber = 1;
bx = tx * 2;
by = (ty - 2) * 3;
} else if (Tilemap.isTileA3(tileId)) {
setNumber = 2;
bx = tx * 2;
by = (ty - 6) * 2;
} else if (Tilemap.isTileA4(tileId)) {
setNumber = 3;
bx = tx * 2;
by = Math.floor((ty - 10) * 2.5 + (ty % 2 === 1 ? 0.5 : 0));
}
ret = {
bitmap: my.bmps[setNumber],
x: bx * this.tileWidth,
y: by * this.tileHeight
};
}
else { // If it's a normal tile.
var setNumber = 0;
if (Tilemap.isTileA5(tileId)) {
setNumber = 4;
} else {
setNumber = 5 + Math.floor(tileId / 256);
}
var sx = (Math.floor(tileId / 128) % 2 * 8 + tileId % 8) * this.tileWidth;
var sy = (Math.floor(tileId % 256 / 8) % 16) * this.tileHeight;
ret = {
bitmap: my.bmps[setNumber],
x: sx,
y: sy
};
}
return ret;
};
/**
* Draws the 3D scene.
*/
Maze_Raycaster.prototype.draw = function () {
var ray;
if (this.auto) {
this.adjustFrameRate();
}
for (var i = 0; i < Math.ceil(this.screenSprite.width / this.scaleFactor); i++) {
ray = this.castRay(i * this.scaleFactor * 2 * my.player.cameraWidth / this.screenSprite.width - my.player.cameraWidth);
this.drawVLine(ray, i);
this.zBuffer[i] = ray.distance;
}
this.drawSprites();
};
/**
* Determines the player's left and right viewing angles and draws the events at the appropriate position.
*/
Maze_Raycaster.prototype.drawSprites = function () {
var cameraX = my.player.x + Math.cos(my.player.direction) * my.player.cameraDistance;
var cameraY = my.player.y + Math.sin(my.player.direction) * my.player.cameraDistance;
var a = my.player.direction + Math.atan2(-my.player.cameraWidth, my.player.cameraDistance);
var b = my.player.direction + Math.atan2(my.player.cameraWidth, my.player.cameraDistance);
if (a < 0) {
a += 2 * Math.PI;
}
else if (a >= 2 * Math.PI) {
a -= 2 * Math.PI;
}
if (b < 0) {
b += 2 * Math.PI;
}
else if (b >= 2 * Math.PI) {
b -= 2 * Math.PI;
}
if (b < a) {
b += 2 * Math.PI;
}
my.controller.events.forEach(e => {
e.draw3D(this.screenSprite._bitmap, this.zBuffer, this.scaleFactor, a, b);
});
};
/**
* Draws a single slice of a wall, determining the correct texture to apply.
* The east and west sides of the wall are drawn with a darker shadow.
* @param ray Ray which has hit the wall.
* @param cameraX On screen x coordinate of the wall.
*/
Maze_Raycaster.prototype.drawVLine = function (ray, cameraX) {
var height = Math.floor(this.screenSprite.height / ray.distance);
var y0 = (this.screenSprite.height - height) / 2;
var y1 = (this.screenSprite.height + height) / 2;
var textureX;
if (ray.side === "EW") {
textureX = my.player.y + ray.distance * Math.sin(ray.direction);
}
else {
textureX = my.player.x + ray.distance * Math.cos(ray.direction);
}
textureX -= Math.floor(textureX);
textureX = Math.floor(textureX * this.tileWidth);
if (ray.side === "EW" && Math.cos(ray.direction) > 0 || ray.side === "NS" && Math.sin(ray.direction) < 0) {
textureX = this.tileWidth - textureX - 1;
}
var texture = this.getTexture(ray.tileX, ray.tileY);
this.screenSprite._bitmap.blt(texture.bitmap, texture.x + textureX, texture.y, 1, this.tileHeight, Math.floor(cameraX * this.scaleFactor), y0, Math.ceil(this.scaleFactor), y1 - y0);
if (ray.side === "EW") {
this.screenSprite._bitmap._context.fillStyle = "rgba(0, 0, 0, 0.25)";
this.screenSprite._bitmap._context.fillRect(Math.floor(cameraX * this.scaleFactor), y0, this.scaleFactor, y1 - y0);
}
};
/**
* Casts a ray from the player until it hits a wall.
* @param cameraX On screen x coordinate of the ray.
* @returns {{distance: number, side: string, tileX: *, tileY: *, direction: *}} Ray data structure, contains the coordinates of the wall hit, its side and distance and direction from the player.
*/
Maze_Raycaster.prototype.castRay = function (cameraX) {
var distance = 0;
var rayX = my.player.x / my.blockWidth;
var rayY = my.player.y / my.blockWidth;
var rayTileX = my.player.tileX;
var rayTileY = my.player.tileY;
var rayDir = my.player.direction + Math.atan2(cameraX, my.player.cameraDistance);
var dx = Math.abs(1 / Math.cos(rayDir));
var dy = Math.abs(1 / Math.sin(rayDir));
var sx = +1;
var sy = +1;
var rayDistX;
var rayDistY;
var lastSide = "";
if (Math.cos(rayDir) < 0) {
rayDistX = (rayX - rayTileX) * dx;
sx = -1;
}
else {
rayDistX = (rayTileX - rayX + 1) * dx;
}
if (Math.sin(rayDir) < 0) {
rayDistY = (rayY - rayTileY) * dy;
sy = -1;
}
else {
rayDistY = (rayTileY - rayY + 1) * dy;
}
while ($gameMap.checkPassage(rayTileX, rayTileY, 0x0f) && rayTileX >= 0 && rayTileX < $gameMap.width() && rayTileY >= 0 && rayTileY < $gameMap.height()) {
if (rayDistX < rayDistY) {
rayDistX += dx;
rayTileX += sx;
lastSide = "EW";
}
else {
rayDistY += dy;
rayTileY += sy;
lastSide = "NS";
}
}
if (lastSide === "EW") {
distance = Math.abs((rayTileX - rayX + (1 - sx) / 2) / Math.cos(rayDir));
}
else {
distance = Math.abs((rayTileY - rayY + (1 - sy) / 2) / Math.sin(rayDir));
}
return {
distance: distance * my.blockWidth,
side: lastSide,
tileX: rayTileX,
tileY: rayTileY,
direction: rayDir
};
};
return my;
}(Maze || {}));