Last active
November 13, 2022 13:52
-
-
Save lslavkov/6f4733d94e2c9e6c0b037d4e558e2dd0 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| function escapeHtml(html) { | |
| const text = document.createTextNode(html); | |
| const p = document.createElement('p'); | |
| p.appendChild(text); | |
| return p.innerHTML; | |
| } | |
| function capitalize(s) { | |
| return s.charAt(0).toUpperCase() + s.slice(1); | |
| } | |
| function hasFeat(actor, featName) { | |
| return actor.items.some((item) => item.type === 'feat' && item.name === featName); | |
| } | |
| function rankToProficiency(rank) { | |
| if (rank === 0) { | |
| return 'untrained'; | |
| } else if (rank === 1) { | |
| return 'trained'; | |
| } else if (rank === 2) { | |
| return 'expert'; | |
| } else if (rank === 3) { | |
| return 'master'; | |
| } else { | |
| return 'legendary'; | |
| } | |
| } | |
| function degreeOfSuccessLabel(degreeOfSuccessLabel) { | |
| if (degreeOfSuccessLabel === 0) { | |
| return 'Critical Failure'; | |
| } else if (degreeOfSuccessLabel === 1) { | |
| return 'Failure'; | |
| } else if (degreeOfSuccessLabel === 2) { | |
| return 'Success'; | |
| } else { | |
| return 'Critical Success'; | |
| } | |
| } | |
| function coinsToString(coins, degreeOfSuccess) { | |
| if (degreeOfSuccess === 'Critical Failure') { | |
| return 'none'; | |
| } else { | |
| return Object.entries(coins) | |
| .map(([key, value]) => `${value} ${game.i18n.localize(CONFIG.PF2E.currencies[key])}`) | |
| .join(', '); | |
| } | |
| } | |
| function chatTemplate(skillName, earnIncomeResult) { | |
| const degreeOfSuccess = degreeOfSuccessLabel(earnIncomeResult.degreeOfSuccess); | |
| const payPerDay = escapeHtml(coinsToString(earnIncomeResult.rewards.perDay, degreeOfSuccess)); | |
| const combinedPay = escapeHtml(coinsToString(earnIncomeResult.rewards.combined, degreeOfSuccess)); | |
| const level = earnIncomeResult.level; | |
| const daysSpentWorking = earnIncomeResult.daysSpentWorking; | |
| const forDays = | |
| daysSpentWorking > 1 ? `<p><strong>Salary for ${daysSpentWorking} days</strong>: ${combinedPay}</p>` : ''; | |
| const successColor = earnIncomeResult.degreeOfSuccess > 1 ? 'darkgreen' : 'darkred'; | |
| const dc = earnIncomeResult.dc; | |
| const roll = earnIncomeResult.roll; | |
| return ` | |
| <div class="pf2e chat-card"> | |
| <header class="card-header flexrow"> | |
| <img src="systems/pf2e/icons/equipment/treasure/currency/gold-pieces.webp" title="Income" width="36" height="36"> | |
| <h3>Earn Income Level ${level}</h3> | |
| </header> | |
| <div class="card-content"> | |
| <p><strong>Result</strong>: <span style="color: ${successColor}">${degreeOfSuccess} (DC: ${dc}, Roll: ${roll})</span></p> | |
| <p><strong>Skill</strong>: ${escapeHtml(skillName)}</p> | |
| <p><strong>Salary per day:</strong> ${payPerDay}</p> | |
| ${forDays} | |
| </div> | |
| </div> | |
| `; | |
| } | |
| function postToChat(skillName, earnIncomeResult) { | |
| const content = chatTemplate(skillName, earnIncomeResult); | |
| const chatData = { | |
| user: game.user.id, | |
| content, | |
| speaker: ChatMessage.getSpeaker(), | |
| }; | |
| ChatMessage.create(chatData, {}); | |
| } | |
| function isProficiencyWithoutLevel() { | |
| return game.settings.get('pf2e', 'proficiencyVariant') === 'ProficiencyWithoutLevel'; | |
| } | |
| function calculateIncome(actor, skill, roll, level, days) { | |
| const dcOptions = { | |
| proficiencyWithoutLevel: isProficiencyWithoutLevel(), | |
| }; | |
| const earnIncomeOptions = { | |
| useLoreAsExperiencedProfessional: hasFeat(actor, 'Experienced Professional') && skill.isLore, | |
| }; | |
| const income = game.pf2e.actions.earnIncome(level, days, roll, skill.proficiency, earnIncomeOptions, dcOptions); | |
| postToChat(skill.name, income); | |
| } | |
| function runEarnIncome(actor, skill, assurance, level, days) { | |
| if (assurance) { | |
| const actorLevel = actor.system.details?.level?.value ?? 1; | |
| const proficiencyLevel = isProficiencyWithoutLevel() ? 0 : actorLevel; | |
| const proficiencyBonus = proficiencyLevel + skill.rank * 2; | |
| calculateIncome(actor, skill, { dieValue: 10, modifier: proficiencyBonus }, level, days); | |
| } else { | |
| const options = actor.getRollOptions(['all', 'skill-check', skill.name]); | |
| options.push('earn-income'); | |
| game.pf2e.Check.roll( | |
| new game.pf2e.CheckModifier( | |
| '<span style="font-family: Pathfinder2eActions">A</span> Earn Income', | |
| actor.system.skills[skill.acronym], | |
| [], | |
| ), | |
| { actor, type: 'skill-check', options }, | |
| event, | |
| (roll) => { | |
| const dieValue = roll.dice[0].results[0].result; | |
| const modifier = roll._total - dieValue; | |
| calculateIncome(actor, skill, { dieValue, modifier }, level, days); | |
| }, | |
| ); | |
| } | |
| } | |
| function getSkills(actor) { | |
| // create a list of skills available for Earn Income | |
| var coreSkillsToCheck = new Map(); | |
| coreSkillsToCheck.set("cra", true); | |
| coreSkillsToCheck.set("prf", true); | |
| if (hasFeat(actor, 'Bargain Hunter')) { | |
| coreSkillsToCheck.set("dip", true); | |
| } | |
| if (hasFeat(actor, 'Fabricated Connections')) { | |
| coreSkillsToCheck.set("dec", true); | |
| } | |
| if (hasFeat(actor, 'City Scavenger')) { | |
| coreSkillsToCheck.set("soc", true); | |
| coreSkillsToCheck.set("sur", true); | |
| } | |
| // first any trained lore | |
| var relevantSkills = | |
| Object.entries(actor.system.skills) | |
| .map(([acronym, value]) => { | |
| return { | |
| acronym, | |
| name: capitalize(value.name), | |
| isLore: value.lore === true, | |
| proficiency: rankToProficiency(value.rank), | |
| rank: value.rank, | |
| }; | |
| }) | |
| // earn income is a trained action | |
| .filter((skill) => ((skill.isLore === true || coreSkillsToCheck.get(skill.acronym) === true) | |
| && skill.proficiency !== "untrained")); | |
| return relevantSkills.sort(function(a, b) { return a.name.localeCompare(b.name) }); | |
| } | |
| function askSkillPopupTemplate(actor, skills) { | |
| const level = parseInt(localStorage.getItem('earnIncomeLevel') ?? 0, 10); | |
| const days = parseInt(localStorage.getItem('earnIncomeDays') ?? 8, 10); | |
| const skillAcronym = localStorage.getItem('earnIncomeSkillAcronym'); | |
| const assurance = localStorage.getItem('earnIncomeAssurance') === 'true'; | |
| var maxLevel = actor.level - 2; | |
| if (hasFeat(actor, 'Experienced Smuggler')) { | |
| maxLevel = actor.level - 1; | |
| } | |
| if (hasFeat(actor, 'Storied Talent')) { | |
| maxLevel = actor.level; | |
| } | |
| if (maxLevel < 0) { | |
| maxLevel = 0; | |
| } | |
| return ` | |
| <form> | |
| <div class="form-group"> | |
| <label>Trained Skills/Lores</label> | |
| <select name="skillAcronym"> | |
| ${skills | |
| .map( | |
| (skill) => | |
| `<option value="${skill.acronym}">${escapeHtml(skill.name)}</option>`, | |
| ) | |
| .join('')} | |
| </select> | |
| </div> | |
| <div class="form-group"> | |
| <label>Use Assurance</label> | |
| <input name="assurance" type="checkbox" ${assurance ? 'checked' : ''}> | |
| </div> | |
| <div class="form-group"> | |
| <label>Task Level</label> | |
| <select name="level"> | |
| ${Array(maxLevel + 1) | |
| .fill(0) | |
| .map((_, index) => `<option value="${index}" ${index === maxLevel ? 'selected' : ''}>${index}</option>`) | |
| .join('')} | |
| </select> | |
| </div> | |
| <div class="form-group"> | |
| <label>Days</label> | |
| <input type="number" name="days" value="${days}"> | |
| </div> | |
| </form> | |
| `; | |
| } | |
| function showEarnIncomePopup(actor) { | |
| if (actor === null || actor === undefined) { | |
| ui.notifications.error(`You must select at least one PC`); | |
| } else { | |
| const skills = getSkills(actor); | |
| new Dialog({ | |
| title: 'Earn Income', | |
| content: askSkillPopupTemplate(actor, skills), | |
| buttons: { | |
| no: { | |
| icon: '<i class="fas fa-times"></i>', | |
| label: 'Cancel', | |
| }, | |
| yes: { | |
| icon: '<i class="fas fa-coins"></i>', | |
| label: 'Earn Income', | |
| callback: ($html) => { | |
| const level = parseInt($html[0].querySelector('[name="level"]').value, 10) ?? 1; | |
| const days = parseInt($html[0].querySelector('[name="days"]').value, 10) ?? 1; | |
| const skillAcronym = $html[0].querySelector('[name="skillAcronym"]').value; | |
| const assurance = $html[0].querySelector('[name="assurance"]').checked; | |
| const skill = skills.find((skill) => skill.acronym === skillAcronym); | |
| localStorage.setItem('earnIncomeLevel', level); | |
| localStorage.setItem('earnIncomeDays', days); | |
| localStorage.setItem('earnIncomeSkillAcronym', skillAcronym); | |
| localStorage.setItem('earnIncomeAssurance', assurance); | |
| runEarnIncome(actor, skill, assurance, level, days); | |
| }, | |
| }, | |
| }, | |
| default: 'yes', | |
| }).render(true); | |
| } | |
| } | |
| showEarnIncomePopup(actor); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment