First commit
This commit is contained in:
100
components/BoxCharacter.vue
Normal file
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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
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>
|
||||
Reference in New Issue
Block a user