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, live on Sui mainnet. The game source (Godot project, Move contracts) lives in sibling repos — see Related repositories.
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 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).
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 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.
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.
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) andindex.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 fetchesindex.wasm/index.pckfromreleases/latest/download/server-side and streams them back same-origin with the correctContent-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'sSuiClientPOSTs 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.
- Binary proxy — a plain redirect to GitHub fails CORS (release assets send
no
- 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.
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:
- Upload the chronicle JSON to Walrus (publisher HTTP API) → get a blob id.
POST /mint-voucherwith the clear report → get an authority-signed voucher.- Build and sign
chronicle::chronicle::mint_chroniclewith 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.
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.
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<pkg>::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 viaCLOUDFLARE_ACCOUNT_IDor~/.cf_account. These are local-only and never enter git. The token should be deleted/revoked after use.
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.
- 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.jsholds public IDs/URLs only. Rotating a key is a dashboard change, no rebuild. - Same-origin
/rpcproxy. 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-voucheris 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.comare served (filters casualcurl/ foreign-site scripting). - KV rate limit — a per-
(wallet, battle)cooldown via theMINT_KVbinding, 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_TYPEare unset.
- Origin check — only requests from
- 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).
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.
The next hardening step makes the result server-verified instead of client-trusted, in two layers:
- 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. - 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.
Prerequisites: Node 18+ and (for serving Functions locally) wrangler.
# 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 publicNotes:
- 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'sindex.wasm/index.pckintopublic/. - 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.varsfile).
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:
# 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/webscripts/deploy.sh will:
- Copy the fresh export's loader + audio worklets into
public/(the committedindex.icon.pngis intentionally kept). - Build the wallet bridge (no secrets baked in).
- Upload
index.wasm+index.pckto a timestamped GitHub Release. - Stamp the Function's edge-cache version with the release tag (automatic cache-bust), then restore the working tree afterward.
wrangler pages deploy public→ theconsss-playproject.- Verify
/,/index.wasm,/index.pckover HTTPS.
One-time setup: create the consss-play Pages project, set the server env
vars from Configuration, and point the custom domain
play.conssswars.com at the project. Remember to rm -f ~/.cf_token and revoke
the token when finished.
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.
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 |
| 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 |
| Site / repo | URL | Host |
|---|---|---|
| Game source (Godot + Move) | ConsssLab/app, ConsssLab/contracts |
— |
conssswars.com — official site |
official-website |
Cloudflare Pages |
consss.wal.app — limited event |
official-limit-time-event |
Walrus Sites |
© ConsssLab. All rights reserved. Game art, music, and story are proprietary; see the individual source repositories for any code-specific licensing.