Files
NumberSwipe_New/campaign-50/tools/apply-original-ui.py
T

58 lines
14 KiB
Python

from pathlib import Path
import hashlib,json,re
root=Path(r'G:\项目\NumberSwipe-Variants\campaign-50');source=Path(r'G:\项目\NumberSwipe')
(root/'reports/ui-before.json').write_text(json.dumps({n:hashlib.sha256((root/n).read_bytes()).hexdigest() for n in ['levels.json','rules.mjs','order-a.json','order-b.json']},indent=2),encoding='utf-8')
css=(source/'modes/01-number-stroke/web/style.css').read_text(encoding='utf-8-sig')
css+='''
/* DEMO additions: preserve the original token and component system. */
#constraints{display:flex;gap:6px;flex-wrap:wrap;justify-content:center;margin:4px 0 0;min-height:0;flex-shrink:0}#constraints .chip{height:28px;min-height:28px;padding:0 10px;font-size:12px}.level-sub{max-width:240px;overflow:hidden;text-overflow:ellipsis}.caption{flex-shrink:0;height:36px;padding:0 4px}.topbar,.level-progress,.bottombar,.gesture-help{flex-shrink:0}#board-wrap{margin-top:8px}.next-overlay{position:absolute;bottom:calc(var(--safe-bottom) + 114px);left:50%;transform:translateX(-50%);padding:0 22px;min-width:160px;height:44px;margin:0;z-index:3}.next-overlay[hidden]{display:none}.next-overlay:active{transform:translateX(-50%) scale(.98)}#status{z-index:4}.rules-button{position:absolute;right:20px;top:calc(var(--safe-top) + 68px);background:none;font-size:12px;color:var(--ink-2);padding:3px}.sheet-backdrop{position:absolute;inset:0;background:rgba(35,36,58,.36);z-index:6;display:flex;align-items:flex-end}.sheet-backdrop[hidden]{display:none}.sheet{width:100%;max-height:75%;overflow:auto;background:var(--bg);border-radius:24px 24px 0 0;padding:24px 20px calc(20px + var(--safe-bottom))}.sheet h2{font-size:22px;font-weight:800;margin:0 0 12px}.sheet p{font-size:15px;line-height:1.7;color:var(--ink-2)}.sheet textarea{width:100%;min-height:100px;padding:12px;border:1px solid var(--line);border-radius:14px;font:inherit;user-select:text}.sheet .sheet-actions{display:flex;gap:8px;margin-top:16px}.sheet .action{height:48px}.sheet .primary{height:48px;padding:0 16px;margin:0}.sr-only{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0)}.select-footer .chip{min-height:44px;height:44px}.level-tile{min-height:44px}.select-footer a{display:flex;align-items:center;text-decoration:none;color:var(--ink-2)}@media(max-height:660px){.select-header{padding-top:8px}.caption{font-size:13px;height:30px}#constraints .chip{height:24px;min-height:24px}.bottombar{padding-top:8px}.action{height:48px}.gesture-help{font-size:12px}.screen{padding-top:calc(var(--safe-top) + 8px)}}
'''
(root/'style.css').write_text(css,encoding='utf-8')
s=(root/'ui.mjs').read_text(encoding='utf-8-sig')
s=s.replace("const pack=PACK;", "const pack=PACK;\nconst BOARD_FONT='ui-rounded, \"SF Pro Rounded\", \"PingFang SC\", \"Hiragino Sans GB\", \"Noto Sans SC\", system-ui, -apple-system, \"Segoe UI\", sans-serif';\nconst UI_THEME={cell:'#E6E7EC',ink:'#23243A',primary:'#6558F5',bright:'#7D72FF',danger:'#E5484D',hint:'#F5A524',ring:'#DFE1EC'};let toastTimer=null;")
s=s.replace("function setStatus(s){$('status').textContent=s;}", "function setStatus(s){$('status').textContent=s;$('status').className='toast show';if(toastTimer)clearTimeout(toastTimer);toastTimer=setTimeout(()=>{$('status').className='toast';},1800);}function showPlay(){$('screen-select').className='screen hidden';$('screen-play').className='screen enter';draw();}function showSelect(){saveRecord();$('screen-select').className='screen enter';$('screen-play').className='screen hidden';renderSelectors();$('status').className='toast';}")
s=s.replace('function start(i){','function start(i,open=true){')
s=s.replace("$('title').textContent=pack.title", "$('title').textContent='数字一笔'")
s=s.replace('renderConstraints();renderSelectors();draw();}', 'renderConstraints();renderSelectors();if(open)showPlay();draw();}',1)
a=s.index('function renderSelectors(){');b=s.index('function rulesFor(',a)
s=s[:a]+'''function renderSelectors(){
const list=$('pack-list');list.replaceChildren();for(const chapter of pack.chapters){const section=document.createElement('section');section.className='pack';const head=document.createElement('div');head.className='pack-title';const title=document.createElement('span');title.textContent=chapter.title;const count=document.createElement('span');count.className='pack-progress';const chapterLevels=pack.levels.filter(l=>l.chapter===chapter.id);count.textContent=chapterLevels.filter(l=>completed.includes(l.id)).length+' / 10';head.append(title,count);const grid=document.createElement('div');grid.className='level-grid';for(const l of chapterLevels){const i=pack.levels.indexOf(l),button=document.createElement('button');button.textContent=String(l.order);button.className='level-tile'+(completed.includes(l.id)?' cleared':'')+(i===index?' current':'');button.onclick=()=>start(i);grid.append(button);}section.append(head,grid);list.append(section);}
$('position').textContent=level.chapter+'-'+((level.order-1)%10+1);$('level-sub').textContent=level.chapterTitle+' · 第'+level.order+'关';$('progress').textContent=completed.length+' / 50';$('overall-fill').style.width=(completed.length*2)+'%';$('btn-continue').textContent='继续 · '+level.chapter+'-'+((level.order-1)%10+1);$('level-progress').replaceChildren();for(const l of pack.levels.filter(l=>l.chapter===level.chapter)){const seg=document.createElement('i');seg.className=(completed.includes(l.id)?'cleared ':'')+(l.id===level.id?'current':'');$('level-progress').append(seg);}
}
''' +s[b:]
a=s.index('function renderConstraints(){');b=s.index('function localGeometry(){',a)
s=s[:a]+'''function renderConstraints(){const list=$('constraints');list.replaceChildren();const add=(text,ids)=>{const b=document.createElement('button');b.className='chip';b.textContent=text;b.onclick=()=>{focus=ids;draw();};list.append(b);};if(level.geometry==='blocks')add('大格一步',level.cells.filter(c=>c.w*c.h>1).map(c=>c.id));if(level.features.includes('direction'))add('通道 · 按图进出',level.cells.filter(c=>c.direction).map(c=>c.id));if(level.features.includes('alternation'))add('蓝 → 橙 → 蓝',[]);if(level.features.includes('parity'))add('奇/偶 · 看步号',level.cells.filter(c=>c.parity).map(c=>c.id));if(level.walls?.length)add('永久墙',level.walls.flat());for(const d of level.doors||[])add(doorOpen(d,path.length)?'门开 · 下一格关':'门关 · 下一格开',d.edge);}
''' +s[b:]
# Keep raw polygons for hit and adjacency; inset rounded silhouettes are only visual.
a=s.index('function draw(){');b=s.index('function drawChannel(',a)
s=s[:a]+'''function draw(){if(!level)return;const width=canvas.clientWidth,height=canvas.clientHeight;if(!width||!height)return;const dpr=window.devicePixelRatio||1;canvas.width=Math.round(width*dpr);canvas.height=Math.round(height*dpr);ctx.setTransform(dpr,0,0,dpr,0,0);ctx.clearRect(0,0,width,height);
const {raw,center}=localGeometry(),all=raw.flatMap(r=>r.p),xmin=Math.min(...all.map(p=>p[0])),xmax=Math.max(...all.map(p=>p[0])),ymin=Math.min(...all.map(p=>p[1])),ymax=Math.max(...all.map(p=>p[1]));unit=Math.min(level.cells.length<=6?96:110,(width-20)/(xmax-xmin),(height-20)/(ymax-ymin));const ox=(width-unit*(xmax-xmin))/2-unit*xmin,oy=(height-unit*(ymax-ymin))/2-unit*ymin-height*.03;const tr=([x,y])=>[ox+x*unit,oy+y*unit];polygons=raw.map(r=>({...r,p:r.p.map(tr)}));centers=new Map([...center].map(([id,p])=>[id,tr(p)]));
// Place irregular-block labels inside the nearest occupied unit, as in the original view.
if(level.geometry!=='hex')for(const c of level.cells)if(c.shape){const middle=center.get(c.id);const candidates=units(c).map(([x,y])=>[x+.5,y+.5]);candidates.sort((a,b)=>Math.hypot(a[0]-middle[0],a[1]-middle[1])-Math.hypot(b[0]-middle[0],b[1]-middle[1]));centers.set(c.id,tr(candidates[0]));}
const bad=violations(level,path),badIds=new Set(bad.flatMap(b=>b.cells));
for(let i=1;i<path.length;i++)drawBridge(path[i-1],path[i]);
for(const c of level.cells){const visited=path.includes(c.id),start=c.clue===1,ring=c.clue!==undefined||c.parity||c.direction;const alternate=c.category!==undefined;let fill=alternate?(c.category==='circle'?'#64acf3':'#ffae55'):badIds.has(c.id)?UI_THEME.danger:cleared?UI_THEME.bright:visited||start?UI_THEME.primary:UI_THEME.cell;const foreground=!alternate&&(visited||start||badIds.has(c.id))?'#FFFFFF':UI_THEME.ink;if(focus.includes(c.id))paintCell(c,unit*.008,UI_THEME.hint);if(ring)paintCell(c,unit*.045,visited||start?UI_THEME.ring:UI_THEME.ink);paintCell(c,unit*.05+(ring?Math.max(2,unit*.05):0),fill);if(alternate&&visited){const [x,y]=centers.get(c.id);ctx.beginPath();ctx.moveTo(x-unit*.2,y+unit*.29);ctx.lineTo(x+unit*.2,y+unit*.29);ctx.strokeStyle=UI_THEME.primary;ctx.lineWidth=3;ctx.stroke();}
const [x,y]=centers.get(c.id),step=path.indexOf(c.id)+1;let value=step||c.clue;const ratio=value===1?.6:value>=10?.46:.52,font=Math.max(14,Math.min(52,unit*ratio));ctx.textAlign='center';ctx.textBaseline='middle';ctx.fillStyle=foreground;
if(c.direction)drawChannel(c.direction,x,y-(value?unit*.23:0),Math.min(unit*.25,value?unit*.14:22),foreground);
if(value){ctx.font='800 '+font+'px '+BOARD_FONT;ctx.fillText(String(value),x,y+(c.direction?unit*.09:c.parity?-unit*.06:0)+font*.04);if(c.parity){ctx.font='700 '+Math.max(10,unit*.19)+'px '+BOARD_FONT;ctx.fillText(c.parity==='odd'?'奇':'偶',x,y+unit*.27);}if(c.category){ctx.font='700 '+Math.max(10,unit*.18)+'px '+BOARD_FONT;ctx.fillText(c.category==='circle'?'○':'△',x,y-unit*.3);}if(c.clue!==undefined&&step&&step!==c.clue){ctx.font='800 '+Math.max(10,unit*.19)+'px '+BOARD_FONT;ctx.fillText('需'+c.clue,x,y-unit*.3);}}
else if(c.parity){ctx.font='700 '+Math.max(16,unit*.44)+'px '+BOARD_FONT;ctx.fillText(c.parity==='odd'?'奇':'偶',x,y+unit*.015);}else if(c.category){ctx.font='700 '+Math.max(14,unit*.36)+'px '+BOARD_FONT;ctx.fillText(c.category==='circle'?'○':'△',x,y);}
}
for(const edge of level.walls||[])drawBarrier(edge,'wall');for(const d of level.doors||[])drawBarrier(d.edge,doorOpen(d,path.length)?'open':'closed');
$('remaining-n').textContent=cleared?'完成':String(level.cells.length-path.length);$('remaining').className='remaining'+(cleared?' done':'');if(level.doors?.length)renderConstraints();if(bad.length)setStatus(bad[0].message+',可以点已走格退回。');$('undo').disabled=!path.length;$('next').hidden=!cleared||index===pack.levels.length-1;
}
function roundedRect(x,y,w,h,r){r=Math.min(r,w/2,h/2);ctx.beginPath();ctx.moveTo(x+r,y);ctx.lineTo(x+w-r,y);ctx.quadraticCurveTo(x+w,y,x+w,y+r);ctx.lineTo(x+w,y+h-r);ctx.quadraticCurveTo(x+w,y+h,x+w-r,y+h);ctx.lineTo(x+r,y+h);ctx.quadraticCurveTo(x,y+h,x,y+h-r);ctx.lineTo(x,y+r);ctx.quadraticCurveTo(x,y,x+r,y);ctx.closePath();}
function paintCell(c,inset,colour){ctx.fillStyle=colour;if(level.geometry==='hex'){const p=polygons.find(p=>p.id===c.id).p,[cx,cy]=centers.get(c.id),scale=1-inset/(unit*.86);const pts=p.map(([x,y])=>[cx+(x-cx)*scale,cy+(y-cy)*scale]);ctx.beginPath();for(let i=0;i<6;i++){const a=pts[(i+5)%6],b=pts[i],d=pts[(i+1)%6],before=[b[0]+(a[0]-b[0])*.12,b[1]+(a[1]-b[1])*.12],after=[b[0]+(d[0]-b[0])*.12,b[1]+(d[1]-b[1])*.12];if(i===0)ctx.moveTo(...before);else ctx.lineTo(...before);ctx.quadraticCurveTo(...b,...after);}ctx.closePath();ctx.fill();return;}
const polygonsHere=polygons.filter(p=>p.id===c.id),u=units(c),origin=[polygonsHere[0].p[0][0]-u[0][0]*unit,polygonsHere[0].p[0][1]-u[0][1]*unit],radius=Math.max(2,unit*.18-inset*.2);
if(!c.shape){roundedRect(origin[0]+c.x*unit+inset,origin[1]+c.y*unit+inset,c.w*unit-inset*2,c.h*unit-inset*2,radius);ctx.fill();return;}
for(const [x,y]of u){roundedRect(origin[0]+x*unit+inset,origin[1]+y*unit+inset,unit-inset*2,unit-inset*2,radius);ctx.fill();}
for(const [x,y]of u)for(const [dx,dy]of [[1,0],[0,1]])if(u.some(p=>p[0]===x+dx&&p[1]===y+dy)){const xx=origin[0]+x*unit,yy=origin[1]+y*unit;const a=dx?[xx+unit-inset-radius,yy+inset,inset*2+radius*2,unit-inset*2]:[xx+inset,yy+unit-inset-radius,unit-inset*2,inset*2+radius*2];roundedRect(...a,0);ctx.fill();}}
function drawBridge(a,b){const segments=sharedSegments(a,b);if(!segments.length)return;const [p,q]=segments[0],cx=(p[0]+q[0])/2,cy=(p[1]+q[1])/2,len=Math.hypot(q[0]-p[0],q[1]-p[1]),nx=-(q[1]-p[1])/len,ny=(q[0]-p[0])/len;ctx.beginPath();ctx.moveTo(cx-nx*unit*.09,cy-ny*unit*.09);ctx.lineTo(cx+nx*unit*.09,cy+ny*unit*.09);ctx.lineWidth=unit*.46;ctx.lineCap='butt';ctx.strokeStyle=cleared?UI_THEME.bright:UI_THEME.primary;ctx.stroke();}
function sharedSegments(a,b){const edges=[];for(const p of polygons.filter(p=>p.id===a))for(const q of polygons.filter(p=>p.id===b))for(let i=0;i<p.p.length;i++)for(let j=0;j<q.p.length;j++){const x=p.p[i],y=p.p[(i+1)%p.p.length],v=q.p[j],w=q.p[(j+1)%q.p.length];if(Math.hypot(x[0]-w[0],x[1]-w[1])<.01&&Math.hypot(y[0]-v[0],y[1]-v[1])<.01)edges.push([x,y]);}return edges;}
''' +s[b:]
s=s.replace('function drawChannel(kind,x,y,r){', 'function drawChannel(kind,x,y,r,ink=UI_THEME.ink){').replace("ctx.strokeStyle='#c1daf7'", "ctx.strokeStyle=ink==='#FFFFFF'?'#8E85FD':'#DFE1EC'").replace("ctx.strokeStyle='#15497b'", "ctx.strokeStyle=ink")
s=s.replace("window.addEventListener('resize',draw);", "$('btn-back').onclick=showSelect;$('btn-continue').onclick=showPlay;$('btn-rules').onclick=()=>{$('rules-sheet').hidden=false;};$('close-rules').onclick=()=>{$('rules-sheet').hidden=true;};$('btn-feedback').onclick=()=>{$('feedback-sheet').hidden=false;};$('close-feedback').onclick=()=>{saveRecord();$('feedback-sheet').hidden=true;};\nwindow.addEventListener('resize',draw);")
s=s.replace('start(initial>=0?initial:0);','start(initial>=0?initial:0,false);')
s=s.replace("records:records.length,path:","records:records.length,screen:$('screen-play').className.includes('hidden')?'select':'play',path:")
(root/'ui.mjs').write_text(s,encoding='utf-8')
print('Original UI CSS and rounded board rendering applied; gameplay rules untouched.')