diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 9c36b06c72..1104be664a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -21,6 +21,8 @@ jobs: - run: npm run build - run: npm run build:seo - run: npm run build:deploy:prod + - run: npm run build:deploy:prod:gtag + - run: npm run build:deploy:prod:cdn - name: Add files run: git add -A diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 28cc9506ee..87343a41f1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -24,6 +24,6 @@ jobs: - name: Upload Release uses: ncipollo/release-action@v1 with: - generateReleaseNotes: true + body: "See the [Changelogs](https://pf2etools.com/changelog.html) page." artifacts: "pf2ools-${{ env.RELEASE_VERSION }}.zip" token: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/Pf2eTools.html b/Pf2eTools.html index a552f3d3e1..156abdd7d3 100644 --- a/Pf2eTools.html +++ b/Pf2eTools.html @@ -10,6 +10,7 @@
Join our server for announcements, updates, and feature voting.
Can I contribute? A copy of the source is available on GitHub.
+ href="https://github.com/Pf2eToolsOrg/Pf2eTools" rel="noopener noreferrer">GitHub.Browser and device support? The latest desktop versions of Chrome and Firefox.
Where's my stuff? There's no account system. Everything is stored in cookies. If you wipe those, or close your incognito window, it's gone forever. You can download your data for safekeeping via the Settings diff --git a/inittrackerplayerview.html b/inittrackerplayerview.html index 7e2514d4d8..73ede2ab06 100644 --- a/inittrackerplayerview.html +++ b/inittrackerplayerview.html @@ -10,6 +10,7 @@
` entry.entries.forEach(e => this._recursiveRender(e, textStack, meta, options)); + textStack[0] += `
` }; this._renderText = function (entry, textStack, meta, options) { @@ -861,12 +863,11 @@ function Renderer () { this._renderAbility_compact = function (entry, textStack, meta, options) { const renderer = Renderer.get(); - textStack[0] += `${entry.name != null ? entry.name : "Activate"} ` - if (entry.activity != null) textStack[0] += `${renderer.render(Parser.timeToFullEntry(entry.activity))}`; + textStack[0] += `
${entry.name != null ? entry.name : "Activate"}` + if (entry.activity != null) textStack[0] += ` ${renderer.render(Parser.timeToFullEntry(entry.activity))}`; if (entry.components != null) textStack[0] += ` ${renderer.render(entry.components.join(", "))}`; if (entry.traits && entry.traits.length) textStack[0] += ` (${entry.traits.map(t => renderer.render(`{@trait ${t.toLowerCase()}}`)).join(", ")})`; - // TODO: Egh! - if ((entry.components != null && (entry.traits == null || entry.traits.length === 0)) || (entry.activity != null && !Parser.TIME_ACTIONS.includes(entry.activity.unit))) textStack[0] += "; "; + entry.components != null || entry.traits != null ? textStack[0] += "; " : textStack[0] += " "; if (entry.frequency != null) textStack[0] += `Frequency ${renderer.render_addTerm(Parser.freqToFullEntry(entry.frequency))} `; if (entry.note != null) textStack[0] += `${renderer.render(entry.note)}; `; if (entry.requirements != null) textStack[0] += `Requirements ${renderer.render_addTerm(entry.requirements)} `; @@ -1156,7 +1157,7 @@ function Renderer () { this._renderPf2BrownBox = function (entry, textStack, meta, options) { const dataString = this._getDataString(entry); - textStack[0] += `
${opts.prefix || ""}${it._displayName || it.name}${opts.suffix || ""}${activity}
${opts.$btnScaleLvl ? opts.$btnScaleLvl : ""}${opts.$btnResetScaleLvl ? opts.$btnResetScaleLvl : ""} - ${type}${level} + ${type} ${level} ${opts.isEmbedded ? ` ${Renderer.get()._renderData_getEmbeddedToggle()}` : ""}
Access ${renderer.render(item.access)}
`); if (item.price) renderStack.push(`Price ${Parser.priceToFull(item.price)}
`); + // This ammunition is for ammunition items and should not be confused with the ammunition data of ranged weapons + if (item.ammunition) renderStack.push(`Ammunition ${renderer.render(Array.isArray(item.ammunition) ? item.ammunition.map(t => `{@item ${t}}`).join(", ") : `{@item ${item.ammunition}}`)}
`); if (item.contract != null) { renderStack.push(``); - if (item.contract.devil != null) renderStack.push(`Devil ${renderer.render(item.contract.devil)}`) - if (item.contract.devil != null && item.contract.decipher != null) renderStack.push("; ") - if (item.contract.decipher != null) renderStack.push(`${renderer.render(`{@action Decipher Writing}`)} ${renderer.render(item.contract.decipher.map(d => `{@skill ${d}}`).join(", "))}`) - renderStack.push(`
`) + if (item.contract.devil != null) renderStack.push(`Devil ${renderer.render(item.contract.devil)}`); + if (item.contract.devil != null && item.contract.decipher != null) renderStack.push("; "); + if (item.contract.decipher != null) renderStack.push(`${renderer.render(`{@action Decipher Writing}`)} ${renderer.render(item.contract.decipher.map(d => `{@skill ${d}}`).join(", "))}`); + renderStack.push(``); } if (item.usage != null || item.bulk != null) { renderStack.push(``); - if (item.usage != null) renderStack.push(`Usage ${renderer.render(item.usage)}`) - if (item.usage != null && item.bulk != null) renderStack.push("; ") - if (item.bulk != null) renderStack.push(`Bulk ${item.bulk}`) + if (item.usage != null) renderStack.push(`Usage ${renderer.render(item.usage)}`); + if (item.usage != null && item.bulk != null) renderStack.push("; "); + if (item.bulk != null) renderStack.push(`Bulk ${item.bulk}`); renderStack.push(`
`); } - if (item.ac != null || item.dexCap != null || item.shieldStats != null) { - let tempStack = []; - // FIXME: Rework this to be more in line with creature AC - if (item.ac != null) tempStack.push(`AC Bonus ${Parser.numToBonus(item.ac)}${item.ac2 ? `/${Parser.numToBonus(item.ac2)}` : ""}`); - if (item.dexCap != null) tempStack.push(`Dex Cap ${Parser.numToBonus(item.dexCap)}`); - if (item.shieldStats) { - if (item.shieldStats.hardness != null) tempStack.push(`Hardness ${item.shieldStats.hardness}`); - if (item.shieldStats.hp != null) tempStack.push(`HP ${item.shieldStats.hp}`); - if (item.shieldStats.bt != null) tempStack.push(`BT ${item.shieldStats.bt}`); - } - renderStack.push(`${tempStack.join("; ")}
`); - } - if (item.str != null || item.checkPen != null || item.speedPen != null) { - let tempStack = [] - if (item.str != null) tempStack.push(`Strength ${item.str}`) - if (item.checkPen != null) tempStack.push(`Check Penalty ${item.checkPen ? `–${item.checkPen}` : "\u2014"}`) - if (item.speedPen != null) tempStack.push(`Speed Penalty ${item.speedPen ? `–${item.speedPen} ft.` : "\u2014"}`) - renderStack.push(`${tempStack.join("; ")}
`) - } if (item.activate) { renderStack.push(`Activate ${renderer.render(Parser.timeToFullEntry(item.activate.activity))} `); if (item.activate.components != null) { @@ -4722,35 +4709,14 @@ Renderer.item = { if (item.onset) { renderStack.push(`
Onset ${item.onset}
`); } - // Weapon Line/s - if (item.ammunition || item.damage || item.hands || item.reload || item.range) { - renderStack.push(``); - if (item.damage) { - renderStack.push(`Damage ${renderer.render(`{@damage ${item.damage}} ${Parser.dmgTypeToFull(item.damageType)}`)}`); - if (item.hands) renderStack.push("; ") - } - if (item.hands) renderStack.push(`Hands ${item.hands}`); - // Due to how many things ranged weapons introduce to the table, it's best to put it in another line - if (item.ammunition || item.range || item.reload) { - renderStack.push(`
`); - if (item.ammunition) { - renderStack.push(`Ammunition ${renderer.render(Array.isArray(item.ammunition) ? item.ammunition.map(t => `{@item ${t}}`).join(", ") : `{@item ${item.ammunition}}`)}`); - - if (item.range != null || item.reload != null) renderStack.push("; ") - } - if (item.range != null) { - renderStack.push(`Range ${renderer.render(`${item.range} ft.`)}`); - if (item.reload != null) renderStack.push("; ") - } - if (item.reload != null) { - renderStack.push(`Reload ${renderer.render(`${item.reload}`)}`); - } - } - renderStack.push(`
`) - } + + renderStack.push(Renderer.item.getshieldData(item)); + renderStack.push(Renderer.item.getArmorStats(item)); + renderStack.push(Renderer.item.getWeaponStats(item)); // General Item Line - if (item.category || item.group) { + const group = item.weaponData && !item.comboWeaponData ? item.weaponData.group : item.armorData ? item.armorData.group : item.group; + if (item.category || group) { renderStack.push(``); if (item.category) { renderStack.push(`Category `); @@ -4758,9 +4724,9 @@ Renderer.item = { if (item.category === "Weapon") renderStack.push(`${item.range ? "Ranged" : "Melee"} `); renderStack.push(`${Array.isArray(item.category) ? item.category.join(", ") : item.category}${item.category === "Worn" ? ` ${item.type}` : ""}`); } - if (item.category != null && item.group != null) renderStack.push("; ") - if (item.group != null) renderStack.push(`Group ${renderer.render(`{@group ${item.group}}`)}`); - renderStack.push(`
`) + if (item.category != null && group != null) renderStack.push("; "); + if (group != null) renderStack.push(`Group ${renderer.render(`{@group ${group}}`)}`); + renderStack.push(``); } if (renderStack.length !== 0) renderStack.push(`${Renderer.utils.getDividerDiv()}`) @@ -4811,6 +4777,67 @@ Renderer.item = { return renderStack.join(""); }, + getshieldData (item) { + if (item.shieldData && Object.keys(item.shieldData).length) { + const shieldData = item.shieldData; + // FIXME: Rework this to be more in line with creature AC + return `+ AC Bonus ${Parser.numToBonus(shieldData.ac)}${shieldData.ac2 ? `/${Parser.numToBonus(shieldData.ac2)}` : ""}; + ${shieldData.dexCap ? `Dex Cap ${Parser.numToBonus(shieldData.dexCap)};` : ""} + Hardness ${shieldData.hardness}; + HP ${shieldData.hp}; + BT ${shieldData.bt} +
+ ${shieldData.speedPen != null ? `Speed Penalty ${shieldData.speedPen ? `–${shieldData.speedPen} ft.` : "\u2014"}
` : ""}`; + } else return ""; + }, + + getArmorStats (item) { + if (item.armorData && Object.keys(item.armorData).length) { + const armorData = item.armorData; + + return `+ AC Bonus ${Parser.numToBonus(armorData.ac)}; + Dex Cap ${Parser.numToBonus(armorData.dexCap)} +
+ Strength ${armorData.str}; + Check Penalty ${armorData.checkPen ? `–${armorData.checkPen}` : "\u2014"}; + Speed Penalty ${armorData.speedPen ? `–${armorData.speedPen} ft.` : "\u2014"} +
`; + } else return ""; + }, + + getWeaponStats (item) { + const weaponData = item.weaponData; + const comboWeaponData = item.comboWeaponData; + if (weaponData && Object.keys(weaponData).length && comboWeaponData && Object.keys(comboWeaponData).length) { + const opts = {doRenderGroup: true}; + return `${weaponData.type}
${Renderer.item._getRenderedWeaponStats(weaponData, opts)}${comboWeaponData.type}
${Renderer.item._getRenderedWeaponStats(comboWeaponData, opts)}Traits ${data.traits.map(t => renderer.render(`{@trait ${t.toLowerCase()}}`)).join(", ")}
` : ""} +Damage ${renderer.render(`{@damage ${data.damage}} ${Parser.dmgTypeToFull(data.damageType)}`)}${data.hands ? `; Hands ${data.hands}` : ""}
+ ${rangedLine ? `${rangedLine}
` : ""} + ${opts.doRenderGroup ? `Group ${renderer.render(`{@group ${data.group}}`)}
` : ""} + `; + }, + getVariantsHtml (item) { if (!item.generic || !item.variants || !item.variants.length) return ""; const renderStack = []; @@ -4827,7 +4854,7 @@ Renderer.item = { renderer.recursiveRender(v.entries, renderStack, {prefix: "", suffix: "
"}); } if (v.craftReq != null) renderStack.push(`; Craft Requirements ${renderer.render(v.craftReq)}`); - if (v.shieldStats != null) renderStack.push(`; The shield has Hardness ${v.shieldStats.hardness}, HP ${v.shieldStats.hp}, and BT ${v.shieldStats.bt}.`); + if (v.shieldData != null) renderStack.push(`; The shield has Hardness ${v.shieldData.hardness}, HP ${v.shieldData.hp}, and BT ${v.shieldData.bt}.`); renderStack.push(`Special ${Renderer.get().render(item.special)}
` + } else return "" + }, + getCompactRenderedString (item, opts) { opts = opts || {}; const renderStack = [""] Renderer.get().recursiveRender(item.entries, renderStack, {pf2StatFix: true}) - return `${Renderer.utils.getExcludedDiv(item, "item", UrlUtil.PG_ITEMS)} ${Renderer.utils.getNameDiv(item, {page: UrlUtil.PG_ITEMS, ...opts})} ${Renderer.utils.getDividerDiv()} @@ -4852,6 +4884,7 @@ Renderer.item = { ${renderStack.join("")} ${Renderer.item.getVariantsHtml(item)} ${Renderer.item.getCraftRequirements(item)} + ${Renderer.item.getSpecial(item)} ${Renderer.utils.getPageP(item)}`; }, @@ -5009,9 +5042,7 @@ Renderer.ritual = { ${ritual.requirements ? `Requirements ${renderer.render(ritual.requirements)}
` : ""} ${Renderer.utils.getDividerDiv()} ${renderStack.join("")} - ${ritual.heightened && ritual.heightened.heightened - ? `${Renderer.utils.getDividerDiv()}${Renderer.spell.getHeightenedEntry(ritual)}` - : ""} + ${ritual.heightened ? `${Renderer.utils.getDividerDiv()}${Renderer.spell.getHeightenedEntry(ritual)}` : ""} ${opts.noPage ? "" : Renderer.utils.getPageP(ritual)}`; }, }; @@ -5090,7 +5121,7 @@ Renderer.spell = { ${Renderer.utils.getTraitsDiv(sp.traits)} ${Renderer.spell.getSubHead(sp)} ${entryStack.join("")} - ${sp.heightened && sp.heightened.heightened ? `${Renderer.utils.getDividerDiv()}${Renderer.spell.getHeightenedEntry(sp)}` : ""} + ${sp.heightened ? `${Renderer.utils.getDividerDiv()}${Renderer.spell.getHeightenedEntry(sp)}` : ""} ${opts.noPage ? "" : Renderer.utils.getPageP(sp)}`; }, @@ -5125,7 +5156,7 @@ Renderer.spell = { }, getHeightenedEntry (sp) { - if (!sp.heightened || !sp.heightened.heightened) return ""; + if (!sp.heightened) return ""; const renderer = Renderer.get(); const renderStack = [""]; const renderArray = (a) => { @@ -5154,15 +5185,6 @@ Renderer.spell = { } }); } - if (sp.heightened.no_x != null) { - if (typeof sp.heightened.no_x.entry === "string") { - renderStack.push(`Heightened ${renderer.render(sp.heightened.no_x.entry)}
`); - } else if (Array.isArray(sp.heightened.no_x.entry)) { - renderStack.push(`Heightened (+${sp.heightened.plus_x.level}) `) - renderArray(sp.heightened.no_x.entry); - renderStack.push(`
`); - } - } return renderStack.join("") }, diff --git a/js/scalecreature.js b/js/scalecreature.js index a003c7051f..be1f1fd166 100644 --- a/js/scalecreature.js +++ b/js/scalecreature.js @@ -149,7 +149,7 @@ class ScaleCreature { "25": [46, 42, 38, 36, 32], }; this._LvlHP = { - "-1": [9, 8, 7, 6, 5], + "-1": [9, 9, 8, 7, 6, 5], "0": [20, 17, 16, 14, 13, 11], "1": [26, 24, 21, 19, 16, 14], "2": [40, 36, 32, 28, 25, 21], @@ -622,43 +622,27 @@ class ScaleCreature { return creature; } - _getIntervalAndIdx (lvl, map, value) { - const ranges = map[lvl]; - let i = 0; - let I = [0, value] - let idx = [-1, -1] - while (i < ranges.length) { - if (ranges[i] >= value) { - I[1] = ranges[i] - idx[1] = i; - } else { - I[0] = ranges[i]; - idx[0] = i; - break; - } - i++; - } - return {I, idx} - } - - _intervalTransform (x, I0, I1) { - const [a0, b0] = I0; - const [a1, b1] = I1; - if (a0 === b0) return a0; - return Math.round((x - a0) * ((b1 - a1) / (b0 - a0)) + a1); - } - - // FIXME: This garbage code might not be the real culprit this time (BUG-78), but it should have prevented the bug nonetheless - // FIXME: This code is unreadable and might create undesired results _scaleValue (lvlIn, toLvl, value, map) { - const {I: I0, idx} = this._getIntervalAndIdx(lvlIn, map, value); - let I1; - // x < value for all x in map[toLvl] - if (idx[1] === -1) I1 = [map[toLvl][idx[0]], map[toLvl][idx[0]] + I0[1] - I0[0]]; - // x >= value for all x in map[toLvl] - else if (idx[0] === -1) I1 = [Math.max(1, map[toLvl][idx[1]] - I0[1] + I0[0]), map[toLvl][idx[1]]]; - else I1 = [map[toLvl][idx[0]], map[toLvl][idx[1]]]; - return this._intervalTransform(value, I0, I1) + const rangesIn = map[lvlIn]; + const toRanges = map[toLvl]; + const lowerIdx = rangesIn.findIndex(it => it < value); + const upperIdx = rangesIn.length - 1 - rangesIn.reverse().findIndex(it => it >= value); + + const a = rangesIn[lowerIdx] || 0; + const b = rangesIn[upperIdx] || value; + let c, d; + // There was no suggested value less than the value we are scaling. + // TODO: Why shouldn't this be less than 1? + if (lowerIdx === -1) c = Math.max(1, toRanges[upperIdx] - b + a); + else c = toRanges[lowerIdx]; + + // There was no suggested value greater than or equal to the value we are scaling. + if (upperIdx === rangesIn.length) d = c + b - a; + else d = toRanges[upperIdx]; + + // Handle singletons, then finally scale the interval [a,b] to [c,d] linearly, and return the scaled value. + if (a === b) return a; + return Math.round((value - a) * ((d - c) / (b - a)) + c); } _getDiceEV (diceExp) { @@ -671,6 +655,7 @@ class ScaleCreature { _scaleDice (initFormula, expectation, opts) { opts = opts || {}; + if (this._getDiceEV(initFormula) === expectation) return initFormula; // Usually a damage expression works best when roughly half the damage is from dice and half is from the flat modifier. const targetDice = opts.noMod ? expectation : expectation / 2; const dice = Number(initFormula.match(/d(\d+)/)[1]); @@ -884,7 +869,7 @@ class ScaleCreature { } } else if (typeof e === "string") { // Do not scale flat check DCs - e = e.replaceAll(/ DC (\d+)(?!\d* flat)/g, (...m) => { + e = e.replaceAll(/ (?:{@dc|DC) (\d+)(?:\}|)(?!\d* flat)/g, (...m) => { return ` DC ${this._scaleValue(lvlIn, toLvl, Number(m[1]), this._LvlSpellDC) + opts.flatAddProf}`; }); // Do not scale status, circumstance, item bonus... @@ -933,7 +918,8 @@ class ScaleCreature { } _toggleProfNoLvl_updateButtonClass () { - $(`.btn-profnolvl`).toggleClass("active", this._isProfNoLvl); + if (this._isProfNoLvl) document.querySelectorAll(".btn-profnolvl").forEach(node => node.className = `${node.className} active`); + else document.querySelectorAll(".btn-profnolvl").forEach(node => node.className = node.className.split(" ").filter(it => it !== "active").join(" ")); } isProfNoLvl () { diff --git a/js/utils-changelog.js b/js/utils-changelog.js index d9cf1f7ef3..9ee903a9c3 100644 --- a/js/utils-changelog.js +++ b/js/utils-changelog.js @@ -64,7 +64,7 @@ class UtilsChangelog { // FIXME: link to download $wrp.prepend(`