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

Allow Spell items to be converted to Scroll-type Consumable items #86

Closed
aaclayton opened this issue Feb 18, 2019 · 18 comments
Closed

Allow Spell items to be converted to Scroll-type Consumable items #86

aaclayton opened this issue Feb 18, 2019 · 18 comments
Assignees
Milestone

Comments

@aaclayton
Copy link
Contributor

we have a quite complete Spells SRD compendium which contains spell items

we also have "template" spell scrolls for levels 1 - 9 in the Items SRD

I'd love to have a utility function which takes a spell-type Item as an input and provides a properly configured spell scroll of that spell as an output

this could allow for us to drop spells either on the spellbook tab of the character sheet to add spells to the spellbook OR on the inventory tab to create spell scrolls of that spell

@aaclayton
Copy link
Contributor Author

marked this issue as related to #87

@aaclayton
Copy link
Contributor Author

Originally in GitLab by @ayyrickay

Poking around the code, I see a couple of relevant methods in the 5e Actor Sheet:

The Drag Item Start method looks like it has some logic for handling item drag events, but I'm guessing it's for items that are being dragged FROM the character sheet.

It also has an onDrop Event that looks more promising.

I'm thinking that the onDrop Event will need to check to see:

  1. Is the item a spell &&
  2. Did the item get dropped in the inventory tab

I'll have to check out the event to see if that info is available. But if it is, I could call that function to convert the spell object to a consumable, create a clone of the spell object assigned with the consumable data, and pass that to the inherited onDrop() method.

I looked through the repo and there are a couple of things I can't find:

  1. Event Details
  2. Schemas for the DND5e.ConsumableScrolls and Spells?

I can figure it out from the template file, but I figure there's always a chance I'll miss something or that the relationship won't be 1:1.

@aaclayton
Copy link
Contributor Author

My advice for the angle of attack here would be to separate the feature into two halves.

  1. A helper function that takes a spell item and returns a consumable item. e.g.
function createScrollFromSpell(spell: Item5e) : Item5e {
  ...
}
  1. An interaction flow where when an item is dropped on the character sheet we sometimes call this function before creating an owned item.

My advice would be to start with #1 first and ignore interaction workflows for now and simply start with the logic to do the transformation properly.

You can find Level 1 through L9 scrolls in the Items5e compendium pack. My vision would be something that blends the data structure of those consumables with the actual spell details of the spell itself.

@aaclayton
Copy link
Contributor Author

Originally in GitLab by @ayyrickay

Yeah, makes sense. Just looked at the spell and spell scroll objects, and it looks like there's plenty involved in that, haha.

Is there a standard/preference for the UX of this? Two choices I'm already seeing are:

  1. The resulting spell scroll name (I think Spell Scroll: <Spell Name> looks nice)
  2. The spell scroll description including the spell description, and a shortened version of the generic spell scroll information.

@aaclayton
Copy link
Contributor Author

Good question, I think my intuition would be that the item name would be something like "Scroll of {Spell Name}", but "Spell Scroll: Spell Name" also seems good.

For the description, I agree, there should probably be some component of the description that explains that it's a scroll and how scrolls work, while still containing the actual spell description.

@aaclayton
Copy link
Contributor Author

Originally in GitLab by @ayyrickay

image

Here's a first stab, with a test for creating a Scroll of Light.

I think the logic is straightforward enough, it's just a lot of boilerplate. I tried to use spread operators to make things a little more concise, while still leaving the code readable enough that you could come back and remember what's going on.

I'm not sure where this type of file even goes and/or if there are better places for the boilerplate and helper functions in it. Happy to get feedback on the code quality and whether or not it's solving the problem in the meantime! :D

@aaclayton
Copy link
Contributor Author

I'm not able to open the file you attached - perhaps you could post the function as a snippet or just directly inline enclosed in triple backticks (`)

My thinking is that either the converstion function can live as an Item5e instance method which converts the current spell item into a consumable, or we can just leave it as a helper function in the 5e module.

@aaclayton
Copy link
Contributor Author

Originally in GitLab by @ayyrickay

const spellScrollBasis = {
    "name": "",
    "permission": {
        "default": 0
    },
    "type": "consumable",
    "data": {
        "description": {
            "value": "",
            "chat": "",
            "unidentified": ""
        },
        "source": "DMG pg. 200",
        "quantity": 1,
        "weight": 0,
        "price": 10,
        "attuned": false,
        "equipped": false,
        "rarity": "Common",
        "identified": true,
        "activation": {
            "type": "action",
            "cost": 1,
            "condition": "Spell must be on the caster's spell list."
        },
        "duration": {
            "value": null,
            "units": ""
        },
        "target": {
            "value": null,
            "units": "",
            "type": ""
        },
        "range": {
            "value": null,
            "long": null,
            "units": ""
        },
        "uses": {
            "value": 1,
            "max": 1,
            "per": "charges",
            "autoUse": true,
            "autoDestroy": true
        },
        "ability": "",
        "actionType": "msak",
        "attackBonus": null,
        "chatFlavor": "",
        "critical": null,
        "damage": {
            "parts": [],
            "versatile": ""
        },
        "formula": "",
        "save": {
            "ability": "",
            "dc": null,
            "scaling": "flat"
        },
        "consumableType": "scroll",
        "charges": {
            "value": 1,
            "max": 1,
            "_deprecated": true
        },
        "consume": {
            "value": "",
            "_deprecated": true
        },
        "autoUse": {
            "value": true,
            "_deprecated": true
        },
        "autoDestroy": {
            "value": true,
            "_deprecated": true
        },
        "attributes": {
            "spelldc": 10
        }
    },
    "sort": 0,
    "flags": {},
    "img": "systems/dnd5e/icons/items/inventory/note-scroll.jpg",
    "_id": ""
}

// TODO: Why is the sort field always 600000?
// To this point, is there a way to do this dynamically?
// No evidence of other files reading from the .db files
const spellScrollDifferences = {
    0: {
        "data": {"price": 10, "attackBonus": 5, save: {dc: 13}},
        "sort": 500000,
        "img": "systems/dnd5e/icons/items/inventory/note-scroll.jpg",
        "_id": "rQ6sO7HDWzqMhSI3"
    },
    1: {
        "data": {"price": 60, "attackBonus": 5, save: {dc: 13}},
        "sort": 2900000,
        "img": "systems/dnd5e/icons/items/inventory/note-scroll.jpg",
        "_id": "9GSfMg0VOA2b4uFN"
    },
    2: {
        "data": {"price": 120, "attackBonus": 5, save: {dc: 13}},
        "sort": 700000,
        "img": "systems/dnd5e/icons/items/inventory/scroll-druid.jpg",
        "_id": "XdDp6CKh9qEvPTuS"
    },
    3: {
        "data": {"price": 200, "attackBonus": 7, save: {dc: 15}},
        "sort": 600000,
        "img": "systems/dnd5e/icons/items/inventory/scroll-druid.jpg",
        "_id": "hqVKZie7x9w3Kqds"
    },
    4: {
        data: {"price": 320, "attackBonus": 7, "save": {dc: 15}},
        "sort": 600000,
        "img": "systems/dnd5e/icons/items/inventory/scroll-cursed.jpg",
        "_id": "DM7hzgL836ZyUFB1"
    },
    5: {
        "data": {"price": 640, "attackBonus": 9, "save": {dc: 17}},
        "sort": 600000,
        "img": "systems/dnd5e/icons/items/inventory/scroll-cursed.jpg",
        "_id": "wa1VF8TXHmkrrR35"
    },
    6: {
        "data": {"price": 1280, "attackBonus": 9, "save": {dc: 17}},
        "sort": 600000,
        "img": "systems/dnd5e/icons/items/inventory/scroll-magic.jpg",
        "_id": "tI3rWx4bxefNCexS"
    },
    7: {
        "data": {"price": 2560, "attackBonus": 10, "save": {dc: 18}},
        "sort": 600000,
        "img": "systems/dnd5e/icons/items/inventory/scroll-magic.jpg",
        "_id": "mtyw4NS1s7j2EJaD"
    },
    8: {
        "data": {"price": 5120, "attackBonus": 10, "save": {dc: 18}},
        "sort": 600000,
        "img": "systems/dnd5e/icons/items/inventory/scroll-magic.jpg",
        "_id": "aOrinPg7yuDZEuWr"
    },
    9: {
        "data": {"price": 5120, "attackBonus": 11, "save": {dc: 19}},
        "sort": 600000,
        "img": "systems/dnd5e/icons/items/inventory/scroll-secret.jpg",
        "_id": "O4YbkJkLlnsgUszZ"
    }

}

  /**
   * Retrieve a subset of the Spell object necessary for generating a spell scroll
   * @param {Item5e} spell     The spell to be made into a scroll
   */
function getSpellSubsetForScroll (spell) {
    const {name, data: {description, source, activation, duration, target, range, damage, save, level}} = spell

    return {
        name,
        data: {
            description,
            source,
            activation,
            duration,
            target,
            range,
            damage,
            save,
            level
        }
    }
}
/**
 * Create a spell scroll object from a spell object
 * @param {Item5e} spell     The spell to be made into a scroll
 * @private
 */
function createSpellScrollDescription (spell) {
    return `<p>A spell scroll bears the words for ${spell.name}, written in a mystical cipher. If the spell is on your class's spell list, you can use an action to read the scroll and cast its spell without having to provide any of the spell's components. </p><h2>${spell.name}</h2>${spell.data.description.value}<hr><p>Once the spell is cast, the words on the scroll fade, and the scroll itself crumbles to dust.</p><p>A wizard spell on a spell scroll can be copied just as spells in spellbooks can be copied. When a spell is copied from a spell scroll, the copier must succeed on an Intelligence (Arcana) check with a DC equal to 10. If the check succeeds, the spell is successfully copied. Whether the check succeeds or fails, the spell scroll is destroyed.</p>`
}

/**
 * Create a spell scroll object from a spell object
 * @param {Item5e} spell     The spell to be made into a scroll
 * @private
 */
function createScrollFromSpell(spell) {
    // Get spell data
    const spellSubset = getSpellSubsetForScroll(spell)
    const scrollDescription = createSpellScrollDescription(spell)

    // Get scroll data
    const scrollData = spellScrollDifferences[spellSubset.data.level]
    scrollData.name = `Scroll of ${spellSubset.name}`

    return Object.assign({
        ...spellScrollBasis,
        ...spellSubset,
        ...scrollData,
        data: Object.assign({
            ...spellScrollBasis.data,
            ...spellSubset.data,
            ...scrollData.data,
            description: Object.assign(spellSubset.data.description, {value: scrollDescription}),
            save: Object.assign({...spellSubset.data.save, ...scrollData.data.save}, {scaling: "flat"})
        }),
    })
}

const testScroll1  = createScrollFromSpell({
    "_id": "Bnn9Nzajixvow9xi",
    "name": "Light",
    "permission": {
        "default": 0
    },
    "type": "spell",
    "data": {
        "description": {
            "value": "<p>You touch one object that is no larger than 10 feet in any dimension. Until the spell ends, the object sheds bright light in a 20-foot radius and dim light for an additional 20 feet. The light can be colored as you like. Completely covering the object with something opaque blocks the light. The spell ends if you cast it again or dismiss it as an action.</p><p>If you target an object held or worn by a hostile creature, that creature must succeed on a Dexterity saving throw to avoid the spell.</p>",
            "chat": "",
            "unidentified": ""
        },
        "source": "PHB pg. 255",
        "activation": {
            "type": "action",
            "cost": 1,
            "condition": ""
        },
        "duration": {
            "value": 1,
            "units": "hour"
        },
        "target": {
            "value": 1,
            "units": "",
            "type": "object"
        },
        "range": {
            "value": null,
            "long": null,
            "units": "touch"
        },
        "uses": {
            "value": 0,
            "max": 0,
            "per": ""
        },
        "ability": "",
        "actionType": "save",
        "attackBonus": 0,
        "chatFlavor": "",
        "critical": null,
        "damage": {
            "parts": [],
            "versatile": "",
            "value": ""
        },
        "formula": "",
        "save": {
            "ability": "dex",
            "dc": 0,
            "scaling": "spell"
        },
        "level": 0,
        "school": "evo",
        "components": {
            "value": "",
            "vocal": true,
            "somatic": false,
            "material": true,
            "ritual": false,
            "concentration": false
        },
        "materials": {
            "value": "A firefly or phosphorescent moss.",
            "consumed": false,
            "cost": 0,
            "supply": 0
        },
        "preparation": {
            "mode": "prepared",
            "prepared": false
        },
        "scaling": {
            "mode": "none",
            "formula": ""
        }
    },
    "sort": 100001,
    "flags": {},
    "img": "systems/dnd5e/icons/spells/light-sky-1.jpg"
})

console.log(testScroll1)

@aaclayton
Copy link
Contributor Author

Originally in GitLab by @ayyrickay

My bad. Posted here!

@aaclayton
Copy link
Contributor Author

Originally in GitLab by @ayyrickay

Following up on this. Not sure if the silence is busyness or fear at the spaghetti code put in front of you, hahaha. 😅 Let me know if you're concerned about it, and I can explain my rational for writing it this way and/or improve it. Otherwise, I'll hold tight if it's just busy! :D

@aaclayton
Copy link
Contributor Author

Just me being busy mostly - I'm focused on the core 0.5.6 update now before cycling back to 5e for the next system update.

I wonder if, as the "template" for each created scroll we could use the compendium items from the 5e items pack.

For example, suppose we need to create a Level 3 spell scroll, we could get the basic scroll template for that item from the Items (SRD) compendium:

const scrollTemplate = await game.packs.get("dnd5e.items").getEntry("hqVKZie7x9w3Kqds")

@aaclayton
Copy link
Contributor Author

Originally in GitLab by @ayyrickay

Thanks for the response, and sorry to bug you! I'll hold tight in the future - and PS, if it helps, I'll take what I learn from this and try and document it for any future contributors!

In any case, updated version (plus a makeshift unit test) below. I haven't hooked it into the actual game logic yet, so I'm mostly guessing it works, there might be errors that I need to work through.

/**
 * Create a spell scroll object from a spell object
 * @param {Item5e} spell     The spell to be made into a scroll
 * @private
 */
async function createScrollFromSpell(spell) {
    // Get spell data
    const {name, data: {description, source, activation, duration, target, range, damage, save, level}} = spell

    const spellSubset = {
        name: `Scroll of ${name}`,
        data: {
            description,
            source,
            activation,
            duration,
            target,
            range,
            damage,
            save,
            level
        }
    }

    const spellScrollIds = [
        'rQ6sO7HDWzqMhSI3',
        '9GSfMg0VOA2b4uFN',
        'XdDp6CKh9qEvPTuS',
        'hqVKZie7x9w3Kqds',
        'DM7hzgL836ZyUFB1',
        'wa1VF8TXHmkrrR35',
        'tI3rWx4bxefNCexS',
        'mtyw4NS1s7j2EJaD',
        'aOrinPg7yuDZEuWr',
        'O4YbkJkLlnsgUszZ'
      ]
    
    const scrollDescription = `<p>A spell scroll bears the words for ${spell.name}, written in a mystical cipher. If the spell is on your class's spell list, you can use an action to read the scroll and cast its spell without having to provide any of the spell's components. </p><h2>${name}</h2>${description.value}<hr><p>Once the spell is cast, the words on the scroll fade, and the scroll itself crumbles to dust.</p><p>A wizard spell on a spell scroll can be copied just as spells in spellbooks can be copied. When a spell is copied from a spell scroll, the copier must succeed on an Intelligence (Arcana) check with a DC equal to 10. If the check succeeds, the spell is successfully copied. Whether the check succeeds or fails, the spell scroll is destroyed.</p>`

    // Get scroll data
    const scrollTemplate = await game.packs.get("dnd5e.items").getEntry(spellScrollIds[spell.data.level])

    return Object.assign({
        ...scrollTemplate,
        ...spellSubset,
        data: Object.assign({
            ...scrollTemplate.data,
            description: Object.assign({}, spellSubset.data.description, {value: scrollDescription}),
            save: Object.assign({...spellSubset.data.save}, {scaling: "flat"})
        }),
    })
}

const testScrollExample =  {
    name: 'Scroll of Light',
    permission: { default: 0 },
    type: 'consumable',
    data: {
      description: {
        value: "<p>A spell scroll bears the words for Light, written in a mystical cipher. If the spell is on your class's spell list, you can use an action to read the scroll and cast its spell without having to provide any of the spell's components. </p><h2>Light</h2><p>You touch one object that is no larger than 10 feet in any dimension. Until the spell ends, the object sheds bright light in a 20-foot radius and dim light for an additional 20 feet. The light can be colored as you like. Completely covering the object with something opaque blocks the light. The spell ends if you cast it again or dismiss it as an action.</p><p>If you target an object held or worn by a hostile creature, that creature must succeed on a Dexterity saving throw to avoid the spell.</p><hr><p>Once the spell is cast, the words on the scroll fade, and the scroll itself crumbles to dust.</p><p>A wizard spell on a spell scroll can be copied just as spells in spellbooks can be copied. When a spell is copied from a spell scroll, the copier must succeed on an Intelligence (Arcana) check with a DC equal to 10. If the check succeeds, the spell is successfully copied. Whether the check succeeds or fails, the spell scroll is destroyed.</p>",
        chat: '',
        unidentified: ''
      },
      source: 'PHB pg. 255',
      quantity: 1,
      weight: 0,
      price: 10,
      attuned: false,
      equipped: false,
      rarity: 'Common',
      identified: true,
      activation: { type: 'action', cost: 1, condition: '' },
      duration: { value: 1, units: 'hour' },
      target: { value: 1, units: '', type: 'object' },
      range: { value: null, long: null, units: 'touch' },
      uses: {
        value: 1,
        max: 1,
        per: 'charges',
        autoUse: true,
        autoDestroy: true
      },
      ability: '',
      actionType: 'msak',
      attackBonus: 5,
      chatFlavor: '',
      critical: null,
      damage: { parts: [], versatile: '', value: '' },
      formula: '',
      save: { ability: 'dex', dc: 13, scaling: 'flat' },
      consumableType: 'scroll',
      charges: { value: 1, max: 1, _deprecated: true },
      consume: { value: '', _deprecated: true },
      autoUse: { value: true, _deprecated: true },
      autoDestroy: { value: true, _deprecated: true },
      attributes: { spelldc: 10 },
      level: 0
    },
    sort: 500000,
    flags: {},
    img: 'systems/dnd5e/icons/items/inventory/note-scroll.jpg',
    _id: 'rQ6sO7HDWzqMhSI3'
  }

const lightSpellObject = {
    "_id": "Bnn9Nzajixvow9xi",
    "name": "Light",
    "permission": {
        "default": 0
    },
    "type": "spell",
    "data": {
        "description": {
            "value": "<p>You touch one object that is no larger than 10 feet in any dimension. Until the spell ends, the object sheds bright light in a 20-foot radius and dim light for an additional 20 feet. The light can be colored as you like. Completely covering the object with something opaque blocks the light. The spell ends if you cast it again or dismiss it as an action.</p><p>If you target an object held or worn by a hostile creature, that creature must succeed on a Dexterity saving throw to avoid the spell.</p>",
            "chat": "",
            "unidentified": ""
        },
        "source": "PHB pg. 255",
        "activation": {
            "type": "action",
            "cost": 1,
            "condition": ""
        },
        "duration": {
            "value": 1,
            "units": "hour"
        },
        "target": {
            "value": 1,
            "units": "",
            "type": "object"
        },
        "range": {
            "value": null,
            "long": null,
            "units": "touch"
        },
        "uses": {
            "value": 0,
            "max": 0,
            "per": ""
        },
        "ability": "",
        "actionType": "save",
        "attackBonus": 0,
        "chatFlavor": "",
        "critical": null,
        "damage": {
            "parts": [],
            "versatile": "",
            "value": ""
        },
        "formula": "",
        "save": {
            "ability": "dex",
            "dc": 0,
            "scaling": "spell"
        },
        "level": 0,
        "school": "evo",
        "components": {
            "value": "",
            "vocal": true,
            "somatic": false,
            "material": true,
            "ritual": false,
            "concentration": false
        },
        "materials": {
            "value": "A firefly or phosphorescent moss.",
            "consumed": false,
            "cost": 0,
            "supply": 0
        },
        "preparation": {
            "mode": "prepared",
            "prepared": false
        },
        "scaling": {
            "mode": "none",
            "formula": ""
        }
    },
    "sort": 100001,
    "flags": {},
    "img": "systems/dnd5e/icons/spells/light-sky-1.jpg"
}

const testScroll1  = createScrollFromSpell(lightSpellObject)

console.log(JSON.stringify(testScrollExample) === JSON.stringify(testScroll1))

@aaclayton
Copy link
Contributor Author

Looking good to me. Here are a couple thoughts/requests:

On Managing the Template Scroll IDs

I don't think it's too much of a problem for us to hard-code the scroll IDs since we can work to make sure not to change the compendium IDs, but I would suggest that we at least expose them as something like CONFIG.SpellScrollIDs so that if a user wants to override the default scroll template in a downstream module they can replace the compendium references. Maybe we could use the compendium UUIDs for the scrolls.

CONFIG.DND5E.spellScrollIds = {
  0: "Compendium.dnd5e.items.rQ6sO7HDWzqMhSI3",
  1: "Compendium.dnd5e.items.9GSfMg0VOA2b4uFN",
  ...
}

Then we can do const scrollTemplate = fromUUID(CONFIG.DND5E.spellScrollIds[item.data.level));

Merging together the data

Once you have the template, try the mergeObject helper instead of the Object.assign that you're doing as that's more of the Foundry "standard" way to handle this problem, you would simply need scrollData = mergeObject(scrollTemplate, spellData);

About the descriptions

I like the way you've structured the resulting description, with first a brief overview of what a scroll is, then the spell description, then further details about scroll usage - what I dont like is hard-coding that description text since that will be a blocker for localization efforts. I think we need some way to do this dynamically using the text in the scroll template and the text in the spell description. Maybe split on paragraphs and do like, template p1, spell description, remaining template paragraphs?

@aaclayton
Copy link
Contributor Author

Originally in GitLab by @ayyrickay

Great points, and I definitely wasn't thinking about localization when I came up with the pared-down scroll text. I also updated the spell description to consider this - I'm assuming that enterprising DMs can go in and fiddle with the text once a general version has been generated.

Don't have a great way to test the new methods, so just including the updated code here:

// TODO: Move to CONFIG
const spellScrollIds = {
    0: 'Compendium.dnd5e.items.rQ6sO7HDWzqMhSI3',
    1: 'Compendium.dnd5e.items.9GSfMg0VOA2b4uFN',
    2: 'Compendium.dnd5e.items.XdDp6CKh9qEvPTuS',
    3: 'Compendium.dnd5e.items.hqVKZie7x9w3Kqds',
    4: 'Compendium.dnd5e.items.DM7hzgL836ZyUFB1',
    5: 'Compendium.dnd5e.items.wa1VF8TXHmkrrR35',
    6: 'Compendium.dnd5e.items.tI3rWx4bxefNCexS',
    7: 'Compendium.dnd5e.items.mtyw4NS1s7j2EJaD',
    8: 'Compendium.dnd5e.items.aOrinPg7yuDZEuWr',
    9: 'Compendium.dnd5e.items.O4YbkJkLlnsgUszZ'
}

/**
 * Create a spell scroll object from a spell object
 * @param {Item5e} spell     The spell to be made into a scroll
 * @private
 */
function createScrollFromSpell(spell) {
    // Get spell data
    const {name, data: {description, source, activation, duration, target, range, damage, save, level}} = spell

    // Get scroll data
    const scrollTemplate = fromUUID(CONFIG.DND5E.spellScrollIds[spellSubset.data.level])

    // Create spell description    
    //TODO: Squishy Logic for paragraph delimiter? Also, better names?
    const genericScrollDescription = scrollTemplate.data.description.value
    const paragraphDelimiter = '</p>'
    const scrollIntroEnd = genericScrollDescription.indexOf(paragraphDelimiter)

    const spellScrollDescription = `${genericScrollDescription.slice(0, scrollIntroEnd + paragraphDelimiter.length)}<h2>${name}</h2>${description}${scrollIntroEnd + paragraphDelimiter.length}`

    const spellScrollData = {
        name: `${scrollTemplate.name}: ${name}`,
        data: {
            spellScrollDescription,
            source,
            activation,
            duration,
            target,
            range,
            damage,
            save,
            level
        }
    }

    return mergeObject(scrollTemplate, spellScrollData)
}

I put some TODOs in there, with the main thing being that the description generator could be a LITTLE clunky. I just wanted to make things like a random 4 (since indexOf would find the beginning of the first paragraph tag, so you need to compensate for that) less arbitrary, hence adding in a constant.

I also have some general questions about unfamiliar methods.

What is fromUUID? It sounds like another helper function - if so, does it come from DND5E or from Core? And is it blocking, or should I also be using async/await here as well?

Similarly, does the mergeObject helper come from Foundry? How does it work, e.g., which object takes priority? Can there be more than two objects in the function?

@aaclayton
Copy link
Contributor Author

Hey Ricky - this looks good, but I think the thing that concerns me the most in what you said is

Don't have a great way to test the new methods, so just including the updated code here:

That's definitely something we should work on so you can have a more effective test sandbox.

My advice would be to create a world script (a .js file in your testing world) which you include in that world.json file as "scripts": ["./test-dnd5e.js"]. That script will be automatically loaded in your world and you can use it to rapidly test changes.

You can "inject" config directly into CONFIG.DND5E.spellScrollIds = {} and then you can test your code by passing in different spell Items and see how it comes out.

fromUuid is a global helper function in the core FVTT software https://foundryvtt.com/api/global.html#fromUuid

@aaclayton
Copy link
Contributor Author

Originally in GitLab by @ayyrickay

I'm giving this a whirl and I think I'm running into some async issues:

  1. I had to put a setInterval function just to keep pinging whether DND5E had been loaded into the CONFIG yet, but...
  2. Even once it's loaded and I run fromUuid, I get:
foundry.js:3199 Uncaught (in promise) TypeError: Cannot read property 'find' of undefined
   at fromUuid (foundry.js:3199)
   at createScrollFromSpell (create-scroll-from-spell.js:29)
   at create-scroll-from-spell.js:193

I ran the debugger, and it's this line of code:
const pack = game.packs.find(p => p.collection === `${scope}.${packName}`);

It's pulling in Compendium.dnd5e.items.rQ6sO7HDWzqMhSI3 properly, but items isn't being found as a pack somehow. I tried changing the case, and still no luck. It could be an async issue, but that seems like it should be the responsibility of from Uuid, not createScrollFromSpell

@aaclayton
Copy link
Contributor Author

Originally in GitLab by @ayyrickay

Went ahead and increased some SetInterval details and confirmed the async problem. Not a problem for testing. Here's an updated script:

CONFIG.DND5E.spellScrollIds = {
        0: 'Compendium.dnd5e.items.rQ6sO7HDWzqMhSI3',
        1: 'Compendium.dnd5e.items.9GSfMg0VOA2b4uFN',
        2: 'Compendium.dnd5e.items.XdDp6CKh9qEvPTuS',
        3: 'Compendium.dnd5e.items.hqVKZie7x9w3Kqds',
        4: 'Compendium.dnd5e.items.DM7hzgL836ZyUFB1',
        5: 'Compendium.dnd5e.items.wa1VF8TXHmkrrR35',
        6: 'Compendium.dnd5e.items.tI3rWx4bxefNCexS',
        7: 'Compendium.dnd5e.items.mtyw4NS1s7j2EJaD',
        8: 'Compendium.dnd5e.items.aOrinPg7yuDZEuWr',
        9: 'Compendium.dnd5e.items.O4YbkJkLlnsgUszZ'
    }

    /**
     * Create a spell scroll object from a spell object
     * @param {Item5e} spell     The spell to be made into a scroll
     * @private
     */
    async function createScrollFromSpell(spell) {
        // Get spell data
        const {name, data: {description, source, activation, duration, target, range, damage, save, level}} = spell

        // Get scroll data
        const baseScrollInfo = await fromUuid(CONFIG.DND5E.spellScrollIds[level])
        const scrollTemplate = baseScrollInfo.data

        // Create spell description    
        //TODO: Squishy Logic for paragraph delimiter? Also, better names?
        const genericScrollDescription = scrollTemplate.data.description.value
        const paragraphDelimiter = '</p>'
        const scrollIntroEnd = genericScrollDescription.indexOf(paragraphDelimiter)

        const spellScrollDescription = `${genericScrollDescription.slice(0, scrollIntroEnd + paragraphDelimiter.length)}<h2>${name}</h2>${description.value}${genericScrollDescription.slice(scrollIntroEnd + paragraphDelimiter.length)}`

        const spellScrollData = {
            name: `${scrollTemplate.name}: ${name}`,
            data: {
                description: {value: spellScrollDescription},
                source,
                activation,
                duration,
                target,
                range,
                damage,
                save,
                level
            }
        }

        return mergeObject(scrollTemplate, spellScrollData)
    }

There is still an issue with the save. It feels like this should be able to be automated, but the save and attack bonus information seems to only exist in the spell scroll description. I can change it to Flat easily, but if the content gets changed to a different language, then there's not a reliable way to use the description to update the save data, plus I think it relies on Actor data as well.

Seems like this might be a progress over perfection thing, since characters can calculate this on the fly, and DMs aren't going to allow a Wish spell to just be cast randomly, but wanted to let you know.

@aaclayton
Copy link
Contributor Author

Merged, cleaned up, and integrated. Thanks for all the work you did!

image

@aaclayton aaclayton self-assigned this Jun 13, 2022
Fyorl pushed a commit that referenced this issue Jan 31, 2024
Add quantity formula to group members, button to roll all quantities
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant