From 8c733ba22feb5657c198fa69b3e70ff311956717 Mon Sep 17 00:00:00 2001 From: Ikaguia Date: Wed, 24 Jul 2024 22:19:53 -0300 Subject: [PATCH] Powercasting --- README.md | 6 + languages/en.json | 104 ++++++++++- module.json | 7 +- scripts/{module.js => module.mjs} | 5 + scripts/patch/config.mjs | 151 +++++++++++++--- scripts/patch/dataModels.mjs | 129 ++++++++++++++ scripts/patch/powercasting.mjs | 287 ++++++++++++++++++++++++++++++ scripts/patch/proficiency.mjs | 28 +-- scripts/patch/properties.mjs | 27 +-- 9 files changed, 674 insertions(+), 70 deletions(-) rename scripts/{module.js => module.mjs} (82%) create mode 100644 scripts/patch/dataModels.mjs create mode 100644 scripts/patch/powercasting.mjs diff --git a/README.md b/README.md index f6440acc7..3b15c890d 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,12 @@ Test module for the implementation of the sw5e system as a module for dnd5e. ## Changelog +### [0.13] - 2024-07-24 + +#### Added + +- Powercasting + ### [0.1] - 2024-07-22 #### Added diff --git a/languages/en.json b/languages/en.json index fd4990637..6964c68ad 100644 --- a/languages/en.json +++ b/languages/en.json @@ -689,11 +689,105 @@ "SW5E.Mastery": "Mastery", "SW5E.HighMastery": "High Mastery", "SW5E.GrandMastery": "Grand Mastery", - "SW5E.SchoolDrk": "Dark", - "SW5E.SchoolEnh": "Enhancement", - "SW5E.SchoolLgt": "Light", - "SW5E.SchoolTec": "Tech", - "SW5E.SchoolUni": "Universal", + "SW5E.Powercasting": { + "Label": "Powercasting", + "Force": { + "Label": "Forcecasting", + "Focus": "Focus Generator", + "Known": { + "Value": "Known force powers", + "Max": "Max number of force powers", + "MaxOverride": "Override max number of force powers" + }, + "Level": { + "Label": "Forcecasting Level", + "Override": "Forcecasting Level Override" + }, + "Limit": { + "Label": "Forcecasting Limit", + "Override": "Forcecasting Limit Override" + }, + "MaxPowerLevel": { + "Label": "Max Force Power Level", + "Override": "Max Force Power Level Override" + }, + "Point": { + "Label": "Force Points", + "Value": "Current Force Points", + "Max": "Maximum Force Points", + "MaxOverride": "Maximum Force Points Override" + }, + "Prog": { + "Label": "Forcecasting Progression", + "Full": "Full Forcecaster", + "3/4": "3/4 Forcecaster", + "Half": "Half Forcecaster", + "Arch": "Archetype Forcecaster" + }, + "School": { + "Label": "Forcecasting School", + "Lgt": { + "Label": "Light", + "Dc": "Light Powers DC", + "Attr": "Light Powers Attribute" + }, + "Uni": { + "Label": "Universal", + "Dc": "Universal Powers DC", + "Attr": "Universal Powers Attribute" + }, + "Drk": { + "Label": "Dark", + "Dc": "Dark Powers DC", + "Attr": "Dark Powers Attribute" + } + }, + "Used": "Used Force 'Slots'" + }, + "Tech": { + "Label": "Techcasting", + "Focus": "Wristpad", + "Known": { + "Value": "Known tech powers", + "Max": "Max number of tech powers", + "MaxOverride": "Override max number of tech powers" + }, + "Level": { + "Label": "Techcasting Level", + "Override": "Techcasting Level Override" + }, + "Limit": { + "Label": "Techcasting Limit", + "Override": "Techcasting Limit Override" + }, + "MaxPowerLevel": { + "Label": "Max Tech Power Level", + "Override": "Max Tech Power Level Override" + }, + "Point": { + "Label": "Tech Points", + "Value": "Current Tech Points", + "Max": "Maximum Tech Points", + "MaxOverride": "Maximum Tech Points Override" + }, + "Prog": { + "Label": "Techcasting Progression", + "Full": "Full Techcaster", + "3/4": "3/4 Techcaster", + "Half": "Half Techcaster", + "Arch": "Archetype Techcaster" + }, + "School": { + "Label": "Techcasting School", + "Tec": { + "Label": "Tech", + "Dc": "Tech Powers DC", + "Attr": "Tech Powers Attribute" + } + }, + "Used": "Used Tech 'Slots'" + } + }, "SW5E.ShieldDamImm": "Shield Damage Immunities", "SW5E.ShieldDamRes": "Shield Damage Resistances", "SW5E.ShieldDamVuln": "Shield Damage Vulnerabilities", diff --git a/module.json b/module.json index b1d79de18..0689ca89f 100644 --- a/module.json +++ b/module.json @@ -2,7 +2,7 @@ "id": "sw5e-module-test", "title": "SW5E module test", "description": "Test", - "version": "0.12", + "version": "0.13", "library": "false", "manifestPlusVersion": "1.2.0", "compatibility": { @@ -40,10 +40,7 @@ ] }, "esmodules": [ - "scripts/module.js" - ], - "scripts": [ - "scripts/lib/lib.js" + "scripts/module.mjs" ], "styles": [ "styles/module.css" diff --git a/scripts/module.js b/scripts/module.mjs similarity index 82% rename from scripts/module.js rename to scripts/module.mjs index 825864c7c..d3487c869 100644 --- a/scripts/module.js +++ b/scripts/module.mjs @@ -1,4 +1,6 @@ import { patchConfig } from "./patch/config.mjs"; +import { patchDataModels } from "./patch/dataModels.mjs"; +import { patchPowercasting } from "./patch/powercasting.mjs"; import { patchProficiencyInit, patchProficiencyReady } from "./patch/proficiency.mjs"; import { patchProperties } from "./patch/properties.mjs"; @@ -6,6 +8,9 @@ const strict = true; Hooks.once('init', async function() { patchConfig(CONFIG.DND5E, strict); + patchDataModels(); + + patchPowercasting(); patchProficiencyInit(); patchProperties(); }); diff --git a/scripts/patch/config.mjs b/scripts/patch/config.mjs index 1a2c19afe..0e4dec19b 100644 --- a/scripts/patch/config.mjs +++ b/scripts/patch/config.mjs @@ -1292,33 +1292,136 @@ export function patchConfig(config, strict=true) { } }; // Powercasting - if (strict) config.spellSchools = {}; - config.powerSchoolsForce = { - lgt: { - label: "SW5E.SchoolLgt", - fullKey: "light" - }, - uni: { - label: "SW5E.SchoolUni", - fullKey: "universal" - }, - drk: { - label: "SW5E.SchoolDrk", - fullKey: "dark" - } - }; - preLocalize( "powerSchoolsForce", { key: "label", sort: true } ); - config.powerSchoolsTech = { - tec: { - label: "SW5E.SchoolTec", - fullKey: "tech" + config.spellPreparationModes.powerCasting = { + label: "SW5E.Powercasting.Label", + usesPoints: true + }; + config.powerCasting = { + force: { + label: "SW5E.Powercasting.Force.Label", + img: "systems/dnd5e/icons/power-tiers/{id}.webp", + attr: ["wis", "cha"], + focus: { + label: "SW5E.Powercasting.Force.Focus", + id: "focusgenerator", + property: "bolstering" + }, + progression: { + full: { + label: "SW5E.Powercasting.Force.Prog.Full", + powerPoints: 4, + powerMaxLevel: [0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 9, 9], + powerLimit: 6, + divisor: 1, + powersKnown: [0, 9, 11, 13, 15, 17, 19, 21, 23, 25, 26, 28, 29, 31, 32, 34, 35, 37, 38, 39, 40] + }, + "3/4": { + label: "SW5E.Powercasting.Force.Prog.3/4", + powerPoints: 3, + powerMaxLevel: [0, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 5, 5, 5, 6, 6, 6, 7, 7, 7, 7], + powerLimit: 5, + divisor: 9 / 7, + powersKnown: [0, 7, 9, 11, 13, 15, 17, 18, 19, 21, 22, 24, 25, 26, 28, 29, 30, 32, 33, 34, 35] + }, + half: { + label: "SW5E.Powercasting.Force.Prog.Half", + powerPoints: 2, + powerMaxLevel: [0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5], + powerLimit: 4, + divisor: 9 / 5, + powersKnown: [0, 5, 7, 9, 10, 12, 13, 14, 15, 17, 18, 19, 20, 22, 23, 24, 25, 27, 28, 29, 30] + }, + arch: { + label: "SW5E.Powercasting.Force.Prog.Arch", + powerPoints: 1, + powerMaxLevel: [0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4], + powerLimit: 4, + divisor: 9 / 4, + powersKnown: [0, 0, 0, 4, 6, 7, 8, 10, 11, 12, 13, 14, 15, 17, 18, 19, 20, 22, 23, 24, 25] + } + }, + schools: { + lgt: { + label: "SW5E.Powercasting.Force.School.Lgt.Label", + attr: ["wis"], + fullKey: "light" + }, + uni: { + label: "SW5E.Powercasting.Force.School.Uni.Label", + attr: ["wis", "cha"], + fullKey: "universal" + }, + drk: { + label: "SW5E.Powercasting.Force.School.Drk.Label", + attr: ["cha"], + fullKey: "dark" + } + } + }, + tech: { + label: "SW5E.Powercasting.Tech.Label", + img: "systems/sw5e/icons/power-tiers/{id}.webp", + attr: ["int"], + focus: { + label: "SW5E.Powercasting.Tech.Focus", + id: "wristpad", + property: "surging" + }, + progression: { + full: { + label: "SW5E.Powercasting.Tech.Prog.Full", + powerPoints: 2, + powerMaxLevel: [0, 1, 1, 2, 2, 3, 3, 4, 4, 5, 5, 6, 6, 7, 7, 8, 8, 9, 9, 9, 9], + powerLimit: 6, + divisor: 1, + powersKnown: [0, 6, 7, 9, 10, 12, 13, 15, 16, 18, 19, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30] + }, + "3/4": { + label: "SW5E.Powercasting.Tech.Prog.3/4", + powerPoints: 1.5, + powerMaxLevel: [0, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 5, 5, 5, 6, 6, 6, 7, 7, 7, 7], + powerLimit: 5, + divisor: 9 / 4, + powersKnown: [0, 0, 0, 7, 8, 9, 11, 12, 13, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26] + }, + half: { + label: "SW5E.Powercasting.Tech.Prog.Half", + powerPoints: 1, + powerMaxLevel: [0, 1, 1, 1, 1, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5, 5], + powerLimit: 4, + divisor: 9 / 5, + powersKnown: [0, 0, 4, 5, 6, 7, 8, 9, 10, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23] + }, + arch: { + label: "SW5E.Powercasting.Tech.Prog.Arch", + powerPoints: 0.5, + powerMaxLevel: [0, 0, 0, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 4, 4, 4, 4], + powerLimit: 4, + divisor: 9 / 4, + powersKnown: [0, 0, 0, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20] + } + }, + schools: { + tec: { + label: "SW5E.Powercasting.Tech.School.Tec.Label", + attr: ["int"], + fullKey: "tech" + } + }, + shortRest: true } - }; - preLocalize( "powerSchoolsTech", { key: "label", sort: true } ); + } + preLocalize( "powercasting", { key: "label", sort: true } ); + preLocalize( "powercasting.force.progression", { key: "label" } ); + preLocalize( "powercasting.tech.progression", { key: "label" } ); + preLocalize( "powercasting.force.schools", { key: "label", sort: true } ); + preLocalize( "powercasting.tech.schools", { key: "label", sort: true } ); + + if (strict) config.spellSchools = {}; config.spellSchools = { ...config.spellSchools, - ...config.powerSchoolsForce, - ...config.powerSchoolsTech + ...config.powerCasting.force.schools, + ...config.powerCasting.tech.schools }; // Weapons if (strict) { diff --git a/scripts/patch/dataModels.mjs b/scripts/patch/dataModels.mjs new file mode 100644 index 000000000..8fa571052 --- /dev/null +++ b/scripts/patch/dataModels.mjs @@ -0,0 +1,129 @@ +const { NumberField, SchemaField, SetField, StringField } = foundry.data.fields; + +/** + * Produce the schema field for a points resource. + * @param {object} [schemaOptions] Options passed to the outer schema. + * @returns {PowerCastingData} + */ +function makePointsResource(schemaOptions = {}) { + const baseLabel = schemaOptions.label; + const schemaObj = { + value: new NumberField({ + nullable: false, + integer: true, + min: 0, + initial: 0, + label: `${baseLabel}Current` + }), + max: new NumberField({ + nullable: true, + integer: true, + min: 0, + initial: null, + label: `${baseLabel}Override` + }), + bonuses: new SchemaField({ + level: new game.dnd5e.dataModels.fields.FormulaField({ deterministic: true, label: `${baseLabel}BonusLevel` }), + overall: new game.dnd5e.dataModels.fields.FormulaField({ deterministic: true, label: `${baseLabel}BonusOverall` }) + }) + }; + if (schemaOptions.hasTemp) schemaObj.temp = new NumberField({ + integer: true, + initial: 0, + min: 0, + label: `${baseLabel}Temp` + }); + if (schemaOptions.hasTempMax) schemaObj.tempmax = new NumberField({ + integer: true, + initial: 0, + label: `${baseLabel}TempMax` + }); + return new SchemaField(schemaObj, schemaOptions); +} +function addProgression(wrapped, ...args) { + const result = wrapped(...args); + result.spellcasting.fields.forceProgression = new StringField({ + required: true, initial: "none", blank: false, label: "SW5E.Powercasting.Force.Prog.Label" + }); + result.spellcasting.fields.techProgression = new StringField({ + required: true, initial: "none", blank: false, label: "SW5E.Powercasting.Tech.Prog.Label" + }); + return result; +} +function addPowercasting(result) { + result.powercasting = new SchemaField({ + force: new SchemaField({ + known: new SchemaField({ + max: new NumberField({ nullable: true, min: 0, initial: null, label: "SW5E.Powercasting.Force.Known.Max.Override" }) + }), + level: new NumberField({ nullable: true, min: 0, initial: null, label: "SW5E.Powercasting.Force.Level.Override" }), + limit: new NumberField({ nullable: true, min: 0, initial: null, label: "SW5E.Powercasting.Force.Limit.Override" }), + maxPowerLevel: new NumberField({ nullable: true, min: 0, initial: null, label: "SW5E.Powercasting.Force.MaxPowerLevel.Override" }), + points: makePointsResource({ label: "SW5E.Powercasting.Force.Point.Label", hasTemp: true }), + schools: new SchemaField({ + lgt: new SchemaField({ + attr: new StringField({ nullable: true, initial: null, label: "SW5E.Powercasting.Force.School.Lgt.Attr.Override" }), + dc: new NumberField({ nullable: true, min: 0, initial: null, label: "SW5E.Powercasting.Force.School.Lgt.Dc.Override" }) + }), + uni: new SchemaField({ + attr: new StringField({ nullable: true, initial: null, label: "SW5E.Powercasting.Force.School.Uni.Attr.Override" }), + dc: new NumberField({ nullable: true, min: 0, initial: null, label: "SW5E.Powercasting.Force.School.Uni.Dc.Override" }) + }), + drk: new SchemaField({ + attr: new StringField({ nullable: true, initial: null, label: "SW5E.Powercasting.Force.School.Drk.Attr.Override" }), + dc: new NumberField({ nullable: true, min: 0, initial: null, label: "SW5E.Powercasting.Force.School.Drk.Dc.Override" }) + }) + }, { label: "SW5E.Powercasting.Force.School.Label" }), + used: new SetField(new NumberField(), { label: "SW5E.Powercasting.Force.Used" }) + }, { label: "SW5E.Powercasting.Force.Label" }), + tech: new SchemaField({ + known: new SchemaField({ + max: new NumberField({ nullable: true, min: 0, initial: null, label: "SW5E.Powercasting.Tech.Known.Max.Override" }) + }), + level: new NumberField({ nullable: true, min: 0, initial: null, label: "SW5E.Powercasting.Tech.Level.Override" }), + limit: new NumberField({ nullable: true, min: 0, initial: null, label: "SW5E.Powercasting.Tech.Limit.Override" }), + maxPowerLevel: new NumberField({ nullable: true, min: 0, initial: null, label: "SW5E.Powercasting.Tech.MaxPowerLevel.Override" }), + points: makePointsResource({ label: "SW5E.Powercasting.Tech.Point.Label", hasTemp: true }), + schools: new SchemaField({ + tec: new SchemaField({ + attr: new StringField({ nullable: true, initial: null, label: "SW5E.Powercasting.Tech.School.Tec.Attr.Override" }), + dc: new NumberField({ nullable: true, min: 0, initial: null, label: "SW5E.Powercasting.Tech.School.Tec.Dc.Override" }) + }) + }, { label: "SW5E.Powercasting.Tech.School.Label" }), + used: new SetField(new NumberField(), { label: "SW5E.Powercasting.Tech.Used" }) + }, { label: "SW5E.Powercasting.Tech.Label" }) + }, { label: "SW5E.Powercasting.Label" }); +} +function changeProficiency(result, type) { + if (type === "creature") { + result.skills.model.fields.value.max = 5; + result.abilities.model.fields.proficient.max = 5; + } else { + if (type !== "weapon") result.proficient.max = 5; + result.proficient.integer = false; + result.proficient.step = 0.5; + } +} + +export function patchDataModels() { + // Powercasting + libWrapper.register('sw5e-module-test', 'dnd5e.dataModels.item.ClassData.defineSchema', addProgression, 'WRAPPER'); + libWrapper.register('sw5e-module-test', 'dnd5e.dataModels.item.SubclassData.defineSchema', addProgression, 'WRAPPER'); + libWrapper.register('sw5e-module-test', 'dnd5e.dataModels.actor.CreatureTemplate.defineSchema', function (wrapped, ...args) { + const result = wrapped(...args); + addPowercasting(result); + changeProficiency(result, "creature"); + return result; + }, 'WRAPPER'); + libWrapper.register('sw5e-module-test', 'dnd5e.dataModels.item.ToolData.defineSchema', function (wrapped, ...args) { + const result = wrapped(...args); + changeProficiency(result, "tool"); + return result; + }, 'WRAPPER'); + libWrapper.register('sw5e-module-test', 'dnd5e.dataModels.item.WeaponData.defineSchema', function (wrapped, ...args) { + const result = wrapped(...args); + changeProficiency(result, "weapon"); + return result; + }, 'WRAPPER'); + +} diff --git a/scripts/patch/powercasting.mjs b/scripts/patch/powercasting.mjs new file mode 100644 index 000000000..785519b24 --- /dev/null +++ b/scripts/patch/powercasting.mjs @@ -0,0 +1,287 @@ +// dataModels file adds: +// - powercasting field to CreatureTemplate +// - spellcasting.force/techProgression to ClassData and SubclassData + +function adjustItemSpellcastingGetter() { + libWrapper.register('sw5e-module-test', 'dnd5e.documents.Item5e.prototype.spellcasting', function (wrapped, ...args) { + const result = wrapped(...args); + + const spellcasting = this.system.spellcasting; + if ( !spellcasting ) return null; + const isSubclass = this.type === "subclass"; + const classSC = isSubclass ? this.class?.system?.spellcasting : spellcasting; + const subclassSC = isSubclass ? spellcasting : this.subclass?.system?.spellcasting; + for (const castType of ["force", "tech"]) { + const prop = castType + "Progression" + delete result[prop]; + const classPC = classSC?.[prop] ?? "none"; + const subclassPC = subclassSC?.[prop] ?? "none"; + if (subclassPC !== "none") result[castType] = subclassPC; + else result[castType] = classPC; + } + + return result; + }, 'WRAPPER'); +} + +function preparePowercasting() { + libWrapper.register('sw5e-module-test', 'dnd5e.documents.Actor5e.prototype._prepareSpellcasting', function (wrapped, ...args) { + if (!this.system.spells) return; + const isNPC = this.type === "npc"; + + // Prepare base progression data + const charProgression = ["force", "tech"].reduce((obj, castType) => { + obj[castType] = { + powersKnownCur: 0, + powersKnownMax: 0, + points: 0, + casterLevel: 0, + maxPowerLevel: 0, + maxClassProg: null, + maxClassLevel: 0, + classes: 0, + attributeOverride: null + }; + return obj; + }, {}); + + for (const [castType, obj] of Object.entries(charProgression)) { + const typeConfig = CONFIG.DND5E.powerCasting[castType]; + if (isNPC) { + const level = this.system.details?.[`power${progression.castType.capitalize()}Level`]; + if (level) { + progression.classes = 1; + progression.points = level * typeConfig.progression.full.powerPoints; + progression.casterLevel = level; + progression.maxClassLevel = level; + progression.maxClassProg = "full"; + } + } else { + // Translate the list of classes into power-casting progression + for (const cls of this.itemTypes?.class ?? []) { + const pc = cls.spellcasting; + + if (!pc || pc.levels < 1) continue; + const progression = pc[castType]; + + if (!(progression in typeConfig.progression) || progression === "none") continue; + if (progression === "half" && castType === "tech" && pc.levels < 2) continue; // Tech half-casters only get techcasting at lvl 2 + + const progConfig = typeConfig.progression[progression]; + + obj.classes++; + obj.powersKnownMax += progConfig.powersKnown[pc.levels]; + obj.points += pc.levels * progConfig.powerPoints; + obj.casterLevel += pc.levels * progConfig.powerMaxLevel[20] / 9; + obj.maxPowerLevel = Math.max(obj.maxPowerLevel, progConfig.powerMaxLevel[20]); + + if (pc.levels > obj.maxClassLevel) { + obj.maxClassLevel = pc.levels; + obj.maxClassProg = progression; + } + } + + // Calculate known powers + for (const pwr of this.itemTypes?.spell ?? []) { + const { preparation, properties, school } = pwr?.system ?? {}; + if (properties?.has("freeLearn")) continue; + if (school in CONFIG.DND5E.powerCasting[castType].schools) obj.powersKnownCur++; + } + } + } + + + // Apply progression data + for (const [castType, obj] of Object.entries(charProgression)) { + const typeConfig = CONFIG.DND5E.powerCasting[castType] ?? {}; + const progConfig = typeConfig.progression[obj.maxClassProg] ?? {}; + + // 'Round Appropriately' + obj.points = Math.round(obj.points); + obj.casterLevel = Math.round(obj.casterLevel); + + // What level is considered 'high level casting' + obj.limit = progConfig.powerLimit ?? 0; + + // What is the maximum power level you can cast + if (obj.classes) { + if (obj.classes === 1) { + obj.maxPowerLevel = progConfig.powerMaxLevel[obj.maxClassLevel]; + } else { + // Don't allow multiclassing to achieve a higher max power level than a 20th level character of any of those classes + obj.maxPowerLevel = Math.min(obj.maxPowerLevel, typeConfig.progression.full[obj.casterLevel]); + } + } + + // Apply the calculated values to the sheet + const target = this.system.powercasting[castType]; + target.known.value = obj.powersKnownCur; + target.known.max ??= obj.powersKnownMax; + target.level ??= obj.casterLevel; + target.limit ??= obj.limit; + target.maxPowerLevel ??= obj.maxPowerLevel; + target.points.max ??= obj.points; + } + + const { simplifyBonus } = dnd5e.utils; + const rollData = this.getRollData(); + + const { abilities, attributes, powercasting } = this.system; + const base = 8 + attributes.prof ?? 0; + const lvl = Number(this.system.details?.level ?? this.system.details.cr ?? 0); + + // TODO: Add rules + // // Simplified forcecasting rule + // if (game.settings.get("sw5e", "simplifiedForcecasting")) { + // CONFIG.DND5E.powerCasting.force.schools.lgt.attr = CONFIG.DND5E.powerCasting.force.schools.uni.attr; + // CONFIG.DND5E.powerCasting.force.schools.drk.attr = CONFIG.DND5E.powerCasting.force.schools.uni.attr; + // } + + // Powercasting DC for Actors and NPCs + const ability = {}; + const bonusAll = simplifyBonus(this.system.bonuses?.power?.dc?.all, rollData); + for (const [castType, typeConfig] of Object.entries(CONFIG.DND5E.powerCasting)) { + for (const [school, schoolConfig] of Object.entries(typeConfig.schools)) { + const bonus = simplifyBonus(this.system.bonuses?.power?.dc?.[school], rollData) + bonusAll; + for (const attr of schoolConfig.attr) { + if (abilities[attr].mod > (ability[school]?.mod ?? -Infinity)) { + ability[school] = { + attr, + mod: abilities[attr].mod + } + } + } + if (ability[school].mod > (ability[castType]?.mod ?? -Infinity)) ability[castType] = ability[school]; + powercasting[castType].schools[school].attr = ability[school]?.attr ?? ""; + powercasting[castType].schools[school].dc = base + (ability[school]?.mod ?? 0) + bonus; + } + } + + // Set Force and tech bonus points for PC Actors + for (const [castType, typeConfig] of Object.entries(CONFIG.DND5E.powerCasting)) { + const cast = this.system.powercasting[castType]; + const castSource = this._source.system.powercasting[castType]; + + if (castSource.points.max !== null) continue; + if (cast.level === 0) continue; + + if (ability[castType]?.mod) cast.points.max += ability[castType].mod; + + const levelBonus = simplifyBonus(cast.points.bonuses.level ?? 0, rollData) * lvl; + const overallBonus = simplifyBonus(cast.points.bonuses.overall ?? 0, rollData); + const focus = this.focuses?.[CONFIG.DND5E.powerCasting[castType].focus.label]; + const focusProperty = CONFIG.DND5E.powerCasting[castType].focus.property; + const focusBonus = focus?.flags?.["sw5e-module-test"]?.properties?.[focusProperty] ?? 0; + + cast.points.max += levelBonus + overallBonus + focusBonus; + } + + return wrapped(...args); + }, 'WRAPPER'); +} + +function makeProgOption(config) { + const option = document.createElement("option"); + option.setAttribute("value", config.value); + if (config.selected) option.setAttribute("selected", null); + const text = document.createTextNode(game.i18n.localize(config.label)); + option.appendChild(text); + return option; +} + +function showPowercastingStats() { + const { simplifyBonus } = dnd5e.utils; + libWrapper.register('sw5e-module-test', 'dnd5e.applications.actor.ActorSheet5eCharacter2.prototype.getData', async function (wrapped, ...args) { + const context = await wrapped(...args); + const msak = simplifyBonus(this.actor.system.bonuses.msak.attack, context.rollData); + const rsak = simplifyBonus(this.actor.system.bonuses.rsak.attack, context.rollData); + for (const castType of ["tech", "force"]) { + const castData = this.actor.system.powercasting[castType]; + if (castData.level === 0) continue; + const sc = castData.schools.tec ?? castData.schools.uni ?? {}; + const ability = this.actor.system.abilities[sc.attr]; + const mod = ability?.mod ?? 0; + const attackBonus = msak === rsak ? msak : 0; + context.spellcasting.push({ + label: game.i18n.localize(`SW5E.Powercasting.${castType.capitalize()}.Label`) + ` (${castData.points.value}/${castData.points.max})`, + ability: { mod: ability?.mod ?? 0, ability: sc.attr ?? "" }, + attack: mod + this.actor.system.attributes.prof + attackBonus, + primary: this.actor.system.attributes.spellcasting === sc.attr, + save: ability?.dc ?? 0 + }); + } + return context; + }, 'WRAPPER'); +} + +function patchItemSheet() { + Hooks.on("renderItemSheet5e", (app, html, data) => { + html.find(`select[name|='system.spellcasting.progression']`).each((idx, el) => { + const root = el.parentNode.parentNode; + for (const castType of ["Tech", "Force"]) { + const selectedValue = app.item.system.spellcasting[`${castType.toLowerCase()}Progression`]; + const div = document.createElement("div"); + div.setAttribute("class", "form-group"); + const label = document.createElement("label"); + const text = document.createTextNode(game.i18n.localize(`SW5E.Powercasting.${castType}.Prog.Label`)); + label.appendChild(text); + div.appendChild(label); + const div2 = document.createElement("div"); + div2.setAttribute("class", "form-fields"); + const select = document.createElement("select"); + select.setAttribute("name", `system.spellcasting.${castType.toLowerCase()}Progression`); + select.appendChild(makeProgOption({ + value: "none", + selected: selectedValue === "none", + label: "DND5E.None" + })); + for (const [key, prog] of Object.entries(CONFIG.DND5E.powerCasting[castType.toLowerCase()].progression)) { + select.appendChild(makeProgOption({ + value: key, + selected: selectedValue === key, + label: prog.label + })); + } + div2.appendChild(select); + div.appendChild(div2); + root.nextElementSibling.insertAdjacentElement("afterend", div); + } + }); + }); +} + +function recoverPowerPoints() { + Hooks.on("dnd5e.shortRest", (actor, config) => { + for (const [castType, castConfig] of Object.entries(CONFIG.DND5E.powerCasting)) { + const points = actor.system.powercasting[castType].points; + if (!castConfig.shortRest) continue; + if (points.value === points.max) continue; + actor.update({ [`system.powercasting.${castType}.points.value`]: points.max }); + } + }); + Hooks.on("dnd5e.longRest", (actor, config) => { + for (const [castType, castConfig] of Object.entries(CONFIG.DND5E.powerCasting)) { + const { value, max } = actor.system.powercasting[castType].points; + if (value === max) continue; + actor.update({ [`system.powercasting.${castType}.points.value`]: max }); + } + }); +} + +function makePowerPointsConsumable() { + Hooks.once("setup", function() { + for (const castType of ["force", "tech"]) { + CONFIG.DND5E.consumableResources.push(`powercasting.${castType}.points.value`); + } + }); +} + + +export function patchPowercasting() { + adjustItemSpellcastingGetter(); + makePowerPointsConsumable(); + patchItemSheet(); + preparePowercasting(); + recoverPowerPoints(); + showPowercastingStats(); +} diff --git a/scripts/patch/proficiency.mjs b/scripts/patch/proficiency.mjs index cc106da1f..57342c4d5 100644 --- a/scripts/patch/proficiency.mjs +++ b/scripts/patch/proficiency.mjs @@ -10,29 +10,10 @@ function adjustProficiencyObject() { }, 'MIXED' ); } -function adjustDataModels() { - libWrapper.register('sw5e-module-test', 'dnd5e.dataModels.actor.CreatureTemplate.defineSchema', function (wrapped, ...args) { - const result = wrapped(...args); - result.skills.model.fields.value.max = 5; - result.abilities.model.fields.proficient.max = 5; - return result; - }, 'MIXED' ); - - libWrapper.register('sw5e-module-test', 'dnd5e.dataModels.item.WeaponData.defineSchema', function (wrapped, ...args) { - const result = wrapped(...args); - result.proficient.integer = false; - result.proficient.step = 0.5; - return result; - }, 'MIXED' ); - - libWrapper.register('sw5e-module-test', 'dnd5e.dataModels.item.ToolData.defineSchema', function (wrapped, ...args) { - const result = wrapped(...args); - result.proficient.max = 5; - result.proficient.integer = false; - result.proficient.step = 0.5; - return result; - }, 'MIXED' ); -} +// dataModels file changes: +// - skills and abilities max proficiency is 5 on CreatureTemplate +// - proficiency can be 0.5 on WeaponData +// - proficiency can be 0.5 and has a max of 5 on ToolData function adjustProficiencyCycleElement() { dnd5e.applications.components.ProficiencyCycleElement.CSS = ` @@ -144,7 +125,6 @@ function adjustProficiencyCycleElement() { export function patchProficiencyInit() { adjustProficiencyObject(); - adjustDataModels(); } export function patchProficiencyReady() { diff --git a/scripts/patch/properties.mjs b/scripts/patch/properties.mjs index fb98217eb..4b8abfbe2 100644 --- a/scripts/patch/properties.mjs +++ b/scripts/patch/properties.mjs @@ -1,15 +1,4 @@ -function patchKeen() { - function useKeen(wrapped, ...args) { - const keen = this.parent?.flags?.["sw5e-module-test"]?.properties?.keen ?? 0; - console.debug("keen", keen); - const result = wrapped(...args); - return result === Infinity ? Math.max(15, 20 - keen) : result; - } - libWrapper.register('sw5e-module-test', 'dnd5e.dataModels.item.WeaponData.prototype._typeCriticalThreshold', useKeen, 'MIXED' ); - libWrapper.register('sw5e-module-test', 'dnd5e.dataModels.item.ConsumableData.prototype._typeCriticalThreshold', useKeen, 'MIXED' ); -} - -export function patchProperties() { +function patchSheet() { Hooks.on("renderItemSheet5e", (app, html, data) => { for (const type of ["equipment", "weapon"]) { const tag = `${type}-properties`; @@ -74,5 +63,19 @@ export function patchProperties() { }); } }); +} + +function patchKeen() { + function useKeen(wrapped, ...args) { + const keen = this.parent?.flags?.["sw5e-module-test"]?.properties?.keen ?? 0; + const result = wrapped(...args); + return result === Infinity ? Math.max(15, 20 - keen) : result; + } + libWrapper.register('sw5e-module-test', 'dnd5e.dataModels.item.WeaponData.prototype._typeCriticalThreshold', useKeen, 'MIXED' ); + libWrapper.register('sw5e-module-test', 'dnd5e.dataModels.item.ConsumableData.prototype._typeCriticalThreshold', useKeen, 'MIXED' ); +} + +export function patchProperties() { + patchSheet(); patchKeen(); }