51 Commits

Author SHA1 Message Date
YouHaveTrouble c828496d1a bump version 2025-12-20 21:45:56 +01:00
YouHaveTrouble 9e5580836c sync the timers with sorting to avoid weird desyncs between them 2025-12-20 21:21:09 +01:00
YouHaveTrouble ea74bbea90 this is a miner node 2025-12-20 20:25:50 +01:00
YouHaveTrouble bc0e866956 bump version 2025-12-20 18:44:58 +01:00
YouHaveTrouble 6beca6a8ea don't render invisible nodes if possible 2025-12-20 18:34:24 +01:00
YouHaveTrouble fe8e7eb098 display time left to node expiry on active nodes 2025-12-20 18:30:40 +01:00
YouHaveTrouble 98008ca288 update depends 2025-12-20 17:58:13 +01:00
YouHaveTrouble 1db76089bc try a bit older vite 2025-04-23 22:52:57 +02:00
YouHaveTrouble 001a8bab3b Revert "update vite"
This reverts commit 56b053fc7f.
2025-04-23 22:51:13 +02:00
YouHaveTrouble 56b053fc7f update vite 2025-04-23 22:47:45 +02:00
YouHaveTrouble f2fb0571cc bump version 2025-04-23 22:46:46 +02:00
YouHaveTrouble e61bc9480a fix lint warnings 2025-04-23 22:46:33 +02:00
YouHaveTrouble d445b59422 google ads 2025-04-23 22:46:22 +02:00
YouHaveTrouble c652821b85 bump version 2025-03-22 23:46:28 +01:00
YouHaveTrouble 44c2ec5b25 fixed default filters not being serialized via serialize() method 2025-03-22 23:44:39 +01:00
YouHaveTrouble 3ff7d43086 fix default filters not parsing correctly 2025-03-22 23:37:59 +01:00
YouHaveTrouble 521da22993 added call to action pointing to contributing doc 2025-02-16 20:40:27 +01:00
YouHaveTrouble 3e26ee2559 add CONTRIBUTING.MD 2025-02-16 20:35:41 +01:00
YouHaveTrouble 05de2e5fcd bump version 2025-02-16 19:56:03 +01:00
YouHaveTrouble c0379df157 add a few more nodes 2025-02-16 19:55:42 +01:00
YouHaveTrouble 0a9fd617e9 node type filter done 2025-02-16 19:55:19 +01:00
YouHaveTrouble 52b7aa13af display version in footer 2025-02-16 19:55:07 +01:00
YouHaveTrouble 0807b8fb86 add debug to data parser 2025-02-16 19:44:18 +01:00
YouHaveTrouble 7cd860d44b always display one decimal in aetheryte coords 2025-02-16 19:29:52 +01:00
YouHaveTrouble 2566f836bb add node type filter 2025-02-16 19:21:36 +01:00
YouHaveTrouble cf8a918ed9 add missing zone aetheryte list and missing node 2025-02-16 18:43:53 +01:00
YouHaveTrouble 036a9cc829 add paddings 2025-02-16 18:41:28 +01:00
YouHaveTrouble 508b7d9acf bump version 2024-07-18 21:29:15 +02:00
YouHaveTrouble cf3bc7b463 complete(?) endwalker data for unspoiled nodes 2024-07-18 21:22:46 +02:00
YouHaveTrouble a50ea1273d zone data now contains aetheryte list 2024-07-18 19:59:05 +02:00
YouHaveTrouble b3ec17f2c0 version bump 2024-07-15 23:25:08 +02:00
YouHaveTrouble c00e4178c5 fix nearest aetheryte finder 2024-07-15 21:09:14 +02:00
YouHaveTrouble f7ac1725f9 versionino bumperino 2024-07-15 20:17:41 +02:00
YouHaveTrouble 07084d36d0 add a little arrow and animation when collapsing and uncollapsing filter groups 2024-07-15 20:13:03 +02:00
YouHaveTrouble 07d041b974 add metadata 2024-07-15 20:12:33 +02:00
YouHaveTrouble 634ae2b457 updates 2024-07-15 18:51:08 +02:00
YouHaveTrouble 592f941a1c better default filters 2024-07-15 18:51:01 +02:00
YouHaveTrouble 7629c9f339 update readme 2024-07-15 18:30:56 +02:00
YouHaveTrouble f0b38d82ba add link to gh repo 2024-07-15 18:30:49 +02:00
YouHaveTrouble 3aec40658f filter shenanigans 2024-07-15 18:14:02 +02:00
YouHaveTrouble be9cffef4f update vite 2024-07-15 18:12:44 +02:00
YouHaveTrouble d1cdceb620 update lockfile 2024-07-15 00:09:56 +02:00
YouHaveTrouble 281b164212 bump version 2024-07-15 00:09:11 +02:00
YouHaveTrouble 36775e45db add most unspoiled dawntrail nodes 2024-07-15 00:06:16 +02:00
YouHaveTrouble 0ef5d72275 fix up some things 2024-07-14 23:45:26 +02:00
YouHaveTrouble 0649004e51 read the readme 2023-10-12 18:03:00 +02:00
YouHaveTrouble 4f44eb58c7 newlines at the end of files 2023-10-12 17:45:47 +02:00
YouHaveTrouble 1308ef8d44 don't wanna eslint node_modules 2023-10-12 17:39:28 +02:00
YouHaveTrouble aa7b1bbdec test code on push 2023-10-12 17:30:51 +02:00
YouHaveTrouble 388e1d853a rename deploy workflow 2023-10-12 17:24:33 +02:00
YouHaveTrouble eb78ce7b6f install and configure eslint, bow before eslint 2023-10-12 17:20:28 +02:00
29 changed files with 4321 additions and 469 deletions
+2 -2
View File
@@ -1,4 +1,4 @@
name: Build Vue
name: Build and deploy
on:
release:
types: [published]
@@ -13,4 +13,4 @@ jobs:
with:
username: 'YouHaveTrouble'
reponame: 'DiscipleOfLand'
token: ${{ secrets.GITHUB_TOKEN }}
token: ${{ secrets.GITHUB_TOKEN }}
+13
View File
@@ -0,0 +1,13 @@
name: Test
on: [push]
jobs:
build_vue:
runs-on: ubuntu-latest
name: Build and lint
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Install
run: npm install
- name: Build and lint
run: npm run build
+1 -1
View File
@@ -1 +1 @@
dol.yht.one
dol.yht.one
+141
View File
@@ -0,0 +1,141 @@
# How to contribute
All meaningful contributions are welcome. If you're not sure about something,
open an issue or [join the discord server](https://discord.youhavetrouble.me/)
to talk it out.
## Reporting issues
If you find a bug or unexpected behavior, open an issue. Include information about
how to reproduce the issue, and any relevant error messages from your browser's
console.
## Pull requests
PLEASE OPEN AN ISSUE OR DISCUSS ON DISCORD BEFORE MAKING A PULL REQUEST.
Noone likes to waste time on a PR that won't be accepted, so please ask first!
## Inputting data
Entire project is data-driven and contributions can be made either via pull request or
submitting a json file with the data formatted as described below. If anything's unclear,
[ask on discord](https://discord.youhavetrouble.me/).
### Items
You can see the existing data [here](https://github.com/YouHaveTrouble/DiscipleOfLand/blob/master/public/data/items.json).
Item data adheres to following json schema:
```json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"patternProperties": {
"^[a-zA-Z0-9-]+$": {
"type": "object",
"properties": {
"name": { "type": "string" },
"level": { "type": "integer" },
"stars": { "type": "integer" },
"perception": { "type": "integer" }
},
"required": ["name", "level"]
}
},
"additionalProperties": false
}
```
### Zones
You can see the existing data [here](https://github.com/YouHaveTrouble/DiscipleOfLand/blob/master/public/data/zones.json).
Zone data adheres to following json schema:
```json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"patternProperties": {
"^[a-zA-Z0-9-]+$": {
"type": "object",
"properties": {
"name": {
"type": "object",
"properties": {
"en": { "type": "string" }
},
"required": ["en"]
},
"aetherytes": {
"type": "array",
"items": {
"type": "object",
"properties": {
"position": {
"type": "object",
"properties": {
"x": { "type": "number" },
"y": { "type": "number" }
},
"required": ["x", "y"]
},
"name": {
"type": "object",
"properties": {
"en": { "type": "string" }
},
"required": ["en"]
}
},
"required": ["position", "name"]
}
}
},
"required": ["name", "aetherytes"]
}
},
"additionalProperties": false
}
```
### Nodes
You can see the existing data [here](https://github.com/YouHaveTrouble/DiscipleOfLand/blob/master/public/data/nodes.json).
Node data adheres to following json schema:
```json
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"properties": {
"job": {
"type": "string",
"enum": ["botanist", "miner"]
},
"type": {
"type": "string",
"enum": ["unspoiled", "legendary"]
},
"position": {
"type": "object",
"properties": {
"zone": { "type": "string" },
"x": { "type": "number" },
"y": { "type": "number" }
},
"required": ["zone", "x", "y"]
},
"times": {
"type": "array",
"items": { "type": "string", "pattern": "^\\d{2}:\\d{2}-\\d{2}:\\d{2}$" },
"minItems": 2,
"maxItems": 2
},
"items": {
"type": "array",
"items": { "type": "string" }
}
},
"required": ["job", "type", "position", "times", "items"]
}
```
`times` is an array of two strings representing the time the node is available in Eorzea time.
First value is the time the node becomes available, second value is the time the node disappears.
`items` elements are ids corresponding to the items in the items.json file.
+24
View File
@@ -0,0 +1,24 @@
# Disciple of Land
I deemed existing gathering timers to be inadequate, so I made my own.
This is currently still missing a lot of functionality and data.
## 1.0 checklist
- [x] Displaying gathering nodes
- [x] Displaying next time gathering node will be available
- [x] Displaying gathering node type
- [x] Displaying node's items and their level
- [x] Sorting based on time when node will be available
- [x] Filter and sorting section/popup
- [x] Filtering based on job
- [x] Filtering based on level
- [x] Filtering based on node type (legendary, ephemeral, etc.)
- [ ] Fully input level 80-100 nodes data
## Nice to have checklist
- [ ] Fully input level 70-80 nodes data
- [ ] Fully input level 60-70 nodes data
- [ ] Fully input level 50-60 nodes data
- [ ] Better mobile support (pwa?)
BIN
View File
Binary file not shown.
+22 -6
View File
@@ -1,13 +1,29 @@
<!DOCTYPE html>
<html lang="en">
<head>
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Disciple of Land</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
<meta property="og-title" content="Disciple of Land">
<meta property="twitter:title" content="Disciple of Land">
<meta property="og-type" content="website">
<meta name="description" content="Track timed gathering nodes in Final Fantasy XIV.">
<meta property="og-description" content="Track timed gathering nodes in Final Fantasy XIV.">
<meta property="twitter:description" content="Track timed gathering nodes in Final Fantasy XIV.">
<meta property="og-url" content="https://dol.yht.one">
<meta property="twitter:url" content="https://dol.yht.one">
<script
async
src="https://pagead2.googlesyndication.com/pagead/js/adsbygoogle.js?client=ca-pub-8262480617556085"
crossorigin="anonymous"
></script>
</head>
<body>
<noscript>
<strong>We're sorry but Disciple of Land doesn't work without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
+2539 -208
View File
File diff suppressed because it is too large Load Diff
+33 -5
View File
@@ -1,19 +1,47 @@
{
"name": "discipleofland",
"version": "0.0.1",
"version": "0.1.1",
"private": true,
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build && cp -r CNAME dist/CNAME",
"preview": "vite preview"
},
"dependencies": {
"sass": "^1.67.0",
"typescript": "^5.2.2",
"vue": "^3.3.4"
},
"devDependencies": {
"@vitejs/plugin-vue": "^4.2.3",
"vite": "^4.3.9"
"sass": "^1.67.0",
"typescript": "^5.2.2",
"@typescript-eslint/eslint-plugin": "^6.7.5",
"@typescript-eslint/parser": "^6.7.5",
"@vitejs/plugin-vue": "^5.0.5",
"@vue/eslint-config-typescript": "^12.0.0",
"eslint": "^8.51.0",
"eslint-plugin-vue": "^9.17.0",
"vite": "^5.4.18",
"vue-eslint-parser": "^9.3.2",
"vite-plugin-eslint": "^1.8.1"
},
"eslintConfig": {
"root": true,
"parser": "vue-eslint-parser",
"extends": [
"plugin:vue/strongly-recommended",
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"@vue/typescript/recommended"
],
"plugins": ["@typescript-eslint"],
"parserOptions": {
"sourceType": "module",
"ecmaVersion": 2020
},
"ignorePatterns": [
"dist/",
"node_modules/",
".github/"
]
}
}
-63
View File
@@ -1,63 +0,0 @@
[
{
"position": {
"zone": "labyrinthos",
"x": 30.3,
"y": 11.9
},
"name": {
"en": "The Archeion"
}
},
{
"position": {
"zone": "labyrinthos",
"x": 21.6,
"y": 20.4
},
"name": {
"en": "Sharlayan Hamlet"
}
},
{
"position": {
"zone": "labyrinthos",
"x": 6.8,
"y": 27.5
},
"name": {
"en": "Aporia"
}
},
{
"position": {
"zone": "thavnair",
"x": 29.5,
"y": 16.5
},
"name": {
"en": "Palaka's Stand"
}
},
{
"position": {
"zone": "thavnair",
"x": 10.9,
"y": 22.2
},
"name": {
"en": "The Great Work"
}
},
{
"position": {
"zone": "thavnair",
"x": 25.3,
"y": 34.0
},
"name": {
"en": "Yedlihmad"
}
}
]
+130 -14
View File
@@ -1,22 +1,138 @@
{
"rarefied-iceberg-lettuce": {
"name": "Rarefied Iceberg Lettuce",
"scrip-color": "purple",
"rarefied-sykon": {
"name": "Rarefied Sykon",
"level": 87
},
"rarefied-elder-nutmeg": {
"name": "Rarefied Elder Nutmeg",
"level": 90
},
"rarefied-dark-rye": {
"name": "Rarefied Dark Rye",
"scrip-color": "white",
"level": 89
},
"rarefied-palm-log": {
"name": "Rarefied Palm Log",
"scrip-color": "white",
"level": 81
},
"rarefied-coconut": {
"name": "Rarefied Coconut",
"scrip-color": "white",
"level": 85
},
"rarefied-palm-log": {
"name": "Rarefied Palm Log",
"level": 85
},
"rarefied-red-pine-log": {
"name": "Rarefied Red Pine Log",
"level": 83
},
"rarefied-dark-rye": {
"name": "Rarefied Dark Rye",
"level": 89
},
"rarefied-iceberg-lettuce": {
"name": "Rarefied Iceberg Lettuce",
"level": 90,
"stars": 1
},
"rarefied-ar-cean-cotton-boll": {
"name": "Rarefied AR-Cean Cotton Boll",
"level": 90,
"stars": 1
},
"rarefied-sharlayan-rock-salt": {
"name": "Rarefied Sharlayan Rock Salt",
"level": 85
},
"rarefied-raw-ametrine": {
"name": "Rarefied Raw Ametrine",
"level": 81
},
"rarefied-eblan-alumen": {
"name": "Rarefied Eblan Alumen",
"level": 90
},
"rarefied-phrygian-gold-ore": {
"name":"Rarefied Phrygian Gold Ore",
"level": 87
},
"rarefied-pewter-ore": {
"name": "Rarefied Pewter Ore",
"level": 90,
"stars": 1
},
"rarefied-bismuth-ore": {
"name": "Rarefied Bismuth Ore",
"level": 83
},
"rarefied-annite": {
"name": "Rarefied Annite",
"level": 90,
"stars": 1
},
"rarefied-blue-zircon": {
"name": "Rarefied Blue Zircon",
"level": 89
},
"rarefied-titanium-gold-ore": {
"name": "Rarefied Titanium Gold Ore",
"level": 96
},
"rarefied-magnesite-ore": {
"name": "Rarefied Magnesite Ore",
"level": 100
},
"rarefied-rakaznar-ore": {
"name": "Rarefied Ra'Kaznar Ore",
"level": 100
},
"rarefied-white-gold-ore": {
"name": "Rarefied White Gold Ore",
"level": 98
},
"rarefied-ash-soil": {
"name": "Rarefied Ash Soil",
"level": 100
},
"rarefied-acacia-log": {
"name": "Rarefied Acacia Log",
"level": 100
},
"rarefied-acacia-bark": {
"name": "Rarefied Acacia Bark",
"level": 98
},
"rarefied-dark-mahogany-log": {
"name": "Rarefied Dark Mahogany Log",
"level": 100
},
"rarefied-sweet-kukuru-bean": {
"name": "Rarefied Sweet Kukuru Bean",
"level": 96
},
"rarefied-windsbalm-bay-leaf": {
"name": "Rarefied Windsbalm Bay Leaf",
"level": 100
},
"rarefied-mountain-flax": {
"name": "Rarefied Mountain Flax",
"level": 93
},
"rarefied-raw-dark-amber": {
"name": "Rarefied Raw Dark Amber",
"level": 93
},
"raw-spodumene": {
"name": "Raw Spodumene",
"level": 90,
"stars": 3,
"perception": 3850
},
"mempisang-log": {
"name": "Mempisang Log",
"level": 90,
"stars": 1,
"perception": 2990
},
"paldao-log": {
"name": "Paldao Log",
"level": 90,
"stars": 2,
"perception": 3600
}
}
+279 -1
View File
@@ -32,6 +32,284 @@
"rarefied-palm-log",
"rarefied-coconut"
]
},
{
"job": "botanist",
"type": "unspoiled",
"position": {
"zone": "ultima-thule",
"x": 14.0,
"y": 28.0
},
"times": [
"08:00-10:00",
"20:00-22:00"
],
"items": [
"rarefied-ar-cean-cotton-boll"
]
},
{
"job": "miner",
"type": "unspoiled",
"position": {
"zone": "labyrinthos",
"x": 32.5,
"y": 21.2
},
"times": [
"12:00-14:00",
"00:00-02:00"
],
"items": [
"rarefied-sharlayan-rock-salt",
"rarefied-raw-ametrine"
]
},
{
"job": "miner",
"type": "unspoiled",
"position": {
"zone": "garlemald",
"x": 12.9,
"y": 21.8
},
"times": [
"14:00-16:00",
"02:00-04:00"
],
"items": [
"rarefied-eblan-alumen",
"rarefied-phrygian-gold-ore"
]
},
{
"job": "miner",
"type": "unspoiled",
"position": {
"zone": "thavnair",
"x": 32.0,
"y": 25.0
},
"times": [
"04:00-06:00",
"16:00-18:00"
],
"items": [
"rarefied-pewter-ore"
]
},
{
"job": "miner",
"type": "unspoiled",
"position": {
"zone": "mare-lamentorum",
"x": 16.0,
"y": 32.0
},
"times": [
"06:00-08:00",
"18:00-20:00"
],
"items": [
"rarefied-bismuth-ore"
]
},
{
"job": "miner",
"type": "unspoiled",
"position": {
"zone": "elpis",
"x": 8.0,
"y": 36.0
},
"times": [
"10:00-12:00",
"22:00-00:00"
],
"items": [
"rarefied-annite",
"rarefied-blue-zircon"
]
},
{
"job": "miner",
"type": "unspoiled",
"position": {
"zone": "shaaloani",
"x": 9.2,
"y": 24.2
},
"times": [
"08:00-10:00",
"20:00-22:00"
],
"items": [
"rarefied-magnesite-ore",
"rarefied-titanium-gold-ore"
]
},
{
"job": "miner",
"type": "unspoiled",
"position": {
"zone": "heritage-found",
"x": 34.6,
"y": 8.2
},
"times": [
"04:00-06:00",
"16:00-18:00"
],
"items": [
"rarefied-rakaznar-ore",
"rarefied-white-gold-ore"
]
},
{
"job": "miner",
"type": "unspoiled",
"position": {
"zone": "living-memory",
"x": 24.9,
"y": 17.3
},
"times": [
"00:00-02:00",
"12:00-14:00"
],
"items": [
"rarefied-ash-soil"
]
},
{
"job": "botanist",
"type": "unspoiled",
"position": {
"zone": "shaaloani",
"x": 31.6,
"y": 20.4
},
"times": [
"06:00-08:00",
"18:00-20:00"
],
"items": [
"rarefied-acacia-log",
"rarefied-acacia-bark"
]
},
{
"job": "botanist",
"type": "unspoiled",
"position": {
"zone": "yak-tel",
"x": 36.9,
"y": 34.8
},
"times": [
"02:00-04:00",
"14:00-16:00"
],
"items": [
"rarefied-acacia-log",
"rarefied-acacia-bark"
]
},
{
"job": "botanist",
"type": "unspoiled",
"position": {
"zone": "living-memory",
"x": 8.7,
"y": 7.6
},
"times": [
"10:00-12:00",
"22:00-00:00"
],
"items": [
"rarefied-windsbalm-bay-leaf"
]
},
{
"job": "botanist",
"type": "unspoiled",
"position": {
"zone": "urquopacha",
"x": 5.9,
"y": 23.8
},
"times": [
"00:00-02:00",
"12:00-14:00"
],
"items": [
"rarefied-mountain-flax"
]
},
{
"job": "miner",
"type": "unspoiled",
"position": {
"zone": "kozamauka",
"x": 6.9,
"y": 7.5
},
"times": [
"10:00-12:00",
"22:00-00:00"
],
"items": [
"rarefied-raw-dark-amber"
]
},
{
"job": "miner",
"type": "legendary",
"position": {
"zone": "elpis",
"x": 30.2,
"y": 18.2
},
"times": [
"08:00-10:00",
"20:00-22:00"
],
"items": [
"raw-spodumene"
]
},
{
"job": "botanist",
"type": "legendary",
"position": {
"zone": "elpis",
"x": 33.1,
"y": 14.7
},
"times": [
"06:00-08:00",
"18:00-20:00"
],
"items": [
"mempisang-log"
]
},
{
"job": "botanist",
"type": "legendary",
"position": {
"zone": "elpis",
"x": 9.8,
"y": 29.8
},
"times": [
"02:00-04:00",
"14:00-16:00"
],
"items": [
"paldao-log"
]
}
]
+371 -2
View File
@@ -2,11 +2,380 @@
"labyrinthos": {
"name": {
"en": "Labyrinthos"
}
},
"aetherytes": [
{
"position": {
"x": 30.3,
"y": 11.9
},
"name": {
"en": "The Archeion"
}
},
{
"position": {
"x": 21.6,
"y": 20.4
},
"name": {
"en": "Sharlayan Hamlet"
}
},
{
"position": {
"x": 6.8,
"y": 27.5
},
"name": {
"en": "Aporia"
}
}
]
},
"thavnair": {
"name": {
"en": "Thavnair"
}
},
"aetherytes": [
{
"position": {
"x": 29.5,
"y": 16.5
},
"name": {
"en": "Palaka's Stand"
}
},
{
"position": {
"x": 10.9,
"y": 22.2
},
"name": {
"en": "The Great Work"
}
},
{
"position": {
"x": 25.3,
"y": 34.0
},
"name": {
"en": "Yedlihmad"
}
}
]
},
"mare-lamentorum": {
"name": {
"en": "Mare Lamentorum"
},
"aetherytes": [
{
"position": {
"x": 10.6,
"y": 34.3
},
"name": {
"en": "Sinus Lacrimarum"
}
},
{
"position": {
"x": 21.7,
"y": 11.1
},
"name": {
"en": "Bestways Burrow"
}
}
]
},
"ultima-thule": {
"name": {
"en": "Ultima Thule"
},
"aetherytes": [
{
"position": {
"x": 22.7,
"y": 8.3
},
"name": {
"en": "Reah Tahra"
}
},
{
"position": {
},
"name": {
"en": "Abode of the Ea"
}
},
{
"position": {
"x": 31.3,
"y": 28.0
},
"name": {
"en": "Base Omnicron"
}
}
]
},
"garlemald": {
"name": {
"en": "Garlemald"
},
"aetherytes": [
{
"position": {
"x": 13.3,
"y": 31.1
},
"name": {
"en": "Camp Broken Glass"
}
},
{
"position": {
"x": 31.7,
"y": 18.0
},
"name": {
"en": "Tertium"
}
}
]
},
"yak-tel": {
"name": {
"en": "Yak T'el"
},
"aetherytes": [
{
"position": {
"x": 13.5,
"y": 12.9
},
"name": {
"en": "Iq Br'aax"
}
},
{
"position": {
"x": 35.9,
"y": 32.0
},
"name": {
"en": "Mamook"
}
}
]
},
"shaaloani": {
"name": {
"en": "Shaaloani"
},
"aetherytes": [
{
"position": {
"x": 15.6,
"y": 19.2
},
"name": {
"en": "Sheshenewezi Springs"
}
},
{
"position": {
"x": 29.0,
"y": 30.8
},
"name": {
"en": "Hhusatahwi"
}
},
{
"position": {
"x": 27.1,
"y": 10.1
},
"name": {
"en": "Mehwahhetsoan"
}
}
]
},
"heritage-found": {
"name": {
"en": "Heritage Found"
},
"aetherytes": [
{
"position": {
"x": 17.0,
"y": 9.8
},
"name": {
"en": "The Outskirts"
}
},
{
"position": {
"x": 31.7,
"y": 25.7
},
"name": {
"en": "Yyasulani Station"
}
},
{
"position": {
"x": 17.0,
"y": 23.9
},
"name": {
"en": "Electrope Strike"
}
}
]
},
"living-memory": {
"name": {
"en": "Living Memory"
},
"aetherytes": [
{
"position": {
"x": 21.5,
"y": 37.3
},
"name": {
"en": "Leynode Mnemo"
}
},
{
"position": {
"x": 34.7,
"y": 15.7
},
"name": {
"en": "Leynode Pyro"
}
},
{
"position": {
"x": 16.4,
"y": 13.5
},
"name": {
"en": "Leynode Aero"
}
}
]
},
"urquopacha": {
"name": {
"en": "Urquopacha"
},
"aetherytes": [
{
"position": {
"x": 30.5,
"y": 34.2
},
"name": {
"en": "Worlar's Echo"
}
},
{
"position": {
"x": 28.1,
"y": 13.1
},
"name": {
"en": "Wachunpelo"
}
}
]
},
"kozamauka": {
"name": {
"en": "Kozama'uka"
},
"aetherytes": [
{
"position": {
"x": 37.1,
"y": 16.9
},
"name": {
"en": "Dock Poga"
}
},
{
"position": {
"x": 18.0,
"y": 12.0
},
"name": {
"en": "Ok'hanu"
}
},
{
"position": {
"x": 11.8,
"y": 27.8
},
"name": {
"en": "Earthenshire"
}
},
{
"position": {
"x": 32.2,
"y": 25.9
},
"name": {
"en": "Many Fires"
}
}
]
},
"elpis": {
"name": {
"en": "Elpis"
},
"aetherytes": [
{
"position": {
"x": 24.6,
"y": 24.0
},
"name": {
"en": "Anagnorisis"
}
},
{
"position": {
"x": 8.7,
"y": 32.3
},
"name": {
"en": "The Twelve Wonders"
}
},
{
"position": {
"x": 10.8,
"y": 17.0
},
"name": {
"en": "Poieten Oikos"
}
}
]
}
}
+133 -67
View File
@@ -1,17 +1,44 @@
<template>
<nav>
<div class="current-eorzea-time">
{{ eorzeaTime.getPrettyTime() }}
</div>
</nav>
<main>
<SortedNodeList
:nodes="nodes"
<div>
<nav>
<div class="current-eorzea-time">
{{ eorzeaTime.getPrettyTime() }}
</div>
<div class="top-bar-menu">
<a
href="https://github.com/YouHaveTrouble/DiscipleOfLand"
target="_blank"
>
<GithubLogo
color="#fff"
size="2rem"
/>
</a>
<button
class="filters-button"
:class="{ active: filtersActive}"
@click="filtersActive = !filtersActive"
>
{{ filtersActive ? ' Close filters' : 'Open filters' }}
</button>
</div>
</nav>
<main>
<SortedNodeList
v-if="!filtersActive"
:nodes="nodes as Node[]"
:zones="zones"
:eorzeaTime="eorzeaTime"
/>
</main>
:eorzea-time="eorzeaTime"
/>
<FiltersMenu
v-if="filtersActive"
/>
</main>
<footer>
<p>v{{ version }}</p>
<p><a href="https://github.com/YouHaveTrouble/DiscipleOfLand/blob/master/CONTRIBUTING.MD">Need YOUR help to input node, item and zone data!</a></p>
</footer>
</div>
</template>
<script lang="ts">
@@ -25,58 +52,51 @@ import {nodeTypeFromString} from "@/enums/NodeType";
import SortedNodeList from "@/components/SortedNodeList.vue";
import TimeRange from "@/entities/TimeRange";
import Zone from "@/entities/Zone";
import FiltersMenu from "@/components/FiltersMenu.vue";
import Filters from "@/util/Filters";
import GithubLogo from "@/components/icons/GithubLogo.vue";
import {version} from "../package.json";
export default defineComponent({
name: 'App',
components: {SortedNodeList},
components: {GithubLogo, FiltersMenu, SortedNodeList},
data: () => ({
version: version,
eorzeaTime: new EorzeaTime() as EorzeaTime,
nodes: [] as Node[],
aetherytes: [] as Aetheryte[],
items: {} as { [key: string]: Item },
zones: {} as { [key: string]: Zone },
filtersActive: false,
filters: new Filters(),
}),
methods: {
findNearestAetheryte(zone: string, x: number, y: number): Aetheryte | null {
findNearestAetheryte(zoneName: string, x: number, y: number): Aetheryte | null {
let result = null;
for (const aetheryte of this.aetherytes) {
let distance = Number.MAX_VALUE;
if (aetheryte.position.zone === zone) {
const a = aetheryte.position.x - x;
const b = aetheryte.position.y - y;
const distanceToAetheryte = Math.sqrt((a * a) + (b * b));
if (distanceToAetheryte < distance) {
distance = distanceToAetheryte;
result = aetheryte;
}
let distance = Number.MAX_SAFE_INTEGER;
const zone = this.zones[zoneName]
if (!zone) return null;
for (const aetheryte of zone.aetherytes) {
const a = aetheryte.position.x - x;
const b = aetheryte.position.y - y;
const distanceToAetheryte = Math.hypot(a, b);
if (distanceToAetheryte < distance) {
distance = distanceToAetheryte;
result = aetheryte;
}
}
return result;
}
},
async mounted() {
this.eorzeaTime = new EorzeaTime();
setInterval(() => {
this.eorzeaTime = new EorzeaTime();
}, 500);
}, 1000);
const aetheryteData = await fetch("/data/aetherytes.json")
.catch(() => {
return null;
});
if (aetheryteData === null) {
console.error("Failed to fetch aetheryte data!")
return;
}
const aetherytes = await aetheryteData.json();
for (const aetheryteData of aetherytes) {
this.aetherytes.push(new Aetheryte(aetheryteData));
}
const itemData = await fetch("/data/items.json")
.catch(() => {
return null;
});
const itemData: Response | null = await fetch("/data/items.json")
.catch((): null => {
return null;
});
if (itemData === null) {
console.error("Failed to fetch item data!")
return;
@@ -88,10 +108,10 @@ export default defineComponent({
this.items[itemId] = new Item(itemId, itemData);
}
const zoneData = await fetch("/data/zones.json")
.catch(() => {
return null;
});
const zoneData: Response | null = await fetch("/data/zones.json")
.catch((): null => {
return null;
});
if (zoneData === null) {
console.error("Failed to fetch zone data!")
return;
@@ -102,9 +122,9 @@ export default defineComponent({
}
const nodeData = await fetch("/data/nodes.json")
.catch(() => {
return null;
});
.catch(() => {
return null;
});
if (nodeData === null) {
console.error("Failed to fetch node data!")
return;
@@ -115,42 +135,57 @@ export default defineComponent({
for (const nodeData of nodes) {
const job = jobFromString(nodeData.job);
if (job === null) continue;
if (job === null) {
console.debug(`Failed to parse job: ${nodeData.job}`);
continue;
}
const nodeType = nodeTypeFromString(nodeData.type);
if (nodeType === null) continue;
if (nodeType === null) {
console.debug(`Failed to parse node type: ${nodeData.type}`);
continue;
}
const items = [] as Item[];
for (const itemId of nodeData.items) {
const item = this.items[itemId];
if (item === undefined) continue;
if (item === undefined) {
console.debug(`Failed to find item with id: ${itemId}`);
continue;
}
items.push(item);
}
const times = [] as TimeRange[];
for (const timeRangeEntry of nodeData.times) {
const timeSplit = timeRangeEntry.split("-");
if (timeSplit.length !== 2) continue;
if (timeSplit.length !== 2) {
console.debug(`Failed to parse time range: ${timeRangeEntry}`);
continue;
}
const startTime = timeSplit[0].split(":");
const endTime = timeSplit[1].split(":");
times.push(new TimeRange(
parseInt(startTime[0]),
parseInt(startTime[1]),
parseInt(endTime[0]),
parseInt(endTime[1])
parseInt(startTime[0]),
parseInt(startTime[1]),
parseInt(endTime[0]),
parseInt(endTime[1])
));
}
const nearestAetheryte = this.findNearestAetheryte(nodeData?.position?.zone, nodeData?.position?.x, nodeData.position?.y);
if (nearestAetheryte === null) continue;
if (nearestAetheryte === null) {
console.debug(`Failed to find nearest aetheryte for node: ${JSON.stringify(nodeData)}`);
continue;
}
this.nodes.push(new Node(
job,
nodeType,
nodeData.position,
times,
items,
nearestAetheryte
)
job,
nodeType,
nodeData.position,
times,
items,
nearestAetheryte
)
)
}
@@ -170,5 +205,36 @@ nav {
.current-eorzea-time {
font-size: 3rem;
}
.top-bar-menu {
display: flex;
gap: 1rem;
flex-direction: row;
}
.filters-button {
display: flex;
padding: 0.5rem;
border: 1px solid #fff;
background-color: transparent;
cursor: pointer;
width: max-content;
white-space: nowrap;
&.active {
background-color: rgba(255, 255, 255, 0.1);
}
}
}
footer {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 1rem 0.25rem;
gap: 0.5rem;
p {
margin: 0;
}
}
</style>
+240
View File
@@ -0,0 +1,240 @@
<template>
<section>
<details open>
<summary>Level of items</summary>
<section>
<label>
<span>Minimum level</span>
<input
type="number"
placeholder="1"
:min="1"
:max="filters.maxLevel"
v-model="filters.minLevel"
@focusout="(e: FocusEvent) => {
const target = e.target as HTMLInputElement;
const numberValue = parseInt(target.value);
if (filters.maxLevel && numberValue > filters.maxLevel) {
filters.maxLevel = filters.minLevel;
}
}"
>
</label>
<label>
<span>Maximum level</span>
<input
type="number"
placeholder="100"
:min="filters.minLevel"
:max="100"
v-model="filters.maxLevel"
@focusout="(e: FocusEvent) => {
const target = e.target as HTMLInputElement;
const numberValue = parseInt(target.value);
if (filters.minLevel && numberValue < filters.minLevel) {
filters.minLevel = filters.maxLevel;
}
}"
>
</label>
</section>
</details>
<details open>
<summary>Jobs</summary>
<section>
<label class="horizontal">
<span>Botanist</span>
<input
type="checkbox"
:checked="filters.jobs.has(Job.BOTANIST)"
@change="(e: Event) => {
const target = e.target as HTMLInputElement;
if (target.checked) {
filters.jobs.add(Job.BOTANIST);
} else {
filters.jobs.delete(Job.BOTANIST);
}
}"
>
</label>
<label class="horizontal">
<span>Miner</span>
<input
type="checkbox"
:checked="filters.jobs.has(Job.MINER)"
@change="(e: Event) => {
const target = e.target as HTMLInputElement;
if (target.checked) {
filters.jobs.add(Job.MINER);
} else {
filters.jobs.delete(Job.MINER);
}
}"
>
</label>
</section>
</details>
<details open>
<summary>Node type</summary>
<section>
<label class="horizontal">
<span>Unspoiled</span>
<input
type="checkbox"
:checked="filters.nodeTypes.has(NodeType.UNSPOILED)"
@change="(e: Event) => {
const target = e.target as HTMLInputElement;
if (target.checked) {
filters.nodeTypes.add(NodeType.UNSPOILED);
} else {
filters.nodeTypes.delete(NodeType.UNSPOILED);
}
}"
>
</label>
<label class="horizontal">
<span>Legendary</span>
<input
type="checkbox"
:checked="filters.nodeTypes.has(NodeType.LEGENDARY)"
@change="(e: Event) => {
const target = e.target as HTMLInputElement;
if (target.checked) {
filters.nodeTypes.add(NodeType.LEGENDARY);
} else {
filters.nodeTypes.delete(NodeType.LEGENDARY);
}
}"
>
</label>
</section>
</details>
</section>
</template>
<script lang="ts">
import {defineComponent} from "vue";
import Filters from "@/util/Filters";
import {Job} from "@/enums/Job";
import {NodeType} from "@/enums/NodeType";
export default defineComponent({
name: "FiltersMenu",
computed: {
NodeType() {
return NodeType
},
Job() {
return Job
}
},
emits: ['update:filters'],
data: () => ({
filters: new Filters(),
}),
watch: {
filters: {
handler(newFilters) {
window.localStorage.setItem("filters", newFilters.serialize());
},
deep: true,
},
minLevel(newValue: string) {
const numberValue = parseInt(newValue);
return isNaN(numberValue) ? 1 : numberValue;
},
maxLevel(newValue: string) {
const numberValue = parseInt(newValue);
return isNaN(numberValue) ? 100 : numberValue;
},
},
mounted() {
const savedFilters = window.localStorage.getItem("filters");
if (!savedFilters) return;
const parsedFilters = JSON.parse(savedFilters);
this.filters = new Filters(parsedFilters);
},
});
</script>
<style scoped lang="scss">
section {
display: flex;
flex-direction: column;
gap: 1rem;
padding-block: 1rem;
padding-inline: 0.25rem;
details {
background-color: #1f1f1f;
display: flex;
flex-direction: column;
border-radius: 0.25rem;
padding: 1rem;
gap: 0.5rem;
position: relative;
&:before {
position: absolute;
right: 1rem;
top: 1rem;
width: 1.5rem;
height: 1.5rem;
display: flex;
justify-content: center;
align-content: center;
content: "▶";
pointer-events: none;
rotate: 90deg;
transition: rotate 0.25s;
}
&[open] {
&:before {
rotate: 270deg;
}
}
summary {
cursor: pointer;
color: white;
border: none;
border-radius: 0.5rem;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 1.25rem;
}
section {
display: flex;
flex-direction: row;
gap: 2rem;
flex-wrap: wrap;
label {
display: flex;
gap: 0.5rem;
flex-direction: column;
&.horizontal {
flex-direction: row;
gap: 0.2rem;
}
input[type="number"] {
padding: 0.5rem;
border: 1px solid #fff;
background-color: transparent;
color: white;
border-radius: 0.25rem;
width: 5rem;
text-align: start;
}
}
}
}
}
</style>
+105 -23
View File
@@ -1,31 +1,64 @@
<template>
<article class="node">
<div class="timer">{{ gatheringNode.isActive(eorzeaTime) ? 'Active' : prettyTimer(gatheringNode.getSecondsToNextActiveTime(eorzeaTime)) }}</div>
<div class="job">
<div class="icon">
<img
<article
class="node"
:class="{active: gatheringNode.isActive(eorzeaTime)}"
>
<div
v-if="!gatheringNode.isActive(eorzeaTime)"
>
<div class="timer">
{{
prettyTimer(secondsToNextActiveTime)
}}
</div>
</div>
<div
v-else
>
<div class="timer">
Active
<div class="countdown">
{{
prettyTimer(secondsToNextInactiveTime)
}}
</div>
</div>
</div>
<div class="job">
<div class="icon">
<img
:alt="gatheringNode.job"
:src="`https://xivapi.com/cj/1/${gatheringNode.job}.png`"
:title="gatheringNode.job"
draggable="false"
>
</div>
</div>
<div class="aetheryte">
<span class="icon">
<img
src="https://xivapi.com/img-misc/mappy/aetheryte.png"
alt="Aetheryte icon"
draggable="false"
>
</span>
<div class="info">
<span>{{ zones[gatheringNode.nearestAetheryte.position.zone]?.name?.en }}</span>
<span>{{ gatheringNode.nearestAetheryte.name.en }}</span>
<span>{{
gatheringNode.nearestAetheryte.position.x.toFixed(1)
}}, {{ gatheringNode.nearestAetheryte.position.y.toFixed(1) }}</span>
</div>
</div>
<div class="items">
<span
v-for="item in gatheringNode.items"
:key="item.name"
>
{{ item.name }} (lv. {{ item.level }})
</span>
</div>
</div>
<div class="aetheryte">
<span class="icon">
<img src="https://xivapi.com/img-misc/mappy/aetheryte.png" alt="Aetheryte icon" draggable="false">
</span>
<div class="info">
<span>{{ zones[gatheringNode.nearestAetheryte.position.zone]?.name?.en }}</span>
<span>{{ gatheringNode.nearestAetheryte.name }}</span>
<span>{{ gatheringNode.nearestAetheryte.position.x }}, {{ gatheringNode.nearestAetheryte.position.y }}</span>
</div>
</div>
<div class="items">
<span v-for="item in gatheringNode.items">{{ item.name }} (lv. {{ item.level }})</span>
</div>
</article>
</article>
</template>
<script lang="ts">
@@ -36,6 +69,12 @@ import Zone from "@/entities/Zone";
export default defineComponent({
name: "GatheringNode",
data() {
return {
secondsToNextInactiveTime: 0 as number,
secondsToNextActiveTime: 0 as number,
};
},
props: {
gatheringNode: {
type: Object as PropType<Node>,
@@ -50,17 +89,44 @@ export default defineComponent({
required: true
},
},
watch: {
eorzeaTime: {
handler() {
this.calculateTimers()
}
}
},
methods: {
prettyTimer(seconds: number): string {
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
return `${minutes}:${remainingSeconds < 10 ? '0' : ''}${remainingSeconds}`;
},
calculateTimers() {
this.secondsToNextInactiveTime = this.gatheringNode.getSecondsToNextInactiveTime(this.eorzeaTime);
this.secondsToNextActiveTime = this.gatheringNode.getSecondsToNextActiveTime(this.eorzeaTime);
}
},
mounted() {
this.calculateTimers();
}
});
</script>
<style scoped lang="scss">
@keyframes pulsing {
0% {
background-color: rgba(255, 255, 255, 0.05);
}
50% {
background-color: rgba(255, 255, 255, 0.075);
}
100% {
background-color: rgba(255, 255, 255, 0.05);
}
}
.node {
display: flex;
flex-direction: row;
@@ -71,26 +137,38 @@ export default defineComponent({
border: 1px solid #fff;
padding: 0.5rem;
border-radius: 0.25rem;
content-visibility: auto;
&.active {
animation: infinite pulsing 6s;
}
.timer {
min-width: 7rem;
font-size: 2rem;
display: flex;
height: 100%;
flex-direction: column;
justify-content: center;
align-items: center;
}
.job {
display: flex;
justify-content: center;
align-items: center;
.icon {
width: 3rem;
height: 3rem;
img {
width: 100%;
height: 100%;
}
}
}
.aetheryte {
display: flex;
flex-direction: row;
@@ -100,15 +178,18 @@ export default defineComponent({
font-size: 1.5rem;
border-radius: 0.75rem;
padding: 0.35rem 1rem;
background-color: rgba(0,0,0, 0.2);
background-color: rgba(0, 0, 0, 0.2);
.icon {
width: 3rem;
img {
width: 100%;
height: 100%;
object-fit: fill;
}
}
.info {
display: flex;
flex-direction: column;
@@ -117,6 +198,7 @@ export default defineComponent({
gap: 0.1rem;
}
}
.items {
display: flex;
flex-direction: column;
@@ -125,4 +207,4 @@ export default defineComponent({
gap: 0.1rem;
}
}
</style>
</style>
+89 -52
View File
@@ -1,12 +1,13 @@
<template>
<div class="node-list">
<GatheringNode
<div class="node-list">
<GatheringNode
v-for="node in displayNodes"
:gathering-node="node"
:eorzeaTime="eorzeaTime"
:key="`${node.location.x}-${node.location.y}-${node.location.zone}`"
:gathering-node="node as Node"
:eorzea-time="eorzeaTime"
:zones="zones"
/>
</div>
/>
</div>
</template>
<script lang="ts">
@@ -15,58 +16,93 @@ import EorzeaTime from "../util/EorzeaTime";
import Node from "@/entities/Node";
import GatheringNode from "@/components/GatheringNode.vue";
import Zone from "@/entities/Zone";
import Filters from "@/util/Filters";
export default defineComponent(
{
name: "SortedNodeList",
components: {GatheringNode},
props: {
nodes: {
type: Array as PropType<Node[]>,
required: true
},
eorzeaTime: {
type: Object as PropType<EorzeaTime>,
required: true
},
zones: {
type: Object as PropType<{ [key: string]: Zone }>,
required: true
},
{
name: "SortedNodeList",
components: {GatheringNode},
props: {
nodes: {
type: Array as PropType<Node[]>,
required: true
},
watch: {
nodes: {
immediate: true,
handler() {
this.displayNodes = this.nodes;
}
eorzeaTime: {
type: Object as PropType<EorzeaTime>,
required: true
},
zones: {
type: Object as PropType<{ [key: string]: Zone }>,
required: true
},
},
watch: {
nodes: {
handler(newNodes: Node[]) {
this.filterNodes(newNodes);
},
eorzeaTime: {
immediate: true,
handler(newValue, oldValue) {
if (oldValue === undefined) return;
if (newValue?.getMinutes() === oldValue?.getMinutes()) return;
this.sortListByTime();
}
deep: true
},
displayNodes: {
handler() {
this.sortListByTime();
}
},
data: () => ({
displayNodes: [] as Node[],
}),
methods: {
sortListByTime() {
this.displayNodes.sort((a, b) => {
const aSeconds = a.getSecondsToNextActiveTime(this.eorzeaTime);
const bSeconds = b.getSecondsToNextActiveTime(this.eorzeaTime);
if (aSeconds === bSeconds) return a;
return aSeconds - bSeconds;
});
},
eorzeaTime: {
handler() {
this.sortListByTime();
}
}
},
data: () => ({
displayNodes: [] as Node[],
}),
methods: {
sortListByTime() {
this.displayNodes.sort((a, b): number => {
const aSeconds = a.getSecondsToNextActiveTime(this.eorzeaTime);
const bSeconds = b.getSecondsToNextActiveTime(this.eorzeaTime);
if (aSeconds === bSeconds) return 1;
return aSeconds - bSeconds;
});
},
mounted() {
this.displayNodes = this.nodes;
filterNodes(nodes: Node[] = []) {
let filters: Filters | null = null;
let filtersString = window.localStorage.getItem("filters");
if (filtersString === null) {
window.localStorage.setItem("filters", new Filters().serialize());
filtersString = window.localStorage.getItem("filters");
}
if (filtersString === null) {
console.error("Failed to get filters from local storage!");
return;
}
const parsedFilters = JSON.parse(filtersString);
filters = new Filters(parsedFilters);
this.displayNodes = nodes.filter((node) => {
let shouldDisplay = false;
if (filters) {
if (!filters.jobs.has(node.job)) return false;
if (!filters.nodeTypes.has(node.nodeType)) return false;
}
for (const item of node.items) {
if (filters && item.level >= filters.minLevel && item.level <= filters.maxLevel) {
shouldDisplay = true;
break;
}
}
return shouldDisplay;
});
},
}
},
mounted() {
this.filterNodes(this.nodes);
},
}
);
</script>
@@ -75,6 +111,7 @@ export default defineComponent(
display: flex;
flex-direction: column;
gap: 0.33rem;
padding-block: 0.5rem;
padding-inline: 0.25rem;
}
</style>
</style>
+48
View File
@@ -0,0 +1,48 @@
<template>
<div
class="github-logo"
aria-label="Github logo"
title="GitHub"
>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 128 128"
>
<g :fill="color">
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M64 5.103c-33.347 0-60.388 27.035-60.388 60.388 0 26.682 17.303 49.317 41.297 57.303 3.017.56 4.125-1.31 4.125-2.905 0-1.44-.056-6.197-.082-11.243-16.8 3.653-20.345-7.125-20.345-7.125-2.747-6.98-6.705-8.836-6.705-8.836-5.48-3.748.413-3.67.413-3.67 6.063.425 9.257 6.223 9.257 6.223 5.386 9.23 14.127 6.562 17.573 5.02.542-3.903 2.107-6.568 3.834-8.076-13.413-1.525-27.514-6.704-27.514-29.843 0-6.593 2.36-11.98 6.223-16.21-.628-1.52-2.695-7.662.584-15.98 0 0 5.07-1.623 16.61 6.19C53.7 35 58.867 34.327 64 34.304c5.13.023 10.3.694 15.127 2.033 11.526-7.813 16.59-6.19 16.59-6.19 3.287 8.317 1.22 14.46.593 15.98 3.872 4.23 6.215 9.617 6.215 16.21 0 23.194-14.127 28.3-27.574 29.796 2.167 1.874 4.097 5.55 4.097 11.183 0 8.08-.07 14.583-.07 16.572 0 1.607 1.088 3.49 4.148 2.897 23.98-7.994 41.263-30.622 41.263-57.294C124.388 32.14 97.35 5.104 64 5.104z"
/>
<path
d="M26.484 91.806c-.133.3-.605.39-1.035.185-.44-.196-.685-.605-.543-.906.13-.31.603-.395 1.04-.188.44.197.69.61.537.91zm2.446 2.729c-.287.267-.85.143-1.232-.28-.396-.42-.47-.983-.177-1.254.298-.266.844-.14 1.24.28.394.426.472.984.17 1.255zM31.312 98.012c-.37.258-.976.017-1.35-.52-.37-.538-.37-1.183.01-1.44.373-.258.97-.025 1.35.507.368.545.368 1.19-.01 1.452zm3.261 3.361c-.33.365-1.036.267-1.552-.23-.527-.487-.674-1.18-.343-1.544.336-.366 1.045-.264 1.564.23.527.486.686 1.18.333 1.543zm4.5 1.951c-.147.473-.825.688-1.51.486-.683-.207-1.13-.76-.99-1.238.14-.477.823-.7 1.512-.485.683.206 1.13.756.988 1.237zm4.943.361c.017.498-.563.91-1.28.92-.723.017-1.308-.387-1.315-.877 0-.503.568-.91 1.29-.924.717-.013 1.306.387 1.306.88zm4.598-.782c.086.485-.413.984-1.126 1.117-.7.13-1.35-.172-1.44-.653-.086-.498.422-.997 1.122-1.126.714-.123 1.354.17 1.444.663zm0 0"
/>
</g>
</svg>
</div>
</template>
<script lang="ts">
import {defineComponent} from 'vue'
export default defineComponent({
name: "GithubLogo",
props: {
color: {
type: String,
default: "#181616"
},
size: {
type: String,
default: "24px"
}
},
})
</script>
<style scoped lang="scss">
.github-logo {
width: v-bind(size);
height: v-bind(size);
}
</style>
+3 -3
View File
@@ -6,11 +6,11 @@ export default class Aetheryte {
}
constructor(
data: any,
data: {position: {x: number, y: number, zone: string}, name: {en: string}}
) {
this.position = data.position;
this.name = data.name.en;
this.name = data.name;
}
}
}
+3 -9
View File
@@ -3,18 +3,12 @@ export default class Item {
readonly id: string;
readonly name: string;
readonly level: number;
readonly scripType: ScripType;
constructor(id: string, data: any) {
constructor(id: string, data: {[key: string]: number | string | undefined}) {
this.id = id;
this.name = data?.name;
this.level = data?.level;
this.scripType = data?.scripType ? ScripType[data.scripType.toUpperCase()] : null;
this.name = data?.name as string;
this.level = data?.level as number;
}
}
enum ScripType {
WHITE = 'white',
PURPLE = 'purple',
}
+26 -3
View File
@@ -1,5 +1,5 @@
import {Job} from "../enums/Job";
import {NodeType} from "../enums/NodeType";
import {Job} from "@/enums/Job";
import {NodeType} from "@/enums/NodeType";
import Item from "./Item";
import Aetheryte from "./Aetheryte";
import TimeRange from "./TimeRange";
@@ -28,6 +28,7 @@ export default class Node {
this.times = times;
this.items = items;
this.nearestAetheryte = nearestAetheryte;
items.sort((a, b) => b.level - a.level);
}
isActive(eorzeaTime: EorzeaTime): boolean {
@@ -46,6 +47,15 @@ export default class Node {
return countdown;
}
getCountdownToInactive(eorzeaTime: EorzeaTime): number {
let countdown: number = Infinity;
for (const timeRange of this.times) {
const endTimeFrame: number = timeRange.getEndTimeFrame(eorzeaTime);
if (endTimeFrame < countdown) countdown = endTimeFrame;
}
return countdown;
}
getNextActiveTime(eorzeaTime: EorzeaTime): EorzeaTime {
let countdownTimeStamp: number = Infinity;
for (const timeRange of this.times) {
@@ -55,8 +65,21 @@ export default class Node {
return EorzeaTime.fromEorzeaTime(new Date(this.getCountdownToActive(eorzeaTime)));
}
getNextInactiveTime(eorzeaTime: EorzeaTime): EorzeaTime {
let countdownTimeStamp: number = Infinity;
for (const timeRange of this.times) {
const endTimeFrame: number = timeRange.getEndTimeFrame(eorzeaTime);
if (endTimeFrame < countdownTimeStamp) countdownTimeStamp = endTimeFrame;
}
return EorzeaTime.fromEorzeaTime(new Date(this.getCountdownToInactive(eorzeaTime)));
}
getSecondsToNextActiveTime(eorzeaTime: EorzeaTime): number {
return Math.floor((this.getNextActiveTime(eorzeaTime).realDate.getTime() - eorzeaTime.realDate.getTime()) / 1000);
}
}
getSecondsToNextInactiveTime(eorzeaTime: EorzeaTime): number {
return Math.floor((this.getNextInactiveTime(eorzeaTime).realDate.getTime() - eorzeaTime.realDate.getTime()) / 1000);
}
}
+14 -3
View File
@@ -13,7 +13,7 @@ export default class TimeRange {
public isWithinTimeFrame(hour: number, minute: number): boolean {
return (
this.from[0] < hour || this.from[0] == hour && this.from[1] <= minute)
&& (hour < this.to[0] || hour == this.to[0] && minute <= this.to[1]
&& (hour < this.to[0] || hour == this.to[0] && minute < this.to[1]
);
}
@@ -29,6 +29,17 @@ export default class TimeRange {
return targetDate.getTime();
}
/**
* Returns the timestamp when the current time range will end
* @param eorzeaTimeFrom
*/
public getEndTimeFrame(eorzeaTimeFrom: EorzeaTime): number {
const targetDate = new Date(eorzeaTimeFrom.eorzeaDate.getTime());
targetDate.setUTCHours(this.to[0], 0, 0, 0);
if (eorzeaTimeFrom.getHours() >= this.to[0]) {
targetDate.setUTCHours(this.to[0] + 24);
}
return targetDate.getTime();
}
}
}
+13 -3
View File
@@ -1,11 +1,21 @@
import Aetheryte from "@/entities/Aetheryte";
export default class Zone {
name: {
en: string,
}
constructor(data: any) {
this.name = data.name;
aetherytes: Array<Aetheryte> = [];
constructor(data: {name: {en: string}, aetherytes: Array<{position: {x: number, y: number, zone: string}, name: {en: string}}>}) {
this.name = {
en: data.name.en
};
if (!Array.isArray(data.aetherytes)) return;
for (const aetheryte of data.aetherytes) {
this.aetherytes.push(new Aetheryte(aetheryte));
}
}
}
}
+1 -1
View File
@@ -12,4 +12,4 @@ export function jobFromString(str: string): Job | null {
default:
return null;
}
}
}
+1 -1
View File
@@ -12,4 +12,4 @@ export function nodeTypeFromString(str: string): NodeType | null {
default:
return null;
}
}
}
+2 -2
View File
@@ -11,7 +11,7 @@ export default class EorzeaTime {
*/
readonly eorzeaDate: Date;
private constructor(realDate: Date = new Date()) {
public constructor(realDate: Date = new Date()) {
this.realDate = realDate;
this.eorzeaDate = new Date(realDate.getTime() * (3600 / 175));
}
@@ -59,4 +59,4 @@ export default class EorzeaTime {
return new EorzeaTime(new Date(eorzeaDate.getTime() / (3600 / 175)));
}
}
}
+65
View File
@@ -0,0 +1,65 @@
import {Job, jobFromString} from "@/enums/Job";
import {NodeType, nodeTypeFromString} from "@/enums/NodeType";
export default class Filters {
minLevel: number;
maxLevel: number;
jobs: Set<Job> = new Set();
nodeTypes: Set<NodeType> = new Set();
constructor(
data?: {
minLevel?: number,
maxLevel?: number,
jobs?: string[],
nodeTypes?: string[],
},
) {
this.minLevel = data?.minLevel || 91;
this.maxLevel = data?.maxLevel || 100;
let jobData = [
Job.BOTANIST.toLowerCase(),
Job.MINER.toLowerCase()
];
if (data?.jobs && Array.isArray(data?.jobs) && data?.jobs?.length > 0) {
jobData = data.jobs;
}
for (const job of jobData) {
const parsedJob = jobFromString(job);
if (!parsedJob) continue;
this.jobs.add(parsedJob);
}
let nodeTypeData = [
NodeType.UNSPOILED.toLowerCase(),
];
if (data?.nodeTypes && Array.isArray(data?.nodeTypes) && data?.nodeTypes?.length > 0) {
nodeTypeData = data.nodeTypes;
}
if (Array.isArray(nodeTypeData)) {
for (const nodeType of nodeTypeData) {
const parsedNodeType = nodeTypeFromString(nodeType);
if (!parsedNodeType) continue;
this.nodeTypes.add(parsedNodeType);
}
}
}
serialize(): string {
const serializedJobs = Array.from(this.jobs);
const serializedNodeTypes = Array.from(this.nodeTypes);
return JSON.stringify({
minLevel: this.minLevel,
maxLevel: this.maxLevel,
jobs: serializedJobs,
nodeTypes: serializedNodeTypes,
});
}
}
+21
View File
@@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"module": "ESNext",
"moduleResolution": "Node",
"strict": true,
"jsx": "preserve",
"resolveJsonModule": true,
"isolatedModules": true,
"esModuleInterop": true,
"lib": ["ESNext", "DOM"],
"skipLibCheck": true,
"noEmit": true,
"allowJs": true,
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
}
+2
View File
@@ -2,11 +2,13 @@ import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import eslintPlugin from 'vite-plugin-eslint'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
eslintPlugin(),
],
resolve: {
alias: {