From 4c967233ad85a37ef33313eba86f0b7b6b3c6c55 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 10 Sep 2026 07:36:35 +0000 Subject: [PATCH 1/2] =?UTF-8?q?=E6=95=B4=E7=90=86=20README=EF=BC=8C.env.ex?= =?UTF-8?q?ample=20=E8=88=87=E7=A8=8B=E5=BC=8F=E7=A2=BC=E9=9B=99=E5=90=91?= =?UTF-8?q?=E5=B0=8D=E9=BD=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit .env.example 補上 OG_PROXY_SECRET(zg 代理拿它簽節點網址,程式碼讀得到但範例沒寫)。 OG_STORAGE_UPLOAD_URL 的說明過時了 —— 現在預設是玩家用自己的錢包上傳, 那個 gateway 是「想讓玩家不用付儲存費」才要的選項,不是「沒設就只產生摘要」。 末尾註明 proxy/ 那台服務的變數設在它自己的專案上,不在這裡。 一律留空值,不放任何真實金鑰。 雙向交叉檢查都乾淨:程式碼讀得到的 13 個變數範例裡都有,範例裡也沒有 程式碼不讀的殘留變數。 README · 部署章節新增「賽道二還需要一台 Node 代理」—— 這步不做「存進 0G Storage」 必定失敗,之前完全沒寫。附上兩行指令與原因(error 1003 / 521)。 · 方式 B 的部署指令補上 --branch main,並註明少了它會部署成 Preview、 正式網址不會更新(我們為此耗掉一整輪除錯)。 · 環境變數表補 OG_ZG_PROXY_BASE(標成「上傳必要」)與 OG_PROXY_SECRET, 修正 OG_STORAGE_UPLOAD_URL 的必要性描述。 · 檔案配置補上這幾天新增的:404.html、_headers、_routes.json、vendor/、 public/js/storage.js、functions/api/og/zg/、proxy/、slides/。 · 安全性那條「0G Storage 的寫入交給外部 gateway」已經不成立 —— 改成寫入 全程走玩家錢包,並新增一條說明節點代理為什麼不是開放代理(HMAC 簽章、 內網過濾、不轉 cookie/authorization)。 行號連結 14 個逐一驗證,5 個在這幾天的改動後跑掉了: public/js/storage.js#L65-L108 → #L72-L116(upload 整支) functions/api/og/storage.js#L76-L114 → #L117-L159(原本指到 unwrap/seqOf) functions/api/og/status.js#L59-L150 → #L59-L157 public/js/og.js#L174-L199 → #L235-L260(原本指到 connect 中段) public/js/og.js#L116-L159 → #L167-L220(原本指到 currentAccount) 現在 14 個全部落在正確函式的起訖行上。 順手更正 storage.js 裡一句過時註解:「刻意忽略呼叫端傳進來的真實 indexer 網址」 —— 呼叫端現在傳的是 proxyBase,不是 indexer。 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_015NBxaWtzqEX3i8NvoZEeq3 --- .env.example | 9 ++++++ README.md | 74 +++++++++++++++++++++++++++++++++++--------- public/js/storage.js | 5 +-- 3 files changed, 71 insertions(+), 17 deletions(-) diff --git a/.env.example b/.env.example index da11c54..f6ac0ab 100644 --- a/.env.example +++ b/.env.example @@ -33,6 +33,10 @@ OG_STORAGE_INDEXER=https://indexer-storage-testnet-turbo.0g.ai # 不設的話前端走同源的 /api/og/zg,在 Cloudflare 上會失敗。 OG_ZG_PROXY_BASE= +# 代理用來簽節點網址的金鑰。擋的是「有人拿這個網域當跳板」,不是機密資料, +# 沒設會用內建常數。要更嚴的話這裡與 proxy 那台服務要設成同一個值。 +OG_PROXY_SECRET= + # 選用:想讓玩家完全不用付儲存費的話,自己架一個 0g-storage-client gateway, # 設了這個端點就改由 Function 轉發上傳(那台 gateway 才需要持有私鑰)。 OG_STORAGE_UPLOAD_URL= @@ -41,3 +45,8 @@ OG_STORAGE_TOKEN= # ══ 賽道三:0G Chain(Galileo 測試網 16602)══════════════════════ # 不設就用公開節點。錨定交易由玩家自己的錢包送出,伺服器不保管任何私鑰。 OG_RPC_URL=https://evmrpc-testnet.0g.ai + +# ══ 代理服務(proxy/)的變數不在這裡 ═══════════════════════════ +# 那是另一個部署(Vercel),變數設在它自己的專案上: +# OG_PROXY_SECRET / OG_STORAGE_INDEXER / ALLOWED_ORIGINS / PUBLIC_BASE_URL +# 說明見 proxy/README.md。 diff --git a/README.md b/README.md index f6d5881..28649a8 100644 --- a/README.md +++ b/README.md @@ -61,12 +61,12 @@ |---|---|---| | **0G Compute Network**
TEE 可驗證推論 | 戰報要永久存檔,寫它的那個 agent 就不能是黑箱。金鑰在 pc.0g.ai 選 **Private(TEE enclave)** 開的,推論實際跑在 enclave 裡,不是只呼叫一個 OpenAI 相容端點。 | [`functions/api/narrate.js#L55-L134`](https://github.com/ConsssLab/web/blob/main/functions/api/narrate.js#L55-L134) | | **0G Compute Network**
Router 設定 | 敵方 agent 與旁白 agent 共用同一份供應商工廠,把 `AI_PROVIDER` 改成 `0g` 就能整支切過去,不用改程式碼。 | [`functions/api/og/_shared.js#L22-L23`](https://github.com/ConsssLab/web/blob/main/functions/api/og/_shared.js#L22-L23) · [`functions/api/og/_shared.js#L82-L90`](https://github.com/ConsssLab/web/blob/main/functions/api/og/_shared.js#L82-L90) | -| **0G Storage**
真實寫入 | 記憶碎片要「永久保存」就必須真的落地。用官方 SDK 走完整協議:切 256-byte chunk 算 merkle root → 對 Flow 合約送 submit(付儲存費)→ 把 segment 傳給 storage node。**全程用玩家自己的錢包簽,伺服器不持有私鑰。** | [`public/js/storage.js#L65-L108`](https://github.com/ConsssLab/web/blob/main/public/js/storage.js#L65-L108)
節點代理(Node):[`proxy/api/index.js`](https://github.com/ConsssLab/web/blob/main/proxy/api/index.js) —— 節點是裸 IP + 非標準埠,Cloudflare Workers 打不到(error 1003 / 521),這一段必須跑在 Node 上 | -| **0G Storage**
indexer 唯讀查詢 | 「上傳沒報錯」不等於存進去了。拿 root hash 回頭問 indexer,確認 storage node 真的收下並 finalized 才敢標成已存檔。唯讀、不需金鑰,評審可自行查證。 | [`functions/api/og/storage.js#L76-L114`](https://github.com/ConsssLab/web/blob/main/functions/api/og/storage.js#L76-L114) | +| **0G Storage**
真實寫入 | 記憶碎片要「永久保存」就必須真的落地。用官方 SDK 走完整協議:切 256-byte chunk 算 merkle root → 對 Flow 合約送 submit(付儲存費)→ 把 segment 傳給 storage node。**全程用玩家自己的錢包簽,伺服器不持有私鑰。** | [`public/js/storage.js#L72-L116`](https://github.com/ConsssLab/web/blob/main/public/js/storage.js#L72-L116)
節點代理(Node):[`proxy/api/index.js`](https://github.com/ConsssLab/web/blob/main/proxy/api/index.js) —— 節點是裸 IP + 非標準埠,Cloudflare Workers 打不到(error 1003 / 521),這一段必須跑在 Node 上 | +| **0G Storage**
indexer 唯讀查詢 | 「上傳沒報錯」不等於存進去了。拿 root hash 回頭問 indexer,確認 storage node 真的收下並 finalized 才敢標成已存檔。唯讀、不需金鑰,評審可自行查證。 | [`functions/api/og/storage.js#L117-L159`](https://github.com/ConsssLab/web/blob/main/functions/api/og/storage.js#L117-L159) | | **0G Storage**
節點活性探測 | 結果畫面的燈號要照實反映網路狀態,不能寫死成綠燈。 | [`functions/api/og/status.js#L34-L57`](https://github.com/ConsssLab/web/blob/main/functions/api/og/status.js#L34-L57) | -| **0G Chain**
錨定寫入 | 戰報需要一個不可竄改、有時間戳的存在證明。40 bytes 結構化 calldata(魔術字 `CSSW` + 版本 + 戰績 + SHA-256),用合約建立交易送出,chainscan 上一眼認得出來。 | [`functions/api/og/shard.js#L62-L84`](https://github.com/ConsssLab/web/blob/main/functions/api/og/shard.js#L62-L84) · [`public/js/og.js#L174-L199`](https://github.com/ConsssLab/web/blob/main/public/js/og.js#L174-L199) | +| **0G Chain**
錨定寫入 | 戰報需要一個不可竄改、有時間戳的存在證明。40 bytes 結構化 calldata(魔術字 `CSSW` + 版本 + 戰績 + SHA-256),用合約建立交易送出,chainscan 上一眼認得出來。 | [`functions/api/og/shard.js#L62-L84`](https://github.com/ConsssLab/web/blob/main/functions/api/og/shard.js#L62-L84) · [`public/js/og.js#L235-L260`](https://github.com/ConsssLab/web/blob/main/public/js/og.js#L235-L260) | | **0G Chain**
鏈上回驗 | **這是整個專案的重點**:按了按鈕、錢包沒報錯,不代表資料真的在鏈上。所以再用 `eth_getTransactionByHash` 把交易讀回來、反解 calldata、跟本地碎片逐欄比對,四項全對才打勾。 | [`functions/api/og/verify.js#L19-L47`](https://github.com/ConsssLab/web/blob/main/functions/api/og/verify.js#L19-L47) · [`functions/api/og/verify.js#L49-L104`](https://github.com/ConsssLab/web/blob/main/functions/api/og/verify.js#L49-L104) | -| **0G Chain**
鏈況與網路切換 | 標題頁即時顯示 Galileo 區塊高度;chainId **從鏈上實際讀回來**校準而不是寫死(0G 換過 chain ID 16601→16602,寫死會讓切鏈整個失敗)。 | [`functions/api/og/status.js#L59-L150`](https://github.com/ConsssLab/web/blob/main/functions/api/og/status.js#L59-L150) · [`public/js/og.js#L116-L159`](https://github.com/ConsssLab/web/blob/main/public/js/og.js#L116-L159) | +| **0G Chain**
鏈況與網路切換 | 標題頁即時顯示 Galileo 區塊高度;chainId **從鏈上實際讀回來**校準而不是寫死(0G 換過 chain ID 16601→16602,寫死會讓切鏈整個失敗)。 | [`functions/api/og/status.js#L59-L157`](https://github.com/ConsssLab/web/blob/main/functions/api/og/status.js#L59-L157) · [`public/js/og.js#L167-L220`](https://github.com/ConsssLab/web/blob/main/public/js/og.js#L167-L220) | | — **敵方 AI agent**
(OpenAI,非 0G) | 對戰時每回合都要叫一次,需要低延遲,所以另外走 OpenAI。模型輸出一律當不可信資料重新過濾。 | [`functions/api/agent.js#L173-L256`](https://github.com/ConsssLab/web/blob/main/functions/api/agent.js#L173-L256) · [`public/js/ai.js#L16-L65`](https://github.com/ConsssLab/web/blob/main/public/js/ai.js#L16-L65) | ### 評審可以自己打的端點 @@ -167,9 +167,12 @@ Dashboard → **Workers & Pages** → **Create** → **Pages** → **Connect to ```bash npx wrangler login -npx wrangler pages deploy public --project-name conssswars-web +npx wrangler pages deploy public --project-name conssswars-web --branch main ``` +> `--branch main` 不能省 —— 少了它會部署成 Preview,只拿得到一個 hash 網址, +> 正式網址不會更新。 + ### 方式 C · GitHub Actions(推 main 就自動部署) [`.github/workflows/deploy.yml`](.github/workflows/deploy.yml) 已經寫好。 @@ -182,6 +185,24 @@ npx wrangler pages deploy public --project-name conssswars-web > A 和 C 擇一即可,兩個都開會讓同一個 commit 部署兩次。 +### 賽道二還需要一台 Node 代理 + +**這一步不做的話,「存進 0G Storage」一定失敗。** + +0G 的 storage node 是**裸 IP + 明文 http + 5678 埠**(實測 `http://34.19.125.196:5678`)。 +Cloudflare Workers 兩樣都打不到 —— 對裸 IP 會回 `error 1003`,非標準埠會回 `error 521` —— +所以「把 segment 轉發給節點」這一小段必須跑在一般 Node 上。 + +```bash +cd proxy && npx vercel --prod # 記下輸出的 Production 網址 +cd .. && npx wrangler pages secret put OG_ZG_PROXY_BASE --project-name conssswars-web +# 貼上:https://<剛剛的網址>/api +``` + +其餘部分照舊留在 Cloudflare。前端不寫死代理網址 —— `/api/og/status` 會把 +`OG_ZG_PROXY_BASE` 當成 `storage.zgProxy` 下發,換代理只要改這個變數。 +完整說明在 [proxy/README.md](proxy/README.md)。 + ### 綁定 web.conssswars.com 專案 → **Custom domains** → **Set up a custom domain** → 輸入 `web.conssswars.com`。 @@ -206,10 +227,15 @@ API 金鑰請選 **Secret**(加密),不要用一般變數。 | `OG_COMPUTE_API_KEY` | 賽道一 | 建議設 | 0G Compute Router 金鑰,從 [pc.0g.ai](https://pc.0g.ai) 取得 | | `OG_COMPUTE_MODEL` | 賽道一 | 選用 | 預設 `deepseek-chat-v3-0324` | | `OG_STORAGE_INDEXER` | 賽道二 | 選用 | 預設 Turbo indexer;唯讀查詢不需金鑰 | -| `OG_STORAGE_UPLOAD_URL` | 賽道二 | 上傳才需要 | 指向 `0g-storage-client` gateway;沒設就只產生摘要 | +| `OG_ZG_PROXY_BASE` | 賽道二 | **上傳必要** | 節點代理的網址(結尾要有 `/api`)。沒設的話前端會走同源的 `/api/og/zg`,那條路在 Cloudflare 上必定失敗 —— 見上面「賽道二還需要一台 Node 代理」 | +| `OG_PROXY_SECRET` | 賽道二 | 選用 | 代理用來簽節點網址的金鑰。擋的是「有人拿這個網域當跳板」,不是機密資料;沒設會用內建常數。要更嚴的話**主站與 proxy 兩邊要設成同一個值** | +| `OG_STORAGE_UPLOAD_URL` | 賽道二 | 選用 | 想讓玩家不用付儲存費才需要:指向自架的 `0g-storage-client` gateway,改由 Function 轉發上傳。預設是玩家用自己的錢包上傳,不需要設 | | `OG_STORAGE_TOKEN` | 賽道二 | 選用 | 上述 gateway 需要的 Bearer token | | `OG_RPC_URL` | 賽道三 | 選用 | 預設 `https://evmrpc-testnet.0g.ai` | +代理那台服務有自己的環境變數(`ALLOWED_ORIGINS`、`PUBLIC_BASE_URL` 等), +列在 [proxy/README.md](proxy/README.md)。 + **一個金鑰都沒設也能玩** —— AI agent 退到本地啟發式對手,畫面上照實標示 「本地備援(未接上模型)」,不會假裝有接。 @@ -240,30 +266,44 @@ cd public && python3 -m http.server 8788 ``` public/ 建置輸出目錄(直接就是成品,無建置步驟) ├─ index.html 五個畫面:標題 / 劇情 / 簡報 / 戰鬥 / 結果 -├─ images/ logo、demo 縮圖、角色形象照 +├─ 404.html 找不到檔案時的真 404(沒有 SPA 後備,缺檔會現形) +├─ _headers 快取與安全標頭;js/css 每次重新驗證 +├─ _routes.json 哪些路徑交給 Pages Functions(/api/*) +├─ manifest.webmanifest PWA 描述 +├─ images/ logo、角色形象拼版圖 ├─ css/style.css 紙白平塗風,iPhone 直式 390×844 為基準 +├─ vendor/ 釘死版本的第三方 bundle(0G Storage SDK、ethers),按下按鈕才載 └─ js/ ├─ rules.js 純規則層,無 DOM 無網路,可直接跑對戰模擬 ├─ story.js 劇情腳本、角色資料與結局文案 - ├─ art.js 全部美術(inline SVG,現畫,無圖檔) + ├─ art.js 全部美術(inline SVG,現畫)+角色形象照切版 ├─ audio.js 程序化 BGM 與音效(WebAudio,無音檔) ├─ ai.js 敵方 AI agent 前端客戶端 + 離線備援 ├─ og.js 0G 前端整合:錢包、切鏈、錨定、回驗、Storage 查詢 + ├─ storage.js 賽道二:用玩家錢包跑官方 SDK 把碎片寫進 0G Storage └─ main.js 主流程、動畫節奏、0G 三賽道狀態面板 -functions/api/ +functions/api/ Cloudflare Pages Functions ├─ agent.js 敵方 AI agent 大腦(OpenAI,唯一持有該金鑰的地方) ├─ narrate.js 賽道一:0G Compute 上的旁白 agent └─ og/ - ├─ _shared.js 三支共用的網路常數與 JSON-RPC + ├─ _shared.js 共用的網路常數與 JSON-RPC ├─ status.js 三賽道即時狀態 - ├─ shard.js 賽道二寫入 + 賽道三 calldata 組裝 - ├─ storage.js 賽道二唯讀查詢(indexer) - └─ verify.js 賽道三鏈上回驗 + ├─ shard.js 碎片正規化、SHA-256、賽道三 calldata 組裝 + ├─ storage.js 賽道二唯讀查詢(indexer)+ storagescan 連結 + ├─ verify.js 賽道三鏈上回驗 + └─ zg/[[path]].js 節點代理的 Cloudflare 版。留著是因為 0G 之後若改回 + 傳網域名稱它就能用;目前實際生效的是下面那個 + +proxy/ 節點代理(Node,部署到 Vercel) +├─ api/index.js 轉發 indexer 與 storage node,HMAC 簽章綁住目標 +└─ README.md 為什麼這段不能跑在 Cloudflare 上 scripts/ ├─ set-key.sh 互動式寫入 .env 的金鑰 └─ simulate.mjs 對戰模擬,改數值前先跑這個 + +slides/ 給評審的 5 頁簡報(PDF) ``` --- @@ -271,8 +311,12 @@ scripts/ ## 安全性 - **API 金鑰只存在 Function 端。** 前端拿不到,也不會出現在 bundle 裡。 -- **不保管任何私鑰。** 上鏈那步走玩家自己的錢包;0G Storage 的寫入交給外部 gateway, - Worker 裡沒有私鑰可偷。 +- **不保管任何私鑰。** 上鏈與 0G Storage 的寫入都走玩家自己的錢包 —— 算 merkle root、 + 對 Flow 合約送 submit(付儲存費)、傳 segment,整段在瀏覽器完成,伺服器沒有私鑰可偷。 +- **節點代理不是開放代理。** 節點是裸 IP,沒有網域可以白名單,所以改用 HMAC 簽章綁住: + 只有代理自己從 indexer 回應吐出來的網址才轉發得動,外人塞任意網址進來拿 403。 + 另外擋掉內網 / link-local / 雲端 metadata 位址,也不把 `cookie`、`authorization` + 轉給第三方主機。 - **模型輸出一律當成不可信資料。** 只取白名單欄位、比對合法手牌清單、夾在算力預算內 ——[`sanitizePlays()`](functions/api/agent.js),前端 `rules.js` 還會再 validate 一次。 台詞用 `textContent` 進 DOM,不走 `innerHTML`。 diff --git a/public/js/storage.js b/public/js/storage.js index 21d7219..79c5e0c 100644 --- a/public/js/storage.js +++ b/public/js/storage.js @@ -105,8 +105,9 @@ export async function upload(shard, { proxyBase, rpc, onStep } = {}) { const signer = await provider.getSigner(); step('送出 Flow 合約 submit 並上傳 segment…'); - // 這裡刻意忽略呼叫端傳進來的真實 indexer 網址,一律走代理 —— - // 直連的話 segment 那步會被 storage node 的 CORS 擋掉。 + // indexer 一律走代理(proxyBase 由 /api/og/status 下發)。 + // 直連的話 segment 那步會死在節點端:明文 http 是 mixed content, + // 而且那些主機沒為瀏覽器開 CORS。 const client = new zg.Indexer(indexerUrl(proxyBase)); const [tx, err] = await client.upload(data, rpc || DEFAULT_RPC, signer); if (err) throw new Error(String(err && err.message ? err.message : err)); From 84e5811ec5cb60367e2e47e66d0a41983fc5697f Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 10 Sep 2026 07:40:38 +0000 Subject: [PATCH 2/2] =?UTF-8?q?=E6=9B=B4=E6=AD=A3=20README=20=E4=B8=89?= =?UTF-8?q?=E8=99=95=E8=88=87=E7=A8=8B=E5=BC=8F=E7=A2=BC=E4=B8=8D=E7=AC=A6?= =?UTF-8?q?=E7=9A=84=E6=95=98=E8=BF=B0=EF=BC=8CCI=20=E8=A3=9C=E4=B8=8A=20-?= =?UTF-8?q?-branch=3Dmain?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 逐段拿 README 對程式碼查了一次,三處對不上: 1. 「美術與音樂全部是程式現畫、現合成的 —— 沒有任何圖檔」 現在 public/ 底下有 heroes.jpeg(915KB)、logo.svg、favicon.svg。音檔那半是 對的(find 找不到任何 mp3/wav/ogg),圖檔那半已經不成立。改成照實說:音樂 沒有音檔,美術除了三位主角的形象照之外都是 art.js 現畫的 inline SVG, 抽掉形象照照樣能玩。 2. 形象照「單格比例約 454×787」 實際那張是 1361×768,單格 453×768。787 不知道從哪來的,照著它做圖臉會被 縱向拉伸 —— 這正是先前修過的那個 bug。順便寫清楚 CSS 的 aspect-ratio 是 1361 / 2304(整張寬 ÷ 單格高×3),換圖要一起改。 3. 「實測 300 場:71% 落敗,平均 3.7 回合」 重跑五輪:遺忘者勝 176 / 176 / 193 / 203 / 206,也就是落敗 59–69%, 平均 3.7–3.8 回合。71% 復現不出來。模擬用隨機出牌,本來就會抖動, 所以改成寫範圍而不是單一數字。 另外 .github/workflows/deploy.yml 的部署指令沒有 --branch=main。CI 裡 wrangler 會從 git 推斷分支,多半會落在 main,但 README 方式 B 才剛特別強調少了這個參數 會部署成 Preview、正式網址不會更新(我們為此耗掉一整輪)。既然要求別人加, 自己的 workflow 沒理由靠推斷。 查過但沒問題的:規則數字(LANES/TRACK=3、CORE_HP=12、MAX_TURNS=7、 COMPUTE_PER_TURN=2、CONTROL_DAMAGE=2)全部對得上 rules.js;三位角色的名字與 稱號對得上 story.js;npm run dev / sim / deploy 三個 script 都存在; /api/og/verify 不帶 digest 也能用(digestMatch 回 null,不會報錯); sanitizePlays() 確實在 agent.js 裡。 Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_015NBxaWtzqEX3i8NvoZEeq3 --- .github/workflows/deploy.yml | 2 +- README.md | 17 +++++++++++------ 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index cf3bd7b..20099e8 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -47,4 +47,4 @@ jobs: with: apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - command: pages deploy public --project-name=conssswars-web + command: pages deploy public --project-name=conssswars-web --branch=main diff --git a/README.md b/README.md index 28649a8..d42de2f 100644 --- a/README.md +++ b/README.md @@ -102,8 +102,10 @@ curl "https://conssswars-web.pages.dev/api/og/verify?tx=0x<你的交易hash>" | > 所以結果畫面有一顆「鏈上回驗」按鈕,會實際去讀那筆交易的 calldata, > 重新解析、跟本地碎片的 SHA-256 比對,比對相符才敢標成 ✓。 -美術與音樂全部是程式現畫、現合成的 —— 沒有任何圖檔、沒有任何音檔。 -`art.js` 是 inline SVG,`audio.js` 是程序化 WebAudio。 +音樂與音效**沒有任何音檔** —— `audio.js` 是程序化 WebAudio,全部現合成。 +美術除了三位主角的形象照(`public/images/heroes.jpeg`)之外,也全是 `art.js` 現畫的 +inline SVG:戰場、卡牌、遺忘者的遮罩、結果畫面都沒有圖檔。把形象照抽掉照樣能玩, +角色卡會自動退回 SVG 版本。 --- @@ -125,8 +127,9 @@ curl "https://conssswars-web.pages.dev/api/og/verify?tx=0x<你的交易hash>" | npm run sim # 跑 300 場隨機對戰,印出勝負分布與平均回合數 ``` -> ⚠️ **改任何數值前請先重跑模擬。** 現行數值是調出來的: -> 合理玩法約第 6 回合收掉,亂打約七成會輸(實測 300 場:71% 落敗,平均 3.7 回合)。 +> ⚠️ **改任何數值前請先重跑模擬。** 現行數值是調出來的:合理玩法約第 6 回合收掉, +> 亂打大多會輸。模擬用的是隨機出牌,每次結果會抖動 —— 連跑五輪的實測範圍是 +> **落敗 59–69%、平均 3.7–3.8 回合**(其餘多為和局,隨機亂打贏面約兩成)。 --- @@ -138,8 +141,10 @@ npm run sim # 跑 300 場隨機對戰,印出勝負分布與平均回合數 | 蕙 Hue | 見證者 | The Witness | | 刃 Ren | 零重刃 | Blade of Gravity | -形象照放 `public/images/heroes.{png,jpg,jpeg,webp}`(**一張**三格拼版,左到右 零→蕙→刃, -單格比例約 454×787)就會自動採用 —— 程式不裁圖,整張載入後用 CSS 取三等分之一。 +形象照放 `public/images/heroes.{png,jpg,jpeg,webp}`(**一張**三格拼版,左到右 零→蕙→刃) +就會自動採用 —— 程式不裁圖,整張載入後用 CSS 取三等分之一。 +現行那張是 **1361×768**,所以單格約 **454×768**;CSS 的 `aspect-ratio` 直接寫成 +`1361 / 2304`(= 整張寬 ÷ 單格高×3),換圖時記得一起改,比例不對臉會被縱向拉伸。 沒放則退回 `art.js` 現畫的 inline SVG,兩種情況都能正常運作。細節見 [`public/images/README.md`](public/images/README.md)。