11 Commits

11 changed files with 427 additions and 32 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.
+1 -1
View File
@@ -14,7 +14,7 @@ This is currently still missing a lot of functionality and data.
- [x] Filter and sorting section/popup - [x] Filter and sorting section/popup
- [x] Filtering based on job - [x] Filtering based on job
- [x] Filtering based on level - [x] Filtering based on level
- [ ] Filtering based on node type (legendary, ephemeral, etc.) - [x] Filtering based on node type (legendary, ephemeral, etc.)
- [ ] Fully input level 80-100 nodes data - [ ] Fully input level 80-100 nodes data
## Nice to have checklist ## Nice to have checklist
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "discipleofland", "name": "discipleofland",
"version": "0.0.6", "version": "0.0.8",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
+22 -1
View File
@@ -69,7 +69,6 @@
"name": "Rarefied Blue Zircon", "name": "Rarefied Blue Zircon",
"level": 89 "level": 89
}, },
"rarefied-titanium-gold-ore": { "rarefied-titanium-gold-ore": {
"name": "Rarefied Titanium Gold Ore", "name": "Rarefied Titanium Gold Ore",
"level": 96 "level": 96
@@ -113,5 +112,27 @@
"rarefied-mountain-flax": { "rarefied-mountain-flax": {
"name": "Rarefied Mountain Flax", "name": "Rarefied Mountain Flax",
"level": 93 "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
} }
} }
+65 -2
View File
@@ -203,7 +203,7 @@
"job": "botanist", "job": "botanist",
"type": "unspoiled", "type": "unspoiled",
"position": { "position": {
"zone": "yar-tel", "zone": "yak-tel",
"x": 36.9, "x": 36.9,
"y": 34.8 "y": 34.8
}, },
@@ -247,6 +247,69 @@
"items": [ "items": [
"rarefied-mountain-flax" "rarefied-mountain-flax"
] ]
},
{
"job": "botanist",
"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"
]
} }
] ]
+77
View File
@@ -300,5 +300,82 @@
} }
} }
] ]
},
"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"
}
}
]
} }
} }
+37 -5
View File
@@ -34,6 +34,10 @@
v-if="filtersActive" v-if="filtersActive"
/> />
</main> </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> </div>
</template> </template>
@@ -51,11 +55,13 @@ import Zone from "@/entities/Zone";
import FiltersMenu from "@/components/FiltersMenu.vue"; import FiltersMenu from "@/components/FiltersMenu.vue";
import Filters from "@/util/Filters"; import Filters from "@/util/Filters";
import GithubLogo from "@/components/icons/GithubLogo.vue"; import GithubLogo from "@/components/icons/GithubLogo.vue";
import {version} from "../package.json";
export default defineComponent({ export default defineComponent({
name: 'App', name: 'App',
components: {GithubLogo, FiltersMenu, SortedNodeList}, components: {GithubLogo, FiltersMenu, SortedNodeList},
data: () => ({ data: () => ({
version: version,
eorzeaTime: new EorzeaTime() as EorzeaTime, eorzeaTime: new EorzeaTime() as EorzeaTime,
nodes: [] as Node[], nodes: [] as Node[],
items: {} as { [key: string]: Item }, items: {} as { [key: string]: Item },
@@ -129,21 +135,33 @@ export default defineComponent({
for (const nodeData of nodes) { for (const nodeData of nodes) {
const job = jobFromString(nodeData.job); 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); 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[]; const items = [] as Item[];
for (const itemId of nodeData.items) { for (const itemId of nodeData.items) {
const item = this.items[itemId]; 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); items.push(item);
} }
const times = [] as TimeRange[]; const times = [] as TimeRange[];
for (const timeRangeEntry of nodeData.times) { for (const timeRangeEntry of nodeData.times) {
const timeSplit = timeRangeEntry.split("-"); 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 startTime = timeSplit[0].split(":");
const endTime = timeSplit[1].split(":"); const endTime = timeSplit[1].split(":");
times.push(new TimeRange( times.push(new TimeRange(
@@ -155,7 +173,10 @@ export default defineComponent({
} }
const nearestAetheryte = this.findNearestAetheryte(nodeData?.position?.zone, nodeData?.position?.x, nodeData.position?.y); 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( this.nodes.push(new Node(
job, job,
@@ -205,4 +226,15 @@ nav {
} }
} }
} }
footer {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 1rem 0.25rem;
gap: 0.5rem;
p {
margin: 0;
}
}
</style> </style>
+53 -16
View File
@@ -46,13 +46,13 @@
<span>Botanist</span> <span>Botanist</span>
<input <input
type="checkbox" type="checkbox"
:checked="filters.jobs.includes(Job.BOTANIST)" :checked="filters.jobs.has(Job.BOTANIST)"
@change="(e: Event) => { @change="(e: Event) => {
const target = e.target as HTMLInputElement; const target = e.target as HTMLInputElement;
if (target.checked) { if (target.checked) {
filters.jobs.push(Job.BOTANIST); filters.jobs.add(Job.BOTANIST);
} else { } else {
filters.jobs = filters.jobs.filter((job) => job !== Job.BOTANIST); filters.jobs.delete(Job.BOTANIST);
} }
}" }"
> >
@@ -61,13 +61,48 @@
<span>Miner</span> <span>Miner</span>
<input <input
type="checkbox" type="checkbox"
:checked="filters.jobs.includes(Job.MINER)" :checked="filters.jobs.has(Job.MINER)"
@change="(e: Event) => { @change="(e: Event) => {
const target = e.target as HTMLInputElement; const target = e.target as HTMLInputElement;
if (target.checked) { if (target.checked) {
filters.jobs.push(Job.MINER); filters.jobs.add(Job.MINER);
} else { } else {
filters.jobs = filters.jobs.filter((job) => job !== Job.MINER); 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);
} }
}" }"
> >
@@ -81,27 +116,27 @@
import {defineComponent} from "vue"; import {defineComponent} from "vue";
import Filters from "@/util/Filters"; import Filters from "@/util/Filters";
import {Job} from "@/enums/Job"; import {Job} from "@/enums/Job";
import {NodeType} from "@/enums/NodeType";
export default defineComponent({ export default defineComponent({
name: "FiltersMenu", name: "FiltersMenu",
computed: { computed: {
NodeType() {
return NodeType
},
Job() { Job() {
return Job return Job
} }
}, },
emits: ['update:filters'], emits: ['update:filters'],
data: () => ({ data: () => ({
filters: { filters: new Filters(),
minLevel: undefined,
maxLevel: undefined,
jobs: [] as Job[],
},
}), }),
watch: { watch: {
filters: { filters: {
handler(newFilters) { handler(newFilters) {
const filters = new Filters(newFilters); const filters = new Filters(newFilters);
window.localStorage.setItem("filters", JSON.stringify(filters)); window.localStorage.setItem("filters", filters.serialize());
}, },
deep: true, deep: true,
}, },
@@ -115,10 +150,11 @@ export default defineComponent({
}, },
}, },
mounted() { mounted() {
const filters = window.localStorage.getItem("filters"); const savedFilters = window.localStorage.getItem("filters");
if (filters) { if (!savedFilters) return;
this.filters = JSON.parse(filters); const parsedFilters = JSON.parse(savedFilters);
} this.filters = new Filters(parsedFilters);
}, },
}); });
@@ -130,6 +166,7 @@ section {
flex-direction: column; flex-direction: column;
gap: 1rem; gap: 1rem;
padding-block: 1rem; padding-block: 1rem;
padding-inline: 0.25rem;
details { details {
background-color: #1f1f1f; background-color: #1f1f1f;
+1 -1
View File
@@ -29,7 +29,7 @@
<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 }}, {{ gatheringNode.nearestAetheryte.position.y }}</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">
+5 -3
View File
@@ -85,8 +85,9 @@ export default defineComponent(
this.displayNodes = nodes.filter((node) => { this.displayNodes = nodes.filter((node) => {
let shouldDisplay = false; let shouldDisplay = false;
if (filters && !filters.jobs.includes(node.job)) { if (filters) {
return false; if (!filters.jobs.has(node.job)) return false;
if (!filters.nodeTypes.has(node.nodeType)) return false;
} }
for (const item of node.items) { for (const item of node.items) {
@@ -112,6 +113,7 @@ export default defineComponent(
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 0.33rem; gap: 0.33rem;
padding-block: 0.5rem;
padding-inline: 0.25rem;
} }
</style> </style>
+24 -2
View File
@@ -1,16 +1,19 @@
import {Job, jobFromString} from "@/enums/Job"; import {Job, jobFromString} from "@/enums/Job";
import {NodeType, nodeTypeFromString} from "@/enums/NodeType";
export default class Filters { export default class Filters {
minLevel: number; minLevel: number;
maxLevel: number; maxLevel: number;
jobs: Job[] = []; jobs: Set<Job> = new Set();
nodeTypes: Set<NodeType> = new Set();
constructor( constructor(
data?: { data?: {
minLevel?: number, minLevel?: number,
maxLevel?: number, maxLevel?: number,
jobs?: string[], jobs?: string[],
nodeTypes?: string[],
}, },
) { ) {
this.minLevel = data?.minLevel || 91; this.minLevel = data?.minLevel || 91;
@@ -20,9 +23,28 @@ export default class Filters {
for (const job of jobData) { for (const job of jobData) {
const parsedJob = jobFromString(job); const parsedJob = jobFromString(job);
if (!parsedJob) continue; if (!parsedJob) continue;
this.jobs.push(parsedJob); this.jobs.add(parsedJob);
} }
const nodeTypeData = data?.nodeTypes || [
NodeType.UNSPOILED,
];
for (const nodeType of nodeTypeData) {
const parsedNodeType = nodeTypeFromString(nodeType);
if (!parsedNodeType) continue;
this.nodeTypes.add(parsedNodeType);
}
}
serialize(): string {
return JSON.stringify({
minLevel: this.minLevel,
maxLevel: this.maxLevel,
jobs: Array.from(this.jobs).map(job => job),
nodeTypes: Array.from(this.nodeTypes).map(nodeType => nodeType),
});
} }
} }