Skip to content

ConsssLab/play

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

24 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

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

engine chain storage rpc host

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.


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 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 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.jsuploadString() 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.jsSuiClient 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 /rpchttps://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 Pagesindex.html, the Godot loader (index.js), the two audio worklets, the icon, and the built wallet bridge bundle.
  • Engine binaries on GitHub Releasesindex.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.
  • 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.

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<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 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.

# 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:

# 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, 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, ConsssLab/contracts
conssswars.com — official site official-website Cloudflare Pages
consss.wal.app — limited event 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.

About

ConSSS Wars: Echoes of Chainoa 鏈州英雄傳:鏈之迴響

Topics

Resources

Stars

2 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors