{"id":159,"date":"2025-09-28T13:07:49","date_gmt":"2025-09-28T13:07:49","guid":{"rendered":"https:\/\/kawan4u.com\/?page_id=159"},"modified":"2025-09-28T13:07:49","modified_gmt":"2025-09-28T13:07:49","slug":"%e5%bb%ba%e5%b1%8b%e5%ad%90%e6%b8%b8%e6%88%8f","status":"publish","type":"page","link":"https:\/\/kawan4u.com\/?page_id=159","title":{"rendered":"\u5efa\u5c4b\u5b50\u6e38\u620f"},"content":{"rendered":"\n<!doctype html>\n<html lang=\"zh\">\n<head>\n<meta charset=\"utf-8\"\/>\n<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\"\/>\n<title>\u5efa\u522b\u5885 \u2014 Kawan4u \u6c49\u5b57\u6e38\u620f\uff081\u201318 \u5173\u5b8c\u6574\u7248\uff09<\/title>\n<!-- \u5b57\u4f53\uff1a\u5c1d\u8bd5\u4f7f\u7528\u6977\u4f53 -->\n<style>\n:root{\n  --bg:#f6f7fb;--card:#ffffff;--muted:#556;--accent:#2563eb;--ok:#16a34a;--warn:#ef4444;\n}\n*{box-sizing:border-box}\nbody{margin:0;font-family: \"KaiTi\",\"\u6977\u4f53\",\"STKaiti\",\"Noto Serif SC\",serif;background:var(--bg);color:#0b1220}\n.container{max-width:980px;margin:18px auto;padding:12px}\n.header{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:12px}\n.brand{display:flex;gap:12px;align-items:center}\n.badge{background:#111;color:#fff;padding:8px 12px;border-radius:999px;font-weight:700}\n.small{font-size:13px;color:var(--muted)}\n.panel{background:var(--card);border-radius:12px;padding:12px;border:1px solid #e6eef8;box-shadow:0 6px 18px rgba(3,7,18,.05)}\n.grid2{display:grid;gap:12px}\n@media(min-width:900px){.grid2{grid-template-columns:1fr 1fr}}\n.bank{display:grid;grid-template-columns:repeat(6,1fr);gap:8px}\n.strokeBtn{background:#fff;border-radius:10px;padding:8px;display:flex;flex-direction:column;align-items:center;gap:6px;border:1px solid #e6eef8;cursor:pointer;user-select:none;touch-action:none}\n.strokeBtn[disabled]{opacity:.35;pointer-events:none}\n.count{font-size:12px;color:var(--muted)}\n.stage{height:320px;border-radius:12px;border:2px dashed #e2e8f0;position:relative;display:flex;align-items:center;justify-content:center;background:linear-gradient(180deg,#eef6ff,#f7fff7 60%)}\n.overlayChar{position:absolute;left:50%;top:40%;transform:translate(-50%,-50%);font-size:140px;color:#111;opacity:.5;pointer-events:none;display:none}\n.msg{color:var(--muted);min-height:22px;margin-top:8px}\n.quiz{display:flex;gap:8px;flex-wrap:wrap;margin-top:10px;justify-content:center}\n.quiz button{padding:8px 12px;border-radius:10px;border:1px solid #e6eef8;background:#fff;cursor:pointer}\n.quiz button.correct{outline:3px solid var(--ok)}\n.quiz button.wrong{outline:3px solid var(--warn)}\n.controls{display:flex;gap:8px;align-items:center;margin-top:10px}\n.footerBtns{display:flex;gap:8px}\n.toast{position:fixed;left:50%;bottom:16px;transform:translateX(-50%);background:#111;color:#fff;padding:8px 12px;border-radius:999px;opacity:0;transition:.25s;z-index:9999}\n.toast.show{opacity:1}\n.certModal{position:fixed;inset:0;background:rgba(0,0,0,.36);display:none;align-items:center;justify-content:center;padding:16px;z-index:9998}\n.certCard{background:#fff;border-radius:12px;padding:14px;max-width:680px;width:100%}\n.input{padding:8px;border-radius:8px;border:1px solid #e6eef8;width:100%;margin-top:6px}\n.topRow{display:flex;gap:12px;align-items:center}\n.kv{background:#fff;border-radius:8px;padding:8px 10px;border:1px solid #e6eef8}\n.smallmuted{font-size:13px;color:var(--muted)}\n.btn{padding:8px 12px;border-radius:8px;border:1px solid #e6eef8;background:#fff;cursor:pointer}\n.hint{font-size:13px;color:#334155;margin-top:8px}\n<\/style>\n<\/head>\n<body>\n<div class=\"container\">\n  <header class=\"header\">\n    <div class=\"brand\">\n      <div class=\"badge\">\u5efa\u522b\u5885<\/div>\n      <div>\n        <div style=\"font-weight:700\">\u5efa\u522b\u5885 \u2014 \u5b66\u6c49\u5b57\u62fc\u97f3\uff081\u201318 \u5173\uff09<\/div>\n        <div class=\"small\">\u70b9\u7b14\u753b \u2192 \u5f53\u5c4b\u524d\u51fa\u73b0\u534a\u900f\u660e\u5b57 \u2192 \u9009\u62fc\u97f3\u5f97\u91d1\u5e01<\/div>\n      <\/div>\n    <\/div>\n    <div class=\"topRow\">\n      <div class=\"kv\">\u5173\u5361 <b id=\"levelLabel\">1<\/b><\/div>\n      <div class=\"kv\">\u91d1\u5e01 <b id=\"coinDisplay\">0<\/b><\/div>\n      <button id=\"dailyBtn\" class=\"btn\">\u6bcf\u65e5 +1<\/button>\n      <button id=\"resetBtn\" class=\"btn\">\u91cd\u7f6e\u5168\u90e8<\/button>\n    <\/div>\n  <\/header>\n\n  <main class=\"panel grid2\">\n    <!-- \u5de6\u680f\uff1a\u7b14\u753b & \u4fe1\u606f -->\n    <div>\n      <div style=\"display:flex;justify-content:space-between;align-items:center;margin-bottom:8px\">\n        <div class=\"smallmuted\">\u53ef\u7ec4\u6210\u7684\u5b57\uff1a<b id=\"composableCount\">0<\/b><\/div>\n        <div class=\"smallmuted\">\u5df2\u89e3\u51b3\uff1a<b id=\"solvedCount\">0<\/b>\/<b id=\"totalCount\">0<\/b><\/div>\n      <\/div>\n\n      <div id=\"bank\" class=\"bank panel\"><\/div>\n\n      <div style=\"margin-top:10px\" class=\"hint\">\n        \u73a9\u6cd5\u8981\u70b9\uff1a\u968f\u610f\u70b9\u7b14\u753b\uff1b\u82e5\u5c4b\u524d\u51fa\u73b0\u534a\u900f\u660e\u5b57\uff0c\u8bf7\u9009\u62e9\u6b63\u786e\u62fc\u97f3\u3002<br\/>\n        \u6e05\u7a7a\u5c4b\u5b50\u4e0d\u4f1a\u6263\u91d1\u5e01\uff1b\u53ea\u6709\u9009\u9519\u62fc\u97f3\u624d\u6263 1 \u679a\u91d1\u5e01\u3002\u6bcf\u9898\u5956\u52b1\u4e0a\u9650 3 \u6b21\u3002\n      <\/div>\n    <\/div>\n\n    <!-- \u53f3\u680f\uff1a\u623f\u5b50 & \u9898\u76ee -->\n    <div>\n      <div class=\"stage panel\" id=\"stage\">\n        <!-- SVG \u623f\u5b50\uff08\u6a21\u5757\u5316\uff09 -->\n        <svg id=\"houseSVG\" viewBox=\"0 0 320 240\" width=\"420\" height=\"315\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" aria-hidden=\"true\">\n          <!-- \u8349\u5730 -->\n          <rect x=\"0\" y=\"200\" width=\"320\" height=\"40\" fill=\"#7dd3ac\"\/>\n          <!-- lot (always present) -->\n          <g id=\"lot\" style=\"opacity:1\">\n            <rect x=\"40\" y=\"180\" width=\"240\" height=\"18\" fill=\"#16a34a\" rx=\"4\"\/>\n          <\/g>\n\n          <!-- columns -->\n          <g id=\"columns\" style=\"opacity:0\">\n            <rect x=\"68\" y=\"110\" width=\"14\" height=\"70\" rx=\"4\" fill=\"#9ca3af\"\/>\n            <rect x=\"238\" y=\"110\" width=\"14\" height=\"70\" rx=\"4\" fill=\"#9ca3af\"\/>\n            <rect x=\"110\" y=\"110\" width=\"14\" height=\"70\" rx=\"4\" fill=\"#9ca3af\"\/>\n            <rect x=\"185\" y=\"110\" width=\"14\" height=\"70\" rx=\"4\" fill=\"#9ca3af\"\/>\n          <\/g>\n\n          <!-- beam -->\n          <g id=\"beam\" style=\"opacity:0\">\n            <rect x=\"60\" y=\"106\" width=\"200\" height=\"10\" rx=\"5\" fill=\"#9ca3af\"\/>\n          <\/g>\n\n          <!-- rafters -->\n          <g id=\"rafters\" style=\"opacity:0\">\n            <rect x=\"48\" y=\"92\" width=\"224\" height=\"8\" rx=\"4\" fill=\"#c084fc\"\/>\n          <\/g>\n\n          <!-- roof -->\n          <g id=\"roof\" style=\"opacity:0\">\n            <polygon points=\"40,92 160,40 280,92\" fill=\"#f97316\"\/>\n          <\/g>\n\n          <!-- walls -->\n          <g id=\"walls\" style=\"opacity:0\">\n            <rect x=\"70\" y=\"110\" width=\"180\" height=\"70\" rx=\"8\" fill=\"#f1f5f9\" stroke=\"#cbd5e1\"\/>\n          <\/g>\n\n          <!-- plaster -->\n          <g id=\"plaster\" style=\"opacity:0\">\n            <rect x=\"70\" y=\"110\" width=\"180\" height=\"70\" rx=\"8\" fill=\"#e2e8f0\"\/>\n          <\/g>\n\n          <!-- doors & windows -->\n          <g id=\"doorsWindows\" style=\"opacity:0\">\n            <rect x=\"100\" y=\"140\" width=\"40\" height=\"40\" rx=\"6\" fill=\"#7c3aed\"\/>\n            <circle cx=\"125\" cy=\"160\" r=\"3\" fill=\"#fde68a\"\/>\n            <rect x=\"190\" y=\"125\" width=\"36\" height=\"24\" rx=\"6\" fill=\"#7dd3fc\" stroke=\"#0ea5e9\"\/>\n          <\/g>\n\n          <!-- details -->\n          <g id=\"details\" style=\"opacity:0\">\n            <rect x=\"68\" y=\"190\" width=\"184\" height=\"6\" fill=\"#94a3b8\"\/>\n          <\/g>\n\n          <!-- painted -->\n          <g id=\"painted\" style=\"opacity:0\">\n            <rect x=\"70\" y=\"110\" width=\"180\" height=\"70\" rx=\"8\" fill=\"#e6eef8\"\/>\n          <\/g>\n\n          <!-- final garden\/pool\/trees etc (for later levels) -->\n          <g id=\"extras\" style=\"opacity:0\">\n            <ellipse cx=\"40\" cy=\"210\" rx=\"20\" ry=\"8\" fill=\"#60a5fa\"\/>\n            <rect x=\"250\" y=\"190\" width=\"40\" height=\"20\" rx=\"6\" fill=\"#86efac\"\/>\n          <\/g>\n        <\/svg>\n\n        <!-- \u534a\u900f\u660e\u5b57\uff08\u5927\u5b57\uff09 -->\n        <div id=\"overlayChar\" class=\"overlayChar\">\u5b57<\/div>\n      <\/div>\n\n      <div id=\"msg\" class=\"msg smallmuted\">\u968f\u610f\u70b9\u7b14\u753b\uff0c\u82e5\u5c4b\u524d\u51fa\u73b0\u534a\u900f\u660e\u5b57\u8bf7\u9009\u62e9\u62fc\u97f3\u3002<\/div>\n\n      <div id=\"quiz\" class=\"quiz panel\" style=\"margin-top:12px;display:none;justify-content:center\"><\/div>\n\n      <div style=\"margin-top:12px;display:flex;gap:8px;align-items:center\">\n        <button id=\"clearBtn\" class=\"btn\">\u6e05\u7a7a\u5c0f\u5c4b<\/button>\n        <button id=\"upgradeBtn\" class=\"btn\" disabled>\u786e\u8ba4\u5347\u7ea7\uff08\u9700\u6d88\u8017\u91d1\u5e01\uff09<\/button>\n        <div style=\"margin-left:auto;text-align:right\">\n          <div class=\"smallmuted\">\u5347\u7ea7\u4e8b\u9879\uff1a<span id=\"upgradeText\">\u2014<\/span><\/div>\n        <\/div>\n      <\/div>\n    <\/div>\n  <\/main>\n\n  <div style=\"margin-top:12px\" class=\"panel\">\n    <div style=\"display:flex;gap:10px;align-items:center\">\n      <img decoding=\"async\" id=\"logo\" src=\"logo.png\" alt=\"logo\" style=\"width:64px;height:64px;object-fit:contain\"\/>\n      <div>\n        <div style=\"font-weight:700\">Kawan4u \u2014 \u5efa\u522b\u5885<\/div>\n        <div class=\"small\">\u5b8c\u6210\u5168\u90e8\u5173\u5361\u53ef\u8d2d\u4e70\u7ed3\u4e1a\u8bc1\u4e66\uff085\u91d1\u5e01\uff09\u5e76\u4e0b\u8f7d\uff0c\u8bc1\u4e66\u4e5f\u4f1a\u5907\u4efd\u81f3\u7ba1\u7406\u5458\u90ae\u7bb1\u3002<\/div>\n      <\/div>\n      <div style=\"margin-left:auto\">\n        <button id=\"buyCertBtn\" class=\"btn\" disabled>\u8d2d\u4e70\u7ed3\u4e1a\u8bc1\u4e66\uff085\u91d1\u5e01\uff09<\/button>\n      <\/div>\n    <\/div>\n  <\/div>\n<\/div>\n\n<!-- toast -->\n<div id=\"toast\" class=\"toast\"><\/div>\n\n<!-- \u8bc1\u4e66 modal -->\n<div id=\"certModal\" class=\"certModal\">\n  <div class=\"certCard panel\">\n    <h3>\u751f\u6210\u7ed3\u4e1a\u8bc1\u4e66\uff08\u6d88\u8017 5 \u679a\u91d1\u5e01\uff09<\/h3>\n    <div class=\"smallmuted\">\u8bf7\u586b\u5199\u8d44\u6599\uff0c\u786e\u8ba4\u540e\u751f\u6210\u8bc1\u4e66 JPG \u5e76\u53d1\u9001\u5907\u4efd\u5230\u7ba1\u7406\u5458\u90ae\u7bb1\u3002<\/div>\n    <div style=\"margin-top:10px\">\n      <label class=\"smallmuted\">\u59d3\u540d<\/label>\n      <input id=\"certName\" class=\"input\" placeholder=\"\u5b66\u751f\u59d3\u540d\"\/>\n      <label class=\"smallmuted\">\u5b66\u6821<\/label>\n      <input id=\"certSchool\" class=\"input\" placeholder=\"\u5b66\u6821\u540d\u79f0\uff08\u53ef\u9009\uff09\"\/>\n      <label class=\"smallmuted\">\u73ed\u7ea7<\/label>\n      <input id=\"certClass\" class=\"input\" placeholder=\"\u73ed\u7ea7\uff08\u53ef\u9009\uff09\"\/>\n      <div style=\"display:flex;gap:8px;justify-content:flex-end;margin-top:10px\">\n        <button id=\"certCancel\" class=\"btn\">\u53d6\u6d88<\/button>\n        <button id=\"certConfirm\" class=\"btn\">\u786e\u8ba4\u5e76\u751f\u6210\u8bc1\u4e66\uff08\u4e0b\u8f7d + \u90ae\u4ef6\uff09<\/button>\n      <\/div>\n    <\/div>\n  <\/div>\n<\/div>\n\n<!-- canvas hidden for certificate generation -->\n<canvas id=\"certCanvas\" width=\"1200\" height=\"900\" style=\"display:none\"><\/canvas>\n\n<!-- EmailJS -->\n<script src=\"https:\/\/cdn.jsdelivr.net\/npm\/@emailjs\/browser@4\/dist\/email.min.js\"><\/script>\n\n<script>\n\/* ===========================\n   \u914d\u7f6e & \u5e38\u91cf\n   =========================== *\/\nconst SERVICE_ID = 'service_weaacfa';    \/\/ \u4f60\u7ed9\u7684\nconst TEMPLATE_ID = 'template_tc9h2be'; \/\/ \u4f60\u7ed9\u7684\nconst PUBLIC_KEY = '0V2naDH6x42A6jSzc'; \/\/ \u4f60\u7ed9\u7684\nemailjs.init(PUBLIC_KEY);\n\n\/* \u5047\u62fc\u97f3\u5e93\uff08\u56fa\u5b9a\uff09\u2014\u2014\u5e72\u6270\u9009\u9879\u6765\u6e90\uff08\u5c3d\u91cf\u8986\u76d6\u5e38\u89c1\u97f3\u8282\uff09 *\/\nconst FAKE_PINYIN_POOL = [\n  'y\u012b','\u00e9r','s\u0101n','s\u00ec','w\u01d4','li\u00f9','q\u012b','b\u0101','ji\u01d4','sh\u00ed',\n  'g\u014dng','t\u01d4','g\u00e0n','zh\u00e8ng','\u011br','sh\u00e0ng','xi\u00e0','r\u00e9n','d\u00e0',\n  'ti\u0101n','m\u00f9','zh\u01ceo','sh\u012b','ji\u0101','ji\u00e0n','ni\u00e1n','zu\u01d2','sh\u00e9n',\n  'gu\u0101n','mi\u00e8','b\u00f9','zh\u00f9','y\u012bng','chu\u00e1ng','b\u00e0n','l\u00e1i','li\u00f9',\n  'y\u00ec','t\u00e0i','g\u00e8','k\u0101i','j\u01d0ng','z\u00e0i'\n];\n\n\/* \u5173\u5361\u6570\u636e\uff081\u201318\uff09\u2014\u2014\u6bcf\u4e2a target \u5305\u542b char, strokes \u8ba1\u6570, pinyin *\/\nconst LEVELS = [\n  { id:1, available:{'\u6a2a':4,'\u7ad6':1}, targets:[\n      {char:'\u5341', strokes:{'\u6a2a':1,'\u7ad6':1}, pinyin:'sh\u00ed'},\n      {char:'\u4e09', strokes:{'\u6a2a':3}, pinyin:'s\u0101n'}\n    ], reward:2, upgradeCost:1, upgradeText:'\u4e00\u7247\u7a7a\u5730', applyUpgrade:()=>togglePart('lot', true) },\n\n  { id:2, available:{'\u6a2a':6,'\u7ad6':3}, targets:[\n      {char:'\u5de5', strokes:{'\u6a2a':2,'\u7ad6':1}, pinyin:'g\u014dng'},\n      {char:'\u571f', strokes:{'\u6a2a':2,'\u7ad6':1}, pinyin:'t\u01d4'},\n      {char:'\u5e72', strokes:{'\u6a2a':2,'\u7ad6':1}, pinyin:'g\u00e0n'}\n    ], reward:2, upgradeCost:1, upgradeText:'4 \u6839\u67f1\u5b50\uff08columns\uff09', applyUpgrade:()=>togglePart('columns', true) },\n\n  { id:3, available:{'\u6a2a':7,'\u7ad6':4}, targets:[\n      {char:'\u6b63', strokes:{'\u6a2a':5,'\u7ad6':1}, pinyin:'zh\u00e8ng'},\n      {char:'\u8033', strokes:{'\u7ad6':1,'\u6487':1,'\u637a':1}, pinyin:'\u011br'}\n    ], reward:2, upgradeCost:1, upgradeText:'\u6a2a\u6881\uff08beam\uff09', applyUpgrade:()=>togglePart('beam', true) },\n\n  { id:4, available:{'\u6a2a':4,'\u7ad6':1,'\u70b9':1}, targets:[\n      {char:'\u4e09', strokes:{'\u6a2a':3}, pinyin:'s\u0101n'},\n      {char:'\u4e0b', strokes:{'\u6a2a':1,'\u7ad6':1,'\u70b9':1}, pinyin:'xi\u00e0'}\n    ], reward:2, upgradeCost:2, upgradeText:'\u5efa\u693d\u5b50\uff08rafter\uff09', applyUpgrade:()=>togglePart('rafters', true) },\n\n  { id:5, available:{'\u6a2a':2,'\u7ad6':3,'\u70b9':2}, targets:[\n      {char:'\u5361', strokes:{'\u6a2a':2,'\u7ad6':1,'\u70b9':1}, pinyin:'k\u01ce'},\n      {char:'\u535c', strokes:{'\u7ad6':1,'\u70b9':1}, pinyin:'b\u01d4'}\n    ], reward:2, upgradeCost:2, upgradeText:'\u76d6\u5c4b\u9876\uff08roof\uff09', applyUpgrade:()=>togglePart('roof', true) },\n\n  { id:6, available:{'\u6a2a':4,'\u7ad6':2,'\u6487':3}, targets:[\n      {char:'\u725b', strokes:{'\u6a2a':1,'\u7ad6':1,'\u6487':1}, pinyin:'ni\u00fa'},\n      {char:'\u5382', strokes:{'\u6a2a':1,'\u7ad6':1}, pinyin:'ch\u01ceng'},\n      {char:'\u5343', strokes:{'\u6a2a':1,'\u6487':1}, pinyin:'qi\u0101n'}\n    ], reward:2, upgradeCost:2, upgradeText:'\u5efa\u5899\u58c1', applyUpgrade:()=>togglePart('walls', true) },\n\n  { id:7, available:{'\u6a2a':6,'\u7ad6':3,'\u6487':3}, targets:[\n      {char:'\u5348', strokes:{'\u6a2a':1,'\u7ad6':1}, pinyin:'w\u01d4'},\n      {char:'\u5f00', strokes:{'\u6a2a':2,'\u7ad6':1}, pinyin:'k\u0101i'},\n      {char:'\u4e95', strokes:{'\u6a2a':2,'\u7ad6':2}, pinyin:'j\u01d0ng'}\n    ], reward:3, upgradeCost:3, upgradeText:'\u5899\u58c1\u62b9\u7070\uff08plastering\uff09', applyUpgrade:()=>togglePart('plaster', true) },\n\n  { id:8, available:{'\u6a2a':7,'\u7ad6':5,'\u6487':3}, targets:[\n      {char:'\u5728', strokes:{'\u6a2a':2,'\u7ad6':2,'\u70b9':1}, pinyin:'z\u00e0i'},\n      {char:'\u5de6', strokes:{'\u6a2a':1,'\u7ad6':1,'\u6487':1}, pinyin:'zu\u01d2'},\n      {char:'\u4ec0', strokes:{'\u7ad6':1,'\u70b9':1,'\u6487':1}, pinyin:'sh\u00e9n'}\n    ], reward:3, upgradeCost:3, upgradeText:'\u5b89\u88c5\u7a97\u53e3\u548c\u95e8', applyUpgrade:()=>togglePart('doorsWindows', true) },\n\n  { id:9, available:{'\u6a2a':8,'\u7ad6':6,'\u6487':7}, targets:[\n      {char:'\u5f62', strokes:{'\u6a2a':2,'\u7ad6':2,'\u6487':1}, pinyin:'x\u00edng'},\n      {char:'\u4f73', strokes:{'\u6a2a':2,'\u7ad6':1,'\u6487':1}, pinyin:'ji\u0101'},\n      {char:'\u4ef6', strokes:{'\u6a2a':1,'\u7ad6':1,'\u6487':1}, pinyin:'ji\u00e0n'}\n    ], reward:3, upgradeCost:3, upgradeText:'\u522b\u5885\u7ec6\u8282\uff08\u706f\u3001\u88d9\u811a\u7b49\uff09', applyUpgrade:()=>togglePart('details', true) },\n\n  { id:10, available:{'\u6a2a':4,'\u7ad6':6,'\u6487':3}, targets:[\n      {char:'\u5e74', strokes:{'\u6a2a':2,'\u7ad6':1,'\u6487':1}, pinyin:'ni\u00e1n'},\n      {char:'\u4f5c', strokes:{'\u6a2a':1,'\u7ad6':1,'\u6487':1,'\u70b9':1}, pinyin:'zu\u00f2'}\n    ], reward:5, upgradeCost:3, upgradeText:'\u6cb9\u6f06\uff0c\u8c6a\u534e\u522b\u5885\u5448\u73b0', applyUpgrade:()=>togglePart('painted', true) },\n\n  { id:11, available:{'\u6a2a':1,'\u6487':3,'\u637a':3}, targets:[\n      {char:'\u516b', strokes:{'\u6487':1,'\u637a':1}, pinyin:'b\u0101'},\n      {char:'\u4eba', strokes:{'\u6487':1,'\u637a':1}, pinyin:'r\u00e9n'},\n      {char:'\u5927', strokes:{'\u6a2a':1,'\u6487':1,'\u637a':1}, pinyin:'d\u00e0'}\n    ], reward:5, upgradeCost:4, upgradeText:'\u73b0\u4ee3\u5355\u5c42\u522b\u5885', applyUpgrade:()=>togglePart('painted', true) },\n\n  { id:12, available:{'\u6a2a':3,'\u7ad6':2,'\u6487':3,'\u637a':3}, targets:[\n      {char:'\u5929', strokes:{'\u6a2a':1,'\u7ad6':1,'\u70b9':1}, pinyin:'ti\u0101n'},\n      {char:'\u4e2a', strokes:{'\u7ad6':1,'\u6a2a':1}, pinyin:'g\u00e8'},\n      {char:'\u6728', strokes:{'\u6a2a':1,'\u7ad6':1,'\u6487':1,'\u637a':1}, pinyin:'m\u00f9'}\n    ], reward:5, upgradeCost:4, upgradeText:'\u73b0\u4ee3\u53cc\u5c42\uff08\u5e26\u9633\u53f0\uff09', applyUpgrade:()=>togglePart('extras', true) },\n\n  { id:13, available:{'\u6a2a':4,'\u7ad6':2,'\u6487':5,'\u637a':3}, targets:[\n      {char:'\u672c', strokes:{'\u6a2a':1,'\u7ad6':1,'\u6487':1}, pinyin:'b\u011bn'},\n      {char:'\u722a', strokes:{'\u6487':3}, pinyin:'zh\u01ceo'},\n      {char:'\u5931', strokes:{'\u6a2a':1,'\u7ad6':1,'\u6487':1,'\u637a':1}, pinyin:'sh\u012b'}\n    ], reward:5, upgradeCost:4, upgradeText:'\u9633\u53f0\u8bbe\u5907\u5b8c\u5584', applyUpgrade:()=>togglePart('extras', true) },\n\n  { id:14, available:{'\u6a2a':4,'\u7ad6':4,'\u6487':3,'\u637a':2}, targets:[\n      {char:'\u8d70', strokes:{'\u6a2a':1,'\u7ad6':1,'\u6487':1,'\u637a':1}, pinyin:'z\u01d2u'},\n      {char:'\u4f11', strokes:{'\u6487':1,'\u7ad6':1}, pinyin:'xi\u016b'}\n    ], reward:8, upgradeCost:6, upgradeText:'\u5de6\u9662\u6cf3\u6c60', applyUpgrade:()=>togglePart('extras', true) },\n\n  { id:15, available:{'\u6a2a':2,'\u6487':3,'\u637a':2,'\u70b9':4}, targets:[\n      {char:'\u516d', strokes:{'\u6a2a':1,'\u637a':1}, pinyin:'li\u00f9'},\n      {char:'\u4e49', strokes:{'\u6a2a':1,'\u6487':1}, pinyin:'y\u00ec'},\n      {char:'\u592a', strokes:{'\u6a2a':1,'\u6487':1,'\u637a':1}, pinyin:'t\u00e0i'}\n    ], reward:8, upgradeCost:7, upgradeText:'\u53f3\u9662\u82b1\u56ed', applyUpgrade:()=>togglePart('extras', true) },\n\n  { id:16, available:{'\u6a2a':4,'\u6487':5,'\u637a':2,'\u70b9':5}, targets:[\n      {char:'\u5934', strokes:{'\u6a2a':1,'\u6487':1}, pinyin:'t\u00f3u'},\n      {char:'\u5173', strokes:{'\u6a2a':1,'\u7ad6':1,'\u6487':1}, pinyin:'gu\u0101n'},\n      {char:'\u706d', strokes:{'\u6a2a':1,'\u6487':1,'\u637a':1}, pinyin:'mi\u00e8'}\n    ], reward:8, upgradeCost:7, upgradeText:'\u767d\u8272\u7bf1\u7b06', applyUpgrade:()=>togglePart('extras', true) },\n\n  { id:17, available:{'\u6a2a':6,'\u7ad6':3,'\u6487':4,'\u70b9':5}, targets:[\n      {char:'\u4e0d', strokes:{'\u6a2a':1,'\u7ad6':1}, pinyin:'b\u00f9'},\n      {char:'\u4f4f', strokes:{'\u7ad6':1,'\u6487':1}, pinyin:'zh\u00f9'},\n      {char:'\u5e94', strokes:{'\u6a2a':1,'\u7ad6':1,'\u6487':1}, pinyin:'y\u012bng'}\n    ], reward:10, upgradeCost:8, upgradeText:'\u7eff\u5730\u4e0e\u6e56\u6cca', applyUpgrade:()=>togglePart('extras', true) },\n\n  { id:18, available:{'\u6a2a':6,'\u7ad6':4,'\u6487':6,'\u637a':2,'\u70b9':3}, targets:[\n      {char:'\u5e8a', strokes:{'\u6a2a':1,'\u7ad6':1,'\u6487':1,'\u637a':1}, pinyin:'chu\u00e1ng'},\n      {char:'\u4f34', strokes:{'\u7ad6':1,'\u6487':1}, pinyin:'b\u00e0n'},\n      {char:'\u6765', strokes:{'\u6a2a':1,'\u7ad6':1,'\u6487':1}, pinyin:'l\u00e1i'}\n    ], reward:10, upgradeCost:8, upgradeText:'\u522b\u5885\u80cc\u5c71\u9762\u6e56\uff08\u5b8c\u6210\uff09', applyUpgrade:()=>togglePart('extras', true) }\n];\n\n\/* ===========================\n   \u72b6\u6001 & \u5b58\u6863\uff08localStorage\uff09\n   =========================== *\/\nconst STORAGE_KEY = 'kawan4u_jb_v1';\nlet state = JSON.parse(localStorage.getItem(STORAGE_KEY) || '{}');\nif(!state.version){\n  state = {\n    coins:0,\n    levelIdx:0,\n    usedCounts:{},\n    solved:{},        \/\/ keys: L{levelId}-{char}\n    rewardTimes:{},   \/\/ times rewarded per solved question\n    lastDaily:'',\n    levelUpgraded:[], \/\/ list of level ids upgraded (applied)\n    version:1\n  };\n  saveState();\n}\nfunction saveState(){ localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); }\n\n\/* ===========================\n   DOM refs\n   =========================== *\/\nconst bankEl = document.getElementById('bank');\nconst overlay = document.getElementById('overlayChar');\nconst quizEl = document.getElementById('quiz');\nconst coinDisplay = document.getElementById('coinDisplay');\nconst composableCountEl = document.getElementById('composableCount');\nconst solvedCountEl = document.getElementById('solvedCount');\nconst totalCountEl = document.getElementById('totalCount');\nconst levelLabel = document.getElementById('levelLabel');\nconst msgEl = document.getElementById('msg');\nconst toastEl = document.getElementById('toast');\nconst upgradeBtn = document.getElementById('upgradeBtn');\nconst upgradeTextEl = document.getElementById('upgradeText');\nconst buyCertBtn = document.getElementById('buyCertBtn');\nconst certModal = document.getElementById('certModal');\nconst certCancel = document.getElementById('certCancel');\nconst certConfirm = document.getElementById('certConfirm');\nconst certNameInput = document.getElementById('certName');\nconst certSchoolInput = document.getElementById('certSchool');\nconst certClassInput = document.getElementById('certClass');\nconst certCanvas = document.getElementById('certCanvas');\nconst dailyBtn = document.getElementById('dailyBtn');\nconst clearBtn = document.getElementById('clearBtn');\nconst resetBtn = document.getElementById('resetBtn');\n\n\/* ===========================\n   \u5de5\u5177\u51fd\u6570\uff1anormalize \u62fc\u97f3\uff08\u53bb\u58f0\u8c03\uff09\n   =========================== *\/\nfunction normalizePinyin(s){\n  if(!s) return '';\n  \/\/ \u5c06\u97f3\u8c03\u53bb\u6389\uff08Unicode NFD\uff09\uff0c\u628a \u00fc \u53d8 u\n  try{\n    let t = s.normalize('NFD').replace(\/[\\u0300-\\u036f]\/g, '');\n    t = t.replace(\/\u00fc\/g,'u').replace(\/\u01d6|\u01d8|\u01da|\u01dc\/g,'u');\n    return t.toLowerCase();\n  }catch(e){\n    return s.toLowerCase();\n  }\n}\nfunction toast(txt){\n  toastEl.textContent = txt; toastEl.classList.add('show');\n  setTimeout(()=>toastEl.classList.remove('show'),1600);\n}\n\n\/* ===========================\n   \u6e32\u67d3\u7b14\u753b bank\uff08\u6309\u94ae\uff09\n   =========================== *\/\nconst STROKE_TYPES = ['\u6a2a','\u7ad6','\u6487','\u637a','\u70b9','\u63d0','\u6298','\u94a9'];\nfunction renderBank(){\n  bankEl.innerHTML = '';\n  const lvl = LEVELS[state.levelIdx];\n  \/\/ Render counts \u2014 only available strokes in this level\n  STROKE_TYPES.forEach(type=>{\n    const total = lvl.available[type] || 0;\n    if(total <= 0) return;\n    const used = state.usedCounts[type] || 0;\n    const left = Math.max(0, total - used);\n    const btn = document.createElement('div');\n    btn.className='strokeBtn';\n    btn.innerHTML = `<div style=\"font-weight:700\">${type}<\/div><div class=\"count\">${left} \u53ef\u7528<\/div>`;\n    if(left <= 0) btn.setAttribute('disabled','');\n    \/\/ support touch and mouse\n    btn.addEventListener('click', ()=>addStroke(type));\n    btn.addEventListener('touchstart', (e)=>{ e.preventDefault(); addStroke(type); });\n    bankEl.appendChild(btn);\n  });\n  updateComposableCount();\n}\n\n\/* ===========================\n   addStroke\n   =========================== *\/\nfunction addStroke(type){\n  state.usedCounts[type] = (state.usedCounts[type] || 0) + 1;\n  saveState();\n  renderBank();\n  detectComposed();\n}\n\n\/* ===========================\n   \u5224\u65ad\u80fd\u5426\u7ec4\u6210\u67d0\u5b57\uff08need\u662f\u5bf9\u8c61\uff09\n   =========================== *\/\nfunction canCompose(have, need){\n  for(const k in need){\n    if((have[k]||0) < need[k]) return false;\n  }\n  return true;\n}\nfunction keyOf(levelIdx, target){ return `L${LEVELS[levelIdx].id}-${target.char}`; }\n\n\/* ===========================\n   \u68c0\u6d4b\u5e76\u663e\u793a\u534a\u900f\u660e\u5b57 &#038; \u51fa\u9898\n   =========================== *\/\nfunction detectComposed(){\n  const lvl = LEVELS[state.levelIdx];\n  const have = state.usedCounts || {};\n  const unsolved = lvl.targets.filter(t=> !state.solved[keyOf(state.levelIdx,t)]);\n  for(const t of unsolved){\n    if(canCompose(have, t.strokes)){\n      overlay.textContent = t.char;\n      overlay.style.display='block';\n      showQuiz(t);\n      updateComposableCount();\n      return;\n    }\n  }\n  overlay.style.display = 'none';\n  hideQuiz();\n  updateComposableCount();\n}\n\n\/* ===========================\n   \u663e\u793a\/\u9690\u85cf quiz\n   =========================== *\/\nfunction showQuiz(target){\n  quizEl.innerHTML = ''; quizEl.style.display = 'flex';\n  const opts = makeOptions(target.pinyin);\n  opts.forEach(p=>{\n    const b = document.createElement('button');\n    b.textContent = p;\n    b.className = 'btn';\n    \/\/ support both events\n    b.addEventListener('click', ()=>answerHandler(target,p,b));\n    b.addEventListener('touchstart', (e)=>{ e.preventDefault(); answerHandler(target,p,b); });\n    quizEl.appendChild(b);\n  });\n  \/\/ Show correct hint for level 1-6\n  if(LEVELS[state.levelIdx].id <= 6){\n    const hint = document.createElement('div');\n    hint.className = 'smallmuted';\n    hint.style.marginLeft = '8px';\n    hint.style.alignSelf = 'center';\n    hint.textContent = `\uff08\u63d0\u793a\uff09\u6b63\u786e\u7684\u62fc\u97f3\u662f ${target.pinyin}`;\n    quizEl.appendChild(hint);\n  }\n}\n\n\/* \u9690\u85cf quiz *\/\nfunction hideQuiz(){ quizEl.style.display = 'none'; quizEl.innerHTML = ''; }\n\n\/* \u751f\u6210\u9009\u9879\uff08\u4fdd\u8bc1\u4e0e\u6b63\u786e\u62fc\u97f3\u4e0d\u540c\uff0c\u4e14\u4e0d\u51fa\u73b0\u540c\u97f3\u4e0d\u540c\u8c03\uff09 *\/\nfunction makeOptions(correct){\n  const pool = FAKE_PINYIN_POOL.slice();\n  const correctNorm = normalizePinyin(correct);\n  \/\/ remove items that normalize same as correct\n  const filtered = pool.filter(p=> normalizePinyin(p) !== correctNorm);\n  shuffle(filtered);\n  const wrong = filtered.slice(0,2);\n  const arr = [correct, ...wrong];\n  return shuffle(arr);\n}\n\n\/* Fisher-Yates \u6d17\u724c *\/\nfunction shuffle(a){ for(let i=a.length-1;i>0;i--){ const j=Math.floor(Math.random()*(i+1)); [a[i],a[j]]=[a[j],a[i]] } return a; }\n\n\/* ===========================\n   \u7b54\u9898\u5904\u7406\n   =========================== *\/\nfunction answerHandler(target, selected, btnEl){\n  \/\/ disable all buttons while processing\n  quizEl.querySelectorAll('button').forEach(b=>b.disabled=true);\n  const k = keyOf(state.levelIdx, target);\n  if(selected === target.pinyin){\n    btnEl.classList.add('correct');\n    state.solved[k] = true;\n    const times = state.rewardTimes[k] || 0;\n    if(times < 3){\n      addCoins(1);\n      state.rewardTimes[k] = times + 1;\n      toast(`\u56de\u7b54\u6b63\u786e +1 \u91d1\u5e01\uff08\u672c\u9898\u5956\u52b1 ${state.rewardTimes[k]}\/3\uff09`);\n    } else {\n      msgEl.textContent = '\u56de\u7b54\u6b63\u786e\uff0c\u4f46\u672c\u9898\u5956\u52b1\u5df2\u8fbe\u4e0a\u9650\uff08\u6700\u591a 3 \u6b21\uff09\u3002';\n    }\n    saveState();\n    \/\/ \u82e5\u672c\u5173\u5168\u90e8\u5b8c\u6210 -> \u5956\u52b1\u5173\u5361\u5956\u52b1\n    const allDone = LEVELS[state.levelIdx].targets.every(t=> state.solved[keyOf(state.levelIdx,t)]);\n    if(allDone){\n      addCoins(LEVELS[state.levelIdx].reward);\n      toast(`\u672c\u5173\u5b8c\u6210\uff0c\u989d\u5916\u5956\u52b1 +${LEVELS[state.levelIdx].reward} \u91d1\u5e01`);\n      enableUpgradeIfAffordable();\n    } else {\n      msgEl.textContent = '\u7b54\u5bf9\u4e86\uff01\u7ee7\u7eed\u5b8c\u6210\u672c\u5173\u5176\u4ed6\u5b57\u3002';\n    }\n  } else {\n    btnEl.classList.add('wrong');\n    addCoins(-1);\n    toast('\u62fc\u97f3\u4e0d\u5bf9\uff0c\u5df2\u6263 1 \u91d1\u5e01');\n    saveState();\n  }\n  \/\/ after answered, remove overlay (so player can continue) \u2014 keep solved state persists\n  overlay.style.display = 'none';\n  hideQuiz();\n  renderBank();\n  renderSolvedCounts();\n}\n\n\/* ===========================\n   \u91d1\u5e01 \/ \u5b58\u6863 \/ \u6bcf\u65e5\u9886\u53d6\n   =========================== *\/\nfunction addCoins(n){\n  state.coins = Math.max(0, (state.coins || 0) + n);\n  coinDisplay.textContent = state.coins;\n  saveState();\n  enableUpgradeIfAffordable();\n}\nfunction claimDaily(){\n  const today = (new Date()).toISOString().slice(0,10);\n  if(state.lastDaily === today){ toast('\u4eca\u5929\u5df2\u9886\u53d6\u8fc7\u6bcf\u65e5\u91d1\u5e01'); return; }\n  addCoins(1);\n  state.lastDaily = today;\n  saveState();\n  toast('\u5df2\u9886\u53d6\u6bcf\u65e5 +1 \u91d1\u5e01');\n}\n\n\/* ===========================\n   \u8ba1\u7b97\u53ef\u7ec4\u6210\u5b57\u6570\u91cf & UI \u8f85\u52a9\n   =========================== *\/\nfunction updateComposableCount(){\n  const lvl = LEVELS[state.levelIdx];\n  const have = state.usedCounts || {};\n  let cnt = 0;\n  lvl.targets.forEach(t=>{ if(canCompose(have,t.strokes) && !state.solved[keyOf(state.levelIdx,t)]) cnt++; });\n  composableCountEl.textContent = cnt;\n}\nfunction renderSolvedCounts(){\n  const lvl = LEVELS[state.levelIdx];\n  const solvedCnt = lvl.targets.filter(t=> state.solved[keyOf(state.levelIdx,t)]).length;\n  solvedCountEl.textContent = solvedCnt;\n  totalCountEl.textContent = lvl.targets.length;\n}\n\n\/* ===========================\n   \u5347\u7ea7\u6d41\u7a0b\n   =========================== *\/\nfunction enableUpgradeIfAffordable(){\n  const lvl = LEVELS[state.levelIdx];\n  const allDone = LEVELS[state.levelIdx].targets.every(t=> state.solved[keyOf(state.levelIdx,t)]);\n  upgradeTextEl.textContent = lvl.upgradeText || '\u2014';\n  if(allDone){\n    upgradeBtn.disabled = !(state.coins >= (lvl.upgradeCost||0));\n    upgradeBtn.textContent = `\u786e\u8ba4\u5347\u7ea7\uff08\u6d88\u8017 ${lvl.upgradeCost||0} \u91d1\u5e01\uff09`;\n    if(state.coins >= (lvl.upgradeCost||0)) upgradeBtn.style.opacity=1; else upgradeBtn.style.opacity=0.6;\n  } else {\n    upgradeBtn.disabled = true;\n    upgradeBtn.textContent = `\u5b8c\u6210\u6240\u6709\u9898\u4ee5\u89e3\u9501\u5347\u7ea7\uff08\u9700 ${lvl.upgradeCost||0} \u91d1\u5e01\uff09`;\n    upgradeBtn.style.opacity=0.6;\n  }\n}\nupgradeBtn.addEventListener('click', confirmUpgrade);\n\n\/* \u5347\u7ea7\u786e\u8ba4 *\/\nfunction confirmUpgrade(){\n  const lvl = LEVELS[state.levelIdx];\n  \/\/ require all done\n  const allDone = lvl.targets.every(t=> state.solved[keyOf(state.levelIdx,t)]);\n  if(!allDone){ toast('\u8bf7\u5148\u5b8c\u6210\u672c\u5173\u6240\u6709\u5b57\u540e\u518d\u5347\u7ea7'); return; }\n  if(state.coins < (lvl.upgradeCost||0)){ toast('\u91d1\u5e01\u4e0d\u8db3'); return; }\n  addCoins(- (lvl.upgradeCost||0));\n  \/\/ apply upgrade visuals\n  if(typeof lvl.applyUpgrade === 'function') lvl.applyUpgrade();\n  if(!state.levelUpgraded.includes(lvl.id)) state.levelUpgraded.push(lvl.id);\n  \/\/ clear used strokes for new level\n  state.usedCounts = {};\n  \/\/ move to next level\n  if(state.levelIdx < LEVELS.length - 1){\n    state.levelIdx++;\n    saveState();\n    bootLevel();\n    toast('\u5347\u7ea7\u5b8c\u6210\uff0c\u8fdb\u5165\u4e0b\u4e00\u5173\uff01');\n  } else {\n    \/\/ finished all levels\n    saveState();\n    toast('\u606d\u559c\u5b8c\u6210\u5168\u90e8\u5173\u5361\uff01\u53ef\u8d2d\u4e70\u7ed3\u4e1a\u8bc1\u4e66\u3002');\n  }\n  enableUpgradeIfAffordable();\n  renderBank();\n}\n\n\/* ===========================\n   \u90e8\u4ef6\u663e\u793a\u8f85\u52a9\n   =========================== *\/\nfunction togglePart(id, on){\n  const el = document.getElementById(id);\n  if(!el) return;\n  el.style.opacity = on ? 1 : 0;\n}\n\n\/* ===========================\n   \u6e05\u7a7a\u5c4b\u5b50\uff08\u4e0d\u4f1a\u6263\u91d1\u5e01\uff09\n   =========================== *\/\nclearBtn.addEventListener('click', ()=>{\n  state.usedCounts = {};\n  saveState();\n  renderBank();\n  overlay.style.display='none';\n  hideQuiz();\n  toast('\u5df2\u6e05\u7a7a\u5c4b\u5b50\uff08\u4e0d\u6263\u91d1\u5e01\uff09');\n});\n\n\/* \u91cd\u7f6e\u5168\u90e8 *\/\nresetBtn.addEventListener('click', ()=>{\n  if(!confirm('\u786e\u8ba4\u8981\u91cd\u7f6e\u6240\u6709\u8fdb\u5ea6\u5417\uff1f')) return;\n  localStorage.removeItem(STORAGE_KEY);\n  location.reload();\n});\n\n\/* \u6bcf\u65e5\u6309\u94ae *\/\ndailyBtn.addEventListener('click', claimDaily);\n\n\/* ===========================\n   \u542f\u52a8\/bootLevel\uff08\u6062\u590d\u8fdb\u5ea6\/\u6e32\u67d3\uff09\n   =========================== *\/\nfunction bootLevel(){\n  if(state.levelIdx >= LEVELS.length) state.levelIdx = LEVELS.length - 1;\n  levelLabel.textContent = LEVELS[state.levelIdx].id;\n  coinDisplay.textContent = state.coins || 0;\n  \/\/ reset all parts first\n  ['lot','columns','beam','rafters','roof','walls','plaster','doorsWindows','details','painted','extras'].forEach(id=>togglePart(id,false));\n  \/\/ apply previously upgraded levels\n  (state.levelUpgraded||[]).forEach(lid=>{\n    const found = LEVELS.find(x=>x.id===lid);\n    if(found && typeof found.applyUpgrade === 'function') found.applyUpgrade();\n  });\n  renderBank();\n  overlay.style.display = 'none';\n  hideQuiz();\n  msgEl.textContent = '\u968f\u610f\u70b9\u7b14\u753b\uff0c\u82e5\u51fa\u73b0\u534a\u900f\u660e\u5b57\u518d\u4f5c\u7b54\u62fc\u97f3\uff08\u7b54\u9519\u624d\u6263\u91d1\u5e01\uff09\u3002';\n  renderSolvedCounts();\n  enableUpgradeIfAffordable();\n  \/\/ if finished all levels, allow certificate purchase button if coins enough\n  checkCertAvailability();\n}\n\n\/* ===========================\n   \u8bc1\u4e66\uff08\u8d2d\u4e70\/\u751f\u6210\/\u53d1\u9001\uff09\n   =========================== *\/\nfunction checkCertAvailability(){\n  \/\/ enable buyCertBtn only when completed last level (upgraded?) and player has >=5 coins\n  const finished = state.levelIdx >= LEVELS.length - 1 && LEVELS[LEVELS.length-1].targets.every(t=> state.solved[`L${LEVELS.length}-${t.char}`]);\n  buyCertBtn.disabled = !(finished && state.coins >= 5);\n}\nbuyCertBtn.addEventListener('click', ()=>{\n  certModal.style.display = 'flex';\n});\ncertCancel.addEventListener('click', ()=>{ certModal.style.display='none'; });\n\ncertConfirm.addEventListener('click', ()=>{\n  const name = certNameInput.value.trim() || '\u5b66\u751f';\n  const school = certSchoolInput.value.trim() || '';\n  \/\/ cost check\n  if(state.coins < 5){ alert('\u91d1\u5e01\u4e0d\u8db3 5 \u679a\uff0c\u65e0\u6cd5\u8d2d\u4e70\u8bc1\u4e66'); return; }\n  \/\/ deduct coins\n  addCoins(-5);\n  \/\/ generate certificate\n  generateAndSendCertificate(name, school, certClassInput.value.trim() || '');\n  certModal.style.display = 'none';\n  saveState();\n});\n\n\/* \u751f\u6210\u8bc1\u4e66\u5e76\u4e0b\u8f7d + \u901a\u8fc7 EmailJS \u53d1\u9001\u5907\u4efd\u7ed9\u7ba1\u7406\u5458 *\/\nfunction generateAndSendCertificate(name, school, klass){\n  const canvas = certCanvas;\n  const ctx = canvas.getContext('2d');\n  \/\/ background\n  ctx.fillStyle = '#fff'; ctx.fillRect(0,0,canvas.width,canvas.height);\n  \/\/ border\n  ctx.strokeStyle = '#000'; ctx.lineWidth = 8;\n  ctx.strokeRect(20,20,canvas.width-40,canvas.height-40);\n  \/\/ logo (if exists)\n  const logo = document.getElementById('logo');\n  \/\/ draw logo if loaded else skip\n  try{\n    if(logo &#038;&#038; logo.complete){\n      ctx.drawImage(logo, 80, 60, 160, 160);\n    }\n  }catch(e){}\n  \/\/ title\n  ctx.font = 'bold 48px KaiTi, \"\u6977\u4f53\", STKaiti, serif';\n  ctx.fillStyle = '#000';\n  ctx.textAlign = 'center';\n  ctx.fillText('\u8363\u8a89\u8bc1\u4e66', canvas.width\/2, 120);\n  \/\/ body\n  ctx.textAlign = 'left';\n  ctx.font = '28px KaiTi, \"\u6977\u4f53\", serif';\n  ctx.fillText(`\u606d\u559c ${name} \u540c\u5b66\u6210\u529f\u5efa\u9020\u4e86\u6700\u9ad8\u7ea7\u7684\u522b\u5885\uff01`, 80, 260);\n  ctx.fillText(`\u5b66\u6821\uff1a${school}`, 80, 320);\n  ctx.fillText(`\u73ed\u7ea7\uff1a${klass}`, 80, 360);\n  \/\/ \u6210\u7ee9\u7edf\u8ba1\uff1a\u7edf\u8ba1\u6b63\u786e\u6570 = all solved across levels\n  const totalTargets = LEVELS.reduce((s,l)=> s + l.targets.length, 0);\n  const correctCount = Object.keys(state.solved || {}).length;\n  ctx.fillText(`\u7b54\u5bf9\u9898\u6570\uff1a${correctCount} \/ ${totalTargets}`, 80, 420);\n  ctx.fillText(`\u65e5\u671f\uff1a${new Date().toLocaleDateString()}`, 80, 480);\n\n  \/\/ \u751f\u6210 base64\n  const base64 = canvas.toDataURL('image\/jpeg', 0.9);\n\n  \/\/ \u4e0b\u8f7d\n  const a = document.createElement('a');\n  a.href = base64;\n  a.download = `kawan4u_certificate_${name.replace(\/\\s+\/g,'_')}.jpg`;\n  document.body.appendChild(a);\n  a.click();\n  a.remove();\n  toast('\u8bc1\u4e66\u5df2\u751f\u6210\u5e76\u5f00\u59cb\u4e0b\u8f7d\uff08\u540c\u6837\u4f1a\u5907\u4efd\u5230\u7ba1\u7406\u5458\u90ae\u7bb1\uff09');\n\n  \/\/ \u53d1\u9001\u5230 EmailJS\uff1a\u628a base64 \u653e\u5165 template \u53c2\u6570\n  const params = {\n    student_name: name,\n    student_school: school,\n    student_class: klass,\n    certificate_base64: base64\n  };\n  emailjs.send(SERVICE_ID, TEMPLATE_ID, params)\n    .then(()=>{ console.log('EmailJS: \u8bc1\u4e66\u5df2\u53d1\u9001\u5907\u4efd'); })\n    .catch(err=>{ console.error('EmailJS \u53d1\u9001\u5931\u8d25', err); alert('\u8bc1\u4e66\u90ae\u4ef6\u53d1\u9001\u5931\u8d25\uff0c\u8bf7\u68c0\u67e5 EmailJS \u914d\u7f6e'); });\n}\n\n\/* ===========================\n   \u6e32\u67d3\/\u66f4\u65b0\u7edf\u8ba1 & \u68c0\u67e5 compose\n   =========================== *\/\nfunction renderBankAndUI(){\n  renderBank();\n  renderSolvedCounts();\n  coinDisplay.textContent = state.coins || 0;\n  levelLabel.textContent = LEVELS[state.levelIdx].id;\n  updateComposableCount();\n  enableUpgradeIfAffordable();\n  checkCertAvailability();\n}\n\n\/* ===========================\n   \u4e8b\u4ef6\uff1a\u6e05\u7a7a\/\u68c0\u6d4b\/\u62d6\u653e\uff08\u8fd9\u91cc\u7528\u70b9\u6309\u5f62\u5f0f\uff09\n   =========================== *\/\n\/* allow keyboard reset for debugging *\/\ndocument.addEventListener('keydown', (e)=>{ if(e.key === 'r') { localStorage.removeItem(STORAGE_KEY); location.reload(); } });\n\n\/* ===========================\n   \u5165\u53e3\uff1a\u9996\u6b21\u6e32\u67d3\n   =========================== *\/\nrenderBankAndUI();\nbootLevel();\n\n\/* expose some helpers to console for debugging *\/\nwindow._kawan_state = state;\nwindow._levels = LEVELS;\n\n<\/script>\n<\/body>\n<\/html>\n\n","protected":false},"excerpt":{"rendered":"<p>\u5efa\u522b\u5885 \u2014 Kawan4u \u6c49\u5b57\u6e38\u620f\uff081\u201318 \u5173\u5b8c\u6574\u7248\uff09 \u5efa\u522b\u5885 \u5efa\u522b\u5885 \u2014 \u5b66\u6c49\u5b57\u62fc\u97f3\uff081\u201318 \u5173\uff09 \u70b9\u7b14\u753b \u2192 \u5f53\u5c4b\u524d\u51fa\u73b0\u534a\u900f\u660e\u5b57 \u2192 \u9009\u62fc\u97f3\u5f97\u91d1\u5e01 \u5173\u5361 1 \u91d1\u5e01 0 \u6bcf\u65e5 [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"site-sidebar-layout":"default","site-content-layout":"","ast-site-content-layout":"default","site-content-style":"default","site-sidebar-style":"default","ast-global-header-display":"","ast-banner-title-visibility":"","ast-main-header-display":"","ast-hfb-above-header-display":"","ast-hfb-below-header-display":"","ast-hfb-mobile-header-display":"","site-post-title":"","ast-breadcrumbs-content":"","ast-featured-img":"","footer-sml-layout":"","ast-disable-related-posts":"","theme-transparent-header-meta":"","adv-header-id-meta":"","stick-header-meta":"","header-above-stick-meta":"","header-main-stick-meta":"","header-below-stick-meta":"","astra-migrate-meta-layouts":"default","ast-page-background-enabled":"default","ast-page-background-meta":{"desktop":{"background-color":"var(--ast-global-color-5)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"ast-content-background-meta":{"desktop":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"tablet":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""},"mobile":{"background-color":"var(--ast-global-color-4)","background-image":"","background-repeat":"repeat","background-position":"center center","background-size":"auto","background-attachment":"scroll","background-type":"","background-media":"","overlay-type":"","overlay-color":"","overlay-opacity":"","overlay-gradient":""}},"footnotes":""},"class_list":["post-159","page","type-page","status-publish","hentry"],"_links":{"self":[{"href":"https:\/\/kawan4u.com\/index.php?rest_route=\/wp\/v2\/pages\/159","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/kawan4u.com\/index.php?rest_route=\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/kawan4u.com\/index.php?rest_route=\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/kawan4u.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/kawan4u.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=159"}],"version-history":[{"count":1,"href":"https:\/\/kawan4u.com\/index.php?rest_route=\/wp\/v2\/pages\/159\/revisions"}],"predecessor-version":[{"id":160,"href":"https:\/\/kawan4u.com\/index.php?rest_route=\/wp\/v2\/pages\/159\/revisions\/160"}],"wp:attachment":[{"href":"https:\/\/kawan4u.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=159"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}