//=============================================================================
// LWP_CustomBattleMenu.js
//=============================================================================
/*:
* @plugindesc Full control of the action menu in battles.
* @author Logan Pickup
*
* @param Enable Debugging Info
* @desc Whether to include debugging log information and unit tests.
* Remember to turn this OFF for release! on/off/true/false
* @default off
*
* @param Max Commands
* @desc The maximum number of commands to display in the command list.
* @default 4
*
* @help
* This is a combination of YEP's Weapon Unleash (which allows replacing
* attack and guard with custom skills) and HIME's Actor Battle Command (
* which allows customising which battle commands are shown), taken to the
* next logical level: you can specify a custom list of actions as the
* battle commands, including changing the order.
*
* Battle commands can be specified on classes, actors, skills, armour,
* weapons and states. Commands from classes are applied first, then actors,
* then armour, etc. Later commands override earlier commands (especially if
* command menu limits are enforced), and if the <battlecommand: no-defaults>
* or <battlecommand: clear> tags are used then they remove commands that
* may have been defined already.
*
* If you don't specify visible and enabled states for the menu items, by
* default they will use the mp/tp calculations to see if they're usable.
*
* Battle commands appear in the menu in the order they are defined. This
* means commands defined in states, etc. will mostly appear at the bottom;
* if you want to override this order, you can specify position:x on any
* battlecommand notetag to influence its position. Higher position values
* make the command appear later in the list; by default the position starts
* at 1 and goes up with every command added. You can set fractional position
* values if you want.
*
* Changing the name of the commands isn't supported yet, but if you change
* the name of the attack skill it will show the new attack skill's name in
* the command list instead of "attack" (same with "guard").
*
* ============================================================================
* Symbols
* ============================================================================
*
* The battle menus are defined by "symbols". Each symbol represents a different
* type of command, and may ahave an additional parameter: "ext", which gives
* it more info, if needed, to carry out the command. Not all commands need the
* "ext" parameter; for example, the default attack command has the symbol
* "attack" and no ext; but the skill menus have the symbol "skill" and the
* ext must be the skill type to show a menu for.
*
* The default symbols are below:
* "attack" - makes a default attack.
* "guard" - performs a guard action.
* "skill" - shows the skill menu for a type of skill. The skill type to show
* is in the "ext" parameter.
* "item" - shows the item menu to allow an item to be selected and used.
*
* Additional symbols added by this plugin are:
* "skill_id" - performs a skill immediately, without needing to show the skill
* menu. The skill to perform in in the "ext" parameter.
* "equip" - opens the weapon or armour menu to allow swapping weapon/armour.
* The "ext" parameter will be set to "weapon" or "armor".
*
* ============================================================================
* Notetags
* ============================================================================
*
* <battlecommand: no-defaults>
* Removes the default battle commands, but not any custom battle commands
* (even if a custom command is the same as a default command).
*
* <battlecommand: clear [all|symbol] [ext:x]>
* Either removes all battle commands (both default and custom) defined up
* until this point, or removes a specific battle command by symbol and ext.
* If ext isn't specified, it will remove all instances of the <symbol>.
*
* <battlecommand: add attack [position:x]>
* Adds a default attack, optionally overriding the default position.
*
* <battlecommand: add guard [position:x]>
* Adds a default guard, optionally overriding the default position.
*
* <battlecommand: add item [position:x]>
* Adds the default item menu, optionally overriding the default position.
*
* <battlecommand: add skills [position:x]>
* Adds all available skill types for the actor, optionally overriding the
* default position. The first skill type will get the exact position value
* specified, and the following types will get position + 0.1, position +
* 0.2, etc.
*
* <battlecommand: add skill type [id] [position:x]>
* Adds a menu for skills of the specified type, optionally overriding the
* default position.
*
* <battlecommand: add skill [id] [position:x]>
* Adds a command to trigger a single skill, optionally overriding the
* default position. If you want to rename the default attack, this is
* one way to do it, by just using the attack skill. Another way is
* using Yanfly's WEapon Unleash plugin, which adds the code to get
* the default attack to use the name of the skill instead of a hardcoded
* name. Ditto for renaming guard.
*
* <battlecommand: add equip weapon [position:x]>
* Adds a command to switch weapons, optionally overriding the default
* position.
*
* <battlecommand: add equip armor [position:x]>
* Adds a command to switch armour, optionally overriding the default
* position.
*
* ============================================================================
* Advanced Notetags
* ============================================================================
*
* By putting "if" at the end of any of the <battlecommand> notetags, you can
* use some of the built-in filtering logic to only show commands when certain
* conditions are met. The available filters are below:
*
* Left side variables:
* wtype: the weapon types of all the currently-weilded weapons.
* weapon: the weapon ids of all the currently-weilded weapons.
* atype: the armour types of all the currently-equipped armour.
* armor: the armour ids of all the currently-equipped armour.
* state: the state ids of all current states.
* vX: the variable with the id <x>.
*
* Operators:
* ==: check passes if at least one value from the left side matches at
* least one value from the right side.
* =: same as ==.
* !=: check passes if all values from the left side do not match any
* values from the right side.
*
* Right side variables:
* x: Any integer value is allowed on the right side.
* vX: Any variable is allowed on the right side.
* x,vX,x...: Lists of values and/or variables are allowed on the right
* side. The list may be all values, all variables, or a
* combination of the two. There must be no spaces between the
* commas and the values/variables.
*
* The "if" condition can be used like this:
* <battlecommand add skill 4 if "wtype==14">
* - skill 4 is added to the battle menu if the actor is using a weapon with
* weapon type 14.
*
* Example with lists of values:
* <battlecommand add skill 4 if "wtype==14,15,16">
* - skill 4 is added to the battle menu if the actor is using a weapon with
* weapon type of either 14, 15 or 16.
*
* Example with variables:
* <battlecommand add skill 4 if "v2==v3">
* - skill 4 is added to the battle menu if variable 2 is equal to variable 3.
*
* Alternatively, you can specify switches:
* sX: The switch with id <x>. The result of the check is the value of the
* switch. example:
* <battlecommand add skill 4 if "s2">
* - skill 4 is added to the battle menu if switch 2 is on.
*
* not sX: The opposite of the switch with id <x>. The result of the check
* is the negated value of the switch. example:
* <battlecommand add skill 4 if "not s2">
* - skill 4 is added to the battle menu if switch 2 is off.
*
* You can combine multiple conditions using "and" and "or". For example:
* <battlecommand add skill 4 if "wtype==14 and s2">
* - skill 4 is added to the battle menu if the actor is using a weapon with
* weapon type of 14 and switch 2 is on.
*
* ============================================================================
* Custom Logic in Notetags
* ============================================================================
*
* If the built-in conditions are not enough, by putting "eval" at the end of
* any of the <battlecommand> notetags, you can add custom logic to determine
* if the notetag should be considered or not by setting the "show" variable,
* for example:
*
* <battlecommand add skill 4 eval>
* if (user.weapons()[0].wtypeId === 3) {
* show = true;
* }
* </battlecommand>
*
* "show" defaults to false. "eval" cannot be mixed with "if". The following
* variables are available in adition to the standard RPG maker globals:
* v(X) - get the value of variable number X.
* s(X) - get the value of switch number X.
* a - the current actor (always the actor whose battle menu we are setting).
* user - same as a, the current actor.
* actor - same as a.
* skill - if the current menu item refers to a skill, this is that skill.
*/
(function() {
function booleanParameter(param, defaultValue) {
if (!LWP.isDefined(param) || param.trim().length === 0) {
return defaultValue;
} else {
return param.toLowerCase() === 'true' || param.toLowerCase() === 'on';
}
}
var parameters = new LWP.Parameters('LWP_CustomBattleMenu');
var debugEnabled = parameters.getBoolean('Enable Debugging Info', false);
var globalMaxBattleCommands = parameters.getInteger('Max Commands', 4);
var logger = LWP.logger('LWP_CustomBattleMenu');
LWP.onDatabaseLoaded(function() {
processNotetags(this, $dataClasses);
processNotetags(this, $dataActors);
processNotetags(this, $dataArmors);
processNotetags(this, $dataWeapons);
processNotetags(this, $dataStates);
});
function LWP_BattleCommand(command, subCommand, ext, position, evalLogic, ifConditions) {
this.command = command;
this.subCommand = subCommand;
this.ext = ext;
this.position = position;
this.evalLogic = evalLogic;
this.ifConditions = ifConditions;
}
LWP_BattleCommand.prototype.isNoDefaults = function() {
return this.command === 'no-defaults';
}
LWP_BattleCommand.prototype.isClear = function() {
return this.command === 'clear';
}
LWP_BattleCommand.prototype.isAdd = function() {
return this.command === 'add';
}
LWP_BattleCommand.prototype.isSkillList = function() {
return this.isAdd() && this.subCommand === 'skills';
}
LWP_BattleCommand.prototype.isClearAll = function() {
return this.isClear() && this.subCommand === 'all';
}
function processNotetags(dataManager, group) {
for (var i = 1; i < group.length; i++) {
var obj = group[i];
var notedata = obj.note.toLowerCase();
var re = /<battlecommand:\s*(add|clear|no-defaults)(?:\s+(\w+|all|attack|guard|item|skills|skill type|skill|equip weapon|equip armou?r)(?!\w)(?:\s+(\d+))?(?:\s+position:\s+([-0-9.]+))?)?(?: (if\s+(?:"[^"]+"|'[^']+')|eval))?>/gi
var match;
obj.battleCommands = [];
while (match = re.exec(notedata)) {
var command = match[1] ? match[1] : match[1];
var subCommand = match[2] ? match[2] : match[2];
var idOrExt = match[3] ? Number(match[3]) : match[3];
var position = match[4] ? Number(match[4]) : match[4];
var evalLogic = null;
if (match[5] === 'eval') {
var closingTagIndex = notedata.indexOf('</battlecommand>', re.lastIndex);
if (closingTagIndex === -1) {
logger.error("Error: no closing tag found for " + match[0] + " in " + obj.name);
} else {
evalLogic = notedata.substring(re.lastIndex, closingTagIndex).trim();
re.lastIndex = closingTagIndex + '</battlecommand>'.length;
}
}
var ifConditions = null;
if (match[5] && match[5].startsWith('if')) {
var expression = match[5].substring(2, match[5].length - 1);
expression = expression.trim().substring(1);
ifConditions = parseIfConditions(expression);
}
var battleCommand = new LWP_BattleCommand(command, subCommand, idOrExt, position, evalLogic, ifConditions);
obj.battleCommands.push(battleCommand);
}
};
}
Scene_Battle.prototype.createActorCommandWindow = Scene_Battle.prototype.createActorCommandWindow.wrap(function(createActorCommandWindow) {
createActorCommandWindow();
this._actorCommandWindow.setHandler('skill_id', this.commandUseSkill.bind(this));
this._actorCommandWindow.setHandler('equip', this.commandEquip.bind(this));
});
Scene_Battle.prototype.commandUseSkill = function() {
var skillId = this._actorCommandWindow.currentExt();
var action = BattleManager.inputtingAction();
action.setSkill(skillId);
BattleManager.actor().setLastBattleSkill($dataSkills[skillId]);
this.onSelectAction();
}
Scene_Battle.prototype.commandEquip = function() {
if (this._actorCommandWindow.currentExt() === 'weapon') {
// equip weapon
throw "TODO";
} else if (this._actorCommandWindow.currentExt() === 'armor') {
// equip armour
throw "TODO";
} else {
logger.error("Unknown equip type " + this._actorCommandWindow.currentExt());
}
}
function getBattleCommandsFromObject(dataObject) {
return dataObject.battleCommands || [];
}
function getBattleCommandsFromArray(dataObjects) {
var battleCommands = [];
for (obj in dataObjects) {
battleCommands = battleCommands.concat(getBattleCommandsFromObject(obj));
}
return battleCommands;
}
function findCommands(list, symbol, ext) {
var indexes = [];
for (var i = 0; i < list.length; ++i) {
if (list[i].symbol === symbol) {
if (!LWP.isDefined(ext) || ext === list[i].ext) {
indexes.push(i);
}
}
}
return indexes;
}
function findCommand(list, symbol, ext) {
for (var i = 0; i < list.length; ++i) {
if (list[i].symbol === symbol) {
if (!LWP.isDefined(ext) || ext === list[i].ext) {
return i;
}
}
}
return -1;
}
function getBattleCommandsForActor(actor) {
var battleCommands = [];
battleCommands = battleCommands.concat(getBattleCommandsFromObject(actor.currentClass()));
battleCommands = battleCommands.concat(getBattleCommandsFromObject(actor.actor()));
battleCommands = battleCommands.concat(getBattleCommandsFromArray(actor.skills()));
battleCommands = battleCommands.concat(getBattleCommandsFromArray(actor.armors()));
battleCommands = battleCommands.concat(getBattleCommandsFromArray(actor.weapons()));
battleCommands = battleCommands.concat(getBattleCommandsFromArray(actor.states()));
return battleCommands;
}
function LWP_CommandListItem(symbol, ext, position, isDefault) {
this.symbol = symbol;
this.ext = ext;
this.position = position;
this.isDefault = isDefault;
}
function replaceOrAdd(commandList, command, forcePosition) {
if (!command) {
return;
}
var existing = findCommand(commandList, command.symbol, command.ext);
if (existing != -1) {
if (forcePosition) {
commandList[existing].position = command.position;
}
commandList[existing].isDefault = commandList[existing].isDefault && command.isDefault;
} else {
commandList.push(command);
}
}
function addSkillTypes(commandList, actor, position, isDefault) {
var currentPosition = LWP.isDefined(position) ? position : commandList.length;
var skillTypes = actor.addedSkillTypes();
skillTypes.sort(function(a, b) {
return a - b;
});
skillTypes.forEach(function(sTypeId) {
var command = new LWP_CommandListItem('skill', sTypeId, currentPosition, isDefault);
replaceOrAdd(commandList, command, LWP.isDefined(position));
currentPosition += 0.1;
}, this);
return commandList;
}
function translateBattleCommand(battleCommand, actor, defaultPosition, isDefault) {
var ext = battleCommand.ext;
var position = LWP.isDefined(battleCommand.position) ? battleCommand.position : defaultPosition;
switch (battleCommand.subCommand) {
case 'attack':
return new LWP_CommandListItem('attack', null, position, isDefault);
case 'guard':
return new LWP_CommandListItem('guard', null, position, isDefault);
case 'item':
return new LWP_CommandListItem('item', null, position, isDefault);
case 'skill type':
if (actor.addedSkillTypes().indexOf(ext) !== -1) {
return new LWP_CommandListItem('skill', ext, position, isDefault);
} else {
logger.error("Error: attempt to add skill type " + ext + " to actor " + actor.name() + " who doesn't have that skill type");
logger.debug(actor);
return null;
}
case 'skill':
if (actor.skills().map(function(x) {return x.id}).indexOf(ext) !== -1) {
return new LWP_CommandListItem('skill_id', ext, position, isDefault);
} else {
// not necessarily an error, as this can be a way to insert specific skills high
// in the list if an actor happens to know them
logger.warning("Warning: attempt to add skill " + ext + " to actor " + actor.name() + " who doesn't know that skill");
logger.debug(actor.skills());
return null;
}
case 'equip weapon':
return new LWP_CommandListItem('equip', 'weapon', position, isDefault);
case 'equip armor':
case 'equip armour':
return new LWP_CommandListItem('equip', 'armor', position, isDefault);
default:
logger.error("Error: unhandled command " + battleCommand.command + ":" + battleCommand.subCommand);
return null;
}
}
function applyBattleCommand(commandList, battleCommand, actor) {
var position = LWP.isDefined(battleCommand.position) ? battleCommand.position : commandList.length;
if (battleCommand.isNoDefaults()) {
return commandList.filter(function(command) {!command.isDefault;});
} else if (battleCommand.isClearAll()) {
return [];
} else if (battleCommand.isClear()) {
var found = findCommands(commandList, battleCommand.subCommand, battleCommand.ext);
// process the indexes in reverse, otherwise the later indexes will change as the list
// is altered
found.reverse().forEach(function(index) {
commandList.splice(index, 1);
});
return commandList;
} else if (battleCommand.isAdd()) {
if (battleCommand.isSkillList()) {
return addSkillTypes(commandList, actor, battleCommand.position, false);
} else {
var newCommand = translateBattleCommand(battleCommand, actor, position, false);
replaceOrAdd(commandList, newCommand, LWP.isDefined(battleCommand.position));
return commandList;
}
} else {
logger.error("Error: unhandled command " + battleCommand.command + ":" + battleCommand.subCommand);
return commandList;
}
}
function addDefaultCommands(commandList, actor) {
commandList.push(new LWP_CommandListItem('attack', null, commandList.length, true));
commandList = addSkillTypes(commandList, actor, null, true);
commandList.push(new LWP_CommandListItem('guard', null, commandList.length, true));
commandList.push(new LWP_CommandListItem('item', null, commandList.length, true));
return commandList;
}
function filteredBattleCommands(battleCommands, actor) {
return battleCommands.filter(function(command) {
if (command.evalLogic) {
// globals meant to be commonly available to evals
var v = function(x) {return $gameVariables.value(x);};
var s = function(x) {return $gameSwitches.value(x);};
var a = actor;
var user = actor;
var show = false;
var skill = actor.skills().filter(function(x) {return x.id === command.ext});
var skill = skill.length > 0 ? skill[0] : null;
eval(command.evalLogic);
return show;
} else if (command.ifConditions) {
return command.ifConditions(actor);
} else {
return true;
}
});
}
Window_ActorCommand.prototype.maxBattleCommands = function() {
return globalMaxBattleCommands;
}
Window_ActorCommand.prototype.makeCommandList = Window_ActorCommand.prototype.makeCommandList.wrap(function(makeCommandList) {
if (this._actor) {
var actor = this._actor;
// get the raw command definitions
var battleCommands = getBattleCommandsForActor(actor);
battleCommands = filteredBattleCommands(battleCommands, actor);
if (battleCommands.length == 0) {
// early out if there are no battle commands defined
makeCommandList();
return;
}
logger.debug("Battle commands:");
logger.debug(battleCommands);
var commandList = [];
commandList = addDefaultCommands(commandList, actor);
logger.debug("Command list - default commands:");
logger.debug(commandList);
// put them in an intermediate format, and apply clears/overrides
battleCommands.forEach(function(command) {
commandList = applyBattleCommand(commandList, command, actor);
});
logger.debug("Command list - after adding commands:");
logger.debug(commandList);
// sort according to position
commandList.sort(function(a, b) {
return a.position - b.position;
});
logger.debug("Command list - after sorting:");
logger.debug(commandList);
// limit command list length
commandList = commandList.slice(0, this.maxBattleCommands());
logger.debug("Command list - after truncating to maximum length:");
logger.debug(commandList);
// add the commands to the menu
var self = this;
commandList.forEach(function(command) {
if (command.symbol === 'attack') {
self.addAttackCommand();
} else if (command.symbol === 'guard') {
self.addGuardCommand();
} else if (command.symbol === 'item') {
self.addItemCommand();
} else if (command.symbol === 'skill') {
var name = $dataSystem.skillTypes[command.ext];
self.addCommand(name, command.symbol, true, command.ext);
} else if (command.symbol === 'skill_id') {
var skill = $dataSkills[command.ext];
var name = skill.commandAttackText
var enabled = actor.meetsSkillConditions(skill);
self.addCommand(name, command.symbol, enabled, command.ext);
} else if (command.symbol === 'equip') {
var enabled = false;
self.addCommand(command.ext, command.symbol, enabled, command.ext);
}
});
} else {
makeCommandList();
}
});
//////////////////////////////////////////////////////////////////
// Expression Parser
//
// Note that this hasn't been well tested; only a few combinations
// are known for certain to work. It's proably OK though! :)
//////////////////////////////////////////////////////////////////
// parse tree:
// or-expression-part: and-expresion | simple-expression
// or-expression: or-expression-part "or" or-expression-part [ "or" or-expression-part ...]
// and-expression-part: simple-expression
// and-expression: and-expression-part "and" and-expression-part [ "and" and-expression-part ...]
// simple-expression: variable-expression | switch-expression
// switch-expression: [not] s + integer
// variable-expression: variable operator value-list
// variable: actor-prop | variable-id
// operator: "==" | "=" | "!="
// value-list: value[,value...] (note: no spaces allowed in this list because reasons (i.e. I'm lazy))
// value: integer | variable-id
// variable-id: "v" + integer
function parseIfConditions(expression) {
return parseOr(expression, expression);
}
function parseOr(subexpression, expression) {
var orClauses = subexpression.split(/\s+or\s+/);
return combineOrClauses(orClauses.map(function(orClause) {
return parseAnd(orClause, expression);
}));
}
function parseAnd(subexpression, expression) {
var andClauses = subexpression.split(/\s+and\s+/);
return combineAndClauses(andClauses.map(function(andClause) {
return parseSimpleExpression(andClause, expression);
}));
}
function combineOrClauses(booleanFunctions) {
return function(actor) {
var result = false;
logger.debug("OR (" + booleanFunctions.length + ")");
booleanFunctions.forEach(function(fn) {
result = result || fn(actor);
logger.debug(result);
});
return result;
}
}
function combineAndClauses(booleanFunctions) {
return function(actor) {
var result = true;
logger.debug("AND (" + booleanFunctions.length + ")");
booleanFunctions.forEach(function(fn) {
result = result && fn(actor);
logger.debug(result);
});
return result;
}
}
var variableAccessors = {
'wtype': function(actor) {
return actor.weapons().map(function(x) {return x.wtypeId;});
},
'weapon': function(actor) {
return actor.weapons().map(function(x) {return x.baseItemId;});
},
'armor': function(actor) {
return actor.armors().map(function(x) {return x.baseItemId;});
},
'atype': function(actor) {
return actor.armors().map(function(x) {return x.atypeId;});
},
'state': function(actor) {
return actor.states().map(function(x) {return x.id;});
},
};
function variableAccessor(variableExpression) {
var variableId = +variableExpression.substring(1);
return function() {return $gameVariables.value(variableId);};
}
function parseSimpleExpression(expression, context) {
var re = /\s*([a-z0-9]+)\s*(==?|!=)\s*([v0-9,]+)\s*|\s*(?:(not) )?s([0-9]+)\s*/i;
var parts = re.exec(expression);
if (parts && parts[1]) {
var variable = parts[1];
var operator = parts[2];
var valueGenerators = parts[3].split(",").map(function(x) {
if (x.startsWith('v')) {
return variableAccessor(x);
} else {
return function() {return +x;};
}
});
var accessor = variableAccessors[variable];
if (!accessor && /v[0-9]+/.test(variable)) {
accessor = variableAccessor(variable);
}
if (!accessor) {
logger.error("Error: no accessor found for variable " + variable + " in expression " + expression + " in: " + context);
return function() {return false;};
}
return function(actor) {
logger.debug("actor:", actor);
var lValues = accessor(actor);
if (!LWP.isDefined(lValues.length)) {
lValues = [lValues];
}
var values = valueGenerators.map(function(x) {return x();});
logger.debug(expression);
logger.debug(lValues, values);
if (operator === '==' || operator === '=') {
return lValues
.map(function(l) { return values.indexOf(l) !== -1; })
.reduce(function(previous, current) {return previous || current}, false);
} else if (operator === '!=') {
return lValues
.map(function(l) { return values.indexOf(l) === -1; })
.reduce(function(previous, current) {return previous && current}, true);
} else {
logger.error("Error: unhandled operator " + operator + " in " + expression + " in: " + context);
return false;
}
};
} else if (parts && parts[5]) {
var not = !!parts[4];
var switchId = +parts[5];
if (not) {
return function() {
logger.debug("not switch", $gameSwitches.value(switchId));
return !$gameSwitches.value(switchId);
};
} else {
return function() {
logger.debug("switch", $gameSwitches.value(switchId));
return $gameSwitches.value(switchId);
};
}
} else {
logger.error("Error: malformed expression " + expression + " in: " + context);
return function() {return false;};
}
}
////////////////// TESTS ///////////////////
DataManager.createGameObjects = DataManager.createGameObjects.wrap(function(createGameObjects) {
createGameObjects();
if (debugEnabled) {
logger.setLogLevel(LWP.LOG_VERBOSE);
expressionParserTests();
}
});
function expressionParserTests() {
logger.info("Tests starting");
testActorPropWeaponType();
testActorPropWeaponId();
testActorPropArmorType();
testActorPropArmorId();
testActorPropState();
testActorPropEqualsExpressionTrue();
testActorPropEqualsExpressionFalse();
testActorPropNotEqualsExpressionTrue();
testActorPropNotEqualsExpressionFalse();
testActorPropEqualsVariableTrue();
testActorPropEqualsVariableFalse();
testActorPropNotEqualsVariableTrue();
testActorPropNotEqualsVariableFalse();
testActorPropEqualsListTrue();
testActorPropEqualsListFalse();
testActorPropNotEqualsListTrue();
testActorPropNotEqualsListFalse();
testActorMultiPropSomeInListEqualsTrue();
testActorMultiPropAllInListEqualsTrue();
testActorMultiPropAllNotInListEqualsFalse();
testActorMultiPropAllNotInListNotEqualsTrue();
testActorMultiPropSomeInListNotEqualsFalse();
testActorMultiPropAllInListNotEqualsFalse();
testActorMultiPropSomeInListWithVariablesEqualsTrue();
testVariableEqualsValue();
testVariableEqualsVariable();
testVariableEqualsList();
testSwitchOn();
testSwitchOff();
testSwitchNotOn();
testSwitchNotOff();
testEqualsTrueAndEqualsTrue();
testEqualsTrueAndEqualsFalse();
testEqualsFalseAndEqualsTrue();
testEqualsFalseAndEqualsFalse();
testEqualsTrueAndSwitchTrue();
testEqualsTrueAndNotSwitchTrue();
testEqualsTrueOrEqualsTrue();
testEqualsTrueOrEqualsFalse();
testEqualsFalseOrEqualsTrue();
testEqualsFalseOrEqualsFalse();
testEqualsFalseOrSwitchTrue();
testEqualsTrueOrNotSwitchFalse();
testAndPrecedenceHigherThanOr();
testLongAndOrChain();
logger.info("Tests finished");
}
function testActorPropWeaponType() {
givenTheExpression("wtype==1")
.andAnActorWith().weapon({id: 3, type: 1})
.whenTheConditionIsChecked()
.theConditionShouldPass();
}
function testActorPropWeaponId() {
givenTheExpression("weapon==3")
.andAnActorWith().weapon({id: 3, type: 1})
.whenTheConditionIsChecked()
.theConditionShouldPass();
}
function testActorPropArmorType() {
givenTheExpression("atype==1")
.andAnActorWith().armor({id: 3, type: 1})
.whenTheConditionIsChecked()
.theConditionShouldPass();
}
function testActorPropArmorId() {
givenTheExpression("armor==3")
.andAnActorWith().armor({id: 3, type: 1})
.whenTheConditionIsChecked()
.theConditionShouldPass();
}
function testActorPropState() {
givenTheExpression("state==3")
.andAnActorWith().state(3)
.whenTheConditionIsChecked()
.theConditionShouldPass();
}
function testActorPropEqualsExpressionTrue() {
givenTheExpression("wtype==1")
.andAnActorWith().weapon({id: 3, type: 1})
.whenTheConditionIsChecked()
.theConditionShouldPass();
}
function testActorPropEqualsExpressionFalse() {
givenTheExpression("wtype==1")
.andAnActorWith().weapon({id: 3, type: 2})
.whenTheConditionIsChecked()
.theConditionShouldFail();
}
function testActorPropNotEqualsExpressionTrue() {
givenTheExpression("wtype!=1")
.andAnActorWith().weapon({id: 3, type: 2})
.whenTheConditionIsChecked()
.theConditionShouldPass();
}
function testActorPropNotEqualsExpressionFalse() {
givenTheExpression("wtype!=1")
.andAnActorWith().weapon({id: 3, type: 1})
.whenTheConditionIsChecked()
.theConditionShouldFail();
}
function testActorPropEqualsVariableTrue() {
givenTheExpression("wtype==v3")
.andAnActorWith().weapon({id: 3, type: 2})
.andAVariable({id: 3, value: 2})
.whenTheConditionIsChecked()
.theConditionShouldPass();
}
function testActorPropEqualsVariableFalse() {
givenTheExpression("wtype==v3")
.andAnActorWith().weapon({id: 3, type: 2})
.andAVariable({id: 3, value: 3})
.whenTheConditionIsChecked()
.theConditionShouldFail();
}
function testActorPropNotEqualsVariableTrue() {
givenTheExpression("wtype!=v3")
.andAnActorWith().weapon({id: 3, type: 2})
.andAVariable({id: 3, value: 3})
.whenTheConditionIsChecked()
.theConditionShouldPass();
}
function testActorPropNotEqualsVariableFalse() {
givenTheExpression("wtype!=v3")
.andAnActorWith().weapon({id: 3, type: 2})
.andAVariable({id: 3, value: 2})
.whenTheConditionIsChecked()
.theConditionShouldFail();
}
function testActorPropEqualsListTrue() {
givenTheExpression("wtype==1,2,3")
.andAnActorWith().weapon({id: 3, type: 2})
.whenTheConditionIsChecked()
.theConditionShouldPass();
}
function testActorPropEqualsListFalse() {
givenTheExpression("wtype==1,2,3")
.andAnActorWith().weapon({id: 3, type: 4})
.whenTheConditionIsChecked()
.theConditionShouldFail();
}
function testActorPropNotEqualsListTrue() {
givenTheExpression("wtype!=1,2,3")
.andAnActorWith().weapon({id: 3, type: 4})
.whenTheConditionIsChecked()
.theConditionShouldPass();
}
function testActorPropNotEqualsListFalse() {
givenTheExpression("wtype!=1,2,3")
.andAnActorWith().weapon({id: 3, type: 2})
.whenTheConditionIsChecked()
.theConditionShouldFail();
}
function testActorMultiPropSomeInListEqualsTrue() {
givenTheExpression("wtype==1,2,3")
.andAnActorWith()
.weapon({id: 3, type: 4})
.weapon({id: 3, type: 3})
.whenTheConditionIsChecked()
.theConditionShouldPass();
}
function testActorMultiPropAllInListEqualsTrue() {
givenTheExpression("wtype==1,2,3")
.andAnActorWith()
.weapon({id: 3, type: 2})
.weapon({id: 3, type: 3})
.whenTheConditionIsChecked()
.theConditionShouldPass();
}
function testActorMultiPropAllNotInListEqualsFalse() {
givenTheExpression("wtype==1,2,3")
.andAnActorWith()
.weapon({id: 3, type: 4})
.weapon({id: 3, type: 5})
.whenTheConditionIsChecked()
.theConditionShouldFail();
}
function testActorMultiPropAllNotInListNotEqualsTrue() {
givenTheExpression("wtype!=1,2,3")
.andAnActorWith()
.weapon({id: 3, type: 4})
.weapon({id: 3, type: 5})
.whenTheConditionIsChecked()
.theConditionShouldPass();
}
function testActorMultiPropSomeInListNotEqualsFalse() {
givenTheExpression("wtype!=1,2,3")
.andAnActorWith()
.weapon({id: 3, type: 2})
.weapon({id: 3, type: 5})
.whenTheConditionIsChecked()
.theConditionShouldFail();
}
function testActorMultiPropAllInListNotEqualsFalse() {
givenTheExpression("wtype!=1,2,3")
.andAnActorWith()
.weapon({id: 3, type: 2})
.weapon({id: 3, type: 3})
.whenTheConditionIsChecked()
.theConditionShouldFail();
}
function testActorMultiPropSomeInListWithVariablesEqualsTrue() {
givenTheExpression("wtype==v1,v2,v3")
.andAnActorWith()
.weapon({id: 3, type: 4})
.weapon({id: 3, type: 6})
.andAVariable({id: 1, value: 5})
.andAVariable({id: 2, value: 5})
.andAVariable({id: 3, value: 4})
.whenTheConditionIsChecked()
.theConditionShouldPass();
}
// At this point, we've fairly exhaustively testing the equals and not equals
// operators. We'll skip every single variation, and focus on different
// combinations of left- and right-hand values.
function testVariableEqualsValue() {
givenTheExpression("v1==3")
.andAVariable({id: 1, value: 3})
.whenTheConditionIsChecked()
.theConditionShouldPass();
}
function testVariableEqualsVariable() {
givenTheExpression("v1==v2")
.andAVariable({id: 1, value: 3})
.andAVariable({id: 2, value: 3})
.whenTheConditionIsChecked()
.theConditionShouldPass();
}
function testVariableEqualsList() {
givenTheExpression("v1==2,3,4")
.andAVariable({id: 1, value: 3})
.whenTheConditionIsChecked()
.theConditionShouldPass();
}
function testSwitchOn() {
givenTheExpression("s3")
.andASwitch({id: 3, value: true})
.whenTheConditionIsChecked()
.theConditionShouldPass();
}
function testSwitchOff() {
givenTheExpression("s3")
.andASwitch({id: 3, value: false})
.whenTheConditionIsChecked()
.theConditionShouldFail();
}
function testSwitchNotOn() {
givenTheExpression("not s3")
.andASwitch({id: 3, value: true})
.whenTheConditionIsChecked()
.theConditionShouldFail();
}
function testSwitchNotOff() {
givenTheExpression("not s3")
.andASwitch({id: 3, value: false})
.whenTheConditionIsChecked()
.theConditionShouldPass();
}
// and/or
function testEqualsTrueAndEqualsTrue() {
givenTheExpression("wtype==4 and weapon==2")
.andAnActorWith()
.weapon({id: 2, type: 4})
.whenTheConditionIsChecked()
.theConditionShouldPass();
}
function testEqualsTrueAndEqualsFalse() {
givenTheExpression("wtype==4 and weapon==2")
.andAnActorWith()
.weapon({id: 1, type: 4})
.whenTheConditionIsChecked()
.theConditionShouldFail();
}
function testEqualsFalseAndEqualsTrue() {
givenTheExpression("wtype==4 and weapon==2")
.andAnActorWith()
.weapon({id: 2, type: 3})
.whenTheConditionIsChecked()
.theConditionShouldFail();
}
function testEqualsFalseAndEqualsFalse() {
givenTheExpression("wtype==4 and weapon==2")
.andAnActorWith()
.weapon({id: 1, type: 3})
.whenTheConditionIsChecked()
.theConditionShouldFail();
}
function testEqualsTrueAndSwitchTrue() {
givenTheExpression("wtype==4 and s3")
.andAnActorWith()
.weapon({id: 2, type: 4})
.andASwitch({id: 3, value: true})
.whenTheConditionIsChecked()
.theConditionShouldPass();
}
function testEqualsTrueAndNotSwitchTrue() {
givenTheExpression("wtype==4 and not s3")
.andAnActorWith()
.weapon({id: 2, type: 4})
.andASwitch({id: 3, value: false})
.whenTheConditionIsChecked()
.theConditionShouldPass();
}
function testEqualsTrueOrEqualsTrue() {
givenTheExpression("wtype==4 or weapon==2")
.andAnActorWith()
.weapon({id: 2, type: 4})
.whenTheConditionIsChecked()
.theConditionShouldPass();
}
function testEqualsTrueOrEqualsFalse() {
givenTheExpression("wtype==4 or weapon==2")
.andAnActorWith()
.weapon({id: 1, type: 4})
.whenTheConditionIsChecked()
.theConditionShouldPass();
}
function testEqualsFalseOrEqualsTrue() {
givenTheExpression("wtype==4 or weapon==2")
.andAnActorWith()
.weapon({id: 2, type: 3})
.whenTheConditionIsChecked()
.theConditionShouldPass();
}
function testEqualsFalseOrEqualsFalse() {
givenTheExpression("wtype==4 or weapon==2")
.andAnActorWith()
.weapon({id: 1, type: 3})
.whenTheConditionIsChecked()
.theConditionShouldFail();
}
function testEqualsFalseOrSwitchTrue() {
givenTheExpression("wtype==4 or s3")
.andAnActorWith()
.weapon({id: 2, type: 3})
.andASwitch({id: 3, value: true})
.whenTheConditionIsChecked()
.theConditionShouldPass();
}
function testEqualsTrueOrNotSwitchFalse() {
givenTheExpression("wtype==4 or not s3")
.andAnActorWith()
.weapon({id: 2, type: 4})
.andASwitch({id: 3, value: true})
.whenTheConditionIsChecked()
.theConditionShouldPass();
}
function testAndPrecedenceHigherThanOr() {
givenTheExpression("s3 or v1==2 and v2==3")
.andAVariable({id: 1, value: 2})
.andAVariable({id: 2, value: 2})
.andASwitch({id: 3, value: true})
.whenTheConditionIsChecked()
.theConditionShouldPass();
}
function testLongAndOrChain() {
givenTheExpression("not s1 or not s2 or s3 and s4 and s5 or not s6 and s7 or not s8")
.andASwitch({id: 1, value: true})
.andASwitch({id: 2, value: true})
.andASwitch({id: 3, value: true})
.andASwitch({id: 4, value: true})
.andASwitch({id: 5, value: true})
.andASwitch({id: 6, value: true})
.andASwitch({id: 7, value: true})
.andASwitch({id: 8, value: true})
.whenTheConditionIsChecked()
.theConditionShouldPass();
}
// Test framework
function givenTheExpression(expression) {
var TARGET_ACTOR = 'the actor';
return {
tag: "Given an expression \"" + expression + "\"\n",
currentlyAnnotating: null,
tagNeedsNewline: false,
variables: {},
switches: {},
actorProps: {
weapons: [],
armors: [],
states: []
},
condition: parseIfConditions(expression),
andAnActorWith: function() {
this._fixAnnotations(null);
this.tag += "And an actor with ";
this.currentlyAnnotating = TARGET_ACTOR;
this.tagNeedsNewline = true;
return this;
},
weapon: function(data) {
this._fixAnnotations(TARGET_ACTOR);
this.tag += "a weapon (id=" + data.id + ", type=" + data.type + ") ";
this.actorProps.weapons.push(makeWeapon(data.id, data.type));
return this;
},
armor: function(data) {
this._fixAnnotations(TARGET_ACTOR);
this.tag += "armour (id=" + data.id + ", type=" + data.type + ") ";
this.actorProps.armors.push(makeArmor(data.id, data.type));
return this;
},
state: function(id) {
this._fixAnnotations(TARGET_ACTOR);
this.tag += "state " + id + " ";
this.actorProps.states.push({id: id});
return this;
},
andAVariable: function(data) {
this._fixAnnotations(null);
this.tag += "And a variable " + data.id + " with the value " + data.value + "\n";
this.variables[data.id] = data.value;
return this;
},
andASwitch: function(data) {
this._fixAnnotations(null);
this.tag += "And a switch " + data.id + " that is " + (data.value ? "on" : "off") + "\n";
this.switches[data.id] = data.value;
return this;
},
whenTheConditionIsChecked: function() {
this._fixAnnotations(null);
this.tag += "When the condition is checked\n";
this.actor = dummyActor(this.actorProps.weapons, this.actorProps.armors, this.actorProps.states);
this._setVariables();
this.checkResult = this.condition(this.actor);
this._restoreVariables();
return this;
},
theConditionShouldPass: function() {
this._fixAnnotations(null);
this.tag += "Then the condition should pass\n";
this._assert(this.checkResult, this.tag);
logger.info(this.tag + " - PASSED");
return this;
},
theConditionShouldFail: function() {
this._fixAnnotations(null);
this.tag += "Then the condition should fail\n";
this._assert(!this.checkResult, this.tag);
logger.info(this.tag + " - PASSED");
return this;
},
_setVariables: function() {
this.oldVariableValues = {}
for (var variableId in this.variables) {
if (this.variables.hasOwnProperty(variableId)) {
this.oldVariableValues = $gameVariables.value(variableId);
$gameVariables.setValue(variableId, this.variables[variableId]);
}
}
this.oldSwitchValues = {}
for (var switchID in this.switches) {
if (this.switches.hasOwnProperty(switchID)) {
this.oldSwitchValues = $gameSwitches.value(switchID);
$gameSwitches.setValue(switchID, this.switches[switchID]);
}
}
},
_restoreVariables: function() {
for (var variableId in this.variables) {
if (this.variables.hasOwnProperty(variableId)) {
$gameVariables.setValue(variableId, this.oldVariableValues[variableId]);
}
}
for (var switchID in this.switches) {
if (this.switches.hasOwnProperty(switchID)) {
$gameSwitches.setValue(switchID, this.oldSwitchValues[switchID]);
}
}
},
_fixAnnotations: function(forAnnotationTarget) {
if (this.currentlyAnnotating != forAnnotationTarget) {
if (this.tagNeedsNewline) {
this.tag += '\n';
this.tagNeedsNewline = false;
}
this.currentlyAnnotating = forAnnotationTarget;
if (forAnnotationTarget) {
this.tag += "And " + forAnnotationTarget + " has ";
this.tagNeedsNewline = true;
}
}
},
_assert: function(test, message) {
if (!test) {
throw new Error(message);
}
}
};
}
function dummyActor(weapons, armors, states) {
if (!weapons) weapons = [];
if (!LWP.isDefined(weapons.length)) weapons = [weapons];
if (!armors) armors = [];
if (!LWP.isDefined(armors.length)) armors = [armors];
if (!states) states = [];
if (!LWP.isDefined(states.length)) states = [states];
return {
weapons: function() {return weapons;},
armors: function() {return armors;},
states: function() {return states;}
}
}
function makeWeapon(id, type) {
return {
baseItemId: id,
wtypeId: type
}
}
function makeArmor(id, type) {
return {
baseItemId: id,
atypeId: type
}
}
})();