Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions .github/workflows/godot-build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
name: Godot Web build

on:
push:
branches:
- main
pull_request:

jobs:
build:
permissions:
contents: read
name: Build Godot Web export
runs-on: ubuntu-latest

steps:
- name: Check out repository
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10

- name: Set up Godot
uses: chickensoft-games/setup-godot@f166999204a4f2722c6fe042fbaa3b3ea0d9c789
with:
version: 4.7.0
use-dotnet: false
include-templates: true
cache: true

- name: Export Godot Web build
run: make godot-export

- name: Upload GitHub Pages artifact
uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b
with:
path: dist

deploy:
name: Deploy to GitHub Pages
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
needs: build
runs-on: ubuntu-latest
concurrency:
group: github-pages
cancel-in-progress: false
permissions:
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}

steps:
- name: Deploy GitHub Pages site
id: deployment
uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e
3 changes: 2 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@ godot-export:
mkdir -p dist; \
DIST_PATH="$$(pwd)/dist/index.html"; \
echo "Exporting Godot Web build with $$GODOT_BIN to $$DIST_PATH"; \
"$$GODOT_BIN" --headless --path godot --export-release Web "$$DIST_PATH"
"$$GODOT_BIN" --headless --path godot --export-release Web "$$DIST_PATH"; \
cp godot/web/stellar_bridge.js "$$(dirname "$$DIST_PATH")/stellar_bridge.js"
19 changes: 19 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Human-vs-bots

[![Godot Web build](https://github.com/Bitcoindefi/Human-vs-bots/actions/workflows/godot-build.yml/badge.svg)](https://github.com/Bitcoindefi/Human-vs-bots/actions/workflows/godot-build.yml)

Turn-based strategy game with a Web3-ready flow on Stellar.

## What this project is
Expand Down Expand Up @@ -95,6 +97,23 @@ The command detects `godot4` or `godot`, runs the `Web` export preset in headles
mode, and writes the generated entry point to `dist/index.html`. The generated
`dist/` directory is intentionally not tracked.

Godot Web exports expose a defensive `WebBridge` autoload for asynchronous
wallet, Stellar commit/reveal, and ZK proof operations. The browser function,
callback, and payload contract is documented in
[`godot/web/README.md`](godot/web/README.md).

### Godot Web CI/CD

The [Godot Web build workflow](.github/workflows/godot-build.yml) runs for every
pull request and every push to `main`. Its `build` job installs and caches Godot
4.7.0 plus the matching Web export templates, calls `make godot-export`, and
uploads `dist/` as the GitHub Pages artifact.

For pushes to `main`, the `deploy` job publishes that artifact to the
`github-pages` environment with `actions/deploy-pages`. Pull requests build and
upload the artifact for validation but never deploy it. The repository's Pages
source must be set to **GitHub Actions** under **Settings → Pages**.

## Third-party references and licenses

- Unciv assets inspiration (visual assets used in demo pipeline)
Expand Down
168 changes: 168 additions & 0 deletions godot/autoloads/WebBridge.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
extends Node

signal wallet_connected(address: String)
signal wallet_connection_failed(error: String)
signal address_received(address: String)
signal commit_submitted(result)
signal reveal_submitted(result)
signal proof_generated(result)
signal proof_exported(result)
signal bridge_error(action: String, error: String)

const CALLBACK_NAMESPACE := "HumanVsBotsGodot"
const BRIDGE_INTERFACE := "HumanVsBotsBridge"

var _bridge = null
var _callback_interface = null
var _callback_references: Array = []


func _ready() -> void:
if not OS.has_feature("web"):
return

JavaScriptBridge.eval(
"globalThis.%s = globalThis.%s || {};" % [CALLBACK_NAMESPACE, CALLBACK_NAMESPACE]
)
_callback_interface = JavaScriptBridge.get_interface(CALLBACK_NAMESPACE)
_bridge = JavaScriptBridge.get_interface(BRIDGE_INTERFACE)

if _callback_interface == null:
return

_register_callback("walletConnected", _on_wallet_connected)
_register_callback("walletConnectionFailed", _on_wallet_connection_failed)
_register_callback("addressReceived", _on_address_received)
_register_callback("commitSubmitted", _on_commit_submitted)
_register_callback("revealSubmitted", _on_reveal_submitted)
_register_callback("proofGenerated", _on_proof_generated)
_register_callback("proofExported", _on_proof_exported)
_register_callback("bridgeError", _on_bridge_error)


func is_available() -> bool:
return OS.has_feature("web") and _bridge != null and _callback_interface != null


func connect_wallet() -> void:
if _ensure_available("connect_wallet"):
_bridge.connectWallet()


func get_address() -> void:
if _ensure_available("get_address"):
_bridge.getAddress()


func commit_action(payload: Dictionary) -> void:
if _ensure_available("commit_action"):
_bridge.commitAction(JSON.stringify(payload))


func reveal_action(payload: Dictionary) -> void:
if _ensure_available("reveal_action"):
_bridge.revealAction(JSON.stringify(payload))


func generate_proof(payload: Dictionary) -> void:
if _ensure_available("generate_proof"):
_bridge.generateProof(JSON.stringify(payload))


func export_proof(payload: Dictionary) -> void:
if _ensure_available("export_proof"):
_bridge.exportProof(JSON.stringify(payload))


func _register_callback(callback_name: String, callable: Callable) -> void:
var callback = JavaScriptBridge.create_callback(callable)
_callback_references.append(callback)
_callback_interface.set(callback_name, callback)


func _ensure_available(action: String) -> bool:
if not OS.has_feature("web"):
_report_error(action, "WebBridge is only available in Godot Web exports.")
return false
if _callback_interface == null:
_report_error(action, "Could not register the browser callback interface.")
return false
if _bridge == null:
_report_error(
action,
"window.%s is unavailable; load stellar_bridge.js before Godot starts." % BRIDGE_INTERFACE
)
return false
return true


func _on_wallet_connected(arguments: Array) -> void:
var address := _read_string_argument("connect_wallet", arguments)
if not address.is_empty():
wallet_connected.emit(address)


func _on_wallet_connection_failed(arguments: Array) -> void:
var error := _read_string_argument("connect_wallet", arguments)
if not error.is_empty():
wallet_connection_failed.emit(error)


func _on_address_received(arguments: Array) -> void:
var address := _read_string_argument("get_address", arguments)
if not address.is_empty():
address_received.emit(address)


func _on_commit_submitted(arguments: Array) -> void:
_emit_json_result("commit_action", commit_submitted, arguments)


func _on_reveal_submitted(arguments: Array) -> void:
_emit_json_result("reveal_action", reveal_submitted, arguments)


func _on_proof_generated(arguments: Array) -> void:
_emit_json_result("generate_proof", proof_generated, arguments)


func _on_proof_exported(arguments: Array) -> void:
_emit_json_result("export_proof", proof_exported, arguments)


func _on_bridge_error(arguments: Array) -> void:
if arguments.size() < 2:
_report_error("unknown", "Browser bridge returned an incomplete error callback.")
return
bridge_error.emit(str(arguments[0]), str(arguments[1]))


func _read_string_argument(action: String, arguments: Array) -> String:
if arguments.is_empty():
_report_error(action, "Browser bridge callback did not include a value.")
return ""
var value := str(arguments[0])
if value.is_empty():
_report_error(action, "Browser bridge callback returned an empty value.")
return value


func _emit_json_result(action: String, target_signal: Signal, arguments: Array) -> void:
if arguments.is_empty() or typeof(arguments[0]) != TYPE_STRING:
_report_error(action, "Browser bridge callback must include a JSON string.")
return

var json := JSON.new()
var parse_error := json.parse(arguments[0])
if parse_error != OK:
_report_error(
action,
"Browser bridge returned invalid JSON: %s" % json.get_error_message()
)
return
target_signal.emit(json.data)


func _report_error(action: String, error: String) -> void:
push_warning("%s: %s" % [action, error])
bridge_error.emit(action, error)
2 changes: 1 addition & 1 deletion godot/export_presets.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ vram_texture_compression/for_desktop=true
vram_texture_compression/for_mobile=true
html/export_icon=true
html/custom_html_shell=""
html/head_include=""
html/head_include="<script src=\"stellar_bridge.js\"></script>"
html/canvas_resize_policy=2
html/focus_canvas_on_start=true
html/experimental_virtual_keyboard=false
Expand Down
4 changes: 4 additions & 0 deletions godot/project.godot
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ config_version=5
config/name="HumanVsBots"
run/main_scene="res://scenes/Main.tscn"

[autoload]

WebBridge="*res://autoloads/WebBridge.gd"

[display]

window/size/viewport_width=1280
Expand Down
89 changes: 89 additions & 0 deletions godot/web/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Godot WebBridge contract

`WebBridge` is a Godot autoload that exchanges asynchronous Stellar/ZK requests
with browser JavaScript. It is active only in a Godot Web export. Native/editor
calls do not touch `JavaScriptBridge`; they emit `bridge_error` instead.

## Loading the browser adapter

The Web export preset adds this tag to the generated page:

```html
<script src="stellar_bridge.js"></script>
```

`make godot-export` copies `godot/web/stellar_bridge.js` next to the generated
`dist/index.html`. If exporting from the Godot editor instead, copy that file
next to the exported HTML manually. The existing repository-root `index.html`
and demo pages are not part of this integration.

Before the game makes a request, configure the adapter with the real wallet,
Stellar, and proof implementations:

```js
window.HumanVsBotsBridge.configure({
connectWallet: async () => ({ address: "G..." }),
getAddress: async () => "G...",
commitAction: async (payload) => ({ txHash: "..." }),
revealAction: async (payload) => ({ txHash: "..." }),
generateProof: async (payload) => ({ proof: "...", publicInputs: [] }),
exportProof: async (payload) => ({ filename: "proof.json" }),
});
```

Handlers may return a value or a Promise. `commitAction`, `revealAction`,
`generateProof`, and `exportProof` receive the parsed JSON object sent by Godot.
This adapter deliberately does not select a wallet SDK, submit transactions, or
generate proofs itself.

## JavaScript functions called by Godot

Godot expects these functions on `window.HumanVsBotsBridge`:

| Function | Input | Successful handler result |
| --- | --- | --- |
| `connectWallet()` | none | address string or `{ address: "G..." }` |
| `getAddress()` | none | address string or `{ address: "G..." }` |
| `commitAction(payloadJson)` | JSON string | any JSON-serializable value |
| `revealAction(payloadJson)` | JSON string | any JSON-serializable value |
| `generateProof(payloadJson)` | JSON string | any JSON-serializable value |
| `exportProof(payloadJson)` | JSON string | any JSON-serializable value |

## Callbacks into Godot

At startup, the autoload creates `window.HumanVsBotsGodot` and registers:

| Callback | Arguments | Godot signal |
| --- | --- | --- |
| `walletConnected` | `address` string | `wallet_connected(address)` |
| `walletConnectionFailed` | error string | `wallet_connection_failed(error)` |
| `addressReceived` | `address` string | `address_received(address)` |
| `commitSubmitted` | JSON result string | `commit_submitted(result)` |
| `revealSubmitted` | JSON result string | `reveal_submitted(result)` |
| `proofGenerated` | JSON result string | `proof_generated(result)` |
| `proofExported` | JSON result string | `proof_exported(result)` |
| `bridgeError` | action string, error string | `bridge_error(action, error)` |

The supplied adapter invokes these callbacks after each handler settles.
Integrations that replace the adapter must use the same callback names and must
JSON-stringify action/proof results. Expected action names for `bridgeError` are
`connect_wallet`, `get_address`, `commit_action`, `reveal_action`,
`generate_proof`, and `export_proof`.

## Godot usage

```gdscript
func _ready() -> void:
WebBridge.wallet_connected.connect(_on_wallet_connected)
WebBridge.commit_submitted.connect(_on_commit_submitted)
WebBridge.bridge_error.connect(_on_bridge_error)

WebBridge.connect_wallet()
WebBridge.commit_action({
"turn": 1,
"commitment": "hex-or-base64-commitment",
})
```

Requests return through signals; wrapper methods do not block or return a
transaction/proof result.
Loading