├── src ├── shared │ ├── matcher │ │ ├── index.ts │ │ └── matcher.ts │ ├── typings │ │ ├── bdocodex │ │ │ ├── query │ │ │ │ ├── index.ts │ │ │ │ ├── results │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── item.interface.ts │ │ │ │ │ ├── npc-drop.interface.ts │ │ │ │ │ ├── node-drop.interface.ts │ │ │ │ │ ├── npc.interface.ts │ │ │ │ │ ├── quest.interface.ts │ │ │ │ │ └── recipe.ts │ │ │ │ └── sorteable-row.interface.ts │ │ │ ├── workers │ │ │ │ ├── index.ts │ │ │ │ └── upgrade.interface.ts │ │ │ ├── interfaces │ │ │ │ ├── index.ts │ │ │ │ └── page-info.interface.ts │ │ │ ├── capras │ │ │ │ ├── index.ts │ │ │ │ ├── enhancement.ts │ │ │ │ └── data.ts │ │ │ ├── types.ts │ │ │ ├── enchantment │ │ │ │ ├── index.ts │ │ │ │ ├── array.interface.ts │ │ │ │ └── level.interface.ts │ │ │ ├── index.ts │ │ │ └── enums.ts │ │ ├── types.ts │ │ ├── app │ │ │ ├── constants.ts │ │ │ ├── types.ts │ │ │ ├── index.ts │ │ │ └── enums.ts │ │ └── index.ts │ ├── context-cache │ │ ├── index.ts │ │ └── context-cache.ts │ ├── index.ts │ └── utils │ │ ├── index.ts │ │ ├── filter-obj.ts │ │ ├── clean-str.ts │ │ ├── short-url.ts │ │ ├── parse-values.ts │ │ └── fetch.ts ├── query │ ├── utils │ │ ├── index.ts │ │ └── map-query-type.ts │ ├── index.ts │ ├── typings │ │ ├── interfaces │ │ │ ├── quests │ │ │ │ ├── index.ts │ │ │ │ ├── reward.interface.ts │ │ │ │ └── rewards.interface.ts │ │ │ ├── recipes │ │ │ │ ├── index.ts │ │ │ │ ├── material.interface.ts │ │ │ │ └── skill-lvl.interface.ts │ │ │ ├── options.interface.ts │ │ │ ├── index.ts │ │ │ ├── descriptor.interface.ts │ │ │ ├── query.interface.ts │ │ │ └── result.interface.ts │ │ ├── enums │ │ │ ├── index.ts │ │ │ ├── groups.enum.ts │ │ │ ├── item-as.enum.ts │ │ │ └── types.enum.ts │ │ ├── refs │ │ │ ├── index.ts │ │ │ ├── exp.ref.ts │ │ │ ├── item.ref.ts │ │ │ └── material-group.ref.ts │ │ ├── index.ts │ │ ├── types.ts │ │ └── entities │ │ │ ├── index.ts │ │ │ ├── npc-drop.entity.ts │ │ │ ├── item.entity.ts │ │ │ ├── node.entity.ts │ │ │ ├── generic.entity.ts │ │ │ ├── npc.entity.ts │ │ │ ├── recipe.entity.ts │ │ │ └── quest.entity.ts │ ├── builders │ │ ├── index.ts │ │ ├── item.builder.ts │ │ ├── npc-drop.builder.ts │ │ ├── node.builder.ts │ │ ├── npc.builder.ts │ │ ├── generic.builder.ts │ │ ├── recipe.builder.ts │ │ └── quest.builder.ts │ ├── factory.ts │ └── query.ts ├── scraper │ ├── builders │ │ ├── quest │ │ │ └── index.ts │ │ ├── recipe │ │ │ ├── index.ts │ │ │ └── recipe.builder.ts │ │ ├── knowledge │ │ │ ├── index.ts │ │ │ └── knowledge.builder.ts │ │ ├── material-group │ │ │ ├── index.ts │ │ │ └── material-group.builder.ts │ │ ├── npc │ │ │ ├── index.ts │ │ │ └── worker.builder.ts │ │ ├── item │ │ │ ├── index.ts │ │ │ ├── consumable.builder.ts │ │ │ └── item.builder.ts │ │ └── index.ts │ ├── index.ts │ ├── typings │ │ ├── interfaces │ │ │ ├── recipes │ │ │ │ ├── index.ts │ │ │ │ └── material.interface.ts │ │ │ ├── quests │ │ │ │ ├── index.ts │ │ │ │ ├── reward.ts │ │ │ │ └── rewards.ts │ │ │ ├── npcs │ │ │ │ ├── type.type.ts │ │ │ │ ├── workers │ │ │ │ │ ├── level.interface.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── growth.interface.ts │ │ │ │ │ └── stats.interface.ts │ │ │ │ ├── index.ts │ │ │ │ └── knowledge.interface.ts │ │ │ ├── equipments │ │ │ │ ├── caphras │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── enhancement.interface.ts │ │ │ │ │ └── wrapper.interface.ts │ │ │ │ ├── index.ts │ │ │ │ ├── stats.interface.ts │ │ │ │ └── enhancement.interface.ts │ │ │ ├── pricings.interface.ts │ │ │ ├── options.interface.ts │ │ │ ├── result.interface.ts │ │ │ ├── scrape.interface.ts │ │ │ └── index.ts │ │ ├── refs │ │ │ ├── exp.ref.ts │ │ │ ├── index.ts │ │ │ ├── npc.ref.ts │ │ │ ├── item.ref.ts │ │ │ ├── quest.ref.ts │ │ │ ├── knowledge.ref.ts │ │ │ └── material-group.ref.ts │ │ ├── enums │ │ │ ├── index.ts │ │ │ ├── types.ts │ │ │ ├── ctgs.enum.ts │ │ │ └── stats.ts │ │ ├── types.ts │ │ ├── index.ts │ │ └── entities │ │ │ ├── material-group.entity.ts │ │ │ ├── knowledge.entity.ts │ │ │ ├── consumable.entity.ts │ │ │ ├── worker.entity.ts │ │ │ ├── index.ts │ │ │ ├── generic.entity.ts │ │ │ ├── recipe.entity.ts │ │ │ ├── equipment.entity.ts │ │ │ ├── npc.entity.ts │ │ │ ├── quest.entity.ts │ │ │ └── item.entity.ts │ ├── factory.ts │ └── scraper.ts └── index.ts ├── .gitattributes ├── .gitignore ├── docs └── images │ └── calpheonjs.png ├── tests ├── utils │ ├── chai.config.ts │ ├── fetch-mock.ts │ ├── cache.ts │ ├── scrape-mock.ts │ └── query-mock.ts ├── queries │ ├── obtained-from │ │ ├── json │ │ │ └── 10103.json │ │ └── all.spec.ts │ ├── invalid.spec.ts │ ├── dropped-in-node │ │ ├── json │ │ │ ├── 15135.json │ │ │ └── 10656.json │ │ └── all.spec.ts │ ├── npc-drop │ │ ├── all.spec.ts │ │ └── json │ │ │ └── 6158.json │ ├── sold-by-npc │ │ ├── all.spec.ts │ │ └── json │ │ │ └── 13210.json │ ├── quest-rewards │ │ ├── all.spec.ts │ │ └── json │ │ │ └── 519.json │ ├── product-in-design │ │ ├── all.spec.ts │ │ └── json │ │ │ └── 10103.json │ ├── material-in-processing │ │ ├── all.spec.ts │ │ └── json │ │ │ └── 10406.json │ └── product-in-recipe │ │ ├── all.spec.ts │ │ └── json │ │ ├── 9213.json │ │ └── 9205.json └── scrapers │ ├── npc │ └── json │ │ ├── 23720.json │ │ ├── 21304.json │ │ └── 23746.json │ ├── item │ ├── json │ │ ├── 4901.json │ │ └── 4085.json │ ├── non-existent.spec.ts │ ├── queries.spec.ts │ └── all.spec.ts │ ├── consumable │ └── json │ │ ├── 741.json │ │ ├── 9422.json │ │ ├── 9213.json │ │ └── 781.json │ ├── knowledge │ ├── json │ │ ├── 103.json │ │ └── 58.json │ └── all.spec.ts │ ├── material-group │ ├── json │ │ └── 1.json │ └── all.spec.ts │ ├── equipment │ ├── json │ │ ├── 13961.json │ │ └── 703549.json │ └── properties │ │ └── enhancement_stats.spec.ts │ ├── recipe │ ├── json │ │ └── 122.json │ └── all.spec.ts │ ├── quest │ └── json │ │ ├── 694-2.json │ │ ├── 347-1.json │ │ ├── 2051-19.json │ │ ├── 815-2.json │ │ └── 6050-1.json │ └── worker │ └── json │ ├── 7614.json │ ├── 7615.json │ └── 7572.json ├── package.json ├── README.md └── tsconfig.json /src/shared/matcher/index.ts: -------------------------------------------------------------------------------- 1 | export { Matcher } from "./matcher"; -------------------------------------------------------------------------------- /src/shared/typings/bdocodex/query/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./results"; -------------------------------------------------------------------------------- /src/shared/typings/types.ts: -------------------------------------------------------------------------------- 1 | export type Undef = T | undefined; -------------------------------------------------------------------------------- /src/query/utils/index.ts: -------------------------------------------------------------------------------- 1 | export { mapQueryType } from "./map-query-type"; -------------------------------------------------------------------------------- /src/scraper/builders/quest/index.ts: -------------------------------------------------------------------------------- 1 | export { Quest } from "./quest.builder"; -------------------------------------------------------------------------------- /src/scraper/builders/recipe/index.ts: -------------------------------------------------------------------------------- 1 | export { Recipe } from "./recipe.builder"; -------------------------------------------------------------------------------- /src/shared/context-cache/index.ts: -------------------------------------------------------------------------------- 1 | export { ContextCache } from "./context-cache"; -------------------------------------------------------------------------------- /src/shared/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./matcher"; 2 | export * from "./context-cache"; -------------------------------------------------------------------------------- /src/shared/typings/app/constants.ts: -------------------------------------------------------------------------------- 1 | export const BASE_URL = 'https://bdocodex.com'; -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /src/query/index.ts: -------------------------------------------------------------------------------- 1 | export * as Queries from "./typings"; 2 | export * from "./factory"; -------------------------------------------------------------------------------- /src/scraper/builders/knowledge/index.ts: -------------------------------------------------------------------------------- 1 | export { Knowledge } from "./knowledge.builder"; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | dist/ 2 | node_modules/ 3 | tests/data/ 4 | yarn-error.log 5 | report.**.**.**.json -------------------------------------------------------------------------------- /src/shared/typings/bdocodex/workers/index.ts: -------------------------------------------------------------------------------- 1 | export { Upgrade } from "./upgrade.interface"; -------------------------------------------------------------------------------- /src/scraper/index.ts: -------------------------------------------------------------------------------- 1 | export * as Scrapers from "./typings"; 2 | export * from "./factory"; 3 | -------------------------------------------------------------------------------- /src/scraper/typings/interfaces/recipes/index.ts: -------------------------------------------------------------------------------- 1 | export { Material } from "./material.interface"; -------------------------------------------------------------------------------- /src/shared/typings/bdocodex/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export { PageInfo } from "./page-info.interface"; -------------------------------------------------------------------------------- /src/scraper/builders/material-group/index.ts: -------------------------------------------------------------------------------- 1 | export { MaterialGroup } from "./material-group.builder"; -------------------------------------------------------------------------------- /src/shared/typings/app/types.ts: -------------------------------------------------------------------------------- 1 | export type FetchFn = (url: string) => Promise | string; -------------------------------------------------------------------------------- /docs/images/calpheonjs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marceloclp/calpheonjs/HEAD/docs/images/calpheonjs.png -------------------------------------------------------------------------------- /src/scraper/builders/npc/index.ts: -------------------------------------------------------------------------------- 1 | export { NPC } from "./npc.builder"; 2 | export { Worker } from "./worker.builder"; -------------------------------------------------------------------------------- /src/shared/typings/app/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./constants"; 2 | export * from "./enums"; 3 | export * from "./types"; -------------------------------------------------------------------------------- /src/scraper/typings/interfaces/quests/index.ts: -------------------------------------------------------------------------------- 1 | export { Reward } from "./reward"; 2 | export { Rewards } from "./rewards"; -------------------------------------------------------------------------------- /src/shared/typings/bdocodex/capras/index.ts: -------------------------------------------------------------------------------- 1 | export { Data } from "./data"; 2 | export { Enhancement } from "./enhancement"; -------------------------------------------------------------------------------- /tests/utils/chai.config.ts: -------------------------------------------------------------------------------- 1 | import chai from "chai"; 2 | import chaiSubset from "chai-subset"; 3 | 4 | chai.use(chaiSubset); -------------------------------------------------------------------------------- /src/scraper/typings/interfaces/npcs/type.type.ts: -------------------------------------------------------------------------------- 1 | export type Type = 2 | | 'normal' 3 | | 'boss' 4 | | 'awakened_boss'; -------------------------------------------------------------------------------- /src/shared/typings/bdocodex/types.ts: -------------------------------------------------------------------------------- 1 | import { Stats } from "./enums"; 2 | 3 | export type StatsObj = { [keyof in Stats]?: string }; -------------------------------------------------------------------------------- /src/shared/typings/index.ts: -------------------------------------------------------------------------------- 1 | export * as App from "./app"; 2 | export * as BDOCodex from "./bdocodex"; 3 | export * from "./types"; -------------------------------------------------------------------------------- /src/scraper/typings/refs/exp.ref.ts: -------------------------------------------------------------------------------- 1 | export interface EXP { 2 | type: 'exp'; 3 | 4 | icon: string; 5 | 6 | name: string; 7 | } -------------------------------------------------------------------------------- /src/shared/typings/bdocodex/enchantment/index.ts: -------------------------------------------------------------------------------- 1 | export { Array } from "./array.interface"; 2 | export { Level } from "./level.interface"; -------------------------------------------------------------------------------- /src/query/typings/interfaces/quests/index.ts: -------------------------------------------------------------------------------- 1 | export { Reward } from "./reward.interface"; 2 | export { Rewards } from "./rewards.interface"; -------------------------------------------------------------------------------- /src/scraper/typings/enums/index.ts: -------------------------------------------------------------------------------- 1 | export { Stats } from "./stats"; 2 | export { Types } from "./types"; 3 | export { Ctgs } from "./ctgs.enum"; -------------------------------------------------------------------------------- /src/query/typings/interfaces/recipes/index.ts: -------------------------------------------------------------------------------- 1 | export { Material } from "./material.interface"; 2 | export { SkillLvl } from "./skill-lvl.interface"; -------------------------------------------------------------------------------- /src/query/typings/enums/index.ts: -------------------------------------------------------------------------------- 1 | export { Groups } from "./groups.enum"; 2 | export { ItemAs } from "./item-as.enum"; 3 | export { Types } from "./types.enum"; -------------------------------------------------------------------------------- /src/scraper/typings/interfaces/npcs/workers/level.interface.ts: -------------------------------------------------------------------------------- 1 | export interface Level { 2 | sell_price: number; 3 | 4 | exp_to_next_lvl: number; 5 | } -------------------------------------------------------------------------------- /src/query/typings/refs/index.ts: -------------------------------------------------------------------------------- 1 | export { EXP } from "./exp.ref"; 2 | export { Item } from "./item.ref"; 3 | export { MaterialGroup } from "./material-group.ref"; -------------------------------------------------------------------------------- /src/scraper/typings/interfaces/equipments/caphras/index.ts: -------------------------------------------------------------------------------- 1 | export { Enhancement } from "./enhancement.interface"; 2 | export { Wrapper } from "./wrapper.interface"; -------------------------------------------------------------------------------- /src/scraper/typings/interfaces/pricings.interface.ts: -------------------------------------------------------------------------------- 1 | export interface Pricings { 2 | buy: number; 3 | 4 | sell: number; 5 | 6 | repair: number; 7 | } -------------------------------------------------------------------------------- /src/scraper/typings/interfaces/npcs/index.ts: -------------------------------------------------------------------------------- 1 | export { Type } from "./type.type"; 2 | export { Knowledge } from "./knowledge.interface"; 3 | export * as Workers from "./workers"; -------------------------------------------------------------------------------- /src/scraper/typings/interfaces/options.interface.ts: -------------------------------------------------------------------------------- 1 | import { App } from "../../../shared/typings"; 2 | 3 | export interface Options { 4 | readonly locale?: App.Locales; 5 | } -------------------------------------------------------------------------------- /src/scraper/builders/item/index.ts: -------------------------------------------------------------------------------- 1 | export { Consumable } from "./consumable.builder"; 2 | export { Equipment } from "./equipment.builder"; 3 | export { Item } from "./item.builder"; -------------------------------------------------------------------------------- /src/scraper/typings/interfaces/npcs/knowledge.interface.ts: -------------------------------------------------------------------------------- 1 | import * as Refs from "../../refs"; 2 | 3 | export type Knowledge = Refs.Knowledge & { 4 | drop_chance: number 5 | }; -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import "cheerio"; 2 | export * from "./query"; 3 | export * from "./scraper"; 4 | export * from "./shared/typings/app/enums"; 5 | export { BASE_URL } from "./shared/typings/app"; -------------------------------------------------------------------------------- /src/query/typings/index.ts: -------------------------------------------------------------------------------- 1 | export * as Entities from "./entities"; 2 | export * as Refs from "./refs"; 3 | export * from "./interfaces"; 4 | export * from "./enums"; 5 | export * from "./types"; -------------------------------------------------------------------------------- /src/scraper/typings/interfaces/equipments/index.ts: -------------------------------------------------------------------------------- 1 | export { Enhancement } from "./enhancement.interface"; 2 | export { Stats } from "./stats.interface"; 3 | export * as Caphras from "./caphras"; -------------------------------------------------------------------------------- /src/scraper/typings/interfaces/npcs/workers/index.ts: -------------------------------------------------------------------------------- 1 | export { Growth } from "./growth.interface"; 2 | export { Level } from "./level.interface"; 3 | export { Stats } from "./stats.interface"; -------------------------------------------------------------------------------- /src/scraper/typings/types.ts: -------------------------------------------------------------------------------- 1 | import { Result } from "./interfaces"; 2 | 3 | export type ScrapeFn = () => Promise>; 4 | 5 | export type Stat = number | [number, number]; -------------------------------------------------------------------------------- /src/shared/typings/app/enums.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The supported locales. 3 | * 4 | * Maps a locales to its url subpath (eg. `/us/item/`). 5 | */ 6 | export enum Locales { 7 | US = 'us', 8 | } -------------------------------------------------------------------------------- /src/scraper/typings/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./enums"; 2 | export * from "./types"; 3 | export * from "./interfaces"; 4 | export * as Entities from "./entities"; 5 | export * as Refs from "./refs"; -------------------------------------------------------------------------------- /src/scraper/typings/interfaces/equipments/stats.interface.ts: -------------------------------------------------------------------------------- 1 | import * as Enums from "../../enums"; 2 | import { Stat } from "../../types"; 3 | 4 | export type Stats = Partial>; -------------------------------------------------------------------------------- /src/query/typings/interfaces/quests/reward.interface.ts: -------------------------------------------------------------------------------- 1 | import * as Refs from "../../refs"; 2 | 3 | export type Reward = 4 | | (Refs.Item & { amount: number }) 5 | | (Refs.EXP & { amount: number }); -------------------------------------------------------------------------------- /src/query/typings/interfaces/options.interface.ts: -------------------------------------------------------------------------------- 1 | import { App } from "../../../shared/typings"; 2 | 3 | /** 4 | * Query settings. 5 | */ 6 | export interface Options { 7 | readonly locale?: App.Locales; 8 | } -------------------------------------------------------------------------------- /src/scraper/typings/interfaces/npcs/workers/growth.interface.ts: -------------------------------------------------------------------------------- 1 | export interface Growth { 2 | work_speed: [number, number]; 3 | 4 | movement_speed: [number, number]; 5 | 6 | luck: [number, number]; 7 | } -------------------------------------------------------------------------------- /src/scraper/typings/interfaces/npcs/workers/stats.interface.ts: -------------------------------------------------------------------------------- 1 | export interface Stats { 2 | work_speed: number; 3 | 4 | movement_speed: number; 5 | 6 | luck: number; 7 | 8 | stamina: number; 9 | } -------------------------------------------------------------------------------- /src/shared/utils/index.ts: -------------------------------------------------------------------------------- 1 | export { fetch } from "./fetch"; 2 | export { cleanStr } from "./clean-str"; 3 | export { filterObj } from "./filter-obj"; 4 | export * from "./parse-values"; 5 | export * from "./short-url"; -------------------------------------------------------------------------------- /src/query/typings/interfaces/recipes/material.interface.ts: -------------------------------------------------------------------------------- 1 | import * as Refs from "../../refs"; 2 | 3 | export type Material = 4 | | (Refs.Item & { amount: number }) 5 | | (Refs.MaterialGroup & { amount: number }); -------------------------------------------------------------------------------- /src/query/typings/refs/exp.ref.ts: -------------------------------------------------------------------------------- 1 | export interface EXP { 2 | type: 'exp'; 3 | 4 | /** The icon used to represent the exp. */ 5 | icon: string; 6 | 7 | /** The type of the exp. */ 8 | name: string; 9 | } -------------------------------------------------------------------------------- /src/query/typings/interfaces/recipes/skill-lvl.interface.ts: -------------------------------------------------------------------------------- 1 | export interface SkillLvl { 2 | /** The mastery name (e.g, Beginner). */ 3 | mastery: string; 4 | 5 | /** The mastery level. */ 6 | lvl: number; 7 | } -------------------------------------------------------------------------------- /src/scraper/typings/entities/material-group.entity.ts: -------------------------------------------------------------------------------- 1 | import * as Refs from "../refs"; 2 | import { Generic } from "./generic.entity"; 3 | 4 | export interface MaterialGroup extends Generic { 5 | items: Refs.Item[]; 6 | } -------------------------------------------------------------------------------- /src/scraper/typings/interfaces/recipes/material.interface.ts: -------------------------------------------------------------------------------- 1 | import * as Refs from "../../refs"; 2 | 3 | export type Material = 4 | | (Refs.Item & { grade: number; amount: number; }) 5 | | (Refs.MaterialGroup) & { amount: number; }; -------------------------------------------------------------------------------- /src/scraper/typings/entities/knowledge.entity.ts: -------------------------------------------------------------------------------- 1 | import * as Refs from "../refs"; 2 | import { Generic } from "./generic.entity"; 3 | 4 | export interface Knowledge extends Generic { 5 | group?: string; 6 | 7 | obtained_from?: Refs.NPC; 8 | } -------------------------------------------------------------------------------- /src/scraper/typings/interfaces/equipments/caphras/enhancement.interface.ts: -------------------------------------------------------------------------------- 1 | import { Stats } from "../stats.interface"; 2 | 3 | export interface Enhancement { 4 | stats: Stats; 5 | 6 | count_next: number; 7 | 8 | count_total: number; 9 | } -------------------------------------------------------------------------------- /src/scraper/builders/index.ts: -------------------------------------------------------------------------------- 1 | export { Generic } from "./generic.builder"; 2 | export * from "./item"; 3 | export * from "./knowledge"; 4 | export * from "./material-group"; 5 | export * from "./npc"; 6 | export * from "./quest"; 7 | export * from "./recipe"; -------------------------------------------------------------------------------- /src/shared/typings/bdocodex/capras/enhancement.ts: -------------------------------------------------------------------------------- 1 | import { Stats } from "../enums"; 2 | 3 | export interface Enhancement { 4 | readonly count: string; 5 | 6 | readonly tcount: string; 7 | 8 | readonly stats: { [keyof in Stats]?: string }; 9 | } -------------------------------------------------------------------------------- /src/scraper/typings/interfaces/equipments/caphras/wrapper.interface.ts: -------------------------------------------------------------------------------- 1 | import { Enhancement } from "./enhancement.interface"; 2 | 3 | export interface Wrapper { 4 | 18?: Enhancement[]; 5 | 6 | 19?: Enhancement[]; 7 | 8 | 20?: Enhancement[]; 9 | } -------------------------------------------------------------------------------- /src/scraper/typings/interfaces/result.interface.ts: -------------------------------------------------------------------------------- 1 | export interface Result { 2 | /** The parsed url that was used to perform the fetch. */ 3 | readonly url: string; 4 | 5 | readonly type: string | null; 6 | 7 | readonly data: T; 8 | } -------------------------------------------------------------------------------- /src/scraper/typings/interfaces/quests/reward.ts: -------------------------------------------------------------------------------- 1 | import * as Refs from "../../refs"; 2 | 3 | export type Reward = 4 | | (Refs.Item & { amount: number }) 5 | | (Refs.EXP & { amount: number }) 6 | | (Refs.NPC & { amity_gained: number }) 7 | | (Refs.Knowledge); -------------------------------------------------------------------------------- /src/scraper/typings/refs/index.ts: -------------------------------------------------------------------------------- 1 | export { Quest } from "./quest.ref"; 2 | export { NPC } from "./npc.ref"; 3 | export { Item } from "./item.ref"; 4 | export { EXP } from "./exp.ref"; 5 | export { Knowledge } from "./knowledge.ref"; 6 | export { MaterialGroup } from "./material-group.ref"; -------------------------------------------------------------------------------- /src/shared/typings/bdocodex/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./enums"; 2 | export * from "./types"; 3 | export * from "./interfaces"; 4 | export * as Query from "./query"; 5 | export * as Caphras from "./capras"; 6 | export * as Enchantment from "./enchantment"; 7 | export * as Workers from "./workers"; -------------------------------------------------------------------------------- /src/scraper/typings/interfaces/quests/rewards.ts: -------------------------------------------------------------------------------- 1 | import { Reward } from "./reward"; 2 | 3 | export interface Rewards { 4 | /** All rewards are given on quest completion. */ 5 | standard: Reward[]; 6 | 7 | /** Choose on of the following rewards on quest completion. */ 8 | choose: Reward[]; 9 | } -------------------------------------------------------------------------------- /src/query/typings/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export { Descriptor } from "./descriptor.interface"; 2 | export { Options } from "./options.interface"; 3 | export { Result } from "./result.interface"; 4 | export { Query } from "./query.interface"; 5 | export * as Recipes from "./recipes"; 6 | export * as Quests from "./quests"; -------------------------------------------------------------------------------- /src/shared/typings/bdocodex/enchantment/array.interface.ts: -------------------------------------------------------------------------------- 1 | import { Level } from "./level.interface"; 2 | 3 | /** 4 | * The enhancement data for an equipment. 5 | */ 6 | export interface Array extends Record { 7 | readonly na: string; 8 | 9 | readonly max_enchant: string; 10 | } -------------------------------------------------------------------------------- /src/shared/typings/bdocodex/query/results/index.ts: -------------------------------------------------------------------------------- 1 | export { Item } from "./item.interface"; 2 | export { NodeDrop } from "./node-drop.interface"; 3 | export { NPCDrop } from "./npc-drop.interface"; 4 | export { NPC } from "./npc.interface"; 5 | export { Quest } from "./quest.interface"; 6 | export { Recipe } from "./recipe"; -------------------------------------------------------------------------------- /src/query/typings/types.ts: -------------------------------------------------------------------------------- 1 | import { Result } from "./interfaces"; 2 | 3 | export type EntityTypes = ( 4 | | 'unknown' 5 | | 'recipe' 6 | | 'npc_drop' 7 | | 'node' 8 | | 'item' 9 | | 'npc' 10 | | 'quest' 11 | | 'exp' 12 | ); 13 | 14 | export type QueryFn = () => Promise>; -------------------------------------------------------------------------------- /src/shared/typings/bdocodex/interfaces/page-info.interface.ts: -------------------------------------------------------------------------------- 1 | export interface PageInfo { 2 | readonly '@context': string; 3 | 4 | readonly '@type': string; 5 | 6 | readonly image: string; 7 | 8 | readonly name: string; 9 | 10 | readonly description: string; 11 | 12 | readonly url: string; 13 | } -------------------------------------------------------------------------------- /src/query/builders/index.ts: -------------------------------------------------------------------------------- 1 | export { Generic } from "./generic.builder"; 2 | export { Recipe } from "./recipe.builder"; 3 | export { NPCDrop } from "./npc-drop.builder"; 4 | export { Node } from "./node.builder"; 5 | export { Item } from "./item.builder"; 6 | export { NPC } from "./npc.builder"; 7 | export { Quest } from "./quest.builder"; -------------------------------------------------------------------------------- /src/query/typings/entities/index.ts: -------------------------------------------------------------------------------- 1 | export { Generic } from "./generic.entity"; 2 | export { Item } from "./item.entity"; 3 | export { Node } from "./node.entity"; 4 | export { NPCDrop } from "./npc-drop.entity"; 5 | export { NPC } from "./npc.entity"; 6 | export { Quest } from "./quest.entity"; 7 | export { Recipe } from "./recipe.entity"; -------------------------------------------------------------------------------- /src/query/typings/enums/groups.enum.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Equivalent to the `a` property. 3 | */ 4 | export enum Groups { 5 | PROCESSING = 'mrecipes', 6 | RECIPE = 'recipes', 7 | DESIGN = 'designs', 8 | DROP = 'drop', 9 | NODE = 'nodes', 10 | ITEM = 'items', 11 | NPC = 'npcs', 12 | QUEST = 'quests', 13 | } -------------------------------------------------------------------------------- /src/scraper/typings/refs/npc.ref.ts: -------------------------------------------------------------------------------- 1 | import * as Entities from "../entities"; 2 | import { ScrapeFn } from "../types"; 3 | 4 | export interface NPC { 5 | type: 'npc'; 6 | 7 | id: string; 8 | 9 | icon: string; 10 | 11 | name: string; 12 | 13 | shortUrl: string; 14 | 15 | scrape: ScrapeFn; 16 | } -------------------------------------------------------------------------------- /src/shared/typings/bdocodex/workers/upgrade.interface.ts: -------------------------------------------------------------------------------- 1 | export interface Upgrade { 2 | readonly upgrade_chance: string; 3 | 4 | readonly work_speed: string; 5 | 6 | readonly move_speed: string; 7 | 8 | readonly luck: string; 9 | 10 | readonly nextlvexp: string; 11 | 12 | readonly sell_price: string; 13 | } -------------------------------------------------------------------------------- /src/query/typings/entities/npc-drop.entity.ts: -------------------------------------------------------------------------------- 1 | import { Generic } from "./generic.entity"; 2 | 3 | export interface NPCDrop extends Generic { 4 | type: 'npc_drop'; 5 | 6 | /** How many items the entity drops. */ 7 | amount: number; 8 | 9 | /** The drop chance percentage as a floating point. */ 10 | chance: number; 11 | } -------------------------------------------------------------------------------- /src/scraper/typings/refs/item.ref.ts: -------------------------------------------------------------------------------- 1 | import * as Entities from "../entities"; 2 | import { ScrapeFn } from "../types"; 3 | 4 | export interface Item { 5 | type: 'item'; 6 | 7 | id: string; 8 | 9 | icon: string; 10 | 11 | name: string; 12 | 13 | shortUrl: string; 14 | 15 | scrape: ScrapeFn; 16 | } -------------------------------------------------------------------------------- /src/scraper/typings/interfaces/scrape.interface.ts: -------------------------------------------------------------------------------- 1 | import { Types } from "../enums"; 2 | import { Result } from "./result.interface"; 3 | import { Options } from "./options.interface"; 4 | 5 | export interface Scrape { 6 | (id: string, type: Types): Promise>; 7 | (id: string, type: Types, options: Options): Promise>; 8 | } -------------------------------------------------------------------------------- /src/scraper/typings/refs/quest.ref.ts: -------------------------------------------------------------------------------- 1 | import * as Entities from "../entities"; 2 | import { ScrapeFn } from "../types"; 3 | 4 | export interface Quest { 5 | type: 'quest'; 6 | 7 | id: string; 8 | 9 | icon: string; 10 | 11 | name: string; 12 | 13 | shortUrl: string; 14 | 15 | scrape: ScrapeFn; 16 | } -------------------------------------------------------------------------------- /src/query/typings/entities/item.entity.ts: -------------------------------------------------------------------------------- 1 | import { Scrapers } from "../../../scraper"; 2 | import { Generic } from "./generic.entity"; 3 | 4 | export interface Item extends Generic { 5 | type: 'item'; 6 | 7 | /** The level required to use the item. */ 8 | lvl: number; 9 | 10 | scrape: Scrapers.ScrapeFn; 11 | } -------------------------------------------------------------------------------- /src/shared/typings/bdocodex/capras/data.ts: -------------------------------------------------------------------------------- 1 | import { Enhancement } from "./enhancement"; 2 | import { Stats } from "../enums"; 3 | 4 | export interface Data { 5 | readonly 18: Enhancement[]; 6 | 7 | readonly 19: Enhancement[]; 8 | 9 | readonly 20: Enhancement[]; 10 | 11 | readonly stats_names: { [keyof in Stats]?: string }; 12 | } -------------------------------------------------------------------------------- /src/scraper/typings/refs/knowledge.ref.ts: -------------------------------------------------------------------------------- 1 | import * as Entities from "../entities"; 2 | import { ScrapeFn } from "../types"; 3 | 4 | export interface Knowledge { 5 | type: 'knowledge'; 6 | 7 | id: string; 8 | 9 | icon: string; 10 | 11 | name: string; 12 | 13 | shortUrl: string; 14 | 15 | scrape: ScrapeFn; 16 | } -------------------------------------------------------------------------------- /src/scraper/typings/enums/types.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Supported entity types that can be scrapped. 3 | * 4 | * Maps an entity type to its url (e.g, `/item/9203`). 5 | */ 6 | export enum Types { 7 | ITEM = 'item', 8 | RECIPE = 'recipe', 9 | QUEST = 'quest', 10 | NPC = 'npc', 11 | MATERIAL_GROUP = 'materialgroup', 12 | KNOWLEDGE = 'theme', 13 | } -------------------------------------------------------------------------------- /src/query/typings/enums/item-as.enum.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Equivalent to the `type` property. 3 | */ 4 | export enum ItemAs { 5 | PRODUCT = 'product', 6 | MATERIAL = 'material', 7 | SELL_SPECIAL_ITEM = 'sellspecialitems', 8 | NPC_DROP = 'npcdropgroups', 9 | NODE_DROP = 'nodedrop', 10 | CONTAINER = 'container', 11 | QUEST_REWARD = 'questrewards', 12 | } -------------------------------------------------------------------------------- /src/scraper/typings/refs/material-group.ref.ts: -------------------------------------------------------------------------------- 1 | import * as Entities from "../entities"; 2 | import { ScrapeFn } from "../types"; 3 | 4 | export interface MaterialGroup { 5 | type: 'material_group'; 6 | 7 | id: string; 8 | 9 | icon: string; 10 | 11 | name: string; 12 | 13 | shortUrl: string; 14 | 15 | scrape: ScrapeFn; 16 | } -------------------------------------------------------------------------------- /src/query/typings/refs/item.ref.ts: -------------------------------------------------------------------------------- 1 | import { Scrapers } from "../../../scraper"; 2 | 3 | export interface Item { 4 | type: 'item'; 5 | 6 | /** The item id. */ 7 | id: string; 8 | 9 | /** The item icon. */ 10 | icon: string; 11 | 12 | /** The item short url. */ 13 | shortUrl: string; 14 | 15 | scrape: Scrapers.ScrapeFn; 16 | } -------------------------------------------------------------------------------- /src/scraper/typings/entities/consumable.entity.ts: -------------------------------------------------------------------------------- 1 | import { Item } from "./item.entity"; 2 | 3 | export interface Consumable extends Item { 4 | /** A list of effects caused by the consumption of the item. */ 5 | effects: string[]; 6 | 7 | /** Effects duration in seconds. */ 8 | duration: number; 9 | 10 | /** Cooldown in seconds. */ 11 | cooldown: number; 12 | } -------------------------------------------------------------------------------- /src/scraper/typings/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export { Pricings } from "./pricings.interface"; 2 | export { Options } from "./options.interface"; 3 | export { Result } from "./result.interface"; 4 | export { Scrape } from "./scrape.interface"; 5 | export * as Equipments from "./equipments"; 6 | export * as Quests from "./quests"; 7 | export * as Recipes from "./recipes"; 8 | export * as NPCs from "./npcs"; -------------------------------------------------------------------------------- /tests/queries/obtained-from/json/10103.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "item", 3 | "url": "https://bdocodex.com/query.php?a=items&type=container&id=10103&l=us", 4 | "data": [{ 5 | "type": "item", 6 | "shortUrl": "/us/item/44931/", 7 | "id": "44931", 8 | "icon": "/items/new_icon/03_etc/00044907.png", 9 | "name": "Offensive Sub-weapon Box", 10 | "lvl": 1 11 | }] 12 | } -------------------------------------------------------------------------------- /src/query/typings/interfaces/descriptor.interface.ts: -------------------------------------------------------------------------------- 1 | import { Groups, ItemAs } from "../enums"; 2 | 3 | /** 4 | * Describes a BDOCodex query with a remapped interface. 5 | */ 6 | export interface Descriptor { 7 | /** Refers to the `a` parameter in a BDOCodex query. */ 8 | readonly group: Groups; 9 | 10 | /** Refers to the `type` parameter in a BDOCodex query. */ 11 | readonly itemAs: ItemAs; 12 | } -------------------------------------------------------------------------------- /tests/utils/fetch-mock.ts: -------------------------------------------------------------------------------- 1 | import * as cache from "./cache"; 2 | import { fetch } from "../../src/shared/utils"; 3 | 4 | export const fetchMock = async (url: string, key: string): Promise => { 5 | if (cache.has(key)) { 6 | const str = cache.get(key); 7 | return str === 'null' ? null : str; 8 | } 9 | const data = await fetch(url); 10 | return cache.set(key, data || 'null'); 11 | } -------------------------------------------------------------------------------- /src/scraper/typings/entities/worker.entity.ts: -------------------------------------------------------------------------------- 1 | import * as Refs from "../refs"; 2 | import * as Workers from "../interfaces/npcs/workers"; 3 | import { Generic } from "./generic.entity"; 4 | 5 | export interface Worker extends Generic { 6 | sellable: boolean; 7 | 8 | max_base_stats: Workers.Stats; 9 | 10 | levels: Workers.Level[]; 11 | 12 | growth: Workers.Growth; 13 | 14 | obtained_from: Refs.NPC; 15 | } -------------------------------------------------------------------------------- /src/query/typings/interfaces/quests/rewards.interface.ts: -------------------------------------------------------------------------------- 1 | import { Reward } from "./reward.interface"; 2 | 3 | export interface Rewards { 4 | /** Rewards that are always received when the quest is finished. */ 5 | standard: Reward[]; 6 | 7 | /** The user must choose one before completion. */ 8 | choose: Reward[]; 9 | 10 | /** Array of amity received. Each index is for a different NPC. */ 11 | amity: number[]; 12 | } -------------------------------------------------------------------------------- /tests/scrapers/npc/json/23720.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "23720", 3 | "icon": "/items/ui_artwork/ic_00559.png", 4 | "name": "Abandoned Iron Mine Executor", 5 | "description": "", 6 | "mob_type": "normal", 7 | "lvl": 55, 8 | "hp": 174007, 9 | "defense": 557, 10 | "evasion": 457, 11 | "dmg_reduction": 100, 12 | "exp": 2505749, 13 | "exp_skill": 808444, 14 | "karma": 80, 15 | "boss": false 16 | } -------------------------------------------------------------------------------- /src/shared/typings/bdocodex/query/sorteable-row.interface.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Indicates a table column that can be sorted through a sort value. 3 | * 4 | * These columns use a different data structure than regular ones inside the 5 | * query result array. 6 | */ 7 | export interface SorteableColumn { 8 | /** String containing the actual value of the row. */ 9 | readonly display: string; 10 | 11 | readonly sort_value: string; 12 | } -------------------------------------------------------------------------------- /tests/scrapers/item/json/4901.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "4901", 3 | "icon": "/items/new_icon/03_etc/07_productmaterial/00004901.png", 4 | "name": "Black Stone Powder", 5 | "name_alt": "블랙스톤 가루", 6 | "type": "item", 7 | "category": "Crafting Materials", 8 | "description": "A basic ingredient used in Crafting.", 9 | "prices": { 10 | "buy": 8250, 11 | "sell": 330 12 | }, 13 | "grade": 0, 14 | "weight": 0.1 15 | } -------------------------------------------------------------------------------- /src/query/typings/refs/material-group.ref.ts: -------------------------------------------------------------------------------- 1 | import { Scrapers } from "../../../scraper"; 2 | 3 | export interface MaterialGroup { 4 | type: 'material_group'; 5 | 6 | /** The material group id. */ 7 | id: string; 8 | 9 | /** An icon to represent the material group. */ 10 | icon: string; 11 | 12 | /** The material group short url. */ 13 | shortUrl: string; 14 | 15 | scrape: Scrapers.ScrapeFn; 16 | } -------------------------------------------------------------------------------- /src/query/typings/interfaces/query.interface.ts: -------------------------------------------------------------------------------- 1 | import { Types } from "../enums"; 2 | import { Result } from "./result.interface"; 3 | import { Options } from "./options.interface"; 4 | import { Descriptor } from "./descriptor.interface"; 5 | 6 | export interface Query { 7 | (id: string, type: Types): Promise>; 8 | (id: string, type: Types, options: Options): Promise>; 9 | (id: string, type: Descriptor): Promise>; 10 | } -------------------------------------------------------------------------------- /src/query/typings/interfaces/result.interface.ts: -------------------------------------------------------------------------------- 1 | import { EntityTypes } from "../types"; 2 | 3 | /** 4 | * The return object of a Query. 5 | */ 6 | export interface Result { 7 | /** The type of the entities inside the `data` property. */ 8 | readonly type: EntityTypes | null; 9 | 10 | /** The parsed url that was used to perform this query. */ 11 | readonly url: string; 12 | 13 | /** The query results. */ 14 | readonly data: T[]; 15 | } -------------------------------------------------------------------------------- /src/query/typings/entities/node.entity.ts: -------------------------------------------------------------------------------- 1 | import { Generic } from "./generic.entity"; 2 | 3 | export interface Node extends Generic { 4 | type: 'node'; 5 | 6 | /** The zone where the node is located. */ 7 | zone: string; 8 | 9 | /** The temperature as a floating point. */ 10 | temperature: number; 11 | 12 | /** The humidity as a floating point. */ 13 | humidity: number; 14 | 15 | /** The water level as a floating point. */ 16 | water: number; 17 | } -------------------------------------------------------------------------------- /src/shared/typings/bdocodex/enums.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Maps stats identifiers to the stats keys. 3 | */ 4 | export enum Stats { 5 | HP = 'hp', 6 | MP = 'mp', 7 | DAMAGE = 'damage', 8 | DEFENSE = 'defense', 9 | ACCURACY = 'accuracy', 10 | EVASION = 'evasion', 11 | DMG_REDUCTION = 'dreduction', 12 | H_DAMAGE = 'hdamage', 13 | H_DEFENSE = 'hdefense', 14 | H_ACCURACY = 'haccuracy', 15 | H_EVASION = 'hevasion', 16 | H_DMG_REDUCTION = 'hdreduction', 17 | } -------------------------------------------------------------------------------- /src/shared/typings/bdocodex/query/results/item.interface.ts: -------------------------------------------------------------------------------- 1 | export interface Item { 2 | readonly aaData: { 3 | /** Item id. */ 4 | readonly 0: string; 5 | 6 | /** HTML for the icon. */ 7 | readonly 1: string; 8 | 9 | /** HTML for the item name. */ 10 | readonly 2: string; 11 | 12 | /** Item level. */ 13 | readonly 3: number; 14 | 15 | readonly 4: string; 16 | 17 | readonly 5: string; 18 | }[]; 19 | } -------------------------------------------------------------------------------- /tests/queries/invalid.spec.ts: -------------------------------------------------------------------------------- 1 | import "../utils/chai.config"; 2 | import { Queries } from "../../src"; 3 | import QueryMock from "../utils/query-mock"; 4 | import { expect } from "chai"; 5 | 6 | describe('Invalid queries', () => { 7 | let result: Queries.Result; 8 | 9 | before(async () => { 10 | result = await QueryMock('715003123133', Queries.Types.OBTAINED_FROM); 11 | }); 12 | 13 | it('#should be null', () => { 14 | expect(result.type).to.be.null; 15 | }); 16 | }); -------------------------------------------------------------------------------- /tests/queries/dropped-in-node/json/15135.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "node", 3 | "url": "https://bdocodex.com/query.php?a=nodes&type=nodedrop&id=15135&l=us", 4 | "data": [{ 5 | "type": "node", 6 | "shortUrl": "/us/node/1157/", 7 | "id": "1157", 8 | "icon": "/images/node_icons/icon_node_9.png", 9 | "name": "Helms Post", 10 | "zone": "Republic of Mediah", 11 | "temperature": 50, 12 | "humidity": 20, 13 | "water": 28 14 | }] 15 | } -------------------------------------------------------------------------------- /src/scraper/typings/entities/index.ts: -------------------------------------------------------------------------------- 1 | export { Generic } from "./generic.entity"; 2 | export { Item } from "./item.entity"; 3 | export { MaterialGroup } from "./material-group.entity"; 4 | export { Equipment } from "./equipment.entity"; 5 | export { Consumable } from "./consumable.entity"; 6 | export { Recipe } from "./recipe.entity"; 7 | export { Quest } from "./quest.entity"; 8 | export { NPC } from "./npc.entity"; 9 | export { Worker } from "./worker.entity"; 10 | export { Knowledge } from "./knowledge.entity"; -------------------------------------------------------------------------------- /src/scraper/typings/enums/ctgs.enum.ts: -------------------------------------------------------------------------------- 1 | export enum Ctgs { 2 | UNKNOWN = "unknown", 3 | EQUIPMENT = "equipment", 4 | CRAFTING_MATERIAL = "crafting_material", 5 | CONSUMABLE = "consumable", 6 | INSTALLABLE_OBJECT = "installable_object", 7 | SPECIAL_ITEM = "special_item", 8 | RECIPE = "recipe", 9 | QUEST = "quest", 10 | NPC = "npc", 11 | WORKER = "worker", 12 | MATERIAL_GROUP = "material_group", 13 | } -------------------------------------------------------------------------------- /src/shared/typings/bdocodex/query/results/npc-drop.interface.ts: -------------------------------------------------------------------------------- 1 | export interface NPCDrop { 2 | readonly aaData: { 3 | /** NPC id. */ 4 | readonly 0: string; 5 | 6 | /** HTML for the icon. */ 7 | readonly 1: string; 8 | 9 | /** HTML for the name of NPC. */ 10 | readonly 2: string; 11 | 12 | /** Quantity as a string. */ 13 | readonly 3: string; 14 | 15 | /** Drop chance in string format as a porcentage. */ 16 | readonly 4: string; 17 | }[]; 18 | } -------------------------------------------------------------------------------- /src/query/typings/entities/generic.entity.ts: -------------------------------------------------------------------------------- 1 | import { EntityTypes } from "../types"; 2 | 3 | export interface Generic { 4 | /** Indicates whcih properties are to be expected from the object. */ 5 | type: EntityTypes; 6 | 7 | /** An entity has a unique identifier. */ 8 | id: string; 9 | 10 | /** An entity has an icon. */ 11 | icon: string; 12 | 13 | /** An entity has a name. */ 14 | name: string; 15 | 16 | /** A shortened version of the entity url without the database base url. */ 17 | shortUrl: string; 18 | } -------------------------------------------------------------------------------- /src/scraper/typings/enums/stats.ts: -------------------------------------------------------------------------------- 1 | export enum Stats { 2 | // The base stat value. 3 | DAMAGE = 'damage', 4 | DEFENSE = 'defense', 5 | ACCURACY = 'accuracy', 6 | EVASION = 'evasion', 7 | DMG_REDUCTION = 'dmg_reduction', 8 | 9 | // The stat bonus value (inside the parenthesis). 10 | H_DAMAGE = 'h_damage', 11 | H_DEFENSE = 'h_defense', 12 | H_ACCURACY = 'h_accuracy', 13 | H_EVASION = 'h_evasion', 14 | H_DMG_REDUCTION = 'h_dmg_reduction', 15 | 16 | // Extra stats. 17 | HP = 'hp', 18 | MP = 'mp', 19 | } -------------------------------------------------------------------------------- /tests/scrapers/item/json/4085.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "4085", 3 | "icon": "/items/new_icon/03_etc/07_productmaterial/00004085.png", 4 | "name": "Melted Noc Shard", 5 | "name_alt": "녹아내린 녹 조각", 6 | "type": "item", 7 | "category": "Crafting Materials", 8 | "description": "Material that has been processed and may be used during crafting. It may also be changed to a different form through alchemy or processing.", 9 | "prices": { 10 | "buy": 12500, 11 | "sell": 500 12 | }, 13 | "grade": 0, 14 | "weight": 0.3 15 | } -------------------------------------------------------------------------------- /src/scraper/typings/entities/generic.entity.ts: -------------------------------------------------------------------------------- 1 | export interface Generic { 2 | /** The entity id. */ 3 | id: string; 4 | 5 | /** The entity icon. */ 6 | icon: string; 7 | 8 | /** The entity name. */ 9 | name: string; 10 | 11 | /** The entity alternative name if available. */ 12 | name_alt?: string; 13 | 14 | /** The entity type. */ 15 | type: string; 16 | 17 | /** The entity category if available. */ 18 | category?: string; 19 | 20 | /** The entity description if available. */ 21 | description?: string; 22 | } -------------------------------------------------------------------------------- /tests/utils/cache.ts: -------------------------------------------------------------------------------- 1 | import { join } from "path"; 2 | import fs from "fs"; 3 | 4 | const parsePath = (key: string): string => { 5 | return join(__dirname, '../data/', key + '.txt'); 6 | } 7 | 8 | export const has = (key: string): boolean => { 9 | return fs.existsSync(parsePath(key)); 10 | } 11 | 12 | export const get = (key: string): string => { 13 | return fs.readFileSync(parsePath(key), { encoding: 'utf-8' }); 14 | } 15 | 16 | export const set = (key: string, data: string): string => { 17 | fs.writeFileSync(parsePath(key), data); 18 | return data; 19 | } -------------------------------------------------------------------------------- /tests/scrapers/consumable/json/741.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "741", 3 | "icon": "/items/new_icon/03_etc/08_potion/00000741.png", 4 | "name": "Strong Griffon's Elixir", 5 | "name_alt": "강력한 그리폰의 비약", 6 | "type": "item", 7 | "category": "Consumable", 8 | "description": "Elixir with an offensive function.", 9 | "prices": { 10 | "buy": 5850, 11 | "sell": 1950 12 | }, 13 | "grade": 2, 14 | "weight": 0.7, 15 | "effects": [ 16 | "Additional Damage Against Kamasylvian Monsters +17" 17 | ], 18 | "duration": 480, 19 | "cooldown": 10 20 | } -------------------------------------------------------------------------------- /tests/scrapers/item/non-existent.spec.ts: -------------------------------------------------------------------------------- 1 | import "../../utils/chai.config"; 2 | import { describe } from "mocha"; 3 | import { expect } from "chai"; 4 | import ScrapeMock, { Scrapers } from "../../utils/scrape-mock"; 5 | 6 | /** 7 | * https://bdocodex.com/us/item/872173/ 8 | */ 9 | describe('Non-existent Items', () => { 10 | let result: Scrapers.Result; 11 | 12 | before(async () => { 13 | result = await ScrapeMock('872173', Scrapers.Types.ITEM); 14 | }); 15 | 16 | it('should return nothing', () => { 17 | expect(result.data).to.be.null; 18 | }); 19 | }); -------------------------------------------------------------------------------- /tests/scrapers/consumable/json/9422.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "9422", 3 | "icon": "/items/new_icon/03_etc/07_productmaterial/00009422.png", 4 | "name": "Dark Pudding", 5 | "name_alt": "암흑 푸딩", 6 | "type": "item", 7 | "category": "Consumable", 8 | "description": "A pudding made with suspicious ingredients.", 9 | "prices": { 10 | "buy": 40000, 11 | "sell": 1600 12 | }, 13 | "grade": 2, 14 | "weight": 0.1, 15 | "effects": [ 16 | "All AP +3", 17 | "Damage Against Humans +2" 18 | ], 19 | "duration": 5400, 20 | "cooldown": 1800 21 | } -------------------------------------------------------------------------------- /tests/scrapers/consumable/json/9213.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "9213", 3 | "icon": "/items/new_icon/03_etc/07_productmaterial/00009213.png", 4 | "name": "Beer", 5 | "name_alt": "맥주", 6 | "type": "item", 7 | "category": "Consumable", 8 | "description": "A mild alcoholic drink brewed from cereal grains", 9 | "prices": { 10 | "buy": 2150, 11 | "sell": 86 12 | }, 13 | "grade": 1, 14 | "weight": 0.1, 15 | "effects": [ 16 | "Worker Stamina Recovery +2", 17 | "(Use through the Worker Menu on the World Map)." 18 | ], 19 | "duration": 0, 20 | "cooldown": 0 21 | } -------------------------------------------------------------------------------- /tests/scrapers/consumable/json/781.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "781", 3 | "icon": "/items/new_icon/03_etc/08_potion/00000781.png", 4 | "name": "Spirit Perfume Elixir", 5 | "name_alt": "정령의 향수 비약", 6 | "type": "item", 7 | "category": "Consumable", 8 | "description": "Elixir with an offensive function.", 9 | "prices": { 10 | "buy": 46620, 11 | "sell": 15540 12 | }, 13 | "grade": 3, 14 | "weight": 0.5, 15 | "effects": [ 16 | "MAX HP +300", 17 | "Critical Hit Rate +5", 18 | "MP/WP/SP +3 per every good hit" 19 | ], 20 | "duration": 1200, 21 | "cooldown": 1200 22 | } -------------------------------------------------------------------------------- /tests/scrapers/knowledge/json/103.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "103", 3 | "icon": "/items/ui_artwork/ic_00103.png", 4 | "name": "Caresto Fonti", 5 | "type": "theme", 6 | "description": "\"Do all things cheerfully and without hesitation.\" That is his motto.\nHe sent his wife and children to Heidel Castle City to ensure their safety.\nStrangely not many people know that he has a wife and children.", 7 | "group": "Serendia Merchant", 8 | "obtained_from": { 9 | "type": "npc", 10 | "id": "41003", 11 | "icon": "/items/ui_artwork/ic_00103.png", 12 | "name": "Caresto Fonti", 13 | "shortUrl": "/us/npc/41003/" 14 | } 15 | } -------------------------------------------------------------------------------- /tests/scrapers/knowledge/json/58.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "58", 3 | "icon": "/items/ui_artwork/ic_00058.png", 4 | "name": "Hans", 5 | "type": "theme", 6 | "description": "A Vigilante stationed at the town entrance of Velia.\nHans guides visiting adventurers, but he also prevents suspicious strangers from entering the town.\n\nHe says he volunteers because he has no job and nothing better to do with his time.", 7 | "group": "Residents of Velia", 8 | "obtained_from": { 9 | "type": "npc", 10 | "id": "40133", 11 | "icon": "/items/ui_artwork/ic_00058.png", 12 | "name": "Hans", 13 | "shortUrl": "/us/npc/40133/" 14 | } 15 | } -------------------------------------------------------------------------------- /src/shared/typings/bdocodex/query/results/node-drop.interface.ts: -------------------------------------------------------------------------------- 1 | export interface NodeDrop { 2 | readonly aaData: { 3 | /** Node id. */ 4 | readonly 0: string; 5 | 6 | /** HTML for the icon. */ 7 | readonly 1: string; 8 | 9 | /** HTML for the node name. */ 10 | readonly 2: string; 11 | 12 | /** Zone name. */ 13 | readonly 3: string; 14 | 15 | /** Temperature in string as a porcentage. */ 16 | readonly 4: string; 17 | 18 | /** Humidity in string as a porcentage. */ 19 | readonly 5: string; 20 | 21 | /** Water in string as a porcentage. */ 22 | readonly 6: string; 23 | }[]; 24 | } -------------------------------------------------------------------------------- /src/shared/context-cache/context-cache.ts: -------------------------------------------------------------------------------- 1 | export class ContextCache { 2 | private readonly contexts: Record = {}; 3 | 4 | for>(ctx: string) { 5 | if (!this.contexts[ctx]) 6 | this.contexts[ctx] = {}; 7 | const layer = this.contexts[ctx] as T; 8 | return { 9 | set: (key: keyof T, val: R): R => { 10 | layer[key] = val as any; 11 | return layer[key]; 12 | }, 13 | get: (key: keyof T): R => { 14 | return layer[key]; 15 | }, 16 | has: (key: keyof T): boolean => { 17 | return !!layer[key]; 18 | } 19 | } 20 | } 21 | } -------------------------------------------------------------------------------- /src/query/factory.ts: -------------------------------------------------------------------------------- 1 | import * as AppUtils from "../shared/utils"; 2 | import * as Utils from "./utils"; 3 | import * as Queries from "./typings"; 4 | import { Query as QueryClass } from "./query"; 5 | import { Scrape } from "../scraper"; 6 | 7 | export const Query: Queries.Query = async ( 8 | id: string, 9 | type: Queries.Types | Queries.Descriptor, 10 | options?: Queries.Options 11 | ): Promise> => { 12 | let q = (typeof type === 'object' && type) || Utils.mapQueryType(type); 13 | 14 | const query = new QueryClass( 15 | id, 16 | q.group, 17 | q.itemAs, 18 | options?.locale, 19 | AppUtils.fetch, 20 | Scrape, 21 | ); 22 | 23 | return await query.parse(); 24 | } -------------------------------------------------------------------------------- /src/query/typings/entities/npc.entity.ts: -------------------------------------------------------------------------------- 1 | import { Scrapers } from "../../../scraper"; 2 | import { Generic } from "./generic.entity"; 3 | 4 | export interface NPC extends Generic { 5 | type: 'npc'; 6 | 7 | /** The level of the NPC. */ 8 | lvl: number; 9 | 10 | /** How much health points the NPC has. */ 11 | hp: number; 12 | 13 | /** How much defense the NPC has. */ 14 | defense: number; 15 | 16 | /** How much evasion the NPC has. */ 17 | evasion: number; 18 | 19 | /** How much exp the NPC gives. */ 20 | exp: number; 21 | 22 | /** How much skill exp the NPC gives. */ 23 | exp_skill: number; 24 | 25 | /** How much karma the NPC has. */ 26 | karma: number; 27 | 28 | scrape: Scrapers.ScrapeFn; 29 | } -------------------------------------------------------------------------------- /src/shared/utils/filter-obj.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Removes properties whose value is considered undefined. 3 | * 4 | * @param obj - The object to be filtered. 5 | * @param filterFn - The function that defines what is undefined. 6 | */ 7 | export const filterObj = >( 8 | obj: T, 9 | filterFn?: (obj: T, key: keyof T) => boolean, 10 | ): T => { 11 | const fn = filterFn || ((obj, key) => ( 12 | obj[key] !== undefined || ( 13 | typeof obj[key] === 'number' && 14 | obj[key] === obj[key] 15 | ) 16 | )); 17 | return Object.keys(obj).reduce((filtered, key) => { 18 | if (fn(obj, key)) 19 | return { ...filtered, [key]: obj[key] }; 20 | return filtered; 21 | }, {} as T); 22 | } -------------------------------------------------------------------------------- /tests/scrapers/npc/json/21304.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "21304", 3 | "icon": "/items/ui_artwork/ic_04924.png", 4 | "name": "Aakman Airbender", 5 | "description": "Aakman Airbenders shoot black energy balls that contain illusions. Getting hit by these projectiles will cause your memory to fade.", 6 | "mob_type": "normal", 7 | "lvl": 60, 8 | "hp": 585219, 9 | "defense": 807, 10 | "evasion": 667, 11 | "dmg_reduction": 140, 12 | "exp": 19251798, 13 | "exp_skill": 182943, 14 | "karma": 28, 15 | "knowledge": { 16 | "type": "knowledge", 17 | "id": "4924", 18 | "icon": "/items/ui_artwork/ic_04924.png", 19 | "name": "Aakman Airbender", 20 | "shortUrl": "/us/theme/4924/", 21 | "drop_chance": 2.5 22 | } 23 | } -------------------------------------------------------------------------------- /src/shared/typings/bdocodex/enchantment/level.interface.ts: -------------------------------------------------------------------------------- 1 | import { Stats } from "../enums"; 2 | 3 | /** 4 | * The enhancement data for each level (+1, +2, etc). 5 | */ 6 | export interface Level extends Record { 7 | readonly enchant_chance: string; 8 | 9 | readonly durability: string; 10 | 11 | readonly cron_value: string; 12 | 13 | readonly cron_tvalue: string; 14 | 15 | readonly edescription: string; 16 | 17 | readonly need_enchant_item_id: string; 18 | 19 | readonly need_enchant_item_icon: string; 20 | 21 | readonly need_enchant_item_name: string; 22 | 23 | readonly enchant_item_counter: string; 24 | 25 | readonly pe_item_counter: string; 26 | 27 | readonly fail_dura_dec: string; 28 | 29 | readonly pe_dura_dec: string; 30 | } -------------------------------------------------------------------------------- /src/query/builders/item.builder.ts: -------------------------------------------------------------------------------- 1 | import * as Queries from "../typings"; 2 | import { BDOCodex } from "../../shared/typings"; 3 | import { Generic } from "./generic.builder"; 4 | 5 | export class Item extends Generic { 6 | static get type() { 7 | return "item"; 8 | } 9 | 10 | build(data: BDOCodex.Query.Item): Queries.Entities.Item[] { 11 | return data.aaData.map(arr => { 12 | const url = this.parseShortURL(arr[2]); 13 | 14 | return { 15 | type: Item.type, 16 | id: arr[0], 17 | icon: this.parseIconURL(arr[1]), 18 | name: this.parseName(arr[2]), 19 | lvl: arr[3], 20 | shortUrl: url, 21 | scrape: this.ScrapeFactory(url), 22 | }; 23 | }) 24 | } 25 | } -------------------------------------------------------------------------------- /src/query/typings/entities/recipe.entity.ts: -------------------------------------------------------------------------------- 1 | import * as Recipes from "../interfaces/recipes"; 2 | import { Scrapers } from "../../../scraper"; 3 | import { Generic } from "./generic.entity"; 4 | 5 | export interface Recipe extends Generic { 6 | type: 'recipe'; 7 | 8 | /** The process used to craft the recipe (e.g, "Simple Cooking"). */ 9 | process?: string; 10 | 11 | /** The exp received on successful craft. */ 12 | exp: number; 13 | 14 | /** The required skill level to craft the recipe. */ 15 | skill_lvl: Recipes.SkillLvl; 16 | 17 | /** A list of items required to craft the recipe. */ 18 | materials: Recipes.Material[]; 19 | 20 | /** A list of possible items acquired from a successful craft. */ 21 | products: Recipes.Material[]; 22 | 23 | scrape: Scrapers.ScrapeFn; 24 | } -------------------------------------------------------------------------------- /src/scraper/typings/entities/recipe.entity.ts: -------------------------------------------------------------------------------- 1 | import * as Recipes from "../interfaces/recipes"; 2 | import { Generic } from "./generic.entity"; 3 | 4 | export interface Recipe extends Generic { 5 | /** The process used to craft the recipe (e.g, "Simple Cooking"). */ 6 | process: string; 7 | 8 | /** The exp received on successful craft. */ 9 | exp: number; 10 | 11 | /** The required skill level to craft the recipe. */ 12 | skill_lvl: { 13 | /** The mastery name (e.g, Beginner). */ 14 | mastery: string; 15 | 16 | /** The mastery level. */ 17 | lvl: number; 18 | }; 19 | 20 | /** A list of items required to craft the recipe. */ 21 | materials: Recipes.Material[]; 22 | 23 | /** A list of possible items acquired from a successful craft. */ 24 | products: Recipes.Material[]; 25 | } -------------------------------------------------------------------------------- /src/shared/typings/bdocodex/query/results/npc.interface.ts: -------------------------------------------------------------------------------- 1 | import { SorteableColumn } from "../sorteable-row.interface"; 2 | 3 | export interface NPC { 4 | readonly aaData: { 5 | /** NPC id. */ 6 | readonly 0: SorteableColumn; 7 | 8 | /** HTML for the icon. */ 9 | readonly 1: string; 10 | 11 | /** HTML for the NPC name. */ 12 | readonly 2: string; 13 | 14 | /** Level. */ 15 | readonly 3: string; 16 | 17 | /** HP. */ 18 | readonly 4: string; 19 | 20 | /** Defense. */ 21 | readonly 5: string; 22 | 23 | /** Evasion. */ 24 | readonly 6: string; 25 | 26 | /** EXP. */ 27 | readonly 7: string; 28 | 29 | /** Skill EXP. */ 30 | readonly 8: string; 31 | 32 | /** Karma. */ 33 | readonly 9: string; 34 | }[]; 35 | } -------------------------------------------------------------------------------- /src/query/typings/entities/quest.entity.ts: -------------------------------------------------------------------------------- 1 | import * as Quests from "../interfaces/quests"; 2 | import { Scrapers } from "../../../scraper"; 3 | import { Generic } from "./generic.entity"; 4 | 5 | export interface Quest extends Generic { 6 | type: 'quest'; 7 | 8 | /** The level required to unlock */ 9 | lvl: number; 10 | 11 | /** The region where the player can get the quest. */ 12 | region: string; 13 | 14 | /** The exp given by completing the quest. */ 15 | exp: number; 16 | 17 | /** The skill exp given by completing the quest. */ 18 | exp_skill: number; 19 | 20 | /** The contribution exp given by completing the quest. */ 21 | exp_contribution: number; 22 | 23 | /** The rewards received by completing the quest. */ 24 | rewards: Quests.Rewards; 25 | 26 | scrape: Scrapers.ScrapeFn; 27 | } -------------------------------------------------------------------------------- /src/shared/utils/clean-str.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Removes decoration and custom characters trailing a string. 3 | * 4 | * These are the decoration characters that will always be removed: ` -–\n`. 5 | * 6 | * @param str - The string to be cleaned. 7 | * @param chars - Additional trailing characters to be removed. 8 | */ 9 | export const cleanStr = (str?: string, chars?: string): string => { 10 | if (!str) 11 | return ''; 12 | 13 | const record = ((chars || '') + ' -–\n').split('').reduce( 14 | (obj, char) => ({ ...obj, [char]: true }), 15 | {} as Record, 16 | ); 17 | 18 | let startIdx = 0, endIdx = str.length - 1; 19 | while (record[str[startIdx]]) 20 | startIdx++; 21 | while (record[str[endIdx]] && endIdx >= startIdx) 22 | endIdx--; 23 | 24 | return str.substring(startIdx, endIdx + 1); 25 | } -------------------------------------------------------------------------------- /tests/scrapers/npc/json/23746.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "23746", 3 | "icon": "/items/ui_artwork/ic_04919.png", 4 | "name": "Agrakhan", 5 | "description": "Leader of Cadry Ruins, and Muskan's brother. He attacked the Cadry Shrine, rich with ancient traces, in order to revive Kzarka in Valencia. The Cadry Shrine turned into ruins, and Agrakhan is still trying to revive Kzarka using the black power.", 6 | "mob_type": "awakened_boss", 7 | "lvl": 58, 8 | "hp": 412814, 9 | "defense": 533, 10 | "evasion": 463, 11 | "dmg_reduction": 70, 12 | "exp": 1486625, 13 | "exp_skill": 914716, 14 | "karma": 28, 15 | "knowledge": { 16 | "type": "knowledge", 17 | "id": "4919", 18 | "icon": "/items/ui_artwork/ic_04919.png", 19 | "name": "Agrakhan", 20 | "shortUrl": "/us/theme/4919/", 21 | "drop_chance": 15 22 | } 23 | } -------------------------------------------------------------------------------- /src/shared/utils/short-url.ts: -------------------------------------------------------------------------------- 1 | import { App } from "../../shared/typings"; 2 | 3 | /** 4 | * Decomposes a BDOCodex short url into locale, type and id. 5 | * 6 | * @param shortUrl - The url to be decomposed. 7 | */ 8 | export const decomposeShortURL = (shortUrl: string) => { 9 | const args = shortUrl.split('/').filter(e => e); 10 | return { 11 | locale: args[0] as App.Locales, 12 | type: args[1], 13 | id: args.slice(2).join('/'), 14 | } 15 | } 16 | 17 | /** 18 | * Composes a BDO short url. 19 | * 20 | * @param id - The entity id. 21 | * @param type - The entity type. 22 | * @param locale - An accepted locale. 23 | */ 24 | export const composeShortURL = ( 25 | id: string, 26 | type: string, 27 | locale: App.Locales.US, 28 | ): string => { 29 | if (!id || !type || !locale) 30 | return undefined as unknown as string; 31 | return '/' + [locale, type, id].join('/') + '/' 32 | } -------------------------------------------------------------------------------- /tests/utils/scrape-mock.ts: -------------------------------------------------------------------------------- 1 | import { App } from "../../src/shared/typings"; 2 | import { Scrapers } from "../../src" 3 | import { Scraper } from "../../src/scraper/scraper"; 4 | import { fetchMock } from "./fetch-mock"; 5 | import QueryMock from "./query-mock"; 6 | 7 | const ScrapeMock: Scrapers.Scrape = async ( 8 | id: string, 9 | type: Scrapers.Types, 10 | options?: Scrapers.Options, 11 | ): Promise> => { 12 | const locale = options?.locale || App.Locales.US; 13 | 14 | const fetch = (url: string) => fetchMock(url, [ 15 | "scrape", 16 | locale, 17 | type, 18 | id.replace('/', '-'), 19 | ].join("-")); 20 | 21 | return await new Scraper( 22 | id, 23 | locale, 24 | type, 25 | fetch, 26 | QueryMock, 27 | ScrapeMock, 28 | ).parse(); 29 | } 30 | 31 | export default ScrapeMock; 32 | export { Scrapers } from "../../src"; -------------------------------------------------------------------------------- /src/query/builders/npc-drop.builder.ts: -------------------------------------------------------------------------------- 1 | import * as AppUtils from "../../shared/utils"; 2 | import * as Queries from "../typings"; 3 | import { BDOCodex } from "../../shared/typings"; 4 | import { Generic } from "./generic.builder"; 5 | 6 | export class NPCDrop extends Generic { 7 | static get type() { 8 | return "npc_drop"; 9 | } 10 | 11 | build(data: BDOCodex.Query.NPCDrop): Queries.Entities.NPCDrop[] { 12 | return data.aaData.map(arr => { 13 | const url = this.parseShortURL(arr[2]); 14 | 15 | return { 16 | type: NPCDrop.type, 17 | id: arr[0], 18 | icon: this.parseIconURL(arr[1]), 19 | name: this.parseName(arr[2]), 20 | amount: AppUtils.parseIntValue(arr[3], 1), 21 | chance: AppUtils.parsePercentageValue(arr[4]), 22 | shortUrl: url, 23 | } 24 | }); 25 | } 26 | } -------------------------------------------------------------------------------- /src/shared/typings/bdocodex/query/results/quest.interface.ts: -------------------------------------------------------------------------------- 1 | import { SorteableColumn } from "../sorteable-row.interface"; 2 | 3 | export interface Quest { 4 | readonly aaData: { 5 | /** Quest id. */ 6 | readonly 0: SorteableColumn; 7 | 8 | /** HTML for the icon. */ 9 | readonly 1: string; 10 | 11 | /** HTML for the quest name. */ 12 | readonly 2: string; 13 | 14 | /** Level required to take the quest. */ 15 | readonly 3: string; 16 | 17 | /** Region. */ 18 | readonly 4: SorteableColumn 19 | 20 | /** EXP. */ 21 | readonly 5: SorteableColumn; 22 | 23 | /** Skill EXP. */ 24 | readonly 6: SorteableColumn; 25 | 26 | /** Contribution EXP. */ 27 | readonly 7: string; 28 | 29 | /** Big weird HTML string containing rewards. */ 30 | readonly 8: string; 31 | 32 | readonly 9: string; 33 | 34 | readonly 10: string; 35 | }[]; 36 | } -------------------------------------------------------------------------------- /src/scraper/typings/entities/equipment.entity.ts: -------------------------------------------------------------------------------- 1 | import * as Equipments from "../interfaces/equipments"; 2 | import { Item } from "./item.entity"; 3 | 4 | export interface Equipment extends Item { 5 | /** The base stats of the equipment. */ 6 | stats: Equipments.Stats; 7 | 8 | /** An array containing the data for each enhancement level. */ 9 | enhancement_stats: Equipments.Enhancement[]; 10 | 11 | /** An object containing the data for each caphras upgrades. */ 12 | caphras_stats: Equipments.Caphras.Wrapper; 13 | 14 | /** A list of effects caused by the equipment. */ 15 | item_effects: string[]; 16 | 17 | /** The effects caused by equiping 2 or more of the same equipment group. */ 18 | set_effects: Record; 19 | 20 | /** Classes that can equip the item. Empty means every character can equip. */ 21 | exclusive_to: string[]; 22 | 23 | /** The exp produced if the equipment is fed to a fairy. */ 24 | fairy_exp: number; 25 | } -------------------------------------------------------------------------------- /src/shared/typings/bdocodex/query/results/recipe.ts: -------------------------------------------------------------------------------- 1 | import { SorteableColumn } from "../sorteable-row.interface"; 2 | 3 | export interface Recipe { 4 | readonly aaData: { 5 | /** Entity id. */ 6 | readonly 0: string; 7 | 8 | /** HTML for the icon. */ 9 | readonly 1: string; 10 | 11 | /** HTML for the recipe name. */ 12 | readonly 2: string; 13 | 14 | /** Processing type. */ 15 | readonly 3: string; 16 | 17 | /** The required level to complete this recipe. */ 18 | readonly 4: SorteableColumn; 19 | 20 | /** EXP. */ 21 | readonly 5: string; 22 | 23 | /** HTML for the materials. */ 24 | readonly 6: string; 25 | 26 | /** HTML for the products. */ 27 | readonly 7: string; 28 | 29 | /** A stringified array containing the materials ids. */ 30 | readonly 8: string; 31 | 32 | readonly 9: string; 33 | 34 | readonly 10: string; 35 | 36 | readonly 11: string; 37 | }[]; 38 | } -------------------------------------------------------------------------------- /src/query/builders/node.builder.ts: -------------------------------------------------------------------------------- 1 | import * as AppUtils from "../../shared/utils"; 2 | import * as Queries from "../typings"; 3 | import { BDOCodex } from "../../shared/typings"; 4 | import { Generic } from "./generic.builder"; 5 | 6 | export class Node extends Generic { 7 | static get type() { 8 | return "node"; 9 | } 10 | 11 | build(data: BDOCodex.Query.NodeDrop): Queries.Entities.Node[] { 12 | return data.aaData.map(arr => { 13 | const url = this.parseShortURL(arr[2]); 14 | 15 | return { 16 | type: Node.type, 17 | id: arr[0], 18 | icon: this.parseIconURL(arr[1]), 19 | name: this.parseName(arr[2]), 20 | zone: arr[3], 21 | temperature: AppUtils.parsePercentageValue(arr[4]), 22 | humidity: AppUtils.parsePercentageValue(arr[5]), 23 | water: AppUtils.parsePercentageValue(arr[6]), 24 | shortUrl: url, 25 | }; 26 | }); 27 | } 28 | } -------------------------------------------------------------------------------- /src/scraper/typings/entities/npc.entity.ts: -------------------------------------------------------------------------------- 1 | import * as NPCs from "../interfaces/npcs"; 2 | import { Generic } from "./generic.entity"; 3 | 4 | export interface NPC extends Generic { 5 | /** Available if it's a killable monster. */ 6 | mob_type?: NPCs.Type; 7 | 8 | /** The level of the NPC. */ 9 | lvl?: number; 10 | 11 | /** The amount of HP the NPC has. */ 12 | hp?: number; 13 | 14 | /** The amount of defense the NPC has. */ 15 | defense?: number; 16 | 17 | /** The amount of evasion the NPC has. */ 18 | evasion?: number; 19 | 20 | /** The amount of damage reduction the NPC has. */ 21 | dmg_reduction?: number; 22 | 23 | /** The exp points given by the NPC when killed. */ 24 | exp?: number; 25 | 26 | /** The skill exp points given by the NPC when killed. */ 27 | exp_skill?: number; 28 | 29 | /** The karma given by the NPC when killed. */ 30 | karma?: number; 31 | 32 | /** A NPC may give a knowledge when interacted with. */ 33 | knowledge?: NPCs.Knowledge; 34 | } -------------------------------------------------------------------------------- /tests/queries/npc-drop/all.spec.ts: -------------------------------------------------------------------------------- 1 | import "../../utils/chai.config"; 2 | import { describe } from "mocha"; 3 | import { expect } from "chai"; 4 | import QueryMock, { Queries } from "../../utils/query-mock"; 5 | 6 | describe('Query for NPC Drops', () => { 7 | /** 8 | * https://bdocodex.com/us/item/6158/ 9 | * Fancy Feather 10 | */ 11 | describe('6158', () => { 12 | const expected: Queries.Result = require('./json/6158.json'); 13 | let result: Queries.Result; 14 | 15 | before(async () => { 16 | result = await QueryMock('6158', Queries.Types.NPC_DROPS); 17 | }); 18 | 19 | it('#type', () => { 20 | expect(result.type).to.equal(expected.type); 21 | }); 22 | 23 | it('#url', () => { 24 | expect(result.url).to.equal(expected.url); 25 | }); 26 | 27 | it('#data', () => { 28 | expect(result.data).to.containSubset(expected.data); 29 | }); 30 | }); 31 | }); -------------------------------------------------------------------------------- /tests/queries/obtained-from/all.spec.ts: -------------------------------------------------------------------------------- 1 | import "../../utils/chai.config"; 2 | import { describe } from "mocha"; 3 | import { expect } from "chai"; 4 | import QueryMock, { Queries } from "../../utils/query-mock"; 5 | 6 | describe('Query for Obtained from', () => { 7 | /** 8 | * https://bdocodex.com/us/item/10103/ 9 | * Axion Shield 10 | */ 11 | describe('10103', () => { 12 | const expected: Queries.Result = require('./json/10103.json'); 13 | let result: Queries.Result; 14 | 15 | before(async () => { 16 | result = await QueryMock('10103', Queries.Types.OBTAINED_FROM); 17 | }); 18 | 19 | it('#type', () => { 20 | expect(result.type).to.equal(expected.type); 21 | }); 22 | 23 | it('#url', () => { 24 | expect(result.url).to.equal(expected.url); 25 | }); 26 | 27 | it('#data', () => { 28 | expect(result.data).to.containSubset(expected.data); 29 | }); 30 | }); 31 | }); -------------------------------------------------------------------------------- /tests/queries/sold-by-npc/all.spec.ts: -------------------------------------------------------------------------------- 1 | import "../../utils/chai.config"; 2 | import { describe } from "mocha"; 3 | import { expect } from "chai"; 4 | import QueryMock, { Queries } from "../../utils/query-mock"; 5 | 6 | describe('Query for Quest Rewards', () => { 7 | /** 8 | * https://bdocodex.com/us/item/13210/ 9 | * Kzarka Shortsword 10 | */ 11 | describe('13210', () => { 12 | const expected: Queries.Result = require('./json/13210.json'); 13 | let result: Queries.Result; 14 | 15 | before(async () => { 16 | result = await QueryMock('13210', Queries.Types.SOLD_BY_NPC); 17 | }); 18 | 19 | it('#type', () => { 20 | expect(result.type).to.equal(expected.type); 21 | }); 22 | 23 | it('#url', () => { 24 | expect(result.url).to.equal(expected.url); 25 | }); 26 | 27 | it('#data', () => { 28 | expect(result.data).to.containSubset(expected.data); 29 | }); 30 | }); 31 | }); -------------------------------------------------------------------------------- /tests/queries/quest-rewards/all.spec.ts: -------------------------------------------------------------------------------- 1 | import "../../utils/chai.config"; 2 | import { describe } from "mocha"; 3 | import { expect } from "chai"; 4 | import QueryMock, { Queries } from "../../utils/query-mock"; 5 | 6 | describe('Query for Quest Rewards', () => { 7 | /** 8 | * https://bdocodex.com/us/item/519/ 9 | * HP Potion (Large) 10 | */ 11 | describe('519', () => { 12 | const expected: Queries.Result = require('./json/519.json'); 13 | let result: Queries.Result; 14 | 15 | before(async () => { 16 | result = await QueryMock('519', Queries.Types.QUEST_REWARD); 17 | }); 18 | 19 | it('#type', () => { 20 | expect(result.type).to.equal(expected.type); 21 | }); 22 | 23 | it('#url', () => { 24 | expect(result.url).to.equal(expected.url); 25 | }); 26 | 27 | it('#data[0]', () => { 28 | expect(result.data[0]).to.containSubset(expected.data[0]); 29 | }); 30 | }); 31 | }); -------------------------------------------------------------------------------- /tests/queries/product-in-design/all.spec.ts: -------------------------------------------------------------------------------- 1 | import "../../utils/chai.config"; 2 | import { describe } from "mocha"; 3 | import { expect } from "chai"; 4 | import QueryMock, { Queries } from "../../utils/query-mock"; 5 | 6 | describe('Query for Product in Design', () => { 7 | /** 8 | * https://bdocodex.com/us/item/10103/ 9 | * Axion Shield 10 | */ 11 | describe('10103', () => { 12 | const expected: Queries.Result = require('./json/10103.json'); 13 | let result: Queries.Result; 14 | 15 | before(async () => { 16 | result = await QueryMock('10103', Queries.Types.PRODUCT_IN_DESIGN); 17 | }); 18 | 19 | it('#type', () => { 20 | expect(result.type).to.equal(expected.type); 21 | }); 22 | 23 | it('#url', () => { 24 | expect(result.url).to.equal(expected.url); 25 | }); 26 | 27 | it('#data[0]', () => { 28 | expect(result.data[0]).to.containSubset(expected.data[0]); 29 | }); 30 | }); 31 | }); -------------------------------------------------------------------------------- /tests/utils/query-mock.ts: -------------------------------------------------------------------------------- 1 | import * as QueryUtils from "../../src/query/utils"; 2 | import { Queries } from "../../src"; 3 | import { Query as QueryClass } from "../../src/query/query"; 4 | import { fetchMock } from "./fetch-mock"; 5 | import ScrapeMock from "./scrape-mock"; 6 | 7 | const QueryMock: Queries.Query = async ( 8 | id: string, 9 | type: Queries.Types | Queries.Descriptor, 10 | options?: Queries.Options, 11 | ): Promise> => { 12 | let q = (typeof type === 'object' && type) || QueryUtils.mapQueryType(type); 13 | 14 | const fetch = (url: string) => fetchMock(url, [ 15 | "query", 16 | options?.locale, 17 | q.group, 18 | q.itemAs, 19 | id.replace('/', '-'), 20 | ].join("-")); 21 | 22 | const query = new QueryClass( 23 | id, 24 | q.group, 25 | q.itemAs, 26 | options?.locale, 27 | fetch, 28 | ScrapeMock, 29 | ); 30 | 31 | return await query.parse(); 32 | } 33 | 34 | export default QueryMock; 35 | export { Queries } from "../../src"; -------------------------------------------------------------------------------- /tests/queries/material-in-processing/all.spec.ts: -------------------------------------------------------------------------------- 1 | import "../../utils/chai.config"; 2 | import { describe } from "mocha"; 3 | import { expect } from "chai"; 4 | import QueryMock, { Queries } from "../../utils/query-mock"; 5 | 6 | describe('Query for Material in Processing', () => { 7 | /** 8 | * https://bdocodex.com/us/item/10406/ 9 | * Ain Amulet 10 | */ 11 | describe('10406', () => { 12 | const expected: Queries.Result = require('./json/10406.json'); 13 | let result: Queries.Result; 14 | 15 | before(async () => { 16 | result = await QueryMock('10406', Queries.Types.MATERIAL_IN_PROCESSING); 17 | }); 18 | 19 | it('#type', () => { 20 | expect(result.type).to.equal(expected.type); 21 | }); 22 | 23 | it('#url', () => { 24 | expect(result.url).to.equal(expected.url); 25 | }); 26 | 27 | it('#data[0]', () => { 28 | expect(result.data[0]).to.containSubset(expected.data[0]); 29 | }); 30 | }); 31 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "calpheonjs", 3 | "version": "1.0.2", 4 | "description": "Interface to retrieve data about the Black Desert Online game.", 5 | "main": "dist/index.js", 6 | "types": "dist/index.d.ts", 7 | "author": "Marcelo Perrella ", 8 | "license": "MIT", 9 | "type": "commonjs", 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/marceloclp/calpheonjs" 13 | }, 14 | "publishConfig": { 15 | "registry": "https://npm.pkg.github.com/" 16 | }, 17 | "scripts": { 18 | "watch": "tsc --watch", 19 | "build": "tsc", 20 | "start": "node ./dist/index.js", 21 | "test": "mocha -r ts-node/register tests/**/*.spec.ts --reporter min", 22 | "prepublish": "tsc" 23 | }, 24 | "devDependencies": { 25 | "@types/chai": "^4.2.11", 26 | "@types/chai-subset": "^1.3.3", 27 | "@types/cheerio": "^0.22.18", 28 | "@types/mocha": "^7.0.2", 29 | "chai": "^4.2.0", 30 | "chai-subset": "^1.6.0", 31 | "mocha": "^7.2.0", 32 | "ts-node": "^8.10.1", 33 | "typescript": "^3.9.3" 34 | }, 35 | "dependencies": { 36 | "cheerio": "^1.0.0-rc.3" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /tests/queries/npc-drop/json/6158.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "npc_drop", 3 | "url": "https://bdocodex.com/query.php?a=drop&type=npcdropgroups&id=6158&l=us", 4 | "data": [{ 5 | "type": "npc_drop", 6 | "shortUrl": "/us/npc/20091/", 7 | "id": "20091", 8 | "icon": "/items/ui_artwork/ic_04075.png", 9 | "name": "Fan Flamingo", 10 | "amount": 1, 11 | "chance": 4.15 12 | }, { 13 | "type": "npc_drop", 14 | "shortUrl": "/us/npc/21536/", 15 | "id": "21536", 16 | "icon": "/items/ui_artwork/ic_05016.png", 17 | "name": "Silk Shade Flamingo", 18 | "amount": 1, 19 | "chance": 4.15 20 | }, { 21 | "type": "npc_drop", 22 | "shortUrl": "/us/npc/23049/", 23 | "id": "23049", 24 | "icon": "/items/ui_artwork/collected_ecology.png", 25 | "name": "Humpback Whale", 26 | "amount": 1, 27 | "chance": 6.225 28 | }, { 29 | "type": "npc_drop", 30 | "shortUrl": "/us/npc/23063/", 31 | "id": "23063", 32 | "icon": "/items/ui_artwork/ic_00559.png", 33 | "name": "Wandering Gargoyle", 34 | "amount": 1, 35 | "chance": 6.225 36 | }] 37 | } -------------------------------------------------------------------------------- /src/query/typings/enums/types.enum.ts: -------------------------------------------------------------------------------- 1 | export enum Types { 2 | /** Queries for processing recipes where the item is one of the products. */ 3 | PRODUCT_IN_PROCESSING, 4 | 5 | /** Queries for cooking recipes or simple alchemy/cooking where the item is one of the products. */ 6 | PRODUCT_IN_RECIPE, 7 | 8 | /** Queries for design recipes where the item is one of the products. */ 9 | PRODUCT_IN_DESIGN, 10 | 11 | /** Queries for processing recipes where the item is one of the materials. */ 12 | MATERIAL_IN_PROCESSING, 13 | 14 | /** Queries for cooking or simple alchemy/cooking where the item is one of the materials. */ 15 | MATERIAL_IN_RECIPE, 16 | 17 | /** Queries for design recipes where the item is one of the materials. */ 18 | MATERIAL_IN_DESIGN, 19 | 20 | /** Queries for npcs that drop the item. */ 21 | NPC_DROPS, 22 | 23 | /** Queries for nodes that drop the item. */ 24 | DROPPED_IN_NODE, 25 | 26 | /** Queries for items that when used can drop the item (e.g, item boxes). */ 27 | OBTAINED_FROM, 28 | 29 | /** Queries for npcs that sell the item. */ 30 | SOLD_BY_NPC, 31 | 32 | /** Queries for quests where the item is one of the rewards. */ 33 | QUEST_REWARD, 34 | } -------------------------------------------------------------------------------- /src/scraper/typings/entities/quest.entity.ts: -------------------------------------------------------------------------------- 1 | import * as Refs from "../refs"; 2 | import * as Quests from "../interfaces/quests"; 3 | import { Generic } from "./generic.entity"; 4 | 5 | export interface Quest extends Generic { 6 | /** The position of the quest in the quest chain if one exists. */ 7 | stage?: number; 8 | 9 | /** The region the quest belongs to. */ 10 | region: string; 11 | 12 | /** The quest category. */ 13 | q_category: string; 14 | 15 | /** The quest type. */ 16 | q_type: string; 17 | 18 | /** The level required to accept the quest. */ 19 | lvl: number; 20 | 21 | /** If the quest is exclusive to some classes. */ 22 | exclusive_to: string[]; 23 | 24 | /** The quest chain this quest belongs to. */ 25 | quest_chain: Refs.Quest[]; 26 | 27 | /** The NPC who will give the quest. */ 28 | npc_start?: Refs.NPC; 29 | 30 | /** The NPC who will finish the quest. */ 31 | npc_end?: Refs.NPC; 32 | 33 | /** The quest description. */ 34 | description?: string; 35 | 36 | /** A quest usually contains a poem-like text for storytelling purposes. */ 37 | text: string[]; 38 | 39 | /** Rewards received on quest completion. */ 40 | rewards: Quests.Rewards; 41 | } -------------------------------------------------------------------------------- /src/query/builders/npc.builder.ts: -------------------------------------------------------------------------------- 1 | import * as AppUtils from "../../shared/utils"; 2 | import * as Queries from "../typings"; 3 | import { BDOCodex } from "../../shared/typings"; 4 | import { Generic } from "./generic.builder"; 5 | 6 | export class NPC extends Generic { 7 | static get type() { 8 | return "npc"; 9 | } 10 | 11 | build(data: BDOCodex.Query.NPC): Queries.Entities.NPC[] { 12 | return data.aaData.map(arr => { 13 | const url = this.parseShortURL(arr[2]); 14 | 15 | return { 16 | type: NPC.type, 17 | id: arr[0].display, 18 | icon: this.parseIconURL(arr[1]), 19 | name: this.parseName(arr[2]), 20 | lvl: AppUtils.parseIntValue(arr[3], 1), 21 | hp: AppUtils.parseIntValue(arr[4]), 22 | defense: AppUtils.parseIntValue(arr[5]), 23 | evasion: AppUtils.parseIntValue(arr[6]), 24 | exp: AppUtils.parseIntValue(arr[7]), 25 | exp_skill: AppUtils.parseIntValue(arr[8]), 26 | karma: AppUtils.parseIntValue(arr[9]), 27 | shortUrl: url, 28 | scrape: this.ScrapeFactory(url), 29 | }; 30 | }); 31 | } 32 | } -------------------------------------------------------------------------------- /src/scraper/typings/entities/item.entity.ts: -------------------------------------------------------------------------------- 1 | import { Generic } from "./generic.entity"; 2 | import { Pricings } from "../interfaces"; 3 | import { Queries } from "../../../query"; 4 | 5 | export interface Item extends Generic { 6 | /** The prices for the item if available. */ 7 | prices: Pricings; 8 | 9 | /** The grade of the item as 0-based. */ 10 | grade: number; 11 | 12 | /** Weight as a floating number in LT. */ 13 | weight: number; 14 | 15 | npc_drops?: Queries.QueryFn; 16 | 17 | quest_rewards?: Queries.QueryFn; 18 | 19 | product_of_recipes?: Queries.QueryFn; 20 | 21 | product_of_processing?: Queries.QueryFn; 22 | 23 | product_of_design?: Queries.QueryFn; 24 | 25 | material_of_recipes?: Queries.QueryFn; 26 | 27 | material_of_processing?: Queries.QueryFn; 28 | 29 | material_of_design?: Queries.QueryFn; 30 | 31 | dropped_in_node?: Queries.QueryFn; 32 | 33 | obtained_from?: Queries.QueryFn; 34 | 35 | sold_by_npc?: Queries.QueryFn; 36 | } -------------------------------------------------------------------------------- /tests/scrapers/material-group/json/1.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "1", 3 | "icon": "/items/new_icon/03_etc/07_productmaterial/00007001.png", 4 | "name": "Cereals", 5 | "type": "materialgroup", 6 | "category": "Item group", 7 | "items": [{ 8 | "type": "item", 9 | "id": "7001", 10 | "icon": "/items/new_icon/03_etc/07_productmaterial/00007001.png", 11 | "name": "Wheat", 12 | "shortUrl": "/us/item/7001/" 13 | }, { 14 | "type": "item", 15 | "id": "7002", 16 | "icon": "/items/new_icon/03_etc/07_productmaterial/00007002.png", 17 | "name": "Barley", 18 | "shortUrl": "/us/item/7002/" 19 | }, { 20 | "type": "item", 21 | "id": "7003", 22 | "icon": "/items/new_icon/03_etc/07_productmaterial/00007003.png", 23 | "name": "Potato", 24 | "shortUrl": "/us/item/7003/" 25 | }, { 26 | "type": "item", 27 | "id": "7004", 28 | "icon": "/items/new_icon/03_etc/07_productmaterial/00007004.png", 29 | "name": "Sweet Potato", 30 | "shortUrl": "/us/item/7004/" 31 | }, { 32 | "type": "item", 33 | "id": "7005", 34 | "icon": "/items/new_icon/03_etc/07_productmaterial/00007005.png", 35 | "name": "Corn", 36 | "shortUrl": "/us/item/7005/" 37 | }] 38 | } -------------------------------------------------------------------------------- /src/shared/utils/parse-values.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Converts values to integers. If the value can't be converted, it will return 3 | * a default value. 4 | * 5 | * @param raw - The value to be converted to integer. 6 | * @param val - The default value in case the conversion fails. 7 | */ 8 | export const parseIntValue = (raw?: string | number, val = 0) => { 9 | if (typeof raw === 'number') 10 | return raw; 11 | if (typeof raw === 'string') 12 | return parseInt(raw.replace(/\D/g, '')) || val; 13 | return val; 14 | } 15 | 16 | /** 17 | * Converts values to floating points. If the value can't be converted, it will 18 | * return a default value. 19 | * 20 | * @param raw - The value to be converted to floating point. 21 | * @param val - The default value in case the conversion fails. 22 | */ 23 | export const parseFloatValue = (raw?: string | number, val = 0) => { 24 | if (typeof raw === 'number') 25 | return raw; 26 | if (typeof raw === 'string') 27 | return parseFloat(raw.replace(/\D/g, '')) || val; 28 | return val; 29 | } 30 | 31 | /** 32 | * Converts a percentage string of shape "xx.x%" into a floating point. If the 33 | * value can't be converted, it will return a default value. 34 | * 35 | * @param raw - The string to be converted. 36 | * @param val - The default value in case the conversion fails. 37 | */ 38 | export const parsePercentageValue = (raw: string, val = 0) => { 39 | return parseFloat(raw.replace(/\%/g, '')) || val; 40 | } -------------------------------------------------------------------------------- /src/scraper/typings/interfaces/equipments/enhancement.interface.ts: -------------------------------------------------------------------------------- 1 | import * as Refs from "../../refs"; 2 | import { Stats } from "./stats.interface"; 3 | 4 | export interface Enhancement { 5 | stats: Stats; 6 | 7 | /** The chance of success as a floating point. */ 8 | success_rate: number; 9 | 10 | /** Max durability at a given enhancement level. */ 11 | durability: number; 12 | 13 | /** The required amount of Cron stones. */ 14 | cron_values: { 15 | /** For the next level. */ 16 | next_lvl: number; 17 | 18 | /** The sum of all the levels before and the current one. */ 19 | total: number; 20 | } 21 | 22 | effects: { 23 | /** The effects caused by the enhancement level. */ 24 | enhancement: string[]; 25 | 26 | /** The effects caused by the item on a given enchantment level. */ 27 | item: string[]; 28 | } 29 | 30 | /** The item required to perform the enhancement. */ 31 | required_enhancement_item?: Refs.Item & { 32 | /** The needed amount of the required item. */ 33 | amount: number; 34 | 35 | /** The durability lost if the enhancement fails. */ 36 | durability_loss_on_failure: number; 37 | }; 38 | 39 | perfect_enhancement?: { 40 | /** The needed amount of the required item. */ 41 | amount: number; 42 | 43 | /** The durability lost if the enhancement fails. */ 44 | durability_loss_on_failure: number; 45 | }; 46 | } -------------------------------------------------------------------------------- /tests/queries/dropped-in-node/json/10656.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "node", 3 | "url": "https://bdocodex.com/query.php?a=nodes&type=nodedrop&id=10656&l=us", 4 | "data": [{ 5 | "type": "node", 6 | "shortUrl": "/us/node/1132/", 7 | "id": "1132", 8 | "icon": "/images/node_icons/icon_node_9.png", 9 | "name": "Sausan Garrison", 10 | "zone": "Republic of Mediah", 11 | "temperature": 60, 12 | "humidity": 10, 13 | "water": 20 14 | }, { 15 | "type": "node", 16 | "shortUrl": "/us/node/1137/", 17 | "id": "1137", 18 | "icon": "/images/node_icons/icon_node_9.png", 19 | "name": "Manes Hideout", 20 | "zone": "Republic of Mediah", 21 | "temperature": 60, 22 | "humidity": 20, 23 | "water": 22 24 | }, { 25 | "type": "node", 26 | "shortUrl": "/us/node/1157/", 27 | "id": "1157", 28 | "icon": "/images/node_icons/icon_node_9.png", 29 | "name": "Helms Post", 30 | "zone": "Republic of Mediah", 31 | "temperature": 50, 32 | "humidity": 20, 33 | "water": 28 34 | }, { 35 | "type": "node", 36 | "shortUrl": "/us/node/1166/", 37 | "id": "1166", 38 | "icon": "/images/node_icons/icon_node_0.png", 39 | "name": "Sausan Garrison Wharf", 40 | "zone": "Republic of Mediah", 41 | "temperature": 60, 42 | "humidity": 10, 43 | "water": 20 44 | }] 45 | } -------------------------------------------------------------------------------- /src/scraper/builders/material-group/material-group.builder.ts: -------------------------------------------------------------------------------- 1 | import * as AppUtils from "../../../shared/utils"; 2 | import * as Scrapers from "../../typings"; 3 | import { Generic } from "../generic.builder"; 4 | 5 | export class MaterialGroup extends Generic { 6 | static get(): typeof Generic { 7 | return MaterialGroup; 8 | } 9 | 10 | static get type(): string { 11 | return "material_group"; 12 | } 13 | 14 | get items(): Scrapers.Refs.Item[] { 15 | const nodes = this.$('hr.hr_long') 16 | .parent() 17 | .children() 18 | .toArray(); 19 | return nodes.reduce((items, node, i, arr) => { 20 | if (node.tagName !== 'div') 21 | return items; 22 | 23 | const elem = this.$(node); 24 | const icon = elem.find('img').attr('src') as string; 25 | const url = elem.find('a').attr('href') as string; 26 | 27 | return [...items, { 28 | type: 'item', 29 | id: AppUtils.decomposeShortURL(url).id, 30 | icon, 31 | name: this.$(arr[i+1]).text(), 32 | shortUrl: url, 33 | scrape: this.ScrapeFactory(url), 34 | }]; 35 | }, [] as Scrapers.Refs.Item[]); 36 | } 37 | 38 | async build(): Promise { 39 | const items = this.items; 40 | return { 41 | ...(await super.build()), 42 | icon: items[0].icon, 43 | items, 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /tests/queries/material-in-processing/json/10406.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "recipe", 3 | "url": "https://bdocodex.com/query.php?a=mrecipes&type=material&item_id=10406&l=us", 4 | "data": [{ 5 | "type": "recipe", 6 | "shortUrl": "/us/mrecipe/749/", 7 | "id": "749", 8 | "icon": "/items/new_icon/03_etc/07_productmaterial/00004051.png", 9 | "name": "Melted Iron Shard", 10 | "process": "Heating", 11 | "skill_lvl": { 12 | "mastery": "Beginner", 13 | "lvl": 0 14 | }, 15 | "exp": 0, 16 | "materials": [{ 17 | "type": "item", 18 | "id": "10406", 19 | "shortUrl": "/us/item/10406/", 20 | "icon": "/items/new_icon/06_pc_equipitem/00_common/01_weapon/00010406.png", 21 | "amount": 1 22 | }], 23 | "products": [{ 24 | "type": "item", 25 | "id": "4051", 26 | "shortUrl": "/us/item/4051/", 27 | "icon": "/items/new_icon/03_etc/07_productmaterial/00004051.png", 28 | "amount": 3 29 | }, { 30 | "type": "item", 31 | "id": "5956", 32 | "shortUrl": "/us/item/5956/", 33 | "icon": "/items/new_icon/03_etc/07_productmaterial/00005956.png", 34 | "amount": 1 35 | }, { 36 | "type": "item", 37 | "id": "4062", 38 | "shortUrl": "/us/item/4062/", 39 | "icon": "/items/new_icon/03_etc/07_productmaterial/00004062.png", 40 | "amount": 1 41 | }] 42 | }] 43 | } -------------------------------------------------------------------------------- /src/shared/utils/fetch.ts: -------------------------------------------------------------------------------- 1 | import https from "https"; 2 | 3 | /** 4 | * Fetches a url and retrieves the payload as a string. 5 | * 6 | * If the data's length exceeds a certain amount, it will be assumed it's an 7 | * invalid url and it will return `null`. This is required because when querying 8 | * BDOCodex, if there is no match for the query parameters, it will return a 9 | * huge payload. 10 | * 11 | * @param url - The url to be fetched. 12 | */ 13 | export const fetch = async (url: string): Promise => { 14 | return new Promise((resolve, reject) => { 15 | const req = https.get(url, (res) => { 16 | const { statusCode } = res; 17 | 18 | if (statusCode !== 200) { 19 | throw new Error(`Request failed. Status Code: ${statusCode}`); 20 | } 21 | 22 | res.setEncoding('utf-8'); 23 | 24 | let payload = ''; 25 | res.on('data', (chunk) => { 26 | // Ignore huge payloads. 27 | if (payload.length > 3500000) { 28 | res.destroy(); 29 | resolve(null); 30 | } 31 | payload += chunk; 32 | }); 33 | 34 | res.on('end', () => { 35 | try { 36 | resolve(payload); 37 | } catch (e) { 38 | reject(e); 39 | } 40 | }); 41 | }); 42 | 43 | req.on('error', (e) => { 44 | reject(e); 45 | }); 46 | 47 | req.end(); 48 | }); 49 | } -------------------------------------------------------------------------------- /src/query/builders/generic.builder.ts: -------------------------------------------------------------------------------- 1 | import cheerio from "cheerio"; 2 | import * as AppUtils from "../../shared/utils"; 3 | import * as Queries from "../typings"; 4 | import { App } from "../../shared/typings"; 5 | import { Scrapers } from "../../scraper"; 6 | 7 | export class Generic { 8 | static get type(): Queries.EntityTypes { 9 | return "unknown"; 10 | } 11 | 12 | constructor( 13 | protected readonly _locale = App.Locales.US, 14 | 15 | protected readonly _scrape: Scrapers.Scrape, 16 | ) {} 17 | 18 | protected ScrapeFactory(shortUrl: string): Scrapers.ScrapeFn { 19 | const { type, id } = AppUtils.decomposeShortURL(shortUrl); 20 | return async () => this._scrape(id, type as Scrapers.Types, { 21 | locale: this._locale 22 | }); 23 | } 24 | 25 | /** Parses the icon url from an html raw string. */ 26 | protected parseIconURL(raw: string): string { 27 | const str = raw 28 | .replace('[', '<') 29 | .replace(']', '>'); 30 | return cheerio.load(str)('img').first().attr('src') as string; 31 | } 32 | 33 | /** Parses an entity short url. */ 34 | protected parseShortURL(raw: string): string { 35 | return cheerio.load(raw)('a').attr('href') as string; 36 | } 37 | 38 | /** Parses an entity name from an html raw string. */ 39 | protected parseName(raw: string): string { 40 | const str = cheerio.load(raw) 41 | .root() 42 | .text(); 43 | return AppUtils.cleanStr(str); 44 | } 45 | 46 | build(data: any): any[] { 47 | return []; 48 | } 49 | } -------------------------------------------------------------------------------- /tests/scrapers/material-group/all.spec.ts: -------------------------------------------------------------------------------- 1 | import "../../utils/chai.config"; 2 | import { describe } from "mocha"; 3 | import { expect } from "chai"; 4 | import ScrapeMock, { Scrapers } from "../../utils/scrape-mock"; 5 | 6 | describe('Material Groups', () => { 7 | /** 8 | * https://bdocodex.com/us/materialgroup/1/ 9 | * Cereals 10 | */ 11 | describe('1', () => { 12 | const expected: Scrapers.Entities.MaterialGroup = require('./json/1.json'); 13 | let result: Scrapers.Result; 14 | 15 | before(async () => { 16 | result = await ScrapeMock('1', Scrapers.Types.MATERIAL_GROUP); 17 | }); 18 | 19 | it('#id', () => { 20 | expect(result.data.id).to.equal(expected.id); 21 | }); 22 | 23 | it('#icon', () => { 24 | expect(result.data.icon).to.equal(expected.icon); 25 | }); 26 | 27 | it('#name', () => { 28 | expect(result.data.name).to.equal(expected.name); 29 | }); 30 | 31 | it('#name_alt', () => { 32 | expect(result.data.name_alt).to.equal(expected.name_alt); 33 | }); 34 | 35 | it('#type', () => { 36 | expect(result.data.type).to.equal(expected.type); 37 | }); 38 | 39 | it('#category', () => { 40 | expect(result.data.category).to.equal(expected.category); 41 | }); 42 | 43 | it('#description', () => { 44 | expect(result.data.description).to.equal(expected.description); 45 | }); 46 | 47 | it('#items', () => { 48 | expect(result.data.items).to.containSubset(expected.items); 49 | }); 50 | }); 51 | }); -------------------------------------------------------------------------------- /tests/scrapers/equipment/json/13961.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "13961", 3 | "icon": "/items/new_icon/06_pc_equipitem/00_common/01_weapon/00012411.png", 4 | "name": "[Oasis] TRI: Ramones’s Blade", 5 | "name_alt": "[PC방] 고 : 라모네스 도검", 6 | "type": "item", 7 | "category": "Equipment", 8 | "description": "Oasis event exclusive blade.", 9 | "prices": { 10 | "buy": 1, 11 | "repair": 43740 12 | }, 13 | "grade": 3, 14 | "weight": 0.1, 15 | "stats": { 16 | "damage": [106, 110], 17 | "defense": 0, 18 | "accuracy": 184, 19 | "evasion": 0, 20 | "dmg_reduction": 0 21 | }, 22 | "enhancement_stats": [{ 23 | "stats": { 24 | "damage": [106, 110], 25 | "defense": 0, 26 | "accuracy": 184, 27 | "evasion": 0, 28 | "dmg_reduction": 0 29 | }, 30 | "success_rate": 100.0, 31 | "durability": 100, 32 | "cron_values": { 33 | "next_lvl": 0, 34 | "total": 0 35 | }, 36 | "effects": { 37 | "enhancement": [], 38 | "item": [ 39 | "Extra Damage to All Species +18", 40 | "Extra AP against Monsters +6", 41 | "Attack Speed +3 Level" 42 | ] 43 | } 44 | }], 45 | "caphras_stats": { 46 | "18": [], 47 | "19": [], 48 | "20": [] 49 | }, 50 | "item_effects": [ 51 | "Extra Damage to All Species +18", 52 | "Extra AP against Monsters +6", 53 | "Attack Speed +3 Level" 54 | ], 55 | "set_effects": {}, 56 | "exclusive_to": [ 57 | "Musa", 58 | "Maehwa" 59 | ], 60 | "fairy_exp": 0 61 | } -------------------------------------------------------------------------------- /tests/scrapers/equipment/json/703549.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "703549", 3 | "icon": "/items/new_icon/06_pc_equipitem/00_common/01_weapon/00011212.png", 4 | "name": "Arsha's Gauntlet (Accuracy)", 5 | "name_alt": "아르샤의 권갑(적중)", 6 | "type": "item", 7 | "category": "Equipment", 8 | "description": "Arena of Arsha Exclusive Gauntlet.", 9 | "prices": { 10 | "buy": 1, 11 | "repair": 0 12 | }, 13 | "grade": 3, 14 | "weight": 0.1, 15 | "stats": { 16 | "damage": [114, 118], 17 | "defense": 0, 18 | "accuracy": 192, 19 | "evasion": 0, 20 | "dmg_reduction": 0 21 | }, 22 | "enhancement_stats": [{ 23 | "stats": { 24 | "damage": [114, 118], 25 | "defense": 0, 26 | "accuracy": 192, 27 | "evasion": 0, 28 | "dmg_reduction": 0 29 | }, 30 | "success_rate": 100.0, 31 | "durability": 100, 32 | "cron_values": { 33 | "next_lvl": 0, 34 | "total": 0 35 | }, 36 | "effects": { 37 | "enhancement": [], 38 | "item": [ 39 | "Extra Damage to All Species +19", 40 | "Attack Speed +3 Level", 41 | "All Accuracy +16", 42 | "Ignore All Resistance +20%" 43 | ] 44 | } 45 | }], 46 | "caphras_stats": { 47 | "18": [], 48 | "19": [], 49 | "20": [] 50 | }, 51 | "item_effects": [ 52 | "Extra Damage to All Species +19", 53 | "Attack Speed +3 Level", 54 | "All Accuracy +16", 55 | "Ignore All Resistance +20%" 56 | ], 57 | "set_effects": {}, 58 | "exclusive_to": [ 59 | "Striker", 60 | "Mystic" 61 | ], 62 | "fairy_exp": 0 63 | } -------------------------------------------------------------------------------- /tests/queries/dropped-in-node/all.spec.ts: -------------------------------------------------------------------------------- 1 | import "../../utils/chai.config"; 2 | import { describe } from "mocha"; 3 | import { expect } from "chai"; 4 | import QueryMock, { Queries } from "../../utils/query-mock"; 5 | 6 | describe('Query for Dropped in Node', () => { 7 | /** 8 | * https://bdocodex.com/us/item/10656/ 9 | * Krea Axe 10 | */ 11 | describe('10656', () => { 12 | const expected: Queries.Result = require('./json/10656.json'); 13 | let result: Queries.Result; 14 | 15 | before(async () => { 16 | result = await QueryMock('10656', Queries.Types.DROPPED_IN_NODE); 17 | }); 18 | 19 | it('#type', () => { 20 | expect(result.type).to.equal(expected.type); 21 | }); 22 | 23 | it('#url', () => { 24 | expect(result.url).to.equal(expected.url); 25 | }); 26 | 27 | it('#data', () => { 28 | expect(result.data).to.containSubset(expected.data); 29 | }); 30 | }); 31 | 32 | /** 33 | * https://bdocodex.com/us/item/15135/ 34 | * Magic Crystal of Infinity - Siege 35 | */ 36 | describe('15135', () => { 37 | const expected: Queries.Result = require('./json/15135.json'); 38 | let result: Queries.Result; 39 | 40 | before(async () => { 41 | result = await QueryMock('15135', Queries.Types.DROPPED_IN_NODE); 42 | }); 43 | 44 | it('#type', () => { 45 | expect(result.type).to.equal(expected.type); 46 | }); 47 | 48 | it('#url', () => { 49 | expect(result.url).to.equal(expected.url); 50 | }); 51 | 52 | it('#data', () => { 53 | expect(result.data).to.containSubset(expected.data); 54 | }); 55 | }); 56 | }); -------------------------------------------------------------------------------- /tests/queries/product-in-recipe/all.spec.ts: -------------------------------------------------------------------------------- 1 | import "../../utils/chai.config"; 2 | import { describe } from "mocha"; 3 | import { expect } from "chai"; 4 | import QueryMock, { Queries } from "../../utils/query-mock"; 5 | 6 | describe('Query for Product in Recipes', () => { 7 | /** 8 | * https://bdocodex.com/us/item/9205/ 9 | * Aloe Cookie 10 | */ 11 | describe('9205', () => { 12 | const expected: Queries.Result = require('./json/9205.json'); 13 | let result: Queries.Result; 14 | 15 | before(async () => { 16 | result = await QueryMock('9205', Queries.Types.PRODUCT_IN_RECIPE); 17 | }); 18 | 19 | it('#type', () => { 20 | expect(result.type).to.equal(expected.type); 21 | }); 22 | 23 | it('#url', () => { 24 | expect(result.url).to.equal(expected.url); 25 | }); 26 | 27 | it('#data[0]', () => { 28 | expect(result.data[0]).to.containSubset(expected.data[0]); 29 | }); 30 | }); 31 | 32 | /** 33 | * https://bdocodex.com/us/item/9213/ 34 | * Beer 35 | */ 36 | describe('9213', () => { 37 | const expected: Queries.Result = require('./json/9213.json'); 38 | let result: Queries.Result; 39 | 40 | before(async () => { 41 | result = await QueryMock('9213', Queries.Types.PRODUCT_IN_RECIPE); 42 | }); 43 | 44 | it('#type', () => { 45 | expect(result.type).to.equal(expected.type); 46 | }); 47 | 48 | it('#url', () => { 49 | expect(result.url).to.equal(expected.url); 50 | }); 51 | 52 | it('#data[0]', () => { 53 | expect(result.data[0]).to.containSubset(expected.data[0]); 54 | }); 55 | }); 56 | }); -------------------------------------------------------------------------------- /src/query/utils/map-query-type.ts: -------------------------------------------------------------------------------- 1 | import * as Queries from "../typings"; 2 | 3 | const map = (group: Queries.Groups, itemAs: Queries.ItemAs) => ({ group, itemAs }); 4 | 5 | /** 6 | * Mops a query type to the BDOCodex query parameters. 7 | * 8 | * @param type - The query type to be mapped. 9 | * @returns - A query descriptor. 10 | */ 11 | export const mapQueryType = (type: Queries.Types): Queries.Descriptor => { 12 | switch (type) { 13 | case Queries.Types.MATERIAL_IN_PROCESSING: 14 | return map(Queries.Groups.PROCESSING, Queries.ItemAs.MATERIAL); 15 | 16 | case Queries.Types.MATERIAL_IN_RECIPE: 17 | return map(Queries.Groups.RECIPE, Queries.ItemAs.MATERIAL); 18 | 19 | case Queries.Types.MATERIAL_IN_DESIGN: 20 | return map(Queries.Groups.DESIGN, Queries.ItemAs.MATERIAL); 21 | 22 | case Queries.Types.PRODUCT_IN_PROCESSING: 23 | return map(Queries.Groups.PROCESSING, Queries.ItemAs.PRODUCT); 24 | 25 | case Queries.Types.PRODUCT_IN_RECIPE: 26 | return map(Queries.Groups.RECIPE, Queries.ItemAs.PRODUCT); 27 | 28 | case Queries.Types.PRODUCT_IN_DESIGN: 29 | return map(Queries.Groups.DESIGN, Queries.ItemAs.PRODUCT); 30 | 31 | case Queries.Types.NPC_DROPS: 32 | return map(Queries.Groups.DROP, Queries.ItemAs.NPC_DROP); 33 | 34 | case Queries.Types.DROPPED_IN_NODE: 35 | return map(Queries.Groups.NODE, Queries.ItemAs.NODE_DROP); 36 | 37 | case Queries.Types.OBTAINED_FROM: 38 | return map(Queries.Groups.ITEM, Queries.ItemAs.CONTAINER); 39 | 40 | case Queries.Types.SOLD_BY_NPC: 41 | return map(Queries.Groups.NPC, Queries.ItemAs.SELL_SPECIAL_ITEM); 42 | 43 | case Queries.Types.QUEST_REWARD: 44 | return map(Queries.Groups.QUEST, Queries.ItemAs.QUEST_REWARD); 45 | } 46 | } -------------------------------------------------------------------------------- /tests/queries/quest-rewards/json/519.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "quest", 3 | "url": "https://bdocodex.com/query.php?a=quests&type=questrewards&id=519&l=us", 4 | "data": [{ 5 | "type": "quest", 6 | "shortUrl": "/us/quest/4019/1/", 7 | "id": "4019/1", 8 | "icon": "/items/quest/altinova_5.png", 9 | "name": "[Co-op] Stop Illezra's Plot", 10 | "lvl": 0, 11 | "region": "Mediah", 12 | "exp": 0, 13 | "exp_skill": 0, 14 | "exp_contribution": 300, 15 | "rewards": { 16 | "standard": [{ 17 | "type": "item", 18 | "id": "40215", 19 | "icon": "/items/new_icon/03_etc/13_puzzleitem/00040121.png", 20 | "shortUrl": "/us/item/40215/", 21 | "amount": 1 22 | }], 23 | "choose": [{ 24 | "type": "item", 25 | "id": "519", 26 | "icon": "/items/new_icon/03_etc/08_potion/00000519.png", 27 | "shortUrl": "/us/item/519/", 28 | "amount": 10 29 | }, { 30 | "type": "item", 31 | "id": "522", 32 | "icon": "/items/new_icon/03_etc/08_potion/00000522.png", 33 | "shortUrl": "/us/item/522/", 34 | "amount": 10 35 | }, { 36 | "type": "item", 37 | "id": "593", 38 | "icon": "/items/new_icon/03_etc/08_potion/00000593.png", 39 | "shortUrl": "/us/item/593/", 40 | "amount": 10 41 | }, { 42 | "type": "item", 43 | "id": "597", 44 | "icon": "/items/new_icon/03_etc/08_potion/00000597.png", 45 | "shortUrl": "/us/item/597/", 46 | "amount": 10 47 | }], 48 | "amity": [] 49 | } 50 | }] 51 | } -------------------------------------------------------------------------------- /tests/scrapers/recipe/json/122.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "122", 3 | "icon": "/items/new_icon/03_etc/07_productmaterial/00009213.png", 4 | "name": "Beer", 5 | "type": "recipe", 6 | "category": "Recipe", 7 | "process": "Cooking", 8 | "exp": 400, 9 | "skill_lvl": { 10 | "mastery": "Beginner", 11 | "lvl": 1 12 | }, 13 | "materials": [{ 14 | "type": "material_group", 15 | "id": "1", 16 | "icon": "/items/new_icon/03_etc/07_productmaterial/00007005.png", 17 | "name": "Cereals", 18 | "shortUrl": "/us/materialgroup/1/", 19 | "amount": 5 20 | }, { 21 | "type": "item", 22 | "id": "9059", 23 | "icon": "/items/new_icon/03_etc/07_productmaterial/00009059.png", 24 | "name": "Mineral Water", 25 | "shortUrl": "/us/item/9059/", 26 | "grade": 0, 27 | "amount": 6 28 | }, { 29 | "type": "item", 30 | "id": "9002", 31 | "icon": "/items/new_icon/03_etc/07_productmaterial/00009002.png", 32 | "name": "Sugar", 33 | "shortUrl": "/us/item/9002/", 34 | "grade": 0, 35 | "amount": 1 36 | }, { 37 | "type": "item", 38 | "id": "9005", 39 | "icon": "/items/new_icon/03_etc/07_productmaterial/00009005.png", 40 | "name": "Leavening Agent", 41 | "shortUrl": "/us/item/9005/", 42 | "grade": 0, 43 | "amount": 2 44 | }], 45 | "products": [{ 46 | "type": "item", 47 | "id": "9213", 48 | "icon": "/items/new_icon/03_etc/07_productmaterial/00009213.png", 49 | "name": "Beer", 50 | "shortUrl": "/us/item/9213/", 51 | "grade": 1, 52 | "amount": 1 53 | }, { 54 | "type": "item", 55 | "id": "9283", 56 | "icon": "/items/new_icon/03_etc/07_productmaterial/00009283.png", 57 | "name": "Cold Draft Beer", 58 | "shortUrl": "/us/item/9283/", 59 | "grade": 2, 60 | "amount": 1 61 | }] 62 | } -------------------------------------------------------------------------------- /tests/queries/product-in-recipe/json/9213.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "recipe", 3 | "url": "https://bdocodex.com/query.php?a=recipes&type=product&item_id=9213&l=us", 4 | "data": [{ 5 | "type": "recipe", 6 | "shortUrl": "/us/recipe/122/", 7 | "id": "122", 8 | "icon": "/items/new_icon/03_etc/07_productmaterial/00009213.png", 9 | "name": "Beer", 10 | "process": "Cooking", 11 | "skill_lvl": { 12 | "mastery": "Beginner", 13 | "lvl": 1 14 | }, 15 | "exp": 400, 16 | "materials": [{ 17 | "type": "material_group", 18 | "shortUrl": "/us/materialgroup/1/", 19 | "id": "1", 20 | "icon": "/items/new_icon/03_etc/07_productmaterial/00007005.png", 21 | "amount": 5 22 | }, { 23 | "type": "item", 24 | "shortUrl": "/us/item/9059/", 25 | "id": "9059", 26 | "icon": "/items/new_icon/03_etc/07_productmaterial/00009059.png", 27 | "amount": 6 28 | }, { 29 | "type": "item", 30 | "shortUrl": "/us/item/9002/", 31 | "id": "9002", 32 | "icon": "/items/new_icon/03_etc/07_productmaterial/00009002.png", 33 | "amount": 1 34 | }, { 35 | "type": "item", 36 | "shortUrl": "/us/item/9005/", 37 | "id": "9005", 38 | "icon": "/items/new_icon/03_etc/07_productmaterial/00009005.png", 39 | "amount": 2 40 | }], 41 | "products": [{ 42 | "type": "item", 43 | "shortUrl": "/us/item/9213/", 44 | "id": "9213", 45 | "icon": "/items/new_icon/03_etc/07_productmaterial/00009213.png", 46 | "amount": 1 47 | }, { 48 | "type": "item", 49 | "shortUrl": "/us/item/9283/", 50 | "id": "9283", 51 | "icon": "/items/new_icon/03_etc/07_productmaterial/00009283.png", 52 | "amount": 1 53 | }] 54 | }] 55 | } -------------------------------------------------------------------------------- /tests/queries/product-in-recipe/json/9205.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "recipe", 3 | "url": "https://bdocodex.com/query.php?a=recipes&type=product&item_id=9205&l=us", 4 | "data": [{ 5 | "type": "recipe", 6 | "shortUrl": "/us/recipe/115/", 7 | "id": "115", 8 | "icon": "/items/new_icon/03_etc/07_productmaterial/00009205.png", 9 | "name": "Aloe Cookie", 10 | "process": "Cooking", 11 | "skill_lvl": { 12 | "mastery": "Beginner", 13 | "lvl": 6 14 | }, 15 | "exp": 400, 16 | "materials": [{ 17 | "type": "item", 18 | "shortUrl": "/us/item/7347/", 19 | "id": "7347", 20 | "icon": "/items/new_icon/03_etc/07_productmaterial/00007347.png", 21 | "amount": 5 22 | }, { 23 | "type": "material_group", 24 | "shortUrl": "/us/materialgroup/2/", 25 | "id": "2", 26 | "icon": "/items/new_icon/03_etc/07_productmaterial/00007205.png", 27 | "amount": 7 28 | }, { 29 | "type": "material_group", 30 | "shortUrl": "/us/materialgroup/48/", 31 | "id": "48", 32 | "icon": "/items/new_icon/03_etc/07_productmaterial/00007702.png", 33 | "amount": 3 34 | }, { 35 | "type": "item", 36 | "shortUrl": "/us/item/9002/", 37 | "id": "9002", 38 | "icon": "/items/new_icon/03_etc/07_productmaterial/00009002.png", 39 | "amount": 4 40 | }], 41 | "products": [{ 42 | "type": "item", 43 | "shortUrl": "/us/item/9205/", 44 | "id": "9205", 45 | "icon": "/items/new_icon/03_etc/07_productmaterial/00009205.png", 46 | "amount": 1 47 | }, { 48 | "type": "item", 49 | "shortUrl": "/us/item/9294/", 50 | "id": "9294", 51 | "icon": "/items/new_icon/03_etc/07_productmaterial/00009294.png", 52 | "amount": 1 53 | }] 54 | }] 55 | } -------------------------------------------------------------------------------- /src/scraper/factory.ts: -------------------------------------------------------------------------------- 1 | import * as AppUtils from "../shared/utils"; 2 | import * as Scrapers from "./typings"; 3 | import { App } from "../shared/typings"; 4 | import { Scraper } from "./scraper"; 5 | import { Query } from "../query"; 6 | 7 | export const Scrape: Scrapers.Scrape = async ( 8 | id: string, 9 | type: Scrapers.Types, 10 | options?: Scrapers.Options, 11 | ): Promise> => { 12 | const locale = options?.locale || App.Locales.US; 13 | 14 | return await new Scraper( 15 | id, 16 | locale, 17 | type, 18 | AppUtils.fetch, 19 | Query, 20 | Scrape, 21 | ).parse(); 22 | } 23 | 24 | export const Item = (id: string, options?: Scrapers.Options) => Scrape(id, Scrapers.Types.ITEM, options || {}); 25 | 26 | export const Consumable = (id: string, options?: Scrapers.Options) => Scrape(id, Scrapers.Types.ITEM, options || {}); 27 | 28 | export const Equipment = (id: string, options?: Scrapers.Options) => Scrape(id, Scrapers.Types.ITEM, options || {}); 29 | 30 | export const Knowledge = (id: string, options?: Scrapers.Options) => Scrape(id, Scrapers.Types.KNOWLEDGE, options || {}); 31 | 32 | export const MaterialGroup = (id: string, options?: Scrapers.Options) => Scrape(id, Scrapers.Types.MATERIAL_GROUP, options || {}); 33 | 34 | export const NPC = (id: string, options?: Scrapers.Options) => Scrape(id, Scrapers.Types.NPC, options || {}); 35 | 36 | export const Worker = (id: string, options?: Scrapers.Options) => Scrape(id, Scrapers.Types.NPC, options || {}); 37 | 38 | export const Quest = (id: string, options?: Scrapers.Options) => Scrape(id, Scrapers.Types.QUEST, options || {}); 39 | 40 | export const Recipe = (id: string, options?: Scrapers.Options) => Scrape(id, Scrapers.Types.RECIPE, options || {}); 41 | -------------------------------------------------------------------------------- /tests/scrapers/recipe/all.spec.ts: -------------------------------------------------------------------------------- 1 | import "../../utils/chai.config"; 2 | import { describe } from "mocha"; 3 | import { expect } from "chai"; 4 | import ScrapeMock, { Scrapers } from "../../utils/scrape-mock"; 5 | 6 | describe('Recipes', () => { 7 | /** 8 | * https://bdocodex.com/us/recipe/122/ 9 | * Beer 10 | */ 11 | describe('122', () => { 12 | const expected: Scrapers.Entities.Recipe = require('./json/122.json'); 13 | let result: Scrapers.Result; 14 | 15 | before(async () => { 16 | result = await ScrapeMock('122', Scrapers.Types.RECIPE); 17 | }); 18 | 19 | it('#id', () => { 20 | expect(result.data.id).to.equal(expected.id); 21 | }); 22 | 23 | it('#icon', () => { 24 | expect(result.data.icon).to.equal(expected.icon); 25 | }); 26 | 27 | it('#name', () => { 28 | expect(result.data.name).to.equal(expected.name); 29 | }); 30 | 31 | it('#name_alt', () => { 32 | expect(result.data.name_alt).to.equal(expected.name_alt); 33 | }); 34 | 35 | it('#type', () => { 36 | expect(result.data.type).to.equal(expected.type); 37 | }); 38 | 39 | it('#category', () => { 40 | expect(result.data.category).to.equal(expected.category); 41 | }); 42 | 43 | it('#description', () => { 44 | expect(result.data.description).to.equal(expected.description); 45 | }); 46 | 47 | it('#process', () => { 48 | expect(result.data.process).to.equal(expected.process); 49 | }); 50 | 51 | it('#exp', () => { 52 | expect(result.data.exp).to.equal(expected.exp); 53 | }); 54 | 55 | it('#skill_lvl', () => { 56 | expect(result.data.skill_lvl).to.deep.equal(expected.skill_lvl); 57 | }); 58 | 59 | it('#materials', () => { 60 | expect(result.data.materials).to.containSubset(expected.materials); 61 | }); 62 | 63 | it('#products', () => { 64 | expect(result.data.products).to.containSubset(expected.products); 65 | }); 66 | }); 67 | }); -------------------------------------------------------------------------------- /tests/queries/product-in-design/json/10103.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "recipe", 3 | "url": "https://bdocodex.com/query.php?a=designs&type=product&item_id=10103&l=us", 4 | "data": [{ 5 | "type": "recipe", 6 | "shortUrl": "/us/design/5509/", 7 | "id": "5509", 8 | "icon": "/items/new_icon/06_pc_equipitem/00_common/08_subweapon/00010103.png", 9 | "name": "Ultimate Axion Shield", 10 | "skill_lvl": { 11 | "mastery": "Beginner", 12 | "lvl": 0 13 | }, 14 | "exp": 0, 15 | "materials": [{ 16 | "type": "item", 17 | "shortUrl": "/us/item/4077/", 18 | "id": "4077", 19 | "icon": "/items/new_icon/03_etc/07_productmaterial/00004077.png", 20 | "amount": 5 21 | }, { 22 | "type": "item", 23 | "shortUrl": "/us/item/4053/", 24 | "id": "4053", 25 | "icon": "/items/new_icon/03_etc/07_productmaterial/00004053.png", 26 | "amount": 1 27 | }, { 28 | "type": "item", 29 | "shortUrl": "/us/item/6151/", 30 | "id": "6151", 31 | "icon": "/items/new_icon/03_etc/07_productmaterial/00006151.png", 32 | "amount": 2 33 | }, { 34 | "type": "item", 35 | "shortUrl": "/us/item/4802/", 36 | "id": "4802", 37 | "icon": "/items/new_icon/03_etc/07_productmaterial/00004802.png", 38 | "amount": 10 39 | }, { 40 | "type": "item", 41 | "shortUrl": "/us/item/4901/", 42 | "id": "4901", 43 | "icon": "/items/new_icon/03_etc/07_productmaterial/00004901.png", 44 | "amount": 10 45 | }], 46 | "products": [{ 47 | "type": "item", 48 | "shortUrl": "/us/item/10113/", 49 | "id": "10113", 50 | "icon": "/items/new_icon/06_pc_equipitem/00_common/08_subweapon/00010103.png", 51 | "amount": 1 52 | }, { 53 | "type": "item", 54 | "shortUrl": "/us/item/10103/", 55 | "id": "10103", 56 | "icon": "/items/new_icon/06_pc_equipitem/00_common/08_subweapon/00010103.png", 57 | "amount": 1 58 | }] 59 | }] 60 | } -------------------------------------------------------------------------------- /src/query/query.ts: -------------------------------------------------------------------------------- 1 | import * as Queries from "./typings"; 2 | import * as Builders from "./builders"; 3 | import { App } from "../shared/typings"; 4 | import { Scrapers } from "../scraper"; 5 | 6 | export class Query { 7 | constructor( 8 | protected readonly _id: string, 9 | 10 | protected readonly _group: Queries.Groups, 11 | 12 | protected readonly _itemAs: Queries.ItemAs, 13 | 14 | protected readonly _locale = App.Locales.US, 15 | 16 | protected readonly fetch: App.FetchFn, 17 | 18 | protected readonly _scrape: Scrapers.Scrape, 19 | ) {} 20 | 21 | get url(): string { 22 | const idKey = [ 23 | Queries.ItemAs.NPC_DROP, 24 | Queries.ItemAs.NODE_DROP, 25 | Queries.ItemAs.CONTAINER, 26 | Queries.ItemAs.QUEST_REWARD, 27 | ].includes(this._itemAs) ? 'id' : 'item_id'; 28 | return App.BASE_URL + '/query.php?' + Object.entries({ 29 | a: this._group, 30 | type: this._itemAs, 31 | [idKey]: this._id, 32 | l: this._locale, 33 | }) 34 | .map(entry => entry.join('=')) 35 | .join('&'); 36 | } 37 | 38 | async parse(): Promise { 39 | const res = await this.fetch(this.url); 40 | if (!res) { 41 | return { url: this.url, type: null, data: [] }; 42 | } 43 | const obj = JSON.parse(res.trim()); 44 | const builder = this.getBuilder(); 45 | return { 46 | url: this.url, 47 | type: builder.type as Queries.EntityTypes, 48 | data: new builder(this._locale, this._scrape).build(obj), 49 | }; 50 | } 51 | 52 | private getBuilder(): typeof Builders.Generic { 53 | const { Groups, ItemAs } = Queries; 54 | const { _group: g, _itemAs: a } = this; 55 | if ([Groups.PROCESSING, Groups.RECIPE, Groups.DESIGN].includes(g)) 56 | return Builders.Recipe; 57 | if ([ItemAs.NPC_DROP].includes(a)) 58 | return Builders.NPCDrop; 59 | if ([ItemAs.NODE_DROP].includes(a)) 60 | return Builders.Node; 61 | if ([ItemAs.CONTAINER].includes(a)) 62 | return Builders.Item; 63 | if ([Groups.NPC].includes(g)) 64 | return Builders.NPC; 65 | if ([Groups.QUEST].includes(g)) 66 | return Builders.Quest; 67 | return Builders.Generic; 68 | } 69 | } -------------------------------------------------------------------------------- /tests/scrapers/quest/json/694-2.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "694/2", 3 | "icon": "/items/quest/black_fairy.png", 4 | "name": "[Awakening Weapon] Make it Stronger by Enhancement!", 5 | "name_alt": "[각성무기] 장비 강화로 더 강력하게!", 6 | "type": "quest", 7 | "category": "Quest", 8 | "description": "Mevo Muranan said you can upgrade your weapon by Enhancement with Black Stone (Weapon).\nLearn more about Enhancement from Mevo Muranan.", 9 | "stage": 2, 10 | "region": "All", 11 | "q_category": "Town", 12 | "q_type": "Character quest", 13 | "lvl": 57, 14 | "exclusive_to": [], 15 | "quest_chain": [{ 16 | "type": "quest", 17 | "id": "694/1", 18 | "icon": "/items/quest/black_fairy.png", 19 | "name": "[Awakening Weapon] Artisans in Mediah..", 20 | "shortUrl": "/us/quest/694/1" 21 | }, { 22 | "type": "quest", 23 | "id": "694/2", 24 | "icon": "/items/quest/black_fairy.png", 25 | "name": "[Awakening Weapon] Make it Stronger by Enhancement!", 26 | "shortUrl": "/us/quest/694/2" 27 | }, { 28 | "type": "quest", 29 | "id": "694/3", 30 | "icon": "/items/quest/black_fairy.png", 31 | "name": "[Awakening Weapon] Max Durab. Recovery", 32 | "shortUrl": "/us/quest/694/3" 33 | }], 34 | "npc_start": { 35 | "type": "npc", 36 | "id": "44008", 37 | "icon": "/items/ui_artwork/ic_00586.png", 38 | "name": "Mevo Muranan", 39 | "shortUrl": "/us/npc/44008/" 40 | }, 41 | "npc_end": { 42 | "type": "npc", 43 | "id": "44008/1", 44 | "icon": "/items/ui_artwork/ic_00586.png", 45 | "name": "Mevo Muranan", 46 | "shortUrl": "/us/npc/44008/1/" 47 | }, 48 | "text": [ 49 | "In order to upgrade weaponry,", 50 | "you need to make good use of Black Stones.", 51 | "If you own a weapon,", 52 | "then you need to enhance it by yourself.", 53 | "\n", 54 | "Enhancement is the process of bringing out a weapon's full potential.", 55 | "\n", 56 | "Do not hesitate to invest,", 57 | "in enhancing your weapon.", 58 | "Making bold investments in enhancement", 59 | "is more to your advantage in the long term." 60 | ], 61 | "rewards": { 62 | "standard": [{ 63 | "type": "item", 64 | "id": "16001", 65 | "icon": "/items/new_icon/03_etc/11_enchant_material/00000008.png", 66 | "name": "Black Stone (Weapon)", 67 | "amount": 10 68 | }, { 69 | "type": "knowledge", 70 | "id": "5618", 71 | "icon": "/items/ui_artwork/collected_journal.png", 72 | "name": "Enhance Awakening Weapon" 73 | }], 74 | "choose": [] 75 | } 76 | } -------------------------------------------------------------------------------- /src/scraper/scraper.ts: -------------------------------------------------------------------------------- 1 | import cheerio from "cheerio"; 2 | import * as Scrapers from "./typings"; 3 | import { App } from "../shared/typings"; 4 | import { Queries } from "../query"; 5 | import { Generic as Builder } from "./builders"; 6 | 7 | export class Scraper { 8 | constructor( 9 | private readonly _id: string, 10 | 11 | private readonly _locale: App.Locales, 12 | 13 | private readonly _type: Scrapers.Types, 14 | 15 | private readonly _fetch: App.FetchFn, 16 | 17 | private readonly _query: Queries.Query, 18 | 19 | private readonly _scrape: Scrapers.Scrape, 20 | ) {} 21 | 22 | private get url(): string { 23 | return [ 24 | App.BASE_URL, 25 | this._locale, 26 | this._type, 27 | this._id, 28 | ].join('/') + '/'; 29 | } 30 | 31 | private getCategoryId($: CheerioStatic): Scrapers.Ctgs { 32 | const ctg_id = $('.category_text').text() 33 | .replace(/[^a-zA-Z ]/g, '') 34 | .toLowerCase() 35 | .trim() 36 | .split(' ') 37 | .join('_'); 38 | return ({ 39 | [App.Locales.US]: { 40 | 'equipment': Scrapers.Ctgs.EQUIPMENT, 41 | 'crafting_materials': Scrapers.Ctgs.CRAFTING_MATERIAL, 42 | 'consumable': Scrapers.Ctgs.CONSUMABLE, 43 | 'installable_object': Scrapers.Ctgs.INSTALLABLE_OBJECT, 44 | 'special_items': Scrapers.Ctgs.SPECIAL_ITEM, 45 | 'recipe': Scrapers.Ctgs.RECIPE, 46 | 'quest': Scrapers.Ctgs.QUEST, 47 | 'worker': Scrapers.Ctgs.WORKER, 48 | 'item_group': Scrapers.Ctgs.MATERIAL_GROUP, 49 | } 50 | }[this._locale] as any)[ctg_id] || Scrapers.Ctgs.UNKNOWN; 51 | } 52 | 53 | private exists($: CheerioStatic): boolean { 54 | return $('table.smallertext').length !== 0; 55 | } 56 | 57 | async fetch(): Promise { 58 | return this._fetch(this.url); 59 | } 60 | 61 | async parse(): Promise { 62 | const res = await this.fetch(); 63 | const $ = cheerio.load(res as string); 64 | 65 | if (!this.exists($)) { 66 | return { url: this.url, type: null, data: null }; 67 | } 68 | 69 | const ctg_id = this.getCategoryId($); 70 | const builder = Builder.get(this._type, ctg_id); 71 | 72 | const data = await new builder( 73 | this._id, 74 | this._locale, 75 | this._type, 76 | $, 77 | this._query, 78 | this._scrape, 79 | ).build(); 80 | 81 | return { url: this.url, type: builder.type, data }; 82 | } 83 | } -------------------------------------------------------------------------------- /src/scraper/builders/knowledge/knowledge.builder.ts: -------------------------------------------------------------------------------- 1 | import * as AppUtils from "../../../shared/utils"; 2 | import * as Scrapers from "../../typings"; 3 | import { App, Undef } from "../../../shared/typings"; 4 | import { Generic } from "../generic.builder"; 5 | import { Matcher } from "../../../shared"; 6 | 7 | export class Knowledge extends Generic { 8 | static get(): typeof Generic { 9 | return Knowledge; 10 | } 11 | 12 | static get type(): string { 13 | return "knowledge"; 14 | } 15 | 16 | get icon(): string { 17 | return this.$('img.quest_icon').attr('src') as string; 18 | } 19 | 20 | get group(): Undef { 21 | const matcher = new Matcher(this._locale, { 22 | [App.Locales.US]: ['Category:'], 23 | }); 24 | const node = this.$('.valign_top') 25 | .contents() 26 | .toArray() 27 | .find(node => matcher.in(node.data)); 28 | if (!node) 29 | return undefined; 30 | return node.data 31 | ?.substr(matcher.indexIn(node.data, true)) 32 | .trim(); 33 | } 34 | 35 | get obtained_from(): Undef { 36 | const elem = this.$('.iconset_wrapper_medium.inlinediv').first(); 37 | const url = elem.find('a').attr('href') as string; 38 | const icon = elem.find('img').attr('src') as string; 39 | const name = elem.parent().find(`a[href="${url}"]`).last().text(); 40 | 41 | return { 42 | type: 'npc', 43 | id: AppUtils.decomposeShortURL(url).id, 44 | icon, 45 | name, 46 | shortUrl: url, 47 | scrape: this.ScrapeFactory(url), 48 | } 49 | } 50 | 51 | get description(): Undef { 52 | const matcher = new Matcher(this._locale, { 53 | [App.Locales.US]: ['Description:'], 54 | }); 55 | 56 | const nodes = this.getTableRow(matcher)?.childNodes || []; 57 | const startIdx = nodes.findIndex(node => matcher.in(node.data)) + 1; 58 | 59 | const strs: string[] = []; 60 | for (let i = startIdx; i < nodes.length; i++) { 61 | const node = nodes[i]; 62 | if (node.tagName === 'hr') 63 | break; 64 | else if (strs.length && node.tagName === 'br') 65 | strs.push('\n'); 66 | else if (node.type === 'text' || node.tagName === 'span') 67 | strs.push(this.$(nodes[i]).text()); 68 | } 69 | return strs.join(''); 70 | } 71 | 72 | async build(): Promise { 73 | return { 74 | ...(await super.build()), 75 | group: this.group, 76 | category: undefined, 77 | obtained_from: this.obtained_from, 78 | } 79 | } 80 | } -------------------------------------------------------------------------------- /tests/scrapers/item/queries.spec.ts: -------------------------------------------------------------------------------- 1 | import "../../utils/chai.config"; 2 | import { describe } from "mocha"; 3 | import { expect } from "chai"; 4 | import { Queries } from "../../utils/query-mock"; 5 | import ScrapeMock, { Scrapers } from "../../utils/scrape-mock"; 6 | 7 | describe('Items > Queries', () => { 8 | /** 9 | * https://bdocodex.com/us/item/4901/ 10 | * Black Stone Powder 11 | */ 12 | describe('4901', () => { 13 | let result: Scrapers.Result; 14 | 15 | before(async () => { 16 | result = await ScrapeMock('4901', Scrapers.Types.ITEM); 17 | }); 18 | 19 | describe('#quest_rewards', () => { 20 | let queryResult: Queries.Result; 21 | 22 | before(async () => { 23 | queryResult = await result.data?.quest_rewards?.() as any; 24 | }); 25 | 26 | it('#type', () => { 27 | expect(queryResult.type).to.equal('quest'); 28 | }); 29 | 30 | it('#data.length', () => { 31 | expect(queryResult.data.length).to.equal(14); 32 | }); 33 | 34 | it('#data[6]', () => { 35 | expect(queryResult.data[6]).to.containSubset({ 36 | type: 'quest', 37 | id: '1100/34', 38 | icon: '/items/quest/00004901.png', 39 | name: 'Production #3 - Introduction to Production', 40 | shortUrl: '/us/quest/1100/34/', 41 | lvl: 0, 42 | region: 'Eastern Balenos', 43 | exp: 0, 44 | exp_skill: 0, 45 | exp_contribution: 140, 46 | rewards: { 47 | standard: [{ 48 | type: 'item', 49 | id: '4901', 50 | icon: '/items/new_icon/03_etc/07_productmaterial/00004901.png', 51 | shortUrl: '/us/item/4901/', 52 | amount: 2, 53 | }, { 54 | type: 'item', 55 | id: '542', 56 | icon: '/items/new_icon/03_etc/12_doapplydirectlyitem/00000000_power.png', 57 | shortUrl: '/us/item/542/', 58 | amount: 1, 59 | }, { 60 | type: 'item', 61 | id: '2', 62 | icon: '/items/new_icon/00000002_special.png', 63 | shortUrl: '/us/item/2/', 64 | amount: 1, 65 | }] 66 | } 67 | }); 68 | }); 69 | }); 70 | 71 | describe('#npc_drops', () => { 72 | it('should be undefined', () => { 73 | expect(result.data.npc_drops).to.be.undefined; 74 | }); 75 | }); 76 | }); 77 | }); -------------------------------------------------------------------------------- /src/query/builders/recipe.builder.ts: -------------------------------------------------------------------------------- 1 | import cheerio from "cheerio"; 2 | import * as AppUtils from "../../shared/utils"; 3 | import * as Queries from "../typings"; 4 | import { BDOCodex, Undef } from "../../shared/typings"; 5 | import { Scrapers } from "../../scraper"; 6 | import { Generic } from "./generic.builder"; 7 | 8 | export class Recipe extends Generic { 9 | static get type() { 10 | return "recipe"; 11 | } 12 | 13 | getProcess(raw: string): Undef { 14 | return raw || undefined; 15 | } 16 | 17 | getSkillLvl(raw: string): Queries.Recipes.SkillLvl { 18 | return { 19 | mastery: raw.replace(/\d/g, '').trim(), 20 | lvl: AppUtils.parseIntValue(raw), 21 | } 22 | } 23 | 24 | getMaterials(raw: string): Queries.Recipes.Material[] { 25 | const $ = cheerio.load('
' + raw + '
'); 26 | return $('div > div.iconset_wrapper_medium').toArray().map(node => { 27 | const elem = $(node); 28 | const anchor = elem.find('a'); 29 | const txt = elem.find('.quantity_small'); 30 | const img = elem.find('.icon_wrapper'); 31 | const url = anchor.attr('href') as string; 32 | const type = AppUtils.decomposeShortURL(url).type; 33 | 34 | switch (type) { 35 | case Scrapers.Types.ITEM: 36 | return { 37 | type: 'item', 38 | id: AppUtils.decomposeShortURL(url).id, 39 | amount: parseInt(txt.text()) || 1, 40 | icon: this.parseIconURL(img.text()), 41 | shortUrl: url, 42 | scrape: this.ScrapeFactory(url), 43 | }; 44 | default: 45 | return { 46 | type: 'material_group', 47 | id: AppUtils.decomposeShortURL(url).id, 48 | amount: parseInt(txt.text()) || 1, 49 | icon: this.parseIconURL(img.text()), 50 | shortUrl: url, 51 | scrape: this.ScrapeFactory(url), 52 | }; 53 | } 54 | }); 55 | } 56 | 57 | build(data: BDOCodex.Query.Recipe): Queries.Entities.Recipe[] { 58 | return data.aaData.map(arr => { 59 | const url = this.parseShortURL(arr[2]); 60 | 61 | return { 62 | type: Recipe.type, 63 | id: arr[0], 64 | icon: this.parseIconURL(arr[1]), 65 | name: this.parseName(arr[2]), 66 | process: this.getProcess(arr[3]), 67 | exp: AppUtils.parseIntValue(arr[5]), 68 | skill_lvl: this.getSkillLvl(arr[4].display), 69 | materials: this.getMaterials(arr[6]), 70 | products: this.getMaterials(arr[7]), 71 | shortUrl: url, 72 | scrape: this.ScrapeFactory(url), 73 | } 74 | }); 75 | } 76 | } -------------------------------------------------------------------------------- /src/scraper/builders/item/consumable.builder.ts: -------------------------------------------------------------------------------- 1 | import * as AppUtils from "../../../shared/utils"; 2 | import * as Scrapers from "../../typings"; 3 | import { App } from "../../../shared/typings"; 4 | import { Item } from "./item.builder"; 5 | import { Matcher } from "../../../shared"; 6 | 7 | export class Consumable extends Item { 8 | static get(): typeof Item { 9 | return Consumable; 10 | } 11 | 12 | static get type(): string { 13 | return "consumable"; 14 | } 15 | 16 | private normalizeTimeUnit(raw: string): number { 17 | const matchers = { 18 | sec: new Matcher(this._locale, { 19 | [App.Locales.US]: ['sec'], 20 | }), 21 | min: new Matcher(this._locale, { 22 | [App.Locales.US]: ['min'], 23 | }), 24 | }; 25 | const value = parseInt(raw.replace(/\D/g, '')); 26 | if (matchers.sec.in(raw)) 27 | return value; 28 | if (matchers.min.in(raw)) 29 | return value * 60; 30 | return value; 31 | } 32 | 33 | get effects(): string[] { 34 | const matcher = new Matcher(this._locale, { 35 | [App.Locales.US]: ['- Effect', 'Effect:'], 36 | }); 37 | 38 | const strs = this.getBodyNodes(true) 39 | .filter(node => node.name !== 'span') 40 | .map(node => node.data || '
'); 41 | let i = strs.findIndex(str => matcher.in(str)); 42 | if (i === -1) return []; 43 | 44 | const effects: string[] = []; 45 | while (i++ < strs.length) { 46 | if (strs[i] === '
' && strs?.[i+1] === '
') 47 | break; 48 | if (strs[i] === '
') 49 | continue; 50 | effects.push(AppUtils.cleanStr(strs[i])); 51 | } 52 | return effects.filter(e => e); 53 | } 54 | 55 | get duration(): number { 56 | const matcher = new Matcher(this._locale, { 57 | [App.Locales.US]: ['Duration'], 58 | }); 59 | const nodes = this.getBodyNodes(true) 60 | .filter(node => node.data); 61 | const i = nodes.findIndex(node => matcher.in(node.data)); 62 | if (!matcher.length) 63 | return 0; 64 | return this.normalizeTimeUnit(nodes[i+1].data as string) 65 | } 66 | 67 | get cooldown(): number { 68 | const matcher = new Matcher(this._locale, { 69 | [App.Locales.US]: ['Cooldown'], 70 | }); 71 | const nodes = this.getBodyNodes(true) 72 | .filter(node => node.data); 73 | const i = nodes.findIndex(node => matcher.in(node.data)); 74 | if (!matcher.length) 75 | return 0; 76 | return this.normalizeTimeUnit(nodes[i+1].data as string); 77 | } 78 | 79 | async build(): Promise { 80 | return { 81 | ...(await super.build()), 82 | effects: this.effects, 83 | duration: this.duration, 84 | cooldown: this.cooldown, 85 | }; 86 | } 87 | } -------------------------------------------------------------------------------- /src/shared/matcher/matcher.ts: -------------------------------------------------------------------------------- 1 | import { App } from "../../shared/typings"; 2 | 3 | export class Matcher { 4 | private readonly _cache: Record = {}; 5 | 6 | private readonly _matches: string[]; 7 | 8 | private _lastMatchedStr?: string; 9 | 10 | private _length = 0; 11 | 12 | constructor( 13 | locale: App.Locales, 14 | matches: Record, 15 | ) { 16 | this._matches = matches[locale]; 17 | } 18 | 19 | /** 20 | * Checks if a string contains a match. 21 | * 22 | * @param str - The string to match. 23 | */ 24 | in(str?: string): boolean { 25 | if (!str) 26 | return false; 27 | if (this._cache[str]) 28 | return this._cache[str].idx !== -1; 29 | 30 | const idxs = this._matches.reduce( 31 | (obj, match) => ({ ...obj, [match]: 0 }), 32 | {} as Record, 33 | ); 34 | 35 | for (let i = 0; i < str.length; i++) { 36 | const char = str[i]; 37 | 38 | for (const match of this._matches) { 39 | if (char === match[idxs[match]]) 40 | idxs[match] += 1; 41 | else idxs[match] = 0; 42 | 43 | if (idxs[match] !== match.length) 44 | continue; 45 | 46 | this._lastMatchedStr = str; 47 | this._length += 1; 48 | this._cache[str] = { 49 | idx: i - match.length + 1, 50 | substr: match, 51 | }; 52 | 53 | return true; 54 | } 55 | } 56 | 57 | this._cache[str] = { idx: - 1}; 58 | return false; 59 | } 60 | 61 | /** 62 | * Retrieves the starting index or the ending index of the match. 63 | * 64 | * @param str - The string to match. 65 | * @param end - Whether to return the starting or ending index. 66 | */ 67 | indexIn(str?: string, end?: boolean): number { 68 | if (!str) 69 | return -1; 70 | const match = this._cache[str]; 71 | if (!match) 72 | this.in(str); 73 | if (!match.substr) 74 | return -1; 75 | if (end) 76 | return match.idx + match.substr.length; 77 | return match.idx; 78 | } 79 | 80 | /** 81 | * Retrieves the match for a given string. 82 | * 83 | * @param str - The string to retrieve the match for. 84 | */ 85 | matchIn(str?: string): string | undefined { 86 | if (!str) 87 | return undefined; 88 | if (!this._cache[str]) 89 | this.in(str); 90 | return this._cache[str].substr; 91 | } 92 | 93 | /** 94 | * The last string that was evaluated to a successful match. 95 | */ 96 | get last(): string | undefined { 97 | return this._lastMatchedStr; 98 | } 99 | 100 | /** 101 | * The number of successful matches. 102 | */ 103 | get length(): number { 104 | return this._length; 105 | } 106 | } -------------------------------------------------------------------------------- /src/scraper/builders/item/item.builder.ts: -------------------------------------------------------------------------------- 1 | import * as Builders from "../"; 2 | import * as Scrapers from "../../typings"; 3 | import { App } from "../../../shared/typings"; 4 | import { Queries } from "../../../query"; 5 | import { Pricings } from "../../typings"; 6 | import { Generic } from "../generic.builder"; 7 | import { Matcher } from "../../../shared"; 8 | 9 | export class Item extends Generic { 10 | static get(type: Scrapers.Types, ctg_id: Scrapers.Ctgs): typeof Generic { 11 | return ({ 12 | [Scrapers.Ctgs.CONSUMABLE]: Builders.Consumable, 13 | [Scrapers.Ctgs.EQUIPMENT]: Builders.Equipment, 14 | } as any)[ctg_id]?.get(type, ctg_id) || Item; 15 | } 16 | 17 | static get type(): string { 18 | return "item"; 19 | } 20 | 21 | get grade(): number { 22 | const str = this.$('.item_title').attr('class') as string; 23 | return parseInt(str.replace(/\D/g, '')); 24 | } 25 | 26 | get weight(): number { 27 | const matcher = new Matcher(this._locale, { 28 | [App.Locales.US]: ['Weight:'] 29 | }); 30 | return parseFloat(this.getTextNodeFromCategoryWrapper(matcher)?.data 31 | ?.substr(matcher.indexIn(matcher.last, true)) 32 | ?.replace(/[ LT]/g, '') || '0'); 33 | } 34 | 35 | get prices(): Pricings { 36 | const matchers = { 37 | buy: new Matcher(this._locale, { 38 | [App.Locales.US]: ['Buy'], 39 | }), 40 | sell: new Matcher(this._locale, { 41 | [App.Locales.US]: ['Sell'], 42 | }), 43 | repair: new Matcher(this._locale, { 44 | [App.Locales.US]: ['Repair'], 45 | }), 46 | }; 47 | const keys = ['buy', 'sell', 'repair']; 48 | 49 | return this.getBodyNodes().reduce((prices, node) => { 50 | if (!node.data) 51 | return prices; 52 | const key = keys.find( 53 | key => !matchers[key].length && matchers[key].in(node.data) 54 | ); 55 | if (!key) 56 | return prices; 57 | return { ...prices, [key]: parseInt(node.data.replace(/\D/g, '')) }; 58 | }, {} as Pricings); 59 | } 60 | 61 | async build(): Promise { 62 | const QTypes = Queries.Types; 63 | const qf = this.QueryFactory.bind(this); 64 | return { 65 | ...(await super.build()), 66 | prices: this.prices, 67 | grade: this.grade, 68 | weight: this.weight, 69 | npc_drops: qf(QTypes.NPC_DROPS), 70 | quest_rewards: qf(QTypes.QUEST_REWARD), 71 | product_of_recipes: qf(QTypes.PRODUCT_IN_DESIGN), 72 | product_of_processing: qf(QTypes.PRODUCT_IN_PROCESSING), 73 | product_of_design: qf(QTypes.PRODUCT_IN_DESIGN), 74 | material_of_recipes: qf(QTypes.MATERIAL_IN_RECIPE), 75 | material_of_processing: qf(QTypes.MATERIAL_IN_PROCESSING), 76 | material_of_design: qf(QTypes.MATERIAL_IN_DESIGN), 77 | dropped_in_node: qf(QTypes.DROPPED_IN_NODE), 78 | obtained_from: qf(QTypes.OBTAINED_FROM), 79 | sold_by_npc: qf(QTypes.SOLD_BY_NPC), 80 | }; 81 | } 82 | } -------------------------------------------------------------------------------- /src/query/builders/quest.builder.ts: -------------------------------------------------------------------------------- 1 | import cheerio from "cheerio"; 2 | import * as AppUtils from "../../shared/utils"; 3 | import * as Queries from "../typings"; 4 | import { App, BDOCodex } from "../../shared/typings"; 5 | import { Generic } from "./generic.builder"; 6 | import { Matcher } from "../../shared"; 7 | 8 | export class Quest extends Generic { 9 | static get type() { 10 | return "quest"; 11 | } 12 | 13 | getRewards(raw: string): Queries.Quests.Rewards { 14 | const matchers = { 15 | choose: new Matcher(this._locale, { 16 | [App.Locales.US]: ['Choose'], 17 | }), 18 | amity: new Matcher(this._locale, { 19 | [App.Locales.US]: ['Amity'], 20 | }), 21 | }; 22 | 23 | const $ = cheerio.load('
' + raw + '
'); 24 | let curr: Queries.Quests.Reward[]; 25 | return $('#root').contents().toArray().reduce((rewards, node) => { 26 | const { data, tagName } = node; 27 | if (tagName === 'br' || node.parent.attribs.id !== 'root') 28 | return rewards; 29 | if (!curr) 30 | curr = rewards.standard; 31 | if (matchers.choose.in(data)) 32 | curr = rewards.choose; 33 | else if (matchers.amity.in(data)) 34 | rewards.amity.push(AppUtils.parseIntValue(data)); 35 | 36 | if (tagName !== 'div') 37 | return rewards; 38 | 39 | const elem = $(node); 40 | const anchor = elem.find('a'); 41 | const amount = AppUtils.parseIntValue( 42 | elem.find('.quantity_small').text(), 1 43 | ); 44 | 45 | if (anchor.length) { 46 | const url = anchor.attr('href') as string; 47 | curr.push({ 48 | type: 'item', 49 | id: AppUtils.decomposeShortURL(url).id, 50 | icon: this.parseIconURL(elem.find('.icon_wrapper').text()), 51 | scrape: this.ScrapeFactory(url), 52 | shortUrl: url, 53 | amount, 54 | }); 55 | } else { 56 | curr.push({ 57 | type: 'exp', 58 | icon: elem.find('img').attr('src') as string, 59 | name: elem.attr('title') as string, 60 | amount, 61 | }); 62 | } 63 | return rewards; 64 | }, { standard: [], choose: [], amity: [] } as Queries.Quests.Rewards); 65 | } 66 | 67 | build(data: BDOCodex.Query.Quest): Queries.Entities.Quest[] { 68 | return data.aaData.map(arr => { 69 | const url = this.parseShortURL(arr[2]); 70 | 71 | return { 72 | type: Quest.type, 73 | id: arr[0].display, 74 | icon: this.parseIconURL(arr[1]), 75 | name: this.parseName(arr[2]), 76 | lvl: AppUtils.parseIntValue(arr[3]), 77 | region: arr[4].display, 78 | exp: AppUtils.parseIntValue(arr[5].display), 79 | exp_skill: AppUtils.parseIntValue(arr[6].display), 80 | exp_contribution: AppUtils.parseIntValue(arr[7]), 81 | rewards: this.getRewards(arr[8]), 82 | shortUrl: url, 83 | scrape: this.ScrapeFactory(url), 84 | }; 85 | }); 86 | } 87 | } -------------------------------------------------------------------------------- /tests/scrapers/knowledge/all.spec.ts: -------------------------------------------------------------------------------- 1 | import "../../utils/chai.config"; 2 | import { describe } from "mocha"; 3 | import { expect } from "chai"; 4 | import ScrapeMock, { Scrapers } from "../../utils/scrape-mock"; 5 | 6 | describe('Knowledges', () => { 7 | /** 8 | * https://bdocodex.com/us/theme/58/ 9 | * Hans 10 | */ 11 | describe('58', () => { 12 | const expected: Scrapers.Entities.Knowledge = require('./json/58.json'); 13 | let result: Scrapers.Result; 14 | 15 | before(async () => { 16 | result = await ScrapeMock('58', Scrapers.Types.KNOWLEDGE); 17 | }); 18 | 19 | it('#id', () => { 20 | expect(result.data.id).to.equal(expected.id); 21 | }); 22 | 23 | it('#icon', () => { 24 | expect(result.data.icon).to.equal(expected.icon); 25 | }); 26 | 27 | it('#name', () => { 28 | expect(result.data.name).to.equal(expected.name); 29 | }); 30 | 31 | it('#name_alt', () => { 32 | expect(result.data.name_alt).to.equal(expected.name_alt); 33 | }); 34 | 35 | it('#type', () => { 36 | expect(result.data.type).to.equal(expected.type); 37 | }); 38 | 39 | it('#category', () => { 40 | expect(result.data.category).to.equal(expected.category); 41 | }); 42 | 43 | it('#description', () => { 44 | expect(result.data.description).to.equal(expected.description); 45 | }); 46 | 47 | it('#group', () => { 48 | expect(result.data.group).to.equal(expected.group); 49 | }); 50 | 51 | it('#obtained_from', () => { 52 | expect(result.data.obtained_from).to.containSubset(expected.obtained_from); 53 | }); 54 | }); 55 | 56 | /** 57 | * https://bdocodex.com/us/theme/103/ 58 | * Caresto Fonti 59 | */ 60 | describe('103', () => { 61 | const expected: Scrapers.Entities.Knowledge = require('./json/103.json'); 62 | let result: Scrapers.Result; 63 | 64 | before(async () => { 65 | result = await ScrapeMock('103', Scrapers.Types.KNOWLEDGE); 66 | }); 67 | 68 | it('#id', () => { 69 | expect(result.data.id).to.equal(expected.id); 70 | }); 71 | 72 | it('#icon', () => { 73 | expect(result.data.icon).to.equal(expected.icon); 74 | }); 75 | 76 | it('#name', () => { 77 | expect(result.data.name).to.equal(expected.name); 78 | }); 79 | 80 | it('#name_alt', () => { 81 | expect(result.data.name_alt).to.equal(expected.name_alt); 82 | }); 83 | 84 | it('#type', () => { 85 | expect(result.data.type).to.equal(expected.type); 86 | }); 87 | 88 | it('#category', () => { 89 | expect(result.data.category).to.equal(expected.category); 90 | }); 91 | 92 | it('#description', () => { 93 | expect(result.data.description).to.equal(expected.description); 94 | }); 95 | 96 | it('#group', () => { 97 | expect(result.data.group).to.equal(expected.group); 98 | }); 99 | 100 | it('#obtained_from', () => { 101 | expect(result.data.obtained_from).to.containSubset(expected.obtained_from); 102 | }); 103 | }); 104 | }); -------------------------------------------------------------------------------- /tests/scrapers/item/all.spec.ts: -------------------------------------------------------------------------------- 1 | import "../../utils/chai.config"; 2 | import { describe } from "mocha"; 3 | import { expect } from "chai"; 4 | import ScrapeMock, { Scrapers } from "../../utils/scrape-mock"; 5 | 6 | describe('Items', () => { 7 | /** 8 | * https://bdocodex.com/us/item/4901/ 9 | * Black Stone Powder 10 | */ 11 | describe('4901', () => { 12 | const expected: Scrapers.Entities.Item = require('./json/4901.json'); 13 | let result: Scrapers.Result; 14 | 15 | before(async () => { 16 | result = await ScrapeMock('4901', Scrapers.Types.ITEM); 17 | }); 18 | 19 | it('#id', () => { 20 | expect(result.data.id).to.equal(expected.id); 21 | }); 22 | 23 | it('#icon', () => { 24 | expect(result.data.icon).to.equal(expected.icon); 25 | }); 26 | 27 | it('#name', () => { 28 | expect(result.data.name).to.equal(expected.name); 29 | }); 30 | 31 | it('#name_alt', () => { 32 | expect(result.data.name_alt).to.equal(expected.name_alt); 33 | }); 34 | 35 | it('#type', () => { 36 | expect(result.data.type).to.equal(expected.type); 37 | }); 38 | 39 | it('#category', () => { 40 | expect(result.data.category).to.equal(expected.category); 41 | }); 42 | 43 | it('#description', () => { 44 | expect(result.data.description).to.equal(expected.description); 45 | }); 46 | 47 | it('#prices', () => { 48 | expect(result.data.prices).to.deep.equal(expected.prices); 49 | }); 50 | 51 | it('#grade', () => { 52 | expect(result.data.grade).to.equal(expected.grade); 53 | }); 54 | }); 55 | 56 | /** 57 | * https://bdocodex.com/us/item/4085/ 58 | * Melted Noc Shard 59 | */ 60 | describe('4085', () => { 61 | const expected: Scrapers.Entities.Item = require('./json/4085.json'); 62 | let result: Scrapers.Result; 63 | 64 | before(async () => { 65 | result = await ScrapeMock('4085', Scrapers.Types.ITEM); 66 | }); 67 | 68 | it('#id', () => { 69 | expect(result.data.id).to.equal(expected.id); 70 | }); 71 | 72 | it('#icon', () => { 73 | expect(result.data.icon).to.equal(expected.icon); 74 | }); 75 | 76 | it('#name', () => { 77 | expect(result.data.name).to.equal(expected.name); 78 | }); 79 | 80 | it('#name_alt', () => { 81 | expect(result.data.name_alt).to.equal(expected.name_alt); 82 | }); 83 | 84 | it('#type', () => { 85 | expect(result.data.type).to.equal(expected.type); 86 | }); 87 | 88 | it('#category', () => { 89 | expect(result.data.category).to.equal(expected.category); 90 | }); 91 | 92 | it('#description', () => { 93 | expect(result.data.description).to.equal(expected.description); 94 | }); 95 | 96 | it('#prices', () => { 97 | expect(result.data.prices).to.deep.equal(expected.prices); 98 | }); 99 | 100 | it('#grade', () => { 101 | expect(result.data.grade).to.equal(expected.grade); 102 | }); 103 | 104 | it('#weight', () => { 105 | expect(result.data.weight).to.equal(expected.weight); 106 | }); 107 | }); 108 | }); -------------------------------------------------------------------------------- /src/scraper/builders/recipe/recipe.builder.ts: -------------------------------------------------------------------------------- 1 | import * as AppUtils from "../../../shared/utils"; 2 | import * as Scrapers from "../../typings"; 3 | import { App } from "../../../shared/typings"; 4 | import { Generic } from "../generic.builder"; 5 | import { Matcher } from "../../../shared"; 6 | 7 | export class Recipe extends Generic { 8 | static get(): typeof Generic { 9 | return Recipe; 10 | } 11 | 12 | static get type(): string { 13 | return "recipe"; 14 | } 15 | 16 | private getMaterials(matcher: Matcher): Scrapers.Recipes.Material[] { 17 | const row = this.getTableRow(matcher); 18 | if (!row) return []; 19 | return this.$(row).find('img').toArray().map(node => { 20 | const parent = node.parent.parent; 21 | const url = parent.attribs.href; 22 | const anchor = this.$(row).find(`a[href="${url}"]`).last(); 23 | const amount = AppUtils.parseIntValue(this.$(parent).text(), 1); 24 | const { type, id } = AppUtils.decomposeShortURL(url); 25 | 26 | if (type === Scrapers.Types.ITEM) { 27 | return { 28 | type: 'item', 29 | id, 30 | icon: node.attribs.src, 31 | name: anchor.text(), 32 | grade: AppUtils.parseIntValue(anchor.attr('class')), 33 | amount, 34 | shortUrl: url, 35 | scrape: this.ScrapeFactory(url), 36 | }; 37 | } else { 38 | return { 39 | type: 'material_group', 40 | id, 41 | icon: node.attribs.src, 42 | name: anchor.text(), 43 | amount, 44 | shortUrl: url, 45 | scrape: this.ScrapeFactory(url), 46 | } 47 | } 48 | }); 49 | } 50 | 51 | get process(): string { 52 | return this.$('.category_text').parent().find('.yellow_text').text(); 53 | } 54 | 55 | get exp(): number { 56 | const matcher = new Matcher(this._locale, { 57 | [App.Locales.US]: ['EXP'], 58 | }); 59 | const node = this.getTextNodeFromCategoryWrapper(matcher); 60 | return AppUtils.parseIntValue(node?.data); 61 | } 62 | 63 | get skill_lvl() { 64 | const matcher = new Matcher(this._locale, { 65 | [App.Locales.US]: ['Skill level:'], 66 | }); 67 | const node = this.getTextNodeFromCategoryWrapper(matcher); 68 | const [mastery, lvl] = this.$(node).text() 69 | ?.substr(matcher.indexIn(matcher.last, true)) 70 | .trim() 71 | .split(' ') as string[]; 72 | return { mastery, lvl: parseInt(lvl) }; 73 | } 74 | 75 | get materials(): Scrapers.Recipes.Material[] { 76 | const matcher = new Matcher(this._locale, { 77 | [App.Locales.US]: ['Crafting Material'], 78 | }); 79 | return this.getMaterials(matcher); 80 | } 81 | 82 | get products(): Scrapers.Recipes.Material[] { 83 | const matcher = new Matcher(this._locale, { 84 | [App.Locales.US]: ['Crafting Result'], 85 | }); 86 | return this.getMaterials(matcher); 87 | } 88 | 89 | async build(): Promise { 90 | return { 91 | ...(await super.build()), 92 | process: this.process, 93 | exp: this.exp, 94 | skill_lvl: this.skill_lvl, 95 | materials: this.materials, 96 | products: this.products, 97 | } 98 | } 99 | } -------------------------------------------------------------------------------- /tests/scrapers/quest/json/347-1.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "347/1", 3 | "icon": "/items/quest/archerawake_001.png", 4 | "name": "[Archer Ascension] Lost Purpose", 5 | "name_alt": "[아처 개방] 길을 찾지 못한 숙명", 6 | "type": "quest", 7 | "category": "Quest", 8 | "description": "The Black Spirit doesn't want to see your long face, and suggested you to visit the Sacred Tree of the Goddess if you have something on your mind.\nLet's take the Black Spirit's advice and go to Kamasylve.", 9 | "stage": 1, 10 | "region": "All", 11 | "q_category": "Story", 12 | "q_type": "Character quest", 13 | "lvl": 56, 14 | "exclusive_to": [ 15 | "Archer" 16 | ], 17 | "quest_chain": [{ 18 | "type": "quest", 19 | "id": "347/1", 20 | "icon": "/items/quest/archerawake_001.png", 21 | "name": "[Archer Ascension] Lost Purpose", 22 | "shortUrl": "/us/quest/347/1" 23 | }, { 24 | "type": "quest", 25 | "id": "347/2", 26 | "icon": "/items/quest/archerawake_002.png", 27 | "name": "[Archer] Searching for Answers", 28 | "shortUrl": "/us/quest/347/2" 29 | }, { 30 | "type": "quest", 31 | "id": "347/3", 32 | "icon": "/items/quest/archerawake_003.png", 33 | "name": "[Archer] Rising Smoke of Darkness", 34 | "shortUrl": "/us/quest/347/3" 35 | }, { 36 | "type": "quest", 37 | "id": "347/4", 38 | "icon": "/items/quest/archerawake_004.png", 39 | "name": "[Archer] Light that Pursues Darkness", 40 | "shortUrl": "/us/quest/347/4" 41 | }, { 42 | "type": "quest", 43 | "id": "347/5", 44 | "icon": "/items/quest/archerawake_007.png", 45 | "name": "[Archer] Light and Darkness Clashes", 46 | "shortUrl": "/us/quest/347/5" 47 | }, { 48 | "type": "quest", 49 | "id": "347/6", 50 | "icon": "/items/quest/archerawake_005.png", 51 | "name": "[Archer] Remnants of a Broken Arrow", 52 | "shortUrl": "/us/quest/347/6" 53 | }, { 54 | "type": "quest", 55 | "id": "347/7", 56 | "icon": "/items/quest/archerawake_006.png", 57 | "name": "[Archer] Encountering the Power of Light", 58 | "shortUrl": "/us/quest/347/7" 59 | }, { 60 | "type": "quest", 61 | "id": "347/8", 62 | "icon": "/items/quest/archerawake_006.png", 63 | "name": "[Archer] Purpose Regained", 64 | "shortUrl": "/us/quest/347/8" 65 | }, { 66 | "type": "quest", 67 | "id": "347/9", 68 | "icon": "/items/new_icon/06_pc_equipitem/00_common/01_weapon/00013902.png", 69 | "name": "[Archer] The Watchers of Adùir - Battle", 70 | "shortUrl": "/us/quest/347/9" 71 | }], 72 | "npc_end": { 73 | "type": "npc", 74 | "id": "57159/1", 75 | "icon": "/items/ui_artwork/ic_00559.png", 76 | "name": "Kamasylve", 77 | "shortUrl": "/us/npc/57159/1/" 78 | }, 79 | "text": [ 80 | "Keke, what's with the long face?", 81 | "Do you have something on your mind?", 82 | "You look sad...", 83 | "You're not homesick, are you?", 84 | "Hmm.. How about you go to see the Sacred Tree Kamasylve,", 85 | "the Goddess you call Mother created?", 86 | "\n", 87 | "So? What do you think, it's a good idea right?", 88 | "I know you better than you know yourself!", 89 | "\n", 90 | "(You smell a sweetness in the air and see leaves dancing in the wind.)" 91 | ], 92 | "rewards": { 93 | "standard": [{ 94 | "type": "item", 95 | "id": "402", 96 | "icon": "/items/new_icon/06_pc_equipitem/00_common/00_etc/00000402.png", 97 | "name": "Contribution EXP 50", 98 | "amount": 50 99 | }], 100 | "choose": [] 101 | } 102 | } -------------------------------------------------------------------------------- /tests/scrapers/worker/json/7614.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "7614", 3 | "icon": "/items/new_ui_common_forlua/widget/worldmap/workericon/worker_giant01.png", 4 | "name": "Acher", 5 | "type": "npc", 6 | "category": "Worker", 7 | "description": "A worker at the Extraction Mill in Serendia.", 8 | "sellable": false, 9 | "max_base_stats": { 10 | "work_speed": 50, 11 | "movement_speed": 2, 12 | "luck": 5, 13 | "stamina": 30 14 | }, 15 | "levels": [ 16 | { 17 | "exp_to_next_lvl": 30, 18 | "sell_price": 0 19 | }, 20 | { 21 | "exp_to_next_lvl": 45, 22 | "sell_price": 0 23 | }, 24 | { 25 | "exp_to_next_lvl": 63, 26 | "sell_price": 0 27 | }, 28 | { 29 | "exp_to_next_lvl": 86, 30 | "sell_price": 0 31 | }, 32 | { 33 | "exp_to_next_lvl": 112, 34 | "sell_price": 0 35 | }, 36 | { 37 | "exp_to_next_lvl": 140, 38 | "sell_price": 0 39 | }, 40 | { 41 | "exp_to_next_lvl": 171, 42 | "sell_price": 0 43 | }, 44 | { 45 | "exp_to_next_lvl": 209, 46 | "sell_price": 0 47 | }, 48 | { 49 | "exp_to_next_lvl": 255, 50 | "sell_price": 0 51 | }, 52 | { 53 | "exp_to_next_lvl": 312, 54 | "sell_price": 0 55 | }, 56 | { 57 | "exp_to_next_lvl": 381, 58 | "sell_price": 0 59 | }, 60 | { 61 | "exp_to_next_lvl": 462, 62 | "sell_price": 0 63 | }, 64 | { 65 | "exp_to_next_lvl": 560, 66 | "sell_price": 0 67 | }, 68 | { 69 | "exp_to_next_lvl": 678, 70 | "sell_price": 0 71 | }, 72 | { 73 | "exp_to_next_lvl": 821, 74 | "sell_price": 0 75 | }, 76 | { 77 | "exp_to_next_lvl": 994, 78 | "sell_price": 0 79 | }, 80 | { 81 | "exp_to_next_lvl": 1203, 82 | "sell_price": 0 83 | }, 84 | { 85 | "exp_to_next_lvl": 1456, 86 | "sell_price": 0 87 | }, 88 | { 89 | "exp_to_next_lvl": 1762, 90 | "sell_price": 0 91 | }, 92 | { 93 | "exp_to_next_lvl": 2133, 94 | "sell_price": 0 95 | }, 96 | { 97 | "exp_to_next_lvl": 2581, 98 | "sell_price": 0 99 | }, 100 | { 101 | "exp_to_next_lvl": 3124, 102 | "sell_price": 0 103 | }, 104 | { 105 | "exp_to_next_lvl": 3781, 106 | "sell_price": 0 107 | }, 108 | { 109 | "exp_to_next_lvl": 4576, 110 | "sell_price": 0 111 | }, 112 | { 113 | "exp_to_next_lvl": 5537, 114 | "sell_price": 0 115 | }, 116 | { 117 | "exp_to_next_lvl": 6700, 118 | "sell_price": 0 119 | }, 120 | { 121 | "exp_to_next_lvl": 8107, 122 | "sell_price": 0 123 | }, 124 | { 125 | "exp_to_next_lvl": 9810, 126 | "sell_price": 0 127 | }, 128 | { 129 | "exp_to_next_lvl": 11871, 130 | "sell_price": 0 131 | }, 132 | { 133 | "exp_to_next_lvl": 14364, 134 | "sell_price": 0 135 | }, 136 | { 137 | "exp_to_next_lvl": 30000, 138 | "sell_price": 0 139 | } 140 | ], 141 | "growth": { 142 | "work_speed": [ 143 | 0.1, 144 | 0.43 145 | ], 146 | "movement_speed": [ 147 | 0.1, 148 | 0.25 149 | ], 150 | "luck": [ 151 | 0.1, 152 | 0.25 153 | ] 154 | } 155 | } -------------------------------------------------------------------------------- /tests/scrapers/worker/json/7615.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "7615", 3 | "icon": "/items/new_ui_common_forlua/widget/worldmap/workericon/worker_human01.png", 4 | "name": "Alzath", 5 | "type": "npc", 6 | "category": "Worker", 7 | "description": "A farm worker at the Moretti Farm in Serendia.", 8 | "sellable": false, 9 | "max_base_stats": { 10 | "work_speed": 45, 11 | "movement_speed": 3, 12 | "luck": 20, 13 | "stamina": 13 14 | }, 15 | "levels": [ 16 | { 17 | "exp_to_next_lvl": 30, 18 | "sell_price": 0 19 | }, 20 | { 21 | "exp_to_next_lvl": 45, 22 | "sell_price": 0 23 | }, 24 | { 25 | "exp_to_next_lvl": 63, 26 | "sell_price": 0 27 | }, 28 | { 29 | "exp_to_next_lvl": 86, 30 | "sell_price": 0 31 | }, 32 | { 33 | "exp_to_next_lvl": 112, 34 | "sell_price": 0 35 | }, 36 | { 37 | "exp_to_next_lvl": 140, 38 | "sell_price": 0 39 | }, 40 | { 41 | "exp_to_next_lvl": 171, 42 | "sell_price": 0 43 | }, 44 | { 45 | "exp_to_next_lvl": 209, 46 | "sell_price": 0 47 | }, 48 | { 49 | "exp_to_next_lvl": 255, 50 | "sell_price": 0 51 | }, 52 | { 53 | "exp_to_next_lvl": 312, 54 | "sell_price": 0 55 | }, 56 | { 57 | "exp_to_next_lvl": 381, 58 | "sell_price": 0 59 | }, 60 | { 61 | "exp_to_next_lvl": 462, 62 | "sell_price": 0 63 | }, 64 | { 65 | "exp_to_next_lvl": 560, 66 | "sell_price": 0 67 | }, 68 | { 69 | "exp_to_next_lvl": 678, 70 | "sell_price": 0 71 | }, 72 | { 73 | "exp_to_next_lvl": 821, 74 | "sell_price": 0 75 | }, 76 | { 77 | "exp_to_next_lvl": 994, 78 | "sell_price": 0 79 | }, 80 | { 81 | "exp_to_next_lvl": 1203, 82 | "sell_price": 0 83 | }, 84 | { 85 | "exp_to_next_lvl": 1456, 86 | "sell_price": 0 87 | }, 88 | { 89 | "exp_to_next_lvl": 1762, 90 | "sell_price": 0 91 | }, 92 | { 93 | "exp_to_next_lvl": 2133, 94 | "sell_price": 0 95 | }, 96 | { 97 | "exp_to_next_lvl": 2581, 98 | "sell_price": 0 99 | }, 100 | { 101 | "exp_to_next_lvl": 3124, 102 | "sell_price": 0 103 | }, 104 | { 105 | "exp_to_next_lvl": 3781, 106 | "sell_price": 0 107 | }, 108 | { 109 | "exp_to_next_lvl": 4576, 110 | "sell_price": 0 111 | }, 112 | { 113 | "exp_to_next_lvl": 5537, 114 | "sell_price": 0 115 | }, 116 | { 117 | "exp_to_next_lvl": 6700, 118 | "sell_price": 0 119 | }, 120 | { 121 | "exp_to_next_lvl": 8107, 122 | "sell_price": 0 123 | }, 124 | { 125 | "exp_to_next_lvl": 9810, 126 | "sell_price": 0 127 | }, 128 | { 129 | "exp_to_next_lvl": 11871, 130 | "sell_price": 0 131 | }, 132 | { 133 | "exp_to_next_lvl": 14364, 134 | "sell_price": 0 135 | }, 136 | { 137 | "exp_to_next_lvl": 30000, 138 | "sell_price": 0 139 | } 140 | ], 141 | "growth": { 142 | "work_speed": [ 143 | 0.1, 144 | 0.43 145 | ], 146 | "movement_speed": [ 147 | 0.1, 148 | 0.23333 149 | ], 150 | "luck": [ 151 | 0.1, 152 | 0.25 153 | ] 154 | } 155 | } -------------------------------------------------------------------------------- /tests/scrapers/quest/json/2051-19.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "2051/19", 3 | "icon": "/items/new_icon/03_etc/04_dropitem/00042293.png", 4 | "name": "[Alchemy] The Truth of Darkness", 5 | "type": "quest", 6 | "category": "Quest", 7 | "description": "The priest said that he could feel the dark energy and that he would help purify the energy.\nHand over the unstable energy, receive the Purification Scroll and use it at the altar.", 8 | "stage": 1, 9 | "region": "Eastern Balenos", 10 | "q_category": "Story", 11 | "q_type": "Family quest", 12 | "lvl": 1, 13 | "exclusive_to": [], 14 | "quest_chain": [{ 15 | "type": "quest", 16 | "id": "2051/19", 17 | "icon": "/items/new_icon/03_etc/04_dropitem/00042293.png", 18 | "name": "[Alchemy] The Truth of Darkness", 19 | "shortUrl": "/us/quest/2051/19" 20 | }, { 21 | "type": "quest", 22 | "id": "2051/20", 23 | "icon": "/items/new_icon/03_etc/04_dropitem/00042293.png", 24 | "name": "[Alchemy] Incomplete Evil God", 25 | "shortUrl": "/us/quest/2051/20" 26 | }], 27 | "npc_start": { 28 | "type": "npc", 29 | "id": "57150", 30 | "icon": "/items/ui_artwork/ic_00559.png", 31 | "name": "Priest", 32 | "shortUrl": "/us/npc/57150/" 33 | }, 34 | "npc_end": { 35 | "type": "npc", 36 | "id": "41073/1", 37 | "icon": "/items/ui_artwork/ic_00190.png", 38 | "name": "Hakkon", 39 | "shortUrl": "/us/npc/41073/1/" 40 | }, 41 | "text": [ 42 | "I can sense extremely dark energy from you.", 43 | "It's so strong that it's giving me a headache.", 44 | "Where did you get that?", 45 | "I'm sure it was difficult to obtain.", 46 | "The darkness will consume you", 47 | "if you continue to possess that.", 48 | "The dark energy must be purified through a ritual", 49 | "before it's too late.", 50 | "\n", 51 | "I suggest you listen to me", 52 | "before being consumed by endless darkness.", 53 | "\n", 54 | "I cannot believe that the energy I felt", 55 | "was from an evil god...", 56 | "Can you really take care of it yourself?", 57 | "Here, take this weapon." 58 | ], 59 | "rewards": { 60 | "standard": [{ 61 | "type": "item", 62 | "id": "42435", 63 | "icon": "/items/new_icon/03_etc/03_quest_item/00042435.png", 64 | "name": "Kzarka's Latent Aura", 65 | "shortUrl": "/us/item/42435/", 66 | "amount": 10 67 | }, { 68 | "type": "item", 69 | "id": "15991", 70 | "icon": "/items/new_icon/03_etc/00015991.png", 71 | "name": "Kzarka's Sealed Weapon Box", 72 | "shortUrl": "/us/item/15991/", 73 | "amount": 1 74 | }, { 75 | "type": "item", 76 | "id": "42556", 77 | "icon": "/items/new_icon/03_etc/03_quest_item/00040136.png", 78 | "name": "[Title] Vanquisher of Darkness", 79 | "shortUrl": "/us/item/42556/", 80 | "amount": 1 81 | }], 82 | "choose": [{ 83 | "type": "item", 84 | "id": "45260", 85 | "icon": "/items/new_icon/03_etc/07_productmaterial/00045260.png", 86 | "name": "Splendid Alchemy Stone of Protection", 87 | "shortUrl": "/us/item/45260/", 88 | "amount": 1 89 | }, { 90 | "type": "item", 91 | "id": "45228", 92 | "icon": "/items/new_icon/03_etc/07_productmaterial/00045228.png", 93 | "name": "Splendid Alchemy Stone of Destruction", 94 | "shortUrl": "/us/item/45228/", 95 | "amount": 1 96 | }, { 97 | "type": "item", 98 | "id": "45292", 99 | "icon": "/items/new_icon/03_etc/07_productmaterial/00045292.png", 100 | "name": "Splendid Alchemy Stone of Life", 101 | "shortUrl": "/us/item/45292/", 102 | "amount": 1 103 | }] 104 | } 105 | } -------------------------------------------------------------------------------- /tests/scrapers/worker/json/7572.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "7572", 3 | "icon": "/items/new_ui_common_forlua/widget/worldmap/workericon/worker_goblin04.png", 4 | "name": "Artisan Goblin Worker", 5 | "type": "npc", 6 | "category": "Worker", 7 | "sellable": true, 8 | "max_base_stats": { 9 | "work_speed": 115, 10 | "movement_speed": 5, 11 | "luck": 6, 12 | "stamina": 15 13 | }, 14 | "levels": [ 15 | { 16 | "exp_to_next_lvl": 30, 17 | "sell_price": 90000 18 | }, 19 | { 20 | "exp_to_next_lvl": 45, 21 | "sell_price": 90000 22 | }, 23 | { 24 | "exp_to_next_lvl": 63, 25 | "sell_price": 112680 26 | }, 27 | { 28 | "exp_to_next_lvl": 86, 29 | "sell_price": 118980 30 | }, 31 | { 32 | "exp_to_next_lvl": 112, 33 | "sell_price": 122760 34 | }, 35 | { 36 | "exp_to_next_lvl": 140, 37 | "sell_price": 125280 38 | }, 39 | { 40 | "exp_to_next_lvl": 171, 41 | "sell_price": 129060 42 | }, 43 | { 44 | "exp_to_next_lvl": 209, 45 | "sell_price": 137880 46 | }, 47 | { 48 | "exp_to_next_lvl": 255, 49 | "sell_price": 147960 50 | }, 51 | { 52 | "exp_to_next_lvl": 312, 53 | "sell_price": 161820 54 | }, 55 | { 56 | "exp_to_next_lvl": 381, 57 | "sell_price": 176940 58 | }, 59 | { 60 | "exp_to_next_lvl": 477, 61 | "sell_price": 199800 62 | }, 63 | { 64 | "exp_to_next_lvl": 597, 65 | "sell_price": 217800 66 | }, 67 | { 68 | "exp_to_next_lvl": 747, 69 | "sell_price": 239400 70 | }, 71 | { 72 | "exp_to_next_lvl": 934, 73 | "sell_price": 262800 74 | }, 75 | { 76 | "exp_to_next_lvl": 1168, 77 | "sell_price": 289800 78 | }, 79 | { 80 | "exp_to_next_lvl": 1460, 81 | "sell_price": 459900 82 | }, 83 | { 84 | "exp_to_next_lvl": 1825, 85 | "sell_price": 522000 86 | }, 87 | { 88 | "exp_to_next_lvl": 2282, 89 | "sell_price": 594900 90 | }, 91 | { 92 | "exp_to_next_lvl": 2853, 93 | "sell_price": 681300 94 | }, 95 | { 96 | "exp_to_next_lvl": 3567, 97 | "sell_price": 783900 98 | }, 99 | { 100 | "exp_to_next_lvl": 4495, 101 | "sell_price": 1234800 102 | }, 103 | { 104 | "exp_to_next_lvl": 5664, 105 | "sell_price": 1440000 106 | }, 107 | { 108 | "exp_to_next_lvl": 7137, 109 | "sell_price": 1684800 110 | }, 111 | { 112 | "exp_to_next_lvl": 8993, 113 | "sell_price": 1969200 114 | }, 115 | { 116 | "exp_to_next_lvl": 11332, 117 | "sell_price": 2307600 118 | }, 119 | { 120 | "exp_to_next_lvl": 14392, 121 | "sell_price": 3546000 122 | }, 123 | { 124 | "exp_to_next_lvl": 18278, 125 | "sell_price": 5020200 126 | }, 127 | { 128 | "exp_to_next_lvl": 23214, 129 | "sell_price": 6938100 130 | }, 131 | { 132 | "exp_to_next_lvl": 29482, 133 | "sell_price": 9399600 134 | }, 135 | { 136 | "exp_to_next_lvl": 30000, 137 | "sell_price": 12555900 138 | } 139 | ], 140 | "growth": { 141 | "work_speed": [ 142 | 0.1, 143 | 1.8 144 | ], 145 | "movement_speed": [ 146 | 0.1, 147 | 0.25 148 | ], 149 | "luck": [ 150 | 0.05, 151 | 0.3 152 | ] 153 | }, 154 | "obtained_from": { 155 | "type": "npc", 156 | "id": "7562", 157 | "icon": "/items/new_ui_common_forlua/widget/worldmap/workericon/worker_goblin03.png", 158 | "name": "Professional Goblin Worker", 159 | "shortUrl": "/us/npc/7562/" 160 | } 161 | } -------------------------------------------------------------------------------- /tests/scrapers/quest/json/815-2.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "815/2", 3 | "icon": "/items/quest/succeed_02.png", 4 | "name": "[Berserker Succession] About Destiny", 5 | "type": "quest", 6 | "category": "Quest", 7 | "description": "Tantu introduced you to Pusa, a famous fortuneteller, and said he already paid her to tell your fortune. Listen to what she has to say about your destiny.", 8 | "stage": 2, 9 | "region": "All", 10 | "q_category": "Story", 11 | "q_type": "Character quest", 12 | "lvl": 1, 13 | "exclusive_to": [], 14 | "quest_chain": [{ 15 | "type": "quest", 16 | "id": "815/1", 17 | "icon": "/items/quest/tantu.png", 18 | "name": "[Berserker Succession] The Valor", 19 | "shortUrl": "/us/quest/815/1" 20 | }, { 21 | "type": "quest", 22 | "id": "815/2", 23 | "icon": "/items/quest/succeed_02.png", 24 | "name": "[Berserker Succession] About Destiny", 25 | "shortUrl": "/us/quest/815/2" 26 | }, { 27 | "type": "quest", 28 | "id": "815/3", 29 | "icon": "/items/quest/succeed_02.png", 30 | "name": "[Berserker Succession] Tantu's Glove", 31 | "shortUrl": "/us/quest/815/3" 32 | }, { 33 | "type": "quest", 34 | "id": "815/4", 35 | "icon": "/items/quest/tantu.png", 36 | "name": "[Berserker Succession] Pursuit", 37 | "shortUrl": "/us/quest/815/4" 38 | }, { 39 | "type": "quest", 40 | "id": "815/5", 41 | "icon": "/items/quest/succeed_03.png", 42 | "name": "[Berserker Succession] A Strange Reunion", 43 | "shortUrl": "/us/quest/815/5" 44 | }, { 45 | "type": "quest", 46 | "id": "815/6", 47 | "icon": "/items/quest/media_col.png", 48 | "name": "[Berserker Succession] Dark Shadow", 49 | "shortUrl": "/us/quest/815/6" 50 | }, { 51 | "type": "quest", 52 | "id": "815/7", 53 | "icon": "/items/quest/black_fairy.png", 54 | "name": "[Berserker Succession] Berserker, Fortified Strength", 55 | "shortUrl": "/us/quest/815/7" 56 | }], 57 | "npc_start": { 58 | "type": "npc", 59 | "id": "58041/25", 60 | "icon": "/items/ui_artwork/ic_00559.png", 61 | "name": "Tantu", 62 | "shortUrl": "/us/npc/58041/25/" 63 | }, 64 | "npc_end": { 65 | "type": "npc", 66 | "id": "58041/1", 67 | "icon": "/items/ui_artwork/ic_00559.png", 68 | "name": "Pusa", 69 | "shortUrl": "/us/npc/58041/1/" 70 | }, 71 | "text": [ 72 | "Altinova is such an incredible place.", 73 | "It's where it once fell into ruin by the Three Days of Darkness...", 74 | "But look at this city now. They recovered it so well!", 75 | "I remember that day when we stopped her rampage using the Bautt Slate.", 76 | "With so many sacrifices, we managed to keep the darkness at bay...", 77 | "Well, by the way,", 78 | "Are you familiar with wielding the Iron Buster now?", 79 | "Haha. I guess that was totally unnecessary to ask you. I heard a lot about your heroic deeds.", 80 | "Perhaps I talked too much.", 81 | "I want you to meet Pusa. She's a famous fortuneteller.", 82 | "{ChangeScene(Succeed_02)They say everything in life is luck", 83 | "but it seems like you're sharing that luck you have with darkness.", 84 | "{ChangeScene(Succeed_02)You lost yourself, then retrieved,", 85 | "and perhaps you may lose it again...", 86 | "{ChangeScene(Succeed_24)Isn't this interesting? Hahaha.", 87 | "I already paid the fortuneteller, so all you have to do is listen.", 88 | "\n", 89 | "{ChangeScene(Succeed_24)Haha, are you nervous?", 90 | "Relax, it's just for fun. Don't take it too seriously.", 91 | "\n", 92 | "Very well then. I will attempt to read the destiny of Tantu.", 93 | "Although you were shrouded in darkness...", 94 | "You succeeded in driving it out completely!", 95 | "But why do you both share the same kind of darkness...?" 96 | ], 97 | "rewards": { 98 | "standard": [{ 99 | "type": "item", 100 | "id": "402", 101 | "icon": "/items/new_icon/06_pc_equipitem/00_common/00_etc/00000402.png", 102 | "name": "Contribution EXP 50", 103 | "amount": 50 104 | }], 105 | "choose": [] 106 | } 107 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | 5 | **Calpheon.js** is an interface to retrieve data about the **Black Desert Online (BDO)** game by scraping [BDOCodex](https://bdocodex.com/us/). It's built using Typescript so you know exactly what is being returned. 6 | 7 | ## Roadmap 8 | 9 | This are the features that will be prioritized on future releases: 10 | * Add support to all query types. 11 | * Query using a search term. 12 | * Add scraping support to: 13 | * Skills 14 | * Achievements 15 | * Nodes 16 | * Houses 17 | 18 | ## Installation 19 | ``` 20 | npm install calpheonjs 21 | ``` 22 | 23 | ## Usage 24 | 25 | ```ts 26 | import { Scrape, Item } from "calpheonjs"; 27 | 28 | const scrape = async (id: string, type: string) => { 29 | // Use Scrape if you don't know the entity type you are scraping. 30 | const entity = await Scrape(id, type); 31 | 32 | // Or use the proper scraper if you know what you are requesting. 33 | // Assume the entity you are requesting is an item. 34 | const item = await Item(id); 35 | 36 | console.log(item.data); 37 | }; 38 | scrape('9213', 'item'); 39 | ``` 40 | 41 | This is the output. 42 | ```json 43 | { 44 | "id": "9213", 45 | "icon": "/items/new_icon/03_etc/07_productmaterial/00009213.png", 46 | "name": "Beer", 47 | "name_alt": "맥주", 48 | "type": "item", 49 | "category": "Special Items", 50 | "description": "A mild alcoholic drink brewed from cereal grains", 51 | "prices": { 52 | "buy": 2150, 53 | "sell": 86 54 | }, 55 | "grade": 1, 56 | "weight": 0.1, 57 | "effects": [ 58 | "Worker Stamina Recovery +2", 59 | "(Use through the Worker Menu on the World Map)." 60 | ] 61 | } 62 | ``` 63 | 64 | You can also query directly from the item, or using the provided `Query` function. 65 | 66 | ```ts 67 | import { Item, Query, Queries } from "calpheonjs"; 68 | 69 | const scrape = async (id: string, type: string) => { 70 | const item = await Item(id); 71 | const recipes = await item.data.product_of_recipe(); 72 | 73 | // Or you can use the Query function by passing the item id and the query type. 74 | // This is useful in case you don't care about the item data, only about the query results. 75 | const recipes = await Query('9213', Queries.Types.PRODUCT_IN_RECIPE); 76 | 77 | console.log(recipes.data); 78 | }; 79 | scrape('9213'); 80 | ``` 81 | 82 | This is the output. 83 | ```json 84 | [{ 85 | "type": "recipe", 86 | "shortUrl": "/us/recipe/122/", 87 | "id": "122", 88 | "icon": "/items/new_icon/03_etc/07_productmaterial/00009213.png", 89 | "name": "Beer", 90 | "process": "Cooking", 91 | "skill_lvl": { 92 | "mastery": "Beginner", 93 | "lvl": 1 94 | }, 95 | "exp": 400, 96 | "materials": [{ 97 | "type": "material_group", 98 | "shortUrl": "/us/materialgroup/1/", 99 | "id": "1", 100 | "icon": "/items/new_icon/03_etc/07_productmaterial/00007005.png", 101 | "amount": 5 102 | }, { 103 | "type": "item", 104 | "shortUrl": "/us/item/9059/", 105 | "id": "9059", 106 | "icon": "/items/new_icon/03_etc/07_productmaterial/00009059.png", 107 | "amount": 6 108 | }, { 109 | "type": "item", 110 | "shortUrl": "/us/item/9002/", 111 | "id": "9002", 112 | "icon": "/items/new_icon/03_etc/07_productmaterial/00009002.png", 113 | "amount": 1 114 | }, { 115 | "type": "item", 116 | "shortUrl": "/us/item/9005/", 117 | "id": "9005", 118 | "icon": "/items/new_icon/03_etc/07_productmaterial/00009005.png", 119 | "amount": 2 120 | }], 121 | "products": [{ 122 | "type": "item", 123 | "shortUrl": "/us/item/9213/", 124 | "id": "9213", 125 | "icon": "/items/new_icon/03_etc/07_productmaterial/00009213.png", 126 | "amount": 1 127 | }, { 128 | "type": "item", 129 | "shortUrl": "/us/item/9283/", 130 | "id": "9283", 131 | "icon": "/items/new_icon/03_etc/07_productmaterial/00009283.png", 132 | "amount": 1 133 | }] 134 | }, ...] 135 | ``` 136 | 137 | ## Contributing 138 | 139 | If you wish to help with this project, please follow the steps below. Any help is appreciated, but please stick to the same patterns that have been used accross the codebase. 140 | 141 | 1) Fork this repository. 142 | 2) Work on the features you want. 143 | 3) Make sure that all tests are passing by running `npm run test` or `yarn test`. 144 | 4) Create a PR when you are done. 145 | 146 | Please follow this [guide](https://github.com/conventional-changelog/commitlint/#what-is-commitlint) when naming your commits. 147 | -------------------------------------------------------------------------------- /src/scraper/builders/npc/worker.builder.ts: -------------------------------------------------------------------------------- 1 | import * as AppUtils from "../../../shared/utils"; 2 | import * as Scrapers from "../../typings"; 3 | import { App, BDOCodex, Undef } from "../../../shared/typings"; 4 | import { Generic } from "../generic.builder" 5 | import { Matcher } from "../../../shared"; 6 | 7 | export class Worker extends Generic { 8 | static get(): typeof Generic { 9 | return Worker; 10 | } 11 | 12 | static get type(): string { 13 | return "worker"; 14 | } 15 | 16 | private getUpgradesArray(): BDOCodex.Workers.Upgrade[] { 17 | const ctx = this.cache.for<{ 18 | data: BDOCodex.Workers.Upgrade[], 19 | }>('upgrades_array'); 20 | if (!ctx.has('data')) { 21 | const raw = this.$('.smallertext') 22 | .first() 23 | .find('script') 24 | .last() 25 | .html(); 26 | if (!raw) 27 | return ctx.set('data', []); 28 | return ctx.set('data', JSON.parse( 29 | raw.substr(raw.indexOf('[')) 30 | )); 31 | } 32 | return ctx.get('data'); 33 | } 34 | 35 | get icon(): string { 36 | return this.$('img.quest_icon').attr('src') as string; 37 | } 38 | 39 | get sellable(): boolean { 40 | const matcher = new Matcher(this._locale, { 41 | [App.Locales.US]: ['Sellable'], 42 | }); 43 | const row = this.getTableRow(matcher); 44 | return !!row?.childNodes.find((node, idx, arr) => { 45 | if (!matcher.in(node.data)) 46 | return; 47 | let i = idx; 48 | while (++i < arr.length) 49 | if (arr[i].tagName === 'span' && arr[i].attribs.class) 50 | if (arr[i].attribs.class.indexOf('glyphicon-ok') !== -1) 51 | return true; 52 | return false; 53 | }); 54 | } 55 | 56 | get max_base_stats(): Scrapers.NPCs.Workers.Stats { 57 | const stamina = parseInt(this.$('#work_speed') 58 | .parent() 59 | .parent() 60 | .parent() 61 | .children() 62 | .last() 63 | .text() 64 | .replace(/\D/g, '')); 65 | return { 66 | work_speed: parseInt(this.$('#work_speed').text()), 67 | movement_speed: parseInt(this.$('#move_speed').text()), 68 | luck: parseInt(this.$('#luck').text()), 69 | stamina, 70 | } 71 | } 72 | 73 | get levels(): Scrapers.NPCs.Workers.Level[] { 74 | return this.getUpgradesArray().map(curr => ({ 75 | exp_to_next_lvl: parseInt(curr.nextlvexp.replace(/\D/g, '')), 76 | sell_price: parseInt(curr.sell_price.replace(/\D/g, '')), 77 | })); 78 | } 79 | 80 | get growth(): Scrapers.NPCs.Workers.Growth { 81 | const table = this 82 | .$('.region_table') 83 | .parent() 84 | .find('> table') 85 | .toArray()[1]; 86 | const rows = this.$(table) 87 | .find('td') 88 | .toArray(); 89 | const find = (matcher: Matcher) => { 90 | return this.$(rows[rows.findIndex(node => 91 | matcher.in(this.$(node).text()) 92 | ) + 1]) 93 | .text() 94 | .split('~') 95 | .map(str => parseFloat(str.trim())) as [number, number]; 96 | }; 97 | return { 98 | work_speed: find(new Matcher(this._locale, { 99 | [App.Locales.US]: ['Work speed growth per level'], 100 | })), 101 | movement_speed: find(new Matcher(this._locale, { 102 | [App.Locales.US]: ['Speed growth per level'], 103 | })), 104 | luck: find(new Matcher(this._locale, { 105 | [App.Locales.US]: ['Luck growth per level'], 106 | })), 107 | }; 108 | } 109 | 110 | get obtained_from(): Undef { 111 | const matcher = new Matcher(this._locale, { 112 | [App.Locales.US]: ['Obtained from:'], 113 | }); 114 | const idx = this.getBodyNodes(true) 115 | .findIndex(node => matcher.in(node.data)); 116 | if (idx === -1) 117 | return undefined; 118 | const img = this.$(this.getBodyNodes(true)[idx + 2]).find('img'); 119 | const anchor = this.$(this.getBodyNodes(true)[idx + 10]); 120 | const url = anchor.attr('href') as string; 121 | return { 122 | type: "npc", 123 | id: AppUtils.decomposeShortURL(url).id, 124 | icon: img.attr('src') as string, 125 | name: anchor.text(), 126 | shortUrl: url, 127 | scrape: this.ScrapeFactory(url), 128 | } 129 | } 130 | 131 | async build(): Promise { 132 | return { 133 | ...(await super.build()), 134 | sellable: this.sellable, 135 | max_base_stats: this.max_base_stats, 136 | levels: this.levels, 137 | growth: this.growth, 138 | obtained_from: this.obtained_from as any, 139 | }; 140 | } 141 | } -------------------------------------------------------------------------------- /tests/scrapers/equipment/properties/enhancement_stats.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe } from "mocha"; 2 | import { expect } from "chai"; 3 | import ScrapeMock, { Scrapers } from "../../../utils/scrape-mock"; 4 | 5 | describe('Equipments > enhancement stats', () => { 6 | /** 7 | * https://bdocodex.com/us/item/11629/ 8 | * Tungrad Necklace 9 | */ 10 | describe('11629', () => { 11 | let result: Scrapers.Result; 12 | 13 | before(async () => { 14 | result = await ScrapeMock('11629', 15 | Scrapers.Types.ITEM 16 | ); 17 | }); 18 | 19 | it('#enhancement_stats[0]', () => { 20 | expect(result.data.enhancement_stats[0]).to.containSubset({ 21 | stats: { 22 | damage: 10, 23 | defense: 0, 24 | accuracy: 4, 25 | evasion: 0, 26 | dmg_reduction: 0, 27 | }, 28 | success_rate: 25.00, 29 | durability: 100, 30 | cron_values: { 31 | next_lvl: 62, 32 | total: 62, 33 | }, 34 | effects: { 35 | enhancement: [], 36 | item: [ 37 | "Self-collectible Black Spirit's Rage +20%" 38 | ], 39 | }, 40 | required_enhancement_item: { 41 | type: 'item', 42 | id: '11629', 43 | icon: '/new_icon/06_pc_equipitem/00_common/15_necklace/00011629.png', 44 | name: 'Tungrad Necklace', 45 | shortUrl: '/us/item/11629/', 46 | amount: 1, 47 | durability_loss_on_failure: 10, 48 | }, 49 | perfect_enhancement: { 50 | amount: 0, 51 | durability_loss_on_failure: 0, 52 | }, 53 | }); 54 | }); 55 | 56 | it('#enhancement_stats[5]', () => { 57 | expect(result.data.enhancement_stats[5]).to.containSubset({ 58 | stats: { 59 | damage: 35, 60 | defense: 0, 61 | accuracy: 24, 62 | evasion: 0, 63 | dmg_reduction: 0, 64 | }, 65 | success_rate: 0, 66 | durability: 100, 67 | cron_values: { 68 | next_lvl: 0, 69 | total: 9872, 70 | }, 71 | effects: { 72 | enhancement: [], 73 | item: [ 74 | "Self-collectible Black Spirit's Rage +20%" 75 | ], 76 | }, 77 | }); 78 | }); 79 | 80 | it('#enhancement_stats.length', () => { 81 | expect(result.data.enhancement_stats.length).to.equal(6); 82 | }); 83 | }); 84 | 85 | /** 86 | * https://bdocodex.com/us/item/13210/ 87 | * Kzarka Shortsword 88 | */ 89 | describe('13210', () => { 90 | let result: Scrapers.Result; 91 | 92 | before(async () => { 93 | result = await ScrapeMock('13210', 94 | Scrapers.Types.ITEM 95 | ); 96 | }); 97 | 98 | it('#enhancement_stats[1]', () => { 99 | expect(result.data.enhancement_stats[1]).to.containSubset({ 100 | stats: { 101 | damage: [22, 26], 102 | defense: 0, 103 | accuracy: 20, 104 | evasion: 0, 105 | dmg_reduction: 0, 106 | }, 107 | success_rate: 100.00, 108 | durability: 100, 109 | cron_values: { 110 | next_lvl: 0, 111 | total: 0, 112 | }, 113 | effects: { 114 | enhancement: [ 115 | 'Extra AP against monsters up (enhancement level PRI or up)', 116 | 'Extra Damage to All Species Up', 117 | 'All AP Up', 118 | 'All Accuracy Up' 119 | ], 120 | item: [ 121 | "Extra Damage to All Species +10", 122 | "Attack Speed +3 Level" 123 | ], 124 | }, 125 | required_enhancement_item: { 126 | type: 'item', 127 | id: '16001', 128 | icon: '/new_icon/03_etc/11_enchant_material/00000008.png', 129 | name: 'Black Stone (Weapon)', 130 | shortUrl: '/us/item/16001/', 131 | amount: 1, 132 | durability_loss_on_failure: 5, 133 | }, 134 | perfect_enhancement: { 135 | amount: 0, 136 | durability_loss_on_failure: 0, 137 | }, 138 | }); 139 | }); 140 | 141 | it('#enhancement_stats.length', () => { 142 | expect(result.data.enhancement_stats.length).to.deep.equal(21); 143 | }); 144 | }); 145 | }); -------------------------------------------------------------------------------- /tests/queries/sold-by-npc/json/13210.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "npc", 3 | "url": "https://bdocodex.com/query.php?a=npcs&type=sellspecialitems&item_id=13210&l=us", 4 | "data": [{ 5 | "type": "npc", 6 | "shortUrl": "/us/npc/40068/1/", 7 | "id": "40068/1", 8 | "icon": "/items/ui_artwork/ic_00051.png", 9 | "name": "Patrigio", 10 | "lvl": 99, 11 | "hp": 10000, 12 | "defense": 10, 13 | "evasion": 10, 14 | "exp": 0, 15 | "exp_skill": 0, 16 | "karma": 0 17 | }, { 18 | "type": "npc", 19 | "shortUrl": "/us/npc/41048/1/", 20 | "id": "41048/1", 21 | "icon": "/items/ui_artwork/ic_00051.png", 22 | "name": "Patrigio", 23 | "lvl": 99, 24 | "hp": 10000, 25 | "defense": 2, 26 | "evasion": 1, 27 | "exp": 0, 28 | "exp_skill": 0, 29 | "karma": 0 30 | }, { 31 | "type": "npc", 32 | "shortUrl": "/us/npc/42015/1/", 33 | "id": "42015/1", 34 | "icon": "/items/ui_artwork/ic_00051.png", 35 | "name": "Patrigio", 36 | "lvl": 99, 37 | "hp": 10000, 38 | "defense": 2, 39 | "evasion": 1, 40 | "exp": 0, 41 | "exp_skill": 0, 42 | "karma": 0 43 | }, { 44 | "type": "npc", 45 | "shortUrl": "/us/npc/44631/1/", 46 | "id": "44631/1", 47 | "icon": "/items/ui_artwork/ic_00051.png", 48 | "name": "Patrigio", 49 | "lvl": 99, 50 | "hp": 10000, 51 | "defense": 2, 52 | "evasion": 1, 53 | "exp": 0, 54 | "exp_skill": 0, 55 | "karma": 0 56 | }, { 57 | "type": "npc", 58 | "shortUrl": "/us/npc/45032/1/", 59 | "id": "45032/1", 60 | "icon": "/items/ui_artwork/ic_00051.png", 61 | "name": "Patrigio", 62 | "lvl": 99, 63 | "hp": 10000, 64 | "defense": 10, 65 | "evasion": 10, 66 | "exp": 0, 67 | "exp_skill": 0, 68 | "karma": 0 69 | }, { 70 | "type": "npc", 71 | "shortUrl": "/us/npc/45562/1/", 72 | "id": "45562/1", 73 | "icon": "/items/ui_artwork/ic_00051.png", 74 | "name": "Patrigio", 75 | "lvl": 99, 76 | "hp": 10000, 77 | "defense": 2, 78 | "evasion": 1, 79 | "exp": 0, 80 | "exp_skill": 0, 81 | "karma": 0 82 | }, { 83 | "type": "npc", 84 | "shortUrl": "/us/npc/46042/1/", 85 | "id": "46042/1", 86 | "icon": "/items/ui_artwork/ic_01283.png", 87 | "name": "Patrigio", 88 | "lvl": 99, 89 | "hp": 0, 90 | "defense": 0, 91 | "evasion": 0, 92 | "exp": 0, 93 | "exp_skill": 0, 94 | "karma": 0 95 | }, { 96 | "type": "npc", 97 | "shortUrl": "/us/npc/59027/1/", 98 | "id": "59027/1", 99 | "icon": "/items/ui_artwork/ic_00559.png", 100 | "name": "Dipados", 101 | "lvl": 99, 102 | "hp": 0, 103 | "defense": 0, 104 | "evasion": 0, 105 | "exp": 0, 106 | "exp_skill": 0, 107 | "karma": 0 108 | }, { 109 | "type": "npc", 110 | "shortUrl": "/us/npc/59028/1/", 111 | "id": "59028/1", 112 | "icon": "/items/ui_artwork/ic_00559.png", 113 | "name": "Dipados", 114 | "lvl": 99, 115 | "hp": 0, 116 | "defense": 0, 117 | "evasion": 0, 118 | "exp": 0, 119 | "exp_skill": 0, 120 | "karma": 0 121 | }, { 122 | "type": "npc", 123 | "shortUrl": "/us/npc/59029/1/", 124 | "id": "59029/1", 125 | "icon": "/items/ui_artwork/ic_00559.png", 126 | "name": "Dipados", 127 | "lvl": 99, 128 | "hp": 0, 129 | "defense": 0, 130 | "evasion": 0, 131 | "exp": 0, 132 | "exp_skill": 0, 133 | "karma": 0 134 | }, { 135 | "type": "npc", 136 | "shortUrl": "/us/npc/59030/1/", 137 | "id": "59030/1", 138 | "icon": "/items/ui_artwork/ic_00559.png", 139 | "name": "Dipados", 140 | "lvl": 99, 141 | "hp": 0, 142 | "defense": 0, 143 | "evasion": 0, 144 | "exp": 0, 145 | "exp_skill": 0, 146 | "karma": 0 147 | }, { 148 | "type": "npc", 149 | "shortUrl": "/us/npc/59031/1/", 150 | "id": "59031/1", 151 | "icon": "/items/ui_artwork/ic_00559.png", 152 | "name": "Dipados", 153 | "lvl": 99, 154 | "hp": 0, 155 | "defense": 0, 156 | "evasion": 0, 157 | "exp": 0, 158 | "exp_skill": 0, 159 | "karma": 0 160 | }, { 161 | "type": "npc", 162 | "shortUrl": "/us/npc/59032/1/", 163 | "id": "59032/1", 164 | "icon": "/items/ui_artwork/ic_00559.png", 165 | "name": "Dipados", 166 | "lvl": 99, 167 | "hp": 0, 168 | "defense": 0, 169 | "evasion": 0, 170 | "exp": 0, 171 | "exp_skill": 0, 172 | "karma": 0 173 | }, { 174 | "type": "npc", 175 | "shortUrl": "/us/npc/59033/1/", 176 | "id": "59033/1", 177 | "icon": "/items/ui_artwork/ic_00559.png", 178 | "name": "Dipados", 179 | "lvl": 99, 180 | "hp": 0, 181 | "defense": 0, 182 | "evasion": 0, 183 | "exp": 0, 184 | "exp_skill": 0, 185 | "karma": 0 186 | }] 187 | } -------------------------------------------------------------------------------- /tests/scrapers/quest/json/6050-1.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "6050/1", 3 | "icon": "/items/quest/perica_00.png", 4 | "name": "[Co-op] Spoils to be shown to Likke Behr", 5 | "name_alt": "[협동] 리케 베어에게 보여줄 전리품", 6 | "type": "quest", 7 | "category": "Quest", 8 | "description": "Jensen was forbidden to hunt because he is from Behr. But he says he needs booty to show Chief Likke Behr when he returns to the town of Behr, so he asks you to hunt for him.", 9 | "stage": 1, 10 | "region": "Kamasylvia", 11 | "q_category": "Story", 12 | "q_type": "Character quest", 13 | "lvl": 1, 14 | "exclusive_to": [], 15 | "quest_chain": [{ 16 | "type": "quest", 17 | "id": "6050/1", 18 | "icon": "/items/quest/perica_00.png", 19 | "name": "[Co-op] Spoils to be shown to Likke Behr", 20 | "shortUrl": "/us/quest/6050/1" 21 | }, { 22 | "type": "quest", 23 | "id": "6050/2", 24 | "icon": "/items/quest/atanis.png", 25 | "name": "Musical Spirits of Okiara", 26 | "shortUrl": "/us/quest/6050/2" 27 | }, { 28 | "type": "quest", 29 | "id": "6050/4", 30 | "icon": "/items/quest/martha.png", 31 | "name": "Voraro's Request", 32 | "shortUrl": "/us/quest/6050/4" 33 | }, { 34 | "type": "quest", 35 | "id": "6050/5", 36 | "icon": "/items/quest/lamo.png", 37 | "name": "[Co-op] Voraros Wandering in the Forest", 38 | "shortUrl": "/us/quest/6050/5" 39 | }, { 40 | "type": "quest", 41 | "id": "6050/8", 42 | "icon": "/items/quest/obi_bellen.png", 43 | "name": "Guardian Owl of the Holo Forest", 44 | "shortUrl": "/us/quest/6050/8" 45 | }, { 46 | "type": "quest", 47 | "id": "6050/9", 48 | "icon": "/items/quest/mirumok.png", 49 | "name": "[Co-op] The Wind Blowing through the Trees", 50 | "shortUrl": "/us/quest/6050/9" 51 | }, { 52 | "type": "quest", 53 | "id": "6050/10", 54 | "icon": "/items/quest/mirumok.png", 55 | "name": "[Co-op] Disappearing Energy of the Forest", 56 | "shortUrl": "/us/quest/6050/10" 57 | }, { 58 | "type": "quest", 59 | "id": "6050/15", 60 | "icon": "/items/quest/atanis.png", 61 | "name": "Guard Tower of the Forest", 62 | "shortUrl": "/us/quest/6050/15" 63 | }], 64 | "npc_start": { 65 | "type": "npc", 66 | "id": "45525", 67 | "icon": "/items/ui_artwork/ic_00454.png", 68 | "name": "Jensen", 69 | "shortUrl": "/us/npc/45525/" 70 | }, 71 | "npc_end": { 72 | "type": "npc", 73 | "id": "45525/1", 74 | "icon": "/items/ui_artwork/ic_00454.png", 75 | "name": "Jensen", 76 | "shortUrl": "/us/npc/45525/1/" 77 | }, 78 | "text": [ 79 | "What do you think the pride of a hunter is?", 80 | "It is booty obtained through victory.", 81 | "It is a token of courage and a lifelong medal.", 82 | "But I am currently prohibited from hunting here.", 83 | "It's only because I am from Behr...", 84 | "They even took my matchlock away.", 85 | "I will be made fun of if I go back empty-handed.", 86 | "Can you get hunting booty from the steppes for me?", 87 | "I will reward you handsomely.", 88 | "But be careful, the Griffons are no pushovers!", 89 | "\n", 90 | "If I go back empty-handed,", 91 | "those arrogant hunters will make fun of me.", 92 | "Please help. Get the spoils of Navarn Steppe.", 93 | "I will reward you handsomely.", 94 | "\n", 95 | "Perfect! If I wear this feathered hat, ", 96 | "I'm sure Indri would give me some recognition.", 97 | "You have to say hi if you see me at Behr later." 98 | ], 99 | "rewards": { 100 | "standard": [{ 101 | "type": "item", 102 | "id": "434", 103 | "icon": "/items/new_icon/06_pc_equipitem/00_common/00_etc/00000433.png", 104 | "name": "Contribution EXP 400", 105 | "shortUrl": "/us/item/434/", 106 | "amount": 400 107 | }, { 108 | "type": "item", 109 | "id": "3", 110 | "icon": "/items/new_icon/00000003_special.png", 111 | "name": "Gold Bar 10G", 112 | "shortUrl": "/us/item/3/", 113 | "amount": 3 114 | }, { 115 | "type": "item", 116 | "id": "44295", 117 | "icon": "/items/new_icon/03_etc/04_dropitem/00044295.png", 118 | "name": "Peridot Leaf", 119 | "shortUrl": "/us/item/44295/", 120 | "amount": 20 121 | }, { 122 | "type": "item", 123 | "id": "35554", 124 | "icon": "/items/new_icon/03_etc/12_doapplydirectlyitem/00000000_know_icon.png", 125 | "name": "Courage of Navarn Steppe", 126 | "shortUrl": "/us/item/35554/", 127 | "amount": 1 128 | }, { 129 | "type": "npc", 130 | "id": "45525", 131 | "icon": "/items/ui_artwork/ic_00454.png", 132 | "name": "Jensen", 133 | "shortUrl": "/us/npc/45525/", 134 | "amity_gained": 20 135 | }, { 136 | "type": "exp", 137 | "icon": "/images/exp.png", 138 | "name": "EXP", 139 | "amount": 100 140 | }], 141 | "choose": [] 142 | } 143 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 8 | "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 9 | // "lib": [], /* Specify library files to be included in the compilation. */ 10 | // "allowJs": true, /* Allow javascript files to be compiled. */ 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 13 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 15 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 16 | // "outFile": "./", /* Concatenate and emit output to single file. */ 17 | "outDir": "./dist", /* Redirect output structure to the directory. */ 18 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 19 | // "composite": true, /* Enable project compilation */ 20 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 21 | // "removeComments": true, /* Do not emit comments to output. */ 22 | // "noEmit": true, /* Do not emit outputs. */ 23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 26 | 27 | /* Strict Type-Checking Options */ 28 | "strict": true, /* Enable all strict type-checking options. */ 29 | "noImplicitAny": false, /* Raise error on expressions and declarations with an implied 'any' type. */ 30 | // "strictNullChecks": true, /* Enable strict null checks. */ 31 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 32 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 33 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 34 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 35 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 36 | 37 | /* Additional Checks */ 38 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 39 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 40 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 41 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 42 | 43 | /* Module Resolution Options */ 44 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 45 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 46 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 47 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 48 | // "typeRoots": [], /* List of folders to include type definitions from. */ 49 | // "types": [], /* Type declaration files to be included in compilation. */ 50 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 51 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 52 | "resolveJsonModule": true, 53 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 54 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 55 | 56 | /* Source Map Options */ 57 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 58 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 59 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 60 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 61 | 62 | /* Experimental Options */ 63 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 64 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 65 | 66 | /* Advanced Options */ 67 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 68 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 69 | }, 70 | "exclude": ["./tests", "./dist"] 71 | } 72 | --------------------------------------------------------------------------------