merged libraries into one
This commit is contained in:
130
packages/lib/src/eve/models/attribute.ts
Normal file
130
packages/lib/src/eve/models/attribute.ts
Normal file
@@ -0,0 +1,130 @@
|
||||
import type { LocalizedString } from './shared-types';
|
||||
import { dataSets, loadModels } from './loadModels';
|
||||
|
||||
export interface Attribute {
|
||||
readonly attribute_id: number;
|
||||
readonly category_id: number;
|
||||
readonly data_type: number;
|
||||
readonly default_value: number;
|
||||
readonly description: LocalizedString;
|
||||
readonly high_is_good: boolean;
|
||||
readonly icon_id?: number;
|
||||
readonly name: string;
|
||||
readonly published: boolean;
|
||||
readonly stackable: boolean;
|
||||
readonly unit_id?: number;
|
||||
readonly display_name: LocalizedString;
|
||||
readonly tooltip_title?: LocalizedString;
|
||||
readonly tooltip_description?: LocalizedString;
|
||||
}
|
||||
|
||||
export const getAttribute = (id: number) => {
|
||||
if (!dataSets.loaded) loadModels();
|
||||
const data = dataSets.dogma_attributes[String(id)];
|
||||
if (!data) throw new Error(`Attribute ID ${id} not found in reference data`);
|
||||
return data;
|
||||
};
|
||||
|
||||
export enum CommonAttribute {
|
||||
// Structure
|
||||
StructureHitpoints = 9,
|
||||
CargoCapacity = 38,
|
||||
DroneCapacity = 283,
|
||||
DroneBandwidth = 1271,
|
||||
Mass = 4,
|
||||
Volume = 161,
|
||||
InertiaModifier = 70,
|
||||
StructureEMResistance = 113,
|
||||
StructureThermalResistance = 110,
|
||||
StructureKineticResistance = 109,
|
||||
StructureExplosiveResistance = 111,
|
||||
|
||||
// Armor
|
||||
ArmorHitpoints = 265,
|
||||
ArmorEMResistance = 267,
|
||||
ArmorThermalResistance = 270,
|
||||
ArmorKineticResistance = 269,
|
||||
ArmorExplosiveResistance = 268,
|
||||
|
||||
// Shield
|
||||
ShieldCapacity = 263,
|
||||
ShieldRechargeTime = 479,
|
||||
ShieldEMResistance = 271,
|
||||
ShieldThermalResistance = 274,
|
||||
ShieldKineticResistance = 273,
|
||||
ShieldExplosiveResistance = 272,
|
||||
|
||||
// Electronic Resistances
|
||||
CapacitorWarfareResistance = 2045,
|
||||
StasisWebifierResistance = 2115,
|
||||
WeaponDisruptionResistance = 2113,
|
||||
|
||||
// Capacitor
|
||||
CapacitorCapacity = 482,
|
||||
CapacitorRechargeTime = 55,
|
||||
|
||||
// Targeting
|
||||
MaxTargetRange = 76,
|
||||
MaxLockedTargets = 192,
|
||||
SignatureRadius = 552,
|
||||
ScanResolution = 564,
|
||||
RadarSensorStrength = 208,
|
||||
MagnetometricSensorStrength = 210,
|
||||
GravimetricSensorStrength = 211,
|
||||
LadarSensorStrength = 209,
|
||||
|
||||
// Jump Drive Systems
|
||||
HasJumpDrive = 861,
|
||||
JumpDriveCapacitorNeed = 898,
|
||||
MaxJumpRange = 867,
|
||||
JumpDriveFuelNeed = 866,
|
||||
JumpDriveConsumptionAmount = 868,
|
||||
FuelBayCapacity = 1549,
|
||||
ConduitJumpConsumptionAmount = 3131,
|
||||
COnduitJumpPassengerCapacity = 3133,
|
||||
|
||||
// Propulsion
|
||||
MaxVelocity = 37,
|
||||
WarpSpeed = 600,
|
||||
|
||||
// FITTING
|
||||
|
||||
// Slots
|
||||
HighSlots = 14,
|
||||
MediumSlots = 13,
|
||||
LowSlots = 12,
|
||||
|
||||
// Stats
|
||||
PowergridOutput = 11,
|
||||
CPUOutput = 48,
|
||||
TurretHardpoints = 102,
|
||||
LauncherHardpoints = 101,
|
||||
|
||||
// Rigging
|
||||
RigSlots = 1137,
|
||||
RigSize = 1547,
|
||||
Calibration = 1132,
|
||||
|
||||
// Module
|
||||
CPUUsage = 50,
|
||||
PowergridUsage = 30,
|
||||
ActivationCost = 6,
|
||||
|
||||
// EWAR
|
||||
MaxVelocityBonus = 20,
|
||||
WarpScrambleStrength = 105,
|
||||
WarpDisruptionStrength = 2425,
|
||||
WarpDisruptionRange = 103,
|
||||
|
||||
// Weapon
|
||||
DamageMultiplier = 64,
|
||||
AccuracyFalloff = 158,
|
||||
OptimalRange = 54,
|
||||
RateOfFire = 51,
|
||||
TrackingSpeed = 160,
|
||||
ReloadTime = 1795,
|
||||
ActivationTime = 73,
|
||||
UsedWithCharge1 = 604,
|
||||
UsedWithCharge2 = 605,
|
||||
ChargeSize = 128,
|
||||
}
|
||||
105
packages/lib/src/eve/models/blueprint.ts
Normal file
105
packages/lib/src/eve/models/blueprint.ts
Normal file
@@ -0,0 +1,105 @@
|
||||
import { ActivityType, type TypeIDQuantity } from './shared-types';
|
||||
import type { Type } from './type';
|
||||
import { getType } from './type';
|
||||
import { dataSets, loadModels } from './loadModels';
|
||||
|
||||
export interface Activity {
|
||||
time: number;
|
||||
}
|
||||
|
||||
export interface ManufacturingActivity extends Activity {
|
||||
time: number;
|
||||
materials: { [type_id: string]: TypeIDQuantity };
|
||||
products: { [type_id: string]: TypeIDQuantity };
|
||||
}
|
||||
|
||||
export interface InventionActivity extends Activity {
|
||||
time: number;
|
||||
materials: { [type_id: string]: TypeIDQuantity };
|
||||
products: { [type_id: string]: TypeIDQuantity };
|
||||
skills: { [skill_type_id: string]: number }; // skill_type_id : level
|
||||
}
|
||||
|
||||
export interface TypeQuantity {
|
||||
type: Type;
|
||||
quantity: number;
|
||||
}
|
||||
|
||||
export interface Blueprint {
|
||||
readonly blueprint_type_id: number;
|
||||
readonly max_production_limit: number;
|
||||
readonly activities: {
|
||||
[ActivityType.MANUFACTURING]?: ManufacturingActivity;
|
||||
[ActivityType.RESEARCH_MATERIAL]?: Activity;
|
||||
[ActivityType.RESEARCH_TIME]?: Activity;
|
||||
[ActivityType.COPYING]?: Activity;
|
||||
[ActivityType.INVENTION]?: InventionActivity;
|
||||
};
|
||||
}
|
||||
|
||||
export function getBlueprint(blueprint_type_id: number) {
|
||||
if (!dataSets.loaded) loadModels();
|
||||
const data = dataSets.blueprints[String(blueprint_type_id)];
|
||||
if (!data) throw new Error(`Blueprint Type ID ${blueprint_type_id} not found in reference data`);
|
||||
return data;
|
||||
}
|
||||
|
||||
export function getManufacturingMaterials(blueprint: Blueprint) {
|
||||
const manufacturing = blueprint.activities[ActivityType.MANUFACTURING];
|
||||
if (!manufacturing) return [];
|
||||
|
||||
return Promise.all(
|
||||
Object.entries(manufacturing.materials).map(([type_id, { quantity }]) => ({
|
||||
type: getType(parseInt(type_id)),
|
||||
quantity,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
export function getManufacturingProducts(blueprint: Blueprint) {
|
||||
const manufacturing = blueprint.activities[ActivityType.MANUFACTURING];
|
||||
if (!manufacturing) return [];
|
||||
|
||||
return Promise.all(
|
||||
Object.entries(manufacturing.products).map(([type_id, { quantity }]) => ({
|
||||
type: getType(parseInt(type_id)),
|
||||
quantity,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
export function getInventionMaterials(blueprint: Blueprint) {
|
||||
const invention = blueprint.activities[ActivityType.INVENTION];
|
||||
if (!invention) return [];
|
||||
|
||||
return Promise.all(
|
||||
Object.entries(invention.materials).map(([type_id, { quantity }]) => ({
|
||||
type: getType(parseInt(type_id)),
|
||||
quantity,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
export function getInventionProducts(blueprint: Blueprint) {
|
||||
const invention = blueprint.activities[ActivityType.INVENTION];
|
||||
if (!invention) return [];
|
||||
|
||||
return Promise.all(
|
||||
Object.entries(invention.products).map(([type_id, { quantity }]) => ({
|
||||
type: getType(parseInt(type_id)),
|
||||
quantity,
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
export function getInventionSkills(blueprint: Blueprint) {
|
||||
const invention = blueprint.activities[ActivityType.INVENTION];
|
||||
if (!invention) return [];
|
||||
|
||||
return Promise.all(
|
||||
Object.entries(invention.skills).map(([skill_type_id, level]) => ({
|
||||
type: getType(parseInt(skill_type_id)),
|
||||
level,
|
||||
})),
|
||||
);
|
||||
}
|
||||
35
packages/lib/src/eve/models/category.ts
Normal file
35
packages/lib/src/eve/models/category.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import type { LocalizedString } from './shared-types';
|
||||
import { dataSets, loadModels } from './loadModels';
|
||||
|
||||
export enum CommonCategory {
|
||||
CARGO = 5,
|
||||
SHIP = 6,
|
||||
MODULE = 7,
|
||||
CHARGE = 8,
|
||||
BLUEPRINT = 9,
|
||||
SKILL = 16,
|
||||
DRONE = 18,
|
||||
IMPLANT = 20,
|
||||
APPAREL = 30,
|
||||
DEPLOYABLE = 22,
|
||||
REACTION = 24,
|
||||
SUBSYSTEM = 32,
|
||||
STRUCTURE = 65,
|
||||
STRUCTURE_MODULE = 66,
|
||||
FIGHTER = 87,
|
||||
}
|
||||
|
||||
export interface Category {
|
||||
readonly category_id: number;
|
||||
readonly name: LocalizedString;
|
||||
readonly published: boolean;
|
||||
readonly group_ids: number[];
|
||||
readonly icon_id?: number;
|
||||
}
|
||||
|
||||
export function getCategory(category_id: number) {
|
||||
if (!dataSets.loaded) loadModels();
|
||||
const data = dataSets.categories[String(category_id)];
|
||||
if (!data) throw new Error(`Category ID ${category_id} not found in reference data`);
|
||||
return data;
|
||||
}
|
||||
65
packages/lib/src/eve/models/effect.ts
Normal file
65
packages/lib/src/eve/models/effect.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { getAttribute } from './attribute';
|
||||
import type { LocalizedString } from './shared-types';
|
||||
import { dataSets, loadModels } from './loadModels';
|
||||
|
||||
interface Modifier {
|
||||
domain: number;
|
||||
func: number;
|
||||
group_id?: number;
|
||||
modified_attribute_id: number;
|
||||
modifying_attribute_id: number;
|
||||
skill_type_id?: number;
|
||||
operator: number;
|
||||
}
|
||||
|
||||
export interface Effect {
|
||||
readonly effect_id: number;
|
||||
readonly disallow_auto_repeat: boolean;
|
||||
readonly discharge_attribute_id?: number;
|
||||
readonly distribution?: number;
|
||||
readonly duration_attribute_id?: number;
|
||||
readonly effect_category: number;
|
||||
readonly effect_name: string;
|
||||
readonly electronic_chance: boolean;
|
||||
readonly falloff_attribute_id?: number;
|
||||
readonly guid: string;
|
||||
readonly is_assistance: boolean;
|
||||
readonly is_offensive: boolean;
|
||||
readonly is_warp_safe: boolean;
|
||||
readonly propulsion_chance: boolean;
|
||||
readonly published: boolean;
|
||||
readonly range_attribute_id?: number;
|
||||
readonly range_chance: boolean;
|
||||
readonly modifiers: Modifier[];
|
||||
readonly tracking_speed_attribute_id?: number;
|
||||
readonly description: LocalizedString;
|
||||
readonly display_name: LocalizedString;
|
||||
readonly name: string;
|
||||
}
|
||||
|
||||
export function getEffect(effect_id: number) {
|
||||
if (!dataSets.loaded) loadModels();
|
||||
const data = dataSets.dogma_effects[String(effect_id)];
|
||||
if (!data) throw new Error(`Effect ID ${effect_id} not found in reference data`);
|
||||
return data;
|
||||
}
|
||||
|
||||
export function getDischargeAttribute(effect: Effect) {
|
||||
return effect.discharge_attribute_id && getAttribute(effect.discharge_attribute_id);
|
||||
}
|
||||
|
||||
export function getFalloffAttribute(effect: Effect) {
|
||||
return effect.falloff_attribute_id && getAttribute(effect.falloff_attribute_id);
|
||||
}
|
||||
|
||||
export function getDurationAttribute(effect: Effect) {
|
||||
return effect.duration_attribute_id && getAttribute(effect.duration_attribute_id);
|
||||
}
|
||||
|
||||
export function getRangeAttribute(effect: Effect) {
|
||||
return effect.range_attribute_id && getAttribute(effect.range_attribute_id);
|
||||
}
|
||||
|
||||
export function getTrackingSpeedAttribute(effect: Effect) {
|
||||
return effect.tracking_speed_attribute_id && getAttribute(effect.tracking_speed_attribute_id);
|
||||
}
|
||||
29
packages/lib/src/eve/models/group.ts
Normal file
29
packages/lib/src/eve/models/group.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import type { LocalizedString } from './shared-types';
|
||||
import { dataSets, loadModels } from './loadModels';
|
||||
|
||||
export interface Group {
|
||||
readonly group_id: number;
|
||||
readonly category_id: number;
|
||||
readonly name: LocalizedString;
|
||||
readonly published: boolean;
|
||||
readonly icon_id?: number;
|
||||
readonly anchorable: boolean;
|
||||
readonly anchored: boolean;
|
||||
readonly fittable_non_singleton: boolean;
|
||||
readonly use_base_price: boolean;
|
||||
readonly type_ids?: number[];
|
||||
}
|
||||
export function getGroup(group_id: number) {
|
||||
if (!dataSets.loaded) loadModels();
|
||||
const data = dataSets.groups[String(group_id)];
|
||||
if (!data) throw new Error(`Group ID ${group_id} not found in reference data`);
|
||||
return data;
|
||||
}
|
||||
|
||||
export function groupEveRefLink(group_id: number) {
|
||||
return `https://everef.net/groups/${group_id}`;
|
||||
}
|
||||
|
||||
export function renderGroupEveRefLink(group: Group, locale: string = 'en') {
|
||||
return `[${group.name[locale] ?? group.name.en}](${groupEveRefLink(group.group_id)})`;
|
||||
}
|
||||
41
packages/lib/src/eve/models/icon.ts
Normal file
41
packages/lib/src/eve/models/icon.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { dataSets, loadModels } from './loadModels';
|
||||
|
||||
export enum IconSize {
|
||||
SIZE_32 = 32,
|
||||
SIZE_64 = 64,
|
||||
SIZE_128 = 128,
|
||||
SIZE_256 = 256,
|
||||
SIZE_512 = 512,
|
||||
}
|
||||
|
||||
export interface Icon {
|
||||
readonly icon_id: number;
|
||||
readonly description: string;
|
||||
readonly file: string;
|
||||
}
|
||||
|
||||
export function getIcon(icon_id: number) {
|
||||
if (!dataSets.loaded) loadModels();
|
||||
const data = dataSets.icons[String(icon_id)];
|
||||
if (!data) throw new Error(`Icon ID ${icon_id} not found in reference data`);
|
||||
return data;
|
||||
}
|
||||
|
||||
export function getIconUrl(
|
||||
icon_id: Icon,
|
||||
{
|
||||
size = IconSize.SIZE_64,
|
||||
isBp = false,
|
||||
isBpc = false,
|
||||
}: {
|
||||
size?: IconSize;
|
||||
isBp?: boolean;
|
||||
isBpc?: boolean;
|
||||
} = {},
|
||||
): string {
|
||||
return `https://images.evetech.net/types/${icon_id}/icon${
|
||||
isBp ? '/bp'
|
||||
: isBpc ? '/bpc'
|
||||
: ''
|
||||
}?size=${size}`;
|
||||
}
|
||||
14
packages/lib/src/eve/models/index.ts
Normal file
14
packages/lib/src/eve/models/index.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
export * from './attribute';
|
||||
export * from './blueprint';
|
||||
export * from './category';
|
||||
export * from './effect';
|
||||
export * from './group';
|
||||
export * from './icon';
|
||||
export * from './market-group';
|
||||
export * from './meta-group';
|
||||
export * from './region';
|
||||
export * from './schematic';
|
||||
export * from './skill';
|
||||
export * from './solar-system';
|
||||
export * from './type';
|
||||
export * from './unit';
|
||||
53
packages/lib/src/eve/models/loadModels.ts
Normal file
53
packages/lib/src/eve/models/loadModels.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import fs from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import type { Unit } from './unit';
|
||||
import type { SolarSystem } from './solar-system';
|
||||
import type { Attribute } from './attribute';
|
||||
import type { Blueprint } from './blueprint';
|
||||
import type { Category } from './category';
|
||||
import type { Effect } from './effect';
|
||||
import type { Group } from './group';
|
||||
import type { Icon } from './icon';
|
||||
import type { MarketGroup } from './market-group';
|
||||
import type { MetaGroup } from './meta-group';
|
||||
import type { Region } from './region';
|
||||
import type { Schematic } from './schematic';
|
||||
import type { Skill } from './skill';
|
||||
import type { Type } from './type';
|
||||
|
||||
const dataSets = {
|
||||
loaded: false,
|
||||
dogma_attributes: {} as Record<string, Attribute>,
|
||||
blueprints: {} as Record<string, Blueprint>,
|
||||
categories: {} as Record<string, Category>,
|
||||
dogma_effects: {} as Record<string, Effect>,
|
||||
groups: {} as Record<string, Group>,
|
||||
icons: {} as Record<string, Icon>,
|
||||
market_groups: {} as Record<string, MarketGroup>,
|
||||
meta_groups: {} as Record<string, MetaGroup>,
|
||||
regions: {} as Record<string, Region>,
|
||||
schematics: {} as Record<string, Schematic>,
|
||||
skills: {} as Record<string, Skill>,
|
||||
solar_systems: {} as Record<string, SolarSystem>,
|
||||
types: {} as Record<string, Type>,
|
||||
units: {} as Record<string, Unit>,
|
||||
};
|
||||
export async function loadModels() {
|
||||
dataSets.dogma_attributes = JSON.parse(fs.readFileSync(join(__dirname, '../data/reference-data/dogma_attributes.json')).toString());
|
||||
dataSets.blueprints = JSON.parse(fs.readFileSync(join(__dirname, '../data/reference-data/blueprints.json')).toString());
|
||||
dataSets.categories = JSON.parse(fs.readFileSync(join(__dirname, '../data/reference-data/categories.json')).toString());
|
||||
dataSets.dogma_effects = JSON.parse(fs.readFileSync(join(__dirname, '../data/reference-data/dogma_effects.json')).toString());
|
||||
dataSets.groups = JSON.parse(fs.readFileSync(join(__dirname, '../data/reference-data/groups.json')).toString());
|
||||
dataSets.icons = JSON.parse(fs.readFileSync(join(__dirname, '../data/reference-data/icons.json')).toString());
|
||||
dataSets.market_groups = JSON.parse(fs.readFileSync(join(__dirname, '../data/reference-data/market_groups.json')).toString());
|
||||
dataSets.meta_groups = JSON.parse(fs.readFileSync(join(__dirname, '../data/reference-data/meta_groups.json')).toString());
|
||||
dataSets.regions = JSON.parse(fs.readFileSync(join(__dirname, '../data/reference-data/regions.json')).toString());
|
||||
dataSets.schematics = JSON.parse(fs.readFileSync(join(__dirname, '../data/reference-data/schematics.json')).toString());
|
||||
dataSets.skills = JSON.parse(fs.readFileSync(join(__dirname, '../data/reference-data/skills.json')).toString());
|
||||
dataSets.solar_systems = JSON.parse(fs.readFileSync(join(__dirname, '../data/reference-data/solar_systems.json')).toString());
|
||||
dataSets.types = JSON.parse(fs.readFileSync(join(__dirname, '../data/reference-data/types.json')).toString());
|
||||
dataSets.units = JSON.parse(fs.readFileSync(join(__dirname, '../data/reference-data/units.json')).toString());
|
||||
dataSets.loaded = true;
|
||||
}
|
||||
|
||||
export { dataSets };
|
||||
24
packages/lib/src/eve/models/market-group.ts
Normal file
24
packages/lib/src/eve/models/market-group.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import type { LocalizedString } from './shared-types';
|
||||
import { dataSets, loadModels } from './loadModels';
|
||||
|
||||
export interface MarketGroup {
|
||||
readonly market_group_id: number;
|
||||
readonly parent_group_id: number;
|
||||
readonly name: LocalizedString;
|
||||
readonly description: LocalizedString;
|
||||
readonly child_market_group_ids: number[];
|
||||
readonly icon_id: number;
|
||||
readonly has_types: boolean;
|
||||
}
|
||||
|
||||
export function getMarketGroup(market_group_id: number) {
|
||||
if (!dataSets.loaded) loadModels();
|
||||
const data = dataSets.market_groups[String(market_group_id)];
|
||||
if (!data) throw new Error(`Market group ID ${market_group_id} not found in reference data`);
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function getAllChildMarketGroups(marketGroup: MarketGroup): Promise<MarketGroup[]> {
|
||||
const children = await Promise.all(marketGroup.child_market_group_ids.map((id) => getMarketGroup(id)));
|
||||
return children.concat(...(await Promise.all(children.map((child) => getAllChildMarketGroups(child)))));
|
||||
}
|
||||
17
packages/lib/src/eve/models/meta-group.ts
Normal file
17
packages/lib/src/eve/models/meta-group.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import type { LocalizedString } from './shared-types';
|
||||
import { dataSets, loadModels } from './loadModels';
|
||||
|
||||
export interface MetaGroup {
|
||||
readonly meta_group_id: number;
|
||||
readonly name: LocalizedString;
|
||||
readonly type_ids: number[];
|
||||
readonly icon_id?: number;
|
||||
readonly icon_suffix?: string;
|
||||
}
|
||||
|
||||
export function getMetaGroup(meta_group_id: number) {
|
||||
if (!dataSets.loaded) loadModels();
|
||||
const data = dataSets.meta_groups[String(meta_group_id)];
|
||||
if (!data) throw new Error(`Meta group ID ${meta_group_id} not found in reference data`);
|
||||
return data;
|
||||
}
|
||||
24
packages/lib/src/eve/models/region.ts
Normal file
24
packages/lib/src/eve/models/region.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import type { LocalizedString, Position } from './shared-types';
|
||||
import { dataSets, loadModels } from './loadModels';
|
||||
|
||||
export interface Region {
|
||||
readonly region_id: number;
|
||||
readonly center: Position;
|
||||
readonly description_id: number;
|
||||
readonly faction_id: number;
|
||||
readonly max: Position;
|
||||
readonly min: Position;
|
||||
readonly name_id: number;
|
||||
readonly wormhole_class_id?: number;
|
||||
readonly nebula_id?: number;
|
||||
readonly universe_id: string;
|
||||
readonly description: LocalizedString;
|
||||
readonly name: LocalizedString;
|
||||
}
|
||||
|
||||
export function getRegion(region_id: number) {
|
||||
if (!dataSets.loaded) loadModels();
|
||||
const data = dataSets.regions[String(region_id)];
|
||||
if (!data) throw new Error(`Region ID ${region_id} not found in reference data`);
|
||||
return data;
|
||||
}
|
||||
37
packages/lib/src/eve/models/schematic.ts
Normal file
37
packages/lib/src/eve/models/schematic.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import type { LocalizedString, TypeIDQuantity } from './shared-types';
|
||||
import { getType } from './type';
|
||||
import { dataSets, loadModels } from './loadModels';
|
||||
|
||||
export interface Schematic {
|
||||
readonly schematic_id: number;
|
||||
readonly cycle_time: number;
|
||||
readonly name: LocalizedString;
|
||||
readonly materials: { [type_id: string]: TypeIDQuantity };
|
||||
readonly products: { [type_id: string]: TypeIDQuantity };
|
||||
readonly pin_type_ids: number[];
|
||||
}
|
||||
|
||||
export function getSchematic(schematic_id: number) {
|
||||
if (!dataSets.loaded) loadModels();
|
||||
const data = dataSets.schematics[String(schematic_id)];
|
||||
if (!data) throw new Error(`Schematic ID ${schematic_id} not found in reference data`);
|
||||
return data;
|
||||
}
|
||||
|
||||
export function getMaterialQuantities(schematic: Schematic) {
|
||||
return Object.entries(schematic.materials).map(([type_id, { quantity }]) => ({
|
||||
type: getType(Number(type_id)),
|
||||
quantity,
|
||||
}));
|
||||
}
|
||||
|
||||
export function getProductQuantities(schematic: Schematic) {
|
||||
return Object.entries(schematic.products).map(([type_id, { quantity }]) => ({
|
||||
type: getType(Number(type_id)),
|
||||
quantity,
|
||||
}));
|
||||
}
|
||||
|
||||
export function getPinTypes(schematic: Schematic) {
|
||||
return schematic.pin_type_ids.map(getType);
|
||||
}
|
||||
56
packages/lib/src/eve/models/shared-types.ts
Normal file
56
packages/lib/src/eve/models/shared-types.ts
Normal file
@@ -0,0 +1,56 @@
|
||||
import type { Type } from './type';
|
||||
|
||||
export interface LocalizedString {
|
||||
de?: string;
|
||||
en?: string;
|
||||
es?: string;
|
||||
fr?: string;
|
||||
ja?: string;
|
||||
ko?: string;
|
||||
ru?: string;
|
||||
zh?: string;
|
||||
}
|
||||
|
||||
export interface TypeIDQuantity {
|
||||
type_id: number;
|
||||
quantity: number;
|
||||
}
|
||||
|
||||
export interface TypeQuantity {
|
||||
type: Type;
|
||||
quantity: number;
|
||||
}
|
||||
|
||||
export interface AttributeIDValue {
|
||||
attribute_id: number;
|
||||
value: number;
|
||||
}
|
||||
|
||||
export interface EffectIDDefault {
|
||||
effect_id: number;
|
||||
is_default: boolean;
|
||||
}
|
||||
|
||||
export interface MaterialIDQuantity {
|
||||
material_type_id: number;
|
||||
quantity: number;
|
||||
}
|
||||
|
||||
export interface BlueprintTypeIDActivity {
|
||||
blueprint_type_id: number;
|
||||
blueprint_activity: ActivityType;
|
||||
}
|
||||
|
||||
export enum ActivityType {
|
||||
MANUFACTURING = 'manufacturing',
|
||||
RESEARCH_MATERIAL = 'research_material',
|
||||
RESEARCH_TIME = 'research_time',
|
||||
COPYING = 'copying',
|
||||
INVENTION = 'invention',
|
||||
}
|
||||
|
||||
export interface Position {
|
||||
x: number;
|
||||
y: number;
|
||||
z: number;
|
||||
}
|
||||
47
packages/lib/src/eve/models/skill.ts
Normal file
47
packages/lib/src/eve/models/skill.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { type Attribute, getAttribute } from './attribute';
|
||||
import { dataSets, loadModels } from './loadModels';
|
||||
|
||||
export interface Skill {
|
||||
readonly type_id: number;
|
||||
readonly primary_dogma_attribute_id: number;
|
||||
readonly secondary_dogma_attribute_id: number;
|
||||
readonly primary_character_attribute_id: number;
|
||||
readonly secondary_character_attribute_id: number;
|
||||
readonly training_time_multiplier: number;
|
||||
readonly required_skills?: { [skill_type_id: string]: number }; // skill_type_id : level
|
||||
}
|
||||
|
||||
export function getSkill(type_id: number) {
|
||||
if (!dataSets.loaded) loadModels();
|
||||
const data = dataSets.skills[String(type_id)];
|
||||
if (!data) throw new Error(`Skill ID ${type_id} not found in reference data`);
|
||||
return data;
|
||||
}
|
||||
|
||||
export function getPrimaryDogmaAttribute(skill: Skill) {
|
||||
return getAttribute(skill.primary_dogma_attribute_id);
|
||||
}
|
||||
|
||||
export function getSecondaryDogmaAttribute(skill: Skill) {
|
||||
return getAttribute(skill.secondary_dogma_attribute_id);
|
||||
}
|
||||
|
||||
export function getPrimaryCharacterAttribute(skill: Skill) {
|
||||
return getAttribute(skill.primary_character_attribute_id);
|
||||
}
|
||||
|
||||
export function getSecondaryCharacterAttribute(skill: Skill) {
|
||||
return getAttribute(skill.secondary_character_attribute_id);
|
||||
}
|
||||
|
||||
export function getPrerequisites(skill: Skill): { skill: Skill; level: number }[] {
|
||||
if (!skill.required_skills) return [];
|
||||
return Object.entries(skill.required_skills).map(([skill_type_id, level]) => ({
|
||||
skill: getSkill(parseInt(skill_type_id)),
|
||||
level,
|
||||
}));
|
||||
}
|
||||
|
||||
export function skillpointsAtLevel(skill: Skill, level: number): number {
|
||||
return Math.pow(2, 2.5 * (level - 1)) * 250 * skill.training_time_multiplier;
|
||||
}
|
||||
42
packages/lib/src/eve/models/solar-system.ts
Normal file
42
packages/lib/src/eve/models/solar-system.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { dataSets, loadModels } from './loadModels';
|
||||
|
||||
export interface SolarSystem {
|
||||
readonly regionID: number;
|
||||
readonly constellationID: number;
|
||||
readonly solarSystemID: number;
|
||||
readonly solarSystemName: string;
|
||||
readonly x: number;
|
||||
readonly y: number;
|
||||
readonly z: number;
|
||||
readonly xMin: number;
|
||||
readonly xMax: number;
|
||||
readonly yMin: number;
|
||||
readonly yMax: number;
|
||||
readonly zMin: number;
|
||||
readonly zMax: number;
|
||||
readonly luminosity: number;
|
||||
readonly border: boolean;
|
||||
readonly fringe: boolean;
|
||||
readonly corridor: boolean;
|
||||
readonly hub: boolean;
|
||||
readonly international: boolean;
|
||||
readonly regional: boolean;
|
||||
readonly security: number;
|
||||
readonly factionID: number;
|
||||
readonly radius: number;
|
||||
readonly sunTypeID: number;
|
||||
readonly securityClass: string;
|
||||
}
|
||||
|
||||
export function getSolarSystem(solarSystemID: number): SolarSystem {
|
||||
if (!dataSets.loaded) loadModels();
|
||||
const data = dataSets.solar_systems[String(solarSystemID)] as any;
|
||||
if (!data) throw new Error(`Solar System ID ${solarSystemID} not found in reference data`);
|
||||
return {
|
||||
...data,
|
||||
security: parseFloat(data.security),
|
||||
radius: parseFloat(data.radius),
|
||||
sunTypeID: parseInt(data.sun_type_id, 10),
|
||||
securityClass: data.security_class ?? 'nullsec', // Default to 'nullsec' if security_class is not present
|
||||
};
|
||||
}
|
||||
197
packages/lib/src/eve/models/type.ts
Normal file
197
packages/lib/src/eve/models/type.ts
Normal file
@@ -0,0 +1,197 @@
|
||||
import type { AttributeIDValue, BlueprintTypeIDActivity, EffectIDDefault, LocalizedString, MaterialIDQuantity } from './shared-types';
|
||||
import { IconSize } from './icon';
|
||||
import { getUnit, type Unit } from './unit';
|
||||
import { CommonAttribute, getAttribute } from './attribute';
|
||||
import { getGroup } from './group';
|
||||
import { getMetaGroup } from './meta-group';
|
||||
import { dataSets, loadModels } from './loadModels';
|
||||
|
||||
interface Masteries {
|
||||
'0': number[];
|
||||
'1': number[];
|
||||
'2': number[];
|
||||
'3': number[];
|
||||
'4': number[];
|
||||
}
|
||||
|
||||
interface Bonus {
|
||||
bonus: number;
|
||||
bonus_text: LocalizedString;
|
||||
importance: number;
|
||||
unit_id: number;
|
||||
}
|
||||
|
||||
interface Traits {
|
||||
misc_bonuses: { [level: string]: Bonus };
|
||||
role_bonuses: { [level: string]: Bonus };
|
||||
types: { [skill_type_id: string]: { [order: string]: Bonus } };
|
||||
}
|
||||
|
||||
export interface Type {
|
||||
readonly type_id: number;
|
||||
readonly name: LocalizedString;
|
||||
readonly description: LocalizedString;
|
||||
readonly published: boolean;
|
||||
readonly group_id?: number;
|
||||
readonly base_price?: number;
|
||||
readonly capacity?: number;
|
||||
readonly faction_id?: number;
|
||||
readonly graphic_id?: number;
|
||||
readonly market_group_id?: number;
|
||||
readonly mass?: number;
|
||||
readonly masteries?: Masteries;
|
||||
readonly meta_group_id?: number;
|
||||
readonly portion_size?: number;
|
||||
readonly race_id?: number;
|
||||
readonly radius?: number;
|
||||
readonly sof_faction_name?: string;
|
||||
readonly sound_id?: number;
|
||||
readonly traits?: Traits;
|
||||
readonly volume?: number;
|
||||
readonly dogma_attributes?: {
|
||||
[attribute_id: string]: AttributeIDValue;
|
||||
};
|
||||
readonly dogma_effects?: { [effect_id: string]: EffectIDDefault };
|
||||
readonly packaged_volume?: number;
|
||||
readonly type_materials?: { [type_id: string]: MaterialIDQuantity };
|
||||
readonly required_skills?: { [skill_type_id: string]: number }; // skill_type_id : level
|
||||
readonly type_variations?: { [meta_group_id: string]: number[] }; // meta_group_id : type_ids[]
|
||||
readonly produced_by_blueprints?: {
|
||||
[blueprint_type_id: string]: BlueprintTypeIDActivity;
|
||||
}; // blueprint_type_id : blueprint_activity
|
||||
readonly buildable_pin_type_ids?: number[];
|
||||
readonly is_ore?: boolean;
|
||||
readonly ore_variations?: { [variant: string]: number }; // variant : type_id
|
||||
readonly produced_by_schematic_ids?: number[];
|
||||
readonly used_by_schematic_ids?: number[];
|
||||
readonly is_blueprint?: boolean;
|
||||
}
|
||||
|
||||
export function getType(type_id: number) {
|
||||
if (!dataSets.loaded) loadModels();
|
||||
const data = dataSets.types[String(type_id)];
|
||||
if (!data) throw new Error(`Type ID ${type_id} not found in reference data`);
|
||||
return data;
|
||||
}
|
||||
|
||||
export function getTypeIconUrl(type: Type, size: IconSize = IconSize.SIZE_64) {
|
||||
return `https://images.evetech.net/types/${type.type_id}/icon${type.is_blueprint ? '/bp' : ''}?size=${size}`;
|
||||
}
|
||||
|
||||
export function getSkillBonuses(type: Type): {
|
||||
skill: Type;
|
||||
bonuses: {
|
||||
bonus: number;
|
||||
bonus_text: LocalizedString;
|
||||
importance: number;
|
||||
unit: Unit;
|
||||
}[];
|
||||
}[] {
|
||||
if (!type.traits) return [];
|
||||
const skillBonuses: {
|
||||
skill: Type;
|
||||
bonuses: {
|
||||
bonus: number;
|
||||
bonus_text: LocalizedString;
|
||||
importance: number;
|
||||
unit: Unit;
|
||||
}[];
|
||||
}[] = [];
|
||||
for (const skill_type_id in type.traits.types) {
|
||||
skillBonuses.push({
|
||||
skill: getType(Number(skill_type_id)),
|
||||
bonuses: Object.keys(type.traits.types[skill_type_id]).map((order) => {
|
||||
const bonus = type.traits!.types[skill_type_id][order];
|
||||
return {
|
||||
bonus: bonus.bonus,
|
||||
bonus_text: bonus.bonus_text,
|
||||
importance: bonus.importance,
|
||||
unit: getUnit(bonus.unit_id),
|
||||
};
|
||||
}),
|
||||
});
|
||||
}
|
||||
return skillBonuses;
|
||||
}
|
||||
|
||||
export function getRoleBonuses(type: Type) {
|
||||
if (!type.traits || !type.traits.role_bonuses) return [];
|
||||
return Object.values(type.traits.role_bonuses).map((bonus) => ({
|
||||
bonus: bonus.bonus,
|
||||
bonus_text: bonus.bonus_text,
|
||||
importance: bonus.importance,
|
||||
unit: bonus.unit_id ? getUnit(bonus.unit_id) : undefined,
|
||||
}));
|
||||
}
|
||||
|
||||
export function eveRefLink(type_id: number) {
|
||||
return `https://everef.net/types/${type_id}`;
|
||||
}
|
||||
|
||||
export function renderTypeEveRefLink(type: Type, locale: string = 'en') {
|
||||
return `[${type.name[locale] ?? type.name.en}](${eveRefLink(type.type_id)})`;
|
||||
}
|
||||
|
||||
export function eveTycoonLink(type_id: number) {
|
||||
return `https://evetycoon.com/market/${type_id}`;
|
||||
}
|
||||
|
||||
export function getTypeAttributes(type: Type) {
|
||||
if (!type.dogma_attributes) return [];
|
||||
Object.keys(type.dogma_attributes).map((attribute_id) => ({
|
||||
attribute: getAttribute(Number(attribute_id)),
|
||||
value: type.dogma_attributes![attribute_id].value,
|
||||
}));
|
||||
}
|
||||
|
||||
export function typeHasAnyAttribute(type: Type, attribute_ids: CommonAttribute[]) {
|
||||
if (!type.dogma_attributes) return false;
|
||||
for (const attribute_id of attribute_ids) {
|
||||
if (type.dogma_attributes[attribute_id]) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function getTypeSkills(type: Type) {
|
||||
if (!type.required_skills) return [];
|
||||
return Object.keys(type.required_skills).map((skill_type_id) => ({
|
||||
skill: getType(Number(skill_type_id)),
|
||||
level: type.required_skills![skill_type_id],
|
||||
}));
|
||||
}
|
||||
|
||||
export function typeGetAttribute(type: Type, attribute_id: number) {
|
||||
if (!type.dogma_attributes || !type.dogma_attributes[attribute_id]) return null;
|
||||
return {
|
||||
attribute: getAttribute(attribute_id),
|
||||
value: type.dogma_attributes[attribute_id].value,
|
||||
};
|
||||
}
|
||||
|
||||
export function getTypeBlueprints(type: Type) {
|
||||
if (!type.produced_by_blueprints) return [];
|
||||
return Object.values(type.produced_by_blueprints).map((blueprint) => ({
|
||||
blueprint: getType(blueprint.blueprint_type_id),
|
||||
activity: blueprint.blueprint_activity,
|
||||
}));
|
||||
}
|
||||
|
||||
export function getTypeSchematics(type: Type) {
|
||||
return type.produced_by_schematic_ids?.map((schematic_id) => getType(schematic_id)) ?? [];
|
||||
}
|
||||
|
||||
export function getTypeGroup(type: Type) {
|
||||
if (!type.group_id) return null;
|
||||
return getGroup(type.group_id);
|
||||
}
|
||||
|
||||
export function getTypeVariants(type: Type) {
|
||||
return Object.entries(type.type_variations || {}).map(([meta_group_id, variant_ids]) => ({
|
||||
metaGroup: getMetaGroup(Number(meta_group_id)),
|
||||
types: variant_ids.map((type_id) => getType(type_id)),
|
||||
}));
|
||||
}
|
||||
|
||||
export function typeHasAttributes(type: Type) {
|
||||
return type.dogma_attributes && Object.keys(type.dogma_attributes).length > 0;
|
||||
}
|
||||
406
packages/lib/src/eve/models/unit.test.ts
Normal file
406
packages/lib/src/eve/models/unit.test.ts
Normal file
@@ -0,0 +1,406 @@
|
||||
import { test, it, expect, mock, beforeEach } from 'bun:test';
|
||||
import { getUnit, renderUnit, isUnitInversePercentage, type Unit } from './unit';
|
||||
|
||||
test('unit.ts', () => {
|
||||
test('getUnit', () => {
|
||||
it('should return unit when found', async () => {
|
||||
const mockUnit: Unit = {
|
||||
unit_id: 123,
|
||||
display_name: 'Test Unit',
|
||||
description: { en: 'Test description' },
|
||||
name: { en: 'Test Name' },
|
||||
};
|
||||
|
||||
mock.module('@star-kitten/util/json-query.js', () => ({
|
||||
queryJsonObject: () => Promise.resolve(mockUnit),
|
||||
}));
|
||||
|
||||
const result = await getUnit(123);
|
||||
|
||||
expect(result).toEqual(mockUnit);
|
||||
});
|
||||
|
||||
it('should throw error when unit not found', async () => {
|
||||
mock.module('@star-kitten/util/json-query.js', () => ({
|
||||
queryJsonObject: () => Promise.resolve(null),
|
||||
}));
|
||||
|
||||
await expect(getUnit(999)).rejects.toThrow('Unit ID 999 not found in reference data');
|
||||
});
|
||||
});
|
||||
|
||||
test('renderUnit', () => {
|
||||
const mockUnit: Unit = {
|
||||
unit_id: 0,
|
||||
display_name: 'Test Unit',
|
||||
description: { en: 'Test description' },
|
||||
name: { en: 'Test Name' },
|
||||
};
|
||||
|
||||
test('inverse percentage units', () => {
|
||||
it('should render unit 108 as inverse percentage', async () => {
|
||||
const unit = { ...mockUnit, unit_id: 108 };
|
||||
const result = await renderUnit(unit, 0.75);
|
||||
expect(result).toBe('0.25 Test Unit');
|
||||
});
|
||||
|
||||
it('should render unit 111 as inverse percentage', async () => {
|
||||
const unit = { ...mockUnit, unit_id: 111 };
|
||||
const result = await renderUnit(unit, 0.3);
|
||||
expect(result).toBe('0.70 Test Unit');
|
||||
});
|
||||
|
||||
it('should handle missing display_name for inverse percentage', async () => {
|
||||
const unit = { ...mockUnit, unit_id: 108, display_name: '' };
|
||||
const result = await renderUnit(unit, 0.5);
|
||||
expect(result).toBe('0.50 ');
|
||||
});
|
||||
});
|
||||
|
||||
test('time units', () => {
|
||||
it('should render unit 3 (seconds) using convertSecondsToTimeString', async () => {
|
||||
const unit = { ...mockUnit, unit_id: 3 };
|
||||
mock.module('@star-kitten/util/text.js', () => ({
|
||||
convertSecondsToTimeString: (seconds: number) => {
|
||||
if (seconds === 330) return '5m 30s';
|
||||
return '0s';
|
||||
},
|
||||
}));
|
||||
|
||||
const result = await renderUnit(unit, 330);
|
||||
|
||||
expect(result).toBe('5m 30s');
|
||||
});
|
||||
|
||||
it('should render unit 101 (milliseconds) using convertMillisecondsToTimeString', async () => {
|
||||
const unit = { ...mockUnit, unit_id: 101 };
|
||||
mock.module('@star-kitten/util/text.js', () => ({
|
||||
convertMillisecondsToTimeString: (milliseconds: number) => {
|
||||
if (milliseconds === 2500) return '2.5s';
|
||||
return '0s';
|
||||
},
|
||||
}));
|
||||
|
||||
const result = await renderUnit(unit, 2500);
|
||||
|
||||
expect(result).toBe('2.5s');
|
||||
});
|
||||
});
|
||||
|
||||
test('size class unit (117)', () => {
|
||||
it('should render size class 1 as Small', async () => {
|
||||
const unit = { ...mockUnit, unit_id: 117 };
|
||||
const result = await renderUnit(unit, 1);
|
||||
expect(result).toBe('Small');
|
||||
});
|
||||
|
||||
it('should render size class 2 as Medium', async () => {
|
||||
const unit = { ...mockUnit, unit_id: 117 };
|
||||
const result = await renderUnit(unit, 2);
|
||||
expect(result).toBe('Medium');
|
||||
});
|
||||
|
||||
it('should render size class 3 as Large', async () => {
|
||||
const unit = { ...mockUnit, unit_id: 117 };
|
||||
const result = await renderUnit(unit, 3);
|
||||
expect(result).toBe('Large');
|
||||
});
|
||||
|
||||
it('should render size class 4 as X-Large', async () => {
|
||||
const unit = { ...mockUnit, unit_id: 117 };
|
||||
const result = await renderUnit(unit, 4);
|
||||
expect(result).toBe('X-Large');
|
||||
});
|
||||
|
||||
it('should render unknown size class as Unknown', async () => {
|
||||
const unit = { ...mockUnit, unit_id: 117 };
|
||||
const result = await renderUnit(unit, 99);
|
||||
expect(result).toBe('Unknown');
|
||||
});
|
||||
});
|
||||
|
||||
test('specialized units', () => {
|
||||
it('should render unit 141 (hardpoints) as string', async () => {
|
||||
const unit = { ...mockUnit, unit_id: 141 };
|
||||
const result = await renderUnit(unit, 8);
|
||||
expect(result).toBe('8');
|
||||
});
|
||||
|
||||
it('should render unit 120 (calibration) with pts suffix', async () => {
|
||||
const unit = { ...mockUnit, unit_id: 120 };
|
||||
const result = await renderUnit(unit, 400);
|
||||
expect(result).toBe('400 pts');
|
||||
});
|
||||
|
||||
it('should render unit 116 (typeID) using type lookup', async () => {
|
||||
const unit = { ...mockUnit, unit_id: 116 };
|
||||
const mockType = {
|
||||
type_id: 12345,
|
||||
name: { en: 'Test Type' },
|
||||
description: { en: 'Test description' },
|
||||
published: true,
|
||||
};
|
||||
|
||||
mock.module('./type', () => ({
|
||||
getType: (type_id: number) => {
|
||||
if (type_id === 12345) return Promise.resolve(mockType);
|
||||
return Promise.resolve(null);
|
||||
},
|
||||
renderTypeEveRefLink: (type: any, lang: string) => {
|
||||
if (type.type_id === 12345 && lang === 'en') return 'Type Link';
|
||||
return null;
|
||||
},
|
||||
}));
|
||||
|
||||
const result = await renderUnit(unit, 12345, 'en');
|
||||
|
||||
expect(result).toBe('Type Link');
|
||||
});
|
||||
|
||||
it('should render unit 116 (typeID) as Unknown when link is null', async () => {
|
||||
const unit = { ...mockUnit, unit_id: 116 };
|
||||
const mockType = {
|
||||
type_id: 12345,
|
||||
name: { en: 'Test Type' },
|
||||
description: { en: 'Test description' },
|
||||
published: true,
|
||||
};
|
||||
|
||||
mock.module('./type', () => ({
|
||||
getType: (type_id: number) => {
|
||||
return Promise.resolve(null);
|
||||
},
|
||||
renderTypeEveRefLink: (type: any, lang: string) => {
|
||||
return null;
|
||||
},
|
||||
}));
|
||||
|
||||
const result = await renderUnit(unit, 12345);
|
||||
|
||||
expect(result).toBe('Unknown');
|
||||
});
|
||||
|
||||
it('should render unit 115 (groupID) using group lookup', async () => {
|
||||
const unit = { ...mockUnit, unit_id: 115 };
|
||||
const mockGroup = {
|
||||
group_id: 67890,
|
||||
name: { en: 'Test Group' },
|
||||
category_id: 1,
|
||||
published: true,
|
||||
anchorable: false,
|
||||
anchored: false,
|
||||
fittable_non_singleton: false,
|
||||
use_base_price: false,
|
||||
};
|
||||
|
||||
mock.module('./group', () => ({
|
||||
getGroup: () => Promise.resolve(mockGroup),
|
||||
renderGroupEveRefLink: (group: any, lang: string) => {
|
||||
if (group.group_id === 67890 && lang === 'fr') return 'Group Link';
|
||||
return null;
|
||||
},
|
||||
}));
|
||||
|
||||
const result = await renderUnit(unit, 67890, 'fr');
|
||||
|
||||
expect(result).toBe('Group Link');
|
||||
});
|
||||
|
||||
it('should render unit 115 (groupID) as Unknown when link is null', async () => {
|
||||
const unit = { ...mockUnit, unit_id: 115 };
|
||||
const mockGroup = {
|
||||
group_id: 67890,
|
||||
name: { en: 'Test Group' },
|
||||
category_id: 1,
|
||||
published: true,
|
||||
anchorable: false,
|
||||
anchored: false,
|
||||
fittable_non_singleton: false,
|
||||
use_base_price: false,
|
||||
};
|
||||
|
||||
mock.module('./group', () => ({
|
||||
getGroup: (group_id: number) => {
|
||||
return Promise.resolve(null);
|
||||
},
|
||||
renderGroupEveRefLink: (group: any, lang: string) => {
|
||||
return null;
|
||||
},
|
||||
}));
|
||||
|
||||
const result = await renderUnit(unit, 67890);
|
||||
|
||||
expect(result).toBe('Unknown');
|
||||
});
|
||||
});
|
||||
|
||||
test('physical units', () => {
|
||||
it('should render unit 10 (m/s)', async () => {
|
||||
const unit = { ...mockUnit, unit_id: 10 };
|
||||
const result = await renderUnit(unit, 150);
|
||||
expect(result).toBe('150 m/s');
|
||||
});
|
||||
|
||||
it('should render unit 11 (m/s²)', async () => {
|
||||
const unit = { ...mockUnit, unit_id: 11 };
|
||||
const result = await renderUnit(unit, 9.8);
|
||||
expect(result).toBe('9.8 m/s²');
|
||||
});
|
||||
|
||||
it('should render unit 9 (m³)', async () => {
|
||||
const unit = { ...mockUnit, unit_id: 9 };
|
||||
const result = await renderUnit(unit, 1000);
|
||||
expect(result).toBe('1000 m³');
|
||||
});
|
||||
|
||||
it('should render unit 8 (m²)', async () => {
|
||||
const unit = { ...mockUnit, unit_id: 8 };
|
||||
const result = await renderUnit(unit, 50);
|
||||
expect(result).toBe('50 m²');
|
||||
});
|
||||
|
||||
it('should render unit 12 (m⁻¹)', async () => {
|
||||
const unit = { ...mockUnit, unit_id: 12 };
|
||||
const result = await renderUnit(unit, 0.1);
|
||||
expect(result).toBe('0.1 m⁻¹');
|
||||
});
|
||||
|
||||
it('should render unit 128 (Mbps)', async () => {
|
||||
const unit = { ...mockUnit, unit_id: 128 };
|
||||
const result = await renderUnit(unit, 100);
|
||||
expect(result).toBe('100 Mbps');
|
||||
});
|
||||
});
|
||||
|
||||
test('default case', () => {
|
||||
it('should render unknown unit with value and display_name', async () => {
|
||||
const unit = { ...mockUnit, unit_id: 999, display_name: 'Custom Unit' };
|
||||
const result = await renderUnit(unit, 42);
|
||||
expect(result).toBe('42 Custom Unit');
|
||||
});
|
||||
|
||||
it('should render unknown unit with empty display_name', async () => {
|
||||
const unit = { ...mockUnit, unit_id: 999, display_name: '' };
|
||||
const result = await renderUnit(unit, 42);
|
||||
expect(result).toBe('42 ');
|
||||
});
|
||||
|
||||
it('should handle undefined display_name', async () => {
|
||||
const unit = { ...mockUnit, unit_id: 999, display_name: undefined as any };
|
||||
const result = await renderUnit(unit, 42);
|
||||
expect(result).toBe('42 ');
|
||||
});
|
||||
});
|
||||
|
||||
test('edge cases', () => {
|
||||
it('should handle zero values', async () => {
|
||||
const unit = { ...mockUnit, unit_id: 10 };
|
||||
const result = await renderUnit(unit, 0);
|
||||
expect(result).toBe('0 m/s');
|
||||
});
|
||||
|
||||
it('should handle negative values', async () => {
|
||||
const unit = { ...mockUnit, unit_id: 120 };
|
||||
const result = await renderUnit(unit, -50);
|
||||
expect(result).toBe('-50 pts');
|
||||
});
|
||||
|
||||
it('should handle very large values', async () => {
|
||||
const unit = { ...mockUnit, unit_id: 9 };
|
||||
const result = await renderUnit(unit, 1e10);
|
||||
expect(result).toBe('10000000000 m³');
|
||||
});
|
||||
|
||||
it('should handle decimal values for inverse percentage', async () => {
|
||||
const unit = { ...mockUnit, unit_id: 108 };
|
||||
const result = await renderUnit(unit, 0.12345);
|
||||
expect(result).toBe('0.88 Test Unit');
|
||||
});
|
||||
|
||||
it('should default to "en" locale when not specified', async () => {
|
||||
const unit = { ...mockUnit, unit_id: 116 };
|
||||
const mockType = {
|
||||
type_id: 12345,
|
||||
name: { en: 'Test Type' },
|
||||
description: { en: 'Test description' },
|
||||
published: true,
|
||||
};
|
||||
|
||||
mock.module('./type', () => ({
|
||||
getType: (type_id: number) => {
|
||||
if (type_id === 12345) return Promise.resolve(mockType);
|
||||
return Promise.resolve(null);
|
||||
},
|
||||
renderTypeEveRefLink: (type: any, lang: string) => {
|
||||
if (type.type_id === 12345 && lang === 'en') return 'Type Link';
|
||||
return null;
|
||||
},
|
||||
}));
|
||||
|
||||
const result = await renderUnit(unit, 12345);
|
||||
expect(result).toBe('Type Link');
|
||||
});
|
||||
});
|
||||
|
||||
test('error handling', () => {
|
||||
it('should handle getType errors gracefully', async () => {
|
||||
const unit = { ...mockUnit, unit_id: 116 };
|
||||
mock.module('./type', () => ({
|
||||
getType: (type_id: number) => {
|
||||
throw new Error('Type not found');
|
||||
},
|
||||
}));
|
||||
|
||||
await expect(renderUnit(unit, 12345)).rejects.toThrow('Type not found');
|
||||
});
|
||||
|
||||
it('should handle getGroup errors gracefully', async () => {
|
||||
const unit = { ...mockUnit, unit_id: 115 };
|
||||
mock.module('./group', () => ({
|
||||
getGroup: (group_id: number) => {
|
||||
throw new Error('Group not found');
|
||||
},
|
||||
}));
|
||||
|
||||
await expect(renderUnit(unit, 67890)).rejects.toThrow('Group not found');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
test('isUnitInversePercentage', () => {
|
||||
const mockUnit: Unit = {
|
||||
unit_id: 0,
|
||||
display_name: 'Test Unit',
|
||||
description: { en: 'Test description' },
|
||||
name: { en: 'Test Name' },
|
||||
};
|
||||
|
||||
it('should return true for unit_id 108', () => {
|
||||
const unit = { ...mockUnit, unit_id: 108 };
|
||||
expect(isUnitInversePercentage(unit)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for unit_id 111', () => {
|
||||
const unit = { ...mockUnit, unit_id: 111 };
|
||||
expect(isUnitInversePercentage(unit)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for other unit_ids', () => {
|
||||
const testCases = [0, 1, 3, 8, 9, 10, 11, 12, 101, 107, 109, 115, 116, 117, 120, 128, 141, 999];
|
||||
|
||||
testCases.forEach((unit_id) => {
|
||||
const unit = { ...mockUnit, unit_id };
|
||||
expect(isUnitInversePercentage(unit)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
it('should use loose equality (==) not strict equality (===)', () => {
|
||||
// Test that the function uses == comparison by verifying it works with string unit_ids
|
||||
const unit108 = { ...mockUnit, unit_id: 108 as any };
|
||||
const unit111 = { ...mockUnit, unit_id: 111 as any };
|
||||
|
||||
expect(isUnitInversePercentage(unit108)).toBe(true);
|
||||
expect(isUnitInversePercentage(unit111)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
66
packages/lib/src/eve/models/unit.ts
Normal file
66
packages/lib/src/eve/models/unit.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
import { convertMillisecondsToTimeString, convertSecondsToTimeString } from '@/eve/utils/markdown.js';
|
||||
import { getGroup, renderGroupEveRefLink } from './group';
|
||||
import type { LocalizedString } from './shared-types';
|
||||
import { getType, renderTypeEveRefLink } from './type';
|
||||
import { dataSets, loadModels } from './loadModels';
|
||||
|
||||
const sizeMap = {
|
||||
1: 'Small',
|
||||
2: 'Medium',
|
||||
3: 'Large',
|
||||
4: 'X-Large',
|
||||
};
|
||||
|
||||
export interface Unit {
|
||||
readonly unit_id: number;
|
||||
readonly display_name: string;
|
||||
readonly description: LocalizedString;
|
||||
readonly name: LocalizedString;
|
||||
}
|
||||
|
||||
export function getUnit(unit_id: number) {
|
||||
if (!dataSets.loaded) loadModels();
|
||||
const unit = dataSets.units[String(unit_id)];
|
||||
if (!unit) throw new Error(`Unit ID ${unit_id} not found in reference data`);
|
||||
return unit;
|
||||
}
|
||||
|
||||
export function renderUnit(unit: Unit, value: number, locale: string = 'en'): string {
|
||||
switch (unit.unit_id) {
|
||||
case 108: // inverse percentage
|
||||
case 111: // Inverse percentage
|
||||
return [(1 - value).toFixed(2), unit.display_name ?? ''].join(' ');
|
||||
case 3: // seconds
|
||||
return `${convertSecondsToTimeString(value)}`;
|
||||
case 101: // milliseconds
|
||||
return `${convertMillisecondsToTimeString(value)}`;
|
||||
case 117: // size class
|
||||
return sizeMap[value] ?? 'Unknown';
|
||||
case 141: // hardpoints
|
||||
return value + '';
|
||||
case 120: // calibration
|
||||
return value + ' pts';
|
||||
case 116: // typeID
|
||||
return getType(value).name[locale] ?? 'Unknown';
|
||||
case 10: // m/s
|
||||
return `${value} m/s`;
|
||||
case 11: // meters per second squared
|
||||
return `${value} m/s²`;
|
||||
case 9: // cubic meters
|
||||
return `${value} m³`;
|
||||
case 8: // square meters
|
||||
return `${value} m²`;
|
||||
case 12: // reciprocal meters
|
||||
return `${value} m⁻¹`;
|
||||
case 128: // megabits per second
|
||||
return `${value} Mbps`;
|
||||
case 115: // groupID
|
||||
return renderGroupEveRefLink(getGroup(value), locale) ?? 'Unknown';
|
||||
default:
|
||||
return [value, unit.display_name ?? ''].join(' ');
|
||||
}
|
||||
}
|
||||
|
||||
export function isUnitInversePercentage(unit: Unit) {
|
||||
return unit.unit_id == 108 || unit.unit_id == 111;
|
||||
}
|
||||
Reference in New Issue
Block a user