diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml deleted file mode 100644 index 2b39afd..0000000 --- a/.github/workflows/build-release.yml +++ /dev/null @@ -1,123 +0,0 @@ -name: Build and Release - -on: - push: - tags: - - 'v*' # Trigger on tags like v0.1.0 - workflow_dispatch: # Allow manual trigger - -jobs: - build-windows: - runs-on: windows-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Setup Rust - uses: actions-rs/toolchain@v1 - with: - toolchain: stable - override: true - - - name: Cache Rust dependencies - uses: Swatinem/rust-cache@v2 - - - name: Build Release - run: cargo build --release - - - name: Create game directory structure - run: | - mkdir -p dist/assets - copy target\release\my-rts-game.exe dist\ - xcopy assets dist\assets\ /E /I /Y - copy README.md dist\ 2>NUL || echo No README found - - - name: Download Inno Setup - run: | - Invoke-WebRequest -Uri "https://files.jrsoftware.org/is/6/innosetup-6.2.2.exe" -OutFile innosetup.exe - Start-Process -FilePath "innosetup.exe" -ArgumentList "/VERYSILENT /SUPPRESSMSGBOXES /NORESTART /SP-" -Wait - - - name: Create Inno Setup Script - run: | - $VERSION = "${{ github.ref_name }}" -replace "v", "" - cat > installer.iss << EOF - #define MyAppName "My RTS Game" - #define MyAppVersion "$VERSION" - #define MyAppPublisher "Your Name" - #define MyAppURL "https://github.com/${{ github.repository }}" - #define MyAppExeName "my-rts-game.exe" - - [Setup] - AppId={{YOUR_UNIQUE_APP_ID_HERE} - AppName={#MyAppName} - AppVersion={#MyAppVersion} - AppPublisher={#MyAppPublisher} - AppPublisherURL={#MyAppURL} - DefaultDirName={autopf}\{#MyAppName} - DefaultGroupName={#MyAppName} - AllowNoIcons=yes - Compression=lzma - SolidCompression=yes - OutputDir=installer - OutputBaseFilename=my-rts-game-setup-{#MyAppVersion} - ArchitecturesInstallIn64BitMode=x64 - - [Languages] - Name: "english"; MessagesFile: "compiler:Default.isl" - - [Tasks] - Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked - - [Files] - Source: "dist\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs - - [Icons] - Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" - Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}" - Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon - - [Run] - Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent - EOF - shell: pwsh - - - name: Compile Installer - run: | - & "C:\Program Files (x86)\Inno Setup 6\ISCC.exe" installer.iss - shell: pwsh - - - name: Create ZIP Archive - run: | - Compress-Archive -Path dist\* -DestinationPath my-rts-game-${{ github.ref_name }}.zip - shell: pwsh - - - name: Upload Installer - uses: actions/upload-artifact@v4 - with: - name: windows-installer - path: installer/*.exe - - - name: Upload ZIP - uses: actions/upload-artifact@v4 - with: - name: windows-zip - path: my-rts-game-${{ github.ref_name }}.zip - - release: - needs: build-windows - runs-on: ubuntu-latest - if: startsWith(github.ref, 'refs/tags/') - steps: - - name: Download all artifacts - uses: actions/download-artifact@v4 - - - name: Create Release - id: create_release - uses: softprops/action-gh-release@v2 - with: - files: | - windows-installer/*.exe - windows-zip/*.zip - draft: false - prerelease: false - generate_release_notes: true diff --git a/.github/workflows/test-build-release.yml b/.github/workflows/test-build-release.yml new file mode 100644 index 0000000..535978b --- /dev/null +++ b/.github/workflows/test-build-release.yml @@ -0,0 +1,219 @@ +name: Test, Build and Release + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + schedule: + - cron: '0 0 * * *' # Run every day at midnight UTC for nightly builds + workflow_dispatch: # Allow manual trigger + +jobs: + test: + name: Run Tests + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Linux dependencies + run: | + sudo apt-get update + sudo apt-get install -y libasound2-dev libudev-dev pkg-config + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + with: + components: clippy + + - name: Cache Rust dependencies + id: rust-cache + uses: Swatinem/rust-cache@v2 + with: + cache-on-failure: true + shared-key: "${{ runner.os }}-rust-${{ hashFiles('**/Cargo.lock') }}" + + - name: Debug cache info + run: | + echo "Runner OS: ${{ runner.os }}" + echo "Rust version: $(rustc --version)" + echo "Cargo.lock hash: ${{ hashFiles('**/Cargo.lock') }}" + echo "Cache saved: ${{ steps.rust-cache.outputs.cache-hit != 'true' }}" + shell: bash + + - name: Run tests + run: cargo test + + - name: Run clippy + run: cargo clippy -- -D warnings + + build-windows: + name: Build Windows + needs: test # This ensures tests must pass before build starts + runs-on: windows-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + + - name: Cache Rust dependencies + id: rust-cache + uses: Swatinem/rust-cache@v2 + with: + cache-on-failure: true + shared-key: "${{ runner.os }}-rust-${{ hashFiles('**/Cargo.lock') }}" + + - name: Debug cache info + run: | + echo "Runner OS: ${{ runner.os }}" + echo "Rust version: $(rustc --version)" + echo "Cargo.lock hash: ${{ hashFiles('**/Cargo.lock') }}" + echo "Cache saved: ${{ steps.rust-cache.outputs.cache-hit != 'true' }}" + shell: pwsh + + - name: Configure Cargo settings + run: | + echo "CARGO_INCREMENTAL=0" >> $env:GITHUB_ENV + echo "CARGO_NET_RETRY=10" >> $env:GITHUB_ENV + echo "CARGO_BUILD_JOBS=8" >> $env:GITHUB_ENV # Adjust based on runner resources + shell: pwsh + + - name: Build Release + run: cargo build --profile ci + + - name: Create game directory structure + run: | + # Create base dist directory + mkdir -p dist + + # Copy executable + Write-Host "Copying executable..." + copy target\ci\my-rts-game.exe dist\ + + # Copy assets to the correct structure - extract from assets/ to dist/ + if (Test-Path -Path assets) { + Write-Host "Copying assets to distribution folder..." + # Get all files in assets directory + Get-ChildItem -Path assets -Recurse -File | ForEach-Object { + # Get relative path from assets directory + $relativePath = $_.FullName.Substring((Get-Item -Path "assets").FullName.Length + 1) + # Create target directory + $targetDir = Join-Path -Path "dist" -ChildPath (Split-Path -Path $relativePath) + if (-not (Test-Path -Path $targetDir)) { + New-Item -Path $targetDir -ItemType Directory | Out-Null + } + # Copy file + Copy-Item -Path $_.FullName -Destination (Join-Path -Path "dist" -ChildPath $relativePath) -Force + } + } + + # Copy documentation + if (Test-Path -Path README.md) { copy README.md dist\ } + + # List final directory structure for debugging + Write-Host "Final directory structure:" + Get-ChildItem -Path dist -Recurse -File | Select-Object FullName + shell: pwsh + + - name: Generate timestamp + id: timestamp + run: echo "value=$(Get-Date -Format 'yyyyMMdd-HHmmss')" >> $env:GITHUB_OUTPUT + shell: pwsh + + - name: Create ZIP Archive + run: | + Compress-Archive -Path dist\* -DestinationPath my-rts-game-nightly-${{ steps.timestamp.outputs.value }}.zip + shell: pwsh + + - name: Upload ZIP + uses: actions/upload-artifact@v4 + with: + name: windows-zip + path: my-rts-game-nightly-${{ steps.timestamp.outputs.value }}.zip + + - name: Install Inno Setup using Chocolatey + run: | + choco install -y innosetup + shell: powershell + + - name: Create Inno Setup Script + run: | + $timestamp = "${{ steps.timestamp.outputs.value }}" + @" + #define MyAppName "My RTS Game" + #define MyAppVersion "nightly-$timestamp" + #define MyAppPublisher "Me" + #define MyAppURL "https://github.com/${{ github.repository }}" + #define MyAppExeName "my-rts-game.exe" + + [Setup] + AppId={{c8b3976e-4dd1-4591-8544-2c638855de99}} + AppName={#MyAppName} + AppVersion={#MyAppVersion} + AppPublisher={#MyAppPublisher} + AppPublisherURL={#MyAppURL} + DefaultDirName={autopf}\{#MyAppName} + DefaultGroupName={#MyAppName} + AllowNoIcons=yes + Compression=lzma + SolidCompression=yes + OutputDir=installer + OutputBaseFilename=my-rts-game-setup-{#MyAppVersion} + ArchitecturesInstallIn64BitMode=x64 + + [Languages] + Name: "english"; MessagesFile: "compiler:Default.isl" + + [Tasks] + Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked + + [Files] + Source: "dist\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs + + [Icons] + Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; WorkingDir: "{app}" + Name: "{group}\{cm:UninstallProgram,{#MyAppName}}"; Filename: "{uninstallexe}" + Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; WorkingDir: "{app}"; Tasks: desktopicon + + [Run] + Filename: "{app}\{#MyAppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(MyAppName, '&', '&&')}}"; Flags: nowait postinstall skipifsilent + "@ | Out-File -FilePath installer.iss -Encoding utf8 + shell: pwsh + + - name: Compile Installer + run: | + & "C:\Program Files (x86)\Inno Setup 6\ISCC.exe" installer.iss + shell: pwsh + + - name: Upload Installer + uses: actions/upload-artifact@v4 + with: + name: windows-installer + path: installer/*.exe + + create-release: + name: Create Release + needs: build-windows + if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + steps: + - name: Generate timestamp for release name + id: timestamp + run: echo "value=$(date +'%Y%m%d-%H%M%S')" >> $GITHUB_OUTPUT + + - name: Download all artifacts + uses: actions/download-artifact@v4 + + - name: Create Nightly Release + uses: softprops/action-gh-release@v1 + with: + name: "Nightly Build ${{ steps.timestamp.outputs.value }}" + tag_name: "nightly-${{ steps.timestamp.outputs.value }}" + files: | + windows-installer/*.exe + windows-zip/*.zip + prerelease: true + generate_release_notes: true diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..6d45c23 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "github-actions.workflows.pinned.workflows": [ + ".github/workflows/test-build-release.yml" + ] +} diff --git a/Cargo.toml b/Cargo.toml index 2c49dd9..de0fba8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,5 +3,20 @@ name = "my-rts-game" version = "0.1.0" edition = "2021" +[profile.ci] +inherits = "release" +debug = false +opt-level = 2 +codegen-units = 16 +lto = "thin" + [dependencies] -bevy = "0.14.0" +bevy = { version = "0.14.2", default-features = false, features = [ + "bevy_asset", + "bevy_sprite", + "bevy_ui", + "bevy_text", + "bevy_render", + "bevy_core_pipeline", + "default_font" # Note: singular "font", not "fonts" +]} diff --git a/TODO.md b/TODO.md index 03df5ef..349b5bb 100644 --- a/TODO.md +++ b/TODO.md @@ -5,6 +5,8 @@ - [x] Fix worker movement bug - prevent workers from drifting downward after filling their inventory - [x] Build an exe file for Windows - [x] Add a dependabot setup +- [x] Setup test framework +- [ ] Add complete unit tests for each .rs file - [ ] Add an icon for the game - [ ] Add a splash screen - [ ] Add a main menu diff --git a/src/components/inventory.rs b/src/components/inventory.rs index b291982..6d65f88 100644 --- a/src/components/inventory.rs +++ b/src/components/inventory.rs @@ -85,3 +85,172 @@ impl Inventory { &self.resources } } + +// Replace the current tests module with this more comprehensive version + +#[cfg(test)] +mod tests { + use super::*; + use crate::resources::ResourceId; + + #[test] + fn test_inventory_new() { + let inventory = Inventory::new(20); + assert_eq!(inventory.capacity(), 20); + assert_eq!(inventory.used_capacity(), 0); + assert!(inventory.is_empty()); + assert!(inventory.resources().is_empty()); + } + + #[test] + fn test_inventory_capacity() { + let inventory = Inventory::new(20); + assert_eq!(inventory.capacity(), 20); + } + + #[test] + fn test_inventory_is_full() { + let mut inventory = Inventory::new(10); + let resource_id = ResourceId("gold".to_string()); + + // Initially empty + assert!(!inventory.is_full()); + + // Add resources until full + inventory.add(&resource_id, 10); + assert!(inventory.is_full()); + } + + #[test] + fn test_inventory_is_empty() { + let mut inventory = Inventory::new(10); + let gold = ResourceId("gold".to_string()); + + // Initially empty + assert!(inventory.is_empty()); + + // Add some resources + inventory.add(&gold, 5); + assert!(!inventory.is_empty()); + + // Remove all resources + inventory.remove(&gold, 5); + assert!(inventory.is_empty()); + } + + #[test] + fn test_inventory_used_capacity() { + let mut inventory = Inventory::new(30); + let gold = ResourceId("gold".to_string()); + let wood = ResourceId("wood".to_string()); + + assert_eq!(inventory.used_capacity(), 0); + + inventory.add(&gold, 10); + assert_eq!(inventory.used_capacity(), 10); + + inventory.add(&wood, 15); + assert_eq!(inventory.used_capacity(), 25); + + inventory.remove(&gold, 5); + assert_eq!(inventory.used_capacity(), 20); + } + + #[test] + fn test_inventory_add_remove() { + let mut inventory = Inventory::new(30); + let gold = ResourceId("gold".to_string()); + let wood = ResourceId("wood".to_string()); + + inventory.add(&gold, 10); + inventory.add(&wood, 5); + + assert_eq!(inventory.get_amount(&gold), 10); + assert_eq!(inventory.get_amount(&wood), 5); + + inventory.remove(&gold, 3); + assert_eq!(inventory.get_amount(&gold), 7); + + // Test removing more than available + inventory.remove(&wood, 10); + assert_eq!(inventory.get_amount(&wood), 0); + } + + #[test] + fn test_inventory_add_beyond_capacity() { + let mut inventory = Inventory::new(10); + let gold = ResourceId("gold".to_string()); + + // Try to add more than capacity + let added = inventory.add(&gold, 15); + + // Should only add up to capacity + assert_eq!(added, 10); + assert_eq!(inventory.get_amount(&gold), 10); + assert_eq!(inventory.used_capacity(), 10); + assert!(inventory.is_full()); + } + + #[test] + fn test_inventory_add_to_existing() { + let mut inventory = Inventory::new(20); + let gold = ResourceId("gold".to_string()); + + inventory.add(&gold, 5); + assert_eq!(inventory.get_amount(&gold), 5); + + inventory.add(&gold, 3); + assert_eq!(inventory.get_amount(&gold), 8); + } + + #[test] + fn test_inventory_remove_from_empty() { + let mut inventory = Inventory::new(20); + let gold = ResourceId("gold".to_string()); + + // Try to remove from empty + let removed = inventory.remove(&gold, 5); + + // Should remove nothing + assert_eq!(removed, 0); + assert_eq!(inventory.used_capacity(), 0); + } + + #[test] + fn test_inventory_resources_map() { + let mut inventory = Inventory::new(30); + let gold = ResourceId("gold".to_string()); + let wood = ResourceId("wood".to_string()); + + inventory.add(&gold, 10); + inventory.add(&wood, 5); + + let resources = inventory.resources(); + assert_eq!(resources.len(), 2); + assert!(resources.contains_key(&gold)); + assert!(resources.contains_key(&wood)); + assert_eq!(*resources.get(&gold).unwrap(), 10); + assert_eq!(*resources.get(&wood).unwrap(), 5); + } + + #[test] + fn test_inventory_get_nonexistent() { + let inventory = Inventory::new(10); + let nonexistent = ResourceId("nonexistent".to_string()); + + // Should return 0 for nonexistent resources + assert_eq!(inventory.get_amount(&nonexistent), 0); + } + + #[test] + fn test_inventory_remove_all_removes_entry() { + let mut inventory = Inventory::new(20); + let gold = ResourceId("gold".to_string()); + + inventory.add(&gold, 5); + assert!(inventory.resources().contains_key(&gold)); + + inventory.remove(&gold, 5); // Remove all + assert!(!inventory.resources().contains_key(&gold)); // Entry should be gone + } +} diff --git a/src/main.rs b/src/main.rs index 1e47644..0d2fc96 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,9 +8,9 @@ mod resources; mod systems; use components::unit::{Selectable, Unit, WorkerAnimation, WorkerAnimationState, Velocity, UnitAttributes}; -use components::resource::ResourceNode; // This should work now +use components::resource::ResourceNode; use resources::{PlayerResources, ResourceRegistry, ResourceId, GameState}; -use systems::selection::{selection_system, highlight_selected, animate_selection_rings, update_selection_ring_positions}; +use systems::selection::{selection_system, highlight_selected, animate_selection_rings, update_selection_ring}; use systems::animation::{animate_workers, update_worker_animations, animate_gather_effects, animate_floating_text}; use systems::movement::{move_command_system, movement_system, show_destination_markers}; use systems::gathering::{resource_gathering_command, gathering_system}; @@ -30,13 +30,13 @@ fn main() { })) .init_resource::() .init_resource::() - .init_resource::() // Initialize resource registry + .init_resource::() .add_systems(Startup, (setup, setup_ui)) .add_systems(Update, ( selection_system, highlight_selected, animate_selection_rings, - update_selection_ring_positions, + update_selection_ring, animate_workers, move_command_system, movement_system, diff --git a/src/resources.rs b/src/resources.rs index 2fd4879..af6e6cc 100644 --- a/src/resources.rs +++ b/src/resources.rs @@ -114,16 +114,8 @@ impl PlayerResources { } // Game state (keep this if you need it) -#[derive(Resource)] +#[derive(Resource, Default)] pub struct GameState { #[allow(dead_code)] pub paused: bool, // Will be used for pause functionality } - -impl Default for GameState { - fn default() -> Self { - GameState { - paused: false, - } - } -} diff --git a/src/systems/gathering.rs b/src/systems/gathering.rs index 44bd99f..6a81f16 100644 --- a/src/systems/gathering.rs +++ b/src/systems/gathering.rs @@ -77,13 +77,14 @@ pub fn resource_gathering_command( } // Enhanced gathering system +#[allow(clippy::type_complexity)] pub fn gathering_system( time: Res