Skip to content
Open
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
31 changes: 31 additions & 0 deletions .github/workflows/godot-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: Godot HTML5 Build

on:
push:
branches: [main, develop]
pull_request:
branches: [main]

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: chickensoft-games/setup-godot@v1

Check failure on line 14 in .github/workflows/godot-ci.yml

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use full commit SHA hash for this dependency.

See more on https://sonarcloud.io/project/issues?id=Bitcoindefi_Human-vs-bots&issues=AZ8Q1oSTIHStKnsF95h4&open=AZ8Q1oSTIHStKnsF95h4&pullRequest=53
with:
version: 4.3.0
use-dotnet: false
- run: godot --version
- run: |
cd godot
godot --headless --import
godot --headless --export-release "Web" ../dist/index.html
- uses: actions/upload-artifact@v4
with:
name: human-vs-bots-html5
path: dist/
- uses: peaceiris/actions-gh-pages@v4

Check failure on line 27 in .github/workflows/godot-ci.yml

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use full commit SHA hash for this dependency.

See more on https://sonarcloud.io/project/issues?id=Bitcoindefi_Human-vs-bots&issues=AZ8Q1oSTIHStKnsF95h5&open=AZ8Q1oSTIHStKnsF95h5&pullRequest=53
if: github.ref == 'refs/heads/main'
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./dist
1 change: 1 addition & 0 deletions .sonarcloud.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
sonar.exclusions=dist/**/*,.godot/**/*,demo/**/*,**/*.min.js,**/*.wasm
14 changes: 14 additions & 0 deletions export_presets.cfg
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
[preset.0]
name="Web"
platform="Web"
runnable=true
export_filter="all_resources"
export_path="dist/index.html"

[preset.0.options]
variant/thread_support=true
vram_texture_compression/for_desktop=true
html/export_icon=true
html/head_include="<script src='web3_bridge.js'></script>"
html/canvas_resize_policy=2
html/focus_canvas_on_start=true
38 changes: 20 additions & 18 deletions godot/project.godot
Original file line number Diff line number Diff line change
@@ -1,29 +1,31 @@
; Engine configuration file.
; Edit this file through the Godot editor when possible.
;
; Format:
; [section]
; param=value

config_version=5

[application]
config/name="Human vs Bots"
config/description="Hexagonal strategy game with Web3/ZK integration"
run/main_scene="res://scenes/main.tscn"
config/features=PackedStringArray("4.3", "Mobile")

config/name="HumanVsBots"
run/main_scene="res://scenes/Main.tscn"
[autoload]
GameState="*res://src/autoload/GameState.gd"
TurnManager="*res://src/autoload/TurnManager.gd"
EconomyManager="*res://src/autoload/EconomyManager.gd"
EventBus="*res://src/autoload/EventBus.gd"
Web3Bridge="*res://src/autoload/Web3Bridge.gd"
AudioManager="*res://src/autoload/AudioManager.gd"

[autoload]

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

[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"
window/stretch/aspect="expand"

[input]
zoom_in={"deadzone":0.5,"events":[Object(InputEventMouseButton,"button_index":4)]}
zoom_out={"deadzone":0.5,"events":[Object(InputEventMouseButton,"button_index":5)]}
pan_map={"deadzone":0.5,"events":[Object(InputEventMouseButton,"button_index":3)]}
select_unit={"deadzone":0.5,"events":[Object(InputEventMouseButton,"button_index":1)]}
toggle_conquer={"deadzone":0.5,"events":[Object(InputEventKey,"keycode":67)]}
end_turn={"deadzone":0.5,"events":[Object(InputEventKey,"keycode":32)]}
9 changes: 9 additions & 0 deletions sonar-project.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# SonarCloud exclusions for Godot HTML5 project
sonar.exclusions=\
dist/**/*,\
.godot/**/*,\
demo/**/*,\
scripts/**/*,\
**/*.min.js,\
**/*.wasm,\
**/*.html
25 changes: 25 additions & 0 deletions src/Main.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
extends Node

func _ready() -> void:
NoiseGenerator.set_seed(randi())
await _load_assets()

var map_gen = MapGenerator.new()
if not map_gen.generate_with_retries():
push_error("Failed to generate map"); return

var hex_map = $Game/HexMap
if hex_map: hex_map.render_map()

EconomyManager.reset()
AudioManager.play_music("ambient_strategy")
EventBus.emit_signal("hud_message", "Ready: produce units, conquer tiles, defeat bot tech-core.", "ok")

func _load_assets() -> void:
for terrain_file in ["Grassland.png", "Forest.png", "Hill.png", "Coast.png", "Desert.png"]:
var path = "res://assets/terrain/" + terrain_file
if ResourceLoader.exists(path): ResourceLoader.load(path)
for unit_file in ["Warrior.png", "Tank.png", "Infantry.png"]:
var path = "res://assets/units/" + unit_file
if ResourceLoader.exists(path): ResourceLoader.load(path)
await get_tree().create_timer(0.1).timeout
108 changes: 108 additions & 0 deletions src/autoload/AudioManager.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
extends Node

const BUS_MASTER: String = "Master"
const BUS_MUSIC: String = "Music"
const BUS_SFX: String = "SFX"

var music_player: AudioStreamPlayer
var sfx_players: Array = []
var max_sfx_players: int = 8
var music_tracks: Dictionary = {}
var sfx_clips: Dictionary = {}
var current_music: String = ""
var music_volume: float = 0.7; var sfx_volume: float = 0.8; var master_volume: float = 1.0

func _ready() -> void:
_setup_audio_buses(); _setup_players(); _connect_events()

func _setup_audio_buses() -> void:
if AudioServer.get_bus_index(BUS_MUSIC) == -1:
AudioServer.add_bus(AudioServer.bus_count)
AudioServer.set_bus_name(AudioServer.bus_count - 1, BUS_MUSIC)
if AudioServer.get_bus_index(BUS_SFX) == -1:
AudioServer.add_bus(AudioServer.bus_count)
AudioServer.set_bus_name(AudioServer.bus_count - 1, BUS_SFX)
AudioServer.set_bus_send(AudioServer.get_bus_index(BUS_MUSIC), BUS_MASTER)
AudioServer.set_bus_send(AudioServer.get_bus_index(BUS_SFX), BUS_MASTER)

func _setup_players() -> void:
music_player = AudioStreamPlayer.new()
music_player.bus = BUS_MUSIC; music_player.name = "MusicPlayer"
add_child(music_player)
for i in range(max_sfx_players):
var player = AudioStreamPlayer.new()
player.bus = BUS_SFX; player.name = "SFXPlayer_%d" % i
add_child(player); sfx_players.append(player)

func _connect_events() -> void:
EventBus.connect("play_sfx", _on_play_sfx)
EventBus.connect("play_music", _on_play_music)
EventBus.connect("stop_music", _on_stop_music)

func _on_play_sfx(sfx_name: String) -> void: play_sfx(sfx_name)
func _on_play_music(track_name: String) -> void: play_music(track_name)
func _on_stop_music() -> void: stop_music()

func play_music(track_name: String, fade_duration: float = 1.0) -> void:
if track_name == current_music and music_player.playing: return
var track = _load_music_track(track_name)
if not track: push_warning("AudioManager: Track not found: " + track_name); return
if music_player.playing and fade_duration > 0:
var tween = create_tween(); tween.tween_property(music_player, "volume_db", -80.0, fade_duration / 2.0)
await tween.finished; music_player.stop()
music_player.stream = track; music_player.volume_db = -80.0; music_player.play(); current_music = track_name
if fade_duration > 0:
var target_db = linear_to_db(music_volume * master_volume)
var tween = create_tween(); tween.tween_property(music_player, "volume_db", target_db, fade_duration / 2.0)

func stop_music(fade_duration: float = 0.5) -> void:
if not music_player.playing: return
if fade_duration > 0:
var tween = create_tween(); tween.tween_property(music_player, "volume_db", -80.0, fade_duration)
await tween.finished
music_player.stop(); current_music = ""

func play_sfx(sfx_name: String, pitch_variation: float = 0.0) -> void:
var clip = _load_sfx_clip(sfx_name)
if not clip: push_warning("AudioManager: SFX not found: " + sfx_name); return
for player in sfx_players:
if not player.playing:
player.stream = clip; player.pitch_scale = 1.0 + randf_range(-pitch_variation, pitch_variation)
player.volume_db = linear_to_db(sfx_volume * master_volume); player.play(); return
sfx_players[0].stop(); sfx_players[0].stream = clip; sfx_players[0].play()

func set_master_volume(volume: float) -> void:
master_volume = clampf(volume, 0.0, 1.0)
AudioServer.set_bus_volume_db(AudioServer.get_bus_index(BUS_MASTER), linear_to_db(master_volume))

func set_music_volume(volume: float) -> void:
music_volume = clampf(volume, 0.0, 1.0)
var target_db = linear_to_db(music_volume * master_volume)
if music_player.playing:
var tween = create_tween(); tween.tween_property(music_player, "volume_db", target_db, 0.3)
else: music_player.volume_db = target_db

func _load_music_track(track_name: String) -> AudioStream:
if music_tracks.has(track_name): return music_tracks[track_name]
var path = "res://assets/audio/music/%s.ogg" % track_name
if ResourceLoader.exists(path):
var track = load(path); music_tracks[track_name] = track; return track
return null

func _load_sfx_clip(sfx_name: String) -> AudioStream:
if sfx_clips.has(sfx_name): return sfx_clips[sfx_name]
for ext in [".wav", ".ogg", ".mp3"]:
var path = "res://assets/audio/sfx/%s%s" % [sfx_name, ext]
if ResourceLoader.exists(path):
var clip = load(path); sfx_clips[sfx_name] = clip; return clip
return null

# Shortcuts
func play_ui_click() -> void: play_sfx("ui_click", 0.1)
func play_unit_move() -> void: play_sfx("unit_move", 0.15)
func play_unit_attack() -> void: play_sfx("unit_attack", 0.2)
func play_unit_die() -> void: play_sfx("unit_die", 0.1)
func play_conquer_tile() -> void: play_sfx("conquer_tile", 0.1)
func play_turn_end() -> void: play_sfx("turn_end")
func play_victory() -> void: play_sfx("victory")
func play_defeat() -> void: play_sfx("defeat")
82 changes: 82 additions & 0 deletions src/autoload/EconomyManager.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
extends Node

var food: int = 0
var production: int = 0
var science: int = 0
var gold: int = 0

var food_income: int = 0
var production_income: int = 0
var science_income: int = 0
var gold_income: int = 0

const TERRAIN_YIELDS: Dictionary = {
GameState.Terrain.PLAINS: {"food": 2, "production": 1, "science": 1, "gold": 1},
GameState.Terrain.FOREST: {"food": 1, "production": 2, "science": 1, "gold": 0},
GameState.Terrain.HILL: {"food": 0, "production": 3, "science": 0, "gold": 1},
GameState.Terrain.DESERT: {"food": 0, "production": 0, "science": 2, "gold": 2},
GameState.Terrain.WATER: {"food": 1, "production": 0, "science": 0, "gold": 2}
}

const STRUCTURE_BONUSES: Dictionary = {
GameState.StructureType.HQ: {"food": 5, "production": 3, "science": 3, "gold": 3},
GameState.StructureType.BARRACKS: {"food": 0, "production": 2, "science": 0, "gold": 0},
GameState.StructureType.FACTORY: {"food": 0, "production": 5, "science": 2, "gold": 1},
GameState.StructureType.TECH_CORE: {"food": 0, "production": 3, "science": 5, "gold": 2}
}

func _ready() -> void:
EventBus.connect("turn_started", _on_turn_started)
EventBus.connect("territory_changed", _on_territory_changed)
EventBus.connect("structure_built", _on_structure_built)

func reset() -> void:
food = 0; production = 0; science = 0; gold = 0
_recalculate_income()
_emit_resources_changed()

func _on_turn_started(_turn: int, _phase: int) -> void:
food += food_income; production += production_income
science += science_income; gold += gold_income
_emit_resources_changed()

func _recalculate_income() -> void:
food_income = 0; production_income = 0; science_income = 0; gold_income = 0

for cell in GameState.map_cells.values():
if cell.owner == GameState.Team.HUMAN:
var yields = TERRAIN_YIELDS.get(cell.terrain, TERRAIN_YIELDS[GameState.Terrain.PLAINS])
food_income += yields.food; production_income += yields.production
science_income += yields.science; gold_income += yields.gold

for structure in GameState.structures.values():
if structure.team == GameState.Team.HUMAN:
var bonus = STRUCTURE_BONUSES.get(structure.structure_type, {})
food_income += bonus.get("food", 0)
production_income += bonus.get("production", 0)
science_income += bonus.get("science", 0)
gold_income += bonus.get("gold", 0)

EventBus.emit_signal("income_calculated", food_income, production_income,
science_income, gold_income)

func _on_territory_changed(_coords: Vector2i, _new_owner: int) -> void: _recalculate_income()
func _on_structure_built(_id: String, _type: int, _team: int) -> void: _recalculate_income()

func _emit_resources_changed() -> void:
EventBus.emit_signal("resources_changed", food, production, science, gold)

func can_afford(costs: Dictionary) -> bool:
return (food >= costs.get("food", 0) and production >= costs.get("production", 0) and
science >= costs.get("science", 0) and gold >= costs.get("gold", 0))

func spend(costs: Dictionary) -> bool:
if not can_afford(costs): return false
food -= costs.get("food", 0); production -= costs.get("production", 0)
science -= costs.get("science", 0); gold -= costs.get("gold", 0)
_emit_resources_changed(); return true

func add_resources(amounts: Dictionary) -> void:
food += amounts.get("food", 0); production += amounts.get("production", 0)
science += amounts.get("science", 0); gold += amounts.get("gold", 0)
_emit_resources_changed()
42 changes: 42 additions & 0 deletions src/autoload/EventBus.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
extends Node

signal cell_clicked(cell_coords: Vector2i)
signal cell_hovered(cell_coords: Vector2i)
signal territory_changed(coords: Vector2i, new_owner: int)
signal unit_moved(unit_id: int, from_coords: Vector2i, to_coords: Vector2i)
signal unit_attacked(attacker_id: int, defender_id: int, damage: int)
signal unit_died(unit_id: int)
signal unit_spawned(unit_id: int, unit_type: int, team: int)
signal unit_selected(unit_id: int)
signal unit_deselected
signal turn_started(turn_number: int, phase: int)
signal turn_ended(turn_number: int)
signal phase_changed(new_phase: int)
signal structure_built(structure_id: String, structure_type: int, team: int)
signal structure_acted(structure_id: String)
signal unit_produced(structure_id: String, unit_id: int)
signal resources_changed(food: int, production: int, science: int, gold: int)
signal income_calculated(food_income: int, production_income: int,
science_income: int, gold_income: int)
signal wallet_connected(address: String)
signal wallet_disconnected
signal game_started_on_chain(tx_hash: String)
signal proof_submitted(proof_id: String)
signal game_ended_on_chain(tx_hash: String)
signal zk_proof_generated(proof_data: Dictionary)
signal zk_proof_verified(result: bool)
signal hud_message(message: String, message_type: String)
signal match_result_displayed(result: String)
signal zoom_changed(zoom_level: float)
signal camera_panned(position: Vector2)
signal play_sfx(sfx_name: String)
signal play_music(track_name: String)
signal stop_music
signal combat_started(attacker_id: int, defender_id: int)
signal combat_ended(attacker_id: int, defender_id: int, attacker_won: bool)
signal tile_conquered(coords: Vector2i, conqueror_team: int)
signal game_reset
signal game_paused
signal game_resumed
signal save_requested
signal load_requested(save_data: Dictionary)
Loading