eve-mentor-mcp 0.1.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 +21 -0
- package/README.md +152 -0
- package/dist/auth.js +178 -0
- package/dist/careers.js +202 -0
- package/dist/character.js +111 -0
- package/dist/combat.js +137 -0
- package/dist/esi.js +169 -0
- package/dist/index.js +217 -0
- package/dist/jargon.js +70 -0
- package/dist/payguide.js +58 -0
- package/dist/skills.js +125 -0
- package/dist/smoke.js +28 -0
- package/dist/zkill.js +72 -0
- package/package.json +45 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Henry Robinson
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# eve-mentor-mcp
|
|
2
|
+
|
|
3
|
+
An MCP server that turns Claude into an **EVE Online mentor for players trying to learn the game**.
|
|
4
|
+
|
|
5
|
+
EVE has the steepest learning cliff in gaming. Existing tools assume you already know what to ask. This one is built around the questions new players actually have:
|
|
6
|
+
|
|
7
|
+
- **"Why do I keep getting blown up?"** — pulls your real losses from zKillboard with the full fit you were flying, who killed you, and with what, so Claude can explain exactly what went wrong.
|
|
8
|
+
- **"Is this system dangerous?"** — live kill and traffic data for any system.
|
|
9
|
+
- **"What is this thing and what does it cost?"** — any ship or module, with live Jita prices.
|
|
10
|
+
- **"What should I do next?"** — log in with EVE SSO and Claude sees your skills, skill queue, wallet, location, and current ship, and can coach from your actual situation.
|
|
11
|
+
|
|
12
|
+
Works with Claude Desktop, Claude Code, and any MCP client.
|
|
13
|
+
|
|
14
|
+
## Tools
|
|
15
|
+
|
|
16
|
+
| Tool | Auth | What it does |
|
|
17
|
+
|------|------|--------------|
|
|
18
|
+
| `can_i_fly` | optional | Full recursive skill prerequisite tree + ordered training plan for any ship/module; diffed against your real skills when logged in |
|
|
19
|
+
| `career_test` | none | The EVE career "sorting hat" — Claude interviews you, then matches you to playstyles |
|
|
20
|
+
| `recent_losses` | none | A character's recent losses with full fit detail (zKillboard + ESI) |
|
|
21
|
+
| `system_intel` | none | Security status + kills/jumps in the last hour for any system |
|
|
22
|
+
| `lookup_item` | none | Item/ship/module description + live Jita buy/sell prices |
|
|
23
|
+
| `eve_login` | — | Browser-based EVE SSO login (OAuth2 PKCE, no secret stored) |
|
|
24
|
+
| `eve_auth_status` | — | Who's logged in |
|
|
25
|
+
| `character_sheet` | SSO | Skillpoints, wallet, location, current ship |
|
|
26
|
+
| `skill_queue` | SSO | What's training and when it finishes |
|
|
27
|
+
| `top_skills` | SSO | Highest-trained skills |
|
|
28
|
+
|
|
29
|
+
## Install (no coding required)
|
|
30
|
+
|
|
31
|
+
You need two things: the **Claude Desktop app** (or Claude Code) and **Node.js**, a free
|
|
32
|
+
runtime that this server runs on — installing it is like installing any other app.
|
|
33
|
+
|
|
34
|
+
### Step 1 — Install Node.js (skip if you have it)
|
|
35
|
+
|
|
36
|
+
Go to [nodejs.org](https://nodejs.org), download the **LTS** version, run the installer,
|
|
37
|
+
click through with the defaults. Done.
|
|
38
|
+
|
|
39
|
+
### Step 2 — Download and build the server
|
|
40
|
+
|
|
41
|
+
Open a terminal — on **Mac**: press `Cmd+Space`, type `Terminal`, hit Enter. On
|
|
42
|
+
**Windows**: press the Windows key, type `PowerShell`, hit Enter. Then copy-paste this
|
|
43
|
+
whole block and press Enter:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
git clone https://github.com/henryjrobinson/eve-mentor-mcp
|
|
47
|
+
cd eve-mentor-mcp
|
|
48
|
+
npm install && npm run build
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
(If Windows says git isn't installed, get it from [git-scm.com](https://git-scm.com) —
|
|
52
|
+
defaults are fine — or click the green **Code → Download ZIP** button on this page,
|
|
53
|
+
unzip it, and run the `cd` + `npm` lines inside that folder.)
|
|
54
|
+
|
|
55
|
+
When it finishes, note the folder's full path — run `pwd` (Mac) or `cd` (Windows) to
|
|
56
|
+
print it. You'll paste it in the next step where it says `/FULL/PATH/TO/eve-mentor-mcp`.
|
|
57
|
+
|
|
58
|
+
### Step 3 — Tell Claude about it
|
|
59
|
+
|
|
60
|
+
**Claude Desktop:** open the config file in any text editor —
|
|
61
|
+
|
|
62
|
+
- Mac: `~/Library/Application Support/Claude/claude_desktop_config.json`
|
|
63
|
+
- Windows: `%APPDATA%\Claude\claude_desktop_config.json`
|
|
64
|
+
|
|
65
|
+
(If it doesn't exist, create it.) Make it look like this:
|
|
66
|
+
|
|
67
|
+
```json
|
|
68
|
+
{
|
|
69
|
+
"mcpServers": {
|
|
70
|
+
"eve-mentor": {
|
|
71
|
+
"command": "node",
|
|
72
|
+
"args": ["/FULL/PATH/TO/eve-mentor-mcp/dist/index.js"]
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Save, then fully quit and reopen Claude Desktop.
|
|
79
|
+
|
|
80
|
+
**Claude Code** (one line instead):
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
claude mcp add eve-mentor -- node /FULL/PATH/TO/eve-mentor-mcp/dist/index.js
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Step 4 — Test it
|
|
87
|
+
|
|
88
|
+
Ask Claude: *"Pull the last 3 losses for character `<any EVE character name>` and explain
|
|
89
|
+
what went wrong."* If it answers with real killmail data, you're live. No EVE login is
|
|
90
|
+
needed for this — losses, system intel, prices, the career test, and the jargon glossary
|
|
91
|
+
all work immediately.
|
|
92
|
+
|
|
93
|
+
### Other AI apps
|
|
94
|
+
|
|
95
|
+
Any MCP-capable client works the same way (Cursor, LM Studio, VS Code, etc. — point them
|
|
96
|
+
at `node /path/dist/index.js`). **ChatGPT** supports MCP but only *remote* servers, not
|
|
97
|
+
local ones — a hosted version of the public tools is on the roadmap.
|
|
98
|
+
|
|
99
|
+
### Character tools (EVE SSO)
|
|
100
|
+
|
|
101
|
+
1. Go to [developers.eveonline.com](https://developers.eveonline.com) → log in with your EVE account → **Create New Application**
|
|
102
|
+
2. Name it anything (e.g. `eve-mentor`), pick **Authentication & API Access**
|
|
103
|
+
3. Select these scopes:
|
|
104
|
+
- `esi-skills.read_skills.v1`
|
|
105
|
+
- `esi-skills.read_skillqueue.v1`
|
|
106
|
+
- `esi-location.read_location.v1`
|
|
107
|
+
- `esi-location.read_ship_type.v1`
|
|
108
|
+
- `esi-wallet.read_character_wallet.v1`
|
|
109
|
+
- `esi-assets.read_assets.v1`
|
|
110
|
+
4. Set the callback URL to exactly `https://ruby-eve.com/callback` — CCP's portal only accepts
|
|
111
|
+
https callbacks; this static page (source in `site/callback/`) relays the login code to the
|
|
112
|
+
local server on port 8484. (Self-hosting? Serve your own copy and set `EVE_REDIRECT_URI`.)
|
|
113
|
+
5. Copy the **Client ID** and add it to the server's environment:
|
|
114
|
+
|
|
115
|
+
```bash
|
|
116
|
+
claude mcp add eve-mentor -e EVE_CLIENT_ID=your_client_id -- node /path/to/eve-mentor-mcp/dist/index.js
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
(or add `"env": {"EVE_CLIENT_ID": "your_client_id"}` to the Claude Desktop config.)
|
|
120
|
+
|
|
121
|
+
Then ask Claude to log you in to EVE — a browser opens, you authorize, done. Tokens are stored in `~/.config/eve-mentor/tokens.json` (mode 600) and refresh automatically.
|
|
122
|
+
|
|
123
|
+
## Try it
|
|
124
|
+
|
|
125
|
+
> *"Pull my last 3 losses for character `Your Pilot Name` and explain what I did wrong in each one."*
|
|
126
|
+
|
|
127
|
+
> *"I'm about to fly through Tama. How dangerous is it right now?"*
|
|
128
|
+
|
|
129
|
+
> *"What should I be training next given my skills and the fact that I want to try small-gang PvP?"*
|
|
130
|
+
|
|
131
|
+
## Verify it works
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
npm run smoke # tests ESI name resolution, market, system intel
|
|
135
|
+
npm run smoke -- "Pilot Name" # also tests the zKillboard loss pipeline
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Notes
|
|
139
|
+
|
|
140
|
+
- Data comes from [ESI](https://esi.evetech.net) (CCP's official API) and [zKillboard](https://zkillboard.com). Be a good citizen: this server sends a proper User-Agent and caches name lookups.
|
|
141
|
+
- Built under the [CCP Developer License](https://developers.eveonline.com/license-agreement) — non-commercial, as required.
|
|
142
|
+
- EVE Online and all related trademarks are the property of [CCP hf](https://www.ccpgames.com).
|
|
143
|
+
|
|
144
|
+
## Roadmap
|
|
145
|
+
|
|
146
|
+
- Ship fitting analysis (slot layout vs. ship bonuses)
|
|
147
|
+
- "What can I fly?" — cross-reference skills against ship prerequisites
|
|
148
|
+
- Route danger scoring (per-jump kill activity)
|
|
149
|
+
- Wormhole / exploration helpers
|
|
150
|
+
- Remote MCP deployment so no local install is needed
|
|
151
|
+
|
|
152
|
+
PRs welcome. MIT licensed.
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EVE SSO via OAuth2 PKCE (native-app flow — no client secret).
|
|
3
|
+
* Requires a free app registered at https://developers.eveonline.com with
|
|
4
|
+
* callback URL http://localhost:8484/callback. Set EVE_CLIENT_ID to its ID.
|
|
5
|
+
* Tokens persist in ~/.config/eve-mentor/tokens.json and refresh automatically.
|
|
6
|
+
*/
|
|
7
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
8
|
+
import { createServer } from "node:http";
|
|
9
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { execFile } from "node:child_process";
|
|
13
|
+
const SSO_AUTHORIZE_URL = "https://login.eveonline.com/v2/oauth/authorize";
|
|
14
|
+
const SSO_TOKEN_URL = "https://login.eveonline.com/v2/oauth/token";
|
|
15
|
+
const CALLBACK_PORT = 8484;
|
|
16
|
+
// Local listener that actually receives the auth code...
|
|
17
|
+
const CALLBACK_URL = `http://localhost:${CALLBACK_PORT}/callback`;
|
|
18
|
+
// ...but CCP's dev portal only accepts https callback URLs, so the registered
|
|
19
|
+
// redirect is a static relay page that immediately forwards the query string
|
|
20
|
+
// to the local listener. Override with EVE_REDIRECT_URI if self-hosting one.
|
|
21
|
+
const REDIRECT_URI = process.env.EVE_REDIRECT_URI ?? "https://ruby-eve.com/callback";
|
|
22
|
+
const LOGIN_TIMEOUT_MS = 180_000;
|
|
23
|
+
const TOKEN_DIR = join(homedir(), ".config", "eve-mentor");
|
|
24
|
+
const TOKEN_FILE = join(TOKEN_DIR, "tokens.json");
|
|
25
|
+
export const SCOPES = [
|
|
26
|
+
"esi-skills.read_skills.v1",
|
|
27
|
+
"esi-skills.read_skillqueue.v1",
|
|
28
|
+
"esi-location.read_location.v1",
|
|
29
|
+
"esi-location.read_ship_type.v1",
|
|
30
|
+
"esi-wallet.read_character_wallet.v1",
|
|
31
|
+
"esi-assets.read_assets.v1",
|
|
32
|
+
].join(" ");
|
|
33
|
+
export class AuthError extends Error {
|
|
34
|
+
constructor(message) {
|
|
35
|
+
super(message);
|
|
36
|
+
this.name = "AuthError";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function getClientId() {
|
|
40
|
+
const clientId = process.env.EVE_CLIENT_ID;
|
|
41
|
+
if (!clientId) {
|
|
42
|
+
throw new AuthError("EVE_CLIENT_ID is not set. Register a free app at https://developers.eveonline.com " +
|
|
43
|
+
`(callback URL: ${REDIRECT_URI}) and set its Client ID in the MCP server env.`);
|
|
44
|
+
}
|
|
45
|
+
return clientId;
|
|
46
|
+
}
|
|
47
|
+
async function loadToken() {
|
|
48
|
+
try {
|
|
49
|
+
return JSON.parse(await readFile(TOKEN_FILE, "utf8"));
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async function saveToken(token) {
|
|
56
|
+
await mkdir(TOKEN_DIR, { recursive: true });
|
|
57
|
+
await writeFile(TOKEN_FILE, JSON.stringify(token, null, 2), { mode: 0o600 });
|
|
58
|
+
}
|
|
59
|
+
function decodeJwtCharacter(accessToken) {
|
|
60
|
+
const payload = JSON.parse(Buffer.from(accessToken.split(".")[1], "base64url").toString("utf8"));
|
|
61
|
+
// sub format: "CHARACTER:EVE:2112345678"
|
|
62
|
+
const id = Number(payload.sub.split(":")[2]);
|
|
63
|
+
if (!id)
|
|
64
|
+
throw new AuthError("Could not read character ID from SSO token.");
|
|
65
|
+
return { id, name: payload.name };
|
|
66
|
+
}
|
|
67
|
+
async function exchangeToken(body) {
|
|
68
|
+
const response = await fetch(SSO_TOKEN_URL, {
|
|
69
|
+
method: "POST",
|
|
70
|
+
headers: {
|
|
71
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
72
|
+
Host: "login.eveonline.com",
|
|
73
|
+
},
|
|
74
|
+
body,
|
|
75
|
+
});
|
|
76
|
+
if (!response.ok) {
|
|
77
|
+
throw new AuthError(`EVE SSO token request failed (${response.status}): ${await response.text()}`);
|
|
78
|
+
}
|
|
79
|
+
const json = (await response.json());
|
|
80
|
+
const character = decodeJwtCharacter(json.access_token);
|
|
81
|
+
return {
|
|
82
|
+
characterId: character.id,
|
|
83
|
+
characterName: character.name,
|
|
84
|
+
accessToken: json.access_token,
|
|
85
|
+
refreshToken: json.refresh_token,
|
|
86
|
+
expiresAt: Date.now() + json.expires_in * 1000,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/** Wait for EVE SSO to redirect back to localhost with the auth code. */
|
|
90
|
+
function waitForCallback(expectedState) {
|
|
91
|
+
return new Promise((resolve, reject) => {
|
|
92
|
+
const server = createServer((req, res) => {
|
|
93
|
+
const url = new URL(req.url ?? "/", CALLBACK_URL);
|
|
94
|
+
if (url.pathname !== "/callback") {
|
|
95
|
+
res.writeHead(404).end();
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
const code = url.searchParams.get("code");
|
|
99
|
+
const state = url.searchParams.get("state");
|
|
100
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
101
|
+
res.end("<h2>Logged in to EVE. You can close this tab and return to Claude.</h2>");
|
|
102
|
+
server.close();
|
|
103
|
+
clearTimeout(timer);
|
|
104
|
+
if (!code || state !== expectedState) {
|
|
105
|
+
reject(new AuthError("SSO callback missing code or state mismatch."));
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
resolve(code);
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
const timer = setTimeout(() => {
|
|
112
|
+
server.close();
|
|
113
|
+
reject(new AuthError(`Timed out after ${LOGIN_TIMEOUT_MS / 1000}s waiting for EVE login.`));
|
|
114
|
+
}, LOGIN_TIMEOUT_MS);
|
|
115
|
+
server.on("error", (err) => {
|
|
116
|
+
clearTimeout(timer);
|
|
117
|
+
reject(new AuthError(`Could not listen on port ${CALLBACK_PORT}: ${err.message}`));
|
|
118
|
+
});
|
|
119
|
+
server.listen(CALLBACK_PORT);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
/** Run the full browser login flow. Returns the logged-in character name. */
|
|
123
|
+
export async function login() {
|
|
124
|
+
const clientId = getClientId();
|
|
125
|
+
const verifier = randomBytes(32).toString("base64url");
|
|
126
|
+
const challenge = createHash("sha256").update(verifier).digest("base64url");
|
|
127
|
+
const state = randomBytes(16).toString("base64url");
|
|
128
|
+
const authorizeUrl = `${SSO_AUTHORIZE_URL}?response_type=code` +
|
|
129
|
+
`&redirect_uri=${encodeURIComponent(REDIRECT_URI)}` +
|
|
130
|
+
`&client_id=${clientId}` +
|
|
131
|
+
`&scope=${encodeURIComponent(SCOPES)}` +
|
|
132
|
+
`&code_challenge=${challenge}&code_challenge_method=S256` +
|
|
133
|
+
`&state=${state}`;
|
|
134
|
+
const callbackPromise = waitForCallback(state);
|
|
135
|
+
openBrowser(authorizeUrl);
|
|
136
|
+
const code = await callbackPromise;
|
|
137
|
+
const token = await exchangeToken(new URLSearchParams({
|
|
138
|
+
grant_type: "authorization_code",
|
|
139
|
+
code,
|
|
140
|
+
client_id: clientId,
|
|
141
|
+
code_verifier: verifier,
|
|
142
|
+
}));
|
|
143
|
+
await saveToken(token);
|
|
144
|
+
return { characterName: token.characterName, characterId: token.characterId };
|
|
145
|
+
}
|
|
146
|
+
function openBrowser(url) {
|
|
147
|
+
if (process.platform === "win32") {
|
|
148
|
+
// "start" is a cmd builtin; the empty string is the window title argument.
|
|
149
|
+
execFile("cmd", ["/c", "start", "", url]);
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
execFile(process.platform === "darwin" ? "open" : "xdg-open", [url]);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
/** Current login state without touching the network. */
|
|
156
|
+
export async function authStatus() {
|
|
157
|
+
const token = await loadToken();
|
|
158
|
+
if (!token)
|
|
159
|
+
return { loggedIn: false };
|
|
160
|
+
return { loggedIn: true, characterName: token.characterName, characterId: token.characterId };
|
|
161
|
+
}
|
|
162
|
+
/** Valid access token + character id, refreshing if needed. */
|
|
163
|
+
export async function getSession() {
|
|
164
|
+
let token = await loadToken();
|
|
165
|
+
if (!token) {
|
|
166
|
+
throw new AuthError('Not logged in to EVE. Use the "eve_login" tool first.');
|
|
167
|
+
}
|
|
168
|
+
if (token.expiresAt < Date.now() + 60_000) {
|
|
169
|
+
const refreshed = await exchangeToken(new URLSearchParams({
|
|
170
|
+
grant_type: "refresh_token",
|
|
171
|
+
refresh_token: token.refreshToken,
|
|
172
|
+
client_id: getClientId(),
|
|
173
|
+
}));
|
|
174
|
+
await saveToken(refreshed);
|
|
175
|
+
token = refreshed;
|
|
176
|
+
}
|
|
177
|
+
return { accessToken: token.accessToken, characterId: token.characterId };
|
|
178
|
+
}
|
package/dist/careers.js
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Career path taxonomy for the playstyle "sorting hat".
|
|
3
|
+
* The MCP client (Claude) runs the interview; this is the matching data.
|
|
4
|
+
* Sources: EVE University careers wiki, CCP's EVE Academy archetypes.
|
|
5
|
+
*/
|
|
6
|
+
export const CAREER_PATHS = [
|
|
7
|
+
{
|
|
8
|
+
name: "Exploration (relic/data sites)",
|
|
9
|
+
archetype: "Explorer",
|
|
10
|
+
description: "Probe-scan hidden sites across dangerous space, hack containers for loot, run from anything that shoots.",
|
|
11
|
+
appealsTo: "Players who like treasure hunting, tension without forced combat, and playing smart instead of strong.",
|
|
12
|
+
social: "solo",
|
|
13
|
+
risk: "medium",
|
|
14
|
+
income: "high",
|
|
15
|
+
activity: "active",
|
|
16
|
+
firstShip: "Heron / Imicus (T1 scanning frigate)",
|
|
17
|
+
firstSteps: "Train Astrometrics, fit a relic/data analyzer + probe launcher, take wormholes or null-sec entrances and hack everything you find.",
|
|
18
|
+
newbieFriendly: true,
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
name: "Faction Warfare",
|
|
22
|
+
archetype: "Soldier",
|
|
23
|
+
description: "Enlist with an empire militia; capture complexes and fight small-ship PvP for loyalty point payouts.",
|
|
24
|
+
appealsTo: "Players who want real PvP fast, in cheap ships, with a built-in team and a reason to fight.",
|
|
25
|
+
social: "either",
|
|
26
|
+
risk: "high",
|
|
27
|
+
income: "medium",
|
|
28
|
+
activity: "active",
|
|
29
|
+
firstShip: "T1 frigate (Tristan, Merlin, Punisher, Rifter)",
|
|
30
|
+
firstSteps: "Join a militia (or an FW newbie corp like Minmatar Fleet Academy), buy 10 cheap fitted frigates, expect to lose them all, learn from each one.",
|
|
31
|
+
newbieFriendly: true,
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: "Mission running",
|
|
35
|
+
archetype: "Enforcer",
|
|
36
|
+
description: "Run combat missions for NPC agents, climbing from level 1 to level 4 for ISK and loyalty points.",
|
|
37
|
+
appealsTo: "Players who want structured PvE with clear goals, steady progression, and low surprise.",
|
|
38
|
+
social: "solo",
|
|
39
|
+
risk: "low",
|
|
40
|
+
income: "medium",
|
|
41
|
+
activity: "active",
|
|
42
|
+
firstShip: "T1 frigate → destroyer → cruiser",
|
|
43
|
+
firstSteps: "Find a level 1 security agent, match your ammo damage type to the enemy faction, work up the ladder.",
|
|
44
|
+
newbieFriendly: true,
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
name: "Abyssal Deadspace",
|
|
48
|
+
archetype: "Enforcer",
|
|
49
|
+
description: "Timed instanced dungeons with escalating tiers — EVE's closest thing to a roguelike arena.",
|
|
50
|
+
appealsTo: "Players who like skill-expression PvE, measurable difficulty tiers, and adrenaline.",
|
|
51
|
+
social: "solo",
|
|
52
|
+
risk: "medium",
|
|
53
|
+
income: "high",
|
|
54
|
+
activity: "active",
|
|
55
|
+
firstShip: "Well-fitted cruiser (Gila is the meta; a T1 cruiser can run low tiers)",
|
|
56
|
+
firstSteps: "Start at tier 0/1 filaments in a cheap cruiser; the timer is the real enemy — die inside and you lose everything.",
|
|
57
|
+
newbieFriendly: true,
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
name: "Mining",
|
|
61
|
+
archetype: "Industrialist",
|
|
62
|
+
description: "Extract ore, ice, and gas to sell or feed into manufacturing.",
|
|
63
|
+
appealsTo: "Players who want low-stress, semi-passive play — podcast in one ear, lasers on rocks.",
|
|
64
|
+
social: "either",
|
|
65
|
+
risk: "low",
|
|
66
|
+
income: "low",
|
|
67
|
+
activity: "semi-active",
|
|
68
|
+
firstShip: "Venture (free from career agents)",
|
|
69
|
+
firstSteps: "Run the industry career agent for a free Venture, mine high-sec belts, sell at the nearest hub; join group mining ops for company.",
|
|
70
|
+
newbieFriendly: true,
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: "Ratting",
|
|
74
|
+
archetype: "Enforcer",
|
|
75
|
+
description: "Hunt NPC pirates in asteroid belts and anomalies for bounty payouts.",
|
|
76
|
+
appealsTo: "Players who want simple combat income without mission structure.",
|
|
77
|
+
social: "solo",
|
|
78
|
+
risk: "low",
|
|
79
|
+
income: "low",
|
|
80
|
+
activity: "active",
|
|
81
|
+
firstShip: "Any combat frigate; Vexor later",
|
|
82
|
+
firstSteps: "Match damage types to the local pirate faction and clear belts/anomalies.",
|
|
83
|
+
newbieFriendly: true,
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: "Hauling / logistics",
|
|
87
|
+
archetype: "Industrialist",
|
|
88
|
+
description: "Move goods between markets via courier contracts or your own trading.",
|
|
89
|
+
appealsTo: "Players who enjoy logistics puzzles and quiet profit over combat.",
|
|
90
|
+
social: "solo",
|
|
91
|
+
risk: "medium",
|
|
92
|
+
income: "medium",
|
|
93
|
+
activity: "semi-active",
|
|
94
|
+
firstShip: "T1 industrial (Iteron Mark V, Badger)",
|
|
95
|
+
firstSteps: "Take small courier contracts; never autopilot through Uedama or Niarja-class chokepoints with valuable cargo.",
|
|
96
|
+
newbieFriendly: true,
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: "Trading / market PvP",
|
|
100
|
+
archetype: "Industrialist",
|
|
101
|
+
description: "Station trading and regional arbitrage — playing the market instead of the spaceship.",
|
|
102
|
+
appealsTo: "Spreadsheet enjoyers, economics nerds, patient compounding-gains people.",
|
|
103
|
+
social: "solo",
|
|
104
|
+
risk: "low",
|
|
105
|
+
income: "high",
|
|
106
|
+
activity: "passive",
|
|
107
|
+
firstShip: "None — a docked alt and seed capital",
|
|
108
|
+
firstSteps: "Start with 0.01-ISK-style margin trading on high-volume items in Jita; learn buy vs sell orders with small stakes.",
|
|
109
|
+
newbieFriendly: true,
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
name: "Manufacturing & research",
|
|
113
|
+
archetype: "Industrialist",
|
|
114
|
+
description: "Buy blueprints, build modules/ships, research efficiency, sell output.",
|
|
115
|
+
appealsTo: "Builders and optimizers who want to make the things everyone else explodes.",
|
|
116
|
+
social: "either",
|
|
117
|
+
risk: "low",
|
|
118
|
+
income: "medium",
|
|
119
|
+
activity: "passive",
|
|
120
|
+
firstShip: "None initially",
|
|
121
|
+
firstSteps: "Build T1 ammo from a cheap blueprint near a trade hub; profit is thin but the loop teaches the whole economy.",
|
|
122
|
+
newbieFriendly: true,
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
name: "Planetary Industry",
|
|
126
|
+
archetype: "Industrialist",
|
|
127
|
+
description: "Semi-passive resource colonies on planets, harvested on a timer.",
|
|
128
|
+
appealsTo: "Players who like idle-game mechanics layered onto their main activity.",
|
|
129
|
+
social: "solo",
|
|
130
|
+
risk: "low",
|
|
131
|
+
income: "low",
|
|
132
|
+
activity: "passive",
|
|
133
|
+
firstShip: "Any",
|
|
134
|
+
firstSteps: "Train Command Center Upgrades, drop colonies on high-sec planets, restart extractors every few days.",
|
|
135
|
+
newbieFriendly: true,
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
name: "Wormhole living",
|
|
139
|
+
archetype: "Explorer",
|
|
140
|
+
description: "Move into uncharted J-space with a corp: scanning, krabbing, and ambush PvP with no local chat.",
|
|
141
|
+
appealsTo: "Players who want the frontier — self-reliance, paranoia, tight-knit crews.",
|
|
142
|
+
social: "group",
|
|
143
|
+
risk: "high",
|
|
144
|
+
income: "high",
|
|
145
|
+
activity: "active",
|
|
146
|
+
firstShip: "Scanning frigate; corp provides doctrine ships",
|
|
147
|
+
firstSteps: "Not a first-month path solo — join a wormhole corp that teaches.",
|
|
148
|
+
newbieFriendly: false,
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
name: "Null-sec sovereignty warfare",
|
|
152
|
+
archetype: "Soldier",
|
|
153
|
+
description: "Join a null bloc (Pandemic Horde, Brave, etc.): big fleet fights, doctrine ships, alliance life.",
|
|
154
|
+
appealsTo: "Players who want to be part of something huge — thousand-pilot battles and politics.",
|
|
155
|
+
social: "group",
|
|
156
|
+
risk: "medium",
|
|
157
|
+
income: "medium",
|
|
158
|
+
activity: "active",
|
|
159
|
+
firstShip: "Whatever the alliance doctrine hands you",
|
|
160
|
+
firstSteps: "Join a newbie-friendly bloc, follow the fleet, shoot what the FC calls. Free ships are common.",
|
|
161
|
+
newbieFriendly: true,
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
name: "Incursions",
|
|
165
|
+
archetype: "Enforcer",
|
|
166
|
+
description: "High-end organized fleet PvE against Sansha invasions — elite income, strict fits.",
|
|
167
|
+
appealsTo: "PvE players who want raid-style group content.",
|
|
168
|
+
social: "group",
|
|
169
|
+
risk: "low",
|
|
170
|
+
income: "high",
|
|
171
|
+
activity: "active",
|
|
172
|
+
firstShip: "Battleship with a community-mandated fit",
|
|
173
|
+
firstSteps: "A months-long training target, not a starting point.",
|
|
174
|
+
newbieFriendly: false,
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
name: "Salvaging",
|
|
178
|
+
archetype: "Industrialist",
|
|
179
|
+
description: "Sweep battlefields and mission wrecks for salvage materials.",
|
|
180
|
+
appealsTo: "Scavenger-brain players; pairs well with mission running.",
|
|
181
|
+
social: "either",
|
|
182
|
+
risk: "low",
|
|
183
|
+
income: "low",
|
|
184
|
+
activity: "active",
|
|
185
|
+
firstShip: "Destroyer with salvagers → Noctis",
|
|
186
|
+
firstSteps: "Follow your own mission wrecks first; public wreck fields later.",
|
|
187
|
+
newbieFriendly: true,
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
name: "Piracy / ganking",
|
|
191
|
+
archetype: "Soldier",
|
|
192
|
+
description: "Legal-but-hostile sandbox crime: suicide ganking, can-flipping, ransoms, scams.",
|
|
193
|
+
appealsTo: "Villain-arc players. EVE genuinely permits it; the community will remember you.",
|
|
194
|
+
social: "either",
|
|
195
|
+
risk: "high",
|
|
196
|
+
income: "medium",
|
|
197
|
+
activity: "active",
|
|
198
|
+
firstShip: "Cheap Catalyst",
|
|
199
|
+
firstSteps: "Understand CONCORD and sec status mechanics first or you'll lock yourself out of empire space.",
|
|
200
|
+
newbieFriendly: false,
|
|
201
|
+
},
|
|
202
|
+
];
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Authenticated character endpoints (require EVE SSO login).
|
|
3
|
+
*/
|
|
4
|
+
import { getSession } from "./auth.js";
|
|
5
|
+
import { esiFetch, getSystem, getType, namesForIds } from "./esi.js";
|
|
6
|
+
export async function getCharacterSheet() {
|
|
7
|
+
const { accessToken, characterId } = await getSession();
|
|
8
|
+
const authed = { token: accessToken };
|
|
9
|
+
const [skills, wallet, location, ship, names] = await Promise.all([
|
|
10
|
+
esiFetch(`/characters/${characterId}/skills/`, authed),
|
|
11
|
+
esiFetch(`/characters/${characterId}/wallet/`, authed),
|
|
12
|
+
esiFetch(`/characters/${characterId}/location/`, authed),
|
|
13
|
+
esiFetch(`/characters/${characterId}/ship/`, authed),
|
|
14
|
+
namesForIds([characterId]),
|
|
15
|
+
]);
|
|
16
|
+
const [system, shipType] = await Promise.all([
|
|
17
|
+
getSystem(location.data.solar_system_id),
|
|
18
|
+
getType(ship.data.ship_type_id),
|
|
19
|
+
]);
|
|
20
|
+
return {
|
|
21
|
+
character: names.get(characterId) ?? `character-${characterId}`,
|
|
22
|
+
totalSkillpoints: skills.data.total_sp,
|
|
23
|
+
skillCount: skills.data.skills.length,
|
|
24
|
+
walletIsk: Math.round(wallet.data),
|
|
25
|
+
location: system.name,
|
|
26
|
+
locationSecurity: Number(system.security_status.toFixed(1)),
|
|
27
|
+
currentShip: shipType.name,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export async function getSkillQueue() {
|
|
31
|
+
const { accessToken, characterId } = await getSession();
|
|
32
|
+
const { data: queue } = await esiFetch(`/characters/${characterId}/skillqueue/`, { token: accessToken });
|
|
33
|
+
const names = await namesForIds(queue.map((entry) => entry.skill_id));
|
|
34
|
+
return queue.map((entry) => ({
|
|
35
|
+
position: entry.queue_position,
|
|
36
|
+
skill: names.get(entry.skill_id) ?? `skill-${entry.skill_id}`,
|
|
37
|
+
toLevel: entry.finished_level,
|
|
38
|
+
finishes: entry.finish_date ?? "paused (queue not training)",
|
|
39
|
+
}));
|
|
40
|
+
}
|
|
41
|
+
const STRUCTURE_ID_FLOOR = 1_000_000_000_000;
|
|
42
|
+
async function locationLabel(locationId, accessToken, stationNames) {
|
|
43
|
+
if (locationId < STRUCTURE_ID_FLOOR) {
|
|
44
|
+
return stationNames.get(locationId) ?? `location-${locationId}`;
|
|
45
|
+
}
|
|
46
|
+
// Player-owned structure: name lookup requires docking access — a 403 here
|
|
47
|
+
// means the character is locked out (assets likely stranded or in asset safety).
|
|
48
|
+
try {
|
|
49
|
+
const { data } = await esiFetch(`/universe/structures/${locationId}/`, { token: accessToken });
|
|
50
|
+
return data.name;
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return `INACCESSIBLE structure ${locationId} (no docking access — assets may be stranded; check asset safety)`;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** All assets grouped by location, with inaccessible structures and asset safety flagged. */
|
|
57
|
+
export async function getAssetsSummary() {
|
|
58
|
+
const { accessToken, characterId } = await getSession();
|
|
59
|
+
const all = [];
|
|
60
|
+
let page = 1;
|
|
61
|
+
let pages = 1;
|
|
62
|
+
do {
|
|
63
|
+
const result = await esiFetch(`/characters/${characterId}/assets/?page=${page}`, { token: accessToken });
|
|
64
|
+
all.push(...result.data);
|
|
65
|
+
pages = result.pages;
|
|
66
|
+
page += 1;
|
|
67
|
+
} while (page <= pages);
|
|
68
|
+
// Top-level assets only: things whose container is a station/structure,
|
|
69
|
+
// not another asset (e.g. modules fitted to a stored ship).
|
|
70
|
+
const ownItemIds = new Set(all.map((asset) => asset.item_id));
|
|
71
|
+
const topLevel = all.filter((asset) => !ownItemIds.has(asset.location_id));
|
|
72
|
+
const byLocation = new Map();
|
|
73
|
+
for (const asset of topLevel) {
|
|
74
|
+
const group = byLocation.get(asset.location_id);
|
|
75
|
+
if (group) {
|
|
76
|
+
group.push(asset);
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
byLocation.set(asset.location_id, [asset]);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
const stationIds = [...byLocation.keys()].filter((id) => id < STRUCTURE_ID_FLOOR);
|
|
83
|
+
const typeIds = topLevel.map((asset) => asset.type_id);
|
|
84
|
+
const names = await namesForIds([...stationIds, ...typeIds]);
|
|
85
|
+
return Promise.all([...byLocation.entries()].map(async ([locationId, assets]) => ({
|
|
86
|
+
location: await locationLabel(locationId, accessToken, names),
|
|
87
|
+
inAssetSafety: assets.some((asset) => asset.location_flag === "AssetSafety"),
|
|
88
|
+
itemCount: assets.length,
|
|
89
|
+
items: assets
|
|
90
|
+
.map((asset) => ({
|
|
91
|
+
name: names.get(asset.type_id) ?? `type-${asset.type_id}`,
|
|
92
|
+
quantity: asset.quantity,
|
|
93
|
+
}))
|
|
94
|
+
.slice(0, 25),
|
|
95
|
+
})));
|
|
96
|
+
}
|
|
97
|
+
export async function getTopSkills(limit) {
|
|
98
|
+
const { accessToken, characterId } = await getSession();
|
|
99
|
+
const { data } = await esiFetch(`/characters/${characterId}/skills/`, {
|
|
100
|
+
token: accessToken,
|
|
101
|
+
});
|
|
102
|
+
const top = [...data.skills]
|
|
103
|
+
.sort((a, b) => b.skillpoints_in_skill - a.skillpoints_in_skill)
|
|
104
|
+
.slice(0, limit);
|
|
105
|
+
const names = await namesForIds(top.map((skill) => skill.skill_id));
|
|
106
|
+
return top.map((skill) => ({
|
|
107
|
+
skill: names.get(skill.skill_id) ?? `skill-${skill.skill_id}`,
|
|
108
|
+
level: skill.trained_skill_level,
|
|
109
|
+
skillpoints: skill.skillpoints_in_skill,
|
|
110
|
+
}));
|
|
111
|
+
}
|