Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Trackers #57

Merged
merged 13 commits into from
Nov 27, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [0.2.0] - 2023-11-26

### Changed

- Trackers (HP, XP) are now created in ddb2alchemy instead of in Alchemy itself.

## [0.1.8] - 2023-10-12

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ddb2alchemy",
"version": "0.1.8",
"version": "0.2.0",
"description": "Convert D&D Beyond characters for use with the Alchemy VTT.",
"main": "src/index.ts",
"license": "MIT",
Expand Down
22 changes: 19 additions & 3 deletions src/alchemy.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@ export interface AlchemyCharacter {
armorClass: number;
copper?: number;
classes: AlchemyClass[];
currentHp: number;
electrum?: number;
exp: number;
eyes?: string;
gold?: number;
hair?: string;
Expand All @@ -16,7 +14,6 @@ export interface AlchemyCharacter {
items: AlchemyItem[];
isNPC: boolean;
isSpellcaster: Boolean;
maxHp: number;
movementModes: AlchemyMovementMode[];
name: string;
platinum?: number;
Expand All @@ -34,6 +31,7 @@ export interface AlchemyCharacter {
spells: AlchemySpell[];
textBlocks: AlchemyTextBlockSection[];
weight?: string;
trackers?: AlchemyTracker[];
}

interface AlchemyStat {
Expand Down Expand Up @@ -138,3 +136,21 @@ interface AlchemyMovementMode {
mode: string;
distance: number;
}

interface AlchemyTracker {
name: string;
value: number;
max: number;
color:
| 'Blue'
| 'Green'
| 'Orange'
| 'Purple'
| 'Red'
| 'Theme Accent'
| 'Yellow';
type: 'Bar' | 'Pip';
category: 'health' | 'experience' | null;
sortOrder?: number;
readOnly?: boolean;
}
47 changes: 35 additions & 12 deletions src/convert.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ import {
AlchemySpellSlot,
AlchemyStat,
AlchemyTextBlockSection,
} from './alchemy';
AlchemyTracker,
} from './alchemy.d';
import {
DDB_SPEED_EQUALS_RE,
DDB_SPEED_IS_RE,
Expand All @@ -25,6 +26,7 @@ import {
DdbSpell,
DdbSpellActivationType,
} from './ddb';
import { getExperienceRequiredForNextLevel } from './fifth-edition';

// Shared between both platforms
const STR = 1;
Expand Down Expand Up @@ -191,14 +193,11 @@ export const DEFAULT_ALCHEMY_CHARACTER: AlchemyCharacter = {
abilityScores: [],
armorClass: 0,
classes: [],
currentHp: 0,
exp: 0,
imageUri: '',
initiativeBonus: 0,
isNPC: false,
isSpellcaster: false,
items: [],
maxHp: 0,
movementModes: [],
name: '',
proficiencies: [],
Expand Down Expand Up @@ -251,9 +250,7 @@ export const convertCharacter = (
...shouldConvert(options, 'armorClass', () => getArmorClass(ddbCharacter)),
...shouldConvert(options, 'copper', () => ddbCharacter.currencies.cp),
...shouldConvert(options, 'classes', () => convertClasses(ddbCharacter)),
...shouldConvert(options, 'currentHp', () => getCurrentHp(ddbCharacter)),
...shouldConvert(options, 'electrum', () => ddbCharacter.currencies.ep),
...shouldConvert(options, 'exp', () => ddbCharacter.currentXp),
...shouldConvert(options, 'eyes', () => ddbCharacter.eyes),
...shouldConvert(options, 'gold', () => ddbCharacter.currencies.gp),
...shouldConvert(options, 'hair', () => ddbCharacter.hair),
Expand All @@ -270,7 +267,6 @@ export const convertCharacter = (
isSpellcaster(ddbCharacter),
),
...shouldConvert(options, 'items', () => convertItems(ddbCharacter)),
...shouldConvert(options, 'maxHp', () => getMaxHp(ddbCharacter)),
...shouldConvert(options, 'movementModes', () =>
getMovementModes(ddbCharacter),
),
Expand Down Expand Up @@ -303,6 +299,7 @@ export const convertCharacter = (
...shouldConvert(options, 'weight', () =>
ddbCharacter.weight ? ddbCharacter.weight.toString() : '',
),
...shouldConvert(options, 'trackers', () => convertTrackers(ddbCharacter)),
});

// Convert D&D Beyond style stat arrays to Alchemy style stat arrays
Expand All @@ -317,7 +314,7 @@ const convertStatArray = (ddbCharacter: DdbCharacter): AlchemyStat[] => {
const getStatValue = (ddbCharacter: DdbCharacter, statId: number): number => {
// Start with whatever the base stat is at level 1
const baseStatValue =
ddbCharacter.stats.find((stat) => stat.id === statId)?.value ||
ddbCharacter.stats?.find((stat) => stat.id === statId)?.value ||
BASE_STAT;

// If there are any overrides, use the highest of those instead of the base value
Expand Down Expand Up @@ -345,7 +342,7 @@ const getModifiers = (
ddbCharacter: DdbCharacter,
options: object,
): DdbModifier[] => {
return Object.values(ddbCharacter.modifiers)
return Object.values(ddbCharacter.modifiers || {})
.flat()
.filter((modifier) =>
Object.keys(options).every((key) => modifier[key] === options[key]),
Expand All @@ -354,15 +351,15 @@ const getModifiers = (

// Find all applicable modifiers based on keys/values in `options` and sum them
const sumModifiers = (ddbCharacter: DdbCharacter, options: object): number => {
return getModifiers(ddbCharacter, options).reduce(
return getModifiers(ddbCharacter, options)?.reduce(
(total, modifier) => total + modifier.value,
0,
);
};

// Find all applicable modifiers based on keys/values in `options` and take the highest
const maxModifier = (ddbCharacter: DdbCharacter, options: object): number => {
return getModifiers(ddbCharacter, options).reduce(
return getModifiers(ddbCharacter, options)?.reduce(
(max, modifier) => Math.max(max, modifier.value),
0,
);
Expand Down Expand Up @@ -430,7 +427,7 @@ const getArmorClass = (ddbCharacter: DdbCharacter): number => {
// Calculate the base HP of the character, inclusive of bonus from CON modifier.
const getBaseHp = (ddbCharacter: DdbCharacter): number => {
const conBonus = getStatBonus(ddbCharacter, CON);
const levels = ddbCharacter.classes.reduce(
const levels = ddbCharacter.classes?.reduce(
(total, c) => total + c.level,
0,
);
Expand Down Expand Up @@ -1042,3 +1039,29 @@ const convertSpellHigherLevels = (ddbSpell: DdbSpell): AlchemySpellAtHigherLevel
}
}
*/

const convertTrackers = (ddbCharacter: DdbCharacter): AlchemyTracker[] => {
const totalExp = ddbCharacter.currentXp;
const nextLevelExp = getExperienceRequiredForNextLevel(totalExp);

return [
{
name: 'XP',
category: 'experience',
color: 'Yellow',
max: nextLevelExp,
value: totalExp,
type: 'Bar',
sortOrder: 0,
},
{
name: 'HP',
category: 'health',
color: 'Green',
max: getMaxHp(ddbCharacter),
value: getCurrentHp(ddbCharacter),
type: 'Bar',
sortOrder: 0,
},
];
};
4 changes: 2 additions & 2 deletions src/ddb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ export interface DdbCharacter {
gp: number;
pp: number;
};
classes: DdbClass[];
modifiers: {
classes?: DdbClass[];
modifiers?: {
race: DdbModifier[];
class: DdbModifier[];
item: DdbModifier[];
Expand Down
51 changes: 51 additions & 0 deletions src/fifth-edition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* Returns the experience required for the next level given a 5e character's
* total experience.
* @param currentExp The character's current total experience.
* @returns The amount of experience required for the next level.
*/
export const getExperienceRequiredForNextLevel = (
currentExp: number,
): number => {
if (currentExp < 300) {
return 300;
} else if (currentExp < 900) {
return 900;
} else if (currentExp < 2700) {
return 2700;
} else if (currentExp < 6500) {
return 6500;
} else if (currentExp < 14000) {
return 14000;
} else if (currentExp < 23000) {
return 23000;
} else if (currentExp < 34000) {
return 34000;
} else if (currentExp < 48000) {
return 48000;
} else if (currentExp < 64000) {
return 64000;
} else if (currentExp < 85000) {
return 85000;
} else if (currentExp < 100000) {
return 100000;
} else if (currentExp < 120000) {
return 120000;
} else if (currentExp < 140000) {
return 140000;
} else if (currentExp < 165000) {
return 165000;
} else if (currentExp < 195000) {
return 195000;
} else if (currentExp < 225000) {
return 225000;
} else if (currentExp < 265000) {
return 265000;
} else if (currentExp < 305000) {
return 305000;
} else if (currentExp < 355000) {
return 355000;
} else {
return 0;
}
};
18 changes: 12 additions & 6 deletions test/convert.currentHp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,12 @@ describe('Convert DDB current HP to Alchemy current HP', () => {
ddbChar.overrideHitPoints = overrideHitPoints;

const converted = convertCharacter(ddbChar as DdbCharacter, {
currentHp: true,
trackers: true,
});

expect(converted.currentHp).toEqual(overrideHitPoints);
expect(
converted.trackers?.find((t) => t.category === 'health')?.value,
).toEqual(overrideHitPoints);
});

test.each`
Expand Down Expand Up @@ -74,10 +76,12 @@ describe('Convert DDB current HP to Alchemy current HP', () => {
con;

const converted = convertCharacter(ddbChar as DdbCharacter, {
currentHp: true,
trackers: true,
});

expect(converted.currentHp).toEqual(expected);
expect(
converted.trackers?.find((t) => t.category === 'health')?.value,
).toEqual(expected);
},
);

Expand All @@ -91,9 +95,11 @@ describe('Convert DDB current HP to Alchemy current HP', () => {
ddbChar.classes.push({ level: 1 });

const converted = convertCharacter(ddbChar as DdbCharacter, {
currentHp: true,
trackers: true,
});

expect(converted.currentHp).toEqual(2);
expect(
converted.trackers?.find((t) => t.category === 'health')?.value,
).toEqual(2);
});
});
40 changes: 32 additions & 8 deletions test/convert.exp.test.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,49 @@
import { describe, expect, test } from '@jest/globals';
import { convertCharacter } from '../src';

import { DdbCharacter } from '../src/ddb';
import { DeepPartial } from './test-helpers';

describe('Convert DDB currentXp to Alchemy exp', () => {
describe('Convert DDB currentXp to Alchemy tracker', () => {
test.each`
currentXp | expected
${10} | ${10}
${0} | ${0}
currentXp | expectedValue | expectedMax
${0} | ${0} | ${300}
${300} | ${300} | ${900}
${900} | ${900} | ${2700}
${2700} | ${2700} | ${6500}
${6500} | ${6500} | ${14000}
${14000} | ${14000} | ${23000}
${23000} | ${23000} | ${34000}
${34000} | ${34000} | ${48000}
${48000} | ${48000} | ${64000}
${64000} | ${64000} | ${85000}
${85000} | ${85000} | ${100000}
${100000} | ${100000} | ${120000}
${120000} | ${120000} | ${140000}
${140000} | ${140000} | ${165000}
${165000} | ${165000} | ${195000}
${195000} | ${195000} | ${225000}
${225000} | ${225000} | ${265000}
${265000} | ${265000} | ${305000}
${305000} | ${305000} | ${355000}
${355000} | ${355000} | ${0}
`(
'returns exp=$expected when currentXp=$currentXp',
({ currentXp, expected }) => {
'returns tracker.value=$expectedValue and tracker.max=$expectedMax when currentXp=$currentXp',
({ currentXp, expectedValue, expectedMax }) => {
const ddbChar: DeepPartial<DdbCharacter> = {
currentXp,
};

const converted = convertCharacter(ddbChar as DdbCharacter, {
exp: true,
trackers: true,
});

expect(converted.exp).toEqual(expected);
const expTracker = converted.trackers?.find(
(t) => t.category === 'experience',
);

expect(expTracker.value).toEqual(expectedValue);
expect(expTracker.max).toEqual(expectedMax);
},
);
});
6 changes: 4 additions & 2 deletions test/convert.maxHp.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,12 @@ describe('Convert DDB maxHP to Alchemy maxHP', () => {
con;

const converted = convertCharacter(ddbChar as DdbCharacter, {
maxHp: true,
trackers: true,
});

expect(converted.maxHp).toEqual(expected);
expect(
converted.trackers?.find((t) => t.category === 'health')?.max,
).toEqual(expected);
},
);
});