Fix damage calc... somewhat unify powder format
This commit is contained in:
parent
24892c91fe
commit
295e8f3e36
4 changed files with 167 additions and 148 deletions
|
@ -147,7 +147,6 @@ function toggle_tab(tab) {
|
|||
} else {
|
||||
document.querySelector("#"+tab).style.display = "none";
|
||||
}
|
||||
console.log(document.querySelector("#"+tab).style.display);
|
||||
}
|
||||
|
||||
// toggle spell arrow
|
||||
|
|
|
@ -22,7 +22,6 @@ function update_armor_powder_specials(elem_id) {
|
|||
//update the label associated w/ the slider
|
||||
let elem = document.getElementById(elem_id);
|
||||
let label = document.getElementById(elem_id + "_label");
|
||||
|
||||
let value = elem.value;
|
||||
|
||||
label.textContent = label.textContent.split(":")[0] + ": " + value
|
||||
|
@ -86,23 +85,18 @@ let powder_special_input = new (class extends ComputeNode {
|
|||
})();
|
||||
|
||||
function updatePowderSpecials(buttonId) {
|
||||
let name = (buttonId).split("-")[0];
|
||||
let power = (buttonId).split("-")[1]; // [1, 5]
|
||||
|
||||
let prefix = (buttonId).split("-")[0].replace(' ', '_') + '-';
|
||||
let elem = document.getElementById(buttonId);
|
||||
if (elem.classList.contains("toggleOn")) { //toggle the pressed button off
|
||||
elem.classList.remove("toggleOn");
|
||||
} else {
|
||||
if (elem.classList.contains("toggleOn")) { elem.classList.remove("toggleOn"); }
|
||||
else {
|
||||
for (let i = 1;i < 6; i++) { //toggle all pressed buttons of the same powder special off
|
||||
//name is same, power is i
|
||||
if(document.getElementById(name.replace(" ", "_") + "-" + i).classList.contains("toggleOn")) {
|
||||
document.getElementById(name.replace(" ", "_") + "-" + i).classList.remove("toggleOn");
|
||||
}
|
||||
const elem2 = document.getElementById(prefix + i);
|
||||
if(elem2.classList.contains("toggleOn")) { elem2.classList.remove("toggleOn"); }
|
||||
}
|
||||
//toggle the pressed button on
|
||||
elem.classList.add("toggleOn");
|
||||
}
|
||||
|
||||
powder_special_input.mark_dirty().update();
|
||||
}
|
||||
|
||||
|
@ -129,6 +123,7 @@ class PowderSpecialCalcNode extends ComputeNode {
|
|||
}
|
||||
|
||||
class PowderSpecialDisplayNode extends ComputeNode {
|
||||
// TODO: Refactor this entirely to be adding more spells to the spell list
|
||||
constructor() {
|
||||
super('builder-powder-special-display');
|
||||
this.fail_cb = true;
|
||||
|
@ -137,27 +132,11 @@ class PowderSpecialDisplayNode extends ComputeNode {
|
|||
compute_func(input_map) {
|
||||
const powder_specials = input_map.get('powder-specials');
|
||||
const stats = input_map.get('stats');
|
||||
const weapon = input_map.get('weapon');
|
||||
const weapon = input_map.get('build').weapon;
|
||||
displayPowderSpecials(document.getElementById("powder-special-stats"), powder_specials, stats, weapon.statMap, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply armor powders.
|
||||
* Encoding shortcut assumes that all powders give +def to one element
|
||||
* and -def to the element "behind" it in cycle ETWFA, which is true
|
||||
* as of now and unlikely to change in the near future.
|
||||
*/
|
||||
function applyArmorPowders(expandedItem, powders) {
|
||||
for(const id of powders){
|
||||
let powder = powderStats[id];
|
||||
let name = powderNames.get(id).charAt(0);
|
||||
let prevName = skp_elements[(skp_elements.indexOf(name) + 4 )% 5];
|
||||
expandedItem.set(name+"Def", (expandedItem.get(name+"Def") || 0) + powder["defPlus"]);
|
||||
expandedItem.set(prevName+"Def", (expandedItem.get(prevName+"Def") || 0) - powder["defMinus"]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Node for getting an item's stats from an item input field.
|
||||
*
|
||||
|
@ -174,6 +153,11 @@ class ItemInputNode extends InputNode {
|
|||
constructor(name, item_input_field, none_item) {
|
||||
super(name, item_input_field);
|
||||
this.none_item = new Item(none_item);
|
||||
this.category = this.none_item.statMap.get('category');
|
||||
if (this.category == 'armor' || this.category == 'weapon') {
|
||||
this.none_item.statMap.set('powders', []);
|
||||
apply_weapon_powders(this.none_item.statMap); // Needed to put in damagecalc zeros
|
||||
}
|
||||
this.none_item.statMap.set('NONE', true);
|
||||
}
|
||||
|
||||
|
@ -197,17 +181,17 @@ class ItemInputNode extends InputNode {
|
|||
item.statMap.set('powders', powdering);
|
||||
}
|
||||
let type_match;
|
||||
if (this.none_item.statMap.get('category') === 'weapon') {
|
||||
type_match = item.statMap.get('category') === 'weapon';
|
||||
if (this.category == 'weapon') {
|
||||
type_match = item.statMap.get('category') == 'weapon';
|
||||
} else {
|
||||
type_match = item.statMap.get('type') === this.none_item.statMap.get('type');
|
||||
type_match = item.statMap.get('type') == this.none_item.statMap.get('type');
|
||||
}
|
||||
if (type_match) {
|
||||
if (item.statMap.get('category') === 'armor') {
|
||||
applyArmorPowders(item.statMap, powdering);
|
||||
if (item.statMap.get('category') == 'armor') {
|
||||
applyArmorPowders(item.statMap);
|
||||
}
|
||||
else if (item.statMap.get('category') === 'weapon') {
|
||||
apply_weapon_powders(item.statMap, powdering);
|
||||
else if (item.statMap.get('category') == 'weapon') {
|
||||
apply_weapon_powders(item.statMap);
|
||||
}
|
||||
return item;
|
||||
}
|
||||
|
@ -579,7 +563,7 @@ class SpellDamageCalcNode extends ComputeNode {
|
|||
}
|
||||
|
||||
compute_func(input_map) {
|
||||
const weapon = new Map(input_map.get('weapon-input').statMap);
|
||||
const weapon = input_map.get('build').weapon.statMap;
|
||||
const spell_info = input_map.get('spell-info');
|
||||
const spell_parts = spell_info[1];
|
||||
const stats = input_map.get('stats');
|
||||
|
@ -647,6 +631,7 @@ class SpellDisplayNode extends ComputeNode {
|
|||
Returns an array in the order:
|
||||
*/
|
||||
function getMeleeStats(stats, weapon) {
|
||||
stats = new Map(stats); // Shallow copy
|
||||
const weapon_stats = weapon.statMap;
|
||||
const skillpoints = [
|
||||
stats.get('str'),
|
||||
|
@ -665,9 +650,8 @@ function getMeleeStats(stats, weapon) {
|
|||
adjAtkSpd = 0;
|
||||
}
|
||||
|
||||
let damage_mult = stats.get("damageMultiplier");
|
||||
if (weapon_stats.get("type") === "relik") {
|
||||
damage_mult = 0.99; // CURSE YOU WYNNCRAFT
|
||||
stats.set('damageMultiplier', 0.99); // CURSE YOU WYNNCRAFT
|
||||
//One day we will create WynnWynn and no longer have shaman 99% melee injustice.
|
||||
//In all seriousness 99% is because wynn uses 0.33 to estimate dividing the damage by 3 to split damage between 3 beams.
|
||||
}
|
||||
|
@ -1076,7 +1060,7 @@ function builder_graph_init() {
|
|||
// Powder specials.
|
||||
let powder_special_calc = new PowderSpecialCalcNode().link_to(powder_special_input, 'powder-specials');
|
||||
new PowderSpecialDisplayNode().link_to(powder_special_input, 'powder-specials')
|
||||
.link_to(stat_agg_node, 'stats').link_to(item_nodes[8], 'weapon');
|
||||
.link_to(stat_agg_node, 'stats').link_to(build_node, 'build');
|
||||
stat_agg_node.link_to(powder_special_calc, 'powder-boost');
|
||||
stat_agg_node.link_to(armor_powder_node, 'armor-powder');
|
||||
powder_special_input.update();
|
||||
|
@ -1093,7 +1077,7 @@ function builder_graph_init() {
|
|||
spell_node.link_to(stat_agg_node, 'stats')
|
||||
|
||||
let calc_node = new SpellDamageCalcNode(i);
|
||||
calc_node.link_to(item_nodes[8], 'weapon-input').link_to(stat_agg_node, 'stats')
|
||||
calc_node.link_to(build_node, 'build').link_to(stat_agg_node, 'stats')
|
||||
.link_to(spell_node, 'spell-info');
|
||||
spelldmg_nodes.push(calc_node);
|
||||
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
const damageMultipliers = new Map([ ["allytotem", .15], ["yourtotem", .35], ["vanish", 0.80], ["warscream", 0.10], ["bash", 0.50] ]);
|
||||
|
||||
const damage_keys = [ "nDam_", "eDam_", "tDam_", "wDam_", "fDam_", "aDam_" ];
|
||||
const damage_present_key = 'damagePresent';
|
||||
function get_base_dps(item) {
|
||||
const attack_speed_mult = baseDamageMultiplier[attackSpeeds.indexOf(item.get("atkSpd"))];
|
||||
//SUPER JANK @HPP PLS FIX
|
||||
|
@ -27,107 +25,6 @@ function get_base_dps(item) {
|
|||
}
|
||||
}
|
||||
|
||||
// THIS MUTATES THE ITEM
|
||||
function apply_weapon_powders(item) {
|
||||
let present;
|
||||
if (item.get("tier") !== "Crafted") {
|
||||
let weapon_result = calc_weapon_powder(item);
|
||||
let damages = weapon_result[0];
|
||||
present = weapon_result[1];
|
||||
for (const i in damage_keys) {
|
||||
item.set(damage_keys[i], damages[i]);
|
||||
}
|
||||
} else {
|
||||
let base_low = [item.get("nDamBaseLow"),item.get("eDamBaseLow"),item.get("tDamBaseLow"),item.get("wDamBaseLow"),item.get("fDamBaseLow"),item.get("aDamBaseLow")];
|
||||
let results_low = calc_weapon_powder(item, base_low);
|
||||
let damage_low = results_low[0];
|
||||
let base_high = [item.get("nDamBaseHigh"),item.get("eDamBaseHigh"),item.get("tDamBaseHigh"),item.get("wDamBaseHigh"),item.get("fDamBaseHigh"),item.get("aDamBaseHigh")];
|
||||
let results_high = calc_weapon_powder(item, base_high);
|
||||
let damage_high = results_high[0];
|
||||
present = results_high[1];
|
||||
|
||||
for (const i in damage_keys) {
|
||||
item.set(damage_keys[i], [damage_low[i], damage_high[i]]);
|
||||
}
|
||||
}
|
||||
console.log(item);
|
||||
item.set(damage_present_key, present);
|
||||
}
|
||||
|
||||
/**
|
||||
* weapon: Weapon to apply powder to
|
||||
* damageBases: used by crafted
|
||||
*/
|
||||
function calc_weapon_powder(weapon, damageBases) {
|
||||
let powders = weapon.get("powders").slice();
|
||||
|
||||
// Array of neutral + ewtfa damages. Each entry is a pair (min, max).
|
||||
let damages = [
|
||||
weapon.get('nDam').split('-').map(Number),
|
||||
weapon.get('eDam').split('-').map(Number),
|
||||
weapon.get('tDam').split('-').map(Number),
|
||||
weapon.get('wDam').split('-').map(Number),
|
||||
weapon.get('fDam').split('-').map(Number),
|
||||
weapon.get('aDam').split('-').map(Number)
|
||||
];
|
||||
|
||||
// Applying spell conversions
|
||||
let neutralBase = damages[0].slice();
|
||||
let neutralRemainingRaw = damages[0].slice();
|
||||
|
||||
//powder application for custom crafted weapons is inherently fucked because there is no base. Unsure what to do.
|
||||
|
||||
//Powder application for Crafted weapons - this implementation is RIGHT YEAAAAAAAAA
|
||||
//1st round - apply each as ingred, 2nd round - apply as normal
|
||||
if (weapon.get("tier") === "Crafted" && !weapon.get("custom")) {
|
||||
for (const p of powders.concat(weapon.get("ingredPowders"))) {
|
||||
let powder = powderStats[p]; //use min, max, and convert
|
||||
let element = Math.floor((p+0.01)/6); //[0,4], the +0.01 attempts to prevent division error
|
||||
let diff = Math.floor(damageBases[0] * powder.convert/100);
|
||||
damageBases[0] -= diff;
|
||||
damageBases[element+1] += diff + Math.floor( (powder.min + powder.max) / 2 );
|
||||
}
|
||||
//update all damages
|
||||
for (let i = 0; i < damages.length; i++) {
|
||||
damages[i] = [Math.floor(damageBases[i] * 0.9), Math.floor(damageBases[i] * 1.1)];
|
||||
}
|
||||
neutralRemainingRaw = damages[0].slice();
|
||||
neutralBase = damages[0].slice();
|
||||
}
|
||||
|
||||
//apply powders to weapon
|
||||
for (const powderID of powders) {
|
||||
const powder = powderStats[powderID];
|
||||
// Bitwise to force conversion to integer (integer division).
|
||||
const element = (powderID/6) | 0;
|
||||
let conversionRatio = powder.convert/100;
|
||||
if (neutralRemainingRaw[1] > 0) {
|
||||
let min_diff = Math.min(neutralRemainingRaw[0], conversionRatio * neutralBase[0]);
|
||||
let max_diff = Math.min(neutralRemainingRaw[1], conversionRatio * neutralBase[1]);
|
||||
|
||||
//damages[element+1][0] = Math.floor(round_near(damages[element+1][0] + min_diff));
|
||||
//damages[element+1][1] = Math.floor(round_near(damages[element+1][1] + max_diff));
|
||||
//neutralRemainingRaw[0] = Math.floor(round_near(neutralRemainingRaw[0] - min_diff));
|
||||
//neutralRemainingRaw[1] = Math.floor(round_near(neutralRemainingRaw[1] - max_diff));
|
||||
damages[element+1][0] += min_diff;
|
||||
damages[element+1][1] += max_diff;
|
||||
neutralRemainingRaw[0] -= min_diff;
|
||||
neutralRemainingRaw[1] -= max_diff;
|
||||
}
|
||||
damages[element+1][0] += powder.min;
|
||||
damages[element+1][1] += powder.max;
|
||||
}
|
||||
|
||||
// The ordering of these two blocks decides whether neutral is present when converted away or not.
|
||||
let present_elements = []
|
||||
for (const damage of damages) {
|
||||
present_elements.push(damage[1] > 0);
|
||||
}
|
||||
|
||||
// The ordering of these two blocks decides whether neutral is present when converted away or not.
|
||||
damages[0] = neutralRemainingRaw;
|
||||
return [damages, present_elements];
|
||||
}
|
||||
|
||||
function calculateSpellDamage(stats, weapon, conversions, use_spell_damage, ignore_speed=false) {
|
||||
// TODO: Roll all the loops together maybe
|
||||
|
@ -227,10 +124,18 @@ function calculateSpellDamage(stats, weapon, conversions, use_spell_damage, igno
|
|||
raw_boost += stats.get(damage_prefix+'Raw') + stats.get(damage_elements[i]+'DamRaw');
|
||||
}
|
||||
// Next, rainraw and propRaw
|
||||
let new_min = damages_obj[0] + raw_boost + (damages_obj[0] / total_min) * prop_raw;
|
||||
let new_max = damages_obj[1] + raw_boost + (damages_obj[1] / total_max) * prop_raw;
|
||||
if (i != 0) { // rainraw
|
||||
new_min += (damages_obj[0] / total_elem_min) * rainbow_raw;
|
||||
let new_min = damages_obj[0] + raw_boost;
|
||||
let new_max = damages_obj[1] + raw_boost;
|
||||
if (total_max > 0) { // TODO: what about total negative all raw?
|
||||
if (total_elem_min > 0) {
|
||||
new_min += (damages_obj[0] / total_min) * prop_raw;
|
||||
}
|
||||
new_max += (damages_obj[1] / total_max) * prop_raw;
|
||||
}
|
||||
if (i != 0 && total_elem_max > 0) { // rainraw TODO above
|
||||
if (total_elem_min > 0) {
|
||||
new_min += (damages_obj[0] / total_elem_min) * rainbow_raw;
|
||||
}
|
||||
new_max += (damages_obj[1] / total_elem_max) * rainbow_raw;
|
||||
}
|
||||
damages_obj[0] = new_min;
|
||||
|
|
131
js/powders.js
131
js/powders.js
|
@ -61,3 +61,134 @@ let powderSpecialStats = [
|
|||
_ps("Courage",new Map([ ["Duration", [6,6.5,7,7.5,8]],["Damage", [75,87.5,100,112.5,125]],["Damage Boost", [70,90,110,130,150]] ]),"Endurance",new Map([ ["Damage", [2,3,4,5,6]],["Duration", [8,8,8,8,8]],["Description", "Hit Taken"] ]),200), //f
|
||||
_ps("Wind Prison",new Map([ ["Duration", [3,3.5,4,4.5,5]],["Damage Boost", [400,450,500,550,600]],["Knockback", [8,12,16,20,24]] ]),"Dodge",new Map([ ["Damage",[2,3,4,5,6]],["Duration",[2,3,4,5,6]],["Description","Near Mobs"] ]),150) //a
|
||||
];
|
||||
|
||||
/**
|
||||
* Apply armor powders.
|
||||
* Encoding shortcut assumes that all powders give +def to one element
|
||||
* and -def to the element "behind" it in cycle ETWFA, which is true
|
||||
* as of now and unlikely to change in the near future.
|
||||
*/
|
||||
function applyArmorPowders(expandedItem) {
|
||||
const powders = expandedItem.get('powders');
|
||||
for(const id of powders){
|
||||
let powder = powderStats[id];
|
||||
let name = powderNames.get(id).charAt(0);
|
||||
let prevName = skp_elements[(skp_elements.indexOf(name) + 4 )% 5];
|
||||
expandedItem.set(name+"Def", (expandedItem.get(name+"Def") || 0) + powder["defPlus"]);
|
||||
expandedItem.set(prevName+"Def", (expandedItem.get(prevName+"Def") || 0) - powder["defMinus"]);
|
||||
}
|
||||
}
|
||||
|
||||
const damage_keys = [ "nDam_", "eDam_", "tDam_", "wDam_", "fDam_", "aDam_" ];
|
||||
const damage_present_key = 'damagePresent';
|
||||
/**
|
||||
* Apply weapon powders. MUTATES THE ITEM!
|
||||
* Adds entries for `damage_keys` and `damage_present_key`
|
||||
* For normal items, `damage_keys` is 6x2 list (elem: [min, max])
|
||||
* For crafted items, `damage_keys` is 6x2x2 list (elem: [minroll: [min, max], maxroll: [min, max]])
|
||||
*/
|
||||
function apply_weapon_powders(item) {
|
||||
let present;
|
||||
if (item.get("tier") !== "Crafted") {
|
||||
let weapon_result = calc_weapon_powder(item);
|
||||
let damages = weapon_result[0];
|
||||
present = weapon_result[1];
|
||||
for (const i in damage_keys) {
|
||||
item.set(damage_keys[i], damages[i]);
|
||||
}
|
||||
} else {
|
||||
let base_low = [item.get("nDamBaseLow"),item.get("eDamBaseLow"),item.get("tDamBaseLow"),item.get("wDamBaseLow"),item.get("fDamBaseLow"),item.get("aDamBaseLow")];
|
||||
let results_low = calc_weapon_powder(item, base_low);
|
||||
let damage_low = results_low[0];
|
||||
let base_high = [item.get("nDamBaseHigh"),item.get("eDamBaseHigh"),item.get("tDamBaseHigh"),item.get("wDamBaseHigh"),item.get("fDamBaseHigh"),item.get("aDamBaseHigh")];
|
||||
let results_high = calc_weapon_powder(item, base_high);
|
||||
let damage_high = results_high[0];
|
||||
present = results_high[1];
|
||||
|
||||
for (const i in damage_keys) {
|
||||
item.set(damage_keys[i], [damage_low[i], damage_high[i]]);
|
||||
}
|
||||
}
|
||||
item.set(damage_present_key, present);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate weapon damage from powder.
|
||||
*
|
||||
* Params:
|
||||
* weapon: Weapon to apply powder to
|
||||
* damageBases: used by crafted
|
||||
*
|
||||
* Return:
|
||||
* [damages, damage_present]
|
||||
*/
|
||||
function calc_weapon_powder(weapon, damageBases) {
|
||||
let powders = weapon.get("powders").slice();
|
||||
|
||||
// Array of neutral + ewtfa damages. Each entry is a pair (min, max).
|
||||
let damages = [
|
||||
weapon.get('nDam').split('-').map(Number),
|
||||
weapon.get('eDam').split('-').map(Number),
|
||||
weapon.get('tDam').split('-').map(Number),
|
||||
weapon.get('wDam').split('-').map(Number),
|
||||
weapon.get('fDam').split('-').map(Number),
|
||||
weapon.get('aDam').split('-').map(Number)
|
||||
];
|
||||
|
||||
// Applying spell conversions
|
||||
let neutralBase = damages[0].slice();
|
||||
let neutralRemainingRaw = damages[0].slice();
|
||||
|
||||
//powder application for custom crafted weapons is inherently fucked because there is no base. Unsure what to do.
|
||||
|
||||
//Powder application for Crafted weapons - this implementation is RIGHT YEAAAAAAAAA
|
||||
//1st round - apply each as ingred, 2nd round - apply as normal
|
||||
if (weapon.get("tier") === "Crafted" && !weapon.get("custom")) {
|
||||
for (const p of powders.concat(weapon.get("ingredPowders"))) {
|
||||
let powder = powderStats[p]; //use min, max, and convert
|
||||
let element = Math.floor((p+0.01)/6); //[0,4], the +0.01 attempts to prevent division error
|
||||
let diff = Math.floor(damageBases[0] * powder.convert/100);
|
||||
damageBases[0] -= diff;
|
||||
damageBases[element+1] += diff + Math.floor( (powder.min + powder.max) / 2 );
|
||||
}
|
||||
//update all damages
|
||||
for (let i = 0; i < damages.length; i++) {
|
||||
damages[i] = [Math.floor(damageBases[i] * 0.9), Math.floor(damageBases[i] * 1.1)];
|
||||
}
|
||||
neutralRemainingRaw = damages[0].slice();
|
||||
neutralBase = damages[0].slice();
|
||||
}
|
||||
|
||||
//apply powders to weapon
|
||||
for (const powderID of powders) {
|
||||
const powder = powderStats[powderID];
|
||||
// Bitwise to force conversion to integer (integer division).
|
||||
const element = (powderID/6) | 0;
|
||||
let conversionRatio = powder.convert/100;
|
||||
if (neutralRemainingRaw[1] > 0) {
|
||||
let min_diff = Math.min(neutralRemainingRaw[0], conversionRatio * neutralBase[0]);
|
||||
let max_diff = Math.min(neutralRemainingRaw[1], conversionRatio * neutralBase[1]);
|
||||
|
||||
//damages[element+1][0] = Math.floor(round_near(damages[element+1][0] + min_diff));
|
||||
//damages[element+1][1] = Math.floor(round_near(damages[element+1][1] + max_diff));
|
||||
//neutralRemainingRaw[0] = Math.floor(round_near(neutralRemainingRaw[0] - min_diff));
|
||||
//neutralRemainingRaw[1] = Math.floor(round_near(neutralRemainingRaw[1] - max_diff));
|
||||
damages[element+1][0] += min_diff;
|
||||
damages[element+1][1] += max_diff;
|
||||
neutralRemainingRaw[0] -= min_diff;
|
||||
neutralRemainingRaw[1] -= max_diff;
|
||||
}
|
||||
damages[element+1][0] += powder.min;
|
||||
damages[element+1][1] += powder.max;
|
||||
}
|
||||
|
||||
// The ordering of these two blocks decides whether neutral is present when converted away or not.
|
||||
let present_elements = []
|
||||
for (const damage of damages) {
|
||||
present_elements.push(damage[1] > 0);
|
||||
}
|
||||
|
||||
// The ordering of these two blocks decides whether neutral is present when converted away or not.
|
||||
damages[0] = neutralRemainingRaw;
|
||||
return [damages, present_elements];
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue