format; linting; aktualizacja do 2023.2.1

This commit is contained in:
2023-10-24 23:28:42 +02:00
parent 57ab6cc02d
commit 1c2a93fbd5
40 changed files with 2019 additions and 1640 deletions
-6
View File
@@ -18,11 +18,6 @@ export default defineComponent({
};
},
async created() {
/* dev info testing */
// if (import.meta.env['VITE_STOCK_DEV'] == '1') {
// const data = await import('../stockInfoDev.json');
// this.store.stockData = data.default as any;
// }
this.store.fetchStockInfoData();
this.store.handleRouting();
},
@@ -66,4 +61,3 @@ h2 {
}
}
</style>
+24 -24
View File
@@ -1,24 +1,24 @@
<template>
<div>
<keep-alive>
<RealStockCard v-if="store.isRealStockListCardOpen" />
</keep-alive>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useStore } from '../../store';
import RealStockCard from '../cards/RealStockCard.vue';
export default defineComponent({
components: { RealStockCard },
data() {
return {
store: useStore(),
};
},
});
</script>
<style scoped></style>
<template>
<div>
<keep-alive>
<RealStockCard v-if="store.isRealStockListCardOpen" />
</keep-alive>
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import { useStore } from "../../store";
import RealStockCard from "../cards/RealStockCard.vue";
export default defineComponent({
components: { RealStockCard },
data() {
return {
store: useStore(),
};
},
});
</script>
<style scoped></style>
+52 -49
View File
@@ -1,49 +1,52 @@
<template>
<footer>
<i18n-t keypath="footer.disclaimer" tag="div" class="text--grayed">
<template #tos>
<a style="color: #ccc" :href="$t('footer.tos-href')" target="_blank">
{{ $t('footer.tos') }}
</a>
</template>
</i18n-t>
<div class="text--grayed" v-if="store.stockData">
{{ $t('footer.version-check', { version: store.stockData.version }) }}
</div>
<div>
&copy;
<a href="https://td2.info.pl/profile/?u=20777" target="_blank">Spythere</a>
{{ new Date().getUTCFullYear() }} | v{{ VERSION }}{{ !isOnProductionHost ? 'dev' : '' }}
</div>
</footer>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import packageInfo from '../../../package.json';
import { useStore } from '../../store';
export default defineComponent({
data() {
return {
isOnProductionHost: location.hostname == 'pojazdownik-td2.web.app',
VERSION: packageInfo.version,
store: useStore(),
};
},
});
</script>
<style lang="scss" scoped>
footer {
display: flex;
flex-direction: column;
gap: 0.25em;
text-align: center;
padding: 1em 1em 0 1em;
margin-top: auto;
}
</style>
<template>
<footer>
<i18n-t keypath="footer.disclaimer" tag="div" class="text--grayed">
<template #tos>
<a style="color: #ccc" :href="$t('footer.tos-href')" target="_blank">
{{ $t("footer.tos") }}
</a>
</template>
</i18n-t>
<div class="text--grayed" v-if="store.stockData">
{{ $t("footer.version-check", { version: store.stockData.version }) }}
</div>
<div>
&copy;
<a href="https://td2.info.pl/profile/?u=20777" target="_blank"
>Spythere</a
>
{{ new Date().getUTCFullYear() }} | v{{ VERSION
}}{{ !isOnProductionHost ? "dev" : "" }}
</div>
</footer>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import packageInfo from "../../../package.json";
import { useStore } from "../../store";
export default defineComponent({
data() {
return {
isOnProductionHost: location.hostname == "pojazdownik-td2.web.app",
VERSION: packageInfo.version,
store: useStore(),
};
},
});
</script>
<style lang="scss" scoped>
footer {
display: flex;
flex-direction: column;
gap: 0.25em;
text-align: center;
padding: 1em 1em 0 1em;
margin-top: auto;
}
</style>
+45 -45
View File
@@ -1,45 +1,45 @@
<template>
<main>
<LogoSection />
<InputsSection />
<TrainImageSection />
<StockSection />
</main>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import LogoSection from '../sections/LogoSection.vue';
import InputsSection from '../sections/InputsSection.vue';
import TrainImageSection from '../sections/TrainImageSection.vue';
import StockSection from '../sections/StockSection.vue';
export default defineComponent({
components: { LogoSection, InputsSection, TrainImageSection, StockSection },
});
</script>
<style lang="scss" scoped>
@import '../../styles/global.scss';
main {
display: grid;
gap: 1em;
width: 100%;
max-width: 1500px;
min-height: 75vh;
grid-template-columns: 1fr 2fr;
grid-template-rows: auto 360px minmax(400px, 1fr);
}
@media screen and (max-width: $breakpointMd) {
main {
display: flex;
flex-direction: column;
grid-template-columns: 1fr;
grid-template-rows: 1fr;
}
}
</style>
<template>
<main>
<LogoSection />
<InputsSection />
<TrainImageSection />
<StockSection />
</main>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import LogoSection from "../sections/LogoSection.vue";
import InputsSection from "../sections/InputsSection.vue";
import TrainImageSection from "../sections/TrainImageSection.vue";
import StockSection from "../sections/StockSection.vue";
export default defineComponent({
components: { LogoSection, InputsSection, TrainImageSection, StockSection },
});
</script>
<style lang="scss" scoped>
@import "../../styles/global.scss";
main {
display: grid;
gap: 1em;
width: 100%;
max-width: 1500px;
min-height: 75vh;
grid-template-columns: 1fr 2fr;
grid-template-rows: auto 360px minmax(400px, 1fr);
}
@media screen and (max-width: $breakpointMd) {
main {
display: flex;
flex-direction: column;
grid-template-columns: 1fr;
grid-template-rows: 1fr;
}
}
</style>
+90 -46
View File
@@ -1,14 +1,25 @@
<template>
<div class="real-stock-card g-card" @keydown.esc="store.isRealStockListCardOpen = false">
<div
class="real-stock-card g-card"
@keydown.esc="store.isRealStockListCardOpen = false"
>
<div class="g-card_bg" @click="store.isRealStockListCardOpen = false"></div>
<div class="card_content">
<div class="card_nav">
<div class="top-pane">
<h1>
{{ $t('realstock.title') }} <a href="https://td2.info.pl/profile/?u=17708" target="_blank">Railtrains997</a>
{{ $t("realstock.title") }}
<a href="https://td2.info.pl/profile/?u=17708" target="_blank"
>Railtrains997</a
>
</h1>
<button class="btn exit-btn" @click="store.isRealStockListCardOpen = false">&Cross;</button>
<button
class="btn exit-btn"
@click="store.isRealStockListCardOpen = false"
>
&Cross;
</button>
</div>
<div class="filters" ref="focus" tabindex="0">
@@ -19,7 +30,11 @@
/>
<datalist id="readyStockDataList">
<option v-for="stock in store.readyStockList" :value="stock.stockId">
<option
v-for="stock in store.readyStockList"
:value="stock.stockId"
:key="stock.name"
>
{{ stock.stockId }}
</option>
</datalist>
@@ -31,12 +46,18 @@
/>
<datalist id="readyStockStringList">
<option v-for="stock in computedAvailableStockTypes" :value="stock">
{{ stock }}
<option
v-for="stockType in computedAvailableStockTypes"
:value="stockType"
:key="stockType"
>
{{ stockType }}
</option>
</datalist>
<button class="btn" @click="resetStockFilters">{{ $t('realstock.action-reset') }}</button>
<button class="btn" @click="resetStockFilters">
{{ $t("realstock.action-reset") }}
</button>
</div>
</div>
@@ -46,14 +67,29 @@
:key="rStock.stockId"
:data-last-selected="store.chosenRealStockName === rStock.stockId"
>
<div class="stock-title" tabindex="0" @click="chooseStock(rStock)" @keydown.enter="chooseStock(rStock)">
<img class="stock-icon" :src="getIconURL(rStock.type)" :alt="rStock.type" />
<b class="text--accent" style="margin-left: 5px"> {{ rStock.name }}</b>
<div
class="stock-title"
tabindex="0"
@click="chooseStock(rStock)"
@keydown.enter="chooseStock(rStock)"
>
<img
class="stock-icon"
:src="getIconURL(rStock.type)"
:alt="rStock.type"
/>
<b class="text--accent" style="margin-left: 5px">
{{ rStock.name }}</b
>
<div>{{ rStock.number }}</div>
</div>
<div class="stock-thumbnails" ref="thumbnailsRef">
<div class="thumbnail-item" v-for="stockType in rStock.stockString.split(';')">
<div
class="thumbnail-item"
v-for="stockType in rStock.stockString.split(';')"
:key="stockType"
>
<div class="thumbnail-container">
<div>{{ stockType }}</div>
<img
@@ -61,7 +97,7 @@
:title="stockType"
style="opacity: 0"
@error="(e) => onStockItemError(e, stockType)"
@load="e => (e.target as HTMLElement).style.opacity = '1'"
@load="(e) => ((e.target as HTMLElement).style.opacity = '1')"
/>
</div>
</div>
@@ -75,23 +111,24 @@
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { defineComponent } from "vue";
import { useStore } from '../../store';
import imageMixin from '../../mixins/imageMixin';
import stockMixin from '../../mixins/stockMixin';
import { useStore } from "../../store";
import imageMixin from "../../mixins/imageMixin";
import stockMixin from "../../mixins/stockMixin";
import { IReadyStockItem } from '../../types';
import { IReadyStockItem } from "../../types";
import http from "../../http";
interface ResponseJSONData {
[key: string]: string;
}
function getVehicleType(stockType: string) {
if (/^E/.test(stockType)) return 'loco-e';
if (/^S/.test(stockType)) return 'loco-s';
if (/^E/.test(stockType)) return "loco-e";
if (/^S/.test(stockType)) return "loco-s";
return 'car-passenger';
return "car-passenger";
}
export default defineComponent({
@@ -99,11 +136,15 @@ export default defineComponent({
data: () => ({
store: useStore(),
responseStatus: 'loading',
isMobile: 'ontouchstart' in document.documentElement && navigator.userAgent.match(/Mobi/) ? true : false,
responseStatus: "loading",
isMobile:
"ontouchstart" in document.documentElement &&
navigator.userAgent.match(/Mobi/)
? true
: false,
observer: null as IntersectionObserver | null,
searchedReadyStockName: '',
searchedReadyStockString: '',
searchedReadyStockName: "",
searchedReadyStockString: "",
visibleIndexesTo: 0,
lastSelectedStockId: null as string | null,
scrollTop: 0,
@@ -115,11 +156,11 @@ export default defineComponent({
},
activated() {
(this.$refs['focus'] as HTMLElement).focus();
(this.$refs["focus"] as HTMLElement).focus();
(this.$refs['list'] as HTMLElement).scrollTo({
(this.$refs["list"] as HTMLElement).scrollTo({
top: this.scrollTop,
behavior: 'auto',
behavior: "auto",
});
},
@@ -130,8 +171,12 @@ export default defineComponent({
return this.store.readyStockList
.filter(
(rs) =>
rs.stockId.toLocaleLowerCase().includes(this.searchedReadyStockName.toLocaleLowerCase()) &&
rs.stockString.toLocaleLowerCase().includes(this.searchedReadyStockString.toLocaleLowerCase())
rs.stockId
.toLocaleLowerCase()
.includes(this.searchedReadyStockName.toLocaleLowerCase()) &&
rs.stockString
.toLocaleLowerCase()
.includes(this.searchedReadyStockString.toLocaleLowerCase()),
)
.filter((_, i) => i <= this.visibleIndexesTo);
},
@@ -139,7 +184,7 @@ export default defineComponent({
computedAvailableStockTypes() {
return this.store.readyStockList
.reduce((acc, rs) => {
rs.stockString.split(';').forEach((s) => {
rs.stockString.split(";").forEach((s) => {
if (!acc.includes(s)) acc.push(s);
});
@@ -153,7 +198,7 @@ export default defineComponent({
computedReadyStockList(curr, prev) {
if (curr.length < prev.length) {
this.visibleIndexesTo = 20;
(this.$refs['list'] as HTMLElement).scrollTo({
(this.$refs["list"] as HTMLElement).scrollTo({
top: 0,
});
}
@@ -162,21 +207,21 @@ export default defineComponent({
methods: {
async fetchStockListData() {
const readyStockJSONData: ResponseJSONData = await (
await fetch(`https://spythere.github.io/api/td2/data/readyStock.json?t=${Math.floor(Date.now() / 60000)}`)
).json();
const readyStockJSONData = (
await http.get<ResponseJSONData>("td2/data/readyStock.json")
).data;
if (!readyStockJSONData) {
this.responseStatus = 'error';
this.responseStatus = "error";
return;
}
for (let stockKey in readyStockJSONData) {
const [type, number, ...name] = stockKey.split(' ');
const [type, number, ...name] = stockKey.split(" ");
const obj = {
number: number.replace(/_/g, '/'),
name: name.join(' '),
number: number.replace(/_/g, "/"),
name: name.join(" "),
stockString: readyStockJSONData[stockKey],
type,
};
@@ -187,7 +232,7 @@ export default defineComponent({
});
}
this.responseStatus = 'loaded';
this.responseStatus = "loaded";
},
mountObserver() {
@@ -195,7 +240,7 @@ export default defineComponent({
if (entries[0].intersectionRatio > 0) this.visibleIndexesTo += 20;
});
this.observer.observe(this.$refs['bottom'] as HTMLElement);
this.observer.observe(this.$refs["bottom"] as HTMLElement);
},
getImageUrl(name: string) {
@@ -203,8 +248,8 @@ export default defineComponent({
},
resetStockFilters() {
this.searchedReadyStockName = '';
this.searchedReadyStockString = '';
this.searchedReadyStockName = "";
this.searchedReadyStockString = "";
},
chooseStock(stockItem: IReadyStockItem) {
@@ -216,7 +261,7 @@ export default defineComponent({
onStockItemError(e: Event, stockType: string) {
const imageEl = e.target as HTMLImageElement;
imageEl.src = `images/${getVehicleType(stockType)}-unknown.png`;
imageEl.style.opacity = '1';
imageEl.style.opacity = "1";
},
onListScroll(e: Event) {
@@ -230,7 +275,7 @@ export default defineComponent({
</script>
<style lang="scss" scoped>
@import '../../styles/global.scss';
@import "../../styles/global.scss";
.exit-btn {
font-size: 1.2em;
@@ -316,7 +361,7 @@ ul {
gap: 1rem;
padding: 0.1em;
&[data-last-selected='true'] .stock-title {
&[data-last-selected="true"] .stock-title {
border: 1px solid $accentColor;
}
@@ -378,4 +423,3 @@ ul {
padding: 1em;
}
</style>
+54 -34
View File
@@ -1,12 +1,13 @@
<template>
<section class="inputs-section">
<div class="input_container">
<h2 class="input_header">{{ $t('inputs.title') }}</h2>
<h2 class="input_header">{{ $t("inputs.title") }}</h2>
<div class="input_list type">
<div class="vehicle-types locos">
<button
v-for="locoType in locomotiveTypeList"
:key="locoType.id"
class="btn btn--choice"
:data-selected="locoType.id == store.chosenLocoPower"
@click="selectLocoType(locoType.id)"
@@ -23,7 +24,9 @@
@keydown.enter.prevent="addOrSwitchVehicle"
@keydown.backspace="removeVehicle"
>
<option :value="null" disabled>{{ $t('inputs.input-vehicle') }}</option>
<option :value="null" disabled>
{{ $t("inputs.input-vehicle") }}
</option>
<option v-for="loco in locoOptions" :value="loco" :key="loco.type">
{{ loco.type }}<b v-if="loco.supportersOnly">*</b>
</option>
@@ -34,6 +37,7 @@
<div class="vehicle-types carwagons">
<button
v-for="carType in carTypeList"
:key="carType.id"
class="btn btn--choice"
:data-selected="carType.id == store.chosenCarUseType"
@click="selectCarWagonType(carType.id)"
@@ -50,7 +54,9 @@
@keydown.enter.prevent="addOrSwitchVehicle"
@keydown.backspace="removeVehicle"
>
<option :value="null" disabled>{{ $t('inputs.input-carwagon') }}</option>
<option :value="null" disabled>
{{ $t("inputs.input-carwagon") }}
</option>
<option v-for="car in carOptions" :value="car" :key="car.type">
{{ car.type }}<b v-if="car.supportersOnly">*</b>
@@ -59,7 +65,7 @@
</div>
<div class="input_list cargo">
<label for="cargo-select">{{ $t('inputs.cargo-title') }}</label>
<label for="cargo-select">{{ $t("inputs.cargo-title") }}</label>
<select
id="cargo-select"
:disabled="
@@ -75,20 +81,30 @@
@keydown.enter.prevent="addOrSwitchVehicle"
@keydown.backspace="removeVehicle"
>
<option :value="null" v-if="!store.chosenCar || !store.chosenCar.loadable">
{{ $t('inputs.no-cargo-available') }}
<option
:value="null"
v-if="!store.chosenCar || !store.chosenCar.loadable"
>
{{ $t("inputs.no-cargo-available") }}
</option>
<option :value="null" v-else>{{ $t('inputs.cargo-empty') }}</option>
<option :value="null" v-else>{{ $t("inputs.cargo-empty") }}</option>
<option v-for="cargo in store.chosenCar?.cargoList" :value="cargo" :key="cargo.id">
<option
v-for="cargo in store.chosenCar?.cargoList"
:value="cargo"
:key="cargo.id"
>
{{ cargo.id }}
</option>
</select>
</div>
<div class="input_actions">
<button class="btn" @click="addVehicle(store.chosenVehicle, store.chosenCargo)">
{{ $t('inputs.action-add') }}
<button
class="btn"
@click="addVehicle(store.chosenVehicle, store.chosenCargo)"
>
{{ $t("inputs.action-add") }}
</button>
<button
class="btn"
@@ -96,14 +112,18 @@
:disabled="store.chosenStockListIndex == -1"
:data-disabled="store.chosenStockListIndex == -1"
>
{{ $t('inputs.action-swap') }}
{{ $t("inputs.action-swap") }}
<b class="text--accent">
{{ store.chosenStockListIndex == -1 ? '' : `${store.chosenStockListIndex + 1}.` }}
{{
store.chosenStockListIndex == -1
? ""
: `${store.chosenStockListIndex + 1}.`
}}
</b>
</button>
<button class="btn" @click="store.isRealStockListCardOpen = true">
<b>{{ $t('inputs.real-stock') }}</b>
<b>{{ $t("inputs.real-stock") }}</b>
</button>
</div>
</div>
@@ -111,12 +131,12 @@
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { defineComponent } from "vue";
import imageMixin from '../../mixins/imageMixin';
import { useStore } from '../../store';
import stockPreviewMixin from '../../mixins/stockPreviewMixin';
import stockMixin from '../../mixins/stockMixin';
import imageMixin from "../../mixins/imageMixin";
import { useStore } from "../../store";
import stockPreviewMixin from "../../mixins/stockPreviewMixin";
import stockMixin from "../../mixins/stockMixin";
export default defineComponent({
mixins: [imageMixin, stockPreviewMixin, stockMixin],
@@ -124,31 +144,31 @@ export default defineComponent({
data: () => ({
locomotiveTypeList: [
{
id: 'loco-e',
desc: 'ELEKTRYCZNE',
id: "loco-e",
desc: "ELEKTRYCZNE",
},
{
id: 'loco-s',
desc: 'SPALINOWE',
id: "loco-s",
desc: "SPALINOWE",
},
{
id: 'loco-ezt',
desc: 'ELEKTR. ZESPOŁY TRAKCYJNE',
id: "loco-ezt",
desc: "ELEKTR. ZESPOŁY TRAKCYJNE",
},
{
id: 'loco-szt',
desc: 'SPAL. ZESPOŁY TRAKCYJNE',
id: "loco-szt",
desc: "SPAL. ZESPOŁY TRAKCYJNE",
},
],
carTypeList: [
{
id: 'car-passenger',
desc: 'PASAŻERSKIE',
id: "car-passenger",
desc: "PASAŻERSKIE",
},
{
id: 'car-cargo',
desc: 'TOWAROWE',
id: "car-cargo",
desc: "TOWAROWE",
},
],
}),
@@ -169,7 +189,8 @@ export default defineComponent({
addOrSwitchVehicle() {
if (!this.store.chosenVehicle) return;
if (this.store.chosenStockListIndex == -1) this.addVehicle(this.store.chosenVehicle, this.store.chosenCargo);
if (this.store.chosenStockListIndex == -1)
this.addVehicle(this.store.chosenVehicle, this.store.chosenCargo);
else this.switchVehicles();
},
@@ -197,7 +218,7 @@ export default defineComponent({
</script>
<style lang="scss" scoped>
@import '../../styles/global';
@import "../../styles/global";
.inputs-section {
display: flex;
@@ -215,7 +236,7 @@ button.btn--choice {
font-size: 0.9em;
padding: 0.3em 0.6em;
&[data-selected='true'] {
&[data-selected="true"] {
background-color: $accentColor;
color: black;
}
@@ -267,4 +288,3 @@ button.btn--choice {
}
}
</style>
+15 -11
View File
@@ -1,11 +1,16 @@
<template>
<section class="logo-section">
<img :src="`/logo-${$i18n.locale}.svg`" alt="logo pojazdownik" @click="navigate" />
<img
:src="`/logo-${$i18n.locale}.svg`"
alt="logo pojazdownik"
@click="navigate"
/>
<div class="actions">
<button
class="btn btn--text"
v-for="action in localeActions"
:key="action.name"
class="btn btn--text"
:data-selected="$i18n.locale == action.locale"
@click="chooseLocale(action.locale)"
>
@@ -21,31 +26,31 @@ export default {
return {
localeActions: [
{
name: 'POLSKI',
locale: 'pl',
name: "POLSKI",
locale: "pl",
},
{
name: 'ENGLISH',
locale: 'en',
name: "ENGLISH",
locale: "en",
},
],
};
},
methods: {
navigate() {
window.location.pathname = '';
window.location.pathname = "";
},
chooseLocale(locale: string) {
this.$i18n.locale = locale;
window.localStorage.setItem('locale', locale);
window.localStorage.setItem("locale", locale);
},
},
};
</script>
<style lang="scss" scoped>
@import '../../styles/global.scss';
@import "../../styles/global.scss";
.logo-section {
grid-row: 1;
@@ -64,7 +69,7 @@ export default {
display: flex;
gap: 0.5em;
button[data-selected='true'] {
button[data-selected="true"] {
font-weight: bold;
color: $accentColor;
text-decoration: underline;
@@ -76,4 +81,3 @@ img {
width: 100%;
}
</style>
+26 -18
View File
@@ -2,9 +2,10 @@
<section class="stock-section">
<div class="section_modes">
<button
v-for="(id, i) in sectionModes"
:key="id"
class="btn"
ref="sectionButtonRefs"
v-for="(id, i) in sectionModes"
@click="chooseSection(id)"
:data-selected="store.stockSectionMode == id"
>
@@ -15,29 +16,37 @@
<transition name="tab-change" mode="out-in">
<keep-alive>
<component :is="chosenSectionComponent" :key="chosenSectionComponent"></component>
<component
:is="chosenSectionComponent"
:key="chosenSectionComponent"
></component>
</keep-alive>
</transition>
</section>
</template>
<script lang="ts" setup>
import { computed, KeepAlive, onMounted, ref } from 'vue';
import { useStore } from '../../store';
import StockListTab from '../tabs/StockListTab.vue';
import StockGeneratorTab from '../tabs/StockGeneratorTab.vue';
import NumberGeneratorTab from '../tabs/NumberGeneratorTab.vue';
import WikiListTab from '../tabs/WikiListTab.vue';
import { computed, onMounted, ref } from "vue";
import { useStore } from "../../store";
import StockListTab from "../tabs/StockListTab.vue";
import StockGeneratorTab from "../tabs/StockGeneratorTab.vue";
import NumberGeneratorTab from "../tabs/NumberGeneratorTab.vue";
import WikiListTab from "../tabs/WikiListTab.vue";
const sectionButtonRefs = ref([]);
const store = useStore();
type SectionMode = typeof store.stockSectionMode;
const sectionModes: SectionMode[] = ['stock-list', 'wiki-list', 'number-generator', 'stock-generator'];
const sectionModes: SectionMode[] = [
"stock-list",
"wiki-list",
"number-generator",
"stock-generator",
];
onMounted(() => {
window.addEventListener('keydown', (e) => {
window.addEventListener("keydown", (e) => {
if (e.target instanceof HTMLInputElement) return;
if (/[1234]/.test(e.key)) {
@@ -50,16 +59,16 @@ onMounted(() => {
const chosenSectionComponent = computed(() => {
switch (store.stockSectionMode) {
case 'stock-list':
case "stock-list":
return StockListTab;
case 'wiki-list':
case "wiki-list":
return WikiListTab;
case 'stock-generator':
case "stock-generator":
return StockGeneratorTab;
case 'number-generator':
case "number-generator":
return NumberGeneratorTab;
default:
@@ -73,7 +82,7 @@ function chooseSection(sectionId: SectionMode) {
</script>
<style lang="scss">
@import '../../styles/global.scss';
@import "../../styles/global.scss";
// Tab change animation
.tab-change {
@@ -115,14 +124,14 @@ function chooseSection(sectionId: SectionMode) {
left: 50%;
transform: translateX(-50%);
content: '';
content: "";
width: 0;
height: 2px;
transition: all 100ms;
background-color: $accentColor;
}
&[data-selected='true']::after {
&[data-selected="true"]::after {
width: 100%;
}
}
@@ -134,4 +143,3 @@ function chooseSection(sectionId: SectionMode) {
}
}
</style>
+43 -20
View File
@@ -1,18 +1,26 @@
<template>
<section class="train-image-section">
<div class="train-image__wrapper">
<div class="train-image__content" :class="{ supporter: store.chosenVehicle?.supportersOnly }">
<div
class="train-image__content"
:class="{ supporter: store.chosenVehicle?.supportersOnly }"
>
<transition name="img-message-anim">
<div class="empty-message" v-if="store.imageLoading && store.chosenVehicle?.imageSrc">
{{ $t('preview.loading') }}
<div
class="empty-message"
v-if="store.imageLoading && store.chosenVehicle?.imageSrc"
>
{{ $t("preview.loading") }}
</div>
</transition>
<div class="no-img" v-if="!store.chosenVehicle">{{ $t('preview.title') }}</div>
<div class="no-img" v-if="!store.chosenVehicle">
{{ $t("preview.title") }}
</div>
<img
v-if="store.chosenVehicle"
:src="`https://spythere.github.io/api/td2/images/${store.chosenVehicle.type}--300px.jpg`"
:src="getThumbnailURL(store.chosenVehicle.type, 'small')"
:alt="store.chosenVehicle.type"
@load="onImageLoad"
@click="onImageClick"
@@ -25,44 +33,57 @@
<b class="text--accent">{{ store.chosenVehicle.type }}</b> &bull;
<b style="color: #ccc">
{{
$t(`preview.${isLocomotive(store.chosenVehicle) ? store.chosenVehicle.power : store.chosenVehicle.useType}`)
$t(
`preview.${
isLocomotive(store.chosenVehicle)
? store.chosenVehicle.power
: store.chosenVehicle.useType
}`,
)
}}
</b>
<div style="color: #ccc">
<div>
{{ store.chosenVehicle.length }}m | {{ store.chosenVehicle.mass }}t |
{{ store.chosenVehicle.maxSpeed }} km/h
{{ store.chosenVehicle.length }}m | {{ store.chosenVehicle.mass }}t
| {{ store.chosenVehicle.maxSpeed }} km/h
</div>
<div v-if="isLocomotive(store.chosenVehicle)">
{{ $t('preview.cabin') }} {{ store.chosenVehicle.cabinType }}
{{ $t("preview.cabin") }} {{ store.chosenVehicle.cabinType }}
</div>
<div v-else>
{{
store.chosenVehicle.useType == 'car-cargo' // ? store.stockData?.usage[store.chosenVehicle.constructionType]
store.chosenVehicle.useType == "car-cargo" // ? store.stockData?.usage[store.chosenVehicle.constructionType]
? $t(`usage.${store.chosenVehicle.constructionType}`)
: `${$t('preview.construction')} ${store.chosenVehicle.constructionType}`
: `${$t("preview.construction")} ${
store.chosenVehicle.constructionType
}`
}}
</div>
<b style="color: salmon" v-if="store.chosenVehicle.supportersOnly">{{ $t('preview.sponsor-only') }}</b>
<b style="color: salmon" v-if="store.chosenVehicle.supportersOnly">{{
$t("preview.sponsor-only")
}}</b>
</div>
</div>
<div class="train-image__info" v-else>{{ $t('preview.desc') }}</div>
<div class="train-image__info" v-else>{{ $t("preview.desc") }}</div>
</div>
</section>
</template>
<script lang="ts">
import { computed, defineComponent } from 'vue';
import { useStore } from '../../store';
import { isLocomotive } from '../../utils/vehicleUtils';
import { ILocomotive, Vehicle } from '../../types';
import { computed, defineComponent } from "vue";
import { useStore } from "../../store";
import { isLocomotive } from "../../utils/vehicleUtils";
import { ILocomotive, Vehicle } from "../../types";
import imageMixin from "../../mixins/imageMixin";
export default defineComponent({
mixins: [imageMixin],
setup() {
const store = useStore();
@@ -94,14 +115,17 @@ export default defineComponent({
if (!chosenVehicle) return;
this.store.vehiclePreviewSrc = `https://spythere.github.io/api/td2/images/${chosenVehicle.type}--800px.jpg`;
this.store.vehiclePreviewSrc = this.getThumbnailURL(
chosenVehicle.type,
"large",
);
},
},
});
</script>
<style lang="scss" scoped>
@import '../../styles/global.scss';
@import "../../styles/global.scss";
.train-image-section {
grid-row: 3;
@@ -185,4 +209,3 @@ export default defineComponent({
}
}
</style>
+68 -32
View File
@@ -1,34 +1,55 @@
<template>
<div class="number-generator tab">
<div class="tab_header">
<h2>{{ $t('numgen.title') }}</h2>
<h2>{{ $t("numgen.title") }}</h2>
</div>
<div class="tab_content">
<div class="options">
<select v-model="chosenCategory" @change="randomizeTrainNumber()">
<option :value="null" disabled>{{ $t('numgen.train-category') }}</option>
<option v-for="(_, category) in genData.categories" :value="category">
<option :value="null" disabled>
{{ $t("numgen.train-category") }}
</option>
<option
v-for="(_, category) in genData.categories"
:key="category"
:value="category"
>
{{ $t(`numgen.categories.${category}`) }}
</option>
</select>
<select v-model="beginRegionName" @change="randomizeTrainNumber()">
<option :value="null" disabled>{{ $t('numgen.start-region') }}</option>
<option v-for="(_, name) in genData.regionNumbers" :value="name">{{ name }}</option>
<option :value="null" disabled>
{{ $t("numgen.start-region") }}
</option>
<option
v-for="(_, name) in genData.regionNumbers"
:key="name"
:value="name"
>
{{ name }}
</option>
</select>
<select v-model="endRegionName" @change="randomizeTrainNumber()">
<option :value="null" disabled>{{ $t('numgen.end-region') }}</option>
<option v-for="(_, name) in genData.regionNumbers" :value="name">{{ name }}</option>
<option :value="null" disabled>{{ $t("numgen.end-region") }}</option>
<option
v-for="(_, name) in genData.regionNumbers"
:key="name"
:value="name"
>
{{ name }}
</option>
</select>
</div>
<div class="generated-number" @click="copyNumber">
<span v-if="trainNumber">
{{ $t('numgen.number-info') }} <b class="text--accent">{{ trainNumber }}</b>
{{ $t("numgen.number-info") }}
<b class="text--accent">{{ trainNumber }}</b>
</span>
<span v-else>{{ $t('numgen.warning') }}</span>
<span v-else>{{ $t("numgen.warning") }}</span>
</div>
<!-- <div v-if="chosenCategory">
@@ -50,25 +71,29 @@
<div class="tab_links">
<a :href="$t('numgen.td2-wiki-link')" target="_blank">
{{ $t('numgen.td2-wiki') }}
{{ $t("numgen.td2-wiki") }}
</a>
</div>
<hr />
<div class="tab_actions">
<button class="btn" @click="randomizeTrainNumber(true)">{{ $t('numgen.action-random-region') }}</button>
<button class="btn" @click="randomizeTrainNumber(false)">{{ $t('numgen.action-random-number') }}</button>
<button class="btn" @click="randomizeTrainNumber(true)">
{{ $t("numgen.action-random-region") }}
</button>
<button class="btn" @click="randomizeTrainNumber(false)">
{{ $t("numgen.action-random-number") }}
</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { Ref, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { Ref, ref } from "vue";
import { useI18n } from "vue-i18n";
import genData from '../../constants/numberGeneratorData.json';
import genData from "../../constants/numberGeneratorData.json";
const i18n = useI18n();
type RegionName = keyof typeof genData.regionNumbers;
@@ -83,7 +108,7 @@ const trainNumber = ref(null) as Ref<string | null>;
const copyNumber = () => {
if (trainNumber.value) {
navigator.clipboard.writeText(trainNumber.value);
alert(i18n.t('numgen.alert'));
alert(i18n.t("numgen.alert"));
}
};
@@ -93,16 +118,21 @@ const randomizeTrainNumber = (randomizeRegions = false) => {
const regionKeys = Object.keys(genData.regionNumbers);
if (beginRegionName.value == null || randomizeRegions)
beginRegionName.value = regionKeys[(regionKeys.length * Math.random()) << 0] as RegionName;
beginRegionName.value = regionKeys[
(regionKeys.length * Math.random()) << 0
] as RegionName;
if (endRegionName.value == null || randomizeRegions)
endRegionName.value = regionKeys[(regionKeys.length * Math.random()) << 0] as RegionName;
endRegionName.value = regionKeys[
(regionKeys.length * Math.random()) << 0
] as RegionName;
let number = '';
let number = "";
if (beginRegionName.value == endRegionName.value) {
const sameRegionsNumbers = genData.sameRegions[beginRegionName.value!];
const randRegionNumber = sameRegionsNumbers[Math.floor(Math.random() * sameRegionsNumbers.length)];
const randRegionNumber =
sameRegionsNumbers[Math.floor(Math.random() * sameRegionsNumbers.length)];
number += randRegionNumber.toString();
} else {
const beginRegionNumber = genData.regionNumbers[beginRegionName.value!];
@@ -117,23 +147,30 @@ const randomizeTrainNumber = (randomizeRegions = false) => {
return;
}
if (chosenCategory.value == null) chosenCategory.value = 'EI';
if (chosenCategory.value == null) chosenCategory.value = "EI";
const rulesArray = genData.categories[chosenCategory.value].split(';').map((r) => ({
index: r.split(':')[0],
rule: r.split(':')[1],
nums: Number(r.split(':')[2] || '1'),
}));
const rulesArray = genData.categories[chosenCategory.value]
.split(";")
.map((r) => ({
index: r.split(":")[0],
rule: r.split(":")[1],
nums: Number(r.split(":")[2] || "1"),
}));
rulesArray.forEach((r) => {
const range = r.rule.split('-');
const range = r.rule.split("-");
if (range.length == 1) number += r.rule;
else {
const [minRange, maxRange] = range;
const randRange = Math.floor(Math.random() * (Number(maxRange) - Number(minRange)) + Number(minRange)).toString();
const randRange = Math.floor(
Math.random() * (Number(maxRange) - Number(minRange)) +
Number(minRange),
).toString();
number += new Array(Math.abs(randRange.length - r.nums)).fill('0').join('') + randRange;
number +=
new Array(Math.abs(randRange.length - r.nums)).fill("0").join("") +
randRange;
}
});
@@ -142,8 +179,8 @@ const randomizeTrainNumber = (randomizeRegions = false) => {
</script>
<style lang="scss" scoped>
@import '../../styles/tab.scss';
@import '../../styles/global.scss';
@import "../../styles/tab.scss";
@import "../../styles/global.scss";
.options {
display: grid;
@@ -189,4 +226,3 @@ const randomizeTrainNumber = (randomizeRegions = false) => {
}
}
</style>
+141 -69
View File
@@ -1,45 +1,64 @@
<template>
<div class="stock-generator tab">
<div class="tab_header">
<h2>{{ $t('stockgen.title') }}</h2>
<h2>{{ $t("stockgen.title") }}</h2>
</div>
<div class="tab_content">
<div>
<h2>{{ $t('stockgen.properties-title') }}</h2>
<h2>{{ $t("stockgen.properties-title") }}</h2>
<b class="text--accent">
{{ $t('stockgen.properties-desc') }}
{{ $t("stockgen.properties-desc") }}
</b>
<div class="tab_attributes">
<label>
{{ $t('stockgen.input-mass') }}
<input type="number" v-model="maxMass" step="100" max="4000" min="0" />
{{ $t("stockgen.input-mass") }}
<input
type="number"
v-model="maxMass"
step="100"
max="4000"
min="0"
/>
</label>
<label>
{{ $t('stockgen.input-length') }}
<input type="number" v-model="maxLength" step="25" max="650" min="0" />
{{ $t("stockgen.input-length") }}
<input
type="number"
v-model="maxLength"
step="25"
max="650"
min="0"
/>
</label>
<label>
{{ $t('stockgen.input-carcount') }}
<input type="number" v-model="maxCarCount" step="1" max="60" min="1" />
{{ $t("stockgen.input-carcount") }}
<input
type="number"
v-model="maxCarCount"
step="1"
max="60"
min="1"
/>
</label>
</div>
</div>
<div>
<h2>{{ $t('stockgen.cargo-title') }}</h2>
<b>{{ $t('stockgen.cargo-desc') }}</b>
<h2>{{ $t("stockgen.cargo-title") }}</h2>
<b>{{ $t("stockgen.cargo-desc") }}</b>
</div>
<div class="generator_cargo">
<button
v-for="(cargoArray, cargoName) in store.stockData?.generator.cargo"
:key="cargoName"
class="btn"
:data-chosen="chosenCargoTypes.includes(cargoName.toString())"
v-for="(cargoArray, cargoName) in store.stockData?.generator.cargo"
@click="toggleCargoChosen(cargoName.toString(), cargoArray)"
>
{{ $t(`cargo.${cargoName}`) }}
@@ -47,15 +66,15 @@
</div>
<div>
<h2>{{ $t('stockgen.chosen-title') }}</h2>
<h2>{{ $t("stockgen.chosen-title") }}</h2>
<div class="generator_warning">
<span v-if="computedChosenCarTypes.size == 0">
{{ $t('stockgen.chosen-empty-warning') }}
{{ $t("stockgen.chosen-empty-warning") }}
</span>
<span v-else>
{{ $t('stockgen.chosen-warning') }}
{{ $t("stockgen.chosen-warning") }}
</span>
</div>
</div>
@@ -78,16 +97,28 @@
<hr />
<div class="tab_actions">
<button class="btn" :data-disabled="computedChosenCarTypes.size == 0" @click="generateStock()">
{{ $t('stockgen.action-generate') }}
<button
class="btn"
:data-disabled="computedChosenCarTypes.size == 0"
@click="generateStock()"
>
{{ $t("stockgen.action-generate") }}
</button>
<button class="btn" :data-disabled="computedChosenCarTypes.size == 0" @click="generateStock(true)">
{{ $t('stockgen.action-generate-empty') }}
<button
class="btn"
:data-disabled="computedChosenCarTypes.size == 0"
@click="generateStock(true)"
>
{{ $t("stockgen.action-generate-empty") }}
</button>
<button class="btn" :data-disabled="computedChosenCarTypes.size == 0" @click="resetChosenCargo">
{{ $t('stockgen.action-reset') }}
<button
class="btn"
:data-disabled="computedChosenCarTypes.size == 0"
@click="resetChosenCargo"
>
{{ $t("stockgen.action-reset") }}
</button>
</div>
</div>
@@ -95,15 +126,15 @@
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useStore } from '../../store';
import { defineComponent } from "vue";
import { useStore } from "../../store";
import stockMixin from '../../mixins/stockMixin';
import { ICargo, ICarWagon, IStock } from '../../types';
import warningsMixin from '../../mixins/warningsMixin';
import stockMixin from "../../mixins/stockMixin";
import { ICargo, ICarWagon, IStock } from "../../types";
import warningsMixin from "../../mixins/warningsMixin";
export default defineComponent({
name: 'stock-generator',
name: "stock-generator",
mixins: [stockMixin, warningsMixin],
@@ -126,7 +157,9 @@ export default defineComponent({
computed: {
computedChosenCarTypes() {
return new Set<string>(this.chosenCarTypes.sort((c1, c2) => (c1 > c2 ? 1 : -1)));
return new Set<string>(
this.chosenCarTypes.slice().sort((c1, c2) => (c1 > c2 ? 1 : -1)),
);
},
},
@@ -150,53 +183,84 @@ export default defineComponent({
},
generateStock(empty = false) {
const generatedChosenStockList = this.chosenCargoTypes.reduce((acc, type) => {
this.store.stockData?.generator.cargo[type]
.filter((c) => !this.excludedCarTypes.includes(c.split(':')[0]))
.forEach((c) => {
const [type, cargoType] = c.split(':');
const generatedChosenStockList = this.chosenCargoTypes.reduce(
(acc, type) => {
this.store.stockData?.generator.cargo[type]
.filter((c) => !this.excludedCarTypes.includes(c.split(":")[0]))
.forEach((c) => {
const [type, cargoType] = c.split(":");
const carWagonObjs = this.store.carDataList.filter((cw) => cw.type.startsWith(type));
const cargoObjs = [] as (ICargo | undefined)[];
const carWagonObjs = this.store.carDataList.filter((cw) =>
cw.type.startsWith(type),
);
const cargoObjs = [] as (ICargo | undefined)[];
if (!cargoType || empty) cargoObjs.push(undefined);
else if (cargoType == 'all') cargoObjs.push(...carWagonObjs[0]?.cargoList);
else cargoObjs.push(carWagonObjs[0]?.cargoList.find((cargo) => cargo.id == cargoType));
if (!cargoType || empty) cargoObjs.push(undefined);
else if (cargoType == "all")
cargoObjs.push(...carWagonObjs[0]!.cargoList);
else
cargoObjs.push(
carWagonObjs[0]?.cargoList.find(
(cargo) => cargo.id == cargoType,
),
);
carWagonObjs.forEach((cw) => {
cargoObjs.forEach((cargoObj) => {
const chosenStock = acc.find((a) => a.constructionType.includes(cw.constructionType));
carWagonObjs.forEach((cw) => {
cargoObjs.forEach((cargoObj) => {
const chosenStock = acc.find((a) =>
a.constructionType.includes(cw.constructionType),
);
if (!chosenStock)
acc.push({
constructionType: cw.constructionType,
carPool: [{ carWagon: cw, cargo: cargoObj }],
});
else chosenStock.carPool.push({ carWagon: cw, cargo: cargoObj });
if (!chosenStock)
acc.push({
constructionType: cw.constructionType,
carPool: [{ carWagon: cw, cargo: cargoObj }],
});
else
chosenStock.carPool.push({ carWagon: cw, cargo: cargoObj });
});
});
});
});
return acc;
}, [] as { constructionType: string; carPool: { carWagon: ICarWagon; cargo?: ICargo }[] }[]);
return acc;
},
[] as {
constructionType: string;
carPool: { carWagon: ICarWagon; cargo?: ICargo }[];
}[],
);
let bestGeneration: { stockList: IStock[]; value: number } = { stockList: [], value: 0 };
let bestGeneration: { stockList: IStock[]; value: number } = {
stockList: [],
value: 0,
};
for (let i = 0; i < 10; i++) {
const headingLoco = this.store.stockList[0]?.isLoco ? this.store.stockList[0] : undefined;
const headingLoco = this.store.stockList[0]?.isLoco
? this.store.stockList[0]
: undefined;
this.store.stockList.length = headingLoco ? 1 : 0;
const maxMass =
this.store.acceptableMass > 0 ? Math.min(this.store.acceptableMass, this.maxMass) : this.maxMass;
this.store.acceptableMass > 0
? Math.min(this.store.acceptableMass, this.maxMass)
: this.maxMass;
let exceeded = false;
while (!exceeded) {
const randomStockType = generatedChosenStockList[~~(Math.random() * generatedChosenStockList.length)];
const { carWagon, cargo } = randomStockType.carPool[~~(Math.random() * randomStockType.carPool.length)];
const randomStockType =
generatedChosenStockList[
~~(Math.random() * generatedChosenStockList.length)
];
const { carWagon, cargo } =
randomStockType.carPool[
~~(Math.random() * randomStockType.carPool.length)
];
if (
this.store.totalMass + (cargo?.totalMass || carWagon.mass) > maxMass ||
this.store.totalMass + (cargo?.totalMass || carWagon.mass) >
maxMass ||
this.store.totalLength + carWagon.length > this.maxLength ||
this.store.stockList.length > this.maxCarCount
) {
@@ -207,7 +271,10 @@ export default defineComponent({
this.addCarWagon(carWagon, cargo);
}
const currentGenerationValue = this.store.totalLength + this.store.totalMass + this.store.stockList.length;
const currentGenerationValue =
this.store.totalLength +
this.store.totalMass +
this.store.stockList.length;
if (bestGeneration.value < currentGenerationValue) {
bestGeneration.stockList = this.store.stockList;
@@ -216,11 +283,12 @@ export default defineComponent({
}
this.store.stockList = bestGeneration.stockList;
this.store.stockSectionMode = 'stock-list';
this.store.stockSectionMode = "stock-list";
},
previewCar(type: string) {
const c = this.store.carDataList.find((c) => c.type.startsWith(type)) || null;
const c =
this.store.carDataList.find((c) => c.type.startsWith(type)) || null;
this.store.chosenVehicle = c;
this.store.chosenCar = c;
@@ -233,33 +301,38 @@ export default defineComponent({
toggleCargoChosen(cargoType: string, vehicles: string[]) {
if (this.chosenCargoTypes.includes(cargoType)) {
vehicles.forEach((v) => {
const [type] = v.split(':');
const [type] = v.split(":");
this.chosenCarTypes.splice(this.chosenCarTypes.indexOf(type), 1);
});
this.chosenCargoTypes.splice(this.chosenCargoTypes.indexOf(cargoType), 1);
this.chosenCargoTypes.splice(
this.chosenCargoTypes.indexOf(cargoType),
1,
);
return;
}
this.chosenCargoTypes.push(cargoType);
vehicles.forEach((v) => {
const [type] = v.split(':');
const [type] = v.split(":");
this.chosenCarTypes.push(type);
});
},
toggleCarExclusion(type: string) {
if (!this.excludedCarTypes.includes(type)) this.excludedCarTypes.push(type);
else this.excludedCarTypes = this.excludedCarTypes.filter((c) => c != type);
if (!this.excludedCarTypes.includes(type))
this.excludedCarTypes.push(type);
else
this.excludedCarTypes = this.excludedCarTypes.filter((c) => c != type);
},
},
});
</script>
<style lang="scss" scoped>
@import '../../styles/global.scss';
@import '../../styles/tab.scss';
@import "../../styles/global.scss";
@import "../../styles/tab.scss";
.generator_cargo,
.generator_vehicles {
@@ -277,14 +350,14 @@ export default defineComponent({
background-color: $secondaryColor;
&[data-chosen='true'] {
&[data-chosen="true"] {
background-color: $accentColor;
color: black;
box-shadow: 0 0 5px 1px $accentColor;
}
&[data-excluded='true'] {
&[data-excluded="true"] {
background-color: gray;
box-shadow: none;
}
@@ -316,4 +389,3 @@ export default defineComponent({
color: black;
}
</style>
+154 -78
View File
@@ -1,47 +1,77 @@
<template>
<section class="stock-list-tab">
<div class="tab_header">
<h2>{{ $t('stocklist.title') }}</h2>
<h2>{{ $t("stocklist.title") }}</h2>
</div>
<div class="stock_actions">
<label class="file-label">
<div class="btn btn--image">
<img src="/images/icon-upload.svg" alt="" />
{{ $t('stocklist.action-upload') }}
{{ $t("stocklist.action-upload") }}
</div>
<input type="file" @change="uploadStock" ref="conFile" accept=".con,.txt" />
<input
type="file"
@change="uploadStock"
ref="conFile"
accept=".con,.txt"
/>
</label>
<button class="btn btn--image" :data-disabled="stockIsEmpty" :disabled="stockIsEmpty" @click="downloadStock">
<button
class="btn btn--image"
:data-disabled="stockIsEmpty"
:disabled="stockIsEmpty"
@click="downloadStock"
>
<img src="/images/icon-download.svg" alt="download icon" />
{{ $t('stocklist.action-download') }}
{{ $t("stocklist.action-download") }}
</button>
<button class="btn btn--image" :data-disabled="stockIsEmpty" :disabled="stockIsEmpty" @click="copyToClipboard">
<button
class="btn btn--image"
:data-disabled="stockIsEmpty"
:disabled="stockIsEmpty"
@click="copyToClipboard"
>
<img src="/images/icon-copy.svg" alt="copy icon" />
{{ $t('stocklist.action-copy') }}
{{ $t("stocklist.action-copy") }}
</button>
<button class="btn btn--image" :data-disabled="stockIsEmpty" :disabled="stockIsEmpty" @click="resetStock">
<button
class="btn btn--image"
:data-disabled="stockIsEmpty"
:disabled="stockIsEmpty"
@click="resetStock"
>
<img src="/images/icon-reset.svg" alt="reset icon" />
{{ $t('stocklist.action-reset') }}
{{ $t("stocklist.action-reset") }}
</button>
<button class="btn btn--image" :data-disabled="stockIsEmpty" :disabled="stockIsEmpty" @click="shuffleCars">
<button
class="btn btn--image"
:data-disabled="stockIsEmpty"
:disabled="stockIsEmpty"
@click="shuffleCars"
>
<img src="/images/icon-shuffle.svg" alt="shuffle icon" />
{{ $t('stocklist.action-shuffle') }}
{{ $t("stocklist.action-shuffle") }}
</button>
</div>
<div class="stock_controls" :data-disabled="store.chosenStockListIndex == -1">
<div
class="stock_controls"
:data-disabled="store.chosenStockListIndex == -1"
>
<b v-if="store.chosenStockListIndex >= 0">
{{ $t('stocklist.vehicle-no') }} <span class="text--accent">{{ store.chosenStockListIndex + 1 }}</span> &nbsp;
{{ $t("stocklist.vehicle-no") }}
<span class="text--accent">{{ store.chosenStockListIndex + 1 }}</span>
&nbsp;
</b>
<b v-else>
{{ $t('stocklist.no-vehicle-chosen') }}
{{ $t("stocklist.no-vehicle-chosen") }}
</b>
<button
@@ -50,7 +80,7 @@
@click="moveUpStock(store.chosenStockListIndex)"
>
<img :src="getIconURL('higher')" alt="move up vehicle" />
{{ $t('stocklist.action-move-up') }}
{{ $t("stocklist.action-move-up") }}
</button>
<button
@@ -59,7 +89,7 @@
@click="moveDownStock(store.chosenStockListIndex)"
>
<img :src="getIconURL('lower')" alt="move down vehicle" />
{{ $t('stocklist.action-move-down') }}
{{ $t("stocklist.action-move-down") }}
</button>
<button
@@ -68,26 +98,34 @@
@click="removeStock(store.chosenStockListIndex)"
>
<img :src="getIconURL('remove')" alt="remove vehicle" />
{{ $t('stocklist.action-remove') }}
{{ $t("stocklist.action-remove") }}
</button>
</div>
<div class="stock_specs">
<b class="real-stock-info" v-if="store.chosenRealStock">
<span class="text--accent">
<img :src="getIconURL(store.chosenRealStock.type)" :alt="store.chosenRealStock.type" />
<img
:src="getIconURL(store.chosenRealStock.type)"
:alt="store.chosenRealStock.type"
/>
{{ store.chosenRealStock.number }} {{ store.chosenRealStock.name }}
</span>
|
</b>
<span>
{{ $t('stocklist.mass') }} <span class="text--accent">{{ store.totalMass }}t</span> ({{
$t('stocklist.mass-accepted')
}}: <span class="text--accent">{{ store.acceptableMass ? store.acceptableMass + 't' : '-' }}</span
>) - {{ $t('stocklist.length') }}:
{{ $t("stocklist.mass") }}
<span class="text--accent">{{ store.totalMass }}t</span> ({{
$t("stocklist.mass-accepted")
}}:
<span class="text--accent">{{
store.acceptableMass ? store.acceptableMass + "t" : "-"
}}</span
>) - {{ $t("stocklist.length") }}:
<span class="text--accent">{{ store.totalLength }}m</span>
- {{ $t('stocklist.vmax') }}: <span class="text--accent">{{ store.maxStockSpeed }} km/h</span>
- {{ $t("stocklist.vmax") }}:
<span class="text--accent">{{ store.maxStockSpeed }} km/h</span>
</span>
</div>
@@ -96,21 +134,25 @@
<input
type="checkbox"
v-model="store.isColdStart"
:disabled="!locoSupportsColdStart(store.stockList[0]?.constructionType || '')"
:disabled="
!locoSupportsColdStart(store.stockList[0]?.constructionType || '')
"
/>
{{ $t('stocklist.coldstart-info') }}
{{ $t("stocklist.coldstart-info") }}
</label>
</div>
<div class="stock_warnings" v-if="stockHasWarnings">
<div class="warning" v-if="locoNotSuitable">(!) {{ $t('stocklist.warning-not-suitable') }}</div>
<div class="warning" v-if="locoNotSuitable">
(!) {{ $t("stocklist.warning-not-suitable") }}
</div>
<div class="warning" v-if="trainTooLong && store.isTrainPassenger">
(!) {{ $t('stocklist.warning-passenger-too-long') }}
(!) {{ $t("stocklist.warning-passenger-too-long") }}
</div>
<div class="warning" v-if="trainTooLong && !store.isTrainPassenger">
(!) {{ $t('stocklist.warning-freight-too-long') }}
(!) {{ $t("stocklist.warning-freight-too-long") }}
</div>
<div class="warning" v-if="trainTooHeavy">
@@ -121,14 +163,14 @@
target="_blank"
href="https://docs.google.com/spreadsheets/d/1bFXUsHsAu4youmNz-46Q1HslZaaoklvfoBDS553TnNk/edit"
>
{{ $t('stocklist.acceptable-mass-docs') }}
{{ $t("stocklist.acceptable-mass-docs") }}
</a>
</template>
</i18n-t>
</div>
<div class="warning" v-if="tooManyLocomotives">
{{ $t('stocklist.warning-too-many-locos') }}
{{ $t("stocklist.warning-too-many-locos") }}
</div>
</div>
@@ -137,7 +179,7 @@
<!-- Stock list -->
<ul ref="stock_list">
<li v-if="stockIsEmpty" class="list-empty">
<div class="stock-info">{{ $t('stocklist.list-empty') }}</div>
<div class="stock-info">{{ $t("stocklist.list-empty") }}</div>
</li>
<TransitionGroup name="stock-list-anim">
@@ -160,18 +202,28 @@
@dragover="allowDrop"
draggable="true"
>
<span class="stock-info__no" :data-selected="i == store.chosenStockListIndex">
<span
class="stock-info__no"
:data-selected="i == store.chosenStockListIndex"
>
<span v-if="i == store.chosenStockListIndex">&bull;&nbsp;</span>
{{ i + 1 }}.
</span>
<span class="stock-info__type" :class="{ supporter: stock.supportersOnly }">
<span
class="stock-info__type"
:class="{ supporter: stock.supportersOnly }"
>
{{ stock.isLoco ? stock.type : getCarSpecFromType(stock.type) }}
</span>
<span class="stock-info__cargo" v-if="stock.cargo"> {{ stock.cargo.id }} </span>
<span class="stock-info__cargo" v-if="stock.cargo">
{{ stock.cargo.id }}
</span>
<span class="stock-info__length"> {{ stock.length }}m </span>
<span class="stock-info__mass">{{ stock.cargo ? stock.cargo.totalMass : stock.mass }}t </span>
<span class="stock-info__mass"
>{{ stock.cargo ? stock.cargo.totalMass : stock.mass }}t
</span>
<span class="stock-info__speed"> {{ stock.maxSpeed }}km/h </span>
</div>
</li>
@@ -181,21 +233,20 @@
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import TrainImage from '../sections/TrainImageSection.vue';
import { defineComponent } from "vue";
import { useStore } from '../../store';
import { useStore } from "../../store";
import { locoSupportsColdStart } from '../../utils/locoUtils';
import warningsMixin from '../../mixins/warningsMixin';
import imageMixin from '../../mixins/imageMixin';
import stockPreviewMixin from '../../mixins/stockPreviewMixin';
import StockThumbnails from '../utils/StockThumbnails.vue';
import stockMixin from '../../mixins/stockMixin';
import { locoSupportsColdStart } from "../../utils/locoUtils";
import warningsMixin from "../../mixins/warningsMixin";
import imageMixin from "../../mixins/imageMixin";
import stockPreviewMixin from "../../mixins/stockPreviewMixin";
import StockThumbnails from "../utils/StockThumbnails.vue";
import stockMixin from "../../mixins/stockMixin";
export default defineComponent({
name: 'stock-list',
components: { TrainImage, StockThumbnails },
name: "stock-list",
components: { StockThumbnails },
mixins: [warningsMixin, imageMixin, stockMixin, stockPreviewMixin],
@@ -218,13 +269,20 @@ export default defineComponent({
stockString() {
return this.store.stockList
.map((stock, i) => {
let stockTypeStr = stock.isLoco || !stock.cargo ? stock.type : `${stock.type}:${stock.cargo.id}`;
let stockTypeStr =
stock.isLoco || !stock.cargo
? stock.type
: `${stock.type}:${stock.cargo.id}`;
let coldStart =
i == 0 && this.store.isColdStart && locoSupportsColdStart(stock.constructionType || '') ? ',c' : '';
i == 0 &&
this.store.isColdStart &&
locoSupportsColdStart(stock.constructionType || "")
? ",c"
: "";
return stockTypeStr + coldStart;
})
.join(';');
.join(";");
},
stockIsEmpty() {
@@ -232,11 +290,18 @@ export default defineComponent({
},
chosenStockVehicle() {
return this.store.chosenStockListIndex == -1 ? undefined : this.store.stockList[this.store.chosenStockListIndex];
return this.store.chosenStockListIndex == -1
? undefined
: this.store.stockList[this.store.chosenStockListIndex];
},
stockHasWarnings() {
return this.tooManyLocomotives || this.trainTooHeavy || this.trainTooLong || this.locoNotSuitable;
return (
this.tooManyLocomotives ||
this.trainTooHeavy ||
this.trainTooLong ||
this.locoNotSuitable
);
},
},
@@ -247,7 +312,7 @@ export default defineComponent({
navigator.clipboard.writeText(this.stockString);
setTimeout(() => {
alert(this.$t('stocklist.alert-copied'));
alert(this.$t("stocklist.alert-copied"));
}, 20);
},
@@ -255,7 +320,10 @@ export default defineComponent({
const stock = this.store.stockList[stockID];
this.store.chosenStockListIndex =
this.store.chosenStockListIndex == stockID && this.store.chosenVehicle?.type == stock.type ? -1 : stockID;
this.store.chosenStockListIndex == stockID &&
this.store.chosenVehicle?.type == stock.type
? -1
: stockID;
if (this.store.chosenStockListIndex == -1) {
this.store.chosenVehicle = null;
@@ -268,12 +336,13 @@ export default defineComponent({
},
getCarSpecFromType(typeStr: string) {
const specArray = typeStr.split('_');
const specArray = typeStr.split("_");
if (specArray.length == 0) return null;
/* 111a_Grafitti_1 */
if (specArray.length == 3) return `${specArray[0]} ${specArray[1]}-${specArray[2]}`;
if (specArray.length == 3)
return `${specArray[0]} ${specArray[1]}-${specArray[2]}`;
/* 111a_PKP_Bnouz_01 */
return `${specArray[0]} ${specArray[2]}-${specArray[3]} (${specArray[1]})`;
@@ -301,9 +370,12 @@ export default defineComponent({
removeStock(index: number) {
if (index == -1) return;
this.store.stockList = this.store.stockList.filter((stock, i) => i != index);
this.store.stockList = this.store.stockList.filter(
(stock, i) => i != index,
);
if (this.store.stockList.length < index + 1) this.store.chosenStockListIndex = -1;
if (this.store.stockList.length < index + 1)
this.store.chosenStockListIndex = -1;
},
moveUpStock(index: number) {
@@ -341,7 +413,8 @@ export default defineComponent({
availableIndexes.splice(i, -1);
const randAvailableIndex = availableIndexes[Math.floor(Math.random() * availableIndexes.length)];
const randAvailableIndex =
availableIndexes[Math.floor(Math.random() * availableIndexes.length)];
const tempSwap = this.store.stockList[randAvailableIndex];
this.store.stockList[randAvailableIndex] = this.store.stockList[i];
@@ -350,30 +423,33 @@ export default defineComponent({
},
downloadStock() {
if (this.store.stockList.length == 0) return alert(this.$t('stocklist.alert-empty'));
if (this.store.stockList.length == 0)
return alert(this.$t("stocklist.alert-empty"));
const defaultName = `${this.store.chosenRealStockName || this.store.stockList[0].type} ${
this.store.totalMass
}t; ${this.store.totalLength}m; vmax ${this.store.maxStockSpeed}`;
const defaultName = `${
this.store.chosenRealStockName || this.store.stockList[0].type
} ${this.store.totalMass}t; ${this.store.totalLength}m; vmax ${
this.store.maxStockSpeed
}`;
const fileName = prompt(this.$t('stocklist.prompt-file'), defaultName);
const fileName = prompt(this.$t("stocklist.prompt-file"), defaultName);
if (!fileName) return;
const blob = new Blob([this.stockString]);
const file = fileName + '.con';
const file = fileName + ".con";
var e = document.createEvent('MouseEvents'),
a = document.createElement('a');
var e = document.createEvent("MouseEvents"),
a = document.createElement("a");
a.download = file;
a.href = window.URL.createObjectURL(blob);
a.dataset.downloadurl = ['', a.download, a.href].join(':');
e.initEvent('click', true, false);
a.dataset.downloadurl = ["", a.download, a.href].join(":");
e.initEvent("click", true, false);
a.dispatchEvent(e);
},
uploadStock() {
const inputEl = this.$refs['conFile'] as HTMLInputElement;
const inputEl = this.$refs["conFile"] as HTMLInputElement;
const files = inputEl.files;
if (files?.length != 1) return;
@@ -385,14 +461,14 @@ export default defineComponent({
reader.onload = (res) => {
const stockString = res.target?.result;
if (!stockString || typeof stockString !== 'string') return;
if (!stockString || typeof stockString !== "string") return;
this.loadStockFromString(stockString);
};
reader.onerror = (err) => console.log(err);
inputEl.value = '';
inputEl.value = "";
},
onDragStart(vehicleIndex: number) {
@@ -402,13 +478,14 @@ export default defineComponent({
onDrop(e: DragEvent, vehicleIndex: number) {
e.preventDefault();
let targetEl = (this.$refs['itemRefs'] as Element[])[vehicleIndex];
let targetEl = (this.$refs["itemRefs"] as Element[])[vehicleIndex];
if (!targetEl) return;
const tempVehicle = this.store.stockList[vehicleIndex];
this.store.stockList[vehicleIndex] = this.store.stockList[this.draggedVehicleID];
this.store.stockList[vehicleIndex] =
this.store.stockList[this.draggedVehicleID];
this.store.stockList[this.draggedVehicleID] = tempVehicle;
this.store.chosenStockListIndex = vehicleIndex;
@@ -422,8 +499,8 @@ export default defineComponent({
</script>
<style lang="scss" scoped>
@import '../../styles/global';
@import '../../styles/tab.scss';
@import "../../styles/global";
@import "../../styles/tab.scss";
.stock-list-tab {
display: grid;
@@ -455,7 +532,7 @@ export default defineComponent({
background-color: #353a57;
&[data-disabled='true'] {
&[data-disabled="true"] {
opacity: 0.8;
user-select: none;
@@ -567,7 +644,7 @@ li > .stock-info {
min-width: 3.5em;
text-align: right;
&[data-selected='true'] {
&[data-selected="true"] {
color: $accentColor;
}
}
@@ -610,4 +687,3 @@ li > .stock-info {
}
}
</style>
+358 -356
View File
@@ -1,356 +1,358 @@
<template>
<section class="wiki-list tab">
<div class="tab_header">
<h2>{{ $t('wiki.title') }}</h2>
</div>
<div class="tab_content">
<div class="actions-panel">
<div class="actions-panel_vehicles">
<button class="btn btn--choice" @click="changeWikiMode('locomotives')">
{{ $t('wiki.action-vehicles') }}
</button>
<button class="btn btn--choice" @click="changeWikiMode('carWagons')">
{{ $t('wiki.action-carriages') }}
</button>
</div>
<div class="actions-panel_search">
<input type="text" :placeholder="$t('wiki.search')" v-model="searchedVehicleTypeName" />
</div>
</div>
<div class="table-wrapper" @scroll="scrollEvent" ref="table-wrapper">
<table>
<thead>
<tr>
<th v-for="header in wikiMode == 'locomotives' ? locoHeaders : carHeaders" @click="toggleSorter(header)">
{{ $t(`wiki.header.${header.id}`) }}
<span v-if="currentModeSorter.id == header.id">
{{ currentModeSorter.direction == 1 ? `&uArr;` : `&dArr;` }}
</span>
</th>
</tr>
</thead>
<tbody v-if="wikiMode == 'locomotives'">
<tr
v-for="loco in computedLocoList"
@click="previewLocomotive(loco)"
@keydown.enter="previewLocomotive(loco)"
@dblclick="addLocomotive(loco)"
tabindex="0"
>
<td>
<img
:src="`https://spythere.github.io/api/td2/images/${loco.type}--300px.jpg`"
loading="lazy"
:alt="`Lokomotywa ${loco.type}`"
/>
</td>
<td>{{ loco.type }}</td>
<td>{{ $t(`wiki.${loco.power}`) }}</td>
<td>{{ loco.constructionType }}</td>
<td>{{ locoSupportsColdStart(loco.constructionType) ? `&check;` : '&cross;' }}</td>
<td>{{ loco.length }}m</td>
<td>{{ loco.mass }}t</td>
<td>{{ loco.maxSpeed }}km/h</td>
</tr>
</tbody>
<tbody v-else>
<tr
v-for="car in computedCarList"
@keydow.enter="previewCarWagon(car)"
@click="previewCarWagon(car)"
@dblclick="addCarWagon(car)"
tabindex="0"
>
<td>
<img
:src="`https://spythere.github.io/api/td2/images/${car.type}--300px.jpg`"
loading="lazy"
:alt="`Lokomotywa ${car.type}`"
/>
</td>
<td>{{ car.type }}</td>
<td>{{ car.constructionType }}</td>
<td>{{ car.length }}m</td>
<td>{{ car.mass }}t</td>
<td>{{ car.maxSpeed }}km/h</td>
<td>{{ car.cargoList.length == 0 ? '-' : car.cargoList.length }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</section>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useStore } from '../../store';
import stockPreviewMixin from '../../mixins/stockPreviewMixin';
import { Vehicle } from '../../types';
import { isLocomotive } from '../../utils/vehicleUtils';
import stockMixin from '../../mixins/stockMixin';
import { locoSupportsColdStart } from '../../utils/locoUtils';
type WikiMode = 'locomotives' | 'carWagons';
type SorterID =
| 'type'
| 'constructionType'
| 'image'
| 'length'
| 'mass'
| 'maxSpeed'
| 'cargoCount'
| 'power'
| 'coldStart';
interface WikiHeader {
id: SorterID;
sortable: boolean;
}
const locoHeaders: WikiHeader[] = [
{ id: 'image', sortable: false },
{ id: 'type', sortable: true },
{ id: 'power', sortable: true },
{ id: 'constructionType', sortable: true },
{ id: 'coldStart', sortable: true },
{ id: 'length', sortable: true },
{ id: 'mass', sortable: true },
{ id: 'maxSpeed', sortable: true },
];
const carHeaders: WikiHeader[] = [
{ id: 'image', sortable: false },
{ id: 'type', sortable: true },
{ id: 'constructionType', sortable: true },
{ id: 'length', sortable: true },
{ id: 'mass', sortable: true },
{ id: 'maxSpeed', sortable: true },
{ id: 'cargoCount', sortable: true },
];
export default defineComponent({
mixins: [stockPreviewMixin, stockMixin],
data() {
return {
store: useStore(),
locoHeaders,
carHeaders,
locosScrollTop: 0,
carsScrollTop: 0,
wikiMode: 'locomotives' as WikiMode,
searchedVehicleTypeName: '',
currentLocoSorter: {
id: 'type' as SorterID,
direction: 1,
},
currentCarSorter: {
id: 'type' as SorterID,
direction: 1,
},
};
},
activated() {
const tableWrapperRef = this.$refs['table-wrapper'] as HTMLElement;
tableWrapperRef.scrollTo({ top: this.wikiMode == 'locomotives' ? this.locosScrollTop : this.carsScrollTop });
},
methods: {
locoSupportsColdStart,
scrollEvent(e: Event) {
const tableScrollTop = (e.target as HTMLElement).scrollTop;
if (this.wikiMode == 'locomotives') this.locosScrollTop = tableScrollTop;
else this.carsScrollTop = tableScrollTop;
},
changeWikiMode(wikiMode: WikiMode) {
this.searchedVehicleTypeName = '';
this.wikiMode = wikiMode;
},
toggleSorter(header: WikiHeader) {
if (!header.sortable) return;
if (header.id == this.currentModeSorter.id) this.currentModeSorter.direction *= -1;
this.currentModeSorter.id = header.id;
},
sortVehicles(vA: Vehicle, vB: Vehicle) {
const { id, direction } = this.currentModeSorter;
const vehiclesAreLocos = isLocomotive(vA) && isLocomotive(vB);
const vehiclesAreCars = !isLocomotive(vA) && !isLocomotive(vB);
switch (id) {
case 'type':
case 'constructionType':
return direction == 1 ? vA[id].localeCompare(vB[id]) : vB[id].localeCompare(vA[id]);
case 'mass':
case 'length':
case 'maxSpeed':
return Math.sign(vA[id] - vB[id]) * direction;
case 'cargoCount':
if (vehiclesAreCars) return Math.sign((vA.cargoList.length || -1) - (vB.cargoList.length || -1)) * direction;
case 'coldStart':
if (vehiclesAreLocos)
return (
(locoSupportsColdStart(vA.constructionType) > locoSupportsColdStart(vB.constructionType) ? 1 : -1) *
direction
);
default:
break;
}
return direction == 1 ? vA.type.localeCompare(vB.type) : vB.type.localeCompare(vA.type);
},
},
computed: {
currentModeSorter() {
return this.wikiMode == 'carWagons' ? this.currentCarSorter : this.currentLocoSorter;
},
computedLocoList() {
const trimmedSearchValue = this.searchedVehicleTypeName.trim();
return this.store.locoDataList
.filter((loco) => new RegExp(`${trimmedSearchValue}`, 'i').test(loco.type))
.sort(this.sortVehicles);
},
computedCarList() {
const trimmedSearchValue = this.searchedVehicleTypeName.trim();
return this.store.carDataList
.filter((car) => new RegExp(`${trimmedSearchValue}`, 'i').test(car.type))
.sort(this.sortVehicles);
},
},
});
</script>
<style lang="scss" scoped>
@import '../../styles/tab.scss';
.actions-panel {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 0.5em;
margin: 0.5em 0;
}
.actions-panel_vehicles {
display: flex;
gap: 0.5em;
}
.actions-panel_search {
input {
width: auto;
}
}
.table-wrapper {
overflow: auto;
height: 750px;
max-height: 95vh;
}
.wiki-list table {
border-collapse: collapse;
width: 100%;
thead {
position: sticky;
top: 0;
}
th {
background-color: #111;
padding: 0.5em;
user-select: none;
-moz-user-select: none;
-webkit-user-select: none;
}
tr {
cursor: pointer;
background-color: #333;
&:nth-child(odd) {
background-color: #444;
}
&:hover {
background-color: #666;
}
}
td {
text-align: center;
padding: 0.25em;
height: 85px;
}
td:first-child {
width: 120px;
}
td img {
display: block;
width: 120px;
}
}
@media screen and (max-width: $breakpointMd) {
.wiki-list table {
td {
width: 100px;
height: auto;
img {
width: 6em;
}
}
}
}
@media screen and (max-width: $breakpointSm) {
.actions-panel {
align-items: stretch;
flex-direction: column;
}
.actions-panel_vehicles {
display: grid;
grid-template-columns: 1fr 1fr;
}
.actions-panel_search {
display: grid;
}
}
</style>
<template>
<section class="wiki-list tab">
<div class="tab_header">
<h2>{{ $t('wiki.title') }}</h2>
</div>
<div class="tab_content">
<div class="actions-panel">
<div class="actions-panel_vehicles">
<button class="btn btn--choice" @click="changeWikiMode('locomotives')">
{{ $t('wiki.action-vehicles') }}
</button>
<button class="btn btn--choice" @click="changeWikiMode('carWagons')">
{{ $t('wiki.action-carriages') }}
</button>
</div>
<div class="actions-panel_search">
<input type="text" :placeholder="$t('wiki.search')" v-model="searchedVehicleTypeName" />
</div>
</div>
<div class="table-wrapper" @scroll="scrollEvent" ref="table-wrapper">
<table>
<thead>
<tr>
<th v-for="header in wikiMode == 'locomotives' ? locoHeaders : carHeaders" @click="toggleSorter(header)" :key="header.id">
{{ $t(`wiki.header.${header.id}`) }}
<span v-if="currentModeSorter.id == header.id">
{{ currentModeSorter.direction == 1 ? `&uArr;` : `&dArr;` }}
</span>
</th>
</tr>
</thead>
<tbody v-if="wikiMode == 'locomotives'">
<tr
v-for="loco in computedLocoList"
:key="loco.type"
@click="previewLocomotive(loco)"
@keydown.enter="previewLocomotive(loco)"
@dblclick="addLocomotive(loco)"
tabindex="0"
>
<td>
<object :data="getThumbnailURL(loco.type, 'small')" type="image/jpeg">
<!-- <img src="default.jpg" /> -->
<div>?</div>
</object>
</td>
<td>{{ loco.type }}</td>
<td>{{ $t(`wiki.${loco.power}`) }}</td>
<td>{{ loco.constructionType }}</td>
<td>
{{ locoSupportsColdStart(loco.constructionType) ? `&check;` : '&cross;' }}
</td>
<td>{{ loco.length }}m</td>
<td>{{ loco.mass }}t</td>
<td>{{ loco.maxSpeed }}km/h</td>
</tr>
</tbody>
<tbody v-else>
<tr
v-for="car in computedCarList"
:key="car.type"
@keydow.enter="previewCarWagon(car)"
@click="previewCarWagon(car)"
@dblclick="addCarWagon(car)"
tabindex="0"
>
<td>
<!-- <img
:src="getThumbnailURL(car.type, 'small')"
loading="lazy"
:alt="`${car.type}`"
/> -->
<object :data="getThumbnailURL(car.type, 'small')" type="image/jpeg" loading="lazy">
<!-- <img src="default.jpg" /> -->
<div>?</div>
</object>
</td>
<td>{{ car.type }}</td>
<td>{{ car.constructionType }}</td>
<td>{{ car.length }}m</td>
<td>{{ car.mass }}t</td>
<td>{{ car.maxSpeed }}km/h</td>
<td>
{{ car.cargoList.length == 0 ? '-' : car.cargoList.length }}
</td>
</tr>
</tbody>
</table>
</div>
</div>
</section>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useStore } from '../../store';
import stockPreviewMixin from '../../mixins/stockPreviewMixin';
import { Vehicle } from '../../types';
import { isLocomotive } from '../../utils/vehicleUtils';
import stockMixin from '../../mixins/stockMixin';
import imageMixin from '../../mixins/imageMixin';
import { locoSupportsColdStart } from '../../utils/locoUtils';
type WikiMode = 'locomotives' | 'carWagons';
type SorterID = 'type' | 'constructionType' | 'image' | 'length' | 'mass' | 'maxSpeed' | 'cargoCount' | 'power' | 'coldStart';
interface WikiHeader {
id: SorterID;
sortable: boolean;
}
const locoHeaders: WikiHeader[] = [
{ id: 'image', sortable: false },
{ id: 'type', sortable: true },
{ id: 'power', sortable: true },
{ id: 'constructionType', sortable: true },
{ id: 'coldStart', sortable: true },
{ id: 'length', sortable: true },
{ id: 'mass', sortable: true },
{ id: 'maxSpeed', sortable: true },
];
const carHeaders: WikiHeader[] = [
{ id: 'image', sortable: false },
{ id: 'type', sortable: true },
{ id: 'constructionType', sortable: true },
{ id: 'length', sortable: true },
{ id: 'mass', sortable: true },
{ id: 'maxSpeed', sortable: true },
{ id: 'cargoCount', sortable: true },
];
export default defineComponent({
mixins: [stockPreviewMixin, stockMixin, imageMixin],
data() {
return {
store: useStore(),
locoHeaders,
carHeaders,
locosScrollTop: 0,
carsScrollTop: 0,
wikiMode: 'locomotives' as WikiMode,
searchedVehicleTypeName: '',
currentLocoSorter: {
id: 'type' as SorterID,
direction: 1,
},
currentCarSorter: {
id: 'type' as SorterID,
direction: 1,
},
};
},
activated() {
const tableWrapperRef = this.$refs['table-wrapper'] as HTMLElement;
tableWrapperRef.scrollTo({
top: this.wikiMode == 'locomotives' ? this.locosScrollTop : this.carsScrollTop,
});
},
methods: {
locoSupportsColdStart,
scrollEvent(e: Event) {
const tableScrollTop = (e.target as HTMLElement).scrollTop;
if (this.wikiMode == 'locomotives') this.locosScrollTop = tableScrollTop;
else this.carsScrollTop = tableScrollTop;
},
changeWikiMode(wikiMode: WikiMode) {
this.searchedVehicleTypeName = '';
this.wikiMode = wikiMode;
},
toggleSorter(header: WikiHeader) {
if (!header.sortable) return;
if (header.id == this.currentModeSorter.id) this.currentModeSorter.direction *= -1;
this.currentModeSorter.id = header.id;
},
sortVehicles(vA: Vehicle, vB: Vehicle) {
const { id, direction } = this.currentModeSorter;
const vehiclesAreLocos = isLocomotive(vA) && isLocomotive(vB);
const vehiclesAreCars = !isLocomotive(vA) && !isLocomotive(vB);
switch (id) {
case 'type':
case 'constructionType':
return direction == 1 ? vA[id].localeCompare(vB[id]) : vB[id].localeCompare(vA[id]);
case 'mass':
case 'length':
case 'maxSpeed':
return Math.sign(vA[id] - vB[id]) * direction;
case 'cargoCount':
if (vehiclesAreCars) return Math.sign((vA.cargoList.length || -1) - (vB.cargoList.length || -1)) * direction;
break;
case 'coldStart':
if (vehiclesAreLocos) return (locoSupportsColdStart(vA.constructionType) > locoSupportsColdStart(vB.constructionType) ? 1 : -1) * direction;
break;
default:
break;
}
return direction == 1 ? vA.type.localeCompare(vB.type) : vB.type.localeCompare(vA.type);
},
},
computed: {
currentModeSorter() {
return this.wikiMode == 'carWagons' ? this.currentCarSorter : this.currentLocoSorter;
},
computedLocoList() {
const trimmedSearchValue = this.searchedVehicleTypeName.trim();
return this.store.locoDataList.filter((loco) => new RegExp(`${trimmedSearchValue}`, 'i').test(loco.type)).sort(this.sortVehicles);
},
computedCarList() {
const trimmedSearchValue = this.searchedVehicleTypeName.trim();
return this.store.carDataList.filter((car) => new RegExp(`${trimmedSearchValue}`, 'i').test(car.type)).sort(this.sortVehicles);
},
},
});
</script>
<style lang="scss" scoped>
@import '../../styles/tab.scss';
.actions-panel {
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 0.5em;
margin: 0.5em 0;
}
.actions-panel_vehicles {
display: flex;
gap: 0.5em;
}
.actions-panel_search {
input {
width: auto;
}
}
.table-wrapper {
overflow: auto;
height: 750px;
max-height: 95vh;
}
.wiki-list table {
border-collapse: collapse;
width: 100%;
thead {
position: sticky;
top: 0;
}
th {
background-color: #111;
padding: 0.5em;
user-select: none;
-moz-user-select: none;
-webkit-user-select: none;
}
tr {
cursor: pointer;
background-color: #333;
&:nth-child(odd) {
background-color: #444;
}
&:hover {
background-color: #666;
}
}
td {
text-align: center;
padding: 0.25em;
height: 85px;
}
td:first-child {
width: 120px;
}
td object[type='image/jpeg'] {
display: flex;
max-width: 120px;
min-height: 60px;
div {
margin: auto;
}
}
}
@media screen and (max-width: $breakpointMd) {
.wiki-list table {
td {
width: 100px;
height: auto;
img {
width: 6em;
}
}
}
}
@media screen and (max-width: $breakpointSm) {
.actions-panel {
align-items: stretch;
flex-direction: column;
}
.actions-panel_vehicles {
display: grid;
grid-template-columns: 1fr 1fr;
}
.actions-panel_search {
display: grid;
}
}
</style>
+52 -52
View File
@@ -1,52 +1,52 @@
<template>
<div
class="image-preview"
@click="store.vehiclePreviewSrc = ''"
@keydown.esc="store.vehiclePreviewSrc = ''"
tabindex="0"
>
<img :src="store.vehiclePreviewSrc" alt="preview" />
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useStore } from '../../store';
export default defineComponent({
data() {
return {
store: useStore(),
};
},
mounted() {
this.$el.focus();
},
});
</script>
<style lang="scss" scoped>
.image-preview {
position: fixed;
top: 0;
left: 0;
z-index: 99;
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
background: rgba(black, 0.85);
cursor: zoom-out;
img {
max-width: 100%;
height: auto;
max-height: 100%;
}
}
</style>
<template>
<div
class="image-preview"
@click="store.vehiclePreviewSrc = ''"
@keydown.esc="store.vehiclePreviewSrc = ''"
tabindex="0"
>
<img :src="store.vehiclePreviewSrc" alt="preview" />
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import { useStore } from "../../store";
export default defineComponent({
data() {
return {
store: useStore(),
};
},
mounted() {
this.$el.focus();
},
});
</script>
<style lang="scss" scoped>
.image-preview {
position: fixed;
top: 0;
left: 0;
z-index: 99;
display: flex;
justify-content: center;
align-items: center;
width: 100%;
height: 100%;
background: rgba(black, 0.85);
cursor: zoom-out;
img {
max-width: 100%;
height: auto;
max-height: 100%;
}
}
</style>
+16 -10
View File
@@ -2,6 +2,7 @@
<div class="stock_thumbnails" ref="thumbnailsRef">
<div
v-for="(stock, stockIndex) in store.stockList"
:key="stockIndex"
:data-selected="store.chosenStockListIndex == stockIndex"
draggable="true"
@dragstart="onDragStart(stockIndex)"
@@ -28,18 +29,18 @@
</template>
<script setup lang="ts">
import { Ref, computed, nextTick, ref, watch } from 'vue';
import { useStore } from '../../store';
import { IStock } from '../../types';
import { Ref, computed, nextTick, ref, watch } from "vue";
import { useStore } from "../../store";
import { IStock } from "../../types";
const store = useStore();
const emit = defineEmits(['listItemClick']);
const emit = defineEmits(["listItemClick"]);
const thumbnailsRef = ref() as Ref<HTMLElement>;
const draggedIndex = ref(-1);
const onListItemClick = (index: number) => {
emit('listItemClick', index);
emit("listItemClick", index);
};
const stockImageError = (e: Event, stock: IStock) => {
@@ -54,9 +55,13 @@ watch(
nextTick(() => {
(thumbnailsRef.value as HTMLElement)
.querySelector(`div:nth-child(${index + 1})`)
?.scrollIntoView({ block: 'nearest', inline: 'start', behavior: 'smooth' });
?.scrollIntoView({
block: "nearest",
inline: "start",
behavior: "smooth",
});
});
}
},
);
// Dragging images
@@ -67,7 +72,9 @@ const onDragStart = (vehicleIndex: number) => {
const onDrop = (e: DragEvent, vehicleIndex: number) => {
e.preventDefault();
let targetEl = thumbnailsRef.value.querySelector(`div:nth-child(${vehicleIndex + 1})`);
let targetEl = thumbnailsRef.value.querySelector(
`div:nth-child(${vehicleIndex + 1})`,
);
if (!targetEl && draggedIndex.value != -1) return;
@@ -95,7 +102,7 @@ const allowDrop = (e: DragEvent) => {
cursor: pointer;
min-height: 100px;
&[data-selected='true'] {
&[data-selected="true"] {
background-color: rebeccapurple;
}
@@ -122,4 +129,3 @@ const allowDrop = (e: DragEvent) => {
color: salmon;
}
</style>
+2 -3
View File
@@ -12,8 +12,8 @@
},
"sameRegions": {
"Losowy": [
10, 11, 19, 91, 93, 97, 99, 20, 22, 29, 30, 33, 39, 40, 44, 49, 94, 50, 55, 59, 90, 95, 96, 66, 60, 69, 77, 70,
79, 88, 80, 89, 92, 98
10, 11, 19, 91, 93, 97, 99, 20, 22, 29, 30, 33, 39, 40, 44, 49, 94, 50,
55, 59, 90, 95, 96, 66, 60, 69, 77, 70, 79, 88, 80, 89, 92, 98
],
"Warszawa": [10, 11, 19, 91, 93, 97, 99],
"Lublin": [20, 22, 29],
@@ -35,4 +35,3 @@
"LT": "2:5;3:0-899:3"
}
}
+6 -2
View File
@@ -27,6 +27,12 @@
},
"cargo": null
},
"EP09": {
"passenger": {
"650": 160
},
"cargo": null
},
"ET41": {
"passenger": {
"700": 125
@@ -58,5 +64,3 @@
}
}
}
+7 -7
View File
@@ -1,9 +1,9 @@
export const enum EVehicleUseType {
LOCO_ELECTRICAL = 'loco-e',
LOCO_DIESEL = "loco-s",
EMU = "loco-ezt",
DMU = "loco-szt",
LOCO_ELECTRICAL = "loco-e",
LOCO_DIESEL = "loco-s",
EMU = "loco-ezt",
DMU = "loco-szt",
CAR_PASSENGER = "car-passenger",
CAR_CARGO = "car-cargo"
}
CAR_PASSENGER = "car-passenger",
CAR_CARGO = "car-cargo",
}
+10
View File
@@ -0,0 +1,10 @@
import axios from "axios";
const http = axios.create({
baseURL:
import.meta.env.VITE_API_DEV === "1"
? "http://localhost:5500"
: "https://spythere.github.io/api",
});
export default http;
+35 -33
View File
@@ -1,33 +1,35 @@
import localePL from './locales/pl.json';
import localeEN from './locales/en.json';
import { createI18n } from 'vue-i18n';
import axios from 'axios';
type LocaleMessageSchema = typeof localePL;
type LocaleKey = 'en' | 'pl';
const locales: { [key in LocaleKey]: LocaleMessageSchema } = {
en: localeEN,
pl: localePL,
};
const locale = window.localStorage.getItem('locale') || (/^pl\b/.test(navigator.language) ? 'pl' : 'en');
const i18n = createI18n<[LocaleMessageSchema], 'en' | 'pl'>({
locale,
fallbackLocale: 'pl',
legacy: false,
globalInjection: true,
messages: locales,
});
async function fetchBackendTranslations() {
const localeData = (await axios.get(`https://spythere.github.io/api/td2/data/locales.json`)).data;
i18n.global.mergeLocaleMessage('pl', localeData.pl);
i18n.global.mergeLocaleMessage('en', localeData.en);
}
fetchBackendTranslations();
export default i18n;
import localePL from "./locales/pl.json";
import localeEN from "./locales/en.json";
import { createI18n } from "vue-i18n";
import http from "./http";
type LocaleMessageSchema = typeof localePL;
type LocaleKey = "en" | "pl";
const locales: { [key in LocaleKey]: LocaleMessageSchema } = {
en: localeEN,
pl: localePL,
};
const locale =
window.localStorage.getItem("locale") ||
(/^pl\b/.test(navigator.language) ? "pl" : "en");
const i18n = createI18n<[LocaleMessageSchema], "en" | "pl">({
locale,
fallbackLocale: "pl",
legacy: false,
globalInjection: true,
messages: locales,
});
async function fetchBackendTranslations() {
const localeData = (await http.get(`td2/data/locales.json`)).data;
i18n.global.mergeLocaleMessage("pl", localeData.pl);
i18n.global.mergeLocaleMessage("en", localeData.en);
}
fetchBackendTranslations();
export default i18n;
+150 -150
View File
@@ -1,150 +1,150 @@
{
"app": {
"title": "ROLLING STOCK EDITOR"
},
"footer": {
"disclaimer": "This site has only an informational intent. The author does not carry any responsibility for creating trains against {tos}!",
"tos": "Train Driver 2 simulator rules",
"tos-href": "https://docs.google.com/document/d/1UAAPUtN0d_RoS4RgOzEzllJZJhA0VcizzCzKW4QylbY/edit#heading=h.1ldcvhomwjp9",
"version-check": "Site is complete for version {version} of Train Driver 2 simulator"
},
"inputs": {
"title": "CHOOSE A VEHICLE",
"input-vehicle": "Choose a traction unit",
"input-carwagon": "Choose a carriage",
"cargo-title": "Cargo (only selected freight cars)",
"no-cargo-available": "no cargo available",
"cargo-empty": "empty",
"loco-e": "ELECTR.",
"loco-s": "DIESEL",
"loco-ezt": "EMU",
"loco-szt": "DMU",
"car-passenger": "PASSENGER",
"car-cargo": "FREIGHT",
"action-add": "ADD NEW",
"action-swap": "SWAP WITH",
"real-stock": "POLISH TRAIN COMPOSITIONS"
},
"preview": {
"title": "RAILWAY VEHICLE PREVIEW",
"loading": "IMAGE LOADING...",
"desc": "Choose a railway vehicle above to see its preview",
"sponsor-only": "* SPONSORS ONLY",
"loco-e": "ELECTRIC LOCO",
"loco-s": "DIESEL LOCO",
"loco-ezt": "ELECTRIC M.U.",
"loco-szt": "DIESEL M.U.",
"car-passenger": "PASSENGER CARRIAGE",
"car-cargo": "FREIGHT CAR",
"cabin": "Cabin type:",
"construction": "Construction type:"
},
"topbar": {
"stock-list": "STOCK",
"wiki-list": "VEHICLES",
"number-generator": "NUMBER GEN.",
"stock-generator": "STOCK GEN."
},
"stocklist": {
"title": "STOCK EDITOR",
"alert-copied": "The rolling stock has been copied to your clipboard!",
"alert-empty": "Lista pojazdów jest pusta!",
"prompt-file": "Name a file and download it to the Presets folder (Documents/TTSK/TrainDriver2):",
"vehicle-no": "VEHICLE NO.",
"no-vehicle-chosen": "NO VEHICLE CHOSEN",
"action-move-up": "MOVE UP",
"action-move-down": "MOVE DOWN",
"action-remove": "REMOVE",
"action-upload": "LOAD",
"action-download": "DOWNLOAD",
"action-copy": "COPY",
"action-reset": "RESET",
"action-shuffle": "SHUFFLE",
"mass": "Mass",
"mass-accepted": "accepted",
"length": "Length",
"vmax": "vMax",
"coldstart-info": "Cold start heading locomotive (only locos 303E & 203E type)",
"list-empty": "Stock list is empty!",
"warning-not-suitable": "EP07 & EP08 type locomotives are designed for passenger traffic only!",
"warning-passenger-too-long": "Maximum length of a passenger train may not be greater than 350m!",
"warning-freight-too-long": "Maximum length of a freight train may not be greater than 650m!",
"warning-too-many-locos": "This train has too many traction units!",
"warning-too-heavy": "This train is too heavy! Check {href}",
"acceptable-mass-docs": "acceptable rolling stock masses (PL)"
},
"stockgen": {
"title": "FREIGHT TRAIN GENERATOR",
"properties-title": "ROLLING STOCK PROPERTIES",
"properties-desc": "⇐ Add a locomotive in the first place of the stock list to include it in a drawing!",
"input-mass": "Max. mass (t)",
"input-length": "Max. length (m)",
"input-carcount": "Max. car count",
"cargo-title": "CARGO",
"cargo-desc": "Choose cargo you want to fill available cars with:",
"chosen-title": "CARS WITH CHOSEN CARGO",
"chosen-empty-warning": "Choose at least one cargo type to see available cars!",
"chosen-warning": "Cars containing chosen cargo are shown below. Hover over a type to see a preview of the car. Click it to include/exclude it from a drawing (only highlighted types will be included).",
"action-generate": "GENERATE",
"action-generate-empty": "GENERATE EMPTY",
"action-reset": "RESET CARGO"
},
"numgen": {
"title": "TRAIN NUMBER GENERATOR",
"alert": "The number has been copied to your clipboard!",
"start-region": "Beginning construction region",
"end-region": "Terminating construction region",
"train-category": "Train category",
"number-info": "Generated train number:",
"warning": "Choose category and (optionally) construction regions",
"td2-wiki": "> Polish rules of train numbering (TD2 wiki)",
"td2-wiki-link": "https://wiki.td2.info.pl/index.php?title=Zasady_numeracji_poci%C4%85g%C3%B3w/en",
"action-random-region": "DRAW REGIONS",
"action-random-number": "DRAW A NUMBER",
"categories": {
"EI": "domestic express (EI)",
"MP/RP": "(inter)voivodeship bullet (MP/RP)",
"RO": "regional passenger (RO)",
"PW": "empty passenger (PW)",
"TM": "mass transport freight (TM)",
"TK": "non-mass transport freight (TK)",
"LT": "locomotive alone (LT)"
},
"rules": {
"EI": "4 digits - ends within the range of 00-99",
"MP/RP": "5 digits - ends within the range of 050-169",
"RO": "5 digits - ends within the range of 200-999",
"PW": "6 digits - '6' on the 3rd place; ends within the range of 000-899",
"TM": "6 digits - '4' on the 3rd place; ends within the range of 000-899",
"TK": "6 digits - '3' on the 3rd place; ends within the range of 000-899",
"LT": "6 digits - '5' on the 3rd place; ends within the range of 000-899"
}
},
"wiki": {
"title": "LIST OF AVAILABLE VEHICLES",
"action-vehicles": "TRACTION UNITS",
"action-carriages": "CARRIAGES",
"search": "Search for a vehicle...",
"header": {
"image": "Image",
"type": "Name",
"power": "Type",
"constructionType": "Construction",
"coldStart": "Cold start",
"length": "Length",
"mass": "Mass",
"maxSpeed": "Speed",
"cargoCount": "Cargo count"
},
"loco-ezt": "EMU",
"loco-szt": "DMU",
"loco-s": "Diesel locomotive",
"loco-e": "Electric locomotive"
},
"realstock": {
"title": "POLISH TRAIN COMPOSITIONS by",
"search-name": "Search by name",
"search-stock": "Search by vehicles",
"action-reset": "RESET"
}
}
{
"app": {
"title": "ROLLING STOCK EDITOR"
},
"footer": {
"disclaimer": "This site has only an informational intent. The author does not carry any responsibility for creating trains against {tos}!",
"tos": "Train Driver 2 simulator rules",
"tos-href": "https://docs.google.com/document/d/1UAAPUtN0d_RoS4RgOzEzllJZJhA0VcizzCzKW4QylbY/edit#heading=h.1ldcvhomwjp9",
"version-check": "Site is complete for version {version} of Train Driver 2 simulator"
},
"inputs": {
"title": "CHOOSE A VEHICLE",
"input-vehicle": "Choose a traction unit",
"input-carwagon": "Choose a carriage",
"cargo-title": "Cargo (only selected freight cars)",
"no-cargo-available": "no cargo available",
"cargo-empty": "empty",
"loco-e": "ELECTR.",
"loco-s": "DIESEL",
"loco-ezt": "EMU",
"loco-szt": "DMU",
"car-passenger": "PASSENGER",
"car-cargo": "FREIGHT",
"action-add": "ADD NEW",
"action-swap": "SWAP WITH",
"real-stock": "POLISH TRAIN COMPOSITIONS"
},
"preview": {
"title": "RAILWAY VEHICLE PREVIEW",
"loading": "IMAGE LOADING...",
"desc": "Choose a railway vehicle above to see its preview",
"sponsor-only": "* SPONSORS ONLY",
"loco-e": "ELECTRIC LOCO",
"loco-s": "DIESEL LOCO",
"loco-ezt": "ELECTRIC M.U.",
"loco-szt": "DIESEL M.U.",
"car-passenger": "PASSENGER CARRIAGE",
"car-cargo": "FREIGHT CAR",
"cabin": "Cabin type:",
"construction": "Construction type:"
},
"topbar": {
"stock-list": "STOCK",
"wiki-list": "VEHICLES",
"number-generator": "NUMBER GEN.",
"stock-generator": "STOCK GEN."
},
"stocklist": {
"title": "STOCK EDITOR",
"alert-copied": "The rolling stock has been copied to your clipboard!",
"alert-empty": "Lista pojazdów jest pusta!",
"prompt-file": "Name a file and download it to the Presets folder (Documents/TTSK/TrainDriver2):",
"vehicle-no": "VEHICLE NO.",
"no-vehicle-chosen": "NO VEHICLE CHOSEN",
"action-move-up": "MOVE UP",
"action-move-down": "MOVE DOWN",
"action-remove": "REMOVE",
"action-upload": "LOAD",
"action-download": "DOWNLOAD",
"action-copy": "COPY",
"action-reset": "RESET",
"action-shuffle": "SHUFFLE",
"mass": "Mass",
"mass-accepted": "accepted",
"length": "Length",
"vmax": "vMax",
"coldstart-info": "Cold start heading locomotive (only locos 303E & 203E type)",
"list-empty": "Stock list is empty!",
"warning-not-suitable": "EP07 & EP08 type locomotives are designed for passenger traffic only!",
"warning-passenger-too-long": "Maximum length of a passenger train may not be greater than 350m!",
"warning-freight-too-long": "Maximum length of a freight train may not be greater than 650m!",
"warning-too-many-locos": "This train has too many traction units!",
"warning-too-heavy": "This train is too heavy! Check {href}",
"acceptable-mass-docs": "acceptable rolling stock masses (PL)"
},
"stockgen": {
"title": "FREIGHT TRAIN GENERATOR",
"properties-title": "ROLLING STOCK PROPERTIES",
"properties-desc": "⇐ Add a locomotive in the first place of the stock list to include it in a drawing!",
"input-mass": "Max. mass (t)",
"input-length": "Max. length (m)",
"input-carcount": "Max. car count",
"cargo-title": "CARGO",
"cargo-desc": "Choose cargo you want to fill available cars with:",
"chosen-title": "CARS WITH CHOSEN CARGO",
"chosen-empty-warning": "Choose at least one cargo type to see available cars!",
"chosen-warning": "Cars containing chosen cargo are shown below. Hover over a type to see a preview of the car. Click it to include/exclude it from a drawing (only highlighted types will be included).",
"action-generate": "GENERATE",
"action-generate-empty": "GENERATE EMPTY",
"action-reset": "RESET CARGO"
},
"numgen": {
"title": "TRAIN NUMBER GENERATOR",
"alert": "The number has been copied to your clipboard!",
"start-region": "Beginning construction region",
"end-region": "Terminating construction region",
"train-category": "Train category",
"number-info": "Generated train number:",
"warning": "Choose category and (optionally) construction regions",
"td2-wiki": "> Polish rules of train numbering (TD2 wiki)",
"td2-wiki-link": "https://wiki.td2.info.pl/index.php?title=Zasady_numeracji_poci%C4%85g%C3%B3w/en",
"action-random-region": "DRAW REGIONS",
"action-random-number": "DRAW A NUMBER",
"categories": {
"EI": "domestic express (EI)",
"MP/RP": "(inter)voivodeship bullet (MP/RP)",
"RO": "regional passenger (RO)",
"PW": "empty passenger (PW)",
"TM": "mass transport freight (TM)",
"TK": "non-mass transport freight (TK)",
"LT": "locomotive alone (LT)"
},
"rules": {
"EI": "4 digits - ends within the range of 00-99",
"MP/RP": "5 digits - ends within the range of 050-169",
"RO": "5 digits - ends within the range of 200-999",
"PW": "6 digits - '6' on the 3rd place; ends within the range of 000-899",
"TM": "6 digits - '4' on the 3rd place; ends within the range of 000-899",
"TK": "6 digits - '3' on the 3rd place; ends within the range of 000-899",
"LT": "6 digits - '5' on the 3rd place; ends within the range of 000-899"
}
},
"wiki": {
"title": "LIST OF AVAILABLE VEHICLES",
"action-vehicles": "TRACTION UNITS",
"action-carriages": "CARRIAGES",
"search": "Search for a vehicle...",
"header": {
"image": "Image",
"type": "Name",
"power": "Type",
"constructionType": "Construction",
"coldStart": "Cold start",
"length": "Length",
"mass": "Mass",
"maxSpeed": "Speed",
"cargoCount": "Cargo count"
},
"loco-ezt": "EMU",
"loco-szt": "DMU",
"loco-s": "Diesel locomotive",
"loco-e": "Electric locomotive"
},
"realstock": {
"title": "POLISH TRAIN COMPOSITIONS by",
"search-name": "Search by name",
"search-stock": "Search by vehicles",
"action-reset": "RESET"
}
}
+150 -150
View File
@@ -1,150 +1,150 @@
{
"app": {
"title": "EDYTOR SKŁADÓW ONLINE"
},
"footer": {
"disclaimer": "Ta strona ma charakter informacyjny. Autor nie ponosi odpowiedzialności za tworzenie pociągów niezgodnych z {tos}!",
"tos": "regulaminem symulatora Train Driver 2",
"tos-href": "https://docs.google.com/document/d/1UAAPUtN0d_RoS4RgOzEzllJZJhA0VcizzCzKW4QylbY/edit",
"version-check": "Strona jest kompletna dla wersji {version} symulatora TD2"
},
"inputs": {
"title": "WYBIERZ POJAZD SZYNOWY",
"input-vehicle": "Wybierz pojazd trakcyjny",
"input-carwagon": "Wybierz wagon",
"cargo-title": "Ładunek (tylko wybrane towarowe)",
"no-cargo-available": "brak dostępnych ładunków",
"cargo-empty": "próżny",
"loco-e": "ELEKTR.",
"loco-s": "SPAL.",
"loco-ezt": "EZT",
"loco-szt": "SZT",
"car-passenger": "PASAŻERSKIE",
"car-cargo": "TOWAROWE",
"action-add": "DODAJ NOWY",
"action-swap": "ZAMIEŃ ZA",
"real-stock": "REALNE ZESTAWIENIA"
},
"preview": {
"title": "PODGLĄD WYBRANEGO POJAZDU",
"loading": "ŁADOWANIE OBRAZU...",
"desc": "Wybierz pojazd lub wagon, aby zobaczyć jego podgląd powyżej",
"sponsor-only": "* TYLKO DLA SPONSORÓW",
"loco-e": "ELEKTROWÓZ",
"loco-s": "SPALINOWÓZ",
"loco-ezt": "EZT",
"loco-szt": "SZT",
"car-passenger": "WAGON PASAŻERSKI",
"car-cargo": "WAGON TOWAROWY",
"cabin": "Typ kabiny:",
"construction": "Typ konstrukcji:"
},
"topbar": {
"stock-list": "SKŁAD",
"wiki-list": "POJAZDY",
"number-generator": "GNR NUMERU",
"stock-generator": "GNR SKŁADU"
},
"stocklist": {
"title": "EDYTOR SKŁADU",
"alert-copied": "Skład został skopiowany do twojego schowka!",
"alert-empty": "Lista pojazdów jest pusta!",
"prompt-file": "Nazwij plik, a następnie pobierz do folderu Presets (Dokumenty/TTSK/TrainDriver2):",
"vehicle-no": "POJAZD NR",
"no-vehicle-chosen": "NIE WYBRANO POJAZDU",
"action-move-up": "PRZENIEŚ WYŻEJ",
"action-move-down": "PRZENIEŚ NIŻEJ",
"action-remove": "USUŃ",
"action-upload": "WCZYTAJ",
"action-download": "POBIERZ",
"action-copy": "SKOPIUJ",
"action-reset": "ZRESETUJ",
"action-shuffle": "PRZETASUJ",
"mass": "Masa",
"mass-accepted": "dopuszczalna",
"length": "Długość",
"vmax": "vMax",
"coldstart-info": "Zimny start lokomotywy czołowej (tylko elektrowozy typów 303E i 203E)",
"list-empty": "Lista pojazdów jest pusta!",
"warning-not-suitable": "Lokomotywy EP07 i EP08 są przeznaczone jedynie do ruchu pasażerskiego!",
"warning-passenger-too-long": "Maksymalna długość składów pasażerskich nie może przekraczać 350m!",
"warning-freight-too-long": "Maksymalna długość składów innych niż pasażerskie nie może przekraczać 650m!",
"warning-too-many-locos": "Ten skład posiada za dużo pojazdów trakcyjnych!",
"warning-too-heavy": "Ten skład jest za ciężki! Sprawdź {href}",
"acceptable-mass-docs": "dopuszczalne masy składów"
},
"stockgen": {
"title": "GENERATOR SKŁADU TOWAROWEGO",
"properties-title": "WŁAŚCIWOŚCI SKŁADU",
"properties-desc": "⇐ Dodaj lokomotywę na pierwsze miejsce listy, aby uwzględnić ją przy losowaniu składu!",
"input-mass": "Maksymalna masa (t)",
"input-length": "Maks. długość (m)",
"input-carcount": "Maks. liczba wagonów",
"cargo-title": "ŁADUNEK",
"cargo-desc": "Wybierz ładunki, którymi chcesz wypełnić dostępne wagony:",
"chosen-title": "WAGONY Z WYBRANYMI ŁADUNKAMI",
"chosen-empty-warning": "Wybierz co najmniej jeden ładunek, aby zobaczyć wagony, które go posiadają!",
"chosen-warning": "Wagony posiadające wybrane ładunki. Najedź na nazwę, aby zobaczyć podgląd wagonu. Kliknij, aby wyłączyć z losowania (tylko podświetlone nazwy będą uwzględnione).",
"action-generate": "WYGENERUJ",
"action-generate-empty": "WYGENERUJ PRÓŻNE WAGONY",
"action-reset": "ZRESETUJ ŁADUNKI"
},
"numgen": {
"title": "GENERATOR NUMERU POCIĄGU",
"alert": "Numer został skopiowany do twojego schowka!",
"start-region": "Początkowy obszar konstrukcyjny",
"end-region": "Końcowy obszar konstrukcyjny",
"train-category": "Kategoria pociągu",
"number-info": "Wygenerowany numer pociągu:",
"warning": "Wybierz kategorię oraz (opcjonalnie) obszary konstrukcyjne",
"td2-wiki": "> Szczegółowe zasady numeracji (wikipedia TD2)",
"td2-wiki-link": "https://wiki.td2.info.pl/index.php?title=Zasady_numeracji_poci%C4%85g%C3%B3w",
"action-random-region": "LOSUJ OBSZARY",
"action-random-number": "LOSUJ NUMER",
"categories": {
"EI": "ekspres krajowy (EI)",
"MP/RP": "(między)wojewódzki pośpieszny (MP/RP)",
"RO": "wojewódzki osobowy (RO)",
"PW": "próżny \"służbowy\" (PW)",
"TM": "towarowy do przewozów masowych (TM)",
"TK": "towarowy do obsługi stacji (TK)",
"LT": "lokomotywa luzem (LT)"
},
"rules": {
"EI": "4 cyfry - końcówka z przedziału 00-99",
"MP/RP": "5 cyfr - końcówka z przedziału 050-169",
"RO": "5 cyfr - końcówka z przedziału 200-999",
"PW": "6 cyfr - '6' na 3. miejscu; końcówka z przedziału 000-899",
"TM": "6 cyfr - '4' na 3. miejscu; końcówka z przedziału 000-899",
"TK": "6 cyfr - '3' na 3. miejscu; końcówka z przedziału 000-899",
"LT": "6 cyfr - '5' na 3. miejscu; końcówka z przedziału 000-899"
}
},
"wiki": {
"title": "LISTA DOSTĘPNYCH POJAZDÓW",
"action-vehicles": "POJ. TRAKCYJNE",
"action-carriages": "WAGONY",
"search": "Wyszukaj pojazd...",
"header": {
"image": "Zdjęcie",
"type": "Nazwa",
"power": "Rodzaj",
"constructionType": "Konstrukcja",
"coldStart": "Zimny start",
"length": "Długość",
"mass": "Masa",
"maxSpeed": "Prędkość",
"cargoCount": "Ładunki"
},
"loco-ezt": "EZT",
"loco-szt": "SZT",
"loco-s": "Spalinowóz",
"loco-e": "Elektrowóz"
},
"realstock": {
"title": "ZESTAWIENIA REALNE by",
"search-name": "Szukaj po nazwie",
"search-stock": "Szukaj po pojazdach",
"action-reset": "RESETUJ"
}
}
{
"app": {
"title": "EDYTOR SKŁADÓW ONLINE"
},
"footer": {
"disclaimer": "Ta strona ma charakter informacyjny. Autor nie ponosi odpowiedzialności za tworzenie pociągów niezgodnych z {tos}!",
"tos": "regulaminem symulatora Train Driver 2",
"tos-href": "https://docs.google.com/document/d/1UAAPUtN0d_RoS4RgOzEzllJZJhA0VcizzCzKW4QylbY/edit",
"version-check": "Strona jest kompletna dla wersji {version} symulatora TD2"
},
"inputs": {
"title": "WYBIERZ POJAZD SZYNOWY",
"input-vehicle": "Wybierz pojazd trakcyjny",
"input-carwagon": "Wybierz wagon",
"cargo-title": "Ładunek (tylko wybrane towarowe)",
"no-cargo-available": "brak dostępnych ładunków",
"cargo-empty": "próżny",
"loco-e": "ELEKTR.",
"loco-s": "SPAL.",
"loco-ezt": "EZT",
"loco-szt": "SZT",
"car-passenger": "PASAŻERSKIE",
"car-cargo": "TOWAROWE",
"action-add": "DODAJ NOWY",
"action-swap": "ZAMIEŃ ZA",
"real-stock": "REALNE ZESTAWIENIA"
},
"preview": {
"title": "PODGLĄD WYBRANEGO POJAZDU",
"loading": "ŁADOWANIE OBRAZU...",
"desc": "Wybierz pojazd lub wagon, aby zobaczyć jego podgląd powyżej",
"sponsor-only": "* TYLKO DLA SPONSORÓW",
"loco-e": "ELEKTROWÓZ",
"loco-s": "SPALINOWÓZ",
"loco-ezt": "EZT",
"loco-szt": "SZT",
"car-passenger": "WAGON PASAŻERSKI",
"car-cargo": "WAGON TOWAROWY",
"cabin": "Typ kabiny:",
"construction": "Typ konstrukcji:"
},
"topbar": {
"stock-list": "SKŁAD",
"wiki-list": "POJAZDY",
"number-generator": "GNR NUMERU",
"stock-generator": "GNR SKŁADU"
},
"stocklist": {
"title": "EDYTOR SKŁADU",
"alert-copied": "Skład został skopiowany do twojego schowka!",
"alert-empty": "Lista pojazdów jest pusta!",
"prompt-file": "Nazwij plik, a następnie pobierz do folderu Presets (Dokumenty/TTSK/TrainDriver2):",
"vehicle-no": "POJAZD NR",
"no-vehicle-chosen": "NIE WYBRANO POJAZDU",
"action-move-up": "PRZENIEŚ WYŻEJ",
"action-move-down": "PRZENIEŚ NIŻEJ",
"action-remove": "USUŃ",
"action-upload": "WCZYTAJ",
"action-download": "POBIERZ",
"action-copy": "SKOPIUJ",
"action-reset": "ZRESETUJ",
"action-shuffle": "PRZETASUJ",
"mass": "Masa",
"mass-accepted": "dopuszczalna",
"length": "Długość",
"vmax": "vMax",
"coldstart-info": "Zimny start lokomotywy czołowej (tylko elektrowozy typów 303E i 203E)",
"list-empty": "Lista pojazdów jest pusta!",
"warning-not-suitable": "Lokomotywy EP07 i EP08 są przeznaczone jedynie do ruchu pasażerskiego!",
"warning-passenger-too-long": "Maksymalna długość składów pasażerskich nie może przekraczać 350m!",
"warning-freight-too-long": "Maksymalna długość składów innych niż pasażerskie nie może przekraczać 650m!",
"warning-too-many-locos": "Ten skład posiada za dużo pojazdów trakcyjnych!",
"warning-too-heavy": "Ten skład jest za ciężki! Sprawdź {href}",
"acceptable-mass-docs": "dopuszczalne masy składów"
},
"stockgen": {
"title": "GENERATOR SKŁADU TOWAROWEGO",
"properties-title": "WŁAŚCIWOŚCI SKŁADU",
"properties-desc": "⇐ Dodaj lokomotywę na pierwsze miejsce listy, aby uwzględnić ją przy losowaniu składu!",
"input-mass": "Maksymalna masa (t)",
"input-length": "Maks. długość (m)",
"input-carcount": "Maks. liczba wagonów",
"cargo-title": "ŁADUNEK",
"cargo-desc": "Wybierz ładunki, którymi chcesz wypełnić dostępne wagony:",
"chosen-title": "WAGONY Z WYBRANYMI ŁADUNKAMI",
"chosen-empty-warning": "Wybierz co najmniej jeden ładunek, aby zobaczyć wagony, które go posiadają!",
"chosen-warning": "Wagony posiadające wybrane ładunki. Najedź na nazwę, aby zobaczyć podgląd wagonu. Kliknij, aby wyłączyć z losowania (tylko podświetlone nazwy będą uwzględnione).",
"action-generate": "WYGENERUJ",
"action-generate-empty": "WYGENERUJ PRÓŻNE WAGONY",
"action-reset": "ZRESETUJ ŁADUNKI"
},
"numgen": {
"title": "GENERATOR NUMERU POCIĄGU",
"alert": "Numer został skopiowany do twojego schowka!",
"start-region": "Początkowy obszar konstrukcyjny",
"end-region": "Końcowy obszar konstrukcyjny",
"train-category": "Kategoria pociągu",
"number-info": "Wygenerowany numer pociągu:",
"warning": "Wybierz kategorię oraz (opcjonalnie) obszary konstrukcyjne",
"td2-wiki": "> Szczegółowe zasady numeracji (wikipedia TD2)",
"td2-wiki-link": "https://wiki.td2.info.pl/index.php?title=Zasady_numeracji_poci%C4%85g%C3%B3w",
"action-random-region": "LOSUJ OBSZARY",
"action-random-number": "LOSUJ NUMER",
"categories": {
"EI": "ekspres krajowy (EI)",
"MP/RP": "(między)wojewódzki pośpieszny (MP/RP)",
"RO": "wojewódzki osobowy (RO)",
"PW": "próżny \"służbowy\" (PW)",
"TM": "towarowy do przewozów masowych (TM)",
"TK": "towarowy do obsługi stacji (TK)",
"LT": "lokomotywa luzem (LT)"
},
"rules": {
"EI": "4 cyfry - końcówka z przedziału 00-99",
"MP/RP": "5 cyfr - końcówka z przedziału 050-169",
"RO": "5 cyfr - końcówka z przedziału 200-999",
"PW": "6 cyfr - '6' na 3. miejscu; końcówka z przedziału 000-899",
"TM": "6 cyfr - '4' na 3. miejscu; końcówka z przedziału 000-899",
"TK": "6 cyfr - '3' na 3. miejscu; końcówka z przedziału 000-899",
"LT": "6 cyfr - '5' na 3. miejscu; końcówka z przedziału 000-899"
}
},
"wiki": {
"title": "LISTA DOSTĘPNYCH POJAZDÓW",
"action-vehicles": "POJ. TRAKCYJNE",
"action-carriages": "WAGONY",
"search": "Wyszukaj pojazd...",
"header": {
"image": "Zdjęcie",
"type": "Nazwa",
"power": "Rodzaj",
"constructionType": "Konstrukcja",
"coldStart": "Zimny start",
"length": "Długość",
"mass": "Masa",
"maxSpeed": "Prędkość",
"cargoCount": "Ładunki"
},
"loco-ezt": "EZT",
"loco-szt": "SZT",
"loco-s": "Spalinowóz",
"loco-e": "Elektrowóz"
},
"realstock": {
"title": "ZESTAWIENIA REALNE by",
"search-name": "Szukaj po nazwie",
"search-stock": "Szukaj po pojazdach",
"action-reset": "RESETUJ"
}
}
+6 -7
View File
@@ -1,14 +1,13 @@
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import { registerSW } from 'virtual:pwa-register';
import { createApp } from "vue";
import { createPinia } from "pinia";
import { registerSW } from "virtual:pwa-register";
import App from './App.vue';
import i18n from './i18n-setup';
import App from "./App.vue";
import i18n from "./i18n-setup";
const pinia = createPinia();
registerSW({
immediate: true,
});
createApp(App).use(pinia).use(i18n).mount('#app');
createApp(App).use(pinia).use(i18n).mount("#app");
+10 -2
View File
@@ -1,9 +1,17 @@
import { defineComponent } from 'vue';
import { defineComponent } from "vue";
export default defineComponent({
methods: {
getIconURL(name: string, ext = 'svg'): string {
getIconURL(name: string, ext = "svg"): string {
return `/images/icon-${name}.${ext}`;
},
getThumbnailURL(vehicleType: string, size: "small" | "large") {
return `${
import.meta.env.VITE_API_DEV === "1"
? "http://localhost:5500"
: "https://spythere.github.io/api"
}/td2/images/${vehicleType}--${size == "small" ? 300 : 800}px.jpg`;
},
},
});
+21 -15
View File
@@ -1,7 +1,7 @@
import { defineComponent } from 'vue';
import { useStore } from '../store';
import { ICargo, ICarWagon, ILocomotive, IStock, Vehicle } from '../types';
import { isLocomotive } from '../utils/vehicleUtils';
import { defineComponent } from "vue";
import { useStore } from "../store";
import { ICargo, ICarWagon, ILocomotive, IStock, Vehicle } from "../types";
import { isLocomotive } from "../utils/vehicleUtils";
export default defineComponent({
setup() {
@@ -39,14 +39,16 @@ export default defineComponent({
const stock = this.getStockObject(vehicle, cargo);
if (stock.isLoco && !this.store.stockList[0]?.isLoco) this.store.stockList.unshift(stock);
if (stock.isLoco && !this.store.stockList[0]?.isLoco)
this.store.stockList.unshift(stock);
else this.store.stockList.push(stock);
},
addLocomotive(loco: ILocomotive) {
const stockObj = this.getStockObject(loco);
if (this.store.stockList.length > 0 && !this.store.stockList[0].isLoco) this.store.stockList.unshift(stockObj);
if (this.store.stockList.length > 0 && !this.store.stockList[0].isLoco)
this.store.stockList.unshift(stockObj);
else this.store.stockList.push(stockObj);
},
@@ -57,7 +59,7 @@ export default defineComponent({
},
loadStockFromString(stockString: string) {
const stockArray = stockString.trim().split(';');
const stockArray = stockString.trim().split(";");
this.store.stockList.length = 0;
this.store.chosenVehicle = null;
@@ -73,22 +75,26 @@ export default defineComponent({
let vehicleCargo: ICargo | null = null;
if (/^(EU|EP|ET|SM|EN|2EN|SN)/.test(type)) {
const [locoType, coldStart] = type.split(',');
vehicle = this.store.locoDataList.find((loco) => loco.type == locoType) || null;
const [locoType, coldStart] = type.split(",");
vehicle =
this.store.locoDataList.find((loco) => loco.type == locoType) ||
null;
if (i == 0 && coldStart == 'c') this.store.isColdStart = true;
if (i == 0 && coldStart == "c") this.store.isColdStart = true;
} else {
const [carType, cargo] = type.split(':');
vehicle = this.store.carDataList.find((car) => car.type == carType) || null;
const [carType, cargo] = type.split(":");
vehicle =
this.store.carDataList.find((car) => car.type == carType) || null;
if (cargo) vehicleCargo = vehicle?.cargoList.find((c) => c.id == cargo) || null;
if (cargo)
vehicleCargo =
vehicle?.cargoList.find((c) => c.id == cargo) || null;
}
if (!vehicle) console.log('Brak pojazdu:', type);
if (!vehicle) console.log("Brak pojazdu:", type);
this.addVehicle(vehicle, vehicleCargo);
});
},
},
});
+17 -10
View File
@@ -1,6 +1,6 @@
import { defineComponent } from 'vue';
import { useStore } from '../store';
import { ICarWagon, ILocomotive, IStock, Vehicle } from '../types';
import { defineComponent } from "vue";
import { useStore } from "../store";
import { ICarWagon, ILocomotive, IStock } from "../types";
export default defineComponent({
setup() {
@@ -12,12 +12,14 @@ export default defineComponent({
computed: {
locoOptions() {
return this.store.locoDataList
.slice()
.sort((a, b) => (a.type > b.type ? 1 : -1))
.filter((loco) => loco.power == this.store.chosenLocoPower);
},
carOptions() {
return this.store.carDataList
.slice()
.sort((a, b) => (a.type > b.type ? 1 : -1))
.filter((car) => car.useType == this.store.chosenCarUseType);
},
@@ -37,28 +39,34 @@ export default defineComponent({
this.store.chosenCargo = null;
},
previewVehicleByType(type: 'loco' | 'car' | 'cargo') {
previewVehicleByType(type: "loco" | "car" | "cargo") {
this.$nextTick(() => {
if (!this.store.chosenLoco && !this.store.chosenCar) return;
this.store.chosenVehicle = type == 'loco' ? this.store.chosenLoco : this.store.chosenCar;
this.store.chosenVehicle =
type == "loco" ? this.store.chosenLoco : this.store.chosenCar;
this.store.chosenCargo =
this.store.chosenCar?.cargoList.find((cargo) => cargo.id == this.store.chosenCargo?.id) || null;
this.store.chosenCar?.cargoList.find(
(cargo) => cargo.id == this.store.chosenCargo?.id,
) || null;
});
},
previewStock(stock: IStock) {
if (this.store.chosenVehicle?.imageSrc != stock.imgSrc) this.store.imageLoading = true;
if (this.store.chosenVehicle?.imageSrc != stock.imgSrc)
this.store.imageLoading = true;
if (stock.isLoco) {
const chosenLoco = this.store.locoDataList.find((v) => v.type == stock.type) || null;
const chosenLoco =
this.store.locoDataList.find((v) => v.type == stock.type) || null;
this.store.chosenVehicle = chosenLoco;
this.store.chosenLoco = chosenLoco;
this.store.chosenCargo = null;
this.store.chosenLocoPower = stock.useType;
} else {
const chosenCar = this.store.carDataList.find((v) => v.type == stock.type) || null;
const chosenCar =
this.store.carDataList.find((v) => v.type == stock.type) || null;
this.store.chosenVehicle = chosenCar;
this.store.chosenCar = chosenCar;
@@ -89,4 +97,3 @@ export default defineComponent({
},
},
});
+15 -8
View File
@@ -1,5 +1,5 @@
import { defineComponent } from 'vue';
import { useStore } from '../store';
import { defineComponent } from "vue";
import { useStore } from "../store";
export default defineComponent({
setup() {
@@ -18,7 +18,10 @@ export default defineComponent({
},
trainTooHeavy() {
return this.store.acceptableMass && this.store.totalMass > this.store.acceptableMass;
return (
this.store.acceptableMass &&
this.store.totalMass > this.store.acceptableMass
);
},
locoNotSuitable() {
@@ -26,15 +29,19 @@ export default defineComponent({
!this.store.isTrainPassenger &&
this.store.stockList.length > 1 &&
!this.store.stockList.every((stock) => stock.isLoco) &&
this.store.stockList.some((stock) => stock.isLoco && stock.type.startsWith('EP'))
this.store.stockList.some(
(stock) => stock.isLoco && stock.type.startsWith("EP"),
)
);
},
tooManyLocomotives() {
return this.store.stockList.reduce((acc, stock) => {
if (stock.isLoco) acc += stock.count;
return acc;
}, 0) > 2;
return (
this.store.stockList.reduce((acc, stock) => {
if (stock.isLoco) acc += stock.count;
return acc;
}, 0) > 2
);
},
},
});
+18 -18
View File
@@ -1,5 +1,5 @@
import { IStore } from './types';
import { defineStore } from 'pinia';
import { IStockData, IStore } from "./types";
import { defineStore } from "pinia";
import {
acceptableMass,
carDataList,
@@ -9,10 +9,11 @@ import {
maxStockSpeed,
totalLength,
totalMass,
} from './utils/vehicleUtils';
} from "./utils/vehicleUtils";
import http from "./http";
export const useStore = defineStore({
id: 'store',
id: "store",
state: () =>
({
chosenCar: null,
@@ -25,8 +26,8 @@ export const useStore = defineStore({
showSupporter: false,
imageLoading: false,
chosenLocoPower: 'loco-e',
chosenCarUseType: 'car-passenger',
chosenLocoPower: "loco-e",
chosenCarUseType: "car-passenger",
stockList: [],
cargoOptions: [],
@@ -38,15 +39,15 @@ export const useStore = defineStore({
chosenStockListIndex: -1,
chosenRealStockName: undefined,
vehiclePreviewSrc: '',
vehiclePreviewSrc: "",
stockSectionMode: 'stock-list',
stockSectionMode: "stock-list",
isRandomizerCardOpen: false,
isRealStockListCardOpen: false,
stockData: undefined,
} as IStore),
}) as IStore,
getters: {
locoDataList: (state) => locoDataList(state),
@@ -61,20 +62,21 @@ export const useStore = defineStore({
actions: {
async fetchStockInfoData() {
const stockData = await (await fetch(`https://spythere.github.io/api/td2/data/stockInfo.json`)).json();
const stockData = (await http.get<IStockData>("td2/data/stockInfo.json"))
.data;
this.stockData = stockData;
},
handleRouting() {
switch (window.location.pathname) {
case '/numgnr':
this.stockSectionMode = 'number-generator';
case "/numgnr":
this.stockSectionMode = "number-generator";
break;
case '/stockgnr':
this.stockSectionMode = 'stock-generator';
case "/stockgnr":
this.stockSectionMode = "stock-generator";
break;
case '/vehicles':
this.stockSectionMode = 'wiki-list';
case "/vehicles":
this.stockSectionMode = "wiki-list";
break;
default:
break;
@@ -82,5 +84,3 @@ export const useStore = defineStore({
},
},
});
+268 -268
View File
@@ -1,268 +1,268 @@
@import url('https://fonts.googleapis.com/css2?family=Lato:wght@400;700;900&display=swap');
$breakpointMd: 960px;
$breakpointSm: 550px;
$bgColor: #2b3552;
$textColor: #fff;
$secondaryColor: #222;
$accentColor: #e4c428;
::-webkit-scrollbar {
width: 7px;
height: 7px;
&-track {
background: #222;
border-radius: 0.5rem;
}
&-thumb {
border-radius: 1rem;
background: #777;
}
&-corner {
background: #222;
}
}
body,
html {
margin: 0;
padding: 0;
font-family: 'Lato', sans-serif;
background-color: $bgColor;
overflow-x: hidden;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
a {
color: white;
text-decoration: none;
transition: color 250ms;
&:visited {
color: white;
}
&:hover,
&:focus {
color: $accentColor;
}
}
select,
option,
input,
button {
font-family: 'Lato', sans-serif;
font-size: 1em;
}
button {
border: none;
outline: none;
background: none;
padding: 0;
margin: 0;
cursor: pointer;
font-size: 1em;
color: white;
&:hover {
color: $accentColor;
}
}
.btn {
padding: 0.4em 0.75em;
outline: none;
background-color: #222;
border-radius: 8px;
font-weight: bold;
transition: all 250ms;
&:hover {
color: $accentColor;
}
&.btn--outline {
background: none;
font-weight: bold;
outline: 1px solid $accentColor;
}
&:focus-visible {
color: $accentColor;
outline: 1px solid white;
}
&[data-disabled='true'] {
user-select: none;
pointer-events: none;
-moz-user-select: none;
-webkit-user-select: none;
opacity: 0.75;
background-color: #2b2b2b;
}
&--image {
display: flex;
justify-content: center;
align-items: center;
gap: 0.5em;
img {
width: 1.3em;
vertical-align: middle;
}
}
&--text {
font-weight: bold;
transition: all 250ms;
background: none;
padding: 0;
&:focus-visible {
outline: 1px solid white;
}
}
}
select,
input[type='text'],
input[type='number'] {
background: none;
border: 2px solid #aaa;
outline: none;
padding: 0.25em 0.35em;
color: white;
font-size: 1em;
width: 18em;
&:focus-visible {
border-color: $accentColor;
}
&::placeholder {
color: #aaa;
}
}
option {
color: white;
border: none;
background-color: $bgColor;
}
ul {
list-style: none;
margin: 0;
padding: 0;
}
.text {
&--accent {
color: $accentColor;
}
&--grayed {
color: #aaa;
}
}
.g-card {
position: fixed;
top: 1em;
left: 0;
width: 100vw;
height: 100vh;
display: flex;
justify-content: center;
z-index: 200;
&_bg {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background-color: #000000aa;
z-index: 10;
}
}
.g-choice {
input {
display: none;
}
span {
padding: 0.25em 1em;
border-radius: 0.25em;
border: 2px solid white;
margin: 0.25em;
cursor: pointer;
transition: all 100ms ease;
}
span:focus {
color: $accentColor;
outline: none;
}
label > input:checked + span {
color: $accentColor;
border-color: $accentColor;
}
}
// Vue Transition anims
.slide-top {
&-enter-from,
&-leave-to {
transform: translateY(-100%);
}
&-enter-active,
&-leave-active {
transition: transform 100ms ease-in-out;
}
}
.card-appear {
&-enter-from,
&-leave-to {
opacity: 0;
}
&-enter-active,
&-leave-active {
transition: all 100ms ease-in-out;
}
}
@import url("https://fonts.googleapis.com/css2?family=Lato:wght@400;700;900&display=swap");
$breakpointMd: 960px;
$breakpointSm: 550px;
$bgColor: #2b3552;
$textColor: #fff;
$secondaryColor: #222;
$accentColor: #e4c428;
::-webkit-scrollbar {
width: 7px;
height: 7px;
&-track {
background: #222;
border-radius: 0.5rem;
}
&-thumb {
border-radius: 1rem;
background: #777;
}
&-corner {
background: #222;
}
}
body,
html {
margin: 0;
padding: 0;
font-family: "Lato", sans-serif;
background-color: $bgColor;
overflow-x: hidden;
}
*,
*::before,
*::after {
box-sizing: border-box;
}
a {
color: white;
text-decoration: none;
transition: color 250ms;
&:visited {
color: white;
}
&:hover,
&:focus {
color: $accentColor;
}
}
select,
option,
input,
button {
font-family: "Lato", sans-serif;
font-size: 1em;
}
button {
border: none;
outline: none;
background: none;
padding: 0;
margin: 0;
cursor: pointer;
font-size: 1em;
color: white;
&:hover {
color: $accentColor;
}
}
.btn {
padding: 0.4em 0.75em;
outline: none;
background-color: #222;
border-radius: 8px;
font-weight: bold;
transition: all 250ms;
&:hover {
color: $accentColor;
}
&.btn--outline {
background: none;
font-weight: bold;
outline: 1px solid $accentColor;
}
&:focus-visible {
color: $accentColor;
outline: 1px solid white;
}
&[data-disabled="true"] {
user-select: none;
pointer-events: none;
-moz-user-select: none;
-webkit-user-select: none;
opacity: 0.75;
background-color: #2b2b2b;
}
&--image {
display: flex;
justify-content: center;
align-items: center;
gap: 0.5em;
img {
width: 1.3em;
vertical-align: middle;
}
}
&--text {
font-weight: bold;
transition: all 250ms;
background: none;
padding: 0;
&:focus-visible {
outline: 1px solid white;
}
}
}
select,
input[type="text"],
input[type="number"] {
background: none;
border: 2px solid #aaa;
outline: none;
padding: 0.25em 0.35em;
color: white;
font-size: 1em;
width: 18em;
&:focus-visible {
border-color: $accentColor;
}
&::placeholder {
color: #aaa;
}
}
option {
color: white;
border: none;
background-color: $bgColor;
}
ul {
list-style: none;
margin: 0;
padding: 0;
}
.text {
&--accent {
color: $accentColor;
}
&--grayed {
color: #aaa;
}
}
.g-card {
position: fixed;
top: 1em;
left: 0;
width: 100vw;
height: 100vh;
display: flex;
justify-content: center;
z-index: 200;
&_bg {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background-color: #000000aa;
z-index: 10;
}
}
.g-choice {
input {
display: none;
}
span {
padding: 0.25em 1em;
border-radius: 0.25em;
border: 2px solid white;
margin: 0.25em;
cursor: pointer;
transition: all 100ms ease;
}
span:focus {
color: $accentColor;
outline: none;
}
label > input:checked + span {
color: $accentColor;
border-color: $accentColor;
}
}
// Vue Transition anims
.slide-top {
&-enter-from,
&-leave-to {
transform: translateY(-100%);
}
&-enter-active,
&-leave-active {
transition: transform 100ms ease-in-out;
}
}
.card-appear {
&-enter-from,
&-leave-to {
opacity: 0;
}
&-enter-active,
&-leave-active {
transition: all 100ms ease-in-out;
}
}
+1 -2
View File
@@ -1,4 +1,4 @@
@import './global.scss';
@import "./global.scss";
.tab {
height: 100%;
@@ -77,4 +77,3 @@ hr {
}
}
}
+20 -12
View File
@@ -1,5 +1,5 @@
export type Vehicle = ILocomotive | ICarWagon;
export type StockSectionMode = 'STOCK_LIST' | 'STOCK_GENERATOR';
export type StockSectionMode = "STOCK_LIST" | "STOCK_GENERATOR";
export interface IStore {
chosenCar: ICarWagon | null;
@@ -28,11 +28,21 @@ export interface IStore {
isRandomizerCardOpen: boolean;
isRealStockListCardOpen: boolean;
stockSectionMode: 'stock-list' | 'stock-generator' | 'number-generator' | 'wiki-list';
stockSectionMode:
| "stock-list"
| "stock-generator"
| "number-generator"
| "wiki-list";
stockData?: IStockData;
}
export type TStockInfoKey = 'loco-e' | 'loco-s' | 'loco-ezt' | 'loco-szt' | 'car-passenger' | 'car-cargo';
export type TStockInfoKey =
| "loco-e"
| "loco-s"
| "loco-ezt"
| "loco-szt"
| "car-passenger"
| "car-cargo";
export interface IStockProps {
type: string;
@@ -52,12 +62,12 @@ export interface IStockData {
};
info: {
'car-cargo': [string, string, boolean, boolean, string][];
'car-passenger': [string, string, boolean, boolean, string][];
'loco-e': [string, string, string, string, boolean][];
'loco-s': [string, string, string, string, boolean][];
'loco-szt': [string, string, string, string, boolean][];
'loco-ezt': [string, string, string, string, boolean][];
"car-cargo": [string, string, boolean, boolean, string][];
"car-passenger": [string, string, boolean, boolean, string][];
"loco-e": [string, string, string, string, boolean][];
"loco-s": [string, string, string, string, boolean][];
"loco-szt": [string, string, string, string, boolean][];
"loco-ezt": [string, string, string, string, boolean][];
};
props: IStockProps[];
@@ -79,9 +89,8 @@ export interface ILocomotive {
}
export interface ICarWagon {
//"203V_PKPC_Fll_01","203V",true,false,"100",img
type: string;
useType: 'car-passenger' | 'car-cargo';
useType: "car-passenger" | "car-cargo";
constructionType: string;
loadable: boolean;
supportersOnly: boolean;
@@ -120,4 +129,3 @@ export interface IReadyStockItem {
number: string;
name: string;
}
+7 -5
View File
@@ -1,5 +1,7 @@
const supportedConstructions = ['303e', '203e'];
export function locoSupportsColdStart(constructionType: string) {
return new RegExp(`(${supportedConstructions.join('|')})`).test(constructionType);
}
const supportedConstructions = ["303e", "203e"];
export function locoSupportsColdStart(constructionType: string) {
return new RegExp(`(${supportedConstructions.join("|")})`).test(
constructionType,
);
}
+10 -5
View File
@@ -1,14 +1,19 @@
import speedLimitTable from '../constants/speedLimits.json';
import speedLimitTable from "../constants/speedLimits.json";
export type LocoType = keyof typeof speedLimitTable;
export const calculateSpeedLimit = (locoType: LocoType, stockMass: number, isTrainPassenger: boolean) => {
const speedTable = speedLimitTable[locoType][isTrainPassenger ? 'passenger' : 'cargo'];
export const calculateSpeedLimit = (
locoType: LocoType,
stockMass: number,
isTrainPassenger: boolean,
) => {
const speedTable =
speedLimitTable[locoType][isTrainPassenger ? "passenger" : "cargo"];
if (!speedTable) return undefined;
let speedLimit = 0;
for (let mass in speedTable) if (stockMass > Number(mass)) speedLimit = (speedTable as any)[mass];
for (const mass in speedTable)
if (stockMass > Number(mass)) speedLimit = (speedTable as any)[mass];
return speedLimit;
};
+60 -30
View File
@@ -1,8 +1,10 @@
import { EVehicleUseType } from '../enums/EVehicleUseType';
import { ICarWagon, ILocomotive, IStore, TStockInfoKey } from '../types';
import { LocoType, calculateSpeedLimit } from './speedLimitUtils';
import { EVehicleUseType } from "../enums/EVehicleUseType";
import { ICarWagon, ILocomotive, IStore } from "../types";
import { LocoType, calculateSpeedLimit } from "./speedLimitUtils";
export function isLocomotive(vehicle: ILocomotive | ICarWagon): vehicle is ILocomotive {
export function isLocomotive(
vehicle: ILocomotive | ICarWagon,
): vehicle is ILocomotive {
return (vehicle as ILocomotive).power !== undefined;
}
@@ -12,15 +14,21 @@ export function locoDataList(state: IStore) {
const stockData = state.stockData;
return Object.keys(stockData.info).reduce((acc, vehiclePower) => {
if (!vehiclePower.startsWith('loco')) return acc;
if (!vehiclePower.startsWith("loco")) return acc;
const locoVehiclesData = stockData.info[vehiclePower as 'loco-e' | 'loco-s' | 'loco-ezt' | 'loco-szt'];
const locoVehiclesData =
stockData.info[
vehiclePower as "loco-e" | "loco-s" | "loco-ezt" | "loco-szt"
];
locoVehiclesData.forEach((loco) => {
if (state.showSupporter && !loco[4]) return;
const [type, constructionType, cabinType, maxSpeed, supportersOnly] = loco;
const locoProps = stockData.props.find((prop) => constructionType == prop.type);
const [type, constructionType, cabinType, maxSpeed, supportersOnly] =
loco;
const locoProps = stockData.props.find(
(prop) => constructionType == prop.type,
);
acc.push({
power: vehiclePower,
@@ -29,10 +37,16 @@ export function locoDataList(state: IStore) {
cabinType,
maxSpeed: Number(maxSpeed),
supportersOnly,
imageSrc: '',
imageSrc: "",
length: locoProps?.length && type.startsWith('2EN') ? locoProps.length * 2 : locoProps?.length || 0,
mass: locoProps?.mass && type.startsWith('2EN') ? 253 : locoProps?.mass || 0,
length:
locoProps?.length && type.startsWith("2EN")
? locoProps.length * 2
: locoProps?.length || 0,
mass:
locoProps?.mass && type.startsWith("2EN")
? 253
: locoProps?.mass || 0,
});
});
@@ -46,29 +60,32 @@ export function carDataList(state: IStore) {
const stockData = state.stockData;
return Object.keys(stockData.info).reduce((acc, vehicleUseType) => {
if (!vehicleUseType.startsWith('car')) return acc;
if (!vehicleUseType.startsWith("car")) return acc;
const carVehiclesData = stockData.info[vehicleUseType as 'car-passenger' | 'car-cargo'];
const carVehiclesData =
stockData.info[vehicleUseType as "car-passenger" | "car-cargo"];
carVehiclesData.forEach((car) => {
if (state.showSupporter && !car[3]) return;
const carPropsData = stockData.props.find((v) => car[0].toString().startsWith(v.type));
const carPropsData = stockData.props.find((v) =>
car[0].toString().startsWith(v.type),
);
acc.push({
useType: vehicleUseType as 'car-passenger' | 'car-cargo',
useType: vehicleUseType as "car-passenger" | "car-cargo",
type: car[0],
constructionType: car[1],
loadable: car[2],
supportersOnly: car[3],
maxSpeed: Number(car[4]),
imageSrc: '',
imageSrc: "",
cargoList:
!carPropsData || carPropsData.cargo === null
? []
: carPropsData.cargo.split(';').map((cargo) => ({
id: cargo.split(':')[0],
totalMass: Number(cargo.split(':')[1]),
: carPropsData.cargo.split(";").map((cargo) => ({
id: cargo.split(":")[0],
totalMass: Number(cargo.split(":")[1]),
})),
mass: carPropsData?.mass || 0,
@@ -82,33 +99,45 @@ export function carDataList(state: IStore) {
export function totalMass(state: IStore) {
return ~~state.stockList.reduce(
(acc, stock) => acc + (stock.cargo ? stock.cargo.totalMass : stock.mass) * stock.count,
0
(acc, stock) =>
acc + (stock.cargo ? stock.cargo.totalMass : stock.mass) * stock.count,
0,
);
}
export function totalLength(state: IStore) {
return state.stockList.reduce((acc, stock) => acc + stock.length * stock.count, 0);
return state.stockList.reduce(
(acc, stock) => acc + stock.length * stock.count,
0,
);
}
export function maxStockSpeed(state: IStore) {
const stockSpeedLimit = state.stockList.reduce(
(acc, stock) => (stock.maxSpeed < acc || acc == 0 ? stock.maxSpeed : acc),
0
0,
);
const headingLoco = state.stockList[0]?.isLoco ? state.stockList[0] : undefined;
const headingLoco = state.stockList[0]?.isLoco
? state.stockList[0]
: undefined;
if (!headingLoco) return stockSpeedLimit;
const locoType = headingLoco.type.split('-')[0];
const locoType = headingLoco.type.split("-")[0];
if (/^(EN|2EN|SN)/.test(locoType)) return stockSpeedLimit;
const stockMass = totalMass(state);
const speedLimitByMass = calculateSpeedLimit(locoType as LocoType, stockMass, isTrainPassenger(state));
const speedLimitByMass = calculateSpeedLimit(
locoType as LocoType,
stockMass,
isTrainPassenger(state),
);
return speedLimitByMass ? Math.min(stockSpeedLimit, speedLimitByMass) : stockSpeedLimit;
return speedLimitByMass
? Math.min(stockSpeedLimit, speedLimitByMass)
: stockSpeedLimit;
}
export function acceptableMass(state: IStore) {
@@ -150,12 +179,13 @@ export function chosenRealStock(state: IStore) {
for (let i = 0; i < stock.count; i++) acc.push(stock.type);
return acc;
}, [] as string[])
.join(';');
.join(";");
const realStockObj = state.readyStockList.find((readyStock) => readyStock.stockString == currentStockString);
const realStockObj = state.readyStockList.find(
(readyStock) => readyStock.stockString == currentStockString,
);
state.chosenRealStockName = realStockObj?.stockId ?? undefined;
return realStockObj;
}
+35 -35
View File
@@ -1,35 +1,35 @@
<template>
<div class="app-container">
<MainContainer />
<Footer />
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useStore } from '../store';
import MainContainer from '../components/app/MainContainer.vue';
import Footer from '../components/app/Footer.vue';
export default defineComponent({
components: {
MainContainer,
Footer,
},
data: () => ({
store: useStore(),
}),
});
</script>
<style lang="scss" scoped>
.app-container {
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
}
</style>
<template>
<div class="app-container">
<MainContainer />
<FooterVue />
</div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import { useStore } from "../store";
import MainContainer from "../components/app/MainContainer.vue";
import FooterVue from "../components/app/Footer.vue";
export default defineComponent({
components: {
MainContainer,
FooterVue,
},
data: () => ({
store: useStore(),
}),
});
</script>
<style lang="scss" scoped>
.app-container {
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
}
</style>
+4 -4
View File
@@ -1,7 +1,7 @@
/// <reference types="vite/client" />
declare module '*.vue' {
import type { DefineComponent } from 'vue'
const component: DefineComponent<{}, {}, any>
export default component
declare module "*.vue" {
import type { DefineComponent } from "vue";
const component: DefineComponent<{}, {}, any>;
export default component;
}