Skip to content

Commit 079c2a3

Browse files
committed
Merge branch 'feat/spell-structured-data-migration' into feature/update-supernatural-shields
2 parents adf730f + a0d2d28 commit 079c2a3

310 files changed

Lines changed: 4993 additions & 19 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
const fs = require('fs');
2+
const path = require('path');
3+
4+
const MAGIC_PACKS_DIR = path.join(__dirname, '..', 'src', 'packs', 'magic');
5+
6+
function parseDamage(description) {
7+
const match = description.match(/Daño *(base)* *(\d+)/i);
8+
return match ? parseInt(match[2], 10) : 0;
9+
}
10+
11+
function parseArea(description) {
12+
const match = description.match(/[Áá]rea de (\d+) metros/i);
13+
return match ? parseInt(match[1], 10) : 0;
14+
}
15+
16+
function parseResistanceEffect(description) {
17+
const resistances = {
18+
physical: 'RF',
19+
disease: 'RE',
20+
poison: 'RV',
21+
magic: 'RM',
22+
psychic: 'RP'
23+
};
24+
25+
for (const [type, code] of Object.entries(resistances)) {
26+
const beforePattern = new RegExp(`(\\d+) *[RFEVMP]{0,2} *o* *${code}`, 'i');
27+
const afterPattern = new RegExp(`${code} *o* *[RFEVMP]{0,2} *(\\d+)`, 'i');
28+
29+
let match = description.match(beforePattern);
30+
if (match) return { value: parseInt(match[1], 10), type };
31+
32+
match = description.match(afterPattern);
33+
if (match) return { value: parseInt(match[1], 10), type };
34+
}
35+
36+
return { value: 0, type: null };
37+
}
38+
39+
function migrateSpellFile(filePath) {
40+
const fileName = path.basename(filePath);
41+
42+
try {
43+
const fileContent = fs.readFileSync(filePath, 'utf8');
44+
const spellData = JSON.parse(fileContent);
45+
46+
if (spellData.type !== 'spell') {
47+
console.log(` ⊘ Skipping ${fileName} (not a spell)`);
48+
return;
49+
}
50+
51+
let modified = false;
52+
const grades = ['base', 'intermediate', 'advanced', 'arcane'];
53+
54+
for (const gradeName of grades) {
55+
const grade = spellData.system?.grades?.[gradeName];
56+
if (!grade) continue;
57+
58+
const description = grade.description?.value || '';
59+
const damage = parseDamage(description);
60+
const area = parseArea(description);
61+
const resistanceEffect = parseResistanceEffect(description);
62+
63+
if (damage > 0 && !grade.damage) {
64+
grade.damage = { value: damage };
65+
modified = true;
66+
} else if (grade.damage && grade.damage.value !== damage && damage > 0) {
67+
console.log(` ⚠ ${fileName} [${gradeName}]: damage mismatch`);
68+
}
69+
70+
if (area > 0 && !grade.area) {
71+
grade.area = { value: area };
72+
modified = true;
73+
} else if (grade.area && grade.area.value !== area && area > 0) {
74+
console.log(` ⚠ ${fileName} [${gradeName}]: area mismatch`);
75+
}
76+
77+
if (resistanceEffect.value > 0 && !grade.resistanceEffect) {
78+
grade.resistanceEffect = resistanceEffect;
79+
modified = true;
80+
} else if (
81+
grade.resistanceEffect &&
82+
(grade.resistanceEffect.value !== resistanceEffect.value ||
83+
grade.resistanceEffect.type !== resistanceEffect.type) &&
84+
resistanceEffect.value > 0
85+
) {
86+
console.log(` ⚠ ${fileName} [${gradeName}]: resistance mismatch`);
87+
}
88+
}
89+
90+
if (modified) {
91+
fs.writeFileSync(filePath, JSON.stringify(spellData, null, 2), 'utf8');
92+
console.log(` ✓ Migrated ${fileName}`);
93+
return true;
94+
}
95+
96+
console.log(` ○ ${fileName} already has structured data`);
97+
return false;
98+
} catch (error) {
99+
console.error(` ✗ Error processing ${fileName}:`, error.message);
100+
return false;
101+
}
102+
}
103+
104+
function migrateAllSpells() {
105+
console.log('Starting spell migration to structured data format...\n');
106+
107+
if (!fs.existsSync(MAGIC_PACKS_DIR)) {
108+
console.error(`Error: Magic packs directory not found at ${MAGIC_PACKS_DIR}`);
109+
process.exit(1);
110+
}
111+
112+
const files = fs.readdirSync(MAGIC_PACKS_DIR)
113+
.filter(file => file.endsWith('.json') && file.startsWith('spell_'));
114+
115+
console.log(`Found ${files.length} spell files\n`);
116+
117+
let migratedCount = 0;
118+
let skippedCount = 0;
119+
let errorCount = 0;
120+
121+
for (const file of files) {
122+
const result = migrateSpellFile(path.join(MAGIC_PACKS_DIR, file));
123+
if (result === true) migratedCount++;
124+
else if (result === false) skippedCount++;
125+
else errorCount++;
126+
}
127+
128+
console.log('\n' + '='.repeat(60));
129+
console.log(`Migration complete!`);
130+
console.log(` ✓ Migrated: ${migratedCount}`);
131+
console.log(` ○ Already migrated: ${skippedCount}`);
132+
if (errorCount > 0) console.log(` ✗ Errors: ${errorCount}`);
133+
console.log('='.repeat(60));
134+
}
135+
136+
migrateAllSpells();
Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,25 @@
1-
export const damageCheck = effect => {
1+
export const damageCheck = gradeDataOrEffect => {
2+
if (typeof gradeDataOrEffect === 'object' && gradeDataOrEffect !== null) {
3+
const damageValue = gradeDataOrEffect.damage?.value;
4+
if (typeof damageValue === 'number') {
5+
return damageValue;
6+
}
7+
if (gradeDataOrEffect.description?.value) {
8+
return parseDescriptionForDamage(gradeDataOrEffect.description.value);
9+
}
10+
}
11+
12+
if (typeof gradeDataOrEffect === 'string') {
13+
return parseDescriptionForDamage(gradeDataOrEffect);
14+
}
15+
16+
return 0;
17+
};
18+
19+
function parseDescriptionForDamage(effect) {
220
effect = effect.replace('.', '');
321
if (/Daño *(base)* *\d+/i.test(effect)) {
422
return parseInt(effect.match(/Daño *(base)* *\d+/i)[0].match(/\d+/)[0]) ?? 0;
5-
} else {
6-
return 0;
723
}
8-
};
24+
return 0;
25+
}

src/module/combat/utils/resistanceEffectCheck.js

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,26 @@
1-
export const resistanceEffectCheck = effect => {
1+
export const resistanceEffectCheck = gradeDataOrEffect => {
2+
if (typeof gradeDataOrEffect === 'object' && gradeDataOrEffect !== null) {
3+
const resEffect = gradeDataOrEffect.resistanceEffect;
4+
if (resEffect && typeof resEffect.value === 'number' && resEffect.type) {
5+
return {
6+
value: resEffect.value,
7+
type: resEffect.type,
8+
check: resEffect.value > 0
9+
};
10+
}
11+
if (gradeDataOrEffect.description?.value) {
12+
return parseDescriptionForResistance(gradeDataOrEffect.description.value);
13+
}
14+
}
15+
16+
if (typeof gradeDataOrEffect === 'string') {
17+
return parseDescriptionForResistance(gradeDataOrEffect);
18+
}
19+
20+
return { value: 0, type: undefined, check: false };
21+
};
22+
23+
function parseDescriptionForResistance(effect) {
224
const resistanceEffect = { value: 0, type: undefined, check: false };
325
const resistances = { physical: 'RF', disease: 'RE', poison: 'RV', magic: 'RM', psychic: 'RP' }
426

@@ -22,4 +44,4 @@ export const resistanceEffectCheck = effect => {
2244
}
2345

2446
return resistanceEffect;
25-
};
47+
}

src/module/dialogs/combat/CombatAttackDialog.js

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -449,14 +449,14 @@ export class CombatAttackDialog extends FormApplication {
449449
this.attackerActor.setFlag(game.animabf.id, 'lastOffensiveSpellUsed', spellUsed);
450450
const { spells } = this.attackerActor.system.mystic;
451451
const spell = spells.find(w => w._id === spellUsed);
452-
const spellUsedEffect = spell?.system.grades[spellGrade].description.value ?? '';
452+
const spellGradeData = spell?.system.grades[spellGrade];
453453
if (this.attackerActor.evaluateCast(spellCasting)) {
454454
this.modalData.attacker.mystic.overrideMysticCast = true;
455455
return;
456456
}
457457
let visibleCheck = spell?.system.visible;
458458

459-
let resistanceEffect = resistanceEffectCheck(spellUsedEffect);
459+
let resistanceEffect = resistanceEffectCheck(spellGradeData);
460460

461461
let combatModifier = 0;
462462
for (const key in attackerCombatMod) {
@@ -662,9 +662,8 @@ export class CombatAttackDialog extends FormApplication {
662662
mystic.spellUsed = spells.find(w => w.system.combatType.value === 'attack')?._id;
663663
}
664664
const spell = spells.find(w => w._id === mystic.spellUsed);
665-
const spellUsedEffect =
666-
spell?.system.grades[mystic.spellGrade].description.value ?? '';
667-
mystic.damage.final = mystic.damage.special + damageCheck(spellUsedEffect);
665+
const spellGradeData = spell?.system.grades[mystic.spellGrade];
666+
mystic.damage.final = mystic.damage.special + damageCheck(spellGradeData);
668667
mystic.spellCasting = this.attackerActor.mysticCanCastEvaluate(
669668
spell,
670669
mystic.spellGrade,
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
/** @typedef {import('./Migration').Migration} Migration */
2+
3+
import { Logger } from '../../../utils';
4+
5+
const newItems = {
6+
get ready() {
7+
return !!this.spell;
8+
},
9+
async init() {
10+
if (this.ready) return;
11+
this.spell = await game.packs.get('animabf.magic').getDocuments();
12+
},
13+
async get(itemType, name) {
14+
if (!this.ready) await this.init();
15+
return this[itemType].find(i => i.name.toLowerCase() === name.toLowerCase());
16+
}
17+
};
18+
19+
/** @type Migration */
20+
export const Migration10UpdateSpellsStructuredData = {
21+
id: 'migration_update-spells-structured-data',
22+
version: '2.0.6',
23+
order: 1,
24+
title: 'Update spells to use structured damage and resistance data',
25+
description:
26+
'This migration updates existing spells to include structured fields for damage, area of effect, ' +
27+
'and resistance bonuses instead of relying on text parsing. This improves performance and ' +
28+
'makes spell data more reliable and easier to work with. ' +
29+
'Custom spell descriptions will be preserved. Custom spells will be left unchanged but may need ' +
30+
'manual updates to benefit from the new structured data format.',
31+
filterItems(item) {
32+
return item.type === 'spell' && !['animabf.magic'].includes(item.pack);
33+
},
34+
filterActors(actor) {
35+
return actor.items.filter(i => i.type === 'spell').length > 0;
36+
},
37+
async updateItem(item) {
38+
if (item.type !== 'spell') {
39+
Logger.error('spell filter not working');
40+
return;
41+
}
42+
43+
const name = item.name.includes('-') ? item.name.split(' - ')[1] : item.name;
44+
const newItem = await newItems.get('spell', name);
45+
if (!newItem) return;
46+
47+
item.name = newItem.name;
48+
const { system } = newItem;
49+
system.description.value = item.system.description.value;
50+
51+
for (const gradeName of ['base', 'intermediate', 'advanced', 'arcane']) {
52+
if (item.system.grades?.[gradeName]?.description?.value) {
53+
system.grades[gradeName].description.value = item.system.grades[gradeName].description.value;
54+
}
55+
}
56+
57+
item.system = system;
58+
return item;
59+
}
60+
};

src/module/migration/migrations/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ export { Migration6OldModifiers } from './6-fix-old-modifiers';
77
export { Migration7WeaponsOwnStrength } from './7-fix-weapons-ownStrength';
88
export { Migration8RemoveWeaponFue } from './8-remove-unused-weaponFue';
99
export { Migration9FixKiSeals } from './9-fix-ki-seals';
10+
export { Migration10UpdateSpellsStructuredData } from './10-update-spells-structured-data';

src/module/types/mystic/SpellItemConfig.js

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,28 +54,52 @@ export const INITIAL_MYSTIC_SPELL_DATA = {
5454
intRequired: { value: 0 },
5555
maintenanceCost: { value: 0 },
5656
zeon: { value: 0 },
57-
description: { value: '' }
57+
description: { value: '' },
58+
damage: { value: 0 },
59+
area: { value: 0 },
60+
resistanceEffect: {
61+
value: 0,
62+
type: ''
63+
}
5864
},
5965
intermediate: {
6066
name: { value: SpellGradeNames.INTERMEDIATE },
6167
intRequired: { value: 0 },
6268
maintenanceCost: { value: 0 },
6369
zeon: { value: 0 },
64-
description: { value: '' }
70+
description: { value: '' },
71+
damage: { value: 0 },
72+
area: { value: 0 },
73+
resistanceEffect: {
74+
value: 0,
75+
type: ''
76+
}
6577
},
6678
advanced: {
6779
name: { value: SpellGradeNames.ADVANCED },
6880
intRequired: { value: 0 },
6981
maintenanceCost: { value: 0 },
7082
zeon: { value: 0 },
71-
description: { value: '' }
83+
description: { value: '' },
84+
damage: { value: 0 },
85+
area: { value: 0 },
86+
resistanceEffect: {
87+
value: 0,
88+
type: ''
89+
}
7290
},
7391
arcane: {
7492
name: { value: SpellGradeNames.ARCANE },
7593
intRequired: { value: 0 },
7694
maintenanceCost: { value: 0 },
7795
zeon: { value: 0 },
78-
description: { value: '' }
96+
description: { value: '' },
97+
damage: { value: 0 },
98+
area: { value: 0 },
99+
resistanceEffect: {
100+
value: 0,
101+
type: ''
102+
}
79103
}
80104
}
81105
};

src/packs/magic/spell_Acechar_en_los_Sue_os_jZh9b9NJOR5eesXb.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,10 @@
5151
},
5252
"description": {
5353
"value": "RM o RP 120."
54+
},
55+
"resistanceEffect": {
56+
"value": 120,
57+
"type": "magic"
5458
}
5559
},
5660
"intermediate": {
@@ -68,6 +72,10 @@
6872
},
6973
"description": {
7074
"value": "RM o RP 160."
75+
},
76+
"resistanceEffect": {
77+
"value": 160,
78+
"type": "magic"
7179
}
7280
},
7381
"advanced": {
@@ -85,6 +93,10 @@
8593
},
8694
"description": {
8795
"value": "RM o RP 200."
96+
},
97+
"resistanceEffect": {
98+
"value": 200,
99+
"type": "magic"
88100
}
89101
},
90102
"arcane": {
@@ -102,6 +114,10 @@
102114
},
103115
"description": {
104116
"value": "RM o RP 240."
117+
},
118+
"resistanceEffect": {
119+
"value": 240,
120+
"type": "magic"
105121
}
106122
}
107123
}

0 commit comments

Comments
 (0)