Files
2026-10-30 23:54:07 +07:00

138 lines
5.3 KiB
JavaScript

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+' "?<gridfightinfo type=item id=(\\d+)', 'g')], ['role', new RegExp(COUNT+' "?<gridfightinfo type=role id=(\\d+)', 'g')], ['gold', new RegExp(COUNT+' Gold Coin', 'g')], ['refresh', new RegExp(COUNT+' free refresh', 'g')], ['cost_role', new RegExp(COUNT+' random (?:1-star )?(\\d)-cost character', 'g')], ['basic', new RegExp(COUNT+' (?:random basic equipment|Random Basic Weapon)', 'gi')], ['emblem', new RegExp(COUNT+' random Emblem', 'g')]];
export const _fixpoint=fixpoint;
function count(token, params){
if(token.startsWith('#')){
const i=Number(token.slice(1))-1;
if(i>=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=[^>]*>|<\/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);