From cde8d9d234e9d1249c1c59ce10a56b30a7462cf9 Mon Sep 17 00:00:00 2001 From: Vinay Date: Sun, 28 Jun 2026 22:14:46 +0530 Subject: [PATCH 01/10] Add base Godot project setup --- .gitignore | 11 ++++++++++- Makefile | 21 +++++++++++++++++++++ README.md | 23 +++++++++++++++++++++++ godot/assets/.gitkeep | 1 + godot/autoloads/.gitkeep | 1 + godot/export_presets.cfg | 28 ++++++++++++++++++++++++++++ godot/project.godot | 25 +++++++++++++++++++++++++ godot/scenes/Main.tscn | 27 +++++++++++++++++++++++++++ godot/scripts/.gitkeep | 1 + godot/shaders/.gitkeep | 1 + 10 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 Makefile create mode 100644 godot/assets/.gitkeep create mode 100644 godot/autoloads/.gitkeep create mode 100644 godot/export_presets.cfg create mode 100644 godot/project.godot create mode 100644 godot/scenes/Main.tscn create mode 100644 godot/scripts/.gitkeep create mode 100644 godot/shaders/.gitkeep diff --git a/.gitignore b/.gitignore index baf5a0b..7bc8ae8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,13 @@ .stellar-game-studio/ .zemeroth/ zemeroth-full/ -.vscode/ \ No newline at end of file +.vscode/ + +# Godot editor cache and generated imports +godot/.godot/ +godot/**/*.import + +# Godot web exports are generated with `make godot-export`. +/dist/ + +# godot/export_presets.cfg is intentionally tracked. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..18fc732 --- /dev/null +++ b/Makefile @@ -0,0 +1,21 @@ +.PHONY: godot-export + +godot-export: + @set -eu; \ + if command -v godot4 >/dev/null 2>&1; then \ + GODOT_BIN=godot4; \ + elif command -v godot >/dev/null 2>&1; then \ + GODOT_BIN=godot; \ + else \ + echo "Error: Godot 4 was not found. Install it and expose 'godot4' or 'godot' in PATH." >&2; \ + exit 1; \ + fi; \ + GODOT_VERSION="$$("$$GODOT_BIN" --version)"; \ + case "$$GODOT_VERSION" in \ + 4.*) ;; \ + *) echo "Error: $$GODOT_BIN is $$GODOT_VERSION; Godot 4.x is required." >&2; exit 1 ;; \ + esac; \ + 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" diff --git a/README.md b/README.md index b3baf8c..0f51e9a 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,29 @@ The flow is aligned with Stellar hackathon architecture and is prepared for deep - `scripts/serve-civ-lite.sh` - `scripts/game-studio.sh` +## Godot project + +The base Godot project lives in `godot/` and requires Godot 4.x. + +To open it, import `godot/project.godot` from the Godot Project Manager or run: + +```bash +godot4 --editor --path godot +``` + +If your Godot 4 executable is named `godot`, use that command instead. + +Install the matching Godot export templates before exporting for the first time. +Then export the Web build from the repository root: + +```bash +make godot-export +``` + +The command detects `godot4` or `godot`, runs the `Web` export preset in headless +mode, and writes the generated entry point to `dist/index.html`. The generated +`dist/` directory is intentionally not tracked. + ## Third-party references and licenses - Unciv assets inspiration (visual assets used in demo pipeline) diff --git a/godot/assets/.gitkeep b/godot/assets/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/godot/assets/.gitkeep @@ -0,0 +1 @@ + diff --git a/godot/autoloads/.gitkeep b/godot/autoloads/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/godot/autoloads/.gitkeep @@ -0,0 +1 @@ + diff --git a/godot/export_presets.cfg b/godot/export_presets.cfg new file mode 100644 index 0000000..07a6945 --- /dev/null +++ b/godot/export_presets.cfg @@ -0,0 +1,28 @@ +[preset.0] + +name="Web" +platform="Web" +runnable=true +advanced_options=false +dedicated_server=false +custom_features="" +export_filter="all_resources" +include_filter="" +exclude_filter="" +export_path="../dist/index.html" + +[preset.0.options] + +custom_template/debug="" +custom_template/release="" +variant/extensions_support=false +variant/thread_support=false +vram_texture_compression/for_desktop=true +vram_texture_compression/for_mobile=true +html/export_icon=true +html/custom_html_shell="" +html/head_include="" +html/canvas_resize_policy=2 +html/focus_canvas_on_start=true +html/experimental_virtual_keyboard=false +progressive_web_app/enabled=false diff --git a/godot/project.godot b/godot/project.godot new file mode 100644 index 0000000..e7777eb --- /dev/null +++ b/godot/project.godot @@ -0,0 +1,25 @@ +; Engine configuration file. +; Edit this file through the Godot editor when possible. +; +; Format: +; [section] +; param=value + +config_version=5 + +[application] + +config/name="HumanVsBots" +run/main_scene="res://scenes/Main.tscn" + +[display] + +window/size/viewport_width=1280 +window/size/viewport_height=720 +window/stretch/mode="canvas_items" + +[rendering] + +renderer/rendering_method="gl_compatibility" +renderer/rendering_method.mobile="gl_compatibility" +renderer/rendering_method.web="gl_compatibility" diff --git a/godot/scenes/Main.tscn b/godot/scenes/Main.tscn new file mode 100644 index 0000000..7467e97 --- /dev/null +++ b/godot/scenes/Main.tscn @@ -0,0 +1,27 @@ +[gd_scene format=3] + +[node name="Main" type="Control"] +layout_mode = 3 +anchors_preset = 15 +anchor_right = 1.0 +anchor_bottom = 1.0 +grow_horizontal = 2 +grow_vertical = 2 + +[node name="Title" type="Label" parent="."] +layout_mode = 1 +anchors_preset = 8 +anchor_left = 0.5 +anchor_top = 0.5 +anchor_right = 0.5 +anchor_bottom = 0.5 +offset_left = -160.0 +offset_top = -16.0 +offset_right = 160.0 +offset_bottom = 16.0 +grow_horizontal = 2 +grow_vertical = 2 +text = "Human vs Bots" +horizontal_alignment = 1 +vertical_alignment = 1 +theme_override_font_sizes/font_size = 24 diff --git a/godot/scripts/.gitkeep b/godot/scripts/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/godot/scripts/.gitkeep @@ -0,0 +1 @@ + diff --git a/godot/shaders/.gitkeep b/godot/shaders/.gitkeep new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/godot/shaders/.gitkeep @@ -0,0 +1 @@ + From d120003a53d272a56b3d1a1f0ea56e97a1f5805e Mon Sep 17 00:00:00 2001 From: Vinay Date: Sun, 28 Jun 2026 22:27:12 +0530 Subject: [PATCH 02/10] Add Godot Web build workflow --- .github/workflows/godot-build.yml | 55 +++++++++++++++++++++++++++++++ README.md | 14 ++++++++ 2 files changed, 69 insertions(+) create mode 100644 .github/workflows/godot-build.yml diff --git a/.github/workflows/godot-build.yml b/.github/workflows/godot-build.yml new file mode 100644 index 0000000..0993425 --- /dev/null +++ b/.github/workflows/godot-build.yml @@ -0,0 +1,55 @@ +name: Godot Web build + +on: + push: + branches: + - main + pull_request: + +permissions: + contents: read + +jobs: + build: + name: Build Godot Web export + runs-on: ubuntu-latest + + steps: + - name: Check out repository + uses: actions/checkout@v6 + + - name: Set up Godot + uses: chickensoft-games/setup-godot@v2 + 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@v4 + 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@v4 diff --git a/README.md b/README.md index 0f51e9a..53f61d9 100644 --- a/README.md +++ b/README.md @@ -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 @@ -95,6 +97,18 @@ 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 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) From 9f4225ca342f34becdf2e6bef61af4ea28228960 Mon Sep 17 00:00:00 2001 From: Vinay Date: Sun, 28 Jun 2026 22:41:30 +0530 Subject: [PATCH 03/10] Pin Godot workflow actions --- .github/workflows/godot-build.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/godot-build.yml b/.github/workflows/godot-build.yml index 0993425..f0fc339 100644 --- a/.github/workflows/godot-build.yml +++ b/.github/workflows/godot-build.yml @@ -16,10 +16,10 @@ jobs: steps: - name: Check out repository - uses: actions/checkout@v6 + uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 - name: Set up Godot - uses: chickensoft-games/setup-godot@v2 + uses: chickensoft-games/setup-godot@f166999204a4f2722c6fe042fbaa3b3ea0d9c789 with: version: 4.7.0 use-dotnet: false @@ -30,7 +30,7 @@ jobs: run: make godot-export - name: Upload GitHub Pages artifact - uses: actions/upload-pages-artifact@v4 + uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b with: path: dist @@ -52,4 +52,4 @@ jobs: steps: - name: Deploy GitHub Pages site id: deployment - uses: actions/deploy-pages@v4 + uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e From 06bf8b8d631afc461f8ee39a5437838c42c21136 Mon Sep 17 00:00:00 2001 From: Vinay Date: Sun, 28 Jun 2026 22:55:30 +0530 Subject: [PATCH 04/10] Scope workflow permissions to build job --- .github/workflows/godot-build.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/godot-build.yml b/.github/workflows/godot-build.yml index f0fc339..dd031f2 100644 --- a/.github/workflows/godot-build.yml +++ b/.github/workflows/godot-build.yml @@ -6,11 +6,10 @@ on: - main pull_request: -permissions: - contents: read - jobs: build: + permissions: + contents: read name: Build Godot Web export runs-on: ubuntu-latest From 8eebfc2bca78d8ab60092b5e51a89d832d153a40 Mon Sep 17 00:00:00 2001 From: Vinay Date: Mon, 29 Jun 2026 23:58:03 +0530 Subject: [PATCH 05/10] Add Godot EventBus autoload --- godot/autoloads/EventBus.gd | 229 ++++++++++++++++++++++++++++++++++++ godot/project.godot | 1 + 2 files changed, 230 insertions(+) create mode 100644 godot/autoloads/EventBus.gd diff --git a/godot/autoloads/EventBus.gd b/godot/autoloads/EventBus.gd new file mode 100644 index 0000000..8ec471c --- /dev/null +++ b/godot/autoloads/EventBus.gd @@ -0,0 +1,229 @@ +extends Node + +## Centralized game-wide signal bus. +## +## Use EventBus wrapper methods for cross-scene notifications so scenes, +## managers, UI, and Web3 bridge code do not need direct node-to-node +## dependencies. Signals are grouped by domain and document their expected +## parameters next to the declaration. + + +# Unit events + +## unit_moved(unit_id: StringName, from_hex: Vector2i, to_hex: Vector2i) +## Emitted after a unit finishes moving from one hex to another. +signal unit_moved(unit_id: StringName, from_hex: Vector2i, to_hex: Vector2i) + +## unit_attacked(attacker_id: StringName, defender_id: StringName, damage: int, remaining_health: int) +## Emitted after a unit attack resolves. +signal unit_attacked( + attacker_id: StringName, + defender_id: StringName, + damage: int, + remaining_health: int +) + +## unit_died(unit_id: StringName, owner_id: StringName, hex: Vector2i) +## Emitted when a unit is removed from the board. +signal unit_died(unit_id: StringName, owner_id: StringName, hex: Vector2i) + +## unit_promoted(unit_id: StringName, promotion_id: StringName, bonuses: Dictionary) +## Emitted when a unit receives a promotion or upgrade. +signal unit_promoted(unit_id: StringName, promotion_id: StringName, bonuses: Dictionary) + + +# City events + +## city_built(city_id: StringName, owner_id: StringName, hex: Vector2i) +## Emitted when a new city is founded or placed on the board. +signal city_built(city_id: StringName, owner_id: StringName, hex: Vector2i) + +## city_captured(city_id: StringName, previous_owner_id: StringName, new_owner_id: StringName) +## Emitted when ownership of an existing city changes. +signal city_captured( + city_id: StringName, + previous_owner_id: StringName, + new_owner_id: StringName +) + +## production_complete(city_id: StringName, production_id: StringName, result: Dictionary) +## Emitted when a city's production queue completes an item. +signal production_complete(city_id: StringName, production_id: StringName, result: Dictionary) + + +# Turn events + +## turn_started(turn_number: int, player_id: StringName) +## Emitted when a player turn begins. +signal turn_started(turn_number: int, player_id: StringName) + +## turn_phase_changed(turn_number: int, previous_phase: StringName, new_phase: StringName) +## Emitted when the current turn moves from one phase label to another. +signal turn_phase_changed( + turn_number: int, + previous_phase: StringName, + new_phase: StringName +) + +## turn_ended(turn_number: int, player_id: StringName) +## Emitted when a player turn ends. +signal turn_ended(turn_number: int, player_id: StringName) + + +# Web3 events + +## wallet_connected(address: String, provider: StringName) +## Emitted after a wallet connection succeeds. +signal wallet_connected(address: String, provider: StringName) + +## proof_generated(proof_id: StringName, proof: Dictionary) +## Emitted after a zero-knowledge proof or proof payload is generated. +signal proof_generated(proof_id: StringName, proof: Dictionary) + +## tx_confirmed(tx_hash: String, receipt: Dictionary) +## Emitted after an on-chain transaction is confirmed. +signal tx_confirmed(tx_hash: String, receipt: Dictionary) + +## web3_error(action: StringName, message: String, details: Dictionary) +## Emitted when a Web3 action fails or returns an invalid result. +signal web3_error(action: StringName, message: String, details: Dictionary) + + +# UI events + +## hex_hovered(hex: Vector2i, world_position: Vector2) +## Emitted when the pointer hover target changes to a board hex. +signal hex_hovered(hex: Vector2i, world_position: Vector2) + +## unit_selected(unit_id: StringName) +## Emitted when the active UI selection changes to a unit. +signal unit_selected(unit_id: StringName) + +## city_selected(city_id: StringName) +## Emitted when the active UI selection changes to a city. +signal city_selected(city_id: StringName) + +## panel_opened(panel_id: StringName, context: Dictionary) +## Emitted when a UI panel is opened with optional context data. +signal panel_opened(panel_id: StringName, context: Dictionary) + + +func emit_unit_moved(unit_id: StringName, from_hex: Vector2i, to_hex: Vector2i) -> void: + _log_event("unit_moved", [unit_id, from_hex, to_hex]) + unit_moved.emit(unit_id, from_hex, to_hex) + + +func emit_unit_attacked( + attacker_id: StringName, + defender_id: StringName, + damage: int, + remaining_health: int +) -> void: + _log_event("unit_attacked", [attacker_id, defender_id, damage, remaining_health]) + unit_attacked.emit(attacker_id, defender_id, damage, remaining_health) + + +func emit_unit_died(unit_id: StringName, owner_id: StringName, hex: Vector2i) -> void: + _log_event("unit_died", [unit_id, owner_id, hex]) + unit_died.emit(unit_id, owner_id, hex) + + +func emit_unit_promoted( + unit_id: StringName, + promotion_id: StringName, + bonuses: Dictionary = {} +) -> void: + _log_event("unit_promoted", [unit_id, promotion_id, bonuses]) + unit_promoted.emit(unit_id, promotion_id, bonuses) + + +func emit_city_built(city_id: StringName, owner_id: StringName, hex: Vector2i) -> void: + _log_event("city_built", [city_id, owner_id, hex]) + city_built.emit(city_id, owner_id, hex) + + +func emit_city_captured( + city_id: StringName, + previous_owner_id: StringName, + new_owner_id: StringName +) -> void: + _log_event("city_captured", [city_id, previous_owner_id, new_owner_id]) + city_captured.emit(city_id, previous_owner_id, new_owner_id) + + +func emit_production_complete( + city_id: StringName, + production_id: StringName, + result: Dictionary = {} +) -> void: + _log_event("production_complete", [city_id, production_id, result]) + production_complete.emit(city_id, production_id, result) + + +func emit_turn_started(turn_number: int, player_id: StringName) -> void: + _log_event("turn_started", [turn_number, player_id]) + turn_started.emit(turn_number, player_id) + + +func emit_turn_phase_changed( + turn_number: int, + previous_phase: StringName, + new_phase: StringName +) -> void: + _log_event("turn_phase_changed", [turn_number, previous_phase, new_phase]) + turn_phase_changed.emit(turn_number, previous_phase, new_phase) + + +func emit_turn_ended(turn_number: int, player_id: StringName) -> void: + _log_event("turn_ended", [turn_number, player_id]) + turn_ended.emit(turn_number, player_id) + + +func emit_wallet_connected(address: String, provider: StringName = &"") -> void: + _log_event("wallet_connected", [address, provider]) + wallet_connected.emit(address, provider) + + +func emit_proof_generated(proof_id: StringName, proof: Dictionary = {}) -> void: + _log_event("proof_generated", [proof_id, proof]) + proof_generated.emit(proof_id, proof) + + +func emit_tx_confirmed(tx_hash: String, receipt: Dictionary = {}) -> void: + _log_event("tx_confirmed", [tx_hash, receipt]) + tx_confirmed.emit(tx_hash, receipt) + + +func emit_web3_error( + action: StringName, + message: String, + details: Dictionary = {} +) -> void: + _log_event("web3_error", [action, message, details]) + web3_error.emit(action, message, details) + + +func emit_hex_hovered(hex: Vector2i, world_position: Vector2 = Vector2.ZERO) -> void: + _log_event("hex_hovered", [hex, world_position]) + hex_hovered.emit(hex, world_position) + + +func emit_unit_selected(unit_id: StringName) -> void: + _log_event("unit_selected", [unit_id]) + unit_selected.emit(unit_id) + + +func emit_city_selected(city_id: StringName) -> void: + _log_event("city_selected", [city_id]) + city_selected.emit(city_id) + + +func emit_panel_opened(panel_id: StringName, context: Dictionary = {}) -> void: + _log_event("panel_opened", [panel_id, context]) + panel_opened.emit(panel_id, context) + + +func _log_event(event_name: StringName, args: Array) -> void: + if not OS.is_debug_build(): + return + print_debug("[EventBus] %s %s" % [event_name, args]) diff --git a/godot/project.godot b/godot/project.godot index 7a31998..3b7a8fa 100644 --- a/godot/project.godot +++ b/godot/project.godot @@ -14,6 +14,7 @@ run/main_scene="res://scenes/Main.tscn" [autoload] +EventBus="*res://autoloads/EventBus.gd" WebBridge="*res://autoloads/WebBridge.gd" [display] From eacb24a294d078ab2e3077bcb42498f8bf721b75 Mon Sep 17 00:00:00 2001 From: Vinay Date: Tue, 30 Jun 2026 00:03:09 +0530 Subject: [PATCH 06/10] Disable Godot web icon export --- godot/export_presets.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/godot/export_presets.cfg b/godot/export_presets.cfg index ccad80a..ead3b18 100644 --- a/godot/export_presets.cfg +++ b/godot/export_presets.cfg @@ -19,7 +19,7 @@ variant/extensions_support=false variant/thread_support=false vram_texture_compression/for_desktop=true vram_texture_compression/for_mobile=true -html/export_icon=true +html/export_icon=false html/custom_html_shell="" html/head_include="" html/canvas_resize_policy=2 From 5a0d72754178bbd4f9385d3a6bc80cfc21185a5b Mon Sep 17 00:00:00 2001 From: Vinay Date: Tue, 30 Jun 2026 00:12:05 +0530 Subject: [PATCH 07/10] Complete Godot Web export preset defaults --- godot/export_presets.cfg | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/godot/export_presets.cfg b/godot/export_presets.cfg index ead3b18..33441a3 100644 --- a/godot/export_presets.cfg +++ b/godot/export_presets.cfg @@ -11,6 +11,14 @@ include_filter="" exclude_filter="" export_path="../dist/index.html" +patches=PackedStringArray() +encryption_include_filters="" +encryption_exclude_filters="" +seed=0 +encrypt_pck=false +encrypt_directory=false +script_export_mode=2 + [preset.0.options] custom_template/debug="" @@ -26,3 +34,11 @@ html/canvas_resize_policy=2 html/focus_canvas_on_start=true html/experimental_virtual_keyboard=false progressive_web_app/enabled=false +progressive_web_app/ensure_cross_origin_isolation_headers=false +progressive_web_app/offline_page="" +progressive_web_app/display=1 +progressive_web_app/orientation=0 +progressive_web_app/icon_144x144="" +progressive_web_app/icon_180x180="" +progressive_web_app/icon_512x512="" +progressive_web_app/background_color=Color(0, 0, 0, 1) From 37c85bb3811f143c19538da2d0bdc065807eff97 Mon Sep 17 00:00:00 2001 From: Vinay Date: Tue, 30 Jun 2026 00:27:43 +0530 Subject: [PATCH 08/10] Fix Godot Web mobile texture export setting --- godot/export_presets.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/godot/export_presets.cfg b/godot/export_presets.cfg index 33441a3..157110b 100644 --- a/godot/export_presets.cfg +++ b/godot/export_presets.cfg @@ -26,7 +26,7 @@ custom_template/release="" variant/extensions_support=false variant/thread_support=false vram_texture_compression/for_desktop=true -vram_texture_compression/for_mobile=true +vram_texture_compression/for_mobile=false html/export_icon=false html/custom_html_shell="" html/head_include="" From 6e6b36ac795e6fd092c375e79582a7258b2b0298 Mon Sep 17 00:00:00 2001 From: Vinay Date: Tue, 30 Jun 2026 00:46:50 +0530 Subject: [PATCH 09/10] Add Godot manager autoloads --- godot/autoloads/EconomyManager.gd | 57 ++++++++++++++++ godot/autoloads/GameState.gd | 108 ++++++++++++++++++++++++++++++ godot/autoloads/TurnManager.gd | 61 +++++++++++++++++ godot/project.godot | 3 + 4 files changed, 229 insertions(+) create mode 100644 godot/autoloads/EconomyManager.gd create mode 100644 godot/autoloads/GameState.gd create mode 100644 godot/autoloads/TurnManager.gd diff --git a/godot/autoloads/EconomyManager.gd b/godot/autoloads/EconomyManager.gd new file mode 100644 index 0000000..69ab4fd --- /dev/null +++ b/godot/autoloads/EconomyManager.gd @@ -0,0 +1,57 @@ +extends Node + +## Calculates simple per-turn resource yields from city data. +## Cities may provide a `yields` dictionary; numeric values are summed by key +## and applied to GameState.resources without adding broader economy rules. + + +func calculate_yields(cities: Array = []) -> Dictionary: + var source_cities := cities + if source_cities.is_empty() and has_node("/root/GameState"): + source_cities = GameState.cities + + var totals := {} + for city in source_cities: + if typeof(city) != TYPE_DICTIONARY: + continue + + var city_yields = city.get("yields", {}) + if typeof(city_yields) != TYPE_DICTIONARY: + continue + + for resource_key in city_yields.keys(): + var amount = city_yields[resource_key] + if typeof(amount) != TYPE_INT and typeof(amount) != TYPE_FLOAT: + continue + + var key := str(resource_key) + totals[key] = totals.get(key, 0) + amount + + return _normalize_numeric_dictionary(totals) + + +func apply_turn_yields() -> Dictionary: + var turn_yields := calculate_yields() + if not has_node("/root/GameState"): + return turn_yields + + for resource_key in turn_yields.keys(): + var current_amount = GameState.resources.get(resource_key, 0) + if typeof(current_amount) != TYPE_INT and typeof(current_amount) != TYPE_FLOAT: + current_amount = 0 + GameState.resources[resource_key] = current_amount + turn_yields[resource_key] + + GameState.resources = _normalize_numeric_dictionary(GameState.resources) + return turn_yields + + +func _normalize_numeric_dictionary(source: Dictionary) -> Dictionary: + var keys := source.keys() + keys.sort_custom(func(left, right): return str(left) < str(right)) + + var normalized := {} + for key in keys: + var value = source[key] + if typeof(value) == TYPE_INT or typeof(value) == TYPE_FLOAT: + normalized[str(key)] = value + return normalized diff --git a/godot/autoloads/GameState.gd b/godot/autoloads/GameState.gd new file mode 100644 index 0000000..c9ad6ff --- /dev/null +++ b/godot/autoloads/GameState.gd @@ -0,0 +1,108 @@ +extends Node + +## Stores the minimal global game snapshot shared by managers and bridge code. +## This autoload intentionally avoids gameplay rules; it only normalizes, +## serializes, and restores state in a predictable dictionary format. + +var map_seed: int = 0 +var current_turn: int = 0 +var active_player_id: StringName = &"" +var players: Array = [] +var units: Array = [] +var cities: Array = [] +var resources: Dictionary = {} +var metadata: Dictionary = {} + + +func reset() -> void: + map_seed = 0 + current_turn = 0 + active_player_id = &"" + players = [] + units = [] + cities = [] + resources = {} + metadata = {} + + +func serialize() -> Dictionary: + return { + "map_seed": map_seed, + "current_turn": current_turn, + "active_player_id": String(active_player_id), + "players": _normalize_array(players), + "units": _normalize_array(units), + "cities": _normalize_array(cities), + "resources": _normalize_dictionary(resources), + "metadata": _normalize_dictionary(metadata), + } + + +func deserialize(state: Dictionary) -> void: + map_seed = int(state.get("map_seed", 0)) + current_turn = int(state.get("current_turn", 0)) + active_player_id = StringName(str(state.get("active_player_id", ""))) + players = _normalize_array(_read_array(state, "players")) + units = _normalize_array(_read_array(state, "units")) + cities = _normalize_array(_read_array(state, "cities")) + resources = _normalize_dictionary(_read_dictionary(state, "resources")) + metadata = _normalize_dictionary(_read_dictionary(state, "metadata")) + + +func sync_from_chain(on_chain_state: Dictionary) -> void: + deserialize(on_chain_state) + + +func _read_array(source: Dictionary, key: String) -> Array: + var value = source.get(key, []) + if typeof(value) != TYPE_ARRAY: + return [] + return value + + +func _read_dictionary(source: Dictionary, key: String) -> Dictionary: + var value = source.get(key, {}) + if typeof(value) != TYPE_DICTIONARY: + return {} + return value + + +func _normalize_array(source: Array) -> Array: + var normalized: Array = [] + for item in source: + normalized.append(_normalize_value(item)) + return normalized + + +func _normalize_dictionary(source: Dictionary) -> Dictionary: + var keys := source.keys() + keys.sort_custom(func(left, right): return str(left) < str(right)) + + var normalized := {} + for key in keys: + normalized[str(key)] = _normalize_value(source[key]) + return normalized + + +func _normalize_value(value): + match typeof(value): + TYPE_DICTIONARY: + return _normalize_dictionary(value) + TYPE_ARRAY: + return _normalize_array(value) + TYPE_STRING_NAME: + return String(value) + TYPE_VECTOR2I: + return { + "x": value.x, + "y": value.y, + } + TYPE_VECTOR2: + return { + "x": value.x, + "y": value.y, + } + TYPE_NIL, TYPE_BOOL, TYPE_INT, TYPE_FLOAT, TYPE_STRING: + return value + _: + return str(value) diff --git a/godot/autoloads/TurnManager.gd b/godot/autoloads/TurnManager.gd new file mode 100644 index 0000000..6ce6e2a --- /dev/null +++ b/godot/autoloads/TurnManager.gd @@ -0,0 +1,61 @@ +extends Node + +## Owns generic turn and phase progression. +## This manager emits EventBus turn notifications and mirrors turn fields into +## GameState, but does not implement commit/reveal or gameplay-specific phases. + +const SETUP: StringName = &"setup" +const START: StringName = &"start" +const MAIN: StringName = &"main" +const END: StringName = &"end" + +var current_turn: int = 0 +var active_player_id: StringName = &"" +var current_phase: StringName = SETUP + + +func reset() -> void: + current_turn = 0 + active_player_id = &"" + current_phase = SETUP + _sync_game_state() + + +func begin_turn(player_id: StringName) -> void: + current_turn += 1 + active_player_id = player_id + current_phase = START + _sync_game_state() + + if has_node("/root/EventBus") and EventBus.has_method("emit_turn_started"): + EventBus.emit_turn_started(current_turn, active_player_id) + + +func change_phase(next_phase: StringName) -> void: + if next_phase == current_phase: + return + + var previous_phase := current_phase + current_phase = next_phase + + if has_node("/root/EventBus") and EventBus.has_method("emit_turn_phase_changed"): + EventBus.emit_turn_phase_changed(current_turn, previous_phase, current_phase) + + +func end_turn() -> void: + var ended_turn := current_turn + var ended_player_id := active_player_id + var previous_phase := current_phase + current_phase = END + + if has_node("/root/EventBus") and EventBus.has_method("emit_turn_phase_changed"): + EventBus.emit_turn_phase_changed(ended_turn, previous_phase, current_phase) + if has_node("/root/EventBus") and EventBus.has_method("emit_turn_ended"): + EventBus.emit_turn_ended(ended_turn, ended_player_id) + + +func _sync_game_state() -> void: + if not has_node("/root/GameState"): + return + GameState.current_turn = current_turn + GameState.active_player_id = active_player_id diff --git a/godot/project.godot b/godot/project.godot index 3b7a8fa..c3782ce 100644 --- a/godot/project.godot +++ b/godot/project.godot @@ -15,6 +15,9 @@ run/main_scene="res://scenes/Main.tscn" [autoload] EventBus="*res://autoloads/EventBus.gd" +GameState="*res://autoloads/GameState.gd" +EconomyManager="*res://autoloads/EconomyManager.gd" +TurnManager="*res://autoloads/TurnManager.gd" WebBridge="*res://autoloads/WebBridge.gd" [display] From a1873a0b73ed629cbcdc8b44aca9c6f79d97d8de Mon Sep 17 00:00:00 2001 From: Vinay Date: Tue, 30 Jun 2026 01:02:21 +0530 Subject: [PATCH 10/10] Add commit reveal turn phases --- godot/autoloads/TurnManager.gd | 124 +++++++++++++++++++++++++++++++-- 1 file changed, 119 insertions(+), 5 deletions(-) diff --git a/godot/autoloads/TurnManager.gd b/godot/autoloads/TurnManager.gd index 6ce6e2a..19f4bfe 100644 --- a/godot/autoloads/TurnManager.gd +++ b/godot/autoloads/TurnManager.gd @@ -2,12 +2,17 @@ extends Node ## Owns generic turn and phase progression. ## This manager emits EventBus turn notifications and mirrors turn fields into -## GameState, but does not implement commit/reveal or gameplay-specific phases. +## GameState. Commit/reveal phases only coordinate state and WebBridge calls; +## gameplay resolution remains outside this manager. const SETUP: StringName = &"setup" const START: StringName = &"start" const MAIN: StringName = &"main" const END: StringName = &"end" +const COMMIT: StringName = &"commit" +const REVEAL: StringName = &"reveal" +const RESOLVE: StringName = &"resolve" +const FINALIZED: StringName = &"finalized" var current_turn: int = 0 var active_player_id: StringName = &"" @@ -24,11 +29,13 @@ func reset() -> void: func begin_turn(player_id: StringName) -> void: current_turn += 1 active_player_id = player_id + var previous_phase := current_phase current_phase = START _sync_game_state() if has_node("/root/EventBus") and EventBus.has_method("emit_turn_started"): EventBus.emit_turn_started(current_turn, active_player_id) + _emit_turn_phase_changed(previous_phase, current_phase) func change_phase(next_phase: StringName) -> void: @@ -37,9 +44,56 @@ func change_phase(next_phase: StringName) -> void: var previous_phase := current_phase current_phase = next_phase + _sync_game_state() + + _emit_turn_phase_changed(previous_phase, current_phase) + + +func begin_commit(player_id: StringName) -> void: + begin_turn(player_id) + change_phase(COMMIT) + + +## Expected payload shape: +## { +## "player_id": String, "turn_number": int, "commitment": Variant, +## "context": Dictionary optional +## } +## The payload is forwarded unchanged to WebBridge.commit_action(). +func submit_commit(payload: Dictionary) -> void: + _set_game_state_metadata("last_commit_payload", payload.duplicate(true)) + _call_web_bridge(&"commit_action", payload) + + +func begin_reveal() -> void: + change_phase(REVEAL) + + +## Expected payload shape: +## { +## "player_id": String, "turn_number": int, "reveal": Variant, +## "proof": Variant optional, "context": Dictionary optional +## } +## The payload is forwarded unchanged to WebBridge.reveal_action(). +func submit_reveal(payload: Dictionary) -> void: + _set_game_state_metadata("last_reveal_payload", payload.duplicate(true)) + _call_web_bridge(&"reveal_action", payload) - if has_node("/root/EventBus") and EventBus.has_method("emit_turn_phase_changed"): - EventBus.emit_turn_phase_changed(current_turn, previous_phase, current_phase) + +func resolve_turn(result: Dictionary = {}) -> void: + _set_game_state_metadata("last_resolution_result", result.duplicate(true)) + change_phase(RESOLVE) + + +## Expected result shape: +## { +## "turn_number": int, "status": String, +## "receipt": Dictionary optional, "state": Dictionary optional +## } +## This records the caller-provided finalisation result without contract details. +func finalise_turn(result: Dictionary = {}) -> void: + _set_game_state_metadata("last_finalisation_result", result.duplicate(true)) + change_phase(FINALIZED) func end_turn() -> void: @@ -47,9 +101,9 @@ func end_turn() -> void: var ended_player_id := active_player_id var previous_phase := current_phase current_phase = END + _sync_game_state() - if has_node("/root/EventBus") and EventBus.has_method("emit_turn_phase_changed"): - EventBus.emit_turn_phase_changed(ended_turn, previous_phase, current_phase) + _emit_turn_phase_changed(previous_phase, current_phase) if has_node("/root/EventBus") and EventBus.has_method("emit_turn_ended"): EventBus.emit_turn_ended(ended_turn, ended_player_id) @@ -59,3 +113,63 @@ func _sync_game_state() -> void: return GameState.current_turn = current_turn GameState.active_player_id = active_player_id + _set_game_state_metadata("current_phase", String(current_phase)) + + +func _set_game_state_metadata(key: String, value) -> void: + if not has_node("/root/GameState"): + return + if typeof(GameState.metadata) != TYPE_DICTIONARY: + GameState.metadata = {} + GameState.metadata[key] = value + + +func _call_web_bridge(method_name: StringName, payload: Dictionary) -> bool: + if not has_node("/root/WebBridge"): + _emit_web3_error( + method_name, + "WebBridge autoload is unavailable.", + {"path": "/root/WebBridge"} + ) + return false + + var bridge := get_node("/root/WebBridge") + if not bridge.has_method(method_name): + _emit_web3_error( + method_name, + "WebBridge does not implement the requested method.", + {"method": String(method_name)} + ) + return false + + if bridge.has_method("is_available") and bridge.call("is_available") != true: + _emit_web3_error( + method_name, + "WebBridge is not available in this runtime.", + { + "path": "/root/WebBridge", + "method": String(method_name), + } + ) + return false + + bridge.call(method_name, payload) + return true + + +func _emit_turn_phase_changed(previous_phase: StringName, next_phase: StringName) -> void: + if previous_phase == next_phase: + return + if has_node("/root/EventBus") and EventBus.has_method("emit_turn_phase_changed"): + EventBus.emit_turn_phase_changed(current_turn, previous_phase, next_phase) + + +func _emit_web3_error( + action: StringName, + message: String, + details: Dictionary = {} +) -> void: + if has_node("/root/EventBus") and EventBus.has_method("emit_web3_error"): + EventBus.emit_web3_error(action, message, details) + else: + push_warning("%s: %s" % [action, message])