Merge branch 'development' into master

# Conflicts:
#	Stars_Without_Number_Revised/Source/workers.js
#	Stars_Without_Number_Revised/Stars_Without_Number_Revised.html
This commit is contained in:
Karlinator
2020-09-05 22:23:07 +02:00
22 changed files with 4169 additions and 3722 deletions
@@ -40,6 +40,19 @@ include pug/rolltemplate.pug
|
|
script(type="text/worker")
include:uglify-es workers.js
|
| (function () {
| "use strict";
include:uglify-es js/constants.js
include:uglify-es js/util.js
include:uglify-es js/calculations.js
include:uglify-es js/validations.js
include:uglify-es js/npc.js
include:uglify-es js/menuBuilders.js
include:uglify-es js/autofill.js
include:uglify-es js/translations.js
include:uglify-es js/ship.js
include:uglify-es js/drones.js
include:uglify-es js/migrations.js
include:uglify-es js/eventHandlers.js
| })();
|
@@ -93,7 +93,7 @@ button[type=roll].sheet-invisible-button {
width: 100%;
height: 100%;
top: 0;
&:hover + div {
&:hover ~ div:not(.sheet-no-highlight) {
color: $brightblue;
border-color: $brightblue;
}
@@ -268,6 +268,7 @@ h3 {
.sheet-col40 { width: 40% }
.sheet-colthird { width: calc(100%/3) }
.sheet-col25 { width: 25% }
.sheet-col20 { width: 20% }
.sheet-colsixth { width: calc(100%/6) }
/* containers */
@@ -293,7 +294,7 @@ input[type=text].sheet-inherit {
color: inherit;
font: inherit;
}
input[type=number].sheet-inline-input {
.sheet-inline-input {
position: relative;
z-index: 2;
vertical-align: baseline;
@@ -633,6 +634,17 @@ input[type=number].sheet-inline-input {
.sheet-slash { margin: 0 2px }
}
/* Shells */
.sheet-shells {
.sheet-shells-grid {
grid: auto / 40px 35px 35px 35px 45px 1fr;
}
}
.sheet-transhuman-setting:not([value="transhuman"]) ~ .sheet-column .sheet-transhuman-enabled { display: none }
.sheet-transhuman-setting[value="transhuman"] ~ .sheet-core .sheet-transhuman-disabled { display: none }
/* Weapons */
.sheet-weapons {
.sheet-edit {
@@ -782,6 +794,7 @@ input[type=number].sheet-inline-input {
}
}
.sheet-input-details:not([value="1"]) ~ div .sheet-details { display: none }
.sheet-input-details:not([value="0"]) ~ div .sheet-notdetails { display: none }
/* Supernatural section controls */
@@ -870,6 +883,25 @@ input[type=number].sheet-inline-input {
&[value="adept"] ~ div .sheet-spellcaster { display: none }
}
/* AI */
.sheet-ai-processing {
display: grid;
grid: auto/ auto 45px 45px 45px 45px 80px;
align-items: center;
grid-column-gap: 4px;
h3 { line-height: 1 }
}
.sheet-ai-processing-nodes {
.sheet-nodes-grid {
grid: auto / 65px 65px 1fr;
}
}
.sheet-ai-setting:not([value="ai"]) ~ .sheet-ai-enabled { display: none }
.sheet-ai-setting[value="ai"] ~ .sheet-column .sheet-ai-enabled { display: block }
.sheet-ai-setting[value="ai"] ~ .sheet-core .sheet-ai-disabled { display: none }
/* Foci */
.sheet-foci-loc {
&[value="left"] ~ .sheet-column .sheet-foci-right,
@@ -0,0 +1,202 @@
/* global getAttrs, setAttrs, getSectionIDs, generateRowID, on, removeRepeatingRow, _, getTranslationByKey */
/* Autofill stuff */
const fillClassStats = () => {
getAttrs(["class", "class_ability", "attack_bonus"], v => {
const label = v.class && reverseClasses[v.class.toLowerCase()];
if (label && autofillData.classes.hasOwnProperty(label)) {
const data = Object.assign({}, autofillData.classes[label]);
Object.keys(data).forEach(key => {
if (!(["", "0"].includes(`${v[key]}`))) delete data[key];
});
mySetAttrs(data, v);
}
});
};
const getShipMultiplier = (shipClass) => {
if ((shipClass || "").toLowerCase() === "frigate") return 2;
else if ((shipClass || "").toLowerCase() === "cruiser") return 3;
else if ((shipClass || "").toLowerCase() === "capital") return 4;
else return 1;
};
const getShipPriceMultiplier = (shipClass) => {
if ((shipClass || "").toLowerCase() === "frigate") return 10;
else if ((shipClass || "").toLowerCase() === "cruiser") return 25;
else if ((shipClass || "").toLowerCase() === "capital") return 100;
else return 1;
}
const getAutofillData = (sName, v, data, label) => {
// Transforms the stored data to be suitable for
// inclusion into the sheet.
const output = Object.assign({}, data);
if (sName === "ship-defenses") {
if (label) output.defense_name = translate(label.toUpperCase());
if (output.defense_mass.includes("#")) {
output.defense_mass = `${parseInt(output.defense_mass) * v.ship_multiplier}`;
}
if (output.defense_price.includes("*")) {
output.defense_price = `${parseInt(output.defense_price) * v.ship_price_multiplier}`;
}
}
if (sName === "ship-fittings") {
if (label) {
output.fitting_name = translate(label.toUpperCase());
output.fitting_effect = translate(`${label.toUpperCase()}_DESC`);
}
if (output.fitting_mass.includes("#")) {
output.fitting_mass = `${Math.round(parseFloat(output.fitting_mass) * v.ship_multiplier)}`;
}
if (output.fitting_power.includes("#")) {
output.fitting_power = `${parseInt(output.fitting_power) * v.ship_multiplier}`;
}
if (output.fitting_price.includes("*")) {
output.fitting_price = `${parseInt(output.fitting_price) * v.ship_price_multiplier}`;
}
}
if (sName === "ship-weapons") {
if (label) output.weapon_name = translate(label.toUpperCase());
if (output.weapon_ammo) {
output.weapon_ammo_max = output.weapon_ammo;
output.weapon_use_ammo = "{{ammo=[[@{weapon_ammo} - 1]] / @{weapon_ammo_max}}}";
}
}
if (sName === "weapons") {
if (label) {
output.weapon_name = translate(label.toUpperCase());
output.weapon_description = translate(`${label.toUpperCase()}_DESC`);
}
if (output.weapon_ammo) {
output.weapon_ammo_max = output.weapon_ammo;
output.weapon_use_ammo = "{{ammo=[[0@{weapon_ammo} - (1 @{weapon_burst})]] / @{weapon_ammo|max}}}";
}
if (output.weapon_shock_damage) {
output.weapon_shock = "{{shock=[[@{weapon_shock_damage} + @{weapon_attribute_mod}[Attribute] + @{weapon_skill_to_damage}[Skill]]] ^{SHOCK_DAMAGE_AGAINST_AC_LEQ} @{weapon_shock_ac}!}}";
}
if (output.weapon_burst) {
output.weapon_burst = "@{burst_query}";
}
if (!output.weapon_attribute_mod) {
output.weapon_attribute_mod = "@{dexterity_mod}";
}
}
if (sName === "armor") {
if (label) {
output.armor_name = translate(label.toUpperCase());
output.armor_description = translate(`${label.toUpperCase()}_DESC`) || "";
}
}
if (sName === "cyberware") {
if (label) {
output.cyberware_name = translate(label.toUpperCase());
output.cyberware_description = translate(`${label.toUpperCase()}_DESC`);
}
}
if (sName === "foci") {
if (label) {
output.focus_name = translate(label.toUpperCase());
output.focus_level = "1";
output.focus_description = translate(`${label.toUpperCase()}_DESC`);
}
}
if (sName === "techniques") {
if (label) {
output.technique_name = translate(label.toUpperCase());
output.technique_description = translate(`${label.toUpperCase()}_DESC`);
}
}
if (sName === "gear") {
if (label) {
output.gear_name = translate(label.toUpperCase());
output.gear_description = translate(`${label.toUpperCase()}_DESC`);
}
if (output.gear_encumbrance === "1#") {
output.gear_encumbrance = "1";
output.gear_bundled = "on";
}
}
return output;
};
const getAutofillInfo = (sName, v, inputData, label) => {
const data = getAutofillData(sName, v, inputData, label);
const formatter = new Intl.NumberFormat();
// Generates info text from the stored data
if (sName === "ship-defenses") {
return `${translate(data.class)}+. ${translate("POWER_INIT")}: ${data.defense_power}, ${
translate("MASS_INIT")}: ${data.defense_mass}, ${translate("CREDITS")}: ${formatter.format(data.defense_price)}. ${data.defense_effect}`;
}
if (sName === "ship-fittings") {
return `${translate(data.class)}+. ${translate("POWER_INIT")}: ${data.fitting_power}, ${
translate("MASS_INIT")}: ${data.fitting_mass}, ${translate("CREDITS")}: ${formatter.format(data.fitting_price)} ${data.fitting_effect}`;
}
if (sName === "ship-weapons") {
return `${translate(data.class)}+. ${
translate("POWER_INIT")}: ${data.weapon_power}, ${
translate("MASS_INIT")}: ${data.weapon_mass}, ${
translate("HARDPOINTS_INIT")}: ${data.weapon_hardpoints}, ${
translate("CREDITS")}: ${formatter.format(data.weapon_price)}. ${
translate("DAMAGE_SHORT")} ${data.weapon_damage}. ${
data.weapon_qualities}${
(data.weapon_ammo ? `, ${translate("AMMO")}: ${data.weapon_ammo}`: "")}.`;
}
if (sName === "weapons") {
const getNamedAttrMod = expr => {
if (expr === "@{dexterity_mod}") return translate("DEXTERITY_SHORT");
else if (expr === "@{strength_mod}") return translate("STRENGTH_SHORT");
else if (expr === "@{str_dex_mod}") return translate("STR_DEX");
};
return `${translate("DAMAGE_SHORT")} ${data.weapon_damage}${data.weapon_burst ? ` (${translate("BURST")})` : ""}${
data.weapon_ab ? `, ${translate("ATTACK_BONUS_SHORT")} +${data.weapon_ab}` : ""}${
data.weapon_range ? `, ${translate("RANGE")} ${data.weapon_range}` : ""}${
data.weapon_ammo ? `, ${translate("AMMO")} ${data.weapon_ammo}` : ""}${
data.weapon_shock ? `, ${data.weapon_shock_damage} ${translate("SHOCK_DAMAGE_AGAINST_AC_LEQ")} ${data.weapon_shock_ac}` : ""}, +${getNamedAttrMod(data.weapon_attribute_mod)}${
data.weapon_encumbrance ? `, ${translate("ENCUMBRANCE_SHORT")} ${data.weapon_encumbrance}` : ""}${
data.weapon_price ? `, ${translate("CREDITS")}: ${formatter.format(data.weapon_price)}` : ""}.`
}
if (sName === "armor") {
return `${translate("AC")} ${data.armor_ac}, ${translate(data.armor_type)
}, ${translate("ENCUMBRANCE_SHORT")} ${data.armor_encumbrance}, ${translate("CREDITS")}: ${formatter.format(data.armor_price)}.`;
}
if (sName === "cyberware") {
return `${translate("STRAIN")}: ${data.cyberware_strain}, ${translate("CREDITS")}: ${formatter.format(data.cyberware_price)}.`;
}
if (sName === "techniques") {
if (data.level === "0") return `${translate("CORE_TECHNIQUE")}.`;
else return `${translate("LEVEL")}-${data.level}.`;
}
if (sName === "gear") {
return `${translate("ENCUMBRANCE_SHORT")} ${data.gear_encumbrance}${data.gear_bundled === "on" ? "#" : ""}${
data.gear_price ? `, ${translate("CREDITS")}: ${formatter.format(data.gear_price)}` : ""
}.`
}
return "";
};
const generateAutofillRow = (sName) => {
// Event handler for generating a new row when button is pressed
getAttrs([`generate_${sName}_source`, "ship_class"], v => {
const label = v[`generate_${sName}_source`];
v.ship_multiplier = getShipMultiplier(v.ship_class);
v.ship_price_multiplier = getShipPriceMultiplier(v.ship_class);
if (label && autofillData[sName].hasOwnProperty(label)) {
const data = getAutofillData(sName, v, autofillData[sName][label], label);
delete data.class;
delete data.level;
fillRepeatingSectionFromData(sName, data);
}
});
};
const generateAutofillInfo = (sName) => {
// Event handler for showing info about the selected item
getAttrs([`generate_${sName}_source`, "ship_class"], v => {
const label = v[`generate_${sName}_source`];
v.ship_multiplier = getShipMultiplier(v.ship_class);
v.ship_price_multiplier = getShipPriceMultiplier(v.ship_class);
if (label && autofillData[sName].hasOwnProperty(label)) {
const info = getAutofillInfo(sName, v, autofillData[sName][label], label);
if (info) setAttrs({
[`generate_${sName}_info`]: info
});
} else setAttrs({
[`generate_${sName}_info`]: " "
});
});
};
@@ -0,0 +1,376 @@
/* global getAttrs, setAttrs, getSectionIDs, generateRowID, on, removeRepeatingRow, _, getTranslationByKey */
/* Calculations */
const calculateSaves = () => {
getAttrs([...attributes.map(attr => `${attr}_mod`), "level",
"homebrew_luck_save", "save_physical", "save_mental", "save_evasion", "save_luck"
], v => {
const base = 16 - (parseInt(v.level) || 1);
const setting = {
save_physical: base - (Math.max(parseInt(v.strength_mod), parseInt(v.constitution_mod)) || 0),
save_mental: base - (Math.max(parseInt(v.charisma_mod), parseInt(v.wisdom_mod)) || 0),
save_evasion: base - (Math.max(parseInt(v.intelligence_mod), parseInt(v.dexterity_mod)) || 0)
};
if (v.homebrew_luck_save === "1") setting.save_luck = base;
mySetAttrs(setting, v);
});
};
const calculateEffort = () => {
getSectionIDs("repeating_psychic-skills", idArray => {
getAttrs([...effortAttributes,
"psionics_total_effort",
...idArray.map(id => `repeating_psychic-skills_${id}_skill`)], v => {
const attrBonus = Math.max(parseInt(v.wisdom_mod), parseInt(v.constitution_mod)) || 0,
skillBonus = Math.max(
...skills.psionic.map(x => parseInt(v[`skill_${x}`]) || 0),
...idArray.map(id => parseInt(v[`repeating_psychic-skills_${id}_skill`]) || 0)
);
const psionics_total_effort = Math.max(1 + attrBonus + skillBonus, 1) + parseInt(v.psionics_extra_effort) || 0 -
parseInt(v.psionics_committed_effort_current) || 0 - parseInt(v.psionics_committed_effort_scene) || 0 -
parseInt(v.psionics_committed_effort_day) || 0;
mySetAttrs({
psionics_total_effort
}, v);
});
});
};
const calculateMagicEffort = () => {
getAttrs(["magic_total_effort", "magic_committed_effort_current", "magic_committed_effort_scene", "magic_committed_effort_day", "magic_uncommitted_effort"], function(v) {
const magic_uncommitted_effort = parseInt(v.magic_total_effort) - (parseInt(v.magic_committed_effort_current) +
parseInt(v.magic_committed_effort_scene) + (v.magic_committed_effort_day)) || 0;
mySetAttrs({
magic_uncommitted_effort
}, v);
});
};
const calculateProcessing = () => {
getSectionIDs("repeating_processing-nodes", idArray => {
const sourceAttrs = [
...idArray.map(id => `repeating_processing-nodes_${id}_node_value`),
...idArray.map(id => `repeating_processing-nodes_${id}_node_connected`),
"wisdom_mod", "intelligence_mod", "ai_extra_processing", "ai_committed_processing_current", "ai_committed_processing_scene", "ai_committed_processing_day", "ai_total_processing"
];
getAttrs(sourceAttrs, v => {
const maxProcessing = 1 + Math.max(parseInt(v.wisdom_mod) || 0, (parseInt(v.intelligence_mod)) || 0) + Math.max(
...idArray.filter(id => v[`repeating_processing-nodes_${id}_node_connected`] === "1")
.map(id => parseInt(v[`repeating_processing-nodes_${id}_node_value`]) || 0), 0
);
const ai_total_processing = maxProcessing + parseInt(v.ai_extra_processing) - parseInt(v.ai_committed_processing_current) -
parseInt(v.ai_committed_processing_scene) - parseInt(v.ai_committed_processing_day) || 0;
mySetAttrs({
ai_total_processing
}, v);
});
});
}
const calculateAC = () => {
getSectionIDs("repeating_armor", idArray => {
const sourceAttrs = [
...idArray.map(id => `repeating_armor_${id}_armor_ac`),
...idArray.map(id => `repeating_armor_${id}_armor_active`),
...idArray.map(id => `repeating_armor_${id}_armor_type`),
"npc", "AC", "innate_ac", "dexterity_mod",
];
getAttrs(sourceAttrs, v => {
if (v.npc === "1") return;
const baseAC = Math.max(
parseInt(v.innate_ac) || 0,
...idArray.filter(id => v[`repeating_armor_${id}_armor_active`] === "1")
.filter(id => v[`repeating_armor_${id}_armor_type`] !== "SHIELD")
.map(id => parseInt(v[`repeating_armor_${id}_armor_ac`]) || 0)
);
const shieldAC = Math.max(
0,
...idArray.filter(id => v[`repeating_armor_${id}_armor_active`] === "1")
.filter(id => v[`repeating_armor_${id}_armor_type`] === "SHIELD")
.map(id => parseInt(v[`repeating_armor_${id}_armor_ac`]) || 0)
);
const AC = (shieldAC > 0 ? shieldAC <= baseAC ? (baseAC + 1) : shieldAC : baseAC) +
(parseInt(v.dexterity_mod) || 0);
mySetAttrs({
AC
}, v);
});
});
};
const calculateMaxStrain = () => {
getAttrs(["constitution", "strain_max"], v => {
mySetAttrs({
strain_max: parseInt(v.constitution) || 0
}, v);
});
};
const calculatePermanentStrain = () => {
getAttrs(["cyberware_strain_total", "strain_permanent_extra", "strain_permanent"], v => {
const permStrain = parseInt(v.cyberware_strain_total) + parseInt(v.strain_permanent_extra) || 0;
mySetAttrs({
strain_permanent: permStrain
}, v);
});
};
const calculateCyberwareStrain = () => {
//Loop through the cyberware and add up the strain.
getSectionIDs("repeating_cyberware", idArray => {
const sourceAttrs = [
...idArray.map(id => `repeating_cyberware_${id}_cyberware_strain`),
"cyberware_strain_total"
];
getAttrs(sourceAttrs, v => {
const cyberwareStrain = idArray.reduce((m, id) => {
m += parseInt(v[`repeating_cyberware_${id}_cyberware_strain`]) || 0;
return m
}, 0)
mySetAttrs({
cyberware_strain_total: cyberwareStrain
}, v);
});
});
};
const calculateAttr = (attr) => {
getAttrs([attr, `${attr}_base`, `${attr}_boosts`], v => {
const setting = {
[`${attr}`]: `${(parseInt(v[`${attr}_base`]) || 10) + (parseInt(v[`${attr}_boosts`]) || 0)}`
};
mySetAttrs(setting, v, null, () => {
calculateMod(attr);
});
});
}
const calculateMod = (attr) => {
getAttrs([attr, `${attr}_bonus`, `${attr}_mod`], v => {
const mod = (value => {
if (value >= 18) return 2;
else if (value >= 14) return 1;
else if (value >= 8) return 0;
else if (value >= 4) return -1;
else return -2;
})(parseInt(v[attr]) || 10);
const setting = {
[`${attr}_mod`]: `${mod + (parseInt(v[`${attr}_bonus`]) || 0)}`
};
mySetAttrs(setting, v, null, () => {
calculateSaves();
generateWeaponDisplay();
calculateStrDexMod();
if (attr === "dexterity") calculateAC();
});
});
};
const calculateStrDexMod = () => {
getAttrs(["str_dex_mod", "strength_mod", "dexterity_mod"], v => {
const str_dex_mod = Math.max(parseInt(v.strength_mod) || 10, parseInt(v.dexterity_mod) || 10);
mySetAttrs({
str_dex_mod
}, v);
});
};
const calculateShellAttrs = () => {
const physicalAttrs = ["strength", "dexterity", "constitution"];
getSectionIDs("repeating_shells", idArray => {
const sourceAttrs = [
...idArray.map(id => `repeating_shells_${id}_shell_active`),
...idArray.map(id => `repeating_shells_${id}_shell_strength`),
...idArray.map(id => `repeating_shells_${id}_shell_dexterity`),
...idArray.map(id => `repeating_shells_${id}_shell_constitution`),
...physicalAttrs, "setting_transhuman_enable", "setting_ai_enable",
];
getAttrs(sourceAttrs, v => {
if (v.setting_transhuman_enable === "transhuman" || v.setting_ai_enable === "ai"){
let attributes = {};
physicalAttrs.forEach(attr => attributes[attr] = idArray
.filter(id => v[`repeating_shells_${id}_shell_active`] === "1")
.map(id => v[`repeating_shells_${id}_shell_${attr}`])[0] || "None")
mySetAttrs(attributes, v, null, () => {
physicalAttrs.filter(attr => attributes[attr] !== "None").forEach(attr => calculateMod(attr));
});
} else {
physicalAttrs.forEach(attr => calculateAttr(attr));
}
})
})
}
const calculateNextLevelXP = () => {
const xp = [0, 3, 6, 12, 18, 27, 39, 54, 72, 93];
getAttrs(["level", "setting_xp_scheme"], v => {
if (v.setting_xp_scheme === "xp") {
if (v.level < 10) {
setAttrs({
xp_next: xp[v.level]
});
} else {
setAttrs({
xp_next: 93 + ((v.level - 9) * 24)
});
}
} else if (v.setting_xp_scheme === "money") {
setAttrs({
xp_next: 2500 * (2 * v.level)
});
}
});
};
const calculateGearReadiedStowed = () => {
const doCalc = (gearIDs, weaponIDs, armorIDs) => {
const attrs = [
...gearIDs.map(id => `repeating_gear_${id}_gear_amount`),
...gearIDs.map(id => `repeating_gear_${id}_gear_encumbrance`),
...gearIDs.map(id => `repeating_gear_${id}_gear_status`),
...gearIDs.map(id => `repeating_gear_${id}_gear_bundled`),
...armorIDs.map(id => `repeating_armor_${id}_armor_encumbrance`),
...armorIDs.map(id => `repeating_armor_${id}_armor_encumbrance_bonus`),
...armorIDs.map(id => `repeating_armor_${id}_armor_status`),
...weaponIDs.map(id => `repeating_weapons_${id}_weapon_encumbrance`),
...weaponIDs.map(id => `repeating_weapons_${id}_weapon_status`),
"gear_readied", "gear_stowed", "strength", "gear_readied_max",
"gear_readied_over", "gear_stowed_over", "gear_stowed_max"
];
getAttrs(attrs, v => {
const [gear_readied, gear_stowed] = armorIDs.reduce((m, id) => {
if (v[`repeating_armor_${id}_armor_status`] === "READIED") {
m[0] += parseInt(v[`repeating_armor_${id}_armor_encumbrance`]) || 0;
} else if (v[`repeating_armor_${id}_armor_status`] === "STOWED")
m[1] += parseInt(v[`repeating_armor_${id}_armor_encumbrance`]) || 0;
return m;
}, weaponIDs.reduce((m, id) => {
if (v[`repeating_weapons_${id}_weapon_status`] === "READIED")
m[0] += parseInt(v[`repeating_weapons_${id}_weapon_encumbrance`]) || 0;
else if (v[`repeating_weapons_${id}_weapon_status`] === "STOWED")
m[1] += parseInt(v[`repeating_weapons_${id}_weapon_encumbrance`]) || 0;
return m;
}, gearIDs.reduce((m, id) => {
const amount = parseInt(v[`repeating_gear_${id}_gear_amount`]) || 0;
let packingFactor = 1;
if (v[`repeating_gear_${id}_gear_bundled`] === "on") {
packingFactor = 3;
}
if (v[`repeating_gear_${id}_gear_status`] === "READIED")
m[0] += Math.ceil((amount * parseFloat(v[`repeating_gear_${id}_gear_encumbrance`]))/packingFactor) || 0;
else if (v[`repeating_gear_${id}_gear_status`] === "STOWED")
m[1] += Math.ceil((amount * parseFloat(v[`repeating_gear_${id}_gear_encumbrance`]))/packingFactor) || 0;
return m;
}, [0, 0])));
const armor_encumbrance_bonus = Math.max(0,
...armorIDs.filter(id => v[`repeating_armor_${id}_armor_status`] === "READIED")
.map(id => parseInt(v[`repeating_armor_${id}_armor_encumbrance_bonus`]) || 0)
);
const gear_stowed_max = parseInt(v.strength) + armor_encumbrance_bonus || 0;
const gear_readied_max = Math.floor(gear_stowed_max / 2);
const setting = {
gear_readied,
gear_stowed,
gear_readied_max,
gear_stowed_max,
gear_readied_over: (parseInt(v.gear_readied) > gear_readied_max) ? "1" : "0",
gear_stowed_over: (parseInt(v.gear_stowed) > gear_stowed_max) ? "1" : "0",
};
mySetAttrs(setting, v, {
silent: true
});
});
};
getSectionIDs("repeating_gear", gearIDs => {
getSectionIDs("repeating_weapons", weaponIDs => {
getSectionIDs("repeating_armor", armorIDs => doCalc(gearIDs, weaponIDs, armorIDs));
});
});
};
const generateWeaponDisplay = () => {
// Generates the weapon menu and sets the display of weapons
// in display mode in one go.
getSectionIDs("repeating_weapons", idArray => {
const prefixes = idArray.map(id => `repeating_weapons_${id}`);
const sourceAttrs = [
...prefixes.map(prefix => `${prefix}_weapon_attack`),
...prefixes.map(prefix => `${prefix}_weapon_name`),
...prefixes.map(prefix => `${prefix}_weapon_skill_bonus`),
...prefixes.map(prefix => `${prefix}_weapon_attribute_mod`),
...prefixes.map(prefix => `${prefix}_weapon_damage`),
...prefixes.map(prefix => `${prefix}_weapon_shock`),
...prefixes.map(prefix => `${prefix}_weapon_shock_damage`),
...prefixes.map(prefix => `${prefix}_weapon_shock_ac`),
...prefixes.map(prefix => `${prefix}_weapon_skill_to_damage`),
...prefixes.map(prefix => `${prefix}_weapon_attack_display`),
...prefixes.map(prefix => `${prefix}_weapon_damage_display`),
...attributes.map(attr => `${attr}_mod`),
...weaponSkills,
"attack_bonus",
"str_dex_mod",
"macro_weapons"
];
getAttrs(sourceAttrs, v => {
const setting = {};
const baseAttackBonus = parseInt(v.attack_bonus) || 0;
prefixes.forEach(prefix => {
const attrBonus = parseInt(v[(v[`${prefix}_weapon_attribute_mod`] || "").slice(2, -1)]) || 0;
const skillBonus = parseInt(v[(v[`${prefix}_weapon_skill_bonus`] || "").slice(2, -1)]) ||
parseInt(v[`${prefix}_weapon_skill_bonus`]) || 0;
const damageBonus = attrBonus +
((v[`${prefix}_weapon_skill_to_damage`] === "@{weapon_skill_bonus}") ? skillBonus : 0);
const weaponDamage = (v[`${prefix}_weapon_damage`] === "0") ? "" : v[`${prefix}_weapon_damage`];
const shockString = (v[`${prefix}_weapon_shock`] !== "0") ? `, ${
(parseInt(v[`${prefix}_weapon_shock_damage`])||0) + damageBonus
}\xa0${translate("SHOCK").toLowerCase()}${
v[`${prefix}_weapon_shock_ac`] ? ` ${translate("VS_AC_LEQ")} ${v[`${prefix}_weapon_shock_ac`]}` : ""
}` : "";
const attack = baseAttackBonus + (parseInt(v[`${prefix}_weapon_attack`]) || 0) +
((skillBonus === -1) ? -2 : skillBonus) + attrBonus;
const damage = weaponDamage + (weaponDamage ?
((damageBonus === 0) ? "" : ((damageBonus > 0) ? ` + ${damageBonus}` : ` - ${-damageBonus}`)) :
damageBonus);
setting[`${prefix}_weapon_attack_display`] = (attack >= 0) ? `+${attack}` : attack;
setting[`${prefix}_weapon_damage_display`] = `${damage || 0}\xa0${translate("DAMAGE").toLowerCase()}${shockString}`;
});
setting.macro_weapons = prefixes.map((prefix, index) => {
const label = `${v[`${prefix}_weapon_name`]} (${setting[`${prefix}_weapon_attack_display`]})`;
return buildLink(label, `${prefix}_attack`, index === prefixes.length - 1);
}).join(" ");
mySetAttrs(setting, v, {
silent: true
});
});
});
};
const handleAmmoAPI = (sName) => {
const formula = (sName === "weapons") ? "[[-1 - @{weapon_burst}]]" : "-1";
getSectionIDs(`repeating_${sName}`, idArray => {
getAttrs([
"setting_use_ammo",
...idArray.map(id => `repeating_${sName}_${id}_weapon_use_ammo`),
...idArray.map(id => `repeating_${sName}_${id}_weapon_api`)
], v => {
const setting = idArray.reduce((m, id) => {
m[`repeating_${sName}_${id}_weapon_api`] =
(v.setting_use_ammo === "1" && v[`repeating_${sName}_${id}_weapon_use_ammo`] !== "0") ?
`\n!modattr --mute --charid @{character_id} --repeating_${sName}_${id}_weapon_ammo|${formula}` :
"";
return m;
}, {});
mySetAttrs(setting, v, {
silent: true
});
});
});
};
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,59 @@
/* global getAttrs, setAttrs, getSectionIDs, generateRowID, on, removeRepeatingRow, _, getTranslationByKey */
/* Drones */
const fillDroneStats = () => {
// This must be run from a repeating event, otherwise it will not know where to draw the data from.
getAttrs(["repeating_drones_drone_model"], v => {
const model = (v.repeating_drones_drone_model || "").toLowerCase().trim().replace(/ /g, "_");
if (autofillData.drones.hasOwnProperty(model)) {
const setting = Object.entries(autofillData.drones[model]).reduce((m, [key, value]) => {
m[`repeating_drones_${key}`] = value;
return m;
}, {});
setting.repeating_drones_drone_HP_max = setting.repeating_drones_drone_HP;
setAttrs(setting);
}
});
};
const fillDroneFitting = (num) => {
const prefix = `repeating_drones_drone_fitting_${num}`;
getAttrs([`${prefix}_desc`, `${prefix}_name`], v => {
const fittingName = (v[`${prefix}_name`] || "").toLowerCase().trim().replace(/ /g, "_");
if (v[`${prefix}_desc`] === "" && autofillData.droneFittings.includes(fittingName)) {
setAttrs({
[`${prefix}_desc`]: translate(`${fittingName.toUpperCase()}_DESC`)
});
}
});
};
const calculateDroneAttack = (prefixes, callback) => {
const sourceAttrs = prefixes.reduce((m, prefix) => {
return m.concat([
`${prefix}_drone_weapon1_ab`,
`${prefix}_drone_weapon1_active`,
`${prefix}_drone_weapon1_attack`,
`${prefix}_drone_weapon2_ab`,
`${prefix}_drone_weapon2_active`,
`${prefix}_drone_weapon2_attack`,
`${prefix}_drone_weapon3_ab`,
`${prefix}_drone_weapon3_active`,
`${prefix}_drone_weapon3_attack`,
]);
}, ["attack_bonus", "intelligence_mod", "skill_pilot", "skill_program"]);
getAttrs(sourceAttrs, v => {
const skillMod = Math.max(parseInt(v.skill_pilot), parseInt(v.skill_program)) || 0,
intMod = parseInt(v.intelligence_mod) || 0,
attackBonus = parseInt(v.attack_bonus) || 0;
const setting = prefixes.reduce((m, prefix) => {
[1, 2, 3].filter(num => v[`${prefix}_drone_weapon${num}_active`] === "1")
.forEach(num => {
m[`${prefix}_drone_weapon${num}_attack`] = intMod +
((skillMod === -1) ? -2 : skillMod) + attackBonus +
parseInt(v[[`${prefix}_drone_weapon${num}_ab`]] || 0);
});
return m;
}, {});
mySetAttrs(setting, v, callback);
});
};
@@ -0,0 +1,126 @@
/* global getAttrs, setAttrs, getSectionIDs, generateRowID, on, removeRepeatingRow, _, getTranslationByKey */
/**
* Register Event handlers
*/
on("sheet:opened", handleUpgrade);
on("sheet:opened change:npc", validateTab);
on("sheet:opened", setTranslatedQueries);
on("sheet:opened change:setting_skill_query", handleAttributeQueries);
on("sheet:opened change:setting_modifier_query", handleModifierQuery);
on("change:homebrew_skill_list", setTranslatedDefaults);
/* API use ammo boilerplate */
["weapons", "ship-weapons"].forEach(sName => {
on(`change:repeating_${sName}:weapon_use_ammo`, () => handleAmmoAPI(sName));
});
on("change:setting_use_ammo", () => {
handleAmmoAPI("weapons");
handleAmmoAPI("ship-weapons");
});
/* Character sheet */
on("change:class", fillClassStats);
attributes.forEach(attr => on(`change:${attr}_base change:${attr}_boosts`, () => calculateAttr(attr)));
attributes.forEach(attr => on(`change:${attr} change:${attr}_bonus`, () => calculateMod(attr)));
on("change:repeating_shells", function(eventInfo) {
if (eventInfo.sourceType === "player") {
validateShells(eventInfo);
}
});
on("change:setting_transhuman_enable change:setting_ai_enable", calculateShellAttrs)
on(weaponDisplayEvent, generateWeaponDisplay);
on("change:repeating_weapons:weapon_name", () => validateWeaponSkills());
on("change:homebrew_skill_list", () => getSectionIDs("repeating_weapons", validateWeaponSkills));
on("change:strain change:strain_permanent", validateStrain);
on("change:constitution", calculateMaxStrain);
on("change:repeating_cyberware", calculateCyberwareStrain);
on("change:strain_permanent_extra change:cyberware_strain_total", calculatePermanentStrain);
on("change:level", calculateSaves);
on([...effortAttributes, "repeating_psychic-skills:skill"].map(x => `change:${x}`).join(" "), calculateEffort);
on("change:magic_committed_effort_current change:magic_committed_effort_scene change:magic_committed_effort_day change:magic_total_effort", calculateMagicEffort);
on("change:ai_committed_processing_current change:ai_committed_processing_scene change:ai_committed_processing_day change:ai_extra_processing change:repeating_processing-nodes", calculateProcessing);
on("change:repeating_armor change:innate_ac remove:repeating_armor", calculateAC);
on("change:strength change:repeating_gear remove:repeating_gear change:repeating_weapons " +
"remove:repeating_weapons change:repeating_armor remove:repeating_armor", calculateGearReadiedStowed);
on("change:level change:setting_xp_scheme", calculateNextLevelXP);
on("change:setting_super_type", validateSuperTab);
/* Character chat macros */
on("change:homebrew_luck_save", buildSaveMenu);
on([
...skills.revised.map(x => `change:skill_${x}`),
...skills.first.map(x => `change:skill_${x}`),
"change:homebrew_skill_list",
"change:repeating_skills",
"change:setting_show_untrained_skills",
"remove:repeating_skills",
].join(" "), buildSkillMenu);
on([
...skills.psionic.map(x => `change:skill_${x}`),
"change:setting_super_type change:repeating_techniques remove:repeating_techniques",
"change:repeating_psychic-skills remove:repeating_psychic-skills"
].join(" "), buildPsionicsMenu);
on("change:setting_super_type change:repeating_spells remove:repeating_spells " +
"change:repeating_magic-skills remove:repeating_magic-skills " +
"change:skill_magic change:skill_magic2_name change:skill_magic2", buildMagicMenu);
/* Repeating autofill */
autofillSections.forEach(sName => {
on(`change:generate_${sName}_source`, () => generateAutofillInfo(sName));
on(`change:generate_${sName}_button`, () => generateAutofillRow(sName));
});
/* Ship sheet */
on("change:ship_hulltype", fillShipStats);
on("change:ship_class", () => {
setShipClass();
["ship-fittings", "ship-defenses"].forEach(sName => generateAutofillInfo(sName));
});
on(shipStatEvent, calculateShipStats);
on("change:repeating_ship-weapons:weapon_name change:repeating_ship-weapons:weapon_attack_bonus " +
"remove:repeating_ship-weapons", buildShipWeaponsMenu);
/* Drones */
[1,2,3,4,5].forEach(num => {
on(`change:repeating_drones:drone_fitting_${num}_name`, () => fillDroneFitting(num));
});
on("change:repeating_drones:drone_model", fillDroneStats);
on("change:attack_bonus change:intelligence_mod change:skill_pilot change:skill_program",
() => getSectionIDs("repeating_drones", idArray => {
calculateDroneAttack(idArray.map(id => `repeating_drones_${id}`));
}));
on([
...[1, 2, 3].map(n => `change:repeating_drones:drone_weapon${n}_ab`),
...[1, 2, 3].map(n => `change:repeating_drones:drone_weapon${n}_active`)
].join(" "), () => calculateDroneAttack(["repeating_drones"]));
/* NPC sheet */
on("change:npc_stat_block", fillNPC);
on("change:npc_rolls_hidden", handleNPCRollHide);
on("change:repeating_npc-attacks:attack_name", addNPCAttackBonus);
on("change:npc_roll_full_attack change:repeating_npc-attacks:attack_number", setNPCMultiAttacks);
on("change:repeating_npc-abilities:ability_name remove:repeating_npc-abilities", buildAbilitiesMenu);
on("change:repeating_npc-attacks:attack_name change:repeating_npc-attacks:attack_ab " +
"change:repeating_npc-attacks:attack_number remove:repeating_npc-attacks", buildAttacksMenu);
on("change:npc change:npc_armor_type change:macro_npc_attacks change:macro_npc_abilities", buildStatblock);
@@ -0,0 +1,196 @@
/* global getAttrs, setAttrs, getSectionIDs, generateRowID, on, removeRepeatingRow, _, getTranslationByKey */
/* Menu builders */
/* Helper functions for producing skill chat menus */
const skillToMacro = v => skill => {
const namedSkills = [
"culture_one", "culture_two", "culture_three",
"culture_alien", "profession", "magic2"
];
if (namedSkills.includes(skill))
return [`@{skill_${skill}_name} (${sign(v[`skill_${skill}`])})`, `skill_${skill}`];
else
return [`^{${skill.toUpperCase()}} (${sign(v[`skill_${skill}`])})`, `skill_${skill}`];
};
const idToSkillMacro = (v, sName) => id => {
const prefix = `repeating_${sName}_${id}_skill`;
return [`${v[`${prefix}_name`]} (${sign(v[prefix])})`, prefix];
};
const buildSkillMacro = ([name, command], index, allSkills) => {
return buildLink(name, command, index + 1 === allSkills.length);
};
/* End helper functions */
const buildSaveMenu = () => {
getAttrs(["homebrew_luck_save", "macro_saves"], v => {
const macro_saves = buildLink("^{PHYSICAL} (v@{save_physical})", ("save_physical")) + " " +
buildLink("^{MENTAL} (v@{save_mental})", ("save_mental")) + " " +
buildLink("^{EVASION} (v@{save_evasion})", ("save_evasion"), v.homebrew_luck_save !== "1") +
((v.homebrew_luck_save === "1") ?
(" " + buildLink("^{LUCK} (v@{save_luck})", ("save_luck"), true)) : "");
mySetAttrs({
macro_saves
}, v);
});
};
const buildSkillMenu = () => {
getSectionIDs("repeating_skills", idArray => {
const sourceAttrs = [
...skills.revised.map(sk => `skill_${sk}`),
...skills.first.map(sk => `skill_${sk}`),
"homebrew_skill_list",
"setting_show_untrained_skills",
"macro_skills",
...idArray.map(id => `repeating_skills_${id}_skill_name`),
...idArray.map(id => `repeating_skills_${id}_skill`),
];
getAttrs(sourceAttrs, v => {
const skillList = skills[v.homebrew_skill_list] || [];
const hasSkills = skillList.filter(skill => v[`skill_${skill}`] !== "-1").length ||
idArray.filter(id => v[`repeating_skills_${id}_skill`] !== "-1").length;
const macro_skills = `${[
...skillList.filter(skill => v[`skill_${skill}`] !== "-1").map(skillToMacro(v)),
...idArray.filter(id => v[`repeating_skills_${id}_skill`] !== "-1").map(idToSkillMacro(v, "skills"))
].map(buildSkillMacro).join(" ")}${v.setting_show_untrained_skills === "1" ? `${
hasSkills ? "\n\n**^{UNTRAINED}**\n" : ""}${[
...skillList.filter(skill => v[`skill_${skill}`] === "-1").map(skillToMacro(v)),
...idArray.filter(id => v[`repeating_skills_${id}_skill`] === "-1").map(idToSkillMacro(v, "skills"))
].map(buildSkillMacro).join(" ")}` : ""}`;
mySetAttrs({
macro_skills
}, v);
});
});
};
const buildPsionicsMenu = () => {
getSectionIDs("repeating_techniques", techniqueIDs => {
getSectionIDs("repeating_psychic-skills", skillIDs => {
const sourceAttrs = [
...skills.psionic.map(sk => `skill_${sk}`),
"setting_super_type",
"macro_psionics",
...skillIDs.map(id => `repeating_psychic-skills_${id}_skill_name`),
...skillIDs.map(id => `repeating_psychic-skills_${id}_skill`),
...techniqueIDs.map(id => `repeating_techniques_${id}_technique_name`),
];
getAttrs(sourceAttrs, v => {
if (v.setting_super_type === "magic" || v.setting_super_type === "neither") return;
const macro_psionics = `${[
...skills.psionic.filter(skill => v[`skill_${skill}`] !== "-1").map(skillToMacro(v)),
...skillIDs.filter(id => v[`repeating_psychic-skills_${id}_skill`] !== "-1")
.map(idToSkillMacro(v, "psychic-skills"))
].map(buildSkillMacro).join(" ")}${(techniqueIDs.length ? "\n\n" : "")}${
techniqueIDs.map((id, index, allIDs) => buildLink(v[`repeating_techniques_${id}_technique_name`],
`repeating_techniques_${id}_technique`, index + 1 === allIDs.length)).join(" ")}`;
mySetAttrs({
macro_psionics
}, v);
});
});
});
};
const buildMagicMenu = () => {
getSectionIDs("repeating_spells", spellIDs => {
getSectionIDs("repeating_magic-skills", skillIDs => {
const sourceAttrs = [
"skill_magic", "skill_magic2", "skill_magic2_name",
"setting_super_type", "macro_magic",
...skillIDs.map(id => `repeating_magic-skills_${id}_skill_name`),
...skillIDs.map(id => `repeating_magic-skills_${id}_skill`),
...spellIDs.map(id => `repeating_spells_${id}_spell_name`),
];
getAttrs(sourceAttrs, v => {
if (v.setting_super_type === "psionics" || v.setting_super_type === "neither") return;
const macro_magic = `${[
...(v.skill_magic2_name ? ["magic", "magic2"] : ["magic"]).map(skillToMacro(v)),
...skillIDs.map(idToSkillMacro(v, "magic-skills"))
].map(buildSkillMacro).join(" ")}${spellIDs.length ? "\n\n" : ""}${
spellIDs.map(id => buildLink(v[`repeating_spells_${id}_spell_name`], `repeating_spells_${id}_spell`))
.join(" ")}`;
mySetAttrs({
macro_magic
}, v);
});
});
});
};
const buildShipWeaponsMenu = () => {
getSectionIDs("repeating_ship-weapons", idArray => {
const sourceAttrs = [
...idArray.map(id => `repeating_ship-weapons_${id}_weapon_name`),
...idArray.map(id => `repeating_ship-weapons_${id}_weapon_attack_bonus`),
"macro_ship_weapons"
];
getAttrs(sourceAttrs, v => {
const macro_ship_weapons = idArray.map((id, index) => {
const title = v[`repeating_ship-weapons_${id}_weapon_name`] +
` ${sign(v[`repeating_ship-weapons_${id}_weapon_attack_bonus`])}`;
return buildLink(title, `repeating_ship-weapons_${id}_attack`, index + 1 === idArray.length);
}).join(" ");
mySetAttrs({
macro_ship_weapons
}, v);
});
});
};
const buildAttacksMenu = () => {
getSectionIDs("repeating_npc-attacks", idArray => {
const sourceAttrs = [
...idArray.map(id => `repeating_npc-attacks_${id}_attack_name`),
...idArray.map(id => `repeating_npc-attacks_${id}_attack_ab`),
...idArray.map(id => `repeating_npc-attacks_${id}_attack_number`),
"macro_npc_attacks"
];
getAttrs(sourceAttrs, v => {
const macro_npc_attacks = idArray.map((id, index) => {
const title = v[`repeating_npc-attacks_${id}_attack_name`] +
` ${sign(v[`repeating_npc-attacks_${id}_attack_ab`])}` +
((v[`repeating_npc-attacks_${id}_attack_number`] !== "1") ?
` (${v[`repeating_npc-attacks_${id}_attack_number`]} attacks)` : "");
return buildLink(title, `repeating_npc-attacks_${id}_attack`, index + 1 === idArray.length);
}).join(" ");
mySetAttrs({
macro_npc_attacks
}, v);
});
});
};
const buildAbilitiesMenu = () => {
getSectionIDs("repeating_npc-abilities", idArray => {
const sourceAttrs = [
...idArray.map(id => `repeating_npc-abilities_${id}_ability_name`),
"macro_npc_abilities"
];
getAttrs(sourceAttrs, v => {
const macro_npc_abilities = idArray.map((id, index) => {
return buildLink(v[`repeating_npc-abilities_${id}_ability_name`],
`repeating_npc-abilities_${id}_ability`, index + 1 === idArray.length);
}).join(" ");
mySetAttrs({
macro_npc_abilities
}, v);
});
});
};
const buildStatblock = () => {
const sourceAttrs = [
"npc",
"macro_npc_abilities",
"macro_npc_attacks",
"macro_statblock",
];
getAttrs(sourceAttrs, v => {
if (v.npc !== "1") return;
const macroList = [
"[**^{SAVES}** v@{npc_saves},](~npc_save) [**^{SKILLS}** +@{npc_skills},](~npc_skill) ",
"[**^{MORALE}** v@{npc_morale}](~npc_morale)\n",
"[**^{INITIATIVE_FIXED}** d8,](~npc_initiative) [**^{REACTION}** 2d6,](~npc_reaction) ",
"**Move** @{npc_move}\n"
];
if (v.macro_npc_attacks) macroList.push("\n**Attacks:** @{macro_npc_attacks}");
if (v.macro_npc_abilities) macroList.push("\n**Abilities:** @{macro_npc_abilities}");
mySetAttrs({
macro_statblock: macroList.join("")
}, v);
});
};
@@ -0,0 +1,574 @@
/* global getAttrs, setAttrs, getSectionIDs, generateRowID, on, removeRepeatingRow, _, getTranslationByKey */
/**
* Migrations
*/
/* Upgrade event handler */
const handleUpgrade = () => {
getAttrs(["character_sheet"], v => {
if (!v.character_sheet || v.character_sheet.indexOf(sheetName) !== 0)
upgradeFrom162();
else if (v.character_sheet.slice(32) !== sheetVersion)
upgradeSheet(v.character_sheet.slice(32), true);
});
};
/* Versioned upgrade */
const upgradeSheet = (version, firstTime = false, finalTime = false) => {
// Any version upgrade code should go here
const performUpgrade = (version) => {
const [major, minor, patch] = version.split(".").map(x => parseInt(x));
console.log(`Upgrading from version ${version}.`);
/** v2.1.0
* convert old format for burst settings for weapons and attacks
* set ammo and shock checkboxes to reasonable values
* convert old format for gear readied/stowed
**/
if (major === 2 && minor < 1) {
const upgradeFunction = _.after(4, () => {
// recalculate these things just to be sure, in case the v1.6.2 update
// missed them.
buildShipWeaponsMenu();
buildAttacksMenu();
buildMagicMenu();
generateWeaponDisplay();
attributes.forEach(calculateMod);
upgradeSheet("2.1.0");
});
getSectionIDs("repeating_weapons", idArray => {
const sourceAttrs = [
...idArray.map(id => `repeating_weapons_${id}_weapon_burst`),
...idArray.map(id => `repeating_weapons_${id}_weapon_shock_damage`),
...idArray.map(id => `repeating_weapons_${id}_weapon_ammo`)
];
getAttrs(sourceAttrs, v => {
const setting = idArray.reduce((m, id) => {
if (v[`repeating_weapons_${id}_weapon_burst`] === "0")
m[`repeating_weapons_${id}_weapon_burst`] = "";
else if (v[`repeating_weapons_${id}_weapon_burst`] === "2")
m[`repeating_weapons_${id}_weapon_burst`] = "+ 2[Burst]";
if (v[`repeating_weapons_${id}_weapon_shock_damage`] !== "0")
m[`repeating_weapons_${id}_weapon_shock`] = "{{shock=[[@{weapon_shock_damage} + @{weapon_attribute_mod}[Attribute] + @{weapon_skill_to_damage}[Skill]]] ^{SHOCK_DAMAGE_AGAINST_AC_LEQ} @{weapon_shock_ac}!}}";
if (v[`repeating_weapons_${id}_weapon_ammo`] && v[`repeating_weapons_${id}_weapon_ammo`] !== "0") {
m[`repeating_weapons_${id}_weapon_use_ammo`] = "{{ammo=[[0@{weapon_ammo} - (1 @{weapon_burst})]] / @{weapon_ammo|max}}}";
}
return m;
}, {});
setAttrs(setting, {}, upgradeFunction);
});
});
getSectionIDs("repeating_ship-weapons", idArray => {
getAttrs(idArray.map(id => `repeating_ship-weapons_${id}_weapon_ammo_max`), v => {
const setting = idArray.reduce((m, id) => {
if (v[`repeating_ship-weapons_${id}_weapon_ammo_max`] && v[`repeating_ship-weapons_${id}_weapon_ammo_max`] !== "0") {
m[`repeating_ship-weapons_${id}_weapon_use_ammo`] = "{{ammo=[[@{weapon_ammo} - 1]] / @{weapon_ammo_max}}}";
}
return m;
}, {});
setAttrs(setting, {}, upgradeFunction);
});
});
getSectionIDs("repeating_npc-attacks", idArray => {
getAttrs(idArray.map(id => `repeating_npc-attacks_${id}_attack_burst`), v => {
const setting = idArray.reduce((m, id) => {
if (v[`repeating_npc-attacks_${id}_attack_burst`] === "0")
m[`repeating_npc-attacks_${id}_attack_burst`] = "";
else if (v[`repeating_npc-attacks_${id}_attack_burst`] === "2")
m[`repeating_npc-attacks_${id}_attack_burst`] = "+ 2[Burst]";
return m;
}, {});
setAttrs(setting, {}, upgradeFunction);
});
});
getSectionIDs("repeating_gear", idArray => {
getAttrs(idArray.map(id => `repeating_gear_${id}_gear_status`), v => {
const setting = idArray.reduce((m, id) => {
m[`repeating_gear_${id}_gear_status`] =
(v[`repeating_gear_${id}_gear_status`] || "").toUpperCase();
return m;
}, {});
mySetAttrs(setting, v, {}, upgradeFunction);
});
});
}
/** v2.2.0
* convert single armor line to repeating armor
* Change @{attribute_query_none} to @{attribute_query}
**/
else if (major === 2 && minor < 2) {
const upgradeFunction = _.after(2, () => {
calculateStrDexMod();
calculateEffort();
buildPsionicsMenu();
buildSkillMenu();
upgradeSheet("2.2.0");
});
getAttrs(["armor_name", "armor_ac", "armor_encumbrance", "armor_type", "setting_ship_tab_name"], v => {
if (v.armor_ac) {
const data = {
armor_active: "1",
armor_ac: v.armor_ac,
armor_encumbrance: v.armor_encumbrance || "0",
armor_name: v.armor_name || "",
armor_status: "READIED",
armor_type: (v.armor_type || "").toUpperCase()
};
fillRepeatingSectionFromData("armor", data, upgradeFunction);
} else upgradeFunction();
if (v.setting_ship_tab_name === "MECH") setAttrs({
ship_vehicle_type: "MECH"
});
});
getSectionIDs("repeating_skills", skillIDs => getSectionIDs("repeating_magic-skills", magicIDs => {
getSectionIDs("repeating_psychic-skills", psychicIDs => {
const sourceAttrs = [
...skillIDs.map(id => `repeating_skills_${id}_skill_query`),
...magicIDs.map(id => `repeating_magic-skills_${id}_skill_query`),
...psychicIDs.map(id => `repeating_psychic-skills_${id}_skill_query`),
...skills.revised.map(skill => `skill_${skill}_query`),
...skills.first.map(skill => `skill_${skill}_query`),
...skills.psionic.map(skill => `skill_${skill}_query`),
"skill_magic_query", "skill_magic2_query"
];
getAttrs(sourceAttrs, v => {
const setting = sourceAttrs.reduce((m, attrName) => {
if (v[attrName] === "@{attribute_query_none}") m[attrName] = "@{attribute_query}";
return m;
}, {});
mySetAttrs(setting, v, {}, upgradeFunction);
});
});
}));
}
/** v2.3.1
* Regenerate drone and weapon ABs
**/
else if (major === 2 && (minor < 3 || (minor === 3 && patch === 0))) {
const upgradeFunction = _.after(1, () => {
upgradeSheet("2.3.1");
});
generateWeaponDisplay();
getSectionIDs("repeating_drones", idArray => {
calculateDroneAttack(idArray.map(id => `repeating_drones_${id}`), upgradeFunction);
});
}
/** v2.4.3
* Regenerate Cyberware strain because it was bugged
**/
else if (major === 2 && (minor < 4 || (minor === 4 && patch < 3))) {
calculateCyberwareStrain();
upgradeSheet("2.4.3");
}
/** v2.4.7
* Move attr to attr_base, and recalculate attr
**/
else if (major === 2 && minor === 4 && patch < 7) {
attributes.forEach(attr => {
getAttrs([attr, `${attr}_base`], v => {
mySetAttrs({[`${attr}_base`]: parseInt(v[`${attr}`] || 10)}, v, null, () => {
calculateAttr(attr);
});
});
});
upgradeSheet("2.4.7");
}
/** v2.4.11
* Recalculate drone attack to remove incorrect double Int bonus
*/
else if (major == 2 && minor == 4 && patch < 12) {
getSectionIDs("repeating_drones", idArray => {
calculateDroneAttack(idArray.map(id => `repeating_drones_${id}`));
})
upgradeSheet("2.4.12");
}
/** Final upgrade clause, always leave this around */
else upgradeSheet(sheetVersion, false, true);
};
if (firstTime) performUpgrade(version);
else setAttrs({
character_sheet: `${sheetName} v${version}`,
}, {}, () => {
if (!finalTime) performUpgrade(version);
});
};
/* Main upgrade from pre-worker versioning */
const upgradeFrom162 = () => {
console.log("Upgrading from versionless sheet (assumed to be fresh or v1.6.2).");
const upgradeFunction = _.after(13, () => {
upgradeSheet("2.0.1");
});
// Legacy migration
getAttrs([1, 2, 3, 4, 5, 6, 7, 8].map(i => `psionics_mastered_${i}`), v => {
const setting = {};
for (let i = 1; i < 9; i++) {
const technique = v[`psionics_mastered_${i}`];
if (technique) {
const newRowId = generateRowID();
setting[`repeating_techniques_${newRowId}_technique_name`] = technique;
}
}
setAttrs(setting);
});
getAttrs([1, 2, 3, 4].map(i => `cyberware_${i}`), v => {
const setting = {};
for (let i = 1; i < 5; i++) {
const cyberware = v[`cyberware_${i}`];
if (cyberware) {
const newRowId = generateRowID();
setting[`repeating_cyberware_${newRowId}_cyberware_name`] = cyberware;
}
}
setAttrs(setting);
});
getAttrs(["languages"], v => {
if (v.languages) {
const setting = {};
v.languages.split(/\r?\n/).filter(l => !!l).forEach(language => {
const newRowId = generateRowID();
setting[`repeating_languages_${newRowId}_language`] = language;
});
setAttrs(setting);
}
});
const attrConversionData = {
armor_enc: "armor_encumbrance",
cha: "charisma",
cha_misc: "charisma_bonus",
con: "constitution",
con_misc: "constitution_bonus",
dex: "dexterity",
dex_misc: "dexterity_bonus",
gender: "species_gender",
hd: "npc_hd",
int: "intelligence",
int_misc: "intelligence_bonus",
morale: "npc_morale",
move: "npc_move",
name: "npc_name",
notes: "npc_notes",
npc_ac: "AC",
saves: "npc_saves",
ship_hp_min: "ship_hp",
ship_current_crew: "ship_crew",
ship_last_maintenance_cost: "ship_last_maintenance",
skills: "npc_skills",
skill_biopsion: "skill_biopsionics",
skill_metapsion: "skill_metapsionics",
skill_points: "unspent_skill_points",
str: "strength",
strain_perm: "strain_permanent",
str_misc: "strength_bonus",
wis: "wisdom",
wis_misc: "wisdom_bonus",
};
const attrsToConvertFromOnTo1 = [
"homebrew_luck_save",
"homebrew_extra_skills",
"setting_heroic_enable",
];
const customConversionAttrs = [
"damage",
"homebrew_psionics_disable",
"npc_attacks",
"npc_attack_bonus",
"setting_space_magic_enable",
"ship_other_notes",
"ship_free_hardpoints",
"ship_free_mass",
"ship_free_power",
"skill_culture_alien_type",
"skill_culture_one_value",
"skill_culture_two_value",
"skill_culture_three_value",
"profession_type",
"tab",
...[1, 2, 3, 4].map(n => `homebrew_custom_counter_${n}_name`),
...[1, 2, 3, 4].map(n => `homebrew_custom_counter_${n}_counter`),
];
// convert non-repeating attributes
getAttrs([
...Object.keys(attrConversionData),
...Object.values(attrConversionData),
...attrsToConvertFromOnTo1,
...customConversionAttrs,
], v => {
const setting = Object.entries(attrConversionData).reduce((m, [oldName, newName]) => {
if (v[oldName] && v[oldName] !== "" && `${v[newName]}` !== `${v[oldName]}`)
m[newName] = v[oldName];
return m;
}, {});
attrsToConvertFromOnTo1.forEach(name => {
if (v[name] === "on") setting[name] = "1";
});
// convert skill name format
["one", "two", "three"].forEach(num => {
if (v[`skill_culture_${num}_value`])
setting[`skill_culture_${num}_name`] = `Culture/${v[`skill_culture_${num}_value`]}`;
});
if (v.profession_type) setting.skill_profession_name = `Profession/${v.profession_type}`;
if (v.skill_culture_alien_type)
setting.skill_culture_alien_name = `Culture/Alien/${v.skill_culture_alien_type}`;
// Write legacy ship data
if (v.ship_free_hardpoints || v.ship_free_mass || v.ship_free_power) {
setting.ship_other_notes = `\nLegacy attributes${
v.ship_free_power ? `\nFree Power: ${v.ship_free_power}` : ""}${
v.ship_free_mass ? `\nFree Mass: ${v.ship_free_mass}` : ""}${
v.ship_free_hardpoints ? `\nFree Hardpoints: ${v.ship_free_hardpoints}` : ""}
${v.ship_other_notes || ""}`;
}
// convert homebrew custom counter stuff
const customCounterData = [1, 2, 3, 4].reduce((m, num) => {
if (v[`homebrew_custom_counter_${num}_name`]) m.push({
resource_name: v[`homebrew_custom_counter_${num}_name`],
resource_count: v[`homebrew_custom_counter_${num}_counter`] || 0
});
return m;
}, []);
fillRepeatingSectionFromData("resources", customCounterData, upgradeFunction);
// Tab
if (`${v.tab}` === "1" || `${v.tab}` === "4") setting.tab = "character";
if (`${v.tab}` === "2") setting.tab = "ship";
if (`${v.tab}` === "3") {
setting.tab = "character";
setting.npc = "1";
}
// NPC attack
if (v.damage) {
const newAttack = {
attack_damage: v.damage,
attack_name: translate("ATTACK"),
attack_number: v.npc_attacks || "1"
};
fillRepeatingSectionFromData("npc-attacks", newAttack, upgradeFunction);
} else upgradeFunction();
// Psionics/Space Magic toggle conversion
if (v.setting_space_magic_enable === "on" && v.homebrew_psionics_disable !== "on")
setting.setting_super_type = "both";
else if (v.setting_space_magic_enable === "on")
setting.setting_super_type = "magic";
else if (v.homebrew_psionics_disable === "on")
setting.setting_super_type = "neither";
setAttrs(setting, {}, upgradeFunction);
});
// convert weapon attributes, and extract ship weapons
getSectionIDs("repeating_weapons", idArray => {
const oldAttrs = [
...idArray.map(id => `repeating_weapons_${id}_attribute_mod`),
...idArray.map(id => `repeating_weapons_${id}_add_skill`),
...idArray.map(id => `repeating_weapons_${id}_weapon_shock`),
...idArray.map(id => `repeating_weapons_${id}_ship_weapon_name`),
...idArray.map(id => `repeating_weapons_${id}_ship_weapon_power`),
...idArray.map(id => `repeating_weapons_${id}_ship_weapon_ab`),
...idArray.map(id => `repeating_weapons_${id}_ship_weapon_damage`),
...idArray.map(id => `repeating_weapons_${id}_ship_weapon_ammo`),
...idArray.map(id => `repeating_weapons_${id}_ship_weapon_special_effects`),
...idArray.map(id => `repeating_weapons_${id}_ship_weapon_broken`),
];
getAttrs(oldAttrs, v => {
const setting = idArray.reduce((m, id) => {
if (v[`repeating_weapons_${id}_add_skill`] === "@{weapon_skill_bonus}")
m[`repeating_weapons_${id}_weapon_skill_to_damage`] = "@{weapon_skill_bonus}";
if (v[`repeating_weapons_${id}_weapon_shock`])
m[`repeating_weapons_${id}_weapon_shock_damage`] = v[`repeating_weapons_${id}_weapon_shock`];
const modValue = v[`repeating_weapons_${id}_attribute_mod`];
switch (modValue) {
case "@{dex_bonus}":
m[`repeating_weapons_${id}_weapon_attribute_mod`] = "@{dexterity_mod}";
break;
case "@{con_bonus}":
m[`repeating_weapons_${id}_weapon_attribute_mod`] = "@{constitution_mod}";
break;
case "@{int_bonus}":
m[`repeating_weapons_${id}_weapon_attribute_mod`] = "@{intelligence_mod}";
break;
case "@{wis_bonus}":
m[`repeating_weapons_${id}_weapon_attribute_mod`] = "@{wisdom_mod}";
break;
case "@{cha_bonus}":
m[`repeating_weapons_${id}_weapon_attribute_mod`] = "@{charisma_mod}";
break;
default:
m[`repeating_weapons_${id}_weapon_attribute_mod`] = "@{strength_mod}";
}
return m;
}, {});
const data = idArray.filter(id => v[`repeating_weapons_${id}_ship_weapon_name`])
.map(id => {
const row = {};
row.weapon_name = v[`repeating_weapons_${id}_ship_weapon_name`];
if (v[`repeating_weapons_${id}_ship_weapon_power`])
row.weapon_power = v[`repeating_weapons_${id}_ship_weapon_power`];
if (v[`repeating_weapons_${id}_ship_weapon_ab`])
row.weapon_attack_bonus = v[`repeating_weapons_${id}_ship_weapon_ab`];
if (v[`repeating_weapons_${id}_ship_weapon_damage`])
row.weapon_damage = v[`repeating_weapons_${id}_ship_weapon_damage`];
if (v[`repeating_weapons_${id}_ship_weapon_ammo`])
row.weapon_ammo = v[`repeating_weapons_${id}_ship_weapon_ammo`];
if (v[`repeating_weapons_${id}_ship_weapon_special_effects`])
row.weapon_qualities = v[`repeating_weapons_${id}_ship_weapon_special_effects`];
if (v[`repeating_weapons_${id}_ship_weapon_broken`] === "on")
row.weapon_broken = "1";
return row;
});
fillRepeatingSectionFromData("ship-weapons", data, upgradeFunction);
setAttrs(setting, null, upgradeFunction);
});
});
// convert skills
getSectionIDs("repeating_skills", idArray => {
const oldAttrs = [
...idArray.map(id => `repeating_skills_${id}_custom_skill_1_name`),
...idArray.map(id => `repeating_skills_${id}_custom_skill_2_name`),
...idArray.map(id => `repeating_skills_${id}_custom_skill_1_level`),
...idArray.map(id => `repeating_skills_${id}_custom_skill_2_level`),
...idArray.map(id => `repeating_skills_${id}_custom_skill_1_specialist`),
...idArray.map(id => `repeating_skills_${id}_custom_skill_2_specialist`),
];
getAttrs(oldAttrs, v => {
const data = idArray.reduce((m, id) => {
[1, 2].forEach(i => {
if (v[`repeating_skills_${id}_custom_skill_${i}_name`]) {
const skillLevel = (typeof v[`repeating_skills_${id}_custom_skill_${i}_level`] === "undefined") ?
"-1" : v[`repeating_skills_${id}_custom_skill_${i}_level`];
m.push({
skill_name: v[`repeating_skills_${id}_custom_skill_${i}_name`],
skill: skillLevel,
skill_specialist: v[`repeating_skills_${id}_custom_skill_${i}_specialist`] || "2d6"
});
}
});
return m;
}, []);
idArray.forEach(id => removeRepeatingRow(`repeating_skills_${id}`));
fillRepeatingSectionFromData("skills", data, upgradeFunction);
});
});
// convert techniques
getSectionIDs("repeating_technique", idArray => {
const oldAttrs = [
...idArray.map(id => `repeating_technique_${id}_technique`),
...idArray.map(id => `repeating_technique_${id}_technique_description`),
];
getAttrs(oldAttrs, v => {
const data = idArray.reduce((m, id) => {
if (v[`repeating_technique_${id}_technique`])
m.push({
technique_name: v[`repeating_technique_${id}_technique`],
technique_description: v[`repeating_technique_${id}_technique_description`] || ""
});
return m;
}, []);
fillRepeatingSectionFromData("techniques", data, upgradeFunction);
idArray.forEach(id => removeRepeatingRow(`repeating_technique_${id}`));
});
});
// convert cyberware name
getSectionIDs("repeating_cyberware", idArray => {
getAttrs(idArray.map(id => `repeating_cyberware_${id}_cyberware`), v => {
const setting = idArray.reduce((m, id) => {
if (v[`repeating_cyberware_${id}_cyberware`])
m[`repeating_cyberware_${id}_cyberware_name`] = v[`repeating_cyberware_${id}_cyberware`];
return m;
}, {});
setAttrs(setting, null, upgradeFunction);
});
});
// convert goals
getSectionIDs("repeating_goals", idArray => {
const oldAttrs = [
...idArray.map(id => `repeating_goals_${id}_misc_goal`),
...idArray.map(id => `repeating_goals_${id}_misc_goal_xp`),
];
getAttrs(oldAttrs, v => {
const setting = idArray.reduce((m, id) => {
if (v[`repeating_goals_${id}_misc_goal`])
m[`repeating_goals_${id}_goal_name`] = v[`repeating_goals_${id}_misc_goal`];
if (v[`repeating_goals_${id}_misc_goal_xp`])
m[`repeating_goals_${id}_goal_xp`] = v[`repeating_goals_${id}_misc_goal_xp`];
return m;
}, {});
setAttrs(setting, null, upgradeFunction);
});
});
// convert languages
getSectionIDs("repeating_languages", idArray => {
getAttrs(idArray.map(id => `repeating_languages_${id}_languages`), v => {
const setting = idArray.reduce((m, id) => {
if (v[`repeating_languages_${id}_languages`])
m[`repeating_languages_${id}_language`] = v[`repeating_languages_${id}_languages`];
return m;
}, {});
setAttrs(setting, null, upgradeFunction);
});
});
// convert gear status
getSectionIDs("repeating_gear", idArray => {
getAttrs(idArray.map(id => `repeating_gear_${id}_gear_readied`), v => {
const setting = idArray.reduce((m, id) => {
if (`${v[`repeating_gear_${id}_gear_readied`]}` === "1")
m[`repeating_gear_${id}_gear_status`] = "readied";
else if (`${v[`repeating_gear_${id}_gear_readied`]}` === "2")
m[`repeating_gear_${id}_gear_status`] = "stowed";
return m;
}, {});
setAttrs(setting, null, upgradeFunction);
});
});
// convert defenses
getSectionIDs("repeating_defenses", idArray => {
const oldAttrs = [
...idArray.map(id => `repeating_defenses_${id}_ship_defense_name`),
...idArray.map(id => `repeating_defenses_${id}_ship_defense_special_effects`),
...idArray.map(id => `repeating_defenses_${id}_ship_defense_broken`),
];
getAttrs(oldAttrs, v => {
const data = idArray.map(id => {
const row = {};
if (v[`repeating_defenses_${id}_ship_defense_name`])
row.defense_name = v[`repeating_defenses_${id}_ship_defense_name`];
if (v[`repeating_defenses_${id}_ship_defense_special_effects`])
row.defense_effect = v[`repeating_defenses_${id}_ship_defense_special_effects`];
if (v[`repeating_defenses_${id}_ship_defense_broken`])
row.defense_broken = "1";
return row;
});
fillRepeatingSectionFromData("ship-defenses", data, upgradeFunction);
});
});
// convert fittings
getSectionIDs("repeating_fittings", idArray => {
const oldAttrs = [
...idArray.map(id => `repeating_fittings_${id}_ship_fitting_name`),
...idArray.map(id => `repeating_fittings_${id}_ship_fitting_special_effects`),
...idArray.map(id => `repeating_fittings_${id}_ship_fitting_broken`),
];
getAttrs(oldAttrs, v => {
const data = idArray.map(id => {
const row = {};
if (v[`repeating_fittings_${id}_ship_fitting_name`])
row.fitting_name = v[`repeating_fittings_${id}_ship_fitting_name`];
if (v[`repeating_fittings_${id}_ship_fitting_special_effects`])
row.fitting_effect = v[`repeating_fittings_${id}_ship_fitting_special_effects`];
if (v[`repeating_fittings_${id}_ship_fitting_broken`])
row.fitting_broken = "1";
return row;
});
fillRepeatingSectionFromData("ship-fittings", data, upgradeFunction);
});
});
};
@@ -0,0 +1,82 @@
/* global getAttrs, setAttrs, getSectionIDs, generateRowID, on, removeRepeatingRow, _, getTranslationByKey */
/* NPC */
const fillNPC = () => {
getAttrs(["npc_stat_block"], (v) => {
if (v.npc_stat_block && autofillData.statblocks[v.npc_stat_block]) {
const [
HD, AC, npc_attack_bonus, damage, attacks, npc_move,
npc_morale, npc_skills, npc_saves, armor_type
] = autofillData.statblocks[v.npc_stat_block];
const setting = {
AC,
npc_attack_bonus,
npc_move,
npc_morale,
npc_skills,
npc_saves
};
if (armor_type) setting.npc_armor_type = armor_type;
if (HD.includes("hp")) setting.HP = HD.replace("hp", "");
else setting.npc_hd = HD;
setAttrs(setting);
if (damage !== "Unarmed") {
const newAttack = {
attack_ab: npc_attack_bonus,
attack_damage: damage,
attack_name: translate("ATTACK"),
attack_number: attacks
};
fillRepeatingSectionFromData("npc-attacks", newAttack);
}
}
});
};
const addNPCAttackBonus = () => {
getAttrs(["repeating_npc-attacks_attack_ab", "npc_attack_bonus"], v => {
if (`${v["repeating_npc-attacks_attack_ab"]}` === "0") {
setAttrs({
["repeating_npc-attacks_attack_ab"]: v.npc_attack_bonus
});
}
});
};
const setNPCMultiAttacks = () => {
getSectionIDs("repeating_npc-attacks", idArray => {
const sourceAttrs = [
...idArray.map(id => `repeating_npc-attacks_${id}_attack_number`),
"npc_roll_full_attack"
];
getAttrs(sourceAttrs, v => {
const setting = idArray.reduce((m, id) => {
if (v.npc_roll_full_attack === "1") {
const num = parseInt(v[`repeating_npc-attacks_${id}_attack_number`]) || 1;
m[`repeating_npc-attacks_${id}_attack_extra_macro`] = [2, 3, 4, 5, 6, 7, 8].map(n => {
if (n <= num)
return `{{attack${n}=[[1d20 + @{attack_ab} @{attack_burst} @{modifier_query}]]}} ` +
`{{damage${n}=[[@{attack_damage} @{attack_burst}]]}} `;
else return "";
}).join("");
} else {
m[`repeating_npc-attacks_${id}_attack_extra_macro`] = "";
}
return m;
}, {});
setAttrs(setting);
});
});
};
const handleNPCRollHide = () => {
const types = ["hp", "initiative", "save", "skill", "morale", "reaction"];
getAttrs(["npc_rolls_hidden", ...types.map(x => `npc_${x}_hidden`)], v => {
const setting = types.reduce((m, n) => {
m[`npc_${n}_hidden`] = v.npc_rolls_hidden;
return m;
}, {});
mySetAttrs(setting, v);
});
};
@@ -0,0 +1,88 @@
/* global getAttrs, setAttrs, getSectionIDs, generateRowID, on, removeRepeatingRow, _, getTranslationByKey */
/* Ship Code */
const fillShipStats = () => {
getAttrs(["ship_hulltype", ...shipStats], v => {
const label = v.ship_hulltype && reverseHullTypes[v.ship_hulltype.toLowerCase()];
if (label && autofillData.hulltypes.hasOwnProperty(label)) {
const data = Object.assign({}, autofillData.hulltypes[label]);
data.ship_hp_max = data.ship_hp;
Object.keys(data).forEach(key => {
//Might remove later. Unsure if desired feature.
if (!(["", 0, "0"].includes(v[key]))) delete data[key]; //Prevents changing Hull Type
});
mySetAttrs(data, v);
calculateShipStats();
}
});
};
const setShipClass = () => {
// Sets the internal ship_class_normalised attribute responsible
// for filtering ship modules according to class.
getAttrs(["ship_class", "ship_class_normalised"], v => {
if (["fighter", translate("FIGHTER").toLowerCase()].includes(v.ship_class.toLowerCase()))
setAttrs({
ship_class_normalised: "fighter"
});
else if (["frigate", translate("FRIGATE").toLowerCase()].includes(v.ship_class.toLowerCase()))
setAttrs({
ship_class_normalised: "frigate"
});
else if (["cruiser", translate("CRUISER").toLowerCase()].includes(v.ship_class.toLowerCase()))
setAttrs({
ship_class_normalised: "cruiser"
});
else mySetAttrs({
ship_class_normalised: ""
}, v);
});
};
const calculateShipStats = () => {
// Calculates power, mass, and hardpoints remaining.
const doCalc = (weaponIDs, fittingIDs, defenseIDs) => {
const oldAttrs = [
...weaponIDs.map(id => `repeating_ship-weapons_${id}_weapon_power`),
...weaponIDs.map(id => `repeating_ship-weapons_${id}_weapon_mass`),
...weaponIDs.map(id => `repeating_ship-weapons_${id}_weapon_hardpoints`),
...weaponIDs.map(id => `repeating_ship-weapons_${id}_weapon_price`),
...fittingIDs.map(id => `repeating_ship-fittings_${id}_fitting_power`),
...fittingIDs.map(id => `repeating_ship-fittings_${id}_fitting_mass`),
...fittingIDs.map(id => `repeating_ship-fittings_${id}_fitting_price`),
...defenseIDs.map(id => `repeating_ship-defenses_${id}_defense_power`),
...defenseIDs.map(id => `repeating_ship-defenses_${id}_defense_mass`),
...defenseIDs.map(id => `repeating_ship-defenses_${id}_defense_price`),
"ship_power_max", "ship_mass_max", "ship_hardpoints_max",
"ship_power", "ship_mass", "ship_hardpoints", "ship_price", "ship_hull_price"
];
getAttrs(oldAttrs, v => {
const ship_power = v.ship_power_max - sum([
...weaponIDs.map(id => `repeating_ship-weapons_${id}_weapon_power`),
...fittingIDs.map(id => `repeating_ship-fittings_${id}_fitting_power`),
...defenseIDs.map(id => `repeating_ship-defenses_${id}_defense_power`),
].map(x => v[x]));
const ship_mass = v.ship_mass_max - sum([
...weaponIDs.map(id => `repeating_ship-weapons_${id}_weapon_mass`),
...fittingIDs.map(id => `repeating_ship-fittings_${id}_fitting_mass`),
...defenseIDs.map(id => `repeating_ship-defenses_${id}_defense_mass`),
].map(x => v[x]));
const ship_hardpoints = v.ship_hardpoints_max -
sum(weaponIDs.map(id => v[`repeating_ship-weapons_${id}_weapon_hardpoints`]));
const ship_price = new Intl.NumberFormat().format(parseInt(v.ship_hull_price) + sum([
...weaponIDs.map(id => `repeating_ship-weapons_${id}_weapon_price`),
...fittingIDs.map(id => `repeating_ship-fittings_${id}_fitting_price`),
...defenseIDs.map(id => `repeating_ship-defenses_${id}_defense_price`),
].map(x => v[x])));
mySetAttrs({
ship_power,
ship_mass,
ship_hardpoints,
ship_price
}, v, {
silent: true
});
});
};
getSectionIDs("repeating_ship-weapons", A => getSectionIDs("repeating_ship-fittings", B => {
getSectionIDs("repeating_ship-defenses", C => doCalc(A, B, C));
}));
};
@@ -0,0 +1,96 @@
/* global getAttrs, setAttrs, getSectionIDs, generateRowID, on, removeRepeatingRow, _, getTranslationByKey */
/* Translations */
const setTranslatedDefaults = () => {
const specialSkills = {
skill_culture_alien_name: {
trans: `${translate("CULTURE_ALIEN")}/`,
default: "Culture/Alien/"
},
skill_culture_one_name: {
trans: `${translate("CULTURE")}/`,
default: "Culture/"
},
skill_culture_two_name: {
trans: `${translate("CULTURE")}/`,
default: "Culture/"
},
skill_culture_three_name: {
trans: `${translate("CULTURE")}/`,
default: "Culture/"
},
skill_profession_name: {
trans: `${translate("PROFESSION")}/`,
default: "Profession/"
},
};
getAttrs([...Object.keys(specialSkills), "homebrew_skill_list", "innate_ac_name"], v => {
const setting = {};
if (v.homebrew_skill_list === "first") {
Object.entries(specialSkills).forEach(([name, data]) => {
if (v[name] === data.default && v[name] !== data.trans) setting[name] = data.trans;
});
}
setting.innate_ac_name = translate("INNATE_AC");
mySetAttrs(setting, v);
});
};
const handleAttributeQueries = () => {
// Attribute query magic to set the global query variables according to
// translations and the state of the setting_skill_query attribute.
const attrQueries = attributes.map(attr => {
const translated = translate(attr.toUpperCase());
return `${translated},+ @{${attr}_mod}[${translated}]]]&#125;&#125; ` +
`{{attribute= + ${translate(`${attr.toUpperCase()}_SHORT`)}&#125;&#125;`;
});
getAttrs(["attribute_query", "setting_skill_query",
...attributes.map(a => `attribute_query_${a.slice(0,3)}`)
], v => {
if (v.setting_skill_query === "hover" || v.setting_skill_query === "hide") {
mySetAttrs({
attribute_query: `?{${translate("ATTRIBUTE")}|${[attrQueries[0], ...attrQueries.slice(1)].join("|")}}`,
attribute_query_str: `+ @{strength_mod}[${translate("STRENGTH")}]]]}} {{attribute= + ${translate("STRENGTH_SHORT")}}}`,
attribute_query_dex: `+ @{dexterity_mod}[${translate("DEXTERITY")}]]]}} {{attribute= + ${translate("DEXTERITY_SHORT")}}}`,
attribute_query_con: `+ @{constitution_mod}[${translate("CONSTITUTION")}]]]}} {{attribute= + ${translate("CONSTITUTION_SHORT")}}}`,
attribute_query_int: `+ @{intelligence_mod}[${translate("INTELLIGENCE")}]]]}} {{attribute= + ${translate("INTELLIGENCE_SHORT")}}}`,
attribute_query_wis: `+ @{wisdom_mod}[${translate("WISDOM")}]]]}} {{attribute= + ${translate("WISDOM_SHORT")}}}`,
attribute_query_cha: `+ @{charisma_mod}[${translate("CHARISMA")}]]]}} {{attribute= + ${translate("CHARISMA_SHORT")}}}`,
}, v);
} else if (v.setting_skill_query === "query") {
mySetAttrs({
attribute_query: `?{${translate("ATTRIBUTE")}|${[attrQueries[0], ...attrQueries.slice(1)].join("|")}}`,
attribute_query_str: `?{${translate("ATTRIBUTE")}|${[attrQueries[0], ...attrQueries.slice(1)].join("|")}}`,
attribute_query_dex: `?{${translate("ATTRIBUTE")}|${[attrQueries[1], attrQueries[0], ...attrQueries.slice(2)].join("|")}}`,
attribute_query_con: `?{${translate("ATTRIBUTE")}|${[attrQueries[2], ...attrQueries.slice(0,2), ...attrQueries.slice(3)].join("|")}}`,
attribute_query_int: `?{${translate("ATTRIBUTE")}|${[attrQueries[3], ...attrQueries.slice(0,3), ...attrQueries.slice(4)].join("|")}}`,
attribute_query_wis: `?{${translate("ATTRIBUTE")}|${[attrQueries[4], ...attrQueries.slice(0,4), ...attrQueries.slice(5)].join("|")}}`,
attribute_query_cha: `?{${translate("ATTRIBUTE")}|${[attrQueries[5], ...attrQueries.slice(0,5)].join("|")}}`,
}, v);
}
});
};
const handleModifierQuery = () => {
getAttrs(["modifier_query", "setting_modifier_query"], v => {
if (`${v.setting_modifier_query}` === "1") {
mySetAttrs({
modifier_query: `+ ?{${translate("MODIFIER")}|0}[${translate("MODIFIER_SHORT")}]`,
}, v);
} else mySetAttrs({
modifier_query: " "
}, v);
});
};
const setTranslatedQueries = () => {
getAttrs(["burst_query", "extra_hp_query", "translation_numdice", "proficient_query", "skill_name_query"], v => {
const setting = {
burst_query: `?{${translate("BURST")}|${translate("YES")},+ 2[${translate("BURST")}]|${translate("NO")},&` + "#" + "32;}",
extra_hp_query: `?{${translate("EXTRA_HP_QUERY")}|0}[${translate("BONUS")}]`,
proficient_query: `?{${translate("PROFICIENT")}|${translate("YES")}, @{npc_skills}|${translate("NO")}, 0}[${translate("SKILL")}]`,
skill_name_query: `?{${translate("SKILL_NAME")}|${translate("SKILL")}}`,
translation_numdice: translate("NUMBER_OF_DICE")
};
mySetAttrs(setting, v);
});
};
@@ -0,0 +1,48 @@
/* global getAttrs, setAttrs, getSectionIDs, generateRowID, on, removeRepeatingRow, _, getTranslationByKey */
/* Utility functions */
const sign = (value) => {
const val = parseInt(value) || 0;
if (val >= 0) return `+${val}`;
else return `${val}`;
};
const sum = (list) => list.reduce((m, c) => m + (parseInt(c) || 0), 0);
const buildLink = (caption, ability, last) => `[${caption}${!last ? "," : ""}](~${ability})`;
const mySetAttrs = (setting, values, ...rest) => {
// This is a version of setAttrs that expects an extra values parameter
// (as received from getAttrs). It will only set values in setting that differ
// from their current value on the sheet. The intention is to not
// set values unnecessarily (it's expensive) and to reduce bloat
// in the Attributes & Abilities tab.
Object.keys(setting).forEach(k => {
if (`${values[k]}` === `${setting[k]}`) delete setting[k];
});
setAttrs(setting, ...rest);
};
const fillRepeatingSectionFromData = (sName, data, callback) => {
// Populates the repeating section repeating_${SName} with new
// rows from the data array. Every entry of the array is expected
// to be an object, and its key/value pairs will be written into
// the repeating section as a new row. If data is not an array
// but a single object, it will be treated like an array with
// a single element.
callback = callback || (() => {});
const createdIDs = [],
getRowID = () => {
while (true) {
let newID = generateRowID();
if (!createdIDs.includes(newID)) {
createdIDs.push(newID);
return newID;
}
}
};
const setting = (Array.isArray(data) ? data : [data]).map(o => {
const newID = getRowID();
return Object.entries(o).reduce((m, [key, value]) => {
m[`repeating_${sName}_${newID}_${key}`] = `${value}`;
return m;
}, {});
}).reduce((m, o) => Object.assign(m, o), {});
setAttrs(setting, {}, callback);
};
@@ -0,0 +1,76 @@
/* global getAttrs, setAttrs, getSectionIDs, generateRowID, on, removeRepeatingRow, _, getTranslationByKey */
/**
* Validations
*/
const validateTab = () => {
getAttrs(["tab", "npc"], v => {
if (v.tab === "character" && v.npc === "1") setAttrs({
tab: "npc"
});
if (v.tab === "npc" && v.npc === "0") setAttrs({
tab: "character"
});
});
};
const validateSuperTab = () => {
getAttrs(["setting_super_type", "tab_super"], v => {
const setting = {};
if (v.setting_super_type === "magic") setting.tab_super = "magic";
if (v.setting_super_type === "psionics") setting.tab_super = "psionics";
mySetAttrs(setting, v);
});
};
const validateStrain = () => {
getAttrs(["strain", "strain_permanent", "strain_max"], v => {
const currentStrain = parseInt(v.strain) || 0,
permanentStrain = parseInt(v.strain_permanent) || 0,
strain = Math.min(parseInt(v.strain_max), Math.max(currentStrain, permanentStrain)) || 0;
if (strain !== currentStrain) setAttrs({
strain
});
});
};
const validateWeaponSkills = (ids) => {
// Makes sure that the select for the weapon skill is never in an invalid state.
const prefixes = (ids && ids.map(id => `repeating_weapons_${id}`)) || ["repeating_weapons"];
getAttrs(["homebrew_skill_list", ...prefixes.map(p => `${p}_weapon_skill_bonus`)], v => {
const revisedList = ["@{skill_exert}", "@{skill_punch}", "@{skill_shoot}", "@{skill_stab}",
"@{skill_telekinesis}", "0"
],
firstList = ["@{skill_combat_energy}", "@{skill_combat_gunnery}", "@{skill_combat_primitive}", "@{skill_combat_projectile}",
"@{skill_combat_psitech}", "@{skill_combat_unarmed}", "@{skill_telekinesis}", "0"
],
type = v.homebrew_skill_list,
setting = {};
prefixes.forEach(prefix => {
if (type === "revised" && !revisedList.includes(v[`${prefix}_weapon_skill_bonus`]))
setting[`${prefix}_weapon_skill_bonus`] = "@{skill_shoot}";
if (type === "first" && !firstList.includes(v[`${prefix}_weapon_skill_bonus`]))
setting[`${prefix}_weapon_skill_bonus`] = "@{skill_combat_energy}";
});
setAttrs(setting);
});
};
const validateShells = (e) => {
//Unchecks every shell 'active' toggle except the one toggle now.
console.log(e.sourceAttribute);
console.log(e.sourceAttribute.slice(17, -13));
console.log(e.sourceAttribute.slice(-6));
if (e.sourceAttribute.slice(-6) === "active") {
console.log("Changing");
getSectionIDs("repeating_shells", idArray => {
idArray = idArray.filter(item => item !== e.sourceAttribute.slice(17, -13))
const setting = {}
idArray.forEach(id => setting[`repeating_shells_${id}_shell_active`] = "0");
setAttrs(setting, () => {
calculateShellAttrs();
})
})
} else {
console.log("No change needed");
calculateShellAttrs();
}
}
@@ -96,6 +96,8 @@ input(type="hidden", name="attr_setting_super_type", value="psionics").super-typ
input(type="hidden", name="attr_setting_foci_location", value="left").foci-loc
input(type="hidden", name="attr_setting_skill_query", value="hover").skills-query
input(type="hidden", name="attr_magic_type", value="arcanist").magic-type
input(type="hidden", name="attr_setting_ai_enable", value="ai").ai-setting
input(type="hidden", name="attr_setting_transhuman_enable", value="transhuman").transhuman-setting
//- Core
.core.core-info.cols-2
@@ -165,10 +167,16 @@ input(type="hidden", name="attr_magic_type", value="arcanist").magic-type
tr
td(data-i18n=attr.toUpperCase(), data-i18n-title=`${attr.toUpperCase()}_DESC`)
td.edit
input(type="number", name=`attr_${attr}_base`, value="10")
if ["strength", "dexterity", "constitution"].includes(attr)
input(type="number", name=`attr_${attr}_base`, value="10").ai-disabled.transhuman-disabled
else
input(type="number", name=`attr_${attr}_base`, value="10")
//span(name=`attr_${attr}`).display
td.edit
input(type="number", name=`attr_${attr}_boosts`, value="0")
if ["strength", "dexterity", "constitution"].includes(attr)
input(type="number", name=`attr_${attr}_boosts`, value="0").ai-disabled.transhuman-disabled
else
input(type="number", name=`attr_${attr}_boosts`, value="0")
//span(name=`attr_${attr}_boosts`).display
td
//input(type="number", name=`attr_${attr}_boosts`, value="0").edit
@@ -317,10 +325,58 @@ input(type="hidden", name="attr_magic_type", value="arcanist").magic-type
span(data-i18n="AMMO")
+text-input("weapon_encumbrance", "ENCUMBRANCE_SHORT")(type="number", value="0").enc.center
//- Shells
.section.ai-enabled.transhuman-enabled
+sectionheader("show_shells", "SHELLS")
.shells
.grey-box
.flex-center
h3(data-i18n="SHELLS")
label.details-button
input(type="checkbox", name="attr_show_shell_details", value="1", checked)
span(data-i18n="EXPAND")
input(type="hidden", name="attr_show_shell_details", value="1").input-details
.table-header.shells-grid.rep-grid.notdetails
.active(data-i18n="ACTIVE?").notdetails
.str(data-i18n="STRENGTH_SHORT").notdetails
.dex(data-i18n="DEXTERITY_SHORT").notdetails
.con(data-i18n="CONSTITUTION_SHORT").notdetails
.affinity(data-i18n="AFFINITY?").notdetails
.name(data-i18n="NAME").notdetails
fieldset.repeating_shells
button.display.invisible-button(name="roll_Shell", type="roll", value!="&{template:swn-default} {{charname=@{character_name}}} {{info=^{SHELL} — STR @{shell_strength} - DEX @{shell_dexterity} - CON @{shell_constitution}}} {{title=@{shell_name}}} {{desc=^{BASIC_ABILITIES}:\n @{shell_ability} @{shell_affinity_active}}}")
.table-header.shells-grid.rep-grid.details.no-highlight
.active(data-i18n="ACTIVE?")
.str(data-i18n="STRENGTH_SHORT")
.dex(data-i18n="DEXTERITY_SHORT")
.con(data-i18n="CONSTITUTION_SHORT")
.affinity(data-i18n="AFFINITY?")
.name(data-i18n="NAME")
.shells-grid.rep-grid
+fake-text-input().center
input(type="checkbox", name="attr_shell_active", value="1").inline-input.rep-radio
+text-input("shell_strength")(type="number", value="0").center.inline-input
+text-input("shell_dexterity")(type="number", value="0").center.inline-input
+text-input("shell_constitution")(type="number", value="0").center.inline-input
+fake-text-input().center
input(type="checkbox", name="attr_shell_affinity_active" value="\n\n^{AFFINITY_ABILITIES}:\n @{shell_affinity}").inline-input
+text-input("shell_name").inline-input.edit
+text-input("shell_name").display
input(type="hidden", name="attr_shell_ability", value="").hide-empty
.rep-grid.details
.table-header
.shell-ability(data-i18n="BASIC_ABILITIES")
span(name="attr_shell_ability").display
textarea(name="attr_shell_ability").edit
.rep-grid.details
.table-header
.shell-affinity(data-i18n="AFFINITY_ABILITIES")
span(name="attr_shell_affinity").display
textarea(name="attr_shell_affinity").edit
//- Foci
.section.foci-left
+foci
//- Psionics and/or Space Magic
.section.super
+sectionheader("show_psionics", false)
span(data-i18n="PSYCHIC_SKILLS_TECHNIQUES").super-psionics-only
@@ -534,6 +590,7 @@ input(type="hidden", name="attr_magic_type", value="arcanist").magic-type
input.damage(type="text", name="attr_spell_damage", value="0")
textarea.details(name="attr_spell_description")
.column
//- Skills
.section
@@ -569,6 +626,48 @@ input(type="hidden", name="attr_magic_type", value="arcanist").magic-type
.section.foci-right
+foci
//- AI
.section.cols-2.ai-enabled
+sectionheader("show_ai", "AI")
.ai.halfgrid
.column
.grey-box.ai-processing
h3(data-i18n="COMMITTED_PROCESSING")
+text-input("ai_extra_processing", "BONUS")(type="number", value="0").center
+text-input("ai_committed_processing_current", "CURRENT")(type="number", value="0", min="0").center
+text-input("ai_committed_processing_scene", "SCENE")(type="number", value="0", min="0").center
+text-input("ai_committed_processing_day", "DAY")(type="number", value="0", min="0").center
+text-input("ai_total_processing", "UNCOMMITTED")(type="number", value="1", readonly, data-i18n-title="MAXIMUM_EFFORT_DESC").center
.grey-box.ai-processing-nodes
h3(data-i18n="PROCESSING_NODES") Processing Nodes
.table-header.nodes-grid.rep-grid
.connected(data-i18n="CONNECTED?")
.value(data-i18n="PROCESSING")
.location(data-i18n="LOCATION")
fieldset.repeating_processing-nodes
.nodes-grid.rep-grid
+fake-text-input().center
input(type="checkbox" value="1" name="attr_node_connected")
+text-input("node_value")(type="number" value="0")
+text-input("node_location")
.column
.routines.grey-box.simple-repsection
.flex-center
h3(data-i18n="AI_ROUTINES") AI Routines
label.details-button
input(type="checkbox", name="attr_show_routines_details", value="1", checked)
span(data-i18n="EXPAND")
input(type="hidden", name="attr_show_routines_details", value="1").input-details
fieldset.repeating_routines
button.display.invisible-button(name="roll_Routine", type="roll", value!="&{template:swn-default} {{charname=@{character_name}}} {{info=^{AI_ROUTINE}}} {{title=@{routine_name}}} {{desc=@{routine_description}}}")
.display
.name.flex
span(name="attr_routine_name")
.details #[span(name="attr_routine_description")]
.edit.flex-wrap
input.name.grow(type="text", name="attr_routine_name", data-i18n-placeholder="ROUTINE")
textarea.details(name="attr_routine_description", data-i18n-placeholder="DESCRIPTION")
//- Gear
.section.cols-2
+sectionheader("show_gear", "GEAR")
@@ -672,7 +771,7 @@ input(type="hidden", name="attr_magic_type", value="arcanist").magic-type
.amount #
.name(data-i18n="NAME")
.details(data-i18n="DESCRIPTION")
.enc(data-i18n="ENCUMBRANCE_SHORT")
.enc(data-i18n="ENCUMBRANCE_SHORT" data-i18n-title="ENCUMBRANCE")
.status(data-i18n="STATUS")
.bundled(data-i18n="BUNDLED")
Bundled
@@ -695,7 +794,7 @@ input(type="hidden", name="attr_magic_type", value="arcanist").magic-type
+text-input("gear_name")(data-i18n-placeholder="ITEM")
+fake-text-input()
textarea(name="attr_gear_description")
+text-input("gear_encumbrance")(type="number", value="0", step="any", "data-i18n-title"="ENCUMBRANCE").center
+text-input("gear_encumbrance")(type="number", value="0", step="any").center
+fake-text-input()
select.status(name="attr_gear_status")
option(value="", data-i18n="OTHER", selected)
@@ -74,11 +74,14 @@ input(type="hidden", name="attr_show_settings" value="0").hider
span(data-i18n="XP_SCHEME_DESC")
p
label
span(data-i18n="SKILL_ATTRIBUTE_CHOICE").colon-after
select(name="attr_setting_skill_query")
option(value="hover", data-i18n="CHOOSE_ON_HOVER", selected)
option(value="query", data-i18n="QUERY")
option(value="hide", data-i18n="EDIT_MODE_ONLY")
span(data-i18n="AI_CHARACTER").colon-after AI Character
input(type="checkbox", name="attr_setting_ai_enable", value="ai")
span(data-i18n="AI_CHARACTER_DESC") Play as an AI, with mechanical Shells and advanced Processing and Routines. Rules for this are in the Deluxe Edition.
p
label
span(data-i18n="TRANSHUMAN_CHARACTERS").colon-after Transhuman Characters
input(type="checkbox", name="attr_setting_transhuman_enable", value="transhuman")
span(data-i18n="TRANSHUMAN_CHARACTERS_DESC") Run a Transhuman campaign with body-swapping. Rules for this are in the Deluxe edition.
div
h2(data-i18n="GENERAL SETTINGS")
p
@@ -97,6 +100,13 @@ input(type="hidden", name="attr_show_settings" value="0").hider
span(data-i18n="QUERY_FOR_MODIFIERS").colon-after
input(type="checkbox", name="attr_setting_modifier_query", value="1", checked)
span(data-i18n="QUERY_FOR_MODIFIERS_DESC")
p
label
span(data-i18n="SKILL_ATTRIBUTE_CHOICE").colon-after
select(name="attr_setting_skill_query")
option(value="hover", data-i18n="CHOOSE_ON_HOVER", selected)
option(value="query", data-i18n="QUERY")
option(value="hide", data-i18n="EDIT_MODE_ONLY")
p
label
span(data-i18n="FINE_NPC_WHISPERING").colon-after
@@ -25,10 +25,11 @@ input(type="hidden", name="attr_ship_vehicle_type", value="SHIP").input-type
+text-input("ship_size", "SIZE").colsixth.vehicle-type.vehicle-type-custom
+text-input("ship_cargo_tonnage", "CARGO").colsixth.vehicle-type.vehicle-type-ship
+text-input("ship_ground_speed", "GROUND_SPEED").col25.vehicle-type.vehicle-type-custom
+text-input("ship_home_port", "HOME_PORT").col25.vehicle-type.vehicle-type-ship
+text-input("ship_operating_cost", "OPERATING_COST").col25.grow
+text-input("ship_maintenance_cost", "MAINTENANCE COST").col25.grow
+text-input("ship_last_maintenance", "LAST_MAINTENANCE").col25.grow
+text-input("ship_home_port", "HOME_PORT").col20.vehicle-type.vehicle-type-ship
+text-input("ship_price", "PRICE").col20.grow
+text-input("ship_operating_cost", "OPERATING_COST").col20.grow
+text-input("ship_maintenance_cost", "MAINTENANCE COST").col20.grow
+text-input("ship_last_maintenance", "LAST_MAINTENANCE").col20.grow
.data.core-box-text
+text-input("ship_speed", "SPEED")(type="number", min="0")
+text-input("ship_ac", "AC")(type="number", min="0")
@@ -77,6 +78,7 @@ input(type="hidden", name="attr_ship_vehicle_type", value="SHIP").input-type
span(data-i18n="ADD")
fieldset.repeating_ship-weapons
input(type="hidden", name="attr_weapon_api", value="")
input(type="hidden", name="attr_weapon_price")
button(type="roll", name="roll_Attack", value!="&{template:swn-attack} {{charname=@{ship_name}}} {{title=@{weapon_name} – @{weapon_qualities}}} {{attack=[[1d20 + @{weapon_attack_bonus}[Attack Bonus] @{modifier_query}]]}} {{damage=[[@{weapon_damage}]]}} @{weapon_use_ammo} @{weapon_api}").display.invisible-button
.display.grey-box-bordered
input(type="hidden", name="attr_weapon_broken", value="0").hider
@@ -179,6 +181,7 @@ input(type="hidden", name="attr_ship_vehicle_type", value="SHIP").input-type
input(type="checkbox", name="attr_generate_ship-fittings_button")
span(data-i18n="ADD")
fieldset.repeating_ship-fittings
input(type="hidden", name="attr_fitting_price")
.display
input(type="hidden", name="attr_fitting_broken", value="0").hider
span.broken.space-after #[span(data-i18n="BROKEN")]:
@@ -220,6 +223,7 @@ input(type="hidden", name="attr_ship_vehicle_type", value="SHIP").input-type
input(type="checkbox", name="attr_generate_ship-defenses_button")
span(data-i18n="ADD")
fieldset.repeating_ship-defenses
input(type="hidden", name="attr_defense_price")
.display
input(type="hidden", name="attr_defense_broken", value="0").hider
span.broken.space-after #[span(data-i18n="BROKEN")]:
File diff suppressed because it is too large Load Diff