Skip to content

Commit

Permalink
Export xlsx (#62)
Browse files Browse the repository at this point in the history
* feat: first draft of instance export

* fix: better filename

* fix: some cleanup

* feat: more data exported and fixed some minor things

* chore: final cleanup
  • Loading branch information
slazor authored Mar 26, 2022

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
1 parent f086e51 commit 55676d1
Showing 8 changed files with 368 additions and 4 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -30,6 +30,7 @@
"electron-updater": "^4.6.1",
"electron-util": "^0.17.2",
"events": "^3.3.0",
"node-xlsx": "^0.21.0",
"prop-types": "^15.7.2",
"react": "^17.0.2",
"react-dom": "^17.0.2",
29 changes: 29 additions & 0 deletions src/app.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
'use strict';

const { app, BrowserWindow, Menu, ipcMain, dialog, globalShortcut, shell } = require('electron');
const fs = require('fs');
const path = require('path');
const { v4: uuidv4 } = require('uuid');
const { is } = require('electron-util');
const unhandled = require('electron-unhandled');
const debug = require('electron-debug');
const contextMenu = require('electron-context-menu');
const xlsx = require('node-xlsx').default;

const config = require('./main/config');
const menu = require('./main/menu');
const checkForUpdates = require('./main/updater');
const Session = require('./main/session');
const LogReader = require('./main/log-reader');
const { exportXls } = require('./main/exporter');

let mainWindow;
let overlayWindow;
@@ -473,6 +476,31 @@ ipcMain.on('change-hunting-set', (_event, selectedHuntingSet) => {
setSelectedHuntingSet(selectedHuntingSet);
});

ipcMain.handle('export-instance', async (_event, { sessionId, instanceId }) => {
const exportSession = await Session.Load(sessionId, instanceId);
const exportData = exportSession.getData();
const exportSheets = await exportXls(exportData);

const options = {
title: 'Save file',
defaultPath: `entropia_tally_run_${exportData.instanceCreatedAt.replaceAll(' ', '_').replaceAll(':', '')}.xlsx`,
buttonLabel: 'Save',

filters: [
{name: 'Excel (xlsx)', extensions: ['xlsx']},
{name: 'All Files', extensions: ['*']},
],
};

return dialog.showSaveDialog(null, options)
.then(({ filePath }) => {
const buffer = xlsx.build(exportSheets);
fs.writeFileSync(filePath, buffer, 'utf-8');
})
.then(() => true)
.catch(() => false);
});

// Ipc Events with response

ipcMain.handle('select-logfile', async () => {
@@ -647,6 +675,7 @@ ipcMain.handle('delete', async (_event, { type, id }) => {

(async () => {
await app.whenReady();

session = await Session.Create();
session.setHuntingSet(activeHuntingSet);
session.emitter.on('session-updated', sessionForcedUpdate);
222 changes: 222 additions & 0 deletions src/main/exporter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
function makeNumber(value) {
return value || 0;
}

// eslint-disable-next-line complexity
async function exportXls(sessionData) {
const aggregated = sessionData.aggregated || {};
const events = sessionData.events || {};
const sheets = [];

const totalLootValue = makeNumber(aggregated.allLoot?.total);
const killCount = makeNumber(aggregated.lootEvent?.count);
const damageInflictedTotal = makeNumber(aggregated.damageInflicted?.total);
const damageInflictedCount = makeNumber(aggregated.damageInflicted?.count);
const damageInflictedCritTotal = makeNumber(aggregated.damageInflictedCrit?.total);
const damageInflictedCritCount = makeNumber(aggregated.damageInflictedCrit?.count);
const damageTakenTotal = makeNumber(aggregated.damageTaken?.total);
const damageTakenCount = makeNumber(aggregated.damageTaken?.count);
const damageTakenCritCount = makeNumber(aggregated.damageTakenCrit?.count);
const playerDeflectCount = makeNumber(aggregated.playerDeflect?.count);
const enemyMissCountValue = makeNumber(aggregated.enemyMiss?.count);

const playerMissedCount = makeNumber(aggregated.playerMiss?.count) + makeNumber(aggregated.enemyDodge?.count) + makeNumber(aggregated.enemyEvade?.count) + makeNumber(aggregated.enemyJam?.count); // Shots fired but missed
const playerAttackCount = damageInflictedCount + playerMissedCount; // Player attacks attempted
const playerEvadeCount = makeNumber(aggregated.playerEvade?.count) + makeNumber(aggregated.playerDodge?.count); // Enemy attacks avoided
const enemyAttackCount = damageTakenCount + playerDeflectCount + enemyMissCountValue + playerEvadeCount; // Enemy attacks attempted
const enemyHitCount = damageTakenCount + playerDeflectCount; // Enemy attacks that hit incl. player deflections
const enemyMissCount = enemyAttackCount - enemyHitCount; // Enemy attacks that missed

const playerAttackHitRate = makeNumber(damageInflictedCount / playerAttackCount) * 100;
const playerAttackCritRate = makeNumber(damageInflictedCritCount / damageInflictedCount) * 100;

let general = [];
general.push(
['Session name', sessionData.sessionName],
['Run created at', sessionData.instanceCreatedAt],
['Run time (seconds)', sessionData.sessionTime],
['Total looted (PED)', totalLootValue],
['Kills/Loot events', killCount],
[''],
['Offensive stats'],
['Damage inflicted', damageInflictedTotal],
['Hits', damageInflictedCount],
['Misses', playerMissedCount],
['Critical hits', damageInflictedCritCount],
['Critical damage inflicted', damageInflictedCritTotal],
['Total attacks', playerAttackCount],
['Hit rate', `${playerAttackHitRate.toFixed(4)}%`],
['Critical hit rate', `${playerAttackCritRate.toFixed(4)}%`],
[''],
['Defensive stats'],
['Damage received', damageTakenTotal],
['Hits received', damageTakenCount],
['Critical hits received', damageTakenCritCount],
['Attacks evaded', playerEvadeCount],
[''],
['Enemy stats'],
['Enemy attacks', enemyAttackCount],
['Enemy hit count', enemyHitCount],
['Enemy miss count', enemyMissCount],
);

if (sessionData.usedHuntingSets) {
const huntingSets = [];
const aggHuntingSetDmg = aggregated.huntingSetDmg || {};
const aggHuntingSetMissed = aggregated.huntingSetMissed || {};
for (const key of Object.keys(sessionData.usedHuntingSets)) {
const huntingSet = sessionData.usedHuntingSets[key];
const setDmgValue = makeNumber(aggHuntingSetDmg[key]?.total);
const setDmgCount = makeNumber(aggHuntingSetDmg[key]?.count);
const setMisses = makeNumber(aggHuntingSetMissed[key]?.count);
const setShotCount = setDmgCount + setMisses;
if (setShotCount > 0) {
huntingSets.push([huntingSet.name, Number(huntingSet.decay), Number(setDmgValue), Number(setShotCount), Number(setMisses)]);
}
}

if (huntingSets.length > 0) {
general.push(
[''],
['Used hunting sets', 'Decay', 'Dmg inflicted', 'Shots fired', 'Missed shots'],
);
general = general.concat(huntingSets);
}
}

sheets.push({ name: 'General', data: general, options: {'!cols': [{wch: 30}, {wch: 30}, {wch: 20}, {wch: 20}, {wch: 20}]} });

if (events.globals) {
const globals = [];
const aggGlobals = aggregated.globals;
globals.push(
['Global (count)', makeNumber(aggGlobals.count)],
['Global (total value)', makeNumber(aggGlobals.total)],
['Global (avg size)', makeNumber(aggGlobals.avg)],
[''],
['Date', 'Value', 'Source'],
);
for (const global of sessionData.events.globals) {
globals.push([global.date, Number(global.value), global.enemy]);
}

sheets.push({ name: 'Globals', data: globals, options: {'!cols': [{wch: 19}, {wch: 10}, {wch: 30}]} });
}

if (events.hofs) {
const hofs = [];
const aggHofs = aggregated.hofs;
hofs.push(
['HoF (count)', makeNumber(aggHofs.count)],
['HoF (total value)', makeNumber(aggHofs.total)],
['HoF (avg size)', makeNumber(aggHofs.avg)],
[''],
['Date', 'Value', 'Source'],
);
for (const hof of events.hofs) {
hofs.push([hof.date, Number(hof.value), hof.enemy]);
}

sheets.push({ name: 'Hofs', data: hofs, options: {'!cols': [{wch: 19}, {wch: 10}, {wch: 30}]} });
}

if (events.rareLoot) {
const rareLoots = [['Date', 'Value', 'Item']];
for (const rareLoot of events.rareLoot) {
rareLoots.push([rareLoot.date, Number(rareLoot.value), rareLoot.item]);
}

sheets.push({ name: 'Rare loot', data: rareLoots, options: {'!cols': [{wch: 19}, {wch: 10}, {wch: 30}]} });
}

if (events.loot) {
const loots = [['Date', 'Amount', 'Value', 'Loot item']];
for (const loot of events.loot) {
loots.push([loot.date, Number(loot.amount), Number(loot.value), loot.name]);
}

sheets.push({ name: 'Loot', data: loots, options: {'!cols': [{wch: 19}, {wch: 10}, {wch: 10}, {wch: 30}]} });

const aggLoot = [['Item', 'Amount', 'Value']];
for (const key in aggregated.loot) {
aggLoot.push([key, Number(aggregated.loot[key].count), Number(aggregated.loot[key].total)]);
}

sheets.push({ name: 'Loot (aggregated)', data: aggLoot, options: {'!cols': [{wch: 35}, {wch: 15}, {wch: 10}]} });
}

if (events.lootEvent) {
const lootEvents = [['Date', 'Items']];
for (const lootEvent of events.lootEvent) {
const lootEventItems = lootEvent.items.map(item => `${item.name} (${item.amount})`);
lootEvents.push([lootEvent.date, lootEventItems.join(', ')]);
}

sheets.push({ name: 'Loot Events', data: lootEvents, options: {'!cols': [{wch: 19}, {wch: 100}]} });
}

if (events.skills) {
const skills = [['Date', 'Value', 'Skill']];
for (const skill of events.skills) {
skills.push([skill.date, Number(skill.value), skill.name]);
}

sheets.push({ name: 'Skills', data: skills, options: {'!cols': [{wch: 19}, {wch: 10}, {wch: 30}]} });

const aggSkills = [['Skill', 'Times gained', 'Total']];
for (const key in aggregated.skills) {
aggSkills.push([key, Number(aggregated.skills[key].count), Number(aggregated.skills[key].total)]);
}

sheets.push({ name: 'Skills (aggregated)', data: aggSkills, options: {'!cols': [{wch: 35}, {wch: 15}, {wch: 10}]} });
}

if (events.attributes) {
const attributes = [['Date', 'Value', 'Skill']];
for (const attribute of events.attributes) {
attributes.push([attribute.date, Number(attribute.value), attribute.name]);
}

sheets.push({ name: 'Attributes', data: attributes, options: {'!cols': [{wch: 19}, {wch: 10}, {wch: 30}]} });

const aggAttributes = [['Attribute', 'Times gained', 'Total']];
for (const key in aggregated.attributes) {
aggAttributes.push([key, Number(aggregated.attributes[key].count), Number(aggregated.attributes[key].total)]);
}

sheets.push({ name: 'Attributes (aggregated)', data: aggAttributes, options: {'!cols': [{wch: 35}, {wch: 15}, {wch: 10}]} });
}

if (events.tierUp) {
const tierUps = [['Date', 'Tier', 'Item']];
for (const tierUp of events.tierUp) {
tierUps.push([tierUp.date, Number(tierUp.tier), tierUp.item]);
}

sheets.push({ name: 'Tier up', data: tierUps, options: {'!cols': [{wch: 19}, {wch: 10}, {wch: 30}]} });
}

if (events.enhancerBreak) {
const enhancerBreaks = [['Date', 'Value', 'Remaining', 'Enhancer', 'Source']];
for (const enhancerBreak of events.enhancerBreak) {
enhancerBreaks.push([enhancerBreak.date, Number(enhancerBreak.value), Number(enhancerBreak.remaining), enhancerBreak.name, enhancerBreak.item]);
}

sheets.push({ name: 'Enhancer breaks', data: enhancerBreaks, options: {'!cols': [{wch: 19}, {wch: 10}, {wch: 10}, {wch: 30}, {wch: 30}]} });
}

if (aggregated.heal) {
const heals = [['Player', 'Total healed (hp)']];
for (const player of Object.keys(aggregated.heal)) {
heals.push([player, Number(aggregated.heal[player].total)]);
}

sheets.push({ name: 'Healing (aggregated)', data: heals, options: {'!cols': [{wch: 35}, {wch: 20}]} });
}

return sheets;
}

module.exports = {
exportXls,
};
3 changes: 3 additions & 0 deletions src/main/preload.js
Original file line number Diff line number Diff line change
@@ -48,6 +48,9 @@ contextBridge.exposeInMainWorld(
selectLogFile() {
return ipcRenderer.invoke('select-logfile');
},
exportInstance(sessionId, instanceId) {
return ipcRenderer.invoke('export-instance', { sessionId, instanceId });
},
removeListener: (eventName, callback) => {
ipcRenderer.removeListener(eventName, callback);
},
1 change: 1 addition & 0 deletions src/main/session.js
Original file line number Diff line number Diff line change
@@ -445,6 +445,7 @@ class Session {
instanceId: this.instanceId,
sessionName: this.name,
sessionCreatedAt: this.createdAt,
instanceCreatedAt: this.instanceCreatedAt,
usedHuntingSets: this.config.usedHuntingSets,
additionalCost: this.config.additionalCost,
notes: this.notes,
15 changes: 12 additions & 3 deletions src/ui/components/modals/history-modal.jsx
Original file line number Diff line number Diff line change
@@ -84,6 +84,14 @@ const HistoryModal = ({ session, sessions, isOpen, closeModal }) => {
setIsDeleteModalOpen(true);
};

const exportInstance = (sessionId, instanceId) => {
window.api.exportInstance(sessionId, instanceId).then(success => {
if (!success) {
console.error('FAILED TO EXPORT');
}
});
};

const moveModalOpen = instanceId => {
setInstanceMoveSource(instanceId);
setInstanceMoveTarget(null);
@@ -104,8 +112,8 @@ const HistoryModal = ({ session, sessions, isOpen, closeModal }) => {
);

const modifiedSessionInstances = sessionInstances.map(instance => {
if (instance.notes && instance.notes.length > 24) {
instance.notes = instance.notes.slice(0, 24) + '...';
if (instance.notes && instance.notes.length > 36) {
instance.notes = instance.notes.slice(0, 36) + '...';
}

return instance;
@@ -195,10 +203,11 @@ const HistoryModal = ({ session, sessions, isOpen, closeModal }) => {
{modifiedSessionInstances.map(instance => (
<tr key={instance.id}>
<td className="halfwidth">{formatLocalTime(instance.created_at)}</td>
<td className="halfwidth">{instance.notes}</td>
<td className="halfwidth">{instance.notes ? instance.notes : '-'}</td>
<td className="has-text-right">
<a className="table-action" onClick={() => onLoadSessionInstance(instance.session_id, instance.id)}>Load</a>
<a className="table-action has-text-info" onClick={() => moveModalOpen(instance.id)}>Move</a>
<a className="table-action has-text-warning-dark" onClick={() => exportInstance(instance.session_id, instance.id)}>Export</a>
<a className="table-action has-text-danger" onClick={() => openDeleteModal('instance', instance.id)}>Delete</a>
</td>
</tr>
2 changes: 1 addition & 1 deletion src/ui/pages/hunting/views/stats-view.jsx
Original file line number Diff line number Diff line change
@@ -50,7 +50,7 @@ const StatsView = ({ avatarName, isKillCountEnabled }) => {
const enemyMissCount = enemyAttackCount - enemyHitCount; // Enemy attacks that missed

const playerAttackHitRate = makeNumber(damageInflictedCount / playerAttackCount) * 100;
const playerAttackCritRate = makeNumber(makeNumber(aggregated?.damageInflictedCrit?.count) / damageInflictedCount) * 100;
const playerAttackCritRate = makeNumber(damageInflictedCritCount / damageInflictedCount) * 100;
const enemyAttackHitCritRate = makeNumber(damageTakenCritCount / enemyHitCount) * 100;
const enemyAttackMissRate = makeNumber((playerEvadeCount + enemyMissCountValue) / enemyAttackCount) * 100;

Loading

0 comments on commit 55676d1

Please sign in to comment.