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 10 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-24

### 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;
}
43 changes: 31 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 Down Expand Up @@ -191,14 +192,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 +249,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 +266,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 +298,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 +313,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 +341,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 +350,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 +426,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 +1038,26 @@ const convertSpellHigherLevels = (ddbSpell: DdbSpell): AlchemySpellAtHigherLevel
}
}
*/

const convertTrackers = (ddbCharacter: DdbCharacter): AlchemyTracker[] => {
return [
{
name: 'XP',
category: 'experience',
color: 'Yellow',
max: 355000,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

doesn't necessarily need to hold up this PR, but I imagine the most common way people use the XP tracker is to track XP until their next level (rather than theoretical max XP at lv20, or something). seems like all we'd need to do to support that is to have a static map between levels and XP counts, and then use the character's level to put the correct total here.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point! And yeah, we actually do this for characters created in Alchemy, too. I could yoink that function from our code.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright, I went head and pulled in those functions from the Alchemy 5e implementation and updated the way the XP tracker is created so that it tracks XP until next level now.

value: ddbCharacter.currentXp,
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
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);
});
});
14 changes: 10 additions & 4 deletions test/convert.exp.test.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,31 @@
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}
`(
'returns exp=$expected when currentXp=$currentXp',
'returns tracker.value=$expected when currentXp=$currentXp',
({ currentXp, expected }) => {
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(expected);
expect(expTracker.max).toEqual(355000);
},
);
});
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);
},
);
});