feat: allow toggling the state of the modules (Offline, Online, Active, Overload) (#10)

This commit is contained in:
Patric Stout
2023-11-13 23:35:43 +01:00
committed by GitHub
parent c5b60e2eae
commit 056a839f4e
15 changed files with 327 additions and 89 deletions

196
.storybook/fits.ts Normal file
View File

@@ -0,0 +1,196 @@
export const eftFit = `[Loki,Loki basic PVE]
Caldari Navy Ballistic Control System
Caldari Navy Ballistic Control System
Caldari Navy Ballistic Control System
Damage Control II
Gist X-Type Large Shield Booster
Republic Fleet Large Cap Battery
Missile Guidance Computer II
10MN Afterburner II
Multispectrum Shield Hardener II
Heavy Assault Missile Launcher II, Mjolnir Rage Heavy Assault Missile
Heavy Assault Missile Launcher II, Mjolnir Rage Heavy Assault Missile
Heavy Assault Missile Launcher II, Mjolnir Rage Heavy Assault Missile
Heavy Assault Missile Launcher II, Mjolnir Rage Heavy Assault Missile
Heavy Assault Missile Launcher II, Mjolnir Rage Heavy Assault Missile
Covert Ops Cloaking Device II
Sisters Core Probe Launcher
Medium Hydraulic Bay Thrusters II
Medium Rocket Fuel Cache Partition II
Medium Rocket Fuel Cache Partition I
Loki Core - Augmented Nuclear Reactor
Loki Defensive - Covert Reconfiguration
Loki Offensive - Launcher Efficiency Configuration
Loki Propulsion - Wake Limiter
Hammerhead II x1
`;
export const fullFit = {
"name": "C3 Ratter : NishEM",
"ship_type_id": 29984,
"description": "",
"items": [
{
"flag": 125,
"quantity": 1,
"type_id": 45626
},
{
"flag": 126,
"quantity": 1,
"type_id": 45591
},
{
"flag": 127,
"quantity": 1,
"type_id": 45601
},
{
"flag": 128,
"quantity": 1,
"type_id": 45615
},
{
"flag": 11,
"quantity": 1,
"type_id": 22291
},
{
"flag": 12,
"quantity": 1,
"type_id": 22291
},
{
"flag": 13,
"quantity": 1,
"type_id": 22291
},
{
"flag": 19,
"quantity": 1,
"type_id": 41218
},
{
"flag": 20,
"quantity": 1,
"type_id": 35790
},
{
"flag": 21,
"quantity": 1,
"type_id": 2281,
"state": "Active"
},
{
"flag": 22,
"quantity": 1,
"type_id": 15766
},
{
"flag": 23,
"quantity": 1,
"type_id": 19187
},
{
"flag": 24,
"quantity": 1,
"type_id": 19187
},
{
"flag": 25,
"quantity": 1,
"type_id": 35790
},
{
"flag": 27,
"quantity": 1,
"type_id": 25715
},
{
"flag": 28,
"quantity": 1,
"type_id": 25715
},
{
"flag": 29,
"quantity": 1,
"type_id": 25715
},
{
"flag": 30,
"quantity": 1,
"type_id": 25715
},
{
"flag": 31,
"quantity": 1,
"type_id": 25715
},
{
"flag": 32,
"quantity": 1,
"type_id": 25715
},
{
"flag": 33,
"quantity": 1,
"type_id": 28756
},
{
"flag": 92,
"quantity": 1,
"type_id": 31724
},
{
"flag": 93,
"quantity": 1,
"type_id": 31824
},
{
"flag": 94,
"quantity": 1,
"type_id": 31378
},
{
"flag": 5,
"quantity": 3720,
"type_id": 24492
},
{
"flag": 5,
"quantity": 5472,
"type_id": 2679
},
{
"flag": 5,
"quantity": 1,
"type_id": 35795
},
{
"flag": 5,
"quantity": 1,
"type_id": 35794
},
{
"flag": 5,
"quantity": 8,
"type_id": 30486
},
{
"flag": 5,
"quantity": 1,
"type_id": 35794
},
{
"flag": 5,
"quantity": 396,
"type_id": 24492
}
]
};

View File

@@ -18,7 +18,7 @@
"author": "Patric Stout <eveshipfit@truebrain.nl>",
"license": "MIT",
"dependencies": {
"@eveshipfit/dogma-engine": "^1.1.0",
"@eveshipfit/dogma-engine": "^2.0.1",
"clsx": "^2.0.0"
},
"devDependencies": {

View File

@@ -1,6 +1,8 @@
import type { Meta, StoryObj } from '@storybook/react';
import React from "react";
import { fullFit } from '../../.storybook/fits';
import { EveDataProvider } from '../EveDataProvider';
import { DogmaEngineContext, DogmaEngineProvider } from './';
@@ -28,7 +30,7 @@ const TestDogmaEngine = () => {
const dogmaEngine = React.useContext(DogmaEngineContext);
if (dogmaEngine?.loaded) {
const stats = dogmaEngine.engine?.calculate({hull: 12747, items: [20639]}, {});
const stats = dogmaEngine.engine?.calculate(fullFit, {});
return (
<div>

View File

@@ -38,8 +38,8 @@ export interface DogmaEngineProps {
* const dogmaEngine = React.useContext(DogmaEngineContext);
*
* if (dogmaEngine?.loaded) {
* // calculate({hull: number, items: number[]}, skills: Map<number, number>)
* const stats = dogmaEngine.engine.calculate({hull: 12747, items: [20639]}, []);
* // calculate(esiFit: EsiFit, skills: Map<number, number>)
* const stats = dogmaEngine.engine.calculate(esiFit, {});
* console.log(stats);
* }
* ```

View File

@@ -7,7 +7,7 @@ import { DogmaAttribute, DogmaEffect, TypeDogma, TypeID } from "./DataTypes";
// eslint-disable-next-line import/extensions
import * as esf_pb2 from "./esf_pb2.js";
const defaultDataUrl = "https://data.eveship.fit/20231023/";
const defaultDataUrl = "https://data.eveship.fit/20231023-3/";
interface DogmaData {
loaded?: boolean;

View File

@@ -1,6 +1,8 @@
import type { Decorator, Meta, StoryObj } from '@storybook/react';
import React from "react";
import { eftFit } from '../../.storybook/fits';
import { EveDataProvider } from '../EveDataProvider';
import { FormatEftToEsi } from './FormatEftToEsi';
@@ -23,37 +25,7 @@ type Story = StoryObj<typeof FormatEftToEsi>;
export const Default: Story = {
args: {
eft: `[Loki,Loki basic PVE]
Caldari Navy Ballistic Control System
Caldari Navy Ballistic Control System
Caldari Navy Ballistic Control System
Damage Control II
Gist X-Type Large Shield Booster
Republic Fleet Large Cap Battery
Missile Guidance Computer II
10MN Afterburner II
Multispectrum Shield Hardener II
Heavy Assault Missile Launcher II, Mjolnir Rage Heavy Assault Missile
Heavy Assault Missile Launcher II, Mjolnir Rage Heavy Assault Missile
Heavy Assault Missile Launcher II, Mjolnir Rage Heavy Assault Missile
Heavy Assault Missile Launcher II, Mjolnir Rage Heavy Assault Missile
Heavy Assault Missile Launcher II, Mjolnir Rage Heavy Assault Missile
Covert Ops Cloaking Device II
Sisters Core Probe Launcher
Medium Hydraulic Bay Thrusters II
Medium Rocket Fuel Cache Partition II
Medium Rocket Fuel Cache Partition I
Loki Core - Augmented Nuclear Reactor
Loki Defensive - Covert Reconfiguration
Loki Offensive - Launcher Efficiency Configuration
Loki Propulsion - Wake Limiter
Hammerhead II x1
`,
eft: eftFit,
},
decorators: [withEveDataProvider],
};

View File

@@ -1,6 +1,8 @@
import type { Decorator, Meta, StoryObj } from '@storybook/react';
import React from "react";
import { fullFit } from '../../.storybook/fits';
import { DogmaEngineProvider } from '../DogmaEngineProvider';
import { EveDataProvider } from '../EveDataProvider';
import { ShipSnapshotProvider } from '../ShipSnapshotProvider';
@@ -20,7 +22,7 @@ const withShipSnapshotProvider: Decorator<{name: string}> = (Story, context) =>
<EveDataProvider>
<DogmaEngineProvider>
<ShipSnapshotProvider {...context.parameters.snapshot}>
<Story />
cpuUsage: <Story />
</ShipSnapshotProvider>
</DogmaEngineProvider>
</EveDataProvider>
@@ -34,7 +36,7 @@ export const Default: Story = {
decorators: [withShipSnapshotProvider],
parameters: {
snapshot: {
fit: JSON.parse("{\"name\": \"test\", \"ship_type_id\": 12747, \"items\": [{\"flag\": 11, \"quantity\": 1, \"type_id\": 20639}]}"),
fit: fullFit,
skills: {},
}
},

View File

@@ -65,22 +65,40 @@
}
.slotInner {
background-color: rgba(0, 0, 0, 0.2);
border-top-left-radius: 100% 4px;
border-top-right-radius: 100% 4px;
border: 1px solid rgba(127, 127, 127, 0.5);
border: 1px solid black;
box-sizing: border-box;
height: calc(var(--radius-slots) / 7);
left: calc(var(--radius-slots) / 8 / 2 * -1);
left: calc(var(--radius-slots) / 7 / 2 * -1);
position: absolute;
top: 2px;
transform: perspective(8px) rotateX(-1.5deg);
width: calc(var(--radius-slots) / 8);
width: calc(var(--radius-slots) / 7);
}
.slotInnerInvalid {
border: 1px solid red;
}
.slotInner[data-state="Passive"] {
background-color: #0000002d;
border-color: #6c6c6c9f;
opacity: 0.3;
}
.slotInner[data-state="Online"] {
background-color: #9595952d;
border-color: #9595959f;
}
.slotInner[data-state="Active"] {
background-color: #87d3282d;
border-color: #87d3289f;
}
.slotInner[data-state="Overload"] {
background-color: #e8342b2d;
border-color: #e8342b9f;
}
.slotItem {
--reverse-rotation: calc(-1 * var(--rotation));
left: -32px;
@@ -89,6 +107,10 @@
transform: rotate(var(--reverse-rotation)) scale(calc(var(--scale) * 0.8));
}
.slotItemOffline {
opacity: 0.3;
}
.slotItem > img {
border-top-left-radius: 32px;
}

View File

@@ -1,6 +1,8 @@
import type { Decorator, Meta, StoryObj } from '@storybook/react';
import React from "react";
import { fullFit } from '../../.storybook/fits';
import { DogmaEngineProvider } from '../DogmaEngineProvider';
import { EveDataProvider } from '../EveDataProvider';
import { ShipSnapshotProvider } from '../ShipSnapshotProvider';
@@ -34,7 +36,7 @@ export const Default: Story = {
decorators: [withShipSnapshotProvider],
parameters: {
snapshot: {
fit: {"name": "C3 Ratter : NishEM", "ship_type_id": 29984, "description": "", "items": [{"flag": 125, "quantity": 1, "type_id": 45626}, {"flag": 126, "quantity": 1, "type_id": 45591}, {"flag": 127, "quantity": 1, "type_id": 45601}, {"flag": 128, "quantity": 1, "type_id": 45615}, {"flag": 11, "quantity": 1, "type_id": 22291}, {"flag": 12, "quantity": 1, "type_id": 22291}, {"flag": 13, "quantity": 1, "type_id": 22291}, {"flag": 19, "quantity": 1, "type_id": 41218}, {"flag": 20, "quantity": 1, "type_id": 35790}, {"flag": 21, "quantity": 1, "type_id": 2281}, {"flag": 22, "quantity": 1, "type_id": 15766}, {"flag": 23, "quantity": 1, "type_id": 19187}, {"flag": 24, "quantity": 1, "type_id": 19187}, {"flag": 25, "quantity": 1, "type_id": 35790}, {"flag": 27, "quantity": 1, "type_id": 25715}, {"flag": 28, "quantity": 1, "type_id": 25715}, {"flag": 29, "quantity": 1, "type_id": 25715}, {"flag": 30, "quantity": 1, "type_id": 25715}, {"flag": 31, "quantity": 1, "type_id": 25715}, {"flag": 32, "quantity": 1, "type_id": 25715}, {"flag": 33, "quantity": 1, "type_id": 28756}, {"flag": 92, "quantity": 1, "type_id": 31724}, {"flag": 93, "quantity": 1, "type_id": 31824}, {"flag": 94, "quantity": 1, "type_id": 31378}, {"flag": 5, "quantity": 3720, "type_id": 24492}, {"flag": 5, "quantity": 5472, "type_id": 2679}, {"flag": 5, "quantity": 1, "type_id": 35795}, {"flag": 5, "quantity": 1, "type_id": 35794}, {"flag": 5, "quantity": 8, "type_id": 30486}, {"flag": 5, "quantity": 1, "type_id": 35794}, {"flag": 5, "quantity": 396, "type_id": 24492}]},
fit: fullFit,
skills: {},
}
},

View File

@@ -24,6 +24,13 @@ const esiFlagMapping: Record<string, number[]> = {
],
};
const stateRotation: Record<string, string[]> = {
"Passive": ["Passive"],
"Online": ["Passive", "Online"],
"Active": ["Passive", "Online", "Active"],
"Overload": ["Passive", "Online", "Active", "Overload"],
};
export const Slot = (props: {type: string, index: number, fittable: boolean, rotation: string}) => {
const eveData = React.useContext(EveDataContext);
const shipSnapshot = React.useContext(ShipSnapshotContext);
@@ -31,7 +38,7 @@ export const Slot = (props: {type: string, index: number, fittable: boolean, rot
const rotationStyle = { "--rotation": props.rotation } as React.CSSProperties;
const esiFlag = esiFlagMapping[props.type][props.index - 1];
const esiItem = shipSnapshot?.fit?.items.find((item) => item.flag == esiFlag);
const esiItem = shipSnapshot?.items?.find((item) => item.flag == esiFlag);
let item = <></>;
/* Not fittable and nothing fitted; no need to render the slot. */
@@ -43,10 +50,28 @@ export const Slot = (props: {type: string, index: number, fittable: boolean, rot
item = <img src={`https://images.evetech.net/types/${esiItem.type_id}/icon?size=64`} title={eveData?.typeIDs?.[esiItem.type_id].name} />
}
return <div className={styles.slot} style={rotationStyle}>
<div className={ctlx(styles.slotInner, { [styles.slotInnerInvalid]: !props.fittable })}>
const isOffline = esiItem?.state === "Passive" && esiItem?.max_state !== "Passive";
function cycleState(e: React.MouseEvent<HTMLDivElement, MouseEvent>) {
if (!shipSnapshot?.loaded || !esiItem) return;
const states = stateRotation[esiItem.max_state];
const stateIndex = states.indexOf(esiItem.state);
let newState;
if (e.shiftKey) {
newState = states[(stateIndex - 1 + states.length) % states.length];
} else {
newState = states[(stateIndex + 1) % states.length];
}
shipSnapshot.setItemState(esiItem.flag, newState);
}
return <div className={styles.slot} style={rotationStyle} onClick={cycleState}>
<div className={ctlx(styles.slotInner, { [styles.slotInnerInvalid]: !props.fittable, })} data-state={esiItem?.state}>
</div>
<div className={styles.slotItem}>
<div className={ctlx(styles.slotItem, { [styles.slotItemOffline]: isOffline })}>
{item}
</div>
</div>

View File

@@ -1,6 +1,8 @@
import type { Decorator, Meta, StoryObj } from '@storybook/react';
import React from "react";
import { fullFit } from '../../.storybook/fits';
import { DogmaEngineProvider } from '../DogmaEngineProvider';
import { EveDataProvider } from '../EveDataProvider';
import { ShipSnapshotProvider } from '../ShipSnapshotProvider';
@@ -34,7 +36,7 @@ export const Default: Story = {
decorators: [withShipSnapshotProvider],
parameters: {
snapshot: {
fit: {"name": "C3 Ratter : NishEM", "ship_type_id": 29984, "description": "", "items": [{"flag": 125, "quantity": 1, "type_id": 45626}, {"flag": 126, "quantity": 1, "type_id": 45591}, {"flag": 127, "quantity": 1, "type_id": 45601}, {"flag": 128, "quantity": 1, "type_id": 45615}, {"flag": 11, "quantity": 1, "type_id": 22291}, {"flag": 12, "quantity": 1, "type_id": 22291}, {"flag": 13, "quantity": 1, "type_id": 22291}, {"flag": 19, "quantity": 1, "type_id": 41218}, {"flag": 20, "quantity": 1, "type_id": 35790}, {"flag": 21, "quantity": 1, "type_id": 2281}, {"flag": 22, "quantity": 1, "type_id": 15766}, {"flag": 23, "quantity": 1, "type_id": 19187}, {"flag": 24, "quantity": 1, "type_id": 19187}, {"flag": 25, "quantity": 1, "type_id": 35790}, {"flag": 27, "quantity": 1, "type_id": 25715}, {"flag": 28, "quantity": 1, "type_id": 25715}, {"flag": 29, "quantity": 1, "type_id": 25715}, {"flag": 30, "quantity": 1, "type_id": 25715}, {"flag": 31, "quantity": 1, "type_id": 25715}, {"flag": 32, "quantity": 1, "type_id": 25715}, {"flag": 33, "quantity": 1, "type_id": 28756}, {"flag": 92, "quantity": 1, "type_id": 31724}, {"flag": 93, "quantity": 1, "type_id": 31824}, {"flag": 94, "quantity": 1, "type_id": 31378}, {"flag": 5, "quantity": 3720, "type_id": 24492}, {"flag": 5, "quantity": 5472, "type_id": 2679}, {"flag": 5, "quantity": 1, "type_id": 35795}, {"flag": 5, "quantity": 1, "type_id": 35794}, {"flag": 5, "quantity": 8, "type_id": 30486}, {"flag": 5, "quantity": 1, "type_id": 35794}, {"flag": 5, "quantity": 396, "type_id": 24492}]},
fit: fullFit,
skills: {},
}
},

View File

@@ -78,10 +78,10 @@ export const ShipFitExtended = (props: ShipFitExtendedProps) => {
<div className={styles.cpuPg}>
<CpuPg title="CPU">
<ShipAttribute name="cpuUsage" fixed={1} />/<ShipAttribute name="cpuOutput" fixed={1} />
<ShipAttribute name="cpuUnused" fixed={1} />/<ShipAttribute name="cpuOutput" fixed={1} />
</CpuPg>
<CpuPg title="Power Grid">
<ShipAttribute name="powerUsage" fixed={1} />/<ShipAttribute name="powerOutput" fixed={1} />
<ShipAttribute name="powerUnused" fixed={1} />/<ShipAttribute name="powerOutput" fixed={1} />
</CpuPg>
</div>
</div>

View File

@@ -1,6 +1,8 @@
import type { Meta, StoryObj } from '@storybook/react';
import React from "react";
import { fullFit } from '../../.storybook/fits';
import { EveDataContext, EveDataProvider } from '../EveDataProvider';
import { DogmaEngineProvider } from '../DogmaEngineProvider';
import { ShipSnapshotItemAttribute, ShipSnapshotContext, ShipSnapshotProvider } from './';
@@ -39,7 +41,7 @@ const TestShipSnapshot = () => {
export const Default: Story = {
args: {
fit: {"name": "test", description: "", "ship_type_id": 29984, "items": [{"flag": 11, "quantity": 1, "type_id": 20639}]},
fit: fullFit,
skills: {},
},
render: (args) => (

View File

@@ -12,6 +12,10 @@ export interface ShipSnapshotItemAttribute {
export interface ShipSnapshotItem {
type_id: number,
quantity: number,
flag: number,
state: string,
max_state: string,
attributes: Map<number, ShipSnapshotItemAttribute>,
effects: number[],
}
@@ -24,6 +28,7 @@ export interface EsiFit {
flag: number;
type_id: number;
quantity: number;
state?: string;
}[];
}
@@ -33,69 +38,75 @@ interface ShipSnapshot {
items?: ShipSnapshotItem[];
fit?: EsiFit;
setItemState: (flag: number, state: string) => void;
}
export const ShipSnapshotContext = React.createContext<ShipSnapshot>({});
export const ShipSnapshotContext = React.createContext<ShipSnapshot>({
loaded: undefined,
setItemState: () => {},
});
export interface ShipSnapshotProps {
/** Children that can use this provider. */
children: React.ReactNode;
/** A ship fit in ESI representation. */
fit: EsiFit;
/** A list of skills to apply to the fit (skill_id, skill_level). */
/** A list of skills to apply to the fit: {skill_id: skill_level}. */
skills: Record<number, number>;
}
const EsiFlagMapping = [
11, 12, 13, 14, 15, 16, 17, 18, // lowslot
19, 20, 21, 22, 23, 24, 25, 26, // medslot
27, 28, 29, 30, 31, 32, 33, 34, // hislot
92, 93, 94, // rig
125, 126, 127, 128, // subsystem
];
function esiFitToDogmaFit(fit: EsiFit): {
hull: number,
items: number[],
} {
const dogmaFit: {
hull: number,
items: number[],
} = {
"hull": fit.ship_type_id,
"items": [],
}
for (const item of fit.items) {
if (EsiFlagMapping.includes(item.flag)) {
dogmaFit.items.push(item.type_id);
}
}
return dogmaFit;
}
/**
* Calculates the current attrbitues and applied effects of a ship fit.
*/
export const ShipSnapshotProvider = (props: ShipSnapshotProps) => {
const [shipSnapshot, setShipSnapshot] = React.useState<ShipSnapshot>({});
const [shipSnapshot, setShipSnapshot] = React.useState<ShipSnapshot>({
loaded: undefined,
setItemState: () => {},
});
const [currentFit, setCurrentFit] = React.useState<EsiFit | undefined>(undefined);
const dogmaEngine = React.useContext(DogmaEngineContext);
const setItemState = React.useCallback((flag: number, state: string) => {
if (!currentFit) return;
setCurrentFit((oldFit: EsiFit | undefined) => {
if (!oldFit) return oldFit;
return {
...oldFit,
items: oldFit?.items?.map((item) => {
if (item.flag === flag) {
return {
...item,
state: state,
};
}
return item;
}),
};
})
}, [currentFit]);
React.useEffect(() => {
if (!dogmaEngine.loaded) return;
if (!props.fit || !props.skills) return;
if (!currentFit || !props.skills) return;
const dogmaFit = esiFitToDogmaFit(props.fit);
const snapshot = dogmaEngine.engine?.calculate(dogmaFit, props.skills);
const snapshot = dogmaEngine.engine?.calculate(currentFit, props.skills);
setShipSnapshot({
loaded: true,
hull: snapshot.hull,
items: snapshot.items,
fit: props.fit,
fit: currentFit,
setItemState,
});
}, [dogmaEngine, props.fit, props.skills]);
}, [dogmaEngine, currentFit, props.skills, setItemState]);
React.useEffect(() => {
setCurrentFit(props.fit);
}, [props.fit]);
return <ShipSnapshotContext.Provider value={shipSnapshot}>
{props.children}

View File

@@ -1,6 +1,8 @@
import type { Decorator, Meta, StoryObj } from '@storybook/react';
import React from "react";
import { fullFit } from '../../.storybook/fits';
import { DogmaEngineProvider } from '../DogmaEngineProvider';
import { EveDataProvider } from '../EveDataProvider';
import { ShipSnapshotProvider } from '../ShipSnapshotProvider';
@@ -31,7 +33,7 @@ export const Default: Story = {
decorators: [withShipSnapshotProvider],
parameters: {
snapshot: {
fit: JSON.parse("{\"name\": \"test\", \"ship_type_id\": 12747, \"items\": [{\"flag\": 11, \"quantity\": 1, \"type_id\": 20639}]}"),
fit: fullFit,
skills: {},
}
},