Add Dune Awakening character builder + initial project scaffolding
- character-builder/: Vue 3 + NestJS + Valkey app for planning house, class, character XP, 5 spec tracks, faction standing, and skill trees. Shareable via short link (POST /api/builds → 8-char nanoid). - character-builder/data/: parsed JSON tables (character XP through L200, 5 specs to L100, 2 faction standing tables, 5 class skill trees). - character-builder/scripts/extract.py: parser that regenerates data/*.json from the gitignored sample-data/*.html snapshots. - Dockerfile + docker-compose.yml: two-container deploy (app + Valkey). - specialization-calculator/: pre-existing single-file XP/quest calculator, carried into the repo. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
commit
98a1792106
48 changed files with 9805 additions and 0 deletions
17
.gitignore
vendored
Normal file
17
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
# raw HTML snapshots from dune.gaming.tools — not committed (large, regenerable)
|
||||||
|
sample-data/
|
||||||
|
|
||||||
|
# node
|
||||||
|
**/node_modules/
|
||||||
|
**/dist/
|
||||||
|
**/.vite/
|
||||||
|
|
||||||
|
# editor / OS
|
||||||
|
.DS_Store
|
||||||
|
*.log
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
|
||||||
|
# python tooling artifacts
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
31
README.md
Normal file
31
README.md
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
# dune-tools
|
||||||
|
|
||||||
|
Fan-made tooling for Dune Awakening.
|
||||||
|
|
||||||
|
## Projects
|
||||||
|
|
||||||
|
- **[character-builder/](character-builder/)** — Vue 3 + NestJS + Valkey app to
|
||||||
|
plan a character (house, class, character XP, 5 specialization tracks, faction
|
||||||
|
standing, skill trees). Shareable via short link. Runs as two Docker
|
||||||
|
containers.
|
||||||
|
- **[specialization-calculator/](specialization-calculator/)** — single-file
|
||||||
|
HTML calculator for spec-track XP / quests / days.
|
||||||
|
|
||||||
|
## Source data
|
||||||
|
|
||||||
|
The character builder is built from saved HTML snapshots of
|
||||||
|
[dune.gaming.tools](https://dune.gaming.tools) that live in `sample-data/`.
|
||||||
|
That directory is **gitignored** (~19 MB of raw HTML).
|
||||||
|
|
||||||
|
To regenerate `character-builder/data/*.json`:
|
||||||
|
|
||||||
|
1. Save the relevant pages (right-click → "Save Page As → Web Page Complete")
|
||||||
|
into `sample-data/`.
|
||||||
|
2. From `character-builder/`, run `uv run python3 scripts/extract.py`.
|
||||||
|
|
||||||
|
The runtime JSON (`character-builder/data/*.json`) **is** committed so the app
|
||||||
|
builds without the raw HTML.
|
||||||
|
|
||||||
|
## Disclaimer
|
||||||
|
|
||||||
|
Unofficial fan project. Not affiliated with Funcom or any rights holder.
|
||||||
5
character-builder/.dockerignore
Normal file
5
character-builder/.dockerignore
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
**/node_modules
|
||||||
|
**/dist
|
||||||
|
**/.DS_Store
|
||||||
|
.git
|
||||||
|
*.log
|
||||||
37
character-builder/Dockerfile
Normal file
37
character-builder/Dockerfile
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
# syntax=docker/dockerfile:1.7
|
||||||
|
|
||||||
|
# ---------- frontend build ----------
|
||||||
|
FROM node:22-alpine AS frontend-build
|
||||||
|
WORKDIR /app/frontend
|
||||||
|
COPY frontend/package.json ./
|
||||||
|
RUN npm install --no-audit --no-fund
|
||||||
|
COPY frontend/ ./
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# ---------- backend build ----------
|
||||||
|
FROM node:22-alpine AS backend-build
|
||||||
|
WORKDIR /app/backend
|
||||||
|
COPY backend/package.json ./
|
||||||
|
RUN npm install --no-audit --no-fund
|
||||||
|
COPY backend/ ./
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# ---------- runtime ----------
|
||||||
|
FROM node:22-alpine AS runtime
|
||||||
|
ENV NODE_ENV=production
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Backend prod deps only.
|
||||||
|
COPY backend/package.json ./
|
||||||
|
RUN npm install --omit=dev --no-audit --no-fund
|
||||||
|
|
||||||
|
# Compiled backend, built frontend, baked data.
|
||||||
|
COPY --from=backend-build /app/backend/dist ./dist
|
||||||
|
COPY --from=frontend-build /app/frontend/dist ./public
|
||||||
|
COPY data ./data
|
||||||
|
|
||||||
|
ENV STATIC_ROOT=/app/public
|
||||||
|
ENV DATA_ROOT=/app/data
|
||||||
|
ENV PORT=3000
|
||||||
|
EXPOSE 3000
|
||||||
|
CMD ["node", "dist/main.js"]
|
||||||
56
character-builder/README.md
Normal file
56
character-builder/README.md
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
# Dune Awakening — Character Builder
|
||||||
|
|
||||||
|
Plan your character across House, Class, Character XP, Specializations, Faction
|
||||||
|
Standing, and Skill Trees. Shareable via short link.
|
||||||
|
|
||||||
|
## Stack
|
||||||
|
|
||||||
|
- **Frontend**: Vue 3 + Vite + TypeScript
|
||||||
|
- **Backend**: NestJS (Node 22, TypeScript)
|
||||||
|
- **Storage**: Valkey (Redis-compatible) for build sharing
|
||||||
|
- **Data**: extracted from `../sample-data/*.html` into `./data/*.json` via `scripts/extract.py`
|
||||||
|
|
||||||
|
Two containers: `app` (NestJS serving the SPA + API) and `valkey`. Wire-up in
|
||||||
|
`docker-compose.yml`.
|
||||||
|
|
||||||
|
## Run
|
||||||
|
|
||||||
|
```sh
|
||||||
|
docker compose up --build
|
||||||
|
```
|
||||||
|
|
||||||
|
Then open <http://localhost:8080>.
|
||||||
|
|
||||||
|
## Update data
|
||||||
|
|
||||||
|
If you save new HTML snapshots into `../sample-data/`, re-run the extractor:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
uv run python3 scripts/extract.py
|
||||||
|
```
|
||||||
|
|
||||||
|
This regenerates `./data/*.json`, which the container picks up on next build.
|
||||||
|
|
||||||
|
## API
|
||||||
|
|
||||||
|
- `POST /api/builds` — body is a build payload (any JSON ≤16 KB). Returns
|
||||||
|
`{ code }` (8-char URL-safe id).
|
||||||
|
- `GET /api/builds/:code` — returns `{ code, build }`.
|
||||||
|
- `GET /api/data/:file` — serves the static JSON tables (allow-listed).
|
||||||
|
|
||||||
|
Builds are stored in Valkey with a rolling 1-year TTL (refreshed on each
|
||||||
|
fetch), so popular shared builds don't expire.
|
||||||
|
|
||||||
|
## Dev (without Docker)
|
||||||
|
|
||||||
|
In two terminals:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# backend
|
||||||
|
cd backend && npm install && VALKEY_URL=redis://localhost:6379 npm run start:dev
|
||||||
|
|
||||||
|
# frontend
|
||||||
|
cd frontend && npm install && npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
The Vite dev server proxies `/api/*` to `localhost:3000`.
|
||||||
8
character-builder/backend/nest-cli.json
Normal file
8
character-builder/backend/nest-cli.json
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://json.schemastore.org/nest-cli",
|
||||||
|
"collection": "@nestjs/schematics",
|
||||||
|
"sourceRoot": "src",
|
||||||
|
"compilerOptions": {
|
||||||
|
"deleteOutDir": true
|
||||||
|
}
|
||||||
|
}
|
||||||
27
character-builder/backend/package.json
Normal file
27
character-builder/backend/package.json
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
{
|
||||||
|
"name": "dune-character-builder-backend",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"build": "nest build",
|
||||||
|
"start": "node dist/main.js",
|
||||||
|
"start:dev": "nest start --watch",
|
||||||
|
"lint": "eslint \"src/**/*.ts\""
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@nestjs/common": "^10.4.15",
|
||||||
|
"@nestjs/core": "^10.4.15",
|
||||||
|
"@nestjs/platform-express": "^10.4.15",
|
||||||
|
"@nestjs/serve-static": "^4.0.2",
|
||||||
|
"ioredis": "^5.4.2",
|
||||||
|
"nanoid": "^3.3.8",
|
||||||
|
"reflect-metadata": "^0.2.2",
|
||||||
|
"rxjs": "^7.8.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@nestjs/cli": "^10.4.9",
|
||||||
|
"@types/express": "^5.0.0",
|
||||||
|
"@types/node": "^22.10.5",
|
||||||
|
"typescript": "^5.7.3"
|
||||||
|
}
|
||||||
|
}
|
||||||
19
character-builder/backend/src/app.module.ts
Normal file
19
character-builder/backend/src/app.module.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { ServeStaticModule } from '@nestjs/serve-static';
|
||||||
|
import { join } from 'path';
|
||||||
|
import { BuildsModule } from './builds/builds.module';
|
||||||
|
import { DataModule } from './data/data.module';
|
||||||
|
|
||||||
|
const staticRoot = process.env.STATIC_ROOT || join(__dirname, '..', 'public');
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [
|
||||||
|
ServeStaticModule.forRoot({
|
||||||
|
rootPath: staticRoot,
|
||||||
|
exclude: ['/api/(.*)'],
|
||||||
|
}),
|
||||||
|
BuildsModule,
|
||||||
|
DataModule,
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class AppModule {}
|
||||||
18
character-builder/backend/src/builds/builds.controller.ts
Normal file
18
character-builder/backend/src/builds/builds.controller.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { Body, Controller, Get, Param, Post } from '@nestjs/common';
|
||||||
|
import { BuildsService } from './builds.service';
|
||||||
|
|
||||||
|
@Controller('builds')
|
||||||
|
export class BuildsController {
|
||||||
|
constructor(private readonly builds: BuildsService) {}
|
||||||
|
|
||||||
|
@Post()
|
||||||
|
create(@Body() body: unknown) {
|
||||||
|
return this.builds.save(body);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get(':code')
|
||||||
|
async fetch(@Param('code') code: string) {
|
||||||
|
const build = await this.builds.load(code);
|
||||||
|
return { code, build };
|
||||||
|
}
|
||||||
|
}
|
||||||
10
character-builder/backend/src/builds/builds.module.ts
Normal file
10
character-builder/backend/src/builds/builds.module.ts
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { BuildsController } from './builds.controller';
|
||||||
|
import { BuildsService } from './builds.service';
|
||||||
|
import { ValkeyProvider } from '../valkey.provider';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [BuildsController],
|
||||||
|
providers: [BuildsService, ValkeyProvider],
|
||||||
|
})
|
||||||
|
export class BuildsModule {}
|
||||||
56
character-builder/backend/src/builds/builds.service.ts
Normal file
56
character-builder/backend/src/builds/builds.service.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
||||||
|
import {
|
||||||
|
BadRequestException,
|
||||||
|
Inject,
|
||||||
|
Injectable,
|
||||||
|
NotFoundException,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { customAlphabet } from 'nanoid';
|
||||||
|
import type Redis from 'ioredis';
|
||||||
|
import { VALKEY } from '../valkey.provider';
|
||||||
|
|
||||||
|
// URL-safe, unambiguous-ish alphabet (no 0/O/1/I/l). 8 chars = 38^8 ≈ 4.4e12 codes.
|
||||||
|
const nanoid = customAlphabet(
|
||||||
|
'23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz',
|
||||||
|
8,
|
||||||
|
);
|
||||||
|
|
||||||
|
const MAX_BYTES = 16 * 1024; // 16 KB cap per shared build
|
||||||
|
const TTL_SECONDS = 60 * 60 * 24 * 365; // 1 year
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class BuildsService {
|
||||||
|
constructor(@Inject(VALKEY) private readonly redis: Redis) {}
|
||||||
|
|
||||||
|
async save(payload: unknown): Promise<{ code: string }> {
|
||||||
|
const json = JSON.stringify(payload);
|
||||||
|
if (!json || json === 'null') {
|
||||||
|
throw new BadRequestException('empty build');
|
||||||
|
}
|
||||||
|
if (Buffer.byteLength(json, 'utf8') > MAX_BYTES) {
|
||||||
|
throw new BadRequestException(`build too large (>${MAX_BYTES} bytes)`);
|
||||||
|
}
|
||||||
|
// Up to 5 retries on collision.
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
const code = nanoid();
|
||||||
|
const key = `build:${code}`;
|
||||||
|
const ok = await this.redis.set(key, json, 'EX', TTL_SECONDS, 'NX');
|
||||||
|
if (ok === 'OK') return { code };
|
||||||
|
}
|
||||||
|
throw new BadRequestException('failed to allocate code');
|
||||||
|
}
|
||||||
|
|
||||||
|
async load(code: string): Promise<unknown> {
|
||||||
|
if (!/^[2-9A-HJ-NP-Za-km-z]{8}$/.test(code)) {
|
||||||
|
throw new BadRequestException('invalid code');
|
||||||
|
}
|
||||||
|
const json = await this.redis.get(`build:${code}`);
|
||||||
|
if (!json) throw new NotFoundException('build not found');
|
||||||
|
// refresh TTL so popular builds stay alive
|
||||||
|
await this.redis.expire(`build:${code}`, TTL_SECONDS);
|
||||||
|
try {
|
||||||
|
return JSON.parse(json);
|
||||||
|
} catch {
|
||||||
|
throw new BadRequestException('corrupt build');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
49
character-builder/backend/src/data/data.controller.ts
Normal file
49
character-builder/backend/src/data/data.controller.ts
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
import {
|
||||||
|
BadRequestException,
|
||||||
|
Controller,
|
||||||
|
Get,
|
||||||
|
Header,
|
||||||
|
NotFoundException,
|
||||||
|
Param,
|
||||||
|
Res,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import type { Response } from 'express';
|
||||||
|
import { createReadStream, existsSync } from 'fs';
|
||||||
|
import { join } from 'path';
|
||||||
|
|
||||||
|
const DATA_ROOT = process.env.DATA_ROOT || join(__dirname, '..', '..', 'data');
|
||||||
|
|
||||||
|
// Allow-list of known filenames — guards against path traversal.
|
||||||
|
const ALLOWED = new Set<string>([
|
||||||
|
'index.json',
|
||||||
|
'character-xp.json',
|
||||||
|
'spec-combat.json',
|
||||||
|
'spec-crafting.json',
|
||||||
|
'spec-exploration.json',
|
||||||
|
'spec-gathering.json',
|
||||||
|
'spec-sabotage.json',
|
||||||
|
'faction-atreides.json',
|
||||||
|
'faction-harkonnen.json',
|
||||||
|
'skills-benegesserit.json',
|
||||||
|
'skills-mentat.json',
|
||||||
|
'skills-planetologist.json',
|
||||||
|
'skills-swordmaster.json',
|
||||||
|
'skills-trooper.json',
|
||||||
|
]);
|
||||||
|
|
||||||
|
@Controller('data')
|
||||||
|
export class DataController {
|
||||||
|
@Get(':file')
|
||||||
|
@Header('Content-Type', 'application/json; charset=utf-8')
|
||||||
|
@Header('Cache-Control', 'public, max-age=3600')
|
||||||
|
one(@Param('file') file: string, @Res() res: Response) {
|
||||||
|
if (!ALLOWED.has(file)) {
|
||||||
|
throw new BadRequestException('unknown data file');
|
||||||
|
}
|
||||||
|
const path = join(DATA_ROOT, file);
|
||||||
|
if (!existsSync(path)) {
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
createReadStream(path).pipe(res);
|
||||||
|
}
|
||||||
|
}
|
||||||
7
character-builder/backend/src/data/data.module.ts
Normal file
7
character-builder/backend/src/data/data.module.ts
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { DataController } from './data.controller';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
controllers: [DataController],
|
||||||
|
})
|
||||||
|
export class DataModule {}
|
||||||
12
character-builder/backend/src/main.ts
Normal file
12
character-builder/backend/src/main.ts
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import 'reflect-metadata';
|
||||||
|
import { NestFactory } from '@nestjs/core';
|
||||||
|
import { AppModule } from './app.module';
|
||||||
|
|
||||||
|
async function bootstrap() {
|
||||||
|
const app = await NestFactory.create(AppModule);
|
||||||
|
app.setGlobalPrefix('api', { exclude: [] });
|
||||||
|
const port = Number(process.env.PORT) || 3000;
|
||||||
|
await app.listen(port, '0.0.0.0');
|
||||||
|
console.log(`[dune-character-builder] listening on :${port}`);
|
||||||
|
}
|
||||||
|
bootstrap();
|
||||||
19
character-builder/backend/src/valkey.provider.ts
Normal file
19
character-builder/backend/src/valkey.provider.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { Logger, Provider } from '@nestjs/common';
|
||||||
|
import Redis from 'ioredis';
|
||||||
|
|
||||||
|
export const VALKEY = Symbol('VALKEY');
|
||||||
|
|
||||||
|
export const ValkeyProvider: Provider = {
|
||||||
|
provide: VALKEY,
|
||||||
|
useFactory: (): Redis => {
|
||||||
|
const url = process.env.VALKEY_URL || 'redis://valkey:6379';
|
||||||
|
const logger = new Logger('Valkey');
|
||||||
|
const client = new Redis(url, {
|
||||||
|
lazyConnect: false,
|
||||||
|
maxRetriesPerRequest: 3,
|
||||||
|
});
|
||||||
|
client.on('connect', () => logger.log(`connected ${url}`));
|
||||||
|
client.on('error', (err) => logger.warn(`error: ${err.message}`));
|
||||||
|
return client;
|
||||||
|
},
|
||||||
|
};
|
||||||
23
character-builder/backend/tsconfig.json
Normal file
23
character-builder/backend/tsconfig.json
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"module": "commonjs",
|
||||||
|
"target": "ES2022",
|
||||||
|
"moduleResolution": "node",
|
||||||
|
"outDir": "./dist",
|
||||||
|
"baseUrl": "./",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"strict": true,
|
||||||
|
"strictPropertyInitialization": false,
|
||||||
|
"noImplicitAny": false,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"declaration": false,
|
||||||
|
"sourceMap": true,
|
||||||
|
"removeComments": true,
|
||||||
|
"incremental": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
1813
character-builder/data/character-xp.json
Normal file
1813
character-builder/data/character-xp.json
Normal file
File diff suppressed because it is too large
Load diff
136
character-builder/data/faction-atreides.json
Normal file
136
character-builder/data/faction-atreides.json
Normal file
|
|
@ -0,0 +1,136 @@
|
||||||
|
{
|
||||||
|
"header": [
|
||||||
|
"Tier",
|
||||||
|
"Name",
|
||||||
|
"Required Rep",
|
||||||
|
"Cumulative"
|
||||||
|
],
|
||||||
|
"tiers": [
|
||||||
|
{
|
||||||
|
"tier": 0,
|
||||||
|
"name": "Outsider",
|
||||||
|
"standingRequired": 0,
|
||||||
|
"totalStanding": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tier": 1,
|
||||||
|
"name": "Mercenary",
|
||||||
|
"standingRequired": 99,
|
||||||
|
"totalStanding": 99
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tier": 2,
|
||||||
|
"name": "Recruit",
|
||||||
|
"standingRequired": 150,
|
||||||
|
"totalStanding": 249
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tier": 3,
|
||||||
|
"name": "Contractor",
|
||||||
|
"standingRequired": 250,
|
||||||
|
"totalStanding": 499
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tier": 4,
|
||||||
|
"name": "Agent",
|
||||||
|
"standingRequired": 500,
|
||||||
|
"totalStanding": 999
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tier": 5,
|
||||||
|
"name": "House Operator",
|
||||||
|
"standingRequired": 1000,
|
||||||
|
"totalStanding": 1999
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tier": 6,
|
||||||
|
"name": "-",
|
||||||
|
"standingRequired": 225,
|
||||||
|
"totalStanding": 2224
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tier": 7,
|
||||||
|
"name": "-",
|
||||||
|
"standingRequired": 300,
|
||||||
|
"totalStanding": 2524
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tier": 8,
|
||||||
|
"name": "-",
|
||||||
|
"standingRequired": 375,
|
||||||
|
"totalStanding": 2899
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tier": 9,
|
||||||
|
"name": "-",
|
||||||
|
"standingRequired": 450,
|
||||||
|
"totalStanding": 3349
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tier": 10,
|
||||||
|
"name": "-",
|
||||||
|
"standingRequired": 525,
|
||||||
|
"totalStanding": 3874
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tier": 11,
|
||||||
|
"name": "-",
|
||||||
|
"standingRequired": 600,
|
||||||
|
"totalStanding": 4474
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tier": 12,
|
||||||
|
"name": "-",
|
||||||
|
"standingRequired": 675,
|
||||||
|
"totalStanding": 5149
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tier": 13,
|
||||||
|
"name": "-",
|
||||||
|
"standingRequired": 750,
|
||||||
|
"totalStanding": 5899
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tier": 14,
|
||||||
|
"name": "-",
|
||||||
|
"standingRequired": 825,
|
||||||
|
"totalStanding": 6724
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tier": 15,
|
||||||
|
"name": "-",
|
||||||
|
"standingRequired": 900,
|
||||||
|
"totalStanding": 7624
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tier": 16,
|
||||||
|
"name": "-",
|
||||||
|
"standingRequired": 975,
|
||||||
|
"totalStanding": 8599
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tier": 17,
|
||||||
|
"name": "-",
|
||||||
|
"standingRequired": 1050,
|
||||||
|
"totalStanding": 9649
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tier": 18,
|
||||||
|
"name": "-",
|
||||||
|
"standingRequired": 1125,
|
||||||
|
"totalStanding": 10774
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tier": 19,
|
||||||
|
"name": "-",
|
||||||
|
"standingRequired": 1200,
|
||||||
|
"totalStanding": 11974
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tier": 20,
|
||||||
|
"name": "Envoy",
|
||||||
|
"standingRequired": 500,
|
||||||
|
"totalStanding": 12474
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
136
character-builder/data/faction-harkonnen.json
Normal file
136
character-builder/data/faction-harkonnen.json
Normal file
|
|
@ -0,0 +1,136 @@
|
||||||
|
{
|
||||||
|
"header": [
|
||||||
|
"Tier",
|
||||||
|
"Name",
|
||||||
|
"Required Rep",
|
||||||
|
"Cumulative"
|
||||||
|
],
|
||||||
|
"tiers": [
|
||||||
|
{
|
||||||
|
"tier": 0,
|
||||||
|
"name": "Outsider",
|
||||||
|
"standingRequired": 0,
|
||||||
|
"totalStanding": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tier": 1,
|
||||||
|
"name": "Mercenary",
|
||||||
|
"standingRequired": 99,
|
||||||
|
"totalStanding": 99
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tier": 2,
|
||||||
|
"name": "Recruit",
|
||||||
|
"standingRequired": 150,
|
||||||
|
"totalStanding": 249
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tier": 3,
|
||||||
|
"name": "Contractor",
|
||||||
|
"standingRequired": 250,
|
||||||
|
"totalStanding": 499
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tier": 4,
|
||||||
|
"name": "Agent",
|
||||||
|
"standingRequired": 500,
|
||||||
|
"totalStanding": 999
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tier": 5,
|
||||||
|
"name": "House Operator",
|
||||||
|
"standingRequired": 1000,
|
||||||
|
"totalStanding": 1999
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tier": 6,
|
||||||
|
"name": "-",
|
||||||
|
"standingRequired": 225,
|
||||||
|
"totalStanding": 2224
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tier": 7,
|
||||||
|
"name": "-",
|
||||||
|
"standingRequired": 300,
|
||||||
|
"totalStanding": 2524
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tier": 8,
|
||||||
|
"name": "-",
|
||||||
|
"standingRequired": 375,
|
||||||
|
"totalStanding": 2899
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tier": 9,
|
||||||
|
"name": "-",
|
||||||
|
"standingRequired": 450,
|
||||||
|
"totalStanding": 3349
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tier": 10,
|
||||||
|
"name": "-",
|
||||||
|
"standingRequired": 525,
|
||||||
|
"totalStanding": 3874
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tier": 11,
|
||||||
|
"name": "-",
|
||||||
|
"standingRequired": 600,
|
||||||
|
"totalStanding": 4474
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tier": 12,
|
||||||
|
"name": "-",
|
||||||
|
"standingRequired": 675,
|
||||||
|
"totalStanding": 5149
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tier": 13,
|
||||||
|
"name": "-",
|
||||||
|
"standingRequired": 750,
|
||||||
|
"totalStanding": 5899
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tier": 14,
|
||||||
|
"name": "-",
|
||||||
|
"standingRequired": 825,
|
||||||
|
"totalStanding": 6724
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tier": 15,
|
||||||
|
"name": "-",
|
||||||
|
"standingRequired": 900,
|
||||||
|
"totalStanding": 7624
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tier": 16,
|
||||||
|
"name": "-",
|
||||||
|
"standingRequired": 975,
|
||||||
|
"totalStanding": 8599
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tier": 17,
|
||||||
|
"name": "-",
|
||||||
|
"standingRequired": 1050,
|
||||||
|
"totalStanding": 9649
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tier": 18,
|
||||||
|
"name": "-",
|
||||||
|
"standingRequired": 1125,
|
||||||
|
"totalStanding": 10774
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tier": 19,
|
||||||
|
"name": "-",
|
||||||
|
"standingRequired": 1200,
|
||||||
|
"totalStanding": 11974
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tier": 20,
|
||||||
|
"name": "Enforcer",
|
||||||
|
"standingRequired": 500,
|
||||||
|
"totalStanding": 12474
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
51
character-builder/data/index.json
Normal file
51
character-builder/data/index.json
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
{
|
||||||
|
"xp": {
|
||||||
|
"character": "character-xp.json",
|
||||||
|
"combat": "spec-combat.json",
|
||||||
|
"crafting": "spec-crafting.json",
|
||||||
|
"exploration": "spec-exploration.json",
|
||||||
|
"gathering": "spec-gathering.json",
|
||||||
|
"sabotage": "spec-sabotage.json"
|
||||||
|
},
|
||||||
|
"factions": {
|
||||||
|
"atreides": "faction-atreides.json",
|
||||||
|
"harkonnen": "faction-harkonnen.json"
|
||||||
|
},
|
||||||
|
"skills": [
|
||||||
|
{
|
||||||
|
"id": "benegesserit",
|
||||||
|
"name": "Bene Gesserit",
|
||||||
|
"file": "skills-benegesserit.json",
|
||||||
|
"nodes": 22,
|
||||||
|
"edges": 23
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "mentat",
|
||||||
|
"name": "Mentat",
|
||||||
|
"file": "skills-mentat.json",
|
||||||
|
"nodes": 22,
|
||||||
|
"edges": 22
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "planetologist",
|
||||||
|
"name": "Planetologist",
|
||||||
|
"file": "skills-planetologist.json",
|
||||||
|
"nodes": 20,
|
||||||
|
"edges": 10
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "swordmaster",
|
||||||
|
"name": "Swordmaster",
|
||||||
|
"file": "skills-swordmaster.json",
|
||||||
|
"nodes": 22,
|
||||||
|
"edges": 22
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "trooper",
|
||||||
|
"name": "Trooper",
|
||||||
|
"file": "skills-trooper.json",
|
||||||
|
"nodes": 22,
|
||||||
|
"edges": 22
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
342
character-builder/data/skills-benegesserit.json
Normal file
342
character-builder/data/skills-benegesserit.json
Normal file
|
|
@ -0,0 +1,342 @@
|
||||||
|
{
|
||||||
|
"id": "benegesserit",
|
||||||
|
"name": "Bene Gesserit",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"tag": "Skills.Spice.BinduDodge",
|
||||||
|
"id": "BinduDodge",
|
||||||
|
"name": "Bindu Dodge",
|
||||||
|
"kind": "Spice",
|
||||||
|
"row": 1,
|
||||||
|
"col": 2,
|
||||||
|
"maxPoints": 1,
|
||||||
|
"icon": "t_ui_iconskilltreebindudodge_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-spice-bindudodge"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Ability.BinduNerveStrike",
|
||||||
|
"id": "BinduNerveStrike",
|
||||||
|
"name": "Prana-Bindu Strikes",
|
||||||
|
"kind": "Ability",
|
||||||
|
"row": 2,
|
||||||
|
"col": 1,
|
||||||
|
"maxPoints": 1,
|
||||||
|
"icon": "t_ui_iconabilitybindunervestrike_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-ability-bindunervestrike"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Ability.WeirdingStep",
|
||||||
|
"id": "WeirdingStep",
|
||||||
|
"name": "Weirding Step",
|
||||||
|
"kind": "Ability",
|
||||||
|
"row": 2,
|
||||||
|
"col": 3,
|
||||||
|
"maxPoints": 1,
|
||||||
|
"icon": "t_ui_iconabilityweirdingstep_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-ability-weirdingstep"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Attribute.WeirdingWay2",
|
||||||
|
"id": "WeirdingWay2",
|
||||||
|
"name": "Short Blade Damage",
|
||||||
|
"kind": "Attribute",
|
||||||
|
"row": 3,
|
||||||
|
"col": 2,
|
||||||
|
"maxPoints": 3,
|
||||||
|
"icon": "t_ui_iconskilltreeskillbrawler_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-attribute-weirdingway2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Perk.Backstabber",
|
||||||
|
"id": "Backstabber",
|
||||||
|
"name": "Manipulate Instability",
|
||||||
|
"kind": "Perk",
|
||||||
|
"row": 4,
|
||||||
|
"col": 1,
|
||||||
|
"maxPoints": 3,
|
||||||
|
"icon": "t_ui_iconskilltreeperkbackstabber_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-perk-backstabber"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Attribute.WeirdingWay1",
|
||||||
|
"id": "WeirdingWay1",
|
||||||
|
"name": "Blade Damage",
|
||||||
|
"kind": "Attribute",
|
||||||
|
"row": 4,
|
||||||
|
"col": 3,
|
||||||
|
"maxPoints": 3,
|
||||||
|
"icon": "t_ui_iconskilltreeskillbrawler_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-attribute-weirdingway1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Ability.Hypersprint",
|
||||||
|
"id": "Hypersprint",
|
||||||
|
"name": "Bindu Sprint",
|
||||||
|
"kind": "Ability",
|
||||||
|
"row": 5,
|
||||||
|
"col": 2,
|
||||||
|
"maxPoints": 3,
|
||||||
|
"icon": "t_ui_iconabilitydash_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-ability-hypersprint"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Spice.VoiceSplash",
|
||||||
|
"id": "VoiceSplash",
|
||||||
|
"name": "Screech",
|
||||||
|
"kind": "Spice",
|
||||||
|
"row": 1,
|
||||||
|
"col": 2,
|
||||||
|
"maxPoints": 1,
|
||||||
|
"icon": "t_ui_iconskilltreescreech_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-spice-voicesplash"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Perk.VoiceAnalysis",
|
||||||
|
"id": "VoiceAnalysis",
|
||||||
|
"name": "Rapid Register",
|
||||||
|
"kind": "Perk",
|
||||||
|
"row": 2,
|
||||||
|
"col": 1,
|
||||||
|
"maxPoints": 1,
|
||||||
|
"icon": "t_ui_iconskilltreevoiceanalysis_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-perk-voiceanalysis"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Ability.VoiceStop",
|
||||||
|
"id": "VoiceStop",
|
||||||
|
"name": "Stop",
|
||||||
|
"kind": "Ability",
|
||||||
|
"row": 2,
|
||||||
|
"col": 3,
|
||||||
|
"maxPoints": 1,
|
||||||
|
"icon": "t_ui_iconabilityvoicestop_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-ability-voicestop"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Ability.Blindspot",
|
||||||
|
"id": "Blindspot",
|
||||||
|
"name": "Ignore",
|
||||||
|
"kind": "Ability",
|
||||||
|
"row": 4,
|
||||||
|
"col": 1,
|
||||||
|
"maxPoints": 1,
|
||||||
|
"icon": "t_ui_iconabilityblindspot_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-ability-blindspot"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Attribute.Manipulation1",
|
||||||
|
"id": "Manipulation1",
|
||||||
|
"name": "Voice Training",
|
||||||
|
"kind": "Attribute",
|
||||||
|
"row": 4,
|
||||||
|
"col": 3,
|
||||||
|
"maxPoints": 3,
|
||||||
|
"icon": "t_ui_iconskilltreebenegesseritcooldown_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-attribute-manipulation1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Ability.VoiceCompel",
|
||||||
|
"id": "VoiceCompel",
|
||||||
|
"name": "Compel",
|
||||||
|
"kind": "Ability",
|
||||||
|
"row": 5,
|
||||||
|
"col": 2,
|
||||||
|
"maxPoints": 1,
|
||||||
|
"icon": "t_ui_iconabilitythevoicecompel_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-ability-voicecompel"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Ability.LitanyAgainstFear",
|
||||||
|
"id": "LitanyAgainstFear",
|
||||||
|
"name": "Litany Against Fear",
|
||||||
|
"kind": "Ability",
|
||||||
|
"row": 1,
|
||||||
|
"col": 3,
|
||||||
|
"maxPoints": 3,
|
||||||
|
"icon": "t_ui_iconabilitylitanyagainstfear_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-ability-litanyagainstfear"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Perk.BinduStability",
|
||||||
|
"id": "BinduStability",
|
||||||
|
"name": "Prana-Bindu Stability",
|
||||||
|
"kind": "Perk",
|
||||||
|
"row": 2,
|
||||||
|
"col": 2,
|
||||||
|
"maxPoints": 3,
|
||||||
|
"icon": "t_ui_iconskilltreebindustability_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-perk-bindustability"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Perk.MetabolizePoison",
|
||||||
|
"id": "MetabolizePoison",
|
||||||
|
"name": "Metabolize Poison",
|
||||||
|
"kind": "Perk",
|
||||||
|
"row": 2,
|
||||||
|
"col": 4,
|
||||||
|
"maxPoints": 1,
|
||||||
|
"icon": "t_ui_iconskilltreemetabolizeposion_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-perk-metabolizepoison"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Attribute.SelfControl3",
|
||||||
|
"id": "SelfControl3",
|
||||||
|
"name": "Vitality",
|
||||||
|
"kind": "Attribute",
|
||||||
|
"row": 3,
|
||||||
|
"col": 1,
|
||||||
|
"maxPoints": 3,
|
||||||
|
"icon": "t_ui_iconskilltreeattributemaxhpbonus_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-attribute-selfcontrol3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Attribute.SelfControl4",
|
||||||
|
"id": "SelfControl4",
|
||||||
|
"name": "Self-Healing",
|
||||||
|
"kind": "Attribute",
|
||||||
|
"row": 3,
|
||||||
|
"col": 3,
|
||||||
|
"maxPoints": 3,
|
||||||
|
"icon": "t_ui_iconskilltreeskillmaxhealth_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-attribute-selfcontrol4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Attribute.SelfControl5",
|
||||||
|
"id": "SelfControl5",
|
||||||
|
"name": "Poison Tolerance",
|
||||||
|
"kind": "Attribute",
|
||||||
|
"row": 3,
|
||||||
|
"col": 5,
|
||||||
|
"maxPoints": 3,
|
||||||
|
"icon": "t_ui_iconskilltreeattributepoisondefense_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-attribute-selfcontrol5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Perk.RegenCap",
|
||||||
|
"id": "RegenCap",
|
||||||
|
"name": "Trauma Recovery",
|
||||||
|
"kind": "Perk",
|
||||||
|
"row": 4,
|
||||||
|
"col": 2,
|
||||||
|
"maxPoints": 3,
|
||||||
|
"icon": "t_ui_iconskilltreeperkhealingfactor_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-perk-regencap"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Attribute.SelfControl2",
|
||||||
|
"id": "SelfControl2",
|
||||||
|
"name": "Sun Tolerance",
|
||||||
|
"kind": "Attribute",
|
||||||
|
"row": 4,
|
||||||
|
"col": 4,
|
||||||
|
"maxPoints": 3,
|
||||||
|
"icon": "t_ui_iconskilltreeattributesundefense_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-attribute-selfcontrol2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Attribute.SelfControl1",
|
||||||
|
"id": "SelfControl1",
|
||||||
|
"name": "Recovery",
|
||||||
|
"kind": "Attribute",
|
||||||
|
"row": 5,
|
||||||
|
"col": 3,
|
||||||
|
"maxPoints": 3,
|
||||||
|
"icon": "t_ui_iconskilltreeskillhealingmultiplier_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-attribute-selfcontrol1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"edges": [
|
||||||
|
{
|
||||||
|
"from": "Skills.Ability.BinduNerveStrike",
|
||||||
|
"to": "Skills.Spice.BinduDodge"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Ability.WeirdingStep",
|
||||||
|
"to": "Skills.Spice.BinduDodge"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Ability.BinduNerveStrike",
|
||||||
|
"to": "Skills.Attribute.WeirdingWay2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Ability.BinduNerveStrike",
|
||||||
|
"to": "Skills.Perk.Backstabber"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Ability.WeirdingStep",
|
||||||
|
"to": "Skills.Attribute.WeirdingWay2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Ability.WeirdingStep",
|
||||||
|
"to": "Skills.Attribute.WeirdingWay1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Attribute.WeirdingWay2",
|
||||||
|
"to": "Skills.Perk.Backstabber"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Attribute.WeirdingWay1",
|
||||||
|
"to": "Skills.Attribute.WeirdingWay2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Ability.Hypersprint",
|
||||||
|
"to": "Skills.Perk.Backstabber"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Attribute.WeirdingWay1",
|
||||||
|
"to": "Skills.Perk.Backstabber"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Ability.Hypersprint",
|
||||||
|
"to": "Skills.Attribute.WeirdingWay1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Ability.LitanyAgainstFear",
|
||||||
|
"to": "Skills.Perk.BinduStability"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Ability.LitanyAgainstFear",
|
||||||
|
"to": "Skills.Perk.MetabolizePoison"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Attribute.SelfControl3",
|
||||||
|
"to": "Skills.Perk.BinduStability"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Attribute.SelfControl4",
|
||||||
|
"to": "Skills.Perk.BinduStability"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Attribute.SelfControl4",
|
||||||
|
"to": "Skills.Perk.MetabolizePoison"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Attribute.SelfControl5",
|
||||||
|
"to": "Skills.Perk.MetabolizePoison"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Attribute.SelfControl3",
|
||||||
|
"to": "Skills.Perk.RegenCap"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Attribute.SelfControl4",
|
||||||
|
"to": "Skills.Perk.RegenCap"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Attribute.SelfControl2",
|
||||||
|
"to": "Skills.Attribute.SelfControl4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Attribute.SelfControl2",
|
||||||
|
"to": "Skills.Attribute.SelfControl5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Attribute.SelfControl1",
|
||||||
|
"to": "Skills.Perk.RegenCap"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Attribute.SelfControl1",
|
||||||
|
"to": "Skills.Attribute.SelfControl2"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
338
character-builder/data/skills-mentat.json
Normal file
338
character-builder/data/skills-mentat.json
Normal file
|
|
@ -0,0 +1,338 @@
|
||||||
|
{
|
||||||
|
"id": "mentat",
|
||||||
|
"name": "Mentat",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"tag": "Skills.Perk.ShieldWeakpoint",
|
||||||
|
"id": "ShieldWeakpoint",
|
||||||
|
"name": "Shield Overcharge",
|
||||||
|
"kind": "Perk",
|
||||||
|
"row": 1,
|
||||||
|
"col": 3,
|
||||||
|
"maxPoints": 1,
|
||||||
|
"icon": "t_ui_iconskilltreeshieldovercharge_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-perk-shieldweakpoint"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Perk.ExploitWeakness",
|
||||||
|
"id": "ExploitWeakness",
|
||||||
|
"name": "Exploit Weakness",
|
||||||
|
"kind": "Perk",
|
||||||
|
"row": 2,
|
||||||
|
"col": 2,
|
||||||
|
"maxPoints": 1,
|
||||||
|
"icon": "t_ui_iconskilltreespiceeffectexploitweakness_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-perk-exploitweakness"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Attribute.MentalCalculus5",
|
||||||
|
"id": "MentalCalculus5",
|
||||||
|
"name": "Rifle Damage",
|
||||||
|
"kind": "Attribute",
|
||||||
|
"row": 2,
|
||||||
|
"col": 4,
|
||||||
|
"maxPoints": 3,
|
||||||
|
"icon": "t_ui_iconskilltreeattributedamagebonusscattergun_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-attribute-mentalcalculus5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Attribute.MentalCalculus3",
|
||||||
|
"id": "MentalCalculus3",
|
||||||
|
"name": "Tailoring",
|
||||||
|
"kind": "Attribute",
|
||||||
|
"row": 3,
|
||||||
|
"col": 1,
|
||||||
|
"maxPoints": 3,
|
||||||
|
"icon": "t_ui_iconskilltreeattributerepairefficiency_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-attribute-mentalcalculus3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Perk.HeadShots",
|
||||||
|
"id": "HeadShots",
|
||||||
|
"name": "Marksman",
|
||||||
|
"kind": "Perk",
|
||||||
|
"row": 3,
|
||||||
|
"col": 3,
|
||||||
|
"maxPoints": 3,
|
||||||
|
"icon": "t_ui_iconskilltreeperkmarksman_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-perk-headshots"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Attribute.MentalCalculus4",
|
||||||
|
"id": "MentalCalculus4",
|
||||||
|
"name": "Pistol Damage",
|
||||||
|
"kind": "Attribute",
|
||||||
|
"row": 3,
|
||||||
|
"col": 5,
|
||||||
|
"maxPoints": 3,
|
||||||
|
"icon": "t_ui_iconskilltreeattributedamagebonusgun_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-attribute-mentalcalculus4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Attribute.MentalCalculus1",
|
||||||
|
"id": "MentalCalculus1",
|
||||||
|
"name": "Garment Keeper",
|
||||||
|
"kind": "Attribute",
|
||||||
|
"row": 4,
|
||||||
|
"col": 2,
|
||||||
|
"maxPoints": 3,
|
||||||
|
"icon": "t_ui_iconskilltreeattributerepair_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-attribute-mentalcalculus1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Attribute.MentalCalculus2",
|
||||||
|
"id": "MentalCalculus2",
|
||||||
|
"name": "Ranged Damage",
|
||||||
|
"kind": "Attribute",
|
||||||
|
"row": 4,
|
||||||
|
"col": 4,
|
||||||
|
"maxPoints": 3,
|
||||||
|
"icon": "t_ui_iconskilltreeattributedamage_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-attribute-mentalcalculus2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Ability.TurretSeeker",
|
||||||
|
"id": "TurretSeeker",
|
||||||
|
"name": "The Sentinel",
|
||||||
|
"kind": "Ability",
|
||||||
|
"row": 5,
|
||||||
|
"col": 3,
|
||||||
|
"maxPoints": 3,
|
||||||
|
"icon": "t_ui_iconabilityturretseeker_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-ability-turretseeker"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Ability.HunterSeeker",
|
||||||
|
"id": "HunterSeeker",
|
||||||
|
"name": "Hunter-Seeker",
|
||||||
|
"kind": "Ability",
|
||||||
|
"row": 1,
|
||||||
|
"col": 2,
|
||||||
|
"maxPoints": 1,
|
||||||
|
"icon": "t_ui_icongadgethunterseeker_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-ability-hunterseeker"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Perk.PoisonTooth",
|
||||||
|
"id": "PoisonTooth",
|
||||||
|
"name": "Poison Tooth",
|
||||||
|
"kind": "Perk",
|
||||||
|
"row": 2,
|
||||||
|
"col": 1,
|
||||||
|
"maxPoints": 3,
|
||||||
|
"icon": "t_ui_iconskilltreepoisontooth_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-perk-poisontooth"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Ability.StunDart",
|
||||||
|
"id": "StunDart",
|
||||||
|
"name": "Stunner",
|
||||||
|
"kind": "Ability",
|
||||||
|
"row": 2,
|
||||||
|
"col": 3,
|
||||||
|
"maxPoints": 1,
|
||||||
|
"icon": "t_ui_iconabilitystunnerdart_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-ability-stundart"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Attribute.Assassination2",
|
||||||
|
"id": "Assassination2",
|
||||||
|
"name": "Assassin's Shot",
|
||||||
|
"kind": "Attribute",
|
||||||
|
"row": 3,
|
||||||
|
"col": 2,
|
||||||
|
"maxPoints": 3,
|
||||||
|
"icon": "t_ui_iconskilltreeattributedamage_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-attribute-assassination2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Ability.PoisonMine",
|
||||||
|
"id": "PoisonMine",
|
||||||
|
"name": "Poison Mine",
|
||||||
|
"kind": "Ability",
|
||||||
|
"row": 4,
|
||||||
|
"col": 1,
|
||||||
|
"maxPoints": 3,
|
||||||
|
"icon": "t_ui_iconabilitypoisonmine_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-ability-poisonmine"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Attribute.Assassination1",
|
||||||
|
"id": "Assassination1",
|
||||||
|
"name": "Headshot Damage",
|
||||||
|
"kind": "Attribute",
|
||||||
|
"row": 4,
|
||||||
|
"col": 3,
|
||||||
|
"maxPoints": 3,
|
||||||
|
"icon": "t_ui_iconskilltreeattributeheadshotbonus_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-attribute-assassination1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Ability.PoisonCapsuleLauncher",
|
||||||
|
"id": "PoisonCapsuleLauncher",
|
||||||
|
"name": "Poison Capsule",
|
||||||
|
"kind": "Ability",
|
||||||
|
"row": 5,
|
||||||
|
"col": 2,
|
||||||
|
"maxPoints": 3,
|
||||||
|
"icon": "t_ui_icongadgetpoisoncapsulelauncher_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-ability-poisoncapsulelauncher"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Ability.PortableGenerator",
|
||||||
|
"id": "PortableGenerator",
|
||||||
|
"name": "Source of Power",
|
||||||
|
"kind": "Ability",
|
||||||
|
"row": 1,
|
||||||
|
"col": 2,
|
||||||
|
"maxPoints": 1,
|
||||||
|
"icon": "t_ui_iconabilityportablegenerator_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-ability-portablegenerator"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Ability.SuspensorMine_Reduction",
|
||||||
|
"id": "SuspensorMine_Reduction",
|
||||||
|
"name": "Anti-gravity Mine",
|
||||||
|
"kind": "Ability",
|
||||||
|
"row": 2,
|
||||||
|
"col": 1,
|
||||||
|
"maxPoints": 1,
|
||||||
|
"icon": "t_ui_icongadgetreductionremotemine_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-ability-suspensormine_reduction"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Perk.IronWill",
|
||||||
|
"id": "IronWill",
|
||||||
|
"name": "Iron Will",
|
||||||
|
"kind": "Perk",
|
||||||
|
"row": 2,
|
||||||
|
"col": 3,
|
||||||
|
"maxPoints": 1,
|
||||||
|
"icon": "t_ui_iconskilltreeironwill_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-perk-ironwill"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Ability.SuspensorMine_Amplification",
|
||||||
|
"id": "SuspensorMine_Amplification",
|
||||||
|
"name": "Gravity Mine",
|
||||||
|
"kind": "Ability",
|
||||||
|
"row": 4,
|
||||||
|
"col": 1,
|
||||||
|
"maxPoints": 1,
|
||||||
|
"icon": "t_ui_icongadgetamplificationremotemine_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-ability-suspensormine_amplification"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Ability.SolidoDecoy",
|
||||||
|
"id": "SolidoDecoy",
|
||||||
|
"name": "Solido Decoy",
|
||||||
|
"kind": "Ability",
|
||||||
|
"row": 4,
|
||||||
|
"col": 3,
|
||||||
|
"maxPoints": 1,
|
||||||
|
"icon": "t_ui_iconabilitysolidodecoy_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-ability-solidodecoy"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Ability.SuspensorWall",
|
||||||
|
"id": "SuspensorWall",
|
||||||
|
"name": "Shield Wall",
|
||||||
|
"kind": "Ability",
|
||||||
|
"row": 5,
|
||||||
|
"col": 2,
|
||||||
|
"maxPoints": 3,
|
||||||
|
"icon": "t_ui_iconabilitysuspensorwall_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-ability-suspensorwall"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"edges": [
|
||||||
|
{
|
||||||
|
"from": "Skills.Perk.ExploitWeakness",
|
||||||
|
"to": "Skills.Perk.ShieldWeakpoint"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Attribute.MentalCalculus5",
|
||||||
|
"to": "Skills.Perk.ShieldWeakpoint"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Attribute.MentalCalculus3",
|
||||||
|
"to": "Skills.Perk.ExploitWeakness"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Perk.ExploitWeakness",
|
||||||
|
"to": "Skills.Perk.HeadShots"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Attribute.MentalCalculus5",
|
||||||
|
"to": "Skills.Perk.HeadShots"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Attribute.MentalCalculus4",
|
||||||
|
"to": "Skills.Attribute.MentalCalculus5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Attribute.MentalCalculus1",
|
||||||
|
"to": "Skills.Attribute.MentalCalculus3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Attribute.MentalCalculus1",
|
||||||
|
"to": "Skills.Perk.HeadShots"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Attribute.MentalCalculus2",
|
||||||
|
"to": "Skills.Perk.HeadShots"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Attribute.MentalCalculus2",
|
||||||
|
"to": "Skills.Attribute.MentalCalculus4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Ability.TurretSeeker",
|
||||||
|
"to": "Skills.Attribute.MentalCalculus1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Ability.TurretSeeker",
|
||||||
|
"to": "Skills.Attribute.MentalCalculus2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Ability.HunterSeeker",
|
||||||
|
"to": "Skills.Perk.PoisonTooth"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Ability.HunterSeeker",
|
||||||
|
"to": "Skills.Ability.StunDart"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Attribute.Assassination2",
|
||||||
|
"to": "Skills.Perk.PoisonTooth"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Ability.PoisonMine",
|
||||||
|
"to": "Skills.Perk.PoisonTooth"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Ability.StunDart",
|
||||||
|
"to": "Skills.Attribute.Assassination2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Ability.StunDart",
|
||||||
|
"to": "Skills.Attribute.Assassination1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Ability.PoisonMine",
|
||||||
|
"to": "Skills.Attribute.Assassination2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Attribute.Assassination1",
|
||||||
|
"to": "Skills.Attribute.Assassination2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Ability.PoisonCapsuleLauncher",
|
||||||
|
"to": "Skills.Ability.PoisonMine"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Ability.PoisonCapsuleLauncher",
|
||||||
|
"to": "Skills.Attribute.Assassination1"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
268
character-builder/data/skills-planetologist.json
Normal file
268
character-builder/data/skills-planetologist.json
Normal file
|
|
@ -0,0 +1,268 @@
|
||||||
|
{
|
||||||
|
"id": "planetologist",
|
||||||
|
"name": "Planetologist",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"tag": "Skills.Perk.BatteryExpert",
|
||||||
|
"id": "BatteryExpert",
|
||||||
|
"name": "Conservation of Energy",
|
||||||
|
"kind": "Perk",
|
||||||
|
"row": 1,
|
||||||
|
"col": 2,
|
||||||
|
"maxPoints": 3,
|
||||||
|
"icon": "t_ui_iconskilltreeperkbatteryexpert_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-perk-batteryexpert"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Attribute.Scientist5",
|
||||||
|
"id": "Scientist5",
|
||||||
|
"name": "Compaction",
|
||||||
|
"kind": "Attribute",
|
||||||
|
"row": 2,
|
||||||
|
"col": 1,
|
||||||
|
"maxPoints": 3,
|
||||||
|
"icon": "t_ui_iconskilltreeattributespiceyield_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-attribute-scientist5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Science.m_PowerMax",
|
||||||
|
"id": "m_PowerMax",
|
||||||
|
"name": "Overcharge",
|
||||||
|
"kind": "Science",
|
||||||
|
"row": 2,
|
||||||
|
"col": 3,
|
||||||
|
"maxPoints": 3,
|
||||||
|
"icon": "t_ui_iconskilltreeattributemineralyield_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-science-m_powermax"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Attribute.Scientist4",
|
||||||
|
"id": "Scientist4",
|
||||||
|
"name": "Deep Analysis",
|
||||||
|
"kind": "Attribute",
|
||||||
|
"row": 3,
|
||||||
|
"col": 2,
|
||||||
|
"maxPoints": 3,
|
||||||
|
"icon": "t_ui_iconskilltreeattributemineralyield_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-attribute-scientist4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Attribute.Scientist2",
|
||||||
|
"id": "Scientist2",
|
||||||
|
"name": "Dew Gathering",
|
||||||
|
"kind": "Attribute",
|
||||||
|
"row": 4,
|
||||||
|
"col": 1,
|
||||||
|
"maxPoints": 3,
|
||||||
|
"icon": "t_ui_iconskilltreeattributewatheryield_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-attribute-scientist2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Attribute.Scientist3",
|
||||||
|
"id": "Scientist3",
|
||||||
|
"name": "Rerouting",
|
||||||
|
"kind": "Attribute",
|
||||||
|
"row": 4,
|
||||||
|
"col": 3,
|
||||||
|
"maxPoints": 3,
|
||||||
|
"icon": "t_ui_iconskilltreeskillpowerefficiency_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-attribute-scientist3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Attribute.Scientist1",
|
||||||
|
"id": "Scientist1",
|
||||||
|
"name": "Cutteray Mining",
|
||||||
|
"kind": "Attribute",
|
||||||
|
"row": 5,
|
||||||
|
"col": 2,
|
||||||
|
"maxPoints": 3,
|
||||||
|
"icon": "t_ui_iconskilltreeattributemineralyield_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-attribute-scientist1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Attribute.Explorer5",
|
||||||
|
"id": "Explorer5",
|
||||||
|
"name": "Spice Surveyor",
|
||||||
|
"kind": "Attribute",
|
||||||
|
"row": 1,
|
||||||
|
"col": 2,
|
||||||
|
"maxPoints": 1,
|
||||||
|
"icon": "t_ui_iconskilltreeattributespice_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-attribute-explorer5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Attribute.Explorer3",
|
||||||
|
"id": "Explorer3",
|
||||||
|
"name": "Scanner Mastery",
|
||||||
|
"kind": "Attribute",
|
||||||
|
"row": 2,
|
||||||
|
"col": 1,
|
||||||
|
"maxPoints": 3,
|
||||||
|
"icon": "t_ui_iconskilltreeattributescanningbonus_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-attribute-explorer3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Attribute.Explorer4",
|
||||||
|
"id": "Explorer4",
|
||||||
|
"name": "Stillsuit Seals",
|
||||||
|
"kind": "Attribute",
|
||||||
|
"row": 2,
|
||||||
|
"col": 3,
|
||||||
|
"maxPoints": 3,
|
||||||
|
"icon": "t_ui_iconskilltreeskillhydration_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-attribute-explorer4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Attribute.Explorer1",
|
||||||
|
"id": "Explorer1",
|
||||||
|
"name": "Cartographer",
|
||||||
|
"kind": "Attribute",
|
||||||
|
"row": 4,
|
||||||
|
"col": 1,
|
||||||
|
"maxPoints": 1,
|
||||||
|
"icon": "t_ui_iconskilltreeskillobservation_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-attribute-explorer1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Attribute.Explorer2",
|
||||||
|
"id": "Explorer2",
|
||||||
|
"name": "Mountaineer",
|
||||||
|
"kind": "Attribute",
|
||||||
|
"row": 4,
|
||||||
|
"col": 3,
|
||||||
|
"maxPoints": 3,
|
||||||
|
"icon": "t_ui_iconskilltreeskillclimber_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-attribute-explorer2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Ability.SuspensorPad",
|
||||||
|
"id": "SuspensorPad",
|
||||||
|
"name": "Suspensor Pad",
|
||||||
|
"kind": "Ability",
|
||||||
|
"row": 5,
|
||||||
|
"col": 2,
|
||||||
|
"maxPoints": 1,
|
||||||
|
"icon": "t_ui_iconabilitysuspensorpad_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-ability-suspensorpad"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Spice.VehicleHeat",
|
||||||
|
"id": "VehicleHeat",
|
||||||
|
"name": "Heat Management",
|
||||||
|
"kind": "Spice",
|
||||||
|
"row": 1,
|
||||||
|
"col": 2,
|
||||||
|
"maxPoints": 1,
|
||||||
|
"icon": "t_ui_iconskilltreeheatmanagement_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-spice-vehicleheat"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Attribute.Driver5",
|
||||||
|
"id": "Driver5",
|
||||||
|
"name": "Fuel Efficient Pilot",
|
||||||
|
"kind": "Attribute",
|
||||||
|
"row": 2,
|
||||||
|
"col": 1,
|
||||||
|
"maxPoints": 3,
|
||||||
|
"icon": "t_ui_iconskilltreeattributevehicle_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-attribute-driver5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Attribute.Driver6",
|
||||||
|
"id": "Driver6",
|
||||||
|
"name": "Sandcrawler Yield",
|
||||||
|
"kind": "Attribute",
|
||||||
|
"row": 2,
|
||||||
|
"col": 3,
|
||||||
|
"maxPoints": 3,
|
||||||
|
"icon": "t_ui_iconskilltreeattributespiceyield_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-attribute-driver6"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Attribute.Driver4",
|
||||||
|
"id": "Driver4",
|
||||||
|
"name": "Vehicle Scanning",
|
||||||
|
"kind": "Attribute",
|
||||||
|
"row": 3,
|
||||||
|
"col": 2,
|
||||||
|
"maxPoints": 3,
|
||||||
|
"icon": "t_ui_iconskilltreeattributescanningbonus_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-attribute-driver4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Attribute.Driver2",
|
||||||
|
"id": "Driver2",
|
||||||
|
"name": "Fuel Efficient Driver",
|
||||||
|
"kind": "Attribute",
|
||||||
|
"row": 4,
|
||||||
|
"col": 1,
|
||||||
|
"maxPoints": 3,
|
||||||
|
"icon": "t_ui_iconskilltreeattributevehicle_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-attribute-driver2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Attribute.Driver3",
|
||||||
|
"id": "Driver3",
|
||||||
|
"name": "Vehicle Mining",
|
||||||
|
"kind": "Attribute",
|
||||||
|
"row": 4,
|
||||||
|
"col": 3,
|
||||||
|
"maxPoints": 3,
|
||||||
|
"icon": "t_ui_iconskilltreeattributemineralyield_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-attribute-driver3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Attribute.Driver1",
|
||||||
|
"id": "Driver1",
|
||||||
|
"name": "Vehicle Repair",
|
||||||
|
"kind": "Attribute",
|
||||||
|
"row": 5,
|
||||||
|
"col": 2,
|
||||||
|
"maxPoints": 3,
|
||||||
|
"icon": "t_ui_iconskilltreeattributerepairefficiency_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-attribute-driver1"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"edges": [
|
||||||
|
{
|
||||||
|
"from": "Skills.Attribute.Scientist5",
|
||||||
|
"to": "Skills.Perk.BatteryExpert"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Perk.BatteryExpert",
|
||||||
|
"to": "Skills.Science.m_PowerMax"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Attribute.Scientist4",
|
||||||
|
"to": "Skills.Attribute.Scientist5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Attribute.Scientist2",
|
||||||
|
"to": "Skills.Attribute.Scientist5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Attribute.Scientist4",
|
||||||
|
"to": "Skills.Science.m_PowerMax"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Attribute.Scientist3",
|
||||||
|
"to": "Skills.Science.m_PowerMax"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Attribute.Scientist2",
|
||||||
|
"to": "Skills.Attribute.Scientist4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Attribute.Scientist3",
|
||||||
|
"to": "Skills.Attribute.Scientist4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Attribute.Scientist1",
|
||||||
|
"to": "Skills.Attribute.Scientist2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Attribute.Scientist1",
|
||||||
|
"to": "Skills.Attribute.Scientist3"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
338
character-builder/data/skills-swordmaster.json
Normal file
338
character-builder/data/skills-swordmaster.json
Normal file
|
|
@ -0,0 +1,338 @@
|
||||||
|
{
|
||||||
|
"id": "swordmaster",
|
||||||
|
"name": "Swordmaster",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"tag": "Skills.Spice.ParryBoost",
|
||||||
|
"id": "ParryBoost",
|
||||||
|
"name": "Precise Parry",
|
||||||
|
"kind": "Spice",
|
||||||
|
"row": 1,
|
||||||
|
"col": 2,
|
||||||
|
"maxPoints": 3,
|
||||||
|
"icon": "t_ui_iconskilltreepreciseparry_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-spice-parryboost"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Ability.Whirlwind",
|
||||||
|
"id": "Whirlwind",
|
||||||
|
"name": "Eye of the Storm",
|
||||||
|
"kind": "Ability",
|
||||||
|
"row": 2,
|
||||||
|
"col": 1,
|
||||||
|
"maxPoints": 3,
|
||||||
|
"icon": "t_ui_iconabilitywhirlwind_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-ability-whirlwind"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Ability.RiposteBreak",
|
||||||
|
"id": "RiposteBreak",
|
||||||
|
"name": "Foil",
|
||||||
|
"kind": "Ability",
|
||||||
|
"row": 2,
|
||||||
|
"col": 3,
|
||||||
|
"maxPoints": 1,
|
||||||
|
"icon": "t_ui_iconabilitybreakingreposte_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-ability-ripostebreak"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Attribute.Blade2",
|
||||||
|
"id": "Blade2",
|
||||||
|
"name": "Long Blade Damage",
|
||||||
|
"kind": "Attribute",
|
||||||
|
"row": 3,
|
||||||
|
"col": 2,
|
||||||
|
"maxPoints": 3,
|
||||||
|
"icon": "t_ui_iconskilltreeskillbrawler_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-attribute-blade2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Perk.MeleeChain",
|
||||||
|
"id": "MeleeChain",
|
||||||
|
"name": "Dance of Blades",
|
||||||
|
"kind": "Perk",
|
||||||
|
"row": 4,
|
||||||
|
"col": 1,
|
||||||
|
"maxPoints": 3,
|
||||||
|
"icon": "t_ui_iconskilltreeperkbladechaining_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-perk-meleechain"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Ability.RiposteInjure",
|
||||||
|
"id": "RiposteInjure",
|
||||||
|
"name": "Retaliate",
|
||||||
|
"kind": "Ability",
|
||||||
|
"row": 4,
|
||||||
|
"col": 3,
|
||||||
|
"maxPoints": 1,
|
||||||
|
"icon": "t_ui_iconabilityinjuringreposte_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-ability-riposteinjure"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Attribute.Blade1",
|
||||||
|
"id": "Blade1",
|
||||||
|
"name": "Blade Damage",
|
||||||
|
"kind": "Attribute",
|
||||||
|
"row": 5,
|
||||||
|
"col": 2,
|
||||||
|
"maxPoints": 3,
|
||||||
|
"icon": "t_ui_iconskilltreeskillbrawler_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-attribute-blade1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Perk.ThriveOnDanger",
|
||||||
|
"id": "ThriveOnDanger",
|
||||||
|
"name": "Thrive on Danger",
|
||||||
|
"kind": "Perk",
|
||||||
|
"row": 1,
|
||||||
|
"col": 2,
|
||||||
|
"maxPoints": 1,
|
||||||
|
"icon": "t_ui_iconskilltreethriveondanger_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-perk-thriveondanger"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Attribute.Resolve2",
|
||||||
|
"id": "Resolve2",
|
||||||
|
"name": "Solid Stance",
|
||||||
|
"kind": "Attribute",
|
||||||
|
"row": 2,
|
||||||
|
"col": 1,
|
||||||
|
"maxPoints": 3,
|
||||||
|
"icon": "t_ui_iconskilltreeattributepoisedefense_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-attribute-resolve2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Attribute.UnstoppableAttacks",
|
||||||
|
"id": "UnstoppableAttacks",
|
||||||
|
"name": "Confidence",
|
||||||
|
"kind": "Attribute",
|
||||||
|
"row": 2,
|
||||||
|
"col": 3,
|
||||||
|
"maxPoints": 3,
|
||||||
|
"icon": "t_ui_iconskilltreeattributedamagemitigation_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-attribute-unstoppableattacks"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Attribute.Resolve1",
|
||||||
|
"id": "Resolve1",
|
||||||
|
"name": "Bleed Tolerance",
|
||||||
|
"kind": "Attribute",
|
||||||
|
"row": 4,
|
||||||
|
"col": 1,
|
||||||
|
"maxPoints": 3,
|
||||||
|
"icon": "t_ui_iconskilltreeskillmaxhealth_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-attribute-resolve1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Perk.ToughLunge",
|
||||||
|
"id": "ToughLunge",
|
||||||
|
"name": "Reckless Lunge",
|
||||||
|
"kind": "Perk",
|
||||||
|
"row": 4,
|
||||||
|
"col": 3,
|
||||||
|
"maxPoints": 3,
|
||||||
|
"icon": "t_ui_iconskilltreetoughlunge_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-perk-toughlunge"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Ability.DeflectionSlow",
|
||||||
|
"id": "DeflectionSlow",
|
||||||
|
"name": "Deflection",
|
||||||
|
"kind": "Ability",
|
||||||
|
"row": 5,
|
||||||
|
"col": 2,
|
||||||
|
"maxPoints": 1,
|
||||||
|
"icon": "t_ui_iconabilitydeflection_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-ability-deflectionslow"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Spice.ShadowStrike",
|
||||||
|
"id": "ShadowStrike",
|
||||||
|
"name": "Prescient Strike",
|
||||||
|
"kind": "Spice",
|
||||||
|
"row": 1,
|
||||||
|
"col": 3,
|
||||||
|
"maxPoints": 1,
|
||||||
|
"icon": "t_ui_iconskilltreeprescientstrike_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-spice-shadowstrike"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Attribute.Aggression3",
|
||||||
|
"id": "Aggression3",
|
||||||
|
"name": "General Conditioning",
|
||||||
|
"kind": "Attribute",
|
||||||
|
"row": 2,
|
||||||
|
"col": 2,
|
||||||
|
"maxPoints": 3,
|
||||||
|
"icon": "t_ui_iconskilltreeattributestamina_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-attribute-aggression3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Attribute.Aggression4",
|
||||||
|
"id": "Aggression4",
|
||||||
|
"name": "Desert Conditioning",
|
||||||
|
"kind": "Attribute",
|
||||||
|
"row": 2,
|
||||||
|
"col": 4,
|
||||||
|
"maxPoints": 3,
|
||||||
|
"icon": "t_ui_iconskilltreeattributewatherdefense_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-attribute-aggression4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Ability.CripplingStrike",
|
||||||
|
"id": "CripplingStrike",
|
||||||
|
"name": "Crippling Strike",
|
||||||
|
"kind": "Ability",
|
||||||
|
"row": 3,
|
||||||
|
"col": 1,
|
||||||
|
"maxPoints": 1,
|
||||||
|
"icon": "t_ui_iconabilitycripplingstrike_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-ability-cripplingstrike"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Perk.SprintStamina",
|
||||||
|
"id": "SprintStamina",
|
||||||
|
"name": "Disciplined Breathing",
|
||||||
|
"kind": "Perk",
|
||||||
|
"row": 3,
|
||||||
|
"col": 3,
|
||||||
|
"maxPoints": 3,
|
||||||
|
"icon": "t_ui_iconskilltreeperkrunner_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-perk-sprintstamina"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Ability.BattleCry",
|
||||||
|
"id": "BattleCry",
|
||||||
|
"name": "Inspiration",
|
||||||
|
"kind": "Ability",
|
||||||
|
"row": 3,
|
||||||
|
"col": 5,
|
||||||
|
"maxPoints": 3,
|
||||||
|
"icon": "t_ui_iconabilitybattlecry_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-ability-battlecry"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Attribute.Aggression1",
|
||||||
|
"id": "Aggression1",
|
||||||
|
"name": "Field Medicine",
|
||||||
|
"kind": "Attribute",
|
||||||
|
"row": 4,
|
||||||
|
"col": 2,
|
||||||
|
"maxPoints": 3,
|
||||||
|
"icon": "t_ui_iconskilltreeskillhealingmultiplier_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-attribute-aggression1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Attribute.Aggression2",
|
||||||
|
"id": "Aggression2",
|
||||||
|
"name": "Optimized Hydration",
|
||||||
|
"kind": "Attribute",
|
||||||
|
"row": 4,
|
||||||
|
"col": 4,
|
||||||
|
"maxPoints": 3,
|
||||||
|
"icon": "t_ui_iconskilltreeattributewatherbonus_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-attribute-aggression2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Ability.KneeCharge",
|
||||||
|
"id": "KneeCharge",
|
||||||
|
"name": "Knee Charge",
|
||||||
|
"kind": "Ability",
|
||||||
|
"row": 5,
|
||||||
|
"col": 3,
|
||||||
|
"maxPoints": 3,
|
||||||
|
"icon": "t_ui_iconabilitykneecharge_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-ability-kneecharge"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"edges": [
|
||||||
|
{
|
||||||
|
"from": "Skills.Ability.Whirlwind",
|
||||||
|
"to": "Skills.Spice.ParryBoost"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Ability.RiposteBreak",
|
||||||
|
"to": "Skills.Spice.ParryBoost"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Ability.Whirlwind",
|
||||||
|
"to": "Skills.Attribute.Blade2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Ability.Whirlwind",
|
||||||
|
"to": "Skills.Perk.MeleeChain"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Ability.RiposteBreak",
|
||||||
|
"to": "Skills.Attribute.Blade2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Ability.RiposteBreak",
|
||||||
|
"to": "Skills.Ability.RiposteInjure"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Attribute.Blade2",
|
||||||
|
"to": "Skills.Perk.MeleeChain"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Ability.RiposteInjure",
|
||||||
|
"to": "Skills.Attribute.Blade2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Attribute.Blade1",
|
||||||
|
"to": "Skills.Perk.MeleeChain"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Ability.RiposteInjure",
|
||||||
|
"to": "Skills.Attribute.Blade1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Attribute.Aggression3",
|
||||||
|
"to": "Skills.Spice.ShadowStrike"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Attribute.Aggression4",
|
||||||
|
"to": "Skills.Spice.ShadowStrike"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Ability.CripplingStrike",
|
||||||
|
"to": "Skills.Attribute.Aggression3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Attribute.Aggression3",
|
||||||
|
"to": "Skills.Perk.SprintStamina"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Attribute.Aggression4",
|
||||||
|
"to": "Skills.Perk.SprintStamina"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Ability.BattleCry",
|
||||||
|
"to": "Skills.Attribute.Aggression4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Ability.CripplingStrike",
|
||||||
|
"to": "Skills.Attribute.Aggression1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Attribute.Aggression1",
|
||||||
|
"to": "Skills.Perk.SprintStamina"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Attribute.Aggression2",
|
||||||
|
"to": "Skills.Perk.SprintStamina"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Ability.BattleCry",
|
||||||
|
"to": "Skills.Attribute.Aggression2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Ability.KneeCharge",
|
||||||
|
"to": "Skills.Attribute.Aggression1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Ability.KneeCharge",
|
||||||
|
"to": "Skills.Attribute.Aggression2"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
338
character-builder/data/skills-trooper.json
Normal file
338
character-builder/data/skills-trooper.json
Normal file
|
|
@ -0,0 +1,338 @@
|
||||||
|
{
|
||||||
|
"id": "trooper",
|
||||||
|
"name": "Trooper",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"tag": "Skills.Ability.EnergyCapsule",
|
||||||
|
"id": "EnergyCapsule",
|
||||||
|
"name": "Energy Capsule",
|
||||||
|
"kind": "Ability",
|
||||||
|
"row": 1,
|
||||||
|
"col": 3,
|
||||||
|
"maxPoints": 1,
|
||||||
|
"icon": "t_ui_iconabilityenergycapsule_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-ability-energycapsule"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Attribute.Weaponry5",
|
||||||
|
"id": "Weaponry5",
|
||||||
|
"name": "Heavy Weapon Damage",
|
||||||
|
"kind": "Attribute",
|
||||||
|
"row": 2,
|
||||||
|
"col": 2,
|
||||||
|
"maxPoints": 3,
|
||||||
|
"icon": "t_ui_iconskilltreeattributedamagebonusheavyweapon_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-attribute-weaponry5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Attribute.Weaponry6",
|
||||||
|
"id": "Weaponry6",
|
||||||
|
"name": "Gunsmith",
|
||||||
|
"kind": "Attribute",
|
||||||
|
"row": 2,
|
||||||
|
"col": 4,
|
||||||
|
"maxPoints": 3,
|
||||||
|
"icon": "t_ui_iconskilltreeattributerepairefficiency_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-attribute-weaponry6"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Perk.HeavyWeaponNaib",
|
||||||
|
"id": "HeavyWeaponNaib",
|
||||||
|
"name": "Heavy Weapon Agility",
|
||||||
|
"kind": "Perk",
|
||||||
|
"row": 3,
|
||||||
|
"col": 1,
|
||||||
|
"maxPoints": 3,
|
||||||
|
"icon": "t_ui_iconskilltreeperkheavyweaponnaib_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-perk-heavyweaponnaib"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Attribute.Weaponry3",
|
||||||
|
"id": "Weaponry3",
|
||||||
|
"name": "Scattergun Damage",
|
||||||
|
"kind": "Attribute",
|
||||||
|
"row": 3,
|
||||||
|
"col": 3,
|
||||||
|
"maxPoints": 3,
|
||||||
|
"icon": "t_ui_iconskilltreeattributedamagebonusrifle_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-attribute-weaponry3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Attribute.Weaponry4",
|
||||||
|
"id": "Weaponry4",
|
||||||
|
"name": "Field Maintenance",
|
||||||
|
"kind": "Attribute",
|
||||||
|
"row": 3,
|
||||||
|
"col": 5,
|
||||||
|
"maxPoints": 3,
|
||||||
|
"icon": "t_ui_iconskilltreeattributerepair_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-attribute-weaponry4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Attribute.Weaponry2",
|
||||||
|
"id": "Weaponry2",
|
||||||
|
"name": "Disruptor Damage",
|
||||||
|
"kind": "Attribute",
|
||||||
|
"row": 4,
|
||||||
|
"col": 2,
|
||||||
|
"maxPoints": 3,
|
||||||
|
"icon": "t_ui_iconskilltreeattributedamagebonussmg_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-attribute-weaponry2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Perk.BodyShots",
|
||||||
|
"id": "BodyShots",
|
||||||
|
"name": "Center of Mass",
|
||||||
|
"kind": "Perk",
|
||||||
|
"row": 4,
|
||||||
|
"col": 4,
|
||||||
|
"maxPoints": 3,
|
||||||
|
"icon": "t_ui_iconskilltreeperkcentralaim_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-perk-bodyshots"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Attribute.Weaponry1",
|
||||||
|
"id": "Weaponry1",
|
||||||
|
"name": "Ranged Damage",
|
||||||
|
"kind": "Attribute",
|
||||||
|
"row": 5,
|
||||||
|
"col": 3,
|
||||||
|
"maxPoints": 3,
|
||||||
|
"icon": "t_ui_iconskilltreeattributedamage_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-attribute-weaponry1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Ability.SuspensorBlast",
|
||||||
|
"id": "SuspensorBlast",
|
||||||
|
"name": "Suspensor Blast",
|
||||||
|
"kind": "Ability",
|
||||||
|
"row": 1,
|
||||||
|
"col": 2,
|
||||||
|
"maxPoints": 1,
|
||||||
|
"icon": "t_ui_iconabilitysuspensorblast_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-ability-suspensorblast"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Perk.DeathFromAbove",
|
||||||
|
"id": "DeathFromAbove",
|
||||||
|
"name": "Death from Above",
|
||||||
|
"kind": "Perk",
|
||||||
|
"row": 2,
|
||||||
|
"col": 1,
|
||||||
|
"maxPoints": 3,
|
||||||
|
"icon": "t_ui_iconskilltreeperkdeathfromabove_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-perk-deathfromabove"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Ability.CollapseGrenade",
|
||||||
|
"id": "CollapseGrenade",
|
||||||
|
"name": "Collapse Grenade",
|
||||||
|
"kind": "Ability",
|
||||||
|
"row": 2,
|
||||||
|
"col": 3,
|
||||||
|
"maxPoints": 1,
|
||||||
|
"icon": "t_ui_iconabilitycollapsegrenade_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-ability-collapsegrenade"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Attribute.SuspensorTech1",
|
||||||
|
"id": "SuspensorTech1",
|
||||||
|
"name": "Suspensor Efficiency",
|
||||||
|
"kind": "Attribute",
|
||||||
|
"row": 3,
|
||||||
|
"col": 2,
|
||||||
|
"maxPoints": 3,
|
||||||
|
"icon": "t_ui_iconskilltreeskillpowerefficiency_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-attribute-suspensortech1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Perk.SuspensorDash",
|
||||||
|
"id": "SuspensorDash",
|
||||||
|
"name": "Suspensor Dash",
|
||||||
|
"kind": "Perk",
|
||||||
|
"row": 4,
|
||||||
|
"col": 1,
|
||||||
|
"maxPoints": 1,
|
||||||
|
"icon": "t_ui_iconskilltreesuspensordash_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-perk-suspensordash"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Ability.SuspensorGrenade_Amplification",
|
||||||
|
"id": "SuspensorGrenade_Amplification",
|
||||||
|
"name": "Gravity Field",
|
||||||
|
"kind": "Ability",
|
||||||
|
"row": 4,
|
||||||
|
"col": 3,
|
||||||
|
"maxPoints": 1,
|
||||||
|
"icon": "t_ui_icongadgetamplificationgrenade_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-ability-suspensorgrenade_amplification"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Ability.SuspensorGrenade_Reduction",
|
||||||
|
"id": "SuspensorGrenade_Reduction",
|
||||||
|
"name": "Anti-gravity Field",
|
||||||
|
"kind": "Ability",
|
||||||
|
"row": 5,
|
||||||
|
"col": 2,
|
||||||
|
"maxPoints": 1,
|
||||||
|
"icon": "t_ui_icongadgetreductionsuspensorgrenade_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-ability-suspensorgrenade_reduction"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Spice.GadgetReload",
|
||||||
|
"id": "GadgetReload",
|
||||||
|
"name": "Reflexive Reload",
|
||||||
|
"kind": "Spice",
|
||||||
|
"row": 1,
|
||||||
|
"col": 2,
|
||||||
|
"maxPoints": 1,
|
||||||
|
"icon": "t_ui_iconskilltreereflexivereload_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-spice-gadgetreload"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Ability.AssaultSeeker",
|
||||||
|
"id": "AssaultSeeker",
|
||||||
|
"name": "Assault Seeker",
|
||||||
|
"kind": "Ability",
|
||||||
|
"row": 2,
|
||||||
|
"col": 1,
|
||||||
|
"maxPoints": 3,
|
||||||
|
"icon": "t_ui_iconabilityassaultseeker_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-ability-assaultseeker"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Ability.MagneticAttractor",
|
||||||
|
"id": "MagneticAttractor",
|
||||||
|
"name": "Attractor Field",
|
||||||
|
"kind": "Ability",
|
||||||
|
"row": 2,
|
||||||
|
"col": 3,
|
||||||
|
"maxPoints": 1,
|
||||||
|
"icon": "t_ui_icongadgetshigmultitoolmagneticattractor_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-ability-magneticattractor"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Ability.FragGrenade",
|
||||||
|
"id": "FragGrenade",
|
||||||
|
"name": "Explosive Grenade",
|
||||||
|
"kind": "Ability",
|
||||||
|
"row": 4,
|
||||||
|
"col": 1,
|
||||||
|
"maxPoints": 3,
|
||||||
|
"icon": "t_ui_icongadgetfraggrenades_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-ability-fraggrenade"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Perk.TrooperCooldowns",
|
||||||
|
"id": "TrooperCooldowns",
|
||||||
|
"name": "Battle Hardened",
|
||||||
|
"kind": "Perk",
|
||||||
|
"row": 4,
|
||||||
|
"col": 3,
|
||||||
|
"maxPoints": 3,
|
||||||
|
"icon": "t_ui_iconskilltreetroopercooldown_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-perk-troopercooldowns"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "Skills.Ability.CablePull",
|
||||||
|
"id": "CablePull",
|
||||||
|
"name": "Shigawire Claw",
|
||||||
|
"kind": "Ability",
|
||||||
|
"row": 5,
|
||||||
|
"col": 2,
|
||||||
|
"maxPoints": 3,
|
||||||
|
"icon": "t_ui_icongadgetshigmultitoolsardaukarpull_d.webp",
|
||||||
|
"url": "https://dune.gaming.tools/skills/skills-ability-cablepull"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"edges": [
|
||||||
|
{
|
||||||
|
"from": "Skills.Ability.EnergyCapsule",
|
||||||
|
"to": "Skills.Attribute.Weaponry5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Ability.EnergyCapsule",
|
||||||
|
"to": "Skills.Attribute.Weaponry6"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Attribute.Weaponry5",
|
||||||
|
"to": "Skills.Perk.HeavyWeaponNaib"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Attribute.Weaponry3",
|
||||||
|
"to": "Skills.Attribute.Weaponry5"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Attribute.Weaponry3",
|
||||||
|
"to": "Skills.Attribute.Weaponry6"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Attribute.Weaponry4",
|
||||||
|
"to": "Skills.Attribute.Weaponry6"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Attribute.Weaponry2",
|
||||||
|
"to": "Skills.Perk.HeavyWeaponNaib"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Attribute.Weaponry2",
|
||||||
|
"to": "Skills.Attribute.Weaponry3"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Attribute.Weaponry3",
|
||||||
|
"to": "Skills.Perk.BodyShots"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Attribute.Weaponry4",
|
||||||
|
"to": "Skills.Perk.BodyShots"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Attribute.Weaponry1",
|
||||||
|
"to": "Skills.Attribute.Weaponry2"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Attribute.Weaponry1",
|
||||||
|
"to": "Skills.Perk.BodyShots"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Ability.SuspensorBlast",
|
||||||
|
"to": "Skills.Perk.DeathFromAbove"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Ability.CollapseGrenade",
|
||||||
|
"to": "Skills.Ability.SuspensorBlast"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Attribute.SuspensorTech1",
|
||||||
|
"to": "Skills.Perk.DeathFromAbove"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Perk.DeathFromAbove",
|
||||||
|
"to": "Skills.Perk.SuspensorDash"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Ability.CollapseGrenade",
|
||||||
|
"to": "Skills.Attribute.SuspensorTech1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Ability.CollapseGrenade",
|
||||||
|
"to": "Skills.Ability.SuspensorGrenade_Amplification"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Attribute.SuspensorTech1",
|
||||||
|
"to": "Skills.Perk.SuspensorDash"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Ability.SuspensorGrenade_Amplification",
|
||||||
|
"to": "Skills.Attribute.SuspensorTech1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Ability.SuspensorGrenade_Reduction",
|
||||||
|
"to": "Skills.Perk.SuspensorDash"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"from": "Skills.Ability.SuspensorGrenade_Amplification",
|
||||||
|
"to": "Skills.Ability.SuspensorGrenade_Reduction"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
610
character-builder/data/spec-combat.json
Normal file
610
character-builder/data/spec-combat.json
Normal file
|
|
@ -0,0 +1,610 @@
|
||||||
|
{
|
||||||
|
"header": [
|
||||||
|
"Level",
|
||||||
|
"Required XP",
|
||||||
|
"Cumulative",
|
||||||
|
"Rewards"
|
||||||
|
],
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"level": 1,
|
||||||
|
"xpRequired": 100,
|
||||||
|
"totalXp": 100,
|
||||||
|
"intelPoints": 23
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 2,
|
||||||
|
"xpRequired": 105,
|
||||||
|
"totalXp": 205,
|
||||||
|
"intelPoints": 25
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 3,
|
||||||
|
"xpRequired": 110,
|
||||||
|
"totalXp": 315,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 4,
|
||||||
|
"xpRequired": 116,
|
||||||
|
"totalXp": 431,
|
||||||
|
"intelPoints": 21
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 5,
|
||||||
|
"xpRequired": 122,
|
||||||
|
"totalXp": 553,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 6,
|
||||||
|
"xpRequired": 128,
|
||||||
|
"totalXp": 681,
|
||||||
|
"intelPoints": 215
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 7,
|
||||||
|
"xpRequired": 135,
|
||||||
|
"totalXp": 816,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 8,
|
||||||
|
"xpRequired": 142,
|
||||||
|
"totalXp": 958,
|
||||||
|
"intelPoints": 23
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 9,
|
||||||
|
"xpRequired": 149,
|
||||||
|
"totalXp": 1107,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 10,
|
||||||
|
"xpRequired": 157,
|
||||||
|
"totalXp": 1264,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 11,
|
||||||
|
"xpRequired": 164,
|
||||||
|
"totalXp": 1428,
|
||||||
|
"intelPoints": 51
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 12,
|
||||||
|
"xpRequired": 171,
|
||||||
|
"totalXp": 1599,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 13,
|
||||||
|
"xpRequired": 178,
|
||||||
|
"totalXp": 1777,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 14,
|
||||||
|
"xpRequired": 186,
|
||||||
|
"totalXp": 1963,
|
||||||
|
"intelPoints": 51
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 15,
|
||||||
|
"xpRequired": 194,
|
||||||
|
"totalXp": 2157,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 16,
|
||||||
|
"xpRequired": 202,
|
||||||
|
"totalXp": 2359,
|
||||||
|
"intelPoints": 520
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 17,
|
||||||
|
"xpRequired": 211,
|
||||||
|
"totalXp": 2570,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 18,
|
||||||
|
"xpRequired": 220,
|
||||||
|
"totalXp": 2790,
|
||||||
|
"intelPoints": 53
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 19,
|
||||||
|
"xpRequired": 229,
|
||||||
|
"totalXp": 3019,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 20,
|
||||||
|
"xpRequired": 239,
|
||||||
|
"totalXp": 3258,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 21,
|
||||||
|
"xpRequired": 246,
|
||||||
|
"totalXp": 3504,
|
||||||
|
"intelPoints": 51
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 22,
|
||||||
|
"xpRequired": 253,
|
||||||
|
"totalXp": 3757,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 23,
|
||||||
|
"xpRequired": 260,
|
||||||
|
"totalXp": 4017,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 24,
|
||||||
|
"xpRequired": 268,
|
||||||
|
"totalXp": 4285,
|
||||||
|
"intelPoints": 101
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 25,
|
||||||
|
"xpRequired": 276,
|
||||||
|
"totalXp": 4561,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 26,
|
||||||
|
"xpRequired": 284,
|
||||||
|
"totalXp": 4845,
|
||||||
|
"intelPoints": 105
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 27,
|
||||||
|
"xpRequired": 292,
|
||||||
|
"totalXp": 5137,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 28,
|
||||||
|
"xpRequired": 301,
|
||||||
|
"totalXp": 5438,
|
||||||
|
"intelPoints": 103
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 29,
|
||||||
|
"xpRequired": 310,
|
||||||
|
"totalXp": 5748,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 30,
|
||||||
|
"xpRequired": 319,
|
||||||
|
"totalXp": 6067,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 31,
|
||||||
|
"xpRequired": 326,
|
||||||
|
"totalXp": 6393,
|
||||||
|
"intelPoints": 101
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 32,
|
||||||
|
"xpRequired": 334,
|
||||||
|
"totalXp": 6727,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 33,
|
||||||
|
"xpRequired": 342,
|
||||||
|
"totalXp": 7069,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 34,
|
||||||
|
"xpRequired": 350,
|
||||||
|
"totalXp": 7419,
|
||||||
|
"intelPoints": 101
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 35,
|
||||||
|
"xpRequired": 358,
|
||||||
|
"totalXp": 7777,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 36,
|
||||||
|
"xpRequired": 366,
|
||||||
|
"totalXp": 8143,
|
||||||
|
"intelPoints": 155
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 37,
|
||||||
|
"xpRequired": 375,
|
||||||
|
"totalXp": 8518,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 38,
|
||||||
|
"xpRequired": 384,
|
||||||
|
"totalXp": 8902,
|
||||||
|
"intelPoints": 153
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 39,
|
||||||
|
"xpRequired": 393,
|
||||||
|
"totalXp": 9295,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 40,
|
||||||
|
"xpRequired": 402,
|
||||||
|
"totalXp": 9697,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 41,
|
||||||
|
"xpRequired": 410,
|
||||||
|
"totalXp": 10107,
|
||||||
|
"intelPoints": 151
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 42,
|
||||||
|
"xpRequired": 418,
|
||||||
|
"totalXp": 10525,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 43,
|
||||||
|
"xpRequired": 426,
|
||||||
|
"totalXp": 10951,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 44,
|
||||||
|
"xpRequired": 434,
|
||||||
|
"totalXp": 11385,
|
||||||
|
"intelPoints": 151
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 45,
|
||||||
|
"xpRequired": 442,
|
||||||
|
"totalXp": 11827,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 46,
|
||||||
|
"xpRequired": 450,
|
||||||
|
"totalXp": 12277,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 47,
|
||||||
|
"xpRequired": 459,
|
||||||
|
"totalXp": 12736,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 48,
|
||||||
|
"xpRequired": 468,
|
||||||
|
"totalXp": 13204,
|
||||||
|
"intelPoints": 153
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 49,
|
||||||
|
"xpRequired": 477,
|
||||||
|
"totalXp": 13681,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 50,
|
||||||
|
"xpRequired": 486,
|
||||||
|
"totalXp": 14167,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 51,
|
||||||
|
"xpRequired": 494,
|
||||||
|
"totalXp": 14661,
|
||||||
|
"intelPoints": 201
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 52,
|
||||||
|
"xpRequired": 502,
|
||||||
|
"totalXp": 15163,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 53,
|
||||||
|
"xpRequired": 510,
|
||||||
|
"totalXp": 15673,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 54,
|
||||||
|
"xpRequired": 518,
|
||||||
|
"totalXp": 16191,
|
||||||
|
"intelPoints": 201
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 55,
|
||||||
|
"xpRequired": 526,
|
||||||
|
"totalXp": 16717,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 56,
|
||||||
|
"xpRequired": 534,
|
||||||
|
"totalXp": 17251,
|
||||||
|
"intelPoints": 205
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 57,
|
||||||
|
"xpRequired": 543,
|
||||||
|
"totalXp": 17794,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 58,
|
||||||
|
"xpRequired": 552,
|
||||||
|
"totalXp": 18346,
|
||||||
|
"intelPoints": 203
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 59,
|
||||||
|
"xpRequired": 561,
|
||||||
|
"totalXp": 18907,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 60,
|
||||||
|
"xpRequired": 570,
|
||||||
|
"totalXp": 19477,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 61,
|
||||||
|
"xpRequired": 575,
|
||||||
|
"totalXp": 20052,
|
||||||
|
"intelPoints": 201
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 62,
|
||||||
|
"xpRequired": 580,
|
||||||
|
"totalXp": 20632,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 63,
|
||||||
|
"xpRequired": 585,
|
||||||
|
"totalXp": 21217,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 64,
|
||||||
|
"xpRequired": 590,
|
||||||
|
"totalXp": 21807,
|
||||||
|
"intelPoints": 251
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 65,
|
||||||
|
"xpRequired": 595,
|
||||||
|
"totalXp": 22402,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 66,
|
||||||
|
"xpRequired": 600,
|
||||||
|
"totalXp": 23002,
|
||||||
|
"intelPoints": 255
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 67,
|
||||||
|
"xpRequired": 606,
|
||||||
|
"totalXp": 23608,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 68,
|
||||||
|
"xpRequired": 612,
|
||||||
|
"totalXp": 24220,
|
||||||
|
"intelPoints": 253
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 69,
|
||||||
|
"xpRequired": 618,
|
||||||
|
"totalXp": 24838,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 70,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 25462,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 71,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 26086,
|
||||||
|
"intelPoints": 251
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 72,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 26710,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 73,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 27334,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 74,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 27958,
|
||||||
|
"intelPoints": 251
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 75,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 28582,
|
||||||
|
"intelPoints": 30
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 76,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 29206,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 77,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 29830,
|
||||||
|
"intelPoints": 3025
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 78,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 30454,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 79,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 31078,
|
||||||
|
"intelPoints": 303
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 80,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 31702,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 81,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 32326,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 82,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 32950,
|
||||||
|
"intelPoints": 301
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 83,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 33574,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 84,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 34198,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 85,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 34822,
|
||||||
|
"intelPoints": 301
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 86,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 35446,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 87,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 36070,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 88,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 36694,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 89,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 37318,
|
||||||
|
"intelPoints": 353
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 90,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 37942,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 91,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 38566,
|
||||||
|
"intelPoints": 355
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 92,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 39190,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 93,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 39814,
|
||||||
|
"intelPoints": 351
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 94,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 40438,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 95,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 41062,
|
||||||
|
"intelPoints": 3520
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 96,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 41686,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 97,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 42310,
|
||||||
|
"intelPoints": 351
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 98,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 42934,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 99,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 43558,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 100,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 44182,
|
||||||
|
"intelPoints": 405
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
610
character-builder/data/spec-crafting.json
Normal file
610
character-builder/data/spec-crafting.json
Normal file
|
|
@ -0,0 +1,610 @@
|
||||||
|
{
|
||||||
|
"header": [
|
||||||
|
"Level",
|
||||||
|
"Required XP",
|
||||||
|
"Cumulative",
|
||||||
|
"Rewards"
|
||||||
|
],
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"level": 1,
|
||||||
|
"xpRequired": 100,
|
||||||
|
"totalXp": 100,
|
||||||
|
"intelPoints": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 2,
|
||||||
|
"xpRequired": 105,
|
||||||
|
"totalXp": 205,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 3,
|
||||||
|
"xpRequired": 110,
|
||||||
|
"totalXp": 315,
|
||||||
|
"intelPoints": 21
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 4,
|
||||||
|
"xpRequired": 116,
|
||||||
|
"totalXp": 431,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 5,
|
||||||
|
"xpRequired": 122,
|
||||||
|
"totalXp": 553,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 6,
|
||||||
|
"xpRequired": 128,
|
||||||
|
"totalXp": 681,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 7,
|
||||||
|
"xpRequired": 135,
|
||||||
|
"totalXp": 816,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 8,
|
||||||
|
"xpRequired": 142,
|
||||||
|
"totalXp": 958,
|
||||||
|
"intelPoints": 2250
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 9,
|
||||||
|
"xpRequired": 149,
|
||||||
|
"totalXp": 1107,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 10,
|
||||||
|
"xpRequired": 157,
|
||||||
|
"totalXp": 1264,
|
||||||
|
"intelPoints": 21
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 11,
|
||||||
|
"xpRequired": 164,
|
||||||
|
"totalXp": 1428,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 12,
|
||||||
|
"xpRequired": 171,
|
||||||
|
"totalXp": 1599,
|
||||||
|
"intelPoints": 5100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 13,
|
||||||
|
"xpRequired": 178,
|
||||||
|
"totalXp": 1777,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 14,
|
||||||
|
"xpRequired": 186,
|
||||||
|
"totalXp": 1963,
|
||||||
|
"intelPoints": 5333
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 15,
|
||||||
|
"xpRequired": 194,
|
||||||
|
"totalXp": 2157,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 16,
|
||||||
|
"xpRequired": 202,
|
||||||
|
"totalXp": 2359,
|
||||||
|
"intelPoints": 550
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 17,
|
||||||
|
"xpRequired": 211,
|
||||||
|
"totalXp": 2570,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 18,
|
||||||
|
"xpRequired": 220,
|
||||||
|
"totalXp": 2790,
|
||||||
|
"intelPoints": 5
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 19,
|
||||||
|
"xpRequired": 229,
|
||||||
|
"totalXp": 3019,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 20,
|
||||||
|
"xpRequired": 239,
|
||||||
|
"totalXp": 3258,
|
||||||
|
"intelPoints": 550
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 21,
|
||||||
|
"xpRequired": 246,
|
||||||
|
"totalXp": 3504,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 22,
|
||||||
|
"xpRequired": 253,
|
||||||
|
"totalXp": 3757,
|
||||||
|
"intelPoints": 10300
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 23,
|
||||||
|
"xpRequired": 260,
|
||||||
|
"totalXp": 4017,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 24,
|
||||||
|
"xpRequired": 268,
|
||||||
|
"totalXp": 4285,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 25,
|
||||||
|
"xpRequired": 276,
|
||||||
|
"totalXp": 4561,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 26,
|
||||||
|
"xpRequired": 284,
|
||||||
|
"totalXp": 4845,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 27,
|
||||||
|
"xpRequired": 292,
|
||||||
|
"totalXp": 5137,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 28,
|
||||||
|
"xpRequired": 301,
|
||||||
|
"totalXp": 5438,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 29,
|
||||||
|
"xpRequired": 310,
|
||||||
|
"totalXp": 5748,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 30,
|
||||||
|
"xpRequired": 319,
|
||||||
|
"totalXp": 6067,
|
||||||
|
"intelPoints": 101
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 31,
|
||||||
|
"xpRequired": 326,
|
||||||
|
"totalXp": 6393,
|
||||||
|
"intelPoints": 101
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 32,
|
||||||
|
"xpRequired": 334,
|
||||||
|
"totalXp": 6727,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 33,
|
||||||
|
"xpRequired": 342,
|
||||||
|
"totalXp": 7069,
|
||||||
|
"intelPoints": 1525
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 34,
|
||||||
|
"xpRequired": 350,
|
||||||
|
"totalXp": 7419,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 35,
|
||||||
|
"xpRequired": 358,
|
||||||
|
"totalXp": 7777,
|
||||||
|
"intelPoints": 1550
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 36,
|
||||||
|
"xpRequired": 366,
|
||||||
|
"totalXp": 8143,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 37,
|
||||||
|
"xpRequired": 375,
|
||||||
|
"totalXp": 8518,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 38,
|
||||||
|
"xpRequired": 384,
|
||||||
|
"totalXp": 8902,
|
||||||
|
"intelPoints": 15
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 39,
|
||||||
|
"xpRequired": 393,
|
||||||
|
"totalXp": 9295,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 40,
|
||||||
|
"xpRequired": 402,
|
||||||
|
"totalXp": 9697,
|
||||||
|
"intelPoints": 15333
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 41,
|
||||||
|
"xpRequired": 410,
|
||||||
|
"totalXp": 10107,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 42,
|
||||||
|
"xpRequired": 418,
|
||||||
|
"totalXp": 10525,
|
||||||
|
"intelPoints": 151
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 43,
|
||||||
|
"xpRequired": 426,
|
||||||
|
"totalXp": 10951,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 44,
|
||||||
|
"xpRequired": 434,
|
||||||
|
"totalXp": 11385,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 45,
|
||||||
|
"xpRequired": 442,
|
||||||
|
"totalXp": 11827,
|
||||||
|
"intelPoints": 20100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 46,
|
||||||
|
"xpRequired": 450,
|
||||||
|
"totalXp": 12277,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 47,
|
||||||
|
"xpRequired": 459,
|
||||||
|
"totalXp": 12736,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 48,
|
||||||
|
"xpRequired": 468,
|
||||||
|
"totalXp": 13204,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 49,
|
||||||
|
"xpRequired": 477,
|
||||||
|
"totalXp": 13681,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 50,
|
||||||
|
"xpRequired": 486,
|
||||||
|
"totalXp": 14167,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 51,
|
||||||
|
"xpRequired": 494,
|
||||||
|
"totalXp": 14661,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 52,
|
||||||
|
"xpRequired": 502,
|
||||||
|
"totalXp": 15163,
|
||||||
|
"intelPoints": 20
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 53,
|
||||||
|
"xpRequired": 510,
|
||||||
|
"totalXp": 15673,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 54,
|
||||||
|
"xpRequired": 518,
|
||||||
|
"totalXp": 16191,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 55,
|
||||||
|
"xpRequired": 526,
|
||||||
|
"totalXp": 16717,
|
||||||
|
"intelPoints": 2025
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 56,
|
||||||
|
"xpRequired": 534,
|
||||||
|
"totalXp": 17251,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 57,
|
||||||
|
"xpRequired": 543,
|
||||||
|
"totalXp": 17794,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 58,
|
||||||
|
"xpRequired": 552,
|
||||||
|
"totalXp": 18346,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 59,
|
||||||
|
"xpRequired": 561,
|
||||||
|
"totalXp": 18907,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 60,
|
||||||
|
"xpRequired": 570,
|
||||||
|
"totalXp": 19477,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 61,
|
||||||
|
"xpRequired": 575,
|
||||||
|
"totalXp": 20052,
|
||||||
|
"intelPoints": 25150
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 62,
|
||||||
|
"xpRequired": 580,
|
||||||
|
"totalXp": 20632,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 63,
|
||||||
|
"xpRequired": 585,
|
||||||
|
"totalXp": 21217,
|
||||||
|
"intelPoints": 25300
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 64,
|
||||||
|
"xpRequired": 590,
|
||||||
|
"totalXp": 21807,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 65,
|
||||||
|
"xpRequired": 595,
|
||||||
|
"totalXp": 22402,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 66,
|
||||||
|
"xpRequired": 600,
|
||||||
|
"totalXp": 23002,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 67,
|
||||||
|
"xpRequired": 606,
|
||||||
|
"totalXp": 23608,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 68,
|
||||||
|
"xpRequired": 612,
|
||||||
|
"totalXp": 24220,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 69,
|
||||||
|
"xpRequired": 618,
|
||||||
|
"totalXp": 24838,
|
||||||
|
"intelPoints": 2550
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 70,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 25462,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 71,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 26086,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 72,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 26710,
|
||||||
|
"intelPoints": 25
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 73,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 27334,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 74,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 27958,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 75,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 28582,
|
||||||
|
"intelPoints": 30
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 76,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 29206,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 77,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 29830,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 78,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 30454,
|
||||||
|
"intelPoints": 3050
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 79,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 31078,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 80,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 31702,
|
||||||
|
"intelPoints": 30333
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 81,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 32326,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 82,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 32950,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 83,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 33574,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 84,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 34198,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 85,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 34822,
|
||||||
|
"intelPoints": 30100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 86,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 35446,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 87,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 36070,
|
||||||
|
"intelPoints": 351
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 88,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 36694,
|
||||||
|
"intelPoints": 351
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 89,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 37318,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 90,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 37942,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 91,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 38566,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 92,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 39190,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 93,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 39814,
|
||||||
|
"intelPoints": 35
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 94,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 40438,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 95,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 41062,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 96,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 41686,
|
||||||
|
"intelPoints": 3550
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 97,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 42310,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 98,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 42934,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 99,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 43558,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 100,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 44182,
|
||||||
|
"intelPoints": 40150
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
610
character-builder/data/spec-exploration.json
Normal file
610
character-builder/data/spec-exploration.json
Normal file
|
|
@ -0,0 +1,610 @@
|
||||||
|
{
|
||||||
|
"header": [
|
||||||
|
"Level",
|
||||||
|
"Required XP",
|
||||||
|
"Cumulative",
|
||||||
|
"Rewards"
|
||||||
|
],
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"level": 1,
|
||||||
|
"xpRequired": 100,
|
||||||
|
"totalXp": 100,
|
||||||
|
"intelPoints": 215
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 2,
|
||||||
|
"xpRequired": 105,
|
||||||
|
"totalXp": 205,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 3,
|
||||||
|
"xpRequired": 110,
|
||||||
|
"totalXp": 315,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 4,
|
||||||
|
"xpRequired": 116,
|
||||||
|
"totalXp": 431,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 5,
|
||||||
|
"xpRequired": 122,
|
||||||
|
"totalXp": 553,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 6,
|
||||||
|
"xpRequired": 128,
|
||||||
|
"totalXp": 681,
|
||||||
|
"intelPoints": 250
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 7,
|
||||||
|
"xpRequired": 135,
|
||||||
|
"totalXp": 816,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 8,
|
||||||
|
"xpRequired": 142,
|
||||||
|
"totalXp": 958,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 9,
|
||||||
|
"xpRequired": 149,
|
||||||
|
"totalXp": 1107,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 10,
|
||||||
|
"xpRequired": 157,
|
||||||
|
"totalXp": 1264,
|
||||||
|
"intelPoints": 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 11,
|
||||||
|
"xpRequired": 164,
|
||||||
|
"totalXp": 1428,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 12,
|
||||||
|
"xpRequired": 171,
|
||||||
|
"totalXp": 1599,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 13,
|
||||||
|
"xpRequired": 178,
|
||||||
|
"totalXp": 1777,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 14,
|
||||||
|
"xpRequired": 186,
|
||||||
|
"totalXp": 1963,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 15,
|
||||||
|
"xpRequired": 194,
|
||||||
|
"totalXp": 2157,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 16,
|
||||||
|
"xpRequired": 202,
|
||||||
|
"totalXp": 2359,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 17,
|
||||||
|
"xpRequired": 211,
|
||||||
|
"totalXp": 2570,
|
||||||
|
"intelPoints": 5150
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 18,
|
||||||
|
"xpRequired": 220,
|
||||||
|
"totalXp": 2790,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 19,
|
||||||
|
"xpRequired": 229,
|
||||||
|
"totalXp": 3019,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 20,
|
||||||
|
"xpRequired": 239,
|
||||||
|
"totalXp": 3258,
|
||||||
|
"intelPoints": 5150
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 21,
|
||||||
|
"xpRequired": 246,
|
||||||
|
"totalXp": 3504,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 22,
|
||||||
|
"xpRequired": 253,
|
||||||
|
"totalXp": 3757,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 23,
|
||||||
|
"xpRequired": 260,
|
||||||
|
"totalXp": 4017,
|
||||||
|
"intelPoints": 1050
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 24,
|
||||||
|
"xpRequired": 268,
|
||||||
|
"totalXp": 4285,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 25,
|
||||||
|
"xpRequired": 276,
|
||||||
|
"totalXp": 4561,
|
||||||
|
"intelPoints": 1015
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 26,
|
||||||
|
"xpRequired": 284,
|
||||||
|
"totalXp": 4845,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 27,
|
||||||
|
"xpRequired": 292,
|
||||||
|
"totalXp": 5137,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 28,
|
||||||
|
"xpRequired": 301,
|
||||||
|
"totalXp": 5438,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 29,
|
||||||
|
"xpRequired": 310,
|
||||||
|
"totalXp": 5748,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 30,
|
||||||
|
"xpRequired": 319,
|
||||||
|
"totalXp": 6067,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 31,
|
||||||
|
"xpRequired": 326,
|
||||||
|
"totalXp": 6393,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 32,
|
||||||
|
"xpRequired": 334,
|
||||||
|
"totalXp": 6727,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 33,
|
||||||
|
"xpRequired": 342,
|
||||||
|
"totalXp": 7069,
|
||||||
|
"intelPoints": 10100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 34,
|
||||||
|
"xpRequired": 350,
|
||||||
|
"totalXp": 7419,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 35,
|
||||||
|
"xpRequired": 358,
|
||||||
|
"totalXp": 7777,
|
||||||
|
"intelPoints": 1550
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 36,
|
||||||
|
"xpRequired": 366,
|
||||||
|
"totalXp": 8143,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 37,
|
||||||
|
"xpRequired": 375,
|
||||||
|
"totalXp": 8518,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 38,
|
||||||
|
"xpRequired": 384,
|
||||||
|
"totalXp": 8902,
|
||||||
|
"intelPoints": 15500
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 39,
|
||||||
|
"xpRequired": 393,
|
||||||
|
"totalXp": 9295,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 40,
|
||||||
|
"xpRequired": 402,
|
||||||
|
"totalXp": 9697,
|
||||||
|
"intelPoints": 15
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 41,
|
||||||
|
"xpRequired": 410,
|
||||||
|
"totalXp": 10107,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 42,
|
||||||
|
"xpRequired": 418,
|
||||||
|
"totalXp": 10525,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 43,
|
||||||
|
"xpRequired": 426,
|
||||||
|
"totalXp": 10951,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 44,
|
||||||
|
"xpRequired": 434,
|
||||||
|
"totalXp": 11385,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 45,
|
||||||
|
"xpRequired": 442,
|
||||||
|
"totalXp": 11827,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 46,
|
||||||
|
"xpRequired": 450,
|
||||||
|
"totalXp": 12277,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 47,
|
||||||
|
"xpRequired": 459,
|
||||||
|
"totalXp": 12736,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 48,
|
||||||
|
"xpRequired": 468,
|
||||||
|
"totalXp": 13204,
|
||||||
|
"intelPoints": 2050
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 49,
|
||||||
|
"xpRequired": 477,
|
||||||
|
"totalXp": 13681,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 50,
|
||||||
|
"xpRequired": 486,
|
||||||
|
"totalXp": 14167,
|
||||||
|
"intelPoints": 2015
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 51,
|
||||||
|
"xpRequired": 494,
|
||||||
|
"totalXp": 14661,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 52,
|
||||||
|
"xpRequired": 502,
|
||||||
|
"totalXp": 15163,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 53,
|
||||||
|
"xpRequired": 510,
|
||||||
|
"totalXp": 15673,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 54,
|
||||||
|
"xpRequired": 518,
|
||||||
|
"totalXp": 16191,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 55,
|
||||||
|
"xpRequired": 526,
|
||||||
|
"totalXp": 16717,
|
||||||
|
"intelPoints": 2050
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 56,
|
||||||
|
"xpRequired": 534,
|
||||||
|
"totalXp": 17251,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 57,
|
||||||
|
"xpRequired": 543,
|
||||||
|
"totalXp": 17794,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 58,
|
||||||
|
"xpRequired": 552,
|
||||||
|
"totalXp": 18346,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 59,
|
||||||
|
"xpRequired": 561,
|
||||||
|
"totalXp": 18907,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 60,
|
||||||
|
"xpRequired": 570,
|
||||||
|
"totalXp": 19477,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 61,
|
||||||
|
"xpRequired": 575,
|
||||||
|
"totalXp": 20052,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 62,
|
||||||
|
"xpRequired": 580,
|
||||||
|
"totalXp": 20632,
|
||||||
|
"intelPoints": 2550
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 63,
|
||||||
|
"xpRequired": 585,
|
||||||
|
"totalXp": 21217,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 64,
|
||||||
|
"xpRequired": 590,
|
||||||
|
"totalXp": 21807,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 65,
|
||||||
|
"xpRequired": 595,
|
||||||
|
"totalXp": 22402,
|
||||||
|
"intelPoints": 2550
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 66,
|
||||||
|
"xpRequired": 600,
|
||||||
|
"totalXp": 23002,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 67,
|
||||||
|
"xpRequired": 606,
|
||||||
|
"totalXp": 23608,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 68,
|
||||||
|
"xpRequired": 612,
|
||||||
|
"totalXp": 24220,
|
||||||
|
"intelPoints": 2550
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 69,
|
||||||
|
"xpRequired": 618,
|
||||||
|
"totalXp": 24838,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 70,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 25462,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 71,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 26086,
|
||||||
|
"intelPoints": 25350
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 72,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 26710,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 73,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 27334,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 74,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 27958,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 75,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 28582,
|
||||||
|
"intelPoints": 30
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 76,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 29206,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 77,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 29830,
|
||||||
|
"intelPoints": 3015
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 78,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 30454,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 79,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 31078,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 80,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 31702,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 81,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 32326,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 82,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 32950,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 83,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 33574,
|
||||||
|
"intelPoints": 3050
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 84,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 34198,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 85,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 34822,
|
||||||
|
"intelPoints": 3050
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 86,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 35446,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 87,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 36070,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 88,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 36694,
|
||||||
|
"intelPoints": 35100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 89,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 37318,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 90,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 37942,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 91,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 38566,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 92,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 39190,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 93,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 39814,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 94,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 40438,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 95,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 41062,
|
||||||
|
"intelPoints": 3550
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 96,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 41686,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 97,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 42310,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 98,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 42934,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 99,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 43558,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 100,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 44182,
|
||||||
|
"intelPoints": 4015
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
610
character-builder/data/spec-gathering.json
Normal file
610
character-builder/data/spec-gathering.json
Normal file
|
|
@ -0,0 +1,610 @@
|
||||||
|
{
|
||||||
|
"header": [
|
||||||
|
"Level",
|
||||||
|
"Required XP",
|
||||||
|
"Cumulative",
|
||||||
|
"Rewards"
|
||||||
|
],
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"level": 1,
|
||||||
|
"xpRequired": 100,
|
||||||
|
"totalXp": 100,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 2,
|
||||||
|
"xpRequired": 105,
|
||||||
|
"totalXp": 205,
|
||||||
|
"intelPoints": 2200
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 3,
|
||||||
|
"xpRequired": 110,
|
||||||
|
"totalXp": 315,
|
||||||
|
"intelPoints": 21000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 4,
|
||||||
|
"xpRequired": 116,
|
||||||
|
"totalXp": 431,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 5,
|
||||||
|
"xpRequired": 122,
|
||||||
|
"totalXp": 553,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 6,
|
||||||
|
"xpRequired": 128,
|
||||||
|
"totalXp": 681,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 7,
|
||||||
|
"xpRequired": 135,
|
||||||
|
"totalXp": 816,
|
||||||
|
"intelPoints": 250
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 8,
|
||||||
|
"xpRequired": 142,
|
||||||
|
"totalXp": 958,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 9,
|
||||||
|
"xpRequired": 149,
|
||||||
|
"totalXp": 1107,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 10,
|
||||||
|
"xpRequired": 157,
|
||||||
|
"totalXp": 1264,
|
||||||
|
"intelPoints": 525
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 11,
|
||||||
|
"xpRequired": 164,
|
||||||
|
"totalXp": 1428,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 12,
|
||||||
|
"xpRequired": 171,
|
||||||
|
"totalXp": 1599,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 13,
|
||||||
|
"xpRequired": 178,
|
||||||
|
"totalXp": 1777,
|
||||||
|
"intelPoints": 5100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 14,
|
||||||
|
"xpRequired": 186,
|
||||||
|
"totalXp": 1963,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 15,
|
||||||
|
"xpRequired": 194,
|
||||||
|
"totalXp": 2157,
|
||||||
|
"intelPoints": 51000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 16,
|
||||||
|
"xpRequired": 202,
|
||||||
|
"totalXp": 2359,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 17,
|
||||||
|
"xpRequired": 211,
|
||||||
|
"totalXp": 2570,
|
||||||
|
"intelPoints": 550
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 18,
|
||||||
|
"xpRequired": 220,
|
||||||
|
"totalXp": 2790,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 19,
|
||||||
|
"xpRequired": 229,
|
||||||
|
"totalXp": 3019,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 20,
|
||||||
|
"xpRequired": 239,
|
||||||
|
"totalXp": 3258,
|
||||||
|
"intelPoints": 1025
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 21,
|
||||||
|
"xpRequired": 246,
|
||||||
|
"totalXp": 3504,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 22,
|
||||||
|
"xpRequired": 253,
|
||||||
|
"totalXp": 3757,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 23,
|
||||||
|
"xpRequired": 260,
|
||||||
|
"totalXp": 4017,
|
||||||
|
"intelPoints": 1050
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 24,
|
||||||
|
"xpRequired": 268,
|
||||||
|
"totalXp": 4285,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 25,
|
||||||
|
"xpRequired": 276,
|
||||||
|
"totalXp": 4561,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 26,
|
||||||
|
"xpRequired": 284,
|
||||||
|
"totalXp": 4845,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 27,
|
||||||
|
"xpRequired": 292,
|
||||||
|
"totalXp": 5137,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 28,
|
||||||
|
"xpRequired": 301,
|
||||||
|
"totalXp": 5438,
|
||||||
|
"intelPoints": 1050
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 29,
|
||||||
|
"xpRequired": 310,
|
||||||
|
"totalXp": 5748,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 30,
|
||||||
|
"xpRequired": 319,
|
||||||
|
"totalXp": 6067,
|
||||||
|
"intelPoints": 1025
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 31,
|
||||||
|
"xpRequired": 326,
|
||||||
|
"totalXp": 6393,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 32,
|
||||||
|
"xpRequired": 334,
|
||||||
|
"totalXp": 6727,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 33,
|
||||||
|
"xpRequired": 342,
|
||||||
|
"totalXp": 7069,
|
||||||
|
"intelPoints": 15100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 34,
|
||||||
|
"xpRequired": 350,
|
||||||
|
"totalXp": 7419,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 35,
|
||||||
|
"xpRequired": 358,
|
||||||
|
"totalXp": 7777,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 36,
|
||||||
|
"xpRequired": 366,
|
||||||
|
"totalXp": 8143,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 37,
|
||||||
|
"xpRequired": 375,
|
||||||
|
"totalXp": 8518,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 38,
|
||||||
|
"xpRequired": 384,
|
||||||
|
"totalXp": 8902,
|
||||||
|
"intelPoints": 1550
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 39,
|
||||||
|
"xpRequired": 393,
|
||||||
|
"totalXp": 9295,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 40,
|
||||||
|
"xpRequired": 402,
|
||||||
|
"totalXp": 9697,
|
||||||
|
"intelPoints": 1525
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 41,
|
||||||
|
"xpRequired": 410,
|
||||||
|
"totalXp": 10107,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 42,
|
||||||
|
"xpRequired": 418,
|
||||||
|
"totalXp": 10525,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 43,
|
||||||
|
"xpRequired": 426,
|
||||||
|
"totalXp": 10951,
|
||||||
|
"intelPoints": 1550
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 44,
|
||||||
|
"xpRequired": 434,
|
||||||
|
"totalXp": 11385,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 45,
|
||||||
|
"xpRequired": 442,
|
||||||
|
"totalXp": 11827,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 46,
|
||||||
|
"xpRequired": 450,
|
||||||
|
"totalXp": 12277,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 47,
|
||||||
|
"xpRequired": 459,
|
||||||
|
"totalXp": 12736,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 48,
|
||||||
|
"xpRequired": 468,
|
||||||
|
"totalXp": 13204,
|
||||||
|
"intelPoints": 2050
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 49,
|
||||||
|
"xpRequired": 477,
|
||||||
|
"totalXp": 13681,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 50,
|
||||||
|
"xpRequired": 486,
|
||||||
|
"totalXp": 14167,
|
||||||
|
"intelPoints": 2025
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 51,
|
||||||
|
"xpRequired": 494,
|
||||||
|
"totalXp": 14661,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 52,
|
||||||
|
"xpRequired": 502,
|
||||||
|
"totalXp": 15163,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 53,
|
||||||
|
"xpRequired": 510,
|
||||||
|
"totalXp": 15673,
|
||||||
|
"intelPoints": 2050
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 54,
|
||||||
|
"xpRequired": 518,
|
||||||
|
"totalXp": 16191,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 55,
|
||||||
|
"xpRequired": 526,
|
||||||
|
"totalXp": 16717,
|
||||||
|
"intelPoints": 201000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 56,
|
||||||
|
"xpRequired": 534,
|
||||||
|
"totalXp": 17251,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 57,
|
||||||
|
"xpRequired": 543,
|
||||||
|
"totalXp": 17794,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 58,
|
||||||
|
"xpRequired": 552,
|
||||||
|
"totalXp": 18346,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 59,
|
||||||
|
"xpRequired": 561,
|
||||||
|
"totalXp": 18907,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 60,
|
||||||
|
"xpRequired": 570,
|
||||||
|
"totalXp": 19477,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 61,
|
||||||
|
"xpRequired": 575,
|
||||||
|
"totalXp": 20052,
|
||||||
|
"intelPoints": 2525
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 62,
|
||||||
|
"xpRequired": 580,
|
||||||
|
"totalXp": 20632,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 63,
|
||||||
|
"xpRequired": 585,
|
||||||
|
"totalXp": 21217,
|
||||||
|
"intelPoints": 25100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 64,
|
||||||
|
"xpRequired": 590,
|
||||||
|
"totalXp": 21807,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 65,
|
||||||
|
"xpRequired": 595,
|
||||||
|
"totalXp": 22402,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 66,
|
||||||
|
"xpRequired": 600,
|
||||||
|
"totalXp": 23002,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 67,
|
||||||
|
"xpRequired": 606,
|
||||||
|
"totalXp": 23608,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 68,
|
||||||
|
"xpRequired": 612,
|
||||||
|
"totalXp": 24220,
|
||||||
|
"intelPoints": 2550
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 69,
|
||||||
|
"xpRequired": 618,
|
||||||
|
"totalXp": 24838,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 70,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 25462,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 71,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 26086,
|
||||||
|
"intelPoints": 3025
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 72,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 26710,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 73,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 27334,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 74,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 27958,
|
||||||
|
"intelPoints": 30400
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 75,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 28582,
|
||||||
|
"intelPoints": 30
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 76,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 29206,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 77,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 29830,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 78,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 30454,
|
||||||
|
"intelPoints": 30100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 79,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 31078,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 80,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 31702,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 81,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 32326,
|
||||||
|
"intelPoints": 3025
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 82,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 32950,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 83,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 33574,
|
||||||
|
"intelPoints": 3550
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 84,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 34198,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 85,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 34822,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 86,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 35446,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 87,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 36070,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 88,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 36694,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 89,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 37318,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 90,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 37942,
|
||||||
|
"intelPoints": 3525
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 91,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 38566,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 92,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 39190,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 93,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 39814,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 94,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 40438,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 95,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 41062,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 96,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 41686,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 97,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 42310,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 98,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 42934,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 99,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 43558,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 100,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 44182,
|
||||||
|
"intelPoints": 4025
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
610
character-builder/data/spec-sabotage.json
Normal file
610
character-builder/data/spec-sabotage.json
Normal file
|
|
@ -0,0 +1,610 @@
|
||||||
|
{
|
||||||
|
"header": [
|
||||||
|
"Level",
|
||||||
|
"Required XP",
|
||||||
|
"Cumulative",
|
||||||
|
"Rewards"
|
||||||
|
],
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"level": 1,
|
||||||
|
"xpRequired": 100,
|
||||||
|
"totalXp": 100,
|
||||||
|
"intelPoints": 250
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 2,
|
||||||
|
"xpRequired": 105,
|
||||||
|
"totalXp": 205,
|
||||||
|
"intelPoints": 21000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 3,
|
||||||
|
"xpRequired": 110,
|
||||||
|
"totalXp": 315,
|
||||||
|
"intelPoints": 250
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 4,
|
||||||
|
"xpRequired": 116,
|
||||||
|
"totalXp": 431,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 5,
|
||||||
|
"xpRequired": 122,
|
||||||
|
"totalXp": 553,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 6,
|
||||||
|
"xpRequired": 128,
|
||||||
|
"totalXp": 681,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 7,
|
||||||
|
"xpRequired": 135,
|
||||||
|
"totalXp": 816,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 8,
|
||||||
|
"xpRequired": 142,
|
||||||
|
"totalXp": 958,
|
||||||
|
"intelPoints": 21000
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 9,
|
||||||
|
"xpRequired": 149,
|
||||||
|
"totalXp": 1107,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 10,
|
||||||
|
"xpRequired": 157,
|
||||||
|
"totalXp": 1264,
|
||||||
|
"intelPoints": 550
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 11,
|
||||||
|
"xpRequired": 164,
|
||||||
|
"totalXp": 1428,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 12,
|
||||||
|
"xpRequired": 171,
|
||||||
|
"totalXp": 1599,
|
||||||
|
"intelPoints": 5150
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 13,
|
||||||
|
"xpRequired": 178,
|
||||||
|
"totalXp": 1777,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 14,
|
||||||
|
"xpRequired": 186,
|
||||||
|
"totalXp": 1963,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 15,
|
||||||
|
"xpRequired": 194,
|
||||||
|
"totalXp": 2157,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 16,
|
||||||
|
"xpRequired": 202,
|
||||||
|
"totalXp": 2359,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 17,
|
||||||
|
"xpRequired": 211,
|
||||||
|
"totalXp": 2570,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 18,
|
||||||
|
"xpRequired": 220,
|
||||||
|
"totalXp": 2790,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 19,
|
||||||
|
"xpRequired": 229,
|
||||||
|
"totalXp": 3019,
|
||||||
|
"intelPoints": 550
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 20,
|
||||||
|
"xpRequired": 239,
|
||||||
|
"totalXp": 3258,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 21,
|
||||||
|
"xpRequired": 246,
|
||||||
|
"totalXp": 3504,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 22,
|
||||||
|
"xpRequired": 253,
|
||||||
|
"totalXp": 3757,
|
||||||
|
"intelPoints": 1020
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 23,
|
||||||
|
"xpRequired": 260,
|
||||||
|
"totalXp": 4017,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 24,
|
||||||
|
"xpRequired": 268,
|
||||||
|
"totalXp": 4285,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 25,
|
||||||
|
"xpRequired": 276,
|
||||||
|
"totalXp": 4561,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 26,
|
||||||
|
"xpRequired": 284,
|
||||||
|
"totalXp": 4845,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 27,
|
||||||
|
"xpRequired": 292,
|
||||||
|
"totalXp": 5137,
|
||||||
|
"intelPoints": 1050
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 28,
|
||||||
|
"xpRequired": 301,
|
||||||
|
"totalXp": 5438,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 29,
|
||||||
|
"xpRequired": 310,
|
||||||
|
"totalXp": 5748,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 30,
|
||||||
|
"xpRequired": 319,
|
||||||
|
"totalXp": 6067,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 31,
|
||||||
|
"xpRequired": 326,
|
||||||
|
"totalXp": 6393,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 32,
|
||||||
|
"xpRequired": 334,
|
||||||
|
"totalXp": 6727,
|
||||||
|
"intelPoints": 10150
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 33,
|
||||||
|
"xpRequired": 342,
|
||||||
|
"totalXp": 7069,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 34,
|
||||||
|
"xpRequired": 350,
|
||||||
|
"totalXp": 7419,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 35,
|
||||||
|
"xpRequired": 358,
|
||||||
|
"totalXp": 7777,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 36,
|
||||||
|
"xpRequired": 366,
|
||||||
|
"totalXp": 8143,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 37,
|
||||||
|
"xpRequired": 375,
|
||||||
|
"totalXp": 8518,
|
||||||
|
"intelPoints": 1550
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 38,
|
||||||
|
"xpRequired": 384,
|
||||||
|
"totalXp": 8902,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 39,
|
||||||
|
"xpRequired": 393,
|
||||||
|
"totalXp": 9295,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 40,
|
||||||
|
"xpRequired": 402,
|
||||||
|
"totalXp": 9697,
|
||||||
|
"intelPoints": 15
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 41,
|
||||||
|
"xpRequired": 410,
|
||||||
|
"totalXp": 10107,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 42,
|
||||||
|
"xpRequired": 418,
|
||||||
|
"totalXp": 10525,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 43,
|
||||||
|
"xpRequired": 426,
|
||||||
|
"totalXp": 10951,
|
||||||
|
"intelPoints": 15100
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 44,
|
||||||
|
"xpRequired": 434,
|
||||||
|
"totalXp": 11385,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 45,
|
||||||
|
"xpRequired": 442,
|
||||||
|
"totalXp": 11827,
|
||||||
|
"intelPoints": 1520
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 46,
|
||||||
|
"xpRequired": 450,
|
||||||
|
"totalXp": 12277,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 47,
|
||||||
|
"xpRequired": 459,
|
||||||
|
"totalXp": 12736,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 48,
|
||||||
|
"xpRequired": 468,
|
||||||
|
"totalXp": 13204,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 49,
|
||||||
|
"xpRequired": 477,
|
||||||
|
"totalXp": 13681,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 50,
|
||||||
|
"xpRequired": 486,
|
||||||
|
"totalXp": 14167,
|
||||||
|
"intelPoints": 2050
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 51,
|
||||||
|
"xpRequired": 494,
|
||||||
|
"totalXp": 14661,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 52,
|
||||||
|
"xpRequired": 502,
|
||||||
|
"totalXp": 15163,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 53,
|
||||||
|
"xpRequired": 510,
|
||||||
|
"totalXp": 15673,
|
||||||
|
"intelPoints": 2050
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 54,
|
||||||
|
"xpRequired": 518,
|
||||||
|
"totalXp": 16191,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 55,
|
||||||
|
"xpRequired": 526,
|
||||||
|
"totalXp": 16717,
|
||||||
|
"intelPoints": 20150
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 56,
|
||||||
|
"xpRequired": 534,
|
||||||
|
"totalXp": 17251,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 57,
|
||||||
|
"xpRequired": 543,
|
||||||
|
"totalXp": 17794,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 58,
|
||||||
|
"xpRequired": 552,
|
||||||
|
"totalXp": 18346,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 59,
|
||||||
|
"xpRequired": 561,
|
||||||
|
"totalXp": 18907,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 60,
|
||||||
|
"xpRequired": 570,
|
||||||
|
"totalXp": 19477,
|
||||||
|
"intelPoints": 2550
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 61,
|
||||||
|
"xpRequired": 575,
|
||||||
|
"totalXp": 20052,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 62,
|
||||||
|
"xpRequired": 580,
|
||||||
|
"totalXp": 20632,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 63,
|
||||||
|
"xpRequired": 585,
|
||||||
|
"totalXp": 21217,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 64,
|
||||||
|
"xpRequired": 590,
|
||||||
|
"totalXp": 21807,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 65,
|
||||||
|
"xpRequired": 595,
|
||||||
|
"totalXp": 22402,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 66,
|
||||||
|
"xpRequired": 600,
|
||||||
|
"totalXp": 23002,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 67,
|
||||||
|
"xpRequired": 606,
|
||||||
|
"totalXp": 23608,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 68,
|
||||||
|
"xpRequired": 612,
|
||||||
|
"totalXp": 24220,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 69,
|
||||||
|
"xpRequired": 618,
|
||||||
|
"totalXp": 24838,
|
||||||
|
"intelPoints": 25150
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 70,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 25462,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 71,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 26086,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 72,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 26710,
|
||||||
|
"intelPoints": 2550
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 73,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 27334,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 74,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 27958,
|
||||||
|
"intelPoints": 3050
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 75,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 28582,
|
||||||
|
"intelPoints": 30
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 76,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 29206,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 77,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 29830,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 78,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 30454,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 79,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 31078,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 80,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 31702,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 81,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 32326,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 82,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 32950,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 83,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 33574,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 84,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 34198,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 85,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 34822,
|
||||||
|
"intelPoints": 3050
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 86,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 35446,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 87,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 36070,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 88,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 36694,
|
||||||
|
"intelPoints": 3550
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 89,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 37318,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 90,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 37942,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 91,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 38566,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 92,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 39190,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 93,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 39814,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 94,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 40438,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 95,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 41062,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 96,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 41686,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 97,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 42310,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 98,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 42934,
|
||||||
|
"intelPoints": 35150
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 99,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 43558,
|
||||||
|
"intelPoints": 0
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"level": 100,
|
||||||
|
"xpRequired": 624,
|
||||||
|
"totalXp": 44182,
|
||||||
|
"intelPoints": 4050
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
38
character-builder/docker-compose.yml
Normal file
38
character-builder/docker-compose.yml
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
services:
|
||||||
|
app:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
image: dune-character-builder:latest
|
||||||
|
container_name: dune-builder-app
|
||||||
|
restart: unless-stopped
|
||||||
|
environment:
|
||||||
|
VALKEY_URL: redis://valkey:6379
|
||||||
|
PORT: "3000"
|
||||||
|
depends_on:
|
||||||
|
valkey:
|
||||||
|
condition: service_healthy
|
||||||
|
ports:
|
||||||
|
- "8080:3000"
|
||||||
|
|
||||||
|
valkey:
|
||||||
|
image: valkey/valkey:8-alpine
|
||||||
|
container_name: dune-builder-valkey
|
||||||
|
restart: unless-stopped
|
||||||
|
command:
|
||||||
|
- valkey-server
|
||||||
|
- --appendonly
|
||||||
|
- "yes"
|
||||||
|
- --save
|
||||||
|
- "60"
|
||||||
|
- "1"
|
||||||
|
volumes:
|
||||||
|
- valkey-data:/data
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "valkey-cli", "ping"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 5
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
valkey-data:
|
||||||
18
character-builder/frontend/index.html
Normal file
18
character-builder/frontend/index.html
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Dune Awakening — Character Builder</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link
|
||||||
|
href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&family=Inter:wght@400;500;600&display=swap"
|
||||||
|
rel="stylesheet"
|
||||||
|
/>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
20
character-builder/frontend/package.json
Normal file
20
character-builder/frontend/package.json
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"name": "dune-character-builder-frontend",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "vue-tsc -b && vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"vue": "^3.5.13"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@vitejs/plugin-vue": "^5.2.1",
|
||||||
|
"typescript": "^5.7.3",
|
||||||
|
"vite": "^6.0.7",
|
||||||
|
"vue-tsc": "^2.2.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
345
character-builder/frontend/src/App.vue
Normal file
345
character-builder/frontend/src/App.vue
Normal file
|
|
@ -0,0 +1,345 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, onMounted, ref, shallowRef, watch } from 'vue';
|
||||||
|
import XpProgressCard from './components/XpProgressCard.vue';
|
||||||
|
import FactionTrack from './components/FactionTrack.vue';
|
||||||
|
import SkillTree from './components/SkillTree.vue';
|
||||||
|
import { applyBuild, build, resetBuild, setClass, setHouse } from './store';
|
||||||
|
import {
|
||||||
|
CLASSES,
|
||||||
|
HOUSES,
|
||||||
|
SPECS,
|
||||||
|
loadCharacterXp,
|
||||||
|
loadFaction,
|
||||||
|
loadSkillTree,
|
||||||
|
loadSpec,
|
||||||
|
} from './data';
|
||||||
|
import type {
|
||||||
|
ClassId,
|
||||||
|
FactionTable,
|
||||||
|
House,
|
||||||
|
SkillTree as TSkillTree,
|
||||||
|
SpecId,
|
||||||
|
XpTable,
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
const charXp = shallowRef<XpTable | null>(null);
|
||||||
|
const specTables = shallowRef<Record<SpecId, XpTable | null>>({
|
||||||
|
combat: null,
|
||||||
|
crafting: null,
|
||||||
|
exploration: null,
|
||||||
|
gathering: null,
|
||||||
|
sabotage: null,
|
||||||
|
});
|
||||||
|
const factionTables = shallowRef<Record<House, FactionTable | null>>({
|
||||||
|
atreides: null,
|
||||||
|
harkonnen: null,
|
||||||
|
});
|
||||||
|
const skillTrees = shallowRef<Record<ClassId, TSkillTree | null>>({
|
||||||
|
benegesserit: null,
|
||||||
|
mentat: null,
|
||||||
|
planetologist: null,
|
||||||
|
swordmaster: null,
|
||||||
|
trooper: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const shareLink = ref<string>('');
|
||||||
|
const shareStatus = ref<string>('');
|
||||||
|
const saving = ref(false);
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
await Promise.all([
|
||||||
|
loadCharacterXp().then((d) => (charXp.value = d)),
|
||||||
|
...SPECS.map((s) =>
|
||||||
|
loadSpec(s).then((d) => (specTables.value = { ...specTables.value, [s]: d })),
|
||||||
|
),
|
||||||
|
loadFaction('atreides').then(
|
||||||
|
(d) => (factionTables.value = { ...factionTables.value, atreides: d }),
|
||||||
|
),
|
||||||
|
loadFaction('harkonnen').then(
|
||||||
|
(d) => (factionTables.value = { ...factionTables.value, harkonnen: d }),
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
// Load all skill trees in background so class switching is instant
|
||||||
|
for (const c of CLASSES) {
|
||||||
|
loadSkillTree(c.id).then(
|
||||||
|
(d) => (skillTrees.value = { ...skillTrees.value, [c.id]: d }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If URL has ?b=<code>, load that build.
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
const code = params.get('b');
|
||||||
|
if (code) {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`/api/builds/${code}`);
|
||||||
|
if (res.ok) {
|
||||||
|
const body = await res.json();
|
||||||
|
if (body.build) {
|
||||||
|
applyBuild(body.build);
|
||||||
|
shareStatus.value = `loaded build ${code}`;
|
||||||
|
shareLink.value = window.location.href;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
shareStatus.value = `build ${code} not found`;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
shareStatus.value = 'failed to load shared build';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const currentFaction = computed(() => factionTables.value[build.house]);
|
||||||
|
const currentTree = computed(() => skillTrees.value[build.classId]);
|
||||||
|
|
||||||
|
const totalSkillPointsAtLevel = computed(() => {
|
||||||
|
if (!charXp.value) return 0;
|
||||||
|
const idx = Math.max(0, Math.min(build.character.level - 1, charXp.value.rows.length - 1));
|
||||||
|
if (idx < 0) return 0;
|
||||||
|
return charXp.value.rows[idx]?.totalSkillPoints || 0;
|
||||||
|
});
|
||||||
|
const totalIntelAtLevel = computed(() => {
|
||||||
|
if (!charXp.value) return 0;
|
||||||
|
const idx = Math.max(0, Math.min(build.character.level - 1, charXp.value.rows.length - 1));
|
||||||
|
if (idx < 0) return 0;
|
||||||
|
return charXp.value.rows[idx]?.totalIntelPoints || 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
async function share() {
|
||||||
|
saving.value = true;
|
||||||
|
shareStatus.value = '';
|
||||||
|
try {
|
||||||
|
const res = await fetch('/api/builds', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(build),
|
||||||
|
});
|
||||||
|
if (!res.ok) {
|
||||||
|
const t = await res.text();
|
||||||
|
throw new Error(t);
|
||||||
|
}
|
||||||
|
const { code } = await res.json();
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
url.search = `?b=${code}`;
|
||||||
|
window.history.replaceState({}, '', url.toString());
|
||||||
|
shareLink.value = url.toString();
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(shareLink.value);
|
||||||
|
shareStatus.value = 'link copied';
|
||||||
|
} catch {
|
||||||
|
shareStatus.value = 'link ready';
|
||||||
|
}
|
||||||
|
} catch (e: any) {
|
||||||
|
shareStatus.value = `error: ${e?.message || 'failed'}`;
|
||||||
|
} finally {
|
||||||
|
saving.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function newBuild() {
|
||||||
|
if (!confirm('Reset build to defaults?')) return;
|
||||||
|
resetBuild();
|
||||||
|
shareLink.value = '';
|
||||||
|
shareStatus.value = 'reset';
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
url.search = '';
|
||||||
|
window.history.replaceState({}, '', url.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => build.classId,
|
||||||
|
() => {
|
||||||
|
// when switching class, ensure tree exists (lazy fetch fallback)
|
||||||
|
if (!skillTrees.value[build.classId]) {
|
||||||
|
loadSkillTree(build.classId).then(
|
||||||
|
(d) =>
|
||||||
|
(skillTrees.value = { ...skillTrees.value, [build.classId]: d }),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const specMeta: Record<SpecId, { name: string; sym: string }> = {
|
||||||
|
combat: { name: 'Combat', sym: 'CMB' },
|
||||||
|
crafting: { name: 'Crafting', sym: 'CRF' },
|
||||||
|
exploration: { name: 'Exploration', sym: 'EXP' },
|
||||||
|
gathering: { name: 'Gathering', sym: 'GTH' },
|
||||||
|
sabotage: { name: 'Sabotage', sym: 'SAB' },
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="share-bar">
|
||||||
|
<div class="share-inner">
|
||||||
|
<strong
|
||||||
|
style="
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
letter-spacing: 0.22em;
|
||||||
|
color: var(--sand-2);
|
||||||
|
text-transform: uppercase;
|
||||||
|
"
|
||||||
|
>Dune Character Builder</strong>
|
||||||
|
<div class="grow"></div>
|
||||||
|
<span v-if="shareLink" class="share-link">{{ shareLink }}</span>
|
||||||
|
<span v-if="shareStatus" class="toast">{{ shareStatus }}</span>
|
||||||
|
<button @click="newBuild">New</button>
|
||||||
|
<button class="primary" @click="share" :disabled="saving">
|
||||||
|
{{ saving ? 'Saving…' : 'Share Build' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="wrap">
|
||||||
|
<header class="hero">
|
||||||
|
<div class="eyebrow">Dune Awakening — Build Ledger</div>
|
||||||
|
<h1>
|
||||||
|
Plan your <em>Arrakeen ascent</em> — XP, specializations, factions,
|
||||||
|
skill trees.
|
||||||
|
</h1>
|
||||||
|
<p class="lede">
|
||||||
|
Pick your house and class, set your starting point, then plot the path
|
||||||
|
ahead. Your build is saved locally and can be shared by short link.
|
||||||
|
</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<!-- HOUSE + CLASS -->
|
||||||
|
<section class="panel">
|
||||||
|
<div class="panel-head">
|
||||||
|
<h2>House & Class</h2>
|
||||||
|
<div class="sub">Identity</div>
|
||||||
|
</div>
|
||||||
|
<div class="house-grid">
|
||||||
|
<div
|
||||||
|
v-for="h in HOUSES"
|
||||||
|
:key="h.id"
|
||||||
|
:class="['house-pick', h.id, build.house === h.id ? 'active' : '']"
|
||||||
|
@click="setHouse(h.id)"
|
||||||
|
>
|
||||||
|
<h3>{{ h.name }}</h3>
|
||||||
|
<div class="meta">
|
||||||
|
{{ h.id === 'atreides' ? 'The serpent and the eagle' : 'Strength without mercy' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="margin-top: 22px">
|
||||||
|
<div
|
||||||
|
style="
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 10.5px;
|
||||||
|
letter-spacing: 0.2em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--ink-muted);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
Class
|
||||||
|
</div>
|
||||||
|
<div class="class-grid">
|
||||||
|
<div
|
||||||
|
v-for="c in CLASSES"
|
||||||
|
:key="c.id"
|
||||||
|
:class="['class-pick', build.classId === c.id ? 'active' : '']"
|
||||||
|
@click="setClass(c.id)"
|
||||||
|
>
|
||||||
|
{{ c.name }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- CHARACTER -->
|
||||||
|
<section class="panel">
|
||||||
|
<div class="panel-head">
|
||||||
|
<h2>Character</h2>
|
||||||
|
<div class="sub">Levels 0–{{ charXp?.rows.length || '?' }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="totals">
|
||||||
|
<div class="total">
|
||||||
|
<div class="lbl">Level</div>
|
||||||
|
<div class="val">{{ build.character.level }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="total">
|
||||||
|
<div class="lbl">Skill Points</div>
|
||||||
|
<div class="val">{{ totalSkillPointsAtLevel }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="total">
|
||||||
|
<div class="lbl">Intel Points</div>
|
||||||
|
<div class="val">{{ totalIntelAtLevel }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="cards">
|
||||||
|
<XpProgressCard
|
||||||
|
title="Character XP"
|
||||||
|
sym="CHR"
|
||||||
|
:table="charXp"
|
||||||
|
:level="build.character.level"
|
||||||
|
:xp-into="build.character.xpInto"
|
||||||
|
@update:level="(n) => (build.character.level = n)"
|
||||||
|
@update:xp-into="(n) => (build.character.xpInto = n)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- SPECIALIZATIONS -->
|
||||||
|
<section class="panel">
|
||||||
|
<div class="panel-head">
|
||||||
|
<h2>Specializations</h2>
|
||||||
|
<div class="sub">5 tracks · L0–L100</div>
|
||||||
|
</div>
|
||||||
|
<div class="cards">
|
||||||
|
<XpProgressCard
|
||||||
|
v-for="s in SPECS"
|
||||||
|
:key="s"
|
||||||
|
:title="specMeta[s].name"
|
||||||
|
:sym="specMeta[s].sym"
|
||||||
|
:table="specTables[s]"
|
||||||
|
:level="build.specs[s].level"
|
||||||
|
:xp-into="build.specs[s].xpInto"
|
||||||
|
@update:level="(n) => (build.specs[s].level = n)"
|
||||||
|
@update:xp-into="(n) => (build.specs[s].xpInto = n)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- FACTION -->
|
||||||
|
<section class="panel">
|
||||||
|
<div class="panel-head">
|
||||||
|
<h2>Faction Standing</h2>
|
||||||
|
<div class="sub">
|
||||||
|
{{ build.house === 'atreides' ? 'House Atreides' : 'House Harkonnen' }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<FactionTrack
|
||||||
|
:house="build.house"
|
||||||
|
:table="currentFaction"
|
||||||
|
:tier="build.faction.tier"
|
||||||
|
:standing-into="build.faction.standingInto"
|
||||||
|
@update:tier="(n) => (build.faction.tier = n)"
|
||||||
|
@update:standing-into="(n) => (build.faction.standingInto = n)"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- SKILL TREE -->
|
||||||
|
<section class="panel">
|
||||||
|
<div class="panel-head">
|
||||||
|
<h2>Skill Tree — {{ CLASSES.find((c) => c.id === build.classId)?.name }}</h2>
|
||||||
|
<div class="sub">Class progression</div>
|
||||||
|
</div>
|
||||||
|
<SkillTree
|
||||||
|
:tree="currentTree"
|
||||||
|
:allocations="build.skills"
|
||||||
|
:available-points="totalSkillPointsAtLevel"
|
||||||
|
@update:allocations="(a) => (build.skills = a)"
|
||||||
|
@reset="() => (build.skills = {})"
|
||||||
|
/>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<footer class="footer-bar">
|
||||||
|
<div>Unofficial fan-made tool · not affiliated with the publisher</div>
|
||||||
|
<div>Data sourced from dune.gaming.tools snapshots</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
94
character-builder/frontend/src/components/FactionTrack.vue
Normal file
94
character-builder/frontend/src/components/FactionTrack.vue
Normal file
|
|
@ -0,0 +1,94 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import type { FactionTable, House } from '../types';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
house: House;
|
||||||
|
table: FactionTable | null;
|
||||||
|
tier: number;
|
||||||
|
standingInto: number;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:tier': [n: number];
|
||||||
|
'update:standingInto': [n: number];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const tierCount = computed(() => props.table?.tiers.length ?? 0);
|
||||||
|
const maxTier = computed(() => Math.max(0, tierCount.value - 1));
|
||||||
|
|
||||||
|
const currentTier = computed(() => {
|
||||||
|
if (!props.table) return null;
|
||||||
|
return props.table.tiers[props.tier] || null;
|
||||||
|
});
|
||||||
|
const nextTier = computed(() => {
|
||||||
|
if (!props.table) return null;
|
||||||
|
return props.table.tiers[props.tier + 1] || null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const pct = computed(() => {
|
||||||
|
if (!nextTier.value || !nextTier.value.standingRequired) return 0;
|
||||||
|
const p = (props.standingInto / nextTier.value.standingRequired) * 100;
|
||||||
|
return Math.min(100, Math.max(0, p));
|
||||||
|
});
|
||||||
|
|
||||||
|
function setTier(e: Event) {
|
||||||
|
const v = Number((e.target as HTMLInputElement).value) || 0;
|
||||||
|
emit('update:tier', Math.max(0, Math.min(maxTier.value, Math.floor(v))));
|
||||||
|
}
|
||||||
|
function setInto(e: Event) {
|
||||||
|
const v = Number((e.target as HTMLInputElement).value) || 0;
|
||||||
|
emit('update:standingInto', Math.max(0, Math.floor(v)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function fmt(n: number): string {
|
||||||
|
return n.toLocaleString('en-US');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="field">
|
||||||
|
<label>Current Tier</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
:max="maxTier"
|
||||||
|
:value="tier"
|
||||||
|
@input="setTier"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>Rep into next tier</label>
|
||||||
|
<input type="number" min="0" :value="standingInto" @input="setInto" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="progress" :style="{ '--pct': pct + '%' }"></div>
|
||||||
|
<div class="progress-meta">
|
||||||
|
<span>
|
||||||
|
{{ currentTier?.name || '—' }} → {{ nextTier?.name || 'Max' }}
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
{{ fmt(standingInto) }} / {{ fmt(nextTier?.standingRequired || 0) }} rep
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tier-list" v-if="table">
|
||||||
|
<div
|
||||||
|
v-for="t in table.tiers"
|
||||||
|
:key="t.tier"
|
||||||
|
:class="[
|
||||||
|
'tier-row',
|
||||||
|
t.tier < tier ? 'reached' : '',
|
||||||
|
t.tier === tier ? 'current' : '',
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<span class="num">{{ t.tier }}</span>
|
||||||
|
<span>{{ t.name || '—' }}</span>
|
||||||
|
<span>{{ fmt(t.standingRequired) }}</span>
|
||||||
|
<span style="color: var(--ink-muted)">{{ fmt(t.totalStanding) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
190
character-builder/frontend/src/components/SkillTree.vue
Normal file
190
character-builder/frontend/src/components/SkillTree.vue
Normal file
|
|
@ -0,0 +1,190 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import type { SkillTree } from '../types';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
tree: SkillTree | null;
|
||||||
|
allocations: Record<string, number>;
|
||||||
|
availablePoints: number;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:allocations': [a: Record<string, number>];
|
||||||
|
reset: [];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
// Cell size + gap in pixels. Match CSS .tree-cell.
|
||||||
|
const CELL = 96;
|
||||||
|
const GAP = 36; // visual spacing including label area
|
||||||
|
|
||||||
|
const gridSize = computed(() => {
|
||||||
|
if (!props.tree) return { rows: 0, cols: 0 };
|
||||||
|
let maxRow = 0;
|
||||||
|
let maxCol = 0;
|
||||||
|
for (const n of props.tree.nodes) {
|
||||||
|
if (n.row > maxRow) maxRow = n.row;
|
||||||
|
if (n.col > maxCol) maxCol = n.col;
|
||||||
|
}
|
||||||
|
return { rows: maxRow, cols: maxCol };
|
||||||
|
});
|
||||||
|
|
||||||
|
const gridStyle = computed(() => ({
|
||||||
|
gridTemplateColumns: `repeat(${gridSize.value.cols}, ${CELL}px)`,
|
||||||
|
gridTemplateRows: `repeat(${gridSize.value.rows}, ${CELL}px)`,
|
||||||
|
width: `${gridSize.value.cols * CELL + (gridSize.value.cols - 1) * GAP}px`,
|
||||||
|
columnGap: `${GAP}px`,
|
||||||
|
rowGap: `${GAP}px`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const totalAllocated = computed(() =>
|
||||||
|
Object.values(props.allocations).reduce((a, b) => a + (b || 0), 0),
|
||||||
|
);
|
||||||
|
|
||||||
|
function centerForNode(row: number, col: number) {
|
||||||
|
const x = (col - 1) * (CELL + GAP) + CELL / 2;
|
||||||
|
const y = (row - 1) * (CELL + GAP) + CELL / 2;
|
||||||
|
return { x, y };
|
||||||
|
}
|
||||||
|
|
||||||
|
const edgeLines = computed(() => {
|
||||||
|
if (!props.tree) return [];
|
||||||
|
const byTag = new Map(props.tree.nodes.map((n) => [n.tag, n] as const));
|
||||||
|
const lines: { x1: number; y1: number; x2: number; y2: number }[] = [];
|
||||||
|
for (const e of props.tree.edges) {
|
||||||
|
const a = byTag.get(e.from);
|
||||||
|
const b = byTag.get(e.to);
|
||||||
|
if (!a || !b) continue;
|
||||||
|
const pa = centerForNode(a.row, a.col);
|
||||||
|
const pb = centerForNode(b.row, b.col);
|
||||||
|
lines.push({ x1: pa.x, y1: pa.y, x2: pb.x, y2: pb.y });
|
||||||
|
}
|
||||||
|
return lines;
|
||||||
|
});
|
||||||
|
|
||||||
|
const svgSize = computed(() => {
|
||||||
|
const w = gridSize.value.cols * CELL + (gridSize.value.cols - 1) * GAP;
|
||||||
|
const h = gridSize.value.rows * CELL + (gridSize.value.rows - 1) * GAP;
|
||||||
|
return { w, h };
|
||||||
|
});
|
||||||
|
|
||||||
|
function pointsFor(tag: string): number {
|
||||||
|
return props.allocations[tag] || 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function add(tag: string, delta: number) {
|
||||||
|
if (!props.tree) return;
|
||||||
|
const node = props.tree.nodes.find((n) => n.tag === tag);
|
||||||
|
if (!node) return;
|
||||||
|
const cur = pointsFor(tag);
|
||||||
|
const max = node.maxPoints;
|
||||||
|
let next = cur + delta;
|
||||||
|
if (next < 0) next = 0;
|
||||||
|
if (next > max) next = max;
|
||||||
|
if (delta > 0 && totalAllocated.value + (next - cur) > props.availablePoints) {
|
||||||
|
return; // not enough points
|
||||||
|
}
|
||||||
|
const out = { ...props.allocations };
|
||||||
|
if (next === 0) delete out[tag];
|
||||||
|
else out[tag] = next;
|
||||||
|
emit('update:allocations', out);
|
||||||
|
}
|
||||||
|
|
||||||
|
function nodeClick(tag: string, e: MouseEvent) {
|
||||||
|
if (e.shiftKey || e.button === 2) add(tag, -1);
|
||||||
|
else add(tag, 1);
|
||||||
|
}
|
||||||
|
function onContext(tag: string, e: MouseEvent) {
|
||||||
|
e.preventDefault();
|
||||||
|
add(tag, -1);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
style="
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
style="
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
letter-spacing: 0.18em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--ink-dim);
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<span style="color: var(--sand)">{{ totalAllocated }}</span> /
|
||||||
|
{{ availablePoints }} skill points spent
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
style="
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--ink-muted);
|
||||||
|
letter-spacing: 0.16em;
|
||||||
|
"
|
||||||
|
>
|
||||||
|
Click +1 · Shift-click / right-click −1
|
||||||
|
</div>
|
||||||
|
<button @click="emit('reset')">Reset Skills</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="tree-wrap" v-if="tree">
|
||||||
|
<div class="tree-grid" :style="gridStyle">
|
||||||
|
<svg
|
||||||
|
class="tree-edges"
|
||||||
|
:viewBox="`0 0 ${svgSize.w} ${svgSize.h}`"
|
||||||
|
:width="svgSize.w"
|
||||||
|
:height="svgSize.h"
|
||||||
|
preserveAspectRatio="none"
|
||||||
|
>
|
||||||
|
<line
|
||||||
|
v-for="(ln, i) in edgeLines"
|
||||||
|
:key="i"
|
||||||
|
:x1="ln.x1"
|
||||||
|
:y1="ln.y1"
|
||||||
|
:x2="ln.x2"
|
||||||
|
:y2="ln.y2"
|
||||||
|
stroke="var(--line)"
|
||||||
|
stroke-width="2"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<div
|
||||||
|
v-for="n in tree.nodes"
|
||||||
|
:key="n.tag"
|
||||||
|
class="tree-node-wrap"
|
||||||
|
:style="{
|
||||||
|
gridColumn: n.col,
|
||||||
|
gridRow: n.row,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
:class="[
|
||||||
|
'tree-node',
|
||||||
|
`kind-${n.kind.toLowerCase()}`,
|
||||||
|
pointsFor(n.tag) > 0 ? 'allocated' : '',
|
||||||
|
pointsFor(n.tag) >= n.maxPoints ? 'maxed' : '',
|
||||||
|
]"
|
||||||
|
@click="(e) => nodeClick(n.tag, e)"
|
||||||
|
@contextmenu="(e) => onContext(n.tag, e)"
|
||||||
|
:title="`${n.name} (${n.kind})`"
|
||||||
|
>
|
||||||
|
<div class="name">{{ n.name }}</div>
|
||||||
|
<div class="pts">{{ pointsFor(n.tag) }}/{{ n.maxPoints }}</div>
|
||||||
|
</div>
|
||||||
|
<div class="label">{{ n.kind }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-else style="padding: 32px; color: var(--ink-muted)">
|
||||||
|
Loading skill tree…
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
87
character-builder/frontend/src/components/XpProgressCard.vue
Normal file
87
character-builder/frontend/src/components/XpProgressCard.vue
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import type { XpTable } from '../types';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
title: string;
|
||||||
|
sym: string;
|
||||||
|
table: XpTable | null;
|
||||||
|
level: number;
|
||||||
|
xpInto: number;
|
||||||
|
maxLevel?: number;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:level': [n: number];
|
||||||
|
'update:xpInto': [n: number];
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const maxLevel = computed(() => props.maxLevel ?? props.table?.rows.length ?? 0);
|
||||||
|
|
||||||
|
const nextRow = computed(() => {
|
||||||
|
if (!props.table) return null;
|
||||||
|
// XP required to go from L -> L+1 lives at table.rows[L] (1-indexed in source,
|
||||||
|
// but rows[0] is level 1). We treat current level L as already reached, so XP
|
||||||
|
// required for next level is rows[L].xpRequired (or 0 if at max).
|
||||||
|
const idx = props.level; // rows[idx].level = idx+1
|
||||||
|
return props.table.rows[idx] || null;
|
||||||
|
});
|
||||||
|
|
||||||
|
const cumulativeAtCurrent = computed(() => {
|
||||||
|
if (!props.table || props.level <= 0) return 0;
|
||||||
|
const r = props.table.rows[props.level - 1];
|
||||||
|
return r ? r.totalXp : 0;
|
||||||
|
});
|
||||||
|
|
||||||
|
const pct = computed(() => {
|
||||||
|
if (!nextRow.value || !nextRow.value.xpRequired) return 0;
|
||||||
|
const p = (props.xpInto / nextRow.value.xpRequired) * 100;
|
||||||
|
return Math.min(100, Math.max(0, p));
|
||||||
|
});
|
||||||
|
|
||||||
|
function onLevel(e: Event) {
|
||||||
|
const v = Number((e.target as HTMLInputElement).value) || 0;
|
||||||
|
emit('update:level', Math.max(0, Math.min(maxLevel.value, Math.floor(v))));
|
||||||
|
}
|
||||||
|
function onXpInto(e: Event) {
|
||||||
|
const v = Number((e.target as HTMLInputElement).value) || 0;
|
||||||
|
emit('update:xpInto', Math.max(0, Math.floor(v)));
|
||||||
|
}
|
||||||
|
function fmt(n: number): string {
|
||||||
|
return n.toLocaleString('en-US');
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="card">
|
||||||
|
<div class="sym">{{ sym }}</div>
|
||||||
|
<h3>{{ title }}</h3>
|
||||||
|
<div class="row">
|
||||||
|
<div class="field">
|
||||||
|
<label>Level</label>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
:max="maxLevel"
|
||||||
|
:value="level"
|
||||||
|
@input="onLevel"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>XP into next</label>
|
||||||
|
<input type="number" min="0" :value="xpInto" @input="onXpInto" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="progress" :style="{ '--pct': pct + '%' }"></div>
|
||||||
|
<div class="progress-meta">
|
||||||
|
<span>L{{ level }} → L{{ level + 1 }}</span>
|
||||||
|
<span>
|
||||||
|
{{ fmt(xpInto) }} / {{ fmt(nextRow?.xpRequired || 0) }} XP
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="progress-meta" style="margin-top: 4px">
|
||||||
|
<span>Cumulative</span>
|
||||||
|
<span>{{ fmt(cumulativeAtCurrent + xpInto) }} XP</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
53
character-builder/frontend/src/data.ts
Normal file
53
character-builder/frontend/src/data.ts
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
import type {
|
||||||
|
ClassId,
|
||||||
|
FactionTable,
|
||||||
|
House,
|
||||||
|
SkillTree,
|
||||||
|
SpecId,
|
||||||
|
XpTable,
|
||||||
|
} from './types';
|
||||||
|
|
||||||
|
const API = '/api/data';
|
||||||
|
|
||||||
|
async function getJSON<T>(file: string): Promise<T> {
|
||||||
|
const res = await fetch(`${API}/${file}`);
|
||||||
|
if (!res.ok) throw new Error(`fetch ${file}: ${res.status}`);
|
||||||
|
return res.json() as Promise<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SPECS: SpecId[] = [
|
||||||
|
'combat',
|
||||||
|
'crafting',
|
||||||
|
'exploration',
|
||||||
|
'gathering',
|
||||||
|
'sabotage',
|
||||||
|
];
|
||||||
|
|
||||||
|
export const CLASSES: { id: ClassId; name: string }[] = [
|
||||||
|
{ id: 'benegesserit', name: 'Bene Gesserit' },
|
||||||
|
{ id: 'mentat', name: 'Mentat' },
|
||||||
|
{ id: 'planetologist', name: 'Planetologist' },
|
||||||
|
{ id: 'swordmaster', name: 'Swordmaster' },
|
||||||
|
{ id: 'trooper', name: 'Trooper' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const HOUSES: { id: House; name: string }[] = [
|
||||||
|
{ id: 'atreides', name: 'House Atreides' },
|
||||||
|
{ id: 'harkonnen', name: 'House Harkonnen' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export async function loadCharacterXp(): Promise<XpTable> {
|
||||||
|
return getJSON<XpTable>('character-xp.json');
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadSpec(id: SpecId): Promise<XpTable> {
|
||||||
|
return getJSON<XpTable>(`spec-${id}.json`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadFaction(house: House): Promise<FactionTable> {
|
||||||
|
return getJSON<FactionTable>(`faction-${house}.json`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function loadSkillTree(id: ClassId): Promise<SkillTree> {
|
||||||
|
return getJSON<SkillTree>(`skills-${id}.json`);
|
||||||
|
}
|
||||||
5
character-builder/frontend/src/main.ts
Normal file
5
character-builder/frontend/src/main.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
||||||
|
import { createApp } from 'vue';
|
||||||
|
import App from './App.vue';
|
||||||
|
import './styles.css';
|
||||||
|
|
||||||
|
createApp(App).mount('#app');
|
||||||
86
character-builder/frontend/src/store.ts
Normal file
86
character-builder/frontend/src/store.ts
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
import { reactive, watch } from 'vue';
|
||||||
|
import type { BuildState, ClassId, House, SpecId } from './types';
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'dune-builder-v1';
|
||||||
|
|
||||||
|
export function defaultBuild(): BuildState {
|
||||||
|
const specs: BuildState['specs'] = {
|
||||||
|
combat: { level: 0, xpInto: 0 },
|
||||||
|
crafting: { level: 0, xpInto: 0 },
|
||||||
|
exploration: { level: 0, xpInto: 0 },
|
||||||
|
gathering: { level: 0, xpInto: 0 },
|
||||||
|
sabotage: { level: 0, xpInto: 0 },
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
v: 1,
|
||||||
|
house: 'atreides',
|
||||||
|
classId: 'swordmaster',
|
||||||
|
character: { level: 0, xpInto: 0 },
|
||||||
|
specs,
|
||||||
|
faction: { tier: 0, standingInto: 0 },
|
||||||
|
skills: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function migrate(raw: any): BuildState {
|
||||||
|
const def = defaultBuild();
|
||||||
|
if (!raw || typeof raw !== 'object') return def;
|
||||||
|
// Shallow merge with defaults; type-safe enough for v1.
|
||||||
|
const out: BuildState = {
|
||||||
|
...def,
|
||||||
|
...raw,
|
||||||
|
character: { ...def.character, ...(raw.character || {}) },
|
||||||
|
specs: { ...def.specs },
|
||||||
|
faction: { ...def.faction, ...(raw.faction || {}) },
|
||||||
|
skills: { ...(raw.skills || {}) },
|
||||||
|
};
|
||||||
|
if (raw.specs && typeof raw.specs === 'object') {
|
||||||
|
for (const k of Object.keys(out.specs) as SpecId[]) {
|
||||||
|
if (raw.specs[k]) out.specs[k] = { ...out.specs[k], ...raw.specs[k] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadFromStorage(): BuildState {
|
||||||
|
try {
|
||||||
|
const raw = JSON.parse(localStorage.getItem(STORAGE_KEY) || 'null');
|
||||||
|
return migrate(raw);
|
||||||
|
} catch {
|
||||||
|
return defaultBuild();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const build = reactive<BuildState>(loadFromStorage());
|
||||||
|
|
||||||
|
watch(
|
||||||
|
build,
|
||||||
|
(val) => {
|
||||||
|
try {
|
||||||
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(val));
|
||||||
|
} catch {
|
||||||
|
// ignore (quota, private mode, etc.)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ deep: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
export function applyBuild(next: BuildState) {
|
||||||
|
const merged = migrate(next);
|
||||||
|
Object.assign(build, merged);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resetBuild() {
|
||||||
|
applyBuild(defaultBuild());
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setHouse(h: House) {
|
||||||
|
build.house = h;
|
||||||
|
// resetting faction progress when house changes is the safest default
|
||||||
|
build.faction = { tier: 0, standingInto: 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setClass(c: ClassId) {
|
||||||
|
build.classId = c;
|
||||||
|
build.skills = {}; // skills are per-class
|
||||||
|
}
|
||||||
546
character-builder/frontend/src/styles.css
Normal file
546
character-builder/frontend/src/styles.css
Normal file
|
|
@ -0,0 +1,546 @@
|
||||||
|
:root {
|
||||||
|
--bg: #1a1410;
|
||||||
|
--bg-2: #221a14;
|
||||||
|
--panel: #2b2018;
|
||||||
|
--panel-2: #352720;
|
||||||
|
--line: #4a3628;
|
||||||
|
--line-soft: #3a2a1f;
|
||||||
|
--ink: #f4e9d8;
|
||||||
|
--ink-dim: #c9b89c;
|
||||||
|
--ink-muted: #8a7560;
|
||||||
|
--sand: #e6c98a;
|
||||||
|
--sand-2: #d4a85a;
|
||||||
|
--ember: #c8643a;
|
||||||
|
--spice: #e08a3c;
|
||||||
|
--green: #8fb87a;
|
||||||
|
--atreides: #4a86c5;
|
||||||
|
--harkonnen: #a93832;
|
||||||
|
--shadow: 0 1px 0 rgba(255, 255, 255, 0.04),
|
||||||
|
0 12px 32px -16px rgba(0, 0, 0, 0.6);
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--ink);
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
font-family: 'Inter', system-ui, sans-serif;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1.5;
|
||||||
|
background: radial-gradient(
|
||||||
|
1200px 600px at 80% -10%,
|
||||||
|
rgba(224, 138, 60, 0.1),
|
||||||
|
transparent 60%
|
||||||
|
),
|
||||||
|
radial-gradient(
|
||||||
|
900px 500px at -10% 110%,
|
||||||
|
rgba(212, 168, 90, 0.06),
|
||||||
|
transparent 60%
|
||||||
|
),
|
||||||
|
var(--bg);
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
a { color: var(--sand); }
|
||||||
|
|
||||||
|
.wrap {
|
||||||
|
max-width: 1240px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 48px 28px 96px;
|
||||||
|
}
|
||||||
|
header.hero {
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
.eyebrow {
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
letter-spacing: 0.22em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--sand-2);
|
||||||
|
margin-bottom: 12px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.eyebrow::after {
|
||||||
|
content: '';
|
||||||
|
flex: 1;
|
||||||
|
height: 1px;
|
||||||
|
background: linear-gradient(90deg, var(--line) 0%, transparent 100%);
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-family: 'Cormorant Garamond', serif;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: clamp(36px, 5vw, 56px);
|
||||||
|
line-height: 1;
|
||||||
|
margin: 0 0 14px;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
h1 em {
|
||||||
|
font-style: italic;
|
||||||
|
color: var(--sand);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.lede {
|
||||||
|
color: var(--ink-dim);
|
||||||
|
max-width: 640px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel {
|
||||||
|
background: linear-gradient(180deg, var(--panel) 0%, var(--panel-2) 100%);
|
||||||
|
border: 1px solid var(--line-soft);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 22px 26px;
|
||||||
|
margin-bottom: 22px;
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
}
|
||||||
|
.panel-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
padding-bottom: 14px;
|
||||||
|
border-bottom: 1px dashed var(--line-soft);
|
||||||
|
}
|
||||||
|
.panel-head h2 {
|
||||||
|
font-family: 'Cormorant Garamond', serif;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 26px;
|
||||||
|
margin: 0;
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
}
|
||||||
|
.panel-head .sub {
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 10.5px;
|
||||||
|
letter-spacing: 0.2em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--ink-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.row-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 18px;
|
||||||
|
}
|
||||||
|
.cards {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
background: var(--bg-2);
|
||||||
|
border: 1px solid var(--line-soft);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 18px 20px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.card h3 {
|
||||||
|
font-family: 'Cormorant Garamond', serif;
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 500;
|
||||||
|
margin: 0 0 12px;
|
||||||
|
}
|
||||||
|
.card .sym {
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 10px;
|
||||||
|
letter-spacing: 0.2em;
|
||||||
|
color: var(--ink-muted);
|
||||||
|
text-transform: uppercase;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.field label {
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 10.5px;
|
||||||
|
letter-spacing: 0.18em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--ink-muted);
|
||||||
|
}
|
||||||
|
.field input[type='number'],
|
||||||
|
.field select {
|
||||||
|
background: var(--bg);
|
||||||
|
border: 1px solid var(--line-soft);
|
||||||
|
color: var(--ink);
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 15px;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-radius: 3px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
.field input[type='number']:focus,
|
||||||
|
.field select:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--sand-2);
|
||||||
|
background: #1f1812;
|
||||||
|
}
|
||||||
|
input[type='number']::-webkit-inner-spin-button,
|
||||||
|
input[type='number']::-webkit-outer-spin-button {
|
||||||
|
-webkit-appearance: none;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
input[type='number'] {
|
||||||
|
-moz-appearance: textfield;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.row.single {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.progress {
|
||||||
|
margin-top: 10px;
|
||||||
|
height: 4px;
|
||||||
|
background: var(--bg);
|
||||||
|
border-radius: 999px;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.progress::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
width: var(--pct, 0%);
|
||||||
|
background: linear-gradient(
|
||||||
|
90deg,
|
||||||
|
var(--ember) 0%,
|
||||||
|
var(--spice) 60%,
|
||||||
|
var(--sand) 100%
|
||||||
|
);
|
||||||
|
transition: width 0.25s ease;
|
||||||
|
}
|
||||||
|
.progress-meta {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 10px;
|
||||||
|
color: var(--ink-muted);
|
||||||
|
margin-top: 6px;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.house-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
.house-pick {
|
||||||
|
border: 1px solid var(--line-soft);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--bg-2);
|
||||||
|
padding: 18px 22px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.15s, background 0.15s, transform 0.15s;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.house-pick:hover {
|
||||||
|
border-color: var(--line);
|
||||||
|
}
|
||||||
|
.house-pick.active {
|
||||||
|
background: linear-gradient(180deg, var(--panel) 0%, var(--panel-2) 100%);
|
||||||
|
border-color: var(--sand-2);
|
||||||
|
}
|
||||||
|
.house-pick h3 {
|
||||||
|
font-family: 'Cormorant Garamond', serif;
|
||||||
|
font-size: 24px;
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.house-pick.active.atreides h3 { color: var(--atreides); }
|
||||||
|
.house-pick.active.harkonnen h3 { color: var(--harkonnen); }
|
||||||
|
.house-pick .meta {
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 10.5px;
|
||||||
|
letter-spacing: 0.18em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--ink-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.class-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.class-pick {
|
||||||
|
border: 1px solid var(--line-soft);
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--bg-2);
|
||||||
|
padding: 14px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: center;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
letter-spacing: 0.16em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--ink-dim);
|
||||||
|
transition: border-color 0.15s, color 0.15s, background 0.15s;
|
||||||
|
}
|
||||||
|
.class-pick:hover {
|
||||||
|
color: var(--sand);
|
||||||
|
border-color: var(--line);
|
||||||
|
}
|
||||||
|
.class-pick.active {
|
||||||
|
color: var(--sand);
|
||||||
|
border-color: var(--sand-2);
|
||||||
|
background: rgba(224, 138, 60, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Buttons */
|
||||||
|
button {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
color: var(--ink-dim);
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
letter-spacing: 0.18em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color 0.15s, color 0.15s, background 0.15s;
|
||||||
|
}
|
||||||
|
button:hover {
|
||||||
|
border-color: var(--sand-2);
|
||||||
|
color: var(--sand);
|
||||||
|
background: rgba(224, 138, 60, 0.06);
|
||||||
|
}
|
||||||
|
button.primary {
|
||||||
|
background: var(--spice);
|
||||||
|
color: #1f1812;
|
||||||
|
border-color: var(--spice);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
button.primary:hover {
|
||||||
|
background: var(--sand);
|
||||||
|
border-color: var(--sand);
|
||||||
|
}
|
||||||
|
button:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.totals {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||||
|
border: 1px solid var(--line-soft);
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--bg-2);
|
||||||
|
margin-bottom: 22px;
|
||||||
|
}
|
||||||
|
.total {
|
||||||
|
padding: 20px 22px;
|
||||||
|
border-right: 1px solid var(--line-soft);
|
||||||
|
background: linear-gradient(180deg, rgba(255, 255, 255, 0.015), transparent);
|
||||||
|
}
|
||||||
|
.total:last-child { border-right: none; }
|
||||||
|
.total .lbl {
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 10.5px;
|
||||||
|
letter-spacing: 0.2em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--ink-muted);
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
.total .val {
|
||||||
|
font-family: 'Cormorant Garamond', serif;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 36px;
|
||||||
|
line-height: 1;
|
||||||
|
color: var(--sand);
|
||||||
|
}
|
||||||
|
.total .val .unit {
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--ink-muted);
|
||||||
|
letter-spacing: 0.15em;
|
||||||
|
margin-left: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Share bar */
|
||||||
|
.share-bar {
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 50;
|
||||||
|
background: rgba(26, 20, 16, 0.92);
|
||||||
|
backdrop-filter: blur(8px);
|
||||||
|
border-bottom: 1px solid var(--line-soft);
|
||||||
|
padding: 12px 0;
|
||||||
|
margin: -48px 0 32px;
|
||||||
|
}
|
||||||
|
.share-inner {
|
||||||
|
max-width: 1240px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 0 28px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
.share-inner .grow { flex: 1; }
|
||||||
|
.share-link {
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--sand);
|
||||||
|
background: var(--bg-2);
|
||||||
|
border: 1px solid var(--line-soft);
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 6px 10px;
|
||||||
|
user-select: all;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
.toast {
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
letter-spacing: 0.18em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--green);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Skill tree */
|
||||||
|
.tree-wrap {
|
||||||
|
position: relative;
|
||||||
|
background: var(--bg);
|
||||||
|
border: 1px solid var(--line-soft);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 24px;
|
||||||
|
overflow: auto;
|
||||||
|
min-height: 520px;
|
||||||
|
}
|
||||||
|
.tree-grid {
|
||||||
|
position: relative;
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
.tree-cell {
|
||||||
|
width: 96px;
|
||||||
|
height: 96px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.tree-node {
|
||||||
|
width: 96px;
|
||||||
|
height: 96px;
|
||||||
|
border: 2px solid var(--line);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--bg-2);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
transition: border-color 0.15s, background 0.15s, transform 0.1s;
|
||||||
|
}
|
||||||
|
.tree-node:hover { border-color: var(--sand-2); }
|
||||||
|
.tree-node.allocated { border-color: var(--spice); background: rgba(224, 138, 60, 0.1); }
|
||||||
|
.tree-node.maxed { border-color: var(--sand); background: rgba(230, 201, 138, 0.14); }
|
||||||
|
.tree-node.kind-ability { border-style: solid; }
|
||||||
|
.tree-node.kind-attribute { border-style: dashed; }
|
||||||
|
.tree-node.kind-perk { border-radius: 50%; }
|
||||||
|
.tree-node.kind-spice { border-style: double; }
|
||||||
|
.tree-node .label {
|
||||||
|
position: absolute;
|
||||||
|
bottom: -22px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 9.5px;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
color: var(--ink-dim);
|
||||||
|
text-align: center;
|
||||||
|
width: 130px;
|
||||||
|
pointer-events: none;
|
||||||
|
text-shadow: 0 0 4px var(--bg);
|
||||||
|
}
|
||||||
|
.tree-node .pts {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 4px;
|
||||||
|
right: 4px;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 10px;
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
padding: 1px 5px;
|
||||||
|
border-radius: 2px;
|
||||||
|
color: var(--sand);
|
||||||
|
}
|
||||||
|
.tree-node .name {
|
||||||
|
font-family: 'Cormorant Garamond', serif;
|
||||||
|
font-size: 14px;
|
||||||
|
text-align: center;
|
||||||
|
line-height: 1.1;
|
||||||
|
padding: 4px;
|
||||||
|
color: var(--ink-dim);
|
||||||
|
}
|
||||||
|
.tree-edges {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 0;
|
||||||
|
}
|
||||||
|
.tree-node-wrap { position: relative; z-index: 1; }
|
||||||
|
|
||||||
|
/* Faction tiers list */
|
||||||
|
.tier-list {
|
||||||
|
display: grid;
|
||||||
|
gap: 6px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
.tier-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 36px 1fr auto auto;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border: 1px solid var(--line-soft);
|
||||||
|
border-radius: 3px;
|
||||||
|
background: var(--bg-2);
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--ink-dim);
|
||||||
|
}
|
||||||
|
.tier-row.reached { border-color: var(--sand-2); color: var(--sand); }
|
||||||
|
.tier-row.current { background: rgba(224, 138, 60, 0.08); border-color: var(--spice); }
|
||||||
|
.tier-row .num {
|
||||||
|
font-family: 'Cormorant Garamond', serif;
|
||||||
|
font-size: 18px;
|
||||||
|
color: var(--sand);
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-bar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
font-family: 'JetBrains Mono', monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--ink-muted);
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
border-top: 1px solid var(--line-soft);
|
||||||
|
padding-top: 18px;
|
||||||
|
margin-top: 40px;
|
||||||
|
gap: 14px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
.wrap { padding: 32px 18px 64px; }
|
||||||
|
.house-grid { grid-template-columns: 1fr; }
|
||||||
|
.totals { grid-template-columns: 1fr; }
|
||||||
|
.total { border-right: none; border-bottom: 1px solid var(--line-soft); }
|
||||||
|
.total:last-child { border-bottom: none; }
|
||||||
|
}
|
||||||
79
character-builder/frontend/src/types.ts
Normal file
79
character-builder/frontend/src/types.ts
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
export type House = 'atreides' | 'harkonnen';
|
||||||
|
export type ClassId =
|
||||||
|
| 'benegesserit'
|
||||||
|
| 'mentat'
|
||||||
|
| 'planetologist'
|
||||||
|
| 'swordmaster'
|
||||||
|
| 'trooper';
|
||||||
|
export type SpecId =
|
||||||
|
| 'combat'
|
||||||
|
| 'crafting'
|
||||||
|
| 'exploration'
|
||||||
|
| 'gathering'
|
||||||
|
| 'sabotage';
|
||||||
|
|
||||||
|
export interface XpRow {
|
||||||
|
level: number;
|
||||||
|
xpRequired: number;
|
||||||
|
totalXp: number;
|
||||||
|
skillPoints?: number;
|
||||||
|
totalSkillPoints?: number;
|
||||||
|
intelPoints?: number;
|
||||||
|
totalIntelPoints?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface XpTable {
|
||||||
|
header: string[];
|
||||||
|
rows: XpRow[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FactionTier {
|
||||||
|
tier: number;
|
||||||
|
name: string;
|
||||||
|
standingRequired: number;
|
||||||
|
totalStanding: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FactionTable {
|
||||||
|
header: string[];
|
||||||
|
tiers: FactionTier[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SkillNode {
|
||||||
|
tag: string;
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
kind: 'Ability' | 'Attribute' | 'Perk' | 'Spice' | string;
|
||||||
|
row: number;
|
||||||
|
col: number;
|
||||||
|
maxPoints: number;
|
||||||
|
icon: string | null;
|
||||||
|
url: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SkillEdge {
|
||||||
|
from: string;
|
||||||
|
to: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SkillTree {
|
||||||
|
id: ClassId;
|
||||||
|
name: string;
|
||||||
|
nodes: SkillNode[];
|
||||||
|
edges: SkillEdge[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SpecProgress {
|
||||||
|
level: number;
|
||||||
|
xpInto: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BuildState {
|
||||||
|
v: 1; // schema version
|
||||||
|
house: House;
|
||||||
|
classId: ClassId;
|
||||||
|
character: { level: number; xpInto: number };
|
||||||
|
specs: Record<SpecId, SpecProgress>;
|
||||||
|
faction: { tier: number; standingInto: number };
|
||||||
|
skills: Record<string, number>; // tag -> allocated points
|
||||||
|
}
|
||||||
21
character-builder/frontend/tsconfig.app.json
Normal file
21
character-builder/frontend/tsconfig.app.json
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"verbatimModuleSyntax": true,
|
||||||
|
"moduleDetection": "force",
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "preserve",
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": false,
|
||||||
|
"noUnusedParameters": false,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"resolveJsonModule": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
|
||||||
|
}
|
||||||
7
character-builder/frontend/tsconfig.json
Normal file
7
character-builder/frontend/tsconfig.json
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"files": [],
|
||||||
|
"references": [
|
||||||
|
{ "path": "./tsconfig.app.json" },
|
||||||
|
{ "path": "./tsconfig.node.json" }
|
||||||
|
]
|
||||||
|
}
|
||||||
13
character-builder/frontend/tsconfig.node.json
Normal file
13
character-builder/frontend/tsconfig.node.json
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"lib": ["ES2023"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"strict": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
18
character-builder/frontend/vite.config.ts
Normal file
18
character-builder/frontend/vite.config.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import vue from '@vitejs/plugin-vue';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [vue()],
|
||||||
|
server: {
|
||||||
|
host: '0.0.0.0',
|
||||||
|
port: 5173,
|
||||||
|
proxy: {
|
||||||
|
'/api': 'http://localhost:3000',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
build: {
|
||||||
|
target: 'es2022',
|
||||||
|
outDir: 'dist',
|
||||||
|
emptyOutDir: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
319
character-builder/scripts/extract.py
Normal file
319
character-builder/scripts/extract.py
Normal file
|
|
@ -0,0 +1,319 @@
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Extract Dune Awakening game data from saved dune.gaming.tools HTML pages.
|
||||||
|
|
||||||
|
Outputs JSON files into ../data/:
|
||||||
|
- character-xp.json
|
||||||
|
- spec-{combat,crafting,exploration,gathering,sabotage}.json
|
||||||
|
- faction-{atreides,harkonnen}.json
|
||||||
|
- skills-{benegesserit,mentat,planetologist,swordmaster,trooper}.json
|
||||||
|
- index.json (manifest)
|
||||||
|
"""
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
from html.parser import HTMLParser
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
SAMPLE = Path(__file__).resolve().parents[2] / "sample-data"
|
||||||
|
OUT = Path(__file__).resolve().parents[1] / "data"
|
||||||
|
OUT.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- generic <table class="datatable"> extractor ----------
|
||||||
|
class TableExtractor(HTMLParser):
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.in_table = False
|
||||||
|
self.in_row = False
|
||||||
|
self.in_cell = False
|
||||||
|
self.current_row = []
|
||||||
|
self.current_cell = ""
|
||||||
|
self.rows = []
|
||||||
|
self.header = []
|
||||||
|
|
||||||
|
def handle_starttag(self, tag, attrs):
|
||||||
|
attrs = dict(attrs)
|
||||||
|
if tag == "table" and "datatable" in (attrs.get("class") or ""):
|
||||||
|
self.in_table = True
|
||||||
|
elif self.in_table and tag == "tr":
|
||||||
|
self.in_row = True
|
||||||
|
self.current_row = []
|
||||||
|
elif self.in_table and tag in ("td", "th"):
|
||||||
|
self.in_cell = True
|
||||||
|
self.current_cell = ""
|
||||||
|
|
||||||
|
def handle_endtag(self, tag):
|
||||||
|
if tag == "table":
|
||||||
|
self.in_table = False
|
||||||
|
elif tag == "tr" and self.in_row:
|
||||||
|
if self.current_row:
|
||||||
|
self.rows.append(self.current_row)
|
||||||
|
self.in_row = False
|
||||||
|
elif tag in ("td", "th") and self.in_cell:
|
||||||
|
self.current_row.append(self.current_cell.strip())
|
||||||
|
self.in_cell = False
|
||||||
|
|
||||||
|
def handle_data(self, data):
|
||||||
|
if self.in_cell:
|
||||||
|
self.current_cell += data
|
||||||
|
|
||||||
|
|
||||||
|
def parse_table(path: Path):
|
||||||
|
p = TableExtractor()
|
||||||
|
p.feed(path.read_text())
|
||||||
|
return p.rows
|
||||||
|
|
||||||
|
|
||||||
|
def to_int(s: str) -> int:
|
||||||
|
cleaned = re.sub(r"[^\d-]", "", s or "")
|
||||||
|
if not cleaned or cleaned == "-":
|
||||||
|
return 0
|
||||||
|
try:
|
||||||
|
return int(cleaned)
|
||||||
|
except ValueError:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def extract_xp_table(path: Path, value_keys: list[str]) -> list[dict]:
|
||||||
|
"""For tables shaped: [Level | XP Required | Total XP | ...].
|
||||||
|
value_keys names the columns after Level."""
|
||||||
|
rows = parse_table(path)
|
||||||
|
if not rows:
|
||||||
|
return []
|
||||||
|
header = rows[0]
|
||||||
|
out = []
|
||||||
|
for r in rows[1:]:
|
||||||
|
if not r or not r[0].strip():
|
||||||
|
continue
|
||||||
|
try:
|
||||||
|
lvl = to_int(r[0])
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
entry = {"level": lvl}
|
||||||
|
for i, key in enumerate(value_keys, start=1):
|
||||||
|
if i < len(r):
|
||||||
|
entry[key] = to_int(r[i])
|
||||||
|
out.append(entry)
|
||||||
|
return {"header": header, "rows": out}
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- skill tree extractor ----------
|
||||||
|
NODE_RE = re.compile(
|
||||||
|
r'<div\s+role="button"[^>]*class="node[^"]*"[^>]*data-tag="(?P<tag>Skills\.[^"]+)"[^>]*'
|
||||||
|
r'style="(?P<style>[^"]*)"[^>]*>'
|
||||||
|
r'(?P<inner>.*?)</div>\s*</div>',
|
||||||
|
re.DOTALL,
|
||||||
|
)
|
||||||
|
ALT_RE = re.compile(r'alt="([^"]+)"')
|
||||||
|
HREF_RE = re.compile(r'href="(https://dune\.gaming\.tools/skills/[^"]+)"')
|
||||||
|
ICON_RE = re.compile(r'src="\./[^"]*/(t_ui_icon[^"]+\.webp)"')
|
||||||
|
GRID_RE = re.compile(r"grid-area:\s*(\d+)\s*/\s*(\d+)")
|
||||||
|
MAX_PTS_RE = re.compile(r">0/(\d+)<")
|
||||||
|
|
||||||
|
# connector lines: parse SVG <line> within the tree container
|
||||||
|
LINE_RE = re.compile(
|
||||||
|
r'<line[^>]*\sx1="(?P<x1>\d+)"[^>]*\sy1="(?P<y1>\d+)"[^>]*\sx2="(?P<x2>\d+)"[^>]*\sy2="(?P<y2>\d+)"'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def extract_skill_tree(path: Path, class_id: str, class_name: str) -> dict:
|
||||||
|
html = path.read_text()
|
||||||
|
nodes = []
|
||||||
|
# We need to also find the alt text + href + icon WITHIN each node's HTML.
|
||||||
|
# Strategy: walk through all data-tag="Skills..." occurrences, slice from
|
||||||
|
# opening of the node <div> to a balanced close. Simple slice: take 2000 chars
|
||||||
|
# after the tag and parse first alt/href/icon/max within it.
|
||||||
|
for m in re.finditer(r'data-tag="(Skills\.[^"]+)"', html):
|
||||||
|
tag = m.group(1)
|
||||||
|
start = m.start()
|
||||||
|
chunk = html[start : start + 2500]
|
||||||
|
gm = GRID_RE.search(chunk)
|
||||||
|
if not gm:
|
||||||
|
continue
|
||||||
|
row, col = int(gm.group(1)), int(gm.group(2))
|
||||||
|
alt = ALT_RE.search(chunk)
|
||||||
|
href = HREF_RE.search(chunk)
|
||||||
|
icon = ICON_RE.search(chunk)
|
||||||
|
max_pts = MAX_PTS_RE.search(chunk)
|
||||||
|
kind = tag.split(".")[1] if "." in tag else "Unknown"
|
||||||
|
nodes.append(
|
||||||
|
{
|
||||||
|
"tag": tag,
|
||||||
|
"id": tag.split(".")[-1],
|
||||||
|
"name": alt.group(1) if alt else tag.split(".")[-1],
|
||||||
|
"kind": kind, # Ability | Attribute | Perk | Spice
|
||||||
|
"row": row,
|
||||||
|
"col": col,
|
||||||
|
"maxPoints": int(max_pts.group(1)) if max_pts else 1,
|
||||||
|
"icon": icon.group(1) if icon else None,
|
||||||
|
"url": href.group(1) if href else None,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
# de-duplicate nodes by tag (the regex can match twice if the same tag appears in
|
||||||
|
# a connector tooltip etc.)
|
||||||
|
seen = {}
|
||||||
|
for n in nodes:
|
||||||
|
if n["tag"] not in seen:
|
||||||
|
seen[n["tag"]] = n
|
||||||
|
nodes = list(seen.values())
|
||||||
|
|
||||||
|
# Build a position->node lookup. Grid is roughly square with ~73px cells based
|
||||||
|
# on observed example (grid 3/5 -> center 364,220 means col*~73, row*~73 with offset).
|
||||||
|
# We'll learn cell size from the data: if there are connectors, we map each (x,y) to
|
||||||
|
# the nearest node by Euclidean distance.
|
||||||
|
# First compute approximate node centers via grid math, calibrated from any node
|
||||||
|
# we can pin: actually a more reliable approach is to use the connector geometry.
|
||||||
|
edges = []
|
||||||
|
lines = list(LINE_RE.finditer(html))
|
||||||
|
if lines and nodes:
|
||||||
|
# Calibrate: find scale by looking at min/max grid coords vs min/max line coords.
|
||||||
|
all_x = [int(x) for ln in lines for x in (ln.group("x1"), ln.group("x2"))]
|
||||||
|
all_y = [int(y) for ln in lines for y in (ln.group("y1"), ln.group("y2"))]
|
||||||
|
min_x, max_x = min(all_x), max(all_x)
|
||||||
|
min_y, max_y = min(all_y), max(all_y)
|
||||||
|
cols = [n["col"] for n in nodes]
|
||||||
|
rows = [n["row"] for n in nodes]
|
||||||
|
min_c, max_c = min(cols), max(cols)
|
||||||
|
min_r, max_r = min(rows), max(rows)
|
||||||
|
# avoid div by zero
|
||||||
|
sx = (max_x - min_x) / max(1, (max_c - min_c))
|
||||||
|
sy = (max_y - min_y) / max(1, (max_r - min_r))
|
||||||
|
|
||||||
|
def center(n):
|
||||||
|
return (
|
||||||
|
min_x + (n["col"] - min_c) * sx,
|
||||||
|
min_y + (n["row"] - min_r) * sy,
|
||||||
|
)
|
||||||
|
|
||||||
|
centers = {n["tag"]: center(n) for n in nodes}
|
||||||
|
|
||||||
|
def nearest(x, y):
|
||||||
|
best_tag, best_d = None, float("inf")
|
||||||
|
for t, (cx, cy) in centers.items():
|
||||||
|
d = (cx - x) ** 2 + (cy - y) ** 2
|
||||||
|
if d < best_d:
|
||||||
|
best_d = d
|
||||||
|
best_tag = t
|
||||||
|
return best_tag
|
||||||
|
|
||||||
|
seen_edges = set()
|
||||||
|
for ln in lines:
|
||||||
|
x1, y1, x2, y2 = (
|
||||||
|
int(ln.group("x1")),
|
||||||
|
int(ln.group("y1")),
|
||||||
|
int(ln.group("x2")),
|
||||||
|
int(ln.group("y2")),
|
||||||
|
)
|
||||||
|
a = nearest(x1, y1)
|
||||||
|
b = nearest(x2, y2)
|
||||||
|
if a and b and a != b:
|
||||||
|
key = tuple(sorted((a, b)))
|
||||||
|
if key not in seen_edges:
|
||||||
|
seen_edges.add(key)
|
||||||
|
edges.append({"from": key[0], "to": key[1]})
|
||||||
|
|
||||||
|
return {
|
||||||
|
"id": class_id,
|
||||||
|
"name": class_name,
|
||||||
|
"nodes": nodes,
|
||||||
|
"edges": edges,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# ---------- main ----------
|
||||||
|
def main():
|
||||||
|
manifest = {"xp": {}, "factions": {}, "skills": []}
|
||||||
|
|
||||||
|
# Character XP (200 levels, 6 value columns)
|
||||||
|
char_xp = extract_xp_table(
|
||||||
|
SAMPLE / "Character XP Table - Dune Awakening.html",
|
||||||
|
[
|
||||||
|
"xpRequired",
|
||||||
|
"totalXp",
|
||||||
|
"skillPoints",
|
||||||
|
"totalSkillPoints",
|
||||||
|
"intelPoints",
|
||||||
|
"totalIntelPoints",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
(OUT / "character-xp.json").write_text(json.dumps(char_xp, indent=2))
|
||||||
|
manifest["xp"]["character"] = "character-xp.json"
|
||||||
|
|
||||||
|
# Specialization XP (5 tracks)
|
||||||
|
specs = ["Combat", "Crafting", "Exploration", "Gathering", "Sabotage"]
|
||||||
|
for spec in specs:
|
||||||
|
data = extract_xp_table(
|
||||||
|
SAMPLE / f"{spec} Track XP Table - Dune Awakening.html",
|
||||||
|
["xpRequired", "totalXp", "intelPoints", "totalIntelPoints"],
|
||||||
|
)
|
||||||
|
slug = spec.lower()
|
||||||
|
(OUT / f"spec-{slug}.json").write_text(json.dumps(data, indent=2))
|
||||||
|
manifest["xp"][slug] = f"spec-{slug}.json"
|
||||||
|
|
||||||
|
# Faction standing — different shape: Tier# | TierName | Required Rep | Cumulative
|
||||||
|
factions = [("Atreides", "atreides"), ("Harkonnen", "harkonnen")]
|
||||||
|
for fac_name, fac_id in factions:
|
||||||
|
rows = parse_table(
|
||||||
|
SAMPLE / f"House {fac_name} Faction Standing Table - Dune Awakening.html"
|
||||||
|
)
|
||||||
|
header = rows[0] if rows else []
|
||||||
|
tiers = []
|
||||||
|
for r in rows[1:]:
|
||||||
|
if not r or len(r) < 4:
|
||||||
|
continue
|
||||||
|
tiers.append(
|
||||||
|
{
|
||||||
|
"tier": to_int(r[0]),
|
||||||
|
"name": r[1].strip(),
|
||||||
|
"standingRequired": to_int(r[2]),
|
||||||
|
"totalStanding": to_int(r[3]),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
(OUT / f"faction-{fac_id}.json").write_text(
|
||||||
|
json.dumps({"header": header, "tiers": tiers}, indent=2)
|
||||||
|
)
|
||||||
|
manifest["factions"][fac_id] = f"faction-{fac_id}.json"
|
||||||
|
|
||||||
|
# Skill trees
|
||||||
|
classes = [
|
||||||
|
("Bene Gesserit", "benegesserit"),
|
||||||
|
("Mentat", "mentat"),
|
||||||
|
("Planetologist", "planetologist"),
|
||||||
|
("Swordmaster", "swordmaster"),
|
||||||
|
("Trooper", "trooper"),
|
||||||
|
]
|
||||||
|
for cls_name, cls_id in classes:
|
||||||
|
path = SAMPLE / f"Dune Awakening Skill Builder - {cls_name}.html"
|
||||||
|
if not path.exists():
|
||||||
|
print(f"!! missing {path.name}")
|
||||||
|
continue
|
||||||
|
tree = extract_skill_tree(path, cls_id, cls_name)
|
||||||
|
(OUT / f"skills-{cls_id}.json").write_text(json.dumps(tree, indent=2))
|
||||||
|
manifest["skills"].append(
|
||||||
|
{
|
||||||
|
"id": cls_id,
|
||||||
|
"name": cls_name,
|
||||||
|
"file": f"skills-{cls_id}.json",
|
||||||
|
"nodes": len(tree["nodes"]),
|
||||||
|
"edges": len(tree["edges"]),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
(OUT / "index.json").write_text(json.dumps(manifest, indent=2))
|
||||||
|
|
||||||
|
# Print summary
|
||||||
|
print("\n=== Extraction summary ===")
|
||||||
|
cx = json.loads((OUT / "character-xp.json").read_text())
|
||||||
|
print(f"character XP rows: {len(cx['rows'])} cols: {cx['header']}")
|
||||||
|
for spec in ["combat", "crafting", "exploration", "gathering", "sabotage"]:
|
||||||
|
d = json.loads((OUT / f"spec-{spec}.json").read_text())
|
||||||
|
print(f" spec {spec:11s} rows: {len(d['rows'])}")
|
||||||
|
for fac in ["atreides", "harkonnen"]:
|
||||||
|
d = json.loads((OUT / f"faction-{fac}.json").read_text())
|
||||||
|
print(f" faction {fac:9s} tiers: {len(d['tiers'])} cols: {d['header']}")
|
||||||
|
for s in manifest["skills"]:
|
||||||
|
print(f" skills {s['id']:14s} nodes: {s['nodes']:3d} edges: {s['edges']:3d}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
662
specialization-calculator/index.html
Normal file
662
specialization-calculator/index.html
Normal file
|
|
@ -0,0 +1,662 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>Lansraad Specialization Calculator</title>
|
||||||
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
||||||
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
||||||
|
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600&family=Inter:wght@400;500;600&display=swap" rel="stylesheet" />
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--bg: #1a1410;
|
||||||
|
--bg-2: #221a14;
|
||||||
|
--panel: #2b2018;
|
||||||
|
--panel-2: #352720;
|
||||||
|
--line: #4a3628;
|
||||||
|
--line-soft: #3a2a1f;
|
||||||
|
--ink: #f4e9d8;
|
||||||
|
--ink-dim: #c9b89c;
|
||||||
|
--ink-muted: #8a7560;
|
||||||
|
--sand: #e6c98a;
|
||||||
|
--sand-2: #d4a85a;
|
||||||
|
--ember: #c8643a;
|
||||||
|
--spice: #e08a3c;
|
||||||
|
--green: #8fb87a;
|
||||||
|
--shadow: 0 1px 0 rgba(255,255,255,0.04), 0 12px 32px -16px rgba(0,0,0,0.6);
|
||||||
|
}
|
||||||
|
* { box-sizing: border-box; }
|
||||||
|
html, body { margin: 0; padding: 0; background: var(--bg); color: var(--ink); }
|
||||||
|
body {
|
||||||
|
font-family: "Inter", system-ui, sans-serif;
|
||||||
|
font-size: 15px;
|
||||||
|
line-height: 1.5;
|
||||||
|
background:
|
||||||
|
radial-gradient(1200px 600px at 80% -10%, rgba(224,138,60,0.10), transparent 60%),
|
||||||
|
radial-gradient(900px 500px at -10% 110%, rgba(212,168,90,0.06), transparent 60%),
|
||||||
|
var(--bg);
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
.wrap { max-width: 1180px; margin: 0 auto; padding: 56px 32px 96px; }
|
||||||
|
header.hero { margin-bottom: 40px; }
|
||||||
|
.eyebrow {
|
||||||
|
font-family: "JetBrains Mono", monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
letter-spacing: 0.22em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--sand-2);
|
||||||
|
margin-bottom: 14px;
|
||||||
|
display: flex; align-items: center; gap: 12px;
|
||||||
|
}
|
||||||
|
.eyebrow::after {
|
||||||
|
content: ""; flex: 1; height: 1px; background: linear-gradient(90deg, var(--line) 0%, transparent 100%);
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-family: "Cormorant Garamond", serif;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: clamp(40px, 5vw, 64px);
|
||||||
|
line-height: 1.0;
|
||||||
|
margin: 0 0 14px;
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
h1 em { font-style: italic; color: var(--sand); font-weight: 500; }
|
||||||
|
.lede {
|
||||||
|
color: var(--ink-dim);
|
||||||
|
max-width: 640px;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Settings strip */
|
||||||
|
.settings {
|
||||||
|
background: linear-gradient(180deg, var(--panel) 0%, var(--panel-2) 100%);
|
||||||
|
border: 1px solid var(--line-soft);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 22px 26px;
|
||||||
|
margin: 36px 0 28px;
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto 1fr auto;
|
||||||
|
gap: 28px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.settings-title {
|
||||||
|
font-family: "JetBrains Mono", monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
letter-spacing: 0.2em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--ink-muted);
|
||||||
|
border-right: 1px solid var(--line-soft);
|
||||||
|
padding-right: 28px;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.settings-fields {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 24px;
|
||||||
|
}
|
||||||
|
.field { display: flex; flex-direction: column; gap: 6px; }
|
||||||
|
.field label {
|
||||||
|
font-family: "JetBrains Mono", monospace;
|
||||||
|
font-size: 10.5px;
|
||||||
|
letter-spacing: 0.18em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--ink-muted);
|
||||||
|
}
|
||||||
|
.field input[type="number"] {
|
||||||
|
background: var(--bg-2);
|
||||||
|
border: 1px solid var(--line-soft);
|
||||||
|
color: var(--ink);
|
||||||
|
font-family: "JetBrains Mono", monospace;
|
||||||
|
font-size: 16px;
|
||||||
|
padding: 10px 12px;
|
||||||
|
border-radius: 3px;
|
||||||
|
width: 100%;
|
||||||
|
transition: border-color .15s, background .15s;
|
||||||
|
}
|
||||||
|
.field input[type="number"]:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--sand-2);
|
||||||
|
background: #1f1812;
|
||||||
|
}
|
||||||
|
/* Custom toggle */
|
||||||
|
.toggle-wrap {
|
||||||
|
display: flex; align-items: center; gap: 12px;
|
||||||
|
background: var(--bg-2);
|
||||||
|
border: 1px solid var(--line-soft);
|
||||||
|
border-radius: 3px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
|
height: 42px;
|
||||||
|
}
|
||||||
|
.toggle-wrap input { display: none; }
|
||||||
|
.toggle-pill {
|
||||||
|
width: 36px; height: 20px; border-radius: 999px;
|
||||||
|
background: #1a1310;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
position: relative;
|
||||||
|
transition: background .2s, border-color .2s;
|
||||||
|
}
|
||||||
|
.toggle-pill::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute; top: 2px; left: 2px;
|
||||||
|
width: 14px; height: 14px; border-radius: 50%;
|
||||||
|
background: var(--ink-muted);
|
||||||
|
transition: transform .2s, background .2s;
|
||||||
|
}
|
||||||
|
.toggle-wrap.on .toggle-pill {
|
||||||
|
background: rgba(224,138,60,0.18);
|
||||||
|
border-color: var(--spice);
|
||||||
|
}
|
||||||
|
.toggle-wrap.on .toggle-pill::after {
|
||||||
|
transform: translateX(16px);
|
||||||
|
background: var(--spice);
|
||||||
|
}
|
||||||
|
.toggle-label {
|
||||||
|
font-family: "JetBrains Mono", monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
letter-spacing: 0.16em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--ink-dim);
|
||||||
|
}
|
||||||
|
.toggle-wrap.on .toggle-label { color: var(--sand); }
|
||||||
|
|
||||||
|
/* Totals */
|
||||||
|
.totals {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 0;
|
||||||
|
margin: 28px 0 36px;
|
||||||
|
border: 1px solid var(--line-soft);
|
||||||
|
border-radius: 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
background: var(--bg-2);
|
||||||
|
}
|
||||||
|
.total {
|
||||||
|
padding: 22px 26px;
|
||||||
|
border-right: 1px solid var(--line-soft);
|
||||||
|
background: linear-gradient(180deg, rgba(255,255,255,0.015), transparent);
|
||||||
|
}
|
||||||
|
.total:last-child { border-right: none; }
|
||||||
|
.total .lbl {
|
||||||
|
font-family: "JetBrains Mono", monospace;
|
||||||
|
font-size: 10.5px;
|
||||||
|
letter-spacing: 0.2em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--ink-muted);
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.total .val {
|
||||||
|
font-family: "Cormorant Garamond", serif;
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 42px;
|
||||||
|
line-height: 1;
|
||||||
|
color: var(--sand);
|
||||||
|
}
|
||||||
|
.total .val .unit {
|
||||||
|
font-family: "JetBrains Mono", monospace;
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--ink-muted);
|
||||||
|
letter-spacing: 0.15em;
|
||||||
|
margin-left: 8px;
|
||||||
|
vertical-align: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Cards */
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
|
||||||
|
gap: 18px;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
background: linear-gradient(180deg, var(--panel) 0%, var(--panel-2) 100%);
|
||||||
|
border: 1px solid var(--line-soft);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 22px 24px 20px;
|
||||||
|
box-shadow: var(--shadow);
|
||||||
|
position: relative;
|
||||||
|
transition: border-color .15s;
|
||||||
|
}
|
||||||
|
.card:hover { border-color: var(--line); }
|
||||||
|
.card.disabled { opacity: 0.55; }
|
||||||
|
|
||||||
|
.card-head {
|
||||||
|
display: flex; align-items: center; justify-content: space-between;
|
||||||
|
margin-bottom: 18px;
|
||||||
|
padding-bottom: 14px;
|
||||||
|
border-bottom: 1px dashed var(--line-soft);
|
||||||
|
}
|
||||||
|
.card-name {
|
||||||
|
font-family: "Cormorant Garamond", serif;
|
||||||
|
font-size: 26px;
|
||||||
|
font-weight: 500;
|
||||||
|
letter-spacing: 0.01em;
|
||||||
|
}
|
||||||
|
.card-sym {
|
||||||
|
font-family: "JetBrains Mono", monospace;
|
||||||
|
font-size: 10px;
|
||||||
|
letter-spacing: 0.2em;
|
||||||
|
color: var(--ink-muted);
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
.card-include {
|
||||||
|
display: flex; align-items: center; gap: 8px;
|
||||||
|
cursor: pointer; user-select: none;
|
||||||
|
}
|
||||||
|
.card-include input { accent-color: var(--spice); }
|
||||||
|
.card-include span {
|
||||||
|
font-family: "JetBrains Mono", monospace;
|
||||||
|
font-size: 10px; letter-spacing: 0.16em; text-transform: uppercase;
|
||||||
|
color: var(--ink-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: grid; grid-template-columns: 1fr 1fr; gap: 12px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
.row.single { grid-template-columns: 1fr; }
|
||||||
|
.row .field input[type="number"] { font-size: 15px; padding: 8px 10px; }
|
||||||
|
|
||||||
|
.results {
|
||||||
|
margin-top: 18px;
|
||||||
|
padding-top: 16px;
|
||||||
|
border-top: 1px solid var(--line-soft);
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
.result {
|
||||||
|
display: flex; flex-direction: column; gap: 4px;
|
||||||
|
}
|
||||||
|
.result .rl {
|
||||||
|
font-family: "JetBrains Mono", monospace;
|
||||||
|
font-size: 9.5px; letter-spacing: 0.18em; text-transform: uppercase;
|
||||||
|
color: var(--ink-muted);
|
||||||
|
}
|
||||||
|
.result .rv {
|
||||||
|
font-family: "JetBrains Mono", monospace;
|
||||||
|
font-size: 18px;
|
||||||
|
color: var(--ink);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.result .rv.warn { color: var(--ember); }
|
||||||
|
.result .rv.good { color: var(--green); }
|
||||||
|
.result .rv .ru { color: var(--ink-muted); font-size: 11px; margin-left: 3px; }
|
||||||
|
|
||||||
|
.progress {
|
||||||
|
margin-top: 14px;
|
||||||
|
height: 4px;
|
||||||
|
background: var(--bg-2);
|
||||||
|
border-radius: 999px;
|
||||||
|
overflow: hidden;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.progress::before {
|
||||||
|
content: ""; position: absolute; inset: 0;
|
||||||
|
width: var(--pct, 0%);
|
||||||
|
background: linear-gradient(90deg, var(--ember) 0%, var(--spice) 60%, var(--sand) 100%);
|
||||||
|
transition: width .25s ease;
|
||||||
|
}
|
||||||
|
.progress-meta {
|
||||||
|
display: flex; justify-content: space-between;
|
||||||
|
font-family: "JetBrains Mono", monospace;
|
||||||
|
font-size: 10px; color: var(--ink-muted);
|
||||||
|
margin-top: 6px;
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reset-bar {
|
||||||
|
margin-top: 28px;
|
||||||
|
display: flex; justify-content: space-between; align-items: center;
|
||||||
|
color: var(--ink-muted);
|
||||||
|
font-family: "JetBrains Mono", monospace;
|
||||||
|
font-size: 11px; letter-spacing: 0.14em;
|
||||||
|
}
|
||||||
|
.reset-bar button {
|
||||||
|
background: transparent;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
color: var(--ink-dim);
|
||||||
|
font-family: "JetBrains Mono", monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
letter-spacing: 0.18em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: 3px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: border-color .15s, color .15s, background .15s;
|
||||||
|
}
|
||||||
|
.reset-bar button:hover {
|
||||||
|
border-color: var(--sand-2);
|
||||||
|
color: var(--sand);
|
||||||
|
background: rgba(224,138,60,0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
footer.foot {
|
||||||
|
margin-top: 64px;
|
||||||
|
padding-top: 24px;
|
||||||
|
border-top: 1px solid var(--line-soft);
|
||||||
|
font-family: "JetBrains Mono", monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--ink-muted);
|
||||||
|
letter-spacing: 0.1em;
|
||||||
|
display: flex; justify-content: space-between; flex-wrap: wrap; gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Subtle decoration */
|
||||||
|
.corner {
|
||||||
|
position: absolute; width: 14px; height: 14px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
}
|
||||||
|
.corner.tl { top: 8px; left: 8px; border-right: none; border-bottom: none; }
|
||||||
|
.corner.tr { top: 8px; right: 8px; border-left: none; border-bottom: none; }
|
||||||
|
.corner.bl { bottom: 8px; left: 8px; border-right: none; border-top: none; }
|
||||||
|
.corner.br { bottom: 8px; right: 8px; border-left: none; border-top: none; }
|
||||||
|
|
||||||
|
/* Spinner removal on number inputs */
|
||||||
|
input[type=number]::-webkit-inner-spin-button,
|
||||||
|
input[type=number]::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; }
|
||||||
|
input[type=number] { -moz-appearance: textfield; }
|
||||||
|
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
.wrap { padding: 32px 18px 64px; }
|
||||||
|
.settings { grid-template-columns: 1fr; gap: 18px; }
|
||||||
|
.settings-title { border-right: none; border-bottom: 1px solid var(--line-soft); padding: 0 0 14px; }
|
||||||
|
.settings-fields { grid-template-columns: 1fr; }
|
||||||
|
.totals { grid-template-columns: 1fr; }
|
||||||
|
.total { border-right: none; border-bottom: 1px solid var(--line-soft); }
|
||||||
|
.total:last-child { border-bottom: none; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="wrap">
|
||||||
|
<header class="hero">
|
||||||
|
<div class="eyebrow">Lansraad — Specialization Ledger</div>
|
||||||
|
<h1>Calculate the XP, quests, and days <em>to your next rank</em>.</h1>
|
||||||
|
<p class="lede">Plug in your current level for each specialization and where you want to be. The ledger handles the math — total XP needed, quests to clear it, and how many days that takes at your pace.</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<section class="settings" aria-label="Global settings">
|
||||||
|
<div class="settings-title">Pace<br/>& Rate</div>
|
||||||
|
<div class="settings-fields">
|
||||||
|
<div class="field">
|
||||||
|
<label for="xpPerQuest">XP per Quest</label>
|
||||||
|
<input id="xpPerQuest" type="number" min="1" value="125" />
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label for="questsPerDay">Quests per Day</label>
|
||||||
|
<input id="questsPerDay" type="number" min="1" value="5" />
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label> </label>
|
||||||
|
<label class="toggle-wrap" id="dblToggle" for="doubleXp">
|
||||||
|
<input id="doubleXp" type="checkbox" />
|
||||||
|
<span class="toggle-pill"></span>
|
||||||
|
<span class="toggle-label">Double XP Weekend</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="text-align:right;">
|
||||||
|
<div style="font-family: 'JetBrains Mono', monospace; font-size: 10.5px; letter-spacing: 0.2em; color: var(--ink-muted); text-transform: uppercase;">Effective</div>
|
||||||
|
<div style="font-family: 'Cormorant Garamond', serif; font-size: 32px; color: var(--sand); line-height: 1;"><span id="effectiveXp">125</span> <span style="font-family: 'JetBrains Mono', monospace; font-size: 11px; color: var(--ink-muted); letter-spacing: 0.15em;">XP/Q</span></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="totals" aria-label="Totals">
|
||||||
|
<div class="total">
|
||||||
|
<div class="lbl">Total XP Needed</div>
|
||||||
|
<div class="val"><span id="totalXp">0</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="total">
|
||||||
|
<div class="lbl">Total Quests</div>
|
||||||
|
<div class="val"><span id="totalQuests">0</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="total">
|
||||||
|
<div class="lbl">Total Days</div>
|
||||||
|
<div class="val"><span id="totalDays">0</span><span class="unit">@ pace</span></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="grid" id="cards"></section>
|
||||||
|
|
||||||
|
<div class="reset-bar">
|
||||||
|
<div>Saved locally · refresh-safe</div>
|
||||||
|
<button id="resetBtn">Reset All</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer class="foot">
|
||||||
|
<div>Lansraad XP table · Levels 1–100</div>
|
||||||
|
<div>Unofficial fan-made calculator · not affiliated with the game's publisher</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// XP required to advance INTO each level (i.e. A[N] is XP to go from N-1 -> N, per the source sheet)
|
||||||
|
// Levels 1..100
|
||||||
|
const XP_TABLE = [
|
||||||
|
100,105,110,116,122,128,135,142,149,157,
|
||||||
|
164,171,178,186,194,202,211,220,229,239,
|
||||||
|
246,253,260,268,276,284,292,301,310,319,
|
||||||
|
326,334,342,350,358,366,375,384,393,402,
|
||||||
|
410,418,426,434,442,450,459,468,477,486,
|
||||||
|
494,502,510,518,526,534,543,552,561,570,
|
||||||
|
575,580,585,590,595,600,606,612,618,624,
|
||||||
|
624,624,624,624,624,624,624,624,624,624,
|
||||||
|
624,624,624,624,624,624,624,624,624,624,
|
||||||
|
624,624,624,624,624,624,624,624,624,624
|
||||||
|
];
|
||||||
|
const MAX_LEVEL = 100;
|
||||||
|
|
||||||
|
const SPECS = [
|
||||||
|
{ id: 'crafting', name: 'Crafting', sym: 'CRF', cur: 52, into: 66, goal: 66 },
|
||||||
|
{ id: 'gathering', name: 'Gathering', sym: 'GTH', cur: 22, into: 23, goal: 23 },
|
||||||
|
{ id: 'exploration', name: 'Exploration', sym: 'EXP', cur: 40, into: 41, goal: 41 },
|
||||||
|
{ id: 'combat', name: 'Combat', sym: 'CMB', cur: 50, into: 51, goal: 51 },
|
||||||
|
{ id: 'sabotage', name: 'Sabotage', sym: 'SAB', cur: 21, into: 27, goal: 27 }
|
||||||
|
];
|
||||||
|
|
||||||
|
const STORAGE_KEY = 'lansraad-calc-v1';
|
||||||
|
|
||||||
|
// Default state
|
||||||
|
const defaultState = () => ({
|
||||||
|
xpPerQuest: 125,
|
||||||
|
questsPerDay: 5,
|
||||||
|
doubleXp: false,
|
||||||
|
specs: SPECS.reduce((acc, s) => {
|
||||||
|
acc[s.id] = {
|
||||||
|
currentLevel: s.cur,
|
||||||
|
xpInto: 0,
|
||||||
|
desiredLevel: s.goal,
|
||||||
|
included: true
|
||||||
|
};
|
||||||
|
return acc;
|
||||||
|
}, {})
|
||||||
|
});
|
||||||
|
|
||||||
|
let state = defaultState();
|
||||||
|
try {
|
||||||
|
const saved = JSON.parse(localStorage.getItem(STORAGE_KEY) || 'null');
|
||||||
|
if (saved && saved.specs) state = { ...defaultState(), ...saved, specs: { ...defaultState().specs, ...saved.specs }};
|
||||||
|
} catch(e) {}
|
||||||
|
|
||||||
|
function save() {
|
||||||
|
try { localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); } catch(e) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// XP from current level (with partial XP into next) to desired level
|
||||||
|
// Logic from sheet: SUM(A[current+1 .. desired]) - xpInto
|
||||||
|
// i.e. you've already earned xpInto toward the next level
|
||||||
|
function calcXp(curLvl, xpInto, desiredLvl) {
|
||||||
|
curLvl = clamp(Math.floor(curLvl||0), 0, MAX_LEVEL);
|
||||||
|
desiredLvl = clamp(Math.floor(desiredLvl||0), 0, MAX_LEVEL);
|
||||||
|
xpInto = Math.max(0, Math.floor(xpInto||0));
|
||||||
|
if (desiredLvl <= curLvl) return 0;
|
||||||
|
let sum = 0;
|
||||||
|
for (let lvl = curLvl + 1; lvl <= desiredLvl; lvl++) {
|
||||||
|
sum += XP_TABLE[lvl - 1] || 0; // A[lvl] = XP_TABLE[lvl-1]
|
||||||
|
}
|
||||||
|
return Math.max(0, sum - xpInto);
|
||||||
|
}
|
||||||
|
function clamp(v, lo, hi) { return Math.min(hi, Math.max(lo, v)); }
|
||||||
|
|
||||||
|
function effectiveXpPerQuest() {
|
||||||
|
const base = Math.max(1, Number(state.xpPerQuest) || 1);
|
||||||
|
return state.doubleXp ? base * 2 : base;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render
|
||||||
|
const cardsEl = document.getElementById('cards');
|
||||||
|
function render() {
|
||||||
|
// Settings inputs reflect state
|
||||||
|
document.getElementById('xpPerQuest').value = state.xpPerQuest;
|
||||||
|
document.getElementById('questsPerDay').value = state.questsPerDay;
|
||||||
|
document.getElementById('doubleXp').checked = state.doubleXp;
|
||||||
|
document.getElementById('dblToggle').classList.toggle('on', state.doubleXp);
|
||||||
|
document.getElementById('effectiveXp').textContent = fmt(effectiveXpPerQuest());
|
||||||
|
|
||||||
|
// Cards
|
||||||
|
cardsEl.innerHTML = SPECS.map(s => {
|
||||||
|
const st = state.specs[s.id];
|
||||||
|
const xp = calcXp(st.currentLevel, st.xpInto, st.desiredLevel);
|
||||||
|
const eff = effectiveXpPerQuest();
|
||||||
|
const quests = xp > 0 ? Math.ceil(xp / eff) : 0;
|
||||||
|
const qpd = Math.max(1, Number(state.questsPerDay) || 1);
|
||||||
|
const days = quests > 0 ? Math.ceil(quests / qpd) : 0;
|
||||||
|
|
||||||
|
// progress: where the player is within their current level
|
||||||
|
const nextLevelXp = XP_TABLE[clamp(st.currentLevel, 0, MAX_LEVEL-1)] || 0;
|
||||||
|
const pct = nextLevelXp > 0 ? clamp((st.xpInto / nextLevelXp) * 100, 0, 100) : 0;
|
||||||
|
|
||||||
|
const valid = st.desiredLevel > st.currentLevel;
|
||||||
|
const disabled = !st.included;
|
||||||
|
|
||||||
|
return `
|
||||||
|
<article class="card ${disabled ? 'disabled' : ''}" data-id="${s.id}">
|
||||||
|
<span class="corner tl"></span><span class="corner tr"></span>
|
||||||
|
<span class="corner bl"></span><span class="corner br"></span>
|
||||||
|
<div class="card-head">
|
||||||
|
<div>
|
||||||
|
<div class="card-sym">${s.sym}</div>
|
||||||
|
<div class="card-name">${s.name}</div>
|
||||||
|
</div>
|
||||||
|
<label class="card-include">
|
||||||
|
<input type="checkbox" data-field="included" ${st.included ? 'checked' : ''} />
|
||||||
|
<span>${st.included ? 'Tracking' : 'Skipped'}</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="field">
|
||||||
|
<label>Current Level</label>
|
||||||
|
<input type="number" min="0" max="${MAX_LEVEL}" data-field="currentLevel" value="${st.currentLevel}" />
|
||||||
|
</div>
|
||||||
|
<div class="field">
|
||||||
|
<label>Desired Level</label>
|
||||||
|
<input type="number" min="0" max="${MAX_LEVEL}" data-field="desiredLevel" value="${st.desiredLevel}" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row single">
|
||||||
|
<div class="field">
|
||||||
|
<label>XP into Next Level</label>
|
||||||
|
<input type="number" min="0" data-field="xpInto" value="${st.xpInto}" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="progress" style="--pct: ${pct}%"></div>
|
||||||
|
<div class="progress-meta">
|
||||||
|
<span>L${st.currentLevel} → L${st.currentLevel+1}</span>
|
||||||
|
<span>${fmt(st.xpInto)} / ${fmt(nextLevelXp)} XP</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="results">
|
||||||
|
<div class="result">
|
||||||
|
<div class="rl">XP Needed</div>
|
||||||
|
<div class="rv ${!valid ? 'good' : ''}">${valid ? fmt(xp) : '—'}</div>
|
||||||
|
</div>
|
||||||
|
<div class="result">
|
||||||
|
<div class="rl">Quests</div>
|
||||||
|
<div class="rv">${valid ? fmt(quests) : '—'}</div>
|
||||||
|
</div>
|
||||||
|
<div class="result">
|
||||||
|
<div class="rl">Days</div>
|
||||||
|
<div class="rv">${valid ? fmt(days) : '—'}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
`;
|
||||||
|
}).join('');
|
||||||
|
|
||||||
|
// Totals (across included specs)
|
||||||
|
let totalXp = 0;
|
||||||
|
SPECS.forEach(s => {
|
||||||
|
const st = state.specs[s.id];
|
||||||
|
if (!st.included) return;
|
||||||
|
totalXp += calcXp(st.currentLevel, st.xpInto, st.desiredLevel);
|
||||||
|
});
|
||||||
|
const eff = effectiveXpPerQuest();
|
||||||
|
const totalQuests = totalXp > 0 ? Math.ceil(totalXp / eff) : 0;
|
||||||
|
const qpd = Math.max(1, Number(state.questsPerDay) || 1);
|
||||||
|
const totalDays = totalQuests > 0 ? Math.ceil(totalQuests / qpd) : 0;
|
||||||
|
|
||||||
|
document.getElementById('totalXp').textContent = fmt(totalXp);
|
||||||
|
document.getElementById('totalQuests').textContent = fmt(totalQuests);
|
||||||
|
document.getElementById('totalDays').textContent = fmt(totalDays);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fmt(n) {
|
||||||
|
return Number(n||0).toLocaleString('en-US');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Event delegation for card inputs
|
||||||
|
cardsEl.addEventListener('input', e => {
|
||||||
|
const t = e.target;
|
||||||
|
const field = t.dataset.field;
|
||||||
|
if (!field) return;
|
||||||
|
const card = t.closest('.card');
|
||||||
|
const id = card.dataset.id;
|
||||||
|
const st = state.specs[id];
|
||||||
|
if (field === 'included') {
|
||||||
|
st.included = t.checked;
|
||||||
|
} else {
|
||||||
|
let v = Number(t.value);
|
||||||
|
if (isNaN(v)) v = 0;
|
||||||
|
if (field === 'currentLevel' || field === 'desiredLevel') v = clamp(Math.floor(v), 0, MAX_LEVEL);
|
||||||
|
if (field === 'xpInto') v = Math.max(0, Math.floor(v));
|
||||||
|
st[field] = v;
|
||||||
|
}
|
||||||
|
save();
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
|
||||||
|
cardsEl.addEventListener('change', e => {
|
||||||
|
if (e.target.dataset.field === 'included') {
|
||||||
|
// re-render to update label text
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Settings handlers
|
||||||
|
document.getElementById('xpPerQuest').addEventListener('input', e => {
|
||||||
|
state.xpPerQuest = Math.max(1, Number(e.target.value) || 1);
|
||||||
|
save(); render();
|
||||||
|
});
|
||||||
|
document.getElementById('questsPerDay').addEventListener('input', e => {
|
||||||
|
state.questsPerDay = Math.max(1, Number(e.target.value) || 1);
|
||||||
|
save(); render();
|
||||||
|
});
|
||||||
|
document.getElementById('doubleXp').addEventListener('change', e => {
|
||||||
|
state.doubleXp = e.target.checked;
|
||||||
|
save(); render();
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('resetBtn').addEventListener('click', () => {
|
||||||
|
state = defaultState();
|
||||||
|
// Zero out the starter goals so it's a clean slate
|
||||||
|
Object.values(state.specs).forEach(s => { s.currentLevel = 0; s.desiredLevel = 0; s.xpInto = 0; });
|
||||||
|
save(); render();
|
||||||
|
});
|
||||||
|
|
||||||
|
render();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
Reference in a new issue