Skip to content
Draft
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
57 changes: 49 additions & 8 deletions src/alchemy.d.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export interface AlchemyCharacter {
abilityScores: AlchemyStat[],
actions?: AlchemyAction[],
age?: string,
armorClass: number,
copper?: number,
Expand Down Expand Up @@ -95,7 +96,7 @@ interface AlchemyCustomSpell {
castingTime: string,
components: string[],
duration: string,
damage?: AlchemyDamage[],
damage?: AlchemyDiceRoll[],
higherLevelDescription: string,
higherLevels: AlchemySpellAtHigherLevel[],
range: string,
Expand All @@ -110,13 +111,7 @@ interface AlchemyCustomSpell {

interface AlchemySpellAtHigherLevel {
applyAtLevels: number[],
damage: AlchemyDamage,
type: string,
}

interface AlchemyDamage {
bonus?: number,
dice: string,
damage: AlchemyDiceRoll,
type: string,
}

Expand All @@ -138,3 +133,49 @@ interface AlchemyMovementMode {
mode: string,
distance: number,
}

interface AlchemyAction {
name: string,
description: string,
sortOrder?: number,
steps: AlchemyActionStep[],
}

interface AlchemyActionStep {
type: string,
journalCommand?: AlchemyActionStepJournalCommand,
diceRoll?: AlchemyDiceRoll[],
attack?: AlchemyAttack,
skillCheck?: AlchemySkillCheck,
}

interface AlchemyActionStepJournalCommand {
command: string,
args: string,
}

interface AlchemyDiceRoll {
abilityName?: string,
bonus?: number,
dice: string,
type?: string,
}

interface AlchemyAttack {
ability: string,
bonus?: number,
crit: number,
damageRolls: AlchemyDiceRoll[],
isProficient: boolean,
isRanged: boolean,
name: string,
range?: number,
longRange?: number,
rollsAttack?: boolean,
savingThrow?: AlchemySavingThrow,
}

interface AlchemySkillCheck {
skillName: string,
rollModifier: string,
}
179 changes: 157 additions & 22 deletions src/convert.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { DdbArmorType, DdbModifier, DdbCharacter, DdbProficiencyType, DdbSpell, DdbSpellActivationType, DDB_SPEED_IS_RE, DDB_SPEED_EQUALS_RE, DDB_SPELL_ACTIVATION_TYPE, DDB_SPELL_COMPONENT_TYPE } from "./ddb"
import { AlchemyCharacter, AlchemyStat, AlchemyClass, AlchemyProficiency, AlchemyMovementMode, AlchemyTextBlockSection, AlchemySkill, AlchemyItem, AlchemySpellSlot, AlchemySpell, AlchemyDamage, AlchemySpellAtHigherLevel } from "./alchemy"
import { DdbArmorType, DdbModifier, DdbCharacter, DdbEntityType, DdbActivationType, DDB_ACTIVATION_TYPE, DdbItem, DdbNoteTypeId, DdbStatType, DdbAttackType, DdbAction, DDB_WEAPON_CATEGORY, DdbSpell, DDB_SPEED_IS_RE, DDB_SPEED_EQUALS_RE, DDB_SPELL_COMPONENT_TYPE } from "./ddb"
import { AlchemyCharacter, AlchemyStat, AlchemyClass, AlchemyProficiency, AlchemyMovementMode, AlchemyTextBlockSection, AlchemySkill, AlchemyItem, AlchemySpellSlot, AlchemySpell, AlchemyAction, AlchemyDiceRoll } from "./alchemy"
import TurndownService from 'turndown'
import * as turndownPluginGfm from 'turndown-plugin-gfm'

Expand Down Expand Up @@ -172,6 +172,7 @@ turndownService.use(turndownPluginGfm.gfm)
export const convertCharacter = (ddbCharacter: DdbCharacter): AlchemyCharacter => ({
abilityScores: convertStatArray(ddbCharacter),
...(ddbCharacter.age) && { age: ddbCharacter.age.toString() },
actions: convertActions(ddbCharacter),
armorClass: getArmorClass(ddbCharacter),
copper: ddbCharacter.currencies.cp,
classes: convertClasses(ddbCharacter),
Expand Down Expand Up @@ -427,7 +428,7 @@ const convertProficiencies = (ddbCharacter: DdbCharacter): AlchemyProficiency[]
// weapons
proficiencies.push(
getModifiers(ddbCharacter, { type: "proficiency" })
.filter(modifier => modifier.entityTypeId === DdbProficiencyType.Weapon)
.filter(modifier => modifier.entityTypeId === DdbEntityType.Weapon)
.map(modifier => ({
name: modifier.friendlySubtypeName,
type: "weapon",
Expand All @@ -437,7 +438,7 @@ const convertProficiencies = (ddbCharacter: DdbCharacter): AlchemyProficiency[]
// tools
proficiencies.push(
getModifiers(ddbCharacter, { type: "proficiency" })
.filter(modifier => modifier.entityTypeId === DdbProficiencyType.Tool)
.filter(modifier => modifier.entityTypeId === DdbEntityType.Tool)
.map(modifier => ({
name: modifier.friendlySubtypeName,
type: "tool",
Expand All @@ -447,7 +448,7 @@ const convertProficiencies = (ddbCharacter: DdbCharacter): AlchemyProficiency[]
// armor
proficiencies.push(
getModifiers(ddbCharacter, { type: "proficiency" })
.filter(modifier => modifier.entityTypeId === DdbProficiencyType.Armor)
.filter(modifier => modifier.entityTypeId === DdbEntityType.Armor)
.map(modifier => ({
name: modifier.friendlySubtypeName,
type: "armor",
Expand All @@ -463,7 +464,7 @@ const getSkills = (ddbCharacter: DdbCharacter): AlchemySkill[] => {
const expertise = getModifiers(ddbCharacter, { type: "expertise" })
.map(modifier => modifier.friendlySubtypeName)
const proficient = getModifiers(ddbCharacter, { type: "proficiency" })
.filter(modifier => modifier.entityTypeId === DdbProficiencyType.Skill)
.filter(modifier => modifier.entityTypeId === DdbEntityType.Skill)
.map(modifier => modifier.friendlySubtypeName)

// Get all skills and set proficiency/expertise accordingly
Expand Down Expand Up @@ -735,17 +736,14 @@ const convertSpell = (ddbSpell: DdbSpell): AlchemySpell => {
const convertSpellCastingTime = (ddbSpell: DdbSpell): string => {
const spell = ddbSpell.definition
switch (spell.activation.activationType) {
case DdbSpellActivationType.Action:
case DdbSpellActivationType.BonusAction:
case DdbSpellActivationType.Reaction:
return `1 ${DDB_SPELL_ACTIVATION_TYPE[spell.activation.activationType]}`
case DdbSpellActivationType.Minute:
case DdbSpellActivationType.Hour:
case DdbSpellActivationType.Day:
case DdbSpellActivationType.LegendaryAction:
case DdbSpellActivationType.LairAction:
case DdbActivationType.Action:
case DdbActivationType.BonusAction:
case DdbActivationType.Reaction:
return `1 ${DDB_ACTIVATION_TYPE[spell.activation.activationType]}`
case DdbActivationType.Minute:
case DdbActivationType.Day:
const s = spell.activation.activationTime > 1 ? "s" : ""
return `${spell.activation.activationTime} ${DDB_SPELL_ACTIVATION_TYPE[spell.activation.activationType]}${s}`
return `${spell.activation.activationTime} ${DDB_ACTIVATION_TYPE[spell.activation.activationType]}${s}`
default:
return ""
}
Expand All @@ -772,14 +770,18 @@ const convertSpellRange = (ddbSpell: DdbSpell): string => {
}

// Convert a spell's damage to Alchemy format
const convertSpellDamage = (ddbSpell: DdbSpell): AlchemyDamage[] => {
const convertSpellDamage = (ddbSpell: DdbSpell): AlchemyDiceRoll[] => {
return ddbSpell.definition.modifiers
.filter(modifier => modifier.type == "damage")
.map(modifier => ({
type: modifier.friendlySubtypeName,
dice: `${modifier.die.diceCount}d${modifier.die.diceValue}`,
bonus: modifier.die.fixedValue,
}))
.map(modifier => {
const dice = modifier.die || modifier.dice

return {
type: modifier.friendlySubtypeName,
dice: `${dice.diceCount}d${dice.diceValue}`,
bonus: dice.fixedValue,
}
})
}

// Convert a spell's damage at higher levels to Alchemy format
Expand Down Expand Up @@ -816,3 +818,136 @@ const convertSpellHigherLevels = (ddbSpell: DdbSpell): AlchemySpellAtHigherLevel
}
}
*/

// Convert anything that seems like an action to an Alchemy action
const convertActions = (ddbCharacter: DdbCharacter): AlchemyAction[] => {
const actions = []

// Add attack actions for any items that deal damage
ddbCharacter.inventory
.filter(item => item.definition.damage)
.map(item => actions.push(createItemAttackAction(ddbCharacter, item)))

// Add actions for all other defined actions, from any source
Object.entries(ddbCharacter.actions)
.flatMap(([source, actions]) => actions)
.filter(action => action)
.map(action => actions.push(createGenericAction(action)))

// Add a sortOrder to each action and return
return actions.map((action, index) => ({
sortOrder: index,
...action
}))
}

const createItemAttackAction = (ddbCharacter: DdbCharacter, item: DdbItem): AlchemyAction => {
// Get the name, checking if it was set as a custom name
let itemName = item.definition.name
const itemNameNotes = ddbCharacter.characterValues
.filter(note => note.valueId == item.id.toString())
.find(note => note.typeId == DdbNoteTypeId.Name)
if (itemNameNotes) { itemName = itemNameNotes.value.toString() }

// Generate a name for the action with the item's type, if it's not obvious
const actionName = itemName == item.definition.type ? itemName : `${itemName} (${item.definition.type})`

// Use str as the damage stat unless the item has finesse, in which case
// use the higher of str and dex
let attackAbility = "str"
if (item.definition.properties.find(prop => prop.name == "Finesse")) {
if (getStatBonus(ddbCharacter, DdbStatType.Dexterity) > getStatBonus(ddbCharacter, DdbStatType.Strength)) {
attackAbility = "dex"
}
}

// Calculate damage dice, starting with base damage and type
const actionDamageRolls: AlchemyDiceRoll[] = []
actionDamageRolls.push({
type: item.definition.damageType,
dice: item.definition.damage.diceString,
bonus: item.definition.damage.fixedValue,
abilityName: attackAbility,
})
item.definition.grantedModifiers
.filter(modifier => modifier.type == "damage")
.forEach(modifier => {
const dice = modifier.die || modifier.dice

actionDamageRolls.push({
type: modifier.friendlySubtypeName,
...(dice) && { dice: dice.diceString },
bonus: modifier.fixedValue || dice.fixedValue,
})
})

// Check if proficient in weapon, weapon type ("light"), or weapon category ("martial")
const isProficientInWeapon = !!getModifiers(ddbCharacter, { type: "proficiency" })
.filter(modifier => modifier.entityTypeId === DdbEntityType.Weapon)
.find(modifier => modifier.friendlySubtypeName === item.definition.type)
const isProficientInWeaponType = !!getModifiers(ddbCharacter, { type: "proficiency" })
.filter(modifier => modifier.entityTypeId === DdbEntityType.WeaponType)
.find(modifier => {
const proficientType = modifier.friendlySubtypeName
const weaponTypes = item.definition.properties.map(prop => `${prop.name} Weapons`)
return weaponTypes.includes(proficientType)
})
const isProficientInWeaponCategory = !!getModifiers(ddbCharacter, { type: "proficiency" })
.filter(modifier => modifier.entityTypeId === DdbEntityType.WeaponType)
.find(modifier => {
const proficientType = modifier.friendlySubtypeName
const weaponCategory = `${DDB_WEAPON_CATEGORY[item.definition.categoryId]} Weapons`
return proficientType === weaponCategory
})

// Create the action
return {
name: actionName,
description: turndownService.turndown(item.definition.description || ""),
steps: [{
attack: {
ability: attackAbility,
crit: 20, // no way to detect/set this in DDB right now
damageRolls: actionDamageRolls,
isProficient: isProficientInWeapon || isProficientInWeaponType || isProficientInWeaponCategory,
isRanged: item.definition.attackType == DdbAttackType.Ranged,
name: itemName,
},
type: "custom-attack"
}]
}
}

const createGenericAction = (ddbAction: DdbAction): AlchemyAction => {
const dice = ddbAction.dice || ddbAction.die

// Add dice roll actions for any actions that can roll dice
if (dice) {
return {
name: ddbAction.name,
description: turndownService.turndown(ddbAction.description || ""),
steps: [{
diceRoll: [{
dice: dice.diceString,
bonus: dice.fixedValue,
}],
type: "custom-dice-roll"
}]
}
}

// Add journal command actions for any other actions that can be activated
if (ddbAction.activation.activationType) {
return {
name: ddbAction.name,
description: turndownService.turndown(ddbAction.description || ""),
steps: [{
journalCommand: {
command: "/me",
args: `uses ${ddbAction.name}!`,
},
type: "journal-command"
}]
}
}
}
Loading