This commit is contained in:
geoffsee
2025-06-30 12:03:59 -04:00
commit 4c6c36eff1
73 changed files with 9322 additions and 0 deletions

76
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,76 @@
name: CI
on:
pull_request:
push:
branches:
- clean-master
jobs:
test:
strategy:
matrix:
os: [windows-latest, ubuntu-latest, macos-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
- uses: actions/cache@v4
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-test-${{ hashFiles('**/Cargo.toml') }}
- uses: dtolnay/rust-toolchain@master
with:
toolchain: stable
- name: Install alsa and udev
run: sudo apt-get update; sudo apt-get install --no-install-recommends libasound2-dev libudev-dev
if: runner.os == 'linux'
- name: Build & run tests
run: cargo test
all-doc-tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/cache@v4
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ubuntu-latest-cargo-all-doc-tests-${{ hashFiles('**/Cargo.toml') }}
- uses: dtolnay/rust-toolchain@master
with:
toolchain: stable
- name: Install alsa and udev
run: sudo apt-get update; sudo apt-get install --no-install-recommends libasound2-dev libudev-dev
- name: Run doc tests with all features (this also compiles README examples)
run: cargo test --doc --all-features
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/cache@v4
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ubuntu-latest-cargo-lint-${{ hashFiles('**/Cargo.toml') }}
- uses: dtolnay/rust-toolchain@master
with:
toolchain: stable
components: rustfmt, clippy
- name: Install alsa and udev
run: sudo apt-get update; sudo apt-get install --no-install-recommends libasound2-dev libudev-dev
- name: Run clippy
run: cargo clippy --workspace --all-targets --all-features -- -Dwarnings
- name: Check format
run: cargo fmt --all -- --check

40
.github/workflows/deploy-page.yaml vendored Normal file
View File

@@ -0,0 +1,40 @@
name: deploy-github-page
on:
workflow_dispatch:
permissions:
contents: write
jobs:
build-web:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install rust toolchain
uses: dtolnay/rust-toolchain@master
with:
toolchain: stable
- name: Install Dependencies
run: sudo apt-get update; sudo apt-get install pkg-config libx11-dev libasound2-dev libudev-dev
- name: Install trunk
uses: jetli/trunk-action@v0.4.0
with:
version: 'latest'
- name: Add wasm target
run: |
rustup target add wasm32-unknown-unknown
- name: Build Release
run: |
trunk build --release
- name: optimize Wasm
uses: NiklasEi/wasm-opt-action@v2
with:
file: dist/*.wasm
- name: Deploy to GitHub Pages
uses: JamesIves/github-pages-deploy-action@v4.2.5
with:
branch: gh-pages
folder: dist

View File

@@ -0,0 +1,75 @@
# For setup instructions regarding this workflow, see https://www.nikl.me/blog/2023/github_workflow_to_publish_android_app/
name: release-android-google-play
on:
workflow_dispatch:
inputs:
version:
description: 'GitHub Release'
required: true
type: string
play_release:
description: 'Release name from google play console'
required: true
type: string
env:
# used for uploading the app to a GitHub release
GAME_EXECUTABLE_NAME: yachtpit
BUNDLE_PATH: "target/x/release/android/mobile.aab"
PACKAGE_NAME: "io.gs.yachtpit"
# release track; you can promote a build to "higher" tracks in the play console or publish to a different track directly
# see track at https://github.com/r0adkll/upload-google-play#inputs for more options
TRACK: internal
MOBILE_DIRECTORY: mobile
permissions:
contents: write
jobs:
bundle-sign-release:
runs-on: ubuntu-latest
timeout-minutes: 40
steps:
- name: Install Dependencies
run: sudo apt-get update; sudo apt-get install pkg-config libx11-dev libasound2-dev libudev-dev lld llvm
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- name: Add Android targets
run: rustup target add aarch64-linux-android armv7-linux-androideabi
- name: Install cargo-binstall
run: curl -L --proto '=https' --tlsv1.2 -sSf https://raw.githubusercontent.com/cargo-bins/cargo-binstall/main/install-from-binstall-release.sh | bash
- name: Install xbuild
run: cargo binstall --git https://github.com/NiklasEi/xbuild --bin-dir x xbuild -y
- name: Build app bundle
run: |
cd ${{ env.MOBILE_DIRECTORY }}
x doctor
x build --release --platform android --store play
- name: sign app bundle
run: |
KEYSTORE_PATH=${{ runner.temp }}/upload-keystore.jks
echo -n "${{ secrets.PLAYSTORE_KEYSTORE }}" | base64 --decode > $KEYSTORE_PATH
jarsigner -verbose -sigalg SHA256withRSA -digestalg SHA-256 -keystore $KEYSTORE_PATH -storepass "${{ secrets.PLAYSTORE_KEYSTORE_PASSWORD }}" ${{ env.BUNDLE_PATH }} upload
- name: Upload self-signed bundle to GitHub
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: ${{ env.BUNDLE_PATH }}
asset_name: ${{ env.GAME_EXECUTABLE_NAME }}_${{ inputs.version }}_android.aab
release_name: ${{ inputs.version }}
tag: ${{ inputs.version }}
overwrite: true
- name: prepare Google play store secrets
run: |
SERVICE_ACCOUNT=${{ runner.temp }}/service-account.json
echo -n "${{ secrets.PLAYSTORE_SERVICE_ACCOUNT }}" | base64 --decode > $SERVICE_ACCOUNT
- name: upload bundle to Google play store
uses: r0adkll/upload-google-play@v1
with:
serviceAccountJson: ${{ runner.temp }}/service-account.json
packageName: ${{ env.PACKAGE_NAME }}
releaseName: ${{ inputs.play_release }}
releaseFiles: ${{ env.BUNDLE_PATH }}
track: ${{ env.TRACK }}

View File

@@ -0,0 +1,97 @@
name: release-ios-testflight
# This workflow builds, archives, exports, validates and uploads your ios app.
# The version from input is only used for artifact names and as the release to upload the final ipa to.
# Bump the versions in `mobile/ios-src/Info.plist` to change the version of your app bundle.
# Special setup and Apple Developer Program membership (99$/year) is required for this workflow!
# For setup instructions, see https://www.nikl.me/blog/2023/github_workflow_to_publish_ios_app/
on:
workflow_dispatch:
inputs:
version:
description: 'Version - in the form of v1.2.3'
required: true
type: string
env:
# used for uploading the app to a GitHub release
GAME_EXECUTABLE_NAME: yachtpit
XCODE_PROJECT: mobile
MOBILE_DIRECTORY: mobile
permissions:
contents: write
jobs:
build-for-iOS:
runs-on: macos-latest
timeout-minutes: 40
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- name: Add iOS targets
run: rustup target add aarch64-apple-ios
- name: Install the Apple certificate and provisioning profile
id: profile
env:
IOS_CERTIFICATE: ${{ secrets.IOS_CERTIFICATE }}
IOS_CERTIFICATE_PASSWORD: ${{ secrets.IOS_CERTIFICATE_PASSWORD }}
IOS_PROVISION_PROFILE: ${{ secrets.IOS_PROVISION_PROFILE }}
IOS_KEYCHAIN_PASSWORD: ${{ secrets.IOS_KEYCHAIN_PASSWORD }}
run: |
# create variables
CERTIFICATE_PATH=${{ runner.temp }}/build_certificate.p12
PP_PATH=${{ runner.temp }}/profile.mobileprovision
KEYCHAIN_PATH=${{ runner.temp }}/app-signing.keychain-db
# import certificate and provisioning profile from secrets
echo -n "$IOS_CERTIFICATE" | base64 --decode -o $CERTIFICATE_PATH
echo -n "$IOS_PROVISION_PROFILE" | base64 --decode -o $PP_PATH
uuid=`grep UUID -A1 -a $PP_PATH | grep -io "[-A-F0-9]\{36\}"`
echo "uuid=$uuid" >> $GITHUB_OUTPUT
# create temporary keychain
security create-keychain -p "$IOS_KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
security set-keychain-settings -lut 21600 $KEYCHAIN_PATH
security unlock-keychain -p "$IOS_KEYCHAIN_PASSWORD" $KEYCHAIN_PATH
# import certificate to keychain
security import $CERTIFICATE_PATH -P "$IOS_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
security list-keychain -d user -s $KEYCHAIN_PATH
# apply provisioning profile
mkdir -p ~/Library/MobileDevice/Provisioning\ Profiles
cp $PP_PATH ~/Library/MobileDevice/Provisioning\ Profiles/$uuid.mobileprovision
- name: Build app for iOS
run: |
cd ${{ env.MOBILE_DIRECTORY }}
xcodebuild PROVISIONING_PROFILE=${{ steps.profile.outputs.uuid }} -scheme ${{ env.XCODE_PROJECT }} clean archive -archivePath "Actions" -configuration Release -arch arm64
- name: export ipa
env:
EXPORT_PLIST: ${{ secrets.IOS_EXPORT_PRODUCTION }}
run: |
EXPORT_PLIST_PATH=${{ runner.temp }}/ExportOptions.plist
echo -n "$EXPORT_PLIST" | base64 --decode --output $EXPORT_PLIST_PATH
xcodebuild PROVISIONING_PROFILE=${{ steps.profile.outputs.uuid }} -exportArchive -archivePath ${{ env.MOBILE_DIRECTORY }}/Actions.xcarchive -exportOptionsPlist $EXPORT_PLIST_PATH -exportPath ${{ runner.temp }}/export
- name: decode API key
env:
API_KEY_BASE64: ${{ secrets.IOS_APPSTORE_API_PRIVATE_KEY }}
run: |
mkdir -p ~/private_keys
echo -n "$API_KEY_BASE64" | base64 --decode --output ~/private_keys/AuthKey_${{ secrets.IOS_APPSTORE_API_KEY_ID }}.p8
- name: Upload to testflight
run: |
xcrun altool --validate-app -f ${{ runner.temp }}/export/${{ env.XCODE_PROJECT }}.ipa -t ios --apiKey ${{ secrets.IOS_APPSTORE_API_KEY_ID }} --apiIssuer ${{ secrets.IOS_APPSTORE_ISSUER_ID }}
xcrun altool --upload-app -f ${{ runner.temp }}/export/${{ env.XCODE_PROJECT }}.ipa -t ios --apiKey ${{ secrets.IOS_APPSTORE_API_KEY_ID }} --apiIssuer ${{ secrets.IOS_APPSTORE_ISSUER_ID }}
- name: Upload release
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: ${{ runner.temp }}/export/${{ env.XCODE_PROJECT }}.ipa
asset_name: ${{ env.GAME_EXECUTABLE_NAME }}_${{ inputs.version }}_ios.ipa
release_name: ${{ inputs.version }}
tag: ${{ inputs.version }}
overwrite: true

271
.github/workflows/release.yaml vendored Normal file
View File

@@ -0,0 +1,271 @@
name: release-flow
on:
push:
tags:
- "v[0-9]+.[0-9]+.[0-9]+*"
workflow_dispatch:
inputs:
version:
description: 'Version - in the form of v1.2.3'
required: true
type: string
env:
# This variable is used to name release output files.
GAME_EXECUTABLE_NAME: yachtpit
GAME_OSX_APP_NAME: yachtpit
permissions:
contents: write
jobs:
get-version:
runs-on: ubuntu-latest
steps:
- name: Get tag
id: tag
run: echo "tag=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"
outputs:
version: ${{ inputs.version || steps.tag.outputs.tag }}
build-macOS:
runs-on: macos-latest
needs: get-version
env:
# macOS 11.0 Big Sur is the first version to support universal binaries
MACOSX_DEPLOYMENT_TARGET: 11.0
VERSION: ${{needs.get-version.outputs.version}}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Remove build script
run: |
rm build.rs
- name: Install rust toolchain for Apple Silicon
uses: dtolnay/rust-toolchain@master
with:
toolchain: stable
targets: aarch64-apple-darwin
- name: Build release for Apple Silicon
run: |
SDKROOT=$(xcrun -sdk macosx --show-sdk-path) cargo build --profile dist --target=aarch64-apple-darwin
- name: Install rust toolchain for Apple x86
uses: dtolnay/rust-toolchain@master
with:
toolchain: stable
targets: x86_64-apple-darwin
- name: Build release for x86 Apple
run: |
SDKROOT=$(xcrun -sdk macosx --show-sdk-path) cargo build --profile dist --target=x86_64-apple-darwin
- name: Create Universal Binary
run: |
lipo -create -output target/dist/${{ env.GAME_EXECUTABLE_NAME }} target/aarch64-apple-darwin/dist/${{ env.GAME_EXECUTABLE_NAME }} target/x86_64-apple-darwin/dist/${{ env.GAME_EXECUTABLE_NAME }}
- name: Create release
run: |
mkdir -p build/macos/src/Game.app/Contents/MacOS/assets
cp -r assets/ build/macos/src/Game.app/Contents/MacOS/assets
cp -r credits/ build/macos/src/Game.app/Contents/MacOS/credits
cp target/dist/${{ env.GAME_EXECUTABLE_NAME }} build/macos/src/Game.app/Contents/MacOS/
mv build/macos/src/Game.app build/macos/src/${{ env.GAME_OSX_APP_NAME }}.app
ln -s /Applications build/macos/src/
hdiutil create -fs HFS+ -volname "${{ env.GAME_OSX_APP_NAME }}" -srcfolder build/macos/src ${{ env.GAME_EXECUTABLE_NAME }}.dmg
- name: Upload release
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: ${{ env.GAME_EXECUTABLE_NAME }}.dmg
asset_name: ${{ env.GAME_EXECUTABLE_NAME }}_${{ env.VERSION }}_macOS.dmg
release_name: ${{ env.VERSION }}
tag: ${{ env.VERSION }}
overwrite: true
build-linux:
runs-on: ubuntu-latest
needs: get-version
env:
VERSION: ${{needs.get-version.outputs.version}}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install rust toolchain
uses: dtolnay/rust-toolchain@master
with:
toolchain: stable
- name: Install Dependencies
run: sudo apt-get update; sudo apt-get install pkg-config libx11-dev libasound2-dev libudev-dev
- name: Build release
run: |
cargo build --profile dist
- name: Prepare release
run: |
chmod +x target/dist/${{ env.GAME_EXECUTABLE_NAME }}
mv target/dist/${{ env.GAME_EXECUTABLE_NAME }} .
- name: Bundle release
run: |
tar -czf ${{ env.GAME_EXECUTABLE_NAME }}_linux.tar.gz ${{ env.GAME_EXECUTABLE_NAME }} assets credits
- name: Upload release
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: ${{ env.GAME_EXECUTABLE_NAME }}_linux.tar.gz
asset_name: ${{ env.GAME_EXECUTABLE_NAME }}_${{ env.VERSION }}_linux.tar.gz
release_name: ${{ env.VERSION }}
tag: ${{ env.VERSION }}
overwrite: true
build-windows:
runs-on: windows-latest
needs: get-version
env:
VERSION: ${{needs.get-version.outputs.version}}
BUILD_INSTALLER: ${{ false }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install rust toolchain
uses: dtolnay/rust-toolchain@master
with:
toolchain: stable
- name: Install dotnet
if: ${{ env.BUILD_INSTALLER }}
uses: actions/setup-dotnet@v3
with:
global-json-file: build/windows/installer/global.json
- name: Build release
run: |
cargo build --profile dist
- name: Prepare release
run: |
mkdir target/dist/assets && cp -r assets target/dist/assets
mkdir target/dist/credits && cp -r credits target/dist/credits
- name: Zip release
uses: vimtor/action-zip@v1.1
with:
files: target/dist/assets/ target/dist/credits/ target/dist/${{ env.GAME_EXECUTABLE_NAME }}.exe
dest: ${{ env.GAME_EXECUTABLE_NAME }}_windows.zip
- name: Create Installer
if: ${{ env.BUILD_INSTALLER }}
shell: bash
run: |
tag=${{ env.VERSION }}
version="${tag#v}"
dotnet build -p:Version=$version -c Release build/windows/installer/Installer.wixproj --output installer
- name: Upload release
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: ${{ env.GAME_EXECUTABLE_NAME }}_windows.zip
asset_name: ${{ env.GAME_EXECUTABLE_NAME }}_${{ env.VERSION }}_windows.zip
tag: ${{ env.VERSION }}
overwrite: true
- name: Upload installer
if: ${{ env.BUILD_INSTALLER }}
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: installer/en-US/installer.msi
asset_name: ${{ env.GAME_EXECUTABLE_NAME }}_${{ env.VERSION }}_windows.msi
release_name: ${{ env.VERSION }}
tag: ${{ env.VERSION }}
overwrite: true
build-web:
runs-on: ubuntu-latest
needs: get-version
env:
VERSION: ${{needs.get-version.outputs.version}}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install rust toolchain
uses: dtolnay/rust-toolchain@master
with:
toolchain: stable
- name: Install Dependencies
run: sudo apt-get update; sudo apt-get install pkg-config libx11-dev libasound2-dev libudev-dev
- name: Install trunk
uses: jetli/trunk-action@v0.4.0
with:
version: latest
- name: Add wasm target
run: |
rustup target add wasm32-unknown-unknown
- name: Build Release
run: |
trunk build --release
- name: Optimize Wasm
uses: NiklasEi/wasm-opt-action@v2
with:
file: dist/*.wasm
- name: Zip release
uses: vimtor/action-zip@v1.1
with:
files: dist/
dest: ${{ env.GAME_EXECUTABLE_NAME }}_web.zip
- name: Upload release
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: ${{ env.GAME_EXECUTABLE_NAME }}_web.zip
asset_name: ${{ env.GAME_EXECUTABLE_NAME }}_${{ env.VERSION }}_web.zip
release_name: ${{ env.VERSION }}
tag: ${{ env.VERSION }}
overwrite: true
build-for-iOS:
runs-on: macos-latest
timeout-minutes: 30
needs: get-version
env:
VERSION: ${{needs.get-version.outputs.version}}
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- name: Add iOS targets
run: rustup target add aarch64-apple-ios x86_64-apple-ios
- name: Build app for iOS
run: |
cd mobile
make xcodebuild-iphone-release
mkdir Payload
mv build/Build/Products/Release-iphoneos/*.app Payload
zip -r ${{ env.GAME_EXECUTABLE_NAME }}.zip Payload
mv ${{ env.GAME_EXECUTABLE_NAME }}.zip ${{ env.GAME_EXECUTABLE_NAME }}.ipa
- name: Upload release
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: mobile/${{ env.GAME_EXECUTABLE_NAME }}.ipa
asset_name: ${{ env.GAME_EXECUTABLE_NAME }}_${{ env.VERSION }}_unsigned_ios.ipa
release_name: ${{ env.VERSION }}
tag: ${{ env.VERSION }}
overwrite: true
build-for-Android:
runs-on: ubuntu-latest
timeout-minutes: 30
needs: get-version
env:
VERSION: ${{needs.get-version.outputs.version}}
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- name: Add Android targets
# could add more targets like armv7-linux-androideabi here (then also add to cargo-apk config)
run: rustup target add aarch64-linux-android
- name: Install Cargo APK
run: cargo install --force cargo-apk
- name: Build app for Android
# This uses a debug build, since release builds require keystore configuration
# For AAB builds that can be pushed to the Play store, see the release-android-google-play workflow.
run: ANDROID_NDK_ROOT=$ANDROID_NDK_LATEST_HOME cargo apk build --package mobile
- name: Upload release
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: target/debug/apk/${{ env.GAME_OSX_APP_NAME }}.apk
asset_name: ${{ env.GAME_EXECUTABLE_NAME }}_${{ env.VERSION }}_android.apk
release_name: ${{ env.VERSION }}
tag: ${{ env.VERSION }}
overwrite: true

5
.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
target/
.idea/
.DS_Store
dist/

275
BACKLOG.md Normal file
View File

@@ -0,0 +1,275 @@
# Backlog - yachtpit
## Goal
Complete the yachtpit instrument cluster with comprehensive icon system and enhance user experience for marine navigation and monitoring.
## Overview
**Duration:** 2 weeks
**Capacity:** 40 story points
**Focus:** Icons, UI/UX, Integration, Testing
---
## High Priority ~ 24 points
### Track: Core Navigation & Instrumentation
#### US-001: Navigation Display Icons (8 points)
**As a** yacht captain
**I want** to see intuitive navigation icons (compass, GPS, waypoints)
**So that** I can quickly understand my vessel's position and heading
**Acceptance Criteria:**
- [ ] Compass rose icon displays current heading
- [ ] GPS satellite icon shows connection status
- [ ] Waypoint icons mark navigation points
- [ ] North arrow provides directional reference
- [ ] Icons are visible in marine lighting conditions
**Technical Tasks:**
- Create compass rose SVG icon (2h)
- Implement GPS status indicator component (3h)
- Add waypoint marker system (4h)
- Update TextureAssets in loading.rs (1h)
- Add navigation icons to assets/textures/icons/ (1h)
#### US-002: Instrument Gauge Icons (8 points)
**As a** yacht operator
**I want** clear visual indicators for speed, depth, and engine status
**So that** I can monitor critical vessel parameters at a glance
**Acceptance Criteria:**
- [ ] Speed gauge displays with knots unit indicator
- [ ] Depth sounder shows water depth with meter units
- [ ] Engine temperature gauge with warning states
- [ ] Fuel level indicator with consumption tracking
- [ ] Battery level with charging status
**Technical Tasks:**
- Design speedometer and depth gauge icons (3h)
- Create engine status indicator components (4h)
- Implement fuel and battery level displays (3h)
- Add gauge needle animations (2h)
- Update YachtData struct with new parameters (1h)
#### US-003: System Status Indicators (8 points)
**As a** yacht crew member
**I want** color-coded status indicators for all systems
**So that** I can immediately identify operational, warning, and fault conditions
**Acceptance Criteria:**
- [ ] Green dots for operational systems
- [ ] Yellow dots for warning states
- [ ] Red dots for fault/offline systems
- [ ] Blue dots for standby mode
- [ ] Status changes reflect real system states
**Technical Tasks:**
- Create status dot icon set (2h)
- Implement SystemStatus component (3h)
- Add status update logic to system displays (4h)
- Create alert notification system (2h)
- Add system health monitoring (2h)
---
## Medium Priority ~ 12 points
### Track: Advanced Marine Systems
#### US-004: Radar & AIS Integration
**As a** yacht navigator
**I want** radar and AIS system displays with appropriate icons
**So that** I can track other vessels and obstacles around my yacht
**Acceptance Criteria:**
- [ ] Radar dish icon shows system status
- [ ] AIS ship icons display other vessels
- [ ] Target blip indicators for radar contacts
- [ ] Radio wave icons for communication status
- [ ] Integration with existing system selection
**Technical Tasks:**
- Design radar and AIS icon set (2h)
- Implement radar sweep animation (3h)
- Add AIS target tracking display (4h)
- Update system interaction handlers (1h)
#### US-005: Weather & Environmental Icons
**As a** yacht captain
**I want** weather condition indicators with wind and atmospheric data
**So that** I can make informed navigation decisions based on conditions
**Acceptance Criteria:**
- [ ] Wind vane shows direction and speed
- [ ] Barometer icon displays atmospheric pressure
- [ ] Temperature and humidity indicators
- [ ] Beaufort scale wind force display
- [ ] Weather data updates in real-time simulation
**Technical Tasks:**
- Create weather icon collection (2h)
- Implement wind direction component (2h)
- Add atmospheric data display (3h)
- Integrate weather simulation system (2h)
---
## Low Priority ~ 4 points
### Track: Safety & Emergency Systems
#### US-006: Safety Equipment Icons
**As a** yacht safety officer
**I want** visual indicators for safety equipment status
**So that** I can ensure all emergency equipment is ready and accessible
**Acceptance Criteria:**
- [ ] Life ring icon for safety equipment
- [ ] Fire extinguisher status indicator
- [ ] First aid kit availability marker
- [ ] Emergency radio communication status
**Technical Tasks:**
- Design safety equipment icon set (1h)
- Add safety system status tracking (2h)
- Implement emergency equipment checklist (1h)
#### US-007: Chart & Navigation Tools
**As a** yacht navigator
**I want** chart symbols and measurement tool icons
**So that** I can perform navigation calculations and chart plotting
**Acceptance Criteria:**
- [ ] Nautical chart icon for chart display mode
- [ ] Ruler icon for distance measurements
- [ ] Protractor icon for bearing calculations
- [ ] Harbor and anchorage markers
**Technical Tasks:**
- Create navigation tool icon set (1h)
- Add measurement tool functionality (2h)
- Implement chart symbol display (1h)
---
## Technical Debt & Infrastructure Tasks
### Track: All
#### TD-001: Asset Management System Enhancement
**Priority:** High
**Description:** Expand TextureAssets to support comprehensive icon loading system
**Tasks:**
- [ ] Create icons subdirectory structure in assets/textures/
- [ ] Update loading.rs with icon asset collections
- [ ] Implement icon resource management system
- [ ] Add icon preloading optimization
- [ ] Create asset validation system
#### TD-002: Component Architecture Refactoring
**Priority:** Medium
**Description:** Optimize component structure for better performance and maintainability
**Tasks:**
- [ ] Refactor instrument cluster components
- [ ] Implement component pooling for status indicators
- [ ] Add component lifecycle management
- [ ] Optimize query systems for better performance
#### TD-003: Testing Infrastructure
**Priority:** Medium
**Description:** Expand test coverage for new components and systems
**Tasks:**
- [ ] Add unit tests for new icon components
- [ ] Create integration tests for system interactions
- [ ] Implement visual regression tests for UI components
- [ ] Add performance benchmarks for rendering systems
---
## Definition of Done
### User Stories
- [ ] All acceptance criteria met
- [ ] Code reviewed and approved
- [ ] Unit tests written and passing
- [ ] Integration tests passing
- [ ] Documentation updated
- [ ] Icons meet design guidelines (high contrast, nautical theme)
- [ ] Performance impact assessed and acceptable
- [ ] Accessibility requirements met
### Technical Tasks
- [ ] Code follows project conventions
- [ ] No new compiler warnings
- [ ] Memory usage within acceptable limits
- [ ] Cross-platform compatibility verified
- [ ] Asset optimization completed
---
## Risk Assessment
### High Risk Items
1. **Icon Design Consistency** - Risk of inconsistent visual style across 100+ icons
- *Mitigation:* Create comprehensive style guide and icon templates
2. **Performance Impact** - Large number of icons may affect rendering performance
- *Mitigation:* Implement icon atlasing and lazy loading
3. **Asset Loading Time** - Extensive icon set may increase initial load time
- *Mitigation:* Progressive loading and asset compression
### Medium Risk Items
1. **Cross-platform Icon Rendering** - Icons may render differently across platforms
- *Mitigation:* Test on all target platforms early
2. **Memory Usage** - Icon textures may consume significant memory
- *Mitigation:* Optimize icon sizes and use appropriate formats
---
## Next Retrospective
### Key Metrics to Track
- Story points completed vs. planned
- Icon implementation velocity
- Performance impact measurements
- User feedback on icon clarity and usability
- Code quality metrics (test coverage, complexity)
### Success Criteria
- High-priority user stories progress
- Icon system foundation established
- Performance remains within acceptable limits
- User experience significantly improved
- Technical debt reduced
---
## Notes for Development
## Personal Development Notes
### Current State Analysis
- **Strengths:** Solid Bevy/Rust foundation, comprehensive component structure in player.rs
- **Gaps:** Missing icon assets, limited TextureAssets configuration, no icon management system
- **Opportunities:** Leverage existing YachtData structure, build on established plugin architecture
### Personal Development Approach
1. Start with core navigation icons (US-001) as foundation
2. Establish icon loading and management patterns early
3. Implement status indicator system for immediate visual feedback
4. Build comprehensive testing as icons are added
5. Focus on performance optimization throughout development
### Resources & Tools Needed
- Icon design resources (icon library or design tools)
- Asset optimization tools for performance
- Performance profiling setup for benchmarking
- ~~Testing setup for cross-platform compatibility~~ This is done in CI.

5519
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

87
Cargo.toml Normal file
View File

@@ -0,0 +1,87 @@
[package]
name = "yachtpit"
version = "0.1.0"
publish = false
authors = ["seemueller-io <git@github.geoffsee>"]
edition = "2021"
exclude = ["dist", "build", "assets", "credits"]
[workspace]
members = ["mobile"]
[profile.dev.package."*"]
opt-level = 3
[profile.dev]
opt-level = 1
# This is used by trunk as it doesn't support custom profiles: https://github.com/trunk-rs/trunk/issues/605
# xbuild also uses this profile for building android AABs because I couldn't find a configuration for it
[profile.release]
opt-level = "z"
lto = 'thin'
codegen-units = 1
strip = true
# Profile for distribution
[profile.dist]
inherits = "release"
opt-level = 3
lto = true
codegen-units = 1
strip = true
[features]
dev = [
"bevy/dynamic_linking",
]
# All of Bevy's default features exept for the audio related ones (bevy_audio, vorbis), since they clash with bevy_kira_audio
# and android_shared_stdcxx/android-game-activity, since those are covered in `mobile`
[dependencies]
bevy = { version = "0.16", default-features = false, features = [
"animation",
"bevy_asset",
"bevy_color",
"bevy_core_pipeline",
"bevy_gilrs",
"bevy_gizmos",
"bevy_gltf",
"bevy_log",
"bevy_mesh_picking_backend",
"bevy_pbr",
"bevy_picking",
"bevy_render",
"bevy_scene",
"bevy_sprite",
"bevy_sprite_picking_backend",
"bevy_state",
"bevy_text",
"bevy_ui",
"bevy_ui_picking_backend",
"bevy_window",
"bevy_winit",
"custom_cursor",
"default_font",
"hdr",
"multi_threaded",
"png",
"smaa_luts",
"sysinfo_plugin",
"tonemapping_luts",
"webgl2",
"x11",
] }
bevy_kira_audio = { version = "0.23.0", features = ["android_shared_stdcxx"] }
bevy_asset_loader = { version = "0.23.0" }
rand = { version = "0.8.3" }
webbrowser = { version = "1", features = ["hardened"] }
# keep the following in sync with Bevy's dependencies
winit = { version = "0.30", default-features = false }
image = { version = "0.25", default-features = false }
## This greatly improves WGPU's performance due to its heavy use of trace! calls
log = { version = "0.4", features = ["max_level_debug", "release_max_level_warn"] }
[build-dependencies]
embed-resource = "1"

161
ICONS_NEEDED.md Normal file
View File

@@ -0,0 +1,161 @@
# Icons Required for Yacht yachtpit Application
This document lists all the icons that need to be generated for the yacht yachtpit application based on the current UI implementation.
## Navigation & Compass Icons
### Primary Navigation
- **Compass Rose Icon** - For the central navigation display
- **North Arrow Icon** - Directional indicator
- **GPS Satellite Icon** - GPS status indicator
- **Waypoint Icon** - Navigation waypoints
- **Route Line Icon** - Planned route visualization
## Instrument Gauge Icons
### Speed Gauge
- **Speedometer Icon** - Circular gauge background
- **Speed Needle Icon** - Gauge pointer/needle
- **Knots Unit Icon** - "KTS" stylized icon
### Depth Gauge
- **Depth Sounder Icon** - Sonar/depth measurement icon
- **Water Depth Icon** - Underwater depth visualization
- **Meter Unit Icon** - "M" stylized icon
## Engine & Systems Icons
### Engine Status
- **Engine Icon** - Marine engine representation
- **Temperature Gauge Icon** - Engine temperature indicator
- **Cooling System Icon** - Engine cooling status
- **Engine Alert Icon** - Warning/alert indicator
### Fuel System
- **Fuel Tank Icon** - Fuel level indicator
- **Fuel Pump Icon** - Fuel system status
- **Fuel Drop Icon** - Fuel consumption indicator
### Electrical System
- **Battery Icon** - Battery level indicator
- **Charging Icon** - Battery charging status
- **Power Icon** - Electrical system status
- **Voltage Meter Icon** - Electrical measurement
## Communication & Navigation Systems
### GPS System
- **GPS Icon** - Global positioning system
- **Satellite Signal Icon** - Signal strength indicator
- **Location Pin Icon** - Current position marker
### Radar System
- **Radar Dish Icon** - Radar antenna representation
- **Radar Sweep Icon** - Radar scanning animation
- **Target Blip Icon** - Radar contact indicator
### AIS (Automatic Identification System)
- **AIS Icon** - Ship identification system
- **Ship Icon** - Other vessel representation
- **Radio Wave Icon** - Communication signal
## Weather & Environmental Icons
### Wind Information
- **Wind Vane Icon** - Wind direction indicator
- **Wind Speed Icon** - Anemometer representation
- **Wind Arrow Icon** - Directional wind indicator
- **Beaufort Scale Icon** - Wind force scale
### Weather Conditions
- **Barometer Icon** - Atmospheric pressure
- **Temperature Icon** - Air temperature
- **Humidity Icon** - Relative humidity indicator
## Status & Alert Icons
### System Status Indicators
- **Green Status Dot** - System operational
- **Red Status Dot** - System fault/offline
- **Yellow Status Dot** - System warning
- **Blue Status Dot** - System standby
### Alert Icons
- **Warning Triangle** - General warning
- **Critical Alert** - Emergency situation
- **Information Icon** - General information
- **Maintenance Icon** - Service required
## UI Control Icons
### Navigation Controls
- **Menu Icon** - Main menu access
- **Settings Icon** - Configuration access
- **Home Icon** - Return to main display
- **Back Arrow** - Navigation back
### Display Controls
- **Brightness Icon** - Screen brightness control
- **Contrast Icon** - Display contrast
- **Night Mode Icon** - Low-light display mode
- **Full Screen Icon** - Display mode toggle
## Chart & Mapping Icons
### Chart Elements
- **Chart Icon** - Nautical chart representation
- **Depth Contour Icon** - Underwater topography
- **Buoy Icon** - Navigation aids
- **Harbor Icon** - Port/marina indicator
- **Anchor Icon** - Anchorage areas
### Measurement Tools
- **Ruler Icon** - Distance measurement
- **Protractor Icon** - Bearing measurement
- **Scale Icon** - Chart scale indicator
## Safety & Emergency Icons
### Safety Equipment
- **Life Ring Icon** - Safety equipment
- **Fire Extinguisher Icon** - Emergency equipment
- **First Aid Icon** - Medical supplies
- **Emergency Radio Icon** - Distress communication
### Emergency Procedures
- **SOS Icon** - Distress signal
- **Mayday Icon** - Emergency call
- **Coast Guard Icon** - Emergency services
- **Evacuation Icon** - Emergency procedures
## File Formats Required
All icons should be generated in the following formats:
- **PNG**: 16x16, 24x24, 32x32, 48x48, 64x64, 128x128, 256x256 pixels
- **SVG**: Scalable vector format for high-DPI displays
- **ICO**: Windows icon format (for desktop application)
## Design Guidelines
### Style Requirements
- **Nautical Theme**: Maritime-inspired design language
- **High Contrast**: Suitable for marine lighting conditions
- **Monochromatic**: Primary colors should be cyan/blue theme
- **Clean Lines**: Minimalist, professional appearance
- **Scalable**: Must remain legible at small sizes
### Color Palette
- **Primary**: Cyan (#00CCFF) - Main UI elements
- **Secondary**: Green (#00FF80) - Operational status
- **Warning**: Orange (#FF8000) - Caution states
- **Alert**: Red (#FF0040) - Critical alerts
- **Neutral**: Gray (#999999) - Inactive elements
## Implementation Notes
These icons will replace the current text-based placeholders in:
- `src/player.rs` - Main instrument cluster
- `src/menu.rs` - Menu system icons
- `src/loading.rs` - Loading screen elements
The icons should be placed in the `assets/textures/icons/` directory and loaded through the existing `TextureAssets` resource system.

121
LICENSE Normal file
View File

@@ -0,0 +1,121 @@
Creative Commons Legal Code
CC0 1.0 Universal
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
HEREUNDER.
Statement of Purpose
The laws of most jurisdictions throughout the world automatically confer
exclusive Copyright and Related Rights (defined below) upon the creator
and subsequent owner(s) (each and all, an "owner") of an original work of
authorship and/or a database (each, a "Work").
Certain owners wish to permanently relinquish those rights to a Work for
the purpose of contributing to a commons of creative, cultural and
scientific works ("Commons") that the public can reliably and without fear
of later claims of infringement build upon, modify, incorporate in other
works, reuse and redistribute as freely as possible in any form whatsoever
and for any purposes, including without limitation commercial purposes.
These owners may contribute to the Commons to promote the ideal of a free
culture and the further production of creative, cultural and scientific
works, or to gain reputation or greater distribution for their Work in
part through the use and efforts of others.
For these and/or other purposes and motivations, and without any
expectation of additional consideration or compensation, the person
associating CC0 with a Work (the "Affirmer"), to the extent that he or she
is an owner of Copyright and Related Rights in the Work, voluntarily
elects to apply CC0 to the Work and publicly distribute the Work under its
terms, with knowledge of his or her Copyright and Related Rights in the
Work and the meaning and intended legal effect of CC0 on those rights.
1. Copyright and Related Rights. A Work made available under CC0 may be
protected by copyright and related or neighboring rights ("Copyright and
Related Rights"). Copyright and Related Rights include, but are not
limited to, the following:
i. the right to reproduce, adapt, distribute, perform, display,
communicate, and translate a Work;
ii. moral rights retained by the original author(s) and/or performer(s);
iii. publicity and privacy rights pertaining to a person's image or
likeness depicted in a Work;
iv. rights protecting against unfair competition in regards to a Work,
subject to the limitations in paragraph 4(a), below;
v. rights protecting the extraction, dissemination, use and reuse of data
in a Work;
vi. database rights (such as those arising under Directive 96/9/EC of the
European Parliament and of the Council of 11 March 1996 on the legal
protection of databases, and under any national implementation
thereof, including any amended or successor version of such
directive); and
vii. other similar, equivalent or corresponding rights throughout the
world based on applicable law or treaty, and any national
implementations thereof.
2. Waiver. To the greatest extent permitted by, but not in contravention
of, applicable law, Affirmer hereby overtly, fully, permanently,
irrevocably and unconditionally waives, abandons, and surrenders all of
Affirmer's Copyright and Related Rights and associated claims and causes
of action, whether now known or unknown (including existing as well as
future claims and causes of action), in the Work (i) in all territories
worldwide, (ii) for the maximum duration provided by applicable law or
treaty (including future time extensions), (iii) in any current or future
medium and for any number of copies, and (iv) for any purpose whatsoever,
including without limitation commercial, advertising or promotional
purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
member of the public at large and to the detriment of Affirmer's heirs and
successors, fully intending that such Waiver shall not be subject to
revocation, rescission, cancellation, termination, or any other legal or
equitable action to disrupt the quiet enjoyment of the Work by the public
as contemplated by Affirmer's express Statement of Purpose.
3. Public License Fallback. Should any part of the Waiver for any reason
be judged legally invalid or ineffective under applicable law, then the
Waiver shall be preserved to the maximum extent permitted taking into
account Affirmer's express Statement of Purpose. In addition, to the
extent the Waiver is so judged Affirmer hereby grants to each affected
person a royalty-free, non transferable, non sublicensable, non exclusive,
irrevocable and unconditional license to exercise Affirmer's Copyright and
Related Rights in the Work (i) in all territories worldwide, (ii) for the
maximum duration provided by applicable law or treaty (including future
time extensions), (iii) in any current or future medium and for any number
of copies, and (iv) for any purpose whatsoever, including without
limitation commercial, advertising or promotional purposes (the
"License"). The License shall be deemed effective as of the date CC0 was
applied by Affirmer to the Work. Should any part of the License for any
reason be judged legally invalid or ineffective under applicable law, such
partial invalidity or ineffectiveness shall not invalidate the remainder
of the License, and in such case Affirmer hereby affirms that he or she
will not (i) exercise any of his or her remaining Copyright and Related
Rights in the Work or (ii) assert any associated claims and causes of
action with respect to the Work, in either case contrary to Affirmer's
express Statement of Purpose.
4. Limitations and Disclaimers.
a. No trademark or patent rights held by Affirmer are waived, abandoned,
surrendered, licensed or otherwise affected by this document.
b. Affirmer offers the Work as-is and makes no representations or
warranties of any kind concerning the Work, express, implied,
statutory or otherwise, including without limitation warranties of
title, merchantability, fitness for a particular purpose, non
infringement, or the absence of latent or other defects, accuracy, or
the present or absence of errors, whether or not discoverable, all to
the greatest extent permissible under applicable law.
c. Affirmer disclaims responsibility for clearing rights of other persons
that may apply to the Work or any use thereof, including without
limitation any person's Copyright and Related Rights in the Work.
Further, Affirmer disclaims responsibility for obtaining any necessary
consents, permissions or other rights required for any use of the
Work.
d. Affirmer understands and acknowledges that Creative Commons is not a
party to this document and has no duty or obligation with respect to
this CC0 or use of the Work.

5
README.md Normal file
View File

@@ -0,0 +1,5 @@
# yachtpit
> Warning: Experimental and incomplete. Project is unfunded.
**yachtpit** is an open-source cross-platform cockpit for Boats, targeting Windows, macOS, Linux, WebAssembly, Android, and iOS.

5
Trunk.toml Normal file
View File

@@ -0,0 +1,5 @@
[build]
public_url = "./"
[serve]
port = 8080

BIN
assets/audio/flying.ogg Normal file

Binary file not shown.

BIN
assets/textures/bevy.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

BIN
assets/textures/github.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

10
build.rs Normal file
View File

@@ -0,0 +1,10 @@
extern crate embed_resource;
use std::env;
fn main() {
let target = env::var("TARGET").unwrap();
if target.contains("windows") {
// on windows we will set our game icon as icon for the executable
embed_resource::compile("build/windows/icon.rc");
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

BIN
build/icon_1024x1024.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

17
build/macos/create_icns.sh Executable file
View File

@@ -0,0 +1,17 @@
#!/usr/bin/env sh
rm -rf AppIcon.iconset/*
mkdir -p AppIcon.iconset
sips -z 16 16 ../icon_1024x1024.png --out AppIcon.iconset/icon_16x16.png
sips -z 32 32 ../icon_1024x1024.png --out AppIcon.iconset/icon_16x16@2x.png
sips -z 32 32 ../icon_1024x1024.png --out AppIcon.iconset/icon_32x32.png
sips -z 64 64 ../icon_1024x1024.png --out AppIcon.iconset/icon_32x32@2x.png
sips -z 128 128 ../icon_1024x1024.png --out AppIcon.iconset/icon_128x128.png
sips -z 256 256 ../icon_1024x1024.png --out AppIcon.iconset/icon_128x128@2x.png
sips -z 256 256 ../icon_1024x1024.png --out AppIcon.iconset/icon_256x256.png
sips -z 512 512 ../icon_1024x1024.png --out AppIcon.iconset/icon_256x256@2x.png
sips -z 512 512 ../icon_1024x1024.png --out AppIcon.iconset/icon_512x512.png
cp ../icon_1024x1024.png AppIcon.iconset/icon_512x512@2x.png
iconutil -c icns AppIcon.iconset
mkdir -p src/Game.app/Contents/Resources
mv AppIcon.icns src/Game.app/Contents/Resources/

View File

@@ -0,0 +1,17 @@
#!/usr/bin/env sh
rm -rf AppIcon.iconset/*
mkdir -p AppIcon.iconset
convert ../icon_1024x1024.png -resize 16x16 AppIcon.iconset/icon_16x16.png
convert ../icon_1024x1024.png -resize 32x32 AppIcon.iconset/icon_16x16@2x.png
convert ../icon_1024x1024.png -resize 32x32 AppIcon.iconset/icon_32x32.png
convert ../icon_1024x1024.png -resize 64x64 AppIcon.iconset/icon_32x32@2x.png
convert ../icon_1024x1024.png -resize 128x128 AppIcon.iconset/icon_128x128.png
convert ../icon_1024x1024.png -resize 256x256 AppIcon.iconset/icon_128x128@2x.png
convert ../icon_1024x1024.png -resize 256x256 AppIcon.iconset/icon_256x256.png
convert ../icon_1024x1024.png -resize 512x512 AppIcon.iconset/icon_256x256@2x.png
convert ../icon_1024x1024.png -resize 512x512 AppIcon.iconset/icon_512x512.png
cp ../icon_1024x1024.png AppIcon.iconset/icon_512x512@2x.png
png2icns ./AppIcon.icns AppIcon.iconset/icon_16x16.png AppIcon.iconset/icon_32x32.png AppIcon.iconset/icon_128x128.png AppIcon.iconset/icon_256x256.png AppIcon.iconset/icon_512x512.png
mkdir -p src/Game.app/Contents/Resources
mv AppIcon.icns src/Game.app/Contents/Resources/

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleDisplayName</key>
<string>yachtpit</string>
<key>CFBundleExecutable</key>
<string>yachtpit</string>
<key>CFBundleIconFile</key>
<string>AppIcon.icns</string>
<key>CFBundleIdentifier</key>
<string>io.gs.yachtpit</string> <!-- ToDo replace all instances with io.gs.yachtpit -->
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>yachtpit</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<!-- Version -->
<string>0.1.0</string>
<key>CFBundleSupportedPlatforms</key>
<array>
<string>MacOSX</string>
</array>
</dict>
</plist>

62
build/web/sound.js Normal file
View File

@@ -0,0 +1,62 @@
// Insert hack to make sound autoplay on Chrome as soon as the user interacts with the tab:
// https://developers.google.com/web/updates/2018/11/web-audio-autoplay#moving-forward
// the following function keeps track of all AudioContexts and resumes them on the first user
// interaction with the page. If the function is called and all contexts are already running,
// it will remove itself from all event listeners.
(function () {
// An array of all contexts to resume on the page
const audioContextList = [];
// An array of various user interaction events we should listen for
const userInputEventNames = [
"click",
"contextmenu",
"auxclick",
"dblclick",
"mousedown",
"mouseup",
"pointerup",
"touchend",
"keydown",
"keyup",
];
// A proxy object to intercept AudioContexts and
// add them to the array for tracking and resuming later
self.AudioContext = new Proxy(self.AudioContext, {
construct(target, args) {
const result = new target(...args);
audioContextList.push(result);
return result;
},
});
// To resume all AudioContexts being tracked
function resumeAllContexts(_event) {
let count = 0;
audioContextList.forEach((context) => {
if (context.state !== "running") {
context.resume();
} else {
count++;
}
});
// If all the AudioContexts have now resumed then we unbind all
// the event listeners from the page to prevent unnecessary resume attempts
// Checking count > 0 ensures that the user interaction happens AFTER the game started up
if (count > 0 && count === audioContextList.length) {
userInputEventNames.forEach((eventName) => {
document.removeEventListener(eventName, resumeAllContexts);
});
}
}
// We bind the resume function for each user interaction
// event on the page
userInputEventNames.forEach((eventName) => {
document.addEventListener(eventName, resumeAllContexts);
});
})();

53
build/web/styles.css Normal file
View File

@@ -0,0 +1,53 @@
body, html {
height: 100%;
}
body {
background-color: transparent;
margin: 0;
display: flex;
justify-content: center;
align-items: center;
}
.game-container {
width: 800px;
height: 600px;
display: flex;
justify-content: center;
align-items: center;
}
.lds-dual-ring {
display: inline-block;
position: absolute;
left: 0;
right: 0;
margin: auto;
width: 80px;
height: 80px;
}
.lds-dual-ring:after {
content: " ";
display: block;
width: 64px;
height: 64px;
border-radius: 50%;
border: 6px solid #fff;
border-color: #fff transparent #fff transparent;
animation: lds-dual-ring 1.2s linear infinite;
}
@keyframes lds-dual-ring {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
#bevy {
z-index: 2;
}

BIN
build/windows/icon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

1
build/windows/icon.rc Normal file
View File

@@ -0,0 +1 @@
app_icon ICON "icon.ico"

2
build/windows/installer/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
/bin/
/obj/

View File

@@ -0,0 +1,73 @@
<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs"
xmlns:ui="http://wixtoolset.org/schemas/v4/wxs/ui">
<!-- Copied from https://github.com/wixtoolset/wix4/blob/6f2511f58f60e63a15357a2fe37f83343dea3090/src/ext/UI/wixlib/WixUI_InstallDir.wxs#L9
but with the license dialog disabled.
See: https://wixtoolset.org/docs/v3/wixui/wixui_customizations/#changing-the-ui-sequence-of-a-built-in-dialog-set. -->
<Fragment>
<UI Id="WixUI_CustomInstallDir">
<TextStyle Id="WixUI_Font_Normal" FaceName="Tahoma" Size="8" />
<TextStyle Id="WixUI_Font_Bigger" FaceName="Tahoma" Size="12" />
<TextStyle Id="WixUI_Font_Title" FaceName="Tahoma" Size="9" Bold="yes" />
<Property Id="DefaultUIFont" Value="WixUI_Font_Normal" />
<DialogRef Id="BrowseDlg" />
<DialogRef Id="DiskCostDlg" />
<DialogRef Id="ErrorDlg" />
<DialogRef Id="FatalError" />
<DialogRef Id="FilesInUse" />
<DialogRef Id="MsiRMFilesInUse" />
<DialogRef Id="PrepareDlg" />
<DialogRef Id="ProgressDlg" />
<DialogRef Id="ResumeDlg" />
<DialogRef Id="UserExit" />
<Publish Dialog="BrowseDlg" Control="OK" Event="SpawnDialog" Value="InvalidDirDlg" Order="4" Condition="NOT WIXUI_DONTVALIDATEPATH AND WIXUI_INSTALLDIR_VALID&lt;&gt;&quot;1&quot;" />
<Publish Dialog="ExitDialog" Control="Finish" Event="EndDialog" Value="Return" Order="999" />
<Publish Dialog="WelcomeDlg" Control="Next" Event="NewDialog" Value="InstallDirDlg" Condition="NOT Installed" />
<Publish Dialog="WelcomeDlg" Control="Next" Event="NewDialog" Value="VerifyReadyDlg" Condition="Installed AND PATCH" />
<Publish Dialog="InstallDirDlg" Control="Back" Event="NewDialog" Value="WelcomeDlg" />
<Publish Dialog="InstallDirDlg" Control="Next" Event="SetTargetPath" Value="[WIXUI_INSTALLDIR]" Order="1" />
<Publish Dialog="InstallDirDlg" Control="Next" Event="SpawnDialog" Value="InvalidDirDlg" Order="3" Condition="NOT WIXUI_DONTVALIDATEPATH AND WIXUI_INSTALLDIR_VALID&lt;&gt;&quot;1&quot;" />
<Publish Dialog="InstallDirDlg" Control="Next" Event="NewDialog" Value="VerifyReadyDlg" Order="4" Condition="WIXUI_DONTVALIDATEPATH OR WIXUI_INSTALLDIR_VALID=&quot;1&quot;" />
<Publish Dialog="InstallDirDlg" Control="ChangeFolder" Property="_BrowseProperty" Value="[WIXUI_INSTALLDIR]" Order="1" />
<Publish Dialog="InstallDirDlg" Control="ChangeFolder" Event="SpawnDialog" Value="BrowseDlg" Order="2" />
<Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="InstallDirDlg" Order="1" Condition="NOT Installed" />
<Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="MaintenanceTypeDlg" Order="2" Condition="Installed AND NOT PATCH" />
<Publish Dialog="VerifyReadyDlg" Control="Back" Event="NewDialog" Value="WelcomeDlg" Order="2" Condition="Installed AND PATCH" />
<Publish Dialog="MaintenanceWelcomeDlg" Control="Next" Event="NewDialog" Value="MaintenanceTypeDlg" />
<Publish Dialog="MaintenanceTypeDlg" Control="RepairButton" Event="NewDialog" Value="VerifyReadyDlg" />
<Publish Dialog="MaintenanceTypeDlg" Control="RemoveButton" Event="NewDialog" Value="VerifyReadyDlg" />
<Publish Dialog="MaintenanceTypeDlg" Control="Back" Event="NewDialog" Value="MaintenanceWelcomeDlg" />
<Property Id="ARPNOMODIFY" Value="1" />
<InstallUISequence>
<Show Dialog="FatalError" OnExit="error" />
<Show Dialog="UserExit" OnExit="cancel" />
<Show Dialog="ExitDialog" OnExit="success" />
</InstallUISequence>
<AdminUISequence>
<Show Dialog="FatalError" OnExit="error" />
<Show Dialog="UserExit" OnExit="cancel" />
<Show Dialog="ExitDialog" OnExit="success" />
</AdminUISequence>
</UI>
<UIRef Id="WixUI_Common" />
</Fragment>
<?foreach WIXUIARCH in X86;X64;A64 ?>
<Fragment>
<UI Id="WixUI_CustomInstallDir_$(WIXUIARCH)">
<Publish Dialog="BrowseDlg" Control="OK" Event="DoAction" Value="WixUIValidatePath_$(WIXUIARCH)" Order="3" Condition="NOT WIXUI_DONTVALIDATEPATH" />
<Publish Dialog="InstallDirDlg" Control="Next" Event="DoAction" Value="WixUIValidatePath_$(WIXUIARCH)" Order="2" Condition="NOT WIXUI_DONTVALIDATEPATH" />
</UI>
</Fragment>
<?endforeach?>
</Wix>

View File

@@ -0,0 +1,40 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{B7DD6F7E-DEF8-4E67-B5B7-07EF123DB6F0}") = "Installer", "Installer.wixproj", "{340293B0-F46C-46A0-88D8-4BB2F3465C53}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Debug|ARM64 = Debug|ARM64
Debug|x64 = Debug|x64
Debug|x86 = Debug|x86
Release|Any CPU = Release|Any CPU
Release|ARM64 = Release|ARM64
Release|x64 = Release|x64
Release|x86 = Release|x86
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{340293B0-F46C-46A0-88D8-4BB2F3465C53}.Debug|Any CPU.ActiveCfg = Debug|x64
{340293B0-F46C-46A0-88D8-4BB2F3465C53}.Debug|Any CPU.Build.0 = Debug|x64
{340293B0-F46C-46A0-88D8-4BB2F3465C53}.Debug|ARM64.ActiveCfg = Debug|ARM64
{340293B0-F46C-46A0-88D8-4BB2F3465C53}.Debug|ARM64.Build.0 = Debug|ARM64
{340293B0-F46C-46A0-88D8-4BB2F3465C53}.Debug|x64.ActiveCfg = Debug|x64
{340293B0-F46C-46A0-88D8-4BB2F3465C53}.Debug|x64.Build.0 = Debug|x64
{340293B0-F46C-46A0-88D8-4BB2F3465C53}.Debug|x86.ActiveCfg = Debug|x86
{340293B0-F46C-46A0-88D8-4BB2F3465C53}.Debug|x86.Build.0 = Debug|x86
{340293B0-F46C-46A0-88D8-4BB2F3465C53}.Release|Any CPU.ActiveCfg = Release|x64
{340293B0-F46C-46A0-88D8-4BB2F3465C53}.Release|Any CPU.Build.0 = Release|x64
{340293B0-F46C-46A0-88D8-4BB2F3465C53}.Release|ARM64.ActiveCfg = Release|ARM64
{340293B0-F46C-46A0-88D8-4BB2F3465C53}.Release|ARM64.Build.0 = Release|ARM64
{340293B0-F46C-46A0-88D8-4BB2F3465C53}.Release|x64.ActiveCfg = Release|x64
{340293B0-F46C-46A0-88D8-4BB2F3465C53}.Release|x64.Build.0 = Release|x64
{340293B0-F46C-46A0-88D8-4BB2F3465C53}.Release|x86.ActiveCfg = Release|x86
{340293B0-F46C-46A0-88D8-4BB2F3465C53}.Release|x86.Build.0 = Release|x86
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
EndGlobal

View File

@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003" Sdk="WixToolset.Sdk/4.0.0">
<PropertyGroup>
<DebugType>none</DebugType>
<OutputName>installer</OutputName>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="WixToolset.UI.wixext" Version="4.0.0" />
<PackageReference Include="WixToolset.Heat" Version="4.0.0" />
</ItemGroup>
<ItemGroup>
<HarvestDirectory Include="..\..\..\assets">
<ComponentGroupName>AssetsDirectory</ComponentGroupName>
<DirectoryRefId>INSTALLFOLDER</DirectoryRefId>
<SuppressRootDirectory>false</SuppressRootDirectory>
</HarvestDirectory>
<BindPath Include="..\..\..\assets" />
</ItemGroup>
<ItemGroup>
<HarvestDirectory Include="..\..\..\credits">
<ComponentGroupName>CreditsDirectory</ComponentGroupName>
<DirectoryRefId>INSTALLFOLDER</DirectoryRefId>
<SuppressRootDirectory>false</SuppressRootDirectory>
</HarvestDirectory>
<BindPath Include="..\..\..\credits" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,6 @@
<!--
This file contains the declaration of all the localizable strings.
-->
<WixLocalization xmlns="http://wixtoolset.org/schemas/v4/wxl" Culture="en-US">
<String Id="DowngradeError" Value="A newer version of [ProductName] is already installed." />
</WixLocalization>

View File

@@ -0,0 +1,50 @@
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs" xmlns:ui="http://wixtoolset.org/schemas/v4/wxs/ui">
<Package
Name="yachtpit"
Manufacturer="gsio"
UpgradeCode="ac8709c2-1d6f-4440-b424-386e4e315425"
Version="0.0.1"
Scope="perUserOrMachine">
<MajorUpgrade DowngradeErrorMessage="!(loc.DowngradeError)" />
<Media Id="1" Cabinet="myapplication.cab" EmbedCab="yes" />
<Property Id="ApplicationFolderName" Value="!(bind.Property.ProductName)" />
<!-- Installer Icon -->
<Icon Id="icon.ico" SourceFile="..\icon.ico"/>
<Property Id="ARPPRODUCTICON" Value="icon.ico" />
<!-- Sets the default installation folder -->
<StandardDirectory Id="ProgramFiles6432Folder">
<Directory Id="INSTALLFOLDER" Name="!(bind.Property.ProductName)" />
</StandardDirectory>
<!-- Shows a UI that allows customizing the install location -->
<ui:WixUI Id="WixUI_CustomInstallDir" InstallDirectory="INSTALLFOLDER" />
<Feature Id="Main">
<ComponentGroupRef Id="MainComponent" />
<ComponentRef Id="StartMenuShortcut" />
<ComponentGroupRef Id="AssetsDirectory" />
<ComponentGroupRef Id="CreditsDirectory" />
</Feature>
<!-- Installs the actual files -->
<ComponentGroup Id="MainComponent" Directory="INSTALLFOLDER">
<Component>
<File Id="Executable" Source="..\..\..\target\dist\yachtpit.exe" Vital="true" />
</Component>
</ComponentGroup>
<!-- Start menu shortcut -->
<!-- Source: https://wixtoolset.org/docs/v3/howtos/files_and_registry/create_start_menu_shortcut/ -->
<StandardDirectory Id="ProgramMenuFolder">
<Component Id="StartMenuShortcut" Guid="*">
<Shortcut Id="ApplicationStartMenuShortcut" Name="!(bind.Property.ProductName)" Target="[!Executable]" WorkingDirectory="INSTALLFOLDER" />
<RegistryValue Root="HKCU" Key="Software\!(bind.Property.ProductName)" Name="installed" Type="integer" Value="1" KeyPath="yes"/>
</Component>
</StandardDirectory>
</Package>
</Wix>

View File

@@ -0,0 +1,6 @@
{
"sdk": {
"version": "9.0.100",
"rollForward": "latestFeature"
}
}

9
cleanup.sh Executable file
View File

@@ -0,0 +1,9 @@
#!/usr/bin/env bash
# Clean up build artifacts and temporary directories
echo "Cleaning up build artifacts and temporary directories..."
cargo clean
# Remove persisted data
find . -name "target" -type d -prune -exec rm -rf {} \;
find . -name "dist" -type d -prune -exec rm -rf {} \;
echo "Cleanup complete!"

5
credits/CREDITS.md Normal file
View File

@@ -0,0 +1,5 @@
# Credits
## Assets
* Bevy icon: [MIT License](licenses/Bevy_MIT_License.md);

View File

@@ -0,0 +1,19 @@
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

22
index.html Normal file
View File

@@ -0,0 +1,22 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
<title>yachtpit</title>
<link data-trunk rel="copy-dir" href="assets"/>
<link data-trunk rel="copy-dir" href="credits"/>
<link data-trunk rel="copy-file" href="build/windows/icon.ico"/>
<link rel="icon" href="icon.ico">
<link data-trunk rel="inline" href="build/web/styles.css"/>
</head>
<body>
<link data-trunk rel="inline" href="build/web/sound.js"/>
<div class="game-container">
<div class="lds-dual-ring"></div>
<canvas id="bevy">
Javascript and support for canvas is required
</canvas>
</div>
</body>
</html>

View File

@@ -0,0 +1,5 @@
# Flag to notify the compiler we're building for the iOS simulator from an Apple silicon mac
# This needs some workarounds for now
# See https://github.com/bevyengine/bevy/pull/10178 - remove if it's not needed anymore.
[target.aarch64-apple-ios-sim]
rustflags = ["--cfg=ios_simulator"]

3
mobile/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
mobile.xcodeproj/xcuserdata/
mobile.xcodeproj/project.xcworkspace/
build/

34
mobile/Cargo.toml Normal file
View File

@@ -0,0 +1,34 @@
[package]
name = "mobile"
version = "0.1.0"
edition = "2021"
publish = false
[lib]
name = "mobile"
crate-type = ["staticlib", "cdylib"]
[dependencies]
yachtpit = { path = ".." }
bevy = { version = "0.16.0", default-features = false, features = ["android-native-activity"] }
[target."cfg(target_os = \"ios\")".dependencies]
objc2-avf-audio = { version = "0.3.0", features = [
"AVAudioSession",
"AVAudioSessionTypes",
] }
[package.metadata.android]
package = "io.gs.yachtpit"
apk_name = "yachtpit"
assets = "../assets"
strip = "strip"
resources = "../build/android/res"
build_targets = ["aarch64-linux-android"]
[package.metadata.android.sdk]
target_sdk_version = 35
[package.metadata.android.application]
icon = "@mipmap/icon"
label = "yachtpit"

28
mobile/Makefile Normal file
View File

@@ -0,0 +1,28 @@
.PHONY: xcodebuild run install boot-sim generate clean
DEVICE = ${DEVICE_ID}
ifndef DEVICE_ID
DEVICE=$(shell xcrun simctl list devices 'iOS' | grep -v 'unavailable' | grep -v '^--' | grep -v '==' | head -n 1 | grep -E -o -i "([0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12})")
endif
run: install
xcrun simctl launch --console $(DEVICE) io.gs.yachtpit
boot-sim:
xcrun simctl boot $(DEVICE) || true
install: xcodebuild-simulator boot-sim
xcrun simctl install $(DEVICE) build/Build/Products/Debug-iphonesimulator/mobile.app
xcodebuild-simulator:
IOS_TARGETS=x86_64-apple-ios xcodebuild -scheme mobile -configuration Debug -derivedDataPath build -destination "id=$(DEVICE)"
xcodebuild-iphone:
IOS_TARGETS=aarch64-apple-ios xcodebuild -scheme mobile -configuration Debug -derivedDataPath build -arch arm64
xcodebuild-iphone-release:
IOS_TARGETS=aarch64-apple-ios xcodebuild -scheme mobile -configuration Release -derivedDataPath build -arch arm64
clean:
rm -r build
cargo clean

55
mobile/build_rust_deps.sh Normal file
View File

@@ -0,0 +1,55 @@
#!/usr/bin/env bash
# based on https://github.com/mozilla/glean/blob/main/build-scripts/xc-universal-binary.sh
set -eux
PATH=$PATH:$HOME/.cargo/bin
RELFLAG=
if [[ "$CONFIGURATION" != "Debug" ]]; then
RELFLAG="--profile dist"
fi
set -euvx
if [[ -n "${DEVELOPER_SDK_DIR:-}" ]]; then
# Assume we're in Xcode, which means we're probably cross-compiling.
# In this case, we need to add an extra library search path for build scripts and proc-macros,
# which run on the host instead of the target.
# (macOS Big Sur does not have linkable libraries in /usr/lib/.)
export LIBRARY_PATH="${DEVELOPER_SDK_DIR}/MacOSX.sdk/usr/lib:${LIBRARY_PATH:-}"
fi
# add homebrew bin path, as it's the most commonly used package manager on macOS
# this is needed for cmake on apple arm processors as it's not available by default
export PATH="$PATH:/opt/homebrew/bin"
IS_SIMULATOR=0
if [ "${LLVM_TARGET_TRIPLE_SUFFIX-}" = "-simulator" ]; then
IS_SIMULATOR=1
fi
for arch in $ARCHS; do
case "$arch" in
x86_64)
if [ $IS_SIMULATOR -eq 0 ]; then
echo "Building for x86_64, but not a simulator build. What's going on?" >&2
exit 2
fi
# Intel iOS simulator
export CFLAGS_x86_64_apple_ios="-target x86_64-apple-ios"
cargo rustc --crate-type staticlib --lib $RELFLAG --target x86_64-apple-ios
;;
arm64)
if [ $IS_SIMULATOR -eq 0 ]; then
# Hardware iOS targets
cargo rustc --crate-type staticlib --lib $RELFLAG --target aarch64-apple-ios
else
# M1 iOS simulator -- currently in Nightly only and requires to build `libstd`
cargo rustc --crate-type staticlib --lib $RELFLAG --target aarch64-apple-ios-sim
fi
esac
done

View File

@@ -0,0 +1,14 @@
{
"images" : [
{
"filename" : "icon_1024x1024.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

View File

@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

35
mobile/ios-src/Info.plist Normal file
View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>0.1.1</string>
<key>CFBundleIconName</key>
<string>AppIcon</string>
<key>CFBundleVersion</key>
<string>0.1.1</string>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIRequiresFullScreen</key>
<false/>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
</array>
</dict>
</plist>

View File

@@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="21701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<device id="retina6_12" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="21679"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<rect key="frame" x="0.0" y="0.0" width="393" height="842"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<imageView clipsSubviews="YES" userInteractionEnabled="NO" contentMode="scaleAspectFit" fixedFrame="YES" image="bevy.png" translatesAutoresizingMaskIntoConstraints="NO" id="sic-lC-kjy">
<rect key="frame" x="113" y="346" width="164" height="153"/>
<autoresizingMask key="autoresizingMask" flexibleMinX="YES" widthSizable="YES" flexibleMaxX="YES" flexibleMinY="YES" heightSizable="YES" flexibleMaxY="YES"/>
</imageView>
</subviews>
<viewLayoutGuide key="safeArea" id="Bcu-3y-fUS"/>
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</view>
<modalPageSheetSimulatedSizeMetrics key="simulatedDestinationMetrics"/>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="53" y="375"/>
</scene>
</scenes>
<resources>
<image name="bevy.png" width="152.33058166503906" height="152.33058166503906"/>
</resources>
</document>

View File

@@ -0,0 +1 @@
void main_rs(void);

6
mobile/ios-src/main.m Normal file
View File

@@ -0,0 +1,6 @@
#import "bindings.h"
int main() {
main_rs();
return 0;
}

11
mobile/manifest.yaml Normal file
View File

@@ -0,0 +1,11 @@
android:
gradle: true
# this assets configuration is currently only used without gradle!
#assets:
# - "../assets/*"
icon: "ios-src/Assets.xcassets/AppIcon.appiconset/icon_1024x1024.png"
manifest:
package: "io.gs.yachtpit"
version_code: 1
application:
label: "yachtpit"

View File

@@ -0,0 +1,473 @@
// !$*UTF8*$!
{
archiveVersion = 1;
classes = {
};
objectVersion = 51;
objects = {
/* Begin PBXBuildFile section */
134866208A035F8615C99114 /* Metal.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 96A1E5B62F48B379829E8A0D /* Metal.framework */; };
2469A4292A6F9AC200ACF4EF /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 2469A4282A6F9AC200ACF4EF /* Assets.xcassets */; };
2469A42B2A6FAC7000ACF4EF /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 2469A42A2A6FAC7000ACF4EF /* LaunchScreen.storyboard */; };
2604C99FAB5A8322EDCABB9F /* UIKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = FE71FBCAA714DB4F42459106 /* UIKit.framework */; };
442540D056ADB9AE61A0A590 /* Security.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 9F1B41978FA53999AA836D0F /* Security.framework */; };
55892F1396056740E1AF9685 /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = AF7DE91055EBD05ED77E57F9 /* main.m */; };
55B7188F81C3C4183F81D3AE /* libc++.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = A39528EB2CCB182F5328223A /* libc++.tbd */; };
57CD6306253C7A940098CD4A /* AudioToolbox.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 57CD6305253C7A940098CD4A /* AudioToolbox.framework */; };
57CD630E253C80EC0098CD4A /* assets in Resources */ = {isa = PBXBuildFile; fileRef = 57CD630A253C7F5F0098CD4A /* assets */; };
6ADF1AB92CCDA73A00AF5F8E /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 6ADF1AB82CCDA73A00AF5F8E /* QuartzCore.framework */; };
D4A53EFF2DDD2FF70035BC01 /* AVFoundation.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = D4A53EFE2DDD2FF70035BC01 /* AVFoundation.framework */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
26BF2C4863C966DABAB40DC8 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = 8DBF1E2B5C613DA41701F6D9 /* Project object */;
proxyType = 1;
remoteGlobalIDString = D08AEBE0B1A9C9A7B8C7B33F;
remoteInfo = cargo_ios;
};
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
160DB77300A3F1806F024D47 /* bindings.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = bindings.h; sourceTree = "<group>"; };
2469A4282A6F9AC200ACF4EF /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "ios-src/Assets.xcassets"; sourceTree = SOURCE_ROOT; };
2469A42A2A6FAC7000ACF4EF /* LaunchScreen.storyboard */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; path = ios-src/LaunchScreen.storyboard; sourceTree = "<group>"; };
55EAC02897847195D2F44C15 /* mobile.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = mobile.app; sourceTree = BUILT_PRODUCTS_DIR; };
57CD6305253C7A940098CD4A /* AudioToolbox.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AudioToolbox.framework; path = System/Library/Frameworks/AudioToolbox.framework; sourceTree = SDKROOT; };
57CD630A253C7F5F0098CD4A /* assets */ = {isa = PBXFileReference; lastKnownFileType = folder; name = assets; path = ../../assets; sourceTree = "<group>"; };
6ADF1AB82CCDA73A00AF5F8E /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; };
8EE7F1E3B0303533925D7E33 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
96A1E5B62F48B379829E8A0D /* Metal.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Metal.framework; path = System/Library/Frameworks/Metal.framework; sourceTree = SDKROOT; };
9F1B41978FA53999AA836D0F /* Security.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Security.framework; path = System/Library/Frameworks/Security.framework; sourceTree = SDKROOT; };
A39528EB2CCB182F5328223A /* libc++.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = "libc++.tbd"; path = "usr/lib/libc++.tbd"; sourceTree = SDKROOT; };
AF7DE91055EBD05ED77E57F9 /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = "<group>"; };
D4A53EFE2DDD2FF70035BC01 /* AVFoundation.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = AVFoundation.framework; path = System/Library/Frameworks/AVFoundation.framework; sourceTree = SDKROOT; };
FE71FBCAA714DB4F42459106 /* UIKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UIKit.framework; path = System/Library/Frameworks/UIKit.framework; sourceTree = SDKROOT; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
D5A822CB2D6847BA8800BE4C /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
6ADF1AB92CCDA73A00AF5F8E /* QuartzCore.framework in Frameworks */,
442540D056ADB9AE61A0A590 /* Security.framework in Frameworks */,
134866208A035F8615C99114 /* Metal.framework in Frameworks */,
2604C99FAB5A8322EDCABB9F /* UIKit.framework in Frameworks */,
55B7188F81C3C4183F81D3AE /* libc++.tbd in Frameworks */,
D4A53EFF2DDD2FF70035BC01 /* AVFoundation.framework in Frameworks */,
57CD6306253C7A940098CD4A /* AudioToolbox.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXFrameworksBuildPhase section */
/* Begin PBXGroup section */
321F7D6A765B38E746C35105 /* Products */ = {
isa = PBXGroup;
children = (
55EAC02897847195D2F44C15 /* mobile.app */,
);
name = Products;
sourceTree = "<group>";
};
4F1D6F28B8A5D1927AB0ADED /* ios-src */ = {
isa = PBXGroup;
children = (
2469A4282A6F9AC200ACF4EF /* Assets.xcassets */,
57CD630A253C7F5F0098CD4A /* assets */,
160DB77300A3F1806F024D47 /* bindings.h */,
8EE7F1E3B0303533925D7E33 /* Info.plist */,
AF7DE91055EBD05ED77E57F9 /* main.m */,
);
path = "ios-src";
sourceTree = "<group>";
};
8F2E3E6040EAD2EC9F3FA530 = {
isa = PBXGroup;
children = (
2469A42A2A6FAC7000ACF4EF /* LaunchScreen.storyboard */,
4F1D6F28B8A5D1927AB0ADED /* ios-src */,
EB028409C2D0655412DA6E44 /* Frameworks */,
321F7D6A765B38E746C35105 /* Products */,
);
sourceTree = "<group>";
};
EB028409C2D0655412DA6E44 /* Frameworks */ = {
isa = PBXGroup;
children = (
D4A53EFE2DDD2FF70035BC01 /* AVFoundation.framework */,
6ADF1AB82CCDA73A00AF5F8E /* QuartzCore.framework */,
57CD6305253C7A940098CD4A /* AudioToolbox.framework */,
A39528EB2CCB182F5328223A /* libc++.tbd */,
96A1E5B62F48B379829E8A0D /* Metal.framework */,
9F1B41978FA53999AA836D0F /* Security.framework */,
FE71FBCAA714DB4F42459106 /* UIKit.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */
/* Begin PBXLegacyTarget section */
D08AEBE0B1A9C9A7B8C7B33F /* cargo_ios */ = {
isa = PBXLegacyTarget;
buildArgumentsString = build_rust_deps.sh;
buildConfigurationList = AA00A0CFDB11F37F2BA3FC2E /* Build configuration list for PBXLegacyTarget "cargo_ios" */;
buildPhases = (
FE045B3D04D57B713A565FF8 /* Sources */,
);
buildToolPath = /bin/sh;
buildWorkingDirectory = .;
dependencies = (
);
name = cargo_ios;
passBuildSettingsInEnvironment = 1;
productName = cargo_ios;
};
/* End PBXLegacyTarget section */
/* Begin PBXNativeTarget section */
3BDB8152E4962373181B4FE5 /* mobile */ = {
isa = PBXNativeTarget;
buildConfigurationList = E714A1AEAAE517C348B5BD27 /* Build configuration list for PBXNativeTarget "mobile" */;
buildPhases = (
9F13800790AD9DBC2BC0F116 /* Sources */,
D5A822CB2D6847BA8800BE4C /* Frameworks */,
57CD630D253C80E60098CD4A /* Resources */,
);
buildRules = (
);
dependencies = (
19D4B9C22ADC6705B5132B4C /* PBXTargetDependency */,
);
name = mobile;
productName = mobile;
productReference = 55EAC02897847195D2F44C15 /* mobile.app */;
productType = "com.apple.product-type.application";
};
/* End PBXNativeTarget section */
/* Begin PBXProject section */
8DBF1E2B5C613DA41701F6D9 /* Project object */ = {
isa = PBXProject;
attributes = {
LastUpgradeCheck = 1240;
};
buildConfigurationList = 9D43D41707A5C30B227B83F9 /* Build configuration list for PBXProject "mobile" */;
compatibilityVersion = "Xcode 10.0";
developmentRegion = en;
hasScannedForEncodings = 0;
knownRegions = (
en,
);
mainGroup = 8F2E3E6040EAD2EC9F3FA530;
projectDirPath = "";
projectRoot = "";
targets = (
3BDB8152E4962373181B4FE5 /* mobile */,
D08AEBE0B1A9C9A7B8C7B33F /* cargo_ios */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
57CD630D253C80E60098CD4A /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
2469A42B2A6FAC7000ACF4EF /* LaunchScreen.storyboard in Resources */,
2469A4292A6F9AC200ACF4EF /* Assets.xcassets in Resources */,
57CD630E253C80EC0098CD4A /* assets in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
9F13800790AD9DBC2BC0F116 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
55892F1396056740E1AF9685 /* main.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
FE045B3D04D57B713A565FF8 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
19D4B9C22ADC6705B5132B4C /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = D08AEBE0B1A9C9A7B8C7B33F /* cargo_ios */;
targetProxy = 26BF2C4863C966DABAB40DC8 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin XCBuildConfiguration section */
4AD7BC6FDD56FF18FA6DA7D7 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_IDENTITY = "";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = dwarf;
ENABLE_STRICT_OBJC_MSGSEND = YES;
ENABLE_TESTABILITY = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_DYNAMIC_NO_PIC = NO;
GCC_NO_COMMON_BLOCKS = YES;
GCC_OPTIMIZATION_LEVEL = 0;
GCC_PREPROCESSOR_DEFINITIONS = (
"$(inherited)",
"DEBUG=1",
);
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0;
};
name = Debug;
};
5B14EC4ADC81FBF1F8CF20E9 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_CXX_LANGUAGE_STANDARD = "c++11";
CLANG_CXX_LIBRARY = "libc++";
CODE_SIGN_IDENTITY = "";
DEVELOPMENT_TEAM = "";
ENABLE_BITCODE = NO;
HEADER_SEARCH_PATHS = (
"$(inherited)",
"ios-src/",
);
INFOPLIST_FILE = "ios-src/Info.plist";
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
"LIBRARY_SEARCH_PATHS[sdk=iphoneos*][arch=arm64]" = (
"$(inherited)",
"../target/aarch64-apple-ios/dist",
);
"LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*][arch=arm64]" = (
"$(inherited)",
"../target/aarch64-apple-ios-sim/dist",
);
"LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*][arch=x86_64]" = (
"$(inherited)",
"../target/x86_64-apple-ios/dist",
);
OTHER_LDFLAGS = (
"$(inherited)",
"-lmobile",
"-lc++abi",
);
PRODUCT_BUNDLE_IDENTIFIER=io.gs.yachtpit;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
79E3C28F06346EA58420A93D /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO;
CLANG_ANALYZER_NONNULL = YES;
CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
CLANG_CXX_LIBRARY = "libc++";
CLANG_ENABLE_MODULES = YES;
CLANG_ENABLE_OBJC_ARC = YES;
CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES;
CLANG_WARN_BOOL_CONVERSION = YES;
CLANG_WARN_COMMA = YES;
CLANG_WARN_CONSTANT_CONVERSION = YES;
CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES;
CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR;
CLANG_WARN_DOCUMENTATION_COMMENTS = YES;
CLANG_WARN_EMPTY_BODY = YES;
CLANG_WARN_ENUM_CONVERSION = YES;
CLANG_WARN_INFINITE_RECURSION = YES;
CLANG_WARN_INT_CONVERSION = YES;
CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES;
CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES;
CLANG_WARN_OBJC_LITERAL_CONVERSION = YES;
CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR;
CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES;
CLANG_WARN_RANGE_LOOP_ANALYSIS = YES;
CLANG_WARN_STRICT_PROTOTYPES = YES;
CLANG_WARN_SUSPICIOUS_MOVE = YES;
CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE;
CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_IDENTITY = "";
COPY_PHASE_STRIP = NO;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
ENABLE_NS_ASSERTIONS = NO;
ENABLE_STRICT_OBJC_MSGSEND = YES;
GCC_C_LANGUAGE_STANDARD = gnu11;
GCC_NO_COMMON_BLOCKS = YES;
GCC_WARN_64_TO_32_BIT_CONVERSION = YES;
GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR;
GCC_WARN_UNDECLARED_SELECTOR = YES;
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES;
PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = iphoneos;
SWIFT_COMPILATION_MODE = wholemodule;
SWIFT_VERSION = 5.0;
VALIDATE_PRODUCT = YES;
};
name = Release;
};
8265913A25816D964A847F1B /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = "com.rust.cargo-ios";
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
A2D5B73DD30D562B6F366526 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_CXX_LANGUAGE_STANDARD = "c++11";
CLANG_CXX_LIBRARY = "libc++";
CODE_SIGN_IDENTITY = "";
DEVELOPMENT_TEAM = "";
ENABLE_BITCODE = NO;
HEADER_SEARCH_PATHS = (
"$(inherited)",
"ios-src/",
);
INFOPLIST_FILE = "ios-src/Info.plist";
IPHONEOS_DEPLOYMENT_TARGET = 12.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
"LIBRARY_SEARCH_PATHS[sdk=iphoneos*][arch=arm64]" = (
"$(inherited)",
"../target/aarch64-apple-ios/debug",
);
"LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*][arch=arm64]" = (
"$(inherited)",
"../target/aarch64-apple-ios-sim/debug",
);
"LIBRARY_SEARCH_PATHS[sdk=iphonesimulator*][arch=x86_64]" = (
"$(inherited)",
"../target/x86_64-apple-ios/debug",
);
OTHER_LDFLAGS = (
"$(inherited)",
"-lmobile",
"-lc++abi",
);
PRODUCT_BUNDLE_IDENTIFIER=io.gs.yachtpit;
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
FEA9B18D9236F9F6DC6DF799 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = "com.rust.cargo-ios";
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
9D43D41707A5C30B227B83F9 /* Build configuration list for PBXProject "mobile" */ = {
isa = XCConfigurationList;
buildConfigurations = (
4AD7BC6FDD56FF18FA6DA7D7 /* Debug */,
79E3C28F06346EA58420A93D /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Debug;
};
AA00A0CFDB11F37F2BA3FC2E /* Build configuration list for PBXLegacyTarget "cargo_ios" */ = {
isa = XCConfigurationList;
buildConfigurations = (
8265913A25816D964A847F1B /* Debug */,
FEA9B18D9236F9F6DC6DF799 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Debug;
};
E714A1AEAAE517C348B5BD27 /* Build configuration list for PBXNativeTarget "mobile" */ = {
isa = XCConfigurationList;
buildConfigurations = (
A2D5B73DD30D562B6F366526 /* Debug */,
5B14EC4ADC81FBF1F8CF20E9 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Debug;
};
/* End XCConfigurationList section */
};
rootObject = 8DBF1E2B5C613DA41701F6D9 /* Project object */;
}

View File

@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1310"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "3BDB8152E4962373181B4FE5"
BuildableName = "mobile.app"
BlueprintName = "mobile"
ReferencedContainer = "container:mobile.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES">
<Testables>
</Testables>
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "3BDB8152E4962373181B4FE5"
BuildableName = "mobile.app"
BlueprintName = "mobile"
ReferencedContainer = "container:mobile.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "3BDB8152E4962373181B4FE5"
BuildableName = "mobile.app"
BlueprintName = "mobile"
ReferencedContainer = "container:mobile.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>

41
mobile/src/lib.rs Normal file
View File

@@ -0,0 +1,41 @@
use bevy::prelude::*;
use bevy::window::WindowMode;
use bevy::winit::WinitSettings;
use yachtpit::GamePlugin;
#[unsafe(no_mangle)]
unsafe extern "C" fn main_rs() {
main();
}
// this macro is a no-op on ios and only needed for anroid since bevy 0.16
// see https://github.com/bevyengine/bevy/pull/14780
#[bevy_main]
fn main() {
#[cfg(target_os = "ios")]
unsafe {
// Sets our audio session to Ambient mode to prevent background music from stopping.
// The default for iOS apps is SoloAmbient, which stops background music.
// See apple docs: https://developer.apple.com/documentation/avfaudio/avaudiosession/category-swift.struct/ambient
if let Err(e) = objc2_avf_audio::AVAudioSession::sharedInstance()
.setCategory_error(objc2_avf_audio::AVAudioSessionCategoryAmbient.unwrap())
{
println!("Error setting audio session category: {:?}", e);
}
}
App::new()
.insert_resource(WinitSettings::mobile())
.add_plugins((
DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
resizable: false,
mode: WindowMode::BorderlessFullscreen(MonitorSelection::Current),
..default()
}),
..default()
}),
GamePlugin,
))
.run();
}

13
package.json Normal file
View File

@@ -0,0 +1,13 @@
{
"name": "yachtpit",
"version": "1.0.0",
"description": "",
"main": "index.js",
"files": ["dist"],
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC"
}

View File

@@ -0,0 +1,35 @@
use bevy::prelude::{ButtonInput, KeyCode, Res};
pub enum GameControl {
Up,
Down,
Left,
Right,
}
impl GameControl {
pub fn pressed(&self, keyboard_input: &Res<ButtonInput<KeyCode>>) -> bool {
match self {
GameControl::Up => {
keyboard_input.pressed(KeyCode::KeyW) || keyboard_input.pressed(KeyCode::ArrowUp)
}
GameControl::Down => {
keyboard_input.pressed(KeyCode::KeyS) || keyboard_input.pressed(KeyCode::ArrowDown)
}
GameControl::Left => {
keyboard_input.pressed(KeyCode::KeyA) || keyboard_input.pressed(KeyCode::ArrowLeft)
}
GameControl::Right => {
keyboard_input.pressed(KeyCode::KeyD) || keyboard_input.pressed(KeyCode::ArrowRight)
}
}
}
}
pub fn get_movement(control: GameControl, input: &Res<ButtonInput<KeyCode>>) -> f32 {
if control.pressed(input) {
1.0
} else {
0.0
}
}

44
src/actions/mod.rs Normal file
View File

@@ -0,0 +1,44 @@
use bevy::prelude::*;
use crate::actions::game_control::{get_movement, GameControl};
use crate::GameState;
mod game_control;
pub const FOLLOW_EPSILON: f32 = 5.;
pub struct ActionsPlugin;
// This plugin listens for keyboard input and converts the input into Actions.
// Actions can then be used as a resource in other systems to act on the player input.
impl Plugin for ActionsPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<Actions>().add_systems(
Update,
set_movement_actions.run_if(in_state(GameState::Playing)),
);
}
}
#[derive(Default, Resource)]
pub struct Actions {
pub player_movement: Option<Vec2>,
}
pub fn set_movement_actions(
mut actions: ResMut<Actions>,
keyboard_input: Res<ButtonInput<KeyCode>>,
) {
let player_movement = Vec2::new(
get_movement(GameControl::Right, &keyboard_input)
- get_movement(GameControl::Left, &keyboard_input),
get_movement(GameControl::Up, &keyboard_input)
- get_movement(GameControl::Down, &keyboard_input),
);
if player_movement != Vec2::ZERO {
actions.player_movement = Some(player_movement.normalize());
} else {
actions.player_movement = None;
}
}

56
src/audio.rs Normal file
View File

@@ -0,0 +1,56 @@
use crate::actions::{set_movement_actions, Actions};
use crate::loading::AudioAssets;
use crate::GameState;
use bevy::prelude::*;
use bevy_kira_audio::prelude::*;
pub struct InternalAudioPlugin;
// This plugin is responsible to control the game audio
impl Plugin for InternalAudioPlugin {
fn build(&self, app: &mut App) {
app.add_plugins(AudioPlugin)
.add_systems(OnEnter(GameState::Playing), start_audio)
.add_systems(
Update,
control_flying_sound
.after(set_movement_actions)
.run_if(in_state(GameState::Playing)),
);
}
}
#[derive(Resource)]
struct FlyingAudio(Handle<AudioInstance>);
fn start_audio(mut commands: Commands, audio_assets: Res<AudioAssets>, audio: Res<Audio>) {
audio.pause();
let handle = audio
.play(audio_assets.flying.clone())
.looped()
.with_volume(0.3)
.handle();
commands.insert_resource(FlyingAudio(handle));
}
fn control_flying_sound(
actions: Res<Actions>,
audio: Res<FlyingAudio>,
mut audio_instances: ResMut<Assets<AudioInstance>>,
) {
if let Some(instance) = audio_instances.get_mut(&audio.0) {
match instance.state() {
PlaybackState::Paused { .. } => {
if actions.player_movement.is_some() {
instance.resume(AudioTween::default());
}
}
PlaybackState::Playing { .. } => {
if actions.player_movement.is_none() {
instance.pause(AudioTween::default());
}
}
_ => {}
}
}
}

54
src/lib.rs Normal file
View File

@@ -0,0 +1,54 @@
#![allow(clippy::type_complexity)]
mod actions;
mod audio;
mod loading;
mod menu;
mod player;
use crate::actions::ActionsPlugin;
use crate::audio::InternalAudioPlugin;
use crate::loading::LoadingPlugin;
use crate::menu::MenuPlugin;
use crate::player::PlayerPlugin;
use bevy::app::App;
#[cfg(debug_assertions)]
use bevy::diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin};
use bevy::prelude::*;
// This example game uses States to separate logic
// See https://bevy-cheatbook.github.io/programming/states.html
// Or https://github.com/bevyengine/bevy/blob/main/examples/ecs/state.rs
#[derive(States, Default, Clone, Eq, PartialEq, Debug, Hash)]
enum GameState {
// During the loading State the LoadingPlugin will load our assets
#[default]
Loading,
// During this State the actual game logic is executed
Playing,
// Here the menu is drawn and waiting for player interaction
Menu,
}
pub struct GamePlugin;
impl Plugin for GamePlugin {
fn build(&self, app: &mut App) {
app.init_state::<GameState>().add_plugins((
LoadingPlugin,
MenuPlugin,
ActionsPlugin,
InternalAudioPlugin,
PlayerPlugin,
));
#[cfg(debug_assertions)]
{
app.add_plugins((
FrameTimeDiagnosticsPlugin::default(),
LogDiagnosticsPlugin::default(),
));
}
}
}

37
src/loading.rs Normal file
View File

@@ -0,0 +1,37 @@
use crate::GameState;
use bevy::prelude::*;
use bevy_asset_loader::prelude::*;
use bevy_kira_audio::AudioSource;
pub struct LoadingPlugin;
/// This plugin loads all assets using [`AssetLoader`] from a third party bevy plugin
/// Alternatively you can write the logic to load assets yourself
/// If interested, take a look at <https://bevy-cheatbook.github.io/features/assets.html>
impl Plugin for LoadingPlugin {
fn build(&self, app: &mut App) {
app.add_loading_state(
LoadingState::new(GameState::Loading)
.continue_to_state(GameState::Playing)
.load_collection::<AudioAssets>()
.load_collection::<TextureAssets>(),
);
}
}
// the following asset collections will be loaded during the State `GameState::Loading`
// when done loading, they will be inserted as resources (see <https://github.com/NiklasEi/bevy_asset_loader>)
#[derive(AssetCollection, Resource)]
pub struct AudioAssets {
#[asset(path = "audio/flying.ogg")]
pub flying: Handle<AudioSource>,
}
#[derive(AssetCollection, Resource)]
pub struct TextureAssets {
#[asset(path = "textures/bevy.png")]
pub bevy: Handle<Image>,
#[asset(path = "textures/github.png")]
pub github: Handle<Image>,
}

61
src/main.rs Normal file
View File

@@ -0,0 +1,61 @@
// disable console on windows for release builds
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
use bevy::asset::AssetMetaCheck;
use bevy::prelude::*;
use bevy::window::PrimaryWindow;
use bevy::winit::WinitWindows;
use bevy::DefaultPlugins;
use yachtpit::GamePlugin;
use std::io::Cursor;
use winit::window::Icon;
fn main() {
App::new()
.insert_resource(ClearColor(Color::NONE))
.add_plugins(
DefaultPlugins
.set(WindowPlugin {
primary_window: Some(Window {
title: "yachtpit".to_string(),
// Bind to canvas included in `index.html`
canvas: Some("#bevy".to_owned()),
fit_canvas_to_parent: true,
// Tells wasm not to override default event handling, like F5 and Ctrl+R
prevent_default_event_handling: false,
..default()
}),
..default()
})
.set(AssetPlugin {
meta_check: AssetMetaCheck::Never,
..default()
}),
)
.add_plugins(GamePlugin)
.add_systems(Startup, set_window_icon)
.run();
}
// Sets the icon on windows and X11
fn set_window_icon(
windows: NonSend<WinitWindows>,
primary_window: Query<Entity, With<PrimaryWindow>>,
) -> Result {
let primary_entity = primary_window.single()?;
let Some(primary) = windows.get_window(primary_entity) else {
return Err(BevyError::from("No primary window!"));
};
let icon_buf = Cursor::new(include_bytes!(
"../build/macos/AppIcon.iconset/icon_256x256.png"
));
if let Ok(image) = image::load(icon_buf, image::ImageFormat::Png) {
let image = image.into_rgba8();
let (width, height) = image.dimensions();
let rgba = image.into_raw();
let icon = Icon::from_rgba(rgba, width, height).unwrap();
primary.set_window_icon(Some(icon));
};
Ok(())
}

209
src/menu.rs Normal file
View File

@@ -0,0 +1,209 @@
use crate::loading::TextureAssets;
use crate::GameState;
use bevy::prelude::*;
pub struct MenuPlugin;
/// This plugin is responsible for the game menu (containing only one button...)
/// The menu is only drawn during the State `GameState::Menu` and is removed when that state is exited
impl Plugin for MenuPlugin {
fn build(&self, app: &mut App) {
app.add_systems(OnEnter(GameState::Menu), setup_menu)
.add_systems(Update, click_play_button.run_if(in_state(GameState::Menu)))
.add_systems(OnExit(GameState::Menu), cleanup_menu);
}
}
#[derive(Component)]
struct ButtonColors {
normal: Color,
hovered: Color,
}
impl Default for ButtonColors {
fn default() -> Self {
ButtonColors {
normal: Color::linear_rgb(0.15, 0.15, 0.15),
hovered: Color::linear_rgb(0.25, 0.25, 0.25),
}
}
}
#[derive(Component)]
struct Menu;
fn setup_menu(mut commands: Commands, textures: Res<TextureAssets>) {
info!("menu");
commands.spawn((Camera2d, Msaa::Off));
commands
.spawn((
Node {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
flex_direction: FlexDirection::Column,
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
..default()
},
Menu,
))
.with_children(|children| {
let button_colors = ButtonColors::default();
children
.spawn((
Button,
Node {
width: Val::Px(140.0),
height: Val::Px(50.0),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
..Default::default()
},
BackgroundColor(button_colors.normal),
button_colors,
ChangeState(GameState::Playing),
))
.with_child((
Text::new("Play"),
TextFont {
font_size: 40.0,
..default()
},
TextColor(Color::linear_rgb(0.9, 0.9, 0.9)),
));
});
commands
.spawn((
Node {
flex_direction: FlexDirection::Row,
align_items: AlignItems::Center,
justify_content: JustifyContent::SpaceAround,
bottom: Val::Px(5.),
width: Val::Percent(100.),
position_type: PositionType::Absolute,
..default()
},
Menu,
))
.with_children(|children| {
children
.spawn((
Button,
Node {
width: Val::Px(170.0),
height: Val::Px(50.0),
justify_content: JustifyContent::SpaceAround,
align_items: AlignItems::Center,
padding: UiRect::all(Val::Px(5.)),
..Default::default()
},
BackgroundColor(Color::NONE),
ButtonColors {
normal: Color::NONE,
..default()
},
OpenLink("https://bevyengine.org"),
))
.with_children(|parent| {
parent.spawn((
Text::new("Made with Bevy"),
TextFont {
font_size: 15.0,
..default()
},
TextColor(Color::linear_rgb(0.9, 0.9, 0.9)),
));
parent.spawn((
ImageNode {
image: textures.bevy.clone(),
..default()
},
Node {
width: Val::Px(32.),
..default()
},
));
});
children
.spawn((
Button,
Node {
width: Val::Px(170.0),
height: Val::Px(50.0),
justify_content: JustifyContent::SpaceAround,
align_items: AlignItems::Center,
padding: UiRect::all(Val::Px(5.)),
..default()
},
BackgroundColor(Color::NONE),
ButtonColors {
normal: Color::NONE,
hovered: Color::linear_rgb(0.25, 0.25, 0.25),
},
OpenLink("https://github.com/NiklasEi/bevy_game_template"),
))
.with_children(|parent| {
parent.spawn((
Text::new("Open source"),
TextFont {
font_size: 15.0,
..default()
},
TextColor(Color::linear_rgb(0.9, 0.9, 0.9)),
));
parent.spawn((
ImageNode::new(textures.github.clone()),
Node {
width: Val::Px(32.),
..default()
},
));
});
});
}
#[derive(Component)]
struct ChangeState(GameState);
#[derive(Component)]
struct OpenLink(&'static str);
fn click_play_button(
mut next_state: ResMut<NextState<GameState>>,
mut interaction_query: Query<
(
&Interaction,
&mut BackgroundColor,
&ButtonColors,
Option<&ChangeState>,
Option<&OpenLink>,
),
(Changed<Interaction>, With<Button>),
>,
) {
for (interaction, mut color, button_colors, change_state, open_link) in &mut interaction_query {
match *interaction {
Interaction::Pressed => {
if let Some(state) = change_state {
next_state.set(state.0.clone());
} else if let Some(link) = open_link {
if let Err(error) = webbrowser::open(link.0) {
warn!("Failed to open link {error:?}");
}
}
}
Interaction::Hovered => {
*color = button_colors.hovered.into();
}
Interaction::None => {
*color = button_colors.normal.into();
}
}
}
}
fn cleanup_menu(mut commands: Commands, menu: Query<Entity, With<Menu>>) {
for entity in menu.iter() {
commands.entity(entity).despawn();
}
}

799
src/player.rs Normal file
View File

@@ -0,0 +1,799 @@
use crate::GameState;
use bevy::prelude::*;
pub struct PlayerPlugin;
#[derive(Component)]
pub struct InstrumentCluster;
#[derive(Component)]
pub struct SpeedGauge;
#[derive(Component)]
pub struct DepthGauge;
#[derive(Component)]
pub struct CompassGauge;
#[derive(Component)]
pub struct EngineStatus;
#[derive(Component)]
pub struct NavigationDisplay;
#[derive(Component)]
pub struct GpsIndicator;
#[derive(Component)]
pub struct RadarIndicator;
#[derive(Component)]
pub struct AisIndicator;
#[derive(Component)]
pub struct SystemDisplay;
#[derive(Resource, Default)]
pub struct SelectedSystem {
pub current: Option<SystemType>,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum SystemType {
Gps,
Radar,
Ais,
}
#[derive(Resource)]
pub struct YachtData {
pub speed: f32, // knots
pub depth: f32, // meters
pub heading: f32, // degrees
pub engine_temp: f32, // celsius
pub fuel_level: f32, // percentage
pub battery_level: f32, // percentage
pub wind_speed: f32, // knots
pub wind_direction: f32, // degrees
}
impl Default for YachtData {
fn default() -> Self {
Self {
speed: 12.5,
depth: 15.2,
heading: 045.0,
engine_temp: 82.0,
fuel_level: 75.0,
battery_level: 88.0,
wind_speed: 8.3,
wind_direction: 120.0,
}
}
}
/// This plugin handles the futuristic yacht instrument cluster
/// Instrument cluster is only active during the State `GameState::Playing`
impl Plugin for PlayerPlugin {
fn build(&self, app: &mut App) {
app.init_resource::<YachtData>()
.init_resource::<SelectedSystem>()
.add_systems(OnEnter(GameState::Playing), setup_instrument_cluster)
.add_systems(
Update,
(update_yacht_data, update_instrument_displays, handle_system_interactions, update_system_display)
.run_if(in_state(GameState::Playing))
);
}
}
fn setup_instrument_cluster(mut commands: Commands) {
// Spawn camera since we're bypassing the menu system
commands.spawn((Camera2d, Msaa::Off));
// Main container for the instrument cluster
commands
.spawn((
Node {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
flex_direction: FlexDirection::Column,
..default()
},
BackgroundColor(Color::linear_rgb(0.05, 0.05, 0.1)),
InstrumentCluster,
))
.with_children(|parent| {
// Top row - Main navigation and speed
parent
.spawn(Node {
width: Val::Percent(100.0),
height: Val::Percent(60.0),
flex_direction: FlexDirection::Row,
justify_content: JustifyContent::SpaceEvenly,
align_items: AlignItems::Center,
padding: UiRect::all(Val::Px(20.0)),
..default()
})
.with_children(|row| {
// Speed Gauge
row.spawn((
Node {
width: Val::Px(180.0),
height: Val::Px(180.0),
border: UiRect::all(Val::Px(2.0)),
flex_direction: FlexDirection::Column,
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
..default()
},
BackgroundColor(Color::linear_rgb(0.1, 0.1, 0.15)),
BorderColor(Color::linear_rgb(0.0, 0.8, 1.0)),
SpeedGauge,
))
.with_children(|gauge| {
gauge.spawn((
Text::new("SPEED"),
TextFont {
font_size: 12.0,
..default()
},
TextColor(Color::linear_rgb(0.0, 0.8, 1.0)),
));
gauge.spawn((
Text::new("12.5"),
TextFont {
font_size: 32.0,
..default()
},
TextColor(Color::linear_rgb(0.0, 1.0, 0.8)),
));
gauge.spawn((
Text::new("KTS"),
TextFont {
font_size: 10.0,
..default()
},
TextColor(Color::linear_rgb(0.6, 0.6, 0.6)),
));
});
// Central Navigation Display
row.spawn((
Node {
width: Val::Px(300.0),
height: Val::Px(300.0),
border: UiRect::all(Val::Px(2.0)),
flex_direction: FlexDirection::Column,
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
..default()
},
BackgroundColor(Color::linear_rgb(0.1, 0.15, 0.2)),
BorderColor(Color::linear_rgb(0.0, 0.8, 1.0)),
NavigationDisplay,
))
.with_children(|nav| {
nav.spawn((
Text::new("NAVIGATION"),
TextFont {
font_size: 16.0,
..default()
},
TextColor(Color::linear_rgb(0.0, 0.8, 1.0)),
));
nav.spawn((
Text::new("045°"),
TextFont {
font_size: 48.0,
..default()
},
TextColor(Color::linear_rgb(0.0, 1.0, 0.8)),
CompassGauge,
));
nav.spawn((
Text::new("HEADING"),
TextFont {
font_size: 14.0,
..default()
},
TextColor(Color::linear_rgb(0.6, 0.6, 0.6)),
));
});
// Depth Gauge
row.spawn((
Node {
width: Val::Px(180.0),
height: Val::Px(180.0),
border: UiRect::all(Val::Px(2.0)),
flex_direction: FlexDirection::Column,
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
..default()
},
BackgroundColor(Color::linear_rgb(0.1, 0.1, 0.15)),
BorderColor(Color::linear_rgb(0.0, 0.8, 1.0)),
DepthGauge,
))
.with_children(|gauge| {
gauge.spawn((
Text::new("DEPTH"),
TextFont {
font_size: 12.0,
..default()
},
TextColor(Color::linear_rgb(0.0, 0.8, 1.0)),
));
gauge.spawn((
Text::new("15.2"),
TextFont {
font_size: 32.0,
..default()
},
TextColor(Color::linear_rgb(0.0, 1.0, 0.8)),
));
gauge.spawn((
Text::new("M"),
TextFont {
font_size: 10.0,
..default()
},
TextColor(Color::linear_rgb(0.6, 0.6, 0.6)),
));
});
});
// Bottom row - Engine and system status
parent
.spawn(Node {
width: Val::Percent(100.0),
height: Val::Percent(40.0),
flex_direction: FlexDirection::Row,
justify_content: JustifyContent::SpaceEvenly,
align_items: AlignItems::Center,
padding: UiRect::all(Val::Px(20.0)),
..default()
})
.with_children(|row| {
// Engine Status Panel
row.spawn((
Node {
width: Val::Px(200.0),
height: Val::Px(150.0),
border: UiRect::all(Val::Px(1.0)),
flex_direction: FlexDirection::Column,
justify_content: JustifyContent::SpaceEvenly,
align_items: AlignItems::Center,
padding: UiRect::all(Val::Px(10.0)),
..default()
},
BackgroundColor(Color::linear_rgb(0.1, 0.1, 0.15)),
BorderColor(Color::linear_rgb(0.8, 0.4, 0.0)),
EngineStatus,
))
.with_children(|engine| {
engine.spawn((
Text::new("ENGINE"),
TextFont {
font_size: 14.0,
..default()
},
TextColor(Color::linear_rgb(0.8, 0.4, 0.0)),
));
engine.spawn((
Text::new("82°C"),
TextFont {
font_size: 24.0,
..default()
},
TextColor(Color::linear_rgb(0.0, 1.0, 0.0)),
));
engine.spawn((
Text::new("TEMP NORMAL"),
TextFont {
font_size: 10.0,
..default()
},
TextColor(Color::linear_rgb(0.6, 0.6, 0.6)),
));
});
// System Status Grid
row.spawn((
Node {
width: Val::Px(250.0),
height: Val::Px(150.0),
border: UiRect::all(Val::Px(1.0)),
flex_direction: FlexDirection::Column,
justify_content: JustifyContent::SpaceEvenly,
padding: UiRect::all(Val::Px(10.0)),
..default()
},
BackgroundColor(Color::linear_rgb(0.08, 0.08, 0.12)),
BorderColor(Color::linear_rgb(0.4, 0.4, 0.6)),
))
.with_children(|grid| {
grid.spawn((
Text::new("SYSTEMS"),
TextFont {
font_size: 12.0,
..default()
},
TextColor(Color::linear_rgb(0.4, 0.4, 0.8)),
));
// Fuel Level Bar
grid.spawn(Node {
flex_direction: FlexDirection::Row,
align_items: AlignItems::Center,
justify_content: JustifyContent::SpaceBetween,
width: Val::Percent(100.0),
..default()
})
.with_children(|bar| {
bar.spawn((
Text::new("FUEL"),
TextFont {
font_size: 10.0,
..default()
},
TextColor(Color::linear_rgb(0.7, 0.7, 0.7)),
));
bar.spawn((
Node {
width: Val::Px(80.0),
height: Val::Px(8.0),
border: UiRect::all(Val::Px(1.0)),
..default()
},
BackgroundColor(Color::linear_rgb(0.2, 0.2, 0.2)),
BorderColor(Color::linear_rgb(0.4, 0.4, 0.4)),
))
.with_children(|bar_bg| {
bar_bg.spawn((
Node {
width: Val::Percent(75.0),
height: Val::Percent(100.0),
..default()
},
BackgroundColor(Color::linear_rgb(0.0, 0.8, 0.0)),
));
});
bar.spawn((
Text::new("75%"),
TextFont {
font_size: 9.0,
..default()
},
TextColor(Color::linear_rgb(0.8, 0.8, 0.8)),
));
});
// Battery Level Bar
grid.spawn(Node {
flex_direction: FlexDirection::Row,
align_items: AlignItems::Center,
justify_content: JustifyContent::SpaceBetween,
width: Val::Percent(100.0),
..default()
})
.with_children(|bar| {
bar.spawn((
Text::new("BATTERY"),
TextFont {
font_size: 10.0,
..default()
},
TextColor(Color::linear_rgb(0.7, 0.7, 0.7)),
));
bar.spawn((
Node {
width: Val::Px(80.0),
height: Val::Px(8.0),
border: UiRect::all(Val::Px(1.0)),
..default()
},
BackgroundColor(Color::linear_rgb(0.2, 0.2, 0.2)),
BorderColor(Color::linear_rgb(0.4, 0.4, 0.4)),
))
.with_children(|bar_bg| {
bar_bg.spawn((
Node {
width: Val::Percent(88.0),
height: Val::Percent(100.0),
..default()
},
BackgroundColor(Color::linear_rgb(0.0, 0.6, 1.0)),
));
});
bar.spawn((
Text::new("88%"),
TextFont {
font_size: 9.0,
..default()
},
TextColor(Color::linear_rgb(0.8, 0.8, 0.8)),
));
});
// System Status Indicators
grid.spawn(Node {
flex_direction: FlexDirection::Row,
justify_content: JustifyContent::SpaceBetween,
width: Val::Percent(100.0),
..default()
})
.with_children(|indicators| {
// GPS Indicator
indicators.spawn((
Button,
Node {
flex_direction: FlexDirection::Column,
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
padding: UiRect::all(Val::Px(8.0)),
width: Val::Px(60.0),
height: Val::Px(40.0),
border: UiRect::all(Val::Px(1.0)),
..default()
},
BackgroundColor(Color::linear_rgb(0.1, 0.1, 0.15)),
BorderColor(Color::linear_rgb(0.3, 0.3, 0.4)),
GpsIndicator,
))
.with_children(|indicator| {
indicator.spawn((
Text::new("🛰️"),
TextFont {
font_size: 16.0,
..default()
},
TextColor(Color::linear_rgb(0.0, 1.0, 0.0)),
));
indicator.spawn((
Text::new("GPS"),
TextFont {
font_size: 8.0,
..default()
},
TextColor(Color::linear_rgb(0.6, 0.6, 0.6)),
));
});
// RADAR Indicator
indicators.spawn((
Button,
Node {
flex_direction: FlexDirection::Column,
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
padding: UiRect::all(Val::Px(8.0)),
width: Val::Px(60.0),
height: Val::Px(40.0),
border: UiRect::all(Val::Px(1.0)),
..default()
},
BackgroundColor(Color::linear_rgb(0.1, 0.1, 0.15)),
BorderColor(Color::linear_rgb(0.3, 0.3, 0.4)),
RadarIndicator,
))
.with_children(|indicator| {
indicator.spawn((
Text::new("📡"),
TextFont {
font_size: 16.0,
..default()
},
TextColor(Color::linear_rgb(0.0, 1.0, 0.0)),
));
indicator.spawn((
Text::new("RADAR"),
TextFont {
font_size: 8.0,
..default()
},
TextColor(Color::linear_rgb(0.6, 0.6, 0.6)),
));
});
// AIS Indicator
indicators.spawn((
Button,
Node {
flex_direction: FlexDirection::Column,
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
padding: UiRect::all(Val::Px(8.0)),
width: Val::Px(60.0),
height: Val::Px(40.0),
border: UiRect::all(Val::Px(1.0)),
..default()
},
BackgroundColor(Color::linear_rgb(0.1, 0.1, 0.15)),
BorderColor(Color::linear_rgb(0.3, 0.3, 0.4)),
AisIndicator,
))
.with_children(|indicator| {
indicator.spawn((
Text::new("🚢"),
TextFont {
font_size: 16.0,
..default()
},
TextColor(Color::linear_rgb(0.8, 0.0, 0.0)),
));
indicator.spawn((
Text::new("AIS"),
TextFont {
font_size: 8.0,
..default()
},
TextColor(Color::linear_rgb(0.6, 0.6, 0.6)),
));
});
});
});
// Wind Information
row.spawn((
Node {
width: Val::Px(200.0),
height: Val::Px(150.0),
border: UiRect::all(Val::Px(1.0)),
flex_direction: FlexDirection::Column,
justify_content: JustifyContent::SpaceEvenly,
align_items: AlignItems::Center,
padding: UiRect::all(Val::Px(10.0)),
..default()
},
BackgroundColor(Color::linear_rgb(0.1, 0.15, 0.1)),
BorderColor(Color::linear_rgb(0.0, 0.8, 0.4)),
))
.with_children(|wind| {
wind.spawn((
Text::new("WIND"),
TextFont {
font_size: 14.0,
..default()
},
TextColor(Color::linear_rgb(0.0, 0.8, 0.4)),
));
wind.spawn((
Text::new("8.3 KTS"),
TextFont {
font_size: 18.0,
..default()
},
TextColor(Color::linear_rgb(0.0, 1.0, 0.6)),
));
wind.spawn((
Text::new("120° REL"),
TextFont {
font_size: 14.0,
..default()
},
TextColor(Color::linear_rgb(0.6, 0.8, 0.6)),
));
});
});
// System Display Area
parent
.spawn((
Node {
width: Val::Percent(100.0),
height: Val::Px(200.0),
border: UiRect::all(Val::Px(2.0)),
flex_direction: FlexDirection::Column,
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
padding: UiRect::all(Val::Px(20.0)),
..default()
},
BackgroundColor(Color::linear_rgb(0.05, 0.05, 0.1)),
BorderColor(Color::linear_rgb(0.2, 0.2, 0.3)),
SystemDisplay,
))
.with_children(|display| {
display.spawn((
Text::new("Select a system above to view details"),
TextFont {
font_size: 16.0,
..default()
},
TextColor(Color::linear_rgb(0.5, 0.5, 0.6)),
));
});
});
}
fn update_yacht_data(mut yacht_data: ResMut<YachtData>, time: Res<Time>) {
let t = time.elapsed_secs();
// Simulate realistic yacht data with some variation
yacht_data.speed = 12.5 + (t * 0.3).sin() * 2.0;
yacht_data.depth = 15.2 + (t * 0.1).sin() * 3.0;
yacht_data.heading = (yacht_data.heading + time.delta_secs() * 5.0) % 360.0;
yacht_data.engine_temp = 82.0 + (t * 0.2).sin() * 3.0;
yacht_data.wind_speed = 8.3 + (t * 0.4).sin() * 1.5;
yacht_data.wind_direction = (yacht_data.wind_direction + time.delta_secs() * 10.0) % 360.0;
// Slowly drain fuel and battery (very slowly for demo purposes)
yacht_data.fuel_level = (yacht_data.fuel_level - time.delta_secs() * 0.01).max(0.0);
yacht_data.battery_level = (yacht_data.battery_level - time.delta_secs() * 0.005).max(0.0);
}
fn update_instrument_displays(
yacht_data: Res<YachtData>,
mut speed_query: Query<&mut Text, (With<SpeedGauge>, Without<DepthGauge>, Without<CompassGauge>)>,
mut depth_query: Query<&mut Text, (With<DepthGauge>, Without<SpeedGauge>, Without<CompassGauge>)>,
mut compass_query: Query<&mut Text, (With<CompassGauge>, Without<SpeedGauge>, Without<DepthGauge>)>,
) {
// Update speed display
for mut text in speed_query.iter_mut() {
if text.0.contains('.') {
text.0 = format!("{:.1}", yacht_data.speed);
}
}
// Update depth display
for mut text in depth_query.iter_mut() {
if text.0.contains('.') {
text.0 = format!("{:.1}", yacht_data.depth);
}
}
// Update compass display
for mut text in compass_query.iter_mut() {
if text.0.contains('°') {
text.0 = format!("{:03.0}°", yacht_data.heading);
}
}
}
fn handle_system_interactions(
mut selected_system: ResMut<SelectedSystem>,
mut interaction_query: Query<
(&Interaction, &mut BackgroundColor, Option<&GpsIndicator>, Option<&RadarIndicator>, Option<&AisIndicator>),
(Changed<Interaction>, With<Button>),
>,
) {
for (interaction, mut background_color, gps, radar, ais) in &mut interaction_query {
match *interaction {
Interaction::Pressed => {
if gps.is_some() {
selected_system.current = Some(SystemType::Gps);
*background_color = BackgroundColor(Color::linear_rgb(0.0, 0.3, 0.5));
} else if radar.is_some() {
selected_system.current = Some(SystemType::Radar);
*background_color = BackgroundColor(Color::linear_rgb(0.0, 0.3, 0.5));
} else if ais.is_some() {
selected_system.current = Some(SystemType::Ais);
*background_color = BackgroundColor(Color::linear_rgb(0.0, 0.3, 0.5));
}
}
Interaction::Hovered => {
*background_color = BackgroundColor(Color::linear_rgb(0.15, 0.15, 0.2));
}
Interaction::None => {
*background_color = BackgroundColor(Color::linear_rgb(0.1, 0.1, 0.15));
}
}
}
}
fn update_system_display(
selected_system: Res<SelectedSystem>,
mut display_query: Query<&mut Text, With<SystemDisplay>>,
yacht_data: Res<YachtData>,
time: Res<Time>,
) {
if let Ok(mut text) = display_query.single_mut() {
match selected_system.current {
Some(SystemType::Gps) => {
text.0 = format!(
"GPS NAVIGATION SYSTEM\n\n\
Position: 43°38'19.5\"N 1°26'58.3\"W\n\
Heading: {:.0}°\n\
Speed: {:.1} knots\n\
Course Over Ground: {:.0}°\n\
Satellites: 12 connected\n\
HDOP: 0.8 (Excellent)\n\
\n\
Next Waypoint: MONACO HARBOR\n\
Distance: 127.3 NM\n\
ETA: 10h 12m",
yacht_data.heading,
yacht_data.speed,
yacht_data.heading + 5.0
);
}
Some(SystemType::Radar) => {
let sweep_angle = (time.elapsed_secs() * 60.0) % 360.0;
text.0 = format!(
"RADAR SYSTEM - 12 NM RANGE\n\n\
Status: ACTIVE\n\
Sweep: {:.0}°\n\
Gain: AUTO\n\
Sea Clutter: -15 dB\n\
Rain Clutter: OFF\n\
\n\
CONTACTS DETECTED:\n\
• Vessel 1: 2.3 NM @ 045° (15 kts)\n\
• Vessel 2: 5.7 NM @ 180° (8 kts)\n\
• Land Mass: 8.2 NM @ 270°\n\
• Buoy: 1.1 NM @ 315°",
sweep_angle
);
}
Some(SystemType::Ais) => {
text.0 = format!(
"AIS - AUTOMATIC IDENTIFICATION SYSTEM\n\n\
Status: RECEIVING\n\
Own Ship MMSI: 123456789\n\
\n\
NEARBY VESSELS:\n\
\n\
🛥️ M/Y SERENITY\n\
MMSI: 987654321\n\
Distance: 2.1 NM @ 045°\n\
Speed: 12.5 kts\n\
Course: 180°\n\
\n\
🚢 CARGO VESSEL ATLANTIS\n\
MMSI: 456789123\n\
Distance: 5.8 NM @ 270°\n\
Speed: 18.2 kts\n\
Course: 090°\n\
\n\
⛵ S/Y WIND DANCER\n\
MMSI: 789123456\n\
Distance: 1.3 NM @ 135°\n\
Speed: 6.8 kts\n\
Course: 225°"
);
}
None => {
text.0 = "Select a system above to view details".to_string();
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_system_type_enum() {
let gps = SystemType::Gps;
let radar = SystemType::Radar;
let ais = SystemType::Ais;
assert_ne!(gps, radar);
assert_ne!(radar, ais);
assert_ne!(ais, gps);
}
#[test]
fn test_selected_system_default() {
let selected_system = SelectedSystem::default();
assert_eq!(selected_system.current, None);
}
#[test]
fn test_yacht_data_default() {
let yacht_data = YachtData::default();
assert_eq!(yacht_data.speed, 12.5);
assert_eq!(yacht_data.depth, 15.2);
assert_eq!(yacht_data.heading, 45.0);
assert_eq!(yacht_data.fuel_level, 75.0);
assert_eq!(yacht_data.battery_level, 88.0);
}
}