//=============================================================================
// Maze.js
//=============================================================================
/*:
@plugindesc Simple raycasting engine which turns the game maps into 3D mazes.
@author Hash'ak'Gik
@param resume
@desc Resume string.
@default Resume
@param retry
@desc Retry string.
@default Retry
@param quit
@desc Quit string.
@default Quit
@param yes
@desc Yes string.
@default Yes
@param no
@desc No string.
@default No
@param quality
@desc Quality setting string.
@default Quality
@param low
@desc Low quality string.
@default Low
@param medium
@desc Medium quality string.
@default Medium
@param High
@desc High quality string.
@default High
@param auto
@desc Automatic quality string.
@default Auto
@param block_width
@desc Size of each 3D block.
@default 1
@param gen_floor
@desc Tile id for the generated maps' floor.
@default 2860
@param gen_wall
@desc Tile id for the generated maps' walls.
@default 6335
@param gen_tileset_id
@desc Tileset id for the generated maps.
@default 3
@help
This plugin generates a 3D maze from an existing map or randomly and can work in two different "modes":
- Normal mode: the maze is simply a different appearance of a game map (running events won't be stopped),
- Maze mode: the maze acts as a minigame which will pause any event until it's won ($mazeClear = true) or lost ($mazeClear = false).
Rendering quality:
Four quality settings are available from the menu (Automatic, Low, Medium and High).
Automatic quality will start at Medium and will:
- reduce quality quickly if the framerate drops below 20 fps,
- increase quality slowly if the framerate remains above 59 fps for a long enough time.
This behaviour guarantees smooth gameplay even in canvas mode.
Compass:
During 3D mode a compass is shown on screen, its needle will be attracted by some events (see section below),
with a pull proportional to their distance from the player (i.e. a close event will pull the compass more than a farther one).
Fake goals' pull can be configured (a true goal has a strength of 1, so a fake goal with strength = 0.5 will be half as strong as a true goal,
while a fake goal with strength = 2 will be twice as strong as a real goal).
Events notes:
<goal> The maze's compass will point towards this event, usually an event with this note should include the "Maze success" plugin command,
<fake:strength> The maze's compass will be interfered by this event with the specified strength (the event might include the "Maze fail" command).
Plugin commands:
Maze on [retry [quit]]
Turns on the 3D effect without changing map (normal mode). Can enable/disable "retry" and "quit" options in the pause menu.
For example: "Maze on false true" disables the retry option, but keeps the quit option enabled.
Maze off
Turns off the 3D effect.
Maze toggle [retry [quit]]
Toggles between on and off (normal mode).
Maze map id x y direction [retry [quit]]
Turns on the 3D effect on a new map (maze mode). When quitting, the player will return to the previous map.
For example: "Maze map 2 10 15 6" loads map 002 and places the player at coordinates 10,15 facing east (direction 6).
Maze generate n [retry [quit]]
Randomly generates a map large 2 * n tiles and automatically places a single event as a goal (maze mode).
Maze success
Turns off the 3D effect, returns the player to the previous map and sets $mazeClear to true.
Maze fail
Turns off the 3D effect, returns the player to the previous map and sets $mazeClear to false (same as selecting "Quit" from the menu).
*/
/**
* Stores the maze's success state.
* True if the maze was escaped by triggering the right event, false if the maze was left from the pause menu or by triggering the wrong event.
* @typedef $mazeClear
* @type {boolean}
*/
var $mazeClear = false;
/**
* @namespace Maze
*/
var Maze = (function (my) {
var parameters = PluginManager.parameters('Maze');
my.yes = String(parameters['yes'] || "Yes");
my.no = String(parameters['no'] || "No");
my.retry = String(parameters['retry'] || "Retry");
my.quit = String(parameters['quit'] || "Quit");
my.resume = String(parameters['resume'] || "Resume");
my.quality = String(parameters['quality'] || "Quality");
my.qLow = String(parameters['low'] || "Low");
my.qMedium = String(parameters['medium'] || "Medium");
my.qHigh = String(parameters['high'] || "High");
my.qAuto = String(parameters['auto'] || "Auto");
my.blockWidth = Number(parameters['block_width'] || 1);
/**
* Size of the generated maps. It will be set from plugin commands (must be >= 4).
* @typedef genSize
* @memberOf Maze
*/
my.genSize = 0;
/**
* Tile id for the floor of generated maps.
* @typedef genFloor
* @memberOf Maze
*/
my.genFloor = Number(parameters['gen_floor'] || 2860);
/**
* Tile id for the wall of generated maps.
* @typedef genWall
* @memberOf Maze
*/
my.genWall = Number(parameters['gen_wall'] || 6335);
/**
* Tileset id for the generated maps.
* @typedef genTilesetId
* @memberOf Maze
*/
my.genTilesetId = Number(parameters['gen_tileset_id'] || 3);
var _Game_Interpreter_pluginCommand =
Game_Interpreter.prototype.pluginCommand;
Game_Interpreter.prototype.pluginCommand = function (command, args) {
_Game_Interpreter_pluginCommand.call(this, command, args);
if (command === 'Maze') {
switch (args[0]) {
case "on":
if (!(SceneManager._scene instanceof my.Scene_Maze)) {
my.oldPosition = {
id: $gameMap._mapId,
x: $gamePlayer._x,
y: $gamePlayer._y,
direction: $gamePlayer._direction
};
my.isMaze = false;
if (args[1] === "false") {
my.canRetry = false;
}
else {
my.canRetry = true;
}
if (args[2] === "false") {
my.canQuit = false;
}
else {
my.canQuit = true;
}
SceneManager.goto(my.Scene_Maze);
}
break;
case "off":
if (SceneManager._scene instanceof my.Scene_Maze) {
SceneManager.goto(Scene_Map);
}
break;
case "toggle":
if (SceneManager._scene instanceof my.Scene_Maze) {
SceneManager.goto(Scene_Map);
}
else {
my.oldPosition = {
id: $gameMap._mapId,
x: $gamePlayer._x,
y: $gamePlayer._y,
direction: $gamePlayer._direction
};
my.isMaze = false;
if (args[1] === "false") {
my.canRetry = false;
}
else {
my.canRetry = true;
}
if (args[2] === "false") {
my.canQuit = false;
}
else {
my.canQuit = true;
}
SceneManager.goto(my.Scene_Maze);
}
break;
case "map":
if (!(SceneManager._scene instanceof my.Scene_Maze)) {
my.isMaze = true;
my.oldPosition = {
id: $gameMap._mapId,
x: $gamePlayer._x,
y: $gamePlayer._y,
direction: $gamePlayer._direction
};
var id = Number(args[1]);
var x = Number(args[2]);
var y = Number(args[3]);
var dir = Number(args[4]);
$gamePlayer.reserveTransfer(id, x, y, dir, 2);
if (args[5] === "false") {
my.canRetry = false;
}
else {
my.canRetry = true;
}
if (args[6] === "false") {
my.canQuit = false;
}
else {
my.canQuit = true;
}
SceneManager.goto(my.Scene_Maze);
}
break;
case "generate":
if (!(SceneManager._scene instanceof my.Scene_Maze)) {
my.isMaze = true;
my.oldPosition = {
id: $gameMap._mapId,
x: $gamePlayer._x,
y: $gamePlayer._y,
direction: $gamePlayer._direction
};
my.genSize = Math.max(Number(args[1]), 4);
$gamePlayer.reserveTransfer(-1, Math.floor(my.genSize / 2) * 2, Math.floor(my.genSize / 2) * 2, 2, 2);
if (args[2] === "false") {
my.canRetry = false;
}
else {
my.canRetry = true;
}
if (args[3] === "false") {
my.canQuit = false;
}
else {
my.canQuit = true;
}
SceneManager.goto(my.Scene_Maze);
}
break;
case "success":
if (SceneManager._scene instanceof my.Scene_Maze) {
$mazeClear = true;
if (my.isMaze) {
$gamePlayer.reserveTransfer(my.oldPosition.id, my.oldPosition.x, my.oldPosition.y, my.oldPosition.direction, 2);
}
SceneManager.goto(Scene_Map);
}
break;
case "fail":
if (SceneManager._scene instanceof my.Scene_Maze) {
$mazeClear = false;
if (my.isMaze) {
$gamePlayer.reserveTransfer(my.oldPosition.id, my.oldPosition.x, my.oldPosition.y, my.oldPosition.direction, 2);
}
SceneManager.goto(Scene_Map);
}
break;
}
}
};
/**
* Generates a random maze with a depth first algorithm ({@link https://en.wikipedia.org/wiki/Maze_generation_algorithm}). Since it's invoked by DataManager, its parameters must be passed with the variables:
* {@link Maze.genSize}, {@link Maze.genFloor}, {@link Maze.genWall} and {@link Maze.genTilesetId}.
*
* @memberOf Maze
*/
my.generateMaze = function () {
var cells = new Array(my.genSize);
for (var i = 0; i < my.genSize; i++) {
cells[i] = new Array(my.genSize);
cells[i].fill(false);
}
var maze = new Array(my.genSize * 2);
for (var i = 0; i < my.genSize * 2; i++) {
maze[i] = new Array(my.genSize * 2);
maze[i].fill(false);
}
// Keep the initial position, for player's sake.
var init = {x: Math.floor(my.genSize / 2) * 2, y: Math.floor(my.genSize / 2) * 2};
// Start from the initial position and mark the cell as visited.
var current = {x: Math.floor(my.genSize / 2), y: Math.floor(my.genSize / 2)};
cells[current.x][current.y] = true;
var unvisited = my.genSize * my.genSize - 1;
var stack = [];
var neighbours = [];
do {
unvisited = 0;
for (var i = 0; i < my.genSize; i++) {
unvisited += cells[i].filter(c => {
return !c;
}).length;
}
neighbours = [
{x: current.x - 1, y: current.y},
{x: current.x + 1, y: current.y},
{x: current.x, y: current.y - 1},
{x: current.x, y: current.y + 1}
].filter(c => {
return c.x > 0 && c.x < my.genSize - 1 && c.y > 0 && c.y < my.genSize - 1 &&
!cells[c.x][c.y];
});
// Pick a random neighbour.
if (neighbours.length > 0) {
var k = Math.floor(Math.random() * neighbours.length);
stack.push({x: current.x, y: current.y});
// Remove the wall between the current node and the selected neighbour.
maze[2 * current.x][2 * current.y] = true;
if (neighbours[k].x === current.x) {
if (neighbours[k].y === current.y + 1) {
maze[2 * current.x][2 * current.y + 1] = true;
}
else if (neighbours[k].y === current.y - 1) {
maze[2 * current.x][2 * current.y - 1] = true;
}
}
else if (neighbours[k].y === current.y) {
if (neighbours[k].x === current.x + 1) {
maze[2 * current.x + 1][2 * current.y] = true;
}
else if (neighbours[k].x === current.x - 1) {
maze[2 * current.x - 1][2 * current.y] = true;
}
}
// Mark the current cell as visited and move to the selected neighbour.
current = {x: neighbours[k].x, y: neighbours[k].y};
cells[current.x][current.y] = true;
}
else if (stack.length > 0) { // If there are no available neighbours, backtrack to previously visited cells.
current = stack.pop();
}
} while (unvisited > 0 && !(neighbours.length === 0 && stack.length === 0));
// Repeat as long as there are unvisited cells and there is at least one cell with available neighbours.
// Generate two events (the event with ID 0 on a map is always null, the other one will trigger a "Maze success" command).
$dataMap.events.push(null);
var ev;
// If the plugin was called from an event on map, there will be at least one event with the locked flag set.
// The maze's goal will have the same appearance of the calling event.
if ($gameMap._events.filter(e => {
return e != null && e._locked;
}).length > 0) {
ev = $gameMap._events.filter(e => {
return e != null && e._locked;
})[0].event();
ev.id = 1;
ev.note = "<goal>";
ev.pages[0].through = false;
ev.pages[0].trigger = 0;
ev.pages[0].directionFix = true;
ev.pages[0].image.direction = 2;
ev.pages[0].list = [
{
"code": 356,
"indent": 0,
"parameters": [
"Maze success"
]
},
{
"code": 0,
"indent": 0,
"parameters": []
}
];
}
else { // Otherwise, the goal will have a dummy "Actor1" appearance.
console.warn("Invoking event not found. Creating dummy event.");
ev = {
"id": 1,
"name": "EV001",
"note": "<goal>",
"pages": [{
"conditions": {
"actorId": 1,
"actorValid": false,
"itemId": 1,
"itemValid": false,
"selfSwitchCh": "A",
"selfSwitchValid": false,
"switch1Id": 1,
"switch1Valid": false,
"switch2Id": 1,
"switch2Valid": false,
"variableId": 1,
"variableValid": false,
"variableValue": 0
},
"directionFix": true,
"image": {
"tileId": 0,
"characterName": "Actor1",
"direction": 2,
"pattern": 0,
"characterIndex": 0
},
"list": [
{
"code": 356,
"indent": 0,
"parameters": [
"Maze success"
]
},
{
"code": 0,
"indent": 0,
"parameters": []
}
],
"moveFrequency": 3,
"moveRoute": {
"list": [{"code": 0, "parameters": []}],
"repeat": true,
"skippable": false,
"wait": false
},
"moveSpeed": 3,
"moveType": 0,
"priorityType": 1,
"stepAnime": true,
"through": false,
"trigger": 0,
"walkAnime": true
}],
"x": 0,
"y": 0
};
}
// Write $dataMap with the generated maze and events.
$dataMap = {};
$dataMap.events = [];
$dataMap.width = maze.length;
$dataMap.height = maze[0].length;
$dataMap.scrollType = 0;
$dataMap.tilesetId = my.genTilesetId;
var width = maze.length;
var height = maze[0].length;
$dataMap.data = new Array(6 * width * height);
$dataMap.data.fill(0);
// Fill the map's data structure, while keeping track of the walkable tiles.
var goals = [];
for (var i = 0; i < width; i++) {
for (var j = 0; j < height; j++) {
if (maze[i][j]) {
$dataMap.data[i + j * width] = my.genFloor;
if (i !== init.x && j !== init.y) {
goals.push({x: i, y: j});
}
}
else {
$dataMap.data[i + j * width] = my.genWall;
}
}
}
// From any of the walkable tiles, choose randomly a goal position.
var goal = goals[Math.floor(Math.random() * goals.length)];
// Set the goal event's position.
ev.x = goal.x;
ev.y = goal.y;
$dataMap.events.push(ev);
};
var _loadMapData = DataManager.loadMapData;
/**
* Overrides the default DataManager.loadMapData method.
* @param mapId if -1 generates a random maze, otherwise it keeps the default behaviour.
*/
DataManager.loadMapData = function (mapId) {
if (mapId === -1) {
my.generateMaze();
}
else {
_loadMapData.call(this, mapId);
}
};
// Rewrite Input._updateGamepadState to include axes informations and whether the last input came from a gamepad or the keyboard.
Input._updateGamepadState = function(gamepad) {
var lastState = this._gamepadStates[gamepad.index] || [];
var newState = [];
var buttons = gamepad.buttons;
var axes = gamepad.axes;
var threshold = 0.5;
newState[12] = false;
newState[13] = false;
newState[14] = false;
newState[15] = false;
for (var i = 0; i < buttons.length; i++) {
newState[i] = buttons[i].pressed;
}
if (axes[1] < -threshold) {
newState[12] = true; // up
} else if (axes[1] > threshold) {
newState[13] = true; // down
}
if (axes[0] < -threshold) {
newState[14] = true; // left
} else if (axes[0] > threshold) {
newState[15] = true; // right
}
for (var j = 0; j < newState.length; j++) {
if (newState[j] !== lastState[j]) {
var buttonName = this.gamepadMapper[j];
if (buttonName) {
this._currentState[buttonName] = newState[j];
}
}
}
this._gamepadStates[gamepad.index] = newState;
this._axes = gamepad.axes;
this._lastInputIsGamepad = newState.filter(b => {return b === true;}).length > 0;
};
// Rewrite Input._onKeyDown to include whether the last input came from a gamepad or the keyboard.
Input._onKeyDown = function(event) {
if (this._shouldPreventDefault(event.keyCode)) {
event.preventDefault();
}
if (event.keyCode === 144) { // Numlock
this.clear();
}
var buttonName = this.keyMapper[event.keyCode];
if (ResourceHandler.exists() && buttonName === 'ok') {
ResourceHandler.retry();
} else if (buttonName) {
this._currentState[buttonName] = true;
}
this._lastInputIsGamepad = false;
};
// Checks where the last input came from.
Input.isLastInputGamepad = function() {
return !!this._lastInputIsGamepad;
};
// Returns the value for a given axis. If the value is below the deadzone it returns 0.
Input.readAxis = function(axis, deadzone = 0.20) {
var ret = 0;
if (this._axes && Math.abs(this._axes[axis]) >= deadzone) {
ret = this._axes[axis];
}
return ret;
};
var _ti_onTouchMove = TouchInput._onTouchMove;
TouchInput._onTouchMove = function(event) {
var oldX = this._x;
var oldY = this._y;
_ti_onTouchMove.call(this, event);
this._dx = this._x - oldX;
this._dy = this._y - oldY;
};
TouchInput.isLastInputTouch = function() {
return this._screenPressed;
};
Object.defineProperty(TouchInput, 'dx', {
get: function() {
return this._dx;
},
configurable: true
});
Object.defineProperty(TouchInput, 'dy', {
get: function() {
return this._dy;
},
configurable: true
});
return my;
}(Maze || {}));