Compare commits

...

124 Commits

Author SHA1 Message Date
Spythere 50102c6127 Merge pull request #30 from Spythere/development
chore: new train categories
2024-05-13 19:25:31 +02:00
Spythere 210c49fb04 chore: new train categories 2024-05-13 19:25:09 +02:00
Spythere bc3db163b1 Merge pull request #29 from Spythere/development
bump(version): 1.8.7
2024-05-13 19:16:04 +02:00
Spythere b364586ebc bump(version): 1.8.7 2024-05-13 19:15:19 +02:00
Spythere f3509ef217 Merge pull request #28 from Spythere/development
1.8.7
2024-05-13 19:14:59 +02:00
Spythere d584d99f4c chore: changed cold start requirements 2024-05-13 19:11:15 +02:00
Spythere 078c941910 chore: fixed preview image loading 2024-05-13 19:04:46 +02:00
Spythere 76670ceb29 Merge pull request #27 from Spythere/development
1.8.6
2024-05-13 16:12:46 +02:00
Spythere 4cdd8ea06a bump(version): 1.8.6 2024-05-13 15:58:39 +02:00
Spythere 454ae138f7 chore: config fix 2024-05-13 15:57:49 +02:00
Spythere 640c5e262b chore: stock upload fix 2024-05-13 15:52:06 +02:00
Spythere 7416d7d59f chore: pwa caching fixes 2024-05-13 15:51:48 +02:00
Spythere ca1255b37e Merge pull request #26 from Spythere/development
1.8.5 (hotfix)
2024-04-16 20:32:43 +02:00
Spythere de0a8520f3 fix(workflows): color prop 2024-04-16 20:31:42 +02:00
Spythere e51919896f Merge pull request #25 from Spythere/development
1.8.5
2024-04-16 20:16:17 +02:00
Spythere f9cd9a2a33 chore(wiki): preserving scroll state 2024-04-16 20:06:23 +02:00
Spythere 897d6d0c36 chore(stock): stock tab design 2024-04-16 19:54:51 +02:00
Spythere b241b60657 chore(numgen): EC/EN categories added 2024-04-16 19:37:09 +02:00
Spythere 1f1bef1cc9 chore(numgen): changed select responsiveness 2024-04-16 00:42:58 +02:00
Spythere 05f8a0ca68 fix(locales): sort casing 2024-04-16 00:28:56 +02:00
Spythere 541572e415 refactor: stock & vehicle typings 2024-04-15 18:32:19 +02:00
Spythere 61c7f15fcf chore(wiki): changed inputs order 2024-04-15 16:41:27 +02:00
Spythere 1da1645c51 chore(preview): cleanup 2024-04-15 16:40:19 +02:00
Spythere 8dc670b631 chore(preview): adjustments in image loading & fallback 2024-04-15 16:39:46 +02:00
Spythere 3424f9a952 chore(pwa): image caching changes 2024-04-15 16:36:30 +02:00
Spythere 4079426506 chore(workflows): added release discord webhook notification 2024-04-15 15:11:03 +02:00
Spythere 35f2a5ca09 chore(wiki): design improvements 2024-04-14 22:08:32 +02:00
Spythere b840a6cd46 fix(stock): keydown number bug 2024-04-14 21:45:51 +02:00
Spythere 1aca0f7ed1 fix(wiki): crashing input regex 2024-04-14 21:33:57 +02:00
Spythere b009bc03e8 bump(version): 1.8.5 2024-04-14 00:13:16 +02:00
Spythere baa39a5a99 chore: wiki list tab revamp 2024-04-14 00:13:01 +02:00
Spythere 99cbde3828 chore: config port change 2024-04-14 00:12:47 +02:00
Spythere e72b73ccf0 fix: global styles fix 2024-04-14 00:09:52 +02:00
Spythere 75e34d9f75 chore(wiki): change table to grid layout - wip 2024-04-13 19:26:52 +02:00
Spythere 13aa1acc54 Merge pull request #24 from Spythere/development
Wersja 1.8.4
2024-04-10 16:32:03 +02:00
Spythere 966181c977 fix: wiki list table height 2024-04-10 16:25:11 +02:00
Spythere 06eb4bc607 fix: button bg color 2024-04-10 16:18:32 +02:00
Spythere c07a16d245 fix: main container responsiveness 2024-04-10 16:12:08 +02:00
Spythere 7e67e34526 chore: wiki list tab sizing 2024-04-10 15:35:51 +02:00
Spythere 6cfc535dec chore: packages update 2024-04-09 16:32:23 +02:00
Spythere d072692db7 chore: pwa vehicles cache 2024-04-09 14:47:56 +02:00
Spythere 6255efd98d fix: http url 2024-04-08 23:44:37 +02:00
Spythere 9c59c30f12 chore(api): url changes 2024-04-08 23:40:24 +02:00
Spythere 31302cc053 chorse: changed api data to stored locally 2024-04-08 22:20:18 +02:00
Spythere 26fd0c67e4 feat: team & sponsor restrictions added 2024-04-07 22:00:09 +02:00
Spythere d98ec94a66 chore: service worker changes, got rid of excessive console logs 2024-04-07 18:54:03 +02:00
Spythere 28485cc28c real stock card exit btn; hotfixes 2024-04-06 16:19:04 +02:00
Spythere b14c7a9502 cargo count sorting fix 2024-04-06 16:08:05 +02:00
Spythere 90e78e5ac5 wiki list lazy load (intersection obs.) 2024-04-06 15:45:39 +02:00
Spythere 27f02e2c2b api & naming changes 2024-04-06 14:54:54 +02:00
Spythere 421ae1db6b Merge pull request #23 from Spythere/development
Wersja 1.8.3.1
2024-04-03 21:53:48 +02:00
Spythere 90824dc0e5 hotfix 2024-04-03 21:51:24 +02:00
Spythere 583c2887e9 Merge pull request #22 from Spythere/development
Wersja 1.8.3
2024-04-03 21:47:55 +02:00
Spythere f915094775 speed disclaimer 2024-04-03 21:43:37 +02:00
Spythere d5e735b59e caching 2024-03-27 16:21:56 +01:00
Spythere 43a724bf13 cleanup 2024-03-27 16:13:33 +01:00
Spythere 337425d21c eslint & prettier update; api fetching from static server 2024-03-27 16:10:53 +01:00
Spythere 9ff9341851 Merge pull request #21 from Spythere/development
Wersja 1.8.2
2024-03-08 16:55:16 +01:00
Spythere f9276f6c71 bump: 1.8.2 2024-03-08 16:54:30 +01:00
Spythere f4f9a4729f poprawki cachowania 2024-03-08 16:54:08 +01:00
Spythere ed0906b63e Merge pull request #20 from Spythere/development
Wersja 1.8.1
2024-03-07 14:22:45 +01:00
Spythere 2ae19123a3 bump: 1.8.1 2024-03-07 14:19:23 +01:00
Spythere 0c2be7b927 speed limits fix 2024-03-07 14:19:04 +01:00
Spythere ffce2b572b Wersja 1.7.4
Wersja 1.7.4
2024-02-20 13:26:22 +01:00
Spythere 1b5a26e380 speed constraints for locomotives-only 2024-02-20 12:14:09 +01:00
Spythere c9b681eaee bump: 1.8.0 2024-02-20 11:49:52 +01:00
Spythere 160879adec hotfix 2024-02-19 21:25:21 +01:00
Spythere d6ddbe7af7 translation fixes 2024-02-19 17:42:41 +01:00
Spythere 1c7b72ea1d removed new categories; awaiting changes in the simulator 2024-02-19 17:41:16 +01:00
Spythere 83f5b07c7e code adaptation for API changes; tons changed to kilos 2024-02-19 17:37:46 +01:00
Spythere 77cb64e25a num. generator: categories update 2024-02-15 23:09:21 +01:00
Spythere 6d0cc8e7cd wiki tab td padding 2024-02-14 15:06:45 +01:00
Spythere 7fb4b0ae26 fix: incorrect wagon count while generating a r.stock 2024-02-14 15:04:23 +01:00
Spythere ed191d597b translations; numgen category drawing 2024-02-05 22:36:36 +01:00
Spythere 5d24accb15 forum link update 2024-02-03 18:19:43 +01:00
Spythere 54c6850121 translation & num gen fixes 2024-02-03 18:17:01 +01:00
Spythere fad8c40b4b translation fixes 2024-02-03 16:41:07 +01:00
Spythere a8485b3531 lock sync hotfix 2024-02-03 16:31:04 +01:00
Spythere e7dcd125ec packages upgrade 2024-02-03 16:26:42 +01:00
Spythere d886f44c59 lock files 2024-02-03 16:18:13 +01:00
Spythere 5ff6c0504e bump to 1.7.4; added new categories to number gen & showing rules 2024-02-03 16:15:22 +01:00
Spythere 2158145ae8 design update & fixes 2024-02-01 16:12:41 +01:00
Spythere 6101c9bfb2 grouping generated stock (wip) 2024-01-30 16:39:16 +01:00
Spythere bd9af9a630 Merge pull request #18 from Spythere/development
Wersja 1.7.3
2024-01-18 17:57:01 +01:00
Spythere 2188dbdf9b bump: 1.7.3 2024-01-18 16:37:15 +01:00
Spythere c4576d7802 hotfix 4E 2024-01-18 16:36:57 +01:00
Spythere 86cb4cff4d Merge pull request #17 - Wersja 1.7.2
Wersja 1.7.2
2023-11-07 17:43:34 +01:00
Spythere 0534848677 bump: 1.7.2 2023-11-06 21:20:22 +01:00
Spythere c5d116e2db added alphabetical cargo sort 2023-11-06 21:20:07 +01:00
Spythere 3dce5ec7c0 responsive font size 2023-11-06 21:08:01 +01:00
Spythere 956ff8afd7 stock thumbnail labels fix 2023-11-06 20:20:00 +01:00
Spythere 5b629833df Merge pull request #16 - Wersja 1.7.1
Wersja 1.7.1
2023-10-31 23:05:43 +01:00
Spythere b53201a7ff hotfix 2023-10-31 23:02:58 +01:00
Spythere f0ddd0e27f custom checkboxes 2023-10-31 03:17:24 +01:00
Spythere 94bfba2c49 resolve fonts edit 2023-10-31 03:17:11 +01:00
Spythere 1e4541ef0d spawn props checking on stock upload 2023-10-28 15:40:22 +02:00
Spythere c25a55a7d9 added double manning checkbox & support 2023-10-28 15:33:09 +02:00
Spythere bea3c59405 thumbnail placeholder fix 2023-10-28 14:12:21 +02:00
Spythere 8cd43adff3 acceptable masses & speeds update 2023-10-28 13:44:55 +02:00
Spythere 5ab1963117 lock files sync 2023-10-27 16:46:57 +02:00
Spythere 50d784a0de 1.7.1 bump 2023-10-27 16:43:39 +02:00
Spythere f61135ce6b packages bump; config 2023-10-27 16:39:54 +02:00
Spythere 5e43ece1aa pwa optimize & fixes 2023-10-27 16:36:25 +02:00
Spythere 37e4149a34 Merge pull request #15 from Spythere/development
Wersja 1.7.0
2023-10-26 21:43:46 +02:00
Spythere e515203557 stockObject & static handling hotfixes 2023-10-26 21:37:33 +02:00
Spythere 0d79c71eba 1.7.0 bump 2023-10-26 21:36:00 +02:00
Spythere 2bbf9a8ac3 section improvements; hotfixes 2023-10-26 21:35:42 +02:00
Spythere 45b2bd01a2 enhanced wiki list 2023-10-26 00:55:13 +02:00
Spythere 665ffb9dce lock: hotfix; bump 1.6.1 2023-10-24 23:31:45 +02:00
Spythere 1c2a93fbd5 format; linting; aktualizacja do 2023.2.1 2023-10-24 23:28:42 +02:00
Spythere 57ab6cc02d gitignore 2023-08-25 20:29:17 +02:00
Spythere aa62240d27 Merge do wersji 1.6.0 2023-08-24 01:29:45 +02:00
Spythere a699c9851f tłumaczenia; fix losowania generatora numeru 2023-08-24 00:01:19 +02:00
Spythere d7c1f8c5b4 fix: tłumaczenia alertów 2023-08-23 23:04:04 +02:00
Spythere c1c01c496a poprawki w widoku składu; tłumaczenia 2023-08-23 22:54:17 +02:00
Spythere a7f92d3ec5 feature: zapamiętywanie języka 2023-08-22 18:34:15 +02:00
Spythere ae7be6d6f8 tłumaczenie PL/EN (cz.3) 2023-08-22 18:30:04 +02:00
Spythere bf204d5e36 hotfix: pliki lock 2023-08-22 00:55:17 +02:00
Spythere 459876f5b4 bump: 1.6.0 2023-08-22 00:53:02 +02:00
Spythere 4a1a840c16 cleanup i18n; wykrywanie języka 2023-08-22 00:52:51 +02:00
Spythere c0552e890c hotfix: synchronizacja plików lock 2023-08-22 00:36:31 +02:00
Spythere 80d6f2b85f tłumaczenie PL/EN (wip, cz.2) 2023-08-22 00:33:51 +02:00
Spythere 17882e3e6e feature: tłumaczenie PL/EN (wip) 2023-08-21 03:19:37 +02:00
Spythere 4a03535b07 feature: ikony akcji 2023-08-19 13:13:00 +02:00
90 changed files with 11211 additions and 13883 deletions
+13
View File
@@ -0,0 +1,13 @@
/* eslint-env node */
require('@rushstack/eslint-patch/modern-module-resolution');
module.exports = {
root: true,
extends: ['plugin:vue/vue3-essential', 'eslint:recommended', '@vue/eslint-config-typescript', '@vue/eslint-config-prettier/skip-formatting'],
rules: {
'vue/multi-word-component-names': 'off',
},
parserOptions: {
ecmaVersion: 'latest',
},
};
-20
View File
@@ -1,20 +0,0 @@
module.exports = {
root: true,
env: {
node: true,
},
extends: [
"plugin:vue/vue3-essential",
"eslint:recommended",
"@vue/typescript/recommended",
"@vue/prettier",
"@vue/prettier/@typescript-eslint",
],
parserOptions: {
ecmaVersion: 2020,
},
rules: {
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
},
};
@@ -0,0 +1,17 @@
on:
release:
types: [published]
jobs:
github-releases-to-discord:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Github Releases To Discord
uses: SethCohen/github-releases-to-discord@v1.13.1
with:
webhook_url: ${{ secrets.WEBHOOK_URL }}
color: "15844367"
footer_title: "Changelog - Pojazdownik"
footer_timestamp: true
+4 -1
View File
@@ -25,4 +25,7 @@ pnpm-debug.log*
node_modules
# Dev files
stockInfoDev.json
stockInfoDev.json
# Lock files
yarn.lock
+6
View File
@@ -0,0 +1,6 @@
{
"semi": true,
"singleQuote": true,
"trailingComma": "es5",
"printWidth": 100
}
-3
View File
@@ -1,3 +0,0 @@
module.exports = {
presets: ["@vue/cli-plugin-babel/preset"],
};
+2 -2
View File
@@ -1,7 +1,7 @@
{
"hosting": {
"public": "dist",
"ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
"ignore": [],
"rewrites": [
{
"source": "**",
@@ -9,4 +9,4 @@
}
]
}
}
}
+8 -10
View File
@@ -1,4 +1,4 @@
<!DOCTYPE html>
<!doctype html>
<html>
<head>
<meta charset="utf-8" />
@@ -8,22 +8,20 @@
<title>Pojazdownik</title>
<meta name="description" content="Edytor pociągów online do symulatora Train Driver 2" />
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
<link rel="manifest" href="/site.webmanifest" />
<link rel="icon" href="/favicon.ico" sizes="any" />
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
<link rel="apple-touch-icon" href="/apple-touch-icon-180x180.png" />
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#5bbad5" />
<link rel="manifest" href="/site.webmanifest" />
<meta name="msapplication-TileColor" content="#da532c" />
<meta name="theme-color" content="#e4c428" />
<meta name="theme-color" content="#111" />
</head>
<body>
<noscript>
<strong>
We're sorry but Pojazdownik doesn't work properly without JavaScript enabled. Please enable it to continue.
</strong>
<strong> We're sorry but Pojazdownik doesn't work properly without JavaScript enabled. Please enable it to continue. </strong>
</noscript>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
+8043 -8878
View File
File diff suppressed because it is too large Load Diff
+20 -6
View File
@@ -1,22 +1,36 @@
{
"name": "pojazdownik",
"version": "1.5.0",
"version": "1.8.7",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vue-tsc --noEmit && vite build",
"preview": "yarn build && vite preview --port 4174"
"preview": "yarn build && vite preview --port 4174",
"type-check": "vue-tsc --noEmit -p tsconfig.app.json --composite false",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
"format": "prettier --write src/"
},
"dependencies": {
"axios": "^1.4.0",
"pinia": "^2.0.17",
"vue": "^3.2.37"
"prettier": "^3.0.3",
"vue": "^3.2.37",
"vue-i18n": "9.11.0"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.1.0",
"@rushstack/eslint-patch": "^1.3.3",
"@vite-pwa/assets-generator": "^0.2.3",
"@vitejs/plugin-vue": "^5.0.3",
"@vue/eslint-config-prettier": "^9.0.0",
"@vue/eslint-config-typescript": "^12.0.0",
"@vue/tsconfig": "^0.5.1",
"eslint": "^8.49.0",
"eslint-plugin-vue": "^9.17.0",
"sass": "^1.59.3",
"typescript": "^5.0.2",
"vite": "^4.2.1",
"vite-plugin-pwa": "^0.14.6",
"vite": "^5.0.12",
"vite-plugin-pwa": "^0.17.5",
"vue-tsc": "^1.2.0"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

+24
View File
@@ -0,0 +1,24 @@
<svg width="500" height="500" viewBox="0 0 500 500" fill="none" xmlns="http://www.w3.org/2000/svg">
<mask id="path-1-inside-1_102_63" fill="white">
<path d="M0 250C0 111.929 111.929 6.10352e-05 250 6.10352e-05C388.071 6.10352e-05 500 111.929 500 250C500 388.071 388.071 500 250 500C111.929 500 0 388.071 0 250Z"/>
</mask>
<path d="M0 250C0 111.929 111.929 6.10352e-05 250 6.10352e-05C388.071 6.10352e-05 500 111.929 500 250C500 388.071 388.071 500 250 500C111.929 500 0 388.071 0 250Z" fill="#242424"/>
<path d="M0 222.821C0 84.7503 111.929 -27.1785 250 -27.1785C388.071 -27.1785 500 84.7503 500 222.821V250C500 126.939 388.071 27.1787 250 27.1787C111.929 27.1787 0 126.939 0 250V222.821ZM500 277.179C500 415.25 388.071 527.179 250 527.179C111.929 527.179 0 415.25 0 277.179V250C0 373.061 111.929 472.821 250 472.821C388.071 472.821 500 373.061 500 250V277.179ZM0 500V6.10352e-05V500ZM500 6.10352e-05V500V6.10352e-05Z" fill="#FFD600" mask="url(#path-1-inside-1_102_63)"/>
<path d="M210.369 301.604C210.369 301.604 210.369 341.807 210.369 364.846C210.369 387.885 202.798 417.491 171.591 417.491C140.385 417.491 132.813 417.491 132.813 417.491L132.812 78.125L250.754 78.125C274.312 78.125 294.504 80.9665 311.331 86.6494C328.311 92.1788 342.232 99.8585 353.093 109.689C364.107 119.519 372.214 131.115 377.415 144.478C382.616 157.84 385.217 172.278 385.217 187.791C385.217 204.533 382.54 219.892 377.186 233.869C371.832 247.846 363.648 259.827 352.634 269.81C341.62 279.794 327.623 287.627 310.643 293.31C293.816 298.839 273.853 301.604 250.754 301.604L210.369 301.604ZM210.369 242.854L250.754 242.854C270.946 242.854 285.479 238.016 294.351 228.34C303.224 218.663 307.66 205.147 307.66 187.791C307.66 180.111 306.512 173.123 304.218 166.825C301.923 160.528 298.405 155.152 293.663 150.698C289.074 146.09 283.184 142.558 275.995 140.1C268.958 137.643 260.544 136.414 250.754 136.414L210.369 136.414L210.369 242.854Z" fill="url(#paint0_linear_102_63)"/>
<path d="M239.215 301.604C239.215 301.604 239.215 341.807 239.215 364.846C239.215 387.885 231.643 417.491 200.437 417.491C169.231 417.491 161.659 417.491 161.659 417.491L161.658 78.125L279.6 78.125C303.158 78.125 323.35 80.9665 340.177 86.6494C357.157 92.1788 371.077 99.8585 381.938 109.689C392.952 119.519 401.06 131.115 406.261 144.478C411.462 157.84 414.062 172.278 414.062 187.791C414.062 204.533 411.385 219.892 406.031 233.869C400.677 247.846 392.493 259.827 381.479 269.81C370.465 279.794 356.468 287.627 339.488 293.31C322.662 298.839 302.699 301.604 279.6 301.604L239.215 301.604ZM239.215 242.854L279.6 242.854C299.792 242.854 314.325 238.016 323.197 228.34C332.069 218.663 336.505 205.147 336.505 187.791C336.505 180.111 335.358 173.123 333.064 166.825C330.769 160.528 327.251 155.152 322.509 150.698C317.919 146.09 312.03 142.558 304.84 140.1C297.804 137.643 289.39 136.414 279.6 136.414L239.215 136.414L239.215 242.854Z" fill="url(#paint1_linear_102_63)"/>
<path d="M210.685 301.604C210.685 301.604 210.685 341.807 210.685 364.846C210.685 387.885 203.082 417.491 171.749 417.491C140.416 417.491 132.813 417.491 132.813 417.491L132.812 78.125L251.233 78.125C274.887 78.125 295.161 80.9665 312.057 86.6494C329.105 92.1788 343.083 99.8585 353.988 109.689C365.046 119.519 373.187 131.115 378.409 144.478C383.631 157.84 386.242 172.278 386.242 187.791C386.242 204.533 383.555 219.892 378.179 233.869C372.803 247.846 364.586 259.827 353.527 269.81C342.468 279.794 328.414 287.627 311.365 293.31C294.47 298.839 274.426 301.604 251.233 301.604L210.685 301.604ZM210.685 242.854L251.233 242.854C271.508 242.854 286.099 238.016 295.008 228.34C303.916 218.663 308.37 205.147 308.37 187.791C308.37 180.111 307.218 173.123 304.914 166.825C302.611 160.528 299.078 155.152 294.316 150.698C289.709 146.09 283.795 142.558 276.576 140.1C269.511 137.643 261.063 136.414 251.233 136.414L210.685 136.414L210.685 242.854Z" fill="url(#paint2_radial_102_63)"/>
<defs>
<linearGradient id="paint0_linear_102_63" x1="259.015" y1="78.125" x2="259.015" y2="417.491" gradientUnits="userSpaceOnUse">
<stop offset="0.135417" stop-color="#FFD600"/>
<stop offset="1"/>
</linearGradient>
<linearGradient id="paint1_linear_102_63" x1="287.86" y1="78.125" x2="287.86" y2="417.491" gradientUnits="userSpaceOnUse">
<stop offset="0.135417" stop-color="#FFD600"/>
<stop offset="1"/>
</linearGradient>
<radialGradient id="paint2_radial_102_63" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(259.527 247.808) rotate(0.36307) scale(345.948 325.206)">
<stop offset="0.484375" stop-color="white"/>
<stop offset="1"/>
</radialGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" fill="white" width="48"><path d="M314.696-195.478q-32.507 0-55.862-23.356-23.356-23.355-23.356-55.862v-549.826q0-32.74 23.356-56.262 23.355-23.522 55.862-23.522h429.826q32.74 0 56.262 23.522t23.522 56.262v549.826q0 32.507-23.522 55.862-23.522 23.356-56.262 23.356H314.696Zm0-79.218h429.826v-549.826H314.696v549.826ZM175.478-55.694q-32.74 0-56.262-23.522t-23.522-56.262v-629.044h79.784v629.044h509.044v79.784H175.478Zm139.218-219.002v-549.826 549.826Z"/></svg>

After

Width:  |  Height:  |  Size: 536 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" fill="white" width="48"><path d="M480-318.087 266.651-531.436l57.131-56.001 116.609 116.609v-343.868h79.218v343.868l116.609-116.609 57.131 56.001L480-318.087ZM225.087-145.869q-32.507 0-55.862-23.356-23.356-23.355-23.356-55.862v-143h79.218v143h509.826v-143h79.783v143q0 32.478-23.521 55.848-23.522 23.37-56.262 23.37H225.087Z"/></svg>

After

Width:  |  Height:  |  Size: 410 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="white" width="18px" height="18px"><path d="M0 0h24v24H0z" fill="none"/><path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>

After

Width:  |  Height:  |  Size: 256 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" fill="white" width="48"><path d="M451-107.304q-128.652-10.565-216.892-105.522-88.239-94.956-88.239-225.609 0-78.13 35.5-147.543 35.5-69.413 99.5-114.674l56.566 56.565q-52.609 32.435-82.478 87.957-29.87 55.521-29.87 117.695 0 98.305 64.587 169.609T451-187.087v79.783Zm60 0v-79.783q98.304-11.435 161.609-82.239 63.304-70.804 63.304-169.109 0-103.913-70.978-177.434-70.978-73.522-174.891-76.913h-21.696l62.261 62.826-47.522 47.522-152.783-152.784 152.783-153.349 47.522 47.522-68.479 68.479h22.261q138.522 0 234.914 98.022 96.391 98.022 96.391 236.109 0 130.653-88.022 225.609Q639.652-117.869 511-107.304Z"/></svg>

After

Width:  |  Height:  |  Size: 688 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" fill="white" width="48"><path d="M575.913-143.782v-72.436h115.956l-173.26-172.825 51.043-51.479 175.261 172.695v-117.391h73v241.436h-242Zm-382.783 2.826L142.652-193l551.217-551.782H575.913v-72.436h242v241.436h-73v-116.391L193.13-140.956Zm198.479-378.522L142.652-768l51.478-52.044 250.088 248.522-52.609 52.044Z"/></svg>

After

Width:  |  Height:  |  Size: 396 B

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="48" viewBox="0 -960 960 960" fill="white" width="48"><path d="M440.391-318.087v-343.868L323.782-544.782l-57.131-56.566L480-814.696l213.349 213.348-57.131 56.566-116.609-117.173v343.868h-79.218ZM225.087-145.869q-32.507 0-55.862-23.356-23.356-23.355-23.356-55.862v-143h79.218v143h509.826v-143h79.783v143q0 32.478-23.521 55.848-23.522 23.37-56.262 23.37H225.087Z"/></svg>

After

Width:  |  Height:  |  Size: 416 B

Before

Width:  |  Height:  |  Size: 1020 B

After

Width:  |  Height:  |  Size: 1020 B

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Before

Width:  |  Height:  |  Size: 932 B

After

Width:  |  Height:  |  Size: 932 B

Before

Width:  |  Height:  |  Size: 953 B

After

Width:  |  Height:  |  Size: 953 B

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 25 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 25 KiB

+14 -3
View File
@@ -5,15 +5,26 @@
"description": "Generator składów online dla symulatora Train Driver 2",
"icons": [
{
"src": "/android-chrome-192x192.png",
"src": "pwa-64x64.png",
"sizes": "64x64",
"type": "image/png"
},
{
"src": "pwa-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/android-chrome-512x512.png",
"src": "pwa-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
"purpose": "any"
},
{
"src": "maskable-icon-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
],
"theme_color": "#2c3149",
Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

+14 -4
View File
@@ -5,19 +5,29 @@
"description": "Generator składów online dla symulatora Train Driver 2",
"icons": [
{
"src": "/android-chrome-192x192.png",
"src": "pwa-64x64.png",
"sizes": "64x64",
"type": "image/png"
},
{
"src": "pwa-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/android-chrome-512x512.png",
"src": "pwa-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "any maskable"
"purpose": "any"
},
{
"src": "maskable-icon-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
],
"theme_color": "#2c3149",
"background_color": "#2c3149",
"display": "standalone"
}
+9 -30
View File
@@ -12,21 +12,17 @@ import AppContainerView from './views/AppContainerView.vue';
import AppModals from './components/app/AppModals.vue';
export default defineComponent({
components: { ImageFullscreenPreview, AppContainerView, AppModals },
data() {
return {
store: useStore(),
};
},
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();
this.store.setupAPIData();
},
components: { ImageFullscreenPreview, AppContainerView, AppModals },
});
</script>
@@ -39,31 +35,14 @@ export default defineComponent({
color: $textColor;
font-size: 1em;
padding: 1em 0.5em;
}
padding: 0;
/* HEADER SECTION */
h2 {
margin: 0;
margin-bottom: 0.5em;
color: $accentColor;
font-weight: 700;
font-size: 1.2em;
}
.header-bottom {
margin: 0;
font-size: 1.5em;
color: #d1d1d1;
}
@media screen and (max-width: $breakpointMd) {
#app {
@media screen and (max-width: $breakpointMd) {
font-size: calc(0.7rem + 0.75vw);
}
@media screen and (orientation: landscape) and (max-width: $breakpointMd) {
font-size: calc(0.75rem + 0.4vw);
}
}
</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>
+44 -52
View File
@@ -1,52 +1,44 @@
<template>
<footer>
<div class="text--grayed">
Ta strona ma charakter informacyjny. Autor nie ponosi odpowiedzialności za tworzenie pociągów niezgodnych z
<a
style="color: #ccc"
href="https://docs.google.com/document/d/1UAAPUtN0d_RoS4RgOzEzllJZJhA0VcizzCzKW4QylbY/edit"
target="_blank"
>
regulaminem symulatora Train Driver 2</a
>!
</div>
<div class="text--grayed" v-if="store.stockData">
Strona jest kompletna dla wersji {{ store.stockData.version }} symulatora TD2
</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.vehiclesData">
{{ $t('footer.version-check', { version: store.vehiclesData.simulatorVersion }) }}
</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 {
text-align: center;
padding: 1em 1em 0 1em;
}
</style>
+50 -45
View File
@@ -1,45 +1,50 @@
<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: 1350px;
grid-template-columns: 1fr 2fr;
grid-template-rows: auto 360px minmax(300px, 1fr);
background-color: darken($color: $bgColor, $amount: 5);
border-radius: 1em;
min-height: 950px;
padding: 1em;
}
@media screen and (max-width: $breakpointMd) {
main {
display: flex;
flex-direction: column;
gap: 1em;
}
}
</style>
+71 -72
View File
@@ -6,46 +6,73 @@
<div class="card_nav">
<div class="top-pane">
<h1>
ZESTAWIENIA REALNE by <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 action-exit" @click="store.isRealStockListCardOpen = false">
<img src="/images/icon-exit.svg" alt="" />
</button>
</div>
<div class="filters" ref="focus" tabindex="0">
<input list="readyStockDataList" v-model="searchedReadyStockName" placeholder="Szukaj po nazwie" />
<input
list="readyStockDataList"
v-model="searchedReadyStockName"
:placeholder="$t('realstock.search-name')"
/>
<datalist id="readyStockDataList">
<option v-for="stock in store.readyStockList" :value="stock.stockId">
<option
v-for="stock in store.realCompositionList"
:value="stock.stockId"
:key="stock.name"
>
{{ stock.stockId }}
</option>
</datalist>
<input list="readyStockStringList" v-model="searchedReadyStockString" placeholder="Szukaj po pojazdach" />
<input
list="readyStockStringList"
v-model="searchedReadyStockString"
:placeholder="$t('realstock.search-stock')"
/>
<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">RESETUJ</button>
<button class="btn action-reset" @click="resetStockFilters">
{{ $t('realstock.action-reset') }}
</button>
</div>
</div>
<ul class="card_list" ref="list" @scroll="onListScroll">
<li
v-for="rStock in computedReadyStockList"
:key="rStock.stockId"
:data-last-selected="store.chosenRealStockName === rStock.stockId"
>
<div class="stock-title" tabindex="0" @click="chooseStock(rStock)" @keydown.enter="chooseStock(rStock)">
<li v-for="rStock in computedReadyStockList" :key="rStock.stockId">
<!-- :data-last-selected="store.ch === 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>{{ 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
@@ -53,7 +80,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>
@@ -73,17 +100,13 @@ import { useStore } from '../../store';
import imageMixin from '../../mixins/imageMixin';
import stockMixin from '../../mixins/stockMixin';
import { IReadyStockItem } from '../../types';
import { IRealComposition, VehicleGroupType } from '../../types';
interface ResponseJSONData {
[key: string]: string;
}
function getVehicleType(stockType: string): VehicleGroupType {
if (/^E/.test(stockType)) return 'loco-electric';
if (/^S/.test(stockType)) return 'loco-diesel';
function getVehicleType(stockType: string) {
if (/^E/.test(stockType)) return 'loco-e';
if (/^S/.test(stockType)) return 'loco-s';
return 'car-passenger';
return 'wagon-passenger';
}
export default defineComponent({
@@ -92,7 +115,10 @@ export default defineComponent({
data: () => ({
store: useStore(),
responseStatus: 'loading',
isMobile: 'ontouchstart' in document.documentElement && navigator.userAgent.match(/Mobi/) ? true : false,
isMobile:
'ontouchstart' in document.documentElement && navigator.userAgent.match(/Mobi/)
? true
: false,
observer: null as IntersectionObserver | null,
searchedReadyStockName: '',
searchedReadyStockString: '',
@@ -101,9 +127,8 @@ export default defineComponent({
scrollTop: 0,
}),
async mounted() {
mounted() {
this.mountObserver();
this.fetchStockListData();
},
activated() {
@@ -116,20 +141,22 @@ export default defineComponent({
},
computed: {
computedReadyStockList() {
if (this.searchedReadyStockName == null) return this.store.readyStockList;
return this.store.readyStockList
computedReadyStockList(): IRealComposition[] {
return this.store.realCompositionList
.filter(
(rs) =>
rs.stockId.toLocaleLowerCase().includes(this.searchedReadyStockName.toLocaleLowerCase()) &&
rs.stockString.toLocaleLowerCase().includes(this.searchedReadyStockString.toLocaleLowerCase())
(rc) =>
rc.stockId
.toLocaleLowerCase()
.includes(this.searchedReadyStockName.toLocaleLowerCase()) &&
rc.stockString
.toLocaleLowerCase()
.includes(this.searchedReadyStockString.toLocaleLowerCase())
)
.filter((_, i) => i <= this.visibleIndexesTo);
},
computedAvailableStockTypes() {
return this.store.readyStockList
return this.store.realCompositionList
.reduce((acc, rs) => {
rs.stockString.split(';').forEach((s) => {
if (!acc.includes(s)) acc.push(s);
@@ -153,35 +180,6 @@ 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();
if (!readyStockJSONData) {
this.responseStatus = 'error';
return;
}
for (let stockKey in readyStockJSONData) {
const [type, number, ...name] = stockKey.split(' ');
const obj = {
number: number.replace(/_/g, '/'),
name: name.join(' '),
stockString: readyStockJSONData[stockKey],
type,
};
this.store.readyStockList.push({
...obj,
stockId: `${obj.type} ${obj.number} ${obj.name}`,
});
}
this.responseStatus = 'loaded';
},
mountObserver() {
this.observer = new IntersectionObserver((entries) => {
if (entries[0].intersectionRatio > 0) this.visibleIndexesTo += 20;
@@ -199,7 +197,7 @@ export default defineComponent({
this.searchedReadyStockString = '';
},
chooseStock(stockItem: IReadyStockItem) {
chooseStock(stockItem: IRealComposition) {
this.loadStockFromString(stockItem.stockString);
this.lastSelectedStockId = stockItem.stockId;
this.store.isRealStockListCardOpen = false;
@@ -224,13 +222,15 @@ export default defineComponent({
<style lang="scss" scoped>
@import '../../styles/global.scss';
.exit-btn {
font-size: 1.2em;
margin: 0.25em 0;
.action-exit {
display: flex;
background-color: #333;
border-radius: 0.25em;
padding: 0.5em;
}
.btn {
background-color: #444;
.action-reset {
background-color: #333;
}
.card_content {
@@ -370,4 +370,3 @@ ul {
padding: 1em;
}
</style>
+72
View File
@@ -0,0 +1,72 @@
<template>
<label>
<input type="checkbox" :data-disabled="disabled" :disabled="disabled" v-model="model" />
<div><slot /></div>
</label>
</template>
<script lang="ts" setup>
const model = defineModel();
defineProps({
disabled: Boolean,
});
</script>
<style lang="scss" scoped>
label {
text-transform: uppercase;
transition: color 200ms;
}
div {
padding: 0.25em 0.5em;
color: white;
cursor: pointer;
background-color: #222;
border-radius: 0.25em;
user-select: none;
&::before {
content: '\2716';
margin-right: 0.5em;
color: #aaa;
}
}
input {
position: absolute;
clip: rect(1px, 1px, 1px, 1px);
padding: 0;
border: 0;
height: 1px;
width: 1px;
overflow: hidden;
&:focus-visible + div {
outline: 1px solid white;
}
&:checked + div {
color: palegreen;
&::before {
color: palegreen;
content: '\2714';
}
}
&:disabled {
user-select: none;
-moz-user-select: none;
-webkit-user-select: none;
+ div {
opacity: 0.55;
cursor: auto;
}
}
}
</style>
+93 -48
View File
@@ -1,17 +1,18 @@
<template>
<section class="inputs-section">
<div class="input_container">
<h2 class="input_header">WYBIERZ POJAZDY / WAGONY</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"
:data-selected="locoType.id == store.chosenLocoGroup"
@click="selectLocoType(locoType.id)"
>
{{ locoType.value }}
{{ $t(`inputs.${locoType.id}`) }}
</button>
</div>
@@ -23,9 +24,12 @@
@keydown.enter.prevent="addOrSwitchVehicle"
@keydown.backspace="removeVehicle"
>
<option :value="null" disabled>Wybierz pojazd trakcyjny</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>
{{ loco.type
}}<b v-if="loco.sponsorOnlyTimestamp && loco.sponsorOnlyTimestamp > Date.now()">*</b>
</option>
</select>
</div>
@@ -34,11 +38,12 @@
<div class="vehicle-types carwagons">
<button
v-for="carType in carTypeList"
:key="carType.id"
class="btn btn--choice"
:data-selected="carType.id == store.chosenCarUseType"
:data-selected="carType.id == store.chosenCarGroup"
@click="selectCarWagonType(carType.id)"
>
{{ carType.value }}
{{ $t(`inputs.${carType.id}`) }}
</button>
</div>
@@ -50,21 +55,24 @@
@keydown.enter.prevent="addOrSwitchVehicle"
@keydown.backspace="removeVehicle"
>
<option :value="null" disabled>Wybierz wagon</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>
{{ car.type
}}<b v-if="car.sponsorOnlyTimestamp && car.sponsorOnlyTimestamp > Date.now()">*</b>
</option>
</select>
</div>
<div class="input_list cargo">
<label for="cargo-select">Ładunek (tylko wybrane towarowe)</label>
<label for="cargo-select">{{ $t('inputs.cargo-title') }}</label>
<select
id="cargo-select"
:disabled="
(store.chosenCar && !store.chosenCar.loadable) ||
(store.chosenCar && store.chosenCar.useType == 'car-passenger') ||
(store.chosenCar && store.chosenCar.group == 'wagon-passenger') ||
!store.chosenCar
"
data-select="cargo"
@@ -75,30 +83,37 @@
@keydown.enter.prevent="addOrSwitchVehicle"
@keydown.backspace="removeVehicle"
>
<option :value="null" v-if="!store.chosenCar || !store.chosenCar.loadable">brak dostępnych ładunków</option>
<option :value="null" v-else>próżny</option>
<option :value="null" v-if="!store.chosenCar || !store.chosenCar.loadable">
{{ $t('inputs.no-cargo-available') }}
</option>
<option v-for="cargo in store.chosenCar?.cargoList" :value="cargo" :key="cargo.id">
<option :value="null" v-else>{{ $t('inputs.cargo-empty') }}</option>
<option v-for="cargo in store.chosenCar?.cargoTypes" :value="cargo" :key="cargo.id">
{{ cargo.id }}
</option>
</select>
</div>
<div class="input_actions">
<button class="btn" @click="addVehicle(store.chosenVehicle, store.chosenCargo)">DODAJ NOWY</button>
<button class="btn" @click="addVehicle(store.chosenVehicle, store.chosenCargo)">
{{ $t('inputs.action-add') }}
</button>
<button
class="btn"
@click="switchVehicles"
:disabled="store.chosenStockListIndex == -1"
:data-disabled="store.chosenStockListIndex == -1"
>
ZAMIEŃ ZA
{{ $t('inputs.action-swap') }}
<b class="text--accent">
{{ store.chosenStockListIndex == -1 ? '' : `${store.chosenStockListIndex + 1}.` }}
</b>
</button>
<button class="btn" @click="store.isRealStockListCardOpen = true"><b>REALNE ZESTAWIENIA</b></button>
<button class="btn" @click="store.isRealStockListCardOpen = true">
<b>{{ $t('inputs.real-stock') }}</b>
</button>
</div>
</div>
</section>
@@ -111,60 +126,58 @@ import imageMixin from '../../mixins/imageMixin';
import { useStore } from '../../store';
import stockPreviewMixin from '../../mixins/stockPreviewMixin';
import stockMixin from '../../mixins/stockMixin';
interface ILocoType {
id: string;
value: string;
desc: string;
}
import { LocoGroupType, WagonGroupType } from '../../types';
export default defineComponent({
mixins: [imageMixin, stockPreviewMixin, stockMixin],
data: () => ({
store: useStore(),
locomotiveTypeList: [
{
id: 'loco-e',
value: 'ELEKTR',
id: 'loco-electric',
desc: 'ELEKTRYCZNE',
},
{
id: 'loco-s',
value: 'SPAL',
id: 'loco-diesel',
desc: 'SPALINOWE',
},
{
id: 'loco-ezt',
value: 'EZT',
id: 'unit-electric',
desc: 'ELEKTR. ZESPOŁY TRAKCYJNE',
},
{
id: 'loco-szt',
value: 'SZT',
id: 'unit-diesel',
desc: 'SPAL. ZESPOŁY TRAKCYJNE',
},
] as ILocoType[],
] as { id: LocoGroupType; desc: string }[],
carTypeList: [
{
id: 'car-passenger',
value: 'PAS',
id: 'wagon-passenger',
desc: 'PASAŻERSKIE',
},
{
id: 'car-cargo',
value: 'TOW',
id: 'wagon-freight',
desc: 'TOWAROWE',
},
],
] as { id: WagonGroupType; desc: string }[],
}),
setup() {
const store = useStore();
computed: {
locoOptions() {
return this.store.locoDataList
.slice()
.sort((a, b) => (a.type > b.type ? 1 : -1))
.filter((loco) => loco.group == this.store.chosenLocoGroup);
},
return {
store,
};
carOptions() {
return this.store.carDataList
.slice()
.sort((a, b) => (a.type > b.type ? 1 : -1))
.filter((car) => car.group == this.store.chosenCarGroup);
},
},
methods: {
@@ -175,17 +188,15 @@ 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();
},
removeVehicle() {
if (this.store.stockList.length == 0) return;
const lastStock = this.store.stockList.slice(-1)[0];
if (lastStock.count > 1) lastStock.count--;
else this.store.stockList.splice(-1);
this.store.stockList.splice(-1);
},
switchVehicles() {
@@ -198,6 +209,32 @@ export default defineComponent({
const stockObject = this.getStockObject(vehicle, this.store.chosenCargo);
this.store.stockList[this.store.chosenStockListIndex] = stockObject;
},
selectLocoType(locoGroupType: LocoGroupType) {
this.store.chosenLocoGroup = locoGroupType;
this.store.chosenVehicle = this.locoOptions[0];
this.store.chosenLoco = this.locoOptions[0];
},
selectCarWagonType(wagonGroupType: WagonGroupType) {
this.store.chosenCarGroup = wagonGroupType;
this.store.chosenVehicle = this.carOptions[0];
this.store.chosenCar = this.carOptions[0];
this.store.chosenCargo = null;
},
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.chosenCargo =
this.store.chosenCar?.cargoTypes.find(
(cargo) => cargo.id == this.store.chosenCargo?.id
) || null;
});
},
},
});
</script>
@@ -213,6 +250,11 @@ export default defineComponent({
grid-column: 1;
}
.input_container {
width: 100%;
max-width: 380px;
}
.input_header {
margin-bottom: 1em;
}
@@ -232,6 +274,10 @@ button.btn--choice {
.input_list {
margin: 0.5em 0;
select {
width: 100%;
}
label {
display: block;
@@ -273,4 +319,3 @@ button.btn--choice {
}
}
</style>
+47 -8
View File
@@ -1,39 +1,78 @@
<template>
<section class="logo-section" @click="navigate">
<img src="/images/logo.svg" alt="logo pojazdownik" />
<section class="logo-section">
<img :src="`/logo-${$i18n.locale}.svg`" alt="logo pojazdownik" @click="navigate" />
<div class="actions">
<button
v-for="action in localeActions"
:key="action.name"
class="btn btn--text"
:data-selected="$i18n.locale == action.locale"
@click="chooseLocale(action.locale)"
>
{{ action.name }}
</button>
</div>
</section>
</template>
<script lang="ts">
export default {
setup() {
return {};
data() {
return {
localeActions: [
{
name: 'POLSKI',
locale: 'pl',
},
{
name: 'ENGLISH',
locale: 'en',
},
],
};
},
methods: {
navigate() {
window.location.pathname = '';
},
chooseLocale(locale: string) {
this.$i18n.locale = locale;
window.localStorage.setItem('locale', locale);
},
},
};
</script>
<style lang="scss" scoped>
@import '../../styles/global.scss';
.logo-section {
grid-row: 1;
grid-column: 1;
margin-bottom: 1.5em;
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
gap: 0.5em;
cursor: pointer;
}
.actions {
display: flex;
gap: 0.5em;
button[data-selected='true'] {
font-weight: bold;
color: $accentColor;
text-decoration: underline;
}
}
img {
max-width: 25em;
width: 100%;
}
</style>
+16 -22
View File
@@ -2,27 +2,28 @@
<section class="stock-section">
<div class="section_modes">
<button
v-for="(id, i) in sectionModes"
:key="id"
class="btn"
ref="sectionButtonRefs"
v-for="(id, name, i) in sectionModes"
@click="chooseSection(id)"
:data-selected="store.stockSectionMode == id"
>
<span class="text--accent">{{ i + 1 }}.</span> {{ name }}
<span class="text--accent">{{ i + 1 }}.</span> {{ $t(`topbar.${id}`) }}
<span v-if="id == 'stock-list'">({{ store.stockList.length }})</span>
</button>
</div>
<transition name="tab-change" mode="out-in">
<keep-alive>
<component :is="chosenSectionComponent" :key="chosenSectionComponent"></component>
<component :is="chosenSectionComponent"></component>
</keep-alive>
</transition>
</section>
</template>
<script lang="ts" setup>
import { computed, KeepAlive, onMounted, ref } from 'vue';
import { computed, onMounted, ref } from 'vue';
import { useStore } from '../../store';
import StockListTab from '../tabs/StockListTab.vue';
import StockGeneratorTab from '../tabs/StockGeneratorTab.vue';
@@ -34,28 +35,22 @@ const sectionButtonRefs = ref([]);
const store = useStore();
type SectionMode = typeof store.stockSectionMode;
const sectionModes: { [key: string]: SectionMode } = {
SKŁAD: 'stock-list',
POJAZDY: 'wiki-list',
'GNR NUMERU': 'number-generator',
'GNR SKŁADU': 'stock-generator',
};
const sectionKeyIndexes: { [key: number]: SectionMode } = {
1: 'stock-list',
2: 'wiki-list',
3: 'number-generator',
4: 'stock-generator',
};
const sectionModes: SectionMode[] = [
'stock-list',
'wiki-list',
'number-generator',
'stock-generator',
];
onMounted(() => {
window.addEventListener('keydown', (e) => {
if (e.target instanceof HTMLInputElement) return;
if (/[1234]/.test(e.key)) {
if (/^[1234]$/.test(e.key)) {
const keyNum = Number(e.key);
store.stockSectionMode = sectionKeyIndexes[keyNum];
(sectionButtonRefs.value[keyNum - 1] as HTMLButtonElement).focus();
store.stockSectionMode = sectionModes[keyNum - 1];
(sectionButtonRefs.value[keyNum - 1] as HTMLButtonElement)?.focus();
}
});
});
@@ -115,7 +110,7 @@ function chooseSection(sectionId: SectionMode) {
gap: 0.5em;
margin-bottom: 0.5em;
margin-bottom: 1em;
button {
position: relative;
@@ -146,4 +141,3 @@ function chooseSection(sectionId: SectionMode) {
}
}
</style>
+153 -121
View File
@@ -1,55 +1,76 @@
<template>
<section class="train-image-section">
<div class="train-image__wrapper">
<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">
ŁADOWANIE OBRAZU...
</div>
</transition>
<div class="no-img" v-if="!store.chosenVehicle">PODGLĄD WYBRANEGO POJAZDU</div>
<div v-if="store.chosenVehicle">
<div class="image-wrapper">
<img
v-if="store.chosenVehicle"
:src="`https://spythere.github.io/api/td2/images/${store.chosenVehicle.type}--300px.jpg`"
:alt="store.chosenVehicle.type"
@load="onImageLoad"
:src="getThumbnailURL(store.chosenVehicle.type, 'small')"
:data-preview-active="store.chosenVehicle !== null"
:data-sponsor-only="
store.chosenVehicle.sponsorOnlyTimestamp &&
store.chosenVehicle.sponsorOnlyTimestamp > Date.now()
"
:data-team-only="store.chosenVehicle.teamOnly"
@click="onImageClick"
@keydown.enter="onImageClick"
@load="onImageLoad"
@error="onImageError"
tabindex="0"
/>
<!-- <div class="empty-message" v-if="store.chosenVehicle && !store.chosenVehicle.imageSrc">Ten pojazd nie ma jeszcze podglądu!</div> -->
</div>
<div class="train-image__info" v-if="store.chosenVehicle">
<div class="image-info">
<b class="text--accent">{{ store.chosenVehicle.type }}</b> &bull;
<b style="color: #ccc">{{
vehicleTypes[
isLocomotive(store.chosenVehicle) ? store.chosenVehicle.power : store.chosenVehicle.useType || 'loco-e'
]
}}</b>
<b style="color: #ccc">
{{
$t(
`preview.${isTractionUnit(store.chosenVehicle) ? store.chosenVehicle.group : store.chosenVehicle.group}`
)
}}
</b>
<div style="color: #ccc">
<div>
{{ store.chosenVehicle.length }}m | {{ store.chosenVehicle.mass }}t |
{{ store.chosenVehicle.length }}m |
{{ (store.chosenVehicle.weight / 1000).toFixed(1) }}t |
{{ store.chosenVehicle.maxSpeed }} km/h
</div>
<div v-if="isLocomotive(store.chosenVehicle)">Typ kabiny: {{ store.chosenVehicle.cabinType }}</div>
<div v-if="isTractionUnit(store.chosenVehicle)">
{{ $t('preview.cabin') }} {{ store.chosenVehicle.cabinType }}
</div>
<div v-else>
{{
store.chosenVehicle.useType == 'car-cargo'
? store.stockData?.usage[store.chosenVehicle.constructionType]
: 'Typ konstrukcji: ' + store.chosenVehicle.constructionType
store.chosenVehicle.group == 'wagon-freight'
? $t(`usage.${store.chosenVehicle.constructionType}`)
: `${$t('preview.construction')} ${store.chosenVehicle.constructionType}`
}}
</div>
<b style="color: salmon;" v-if="store.chosenVehicle.supportersOnly">* TYLKO DLA SPONSORÓW</b>
<b
v-if="
store.chosenVehicle.sponsorOnlyTimestamp &&
store.chosenVehicle.sponsorOnlyTimestamp > Date.now()
"
class="sponsor-only"
>
{{
$t('preview.sponsor-only', [
new Date(store.chosenVehicle.sponsorOnlyTimestamp).toLocaleDateString(
$i18n.locale == 'pl' ? 'pl-PL' : 'en-GB'
),
])
}}
</b>
<b v-if="store.chosenVehicle.teamOnly" class="team-only">{{ $t('preview.team-only') }}</b>
</div>
</div>
</div>
<div class="train-image__info" v-else>Wybierz pojazd lub wagon, aby zobaczyć jego podgląd powyżej</div>
<div v-else>
<img src="/images/placeholder.jpg" alt="placeholder image" />
<div class="image-info">{{ $t('preview.desc') }}</div>
</div>
</section>
</template>
@@ -57,137 +78,149 @@
<script lang="ts">
import { computed, defineComponent } from 'vue';
import { useStore } from '../../store';
import { isLocomotive } from '../../utils/vehicleUtils';
import { ILocomotive, Vehicle } from '../../types';
import { isTractionUnit } from '../../utils/vehicleUtils';
import imageMixin from '../../mixins/imageMixin';
import { watch } from 'vue';
import { storeToRefs } from 'pinia';
import { Ref } from 'vue';
import { ref } from 'vue';
export default defineComponent({
mixins: [imageMixin],
data() {
return {
noImageAvailable: false,
};
},
setup() {
const store = useStore();
const { chosenVehicle } = storeToRefs(store);
const imageStatus = ref('none') as Ref<'none' | 'loading' | 'loaded' | 'error'>;
watch(chosenVehicle, () => {
imageStatus.value = 'loading';
});
return {
store,
imageStatus,
chosenVehicle: computed(() => store.chosenVehicle),
};
},
data() {
return {
vehicleTypes: {
'loco-e': 'ELEKTROWÓZ',
'loco-s': 'SPALINOWÓZ',
'loco-ezt': 'EZT',
'loco-szt': 'SZT',
'car-passenger': 'WAGON PASAŻERSKI',
'car-cargo': 'WAGON TOWAROWY',
} as { [key: string]: string },
};
},
watch: {
chosenVehicle(vehicle: Vehicle, prevVehicle: Vehicle) {
if (vehicle && vehicle.type != prevVehicle?.type) {
this.store.imageLoading = true;
}
},
},
methods: {
onImageLoad() {
this.store.imageLoading = false;
isTractionUnit,
onImageError(e: Event) {
const el = e.target as HTMLImageElement;
if (el.src == '/images/placeholder.jpg') return;
el.src = '/images/placeholder.jpg';
this.imageStatus = 'error';
},
isLocomotive(vehicle: Vehicle): vehicle is ILocomotive {
return isLocomotive(vehicle);
onImageLoad(e: Event) {
this.imageStatus = 'loaded';
},
onImageClick() {
onImageClick(e: Event) {
const target = e.target as HTMLElement;
const chosenVehicle = this.store.chosenVehicle;
if (!chosenVehicle) return;
this.store.vehiclePreviewSrc = `https://spythere.github.io/api/td2/images/${chosenVehicle.type}--800px.jpg`;
this.store.lastFocusedElement = target;
this.store.vehiclePreviewSrc = this.getThumbnailURL(chosenVehicle.type, 'large');
},
},
});
</script>
<style lang="scss" scoped>
@import '../../styles/global.scss';
@import '../../styles/global';
.train-image-section {
grid-row: 3;
grid-column: 1;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
margin-top: 2em;
height: 22em;
}
min-height: 250px;
.train-image {
&__wrapper {
text-align: center;
}
&__content {
border: 1px solid white;
& > div {
position: relative;
overflow: hidden;
max-width: 22em;
height: 13em;
margin: 0 auto;
&.supporter {
border: 1px solid salmon;
}
img {
width: 100%;
height: 100%;
cursor: zoom-in;
}
.empty-message,
.no-img {
position: absolute;
left: 0;
bottom: 0;
padding: 0.3em 0;
width: 100%;
}
.empty-message {
background: rgba(#000, 0.75);
}
max-width: 100%;
width: 380px;
}
}
.train-image__info {
margin: 1em 0;
img {
width: 100%;
&[data-preview-active='true'] {
border: 1px solid white;
cursor: zoom-in;
}
&[data-sponsor-only='true'] {
border: 1px solid $sponsorColor;
}
&[data-team-only='true'] {
border: 1px solid $teamColor;
}
}
.image-wrapper {
position: relative;
min-height: 220px;
}
.loading-placeholder {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 100%;
display: flex;
align-items: center;
justify-content: center;
background-color: #2b2b2b;
padding: 1em;
font-size: 1.2em;
}
.image-info {
font-size: 1.1em;
padding: 0 1em;
padding: 0.5em;
// margin: 0.5em auto;
line-height: 1.35;
b {
font-size: 1.1em;
}
width: 100%;
max-width: 380px;
div {
margin: 0.25em 0;
}
background-color: $secondaryColor;
font-weight: bold;
}
// Transition animations
.img-message-anim {
&-enter-from,
&-leave-to {
opacity: 0;
}
.placeholder {
height: 250px;
&-enter-active,
&-leave-active {
transition: opacity 75ms ease-in 100ms;
}
background-color: $bgColor;
}
.sponsor-only {
color: $sponsorColor;
}
.team-only {
color: $teamColor;
}
@media screen and (max-width: $breakpointMd) {
@@ -196,4 +229,3 @@ export default defineComponent({
}
}
</style>
+187 -47
View File
@@ -1,45 +1,132 @@
<template>
<div class="number-generator tab">
<div class="tab_header">
<h2>GENERATOR NUMERU POCIĄGU</h2>
<h2>{{ $t('numgen.title') }}</h2>
<h3>{{ $t('numgen.subtitle') }}</h3>
</div>
<div class="tab_content">
<div class="options">
<select v-model="beginRegionName" @change="randomizeTrainNumber()">
<option :value="null" disabled>Początkowy obszar konstrukcyjny</option>
<option v-for="(_, name) in genData.regionNumbers" :value="name">{{ name }}</option>
</select>
<div class="inputs">
<label>
<span>{{ $t('numgen.train-category') }}</span>
<select v-model="chosenCategory" @change="randomizeTrainNumber()">
<option :value="null" disabled>
{{ $t('numgen.train-category') }}
</option>
<option
v-for="(_, category) in genData.categoriesRules"
:key="category"
:value="category"
>
{{ $t(`numgen.categories.${category}`) }}
</option>
</select>
</label>
<select v-model="endRegionName" @change="randomizeTrainNumber()">
<option :value="null" disabled>Końcowy obszar konstrukcyjny</option>
<option v-for="(_, name) in genData.regionNumbers" :value="name">{{ name }}</option>
</select>
<label>
<span>{{ $t('numgen.start-region') }}</span>
<select v-model="beginRegionName" @change="randomizeTrainNumber()">
<option :value="null" disabled>
{{ $t('numgen.start-region') }}
</option>
<option v-for="(_, name) in genData.regionNumbers" :key="name" :value="name">
{{ name }}
</option>
</select>
</label>
<select v-model="categoryRules" @change="randomizeTrainNumber()">
<option :value="null" disabled>Kategoria pociągu</option>
<option v-for="(rules, category) in genData.categories" :value="rules">{{ category }}</option>
</select>
<label>
<span> {{ $t('numgen.end-region') }}</span>
<select v-model="endRegionName" @change="randomizeTrainNumber()">
<option :value="null" disabled>
{{ $t('numgen.end-region') }}
</option>
<option v-for="(_, name) in genData.regionNumbers" :key="name" :value="name">
{{ name }}
</option>
</select>
</label>
</div>
<div class="generated-number" @click="copyNumber">
<span v-if="trainNumber">
Wygenerowany numer pociągu: <b class="text--accent">{{ trainNumber }}</b>
{{ $t('numgen.number-info') }}
<b class="text--accent">{{ trainNumber }}</b>
</span>
<span v-else>Wybierz kategorię oraz obszary konstrukcyjne (opcjonalnie)</span>
<span v-else>{{ $t('numgen.warning') }}</span>
</div>
<div class="category-rules" v-if="chosenCategory && categoryRules && trainNumber">
<!-- First & second digit (the same regions) -->
<div v-if="beginRegionName && endRegionName && beginRegionName == endRegionName">
<b>{{ $t('numgen.rules.two-first-digits') }}</b>
{{ $t('numgen.rules.from-pool') }}
<b class="text--accent">{{ genData.sameRegions[beginRegionName].join(', ') }}</b>
{{ $t('numgen.rules.for-region') }} {{ beginRegionName }}
</div>
<!-- First & second digit (different regions) -->
<div v-else>
<div>
<b>
{{ $t('numgen.rules.first-digit') }}
<span class="text--accent">{{ trainNumber[0] }}</span>
</b>
{{ $t('numgen.rules.for-region-begin') }} {{ beginRegionName }}
</div>
<div>
<b>
{{ $t('numgen.rules.second-digit') }}
<span class="text--accent">{{ trainNumber[1] }} </span>
</b>
{{ $t('numgen.rules.for-region-end') }} {{ endRegionName }}
</div>
</div>
<!-- Third digit (non-passenger only) -->
<div v-if="categoryRules[0] != null">
<b>
{{ $t('numgen.rules.third-digit') }}
<span class="text--accent">{{ categoryRules[0] }}</span>
</b>
{{ $t('numgen.rules.for-category') }} {{ chosenCategory }}
</div>
<!-- Last digits -->
<div>
<b>
{{
$t(`numgen.rules.${categoryRules[1]?.length == 3 ? 'three' : 'two'}-last-digits`)
}}</b
>
{{ $t('numgen.rules.from-range') }}
<b class="text--accent">{{ categoryRules[1] }}-{{ categoryRules[2] }}</b>
</div>
</div>
<hr />
<div class="tab_links">
<a href="https://wiki.td2.info.pl/index.php?title=Zasady_numeracji_poci%C4%85g%C3%B3w" target="_blank">
> Szczegółowe zasady numeracji (wikipedia TD2)
<a :href="$t('numgen.td2-wiki-link')" target="_blank">
{{ $t('numgen.td2-wiki') }}
</a>
</div>
<hr />
<div class="tab_actions">
<button class="btn" @click="randomizeTrainNumber(true)">LOSUJ OBSZARY</button>
<button class="btn" @click="randomizeTrainNumber(false)">LOSUJ NUMER</button>
<button class="btn" @click="randomizeTrainNumber(true)">
{{ $t('numgen.action-random-region') }}
</button>
<button class="btn" @click="randomizeCategory">
{{ $t('numgen.action-random-category') }}
</button>
<button class="btn" @click="randomizeTrainNumber(false)">
{{ $t('numgen.action-random-number') }}
</button>
</div>
</div>
</div>
@@ -47,26 +134,43 @@
<script setup lang="ts">
import { Ref, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import genData from '../../constants/numberGeneratorData.json';
import { computed } from 'vue';
const i18n = useI18n();
type RegionName = keyof typeof genData.regionNumbers;
type Category = keyof typeof genData.categoriesRules;
const beginRegionName = ref(null) as Ref<RegionName | null>;
const endRegionName = ref(null) as Ref<RegionName | null>;
const categoryRules = ref(null) as Ref<string | null>;
const chosenCategory = ref(null) as Ref<Category | null>;
const trainNumber = ref(null) as Ref<string | null>;
const copyNumber = () => {
if (trainNumber.value) {
navigator.clipboard.writeText(trainNumber.value);
alert('Skopiowano numer do schowka!');
alert(i18n.t('numgen.alert'));
}
};
const categoryRules = computed(() => {
if (!chosenCategory.value) return null;
return genData.categoriesRules[chosenCategory.value];
});
const randomizeCategory = () => {
const categoryKeys = Object.keys(genData.categoriesRules) as Category[];
chosenCategory.value = categoryKeys[~~(Math.random() * categoryKeys.length)];
randomizeTrainNumber(false);
};
const randomizeTrainNumber = (randomizeRegions = false) => {
if (categoryRules.value == null) return;
// if (categoryRules.value == null) return;
const regionKeys = Object.keys(genData.regionNumbers);
@@ -78,9 +182,11 @@ const randomizeTrainNumber = (randomizeRegions = false) => {
let number = '';
// Two first numbers (begin & end regions)
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!];
@@ -89,29 +195,29 @@ const randomizeTrainNumber = (randomizeRegions = false) => {
number += `${beginRegionNumber}${endRegionNumber}`;
}
// Do not roll the rest of number again if only randomize regions
if (randomizeRegions) {
// Do not roll the rest of number again if only randomize regions and category rules are already selected
if (randomizeRegions && chosenCategory.value != null) {
trainNumber.value = number + trainNumber.value?.substring(2);
return;
}
const rulesArray = categoryRules.value.split(';').map((r) => ({
index: r.split(':')[0],
rule: r.split(':')[1],
nums: Number(r.split(':')[2] || '1'),
}));
// Choose default category if it's not chosen
if (chosenCategory.value == null) chosenCategory.value = 'EI';
rulesArray.forEach((r) => {
const range = r.rule.split('-');
// Get category rules
const [thirdNumber, minRange, maxRange] = categoryRules.value!;
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();
// Third number
number += thirdNumber ?? '';
number += new Array(Math.abs(randRange.length - r.nums)).fill('0').join('') + randRange;
}
});
// Remaining numbers
const rangeNums = minRange!.length;
const randRange = Math.floor(
Math.random() * (Number(maxRange) - Number(minRange)) + Number(minRange)
).toString();
const leadingZeros = new Array(Math.abs(randRange.length - rangeNums)).fill('0').join('');
number += `${leadingZeros}${randRange}`;
trainNumber.value = number;
};
@@ -121,13 +227,31 @@ const randomizeTrainNumber = (randomizeRegions = false) => {
@import '../../styles/tab.scss';
@import '../../styles/global.scss';
.options {
display: flex;
.category-select {
select {
width: auto;
min-width: 50%;
}
margin-bottom: 1em;
}
.inputs {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(15em, 1fr));
flex-wrap: wrap;
gap: 0.5em;
}
select {
width: calc(50% - 0.5em);
.inputs > label {
display: flex;
flex-direction: column;
gap: 0.25em;
span {
color: #ccc;
font-weight: bold;
}
}
@@ -143,14 +267,19 @@ const randomizeTrainNumber = (randomizeRegions = false) => {
background-color: $secondaryColor;
}
.category-rules {
margin-bottom: 0.5em;
}
.tab_actions {
grid-template-columns: 1fr 1fr;
grid-template-columns: repeat(3, 1fr);
margin: 0.5em 0;
}
.tab_links {
display: flex;
justify-content: flex-end;
margin: 0.25em 0;
}
@media screen and (max-width: $breakpointMd) {
@@ -160,9 +289,20 @@ const randomizeTrainNumber = (randomizeRegions = false) => {
}
@media screen and (max-width: $breakpointSm) {
.options select {
.regions-select {
flex-wrap: wrap;
}
.regions-select select {
width: 100%;
}
.category-select select {
min-width: 100%;
}
.category-rules {
text-align: center;
}
}
</style>
+167 -75
View File
@@ -1,62 +1,68 @@
<template>
<div class="stock-generator tab">
<div class="tab_header">
<h2>GENERATOR SKŁADU TOWAROWEGO</h2>
<h2>{{ $t('stockgen.title') }}</h2>
</div>
<div class="tab_content">
<div>
<h2>WŁAŚCIWOŚCI SKŁADU</h2>
<h2>{{ $t('stockgen.properties-title') }}</h2>
<b class="text--accent">
&lArr; Dodaj lokomotywę na pierwsze miejsce listy, aby uwzględnić przy losowaniu składu!
{{ $t('stockgen.properties-desc') }}
</b>
<div class="tab_attributes">
<div class="inputs">
<label>
Maksymalna masa (t)
<input type="number" v-model="maxMass" step="100" max="4000" min="0" />
<span>{{ $t('stockgen.input-mass') }}</span>
<input type="number" v-model="maxTons" step="100" max="4000" min="0" />
</label>
<label>
Maks. długość (m)
<span>{{ $t('stockgen.input-length') }}</span>
<input type="number" v-model="maxLength" step="25" max="650" min="0" />
</label>
<label>
Maks. liczba wagonów
<span>{{ $t('stockgen.input-carcount') }}</span>
<input type="number" v-model="maxCarCount" step="1" max="60" min="1" />
</label>
</div>
<!-- <hr style="margin: 1em 0" /> -->
<!-- <div class="generator_options">
<Checkbox v-model="isCarGroupingEnabled">Grupuj wylosowane wagony (ustawia podobne wagony obok siebie w składzie)</Checkbox>
</div> -->
</div>
<div>
<h2>ŁADUNEK</h2>
<b>Wybierz ładunki, którymi chcesz wypełnić dostępne wagony:</b>
<h2>{{ $t('stockgen.cargo-title') }}</h2>
<b>{{ $t('stockgen.cargo-desc') }}</b>
</div>
<div class="generator_cargo">
<button
v-for="cargo in computedCargoData"
:key="cargo.name"
class="btn"
:data-chosen="chosenCargoTypes.includes(k.toString())"
v-for="(v, k) in store.stockData?.generator.cargo"
@click="toggleCargoChosen(k.toString(), v)"
:data-chosen="chosenCargoTypes.includes(cargo.name)"
@click="toggleCargoChosen(cargo.name, cargo.cargoList)"
>
{{ k }}
{{ $t(`cargo.${cargo.name}`) }}
</button>
</div>
<div>
<h2>WAGONY Z WYBRANYMI ŁADUNKAMI</h2>
<h2>{{ $t('stockgen.chosen-title') }}</h2>
<div class="generator_warning">
<span v-if="computedChosenCarTypes.size == 0">
Wybierz co najmniej jeden ładunek, aby zobaczyć wagony, które go posiadają!
{{ $t('stockgen.chosen-empty-warning') }}
</span>
<span v-else>
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).
{{ $t('stockgen.chosen-warning') }}
</span>
</div>
</div>
@@ -79,15 +85,28 @@
<hr />
<div class="tab_actions">
<button class="btn" :data-disabled="computedChosenCarTypes.size == 0" @click="generateStock()">
WYGENERUJ
</button>
<button class="btn" :data-disabled="computedChosenCarTypes.size == 0" @click="generateStock(true)">
WYGENERUJ PRÓŻNE WAGONY
<button
class="btn"
:data-disabled="computedChosenCarTypes.size == 0"
@click="generateStock()"
>
{{ $t('stockgen.action-generate') }}
</button>
<button class="btn" :data-disabled="computedChosenCarTypes.size == 0" @click="resetChosenCargo">
ZRESETUJ ŁADUNKI
<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>
</div>
</div>
@@ -100,12 +119,11 @@ import { useStore } from '../../store';
import stockMixin from '../../mixins/stockMixin';
import { ICargo, ICarWagon, IStock } from '../../types';
import warningsMixin from '../../mixins/warningsMixin';
import { isTractionUnit } from '../../utils/vehicleUtils';
export default defineComponent({
name: 'stock-generator',
mixins: [stockMixin, warningsMixin],
mixins: [stockMixin],
data() {
return {
@@ -116,17 +134,32 @@ export default defineComponent({
previewTimeout: -1,
maxMass: 3000,
maxTons: 3000,
maxLength: 650,
maxCarCount: 50,
isCarGroupingEnabled: false,
store: useStore(),
};
},
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)));
},
computedCargoData() {
if (!this.store.vehiclesData?.generator.cargo) return [];
const cargoGeneratorData = this.store.vehiclesData.generator.cargo;
return Object.keys(cargoGeneratorData)
.sort((v1, v2) => this.$t(`cargo.${v1}`).localeCompare(this.$t(`cargo.${v2}`)))
.map((v) => ({
name: v,
cargoList: cargoGeneratorData[v],
}));
},
},
@@ -149,65 +182,95 @@ export default defineComponent({
this.excludedCarTypes.length = 0;
},
// WIP
groupStock(stockList: IStock[]) {
if (!this.isCarGroupingEnabled) return false;
stockList.sort((s1, s2) => {
return (s1.vehicleRef.constructionType + s1.cargo?.id).localeCompare(
s2.vehicleRef.constructionType + s2.cargo?.id
);
});
},
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.vehiclesData?.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]!.cargoTypes);
else
cargoObjs.push(carWagonObjs[0]?.cargoTypes.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;
this.store.stockList.length = headingLoco ? 1 : 0;
this.store.stockList.splice(
this.store.stockList.length > 0 && isTractionUnit(this.store.stockList[0].vehicleRef)
? 1
: 0
);
const maxMass =
this.store.acceptableMass > 0 ? Math.min(this.store.acceptableMass, this.maxMass) : this.maxMass;
let carCount = 0;
const maxWeight =
this.store.acceptableWeight > 0
? Math.min(this.store.acceptableWeight, this.maxTons * 1000)
: this.maxTons * 1000;
let exceeded = false;
while (!exceeded) {
const randomStockType = generatedChosenStockList[~~(Math.random() * generatedChosenStockList.length)];
const { carWagon, cargo } = randomStockType.carPool[~~(Math.random() * randomStockType.carPool.length)];
// eslint-disable-next-line no-constant-condition
while (true) {
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.totalWeight + (carWagon.weight + (cargo?.weight ?? 0)) > maxWeight ||
this.store.totalLength + carWagon.length > this.maxLength ||
this.store.stockList.length > this.maxCarCount
carCount >= this.maxCarCount
) {
exceeded = true;
break;
}
this.addCarWagon(carWagon, cargo);
carCount++;
}
const currentGenerationValue = this.store.totalLength + this.store.totalMass + this.store.stockList.length;
const currentGenerationValue = this.store.totalLength + this.store.totalWeight + carCount;
if (bestGeneration.value < currentGenerationValue) {
bestGeneration.stockList = this.store.stockList;
@@ -215,6 +278,10 @@ export default defineComponent({
}
}
const bestStockList = bestGeneration.stockList;
this.groupStock(bestStockList);
this.store.stockList = bestGeneration.stockList;
this.store.stockSectionMode = 'stock-list';
},
@@ -227,7 +294,7 @@ export default defineComponent({
this.store.chosenLoco = null;
this.store.chosenCargo = null;
if (c) this.store.chosenCarUseType = c?.useType;
if (c) this.store.chosenCarGroup = c?.group;
},
toggleCargoChosen(cargoType: string, vehicles: string[]) {
@@ -261,6 +328,11 @@ export default defineComponent({
@import '../../styles/global.scss';
@import '../../styles/tab.scss';
h2 {
margin-top: 0;
margin-bottom: 0.5em;
}
.generator_cargo,
.generator_vehicles {
display: grid;
@@ -277,13 +349,6 @@ export default defineComponent({
background-color: $secondaryColor;
&[data-chosen='true'] {
background-color: $accentColor;
color: black;
box-shadow: 0 0 5px 1px $accentColor;
}
&[data-excluded='true'] {
background-color: gray;
box-shadow: none;
@@ -308,6 +373,34 @@ export default defineComponent({
gap: 1em;
}
.inputs {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(12em, 1fr));
flex-wrap: wrap;
gap: 0.5em;
}
.inputs > label {
display: flex;
flex-direction: column;
gap: 0.25em;
span {
color: #ccc;
}
input[type='text'] {
background-color: white;
}
}
.generator_options {
display: flex;
flex-direction: column;
gap: 0.5em;
}
.generator_warning {
background-color: $accentColor;
padding: 0.5em;
@@ -316,4 +409,3 @@ export default defineComponent({
color: black;
}
</style>
+360 -245
View File
@@ -1,176 +1,249 @@
<template>
<section class="stock-list-tab">
<div class="stock_controls" :data-disabled="store.chosenStockListIndex == -1">
<b class="no">
POJAZD NR <span class="text--accent">{{ store.chosenStockListIndex + 1 }}</span> &nbsp;
</b>
<button
class="btn"
:tabindex="store.chosenStockListIndex == -1 ? -1 : 0"
@click="moveUpStock(store.chosenStockListIndex)"
>
<img :src="getIconURL('higher')" alt="move up vehicle" />
PRZENIEŚ WYŻEJ
</button>
<button
class="btn"
:tabindex="store.chosenStockListIndex == -1 ? -1 : 0"
@click="moveDownStock(store.chosenStockListIndex)"
>
<img :src="getIconURL('lower')" alt="move down vehicle" />
PRZENIEŚ NIŻEJ
</button>
<button
class="btn"
:tabindex="store.chosenStockListIndex == -1 ? -1 : 0"
@click="removeStock(store.chosenStockListIndex)"
>
<img :src="getIconURL('remove')" alt="remove vehicle" />
USUŃ
</button>
<div class="tab_header">
<h2>{{ $t('stocklist.title') }}</h2>
</div>
<div class="stock_actions">
<label class="file-label">
<div class="btn">WCZYTAJ</div>
<input type="file" @change="uploadStock" ref="conFile" accept=".con,.txt" />
</label>
<div class="tab_content">
<div class="stock_actions">
<button class="btn btn--image" @click="clickFileInput">
<input type="file" @change="uploadStock" ref="conFile" accept=".con,.txt" />
<img src="/images/icon-upload.svg" alt="upload icon" />
{{ $t('stocklist.action-upload') }}
</button>
<button class="btn" :data-disabled="stockIsEmpty" :disabled="stockIsEmpty" @click="downloadStock">POBIERZ</button>
<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') }}
</button>
<button class="btn" :data-disabled="stockIsEmpty" :disabled="stockIsEmpty" @click="copyToClipboard">
SKOPIUJ
</button>
<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') }}
</button>
<button class="btn" :data-disabled="stockIsEmpty" :disabled="stockIsEmpty" @click="resetStock">ZRESETUJ</button>
<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') }}
</button>
<button class="btn" :data-disabled="stockIsEmpty" :disabled="stockIsEmpty" @click="shuffleCars">PRZETASUJ</button>
</div>
<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') }}
</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" />
{{ store.chosenRealStock.number }} {{ store.chosenRealStock.name }}
<div class="stock_controls" :data-disabled="store.chosenStockListIndex == -1">
<button
class="btn btn--image"
:tabindex="store.chosenStockListIndex == -1 ? -1 : 0"
@click="moveUpStock(store.chosenStockListIndex)"
>
<img :src="getIconURL('higher')" alt="move up vehicle" />
{{ $t('stocklist.action-move-up') }}
</button>
<button
class="btn btn--image"
:tabindex="store.chosenStockListIndex == -1 ? -1 : 0"
@click="moveDownStock(store.chosenStockListIndex)"
>
<img :src="getIconURL('lower')" alt="move down vehicle" />
{{ $t('stocklist.action-move-down') }}
</button>
<button
class="btn btn--image"
:tabindex="store.chosenStockListIndex == -1 ? -1 : 0"
@click="removeStock(store.chosenStockListIndex)"
>
<img :src="getIconURL('remove')" alt="remove vehicle" />
{{ $t('stocklist.action-remove') }}
</button>
</div>
<div class="stock_specs">
<b class="real-stock-info" v-if="chosenRealComposition">
<span class="text--accent">
<img :src="getIconURL(chosenRealComposition.type)" :alt="chosenRealComposition.type" />
{{ chosenRealComposition.number }} {{ chosenRealComposition.name }}
</span>
|
</b>
<span>
{{ $t('stocklist.mass') }}
<span class="text--accent">{{ (store.totalWeight / 1000).toFixed(1) }}t</span>
({{ $t('stocklist.mass-accepted') }}:
<span class="text--accent">{{
store.acceptableWeight ? `${~~(store.acceptableWeight / 1000)}t` : '-'
}}</span
>) - {{ $t('stocklist.length') }}:
<span class="text--accent">{{ store.totalLength }}m</span>
- {{ $t('stocklist.vmax') }}
<span tabindex="0" :data-tooltip="$t('stocklist.disclaimer')">(?)</span>:
<span class="text--accent">{{ store.maxStockSpeed }} km/h</span>
</span>
|
</b>
</div>
<span>
Masa: <span class="text--accent">{{ store.totalMass }}t</span> (dopuszczalna:
<span class="text--accent">{{ store.acceptableMass ? store.acceptableMass + 't' : '-' }}</span
>) - Długość:
<span class="text--accent">{{ store.totalLength }}m</span>
- vMax: <span class="text--accent">{{ store.maxStockSpeed }} km/h</span>
</span>
<div></div>
<div class="stock_spawn-settings">
<Checkbox :disabled="!store.stockSupportsColdStart" v-model="store.isColdStart">
{{ $t('stocklist.coldstart-info') }}
</Checkbox>
<Checkbox :disabled="!store.stockSupportsDoubleManning" v-model="store.isDoubleManned">
{{ $t('stocklist.doublemanning-info') }}
</Checkbox>
</div>
<div class="stock_warnings" v-if="hasAnyWarnings">
<div class="warning" v-if="locoNotSuitable">
(!) {{ $t('stocklist.warning-not-suitable') }}
</div>
<div class="warning" v-if="lengthExceeded && store.isTrainPassenger">
(!) {{ $t('stocklist.warning-passenger-too-long') }}
</div>
<div class="warning" v-if="lengthExceeded && !store.isTrainPassenger">
(!) {{ $t('stocklist.warning-freight-too-long') }}
</div>
<div class="warning" v-if="teamOnlyVehicles.length > 0">
(!)
{{
$t('stocklist.warning-team-only-vehicle', [
teamOnlyVehicles.map((v) => v.vehicleRef.type).join(', '),
])
}}
</div>
<div class="warning" v-if="weightExceeded">
(!)
<i18n-t keypath="stocklist.warning-too-heavy">
<template #href>
<a
target="_blank"
href="https://docs.google.com/spreadsheets/d/1KVa5vn2d8XGkXQFwbavVudwKqUQxbLOucHWs2VYqAUE"
>
{{ $t('stocklist.acceptable-mass-docs') }}
</a>
</template>
</i18n-t>
</div>
<div class="warning" v-if="locoCountExceeded">
{{ $t('stocklist.warning-too-many-locos') }}
</div>
</div>
<StockThumbnails :onListItemClick="onListItemClick" />
<!-- Stock list -->
<div class="list-wrapper">
<div v-if="stockIsEmpty" class="list-empty">
<div class="stock-info">{{ $t('stocklist.list-empty') }}</div>
</div>
<ul v-else>
<transition-group name="stock-list-anim">
<li
v-for="(stock, i) in store.stockList"
:key="stock.id"
:class="{ loco: isTractionUnit(stock.vehicleRef) }"
tabindex="0"
@click="onListItemClick(i)"
@keydown.enter="onListItemClick(i)"
@keydown.w="moveUpStock(i)"
@keydown.s="moveDownStock(i)"
@keydown.backspace="removeStock(i)"
ref="itemRefs"
>
<div
class="stock-info"
@dragstart="onDragStart(i)"
@drop="onDrop($event, i)"
@dragover="allowDrop"
draggable="true"
>
<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"
:data-sponsor-only="
stock.vehicleRef.sponsorOnlyTimestamp &&
stock.vehicleRef.sponsorOnlyTimestamp > Date.now()
"
:data-team-only="stock.vehicleRef.teamOnly"
>
{{
isTractionUnit(stock.vehicleRef)
? stock.vehicleRef.type
: getCarSpecFromType(stock.vehicleRef.type)
}}
</span>
<span class="stock-info-cargo" v-if="stock.cargo">
{{ stock.cargo.id }}
</span>
<span class="stock-info-length">{{ stock.vehicleRef.length }}m</span>
<span class="stock-info-mass">
{{ ((stock.vehicleRef.weight + (stock.cargo?.weight ?? 0)) / 1000).toFixed(1) }}t
</span>
<span class="stock-info-speed">{{ stock.vehicleRef.maxSpeed }}km/h</span>
</div>
</li>
</transition-group>
</ul>
</div>
</div>
<div class="stock_cold-start">
<label>
<input
type="checkbox"
v-model="store.isColdStart"
:disabled="!locoSupportsColdStart(store.stockList[0]?.constructionType || '')"
/>
Zimny start lokomotywy czołowej (tylko elektrowozy typów 303E i 203E)
</label>
</div>
<div class="stock_warnings" v-if="stockHasWarnings">
<div class="warning" v-if="locoNotSuitable">
Lokomotywy EP07 i EP08 przeznaczone jedynie do ruchu pasażerskiego!
</div>
<div class="warning" v-if="trainTooLong && store.isTrainPassenger">
Maksymalna długość składów pasażerskich nie może przekraczać 350m!
</div>
<div class="warning" v-if="trainTooLong && !store.isTrainPassenger">
Maksymalna długość składów innych niż pasażerskie nie może przekraczać 650m!
</div>
<div class="warning" v-if="trainTooHeavy">
Ten skład jest za ciężki! Sprawdź
<a
target="_blank"
href="https://docs.google.com/spreadsheets/d/1bFXUsHsAu4youmNz-46Q1HslZaaoklvfoBDS553TnNk/edit"
>
dopuszczalne masy składów
</a>
</div>
<div class="warning" v-if="tooManyLocomotives">Ten skład posiada za dużo pojazdów trakcyjnych!</div>
</div>
<StockThumbnails :onListItemClick="onListItemClick" />
<!-- Stock list -->
<ul ref="stock_list">
<li v-if="stockIsEmpty" class="list-empty">
<div class="stock-info">Lista pojazdów jest pusta!</div>
</li>
<TransitionGroup name="stock-list-anim">
<li
v-for="(stock, i) in store.stockList"
:key="stock.id"
:class="{ loco: stock.isLoco }"
tabindex="0"
@click="onListItemClick(i)"
@keydown.enter="onListItemClick(i)"
@keydown.w="moveUpStock(i)"
@keydown.s="moveDownStock(i)"
@keydown.backspace="removeStock(i)"
ref="itemRefs"
>
<div
class="stock-info"
@dragstart="onDragStart(i)"
@drop="onDrop($event, i)"
@dragover="allowDrop"
draggable="true"
>
<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 }">
{{ 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__length"> {{ stock.length }}m </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>
</TransitionGroup>
</ul>
</section>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import TrainImage from '../sections/TrainImageSection.vue';
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 Checkbox from '../common/Checkbox.vue';
import { isTractionUnit } from '../../utils/vehicleUtils';
export default defineComponent({
name: 'stock-list',
components: { TrainImage, StockThumbnails },
components: { StockThumbnails, Checkbox },
mixins: [warningsMixin, imageMixin, stockMixin, stockPreviewMixin],
mixins: [imageMixin, stockMixin, stockPreviewMixin],
setup() {
const store = useStore();
@@ -182,19 +255,36 @@ export default defineComponent({
data: () => ({
imageOffsetY: 0,
draggedVehicleID: -1,
stockActions: [{}],
}),
computed: {
chosenRealComposition() {
const currentStockString = this.store.stockList.map((s) => s.vehicleRef.type).join(';');
return this.store.realCompositionList.find((rc) => rc.stockString == currentStockString);
},
stockString() {
if (this.store.stockList.length == 0) return '';
const includeColdStart = this.store.isColdStart && this.store.stockSupportsColdStart;
const includeDoubleManned =
this.store.isDoubleManned && this.store.stockSupportsDoubleManning;
return this.store.stockList
.map((stock, i) => {
let stockTypeStr = stock.isLoco || !stock.cargo ? stock.type : `${stock.type}:${stock.cargo.id}`;
let coldStart =
i == 0 && this.store.isColdStart && locoSupportsColdStart(stock.constructionType || '') ? ',c' : '';
let stockTypeStr =
isTractionUnit(stock.vehicleRef) || !stock.cargo
? stock.vehicleRef.type
: `${stock.vehicleRef.type}:${stock.cargo.id}`;
return stockTypeStr + coldStart;
if (i == 0 && (includeColdStart || includeDoubleManned))
return `${stockTypeStr},${includeColdStart ? 'c' : ''}${includeDoubleManned ? 'd' : ''}`;
return stockTypeStr;
})
.join(';');
},
@@ -204,30 +294,80 @@ 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;
lengthExceeded() {
return (
(this.store.totalLength > 350 && this.store.isTrainPassenger) ||
(this.store.totalLength > 650 && !this.store.isTrainPassenger)
);
},
weightExceeded() {
return this.store.acceptableWeight && this.store.totalWeight > this.store.acceptableWeight;
},
locoNotSuitable() {
return (
!this.store.isTrainPassenger &&
this.store.stockList.length > 1 &&
!this.store.stockList.every((stock) => isTractionUnit(stock.vehicleRef)) &&
this.store.stockList.some(
(stock) => isTractionUnit(stock.vehicleRef) && stock.vehicleRef.type.startsWith('EP')
)
);
},
locoCountExceeded() {
return (
this.store.stockList.reduce((acc, stock) => {
if (isTractionUnit(stock.vehicleRef)) acc += 1;
return acc;
}, 0) > 2
);
},
teamOnlyVehicles() {
return this.store.stockList.filter((stock) => stock.vehicleRef.teamOnly);
},
hasAnyWarnings() {
return (
this.locoCountExceeded ||
this.weightExceeded ||
this.lengthExceeded ||
this.locoNotSuitable ||
this.teamOnlyVehicles
);
},
},
methods: {
locoSupportsColdStart,
isTractionUnit,
copyToClipboard() {
navigator.clipboard.writeText(this.stockString);
setTimeout(() => {
alert('Pociąg został skopiowany do schowka!');
alert(this.$t('stocklist.alert-copied'));
}, 20);
},
clickFileInput() {
(this.$refs['conFile'] as HTMLInputElement).click();
},
onListItemClick(stockID: number) {
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.vehicleRef.type
? -1
: stockID;
if (this.store.chosenStockListIndex == -1) {
this.store.chosenVehicle = null;
@@ -256,20 +396,6 @@ export default defineComponent({
this.store.chosenStockListIndex = -1;
},
addStock(index: number) {
if (index == -1) return;
this.store.stockList[index].count++;
},
subStock(index: number) {
if (index == -1) return;
if (this.store.stockList[index].count < 2) return;
this.store.stockList[index].count--;
},
removeStock(index: number) {
if (index == -1) return;
@@ -303,7 +429,7 @@ export default defineComponent({
shuffleCars() {
const availableIndexes = this.store.stockList.reduce((acc, stock, i) => {
if (!stock.isLoco) acc.push(i);
if (!isTractionUnit(stock.vehicleRef)) acc.push(i);
return acc;
}, [] as number[]);
@@ -313,7 +439,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];
@@ -322,16 +449,13 @@ export default defineComponent({
},
downloadStock() {
if (this.store.stockList.length == 0) return alert('Lista pojazdów jest pusta!');
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.chosenRealComposition ? this.chosenRealComposition.stockId + ' ' : ''}${this.store.stockList[0].vehicleRef.type} ${(this.store.totalWeight / 1000).toFixed(1)}t; ${
this.store.totalLength
}m; vmax ${this.store.maxStockSpeed}`;
const fileName = prompt(
'Nazwij plik, a następnie pobierz do folderu Presets (Dokumenty/TTSK/TrainDriver2):',
defaultName
);
const fileName = prompt(this.$t('stocklist.prompt-file'), defaultName);
if (!fileName) return;
@@ -365,7 +489,7 @@ export default defineComponent({
this.loadStockFromString(stockString);
};
reader.onerror = (err) => console.log(err);
reader.onerror = (err) => console.error(err);
inputEl.value = '';
},
@@ -398,14 +522,17 @@ export default defineComponent({
<style lang="scss" scoped>
@import '../../styles/global';
@import '../../styles/tab.scss';
.stock-list-tab {
display: grid;
grid-gap: 0.5em;
.tab_content {
display: flex;
flex-direction: column;
gap: 0.5em;
}
.warning {
padding: 0.25em;
margin: 0.25em 0;
background: $accentColor;
color: black;
@@ -421,9 +548,9 @@ export default defineComponent({
display: flex;
justify-content: center;
align-items: center;
flex-wrap: wrap;
gap: 0.5em;
flex-wrap: wrap;
padding: 0.5em;
@@ -438,48 +565,49 @@ export default defineComponent({
pointer-events: none;
}
input#stock-count {
width: 3em;
margin: 0;
padding: 0.25em;
outline: none;
border: none;
}
button {
img {
margin-right: 0.25em;
}
}
}
.stock_actions {
display: grid;
gap: 0.5em;
grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
label.file-label {
text-align: center;
cursor: pointer;
button {
width: 100%;
input {
display: none;
opacity: 0;
width: 0;
height: 0;
}
}
}
.stock_spawn-settings {
display: flex;
gap: 0.5em;
}
.real-stock-info {
img {
height: 1.3ch;
}
}
ul {
.list-wrapper {
position: relative;
overflow: auto;
}
.list-empty {
background-color: $secondaryColor;
border-radius: 0.5em;
padding: 0.75em;
font-weight: bold;
}
ul {
overflow-y: scroll;
height: 500px;
}
@@ -497,16 +625,11 @@ ul > li {
&:focus-visible {
outline: 1px solid white;
}
&.list-empty {
background-color: $secondaryColor;
border-radius: 0.5em;
padding: 0.75em;
}
}
li > .stock-info {
display: flex;
gap: 0.25em;
color: white;
font-weight: 700;
@@ -515,46 +638,39 @@ li > .stock-info {
& > span {
padding: 0.5em;
margin-right: 0.25em;
display: flex;
justify-content: center;
align-items: center;
}
}
.supporter {
color: salmon;
.stock-info-no,
.stock-info-type {
background-color: $secondaryColor;
&[data-team-only='true'] {
color: $teamColor;
}
&[data-sponsor-only='true'] {
color: $sponsorColor;
}
}
.stock-info {
&__no,
&__type {
background-color: $secondaryColor;
}
.stock-info-no {
min-width: 3.5em;
text-align: right;
&__count {
background-color: #e04e3e;
&[data-selected='true'] {
color: $accentColor;
}
}
&__no {
min-width: 3.5em;
text-align: right;
.stock-info-cargo {
background-color: #333;
}
&[data-selected='true'] {
color: $accentColor;
}
}
&__cargo {
background-color: #333;
}
&__length,
&__mass,
&__speed {
background-color: #555;
}
.stock-info-length,
.stock-info-mass,
.stock-info-speed {
background-color: #555;
}
.stock-list-anim {
@@ -584,4 +700,3 @@ li > .stock-info {
}
}
</style>
+308 -361
View File
@@ -1,361 +1,308 @@
<template>
<section class="wiki-list tab">
<div class="tab_header">
<h2>LISTA DOSTĘPNYCH POJAZDÓW</h2>
</div>
<div class="tab_content">
<div class="actions-panel">
<div class="actions-panel_vehicles">
<button class="btn btn--choice" @click="changeWikiMode('locomotives')">POJ. TRAKCYJNE</button>
<button class="btn btn--choice" @click="changeWikiMode('carWagons')">WAGONY</button>
</div>
<div class="actions-panel_search">
<input type="text" placeholder="Wyszukaj pojazd..." 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)">
{{ header.name }}
<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>{{ vehicleTypes[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 {
name: string;
id: SorterID;
sortable: boolean;
}
const locoHeaders: WikiHeader[] = [
{ name: 'Zdjęcie', id: 'image', sortable: false },
{ name: 'Nazwa', id: 'type', sortable: true },
{ name: 'Rodzaj', id: 'power', sortable: true },
{ name: 'Konstrukcja', id: 'constructionType', sortable: true },
{ name: 'Zimny start', id: 'coldStart', sortable: true },
{ name: 'Długość', id: 'length', sortable: true },
{ name: 'Masa', id: 'mass', sortable: true },
{ name: 'Prędkość', id: 'maxSpeed', sortable: true },
];
const carHeaders: WikiHeader[] = [
{ name: 'Zdjęcie', id: 'image', sortable: false },
{ name: 'Nazwa', id: 'type', sortable: true },
{ name: 'Konstrukcja', id: 'constructionType', sortable: true },
{ name: 'Długość', id: 'length', sortable: true },
{ name: 'Masa', id: 'mass', sortable: true },
{ name: 'Prędkość', id: 'maxSpeed', sortable: true },
{ name: 'Ładunki', id: 'cargoCount', sortable: true },
];
const vehicleTypes: { [key: string]: string } = {
'loco-ezt': 'EZT',
'loco-szt': 'SZT',
'loco-s': 'Spalinowóz',
'loco-e': 'Elektrowóz',
};
export default defineComponent({
mixins: [stockPreviewMixin, stockMixin],
data() {
return {
store: useStore(),
locoHeaders,
carHeaders,
vehicleTypes,
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">
<label>
<span>{{ $t('wiki.labels.search-vehicle') }}</span>
<input
type="text"
:placeholder="$t('wiki.labels.search-vehicle-placeholder')"
v-model="searchedVehicleTypeName"
/>
</label>
<label>
<span>{{ $t('wiki.labels.vehicles') }}</span>
<select name="filter-type" id="filter-type" v-model="filterType">
<option v-for="filter in filters" :key="filter" :value="filter">
{{ $t(`wiki.filters.${filter}`) }}
</option>
</select>
</label>
<label>
<span>{{ $t('wiki.labels.sort-by') }}</span>
<select name="sorter-type" id="sorter-type" v-model="sorterType">
<option v-for="sorter in sorters" :key="sorter" :value="sorter">
{{ $t(`wiki.sort-by.${sorter}`) }}
</option>
</select>
</label>
<label>
<span>{{ $t('wiki.labels.sort-direction') }}</span>
<select name="sorter-direction" id="sorter-direction" v-model="sorterDirection">
<option value="asc">{{ $t('wiki.sort-direction.asc') }}</option>
<option value="desc">{{ $t('wiki.sort-direction.desc') }}</option>
</select>
</label>
</div>
<ul class="vehicles" ref="vehicles">
<li
v-for="vehicle in computedVehicles"
:key="vehicle.type"
:data-preview="vehicle.type === store.chosenVehicle?.type"
@click="previewVehicle(vehicle)"
@dblclick="addVehicle(vehicle)"
@keydown.enter="onVehicleSelect(vehicle)"
tabindex="0"
>
<img loading="lazy" width="120" :src="getThumbnailURL(vehicle.type, 'small')" />
<span>
<span
class="vehicle-name"
:class="{
'sponsor-only':
vehicle.sponsorOnlyTimestamp && vehicle.sponsorOnlyTimestamp > Date.now(),
'team-only': vehicle.teamOnly,
}"
>
<b>{{ vehicle.type.replace(/_/g, ' ') }}</b>
</span>
<div class="vehicle-group">
{{ $t(`wiki.${vehicle.group}`) }} |
{{ isTractionUnit(vehicle) ? vehicle.cabinType : vehicle.constructionType }}
</div>
<div class="vehicle-props">
{{ vehicle.length }}m | {{ (vehicle.weight / 1000).toFixed(1) }}t |
{{ vehicle.maxSpeed }}km/h
</div>
</span>
</li>
</ul>
</div>
</section>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { useStore } from '../../store';
import stockPreviewMixin from '../../mixins/stockPreviewMixin';
import { IVehicle } from '../../types';
import { isTractionUnit } from '../../utils/vehicleUtils';
import stockMixin from '../../mixins/stockMixin';
import imageMixin from '../../mixins/imageMixin';
const sorters = ['type', 'group', 'length', 'weight', 'maxSpeed'] as const;
const filters = ['vehicles-all', 'vehicles-traction', 'vehicles-wagon'] as const;
type SorterType = (typeof sorters)[number];
type SorterDirection = 'asc' | 'desc';
type FilterType = (typeof filters)[number];
export default defineComponent({
mixins: [stockPreviewMixin, stockMixin, imageMixin],
data() {
return {
store: useStore(),
observer: null as IntersectionObserver | null,
sorters: sorters,
filters: filters,
searchedVehicleTypeName: '',
sorterType: 'type' as SorterType,
sorterDirection: 'asc' as SorterDirection,
filterType: 'vehicles-all' as FilterType,
lastScrollTop: 0,
};
},
deactivated() {
this.lastScrollTop = (this.$refs['vehicles'] as HTMLUListElement)?.scrollTop || 0;
},
activated() {
(this.$refs['vehicles'] as HTMLUListElement)?.scrollTo({ top: this.lastScrollTop });
},
watch: {
computedVehicles() {
const vehiclesRef = this.$refs['vehicles'] as HTMLElement;
vehiclesRef.scrollTo({
top: 0,
});
},
},
methods: {
isTractionUnit,
onVehicleSelect(vehicle: IVehicle) {
if (this.store.chosenVehicle?.type === vehicle.type) this.addVehicle(vehicle);
this.previewVehicle(vehicle);
},
filterVehicles(v: IVehicle) {
if (this.searchedVehicleTypeName)
return v.type
.toLocaleLowerCase()
.includes(this.searchedVehicleTypeName.toLocaleLowerCase());
switch (this.filterType) {
case 'vehicles-all':
return true;
case 'vehicles-traction':
return isTractionUnit(v);
case 'vehicles-wagon':
return !isTractionUnit(v);
default:
return false;
}
},
sortVehicles(v1: IVehicle, v2: IVehicle) {
const direction = this.sorterDirection == 'asc' ? 1 : -1;
switch (this.sorterType) {
case 'type':
case 'group':
return direction * v1[this.sorterType].localeCompare(v2[this.sorterType]);
case 'weight':
case 'length':
case 'maxSpeed':
return Math.sign(v1[this.sorterType] - v2[this.sorterType]) * direction;
// case 'cargoCount':
// return (
// Math.sign(
// (!isTractionUnit(v1) ? v1.cargoTypes.length || -1 : -1) -
// (!isTractionUnit(row2.vehicle) ? row2.vehicle.cargoTypes.length || -1 : -1)
// ) * direction
// );
// case 'coldStart':
// return (
// ((isTractionUnit(v1) && v1.coldStart ? 1 : -1) -
// (isTractionUnit(row2.vehicle) && row2.vehicle.coldStart ? 1 : -1)) *
// direction
// );
default:
return v1.type.localeCompare(v2.type) * direction;
}
},
},
computed: {
computedVehicles() {
return this.store.vehicleDataList.filter(this.filterVehicles).sort(this.sortVehicles);
},
},
});
</script>
<style lang="scss" scoped>
@import '../../styles/tab.scss';
.actions {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(10em, 1fr));
flex-wrap: wrap;
gap: 0.5em;
}
.actions > label {
display: flex;
flex-direction: column;
gap: 0.25em;
span {
color: #ccc;
}
}
.vehicles {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 0.5em;
overflow: auto;
max-height: 730px;
margin-top: 0.75em;
padding: 0.25em;
}
.vehicles > li {
display: flex;
gap: 0.5em;
background-color: #161c2e;
padding: 0.5em;
min-height: 75px;
cursor: pointer;
&[data-preview='true'] {
background-color: #435288;
}
& > span {
display: flex;
flex-direction: column;
justify-content: space-between;
overflow: hidden;
}
}
.vehicle-name {
overflow: hidden;
text-wrap: nowrap;
text-overflow: ellipsis;
}
.sponsor-only {
color: $sponsorColor;
&::after {
content: '*';
}
}
.team-only {
color: $teamColor;
&::after {
content: '*';
}
}
.vehicle-props {
color: #ccc;
}
@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>
+58 -41
View File
@@ -1,28 +1,31 @@
<template>
<div class="stock_thumbnails" ref="thumbnailsRef">
<div class="stock-thumbnails" ref="thumbnailsRef">
<div
class="thumbnail-item"
v-for="(stock, stockIndex) in store.stockList"
:key="stockIndex"
:data-selected="store.chosenStockListIndex == stockIndex"
:data-sponsor-only="
stock.vehicleRef.sponsorOnlyTimestamp && stock.vehicleRef.sponsorOnlyTimestamp > Date.now()
"
:data-team-only="stock.vehicleRef.teamOnly"
draggable="true"
@dragstart="onDragStart(stockIndex)"
@drop="onDrop($event, stockIndex)"
@dragover="allowDrop"
@click="onListItemClick(stockIndex)"
>
<span @click="onListItemClick(stockIndex)" :key="stock.id">
<b :class="{ supporter: stock.supportersOnly }">
{{ stock.type }}
</b>
<b>
{{ stock.vehicleRef.type }}
</b>
<span>
<img
draggable="false"
:src="`https://rj.td2.info.pl/dist/img/thumbnails/${stock.type}.png`"
:alt="stock.type"
:title="stock.type"
@error="stockImageError($event, stock)"
/>
</span>
</span>
<img
draggable="false"
:src="`https://rj.td2.info.pl/dist/img/thumbnails/${stock.vehicleRef.type}.png`"
:alt="stock.vehicleRef.type"
:title="stock.vehicleRef.type"
@error="stockImageError($event, stock)"
/>
</div>
</div>
</template>
@@ -43,7 +46,7 @@ const onListItemClick = (index: number) => {
};
const stockImageError = (e: Event, stock: IStock) => {
(e.target as HTMLImageElement).src = `images/${stock.useType}-unknown.png`;
(e.target as HTMLImageElement).src = `images/${stock.vehicleRef.group}-unknown.png`;
};
watch(
@@ -54,7 +57,11 @@ 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',
});
});
}
);
@@ -84,42 +91,52 @@ const allowDrop = (e: DragEvent) => {
</script>
<style lang="scss" scoped>
.stock_thumbnails {
@import '../../styles/global.scss';
.stock-thumbnails {
display: flex;
overflow: auto;
background-color: #353a57;
min-height: 100px;
}
> div {
display: flex;
align-items: flex-end;
cursor: pointer;
min-height: 100px;
.thumbnail-item {
display: flex;
align-items: center;
justify-content: space-between;
&[data-selected='true'] {
background-color: rebeccapurple;
}
flex-direction: column;
gap: 0.5em;
> span {
display: flex;
flex-direction: column;
gap: 0.5em;
padding: 0.5em 0;
padding-top: 0.5em;
text-align: center;
cursor: pointer;
min-height: 100px;
font-size: 0.85em;
font-size: 0.85em;
user-select: none;
-moz-user-select: none;
}
user-select: none;
-moz-user-select: none;
-webkit-user-select: none;
&[data-selected='true'] {
background-color: rebeccapurple;
}
b {
color: #ccc;
margin: 0 1em;
}
&[data-sponsor-only='true'] > b {
color: $sponsorColor;
}
&[data-team-only='true'] > b {
color: $teamColor;
}
img {
max-height: 60px;
}
}
.supporter {
color: salmon;
}
</style>
+11
View File
@@ -0,0 +1,11 @@
{
"EU06": [650000, 2000000],
"EU07": [650000, 2000000],
"4E": [650000, 2000000],
"EU07E": [650000, 2000000],
"EP07": [650000, 650000],
"EP08": [650000, 650000],
"EP09": [800000, 800000],
"ET41": [700000, 4000000],
"SM42": [2400000, 2400000]
}
+52 -29
View File
@@ -1,38 +1,61 @@
{
"regionNumbers": {
"Warszawa": 1,
"Lublin": 2,
"Kraków": 3,
"Sosnowiec": 4,
"Gdańsk": 5,
"Wrocław": 6,
"Poznań": 7,
"Szczecin": 8,
"Rezerwa": 9
"Warszawa (1)": 1,
"Lublin (2)": 2,
"Kraków (3)": 3,
"Sosnowiec (4)": 4,
"Gdańsk (5)": 5,
"Wrocław (6)": 6,
"Poznań (7)": 7,
"Szczecin (8)": 8,
"Rezerwa (9)": 9
},
"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],
"Kraków": [30, 33, 39],
"Sosnowiec": [40, 44, 49, 94],
"Gdańsk": [50, 55, 59, 90, 95, 96],
"Wrocław": [66, 60, 69],
"Poznań": [77, 70, 79],
"Szczecin": [88, 80],
"Rezerwa": [89, 92, 98]
"Warszawa (1)": [10, 11, 19, 91, 93, 97, 99],
"Lublin (2)": [20, 22, 29],
"Kraków (3)": [30, 33, 39],
"Sosnowiec (4)": [40, 44, 49, 94],
"Gdańsk (5)": [50, 55, 59, 90, 95, 96],
"Wrocław (6)": [66, 60, 69],
"Poznań (7)": [77, 70, 79],
"Szczecin (8)": [88, 80],
"Rezerwa (9)": [89, 92, 98]
},
"categories": {
"ekspres krajowy (EI)": "2:00-99:2",
"(między)wojewódzki pośpieszny (MP/RP)": "2:050-169:3",
"wojewódzki osobowy (RO)": "2:200-999:3",
"próżny \"służbowy\" (PW)": "2:6;3:0-899:3",
"towarowy do przewozów masowych (TM)": "2:4;3:0-899:3",
"towarowy do obsługi stacji (TK)": "2:3;3:0-899:3",
"lokomotywa luzem (LT)": "2:5;3:0-899:3"
"categoriesRules": {
"EI": [null, "00", "99"],
"EC": [null, "000", "049"],
"EN": [null, "000", "049"],
"RO": [null, "200", "999"],
"RP": [null, "050", "169"],
"RM": [null, "200", "999"],
"RA": [null, "200", "999"],
"MO": [null, "200", "999"],
"MP": [null, "050", "169"],
"MM": [null, "001", "049"],
"MH": [null, "170", "199"],
"PW": ["6", "000", "899"],
"PX": ["6", "000", "899"],
"TC": ["0", "000", "899"],
"TG": ["1", "000", "899"],
"TR": ["1", "000", "899"],
"TD": ["2", "000", "899"],
"TK": ["3", "000", "899"],
"TN": ["3", "000", "899"],
"TM": ["4", "000", "899"],
"TS": ["5", "000", "899"],
"LT": ["5", "000", "899"],
"LP": ["6", "000", "899"],
"LS": ["9", "000", "899"],
"ZN": ["9", "000", "899"]
}
}
+52 -32
View File
@@ -1,62 +1,82 @@
{
"EU07": {
"passenger": {
"650": 125
"650000": 125
},
"cargo": {
"2000": 70
}
"2000000": 70
},
"none": 110
},
"4E": {
"passenger": {
"650000": 125
},
"cargo": {
"2000000": 70
},
"none": 110
},
"EU07E": {
"passenger": {
"650": 125
"650000": 125
},
"cargo": {
"2000": 70
}
"2000000": 70
},
"none": 110
},
"EP07": {
"passenger": {
"650": 125
"650000": 125
},
"cargo": null
"cargo": null,
"none": 110
},
"EP08": {
"passenger": {
"650": 140
"650000": 140
},
"cargo": null
"cargo": null,
"none": 110
},
"EP09": {
"passenger": {
"650000": 160
},
"cargo": null,
"none": 160
},
"ET41": {
"passenger": {
"700": 125
"700000": 125
},
"cargo": {
"4000": 70
}
"4000000": 70
},
"none": 110
},
"SM42": {
"passenger": {
"95": 90,
"200": 80,
"300": 70,
"450": 60,
"750": 50,
"1130": 40,
"1720": 30,
"2400": 20
"95000": 90,
"200000": 80,
"300000": 70,
"450000": 60,
"750000": 50,
"1130000": 40,
"1720000": 30,
"2400000": 20
},
"cargo": {
"95": 90,
"200": 80,
"300": 70,
"450": 60,
"750": 50,
"1130": 40,
"1720": 30,
"2400": 20
}
"95000": 90,
"200000": 80,
"300000": 70,
"450000": 60,
"750000": 50,
"1130000": 40,
"1720000": 30,
"2400000": 20
},
"none": 90
}
}
-9
View File
@@ -1,9 +0,0 @@
export const enum EVehicleUseType {
LOCO_ELECTRICAL = 'loco-e',
LOCO_DIESEL = "loco-s",
EMU = "loco-ezt",
DMU = "loco-szt",
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' && import.meta.env.DEV
? 'http://localhost:3001'
: 'https://stacjownik.spythere.eu',
});
export default http;
+24
View File
@@ -0,0 +1,24 @@
import localePL from './locales/pl.json';
import localeEN from './locales/en.json';
import { createI18n } from 'vue-i18n';
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,
});
export default i18n;
+203
View File
@@ -0,0 +1,203 @@
{
"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-electric": "ELECTR.",
"loco-diesel": "DIESEL",
"unit-electric": "EMU",
"unit-diesel": "DMU",
"wagon-passenger": "PASSENGER",
"wagon-freight": "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 UNTIL {0}",
"team-only": "* TD2 TEAM ONLY",
"loco-electric": "ELECTRIC LOCO",
"loco-diesel": "DIESEL LOCO",
"unit-electric": "ELECTRIC M.U.",
"unit-diesel": "DIESEL M.U.",
"wagon-passenger": "PASSENGER CARRIAGE",
"wagon-freight": "FREIGHT CARRIAGE",
"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",
"disclaimer": "Theorethical value based on vehicles maximum speed in the current composition. It may be inaccurate in relation to the correct operational speed in certain configurations.",
"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": "Locomotive cold start",
"doublemanning-info": "Double manning",
"list-empty": "Stock list is empty!",
"warning-not-suitable": "EP series 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}",
"warning-team-only-vehicle": "There's at least one vehicle available only for TD2 team members in your stock composition! ({0})",
"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",
"subtitle": "Generates real train number based on Polish railway instruction Ir-11",
"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 (forum thread)",
"td2-wiki-link": "https://td2.info.pl/english-boards/new-train-categories-in-the-simulator/",
"action-random-region": "DRAW REGIONS",
"action-random-number": "DRAW LAST DIGITS",
"action-random-category": "DRAW A CATEGORY",
"rules": {
"first-digit": "First digit:",
"second-digit": "Second digit:",
"third-digit": "Third digit:",
"two-first-digits": "Two first digits:",
"two-last-digits": "Two last digits:",
"three-last-digits": "Three last digits:",
"from-pool": "from pool of",
"for-category": "for category",
"for-region": "for region",
"for-region-begin": "for the beginning construction region",
"for-region-end": "for the terminating construction region",
"from-range": "from range of"
},
"categories": {
"EI": "EI - domestic express",
"EC": "EC - international express",
"EN": "EN - domestic night express",
"MP": "MP - intervoivodeship bullet",
"RP": "RP - voivodeship bullet",
"MO": "MO - intervoivodeship regio",
"RO": "RO - voivodeship regio",
"MM": "MM - international bullet",
"MH": "MH - intervoivodeship bullet (night / hotel)",
"RM": "RM - international voivodeship regio",
"RA": "RA - voivodeship regio (urban)",
"PW": "PW - empty passenger",
"PX": "PX - empty passenger test drive",
"TC": "TC - international freight (intermodal)",
"TG": "TG - international freight (cargo)",
"TR": "TR - international freight (no cargo)",
"TD": "TD - domestic freight (intermodal)",
"TM": "TM - domestic freight (cargo)",
"TN": "TN - domestic freight (no cargo)",
"TK": "TK - freight (stations & sidings)",
"TS": "TS - empty freight test drive",
"LT": "LT - locomotive only",
"LT-new": "LT - freight locomotive only",
"LP": "LP - passenger locomotive only",
"LS": "LS - shunting locomotive",
"ZN": "ZN - inspection / diagnostic"
}
},
"wiki": {
"title": "LIST OF AVAILABLE VEHICLES",
"labels": {
"vehicles": "Vehicles",
"sort-by": "Sort by",
"sort-direction": "Sort direction",
"search-vehicle": "Find a vehicle",
"search-vehicle-placeholder": "Input a vehicle name"
},
"filters": {
"vehicles-all": "all",
"vehicles-traction": "traction units",
"vehicles-wagon": "wagons"
},
"sort-by": {
"type": "name",
"group": "type group",
"length": "length",
"weight": "mass",
"maxSpeed": "speed",
"coldStart": "cold start",
"cargoCount": "cargo count"
},
"sort-direction": {
"asc": "ascending",
"desc": "descending"
},
"unit-electric": "EMU",
"unit-diesel": "DMU",
"loco-diesel": "Diesel locomotive",
"loco-electric": "Electric locomotive",
"wagon-passenger": "Passenger carriage",
"wagon-freight": "Frieght carriage"
},
"realstock": {
"title": "POLISH TRAIN COMPOSITIONS by",
"search-name": "Search by name",
"search-stock": "Search by vehicles",
"action-reset": "RESET"
}
}
+203
View File
@@ -0,0 +1,203 @@
{
"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",
"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-electric": "ELEKTR.",
"loco-diesel": "SPAL.",
"unit-electric": "EZT",
"unit-diesel": "SZT",
"wagon-passenger": "PASAŻERSKIE",
"wagon-freight": "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 DO {0}",
"team-only": "* TYLKO DLA ZESPOŁU TD2",
"loco-electric": "ELEKTROWÓZ",
"loco-diesel": "SPALINOWÓZ",
"unit-electric": "EZT",
"unit-diesel": "SZT",
"wagon-passenger": "WAGON PASAŻERSKI",
"wagon-freight": "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",
"disclaimer": "Wartość poglądowa wzorowana na prędkościach maksymalnych poszczególnych pojazdów w zestawieniu. Może nie zgadzać się z prawdziwymi prędkościami eksploatacyjnymi w konkretnych konfiguracjach.",
"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",
"doublemanning-info": "Podwójna obsada",
"list-empty": "Lista pojazdów jest pusta!",
"warning-not-suitable": "Lokomotywy serii EP 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}",
"warning-team-only-vehicle": "W zestawieniu znajduje się co najmniej jeden pojazd dostępny tylko dla członków zespołu TD2! ({0})",
"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",
"subtitle": "Generuje realny numer pociągu na podstawie instrukcji Ir-11",
"alert": "Numer został skopiowany do twojego schowka!",
"start-region": "Obszar początkowy",
"end-region": "Obszar końcowy",
"train-category": "Kategoria pociągu",
"number-info": "Wygenerowany numer pociągu:",
"warning": "Wybierz kategorię oraz (opcjonalnie) obszary konstrukcyjne",
"td2-wiki": "> Szczegółowe zasady numeracji (wątek forum)",
"td2-wiki-link": "https://td2.info.pl/ogloszenia/nowe-kategorie-pociagow-w-symulatorze/",
"action-random-region": "LOSUJ OBSZARY",
"action-random-number": "LOSUJ KOŃCÓWKĘ",
"action-random-category": "LOSUJ KATEGORIĘ",
"rules": {
"first-digit": "Pierwsza cyfra:",
"second-digit": "Druga cyfra:",
"third-digit": "Trzecia cyfra:",
"two-first-digits": "Dwie pierwsze cyfry:",
"two-last-digits": "Dwie ostatnie cyfry:",
"three-last-digits": "Trzy ostatnie cyfry:",
"from-pool": "z puli",
"for-category": "dla kategorii",
"for-region": "dla obszaru",
"for-region-begin": "dla początkowego obszaru konstrukcyjnego",
"for-region-end": "dla końcowego obszaru konstrukcyjnego",
"from-range": "z przedziału"
},
"categories": {
"EI": "EI - ekspres krajowy",
"EC": "EC - ekspres międzynarodowy",
"EN": "EN - ekspres krajowy nocny",
"MP": "MP - międzywoj. pośpieszny",
"RP": "RP - wojewódzki pośpieszny",
"MO": "MO - międzywoj. osobowy",
"RO": "RO - wojewódzki osobowy",
"MM": "MM - międzynar. pośpieszny",
"MH": "MH - międzywoj. pośpieszny hotelowy",
"RM": "RM - woj. osobowy międzynarodowy",
"RA": "RA - woj. osobowy algomeracyjny",
"PW": "PW - pasażerski próżny - służbowy",
"PX": "PX - pasażerski próżny próbny",
"TC": "TC - towarowy międzynarodowy intermodalny",
"TG": "TG - towarowy międzynarodowy masowy",
"TR": "TR - towarowy międzynarodowy niemasowy",
"TD": "TD - towarowy krajowy intermodalny",
"TM": "TM - towarowy krajowy masowy",
"TN": "TN - towarowy krajowy niemasowy",
"TK": "TK - towarowy (stacje i bocznice)",
"TS": "TS - towarowy próżny próbny",
"LT": "LT - lokomotywa luzem",
"LT-new": "LT - lokomotywa towarowa luzem",
"LP": "LP - lokomotywa pasażerska luzem",
"LS": "LS - lokomotywa manewrowa",
"ZN": "ZN - inspekcyjny / diagnostyczny"
}
},
"wiki": {
"title": "LISTA DOSTĘPNYCH POJAZDÓW",
"labels": {
"vehicles": "Pojazdy",
"sort-by": "Sortuj wg",
"sort-direction": "Kierunek sortowania",
"search-vehicle": "Wyszukaj pojazd",
"search-vehicle-placeholder": "Wpisz nazwę pojazdu"
},
"filters": {
"vehicles-all": "wszystkie",
"vehicles-traction": "trakcyjne",
"vehicles-wagon": "wagony"
},
"sort-by": {
"type": "nazwa",
"group": "rodzaj",
"length": "długość",
"weight": "masa",
"maxSpeed": "prędkość",
"coldStart": "zimny start",
"cargoCount": "ładunki"
},
"sort-direction": {
"asc": "rosnąco",
"desc": "malejąco"
},
"loco-diesel": "Spalinowóz",
"loco-electric": "Elektrowóz",
"unit-electric": "EZT",
"unit-diesel": "SZT",
"wagon-passenger": "Wagon pasażerski",
"wagon-freight": "Wagon towarowy"
},
"realstock": {
"title": "ZESTAWIENIA REALNE by",
"search-name": "Szukaj po nazwie",
"search-stock": "Szukaj po pojazdach",
"action-reset": "RESETUJ"
}
}
+2 -8
View File
@@ -1,14 +1,8 @@
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import { registerSW } from 'virtual:pwa-register';
import App from './App.vue';
import i18n from './i18n-setup';
const pinia = createPinia();
const updateSW = registerSW({
immediate: true,
});
createApp(App).use(pinia).mount('#app');
createApp(App).use(pinia).use(i18n).mount('#app');
+4
View File
@@ -5,5 +5,9 @@ export default defineComponent({
getIconURL(name: string, ext = 'svg'): string {
return `/images/icon-${name}.${ext}`;
},
getThumbnailURL(vehicleType: string, size: 'small' | 'large') {
return `https://static.spythere.eu/images/${vehicleType}--${size == 'small' ? 300 : 800}px.jpg`;
},
},
});
+28 -26
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 { ICarWagon, ILocomotive, IStock, ICargo, IVehicle } from '../types';
import { isTractionUnit } from '../utils/vehicleUtils';
export default defineComponent({
setup() {
@@ -15,38 +15,33 @@ export default defineComponent({
return `${Math.random().toString(36).slice(5)}`;
},
getStockObject(vehicle: Vehicle, cargo?: ICargo | null, count = 1): IStock {
const isLoco = isLocomotive(vehicle);
getStockObject(vehicle: IVehicle, cargo?: ICargo | null): IStock {
return {
id: this.getStockId(),
type: vehicle.type,
length: vehicle.length,
mass: vehicle.mass,
maxSpeed: vehicle.maxSpeed,
isLoco,
cargo: !isLoco && vehicle.loadable && cargo ? cargo : undefined,
count,
imgSrc: vehicle.imageSrc,
useType: isLoco ? vehicle.power : vehicle.useType,
supportersOnly: vehicle.supportersOnly,
constructionType: vehicle.constructionType,
vehicleRef: vehicle,
cargo: !isTractionUnit(vehicle) && vehicle.loadable && cargo ? cargo : undefined,
};
},
addVehicle(vehicle: Vehicle | null, cargo?: ICargo | null) {
addVehicle(vehicle: IVehicle | null, cargo?: ICargo | null) {
if (!vehicle) return;
const stock = this.getStockObject(vehicle, cargo);
if (stock.isLoco && !this.store.stockList[0]?.isLoco) this.store.stockList.unshift(stock);
if (
isTractionUnit(stock.vehicleRef) &&
this.store.stockList.length > 0 &&
!isTractionUnit(this.store.stockList[0].vehicleRef)
)
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 && !isTractionUnit(this.store.stockList[0].vehicleRef))
this.store.stockList.unshift(stockObj);
else this.store.stockList.push(stockObj);
},
@@ -69,26 +64,33 @@ export default defineComponent({
this.store.swapVehicles = false;
stockArray.forEach((type, i) => {
let vehicle: Vehicle | null = null;
let vehicle: IVehicle | null = null;
let vehicleCargo: ICargo | null = null;
if (/^(EU|EP|ET|SM|EN|2EN|SN)/.test(type)) {
const [locoType, coldStart] = type.split(',');
const isTractionUnit = /^([a-zA-Z\d]{0,}-\d{0,})/.test(type);
console.log(type, isTractionUnit);
if (isTractionUnit) {
const [locoType, spawnProps] = type.split(',');
vehicle = this.store.locoDataList.find((loco) => loco.type == locoType) || null;
if (i == 0 && coldStart == 'c') this.store.isColdStart = true;
// Spawn settings
if (i == 0 && spawnProps) {
this.store.isColdStart = spawnProps.includes('c');
this.store.isDoubleManned = spawnProps.includes('d');
}
} else {
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?.cargoTypes.find((c) => c.id == cargo) || null;
}
if (!vehicle) console.log('Brak pojazdu:', type);
if (!vehicle) console.warn('Brak pojazdu / rodzaj pojazdu źle wczytany:', type);
this.addVehicle(vehicle, vehicleCargo);
});
},
},
});
+18 -52
View File
@@ -1,6 +1,7 @@
import { defineComponent } from 'vue';
import { useStore } from '../store';
import { ICarWagon, ILocomotive, IStock, Vehicle } from '../types';
import { ICarWagon, ILocomotive, IStock, IVehicle, LocoGroupType, WagonGroupType } from '../types';
import { isTractionUnit } from '../utils/vehicleUtils';
export default defineComponent({
setup() {
@@ -9,78 +10,44 @@ export default defineComponent({
};
},
computed: {
locoOptions() {
return this.store.locoDataList
.sort((a, b) => (a.type > b.type ? 1 : -1))
.filter((loco) => loco.power == this.store.chosenLocoPower);
},
carOptions() {
return this.store.carDataList
.sort((a, b) => (a.type > b.type ? 1 : -1))
.filter((car) => car.useType == this.store.chosenCarUseType);
},
},
computed: {},
methods: {
selectLocoType(locoTypeId: string) {
this.store.chosenLocoPower = locoTypeId;
this.store.chosenVehicle = this.locoOptions[0];
this.store.chosenLoco = this.locoOptions[0];
},
selectCarWagonType(carWagonTypeId: string) {
this.store.chosenCarUseType = carWagonTypeId;
this.store.chosenVehicle = this.carOptions[0];
this.store.chosenCar = this.carOptions[0];
this.store.chosenCargo = null;
},
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.chosenCargo =
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;
const vehicleRef = stock.vehicleRef;
if (stock.isLoco) {
const chosenLoco = this.store.locoDataList.find((v) => v.type == stock.type) || null;
this.store.chosenVehicle = chosenLoco;
this.store.chosenLoco = chosenLoco;
this.store.chosenVehicle = vehicleRef;
if (isTractionUnit(vehicleRef)) {
this.store.chosenLoco = vehicleRef;
this.store.chosenCargo = null;
this.store.chosenLocoPower = stock.useType;
this.store.chosenLocoGroup = vehicleRef.group as LocoGroupType;
} else {
const chosenCar = this.store.carDataList.find((v) => v.type == stock.type) || null;
this.store.chosenVehicle = chosenCar;
this.store.chosenCar = chosenCar;
this.store.chosenCar = vehicleRef;
this.store.chosenCargo = stock.cargo || null;
this.store.chosenCarUseType = stock.useType;
this.store.chosenCarGroup = vehicleRef.group as WagonGroupType;
}
},
previewLocomotive(loco: ILocomotive) {
this.store.chosenLoco = loco;
this.store.chosenVehicle = loco;
this.store.chosenLocoPower = loco.power;
this.store.chosenLocoGroup = loco.group;
},
previewCarWagon(carWagon: ICarWagon) {
this.store.chosenCar = carWagon;
this.store.chosenCarUseType = carWagon.useType;
this.store.chosenCarGroup = carWagon.group;
this.store.chosenVehicle = carWagon;
this.store.chosenCargo = null;
},
previewVehicle(vehicle: IVehicle) {
if (isTractionUnit(vehicle)) this.previewLocomotive(vehicle);
else this.previewCarWagon(vehicle);
},
resetPreview() {
this.store.chosenVehicle = null;
this.store.chosenCar = null;
@@ -89,4 +56,3 @@ export default defineComponent({
},
},
});
-40
View File
@@ -1,40 +0,0 @@
import { defineComponent } from 'vue';
import { useStore } from '../store';
export default defineComponent({
setup() {
const store = useStore();
return {
store,
};
},
computed: {
trainTooLong() {
return (
(this.store.totalLength > 350 && this.store.isTrainPassenger) ||
(this.store.totalLength > 650 && !this.store.isTrainPassenger)
);
},
trainTooHeavy() {
return this.store.acceptableMass && this.store.totalMass > this.store.acceptableMass;
},
locoNotSuitable() {
return (
!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'))
);
},
tooManyLocomotives() {
return this.store.stockList.reduce((acc, stock) => {
if (stock.isLoco) acc += stock.count;
return acc;
}, 0) > 2;
},
},
});
+120 -41
View File
@@ -1,68 +1,149 @@
import { IStore } from './types';
import {
IVehiclesData,
ICarWagon,
ILocomotive,
ICargo,
IVehicle,
IStock,
IRealComposition,
LocoGroupType,
WagonGroupType,
} from './types';
import { defineStore } from 'pinia';
import {
acceptableMass,
acceptableWeight,
carDataList,
chosenRealStock,
isTractionUnit,
isTrainPassenger,
locoDataList,
maxStockSpeed,
totalLength,
totalMass,
totalWeight,
} from './utils/vehicleUtils';
import i18n from './i18n-setup';
import http from './http';
export const useStore = defineStore({
id: 'store',
state: () =>
({
chosenCar: null,
chosenLoco: null,
chosenCargo: null,
chosenVehicle: null,
state: () => ({
chosenCar: null as ICarWagon | null,
chosenLoco: null as ILocomotive | null,
chosenCargo: null as ICargo | null,
chosenVehicle: null as IVehicle | null,
isColdStart: false,
isColdStart: false,
isDoubleManned: false,
showSupporter: false,
imageLoading: false,
chosenLocoGroup: 'loco-electric' as LocoGroupType,
chosenCarGroup: 'wagon-passenger' as WagonGroupType,
chosenLocoPower: 'loco-e',
chosenCarUseType: 'car-passenger',
stockList: [] as IStock[],
cargoOptions: [] as any[][],
stockList: [],
cargoOptions: [],
swapVehicles: false,
readyStockList: [],
chosenStockListIndex: -1,
swapVehicles: false,
vehiclePreviewSrc: '',
chosenStockListIndex: -1,
chosenRealStockName: undefined,
stockSectionMode: 'stock-list',
vehiclePreviewSrc: '',
isRandomizerCardOpen: false,
isRealStockListCardOpen: false,
stockSectionMode: 'stock-list',
vehiclesData: undefined as IVehiclesData | undefined,
isRandomizerCardOpen: false,
isRealStockListCardOpen: false,
stockData: undefined,
} as IStore),
lastFocusedElement: null as HTMLElement | null,
}),
getters: {
locoDataList: (state) => locoDataList(state),
carDataList: (state) => carDataList(state),
totalMass: (state) => totalMass(state),
totalLength: (state) => totalLength(state),
maxStockSpeed: (state) => maxStockSpeed(state),
isTrainPassenger: (state) => isTrainPassenger(state),
chosenRealStock: (state) => chosenRealStock(state),
acceptableMass: (state) => acceptableMass(state),
locoDataList: (state) => locoDataList(state.vehiclesData),
carDataList: (state) => carDataList(state.vehiclesData),
vehicleDataList: (state) => [
...locoDataList(state.vehiclesData),
...carDataList(state.vehiclesData),
],
totalWeight: (state) => totalWeight(state.stockList),
totalLength: (state) => totalLength(state.stockList),
maxStockSpeed: (state) => maxStockSpeed(state.stockList),
isTrainPassenger: (state) => isTrainPassenger(state.stockList),
acceptableWeight: (state) => acceptableWeight(state.stockList),
realCompositionList: (state) => {
if (!state.vehiclesData) return [];
return Object.keys(state.vehiclesData.realCompositions).reduce<IRealComposition[]>(
(acc, key) => {
const [type, number, ...name] = key.split(' ');
const obj = {
number: number.replace(/_/g, '/'),
name: name.join(' '),
stockString: state.vehiclesData!.realCompositions[key],
type,
};
acc.push({
stockId: `${obj.type} ${obj.number} ${obj.name}`,
...obj,
});
return acc;
},
[]
);
},
stockSupportsColdStart: (state) => {
if (state.stockList.length == 0) return false;
if (!isTractionUnit(state.stockList[0].vehicleRef)) return false;
else if (state.stockList.length > 1) return false;
const headingLoco = state.stockList[0];
return (
state.vehiclesData?.vehicleProps.find(
(stock) => stock.type == headingLoco.vehicleRef.constructionType
)?.coldStart ?? false
);
},
stockSupportsDoubleManning: (state) => {
if (state.stockList.length == 0) return false;
if (!isTractionUnit(state.stockList[0].vehicleRef)) return false;
const headingLoco = state.stockList[0];
return (
state.vehiclesData?.vehicleProps.find(
(stock) => stock.type == headingLoco.vehicleRef.constructionType
)?.doubleManned ?? false
);
},
},
actions: {
async fetchStockInfoData() {
const stockData = await (await fetch(`https://spythere.github.io/api/td2/data/stockInfo.json`)).json();
this.stockData = stockData;
async fetchVehiclesAPI() {
try {
const vehiclesData = (await http.get<IVehiclesData>('/vehicles')).data;
this.vehiclesData = vehiclesData;
} catch (error) {
console.error(error);
}
},
async setupAPIData() {
await this.fetchVehiclesAPI();
this.mergeBackendTranslations();
},
async mergeBackendTranslations() {
if (!this.vehiclesData) return;
i18n.global.mergeLocaleMessage('pl', this.vehiclesData.vehicleLocales.pl);
i18n.global.mergeLocaleMessage('en', this.vehiclesData.vehicleLocales.en);
},
handleRouting() {
@@ -82,5 +163,3 @@ export const useStore = defineStore({
},
},
});
+331 -256
View File
@@ -1,256 +1,331 @@
@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;
}
&--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;
}
}
$breakpointMd: 960px;
$breakpointSm: 550px;
$bgColor: #2b3552;
$textColor: #fff;
$secondaryColor: #1b1b1b;
$accentColor: #e4c428;
$sponsorColor: gold;
$teamColor: #ff4848;
@font-face {
font-family: 'Lato';
src:
url('/fonts/Lato-Light.woff2') format('woff2'),
url('/fonts/Lato-Light.woff') format('woff');
font-weight: 300;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Lato';
src:
url('/fonts/Lato-Bold.woff2') format('woff2'),
url('/fonts/Lato-Bold.woff') format('woff');
font-weight: bold;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Lato';
src:
url('/fonts/Lato-Regular.woff2') format('woff2'),
url('/fonts/Lato-Regular.woff') format('woff');
font-weight: normal;
font-style: normal;
font-display: swap;
}
::-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;
}
}
[data-tooltip]:hover::after,
[data-tooltip]:focus::after {
position: absolute;
transform: translateX(10px);
content: attr(data-tooltip);
color: white;
background: black;
padding: 0.5em;
max-width: 300px;
z-index: 100;
}
[data-tooltip] {
cursor: pointer;
}
.btn {
padding: 0.4em 0.75em;
outline: none;
background-color: $secondaryColor;
border-radius: 8px;
font-weight: bold;
transition:
color 150ms,
background-color 150ms;
&:hover {
color: $accentColor;
}
&.btn--outline {
background: none;
font-weight: bold;
outline: 1px solid $accentColor;
}
&:focus-visible {
color: $accentColor;
outline: 1px solid white;
}
&[data-chosen='true'] {
background-color: $accentColor;
color: black;
box-shadow: 0 0 5px 1px $accentColor;
}
&[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: $bgColor;
border: 2px solid #aaa;
outline: none;
padding: 0.25em 0.35em;
height: 100%;
color: white;
font-size: 1em;
&: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;
}
}
hr {
height: 3px;
background-color: white;
outline: none;
margin: 0;
}
.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;
}
}
+7 -9
View File
@@ -8,12 +8,18 @@
padding: 0.5em 1em;
background-color: $secondaryColor;
text-align: center;
h2 {
margin: 0;
color: white;
font-size: 1.35em;
text-align: center;
}
h3 {
margin: 0.5em 0 0 0;
font-size: 1.15em;
color: #aaa;
}
button {
@@ -58,13 +64,6 @@
}
}
hr {
height: 3px;
background-color: white;
outline: none;
margin: 0;
}
@media only screen and (max-width: 470px) {
.tab_attributes {
label {
@@ -77,4 +76,3 @@ hr {
}
}
}
+46 -79
View File
@@ -1,123 +1,90 @@
export type Vehicle = ILocomotive | ICarWagon;
export type IVehicle = ILocomotive | ICarWagon;
export type StockSectionMode = 'STOCK_LIST' | 'STOCK_GENERATOR';
export interface IStore {
chosenCar: ICarWagon | null;
chosenLoco: ILocomotive | null;
chosenCargo: ICargo | null;
chosenVehicle: Vehicle | null;
export type LocoGroupType = 'loco-electric' | 'loco-diesel' | 'unit-electric' | 'unit-diesel';
export type WagonGroupType = 'wagon-passenger' | 'wagon-freight';
export type VehicleGroupType = LocoGroupType | WagonGroupType;
export type RestrictionType = 'sponsorOnly' | 'teamOnly';
isColdStart: boolean;
showSupporter: boolean;
imageLoading: boolean;
chosenLocoPower: string;
chosenCarUseType: string;
stockList: IStock[];
readyStockList: IReadyStockItem[];
cargoOptions: any[][];
chosenStockListIndex: number;
chosenRealStockName?: string;
swapVehicles: boolean;
vehiclePreviewSrc: string;
isRandomizerCardOpen: boolean;
isRealStockListCardOpen: boolean;
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 interface IStockProps {
export interface IVehicleProps {
type: string;
speed: number;
length: number;
mass: number;
cargo: string;
weight: number;
cargoTypes?: ICargo[];
coldStart?: boolean;
doubleManned?: boolean;
}
export interface IStockData {
version: string;
export interface ICargo {
id: string;
weight: number;
}
export interface IVehiclesData {
simulatorVersion: string;
generator: {
passenger: any;
cargo: {
[key: string]: string[];
};
};
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][];
vehicleList: any[][];
vehicleProps: IVehicleProps[];
vehicleLocales: {
pl: {
cargo: Record<string, string>;
usage: Record<string, string>;
};
en: {
cargo: Record<string, string>;
usage: Record<string, string>;
};
};
props: IStockProps[];
usage: { [key: string]: string };
realCompositions: Record<string, string>;
}
export interface ILocomotive {
type: string;
power: string;
group: LocoGroupType;
constructionType: string;
cabinType: string;
maxSpeed: number;
supportersOnly: boolean;
imageSrc: string;
mass: number;
weight: number;
length: number;
coldStart: boolean;
doubleManned: boolean;
sponsorOnlyTimestamp: number;
teamOnly: boolean;
}
export interface ICarWagon {
//"203V_PKPC_Fll_01","203V",true,false,"100",img
type: string;
useType: 'car-passenger' | 'car-cargo';
group: WagonGroupType;
constructionType: string;
loadable: boolean;
supportersOnly: boolean;
maxSpeed: number;
imageSrc: string;
mass: number;
weight: number;
length: number;
cargoList: { id: string; totalMass: number }[];
}
export interface ICargo {
id: string;
totalMass: number;
cargoTypes: ICargo[];
sponsorOnlyTimestamp: number;
teamOnly: boolean;
}
export interface IStock {
id: string;
type: string;
useType: string;
constructionType: string;
length: number;
mass: number;
maxSpeed: number;
cargo?: { id: string; totalMass: number };
isLoco: boolean;
supportersOnly: boolean;
count: number;
imgSrc?: string;
vehicleRef: IVehicle;
cargo?: ICargo;
}
export interface IReadyStockItem {
export interface IRealComposition {
stockId: string;
stockString: string;
type: string;
number: string;
name: string;
}
-5
View File
@@ -1,5 +0,0 @@
const supportedConstructions = ['303e', '203e'];
export function locoSupportsColdStart(constructionType: string) {
return new RegExp(`(${supportedConstructions.join('|')})`).test(constructionType);
}
-14
View File
@@ -1,14 +0,0 @@
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'];
if (!speedTable) return undefined;
let speedLimit = 0;
for (let mass in speedTable) if (stockMass > Number(mass)) speedLimit = (speedTable as any)[mass];
return speedLimit;
};
+33
View File
@@ -0,0 +1,33 @@
import speedLimits from '../constants/speedLimits.json';
import massLimits from '../constants/massLimits.json';
export type SpeedLimitLocoType = keyof typeof speedLimits;
export type MassLimitLocoType = keyof typeof massLimits;
export function calculateSpeedLimit(
locoType: SpeedLimitLocoType,
stockTotalWeight: number,
stockCount: number,
isTrainPassenger: boolean
) {
if (speedLimits[locoType] === undefined) return 0;
if (stockCount == 1) return speedLimits[locoType]['none'];
const stockType = isTrainPassenger ? 'passenger' : 'cargo';
const speedTable = speedLimits[locoType][stockType];
if (!speedTable) return undefined;
let speedLimit = 0;
for (const mass in speedTable)
if (stockTotalWeight > Number(mass)) speedLimit = (speedTable as any)[mass];
return speedLimit;
}
export function calculateMassLimit(locoType: MassLimitLocoType, isTrainPassenger: boolean) {
if (massLimits[locoType] === undefined) return 0;
return massLimits[locoType][isTrainPassenger ? 0 : 1] || 0;
}
+95 -110
View File
@@ -1,161 +1,146 @@
import { EVehicleUseType } from '../enums/EVehicleUseType';
import { ICarWagon, ILocomotive, IStore, TStockInfoKey } from '../types';
import { LocoType, calculateSpeedLimit } from './speedLimitUtils';
import {
ICarWagon,
ILocomotive,
IStock,
IVehiclesData,
LocoGroupType,
WagonGroupType,
} from '../types';
import {
MassLimitLocoType,
SpeedLimitLocoType,
calculateMassLimit,
calculateSpeedLimit,
} from './vehicleLimitsUtils';
export function isLocomotive(vehicle: ILocomotive | ICarWagon): vehicle is ILocomotive {
return (vehicle as ILocomotive).power !== undefined;
export function isTractionUnit(vehicle: ILocomotive | ICarWagon): vehicle is ILocomotive {
return (vehicle as ILocomotive).cabinType !== undefined;
}
export function locoDataList(state: IStore) {
if (!state.stockData) return [];
export function locoDataList(vehiclesData: IVehiclesData | undefined) {
if (!vehiclesData) return [];
const stockData = state.stockData;
return vehiclesData.vehicleList.reduce<ILocomotive[]>((acc, vehicleInfoArray) => {
// check if data array has 5 elements (locos & units only)
if (vehicleInfoArray.length != 5) return acc;
return Object.keys(stockData.info).reduce((acc, vehiclePower) => {
if (!vehiclePower.startsWith('loco')) return acc;
const [type, constructionType, cabinType, group, restrictions] = vehicleInfoArray;
const locoProps = vehiclesData.vehicleProps.find((prop) => constructionType == prop.type);
const locoVehiclesData = stockData.info[vehiclePower as 'loco-e' | 'loco-s' | 'loco-ezt' | 'loco-szt'];
if (!locoProps) {
console.warn('Brak atrybutów dla pojazdu:', type);
return acc;
}
locoVehiclesData.forEach((loco) => {
if (state.showSupporter && !loco[4]) return;
acc.push({
group: group as LocoGroupType,
const [type, constructionType, cabinType, maxSpeed, supportersOnly] = loco;
const locoProps = stockData.props.find((prop) => constructionType == prop.type);
type,
constructionType,
cabinType,
acc.push({
power: vehiclePower,
type,
constructionType,
cabinType,
maxSpeed: Number(maxSpeed),
supportersOnly,
imageSrc: '',
sponsorOnlyTimestamp: restrictions?.sponsorOnly ?? 0,
teamOnly: restrictions?.teamOnly ?? false,
length: locoProps?.length && type.startsWith('2EN') ? locoProps.length * 2 : locoProps?.length || 0,
mass: locoProps?.mass && type.startsWith('2EN') ? 253 : locoProps?.mass || 0,
});
maxSpeed: locoProps.speed,
length: locoProps.length,
weight: locoProps.weight,
coldStart: locoProps.coldStart ?? false,
doubleManned: locoProps.doubleManned ?? false,
});
return acc;
}, [] as ILocomotive[]);
}, []);
}
export function carDataList(state: IStore) {
if (!state.stockData) return [];
export function carDataList(vehiclesData: IVehiclesData | undefined) {
if (!vehiclesData) return [];
const stockData = state.stockData;
return vehiclesData.vehicleList.reduce<ICarWagon[]>((acc, vehicleInfoArray) => {
// check if data array has 4 elements (wagons only)
if (vehicleInfoArray.length != 4) return acc;
return Object.keys(stockData.info).reduce((acc, vehicleUseType) => {
if (!vehicleUseType.startsWith('car')) return acc;
const [type, constructionType, group, restrictions] = vehicleInfoArray;
const wagonProps = vehiclesData.vehicleProps.find((prop) => constructionType == prop.type);
const carVehiclesData = stockData.info[vehicleUseType as 'car-passenger' | 'car-cargo'];
if (!wagonProps) {
console.warn('Brak atrybutów dla pojazdu:', type);
return acc;
}
carVehiclesData.forEach((car) => {
if (state.showSupporter && !car[3]) return;
acc.push({
group: group as WagonGroupType,
type,
constructionType,
loadable: wagonProps.cargoTypes ? wagonProps.cargoTypes.length > 0 : false,
cargoTypes: wagonProps?.cargoTypes ?? [],
const carPropsData = stockData.props.find((v) => car[0].toString().startsWith(v.type));
sponsorOnlyTimestamp: restrictions?.sponsorOnly ?? 0,
teamOnly: restrictions?.teamOnly ?? false,
acc.push({
useType: vehicleUseType as 'car-passenger' | 'car-cargo',
type: car[0],
constructionType: car[1],
loadable: car[2],
supportersOnly: car[3],
maxSpeed: Number(car[4]),
imageSrc: '',
cargoList:
!carPropsData || carPropsData.cargo === null
? []
: carPropsData.cargo.split(';').map((cargo) => ({
id: cargo.split(':')[0],
totalMass: Number(cargo.split(':')[1]),
})),
mass: carPropsData?.mass || 0,
length: carPropsData?.length || 0,
});
maxSpeed: wagonProps.speed,
weight: wagonProps?.weight || 0,
length: wagonProps?.length || 0,
});
return acc;
}, [] as ICarWagon[]);
}, []);
}
export function totalMass(state: IStore) {
return ~~state.stockList.reduce(
(acc, stock) => acc + (stock.cargo ? stock.cargo.totalMass : stock.mass) * stock.count,
export function totalWeight(stockList: IStock[]) {
return stockList.reduce(
(acc, stock) => acc + (stock.vehicleRef.weight + (stock.cargo?.weight ?? 0)),
0
);
}
export function totalLength(state: IStore) {
return state.stockList.reduce((acc, stock) => acc + stock.length * stock.count, 0);
export function totalLength(stockList: IStock[]) {
return stockList.reduce((acc, stock) => acc + stock.vehicleRef.length, 0);
}
export function maxStockSpeed(state: IStore) {
const stockSpeedLimit = state.stockList.reduce(
(acc, stock) => (stock.maxSpeed < acc || acc == 0 ? stock.maxSpeed : acc),
export function maxStockSpeed(stockList: IStock[]) {
const stockSpeedLimit = stockList.reduce(
(acc, stock) => (stock.vehicleRef.maxSpeed < acc || acc == 0 ? stock.vehicleRef.maxSpeed : acc),
0
);
const headingLoco = state.stockList[0]?.isLoco ? state.stockList[0] : undefined;
const headingLoco =
stockList[0] && isTractionUnit(stockList[0].vehicleRef) ? stockList[0] : undefined;
if (!headingLoco) return stockSpeedLimit;
const locoType = headingLoco.type.split('-')[0];
const locoType = headingLoco.vehicleRef.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 SpeedLimitLocoType,
totalWeight(stockList),
stockList.length,
isTrainPassenger(stockList)
);
return speedLimitByMass ? Math.min(stockSpeedLimit, speedLimitByMass) : stockSpeedLimit;
}
export function acceptableMass(state: IStore) {
if (state.stockList.length == 0 || !state.stockList[0].isLoco) return 0;
const activeLocomotiveType = state.stockList[0].type;
export function acceptableWeight(stockList: IStock[]) {
if (stockList.length == 0 || !isTractionUnit(stockList[0].vehicleRef)) return 0;
if (/^SM/.test(activeLocomotiveType)) return 2400;
const activeLocomotiveType = stockList[0].vehicleRef.type.split('-')[0];
// Elektryczne EU07 / EP07 / EP08 / ET41
const locoMassLimit = calculateMassLimit(
activeLocomotiveType as MassLimitLocoType,
isTrainPassenger(stockList)
);
// Pasażerski elektr.
if (isTrainPassenger(state)) {
if (/^(EU|EP)/.test(activeLocomotiveType)) return 650;
if (/^ET/.test(activeLocomotiveType)) return 700;
return 0;
}
// Towarowy / inny elektr.
if (/^EU/.test(activeLocomotiveType)) return 2000;
if (/^ET/.test(activeLocomotiveType)) return 4000;
if (/^EP/.test(activeLocomotiveType)) return 650;
return 0;
return locoMassLimit;
}
export function isTrainPassenger(state: IStore) {
if (state.stockList.length == 0) return false;
if (state.stockList.every((stock) => stock.isLoco)) return false;
export function isTrainPassenger(stockList: IStock[]) {
if (stockList.length == 0) return false;
if (stockList.every((stock) => isTractionUnit(stock.vehicleRef))) return false;
return state.stockList
.filter((stock) => !stock.isLoco)
.every((stock) => stock.useType === EVehicleUseType.CAR_PASSENGER);
return stockList
.filter((stock) => !isTractionUnit(stock.vehicleRef))
.every((stock) => stock.vehicleRef.group === 'wagon-passenger');
}
export function chosenRealStock(state: IStore) {
const currentStockString = state.stockList
.reduce((acc, stock) => {
for (let i = 0; i < stock.count; i++) acc.push(stock.type);
return acc;
}, [] as string[])
.join(';');
const realStockObj = state.readyStockList.find((readyStock) => readyStock.stockString == currentStockString);
state.chosenRealStockName = realStockObj?.stockId ?? undefined;
return realStockObj;
}
+37 -35
View File
@@ -1,35 +1,37 @@
<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 {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
min-height: 100vh;
padding: 0.5em;
}
</style>
+3 -3
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
import type { DefineComponent } from 'vue';
const component: DefineComponent<{}, {}, any>;
export default component;
}
+20 -7
View File
@@ -6,35 +6,48 @@ import { VitePWA } from 'vite-plugin-pwa';
// https://vitejs.dev/config/
export default defineConfig({
server: {
port: 2137,
port: 2138,
},
plugins: [
vue(),
VitePWA({
registerType: 'autoUpdate',
includeAssets: ['/images/*.{png,svg,jpg}', '/fonts/*.{woff,woff2,ttf}'],
devOptions: {
suppressWarnings: true,
enabled: true,
},
workbox: {
// globPatterns: ['**/*.{js,css,html,png,svg,img}'],
globPatterns: ['**/*.{js,css,html,jpg,png,svg,img,woff,woff2}'],
runtimeCaching: [
{
urlPattern: /^https:\/\/rj.td2.info.pl\/dist\/img\/thumbnails\/.*/i,
urlPattern: new RegExp('^https://rj.td2.info.pl/dist/img/thumbnails/*', 'i'),
handler: 'CacheFirst',
options: {
cacheName: 'swdr-images-cache',
expiration: {
maxEntries: 50,
maxAgeSeconds: 60 * 60 * 24 * 7, // <== 7 days
maxAgeSeconds: 60 * 60 * 24, // <== 1 day
},
cacheableResponse: {
statuses: [404],
statuses: [200],
},
},
},
{
urlPattern: new RegExp('^https://stacjownik.spythere.eu/vehicles', 'i'),
handler: 'NetworkFirst',
options: {
cacheName: 'vehicles-cache',
},
},
],
},
}),
],
});
-2958
View File
File diff suppressed because it is too large Load Diff