import * as e from './common.mjs'; import { fixpoint } from './trait_effects.mjs'; const CONDITION=/\b(after|each|every|when|whenever|upon|until|at the (beginning|start|end)|in this run|lasting|if|per|subsequent|next)\b/i; const COUNT='(#\\d|\\d+|a|an|one|Obtains?)'; const GRANTS=[['item', new RegExp(COUNT+' "?=params.length)return null; const v=params[i]; return v>0&&Math.abs(v-Math.round(v))<1e-6?Math.round(v):null; } return ['a', 'an', 'one', 'Obtain', 'Obtains'].includes(token)?1:Number(token); } export function parse_grants(text, params){ text=text.replace(/<\/?unbreak>|]*>|<\/color>/g, '').replaceAll('[i]', ''); let grants=[], complete=true; for(const sentence of text.split(/(?<=[.!])"?\s+(?=[A-Z])/)){ if(!/^(Immediately )?Obtains?\b/.test(sentence)||CONDITION.test(sentence.replace(/<[^>]*>/g, ''))){ complete=false; continue; } const found=[]; for(const [kind, pattern]of GRANTS)for(const m of sentence.matchAll(pattern)){ const n=count(m[1], params); if(n===null)return [null, false]; const g={ kind, count:n }; if(['item', 'role'].includes(kind))g.id=Number(m[2]); if(kind==='cost_role')g.cost=Number(m[2]); found.push([m.index, g]); } if(!found.length)return [null, false]; grants.push(...found.sort((a, b)=>a[0]-b[0]).map(([, g])=>g)); } return [grants, complete]; } export function build(raw, season){ const buffs=new Map(raw.GridFightPortalBuffConfigRow.map(r=>[r.ID, r])), portals=[]; for(const link of raw.GridFightSeasonPortalConfigRow){ if(link.SeasonID!==season)continue; const row=buffs.get(link.PortalID); if(!row)e.fail('season portal '+link.PortalID+' has no PortalBuff row'); portals.push({ id:row.ID, oc:!!(row.IsOCEffective??0), title_hash:row.PortalBuffTitle.hash, json_path:row.JsonPath??'' }); } portals.sort((a, b)=>a.id-b.id); if(!portals.length||!e.uniqueValues(portals, p=>p.id))e.fail('missing or duplicate season portals'); return { season_id:season, portals }; } const PINNED={ 211701:[{ kind:'item', count:1, id:355100302 }, { kind:'role', count:1, id:1501 }], 100301:[{ kind:'refresh', count:11 }] }; export function check_grants(augment, grants, params, item_ids){ const granted=new Set(grants.filter(g=>g.kind==='item').map(g=>g.id)); for(const v of params)if(Math.abs(v-Math.round(v))<1e-6&&item_ids.has(Math.round(v))&&!granted.has(Math.round(v)))e.fail(`strategy ${augment}: item param ${Math.trunc(v)} is not granted`); if(PINNED[augment]&&e.json(grants)!==e.json(PINNED[augment]))e.fail('strategy '+augment+': grants differ from pinned'); } export function build_augments(raw, season, texts=null){ const rows=new Map(raw.GridFightAugmentConfigRow.map(r=>[r.ID, r])), item_ids=new Set(raw.GridFightItemConfigRow.map(r=>r.ID)), augments=[]; for(const link of raw.GridFightSeasonAugmentConfigRow){ if(link.SeasonID!==season)continue; const row=rows.get(link.AugmentID); if(!row)e.fail('season augment '+link.AugmentID+' has no Augment row'); const a={ id:row.ID, quality:row.Quality, chapters:row.ChapterLimitList??[], oc:!!(row.IsOCEffective??0) }; augments.push(a); const text=texts?.[row.ID]; if(text){ const params=(row.EffectParamList??[]).map(p=>fixpoint(p.wire)), [grants, complete]=parse_grants(text, params); if(grants?.length){ check_grants(row.ID, grants, params, item_ids); a.grants=grants; a.grants_complete=complete; } } } augments.sort((a, b)=>a.id-b.id); if(!augments.length||!e.uniqueValues(augments, a=>a.id))e.fail('missing or duplicate season augments'); return { season_id:season, augments }; } export function main(){ const a=e.cli([], { items:e.resource('items.raw.json'), season:2, output:e.resource('portals.json'), augments:e.resource('augments.json'), catalog:e.catalogDefault }), raw=e.loadJSON(a.items), data=build(raw, a.season); let texts=null; if(e.fs.existsSync(a.catalog))texts=Object.fromEntries(e.loadJSON(a.catalog).tables.GridFightAugmentConfigRow.map(r=>[r.ID, r.HexDesc?.text??''])); const augments=build_augments(raw, a.season, texts); e.atomic_json(a.output, data, 1, true); e.atomic_json(a.augments, augments, 1, true); console.log(`${a.output}: ${data.portals.length} portals for season ${a.season}`); const granting=augments.augments.filter(a=>a.grants); console.log(`${a.augments}: ${augments.augments.length} augments for season ${a.season}, ${granting.length} with immediate grants (${granting.filter(a=>!a.grants_complete).length} partial)`); } e.runMain(import.meta.url, main);