UNPKG

@ffxiv-teamcraft/simulator

Version:
589 lines 25.8 kB
"use strict"; var __assign = (this && this.__assign) || function () { __assign = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.Simulation = void 0; var buff_enum_1 = require("../model/buff.enum"); var tables_1 = require("../model/tables"); var simulation_fail_cause_enum_1 = require("../model/simulation-fail-cause.enum"); var step_state_1 = require("../model/step-state"); var final_appraisal_1 = require("../model/actions/buff/final-appraisal"); var remove_final_appraisal_1 = require("../model/actions/other/remove-final-appraisal"); var Simulation = /** @class */ (function () { function Simulation(recipe, actions, _crafterStats, hqIngredients, stepStates, fails, startingQuality) { if (hqIngredients === void 0) { hqIngredients = []; } if (stepStates === void 0) { stepStates = {}; } if (fails === void 0) { fails = []; } if (startingQuality === void 0) { startingQuality = 0; } this.recipe = recipe; this.actions = actions; this._crafterStats = _crafterStats; this.hqIngredients = hqIngredients; this.stepStates = stepStates; this.fails = fails; this.progression = 0; this.quality = 0; this.startingQuality = 0; this.state = step_state_1.StepState.NORMAL; this.buffs = []; this.success = undefined; this.steps = []; this.lastPossibleReclaimStep = -1; // equals the index of the last step where you have CP/durability for Reclaim, // or -1 if Reclaim is uncastable (i.e. not enough CP) this.safe = false; this.possibleConditions = []; this.durability = recipe.durability; this.availableCP = this._crafterStats.cp; this.maxCP = this.availableCP; var _loop_1 = function (ingredient) { // Get the ingredient in the recipe var ingredientDetails = this_1.recipe.ingredients.find(function (i) { return i.id === ingredient.id; }); // Check that the ingredient in included in the recipe if (ingredientDetails !== undefined && ingredientDetails.quality) { this_1.quality += ingredientDetails.quality * ingredient.amount; } }; var this_1 = this; for (var _i = 0, _a = this.hqIngredients; _i < _a.length; _i++) { var ingredient = _a[_i]; _loop_1(ingredient); } if (this.hqIngredients.length === 0) { this.quality = startingQuality; } this.quality = Math.floor(this.quality); this.startingQuality = this.quality; this.possibleConditions = this.recipe.conditionsFlag .toString(2) .split('') .reverse() .map(function (value, index) { if (value === '1') { return (index + 1); } else { return null; } }) .filter(function (condition) { return condition !== null; }); } Object.defineProperty(Simulation.prototype, "lastStep", { get: function () { return this.steps[this.steps.length - 1]; }, enumerable: false, configurable: true }); Simulation.prototype.hasComboAvailable = function (actionId) { for (var index = this.steps.length - 1; index >= 0; index--) { var step = this.steps[index]; // If we end up finding the action, the combo is available if (step.action.getIds()[0] === actionId && step.success) { return true; } // If there's an action that isn't skipped (fail or not), combo is broken if (!step.skipped) { return false; } } return false; }; Object.defineProperty(Simulation.prototype, "crafterStats", { get: function () { return this._crafterStats; }, enumerable: false, configurable: true }); Simulation.prototype.getReliabilityReport = function () { this.reset(); var results = []; // Let's run the simulation 200 times. for (var i = 0; i < 200; i++) { results.push(this.run(false)); this.reset(); } var successPercent = (results.filter(function (res) { return res.success; }).length / results.length) * 100; var hqPercent = results.reduce(function (p, c) { return p + c.hqPercent; }, 0) / results.length; var hqMedian; results = results.sort(function (a, b) { return a.hqPercent - b.hqPercent; }); if (results.length % 2) { hqMedian = results[Math.floor(results.length / 2)].hqPercent; } else { hqMedian = (results[Math.floor(results.length / 2)].hqPercent + results[Math.ceil(results.length / 2)].hqPercent) / 2; } return { rawData: results, successPercent: Math.round(successPercent), averageHQPercent: Math.round(hqPercent), medianHQPercent: hqMedian, minHQPercent: results[0].hqPercent, maxHQPercent: results[results.length - 1].hqPercent, }; }; Simulation.prototype.addInnerQuietStacks = function (stacks) { if (!this.hasBuff(buff_enum_1.Buff.INNER_QUIET)) { this.buffs.push({ appliedStep: this.steps.length, stacks: Math.min(stacks, 10), buff: buff_enum_1.Buff.INNER_QUIET, duration: Infinity, }); } else { var iq = this.getBuff(buff_enum_1.Buff.INNER_QUIET); iq.stacks = Math.min(iq.stacks + stacks, 10); } }; /** * * @param thresholds an array of quality thresholds, Collectibility ratings must be scaled before input * @returns a boolean for successful calculation, and the minimum value for each stat */ Simulation.prototype.getMinStats = function (thresholds) { var _this = this; var _a, _b; if (thresholds === void 0) { thresholds = []; } var totalIterations = 0; var result = this.run(true); var originalHqPercent = result.hqPercent; var originalQuality = result.simulation.quality; var originalStats = __assign({}, this.crafterStats); var res = { control: this.crafterStats._control, craftsmanship: this.crafterStats.craftsmanship, cp: this.crafterStats.cp, found: true, }; // Note that thresholds are actual quality, so Collectibility rating must scale before input var rating = originalQuality; if (thresholds.length > 0) { rating = thresholds.reduce(function (current, next) { return (next > originalQuality ? current : Math.max(current, next)); }, 0); } var bisect = function (stat, start, end) { if (start === end) { return start; } totalIterations++; // Our operating new stat value var test = Math.floor((start + end) / 2); switch (stat) { case 'cms': _this.crafterStats.craftsmanship = test; break; case 'cp': _this.crafterStats.cp = test; break; } _this.reset(); result = _this.run(true); // CP needs to know we didn't gimp quality, so check both values if (result.success && result.hqPercent >= originalHqPercent) { // Due to flooring, if the 2 numbers are adjacent, test will be the same as the lower if (test === start) { return test; } return bisect(stat, start, test); } else { // If it fails but our test was 1 below the "good" side, then the end was the answer if (test === end - 1) { switch (stat) { case 'cms': _this.crafterStats.craftsmanship = end; break; case 'cp': _this.crafterStats.cp = end; break; } return end; } return bisect(stat, test, end); } }; var bisectControl = function (start, end) { if (start === end) { return start; } totalIterations++; // Our operating new stat value var test = Math.floor((start + end) / 2); _this.crafterStats._control = test; _this.reset(); result = _this.run(true); // If we have thresholds and didn't max the recipe, target rating, otherwise HQ chance var comparator = thresholds.length > 0 && originalHqPercent < 100 ? rating : originalHqPercent; // If we have thresholds and didn't max the recipe, use quality, otherwise HQ chance var outcome = thresholds.length > 0 && originalHqPercent < 100 ? result.simulation.quality : result.hqPercent; // Switch between the 2 control targets if (outcome < comparator) { if (test === end - 1) { _this.crafterStats._control = end; return end; } return bisectControl(test, end); } else { if (test === start) { return test; } return bisectControl(start, test); } }; // Narrow the window when possible, or return the min if we're too low var cmsBase = (_a = this.recipe.craftsmanshipReq) !== null && _a !== void 0 ? _a : 1; res.craftsmanship = cmsBase < originalStats.craftsmanship ? bisect('cms', cmsBase, originalStats.craftsmanship) : cmsBase; var ctlBase = (_b = this.recipe.controlReq) !== null && _b !== void 0 ? _b : 1; res.control = ctlBase < originalStats._control ? bisectControl(ctlBase, originalStats._control) : ctlBase; // We need to reset control to make sure result.hqPercent is accurate this.crafterStats._control = originalStats._control; res.cp = bisect('cp', 180, originalStats.cp); if (totalIterations >= 10000) { res.found = false; } this.crafterStats.cp = originalStats.cp; this.crafterStats.craftsmanship = originalStats.craftsmanship; this.crafterStats._control = originalStats._control; return res; }; Simulation.prototype.reset = function () { delete this.success; this.progression = 0; this.durability = this.recipe.durability; this.quality = this.startingQuality; this.buffs = []; this.steps = []; this.maxCP = this.crafterStats.cp; this.availableCP = this.maxCP; this.state = step_state_1.StepState.NORMAL; this.safe = false; }; /** * Run the simulation. * @param {boolean} linear should everything be linear (aka no fail on actions, Initial preparations never procs) * @param maxTurns * @param safeMode Safe mode makes all the actions that have success chances < 100 * @returns {ActionResult[]} */ Simulation.prototype.run = function (linear, maxTurns, safeMode) { var _this = this; if (linear === void 0) { linear = false; } if (maxTurns === void 0) { maxTurns = Infinity; } if (safeMode === void 0) { safeMode = false; } this.lastPossibleReclaimStep = -1; this.actions .filter(function (a) { return a !== undefined; }) .forEach(function (action, index) { _this.state = _this.stepStates[index] || step_state_1.StepState.NORMAL; var result; var failCause = undefined; var canUseAction = action.canBeUsed(_this, linear); if (!canUseAction) { failCause = action.getFailCause(_this, linear, safeMode); } var hasEnoughCP = action.getBaseCPCost(_this) <= _this.availableCP; if (!hasEnoughCP) { failCause = simulation_fail_cause_enum_1.SimulationFailCause.NOT_ENOUGH_CP; } // If we can use the action if (_this.success === undefined && hasEnoughCP && _this.steps.length < maxTurns && canUseAction) { result = _this.runAction(action, linear, safeMode, index); } else { // If we can't, add the step to the result but skip it. result = { action: action, success: null, addedQuality: 0, addedProgression: 0, cpDifference: 0, skipped: true, solidityDifference: 0, state: _this.state, failCause: failCause, }; } if (_this.steps.length < maxTurns) { var qualityBefore = _this.quality; var progressionBefore = _this.progression; var durabilityBefore = _this.durability; var cpBefore = _this.availableCP; var skipTicksOnFail = !result.success && action.skipOnFail(); if (_this.success === undefined && !action.skipsBuffTicks() && !skipTicksOnFail) { // Tick buffs after checking synth result, so if we reach 0 durability, synth fails. _this.tickBuffs(linear, action); } result.afterBuffTick = { // Amount of progression added to the craft addedProgression: _this.progression - progressionBefore, // Amount of quality added to the craft addedQuality: _this.quality - qualityBefore, // CP added to the craft (negative if removed) cpDifference: _this.availableCP - cpBefore, // Solidity added to the craft (negative if removed) solidityDifference: _this.durability - durabilityBefore, }; } // Tick state to change it for next turn if not in linear mode if (!linear && !action.is(final_appraisal_1.FinalAppraisal) && !action.is(remove_final_appraisal_1.RemoveFinalAppraisal)) { _this.tickState(); } _this.steps.push(result); }); var failedAction = this.steps.find(function (step) { return step.failCause !== undefined; }); var res = { steps: this.steps, hqPercent: this.getHQPercent(), success: this.progression >= this.recipe.progress, simulation: this, }; if (this.recipe.requiredQuality) { var qualityRequirementMet = this.quality >= this.recipe.requiredQuality; res.success = res.success && qualityRequirementMet; if (!res.success) { res.failCause = simulation_fail_cause_enum_1.SimulationFailCause[simulation_fail_cause_enum_1.SimulationFailCause.QUALITY_TOO_LOW]; } } if (failedAction !== undefined && failedAction.failCause) { res.failCause = simulation_fail_cause_enum_1.SimulationFailCause[failedAction.failCause]; } return res; }; /** * Runs an action, can be called from external class (Whistle for instance). * @param {CraftingAction} action * @param {boolean} linear * @param {boolean} safeMode * @param index */ Simulation.prototype.runAction = function (action, linear, safeMode, index) { if (linear === void 0) { linear = false; } if (safeMode === void 0) { safeMode = false; } if (index === void 0) { index = -1; } // The roll for the current action's success rate, 0 if ideal mode, as 0 will even match a 1% chances. var probabilityRoll = linear ? 0 : Math.random() * 100; if (this.fails.includes(index)) { // Impossible to succeed probabilityRoll = 999; } var qualityBefore = this.quality; var progressionBefore = this.progression; var durabilityBefore = this.durability; var cpBefore = this.availableCP; var combo = action.hasCombo(this); var failCause = undefined; var success = false; if (safeMode && (action.getSuccessRate(this) < 100 || (action.requiresGood() && !this.hasBuff(buff_enum_1.Buff.HEART_AND_SOUL)))) { failCause = simulation_fail_cause_enum_1.SimulationFailCause.UNSAFE_ACTION; action.onFail(this); this.safe = false; } else { if (action.getSuccessRate(this) >= probabilityRoll) { action.execute(this, safeMode); success = true; } else { action.onFail(this); } } // Even if the action failed, we have to remove the durability cost if (this.hasBuff(buff_enum_1.Buff.TRAINED_PERFECTION) && action.getDurabilityCost(this) > 0) { this.removeBuff(buff_enum_1.Buff.TRAINED_PERFECTION); } else { this.durability -= action.getDurabilityCost(this); } // Even if the action failed, CP has to be consumed too this.availableCP -= action.getCPCost(this, linear); if (this.progression >= this.recipe.progress) { this.success = true; } else if (this.durability <= 0) { failCause = simulation_fail_cause_enum_1.SimulationFailCause.DURABILITY_REACHED_ZERO; // Check durability to see if the craft is failed or not this.success = false; } // return action result return { action: action, success: success, addedQuality: this.quality - qualityBefore, addedProgression: this.progression - progressionBefore, cpDifference: this.availableCP - cpBefore, skipped: false, solidityDifference: this.durability - durabilityBefore, state: this.state, failCause: failCause, combo: combo, }; }; Simulation.prototype.hasBuff = function (buff) { return this.buffs.find(function (row) { return row.buff === buff; }) !== undefined; }; Simulation.prototype.getBuff = function (buff) { return this.buffs.find(function (row) { return row.buff === buff; }); }; Simulation.prototype.removeBuff = function (buff) { this.buffs = this.buffs.filter(function (row) { return row.buff !== buff; }); }; Simulation.prototype.repair = function (amount) { this.durability += amount; if (this.durability > this.recipe.durability) { this.durability = this.recipe.durability; } }; Simulation.prototype.clone = function () { return new Simulation(this.recipe, this.actions, this.crafterStats, this.hqIngredients, this.stepStates, this.fails, this.startingQuality); }; Simulation.prototype.getHQPercent = function () { var qualityPercent = Math.min(this.quality / this.recipe.quality, 1) * 100; if (qualityPercent === 0) { return 1; } else if (qualityPercent >= 100) { return 100; } else { return tables_1.Tables.HQ_TABLE[Math.floor(qualityPercent)]; } }; Simulation.prototype.tickBuffs = function (linear, action) { var _this = this; if (linear === void 0) { linear = false; } for (var _i = 0, _a = this.buffs; _i < _a.length; _i++) { var effectiveBuff = _a[_i]; // We are checking the appliedStep because ticks only happen at the beginning of the second turn after the application, // For instance, Great strides launched at turn 1 will start to loose duration at the beginning of turn 3 if (effectiveBuff.appliedStep < this.steps.length) { // If the buff has something to do, let it do it if (effectiveBuff.tick !== undefined) { effectiveBuff.tick(this, linear, action); } effectiveBuff.duration--; } } this.buffs .filter(function (buff) { return buff.duration <= 0 && buff.onExpire !== undefined; }) .forEach(function (expired) { expired.onExpire(_this, linear); }); this.buffs = this.buffs.filter(function (buff) { return buff.duration > 0; }); }; /** * Changes the state of the craft, * source: https://github.com/Ermad/ffxiv-craft-opt-web/blob/master/app/js/ffxivcraftmodel.js#L255 */ Simulation.prototype.tickState = function () { var _this = this; // If current state is EXCELLENT, then next one is poor if (this.state === step_state_1.StepState.EXCELLENT) { this.state = step_state_1.StepState.POOR; return; } // If current state is GOOD_OMEN, then next one is GOOD if (this.state === step_state_1.StepState.GOOD_OMEN) { this.state = step_state_1.StepState.GOOD; return; } // If current state is ROBUST, then next one is STURDY if (this.state === step_state_1.StepState.ROBUST) { this.state = step_state_1.StepState.STURDY; return; } // LV 63 Trait for improved Good chances (Quality Assurance) var goodChance = this.crafterStats.level >= 63 ? 0.25 : 0.2; var statesAndRates = this.possibleConditions .filter(function (condition) { return condition !== step_state_1.StepState.NORMAL; }) .map(function (condition) { // Default rate - most conditions are 12% so here we are. var rate = 0.12; switch (condition) { case step_state_1.StepState.GOOD: rate = _this.recipe.expert ? 0.12 : goodChance; break; case step_state_1.StepState.EXCELLENT: rate = _this.recipe.expert ? 0 : 0.04; break; case step_state_1.StepState.POOR: rate = 0; break; case step_state_1.StepState.CENTERED: rate = 0.15; break; case step_state_1.StepState.PLIANT: rate = 0.12; break; case step_state_1.StepState.STURDY: rate = 0.15; break; case step_state_1.StepState.ROBUST: rate = 0.1; break; case step_state_1.StepState.MALLEABLE: rate = 0.12; break; case step_state_1.StepState.PRIMED: rate = 0.12; break; case step_state_1.StepState.GOOD_OMEN: rate = 0.1; break; } return { item: condition, weight: rate, }; }); var nonNormalRate = statesAndRates .map(function (val) { return val.weight; }) .reduce(function (accumulator, weight) { return accumulator + weight; }); statesAndRates.push({ item: step_state_1.StepState.NORMAL, weight: 1 - nonNormalRate, }); this.state = getWeightedRandom(statesAndRates); }; return Simulation; }()); exports.Simulation = Simulation; var getWeightedRandom = function (weightedItems) { var totalWeights = weightedItems .map(function (val) { return val.weight; }) .reduce(function (accumulator, weight) { return accumulator + weight; }); var threshold = Math.random() * totalWeights; var check = 0; for (var _i = 0, weightedItems_1 = weightedItems; _i < weightedItems_1.length; _i++) { var _a = weightedItems_1[_i], item = _a.item, weight = _a.weight; check += weight; if (check > threshold) { return item; } } return weightedItems[weightedItems.length - 1].item; }; //# sourceMappingURL=simulation.js.map