@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 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
@@ -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
+ }