@ffxiv-teamcraft/simulator
Version:
A FINAL FANTASY XIV crafting simulator
589 lines • 25.8 kB
JavaScript
"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