import { intro, outro, select, spinner, isCancel, cancel, } from '@clack/prompts'; import color from 'picocolors'; import { match, P } from 'ts-pattern'; import csvToJson from 'convert-csv-to-json'; import itemProto from './input/item_proto.txt' with { type: 'text' }; if (!itemProto) { throw new Error('The file input/item_proto.txt was not found'); } intro(color.inverse('JSON Transform')); const fileType = await select({ message: 'Pick a file to transform', options: [ { value: 'special_item_group', label: 'special_item_group.txt', }, ], }); if (isCancel(fileType)) { cancel('Operation cancelled'); process.exit(0); } await match(fileType) .with('special_item_group', async () => { const s = spinner(); const proto = csvToJson.fieldDelimiter('\t').csvStringToJson(itemProto); s.start('Transforming'); const input = Bun.file('./input/special_item_group.txt'); if (!input.exists()) { throw new Error('The file input/special_item_group.txt was not found'); } const specialItemGroup = await input.text(); const REGEX = /group\s+([^\n]+)\s*\{\s*vnum\s+([^\n]+)\s*(?:type\s+([^\n]+)\s*)?((?:\d+\s+[^\n]+\s*)+)\s*(?:effect\s+([^\n]+)\s*)?}/gim; const matches = Array.from(specialItemGroup.matchAll(REGEX)).map( (instance) => { const [, name, vnum, baseType, rowsBlock, effect] = instance; const type = match(baseType?.trim().toLocaleLowerCase()) .with('pct', () => 'percentage' as const) .with('attr', () => 'attribute' as const) .with('quest', () => 'quest' as const) .with('special', () => 'special' as const) .otherwise(() => 'normal' as const); const STANDARD_ROW_REGEX = /^[ \t]*\d+\s+(\S+)\s+(\d+)\s+(\d+)(?:\s+(\S+))?/gm; const ATTR_ROW_REGEX = /^[ \t]*\d+\s+(\d+)\s+(\d+)?/gm; const rows = Array.from( rowsBlock.matchAll( type === 'attribute' ? ATTR_ROW_REGEX : STANDARD_ROW_REGEX, ), ).map((row) => { const value1 = Number(row[2]); const value2 = Number(row[3]); const value3 = Number(row[4] || 0); return match(type) .with('attribute', () => ({ apply_type: Number(row[1]), apply_value: value1, })) .otherwise(() => ({ vnum: match(row[1].trim()) .with(P.union('경험치', 'exp'), () => 'exp') .with( P.union('mob', 'slow', 'drain_hp', 'poison', 'group'), (v) => v, ) .when( (v) => Number.isNaN(Number(v)), (v) => { const itemMatch = proto.find( (item) => item['ITEM_NAME(K)'] === v, ); if (!itemMatch) { throw new Error(`Item not found ${v}`); } return Number(itemMatch['ITEM번호']); }, ) .otherwise((value) => Number(value)), count: value1, probability: value2, rare_percentage: value3, })); }); return { name: name.trim(), type, vnum: Number(vnum), rows, effect: effect ? effect.trim().substring(1, effect.trim().length - 1) : null, }; }, ); s.stop('special_item_group.txt was transformed'); const out = Bun.file('./output/special_item_group.json'); if (await out.exists()) { await out.delete(); } out.write( JSON.stringify( { $schema: 'https://raw.githubusercontent.com/WildEgo/m2-json-schemas/refs/heads/main/special_item_group.json', rows: matches, }, null, 2, ), ); }) .otherwise(() => { outro(color.bgRed('Invalid file type')); process.exit(1); }); outro("You're all set now");