15 Commits

12 changed files with 850 additions and 222 deletions
+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.
+5
View File
@@ -13,6 +13,11 @@
<meta property="twitter: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="og-url" content="https://dol.yht.one">
<meta property="twitter: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> </head>
<body> <body>
<noscript> <noscript>
+582 -194
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -1,6 +1,6 @@
{ {
"name": "discipleofland", "name": "discipleofland",
"version": "0.0.7", "version": "0.1.0",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
@@ -20,7 +20,7 @@
"@vue/eslint-config-typescript": "^12.0.0", "@vue/eslint-config-typescript": "^12.0.0",
"eslint": "^8.51.0", "eslint": "^8.51.0",
"eslint-plugin-vue": "^9.17.0", "eslint-plugin-vue": "^9.17.0",
"vite": "^5.3.3", "vite": "^5.4.18",
"vue-eslint-parser": "^9.3.2", "vue-eslint-parser": "^9.3.2",
"vite-plugin-eslint": "^1.8.1" "vite-plugin-eslint": "^1.8.1"
}, },
+7
View File
@@ -36,6 +36,7 @@
</main> </main>
<footer> <footer>
<p>v{{ version }}</p> <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> </footer>
</div> </div>
</template> </template>
@@ -227,7 +228,13 @@ nav {
} }
footer { footer {
display: flex; display: flex;
flex-direction: column;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
padding: 1rem 0.25rem;
gap: 0.5rem;
p {
margin: 0;
}
} }
</style> </style>
+1 -2
View File
@@ -135,8 +135,7 @@ export default defineComponent({
watch: { watch: {
filters: { filters: {
handler(newFilters) { handler(newFilters) {
const filters = new Filters(newFilters); window.localStorage.setItem("filters", newFilters.serialize());
window.localStorage.setItem("filters", filters.serialize());
}, },
deep: true, deep: true,
}, },
+35 -8
View File
@@ -3,10 +3,26 @@
class="node" class="node"
:class="{active: gatheringNode.isActive(eorzeaTime)}" :class="{active: gatheringNode.isActive(eorzeaTime)}"
> >
<div class="timer"> <div
{{ v-if="!gatheringNode.isActive(eorzeaTime)"
gatheringNode.isActive(eorzeaTime) ? 'Active' : prettyTimer(gatheringNode.getSecondsToNextActiveTime(eorzeaTime)) >
}} <div class="timer">
{{
prettyTimer(gatheringNode.getSecondsToNextActiveTime(eorzeaTime))
}}
</div>
</div>
<div
v-else
>
<div class="timer">
Active
<div class="countdown">
{{
prettyTimer(gatheringNode.getSecondsToNextInactiveTime(eorzeaTime))
}}
</div>
</div>
</div> </div>
<div class="job"> <div class="job">
<div class="icon"> <div class="icon">
@@ -29,7 +45,9 @@
<div class="info"> <div class="info">
<span>{{ zones[gatheringNode.nearestAetheryte.position.zone]?.name?.en }}</span> <span>{{ zones[gatheringNode.nearestAetheryte.position.zone]?.name?.en }}</span>
<span>{{ gatheringNode.nearestAetheryte.name.en }}</span> <span>{{ gatheringNode.nearestAetheryte.name.en }}</span>
<span>{{ gatheringNode.nearestAetheryte.position.x.toFixed(1) }}, {{ gatheringNode.nearestAetheryte.position.y.toFixed(1) }}</span> <span>{{
gatheringNode.nearestAetheryte.position.x.toFixed(1)
}}, {{ gatheringNode.nearestAetheryte.position.y.toFixed(1) }}</span>
</div> </div>
</div> </div>
<div class="items"> <div class="items">
@@ -78,9 +96,15 @@ export default defineComponent({
<style scoped lang="scss"> <style scoped lang="scss">
@keyframes pulsing { @keyframes pulsing {
0% {background-color: rgba(255,255,255, 0.05);} 0% {
50% {background-color: rgba(255,255,255, 0.075);} background-color: rgba(255, 255, 255, 0.05);
100% {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 { .node {
@@ -93,6 +117,7 @@ export default defineComponent({
border: 1px solid #fff; border: 1px solid #fff;
padding: 0.5rem; padding: 0.5rem;
border-radius: 0.25rem; border-radius: 0.25rem;
content-visibility: auto;
&.active { &.active {
animation: infinite pulsing 6s; animation: infinite pulsing 6s;
@@ -102,6 +127,8 @@ export default defineComponent({
min-width: 7rem; min-width: 7rem;
font-size: 2rem; font-size: 2rem;
display: flex; display: flex;
height: 100%;
flex-direction: column;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
} }
+1 -1
View File
@@ -72,7 +72,7 @@ export default defineComponent(
let filters: Filters | null = null; let filters: Filters | null = null;
let filtersString = window.localStorage.getItem("filters"); let filtersString = window.localStorage.getItem("filters");
if (filtersString === null) { if (filtersString === null) {
window.localStorage.setItem("filters", JSON.stringify(new Filters())); window.localStorage.setItem("filters", new Filters().serialize());
filtersString = window.localStorage.getItem("filters"); filtersString = window.localStorage.getItem("filters");
} }
if (filtersString === null) { if (filtersString === null) {
+16 -5
View File
@@ -1,11 +1,22 @@
<template> <template>
<div class="github-logo" aria-label="Github logo" title="GitHub"> <div
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 128 128"> 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"> <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 <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"/> 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> </g>
</svg> </svg>
</div> </div>
+22
View File
@@ -47,6 +47,15 @@ export default class Node {
return countdown; 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 { getNextActiveTime(eorzeaTime: EorzeaTime): EorzeaTime {
let countdownTimeStamp: number = Infinity; let countdownTimeStamp: number = Infinity;
for (const timeRange of this.times) { for (const timeRange of this.times) {
@@ -56,8 +65,21 @@ export default class Node {
return EorzeaTime.fromEorzeaTime(new Date(this.getCountdownToActive(eorzeaTime))); 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 { getSecondsToNextActiveTime(eorzeaTime: EorzeaTime): number {
return Math.floor((this.getNextActiveTime(eorzeaTime).realDate.getTime() - eorzeaTime.realDate.getTime()) / 1000); 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);
}
} }
+13
View File
@@ -29,4 +29,17 @@ export default class TimeRange {
return targetDate.getTime(); 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();
}
} }
+25 -10
View File
@@ -18,7 +18,13 @@ export default class Filters {
) { ) {
this.minLevel = data?.minLevel || 91; this.minLevel = data?.minLevel || 91;
this.maxLevel = data?.maxLevel || 100; this.maxLevel = data?.maxLevel || 100;
const jobData = data?.jobs || [Job.BOTANIST, Job.MINER]; 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) { for (const job of jobData) {
const parsedJob = jobFromString(job); const parsedJob = jobFromString(job);
@@ -26,24 +32,33 @@ export default class Filters {
this.jobs.add(parsedJob); this.jobs.add(parsedJob);
} }
const nodeTypeData = data?.nodeTypes || [ let nodeTypeData = [
NodeType.UNSPOILED, NodeType.UNSPOILED.toLowerCase(),
]; ];
if (data?.nodeTypes && Array.isArray(data?.nodeTypes) && data?.nodeTypes?.length > 0) {
for (const nodeType of nodeTypeData) { nodeTypeData = data.nodeTypes;
const parsedNodeType = nodeTypeFromString(nodeType);
if (!parsedNodeType) continue;
this.nodeTypes.add(parsedNodeType);
} }
if (Array.isArray(nodeTypeData)) {
for (const nodeType of nodeTypeData) {
const parsedNodeType = nodeTypeFromString(nodeType);
if (!parsedNodeType) continue;
this.nodeTypes.add(parsedNodeType);
}
}
} }
serialize(): string { serialize(): string {
const serializedJobs = Array.from(this.jobs);
const serializedNodeTypes = Array.from(this.nodeTypes);
return JSON.stringify({ return JSON.stringify({
minLevel: this.minLevel, minLevel: this.minLevel,
maxLevel: this.maxLevel, maxLevel: this.maxLevel,
jobs: Array.from(this.jobs).map(job => job), jobs: serializedJobs,
nodeTypes: Array.from(this.nodeTypes).map(nodeType => nodeType), nodeTypes: serializedNodeTypes,
}); });
} }