// We hardcode a type chart because it's very unlikely to change, and I don't want
// to have it take up valuable IMPORTRANGE() bandwidth.
const TYPE_CHART = {
'Bug': { 'chart':[0,0,0,0,0,-1,1,1,0,-1,-1,0,0,0,0,1,0,0], 'index':1 },
'Dark': { 'chart':[1,-1,0,0,1,1,0,0,-1,0,0,0,0,0,-99,0,0,0], 'index':2 },
'Dragon': { 'chart':[0,0,1,-1,1,0,-1,0,0,-1,0,1,0,0,0,0,0,-1], 'index':3 },
'Electric': { 'chart':[0,0,0,-1,0,0,0,-1,0,0,1,0,0,0,0,0,-1,0], 'index':4 },
'Fairy': { 'chart':[-1,-1,-99,0,0,-1,0,0,0,0,0,0,0,1,0,0,1,0], 'index':5 },
'Fighting': { 'chart':[-1,-1,0,0,1,0,0,1,0,0,0,0,0,0,1,-1,0,0], 'index':6 },
'Fire': { 'chart':[-1,0,0,0,-1,0,-1,0,0,-1,1,-1,0,0,0,1,-1,1], 'index':7 },
'Flying': { 'chart':[-1,0,0,1,0,-1,0,0,0,-1,-99,1,0,0,0,1,0,0], 'index':8 },
'Ghost': { 'chart':[-1,1,0,0,0,-99,0,0,1,0,0,0,-99,-1,0,0,0,0], 'index':9 },
'Grass': { 'chart':[1,0,0,-1,0,0,1,1,0,-1,-1,1,0,1,0,0,0,-1], 'index':10 },
'Ground': { 'chart':[0,0,0,-99,0,0,0,0,0,1,0,1,0,-1,0,-1,0,1], 'index':11 },
'Ice': { 'chart':[0,0,0,0,0,1,1,0,0,0,0,-1,0,0,0,1,1,0], 'index':12 },
'Normal': { 'chart':[0,0,0,0,0,1,0,0,-99,0,0,0,0,0,0,0,0,0], 'index':13 },
'Poison': { 'chart':[-1,0,0,0,-1,-1,0,0,0,-1,1,0,0,-1,1,0,0,0], 'index':14 },
'Psychic': { 'chart':[1,1,0,0,0,-1,0,0,1,0,0,0,0,0,-1,0,0,0], 'index':15 },
'Rock': { 'chart':[0,0,0,0,0,1,-1,-1,0,1,1,0,-1,-1,0,0,1,1], 'index':16 },
'Steel': { 'chart':[-1,0,-1,0,-1,1,1,-1,0,-1,1,-1,-1,-99,-1,-1,-1,0],'index':17 },
'Water': { 'chart':[0,0,0,1,0,0,-1,0,0,1,0,-1,0,0,0,0,-1,-1], 'index':18 }
}
// Hardcoded chart of Effectiveness Multipliers, since they are unlikely to change.
// Negative indexes count from the end of the array.
const EFFECTIVENESS_CHART = [1, 1.5, 2, 2.5, 3, 0.01, 0.25, 0.5, 0.75];
/**
* Clears the stack of Pokemon awaiting analysis in the Matchup page.
*/
function clearMatchupPokemonConfig() {
const ui = SpreadsheetApp.getUi();
const response = ui.alert(
"Clear Matchup Pokemon Configuration",
"Delete all prepared Pokemon awaiting analysis in the Matchup page, as well as all "+
"custom Movepools stored in the Matchup page; leaving a blank slate?",
Browser.Buttons.YES_NO)
if (response === ui.Button.YES) {
ui.alert('You clicked "Yes".')
}
}
/**
* Receives two Species names, a Level, a movepool, and a set of configurations.
* Outputs a sorted array of moves, each with a type, a damage estimate, and an
* additional parameter as specified by the configuration.
*
* @param {Array} attackerInput The row of imported Pokemon Data for the ally Pokemon.
* @param {Array} defenderInput The row of imported Pokemon Data for the enemy Pokemon.
* @param {Array} movepool The movepool of the ally Pokemon.
* @param {Array<Array>} optionsRange An array of the matchup tool's configuration options for the ally Pokemon's side.
* @param {Array<Array>} moveBattleDataRange The range of imported Move data.
* @param {Array<Array>} validMoveWarnlists An array of Warnlists currently set to apply to the Matchup Tool.
*
* @customfunction
*/
function MAKEMATCHUPMOVEPOOL(attackerInput, defenderInput, movepool, optionsRange, moveBattleDataRange, validMoveWarnlists) {
// First, parse the passed optionsRange into an Object.
const options = {
"Sort": optionsRange[0][0],
"Flex Column": optionsRange[1][0],
"Filter": optionsRange[2][0],
"Power": optionsRange[3][0]
}
// Dummy check.
if ( null == attackerInput || null == defenderInput) {
return "Invalid Species.";
}
// Arrange species data for the two pokemon into objects
const attackerSpecies = {
"Name": attackerInput[0][0], "Types": attackerInput[0][1], "Abilities": attackerInput[0][2], "Hidden": attackerInput[0][3],
"HP": attackerInput[0][4], "Atk": attackerInput[0][5], "Def": attackerInput[0][6],
"SpA": attackerInput[0][7], "SpD": attackerInput[0][8], "Spe": attackerInput[0][9],
"Size": attackerInput[0][10], "Weight": attackerInput[0][11], "Sig": attackerInput[0][12]
}
const defenderSpecies = {
"Name": defenderInput[0][0], "Types": defenderInput[0][1], "Abilities": defenderInput[0][2], "Hidden": defenderInput[0][3],
"HP": defenderInput[0][4], "Atk": defenderInput[0][5], "Def": defenderInput[0][6],
"SpA": defenderInput[0][7], "SpD": defenderInput[0][8], "Spe": defenderInput[0][9],
"Size": defenderInput[0][10], "Weight": defenderInput[0][11], "Sig": defenderInput[0][12]
}
// Find the stat differences between the attacker and defender, and make a defending effectiveness table for the enemy.
const physDiff = attackerSpecies["Atk"] - defenderSpecies["Def"];
const specDiff = attackerSpecies["SpA"] - defenderSpecies["SpD"];
const accDiff = 0; // Used for the "Accurate" filter
const defenderTypeChart = makeEffectivenessObject(defenderSpecies["Types"]);
// Fetch and hold the table of battle data for moves.
const movesData = moveBattleDataRange;
const moveWarnlist = validMoveWarnlists[0];
// Find the column index that should be used for the Flex Column.
let flexColumnDataIndex = 0;
let flexColumnSortFunction = function(a, b) { return b[2] - a[2] };
switch ( options["Flex Column" ]) {
case "Category":
flexColumnDataIndex = 2;
flexColumnSortFunction = function(a, b) { return a[2].localeCompare(b[2]) };
break;
case "Target Scope":
flexColumnDataIndex = 3;
flexColumnSortFunction = function(a, b) { return a[2].localeCompare(b[2]) };
break;
case "Accuracy":
flexColumnDataIndex = 5;
flexColumnSortFunction = function(a, b) { let aNum = parseInt(a[2]); aNum = isNaN(aNum) ? 0 : aNum;
let bNum = parseInt(b[2]); bNum = isNaN(bNum) ? 0 : bNum;
return bNum - aNum };
break;
case "Energy Cost":
flexColumnDataIndex = 6;
break;
case "Effect Chance":
flexColumnDataIndex = 7;
flexColumnSortFunction = function(a, b) { let aNum = parseInt(a[2]); aNum = isNaN(aNum) ? 0 : aNum;
let bNum = parseInt(b[2]); bNum = isNaN(bNum) ? 0 : bNum;
return bNum - aNum };
break;
case "Priority":
flexColumnDataIndex = 8;
break;
case "C.Limit":
flexColumnDataIndex = 9;
flexColumnSortFunction = function(a, b) { let aNum = a[2] == "Banned" ? 2 : a[2] == "Status" ? 1 : a[2] == "One" ? 0 : -1;
let bNum = b[2] == "Banned" ? 2 : b[2] == "Status" ? 1 : b[2] == "One" ? 0 : -1;
return bNum - aNum };
break;
case "Contact":
flexColumnDataIndex = 10;
flexColumnSortFunction = function(a, b) { return b[2].localeCompare(a[2]) }; // Reverse sort for flag columns so that "Yes" is before "No".
break;
case "Snatchable":
flexColumnDataIndex = 11;
flexColumnSortFunction = function(a, b) { return b[2].localeCompare(a[2]) }; // Reverse sort for flag columns so that "Yes" is before "No".
break;
case "Reflectable":
flexColumnDataIndex = 12;
flexColumnSortFunction = function(a, b) { return b[2].localeCompare(a[2]) }; // Reverse sort for flag columns so that "Yes" is before "No".
break;
default:
break;
}
// Filter the movelist to remove any entries that don't meet the passed filter criteria.
let filterFunction = function(moveData) { return true; }
switch (options["Filter"]) {
case "Super-Effective":
filterFunction = function(moveData) { return defenderTypeChart[moveData[1]] > 1 && moveData[2] != "Other"; }
break;
case "Attacks":
filterFunction = function(moveData) { return moveData[2] != "Other"; }
break;
case "Non-Attacks":
filterFunction = function(moveData) { return moveData[2] == "Other"; }
break;
case "Combo-Legal":
filterFunction = function(moveData) { return moveData[9] != "Banned"; }
break;
case "Accurate":
filterFunction = function(moveData) { return (moveData[5] == "--" || (parseInt(moveData[5]) + accDiff) >= 100) }
break;
case "Inaccurate":
filterFunction = function(moveData) { return (moveData[5] != "--" && (parseInt(moveData[5]) + accDiff) < 100) }
break;
case "Warnlist":
filterFunction = function(moveData) { return moveWarnlist.includes(moveData[0]) }
break;
case "Not Warnlist":
filterFunction = function(moveData) { return !moveWarnlist.includes(moveData[0]) }
break;
}
// Build the actual table to be output.
let movepoolTable = [];
const splitMovepool = movepool.map( (moveRow) => moveRow[0] );
// For each extant Move...
for (var i = 0; i < movesData.length; i++) {
const moveRow = movesData[i];
// ...Check if that Move is in our attacker's movepool.
if ( splitMovepool.includes(moveRow[0]) ) {
// Skip the Move if it fails the passed filter criteria.
if ( !filterFunction(moveRow) ) { continue; }
// Format the power to be displayed in the table.
let powerDisplay = moveRow[4]; // Don't do parseInt() yet.
if ( !isNaN(parseFloat(powerDisplay)) && options["Power"] != "Raw BAP") {
powerDisplay = parseFloat(powerDisplay);
powerDisplay += (moveRow[2] == "Physical") ? physDiff : specDiff;
powerDisplay += (attackerSpecies["Types"].split('/').includes(moveRow[1])) ? 4 : 0; // STAB
if (options["Power"] == "Estimated Damage") {
powerDisplay *= defenderTypeChart[moveRow[1]];
}
powerDisplay = Math.max(1, powerDisplay);
}
movepoolTable.push([moveRow[0], moveRow[1], moveRow[flexColumnDataIndex], powerDisplay]);
}
}
if (movepoolTable === undefined || movepoolTable.length == 0) { return "No moves found."; }
// Sort the table based on the passed options.
switch (options["Sort"]) {
case "Type":
return movepoolTable.sort((a, b) => a[1].localeCompare(b[1]));
case "Flex Column":
return movepoolTable.sort(flexColumnSortFunction);
case "Power":
return movepoolTable.sort(function(a, b) {let aNum = a[3] == "??" ? 999 : parseFloat(a[3]); aNum = isNaN(aNum) ? 0 : aNum;
let bNum = b[3] == "??" ? 999 : parseFloat(b[3]); bNum = isNaN(bNum) ? 0 : bNum;
return bNum - aNum });
default: // the array is already sorted by Name by default.
return movepoolTable;
}
}
/**
* Receiving a Type, or two Types in one String seperated by "/", returns an Object with
* an Effectiveness for each extant Type.
*/
function makeEffectivenessObject(inputTypes = "") {
// Get the list of object keys in the type chart.
const extantTypes = Object.keys(TYPE_CHART);
// Ensure that any input types actually exist.
let ourTypes = [];
for (const type of inputTypes.split('/')) {
if (-1 != extantTypes.indexOf(type)) {
ourTypes.push(type);
}
}
// this object will hold all of our { "Bug":1.5, "Dark":1.5 ... } multipliers for export.
var multiChart = {};
// for each extant type... get the type's rowIndex
for (var attackTypeName of extantTypes) { // { chart:[0,0,0], index:0}
// get the index of the "attacking type"; our types are the "defending types"
var attackingIndex = TYPE_CHART[attackTypeName].index - 1; // The DAT stores index as "rowIndex" starting at 1. So we adjust index by 1.
var effectivenessLevel = 0;
// Iterate over our types, tallying up the effectiveness of the attackType over our two type names.
for (var defTypeName of ourTypes) {
var defType = TYPE_CHART[defTypeName];
effectivenessLevel += defType.chart[attackingIndex];
}
// Set the multiChart's value for the current attacking type to the effectiveness multiplier that we've derived.
if (effectivenessLevel < -50) {
// If immune, set multi to zero and move on...
multiChart[attackTypeName] = 0;
} else {
// ..otherwise, reference the conversion table, like in Rule 8.5
effectivenessLevel = Math.max(Math.min(effectivenessLevel, 4), -4);
multiChart[attackTypeName] = EFFECTIVENESS_CHART.at(effectivenessLevel);
}
}
return multiChart;
}