First commit
0
.gbf-menu-hover
Normal file
340
Replicard.vue
Normal file
@@ -0,0 +1,340 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<h1 class="self-center mb-8">Replicard Sandbox Maps</h1>
|
||||||
|
|
||||||
|
<div class="flex flex-row flex-wrap mb-4 items-center gap-4">
|
||||||
|
<button class="btn" :class="show_help ? 'btn-blue' : 'btn-white'" @click="show_help = ! show_help">
|
||||||
|
<fa-icon :icon="['fas', 'info-circle']" class="text-xl"></fa-icon> Usage
|
||||||
|
</button>
|
||||||
|
<checkbox v-model="show_loot">Show loot</checkbox>
|
||||||
|
<checkbox v-model="show_progression">Show progression</checkbox>
|
||||||
|
<button class="btn btn-blue" v-if="show_progression && isUserLogged" @click="save()">
|
||||||
|
<fa-icon :icon="['fas', 'save']" class="text-xl"></fa-icon> Save progression
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Usage -->
|
||||||
|
<div class="bg-secondary self-center rounded p-4 mb-2" v-if="show_help">
|
||||||
|
<h2>Number of fights</h2>
|
||||||
|
<p class="pb-4">
|
||||||
|
The number above each circle represents the number of fights needed to guarantee the appearance of a Defender.
|
||||||
|
</p>
|
||||||
|
<h2>To fight the Boss</h2>
|
||||||
|
<p class="pb-4">
|
||||||
|
To fight the Boss of each zone, you need components dropped by Defenders.<br>
|
||||||
|
Chain fights of a node until the Defender appears.<br>
|
||||||
|
Each component is indicated in parentheses after the color of its Defender.
|
||||||
|
</p>
|
||||||
|
<h2>Track your progression</h2>
|
||||||
|
<p class="pb-4">
|
||||||
|
Click on the stars to track your progression.<br>
|
||||||
|
Each node has 3 quests. Add a star when you complete the corresponding quest.<br>
|
||||||
|
Mouseover the stars to see the name of the monster.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tabs -->
|
||||||
|
<span class="flex flex-col w-full">
|
||||||
|
<div class="self-start flex flex-row flex-wrap border-primary border-b font-bold w-full">
|
||||||
|
<a
|
||||||
|
v-for="(zone, letter) in getZones"
|
||||||
|
:key="letter"
|
||||||
|
@click="show_tab = letter"
|
||||||
|
class="px-4 py-2 text-primary cursor-pointer rounded-t"
|
||||||
|
:class="show_tab === letter ? 'bg-secondary' : ''"
|
||||||
|
>
|
||||||
|
{{ zone.name }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="currentZone.boss4">
|
||||||
|
<div><span class="text-pink-500">Pink:</span> {{ currentZone.boss1 }} (Gospel of Egeiro)</div>
|
||||||
|
<div><span class="text-emerald-500">Green:</span> {{ currentZone.boss2 }} (Gospel of Genea)</div>
|
||||||
|
<div><span class="text-amber-500">Yellow:</span> {{ currentZone.boss3 }} (Gospel of Thysia)</div>
|
||||||
|
<div><span class="text-blue-500">Blue:</span> {{ currentZone.boss4 }} (Gospel of Analipsis)</div>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="currentZone.boss3">
|
||||||
|
<div><span class="text-pink-500">Pink:</span> {{ currentZone.boss1 }} (Organ)</div>
|
||||||
|
<div><span class="text-emerald-500">Green:</span> {{ currentZone.boss2 }} (Rib)</div>
|
||||||
|
<div><span class="text-amber-500">Yellow:</span> {{ currentZone.boss3 }} (Core)</div>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<div><span class="text-pink-500">Pink:</span> {{ currentZone.boss1 }} (Invocation)</div>
|
||||||
|
<div><span class="text-emerald-500">Green:</span> {{ currentZone.boss2 }} (Masquerade)</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="relative bg-primary overflow-x-auto">
|
||||||
|
|
||||||
|
<picture v-if="show_loot" class="block" :style="currentZoneWidth">
|
||||||
|
<source type="image/webp" :srcset="'/img/arcarum/replicard_' + show_tab + '_loot.webp'">
|
||||||
|
<img :src="'/img/arcarum/replicard_' + show_tab + '_loot.png'">
|
||||||
|
</picture>
|
||||||
|
<picture :class="show_loot ? 'absolute top-0 left-0' : ''" :style="currentZoneWidth">
|
||||||
|
<source type="image/webp" :srcset="'/img/arcarum/replicard_' + show_tab + '.webp'">
|
||||||
|
<img :src="'/img/arcarum/replicard_' + show_tab + '.png'">
|
||||||
|
</picture>
|
||||||
|
<span
|
||||||
|
v-if="show_progression"
|
||||||
|
class="absolute top-0 left-0"
|
||||||
|
:style="currentZoneWidth"
|
||||||
|
>
|
||||||
|
<div class="relative">
|
||||||
|
<stars-line
|
||||||
|
v-for="(star, index) in currentZone.stars"
|
||||||
|
:key="index"
|
||||||
|
class="absolute bg-black"
|
||||||
|
:style="'top: ' + star.top + 'px; left: ' + star.left + 'px;'"
|
||||||
|
:base="2"
|
||||||
|
:extra="3"
|
||||||
|
:current.sync="progression[show_tab][index]"
|
||||||
|
:max="3"
|
||||||
|
:title="star.name"
|
||||||
|
></stars-line>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Utils from '@/js/utils.js'
|
||||||
|
|
||||||
|
import Checkbox from '@/components/common/Checkbox.vue'
|
||||||
|
import StarsLine from '@/components/StarsLine.vue'
|
||||||
|
|
||||||
|
import replicardMixin from '@/store/modules/replicard'
|
||||||
|
|
||||||
|
const lsMgt = new Utils.LocalStorageMgt('Replicard');
|
||||||
|
|
||||||
|
const ZONES = {
|
||||||
|
'E': {
|
||||||
|
name: 'Eletio',
|
||||||
|
boss1: 'The Devil',
|
||||||
|
boss2: 'The Sun',
|
||||||
|
boss3: 'The Star',
|
||||||
|
width: '1377',
|
||||||
|
stars: [
|
||||||
|
{top: 140, left: 45, name: 'Slithering Seductress'},
|
||||||
|
{top: 135, left: 322, name: 'Living Lightning Rod'},
|
||||||
|
{top: 265, left: 272, name: 'Eletion Drake'},
|
||||||
|
{top: 142, left: 553, name: 'Paradoxical Gate'},
|
||||||
|
{top: 128, left: 685, name: 'Blazing Everwing'},
|
||||||
|
{top: 208, left: 812, name: 'Death Seer'},
|
||||||
|
{top: 66, left: 965, name: 'Hundred-Armed Hulk'},
|
||||||
|
{top: 193, left: 1135, name: 'Terror Trifecta'},
|
||||||
|
{top: 265, left: 1125, name: 'Rageborn One'},
|
||||||
|
{top: 135, left: 1280, name: 'Eletion Glider'}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'F': {
|
||||||
|
name: 'Faym',
|
||||||
|
boss1: 'Justice',
|
||||||
|
boss2: 'The Moon',
|
||||||
|
boss3: 'Death',
|
||||||
|
width: '1377',
|
||||||
|
stars: [
|
||||||
|
{top: 146, left: 30, name: 'Trident Grandmaster'},
|
||||||
|
{top: 206, left: 167, name: 'Hoarfrost Icequeen'},
|
||||||
|
{top: 263, left: 352, name: 'Oceanic Archon'},
|
||||||
|
{top: 146, left: 508, name: 'Farsea Predator'},
|
||||||
|
{top: 206, left: 646, name: 'Faymian Fortress'},
|
||||||
|
{top: 146, left: 791, name: 'Draconic Simulacrum'},
|
||||||
|
{top: 146, left: 993, name: 'Azureflame Dragon'},
|
||||||
|
{top: 263, left: 1091, name: 'Eye of Sorrow'},
|
||||||
|
{top: 149, left: 1275, name: 'Mad Shearwielder'},
|
||||||
|
{top: 154, left: 1132, name: 'Faymian Gun'}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'G': {
|
||||||
|
name: 'Goliath',
|
||||||
|
boss1: 'The Hanged Man',
|
||||||
|
boss2: 'The Tower',
|
||||||
|
boss3: 'Death',
|
||||||
|
width: '1377',
|
||||||
|
stars: [
|
||||||
|
{top: 257, left: 164, name: 'Avatar of Avarice'},
|
||||||
|
{top: 29, left: 237, name: 'Temptation\'s Guide'},
|
||||||
|
{top: 143, left: 285, name: 'World\'s Veil'},
|
||||||
|
{top: 138, left: 592, name: 'Bloodstained Barbarian'},
|
||||||
|
{top: 257, left: 563, name: 'Goliath Keeper'},
|
||||||
|
{top: 120, left: 810, name: 'Frenzied Howler'},
|
||||||
|
{top: 141, left: 963, name: 'Vestige of Truth'},
|
||||||
|
{top: 257, left: 857, name: 'Goliath Vanguard'},
|
||||||
|
{top: 194, left: 1085, name: 'Writhing Despair'},
|
||||||
|
{top: 198, left: 1282, name: 'Goliath Triune'}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'H': {
|
||||||
|
name: 'Harbinger',
|
||||||
|
boss1: 'Temperance',
|
||||||
|
boss2: 'Judgement',
|
||||||
|
boss3: 'The Star',
|
||||||
|
width: '1377',
|
||||||
|
stars: [
|
||||||
|
{top: 174, left: 29, name: 'Dirgesinger'},
|
||||||
|
{top: 34, left: 208, name: 'Vengeful Demigod'},
|
||||||
|
{top: 250, left: 159, name: 'Wildwind Conjurer / Fullthunder Conjurer'},
|
||||||
|
{top: 146, left: 334, name: 'Harbinger Simurgh'},
|
||||||
|
{top: 260, left: 486, name: 'Harbinger Hardwood'},
|
||||||
|
{top: 188, left: 583, name: 'Demanding Stormgod'},
|
||||||
|
{top: 143, left: 960, name: 'Harbinger Tyrant'},
|
||||||
|
{top: 256, left: 1099, name: 'Phantasmagoric Aberration'},
|
||||||
|
{top: 186, left: 1220, name: 'Dimentional Riftwalker'},
|
||||||
|
{top: 48, left: 670, name: 'Harbinger Stormer'}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'I': {
|
||||||
|
name: 'Invidia',
|
||||||
|
boss1: 'The Sun / The Devil',
|
||||||
|
boss2: 'The Star',
|
||||||
|
width: '884',
|
||||||
|
stars: [
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'J': {
|
||||||
|
name: 'Joculator',
|
||||||
|
boss1: 'The Moon / Justice',
|
||||||
|
boss2: 'Death',
|
||||||
|
width: '884',
|
||||||
|
stars: [
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'K': {
|
||||||
|
name: 'Kalendae',
|
||||||
|
boss1: 'Death',
|
||||||
|
boss2: 'The Hanged Man / The Tower',
|
||||||
|
width: '884',
|
||||||
|
stars: [
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'L': {
|
||||||
|
name: 'Liber',
|
||||||
|
boss1: 'Temperance / Judgment',
|
||||||
|
boss2: 'The Star',
|
||||||
|
width: '884',
|
||||||
|
stars: [
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'M': {
|
||||||
|
name: 'Mundus',
|
||||||
|
boss1: ' ',
|
||||||
|
boss2: ' ',
|
||||||
|
boss3: ' ',
|
||||||
|
boss4: ' ',
|
||||||
|
width: 960,
|
||||||
|
stars: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Checkbox,
|
||||||
|
StarsLine
|
||||||
|
},
|
||||||
|
mixins: [
|
||||||
|
replicardMixin
|
||||||
|
],
|
||||||
|
head: {
|
||||||
|
title: 'Granblue.Party - Replicard Sandbox Maps',
|
||||||
|
desc: 'View Replicard Sandbox Maps with loots, colors, and progression for each node',
|
||||||
|
image: 'https://www.granblue.party/img/card_replicard.jpg',
|
||||||
|
keywords: 'Replicard, Sandbox, Maps, Arcarum, Evoker, Eletio, Faym, Goliath, Harbinger, Invidia, Joculator, Kalendae, Liber, Mundus'
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
show_help: false,
|
||||||
|
show_loot: true,
|
||||||
|
show_progression: true,
|
||||||
|
scrollbar: true,
|
||||||
|
show_tab: 'E',
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
save() {
|
||||||
|
this.axios.post('/replicard/save', this.progression)
|
||||||
|
.then(response => this.$store.dispatch('addMessage', {message: 'Data saved successfully'}))
|
||||||
|
.catch(error => this.$store.dispatch('addAxiosErrorMessage', error));
|
||||||
|
},
|
||||||
|
loadServerData() {
|
||||||
|
if (this.data_fetched === false) {
|
||||||
|
return this.axios.get('/replicard/load')
|
||||||
|
.then(response => {
|
||||||
|
if (response.data !== null) {
|
||||||
|
this.$set(this, 'progression', response.data);
|
||||||
|
this.data_fetched = true;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => this.$store.dispatch('addAxiosErrorMessage', error));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
loadData() {
|
||||||
|
if (this.isUserLogged) {
|
||||||
|
this.loadServerData();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.$store.commit('replicard/resetReplicard');
|
||||||
|
|
||||||
|
const progression = lsMgt.fetchValue('progression');
|
||||||
|
if (progression !== undefined) {
|
||||||
|
this.progression = progression;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
isUserLogged() {
|
||||||
|
return this.$store.getters.getUserId !== null;
|
||||||
|
},
|
||||||
|
getZones() {
|
||||||
|
return ZONES;
|
||||||
|
},
|
||||||
|
currentZone() {
|
||||||
|
return this.getZones[this.show_tab];
|
||||||
|
},
|
||||||
|
currentZoneWidth() {
|
||||||
|
return "min-width: " + this.currentZone.width + "px;";
|
||||||
|
},
|
||||||
|
progression: {
|
||||||
|
get() { return this.$store.state.replicard.progression },
|
||||||
|
set(value) { this.$store.commit('replicard/setReplicardData', value) }
|
||||||
|
},
|
||||||
|
data_fetched: {
|
||||||
|
get() { return this.$store.state.replicard.data_fetched },
|
||||||
|
set(value) { this.$store.commit('replicard/setReplicardFetched', value) }
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
'$store.getters.getUserId'() {
|
||||||
|
this.data_fetched = false;
|
||||||
|
this.loadData();
|
||||||
|
},
|
||||||
|
show_loot() {
|
||||||
|
lsMgt.setValue('show_loot', this);
|
||||||
|
},
|
||||||
|
show_tab() {
|
||||||
|
lsMgt.setValue('show_tab', this);
|
||||||
|
},
|
||||||
|
progression: {
|
||||||
|
handler() {
|
||||||
|
if ( ! this.isUserLogged) {
|
||||||
|
lsMgt.setValue('progression', this);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
deep: true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
serverPrefetch() {
|
||||||
|
if (this.isUserLogged) {
|
||||||
|
return this.loadServerData();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
lsMgt.getValue(this, 'show_loot');
|
||||||
|
lsMgt.getValue(this, 'show_tab');
|
||||||
|
|
||||||
|
this.loadData();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
12
app.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import Vue from "vue"
|
||||||
|
import App from "./src/App.vue"
|
||||||
|
import Vuex from "vuex"
|
||||||
|
import store from "./src/store"
|
||||||
|
|
||||||
|
Vue.use(Vuex)
|
||||||
|
|
||||||
|
new Vue({
|
||||||
|
el: "#app",
|
||||||
|
store,
|
||||||
|
render: h => h(App)
|
||||||
|
})
|
||||||
63
build.js
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
const esbuild = require("esbuild")
|
||||||
|
const fs = require("fs")
|
||||||
|
const { parse, compileTemplate } = require("@vue/compiler-sfc")
|
||||||
|
|
||||||
|
esbuild.build({
|
||||||
|
entryPoints: ["app.js"],
|
||||||
|
bundle: true,
|
||||||
|
outfile: "dist.js",
|
||||||
|
format: "iife",
|
||||||
|
platform: "browser",
|
||||||
|
|
||||||
|
define: {
|
||||||
|
"process.env.NODE_ENV": '"production"'
|
||||||
|
},
|
||||||
|
|
||||||
|
plugins: [
|
||||||
|
{
|
||||||
|
name: "vue2-sfc",
|
||||||
|
setup(build) {
|
||||||
|
|
||||||
|
build.onLoad({ filter: /\.vue$/ }, async (args) => {
|
||||||
|
|
||||||
|
const source = fs.readFileSync(args.path, "utf8")
|
||||||
|
|
||||||
|
const parsed = parse(source)
|
||||||
|
|
||||||
|
if (!parsed.descriptor) {
|
||||||
|
return { contents: "" }
|
||||||
|
}
|
||||||
|
|
||||||
|
const descriptor = parsed.descriptor
|
||||||
|
|
||||||
|
let scriptCode = ""
|
||||||
|
|
||||||
|
if (descriptor.script && descriptor.script.content) {
|
||||||
|
scriptCode = descriptor.script.content
|
||||||
|
}
|
||||||
|
|
||||||
|
let templateCode = ""
|
||||||
|
|
||||||
|
if (descriptor.template && descriptor.template.content) {
|
||||||
|
|
||||||
|
const compiled = compileTemplate({
|
||||||
|
source: descriptor.template.content,
|
||||||
|
filename: args.path,
|
||||||
|
id: args.path
|
||||||
|
})
|
||||||
|
|
||||||
|
templateCode = compiled.code
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
contents: `
|
||||||
|
${scriptCode}
|
||||||
|
|
||||||
|
${templateCode}
|
||||||
|
`
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}).catch(() => process.exit(1))
|
||||||
100
components/BoxCharacter.vue
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col" style="min-width: 78px; max-width: 78px;">
|
||||||
|
<!-- Title -->
|
||||||
|
<a
|
||||||
|
class="text-xs text-primary h-5 px-1 text-center truncate"
|
||||||
|
target="_blank"
|
||||||
|
:href="'https://gbf.wiki/' + object.nameen"
|
||||||
|
:title="getName"
|
||||||
|
v-if="! objectIsEmpty"
|
||||||
|
>{{ getName }}</a>
|
||||||
|
<span class="text-xs h-5" v-else> </span>
|
||||||
|
|
||||||
|
<!-- Portrait -->
|
||||||
|
<portrait
|
||||||
|
:object="object"
|
||||||
|
:showRing="showRing"
|
||||||
|
@drag-portrait="$emit('drag-portrait', $event)"
|
||||||
|
@drop-portrait="drop"
|
||||||
|
@click-portrait="$emit('click-portrait')"
|
||||||
|
@click-pring="clickPRing"
|
||||||
|
@stars-changed="starsChanged"
|
||||||
|
></portrait>
|
||||||
|
|
||||||
|
<!-- Stats -->
|
||||||
|
<div class="flex flex-row flex-nowrap justify-around text-xs" v-if="! objectIsEmpty && showLevel">
|
||||||
|
<stat-input shortName="lvl" longName="Character level" :prop.sync="object.level" :length="3"></stat-input>
|
||||||
|
<stat-input shortName="+" longName="Plus bonuses" :prop.sync="object.pluses" :length="3"></stat-input>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Skills -->
|
||||||
|
<skills
|
||||||
|
:object="object"
|
||||||
|
@click-skill="$emit('click-skill', $event)"
|
||||||
|
></skills>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { objectIsEmpty, getName } from "@/js/mixins"
|
||||||
|
import { mapState } from 'vuex'
|
||||||
|
import UtilsParty from '@/js/utils-party'
|
||||||
|
|
||||||
|
import Portrait from '@/components/BoxCharacterPortrait.vue'
|
||||||
|
import Skills from '@/components/BoxCharacterSkills.vue'
|
||||||
|
import StatInput from '@/components/common/StatInput.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Portrait,
|
||||||
|
Skills,
|
||||||
|
StatInput,
|
||||||
|
},
|
||||||
|
mixins: [
|
||||||
|
objectIsEmpty,
|
||||||
|
getName
|
||||||
|
],
|
||||||
|
props: {
|
||||||
|
object: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
showLevel: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
showRing: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
drop(ev) {
|
||||||
|
const data = ev.dataTransfer.getData("character");
|
||||||
|
if (data.length > 0) {
|
||||||
|
this.$emit('swap', JSON.parse(data));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
clickPRing() {
|
||||||
|
if (this.party_mode !== this.$MODE.ReadOnly) {
|
||||||
|
this.object.haspring = ! this.object.haspring
|
||||||
|
}
|
||||||
|
},
|
||||||
|
starsChanged(object, count) {
|
||||||
|
this.$set(object, "stars", count);
|
||||||
|
|
||||||
|
if ( ! this.showLevel || object.level > this.getLevel) {
|
||||||
|
this.$set(object, "level", this.getLevel);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
party_mode: state => state.party_builder.party_mode
|
||||||
|
}),
|
||||||
|
getLevel() {
|
||||||
|
return UtilsParty.getCharacterLevel(this.object);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
83
components/BoxCharacterPortrait.vue
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
<template>
|
||||||
|
<span class="relative">
|
||||||
|
<img
|
||||||
|
class="w-full"
|
||||||
|
:class="party_mode === $MODE.Edit ? 'cursor-pointer' : ''"
|
||||||
|
style="min-height: 142px; max-height: 142px;"
|
||||||
|
:draggable="! objectIsEmpty"
|
||||||
|
:src="getImage"
|
||||||
|
@click="$emit('click-portrait')"
|
||||||
|
@dragstart="$emit('drag-portrait', $event)"
|
||||||
|
@dragover.prevent
|
||||||
|
@drop.prevent="$emit('drop-portrait', $event)"
|
||||||
|
>
|
||||||
|
|
||||||
|
<stars-line
|
||||||
|
v-if="! objectIsEmpty"
|
||||||
|
class="absolute top-0 bg-black/50"
|
||||||
|
:base="object.starsbase"
|
||||||
|
:extra="object.starsmax"
|
||||||
|
:current="object.stars"
|
||||||
|
@update:current="$emit('stars-changed', object, $event)"
|
||||||
|
:max="5"
|
||||||
|
:transcendance="true"
|
||||||
|
:readOnly="party_mode === $MODE.ReadOnly"
|
||||||
|
></stars-line>
|
||||||
|
|
||||||
|
<img
|
||||||
|
v-if="! objectIsEmpty && showRing"
|
||||||
|
class="absolute bottom-0 right-0"
|
||||||
|
:class="getPRingClasses"
|
||||||
|
src="/img/icon_pring.png"
|
||||||
|
title="Perpetuity Ring"
|
||||||
|
@click="$emit('click-pring')"
|
||||||
|
>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { objectIsEmpty } from "@/js/mixins"
|
||||||
|
import { mapState } from 'vuex'
|
||||||
|
|
||||||
|
import StarsLine from '@/components/StarsLine.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
StarsLine,
|
||||||
|
},
|
||||||
|
mixins: [
|
||||||
|
objectIsEmpty
|
||||||
|
],
|
||||||
|
props: {
|
||||||
|
object: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
showRing: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
party_mode: state => state.party_builder.party_mode
|
||||||
|
}),
|
||||||
|
getPRingClasses() {
|
||||||
|
let classes = this.object.haspring ? '' : 'grayscale-80 opacity-70';
|
||||||
|
if (this.party_mode !== this.$MODE.ReadOnly) {
|
||||||
|
classes += ' cursor-pointer';
|
||||||
|
}
|
||||||
|
return classes;
|
||||||
|
},
|
||||||
|
getImage() {
|
||||||
|
if (this.objectIsEmpty) {
|
||||||
|
if (this.party_mode !== this.$MODE.Edit) {
|
||||||
|
return '/img/empty_chara_ro.jpg';
|
||||||
|
}
|
||||||
|
return '/img/empty_chara.jpg';
|
||||||
|
}
|
||||||
|
return "/img/unit/" + this.object.characterid + "000.jpg";
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
45
components/BoxCharacterSkills.vue
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-row flex-wrap" style="min-height: 78px; max-height: 78px;" v-if="! objectIsEmpty">
|
||||||
|
<span v-for="skill in getSkills" :key="skill.index" class="w-1/2 tooltip-parent">
|
||||||
|
<img
|
||||||
|
class="w-full"
|
||||||
|
:class="party_mode === $MODE.Action ? 'cursor-pointer' : ''"
|
||||||
|
:src="getSkillImage(skill.index)"
|
||||||
|
@click="$emit('click-skill', skill.index)"
|
||||||
|
>
|
||||||
|
<span class="tooltip">{{ skill.name }}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { objectIsEmpty } from "@/js/mixins"
|
||||||
|
import { mapState } from 'vuex'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
mixins: [
|
||||||
|
objectIsEmpty
|
||||||
|
],
|
||||||
|
props: {
|
||||||
|
object: Object,
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getSkillImage(index) {
|
||||||
|
return '/img/chara_skills/' + this.object.characterid + '_' + index + '.png';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
party_mode: state => state.party_builder.party_mode
|
||||||
|
}),
|
||||||
|
getSkills() {
|
||||||
|
return this.object.skills.flatMap(s => {
|
||||||
|
if (this.object.level >= s.obtain) {
|
||||||
|
return [s];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
})
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
68
components/BoxClass.vue
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col" style="min-width: 78px; max-width: 78px;">
|
||||||
|
<!-- Title -->
|
||||||
|
<a
|
||||||
|
class="text-xs text-primary rounded-t h-5 px-1 text-center truncate"
|
||||||
|
target="_blank"
|
||||||
|
:href="'https://gbf.wiki/' + object.nameen"
|
||||||
|
:title="object.nameen"
|
||||||
|
>{{ object.nameen }}</a>
|
||||||
|
|
||||||
|
<!-- Portrait -->
|
||||||
|
<img
|
||||||
|
:class="party_mode === $MODE.Edit ? 'cursor-pointer' : ''"
|
||||||
|
style="min-height: 142px; max-height: 142px;"
|
||||||
|
:src="getPortraitImage"
|
||||||
|
@click="$emit('click-portrait')">
|
||||||
|
|
||||||
|
<!-- Skills -->
|
||||||
|
<div class="flex flex-row flex-wrap">
|
||||||
|
<span v-for="(skill, skillIndex) in object.skills" :key="skillIndex" class="w-1/2 tooltip-parent">
|
||||||
|
<img
|
||||||
|
class="w-full"
|
||||||
|
:class="party_mode !== $MODE.ReadOnly ? 'cursor-pointer' : ''"
|
||||||
|
:src="getSkillImage(skillIndex)"
|
||||||
|
@click="$emit('click-skill', skillIndex)"
|
||||||
|
>
|
||||||
|
<span v-if="skill" class="tooltip">{{ skill.nameen }}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapState } from 'vuex'
|
||||||
|
|
||||||
|
import Utils from '@/js/utils.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
object: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getSkillImage(index) {
|
||||||
|
if (this.object.skills !== undefined && this.object.skills[index] !== null) {
|
||||||
|
return '/img/class_skills/' + this.object.skills[index].skillid + '.png';
|
||||||
|
}
|
||||||
|
return '/img/class_skills/null.png';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
party_mode: state => state.party_builder.party_mode
|
||||||
|
}),
|
||||||
|
getPortraitImage() {
|
||||||
|
if (Utils.isEmpty(this.object.nameen)) {
|
||||||
|
if (this.party_mode !== this.$MODE.Edit) {
|
||||||
|
return '/img/empty_chara_ro.jpg';
|
||||||
|
}
|
||||||
|
return '/img/empty_chara.jpg';
|
||||||
|
}
|
||||||
|
return '/img/class/' + this.object.nameen.replace(/\s/g, '_') + '.jpg';
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
92
components/BoxSummon.vue
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col" style="min-width: 110px; max-width: 110px;">
|
||||||
|
<!-- Title -->
|
||||||
|
<a
|
||||||
|
class="text-xs text-primary hover:text-primary rounded-t h-5 px-1 text-center truncate"
|
||||||
|
target="_blank"
|
||||||
|
:href="'https://gbf.wiki/' + object.nameen"
|
||||||
|
:style="getTitleColor"
|
||||||
|
:title="getName"
|
||||||
|
v-if="! objectIsEmpty"
|
||||||
|
>{{ getName }}</a>
|
||||||
|
<span class="text-xs h-5" v-else> </span>
|
||||||
|
|
||||||
|
<!-- Portrait -->
|
||||||
|
<portrait
|
||||||
|
:object="object"
|
||||||
|
@drag-portrait="$emit('drag-portrait', $event)"
|
||||||
|
@drop-portrait="drop"
|
||||||
|
@click-portrait="$emit('click-portrait')"
|
||||||
|
@stars-changed="starsChanged"
|
||||||
|
></portrait>
|
||||||
|
|
||||||
|
<!-- Stats -->
|
||||||
|
<div class="flex flex-row flex-nowrap justify-around text-xs" v-if="! objectIsEmpty && showLevel">
|
||||||
|
<stat-input shortName="lvl" longName="Summon level" :prop.sync="object.level" :length="3"></stat-input>
|
||||||
|
<stat-input shortName="+" longName="Plus bonuses" :prop.sync="object.pluses" :length="3"></stat-input>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { objectIsEmpty, getName } from "@/js/mixins"
|
||||||
|
import UtilsParty from '@/js/utils-party'
|
||||||
|
|
||||||
|
import Portrait from '@/components/BoxSummonPortrait.vue'
|
||||||
|
import StatInput from '@/components/common/StatInput.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
StatInput,
|
||||||
|
Portrait
|
||||||
|
},
|
||||||
|
mixins: [
|
||||||
|
objectIsEmpty,
|
||||||
|
getName
|
||||||
|
],
|
||||||
|
props: {
|
||||||
|
object: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
showLevel: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
drop(ev) {
|
||||||
|
const data = ev.dataTransfer.getData("summon");
|
||||||
|
if (data.length > 0) {
|
||||||
|
this.$emit('swap', JSON.parse(data));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
starsChanged(count) {
|
||||||
|
this.$set(this.object, "stars", count);
|
||||||
|
|
||||||
|
if ( ! this.showLevel || this.object.level > this.getLevel) {
|
||||||
|
this.$set(this.object, "level", this.getLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
UtilsParty.setSummonCurrentData(this.object);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
getTitleColor() {
|
||||||
|
switch (this.object.stars) {
|
||||||
|
case 3:
|
||||||
|
return 'color: #ffa826;';
|
||||||
|
case 4:
|
||||||
|
return 'color: #e3b7ff;';
|
||||||
|
case 5:
|
||||||
|
return 'color: #a1e3ff;';
|
||||||
|
default:
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getLevel() {
|
||||||
|
return UtilsParty.getSummonLevel(this.object);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
76
components/BoxSummonPortrait.vue
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
<template>
|
||||||
|
<span class="relative tooltip-parent">
|
||||||
|
<img
|
||||||
|
class="w-full"
|
||||||
|
:class="party_mode !== $MODE.ReadOnly ? 'cursor-pointer' : ''"
|
||||||
|
style="min-height: 83px; max-height: 83px;"
|
||||||
|
:draggable="! objectIsEmpty"
|
||||||
|
:src="getImage"
|
||||||
|
@click="tryToEmit('click-portrait')"
|
||||||
|
@dragstart="tryToEmit('drag-portrait', $event)"
|
||||||
|
@dragover.prevent
|
||||||
|
@drop.prevent="tryToEmit('drop-portrait', $event)"
|
||||||
|
>
|
||||||
|
|
||||||
|
<stars-line
|
||||||
|
v-if="! objectIsEmpty"
|
||||||
|
class="absolute bottom-0 right-0 w-4/5 bg-black/50"
|
||||||
|
:base="object.starsbase"
|
||||||
|
:extra="object.starsmax"
|
||||||
|
:current="object.stars"
|
||||||
|
@update:current="$emit('stars-changed', $event)"
|
||||||
|
:max="5"
|
||||||
|
:transcendance="true"
|
||||||
|
:readOnly="party_mode === $MODE.ReadOnly"
|
||||||
|
></stars-line>
|
||||||
|
|
||||||
|
<span class="tooltip" v-if="object.current_data !== undefined">
|
||||||
|
<span class="font-mono text-xs" v-for="(data, index) in object.data[object.current_data]" :key="index">
|
||||||
|
<span class="capitalize">{{ data.aura_type }}</span> {{ data.stat }} {{ data.percent }}% <span v-if="data.slot">({{ data.slot }})</span><br>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { objectIsEmpty } from "@/js/mixins"
|
||||||
|
import { mapState } from 'vuex'
|
||||||
|
|
||||||
|
import StarsLine from '@/components/StarsLine.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
StarsLine,
|
||||||
|
},
|
||||||
|
mixins: [
|
||||||
|
objectIsEmpty
|
||||||
|
],
|
||||||
|
props: {
|
||||||
|
object: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
tryToEmit(name, event) {
|
||||||
|
if (this.party_mode !== this.$MODE.ReadOnly) {
|
||||||
|
this.$emit(name, event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
party_mode: state => state.party_builder.party_mode
|
||||||
|
}),
|
||||||
|
getImage() {
|
||||||
|
if (this.objectIsEmpty) {
|
||||||
|
if (this.party_mode === this.$MODE.Edit) {
|
||||||
|
return '/img/empty_summon.jpg';
|
||||||
|
}
|
||||||
|
return '/img/empty_summon_ro.jpg';
|
||||||
|
}
|
||||||
|
return "/img/unit/" + this.object.summonid + "000.jpg";
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
178
components/BoxWeapon.vue
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col" style="max-width: 105px;">
|
||||||
|
<!-- Title -->
|
||||||
|
<a
|
||||||
|
class="text-xs text-primary h-5 px-1 text-center truncate"
|
||||||
|
target="_blank"
|
||||||
|
:href="'https://gbf.wiki/' + object.nameen"
|
||||||
|
:title="getName"
|
||||||
|
v-if="! objectIsEmpty"
|
||||||
|
>{{ getName }}</a>
|
||||||
|
<span class="text-xs h-5" v-else> </span>
|
||||||
|
|
||||||
|
<!-- Portrait -->
|
||||||
|
<portrait
|
||||||
|
:object="object"
|
||||||
|
:isArcarum="isArcarum"
|
||||||
|
@click-portrait="$emit('click-portrait')"
|
||||||
|
@drag-portrait="$emit('drag-portrait', $event)"
|
||||||
|
@drop-portrait="drop"
|
||||||
|
@stars-changed="starsChanged"
|
||||||
|
></portrait>
|
||||||
|
|
||||||
|
<!-- Stats -->
|
||||||
|
<div class="flex flex-row flex-nowrap justify-around text-xs" v-if="! objectIsEmpty && showLevel">
|
||||||
|
<stat-input shortName="lvl" longName="Weapon level" :prop.sync="object.level" :length="3"></stat-input>
|
||||||
|
<stat-input shortName="sl" longName="Skill level" :prop.sync="object.sklevel" :length="2"></stat-input>
|
||||||
|
<stat-input shortName="+" longName="Plus bonuses" :prop.sync="object.pluses" :length="3"></stat-input>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Skills -->
|
||||||
|
<skills :object="object" :skills="getSkills"></skills>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { objectIsEmpty, getName } from "@/js/mixins"
|
||||||
|
import Utils from '@/js/utils'
|
||||||
|
import UtilsParty from '@/js/utils-party'
|
||||||
|
|
||||||
|
import Portrait from '@/components/BoxWeaponPortrait.vue'
|
||||||
|
import Skills from '@/components/BoxWeaponSkills.vue'
|
||||||
|
import StatInput from '@/components/common/StatInput.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Portrait,
|
||||||
|
Skills,
|
||||||
|
StatInput,
|
||||||
|
},
|
||||||
|
mixins: [
|
||||||
|
objectIsEmpty,
|
||||||
|
getName
|
||||||
|
],
|
||||||
|
props: {
|
||||||
|
object: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
skills: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
showLevel: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
isArcarum: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
drop(ev) {
|
||||||
|
const data = ev.dataTransfer.getData("weapon");
|
||||||
|
if (data.length > 0) {
|
||||||
|
this.$emit('swap', JSON.parse(data));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
starsChanged(object, count) {
|
||||||
|
this.$set(object, "stars", count);
|
||||||
|
|
||||||
|
if ( ! this.showLevel || object.level > this.getLevel) {
|
||||||
|
this.$set(object, 'level', this.getLevel);
|
||||||
|
}
|
||||||
|
if ( ! this.showLevel || object.sklevel > this.getSkillLevel) {
|
||||||
|
this.$set(object, 'sklevel', this.getSkillLevel);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getWeaponSkillValue(skill_level, percent, stat) {
|
||||||
|
let ratio = percent[1];
|
||||||
|
|
||||||
|
if (stat === 'stamina') {
|
||||||
|
if (this.getPercentHP < 25) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (skill_level <= 15) {
|
||||||
|
return (Math.pow(this.getPercentHP / (ratio - skill_level), 2.9) + 2.1) / 100;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return (Math.pow(this.getPercentHP / (ratio - (15 + (0.4 * (skill_level-15)))), 2.9) + 2.1) / 100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (skill_level === 20 && percent.hasOwnProperty(20)) {
|
||||||
|
ratio = percent[20];
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (skill_level > 1 && percent.hasOwnProperty(10)) {
|
||||||
|
ratio += (percent[10] - percent[1]) / 9 * Math.min(skill_level - 1, 9);
|
||||||
|
}
|
||||||
|
if (skill_level > 10 && percent.hasOwnProperty(15)) {
|
||||||
|
ratio += (percent[15] - percent[10]) / 5 * Math.min(skill_level - 10, 5);
|
||||||
|
}
|
||||||
|
if (skill_level > 15 && percent.hasOwnProperty(20)) {
|
||||||
|
ratio += (percent[20] - percent[15]) / 5 * Math.min(skill_level - 15, 5);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stat === 'enmity') {
|
||||||
|
const missing_hp_ratio = 1 - this.getPercentHP / 100;
|
||||||
|
return (ratio / 100) * ((1 + 2 * missing_hp_ratio) * missing_hp_ratio);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ratio / 100;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
getPercentHP() {
|
||||||
|
return this.$store.state.party_builder.percent_HP;
|
||||||
|
},
|
||||||
|
getLevel() {
|
||||||
|
return UtilsParty.getWeaponLevel(this.object);
|
||||||
|
},
|
||||||
|
getSkillLevel() {
|
||||||
|
return UtilsParty.getWeaponSkillLevel(this.object);
|
||||||
|
},
|
||||||
|
getSkills() {
|
||||||
|
if (this.object.skills === undefined) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
let skills = this.object.skills.flatMap((slot, index) => {
|
||||||
|
let skill = [];
|
||||||
|
for (let item of slot) {
|
||||||
|
if (item.level > this.object.level) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (this.object.keys[index] !== null) {
|
||||||
|
// Skill key selected
|
||||||
|
skill = this.object.keys[index];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
skill = item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return skill;
|
||||||
|
});
|
||||||
|
|
||||||
|
// This is probably not optimal, since level change triggers this for nothing
|
||||||
|
skills.forEach(s => {
|
||||||
|
if (s.data) {
|
||||||
|
for (let d of s.data) {
|
||||||
|
this.$set(d, 'value', this.getWeaponSkillValue(this.object.sklevel, d.percent, d.stat));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Keep in the store
|
||||||
|
for (let i=0; i<skills.length; i++) {
|
||||||
|
this.$set(this.skills, i, skills[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return skills;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
69
components/BoxWeaponPortrait.vue
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
<template>
|
||||||
|
<span class="relative">
|
||||||
|
<img
|
||||||
|
class="w-full"
|
||||||
|
:class="party_mode === $MODE.Edit ? 'cursor-pointer' : ''"
|
||||||
|
style="max-height: 60px;"
|
||||||
|
:draggable="! objectIsEmpty"
|
||||||
|
:src="getImage"
|
||||||
|
@click="$emit('click-portrait')"
|
||||||
|
@dragstart="$emit('drag-portrait', $event)"
|
||||||
|
@dragover.prevent
|
||||||
|
@drop.prevent="$emit('drop-portrait', $event)"
|
||||||
|
>
|
||||||
|
|
||||||
|
<stars-line
|
||||||
|
v-if="! objectIsEmpty"
|
||||||
|
class="absolute bottom-0 right-0 w-3/4 bg-black/50"
|
||||||
|
:base="object.starsbase"
|
||||||
|
:extra="object.starsmax"
|
||||||
|
:current="object.stars"
|
||||||
|
@update:current="$emit('stars-changed', object, $event)"
|
||||||
|
:max="5"
|
||||||
|
:readOnly="party_mode === $MODE.ReadOnly"
|
||||||
|
></stars-line>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { objectIsEmpty } from "@/js/mixins"
|
||||||
|
import { mapState } from 'vuex'
|
||||||
|
|
||||||
|
import StarsLine from '@/components/StarsLine.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
StarsLine,
|
||||||
|
},
|
||||||
|
mixins: [
|
||||||
|
objectIsEmpty
|
||||||
|
],
|
||||||
|
props: {
|
||||||
|
object: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
isArcarum: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
party_mode: state => state.party_builder.party_mode
|
||||||
|
}),
|
||||||
|
getImage() {
|
||||||
|
if (this.objectIsEmpty) {
|
||||||
|
if (this.party_mode !== this.$MODE.Edit) {
|
||||||
|
return '/img/empty_weapon_ro.jpg';
|
||||||
|
}
|
||||||
|
if (this.isArcarum) {
|
||||||
|
return '/img/empty_weapon_arcarum.jpg';
|
||||||
|
}
|
||||||
|
return '/img/empty_weapon.jpg';
|
||||||
|
}
|
||||||
|
return '/img/weapon/' + this.object.weaponid + '00.jpg';
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
72
components/BoxWeaponSkills.vue
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-row flex-nowrap justify-around" v-if="! objectIsEmpty">
|
||||||
|
<span v-for="(skill, skillIndex) in skills" :key="skillIndex" class="tooltip-parent">
|
||||||
|
<img
|
||||||
|
:class="(skill.keyid !== null && party_mode !== $MODE.ReadOnly) ? 'cursor-pointer' : ''"
|
||||||
|
style="width: 30px; height: 30px;"
|
||||||
|
:src="'/img/weapon_skills/' + skill.icon"
|
||||||
|
@click="showKeyModal(skillIndex, skill.keyid)"
|
||||||
|
>
|
||||||
|
<span class="tooltip">
|
||||||
|
{{ skill.name }} <span v-if="skill.boost">({{ skill.boost }})</span><br>
|
||||||
|
<p v-if="skill.data !== null">
|
||||||
|
<span class="font-mono text-xs" v-for="(data, dataIndex) in skill.data" :key="dataIndex">
|
||||||
|
<span class="capitalize">{{ data.aura_type }}</span> {{ data.stat }} {{ (data.value * 100).toFixed(2) }}%<br>
|
||||||
|
</span>
|
||||||
|
</p>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- Modal -->
|
||||||
|
<modal-keys
|
||||||
|
v-if="party_mode !== $MODE.ReadOnly"
|
||||||
|
v-model="show_modal_keys"
|
||||||
|
:keyId="modal_key_id"
|
||||||
|
@key-selected="selectKey"
|
||||||
|
></modal-keys>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { objectIsEmpty } from "@/js/mixins"
|
||||||
|
import { mapState } from 'vuex'
|
||||||
|
|
||||||
|
import ModalKeys from '@/components/ModalKeys.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
ModalKeys,
|
||||||
|
},
|
||||||
|
mixins: [
|
||||||
|
objectIsEmpty
|
||||||
|
],
|
||||||
|
props: {
|
||||||
|
object: Object,
|
||||||
|
skills: Array,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
show_modal_keys: false,
|
||||||
|
modal_skill_index: 0,
|
||||||
|
modal_key_id: 0,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
showKeyModal(index, keyid) {
|
||||||
|
if (keyid !== null && this.party_mode !== this.$MODE.ReadOnly) {
|
||||||
|
this.modal_skill_index = index;
|
||||||
|
this.modal_key_id = keyid;
|
||||||
|
this.show_modal_keys = true;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
selectKey(key) {
|
||||||
|
this.$set(this.object.keys, this.modal_skill_index, key);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
party_mode: state => state.party_builder.party_mode
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
118
components/CalcBulletsList.vue
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col flex-wrap gap-4 items-center w-full">
|
||||||
|
<a
|
||||||
|
class="btn-blue rounded-t font-bold hover:text-primary cursor-pointer p-2"
|
||||||
|
:class="show ? '' : 'rounded-b'"
|
||||||
|
@click="show = !show"
|
||||||
|
title="Click to toggle..."
|
||||||
|
>
|
||||||
|
{{ header }}
|
||||||
|
<fa-icon v-if=" ! show" :icon="['fas', 'angle-right']" class="ml-2"></fa-icon>
|
||||||
|
<fa-icon v-else :icon="['fas', 'angle-down']" class="ml-2"></fa-icon>
|
||||||
|
</a>
|
||||||
|
<div v-if="show" class="flex flex-col w-full items-center" >
|
||||||
|
<span class="mb-4">{{ explainer }}</span>
|
||||||
|
<div v-for="bullets in getBullets" class="flex flex-col bg-secondary p-2 w-full lg:w-1/2 items-center rounded">
|
||||||
|
<h2 class="mb-4">{{ bullets.name }}</h2>
|
||||||
|
|
||||||
|
<div :class="displayList === 0 ? 'flex flex-row flex-wrap' : 'grid grid-cols-1 sm:grid-cols-2'">
|
||||||
|
<div v-for="bullet in bullets.bullets" class="tooltip-parent">
|
||||||
|
<div v-if="displayList === 0">
|
||||||
|
<img :src="'/img/item/' + bullet.image + '.jpg'" width="60px" :title="bullet.name" :class="getBullet(bullet) > 0 ? '' : 'grayscale-80'">
|
||||||
|
<div class="flex flex-row justify-around">
|
||||||
|
<a :class="getBullet(bullet) > 0 ? 'cursor-pointer' : ''" @click="removeBullet(bullet)">
|
||||||
|
<fa-icon
|
||||||
|
:icon="['fa', 'minus-circle']"
|
||||||
|
class="text-sm"
|
||||||
|
:class="getBullet(bullet) > 0 ? 'text-primary hover:text-link-hover' : 'text-transparent'"
|
||||||
|
></fa-icon>
|
||||||
|
</a>
|
||||||
|
<span class="select-none">{{ getBullet(bullet) }}</span>
|
||||||
|
<a class="cursor-pointer" @click="addBullet(bullet)">
|
||||||
|
<fa-icon :icon="['fa', 'plus-circle']" class="text-primary text-sm hover:text-link-hover"></fa-icon>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<span class="tooltip">
|
||||||
|
{{ bullet.name }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div v-else class="flex flex-row flex-wrap items-center">
|
||||||
|
<img :src="'/img/item/' + bullet.image + '.jpg'" width="40px" :title="bullet.name" class="ml-4 mr-2" :class="getBullet(bullet) > 0 ? '' : 'grayscale-80'">
|
||||||
|
<span class="flex flex-col">
|
||||||
|
<div>{{ bullet.name }}</div>
|
||||||
|
<div class="flex flex-row gap-4">
|
||||||
|
<a :class="getBullet(bullet) > 0 ? 'cursor-pointer' : ''" @click="removeBullet(bullet)">
|
||||||
|
<fa-icon
|
||||||
|
:icon="['fa', 'minus-circle']"
|
||||||
|
class="text-sm"
|
||||||
|
:class="getBullet(bullet) > 0 ? 'text-primary hover:text-link-hover' : 'text-transparent'"
|
||||||
|
></fa-icon>
|
||||||
|
</a>
|
||||||
|
<span class="select-none">{{ getBullet(bullet) }}</span>
|
||||||
|
<a class="cursor-pointer" @click="addBullet(bullet)">
|
||||||
|
<fa-icon :icon="['fa', 'plus-circle']" class="text-primary text-sm hover:text-link-hover"></fa-icon>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
header: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
explainer: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
displayList: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
showBullets: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
bullets: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
getBullet: {
|
||||||
|
type: Function,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
addBullet: {
|
||||||
|
type: Function,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
removeBullet: {
|
||||||
|
type: Function,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
getBullets() {
|
||||||
|
return this.bullets;
|
||||||
|
},
|
||||||
|
show: {
|
||||||
|
get() {
|
||||||
|
return this.showBullets;
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
this.$emit('update:showBullets', value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
69
components/CalcPreviewItem.vue
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-row items-center pr-2" :class="localQuantity === max ? 'bg-secondary' : 'bg-primary'">
|
||||||
|
<img :src="'/img/item/' + tag + (animated ? '.gif' : '.jpg')" style="max-height: 50px; max-width: 50px;" class="mr-2">
|
||||||
|
|
||||||
|
<div class="flex flex-col items-center">
|
||||||
|
<a :href="'https://gbf.wiki/' + name" target="_blank" title="Go to gbf.wiki">
|
||||||
|
{{ name }}
|
||||||
|
</a>
|
||||||
|
<div>
|
||||||
|
<stat-input
|
||||||
|
longName="Quantity"
|
||||||
|
:prop.sync="localQuantity"
|
||||||
|
:length="max.toString().length + 1"
|
||||||
|
:max="max"
|
||||||
|
:alignRight="true"
|
||||||
|
></stat-input> / {{ max }}
|
||||||
|
<fa-icon
|
||||||
|
v-if="localQuantity < max"
|
||||||
|
@click="localQuantity = max"
|
||||||
|
:icon="['fas', 'check']"
|
||||||
|
class="ml-1 cursor-pointer"
|
||||||
|
title="Max quantity"
|
||||||
|
></fa-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import StatInput from '@/components/common/StatInput.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
StatInput
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
tag: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
quantity: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
max: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
animated: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
localQuantity: {
|
||||||
|
get() {
|
||||||
|
return this.quantity;
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
this.$emit('update:quantity', value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
66
components/CalcPreviewList.vue
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-row items-center">
|
||||||
|
<a :href="'https://gbf.wiki/' + name" target="_blank" title="Go to gbf.wiki" class="mr-2">
|
||||||
|
<img :src="'/img/item/' + tag + (animated ? '.gif' : '.jpg')" style="max-height: 25px; max-width: 25px;">
|
||||||
|
{{ name }}
|
||||||
|
</a>
|
||||||
|
<div>
|
||||||
|
<stat-input
|
||||||
|
longName="Quantity"
|
||||||
|
:prop.sync="localQuantity"
|
||||||
|
:length="max.toString().length + 1"
|
||||||
|
:max="max"
|
||||||
|
:alignRight="true"
|
||||||
|
></stat-input> / {{ max }}
|
||||||
|
<fa-icon
|
||||||
|
v-if="localQuantity < max"
|
||||||
|
@click="localQuantity = max"
|
||||||
|
:icon="['fas', 'check']"
|
||||||
|
class="ml-1 cursor-pointer"
|
||||||
|
title="Max quantity"
|
||||||
|
></fa-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import StatInput from '@/components/common/StatInput.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
StatInput
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
tag: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
quantity: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
max: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
animated: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
localQuantity: {
|
||||||
|
get() {
|
||||||
|
return this.quantity;
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
this.$emit('update:quantity', value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
411
components/Calculator.vue
Normal file
@@ -0,0 +1,411 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col items-center lg:w-3/4">
|
||||||
|
<!-- Toolbar -->
|
||||||
|
<div class="flex flex-row flex-wrap gap-4 items-center">
|
||||||
|
<span>
|
||||||
|
<dropdown v-model.number="unit_index" v-if="Object.keys(getUnitToAdd).length > 0" >
|
||||||
|
<option :value="-1" disabled hidden>--- Select {{ unitsLabel }} ---</option>
|
||||||
|
<option
|
||||||
|
v-for="(target, index) in getUnitToAdd"
|
||||||
|
:key="target.summon"
|
||||||
|
:value="index"
|
||||||
|
>
|
||||||
|
{{ target.name }}
|
||||||
|
</option>
|
||||||
|
</dropdown>
|
||||||
|
<button class="btn btn-blue" @click="addUnit()" :disabled="unit_index < 0" v-if="Object.keys(getUnitToAdd).length > 0" >
|
||||||
|
Add
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<checkbox v-model="splitMats">Split materials for each step</checkbox>
|
||||||
|
<checkbox v-model="hideCompletedMats">Hide completed materials</checkbox>
|
||||||
|
<label>Display:
|
||||||
|
<dropdown v-model.number="displayList">
|
||||||
|
<option :value="0">Grid</option>
|
||||||
|
<option :value="1">List</option>
|
||||||
|
</dropdown>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Unit box -->
|
||||||
|
<div v-for="(_, unitKey) in progress" :key="unitKey" class="flex flex-col mt-8 border-4 border-secondary rounded p-1 lg:p-4 bg-tertiary w-full">
|
||||||
|
|
||||||
|
<span class="flex flex-row justify-between text-3xl font-bold">
|
||||||
|
<div></div>
|
||||||
|
<a class="cursor-pointer" @click="toggleFolded(progress[unitKey])">
|
||||||
|
{{ getUnits[unitKey].name }}
|
||||||
|
<fa-icon v-if="progress[unitKey].fold" :icon="['fas', 'angle-right']" class="ml-2"></fa-icon>
|
||||||
|
<fa-icon v-else :icon="['fas', 'angle-down']" class="ml-2"></fa-icon>
|
||||||
|
</a>
|
||||||
|
<a class="cursor-pointer" @click="removeUnit(unitKey)" title="Remove">
|
||||||
|
<fa-icon :icon="['fas', 'trash']" class="ml-2"></fa-icon>
|
||||||
|
</a>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span v-if=" ! progress[unitKey].fold" class="flex flex-col mt-6">
|
||||||
|
<!-- Options -->
|
||||||
|
<div class="flex flex-row flex-wrap self-center items-center gap-2">
|
||||||
|
<span>Completed step</span>
|
||||||
|
<dropdown v-model.number="progress[unitKey].from" @change="selectFromStep(unitKey)">
|
||||||
|
<option :value="-1">-- Nothing --</option>
|
||||||
|
<option
|
||||||
|
v-for="(step, index) in getUnitsMaterials.slice(0, -1)"
|
||||||
|
:key="step.name"
|
||||||
|
:value="index"
|
||||||
|
>
|
||||||
|
{{ step.name }}
|
||||||
|
</option>
|
||||||
|
</dropdown>
|
||||||
|
|
||||||
|
<span>Target step</span>
|
||||||
|
<dropdown v-model.number="progress[unitKey].to">
|
||||||
|
<option
|
||||||
|
v-for="(step, index) in getUnitsMaterials"
|
||||||
|
:key="step.name"
|
||||||
|
:value="index"
|
||||||
|
:disabled="progress[unitKey].from >= index"
|
||||||
|
>
|
||||||
|
{{ step.name }}
|
||||||
|
</option>
|
||||||
|
</dropdown>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Materials -->
|
||||||
|
<div v-for="(material, matKey) in getMaterialsFor(unitKey)" :key="matKey">
|
||||||
|
<h2 class="my-4">{{ material.name }}</h2>
|
||||||
|
|
||||||
|
<div v-if="displayList == 0" class="flex flex-row flex-wrap gap-1">
|
||||||
|
<calc-preview-item v-for="(item, keyItem) in filterCompletedItems(unitKey, matKey, material.items)" :key="keyItem"
|
||||||
|
:tag="item.item"
|
||||||
|
:name="item.name"
|
||||||
|
:quantity="getQuantityForItem(unitKey, matKey, item.item)"
|
||||||
|
@update:quantity="val => setQuantityForItem(unitKey, matKey, item, val)"
|
||||||
|
:max="item.max"
|
||||||
|
:animated="item.animated"
|
||||||
|
class="border-secondary border rounded"
|
||||||
|
></calc-preview-item>
|
||||||
|
</div>
|
||||||
|
<div v-else class="flex flex-col">
|
||||||
|
<calc-preview-list v-for="(item, keyItem) in filterCompletedItems(unitKey, matKey, material.items)" :key="keyItem"
|
||||||
|
:tag="item.item"
|
||||||
|
:name="item.name"
|
||||||
|
:quantity="getQuantityForItem(unitKey, matKey, item.item)"
|
||||||
|
@update:quantity="val => setQuantityForItem(unitKey, matKey, item, val)"
|
||||||
|
:max="item.max"
|
||||||
|
:animated="item.animated"
|
||||||
|
></calc-preview-list>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import supplies from '@/js/supplies'
|
||||||
|
import utils from '@/js/utils'
|
||||||
|
|
||||||
|
import Checkbox from '@/components/common/Checkbox.vue'
|
||||||
|
import Dropdown from '@/components/common/Dropdown.vue'
|
||||||
|
import CalcPreviewItem from '@/components/CalcPreviewItem.vue'
|
||||||
|
import CalcPreviewList from '@/components/CalcPreviewList.vue'
|
||||||
|
|
||||||
|
class UnitProgress {
|
||||||
|
constructor(dataLength) {
|
||||||
|
this.from = -1;
|
||||||
|
this.to = 0;
|
||||||
|
this.fold = false;
|
||||||
|
this.materials = Array.from({length: dataLength}, _ => { return {} });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sortMaterials = (lhs, rhs) => {
|
||||||
|
if (lhs.category === rhs.category) {
|
||||||
|
// Smaller quantity first
|
||||||
|
return lhs.max > rhs.max;
|
||||||
|
}
|
||||||
|
// Group by category
|
||||||
|
return lhs.category > rhs.category;
|
||||||
|
};
|
||||||
|
|
||||||
|
class MaterialStep {
|
||||||
|
constructor(name, items) {
|
||||||
|
this.name = name;
|
||||||
|
this.items = items;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Checkbox,
|
||||||
|
Dropdown,
|
||||||
|
CalcPreviewItem,
|
||||||
|
CalcPreviewList
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
// { 2040236: new UnitProgress([{chaotichaze: 0, ...}, ...]), ... }
|
||||||
|
unitsProgress: {
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
unitsData: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
unitsLabel: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
unitsSplitMats: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
unitsHideCompletedMats: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
unitsDisplayList: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
unit_index: -1,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
addUnit() {
|
||||||
|
this.$set(this.progress, this.unit_index, new UnitProgress(this.unitsData.materials.length));
|
||||||
|
this.unit_index = -1;
|
||||||
|
},
|
||||||
|
removeUnit(unitKey) {
|
||||||
|
this.$delete(this.progress, unitKey);
|
||||||
|
},
|
||||||
|
getItemProgressFor(unitKey, item, materialStep) {
|
||||||
|
let itemRefs = [];
|
||||||
|
|
||||||
|
if (item.item) {
|
||||||
|
itemRefs.push(item.item);
|
||||||
|
}
|
||||||
|
else if (item.group) {
|
||||||
|
switch (supplies.groups[item.group].type) {
|
||||||
|
case 'element': {
|
||||||
|
const tmpRef = supplies.groups[item.group][this.unitsData.units[unitKey].element];
|
||||||
|
if (tmpRef instanceof Array) {
|
||||||
|
itemRefs = tmpRef;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
itemRefs.push(tmpRef);
|
||||||
|
}
|
||||||
|
} break;
|
||||||
|
case 'summon': {
|
||||||
|
const tmpRef = supplies.groups[item.group][unitKey];
|
||||||
|
if (tmpRef instanceof Array) {
|
||||||
|
itemRefs = tmpRef;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
itemRefs.push(tmpRef);
|
||||||
|
}
|
||||||
|
} break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
console.log("Unknown type of item for:");
|
||||||
|
console.log(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
let resultArray = [];
|
||||||
|
for (let itemRef of itemRefs) {
|
||||||
|
if (this.progress[unitKey].materials[materialStep][itemRef] === undefined) {
|
||||||
|
this.$set(this.progress[unitKey].materials[materialStep], itemRef, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Round non integer values
|
||||||
|
let max = item.q / itemRefs.length;
|
||||||
|
if (item.q % itemRefs.length > 0) {
|
||||||
|
if (max < 5) max = Math.ceil(max);
|
||||||
|
else max = Math.floor(max);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (supplies.items[itemRef] === undefined) {
|
||||||
|
console.log(itemRef + ' does not exist.')
|
||||||
|
}
|
||||||
|
|
||||||
|
resultArray.push({
|
||||||
|
item: itemRef,
|
||||||
|
group: item.group,
|
||||||
|
name: supplies.items[itemRef].name,
|
||||||
|
category: supplies.items[itemRef].category,
|
||||||
|
max: max,
|
||||||
|
animated: supplies.items[itemRef].animated,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return resultArray;
|
||||||
|
},
|
||||||
|
getMaterialsFor(unitKey) {
|
||||||
|
const result = [];
|
||||||
|
|
||||||
|
// In case steps are added later
|
||||||
|
while (this.unitsData.materials.length > this.progress[unitKey].materials.length) {
|
||||||
|
this.progress[unitKey].materials.push({});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.splitMats) {
|
||||||
|
this.unitsData.materials
|
||||||
|
.slice(this.progress[unitKey].from + 1, this.progress[unitKey].to + 1)
|
||||||
|
.forEach((items, index) => {
|
||||||
|
result.push(new MaterialStep(
|
||||||
|
items.name,
|
||||||
|
Object.values(items.items
|
||||||
|
.flatMap(item => this.getItemProgressFor(unitKey, item, index + this.progress[unitKey].from + 1))
|
||||||
|
.reduce((buffer, item) => {
|
||||||
|
// Merge materials
|
||||||
|
if (buffer.hasOwnProperty(item.item)) {
|
||||||
|
buffer[item.item].max += item.max;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
buffer[item.item] = item;
|
||||||
|
}
|
||||||
|
return buffer;
|
||||||
|
}, {})
|
||||||
|
)
|
||||||
|
.sort(sortMaterials)
|
||||||
|
));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
const buffer = {};
|
||||||
|
this.unitsData.materials
|
||||||
|
.slice(this.progress[unitKey].from + 1, this.progress[unitKey].to + 1)
|
||||||
|
.forEach((items, index) => {
|
||||||
|
items.items
|
||||||
|
.flatMap(item => this.getItemProgressFor(unitKey, item, index + this.progress[unitKey].from + 1))
|
||||||
|
.forEach(item => {
|
||||||
|
// Merge materials
|
||||||
|
if (buffer.hasOwnProperty(item.item)) {
|
||||||
|
buffer[item.item].max += item.max;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
buffer[item.item] = item;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
result.push(new MaterialStep('Materials', Object.values(buffer)));
|
||||||
|
result[0].items.sort(sortMaterials);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
selectFromStep(unitKey) {
|
||||||
|
// Dropdown takes some time to update
|
||||||
|
this.$nextTick().then(() => {
|
||||||
|
if (this.progress[unitKey].from >= this.progress[unitKey].to) {
|
||||||
|
this.progress[unitKey].to = this.progress[unitKey].from + 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
filterCompletedItems(unitKey, matKey, items) {
|
||||||
|
if (this.hideCompletedMats) {
|
||||||
|
return items.filter(item => this.getQuantityForItem(unitKey, matKey, item.item) < item.max);
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
},
|
||||||
|
toggleFolded(progressForUnit) {
|
||||||
|
this.$set(progressForUnit, 'fold', ! progressForUnit.fold);
|
||||||
|
},
|
||||||
|
getQuantityForItem(unitKey, matKey, itemKey) {
|
||||||
|
if (this.splitMats) {
|
||||||
|
return this.progress[unitKey].materials[matKey + this.progress[unitKey].from + 1][itemKey];
|
||||||
|
}
|
||||||
|
return this.progress[unitKey].materials
|
||||||
|
.slice(matKey + this.progress[unitKey].from + 1, matKey + this.progress[unitKey].to + 1)
|
||||||
|
.reduce((acc, items) => {
|
||||||
|
if (items.hasOwnProperty(itemKey)) {
|
||||||
|
return acc + items[itemKey];
|
||||||
|
}
|
||||||
|
return acc;
|
||||||
|
}, 0);
|
||||||
|
},
|
||||||
|
setQuantityForItem(unitKey, matKey, item, value) {
|
||||||
|
if (this.splitMats) {
|
||||||
|
this.progress[unitKey].materials[matKey + this.progress[unitKey].from + 1][item.item] = value;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
let totalToAdd = value;
|
||||||
|
this.progress[unitKey].materials
|
||||||
|
.slice(matKey + this.progress[unitKey].from + 1, matKey + this.progress[unitKey].to + 1)
|
||||||
|
.forEach((items, index) => {
|
||||||
|
if (items.hasOwnProperty(item.item)) {
|
||||||
|
// Find max quantity of item
|
||||||
|
const foundItem = this.unitsData.materials[matKey + this.progress[unitKey].from + index + 1].items.find(i =>
|
||||||
|
i.item ? i.item == item.item : i.group == item.group
|
||||||
|
);
|
||||||
|
// Protection against bad localStorage data
|
||||||
|
if (foundItem) {
|
||||||
|
const max = foundItem.q;
|
||||||
|
const toAdd = Math.min(totalToAdd, max);
|
||||||
|
this.progress[unitKey].materials[matKey + this.progress[unitKey].from + index + 1][item.item] = toAdd;
|
||||||
|
totalToAdd -= toAdd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
getUnitToAdd() {
|
||||||
|
return utils.filterObject(this.unitsData.units, ([key, _]) => ! this.progress.hasOwnProperty(key));
|
||||||
|
},
|
||||||
|
getUnits() {
|
||||||
|
return this.unitsData.units;
|
||||||
|
},
|
||||||
|
getUnitsMaterials() {
|
||||||
|
return this.unitsData.materials;
|
||||||
|
},
|
||||||
|
localQuantity: {
|
||||||
|
get() {
|
||||||
|
return this.quantity;
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
this.$emit('update:quantity', value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
progress: {
|
||||||
|
get() {
|
||||||
|
return this.unitsProgress;
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
this.$emit('update:unitsProgress', value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
splitMats: {
|
||||||
|
get() {
|
||||||
|
return this.unitsSplitMats;
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
this.$emit('update:unitsSplitMats', value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
hideCompletedMats: {
|
||||||
|
get() {
|
||||||
|
return this.unitsHideCompletedMats;
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
this.$emit('update:unitsHideCompletedMats', value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
displayList: {
|
||||||
|
get() {
|
||||||
|
return this.unitsDisplayList;
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
this.$emit('update:unitsDisplayList', value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
40
components/GroupActions.vue
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<span class="flex flex-row justify-between items-center mt-1">
|
||||||
|
<button class="btn btn-blue" @click="clickAttack">Attack</button>
|
||||||
|
<p v-if="party_mode === $MODE.Edit" class="text-sm text-center mx-2">Uncheck <i>Edit Grid</i> to click and add skills and summons below</p>
|
||||||
|
<span class="flex flex-row">
|
||||||
|
<button class="btn btn-white mr-1" @click="clickUndo">Undo</button>
|
||||||
|
<button class="btn btn-red" @click="clickClear">Clear</button>
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<textarea class="w-full h-full appearance-none text-primary bg-primary" v-model="getActionsText" readonly></textarea>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapState } from 'vuex'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
methods: {
|
||||||
|
clickAttack() {
|
||||||
|
this.$store.commit('addActionAttack');
|
||||||
|
},
|
||||||
|
clickUndo() {
|
||||||
|
this.$store.commit('undoAction');
|
||||||
|
},
|
||||||
|
clickClear() {
|
||||||
|
this.$store.commit('clearActions');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
party_mode: state => state.party_builder.party_mode
|
||||||
|
}),
|
||||||
|
getActionsText() {
|
||||||
|
return this.$store.getters.getActionsText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
167
components/GroupCharacters.vue
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-row flex-wrap group-characters">
|
||||||
|
<box-character
|
||||||
|
v-for="(object, index) in objects"
|
||||||
|
:key="index"
|
||||||
|
:object="object"
|
||||||
|
:showLevel="showLevel"
|
||||||
|
:showRing="showRing"
|
||||||
|
@click-portrait="showModal(index)"
|
||||||
|
@click-skill="clickSkill(index, $event)"
|
||||||
|
@drag-portrait="drag($event, index)"
|
||||||
|
@swap="swap($event, index)"
|
||||||
|
></box-character>
|
||||||
|
|
||||||
|
<!-- Modal -->
|
||||||
|
<modal
|
||||||
|
v-if="party_mode !== $MODE.ReadOnly"
|
||||||
|
v-model="show_modal"
|
||||||
|
route="/party/characters"
|
||||||
|
:categories="getCategories"
|
||||||
|
@item-selected="changeObject"
|
||||||
|
></modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapState } from 'vuex'
|
||||||
|
|
||||||
|
import Utils from '@/js/utils'
|
||||||
|
|
||||||
|
import BoxCharacter from '@/components/BoxCharacter.vue'
|
||||||
|
import Modal from '@/components/ModalSelector.vue'
|
||||||
|
|
||||||
|
const CATEGORIES = [
|
||||||
|
{
|
||||||
|
name: "Name",
|
||||||
|
isColumn: true,
|
||||||
|
isFilter: false,
|
||||||
|
key: "n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Rarity",
|
||||||
|
isColumn: false,
|
||||||
|
isFilter: true,
|
||||||
|
key: "ri",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Element",
|
||||||
|
isColumn: true,
|
||||||
|
isFilter: true,
|
||||||
|
key: "e",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Type",
|
||||||
|
isColumn: true,
|
||||||
|
isFilter: true,
|
||||||
|
key: "t",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Race",
|
||||||
|
isColumn: true,
|
||||||
|
isFilter: true,
|
||||||
|
key: "ra",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Weapon",
|
||||||
|
isColumn: true,
|
||||||
|
isFilter: true,
|
||||||
|
key: "w",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
BoxCharacter,
|
||||||
|
Modal,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
showLevel: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
showRing: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
show_modal: false,
|
||||||
|
selected_box_index: 0,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
drag(ev, index) {
|
||||||
|
ev.dataTransfer.setData("character", JSON.stringify(index));
|
||||||
|
},
|
||||||
|
showModal(index) {
|
||||||
|
switch (this.party_mode) {
|
||||||
|
case this.$MODE.Action:
|
||||||
|
// TODO show warning
|
||||||
|
break;
|
||||||
|
|
||||||
|
case this.$MODE.Edit:
|
||||||
|
this.selected_box_index = index;
|
||||||
|
this.show_modal = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case this.$MODE.ReadOnly:
|
||||||
|
// Do nothing
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
changeObject(id) {
|
||||||
|
const slot = this.selected_box_index;
|
||||||
|
if (Utils.isEmpty(id)) {
|
||||||
|
this.$store.commit('setCharacter', { index: slot, data: {} });
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.axios.get('/party/characters/' + id)
|
||||||
|
.then(response => this.$store.commit('setCharacter', { index: slot, data: response.data }))
|
||||||
|
.catch(error => this.$store.dispatch('addAxiosErrorMessage', error));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
clickSkill(index, skillIndex) {
|
||||||
|
switch (this.party_mode) {
|
||||||
|
case this.$MODE.Action:
|
||||||
|
if (this.objects[index].skills[skillIndex] !== undefined) {
|
||||||
|
this.$store.commit('addActionCharacterSkill', { slot: index, index: skillIndex });
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case this.$MODE.Edit:
|
||||||
|
// TODO warning
|
||||||
|
break;
|
||||||
|
|
||||||
|
case this.$MODE.ReadOnly:
|
||||||
|
// Do nothing
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
swap(from, to) {
|
||||||
|
if (this.party_mode === this.$MODE.ReadOnly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let tmp = this.objects[from];
|
||||||
|
this.$store.commit('setCharacter', { index: from, data: this.objects[to] })
|
||||||
|
this.$store.commit('setCharacter', { index: to, data: tmp })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
objects: state => state.party_builder.characters,
|
||||||
|
party_mode: state => state.party_builder.party_mode
|
||||||
|
}),
|
||||||
|
getCategories() {
|
||||||
|
return CATEGORIES;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.group-characters > :nth-child(3) {
|
||||||
|
@apply mr-2;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
142
components/GroupClass.vue
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-row flex-wrap">
|
||||||
|
<box-class
|
||||||
|
:object="getObject"
|
||||||
|
@click-portrait="showModalClass"
|
||||||
|
@click-skill="clickSkill"
|
||||||
|
></box-class>
|
||||||
|
|
||||||
|
<!-- Modals -->
|
||||||
|
<modal
|
||||||
|
v-if="party_mode !== $MODE.ReadOnly"
|
||||||
|
v-model="show_modal_class"
|
||||||
|
route="/party/classes"
|
||||||
|
:categories="getCategoriesClass"
|
||||||
|
@item-selected="changeObject"
|
||||||
|
></modal>
|
||||||
|
<modal
|
||||||
|
v-if="party_mode !== $MODE.ReadOnly"
|
||||||
|
v-model="show_modal_skill"
|
||||||
|
route="/party/skills"
|
||||||
|
:routeParameters="'family=' + getObject.family"
|
||||||
|
:categories="getCategoriesSkill"
|
||||||
|
@item-selected="changeSkill"
|
||||||
|
></modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapState } from 'vuex'
|
||||||
|
|
||||||
|
import Utils from '@/js/utils'
|
||||||
|
|
||||||
|
import BoxClass from "@/components/BoxClass.vue";
|
||||||
|
import Modal from '@/components/ModalSelector.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
BoxClass,
|
||||||
|
Modal,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
show_modal_class: false,
|
||||||
|
show_modal_skill: false,
|
||||||
|
selected_skill_index: 0,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
showModalClass() {
|
||||||
|
switch (this.party_mode) {
|
||||||
|
case this.$MODE.Action:
|
||||||
|
// TODO show warning
|
||||||
|
break;
|
||||||
|
|
||||||
|
case this.$MODE.Edit:
|
||||||
|
this.show_modal_class = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case this.$MODE.ReadOnly:
|
||||||
|
// Do nothing
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
clickSkill(index) {
|
||||||
|
switch (this.party_mode) {
|
||||||
|
case this.$MODE.Action:
|
||||||
|
if (this.getObject.skills[index] !== null) {
|
||||||
|
// Send action
|
||||||
|
this.$store.commit('addActionMCSkill', index);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case this.$MODE.Edit:
|
||||||
|
// Show modal
|
||||||
|
if (this.getObject.skills[index] === null || ! this.getObject.skills[index].fixed) {
|
||||||
|
this.selected_skill_index = index;
|
||||||
|
this.show_modal_skill = true;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case this.$MODE.ReadOnly:
|
||||||
|
// Do nothing
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
changeObject(id) {
|
||||||
|
if (Utils.isEmpty(id)) {
|
||||||
|
this.$store.commit('setClasse', {});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.axios.get('/party/classes/' + id)
|
||||||
|
.then(response => this.$store.commit('setClasse', response.data))
|
||||||
|
.catch(error => this.$store.dispatch('addAxiosErrorMessage', error));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
changeSkill(id) {
|
||||||
|
if (Utils.isEmpty(id)) {
|
||||||
|
this.$store.commit('setClasseSkill', {slot: this.selected_skill_index, data: null})
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.axios.get('/party/skills/' + id)
|
||||||
|
.then(response => this.$store.commit('setClasseSkill', {slot: this.selected_skill_index, data: response.data}))
|
||||||
|
.catch(error => this.$store.dispatch('addAxiosErrorMessage', error));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
party_mode: state => state.party_builder.party_mode
|
||||||
|
}),
|
||||||
|
getObject() {
|
||||||
|
return this.$store.state.party_builder.classe;
|
||||||
|
},
|
||||||
|
getCategoriesClass() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: "Name",
|
||||||
|
isColumn: true,
|
||||||
|
isFilter: false,
|
||||||
|
key: "n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Row",
|
||||||
|
isColumn: true,
|
||||||
|
isFilter: true,
|
||||||
|
key: "row",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
},
|
||||||
|
getCategoriesSkill() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: "Name",
|
||||||
|
isColumn: true,
|
||||||
|
isFilter: false,
|
||||||
|
key: "n",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
14
components/GroupDescription.vue
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<template>
|
||||||
|
<textarea class="w-full h-full appearance-none text-primary bg-secondary" v-model="description"></textarea>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
computed: {
|
||||||
|
description: {
|
||||||
|
get() { return this.$store.state.party_builder.description },
|
||||||
|
set(value) { this.$store.commit('setDescription', value) }
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
374
components/GroupPartyStats.vue
Normal file
@@ -0,0 +1,374 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<!-- Options -->
|
||||||
|
<div class="flex flex-row flex-wrap items-center">
|
||||||
|
<label class="mr-2">
|
||||||
|
Enemy Element
|
||||||
|
<dropdown v-model="enemy_element">
|
||||||
|
<option value="fire">Fire</option>
|
||||||
|
<option value="wind">Wind</option>
|
||||||
|
<option value="earth">Earth</option>
|
||||||
|
<option value="water">Water</option>
|
||||||
|
<option value="light">Light</option>
|
||||||
|
<option value="dark">Dark</option>
|
||||||
|
<option value="none">None</option>
|
||||||
|
</dropdown>
|
||||||
|
</label>
|
||||||
|
<label>
|
||||||
|
HP%
|
||||||
|
<input
|
||||||
|
class="input"
|
||||||
|
style="width: 5ch;"
|
||||||
|
v-model.number="percentHP"
|
||||||
|
@keydown.arrow-up="percentHP++"
|
||||||
|
@keydown.arrow-down="percentHP--">
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Table -->
|
||||||
|
<div class="overflow-y-auto" v-if="getStatsForCharacters.length > 0">
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th></th>
|
||||||
|
<th>Elem</th>
|
||||||
|
<th><abbr title="Normal and Optimus modifiers ratio">Norm</abbr></th>
|
||||||
|
<th><abbr title="Omega">Ω</abbr></th>
|
||||||
|
<th>Ex</th>
|
||||||
|
<th><abbr title="Character-specific unique attack ratio">Chara</abbr></th>
|
||||||
|
<th><abbr title="Critical damage ratio">Crit</abbr></th>
|
||||||
|
<th><abbr title="Double attack ratio">DA</abbr></th>
|
||||||
|
<th><abbr title="Triple attack ratio">TA</abbr></th>
|
||||||
|
<!--<th><abbr title="Attack cap">Cap</abbr></th>-->
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="(chara, index) in getStatsForCharacters" :key="index">
|
||||||
|
<td class="truncate" style="max-width: 4rem;">{{ chara.name }}</td>
|
||||||
|
<td>{{ (chara.elem_atk * 100).toFixed(0) }}%</td>
|
||||||
|
<td>{{ (chara.normal_atk * 100).toFixed(0) }}%</td>
|
||||||
|
<td>{{ (chara.omega_atk * 100).toFixed(0) }}%</td>
|
||||||
|
<td>{{ (chara.ex_atk * 100).toFixed(0) }}%</td>
|
||||||
|
<td>{{ (chara.chara_atk * 100).toFixed(0) }}%</td>
|
||||||
|
<td>{{ Math.floor(chara.crit * 100) }}%</td>
|
||||||
|
<td>{{ Math.min(75, (chara.da * 100).toFixed(0)) }}%</td>
|
||||||
|
<td>{{ Math.min(75, (chara.ta * 100).toFixed(0)) }}%</td>
|
||||||
|
<!--<td>{{ ((chara.atk_cap-1) * 100).toFixed(0) }}%</td>-->
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<span class="tag bg-red-500">Warning</span>
|
||||||
|
Some things might still be missing.
|
||||||
|
Mouse over the skills and summons to see what's already implemented.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapState } from 'vuex'
|
||||||
|
|
||||||
|
import Utils from '@/js/utils.js'
|
||||||
|
import DataModel from '@/js/data-model'
|
||||||
|
|
||||||
|
import Dropdown from '@/components/common/Dropdown.vue';
|
||||||
|
|
||||||
|
const lsMgt = new Utils.LocalStorageMgt('PartyBuilder');
|
||||||
|
|
||||||
|
const RATIO_TYPES = {
|
||||||
|
atk: 0,
|
||||||
|
atk_cap: 0,
|
||||||
|
hp: 0,
|
||||||
|
da: 0,
|
||||||
|
ta: 0,
|
||||||
|
crit: 0,
|
||||||
|
ca_dmg: 0,
|
||||||
|
ca_cap: 0,
|
||||||
|
chainburst_dmg: 0,
|
||||||
|
chainburst_cap: 0,
|
||||||
|
stamina: 0,
|
||||||
|
enmity: 0,
|
||||||
|
supplemental_dmg: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
const ELEM_SUPERIORITY = {
|
||||||
|
fire: {
|
||||||
|
superior: 'wind',
|
||||||
|
weak: 'water'
|
||||||
|
},
|
||||||
|
wind: {
|
||||||
|
superior: 'earth',
|
||||||
|
weak: 'fire'
|
||||||
|
},
|
||||||
|
earth: {
|
||||||
|
superior: 'water',
|
||||||
|
weak: 'wind'
|
||||||
|
},
|
||||||
|
water: {
|
||||||
|
superior: 'fire',
|
||||||
|
weak: 'earth'
|
||||||
|
},
|
||||||
|
|
||||||
|
light: {
|
||||||
|
superior: 'dark',
|
||||||
|
weak: ''
|
||||||
|
},
|
||||||
|
dark: {
|
||||||
|
superior: 'light',
|
||||||
|
weak: ''
|
||||||
|
},
|
||||||
|
|
||||||
|
none: {
|
||||||
|
superior: '',
|
||||||
|
weak: ''
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Dropdown
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
enemy_element: "water",
|
||||||
|
// enemy_defense: 10,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getElemSuperiority(chara_element) {
|
||||||
|
if (this.enemy_element === "none") {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ELEM_SUPERIORITY[chara_element].superior === this.enemy_element) {
|
||||||
|
return 0.50;
|
||||||
|
}
|
||||||
|
if (ELEM_SUPERIORITY[chara_element].weak === this.enemy_element) {
|
||||||
|
return -0.25;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
},
|
||||||
|
getSummonAuras(chara_element) {
|
||||||
|
let auras = {
|
||||||
|
elemental: Object.assign({}, RATIO_TYPES),
|
||||||
|
normal: Object.assign({}, RATIO_TYPES),
|
||||||
|
optimus: Object.assign({}, RATIO_TYPES),
|
||||||
|
omega: Object.assign({}, RATIO_TYPES),
|
||||||
|
mysterious: Object.assign({}, RATIO_TYPES),
|
||||||
|
seraphic: Object.assign({}, RATIO_TYPES),
|
||||||
|
ranko: Object.assign({}, RATIO_TYPES),
|
||||||
|
};
|
||||||
|
|
||||||
|
[this.summons[0], this.summons[5]].forEach(summon => {
|
||||||
|
if ( ! Utils.isEmpty(summon) && summon.current_data !== undefined) {
|
||||||
|
for (let aura of summon.data[summon.current_data]) {
|
||||||
|
if (chara_element === aura.element || aura.element === "any") {
|
||||||
|
// TODO formula
|
||||||
|
auras[aura.aura_type][aura.stat] += aura.percent / 100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.summons.slice(1, 5).forEach(summon => {
|
||||||
|
if ( ! Utils.isEmpty(summon) && summon.current_data !== undefined) {
|
||||||
|
for (let aura of summon.data[summon.current_data]) {
|
||||||
|
if (aura.slot === 'sub' && (chara_element === aura.element || aura.element === "any")) {
|
||||||
|
// TODO formula
|
||||||
|
auras[aura.aura_type][aura.stat] += aura.percent / 100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
this.summons.slice(6, 8).forEach(summon => {
|
||||||
|
if ( ! Utils.isEmpty(summon) && summon.current_data !== undefined) {
|
||||||
|
for (let aura of summon.data[summon.current_data]) {
|
||||||
|
if (aura.slot === 'sub' && (chara_element === aura.element || aura.element === "any")) {
|
||||||
|
// TODO formula
|
||||||
|
auras[aura.aura_type][aura.stat] += aura.percent / 100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return auras;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
characters: state => state.party_builder.characters,
|
||||||
|
summons: state => state.party_builder.summons,
|
||||||
|
weapons: state => state.party_builder.weapons,
|
||||||
|
}),
|
||||||
|
percentHP: {
|
||||||
|
get() { return this.$store.state.party_builder.percent_HP },
|
||||||
|
set(value) { this.$store.commit('setPercentHP', value) }
|
||||||
|
},
|
||||||
|
getStatsForCharacters() {
|
||||||
|
let stats = [];
|
||||||
|
|
||||||
|
for (let c of this.characters) {
|
||||||
|
if ( ! Utils.isEmpty(c)) {
|
||||||
|
let data = {
|
||||||
|
name: c.nameen,
|
||||||
|
ratio: {
|
||||||
|
elemental: Object.assign({}, RATIO_TYPES),
|
||||||
|
normal: Object.assign({}, RATIO_TYPES),
|
||||||
|
optimus: Object.assign({}, RATIO_TYPES),
|
||||||
|
omega: Object.assign({}, RATIO_TYPES),
|
||||||
|
ex: Object.assign({}, RATIO_TYPES),
|
||||||
|
mysterious: Object.assign({}, RATIO_TYPES),
|
||||||
|
seraphic: Object.assign({}, RATIO_TYPES),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let chara_element = DataModel.e.data[c.elementid].name.toLowerCase();
|
||||||
|
// Characters with "any" element get the main weapon element
|
||||||
|
if (chara_element === "any") {
|
||||||
|
if ( ! Utils.isEmpty(this.weapons[0])) {
|
||||||
|
chara_element = DataModel.e.data[this.weapons[0].elementid].name.toLowerCase();
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
chara_element = "dark";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const chara_race = DataModel.ra.data[c.raceid].name.toLowerCase();
|
||||||
|
const chara_weapons = c.weapontypeid.flatMap(w => [DataModel.w.data[w].name.toLowerCase()]);
|
||||||
|
|
||||||
|
for (let index=0; index<this.weapons.length; index++) {
|
||||||
|
const w = this.weapons[index];
|
||||||
|
|
||||||
|
if ( ! Utils.isEmpty(w)) {
|
||||||
|
// Weapon skills
|
||||||
|
for (let skill_data of this.$store.getters.getWeaponsCurrentData[index].flat()) {
|
||||||
|
let add_value = true;
|
||||||
|
if (skill_data.restriction) {
|
||||||
|
for (let [key, value] of Object.entries(skill_data.restriction)) {
|
||||||
|
if (key === 'element') {
|
||||||
|
if ( ! value.some(v => v === chara_element)) {
|
||||||
|
add_value = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (key === 'race') {
|
||||||
|
if ( ! value.some(v => v === chara_race)) {
|
||||||
|
add_value = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (key === 'weapon') {
|
||||||
|
if ( ! value.some(v => chara_weapons.some(w => v === w))) {
|
||||||
|
add_value = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (add_value === true) {
|
||||||
|
data.ratio[skill_data.aura_type][skill_data.stat] += skill_data.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const chara_auras = this.getSummonAuras(chara_element);
|
||||||
|
const elem_sup = this.getElemSuperiority(chara_element);
|
||||||
|
|
||||||
|
data.elem_atk = (1
|
||||||
|
+ elem_sup
|
||||||
|
+ chara_auras.elemental.atk
|
||||||
|
//+ Elem EMP buffs {[Element] ATK up}
|
||||||
|
//+ Elem ATK buffs {[Element] ATK up}
|
||||||
|
//- Elem ATK debuffs {[Element] ATK down}
|
||||||
|
);
|
||||||
|
|
||||||
|
data.normal_atk =
|
||||||
|
(1
|
||||||
|
+ data.ratio.optimus.atk * (1 + chara_auras.optimus.atk)
|
||||||
|
+ data.ratio.normal.atk + data.ratio.normal.enmity + data.ratio.normal.stamina
|
||||||
|
+ chara_auras.normal.atk
|
||||||
|
//+ Norm buffs (ATK up)
|
||||||
|
//- Norm debuffs (ATK down)
|
||||||
|
) * (1
|
||||||
|
+ data.ratio.optimus.enmity * (1 + chara_auras.optimus.atk)
|
||||||
|
) * (1
|
||||||
|
+ data.ratio.optimus.stamina * (1 + chara_auras.optimus.atk)
|
||||||
|
);
|
||||||
|
data.omega_atk =
|
||||||
|
(1
|
||||||
|
+ data.ratio.omega.atk * (1 + chara_auras.omega.atk)
|
||||||
|
) * (1
|
||||||
|
+ data.ratio.omega.enmity * (1 + chara_auras.omega.atk)
|
||||||
|
) * (1
|
||||||
|
+ data.ratio.omega.stamina * (1 + chara_auras.omega.atk)
|
||||||
|
);
|
||||||
|
data.ex_atk =
|
||||||
|
(1
|
||||||
|
+ data.ratio.ex.atk
|
||||||
|
+ data.ratio.mysterious.atk * (1 + chara_auras.ranko.atk)
|
||||||
|
) * (1
|
||||||
|
+ data.ratio.ex.enmity
|
||||||
|
) * (1
|
||||||
|
+ data.ratio.ex.stamina
|
||||||
|
);
|
||||||
|
data.fixed_atk_mod = 0; //- Fixed ATK Modifiers {ex: 15% ATK cut from Qinglong Manewhip.}
|
||||||
|
|
||||||
|
data.normal_omega_ex_atk = data.normal_atk * data.omega_atk * data.ex_atk - data.fixed_atk_mod;
|
||||||
|
|
||||||
|
data.chara_atk = (1
|
||||||
|
//+ Jammed mod
|
||||||
|
//+ Enmity EMP mod
|
||||||
|
//+ Ring Enmity mod
|
||||||
|
) * (1
|
||||||
|
//+ Strength mod
|
||||||
|
//+ Stamina EMP mod
|
||||||
|
//+ Ring Stamina mod
|
||||||
|
) * (1
|
||||||
|
+ (c.haspring === true ? 0.1 : 0) // Total Char Unique ATK boosts
|
||||||
|
)
|
||||||
|
|
||||||
|
// Increase atk cap
|
||||||
|
//data.atk_cap = 1
|
||||||
|
// + Object.entries(data.ratio).reduce((acc, [key, cur]) => (key != 'seraphic') ? acc + cur.atk_cap : acc, 0)
|
||||||
|
// + Object.entries(chara_auras).reduce((acc, [key, cur]) => (key != 'seraphic') ? acc + cur.atk_cap : acc, 0);
|
||||||
|
//if (elem_sup > 0) {
|
||||||
|
// data.atk_cap = data.atk_cap * (1 + data.ratio.seraphic.atk_cap + chara_auras.seraphic.atk_cap)
|
||||||
|
//}
|
||||||
|
|
||||||
|
// Crit proba
|
||||||
|
data.crit = 0;
|
||||||
|
if (elem_sup > 0 || this.enemy_element === "none") {
|
||||||
|
data.crit = data.ratio.omega.crit * (1 + chara_auras.omega.atk)
|
||||||
|
+ data.ratio.optimus.crit * (1 + chara_auras.optimus.atk)
|
||||||
|
+ data.ratio.normal.crit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// DA/TA
|
||||||
|
data.da = data.ratio.omega.da * (1 + chara_auras.omega.atk)
|
||||||
|
+ data.ratio.optimus.da * (1 + chara_auras.optimus.atk)
|
||||||
|
+ data.ratio.normal.da;
|
||||||
|
data.ta = data.ratio.omega.ta * (1 + chara_auras.omega.atk)
|
||||||
|
+ data.ratio.optimus.ta * (1 + chara_auras.optimus.atk)
|
||||||
|
+ data.ratio.normal.ta;
|
||||||
|
|
||||||
|
stats.push(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
enemy_element() {
|
||||||
|
lsMgt.setValue('enemy_element', this);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
lsMgt.getValue(this, 'enemy_element');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
177
components/GroupSummons.vue
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-row flex-wrap gap-x-2">
|
||||||
|
<span class="flex flex-col">
|
||||||
|
<span class="bg-secondary rounded-lg text-center mb-1">Main</span>
|
||||||
|
|
||||||
|
<box-summon
|
||||||
|
:object="objects[0]"
|
||||||
|
:showLevel="showLevel"
|
||||||
|
@click-portrait="showModal(0)"
|
||||||
|
@drag-portrait="drag($event, 0)"
|
||||||
|
@swap="swap($event, 0)"
|
||||||
|
></box-summon>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="flex flex-col flex-wrap">
|
||||||
|
<div class="flex flex-row justify-around mb-1">
|
||||||
|
<span>Atk {{ getStats.atk }}</span>
|
||||||
|
<span>HP {{ getStats.hp }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-row flex-wrap" v-for="(line, lineIndex) in getIndexes" :key="lineIndex">
|
||||||
|
<span v-for="index in line" :key="index" :class="lineIndex === 1 ? 'mt-1' : ''">
|
||||||
|
<box-summon
|
||||||
|
:object="objects[index]"
|
||||||
|
:showLevel="showLevel"
|
||||||
|
@click-portrait="showModal(index)"
|
||||||
|
@drag-portrait="drag($event, index)"
|
||||||
|
@swap="swap($event, index)"
|
||||||
|
></box-summon>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="flex flex-col">
|
||||||
|
<span class="bg-secondary rounded-lg text-center mb-1">Friend</span>
|
||||||
|
|
||||||
|
<box-summon
|
||||||
|
:object="objects[5]"
|
||||||
|
:showLevel="showLevel"
|
||||||
|
@click-portrait="showModal(5)"
|
||||||
|
@drag-portrait="drag($event, 5)"
|
||||||
|
@swap="swap($event, 5)"
|
||||||
|
></box-summon>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- Sub Aura -->
|
||||||
|
<span class="flex flex-col bg-secondary rounded-lg gap-y-1">
|
||||||
|
<span class="rounded-lg text-center">Sub Aura</span>
|
||||||
|
<box-summon
|
||||||
|
:object="objects[6]"
|
||||||
|
:showLevel="showLevel"
|
||||||
|
@click-portrait="showModal(6)"
|
||||||
|
@drag-portrait="drag($event, 6)"
|
||||||
|
@swap="swap($event, 6)"
|
||||||
|
></box-summon>
|
||||||
|
<box-summon
|
||||||
|
:object="objects[7]"
|
||||||
|
:showLevel="showLevel"
|
||||||
|
@click-portrait="showModal(7)"
|
||||||
|
@drag-portrait="drag($event, 7)"
|
||||||
|
@swap="swap($event, 7)"
|
||||||
|
></box-summon>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<!-- Modal -->
|
||||||
|
<modal
|
||||||
|
v-if="party_mode !== $MODE.ReadOnly"
|
||||||
|
v-model="show_modal"
|
||||||
|
route="/party/summons"
|
||||||
|
:categories="getCategories"
|
||||||
|
@item-selected="changeObject"
|
||||||
|
></modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapState } from 'vuex'
|
||||||
|
|
||||||
|
import Utils from '@/js/utils'
|
||||||
|
|
||||||
|
import BoxSummon from "@/components/BoxSummon.vue";
|
||||||
|
import Modal from '@/components/ModalSelector.vue'
|
||||||
|
|
||||||
|
const CATEGORIES = [
|
||||||
|
{
|
||||||
|
name: "Name",
|
||||||
|
isColumn: true,
|
||||||
|
isFilter: false,
|
||||||
|
key: "n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Rarity",
|
||||||
|
isColumn: true,
|
||||||
|
isFilter: true,
|
||||||
|
key: "ri",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Element",
|
||||||
|
isColumn: true,
|
||||||
|
isFilter: true,
|
||||||
|
key: "e",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
BoxSummon,
|
||||||
|
Modal,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
showLevel: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
show_modal: false,
|
||||||
|
selected_box_index: 0,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
drag(ev, index) {
|
||||||
|
ev.dataTransfer.setData("summon", JSON.stringify(index));
|
||||||
|
},
|
||||||
|
showModal(index) {
|
||||||
|
switch (this.party_mode) {
|
||||||
|
case this.$MODE.Action:
|
||||||
|
this.$store.commit('addActionSummon', index);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case this.$MODE.Edit:
|
||||||
|
this.selected_box_index = index;
|
||||||
|
this.show_modal = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case this.$MODE.ReadOnly:
|
||||||
|
// Do nothing
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
changeObject(id) {
|
||||||
|
const slot = this.selected_box_index;
|
||||||
|
if (Utils.isEmpty(id)) {
|
||||||
|
this.$store.commit('setSummon', { index: slot, data: {} });
|
||||||
|
} else {
|
||||||
|
this.axios.get('/party/summons/' + id)
|
||||||
|
.then(response => this.$store.commit('setSummon', { index: slot, data: response.data }))
|
||||||
|
.catch(error => this.$store.dispatch('addAxiosErrorMessage', error));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
swap(from, to) {
|
||||||
|
if (this.party_mode === this.$MODE.ReadOnly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let tmp = this.objects[from];
|
||||||
|
this.$store.commit('setSummon', { index: from, data: this.objects[to] })
|
||||||
|
this.$store.commit('setSummon', { index: to, data: tmp })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
objects: state => state.party_builder.summons,
|
||||||
|
party_mode: state => state.party_builder.party_mode
|
||||||
|
}),
|
||||||
|
getStats() {
|
||||||
|
return this.$store.getters.getSummonsStats;
|
||||||
|
},
|
||||||
|
getCategories() {
|
||||||
|
return CATEGORIES;
|
||||||
|
},
|
||||||
|
getIndexes() {
|
||||||
|
return [[1, 2], [3, 4]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
53
components/GroupWeaponKeys.vue
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div v-for="(w, i) in getWeaponKeys" :key="'w'+i" class="mb-3">
|
||||||
|
<b>{{ w.name }}</b>
|
||||||
|
<ul class="list-disc ml-4">
|
||||||
|
<li v-for="(k, j) in w.keys" :key="'k'+j">{{ k }}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<br>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapState } from 'vuex'
|
||||||
|
|
||||||
|
import { LANGUAGES } from '@/js/lang'
|
||||||
|
import Utils from '@/js/utils.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
weapons: state => state.party_builder.weapons,
|
||||||
|
}),
|
||||||
|
englishNames() {
|
||||||
|
return this.$store.getters.getLang === LANGUAGES.EN;
|
||||||
|
},
|
||||||
|
getWeaponKeys() {
|
||||||
|
let keys = [];
|
||||||
|
|
||||||
|
for (const w of this.weapons) {
|
||||||
|
if ( ! Utils.isEmpty(w)) {
|
||||||
|
let weapon = {
|
||||||
|
name: this.englishNames ? w.nameen : w.namejp,
|
||||||
|
keys: []
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const k of w.keys) {
|
||||||
|
if ( ! Utils.isEmpty(k)) {
|
||||||
|
weapon.keys.push(k.desc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (weapon.keys.length > 0) {
|
||||||
|
keys.push(weapon);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return keys;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
165
components/GroupWeapons.vue
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-row flex-wrap">
|
||||||
|
<!-- Main weapon -->
|
||||||
|
<box-weapon
|
||||||
|
:object="objects[0]"
|
||||||
|
:skills="skills[0]"
|
||||||
|
:showLevel="showLevel"
|
||||||
|
@click-portrait="showModal(0)"
|
||||||
|
@drag-portrait="drag($event, 0)"
|
||||||
|
@swap="swap($event, 0)"
|
||||||
|
></box-weapon>
|
||||||
|
|
||||||
|
<!-- Grid -->
|
||||||
|
<div class="flex flex-col flex-wrap gap-y-1">
|
||||||
|
<div class="flex flex-row px-2" v-for="(line, lineIndex) in getIndexes" :key="lineIndex">
|
||||||
|
<span v-for="index in line" :key="index">
|
||||||
|
<box-weapon
|
||||||
|
:object="objects[index]"
|
||||||
|
:skills="skills[index]"
|
||||||
|
:showLevel="showLevel"
|
||||||
|
@click-portrait="showModal(index)"
|
||||||
|
@drag-portrait="drag($event, index)"
|
||||||
|
@swap="swap($event, index)"
|
||||||
|
></box-weapon>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="showArcarum" class="flex flex-row shrink bg-secondary rounded px-2 pb-1">
|
||||||
|
<span v-for="index in [10, 11, 12]" :key="index">
|
||||||
|
<box-weapon
|
||||||
|
:object="objects[index]"
|
||||||
|
:skills="skills[index]"
|
||||||
|
:showLevel="showLevel"
|
||||||
|
:isArcarum="true"
|
||||||
|
@click-portrait="showModal(index)"
|
||||||
|
@drag-portrait="drag($event, index)"
|
||||||
|
@swap="swap($event, index)"
|
||||||
|
></box-weapon>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modal -->
|
||||||
|
<modal
|
||||||
|
v-if="party_mode !== $MODE.ReadOnly"
|
||||||
|
v-model="show_modal"
|
||||||
|
route="/party/weapons"
|
||||||
|
:categories="getCategories"
|
||||||
|
@item-selected="changeObject"
|
||||||
|
></modal>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapState } from 'vuex'
|
||||||
|
|
||||||
|
import Utils from '@/js/utils'
|
||||||
|
|
||||||
|
import BoxWeapon from "@/components/BoxWeapon.vue";
|
||||||
|
import Modal from '@/components/ModalSelector.vue'
|
||||||
|
|
||||||
|
const CATEGORIES = [
|
||||||
|
{
|
||||||
|
name: "Name",
|
||||||
|
isColumn: true,
|
||||||
|
isFilter: false,
|
||||||
|
key: "n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Rarity",
|
||||||
|
isColumn: false,
|
||||||
|
isFilter: false,
|
||||||
|
key: "ri",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Element",
|
||||||
|
isColumn: true,
|
||||||
|
isFilter: true,
|
||||||
|
key: "e",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Weapon",
|
||||||
|
isColumn: true,
|
||||||
|
isFilter: true,
|
||||||
|
key: "w",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const INDEXES = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
BoxWeapon,
|
||||||
|
Modal,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
showLevel: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
showArcarum: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
show_modal: false,
|
||||||
|
selected_box_index: 0,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
drag(ev, index) {
|
||||||
|
ev.dataTransfer.setData("weapon", JSON.stringify(index));
|
||||||
|
},
|
||||||
|
showModal(index) {
|
||||||
|
switch (this.party_mode) {
|
||||||
|
case this.$MODE.Action:
|
||||||
|
// TODO show warning
|
||||||
|
break;
|
||||||
|
|
||||||
|
case this.$MODE.Edit:
|
||||||
|
this.selected_box_index = index;
|
||||||
|
this.show_modal = true;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case this.$MODE.ReadOnly:
|
||||||
|
// Do nothing
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
changeObject(id) {
|
||||||
|
const slot = this.selected_box_index;
|
||||||
|
if (Utils.isEmpty(id)) {
|
||||||
|
this.$store.commit('setWeapon', { index: slot, data: {} })
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.axios.get('/party/weapons/' + id)
|
||||||
|
.then(response => this.$store.commit('setWeapon', { index: slot, data: response.data }))
|
||||||
|
.catch(error => this.$store.dispatch('addAxiosErrorMessage', error));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
swap(from, to) {
|
||||||
|
if (this.party_mode === this.$MODE.ReadOnly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let tmp = this.objects[from];
|
||||||
|
this.$store.commit('setWeapon', { index: from, data: this.objects[to] })
|
||||||
|
this.$store.commit('setWeapon', { index: to, data: tmp })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
objects: state => state.party_builder.weapons,
|
||||||
|
skills: state => state.party_builder.weapons_skills,
|
||||||
|
party_mode: state => state.party_builder.party_mode
|
||||||
|
}),
|
||||||
|
getCategories() {
|
||||||
|
return CATEGORIES;
|
||||||
|
},
|
||||||
|
getIndexes() {
|
||||||
|
return INDEXES;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
54
components/ModalConfirm.vue
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<template>
|
||||||
|
<modal :show="show" @close="$emit('close', false)">
|
||||||
|
<template v-slot:header>
|
||||||
|
<h2>Are you sure?</h2>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<p class="my-8">
|
||||||
|
<fa-icon :icon="['fas', 'exclamation-triangle']" class="text-red-400"></fa-icon> {{ text }}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="flex flex-row justify-between">
|
||||||
|
<button class="btn btn-red" @click="confirmAction()">{{ button }}</button>
|
||||||
|
<button class="btn btn-blue" @click="$emit('close', false)">Close</button>
|
||||||
|
</div>
|
||||||
|
</modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Modal from './common/Modal.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
model: {
|
||||||
|
prop: 'show',
|
||||||
|
event: 'close'
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
Modal
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
show: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
confirm: {
|
||||||
|
type: Function,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
button: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
confirmAction() {
|
||||||
|
this.confirm();
|
||||||
|
this.$emit('close', false);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
69
components/ModalKeys.vue
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
<template>
|
||||||
|
<modal :show="show" @close="close()">
|
||||||
|
<template v-slot:header>
|
||||||
|
<h1>Select a skill key</h1>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Name</th>
|
||||||
|
<th>Key</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr @click="selectItem(null)">
|
||||||
|
<td>-</td>
|
||||||
|
<td>Remove current key</td>
|
||||||
|
</tr>
|
||||||
|
<tr
|
||||||
|
v-for="(item, index) in getData"
|
||||||
|
:key="index"
|
||||||
|
@click="selectItem(item)"
|
||||||
|
>
|
||||||
|
<td>{{ item.name }}</td>
|
||||||
|
<td>{{ item.desc }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import KeyData from '@/js/key-data.js'
|
||||||
|
import Modal from './common/Modal.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
model: {
|
||||||
|
prop: 'show',
|
||||||
|
event: 'close'
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
Modal
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
show: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
keyId: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
selectItem(item) {
|
||||||
|
this.$emit('key-selected', item);
|
||||||
|
this.close();
|
||||||
|
},
|
||||||
|
close() {
|
||||||
|
this.$emit('close', false);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
getData() {
|
||||||
|
return KeyData.data[this.keyId];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
118
components/ModalLogin.vue
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
<template>
|
||||||
|
<modal :show="show" @close="$emit('close', false)">
|
||||||
|
<template v-slot:header>
|
||||||
|
<h1 v-if="reset_password">Reset password</h1>
|
||||||
|
<h1 v-else>Login</h1>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Reset password -->
|
||||||
|
<form v-if="reset_password" @submit.prevent="resetPassword()" class="m-1">
|
||||||
|
<p class="p-4 bg-secondary rounded">
|
||||||
|
Which email address did you link your account to?<br>
|
||||||
|
We'll send you an email with a link to reset your password.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="mt-4">
|
||||||
|
<label for="email" class="">Email address</label>
|
||||||
|
<input class="input w-full" id="email" ref="email" type="email" placeholder="Email address" v-model="email" required autofocus>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="mt-4">
|
||||||
|
<!-- Honeypot -->
|
||||||
|
<input type="checkbox" name="captcha" v-model="captcha" style="display:none !important" tabindex="-1" autocomplete="off">
|
||||||
|
<a class="cursor-pointer" @click="reset_password = false">Back to login</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<input class="btn btn-blue mt-6" type="submit" value="Send Email">
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<!-- Login -->
|
||||||
|
<form v-else @submit.prevent="doLogin()" class="m-1">
|
||||||
|
<div>
|
||||||
|
<label for="username" class="">Username</label>
|
||||||
|
<input class="input w-full" id="username" ref="username" type="text" placeholder="Username" v-model="username" required autofocus>
|
||||||
|
</div>
|
||||||
|
<div class="pb-2">
|
||||||
|
<label for="password">Password</label>
|
||||||
|
<input class="input w-full" id="password" ref="password" placeholder="Password" type="password" minlength="1" required>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p class="mt-4">
|
||||||
|
<a class="cursor-pointer" @click="reset_password = true">Forgot password?</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<input class="btn btn-blue mt-6" type="submit" value="Login">
|
||||||
|
</form>
|
||||||
|
</modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Modal from './common/Modal.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
model: {
|
||||||
|
prop: 'show',
|
||||||
|
event: 'close'
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
Modal
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
show: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
username: "",
|
||||||
|
email: "",
|
||||||
|
reset_password: false,
|
||||||
|
captcha: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
doLogin() {
|
||||||
|
const body = {
|
||||||
|
username: this.username,
|
||||||
|
password: this.$refs.password.value,
|
||||||
|
}
|
||||||
|
|
||||||
|
this.axios.post('/user/login', body)
|
||||||
|
.then(response => {
|
||||||
|
this.$emit('close', false);
|
||||||
|
this.$emit('user-logged', this.username, response.data.data.userid);
|
||||||
|
})
|
||||||
|
.catch(error => this.$store.dispatch('addAxiosErrorMessage', error));
|
||||||
|
},
|
||||||
|
resetPassword() {
|
||||||
|
const body = {
|
||||||
|
email: this.email,
|
||||||
|
captcha: this.captcha
|
||||||
|
}
|
||||||
|
|
||||||
|
this.reset_password = false;
|
||||||
|
this.email = "";
|
||||||
|
this.axios.post('/user/sendreset', body)
|
||||||
|
.then(response => this.$store.dispatch('addMessage', {message: 'Email sent'}))
|
||||||
|
.catch(error => this.$store.dispatch('addAxiosErrorMessage', error));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.username = this.$store.getters.getUsername;
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
show() {
|
||||||
|
if (this.show === true) {
|
||||||
|
let self = this;
|
||||||
|
this.$nextTick().then(() => {
|
||||||
|
self.$refs.username.focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'$store.getters.getUsername'() {
|
||||||
|
this.username = this.$store.getters.getUsername;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
176
components/ModalSelector.vue
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
<template>
|
||||||
|
<modal :show="show" @close="close()">
|
||||||
|
<template v-slot:header>
|
||||||
|
<span class="flex flex-row flex-wrap items-center">
|
||||||
|
<input class="input input-sm" placeholder="Name" ref="nameField" type="text" v-model="name_searched">
|
||||||
|
<button class="btn btn-sm btn-red mx-2" @click="name_searched = ''">Clear</button>
|
||||||
|
|
||||||
|
<data-filter
|
||||||
|
class="my-2 mr-2"
|
||||||
|
v-for="(category, index) in getFilters"
|
||||||
|
:key="index"
|
||||||
|
:category="category.name"
|
||||||
|
:data="data_model[category.key].data">
|
||||||
|
</data-filter>
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div v-if="message.length < 1">
|
||||||
|
Loading...
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<table class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th v-for="(category, index) in getColumns" :key="index" class="whitespace-nowrap">{{ category.name }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-if="canUnselect" @click="selectItem(null)">
|
||||||
|
<td v-for="index in getColumns.length" :key="index">-</td>
|
||||||
|
</tr>
|
||||||
|
<tr v-for="item in getData" :key="item.id" @click="selectItem(item.id)">
|
||||||
|
<td v-for="(val, index) in getColumn(item)" :key="index">{{ val }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template v-slot:footer>
|
||||||
|
{{ getResultsCount }}
|
||||||
|
</template>
|
||||||
|
</modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import DataModel from '@/js/data-model'
|
||||||
|
import Utils from '@/js/utils'
|
||||||
|
|
||||||
|
import Modal from './common/Modal.vue'
|
||||||
|
import DataFilter from './common/DataFilter.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
model: {
|
||||||
|
prop: 'show',
|
||||||
|
event: 'close'
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
Modal,
|
||||||
|
DataFilter
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
show: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
route: {
|
||||||
|
type: String,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
routeParameters: {
|
||||||
|
type: String,
|
||||||
|
default: undefined
|
||||||
|
},
|
||||||
|
categories: {
|
||||||
|
type: Array,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
dataModel: {
|
||||||
|
type: Object,
|
||||||
|
default: undefined
|
||||||
|
},
|
||||||
|
canUnselect: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
message: [],
|
||||||
|
previousRoute: '',
|
||||||
|
data_model: {},
|
||||||
|
name_searched: '',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
selectItem(id) {
|
||||||
|
this.$emit('item-selected', id);
|
||||||
|
this.close();
|
||||||
|
},
|
||||||
|
close() {
|
||||||
|
this.$emit('close', false);
|
||||||
|
},
|
||||||
|
getColumn(item) {
|
||||||
|
return this.getColumns.map(c => {
|
||||||
|
return this.data_model[c.key].expand(item, this.getLang);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
getData() {
|
||||||
|
return this.message.filter(item => {
|
||||||
|
return item.n.toLowerCase().includes(this.name_searched.toLowerCase()) &&
|
||||||
|
this.getFilters.every(f => {
|
||||||
|
return this.data_model[f.key].show(item[f.key])
|
||||||
|
});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
getResultsCount() {
|
||||||
|
if (this.getData.length == 1) {
|
||||||
|
return this.getData.length + ' result'
|
||||||
|
}
|
||||||
|
return this.getData.length + ' results'
|
||||||
|
},
|
||||||
|
getColumns() {
|
||||||
|
return this.categories.filter(c => { return c.isColumn });
|
||||||
|
},
|
||||||
|
getFilters() {
|
||||||
|
return this.categories.filter(c => { return c.isFilter });
|
||||||
|
},
|
||||||
|
getLang() {
|
||||||
|
return this.$store.getters.getLang;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
show() {
|
||||||
|
if (this.show) {
|
||||||
|
let currentRoute = this.route;
|
||||||
|
if (this.routeParameters !== undefined) {
|
||||||
|
currentRoute += '?' + this.routeParameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch the message again if the route changed
|
||||||
|
if (this.previousRoute !== currentRoute) {
|
||||||
|
this.message = [];
|
||||||
|
}
|
||||||
|
this.previousRoute = currentRoute;
|
||||||
|
|
||||||
|
if (this.message.length === 0) {
|
||||||
|
this.axios.get(currentRoute)
|
||||||
|
.then(response => this.message = response.data)
|
||||||
|
.catch(error => this.$store.dispatch('addAxiosErrorMessage', error));
|
||||||
|
}
|
||||||
|
|
||||||
|
let self = this;
|
||||||
|
this.$nextTick().then(() => {
|
||||||
|
self.name_searched = '';
|
||||||
|
self.$refs.nameField.focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
// Copy the data model locally to modify "checked" properties
|
||||||
|
this.categories.forEach(c => {
|
||||||
|
this.$set(this.data_model, c.key, Utils.copy(DataModel[c.key]));
|
||||||
|
});
|
||||||
|
|
||||||
|
// Overload data model if specified
|
||||||
|
if (this.dataModel !== undefined) {
|
||||||
|
for (let [key, data] of Object.entries(this.dataModel)) {
|
||||||
|
this.$set(this.data_model, key, data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
105
components/ModalSignup.vue
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
<template>
|
||||||
|
<modal :show="show" @close="$emit('close', false)">
|
||||||
|
<template v-slot:header>
|
||||||
|
<h1>Create an account</h1>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<form @submit.prevent="doLogin()" class="m-1">
|
||||||
|
<div>
|
||||||
|
<label for="username" class="">Username</label>
|
||||||
|
<input class="input w-full" id="username" ref="username" type="text" placeholder="Username" v-model="username" required autofocus>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="password">Password</label>
|
||||||
|
<input class="input w-full" id="password" ref="password" placeholder="Password" type="password" minlength="1" required>
|
||||||
|
</div>
|
||||||
|
<div class="pb-2">
|
||||||
|
<label for="password2">Confirm password</label>
|
||||||
|
<input class="input w-full" id="password2" ref="password2" placeholder="Password" type="password" minlength="1" required>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label for="email" class="">Email address (optional)</label>
|
||||||
|
<input class="input w-full" id="email" ref="email" type="email" placeholder="Email address" v-model="email">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p v-if="error_message.length > 0">
|
||||||
|
<fa-icon :icon="['fas', 'exclamation-triangle']" class="text-red-400"></fa-icon>
|
||||||
|
{{ error_message }}
|
||||||
|
</p>
|
||||||
|
<p v-else> </p>
|
||||||
|
|
||||||
|
<!-- Honeypot -->
|
||||||
|
<input type="checkbox" name="captcha" v-model="captcha" style="display:none !important" tabindex="-1" autocomplete="off">
|
||||||
|
|
||||||
|
<input class="btn btn-blue pt-2" type="submit" value="Create">
|
||||||
|
</form>
|
||||||
|
</modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Modal from './common/Modal.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
model: {
|
||||||
|
prop: 'show',
|
||||||
|
event: 'close'
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
Modal
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
show: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
username: "",
|
||||||
|
email: "",
|
||||||
|
error_message: "",
|
||||||
|
captcha: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
doLogin() {
|
||||||
|
this.error_message = "";
|
||||||
|
|
||||||
|
if (this.$refs.password.value != this.$refs.password2.value) {
|
||||||
|
this.error_message = "Passwords don't match";
|
||||||
|
this.$refs.password.value = "";
|
||||||
|
this.$refs.password2.value = "";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (/([\x00-\x1F\x7F]|\s)/.test(this.username)) {
|
||||||
|
this.error_message = "Username doesn't match requirements (no whitespace or control characters)";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = {
|
||||||
|
username: this.username,
|
||||||
|
email: this.email,
|
||||||
|
password: this.$refs.password.value,
|
||||||
|
captcha: this.captcha
|
||||||
|
}
|
||||||
|
|
||||||
|
this.axios.post('/user/register', body)
|
||||||
|
.then(response => {
|
||||||
|
this.$emit('close', false);
|
||||||
|
this.$emit('user-logged', this.username, response.data.data.userid);
|
||||||
|
})
|
||||||
|
.catch(error => this.$store.dispatch('addAxiosErrorMessage', error));
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
show() {
|
||||||
|
if (this.show === true) {
|
||||||
|
let self = this;
|
||||||
|
this.$nextTick().then(() => {
|
||||||
|
self.$refs.username.focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
116
components/ModalTeamPreview.vue
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
<template>
|
||||||
|
<modal :show="show" :large="true" @close="close()">
|
||||||
|
<template v-slot:header>
|
||||||
|
<div v-if=" ! loading" class="columns-2 md:columns-3 grow">
|
||||||
|
<h3 class="w-1/2 truncate hidden md:block md:w-32 lg:w-64 xl:w-96" :title="party_name">{{ party_name }}</h3>
|
||||||
|
|
||||||
|
<a :href="'/builder?p=' + teamId" class="text-center">Open in Party Builder</a>
|
||||||
|
|
||||||
|
<like-button :teamId="teamId"></like-button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div v-if="loading" class="text-center">
|
||||||
|
Loading...
|
||||||
|
</div>
|
||||||
|
<div v-else class="flex flex-col justify-center items-center flex-wrap gap-2">
|
||||||
|
<div class="flex flex-row justify-center items-start flex-wrap gap-2">
|
||||||
|
<group-class></group-class>
|
||||||
|
<group-characters :showLevel="false" :showRing="true"></group-characters>
|
||||||
|
<group-summons :showLevel="false"></group-summons>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-row flex-wrap justify-center items-start gap-2">
|
||||||
|
<group-weapons :showLevel="false" :showArcarum="true"></group-weapons>
|
||||||
|
<span class="flex flex-col w-full self-stretch md:w-128">
|
||||||
|
<h3 v-if="video">YouTube video:</h3>
|
||||||
|
<a v-if="video" :href="videoURL" target="_blank">{{ videoURL }}</a>
|
||||||
|
|
||||||
|
<h3 v-if="description">Description</h3>
|
||||||
|
<textarea v-if="description" class="self-stretch h-full w-full md:w-128 appearance-none text-primary bg-primary" v-model="description" readonly></textarea>
|
||||||
|
|
||||||
|
<h3 v-if="getActionsText">Actions</h3>
|
||||||
|
<textarea v-if="getActionsText" class="self-stretch h-full w-full md:w-128 appearance-none text-primary bg-primary" v-model="getActionsText" readonly></textarea>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapState, mapGetters } from 'vuex'
|
||||||
|
import Utils from '@/js/utils.js'
|
||||||
|
|
||||||
|
import LikeButton from '@/components/common/LikeButton.vue'
|
||||||
|
import Modal from './common/Modal.vue'
|
||||||
|
import GroupClass from "@/components/GroupClass.vue";
|
||||||
|
import GroupCharacters from "@/components/GroupCharacters.vue";
|
||||||
|
import GroupSummons from "@/components/GroupSummons.vue";
|
||||||
|
import GroupWeapons from "@/components/GroupWeapons.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
model: {
|
||||||
|
prop: 'show',
|
||||||
|
event: 'close'
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
LikeButton,
|
||||||
|
Modal,
|
||||||
|
GroupClass,
|
||||||
|
GroupCharacters,
|
||||||
|
GroupSummons,
|
||||||
|
GroupWeapons
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
show: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
teamId: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
loading: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
close() {
|
||||||
|
this.$emit('close', false);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
party_name: state => state.party_builder.party_name,
|
||||||
|
description: state => state.party_builder.description,
|
||||||
|
video: state => state.party_builder.video_id,
|
||||||
|
}),
|
||||||
|
...mapGetters([
|
||||||
|
'getActionsText'
|
||||||
|
]),
|
||||||
|
videoURL() {
|
||||||
|
if (this.video) {
|
||||||
|
return 'https://www.youtube.com/watch?v=' + this.video;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
teamId() {
|
||||||
|
if (this.teamId > 0) {
|
||||||
|
this.loading = true;
|
||||||
|
this.axios.get('/party/load/' + this.teamId)
|
||||||
|
.then(response => {
|
||||||
|
let res = Utils.getPartyResponse(response);
|
||||||
|
res.party_mode = this.$MODE.ReadOnly;
|
||||||
|
this.$store.dispatch('loadParty', res);
|
||||||
|
this.loading = false;
|
||||||
|
})
|
||||||
|
.catch(error => this.$store.dispatch('addAxiosErrorMessage', error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
58
components/ModalWikiURL.vue
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
<template>
|
||||||
|
<modal :show="show" @close="$emit('close', false)">
|
||||||
|
<template v-slot:header>
|
||||||
|
<h2>Please copy your gbf.wiki collection URL below</h2>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<form @submit.prevent="getURL()" class="m-1">
|
||||||
|
<div class="mb-2">
|
||||||
|
<label for="url" class="">URL</label>
|
||||||
|
<input class="input w-full" id="url" ref="url" type="text" placeholder="URL" v-model="url" required autofocus>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input class="btn btn-blue pt-2" type="submit" value="Import">
|
||||||
|
</form>
|
||||||
|
</modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Modal from './common/Modal.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
model: {
|
||||||
|
prop: 'show',
|
||||||
|
event: 'close'
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
Modal
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
show: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
url: "",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getURL() {
|
||||||
|
this.$emit('import', this.url);
|
||||||
|
this.$emit('close', false);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
show() {
|
||||||
|
if (this.show === true) {
|
||||||
|
let self = this;
|
||||||
|
this.$nextTick().then(() => {
|
||||||
|
self.url = '';
|
||||||
|
self.$refs.url.focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
130
components/ModalYoutube.vue
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
<template>
|
||||||
|
<modal :show="show" @close="$emit('close', false)">
|
||||||
|
<template v-slot:header>
|
||||||
|
<h2>Add a YouTube video</h2>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<form @submit.prevent="fetchVideo()" class="flex flex-row m-1 gap-x-4">
|
||||||
|
<label class="flex flex-row whitespace-nowrap items-center gap-x-2 grow">URL
|
||||||
|
<input class="input w-full" ref="url" type="text" placeholder="YouTube URL" v-model="input_url" required autofocus>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<input class="btn btn-blue pt-2" type="submit" value="Fetch">
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div class="flex justify-center" style="height: 360px">
|
||||||
|
{{ error_message }}
|
||||||
|
<div id="player"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input class="btn btn-blue pt-2" type="button" value="Add" @click="addVideo()">
|
||||||
|
<input v-if="url" class="btn btn-red pt-2" type="button" value="Remove" @click="removeVideo()">
|
||||||
|
</modal>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Modal from './common/Modal.vue'
|
||||||
|
|
||||||
|
// 2. This code loads the IFrame Player API code asynchronously.
|
||||||
|
if (VUE_ENV !== 'server') {
|
||||||
|
let tag = document.createElement('script');
|
||||||
|
tag.src = "https://www.youtube.com/iframe_api";
|
||||||
|
let firstScriptTag = document.getElementsByTagName('script')[0];
|
||||||
|
firstScriptTag.parentNode.insertBefore(tag, firstScriptTag);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default {
|
||||||
|
model: {
|
||||||
|
prop: 'show',
|
||||||
|
event: 'close'
|
||||||
|
},
|
||||||
|
components: {
|
||||||
|
Modal
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
show: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
url: {
|
||||||
|
type: String
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
input_url: "",
|
||||||
|
video_id: null,
|
||||||
|
player: null,
|
||||||
|
error_message: "",
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
fetchVideo() {
|
||||||
|
if (this.input_url.length > 0) {
|
||||||
|
this.video_id = this.getYouTubeId(this.input_url);
|
||||||
|
if (this.video_id == null) {
|
||||||
|
this.error_message = "Invalid URL";
|
||||||
|
this.closePlayer();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.error_message = "";
|
||||||
|
|
||||||
|
if (this.player === null) {
|
||||||
|
this.player = new YT.Player('player', {
|
||||||
|
height: '360',
|
||||||
|
width: '640',
|
||||||
|
videoId: this.video_id,
|
||||||
|
rel: 0,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.player.cueVideoById(this.video_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getYouTubeId(url) {
|
||||||
|
const reg = /^(https?:)?(\/\/)?((www\.|m\.)?youtube(-nocookie)?\.com\/((watch)?\?(feature=\w*&)?vi?=|embed\/|vi?\/|e\/)|youtu.be\/)([\w\-]{10,20})/i
|
||||||
|
const match = url.match(reg);
|
||||||
|
if (match) {
|
||||||
|
return match[9];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
addVideo() {
|
||||||
|
this.closePlayer();
|
||||||
|
this.video_id = this.getYouTubeId(this.input_url);
|
||||||
|
this.$emit('add', this.video_id);
|
||||||
|
this.$emit('close', false);
|
||||||
|
},
|
||||||
|
removeVideo() {
|
||||||
|
this.video_id = null;
|
||||||
|
this.addVideo();
|
||||||
|
},
|
||||||
|
closePlayer() {
|
||||||
|
if (this.player) {
|
||||||
|
this.player.destroy();
|
||||||
|
this.player = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
show() {
|
||||||
|
if (this.show === true) {
|
||||||
|
if (this.url) {
|
||||||
|
this.video_id = this.url;
|
||||||
|
this.input_url = "https://www.youtube.com/watch?v=" + this.url;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.video_id = null;
|
||||||
|
this.input_url = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
let self = this;
|
||||||
|
this.$nextTick().then(() => {
|
||||||
|
self.$refs.url.focus();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
352
components/Parties.vue
Normal file
@@ -0,0 +1,352 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<div class="flex flex-row flex-wrap items-center gap-2" v-if="isUserLogged">
|
||||||
|
<button class="btn btn-red" @click="new_party_modal = true">
|
||||||
|
<fa-icon :icon="['fas', 'file']" class="text-xl"></fa-icon> New Party
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-blue" @click="show_parties_modal = true">
|
||||||
|
<fa-icon :icon="['fas', 'folder-open']" class="text-xl"></fa-icon> Load…
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-blue" @click="clickPartySave(current_party)" :disabled=" ! isMyParty">
|
||||||
|
<fa-icon :icon="['fas', 'save']" class="text-xl"></fa-icon> Save
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-blue" @click="save_as_modal = true">
|
||||||
|
<fa-icon :icon="['fas', 'file-pen']" class="text-xl"></fa-icon> Save As…
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-red" @click="delete_party_modal = true" :disabled=" ! isMyParty">
|
||||||
|
<fa-icon :icon="['fas', 'trash']" class="text-xl"></fa-icon> Delete
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-blue" @click="add_video_modal = true" :disabled=" ! isMyParty">
|
||||||
|
<fa-icon :icon="['fab', 'youtube']" class="text-xl"></fa-icon> Add Video
|
||||||
|
</button>
|
||||||
|
<!-- TODO Twitter, Facebook -->
|
||||||
|
<button class="btn btn-blue" @click="clickPartyShare()" :disabled=" ! isMyParty">
|
||||||
|
<fa-icon :icon="['fas', 'share-alt']" class="text-xl"></fa-icon> Share
|
||||||
|
</button>
|
||||||
|
<span v-if="current_party === null">(unsaved)</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-row flex-wrap items-center gap-2">
|
||||||
|
<input class="input w-52" type="text" placeholder="Party name" v-model="party_name" maxlength="64">
|
||||||
|
<label>
|
||||||
|
Category
|
||||||
|
<content-categories v-model.number="content"></content-categories>
|
||||||
|
</label>
|
||||||
|
<checkbox
|
||||||
|
v-if="isUserLogged"
|
||||||
|
v-model="isPublic"
|
||||||
|
:disabled="cannotBePublic"
|
||||||
|
:title="cannotBePublic ? 'Parties uncategorized or without a main weapon cannot be made public' : 'Make this team visible in the Teams section'"
|
||||||
|
>
|
||||||
|
Public Team
|
||||||
|
</checkbox>
|
||||||
|
<a :href="'https://www.youtube.com/watch?v=' + video_id" target="_blank" v-if="video_id" title="Open YouTube video">
|
||||||
|
<fa-icon :icon="['fab', 'youtube']" class="text-4xl"></fa-icon>
|
||||||
|
</a>
|
||||||
|
<like-button :teamId="this.current_party"></like-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Modals -->
|
||||||
|
<modal-selector
|
||||||
|
v-model="show_parties_modal"
|
||||||
|
route="/party/list"
|
||||||
|
:categories="getCategories"
|
||||||
|
:canUnselect="false"
|
||||||
|
@item-selected="loadPartyFromModal"
|
||||||
|
:key="reload_route"
|
||||||
|
></modal-selector>
|
||||||
|
<modal-confirm
|
||||||
|
v-model="new_party_modal"
|
||||||
|
:confirm="clickPartyNew"
|
||||||
|
text="This will clear the current party and start a new one."
|
||||||
|
button="New Party"
|
||||||
|
></modal-confirm>
|
||||||
|
<modal-confirm
|
||||||
|
v-model="save_as_modal"
|
||||||
|
:confirm="clickPartySave"
|
||||||
|
:text="'This will create a new ' + (this.isPublic ? 'public' : 'private') + ' Party' + (this.party_name ? ' called \'' + this.party_name + '\'.' : ' with no name.')"
|
||||||
|
button="Save new Party"
|
||||||
|
></modal-confirm>
|
||||||
|
<modal-confirm
|
||||||
|
v-model="delete_party_modal"
|
||||||
|
:confirm="clickPartyDelete"
|
||||||
|
text="This will permanently delete this party from your account."
|
||||||
|
button="Delete Party"
|
||||||
|
></modal-confirm>
|
||||||
|
<modal-youtube
|
||||||
|
v-model="add_video_modal"
|
||||||
|
:url="video_id"
|
||||||
|
@add="addVideo"
|
||||||
|
></modal-youtube>
|
||||||
|
|
||||||
|
<input v-show="clipboard_text.length > 0" id="clipboardInput" readonly type="text" :value="clipboard_text">
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapState } from 'vuex'
|
||||||
|
|
||||||
|
import Utils from '@/js/utils.js'
|
||||||
|
|
||||||
|
import Checkbox from '@/components/common/Checkbox.vue'
|
||||||
|
import ContentCategories from '@/components/common/ContentCategories.vue'
|
||||||
|
import Dropdown from '@/components/common/Dropdown.vue'
|
||||||
|
import LikeButton from '@/components/common/LikeButton.vue'
|
||||||
|
import ModalConfirm from '@/components/ModalConfirm.vue'
|
||||||
|
import ModalSelector from '@/components/ModalSelector.vue'
|
||||||
|
import ModalYoutube from '@/components/ModalYoutube.vue'
|
||||||
|
|
||||||
|
const BOOKMARKLET_VERSION = 6;
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Checkbox,
|
||||||
|
ContentCategories,
|
||||||
|
Dropdown,
|
||||||
|
LikeButton,
|
||||||
|
ModalConfirm,
|
||||||
|
ModalSelector,
|
||||||
|
ModalYoutube,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
clipboard_text: '',
|
||||||
|
new_party_modal: false,
|
||||||
|
save_as_modal: false,
|
||||||
|
delete_party_modal: false,
|
||||||
|
show_parties_modal: false,
|
||||||
|
add_video_modal: false,
|
||||||
|
reload_route: 0,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
clickPartyNew() {
|
||||||
|
this.$store.commit('resetParty');
|
||||||
|
this.cleanURL();
|
||||||
|
},
|
||||||
|
cleanURL() {
|
||||||
|
if (VUE_ENV !== 'server') {
|
||||||
|
history.pushState(null, null, window.location.origin + window.location.pathname);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
loadPartyFromModal(party) {
|
||||||
|
this.axios.get('/party/load/' + party)
|
||||||
|
.then(response => this.loadParty(Utils.getPartyResponse(response)))
|
||||||
|
.catch(error => this.$store.dispatch('addAxiosErrorMessage', error));
|
||||||
|
},
|
||||||
|
clickPartyShare() {
|
||||||
|
const text = '?p=' + this.current_party + '#';
|
||||||
|
|
||||||
|
this.clipboard_text = window.location.href.split('?')[0] + text;
|
||||||
|
// history.pushState(null, null, text);
|
||||||
|
|
||||||
|
let self = this;
|
||||||
|
this.$nextTick().then(() => {
|
||||||
|
const input = document.getElementById("clipboardInput");
|
||||||
|
input.select();
|
||||||
|
document.execCommand("copy");
|
||||||
|
self.clipboard_text = '';
|
||||||
|
|
||||||
|
self.$store.dispatch('addMessage', {message: 'URL copied to clipboard'});
|
||||||
|
});
|
||||||
|
},
|
||||||
|
clickPartyDelete() {
|
||||||
|
if (this.current_party) {
|
||||||
|
this.axios.post('/party/delete', {id: this.current_party})
|
||||||
|
.then(() => {
|
||||||
|
this.clickPartyNew();
|
||||||
|
this.reloadParties();
|
||||||
|
this.$store.dispatch('addMessage', {message: 'Party deleted successfully'});
|
||||||
|
})
|
||||||
|
.catch(error => this.$store.dispatch('addAxiosErrorMessage', error));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
addVideo(id) {
|
||||||
|
this.video_id = (id && id.length > 0) ? id : null;
|
||||||
|
},
|
||||||
|
clickPartySave(partyId = null) {
|
||||||
|
let data = {
|
||||||
|
classe: this.classe.classid,
|
||||||
|
class_skills: Utils.isEmpty(this.classe.skills) ? null : this.classe.skills.map(s => {return s ? s.skillid : null}), // Skill ids
|
||||||
|
characters: this.characters.map(e => { return Utils.isEmpty(e) ? null : e.characterid }),
|
||||||
|
characters_stars: this.characters.map(e => { return Utils.isEmpty(e) ? null : e.stars }),
|
||||||
|
characters_levels: this.characters.map(e => { return Utils.isEmpty(e) ? null : e.level }),
|
||||||
|
characters_pluses: this.characters.map(e => { return Utils.isEmpty(e) ? null : e.pluses }),
|
||||||
|
characters_prings: this.characters.map(e => { return Utils.isEmpty(e) ? null : e.haspring }),
|
||||||
|
summons: this.summons.map(e => { return Utils.isEmpty(e) ? null : e.summonid }),
|
||||||
|
summons_levels: this.summons.map(e => { return Utils.isEmpty(e) ? null : e.level }),
|
||||||
|
summons_pluses: this.summons.map(e => { return Utils.isEmpty(e) ? null : e.pluses }),
|
||||||
|
summons_stars: this.summons.map(e => { return Utils.isEmpty(e) ? null : e.stars }),
|
||||||
|
weapons: this.weapons.map(e => { return Utils.isEmpty(e) ? null : e.weaponid }),
|
||||||
|
weapons_levels: this.weapons.map(e => { return Utils.isEmpty(e) ? null : e.level }),
|
||||||
|
weapons_pluses: this.weapons.map(e => { return Utils.isEmpty(e) ? null : e.pluses }),
|
||||||
|
weapons_skill_levels: this.weapons.map(e => { return Utils.isEmpty(e) ? null : e.sklevel }),
|
||||||
|
weapons_skill_names: this.weapons.map(e => { return Utils.isEmpty(e) ? null : e.keys.map(k => k ? k.name : null) }),
|
||||||
|
weapons_stars: this.weapons.map(e => { return Utils.isEmpty(e) ? null : e.stars }),
|
||||||
|
actions: this.actions.map(e => { return [e.sourceSlot-1, e.skillSlot-1, e.type] }),
|
||||||
|
}
|
||||||
|
|
||||||
|
this.axios.post('/party/save', {
|
||||||
|
id: partyId,
|
||||||
|
name: this.party_name,
|
||||||
|
data: data,
|
||||||
|
content: this.content,
|
||||||
|
isPublic: this.isPublic,
|
||||||
|
desc: this.description,
|
||||||
|
video: this.video_id,
|
||||||
|
})
|
||||||
|
.then(response => {
|
||||||
|
this.current_party = response.data.id;
|
||||||
|
this.team_owner = this.$store.getters.getUserId;
|
||||||
|
this.reloadParties();
|
||||||
|
this.$store.dispatch('addMessage', {message: 'Party saved successfully'});
|
||||||
|
})
|
||||||
|
.catch(error => this.$store.dispatch('addAxiosErrorMessage', error));
|
||||||
|
},
|
||||||
|
reloadParties() {
|
||||||
|
this.reload_route++;
|
||||||
|
},
|
||||||
|
loadParty(data) {
|
||||||
|
// Clean URL when party id changes
|
||||||
|
if ( ! Utils.isEmpty(this.$route.query.p)) {
|
||||||
|
const param = parseInt(this.$route.query.p, 10);
|
||||||
|
if (data.id !== param) {
|
||||||
|
this.cleanURL();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$store.dispatch('loadParty', data);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
classe: state => state.party_builder.classe,
|
||||||
|
characters: state => state.party_builder.characters,
|
||||||
|
summons: state => state.party_builder.summons,
|
||||||
|
weapons: state => state.party_builder.weapons,
|
||||||
|
actions: state => state.party_builder.actions,
|
||||||
|
description: state => state.party_builder.description,
|
||||||
|
}),
|
||||||
|
isUserLogged() {
|
||||||
|
return this.$store.getters.getUserId !== null;
|
||||||
|
},
|
||||||
|
content: {
|
||||||
|
get() { return this.$store.state.party_builder.content },
|
||||||
|
set(value) { this.$store.commit('setContent', value) }
|
||||||
|
},
|
||||||
|
isPublic: {
|
||||||
|
get() { return this.$store.state.party_builder.isPublic },
|
||||||
|
set(value) { this.$store.commit('setPublic', value) }
|
||||||
|
},
|
||||||
|
team_owner: {
|
||||||
|
get() { return this.$store.state.party_builder.team_owner },
|
||||||
|
set(value) { this.$store.commit('setTeamOwner', value) }
|
||||||
|
},
|
||||||
|
party_name: {
|
||||||
|
get() { return this.$store.state.party_builder.party_name },
|
||||||
|
set(value) { this.$store.commit('setPartyName', value) }
|
||||||
|
},
|
||||||
|
current_party: {
|
||||||
|
get() { return this.$store.state.party_builder.current_party },
|
||||||
|
set(value) { this.$store.commit('setCurrentParty', value) }
|
||||||
|
},
|
||||||
|
video_id: {
|
||||||
|
get() { return this.$store.state.party_builder.video_id },
|
||||||
|
set(value) { this.$store.commit('setVideoId', value) }
|
||||||
|
},
|
||||||
|
isMyParty() {
|
||||||
|
return this.team_owner === this.$store.getters.getUserId;
|
||||||
|
},
|
||||||
|
cannotBePublic() {
|
||||||
|
return this.content === null || Utils.isEmpty(this.weapons[0]);
|
||||||
|
},
|
||||||
|
getCategories() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: "Name",
|
||||||
|
isColumn: true,
|
||||||
|
isFilter: false,
|
||||||
|
key: "n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Element",
|
||||||
|
isColumn: true,
|
||||||
|
isFilter: true,
|
||||||
|
key: "e",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Public",
|
||||||
|
isColumn: true,
|
||||||
|
isFilter: true,
|
||||||
|
key: "pub",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
'$store.getters.getUserId'(id) {
|
||||||
|
if (id !== null) {
|
||||||
|
this.reloadParties();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
serverPrefetch() {
|
||||||
|
let promises = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load party from URL
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Bookmarklet
|
||||||
|
if ( ! Utils.isEmpty(this.$route.query.l)) {
|
||||||
|
const data = JSON.parse(this.$route.query.l);
|
||||||
|
const postData = {
|
||||||
|
classe: data.p,
|
||||||
|
class_skills: data.ps, // Skill names
|
||||||
|
characters: data.c,
|
||||||
|
summons: data.s,
|
||||||
|
weapons: data.w,
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! data.v || data.v < BOOKMARKLET_VERSION) {
|
||||||
|
this.$emit('update-bookmarklet');
|
||||||
|
}
|
||||||
|
|
||||||
|
promises.push(
|
||||||
|
this.axios.post('/party/load', postData)
|
||||||
|
.then(response => this.loadParty({
|
||||||
|
data: response.data,
|
||||||
|
characters_levels: data.cl,
|
||||||
|
characters_stars: data.cs,
|
||||||
|
characters_pluses: data.cp,
|
||||||
|
characters_prings: data.cwr,
|
||||||
|
summons_levels: data.sl,
|
||||||
|
summons_stars: data.ss,
|
||||||
|
summons_pluses: data.sp,
|
||||||
|
weapons_levels: data.wll,
|
||||||
|
weapons_skill_levels: data.wl,
|
||||||
|
weapons_skill_names: data.wsn,
|
||||||
|
weapons_pluses: data.wp
|
||||||
|
}))
|
||||||
|
.catch(error => this.$store.dispatch('addAxiosErrorMessage', error))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
// Party ID
|
||||||
|
else if ( ! Utils.isEmpty(this.$route.query.p)) {
|
||||||
|
const param = parseInt(this.$route.query.p, 10);
|
||||||
|
|
||||||
|
promises.push(
|
||||||
|
this.axios.get('/party/load/' + param)
|
||||||
|
.then(response => {
|
||||||
|
this.loadParty(Utils.getPartyResponse(response));
|
||||||
|
const timestamp = response.data.updated ? response.data.updated : '0';
|
||||||
|
this.$ssrContext.head_image = 'https://www.granblue.party/previews/party/party_' + param + '.' + timestamp + '.jpg';
|
||||||
|
})
|
||||||
|
.catch(_ => this.$store.dispatch('addMessage', { title: 'Error', message: 'Party not found'}))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.all(promises);
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
204
components/SearchCharacters.vue
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<span class="flex flex-row flex-wrap gap-2 items-center">
|
||||||
|
<div>Search in</div>
|
||||||
|
<checkbox v-model="search_name">Names</checkbox>
|
||||||
|
<checkbox v-model="search_ca_names">Charge attack names</checkbox>
|
||||||
|
<checkbox v-model="search_ca_desc">Charge attack descriptions</checkbox>
|
||||||
|
<checkbox v-model="search_skill_names">Skill names</checkbox>
|
||||||
|
<checkbox v-model="search_skill_desc">Skill descriptions</checkbox>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div class="flex flex-row flex-wrap items-center gap-2">
|
||||||
|
<data-filter
|
||||||
|
v-for="category in getFilters"
|
||||||
|
:key="category.name"
|
||||||
|
:category="category.name"
|
||||||
|
:data="data_model[category.key].data"
|
||||||
|
></data-filter>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form @submit.prevent="search()" class="flex flex-row flex-wrap gap-2">
|
||||||
|
<input class="input" type="text" placeholder="Search" ref="searchfield" autofocus>
|
||||||
|
<button class="btn btn-blue" type="submit">
|
||||||
|
<fa-icon :icon="['fas', 'search']" class="text-xl"></fa-icon> Search
|
||||||
|
</button>
|
||||||
|
<checkbox v-model="case_sensitive">Match case</checkbox>
|
||||||
|
<checkbox v-model="whole_word">Match whole word</checkbox>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div id="results">
|
||||||
|
Results: {{ getResults.length }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-for="(item, k) in getResultsSlice()" :key="k" class="flex flex-col lg:flex-row bg-secondary p-2 gap-2">
|
||||||
|
<span class="flex flex-col gap-2">
|
||||||
|
<!-- Name -->
|
||||||
|
<span>
|
||||||
|
{{ index * slice_size + k + 1 }}-
|
||||||
|
<a class="font-medium" target="_blank" :href="'https://gbf.wiki/' + item.n" v-html="highlight(item.n, search_name)"></a>
|
||||||
|
<br>
|
||||||
|
<div class="font-medium">
|
||||||
|
{{ data_model['e'].expand(item) }}
|
||||||
|
{{ data_model['w'].expand(item) }}
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<img style="max-height: 96px; max-width:168px; height: 96px; width:168px;" :src="'/img/unit_small/' + item.id + '000.jpg'">
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="flex flex-col">
|
||||||
|
<!-- Ougi -->
|
||||||
|
<div class="flex flex-col lg:flex-row lg:items-center flex-wrap lg:flex-nowrap mt-2 gap-2">
|
||||||
|
<h4 class="whitespace-nowrap">Charge attack:</h4>
|
||||||
|
<div class="divide-y divide-inherit border-secondary">
|
||||||
|
<div v-for="(ougi, l) in item.o" :key="l" class="flex flex-col lg:flex-row lg:items-center gap-x-4">
|
||||||
|
<div class="whitespace-nowrap font-medium" v-html="highlight(ougi.n, search_ca_names)" />
|
||||||
|
<div class="text-sm" v-html="highlight(ougi.d, search_ca_desc)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Skills -->
|
||||||
|
<div v-if="item.s" class="flex flex-col lg:flex-row lg:items-center flex-wrap lg:flex-nowrap mt-2 gap-2">
|
||||||
|
<h4 class="whitespace-nowrap">Skills:</h4>
|
||||||
|
<div class="divide-y divide-inherit border-secondary">
|
||||||
|
<div v-for="(skill, l) in item.s" :key="l" class="flex flex-col lg:flex-row lg:items-center gap-x-4">
|
||||||
|
<div class="whitespace-nowrap font-medium" v-html="highlight(skill.n, search_skill_names)" />
|
||||||
|
<div class="text-sm" v-html="highlight(skill.d, search_skill_desc)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="flex flex-row flex-wrap gap-2" role="navigation" aria-label="pagination" v-if="getResults.length > slice_size">
|
||||||
|
<span
|
||||||
|
class="px-2 py-1 rounded cursor-pointer hover:text-link-hover"
|
||||||
|
:class="index === i-1 ? 'bg-tertiary' : 'bg-secondary'"
|
||||||
|
v-for="i in Math.ceil(getResults.length / slice_size)"
|
||||||
|
:key="i"
|
||||||
|
@click="changeSlice(i)"
|
||||||
|
>{{ i }}</span>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Utils from '@/js/utils.js'
|
||||||
|
import MixinSearch from '@/js/mixin-search.js'
|
||||||
|
|
||||||
|
import Checkbox from '@/components/common/Checkbox.vue'
|
||||||
|
import DataFilter from '@/components/common/DataFilter.vue'
|
||||||
|
|
||||||
|
const lsMgt = new Utils.LocalStorageMgt('SearchCharacters');
|
||||||
|
|
||||||
|
const categories = [
|
||||||
|
{
|
||||||
|
name: "Rarity",
|
||||||
|
isColumn: false,
|
||||||
|
isFilter: true,
|
||||||
|
key: "ri",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Element",
|
||||||
|
isColumn: true,
|
||||||
|
isFilter: true,
|
||||||
|
key: "e",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Type",
|
||||||
|
isColumn: true,
|
||||||
|
isFilter: true,
|
||||||
|
key: "t",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Race",
|
||||||
|
isColumn: true,
|
||||||
|
isFilter: true,
|
||||||
|
key: "ra",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Weapon",
|
||||||
|
isColumn: true,
|
||||||
|
isFilter: true,
|
||||||
|
key: "w",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Checkbox,
|
||||||
|
DataFilter
|
||||||
|
},
|
||||||
|
mixins: [
|
||||||
|
MixinSearch(categories, lsMgt)
|
||||||
|
],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
search_name: false,
|
||||||
|
search_ca_names: false,
|
||||||
|
search_ca_desc: true,
|
||||||
|
search_skill_names: false,
|
||||||
|
search_skill_desc: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
search_impl(re) {
|
||||||
|
this.results = this.data.filter(obj => {
|
||||||
|
if (this.search_name && obj.n !== null) {
|
||||||
|
if (re.test(obj.n)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (obj.o !== null) {
|
||||||
|
if (this.search_ca_names && obj.o.some(ougi => re.test(ougi.n))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (this.search_ca_desc && obj.o.some(ougi => re.test(ougi.d))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (obj.s !== null) {
|
||||||
|
if (this.search_skill_names && obj.s.some(skill => re.test(skill.n))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (this.search_skill_desc && obj.s.some(skill => re.test(skill.d))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
search_name() {
|
||||||
|
lsMgt.setValue('search_name', this);
|
||||||
|
},
|
||||||
|
search_ca_names() {
|
||||||
|
lsMgt.setValue('search_ca_names', this);
|
||||||
|
},
|
||||||
|
search_ca_desc() {
|
||||||
|
lsMgt.setValue('search_ca_desc', this);
|
||||||
|
},
|
||||||
|
search_skill_names() {
|
||||||
|
lsMgt.setValue('search_skill_names', this);
|
||||||
|
},
|
||||||
|
search_skill_desc() {
|
||||||
|
lsMgt.setValue('search_skill_desc', this);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
lsMgt.getValue(this, 'search_name');
|
||||||
|
lsMgt.getValue(this, 'search_ca_names');
|
||||||
|
lsMgt.getValue(this, 'search_ca_desc');
|
||||||
|
lsMgt.getValue(this, 'search_skill_names');
|
||||||
|
lsMgt.getValue(this, 'search_skill_desc');
|
||||||
|
|
||||||
|
this.axios.get('/search/characters')
|
||||||
|
.then(response => this.data = response.data)
|
||||||
|
.catch(error => this.$store.dispatch('addAxiosErrorMessage', error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
230
components/SearchSummons.vue
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<span class="flex flex-row flex-wrap gap-2 items-center">
|
||||||
|
<div>Search in</div>
|
||||||
|
<checkbox v-model="search_name">Names</checkbox>
|
||||||
|
<checkbox v-model="search_call_names">Call names</checkbox>
|
||||||
|
<checkbox v-model="search_call_desc">Call descriptions</checkbox>
|
||||||
|
<checkbox v-model="search_auras">Auras</checkbox>
|
||||||
|
<checkbox v-model="search_subauras">Sub auras</checkbox>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div class="flex flex-row flex-wrap items-center gap-2">
|
||||||
|
<data-filter
|
||||||
|
v-for="category in getFilters"
|
||||||
|
:key="category.name"
|
||||||
|
:category="category.name"
|
||||||
|
:data="data_model[category.key].data"
|
||||||
|
></data-filter>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form @submit.prevent="search()" class="flex flex-row flex-wrap gap-2">
|
||||||
|
<input class="input" type="text" placeholder="Search" ref="searchfield" autofocus>
|
||||||
|
<button class="btn btn-blue" type="submit">
|
||||||
|
<fa-icon :icon="['fas', 'search']" class="text-xl"></fa-icon> Search
|
||||||
|
</button>
|
||||||
|
<checkbox v-model="case_sensitive">Match case</checkbox>
|
||||||
|
<checkbox v-model="whole_word">Match whole word</checkbox>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div id="results">
|
||||||
|
Results: {{ getResults.length }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-for="(item, k) in getResultsSlice()" :key="k" class="flex flex-col lg:flex-row bg-secondary p-2 gap-2">
|
||||||
|
<span class="flex flex-col gap-2">
|
||||||
|
<!-- Name -->
|
||||||
|
<span>
|
||||||
|
{{ index * slice_size + k + 1 }}-
|
||||||
|
<a class="font-medium" target="_blank" :href="'https://gbf.wiki/' + item.n" v-html="highlight(item.n, search_name)"></a>
|
||||||
|
<br>
|
||||||
|
<div class="font-medium">
|
||||||
|
{{ data_model['e'].expand(item) }}
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<img style="max-height: 104px; max-width:138px; height: 104px; width:138px;" :src="'/img/unit/' + item.id + '000.jpg'">
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="flex flex-col">
|
||||||
|
|
||||||
|
<!-- Call -->
|
||||||
|
<div class="flex flex-col lg:flex-row lg:items-center flex-wrap lg:flex-nowrap mt-2 gap-2">
|
||||||
|
<h4 class="whitespace-nowrap">Call:</h4>
|
||||||
|
<div class="whitespace-nowrap font-medium" v-html="highlight(item.cn, search_call_names)" />
|
||||||
|
<div v-if="hasData(item.c)" class="divide-y divide-inherit border-secondary">
|
||||||
|
<div v-if="item.c[0]" class="text-sm" v-html="highlight(item.c[0], search_call_desc)" />
|
||||||
|
<div v-if="item.c[1]" class="flex flex-row lg:items-center gap-x-4">
|
||||||
|
<div class="whitespace-nowrap font-medium">MLB</div>
|
||||||
|
<div class="text-sm" v-html="highlight(item.c[1], search_call_desc)" />
|
||||||
|
</div>
|
||||||
|
<div v-if="item.c[2]" class="flex flex-row lg:items-center gap-x-4">
|
||||||
|
<div class="whitespace-nowrap font-medium">FLB</div>
|
||||||
|
<div class="text-sm" v-html="highlight(item.c[2], search_call_desc)" />
|
||||||
|
</div>
|
||||||
|
<div v-if="item.c[3]" class="flex flex-row lg:items-center gap-x-4">
|
||||||
|
<div class="whitespace-nowrap font-medium">ULB</div>
|
||||||
|
<div class="text-sm" v-html="highlight(item.c[3], search_call_desc)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Aura -->
|
||||||
|
<div v-if="hasData(item.a)" class="flex flex-row lg:items-center flex-wrap lg:flex-nowrap mt-2 gap-2">
|
||||||
|
<h4 class="whitespace-nowrap">Auras:</h4>
|
||||||
|
<div class="divide-y divide-inherit border-secondary">
|
||||||
|
<div v-if="item.a[0]" class="text-sm" v-html="highlight(item.a[0], search_auras)" />
|
||||||
|
<div v-if="item.a[1]" class="flex flex-row lg:items-center gap-x-4">
|
||||||
|
<div class="whitespace-nowrap font-medium">MLB</div>
|
||||||
|
<div class="text-sm" v-html="highlight(item.a[1], search_auras)" />
|
||||||
|
</div>
|
||||||
|
<div v-if="item.a[2]" class="flex flex-row lg:items-center gap-x-4">
|
||||||
|
<div class="whitespace-nowrap font-medium">FLB</div>
|
||||||
|
<div class="text-sm" v-html="highlight(item.a[2], search_auras)" />
|
||||||
|
</div>
|
||||||
|
<div v-if="item.a[3]" class="flex flex-row lg:items-center gap-x-4">
|
||||||
|
<div class="whitespace-nowrap font-medium">ULB</div>
|
||||||
|
<div class="text-sm" v-html="highlight(item.a[3], search_auras)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Sub Aura -->
|
||||||
|
<div v-if="hasData(item.s)" class="flex flex-row lg:items-center flex-wrap lg:flex-nowrap mt-2 gap-2">
|
||||||
|
<h4 class="whitespace-nowrap">Sub auras:</h4>
|
||||||
|
<div class="divide-y divide-inherit border-secondary">
|
||||||
|
<div v-if="item.s[0]" class="text-sm" v-html="highlight(item.s[0], search_subauras)" />
|
||||||
|
<div v-if="item.s[1]" class="flex flex-row lg:items-center gap-x-4">
|
||||||
|
<div class="whitespace-nowrap font-medium">MLB</div>
|
||||||
|
<div class="text-sm" v-html="highlight(item.s[1], search_subauras)" />
|
||||||
|
</div>
|
||||||
|
<div v-if="item.s[2]" class="flex flex-row lg:items-center gap-x-4">
|
||||||
|
<div class="whitespace-nowrap font-medium">FLB</div>
|
||||||
|
<div class="text-sm" v-html="highlight(item.s[2], search_subauras)" />
|
||||||
|
</div>
|
||||||
|
<div v-if="item.s[3]" class="flex flex-row lg:items-center gap-x-4">
|
||||||
|
<div class="whitespace-nowrap font-medium">ULB</div>
|
||||||
|
<div class="text-sm" v-html="highlight(item.s[3], search_subauras)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="flex flex-row flex-wrap gap-2" role="navigation" aria-label="pagination" v-if="getResults.length > slice_size">
|
||||||
|
<span
|
||||||
|
class="px-2 py-1 rounded cursor-pointer hover:text-link-hover"
|
||||||
|
:class="index === i-1 ? 'bg-tertiary' : 'bg-secondary'"
|
||||||
|
v-for="i in Math.ceil(getResults.length / slice_size)"
|
||||||
|
:key="i"
|
||||||
|
@click="changeSlice(i)"
|
||||||
|
>{{ i }}</span>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Utils from '@/js/utils.js'
|
||||||
|
import MixinSearch from '@/js/mixin-search.js'
|
||||||
|
|
||||||
|
import Checkbox from '@/components/common/Checkbox.vue'
|
||||||
|
import DataFilter from '@/components/common/DataFilter.vue'
|
||||||
|
|
||||||
|
const lsMgt = new Utils.LocalStorageMgt('SearchSummons');
|
||||||
|
|
||||||
|
const categories = [
|
||||||
|
{
|
||||||
|
name: "Rarity",
|
||||||
|
isColumn: false,
|
||||||
|
isFilter: true,
|
||||||
|
key: "ri",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Element",
|
||||||
|
isColumn: true,
|
||||||
|
isFilter: true,
|
||||||
|
key: "e",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Checkbox,
|
||||||
|
DataFilter
|
||||||
|
},
|
||||||
|
mixins: [
|
||||||
|
MixinSearch(categories, lsMgt)
|
||||||
|
],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
search_name: false,
|
||||||
|
search_call_names: false,
|
||||||
|
search_call_desc: true,
|
||||||
|
search_auras: true,
|
||||||
|
search_subauras: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
search_impl(re) {
|
||||||
|
this.results = this.data.filter(obj => {
|
||||||
|
if (this.search_name && obj.n !== null) {
|
||||||
|
if (re.test(obj.n)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.search_call_names && obj.cn !== null) {
|
||||||
|
if (re.test(obj.cn)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.search_call_desc && obj.c !== null) {
|
||||||
|
if (obj.c.some(call => re.test(call))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.search_auras && obj.a !== null) {
|
||||||
|
if (obj.a.some(call => re.test(call))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.search_subauras && obj.s !== null) {
|
||||||
|
if (obj.s.some(call => re.test(call))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
search_name() {
|
||||||
|
lsMgt.setValue('search_name', this);
|
||||||
|
},
|
||||||
|
search_call_names() {
|
||||||
|
lsMgt.setValue('search_call_names', this);
|
||||||
|
},
|
||||||
|
search_call_desc() {
|
||||||
|
lsMgt.setValue('search_call_desc', this);
|
||||||
|
},
|
||||||
|
search_auras() {
|
||||||
|
lsMgt.setValue('search_auras', this);
|
||||||
|
},
|
||||||
|
search_subauras() {
|
||||||
|
lsMgt.setValue('search_subauras', this);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
lsMgt.getValue(this, 'search_name');
|
||||||
|
lsMgt.getValue(this, 'search_call_names');
|
||||||
|
lsMgt.getValue(this, 'search_call_desc');
|
||||||
|
lsMgt.getValue(this, 'search_auras');
|
||||||
|
lsMgt.getValue(this, 'search_subauras');
|
||||||
|
|
||||||
|
this.axios.get('/search/summons')
|
||||||
|
.then(response => this.data = response.data)
|
||||||
|
.catch(error => this.$store.dispatch('addAxiosErrorMessage', error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
182
components/SearchWeapons.vue
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<span class="flex flex-row flex-wrap gap-2 items-center">
|
||||||
|
<div>Search in</div>
|
||||||
|
<checkbox v-model="search_name">Names</checkbox>
|
||||||
|
<checkbox v-model="search_ca">Charge attacks</checkbox>
|
||||||
|
<checkbox v-model="search_skill_names">Skill names</checkbox>
|
||||||
|
<checkbox v-model="search_skill_desc">Skill descriptions</checkbox>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div class="flex flex-row flex-wrap items-center gap-2">
|
||||||
|
<data-filter
|
||||||
|
v-for="category in getFilters"
|
||||||
|
:key="category.name"
|
||||||
|
:category="category.name"
|
||||||
|
:data="data_model[category.key].data"
|
||||||
|
></data-filter>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form @submit.prevent="search()" class="flex flex-row flex-wrap gap-2">
|
||||||
|
<input class="input" type="text" placeholder="Search" ref="searchfield" autofocus>
|
||||||
|
<button class="btn btn-blue" type="submit">
|
||||||
|
<fa-icon :icon="['fas', 'search']" class="text-xl"></fa-icon> Search
|
||||||
|
</button>
|
||||||
|
<checkbox v-model="case_sensitive">Match case</checkbox>
|
||||||
|
<checkbox v-model="whole_word">Match whole word</checkbox>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div id="results">
|
||||||
|
Results: {{ getResults.length }}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-for="(item, k) in getResultsSlice()" :key="k" class="flex flex-col lg:flex-row bg-secondary p-2 gap-2">
|
||||||
|
<span class="flex flex-col gap-2">
|
||||||
|
<!-- Name -->
|
||||||
|
<span>
|
||||||
|
{{ index * slice_size + k + 1 }}-
|
||||||
|
<a class="font-medium" target="_blank" :href="'https://gbf.wiki/' + item.n" v-html="highlight(item.n, search_name)"></a>
|
||||||
|
<br>
|
||||||
|
<div class="font-medium">
|
||||||
|
{{ data_model['e'].expand(item) }}
|
||||||
|
{{ data_model['w'].expand(item) }}
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<img style="max-height: 96px; max-width:168px; height: 96px; width:168px;" :src="'/img/weapon/' + item.id + '00.jpg'">
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span class="flex flex-col">
|
||||||
|
<!-- Ougi -->
|
||||||
|
<div class="flex flex-col lg:flex-row lg:items-center flex-wrap lg:flex-nowrap mt-2 gap-2">
|
||||||
|
<h4 class="whitespace-nowrap">Charge attack:</h4>
|
||||||
|
<div class="divide-y divide-inherit border-secondary">
|
||||||
|
<div v-if="item.o[0]" class="text-sm" v-html="highlight(item.o[0], search_ca)" />
|
||||||
|
<div v-if="item.o[1]" class="flex flex-col lg:flex-row lg:items-center gap-x-4">
|
||||||
|
<div class="whitespace-nowrap font-medium">FLB effect</div>
|
||||||
|
<div class="text-sm" v-html="highlight(item.o[1], search_ca)" />
|
||||||
|
</div>
|
||||||
|
<div v-if="item.o[2]" class="flex flex-col lg:flex-row lg:items-center gap-x-4">
|
||||||
|
<div class="whitespace-nowrap font-medium">ULB effect</div>
|
||||||
|
<div class="text-sm" v-html="highlight(item.o[2], search_ca)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Skills -->
|
||||||
|
<div v-if="item.s" class="flex flex-col lg:flex-row lg:items-center flex-wrap lg:flex-nowrap mt-2 gap-2">
|
||||||
|
<h4 class="whitespace-nowrap">Skills:</h4>
|
||||||
|
<div class="divide-y divide-inherit border-secondary">
|
||||||
|
<div v-for="(skill, l) in item.s" :key="l" class="flex flex-col lg:flex-row lg:items-center gap-x-4">
|
||||||
|
<div class="whitespace-nowrap font-medium" v-html="highlight(skill.n, search_skill_names)" />
|
||||||
|
<div class="text-sm" v-html="highlight(skill.d, search_skill_desc)" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<nav class="flex flex-row flex-wrap gap-2" role="navigation" aria-label="pagination" v-if="getResults.length > slice_size">
|
||||||
|
<span
|
||||||
|
class="px-2 py-1 rounded cursor-pointer hover:text-link-hover"
|
||||||
|
:class="index === i-1 ? 'bg-tertiary' : 'bg-secondary'"
|
||||||
|
v-for="i in Math.ceil(getResults.length / slice_size)"
|
||||||
|
:key="i"
|
||||||
|
@click="changeSlice(i)"
|
||||||
|
>{{ i }}</span>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Utils from '@/js/utils.js'
|
||||||
|
import MixinSearch from '@/js/mixin-search.js'
|
||||||
|
|
||||||
|
import Checkbox from '@/components/common/Checkbox.vue'
|
||||||
|
import DataFilter from '@/components/common/DataFilter.vue'
|
||||||
|
|
||||||
|
const lsMgt = new Utils.LocalStorageMgt('SearchWeapons');
|
||||||
|
|
||||||
|
const categories = [
|
||||||
|
{
|
||||||
|
name: "Element",
|
||||||
|
isColumn: true,
|
||||||
|
isFilter: true,
|
||||||
|
key: "e",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Type",
|
||||||
|
isColumn: true,
|
||||||
|
isFilter: true,
|
||||||
|
key: "w",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Checkbox,
|
||||||
|
DataFilter
|
||||||
|
},
|
||||||
|
mixins: [
|
||||||
|
MixinSearch(categories, lsMgt)
|
||||||
|
],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
search_name: false,
|
||||||
|
search_ca: true,
|
||||||
|
search_skill_names: false,
|
||||||
|
search_skill_desc: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
search_impl(re) {
|
||||||
|
this.results = this.data.filter(obj => {
|
||||||
|
if (this.search_name && obj.n !== null) {
|
||||||
|
if (re.test(obj.n)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (this.search_ca && obj.o !== null) {
|
||||||
|
if (obj.o.some(ougi => re.test(ougi))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (obj.s !== null) {
|
||||||
|
if (this.search_skill_names && obj.s.some(skill => re.test(skill.n))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (this.search_skill_desc && obj.s.some(skill => re.test(skill.d))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
search_name() {
|
||||||
|
lsMgt.setValue('search_name', this);
|
||||||
|
},
|
||||||
|
search_ca() {
|
||||||
|
lsMgt.setValue('search_ca', this);
|
||||||
|
},
|
||||||
|
search_skill_names() {
|
||||||
|
lsMgt.setValue('search_skill_names', this);
|
||||||
|
},
|
||||||
|
search_skill_desc() {
|
||||||
|
lsMgt.setValue('search_skill_desc', this);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
lsMgt.getValue(this, 'search_name');
|
||||||
|
lsMgt.getValue(this, 'search_ca');
|
||||||
|
lsMgt.getValue(this, 'search_skill_names');
|
||||||
|
lsMgt.getValue(this, 'search_skill_desc');
|
||||||
|
|
||||||
|
this.axios.get('/search/weapons')
|
||||||
|
.then(response => this.data = response.data)
|
||||||
|
.catch(error => this.$store.dispatch('addAxiosErrorMessage', error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
65
components/SparkUnit.vue
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-col relative" :style="'width: ' + width + 'px;'">
|
||||||
|
<a
|
||||||
|
class="text-xs text-primary h-5 px-1 text-center truncate"
|
||||||
|
target="_blank"
|
||||||
|
:href="'https://gbf.wiki/' + unit.n"
|
||||||
|
:title="getName(unit)"
|
||||||
|
>{{ getName(unit) }}</a>
|
||||||
|
<img
|
||||||
|
class="cursor-pointer"
|
||||||
|
:style="'min-width: ' + width + 'px;'"
|
||||||
|
:height="height"
|
||||||
|
:width="width"
|
||||||
|
:title="getName(unit)"
|
||||||
|
:src="'/img/unit_small/' + unit.id + '000.jpg'"
|
||||||
|
@click="$emit('left-click-unit')"
|
||||||
|
@contextmenu.prevent="$emit('right-click-unit')"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
v-if="isSpark === true"
|
||||||
|
class="absolute bottom-0 right-0 pointer-events-none"
|
||||||
|
height="39"
|
||||||
|
width="38"
|
||||||
|
src="/img/item/ceruleanspark.png"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { LANGUAGES } from '@/js/lang'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
unit: { // Yellow stars
|
||||||
|
type: Object,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
width: {
|
||||||
|
type: Number,
|
||||||
|
default: 105
|
||||||
|
},
|
||||||
|
height: {
|
||||||
|
type: Number,
|
||||||
|
default: 60
|
||||||
|
},
|
||||||
|
isSpark: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getName(element) {
|
||||||
|
if (this.isLangEnglish) {
|
||||||
|
return element.n;
|
||||||
|
}
|
||||||
|
return element.nj;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
isLangEnglish() {
|
||||||
|
return this.$store.getters.getLang === LANGUAGES.EN;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
133
components/StarsLine.vue
Normal file
@@ -0,0 +1,133 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="base !== undefined" class="flex flex-col flex-nowrap" :title="title">
|
||||||
|
<div class="flex flex-row flex-nowrap">
|
||||||
|
<img
|
||||||
|
v-for="i in getYellowStarsCount"
|
||||||
|
:key="'y' + i"
|
||||||
|
:class="readOnly ? '' : 'cursor-pointer'"
|
||||||
|
:src="getImage('y', i)"
|
||||||
|
:style="'width: ' + 100/max + '%;'"
|
||||||
|
@click="click(i)"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
v-for="i in getBlueStarsCount"
|
||||||
|
:key="'b' + i"
|
||||||
|
:class="readOnly ? '' : 'cursor-pointer'"
|
||||||
|
:src="getImage('b', i+base)"
|
||||||
|
:style="'width: ' + 100/max + '%;'"
|
||||||
|
@click="click(i+base)"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
v-for="i in getInvisibleStarsCount"
|
||||||
|
:key="'i' + i"
|
||||||
|
class="hidden"
|
||||||
|
src="/img/star_b1.png"
|
||||||
|
:style="'width: ' + 100/max + '%;'"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
<div v-if="transcendance && extra > 5" class="flex flex-row flex-nowrap">
|
||||||
|
<img
|
||||||
|
v-for="i in 5"
|
||||||
|
:key="'v' + i"
|
||||||
|
:class="getTranscendanceClass(5+i)"
|
||||||
|
:src="getTranscendanceImage(i)"
|
||||||
|
:style="'width: 20%;'"
|
||||||
|
@click="click(5+i)"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
base: { // Yellow stars
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
extra: { // Blue stars
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
current: { // Number of checked stars
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
max: { // For line size
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
transcendance: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
readOnly: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
type: String,
|
||||||
|
default: 'Uncap level'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
isStarEnabled(index) {
|
||||||
|
return index <= this.current;
|
||||||
|
},
|
||||||
|
getImage(type, index) {
|
||||||
|
return '/img/star_' + type + (this.isStarEnabled(index) ? "1" : "0") + '.png';
|
||||||
|
},
|
||||||
|
getTranscendanceImage(index) {
|
||||||
|
if ( ! this.isStarEnabled(index + 5)) {
|
||||||
|
return '/img/star_v0.png';
|
||||||
|
}
|
||||||
|
return '/img/star_v' + index + '.png';
|
||||||
|
//return '/img/star_v' + index + '.png';
|
||||||
|
},
|
||||||
|
getTranscendanceClass(index) {
|
||||||
|
let result = '';
|
||||||
|
if ( ! this.readOnly) result += 'cursor-pointer ';
|
||||||
|
if ( ! this.isStarEnabled(index)) result += 'grayscale-80 opacity-70';
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
click(index) {
|
||||||
|
if (this.readOnly) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let current_stars = index;
|
||||||
|
|
||||||
|
if (index === 1 && this.current !== 0) {
|
||||||
|
// First click on 1st star sets 0 stars instead of 1
|
||||||
|
current_stars = 0;
|
||||||
|
}
|
||||||
|
else if (index === this.current) {
|
||||||
|
current_stars = index - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$emit('update:current', current_stars);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
getYellowStarsCount() {
|
||||||
|
return this.base;
|
||||||
|
},
|
||||||
|
getBlueStarsCount() {
|
||||||
|
if (this.base > this.extra) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
if (this.transcendance && this.extra > 5) {
|
||||||
|
return Math.min(this.extra, 5) - this.base;
|
||||||
|
}
|
||||||
|
return this.extra - this.base;
|
||||||
|
},
|
||||||
|
getInvisibleStarsCount() {
|
||||||
|
const baseStars = Math.max(this.base, this.extra);
|
||||||
|
if (baseStars > this.max) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
return this.max - baseStars;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
64
components/common/Checkbox.vue
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
@click="changeValue()"
|
||||||
|
class="select-none flex flex-row flex-nowrap items-center"
|
||||||
|
:class="getClasses"
|
||||||
|
>
|
||||||
|
<!-- Box -->
|
||||||
|
<fa-icon v-if="value === true" :icon="on" :class="iconSize"></fa-icon>
|
||||||
|
<fa-icon v-else :icon="off" :class="iconSize"></fa-icon>
|
||||||
|
<!-- Label -->
|
||||||
|
<span class="ml-1 my-1 text-primary">
|
||||||
|
<slot></slot>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
on: {
|
||||||
|
type: Array,
|
||||||
|
default: () => ['fa', 'toggle-on']
|
||||||
|
},
|
||||||
|
off: {
|
||||||
|
type: Array,
|
||||||
|
default: () => ['fa', 'toggle-off']
|
||||||
|
},
|
||||||
|
iconSize: {
|
||||||
|
type: String,
|
||||||
|
default: 'text-4xl'
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
changeValue() {
|
||||||
|
if ( ! this.disabled) {
|
||||||
|
this.$emit("input", ! this.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
getClasses() {
|
||||||
|
let classes = this.value ? 'text-blue-300 ' : 'text-rose-400 ';
|
||||||
|
if (this.disabled) {
|
||||||
|
classes += ' cursor-not-allowed grayscale-70 opacity-70 ';
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
classes += 'cursor-pointer hover:underline hover:decoration-2 ';
|
||||||
|
classes += this.value ?
|
||||||
|
'hover:text-blue-400 hover:decoration-blue-400 ' :
|
||||||
|
'hover:text-rose-600 hover:decoration-rose-600 ';
|
||||||
|
}
|
||||||
|
return classes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
69
components/common/ContentCategories.vue
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
<template>
|
||||||
|
<!-- <dropdown :value="value" :index="index" @change="changeValue" ref="select"> -->
|
||||||
|
<dropdown :value="value" @change="changeValue" class="w-52" :disabled="disabled">
|
||||||
|
<option v-if="all" :value="-1">--- All Content ---</option>
|
||||||
|
<optgroup
|
||||||
|
v-for="(step, index) in content"
|
||||||
|
:key="index"
|
||||||
|
:label="step.name"
|
||||||
|
>
|
||||||
|
<option v-for="raid in filterContent(step.content)" :key="raid.id" :value="raid.id">
|
||||||
|
{{ raid.name }}
|
||||||
|
</option>
|
||||||
|
</optgroup>
|
||||||
|
</dropdown>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Content from '@/js/content'
|
||||||
|
|
||||||
|
import Dropdown from '@/components/common/Dropdown.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Dropdown,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
//required: true, Cannot uncomment, since it can be null
|
||||||
|
type: Number
|
||||||
|
},
|
||||||
|
all: {
|
||||||
|
required: false,
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
showPrivateCategories: {
|
||||||
|
required: false,
|
||||||
|
type: Boolean,
|
||||||
|
default: true
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
filterContent(content) {
|
||||||
|
if ( ! this.showPrivateCategories) {
|
||||||
|
return content.filter(raid => raid.private !== true);
|
||||||
|
}
|
||||||
|
return content;
|
||||||
|
},
|
||||||
|
changeValue(e) {
|
||||||
|
this.$emit('change', e);
|
||||||
|
if (e.target.value === '') {
|
||||||
|
this.$emit('input', null);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.$emit('input', e.target.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
content() {
|
||||||
|
return Content;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
115
components/common/DataFilter.vue
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
<template>
|
||||||
|
<div class="flex flex-row items-center">
|
||||||
|
<span class="pr-2">{{ category }}</span>
|
||||||
|
|
||||||
|
<span class="inline-flex flex-row flex-wrap btn-group">
|
||||||
|
<button
|
||||||
|
v-if="hasAll"
|
||||||
|
class="btn btn-sm"
|
||||||
|
:class="all ? 'btn-blue' : 'btn-white'"
|
||||||
|
@click="clickAll()"
|
||||||
|
>
|
||||||
|
All
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class="btn btn-sm relative"
|
||||||
|
:class="getClassesForItem(item)"
|
||||||
|
v-for="(item, index) in data_view"
|
||||||
|
:key="index"
|
||||||
|
@click="clickItem(index)"
|
||||||
|
>
|
||||||
|
{{ item.name }}
|
||||||
|
<div
|
||||||
|
v-if="count"
|
||||||
|
class="absolute w-5 -top-2 -right-1 z-10 rounded-full text-xs leading-5 tracking-tight bg-tertiary text-primary"
|
||||||
|
>
|
||||||
|
{{ count[index] > 0 ? count[index] : '-' }}
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import Utils from '@/js/utils.js'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
data: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
count: {
|
||||||
|
type: Array,
|
||||||
|
default: undefined
|
||||||
|
},
|
||||||
|
category: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
hasAll: {
|
||||||
|
type: Boolean,
|
||||||
|
default: true,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
all: true,
|
||||||
|
data_view: [],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
clickAll() {
|
||||||
|
// check
|
||||||
|
if ( ! this.all) {
|
||||||
|
this.all = true;
|
||||||
|
this.data_view.forEach(e => e.checked = false);
|
||||||
|
this.data.forEach(e => e.checked = true);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
clickItem(index) {
|
||||||
|
this.data_view[index].checked = ! this.data_view[index].checked;
|
||||||
|
|
||||||
|
if (this.all === true) {
|
||||||
|
this.all = false;
|
||||||
|
// Propagate change
|
||||||
|
for (let i=0; i<this.data.length; i++) {
|
||||||
|
this.$set(this.data[i], 'checked', this.data_view[i].checked);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.$set(this.data[index], 'checked', this.data_view[index].checked);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
getClassesForItem(item) {
|
||||||
|
let classes = item.checked ? 'btn-blue ' : 'btn-white ';
|
||||||
|
if (this.count) {
|
||||||
|
classes += 'pr-3 ';
|
||||||
|
}
|
||||||
|
return classes;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
if (this.hasAll === true) {
|
||||||
|
// Copy data locally to deal with the All button
|
||||||
|
this.data_view = Utils.copy(this.data);
|
||||||
|
|
||||||
|
if (this.data_view.some(e => ! e.checked)) {
|
||||||
|
this.all = false;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.data_view.forEach(e => e.checked = false);
|
||||||
|
this.all = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
// Data view is the same as the data
|
||||||
|
this.data_view = this.data;
|
||||||
|
|
||||||
|
// Disable All button
|
||||||
|
this.all = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
43
components/common/Dropdown.vue
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<template>
|
||||||
|
<div class="inline-block relative">
|
||||||
|
<select :value="value" @change="changeValue" class="block select select-sm w-full" ref="select" :disabled="disabled">
|
||||||
|
<slot></slot>
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<div v-if="! disabled" class="pointer-events-none absolute inset-y-0 right-0 rounded-md flex items-center px-2 text-gray-700">
|
||||||
|
<fa-icon :icon="['fas', 'angle-down']" class="text-xl"></fa-icon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
value: {
|
||||||
|
required: false
|
||||||
|
},
|
||||||
|
index: {
|
||||||
|
type: Number,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
changeValue(e) {
|
||||||
|
this.$emit('change', e);
|
||||||
|
this.$emit('input', this.$refs.select.value);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
value() {
|
||||||
|
// Selected index takes some time to update. Don't batch it in changeValue
|
||||||
|
this.$nextTick().then(() => {
|
||||||
|
this.$emit('update:index', this.$refs.select.selectedIndex);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
76
components/common/GoogleAds.vue
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
<template>
|
||||||
|
<ins
|
||||||
|
class="adsbygoogle responsive_ad"
|
||||||
|
style="display:block"
|
||||||
|
:style="$isDebug ? 'border: 1px solid grey;' : ''"
|
||||||
|
data-ad-client="ca-pub-2769716391947040"
|
||||||
|
:data-ad-slot="adSlot"
|
||||||
|
:data-ad-region="ad_region"
|
||||||
|
:key="ad_region"
|
||||||
|
data-ad-format="fluid"
|
||||||
|
data-full-width-responsive="true"
|
||||||
|
></ins>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
adSlot: {
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
ad_region: null,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
getNewAds() {
|
||||||
|
if (VUE_ENV === 'server') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.ad_region = 'page-' + Math.random();
|
||||||
|
|
||||||
|
this.$nextTick(() => {
|
||||||
|
try {
|
||||||
|
(window.adsbygoogle = window.adsbygoogle || []).push({})
|
||||||
|
} catch (error) {
|
||||||
|
//console.error(error)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.getNewAds();
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
'$route.path'(to, from) {
|
||||||
|
if (to !== from) {
|
||||||
|
this.getNewAds();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
|
||||||
|
.responsive_ad {
|
||||||
|
width: 320px;
|
||||||
|
height: 100px;
|
||||||
|
}
|
||||||
|
@media(min-width: 500px) {
|
||||||
|
.responsive_ad {
|
||||||
|
width: 468px;
|
||||||
|
height: 60px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@media(min-width: 800px) {
|
||||||
|
.responsive_ad {
|
||||||
|
width: 728px;
|
||||||
|
height: 90px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</style>
|
||||||
66
components/common/LikeButton.vue
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
<template>
|
||||||
|
<checkbox
|
||||||
|
v-show="team_owner && ! isMyParty"
|
||||||
|
:value="liked"
|
||||||
|
@input="clickLike"
|
||||||
|
:disabled="team_owner && ! isUserLogged"
|
||||||
|
:title="isUserLogged ? '' : 'Log in to like this team'"
|
||||||
|
:off="['far', 'circle']"
|
||||||
|
:on="['far', 'face-grin-wide']"
|
||||||
|
iconSize="text-3xl"
|
||||||
|
>
|
||||||
|
{{ liked ? 'Liked' : 'Like this team' }}
|
||||||
|
</checkbox>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { mapState } from 'vuex'
|
||||||
|
|
||||||
|
import Checkbox from '@/components/common/Checkbox.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
Checkbox,
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
teamId: {
|
||||||
|
type: Number
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
clickLike(e) {
|
||||||
|
if (e) {
|
||||||
|
this.axios.get('/party/like/' + this.teamId)
|
||||||
|
.then(_ => {
|
||||||
|
this.$store.dispatch('addMessage', {message: 'You liked this party'});
|
||||||
|
this.liked = e;
|
||||||
|
})
|
||||||
|
.catch(error => this.$store.dispatch('addAxiosErrorMessage', error));
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.axios.get('/party/unlike/' + this.teamId)
|
||||||
|
.then(_ => {
|
||||||
|
this.$store.dispatch('addMessage', {message: 'You stopped liking this party'});
|
||||||
|
this.liked = e;
|
||||||
|
})
|
||||||
|
.catch(error => this.$store.dispatch('addAxiosErrorMessage', error));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapState({
|
||||||
|
team_owner: state => state.party_builder.team_owner,
|
||||||
|
}),
|
||||||
|
liked: {
|
||||||
|
get() { return this.$store.state.party_builder.liked },
|
||||||
|
set(value) { this.$store.commit('setLiked', value) }
|
||||||
|
},
|
||||||
|
isUserLogged() {
|
||||||
|
return this.$store.getters.getUserId !== null;
|
||||||
|
},
|
||||||
|
isMyParty() {
|
||||||
|
return this.team_owner === this.$store.getters.getUserId;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
64
components/common/Modal.vue
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
<template>
|
||||||
|
<div v-if="show" class="fixed inset-0 z-40 flex justify-center items-start bg-black/80" @click.self="close()">
|
||||||
|
<div
|
||||||
|
class="w-screen h-auto mt-8 bg-primary md:rounded flex flex-col overflow-hidden"
|
||||||
|
:class="large ? 'md:w-11/12' : 'md:w-3/4 xl:w-3/5 2xl:1/2'"
|
||||||
|
style="max-height: calc(100vh - 4rem);"
|
||||||
|
>
|
||||||
|
<div class="px-4 py-2 flex flex-col-reverse sm:flex-row sm:justify-between">
|
||||||
|
<slot name="header"></slot>
|
||||||
|
<button @click.prevent="close()" class="self-end sm:self-start">
|
||||||
|
<fa-icon :icon="['fas', 'times-circle']" class="text-link-primary hover:text-link-hover text-2xl"></fa-icon>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="overflow-auto break-all px-4" >
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="px-4 py-2 flex shrink-0">
|
||||||
|
<slot name="footer"></slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
model: {
|
||||||
|
prop: 'show',
|
||||||
|
event: 'close'
|
||||||
|
},
|
||||||
|
props: {
|
||||||
|
show: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
large: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
close() {
|
||||||
|
this.$emit("close", ! this.show);
|
||||||
|
},
|
||||||
|
toggleOverflow() {
|
||||||
|
if (this.show === true) {
|
||||||
|
document.querySelector("HTML").classList.add("overflow-hidden");
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
document.querySelector("HTML").classList.remove("overflow-hidden");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
show() {
|
||||||
|
this.toggleOverflow();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.toggleOverflow();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
49
components/common/StatInput.vue
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<template>
|
||||||
|
<label :title="longName">
|
||||||
|
{{ shortName }}
|
||||||
|
<input
|
||||||
|
class="appearance-none text-primary bg-transparent"
|
||||||
|
:class="alignRight ? 'text-right' : ''"
|
||||||
|
type="tel"
|
||||||
|
:style="'width: ' + length + 'ch;'"
|
||||||
|
v-model.number.lazy="localProp"
|
||||||
|
@keydown.arrow-up="incrementProp()"
|
||||||
|
@keydown.arrow-down="decrementProp()"
|
||||||
|
>
|
||||||
|
</label>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
prop: Number,
|
||||||
|
shortName: String,
|
||||||
|
longName: String,
|
||||||
|
length: Number,
|
||||||
|
max: Number,
|
||||||
|
alignRight: Boolean,
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
incrementProp() {
|
||||||
|
if (this.max === undefined || this.max > this.localProp) {
|
||||||
|
this.localProp++;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
decrementProp() {
|
||||||
|
if (this.localProp > 0) {
|
||||||
|
this.localProp--;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
localProp: {
|
||||||
|
get() {
|
||||||
|
return this.prop;
|
||||||
|
},
|
||||||
|
set(value) {
|
||||||
|
this.$emit('update:prop', value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
BIN
favicon-32.png
Normal file
|
After Width: | Height: | Size: 1.7 KiB |
BIN
favicon-64.png
Normal file
|
After Width: | Height: | Size: 4.8 KiB |
BIN
favicon-96.png
Normal file
|
After Width: | Height: | Size: 9.1 KiB |
BIN
favicon.ico
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
favicon.png
Normal file
|
After Width: | Height: | Size: 709 B |
BIN
img/card_calceternal.jpg
Normal file
|
After Width: | Height: | Size: 80 KiB |
BIN
img/card_calcevent.jpg
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
img/card_calcevoker.jpg
Normal file
|
After Width: | Height: | Size: 65 KiB |
BIN
img/card_calcgw.jpg
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
img/card_collection.jpg
Normal file
|
After Width: | Height: | Size: 129 KiB |
BIN
img/card_dailygrind.jpg
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
img/card_friendsum.jpg
Normal file
|
After Width: | Height: | Size: 83 KiB |
BIN
img/card_index.jpg
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
img/card_party.jpg
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
img/card_release.jpg
Normal file
|
After Width: | Height: | Size: 130 KiB |
BIN
img/card_replicard.jpg
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
img/card_roomname.jpg
Normal file
|
After Width: | Height: | Size: 71 KiB |
BIN
img/card_search.jpg
Normal file
|
After Width: | Height: | Size: 109 KiB |
BIN
img/card_spark.jpg
Normal file
|
After Width: | Height: | Size: 121 KiB |
BIN
img/e_dark.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
img/e_earth.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
img/e_fire.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
img/e_light.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
img/e_water.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
img/e_wind.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
img/empty_chara.jpg
Normal file
|
After Width: | Height: | Size: 3.3 KiB |
BIN
img/empty_chara_ro.jpg
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
img/empty_summon.jpg
Normal file
|
After Width: | Height: | Size: 7.3 KiB |
BIN
img/empty_summon_ro.jpg
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
img/empty_weapon.jpg
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
img/empty_weapon_arcarum.jpg
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
img/empty_weapon_ro.jpg
Normal file
|
After Width: | Height: | Size: 3.5 KiB |
BIN
img/icon_pring.png
Normal file
|
After Width: | Height: | Size: 1.6 KiB |
BIN
img/img/card_calceternal.jpg
Normal file
|
After Width: | Height: | Size: 80 KiB |
BIN
img/img/card_calcevent.jpg
Normal file
|
After Width: | Height: | Size: 72 KiB |
BIN
img/img/card_calcevoker.jpg
Normal file
|
After Width: | Height: | Size: 65 KiB |
BIN
img/img/card_calcgw.jpg
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
img/img/card_collection.jpg
Normal file
|
After Width: | Height: | Size: 129 KiB |
BIN
img/img/card_dailygrind.jpg
Normal file
|
After Width: | Height: | Size: 70 KiB |
BIN
img/img/card_friendsum.jpg
Normal file
|
After Width: | Height: | Size: 83 KiB |
BIN
img/img/card_index.jpg
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
img/img/card_party.jpg
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
img/img/card_release.jpg
Normal file
|
After Width: | Height: | Size: 130 KiB |
BIN
img/img/card_replicard.jpg
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
img/img/card_roomname.jpg
Normal file
|
After Width: | Height: | Size: 71 KiB |
BIN
img/img/card_search.jpg
Normal file
|
After Width: | Height: | Size: 109 KiB |
BIN
img/img/card_spark.jpg
Normal file
|
After Width: | Height: | Size: 121 KiB |
BIN
img/img/e_dark.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
img/img/e_earth.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
img/img/e_fire.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
img/img/e_light.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |
BIN
img/img/e_water.png
Normal file
|
After Width: | Height: | Size: 1.2 KiB |