Skip to content

Commit 12decc8

Browse files
moopedalgnc
andauthored
Improvements to NPC Parser, Items, and general bug fixes. Also adds the user guide. (#133)
* Handle missing backstabDamage. Placeholder for backstabDamage matches the default damage. Closes #117. * Add page numbers to spell item sheets. Closes #116. * Add HD to upper level and zero sheets. Tweak HD style to suit. HD is now a common attribute, defaulting to 1d4 for PCs and 1d6 for NPCs. * Implement rollable hit dice and draggable hit dice macros. Closes #50. * Support ability modifiers between 0 and 3. Closes #119. * Move NPC import to the Actors Directory, and create the actor in the process. Support automatically naming actors (from the statline) or by adding e.g. 'Name: Dave' to a Purple Sorcerer Plain Text character. Closes #120. * Support importing multiple actors from JSON data. Support importing actors directly to a folder. * Support importing multiple zero level or upper level characters as JSON or Plain Text. * Handle importing multiple NPCs. * Rework actor import into a FormApplication to improve layout and flexibility. * Tests for new actor parser output and for multiple actors. * Register sheets only for Player type actors. * Enable Compute AC for import PCs and guess at a sheet class. * Add a value row to Weapons, Equipment (covers Ammunition and Mounts also), and Armor sheets. Closes #96. * Add Weight to Armor, Weapons, and Treasure sheets. Closes #69. * Use non-greedy match to parse NPC names so we never go past the first colon. Closes #16. * Begin refactor to split weapon attack rolls from damage rolls (and potentially crit/fumble rolls). Refactor useStandardDiceRoller setting lookup into the sheet class, storing the result in an options object passed to all the attack related methods. * Separate attack rolls from damage rolls, and both from the presentation. * Handle invalid to hit or damage formulae gracefully. Closes #25. * Remove spurious parenthesis from the attack roll card. * Roll crits and fumbles at the correct time for both attack roll flows. Needs futher refactor on crits and fumbles similar to attack and damage rolls. * Fix standard card path crit and fumble rolls. * Restore Backstab locstring. * Restore the old name of rollWeaponAttack to support legacy macros. Formatting fixes. * Move the check for useStandardDiceRoller to keep weapon roll macros behaving correctly. * Handle special attacks in NPC weapon parser. Anything starting with a number has zero damage, but the damage string is copied into the weapon Summary for reference. Closes #6. * Detect if Roll.validate is present and fallback (for pre 0.7.x support). * Update system.json for v0.21. * Point at the preview branch's manifest with a preview release. * First draft DCC User Guide * Fix system.json * Rename the user guide pack and fix the table of contents. * Expose createActors from the parser module and return the actors. * Tweak actor import UI and amend userguide to match. * Update manifest and download urls. * Bump template version. Co-authored-by: Christian Ovsenik <[email protected]>
1 parent ff68937 commit 12decc8

26 files changed

+1845
-488
lines changed

lang/en.json

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,11 @@
3939
"DCC.ActionRWAK": "Ranged Weapon Attack",
4040
"DCC.ActionSave": "Saving Throw",
4141
"DCC.ActionUtil": "Utility",
42+
"DCC.ActorImport": "Import Actor(s)",
43+
"DCC.ActorImportStatblock": "Statblock(s) or Statline(s)",
44+
"DCC.ActorType": "Actor Type",
45+
"DCC.ActorTypeNPC": "NPC Statline(s)",
46+
"DCC.ActorTypePlayer": "Player Statblock(s)",
4247
"DCC.Add": "Add",
4348
"DCC.AddClassLevelToInitiative": "Add Class Level to Initiative",
4449
"DCC.AdjacentAlignment": "Adjacent",
@@ -59,14 +64,17 @@
5964
"DCC.Attack": "Attack",
6065
"DCC.AttackBonus": "Attack Bonus",
6166
"DCC.AttackPl": "Attacks",
62-
"DCC.AttackRoll": "Attack Roll",
67+
"DCC.AttackRoll": "Attack Roll with {weapon}",
6368
"DCC.AttackRollEmote": "Attacks with their {weaponName} and hits AC {rollHTML} for {damageRollHTML} points of damage!{crit}{fumble}",
69+
"DCC.AttackRollInvalidFormula": "Invalid To Hit formula {formula} with {weapon}",
70+
"DCC.AttackRollInvalidFormulaInline": "<b>Invalid To Hit formula '{formula}'</b>",
6471
"DCC.Attributes": "Attributes",
6572
"DCC.Attuned": "Attuned",
6673
"DCC.Background": "Background",
6774
"DCC.Backstab": "Backstab",
68-
"DCC.BackstabRoll": "Backstab Roll",
75+
"DCC.BackstabRoll": "Backstab Roll with {weapon}",
6976
"DCC.BackstabEmote": "Backstabs with their {weaponName} and hits AC {rollHTML} for {damageRollHTML} points of damage!{crit}{fumble}",
77+
"DCC.BackstabRollInvalidFormula": "Invalid Damage formula {formula} backstabbing with {weapon}",
7078
"DCC.BadValueFormulaWarning": "Bad formula in item value field!",
7179
"DCC.BaseACAbilityConfig": "Base AC Ability",
7280
"DCC.Bio": "Bio",
@@ -102,8 +110,6 @@
102110
"DCC.ClassSkillsChosen": "Chosen Class Skills",
103111
"DCC.ClassSkillsNumber": "Number of Starting Skills",
104112
"DCC.Clear": "Clear",
105-
"DCC.ClearSheet": "Clear all data from this sheet?",
106-
"DCC.ClearSheetExplain": "This will remove all numbers and text on this sheet.",
107113
"DCC.Cleric": "Cleric",
108114
"DCC.ClericAbilities": "Cleric Abilities",
109115
"DCC.ClericSpellLevel": "Spell Level",
@@ -187,8 +193,11 @@
187193
"DCC.DamageLightning": "Lightning",
188194
"DCC.DamagePiercing": "Piercing",
189195
"DCC.DamagePoison": "Poison",
190-
"DCC.DamageRoll": "Damage Roll",
196+
"DCC.DamageRoll": "Damage Roll for {weapon}",
197+
"DCC.DamageRollInvalidFormula": "Invalid Damage formula {formula} with {weapon}",
198+
"DCC.DamageRollInvalidFormulaInline": "<b>Invalid Damage formula '{formula}'</b>",
191199
"DCC.DamageSlashing": "Slashing",
200+
"DCC.DamageSpecial": "Special Damage",
192201
"DCC.Day": "Day",
193202
"DCC.DeedRoll": "Deed Roll",
194203
"DCC.Default": "Default",
@@ -246,6 +255,7 @@
246255
"DCC.FindTrap": "Find Trap",
247256
"DCC.FlagsSave": "Update Special Traits",
248257
"DCC.FlagsTitle": "Configure Special Traits",
258+
"DCC.Folder": "Folder",
249259
"DCC.ForgeDocument": "Forge Document",
250260
"DCC.Formula": "Formula",
251261
"DCC.Fumble": "Fumble",
@@ -274,7 +284,6 @@
274284
"DCC.HitDiceWarn": "{name} has no available {formula} Hit Dice remaining!",
275285
"DCC.HitPoints": "HitPoints",
276286
"DCC.Identified": "Identified",
277-
"DCC.ImportStats": "Import Stats",
278287
"DCC.Infravision": "Infravision",
279288
"DCC.InheritActionDie": "Inherit Action Die",
280289
"DCC.InheritCritRange": "Inherit Crit Range",
@@ -374,14 +383,20 @@
374383
"DCC.Mount": "Mounts",
375384
"DCC.Mounts": "Mounts",
376385
"DCC.Name": "Name",
386+
"DCC.NewActorName": "New {type}",
377387
"DCC.NoCharges": "No Charges",
378388
"DCC.None": "None",
379389
"DCC.Normal": "Normal",
380390
"DCC.Notes": "Notes",
391+
"DCC.NoFolder": "-",
381392
"DCC.Occupation": "Occupation",
382393
"DCC.OpposedAlignment": "Opposed",
383394
"DCC.OtherFormula": "Other Formula",
384395
"DCC.OtherSpeeds": "Other Speeds",
396+
"DCC.ParseNPCWarning" : "Failed to read NPCs/monsters (expecting one or more DCC statblocks)",
397+
"DCC.ParseSingleNPCWarning" : "Failed to read one or more NPC/monster (expecting one or more DCC statblocks)",
398+
"DCC.ParsePlayerWarning" : "Failed to read player characters (expecting Purple Sorcerer Plain Text or JSON)",
399+
"DCC.ParseSinglePlayerWarning" : "Failed to read one or more player characters from Purple Sorcerer Plain Text ",
385400
"DCC.PasteBlock": "Paste In A Stat Block to Import",
386401
"DCC.Patron": "Patron",
387402
"DCC.Penalty": "Penalty",
@@ -393,6 +408,7 @@
393408
"DCC.Proficiency": "Proficiency",
394409
"DCC.Proficient": "Proficient",
395410
"DCC.PurpleSorcererPCLink": "Zero-Levels from Purple Sorcerer",
411+
"DCC.PurpleSorcererUpperLevelLink": "Upper Levels from Purple Sorcerer",
396412
"DCC.Quantity": "Quantity",
397413
"DCC.Race": "Race",
398414
"DCC.Range": "Range",
@@ -481,6 +497,7 @@
481497
"DCC.SpellMaterials": "Spellcasting Materials",
482498
"DCC.SpellName": "Spell Name",
483499
"DCC.SpellNone": "None",
500+
"DCC.SpellPage": "Page",
484501
"DCC.SpellPrepAlways": "Always Prepared",
485502
"DCC.SpellPrepPrepared": "Prepared",
486503
"DCC.SpellPreparationMode": "Spell Preparation Mode",

module/__mocks__/foundry.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,11 +192,14 @@ global.rollToMessageMock = jest.fn((messageData = {}, { rollMode = null, create
192192
})
193193
global.rollRollMock = jest.fn(() => {
194194
// console.log('Mock Roll: roll was called')
195-
return { total: 1 }
195+
return { total: 2 }
196196
})
197197
global.rollCleanFormulaMock = jest.fn((terms) => {
198198
return terms
199199
})
200+
global.rollValidateMock = jest.fn((formula) => {
201+
return true
202+
})
200203
global.Roll = jest.fn((formula, data = {}) => {
201204
return {
202205
dice: [{ results: [10], options: {} }],
@@ -205,6 +208,7 @@ global.Roll = jest.fn((formula, data = {}) => {
205208
}
206209
}).mockName('Roll')
207210
global.Roll.cleanFormula = global.rollCleanFormulaMock
211+
global.Roll.validate = global.rollValidateMock
208212

209213
/**
210214
* ChatMessage

module/__tests__/npc-parser.test.js

Lines changed: 152 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
/* Tests for NPC Parser */
22
/* eslint-env jest */
33

4-
import parseNPC from '../npc-parser.js'
4+
import parseNPCs from '../npc-parser.js'
55

66
/* Test snake */
77
test('super snake', () => {
8-
const parsedNPC = parseNPC('Very long, the power super-snake: Init +0; Atk bite +6 melee; Dmg 1d8;\r\n AC 13; HP 21; MV 20’; Act 1d20; SV Fort +8, Ref +4, Will +4; AL L.')
8+
const parsedNPC = parseNPCs('Very long, the power super-snake: Init +0; Atk bite +6 melee; Dmg 1d8;\r\n AC 13; HP 21; MV 20’; Act 1d20; SV Fort +8, Ref +4, Will +4; AL L.')
99
const expected = {
1010
name: 'Very long, the power super-snake',
1111
'data.attributes.init.value': '+0',
@@ -31,12 +31,12 @@ test('super snake', () => {
3131
}
3232
]
3333
}
34-
expect(parsedNPC).toMatchObject(expected)
34+
expect(parsedNPC).toMatchObject([expected])
3535
})
3636

3737
/* Test dry pile of bones */
3838
test('pile of bones', () => {
39-
const parsedNPC = parseNPC('Seven items of dry stuff: Init -2; Atk bite +0 melee; Dmg 1d4-1; AC 8; HP 3; MV 5’;\r\n Act 1d20; SV Fort +0, Ref -4, Will +1; AL C.')
39+
const parsedNPC = parseNPCs('Seven items of dry stuff: Init -2; Atk bite +0 melee; Dmg 1d4-1; AC 8; HP 3; MV 5’;\r\n Act 1d20; SV Fort +0, Ref -4, Will +1; AL C.')
4040
const expected = {
4141
name: 'Seven items of dry stuff',
4242
'data.attributes.init.value': '-2',
@@ -60,12 +60,12 @@ test('pile of bones', () => {
6060
}
6161
]
6262
}
63-
expect(parsedNPC).toMatchObject(expected)
63+
expect(parsedNPC).toMatchObject([expected])
6464
})
6565

6666
/* Test orcs */
6767
test('orcs', () => {
68-
const parsedNPC = parseNPC('Cute-Infused Orcs (3): Init +2; Atk claw +1 melee (1d4) or spear +1 melee (1d8); AC 15; HD 2d8+2; hp 13 each; MV 30’; Act 1d20; SP none; SV Fort +3, Ref +0, Will -1; AL C.')
68+
const parsedNPC = parseNPCs('Cute-Infused Orcs (3): Init +2; Atk claw +1 melee (1d4) or spear +1 melee (1d8); AC 15; HD 2d8+2; hp 13 each; MV 30’; Act 1d20; SP none; SV Fort +3, Ref +0, Will -1; AL C.')
6969
const expected = {
7070
name: 'Cute-Infused Orcs',
7171
'data.attributes.init.value': '+2',
@@ -103,12 +103,12 @@ test('orcs', () => {
103103
}
104104
]
105105
}
106-
expect(parsedNPC).toMatchObject(expected)
106+
expect(parsedNPC).toMatchObject([expected])
107107
})
108108

109109
/* Test spider */
110110
test('spider', () => {
111-
const parsedNPC = parseNPC('Xformed, Unicorn-Filled Spider: Init +1; Atk bite +2 melee (1d4 plus poison) or web +4 ranged (restrained, 20’ range); AC 13; HD 2d12 +2; hp 20; MV 30’ or climb 30’; Act 1d20; SP poison (DC 14 Fort save or additional 3d4 damage and lose 1 point of Strength, 1d4 damage if successful), create web, filled with bats; SV Fort +2, Ref +4, Will +0; AL N.\n')
111+
const parsedNPC = parseNPCs('Xformed, Unicorn-Filled Spider: Init +1; Atk bite +2 melee (1d4 plus poison) or web +4 ranged (restrained, 20’ range); AC 13; HD 2d12 +2; hp 20; MV 30’ or climb 30’; Act 1d20; SP poison (DC 14 Fort save or additional 3d4 damage and lose 1 point of Strength, 1d4 damage if successful), create web, filled with bats; SV Fort +2, Ref +4, Will +0; AL N.\n')
112112
const expected = {
113113
name: 'Xformed, Unicorn-Filled Spider',
114114
'data.attributes.init.value': '+1',
@@ -140,19 +140,21 @@ test('spider', () => {
140140
type: 'weapon',
141141
data: {
142142
toHit: '+4',
143-
melee: false
144-
// damage": '0', //@TODO: change damage to 0 when there is no die roll
145-
// description: { value: 'restrained, 20’ range' } // @TODO: Parse out this special
143+
melee: false,
144+
damage: '0',
145+
description: {
146+
summary: 'restrained, 20’ range'
147+
}
146148
}
147149
}
148150
]
149151
}
150-
expect(parsedNPC).toMatchObject(expected)
152+
expect(parsedNPC).toMatchObject([expected])
151153
})
152154

153155
/* Test wetad */
154156
test('wedad', () => {
155-
const parsedNPC = parseNPC('Gerieah (in her tree): Init +1; Atk tree limb slam +5 melee (1d10); AC 15;\n HD 4d10; hp 30; MV none; Act 1d20; SP takes 2x damage from fire, can attack targets up to 20’ away with tree limbs; SV Fort +6, Ref -2, Will +4; AL N.')
157+
const parsedNPC = parseNPCs('Gerieah (in her tree): Init +1; Atk tree limb slam +5 melee (1d10); AC 15;\n HD 4d10; hp 30; MV none; Act 1d20; SP takes 2x damage from fire, can attack targets up to 20’ away with tree limbs; SV Fort +6, Ref -2, Will +4; AL N.')
156158
const expected = {
157159
name: 'Gerieah (in her tree)',
158160
'data.attributes.init.value': '+1',
@@ -180,12 +182,45 @@ test('wedad', () => {
180182
}
181183
]
182184
}
183-
expect(parsedNPC).toMatchObject(expected)
185+
expect(parsedNPC).toMatchObject([expected])
186+
})
187+
188+
/* Test smultist */
189+
test('smultist', () => {
190+
const parsedNPC = parseNPCs('Green-robed smultist (1): Init +4; Atk dagger +5 melee (1d4+3); AC 11; HD 5d4+5; hp 21; MV 20’; SP 3d6 control check, able to cast arms of the angel, squid-mass (when killed, an squid-mass emerges; see stats below); Act 1d20; SV Fort +3, Ref +4, Will +0; AL C. Equipment: bird-shaped talisman of gold tied on a leather thong (worth 10 gp; see level 3).')
191+
const expected = {
192+
name: 'Green-robed smultist',
193+
'data.attributes.init.value': '+4',
194+
'data.attributes.ac.value': '11',
195+
'data.attributes.hitDice.value': '5d4+5',
196+
'data.attributes.hp.value': '21',
197+
'data.attributes.hp.max': '21',
198+
'data.attributes.speed.value': '20’',
199+
'data.attributes.special.value': '3d6 control check, able to cast arms of the angel, squid-mass (when killed, an squid-mass emerges', // @TODO: Is it worth finding a way to ignore colons inside brackets?
200+
'data.config.actionDice': '1d20',
201+
'data.saves.frt.value': '+3',
202+
'data.saves.ref.value': '+4',
203+
'data.saves.wil.value': '+0',
204+
'data.details.alignment': 'c',
205+
items: [
206+
{
207+
name: 'dagger',
208+
type: 'weapon',
209+
data: {
210+
toHit: '+5',
211+
damage: '1d4+3',
212+
description: { value: '' },
213+
melee: true
214+
}
215+
}
216+
]
217+
}
218+
expect(parsedNPC).toMatchObject([expected])
184219
})
185220

186221
/* Test short statline */
187222
test('shortstats', () => {
188-
const parsedNPC = parseNPC('Stunty, the short and muddled: Init +1; Atk kick +2 melee (1d3); AC 15;\n hp 4; Act 1d20; SV Ref +6, Fort -2, Will +4.')
223+
const parsedNPC = parseNPCs('Stunty, the short and muddled: Init +1; Atk kick +2 melee (1d3); AC 15;\n hp 4; Act 1d20; SV Ref +6, Fort -2, Will +4.')
189224
const expected = {
190225
name: 'Stunty, the short and muddled',
191226
'data.attributes.init.value': '+1',
@@ -213,12 +248,12 @@ test('shortstats', () => {
213248
}
214249
]
215250
}
216-
expect(parsedNPC).toMatchObject(expected)
251+
expect(parsedNPC).toMatchObject([expected])
217252
})
218253

219254
/* Test the bad guy's familiar with a minimal stat line */
220255
test('familiar', () => {
221-
const parsedNPC = parseNPC('The bad guy\'s familiar: Atk claw +3 melee (1d4), AC 15, HP 2.')
256+
const parsedNPC = parseNPCs('The bad guy\'s familiar: Atk claw +3 melee (1d4), AC 15, HP 2.')
222257
const expected = {
223258
name: 'The bad guy\'s familiar',
224259
'data.attributes.init.value': '+0',
@@ -246,12 +281,12 @@ test('familiar', () => {
246281
}
247282
]
248283
}
249-
expect(parsedNPC).toMatchObject(expected)
284+
expect(parsedNPC).toMatchObject([expected])
250285
})
251286

252287
/* Test damage modifiers */
253288
test('bonusguy', () => {
254-
const parsedNPC = parseNPC('Bonus Guy: Init -1; Atk big club +3 melee (1d4+2) or small club -2 melee (1d4 - 3); AC 13; HD 1d8+2; MV 30’; Act 1d20; SV Fort +2, Ref +1, Will -2; AL C.')
289+
const parsedNPC = parseNPCs('Bonus Guy: Init -1; Atk big club +3 melee (1d4+2) or small club -2 melee (1d4 - 3); AC 13; HD 1d8+2; MV 30’; Act 1d20; SV Fort +2, Ref +1, Will -2; AL C.')
255290
const expected = {
256291
name: 'Bonus Guy',
257292
'data.attributes.init.value': '-1',
@@ -284,5 +319,103 @@ test('bonusguy', () => {
284319
}
285320
]
286321
}
322+
expect(parsedNPC).toMatchObject([expected])
323+
})
324+
325+
/* Test multiple statlines */
326+
test('rodentsquad', () => {
327+
const parsedNPC = parseNPCs(
328+
`Mega Mole: Init +5; Atk claws +6 melee (1d8+3) ; AC 17;
329+
HD 3d8; hp 16; MV 20’; Act 1d20; SV Fort +4, Ref +4, Will +2;
330+
AL C.
331+
332+
Large Rat: Init +2; Atk teeth +2 melee (1d6) or tail +3 melee
333+
(1d4); AC 13; HD 1d8; hp 4 each; MV 30’; Act 1d20; SV Fort +2,
334+
Ref +2, Will +0; AL C.
335+
336+
Medium Mouse: Init +1; Atk bite +2 melee (1d3-1); AC 9;
337+
HD 1d6; hp 4 each; MV 35’ or leap 20’; Act 1d20; SV Fort +0,
338+
Ref +4, Will +2; AL C.`
339+
)
340+
const expected = [
341+
{
342+
name: 'Mega Mole',
343+
'data.attributes.init.value': '+5',
344+
'data.attributes.ac.value': '17',
345+
'data.attributes.hitDice.value': '3d8',
346+
'data.attributes.speed.value': '20’',
347+
'data.config.actionDice': '1d20',
348+
'data.saves.frt.value': '+4',
349+
'data.saves.ref.value': '+4',
350+
'data.saves.wil.value': '+2',
351+
'data.details.alignment': 'c',
352+
items: [
353+
{
354+
name: 'claws',
355+
type: 'weapon',
356+
data: {
357+
toHit: '+6',
358+
damage: '1d8+3',
359+
melee: true
360+
}
361+
}
362+
]
363+
},
364+
{
365+
name: 'Large Rat',
366+
'data.attributes.init.value': '+2',
367+
'data.attributes.ac.value': '13',
368+
'data.attributes.hitDice.value': '1d8',
369+
'data.attributes.speed.value': '30’',
370+
'data.config.actionDice': '1d20',
371+
'data.saves.frt.value': '+2',
372+
'data.saves.ref.value': '+2',
373+
'data.saves.wil.value': '+0',
374+
'data.details.alignment': 'c',
375+
items: [
376+
{
377+
name: 'teeth',
378+
type: 'weapon',
379+
data: {
380+
toHit: '+2',
381+
damage: '1d6',
382+
melee: true
383+
}
384+
},
385+
{
386+
name: 'tail',
387+
type: 'weapon',
388+
data: {
389+
toHit: '+3',
390+
damage: '1d4',
391+
melee: true
392+
}
393+
}
394+
]
395+
},
396+
{
397+
name: 'Medium Mouse',
398+
'data.attributes.init.value': '+1',
399+
'data.attributes.ac.value': '9',
400+
'data.attributes.hitDice.value': '1d6',
401+
'data.attributes.speed.value': '35’',
402+
'data.config.actionDice': '1d20',
403+
'data.saves.frt.value': '+0',
404+
'data.saves.ref.value': '+4',
405+
'data.saves.wil.value': '+2',
406+
'data.details.alignment': 'c',
407+
items: [
408+
{
409+
name: 'bite',
410+
type: 'weapon',
411+
data: {
412+
toHit: '+2',
413+
damage: '1d3-1',
414+
melee: true
415+
}
416+
}
417+
]
418+
}
419+
]
287420
expect(parsedNPC).toMatchObject(expected)
288421
})

0 commit comments

Comments
 (0)