Files
shanhe-puzzle/scripts/triangulate-county-holes.mjs
codex-bot 4845826deb
Shanhe source and offline package checks / verify (pull_request) Failing after 5s
feat: sync Shanhe 2.2 source and add remote package verification
2026-11-01 17:13:46 +08:00

15 lines
2.7 KiB
JavaScript

// Preparation only: static hole-aware convex parts and robust decomposition fallback.
import fs from 'node:fs';import earcut from 'earcut';import assert from 'node:assert/strict';import {createHash} from 'node:crypto';import M from 'matter-js';import gameModule from '../src/game.js';
function inside([x,y],ring){let hit=false;for(let i=0,j=ring.length-1;i<ring.length;j=i++){const a=ring[i],b=ring[j];if((a[1]>y)!==(b[1]>y)&&x<(b[0]-a[0])*(y-a[1])/(b[1]-a[1])+a[0])hit=!hit;}return hit;}
function triangulate(region){region.physics=[];let assigned=0;for(const outer of region.rings){const holes=(region.holes||[]).filter(h=>inside(h[0],outer));assigned+=holes.length;const flat=earcut.flatten([outer,...holes]),indices=earcut(flat.vertices,flat.holes,2);assert.ok(earcut.deviation(flat.vertices,flat.holes,2,indices)<1e-8);for(let i=0;i<indices.length;i+=3)region.physics.push(indices.slice(i,i+3).map(j=>[flat.vertices[j*2],flat.vertices[j*2+1]]));}assert.equal(assigned,(region.holes||[]).length);}
let regions=0,parts=0,fallbacks=0;const index=JSON.parse(fs.readFileSync('research/county-map-index.json','utf8'));
for(const name of fs.readdirSync('src/county').filter(n=>n.endsWith('.js'))){const path='src/county/'+name,level=JSON.parse(fs.readFileSync(path,'utf8').replace(/^window\.PuzzleAtlas\.register\(/,'').replace(/\);\s*$/,''));
for(const region of level.regions)if(region.holes&&region.holes.length){regions++;triangulate(region);parts+=region.physics.length;}
// Exercise the exact game's decomposition at common viewport scales. A valid
// but awkward outline must not leave missing physical parts after max-depth.
const fallback=new Set();for(const [w,h] of [[320,568],[340,560],[440,680]]){const game=new gameModule.Game(level,w,h,4);for(let id=0;id<level.regions.length;id++){let warned=false;const previous=console.warn;console.warn=(...args)=>{if(String(args[0]).includes('quickDecomp'))warned=true;else previous(...args);};let group;try{group=game.createGroup([id],{tx:0,ty:0,angle:0});}finally{console.warn=previous;}if(warned)fallback.add(id);M.Composite.remove(game.engine.world,group.body);game.groups=[];}game.destroy();}
for(const id of fallback){triangulate(level.regions[id]);fallbacks++;}
const encoded='window.PuzzleAtlas.register('+JSON.stringify(level)+');\n';fs.writeFileSync(path,encoded);const entry=index.levels.find(l=>l.id===level.id);entry.assetSha256=createHash('sha256').update(encoded).digest('hex');entry.collisionFallbacks=Array.from(fallback,id=>level.regions[id].id);
}
fs.writeFileSync('research/county-map-index.json',JSON.stringify(index)+'\n');console.log('County physics:',regions,'hole regions,',parts,'hole parts;',fallbacks,'static decomposition fallbacks');