Skip to content

Instantly share code, notes, and snippets.

@lslavkov
Last active November 13, 2022 13:52
Show Gist options
  • Select an option

  • Save lslavkov/6f4733d94e2c9e6c0b037d4e558e2dd0 to your computer and use it in GitHub Desktop.

Select an option

Save lslavkov/6f4733d94e2c9e6c0b037d4e558e2dd0 to your computer and use it in GitHub Desktop.
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