number }>} */
itemSortOrderMap = {
'default': {
desc: UiLocale.chestDrop.sortOrder.default[language],
weight: null,
},
'rarity': {
desc: UiLocale.chestDrop.sortOrder.rarity[language],
weight: (item, rarity) => rarity * 1e15 + Market.getPriceByHrid(item.hrid),
},
'totalBid': {
desc: UiLocale.chestDrop.sortOrder.totalBid[language],
weight: (item, rarity) => Market.getPriceByHrid(item.hrid) * item.count,
},
'unitBid': {
desc: UiLocale.chestDrop.sortOrder.unitBid[language],
weight: (item, rarity) => Market.getPriceByHrid(item.hrid),
},
}
constructor() {
MessageHandler.addListener('loot_opened', msg => { this.onLootOpened(msg); });
document.addEventListener('copy', (e) => {
// @ts-ignore
if (!document.getElementById('lll_chestOpenPopup')?.contains(e.target)) return;
if (!e.clipboardData) return;
let content = window?.getSelection().toString();
content = content.replaceAll(/└|├|\x20/g, '').replaceAll('\t', '').replaceAll('\n', ' ').replaceAll(':', ': ');
e.clipboardData.setData('text/plain', content.trim());
e.preventDefault();
});
}
/**
* @param {CountedItem} openedItem
* @param {CountedItem[]} gainedItems
*/
constructDetailsPanel(openedItem, gainedItems) {
let panel = document.createElement('div');
panel.style.padding = '20px';
const detailsPanel = () => {
const contentDiv = document.createElement('div');
panel.appendChild(contentDiv);
// 创建图表
const canvas = ChartRenderer.getCanvas();
contentDiv.appendChild(canvas.wrapper);
this.renderDetailsChart(canvas.canvas, openedItem, gainedItems);
// 添加自定义按钮
const customButton = Ui.button(UiLocale.chestDrop.distribution.allChest[language]);
customButton.onclick = () => {
panel.removeChild(contentDiv);
customPanel();
};
contentDiv.appendChild(Ui.div(null, customButton));
}
const customPanel = () => {
const defaultChestHrid = openedItem.hrid;
const defaultChestCount = openedItem.count;
const maxCount = Config.chestDrop.ui.customPanelMaxCount;
const maxSliderValue = Config.chestDrop.ui.customPanelMaxSliderValue;
let count = defaultChestCount;
const renderChart = (value = null) => {
const itemHrid = mapSelect.options[mapSelect.selectedIndex].value;
if (value !== null) count = value;
while (canvasDiv.lastChild) canvasDiv.removeChild(canvasDiv.lastChild);
const canvas = ChartRenderer.getCanvas();
canvasDiv.appendChild(canvas.wrapper);
this.renderCustomChart(canvas.canvas, { hrid: itemHrid, count: count });
}
const contentDiv = Ui.div('lll_div_column');
panel.appendChild(contentDiv);
// 设置
const configDiv = Ui.div({ style: 'padding: 5px 0; gap: 15px; display: flex; justify-content: space-around;' });
contentDiv.appendChild(configDiv);
const mapSelectorDiv = Ui.div({ style: 'display: flex; gap: 10px;' });
mapSelectorDiv.appendChild(Ui.div('lll_label', UiLocale.chestDrop.distribution.chestSelect[language]));
const mapSelect = Ui.elem('select', 'lll_input_select');
mapSelectorDiv.appendChild(mapSelect);
const sortedChestData = Object.entries(Market.chestDropData)
.sort((a, b) => a[1].order - b[1].order);
for (let [hrid, data] of sortedChestData) {
const text = Localizer.hridToName(hrid);
let option = new Option(text, hrid);
if (defaultChestHrid === hrid) option.selected = true;
mapSelect.options.add(option);
}
mapSelect.onchange = () => { renderChart(); };
configDiv.appendChild(mapSelectorDiv);
let runCountInputDiv = Ui.div({ style: 'display: flex; gap: 10px;' });
configDiv.appendChild(runCountInputDiv);
runCountInputDiv.appendChild(Ui.div('lll_label', UiLocale.chestDrop.distribution.cntInput[language]));
const getRunCount = (val, inv = 1) => {
const A = maxSliderValue * maxCount / (maxCount - maxSliderValue);
const x = parseInt(val);
return Math.round(A * x / (A - x * inv));
};
const runCountInput = Ui.slider({
initValue: defaultChestCount,
minValue: 1,
maxValue: maxCount,
mapFunc: x => getRunCount(x, 1),
invMapFunc: x => getRunCount(x, -1),
oninput: x => { if (!isMobile) renderChart(x); },
onchange: x => { renderChart(x); },
}, null, { style: { minWidth: '60px' } })
runCountInputDiv.appendChild(runCountInput);
// 图表容器
const canvasDiv = Ui.div();
contentDiv.appendChild(canvasDiv);
renderChart();
// 返回到详细页面
const customButton = Ui.button(UiLocale.chestDrop.distribution.return[language]);
customButton.onclick = () => {
panel.removeChild(contentDiv);
detailsPanel();
};
contentDiv.appendChild(Ui.div(null, customButton));
}
detailsPanel();
return panel;
}
/**
* @param {HTMLCanvasElement} canvas
* @param {CountedItem} openedItem
* @param {CountedItem[]} gainedItems
*/
renderDetailsChart(canvas, openedItem, gainedItems) {
const eps = Config.chestDrop.ui.detailsChartCdfEps;
const coeff = Config.chestDrop.ui.detailsChartSigmaCoeff;
const income = Market.getTotalPrice(gainedItems);
const stat = ChestDropAnalyzer.analyze(openedItem, income);
const dist = stat.cdf;
const mu = stat.incomeExpectation;
const sigma = Math.sqrt(stat.incomeVariance);
const limit = dist.limit;
const data = {
limitL: Math.max(mu - coeff * sigma, 0),
limitR: Math.max(income, mu + coeff * sigma),
datasets: [{
label: Localizer.hridToName(openedItem.hrid),
display: true,
shadow: income,
color: 0,
cdf: dist.cdf,
}],
};
for (const chest of data.datasets) {
data.limitL = Math.min(data.limitL, Utils.binarySearch(chest.cdf, 0, limit, eps));
data.limitR = Math.max(data.limitR, Utils.binarySearch(chest.cdf, 0, limit, 1 - eps));
}
ChartRenderer.cdfPdfChart(canvas, data);
}
/**
* @param {HTMLCanvasElement} canvas
* @param {CountedItem} openedItem
*/
renderCustomChart(canvas, openedItem) {
const eps = Config.chestDrop.ui.customChartCdfEps;
const coeff = Config.chestDrop.ui.customChartSigmaCoeff;
const stat = ChestDropAnalyzer.analyze(openedItem, 0);
const dist = stat.cdf;
let limitL = Utils.binarySearch(dist.cdf, 0, dist.limit, eps);
let limitR = Utils.binarySearch(dist.cdf, 0, dist.limit, 1 - eps);
const median = Utils.binarySearch(dist.cdf, 0, dist.limit, 0.5);
const mu = stat.incomeExpectation;
const sigma = Math.sqrt(stat.incomeVariance);
limitL = Math.max(Math.min(limitL, mu - coeff * sigma), 0);
limitR = Math.max(limitR, mu + coeff * sigma);
ChartRenderer.cdfPdfWithMedianMeanChart(canvas, {
limitL: limitL,
limitR: limitR,
cdf: dist.cdf,
mu: mu,
sigma: sigma,
median: median,
})
}
constructSettingsPanel() {
let panel = Ui.div('lll_div_settingPanelContent');
const locale = UiLocale.chestDrop.settings;
panel.appendChild(SettingsUi.settingRow(
locale.useOriPopup[language], null,
Ui.checkBox({
checked: Config.chestDrop.ui.useOriginalPopup,
onchange: checked => {
Config.chestDrop.ui.useOriginalPopup = checked;
ConfigManager.saveConfig();
}
})
));
return panel;
}
/**
* @param {CountedItem} openedItem
* @param {CountedItem[]} gainedItems
*/
showPopup(openedItem, gainedItems) {
this.popup.open();
// this.popup.addTab('概览', () => this.constructOverviewPanel(), null);
this.popup.addTab(UiLocale.chestDrop.distribution.tabLabel[language], () => this.constructDetailsPanel(openedItem, gainedItems), null);
// this.popup.addTab('历史', () => this.constructHistoryPanel(), null);
this.popup.addTab(UiLocale.chestDrop.settings.tabLabel[language], () => this.constructSettingsPanel(), null);
}
/**
* @param {CountedItem} openedItem
* @param {CountedItem[]} gainedItems
*/
constructOpenChestPopup(openedItem, gainedItems) {
if (Config.chestDrop.verbose) out(openedItem, gainedItems);
const itemStyle = rarity => {
if (rarity === 0) return 'border: 1px solid rgba(96, 96, 109, 1); background-color:rgba(96, 96, 109, 0.5);';
if (rarity === 0.5) return 'border: 1px solid rgb(121, 121, 131); background-color:rgba(112, 112, 126, 0.5); box-shadow: 0 0 3px 1px rgba(138, 138, 150, 0.8);';
if (rarity === 1) return 'border: 1px solid rgba(107, 129, 109, 1); background-color: rgba(107, 129, 109, 0.5);';
if (rarity === 1.5) return 'border: 1px solid rgb(118, 148, 120); background-color: rgba(117, 145, 120, 0.5); box-shadow: 0 0 3px 1px rgba(130, 159, 132, 0.8);';
if (rarity === 2) return 'border: 1px solid rgba(121, 140, 165, 1); background-color: rgba(121, 140, 165, 0.5);';
if (rarity === 2.5) return 'border: 1px solid rgb(134, 160, 180); background-color: rgba(146, 170, 189, 0.5); box-shadow: 0 0 3px 1px rgba(138, 171, 182, 0.8);';
if (rarity === 3 || rarity === 3.5) return 'border: 1px solid rgba(139, 113, 156, 1); background-color: rgba(139, 113, 156, 0.5);';
if (rarity === 4 || rarity === 4.5) return 'border: 1px solid rgba(208, 167, 127, 1); background-color: rgba(208, 167, 127, 0.5);';
if (rarity === 5 || rarity === 5.5) return 'border: 1px solid rgb(196, 130, 130); background-color: rgba(189, 128, 128, 0.5); box-shadow: 0 0 3px 1px rgba(216, 143, 143, 0.8);';
if (rarity === 6 || rarity === 6.5) return 'border: 1px solid rgba(234, 231, 147, 1); background-color: rgba(234, 231, 147, 0.5); box-shadow: 0 0 3px 1.5px rgba(234, 231, 147, 0.8);';
return 'border: 1px solid rgba(96, 96, 109, 1); background-color:rgba(96, 96, 109, 0.5);';
};
const itemIcon = (item, rarity) => {
const { hrid, count } = item;
const ret = Ui.div(
{ style: `margin: auto; width: 60px; height: 60px; font-size: 13px; display: grid; border-radius: 4px; ${itemStyle(rarity)}` },
[
Ui.div({ style: 'grid-area: 1/1; width: 42px; height: 42px; margin: auto;' },
Ui.itemSvgIcon(hrid, 42, true),
),
Ui.div({ style: 'grid-area: 1/1; font-size: 13px; font-weight: 500; display: flex; align-items: flex-end; justify-content: flex-end; margin: 0 2px -1px 0; text-shadow: -1px 0 var(--color-background-game),0 1px var(--color-background-game),1px 0 var(--color-background-game),0 -1px var(--color-background-game); user-select: none;' }, Utils.formatPrice(count, { type: 'mwi' })),
]
);
Tooltip.attach(ret, Tooltip.item(hrid, count), 'center');
return ret;
};
const getRarity = ChestDropAnalyzer.getRarity(openedItem);
const order = this.itemSortOrderMap[Config.chestDrop.ui.overviewItemSortOrder].weight;
const sortedItems = order === null ? gainedItems : gainedItems.sort(
(a, b) => order(b, getRarity(b)) - order(a, getRarity(a))
);
const itemIconList = [];
sortedItems.forEach(item => {
itemIconList.push(itemIcon(item, getRarity(item)))
});
const stat = ChestDropAnalyzer.analyze(openedItem, Market.getTotalPrice(gainedItems));
const colorLuck = `color: ${Utils.luckColor(stat.luck)}`;
const colorAvg = `color: ${Utils.luckColor(stat.income > stat.incomeExpectation)}`;
const tablePrice = (x) => {
let i = x.length - 1;
for (; i >= 0; --i) if (x[i] >= '0' && x[i] <= '9') break;
const unit = x.slice(i + 1);
let num = x.slice(0, i + 1);
return `${num} | ${unit} | `;
};
const currentDiv = Ui.div({ style: 'margin: -2px -4px; font-size: 13px;' }, Ui.elem('table', { style: 'line-height: 1.1; width: 100%;' }, `
${UiLocale.chestDrop.chestOpen.count[language]}: |
${tablePrice(Utils.formatPrice(openedItem.count))}
${UiLocale.chestDrop.chestOpen.income[language]}: |
${tablePrice(Utils.formatPrice(stat.income))}
${stat.income == stat.profit ? '' : `
${UiLocale.chestDrop.chestOpen.profit[language]}: |
${tablePrice(Utils.formatPrice(stat.profit).replace('-', '-'))}
`}
${UiLocale.chestDrop.chestOpen.luck[language]}: |
${tablePrice(Utils.formatLuck(stat.luck))}
|
${UiLocale.chestDrop.chestOpen.incomeExpt[language]}: |
${tablePrice(Utils.formatPrice(stat.incomeExpectation))}
└${UiLocale.chestDrop.chestOpen.stdDev[language]}: |
${tablePrice(Utils.formatPrice(Math.sqrt(stat.incomeVariance)))}
${UiLocale.chestDrop.chestOpen[stat.income > stat.incomeExpectation ? 'higherThanExpt' : 'lowerThanExpt'][language]}:
|
${tablePrice(Utils.formatPrice(Math.abs(stat.income - stat.incomeExpectation)))}
`));
const chestOpenHistory = JSON.parse(localStorage.getItem('Edible_Tools') ?? 'null')?.Chest_Open_Data?.[CharacterData.playerId]
?.开箱数据?.[ClientData.hrid2name(openedItem.hrid)];
let historyDiv;
if (!chestOpenHistory) historyDiv = Ui.div(null, '需安装食用工具');
else {
const count = chestOpenHistory.总计开箱数量 + openedItem.count;
const income = Object.entries(chestOpenHistory.获得物品).reduce(
(pre, cur) => pre + cur[1].数量 * Market.getPriceByName(cur[0]), 0
) + stat.income;
const historyStat = ChestDropAnalyzer.analyze({ hrid: openedItem.hrid, count: count }, income);
const colorLuckHist = `color: ${Utils.luckColor(historyStat.luck)}`;
const colorAvgHist = `color: ${Utils.luckColor(historyStat.income > historyStat.incomeExpectation)}`;
historyDiv = Ui.div({ style: 'margin: -2px -4px; font-size:13px;' }, Ui.elem('table', { style: 'line-height: 1.1; width: 100%;' }, `
${UiLocale.chestDrop.chestOpen.count[language]}: |
${tablePrice(Utils.formatPrice(count))}
${UiLocale.chestDrop.chestOpen.income[language]}: |
${tablePrice(Utils.formatPrice(historyStat.income))}
${historyStat.income == historyStat.profit ? '' : `
${UiLocale.chestDrop.chestOpen.profit[language]}: |
${tablePrice(Utils.formatPrice(historyStat.profit).replace('-', '-'))}
`}
${UiLocale.chestDrop.chestOpen.histLuck[language]}: |
${tablePrice(Utils.formatLuck(historyStat.luck))}
|
${UiLocale.chestDrop.chestOpen.incomeExpt[language]}: |
${tablePrice(Utils.formatPrice(historyStat.incomeExpectation))}
└${UiLocale.chestDrop.chestOpen.stdDev[language]}: |
${tablePrice(Utils.formatPrice(Math.sqrt(historyStat.incomeVariance)))}
${UiLocale.chestDrop.chestOpen[historyStat.income > historyStat.incomeExpectation ? 'higherThanExpt' : 'lowerThanExpt'][language]}:
|
${tablePrice(Utils.formatPrice(Math.abs(historyStat.income - historyStat.incomeExpectation)))}
`));
}
return Ui.div({ style: 'padding: 5px;', id: 'lll_chestOpenPopup' },
Ui.div('lll_div_chestOpenContent', [
Ui.div('lll_div_row', itemIcon(openedItem, 0)),
Ui.div({ className: 'lll_div_row', style: 'margin-top: 8px;' }, Ui.div('lll_div_card', [
Ui.div('lll_div_cardTitle', UiLocale.chestDrop.chestOpen.youFound[language]),
Ui.div({ style: 'margin-top: 3px; width: 100%; display: grid; grid-template-columns: repeat(4,60px); grid-gap: 6px; justify-content: center;' }, itemIconList),
])),
Ui.div('lll_div_row', [
Ui.div('lll_div_card', [
Ui.div('lll_div_cardTitle', UiLocale.chestDrop.chestOpen.currentChest[language]),
currentDiv,
]),
Ui.div('lll_div_card', [
Ui.div('lll_div_cardTitle', UiLocale.chestDrop.chestOpen.history[language]),
historyDiv,
]),
]),
Ui.div('lll_div_row', [
Ui.elem('button', { className: 'Button_button__1Fe9z', style: 'margin: auto;', onclick: () => { this.openChestPopup.close(); } }, UiLocale.chestDrop.chestOpen.close[language]),
Ui.elem('button', { className: 'Button_button__1Fe9z', style: 'margin: auto;', onclick: () => { this.openChestPopup.close(); this.showPopup(openedItem, gainedItems) } }, UiLocale.chestDrop.chestOpen.details[language]),
]),
])
);
}
showOpenChestPopup(msg) {
const formatter = item => ({ hrid: item.itemHrid, count: item.count });
const openedItem = formatter(msg.openedItem);
const gainedItems = msg.gainedItems.map(formatter);
this.openChestPopup.setContent(this.constructOpenChestPopup(openedItem, gainedItems), UiLocale.chestDrop.chestOpen.openedLoot[language]);
this.openChestPopup.open();
}
handleOriginalPopup(node) {
let closeBtn = node.querySelector('div.Modal_background__2B88R');
closeBtn.click?.();
}
observeOriginalPopup() {
const observer = new MutationObserver((mutationsList, observer) => {
mutationsList.forEach(mutation => {
mutation.addedNodes.forEach(addedNode => {
// @ts-ignore
if (addedNode.classList && addedNode.classList.contains('Modal_modalContainer__3B80m')) {
this.handleOriginalPopup(addedNode);
observer.disconnect();
}
});
});
});
const rootNode = document.body;
const config = { childList: true, subtree: true };
observer.observe(rootNode, config);
}
onLootOpened(msg) {
if (Config.chestDrop.ui.useOriginalPopup) return;
this.observeOriginalPopup();
this.showOpenChestPopup(msg);
}
};
//#endregion
MessageHandler.handleMessageRecv(localStorage.getItem("initClientData"));
})();