// ==UserScript==
// @name VV XP (old)
// @namespace kire12
// @match https://myforge-old.vulcanforged.com/MyWallet/Lava*
// @grant none
// @license MIT
// @version 2.0.13
// @author -
// @description XP Dashboard for VulcanVerse and other VulcanForged games.
// @require https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js
// @require https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/moment.min.js
// ==/UserScript==
window.vv_vue = false
const vvxp_main = /* html */ `
<section class="p-2 p-md-3 p-lg-5 p-xxl-5">
<style>
#vvxp_main .hoverRow:hover {
background-color: #303030;
}
#vvxp_main button {
color: white;
}
#vvxp_main button:disabled {
color: gray;
pointer-events: initial;
cursor: not-allowed;
}
#vvxp_main input[type="number"] {
width: 50px;
}
.quadrant-heading {
margin-top: 0.5rem;
}
.landmark :not(.visited-landmark) {
color: white;
}
.bullet:before {
content: '• '
}
.visited-landmark {
text-decoration: line-through;
}
.total-border {
margin-top: 0.5rem;
font-weight: bold;
border-top: 2px solid;
}
.center {
text-align: center;
}
.right-padding {
padding-right: 0.25rem;
}
tr.tr-outline {
outline: thin solid;
}
tr.tr-padding > td, tr.tr-padding > th {
padding: 0.25rem;
}
.td-padding-right {
padding: 0.25rem;
}
.td-align-top {
vertical-align: top;
}
.bold {
font-weight: bold;
}
.red {
color: red;
}
</style>
<div id="vvxp_main" class="col-auto" v-cloak></div>
</section>
`
// LANDMARKS
Vue.component('vvxp-landmarks', {
template: /* html */ `
<div class="p-4 CurrentLavaStatus">
<div class="col text-uppercase">
<h4 class="GredientText">{{title}}</h4>
</div>
<table>
<tr>
<td class="center right-padding" :class="{'text-white': !quadrant_bonus}">{{quadrant_bonus ? '✅' : '✖'}}</td>
<td>Quadrant bonus (20 XP)</td>
</tr>
<tr>
<td class="center right-padding td-align-top" :class="{'text-white': !all_landmarks}">
<span v-if="all_landmarks">✅</span>
<span v-else>{{count_landmarks}} /</span>
</td>
<td>{{total_landmarks}} Landmarks visited (10 XP each)</td>
</tr>
</table>
<div v-for="quadrant in quadrants">
<div class="TruenoSemiBold Fsize_14 text-uppercase quadrant-heading">{{quadrant}}</div>
<div v-for="landmark in daily_landmarks.filter(landmark => landmark.quadrant == quadrant)" class='landmark bullet'>
<span :class="{'visited-landmark': landmark.visited}">{{landmark.names.slice(-1)[0]}}</span>
</div>
</div>
<!-- Troy (not yet) -->
<div class="total-border">{{total_xp}} XP total earned</div>
</div>
`,
props: {
entries: { type: Array }
},
computed: {
landmarkEntries() {
return this.entries.filter(entry => entry.detailMessage.includes(this.detailMessage))
},
quadrant_entry() {
return this.entries.filter(entry => entry.detailMessage.includes(this.quadrant_detailMessage))
},
quadrant_bonus() {
return !!this.quadrant_entry.length
},
count_landmarks() {
return this.landmarkEntries.length
},
total_landmarks() {
return this.landmarks.length
},
all_landmarks() {
return this.count_landmarks >= this.total_landmarks
},
quadrants() {
return uniqueArr(this.landmarks.map(landmark => landmark.quadrant))
},
daily_landmarks() {
return this.landmarks.map(landmark => {
landmark.visited = !!landmark.names.find(name => !!this.landmarkEntries.find(entry => entry.detailMessage.includes(`[Visit ${name}]`)))
return landmark
})
},
total_xp() {
return ssr(this.landmarkEntries) + ssr(this.quadrant_entry)
}
},
data() {
return {
title: 'Landmarks',
detailMessage: '[Visit ',
quadrant_detailMessage: '[Daily LANDMARK ON EACH QUADRANT]',
landmarks: [{
names: ['ENTRANCE TO THE MINOTAUR LABYRINTH', "Minotaur's Labyrinth"],
quadrant: 'Boreas'
}, {
names: ['HARPIES NEST', 'Harpies Nest'],
quadrant: 'Boreas'
}, {
names: ['FORTRESS OF THE WIND', 'Fortress of The Wind'],
quadrant: 'Boreas'
}, {
names: ['LAIR OF THE CYCLOPS', 'Lair of the Cyclops'],
quadrant: 'Boreas'
}, {
names: ['DEEP FOREST', 'Deep Forest'],
quadrant: 'Arcadia'
}, {
names: ['SUMMER PALACE', 'Summer Palace'],
quadrant: 'Arcadia'
}, {
names: ['DRUID SHRINE', 'Druid Shrine'],
quadrant: 'Arcadia'
}, {
names: ['WOODLANDS OF AMBROSIA', 'Woodlands of Ambrosia'],
quadrant: 'Arcadia'
}, {
names: ['WINERIES OF THE NECTAR OF THE GODS', 'Wineries of the Nectar of the Gods'],
quadrant: 'Arcadia'
}, {
names: ['SHRINE TO TETHIS', 'Shrine of Tethis'],
quadrant: 'Notus'
}, {
names: ['PYRAMID MAUSOLEUM 1', 'Pyramid Mausoleum A'],
quadrant: 'Notus'
}, {
names: ['PYRAMID MAUSOLEUM 3', 'Pyramid Mausoleum C'],
quadrant: 'Notus'
}, {
names: ['PYRAMID MAUSOLEUM 2', 'Pyramid Mausoleum B'],
quadrant: 'Notus'
}, {
names: ['PLANES OF THE HOLLOWING DARKNESS', 'Plains of the Howling Darkness'],
quadrant: 'Hades'
}, {
names: ['THE NECROPOLIS', 'Unknown Landmark', 'The Necropolis'],
quadrant: 'Hades'
}, {
names: ['PALACE OF THE DEAD', 'Palace of the Dead'],
quadrant: 'Hades'
}],
}
}
})
// FORAGES
Vue.component('vvxp-forages', {
template: /* html */ `
<div class="p-4 CurrentLavaStatus">
<div class="col text-uppercase">
<h4 class="GredientText">{{title}}</h4>
</div>
<table>
<tr>
<td class="center right-padding" :class="{'text-white': !quadrant_bonus}">{{quadrant_bonus ? '✅' : '✖'}}</td>
<td>Quadrant bonus (20 XP)</td>
</tr>
<tr>
<td class="center right-padding td-align-top" :class="{'text-white': quota_not_met}">
<span v-if="quota_bonus">{{quota_bonus}}</span>
<span v-if="quota_not_met">{{count_forages}} /</span>
</td>
<td>{{quota_goal}} Daily Forages (25 XP)</td>
</tr>
</table>
<br>
<table>
<tr>
<th class="center">Count</th>
<th class="center">Total</th>
<th>Forage Type</th>
<th>Drop %</th>
</tr>
<tr v-for="rarity in daily_forages">
<td class="center">{{rarity.count}}</td>
<td class="center">{{rarity.total_xp}} XP</td>
<td class="td-padding-right">{{rarity.type}} ({{rarity.xp}} XP)</td>
<td>{{rarity.percent}}%</td>
</tr>
<tr class="tr-outline tr-padding">
<td class="center">{{forage_totals.count}}</td>
<td class="center">{{forage_totals.total_xp}} XP</td>
<td>Average:<br><span class="bold">{{forage_totals.average}} XP/forage</span></td>
</tr>
</table>
<div class="total-border">{{total_xp}} XP total earned</div>
<!--
<div class="TruenoSemiBold Fsize_14 text-uppercase quadrant-heading">Weekly Average</div>
-->
</div>
`,
props: {
entries: { type: Array }
},
computed: {
forageEntries() {
return this.entries.filter(entry => entry.detailMessage.includes(this.detailMessage))
},
quadrant_entry() {
return this.entries.filter(entry => entry.detailMessage.includes(this.quadrant_detailMessage))
},
quadrant_bonus() {
return !!this.quadrant_entry.length
},
quota_entry() {
return this.entries.filter(entry => entry.detailMessage.includes(this.quota_detailMessage))
},
quota_bonus() {
const count = this.quota_entry.length
if (count == 0) return false
else if (count == 1) return '✅'
else if (count > 1) return `✅x${count}` // possible bug where "Daily 10 FORAGES" happens twice in a day
// return !!this.quota_entry.length
},
count_forages() {
return this.forageEntries.length
},
quota_not_met() {
return this.count_forages < this.quota_goal
},
daily_forages() {
return this.rarities.map(rarity => {
rarity.count = this.forageEntries.filter(forage => forage.amount == rarity.xp).length
rarity.total_xp = rarity.count * rarity.xp
rarity.percent = round((rarity.count / this.forageEntries.length) * 100, 2)
return rarity
})
},
forage_totals() {
const count = ssr(this.daily_forages, 'count')
const total_xp = ssr(this.daily_forages, 'total_xp')
const average = round((total_xp / count), 2)
return {
count,
total_xp,
average
}
},
total_xp() {
return ssr(this.forageEntries) + ssr(this.quadrant_entry) + ssr(this.quota_entry)
}
},
data() {
return {
title: 'Forages',
detailMessage: '[Foraging]',
quadrant_detailMessage: '[Daily FORAGE ON EACH QUADRANT]',
quota_detailMessage: '[Daily 10 FORAGES]',
quota_goal: 10,
rarities: [{
type: 'Common',
xp: 1
}, {
type: 'Rare',
xp: 2
}, {
type: 'Epic',
xp: 3
}, {
type: 'Mythic',
xp: 4
}, {
type: 'Legendary',
xp: 5
}],
}
}
})
// BATTLES
Vue.component('vvxp-battles', {
template: /* html */ `
<div class="p-4 CurrentLavaStatus">
<div class="col text-uppercase">
<h4 class="GredientText">{{title}}</h4>
</div>
<table>
<tr>
<td class="center right-padding" :class="{'text-white': !quadrant_bonus}">{{quadrant_bonus ? '✅' : '✖'}}</td>
<td>Quadrant bonus (20 XP)</td>
</tr>
<tr>
<td class="center right-padding td-align-top" :class="{'text-white': !quota_met}">
<span v-if="quota_met">✅</span>
<span v-else>{{count_battles}} /</span>
</td>
<td>{{quota_goal}} Daily Battles (50 XP)</td>
</tr>
</table>
<br>
<table>
<tr class="tr-padding">
<th class="center">Count</th>
<th class="center">Total</th>
<th></th>
</tr>
<tr class="tr-outline tr-padding">
<td class="center">{{count_battles}}</td>
<td class="center">{{battle_total_xp}} XP</td>
<td>Battles (1 XP)</td>
</tr>
</table>
<div class="total-border">{{total_xp}} XP total earned</div>
</div>
`,
props: {
entries: { type: Array }
},
computed: {
battleEntries() {
return this.entries.filter(entry => entry.detailMessage.includes(this.detailMessage))
},
quadrant_entry() {
return this.entries.filter(entry => entry.detailMessage.includes(this.quadrant_detailMessage))
},
quadrant_bonus() {
return !!this.quadrant_entry.length
},
quota_entry() {
return this.entries.filter(entry => entry.detailMessage.includes(this.quota_detailMessage))
},
quota_bonus() {
return !!this.quota_entry.length
},
count_battles() {
return this.battleEntries.length
},
quota_met() {
return this.count_battles >= this.quota_goal
},
battle_total_xp() {
return ssr(this.battleEntries)
},
total_xp() {
return this.battle_total_xp + ssr(this.quadrant_entry) + ssr(this.quota_entry)
}
},
data() {
return {
title: 'Battles',
detailMessage: '[Fight Won]',
quadrant_detailMessage: '[Daily FIGHT ON EACH QUADRANT]',
quota_detailMessage: '[Daily 10 FIGHT WINS]',
quota_goal: 10,
}
}
})
// QUESTS
Vue.component('vvxp-quests', {
template: /* html */ `
<div class="p-4 CurrentLavaStatus">
<div class="col text-uppercase">
<h4 class="GredientText">{{title}}</h4>
</div>
<table>
<tr class="tr-padding">
<th class="center">Count</th>
<th class="center">Total</th>
<th></th>
</tr>
<tr class="tr-outline tr-padding">
<td class="center">{{count_quests}}</td>
<td class="center">{{total_xp_day}} XP</td>
<td>Quests (XP varies)</td>
</tr>
</table>
<div class="total-border">{{total_xp_day}} XP total earned (on {{active_date}})</div>
<br>
<div class="TruenoSemiBold Fsize_14 text-uppercase quadrant-heading">Weekly</div>
<!--
<div v-for="quest in weekly_quests" class='landmark bullet'>
<span :class="{'visited-landmark': quest.completed}">{{quest.name}}</span>
</div>
-->
<table>
<tr class="tr-padding">
<th class="center">Done</th>
<!--<th class="center">Total</th>-->
<th>Quest</th>
<th>Reward</th>
</tr>
<tr v-for="quest in weekly_quests" class="tr-padding hoverRow" :title="'Objective: ' + quest.objective">
<td class="center">{{quest.completed}}</td>
<!--<td class="center">{{quest.total_xp}} XP</td>-->
<td>{{quest.name}} ({{quest.xp}} XP)</td>
<td>{{quest.reward}}</td>
</tr>
<!--
<tr class="tr-outline tr-padding">
<td class="center">{{forage_totals.count}}</td>
<td class="center">{{forage_totals.total_xp}} XP</td>
<td>Average:<br><span class="bold">{{forage_totals.average}} XP/forage</span></td>
</tr>
-->
</table>
<div class="total-border">{{total_xp_week}} XP total earned (from {{weeklyQuestsDate.start}} thru {{active_date}})</div>
<div class="red" v-if="weeklyQuestsDate.notFullyLoaded">Week's entries not fully loaded.<br>Increase: "Number of weeks to load."</div>
</div>
`,
props: {
entries: { type: Array },
weeklyEntries: { type: Array },
active_date: { type: String },
xWeeksAgo: { type: String },
},
computed: {
questEntries() {
return this.entries.filter(entry => entry.detailMessage.includes(this.detailMessage))
},
count_quests() {
return this.questEntries.length
},
total_xp_day() {
return ssr(this.questEntries)
},
prevMonday() {
return moment(this.active_date).isoWeekday(1).format('YYYY-MM-DD')
},
entriesSinceMonday() {
return this.weeklyEntries.filter(entry => {
const entry_date = entry.logDate.split('T')[0]
return !moment(entry_date).isBefore(this.prevMonday) && !moment(entry_date).isAfter(this.active_date)
})
},
weeklyQuestEntries() {
return this.entriesSinceMonday.filter(entry => entry.detailMessage.includes(this.detailMessage))
},
weekly_quests() {
return this.quests.map(quest => {
const match = this.weeklyQuestEntries.filter(entry => entry.detailMessage.includes(`${quest.name}]`))//.includes(`[Quest - ${quest.name}]`))
quest.count = match.length
quest.total_xp = ssr(match)
quest.completed = !!quest.count ? (quest.count == 1 ? '✅' : `✅x${quest.count}`) : '✖' //!!quest.count //!!this.weeklyQuestEntries.find(entry => entry.detailMessage.includes(`[Quest ${quest.name}]`))
return quest
})
},
total_xp_week() {
return ssr(this.weekly_quests, "total_xp")
},
weeklyQuestsDate() {
const check = moment(this.xWeeksAgo).isAfter(this.prevMonday)
return {
start: check ? this.xWeeksAgo : this.prevMonday,
notFullyLoaded: check
}
},
},
data() {
return {
title: 'Quests',
detailMessage: '[Quest ',
quests: [
{
"name": "Buried Treasure",
"xp": 5,
"reward": "Terracotta x20",
"quest_giver": "",
"quest_giver_location": "",
"objective": "Forage 5 unique plots"
},
{
"name": "Bleed for the Dead",
"xp": 5,
"reward": "Soil x25",
"quest_giver": "",
"quest_giver_location": "",
"objective": "Take 2500 damage in fight"
},
{
"name": "Fly the Flag",
"xp": 25,
"reward": "Crystal x1",
"quest_giver": "",
"quest_giver_location": "",
"objective": "15 forages"
},
{
"name": "A Thirst for Blood",
"xp": 10,
"reward": "Terracotta x25",
"quest_giver": "",
"quest_giver_location": "Arcadia",
"objective": "Win 5 fights"
},
{
"name": "A New Robe",
"xp": 10,
"reward": "Leather x10",
"quest_giver": "",
"quest_giver_location": "Arcadia",
"objective": "Forage 5 Thread"
},
{
"name": "Failure is a Lesson Learned",
"xp": 25,
"reward": "Crystal x1",
"quest_giver": "",
"quest_giver_location": "",
"objective": "Lose 5 fights"
},
{
"name": "Deliver Riddles",
"xp": 10,
"reward": "Wood x25",
"quest_giver": "",
"quest_giver_location": "Arcadia? Notus?",
"objective": "Visit Hades, Arcadia, Boreas"
},
{
"name": "Map the World",
"xp": 15,
"reward": "Wood x10",
"quest_giver": "",
"quest_giver_location": "",
"objective": "Visit each quadrant and VC"
},
{
"name": "What lies beneath",
"xp": 10,
"reward": "Crystal x1",
"quest_giver": "",
"quest_giver_location": "",
"objective": "10 forages"
},
{
"name": "Baubles for a Queen",
"xp": 10,
"reward": "Soil x10",
"quest_giver": "",
"quest_giver_location": "Notus",
"objective": "Forage 3 Halite"
},
{
"name": "Punishment",
"xp": 15,
"reward": "Copper x10",
"quest_giver": "",
"quest_giver_location": "",
"objective": "Deal 1000 damage in fight"
},
{
"name": "Sightseeing",
"xp": 25,
"reward": "Crystal x1",
"quest_giver": "",
"quest_giver_location": "",
"objective": "Visit each quadrant and VC"
}
] // https://vulcanforgedco.medium.com/vulcanverse-roadmap-reveal-c35278813e33
}
}
})
// OTHER
Vue.component('vvxp-other', {
template: /* html */ `
<div class="p-4 CurrentLavaStatus">
<div class="col text-uppercase">
<h4 class="GredientText">{{title}}</h4>
</div>
<table>
<tr>
<td class="center right-padding" :class="{'text-white': !trades_bonus}">{{trades_bonus ? '✅' : '✖'}}</td>
<td>5 Trades (10 XP)</td>
</tr>
<tr>
<td class="center right-padding" :class="{'text-white': !activity_bonus}">{{activity_bonus ? '✅' : '✖'}}</td>
<td>1 hour activity (10 XP)</td>
</tr>
</table>
<div class="total-border">{{total_xp}} XP total earned</div>
</div>
`,
props: {
entries: { type: Array }
},
computed: {
trades_entries() {
return this.entries.filter(entry => entry.detailMessage.includes(this.trades_detailMessage))
},
trades_bonus() {
return !!this.trades_entries.length
},
trades_xp() {
return ssr(this.trades_entries)
},
activity_entries() {
return this.entries.filter(entry => entry.detailMessage.includes(this.activity_detailMessage))
},
activity_bonus() {
return !!this.activity_entries.length
},
activity_xp() {
return ssr(this.activity_entries)
},
total_xp() {
return this.trades_xp + this.activity_xp
}
},
data() {
return {
title: 'Other',
trades_detailMessage: '[Daily TRADES]',
activity_detailMessage: '[Daily Activity]',
}
}
})
// BERSERK
Vue.component('vvxp-berserk', {
template: /* html */ `
<div class="p-4 CurrentLavaStatus">
<div class="col text-uppercase">
<h4 class="GredientText">{{title}}</h4>
</div>
<table>
<tr class="tr-padding">
<th class="center">Count</th>
<th class="center">Total</th>
<th></th>
</tr>
<tr>
<td class="center">{{win_count}}</td>
<td class="center">{{win_total_xp}} XP</td>
<td>Win (10 XP)</td>
</tr>
<tr>
<td class="center">{{loss_count}}</td>
<td class="center">{{loss_total_xp}} XP</td>
<td>Loss (3 XP)</td>
</tr>
<tr class="tr-outline tr-padding">
<td class="center">{{total_count}}</td>
<td class="center">{{total_xp}} XP</td>
<td>Average:<br><span class="bold">{{match_average}} XP/match</span></td>
</tr>
</table>
<br>
<div>W/L ratio: {{match_ratio}}</div>
<div class="total-border">{{total_xp}} XP total earned</div>
</div>
`,
props: {
entries: { type: Array }
},
computed: {
berserkEntries() {
return this.entries.filter(entry => entry.message.includes(this.message))
},
win_entries() {
return this.berserkEntries.filter(entry => entry.detailMessage.includes(this.win_detailMessage))
},
loss_entries() {
return this.berserkEntries.filter(entry => entry.detailMessage.includes(this.loss_detailMessage))
},
win_count() {
return this.win_entries.length
},
loss_count() {
return this.loss_entries.length
},
win_total_xp() {
return ssr(this.win_entries)
},
loss_total_xp() {
return ssr(this.loss_entries)
},
total_count() {
return this.win_count + this.loss_count
},
total_xp() {
return this.win_total_xp + this.loss_total_xp
},
match_average() {
return round((this.total_xp / this.total_count), 2)
},
match_ratio() {
if (!this.win_count || !this.loss_count)
return `N/A`
else return round((this.win_count / this.loss_count), 2)
}
},
data() {
return {
title: 'Berserk',
message: 'Berserk',
win_detailMessage: 'Win',
loss_detailMessage: 'Loss'
}
}
})
// OVERVIEW
Vue.component('vvxp-overview', {
template: /* html */ `
<div class="p-4 CurrentLavaStatus">
<div class="col text-uppercase">
<h4 class="GredientText">{{title}}</h4>
</div>
<div v-if="!detailsAvailable">VulcanVerse XP Function details not available.</div>
<table v-if="detailsAvailable">
<thead style="font-weight: bold;">
<tr>
<td>Dailies</td>
<td style="text-align:center;">Done</td>
<td style="text-align:center; min-width: 50px;">Quota</td>
<td>Quad</td>
<td style="text-align:center; min-width: 50px;">Earned</td>
</tr>
</thead>
<tbody>
<tr v-for="daily in dailies" class="hoverRow" :title="titleMessage(daily.name)">
<td>{{daily.name}}</td>
<td style="text-align:center;">{{daily.count}}</td>
<td style="text-align:center;">{{daily.remaining_for_bonus}}</td>
<td style="text-align:center;">{{daily.quadrant_bonus ? '✅' : '✖'}}</td>
<td>{{daily.xp_total}}</td>
</tr>
<tr class="hoverRow">
<td>1hr Activity</td>
<td style="text-align:center;">{{daily_activity}}</td>
<td></td>
<td></td>
<td>{{daily_activity == "✅" ? "10 XP" : "0 XP"}}</td>
</tr>
<tr class="hoverRow">
<td>5 Trades</td>
<td style="text-align:center;">{{daily_trades}}</td>
<td></td>
<td></td>
<td>{{daily_trades == "✅" ? "10 XP" : "0 XP"}}</td>
</tr>
<tr v-if="vv_quests" :title="vv_quests.title_popup" class="hoverRow">
<td>Quests</td>
<td style="text-align:center;">{{vv_quests.done_count}}</td>
<td></td>
<td></td>
<td>{{vv_quests.xp_earned}}</td>
</tr>
</tbody>
</table>
<br>
<table>
<thead style="font-weight: bold;">
<tr>
<td style="width: 50px;">Totals</td>
<td>Function</td>
</tr>
</thead>
<tbody>
<tr v-for="item in xpCategoryTotalsByDate">
<td>{{item.total}}</td>
<td>{{item.category}}</td>
</tr>
</tbody>
</table>
<div style="font-weight: bold; border-top: 1px solid;">{{xpByDate}} XP total</div>
</div>
`,
props: {
entriesByDate: { type: Array }
},
computed: {
detailsAvailable() {
return !moment(this.active_date).isBefore(this.earliestDetailsDate)
},
daily_activity() { // 1 hour activity bonus
const activity_detail = '[Daily Activity]'
const activity_detail_match = this.entriesByDate.find(entry => entry.detailMessage.includes(activity_detail))
const activity_bonus = !!activity_detail_match ? '✅' : '✖'
return activity_bonus
},
daily_trades() { // 5 trades bonus
const trade_detail = '[Daily TRADES]'
const trade_detail_match = this.entriesByDate.find(entry => entry.detailMessage.includes(trade_detail))
const trade_bonus = !!trade_detail_match ? '✅' : '✖'
return trade_bonus
},
remaining_landmarks() {
const remaining = this.landmarks.map(landmark => {
const filter = this.entriesByDate.find(entry => {
return landmark.names.reduce((acc, name) => {
if (entry.detailMessage.includes(`[Visit ${name}]`))
return acc += 1
else
return acc
}, 0) > 0
})
if (!filter) return `[${landmark.quadrant}] ${landmark.names.slice(-1)[0]}` // show latest name
else return ''
}).filter(x => x != '')
if (remaining.length) {
remaining.unshift('Remaining:')
return remaining.join('\n')
}
else return 'Remaining: None'
},
foraged_rarities() {
const forages_arr = this.entriesByDate.filter(entry => entry.detailMessage.includes('[Foraging]'))
const forages = forages_arr.reduce((acc, entry) => {
const xp = entry.amount
if (xp == 1) acc.common += 1
if (xp == 2) acc.rare += 1
if (xp == 3) acc.epic += 1
if (xp == 4) acc.mythic += 1
if (xp == 5) acc.legendary += 1
return acc
}, {
common: 0,
rare: 0,
epic: 0,
mythic: 0,
legendary: 0
})
return `Forages by rarity:
${forages.common} - Common (1 XP)
${forages.rare} - Rare (2 XP)
${forages.epic} - Epic (3 XP)
${forages.mythic} - Mythic (4 XP)
${forages.legendary} - Legendary (5 XP)`
},
xpByDate() {
return this.entriesByDate.reduce((acc, entry) => acc += entry.amount, 0)
},
dailies() {
let dailies = [{
name: 'Landmarks',
bonus_requirement: 16,
detail_text: '[Visit ',
count: '-',
remaining_for_bonus: '-',
quadrant_detail: '[Daily LANDMARK ON EACH QUADRANT]',
quadrant_bonus: false,
xp_total: 0
}, {
name: 'Forages',
bonus_requirement: 10,
detail_text: '[Foraging]',
count: '-',
quota_detail: '[Daily 10 FORAGES]',
quadrant_detail: '[Daily FORAGE ON EACH QUADRANT]',
remaining_for_bonus: '-',
quadrant_bonus: false,
xp_total: 0
}, {
name: 'Battles',
bonus_requirement: 10,
detail_text: '[Fight Won]',
count: '-',
quota_detail: '[Daily 10 FIGHT WINS]',
quadrant_detail: '[Daily FIGHT ON EACH QUADRANT]',
remaining_for_bonus: '-',
quadrant_bonus: false,
xp_total: 0
}]
return dailies = dailies.map(daily => {
const detailMessageMatches = this.entriesByDate.filter(entry => entry.detailMessage.includes(daily.detail_text))
const count = detailMessageMatches.length
daily.count = count
const quota_detail = this.entriesByDate.find(entry => entry.detailMessage.includes(daily.quota_detail))
const quota_bonus = !!quota_detail
daily.remaining_for_bonus = (!quota_bonus && count < daily.bonus_requirement) ? daily.bonus_requirement - count + ' remaining' : '✅'
const quadrant_detail = this.entriesByDate.find(entry => entry.detailMessage.includes(daily.quadrant_detail))
daily.quadrant_bonus = !!quadrant_detail
daily.xp_total = detailMessageMatches.reduce((acc, entry) => acc + entry.amount, 0) + (quota_bonus ? quota_detail.amount : 0) + (daily.quadrant_bonus ? quadrant_detail.amount : 0) + ' XP'
return daily
})
},
xpCategoryTotalsByDate() {
const categories = [...new Set(this.entriesByDate.map(entry => entry.detailMessage.split(' [')[0]))]
let totals = categories.map(category => {
const total = this.entriesByDate.filter(entry => entry.detailMessage.split(' [')[0].includes(category))
.reduce((acc, entry) => acc += entry.amount, 0)
return {
category,
total
}
})
return totals
},
vv_quests() {
const detail_text = '[Quest - '
const questMatches = this.entriesByDate.filter(entry => entry.detailMessage.includes(detail_text))
if (!questMatches.length)
return false
const title_popup = `Quests completed:\n` + questMatches.map(entry => entry.detailMessage.replace(/^VulcanVerse \[Quest - /, '').replace(/\]$/, '').trim()).join('\n')
const done_count = questMatches.length
const xp_earned = questMatches.reduce((acc, entry) => acc + entry.amount, 0) + ' XP'
return {
title_popup,
done_count,
xp_earned,
}
}
},
data() {
return {
title: 'Overview',
earliestDetailsDate: '2022-03-05',
landmarks: [{
names: ['ENTRANCE TO THE MINOTAUR LABYRINTH', "Minotaur's Labyrinth"],
quadrant: 'Boreas'
}, {
names: ['HARPIES NEST', 'Harpies Nest'],
quadrant: 'Boreas'
}, {
names: ['FORTRESS OF THE WIND', 'Fortress of The Wind'],
quadrant: 'Boreas'
}, {
names: ['LAIR OF THE CYCLOPS', 'Lair of the Cyclops'],
quadrant: 'Boreas'
}, {
names: ['DEEP FOREST', 'Deep Forest'],
quadrant: 'Arcadia'
}, {
names: ['SUMMER PALACE', 'Summer Palace'],
quadrant: 'Arcadia'
}, {
names: ['DRUID SHRINE', 'Druid Shrine'],
quadrant: 'Arcadia'
}, {
names: ['WOODLANDS OF AMBROSIA', 'Woodlands of Ambrosia'],
quadrant: 'Arcadia'
}, {
names: ['WINERIES OF THE NECTAR OF THE GODS', 'Wineries of the Nectar of the Gods'],
quadrant: 'Arcadia'
}, {
names: ['SHRINE TO TETHIS', 'Shrine of Tethis'],
quadrant: 'Notus'
}, {
names: ['PYRAMID MAUSOLEUM 1', 'Pyramid Mausoleum A'],
quadrant: 'Notus'
}, {
names: ['PYRAMID MAUSOLEUM 3', 'Pyramid Mausoleum C'],
quadrant: 'Notus'
}, {
names: ['PYRAMID MAUSOLEUM 2', 'Pyramid Mausoleum B'],
quadrant: 'Notus'
}, {
names: ['PLANES OF THE HOLLOWING DARKNESS', 'Plains of the Howling Darkness'],
quadrant: 'Hades'
}, {
names: ['THE NECROPOLIS', 'Unknown Landmark', 'The Necropolis'],
quadrant: 'Hades'
}, {
names: ['PALACE OF THE DEAD', 'Palace of the Dead'],
quadrant: 'Hades'
}],
}
},
methods: {
titleMessage(daily_name) {
if (daily_name == 'Landmarks')
return this.remaining_landmarks
if (daily_name == 'Forages')
return this.foraged_rarities
return ''
},
}
})
window.vv_vue = new Vue({
el: '#vvxp_main',
template: /* html */ `
<div id="vvxp_main" class="col-auto" v-cloak>
<h1 class="my-3">XP Dashboard</h1>
<div class="row g-3 mb-4"">
<div class="col-lg-3">
<div class="p-4 CurrentLavaStatus">
<div>
<label><input class="" type="number" v-model.number="settings.numberOfWeeksToLoad" @keyup.enter="reload" :disabled="!earliestLoadedDate"> Number of weeks to load.</label>
<span v-if="reachedLastLoadedDate" class="red">← Increase and reload.</span>
</div>
<a class="btn BtnGradientOrange w-100 py-2 mt-auto" @click="reload">Reload</a>
<div>Time until next reset for dailies: {{timeRemaining}}</div>
<br>
<div class="center">
<button class="btn" @click="subtractDay" :disabled="reachedLastLoadedDate || !earliestLoadedDate">←</button>
<input class="" type="date" v-model="active_date" :max="today" :min="earliestLoadedDate" :disabled="!earliestLoadedDate">
<button class="btn" @click="addDay" :disabled="isToday || !earliestLoadedDate">→</button>
<h4>{{relativeDate}}</h4>
<div v-if="reachedLastLoadedDate" class="red">Last day of loaded entries. See above to load more.</div>
</div>
</div>
<br>
<vvxp-overview :entriesByDate="entriesByDate"></vvxp-overview>
<br>
<vvxp-other :entries="entriesByDate"></vvxp-other>
</div>
<div class="col-lg-3">
<vvxp-landmarks :entries="entriesByDate"></vvxp-landmarks>
</div>
<div class="col-lg-3">
<vvxp-forages :entries="entriesByDate"></vvxp-forages>
<br>
<vvxp-battles :entries="entriesByDate"></vvxp-battles>
</div>
<div class="col-lg-3">
<vvxp-quests :entries="entriesByDate" :weeklyEntries="entriesByWeek" :active_date="active_date" :xWeeksAgo="xWeeksAgo"></vvxp-quests>
</div>
</div>
<div class="row g-3 mb-4">
<div class="col-lg-3">
<vvxp-berserk :entries="entriesByDate"></vvxp-berserk>
<br>
<!-- RESOURCES -->
<div class="p-4 CurrentLavaStatus">
<div class="col text-uppercase">
<h4 class="GredientText">Resources</h4>
</div>
<div class="bullet"><a href="https://www.vulcanverselore.com/HouseOfRecords/index.php/Landmark_visit_rewards" target="_blank" rel="noopener noreferrer">Landmarks Wiki</a></div>
<div class="bullet"><a href="https://vv.vulcanforged.com/Vulcanites" target="_blank" rel="noopener noreferrer">Vulcanite Stats</a> (for foraging and battles)</div>
<div class="bullet">
<a href="https://onedrive.live.com/view.aspx?resid=451F8E01CD020113!105&ithint=file%2cxlsx&authkey=!AMyWuzs-AR00Yq8" target="_blank" rel="noopener noreferrer">Foraging Spreadsheet</a>
(<a href="https://discord.com/channels/759537056153337906/870998406200459274/962514701105913877" target="_blank" rel="noopener noreferrer">source</a>)
</div>
<div class="bullet"><a href="https://docs.vulcanforged.com/" target="_blank" rel="noopener noreferrer">Official VulcanForged documentation</a></div>
<br><div class="total-border"></div>
<br>Link to this userscript: <a href="https://greasyfork.org/en/scripts/441027-vv-xp/" target="_blank" rel="noopener noreferrer">VV XP</a>
<br>
<br>All feedback welcome!
<br>Share feature suggestions and report bugs in the
<br>VulcanForged Discord Channel: <a href="https://discord.com/channels/759537056153337906/974649458153373696" target="_blank" rel="noopener noreferrer">#community-developers</a>
<br>Tag the creator: <strong>@Kire12</strong>
<!-- NEWS -->
</div>
</div>
<div class="col-lg-9">
<div style="margin: 24px 0 15px;">
<label>Showing <input class="" type="number" v-model.number="settings.numberOfEntriesToShow" :max="loadedEntries.length" min="0"> / {{loadedEntries.length}} entries. (Showing many entries slows down the page.)</label>
</div>
<table v-show="settings.numberOfEntriesToShow > 0">
<thead style="font-weight: bold;">
<tr class="tr-padding">
<td style="width: 50px;">XP</td>
<td style="width: 150px;">Date-time</td>
<td style="width: 150px;">Local Date-time</td>
<td>Message</td>
<td>Detail Message</td>
</tr>
</thead>
<tbody>
<tr v-for="entry in shownEntries" class="hoverRow tr-padding">
<td>{{entry.amount}}</td>
<td>{{entry.logDate}}</td>
<td>{{convertTime(entry.logDate)}}</td>
<td>{{entry.message}}</td>
<td>{{entry.detailMessage}}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
`,
created() {
// console.log('created')
this.init()
},
mounted() {
this.reload()
},
data: {
today: moment.utc().toISOString().split('T')[0],
loadedEntries: [],
timeRemaining: 'N/A',
settings: {
numberOfWeeksToLoad: 1,
numberOfEntriesToShow: 15 // set this to zero to hide
},
active_date: moment.utc().toISOString().split('T')[0],
numberOfWeeksSetToLoad: 0,
earliestDate: '2021-07-07'
},
computed: {
// active_date2: { // non-working workaround for input date in different locale https://discord.com/channels/958025800509161472/958247945243852830/961359879841398825
// get() {
// const format = moment.localeData().longDateFormat('L')
// var a = moment(this.active_date, 'YYYY-MM-DD').format(format)
// console.log({ a })
// return a
// },
// set(newValue) {
// const format = moment.localeData().longDateFormat('L')
// const b = moment(newValue, format).format('YYYY-MM-DD')
// console.log({ b })
// this.active_date = b
// }
// },
entriesByDate() {
return this.loadedEntries.filter(entry => entry.logDate.split('T')[0] == this.active_date)
},
entriesByWeek() {
return this.loadedEntries.filter(entry => {
const entry_date = entry.logDate.split('T')[0]
const oneWeekPrev = moment(this.active_date).subtract(1, 'weeks').format('YYYY-MM-DD')
return moment(entry_date).isAfter(oneWeekPrev) && !moment(entry_date).isAfter(this.active_date)
})
},
earliestLoadedDate() {
if (!this.loadedEntries.length) return false
return this.loadedEntries.slice(-1)[0].logDate.split('T')[0]
},
reachedLastLoadedDate() {
// return false
if (//this.loadedEntries.length == this.numberOfEntriesSetToLoad && // if < then may be first day of account
!moment(this.active_date).isAfter(this.earliestLoadedDate) &&
this.earliestLoadedDate)
return true
else return false
}, // set a watcher on this, then increase as needed
isToday() {
return !moment(this.active_date).isBefore(this.today)
},
relativeDate() {
if (!this.loadedEntries.length) return 'Loading...'
const diff = moment(this.today).diff(moment(this.active_date), 'days')
if (diff == 0) {
return 'Today'
} else if (diff == 1) {
return `1 day ago`
} else {
return `${diff} days ago`
}
},
shownEntries() {
return this.loadedEntries.slice(0, Math.min(this.settings.numberOfEntriesToShow, this.loadedEntries.length))
},
xWeeksAgo() {
// const days = Math.round(this.settings.numberOfWeeksToLoad * 7)
// return moment(this.today).subtract(days, 'days').format('YYYY-MM-DD')
const weeksAgo = moment(this.today).subtract(this.settings.numberOfWeeksToLoad, 'weeks').format('YYYY-MM-DD')
return moment(weeksAgo).isAfter(this.earliestDate) ? weeksAgo : this.earliestDate
},
},
methods: {
init() {
$('#Earnings').before(vvxp_main)
// console.log('mounted')
this.startTimer()
},
click() {
alert('clicked')
},
addDay() {
this.active_date = moment(this.active_date).add(1, 'days').format('YYYY-MM-DD')
},
subtractDay() {
this.active_date = moment(this.active_date).subtract(1, 'days').format('YYYY-MM-DD')
},
reload() {
$.ajax({
type: "POST",
url: "/MyWallet/Get_Lava_Earned_Spent",
cache: false,
data: {
type: 'Credit',
section: 'XP',
pageNo: 0,
pageSize: 999999999,
start: `${this.xWeeksAgo} 00:00:00.0000000`,
end: `${this.today} 23:59:59.9999999`,
__RequestVerificationToken: GetAntiForgeyToken()
},
// type=Credit§ion=XP&pageNo=0&pageSize=10&start=2022-05-26+00%3A00%3A00.0000000&end=2022-05-27+23%3A59%3A59.9999999
success: function (Response) {
if (Response.status == 1) {
// console.log(Response.data)
vv_vue.loadedEntries = Response.data
vv_vue.numberOfWeeksSetToLoad = vv_vue.settings.numberOfWeeksToLoad
vv_vue.today = moment.utc().toISOString().split('T')[0]
}
else {
console.log("Error !!!", Response.message, "error")
}
}
})
},
startTimer() {
setInterval(() => {
this.timeRemaining = moment.utc().add(1, 'days').startOf('day').subtract(moment()).format('H:mm:ss')
}, 1000)
},
convertTime(date) {
return moment.utc(date).local().format('YYYY-MM-DD HH:mm:ss');
},
filterEntriesByDate(text) {
return this.entriesByDate.filter(entry => entry.detailMessage.includes(text))
}
}
})
// TODO
// - use localStorage to save settings of entries to load/show.
// - Make number inputs scrollable/clickable to increase/decrease.
// - Show breakdown of lava earnings per day (based on type automatically, including landmark run and earning share)
// - catch all for VulcanVerse functions that are not supported
// show entries of selected day, instead of all (or add as option)
// hide panels until xp list is loaded, to prevent prematurely showing incomplete or NAN
// TODO Ideas: Include how much XP every bonus is worth. Table for XP earned daily for past month, with average. Donation address
// - save settings in local stroage
// - daily, weekly, and full histroical (of what's loaded) views.
// full historical will be a table going down, with averages of forages, etc. Columns for each panel category.
// Tsukasa suggests hourly XP totals because lava distributed every hour at X:56. I could add a column to the right of xp entries full list, which matches lava income with xp by hour, and how much lava per xp or hour.
// fjzdm - hourly lava reset timer
// mark quest reset time too (monday)
// use built-in calendar for start/end dates
// 2022-07-02 Wow, today I noticed that Xp and Lava tables have a calendar filter, and I can fetch by start/end date in the API! This will make things so much easier.
// Before, I thought I would have to programtically guess-and-check to get all entries from a week. This will make things a breeze, and will clean up the interface more!
function uniqueArr(arr) {
return [...new Set(arr)]
}
// ssr = simple sum reduce
function ssr(arr, field = 'amount') {
if (!arr) return 0
return arr.reduce((acc, entry) => acc += entry[field], 0)
}
function round(value, precision) {
if (isNaN(value)) value = 0
var multiplier = Math.pow(10, precision || 0)
const num = Math.round(value * multiplier) / multiplier
return num//.toFixed(precision)
}