Skip to content

Commit

Permalink
Merge pull request #57 from alchemyrpg/trackers
Browse files Browse the repository at this point in the history
  • Loading branch information
voidrender authored Nov 27, 2023
2 parents 20c3adc + a429c82 commit d75a707
Show file tree
Hide file tree
Showing 9 changed files with 162 additions and 34 deletions.
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);
},
);
});

0 comments on commit d75a707

Please sign in to comment.