14 Commits

Author SHA1 Message Date
3a3a83b071 update readme
Some checks failed
CI / Tests (push) Failing after 47s
CI / Clippy lints (push) Failing after 52s
CI / Bevy lints (push) Failing after 1m21s
2026-03-18 15:35:12 -04:00
69215bdb91 update workflow
Some checks failed
CI / Tests (push) Failing after 1m10s
CI / Clippy lints (push) Failing after 1m24s
CI / Bevy lints (push) Failing after 1m50s
2026-03-18 15:24:56 -04:00
9f8d1888ec update workflow 2026-03-18 15:21:38 -04:00
ebae1599f1 building spawning update
Some checks failed
Rust / build (push) Failing after 4s
2026-03-18 15:15:11 -04:00
8bf58bb04c more ui testing
Some checks failed
Rust / build (push) Failing after 4s
2026-03-17 17:57:41 -04:00
f342970228 ui testing
Some checks failed
Rust / build (push) Failing after 4s
2026-03-16 22:58:12 -04:00
fca907aee4 ui testing
Some checks failed
Rust / build (push) Failing after 4s
2026-03-16 20:31:40 -04:00
f9e96cc74b configured ui camera
Some checks failed
Rust / build (push) Failing after 4s
2026-03-16 16:59:40 -04:00
3551ecfde8 configured ui camera 2026-03-16 16:59:29 -04:00
227b654e50 hex coords unit tests wip
Some checks failed
Rust / build (push) Failing after 4s
2026-03-14 20:56:06 -04:00
13a74f7620 refactoring hex coords
Some checks failed
Rust / build (push) Failing after 4s
2026-03-14 19:56:01 -04:00
912ee376c6 refactoring hex coords 2026-03-14 19:55:53 -04:00
c5da119109 Implement coords collection + footprint neightbors
Some checks failed
Rust / build (push) Failing after 4s
2026-03-13 23:19:49 -04:00
945bb8c4de Implement coords collection + footprint neightbors
Some checks failed
Rust / build (push) Failing after 4s
2026-03-13 23:19:33 -04:00
57 changed files with 2093 additions and 599 deletions

140
.github/workflows/ci.yaml vendored Normal file
View File

@@ -0,0 +1,140 @@
name: CI
on:
push:
branches: [master]
pull_request:
branches: [master]
concurrency:
group: ${{ github.workflow }}-${{ github.ref || github.run_id }}
cancel-in-progress: true
env:
# Reduce compile time and cache size.
RUSTFLAGS: -Zshare-generics=y -Zthreads=0
RUSTDOCFLAGS: -Zshare-generics=y -Zthreads=0
# Use the same Rust toolchain across jobs so they can share a cache.
toolchain: nightly-2025-04-03
jobs:
# Run Clippy lints.
clippy-lints:
name: Clippy lints
runs-on: ubuntu-latest
env:
RUSTFLAGS: "-Dwarnings"
timeout-minutes: 20
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ env.toolchain }}
components: clippy
- name: Restore Rust cache
id: cache
uses: Swatinem/rust-cache@v2
with:
shared-key: ci
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install build dependencies
if: steps.cache.outputs.cache-hit != 'true'
run: sudo apt-get -y update; sudo apt-get -y install --no-install-recommends libasound2-dev libudev-dev libwayland-dev
- name: Run Clippy lints
run: cargo clippy --locked --workspace --all-targets --profile ci --all-features
# Run Bevy lints.
bevy-lints:
name: Bevy lints
runs-on: ubuntu-latest
env:
RUSTFLAGS: "-Dwarnings"
timeout-minutes: 20
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install Rust toolchain (plus bevy_lint)
uses: TheBevyFlock/bevy_cli/bevy_lint@lint-v0.3.0
- name: Restore Rust cache
id: cache
uses: Swatinem/rust-cache@v2
with:
shared-key: ci
save-if: false
- name: Install build dependencies
if: steps.cache.outputs.cache-hit != 'true'
run: sudo apt-get -y update; sudo apt-get -y install --no-install-recommends libasound2-dev libudev-dev libwayland-dev
- name: Run Bevy lints
run: bevy_lint --locked --workspace --all-targets --profile ci --all-features
# Run tests.
tests:
name: Tests
runs-on: ubuntu-latest
timeout-minutes: 40
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up environment
run: echo "RUSTFLAGS=${RUSTFLAGS:+$RUSTFLAGS }-Zcodegen-backend=cranelift" >> "${GITHUB_ENV}"
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@master
with:
toolchain: ${{ env.toolchain }}
components: rustc-codegen-cranelift-preview
- name: Restore Rust cache
uses: Swatinem/rust-cache@v2
with:
shared-key: test
cache-directories: ${{ env.LD_LIBRARY_PATH }}
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install build dependencies
run: sudo apt-get -y update; sudo apt-get -y install --no-install-recommends libasound2-dev libudev-dev libwayland-dev
- name: Run tests
run: cargo test --locked --workspace --all-targets --profile ci --no-fail-fast
# Check that the web build compiles.
# check-web:
# name: Check web
# runs-on: ubuntu-latest
# env:
# RUSTFLAGS: -Zshare-generics=y -Zthreads=0 --cfg getrandom_backend="wasm_js"
# timeout-minutes: 20
# steps:
# - name: Checkout repository
# uses: actions/checkout@v4
# - name: Install Rust toolchain
# uses: dtolnay/rust-toolchain@master
# with:
# toolchain: ${{ env.toolchain }}
# targets: wasm32-unknown-unknown
# - name: Restore Rust cache
# id: cache
# uses: Swatinem/rust-cache@v2
# with:
# shared-key: web-ci
# save-if: ${{ github.ref == 'refs/heads/main' }}
# - name: Install build dependencies
# if: steps.cache.outputs.cache-hit != 'true'
# run: sudo apt-get -y update; sudo apt-get -y install --no-install-recommends libasound2-dev libudev-dev libwayland-dev
# - name: Check web
# run: cargo check --config 'profile.web.inherits="dev"' --profile ci --no-default-features --features "dev web" --target wasm32-unknown-unknown

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

@@ -0,0 +1,335 @@
name: Release
on:
# Trigger this workflow when a tag is pushed in the format `v1.2.3`.
push:
tags:
# Pattern syntax: <https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#filter-pattern-cheat-sheet>.
- "v[0-9]+.[0-9]+.[0-9]+*"
# Trigger this workflow manually via workflow dispatch.
workflow_dispatch:
inputs:
version:
description: "Version number in the format `v1.2.3`"
required: true
type: string
concurrency:
group: ${{ github.workflow }}-${{ github.ref || github.run_id }}
cancel-in-progress: true
# Configure the release workflow by editing the following values.
env:
# The base filename of the binary produced by `cargo build`.
cargo_build_binary_name: phos
# The path to the assets directory.
assets_path: assets
# Whether to build and package a release for a given target platform.
build_for_web: false
build_for_linux: true
build_for_windows: true
build_for_macos: false
# Whether to upload the packages produced by this workflow to a GitHub release.
upload_to_github: true
# The itch.io project to upload to in the format `user-name/project-name`.
# There will be no upload to itch.io if this is commented out.
# upload_to_itch: amatsugu/phos
############
# ADVANCED #
############
# The ID of the app produced by this workflow.
# Applies to macOS releases.
# Must contain only A-Z, a-z, 0-9, hyphen, and period: <https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundleidentifier>.
app_id: amatsugu.phos
# The base filename of the binary in the package produced by this workflow.
# Applies to Windows, macOS, and Linux releases.
# Defaults to `cargo_build_binary_name` if commented out.
#app_binary_name: bevy_jam_6
# The name of the `.zip` or `.dmg` file produced by this workflow.
# Defaults to `app_binary_name` if commented out.
#app_package_name: bevy-new-2d
# The display name of the app produced by this workflow.
# Applies to macOS releases.
# Defaults to `app_package_name` if commented out.
app_display_name: phos
# The short display name of the app produced by this workflow.
# Applies to macOS releases.
# Must be 15 or fewer characters: <https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundlename>.
# Defaults to `app_display_name` if commented out.
#app_short_name: Phos
# Before enabling LFS, please take a look at GitHub's documentation for costs and quota limits:
# <https://docs.github.com/en/repositories/working-with-files/managing-large-files/about-storage-and-bandwidth-usage>
git_lfs: false
# Enabling this only helps with consecutive releases to the same tag (and takes up cache storage space).
# See: <https://github.com/orgs/community/discussions/27059>.
use_github_cache: false
# Reduce compile time.
RUSTFLAGS: -Zshare-generics=y -Zthreads=0
jobs:
# Forward some environment variables as outputs of this job.
# This is needed because the `env` context can't be used in the `if:` condition of a job:
# <https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability>
forward-env:
runs-on: ubuntu-latest
steps:
- name: Do nothing
run: "true"
outputs:
upload_to_itch: ${{ env.upload_to_itch }}
# Determine the version number for this workflow.
get-version:
runs-on: ubuntu-latest
steps:
- name: Determine version number
id: tag
run: echo "tag=${GITHUB_REF#refs/tags/}" >> "${GITHUB_OUTPUT}"
outputs:
# Use the input from workflow dispatch, or fall back to the git tag.
version: ${{ inputs.version || steps.tag.outputs.tag }}
# Build and package a release for each platform.
build:
needs:
- get-version
env:
version: ${{ needs.get-version.outputs.version }}
# Avoid rate-limiting. See: <https://github.com/cargo-bins/cargo-binstall/issues/2045>.
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
strategy:
matrix:
include:
- platform: web
targets: wasm32-unknown-unknown
package_ext: .zip
runner: ubuntu-latest
- platform: linux
targets: x86_64-unknown-linux-gnu
package_ext: .zip
runner: ubuntu-latest
- platform: windows
targets: x86_64-pc-windows-msvc
binary_ext: .exe
package_ext: .zip
runner: windows-latest
- platform: macos
targets: x86_64-apple-darwin aarch64-apple-darwin
app_suffix: .app/Contents/MacOS
package_ext: .dmg
runner: macos-latest
runs-on: ${{ matrix.runner }}
permissions:
# Required to create a GitHub release: <https://docs.github.com/en/rest/releases/releases#create-a-release>.
contents: write
defaults:
run:
shell: bash
steps:
- name: Set up environment
run: |
# Default values:
echo "app_binary_name=${app_binary_name:=${{ env.cargo_build_binary_name }}}" >> "${GITHUB_ENV}"
echo "app_package_name=${app_package_name:=${app_binary_name}}" >> "${GITHUB_ENV}"
echo "app_display_name=${app_display_name:=${app_package_name}}" >> "${GITHUB_ENV}"
echo "app_short_name=${app_short_name:=${app_display_name}}" >> "${GITHUB_ENV}"
# File paths:
echo "app=tmp/app/${app_package_name}"'${{ matrix.app_suffix }}' >> "${GITHUB_ENV}"
echo "package=${app_package_name}-"'${{ matrix.platform }}${{ matrix.package_ext }}' >> "${GITHUB_ENV}"
# macOS environment:
if [ '${{ matrix.platform }}' = 'macos' ]; then
echo 'MACOSX_DEPLOYMENT_TARGET=11.0' >> "${GITHUB_ENV}" # macOS 11.0 Big Sur is the first version to support universal binaries.
echo "SDKROOT=$(xcrun --sdk macosx --show-sdk-path)" >> "${GITHUB_ENV}"
fi
# Check if building for this platform is enabled.
echo 'is_platform_enabled=${{
(matrix.platform == 'web' && env.build_for_web == 'true') ||
(matrix.platform == 'linux' && env.build_for_linux == 'true') ||
(matrix.platform == 'windows' && env.build_for_windows == 'true') ||
(matrix.platform == 'macos' && env.build_for_macos == 'true')
}}' >> "${GITHUB_ENV}"
- name: Checkout repository
if: ${{ env.is_platform_enabled == 'true' }}
uses: actions/checkout@v4
with:
lfs: ${{ env.git_lfs }}
- name: Install Rust toolchain
if: ${{ env.is_platform_enabled == 'true' }}
uses: dtolnay/rust-toolchain@nightly
with:
targets: ${{ matrix.targets }}
- name: Restore Rust cache
if: ${{ env.is_platform_enabled == 'true' && env.use_github_cache == 'true' }}
uses: Swatinem/rust-cache@v2
with:
shared-key: release
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install build dependencies (Linux)
if: ${{ env.is_platform_enabled == 'true' && matrix.platform == 'linux' }}
run: sudo apt-get -y update; sudo apt-get -y install --no-install-recommends libasound2-dev libudev-dev libwayland-dev libxkbcommon-dev
- name: Prepare output directories
if: ${{ env.is_platform_enabled == 'true' }}
run: rm -rf tmp; mkdir -p tmp/binary '${{ env.app }}'
- name: Install cargo-binstall
if: ${{ env.is_platform_enabled == 'true' }}
uses: cargo-bins/cargo-binstall@main
- name: Install Bevy CLI
if: ${{ env.is_platform_enabled == 'true' }}
run: cargo binstall --locked --no-confirm --force --git='https://github.com/TheBevyFlock/bevy_cli' bevy_cli
- name: Build and add web bundle to app (Web)
if: ${{ env.is_platform_enabled == 'true' && matrix.platform == 'web' }}
run: |
cargo binstall --locked --no-confirm --force wasm-bindgen-cli
cargo binstall --locked --no-confirm --force wasm-opt
RUSTFLAGS='-Zshare-generics=y -Zthreads=0 --cfg getrandom_backend="wasm_js"' bevy build --locked --release --features='${{ matrix.features }} web' --yes web --bundle
mv 'target/bevy_web/web-release/${{ env.cargo_build_binary_name }}' '${{ env.app }}'
- name: Build and add binaries to app (non-Web)
if: ${{ env.is_platform_enabled == 'true' && matrix.platform != 'web' }}
run: |
for target in ${{ matrix.targets }}; do
bevy build --locked --release --target="${target}" --features='${{ matrix.features }}'
mv target/"${target}"/release/'${{ env.cargo_build_binary_name }}${{ matrix.binary_ext }}' tmp/binary/"${target}"'${{ matrix.binary_ext }}'
done
if [ '${{ matrix.platform }}' = 'macos' ]; then
lipo tmp/binary/*'${{ matrix.binary_ext }}' -create -output '${{ env.app }}/${{ env.app_binary_name }}${{ matrix.binary_ext }}'
else
mv tmp/binary/*'${{ matrix.binary_ext }}' '${{ env.app }}/${{ env.app_binary_name }}${{ matrix.binary_ext }}'
fi
- name: Add assets to app (non-Web)
if: ${{ env.is_platform_enabled == 'true' && matrix.platform != 'web' }}
run: cp -R ./'${{ env.assets_path }}' '${{ env.app }}' || true # Ignore error if assets folder does not exist.
- name: Add metadata to app (macOS)
if: ${{ env.is_platform_enabled == 'true' && matrix.platform == 'macos' }}
run: |
cat >'${{ env.app }}/../Info.plist' <<EOF
<?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>${{ env.app_display_name }}</string>
<key>CFBundleExecutable</key>
<string>${{ env.app_binary_name }}</string>
<key>CFBundleIdentifier</key>
<string>${{ env.app_id }}</string>
<key>CFBundleName</key>
<string>${{ env.app_short_name }}</string>
<key>CFBundleShortVersionString</key>
<string>${{ env.version }}</string>
<key>CFBundleVersion</key>
<string>${{ env.version }}</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleSupportedPlatforms</key>
<array>
<string>MacOSX</string>
</array>
</dict>
</plist>
EOF
- name: Package app (non-Windows)
if: ${{ env.is_platform_enabled == 'true' && matrix.platform != 'windows' }}
working-directory: tmp/app
run: |
if [ '${{ matrix.platform }}' = 'macos' ]; then
ln -s /Applications .
hdiutil create -fs HFS+ -volname '${{ env.app_package_name }}' -srcfolder . '${{ env.package }}'
else
zip --recurse-paths '${{ env.package }}' '${{ env.app_package_name }}'
fi
- name: Package app (Windows)
if: ${{ env.is_platform_enabled == 'true' && matrix.platform == 'windows' }}
working-directory: tmp/app
shell: pwsh
run: Compress-Archive -Path '${{ env.app_package_name }}' -DestinationPath '${{ env.package }}'
- name: Upload package to workflow artifacts
if: ${{ env.is_platform_enabled == 'true' }}
uses: actions/upload-artifact@v4
with:
path: tmp/app/${{ env.package }}
name: package-${{ matrix.platform }}
retention-days: 1
- name: Upload package to GitHub release
if: ${{ env.is_platform_enabled == 'true' && env.upload_to_github == 'true' }}
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: tmp/app/${{ env.package }}
asset_name: ${{ env.package }}
release_name: ${{ env.version }}
tag: ${{ env.version }}
overwrite: true
# Upload all packages to itch.io.
upload-to-itch:
runs-on: ubuntu-latest
needs:
- forward-env
- get-version
- build
if: ${{ needs.forward-env.outputs.upload_to_itch != '' }}
steps:
- name: Download all packages
uses: actions/download-artifact@v4
with:
pattern: package-*
path: tmp
- name: Install butler
run: |
curl -L -o butler.zip 'https://broth.itch.zone/butler/linux-amd64/LATEST/archive/default'
unzip butler.zip
chmod +x butler
./butler -V
- name: Upload all packages to itch.io
env:
BUTLER_API_KEY: ${{ secrets.BUTLER_CREDENTIALS }}
run: |
for channel in $(ls tmp); do
./butler push \
--fix-permissions \
--userversion='${{ needs.get-version.outputs.version }}' \
tmp/"${channel}"/* \
'${{ env.upload_to_itch }}':"${channel#package-}"
done

347
.github/workflows/release.yaml.template vendored Normal file
View File

@@ -0,0 +1,347 @@
name: Release
on:
# Trigger this workflow when a tag is pushed in the format `v1.2.3`.
push:
tags:
# Pattern syntax: <https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#filter-pattern-cheat-sheet>.
- "v[0-9]+.[0-9]+.[0-9]+*"
# Trigger this workflow manually via workflow dispatch.
workflow_dispatch:
inputs:
version:
description: "Version number in the format `v1.2.3`"
required: true
type: string
{% raw -%}
concurrency:
group: ${{ github.workflow }}-${{ github.ref || github.run_id }}
cancel-in-progress: true
{%- endraw %}
# Configure the release workflow by editing the following values.
env:
# The base filename of the binary produced by `cargo build`.
cargo_build_binary_name: {{project-name}}
# The path to the assets directory.
assets_path: assets
# Whether to build and package a release for a given target platform.
build_for_web: true
build_for_linux: true
build_for_windows: true
build_for_macos: true
# Whether to upload the packages produced by this workflow to a GitHub release.
upload_to_github: true
# The itch.io project to upload to in the format `user-name/project-name`.
# There will be no upload to itch.io if this is commented out.
{%- if itch_username != "" %}
{%- if itch_project != "" %}
upload_to_itch: {{itch_username}}/{{itch_project}}
{%- else %}
upload_to_itch: {{itch_username}}/{{project-name}}
{%- endif %}
{%- else %}
#upload_to_itch: your-itch-username/{{project-name}}
{%- endif %}
############
# ADVANCED #
############
# The ID of the app produced by this workflow.
# Applies to macOS releases.
# Must contain only A-Z, a-z, 0-9, hyphen, and period: <https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundleidentifier>.
app_id: {{itch_username | kebab_case}}.{{project-name | kebab_case}}
# The base filename of the binary in the package produced by this workflow.
# Applies to Windows, macOS, and Linux releases.
# Defaults to `cargo_build_binary_name` if commented out.
#app_binary_name: {{project-name}}
# The name of the `.zip` or `.dmg` file produced by this workflow.
# Defaults to `app_binary_name` if commented out.
#app_package_name: {{project-name | kebab_case}}
# The display name of the app produced by this workflow.
# Applies to macOS releases.
# Defaults to `app_package_name` if commented out.
#app_display_name: {{project-name | title_case}}
# The short display name of the app produced by this workflow.
# Applies to macOS releases.
# Must be 15 or fewer characters: <https://developer.apple.com/documentation/bundleresources/information_property_list/cfbundlename>.
# Defaults to `app_display_name` if commented out.
#app_short_name: {{project-name | title_case | truncate: 15, "…"}}
# Before enabling LFS, please take a look at GitHub's documentation for costs and quota limits:
# <https://docs.github.com/en/repositories/working-with-files/managing-large-files/about-storage-and-bandwidth-usage>
git_lfs: false
# Enabling this only helps with consecutive releases to the same tag (and takes up cache storage space).
# See: <https://github.com/orgs/community/discussions/27059>.
use_github_cache: false
# Reduce compile time.
RUSTFLAGS: -Dwarnings -Zshare-generics=y -Zthreads=0
{% raw -%}
jobs:
# Forward some environment variables as outputs of this job.
# This is needed because the `env` context can't be used in the `if:` condition of a job:
# <https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability>
forward-env:
runs-on: ubuntu-latest
steps:
- name: Do nothing
run: "true"
outputs:
upload_to_itch: ${{ env.upload_to_itch }}
# Determine the version number for this workflow.
get-version:
runs-on: ubuntu-latest
steps:
- name: Determine version number
id: tag
run: echo "tag=${GITHUB_REF#refs/tags/}" >> "${GITHUB_OUTPUT}"
outputs:
# Use the input from workflow dispatch, or fall back to the git tag.
version: ${{ inputs.version || steps.tag.outputs.tag }}
# Build and package a release for each platform.
build:
needs:
- get-version
env:
version: ${{ needs.get-version.outputs.version }}
# Avoid rate-limiting. See: <https://github.com/cargo-bins/cargo-binstall/issues/2045>.
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
strategy:
matrix:
include:
- platform: web
targets: wasm32-unknown-unknown
package_ext: .zip
runner: ubuntu-latest
- platform: linux
targets: x86_64-unknown-linux-gnu
package_ext: .zip
runner: ubuntu-latest
- platform: windows
targets: x86_64-pc-windows-msvc
binary_ext: .exe
package_ext: .zip
runner: windows-latest
- platform: macos
targets: x86_64-apple-darwin aarch64-apple-darwin
app_suffix: .app/Contents/MacOS
package_ext: .dmg
runner: macos-latest
runs-on: ${{ matrix.runner }}
permissions:
# Required to create a GitHub release: <https://docs.github.com/en/rest/releases/releases#create-a-release>.
contents: write
defaults:
run:
shell: bash
steps:
- name: Set up environment
run: |
# Default values:
echo "app_binary_name=${app_binary_name:=${{ env.cargo_build_binary_name }}}" >> "${GITHUB_ENV}"
echo "app_package_name=${app_package_name:=${app_binary_name}}" >> "${GITHUB_ENV}"
echo "app_display_name=${app_display_name:=${app_package_name}}" >> "${GITHUB_ENV}"
echo "app_short_name=${app_short_name:=${app_display_name}}" >> "${GITHUB_ENV}"
# File paths:
echo "app=tmp/app/${app_package_name}"'${{ matrix.app_suffix }}' >> "${GITHUB_ENV}"
echo "package=${app_package_name}-"'${{ matrix.platform }}${{ matrix.package_ext }}' >> "${GITHUB_ENV}"
# macOS environment:
if [ '${{ matrix.platform }}' = 'macos' ]; then
echo 'MACOSX_DEPLOYMENT_TARGET=11.0' >> "${GITHUB_ENV}" # macOS 11.0 Big Sur is the first version to support universal binaries.
echo "SDKROOT=$(xcrun --sdk macosx --show-sdk-path)" >> "${GITHUB_ENV}"
fi
# Check if building for this platform is enabled.
echo 'is_platform_enabled=${{
(matrix.platform == 'web' && env.build_for_web == 'true') ||
(matrix.platform == 'linux' && env.build_for_linux == 'true') ||
(matrix.platform == 'windows' && env.build_for_windows == 'true') ||
(matrix.platform == 'macos' && env.build_for_macos == 'true')
}}' >> "${GITHUB_ENV}"
- name: Checkout repository
if: ${{ env.is_platform_enabled == 'true' }}
uses: actions/checkout@v4
with:
lfs: ${{ env.git_lfs }}
- name: Install Rust toolchain
if: ${{ env.is_platform_enabled == 'true' }}
uses: dtolnay/rust-toolchain@nightly
with:
targets: ${{ matrix.targets }}
- name: Restore Rust cache
if: ${{ env.is_platform_enabled == 'true' && env.use_github_cache == 'true' }}
uses: Swatinem/rust-cache@v2
with:
shared-key: release
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install build dependencies (Linux)
if: ${{ env.is_platform_enabled == 'true' && matrix.platform == 'linux' }}
run: sudo apt-get update; sudo apt-get install --no-install-recommends libasound2-dev libudev-dev libwayland-dev libxkbcommon-dev
- name: Prepare output directories
if: ${{ env.is_platform_enabled == 'true' }}
run: rm -rf tmp; mkdir -p tmp/binary '${{ env.app }}'
- name: Install cargo-binstall
if: ${{ env.is_platform_enabled == 'true' }}
uses: cargo-bins/cargo-binstall@main
- name: Install Bevy CLI
if: ${{ env.is_platform_enabled == 'true' }}
run: cargo binstall --locked --no-confirm --force --git='https://github.com/TheBevyFlock/bevy_cli' bevy_cli
- name: Build and add web bundle to app (Web)
if: ${{ env.is_platform_enabled == 'true' && matrix.platform == 'web' }}
run: |
cargo binstall --locked --no-confirm --force wasm-bindgen-cli
cargo binstall --locked --no-confirm --force wasm-opt
bevy build --locked --release --features='${{ matrix.features }}' --yes web --bundle
mv 'target/bevy_web/web-release/${{ env.cargo_build_binary_name }}' '${{ env.app }}'
- name: Build and add binaries to app (non-Web)
if: ${{ env.is_platform_enabled == 'true' && matrix.platform != 'web' }}
run: |
for target in ${{ matrix.targets }}; do
bevy build --locked --release --target="${target}" --features='${{ matrix.features }}'
mv target/"${target}"/release/'${{ env.cargo_build_binary_name }}${{ matrix.binary_ext }}' tmp/binary/"${target}"'${{ matrix.binary_ext }}'
done
if [ '${{ matrix.platform }}' = 'macos' ]; then
lipo tmp/binary/*'${{ matrix.binary_ext }}' -create -output '${{ env.app }}/${{ env.app_binary_name }}${{ matrix.binary_ext }}'
else
mv tmp/binary/*'${{ matrix.binary_ext }}' '${{ env.app }}/${{ env.app_binary_name }}${{ matrix.binary_ext }}'
fi
- name: Add assets to app (non-Web)
if: ${{ env.is_platform_enabled == 'true' && matrix.platform != 'web' }}
run: cp -R ./'${{ env.assets_path }}' '${{ env.app }}' || true # Ignore error if assets folder does not exist.
- name: Add metadata to app (macOS)
if: ${{ env.is_platform_enabled == 'true' && matrix.platform == 'macos' }}
run: |
cat >'${{ env.app }}/../Info.plist' <<EOF
<?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>${{ env.app_display_name }}</string>
<key>CFBundleExecutable</key>
<string>${{ env.app_binary_name }}</string>
<key>CFBundleIdentifier</key>
<string>${{ env.app_id }}</string>
<key>CFBundleName</key>
<string>${{ env.app_short_name }}</string>
<key>CFBundleShortVersionString</key>
<string>${{ env.version }}</string>
<key>CFBundleVersion</key>
<string>${{ env.version }}</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleSupportedPlatforms</key>
<array>
<string>MacOSX</string>
</array>
</dict>
</plist>
EOF
- name: Package app (non-Windows)
if: ${{ env.is_platform_enabled == 'true' && matrix.platform != 'windows' }}
working-directory: tmp/app
run: |
if [ '${{ matrix.platform }}' = 'macos' ]; then
ln -s /Applications .
hdiutil create -fs HFS+ -volname '${{ env.app_package_name }}' -srcfolder . '${{ env.package }}'
else
zip --recurse-paths '${{ env.package }}' '${{ env.app_package_name }}'
fi
- name: Package app (Windows)
if: ${{ env.is_platform_enabled == 'true' && matrix.platform == 'windows' }}
working-directory: tmp/app
shell: pwsh
run: Compress-Archive -Path '${{ env.app_package_name }}' -DestinationPath '${{ env.package }}'
- name: Upload package to workflow artifacts
if: ${{ env.is_platform_enabled == 'true' }}
uses: actions/upload-artifact@v4
with:
path: tmp/app/${{ env.package }}
name: package-${{ matrix.platform }}
retention-days: 1
- name: Upload package to GitHub release
if: ${{ env.is_platform_enabled == 'true' && env.upload_to_github == 'true' }}
uses: svenstaro/upload-release-action@v2
with:
repo_token: ${{ secrets.GITHUB_TOKEN }}
file: tmp/app/${{ env.package }}
asset_name: ${{ env.package }}
release_name: ${{ env.version }}
tag: ${{ env.version }}
overwrite: true
# Upload all packages to itch.io.
upload-to-itch:
runs-on: ubuntu-latest
needs:
- forward-env
- get-version
- build
if: ${{ needs.forward-env.outputs.upload_to_itch != '' }}
steps:
- name: Download all packages
uses: actions/download-artifact@v4
with:
pattern: package-*
path: tmp
- name: Install butler
run: |
curl -L -o butler.zip 'https://broth.itch.zone/butler/linux-amd64/LATEST/archive/default'
unzip butler.zip
chmod +x butler
./butler -V
- name: Upload all packages to itch.io
env:
BUTLER_API_KEY: ${{ secrets.BUTLER_CREDENTIALS }}
run: |
for channel in $(ls tmp); do
./butler push \
--fix-permissions \
--userversion='${{ needs.get-version.outputs.version }}' \
tmp/"${channel}"/* \
'${{ env.upload_to_itch }}':"${channel#package-}"
done
{%- endraw %}

View File

@@ -1,24 +0,0 @@
name: Rust
on:
push:
branches: [ "master" ]
pull_request:
branches: [ "master" ]
env:
CARGO_TERM_COLOR: always
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install deps
run: sudo apt install libasound2-dev libudev-dev
- name: Build
run: cargo build
- name: Run tests
run: cargo test

17
Cargo.lock generated
View File

@@ -445,9 +445,9 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
[[package]]
name = "bevy"
version = "0.18.0"
version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec689b5a79452b6f777b889bbff22d3216b82a8d2ab7814d4a0eb571e9938d97"
checksum = "1fd310426290cec560221f9750c2f4484be4a8eeea7de3483c423329b465c40e"
dependencies = [
"bevy_internal",
"tracing",
@@ -1993,6 +1993,7 @@ dependencies = [
"bevy",
"bevy_asset_loader",
"bevy_rapier3d",
"hex",
"ron 0.12.0",
"serde",
"serde_json",
@@ -3493,6 +3494,14 @@ version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
[[package]]
name = "hex"
version = "0.1.0"
dependencies = [
"bevy",
"serde",
]
[[package]]
name = "hexasphere"
version = "16.0.0"
@@ -4933,6 +4942,7 @@ dependencies = [
"bevy_asset_loader",
"bevy_rapier3d",
"buildings",
"hex",
"image",
"noise",
"rayon",
@@ -5714,6 +5724,7 @@ name = "shared"
version = "0.1.0"
dependencies = [
"bevy",
"hex",
"serde",
"world_generation",
]
@@ -6321,6 +6332,7 @@ dependencies = [
"bevy",
"bevy_asset_loader",
"bevy_rapier3d",
"hex",
"ordered-float",
"pathfinding",
"quadtree_rs",
@@ -7515,6 +7527,7 @@ dependencies = [
"bevy",
"bevy-inspector-egui",
"bevy_asset_loader",
"hex",
"image",
"noise",
"num 0.4.3",

View File

@@ -9,6 +9,7 @@ members = [
"engine/world_generation",
"engine/asset_loader",
"engine/data",
"engine/hex",
]
# Enable a small amount of optimization in debug mode

View File

@@ -1,3 +1,5 @@
# Phos
You play as a robot colonizer that has been sent out to an alien planet with one goal. Harvest its resources and send them back home. To accomplish this you must build a base on the planet. As your base develops you will be able to build more advanced structures to harvest at an increased scale. And eventually you will be able to accomplish your goal of sending resources back to your mother planet. But your invasion wont go unchallenged. The native inhabitants of the planet wont sit idly by as you strip their world of its resources.
![development screenshot](https://aoba.app/m/69bafe2f7c042ee871b94dcf)

8
engine/hex/Cargo.toml Normal file
View File

@@ -0,0 +1,8 @@
[package]
name = "hex"
version = "0.1.0"
edition = "2024"
[dependencies]
bevy = "0.18.1"
serde = { version = "1.0.228", features = ["derive"] }

View File

@@ -1,9 +1,9 @@
use crate::hex_utils::SHORT_DIAGONAL;
use crate::SHORT_DIAGONAL;
use bevy::prelude::*;
#[derive(Clone)]
pub struct Chunk {
pub struct Chunk
{
pub heights: [f32; Chunk::AREA],
pub textures: [[u32; 2]; Chunk::AREA],
// pub biome_data: [BiomeData; Chunk::AREA],
@@ -13,8 +13,10 @@ pub struct Chunk {
pub max_level: f32,
}
impl Default for Chunk {
fn default() -> Self {
impl Default for Chunk
{
fn default() -> Self
{
Self {
heights: [0.; Chunk::AREA],
textures: [[0; 2]; Chunk::AREA],
@@ -27,17 +29,20 @@ impl Default for Chunk {
}
}
impl Chunk {
impl Chunk
{
pub const SIZE: usize = 64;
pub const AREA: usize = Chunk::SIZE * Chunk::SIZE;
pub const WORLD_WIDTH: f32 = Chunk::SIZE as f32 * SHORT_DIAGONAL;
pub const WORLD_HEIGHT: f32 = Chunk::SIZE as f32 * 1.5;
pub const WORLD_SIZE: Vec2 = Vec2::new(Chunk::WORLD_WIDTH, Chunk::WORLD_HEIGHT);
pub fn get_pos_z_edge(&self) -> [f32; Chunk::SIZE] {
pub fn get_pos_z_edge(&self) -> [f32; Chunk::SIZE]
{
let mut data = [0.; Chunk::SIZE];
for x in 0..Chunk::SIZE {
for x in 0..Chunk::SIZE
{
let idx = x + (Chunk::SIZE - 1) * Chunk::SIZE;
data[x] = self.heights[idx];
}
@@ -45,20 +50,24 @@ impl Chunk {
return data;
}
pub fn get_neg_z_edge(&self) -> [f32; Chunk::SIZE] {
pub fn get_neg_z_edge(&self) -> [f32; Chunk::SIZE]
{
let mut data = [0.; Chunk::SIZE];
for x in 0..Chunk::SIZE {
for x in 0..Chunk::SIZE
{
data[x] = self.heights[x];
}
return data;
}
pub fn get_pos_x_edge(&self) -> [f32; Chunk::SIZE] {
pub fn get_pos_x_edge(&self) -> [f32; Chunk::SIZE]
{
let mut data = [0.; Chunk::SIZE];
for z in 0..Chunk::SIZE {
for z in 0..Chunk::SIZE
{
let idx = (Chunk::SIZE - 1) + z * Chunk::SIZE;
data[z] = self.heights[idx];
}
@@ -66,10 +75,12 @@ impl Chunk {
return data;
}
pub fn get_neg_x_edge(&self) -> [f32; Chunk::SIZE] {
pub fn get_neg_x_edge(&self) -> [f32; Chunk::SIZE]
{
let mut data = [0.; Chunk::SIZE];
for z in 0..Chunk::SIZE {
for z in 0..Chunk::SIZE
{
let idx = z * Chunk::SIZE;
data[z] = self.heights[idx];
}

View File

@@ -1,27 +1,25 @@
use std::fmt::Display;
use crate::prelude::Chunk;
use bevy::prelude::*;
use serde::{Deserialize, Serialize};
pub const OUTER_RADIUS: f32 = 1.;
pub const INNER_RADIUS: f32 = OUTER_RADIUS * (SQRT_3 / 2.);
pub const SHORT_DIAGONAL: f32 = 1. * SQRT_3;
pub const LONG_DIAGONAL: f32 = 2. * OUTER_RADIUS;
const SQRT_3: f32 = 1.7320508076;
use crate::{INNER_RADIUS, OUTER_RADIUS, chunk::Chunk};
pub fn offset3d_to_world(offset: Vec3) -> Vec3 {
pub fn offset3d_to_world(offset: Vec3) -> Vec3
{
let x = (offset.x + (offset.z * 0.5) - (offset.z / 2.).floor()) * (INNER_RADIUS * 2.);
return Vec3::new(x, offset.y, offset.z * OUTER_RADIUS * 1.5);
}
pub fn offset_to_world(offset: IVec2, height: f32) -> Vec3 {
pub fn offset_to_world(offset: IVec2, height: f32) -> Vec3
{
let off = offset.as_vec2();
let x = (off.x + (off.y * 0.5) - (off.y / 2.).floor()) * (INNER_RADIUS * 2.);
return Vec3::new(x, height, off.y * OUTER_RADIUS * 1.5);
}
pub fn offset_to_hex(offset: IVec2) -> IVec3 {
pub fn offset_to_hex(offset: IVec2) -> IVec3
{
let mut v = IVec3 {
x: offset.x - (offset.y / 2),
y: offset.y,
@@ -31,15 +29,18 @@ pub fn offset_to_hex(offset: IVec2) -> IVec3 {
return v;
}
pub fn offset_to_index(offset: IVec2, width: usize) -> usize {
pub fn offset_to_index(offset: IVec2, width: usize) -> usize
{
return offset.x as usize + offset.y as usize * width;
}
pub fn snap_to_hex_grid(world_pos: Vec3) -> Vec3 {
pub fn snap_to_hex_grid(world_pos: Vec3) -> Vec3
{
return offset_to_world(world_to_offset_pos(world_pos), world_pos.y);
}
pub fn world_to_offset_pos(world_pos: Vec3) -> IVec2 {
pub fn world_to_offset_pos(world_pos: Vec3) -> IVec2
{
let offset = world_pos.z / (OUTER_RADIUS * 3.);
let x = (world_pos.x / (INNER_RADIUS * 2.)) - offset;
let z = -world_pos.x - offset;
@@ -51,26 +52,32 @@ pub fn world_to_offset_pos(world_pos: Vec3) -> IVec2 {
return IVec2::new(ox, oz);
}
pub fn tile_to_world_distance(dist: u32) -> f32 {
pub fn tile_to_world_distance(dist: u32) -> f32
{
return dist as f32 * (2. * INNER_RADIUS);
}
pub fn get_tile_count_in_range(radius: usize) -> usize {
pub fn get_tile_count_in_range(radius: usize) -> usize
{
return 1 + 3 * (radius + 1) * radius;
}
#[derive(Default, Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize, Hash)]
pub struct HexCoord {
pub struct HexCoord
{
pub hex: IVec3,
}
impl Display for HexCoord {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
impl Display for HexCoord
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result
{
f.write_fmt(format_args!("HexCoord{}", self.hex))
}
}
impl HexCoord {
impl HexCoord
{
pub const DIRECTIONS: [IVec3; 6] = [
IVec3::new(0, 1, -1),
IVec3::new(1, 0, -1),
@@ -82,27 +89,33 @@ impl HexCoord {
pub const ZERO: HexCoord = HexCoord { hex: IVec3::ZERO };
pub fn new(x: i32, z: i32) -> Self {
pub fn new(x: i32, z: i32) -> Self
{
return HexCoord {
hex: IVec3::new(x, z, -x - z),
};
}
pub fn from_hex(hex: IVec2) -> Self {
pub fn from_axial(hex: IVec2) -> Self
{
return HexCoord {
hex: IVec3::new(hex.x, hex.y, -hex.x - hex.y),
};
}
pub fn from_grid_pos(x: usize, z: usize) -> Self {
pub fn from_offset_pos(x: usize, z: usize) -> Self
{
return HexCoord::new(x as i32 - (z as i32 / 2), z as i32);
}
pub fn from_offset(offset_pos: IVec2) -> Self {
pub fn from_offset(offset_pos: IVec2) -> Self
{
return HexCoord {
hex: offset_to_hex(offset_pos),
};
}
pub fn from_world_pos(world_pos: Vec3) -> Self {
pub fn from_world_pos(world_pos: Vec3) -> Self
{
let offset = world_pos.z / (OUTER_RADIUS * 3.);
let mut x = world_pos.x / (INNER_RADIUS * 2.);
let mut z = -x;
@@ -116,26 +129,31 @@ impl HexCoord {
return Self::from_offset(offset_pos);
}
pub fn is_in_bounds(&self, map_height: usize, map_width: usize) -> bool {
pub fn is_in_bounds(&self, map_height: usize, map_width: usize) -> bool
{
let off = self.to_offset();
if off.x < 0 || off.y < 0 {
if off.x < 0 || off.y < 0
{
return false;
}
if off.x >= map_width as i32 || off.y >= map_height as i32 {
if off.x >= map_width as i32 || off.y >= map_height as i32
{
return false;
}
return true;
}
pub fn is_on_chunk_edge(&self) -> bool {
pub fn is_on_chunk_edge(&self) -> bool
{
let offset = self.to_offset().rem_euclid(IVec2::splat(Chunk::SIZE as i32));
let e = (Chunk::SIZE - 1) as i32;
return offset.x == 0 || offset.y == 0 || offset.x == e || offset.y == e;
}
pub fn to_chunk_pos(&self) -> IVec2 {
pub fn to_chunk_pos(&self) -> IVec2
{
let off = self.to_offset();
return IVec2 {
@@ -145,7 +163,8 @@ impl HexCoord {
}
/// Converts this coordinate to it's chunk local equivalent
pub fn to_chunk(&self) -> HexCoord {
pub fn to_chunk(&self) -> HexCoord
{
let c_pos = self.to_chunk_pos();
let off = self.to_offset();
return HexCoord::from_offset(
@@ -157,76 +176,94 @@ impl HexCoord {
);
}
pub fn to_world(&self, height: f32) -> Vec3 {
pub fn to_world(&self, height: f32) -> Vec3
{
return offset_to_world(self.to_offset(), height);
}
pub fn to_offset(&self) -> IVec2 {
pub fn to_offset(&self) -> IVec2
{
return IVec2::new(self.hex.x + (self.hex.y / 2), self.hex.y);
}
/// Convert the current coordiante to an index
pub fn to_index(&self, width: usize) -> usize {
pub fn to_index(&self, width: usize) -> usize
{
return ((self.hex.x + self.hex.y * width as i32) + (self.hex.y / 2)) as usize;
}
/// Gets the index of this coord in the chunk array.
///
/// [`width`] is in number of chunks
pub fn to_chunk_index(&self, width: usize) -> usize {
pub fn to_chunk_index(&self, width: usize) -> usize
{
let pos = self.to_chunk_pos();
return (pos.x + pos.y * width as i32) as usize;
}
/// Gets the index of this tile in the chunk
pub fn to_chunk_local_index(&self) -> usize {
pub fn to_chunk_local_index(&self) -> usize
{
return self.to_chunk().to_index(Chunk::SIZE);
}
pub fn distance(&self, other: &HexCoord) -> i32 {
pub fn distance(&self, other: &HexCoord) -> i32
{
return (self.hex.x - other.hex.x).abs() + (self.hex.y - other.hex.y).abs() + (self.hex.z - other.hex.z).abs();
}
pub fn rotate_around(&self, center: &HexCoord, angle: i32) -> HexCoord {
if self == center || angle == 0 {
pub fn rotate_around(&self, center: &HexCoord, angle: i32) -> HexCoord
{
if self == center || angle == 0
{
return self.clone();
}
let mut a = angle % 6;
let mut pc = self.hex - center.hex;
if a > 0 {
for _ in 0..a {
if a > 0
{
for _ in 0..a
{
pc = Self::slide_right(pc);
}
} else {
}
else
{
a = a.abs();
for _ in 0..a {
for _ in 0..a
{
pc = Self::slide_left(pc);
}
}
return HexCoord::from_hex(pc.xy() + center.hex.xy());
return HexCoord::from_axial(pc.xy() + center.hex.xy());
}
fn slide_left(hex: IVec3) -> IVec3 {
fn slide_left(hex: IVec3) -> IVec3
{
return (hex * -1).yzx();
}
fn slide_right(hex: IVec3) -> IVec3 {
fn slide_right(hex: IVec3) -> IVec3
{
return (hex * -1).zxy();
}
pub fn scale(&self, dir: i32, radius: usize) -> HexCoord {
let s = Self::DIRECTIONS[(dir % 6) as usize] * radius as i32;
return Self::from_hex(self.hex.xy() + s.xy());
pub fn scale(&self, dir: usize, radius: usize) -> HexCoord
{
let s = Self::DIRECTIONS[dir % 6] * radius as i32;
return Self::from_axial(self.hex.xy() + s.xy());
}
pub fn get_neighbor(&self, dir: usize) -> HexCoord {
pub fn get_neighbor(&self, dir: usize) -> HexCoord
{
let d = Self::DIRECTIONS[dir % 6];
return Self::from_hex(self.hex.xy() + d.xy());
return Self::from_axial(self.hex.xy() + d.xy());
}
pub fn get_neighbors(&self) -> [HexCoord; 6] {
pub fn get_neighbors(&self) -> [HexCoord; 6]
{
return [
self.get_neighbor(0),
self.get_neighbor(1),
@@ -237,18 +274,23 @@ impl HexCoord {
];
}
pub fn hex_select(&self, radius: usize, include_center: bool) -> Vec<HexCoord> {
pub fn hex_select(&self, radius: usize, include_center: bool) -> Vec<HexCoord>
{
assert!(radius != 0, "Radius cannot be zero");
let mut result = Vec::with_capacity(get_tile_count_in_range(radius));
if include_center {
if include_center
{
result.push(*self);
}
for k in 0..(radius + 1) {
for k in 0..(radius + 1)
{
let mut p = self.scale(4, k);
for i in 0..6 {
for _j in 0..k {
for i in 0..6
{
for _j in 0..k
{
p = p.get_neighbor(i);
result.push(p);
}
@@ -258,28 +300,30 @@ impl HexCoord {
return result;
}
pub fn hex_select_bounded(
&self,
radius: usize,
include_center: bool,
height: usize,
width: usize,
) -> Vec<HexCoord> {
pub fn hex_select_bounded(&self, radius: usize, include_center: bool, height: usize, width: usize)
-> Vec<HexCoord>
{
assert!(radius != 0, "Radius cannot be zero");
let mut result = Vec::with_capacity(get_tile_count_in_range(radius));
if include_center {
if self.is_in_bounds(height, width) {
if include_center
{
if self.is_in_bounds(height, width)
{
result.push(*self);
}
}
for k in 0..(radius + 1) {
for k in 0..(radius + 1)
{
let mut p = self.scale(4, k);
for i in 0..6 {
for _j in 0..k {
for i in 0..6
{
for _j in 0..k
{
p = p.get_neighbor(i);
if p.is_in_bounds(height, width) {
if p.is_in_bounds(height, width)
{
result.push(p);
}
}
@@ -289,7 +333,8 @@ impl HexCoord {
return result;
}
pub fn select_ring(&self, radius: usize) -> Vec<HexCoord> {
pub fn select_ring(&self, radius: usize) -> Vec<HexCoord>
{
assert!(radius != 0, "Radius cannot be zero");
let mut result = Vec::with_capacity(radius * 6);
@@ -300,8 +345,10 @@ impl HexCoord {
// return result;
// }
for i in 0..6 {
for _j in 0..radius {
for i in 0..6
{
for _j in 0..radius
{
result.push(p);
p = p.get_neighbor(i);
}

17
engine/hex/src/lib.rs Normal file
View File

@@ -0,0 +1,17 @@
mod chunk;
mod hex_coord;
pub mod prelude
{
pub use crate::chunk::*;
pub use crate::hex_coord::*;
pub use crate::*;
}
pub const OUTER_RADIUS: f32 = 1.;
pub const INNER_RADIUS: f32 = OUTER_RADIUS * (SQRT_3 / 2.);
pub const SHORT_DIAGONAL: f32 = 1. * SQRT_3;
pub const LONG_DIAGONAL: f32 = 2. * OUTER_RADIUS;
pub const SQRT_3: f32 = 1.7320508076;
#[cfg(test)]
mod tests;

11
engine/hex/src/tests.rs Normal file
View File

@@ -0,0 +1,11 @@
use super::prelude::*;
#[test]
fn create_coord()
{
let center = HexCoord::from_offset_pos(3, 3);
for dir in 0..6
{
assert_eq!(center.get_neighbor(dir).get_neighbor(dir), center.scale(dir, 2));
}
}

View File

@@ -20,6 +20,7 @@ bevy_asset_loader = { version = "0.25.0", features = [
ron = "0.12.0"
image = "0.25.9"
num = "0.4.3"
hex = { path = "../hex" }
[features]
tracing = ["bevy/trace_tracy"]

View File

@@ -1,6 +1,7 @@
use bevy::{mesh::MeshVertexAttribute, prelude::*, render::render_resource::VertexFormat};
use hex::{INNER_RADIUS, OUTER_RADIUS};
use crate::hex_utils::{INNER_RADIUS, OUTER_RADIUS};
// use crate::hex_utils::{INNER_RADIUS, OUTER_RADIUS};
pub const TEX_MULTI: Vec2 = Vec2::new(1000., 1.);

View File

@@ -1,20 +1,24 @@
use crate::{hex_utils::*, prelude::*};
use crate::prelude::*;
#[cfg(feature = "tracing")]
use bevy::log::*;
use bevy::prelude::*;
use hex::prelude::*;
const CHUNK_TOTAL: usize = Chunk::SIZE * Chunk::SIZE;
pub fn generate_chunk_collider(chunk: &MeshChunkData) -> (Vec<Vec3>, Vec<[u32; 3]>) {
pub fn generate_chunk_collider(chunk: &MeshChunkData) -> (Vec<Vec3>, Vec<[u32; 3]>)
{
#[cfg(feature = "tracing")]
let span = info_span!("generate_chunk_collider").entered();
let vertex_count: usize = CHUNK_TOTAL * 6;
let mut verts = Vec::with_capacity(vertex_count);
let mut indices = Vec::with_capacity(vertex_count);
for z in 0..Chunk::SIZE {
for x in 0..Chunk::SIZE {
for z in 0..Chunk::SIZE
{
for x in 0..Chunk::SIZE
{
let height = chunk.heights[x + z * Chunk::SIZE];
let coord = HexCoord::from_grid_pos(x, z);
let coord = HexCoord::from_offset_pos(x, z);
let neighbors = chunk.get_neighbors(&coord);
let off_pos = Vec3::new(x as f32, height, z as f32);
let tile_pos = offset3d_to_world(off_pos);
@@ -24,9 +28,11 @@ pub fn generate_chunk_collider(chunk: &MeshChunkData) -> (Vec<Vec3>, Vec<[u32; 3
return (verts, indices);
}
fn create_tile_collider(pos: Vec3, verts: &mut Vec<Vec3>, indices: &mut Vec<[u32; 3]>, neighbors: &[f32; 6]) {
fn create_tile_collider(pos: Vec3, verts: &mut Vec<Vec3>, indices: &mut Vec<[u32; 3]>, neighbors: &[f32; 6])
{
let idx = verts.len() as u32;
for i in 0..6 {
for i in 0..6
{
let p = pos + HEX_CORNERS[i];
verts.push(p);
}
@@ -37,9 +43,11 @@ fn create_tile_collider(pos: Vec3, verts: &mut Vec<Vec3>, indices: &mut Vec<[u32
indices.push([idx + 2, idx + 4, idx + 5]);
indices.push([idx + 2, idx + 3, idx + 4]);
for i in 0..neighbors.len() {
for i in 0..neighbors.len()
{
let n_height = neighbors[i];
if n_height < pos.y {
if n_height < pos.y
{
create_tile_wall_collider(
idx,
Vec3::new(pos.x, n_height.min(pos.y - OUTER_RADIUS / 2.), pos.z),
@@ -51,7 +59,8 @@ fn create_tile_collider(pos: Vec3, verts: &mut Vec<Vec3>, indices: &mut Vec<[u32
}
}
fn create_tile_wall_collider(idx: u32, pos: Vec3, dir: usize, verts: &mut Vec<Vec3>, indices: &mut Vec<[u32; 3]>) {
fn create_tile_wall_collider(idx: u32, pos: Vec3, dir: usize, verts: &mut Vec<Vec3>, indices: &mut Vec<[u32; 3]>)
{
let idx2 = verts.len() as u32;
verts.push(pos + HEX_CORNERS[(dir) % 6]);

View File

@@ -1,12 +1,13 @@
use crate::hex_utils::HexCoord;
use crate::{hex_utils::offset3d_to_world, prelude::*};
use crate::prelude::*;
use bevy::asset::RenderAssetUsages;
#[cfg(feature = "tracing")]
use bevy::log::*;
use bevy::mesh::{Indices, PrimitiveTopology};
use bevy::prelude::*;
use hex::prelude::*;
pub fn generate_chunk_mesh(chunk: &MeshChunkData) -> Mesh {
pub fn generate_chunk_mesh(chunk: &MeshChunkData) -> Mesh
{
#[cfg(feature = "tracing")]
let span = info_span!("generate_chunk_mesh").entered();
@@ -16,13 +17,15 @@ pub fn generate_chunk_mesh(chunk: &MeshChunkData) -> Mesh {
let mut indices = Vec::with_capacity(vertex_count);
let mut normals = Vec::with_capacity(vertex_count);
for z in 0..Chunk::SIZE {
for x in 0..Chunk::SIZE {
for z in 0..Chunk::SIZE
{
for x in 0..Chunk::SIZE
{
let idx = x + z * Chunk::SIZE;
let height = chunk.heights[idx];
let off_pos = Vec3::new(x as f32, height, z as f32);
let tile_pos = offset3d_to_world(off_pos);
let coord = HexCoord::from_grid_pos(x, z);
let coord = HexCoord::from_offset_pos(x, z);
let n = chunk.get_neighbors(&coord);
create_tile(
@@ -59,20 +62,23 @@ fn create_tile(
normals: &mut Vec<Vec3>,
texture_index: u32,
side_texture_index: u32,
) {
)
{
let uv_offset = Vec2::splat(0.5);
let tex_off = Vec2::new(texture_index as f32, 0.);
let side_tex_off = Vec2::new(side_texture_index as f32, 0.);
let idx = verts.len() as u32;
for i in 0..6 {
for i in 0..6
{
let p = pos + HEX_CORNERS[i];
verts.push(p);
let uv = (HEX_CORNERS[i].xz() / 2.) + uv_offset;
uvs.push((uv / TEX_MULTI) + tex_off);
normals.push(Vec3::Y);
}
for i in 0..3 {
for i in 0..3
{
let off = i * 2;
indices.push(off + idx);
indices.push(((off + 1) % 6) + idx);
@@ -82,15 +88,19 @@ fn create_tile(
indices.push(idx + 2);
indices.push(idx + 4);
for i in 0..neighbors.len() {
for i in 0..neighbors.len()
{
let n_height = neighbors[i];
if n_height < pos.y {
if n_height < pos.y
{
create_tile_wall(pos, i, n_height, verts, uvs, indices, normals, side_tex_off);
}
}
}
pub fn generate_chunk_water_mesh(chunk: &MeshChunkData, sealevel: f32, map_width: usize, map_height: usize) -> Mesh {
#[allow(unused)]
pub fn generate_chunk_water_mesh(chunk: &MeshChunkData, sealevel: f32, map_width: usize, map_height: usize) -> Mesh
{
#[cfg(feature = "tracing")]
let _gen_mesh = info_span!("Generate Water Surface Mesh").entered();
let vertex_count: usize = Chunk::SIZE * Chunk::SIZE * 7;
@@ -99,16 +109,19 @@ pub fn generate_chunk_water_mesh(chunk: &MeshChunkData, sealevel: f32, map_width
let mut indices = Vec::with_capacity(vertex_count);
let mut normals = Vec::with_capacity(vertex_count);
for z in 0..Chunk::SIZE {
for x in 0..Chunk::SIZE {
for z in 0..Chunk::SIZE
{
for x in 0..Chunk::SIZE
{
let idx = x + z * Chunk::SIZE;
let height = chunk.heights[idx];
if height > sealevel {
if height > sealevel
{
continue;
}
let off_pos = Vec3::new(x as f32, sealevel, z as f32);
let tile_pos = offset3d_to_world(off_pos);
let coord = HexCoord::from_grid_pos(x, z);
let coord = HexCoord::from_offset_pos(x, z);
let (n, neighbor_has_land) = chunk.get_neighbors_with_water_info(&coord);
create_tile_water_surface(
@@ -143,8 +156,10 @@ fn create_tile_water_surface(
uvs: &mut Vec<Vec2>,
indices: &mut Vec<u32>,
normals: &mut Vec<Vec3>,
) {
if !neighbor_has_land {
)
{
if !neighbor_has_land
{
crate_tile_water_inner_surface(pos, dist_to_land, neighbors, verts, uvs, indices, normals);
return;
}
@@ -159,23 +174,29 @@ fn crate_tile_water_inner_surface(
uvs: &mut Vec<Vec2>,
indices: &mut Vec<u32>,
normals: &mut Vec<Vec3>,
) {
)
{
//todo: share verts
let idx = verts.len() as u32;
for i in 0..6 {
for i in 0..6
{
let p = pos + HEX_CORNERS[i];
verts.push(p);
let n1 = if let Some(v) = neighbors[i].1 { v } else { dist_to_land };
let n2 = if let Some(v) = neighbors[(i + 5) % 6].1 {
let n2 = if let Some(v) = neighbors[(i + 5) % 6].1
{
v
} else {
}
else
{
dist_to_land
};
let d = (n1 + n2 + dist_to_land) / 3.0;
uvs.push(Vec2::new(0.0, d.remap(0., 4., 1.0, 0.0)));
normals.push(Vec3::Y);
}
for i in 0..3 {
for i in 0..3
{
let off = i * 2;
indices.push(off + idx);
indices.push(((off + 1) % 6) + idx);
@@ -194,13 +215,15 @@ fn crate_tile_water_shore_surface(
uvs: &mut Vec<Vec2>,
indices: &mut Vec<u32>,
normals: &mut Vec<Vec3>,
) {
)
{
let idx = verts.len() as u32;
//todo: only use triangle fan when on shoreline
verts.push(pos);
uvs.push(Vec2::new(0.0, dist_to_land.remap(0., 4., 1.0, 0.0)));
normals.push(Vec3::Y);
for i in 0..12 {
for i in 0..12
{
let p = pos + WATER_HEX_CORNERS[i];
verts.push(p);
let ni = i / 2;
@@ -208,16 +231,21 @@ fn crate_tile_water_shore_surface(
let nn = neighbors[(ni + 5) % 6];
let mut uv = Vec2::new(0.0, dist_to_land.remap(0., 4., 1.0, 0.0));
if nn.0 > pos.y || n.0 > pos.y {
if nn.0 > pos.y || n.0 > pos.y
{
uv.x = 1.0;
}
if ni * 2 != i {
if n.0 <= pos.y {
if ni * 2 != i
{
if n.0 <= pos.y
{
uv.x = 0.0;
}
let d = if let Some(v) = n.1 { v } else { dist_to_land };
uv.y = ((d + dist_to_land) / 2.0).remap(0., 4., 1.0, 0.0);
} else {
}
else
{
let d = if let Some(v) = n.1 { v } else { dist_to_land };
let d2 = if let Some(v) = nn.1 { v } else { dist_to_land };
uv.y = ((d + d2 + dist_to_land) / 3.0).remap(0., 4., 1.0, 0.0);
@@ -241,7 +269,8 @@ fn create_tile_wall(
indices: &mut Vec<u32>,
normals: &mut Vec<Vec3>,
tex_off: Vec2,
) {
)
{
let p1 = HEX_CORNERS[(dir) % 6] + pos;
let p2 = HEX_CORNERS[(dir + 1) % 6] + pos;
let p3 = Vec3::new(p1.x, height, p1.z);
@@ -275,12 +304,14 @@ fn create_tile_wall(
}
#[cfg(test)]
mod tests {
mod tests
{
use super::*;
#[test]
fn generate_tile_wall() {
fn generate_tile_wall()
{
let mut verts = Vec::new();
let mut uvs = Vec::new();
let mut normals = Vec::new();
@@ -307,7 +338,8 @@ mod tests {
}
#[test]
fn generate_tile() {
fn generate_tile()
{
let mut verts = Vec::new();
let mut uvs = Vec::new();
let mut normals = Vec::new();

View File

@@ -1,22 +1,25 @@
use crate::hex_utils::HexCoord;
use crate::prelude::*;
use bevy::asset::RenderAssetUsages;
use bevy::{
mesh::{Indices, PrimitiveTopology},
prelude::*,
};
use hex::prelude::*;
pub fn generate_packed_chunk_mesh(chunk: &MeshChunkData) -> Mesh {
pub fn generate_packed_chunk_mesh(chunk: &MeshChunkData) -> Mesh
{
let vertex_count: usize = Chunk::SIZE * Chunk::SIZE * 6;
let mut packed_data = Vec::with_capacity(vertex_count);
let mut indices = Vec::with_capacity(vertex_count);
let mut heights = Vec::with_capacity(vertex_count);
for z in 0..Chunk::SIZE {
for x in 0..Chunk::SIZE {
for z in 0..Chunk::SIZE
{
for x in 0..Chunk::SIZE
{
let idx = x + z * Chunk::SIZE;
let height = chunk.heights[idx];
let coord = HexCoord::from_grid_pos(x, z);
let coord = HexCoord::from_offset_pos(x, z);
let n = chunk.get_neighbors(&coord);
create_packed_tile(
@@ -51,12 +54,14 @@ fn create_packed_tile(
heights: &mut Vec<f32>,
texture_index: u32,
side_texture_index: u32,
) {
)
{
let idx = packed_data.len() as u32;
packed_data.push(pack_vertex_data(offset, 0, texture_index));
heights.push(height);
for i in 0..6 {
for i in 0..6
{
packed_data.push(pack_vertex_data(offset, i + 1, texture_index));
indices.push(idx);
indices.push(idx + 1 + i as u32);
@@ -64,9 +69,11 @@ fn create_packed_tile(
heights.push(height);
}
for i in 0..neighbors.len() {
for i in 0..neighbors.len()
{
let n_height = neighbors[i];
if n_height < height {
if n_height < height
{
create_packed_tile_wall(
offset,
height,
@@ -90,7 +97,8 @@ fn create_packed_tile_wall(
indices: &mut Vec<u32>,
heights: &mut Vec<f32>,
side_texture_index: u32,
) {
)
{
let idx = packed_data.len() as u32;
let side_2 = ((side + 1) % 6) + 1;
@@ -113,7 +121,8 @@ fn create_packed_tile_wall(
indices.push(idx + 3);
}
fn pack_vertex_data(offset: UVec2, vert: usize, tex: u32) -> u32 {
fn pack_vertex_data(offset: UVec2, vert: usize, tex: u32) -> u32
{
//6 + 6 bits offset
//4 bits vert
//12 bits texture

View File

@@ -3,6 +3,7 @@ use core::f32;
use bevy::math::{IVec2, UVec2};
use bevy::prelude::{FloatExt, Vec2};
use bevy::utils::default;
use hex::prelude::*;
use noise::{NoiseFn, Simplex, SuperSimplex};
use rayon::iter::{IntoParallelIterator, ParallelIterator};
@@ -10,7 +11,8 @@ use crate::biome_painter::BiomePainter;
use crate::map::biome_map::{BiomeChunk, BiomeData, BiomeMap};
use crate::prelude::*;
pub fn generate_heightmap(cfg: &GenerationConfig, seed: u32, painter: &BiomePainter) -> (Map, BiomeMap) {
pub fn generate_heightmap(cfg: &GenerationConfig, seed: u32, painter: &BiomePainter) -> (Map, BiomeMap)
{
let biomes = generate_biomes(cfg, seed, painter);
let biomes_borrow = &biomes;
// let mut chunks: Vec<Chunk> = Vec::with_capacity(cfg.size.length_squared() as usize);
@@ -25,11 +27,14 @@ pub fn generate_heightmap(cfg: &GenerationConfig, seed: u32, painter: &BiomePain
.collect();
let mut min = f32::MAX;
let mut max = f32::MIN;
for chunk in &chunks {
if chunk.min_level < min {
for chunk in &chunks
{
if chunk.min_level < min
{
min = chunk.min_level;
}
if chunk.max_level > max {
if chunk.max_level > max
{
max = chunk.max_level;
}
}
@@ -48,7 +53,8 @@ pub fn generate_heightmap(cfg: &GenerationConfig, seed: u32, painter: &BiomePain
);
}
pub fn generate_biomes(cfg: &GenerationConfig, seed: u32, biome_painter: &BiomePainter) -> BiomeMap {
pub fn generate_biomes(cfg: &GenerationConfig, seed: u32, biome_painter: &BiomePainter) -> BiomeMap
{
let mut map = BiomeMap::new(cfg.size, biome_painter.biomes.len());
map.chunks = (0..cfg.size.y)
.into_par_iter()
@@ -68,7 +74,8 @@ pub fn generate_biome_chunk(
cfg: &GenerationConfig,
seed: u32,
biome_painter: &BiomePainter,
) -> BiomeChunk {
) -> BiomeChunk
{
let mut chunk = BiomeChunk {
offset: UVec2::new(chunk_x as u32, chunk_y as u32),
data: [BiomeData::default(); Chunk::AREA],
@@ -78,8 +85,10 @@ pub fn generate_biome_chunk(
let noise_t = Simplex::new(seed + 2);
let noise_c = Simplex::new(seed + 3);
for z in 0..Chunk::SIZE {
for x in 0..Chunk::SIZE {
for z in 0..Chunk::SIZE
{
for x in 0..Chunk::SIZE
{
let moisture = sample_point(
x as f64 + chunk_x as f64 * Chunk::SIZE as f64,
z as f64 + chunk_y as f64 * Chunk::SIZE as f64,
@@ -123,14 +132,16 @@ pub fn generate_biome_chunk(
return chunk;
}
pub fn generate_noise_map(size: UVec2, seed: u32, cfg: &NoiseConfig, border_size: f32) -> Vec<f32> {
pub fn generate_noise_map(size: UVec2, seed: u32, cfg: &NoiseConfig, border_size: f32) -> Vec<f32>
{
let noise = SuperSimplex::new(seed);
let data: Vec<_> = (0..(size.y as usize * Chunk::SIZE))
.into_par_iter()
.flat_map(|y| {
let mut row = Vec::with_capacity(size.x as usize * Chunk::SIZE);
for x in 0..row.capacity() {
for x in 0..row.capacity()
{
row.push(sample_point(
x as f64,
y as f64,
@@ -154,21 +165,26 @@ pub fn generate_chunk(
seed: u32,
biome_chunk: &BiomeChunk,
biome_painter: &BiomePainter,
) -> Chunk {
) -> Chunk
{
let mut result: [f32; Chunk::SIZE * Chunk::SIZE] = [0.; Chunk::AREA];
let mut data = [BiomeData::default(); Chunk::AREA];
let mut biome_ids = [0; Chunk::AREA];
let noise = Simplex::new(seed);
let mut min = f32::MAX;
let mut max = f32::MIN;
for z in 0..Chunk::SIZE {
for x in 0..Chunk::SIZE {
for z in 0..Chunk::SIZE
{
for x in 0..Chunk::SIZE
{
let biome_data = biome_chunk.get_biome_data(x, z);
let biome_blend = biome_chunk.get_biome(x, z);
let mut sample = 0.;
for i in 0..biome_blend.len() {
for i in 0..biome_blend.len()
{
let blend = biome_blend[i];
if blend == 0. {
if blend == 0.
{
continue;
}
let biome = &biome_painter.biomes[i];
@@ -185,10 +201,12 @@ pub fn generate_chunk(
let idx = x + z * Chunk::SIZE;
biome_ids[idx] = biome_chunk.get_biome_id_dithered(x, z, &noise, cfg.biome_dither);
result[idx] = sample;
if sample > max {
if sample > max
{
max = sample;
}
if sample < min {
if sample < min
{
min = sample;
}
data[idx] = biome_data.clone();
@@ -212,23 +230,29 @@ fn sample_point(
size: Vec2,
border_size: f32,
border_value: f32,
) -> f32 {
) -> f32
{
let x_s = x / cfg.scale;
let z_s = z / cfg.scale;
let mut elevation: f64 = 0.;
for i in 0..cfg.layers.len() {
for i in 0..cfg.layers.len()
{
let value: f64;
let layer = &cfg.layers[i];
if layer.is_rigid {
if layer.is_rigid
{
value = sample_rigid(x_s, z_s, layer, noise);
} else {
}
else
{
value = sample_simple(x_s, z_s, layer, noise);
}
elevation += value;
}
if border_size == 0.0 {
if border_size == 0.0
{
return elevation as f32;
}
@@ -243,12 +267,14 @@ fn sample_point(
return border_value.lerp(elevation as f32, d);
}
fn sample_simple(x: f64, z: f64, cfg: &GeneratorLayer, noise: &impl NoiseFn<f64, 2>) -> f64 {
fn sample_simple(x: f64, z: f64, cfg: &GeneratorLayer, noise: &impl NoiseFn<f64, 2>) -> f64
{
let mut freq: f64 = cfg.base_roughness;
let mut amp: f64 = 1.;
let mut value = 0.;
for _ in 0..cfg.layers {
for _ in 0..cfg.layers
{
let v = noise.get([x * freq, z * freq]);
value += (v + 1.) * 0.5 * amp;
freq *= cfg.roughness;
@@ -257,12 +283,14 @@ fn sample_simple(x: f64, z: f64, cfg: &GeneratorLayer, noise: &impl NoiseFn<f64,
value -= cfg.min_value;
return value * cfg.strength;
}
fn sample_rigid(x: f64, z: f64, cfg: &GeneratorLayer, noise: &impl NoiseFn<f64, 2>) -> f64 {
fn sample_rigid(x: f64, z: f64, cfg: &GeneratorLayer, noise: &impl NoiseFn<f64, 2>) -> f64
{
let mut freq: f64 = cfg.base_roughness;
let mut amp: f64 = 1.;
let mut value = 0.;
let mut weight = 1.;
for _ in 0..cfg.layers {
for _ in 0..cfg.layers
{
let mut v = 1. - noise.get([x * freq, z * freq]).abs();
v *= v;
v *= weight;

View File

@@ -1,11 +1,10 @@
pub mod biome_asset;
pub mod biome_painter;
pub mod consts;
pub mod generators;
pub mod heightmap;
pub mod hex_utils;
pub mod map;
pub mod prelude;
pub mod states;
pub mod tile_manager;
pub mod tile_mapper;
pub mod biome_asset;

View File

@@ -5,10 +5,11 @@ use bevy::{
use noise::NoiseFn;
use rayon::iter::{IntoParallelIterator, ParallelIterator};
use super::chunk::Chunk;
use hex::prelude::*;
#[derive(Clone, Resource)]
pub struct BiomeMap {
pub struct BiomeMap
{
pub height: usize,
pub width: usize,
pub size: UVec2,
@@ -17,26 +18,33 @@ pub struct BiomeMap {
}
#[derive(Default, Clone, Copy)]
pub struct BiomeData {
pub struct BiomeData
{
pub moisture: f32,
pub temperature: f32,
pub continentality: f32,
}
impl Into<Vec3> for &BiomeData {
fn into(self) -> Vec3 {
impl Into<Vec3> for &BiomeData
{
fn into(self) -> Vec3
{
return Vec3::new(self.moisture, self.temperature, self.continentality);
}
}
impl Into<Vec3> for BiomeData {
fn into(self) -> Vec3 {
impl Into<Vec3> for BiomeData
{
fn into(self) -> Vec3
{
return Vec3::new(self.moisture, self.temperature, self.continentality);
}
}
impl BiomeMap {
pub fn new(size: UVec2, biome_count: usize) -> Self {
impl BiomeMap
{
pub fn new(size: UVec2, biome_count: usize) -> Self
{
let len = size.x as usize * size.y as usize * Chunk::AREA;
return BiomeMap {
size,
@@ -47,14 +55,17 @@ impl BiomeMap {
};
}
pub fn blend(&mut self, count: usize) {
pub fn blend(&mut self, count: usize)
{
assert!(count != 0, "Count cannot be 0");
for _ in 0..count {
for _ in 0..count
{
self.blend_once();
}
}
fn blend_once(&mut self) {
fn blend_once(&mut self)
{
let c: Vec<BiomeChunk> = (0..self.chunks.len())
.into_par_iter()
.map(|i| &self.chunks[i])
@@ -63,7 +74,8 @@ impl BiomeMap {
.into_par_iter()
.map(|y| {
let mut new_tiles = Vec::with_capacity(self.width);
for x in 0..Chunk::SIZE {
for x in 0..Chunk::SIZE
{
let tx = x as u32 + chunk.offset.x * Chunk::SIZE as u32;
let ty = y as u32 + chunk.offset.y * Chunk::SIZE as u32;
let kernel = self.get_kernel(tx as i32, ty as i32);
@@ -76,7 +88,8 @@ impl BiomeMap {
});
let sum: f32 = r.iter().sum();
if sum == 0. {
if sum == 0.
{
new_tiles.push(vec![0.; self.biome_count]);
continue;
}
@@ -96,7 +109,8 @@ impl BiomeMap {
self.chunks = c;
}
fn get_kernel(&self, x: i32, y: i32) -> [Option<&Vec<f32>>; 9] {
fn get_kernel(&self, x: i32, y: i32) -> [Option<&Vec<f32>>; 9]
{
return [
self.get_biome(x - 1, y - 1),
self.get_biome(x, y - 1),
@@ -110,11 +124,14 @@ impl BiomeMap {
];
}
pub fn get_biome(&self, x: i32, y: i32) -> Option<&Vec<f32>> {
if x < 0 || y < 0 {
pub fn get_biome(&self, x: i32, y: i32) -> Option<&Vec<f32>>
{
if x < 0 || y < 0
{
return None;
}
if x >= self.width as i32 || y >= self.height as i32 {
if x >= self.width as i32 || y >= self.height as i32
{
return None;
}
@@ -125,7 +142,8 @@ impl BiomeMap {
return Some(chunk.get_biome(x as usize - cx * Chunk::SIZE, y as usize - cy * Chunk::SIZE));
}
pub fn get_biome_id(&self, x: usize, y: usize) -> usize {
pub fn get_biome_id(&self, x: usize, y: usize) -> usize
{
let cx = (x as f32 / Chunk::SIZE as f32).floor() as usize;
let cy = (y as f32 / Chunk::SIZE as f32).floor() as usize;
@@ -134,7 +152,8 @@ impl BiomeMap {
return chunk.get_biome_id(x - (cx * Chunk::SIZE), y - (cy * Chunk::SIZE));
}
pub fn get_biome_id_dithered(&self, x: usize, y: usize, noise: &impl NoiseFn<f64, 2>, scale: f64) -> usize {
pub fn get_biome_id_dithered(&self, x: usize, y: usize, noise: &impl NoiseFn<f64, 2>, scale: f64) -> usize
{
let cx = (x as f32 / Chunk::SIZE as f32).floor() as usize;
let cy = (y as f32 / Chunk::SIZE as f32).floor() as usize;
@@ -143,7 +162,8 @@ impl BiomeMap {
return chunk.get_biome_id_dithered(x - (cx * Chunk::SIZE), y - (cy * Chunk::SIZE), noise, scale);
}
pub fn get_biome_data(&self, x: usize, y: usize) -> &BiomeData {
pub fn get_biome_data(&self, x: usize, y: usize) -> &BiomeData
{
let cx = (x as f32 / Chunk::SIZE as f32).floor() as usize;
let cy = (y as f32 / Chunk::SIZE as f32).floor() as usize;
@@ -154,28 +174,35 @@ impl BiomeMap {
}
#[derive(Clone)]
pub struct BiomeChunk {
pub struct BiomeChunk
{
pub tiles: Vec<Vec<f32>>,
pub offset: UVec2,
pub data: [BiomeData; Chunk::AREA],
}
impl BiomeChunk {
pub fn get_biome(&self, x: usize, y: usize) -> &Vec<f32> {
impl BiomeChunk
{
pub fn get_biome(&self, x: usize, y: usize) -> &Vec<f32>
{
return &self.tiles[x + y * Chunk::SIZE];
}
pub fn get_biome_data(&self, x: usize, y: usize) -> &BiomeData {
pub fn get_biome_data(&self, x: usize, y: usize) -> &BiomeData
{
return &self.data[x + y * Chunk::SIZE];
}
pub fn get_biome_id(&self, x: usize, y: usize) -> usize {
pub fn get_biome_id(&self, x: usize, y: usize) -> usize
{
let b = self.get_biome(x, y);
let mut max = 0.;
let mut idx = 0;
for i in 0..b.len() {
for i in 0..b.len()
{
let blend = b[i];
if blend > max {
if blend > max
{
max = blend;
idx = i;
}
@@ -183,17 +210,21 @@ impl BiomeChunk {
return idx;
}
pub fn get_biome_id_dithered(&self, x: usize, y: usize, noise: &impl NoiseFn<f64, 2>, scale: f64) -> usize {
pub fn get_biome_id_dithered(&self, x: usize, y: usize, noise: &impl NoiseFn<f64, 2>, scale: f64) -> usize
{
let mut cur_id = self.get_biome_id(x, y);
let b = self.get_biome(x, y);
let n = (noise.get([x as f64 / scale, y as f64 / scale]) as f32 - 0.5)/ 2.0;
let n = (noise.get([x as f64 / scale, y as f64 / scale]) as f32 - 0.5) / 2.0;
let mut max = b[cur_id] + n;
for i in 0..b.len() {
for i in 0..b.len()
{
let blend = b[i];
if blend == 0. {
if blend == 0.
{
continue;
}
if blend > max {
if blend > max
{
max = blend + n;
cur_id = i;
}
@@ -204,18 +235,22 @@ impl BiomeChunk {
}
#[cfg(test)]
mod tests {
mod tests
{
use super::*;
#[test]
fn biome_blend() {
fn biome_blend()
{
let mut biome = BiomeMap::new(UVec2::splat(4), 8);
let w = biome.size.x as usize;
let h = biome.size.y as usize;
for y in 0..h {
for x in 0..w {
for y in 0..h
{
for x in 0..w
{
let mut b = vec![0.; biome.biome_count];
let idx = (x + y) % biome.biome_count;
b[idx] = 1.;
@@ -227,7 +262,8 @@ mod tests {
assert!(biome.chunks.iter().all(|f| f.tiles.len() == Chunk::AREA), "Data Lost");
}
fn generate_chunk(x: usize, y: usize, biome: Vec<f32>) -> BiomeChunk {
fn generate_chunk(x: usize, y: usize, biome: Vec<f32>) -> BiomeChunk
{
let chunk = BiomeChunk {
offset: UVec2::new(x as u32, y as u32),
data: [BiomeData::default(); Chunk::AREA],

View File

@@ -2,11 +2,12 @@ use bevy::prelude::*;
use bevy_inspector_egui::InspectorOptions;
use serde::{Deserialize, Serialize};
use super::chunk::Chunk;
use hex::prelude::*;
#[derive(Resource, Reflect, Default, Clone)]
#[reflect(Resource)]
pub struct GenerationConfig {
pub struct GenerationConfig
{
pub sea_level: f64,
pub border_size: f32,
pub biome_blend: usize,
@@ -17,23 +18,28 @@ pub struct GenerationConfig {
pub size: UVec2,
}
impl GenerationConfig {
pub fn get_total_width(&self) -> usize {
impl GenerationConfig
{
pub fn get_total_width(&self) -> usize
{
return self.size.x as usize * Chunk::SIZE;
}
pub fn get_total_height(&self) -> usize {
pub fn get_total_height(&self) -> usize
{
return self.size.y as usize * Chunk::SIZE;
}
}
#[derive(Serialize, Deserialize, Default, Reflect, Clone, Debug)]
pub struct NoiseConfig {
pub struct NoiseConfig
{
pub scale: f64,
pub layers: Vec<GeneratorLayer>,
}
#[derive(Reflect, InspectorOptions, Serialize, Deserialize, Debug, Clone, Default)]
pub struct GeneratorLayer {
pub struct GeneratorLayer
{
pub strength: f64,
pub min_value: f64,
pub base_roughness: f64,

View File

@@ -1,14 +1,11 @@
use bevy::prelude::*;
use hex::prelude::*;
use crate::hex_utils::*;
use super::{
chunk::Chunk,
mesh_chunk::MeshChunkData,
};
use super::mesh_chunk::MeshChunkData;
#[derive(Resource, Clone)]
pub struct Map {
pub struct Map
{
pub chunks: Vec<Chunk>,
pub height: usize,
pub width: usize,
@@ -18,20 +15,25 @@ pub struct Map {
pub biome_count: usize,
}
impl Map {
pub fn get_tile_count(&self) -> usize {
impl Map
{
pub fn get_tile_count(&self) -> usize
{
return self.get_tile_width() * self.get_tile_height();
}
pub fn get_tile_width(&self) -> usize {
pub fn get_tile_width(&self) -> usize
{
return self.width * Chunk::SIZE;
}
pub fn get_tile_height(&self) -> usize {
pub fn get_tile_height(&self) -> usize
{
return self.height * Chunk::SIZE;
}
pub fn get_chunk_mesh_data(&self, chunk_index: usize) -> MeshChunkData {
pub fn get_chunk_mesh_data(&self, chunk_index: usize) -> MeshChunkData
{
#[cfg(feature = "tracing")]
let _spawn_span = info_span!("Chunk Mesh Data").entered();
let chunk = &self.chunks[chunk_index];
@@ -45,36 +47,45 @@ impl Map {
};
}
fn get_distance_from_land(&self, chunk_offset: IVec2, range: usize) -> [f32; Chunk::AREA] {
fn get_distance_from_land(&self, chunk_offset: IVec2, range: usize) -> [f32; Chunk::AREA]
{
#[cfg(feature = "tracing")]
let _spawn_span = info_span!("Chunk Land Dist Data").entered();
let mut dists = [0.0; Chunk::AREA];
let cx = chunk_offset.x as usize * Chunk::SIZE;
let cz = chunk_offset.y as usize * Chunk::SIZE;
for z in 0..Chunk::SIZE {
for x in 0..Chunk::SIZE {
let coord = HexCoord::from_grid_pos(x + cx, z + cz);
for z in 0..Chunk::SIZE
{
for x in 0..Chunk::SIZE
{
let coord = HexCoord::from_offset_pos(x + cx, z + cz);
let index = coord.to_chunk_local_index();
if !self.is_in_bounds(&coord) {
if !self.is_in_bounds(&coord)
{
warn!("Coord is not in bounds!?");
}
//Current tile is land tile
if self.sample_height(&coord) > self.sealevel {
if self.sample_height(&coord) > self.sealevel
{
dists[index] = 0.0;
continue;
}
//Find closest land tile
if let Some(d) = self.hex_select_first(&coord, range, false, |_t, h, r| {
if h > self.sealevel {
if h > self.sealevel
{
return Some(r as f32);
}
return None;
}) {
})
{
dists[index] = d;
} else {
}
else
{
dists[index] = range as f32;
}
}
@@ -82,14 +93,17 @@ impl Map {
return dists;
}
pub fn get_neighbors(&self, pos: &HexCoord) -> [Option<f32>; 6] {
pub fn get_neighbors(&self, pos: &HexCoord) -> [Option<f32>; 6]
{
let mut results: [Option<f32>; 6] = [None; 6];
let w = self.width * Chunk::SIZE;
let h = self.height * Chunk::SIZE;
let n_tiles = pos.get_neighbors();
for i in 0..6 {
for i in 0..6
{
let n_tile = n_tiles[i];
if !n_tile.is_in_bounds(h, w) {
if !n_tile.is_in_bounds(h, w)
{
continue;
}
let c_idx = n_tile.to_chunk_index(self.width);
@@ -100,7 +114,8 @@ impl Map {
return results;
}
pub fn sample_height(&self, pos: &HexCoord) -> f32 {
pub fn sample_height(&self, pos: &HexCoord) -> f32
{
assert!(
self.is_in_bounds(pos),
"The provided coordinate is not within the map bounds"
@@ -110,7 +125,8 @@ impl Map {
return chunk.heights[pos.to_chunk_local_index()];
}
pub fn sample_height_mut(&mut self, pos: &HexCoord) -> &mut f32 {
pub fn sample_height_mut(&mut self, pos: &HexCoord) -> &mut f32
{
assert!(
self.is_in_bounds(pos),
"The provided coordinate is not within the map bounds"
@@ -120,11 +136,13 @@ impl Map {
return &mut chunk.heights[pos.to_chunk_local_index()];
}
pub fn is_in_bounds(&self, pos: &HexCoord) -> bool {
pub fn is_in_bounds(&self, pos: &HexCoord) -> bool
{
return pos.is_in_bounds(self.height * Chunk::SIZE, self.width * Chunk::SIZE);
}
pub fn get_biome_id(&self, pos: &HexCoord) -> usize {
pub fn get_biome_id(&self, pos: &HexCoord) -> usize
{
assert!(
self.is_in_bounds(pos),
"The provided coordinate is not within the map bounds"
@@ -134,13 +152,15 @@ impl Map {
return chunk.biome_id[pos.to_chunk_local_index()];
}
pub fn get_center(&self) -> Vec3 {
pub fn get_center(&self) -> Vec3
{
let w = self.get_world_width();
let h = self.get_world_height();
return Vec3::new(w / 2., self.sealevel, h / 2.);
}
pub fn get_center_with_height(&self) -> Vec3 {
pub fn get_center_with_height(&self) -> Vec3
{
let w = self.get_world_width();
let h = self.get_world_height();
let mut pos = Vec3::new(w / 2., self.sealevel, h / 2.);
@@ -148,22 +168,27 @@ impl Map {
return pos;
}
pub fn get_world_width(&self) -> f32 {
pub fn get_world_width(&self) -> f32
{
return (self.width * Chunk::SIZE) as f32 * SHORT_DIAGONAL;
}
pub fn get_world_height(&self) -> f32 {
pub fn get_world_height(&self) -> f32
{
return (self.height * Chunk::SIZE) as f32 * 1.5;
}
pub fn get_world_size(&self) -> Vec2 {
pub fn get_world_size(&self) -> Vec2
{
return Vec2::new(self.get_world_width(), self.get_world_height());
}
pub fn set_height(&mut self, pos: &HexCoord, height: f32) {
pub fn set_height(&mut self, pos: &HexCoord, height: f32)
{
self.chunks[pos.to_chunk_index(self.width)].heights[pos.to_chunk_local_index()] = height;
}
pub fn create_crater(&mut self, pos: &HexCoord, radius: usize, depth: f32) -> Vec<(HexCoord, f32)> {
pub fn create_crater(&mut self, pos: &HexCoord, radius: usize, depth: f32) -> Vec<(HexCoord, f32)>
{
assert!(radius != 0, "Radius cannot be zero");
let tiles = self.hex_select_mut(pos, radius, true, |p, h, r| {
@@ -184,22 +209,30 @@ impl Map {
{
assert!(radius != 0, "Radius cannot be zero");
let mut result = if include_center {
let mut result = if include_center
{
Vec::with_capacity(get_tile_count_in_range(radius) + 1)
} else {
}
else
{
Vec::with_capacity(get_tile_count_in_range(radius))
};
if include_center {
if include_center
{
let h = self.sample_height(&center);
result.push((op)(center, h, 0));
}
for k in 0..(radius + 1) {
for k in 0..(radius + 1)
{
let mut p = center.scale(4, k);
for i in 0..6 {
for _j in 0..k {
for i in 0..6
{
for _j in 0..k
{
p = p.get_neighbor(i);
if self.is_in_bounds(&p) {
if self.is_in_bounds(&p)
{
let h = self.sample_height(&p);
result.push((op)(&p, h, k));
}
@@ -222,23 +255,30 @@ impl Map {
{
assert!(radius != 0, "Radius cannot be zero");
if include_center {
if include_center
{
let h = self.sample_height(&center);
let r = (op)(center, h, 0);
if r.is_some() {
if r.is_some()
{
return r;
}
}
for k in 0..(radius + 1) {
for k in 0..(radius + 1)
{
let mut p = center.scale(4, k);
for i in 0..6 {
for _j in 0..k {
for i in 0..6
{
for _j in 0..k
{
p = p.get_neighbor(i);
if self.is_in_bounds(&p) {
if self.is_in_bounds(&p)
{
let h = self.sample_height(&p);
let r = (op)(&p, h, k);
if r.is_some() {
if r.is_some()
{
return r;
}
}
@@ -265,15 +305,20 @@ impl Map {
"Start radius cannot be lower than end radius"
);
for k in start_radius..(end_radius + 1) {
for k in start_radius..(end_radius + 1)
{
let mut p = center.scale(4, k);
for i in 0..6 {
for _j in 0..k {
for i in 0..6
{
for _j in 0..k
{
p = p.get_neighbor(i);
if self.is_in_bounds(&p) {
if self.is_in_bounds(&p)
{
let h = self.sample_height(&p);
let r = (op)(&p, h, k);
if r.is_some() {
if r.is_some()
{
return r;
}
}
@@ -296,22 +341,30 @@ impl Map {
{
assert!(radius != 0, "Radius cannot be zero");
let mut result = if include_center {
let mut result = if include_center
{
Vec::with_capacity(get_tile_count_in_range(radius) + 1)
} else {
}
else
{
Vec::with_capacity(get_tile_count_in_range(radius))
};
if include_center {
if include_center
{
let h = self.sample_height_mut(&center);
result.push((op)(center, h, 0));
}
for k in 0..(radius + 1) {
for k in 0..(radius + 1)
{
let mut p = center.scale(4, k);
for i in 0..6 {
for _j in 0..k {
for i in 0..6
{
for _j in 0..k
{
p = p.get_neighbor(i);
if self.is_in_bounds(&p) {
if self.is_in_bounds(&p)
{
let h = self.sample_height_mut(&p);
result.push((op)(&p, h, k));
}

View File

@@ -4,9 +4,9 @@ use bevy::{math::VectorSpace, prelude::*};
use image::ImageBuffer;
use rayon::prelude::*;
use crate::hex_utils::HexCoord;
use hex::prelude::*;
use super::{biome_map::BiomeMap, chunk::Chunk, map::Map};
use super::{biome_map::BiomeMap, map::Map};
pub fn render_image(
size: UVec2,
@@ -64,7 +64,7 @@ pub fn render_map(map: &Map, smooth: f32) -> ImageBuffer<image::Rgba<u8>, Vec<u8
pub fn update_map(map: &Map, smooth: f32, image: &mut ImageBuffer<image::Rgba<u8>, Vec<u8>>)
{
image.par_enumerate_pixels_mut().for_each(|(x, y, pixel)| {
let coord = HexCoord::from_grid_pos(x as usize, y as usize);
let coord = HexCoord::from_offset_pos(x as usize, y as usize);
let right = coord.get_neighbor(1);
let height = map.sample_height(&coord);
@@ -155,7 +155,7 @@ pub fn update_biome_map(map: &Map, biome_map: &BiomeMap, image: &mut ImageBuffer
{
let map_biome_count = map.biome_count as f32;
image.par_enumerate_pixels_mut().for_each(|(x, y, pixel)| {
let coord = HexCoord::from_grid_pos(x as usize, y as usize);
let coord = HexCoord::from_offset_pos(x as usize, y as usize);
let biome_blend = biome_map.get_biome(x as i32, y as i32).unwrap();
let right = coord.get_neighbor(1);
let mut color = Oklaba::BLACK;

View File

@@ -2,9 +2,7 @@ use std::collections::VecDeque;
use bevy::math::IVec2;
use crate::hex_utils::HexCoord;
use super::chunk::Chunk;
use hex::prelude::*;
pub struct MeshChunkData
{
@@ -76,7 +74,7 @@ impl MeshChunkData
{
for x in 0..Chunk::SIZE
{
let coord = HexCoord::from_grid_pos(x + offset_x, z + offset_z);
let coord = HexCoord::from_offset_pos(x + offset_x, z + offset_z);
let idx = coord.to_chunk_local_index();
let h = self.heights[idx];
self.distance_to_land[idx] = if h > self.sealevel { 0.0 } else { 4.0 };

View File

@@ -1,6 +1,5 @@
pub mod chunk;
pub mod mesh_chunk;
pub mod biome_map;
pub mod config;
pub mod map;
pub mod biome_map;
pub mod map_utils;
pub mod mesh_chunk;

View File

@@ -1,5 +1,4 @@
pub use crate::consts::*;
pub use crate::map::chunk::*;
pub use crate::map::config::*;
pub use crate::map::map::*;
pub use crate::map::mesh_chunk::*;

View File

@@ -18,6 +18,7 @@ bevy_asset_loader = { version = "0.25.0", features = [
"standard_dynamic_assets",
"3d",
] }
hex = { path = "../../engine/hex" }
[features]
tracing = []

View File

@@ -1,17 +1,20 @@
use asset_loader::create_asset_loader;
use bevy::{
ecs::relationship::RelatedSpawnerCommands,
gltf::{GltfMesh, GltfNode},
prelude::*,
};
use serde::{Deserialize, Serialize};
use shared::identifiers::ResourceIdentifier;
use shared::{component_defination::ComponentDefination, identifiers::ResourceIdentifier};
use crate::{
buildings::{
conduit_building::ResourceConduitInfo, factory_building::FactoryBuildingInfo,
resource_gathering::ResourceGatheringBuildingInfo,
basic_building::BasicBuildingInfo, conduit_building::ResourceConduitInfo,
factory_building::FactoryBuildingInfo, resource_gathering::ResourceGatheringBuildingInfo,
tech_building::TechBuildingInfo,
},
footprint::BuildingFootprint,
prelude::Building,
};
#[derive(Asset, TypePath, Debug, Serialize, Deserialize)]
@@ -32,7 +35,7 @@ pub struct BuildingAsset
pub health: u32,
pub building_type: BuildingType,
// pub components: Option<Vec<ComponentDefination>>,
pub components: Option<Vec<ComponentDefination>>,
}
impl BuildingAsset
@@ -48,87 +51,101 @@ impl BuildingAsset
nodes: &Assets<GltfNode>,
) -> Option<Entity>
{
todo!("Update building spawning");
// let base_node = &gltf.named_nodes[&self.base_mesh_path.clone().into_boxed_str()];
// if let Some(node) = nodes.get(base_node.id()) {
// if let Some(mesh_handle) = &node.mesh {
// if let Some(gltf_mesh) = meshes.get(mesh_handle.id()) {
// let (mesh, mat) = gltf_mesh.unpack();
// let mut entity = commands.spawn((
// Mesh3d(mesh),
// MeshMaterial3d(mat),
// Transform::from_translation(pos).with_rotation(rot),
// Building,
// ));
// entity.with_children(|b| {
// for child in &node.children {
// let child_node = nodes.get(child.id());
// if child_node.is_none() {
// continue;
// }
// self.process_node(child_node.unwrap(), meshes, nodes, b, &node.name);
// }
// });
// if let Some(component) = self.get_component_def(&format!("/{0}", &node.name)) {
// component.apply(&mut entity);
// }
// return Some(entity.id());
// }
// }
// }
// return None;
let base_node = &gltf.named_nodes[&self.base_mesh_path.clone().into_boxed_str()];
if let Some(node) = nodes.get(base_node.id()) {
if let Some(mesh_handle) = &node.mesh {
if let Some(gltf_mesh) = meshes.get(mesh_handle.id()) {
if let Some(primitive) = gltf_mesh.primitives.first() {
let mesh = primitive.mesh.clone();
let mat = primitive
.material
.clone()
.expect(format!("Mesh '{}' does not have a meterial", primitive.name.as_str()).as_str());
let mut entity = commands.spawn((
Mesh3d(mesh),
MeshMaterial3d(mat),
Transform::from_translation(pos).with_rotation(rot),
Building,
));
entity.with_children(|b| {
for child in &node.children {
let child_node = nodes.get(child.id());
if child_node.is_none() {
continue;
}
self.process_node(child_node.unwrap(), meshes, nodes, b, &node.name);
}
});
if let Some(component) = self.get_component_def(&format!("/{0}", &node.name)) {
component.apply(&mut entity);
}
return Some(entity.id());
}
}
}
}
return None;
}
// fn process_node(
// &self,
// node: &GltfNode,
// meshes: &Assets<GltfMesh>,
// nodes: &Assets<GltfNode>,
// commands: &mut ChildBuilder,
// parent: &String,
// ) -> Option<Entity> {
// let path = format!("{0}/{1}", parent, node.name);
// if let Some(mesh) = &node.mesh {
// if let Some(gltf_mesh) = meshes.get(mesh.id()) {
// let (mesh, mat) = gltf_mesh.unpack();
// let mut entity = commands.spawn((Mesh3d(mesh), MeshMaterial3d(mat), node.transform, Building));
// entity.with_children(|b| {
// for child in &node.children {
// let child_node = nodes.get(child.id());
// if child_node.is_none() {
// continue;
// }
// self.process_node(child_node.unwrap(), meshes, nodes, b, &path);
// }
// });
// if let Some(component) = self.get_component_def(&path) {
// component.apply(&mut entity);
// }
// return Some(entity.id());
// }
// }
// return None;
// }
fn process_node(
&self,
node: &GltfNode,
meshes: &Assets<GltfMesh>,
nodes: &Assets<GltfNode>,
commands: &mut RelatedSpawnerCommands<ChildOf>,
parent: &String,
) -> Option<Entity>
{
let path = format!("{0}/{1}", parent, node.name);
if let Some(mesh) = &node.mesh {
if let Some(gltf_mesh) = meshes.get(mesh.id()) {
if let Some(primitive) = gltf_mesh.primitives.first() {
let mesh = primitive.mesh.clone();
let mat = primitive
.material
.clone()
.expect(format!("Mesh '{}' does not have a meterial", primitive.name.as_str()).as_str());
let mut entity = commands.spawn((Mesh3d(mesh), MeshMaterial3d(mat), node.transform, Building));
entity.with_children(|b| {
for child in &node.children {
let child_node = nodes.get(child.id());
if child_node.is_none() {
continue;
}
self.process_node(child_node.unwrap(), meshes, nodes, b, &path);
}
});
if let Some(component) = self.get_component_def(&path) {
component.apply(&mut entity);
}
return Some(entity.id());
}
}
}
return None;
}
// fn get_component_def(&self, path: &String) -> Option<&ComponentDefination> {
// if let Some(components) = &self.components {
// for c in components {
// if c.path.ends_with(path) {
// return Some(c);
// }
// }
// }
// return None;
// }
fn get_component_def(&self, path: &String) -> Option<&ComponentDefination>
{
if let Some(components) = &self.components {
for c in components {
if c.path.ends_with(path) {
return Some(c);
}
}
}
return None;
}
}
#[derive(Serialize, Deserialize, Debug, TypePath)]
pub enum BuildingType
{
Basic,
Basic(BasicBuildingInfo),
Gathering(ResourceGatheringBuildingInfo),
FactoryBuildingInfo(FactoryBuildingInfo),
ResourceConduit(ResourceConduitInfo),
Tech(TechBuildingInfo),
}
create_asset_loader!(

View File

@@ -1,15 +1,17 @@
use bevy::prelude::Resource;
use hex::prelude::*;
use shared::building::BuildingIdentifier;
use world_generation::hex_utils::HexCoord;
#[derive(Resource)]
pub struct BuildQueue {
pub struct BuildQueue
{
pub queue: Vec<QueueEntry>,
}
impl Default for BuildQueue {
fn default() -> Self {
impl Default for BuildQueue
{
fn default() -> Self
{
Self {
queue: Default::default(),
}
@@ -17,7 +19,8 @@ impl Default for BuildQueue {
}
#[derive(PartialEq, Eq)]
pub struct QueueEntry {
pub struct QueueEntry
{
pub building: BuildingIdentifier,
pub pos: HexCoord,
}

View File

@@ -1,3 +1,6 @@
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]
pub struct BasicBuildingInfo {}
pub struct BasicBuildingInfo
{
health: u32,
}

View File

@@ -1,21 +1,26 @@
use bevy::prelude::*;
use world_generation::{hex_utils::HexCoord, prelude::Chunk};
use hex::prelude::*;
#[derive(Resource)]
pub struct BuildingMap {
pub struct BuildingMap
{
pub chunks: Vec<BuildingChunk>,
pub size: UVec2,
}
impl BuildingMap {
pub fn new(size: UVec2) -> Self {
impl BuildingMap
{
pub fn new(size: UVec2) -> Self
{
let mut db = BuildingMap {
size,
chunks: Vec::with_capacity(size.length_squared() as usize),
};
for y in 0..size.y as i32 {
for x in 0..size.x as i32 {
for y in 0..size.y as i32
{
for x in 0..size.x as i32
{
let offset = IVec2::new(x, y);
let index = (x + y * size.x as i32) as usize;
db.chunks.push(BuildingChunk::new(offset, index));
@@ -25,7 +30,8 @@ impl BuildingMap {
return db;
}
pub fn get_buildings_in_range(&self, coord: &HexCoord, radius: usize) -> Vec<&BuildingEntry> {
pub fn get_buildings_in_range(&self, coord: &HexCoord, radius: usize) -> Vec<&BuildingEntry>
{
assert!(radius != 0, "Radius cannot be zero");
let w = self.size.x as usize * Chunk::SIZE;
@@ -34,10 +40,13 @@ impl BuildingMap {
return self.get_buildings_in_coords(coords);
}
pub fn get_buildings_in_coords(&self, coords: Vec<HexCoord>) -> Vec<&BuildingEntry> {
pub fn get_buildings_in_coords(&self, coords: Vec<HexCoord>) -> Vec<&BuildingEntry>
{
let mut result = Vec::new();
for coord in &coords {
if let Some(buidling) = self.get_building(coord) {
for coord in &coords
{
if let Some(buidling) = self.get_building(coord)
{
result.push(buidling);
}
}
@@ -45,25 +54,30 @@ impl BuildingMap {
return result;
}
pub fn get_building(&self, coord: &HexCoord) -> Option<&BuildingEntry> {
pub fn get_building(&self, coord: &HexCoord) -> Option<&BuildingEntry>
{
let chunk = &self.chunks[coord.to_chunk_index(self.size.x as usize)];
return chunk.get_building(coord);
}
pub fn add_building(&mut self, entry: BuildingEntry) {
pub fn add_building(&mut self, entry: BuildingEntry)
{
let chunk = &mut self.chunks[entry.coord.to_chunk_index(self.size.x as usize)];
chunk.add_building(entry);
}
}
pub struct BuildingChunk {
pub struct BuildingChunk
{
pub entries: Vec<BuildingEntry>,
pub index: usize,
pub offset: IVec2,
}
impl BuildingChunk {
pub fn new(offset: IVec2, index: usize) -> Self {
impl BuildingChunk
{
pub fn new(offset: IVec2, index: usize) -> Self
{
return BuildingChunk {
entries: Vec::new(),
index,
@@ -71,16 +85,19 @@ impl BuildingChunk {
};
}
pub fn get_building(&self, coord: &HexCoord) -> Option<&BuildingEntry> {
pub fn get_building(&self, coord: &HexCoord) -> Option<&BuildingEntry>
{
return self.entries.iter().find(|b| &b.coord == coord);
}
pub fn add_building(&mut self, entry: BuildingEntry) {
pub fn add_building(&mut self, entry: BuildingEntry)
{
self.entries.push(entry);
}
}
pub struct BuildingEntry {
pub struct BuildingEntry
{
pub coord: HexCoord,
pub entity: Entity,
pub is_main: bool,
@@ -89,8 +106,10 @@ pub struct BuildingEntry {
pub child_entities: Option<Vec<Entity>>,
}
impl BuildingEntry {
pub fn new(coord: HexCoord, entity: Entity) -> Self {
impl BuildingEntry
{
pub fn new(coord: HexCoord, entity: Entity) -> Self
{
return BuildingEntry {
coord,
entity,
@@ -101,7 +120,8 @@ impl BuildingEntry {
};
}
pub fn new_with_children(coord: HexCoord, entity: Entity, children: Vec<Entity>) -> BuildingEntry {
pub fn new_with_children(coord: HexCoord, entity: Entity, children: Vec<Entity>) -> BuildingEntry
{
return BuildingEntry {
coord,
entity,
@@ -112,7 +132,8 @@ impl BuildingEntry {
};
}
pub fn new_with_parent(coord: HexCoord, entity: Entity, main: Entity) -> BuildingEntry {
pub fn new_with_parent(coord: HexCoord, entity: Entity, main: Entity) -> BuildingEntry
{
return BuildingEntry {
coord,
entity,

View File

@@ -1,32 +1,39 @@
use bevy::math::{IVec2, Vec3Swizzles};
use hex::prelude::*;
use serde::{Deserialize, Serialize};
use world_generation::hex_utils::HexCoord;
use shared::coords::CoordsCollection;
#[derive(Serialize, Deserialize, Debug)]
pub struct BuildingFootprint {
pub struct BuildingFootprint
{
pub footprint: Vec<IVec2>,
}
impl BuildingFootprint {
pub fn get_footprint(&self, center: &HexCoord) -> Vec<HexCoord> {
let c = center.hex.xy();
return self.footprint.iter().map(|p| HexCoord::from_hex(*p + c)).collect();
impl BuildingFootprint
{
pub fn get_footprint(&self, position: &HexCoord) -> CoordsCollection
{
CoordsCollection::from_points(self.footprint.clone()).with_translation(position)
}
pub fn get_footprint_rotated(&self, center: &HexCoord, rotation: i32) -> Vec<HexCoord> {
let c = center.hex.xy();
return self
pub fn get_neighbors(&self, position: &HexCoord) -> CoordsCollection
{
let n_points: Vec<IVec2> = self
.footprint
.iter()
.map(|p| HexCoord::from_hex(*p + c).rotate_around(center, rotation))
.flat_map(|p| HexCoord::from_axial(*p).get_neighbors())
.map(|c| c.hex.xy())
.filter(|p| !self.footprint.contains(p))
.collect();
}
pub fn get_neighbors(&self, center: &HexCoord) -> Vec<HexCoord> {
todo!()
}
pub fn get_neighbors_rotated(&self, center: &HexCoord, rotation: i32) -> Vec<HexCoord> {
todo!();
let mut out_points: Vec<IVec2> = Vec::with_capacity(n_points.len());
for p in n_points
{
if out_points.contains(&p)
{
continue;
}
out_points.push(p);
}
return CoordsCollection::from_points(out_points).with_translation(position);
}
}

View File

@@ -27,9 +27,12 @@ bevy_asset_loader = { version = "0.25.0", features = [
] }
ron = "0.12.0"
image = "0.25.9"
hex = { path = "../../engine/hex" }
# bevy_lunex = "0.2.4"
[features]
editor = []
tracing = [
"bevy/trace_tracy",
"world_generation/tracing",

View File

@@ -1,11 +1,12 @@
use bevy::anti_alias::taa::TemporalAntiAliasing;
use bevy::camera::visibility::RenderLayers;
use bevy::core_pipeline::prepass::DepthPrepass;
use bevy::input::mouse::{MouseMotion, MouseScrollUnit, MouseWheel};
use bevy::prelude::*;
use bevy::window::{CursorGrabMode, CursorOptions, PrimaryWindow};
use hex::prelude::*;
use shared::sets::GameplaySet;
use shared::tags::MainCamera;
use world_generation::hex_utils::HexCoord;
use world_generation::prelude::Map;
use world_generation::states::GeneratorState;
@@ -53,10 +54,8 @@ fn setup(mut commands: Commands)
PhosOrbitCamera::default(),
TemporalAntiAliasing::default(),
))
.insert(Msaa::Off);
// .insert(RenderLayers::layer(0))
// *msaa = Msaa::Off;
.insert(Msaa::Off)
.insert(RenderLayers::default());
}
fn orbit_camera_upate(

View File

@@ -1,6 +1,6 @@
use bevy::prelude::*;
use hex::prelude::*;
use rayon::str;
use world_generation::prelude::Chunk;
#[derive(Component, Reflect)]
#[reflect(Component)]

View File

@@ -4,13 +4,14 @@ use bevy::{light::NotShadowCaster, pbr::ExtendedMaterial, prelude::*};
use bevy_asset_loader::prelude::*;
use bevy_inspector_egui::quick::ResourceInspectorPlugin;
use hex::prelude::*;
use rayon::iter::{IntoParallelRefIterator, ParallelIterator};
use shared::states::{AssetLoadState, GameplayState, MenuState};
use world_generation::{
biome_asset::{BiomeAsset, BiomeAssetPlugin},
biome_painter::*,
heightmap::generate_heightmap,
hex_utils::{offset_to_index, SHORT_DIAGONAL},
map::biome_map::BiomeMap,
prelude::*,
tile_manager::*,

View File

@@ -2,7 +2,6 @@ use crate::camera_system::components::PhosCamera;
use crate::map_rendering::map_init::MapInitPlugin;
use crate::map_rendering::render_distance_system::RenderDistancePlugin;
use crate::ui::build_ui::BuildUIPlugin;
use crate::utlis::editor_plugin::EditorPlugin;
use crate::utlis::tile_selection_plugin::TileSelectionPlugin;
use crate::{camera_system::camera_plugin::PhosCameraPlugin, utlis::debug_plugin::DebugPlugin};
use bevy::diagnostic::{EntityCountDiagnosticsPlugin, FrameTimeDiagnosticsPlugin};
@@ -12,13 +11,9 @@ use bevy_asset_loader::prelude::*;
use bevy_rapier3d::dynamics::{Ccd, RigidBody, Velocity};
use bevy_rapier3d::geometry::Collider;
use bevy_rapier3d::plugin::{NoUserData, RapierPhysicsPlugin};
// use buildings::BuildingPugin;
// use iyes_perf_ui::prelude::*;
// use shared::animation_plugin::SimpleAnimationPlugin;
use shared::sets::GameplaySet;
use shared::states::{GameplayState, MenuState};
use shared::{despawn::DespawnPuglin, states::AssetLoadState};
// use units::units_plugin::UnitsPlugin;
use world_generation::states::GeneratorState;
pub struct PhosGamePlugin;
@@ -45,8 +40,8 @@ impl Plugin for PhosGamePlugin
// UnitsPlugin,
DespawnPuglin,
TileSelectionPlugin,
#[cfg(debug_assertions)]
EditorPlugin,
#[cfg(feature = "editor")]
crate::utlis::editor_plugin::EditorPlugin,
#[cfg(debug_assertions)]
DebugPlugin,
));
@@ -146,8 +141,7 @@ fn spawn_sphere(
mat: Res<SphereMat>,
)
{
if keyboard_input.just_pressed(KeyCode::KeyF)
{
if keyboard_input.just_pressed(KeyCode::KeyF) {
commands.spawn((
Mesh3d(meshes.add(Sphere::new(0.3))),
MeshMaterial3d(mat.0.clone()),

View File

@@ -1,39 +1,223 @@
use bevy::{camera::visibility::RenderLayers, prelude::*};
use bevy::{
camera::{visibility::RenderLayers, CameraOutputMode},
prelude::*,
render::render_resource::BlendState,
};
use shared::states::AssetLoadState;
use crate::ui::states::BuildUIState;
pub struct BuildUIPlugin;
#[derive(Component, Default)]
struct BuildUIItem;
#[derive(Component, Default)]
#[require(BuildUIItem)]
struct BuildUIMenuItem;
#[derive(Component, Default)]
struct BuildMenuRoot;
#[derive(Resource, Reflect, Debug, Default)]
struct BuildUIInfo
{
menu: BuildUIMenu,
}
#[derive(Default, Reflect, Debug)]
enum BuildUIMenu
{
#[default]
Structure,
Defense,
}
impl Plugin for BuildUIPlugin
{
fn build(&self, app: &mut App)
{
app.add_systems(Startup, setup_cameras);
app.add_systems(Update, spawn_ui.run_if(in_state(AssetLoadState::LoadComplete)));
app.init_resource::<BuildUIInfo>();
app.insert_state(BuildUIState::Init);
app.add_systems(
Update,
(
spawn_ui.run_if(in_state(AssetLoadState::LoadComplete).and(in_state(BuildUIState::Init))),
draw_menu_ui.run_if(in_state(AssetLoadState::LoadComplete).and(in_state(BuildUIState::DrawMenu))),
),
);
app.add_systems(PostUpdate, cleanup_ui.run_if(in_state(BuildUIState::Cleanup)));
}
}
fn setup_cameras(mut commands: Commands)
{
commands.spawn((Camera2d, IsDefaultUiCamera, RenderLayers::layer(2)));
commands
.spawn((
Camera2d,
Camera {
order: 1,
clear_color: ClearColorConfig::None,
msaa_writeback: MsaaWriteback::Always,
output_mode: CameraOutputMode::Write {
blend_state: Some(BlendState::ALPHA_BLENDING),
clear_color: ClearColorConfig::None,
},
..default()
},
IsDefaultUiCamera,
))
.insert(RenderLayers::layer(1))
.insert(Msaa::Off);
}
fn spawn_ui(mut commands: Commands)
fn spawn_ui(mut commands: Commands, mut next_state: ResMut<NextState<BuildUIState>>)
{
commands
.spawn((Node {
width: Val::Percent(100.),
height: Val::Percent(100.),
justify_content: JustifyContent::Center,
align_items: AlignItems::End,
..default()
},))
.spawn((
Node {
width: Val::Percent(100.),
height: Val::Percent(100.),
justify_content: JustifyContent::Center,
align_items: AlignItems::End,
..default()
},
RenderLayers::layer(1),
Name::new("Build UI Root"),
BuildUIItem,
))
// .insert(PickingBehavior::IGNORE)
.with_children(|parent| {
parent.spawn((
Node {
width: Val::Px(500.),
..Default::default()
},
BackgroundColor(LinearRgba::GREEN.into()),
));
.with_children(|build_root| {
build_root
.spawn((
Name::new("Build UI"),
Node {
width: Val::Px(500.),
height: Val::Px(100.),
justify_content: JustifyContent::Stretch,
flex_direction: FlexDirection::Column,
..default()
},
BackgroundColor(LinearRgba::GREEN.into()),
))
.with_children(|build_ui| {
build_ui.spawn((
Name::new("Menu Root"),
BuildMenuRoot,
Node {
width: Val::Percent(100.),
height: Val::Px(70.),
column_gap: Val::Px(5.),
padding: UiRect::all(Val::Px(2.)),
flex_direction: FlexDirection::Row,
overflow: Overflow::scroll_x(),
..default()
},
));
build_ui
.spawn((
Name::new("Toolbar"),
Node {
width: Val::Percent(100.),
height: Val::Px(30.),
column_gap: Val::Px(5.),
padding: UiRect::horizontal(Val::Px(10.)),
justify_content: JustifyContent::Stretch,
align_self: AlignSelf::End,
..default()
},
BackgroundColor(LinearRgba::BLUE.into()),
))
.with_children(|toolbar| {
for i in 0..6
{
toolbar.spawn((
Name::new(format!("Button {}", i)),
Button,
Node {
height: Val::Percent(100.),
width: Val::Percent(100.),
align_items: AlignItems::Center,
justify_content: JustifyContent::Center,
..default()
},
BackgroundColor(LinearRgba::WHITE.into()),
children![(
Text::new(format!("Button {}", i)),
TextFont {
font_size: 15.,
..default()
},
TextColor(LinearRgba::BLACK.into()),
TextShadow {
offset: Vec2::splat(2.),
..default()
}
)],
));
}
});
});
});
next_state.set(BuildUIState::DrawMenu);
}
fn draw_menu_ui(
mut commands: Commands,
menu_root: Single<Entity, With<BuildMenuRoot>>,
menu_items: Query<Entity, With<BuildUIMenuItem>>,
menu_info: Res<BuildUIInfo>,
mut next: ResMut<NextState<BuildUIState>>,
)
{
for entity in menu_items.iter()
{
commands.entity(entity).despawn();
}
info!("Draw Menu");
match menu_info.menu
{
BuildUIMenu::Structure => draw_structure_ui(commands, menu_root.into_inner()),
BuildUIMenu::Defense => (),
}
next.set(BuildUIState::Update);
}
fn draw_structure_ui(mut commands: Commands, root: Entity)
{
commands.entity(root).with_children(|root| {
for i in 0..10
{
root.spawn((
BuildUIMenuItem,
Node {
height: Val::Percent(100.),
width: Val::Px(100.),
display: Display::Grid,
grid_template_rows: vec![RepeatedGridTrack::px(1, 100.), RepeatedGridTrack::fr(1, 1.)],
..default()
},
children![
(
Node {
height: Val::Px(100.),
width: Val::Px(100.),
..default()
},
BackgroundColor(LinearRgba::RED.into())
),
(Text::new(format!("Icon {}", i))),
],
));
}
});
}
fn cleanup_ui(mut commands: Commands, ui_items: Query<Entity, With<BuildUIItem>>)
{
for entity in ui_items.iter()
{
commands.entity(entity).despawn();
}
}

View File

@@ -1 +1,2 @@
pub mod build_ui;
pub mod states;

View File

@@ -0,0 +1,10 @@
use bevy::prelude::*;
#[derive(States, Debug, Clone, PartialEq, Eq, Hash)]
pub enum BuildUIState
{
Init,
DrawMenu,
Update,
Cleanup,
}

View File

@@ -7,6 +7,7 @@ use bevy::{
mesh::Mesh,
};
use bevy_rapier3d::geometry::{Collider, TriMeshFlags};
use hex::prelude::*;
use rayon::iter::{IntoParallelRefMutIterator, ParallelIterator};
use world_generation::{
biome_painter::BiomePainter,
@@ -14,8 +15,7 @@ use world_generation::{
chunk_colliders::generate_chunk_collider,
mesh_generator::{generate_chunk_mesh, generate_chunk_water_mesh},
},
hex_utils::offset_to_world,
prelude::{Chunk, Map, MeshChunkData},
prelude::{Map, MeshChunkData},
tile_manager::TileAsset,
tile_mapper::TileMapperAsset,
};

View File

@@ -1,6 +1,6 @@
use bevy::asset::RenderAssetUsages;
use bevy::prelude::*;
use bevy_inspector_egui::bevy_egui::EguiContexts;
use bevy_inspector_egui::bevy_egui::{EguiContexts, EguiTextureHandle};
use bevy_inspector_egui::egui::{self};
use image::{ImageBuffer, Rgba};
use world_generation::biome_asset::BiomeAsset;
@@ -92,14 +92,14 @@ fn asset_reloaded(
}
fn render_map_ui(
// image: Res<MapImage>,
image: Res<MapImage>,
heightmap: Res<Map>,
biome_map: Res<BiomeMap>,
mut contexts: EguiContexts,
mut state: ResMut<UIState>,
)
{
// let id = contexts.add_image(image.0.);
let id = contexts.add_image(EguiTextureHandle::Weak(image.0.id()));
let mut map_type = state.target_map_type;
let ctx = contexts.ctx_mut().expect("Failed to get egui context");
egui::Window::new("Map").open(&mut state.is_open).show(ctx, |ui| {
@@ -127,10 +127,10 @@ fn render_map_ui(
);
});
// ui.add(egui::widgets::Image::new(egui::load::SizedTexture::new(
// id,
// [512.0, 512.0],
// )));
ui.add(egui::widgets::Image::new(egui::load::SizedTexture::new(
id,
[512.0, 512.0],
)));
if ui.button("Save Image").clicked()
{

View File

@@ -1,4 +1,5 @@
pub mod chunk_utils;
pub mod debug_plugin;
#[cfg(feature = "editor")]
pub mod editor_plugin;
pub mod tile_selection_plugin;

View File

@@ -1,10 +1,12 @@
use bevy::{prelude::*, window::PrimaryWindow};
use bevy_rapier3d::{plugin::ReadRapierContext, prelude::QueryFilter};
use hex::prelude::*;
use shared::{
resources::{TileContact, TileUnderCursor},
tags::MainCamera,
};
use world_generation::{hex_utils::HexCoord, prelude::Map, states::GeneratorState};
use world_generation::{prelude::Map, states::GeneratorState};
pub struct TileSelectionPlugin;
impl Plugin for TileSelectionPlugin

View File

@@ -9,6 +9,7 @@ edition = "2021"
bevy = "0.18.0"
serde = { version = "1.0.228", features = ["derive"] }
world_generation = { path = "../../engine/world_generation" }
hex = { path = "../../engine/hex" }
[features]

View File

@@ -1,18 +1,18 @@
use bevy::{
ecs::system::EntityCommands, math::{Quat, Vec3}, prelude::*
};
use bevy::{ecs::system::EntityCommands, prelude::*};
use serde::{Deserialize, Serialize};
use crate::prefab_defination::AnimationComponent;
#[derive(Serialize, Deserialize, Debug)]
pub struct ComponentDefination {
pub struct ComponentDefination
{
pub path: String,
pub animations: Vec<AnimationComponent>,
}
impl ComponentDefination {
pub fn apply(&self, commands: &mut EntityCommands){
impl ComponentDefination
{
pub fn apply(&self, commands: &mut EntityCommands)
{
for c in &self.animations {
c.apply(commands);
}

69
game/shared/src/coords.rs Normal file
View File

@@ -0,0 +1,69 @@
use bevy::prelude::*;
use hex::prelude::*;
#[derive(Default, Debug, Reflect)]
pub struct CoordsCollection
{
points: Vec<IVec2>,
origin: IVec2,
translation: IVec2,
rotation: i32,
}
impl CoordsCollection
{
pub fn from_hex(coords: Vec<HexCoord>) -> Self
{
CoordsCollection {
points: coords.iter().map(|c| c.hex.xy()).collect(),
..default()
}
}
pub fn from_points(points: Vec<IVec2>) -> Self
{
CoordsCollection { points, ..default() }
}
pub fn with_translation(mut self, translation: &HexCoord) -> Self
{
self.translation = translation.hex.xy();
return self;
}
pub fn with_translation_vec(mut self, translation: IVec2) -> Self
{
self.translation = translation;
return self;
}
pub fn with_origin(mut self, orign: &HexCoord) -> Self
{
self.origin = orign.hex.xy();
return self;
}
pub fn with_rotation(mut self, rotation: i32) -> Self
{
self.rotation = rotation;
return self;
}
pub fn get_coords(&self) -> Vec<HexCoord>
{
let center = HexCoord::from_axial(self.origin);
return self
.points
.iter()
.map(|p| HexCoord::from_axial(p + self.origin).rotate_around(&center, self.rotation))
.collect();
}
}
impl Into<Vec<HexCoord>> for CoordsCollection
{
fn into(self) -> Vec<HexCoord>
{
self.get_coords()
}
}

View File

@@ -1,13 +1,15 @@
use bevy::prelude::*;
use world_generation::hex_utils::*;
use hex::prelude::*;
#[derive(Message)]
pub enum TileModifiedEvent {
pub enum TileModifiedEvent
{
HeightChanged(HexCoord, f32),
TypeChanged(HexCoord, usize),
}
#[derive(Message)]
pub struct ChunkModifiedEvent {
pub struct ChunkModifiedEvent
{
pub index: usize,
}

View File

@@ -1,20 +1,22 @@
use bevy::reflect::Reflect;
use serde::{Deserialize, Serialize};
pub mod animation_plugin;
pub mod building;
pub mod component_defination;
pub mod coords;
pub mod despawn;
pub mod events;
pub mod identifiers;
pub mod prefab_defination;
pub mod resources;
pub mod sets;
pub mod states;
pub mod tags;
// pub mod prefab_defination;
// pub mod animation_plugin;
// pub mod component_defination;
#[derive(Debug, Serialize, Deserialize)]
pub enum Tier {
pub enum Tier
{
Zero,
One,
Two,
@@ -23,7 +25,8 @@ pub enum Tier {
}
#[derive(Serialize, Deserialize, Debug, Reflect)]
pub enum StatusEffect {
pub enum StatusEffect
{
UnitRange(f32),
UnitAttack(f32),
UnitHealth(f32),

View File

@@ -1,12 +1,13 @@
use bevy::{
ecs::system::{EntityCommand, EntityCommands},
ecs::{relationship::RelatedSpawnerCommands, system::EntityCommands},
gltf::{Gltf, GltfMesh},
math::{Quat, Vec3},
prelude::*,
};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]
pub struct PrefabDefination {
pub struct PrefabDefination
{
pub path: String,
pub pos: Vec3,
pub rot: Vec3,
@@ -14,38 +15,54 @@ pub struct PrefabDefination {
pub animations: Option<Vec<AnimationComponent>>,
}
impl PrefabDefination {
pub fn spawn_recursive(&self, gltf: &Gltf, commands: &mut ChildBuilder, meshes: &Assets<GltfMesh>) {
impl PrefabDefination
{
pub fn spawn_recursive(
&self,
gltf: &Gltf,
commands: &mut RelatedSpawnerCommands<ChildOf>,
meshes: &Assets<GltfMesh>,
)
{
let mesh_handle = &gltf.named_meshes[&self.path.clone().into_boxed_str()];
if let Some(gltf_mesh) = meshes.get(mesh_handle.id()) {
let (m, mat) = gltf_mesh.unpack();
let mut entity = commands.spawn((
Mesh3d(m),
MeshMaterial3d(mat),
Transform::from_translation(self.pos).with_rotation(Quat::from_euler(
bevy::math::EulerRot::XYZ,
self.rot.x,
self.rot.y,
self.rot.z,
)),
));
if let Some(children) = &self.children {
entity.with_children(|b| {
for child in children {
child.spawn_recursive(gltf, b, meshes);
}
});
if let Some(primitive) = gltf_mesh.primitives.first() {
let mesh = primitive.mesh.clone();
let mat = primitive
.material
.clone()
.expect(format!("Mesh '{}' does not have a meterial", primitive.name.as_str()).as_str());
let mut entity = commands.spawn((
Mesh3d(mesh),
MeshMaterial3d(mat),
Transform::from_translation(self.pos).with_rotation(Quat::from_euler(
bevy::math::EulerRot::XYZ,
self.rot.x,
self.rot.y,
self.rot.z,
)),
));
if let Some(children) = &self.children {
entity.with_children(|b| {
for child in children {
child.spawn_recursive(gltf, b, meshes);
}
});
}
}
}
}
}
pub trait UnpackGltfMesh {
pub trait UnpackGltfMesh
{
fn unpack(&self) -> (Handle<Mesh>, Handle<StandardMaterial>);
}
impl UnpackGltfMesh for GltfMesh {
fn unpack(&self) -> (Handle<Mesh>, Handle<StandardMaterial>) {
impl UnpackGltfMesh for GltfMesh
{
fn unpack(&self) -> (Handle<Mesh>, Handle<StandardMaterial>)
{
let p = &self.primitives[0];
let mut mat: Handle<StandardMaterial> = default();
if let Some(mesh_material) = &p.material {
@@ -56,19 +73,23 @@ impl UnpackGltfMesh for GltfMesh {
}
#[derive(Serialize, Deserialize, Debug)]
pub enum AnimationComponent {
pub enum AnimationComponent
{
Rotation(RotationAnimation),
Slider,
}
#[derive(Serialize, Deserialize, Debug, Component, Clone, Copy)]
pub struct RotationAnimation {
pub struct RotationAnimation
{
pub axis: Vec3,
pub speed: f32,
}
impl AnimationComponent {
pub fn apply(&self, commands: &mut EntityCommands) {
impl AnimationComponent
{
pub fn apply(&self, commands: &mut EntityCommands)
{
match self {
AnimationComponent::Rotation(comp) => {
commands.insert(comp.clone());

View File

@@ -1,18 +1,21 @@
use bevy::prelude::*;
use world_generation::hex_utils::HexCoord;
use hex::prelude::*;
#[derive(Resource, Default)]
pub struct TileUnderCursor(pub Option<TileContact>);
#[derive(Clone, Copy)]
pub struct TileContact {
pub struct TileContact
{
pub tile: HexCoord,
pub point: Vec3,
pub surface: Vec3,
}
impl TileContact {
pub fn new(tile: HexCoord, contact: Vec3, surface: Vec3) -> Self {
impl TileContact
{
pub fn new(tile: HexCoord, contact: Vec3, surface: Vec3) -> Self
{
return Self {
tile,
point: contact,

View File

@@ -1,7 +1,8 @@
use bevy::prelude::*;
#[derive(States, Debug, Clone, PartialEq, Eq, Hash)]
pub enum MenuState {
pub enum MenuState
{
Loading,
Startup,
MainMenu,
@@ -10,14 +11,16 @@ pub enum MenuState {
}
#[derive(States, Debug, Clone, PartialEq, Eq, Hash)]
pub enum GameplayState {
pub enum GameplayState
{
Waiting,
PlaceHQ,
Playing,
}
#[derive(States, Debug, Clone, PartialEq, Eq, Hash)]
pub enum AssetLoadState {
pub enum AssetLoadState
{
Loading,
FinalizeAssets,
LoadComplete,

View File

@@ -6,6 +6,7 @@ edition = "2021"
[dependencies]
bevy = "0.18.0"
world_generation = { path = "../../engine/world_generation" }
hex = { path = "../../engine/hex" }
shared = { path = "../shared" }
bevy_rapier3d = "0.33.0"
serde = { version = "1.0.228", features = ["derive"] }

View File

@@ -1,6 +1,6 @@
use bevy::{ecs::world::CommandQueue, prelude::*, tasks::Task};
use hex::prelude::*;
use serde::{Deserialize, Serialize};
use world_generation::hex_utils::HexCoord;
#[derive(Component, Debug)]
pub struct Unit;
@@ -13,7 +13,8 @@ pub struct LandUnit;
pub struct NavalUnit;
#[derive(Serialize, Deserialize, Debug, Clone, Copy)]
pub enum UnitDomain {
pub enum UnitDomain
{
Land,
Air,
Naval,

View File

@@ -1,21 +1,27 @@
use bevy::prelude::*;
use hex::prelude::*;
use ordered_float::OrderedFloat;
use world_generation::{hex_utils::HexCoord, prelude::Map};
use world_generation::prelude::Map;
#[derive(Clone, Resource)]
pub struct NavData {
pub struct NavData
{
pub tiles: Vec<NavTile>,
pub map_height: usize,
pub map_width: usize,
}
impl NavData {
pub fn get_neighbors(&self, coord: &HexCoord) -> Vec<(HexCoord, OrderedFloat<f32>)> {
impl NavData
{
pub fn get_neighbors(&self, coord: &HexCoord) -> Vec<(HexCoord, OrderedFloat<f32>)>
{
let mut neighbors = Vec::with_capacity(6);
let cur_height = self.get_height(coord);
for i in 0..6 {
for i in 0..6
{
let n = coord.get_neighbor(i);
if !self.is_in_bounds(&n) {
if !self.is_in_bounds(&n)
{
continue;
}
let n_height = self.get_height(&n);
@@ -23,27 +29,33 @@ impl NavData {
}
return neighbors;
}
pub fn get(&self, coord: &HexCoord) -> &NavTile {
pub fn get(&self, coord: &HexCoord) -> &NavTile
{
return &self.tiles[coord.to_index(self.map_width)];
}
pub fn get_height(&self, coord: &HexCoord) -> f32 {
pub fn get_height(&self, coord: &HexCoord) -> f32
{
return self.tiles[coord.to_index(self.map_width)].height;
}
pub fn is_in_bounds(&self, pos: &HexCoord) -> bool {
pub fn is_in_bounds(&self, pos: &HexCoord) -> bool
{
return pos.is_in_bounds(self.map_height, self.map_width);
}
pub fn build(map: &Map) -> NavData {
pub fn build(map: &Map) -> NavData
{
#[cfg(feature = "tracing")]
let _path_span = info_span!("Build Nav Data").entered();
let mut tiles = Vec::with_capacity(map.get_tile_count());
let h = map.get_tile_height();
let w = map.get_tile_width();
for y in 0..h {
for x in 0..w {
let coord = HexCoord::from_grid_pos(x, y);
for y in 0..h
{
for x in 0..w
{
let coord = HexCoord::from_offset_pos(x, y);
let height = map.sample_height(&coord);
let tile = NavTile {
coord,
@@ -61,14 +73,17 @@ impl NavData {
};
}
pub fn update(&mut self, map: &Map) {
pub fn update(&mut self, map: &Map)
{
#[cfg(feature = "tracing")]
let _path_span = info_span!("Update Nav Data").entered();
let h = map.get_tile_height();
let w = map.get_tile_width();
for y in 0..h {
for x in 0..w {
let coord = HexCoord::from_grid_pos(x, y);
for y in 0..h
{
for x in 0..w
{
let coord = HexCoord::from_offset_pos(x, y);
let height = map.sample_height(&coord);
let tile = NavTile {
coord,
@@ -79,7 +94,8 @@ impl NavData {
}
}
}
pub fn update_tile(&mut self, coord: &HexCoord, height: f32, move_cost: f32) {
pub fn update_tile(&mut self, coord: &HexCoord, height: f32, move_cost: f32)
{
let tile = &mut self.tiles[coord.to_index(self.map_width)];
tile.move_cost = move_cost;
tile.height = height;
@@ -87,14 +103,17 @@ impl NavData {
}
#[derive(Clone)]
pub struct NavTile {
pub struct NavTile
{
pub height: f32,
pub move_cost: f32,
pub coord: HexCoord,
}
impl NavTile {
pub fn calculate_heuristic(&self, to: &HexCoord) -> OrderedFloat<f32> {
impl NavTile
{
pub fn calculate_heuristic(&self, to: &HexCoord) -> OrderedFloat<f32>
{
return (self.coord.distance(to) as f32).into();
}
}

View File

@@ -5,9 +5,10 @@ use bevy::{
prelude::*,
tasks::{futures, AsyncComputeTaskPool},
};
use hex::prelude::*;
use pathfinding::prelude::astar;
use shared::{events::TileModifiedEvent, resources::TileUnderCursor, sets::GameplaySet};
use world_generation::{hex_utils::HexCoord, prelude::Map, states::GeneratorState};
use world_generation::{prelude::Map, states::GeneratorState};
#[cfg(debug_assertions)]
use crate::units_debug_plugin::UnitsDebugPlugin;
@@ -20,8 +21,10 @@ use crate::{
pub struct UnitsPlugin;
impl Plugin for UnitsPlugin {
fn build(&self, app: &mut App) {
impl Plugin for UnitsPlugin
{
fn build(&self, app: &mut App)
{
app.init_resource::<PathBatchId>();
app.add_plugins(UnitAssetPlugin);
@@ -40,15 +43,20 @@ impl Plugin for UnitsPlugin {
}
}
fn build_navdata(mut commands: Commands, map: Res<Map>) {
fn build_navdata(mut commands: Commands, map: Res<Map>)
{
let nav_data = NavData::build(&map);
commands.insert_resource(nav_data);
}
fn update_navdata(mut tile_updates: MessageReader<TileModifiedEvent>, mut nav_data: ResMut<NavData>) {
for event in tile_updates.read() {
match event {
TileModifiedEvent::HeightChanged(coord, new_height) => {
fn update_navdata(mut tile_updates: MessageReader<TileModifiedEvent>, mut nav_data: ResMut<NavData>)
{
for event in tile_updates.read()
{
match event
{
TileModifiedEvent::HeightChanged(coord, new_height) =>
{
nav_data.update_tile(coord, *new_height, 1.0);
}
_ => (),
@@ -56,6 +64,7 @@ fn update_navdata(mut tile_updates: MessageReader<TileModifiedEvent>, mut nav_da
}
}
#[allow(unused)]
fn units_control(tile_under_cursor: Res<TileUnderCursor>) {}
fn move_unit(
@@ -63,22 +72,27 @@ fn move_unit(
time: Res<Time>,
map: Res<Map>,
mut commands: Commands,
) {
for (mut t, mut path, entity) in units.iter_mut() {
if path.1 >= path.0.len() {
)
{
for (mut t, mut path, entity) in units.iter_mut()
{
if path.1 >= path.0.len()
{
commands.entity(entity).remove::<Path>();
continue;
}
let p = path.0[path.1];
let d = p - t.translation;
if d.length() < 0.1 {
if d.length() < 0.1
{
path.1 += 1;
continue;
}
let vel = d.normalize() * 10.0 * time.delta_secs();
t.translation += vel;
let coord = HexCoord::from_world_pos(t.translation);
if map.is_in_bounds(&coord) {
if map.is_in_bounds(&coord)
{
t.translation.y = map.sample_height(&coord);
}
}
@@ -90,22 +104,28 @@ fn dispatch_path_requests(
nav_data: Res<NavData>,
mut batch_id: ResMut<PathBatchId>,
mut commands: Commands,
) {
if units.is_empty() {
)
{
if units.is_empty()
{
return;
}
let mut groups: HashMap<HexCoord, Vec<PathRequest>> = HashMap::new();
#[cfg(feature = "tracing")]
let _group_span = info_span!("Grouping").entered();
for (transform, target, entity) in units.iter() {
for (transform, target, entity) in units.iter()
{
let req = PathRequest {
entity,
from: HexCoord::from_world_pos(transform.translation),
};
if let Some(group) = groups.get_mut(&target.0) {
if let Some(group) = groups.get_mut(&target.0)
{
group.push(req);
} else {
}
else
{
groups.insert(target.0, vec![req]);
}
}
@@ -113,11 +133,13 @@ fn dispatch_path_requests(
drop(_group_span);
let pool = AsyncComputeTaskPool::get();
for (target, units) in groups {
for (target, units) in groups
{
let id = batch_id.0;
batch_id.0 += 1;
for req in &units {
for req in &units
{
commands
.entity(req.entity)
.insert(PathTaskPending(id))
@@ -136,17 +158,21 @@ fn dispatch_path_requests(
let batch_task = pool.spawn(async move {
let mut i = 0;
let mut queue = CommandQueue::default();
for entitiy_req in req.entities {
for entitiy_req in req.entities
{
let dst = req.destination[i];
i += 1;
#[cfg(feature = "tracing")]
let _path_span = info_span!("Path Finding").entered();
if let Some(path) = calculate_path(&entitiy_req.from, &dst, &local_nav_data) {
if let Some(path) = calculate_path(&entitiy_req.from, &dst, &local_nav_data)
{
queue.push(move |world: &mut World| {
let mut unit_e = world.entity_mut(entitiy_req.entity);
if let Some(pending_task) = unit_e.get::<PathTaskPending>() {
if pending_task.0 == id {
if let Some(pending_task) = unit_e.get::<PathTaskPending>()
{
if pending_task.0 == id
{
unit_e.insert(path);
unit_e.remove::<PathTaskPending>();
}
@@ -154,7 +180,8 @@ fn dispatch_path_requests(
});
}
}
if queue.is_empty() {
if queue.is_empty()
{
return None;
}
return Some(queue);
@@ -163,26 +190,36 @@ fn dispatch_path_requests(
}
}
fn get_end_points(coord: &HexCoord, count: usize, map: &Map) -> Vec<HexCoord> {
fn get_end_points(coord: &HexCoord, count: usize, map: &Map) -> Vec<HexCoord>
{
let mut result = Vec::with_capacity(count);
if count == 1 {
if count == 1
{
return vec![*coord];
}
result.push(*coord);
let mut r = 1;
while result.len() < count {
while result.len() < count
{
let tiles = HexCoord::select_ring(coord, r);
let needed = count - result.len();
if needed >= tiles.len() {
for t in tiles {
if map.is_in_bounds(&t) {
if needed >= tiles.len()
{
for t in tiles
{
if map.is_in_bounds(&t)
{
result.push(t);
}
}
} else {
for i in 0..needed {
}
else
{
for i in 0..needed
{
let t = tiles[i];
if map.is_in_bounds(&t) {
if map.is_in_bounds(&t)
{
result.push(t);
}
}
@@ -193,10 +230,14 @@ fn get_end_points(coord: &HexCoord, count: usize, map: &Map) -> Vec<HexCoord> {
return result;
}
fn resolve_path_task(mut tasks: Query<(&mut PathTask, Entity)>, mut commands: Commands) {
for (mut task, entity) in tasks.iter_mut() {
if let Some(c) = futures::check_ready(&mut task.0) {
if let Some(mut queue) = c {
fn resolve_path_task(mut tasks: Query<(&mut PathTask, Entity)>, mut commands: Commands)
{
for (mut task, entity) in tasks.iter_mut()
{
if let Some(c) = futures::check_ready(&mut task.0)
{
if let Some(mut queue) = c
{
commands.append(&mut queue);
}
commands.entity(entity).despawn();
@@ -204,32 +245,38 @@ fn resolve_path_task(mut tasks: Query<(&mut PathTask, Entity)>, mut commands: Co
}
}
fn calculate_path(from: &HexCoord, to: &HexCoord, nav: &NavData) -> Option<Path> {
fn calculate_path(from: &HexCoord, to: &HexCoord, nav: &NavData) -> Option<Path>
{
let path = astar(
from,
|n| nav.get_neighbors(n),
|n| nav.get(n).calculate_heuristic(to),
|n| n == to,
);
if let Some((nodes, _cost)) = path {
if let Some((nodes, _cost)) = path
{
let result: Vec<_> = nodes.iter().map(|f| f.to_world(nav.get_height(f))).collect();
return Some(Path(result, 1));
}
return None;
}
struct PathRequest {
struct PathRequest
{
pub entity: Entity,
pub from: HexCoord,
}
struct BatchPathRequest {
struct BatchPathRequest
{
pub entities: Vec<PathRequest>,
pub destination: Vec<HexCoord>,
}
impl BatchPathRequest {
pub fn new(entities: Vec<PathRequest>, dst: Vec<HexCoord>) -> Self {
impl BatchPathRequest
{
pub fn new(entities: Vec<PathRequest>, dst: Vec<HexCoord>) -> Self
{
return Self {
destination: dst,
entities,

View File

@@ -1,4 +1,3 @@
hard_tabs = true
max_width = 120
brace_style = "AlwaysNextLine"
control_brace_style = "AlwaysNextLine"