diff --git a/RolemasterUnified_Official/rolemasterunified.css b/RolemasterUnified_Official/rolemasterunified.css index 7bbdc3adcbcb..006840ad0b4b 100644 --- a/RolemasterUnified_Official/rolemasterunified.css +++ b/RolemasterUnified_Official/rolemasterunified.css @@ -42,6 +42,11 @@ h3 { font-family: 'PragRoman', 'myPragRoman', 'IM Fell DW Pica', 'Kaushan Script', 'Chalkduster', 'Trattatello', 'Luminari', fantasy, serif; } +.sheet-hangingindent, .hangingindent { + padding-left: 1em; + text-indent: -1em; +} + .creaturesheet { display: none; @@ -710,7 +715,6 @@ div.repcontrol { .cm_info { border: 3px solid rgb(20,21,22); border-radius: 255px 15px 225px 15px/15px 225px 15px 255px; - height: 80%; } @@ -802,7 +806,7 @@ button.nodie { { background-color: transparent; border: none !important; - padding: none; + padding: 0; font-weight: inherit; cursor: help; font-size: 1em; @@ -1080,6 +1084,11 @@ button.nodie { background-image: linear-gradient(to right, #4776e6, #8e54e9); } +.creaturemove { + padding: 0.5em; border: 1px solid rgb(22,11,12); border-radius: 0.3em; margin: 1em; box-shadow: 0 5px 20px 0 rgba(0,0,0,0.25), 0 5px 10px 0 rgba(0,0,0,0.17);; + background-image: linear-gradient(to right, #ff512f, #f09819); +} + .creature-multicolumn { column-count: auto; column-width: 18em; diff --git a/RolemasterUnified_Official/rolemasterunified.html b/RolemasterUnified_Official/rolemasterunified.html index 725ead55c902..583e098b6a2b 100644 --- a/RolemasterUnified_Official/rolemasterunified.html +++ b/RolemasterUnified_Official/rolemasterunified.html @@ -63,23 +63,33 @@ computed::injurystring is the injury enced string to send accross. + + + {{#^rollTotal() manueverpenalty 0}} + {{manueverpenalty}} [Manuever Penalty] + {{/^rollTotal() manueverpenalty 0}} + -->
{{name}} attacks {{target}} with a {{weapon}}
- Rolls {{attackrollum}} + OB{{attackbonus}} - DB {{targetdb}} ⇒ {{computed::attackroll}} - {{computed::attackresult}}!
+ + {{computed::attackresult}}! +

+ {{computed::attackroll}} = + {{attackrollum}} [Roll] {{computed::attackmoddescription}}
+

{{#rollTotal() computed::ishit 1}} - Hits {{computed::hitlocation}} for {{computed::damage}} + Hits {{computed::hitlocation}} for {{computed::damage}} {{#rollTotal() computed::hascritical 1}}
Critical Roll {{computed::critical}}
{{computed::critdescription}}
{{computed::criteffect}} {{/rollTotal() computed::hascritical 1}} {{/rollTotal() computed::ishit 1}} - +
- [Apply](~{{target}}|applyattackresult||{{computed::injurystring}}) + [Apply](~{{computed::targetname}}|applyattackresult||{{computed::injurystring}})
@@ -191,7 +201,8 @@
{{who}} casts {{computed::spell}}
- Modifiers: {{mod}} = {{computed::modlog}}
+

+ Modifiers: {{mod}} = {{computed::modlog}}

Result: {{computed::total}} = {{computed::roll}} + {{mod}}
{{computed::result}}
{{who}} now has {{computed::pp}} PP ({{computed::ppmsg}}) @@ -2120,7 +2131,7 @@

-

+

Creature

@@ -2141,6 +2152,9 @@

Defenses + Level + + AT (DB) @@ -2161,7 +2175,7 @@

Initiative - + @@ -2238,6 +2252,16 @@

+
+ Movement + +
+ + (Max ) + '/rnd +
+ +

Actions

@@ -2404,7 +2428,7 @@

Statistics

ProfessionRaceLevelCultureRealmAgeSkinEyesGenderBuildHeightWeightyWeight allowance CarriedEnc PenaltyMax PaceManuever PenaltyyBase Move (BMR) -InitiativeSizeRecovery MultiplierRoutineEasyLightMediumHardVery HardExt. HardSheer FollyAbsurdNigh Impossible +SizeRecovery MultiplierRoutineEasyLightMediumHardVery HardExt. HardSheer FollyAbsurdNigh Impossible EP @@ -2591,6 +2615,10 @@

Defense

Attacks

+
+ y Initiative + +
@@ -2643,6 +2671,11 @@

Attacks

+
+ + + +
@@ -2651,68 +2684,69 @@

Attacks

Attack Skill
Table
Size
@@ -5114,7 +5147,11 @@

Recalculate


-Revision 312226f26becda9be73d84535e75bcb5c7156fe7 +Sheet Version: + +
+ +Revision 3d329790ea74dce78d71c9d4b89143b32e0ce159
@@ -6135,9 +6172,9 @@

Recalculate

let value = parseIntDefault(obj.value, 0); if (value < 0) { // Negatives print their own sign - log += ` ${value} [${obj.name}]`; + log += ` ${value} [${obj.name}]`; } else { - log += ` +${value} [${obj.name}]`; + log += ` +${value} [${obj.name}]`; } total += value; } else if (obj.hasOwnProperty('message')) { @@ -7408,6 +7445,8 @@

Recalculate

'castingvoice', 'castingpreprounds', 'castingsubtle', 'castinghands', // AP 'castinguseap', 'aptrack_spell', + // Manuever Penalty + 'manuever_penalty', ], (spelldata) => { console.log(spelldata); @@ -7450,10 +7489,10 @@

Recalculate

return; } if (ap == 2) { - mlog += '-50 [Fast Cast 2AP]'; + mlog += '-50 [Fast Cast 2AP]'; mod += -50; } else if (ap == 3) { - mlog += '-25 [Fast Cast 3AP]'; + mlog += '-25 [Fast Cast 3AP]'; mod += -25; } } @@ -7468,17 +7507,29 @@

Recalculate

console.log(subtle); } + { + const manp = parseIntDefault(spelldata.manuever_penalty, 0); + if (manp < 0) { + mlog += ` ${manp} [Manuever Penalty]`; + mod += manp; + } + } + { const miscmod = parseIntDefault(spelldata.castingmiscmod, 0); if (miscmod) { mod += miscmod; - mlog += ` ${miscmod} [Other modifier]`; + if (miscmod > 0) { + mlog += ` +${miscmod} [Other modifier]`; + } else { + mlog += ` ${miscmod} [Other modifier]`; + } } } if (preprounds > 0) { mod += preprounds * 10; - mlog += ` +(${preprounds} * 10) [Prep]`; + mlog += ` +(${preprounds} * 10) [Prep]`; } if (casterlevel < level) { @@ -7486,29 +7537,29 @@

Recalculate

const overpenalty = overcast * 20; if (grace > overpenalty) { - mlog += ` -0 [Overcast ${overcast} levels (${overpenalty} + ${grace} Grace (0 capped))]`; + mlog += ` -0 [Overcast ${overcast} levels (${overpenalty} + ${grace} Grace (0 capped))]`; } else if (grace > 0) { const modified = overpenalty - grace; - mlog += ` -${modified} [Overcast ${overcast} levels ({${overpenalty} + ${grace} Grace)]` + mlog += ` -${modified} [Overcast ${overcast} levels ({${overpenalty} + ${grace} Grace)]` mod -= modified; } else { // FIXME: add grace here - mlog += ` -${overpenalty} (Overcast ${overcast} levels * 20)` + mlog += ` -${overpenalty} (Overcast ${overcast} levels * 20)` mod -= overpenalty; } } if (group == 'spellownbase') { mod += 5; - mlog += ' +5 [Base List]'; + mlog += ' +5 [Base List]'; } else if (group == 'spellopen') { - mlog += ' +0 [Open List]'; + mlog += ' +0 [Open List]'; } else if (group == 'spellclosed') { mod += -5; - mlog += ' -5 [Closed List]'; + mlog += ' -5 [Closed List]'; } else { mod += -10; - mlog += ' -10 [Other List]'; + mlog += ' -10 [Other List]'; } const scrbonus = miscBonusTotalFlat(spelldata.scr_misc); @@ -9736,6 +9787,17 @@

Recalculate

addPendingFunction("CMFinish: update all skills", RMUSkills.updateAllSkills); addPendingFunction("CMFinish: Front page", updateFrontPage); + + addPendingFunction("CMFinish: Set PP and HP to some initial values", () => { + getAttrsPending(['pp', 'hp_max', 'pp_max'], (v) => { + const update = {} + update.pp = v.pp_max || 0; + update.hp = v.hp_max || 0; + update.injury_penalty = 0; + update.fatigue = 0; + VrmuSetAttrs(update); + }); + }); addPendingFunction("CMFinish: Set character created flag", () => { setAttrsPending({charactercreated: 'true', flag_cmancer_show: '0'}); }); @@ -11589,6 +11651,10 @@

Recalculate

// - cdata.attackersize - Size of the attacker // - cdata.targetsize = values.target_size || 0; // - cdata.targetcrit = values.target_crit || ''; + // - cdata.attackmod = The numeric attack modifer - all mods + // - cdata.attackmoddescription = Description of the above + // - cdata.manuever_penalty = values.manuever_penalty || 0; + // // Results are: // - attackresult -> "Hit/Miss/Fumble" @@ -11602,9 +11668,14 @@

Recalculate

attacks.resolveAttack = function(rollId, rolls, cdata) { let result = {}; let injuryhits = 0; + let moddescription = ''; // Nil out the defined fields result.attackresult = "??"; result.finalroll = "??"; + + result.attackmoddescription = cdata.attackmoddescription; + result.attackmod = cdata.attackmod; + // First check for a fumble if (rolls.attackrollum.result <= cdata.fumblerange) { result.attackresult = "Fumble"; @@ -11612,8 +11683,10 @@

Recalculate

finishRoll(rollId, result); return; } + result.finalroll = rolls.attackroll.result; + // FIXME: Expand this roll into the log // Now did we hit or miss? if (result.finalroll < cdata.min) { @@ -11713,6 +11786,10 @@

Recalculate

result.hitside = hit.side result.critical = critroll; + // The 'computed' version of the target is html entity encoded + // FIXME: Entity should be all; not just ) + result.targetname = cdata.targetname.replace(")", ')'); + console.log("rep", cdata.targetname, result.targetescaped); // Get critical if (critinfo.critical) { @@ -11729,6 +11806,7 @@

Recalculate

}); return; } + result.injurystring = injury.updateInjuryString(injuryhits, {}); finishRoll(rollId, result); } @@ -11766,22 +11844,66 @@

Recalculate

} attacks.startAttack = function(cdata) { + let attackmoddescription = ''; + let attackmod = 0; + + attackmod += cdata.attackbonus; + attackmoddescription = ` +${cdata.attackbonus} [Attack Bonus]`; + + if (cdata.manuever_penalty < 0) { + attackmod += cdata.manuever_penalty; + attackmoddescription += ` ${cdata.manuever_penalty} [Manuever Penalty]`; + } + if (cdata.targetsize < cdata.attackersize) { - cdata.sizedbmod = 5 * (cdata.attackersize - cdata.targetsize); - cdata.targetdb += cdata.sizedbmod; + const dbdiff = (5 * (cdata.attackersize - cdata.targetsize)); + attackmod -= dbdiff; + attackmoddescription += + ` -${dbdiff} [Size Difference (${cdata.attackersize} vs ${cdataltargetsize})]`; + } + + attackmod -= cdata.targetdb; + if (cdata.targetdb < 0) { + attackmoddescription += ` -(${cdata.targetdb}) [Target DB]`; + } else { + attackmoddescription += ` -${cdata.targetdb} [Target DB]`; + } + + if (cdata.useap) { + if (cdata.apused < 2) { + sendMessage("Must use at least 2 AP to attack"); + return; + } + + // FIXME: Missile. + let appenalty = 0; + if (cdata.apused == 2) { + appenalty = 50; + } else if (cdata.apused == 3) { + appenalty = 25; + } + if (appenalty != 0) { + attackmoddescription += ` -${appenalty} [Fast Attack (${cdata.apused} AP)]`; + attackmod -= appenalty; + } } + + cdata.attackmod = attackmod; + cdata.attackmoddescription = attackmoddescription; + const roll_string = "&{template:rmuattack} " + - `[[ [[ 1d100!>96 ]] + [[${cdata.attackbonus}]] - ([[${cdata.targetdb}]]) ]] ` + - "[[ 1d100 ]] " + + `[[ [[ 1d100!>96 ]] + ${attackmod} ]] ` + + "[[ 1d100 ]] " + // crit roll `{\{name=${cdata.character}}} ` + `{\{target=${cdata.targetname}}} ` + `{\{weapon=${cdata.attackname}}} ` + + `{\{manueverpenalty=${cdata.manuever_penalty}}} ` + + `{\{attackmoddescription=[[0]]}} ` + + `{\{targetname=[[0]]}} ` + "{\{hitlocation=[[1d7]] }} " + "{\{attackrollum=$[[0]]}} " + - "{\{attackbonus=$[[1]]}} " + - "{\{targetdb=$[[2]]}} " + - "{\{attackroll=$[[3]]}} " + - "{\{critical=$[[4]]}} " + + "{\{attackroll=$[[1]]}} " + + "{\{critical=$[[2]]}} " + "{\{hascritical=[[0]]}} " + "{\{critdescription=[[0]]}} " + "{\{criteffect=[[0]]}} " + @@ -11793,7 +11915,7 @@

Recalculate

console.log(cdata); startRoll(roll_string, roll => { attacks.resolveAttack(roll.rollId, roll.results, cdata); - }); + }); } onCheck('clicked:repeating_attack:rmuattackroll', (ev) => { @@ -11804,8 +11926,10 @@

Recalculate

`${basename}_attackname`, `${basename}_attackfumble`, 'size', `${basename}_attacksize`, + 'manuever_penalty', 'target_name', 'target_db', 'target_at', 'target_size', 'target_crit', + 'attackuseap', 'aptrack_melee', 'aptrack_missile', ], values => { console.log('New attack: get attrs', values, ev); cdata = {} @@ -11824,6 +11948,10 @@

Recalculate

cdata.targetdb = parseIntDefault(values.target_db, 0); cdata.targetsize = parseIntDefault(values.target_size, 5); cdata.targetcrit = parseIntDefault(values.target_crit, 0); + cdata.manuever_penalty = parseIntDefault(values.manuever_penalty, 0); + cdata.useap = (values.attackuseap == 'on') ? true : false; + // FIXME: Missile is wrong + cdata.apused = parseIntDefault(values.aptrack_melee, 0); attacks.startAttack(cdata); }); }); @@ -11982,6 +12110,7 @@

Recalculate

updates[`${prefix}_attacksource`] = 'user'; updates[`${prefix}_attackskill`] = items.attackaddskill; updates[`${prefix}_attackri`] = items.attackaddri; + updates[`${prefix}_attackmodifier`] = parseIntDefault(items.attackaddmodifier, 0); updates[`${prefix}_attackmin`] = cdata?.data?.min?.match(/\d+/)[0] || 0; updates[`${prefix}_attackmax`] = cdata?.data?.max?.match(/\d+/)[0] || 0; @@ -12010,19 +12139,26 @@

Recalculate

// Now I need to get the skill bonuses const skillSet = new Set(); ids.forEach((id) => { - skillSet.add(attackinfo[`repeating_attack_${id}_attackskill`] + '_bonus'); - skillSet.add(attackinfo[`repeating_attack_${id}_attackskill`] + '_ranks'); + const name = attackinfo[`repeating_attack_${id}_attackskill`]; + if (name != 'none') { + skillSet.add(attackinfo[`repeating_attack_${id}_attackskill`] + '_bonus'); + skillSet.add(attackinfo[`repeating_attack_${id}_attackskill`] + '_ranks'); + } }); getAttrs(Array.from(skillSet), (skills) => { console.log(skills); ids.forEach((id) => { let prefix = `repeating_attack_${id}_`; - let skill = attackinfo[`repeating_attack_${id}_attackskill`]; let modifier = parseIntDefault(attackinfo[`repeating_attack_${id}_attackmodifier`], 0); - let skillbonus = parseIntDefault(skills[skill + '_bonus'], 0); - let skillranks = parseIntDefault(skills[skill + '_ranks'], 0); + let skill = attackinfo[`repeating_attack_${id}_attackskill`]; + let skillbonus = 0; + let skillranks = 0; + if (skill != 'none') { + skillbonus = parseIntDefault(skills[skill + '_bonus'], 0); + skillranks = parseIntDefault(skills[skill + '_ranks'], 0); + } let fumble = parseIntDefault(attackinfo[`repeating_attack_${id}_attackbasefumble`], 0); @@ -12371,6 +12507,15 @@

Recalculate

}); }); +onCheck("clicked:rollinitative", () => { + startRoll('&{template:rmuinit} [[ [[ 2d10 ]] + @{initiative} &{tracker} ]] ' + + '{\{actor=@{character_name} }} ' + + '{]{bonus=@{initiative} }} {\{total=$[[1]]}} {\{roll=$[[0]]}}', + (roll) => { + finishRoll(roll.rollId, {}); + }); +}); + @@ -12496,6 +12641,7 @@

Recalculate

// FIXME: Kill this after my next upload. updates.critreduction = cdata.critreduction || cdata.critreduciton; + updates.level = parseInt(cdata.level); updates.hits = parseInt(cdata.hits); updates.hp = updates.hits; updates.hp_max = updates.hp; @@ -12508,7 +12654,7 @@

Recalculate

// Status stuff updates.hppenalty = 0; // Nothing by default updates.injurypenalty = 0; - updates.endurance = 99; + updates.endurance = cdata.Fatigue; updates.fatigue = 0; updates.manuever_penalty = 0; @@ -12559,6 +12705,18 @@

Recalculate

updates[`${prefix}_type`] = type; } + let moves = JSON.parse(cdata['data-move']) + for (const { type, pace, bmr } of moves) { + //for (const move of moves) { + console.log("mocve", type, pace, bmr); + const rowid = generateRowID(); + const prefix = `repeating_creaturemove_${rowid}`; + updates[`${prefix}_type`] = type; + updates[`${prefix}_pace`] = pace; + updates[`${prefix}_bmr`] = bmr; + } + // type, pace, bmr + VrmuSetAttrs(updates); /* console.log({ @@ -12631,10 +12789,25 @@

Recalculate

const injury = {} -// Grabs all the known fields and crreate an injury string. +// Maps the critical table to the critical encoding. +// Hits are not covered, nor is the description field +injury.criticalmaps = { + P : 'P' /* Ijury penalty */, + F: 'F' /* Fatigue */, + B: 'B' /* Bleed */, + St: 'S' /* staggered */, + Prn: 'P' /* Prone */, + K: 'K' /* Knock back */, + G: 'G' /* Grapple % */, + "S-25" : 'Q', "S-50" : 'W', "S-75" : 'E', /* Stuns */ + Br: 'X' /* Breakage roll */, + D: 'D' /* Death/defeat */, +}; + +// Grabs all the known fields and create an injury string. // returns it as a string. // FIXME: Should take location and side -injury.updateInjuryString = function(hits, critical) { +injury.updateInjuryString = function(hits, critical, attacker) { let istr = ""; console.log("Update Injury string", hits, critical); // First get base hits + critical hits @@ -12650,15 +12823,25 @@

Recalculate

return istr; } - if (critical.P) { - istr += `P${Math.abs(critical.P)}`; - } - if (critical.F) { - istr += `F${Math.abs(critical.F)}`; + for (const [critfield, value] of Object.entries(critical)) { + const istrfield = injury.criticalmaps[critfield]; + if (typeof(istrfield) !== 'string') { + rmuerror("Unknown critical type", critfield, value, critical); + } else { + istr += `${istrfield}${Math.abs(value)}`; + } } + if (attacker && typeof(attacker) === 'string') { + const asan = attacker.replaceAll('@', '').replaceAll(")", ')'); + istr += `@${asan}@`; + } - // istr += '"AttackResult"'; + if (critical.description) { + // kill double quotes + const desc = critical.description.replaceAll('"', '').replaceAll(")", ')'); + istr += `"${desc}"`; + } console.log(istr); return istr; @@ -12729,7 +12912,7 @@

Recalculate

setAttrs({"flag_cmancer_show": "on"}) }); - getAttrs(['sheetselect'], (sheet) => { + getAttrs(['sheetselect', 'version'], (sheet) => { if (!sheet.sheetselect) { sheet.sheetselect = 'playersheet'; setAttrs(sheet); @@ -12737,9 +12920,57 @@

Recalculate

if (sheet.sheetselect == 'playersheet') { RMUSkills.updateStatCache(); } + + doVersionCheck(sheet.version || 1); }); + }); +function doVersionCheck(version) { + const oldversion = version; + if (version == 1) { + addPendingFunction("Upgrade from 1 to 2", upgradeV1to2); + version = 2; + } + + if (oldversion != version) { + addPendingFunction(`Version update to ${version}`, () => { + setAttrsPending({version: version}); + }); + } +} + +/** + * Version 1 has costs in charactermancer. Version 2 moves to each field. + */ +function upgradeV1to2() { + // get profeession. + getAttrsPending(['profession'], ({profession}) => { + console.log('profession', profession); + getCompendiumPage(`Profession:${profession}`, (pdata) => { + const skills = JSON.parse(pdata.data['data-skillCost']); + const updates = {}; + for (const [group, cost] of Object.entries(skills)) { + updates[RMUSkills.getCatByDisplayName(group)?.aname + "_cost"] = cost; + } + { + const mcosts = JSON.parse(pdata.data['data-Spellcasting']); + updates.magicalritual_cost = mcosts['Magic Ritual']; + updates.spellownbase_cost = mcosts['Base']; + updates.spellopen_cost = mcosts['Open']; + updates.spellclosed_cost = mcosts['Closed']; + updates.spellrestricted_cost = mcosts['Restricted']; + updates.spellarcane_cost = mcosts['Arcane']; + } + updates.costdatasaved = true; + console.log(updates); + setAttrsPending(updates); + }); + + }); +} + + // vim: set sw=2 sts=2 syn=javascript : diff --git a/RolemasterUnified_Official/sheet.json b/RolemasterUnified_Official/sheet.json index 497cefcf0b48..3720d06d1da2 100644 --- a/RolemasterUnified_Official/sheet.json +++ b/RolemasterUnified_Official/sheet.json @@ -8,5 +8,5 @@ "legacy": false, "printable": true, "compendium": "RMU", - "version": "1724166334" + "version": "1724738649" } diff --git a/RolemasterUnified_Official/updates.md b/RolemasterUnified_Official/updates.md index 7dbb45a3203b..2e5c0a884c83 100644 --- a/RolemasterUnified_Official/updates.md +++ b/RolemasterUnified_Official/updates.md @@ -1,10 +1,31 @@ +# 2024-8-27 + +- Add movement for creatures +- Set default values for hp, pp and injuries and the like. +- Sort skill and table names in attack add +- Add option for no attack skill +- Spells show +/- +- Initiative is an action; not a roll. No d20 logo. +- Attacks + - include manuever penalty + - add AP tracking (melee only) + - Expand modifiers clearly +- Spells now include manuever penalty +- Bugs + - Fix missing attack modifiers + - Handle applying damage to creatures with ')' in their name +- Add internal sheet versions. Unfortuantely +- Display sheet version on last page +- Automatically upgrade sheet 1 -> 2 + - Sheet 2 is skill costs on sheet (Character support) + # 2024-8-20 - Characters now save their skill costs on creation - Enables use of custom professions - Helps with Roll20 Characters (create characters outside of game) - Support: Add "charactermancer" button to the settings page. - - Will break your harcater. You hae been warned + - Will break your character. You hae been warned - Allows migration to new skill costs - Bugfix: Highest stat gets confused if the first is highest, and the second is not the secondhighest. Add tests for these cases.