@eveshipfit/fitting 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +19 -0
- package/README.md +68 -0
- package/dist/index.d.mts +155 -0
- package/dist/index.mjs +571 -0
- package/package.json +41 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Copyright (c) 2023 EVEShipFit Team
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
in the Software without restriction, including without limitation the rights
|
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
|
11
|
+
copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
19
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# @eveshipfit/fitting
|
|
2
|
+
|
|
3
|
+
EVE Online ship fits that recalculate themselves, on top of
|
|
4
|
+
[`@eveshipfit/dogma-engine`](https://www.npmjs.com/package/@eveshipfit/dogma-engine).
|
|
5
|
+
|
|
6
|
+
Part of [EVEShip.fit](https://eveship.fit).
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
npm install @eveshipfit/fitting @eveshipfit/dogma-engine @eveshipfit/sde-loader @eveshipfit/sde
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Usage
|
|
15
|
+
|
|
16
|
+
The examples use Vite's `?url` imports to get the URL of a data file.
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
import wasmUrl from "@eveshipfit/dogma-engine/esf_dogma_engine_bg.wasm?url";
|
|
20
|
+
import { createEngine } from "@eveshipfit/fitting";
|
|
21
|
+
import sdeUrl from "@eveshipfit/sde/dist/sde.dat?url";
|
|
22
|
+
import { loadSde } from "@eveshipfit/sde-loader";
|
|
23
|
+
|
|
24
|
+
const engine = await createEngine(await loadSde({ url: sdeUrl }), { wasm: wasmUrl });
|
|
25
|
+
|
|
26
|
+
const fit = engine.createFit({ ship: 587 }); // a Rifter
|
|
27
|
+
fit.fit(engine.sde.typeByName("200mm AutoCannon II")!.id); // first free high slot
|
|
28
|
+
fit.fit(engine.sde.typeByName("EMP S")!.id); // loaded into every gun that takes it
|
|
29
|
+
|
|
30
|
+
const { stats } = fit.getSnapshot();
|
|
31
|
+
stats.ship.get("cpuLoad");
|
|
32
|
+
stats.slots.high; // { used: 1, total: 3 }
|
|
33
|
+
stats.violations; // the fitting rules the fit breaks
|
|
34
|
+
|
|
35
|
+
fit.undo();
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
The engine can hold only one SDE per page; `createEngine` with another one throws.
|
|
39
|
+
|
|
40
|
+
A fit is flown by a character with every skill at V, unless you pass another to `createFit` or `setCharacter`.
|
|
41
|
+
|
|
42
|
+
### React
|
|
43
|
+
|
|
44
|
+
`subscribe` and `getSnapshot` are bound, so a store goes straight into `useSyncExternalStore`:
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
const { fit, stats } = useSyncExternalStore(store.subscribe, store.getSnapshot);
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Every change gives a new snapshot; old ones never change.
|
|
51
|
+
|
|
52
|
+
### Previews
|
|
53
|
+
|
|
54
|
+
`preview` shows what an edit would do, without doing it:
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
const { before, after } = fit.preview((draft) => draft.fit(typeId));
|
|
58
|
+
after.stats.ship.get("cpuLoad")! - before.stats.ship.get("cpuLoad")!;
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Rules
|
|
62
|
+
|
|
63
|
+
`placementOf`, `canFit`, `acceptsCharge` and `chargesFor` say where a type goes, whether a module fits a hull, and
|
|
64
|
+
which charges go in a module; for example to filter a market browser.
|
|
65
|
+
|
|
66
|
+
## License
|
|
67
|
+
|
|
68
|
+
MIT
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { Calculation, Character, Fit, FitItem, InitInput, ItemResult, Rule, Slot, Slot as Slot$1, State, Violation } from "@eveshipfit/dogma-engine";
|
|
2
|
+
import { Sde, SdeType } from "@eveshipfit/sde-loader";
|
|
3
|
+
//#region src/types.d.ts
|
|
4
|
+
type SlotType = Slot$1["type"];
|
|
5
|
+
/** The racks a ship has a fixed number of numbered slots in. */
|
|
6
|
+
type Rack = "high" | "medium" | "low" | "rig" | "subsystem" | "service";
|
|
7
|
+
/** An item of a fit, as its index into `fit.items`. */
|
|
8
|
+
type ItemRef = number;
|
|
9
|
+
//#endregion
|
|
10
|
+
//#region src/character.d.ts
|
|
11
|
+
/** A character with every published skill at `level`. */
|
|
12
|
+
export declare function allSkills(sde: Sde, level: number): Character;
|
|
13
|
+
//#endregion
|
|
14
|
+
//#region src/stats.d.ts
|
|
15
|
+
interface Usage {
|
|
16
|
+
readonly used: number;
|
|
17
|
+
readonly total: number;
|
|
18
|
+
}
|
|
19
|
+
/** Calculated attributes of one thing in the fit, by name or by ID. */
|
|
20
|
+
export declare class Attributes {
|
|
21
|
+
#private;
|
|
22
|
+
constructor(sde: Sde, result: ItemResult);
|
|
23
|
+
/** The value after every effect applied. */
|
|
24
|
+
get(attribute: string | number): number | undefined;
|
|
25
|
+
/** The value before any effect applied. */
|
|
26
|
+
base(attribute: string | number): number | undefined;
|
|
27
|
+
}
|
|
28
|
+
interface ItemStats {
|
|
29
|
+
/** The state reached, which can be lower than the one asked for. */
|
|
30
|
+
readonly state: State;
|
|
31
|
+
readonly maxState: State;
|
|
32
|
+
readonly attributes: Attributes;
|
|
33
|
+
readonly charge: Attributes | undefined;
|
|
34
|
+
}
|
|
35
|
+
/** A calculation of a fit, with named access and the totals a fitting window shows. */
|
|
36
|
+
export declare class Stats {
|
|
37
|
+
readonly calculation: Calculation;
|
|
38
|
+
readonly ship: Attributes;
|
|
39
|
+
readonly character: Attributes;
|
|
40
|
+
readonly mode: Attributes | undefined;
|
|
41
|
+
/** Index-parallel to `fit.items`. */
|
|
42
|
+
readonly items: readonly ItemStats[];
|
|
43
|
+
/** Every fitting rule the fit breaks; empty when it can be flown as is. */
|
|
44
|
+
readonly violations: readonly Violation[];
|
|
45
|
+
readonly slots: Readonly<Record<Rack, Usage>>;
|
|
46
|
+
readonly hardpoints: {
|
|
47
|
+
readonly turret: Usage;
|
|
48
|
+
readonly launcher: Usage;
|
|
49
|
+
};
|
|
50
|
+
/** In m³. */
|
|
51
|
+
readonly cargo: Usage;
|
|
52
|
+
constructor(sde: Sde, fit: Fit, calculation: Calculation);
|
|
53
|
+
}
|
|
54
|
+
//#endregion
|
|
55
|
+
//#region src/edits.d.ts
|
|
56
|
+
export declare function emptyFit(shipTypeId: number): Fit;
|
|
57
|
+
//#endregion
|
|
58
|
+
//#region src/store.d.ts
|
|
59
|
+
interface Snapshot {
|
|
60
|
+
/** Without `character`; who flies the fit is set on the store. */
|
|
61
|
+
readonly fit: Fit;
|
|
62
|
+
readonly stats: Stats;
|
|
63
|
+
}
|
|
64
|
+
interface Preview {
|
|
65
|
+
readonly before: Snapshot;
|
|
66
|
+
readonly after: Snapshot;
|
|
67
|
+
}
|
|
68
|
+
interface Calculator {
|
|
69
|
+
readonly sde: Sde;
|
|
70
|
+
calculate(fit: Fit, character: Character): Stats;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* A fit that recalculates itself on every change. `subscribe` and
|
|
74
|
+
* `getSnapshot` are bound, so they can go straight into `useSyncExternalStore`.
|
|
75
|
+
*/
|
|
76
|
+
export declare class FitStore {
|
|
77
|
+
#private;
|
|
78
|
+
constructor(calculator: Calculator, fit: Fit, character: Character);
|
|
79
|
+
getSnapshot: () => Snapshot;
|
|
80
|
+
subscribe: (listener: () => void) => (() => void);
|
|
81
|
+
get character(): Character;
|
|
82
|
+
get canUndo(): boolean;
|
|
83
|
+
get canRedo(): boolean;
|
|
84
|
+
/** Picks the rack and the first free slot, unless `slot` says where. */
|
|
85
|
+
fit(typeId: number, slot?: Slot): ItemRef | undefined;
|
|
86
|
+
remove(ref: ItemRef): void;
|
|
87
|
+
setState(ref: ItemRef, state: State): void;
|
|
88
|
+
setCharge(ref: ItemRef, chargeTypeId: number | undefined): void;
|
|
89
|
+
setQuantity(ref: ItemRef, quantity: number): void;
|
|
90
|
+
setName(name: string): void;
|
|
91
|
+
/** Swap in another fit entirely, like an import; undo brings the old one back. */
|
|
92
|
+
replace(fit: Fit): void;
|
|
93
|
+
/** Who flies the fit is not an edit of it, so this does not go in the history. */
|
|
94
|
+
setCharacter(character: Character): void;
|
|
95
|
+
undo(): void;
|
|
96
|
+
redo(): void;
|
|
97
|
+
/** What `edit` would do, without doing it. */
|
|
98
|
+
preview(edit: (fit: FitStore) => void): Preview;
|
|
99
|
+
}
|
|
100
|
+
//#endregion
|
|
101
|
+
//#region src/engine.d.ts
|
|
102
|
+
interface EngineOptions {
|
|
103
|
+
/** Where to load the WASM from; by default, next to the engine's JavaScript. */
|
|
104
|
+
wasm?: InitInput;
|
|
105
|
+
}
|
|
106
|
+
export declare function createEngine(sde: Sde, options?: EngineOptions): Promise<Engine>;
|
|
107
|
+
export declare class Engine implements Calculator {
|
|
108
|
+
#private;
|
|
109
|
+
readonly sde: Sde;
|
|
110
|
+
constructor(sde: Sde);
|
|
111
|
+
/** Every published skill at level V; what a fit is flown by until told otherwise. */
|
|
112
|
+
get defaultCharacter(): Character;
|
|
113
|
+
createFit(fit: Fit | {
|
|
114
|
+
ship: number;
|
|
115
|
+
}, character?: Character): FitStore;
|
|
116
|
+
calculate(fit: Fit, character?: Character): Stats;
|
|
117
|
+
}
|
|
118
|
+
//#endregion
|
|
119
|
+
//#region src/rules/attributes.d.ts
|
|
120
|
+
/** A type's value for an attribute as the SDE has it, before any effect applies. */
|
|
121
|
+
export declare function baseValue(sde: Sde, type: SdeType, name: string): number | undefined;
|
|
122
|
+
//#endregion
|
|
123
|
+
//#region src/rules/filters.d.ts
|
|
124
|
+
/** Whether `module` can load `charge`: the right group, the right size, and room for at least one. */
|
|
125
|
+
export declare function acceptsCharge(sde: Sde, module: SdeType, charge: SdeType): boolean;
|
|
126
|
+
/** Every published charge `module` can load, sorted by name. */
|
|
127
|
+
export declare function chargesFor(sde: Sde, module: SdeType): SdeType[];
|
|
128
|
+
/**
|
|
129
|
+
* Whether `type` may go on `ship` at all: hull restrictions, rig size and
|
|
130
|
+
* subsystems of the right hull. Whether there is room left is the
|
|
131
|
+
* calculation's job, as that depends on the rest of the fit.
|
|
132
|
+
*/
|
|
133
|
+
export declare function canFit(sde: Sde, type: SdeType, ship: SdeType): boolean;
|
|
134
|
+
//#endregion
|
|
135
|
+
//#region src/rules/placement.d.ts
|
|
136
|
+
type Placement = {
|
|
137
|
+
type: Exclude<Rack, "subsystem">;
|
|
138
|
+
} |
|
|
139
|
+
/** Implants, boosters and subsystems each have one slot they go in. */
|
|
140
|
+
{
|
|
141
|
+
type: "implant" | "booster" | "subsystem";
|
|
142
|
+
index: number;
|
|
143
|
+
} | {
|
|
144
|
+
type: "drone_bay" | "fighter_bay" | "cargo";
|
|
145
|
+
} |
|
|
146
|
+
/** Loaded into a module; `cargo` when no module takes it. */
|
|
147
|
+
{
|
|
148
|
+
type: "charge";
|
|
149
|
+
};
|
|
150
|
+
/** Where an item of this type goes when fitted; `undefined` for what cannot be part of a fit, like a ship. */
|
|
151
|
+
export declare function placementOf(sde: Sde, type: SdeType): Placement | undefined;
|
|
152
|
+
/** The lowest slot index in `rack` nothing is fitted in, if below `available`. */
|
|
153
|
+
export declare function firstFreeIndex(fit: Fit, rack: SlotType, available: number): number | undefined;
|
|
154
|
+
//#endregion
|
|
155
|
+
export type { Calculation, Calculator, Character, EngineOptions, Fit, FitItem, ItemRef, ItemResult, ItemStats, Placement, Preview, Rack, Rule, Slot, SlotType, Snapshot, State, Usage, Violation };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
import init, { calculate, load_sde } from "@eveshipfit/dogma-engine";
|
|
2
|
+
//#region src/ids.ts
|
|
3
|
+
/** Well-known IDs from the SDE; CCP does not renumber these. */
|
|
4
|
+
const Category = {
|
|
5
|
+
Ship: 6,
|
|
6
|
+
Module: 7,
|
|
7
|
+
Charge: 8,
|
|
8
|
+
Skill: 16,
|
|
9
|
+
Drone: 18,
|
|
10
|
+
Implant: 20,
|
|
11
|
+
Subsystem: 32,
|
|
12
|
+
Fighter: 87
|
|
13
|
+
};
|
|
14
|
+
const Effect = {
|
|
15
|
+
LowPower: 11,
|
|
16
|
+
HighPower: 12,
|
|
17
|
+
MediumPower: 13,
|
|
18
|
+
LauncherFitted: 40,
|
|
19
|
+
TurretFitted: 42,
|
|
20
|
+
RigSlot: 2663,
|
|
21
|
+
Subsystem: 3772,
|
|
22
|
+
ServiceSlot: 6306
|
|
23
|
+
};
|
|
24
|
+
//#endregion
|
|
25
|
+
//#region src/character.ts
|
|
26
|
+
/** A character with every published skill at `level`. */
|
|
27
|
+
function allSkills(sde, level) {
|
|
28
|
+
const skills = {};
|
|
29
|
+
for (const type of sde.types()) if (type.categoryId === Category.Skill && type.published) skills[type.id] = level;
|
|
30
|
+
return { skills };
|
|
31
|
+
}
|
|
32
|
+
//#endregion
|
|
33
|
+
//#region src/rules/attributes.ts
|
|
34
|
+
/** A type's value for an attribute as the SDE has it, before any effect applies. */
|
|
35
|
+
function baseValue(sde, type, name) {
|
|
36
|
+
const id = sde.attributeId(name);
|
|
37
|
+
return id === void 0 ? void 0 : type.attributes.get(id);
|
|
38
|
+
}
|
|
39
|
+
/** Every value of a numbered attribute family, like `chargeGroup1` to `chargeGroup5`. */
|
|
40
|
+
function baseValues(sde, type, names) {
|
|
41
|
+
const values = [];
|
|
42
|
+
for (const name of names) {
|
|
43
|
+
const value = baseValue(sde, type, name);
|
|
44
|
+
if (value !== void 0) values.push(value);
|
|
45
|
+
}
|
|
46
|
+
return values;
|
|
47
|
+
}
|
|
48
|
+
//#endregion
|
|
49
|
+
//#region src/rules/filters.ts
|
|
50
|
+
const chargeGroups = [
|
|
51
|
+
"chargeGroup1",
|
|
52
|
+
"chargeGroup2",
|
|
53
|
+
"chargeGroup3",
|
|
54
|
+
"chargeGroup4",
|
|
55
|
+
"chargeGroup5"
|
|
56
|
+
];
|
|
57
|
+
const shipGroups = Array.from({ length: 20 }, (_, i) => `canFitShipGroup${String(i + 1).padStart(2, "0")}`);
|
|
58
|
+
const shipTypes = Array.from({ length: 12 }, (_, i) => `canFitShipType${i + 1}`);
|
|
59
|
+
/** Whether `module` can load `charge`: the right group, the right size, and room for at least one. */
|
|
60
|
+
function acceptsCharge(sde, module, charge) {
|
|
61
|
+
if (charge.categoryId !== Category.Charge) return false;
|
|
62
|
+
if (!baseValues(sde, module, chargeGroups).includes(charge.groupId)) return false;
|
|
63
|
+
const size = baseValue(sde, module, "chargeSize");
|
|
64
|
+
if (size !== void 0 && baseValue(sde, charge, "chargeSize") !== size) return false;
|
|
65
|
+
return (charge.volume ?? 0) <= (module.capacity ?? 0);
|
|
66
|
+
}
|
|
67
|
+
/** Every published charge `module` can load, sorted by name. */
|
|
68
|
+
function chargesFor(sde, module) {
|
|
69
|
+
const groups = new Set(baseValues(sde, module, chargeGroups));
|
|
70
|
+
if (groups.size === 0) return [];
|
|
71
|
+
const charges = [];
|
|
72
|
+
for (const type of sde.types()) if (type.published && groups.has(type.groupId) && acceptsCharge(sde, module, type)) charges.push(type);
|
|
73
|
+
return charges.toSorted((a, b) => a.name.localeCompare(b.name));
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Whether `type` may go on `ship` at all: hull restrictions, rig size and
|
|
77
|
+
* subsystems of the right hull. Whether there is room left is the
|
|
78
|
+
* calculation's job, as that depends on the rest of the fit.
|
|
79
|
+
*/
|
|
80
|
+
function canFit(sde, type, ship) {
|
|
81
|
+
const groups = baseValues(sde, type, shipGroups);
|
|
82
|
+
const types = baseValues(sde, type, shipTypes);
|
|
83
|
+
if ((groups.length > 0 || types.length > 0) && !groups.includes(ship.groupId) && !types.includes(ship.id)) return false;
|
|
84
|
+
const rigSize = baseValue(sde, type, "rigSize");
|
|
85
|
+
if (rigSize !== void 0 && rigSize !== baseValue(sde, ship, "rigSize")) return false;
|
|
86
|
+
const hull = baseValue(sde, type, "fitsToShipType");
|
|
87
|
+
if (hull !== void 0 && hull !== ship.id) return false;
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
//#endregion
|
|
91
|
+
//#region src/rules/placement.ts
|
|
92
|
+
const rackEffects = [
|
|
93
|
+
[Effect.HighPower, "high"],
|
|
94
|
+
[Effect.MediumPower, "medium"],
|
|
95
|
+
[Effect.LowPower, "low"],
|
|
96
|
+
[Effect.RigSlot, "rig"],
|
|
97
|
+
[Effect.ServiceSlot, "service"]
|
|
98
|
+
];
|
|
99
|
+
/** Where an item of this type goes when fitted; `undefined` for what cannot be part of a fit, like a ship. */
|
|
100
|
+
function placementOf(sde, type) {
|
|
101
|
+
switch (type.categoryId) {
|
|
102
|
+
case Category.Ship: return;
|
|
103
|
+
case Category.Charge: return { type: "charge" };
|
|
104
|
+
case Category.Drone: return { type: "drone_bay" };
|
|
105
|
+
case Category.Fighter: return { type: "fighter_bay" };
|
|
106
|
+
case Category.Subsystem: {
|
|
107
|
+
const flag = baseValue(sde, type, "subSystemSlot");
|
|
108
|
+
return flag === void 0 ? void 0 : {
|
|
109
|
+
type: "subsystem",
|
|
110
|
+
index: flag - 125
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
case Category.Implant: {
|
|
114
|
+
const implant = baseValue(sde, type, "implantness");
|
|
115
|
+
if (implant !== void 0) return {
|
|
116
|
+
type: "implant",
|
|
117
|
+
index: implant
|
|
118
|
+
};
|
|
119
|
+
const booster = baseValue(sde, type, "boosterness");
|
|
120
|
+
if (booster !== void 0) return {
|
|
121
|
+
type: "booster",
|
|
122
|
+
index: booster
|
|
123
|
+
};
|
|
124
|
+
return { type: "cargo" };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
for (const [effect, rack] of rackEffects) if (type.effectIds.has(effect)) return { type: rack };
|
|
128
|
+
return { type: "cargo" };
|
|
129
|
+
}
|
|
130
|
+
/** The lowest slot index in `rack` nothing is fitted in, if below `available`. */
|
|
131
|
+
function firstFreeIndex(fit, rack, available) {
|
|
132
|
+
const taken = /* @__PURE__ */ new Set();
|
|
133
|
+
for (const item of fit.items) if (item.slot.type === rack && "index" in item.slot) taken.add(item.slot.index);
|
|
134
|
+
for (let index = 0; index < available; index++) if (!taken.has(index)) return index;
|
|
135
|
+
}
|
|
136
|
+
//#endregion
|
|
137
|
+
//#region src/edits.ts
|
|
138
|
+
function emptyFit(shipTypeId) {
|
|
139
|
+
return {
|
|
140
|
+
ship: { type_id: shipTypeId },
|
|
141
|
+
items: []
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Put a type where EVE would: modules in the first free slot of their rack,
|
|
146
|
+
* charges in every module that takes them, drones and fighters in their bay,
|
|
147
|
+
* anything else in the cargo. With `slot`, only that slot is tried, replacing
|
|
148
|
+
* what is there.
|
|
149
|
+
*/
|
|
150
|
+
function fitType(sde, fit, stats, typeId, slot) {
|
|
151
|
+
const nowhere = {
|
|
152
|
+
fit,
|
|
153
|
+
ref: void 0
|
|
154
|
+
};
|
|
155
|
+
const type = sde.type(typeId);
|
|
156
|
+
const placement = type && placementOf(sde, type);
|
|
157
|
+
if (type === void 0 || placement === void 0) return nowhere;
|
|
158
|
+
if (slot !== void 0 && placement.type !== "charge" && slot.type !== placement.type) return nowhere;
|
|
159
|
+
switch (placement.type) {
|
|
160
|
+
case "high":
|
|
161
|
+
case "medium":
|
|
162
|
+
case "low":
|
|
163
|
+
case "rig":
|
|
164
|
+
case "service": {
|
|
165
|
+
const index = slot === void 0 ? firstFreeIndex(fit, placement.type, stats.slots[placement.type].total) : slotIndex(slot);
|
|
166
|
+
if (index === void 0) return nowhere;
|
|
167
|
+
return putInSlot(fit, {
|
|
168
|
+
type_id: typeId,
|
|
169
|
+
slot: {
|
|
170
|
+
type: placement.type,
|
|
171
|
+
index
|
|
172
|
+
},
|
|
173
|
+
state: "active"
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
case "subsystem":
|
|
177
|
+
case "implant":
|
|
178
|
+
case "booster":
|
|
179
|
+
if (slot !== void 0 && !sameSlot(slot, placement)) return nowhere;
|
|
180
|
+
return putInSlot(fit, {
|
|
181
|
+
type_id: typeId,
|
|
182
|
+
slot: placement,
|
|
183
|
+
state: "active"
|
|
184
|
+
});
|
|
185
|
+
case "charge": {
|
|
186
|
+
const modules = fit.items.map((item, ref) => ({
|
|
187
|
+
item,
|
|
188
|
+
ref
|
|
189
|
+
})).filter(({ item }) => slot === void 0 || sameSlot(item.slot, slot)).filter(({ item }) => {
|
|
190
|
+
const module = sde.type(item.type_id);
|
|
191
|
+
return module !== void 0 && acceptsCharge(sde, module, type);
|
|
192
|
+
});
|
|
193
|
+
if (modules.length === 0) {
|
|
194
|
+
if (slot !== void 0) return nowhere;
|
|
195
|
+
return addToStack(fit, {
|
|
196
|
+
type_id: typeId,
|
|
197
|
+
slot: { type: "cargo" },
|
|
198
|
+
quantity: 1,
|
|
199
|
+
state: "offline"
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
let loaded = fit;
|
|
203
|
+
for (const { ref } of modules) loaded = setCharge(loaded, ref, typeId);
|
|
204
|
+
return {
|
|
205
|
+
fit: loaded,
|
|
206
|
+
ref: modules[0].ref
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
case "drone_bay": return addToStack(fit, {
|
|
210
|
+
type_id: typeId,
|
|
211
|
+
slot: { type: "drone_bay" },
|
|
212
|
+
quantity: 1,
|
|
213
|
+
state: "active"
|
|
214
|
+
});
|
|
215
|
+
case "fighter_bay": return append(fit, {
|
|
216
|
+
type_id: typeId,
|
|
217
|
+
slot: { type: "fighter_bay" },
|
|
218
|
+
quantity: baseValue(sde, type, "fighterSquadronMaxSize") ?? 1,
|
|
219
|
+
state: "offline"
|
|
220
|
+
});
|
|
221
|
+
case "cargo": return addToStack(fit, {
|
|
222
|
+
type_id: typeId,
|
|
223
|
+
slot: { type: "cargo" },
|
|
224
|
+
quantity: 1,
|
|
225
|
+
state: "offline"
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
function remove(fit, ref) {
|
|
230
|
+
if (fit.items[ref] === void 0) return fit;
|
|
231
|
+
return {
|
|
232
|
+
...fit,
|
|
233
|
+
items: fit.items.toSpliced(ref, 1)
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
function setState(fit, ref, state) {
|
|
237
|
+
return update(fit, ref, (item) => item.state === state ? item : {
|
|
238
|
+
...item,
|
|
239
|
+
state
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
function setCharge(fit, ref, chargeTypeId) {
|
|
243
|
+
return update(fit, ref, (item) => {
|
|
244
|
+
if (item.charge?.type_id === chargeTypeId) return item;
|
|
245
|
+
if (chargeTypeId !== void 0) return {
|
|
246
|
+
...item,
|
|
247
|
+
charge: { type_id: chargeTypeId }
|
|
248
|
+
};
|
|
249
|
+
const { charge: _, ...unloaded } = item;
|
|
250
|
+
return unloaded;
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
/** A quantity of zero removes the item. */
|
|
254
|
+
function setQuantity(fit, ref, quantity) {
|
|
255
|
+
if (quantity <= 0) return remove(fit, ref);
|
|
256
|
+
return update(fit, ref, (item) => (item.quantity ?? 1) === quantity ? item : {
|
|
257
|
+
...item,
|
|
258
|
+
quantity
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
function setName(fit, name) {
|
|
262
|
+
return fit.name === name ? fit : {
|
|
263
|
+
...fit,
|
|
264
|
+
name
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
function update(fit, ref, change) {
|
|
268
|
+
const item = fit.items[ref];
|
|
269
|
+
if (item === void 0) return fit;
|
|
270
|
+
const changed = change(item);
|
|
271
|
+
return changed === item ? fit : {
|
|
272
|
+
...fit,
|
|
273
|
+
items: fit.items.with(ref, changed)
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
function append(fit, item) {
|
|
277
|
+
return {
|
|
278
|
+
fit: {
|
|
279
|
+
...fit,
|
|
280
|
+
items: [...fit.items, item]
|
|
281
|
+
},
|
|
282
|
+
ref: fit.items.length
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
function putInSlot(fit, item) {
|
|
286
|
+
const ref = fit.items.findIndex((existing) => sameSlot(existing.slot, item.slot));
|
|
287
|
+
if (ref === -1) return append(fit, item);
|
|
288
|
+
return {
|
|
289
|
+
fit: {
|
|
290
|
+
...fit,
|
|
291
|
+
items: fit.items.with(ref, item)
|
|
292
|
+
},
|
|
293
|
+
ref
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
/** Drones and cargo of the same type and state share a stack. */
|
|
297
|
+
function addToStack(fit, item) {
|
|
298
|
+
const ref = fit.items.findIndex((existing) => existing.slot.type === item.slot.type && existing.type_id === item.type_id && existing.state === item.state);
|
|
299
|
+
if (ref === -1) return append(fit, item);
|
|
300
|
+
return {
|
|
301
|
+
fit: setQuantity(fit, ref, (fit.items[ref].quantity ?? 1) + 1),
|
|
302
|
+
ref
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
function slotIndex(slot) {
|
|
306
|
+
return "index" in slot ? slot.index : void 0;
|
|
307
|
+
}
|
|
308
|
+
function sameSlot(a, b) {
|
|
309
|
+
return a.type === b.type && slotIndex(a) === slotIndex(b);
|
|
310
|
+
}
|
|
311
|
+
//#endregion
|
|
312
|
+
//#region src/stats.ts
|
|
313
|
+
/** Calculated attributes of one thing in the fit, by name or by ID. */
|
|
314
|
+
var Attributes = class {
|
|
315
|
+
#sde;
|
|
316
|
+
#result;
|
|
317
|
+
constructor(sde, result) {
|
|
318
|
+
this.#sde = sde;
|
|
319
|
+
this.#result = result;
|
|
320
|
+
}
|
|
321
|
+
/** The value after every effect applied. */
|
|
322
|
+
get(attribute) {
|
|
323
|
+
return this.#find(attribute)?.value;
|
|
324
|
+
}
|
|
325
|
+
/** The value before any effect applied. */
|
|
326
|
+
base(attribute) {
|
|
327
|
+
return this.#find(attribute)?.base;
|
|
328
|
+
}
|
|
329
|
+
#find(attribute) {
|
|
330
|
+
const id = typeof attribute === "number" ? attribute : this.#sde.attributeId(attribute);
|
|
331
|
+
return id === void 0 ? void 0 : this.#result.attributes.get(id);
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
const rackAttributes = {
|
|
335
|
+
high: "hiSlots",
|
|
336
|
+
medium: "medSlots",
|
|
337
|
+
low: "lowSlots",
|
|
338
|
+
rig: "rigSlots",
|
|
339
|
+
subsystem: "maxSubSystems",
|
|
340
|
+
service: "serviceSlots"
|
|
341
|
+
};
|
|
342
|
+
/** A calculation of a fit, with named access and the totals a fitting window shows. */
|
|
343
|
+
var Stats = class {
|
|
344
|
+
calculation;
|
|
345
|
+
ship;
|
|
346
|
+
character;
|
|
347
|
+
mode;
|
|
348
|
+
/** Index-parallel to `fit.items`. */
|
|
349
|
+
items;
|
|
350
|
+
/** Every fitting rule the fit breaks; empty when it can be flown as is. */
|
|
351
|
+
violations;
|
|
352
|
+
slots;
|
|
353
|
+
hardpoints;
|
|
354
|
+
/** In m³. */
|
|
355
|
+
cargo;
|
|
356
|
+
constructor(sde, fit, calculation) {
|
|
357
|
+
this.calculation = calculation;
|
|
358
|
+
this.ship = new Attributes(sde, calculation.ship);
|
|
359
|
+
this.character = new Attributes(sde, calculation.character);
|
|
360
|
+
this.mode = calculation.mode && new Attributes(sde, calculation.mode);
|
|
361
|
+
this.items = calculation.items.map((item) => ({
|
|
362
|
+
state: item.state,
|
|
363
|
+
maxState: item.max_state,
|
|
364
|
+
attributes: new Attributes(sde, item),
|
|
365
|
+
charge: item.charge && new Attributes(sde, item.charge)
|
|
366
|
+
}));
|
|
367
|
+
this.violations = calculation.violations ?? [];
|
|
368
|
+
const used = {
|
|
369
|
+
high: 0,
|
|
370
|
+
medium: 0,
|
|
371
|
+
low: 0,
|
|
372
|
+
rig: 0,
|
|
373
|
+
subsystem: 0,
|
|
374
|
+
service: 0,
|
|
375
|
+
turret: 0,
|
|
376
|
+
launcher: 0,
|
|
377
|
+
cargo: 0
|
|
378
|
+
};
|
|
379
|
+
for (const item of fit.items) {
|
|
380
|
+
const type = sde.type(item.type_id);
|
|
381
|
+
switch (item.slot.type) {
|
|
382
|
+
case "high":
|
|
383
|
+
used.high += 1;
|
|
384
|
+
if (type?.effectIds.has(Effect.TurretFitted)) used.turret += 1;
|
|
385
|
+
if (type?.effectIds.has(Effect.LauncherFitted)) used.launcher += 1;
|
|
386
|
+
break;
|
|
387
|
+
case "medium":
|
|
388
|
+
case "low":
|
|
389
|
+
case "rig":
|
|
390
|
+
case "subsystem":
|
|
391
|
+
case "service":
|
|
392
|
+
used[item.slot.type] += 1;
|
|
393
|
+
break;
|
|
394
|
+
case "cargo": used.cargo += (type?.volume ?? 0) * (item.quantity ?? 1);
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
const total = (attribute) => this.ship.get(attribute) ?? 0;
|
|
398
|
+
const usage = (rack) => ({
|
|
399
|
+
used: used[rack],
|
|
400
|
+
total: total(rackAttributes[rack])
|
|
401
|
+
});
|
|
402
|
+
this.slots = {
|
|
403
|
+
high: usage("high"),
|
|
404
|
+
medium: usage("medium"),
|
|
405
|
+
low: usage("low"),
|
|
406
|
+
rig: usage("rig"),
|
|
407
|
+
subsystem: usage("subsystem"),
|
|
408
|
+
service: usage("service")
|
|
409
|
+
};
|
|
410
|
+
this.hardpoints = {
|
|
411
|
+
turret: {
|
|
412
|
+
used: used.turret,
|
|
413
|
+
total: total("turretSlotsLeft")
|
|
414
|
+
},
|
|
415
|
+
launcher: {
|
|
416
|
+
used: used.launcher,
|
|
417
|
+
total: total("launcherSlotsLeft")
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
this.cargo = {
|
|
421
|
+
used: used.cargo,
|
|
422
|
+
total: total("capacity")
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
};
|
|
426
|
+
//#endregion
|
|
427
|
+
//#region src/store.ts
|
|
428
|
+
const HISTORY_LIMIT = 100;
|
|
429
|
+
/**
|
|
430
|
+
* A fit that recalculates itself on every change. `subscribe` and
|
|
431
|
+
* `getSnapshot` are bound, so they can go straight into `useSyncExternalStore`.
|
|
432
|
+
*/
|
|
433
|
+
var FitStore = class FitStore {
|
|
434
|
+
#calculator;
|
|
435
|
+
#character;
|
|
436
|
+
#snapshot;
|
|
437
|
+
#undo = [];
|
|
438
|
+
#redo = [];
|
|
439
|
+
#listeners = /* @__PURE__ */ new Set();
|
|
440
|
+
constructor(calculator, fit, character) {
|
|
441
|
+
this.#calculator = calculator;
|
|
442
|
+
this.#character = character;
|
|
443
|
+
this.#snapshot = this.#calculate(withoutCharacter(fit));
|
|
444
|
+
}
|
|
445
|
+
getSnapshot = () => this.#snapshot;
|
|
446
|
+
subscribe = (listener) => {
|
|
447
|
+
this.#listeners.add(listener);
|
|
448
|
+
return () => this.#listeners.delete(listener);
|
|
449
|
+
};
|
|
450
|
+
get character() {
|
|
451
|
+
return this.#character;
|
|
452
|
+
}
|
|
453
|
+
get canUndo() {
|
|
454
|
+
return this.#undo.length > 0;
|
|
455
|
+
}
|
|
456
|
+
get canRedo() {
|
|
457
|
+
return this.#redo.length > 0;
|
|
458
|
+
}
|
|
459
|
+
/** Picks the rack and the first free slot, unless `slot` says where. */
|
|
460
|
+
fit(typeId, slot) {
|
|
461
|
+
const { fit, ref } = fitType(this.#calculator.sde, this.#snapshot.fit, this.#snapshot.stats, typeId, slot);
|
|
462
|
+
this.#commit(fit);
|
|
463
|
+
return ref;
|
|
464
|
+
}
|
|
465
|
+
remove(ref) {
|
|
466
|
+
this.#commit(remove(this.#snapshot.fit, ref));
|
|
467
|
+
}
|
|
468
|
+
setState(ref, state) {
|
|
469
|
+
this.#commit(setState(this.#snapshot.fit, ref, state));
|
|
470
|
+
}
|
|
471
|
+
setCharge(ref, chargeTypeId) {
|
|
472
|
+
this.#commit(setCharge(this.#snapshot.fit, ref, chargeTypeId));
|
|
473
|
+
}
|
|
474
|
+
setQuantity(ref, quantity) {
|
|
475
|
+
this.#commit(setQuantity(this.#snapshot.fit, ref, quantity));
|
|
476
|
+
}
|
|
477
|
+
setName(name) {
|
|
478
|
+
this.#commit(setName(this.#snapshot.fit, name));
|
|
479
|
+
}
|
|
480
|
+
/** Swap in another fit entirely, like an import; undo brings the old one back. */
|
|
481
|
+
replace(fit) {
|
|
482
|
+
this.#commit(withoutCharacter(fit));
|
|
483
|
+
}
|
|
484
|
+
/** Who flies the fit is not an edit of it, so this does not go in the history. */
|
|
485
|
+
setCharacter(character) {
|
|
486
|
+
this.#character = character;
|
|
487
|
+
this.#publish(this.#calculate(this.#snapshot.fit));
|
|
488
|
+
}
|
|
489
|
+
undo() {
|
|
490
|
+
const fit = this.#undo.pop();
|
|
491
|
+
if (fit === void 0) return;
|
|
492
|
+
this.#redo.push(this.#snapshot.fit);
|
|
493
|
+
this.#publish(this.#calculate(fit));
|
|
494
|
+
}
|
|
495
|
+
redo() {
|
|
496
|
+
const fit = this.#redo.pop();
|
|
497
|
+
if (fit === void 0) return;
|
|
498
|
+
this.#undo.push(this.#snapshot.fit);
|
|
499
|
+
this.#publish(this.#calculate(fit));
|
|
500
|
+
}
|
|
501
|
+
/** What `edit` would do, without doing it. */
|
|
502
|
+
preview(edit) {
|
|
503
|
+
const draft = new FitStore(this.#calculator, this.#snapshot.fit, this.#character);
|
|
504
|
+
edit(draft);
|
|
505
|
+
return {
|
|
506
|
+
before: this.#snapshot,
|
|
507
|
+
after: draft.getSnapshot()
|
|
508
|
+
};
|
|
509
|
+
}
|
|
510
|
+
#commit(fit) {
|
|
511
|
+
if (fit === this.#snapshot.fit) return;
|
|
512
|
+
this.#undo.push(this.#snapshot.fit);
|
|
513
|
+
if (this.#undo.length > HISTORY_LIMIT) this.#undo.shift();
|
|
514
|
+
this.#redo.length = 0;
|
|
515
|
+
this.#publish(this.#calculate(fit));
|
|
516
|
+
}
|
|
517
|
+
#calculate(fit) {
|
|
518
|
+
return Object.freeze({
|
|
519
|
+
fit,
|
|
520
|
+
stats: this.#calculator.calculate(fit, this.#character)
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
#publish(snapshot) {
|
|
524
|
+
this.#snapshot = snapshot;
|
|
525
|
+
for (const listener of this.#listeners) listener();
|
|
526
|
+
}
|
|
527
|
+
};
|
|
528
|
+
function withoutCharacter({ character: _, ...fit }) {
|
|
529
|
+
return fit;
|
|
530
|
+
}
|
|
531
|
+
//#endregion
|
|
532
|
+
//#region src/engine.ts
|
|
533
|
+
let loaded;
|
|
534
|
+
async function createEngine(sde, options = {}) {
|
|
535
|
+
if (loaded === void 0) loaded = {
|
|
536
|
+
sde,
|
|
537
|
+
ready: init({ module_or_path: options.wasm }).then(() => void load_sde(sde.bytes))
|
|
538
|
+
};
|
|
539
|
+
else if (loaded.sde !== sde) throw new Error("The engine already holds another SDE; there can only be one per page");
|
|
540
|
+
const { ready } = loaded;
|
|
541
|
+
try {
|
|
542
|
+
await ready;
|
|
543
|
+
} catch (error) {
|
|
544
|
+
if (loaded?.ready === ready) loaded = void 0;
|
|
545
|
+
throw error;
|
|
546
|
+
}
|
|
547
|
+
return new Engine(sde);
|
|
548
|
+
}
|
|
549
|
+
var Engine = class {
|
|
550
|
+
sde;
|
|
551
|
+
#defaultCharacter;
|
|
552
|
+
constructor(sde) {
|
|
553
|
+
this.sde = sde;
|
|
554
|
+
}
|
|
555
|
+
/** Every published skill at level V; what a fit is flown by until told otherwise. */
|
|
556
|
+
get defaultCharacter() {
|
|
557
|
+
this.#defaultCharacter ??= allSkills(this.sde, 5);
|
|
558
|
+
return this.#defaultCharacter;
|
|
559
|
+
}
|
|
560
|
+
createFit(fit, character = this.defaultCharacter) {
|
|
561
|
+
return new FitStore(this, "items" in fit ? fit : emptyFit(fit.ship), character);
|
|
562
|
+
}
|
|
563
|
+
calculate(fit, character = this.defaultCharacter) {
|
|
564
|
+
return new Stats(this.sde, fit, calculate({
|
|
565
|
+
...fit,
|
|
566
|
+
character
|
|
567
|
+
}, { validate: true }));
|
|
568
|
+
}
|
|
569
|
+
};
|
|
570
|
+
//#endregion
|
|
571
|
+
export { Attributes, Engine, FitStore, Stats, acceptsCharge, allSkills, baseValue, canFit, chargesFor, createEngine, emptyFit, firstFreeIndex, placementOf };
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@eveshipfit/fitting",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "EVE Online ship fits that recalculate themselves",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "EVEShipFit Team <info@eveship.fit>",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/EVEShipFit/eveship.fit.git",
|
|
10
|
+
"directory": "packages/fitting"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"dist"
|
|
14
|
+
],
|
|
15
|
+
"type": "module",
|
|
16
|
+
"sideEffects": false,
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./dist/index.d.mts",
|
|
20
|
+
"default": "./dist/index.mjs"
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"@eveshipfit/sde-loader": "1.0.0"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@eveshipfit/dogma-engine": "^13.0.1",
|
|
31
|
+
"@eveshipfit/sde": "^7.3539543.0",
|
|
32
|
+
"@types/node": "^22.0.0",
|
|
33
|
+
"tsdown": "^0.23.0"
|
|
34
|
+
},
|
|
35
|
+
"peerDependencies": {
|
|
36
|
+
"@eveshipfit/dogma-engine": "^13.0.0"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "tsdown"
|
|
40
|
+
}
|
|
41
|
+
}
|