merged libraries into one

This commit is contained in:
JB
2026-01-01 22:07:16 -05:00
parent a6642ac829
commit 6e31d40d49
185 changed files with 383 additions and 4013 deletions

View 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,
}

View 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,
})),
);
}

View 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;
}

View 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);
}

View 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)})`;
}

View 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}`;
}

View 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';

View 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 };

View 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)))));
}

View 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;
}

View 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;
}

View 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);
}

View 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;
}

View 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;
}

View 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
};
}

View 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;
}

View 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);
});
});
});

View 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}`;
case 8: // square meters
return `${value}`;
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;
}