https://github.com/conssslab/play
ConSSS Wars: Echoes of Chainoa 鏈州英雄傳:鏈之迴響 — playable web build host (play.conssswars.com; Cloudflare Pages shell + GitHub Releases for wasm/pck)
https://github.com/conssslab/play
Last synced: 24 days ago
JSON representation
ConSSS Wars: Echoes of Chainoa 鏈州英雄傳:鏈之迴響 — playable web build host (play.conssswars.com; Cloudflare Pages shell + GitHub Releases for wasm/pck)
- Host: GitHub
- URL: https://github.com/conssslab/play
- Owner: ConsssLab
- Created: 2026-06-01T16:29:05.000Z (28 days ago)
- Default Branch: main
- Last Pushed: 2026-06-03T08:52:08.000Z (26 days ago)
- Last Synced: 2026-06-03T09:26:31.019Z (26 days ago)
- Language: JavaScript
- Homepage: https://play.conssswars.com
- Size: 131 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
Awesome Lists containing this project
README
ConSSS Wars: Echoes of Chainoa
鏈州英雄傳:鏈之迴響
A hand-drawn, turn-based tactics RPG set on the chain-continent of Chainoa —
where the battles you win become on-chain Chronicles and the sagas you write are
preserved on Walrus.
▶ Play now ·
Website ·
Limited event
> This repository is the **deployment repo** for the playable web build at
> [play.conssswars.com](https://play.conssswars.com), **live on Sui mainnet**.
> The game source (Godot project, Move contracts) lives in sibling repos — see
> [Related repositories](#related-repositories).
---
## Overview
**ConSSS Wars** is a browser-first, web3-second tactics RPG. The full game plays
in any modern browser with no install and **no wallet required**. Connect a Sui
wallet and your results stop being throwaway: clear a battle and you can mint a
**Chronicle** — a Sui NFT that records *which* battle you cleared, *how well* you
fought (an on-chain tier), and a personal long-form saga you author, with the
saga JSON stored on **Walrus**.
This repo packages a [Godot 4.6](https://godotengine.org) HTML5 export together
with a vanilla-JS Sui + Walrus dApp bridge, and serves both from **Cloudflare
Pages** with a Pages Function handling the heavy lifting (binary streaming, a
Tatum-keyed RPC proxy, and an anti-cheat mint voucher).
## Features
**Gameplay**
- Turn-based tactical combat with multi-phase boss fights and mid-battle events.
- Painterly, hand-drawn art direction and an original score.
- Fully playable in the browser — no install, no wallet required.
**On-chain (optional, opt-in)**
- **Connect any Sui wallet** — Slush, Sui Wallet, Suiet and other
[Wallet Standard](https://github.com/wallet-standard/wallet-standard) wallets.
First-party page, no popup blocking.
- **Tiered Chronicle NFTs** — clearing a battle mints a Chronicle whose rank is
computed **on-chain** from a per-battle clear counter and your remaining HP.
- **Mint integrity** — every mint is gated by an authority-signed ed25519
voucher, so a Chronicle cannot be granted by hand-crafting a transaction.
- **Walrus sagas** — the chronicle JSON (battle log + long-form text) is uploaded
to Walrus; only its blob id is pinned on-chain as `metadata_blob_id`.
## Hackathon integration points
This build integrates two of the track technologies as **core functionality**,
not decoration. The table maps each to exactly where it is used in this repo.
| Technology | Role | Where (browser side) | Where (server side) | Endpoint(s) used |
|------------|------|----------------------|---------------------|------------------|
| **Walrus** (decentralized storage) | Stores the player's Chronicle metadata JSON off-chain; only the returned blob id is pinned on-chain. | `bridge/src/walrus.js` — `uploadString()` **PUTs** the chronicle JSON to a Walrus **publisher** and returns a `blobId`; `fetchString()` / `blobUrl()` resolve it back from a Walrus **aggregator** (also used for NFT Display `image_url`). Exposed as `window.consss.uploadToWalrus` / `readFromWalrus`. | None — the publisher pays for storage, so no server proxy and no extra wallet tx are needed. | Publisher `PUT {publisherUrl}/v1/blobs?epochs={n}` · Aggregator `GET {aggregatorUrl}/v1/blobs/{blobId}` |
| **Tatum API** (Sui RPC gateway) | The track's RPC gateway for all Sui JSON-RPC reads (owned-object queries, etc.). | `bridge/src/sui-client.js` — `SuiClient` is pointed at the **same-origin** `/rpc` path (`config.rpcProxyPath`), never at Tatum directly. The Tatum key is never in the browser bundle. | `functions/[[path]].js` (`/rpc` branch) injects the **Tatum** API key as an `x-api-key` header server-side, forwards to the Tatum gateway, and **falls back to the public Sui fullnode** on any error. | `POST /rpc` → `https://sui-mainnet.gateway.tatum.io` (key from `TATUM_API_KEY`) → public fullnode fallback |
**Mainnet Walrus endpoints** (from `bridge/config.public.js`): publisher
`https://walrus-publisher.rubynodes.io`, aggregator
`https://aggregator.walrus-mainnet.walrus.space`, `storageEpochs = 5`.
> **Why the community rubynodes publisher?** Mysten does not run a free public
> **publisher** on Walrus mainnet (writes cost storage), so the bridge uses the
> community **rubynodes** publisher for uploads. Reads use the standard Walrus
> mainnet **aggregator**. Both are plain HTTP — the documented Walrus Web API
> pattern — so no client-side encoding or extra wallet signature is required.
## Architecture
A Godot 4 HTML5 export produces two binaries far larger than Cloudflare Pages'
per-file limit, so the build is split across hosts and stitched back together at
the edge. The browser only ever talks to `play.conssswars.com`.
```
play.conssswars.com (Cloudflare Pages)
browser ──────────────────────────┬─────────────────────────────────
│
public/ (static shell, < 2 MB) │ functions/[[path]].js (Pages Function)
• index.html │ • /index.wasm ┐ proxied + edge-cached
• index.js loader + audio worklets │ • /index.pck ┘ from GitHub Releases
• dist/ wallet bridge bundle │ (same-origin; fixes CORS + Content-Type)
• _headers · _redirects │ • /rpc → Tatum (server key) → public RPC
│ • /mint-voucher → ed25519 voucher (anti-cheat)
│
GitHub Releases (ConsssLab/play, releases/latest/download):
index.pck (~385 MB) + index.wasm (~36 MB)
Walrus (no proxy): browser ── PUT ──▶ rubynodes publisher (write saga JSON)
browser ── GET ──▶ walrus-mainnet aggregator (read saga JSON)
```
What each piece does:
- **Static shell on Pages** — `index.html`, the Godot loader (`index.js`), the
two audio worklets, the icon, and the built wallet bridge bundle.
- **Engine binaries on GitHub Releases** — `index.pck` (~385 MB) and
`index.wasm` (~36 MB) exceed both Cloudflare Pages' 25 MiB/file limit and
GitHub's 100 MB git limit, so each deploy uploads them as Release assets.
- **The Pages Function stitches it together** (`functions/[[path]].js`):
- **Binary proxy** — a plain redirect to GitHub fails CORS (release assets send
no `Access-Control-Allow-Origin`), so the Function fetches `index.wasm` /
`index.pck` from `releases/latest/download/` server-side and streams them back
**same-origin** with the correct `Content-Type` (so WASM streaming
compilation works). It **edge-caches** them under a version-stamped key that
the deploy script bumps per release, so repeat loads are fast and a new
release retires the old cached bytes automatically.
- **`/rpc`** — a Sui JSON-RPC proxy. The browser's `SuiClient` POSTs here; the
Function injects the **Tatum** API key (`x-api-key`) server-side and forwards,
falling back to the public Sui fullnode on any failure.
- **`/mint-voucher`** — signs an authority ed25519 voucher binding the mint to
`(registry, player, battle, hero, hp_pct, nonce, expiry)`. See
[Security model](#security-model).
- **Single-threaded export** — cross-origin isolation is intentionally disabled
(`ensureCrossOriginIsolationHeaders: false`, no COOP/COEP), so no
SharedArrayBuffer setup is needed and the cross-origin engine load is not
blocked. *Keep the Godot export single-threaded or this hosting model breaks.*
### dApp bridge
`bridge/` builds a vanilla-JS (no React, no dapp-kit) bundle that exposes
`window.consss = { connect, mint, uploadToWalrus, readFromWalrus,
getOwnedChronicles }` so the Godot HTML5 shell can drive Sui + Walrus via
`JavaScriptBridge`. The mint flow is:
1. Upload the chronicle JSON to **Walrus** (publisher HTTP API) → get a blob id.
2. `POST /mint-voucher` with the clear report → get an authority-signed voucher.
3. Build and sign `chronicle::chronicle::mint_chronicle` with the player's own
wallet, passing the report + voucher + `metadata_blob_id`; the tier is
computed on-chain. The server never holds player funds.
`bridge/config.public.js` contains **only public on-chain data** (network,
package/registry IDs, Walrus endpoints, `/rpc` path) — no secrets — and is safe
to commit and ship in the browser bundle.
## Configuration
### Public config (committed)
`bridge/config.public.js` — network, RPC proxy path, on-chain IDs, and Walrus
endpoints. Public data only; bundled into the browser. See
[On-chain deployments](#on-chain-deployments).
### Server-side secrets (Cloudflare only)
Secrets live **only** as Cloudflare Pages environment variables / bindings on the
`consss-play` project and are read solely inside the Pages Function. They are
never bundled, never committed, never shipped to the browser.
| Variable | Required | Purpose |
|----------|----------|---------|
| `TATUM_API_KEY` | yes | Tatum Sui gateway key injected into `/rpc` (falls back to public RPC if unset/failing). |
| `AUTHORITY_PRIVKEY_HEX` | yes | ed25519 private key that signs `/mint-voucher` vouchers. |
| `CHRONICLE_REGISTRY_ID` | yes | Registry id the voucher is bound to (anti cross-deployment replay). |
| `CHRONICLE_TYPE` | optional | Chronicle struct type (`0x::chronicle::Chronicle`) — enables the progression gate. |
| `MINT_KV` | optional | KV binding for the per-`(wallet, battle)` mint rate limit. |
| `TATUM_RPC_URL` / `PUBLIC_RPC_URL` | optional | Override the default mainnet Tatum / public fullnode endpoints. |
> Deploying also requires a Cloudflare API token at `~/.cf_token` (Pages → Edit)
> and the account id via `CLOUDFLARE_ACCOUNT_ID` or `~/.cf_account`. These are
> local-only and never enter git. The token should be deleted/revoked after use.
## Security model
This is an honest, scoped model: a fully client-side game can never make the
client untrusted by itself, so we are explicit about what the current defenses
**do** guarantee, what they **don't yet**, and why that trade-off is acceptable
today.
### Defenses we have today
- **No secrets in the browser.** The Tatum API key and the voucher authority
ed25519 signing key are Cloudflare **runtime secrets**, read only inside the
Pages Function. Local deploys read a Cloudflare API token from a deploy-only
`~/.cf_token`. None of these are ever in the committed browser bundle —
`config.public.js` holds public IDs/URLs only. Rotating a key is a dashboard
change, no rebuild.
- **Same-origin `/rpc` proxy.** The browser POSTs to `/rpc`; the Function
attaches the Tatum key server-side and forwards, falling back to the public
fullnode. The key never reaches the browser and there is no CORS surface.
- **Single mint chokepoint.** `/mint-voucher` is the *only* way to obtain a valid
voucher, and the on-chain contract **rejects any mint without one**. So even
though anyone can craft a Sui transaction, nobody can mint a Chronicle without
going through our server. It signs an ed25519 voucher over `(registry, player,
battle, hero, hp_pct, nonce, expiry)`; the contract verifies signature, sender,
nonce (anti-replay), expiry, and computes the tier itself. Players sign the
mint with their own wallet.
- **Layer-1 anti-scripting guards** on `/mint-voucher`:
- **Origin check** — only requests from `https://play.conssswars.com` are
served (filters casual `curl` / foreign-site scripting).
- **KV rate limit** — a per-`(wallet, battle)` cooldown via the `MINT_KV`
binding, throttling automated re-requests.
- **Progression gate** — battle *N* requires already owning the battle *N-1*
Chronicle (queried via RPC), so the sequence can't be skipped.
- Rate limit and progression gate are **graceful** — they skip cleanly if
`MINT_KV` / `CHRONICLE_TYPE` are unset.
- **Versioned edge caching** of the engine binaries, so a new release retires
old cached bytes by key bump (no broad cache-purge token scope needed).
### What we do NOT (yet) do — stated plainly
The Layer-1 guards plus the voucher chokepoint give us **anti-scripting**: they
stop casual automation and they stop hand-crafted-PTB mints that bypass the
server. They do **not** stop a determined player from driving the real client
and reporting a *favorable* result, because `/mint-voucher` still **trusts the
client-reported `hp_pct` / `battle_id`**. There is no server-authoritative
simulation today, so that is the documented ceiling of the current design.
**Why we accept this for now.** Every mint costs **real Sui mainnet gas**, paid
by the attacker, and the reward is a collectible NFT — there is no token,
yield, or marketplace value to extract. For a small project, the cost and effort
of a sophisticated client-tampering exploit is high relative to that reward, so
hardening past anti-scripting is not yet worth it. We chose to ship an honest
chokepoint now rather than over-build.
### Planned future anti-cheat ("no real play, no mint")
The next hardening step makes the result **server-verified** instead of
client-trusted, in two layers:
1. **Server-authoritative validation** — the backend deterministically
**re-simulates / replays** the battle from a server-seeded start using the
recorded player inputs, and only issues a voucher if the replayed outcome
matches the claimed `hp_pct` / clear.
2. **zk-proof of correct play** — a zero-knowledge proof that a valid play
session produced the claimed result, so the server can verify *without*
trusting (or even re-running) the raw client trace.
**Importantly, neither requires a smart-contract change.** The voucher interface
(`mint_chronicle` + the signed message format) stays exactly as it is today; only
the **backend's decision to sign** gets stricter. That keeps this upgrade path
fully forward-compatible with the deployed mainnet package.
## Local development
Prerequisites: Node 18+ and (for serving Functions locally) `wrangler`.
```bash
# Build the wallet bridge bundle into public/dist/
cd bridge
npm install
npm run build # or: npm run build:watch
# Serve the static shell + Pages Function locally
cd ..
npx wrangler pages dev public
```
Notes:
- The engine binaries (`index.wasm`, `index.pck`) are **not** in this repo; they
are served from GitHub Releases in production. To run the full game locally,
drop a fresh Godot Web export's `index.wasm` / `index.pck` into `public/`.
- For the wallet/mint/RPC paths to work locally, provide the server env vars
above to `wrangler pages dev` (e.g. via `--binding` / a `.dev.vars` file).
## Deployment
Deploys are **manual by design** — a human gate against shipping a bad build, and
the Cloudflare token never lives in GitHub. One command runs the whole pipeline:
```bash
# 1. In Godot: export the "Web" preset (keep thread support OFF) to
# ../app/exports/web/ (override the path as an arg if different)
# 2. From this repo root:
scripts/deploy.sh [EXPORT_DIR] # default EXPORT_DIR = ../app/exports/web
```
`scripts/deploy.sh` will:
1. Copy the fresh export's loader + audio worklets into `public/` (the committed
`index.icon.png` is intentionally kept).
2. Build the wallet bridge (no secrets baked in).
3. Upload `index.wasm` + `index.pck` to a timestamped GitHub Release.
4. Stamp the Function's edge-cache version with the release tag (automatic
cache-bust), then restore the working tree afterward.
5. `wrangler pages deploy public` → the `consss-play` project.
6. Verify `/`, `/index.wasm`, `/index.pck` over HTTPS.
**One-time setup:** create the `consss-play` Pages project, set the server env
vars from [Configuration](#configuration), and point the custom domain
`play.conssswars.com` at the project. Remember to `rm -f ~/.cf_token` and revoke
the token when finished.
## Repository layout
```
public/ Cloudflare Pages output (static shell)
├── index.html game shell (cross-origin isolation off; loads bridge + engine)
├── index.js Godot Web loader
├── index.audio*.worklet.js audio worklets
├── index.icon.png committed favicon
├── dist/ built wallet bridge bundle (gitignored)
├── _headers · _redirects edge caching / routing policy
functions/
└── [[path]].js Pages Function: binary proxy + /rpc (Tatum) + /mint-voucher
bridge/ Sui + Walrus wallet bridge (esbuild → public/dist)
├── config.public.js public IDs / URLs only — no secrets
├── scripts/build.mjs esbuild driver
└── src/ index · bridge · wallet · sui-client · walrus · mint · chronicles
scripts/
└── deploy.sh one-command manual deploy
```
The built bridge (`public/dist/`) and the engine binaries (`*.wasm`, `*.pck`)
are gitignored: the bridge is built at deploy time, the binaries live in Releases.
## On-chain deployments
**Network: Sui mainnet.** IDs below are public on-chain data (mirrored in
`bridge/config.public.js`).
| Object | ID |
|--------|----|
| Chronicle package | `0x5760b2685d41bd45e2991dedc242e866b1aca9ff3c3a5e193445751c2b8dfe4b` |
| Chronicle registry (shared) | `0x9ff1d9e50e8feca77ccddf5901bd774d3baa4732dac37ae261ca36b2352ced8b` |
| Finale registry (shared) | `0x2c752d82144701e2b476cd35fd8c5482c9f3aabfe27e155729b657b369493d19` |
| Walrus (mainnet) | publisher `walrus-publisher.rubynodes.io` · aggregator `aggregator.walrus-mainnet.walrus.space` · 5 epochs |
## Tech stack
| Layer | Tech |
|-------|------|
| Game engine | Godot 4.6 → HTML5 (single-threaded, WebGL2) |
| Smart contracts | Sui Move (mainnet) — Chronicle NFT, tiers, mint voucher |
| Decentralized storage | Walrus — chronicle saga blobs (HTTP publisher/aggregator) |
| Wallet | `@mysten/wallet-standard` (vanilla JS, no React) |
| Sui SDK | `@mysten/sui` · `@mysten/walrus` |
| Hosting | Cloudflare Pages + Pages Functions + GitHub Releases (engine binaries) |
| RPC | Tatum Sui gateway (server-side key) with public fullnode fallback |
| Bundler | esbuild |
## Related repositories
| Site / repo | URL | Host |
|-------------|-----|------|
| Game source (Godot + Move) | [`ConsssLab/app`](https://github.com/ConsssLab/app), [`ConsssLab/contracts`](https://github.com/ConsssLab/contracts) | — |
| `conssswars.com` — official site | [`official-website`](https://github.com/ConsssLab/official-website) | Cloudflare Pages |
| `consss.wal.app` — limited event | [`official-limit-time-event`](https://github.com/ConsssLab/official-limit-time-event) | Walrus Sites |
## License
© ConsssLab. All rights reserved. Game art, music, and story are proprietary;
see the individual source repositories for any code-specific licensing.