generated from GarandPLG/rust-flake-template
Compare commits
41
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
305cc2c4a2 | ||
|
|
276646d011 | ||
|
|
c3dbfe435a | ||
|
|
8ee037ccef | ||
|
|
40dd03ed62 | ||
|
|
a94e36f646 | ||
|
|
dfe46cab96 | ||
|
|
c4313b4117 | ||
|
|
9e09aaf75e | ||
|
|
fcdbbaf74c | ||
|
|
9bc42dc898 | ||
|
|
a7aa72ba95 | ||
|
|
57df985112 | ||
|
|
00eeb97ed8 | ||
|
|
c3b76a7c2e | ||
|
|
e3c3ad96e3 | ||
|
|
ec4c68364d | ||
|
|
5b960c65ff | ||
|
|
34c5e448ce | ||
|
|
291670fad3 | ||
|
|
d2f5968861 | ||
|
|
7c9e3d28f0 | ||
|
|
e4728aab5d | ||
|
|
a7d8823bf2 | ||
|
|
f554fa2efc | ||
|
|
a3bb9dc6f1 | ||
|
|
d87316e4fe | ||
|
|
19f4417172 | ||
|
|
cd1822d05e | ||
|
|
b66c1d8e25 | ||
|
|
41020708e4 | ||
|
|
644d8648b2 | ||
|
|
1c0f27f115 | ||
|
|
a1d0a84885 | ||
|
|
35209dd064 | ||
|
|
b1183175be | ||
|
|
55a260755c | ||
|
|
766b65b2fc | ||
|
|
1daa9802ed | ||
|
|
5b03f54efb | ||
|
|
e4705fa32a |
No files matched your search
@@ -0,0 +1,23 @@
|
||||
name: Nix package
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
container: node:22-bookworm
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: https://github.com/actions/checkout@v4
|
||||
- name: Install runner prerequisites
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install -y --no-install-recommends sudo
|
||||
- name: Install Nix
|
||||
uses: https://github.com/cachix/install-nix-action@v31
|
||||
with:
|
||||
enable_kvm: false
|
||||
- name: Build default.nix package
|
||||
run: nix build .#default --no-update-lock-file --no-link --print-out-paths --print-build-logs
|
||||
@@ -0,0 +1,109 @@
|
||||
name: Linux package builds (test)
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
linux:
|
||||
runs-on: ubuntu-latest
|
||||
container: node:22-bookworm
|
||||
env:
|
||||
CARGO_HOME: /root/.cargo
|
||||
RUSTUP_HOME: /root/.rustup
|
||||
RUST_VERSION: 1.99.1
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: https://github.com/actions/checkout@v4
|
||||
- name: Install packaging dependencies
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install -y --no-install-recommends ca-certificates curl build-essential pkg-config libasound2-dev python3 rpm cpio desktop-file-utils squashfs-tools p7zip-full file flatpak ostree zstd
|
||||
- name: Rust toolchain cache
|
||||
uses: https://github.com/actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
/root/.rustup
|
||||
/root/.cargo/bin
|
||||
key: rust-toolchain-v1-linux-x64-${{ env.RUST_VERSION }}
|
||||
- name: Install Rust
|
||||
run: |
|
||||
if [ ! -x "$CARGO_HOME/bin/rustup" ]; then
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs -o /tmp/rustup-init.sh
|
||||
sh /tmp/rustup-init.sh -y --profile minimal --default-toolchain none --no-modify-path
|
||||
fi
|
||||
"$CARGO_HOME/bin/rustup" toolchain install "$RUST_VERSION" --profile minimal --component rustfmt --component clippy
|
||||
"$CARGO_HOME/bin/rustup" default "$RUST_VERSION"
|
||||
echo "$CARGO_HOME/bin" >> "$GITHUB_PATH"
|
||||
- name: Cargo cache
|
||||
uses: https://github.com/actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
/root/.cargo/registry
|
||||
/root/.cargo/git
|
||||
target
|
||||
key: packages-v1-linux-x64-${{ env.RUST_VERSION }}-${{ hashFiles('Cargo.lock') }}-${{ hashFiles('Cargo.toml', 'src/**', 'tests/**') }}
|
||||
restore-keys: |
|
||||
packages-v1-linux-x64-${{ env.RUST_VERSION }}-${{ hashFiles('Cargo.lock') }}-
|
||||
- name: Compile Linux binary
|
||||
id: compile
|
||||
run: cargo build --release --locked
|
||||
- name: Build deb
|
||||
if: ${{ !cancelled() && steps.compile.outcome == 'success' }}
|
||||
run: bash packaging/build.sh deb
|
||||
- name: Build rpm
|
||||
if: ${{ !cancelled() && steps.compile.outcome == 'success' }}
|
||||
run: bash packaging/build.sh rpm
|
||||
- name: Build AppImage
|
||||
if: ${{ !cancelled() && steps.compile.outcome == 'success' }}
|
||||
run: bash packaging/build.sh appimage
|
||||
- name: Build Flatpak
|
||||
if: ${{ !cancelled() && steps.compile.outcome == 'success' }}
|
||||
run: bash packaging/build.sh flatpak
|
||||
- name: Save test packages
|
||||
if: ${{ !cancelled() }}
|
||||
uses: https://github.com/actions/upload-artifact@v3
|
||||
with:
|
||||
name: linux-packages-x86_64
|
||||
path: dist/*
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
|
||||
aur:
|
||||
runs-on: ubuntu-latest
|
||||
container: archlinux:base-devel
|
||||
steps:
|
||||
- name: Install Arch dependencies
|
||||
run: |
|
||||
pacman -Syu --noconfirm --needed git nodejs rust cargo pkgconf alsa-lib python zstd
|
||||
node --version
|
||||
- name: Checkout
|
||||
uses: https://github.com/actions/checkout@v4
|
||||
- name: Build AUR recipe from this commit
|
||||
run: |
|
||||
version=$(python -c 'import tomllib; print(tomllib.load(open("Cargo.toml", "rb"))["package"]["version"])')
|
||||
recipe_version=$(bash -c 'source packaging/aur/PKGBUILD; printf "%s" "$pkgver"')
|
||||
test "$version" = "$recipe_version"
|
||||
mkdir -p /tmp/aur-build
|
||||
cp packaging/aur/PKGBUILD /tmp/aur-build/
|
||||
tar --exclude=.git --exclude=target --exclude=dist --transform="s,^\./,war-in-tunnels-$version/," -czf "/tmp/aur-build/war-in-tunnels-$version.tar.gz" .
|
||||
cd /tmp/aur-build
|
||||
checksum=$(sha256sum "war-in-tunnels-$version.tar.gz" | cut -d ' ' -f 1)
|
||||
sed -i "s/sha256sums=('SKIP')/sha256sums=('$checksum')/" PKGBUILD
|
||||
useradd -m builder
|
||||
chown -R builder:builder /tmp/aur-build
|
||||
runuser -u builder -- bash -c 'cd /tmp/aur-build && makepkg --cleanbuild --noconfirm && makepkg --printsrcinfo > .SRCINFO'
|
||||
pacman -U --noconfirm ./*.pkg.tar.zst
|
||||
cd /tmp
|
||||
war-in-tunnels --version
|
||||
- name: Save Arch test package and recipe
|
||||
uses: https://github.com/actions/upload-artifact@v3
|
||||
with:
|
||||
name: arch-package-x86_64
|
||||
path: |
|
||||
/tmp/aur-build/*.pkg.tar.zst
|
||||
/tmp/aur-build/PKGBUILD
|
||||
/tmp/aur-build/.SRCINFO
|
||||
/tmp/aur-build/*.tar.gz
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
@@ -0,0 +1,58 @@
|
||||
name: Rust checks
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
check:
|
||||
runs-on: ubuntu-latest
|
||||
container: node:22-bookworm
|
||||
env:
|
||||
RUST_VERSION: 1.99.1
|
||||
CARGO_HOME: /root/.cargo
|
||||
RUSTUP_HOME: /root/.rustup
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: https://github.com/actions/checkout@v4
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install -y --no-install-recommends ca-certificates curl build-essential pkg-config libasound2-dev zstd
|
||||
- name: Rust toolchain cache
|
||||
uses: https://github.com/actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
/root/.rustup
|
||||
/root/.cargo/bin
|
||||
key: rust-toolchain-v1-linux-x64-${{ env.RUST_VERSION }}
|
||||
- name: Install Rust
|
||||
run: |
|
||||
if [ ! -x "$CARGO_HOME/bin/rustup" ]; then
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs -o /tmp/rustup-init.sh
|
||||
sh /tmp/rustup-init.sh -y --profile minimal --default-toolchain none --no-modify-path
|
||||
fi
|
||||
"$CARGO_HOME/bin/rustup" toolchain install "$RUST_VERSION" --profile minimal --component rustfmt --component clippy
|
||||
"$CARGO_HOME/bin/rustup" default "$RUST_VERSION"
|
||||
echo "$CARGO_HOME/bin" >> "$GITHUB_PATH"
|
||||
- name: Cargo cache
|
||||
uses: https://github.com/actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
/root/.cargo/registry
|
||||
/root/.cargo/git
|
||||
target
|
||||
key: cargo-v1-linux-x64-${{ env.RUST_VERSION }}-${{ hashFiles('Cargo.lock') }}-${{ hashFiles('Cargo.toml', 'src/**', 'tests/**') }}
|
||||
restore-keys: |
|
||||
cargo-v1-linux-x64-${{ env.RUST_VERSION }}-${{ hashFiles('Cargo.lock') }}-
|
||||
cargo-v1-linux-x64-${{ env.RUST_VERSION }}-
|
||||
- name: Formatting
|
||||
run: cargo fmt --all -- --check
|
||||
- name: Compilation
|
||||
run: cargo check --all-targets --locked
|
||||
- name: Tests
|
||||
run: cargo test --all-targets --locked
|
||||
- name: Release tests
|
||||
run: cargo test --all-targets --release --locked
|
||||
- name: Clippy
|
||||
run: cargo clippy --all-targets --locked
|
||||
@@ -1,4 +1,5 @@
|
||||
/target
|
||||
/result
|
||||
/dist
|
||||
|
||||
war_in_tunnels.log
|
||||
Generated
+211
-198
@@ -24,7 +24,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "812947049edcd670a82cd5c73c3661d2e58468577ba8489de58e1a73c04cbd5d"
|
||||
dependencies = [
|
||||
"alsa-sys",
|
||||
"bitflags 2.11.0",
|
||||
"bitflags 2.13.2",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
]
|
||||
@@ -56,9 +56,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.13"
|
||||
version = "1.0.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
|
||||
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
@@ -75,7 +75,7 @@ version = "1.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -86,7 +86,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"once_cell_polyfill",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -95,6 +95,15 @@ version = "1.0.102"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
|
||||
|
||||
[[package]]
|
||||
name = "approx"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arrayvec"
|
||||
version = "0.7.6"
|
||||
@@ -145,9 +154,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.11.0"
|
||||
version = "2.13.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af"
|
||||
checksum = "3ded4057c258ba199e2d26386d3af3780957ecaee6c4ef4041c6b4b8b97c0b06"
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
@@ -173,6 +182,12 @@ version = "3.20.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
|
||||
|
||||
[[package]]
|
||||
name = "by_address"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06"
|
||||
|
||||
[[package]]
|
||||
name = "bytemuck"
|
||||
version = "1.25.0"
|
||||
@@ -194,12 +209,6 @@ dependencies = [
|
||||
"rustversion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cesu8"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
@@ -225,9 +234,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.6.0"
|
||||
version = "4.6.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351"
|
||||
checksum = "aa8876b300ab35ba921adea3dfd70157a46249b33f95c9084ae5709785478946"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
@@ -235,9 +244,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.6.0"
|
||||
version = "4.6.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
|
||||
checksum = "ec0797fb7aeb1406c84efac526901f7ec3ead2124f946b494e72879d4b54704d"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@@ -247,14 +256,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.6.0"
|
||||
version = "4.6.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a"
|
||||
checksum = "f9c751b79415d4e559e3d1fcf128e09e720eb673a06d26cf6f392d37d75b66e0"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
"syn 3.0.9",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -304,11 +313,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "coreaudio-rs"
|
||||
version = "0.14.1"
|
||||
version = "0.14.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "16dd574a72a021b90c7656c474ea31d11a2f0366a8eff574186e761e0b9e3586"
|
||||
checksum = "7d5d7dca3ebcf65a035582c9ad4385371a9d9ee6537474d2a278f4e1e475bb58"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"bitflags 2.13.2",
|
||||
"libc",
|
||||
"objc2-audio-toolbox",
|
||||
"objc2-core-audio",
|
||||
@@ -318,11 +327,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "cpal"
|
||||
version = "0.17.3"
|
||||
version = "0.18.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8942da362c0f0d895d7cac616263f2f9424edc5687364dfd1d25ef7eba506d7"
|
||||
checksum = "6f02e8d0327b42d3e2e4ab2119af397344eb9fc54a34bf0ddeaa1277af8681f1"
|
||||
dependencies = [
|
||||
"alsa",
|
||||
"block2",
|
||||
"coreaudio-rs",
|
||||
"dasp_sample",
|
||||
"jni",
|
||||
@@ -340,10 +350,9 @@ dependencies = [
|
||||
"objc2-core-audio-types",
|
||||
"objc2-core-foundation",
|
||||
"objc2-foundation",
|
||||
"wasm-bindgen",
|
||||
"wasm-bindgen-futures",
|
||||
"web-sys",
|
||||
"windows",
|
||||
"windows-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -364,13 +373,19 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "critical-section"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b"
|
||||
|
||||
[[package]]
|
||||
name = "crossterm"
|
||||
version = "0.29.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"bitflags 2.13.2",
|
||||
"crossterm_winapi",
|
||||
"derive_more",
|
||||
"document-features",
|
||||
@@ -504,7 +519,7 @@ version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"bitflags 2.13.2",
|
||||
"objc2",
|
||||
]
|
||||
|
||||
@@ -545,7 +560,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -614,30 +629,6 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
|
||||
|
||||
[[package]]
|
||||
name = "futures-core"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
|
||||
|
||||
[[package]]
|
||||
name = "futures-task"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-task",
|
||||
"pin-project-lite",
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
@@ -694,6 +685,17 @@ dependencies = [
|
||||
"foldhash 0.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||
dependencies = [
|
||||
"allocator-api2",
|
||||
"equivalent",
|
||||
"foldhash 0.2.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
@@ -775,18 +777,32 @@ checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
|
||||
|
||||
[[package]]
|
||||
name = "jni"
|
||||
version = "0.21.1"
|
||||
version = "0.22.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97"
|
||||
checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498"
|
||||
dependencies = [
|
||||
"cesu8",
|
||||
"cfg-if",
|
||||
"combine",
|
||||
"jni-sys 0.3.1",
|
||||
"jni-macros",
|
||||
"jni-sys 0.4.1",
|
||||
"log",
|
||||
"thiserror 1.0.69",
|
||||
"simd_cesu8",
|
||||
"thiserror 2.0.18",
|
||||
"walkdir",
|
||||
"windows-sys 0.45.0",
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jni-macros"
|
||||
version = "0.22.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"rustc_version",
|
||||
"simd_cesu8",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -823,8 +839,6 @@ version = "0.3.94"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2e04e2ef80ce82e13552136fabeef8a5ed1f985a96805761cbb9a2c34e7664d9"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"futures-util",
|
||||
"once_cell",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
@@ -876,7 +890,7 @@ version = "0.3.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5f4de44e98ddbf09375cbf4d17714d18f39195f4f4894e8524501726fd9a8a4a"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"bitflags 2.13.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -902,17 +916,17 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "log"
|
||||
version = "0.4.29"
|
||||
version = "0.4.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||
checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
|
||||
|
||||
[[package]]
|
||||
name = "lru"
|
||||
version = "0.16.3"
|
||||
version = "0.18.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593"
|
||||
checksum = "ef9ac18847474e638e3702b76c65d4eb93428471a74778ef0f1be711717f89b5"
|
||||
dependencies = [
|
||||
"hashbrown 0.16.1",
|
||||
"hashbrown 0.17.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -927,12 +941,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mach2"
|
||||
version = "0.5.0"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6a1b95cd5421ec55b445b5ae102f5ea0e768de1f82bd3001e11f426c269c3aea"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
checksum = "dae608c151f68243f2b000364e1f7b186d9c29845f7d2d85bd31b9ad77ad552b"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
@@ -970,7 +981,7 @@ dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"wasi",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -979,7 +990,7 @@ version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3f42e7bbe13d351b6bead8286a43aac9534b82bd3cc43e47037f012ebfd62d4"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"bitflags 2.13.2",
|
||||
"jni-sys 0.3.1",
|
||||
"log",
|
||||
"ndk-sys",
|
||||
@@ -1008,7 +1019,7 @@ version = "0.29.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"bitflags 2.13.2",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
@@ -1128,7 +1139,7 @@ version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6948501a91121d6399b79abaa33a8aa4ea7857fe019f341b8c23ad6e81b79b08"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"bitflags 2.13.2",
|
||||
"libc",
|
||||
"objc2",
|
||||
"objc2-core-audio",
|
||||
@@ -1143,6 +1154,7 @@ version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13a380031deed8e99db00065c45937da434ca987c034e13b87e4441f9e4090be"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"objc2",
|
||||
"objc2-foundation",
|
||||
]
|
||||
@@ -1166,7 +1178,7 @@ version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5a89f2ec274a0cf4a32642b2991e8b351a404d290da87bb6a9a9d8632490bd1c"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"bitflags 2.13.2",
|
||||
"objc2",
|
||||
]
|
||||
|
||||
@@ -1176,7 +1188,7 @@ version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"bitflags 2.13.2",
|
||||
"block2",
|
||||
"dispatch2",
|
||||
"libc",
|
||||
@@ -1195,7 +1207,7 @@ version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3e0adef53c21f888deb4fa59fc59f7eb17404926ee8a6f59f5df0fd7f9f3272"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"bitflags 2.13.2",
|
||||
"block2",
|
||||
"libc",
|
||||
"objc2",
|
||||
@@ -1223,6 +1235,39 @@ dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "palette"
|
||||
version = "0.7.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ddeed8580d347d2abf3dcf06a5f0b3dc020258338526b277847cd4248a70fc64"
|
||||
dependencies = [
|
||||
"approx",
|
||||
"libm",
|
||||
"palette_derive",
|
||||
"palette_math",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "palette_derive"
|
||||
version = "0.7.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "88537020289b719d81be994ccf1bbf4990f477e2f69ee52fe3e45f43a02e56be"
|
||||
dependencies = [
|
||||
"by_address",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.117",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "palette_math"
|
||||
version = "0.7.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e6eb142958d64335fb0e345c5b9ead2ecd6fc438c307e9d7d3c4fd428dbaf12"
|
||||
dependencies = [
|
||||
"libm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.12.5"
|
||||
@@ -1341,12 +1386,6 @@ dependencies = [
|
||||
"siphasher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project-lite"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.32"
|
||||
@@ -1452,31 +1491,35 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ratatui"
|
||||
version = "0.30.0"
|
||||
version = "0.30.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d1ce67fb8ba4446454d1c8dbaeda0557ff5e94d39d5e5ed7f10a65eb4c8266bc"
|
||||
checksum = "3274ba0a2c5e1bcad2a2005d20f4dc59dad26b2eb0940fb094500dba4099d57d"
|
||||
dependencies = [
|
||||
"instability",
|
||||
"ratatui-core",
|
||||
"ratatui-crossterm",
|
||||
"ratatui-macros",
|
||||
"ratatui-termina",
|
||||
"ratatui-termwiz",
|
||||
"ratatui-widgets",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ratatui-core"
|
||||
version = "0.1.0"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ef8dea09a92caaf73bff7adb70b76162e5937524058a7e5bff37869cbbec293"
|
||||
checksum = "cbb175c433c8e28a809d1f5773a2ae96e68c0ce40db865cbab1020bf33ae479c"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"bitflags 2.13.2",
|
||||
"compact_str",
|
||||
"hashbrown 0.16.1",
|
||||
"indoc",
|
||||
"critical-section",
|
||||
"hashbrown 0.17.1",
|
||||
"itertools",
|
||||
"kasuari",
|
||||
"lru",
|
||||
"palette",
|
||||
"serde",
|
||||
"strum",
|
||||
"thiserror 2.0.18",
|
||||
"unicode-segmentation",
|
||||
@@ -1486,9 +1529,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ratatui-crossterm"
|
||||
version = "0.1.0"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "577c9b9f652b4c121fb25c6a391dd06406d3b092ba68827e6d2f09550edc54b3"
|
||||
checksum = "567584a3b0e6a8203c23de40b4861497266725eb5363dbfd18a1edd603cca9f0"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"crossterm",
|
||||
@@ -1498,19 +1541,30 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ratatui-macros"
|
||||
version = "0.7.0"
|
||||
version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7f1342a13e83e4bb9d0b793d0ea762be633f9582048c892ae9041ef39c936f4"
|
||||
checksum = "ed7dc68daa7498a43e4d68e0eb078427e10c38fbcfbb1e42d955f1fa2140d814"
|
||||
dependencies = [
|
||||
"ratatui-core",
|
||||
"ratatui-widgets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ratatui-termwiz"
|
||||
name = "ratatui-termina"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0f76fe0bd0ed4295f0321b1676732e2454024c15a35d01904ddb315afd3d545c"
|
||||
checksum = "c0bf912d9e66f057a759d92e386a280ea886b352ab757d6ac4d653c7ed2c43c2"
|
||||
dependencies = [
|
||||
"instability",
|
||||
"ratatui-core",
|
||||
"termina",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ratatui-termwiz"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "faf03e0380b7744054d6cb74224fe3adf062a029754933f575ca1e3b4c2ce977"
|
||||
dependencies = [
|
||||
"ratatui-core",
|
||||
"termwiz",
|
||||
@@ -1518,17 +1572,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "ratatui-widgets"
|
||||
version = "0.3.0"
|
||||
version = "0.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d7dbfa023cd4e604c2553483820c5fe8aa9d71a42eea5aa77c6e7f35756612db"
|
||||
checksum = "66e3d19bcc9130ca376277d93b60767ff121ace3be06f5f95f81dd68956407d1"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"hashbrown 0.16.1",
|
||||
"bitflags 2.13.2",
|
||||
"hashbrown 0.17.1",
|
||||
"indoc",
|
||||
"instability",
|
||||
"itertools",
|
||||
"line-clipping",
|
||||
"ratatui-core",
|
||||
"serde",
|
||||
"strum",
|
||||
"time",
|
||||
"unicode-segmentation",
|
||||
@@ -1541,7 +1596,7 @@ version = "0.5.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"bitflags 2.13.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1575,9 +1630,9 @@ checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
|
||||
|
||||
[[package]]
|
||||
name = "rodio"
|
||||
version = "0.22.2"
|
||||
version = "0.23.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d0a536bb79db59098ef71a4dd4246c02eb87b316deceb1b68e0cde7167ec01eb"
|
||||
checksum = "36e3ee0a5ca418bac59a15d5506e498ab138359edbb67257a6b8a3b93d411f39"
|
||||
dependencies = [
|
||||
"cpal",
|
||||
"dasp_sample",
|
||||
@@ -1610,11 +1665,11 @@ version = "1.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"bitflags 2.13.2",
|
||||
"errno",
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1735,6 +1790,22 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simd_cesu8"
|
||||
version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "11031e251abf8611c80f460e19dbdeb54a66db918e49c65a7065b46ac7aec520"
|
||||
dependencies = [
|
||||
"rustc_version",
|
||||
"simdutf8",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "simdutf8"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e"
|
||||
|
||||
[[package]]
|
||||
name = "simplelog"
|
||||
version = "0.12.2"
|
||||
@@ -1752,12 +1823,6 @@ version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e"
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.15.1"
|
||||
@@ -1778,18 +1843,18 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "strum"
|
||||
version = "0.27.2"
|
||||
version = "0.28.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf"
|
||||
checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd"
|
||||
dependencies = [
|
||||
"strum_macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strum_macros"
|
||||
version = "0.27.2"
|
||||
version = "0.28.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7"
|
||||
checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
@@ -1966,6 +2031,17 @@ dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "3.0.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d78c8dee4c7bf0e14673097256fed6142ce9d3b85a408189d07482442145823b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "termcolor"
|
||||
version = "1.4.1"
|
||||
@@ -1975,6 +2051,19 @@ dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "termina"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9048a889effe34a5cddee0af7f53285198b16dca3be510858d38dfdb3e62a04e"
|
||||
dependencies = [
|
||||
"bitflags 2.13.2",
|
||||
"parking_lot",
|
||||
"rustix",
|
||||
"signal-hook",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "terminfo"
|
||||
version = "0.9.0"
|
||||
@@ -2004,7 +2093,7 @@ checksum = "4676b37242ccbd1aabf56edb093a4827dc49086c0ffd764a5705899e0f35f8f7"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64",
|
||||
"bitflags 2.11.0",
|
||||
"bitflags 2.13.2",
|
||||
"fancy-regex",
|
||||
"filedescriptor",
|
||||
"finl_unicode",
|
||||
@@ -2280,16 +2369,6 @@ dependencies = [
|
||||
"wasm-bindgen-shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-futures"
|
||||
version = "0.4.67"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "03623de6905b7206edd0a75f69f747f134b7f0a2323392d664448bf2d3c5d87e"
|
||||
dependencies = [
|
||||
"js-sys",
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasm-bindgen-macro"
|
||||
version = "0.2.117"
|
||||
@@ -2350,7 +2429,7 @@ version = "0.244.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
|
||||
dependencies = [
|
||||
"bitflags 2.11.0",
|
||||
"bitflags 2.13.2",
|
||||
"hashbrown 0.15.5",
|
||||
"indexmap",
|
||||
"semver",
|
||||
@@ -2460,7 +2539,7 @@ version = "0.1.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||
dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2570,15 +2649,6 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.45.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
|
||||
dependencies = [
|
||||
"windows-targets",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
@@ -2588,21 +2658,6 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-targets"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
|
||||
dependencies = [
|
||||
"windows_aarch64_gnullvm",
|
||||
"windows_aarch64_msvc",
|
||||
"windows_i686_gnu",
|
||||
"windows_i686_msvc",
|
||||
"windows_x86_64_gnu",
|
||||
"windows_x86_64_gnullvm",
|
||||
"windows_x86_64_msvc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows-threading"
|
||||
version = "0.2.1"
|
||||
@@ -2612,48 +2667,6 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_gnullvm"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
|
||||
|
||||
[[package]]
|
||||
name = "windows_aarch64_msvc"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_gnu"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
|
||||
|
||||
[[package]]
|
||||
name = "windows_i686_msvc"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnu"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_gnullvm"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
|
||||
|
||||
[[package]]
|
||||
name = "windows_x86_64_msvc"
|
||||
version = "0.42.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
|
||||
|
||||
[[package]]
|
||||
name = "winnow"
|
||||
version = "1.0.1"
|
||||
@@ -2721,7 +2734,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags 2.11.0",
|
||||
"bitflags 2.13.2",
|
||||
"indexmap",
|
||||
"log",
|
||||
"serde",
|
||||
|
||||
+14
-4
@@ -2,17 +2,27 @@
|
||||
name = "war_in_tunnels"
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
rust-version = "1.88"
|
||||
license = "MIT"
|
||||
|
||||
[lints.rust]
|
||||
unused_must_use = "deny"
|
||||
unsafe_code = "forbid"
|
||||
|
||||
[lints.clippy]
|
||||
unused_unit = "allow"
|
||||
|
||||
[profile.release]
|
||||
codegen-units = 1
|
||||
lto = true
|
||||
opt-level = 3
|
||||
strip = true
|
||||
overflow-checks = true
|
||||
|
||||
[dependencies]
|
||||
clap = { version = "4.5.53", features = ["derive"] }
|
||||
ratatui = "0.30.0"
|
||||
log = "0.4.29"
|
||||
clap = { version = "4.6.7", features = ["derive"] }
|
||||
ratatui = "0.30.2"
|
||||
log = "0.4.34"
|
||||
simplelog = "0.12.2"
|
||||
rodio = "0.22.2"
|
||||
rodio = "0.23.0"
|
||||
once_cell = "1.21.4"
|
||||
@@ -1,121 +1,21 @@
|
||||
Creative Commons Legal Code
|
||||
MIT License
|
||||
|
||||
CC0 1.0 Universal
|
||||
Copyright (c) 2026 GarandPLG
|
||||
|
||||
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
|
||||
LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
|
||||
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
|
||||
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
|
||||
REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
|
||||
PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
|
||||
THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
|
||||
HEREUNDER.
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
Statement of Purpose
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
The laws of most jurisdictions throughout the world automatically confer
|
||||
exclusive Copyright and Related Rights (defined below) upon the creator
|
||||
and subsequent owner(s) (each and all, an "owner") of an original work of
|
||||
authorship and/or a database (each, a "Work").
|
||||
|
||||
Certain owners wish to permanently relinquish those rights to a Work for
|
||||
the purpose of contributing to a commons of creative, cultural and
|
||||
scientific works ("Commons") that the public can reliably and without fear
|
||||
of later claims of infringement build upon, modify, incorporate in other
|
||||
works, reuse and redistribute as freely as possible in any form whatsoever
|
||||
and for any purposes, including without limitation commercial purposes.
|
||||
These owners may contribute to the Commons to promote the ideal of a free
|
||||
culture and the further production of creative, cultural and scientific
|
||||
works, or to gain reputation or greater distribution for their Work in
|
||||
part through the use and efforts of others.
|
||||
|
||||
For these and/or other purposes and motivations, and without any
|
||||
expectation of additional consideration or compensation, the person
|
||||
associating CC0 with a Work (the "Affirmer"), to the extent that he or she
|
||||
is an owner of Copyright and Related Rights in the Work, voluntarily
|
||||
elects to apply CC0 to the Work and publicly distribute the Work under its
|
||||
terms, with knowledge of his or her Copyright and Related Rights in the
|
||||
Work and the meaning and intended legal effect of CC0 on those rights.
|
||||
|
||||
1. Copyright and Related Rights. A Work made available under CC0 may be
|
||||
protected by copyright and related or neighboring rights ("Copyright and
|
||||
Related Rights"). Copyright and Related Rights include, but are not
|
||||
limited to, the following:
|
||||
|
||||
i. the right to reproduce, adapt, distribute, perform, display,
|
||||
communicate, and translate a Work;
|
||||
ii. moral rights retained by the original author(s) and/or performer(s);
|
||||
iii. publicity and privacy rights pertaining to a person's image or
|
||||
likeness depicted in a Work;
|
||||
iv. rights protecting against unfair competition in regards to a Work,
|
||||
subject to the limitations in paragraph 4(a), below;
|
||||
v. rights protecting the extraction, dissemination, use and reuse of data
|
||||
in a Work;
|
||||
vi. database rights (such as those arising under Directive 96/9/EC of the
|
||||
European Parliament and of the Council of 11 March 1996 on the legal
|
||||
protection of databases, and under any national implementation
|
||||
thereof, including any amended or successor version of such
|
||||
directive); and
|
||||
vii. other similar, equivalent or corresponding rights throughout the
|
||||
world based on applicable law or treaty, and any national
|
||||
implementations thereof.
|
||||
|
||||
2. Waiver. To the greatest extent permitted by, but not in contravention
|
||||
of, applicable law, Affirmer hereby overtly, fully, permanently,
|
||||
irrevocably and unconditionally waives, abandons, and surrenders all of
|
||||
Affirmer's Copyright and Related Rights and associated claims and causes
|
||||
of action, whether now known or unknown (including existing as well as
|
||||
future claims and causes of action), in the Work (i) in all territories
|
||||
worldwide, (ii) for the maximum duration provided by applicable law or
|
||||
treaty (including future time extensions), (iii) in any current or future
|
||||
medium and for any number of copies, and (iv) for any purpose whatsoever,
|
||||
including without limitation commercial, advertising or promotional
|
||||
purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
|
||||
member of the public at large and to the detriment of Affirmer's heirs and
|
||||
successors, fully intending that such Waiver shall not be subject to
|
||||
revocation, rescission, cancellation, termination, or any other legal or
|
||||
equitable action to disrupt the quiet enjoyment of the Work by the public
|
||||
as contemplated by Affirmer's express Statement of Purpose.
|
||||
|
||||
3. Public License Fallback. Should any part of the Waiver for any reason
|
||||
be judged legally invalid or ineffective under applicable law, then the
|
||||
Waiver shall be preserved to the maximum extent permitted taking into
|
||||
account Affirmer's express Statement of Purpose. In addition, to the
|
||||
extent the Waiver is so judged Affirmer hereby grants to each affected
|
||||
person a royalty-free, non transferable, non sublicensable, non exclusive,
|
||||
irrevocable and unconditional license to exercise Affirmer's Copyright and
|
||||
Related Rights in the Work (i) in all territories worldwide, (ii) for the
|
||||
maximum duration provided by applicable law or treaty (including future
|
||||
time extensions), (iii) in any current or future medium and for any number
|
||||
of copies, and (iv) for any purpose whatsoever, including without
|
||||
limitation commercial, advertising or promotional purposes (the
|
||||
"License"). The License shall be deemed effective as of the date CC0 was
|
||||
applied by Affirmer to the Work. Should any part of the License for any
|
||||
reason be judged legally invalid or ineffective under applicable law, such
|
||||
partial invalidity or ineffectiveness shall not invalidate the remainder
|
||||
of the License, and in such case Affirmer hereby affirms that he or she
|
||||
will not (i) exercise any of his or her remaining Copyright and Related
|
||||
Rights in the Work or (ii) assert any associated claims and causes of
|
||||
action with respect to the Work, in either case contrary to Affirmer's
|
||||
express Statement of Purpose.
|
||||
|
||||
4. Limitations and Disclaimers.
|
||||
|
||||
a. No trademark or patent rights held by Affirmer are waived, abandoned,
|
||||
surrendered, licensed or otherwise affected by this document.
|
||||
b. Affirmer offers the Work as-is and makes no representations or
|
||||
warranties of any kind concerning the Work, express, implied,
|
||||
statutory or otherwise, including without limitation warranties of
|
||||
title, merchantability, fitness for a particular purpose, non
|
||||
infringement, or the absence of latent or other defects, accuracy, or
|
||||
the present or absence of errors, whether or not discoverable, all to
|
||||
the greatest extent permissible under applicable law.
|
||||
c. Affirmer disclaims responsibility for clearing rights of other persons
|
||||
that may apply to the Work or any use thereof, including without
|
||||
limitation any person's Copyright and Related Rights in the Work.
|
||||
Further, Affirmer disclaims responsibility for obtaining any necessary
|
||||
consents, permissions or other rights required for any use of the
|
||||
Work.
|
||||
d. Affirmer understands and acknowledges that Creative Commons is not a
|
||||
party to this document and has no duty or obligation with respect to
|
||||
this CC0 or use of the Work.
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,3 +1,121 @@
|
||||
# rust-flake-template
|
||||
# War in Tunnels
|
||||
|
||||
Basic flake for rust development and production.
|
||||
**War in Tunnels** – to terminalowa gra typu RTS (real‑time strategy) rozgrywająca się pod ziemią. Gra wykorzystuje bibliotekę **ratatui** do budowy interfejsu tekstowego oraz **rodio** do odtwarzania ścieżek dźwiękowych. Projekt jest skonfigurowany jako **Nix Flake**, co ułatwia zarządzanie zależnościami, budowanie wersji produkcyjnej oraz deweloperskiej.
|
||||
|
||||
## Funkcjonalności
|
||||
|
||||
- Tekstowy interfejs użytkownika z podziałem na menu, podgląd mapy i panel boczny.
|
||||
- Różne tryby gry (np. Last‑Man‑Standing).
|
||||
- Skalowanie mapy i poziom przybliżenia UI.
|
||||
- Wsparcie dla kilku zestawów perków.
|
||||
- Konfigurowalne zasoby startowe (drewno, żelazo) oraz limit jednostek.
|
||||
- System punktów doświadczenia i limitu punktów umiejętności.
|
||||
- Odtwarzanie ścieżek dźwiękowych (możliwość wyciszenia lub wyłączenia).
|
||||
|
||||
## Wymagania
|
||||
|
||||
- **Nix** (z obsługą Flake) – do budowania i uruchamiania projektu.
|
||||
- **Rust ≥ 1.88**, edycja 2024 – używany wewnątrz środowiska Nix (fenix toolchain).
|
||||
- Biblioteki systemowe: `alsa-lib` (dla obsługi dźwięku).
|
||||
|
||||
## Budowanie i uruchamianie
|
||||
|
||||
Projekt udostępnia dwa profile Flake:
|
||||
|
||||
- **default** – wersja produkcyjna (z optymalizacjami).
|
||||
- **develop** – wersja deweloperska z dodatkowymi zależnościami i nie‑zoptymalizowanym kodem.
|
||||
|
||||
### Przykładowe polecenia
|
||||
|
||||
```bash
|
||||
# Budowanie wersji produkcyjnej
|
||||
nix build
|
||||
|
||||
# Uruchomienie wersji produkcyjnej
|
||||
nix run
|
||||
|
||||
# Budowanie wersji deweloperskiej
|
||||
nix build .#develop
|
||||
|
||||
# Uruchomienie wersji deweloperskiej
|
||||
nix run .#develop
|
||||
```
|
||||
|
||||
W środowisku deweloperskim dostępny jest także **shell** z pre‑konfigurowanymi zmiennymi:
|
||||
|
||||
```bash
|
||||
nix develop
|
||||
```
|
||||
|
||||
Po wejściu do powłoki pojawi się informacja o wersji oraz dostępne komendy.
|
||||
|
||||
## Opcje wiersza poleceń (CLI)
|
||||
|
||||
Gra korzysta z biblioteki **clap** – wszystkie opcje są dostępne jako flagi. Poniżej najważniejsze:
|
||||
|
||||
| Skrót | Flaga | Domyślna wartość | Opis |
|
||||
|-------|-------|------------------|------|
|
||||
| `-v` | `--view` | `MainMenu` | Określa początkowy widok UI. |
|
||||
| `-u` | `--username` | `Player` | Nazwa gracza wyświetlana w interfejsie. |
|
||||
| `-g` | `--game-mode` | `LastManStanding` | Tryb rozgrywki. |
|
||||
| `-W` | `--map-width` | `42` | Szerokość generowanej mapy (36‑108). |
|
||||
| `-H` | `--map-height` | `11` | Wysokość mapy (11‑39). |
|
||||
| `-z` | `--zoom-level` | `Default` | Poziom przybliżenia UI. |
|
||||
| `-p` | `--perk-deck` | `Silesian` | Zestaw perków. |
|
||||
| `-w` | `--starting-wood` | `50` | Początkowa ilość drewna (1‑150). |
|
||||
| `-i` | `--starting-iron` | `25` | Początkowa ilość żelaza (1‑150). |
|
||||
| `-L` | `--supply-limit` | `99` | Limit jednostek na mapie (49‑149). |
|
||||
| `-x` | `--xp-modifier` | `1.0` | Mnożnik doświadczenia (0.5‑2.0). |
|
||||
| `-P` | `--skill-points-limit` | `120` | Górny limit punktów umiejętności (90‑690). |
|
||||
| `-S` | `--soundtrack` | `Default` | Wybór ścieżki dźwiękowej. |
|
||||
| `-D` | `--disable-audio` | `false` | Wyłącza odtwarzanie dźwięku. |
|
||||
| `-m` | `--mute` | `false` | Wycisza odtwarzany dźwięk. |
|
||||
| `-l` | `--log` | `false` | Włącza logowanie do pliku. |
|
||||
|
||||
Aby wyświetlić pomoc, uruchom:
|
||||
|
||||
```bash
|
||||
cargo run -- -h
|
||||
```
|
||||
|
||||
## Struktura projektu
|
||||
|
||||
- `src/main.rs` – punkt wejścia, inicjalizuje terminal, wątki i główną pętlę aplikacji.
|
||||
- `src/lib.rs` – eksportuje moduły aplikacji i CLI.
|
||||
- `src/app/` – logika gry, stany, widoki i obsługa klawiszy.
|
||||
- `src/cli.rs` – definicja opcji CLI.
|
||||
- `Cargo.toml` – zależności Rust (ratatui, clap, log, simplelog, rodio, once_cell).
|
||||
- `flake.nix` – opis flake, zależności Nix, profile build i develop.
|
||||
|
||||
## Kopanie
|
||||
|
||||
W potyczce gracz zaczyna z jednym górnikiem `Miner` w swojej bazie. Ustaw kursor na jednostce (z początkowej pozycji wystarczy strzałka w lewo), naciśnij spację, wytycz ścieżkę strzałkami i zatwierdź spacją. Backspace cofa ostatni krok, Delete anuluje rysowanie.
|
||||
|
||||
Górnik wykonuje zadania kolejno. Kamień stopniowo traci durability; po wykopaniu zmienia się w tunel, do którego jednostka wchodzi w tym samym ticku. Pełna kratka kamienia zajmuje około 6 sekund. Procent pozostałej wytrzymałości widać na uszkodzonej kratce; Tab otwiera panel z dokładną wartością dla pola pod kursorem. Przejście do istniejącego tunelu lub niezajętej bazy trwa około 3 sekund na kratkę. Znacznik jednostki ma kolor właściciela: niebieski dla gracza, czerwony dla przeciwnika.
|
||||
|
||||
Podczas przechodzenia na kratce jednostki pojawia się procent ukończenia bieżącego kroku, np. `50%`. Wskaźnik resetuje się po przejściu i znika, gdy jednostka nie podróżuje. Jest widoczny na wszystkich poziomach zoomu, w kolorze właściciela jednostki.
|
||||
|
||||
Zatwierdzona ścieżka dzieli się automatycznie na zadania `Moving` (`→`, gotowe tunele i bazy) oraz `Digging` (`⛏`, kamień). Jednostka wykonuje je kolejno; po wykopaniu kamienia wejście do nowego tunelu pozostaje częścią kopania.
|
||||
|
||||
W kolejce pod Tab każde zadanie pokazuje `Step` (postęp bieżącej kratki) oraz `Total` (postęp całego zadania według liczby kratek, z uwzględnieniem częściowo wykonanego etapu). Zadania oczekujące mają oba wskaźniki na 0%.
|
||||
|
||||
Możesz zaznaczyć pracującego górnika ponownie: rysowanie kolejnego zadania zacznie się na końcu jego dotychczasowej kolejki. Podczas rysowania wybrana jednostka wstrzymuje pracę. Zajęte pole lub złoże rudy zatrzymuje wykonywanie ścieżki; ten typ zadania kopie tylko kamień.
|
||||
|
||||
## Dźwięk
|
||||
|
||||
Ścieżki dźwiękowe znajdują się w folderze `soundtracks`. Biblioteka **rodio** odtwarza je w oddzielnym wątku. Dostępne flagi pozwalają wybrać konkretną ścieżkę, wyciszyć lub całkowicie wyłączyć dźwięk.
|
||||
|
||||
## Logowanie
|
||||
|
||||
Gdy flaga `--log` jest włączona, aplikacja zapisuje logi do pliku przy użyciu **simplelog**. Domyślnie logowanie jest wyłączone.
|
||||
|
||||
## Kontrole jakości
|
||||
|
||||
`Cargo.toml` zabrania `unsafe` we własnym kodzie i ignorowania wyników `must_use`, dopuszcza jawne `-> ()` oraz włącza kontrolę przepełnień także w release. Gitea Actions w `.gitea/workflows/rust.yml` sprawdza formatowanie, kompilację, testy debug/release i Clippy przez Nix, z zachowaniem plików lock. Uruchomienie CI wymaga włączonych Actions i runnera `ubuntu-latest`.
|
||||
|
||||
Polecenia lokalne i szczegóły środowiska opisano w [docs/TESTING.md](docs/TESTING.md).
|
||||
|
||||
## Licencja
|
||||
|
||||
Projekt jest udostępniany na licencji [MIT](LICENSE).
|
||||
Copyright (c) 2026 GarandPLG.
|
||||
+1
-1
@@ -22,7 +22,7 @@ in
|
||||
# hash = lib.fakeHash;
|
||||
# };
|
||||
|
||||
cargoHash = "sha256-Cv/sUzkiQuQS3ybg2glfXucIu+HtPi2brH/D7tqc3Mc=";
|
||||
cargoHash = "sha256-cXG/SZxmq/w2CFJiaOYtE6IpShkGY2k0Q+E0QCxgY0I=";
|
||||
|
||||
nativeBuildInputs = [pkg-config];
|
||||
buildInputs = [alsa-lib];
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
# Praca nad War in Tunnels
|
||||
|
||||
Instrukcje i notatki dla agentów przechowujemy w `docs/`. Ten plik jest punktem wejścia do kontekstu projektu; ponieważ znajduje się w `docs/`, agent rozpoczynający pracę w katalogu głównym powinien otworzyć go jawnie.
|
||||
|
||||
## Przed zmianą
|
||||
|
||||
1. Przeczytaj [mapę kodu](CODE_MAP.md) i [dziennik prac](PROGRESS.md), następnie aktualny kod zmienianej funkcji i jej wywołania (`rg`). Dokumentacja jest migawką, kod jest źródłem prawdy.
|
||||
2. Sprawdź `git status --short`. Nie nadpisuj zastanych zmian użytkownika. Przy rozpoczęciu tej analizy zmodyfikowany był `flake.lock`.
|
||||
3. Ustal zasięg: stan, obsługa wejścia, renderowanie, CLI, eksporty `mod.rs`, testy i konfiguracja. Zmieniaj tylko elementy potrzebne do zadania.
|
||||
4. Aktualizuj `docs/PROGRESS.md`: cel, konkretne zmiany, wykonane kontrole i ograniczenia. Przy zmianie architektury aktualizuj też mapę.
|
||||
|
||||
## Narzędzia i weryfikacja
|
||||
|
||||
Nie wolno zakończyć zadania, jeżeli kompilator Rust zgłasza błędy uniemożliwiające kompilację. Przed zakończeniem uruchom `nix develop -c cargo check --all-targets`; jeśli wystąpią błędy kompilacji, usuń je i powtórz kontrolę aż do uzyskania sukcesu.
|
||||
|
||||
Wszystkie narzędzia Rust uruchamiaj przez `nix develop -c` z katalogu repozytorium:
|
||||
|
||||
```sh
|
||||
nix develop -c cargo test --all-targets --locked
|
||||
nix develop -c cargo fmt --all -- --check
|
||||
nix develop -c cargo clippy --all-targets --locked
|
||||
nix develop -c cargo run -- --help
|
||||
nix develop -c cargo run -- --disable-audio --view skirmish
|
||||
```
|
||||
|
||||
Ostatnie polecenie wymaga interaktywnego terminala. `shellHook` drukuje dostępne polecenia, bez automatycznej kompilacji. Przy kontrolach używaj `--locked`, aby nie aktualizować `Cargo.lock`. Formatowanie stosuj wyłącznie do uzgodnionych zmian; nie używaj automatycznego `clippy --fix` do porządkowania całego repozytorium.
|
||||
|
||||
Aktualny zestaw obejmuje 46 testów. Stan bazowy Clippy to 50 ostrzeżeń dla biblioteki. `clippy::unused_unit` jest dozwolony ze względu na wymagane jawne `-> ()`. Zakres i polecenia opisano w [TESTING.md](TESTING.md). Nie traktuj tego jako potwierdzenia działania pełnej rozgrywki. Nowa nietrywialna logika wymaga małego testu regresji. Nie dodawaj zależności ani warstw abstrakcji bez potrzeby.
|
||||
|
||||
`Cargo.toml` wymaga Rust 1.88 lub nowszego, zabrania `unsafe` we własnym kodzie i ignorowania wyników `must_use`, a w release włącza kontrolę przepełnień. CI Gitea w `.gitea/workflows/rust.yml` sprawdza formatowanie, kompilację, testy debug/release i Clippy bez Nix, przez Rust 1.99.1 instalowany rustup; lokalnie nadal używaj Nix. Oddzielny `.gitea/workflows/nix.yml` buduje `.#default` z `default.nix`, z testami pakietu i kontrolą instalacji. Nie dodawaj `-D warnings` przed uporządkowaniem istniejących ostrzeżeń.
|
||||
|
||||
Audio wymaga ALSA i urządzenia odtwarzania; do pracy nad TUI używaj `--disable-audio`. `--mute` nadal uruchamia wątek audio. Ścieżki zasobów i pakowanie sprawdzaj osobno przy zmianach audio/Nix. Rozróżniaj lokalny build Cargo, pakiet `.#develop` i domyślny pakiet Nix.
|
||||
|
||||
`.gitea/workflows/packages.yml` testuje paczki Linux x86_64 bez Nix: deb, rpm, AppImage, Flatpak oraz przepis AUR budowany przez makepkg. Nie publikuje wydań ani tagów. Przepisy, layout i ograniczenia opisano w [PACKAGING.md](PACKAGING.md); zmiany pakowania muszą zachować muzykę obok binarki i działanie poza katalogiem repozytorium.
|
||||
|
||||
## Konwencje kodu
|
||||
|
||||
- Formatowanie określa `rustfmt.toml`: styl Rust 2024, wcięcia po 2 spacje, szerokość docelowa 100 znaków i zakończenia LF. Stosuj `nix develop -c cargo fmt --all`; kontroluj przez `nix develop -c cargo fmt --all -- --check`.
|
||||
- Jawne adnotacje typów są OBOWIĄZKOWE w nowym i zmienianym kodzie: przy zmiennych i stałych, polach struktur, parametrach funkcji i domknięć oraz typach zwracanych (także `-> ()` dla funkcji bez wyniku). Nie polegaj na inferencji typu tam, gdzie można podać typ jawnie. Dotyczy to również testów. Jest to zasada jawnego typowania; rzutowania wartości (`as`, `From`, `TryFrom`) stosuj tylko wtedy, gdy potrzebna jest rzeczywista konwersja.
|
||||
- Rust 2024, jedna biblioteka i jeden binarny punkt wejścia; bez workspace.
|
||||
- Wykorzystuj istniejące `App`, `GameStates`, `BoardState`, akcje i widgety.
|
||||
- Współrzędne planszy to `(row, col)`, indeksowanie `cells[row][col]`; rozmiar mapy i rozmiar widocznego obszaru to różne wartości.
|
||||
- `CellWidget` przechowuje także dane gry. Zmiana modelu komórki dotyka planszy, jednostek, struktur i panelu bocznego.
|
||||
- Lista skrótów w widokach oraz tabela `KEYBINDINGS` muszą odpowiadać obsłudze akcji.
|
||||
- `advance_turn` jest wywoływane przez tick potyczki. Zachowaj jedną aktualizację na jednostkę na tick, aktualność jej współrzędnych, kolejność zadań i ochronę zajętych pól. Regresje sprawdzaj przez `tests/digging.rs`.
|
||||
@@ -0,0 +1,132 @@
|
||||
# Mapa kodu i przepływu
|
||||
|
||||
Migawka: 2026-10-18. Projekt `war_in_tunnels` 0.1.0 to prototyp terminalowego RTS. Działają menu, plansza, nawigacja, zlecanie i wykonywanie kopania oraz audio. Zegar obsługuje zadania jednostek; ekonomia i walka pozostają poza zakresem obecnej symulacji.
|
||||
|
||||
## Moduły i odpowiedzialność
|
||||
|
||||
Wszystkie ścieżki poniżej są względem katalogu głównego repozytorium.
|
||||
|
||||
| Obszar | Pliki | Odpowiedzialność |
|
||||
|---|---|---|
|
||||
| Start | `src/main.rs`, `src/lib.rs` | CLI, kanały, wątki, terminal, eksporty biblioteki |
|
||||
| Parametry | `src/cli.rs`, `src/logs.rs` | Clap, walidacja zakresów, opcjonalny logger |
|
||||
| Pętla aplikacji | `src/app/app.rs` | `App`, inicjalizacja stanów przy pierwszym rysowaniu, wejście, resize, update |
|
||||
| Zbiór stanów | `src/app/state.rs`, `src/app/states/` | `GameStates`, menu, potyczka, ustawienia, perki, umiejętności |
|
||||
| Routing widoku | `src/app/view.rs`, `src/app/views/` | `View`, implementacja `Widget for &mut App`, layouty i ekrany |
|
||||
| Wejście | `src/app/keybind.rs`, `src/app/keybindings/` | Routing według widoku, `KEYBINDINGS`, `Action`, `Group`, mapowanie zdarzeń |
|
||||
| Plansza | `src/app/states/skirmish_states/board.rs` | Tworzenie mapy, kamera, zoom, focus, zaznaczenia, zlecanie i wykonywanie zadań |
|
||||
| Współrzędne | `focused_cell.rs`, `offset.rs`, `marked_cells.rs`, `zoom_level.rs` w `skirmish_states/` | Granice kursora i przewijania, ścieżka zaznaczenia, rozmiar komórek |
|
||||
| Obiekty gry | `skirmish_states/structures/`, `units/`, `tasks/` | Traity `Structure`, `Unit`, `Task` i delegujące enumy `Structures`, `Units`, `Tasks` |
|
||||
| Widgety | `src/app/widgets/` | Plansza, komórka, panel boczny i legenda klawiszy |
|
||||
| Pomocniczy layout | `src/app/helpers/` | Obszar komórek, tytuły bloków i tekst opcji menu |
|
||||
| Wątki | `src/app/threads/{events,tick,audio}.rs` | Crossterm, zegar 33 ms, Rodio i komendy audio |
|
||||
| Środowisko | `Cargo.toml`, `flake.nix`, `default.nix`, `flake.lock` | Zależności, toolchain Fenix, ALSA, pakowanie przez Naersk lub buildRustPackage |
|
||||
| Formatowanie | `rustfmt.toml` | Rustfmt, styl 2024, wcięcia po 2 spacje, docelowo 100 znaków i zakończenia LF |
|
||||
| Kontrole CI | `.gitea/workflows/rust.yml` | Gitea Actions w `node:22-bookworm`: Rust 1.99.1 przez rustup, cache toolchaina i Cargo, formatowanie, kompilacja, testy debug/release i Clippy |
|
||||
| Budowa paczki Nix | `.gitea/workflows/nix.yml` | Oddzielny workflow buduje `.#default`, czyli `default.nix` przez nixpkgs przypięte w `flake.lock`, z testami i kontrolą instalacji pakietu |
|
||||
| Paczki Linux | `.gitea/workflows/packages.yml`, `packaging/` | Testowe deb, rpm, AppImage, Flatpak oraz makepkg dla AUR; bez wydań i tagów; szczegóły w `docs/PACKAGING.md` |
|
||||
| Licencja | `LICENSE`, `Cargo.toml`, `default.nix` | MIT, Copyright (c) 2026 GarandPLG |
|
||||
| Zasoby | `soundtracks/default/`, `ui_draft/` | Dwa pliki OGG i tekstowe szkice ekranów |
|
||||
|
||||
Ścieżki skrócone `skirmish_states/` oznaczają `src/app/states/skirmish_states/`. Nie ma osobnego silnika gry, bazy danych ani warstwy sieciowej. Zależności Cargo: Ratatui/Crossterm, Clap, Rodio, log/simplelog i once_cell.
|
||||
|
||||
`Cargo.toml` deklaruje Rust 1.88 jako minimalną wspieraną wersję, wymusza `unused_must_use = "deny"` i `unsafe_code = "forbid"` we własnym kodzie, dopuszcza `clippy::unused_unit` oraz włącza `overflow-checks` w release. Toolchain deweloperski nadal pochodzi z Fenix przypiętego przez `flake.lock`; wersję 1.88.0 sprawdzono osobno.
|
||||
|
||||
## Uruchomienie i pętla
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
CLI[Cli::parse / get_args] --> MAIN[main]
|
||||
MAIN --> INPUT[Wątek handle_ct_events]
|
||||
MAIN --> TICK[Wątek handle_tick_event: 33 ms]
|
||||
MAIN --> AUDIO[Wątek handle_audio, jeśli włączony]
|
||||
MAIN --> INIT[ratatui::init / App::new]
|
||||
INIT --> LOOP[App::run]
|
||||
INPUT -->|kanały key i resize| LOOP
|
||||
TICK -->|kanał tick| LOOP
|
||||
LOOP --> DRAW[terminal.draw / App::draw]
|
||||
DRAW --> STATES[GameStates::new przy pierwszym rozmiarze okna]
|
||||
LOOP --> KEYS[handle_keybindings]
|
||||
KEYS -->|AudioCmd| AUDIO
|
||||
LOOP --> UPDATE[App::update / SkirmishState::tick_update]
|
||||
LOOP --> SLEEP[sleep 10 ms]
|
||||
SLEEP --> LOOP
|
||||
LOOP -->|exit lub błąd| RESTORE[ratatui::restore]
|
||||
```
|
||||
|
||||
Kanały to standardowe `std::sync::mpsc`. Stan gry mutuje główny wątek. Każdy obrót pętli najpierw rysuje, następnie próbuje odebrać po jednym zdarzeniu z każdego kanału. Aktualizacja potyczki odbywa się niezależnie od wybranego ekranu. `tick_update` wykonuje `board.advance_turn()` i zwiększa `turn_counter`.
|
||||
|
||||
`GameStates::new` tworzy wszystkie stany jednocześnie. `SettingsState` przechowuje parametry z CLI; samo istnienie parametrów ekonomii, XP i trybu gry nie oznacza zaimplementowanych mechanik. `View` zawiera MainMenu, Skirmish, PerkDecks i SkillsConfig. Dwa ostatnie używają `default_view` z napisem „Work in progress”; ustawienia nie mają osobnego wariantu `View`.
|
||||
|
||||
## Renderowanie i dane
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
APP[App + GameStates] --> ROUTE[View::render]
|
||||
ROUTE --> MENU[main_menu_view]
|
||||
ROUTE --> SK[skirmish_view i layouty]
|
||||
ROUTE --> WIP[default_view]
|
||||
SK --> BOARD[BoardWidget]
|
||||
SK --> SIDE[SidePanelWidget]
|
||||
SK --> HELP[KeybindingsWidget]
|
||||
BOARD --> CELL[CellWidget]
|
||||
CELL --> STRUCT[Structures / Structure]
|
||||
CELL --> UNIT[Option Units / Unit]
|
||||
UNIT --> TASK[VecDeque Tasks / Task]
|
||||
```
|
||||
|
||||
`BoardState.cells: Vec<Vec<CellWidget>>` łączy model i prezentację: komórka zawiera strukturę, opcjonalną jednostkę, pozycję i flagi UI. `BoardWidget` wybiera widoczne komórki przez offsety i layout; `CellWidget` odpowiada za ich tekst i styl. Panel boczny prezentuje dane zaznaczenia. Zoom wpływa na wielkość komórek i liczbę widocznych wierszy/kolumn; resize i przełączanie panelu zmieniają granice offsetów.
|
||||
|
||||
Mapa powstaje deterministycznie: kamień, baza gracza w `(map_height / 2, 2)`, baza przeciwnika w `(map_height / 2, map_width - 3)`, sąsiadujące złoża i po jednym górniku w bazie. CLI domyślnie wybiera mapę 42 × 11; dopuszczalne zakresy to szerokość 36–108 i wysokość 11–39. Dostępne struktury to Base, Ore, Stone, Tunnel; jedyna jednostka to Miner. Zadania to Digging i Moving, implementujące wspólny trait Task.
|
||||
|
||||
## Klawisze i zlecanie kopania
|
||||
|
||||
1. `handle_ct_events` odbiera zdarzenie Crossterm i przekazuje je kanałem do `App`.
|
||||
2. `handle_keybindings` wybiera obsługę menu, potyczki albo domyślną.
|
||||
3. `event_to_action` porównuje kod, rodzaj i modyfikatory z `KEYBINDINGS`, następnie obsługuje znak wildcard.
|
||||
4. Akcje wspólne realizują wyjście, powrót do menu i komendy audio.
|
||||
5. W potyczce ruch zmienia focus i ewentualnie kamerę; zoom przelicza rozmiary; Tab przełącza panel.
|
||||
6. Spacja przełącza zaznaczanie. `start_marking_cells` zapamiętuje jednostkę z komórki początkowej; ruch dopisuje współrzędne do kolejki. Backspace cofa ścieżkę, Delete ją anuluje.
|
||||
7. `end_marking_cells(false)` pomija pierwsze pole i dzieli trasę na kolejne jednorodne odcinki: `MovingTask` dla tuneli/baz i `DiggingTask` dla kamienia. Dodaje je do kolejki w kolejności ścieżki; w panelu zadania mają ikony `→` i `⛏`.
|
||||
|
||||
`start_marking_cells` dopuszcza wyłącznie własnego górnika. Jeśli ma już zadania, nowa ścieżka zaczyna się na końcu ostatniego zadania. Jednostka wybrana do rysowania wstrzymuje wykonywanie zadań do zatwierdzenia lub anulowania trasy; powtarzany ruch przy granicy nie dopisuje kolejnych kopii tej samej kratki.
|
||||
|
||||
`advance_turn` najpierw zbiera pozycje pracujących jednostek, aby każda była aktualizowana tylko raz. Wyjmuje jednostkę i pierwsze zadanie, sprawdza granice, sąsiedztwo i zajętość celu. Przy kamieniu różnica skumulowanego postępu zadania daje obrażenia: `digging_power` 0.55 oznacza około 5.5 durability na tick przy maksymalnym durability 1000. Zaokrąglenie skumulowanej wartości zachowuje część ułamkową. Pełne kopanie trwa około 182 ticków (6 sekund). `Stone::take_damage` nie pozwala zejść poniżej zera.
|
||||
|
||||
Po zniszczeniu kamienia cel staje się tunelem i jednostka wchodzi do niego w tym samym ticku w ramach `Digging`. `MovingTask` obsługuje przejście do istniejącego tunelu lub pustej bazy: wymaga 91 ticków (3003 ms przy `TICK_MS = 33`), nie zmienia durability. Jego licznik `ticks` resetuje się dla każdego kroku; blokada lub rysowanie trasy wstrzymuje naliczanie. Jeśli teren zmieni się od zaplanowania trasy (np. kamień został już wykopany albo ścieżka wraca przez nowy tunel), `advance_turn` zmienia typ bieżącego zadania według aktualnego celu, zachowując pozostałą ścieżkę i kolejkę.
|
||||
|
||||
Ruch aktualizuje współrzędne jednostki i usuwa krok ścieżki. Zakończone zadanie znika, następne może wykonać się od kolejnego ticka. Zajęty cel lub ruda blokuje pracę bez niszczenia obiektu. Pusta ścieżka jest usuwana, a cel poza mapą lub niesąsiadujący anuluje bieżące zadanie bez teleportacji. Uszkodzony kamień pokazuje pozostałą wytrzymałość w procentach na planszy; panel boczny pokazuje dokładne durability. Znacznik jednostki jest osobnym stylowanym `Span` z kolorem `Players::get_color()`, niezależnym od koloru tunelu i zaznaczenia komórki.
|
||||
|
||||
Postęp bieżącego kroku jest udostępniany przez `MovingTask::get_movement_progress()`: brak wartości przed rozpoczęciem ruchu, procent obliczany z tego samego `MOVEMENT_TICKS`, który wyznacza czas przejścia. `CellWidget` odczytuje go wyłącznie z aktywnego `Tasks::Moving` i pokazuje na środku kratki jednostki w kolorze jej właściciela. W normalnym i powiększonym widoku pozostaje widoczny osobny znacznik jednostki; w pomniejszonym procent zastępuje znak struktury na czas ruchu. Zakończenie kroku resetuje wskaźnik. `DiggingTask` przechowuje wyłącznie postęp kopania, bez licznika podróży.
|
||||
|
||||
`Task::get_progress()` oznacza postęp bieżącej kratki, a `get_total_progress()` uwzględnia wykonane kratki i bieżący etap. Oba typy zadań zapamiętują początkową długość trasy w `total_steps`; `from_digging` i `from_moving` zachowują ją przy zmianie typu. `MinerUnit::get_tasks_formatted()` prezentuje każde zadanie w dwóch wierszach: trasa oraz `Step: …% | Total: …%`. Wartość całkowita jest liczona według liczby kratek, nie szacowanego czasu pozostałego do końca.
|
||||
|
||||
## Audio i pakowanie
|
||||
|
||||
Główny wątek wysyła `AudioCmd`; osobny wątek obsługuje głośność, mute, część i soundtrack. Cache wykorzystuje `Lazy`, `RwLock`, `Arc` i bajty OGG. Calm ładuje `calm.ogg`; Buildup, Assault i Outro korzystają obecnie z `test.ogg`. Błędy urządzenia, odczytu i dekodowania mogą zakończyć wątek przez `expect`.
|
||||
|
||||
Rodio zapętla źródło przez `Decoder::new_looped`. Wątek sterujący blokuje się na odbiorniku (`for cmd in rx`), zamiast aktywnie sprawdzać komendy i koniec utworu. Zmiana części lub soundtracku dopisuje zapętlone źródło i pomija bieżące przez `skip_one`. Zamknięcie kanału kończy wątek. Soundtracki są szukane obok rzeczywistej binarki, z fallbackiem do bieżącego katalogu dla Cargo. Testy audio obejmują zapętlanie OGG i ładowanie zasobów poza repozytorium, bez urządzenia audio.
|
||||
|
||||
`flake.nix` dostarcza devShell dla `x86_64-linux`, domyślny pakiet z `default.nix` i pakiet `.#develop` budowany przez Naersk. Obie ścieżki pakowania kopiują soundtracki do `$out/bin` i zmieniają nazwę binarki na `war-in-tunnels`. `default.nix` ma stały `cargoHash`; po zmianie zależności wymaga osobnej weryfikacji. Samo `cargo test` nie sprawdza pakowania.
|
||||
|
||||
## Punkty wymagające uwagi przed dalszą implementacją
|
||||
|
||||
To obserwacje statyczne, nie lista napraw wykonanych w tej sesji:
|
||||
|
||||
- **Blokady:** jednostki czekają przed zajętym polem i rudą. Nie ma automatycznego omijania przeszkód ani rozwiązywania sytuacji, w której dwie jednostki wzajemnie blokują przejście.
|
||||
- **Zaznaczenie:** wybrany górnik czeka podczas rysowania trasy; inne jednostki wykonują zadania normalnie.
|
||||
- **Audio:** usunięto aktywne odpytywanie; pomiar CPU na docelowym urządzeniu i odsłuch pozostają do wykonania. Test dekodera nie weryfikuje urządzenia audio.
|
||||
- **Granice UI:** małe terminale, resize i trzy poziomy zoomu wymagają interaktywnego sprawdzenia. Indeksowanie planszy zakłada poprawne współrzędne.
|
||||
- **Pokrycie:** 46 testów CLI, sterowania, renderowania, planszy, wykonywania zadań i dekodowania audio; szczegóły w [TESTING.md](TESTING.md). Workflow Gitea sprawdza też profil release; nie obejmuje pełnej rozgrywki. Clippy zgłasza 50 ostrzeżeń biblioteki, `unused_unit` jest jawnie dozwolony. Nie uruchamiać automatycznych poprawek poza zakresem zadania.
|
||||
|
||||
## Gdzie zacząć konkretną zmianę
|
||||
|
||||
| Zadanie | Główne miejsca i powiązania |
|
||||
|---|---|
|
||||
| Nowa akcja | `keybindings/keybindings.rs` → handler widoku → lista akcji w `views/` |
|
||||
| Mechanika kopania | `tasks/digging.rs` → `units/` → `board.rs::advance_turn` → `skirmish.rs::tick_update` |
|
||||
| Nowy obiekt | Trait i enum w `structures/` lub `units/`, eksporty `mod.rs`, tworzenie mapy, komórka i panel |
|
||||
| Kamera i zoom | `board.rs`, `focused_cell.rs`, `offset.rs`, `zoom_level.rs`, `helpers/cells_area.rs`, layout i widget planszy |
|
||||
| Nowy ekran | `View`, `GameStates`, `states/`, `views/`, routing klawiszy i menu |
|
||||
| Parametr CLI | `cli.rs`, walidacja, `GameStates::new`, faktyczny konsument i README |
|
||||
| Audio lub dystrybucja | `threads/audio.rs`, `main.rs`, zasoby, `flake.nix`, `default.nix` |
|
||||
@@ -0,0 +1,37 @@
|
||||
# Testowe paczki Linux
|
||||
|
||||
Workflow `.gitea/workflows/packages.yml` uruchamia się po pushu i dla pull requestów. Nie tworzy tagów, wydań ani wpisów w AUR/Flathub. Paczki są niesygnowanymi artefaktami CI przechowywanymi przez 7 dni. Używa `upload-artifact@v3` dla zgodności z Gitea.
|
||||
|
||||
Obsługiwana architektura to x86_64. Są to formaty dystrybucji Linux, nie kompilacje Windows/macOS.
|
||||
|
||||
Job AUR instaluje `nodejs` przed checkoutem: runner 0.6.1 uruchamia akcje JavaScript poleceniem `node`, którego obraz `archlinux:base-devel` nie zawiera. Jest to zależność CI (checkout i upload-artifact), nie zależność gry ani PKGBUILD.
|
||||
|
||||
| Format | Budowanie i kontrola |
|
||||
|---|---|
|
||||
| `.deb` | Rust 1.99.1 w Debian Bookworm, `dpkg-deb`, odczyt metadanych i uruchomienie rozpakowanej binarki z `--version` |
|
||||
| `.rpm` | Ta sama binarka, `rpmbuild` i `packaging/war-in-tunnels.spec`, automatyczne zależności ELF, ekstrakcja i `--version` |
|
||||
| AppImage | AppDir z binarką, muzyką i libasound.so.2; appimagetool 1.9.1; ekstrakcja narzędzia i paczki przez 7z, uruchomienie AppRun z `--version` bez FUSE |
|
||||
| AUR | `packaging/aur/PKGBUILD`, lokalne archiwum bieżącego checkoutu z SHA256, `makepkg` jako zwykły użytkownik w Arch Linux; kompilacja, testy, instalacja paczki i `--version`; eksport .SRCINFO |
|
||||
| Flatpak | Binarka z Debiana pakowana poleceniami `flatpak build-init`, `build-finish`, `build-export`, `build-bundle`; runtime org.freedesktop.Platform 25.08; instalacja paczki w kontenerze, kontrola soundtracku i `--version` zainstalowanej binarki bez sandboxa |
|
||||
|
||||
`packaging/stage.sh` instaluje binarkę i soundtracki w `PREFIX/lib/war-in-tunnels`, launcher w `PREFIX/bin`, licencję, plik desktop z `Terminal=true` i ikonę SVG. Nie zmienia katalogu roboczego, więc opcja `--log` nadal zapisuje log w katalogu wybranym przez użytkownika. Audio najpierw szuka soundtracków obok rzeczywistej binarki, a następnie w bieżącym katalogu dla lokalnego Cargo. Test audio sprawdza działanie poza repozytorium.
|
||||
|
||||
W kontenerze Debian z zależnościami workflow można wykonać:
|
||||
|
||||
```sh
|
||||
cargo build --release --locked
|
||||
bash packaging/build.sh deb
|
||||
bash packaging/build.sh rpm
|
||||
bash packaging/build.sh appimage
|
||||
bash packaging/build.sh flatpak
|
||||
```
|
||||
|
||||
Lokalnie na hoście projektu polecenia Cargo uruchamiaj przez `nix develop -c`; binarka zbudowana przez Nix może zależeć od `/nix/store`, dlatego do paczek dystrybucyjnych używaj kompilacji kontenerowej. Skrypty oczekują bieżącego katalogu repozytorium i zapisują wyniki do `dist/`.
|
||||
|
||||
Przepis AUR jest testowy: nie wskazuje publicznego tarballa wydania. Workflow dostarcza lokalny tarball oraz zastępuje `SKIP` rzeczywistym SHA256. Wersja `pkgver` musi odpowiadać Cargo.toml; CI to sprawdza. Artefakt AUR zawiera paczkę, przepis, .SRCINFO i pasujące źródła.
|
||||
|
||||
Flatpak jest testem pakowania istniejącej binarki, nie kompilacją wewnątrz SDK Flatpak. Bookworm dostarcza glibc 2.36, runtime 25.08 jest nowszy. Pakowana jest biblioteka ALSA; uprawnienia obejmują PulseAudio i urządzenia dla bezpośredniego ALSA. `build-export --disable-sandbox` wyłącza wyłącznie sandbox walidatora własnej ikony z repozytorium, nie walidację ani sandbox gotowej aplikacji. Tworzenie paczki nie potrzebuje privileged ani zmiany polityki Dockera. Instalacja w Dockerze może zgłaszać bwrap dla hooków integracji pulpitu mimo poprawnego zainstalowania paczki; sprawdzamy kod wyjścia, pliki i binarkę. Pełnego uruchomienia TUI i audio w sandboxie te kontrole nie zastępują. Runtime nie jest zawarty w pliku .flatpak; użytkownik musi go zainstalować z Flathub. CI używa --no-related, aby nie pobierać opcjonalnych dodatków GPU i tłumaczeń.
|
||||
|
||||
Paczki Debian, RPM i AppImage mają bazę ABI Bookworm (glibc >= 2.36); nie gwarantują działania na starszych dystrybucjach. AppImage zawiera ALSA, ale korzysta z konfiguracji audio hosta. Testy nie potwierdzają odsłuchu ani otwierania terminala z menu desktop. Appimagetool jest przypięty do wersji; jego domyślny runtime jest pobierany z upstream podczas pakowania. Rust i Cargo mają cache oddzielny od zwykłego workflow Rust; dla cache wymagany jest działający serwer runnera. Arch i Flatpak pobierają własne zależności bez trwałego cache.
|
||||
|
||||
Dokumentacja narzędzi: [AppImage](https://github.com/AppImage/appimagetool/tree/1.9.1), [Flatpak](https://docs.flatpak.org/en/latest/flatpak-command-reference.html), [makepkg](https://man.archlinux.org/man/makepkg.8).
|
||||
@@ -0,0 +1,199 @@
|
||||
# Dziennik prac
|
||||
|
||||
## 2026-10-19 — Node.js wymagany przez akcje joba AUR
|
||||
|
||||
Log Gitea wykazał brak `node` podczas uruchamiania checkout@v4 i jego kroku post. Arch base-devel nie zawiera Node.js, a runner 0.6.1 wywołuje akcje JavaScript przez `node` w kontenerze. Dodano `nodejs` do instalacji zależności przed checkoutem oraz kontrolę `node --version`. Node.js jest zależnością CI, nie paczki gry. Poprzednia próba lokalna weryfikowała makepkg, lecz nie uruchamiała akcji JavaScript; uzupełniono ten brak.
|
||||
|
||||
Weryfikacja w jednorazowym Arch base-devel: zainstalowano Node.js, `node --version` zwróciło v26.12.0; sprawdzono `node --check` rzeczywistych entrypointów checkout@v4 i upload-artifact@v3. Uruchomienie checkoutu doszło do oczekiwanego błędu brakującego inputu token (bez poświadczeń i klonowania repozytorium), potwierdzając poprawny start akcji. `nix develop -c cargo check --all-targets --locked` i `git diff --check` przeszły. Nie powtarzano budowania gry ani nie uruchamiano pełnego workflow na serwerze; transport artefaktów pozostaje do potwierdzenia na Gitea.
|
||||
|
||||
## 2026-10-19 — testowe paczki Linux poza Nix
|
||||
|
||||
Dodano `.gitea/workflows/packages.yml` dla push i pull request: job Debian kompiluje Rust 1.99.1 i tworzy deb, rpm, AppImage oraz Flatpak; job Arch kompiluje i testuje lokalny przepis AUR przez makepkg jako użytkownik builder. Workflow zapisuje artefakty przez 7 dni, nie tworzy wydań, tagów ani publikacji AUR/Flathub. Zależności i toolchain Cargo mają cache; Flatpak i Arch nie mają jeszcze trwałego cache. Obsługiwana architektura: x86_64.
|
||||
|
||||
Przepisy i wspólna instalacja są w `packaging/`, a zakres i ograniczenia w `docs/PACKAGING.md`. Pakowane są muzyka, licencja, launcher zachowujący katalog roboczy, plik desktop Terminal=true i ikona SVG. Flatpak pakuje binarkę z Debiana do runtime 25.08, bez kompilacji w SDK i bez zagnieżdżonego sandboxa. Przepis AUR dostaje archiwum checkoutu z rzeczywistym SHA256; nie jest jeszcze przepisem publikowanego wydania. Dodano `dist/` do .gitignore.
|
||||
|
||||
Audio szuka zasobów obok rzeczywistej binarki z fallbackiem do katalogu roboczego, aby instalowane paczki działały poza repozytorium bez zmiany miejsca zapisu logów. Dodano regresję uruchamianą w osobnym procesie z przeniesioną binarką testu i soundtrackami. Łącznie 46 testów integracyjnych.
|
||||
|
||||
Weryfikacja: cargo check --all-targets --locked, cargo test --all-targets --locked (46 testów), cargo fmt --all -- --check i cargo clippy --all-targets --locked przez nix develop przeszły; Clippy nadal ma 50 ostrzeżeń biblioteki. Przeszły git diff --check i bash -n. W jednorazowym Debianie rzeczywiście skompilowano grę, utworzono deb i rpm, rozpakowano i sprawdzono --version; zbudowano też AppImage, rozpakowano je i uruchomiono AppRun --version. Narzędzie i wynik AppImage rozpakowywane są przez 7z, bez FUSE i kolizji z binfmt hosta. Osobny Arch Linux wykonał makepkg, testy release i doctest, instalację paczki oraz --version. Flatpak został wyeksportowany, zapisany do pliku i zainstalowany w kontenerze; zweryfikowano soundtrack i --version zainstalowanej binarki. Wyłączono sandbox wyłącznie walidatora własnej ikony przy build-export; instalacja może zgłaszać ostrzeżenia bwrap dla hooków integracji pulpitu w standardowym Dockerze. Nie wyłączono sandboxa gotowej aplikacji. Instalacja runtime i aplikacji używa --no-related, aby nie pobierać opcjonalnych sterowników GPU ani tłumaczeń dla testu TUI.
|
||||
|
||||
Nie sprawdzono pełnej rozgrywki, odsłuchu, uruchamiania z menu pulpitu ani działania na starszych dystrybucjach; pełny workflow i transport artefaktów wymagają przebiegu na Gitea. Testowe kontenery miały repozytorium tylko do odczytu; nie instalowano paczek na hoście. Usunięto tymczasowy kontener war-packages-check i obraz war-in-tunnels-packaging-test:local.
|
||||
|
||||
## 2026-10-19 — oddzielny workflow budowania default.nix
|
||||
|
||||
Dodano `.gitea/workflows/nix.yml` dla push i pull request. Workflow w `node:22-bookworm` instaluje sudo i Nix (KVM wyłączone), następnie buduje `.#default` z przypiętym `flake.lock`, bez aktualizacji lockfile i bez devShell. Obejmuje testy buildRustPackage oraz versionCheckHook. Nie zmieniono workflow Rust ani konfiguracji runnera; nie dodano trwałego cache Nix.
|
||||
|
||||
Próbna budowa ujawniła nieaktualny cargoHash w `default.nix`; poprawiono go na hash rzeczywiście pobranych zależności. Ponowne `nix build .#default --no-update-lock-file --no-link --print-out-paths --print-build-logs` zakończyło się sukcesem. Lokalna kontrola `nix develop -c cargo check --all-targets --locked` również przeszła. Zaktualizowano mapę, instrukcje i opis CI. Zachowano zastaną zmianę użytkownika w `flake.nix`. Nowy workflow wymaga jeszcze uruchomienia na Gitea.
|
||||
|
||||
## 2026-10-19 — CI Rust bez Nix
|
||||
|
||||
Na życzenie użytkownika usunięto instalację Nix i wywołania `nix develop` z workflow. W kontenerze `node:22-bookworm` rustup instaluje Rust 1.99.1 z rustfmt i Clippy. Dodano zależności systemowe ALSA, pkg-config i kompilator C oraz dwa cache `actions/cache@v4`: toolchain i Cargo (registry, git, target). Klucze Cargo uwzględniają wersję Rust, Cargo.lock, manifest i źródła; częściowe trafienia pozwalają wykorzystać wcześniejsze artefakty. Zachowano wszystkie pięć kontroli i `--locked`. Lokalne środowisko Nix pozostaje bez zmian. Zaktualizowano mapę i instrukcje.
|
||||
|
||||
Weryfikacja: lokalne `nix develop -c cargo check --all-targets --locked`, `nix develop -c cargo fmt --all -- --check` i `git diff --check` zakończyły się sukcesem. Pełne wykonanie nowego workflow i skuteczność cache wymagają sprawdzenia na Gitea; lokalna kontrola nie potwierdza instalacji rustup ani sieciowego dostępu do cache serwera.
|
||||
|
||||
## 2026-10-19 — naprawa instalacji Nix w kontenerze Gitea Actions
|
||||
|
||||
Log runnera 0.6.1 wskazał `sudo: command not found` w `cachix/install-nix-action@v31`. Etykieta `ubuntu-latest` była mapowana na `node:16-bullseye`, który nie zawiera sudo. Lokalny test odtworzenia wykazał dodatkowo błąd 404 podczas instalacji sudo z repozytorium starego obrazu.
|
||||
|
||||
W `.gitea/workflows/rust.yml` wskazano jawnie kontener `node:22-bookworm`, dodano przed instalacją Nix krok `apt-get update` i `apt-get install -y --no-install-recommends sudo` oraz ustawiono `enable_kvm: false`. Nie zmieniano konfiguracji runnera na serwerze ani nie dodawano trybu privileged.
|
||||
|
||||
Weryfikacja: uruchomiono jednorazowy kontener `node:22-bookworm`, zainstalowano sudo i wykonano skrypt `install-nix.sh` z v31 z wyłączonym KVM. Instalacja zakończyła się sukcesem, `nix --version` zwróciło 2.35.3, a `nix eval --expr "1 + 1"` zwróciło 2. Instalator ostrzega o pracy jako root; w tym izolowanym kontenerze instalacja i próbne polecenia przeszły. Nix automatycznie użył sandbox-fallback z powodu niedostępności namespaces; nie zmieniano ustawienia sandbox w workflow.
|
||||
|
||||
`nix develop --no-update-lock-file -c cargo check --all-targets --locked` zakończone sukcesem. Nie powtarzano testów gry przy zmianie samego workflow. Zaktualizowano opis CI i mapę. Pełny przebieg Actions wymaga ponownego pushu i wykonania na serwerze; lokalny test obejmował instalację Nix i podstawowe wywołanie, nie cały workflow.
|
||||
|
||||
## 2026-10-19 — ujednolicenie licencji na MIT
|
||||
|
||||
Na polecenie użytkownika zastąpiono CC0 w `LICENSE` standardowym tekstem MIT z nagłówkiem `Copyright (c) 2026 GarandPLG`, zgodnym z nazwą autora i rokiem w historii Git. Dodano `license = "MIT"` w `Cargo.toml`, sekcję licencji w README i wpis w mapie. `default.nix` już deklarował MIT i nie wymagał edycji.
|
||||
|
||||
Zmiana dotyczy bieżącego stanu repozytorium i nie cofa uprawnień do wersji wcześniej udostępnionych na CC0. Nie wykonywano audytu praw do muzyki ani innych materiałów zewnętrznych. Nie zmieniano źródeł Rust ani zależności; testów działania gry nie powtarzano.
|
||||
|
||||
Weryfikacja: `nix develop --no-update-lock-file -c cargo check --all-targets --locked` zakończone sukcesem; sprawdzono zgodność deklaracji MIT w LICENSE, Cargo.toml i default.nix oraz `git diff --check`.
|
||||
|
||||
## 2026-10-19 — linty, minimalny Rust i CI
|
||||
|
||||
W `Cargo.toml` dodano `rust-version = "1.88"`, `unused_must_use = "deny"`, `unsafe_code = "forbid"`, `clippy::unused_unit = "allow"` oraz `overflow-checks = true` dla release. Minimalna wersja wynika z wymagań aktualnych zależności (m.in. Ratatui 0.30.2 i Darling 0.23) i składni kodu; potwierdzono kompilację wszystkich targetów na Rust 1.88.0 przez tymczasowy toolchain Fenix wewnątrz `nix develop`. Nie zmieniono domyślnego toolchaina ani plików lock.
|
||||
|
||||
Dodano `.gitea/workflows/rust.yml` dla pushów i pull requestów: checkout, instalacja Nix, kontrola formatowania, kompilacja, testy debug/release i Clippy. Kontrole używają `nix develop --no-update-lock-file`, polecenia Cargo kompilujące kod mają `--locked`. Również `shellHook` we `flake.nix` wykonuje teraz pomoc CLI z `--locked`. Nie włączano `-D warnings`, grup `pedantic` ani `restriction`.
|
||||
|
||||
Weryfikacja lokalna na domyślnym toolchainie Nix: formatowanie, `cargo check --all-targets --locked`, `cargo test --all-targets --locked` (45 zaliczonych), `cargo test --all-targets --release --locked` (45 zaliczonych) i `cargo clippy --all-targets --locked` zakończone sukcesem. Clippy nadal zgłasza 50 ostrzeżeń biblioteki; 4 ostrzeżenia o jawnych `-> ()` zniknęły zgodnie z nowym ustawieniem. Dodatkowa kontrola Rust 1.88.0: `cargo check --all-targets --locked` zakończone sukcesem.
|
||||
|
||||
Zaktualizowano instrukcje agentów, mapę i opis testów. Nie zmieniano mechaniki gry ani zależności. Workflow przygotowano i przejrzano lokalnie; nie był uruchamiany na serwerze. Wymaga włączonych Actions i runnera Linux x86_64 o etykiecie `ubuntu-latest`, obsługującego akcje Node i instalację Nix. Nie rejestrowano runnera ani nie zmieniano ustawień serwera Gitea.
|
||||
|
||||
## 2026-10-19 — formatowanie Rust z wcięciami po 2 spacje
|
||||
|
||||
Dodano `rustfmt.toml` z uzgodnionymi opcjami: edycja i styl 2024, `max_width = 100`, `hard_tabs = false`, `tab_spaces = 2`, zakończenia Unix, sortowanie importów i modułów oraz domyślne heurystyki krótkich konstrukcji. Zastosowano `nix develop -c cargo fmt --all` do źródeł i testów (56 zmienionych plików Rust). Zaktualizowano instrukcje agentów i mapę konfiguracji projektu.
|
||||
|
||||
Weryfikacja przez `nix develop -c`: `cargo fmt --all -- --check`, `cargo check --all-targets` i `cargo test --all-targets` zakończone sukcesem; 45 testów zaliczonych. Nie wprowadzano zmian mechanik ani zależności. Nie powtarzano Clippy ani ręcznej sesji TUI przy zmianie formatowania.
|
||||
|
||||
## 2026-10-18 — przekazanie kontekstu na kolejną sesję
|
||||
|
||||
Użytkownik zaakceptował aktualny stan. Nie ma ustalonego kolejnego zadania. Na start kolejnej sesji przeczytaj `docs/AGENTS.md`, `docs/CODE_MAP.md` i `docs/TESTING.md`; instrukcje w `docs/` mogą wymagać jawnego wskazania agentowi.
|
||||
|
||||
Stan końcowy: zoptymalizowane audio, wykonywanie kopania i ruchu w osobnych zadaniach, 3-sekundowy krok, kolor jednostki według właściciela, procent ruchu na kratce oraz postęp etapu i całości zadania pod Tab. Ostatni pełny zestaw: 45 testów zaliczonych, kompilacja i formatowanie poprawne. Ostrzeżenia Clippy: 50 biblioteki oraz 4 stylistyczne uwagi o wymaganym jawnym `-> ()` w testach.
|
||||
|
||||
Uwagi do dalszej pracy:
|
||||
|
||||
- `Total` jest postępem według liczby kratek, nie przewidywanym czasem zakończenia. Przy zmianie rodzaju zadania zachowuje liczbę wykonanych etapów, ale postęp bieżącego etapu jest resetowany.
|
||||
- Zajęte pole lub ruda zatrzymuje jednostkę; brak omijania przeszkód. Rysowanie trasy pauzuje wybraną jednostkę. Potyczka nadal otrzymuje ticki także poza jej ekranem.
|
||||
- Kolejka w panelu zajmuje dwa wiersze na zadanie. Długie kolejki w małym terminalu mogą zostać ucięte; przewijania panelu nie dodano.
|
||||
- Przed wydaniem pozostają ręczne sprawdzenie TUI na różnych rozmiarach terminala, odsłuch i pomiar CPU audio oraz budowa pakietów Nix. Zmiany zależności z tej sesji mogą wymagać aktualizacji `cargoHash` w `default.nix`; nie było to weryfikowane.
|
||||
- Zmiany pozostają niezatwierdzone w Git, w tym nowe `tasks/moving.rs` i `tests/digging.rs`. Przed commitem sprawdź pełne `git status`, aby uwzględnić także pliki nieśledzone. Nie wykonano commita ani publikacji.
|
||||
|
||||
W tym kroku zmieniono wyłącznie dokumentację; nie rozpoczynano nowych poprawek ani refaktoryzacji. Końcowe `nix develop -c cargo check --all-targets` zakończone sukcesem; testów nie powtarzano po zmianie samej dokumentacji.
|
||||
|
||||
## 2026-10-18 — postęp zadań w panelu i obowiązkowe adnotacje typów
|
||||
|
||||
Kolejka pod Tab pokazuje osobno postęp bieżącej kratki (`Step`) i całego zadania (`Total`). Całość jest liczona według początkowej liczby kratek: wykonane etapy plus ułamek bieżącego. Oba typy zadań przechowują `total_steps`, zachowywane także przy zmianie Moving/Digging wskutek zmiany terenu. Zadania oczekujące pokazują 0%, zakończone osiągają 100%; pusta trasa nie powoduje dzielenia przez zero.
|
||||
|
||||
Dodano do `docs/AGENTS.md` obowiązek jawnych adnotacji typów w nowym i zmienianym kodzie, włącznie z testami, parametrami domknięć i wynikami funkcji. Rozróżniono adnotacje typów od rzeczywistych konwersji wartości; nie wykonywano masowego przepisywania wcześniejszego kodu.
|
||||
|
||||
Dodano 4 testy: postęp całego kopania między etapami, zachowanie początkowej liczby etapów przy konwersji typu, formatowanie kopania i oczekującego ruchu oraz renderowanie panelu Tab z aktywnym ruchem (`Step: 50% | Total: 75%`) i kolejnym zadaniem na 0%. Zaktualizowano README i dokumentację.
|
||||
|
||||
Weryfikacja przez `nix develop -c`: `cargo check --all-targets`, `cargo test --all-targets` (45 zaliczonych), `cargo fmt --all -- --check` i `cargo clippy --all-targets` zakończone sukcesem. Clippy raportuje 50 dotychczasowych ostrzeżeń biblioteki i 4 ostrzeżenia `unused_unit` w testach za jawne `-> ()`, wymagane nową konwencją. Nie wykonywano ręcznej sesji terminala ani budowy pakietów Nix.
|
||||
|
||||
## 2026-10-18 — osobne zadanie Moving
|
||||
|
||||
Dodano `MovingTask` i wariant `Tasks::Moving`. Przeniesiono do niego licznik 3-sekundowego przejścia, procentowy postęp i reset dla każdej kratki. `DiggingTask` zawiera teraz tylko stan kopania. Eksporty i delegowanie wspólnego traitu `Task` obsługują oba warianty.
|
||||
|
||||
Zatwierdzona spacją ścieżka jest dzielona na odcinki `Moving` (tunele/bazy) i `Digging` (kamień), z zachowaniem kolejności. Kolejka pokazuje odpowiednio ikony `→` i `⛏`. Wykonywanie ruchu nie uszkadza struktur; wejście do świeżo wykopanego tunelu nadal następuje w ramach kopania. Gdy teren zmieni się od planowania lub jednostka wraca przez właśnie wykopane kratki, typ aktywnego zadania dopasowuje się do aktualnego celu bez utraty reszty trasy. TUI pobiera timer tylko z `Moving`.
|
||||
|
||||
Dostosowano test timera do `MovingTask`. Dodano regresje podziału mieszanej ścieżki i wykonania całej kolejki, zachowania durability podczas ruchu, powrotu przez świeżo wykopany tunel i zmiany typu zadania po zmianie terenu. Zaktualizowano README, mapę i instrukcje agentów.
|
||||
|
||||
Weryfikacja przez `nix develop -c`: `cargo check --all-targets`, `cargo test --all-targets` (41 zaliczonych), `cargo fmt --all -- --check` i `cargo clippy --all-targets` zakończone sukcesem. Clippy nadal zgłasza 50 ostrzeżeń biblioteki. Bez nowych zależności. Nie wykonywano ręcznej sesji TUI ani budowy pakietów Nix.
|
||||
|
||||
## 2026-10-18 — procentowy wskaźnik przejścia
|
||||
|
||||
Dodano na kratce poruszającej się jednostki procent ukończenia bieżącego kroku, w kolorze właściciela. Wskaźnik korzysta z istniejącego licznika ruchu, a czas przejścia i wyświetlane procenty mają wspólną stałą `MOVEMENT_TICKS`. Po wejściu na następną kratkę wskaźnik resetuje się; bez ruchu nie jest wyświetlany. W pomniejszonym widoku na czas ruchu zastępuje znak struktury, w pozostałych nie zasłania znacznika jednostki. Pauza ruchu zatrzymuje również postęp wskaźnika.
|
||||
|
||||
Dodano test rozdzielenia postępu kopania i ruchu oraz resetu po kroku. Test TUI sprawdza 50% w połowie przejścia na wszystkich poziomach zoomu, zachowanie koloru `M`, reset na kolejnym polu i zniknięcie wskaźnika po ukończeniu zadania. Zaktualizowano README i dokumentację.
|
||||
|
||||
Weryfikacja przez `nix develop -c`: `cargo check --all-targets`, `cargo test --all-targets` (37 zaliczonych), `cargo fmt --all -- --check` i `cargo clippy --all-targets` zakończone sukcesem. Clippy nadal zgłasza 50 ostrzeżeń biblioteki. Nie wykonywano ręcznych oględzin terminala; renderowanie sprawdzono buforem Ratatui.
|
||||
|
||||
## 2026-10-18 — czas podróży i kolor znacznika jednostki
|
||||
|
||||
Przejście do istniejącego tunelu lub pustej bazy trwa teraz 91 ticków, czyli około 3 sekund przy ticku 33 ms. Licznik ruchu znajduje się w zadaniu i resetuje po każdym wykonanym kroku. Zajęty cel oraz rysowanie trasy wstrzymują naliczanie. Wejście do świeżo wykopanego tunelu nadal odbywa się w ticku zniszczenia kamienia.
|
||||
|
||||
Znacznik jednostki w normalnym i powiększonym widoku jest osobnym `Span` w kolorze właściciela (`Players::get_color`), również na tunelu oraz przy zaznaczeniu lub oznaczeniu komórki. Dotychczasowy układ komórki i kolor jej obramowania pozostają zachowane.
|
||||
|
||||
Dostosowano testy ruchu, kolejek i blokad do nowego czasu. Dodano regresję natychmiastowego wejścia po kopaniu oraz test koloru znacznika dla obu graczy, dwóch poziomów zoomu i wszystkich kombinacji zaznaczenia/oznaczenia. Łącznie 35 testów przechodzi. Zaktualizowano README oraz dokumentację agentów.
|
||||
|
||||
Weryfikacja przez `nix develop -c`: `cargo check --all-targets`, `cargo test --all-targets`, `cargo fmt --all -- --check` i `cargo clippy --all-targets` zakończone sukcesem. Clippy nadal raportuje 50 ostrzeżeń biblioteki. Nie wykonywano ręcznego pomiaru czasu ani oględzin terminala; czas zweryfikowano tickami, kolory buforem Ratatui. W pomniejszonym widoku nadal wyświetlany jest wyłącznie znacznik struktury.
|
||||
|
||||
## 2026-10-18 — wykonywanie kopania przez jednostki
|
||||
|
||||
Podłączono `BoardState::advance_turn` do ticków potyczki. Zastąpiono szkic symulacji przetwarzaniem jednej aktualizacji każdej aktywnej jednostki na tick. Górnik stopniowo uszkadza kamień, po osiągnięciu zera tworzy tunel i przechodzi do niego. Ruch aktualizuje współrzędne jednostki, usuwa wykonany krok i po zakończeniu ścieżki usuwa zadanie. Kolejne zadania wykonują się w kolejności; tunele i niezajęte bazy można przechodzić bez kopania.
|
||||
|
||||
Kamień pokazuje pozostałą wytrzymałość procentowo na wszystkich poziomach zoomu, a istniejący panel boczny pod Tab odświeża dokładne durability. Użyto istniejącej siły kopania 0.55% maksymalnej wytrzymałości na tick, z zachowaniem części ułamkowej obrażeń (około 6 sekund na pełną kratkę).
|
||||
|
||||
Zaznaczenie spacją działa tylko dla własnego górnika. Podczas rysowania wybrany górnik czeka; nowa ścieżka przedłuża koniec jego kolejki. Pominięto powtarzanie tej samej kratki przy ruchu na granicy mapy. Zajęte pola i ruda blokują ruch, nie są nadpisywane. Błędne cele spoza mapy lub niesąsiadujące anulują bieżące zadanie bez utraty jednostki.
|
||||
|
||||
Dodano 10 testów w `tests/digging.rs`: rzeczywisty przepływ klawiszy i ticków, utrata durability i ukończenie kopania, ruch w obu kierunkach tylko raz na tick, kolejki i ich przedłużanie przez zaznaczenie, aktualność współrzędnych, zajęte pola, ochrona rudy, błędne/puste ścieżki, własność jednostek, pauza przy rysowaniu i renderowanie procentów oraz dokładnych wartości. Dostosowano test cofania ścieżki do wyboru rzeczywistego górnika.
|
||||
|
||||
Weryfikacja przez `nix develop -c`: `cargo check --all-targets`, `cargo test --all-targets` (33 zaliczone), `cargo fmt --all -- --check` i `cargo clippy --all-targets` zakończone sukcesem. Clippy zgłasza 50 ostrzeżeń dla biblioteki. Zaktualizowano README, mapę, instrukcje agentów i opis testów. Nie dodano zależności.
|
||||
|
||||
Nie wykonywano ręcznej sesji w terminalu, odsłuchu ani budowy pakietów Nix; renderowanie sprawdzono przez TestBackend. Omijanie przeszkód, wydobycie rudy, ekonomia i walka pozostają poza zakresem tej zmiany.
|
||||
|
||||
## 2026-10-18 — przeniesienie testu audio
|
||||
|
||||
Przeniesiono `audio_repeats_after_end_of_file` z `src/app/threads/audio.rs` do `tests/audio.rs`, bez zmiany jego asercji. Udostępniono `load_audio` przez `app::threads`, aby test integracyjny nadal sprawdzał produkcyjną funkcję dekodowania. Zaktualizowano `docs/TESTING.md`.
|
||||
|
||||
Weryfikacja przez `nix develop -c`: `cargo check --all-targets`, `cargo test --all-targets` i `cargo fmt --all -- --check` zakończone sukcesem. Zestaw nadal liczy 23 testy. Nie powtarzano odsłuchu ani pomiarów CPU; zachowanie odtwarzania nie było zmieniane.
|
||||
|
||||
## 2026-10-18 — zestaw testów projektu
|
||||
|
||||
Dodano 22 testy integracyjne w `tests/cli.rs`, `tests/app.rs`, `tests/board.rs` i `tests/tasks.rs`. Razem z wcześniejszym testem audio zestaw liczy 23 przypadki. Zakres i polecenia zapisano w `docs/TESTING.md`, a mapę i instrukcje agentów zaktualizowano.
|
||||
|
||||
Testy obejmują walidację CLI i uruchomienie binarki bez TUI, menu i klawisze potyczki, przesyłanie komend audio, renderowanie ekranów przez TestBackend, generowanie planszy, granice kursora i kamery, zaznaczanie/cofanie/anulowanie ścieżki, kolejki i postęp zadań.
|
||||
|
||||
Najpierw uruchomiono testy na istniejącym kodzie. Wykryły trzy błędy, następnie poprawione w ich wspólnych implementacjach:
|
||||
|
||||
- `BoardState::new`: odejmowanie liczby widocznych wierszy od wysokości mapy powodowało panic przy dużym terminalu; użyto odejmowania nasycającego.
|
||||
- `Offset`: konstruktor i settery pozwalały przekroczyć limit, także po resize; wartość jest teraz ograniczana do maksimum.
|
||||
- `DiggingTask::progress`: obliczony postęp był odrzucany; jest teraz zapisywany z ograniczeniem do 0–100.
|
||||
|
||||
Nie włączano `advance_turn` ani nie zmieniano zasad symulacji. Zastane zmiany zależności w `Cargo.toml` i `Cargo.lock` oraz wcześniejsze zmiany audio pozostawiono; nie dodano zależności testowych.
|
||||
|
||||
Weryfikacja przez `nix develop -c`: `cargo check --all-targets`, `cargo test --all-targets` (23 zaliczone, 0 błędów), `cargo fmt --all -- --check` i `cargo clippy --all-targets` zakończone sukcesem. Clippy raportuje 51 ostrzeżeń dla biblioteki; nowe pliki testów nie zgłaszają ostrzeżeń. Nie sprawdzano urządzenia audio, pełnej symulacji ani pakowania Nix.
|
||||
|
||||
## 2026-10-18 — usunięcie aktywnej pętli audio
|
||||
|
||||
Przyczyna obciążenia: `handle_audio` bez oczekiwania wykonywał `rx.try_iter()` i `player.empty()`, także podczas normalnego odtwarzania bez komend.
|
||||
|
||||
Zmiany w `src/app/threads/audio.rs`:
|
||||
|
||||
- Zapętlanie przeniesiono do `Decoder::new_looped`; nie trzeba sprawdzać końca utworu ani ponownie kopiować danych z cache przy każdym powtórzeniu.
|
||||
- Odbiór komend blokuje wątek przez `for cmd in rx`; zamknięcie kanału kończy wątek. Zachowano obsługę mute, głośności, zmiany części i soundtracku oraz dotychczasowy poziom wzmocnienia.
|
||||
- Dodano test regresji odczytujący cały `calm.ogg` i kolejne 1024 próbki z zapętlonego źródła. Nie wymaga urządzenia audio.
|
||||
- Zaktualizowano `docs/CODE_MAP.md`. Bez zmian zależności, CLI, zasobów i konfiguracji Nix.
|
||||
|
||||
Weryfikacja przez `nix develop -c`: `cargo check --all-targets`, `cargo test --all-targets` (1 test zaliczony), `cargo fmt --all -- --check` i `cargo clippy --all-targets` zakończone sukcesem. Clippy nadal raportuje 54 ostrzeżenia dla biblioteki.
|
||||
|
||||
Nie wykonano pomiaru CPU przed/po na urządzeniu docelowym ani odsłuchu i interaktywnego sprawdzenia komend. Usunięcie aktywnego odpytywania wynika z przepływu kodu; konkretnego procentowego spadku CPU nie zmierzono.
|
||||
|
||||
## 2026-10-18 — obowiązek poprawnej kompilacji
|
||||
|
||||
Dopisano do `docs/AGENTS.md` zakaz kończenia zadania z błędami kompilacji Rust oraz obowiązek wykonania `nix develop -c cargo check --all-targets` i usunięcia błędów przed zakończeniem. Zmiana dotyczy wyłącznie dokumentacji.
|
||||
|
||||
Weryfikacja: `nix develop -c cargo check --all-targets` zakończone sukcesem. Nie powtarzano testów działania gry przy zmianie samej dokumentacji.
|
||||
|
||||
## 2026-10-18 — przygotowanie kontekstu repozytorium
|
||||
|
||||
Cel: rozpoznać architekturę i przepływy oraz przygotować trwały kontekst do kolejnych zadań.
|
||||
|
||||
Wykonano:
|
||||
|
||||
- Przejrzano źródła Rust, manifest, konfigurację Nix, README, strukturę zasobów i szkiców UI.
|
||||
- Przygotowano `docs/CODE_MAP.md`: odpowiedzialności modułów, diagramy uruchomienia i renderowania, przepływ klawiszy i zadań, audio, pakowanie oraz miejsca rozszerzeń.
|
||||
- Przygotowano `docs/AGENTS.md`: punkt wejścia dla agentów, zasady pracy, polecenia i ograniczenia weryfikacji.
|
||||
- Oddzielono działające ścieżki od placeholderów i niepodłączonej symulacji. Zapisano ryzyka w kodzie kopania do sprawdzenia przed jego aktywacją.
|
||||
- Nie zmieniano źródeł gry ani konfiguracji. Zastana modyfikacja `flake.lock` należy do wcześniejszego stanu roboczego.
|
||||
|
||||
Weryfikacja przez środowisko Nix:
|
||||
|
||||
| Polecenie | Wynik |
|
||||
|---|---|
|
||||
| `nix develop -c cargo test --all-targets` | Sukces; oba targety wykonują 0 testów |
|
||||
| `nix develop -c cargo fmt --all -- --check` | Sukces |
|
||||
| `nix develop -c cargo clippy --all-targets` | Sukces z 54 ostrzeżeniami dla biblioteki, raportowanymi też dla targetu testowego |
|
||||
| `cargo run -- -h` wykonywane automatycznie przez shellHook | Kompilacja i pomoc CLI działają |
|
||||
|
||||
Nie wykonano: interaktywnej rozgrywki, odtwarzania audio, budowy pakietów Nix ani napraw znalezionych problemów. Brak testów oznacza, że pomyślna kompilacja nie potwierdza poprawności mechanik. Kolejne zmiany należy dopisywać jako osobne wpisy z ich walidacją.
|
||||
@@ -0,0 +1,68 @@
|
||||
# Testy
|
||||
|
||||
Uruchomienie całego zestawu z katalogu głównego:
|
||||
|
||||
```sh
|
||||
nix develop -c cargo test --all-targets --locked
|
||||
```
|
||||
|
||||
Testy korzystają z wbudowanego mechanizmu Rust, Clap i `ratatui::backend::TestBackend`. Nie dodano zależności. Nie wymagają terminala interaktywnego ani urządzenia audio. Test audio używa pliku `soundtracks/default/calm.ogg`, więc zasoby muszą być dostępne w repozytorium.
|
||||
|
||||
## Zakres
|
||||
|
||||
| Plik | Liczba | Zachowania |
|
||||
|---|---:|---|
|
||||
| `tests/cli.rs` | 5 | Domyślne ustawienia, granice wszystkich opcji liczbowych, XP (także NaN i infinity), błędne enumy, flagi audio, uruchomienie binarki z help/version i błędnym argumentem |
|
||||
| `tests/app.rs` | 7 | Nawigacja menu, wybór ekranów, wyjście, ignorowanie zwolnienia klawisza, komendy audio i zamknięty kanał, panel, zoom, ruch, renderowanie wszystkich widoków bez mutacji stanu, wejście przed inicjalizacją |
|
||||
| `tests/board.rs` | 7 | Rozmieszczenie baz/złóż/jednostek, granice kursora i kamery, zatwierdzanie/anulowanie ścieżki, cofanie z powtórzonym polem, duży viewport, resize i zoom |
|
||||
| `tests/tasks.rs` | 7 | Przyrost i limit kopania, reset etapu, pusta ścieżka, kolejność zadań, timer MovingTask, całkowity postęp, zachowanie liczby etapów przy konwersji, formatowanie postępu aktywnych i oczekujących zadań |
|
||||
| `tests/audio.rs` | 2 | Zapętlanie rzeczywistego pliku OGG po jego końcu i ładowanie soundtracków obok binarki poza katalogiem repozytorium |
|
||||
| `tests/digging.rs` | 18 | Wykonywanie kopania i ruchu, kolejki, 3 sekundy na krok, wejście po wykopaniu, blokady i walidacja, kolory i procenty w TUI, podział trasy Moving/Digging, ochrona struktur, zmiana rodzaju zadania, panel Tab z postępem etapu i całego zadania |
|
||||
|
||||
Łącznie: 46 testów. Wszystkie testy znajdują się w `tests/` i sprawdzają publiczne interfejsy biblioteki lub binarkę. Funkcja `load_audio` jest eksportowana przez `app::threads`, dzięki czemu test korzysta z tego samego dekodera co odtwarzanie.
|
||||
|
||||
Wybrane zestawy i pojedynczy przypadek:
|
||||
|
||||
```sh
|
||||
nix develop -c cargo test --test cli
|
||||
nix develop -c cargo test --test app
|
||||
nix develop -c cargo test --test board
|
||||
nix develop -c cargo test --test tasks
|
||||
nix develop -c cargo test --test audio
|
||||
nix develop -c cargo test --test digging
|
||||
nix develop -c cargo test --test board large_viewport_and_zoom_keep_camera_inside_map
|
||||
```
|
||||
|
||||
Kontrole uzupełniające:
|
||||
|
||||
```sh
|
||||
nix develop -c cargo check --all-targets --locked
|
||||
nix develop -c cargo fmt --all -- --check
|
||||
nix develop -c cargo clippy --all-targets --locked
|
||||
```
|
||||
|
||||
`shellHook` środowiska Nix wyświetla dostępne polecenia; nie kompiluje automatycznie programu.
|
||||
|
||||
## CI i profile kompilacji
|
||||
|
||||
Testowe paczki deb, rpm, AppImage, AUR i Flatpak sprawdza niezależny `.gitea/workflows/packages.yml`, bez publikowania wydań. Zasady budowania, artefakty i ograniczenia opisano w [PACKAGING.md](PACKAGING.md).
|
||||
|
||||
Osobny workflow `.gitea/workflows/nix.yml` (push i pull request) instaluje Nix w `node:22-bookworm` i wykonuje `nix build .#default --no-update-lock-file --no-link --print-out-paths --print-build-logs`. Atrybut `.#default` wywołuje `pkgs.callPackage ./default.nix {}` z nixpkgs przypiętym w `flake.lock`; nie buduje wariantu `.#develop` opartego na Fenix. Budowa obejmuje standardową fazę testów `buildRustPackage` i włączony w pakiecie `versionCheckHook`. Nie wymaga devShell ani uruchamiania gry. Instalator potrzebuje sudo; KVM jest wyłączone. Workflow nie konfiguruje trwałego cache magazynu Nix, więc nowy job może ponownie pobierać zależności. Kontrole Rust opisane poniżej pozostają niezależne.
|
||||
|
||||
Workflow `.gitea/workflows/rust.yml` uruchamia kontrole po pushu i dla pull requestów. Wymaga włączonych Actions w repozytorium Gitea i runnera Linux x86_64 o etykiecie `ubuntu-latest`, obsługującego akcje Node. Nazwy akcji zawierają pełne adresy GitHub. Konfiguracja nie rejestruje runnera ani nie włącza Actions na serwerze; zobacz [instrukcję Gitea](https://docs.gitea.com/usage/actions/quickstart/).
|
||||
|
||||
Workflow określa obraz zadania jawnie jako `node:22-bookworm`. Kontener działa jako root i instaluje pakiety potrzebne do budowania: build-essential, pkg-config i libasound2-dev oraz curl, ca-certificates i zstd. Rust 1.99.1 z rustfmt i Clippy jest instalowany przez rustup. CI nie instaluje Nix i nie wykonuje jego shellHook.
|
||||
|
||||
Wersję Rust w CI określa `RUST_VERSION` w workflow, niezależnie od lokalnego `flake.lock`. Formatowanie, kompilacja, testy i Clippy są oddzielnymi krokami Cargo. Polecenia kompilujące projekt mają `--locked`; Clippy nie używa `-D warnings` z powodu istniejących ostrzeżeń. `actions/cache@v4` zachowuje osobno toolchain oraz rejestr Cargo, repozytoria git i `target`. Klucze uwzględniają wersję Rust, a dla Cargo także manifest, lockfile i kod; klucze częściowe pozwalają ponownie wykorzystać wcześniejsze kompilacje. Działanie cache wymaga dostępnego serwera cache runnera. Pierwszy przebieg wypełnia cache; błędy pobierania cache mogą spowodować ponowną instalację i kompilację.
|
||||
|
||||
Release ma `overflow-checks = true`. Ten sam zestaw 46 testów można wykonać z kontrolą przepełnień w profilu produkcyjnym:
|
||||
|
||||
```sh
|
||||
nix develop -c cargo test --all-targets --release --locked
|
||||
```
|
||||
|
||||
Manifest określa minimalną wspieraną wersję Rust jako 1.88. Potwierdzono `cargo check --all-targets --locked` na Rust 1.88.0 dostarczonym tymczasowo przez Fenix (`toolchainOf`, kanał `1.88.0`) wewnątrz `nix develop`. Domyślny toolchain pozostaje zarządzany przez Fenix i `flake.lock`; nie dodano drugiego pliku przypinającego toolchain. Linty z `Cargo.toml` zabraniają `unsafe` we własnym kodzie oraz ignorowania wyników `must_use`; `unused_unit` jest dozwolony, by zachować wymagane jawne `-> ()`.
|
||||
|
||||
## Ograniczenia
|
||||
|
||||
Zestaw weryfikuje wykonywanie zadań przez ticki potyczki, ale nie pełną rozgrywkę, ekonomię ani walkę. Nie potwierdza odsłuchu, zużycia CPU, obsługi urządzeń, pakowania Nix ani wszystkich układów małych terminali. Testy renderowania są kontrolą podstawową, bez utrwalania pełnych zrzutów ekranów. Nowy workflow bez Nix wymaga przebiegu na Gitea; odczyt cache należy potwierdzić w kolejnym przebiegu.
|
||||
Generated
+12
-12
@@ -8,11 +8,11 @@
|
||||
"rust-analyzer-src": "rust-analyzer-src"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1775462255,
|
||||
"narHash": "sha256-YRzdvh6nvMebcgO2nDpr8dqVwKHpp1BBRUHeMxX9UAY=",
|
||||
"lastModified": 1792320163,
|
||||
"narHash": "sha256-3YqCuAdykExGnEFw44w+8qsjdOEZMBCViDpvue5ENoc=",
|
||||
"owner": "nix-community",
|
||||
"repo": "fenix",
|
||||
"rev": "f90343f1ed330243d4bbdbce51acbd93b776a797",
|
||||
"rev": "53d7844fe302be84e5e63b2ca92a2f84c6490b4e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -31,11 +31,11 @@
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1769799857,
|
||||
"narHash": "sha256-88IFXZ7Sa1vxbz5pty0Io5qEaMQMMUPMonLa3Ls/ss4=",
|
||||
"lastModified": 1792096940,
|
||||
"narHash": "sha256-75jRkpI2iya4fd/FPOyE9+BuiAp/2sV/hYtwrj6IyME=",
|
||||
"owner": "nix-community",
|
||||
"repo": "naersk",
|
||||
"rev": "9d4ed44d8b8cecdceb1d6fd76e74123d90ae6339",
|
||||
"rev": "5c7912af5141e323ff0bd25c95bd6d3b889b8c53",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -46,11 +46,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1775036866,
|
||||
"narHash": "sha256-ZojAnPuCdy657PbTq5V0Y+AHKhZAIwSIT2cb8UgAz/U=",
|
||||
"lastModified": 1792094642,
|
||||
"narHash": "sha256-zp8ToDMnqZcu4o4IH9pdwXFXGp7yLm/M0dEnNWnyd7I=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "6201e203d09599479a3b3450ed24fa81537ebc4e",
|
||||
"rev": "fe13242281a67641c0102cdb1087f0ebe586c7df",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@@ -70,11 +70,11 @@
|
||||
"rust-analyzer-src": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1775429583,
|
||||
"narHash": "sha256-bFC/p7Ywyd9QIr9DbU3Q75c7AcaCm9wVmEvcI3702cY=",
|
||||
"lastModified": 1792141191,
|
||||
"narHash": "sha256-H161gJK9JlpkP3bGnsbTdzzRcP51oiFYrHbCdsQH4kc=",
|
||||
"owner": "rust-lang",
|
||||
"repo": "rust-analyzer",
|
||||
"rev": "38fb8f92ac15853d7fa9fb47fc2d81fdd5cd6c7e",
|
||||
"rev": "70e0c46be740a471bcc95f2be290f4bbfdcffd87",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
||||
@@ -70,8 +70,6 @@
|
||||
echo " nix run - Run production version"
|
||||
echo " nix build .#develop - Build development version"
|
||||
echo " nix run .#develop - Run development version"
|
||||
echo ""
|
||||
cargo run -- -h
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
# Test recipe: CI supplies the source archive from the checked-out commit.
|
||||
pkgname=war-in-tunnels
|
||||
pkgver=0.1.0
|
||||
pkgrel=1
|
||||
pkgdesc='Underground strategy game in your terminal'
|
||||
arch=('x86_64')
|
||||
url='https://gitea.garandplg.com/GarandPLG/war-in-tunnels'
|
||||
license=('MIT')
|
||||
depends=('alsa-lib' 'gcc-libs' 'glibc')
|
||||
makedepends=('cargo' 'pkgconf')
|
||||
options=('!debug')
|
||||
source=("$pkgname-$pkgver.tar.gz")
|
||||
sha256sums=('SKIP')
|
||||
|
||||
build() {
|
||||
cd "$srcdir/$pkgname-$pkgver"
|
||||
cargo build --release --locked
|
||||
}
|
||||
|
||||
check() {
|
||||
cd "$srcdir/$pkgname-$pkgver"
|
||||
cargo test --release --locked
|
||||
}
|
||||
|
||||
package() {
|
||||
cd "$srcdir/$pkgname-$pkgver"
|
||||
sh packaging/stage.sh "$pkgdir/usr"
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
format=${1:?expected deb, rpm, appimage or flatpak}
|
||||
version=$(python3 -c 'import tomllib; print(tomllib.load(open("Cargo.toml", "rb"))["package"]["version"])')
|
||||
mkdir -p dist
|
||||
work=$(mktemp -d)
|
||||
trap 'rm -rf "$work"' EXIT
|
||||
sh packaging/stage.sh "$work/root/usr"
|
||||
desktop-file-validate "$work/root/usr/share/applications/com.garandplg.WarInTunnels.desktop"
|
||||
"$work/root/usr/bin/war-in-tunnels" --version | grep -Fx "war_in_tunnels $version"
|
||||
test -s "$work/root/usr/lib/war-in-tunnels/soundtracks/default/calm.ogg"
|
||||
|
||||
case "$format" in
|
||||
deb)
|
||||
mkdir -p "$work/root/DEBIAN"
|
||||
cat > "$work/root/DEBIAN/control" <<EOF
|
||||
Package: war-in-tunnels
|
||||
Version: $version
|
||||
Architecture: amd64
|
||||
Maintainer: GarandPLG
|
||||
Depends: libasound2, libc6 (>= 2.36), libgcc-s1
|
||||
Section: games
|
||||
Priority: optional
|
||||
Description: Underground strategy game in your terminal
|
||||
EOF
|
||||
output="$PWD/dist/war-in-tunnels_${version}_amd64.deb"
|
||||
dpkg-deb --root-owner-group --build "$work/root" "$output"
|
||||
dpkg-deb --info "$output"
|
||||
dpkg-deb --extract "$output" "$work/extracted"
|
||||
"$work/extracted/usr/bin/war-in-tunnels" --version
|
||||
;;
|
||||
rpm)
|
||||
rpmbuild -bb packaging/war-in-tunnels.spec \
|
||||
--define "_topdir $work/rpmbuild" \
|
||||
--define "package_version $version" \
|
||||
--define "staging $work/root" \
|
||||
--define '_build_id_links none'
|
||||
cp "$work"/rpmbuild/RPMS/x86_64/*.rpm dist/
|
||||
rpm -qp --requires dist/*.rpm
|
||||
mkdir "$work/extracted"
|
||||
rpm2cpio dist/*.rpm | (cd "$work/extracted" && cpio -idm --quiet)
|
||||
"$work/extracted/usr/bin/war-in-tunnels" --version
|
||||
;;
|
||||
appimage)
|
||||
appdir="$work/WarInTunnels.AppDir"
|
||||
mv "$work/root" "$appdir"
|
||||
cp packaging/com.garandplg.WarInTunnels.desktop "$appdir/"
|
||||
cp packaging/com.garandplg.WarInTunnels.svg "$appdir/"
|
||||
mkdir -p "$appdir/usr/lib/bundled"
|
||||
# Bundle ALSA; glibc and the ELF loader come from the host.
|
||||
cp -L /usr/lib/x86_64-linux-gnu/libasound.so.2 "$appdir/usr/lib/bundled/"
|
||||
cat > "$appdir/AppRun" <<'EOF'
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
appdir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
export LD_LIBRARY_PATH="$appdir/usr/lib/bundled${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"
|
||||
exec "$appdir/usr/bin/war-in-tunnels" "$@"
|
||||
EOF
|
||||
chmod +x "$appdir/AppRun"
|
||||
curl -fL --retry 3 https://github.com/AppImage/appimagetool/releases/download/1.9.1/appimagetool-x86_64.AppImage -o "$work/appimagetool"
|
||||
# Extract without executing the runtime: works without FUSE and with host binfmt handlers.
|
||||
7z x "$work/appimagetool" "-o$work/appimagetool-dir" > /dev/null
|
||||
output="$PWD/dist/war-in-tunnels-${version}-x86_64.AppImage"
|
||||
ARCH=x86_64 "$work/appimagetool-dir/AppRun" --no-appstream "$appdir" "$output"
|
||||
7z x "$output" "-o$work/appimage-check" > /dev/null
|
||||
"$work/appimage-check/AppRun" --version
|
||||
;;
|
||||
flatpak)
|
||||
appid=com.garandplg.WarInTunnels
|
||||
# Package the Debian-built binary against a newer glibc runtime; no nested sandbox is needed.
|
||||
flatpak --user remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
|
||||
flatpak --user install -y --noninteractive --no-related flathub org.freedesktop.Platform//25.08
|
||||
flatpak build-init "$work/flatpak" "$appid" org.freedesktop.Platform org.freedesktop.Platform 25.08
|
||||
cp -a "$work/root/usr/." "$work/flatpak/files/"
|
||||
install -Dm755 /usr/lib/x86_64-linux-gnu/libasound.so.2 "$work/flatpak/files/lib/libasound.so.2"
|
||||
flatpak build-finish --command=war-in-tunnels --socket=pulseaudio --device=all "$work/flatpak"
|
||||
# The icon is repository-owned; its validator cannot create a sandbox in standard Docker.
|
||||
flatpak build-export --disable-sandbox "$work/repo" "$work/flatpak" test
|
||||
output="$PWD/dist/war-in-tunnels-${version}-x86_64.flatpak"
|
||||
flatpak build-bundle --runtime-repo=https://flathub.org/repo/flathub.flatpakrepo "$work/repo" "$output" "$appid" test
|
||||
flatpak --user install -y --noninteractive --no-related "$output"
|
||||
location=$(flatpak --user info --show-location "$appid")
|
||||
test -s "$location/files/lib/war-in-tunnels/soundtracks/default/calm.ogg"
|
||||
"$location/files/bin/war-in-tunnels" --version
|
||||
;;
|
||||
*)
|
||||
echo "Unknown package format: $format" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
@@ -0,0 +1,8 @@
|
||||
[Desktop Entry]
|
||||
Type=Application
|
||||
Name=War in Tunnels
|
||||
Comment=Underground strategy game in your terminal
|
||||
Exec=war-in-tunnels
|
||||
Icon=com.garandplg.WarInTunnels
|
||||
Terminal=true
|
||||
Categories=Game;StrategyGame;
|
||||
@@ -0,0 +1,5 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="128" height="128" viewBox="0 0 128 128">
|
||||
<rect width="128" height="128" rx="16" fill="#20252b"/>
|
||||
<path d="M20 104V56a44 44 0 0 1 88 0v48H88V56a24 24 0 0 0-48 0v48Z" fill="#bd985e"/>
|
||||
<path d="M51 91V59l13 15 13-15v32" fill="none" stroke="#e9edf0" stroke-width="7"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 325 B |
@@ -0,0 +1,18 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
prefix=${1:?installation prefix required}
|
||||
binary=${2:-target/release/war_in_tunnels}
|
||||
install -Dm755 "$binary" "$prefix/lib/war-in-tunnels/war-in-tunnels"
|
||||
mkdir -p "$prefix/bin"
|
||||
cat > "$prefix/bin/war-in-tunnels" <<'EOF'
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
bindir=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)
|
||||
exec "$bindir/../lib/war-in-tunnels/war-in-tunnels" "$@"
|
||||
EOF
|
||||
chmod +x "$prefix/bin/war-in-tunnels"
|
||||
cp -r soundtracks "$prefix/lib/war-in-tunnels/"
|
||||
install -Dm644 LICENSE "$prefix/share/licenses/war-in-tunnels/LICENSE"
|
||||
install -Dm644 packaging/com.garandplg.WarInTunnels.desktop "$prefix/share/applications/com.garandplg.WarInTunnels.desktop"
|
||||
install -Dm644 packaging/com.garandplg.WarInTunnels.svg "$prefix/share/icons/hicolor/scalable/apps/com.garandplg.WarInTunnels.svg"
|
||||
@@ -0,0 +1,21 @@
|
||||
Name: war-in-tunnels
|
||||
Version: %{package_version}
|
||||
Release: 1
|
||||
Summary: Underground strategy game in your terminal
|
||||
License: MIT
|
||||
URL: https://gitea.garandplg.com/GarandPLG/war-in-tunnels
|
||||
Requires: alsa-lib
|
||||
|
||||
%description
|
||||
A terminal-based underground strategy game.
|
||||
|
||||
%install
|
||||
mkdir -p %{buildroot}
|
||||
cp -a %{staging}/. %{buildroot}/
|
||||
|
||||
%files
|
||||
/usr/bin/war-in-tunnels
|
||||
/usr/lib/war-in-tunnels
|
||||
/usr/share/applications/com.garandplg.WarInTunnels.desktop
|
||||
/usr/share/icons/hicolor/scalable/apps/com.garandplg.WarInTunnels.svg
|
||||
%license /usr/share/licenses/war-in-tunnels/LICENSE
|
||||
@@ -0,0 +1,11 @@
|
||||
edition = "2024"
|
||||
style_edition = "2024"
|
||||
|
||||
max_width = 100
|
||||
hard_tabs = false
|
||||
tab_spaces = 2
|
||||
newline_style = "Unix"
|
||||
|
||||
reorder_imports = true
|
||||
reorder_modules = true
|
||||
use_small_heuristics = "Default"
|
||||
Binary file not shown.
+83
-77
@@ -1,96 +1,102 @@
|
||||
use crate::{
|
||||
app::{
|
||||
GameStates, handle_keybindings,
|
||||
threads::{AppChannels, AudioCmd},
|
||||
view::View,
|
||||
},
|
||||
cli::Cli,
|
||||
app::{GameStates, handle_keybindings, threads::AudioCmd, view::View},
|
||||
cli::Cli,
|
||||
};
|
||||
use ratatui::{DefaultTerminal, Frame, crossterm::event::KeyEvent, layout::Rect};
|
||||
use std::{io::Result, sync::mpsc::Sender, thread::sleep, time::Duration};
|
||||
use std::{
|
||||
io::Result,
|
||||
sync::mpsc::{Receiver, Sender},
|
||||
thread::sleep,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
pub struct App {
|
||||
pub exit: bool,
|
||||
pub view: View,
|
||||
pub window_area: Rect,
|
||||
pub args: Cli,
|
||||
pub states: Option<GameStates>,
|
||||
pub audio_tx: Sender<AudioCmd>,
|
||||
pub exit: bool,
|
||||
pub view: View,
|
||||
pub window_area: Rect,
|
||||
pub args: Cli,
|
||||
pub states: Option<GameStates>,
|
||||
pub audio_tx: Sender<AudioCmd>,
|
||||
}
|
||||
|
||||
impl App {
|
||||
pub fn new(args: Cli, audio_tx: Sender<AudioCmd>) -> Self {
|
||||
Self {
|
||||
exit: false,
|
||||
view: args.view,
|
||||
window_area: Rect::default(),
|
||||
args: args,
|
||||
states: None,
|
||||
audio_tx,
|
||||
}
|
||||
pub fn new(args: Cli, audio_tx: Sender<AudioCmd>) -> Self {
|
||||
Self {
|
||||
exit: false,
|
||||
view: args.view,
|
||||
window_area: Rect::default(),
|
||||
args: args,
|
||||
states: None,
|
||||
audio_tx,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn states(&self) -> Option<&GameStates> {
|
||||
self.states.as_ref()
|
||||
}
|
||||
pub fn states_ref(&self) -> Option<&GameStates> {
|
||||
self.states.as_ref()
|
||||
}
|
||||
|
||||
pub fn states_mut(&mut self) -> Option<&mut GameStates> {
|
||||
self.states.as_mut()
|
||||
}
|
||||
pub fn states_mut(&mut self) -> Option<&mut GameStates> {
|
||||
self.states.as_mut()
|
||||
}
|
||||
|
||||
pub fn run(&mut self, terminal: &mut DefaultTerminal, channels: AppChannels) -> Result<()> {
|
||||
while !self.exit {
|
||||
terminal.draw(|frame: &mut Frame<'_>| self.draw(frame))?;
|
||||
pub fn run(
|
||||
&mut self,
|
||||
terminal: &mut DefaultTerminal,
|
||||
input_rx: Receiver<KeyEvent>,
|
||||
resize_rx: Receiver<(u16, u16)>,
|
||||
tick_rx: Receiver<()>,
|
||||
) -> Result<()> {
|
||||
while !self.exit {
|
||||
terminal.draw(|frame: &mut Frame<'_>| self.draw(frame))?;
|
||||
|
||||
if let Ok(key) = channels.input_rx.try_recv() {
|
||||
self.handle_key_event(key)?;
|
||||
}
|
||||
if let Ok(key) = input_rx.try_recv() {
|
||||
self.handle_key_event(key)?;
|
||||
}
|
||||
|
||||
if let Ok((_, _)) = channels.resize_rx.try_recv() {
|
||||
let window_area: Rect = self.window_area;
|
||||
if let Some(state) = self.states_mut() {
|
||||
state
|
||||
.skirmish
|
||||
.board
|
||||
.change_resize(&window_area, state.skirmish.side_panel);
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(()) = channels.tick_rx.try_recv() {
|
||||
self.update()?;
|
||||
}
|
||||
|
||||
sleep(Duration::from_millis(10));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw(&mut self, frame: &mut Frame<'_>) {
|
||||
let window_area: Rect = frame.area();
|
||||
|
||||
if window_area.width != self.window_area.width
|
||||
|| window_area.height != self.window_area.height
|
||||
{
|
||||
self.window_area = window_area;
|
||||
if self.states.is_none() {
|
||||
self.states = Some(GameStates::new(&self.args, &self.window_area))
|
||||
}
|
||||
}
|
||||
|
||||
frame.render_widget(self, window_area);
|
||||
}
|
||||
|
||||
fn handle_key_event(&mut self, key_event: KeyEvent) -> Result<()> {
|
||||
handle_keybindings(self, key_event);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update(&mut self) -> Result<()> {
|
||||
if let Ok((_, _)) = resize_rx.try_recv() {
|
||||
let window_area: Rect = self.window_area;
|
||||
if let Some(state) = self.states_mut() {
|
||||
state.skirmish.tick_update();
|
||||
state
|
||||
.skirmish
|
||||
.board
|
||||
.change_resize(&window_area, state.skirmish.side_panel);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
if let Ok(()) = tick_rx.try_recv() {
|
||||
self.update()?;
|
||||
}
|
||||
|
||||
sleep(Duration::from_millis(10));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw(&mut self, frame: &mut Frame<'_>) {
|
||||
let window_area: Rect = frame.area();
|
||||
|
||||
if window_area.width != self.window_area.width || window_area.height != self.window_area.height
|
||||
{
|
||||
self.window_area = window_area;
|
||||
if self.states.is_none() {
|
||||
self.states = Some(GameStates::new(&self.args, &self.window_area))
|
||||
}
|
||||
}
|
||||
|
||||
frame.render_widget(self, window_area);
|
||||
}
|
||||
|
||||
fn handle_key_event(&mut self, key_event: KeyEvent) -> Result<()> {
|
||||
handle_keybindings(self, key_event);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update(&mut self) -> Result<()> {
|
||||
if let Some(state) = self.states_mut() {
|
||||
state.skirmish.tick_update();
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
use ratatui::{
|
||||
style::{Color, Style, Stylize},
|
||||
text::{Line, Span},
|
||||
style::{Color, Style, Stylize},
|
||||
text::{Line, Span},
|
||||
};
|
||||
|
||||
/// Creates a styled title `Line` for a UI block.
|
||||
@@ -9,27 +9,27 @@ use ratatui::{
|
||||
/// the spans, so the returned `Line` owns all its data and does not borrow from
|
||||
/// the caller.
|
||||
pub fn block_title_helper(
|
||||
texts: &[(String, Color)],
|
||||
separator: Option<&'static str>,
|
||||
texts: &[(String, Color)],
|
||||
separator: Option<&'static str>,
|
||||
) -> Line<'static> {
|
||||
let mut line: Line<'static> = Line::default();
|
||||
let mut line: Line<'static> = Line::default();
|
||||
|
||||
line.spans.push(Span::from("[ ").gray());
|
||||
line.spans.push(Span::from("[ ").gray());
|
||||
|
||||
for (i, (text, color)) in texts.iter().enumerate() {
|
||||
let span = Span::styled(text.clone(), Style::new().fg(*color));
|
||||
line.spans.push(span);
|
||||
for (i, (text, color)) in texts.iter().enumerate() {
|
||||
let span: Span<'_> = Span::styled(text.clone(), Style::new().fg(*color));
|
||||
line.spans.push(span);
|
||||
|
||||
if let Some(sep) = separator {
|
||||
if i + 1 < texts.len() {
|
||||
line.spans.push(Span::from(sep).gray());
|
||||
}
|
||||
}
|
||||
if let Some(sep) = separator {
|
||||
if i + 1 < texts.len() {
|
||||
line.spans.push(Span::from(sep).gray());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
line.spans.push(Span::from(" ]").gray());
|
||||
line.spans.push(Span::from(" ]").gray());
|
||||
|
||||
line
|
||||
line
|
||||
}
|
||||
|
||||
/// Convenience wrapper for a single‑title line.
|
||||
@@ -38,5 +38,5 @@ pub fn block_title_helper(
|
||||
/// forwards it to `block_title_helper`. Because `block_title_helper` clones the
|
||||
/// string, the temporary slice is safe.
|
||||
pub fn block_single_title_helper(text: String, color: Color) -> Line<'static> {
|
||||
block_title_helper(&[(text, color)], None)
|
||||
block_title_helper(&[(text, color)], None)
|
||||
}
|
||||
@@ -1,17 +1,17 @@
|
||||
use crate::app::views::{skirmish_layout, skirmish_main_area_layout};
|
||||
use ratatui::{
|
||||
layout::{Margin, Rect},
|
||||
widgets::{Block, Borders},
|
||||
layout::{Margin, Rect},
|
||||
widgets::{Block, Borders},
|
||||
};
|
||||
|
||||
pub fn cells_area_helper(area: &Rect, side_panel: bool) -> Rect {
|
||||
let board_area: Rect = skirmish_main_area_layout(skirmish_layout(*area)[1], side_panel)[0];
|
||||
let board_area: Rect = skirmish_main_area_layout(skirmish_layout(*area)[1], side_panel)[0];
|
||||
|
||||
Block::new()
|
||||
.borders(Borders::LEFT | Borders::TOP | Borders::RIGHT)
|
||||
.inner(board_area)
|
||||
.inner(Margin {
|
||||
horizontal: 1,
|
||||
vertical: 1,
|
||||
})
|
||||
Block::new()
|
||||
.borders(Borders::LEFT | Borders::TOP | Borders::RIGHT)
|
||||
.inner(board_area)
|
||||
.inner(Margin {
|
||||
horizontal: 1,
|
||||
vertical: 1,
|
||||
})
|
||||
}
|
||||
@@ -1,13 +1,13 @@
|
||||
pub fn main_menu_option_helper(s: String) -> String {
|
||||
if let Some(pos) = s
|
||||
.char_indices()
|
||||
.filter(|(_, c)| c.is_ascii_uppercase())
|
||||
.nth(1)
|
||||
.map(|(i, _)| i)
|
||||
{
|
||||
let (first, second) = s.split_at(pos);
|
||||
format!("{first} {second}")
|
||||
} else {
|
||||
s
|
||||
}
|
||||
if let Some(pos) = s
|
||||
.char_indices()
|
||||
.filter(|(_, c)| c.is_ascii_uppercase())
|
||||
.nth(1)
|
||||
.map(|(i, _)| i)
|
||||
{
|
||||
let (first, second) = s.split_at(pos);
|
||||
format!("{first} {second}")
|
||||
} else {
|
||||
s
|
||||
}
|
||||
}
|
||||
+8
-8
@@ -1,14 +1,14 @@
|
||||
use crate::app::{
|
||||
App,
|
||||
keybindings::{default_keybindings, main_menu_keybindings, skirmish_keybindings},
|
||||
view::View,
|
||||
App,
|
||||
keybindings::{default_keybindings, main_menu_keybindings, skirmish_keybindings},
|
||||
view::View,
|
||||
};
|
||||
use ratatui::crossterm::event::KeyEvent;
|
||||
|
||||
pub fn handle_keybindings(app: &mut App, key_event: KeyEvent) {
|
||||
match app.view {
|
||||
View::MainMenu => main_menu_keybindings(app, &key_event),
|
||||
View::Skirmish => skirmish_keybindings(app, &key_event),
|
||||
_ => default_keybindings(app, &key_event),
|
||||
}
|
||||
match app.view {
|
||||
View::MainMenu => main_menu_keybindings(app, &key_event),
|
||||
View::Skirmish => skirmish_keybindings(app, &key_event),
|
||||
_ => default_keybindings(app, &key_event),
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,23 @@
|
||||
use crate::app::{
|
||||
App, View,
|
||||
keybindings::{Action, event_to_action},
|
||||
threads::AudioCmd,
|
||||
App, View,
|
||||
keybindings::{Action, event_to_action},
|
||||
threads::AudioCmd,
|
||||
};
|
||||
use ratatui::crossterm::event::KeyEvent;
|
||||
|
||||
pub fn common_keybindings(app: &mut App, action: Action) {
|
||||
match action {
|
||||
Action::Quit | Action::Quit2 => app.exit = true,
|
||||
Action::Esc => app.view = View::MainMenu,
|
||||
Action::Mute => if let Err(_) = app.audio_tx.send(AudioCmd::Mute) {},
|
||||
Action::VolumeUp => if let Err(_) = app.audio_tx.send(AudioCmd::VolumeUp) {},
|
||||
Action::VolumeDown => if let Err(_) = app.audio_tx.send(AudioCmd::VolumeDown) {},
|
||||
_ => (),
|
||||
}
|
||||
match action {
|
||||
Action::Quit | Action::Quit2 => app.exit = true,
|
||||
Action::Esc => app.view = View::MainMenu,
|
||||
Action::Mute => if let Err(_) = app.audio_tx.send(AudioCmd::Mute) {},
|
||||
Action::VolumeUp => if let Err(_) = app.audio_tx.send(AudioCmd::VolumeUp) {},
|
||||
Action::VolumeDown => if let Err(_) = app.audio_tx.send(AudioCmd::VolumeDown) {},
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn default_keybindings(app: &mut App, key_event: &KeyEvent) {
|
||||
if let Some(action) = event_to_action(&key_event) {
|
||||
common_keybindings(app, action);
|
||||
}
|
||||
if let Some(action) = event_to_action(&key_event) {
|
||||
common_keybindings(app, action);
|
||||
}
|
||||
}
|
||||
+306
-306
@@ -9,76 +9,76 @@ use std::collections::HashMap;
|
||||
/// character key and carries the actual character pressed.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub enum Action {
|
||||
/// Request the application to quit.
|
||||
Quit,
|
||||
/// Alternate quit action.
|
||||
Quit2,
|
||||
/// Move the selection cursor up.
|
||||
Up,
|
||||
/// Move the selection cursor down.
|
||||
Down,
|
||||
/// Move the selection cursor left.
|
||||
Left,
|
||||
/// Move the selection cursor right.
|
||||
Right,
|
||||
/// Scroll up without moving the cursor.
|
||||
ScrollUp,
|
||||
/// Scroll down without moving the cursor.
|
||||
ScrollDown,
|
||||
/// Scroll left without moving the cursor.
|
||||
ScrollLeft,
|
||||
/// Scroll right without moving the cursor.
|
||||
ScrollRight,
|
||||
/// Toggle selecting cell.
|
||||
Space,
|
||||
/// Unmark last selected cell.
|
||||
Backspace,
|
||||
/// Cancel marking cell.
|
||||
Delete,
|
||||
/// Submit or confirm the current choice.
|
||||
Enter,
|
||||
/// Return to the main menu or previous screen.
|
||||
Esc,
|
||||
/// Zoom the view in.
|
||||
ZoomIn,
|
||||
/// Zoom the view out.
|
||||
ZoomOut,
|
||||
/// Open side panel.
|
||||
Tab,
|
||||
/// Toggle skirmish board/skills view.
|
||||
ShiftTab,
|
||||
/// Mute music.
|
||||
Mute,
|
||||
/// Volume up.
|
||||
VolumeUp,
|
||||
/// Volume down.
|
||||
VolumeDown,
|
||||
/// Matches any character key; the inner `char` is the actual key pressed.
|
||||
WildCard(char),
|
||||
/// Request the application to quit.
|
||||
Quit,
|
||||
/// Alternate quit action.
|
||||
Quit2,
|
||||
/// Move the selection cursor up.
|
||||
Up,
|
||||
/// Move the selection cursor down.
|
||||
Down,
|
||||
/// Move the selection cursor left.
|
||||
Left,
|
||||
/// Move the selection cursor right.
|
||||
Right,
|
||||
/// Scroll up without moving the cursor.
|
||||
ScrollUp,
|
||||
/// Scroll down without moving the cursor.
|
||||
ScrollDown,
|
||||
/// Scroll left without moving the cursor.
|
||||
ScrollLeft,
|
||||
/// Scroll right without moving the cursor.
|
||||
ScrollRight,
|
||||
/// Toggle selecting cell.
|
||||
Space,
|
||||
/// Unmark last selected cell.
|
||||
Backspace,
|
||||
/// Cancel marking cell.
|
||||
Delete,
|
||||
/// Submit or confirm the current choice.
|
||||
Enter,
|
||||
/// Return to the main menu or previous screen.
|
||||
Esc,
|
||||
/// Zoom the view in.
|
||||
ZoomIn,
|
||||
/// Zoom the view out.
|
||||
ZoomOut,
|
||||
/// Open side panel.
|
||||
Tab,
|
||||
/// Toggle skirmish board/skills view.
|
||||
ShiftTab,
|
||||
/// Mute music.
|
||||
Mute,
|
||||
/// Volume up.
|
||||
VolumeUp,
|
||||
/// Volume down.
|
||||
VolumeDown,
|
||||
/// Matches any character key; the inner `char` is the actual key pressed.
|
||||
WildCard(char),
|
||||
}
|
||||
|
||||
/// Logical groups of key bindings used for organising help screens or
|
||||
/// configuration sections.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, ValueEnum)]
|
||||
pub enum Group {
|
||||
/// Bindings that combine `Ctrl` with movement keys.
|
||||
CtrlMovement,
|
||||
/// Plain movement bindings.
|
||||
Movement,
|
||||
/// Scroll bindings.
|
||||
Scroll,
|
||||
/// Selection bindings.
|
||||
Select,
|
||||
/// Text input related bindings.
|
||||
Input,
|
||||
/// Zoom related bindings.
|
||||
Zoom,
|
||||
/// Music related bindings.
|
||||
Music,
|
||||
/// Opens/Closes/Toggles elements in current view.
|
||||
Opener,
|
||||
/// Quit related bindings.
|
||||
Quit,
|
||||
/// Bindings that combine `Ctrl` with movement keys.
|
||||
CtrlMovement,
|
||||
/// Plain movement bindings.
|
||||
Movement,
|
||||
/// Scroll bindings.
|
||||
Scroll,
|
||||
/// Selection bindings.
|
||||
Select,
|
||||
/// Text input related bindings.
|
||||
Input,
|
||||
/// Zoom related bindings.
|
||||
Zoom,
|
||||
/// Music related bindings.
|
||||
Music,
|
||||
/// Opens/Closes/Toggles elements in current view.
|
||||
Opener,
|
||||
/// Quit related bindings.
|
||||
Quit,
|
||||
}
|
||||
|
||||
/// Description of a single key binding.
|
||||
@@ -87,20 +87,20 @@ pub enum Group {
|
||||
/// together with metadata used for displaying help information.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct KeyBinding {
|
||||
/// The logical action performed when this binding is triggered.
|
||||
pub action: Action,
|
||||
/// The raw `crossterm::event::KeyCode` that identifies the key.
|
||||
pub code: KeyCode,
|
||||
/// The kind of key event (crossterm::event::KeyEventKind).
|
||||
pub kind: KeyEventKind,
|
||||
/// Any modifier keys that must be held (crossterm::event::KeyModifiers).
|
||||
pub modifiers: KeyModifiers,
|
||||
/// The group this binding belongs to, useful for categorising help.
|
||||
pub group: Group,
|
||||
/// Human‑readable symbol shown in UI/help screens.
|
||||
pub symbol: &'static str,
|
||||
/// Short description of what the binding does.
|
||||
pub description: &'static str,
|
||||
/// The logical action performed when this binding is triggered.
|
||||
pub action: Action,
|
||||
/// The raw `crossterm::event::KeyCode` that identifies the key.
|
||||
pub code: KeyCode,
|
||||
/// The kind of key event (crossterm::event::KeyEventKind).
|
||||
pub kind: KeyEventKind,
|
||||
/// Any modifier keys that must be held (crossterm::event::KeyModifiers).
|
||||
pub modifiers: KeyModifiers,
|
||||
/// The group this binding belongs to, useful for categorising help.
|
||||
pub group: Group,
|
||||
/// Human‑readable symbol shown in UI/help screens.
|
||||
pub symbol: &'static str,
|
||||
/// Short description of what the binding does.
|
||||
pub description: &'static str,
|
||||
}
|
||||
|
||||
/// Complete list of all key bindings used by the application.
|
||||
@@ -108,218 +108,218 @@ pub struct KeyBinding {
|
||||
/// The slice is ordered primarily for readability; the runtime lookup scans
|
||||
/// this slice linearly, which is acceptable given the modest size of the list.
|
||||
static KEYBINDINGS: &[KeyBinding] = &[
|
||||
KeyBinding {
|
||||
action: Action::Quit,
|
||||
code: KeyCode::Char('q'),
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
group: Group::Quit,
|
||||
symbol: "q",
|
||||
description: "Quit",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::Quit2,
|
||||
code: KeyCode::Char('c'),
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
group: Group::Quit,
|
||||
symbol: "Ctrl + c",
|
||||
description: "Quit",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::Up,
|
||||
code: KeyCode::Up,
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
group: Group::Movement,
|
||||
symbol: "↑",
|
||||
description: "Up",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::Down,
|
||||
code: KeyCode::Down,
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
group: Group::Movement,
|
||||
symbol: "↓",
|
||||
description: "Down",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::Left,
|
||||
code: KeyCode::Left,
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
group: Group::Movement,
|
||||
symbol: "←",
|
||||
description: "Left",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::Right,
|
||||
code: KeyCode::Right,
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
group: Group::Movement,
|
||||
symbol: "→",
|
||||
description: "Right",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::ScrollUp,
|
||||
code: KeyCode::Up,
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
group: Group::CtrlMovement,
|
||||
symbol: "Ctrl + ↑",
|
||||
description: "Scroll Up ",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::ScrollDown,
|
||||
code: KeyCode::Down,
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
group: Group::CtrlMovement,
|
||||
symbol: "Ctrl + ↓",
|
||||
description: "Scroll Down",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::ScrollLeft,
|
||||
code: KeyCode::Left,
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
group: Group::CtrlMovement,
|
||||
symbol: "Ctrl + ←",
|
||||
description: "Scroll Left",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::ScrollRight,
|
||||
code: KeyCode::Right,
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
group: Group::CtrlMovement,
|
||||
symbol: "Ctrl + →",
|
||||
description: "Scroll Right",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::Space,
|
||||
code: KeyCode::Char(' '),
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
group: Group::Select,
|
||||
symbol: "Space",
|
||||
description: "Marking cells",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::Backspace,
|
||||
code: KeyCode::Backspace,
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
group: Group::Select,
|
||||
symbol: "Backspace",
|
||||
description: "Unmark cell",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::Delete,
|
||||
code: KeyCode::Delete,
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
group: Group::Select,
|
||||
symbol: "Delete",
|
||||
description: "Cancel marking",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::Enter,
|
||||
code: KeyCode::Enter,
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
group: Group::Select,
|
||||
symbol: "Enter",
|
||||
description: "Submit",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::Esc,
|
||||
code: KeyCode::Esc,
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
group: Group::Quit,
|
||||
symbol: "Esc",
|
||||
description: "Main menu",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::ZoomIn,
|
||||
code: KeyCode::Char(','),
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
group: Group::Zoom,
|
||||
symbol: ",",
|
||||
description: "Zoom in",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::ZoomOut,
|
||||
code: KeyCode::Char('.'),
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
group: Group::Zoom,
|
||||
symbol: ".",
|
||||
description: "Zoom out",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::Mute,
|
||||
code: KeyCode::Char('m'),
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
group: Group::Music,
|
||||
symbol: "m",
|
||||
description: "🔈",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::VolumeUp,
|
||||
code: KeyCode::Char('b'),
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
group: Group::Music,
|
||||
symbol: "b",
|
||||
description: "🔊",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::VolumeDown,
|
||||
code: KeyCode::Char('n'),
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
group: Group::Music,
|
||||
symbol: "n",
|
||||
description: "🔉",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::Tab,
|
||||
code: KeyCode::Tab,
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
group: Group::Opener,
|
||||
symbol: "Tab",
|
||||
description: "Toggle side panel",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::ShiftTab,
|
||||
code: KeyCode::Tab,
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::SHIFT,
|
||||
group: Group::Opener,
|
||||
symbol: "Shift + Tab",
|
||||
description: "Toggle Map/Skills window",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::WildCard('_'),
|
||||
code: KeyCode::Char('_'),
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
group: Group::Input,
|
||||
symbol: "[All]",
|
||||
description: "All keyboard characters",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::Quit,
|
||||
code: KeyCode::Char('q'),
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
group: Group::Quit,
|
||||
symbol: "q",
|
||||
description: "Quit",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::Quit2,
|
||||
code: KeyCode::Char('c'),
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
group: Group::Quit,
|
||||
symbol: "Ctrl + c",
|
||||
description: "Quit",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::Up,
|
||||
code: KeyCode::Up,
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
group: Group::Movement,
|
||||
symbol: "↑",
|
||||
description: "Up",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::Down,
|
||||
code: KeyCode::Down,
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
group: Group::Movement,
|
||||
symbol: "↓",
|
||||
description: "Down",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::Left,
|
||||
code: KeyCode::Left,
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
group: Group::Movement,
|
||||
symbol: "←",
|
||||
description: "Left",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::Right,
|
||||
code: KeyCode::Right,
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
group: Group::Movement,
|
||||
symbol: "→",
|
||||
description: "Right",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::ScrollUp,
|
||||
code: KeyCode::Up,
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
group: Group::CtrlMovement,
|
||||
symbol: "Ctrl + ↑",
|
||||
description: "Scroll Up ",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::ScrollDown,
|
||||
code: KeyCode::Down,
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
group: Group::CtrlMovement,
|
||||
symbol: "Ctrl + ↓",
|
||||
description: "Scroll Down",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::ScrollLeft,
|
||||
code: KeyCode::Left,
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
group: Group::CtrlMovement,
|
||||
symbol: "Ctrl + ←",
|
||||
description: "Scroll Left",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::ScrollRight,
|
||||
code: KeyCode::Right,
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
group: Group::CtrlMovement,
|
||||
symbol: "Ctrl + →",
|
||||
description: "Scroll Right",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::Space,
|
||||
code: KeyCode::Char(' '),
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
group: Group::Select,
|
||||
symbol: "Space",
|
||||
description: "Marking cells",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::Backspace,
|
||||
code: KeyCode::Backspace,
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
group: Group::Select,
|
||||
symbol: "Backspace",
|
||||
description: "Unmark cell",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::Delete,
|
||||
code: KeyCode::Delete,
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
group: Group::Select,
|
||||
symbol: "Delete",
|
||||
description: "Cancel marking",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::Enter,
|
||||
code: KeyCode::Enter,
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
group: Group::Select,
|
||||
symbol: "Enter",
|
||||
description: "Submit",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::Esc,
|
||||
code: KeyCode::Esc,
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
group: Group::Quit,
|
||||
symbol: "Esc",
|
||||
description: "Main menu",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::ZoomIn,
|
||||
code: KeyCode::Char(','),
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
group: Group::Zoom,
|
||||
symbol: ",",
|
||||
description: "Zoom in",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::ZoomOut,
|
||||
code: KeyCode::Char('.'),
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
group: Group::Zoom,
|
||||
symbol: ".",
|
||||
description: "Zoom out",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::Mute,
|
||||
code: KeyCode::Char('m'),
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
group: Group::Music,
|
||||
symbol: "m",
|
||||
description: "🔈",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::VolumeUp,
|
||||
code: KeyCode::Char('b'),
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
group: Group::Music,
|
||||
symbol: "b",
|
||||
description: "🔊",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::VolumeDown,
|
||||
code: KeyCode::Char('n'),
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
group: Group::Music,
|
||||
symbol: "n",
|
||||
description: "🔉",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::Tab,
|
||||
code: KeyCode::Tab,
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
group: Group::Opener,
|
||||
symbol: "Tab",
|
||||
description: "Toggle side panel",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::ShiftTab,
|
||||
code: KeyCode::Tab,
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::SHIFT,
|
||||
group: Group::Opener,
|
||||
symbol: "Shift + Tab",
|
||||
description: "Toggle Map/Skills window",
|
||||
},
|
||||
KeyBinding {
|
||||
action: Action::WildCard('_'),
|
||||
code: KeyCode::Char('_'),
|
||||
kind: KeyEventKind::Press,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
group: Group::Input,
|
||||
symbol: "[All]",
|
||||
description: "All keyboard characters",
|
||||
},
|
||||
];
|
||||
|
||||
/// Returns the `KeyBinding` associated with the given `Action`, if one exists.
|
||||
pub fn binding_for(action: Action) -> Option<&'static KeyBinding> {
|
||||
KEYBINDINGS.iter().find(|b| b.action == action)
|
||||
KEYBINDINGS.iter().find(|b| b.action == action)
|
||||
}
|
||||
|
||||
/// Converts a raw `KeyEvent` into an `Action` if a matching binding is found.
|
||||
@@ -330,23 +330,23 @@ pub fn binding_for(action: Action) -> Option<&'static KeyBinding> {
|
||||
/// was pressed. Returns `None` when the event does not correspond to any known
|
||||
/// action.
|
||||
pub fn event_to_action(event: &KeyEvent) -> Option<Action> {
|
||||
if let Some(b) = KEYBINDINGS
|
||||
.iter()
|
||||
.find(|b| b.code == event.code && b.kind == event.kind && b.modifiers == event.modifiers)
|
||||
{
|
||||
return Some(b.action);
|
||||
}
|
||||
if let Some(b) = KEYBINDINGS
|
||||
.iter()
|
||||
.find(|b| b.code == event.code && b.kind == event.kind && b.modifiers == event.modifiers)
|
||||
{
|
||||
return Some(b.action);
|
||||
}
|
||||
|
||||
if KEYBINDINGS
|
||||
.iter()
|
||||
.any(|b| matches!(b.action, Action::WildCard(_)) && b.kind == event.kind)
|
||||
{
|
||||
if let KeyCode::Char(c) = event.code {
|
||||
return Some(Action::WildCard(c));
|
||||
}
|
||||
if KEYBINDINGS
|
||||
.iter()
|
||||
.any(|b| matches!(b.action, Action::WildCard(_)) && b.kind == event.kind)
|
||||
{
|
||||
if let KeyCode::Char(c) = event.code {
|
||||
return Some(Action::WildCard(c));
|
||||
}
|
||||
}
|
||||
|
||||
None
|
||||
None
|
||||
}
|
||||
|
||||
/// Determines the size of the largest group among a slice of actions.
|
||||
@@ -355,13 +355,13 @@ pub fn event_to_action(event: &KeyEvent) -> Option<Action> {
|
||||
/// returns the highest count. This can be used, for example, to size UI
|
||||
/// elements that display groups of bindings.
|
||||
pub fn count_largest_group(actions: &[Action]) -> u16 {
|
||||
let mut group_counts: HashMap<Group, u16> = HashMap::new();
|
||||
let mut group_counts: HashMap<Group, u16> = HashMap::new();
|
||||
|
||||
for action in actions {
|
||||
if let Some(binding) = binding_for(*action) {
|
||||
*group_counts.entry(binding.group).or_insert(0) += 1;
|
||||
}
|
||||
for action in actions {
|
||||
if let Some(binding) = binding_for(*action) {
|
||||
*group_counts.entry(binding.group).or_insert(0) += 1;
|
||||
}
|
||||
}
|
||||
|
||||
group_counts.values().copied().max().map_or(0, |v| v)
|
||||
group_counts.values().copied().max().map_or(0, |v| v)
|
||||
}
|
||||
@@ -1,38 +1,36 @@
|
||||
use crate::app::{
|
||||
App, View,
|
||||
keybindings::{Action, common_keybindings, event_to_action},
|
||||
App, View,
|
||||
keybindings::{Action, common_keybindings, event_to_action},
|
||||
};
|
||||
use ratatui::crossterm::event::KeyEvent;
|
||||
|
||||
pub fn main_menu_keybindings(app: &mut App, event: &KeyEvent) {
|
||||
if let Some(action) = event_to_action(&event) {
|
||||
common_keybindings(app, action);
|
||||
if let Some(action) = event_to_action(&event) {
|
||||
common_keybindings(app, action);
|
||||
|
||||
let Some(states) = app.states_mut() else {
|
||||
return;
|
||||
};
|
||||
let Some(states) = app.states_mut() else {
|
||||
return;
|
||||
};
|
||||
|
||||
match action {
|
||||
Action::Up => {
|
||||
states.main_menu.selected_view =
|
||||
states.main_menu.selected_view.saturating_sub(1).max(1);
|
||||
}
|
||||
Action::Down => {
|
||||
states.main_menu.selected_view =
|
||||
states.main_menu.selected_view.saturating_add(1).min(3);
|
||||
}
|
||||
Action::Space => {
|
||||
let selected_view: usize = states.main_menu.selected_view;
|
||||
match action {
|
||||
Action::Up => {
|
||||
states.main_menu.selected_view = states.main_menu.selected_view.saturating_sub(1).max(1);
|
||||
}
|
||||
Action::Down => {
|
||||
states.main_menu.selected_view = states.main_menu.selected_view.saturating_add(1).min(3);
|
||||
}
|
||||
Action::Space => {
|
||||
let selected_view: usize = states.main_menu.selected_view;
|
||||
|
||||
if selected_view == 1 {
|
||||
app.view = View::Skirmish;
|
||||
} else if selected_view == 2 {
|
||||
app.view = View::PerkDecks;
|
||||
} else if selected_view == 3 {
|
||||
app.view = View::SkillsConfig;
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
if selected_view == 1 {
|
||||
app.view = View::Skirmish;
|
||||
} else if selected_view == 2 {
|
||||
app.view = View::PerkDecks;
|
||||
} else if selected_view == 3 {
|
||||
app.view = View::SkillsConfig;
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@ mod skirmish;
|
||||
|
||||
pub use default::{common_keybindings, default_keybindings};
|
||||
pub use keybindings::{
|
||||
Action, Group, KeyBinding, binding_for, count_largest_group, event_to_action,
|
||||
Action, Group, KeyBinding, binding_for, count_largest_group, event_to_action,
|
||||
};
|
||||
pub use main_menu::main_menu_keybindings;
|
||||
pub use skirmish::skirmish_keybindings;
|
||||
@@ -1,98 +1,98 @@
|
||||
use crate::app::{
|
||||
App,
|
||||
keybindings::{Action, common_keybindings, event_to_action},
|
||||
states::skirmish_states::{BoardState, MoveFocusedCell, ZoomLevel},
|
||||
App,
|
||||
keybindings::{Action, common_keybindings, event_to_action},
|
||||
states::skirmish_states::{BoardState, MoveFocusedCell, ZoomLevel},
|
||||
};
|
||||
use ratatui::{crossterm::event::KeyEvent, layout::Rect};
|
||||
|
||||
pub fn skirmish_keybindings(app: &mut App, key_event: &KeyEvent) {
|
||||
if let Some(action) = event_to_action(&key_event) {
|
||||
common_keybindings(app, action);
|
||||
if let Some(action) = event_to_action(&key_event) {
|
||||
common_keybindings(app, action);
|
||||
|
||||
let window_area: Rect = app.window_area;
|
||||
let window_area: Rect = app.window_area;
|
||||
|
||||
let Some(states) = app.states_mut() else {
|
||||
return;
|
||||
};
|
||||
let Some(states) = app.states_mut() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let board: &mut BoardState = &mut states.skirmish.board;
|
||||
let board: &mut BoardState = &mut states.skirmish.board;
|
||||
|
||||
match action {
|
||||
Action::Up => {
|
||||
board.change_focused_cell(MoveFocusedCell::Up);
|
||||
if !board.is_focused_cell_visible() {
|
||||
board.vertical_offset.prev();
|
||||
}
|
||||
}
|
||||
Action::Down => {
|
||||
board.change_focused_cell(MoveFocusedCell::Down);
|
||||
if !board.is_focused_cell_visible() {
|
||||
board.vertical_offset.next();
|
||||
}
|
||||
}
|
||||
Action::Left => {
|
||||
board.change_focused_cell(MoveFocusedCell::Left);
|
||||
if !board.is_focused_cell_visible() {
|
||||
board.horizontal_offset.prev();
|
||||
}
|
||||
}
|
||||
Action::Right => {
|
||||
board.change_focused_cell(MoveFocusedCell::Right);
|
||||
if !board.is_focused_cell_visible() {
|
||||
board.horizontal_offset.next();
|
||||
}
|
||||
}
|
||||
Action::ScrollUp => {
|
||||
board.vertical_offset.prev();
|
||||
board.change_focused_cell(MoveFocusedCell::Up);
|
||||
}
|
||||
Action::ScrollDown => {
|
||||
board.vertical_offset.next();
|
||||
board.change_focused_cell(MoveFocusedCell::Down);
|
||||
}
|
||||
Action::ScrollLeft => {
|
||||
board.horizontal_offset.prev();
|
||||
board.change_focused_cell(MoveFocusedCell::Left);
|
||||
}
|
||||
Action::ScrollRight => {
|
||||
board.horizontal_offset.next();
|
||||
board.change_focused_cell(MoveFocusedCell::Right);
|
||||
}
|
||||
Action::ZoomIn => match board.zoom_level {
|
||||
ZoomLevel::ZoomedIn => {}
|
||||
ZoomLevel::Default => {
|
||||
board.change_zoom(ZoomLevel::ZoomedIn);
|
||||
board.vertical_offset.next();
|
||||
}
|
||||
ZoomLevel::ZoomedOut => {
|
||||
board.change_zoom(ZoomLevel::Default);
|
||||
board.vertical_offset.next();
|
||||
board.vertical_offset.next();
|
||||
}
|
||||
},
|
||||
Action::ZoomOut => match board.zoom_level {
|
||||
ZoomLevel::ZoomedIn => {
|
||||
board.change_zoom(ZoomLevel::Default);
|
||||
board.vertical_offset.prev();
|
||||
}
|
||||
ZoomLevel::Default => {
|
||||
board.change_zoom(ZoomLevel::ZoomedOut);
|
||||
board.vertical_offset.prev();
|
||||
board.vertical_offset.prev();
|
||||
}
|
||||
ZoomLevel::ZoomedOut => {}
|
||||
},
|
||||
Action::Space => board.toggle_marking(),
|
||||
Action::Backspace => board.undo_marked_cell(),
|
||||
Action::Delete => {
|
||||
board.marking_cells = false;
|
||||
board.clear_marked_cells()
|
||||
}
|
||||
Action::Tab => {
|
||||
states.skirmish.side_panel = !states.skirmish.side_panel;
|
||||
board.change_resize(&window_area, states.skirmish.side_panel);
|
||||
}
|
||||
_ => (),
|
||||
match action {
|
||||
Action::Up => {
|
||||
board.change_focused_cell(MoveFocusedCell::Up);
|
||||
if !board.is_focused_cell_visible() {
|
||||
board.vertical_offset.prev();
|
||||
}
|
||||
}
|
||||
Action::Down => {
|
||||
board.change_focused_cell(MoveFocusedCell::Down);
|
||||
if !board.is_focused_cell_visible() {
|
||||
board.vertical_offset.next();
|
||||
}
|
||||
}
|
||||
Action::Left => {
|
||||
board.change_focused_cell(MoveFocusedCell::Left);
|
||||
if !board.is_focused_cell_visible() {
|
||||
board.horizontal_offset.prev();
|
||||
}
|
||||
}
|
||||
Action::Right => {
|
||||
board.change_focused_cell(MoveFocusedCell::Right);
|
||||
if !board.is_focused_cell_visible() {
|
||||
board.horizontal_offset.next();
|
||||
}
|
||||
}
|
||||
Action::ScrollUp => {
|
||||
board.vertical_offset.prev();
|
||||
board.change_focused_cell(MoveFocusedCell::Up);
|
||||
}
|
||||
Action::ScrollDown => {
|
||||
board.vertical_offset.next();
|
||||
board.change_focused_cell(MoveFocusedCell::Down);
|
||||
}
|
||||
Action::ScrollLeft => {
|
||||
board.horizontal_offset.prev();
|
||||
board.change_focused_cell(MoveFocusedCell::Left);
|
||||
}
|
||||
Action::ScrollRight => {
|
||||
board.horizontal_offset.next();
|
||||
board.change_focused_cell(MoveFocusedCell::Right);
|
||||
}
|
||||
Action::ZoomIn => match board.zoom_level {
|
||||
ZoomLevel::ZoomedIn => {}
|
||||
ZoomLevel::Default => {
|
||||
board.change_zoom(ZoomLevel::ZoomedIn);
|
||||
board.vertical_offset.next();
|
||||
}
|
||||
ZoomLevel::ZoomedOut => {
|
||||
board.change_zoom(ZoomLevel::Default);
|
||||
board.vertical_offset.next();
|
||||
board.vertical_offset.next();
|
||||
}
|
||||
},
|
||||
Action::ZoomOut => match board.zoom_level {
|
||||
ZoomLevel::ZoomedIn => {
|
||||
board.change_zoom(ZoomLevel::Default);
|
||||
board.vertical_offset.prev();
|
||||
}
|
||||
ZoomLevel::Default => {
|
||||
board.change_zoom(ZoomLevel::ZoomedOut);
|
||||
board.vertical_offset.prev();
|
||||
board.vertical_offset.prev();
|
||||
}
|
||||
ZoomLevel::ZoomedOut => {}
|
||||
},
|
||||
Action::Space => board.toggle_marking(),
|
||||
Action::Backspace => board.pop_marked_cell(),
|
||||
Action::Delete => {
|
||||
board.marked_cells.marking_cells = false;
|
||||
board.end_marking_cells(true)
|
||||
}
|
||||
Action::Tab => {
|
||||
states.skirmish.side_panel = !states.skirmish.side_panel;
|
||||
board.change_resize(&window_area, states.skirmish.side_panel);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
+51
-51
@@ -1,62 +1,62 @@
|
||||
use crate::{
|
||||
app::states::{
|
||||
MainMenuState, PerkDecksState, SettingsState, SkillsConfigState, SkirmishState,
|
||||
skirmish_states::BoardState,
|
||||
},
|
||||
cli::Cli,
|
||||
app::states::{
|
||||
MainMenuState, PerkDecksState, SettingsState, SkillsConfigState, SkirmishState,
|
||||
skirmish_states::BoardState,
|
||||
},
|
||||
cli::Cli,
|
||||
};
|
||||
use ratatui::layout::Rect;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct GameStates {
|
||||
pub main_menu: MainMenuState,
|
||||
pub skirmish: SkirmishState,
|
||||
pub perk_decks: PerkDecksState,
|
||||
pub skills_config: SkillsConfigState,
|
||||
pub settings: SettingsState,
|
||||
pub main_menu: MainMenuState,
|
||||
pub skirmish: SkirmishState,
|
||||
pub perk_decks: PerkDecksState,
|
||||
pub skills_config: SkillsConfigState,
|
||||
pub settings: SettingsState,
|
||||
}
|
||||
|
||||
impl GameStates {
|
||||
pub fn new(args: &Cli, area: &Rect) -> Self {
|
||||
Self {
|
||||
main_menu: MainMenuState {
|
||||
id: 0,
|
||||
name: "Main Menu",
|
||||
selected_view: 1,
|
||||
},
|
||||
skirmish: SkirmishState {
|
||||
id: 1,
|
||||
name: "Skirmish",
|
||||
board: BoardState::new(
|
||||
area,
|
||||
args.map_width as usize,
|
||||
args.map_height as usize,
|
||||
args.zoom_level,
|
||||
false,
|
||||
),
|
||||
side_panel: false,
|
||||
turn_counter: 0,
|
||||
},
|
||||
perk_decks: PerkDecksState {
|
||||
id: 2,
|
||||
name: "Perk Decks",
|
||||
selected_perk_deck: 0,
|
||||
},
|
||||
skills_config: SkillsConfigState {
|
||||
id: 3,
|
||||
name: "Skills Config",
|
||||
selected_skill: 0,
|
||||
},
|
||||
settings: SettingsState {
|
||||
username: args.username.clone(),
|
||||
game_mode: args.game_mode.clone(),
|
||||
perk_deck: args.perk_deck.clone(),
|
||||
starting_wood: args.starting_wood.clone(),
|
||||
starting_iron: args.starting_iron.clone(),
|
||||
supply_limit: args.supply_limit.clone(),
|
||||
xp_modifier: args.xp_modifier.clone(),
|
||||
skill_points_limit: args.skill_points_limit.clone(),
|
||||
},
|
||||
}
|
||||
pub fn new(args: &Cli, area: &Rect) -> Self {
|
||||
Self {
|
||||
main_menu: MainMenuState {
|
||||
id: 0,
|
||||
name: "Main Menu",
|
||||
selected_view: 1,
|
||||
},
|
||||
skirmish: SkirmishState {
|
||||
id: 1,
|
||||
name: "Skirmish",
|
||||
board: BoardState::new(
|
||||
area,
|
||||
args.map_width as usize,
|
||||
args.map_height as usize,
|
||||
args.zoom_level,
|
||||
false,
|
||||
),
|
||||
side_panel: false,
|
||||
turn_counter: 0,
|
||||
},
|
||||
perk_decks: PerkDecksState {
|
||||
id: 2,
|
||||
name: "Perk Decks",
|
||||
selected_perk_deck: 0,
|
||||
},
|
||||
skills_config: SkillsConfigState {
|
||||
id: 3,
|
||||
name: "Skills Config",
|
||||
selected_skill: 0,
|
||||
},
|
||||
settings: SettingsState {
|
||||
username: args.username.clone(),
|
||||
game_mode: args.game_mode.clone(),
|
||||
perk_deck: args.perk_deck.clone(),
|
||||
starting_wood: args.starting_wood.clone(),
|
||||
starting_iron: args.starting_iron.clone(),
|
||||
supply_limit: args.supply_limit.clone(),
|
||||
xp_modifier: args.xp_modifier.clone(),
|
||||
skill_points_limit: args.skill_points_limit.clone(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct MainMenuState {
|
||||
pub id: usize,
|
||||
pub name: &'static str,
|
||||
pub selected_view: usize,
|
||||
pub id: usize,
|
||||
pub name: &'static str,
|
||||
pub selected_view: usize,
|
||||
}
|
||||
@@ -3,24 +3,24 @@ use std::fmt::{Display, Formatter, Result};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct PerkDecksState {
|
||||
pub id: usize,
|
||||
pub name: &'static str,
|
||||
pub selected_perk_deck: usize,
|
||||
pub id: usize,
|
||||
pub name: &'static str,
|
||||
pub selected_perk_deck: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, ValueEnum)]
|
||||
pub enum PerkDecks {
|
||||
Silesian,
|
||||
BogeyMan,
|
||||
Anteater,
|
||||
Silesian,
|
||||
BogeyMan,
|
||||
Anteater,
|
||||
}
|
||||
|
||||
impl Display for PerkDecks {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
|
||||
match self {
|
||||
PerkDecks::Silesian => write!(f, "Silesian"),
|
||||
PerkDecks::BogeyMan => write!(f, "Bogey Man"),
|
||||
PerkDecks::Anteater => write!(f, "Anteater"),
|
||||
}
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
|
||||
match self {
|
||||
PerkDecks::Silesian => write!(f, "Silesian"),
|
||||
PerkDecks::BogeyMan => write!(f, "Bogey Man"),
|
||||
PerkDecks::Anteater => write!(f, "Anteater"),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,12 @@ use crate::app::states::{PerkDecks, skirmish_states::GameMode};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct SettingsState {
|
||||
pub username: String,
|
||||
pub game_mode: GameMode,
|
||||
pub perk_deck: PerkDecks,
|
||||
pub starting_wood: u16,
|
||||
pub starting_iron: u16,
|
||||
pub supply_limit: u8,
|
||||
pub xp_modifier: f32,
|
||||
pub skill_points_limit: u16,
|
||||
pub username: String,
|
||||
pub game_mode: GameMode,
|
||||
pub perk_deck: PerkDecks,
|
||||
pub starting_wood: u16,
|
||||
pub starting_iron: u16,
|
||||
pub supply_limit: u8,
|
||||
pub xp_modifier: f32,
|
||||
pub skill_points_limit: u16,
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct SkillsConfigState {
|
||||
pub id: usize,
|
||||
pub name: &'static str,
|
||||
pub selected_skill: usize,
|
||||
pub id: usize,
|
||||
pub name: &'static str,
|
||||
pub selected_skill: usize,
|
||||
}
|
||||
+11
-11
@@ -1,20 +1,20 @@
|
||||
use crate::app::states::skirmish_states::BoardState;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct SkirmishState {
|
||||
pub id: usize,
|
||||
pub name: &'static str,
|
||||
pub board: BoardState,
|
||||
pub side_panel: bool,
|
||||
pub turn_counter: u64,
|
||||
pub id: usize,
|
||||
pub name: &'static str,
|
||||
pub board: BoardState,
|
||||
pub side_panel: bool,
|
||||
pub turn_counter: u64,
|
||||
}
|
||||
|
||||
impl SkirmishState {
|
||||
pub fn tick_update(&mut self) {
|
||||
// self.board.advance_turn();
|
||||
pub fn tick_update(&mut self) {
|
||||
self.board.advance_turn();
|
||||
|
||||
// if self.board.is_victory() {}
|
||||
// if self.board.is_victory() {}
|
||||
|
||||
self.turn_counter += 1;
|
||||
}
|
||||
self.turn_counter += 1;
|
||||
}
|
||||
}
|
||||
@@ -1,284 +1,467 @@
|
||||
use crate::app::{
|
||||
helpers::cells_area_helper,
|
||||
states::{
|
||||
FocusedCell, Offset,
|
||||
skirmish_states::{
|
||||
CellSizes, MoveFocusedCell, Players, ZoomLevel,
|
||||
structures::{BaseBuilding, Stone, Structures},
|
||||
units::{MinerUnit, Units},
|
||||
},
|
||||
helpers::cells_area_helper,
|
||||
states::{
|
||||
FocusedCell, Offset,
|
||||
skirmish_states::{
|
||||
CellSizes, MarkedCells, MoveFocusedCell, Players, ZoomLevel,
|
||||
structures::{BaseBuilding, Ore, Stone, Structure, Structures, Tunnel},
|
||||
tasks::{DiggingTask, MovingTask, Task, Tasks},
|
||||
units::{MinerUnit, Unit, Units},
|
||||
},
|
||||
widgets::CellWidget,
|
||||
},
|
||||
widgets::CellWidget,
|
||||
};
|
||||
use ratatui::layout::Rect;
|
||||
use std::collections::VecDeque;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct BoardState {
|
||||
cells_area: Rect,
|
||||
pub cell_width: usize,
|
||||
pub cell_height: usize,
|
||||
pub map_width: usize,
|
||||
pub map_height: usize,
|
||||
pub cols: usize,
|
||||
pub rows: usize,
|
||||
pub vertical_offset: Offset,
|
||||
pub horizontal_offset: Offset,
|
||||
pub cells: Vec<Vec<CellWidget>>,
|
||||
pub zoom_level: ZoomLevel,
|
||||
focused_cell: FocusedCell,
|
||||
player_base_coords: (usize, usize),
|
||||
enemy_base_coords: (usize, usize),
|
||||
pub marking_cells: bool,
|
||||
marked_cells: Vec<(usize, usize)>,
|
||||
cells_area: Rect,
|
||||
pub cell_width: usize,
|
||||
pub cell_height: usize,
|
||||
pub map_width: usize,
|
||||
pub map_height: usize,
|
||||
pub cols: usize,
|
||||
pub rows: usize,
|
||||
pub vertical_offset: Offset,
|
||||
pub horizontal_offset: Offset,
|
||||
pub cells: Vec<Vec<CellWidget>>,
|
||||
pub zoom_level: ZoomLevel,
|
||||
focused_cell: FocusedCell,
|
||||
pub marked_cells: MarkedCells,
|
||||
}
|
||||
|
||||
impl BoardState {
|
||||
pub fn new(
|
||||
area: &Rect,
|
||||
map_width: usize,
|
||||
map_height: usize,
|
||||
zoom_level: ZoomLevel,
|
||||
side_panel: bool,
|
||||
) -> Self {
|
||||
let cells_area: Rect = cells_area_helper(area, side_panel);
|
||||
pub fn new(
|
||||
area: &Rect,
|
||||
map_width: usize,
|
||||
map_height: usize,
|
||||
zoom_level: ZoomLevel,
|
||||
side_panel: bool,
|
||||
) -> Self {
|
||||
let cells_area: Rect = cells_area_helper(area, side_panel);
|
||||
|
||||
let cell_width: usize = zoom_level.get_cell_size(CellSizes::Width);
|
||||
let cell_height: usize = zoom_level.get_cell_size(CellSizes::Height);
|
||||
let cell_width: usize = zoom_level.get_cell_size(CellSizes::Width);
|
||||
let cell_height: usize = zoom_level.get_cell_size(CellSizes::Height);
|
||||
|
||||
let cols: usize = (cells_area.width / cell_width as u16) as usize;
|
||||
let rows: usize = (cells_area.height / cell_height as u16) as usize;
|
||||
let cols: usize = (cells_area.width / cell_width as u16) as usize;
|
||||
let rows: usize = (cells_area.height / cell_height as u16) as usize;
|
||||
|
||||
let v_max_offset: usize = Self::max_offset(map_height, rows);
|
||||
let h_max_offset: usize = Self::max_offset(map_width, cols);
|
||||
let v_max_offset: usize = Self::max_offset(map_height, rows);
|
||||
let h_max_offset: usize = Self::max_offset(map_width, cols);
|
||||
|
||||
let vertical_offset: Offset = Offset::new(
|
||||
Some((map_height - rows as usize + 1) / 2),
|
||||
Some(v_max_offset),
|
||||
);
|
||||
let horizontal_offset: Offset = Offset::new(None, Some(h_max_offset));
|
||||
let vertical_offset: Offset = Offset::new(
|
||||
Some(map_height.saturating_sub(rows).div_ceil(2)),
|
||||
Some(v_max_offset),
|
||||
);
|
||||
let horizontal_offset: Offset = Offset::new(None, Some(h_max_offset));
|
||||
|
||||
let focused_cell: FocusedCell =
|
||||
FocusedCell::new((map_height) / 2, 2, map_height, map_width);
|
||||
let focused_cell: FocusedCell = FocusedCell::new((map_height) / 2, 3, map_height, map_width);
|
||||
|
||||
let mut cells: Vec<Vec<CellWidget>> = Vec::new();
|
||||
let mut cells: Vec<Vec<CellWidget>> = Vec::new();
|
||||
|
||||
let player_base_coords: (usize, usize) = ((map_height) / 2, 1);
|
||||
let enemy_base_coords: (usize, usize) = ((map_height) / 2, map_width - 2);
|
||||
let player_base_coords: (usize, usize) = ((map_height) / 2, 2);
|
||||
let enemy_base_coords: (usize, usize) = ((map_height) / 2, map_width - 3);
|
||||
|
||||
for row in 0..map_height {
|
||||
let mut rows: Vec<CellWidget> = Vec::new();
|
||||
for row in 0..map_height {
|
||||
let mut rows: Vec<CellWidget> = Vec::new();
|
||||
|
||||
for col in 0..map_width {
|
||||
let selected: bool = row == focused_cell.get_row() && col == focused_cell.get_col();
|
||||
let player_base: bool = row == player_base_coords.0 && col == player_base_coords.1;
|
||||
let enemy_base: bool = row == enemy_base_coords.0 && col == enemy_base_coords.1;
|
||||
for col in 0..map_width {
|
||||
let (focused_row, focused_col) = focused_cell.get_coords();
|
||||
let selected: bool = row == focused_row && col == focused_col;
|
||||
let player_base: bool = row == player_base_coords.0 && col == player_base_coords.1;
|
||||
let player_ore: bool = row == player_base_coords.0 && col == player_base_coords.1 - 1;
|
||||
let enemy_base: bool = row == enemy_base_coords.0 && col == enemy_base_coords.1;
|
||||
let enemy_ore: bool = row == enemy_base_coords.0 && col == enemy_base_coords.1 + 1;
|
||||
|
||||
let structure: Structures = if player_base {
|
||||
Structures::Base(BaseBuilding::new(Players::Player))
|
||||
} else if enemy_base {
|
||||
Structures::Base(BaseBuilding::new(Players::Enemy))
|
||||
} else {
|
||||
Structures::Stone(Stone::new())
|
||||
};
|
||||
|
||||
let unit: Option<Units> = if player_base {
|
||||
Some(Units::Miner(MinerUnit::new(Players::Player)))
|
||||
} else if enemy_base {
|
||||
Some(Units::Miner(MinerUnit::new(Players::Enemy)))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
rows.push(CellWidget::new(
|
||||
row, col, zoom_level, selected, structure, unit,
|
||||
));
|
||||
}
|
||||
|
||||
cells.push(rows);
|
||||
}
|
||||
|
||||
let marking_cells: bool = false;
|
||||
let marked_cells: Vec<(usize, usize)> = Vec::new();
|
||||
|
||||
Self {
|
||||
cells_area,
|
||||
cell_width,
|
||||
cell_height,
|
||||
map_width,
|
||||
map_height,
|
||||
cols,
|
||||
rows,
|
||||
vertical_offset,
|
||||
horizontal_offset,
|
||||
cells,
|
||||
zoom_level,
|
||||
focused_cell,
|
||||
player_base_coords,
|
||||
enemy_base_coords,
|
||||
marking_cells,
|
||||
marked_cells,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_ref_cell(&self, row: usize, col: usize) -> &CellWidget {
|
||||
&self.cells[row][col]
|
||||
}
|
||||
|
||||
fn get_mut_cell(&mut self, row: usize, col: usize) -> &mut CellWidget {
|
||||
&mut self.cells[row][col]
|
||||
}
|
||||
|
||||
pub fn get_marked_cells(&self) -> Vec<&CellWidget> {
|
||||
self.marked_cells
|
||||
.iter()
|
||||
.map(move |&(row, col)| &self.cells[row][col])
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn toggle_marking(&mut self) {
|
||||
self.marking_cells = !self.marking_cells;
|
||||
|
||||
if self.marking_cells {
|
||||
self.start_marking_cells();
|
||||
let structure: Structures = if player_base {
|
||||
Structures::Base(BaseBuilding::new(Players::Player))
|
||||
} else if enemy_base {
|
||||
Structures::Base(BaseBuilding::new(Players::Enemy))
|
||||
} else if player_ore {
|
||||
Structures::Ore(Ore::new(Players::Player, 1))
|
||||
} else if enemy_ore {
|
||||
Structures::Ore(Ore::new(Players::Enemy, 1))
|
||||
} else {
|
||||
self.clear_marked_cells();
|
||||
}
|
||||
Structures::Stone(Stone::new())
|
||||
};
|
||||
|
||||
let unit: Option<Units> = if player_base {
|
||||
Some(Units::Miner(MinerUnit::new(
|
||||
Players::Player,
|
||||
player_base_coords,
|
||||
)))
|
||||
} else if enemy_base {
|
||||
Some(Units::Miner(MinerUnit::new(
|
||||
Players::Enemy,
|
||||
enemy_base_coords,
|
||||
)))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
rows.push(CellWidget::new(
|
||||
row, col, zoom_level, selected, structure, unit,
|
||||
));
|
||||
}
|
||||
|
||||
cells.push(rows);
|
||||
}
|
||||
|
||||
pub fn set_marked_cell(&mut self, new_cell: (usize, usize)) {
|
||||
let cell: &mut CellWidget = self.get_mut_cell(new_cell.0, new_cell.1);
|
||||
let marked_cells: MarkedCells = MarkedCells::new();
|
||||
|
||||
if !cell.get_marked() {
|
||||
cell.set_marked(true);
|
||||
Self {
|
||||
cells_area,
|
||||
cell_width,
|
||||
cell_height,
|
||||
map_width,
|
||||
map_height,
|
||||
cols,
|
||||
rows,
|
||||
vertical_offset,
|
||||
horizontal_offset,
|
||||
cells,
|
||||
zoom_level,
|
||||
focused_cell,
|
||||
marked_cells,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_ref_cell(&self, row: usize, col: usize) -> &CellWidget {
|
||||
&self.cells[row][col]
|
||||
}
|
||||
|
||||
fn get_mut_cell(&mut self, row: usize, col: usize) -> &mut CellWidget {
|
||||
&mut self.cells[row][col]
|
||||
}
|
||||
|
||||
pub fn get_marked_cells_widgets(&self) -> VecDeque<&CellWidget> {
|
||||
self
|
||||
.marked_cells
|
||||
.marked_cells
|
||||
.iter()
|
||||
.map(|&(row, col)| &self.cells[row][col])
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn get_marked_cells(&self) -> &VecDeque<(usize, usize)> {
|
||||
&self.marked_cells.marked_cells
|
||||
}
|
||||
|
||||
pub fn toggle_marking(&mut self) {
|
||||
self.marked_cells.marking_cells = !self.marked_cells.marking_cells;
|
||||
|
||||
if self.marked_cells.marking_cells {
|
||||
self.start_marking_cells();
|
||||
} else {
|
||||
self.end_marking_cells(false);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn push_marked_cell(&mut self, new_cell: (usize, usize)) {
|
||||
if self.marked_cells.marked_cells.back() == Some(&new_cell) {
|
||||
return;
|
||||
}
|
||||
let cell: &mut CellWidget = self.get_mut_cell(new_cell.0, new_cell.1);
|
||||
|
||||
if !cell.get_marked() {
|
||||
cell.set_marked(true);
|
||||
}
|
||||
|
||||
self
|
||||
.marked_cells
|
||||
.marked_cells
|
||||
.push_back((new_cell.0, new_cell.1));
|
||||
}
|
||||
|
||||
pub fn pop_marked_cell(&mut self) {
|
||||
if self.marked_cells.marked_cells.len() < 2 {
|
||||
return;
|
||||
}
|
||||
|
||||
let old: (usize, usize) =
|
||||
self.marked_cells.marked_cells[self.marked_cells.marked_cells.len() - 1];
|
||||
let new: (usize, usize) =
|
||||
self.marked_cells.marked_cells[self.marked_cells.marked_cells.len() - 2];
|
||||
let old_is_unique: bool = self
|
||||
.marked_cells
|
||||
.marked_cells
|
||||
.iter()
|
||||
.filter(|x| **x == old)
|
||||
.count()
|
||||
== 1;
|
||||
|
||||
let old_cell: &mut CellWidget = self.get_mut_cell(old.0, old.1).set_selected(false);
|
||||
if old_is_unique {
|
||||
old_cell.set_marked(false);
|
||||
}
|
||||
|
||||
self.get_mut_cell(new.0, new.1).set_selected(true);
|
||||
|
||||
self.marked_cells.marked_cells.pop_back();
|
||||
|
||||
self.focused_cell.set_focused_cell(new);
|
||||
}
|
||||
|
||||
pub fn start_marking_cells(&mut self) {
|
||||
let (row, col) = self.focused_cell.get_coords();
|
||||
|
||||
let selected_unit: Option<Units> = self.get_ref_cell(row, col).get_option_unit();
|
||||
if selected_unit.get_owner() != Players::Player || !selected_unit.get_can_dig() {
|
||||
self.marked_cells.marking_cells = false;
|
||||
return;
|
||||
}
|
||||
// Extend queued work from its final destination, not the miner's current cell.
|
||||
let start: (usize, usize) = selected_unit
|
||||
.get_ref_tasks()
|
||||
.back()
|
||||
.and_then(|task| task.get_path().back())
|
||||
.copied()
|
||||
.unwrap_or((row, col));
|
||||
self.focused_cell.set_focused_cell(start);
|
||||
let start: (usize, usize) = self.focused_cell.get_coords();
|
||||
self.get_mut_cell(row, col).set_selected(false);
|
||||
self
|
||||
.get_mut_cell(start.0, start.1)
|
||||
.set_selected(true)
|
||||
.set_marked(true);
|
||||
if !self.is_focused_cell_visible() {
|
||||
self
|
||||
.vertical_offset
|
||||
.set_value(start.0.saturating_sub(self.rows / 2));
|
||||
self
|
||||
.horizontal_offset
|
||||
.set_value(start.1.saturating_sub(self.cols / 2));
|
||||
}
|
||||
self.marked_cells.selected_unit = selected_unit;
|
||||
self.marked_cells.marked_cells.push_back(start);
|
||||
}
|
||||
|
||||
pub fn end_marking_cells(&mut self, del: bool) {
|
||||
self.marked_cells.marking_cells = false;
|
||||
for (row, col) in self.marked_cells.marked_cells.clone() {
|
||||
self.get_mut_cell(row, col).set_marked(false);
|
||||
}
|
||||
|
||||
let (row, col) = self.marked_cells.selected_unit.get_coords();
|
||||
|
||||
if self.marked_cells.marked_cells.len() > 1 && !del {
|
||||
let path: Vec<(usize, usize)> = self
|
||||
.marked_cells
|
||||
.marked_cells
|
||||
.iter()
|
||||
.skip(1)
|
||||
.cloned()
|
||||
.collect();
|
||||
|
||||
let tick_increase: f32 = self.marked_cells.selected_unit.get_digging_power();
|
||||
|
||||
let tasks: VecDeque<Tasks> = path
|
||||
.chunk_by(|a, b| self.is_walkable(*a) == self.is_walkable(*b))
|
||||
.map(|segment| {
|
||||
let path = segment.iter().copied().collect();
|
||||
if self.is_walkable(segment[0]) {
|
||||
Tasks::Moving(MovingTask::new(path))
|
||||
} else {
|
||||
Tasks::Digging(DiggingTask::new(path, tick_increase))
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
if let Some(unit) = self.get_mut_cell(row, col).get_mut_option_unit().as_mut() {
|
||||
unit.get_mut_tasks().extend(tasks);
|
||||
}
|
||||
}
|
||||
|
||||
self.marked_cells.selected_unit = None;
|
||||
self.marked_cells.marked_cells.clear();
|
||||
}
|
||||
|
||||
fn max_offset(map_size: usize, size: usize) -> usize {
|
||||
if map_size > size { map_size - size } else { 0 }
|
||||
}
|
||||
|
||||
fn is_walkable(&self, (row, col): (usize, usize)) -> bool {
|
||||
self
|
||||
.cells
|
||||
.get(row)
|
||||
.and_then(|cells| cells.get(col))
|
||||
.is_some_and(|cell| {
|
||||
matches!(
|
||||
cell.get_structure(),
|
||||
Structures::Tunnel(_) | Structures::Base(_)
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get_focused_cell(&self) -> &FocusedCell {
|
||||
&self.focused_cell
|
||||
}
|
||||
|
||||
pub fn is_focused_cell_visible(&self) -> bool {
|
||||
let vertical_offset: usize = self.vertical_offset.get_value();
|
||||
let horizontal_offset: usize = self.horizontal_offset.get_value();
|
||||
|
||||
let (row, col) = self.focused_cell.get_coords();
|
||||
|
||||
if 0usize.saturating_add(vertical_offset) > row
|
||||
|| row >= self.rows.saturating_add(vertical_offset)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if 0usize.saturating_add(horizontal_offset) > col
|
||||
|| col >= self.cols.saturating_add(horizontal_offset)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
pub fn change_resize(&mut self, area: &Rect, side_panel: bool) {
|
||||
self.cells_area = cells_area_helper(area, side_panel);
|
||||
|
||||
self.cols = (self.cells_area.width / self.cell_width as u16) as usize;
|
||||
self.rows = (self.cells_area.height / self.cell_height as u16) as usize;
|
||||
|
||||
let v_max_offset: usize = Self::max_offset(self.map_height, self.rows);
|
||||
let h_max_offset: usize = Self::max_offset(self.map_width, self.cols);
|
||||
|
||||
self.horizontal_offset =
|
||||
Offset::new(Some(self.horizontal_offset.get_value()), Some(h_max_offset));
|
||||
self.vertical_offset = Offset::new(Some(self.vertical_offset.get_value()), Some(v_max_offset));
|
||||
}
|
||||
|
||||
pub fn change_zoom(&mut self, new_zoom_level: ZoomLevel) {
|
||||
self.zoom_level = new_zoom_level;
|
||||
|
||||
self.cell_width = new_zoom_level.get_cell_size(CellSizes::Width);
|
||||
self.cell_height = new_zoom_level.get_cell_size(CellSizes::Height);
|
||||
|
||||
self.cols = (self.cells_area.width / self.cell_width as u16) as usize;
|
||||
self.rows = (self.cells_area.height / self.cell_height as u16) as usize;
|
||||
|
||||
let v_max_offset: usize = Self::max_offset(self.map_height, self.rows);
|
||||
let h_max_offset: usize = Self::max_offset(self.map_width, self.cols);
|
||||
|
||||
self.vertical_offset = Offset::new(Some(self.vertical_offset.get_value()), Some(v_max_offset));
|
||||
self.horizontal_offset =
|
||||
Offset::new(Some(self.horizontal_offset.get_value()), Some(h_max_offset));
|
||||
|
||||
let (focused_row, focused_coll) = self.focused_cell.get_coords();
|
||||
|
||||
self.focused_cell =
|
||||
FocusedCell::new(focused_row, focused_coll, self.map_height, self.map_width);
|
||||
|
||||
for row in 0..self.map_height {
|
||||
for col in 0..self.map_width {
|
||||
self.get_mut_cell(row, col).set_zoom_level(new_zoom_level);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn change_focused_cell(&mut self, direction: MoveFocusedCell) {
|
||||
let old_cell: (usize, usize) = self.focused_cell.get_coords();
|
||||
let new_cell: (usize, usize) = self.focused_cell.move_focused_cell(direction);
|
||||
|
||||
self
|
||||
.get_mut_cell(old_cell.0, old_cell.1)
|
||||
.set_selected(false);
|
||||
self.get_mut_cell(new_cell.0, new_cell.1).set_selected(true);
|
||||
|
||||
if self.marked_cells.marking_cells {
|
||||
self.push_marked_cell(new_cell);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn advance_turn(&mut self) {
|
||||
// Snapshot positions so moving to a later cell cannot grant another turn.
|
||||
let active: Vec<(usize, usize)> = self
|
||||
.cells
|
||||
.iter()
|
||||
.enumerate()
|
||||
.flat_map(|(row, cells)| {
|
||||
cells.iter().enumerate().filter_map(move |(col, cell)| {
|
||||
cell
|
||||
.get_ref_option_unit()
|
||||
.as_ref()
|
||||
.filter(|unit| !unit.get_ref_tasks().is_empty())
|
||||
.map(|_| (row, col))
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
for (row, col) in active {
|
||||
// Keep the route's origin stable while the player is drawing it.
|
||||
if self.marked_cells.marking_cells
|
||||
&& self.marked_cells.selected_unit.get_coords() == (row, col)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
let mut unit: Units = self
|
||||
.get_mut_cell(row, col)
|
||||
.get_mut_option_unit()
|
||||
.take()
|
||||
.unwrap();
|
||||
let mut task: Tasks = unit.get_mut_tasks().pop_front().unwrap();
|
||||
let mut destination = (row, col);
|
||||
|
||||
if let Some((target_row, target_col)) = task.get_path().front().copied() {
|
||||
let adjacent: bool = row
|
||||
.abs_diff(target_row)
|
||||
.saturating_add(col.abs_diff(target_col))
|
||||
== 1;
|
||||
if target_row >= self.map_height || target_col >= self.map_width || !adjacent {
|
||||
// Discard invalid routes rather than teleporting or indexing outside the map.
|
||||
*self.get_mut_cell(row, col).get_mut_option_unit() = Some(unit);
|
||||
continue;
|
||||
}
|
||||
|
||||
self.marked_cells.push((new_cell.0, new_cell.1));
|
||||
}
|
||||
|
||||
pub fn undo_marked_cell(&mut self) {
|
||||
if self.marked_cells.len() < 2 {
|
||||
return;
|
||||
}
|
||||
|
||||
let old: (usize, usize) = self.marked_cells[self.marked_cells.len() - 1];
|
||||
let new: (usize, usize) = self.marked_cells[self.marked_cells.len() - 2];
|
||||
let old_is_unique: bool = self.marked_cells.iter().filter(|x| **x == old).count() == 1;
|
||||
|
||||
let old_cell: &mut CellWidget = self.get_mut_cell(old.0, old.1).set_selected(false);
|
||||
if old_is_unique {
|
||||
old_cell.set_marked(false);
|
||||
}
|
||||
|
||||
self.get_mut_cell(new.0, new.1).set_selected(true);
|
||||
|
||||
self.marked_cells.pop();
|
||||
|
||||
self.focused_cell.set_focused_cell(new);
|
||||
}
|
||||
|
||||
pub fn start_marking_cells(&mut self) {
|
||||
let row: usize = self.focused_cell.get_row();
|
||||
let col: usize = self.focused_cell.get_col();
|
||||
|
||||
let cell: &mut CellWidget = self.get_mut_cell(row, col);
|
||||
cell.set_marked(true);
|
||||
|
||||
self.marked_cells.push((row, col));
|
||||
}
|
||||
|
||||
pub fn clear_marked_cells(&mut self) {
|
||||
for (row, col) in self.marked_cells.clone() {
|
||||
self.get_mut_cell(row, col).set_marked(false);
|
||||
}
|
||||
|
||||
self.marked_cells.clear();
|
||||
}
|
||||
|
||||
fn max_offset(map_size: usize, size: usize) -> usize {
|
||||
if map_size > size { map_size - size } else { 0 }
|
||||
}
|
||||
|
||||
pub fn get_focused_cell(&self) -> &FocusedCell {
|
||||
&self.focused_cell
|
||||
}
|
||||
|
||||
pub fn is_focused_cell_visible(&self) -> bool {
|
||||
let vertical_offset: usize = self.vertical_offset.get_value();
|
||||
let horizontal_offset: usize = self.horizontal_offset.get_value();
|
||||
|
||||
let row: usize = self.focused_cell.get_row();
|
||||
let col: usize = self.focused_cell.get_col();
|
||||
|
||||
if 0usize.saturating_add(vertical_offset) > row
|
||||
|| row >= self.rows.saturating_add(vertical_offset)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if 0usize.saturating_add(horizontal_offset) > col
|
||||
|| col >= self.cols.saturating_add(horizontal_offset)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
pub fn change_resize(&mut self, area: &Rect, side_panel: bool) {
|
||||
self.cells_area = cells_area_helper(area, side_panel);
|
||||
|
||||
self.cols = (self.cells_area.width / self.cell_width as u16) as usize;
|
||||
self.rows = (self.cells_area.height / self.cell_height as u16) as usize;
|
||||
|
||||
let v_max_offset: usize = Self::max_offset(self.map_height, self.rows);
|
||||
let h_max_offset: usize = Self::max_offset(self.map_width, self.cols);
|
||||
|
||||
self.horizontal_offset =
|
||||
Offset::new(Some(self.horizontal_offset.get_value()), Some(h_max_offset));
|
||||
self.vertical_offset =
|
||||
Offset::new(Some(self.vertical_offset.get_value()), Some(v_max_offset));
|
||||
}
|
||||
|
||||
pub fn change_zoom(&mut self, new_zoom_level: ZoomLevel) {
|
||||
self.zoom_level = new_zoom_level;
|
||||
|
||||
self.cell_width = new_zoom_level.get_cell_size(CellSizes::Width);
|
||||
self.cell_height = new_zoom_level.get_cell_size(CellSizes::Height);
|
||||
|
||||
self.cols = (self.cells_area.width / self.cell_width as u16) as usize;
|
||||
self.rows = (self.cells_area.height / self.cell_height as u16) as usize;
|
||||
|
||||
let v_max_offset: usize = Self::max_offset(self.map_height, self.rows);
|
||||
let h_max_offset: usize = Self::max_offset(self.map_width, self.cols);
|
||||
|
||||
self.vertical_offset =
|
||||
Offset::new(Some(self.vertical_offset.get_value()), Some(v_max_offset));
|
||||
self.horizontal_offset =
|
||||
Offset::new(Some(self.horizontal_offset.get_value()), Some(h_max_offset));
|
||||
|
||||
self.focused_cell = FocusedCell::new(
|
||||
self.focused_cell.get_row(),
|
||||
self.focused_cell.get_col(),
|
||||
self.map_height,
|
||||
self.map_width,
|
||||
);
|
||||
|
||||
for row in 0..self.map_height {
|
||||
for col in 0..self.map_width {
|
||||
self.get_mut_cell(row, col).set_zoom_level(new_zoom_level);
|
||||
let cell: &mut CellWidget = self.get_mut_cell(target_row, target_col);
|
||||
if cell.get_ref_option_unit().is_none() {
|
||||
let structure: Structures = cell.get_structure();
|
||||
// Previously planned stone may already be a tunnel when this task starts.
|
||||
task = match (task, structure) {
|
||||
(Tasks::Digging(dig), Structures::Tunnel(_) | Structures::Base(_)) => {
|
||||
Tasks::Moving(MovingTask::from_digging(dig))
|
||||
}
|
||||
(Tasks::Moving(moving), Structures::Stone(_)) => {
|
||||
Tasks::Digging(DiggingTask::from_moving(moving, unit.get_digging_power()))
|
||||
}
|
||||
(task, _) => task,
|
||||
};
|
||||
match (&mut task, structure) {
|
||||
(Tasks::Digging(dig), Structures::Stone(mut stone)) => {
|
||||
let max: f32 = f32::from(stone.get_max_durability());
|
||||
let before: u16 = (dig.get_progress() * max / 100.0) as u16;
|
||||
dig.progress();
|
||||
let after: u16 = (dig.get_progress() * max / 100.0) as u16;
|
||||
stone.take_damage(after.saturating_sub(before));
|
||||
if stone.get_durability() == 0 {
|
||||
cell.set_structure(Structures::Tunnel(Tunnel::new()));
|
||||
destination = (target_row, target_col);
|
||||
dig.pop_front();
|
||||
} else {
|
||||
cell.set_structure(Structures::Stone(stone));
|
||||
}
|
||||
}
|
||||
(Tasks::Moving(moving), Structures::Tunnel(_) | Structures::Base(_)) => {
|
||||
moving.progress();
|
||||
if moving.is_finished() {
|
||||
destination = (target_row, target_col);
|
||||
moving.pop_front();
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn change_focused_cell(&mut self, direction: MoveFocusedCell) {
|
||||
let old_cell: (usize, usize) = (self.focused_cell.get_row(), self.focused_cell.get_col());
|
||||
let new_cell: (usize, usize) = self.focused_cell.move_focused_cell(direction);
|
||||
|
||||
self.get_mut_cell(old_cell.0, old_cell.1)
|
||||
.set_selected(false);
|
||||
self.get_mut_cell(new_cell.0, new_cell.1).set_selected(true);
|
||||
|
||||
if self.marking_cells {
|
||||
self.set_marked_cell(new_cell);
|
||||
if !task.get_path().is_empty() {
|
||||
unit.get_mut_tasks().push_front(task);
|
||||
}
|
||||
}
|
||||
unit.set_coords(destination);
|
||||
*self
|
||||
.get_mut_cell(destination.0, destination.1)
|
||||
.get_mut_option_unit() = Some(unit);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,57 +1,53 @@
|
||||
pub enum MoveFocusedCell {
|
||||
Up,
|
||||
Down,
|
||||
Left,
|
||||
Right,
|
||||
Up,
|
||||
Down,
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct FocusedCell {
|
||||
row: usize,
|
||||
col: usize,
|
||||
max_row: usize,
|
||||
max_col: usize,
|
||||
row: usize,
|
||||
col: usize,
|
||||
max_row: usize,
|
||||
max_col: usize,
|
||||
}
|
||||
|
||||
impl FocusedCell {
|
||||
pub fn new(row: usize, col: usize, max_row: usize, max_col: usize) -> Self {
|
||||
Self {
|
||||
row,
|
||||
col,
|
||||
max_row,
|
||||
max_col,
|
||||
}
|
||||
pub fn new(row: usize, col: usize, max_row: usize, max_col: usize) -> Self {
|
||||
Self {
|
||||
row,
|
||||
col,
|
||||
max_row,
|
||||
max_col,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_coords(&self) -> (usize, usize) {
|
||||
(self.row, self.col)
|
||||
}
|
||||
|
||||
pub fn move_focused_cell(&mut self, direction: MoveFocusedCell) -> (usize, usize) {
|
||||
match direction {
|
||||
MoveFocusedCell::Up => {
|
||||
self.row = self.row.saturating_sub(1).max(0);
|
||||
}
|
||||
MoveFocusedCell::Down => {
|
||||
self.row = self.row.saturating_add(1).min(self.max_row - 1);
|
||||
}
|
||||
MoveFocusedCell::Left => {
|
||||
self.col = self.col.saturating_sub(1).max(0);
|
||||
}
|
||||
MoveFocusedCell::Right => {
|
||||
self.col = self.col.saturating_add(1).min(self.max_col - 1);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_row(&self) -> usize {
|
||||
self.row
|
||||
}
|
||||
(self.row, self.col)
|
||||
}
|
||||
|
||||
pub fn get_col(&self) -> usize {
|
||||
self.col
|
||||
}
|
||||
|
||||
pub fn move_focused_cell(&mut self, direction: MoveFocusedCell) -> (usize, usize) {
|
||||
match direction {
|
||||
MoveFocusedCell::Up => {
|
||||
self.row = self.row.saturating_sub(1).max(0);
|
||||
}
|
||||
MoveFocusedCell::Down => {
|
||||
self.row = self.row.saturating_add(1).min(self.max_row - 1);
|
||||
}
|
||||
MoveFocusedCell::Left => {
|
||||
self.col = self.col.saturating_sub(1).max(0);
|
||||
}
|
||||
MoveFocusedCell::Right => {
|
||||
self.col = self.col.saturating_add(1).min(self.max_col - 1);
|
||||
}
|
||||
}
|
||||
|
||||
(self.row, self.col)
|
||||
}
|
||||
|
||||
pub fn set_focused_cell(&mut self, cell: (usize, usize)) {
|
||||
self.row = cell.0.max(0).min(self.max_row - 1);
|
||||
self.col = cell.1.max(0).min(self.max_col - 1);
|
||||
}
|
||||
pub fn set_focused_cell(&mut self, cell: (usize, usize)) {
|
||||
self.row = cell.0.max(0).min(self.max_row - 1);
|
||||
self.col = cell.1.max(0).min(self.max_col - 1);
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,6 @@ use clap::ValueEnum;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, ValueEnum)]
|
||||
pub enum GameMode {
|
||||
LastManStanding,
|
||||
FrontLines,
|
||||
LastManStanding,
|
||||
FrontLines,
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
use crate::app::states::skirmish_states::units::Units;
|
||||
use std::collections::VecDeque;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct MarkedCells {
|
||||
pub marking_cells: bool,
|
||||
pub marked_cells: VecDeque<(usize, usize)>,
|
||||
pub selected_unit: Option<Units>,
|
||||
}
|
||||
|
||||
impl MarkedCells {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
marking_cells: false,
|
||||
marked_cells: VecDeque::new(),
|
||||
selected_unit: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,18 @@
|
||||
mod board;
|
||||
mod focused_cell;
|
||||
mod game_mode;
|
||||
mod marked_cells;
|
||||
mod offset;
|
||||
mod players;
|
||||
pub mod structures;
|
||||
pub mod tasks;
|
||||
pub mod units;
|
||||
pub mod zoom_level;
|
||||
|
||||
pub use board::BoardState;
|
||||
pub use focused_cell::{FocusedCell, MoveFocusedCell};
|
||||
pub use game_mode::GameMode;
|
||||
pub use marked_cells::MarkedCells;
|
||||
pub use offset::Offset;
|
||||
pub use players::Players;
|
||||
pub use zoom_level::{CellSizes, ZoomLevel};
|
||||
@@ -1,34 +1,35 @@
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct Offset {
|
||||
value: usize,
|
||||
max: usize,
|
||||
value: usize,
|
||||
max: usize,
|
||||
}
|
||||
|
||||
impl Offset {
|
||||
pub fn new(initial_value: Option<usize>, initial_max: Option<usize>) -> Self {
|
||||
Self {
|
||||
value: initial_value.unwrap_or(0),
|
||||
max: initial_max.unwrap_or(0),
|
||||
}
|
||||
pub fn new(initial_value: Option<usize>, initial_max: Option<usize>) -> Self {
|
||||
Self {
|
||||
value: initial_value.unwrap_or(0).min(initial_max.unwrap_or(0)),
|
||||
max: initial_max.unwrap_or(0),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_value(&self) -> usize {
|
||||
self.value
|
||||
}
|
||||
pub fn get_value(&self) -> usize {
|
||||
self.value
|
||||
}
|
||||
|
||||
pub fn set_value(&mut self, value: usize) {
|
||||
self.value = value;
|
||||
}
|
||||
pub fn set_value(&mut self, value: usize) {
|
||||
self.value = value.min(self.max);
|
||||
}
|
||||
|
||||
pub fn set_max(&mut self, max: usize) {
|
||||
self.max = max;
|
||||
}
|
||||
pub fn set_max(&mut self, max: usize) {
|
||||
self.max = max;
|
||||
self.value = self.value.min(max);
|
||||
}
|
||||
|
||||
pub fn next(&mut self) {
|
||||
self.value = self.value.saturating_add(1).min(self.max);
|
||||
}
|
||||
pub fn next(&mut self) {
|
||||
self.value = self.value.saturating_add(1).min(self.max);
|
||||
}
|
||||
|
||||
pub fn prev(&mut self) {
|
||||
self.value = self.value.saturating_sub(1).max(0)
|
||||
}
|
||||
pub fn prev(&mut self) {
|
||||
self.value = self.value.saturating_sub(1).max(0)
|
||||
}
|
||||
}
|
||||
@@ -1,37 +1,37 @@
|
||||
use ratatui::{
|
||||
style::{Color, Stylize},
|
||||
text::Span,
|
||||
style::{Color, Stylize},
|
||||
text::Span,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Players {
|
||||
Player,
|
||||
Enemy,
|
||||
Unclaimed,
|
||||
Player,
|
||||
Enemy,
|
||||
Unclaimed,
|
||||
}
|
||||
|
||||
impl Players {
|
||||
pub fn get_text(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Player => "Player",
|
||||
Self::Enemy => "Enemy",
|
||||
Self::Unclaimed => "Unclaimed",
|
||||
}
|
||||
pub fn get_text(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Player => "Player",
|
||||
Self::Enemy => "Enemy",
|
||||
Self::Unclaimed => "Unclaimed",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_color(&self) -> Color {
|
||||
match self {
|
||||
Self::Player => Color::LightBlue,
|
||||
Self::Enemy => Color::LightRed,
|
||||
Self::Unclaimed => Color::White,
|
||||
}
|
||||
pub fn get_color(&self) -> Color {
|
||||
match self {
|
||||
Self::Player => Color::LightBlue,
|
||||
Self::Enemy => Color::LightRed,
|
||||
Self::Unclaimed => Color::White,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_span(&self) -> Span<'static> {
|
||||
match self {
|
||||
Self::Player => self.get_text().fg(self.get_color()),
|
||||
Self::Enemy => self.get_text().fg(self.get_color()),
|
||||
Self::Unclaimed => self.get_text().fg(self.get_color()),
|
||||
}
|
||||
pub fn get_span(&self) -> Span<'static> {
|
||||
match self {
|
||||
Self::Player => self.get_text().fg(self.get_color()),
|
||||
Self::Enemy => self.get_text().fg(self.get_color()),
|
||||
Self::Unclaimed => self.get_text().fg(self.get_color()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,59 +3,57 @@ use ratatui::style::Color;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct BaseBuilding {
|
||||
durability: u16,
|
||||
max_durability: u16,
|
||||
stress: u8,
|
||||
owner: Players,
|
||||
level: u8,
|
||||
durability: u16,
|
||||
stress: u8,
|
||||
owner: Players,
|
||||
level: u8,
|
||||
}
|
||||
|
||||
impl BaseBuilding {
|
||||
pub fn new(owner: Players) -> Self {
|
||||
Self {
|
||||
durability: 1500,
|
||||
max_durability: 1500,
|
||||
stress: 0,
|
||||
owner,
|
||||
level: b'1',
|
||||
}
|
||||
pub fn new(owner: Players) -> Self {
|
||||
Self {
|
||||
durability: 1500,
|
||||
stress: 0,
|
||||
owner,
|
||||
level: 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Structure for BaseBuilding {
|
||||
fn get_tag(&self) -> char {
|
||||
'B'
|
||||
}
|
||||
fn get_tag(&self) -> char {
|
||||
'B'
|
||||
}
|
||||
|
||||
fn get_color(&self) -> Color {
|
||||
match self.owner {
|
||||
Players::Player => Color::LightBlue,
|
||||
Players::Enemy => Color::LightRed,
|
||||
Players::Unclaimed => Color::LightGreen,
|
||||
}
|
||||
fn get_color(&self) -> Color {
|
||||
match self.owner {
|
||||
Players::Player => Color::LightBlue,
|
||||
Players::Enemy => Color::LightRed,
|
||||
Players::Unclaimed => Color::LightGreen,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_level(&self) -> char {
|
||||
self.level as char
|
||||
}
|
||||
fn get_level(&self) -> char {
|
||||
(self.level + b'0') as char
|
||||
}
|
||||
|
||||
fn get_durability(&self) -> u16 {
|
||||
self.durability
|
||||
}
|
||||
fn get_durability(&self) -> u16 {
|
||||
self.durability
|
||||
}
|
||||
|
||||
fn get_max_durability(&self) -> u16 {
|
||||
self.max_durability
|
||||
}
|
||||
fn get_max_durability(&self) -> u16 {
|
||||
1500
|
||||
}
|
||||
|
||||
fn get_stress(&self) -> u8 {
|
||||
self.stress
|
||||
}
|
||||
fn get_stress(&self) -> u8 {
|
||||
self.stress
|
||||
}
|
||||
|
||||
fn get_name(&self) -> &'static str {
|
||||
"Base"
|
||||
}
|
||||
fn get_name(&self) -> &'static str {
|
||||
"Base"
|
||||
}
|
||||
|
||||
fn get_owner(&self) -> Players {
|
||||
self.owner
|
||||
}
|
||||
fn get_owner(&self) -> Players {
|
||||
self.owner
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
mod base;
|
||||
mod ore;
|
||||
mod stone;
|
||||
mod structure_trait;
|
||||
mod structures_enum;
|
||||
mod structures_trait;
|
||||
mod tunnel;
|
||||
|
||||
pub use base::BaseBuilding;
|
||||
pub use ore::Ore;
|
||||
pub use stone::Stone;
|
||||
pub use structure_trait::Structure;
|
||||
pub use structures_enum::Structures;
|
||||
pub use structures_trait::Structure;
|
||||
pub use tunnel::Tunnel;
|
||||
@@ -0,0 +1,94 @@
|
||||
use crate::app::states::skirmish_states::{Players, structures::Structure};
|
||||
use ratatui::{
|
||||
style::{Color, Stylize},
|
||||
text::Line,
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Ore {
|
||||
owner: Players,
|
||||
ore_amount: u16,
|
||||
durability: u16,
|
||||
stress: u8,
|
||||
level: u8,
|
||||
}
|
||||
|
||||
impl Ore {
|
||||
pub fn new(owner: Players, level: u8) -> Self {
|
||||
Self {
|
||||
owner,
|
||||
ore_amount: 5000,
|
||||
durability: 1000,
|
||||
stress: 0,
|
||||
level,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_ore_amount(&self) -> u16 {
|
||||
self.ore_amount
|
||||
}
|
||||
|
||||
pub fn get_max_ore_amount(&self) -> u16 {
|
||||
5000
|
||||
}
|
||||
|
||||
pub fn set_owner(&mut self, new_owner: Players) -> &mut Self {
|
||||
self.owner = new_owner;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Structure for Ore {
|
||||
fn get_tag(&self) -> char {
|
||||
'O'
|
||||
}
|
||||
|
||||
fn get_color(&self) -> Color {
|
||||
match self.owner {
|
||||
Players::Player => self.owner.get_color(),
|
||||
Players::Enemy => self.owner.get_color(),
|
||||
Players::Unclaimed => Color::Indexed(217),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_durability(&self) -> u16 {
|
||||
self.durability
|
||||
}
|
||||
|
||||
fn get_level(&self) -> char {
|
||||
if self.level != 0 {
|
||||
(self.level + b'0') as char
|
||||
} else {
|
||||
' '
|
||||
}
|
||||
}
|
||||
|
||||
fn get_max_durability(&self) -> u16 {
|
||||
1000
|
||||
}
|
||||
|
||||
fn get_name(&self) -> &'static str {
|
||||
"Ore"
|
||||
}
|
||||
|
||||
fn get_owner(&self) -> Players {
|
||||
self.owner
|
||||
}
|
||||
|
||||
fn get_stress(&self) -> u8 {
|
||||
self.stress
|
||||
}
|
||||
|
||||
fn text(&self) -> Vec<Line<'_>> {
|
||||
let mut lines: Vec<Line<'_>> = Structure::base_text(self);
|
||||
|
||||
lines.push(Line::from_iter([
|
||||
"Ore: ".gray(),
|
||||
self.get_ore_amount().to_string().cyan(),
|
||||
"/".gray(),
|
||||
self.get_max_ore_amount().to_string().cyan(),
|
||||
]));
|
||||
|
||||
lines
|
||||
}
|
||||
}
|
||||
@@ -3,51 +3,53 @@ use ratatui::style::Color;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Stone {
|
||||
durability: u16,
|
||||
max_durability: u16,
|
||||
stress: u8,
|
||||
durability: u16,
|
||||
stress: u8,
|
||||
}
|
||||
|
||||
impl Stone {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
durability: 1000,
|
||||
max_durability: 1000,
|
||||
stress: 0,
|
||||
}
|
||||
pub fn take_damage(&mut self, damage: u16) {
|
||||
self.durability = self.durability.saturating_sub(damage);
|
||||
}
|
||||
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
durability: 1000,
|
||||
stress: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Structure for Stone {
|
||||
fn get_tag(&self) -> char {
|
||||
' '
|
||||
}
|
||||
fn get_tag(&self) -> char {
|
||||
' '
|
||||
}
|
||||
|
||||
fn get_color(&self) -> Color {
|
||||
Color::White
|
||||
}
|
||||
fn get_color(&self) -> Color {
|
||||
Color::White
|
||||
}
|
||||
|
||||
fn get_level(&self) -> char {
|
||||
' '
|
||||
}
|
||||
fn get_level(&self) -> char {
|
||||
' '
|
||||
}
|
||||
|
||||
fn get_durability(&self) -> u16 {
|
||||
self.durability
|
||||
}
|
||||
fn get_durability(&self) -> u16 {
|
||||
self.durability
|
||||
}
|
||||
|
||||
fn get_max_durability(&self) -> u16 {
|
||||
self.max_durability
|
||||
}
|
||||
fn get_max_durability(&self) -> u16 {
|
||||
1000
|
||||
}
|
||||
|
||||
fn get_stress(&self) -> u8 {
|
||||
self.stress
|
||||
}
|
||||
fn get_stress(&self) -> u8 {
|
||||
self.stress
|
||||
}
|
||||
|
||||
fn get_name(&self) -> &'static str {
|
||||
"Stone"
|
||||
}
|
||||
fn get_name(&self) -> &'static str {
|
||||
"Stone"
|
||||
}
|
||||
|
||||
fn get_owner(&self) -> Players {
|
||||
Players::Unclaimed
|
||||
}
|
||||
fn get_owner(&self) -> Players {
|
||||
Players::Unclaimed
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
use crate::app::states::skirmish_states::Players;
|
||||
use ratatui::{
|
||||
style::{Color, Stylize},
|
||||
text::Line,
|
||||
};
|
||||
|
||||
pub trait Structure {
|
||||
fn get_tag(&self) -> char;
|
||||
|
||||
fn get_color(&self) -> Color;
|
||||
|
||||
fn get_level(&self) -> char;
|
||||
|
||||
fn get_stress(&self) -> u8;
|
||||
|
||||
fn get_durability(&self) -> u16;
|
||||
|
||||
fn get_max_durability(&self) -> u16;
|
||||
|
||||
fn get_name(&self) -> &'static str;
|
||||
|
||||
fn get_owner(&self) -> Players;
|
||||
|
||||
fn base_text(&self) -> Vec<Line<'_>> {
|
||||
let durability: u16 = self.get_durability();
|
||||
let max_durability: u16 = self.get_max_durability();
|
||||
let durability_percent: u32 = durability as u32 * 100 / max_durability as u32;
|
||||
let stress: u8 = self.get_stress();
|
||||
let level: char = self.get_level();
|
||||
|
||||
let mut lines: Vec<Line<'_>> = vec![
|
||||
Line::from_iter([
|
||||
"Durability: ".gray(),
|
||||
durability.to_string().cyan(),
|
||||
"/".gray(),
|
||||
max_durability.to_string().cyan(),
|
||||
" ( ".gray(),
|
||||
durability_percent.to_string().cyan(),
|
||||
"% )".gray(),
|
||||
]),
|
||||
Line::from_iter(["Stress: ".gray(), stress.to_string().cyan(), "%".gray()]),
|
||||
];
|
||||
|
||||
if level != ' ' {
|
||||
lines.push(Line::from_iter([
|
||||
"Level: ".gray(),
|
||||
level.to_string().cyan(),
|
||||
]));
|
||||
}
|
||||
|
||||
lines
|
||||
}
|
||||
|
||||
fn text(&self) -> Vec<Line<'_>> {
|
||||
self.base_text()
|
||||
}
|
||||
}
|
||||
@@ -1,57 +1,78 @@
|
||||
use crate::app::states::skirmish_states::{
|
||||
Players,
|
||||
structures::{BaseBuilding, Stone, Structure, Tunnel},
|
||||
Players,
|
||||
structures::{BaseBuilding, Ore, Stone, Structure, Tunnel},
|
||||
};
|
||||
use ratatui::{
|
||||
style::{Color, Stylize},
|
||||
text::Line,
|
||||
};
|
||||
use ratatui::style::Color;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Structures {
|
||||
Base(BaseBuilding),
|
||||
Tunnel(Tunnel),
|
||||
Stone(Stone),
|
||||
Base(BaseBuilding),
|
||||
Tunnel(Tunnel),
|
||||
Stone(Stone),
|
||||
Ore(Ore),
|
||||
}
|
||||
|
||||
impl Structures {
|
||||
#[inline]
|
||||
fn structure(&self) -> &dyn Structure {
|
||||
match self {
|
||||
Self::Base(b) => b,
|
||||
Self::Tunnel(t) => t,
|
||||
Self::Stone(s) => s,
|
||||
}
|
||||
#[inline]
|
||||
fn s(&self) -> &dyn Structure {
|
||||
match self {
|
||||
Self::Base(b) => b,
|
||||
Self::Tunnel(t) => t,
|
||||
Self::Stone(s) => s,
|
||||
Self::Ore(o) => o,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Structure for Structures {
|
||||
fn get_color(&self) -> Color {
|
||||
self.structure().get_color()
|
||||
}
|
||||
fn get_color(&self) -> Color {
|
||||
self.s().get_color()
|
||||
}
|
||||
|
||||
fn get_tag(&self) -> char {
|
||||
self.structure().get_tag()
|
||||
}
|
||||
fn get_tag(&self) -> char {
|
||||
self.s().get_tag()
|
||||
}
|
||||
|
||||
fn get_level(&self) -> char {
|
||||
self.structure().get_level()
|
||||
}
|
||||
fn get_level(&self) -> char {
|
||||
self.s().get_level()
|
||||
}
|
||||
|
||||
fn get_name(&self) -> &'static str {
|
||||
self.structure().get_name()
|
||||
}
|
||||
fn get_name(&self) -> &'static str {
|
||||
self.s().get_name()
|
||||
}
|
||||
|
||||
fn get_durability(&self) -> u16 {
|
||||
self.structure().get_durability()
|
||||
}
|
||||
fn get_durability(&self) -> u16 {
|
||||
self.s().get_durability()
|
||||
}
|
||||
|
||||
fn get_max_durability(&self) -> u16 {
|
||||
self.structure().get_max_durability()
|
||||
}
|
||||
fn get_max_durability(&self) -> u16 {
|
||||
self.s().get_max_durability()
|
||||
}
|
||||
|
||||
fn get_stress(&self) -> u8 {
|
||||
self.structure().get_stress()
|
||||
}
|
||||
fn get_stress(&self) -> u8 {
|
||||
self.s().get_stress()
|
||||
}
|
||||
|
||||
fn get_owner(&self) -> Players {
|
||||
self.structure().get_owner()
|
||||
fn get_owner(&self) -> Players {
|
||||
self.s().get_owner()
|
||||
}
|
||||
|
||||
fn text(&self) -> Vec<Line<'_>> {
|
||||
match self.s().get_owner() {
|
||||
Players::Unclaimed => self.s().text(),
|
||||
Players::Player | Players::Enemy => {
|
||||
let mut lines: Vec<Line<'_>> = self.s().text();
|
||||
|
||||
lines.push(Line::from_iter([
|
||||
"Owner: ".gray(),
|
||||
self.s().get_owner().get_span(),
|
||||
]));
|
||||
|
||||
lines
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
use crate::app::states::skirmish_states::Players;
|
||||
use ratatui::style::Color;
|
||||
|
||||
pub trait Structure {
|
||||
fn get_tag(&self) -> char;
|
||||
fn get_color(&self) -> Color;
|
||||
fn get_level(&self) -> char;
|
||||
fn get_stress(&self) -> u8;
|
||||
fn get_durability(&self) -> u16;
|
||||
fn get_max_durability(&self) -> u16;
|
||||
fn get_name(&self) -> &'static str;
|
||||
fn get_owner(&self) -> Players;
|
||||
}
|
||||
@@ -3,57 +3,55 @@ use ratatui::style::Color;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct Tunnel {
|
||||
durability: u16,
|
||||
max_durability: u16,
|
||||
stress: u8,
|
||||
roof_support: bool,
|
||||
rail: bool,
|
||||
lamp: bool,
|
||||
durability: u16,
|
||||
stress: u8,
|
||||
roof_support: bool,
|
||||
rail: bool,
|
||||
lamp: bool,
|
||||
}
|
||||
|
||||
impl Tunnel {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
durability: 500,
|
||||
max_durability: 500,
|
||||
stress: 25,
|
||||
roof_support: false,
|
||||
rail: false,
|
||||
lamp: false,
|
||||
}
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
durability: 500,
|
||||
stress: 25,
|
||||
roof_support: false,
|
||||
rail: false,
|
||||
lamp: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Structure for Tunnel {
|
||||
fn get_tag(&self) -> char {
|
||||
'T'
|
||||
}
|
||||
fn get_tag(&self) -> char {
|
||||
'T'
|
||||
}
|
||||
|
||||
fn get_color(&self) -> Color {
|
||||
Color::Gray
|
||||
}
|
||||
fn get_color(&self) -> Color {
|
||||
Color::Gray
|
||||
}
|
||||
|
||||
fn get_level(&self) -> char {
|
||||
' '
|
||||
}
|
||||
fn get_level(&self) -> char {
|
||||
' '
|
||||
}
|
||||
|
||||
fn get_durability(&self) -> u16 {
|
||||
self.durability
|
||||
}
|
||||
fn get_durability(&self) -> u16 {
|
||||
self.durability
|
||||
}
|
||||
|
||||
fn get_max_durability(&self) -> u16 {
|
||||
self.max_durability
|
||||
}
|
||||
fn get_max_durability(&self) -> u16 {
|
||||
500
|
||||
}
|
||||
|
||||
fn get_stress(&self) -> u8 {
|
||||
self.stress
|
||||
}
|
||||
fn get_stress(&self) -> u8 {
|
||||
self.stress
|
||||
}
|
||||
|
||||
fn get_name(&self) -> &'static str {
|
||||
"Tunnel"
|
||||
}
|
||||
fn get_name(&self) -> &'static str {
|
||||
"Tunnel"
|
||||
}
|
||||
|
||||
fn get_owner(&self) -> Players {
|
||||
Players::Unclaimed
|
||||
}
|
||||
fn get_owner(&self) -> Players {
|
||||
Players::Unclaimed
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
use crate::app::states::skirmish_states::tasks::{MovingTask, Task};
|
||||
use std::collections::VecDeque;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct DiggingTask {
|
||||
path: VecDeque<(usize, usize)>,
|
||||
progress: f32,
|
||||
tick_increase: f32,
|
||||
total_steps: usize,
|
||||
}
|
||||
|
||||
impl DiggingTask {
|
||||
pub fn new(path: VecDeque<(usize, usize)>, tick_increase: f32) -> Self {
|
||||
Self {
|
||||
total_steps: path.len(),
|
||||
path,
|
||||
progress: 0.0,
|
||||
tick_increase,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pop_front(&mut self) {
|
||||
self.path.pop_front();
|
||||
self.progress = 0.0;
|
||||
}
|
||||
|
||||
pub fn from_moving(task: MovingTask, tick_increase: f32) -> Self {
|
||||
Self {
|
||||
path: task.get_path().clone(),
|
||||
progress: 0.0,
|
||||
tick_increase,
|
||||
total_steps: task.get_total_steps(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn current_target(&self) -> Option<(usize, usize)> {
|
||||
self.path.front().copied()
|
||||
}
|
||||
}
|
||||
|
||||
impl Task for DiggingTask {
|
||||
fn get_icon(&self) -> char {
|
||||
'⛏'
|
||||
}
|
||||
|
||||
fn get_path(&self) -> &VecDeque<(usize, usize)> {
|
||||
&self.path
|
||||
}
|
||||
|
||||
fn get_path_front_coords(&self) -> &(usize, usize) {
|
||||
self.path.front().map_or(&(0, 0), |c| c)
|
||||
}
|
||||
|
||||
fn get_path_back_coords(&self) -> &(usize, usize) {
|
||||
self.path.back().map_or(&(0, 0), |c| c)
|
||||
}
|
||||
|
||||
fn get_progress(&self) -> f32 {
|
||||
self.progress
|
||||
}
|
||||
|
||||
fn get_total_steps(&self) -> usize {
|
||||
self.total_steps
|
||||
}
|
||||
|
||||
fn progress(&mut self) {
|
||||
self.progress = (self.progress + self.tick_increase).clamp(0.0, 100.0);
|
||||
}
|
||||
|
||||
fn is_finished(&self) -> bool {
|
||||
self.progress >= 100.0
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
mod digging;
|
||||
mod moving;
|
||||
mod task_trait;
|
||||
mod tasks_enum;
|
||||
|
||||
pub use digging::DiggingTask;
|
||||
pub use moving::MovingTask;
|
||||
pub use task_trait::Task;
|
||||
pub use tasks_enum::Tasks;
|
||||
@@ -0,0 +1,75 @@
|
||||
use crate::app::{
|
||||
states::skirmish_states::tasks::{DiggingTask, Task},
|
||||
threads::TICK_MS,
|
||||
};
|
||||
use std::collections::VecDeque;
|
||||
|
||||
const MOVEMENT_TICKS: u16 = 3000_u16.div_ceil(TICK_MS as u16);
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct MovingTask {
|
||||
path: VecDeque<(usize, usize)>,
|
||||
ticks: u16,
|
||||
total_steps: usize,
|
||||
}
|
||||
|
||||
impl MovingTask {
|
||||
pub fn new(path: VecDeque<(usize, usize)>) -> Self {
|
||||
Self {
|
||||
total_steps: path.len(),
|
||||
path,
|
||||
ticks: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_digging(task: DiggingTask) -> Self {
|
||||
Self {
|
||||
total_steps: task.get_total_steps(),
|
||||
path: task.get_path().clone(),
|
||||
ticks: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn pop_front(&mut self) {
|
||||
self.path.pop_front();
|
||||
self.ticks = 0;
|
||||
}
|
||||
|
||||
pub fn get_movement_progress(&self) -> Option<u16> {
|
||||
(self.ticks > 0).then(|| self.ticks * 100 / MOVEMENT_TICKS)
|
||||
}
|
||||
}
|
||||
|
||||
impl Task for MovingTask {
|
||||
fn get_icon(&self) -> char {
|
||||
'→'
|
||||
}
|
||||
|
||||
fn get_path(&self) -> &VecDeque<(usize, usize)> {
|
||||
&self.path
|
||||
}
|
||||
|
||||
fn get_path_front_coords(&self) -> &(usize, usize) {
|
||||
self.path.front().unwrap_or(&(0, 0))
|
||||
}
|
||||
|
||||
fn get_path_back_coords(&self) -> &(usize, usize) {
|
||||
self.path.back().unwrap_or(&(0, 0))
|
||||
}
|
||||
|
||||
fn get_progress(&self) -> f32 {
|
||||
f32::from(self.ticks) * 100.0 / f32::from(MOVEMENT_TICKS)
|
||||
}
|
||||
|
||||
fn get_total_steps(&self) -> usize {
|
||||
self.total_steps
|
||||
}
|
||||
|
||||
fn progress(&mut self) {
|
||||
self.ticks = (self.ticks + 1).min(MOVEMENT_TICKS);
|
||||
}
|
||||
|
||||
fn is_finished(&self) -> bool {
|
||||
self.ticks == MOVEMENT_TICKS
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
use std::collections::VecDeque;
|
||||
|
||||
pub trait Task {
|
||||
fn get_icon(&self) -> char;
|
||||
|
||||
fn get_path(&self) -> &VecDeque<(usize, usize)>;
|
||||
|
||||
fn get_path_front_coords(&self) -> &(usize, usize);
|
||||
|
||||
fn get_path_back_coords(&self) -> &(usize, usize);
|
||||
|
||||
fn get_progress(&self) -> f32;
|
||||
|
||||
fn get_total_steps(&self) -> usize;
|
||||
|
||||
fn get_total_progress(&self) -> f32 {
|
||||
let total: usize = self.get_total_steps();
|
||||
if total == 0 {
|
||||
return 100.0;
|
||||
}
|
||||
let completed: usize = total.saturating_sub(self.get_path().len());
|
||||
((completed as f32 * 100.0 + self.get_progress()) / total as f32).clamp(0.0, 100.0)
|
||||
}
|
||||
|
||||
fn progress(&mut self);
|
||||
|
||||
fn is_finished(&self) -> bool;
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
use crate::app::states::skirmish_states::tasks::{DiggingTask, MovingTask, Task};
|
||||
use std::collections::VecDeque;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Tasks {
|
||||
Digging(DiggingTask),
|
||||
Moving(MovingTask),
|
||||
}
|
||||
|
||||
impl Tasks {
|
||||
#[inline]
|
||||
fn t(&self) -> &dyn Task {
|
||||
match self {
|
||||
Self::Digging(d) => d,
|
||||
Self::Moving(m) => m,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn t_mut(&mut self) -> &mut dyn Task {
|
||||
match self {
|
||||
Self::Digging(d) => d,
|
||||
Self::Moving(m) => m,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Task for Tasks {
|
||||
fn get_icon(&self) -> char {
|
||||
self.t().get_icon()
|
||||
}
|
||||
|
||||
fn get_path(&self) -> &VecDeque<(usize, usize)> {
|
||||
self.t().get_path()
|
||||
}
|
||||
|
||||
fn get_path_front_coords(&self) -> &(usize, usize) {
|
||||
self.t().get_path_front_coords()
|
||||
}
|
||||
|
||||
fn get_path_back_coords(&self) -> &(usize, usize) {
|
||||
self.t().get_path_back_coords()
|
||||
}
|
||||
|
||||
fn get_progress(&self) -> f32 {
|
||||
self.t().get_progress()
|
||||
}
|
||||
|
||||
fn get_total_steps(&self) -> usize {
|
||||
self.t().get_total_steps()
|
||||
}
|
||||
|
||||
fn is_finished(&self) -> bool {
|
||||
self.t().is_finished()
|
||||
}
|
||||
|
||||
fn progress(&mut self) {
|
||||
self.t_mut().progress();
|
||||
}
|
||||
}
|
||||
@@ -1,26 +1,125 @@
|
||||
use crate::app::states::skirmish_states::{Players, units::Unit};
|
||||
use ratatui::{style::Stylize, text::Line};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
use crate::app::states::skirmish_states::{
|
||||
Players,
|
||||
tasks::{Task, Tasks},
|
||||
units::Unit,
|
||||
};
|
||||
use std::collections::VecDeque;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct MinerUnit {
|
||||
owner: Players,
|
||||
owner: Players,
|
||||
hp: u16,
|
||||
can_dig: bool,
|
||||
digging_power: f32,
|
||||
coords: (usize, usize),
|
||||
tasks: VecDeque<Tasks>,
|
||||
}
|
||||
|
||||
impl MinerUnit {
|
||||
pub fn new(owner: Players) -> Self {
|
||||
Self { owner }
|
||||
pub fn new(owner: Players, coords: (usize, usize)) -> Self {
|
||||
Self {
|
||||
owner,
|
||||
hp: 100,
|
||||
can_dig: true,
|
||||
digging_power: 0.55,
|
||||
coords,
|
||||
tasks: VecDeque::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn col_to_letters(&self, col: usize) -> String {
|
||||
let mut col: usize = col + 1;
|
||||
let mut letters: Vec<char> = Vec::new();
|
||||
|
||||
while col > 0 {
|
||||
letters.push((b'A' + ((col - 1) % 26) as u8) as char);
|
||||
col = (col - 1) / 26;
|
||||
}
|
||||
|
||||
letters.iter().rev().collect()
|
||||
}
|
||||
|
||||
fn display_coords(&self, coords: &(usize, usize)) -> String {
|
||||
format!("{}{}", self.col_to_letters(coords.0), coords.1)
|
||||
}
|
||||
}
|
||||
|
||||
impl Unit for MinerUnit {
|
||||
fn get_owner(&self) -> Players {
|
||||
self.owner
|
||||
}
|
||||
fn get_owner(&self) -> Players {
|
||||
self.owner
|
||||
}
|
||||
|
||||
fn get_tag(&self) -> char {
|
||||
'M'
|
||||
}
|
||||
fn get_tag(&self) -> char {
|
||||
'M'
|
||||
}
|
||||
|
||||
fn get_name(&self) -> &'static str {
|
||||
"Miner"
|
||||
}
|
||||
fn get_name(&self) -> &'static str {
|
||||
"Miner"
|
||||
}
|
||||
|
||||
fn get_hp(&self) -> u16 {
|
||||
self.hp
|
||||
}
|
||||
|
||||
fn get_max_hp(&self) -> u16 {
|
||||
100
|
||||
}
|
||||
|
||||
fn get_can_dig(&self) -> bool {
|
||||
self.can_dig
|
||||
}
|
||||
|
||||
fn get_digging_power(&self) -> f32 {
|
||||
self.digging_power
|
||||
}
|
||||
|
||||
fn get_coords(&self) -> (usize, usize) {
|
||||
self.coords
|
||||
}
|
||||
|
||||
fn set_coords(&mut self, coords: (usize, usize)) {
|
||||
self.coords = coords;
|
||||
}
|
||||
|
||||
fn get_ref_tasks(&self) -> &VecDeque<Tasks> {
|
||||
&self.tasks
|
||||
}
|
||||
|
||||
fn get_mut_tasks(&mut self) -> &mut VecDeque<Tasks> {
|
||||
&mut self.tasks
|
||||
}
|
||||
|
||||
fn get_tasks_formatted(&self) -> Vec<Line<'_>> {
|
||||
Vec::from(
|
||||
self
|
||||
.tasks
|
||||
.iter()
|
||||
.enumerate()
|
||||
.flat_map(|(i, task): (usize, &Tasks)| -> [Line<'_>; 2] {
|
||||
[
|
||||
Line::from_iter([
|
||||
format!("{}. ", i + 1).gray(),
|
||||
task.get_icon().gray(),
|
||||
" ".gray(),
|
||||
self.display_coords(task.get_path_front_coords()).green(),
|
||||
" -> ".gray(),
|
||||
self.display_coords(task.get_path_back_coords()).yellow(),
|
||||
]),
|
||||
Line::from(format!(
|
||||
" Step: {}% | Total: {}%",
|
||||
task.get_progress() as u16,
|
||||
task.get_total_progress() as u16,
|
||||
))
|
||||
.cyan(),
|
||||
]
|
||||
})
|
||||
.collect::<Vec<Line<'_>>>(),
|
||||
)
|
||||
}
|
||||
|
||||
fn set_task(&mut self, task: Tasks) {
|
||||
self.tasks.push_back(task);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
mod miner;
|
||||
mod unit_trait;
|
||||
mod units_enum;
|
||||
mod units_trait;
|
||||
|
||||
pub use miner::MinerUnit;
|
||||
pub use unit_trait::Unit;
|
||||
pub use units_enum::Units;
|
||||
pub use units_trait::Unit;
|
||||
@@ -0,0 +1,82 @@
|
||||
use crate::app::states::skirmish_states::{Players, tasks::Tasks};
|
||||
use ratatui::{
|
||||
style::Stylize,
|
||||
text::{Line, Span},
|
||||
};
|
||||
use std::collections::VecDeque;
|
||||
|
||||
pub trait Unit {
|
||||
fn is_unit(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn get_owner(&self) -> Players;
|
||||
|
||||
fn get_tag(&self) -> char;
|
||||
|
||||
fn get_name(&self) -> &'static str;
|
||||
|
||||
fn get_hp(&self) -> u16;
|
||||
|
||||
fn get_max_hp(&self) -> u16;
|
||||
|
||||
fn get_can_dig(&self) -> bool;
|
||||
|
||||
fn get_digging_power(&self) -> f32;
|
||||
|
||||
fn get_coords(&self) -> (usize, usize);
|
||||
|
||||
fn set_coords(&mut self, coords: (usize, usize));
|
||||
|
||||
fn get_ref_tasks(&self) -> &VecDeque<Tasks>;
|
||||
|
||||
fn get_mut_tasks(&mut self) -> &mut VecDeque<Tasks>;
|
||||
|
||||
fn set_task(&mut self, task: Tasks);
|
||||
|
||||
fn get_tasks_formatted(&self) -> Vec<Line<'_>>;
|
||||
|
||||
fn base_text(&self) -> Vec<Line<'_>> {
|
||||
let owner: Span<'_> = self.get_owner().get_span();
|
||||
let hp: u16 = self.get_hp();
|
||||
let max_hp: u16 = self.get_max_hp();
|
||||
let hp_percent: u16 = hp * 100 / max_hp;
|
||||
let can_dig: bool = self.get_can_dig();
|
||||
let digging_power: f32 = self.get_digging_power();
|
||||
let have_any_tasks: bool = self.get_ref_tasks().is_empty();
|
||||
let tasks: Vec<Line<'_>> = self.get_tasks_formatted();
|
||||
|
||||
let mut lines: Vec<Line<'_>> = Vec::from([
|
||||
Line::from_iter(["Owner: ".gray(), owner]),
|
||||
Line::from_iter([
|
||||
"HP: ".gray(),
|
||||
hp.green(),
|
||||
"/".gray(),
|
||||
max_hp.green(),
|
||||
" ( ".gray(),
|
||||
hp_percent.green(),
|
||||
"% )".gray(),
|
||||
]),
|
||||
]);
|
||||
|
||||
if can_dig {
|
||||
lines.push(Line::from_iter([
|
||||
"Digging power: ".gray(),
|
||||
digging_power.green(),
|
||||
]));
|
||||
}
|
||||
|
||||
if !have_any_tasks {
|
||||
lines.push(Line::from("Tasks Queue: ".gray()));
|
||||
for task in tasks {
|
||||
lines.push(task);
|
||||
}
|
||||
}
|
||||
|
||||
lines
|
||||
}
|
||||
|
||||
fn text(&self) -> Vec<Line<'_>> {
|
||||
self.base_text()
|
||||
}
|
||||
}
|
||||
@@ -1,45 +1,150 @@
|
||||
use crate::app::states::skirmish_states::{
|
||||
Players,
|
||||
units::{MinerUnit, Unit},
|
||||
Players,
|
||||
tasks::Tasks,
|
||||
units::{MinerUnit, Unit},
|
||||
};
|
||||
use ratatui::text::Line;
|
||||
use std::collections::VecDeque;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum Units {
|
||||
Miner(MinerUnit),
|
||||
Miner(MinerUnit),
|
||||
}
|
||||
|
||||
impl Units {
|
||||
fn unit(&self) -> &dyn Unit {
|
||||
match self {
|
||||
Self::Miner(m) => m,
|
||||
}
|
||||
#[inline]
|
||||
pub fn u(&self) -> &dyn Unit {
|
||||
match self {
|
||||
Self::Miner(m) => m,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn u_mut(&mut self) -> &mut dyn Unit {
|
||||
match self {
|
||||
Self::Miner(m) => m,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Unit for Units {
|
||||
fn get_tag(&self) -> char {
|
||||
self.unit().get_tag()
|
||||
}
|
||||
fn get_tag(&self) -> char {
|
||||
self.u().get_tag()
|
||||
}
|
||||
|
||||
fn get_name(&self) -> &'static str {
|
||||
self.unit().get_name()
|
||||
}
|
||||
fn get_name(&self) -> &'static str {
|
||||
self.u().get_name()
|
||||
}
|
||||
|
||||
fn get_owner(&self) -> Players {
|
||||
self.unit().get_owner()
|
||||
}
|
||||
fn get_owner(&self) -> Players {
|
||||
self.u().get_owner()
|
||||
}
|
||||
|
||||
fn get_hp(&self) -> u16 {
|
||||
self.u().get_hp()
|
||||
}
|
||||
|
||||
fn get_max_hp(&self) -> u16 {
|
||||
self.u().get_max_hp()
|
||||
}
|
||||
|
||||
fn get_can_dig(&self) -> bool {
|
||||
self.u().get_can_dig()
|
||||
}
|
||||
|
||||
fn get_digging_power(&self) -> f32 {
|
||||
self.u().get_digging_power()
|
||||
}
|
||||
|
||||
fn get_coords(&self) -> (usize, usize) {
|
||||
self.u().get_coords()
|
||||
}
|
||||
|
||||
fn set_coords(&mut self, coords: (usize, usize)) {
|
||||
self.u_mut().set_coords(coords);
|
||||
}
|
||||
|
||||
fn get_ref_tasks(&self) -> &VecDeque<Tasks> {
|
||||
self.u().get_ref_tasks()
|
||||
}
|
||||
|
||||
fn get_mut_tasks(&mut self) -> &mut VecDeque<Tasks> {
|
||||
self.u_mut().get_mut_tasks()
|
||||
}
|
||||
|
||||
fn get_tasks_formatted(&self) -> Vec<Line<'_>> {
|
||||
self.u().get_tasks_formatted()
|
||||
}
|
||||
|
||||
fn set_task(&mut self, task: Tasks) {
|
||||
self.u_mut().set_task(task);
|
||||
}
|
||||
}
|
||||
|
||||
impl Unit for Option<Units> {
|
||||
fn get_tag(&self) -> char {
|
||||
self.map_or(' ', |u| u.get_tag())
|
||||
}
|
||||
fn is_unit(&self) -> bool {
|
||||
self.as_ref().map_or(false, |u| u.is_unit())
|
||||
}
|
||||
|
||||
fn get_name(&self) -> &'static str {
|
||||
self.map_or("", |u| u.get_name())
|
||||
}
|
||||
fn get_tag(&self) -> char {
|
||||
self.as_ref().map_or(' ', |u| u.get_tag())
|
||||
}
|
||||
|
||||
fn get_owner(&self) -> Players {
|
||||
self.map_or(Players::Unclaimed, |u| u.get_owner())
|
||||
fn get_name(&self) -> &'static str {
|
||||
self.as_ref().map_or("", |u| u.get_name())
|
||||
}
|
||||
|
||||
fn get_owner(&self) -> Players {
|
||||
self.as_ref().map_or(Players::Unclaimed, |u| u.get_owner())
|
||||
}
|
||||
|
||||
fn get_hp(&self) -> u16 {
|
||||
self.as_ref().map_or(0, |u| u.get_hp())
|
||||
}
|
||||
|
||||
fn get_max_hp(&self) -> u16 {
|
||||
self.as_ref().map_or(0, |u| u.get_max_hp())
|
||||
}
|
||||
|
||||
fn get_can_dig(&self) -> bool {
|
||||
self.as_ref().map_or(false, |u| u.get_can_dig())
|
||||
}
|
||||
|
||||
fn get_digging_power(&self) -> f32 {
|
||||
self.as_ref().map_or(0.0, |u| u.get_digging_power())
|
||||
}
|
||||
|
||||
fn get_coords(&self) -> (usize, usize) {
|
||||
self.as_ref().map_or((0, 0), |u| u.get_coords())
|
||||
}
|
||||
|
||||
fn set_coords(&mut self, coords: (usize, usize)) {
|
||||
if let Some(unit) = self.as_mut() {
|
||||
unit.set_coords(coords);
|
||||
}
|
||||
}
|
||||
|
||||
fn get_ref_tasks(&self) -> &VecDeque<Tasks> {
|
||||
self
|
||||
.as_ref()
|
||||
.expect("attempted to get referenced tasks from `None`")
|
||||
.get_ref_tasks()
|
||||
}
|
||||
|
||||
fn get_mut_tasks(&mut self) -> &mut VecDeque<Tasks> {
|
||||
self
|
||||
.as_mut()
|
||||
.expect("attempted to get mutable tasks from `None`")
|
||||
.get_mut_tasks()
|
||||
}
|
||||
|
||||
fn get_tasks_formatted(&self) -> Vec<Line<'_>> {
|
||||
self
|
||||
.as_ref()
|
||||
.map_or(Vec::new(), |u| u.get_tasks_formatted())
|
||||
}
|
||||
|
||||
fn set_task(&mut self, task: Tasks) {
|
||||
self.as_mut().map_or((), |u| u.set_task(task))
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
use crate::app::states::skirmish_states::Players;
|
||||
|
||||
pub trait Unit {
|
||||
fn get_owner(&self) -> Players;
|
||||
fn get_tag(&self) -> char;
|
||||
fn get_name(&self) -> &'static str;
|
||||
}
|
||||
@@ -1,50 +1,58 @@
|
||||
use clap::ValueEnum;
|
||||
use ratatui::text::Line;
|
||||
use ratatui::text::{Line, Span};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, ValueEnum)]
|
||||
pub enum ZoomLevel {
|
||||
ZoomedIn,
|
||||
Default,
|
||||
ZoomedOut,
|
||||
ZoomedIn,
|
||||
Default,
|
||||
ZoomedOut,
|
||||
}
|
||||
|
||||
pub enum CellSizes {
|
||||
Width,
|
||||
Height,
|
||||
Width,
|
||||
Height,
|
||||
}
|
||||
|
||||
impl ZoomLevel {
|
||||
pub fn get_cell_size(&self, cell_size: CellSizes) -> usize {
|
||||
match (cell_size, self) {
|
||||
(CellSizes::Width, ZoomLevel::ZoomedIn) => 13,
|
||||
(CellSizes::Width, ZoomLevel::Default) => 9,
|
||||
(CellSizes::Width, ZoomLevel::ZoomedOut) => 5,
|
||||
(CellSizes::Height, ZoomLevel::ZoomedIn) => 7,
|
||||
(CellSizes::Height, ZoomLevel::Default) => 5,
|
||||
(CellSizes::Height, ZoomLevel::ZoomedOut) => 3,
|
||||
}
|
||||
pub fn get_cell_size(&self, cell_size: CellSizes) -> usize {
|
||||
match (cell_size, self) {
|
||||
(CellSizes::Width, ZoomLevel::ZoomedIn) => 13,
|
||||
(CellSizes::Width, ZoomLevel::Default) => 9,
|
||||
(CellSizes::Width, ZoomLevel::ZoomedOut) => 5,
|
||||
(CellSizes::Height, ZoomLevel::ZoomedIn) => 7,
|
||||
(CellSizes::Height, ZoomLevel::Default) => 5,
|
||||
(CellSizes::Height, ZoomLevel::ZoomedOut) => 3,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_cell_text_area(
|
||||
&self,
|
||||
tag: char,
|
||||
structure_level: char,
|
||||
unit: char,
|
||||
) -> Vec<Line<'_>> {
|
||||
match self {
|
||||
ZoomLevel::ZoomedIn => vec![
|
||||
Line::from(format!(" {} {} ", structure_level, unit)),
|
||||
Line::from(format!(" ")),
|
||||
Line::from(format!(" {} ", tag)),
|
||||
Line::from(format!(" ")),
|
||||
Line::from(format!(" ")),
|
||||
],
|
||||
ZoomLevel::Default => vec![
|
||||
Line::from(format!(" {} {} ", structure_level, unit)),
|
||||
Line::from(format!(" {} ", tag)),
|
||||
Line::from(format!(" ")),
|
||||
],
|
||||
ZoomLevel::ZoomedOut => vec![Line::from(format!(" {} ", tag))],
|
||||
}
|
||||
pub fn get_cell_text_area<'a>(
|
||||
&self,
|
||||
tag: char,
|
||||
structure_level: char,
|
||||
unit: Span<'a>,
|
||||
) -> Vec<Line<'a>> {
|
||||
match self {
|
||||
ZoomLevel::ZoomedIn => vec![
|
||||
Line::from(vec![
|
||||
Span::raw(format!(" {} ", structure_level)),
|
||||
unit,
|
||||
Span::raw(" "),
|
||||
]),
|
||||
Line::from(format!(" ")),
|
||||
Line::from(format!(" {} ", tag)),
|
||||
Line::from(format!(" ")),
|
||||
Line::from(format!(" ")),
|
||||
],
|
||||
ZoomLevel::Default => vec![
|
||||
Line::from(vec![
|
||||
Span::raw(format!(" {} ", structure_level)),
|
||||
unit,
|
||||
Span::raw(" "),
|
||||
]),
|
||||
Line::from(format!(" {} ", tag)),
|
||||
Line::from(format!(" ")),
|
||||
],
|
||||
ZoomLevel::ZoomedOut => vec![Line::from(format!(" {} ", tag))],
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
use crate::app::threads::AudioCmd;
|
||||
use ratatui::crossterm::event::KeyEvent;
|
||||
use std::sync::mpsc::{Receiver, Sender, channel};
|
||||
|
||||
pub struct AppChannels {
|
||||
pub input_tx: Sender<KeyEvent>,
|
||||
pub input_rx: Receiver<KeyEvent>,
|
||||
|
||||
pub resize_tx: Sender<(u16, u16)>,
|
||||
pub resize_rx: Receiver<(u16, u16)>,
|
||||
|
||||
pub tick_tx: Sender<()>,
|
||||
pub tick_rx: Receiver<()>,
|
||||
|
||||
pub audio_tx: Sender<AudioCmd>,
|
||||
pub audio_rx: Receiver<AudioCmd>,
|
||||
}
|
||||
|
||||
impl AppChannels {
|
||||
pub fn new() -> Self {
|
||||
let (input_tx, input_rx) = channel::<KeyEvent>();
|
||||
let (resize_tx, resize_rx) = channel::<(u16, u16)>();
|
||||
let (tick_tx, tick_rx) = channel::<()>();
|
||||
let (audio_tx, audio_rx) = channel::<AudioCmd>();
|
||||
|
||||
Self {
|
||||
input_tx,
|
||||
input_rx,
|
||||
resize_tx,
|
||||
resize_rx,
|
||||
tick_tx,
|
||||
tick_rx,
|
||||
audio_tx,
|
||||
audio_rx,
|
||||
}
|
||||
}
|
||||
}
|
||||
+137
-132
@@ -1,180 +1,185 @@
|
||||
use clap::ValueEnum;
|
||||
use once_cell::sync::Lazy;
|
||||
use rodio::{
|
||||
Decoder, DeviceSinkBuilder, MixerDeviceSink, Player, Source, cpal::BufferSize, source::Amplify,
|
||||
Decoder, DeviceSinkBuilder, MixerDeviceSink, Player, Source, cpal::BufferSize,
|
||||
decoder::LoopedDecoder, source::Amplify,
|
||||
};
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fs::read,
|
||||
io::{BufReader, Cursor},
|
||||
process::exit,
|
||||
sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard, mpsc::Receiver},
|
||||
collections::HashMap,
|
||||
fs::read,
|
||||
io::{BufReader, Cursor},
|
||||
path::{Path, PathBuf},
|
||||
process::exit,
|
||||
sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard, mpsc::Receiver},
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum AudioCmd {
|
||||
Mute,
|
||||
VolumeUp,
|
||||
VolumeDown,
|
||||
ChangePart(SoundrackParts),
|
||||
ChangeSoundtrack(Soundtrack),
|
||||
Mute,
|
||||
VolumeUp,
|
||||
VolumeDown,
|
||||
ChangePart(SoundrackParts),
|
||||
ChangeSoundtrack(Soundtrack),
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy, ValueEnum)]
|
||||
pub enum Soundtrack {
|
||||
Default,
|
||||
Default,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Hash, Clone, Copy)]
|
||||
pub enum SoundrackParts {
|
||||
Calm,
|
||||
Buildup,
|
||||
Assault,
|
||||
Outro,
|
||||
Calm,
|
||||
Buildup,
|
||||
Assault,
|
||||
Outro,
|
||||
}
|
||||
|
||||
type SoundtrackCache = HashMap<SoundrackParts, Vec<u8>>;
|
||||
|
||||
static PRELOADED_SOUNDTRACKS: Lazy<RwLock<HashMap<Soundtrack, Arc<SoundtrackCache>>>> =
|
||||
Lazy::new(|| {
|
||||
let mut map: HashMap<Soundtrack, Arc<HashMap<SoundrackParts, Vec<u8>>>> = HashMap::new();
|
||||
Lazy::new(|| {
|
||||
let mut map: HashMap<Soundtrack, Arc<HashMap<SoundrackParts, Vec<u8>>>> = HashMap::new();
|
||||
|
||||
map.insert(
|
||||
Soundtrack::Default,
|
||||
Arc::new(load_soundtrack_folder("default")),
|
||||
);
|
||||
map.insert(
|
||||
Soundtrack::Default,
|
||||
Arc::new(load_soundtrack_folder("default")),
|
||||
);
|
||||
|
||||
RwLock::new(map)
|
||||
});
|
||||
RwLock::new(map)
|
||||
});
|
||||
|
||||
fn load_soundtrack_folder(folder: &str) -> SoundtrackCache {
|
||||
let base: String = format!("soundtracks/{}", folder);
|
||||
let mut cache: HashMap<SoundrackParts, Vec<u8>> = HashMap::new();
|
||||
let soundtrack_root: PathBuf = std::env::current_exe()
|
||||
.ok()
|
||||
.and_then(|executable: PathBuf| -> Option<PathBuf> {
|
||||
executable
|
||||
.parent()
|
||||
.map(|directory: &Path| -> PathBuf { directory.join("soundtracks") })
|
||||
})
|
||||
.filter(|directory: &PathBuf| -> bool { directory.is_dir() })
|
||||
.unwrap_or_else(|| -> PathBuf { PathBuf::from("soundtracks") });
|
||||
let base: PathBuf = soundtrack_root.join(folder);
|
||||
let mut cache: HashMap<SoundrackParts, Vec<u8>> = HashMap::new();
|
||||
|
||||
macro_rules! load {
|
||||
($part:ident, $file:expr) => {
|
||||
let path = format!("{}/{}", base, $file);
|
||||
cache.insert(
|
||||
SoundrackParts::$part,
|
||||
read(&path).expect(&format!("cannot read {}", path)),
|
||||
);
|
||||
};
|
||||
}
|
||||
macro_rules! load {
|
||||
($part:ident, $file:expr) => {
|
||||
let path: PathBuf = base.join($file);
|
||||
cache.insert(
|
||||
SoundrackParts::$part,
|
||||
read(&path).expect(&format!("cannot read {}", path.display())),
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
load!(Calm, "test.ogg"); // calm.ogg
|
||||
load!(Buildup, "test.ogg"); // buildup.ogg
|
||||
load!(Assault, "test.ogg"); // assault.ogg
|
||||
load!(Outro, "test.ogg"); // outro.ogg
|
||||
load!(Calm, "calm.ogg"); // calm.ogg
|
||||
load!(Buildup, "test.ogg"); // buildup.ogg
|
||||
load!(Assault, "test.ogg"); // assault.ogg
|
||||
load!(Outro, "test.ogg"); // outro.ogg
|
||||
|
||||
cache
|
||||
cache
|
||||
}
|
||||
|
||||
fn load_audio(
|
||||
soundtrack: &Soundtrack,
|
||||
part: &SoundrackParts,
|
||||
) -> Amplify<Decoder<BufReader<Cursor<Vec<u8>>>>> {
|
||||
{
|
||||
let guard: RwLockReadGuard<'_, HashMap<Soundtrack, Arc<HashMap<SoundrackParts, Vec<u8>>>>> =
|
||||
PRELOADED_SOUNDTRACKS
|
||||
.read()
|
||||
.map_err(|e| {
|
||||
eprintln!("RwLock poisoned: {}", e);
|
||||
exit(1);
|
||||
})
|
||||
.expect("lock issue");
|
||||
pub fn load_audio(
|
||||
soundtrack: &Soundtrack,
|
||||
part: &SoundrackParts,
|
||||
) -> Amplify<LoopedDecoder<BufReader<Cursor<Vec<u8>>>>> {
|
||||
{
|
||||
let guard: RwLockReadGuard<'_, HashMap<Soundtrack, Arc<HashMap<SoundrackParts, Vec<u8>>>>> =
|
||||
PRELOADED_SOUNDTRACKS
|
||||
.read()
|
||||
.map_err(|e| {
|
||||
eprintln!("RwLock poisoned: {}", e);
|
||||
exit(1);
|
||||
})
|
||||
.expect("lock issue");
|
||||
|
||||
if let Some(soundtrack_cache) = guard.get(soundtrack) {
|
||||
let data: &Vec<u8> = soundtrack_cache.get(part).expect("part missing in cache");
|
||||
if let Some(soundtrack_cache) = guard.get(soundtrack) {
|
||||
let data: &Vec<u8> = soundtrack_cache.get(part).expect("part missing in cache");
|
||||
|
||||
return Decoder::new(BufReader::new(Cursor::new(data.clone())))
|
||||
.expect("decoder issue")
|
||||
.amplify(0.20);
|
||||
}
|
||||
return Decoder::new_looped(BufReader::new(Cursor::new(data.clone())))
|
||||
.expect("decoder issue")
|
||||
.amplify(0.20);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let mut guard: RwLockWriteGuard<
|
||||
'_,
|
||||
HashMap<Soundtrack, Arc<HashMap<SoundrackParts, Vec<u8>>>>,
|
||||
> = PRELOADED_SOUNDTRACKS
|
||||
.write()
|
||||
.map_err(|e| {
|
||||
eprintln!("RwLock poisoned: {}", e);
|
||||
exit(1);
|
||||
})
|
||||
.expect("lock issue");
|
||||
|
||||
if let Some(soundtrack_cache) = guard.get(soundtrack) {
|
||||
let data: &Vec<u8> = soundtrack_cache.get(part).expect("part missing in cache");
|
||||
|
||||
return Decoder::new_looped(BufReader::new(Cursor::new(data.clone())))
|
||||
.expect("decoder issue")
|
||||
.amplify(0.20);
|
||||
}
|
||||
|
||||
{
|
||||
let mut guard: RwLockWriteGuard<
|
||||
'_,
|
||||
HashMap<Soundtrack, Arc<HashMap<SoundrackParts, Vec<u8>>>>,
|
||||
> = PRELOADED_SOUNDTRACKS
|
||||
.write()
|
||||
.map_err(|e| {
|
||||
eprintln!("RwLock poisoned: {}", e);
|
||||
exit(1);
|
||||
})
|
||||
.expect("lock issue");
|
||||
let folder_name: &str = match soundtrack {
|
||||
Soundtrack::Default => "default",
|
||||
};
|
||||
|
||||
if let Some(soundtrack_cache) = guard.get(soundtrack) {
|
||||
let data: &Vec<u8> = soundtrack_cache.get(part).expect("part missing in cache");
|
||||
let new_cache: Arc<HashMap<SoundrackParts, Vec<u8>>> =
|
||||
Arc::new(load_soundtrack_folder(folder_name));
|
||||
|
||||
return Decoder::new(BufReader::new(Cursor::new(data.clone())))
|
||||
.expect("decoder issue")
|
||||
.amplify(0.20);
|
||||
}
|
||||
let data: &Vec<u8> = new_cache.get(&part).expect("part missing after load");
|
||||
|
||||
let folder_name: &str = match soundtrack {
|
||||
Soundtrack::Default => "default",
|
||||
};
|
||||
guard.insert(*soundtrack, Arc::clone(&new_cache));
|
||||
|
||||
let new_cache: Arc<HashMap<SoundrackParts, Vec<u8>>> =
|
||||
Arc::new(load_soundtrack_folder(folder_name));
|
||||
|
||||
let data: &Vec<u8> = new_cache.get(&part).expect("part missing after load");
|
||||
|
||||
guard.insert(*soundtrack, Arc::clone(&new_cache));
|
||||
|
||||
Decoder::new(BufReader::new(Cursor::new(data.clone())))
|
||||
.expect("decoder issue")
|
||||
.amplify(0.20)
|
||||
}
|
||||
Decoder::new_looped(BufReader::new(Cursor::new(data.clone())))
|
||||
.expect("decoder issue")
|
||||
.amplify(0.20)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn handle_audio(rx: Receiver<AudioCmd>, mute: bool, soundtrack: Soundtrack) {
|
||||
let mut handle: MixerDeviceSink = DeviceSinkBuilder::from_default_device()
|
||||
.expect("get default device")
|
||||
.with_buffer_size(BufferSize::Fixed(4096))
|
||||
.open_sink_or_fallback()
|
||||
.expect("open device audio stream");
|
||||
handle.log_on_drop(false);
|
||||
let mut handle: MixerDeviceSink = DeviceSinkBuilder::from_default_device()
|
||||
.expect("get default device")
|
||||
.with_buffer_size(BufferSize::Fixed(4096))
|
||||
.open_sink_or_fallback()
|
||||
.expect("open device audio stream");
|
||||
handle.log_on_drop(false);
|
||||
|
||||
let player: Player = Player::connect_new(&handle.mixer());
|
||||
let player: Player = Player::connect_new(&handle.mixer());
|
||||
|
||||
let mut volume: f32 = player.volume();
|
||||
player.set_volume(if mute { 0.0 } else { volume });
|
||||
let mut volume: f32 = player.volume();
|
||||
player.set_volume(if mute { 0.0 } else { volume });
|
||||
|
||||
let mut current_soundtrack: Soundtrack = soundtrack;
|
||||
let mut current_part: SoundrackParts = SoundrackParts::Calm;
|
||||
player.append(load_audio(¤t_soundtrack, ¤t_part));
|
||||
let mut current_soundtrack: Soundtrack = soundtrack;
|
||||
let mut current_part: SoundrackParts = SoundrackParts::Calm;
|
||||
player.append(load_audio(¤t_soundtrack, ¤t_part));
|
||||
|
||||
loop {
|
||||
for cmd in rx.try_iter() {
|
||||
match cmd {
|
||||
AudioCmd::Mute => {
|
||||
player.set_volume(if player.volume() == 0.0 { volume } else { 0.0 });
|
||||
}
|
||||
AudioCmd::VolumeUp => {
|
||||
volume = (volume + 0.1).min(1.0);
|
||||
player.set_volume(volume);
|
||||
}
|
||||
AudioCmd::VolumeDown => {
|
||||
volume = (volume - 0.1).max(0.0);
|
||||
player.set_volume(volume);
|
||||
}
|
||||
AudioCmd::ChangePart(part) => {
|
||||
current_part = part;
|
||||
player.append(load_audio(¤t_soundtrack, ¤t_part));
|
||||
player.skip_one();
|
||||
}
|
||||
AudioCmd::ChangeSoundtrack(st) => {
|
||||
current_soundtrack = st;
|
||||
player.append(load_audio(¤t_soundtrack, ¤t_part));
|
||||
player.skip_one();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if player.empty() {
|
||||
player.append(load_audio(¤t_soundtrack, ¤t_part));
|
||||
}
|
||||
for cmd in rx {
|
||||
match cmd {
|
||||
AudioCmd::Mute => {
|
||||
player.set_volume(if player.volume() == 0.0 { volume } else { 0.0 });
|
||||
}
|
||||
AudioCmd::VolumeUp => {
|
||||
volume = (volume + 0.1).min(1.0);
|
||||
player.set_volume(volume);
|
||||
}
|
||||
AudioCmd::VolumeDown => {
|
||||
volume = (volume - 0.1).max(0.0);
|
||||
player.set_volume(volume);
|
||||
}
|
||||
AudioCmd::ChangePart(part) => {
|
||||
current_part = part;
|
||||
player.append(load_audio(¤t_soundtrack, ¤t_part));
|
||||
player.skip_one();
|
||||
}
|
||||
AudioCmd::ChangeSoundtrack(st) => {
|
||||
current_soundtrack = st;
|
||||
player.append(load_audio(¤t_soundtrack, ¤t_part));
|
||||
player.skip_one();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+14
-14
@@ -2,20 +2,20 @@ use ratatui::crossterm::event::{Event, KeyEvent, read};
|
||||
use std::sync::mpsc::Sender;
|
||||
|
||||
pub fn handle_ct_events(input_tx: Sender<KeyEvent>, resize_tx: Sender<(u16, u16)>) {
|
||||
loop {
|
||||
match read() {
|
||||
Ok(Event::Key(k)) => {
|
||||
if input_tx.send(k).is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(Event::Resize(cols, rows)) => {
|
||||
if resize_tx.send((cols, rows)).is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(_) => continue,
|
||||
loop {
|
||||
match read() {
|
||||
Ok(Event::Key(k)) => {
|
||||
if input_tx.send(k).is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(Event::Resize(cols, rows)) => {
|
||||
if resize_tx.send((cols, rows)).is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
Ok(_) => {}
|
||||
Err(_) => continue,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
mod app_channels;
|
||||
mod audio;
|
||||
mod events;
|
||||
mod tick;
|
||||
|
||||
pub use app_channels::AppChannels;
|
||||
pub use audio::{AudioCmd, SoundrackParts, Soundtrack, handle_audio};
|
||||
pub use audio::{AudioCmd, SoundrackParts, Soundtrack, handle_audio, load_audio};
|
||||
pub use events::handle_ct_events;
|
||||
pub use tick::handle_tick_event;
|
||||
pub use tick::{TICK_MS, handle_tick_event};
|
||||
+15
-13
@@ -1,19 +1,21 @@
|
||||
use std::{
|
||||
sync::mpsc::Sender,
|
||||
thread,
|
||||
time::{Duration, Instant},
|
||||
sync::mpsc::Sender,
|
||||
thread,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
pub fn handle_tick_event(tx: Sender<()>, interval_ms: u8) {
|
||||
let interval: Duration = Duration::from_millis(interval_ms as u64);
|
||||
loop {
|
||||
if tx.send(()).is_err() {
|
||||
break;
|
||||
}
|
||||
pub const TICK_MS: u8 = 33;
|
||||
|
||||
let elapsed: Duration = Instant::now().elapsed();
|
||||
if interval > elapsed {
|
||||
thread::sleep(interval - elapsed);
|
||||
}
|
||||
pub fn handle_tick_event(tx: Sender<()>) {
|
||||
let interval: Duration = Duration::from_millis(TICK_MS as u64);
|
||||
loop {
|
||||
if tx.send(()).is_err() {
|
||||
break;
|
||||
}
|
||||
|
||||
let elapsed: Duration = Instant::now().elapsed();
|
||||
if interval > elapsed {
|
||||
thread::sleep(interval - elapsed);
|
||||
}
|
||||
}
|
||||
}
|
||||
+15
-15
@@ -1,27 +1,27 @@
|
||||
use crate::app::{
|
||||
App,
|
||||
views::{default_view, main_menu_view, skirmish_view},
|
||||
App,
|
||||
views::{default_view, main_menu_view, skirmish_view},
|
||||
};
|
||||
use clap::ValueEnum;
|
||||
use ratatui::{buffer::Buffer, layout::Rect, widgets::Widget};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, ValueEnum)]
|
||||
pub enum View {
|
||||
MainMenu,
|
||||
Skirmish,
|
||||
PerkDecks,
|
||||
SkillsConfig,
|
||||
MainMenu,
|
||||
Skirmish,
|
||||
PerkDecks,
|
||||
SkillsConfig,
|
||||
}
|
||||
|
||||
impl Widget for &mut App {
|
||||
fn render(self, area: Rect, buf: &mut Buffer)
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
match self.view {
|
||||
View::MainMenu => main_menu_view(self, area, buf),
|
||||
View::Skirmish => skirmish_view(self, area, buf),
|
||||
_ => default_view(area, buf),
|
||||
}
|
||||
fn render(self, area: Rect, buf: &mut Buffer)
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
match self.view {
|
||||
View::MainMenu => main_menu_view(self, area, buf),
|
||||
View::Skirmish => skirmish_view(self, area, buf),
|
||||
_ => default_view(area, buf),
|
||||
}
|
||||
}
|
||||
}
|
||||
+26
-26
@@ -1,40 +1,40 @@
|
||||
use crate::app::{
|
||||
keybindings::{Action, count_largest_group},
|
||||
widgets::KeybindingsWidget,
|
||||
keybindings::{Action, count_largest_group},
|
||||
widgets::KeybindingsWidget,
|
||||
};
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::{Alignment, Constraint, Layout, Rect},
|
||||
style::Stylize,
|
||||
widgets::{Block, Borders, Paragraph, Widget},
|
||||
buffer::Buffer,
|
||||
layout::{Alignment, Constraint, Layout, Rect},
|
||||
style::Stylize,
|
||||
widgets::{Block, Borders, Paragraph, Widget},
|
||||
};
|
||||
|
||||
const ACTIONS: &[Action] = &[Action::Quit, Action::Quit2, Action::Esc];
|
||||
|
||||
fn default_view_layout(area: Rect) -> [Rect; 2] {
|
||||
Layout::vertical([
|
||||
Constraint::Fill(1),
|
||||
Constraint::Length(count_largest_group(&ACTIONS) + 2),
|
||||
])
|
||||
.areas(area)
|
||||
Layout::vertical([
|
||||
Constraint::Fill(1),
|
||||
Constraint::Length(count_largest_group(&ACTIONS) + 2),
|
||||
])
|
||||
.areas(area)
|
||||
}
|
||||
|
||||
pub fn default_view(area: Rect, buf: &mut Buffer) {
|
||||
let [main_area, keybindings_area] = default_view_layout(area);
|
||||
let [main_area, keybindings_area] = default_view_layout(area);
|
||||
|
||||
{
|
||||
Paragraph::new("Work in progress")
|
||||
.alignment(Alignment::Center)
|
||||
.yellow()
|
||||
.block(
|
||||
Block::new()
|
||||
.gray()
|
||||
.borders(Borders::LEFT | Borders::TOP | Borders::RIGHT),
|
||||
)
|
||||
.render(main_area, buf);
|
||||
}
|
||||
{
|
||||
Paragraph::new("Work in progress")
|
||||
.alignment(Alignment::Center)
|
||||
.yellow()
|
||||
.block(
|
||||
Block::new()
|
||||
.gray()
|
||||
.borders(Borders::LEFT | Borders::TOP | Borders::RIGHT),
|
||||
)
|
||||
.render(main_area, buf);
|
||||
}
|
||||
|
||||
{
|
||||
KeybindingsWidget::new(ACTIONS, keybindings_area.height).render(keybindings_area, buf);
|
||||
}
|
||||
{
|
||||
KeybindingsWidget::new(ACTIONS, keybindings_area.height).render(keybindings_area, buf);
|
||||
}
|
||||
}
|
||||
+74
-72
@@ -1,98 +1,100 @@
|
||||
use crate::app::{
|
||||
App, View,
|
||||
helpers::main_menu_option_helper,
|
||||
keybindings::{Action, count_largest_group},
|
||||
widgets::KeybindingsWidget,
|
||||
App, View,
|
||||
helpers::main_menu_option_helper,
|
||||
keybindings::{Action, count_largest_group},
|
||||
widgets::KeybindingsWidget,
|
||||
};
|
||||
use clap::ValueEnum;
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::{Alignment, Constraint, Layout, Rect},
|
||||
style::Stylize,
|
||||
text::Line,
|
||||
widgets::{Block, Borders, Paragraph, Widget},
|
||||
buffer::Buffer,
|
||||
layout::{Alignment, Constraint, Layout, Rect},
|
||||
style::Stylize,
|
||||
text::Line,
|
||||
widgets::{Block, Borders, Paragraph, Widget},
|
||||
};
|
||||
|
||||
const ACTIONS: &[Action] = &[
|
||||
Action::Up,
|
||||
Action::Down,
|
||||
Action::Space,
|
||||
Action::VolumeUp,
|
||||
Action::VolumeDown,
|
||||
Action::Mute,
|
||||
Action::Quit,
|
||||
Action::Quit2,
|
||||
Action::Up,
|
||||
Action::Down,
|
||||
Action::Space,
|
||||
Action::VolumeUp,
|
||||
Action::VolumeDown,
|
||||
Action::Mute,
|
||||
Action::Quit,
|
||||
Action::Quit2,
|
||||
];
|
||||
|
||||
fn main_menu_layout(area: Rect) -> [Rect; 2] {
|
||||
Layout::vertical([
|
||||
Constraint::Fill(1),
|
||||
Constraint::Length(count_largest_group(&ACTIONS) + 2),
|
||||
])
|
||||
.areas(area)
|
||||
Layout::vertical([
|
||||
Constraint::Fill(1),
|
||||
Constraint::Length(count_largest_group(&ACTIONS) + 2),
|
||||
])
|
||||
.areas(area)
|
||||
}
|
||||
|
||||
pub fn main_menu_view(app: &App, area: Rect, buf: &mut Buffer) {
|
||||
let Some(states) = app.states() else { return };
|
||||
let Some(states) = app.states_ref() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let [main_menu_area, keybindings_area] = main_menu_layout(area);
|
||||
let [main_menu_area, keybindings_area] = main_menu_layout(area);
|
||||
|
||||
Block::new()
|
||||
.borders(Borders::LEFT | Borders::TOP | Borders::RIGHT)
|
||||
.render(main_menu_area, buf);
|
||||
Block::new()
|
||||
.borders(Borders::LEFT | Borders::TOP | Borders::RIGHT)
|
||||
.render(main_menu_area, buf);
|
||||
|
||||
let main_menu_areas: Vec<Rect> = main_menu_area.layout_vec(&Layout::vertical([
|
||||
Constraint::Percentage(50),
|
||||
Constraint::Percentage(50),
|
||||
]));
|
||||
let main_menu_areas: Vec<Rect> = main_menu_area.layout_vec(&Layout::vertical([
|
||||
Constraint::Percentage(50),
|
||||
Constraint::Percentage(50),
|
||||
]));
|
||||
|
||||
{
|
||||
let title_area: Rect = main_menu_areas[0].centered_vertically(Constraint::Percentage(50));
|
||||
{
|
||||
let title_area: Rect = main_menu_areas[0].centered_vertically(Constraint::Percentage(50));
|
||||
|
||||
let title_text: String = vec![
|
||||
r" __ __ _ _____ _ ",
|
||||
r"/ / /\ \ \__ _ _ __ (_)_ __ /__ \_ _ _ __ _ __ ___| |___ ",
|
||||
r"\ \/ \/ / _` | '__| | | '_ \ / /\/ | | | '_ \| '_ \ / _ \ / __|",
|
||||
r" \ /\ / (_| | | | | | | | / / | |_| | | | | | | | __/ \__ \",
|
||||
r" \/ \/ \__,_|_| |_|_| |_| \/ \__,_|_| |_|_| |_|\___|_|___/",
|
||||
]
|
||||
.join("\n");
|
||||
let title_text: String = vec![
|
||||
r" __ __ _ _____ _ ",
|
||||
r"/ / /\ \ \__ _ _ __ (_)_ __ /__ \_ _ _ __ _ __ ___| |___ ",
|
||||
r"\ \/ \/ / _` | '__| | | '_ \ / /\/ | | | '_ \| '_ \ / _ \ / __|",
|
||||
r" \ /\ / (_| | | | | | | | / / | |_| | | | | | | | __/ \__ \",
|
||||
r" \/ \/ \__,_|_| |_|_| |_| \/ \__,_|_| |_|_| |_|\___|_|___/",
|
||||
]
|
||||
.join("\n");
|
||||
|
||||
Paragraph::new(title_text)
|
||||
.alignment(Alignment::Center)
|
||||
.yellow()
|
||||
.block(Block::new().gray().borders(Borders::LEFT | Borders::RIGHT))
|
||||
.render(title_area, buf);
|
||||
}
|
||||
Paragraph::new(title_text)
|
||||
.alignment(Alignment::Center)
|
||||
.yellow()
|
||||
.block(Block::new().gray().borders(Borders::LEFT | Borders::RIGHT))
|
||||
.render(title_area, buf);
|
||||
}
|
||||
|
||||
{
|
||||
let options_area: Rect = main_menu_areas[1];
|
||||
{
|
||||
let options_area: Rect = main_menu_areas[1];
|
||||
|
||||
let lines: Vec<Line<'_>> = View::value_variants()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, v)| v != &&View::MainMenu)
|
||||
.map(|(i, view)| {
|
||||
let view_string: String = main_menu_option_helper(format!("{:?}", view));
|
||||
let lines: Vec<Line<'_>> = View::value_variants()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, v)| v != &&View::MainMenu)
|
||||
.map(|(i, view)| {
|
||||
let view_string: String = main_menu_option_helper(format!("{:?}", view));
|
||||
|
||||
let styled: Line<'_> = if states.main_menu.selected_view == i {
|
||||
Line::from_iter(["> ".cyan(), view_string.yellow()]).yellow()
|
||||
} else {
|
||||
Line::from(format!(" {view_string}")).white()
|
||||
};
|
||||
let styled: Line<'_> = if states.main_menu.selected_view == i {
|
||||
Line::from_iter(["> ".cyan(), view_string.yellow()]).yellow()
|
||||
} else {
|
||||
Line::from(format!(" {view_string}")).white()
|
||||
};
|
||||
|
||||
styled
|
||||
})
|
||||
.collect();
|
||||
styled
|
||||
})
|
||||
.collect();
|
||||
|
||||
Paragraph::new(lines)
|
||||
.alignment(Alignment::Center)
|
||||
.green()
|
||||
.block(Block::new().gray().borders(Borders::LEFT | Borders::RIGHT))
|
||||
.render(options_area, buf);
|
||||
}
|
||||
Paragraph::new(lines)
|
||||
.alignment(Alignment::Center)
|
||||
.green()
|
||||
.block(Block::new().gray().borders(Borders::LEFT | Borders::RIGHT))
|
||||
.render(options_area, buf);
|
||||
}
|
||||
|
||||
{
|
||||
KeybindingsWidget::new(ACTIONS, keybindings_area.height).render(keybindings_area, buf);
|
||||
}
|
||||
{
|
||||
KeybindingsWidget::new(ACTIONS, keybindings_area.height).render(keybindings_area, buf);
|
||||
}
|
||||
}
|
||||
+131
-123
@@ -1,145 +1,153 @@
|
||||
use crate::app::{
|
||||
App,
|
||||
helpers::block_title_helper,
|
||||
keybindings::{Action, count_largest_group},
|
||||
states::skirmish_states::BoardState,
|
||||
widgets::{BoardWidget, KeybindingsWidget, SidePanelWidget},
|
||||
App,
|
||||
helpers::block_title_helper,
|
||||
keybindings::{Action, count_largest_group},
|
||||
states::skirmish_states::BoardState,
|
||||
widgets::{BoardWidget, KeybindingsWidget, SidePanelWidget},
|
||||
};
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::{Alignment, Constraint, Layout, Margin, Rect},
|
||||
style::{Color, Stylize},
|
||||
text::Line,
|
||||
widgets::{Block, Borders, Padding, Paragraph, Widget},
|
||||
buffer::Buffer,
|
||||
layout::{Alignment, Constraint, Layout, Margin, Rect},
|
||||
style::{Color, Stylize},
|
||||
text::Line,
|
||||
widgets::{Block, Borders, Padding, Paragraph, Widget},
|
||||
};
|
||||
|
||||
const ACTIONS: &[Action] = &[
|
||||
Action::Up,
|
||||
Action::Down,
|
||||
Action::Left,
|
||||
Action::Right,
|
||||
Action::ScrollUp,
|
||||
Action::ScrollDown,
|
||||
Action::ScrollLeft,
|
||||
Action::ScrollRight,
|
||||
Action::ZoomIn,
|
||||
Action::ZoomOut,
|
||||
Action::VolumeUp,
|
||||
Action::VolumeDown,
|
||||
Action::Space,
|
||||
Action::Backspace,
|
||||
Action::Delete,
|
||||
Action::Mute,
|
||||
Action::Tab,
|
||||
Action::Quit,
|
||||
Action::Quit2,
|
||||
Action::Esc,
|
||||
Action::Up,
|
||||
Action::Down,
|
||||
Action::Left,
|
||||
Action::Right,
|
||||
Action::ScrollUp,
|
||||
Action::ScrollDown,
|
||||
Action::ScrollLeft,
|
||||
Action::ScrollRight,
|
||||
Action::ZoomIn,
|
||||
Action::ZoomOut,
|
||||
Action::VolumeUp,
|
||||
Action::VolumeDown,
|
||||
Action::Space,
|
||||
Action::Backspace,
|
||||
Action::Delete,
|
||||
Action::Mute,
|
||||
Action::Tab,
|
||||
Action::Quit,
|
||||
Action::Quit2,
|
||||
Action::Esc,
|
||||
];
|
||||
|
||||
pub fn skirmish_layout(area: Rect) -> [Rect; 3] {
|
||||
Layout::vertical([
|
||||
Constraint::Length(4),
|
||||
Constraint::Fill(1),
|
||||
Constraint::Length(count_largest_group(&ACTIONS) + 2),
|
||||
])
|
||||
.areas(area)
|
||||
Layout::vertical([
|
||||
Constraint::Length(4),
|
||||
Constraint::Fill(1),
|
||||
Constraint::Length(count_largest_group(&ACTIONS) + 2),
|
||||
])
|
||||
.areas(area)
|
||||
}
|
||||
|
||||
pub fn skirmish_main_area_layout(area: Rect, side_panel: bool) -> [Rect; 2] {
|
||||
let constraint: [Constraint; 2] = if side_panel {
|
||||
[Constraint::Percentage(80), Constraint::Percentage(20)]
|
||||
} else {
|
||||
[Constraint::Percentage(100), Constraint::Percentage(0)]
|
||||
};
|
||||
let constraint: [Constraint; 2] = if side_panel {
|
||||
[Constraint::Fill(1), Constraint::Length(39)]
|
||||
} else {
|
||||
[Constraint::Percentage(100), Constraint::Percentage(0)]
|
||||
};
|
||||
|
||||
Layout::horizontal(constraint).areas(area)
|
||||
Layout::horizontal(constraint).areas(area)
|
||||
}
|
||||
|
||||
pub fn skirmish_view(app: &App, area: Rect, buf: &mut Buffer) {
|
||||
let Some(states) = app.states() else { return };
|
||||
let board: &BoardState = &states.skirmish.board;
|
||||
let Some(states) = app.states_ref() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let [title_area, main_area, keybindings_area] = skirmish_layout(area);
|
||||
let board: &BoardState = &states.skirmish.board;
|
||||
|
||||
{
|
||||
let lines: Vec<Line<'_>> = Vec::from_iter([
|
||||
Line::raw("War in Tunnels").yellow(),
|
||||
Line::from_iter([
|
||||
format!("Wood: {} (+{}) | ", states.settings.starting_wood, 5),
|
||||
format!("Iron: {} (+{}) | ", states.settings.starting_iron, 1),
|
||||
format!("Supply Limit: {}/{} | ", 10, states.settings.supply_limit),
|
||||
format!(
|
||||
"Skills points: {} ({}/{}) | ",
|
||||
1, 20, states.settings.skill_points_limit
|
||||
),
|
||||
format!("Perk Deck: {}/9 | ", 5),
|
||||
format!("Tick: {}", states.skirmish.turn_counter),
|
||||
]),
|
||||
]);
|
||||
let [title_area, main_area, keybindings_area] = skirmish_layout(area);
|
||||
|
||||
Paragraph::new(lines)
|
||||
.alignment(Alignment::Left)
|
||||
.block(
|
||||
Block::new()
|
||||
.gray()
|
||||
.padding(Padding {
|
||||
left: 1,
|
||||
right: 1,
|
||||
top: 0,
|
||||
bottom: 1,
|
||||
})
|
||||
.borders(Borders::LEFT | Borders::TOP | Borders::RIGHT),
|
||||
)
|
||||
.render(title_area, buf);
|
||||
{
|
||||
let lines: Vec<Line<'_>> = Vec::from_iter([
|
||||
Line::raw("War in Tunnels").yellow(),
|
||||
Line::from_iter([
|
||||
format!("Wood: {} (+{}) | ", states.settings.starting_wood, 5),
|
||||
format!("Iron: {} (+{}) | ", states.settings.starting_iron, 1),
|
||||
format!("Supply Limit: {}/{} | ", 10, states.settings.supply_limit),
|
||||
format!(
|
||||
"Skills points: {} ({}/{}) | ",
|
||||
1, 20, states.settings.skill_points_limit
|
||||
),
|
||||
format!("Perk Deck: {}/9 | ", 5),
|
||||
format!("Tick: {}", states.skirmish.turn_counter),
|
||||
]),
|
||||
]);
|
||||
|
||||
Paragraph::new(lines)
|
||||
.alignment(Alignment::Left)
|
||||
.block(
|
||||
Block::new()
|
||||
.gray()
|
||||
.padding(Padding {
|
||||
left: 1,
|
||||
right: 1,
|
||||
top: 0,
|
||||
bottom: 1,
|
||||
})
|
||||
.borders(Borders::LEFT | Borders::TOP | Borders::RIGHT),
|
||||
)
|
||||
.render(title_area, buf);
|
||||
}
|
||||
|
||||
{
|
||||
let main_block: Block = Block::new()
|
||||
.gray()
|
||||
.title(block_title_helper(
|
||||
&[
|
||||
("Skirmish".to_string(), Color::Magenta),
|
||||
("Map".to_string(), Color::Green),
|
||||
],
|
||||
Some(" - "),
|
||||
))
|
||||
.borders(Borders::LEFT | Borders::TOP | Borders::RIGHT);
|
||||
|
||||
let main_inner_area: Rect = main_block.inner(main_area);
|
||||
main_block.render(main_area, buf);
|
||||
|
||||
let [board_area, side_panel_area] =
|
||||
skirmish_main_area_layout(main_inner_area, states.skirmish.side_panel);
|
||||
|
||||
let cells_area: Rect = board_area.inner(Margin {
|
||||
horizontal: 1,
|
||||
vertical: 1,
|
||||
});
|
||||
// .centered(
|
||||
// Constraint::Length(
|
||||
// (board.cell_width * board.cols) as u16,
|
||||
// ),
|
||||
// Constraint::Length(
|
||||
// (board.cell_height * board.rows) as u16,
|
||||
// ),
|
||||
// );
|
||||
|
||||
BoardWidget::new(&board).render(cells_area, buf);
|
||||
|
||||
if states.skirmish.side_panel {
|
||||
let (row, col) = board.get_focused_cell().get_coords();
|
||||
let mut is_unit_marked: bool = false;
|
||||
|
||||
if let Some((first_row, first_col)) = board.marked_cells.marked_cells.get(0) {
|
||||
is_unit_marked = first_row == &row && first_col == &col;
|
||||
}
|
||||
|
||||
SidePanelWidget::new(
|
||||
(row, col),
|
||||
&board.get_ref_cell(row, col).get_structure(),
|
||||
&board.get_ref_cell(row, col).get_ref_option_unit(),
|
||||
is_unit_marked,
|
||||
)
|
||||
.render(side_panel_area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
let main_block: Block = Block::new()
|
||||
.gray()
|
||||
.title(block_title_helper(
|
||||
&[
|
||||
("Skirmish".to_string(), Color::Magenta),
|
||||
("Map".to_string(), Color::Green),
|
||||
],
|
||||
Some(" - "),
|
||||
))
|
||||
.borders(Borders::LEFT | Borders::TOP | Borders::RIGHT);
|
||||
|
||||
let main_inner_area: Rect = main_block.inner(main_area);
|
||||
main_block.render(main_area, buf);
|
||||
|
||||
let [board_area, side_panel_area] =
|
||||
skirmish_main_area_layout(main_inner_area, states.skirmish.side_panel);
|
||||
|
||||
let cells_area: Rect = board_area.inner(Margin {
|
||||
horizontal: 1,
|
||||
vertical: 1,
|
||||
});
|
||||
// .centered(
|
||||
// Constraint::Length(
|
||||
// (board.cell_width * board.cols) as u16,
|
||||
// ),
|
||||
// Constraint::Length(
|
||||
// (board.cell_height * board.rows) as u16,
|
||||
// ),
|
||||
// );
|
||||
|
||||
BoardWidget::new(&board).render(cells_area, buf);
|
||||
|
||||
if states.skirmish.side_panel {
|
||||
let row: usize = board.get_focused_cell().get_row();
|
||||
let col: usize = board.get_focused_cell().get_col();
|
||||
|
||||
SidePanelWidget::new(
|
||||
(row, col),
|
||||
&board.get_ref_cell(row, col).get_structure(),
|
||||
&board.get_ref_cell(row, col).get_option_unit(),
|
||||
)
|
||||
.render(side_panel_area, buf);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
KeybindingsWidget::new(ACTIONS, keybindings_area.height).render(keybindings_area, buf);
|
||||
}
|
||||
{
|
||||
KeybindingsWidget::new(ACTIONS, keybindings_area.height).render(keybindings_area, buf);
|
||||
}
|
||||
}
|
||||
+30
-29
@@ -1,45 +1,46 @@
|
||||
use crate::app::states::skirmish_states::BoardState;
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::{Constraint, Layout, Rect},
|
||||
widgets::Widget,
|
||||
buffer::Buffer,
|
||||
layout::{Constraint, Layout, Rect},
|
||||
widgets::Widget,
|
||||
};
|
||||
use std::rc::Rc;
|
||||
|
||||
pub struct BoardWidget<'a> {
|
||||
state: &'a BoardState,
|
||||
state: &'a BoardState,
|
||||
}
|
||||
|
||||
impl<'a> BoardWidget<'a> {
|
||||
pub fn new(state: &'a BoardState) -> Self {
|
||||
Self { state }
|
||||
}
|
||||
pub fn new(state: &'a BoardState) -> Self {
|
||||
Self { state }
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for BoardWidget<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let horizontal: Rc<[Rect]> =
|
||||
Layout::horizontal(vec![
|
||||
Constraint::Length(self.state.cell_width as u16);
|
||||
self.state.cols
|
||||
])
|
||||
.split(area);
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let horizontal: Rc<[Rect]> =
|
||||
Layout::horizontal(vec![
|
||||
Constraint::Length(self.state.cell_width as u16);
|
||||
self.state.cols
|
||||
])
|
||||
.split(area);
|
||||
|
||||
for (col_idx, col_area) in horizontal.iter().enumerate() {
|
||||
let vertical: Rc<[Rect]> =
|
||||
Layout::vertical(vec![
|
||||
Constraint::Length(self.state.cell_height as u16);
|
||||
self.state.rows
|
||||
])
|
||||
.split(*col_area);
|
||||
for (col_idx, col_area) in horizontal.iter().enumerate() {
|
||||
let vertical: Rc<[Rect]> =
|
||||
Layout::vertical(vec![
|
||||
Constraint::Length(self.state.cell_height as u16);
|
||||
self.state.rows
|
||||
])
|
||||
.split(*col_area);
|
||||
|
||||
for (row_idx, cell_area) in vertical.iter().enumerate() {
|
||||
self.state
|
||||
.cells
|
||||
.get(row_idx + self.state.vertical_offset.get_value())
|
||||
.and_then(|rows| rows.get(col_idx + self.state.horizontal_offset.get_value()))
|
||||
.map(|cell| cell.render(*cell_area, buf));
|
||||
}
|
||||
}
|
||||
for (row_idx, cell_area) in vertical.iter().enumerate() {
|
||||
self
|
||||
.state
|
||||
.cells
|
||||
.get(row_idx + self.state.vertical_offset.get_value())
|
||||
.and_then(|rows| rows.get(col_idx + self.state.horizontal_offset.get_value()))
|
||||
.map(|cell| cell.clone().render(*cell_area, buf));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+140
-111
@@ -1,133 +1,162 @@
|
||||
use crate::app::states::skirmish_states::{
|
||||
ZoomLevel,
|
||||
structures::{Stone, Structure, Structures},
|
||||
units::{Unit, Units},
|
||||
ZoomLevel,
|
||||
structures::{Stone, Structure, Structures},
|
||||
tasks::Tasks,
|
||||
units::{Unit, Units},
|
||||
};
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::{Alignment, Rect},
|
||||
style::{Color, Style, Stylize},
|
||||
text::{Line, Span, ToSpan},
|
||||
widgets::{Block, BorderType, Borders, Paragraph, Widget},
|
||||
buffer::Buffer,
|
||||
layout::{Alignment, Rect},
|
||||
style::{Color, Style, Stylize},
|
||||
text::{Line, Span, ToSpan},
|
||||
widgets::{Block, BorderType, Borders, Paragraph, Widget},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct CellWidget {
|
||||
row: usize,
|
||||
col: usize,
|
||||
selected: bool,
|
||||
zoom_level: ZoomLevel,
|
||||
structure: Structures,
|
||||
marked: bool,
|
||||
unit: Option<Units>,
|
||||
row: usize,
|
||||
col: usize,
|
||||
selected: bool,
|
||||
zoom_level: ZoomLevel,
|
||||
structure: Structures,
|
||||
marked: bool,
|
||||
unit: Option<Units>,
|
||||
}
|
||||
|
||||
impl CellWidget {
|
||||
pub fn new(
|
||||
row: usize,
|
||||
col: usize,
|
||||
zoom_level: ZoomLevel,
|
||||
selected: bool,
|
||||
structure: Structures,
|
||||
unit: Option<Units>,
|
||||
) -> Self {
|
||||
Self {
|
||||
row,
|
||||
col,
|
||||
selected,
|
||||
zoom_level,
|
||||
marked: false,
|
||||
structure,
|
||||
unit,
|
||||
}
|
||||
pub fn new(
|
||||
row: usize,
|
||||
col: usize,
|
||||
zoom_level: ZoomLevel,
|
||||
selected: bool,
|
||||
structure: Structures,
|
||||
unit: Option<Units>,
|
||||
) -> Self {
|
||||
Self {
|
||||
row,
|
||||
col,
|
||||
selected,
|
||||
zoom_level,
|
||||
marked: false,
|
||||
structure,
|
||||
unit,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_selected(&mut self, selected: bool) -> &mut Self {
|
||||
self.selected = selected;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn set_zoom_level(&mut self, zoom_level: ZoomLevel) -> &mut Self {
|
||||
self.zoom_level = zoom_level;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn get_structure(&self) -> Structures {
|
||||
self.structure
|
||||
}
|
||||
|
||||
pub fn get_option_unit(&self) -> Option<Units> {
|
||||
self.unit.clone()
|
||||
}
|
||||
|
||||
pub fn get_ref_option_unit(&self) -> &Option<Units> {
|
||||
&self.unit
|
||||
}
|
||||
|
||||
pub fn get_mut_option_unit(&mut self) -> &mut Option<Units> {
|
||||
&mut self.unit
|
||||
}
|
||||
|
||||
pub fn set_structure(&mut self, structure: Structures) -> &mut Self {
|
||||
self.structure = structure;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn get_marked(&self) -> bool {
|
||||
self.marked
|
||||
}
|
||||
|
||||
pub fn set_marked(&mut self, marked: bool) -> &mut Self {
|
||||
self.marked = marked;
|
||||
self
|
||||
}
|
||||
|
||||
fn col_to_letters(&self) -> String {
|
||||
let mut col: usize = self.col + 1;
|
||||
let mut letters: Vec<char> = Vec::new();
|
||||
|
||||
while col > 0 {
|
||||
letters.push((b'A' + ((col - 1) % 26) as u8) as char);
|
||||
col = (col - 1) / 26;
|
||||
}
|
||||
|
||||
pub fn set_selected(&mut self, selected: bool) -> &mut Self {
|
||||
self.selected = selected;
|
||||
self
|
||||
letters.iter().rev().collect()
|
||||
}
|
||||
|
||||
fn display_coords(&self) -> Span<'_> {
|
||||
if self.selected || self.structure != Structures::Stone(Stone::new()) || self.marked {
|
||||
format!("{}{}", self.col_to_letters(), self.row).green()
|
||||
} else {
|
||||
"".to_span()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_zoom_level(&mut self, zoom_level: ZoomLevel) -> &mut Self {
|
||||
self.zoom_level = zoom_level;
|
||||
self
|
||||
fn fg_color(&self) -> Color {
|
||||
if self.marked && self.selected {
|
||||
Color::Magenta
|
||||
} else if self.marked && !self.selected {
|
||||
Color::LightMagenta
|
||||
} else if !self.marked && self.selected {
|
||||
Color::LightYellow
|
||||
} else {
|
||||
self.structure.get_color()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_structure(&self) -> Structures {
|
||||
self.structure
|
||||
fn get_text_area(&self) -> Vec<Line<'_>> {
|
||||
let mut lines: Vec<Line<'_>> = self.zoom_level.get_cell_text_area(
|
||||
self.structure.get_tag(),
|
||||
self.structure.get_level(),
|
||||
self
|
||||
.unit
|
||||
.get_tag()
|
||||
.to_string()
|
||||
.fg(self.unit.get_owner().get_color()),
|
||||
);
|
||||
if let Structures::Stone(stone) = self.structure
|
||||
&& stone.get_durability() < stone.get_max_durability()
|
||||
{
|
||||
let percent: u32 =
|
||||
u32::from(stone.get_durability()) * 100 / u32::from(stone.get_max_durability());
|
||||
let middle = lines.len() / 2;
|
||||
lines[middle] = Line::from(format!("{percent}%")).cyan();
|
||||
}
|
||||
|
||||
pub fn get_option_unit(&self) -> Option<Units> {
|
||||
self.unit
|
||||
if let Some(unit) = &self.unit
|
||||
&& let Some(Tasks::Moving(task)) = unit.get_ref_tasks().front()
|
||||
&& let Some(percent) = task.get_movement_progress()
|
||||
{
|
||||
let middle = lines.len() / 2;
|
||||
lines[middle] = Line::from(format!("{percent}%")).fg(unit.get_owner().get_color());
|
||||
}
|
||||
lines
|
||||
}
|
||||
|
||||
pub fn set_structure(&mut self, structure: Structures) -> &mut Self {
|
||||
self.structure = structure;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn get_marked(&self) -> bool {
|
||||
self.marked
|
||||
}
|
||||
|
||||
pub fn set_marked(&mut self, marked: bool) -> &mut Self {
|
||||
self.marked = marked;
|
||||
self
|
||||
}
|
||||
|
||||
fn col_to_letters(&self) -> String {
|
||||
let mut col: usize = self.col + 1;
|
||||
let mut letters: Vec<char> = Vec::new();
|
||||
|
||||
while col > 0 {
|
||||
letters.push((b'A' + ((col - 1) % 26) as u8) as char);
|
||||
col = (col - 1) / 26;
|
||||
}
|
||||
|
||||
letters.iter().rev().collect()
|
||||
}
|
||||
|
||||
fn display_coords(&self) -> Span<'_> {
|
||||
if self.selected || self.structure != Structures::Stone(Stone::new()) || self.marked {
|
||||
format!("{}{}", self.col_to_letters(), self.row).green()
|
||||
} else {
|
||||
"".to_span()
|
||||
}
|
||||
}
|
||||
|
||||
fn fg_color(&self) -> Color {
|
||||
if self.marked && self.selected {
|
||||
Color::Magenta
|
||||
} else if self.marked && !self.selected {
|
||||
Color::LightMagenta
|
||||
} else if !self.marked && self.selected {
|
||||
Color::LightYellow
|
||||
} else {
|
||||
self.structure.get_color()
|
||||
}
|
||||
}
|
||||
|
||||
fn get_text_area(&self) -> Vec<Line<'_>> {
|
||||
self.zoom_level.get_cell_text_area(
|
||||
self.structure.get_tag(),
|
||||
self.structure.get_level(),
|
||||
self.unit.get_tag(),
|
||||
)
|
||||
}
|
||||
|
||||
fn get_block(&self) -> Block<'_> {
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.style(Style::default().fg(self.fg_color()))
|
||||
.title(self.display_coords())
|
||||
.border_type(BorderType::Rounded)
|
||||
}
|
||||
fn get_block(&self) -> Block<'_> {
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.style(Style::default().fg(self.fg_color()))
|
||||
.title(self.display_coords())
|
||||
.border_type(BorderType::Rounded)
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for CellWidget {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
Paragraph::new(self.get_text_area())
|
||||
.alignment(Alignment::Center)
|
||||
.block(self.get_block())
|
||||
.render(area, buf);
|
||||
}
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
Paragraph::new(self.get_text_area())
|
||||
.alignment(Alignment::Center)
|
||||
.block(self.get_block())
|
||||
.render(area, buf);
|
||||
}
|
||||
}
|
||||
+103
-108
@@ -1,14 +1,14 @@
|
||||
use crate::app::{
|
||||
helpers::block_single_title_helper,
|
||||
keybindings::{Action, Group, KeyBinding, binding_for},
|
||||
helpers::block_single_title_helper,
|
||||
keybindings::{Action, Group, KeyBinding, binding_for},
|
||||
};
|
||||
use clap::ValueEnum;
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::{Alignment, Constraint, Layout, Rect},
|
||||
style::{Color, Stylize},
|
||||
text::Line,
|
||||
widgets::{Block, Borders, Padding, Paragraph, Widget},
|
||||
buffer::Buffer,
|
||||
layout::{Alignment, Constraint, Layout, Rect},
|
||||
style::{Color, Stylize},
|
||||
text::Line,
|
||||
widgets::{Block, Borders, Padding, Paragraph, Widget},
|
||||
};
|
||||
use std::collections::HashSet;
|
||||
|
||||
@@ -18,123 +18,118 @@ use std::collections::HashSet;
|
||||
/// logical group of keybindings (e.g. navigation, editing, etc.). The paragraphs
|
||||
/// are rendered side‑by‑side inside a bordered container.
|
||||
pub struct KeybindingsWidget {
|
||||
/// Paragraphs, one per used `Group`, containing the formatted keybinding lines.
|
||||
grouped: Vec<Paragraph<'static>>,
|
||||
/// Paragraphs, one per used `Group`, containing the formatted keybinding lines.
|
||||
grouped: Vec<Paragraph<'static>>,
|
||||
}
|
||||
|
||||
impl KeybindingsWidget {
|
||||
/// Create a new widget from a list of actions.
|
||||
///
|
||||
/// For each action, the corresponding static [`KeyBinding`] is looked up
|
||||
/// using [`binding_for`]. Only groups that have at least one binding are
|
||||
/// displayed. The resulting widget contains a paragraph for each group,
|
||||
/// formatted with the key symbol (red & bold) followed by its description.
|
||||
///
|
||||
/// # Parameters
|
||||
///
|
||||
/// * `actions` – A slice of actions for which keybindings should be shown.
|
||||
/// * `area_height` – The height of the area in which the widget will be rendered.
|
||||
/// This value is used to calculate vertical padding so that the keybinding rows
|
||||
/// are vertically centered when the widget's allocated height is larger than the
|
||||
/// number of lines needed to display the bindings.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A fully‑initialised `KeybindingsWidget` ready for rendering.
|
||||
pub fn new(actions: &[Action], area_height: u16) -> Self {
|
||||
let keybindings: Vec<Option<&'static KeyBinding>> =
|
||||
actions.iter().map(|a| binding_for(*a)).collect();
|
||||
/// Create a new widget from a list of actions.
|
||||
///
|
||||
/// For each action, the corresponding static [`KeyBinding`] is looked up
|
||||
/// using [`binding_for`]. Only groups that have at least one binding are
|
||||
/// displayed. The resulting widget contains a paragraph for each group,
|
||||
/// formatted with the key symbol (red & bold) followed by its description.
|
||||
///
|
||||
/// # Parameters
|
||||
///
|
||||
/// * `actions` – A slice of actions for which keybindings should be shown.
|
||||
/// * `area_height` – The height of the area in which the widget will be rendered.
|
||||
/// This value is used to calculate vertical padding so that the keybinding rows
|
||||
/// are vertically centered when the widget's allocated height is larger than the
|
||||
/// number of lines needed to display the bindings.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// A fully‑initialised `KeybindingsWidget` ready for rendering.
|
||||
pub fn new(actions: &[Action], area_height: u16) -> Self {
|
||||
let keybindings: Vec<Option<&'static KeyBinding>> =
|
||||
actions.iter().map(|a| binding_for(*a)).collect();
|
||||
|
||||
let used_groups: HashSet<Group> = keybindings
|
||||
.iter()
|
||||
.filter_map(|b| b.map(|binding| binding.group))
|
||||
.collect();
|
||||
let used_groups: HashSet<Group> = keybindings
|
||||
.iter()
|
||||
.filter_map(|b| b.map(|binding| binding.group))
|
||||
.collect();
|
||||
|
||||
let mut grouped_keybindings: Vec<Paragraph<'static>> = Vec::new();
|
||||
let mut grouped_keybindings: Vec<Paragraph<'static>> = Vec::new();
|
||||
|
||||
for group in Group::value_variants().iter() {
|
||||
if !used_groups.contains(group) {
|
||||
continue;
|
||||
}
|
||||
for group in Group::value_variants().iter() {
|
||||
if !used_groups.contains(group) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let grouped_lines: Vec<Line<'_>> = keybindings
|
||||
.iter()
|
||||
.filter(|b| b.as_ref().map_or(false, |kb| kb.group == *group))
|
||||
.filter_map(|b| {
|
||||
b.as_ref().map(|kb| {
|
||||
Line::from_iter([
|
||||
kb.symbol.red().bold(),
|
||||
" - ".into(),
|
||||
kb.description.into(),
|
||||
])
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
let grouped_lines: Vec<Line<'_>> = keybindings
|
||||
.iter()
|
||||
.filter(|b| b.as_ref().map_or(false, |kb| kb.group == *group))
|
||||
.filter_map(|b| {
|
||||
b.as_ref().map(|kb| {
|
||||
Line::from_iter([kb.symbol.red().bold(), " - ".into(), kb.description.into()])
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
|
||||
let height_diff: u16 =
|
||||
area_height.saturating_sub(grouped_lines.len().saturating_add(2) as u16);
|
||||
let height_diff: u16 =
|
||||
area_height.saturating_sub(grouped_lines.len().saturating_add(2) as u16);
|
||||
|
||||
let padding_top: u16 = if height_diff <= 1 {
|
||||
height_diff
|
||||
} else {
|
||||
height_diff / 2
|
||||
};
|
||||
let padding_top: u16 = if height_diff <= 1 {
|
||||
height_diff
|
||||
} else {
|
||||
height_diff / 2
|
||||
};
|
||||
|
||||
let mut block: Block<'_> = Block::default().padding(Padding::new(1, 1, padding_top, 0));
|
||||
let mut block: Block<'_> = Block::default().padding(Padding::new(1, 1, padding_top, 0));
|
||||
|
||||
if !grouped_keybindings.is_empty() {
|
||||
block = block.borders(Borders::LEFT);
|
||||
}
|
||||
if !grouped_keybindings.is_empty() {
|
||||
block = block.borders(Borders::LEFT);
|
||||
}
|
||||
|
||||
grouped_keybindings.push(
|
||||
Paragraph::new(grouped_lines)
|
||||
.alignment(Alignment::Center)
|
||||
.block(block),
|
||||
);
|
||||
}
|
||||
|
||||
Self {
|
||||
grouped: grouped_keybindings,
|
||||
}
|
||||
grouped_keybindings.push(
|
||||
Paragraph::new(grouped_lines)
|
||||
.alignment(Alignment::Center)
|
||||
.block(block),
|
||||
);
|
||||
}
|
||||
|
||||
Self {
|
||||
grouped: grouped_keybindings,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for KeybindingsWidget {
|
||||
/// Render the widget inside the given rectangular area.
|
||||
///
|
||||
/// The widget first draws a surrounding border with the title *Keybindings*.
|
||||
/// It then divides the inner area into equal horizontal slices—one per
|
||||
/// used group—and renders each group's paragraph into its slice.
|
||||
///
|
||||
/// If no groups are present, the function returns early without drawing
|
||||
/// anything.
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let count: u16 = self.grouped.len() as u16;
|
||||
/// Render the widget inside the given rectangular area.
|
||||
///
|
||||
/// The widget first draws a surrounding border with the title *Keybindings*.
|
||||
/// It then divides the inner area into equal horizontal slices—one per
|
||||
/// used group—and renders each group's paragraph into its slice.
|
||||
///
|
||||
/// If no groups are present, the function returns early without drawing
|
||||
/// anything.
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let count: u16 = self.grouped.len() as u16;
|
||||
|
||||
if count == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let block: Block<'_> =
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.title(block_single_title_helper(
|
||||
"Keybindings".to_string(),
|
||||
Color::Magenta,
|
||||
));
|
||||
|
||||
let inner: Rect = block.inner(area);
|
||||
|
||||
block.render(area, buf);
|
||||
|
||||
let base: u16 = if count == 0 { 0 } else { 100u16 / count };
|
||||
|
||||
let constraints: Vec<Constraint> = vec![Constraint::Percentage(base); count as usize];
|
||||
|
||||
let chunks: Vec<Rect> = Layout::horizontal(constraints).split(inner).to_vec();
|
||||
|
||||
for (paragraph, chunk) in self.grouped.into_iter().zip(chunks.into_iter()) {
|
||||
paragraph.render(chunk, buf);
|
||||
}
|
||||
if count == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let block: Block<'_> = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.title(block_single_title_helper(
|
||||
"Keybindings".to_string(),
|
||||
Color::Magenta,
|
||||
));
|
||||
|
||||
let inner: Rect = block.inner(area);
|
||||
|
||||
block.render(area, buf);
|
||||
|
||||
let base: u16 = if count == 0 { 0 } else { 100u16 / count };
|
||||
|
||||
let constraints: Vec<Constraint> = vec![Constraint::Percentage(base); count as usize];
|
||||
|
||||
let chunks: Vec<Rect> = Layout::horizontal(constraints).split(inner).to_vec();
|
||||
|
||||
for (paragraph, chunk) in self.grouped.into_iter().zip(chunks.into_iter()) {
|
||||
paragraph.render(chunk, buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
+86
-112
@@ -1,134 +1,108 @@
|
||||
use crate::app::{
|
||||
helpers::block_single_title_helper,
|
||||
states::skirmish_states::{
|
||||
structures::{Structure, Structures},
|
||||
units::{Unit, Units},
|
||||
},
|
||||
helpers::block_single_title_helper,
|
||||
states::skirmish_states::{
|
||||
structures::{Structure, Structures},
|
||||
units::{Unit, Units},
|
||||
},
|
||||
};
|
||||
use ratatui::{
|
||||
buffer::Buffer,
|
||||
layout::{Alignment, Constraint, Layout, Margin, Rect},
|
||||
style::{Color, Stylize},
|
||||
text::{Line, Span},
|
||||
widgets::{Block, BorderType, Borders, Padding, Paragraph, Widget},
|
||||
buffer::Buffer,
|
||||
layout::{Alignment, Constraint, Layout, Margin, Rect},
|
||||
style::Color,
|
||||
widgets::{Block, BorderType, Borders, Padding, Paragraph, Widget},
|
||||
};
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct SidePanelWidget<'a> {
|
||||
coords: (usize, usize),
|
||||
structure: &'a Structures,
|
||||
unit: &'a Option<Units>,
|
||||
coords: (usize, usize),
|
||||
structure: &'a Structures,
|
||||
unit: &'a Option<Units>,
|
||||
is_unit_marked: bool,
|
||||
}
|
||||
|
||||
impl<'a> SidePanelWidget<'a> {
|
||||
pub fn new(coords: (usize, usize), structure: &'a Structures, unit: &'a Option<Units>) -> Self {
|
||||
Self {
|
||||
coords,
|
||||
structure,
|
||||
unit,
|
||||
}
|
||||
pub fn new(
|
||||
coords: (usize, usize),
|
||||
structure: &'a Structures,
|
||||
unit: &'a Option<Units>,
|
||||
is_unit_marked: bool,
|
||||
) -> Self {
|
||||
Self {
|
||||
coords,
|
||||
structure,
|
||||
unit,
|
||||
is_unit_marked,
|
||||
}
|
||||
}
|
||||
|
||||
fn col_to_letters(&self) -> String {
|
||||
let mut col: usize = self.coords.1 + 1;
|
||||
let mut letters: Vec<char> = Vec::new();
|
||||
|
||||
while col > 0 {
|
||||
letters.push((b'A' + ((col - 1) % 26) as u8) as char);
|
||||
col = (col - 1) / 26;
|
||||
}
|
||||
|
||||
fn col_to_letters(&self) -> String {
|
||||
let mut col: usize = self.coords.1 + 1;
|
||||
let mut letters: Vec<char> = Vec::new();
|
||||
letters.iter().rev().collect()
|
||||
}
|
||||
|
||||
while col > 0 {
|
||||
letters.push((b'A' + ((col - 1) % 26) as u8) as char);
|
||||
col = (col - 1) / 26;
|
||||
}
|
||||
fn get_outer_block(&self) -> Block<'_> {
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Double)
|
||||
.title(block_single_title_helper(
|
||||
format!("{}{}", self.col_to_letters(), self.coords.0),
|
||||
Color::Yellow,
|
||||
))
|
||||
.title_alignment(Alignment::Center)
|
||||
}
|
||||
|
||||
letters.iter().rev().collect()
|
||||
}
|
||||
|
||||
fn get_outer_block(&self) -> Block<'_> {
|
||||
Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_type(BorderType::Double)
|
||||
.title(block_single_title_helper(
|
||||
format!("{}{}", self.col_to_letters(), self.coords.0),
|
||||
Color::Yellow,
|
||||
))
|
||||
.title_alignment(Alignment::Center)
|
||||
}
|
||||
|
||||
fn get_inner_block<'b>(&self, title: String, color: Color) -> Block<'b> {
|
||||
Block::default()
|
||||
.borders(Borders::LEFT | Borders::TOP)
|
||||
.title(block_single_title_helper(title, color))
|
||||
.title_alignment(Alignment::Center)
|
||||
.padding(Padding::symmetric(1, 1))
|
||||
}
|
||||
|
||||
fn get_area_constraints(&self) -> [Constraint; 2] {
|
||||
if !self.unit.get_name().is_empty() {
|
||||
[Constraint::Percentage(50), Constraint::Percentage(50)]
|
||||
} else {
|
||||
[Constraint::Percentage(100), Constraint::Percentage(0)]
|
||||
}
|
||||
}
|
||||
|
||||
fn structure_text(&self) -> Vec<Line<'_>> {
|
||||
let s: &Structures = self.structure;
|
||||
let durability: u16 = s.get_durability();
|
||||
let max_durability: u16 = s.get_max_durability();
|
||||
let durability_percent: u32 = durability as u32 * 100u32 / max_durability as u32;
|
||||
let stress: u8 = s.get_stress();
|
||||
let level: char = s.get_level();
|
||||
|
||||
let mut lines: Vec<Line<'_>> = vec![
|
||||
Line::from_iter([
|
||||
"Durability: ".gray(),
|
||||
durability.to_string().cyan(),
|
||||
"/".gray(),
|
||||
max_durability.to_string().cyan(),
|
||||
" ( ".gray(),
|
||||
durability_percent.to_string().cyan(),
|
||||
"% )".gray(),
|
||||
]),
|
||||
Line::from_iter(["Stress: ".gray(), stress.to_string().cyan(), "%".gray()]),
|
||||
];
|
||||
|
||||
if level != ' ' {
|
||||
lines.push(Line::from_iter([
|
||||
"Level: ".gray(),
|
||||
level.to_string().cyan(),
|
||||
]));
|
||||
}
|
||||
|
||||
lines
|
||||
}
|
||||
|
||||
fn unit_text(&self) -> Vec<Line<'_>> {
|
||||
let u: &Option<Units> = self.unit;
|
||||
let owner: Span<'_> = u.get_owner().get_span();
|
||||
|
||||
vec![Line::from_iter(vec!["Owner: ".gray(), owner])]
|
||||
fn get_inner_block<'b>(&self, title: String, color: Color) -> Block<'b> {
|
||||
Block::default()
|
||||
.borders(Borders::TOP)
|
||||
.title(block_single_title_helper(title, color))
|
||||
.title_alignment(Alignment::Center)
|
||||
.padding(Padding::symmetric(1, 1))
|
||||
}
|
||||
|
||||
fn get_area_constraints(&self) -> [Constraint; 2] {
|
||||
if self.unit.is_unit() {
|
||||
[Constraint::Percentage(50), Constraint::Percentage(50)]
|
||||
} else {
|
||||
[Constraint::Percentage(100), Constraint::Percentage(0)]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Widget for SidePanelWidget<'_> {
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let block: Block<'_> = self.get_outer_block();
|
||||
let inner_area: Rect = block.inner(area).inner(Margin {
|
||||
horizontal: 2,
|
||||
vertical: 1,
|
||||
});
|
||||
block.render(area, buf);
|
||||
fn render(self, area: Rect, buf: &mut Buffer) {
|
||||
let block: Block<'_> = self.get_outer_block();
|
||||
let inner_area: Rect = block.inner(area).inner(Margin {
|
||||
horizontal: 2,
|
||||
vertical: 1,
|
||||
});
|
||||
block.render(area, buf);
|
||||
|
||||
let [structure_area, unit_area] =
|
||||
Layout::vertical(self.get_area_constraints()).areas(inner_area);
|
||||
let [structure_area, unit_area] =
|
||||
Layout::vertical(self.get_area_constraints()).areas(inner_area);
|
||||
|
||||
Paragraph::new(self.structure_text())
|
||||
.alignment(Alignment::Left)
|
||||
.block(self.get_inner_block(self.structure.get_name().to_string(), Color::Cyan))
|
||||
.render(structure_area, buf);
|
||||
Paragraph::new(self.structure.text())
|
||||
.alignment(Alignment::Left)
|
||||
.block(self.get_inner_block(self.structure.get_name().to_string(), Color::Cyan))
|
||||
.render(structure_area, buf);
|
||||
|
||||
if !self.unit.get_name().is_empty() {
|
||||
Paragraph::new(self.unit_text())
|
||||
.alignment(Alignment::Left)
|
||||
.block(self.get_inner_block(self.unit.get_name().to_string(), Color::Green))
|
||||
.render(unit_area, buf);
|
||||
}
|
||||
if self.unit.is_unit() {
|
||||
let mut unit_name: String = self.unit.get_name().to_string();
|
||||
|
||||
if self.is_unit_marked {
|
||||
unit_name = format!("> {} <", unit_name);
|
||||
}
|
||||
|
||||
Paragraph::new(self.unit.text())
|
||||
.alignment(Alignment::Left)
|
||||
.block(self.get_inner_block(unit_name, Color::Green))
|
||||
.render(unit_area, buf);
|
||||
}
|
||||
}
|
||||
}
|
||||
+106
-82
@@ -1,13 +1,13 @@
|
||||
use crate::{
|
||||
app::{
|
||||
states::{
|
||||
PerkDecks,
|
||||
skirmish_states::{GameMode, ZoomLevel},
|
||||
},
|
||||
threads::Soundtrack,
|
||||
view::View,
|
||||
app::{
|
||||
states::{
|
||||
PerkDecks,
|
||||
skirmish_states::{GameMode, ZoomLevel},
|
||||
},
|
||||
logs::init_logger,
|
||||
threads::Soundtrack,
|
||||
view::View,
|
||||
},
|
||||
logs::init_logger,
|
||||
};
|
||||
use clap::{Error, Parser, error::ErrorKind, value_parser};
|
||||
use std::num::ParseFloatError;
|
||||
@@ -21,150 +21,174 @@ use std::num::ParseFloatError;
|
||||
#[derive(Parser, Debug, Clone)]
|
||||
#[command(version, about = "War in Tunnels", long_about = "War in Tunnels")]
|
||||
pub struct Cli {
|
||||
/// The initial view/window to display.
|
||||
#[arg(
|
||||
/// The initial view/window to display.
|
||||
#[arg(
|
||||
long,
|
||||
short,
|
||||
help = "Default window",
|
||||
value_name = "...",
|
||||
default_value_t = View::MainMenu,
|
||||
value_enum
|
||||
)]
|
||||
pub view: View,
|
||||
pub view: View,
|
||||
|
||||
/// Player name shown in the UI.
|
||||
#[arg(
|
||||
long,
|
||||
help = "Username",
|
||||
value_name = "String",
|
||||
default_value = "Player"
|
||||
)]
|
||||
pub username: String,
|
||||
/// Player name shown in the UI.
|
||||
#[arg(
|
||||
long,
|
||||
short,
|
||||
help = "Username",
|
||||
value_name = "String",
|
||||
default_value = "Player"
|
||||
)]
|
||||
pub username: String,
|
||||
|
||||
/// Game mode selection.
|
||||
#[arg(
|
||||
/// Game mode selection.
|
||||
#[arg(
|
||||
long,
|
||||
short,
|
||||
help = "Game mode",
|
||||
value_name = "...",
|
||||
default_value_t = GameMode::LastManStanding,
|
||||
value_enum
|
||||
)]
|
||||
pub game_mode: GameMode,
|
||||
pub game_mode: GameMode,
|
||||
|
||||
/// Width of the generated map (36–100 tiles).
|
||||
#[arg(
|
||||
/// Width of the generated map (36–100 tiles).
|
||||
#[arg(
|
||||
long,
|
||||
short = 'W',
|
||||
help = "Map width",
|
||||
value_name = "Positive integer [36; 108]",
|
||||
default_value = "42",
|
||||
value_parser = value_parser!(u8).range(36..=108)
|
||||
)]
|
||||
pub map_width: u8,
|
||||
pub map_width: u8,
|
||||
|
||||
/// Height of the generated map (11–50 tiles).
|
||||
#[arg(
|
||||
/// Height of the generated map (11–50 tiles).
|
||||
#[arg(
|
||||
long,
|
||||
short = 'H',
|
||||
help = "Map height",
|
||||
value_name = "Positive integer [11; 39]",
|
||||
default_value = "11",
|
||||
value_parser = value_parser!(u8).range(11..=39)
|
||||
)]
|
||||
pub map_height: u8,
|
||||
pub map_height: u8,
|
||||
|
||||
/// Zoom level used for the UI.
|
||||
#[arg(
|
||||
/// Zoom level used for the UI.
|
||||
#[arg(
|
||||
long,
|
||||
short,
|
||||
help = "Zoom level",
|
||||
value_name = "...",
|
||||
default_value_t = ZoomLevel::Default,
|
||||
value_enum
|
||||
)]
|
||||
pub zoom_level: ZoomLevel,
|
||||
pub zoom_level: ZoomLevel,
|
||||
|
||||
/// Which perk deck to use.
|
||||
#[arg(
|
||||
/// Which perk deck to use.
|
||||
#[arg(
|
||||
long,
|
||||
short,
|
||||
help = "Perk Deck",
|
||||
value_name = "...",
|
||||
default_value_t = PerkDecks::Silesian,
|
||||
value_enum
|
||||
)]
|
||||
pub perk_deck: PerkDecks,
|
||||
pub perk_deck: PerkDecks,
|
||||
|
||||
/// Starting amount of wood (1–150).
|
||||
#[arg(
|
||||
/// Starting amount of wood (1–150).
|
||||
#[arg(
|
||||
long,
|
||||
short = 'w',
|
||||
help = "Starting wood",
|
||||
value_name = "Positive integer [1; 150]",
|
||||
default_value = "50",
|
||||
value_parser = value_parser!(u16).range(1..=150)
|
||||
)]
|
||||
pub starting_wood: u16,
|
||||
pub starting_wood: u16,
|
||||
|
||||
/// Starting amount of iron (1–150).
|
||||
#[arg(
|
||||
/// Starting amount of iron (1–150).
|
||||
#[arg(
|
||||
long,
|
||||
short = 'i',
|
||||
help = "Starting iron",
|
||||
value_name = "Positive integer [1; 150]",
|
||||
default_value = "25",
|
||||
value_parser = value_parser!(u16).range(1..=150)
|
||||
)]
|
||||
pub starting_iron: u16,
|
||||
pub starting_iron: u16,
|
||||
|
||||
/// Maximum amount of units (49–149).
|
||||
#[arg(
|
||||
/// Maximum amount of units (49–149).
|
||||
#[arg(
|
||||
long,
|
||||
short = 'L',
|
||||
help = "Supply limit",
|
||||
value_name = "Positive integer [49; 149]",
|
||||
default_value = "99",
|
||||
value_parser = value_parser!(u8).range(49..=149)
|
||||
)]
|
||||
pub supply_limit: u8,
|
||||
pub supply_limit: u8,
|
||||
|
||||
/// Modifier applied to experience points (0.5–2.0).
|
||||
#[arg(
|
||||
/// Modifier applied to experience points (0.5–2.0).
|
||||
#[arg(
|
||||
long,
|
||||
short,
|
||||
help = "XP modifier",
|
||||
value_name = "Float [0.5; 2.0]",
|
||||
default_value = "1.0",
|
||||
value_parser = parse_xp
|
||||
)]
|
||||
pub xp_modifier: f32,
|
||||
pub xp_modifier: f32,
|
||||
|
||||
/// Upper bound for skill points that can be earned (90–690).
|
||||
#[arg(
|
||||
/// Upper bound for skill points that can be earned (90–690).
|
||||
#[arg(
|
||||
long,
|
||||
short = 'P',
|
||||
help = "Skill points limit",
|
||||
value_name = "Positive integer [90; 690]",
|
||||
default_value = "120",
|
||||
value_parser = value_parser!(u16).range(90..=690)
|
||||
)]
|
||||
pub skill_points_limit: u16,
|
||||
pub skill_points_limit: u16,
|
||||
|
||||
/// Which soundtrack to play (default: default)
|
||||
#[arg(
|
||||
/// Which soundtrack to play (default: default)
|
||||
#[arg(
|
||||
long,
|
||||
short = 'S',
|
||||
help = "Soundtrack",
|
||||
value_name = "...",
|
||||
default_value_t = Soundtrack::Default,
|
||||
value_enum
|
||||
)]
|
||||
pub sound_track: Soundtrack,
|
||||
pub soundtrack: Soundtrack,
|
||||
|
||||
/// Mute soundtrack (default: disabled).
|
||||
#[arg(
|
||||
long,
|
||||
help = "Mute soundtrack (default: disabled)",
|
||||
default_value_t = false
|
||||
)]
|
||||
pub mute: bool,
|
||||
/// Disable soundtrack (default: disabled).
|
||||
#[arg(
|
||||
long,
|
||||
short = 'D',
|
||||
help = "Disable soundtrack (default: disable)",
|
||||
default_value_t = false
|
||||
)]
|
||||
pub disable_audio: bool,
|
||||
|
||||
/// Enable logging to a file (default: disabled).
|
||||
#[arg(
|
||||
long,
|
||||
help = "Enable logging to file (default: disabled)",
|
||||
default_value_t = false
|
||||
)]
|
||||
pub log: bool,
|
||||
/// Mute soundtrack (default: disabled).
|
||||
#[arg(
|
||||
long,
|
||||
short = 'm',
|
||||
help = "Mute soundtrack (default: disabled)",
|
||||
default_value_t = false
|
||||
)]
|
||||
pub mute: bool,
|
||||
|
||||
/// Enable logging to a file (default: disabled).
|
||||
#[arg(
|
||||
long,
|
||||
short = 'l',
|
||||
help = "Enable logging to file (default: disabled)",
|
||||
default_value_t = false
|
||||
)]
|
||||
pub log: bool,
|
||||
}
|
||||
|
||||
/// Parses the command line arguments and returns a fully populated `Cli`
|
||||
@@ -172,13 +196,13 @@ pub struct Cli {
|
||||
/// handles argument validation and displays helpful error messages if
|
||||
/// the user supplies invalid input.
|
||||
pub fn get_args() -> Cli {
|
||||
let args: Cli = Cli::parse();
|
||||
let args: Cli = Cli::parse();
|
||||
|
||||
if args.log {
|
||||
init_logger();
|
||||
}
|
||||
if args.log {
|
||||
init_logger();
|
||||
}
|
||||
|
||||
args
|
||||
args
|
||||
}
|
||||
|
||||
/// Parses a string into a floating‑point XP modifier and validates that it
|
||||
@@ -193,15 +217,15 @@ pub fn get_args() -> Cli {
|
||||
/// * Returns an error if the string cannot be parsed as `f32`.
|
||||
/// * Returns an error if the parsed value is outside the allowed range.
|
||||
fn parse_xp(s: &str) -> Result<f32, Error> {
|
||||
let v: f32 = s
|
||||
.parse()
|
||||
.map_err(|e: ParseFloatError| Error::raw(ErrorKind::InvalidValue, e.to_string()))?;
|
||||
if (0.5..=2.0).contains(&v) {
|
||||
Ok(v)
|
||||
} else {
|
||||
Err(Error::raw(
|
||||
ErrorKind::InvalidValue,
|
||||
format!("Value {} is out of range 0.5..=2.0", v),
|
||||
))
|
||||
}
|
||||
let v: f32 = s
|
||||
.parse()
|
||||
.map_err(|e: ParseFloatError| Error::raw(ErrorKind::InvalidValue, e.to_string()))?;
|
||||
if (0.5..=2.0).contains(&v) {
|
||||
Ok(v)
|
||||
} else {
|
||||
Err(Error::raw(
|
||||
ErrorKind::InvalidValue,
|
||||
format!("Value {} is out of range 0.5..=2.0", v),
|
||||
))
|
||||
}
|
||||
}
|
||||
+6
-6
@@ -28,10 +28,10 @@ use std::fs::File;
|
||||
/// }
|
||||
/// ```
|
||||
pub fn init_logger() {
|
||||
WriteLogger::init(
|
||||
LevelFilter::Info,
|
||||
Config::default(),
|
||||
File::create("war_in_tunnels.log").expect("Failed to create log file"),
|
||||
)
|
||||
.expect("Failed to initialize logger");
|
||||
WriteLogger::init(
|
||||
LevelFilter::Info,
|
||||
Config::default(),
|
||||
File::create("war_in_tunnels.log").expect("Failed to create log file"),
|
||||
)
|
||||
.expect("Failed to initialize logger");
|
||||
}
|
||||
+23
-35
@@ -1,54 +1,42 @@
|
||||
use ratatui::{Terminal, crossterm::event::KeyEvent, prelude::CrosstermBackend};
|
||||
use std::{
|
||||
io::{Result, Stdout},
|
||||
mem::replace,
|
||||
sync::mpsc::{Receiver, Sender, channel},
|
||||
thread::{self, JoinHandle},
|
||||
io::{Result, Stdout},
|
||||
sync::mpsc::channel,
|
||||
thread,
|
||||
};
|
||||
use war_in_tunnels::{
|
||||
app::{
|
||||
App,
|
||||
threads::{AppChannels, AudioCmd, handle_audio, handle_ct_events, handle_tick_event},
|
||||
},
|
||||
cli::{Cli, get_args},
|
||||
app::{
|
||||
App,
|
||||
threads::{AudioCmd, handle_audio, handle_ct_events, handle_tick_event},
|
||||
},
|
||||
cli::{Cli, get_args},
|
||||
};
|
||||
|
||||
const TICK_MS: u8 = 33;
|
||||
|
||||
/// Starts the terminal UI application.
|
||||
///
|
||||
/// The function follows the steps outlined in the module‑level documentation.
|
||||
/// It returns any `std::io::Error` encountered while initializing or restoring the
|
||||
/// terminal, or while running the `App`.
|
||||
fn main() -> Result<()> {
|
||||
let args: Cli = get_args();
|
||||
let args: Cli = get_args();
|
||||
|
||||
let mut channels: AppChannels = AppChannels::new();
|
||||
let (input_tx, input_rx) = channel::<KeyEvent>();
|
||||
let (resize_tx, resize_rx) = channel::<(u16, u16)>();
|
||||
let (tick_tx, tick_rx) = channel::<()>();
|
||||
let (audio_tx, audio_rx) = channel::<AudioCmd>();
|
||||
|
||||
let input_tx: Sender<KeyEvent> = channels.input_tx.clone();
|
||||
let resize_tx: Sender<(u16, u16)> = channels.resize_tx.clone();
|
||||
let events_thread: JoinHandle<()> =
|
||||
thread::spawn(move || handle_ct_events(input_tx, resize_tx));
|
||||
thread::spawn(move || handle_ct_events(input_tx, resize_tx));
|
||||
thread::spawn(move || handle_tick_event(tick_tx));
|
||||
if !args.disable_audio {
|
||||
thread::spawn(move || handle_audio(audio_rx, args.mute, args.soundtrack));
|
||||
}
|
||||
|
||||
let tick_tx: Sender<()> = channels.tick_tx.clone();
|
||||
let tick_thread: JoinHandle<()> = thread::spawn(move || handle_tick_event(tick_tx, TICK_MS));
|
||||
let mut terminal: Terminal<CrosstermBackend<Stdout>> = ratatui::init();
|
||||
let mut app: App = App::new(args, audio_tx);
|
||||
|
||||
let audio_rx: Receiver<AudioCmd> = replace(&mut channels.audio_rx, channel().1);
|
||||
// let audio_thread: JoinHandle<()> =
|
||||
thread::spawn(move || {
|
||||
handle_audio(audio_rx, args.mute, args.sound_track);
|
||||
});
|
||||
let app_result: Result<()> = app.run(&mut terminal, input_rx, resize_rx, tick_rx);
|
||||
|
||||
let mut terminal: Terminal<CrosstermBackend<Stdout>> = ratatui::init();
|
||||
let mut app: App = App::new(args, channels.audio_tx.clone());
|
||||
ratatui::restore();
|
||||
|
||||
let app_result: Result<()> = app.run(&mut terminal, channels);
|
||||
|
||||
ratatui::restore();
|
||||
|
||||
let _ = events_thread.join();
|
||||
let _ = tick_thread.join();
|
||||
// let _ = audio_thread.join(); // TODO: kill playing music
|
||||
|
||||
app_result
|
||||
app_result
|
||||
}
|
||||
+162
@@ -0,0 +1,162 @@
|
||||
use clap::Parser;
|
||||
use ratatui::{
|
||||
Terminal,
|
||||
backend::TestBackend,
|
||||
crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyModifiers},
|
||||
layout::Rect,
|
||||
};
|
||||
use std::sync::mpsc::{Receiver, channel};
|
||||
use war_in_tunnels::{
|
||||
app::{App, GameStates, View, handle_keybindings, threads::AudioCmd},
|
||||
cli::Cli,
|
||||
};
|
||||
|
||||
fn app() -> (App, Receiver<AudioCmd>) {
|
||||
let args = Cli::try_parse_from(["game", "--disable-audio"]).unwrap();
|
||||
let (tx, rx) = channel();
|
||||
let mut app = App::new(args, tx);
|
||||
app.window_area = Rect::new(0, 0, 120, 40);
|
||||
app.states = Some(GameStates::new(&app.args, &app.window_area));
|
||||
(app, rx)
|
||||
}
|
||||
|
||||
fn press(app: &mut App, code: KeyCode) {
|
||||
handle_keybindings(app, KeyEvent::new(code, KeyModifiers::NONE));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn menu_selection_stays_in_bounds_and_opens_each_view() {
|
||||
let (mut app, _) = app();
|
||||
for _ in 0..10 {
|
||||
press(&mut app, KeyCode::Up);
|
||||
}
|
||||
assert_eq!(app.states_ref().unwrap().main_menu.selected_view, 1);
|
||||
for view in [View::Skirmish, View::PerkDecks, View::SkillsConfig] {
|
||||
press(&mut app, KeyCode::Char(' '));
|
||||
assert_eq!(app.view, view);
|
||||
press(&mut app, KeyCode::Esc);
|
||||
assert_eq!(app.view, View::MainMenu);
|
||||
press(&mut app, KeyCode::Down);
|
||||
}
|
||||
for _ in 0..10 {
|
||||
press(&mut app, KeyCode::Down);
|
||||
}
|
||||
assert_eq!(app.states_ref().unwrap().main_menu.selected_view, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quit_keys_work_in_every_view() {
|
||||
for view in [
|
||||
View::MainMenu,
|
||||
View::Skirmish,
|
||||
View::PerkDecks,
|
||||
View::SkillsConfig,
|
||||
] {
|
||||
for key in [
|
||||
KeyEvent::new(KeyCode::Char('q'), KeyModifiers::NONE),
|
||||
KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL),
|
||||
] {
|
||||
let (mut app, _) = app();
|
||||
app.view = view;
|
||||
handle_keybindings(&mut app, key);
|
||||
assert!(app.exit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn key_release_does_not_trigger_quit() {
|
||||
let (mut app, _) = app();
|
||||
handle_keybindings(
|
||||
&mut app,
|
||||
KeyEvent::new_with_kind(
|
||||
KeyCode::Char('q'),
|
||||
KeyModifiers::NONE,
|
||||
KeyEventKind::Release,
|
||||
),
|
||||
);
|
||||
assert!(!app.exit);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn music_keys_send_commands_without_opening_audio_device() {
|
||||
let (mut app, rx) = app();
|
||||
press(&mut app, KeyCode::Char('m'));
|
||||
press(&mut app, KeyCode::Char('b'));
|
||||
press(&mut app, KeyCode::Char('n'));
|
||||
assert!(matches!(rx.try_recv().unwrap(), AudioCmd::Mute));
|
||||
assert!(matches!(rx.try_recv().unwrap(), AudioCmd::VolumeUp));
|
||||
assert!(matches!(rx.try_recv().unwrap(), AudioCmd::VolumeDown));
|
||||
drop(rx);
|
||||
press(&mut app, KeyCode::Char('m'));
|
||||
assert!(!app.exit);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skirmish_keys_move_focus_toggle_panel_and_limit_zoom() {
|
||||
let (mut app, _) = app();
|
||||
app.view = View::Skirmish;
|
||||
press(&mut app, KeyCode::Right);
|
||||
assert_eq!(
|
||||
app
|
||||
.states_ref()
|
||||
.unwrap()
|
||||
.skirmish
|
||||
.board
|
||||
.get_focused_cell()
|
||||
.get_coords(),
|
||||
(5, 4)
|
||||
);
|
||||
press(&mut app, KeyCode::Tab);
|
||||
assert!(app.states_ref().unwrap().skirmish.side_panel);
|
||||
press(&mut app, KeyCode::Tab);
|
||||
assert!(!app.states_ref().unwrap().skirmish.side_panel);
|
||||
for _ in 0..4 {
|
||||
press(&mut app, KeyCode::Char(','));
|
||||
}
|
||||
assert_eq!(
|
||||
app.states_ref().unwrap().skirmish.board.zoom_level,
|
||||
war_in_tunnels::app::states::skirmish_states::ZoomLevel::ZoomedIn
|
||||
);
|
||||
for _ in 0..4 {
|
||||
press(&mut app, KeyCode::Char('.'));
|
||||
}
|
||||
assert_eq!(
|
||||
app.states_ref().unwrap().skirmish.board.zoom_level,
|
||||
war_in_tunnels::app::states::skirmish_states::ZoomLevel::ZoomedOut
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_views_render_without_real_terminal() {
|
||||
let (mut app, _) = app();
|
||||
let mut terminal = Terminal::new(TestBackend::new(120, 40)).unwrap();
|
||||
for view in [
|
||||
View::MainMenu,
|
||||
View::Skirmish,
|
||||
View::PerkDecks,
|
||||
View::SkillsConfig,
|
||||
] {
|
||||
app.view = view;
|
||||
let before = app.states.clone();
|
||||
terminal
|
||||
.draw(|frame| frame.render_widget(&mut app, frame.area()))
|
||||
.unwrap();
|
||||
let buffer = terminal.backend().buffer();
|
||||
let text: String = buffer.content.iter().map(|cell| cell.symbol()).collect();
|
||||
assert!(!text.trim().is_empty(), "{view:?}");
|
||||
if matches!(view, View::PerkDecks | View::SkillsConfig) {
|
||||
assert!(text.contains("Work in progress"));
|
||||
}
|
||||
assert_eq!(app.states, before);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn common_keys_work_before_states_are_initialized() {
|
||||
let (mut app, _) = app();
|
||||
app.states = None;
|
||||
press(&mut app, KeyCode::Down);
|
||||
press(&mut app, KeyCode::Char('q'));
|
||||
assert!(app.exit);
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
use rodio::Decoder;
|
||||
use std::{
|
||||
fs::{self, read},
|
||||
io::{BufReader, Cursor},
|
||||
path::PathBuf,
|
||||
process::{Command, Output},
|
||||
};
|
||||
use war_in_tunnels::app::threads::{SoundrackParts, Soundtrack, load_audio};
|
||||
|
||||
#[test]
|
||||
fn audio_repeats_after_end_of_file() {
|
||||
let data = read("soundtracks/default/calm.ogg").unwrap();
|
||||
let sample_count = Decoder::new(BufReader::new(Cursor::new(data)))
|
||||
.unwrap()
|
||||
.count();
|
||||
assert!(sample_count > 0);
|
||||
|
||||
let mut source = load_audio(&Soundtrack::Default, &SoundrackParts::Calm);
|
||||
assert_eq!(source.by_ref().take(sample_count).count(), sample_count);
|
||||
assert_eq!(source.take(1024).count(), 1024);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn packaged_audio_loads_without_project_working_directory() -> () {
|
||||
if std::env::var_os("WAR_IN_TUNNELS_PACKAGED_AUDIO_TEST").is_some() {
|
||||
let mut source: rodio::source::Amplify<
|
||||
rodio::decoder::LoopedDecoder<BufReader<Cursor<Vec<u8>>>>,
|
||||
> = load_audio(&Soundtrack::Default, &SoundrackParts::Calm);
|
||||
assert_eq!(source.by_ref().take(1024).count(), 1024);
|
||||
return;
|
||||
}
|
||||
|
||||
let directory: PathBuf = std::env::temp_dir().join(format!(
|
||||
"war-in-tunnels-packaged-audio-{}",
|
||||
std::process::id()
|
||||
));
|
||||
fs::create_dir_all(directory.join("soundtracks/default")).unwrap();
|
||||
fs::create_dir_all(directory.join("working-directory")).unwrap();
|
||||
let executable: PathBuf = directory.join("audio-test");
|
||||
fs::copy(std::env::current_exe().unwrap(), &executable).unwrap();
|
||||
for filename in ["calm.ogg", "test.ogg"] {
|
||||
fs::copy(
|
||||
PathBuf::from("soundtracks/default").join(filename),
|
||||
directory.join("soundtracks/default").join(filename),
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
let output: Output = Command::new(executable)
|
||||
.args([
|
||||
"--exact",
|
||||
"packaged_audio_loads_without_project_working_directory",
|
||||
"--nocapture",
|
||||
])
|
||||
.env("WAR_IN_TUNNELS_PACKAGED_AUDIO_TEST", "1")
|
||||
.current_dir(directory.join("working-directory"))
|
||||
.output()
|
||||
.unwrap();
|
||||
fs::remove_dir_all(directory).unwrap();
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"{}{}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr)
|
||||
);
|
||||
}
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
use ratatui::layout::Rect;
|
||||
use war_in_tunnels::app::states::skirmish_states::{
|
||||
BoardState, FocusedCell, MoveFocusedCell, Offset, Players, ZoomLevel,
|
||||
structures::{Structure, Structures},
|
||||
tasks::Task,
|
||||
units::Unit,
|
||||
};
|
||||
|
||||
fn board() -> BoardState {
|
||||
BoardState::new(&Rect::new(0, 0, 120, 40), 42, 11, ZoomLevel::Default, false)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn map_has_opposing_bases_ore_and_two_miners() {
|
||||
let board = board();
|
||||
assert_eq!(board.cells.len(), 11);
|
||||
assert!(board.cells.iter().all(|row| row.len() == 42));
|
||||
for (col, owner, ore_col) in [(2, Players::Player, 1), (39, Players::Enemy, 40)] {
|
||||
let cell = board.get_ref_cell(5, col);
|
||||
assert!(matches!(cell.get_structure(), Structures::Base(_)));
|
||||
assert_eq!(cell.get_structure().get_owner(), owner);
|
||||
let unit = cell.get_ref_option_unit().as_ref().unwrap();
|
||||
assert_eq!(unit.get_coords(), (5, col));
|
||||
assert_eq!(unit.get_owner(), owner);
|
||||
assert!(matches!(
|
||||
board.get_ref_cell(5, ore_col).get_structure(),
|
||||
Structures::Ore(_)
|
||||
));
|
||||
}
|
||||
assert_eq!(
|
||||
board
|
||||
.cells
|
||||
.iter()
|
||||
.flatten()
|
||||
.filter(|c| c.get_ref_option_unit().is_some())
|
||||
.count(),
|
||||
2
|
||||
);
|
||||
assert!(matches!(
|
||||
board.get_ref_cell(0, 0).get_structure(),
|
||||
Structures::Stone(_)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn focus_cannot_leave_map_even_with_extreme_coordinates() {
|
||||
let mut focus = FocusedCell::new(0, 0, 11, 42);
|
||||
assert_eq!(focus.move_focused_cell(MoveFocusedCell::Up), (0, 0));
|
||||
assert_eq!(focus.move_focused_cell(MoveFocusedCell::Left), (0, 0));
|
||||
focus.set_focused_cell((usize::MAX, usize::MAX));
|
||||
assert_eq!(focus.get_coords(), (10, 41));
|
||||
assert_eq!(focus.move_focused_cell(MoveFocusedCell::Down), (10, 41));
|
||||
assert_eq!(focus.move_focused_cell(MoveFocusedCell::Right), (10, 41));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn offsets_remain_within_bounds_when_limits_change() {
|
||||
let mut offset = Offset::new(Some(100), Some(5));
|
||||
assert_eq!(offset.get_value(), 5);
|
||||
offset.next();
|
||||
assert_eq!(offset.get_value(), 5);
|
||||
offset.set_max(2);
|
||||
assert_eq!(offset.get_value(), 2);
|
||||
offset.set_value(usize::MAX);
|
||||
assert_eq!(offset.get_value(), 2);
|
||||
for _ in 0..4 {
|
||||
offset.prev();
|
||||
}
|
||||
assert_eq!(offset.get_value(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn confirming_path_queues_task_without_including_start_cell() {
|
||||
let mut board = board();
|
||||
board.change_focused_cell(MoveFocusedCell::Left);
|
||||
board.toggle_marking();
|
||||
board.change_focused_cell(MoveFocusedCell::Right);
|
||||
board.change_focused_cell(MoveFocusedCell::Down);
|
||||
board.toggle_marking();
|
||||
let unit = board
|
||||
.get_ref_cell(5, 2)
|
||||
.get_ref_option_unit()
|
||||
.as_ref()
|
||||
.unwrap();
|
||||
let tasks = unit.get_ref_tasks();
|
||||
assert_eq!(tasks.len(), 1);
|
||||
assert_eq!(
|
||||
tasks[0].get_path().iter().copied().collect::<Vec<_>>(),
|
||||
[(5, 3), (6, 3)]
|
||||
);
|
||||
assert!(!board.marked_cells.marking_cells);
|
||||
assert!(board.get_marked_cells().is_empty());
|
||||
assert!(board.cells.iter().flatten().all(|c| !c.get_marked()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cancelling_path_clears_marks_without_queuing_task() {
|
||||
let mut board = board();
|
||||
board.change_focused_cell(MoveFocusedCell::Left);
|
||||
board.toggle_marking();
|
||||
board.change_focused_cell(MoveFocusedCell::Right);
|
||||
board.marked_cells.marking_cells = false;
|
||||
board.end_marking_cells(true);
|
||||
assert!(
|
||||
board
|
||||
.get_ref_cell(5, 2)
|
||||
.get_ref_option_unit()
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.get_ref_tasks()
|
||||
.is_empty()
|
||||
);
|
||||
assert!(board.get_marked_cells().is_empty());
|
||||
assert!(board.marked_cells.selected_unit.is_none());
|
||||
assert!(board.cells.iter().flatten().all(|c| !c.get_marked()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn backtracking_revisited_cell_preserves_earlier_mark() {
|
||||
let mut board = board();
|
||||
board.change_focused_cell(MoveFocusedCell::Left);
|
||||
board.toggle_marking();
|
||||
board.change_focused_cell(MoveFocusedCell::Right);
|
||||
board.change_focused_cell(MoveFocusedCell::Left);
|
||||
board.pop_marked_cell();
|
||||
assert_eq!(board.get_focused_cell().get_coords(), (5, 3));
|
||||
assert!(board.get_ref_cell(5, 2).get_marked());
|
||||
board.pop_marked_cell();
|
||||
board.pop_marked_cell();
|
||||
assert_eq!(board.get_focused_cell().get_coords(), (5, 2));
|
||||
assert_eq!(board.get_marked_cells().len(), 1);
|
||||
assert!(!board.get_ref_cell(5, 3).get_marked());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn large_viewport_and_zoom_keep_camera_inside_map() {
|
||||
for zoom in [
|
||||
ZoomLevel::ZoomedIn,
|
||||
ZoomLevel::Default,
|
||||
ZoomLevel::ZoomedOut,
|
||||
] {
|
||||
let mut board = BoardState::new(&Rect::new(0, 0, 240, 120), 36, 11, zoom, false);
|
||||
assert_eq!(board.vertical_offset.get_value(), 0);
|
||||
board.horizontal_offset.set_value(usize::MAX);
|
||||
board.change_resize(&Rect::new(0, 0, 500, 120), false);
|
||||
assert_eq!(board.horizontal_offset.get_value(), 0);
|
||||
board.change_zoom(ZoomLevel::ZoomedOut);
|
||||
assert!(board.is_focused_cell_visible());
|
||||
assert_eq!(board.cells.len(), 11);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
use clap::{Parser, error::ErrorKind};
|
||||
use std::process::Command;
|
||||
use war_in_tunnels::{app::View, cli::Cli};
|
||||
|
||||
#[test]
|
||||
fn defaults_start_in_menu_without_logging() {
|
||||
let args = Cli::try_parse_from(["game"]).unwrap();
|
||||
assert_eq!(args.view, View::MainMenu);
|
||||
assert_eq!((args.map_width, args.map_height), (42, 11));
|
||||
assert_eq!(args.username, "Player");
|
||||
assert!(!args.log);
|
||||
assert!(!args.disable_audio);
|
||||
assert!(!args.mute);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn numeric_options_accept_boundaries_and_reject_out_of_range_values() {
|
||||
for (flag, min, max) in [
|
||||
("--map-width", 36, 108),
|
||||
("--map-height", 11, 39),
|
||||
("--starting-wood", 1, 150),
|
||||
("--starting-iron", 1, 150),
|
||||
("--supply-limit", 49, 149),
|
||||
("--skill-points-limit", 90, 690),
|
||||
] {
|
||||
for value in [min, max] {
|
||||
assert!(
|
||||
Cli::try_parse_from(["game", flag, &value.to_string()]).is_ok(),
|
||||
"{flag}={value}"
|
||||
);
|
||||
}
|
||||
for value in [min - 1, max + 1] {
|
||||
assert!(
|
||||
Cli::try_parse_from(["game", flag, &value.to_string()]).is_err(),
|
||||
"{flag}={value}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn xp_modifier_rejects_non_finite_malformed_and_out_of_range_values() {
|
||||
for value in ["0.5", "1", "2.0"] {
|
||||
let args = Cli::try_parse_from(["game", "--xp-modifier", value]).unwrap();
|
||||
assert_eq!(args.xp_modifier, value.parse::<f32>().unwrap());
|
||||
}
|
||||
for value in ["0.49", "2.01", "NaN", "inf", "text"] {
|
||||
assert!(
|
||||
Cli::try_parse_from(["game", "--xp-modifier", value]).is_err(),
|
||||
"{value}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn enum_options_and_audio_flags_are_parsed() {
|
||||
let args =
|
||||
Cli::try_parse_from(["game", "--view", "skirmish", "--disable-audio", "--mute"]).unwrap();
|
||||
assert_eq!(args.view, View::Skirmish);
|
||||
assert!(args.disable_audio && args.mute);
|
||||
for flag in [
|
||||
"--view",
|
||||
"--game-mode",
|
||||
"--zoom-level",
|
||||
"--perk-deck",
|
||||
"--soundtrack",
|
||||
] {
|
||||
assert_eq!(
|
||||
Cli::try_parse_from(["game", flag, "unknown"])
|
||||
.unwrap_err()
|
||||
.kind(),
|
||||
ErrorKind::InvalidValue
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn binary_help_and_version_work_without_terminal_or_audio() {
|
||||
for (flag, expected) in [("--help", "Usage:"), ("--version", "0.1.0")] {
|
||||
let output = Command::new(env!("CARGO_BIN_EXE_war_in_tunnels"))
|
||||
.arg(flag)
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(output.status.success());
|
||||
assert!(String::from_utf8(output.stdout).unwrap().contains(expected));
|
||||
}
|
||||
let output = Command::new(env!("CARGO_BIN_EXE_war_in_tunnels"))
|
||||
.args(["--map-width", "0"])
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(!output.status.success());
|
||||
assert!(
|
||||
String::from_utf8(output.stderr)
|
||||
.unwrap()
|
||||
.contains("--map-width")
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,534 @@
|
||||
use clap::Parser;
|
||||
use ratatui::{
|
||||
Terminal,
|
||||
backend::TestBackend,
|
||||
buffer::Buffer,
|
||||
crossterm::event::{KeyCode, KeyEvent, KeyModifiers},
|
||||
layout::Rect,
|
||||
widgets::Widget,
|
||||
};
|
||||
use std::sync::mpsc::{Receiver, Sender, channel};
|
||||
use war_in_tunnels::{
|
||||
app::{
|
||||
App, GameStates, View, handle_keybindings,
|
||||
states::{
|
||||
SkirmishState,
|
||||
skirmish_states::{
|
||||
BoardState, CellSizes, MoveFocusedCell, Players, ZoomLevel,
|
||||
structures::{Structure, Structures, Tunnel},
|
||||
tasks::{DiggingTask, MovingTask, Task, Tasks},
|
||||
units::{MinerUnit, Unit, Units},
|
||||
},
|
||||
},
|
||||
threads::{AudioCmd, TICK_MS},
|
||||
},
|
||||
cli::Cli,
|
||||
};
|
||||
|
||||
fn game() -> SkirmishState {
|
||||
let args = Cli::try_parse_from(["game", "--disable-audio"]).unwrap();
|
||||
GameStates::new(&args, &Rect::new(0, 0, 120, 40)).skirmish
|
||||
}
|
||||
|
||||
fn queue(board: &mut BoardState, origin: (usize, usize), path: &[(usize, usize)]) {
|
||||
let unit = board.cells[origin.0][origin.1]
|
||||
.get_mut_option_unit()
|
||||
.as_mut()
|
||||
.unwrap();
|
||||
let power = unit.get_digging_power();
|
||||
unit.set_task(Tasks::Digging(DiggingTask::new(
|
||||
path.iter().copied().collect(),
|
||||
power,
|
||||
)));
|
||||
}
|
||||
|
||||
fn tunnel(board: &mut BoardState, row: usize, col: usize) {
|
||||
board.cells[row][col].set_structure(Structures::Tunnel(Tunnel::new()));
|
||||
}
|
||||
|
||||
fn unit(board: &BoardState, row: usize, col: usize) -> &Units {
|
||||
board.cells[row][col]
|
||||
.get_ref_option_unit()
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn travel_ticks() -> u16 {
|
||||
3000_u16.div_ceil(u16::from(TICK_MS))
|
||||
}
|
||||
|
||||
fn advance(game: &mut SkirmishState, ticks: u16) {
|
||||
for _ in 0..ticks {
|
||||
game.tick_update();
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tick_damages_stone_before_moving_and_finishes_entire_route() {
|
||||
let mut game = game();
|
||||
queue(&mut game.board, (5, 2), &[(5, 3), (5, 4)]);
|
||||
game.tick_update();
|
||||
assert_eq!(game.turn_counter, 1);
|
||||
let durability = game.board.cells[5][3].get_structure().get_durability();
|
||||
assert!(durability < 1000 && durability > 0);
|
||||
assert_eq!(unit(&game.board, 5, 2).get_coords(), (5, 2));
|
||||
assert!(game.board.cells[5][3].get_ref_option_unit().is_none());
|
||||
for _ in 0..500 {
|
||||
game.tick_update();
|
||||
}
|
||||
for col in [3, 4] {
|
||||
assert!(matches!(
|
||||
game.board.cells[5][col].get_structure(),
|
||||
Structures::Tunnel(_)
|
||||
));
|
||||
}
|
||||
assert!(matches!(
|
||||
game.board.cells[5][2].get_structure(),
|
||||
Structures::Base(_)
|
||||
));
|
||||
assert_eq!(unit(&game.board, 5, 4).get_coords(), (5, 4));
|
||||
assert!(unit(&game.board, 5, 4).get_ref_tasks().is_empty());
|
||||
assert!(game.board.cells[5][2].get_ref_option_unit().is_none());
|
||||
assert!(game.board.cells[5][3].get_ref_option_unit().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn each_tunnel_step_takes_three_seconds_in_both_directions() {
|
||||
for path in [[(5, 3), (5, 4)], [(4, 2), (3, 2)]] {
|
||||
let mut game = game();
|
||||
for (row, col) in path {
|
||||
tunnel(&mut game.board, row, col);
|
||||
}
|
||||
queue(&mut game.board, (5, 2), &path);
|
||||
advance(&mut game, travel_ticks() - 1);
|
||||
assert_eq!(unit(&game.board, 5, 2).get_coords(), (5, 2));
|
||||
game.tick_update();
|
||||
assert_eq!(
|
||||
unit(&game.board, path[0].0, path[0].1).get_coords(),
|
||||
path[0]
|
||||
);
|
||||
assert_eq!(
|
||||
unit(&game.board, path[0].0, path[0].1).get_ref_tasks()[0]
|
||||
.get_path()
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
advance(&mut game, travel_ticks() - 1);
|
||||
assert_eq!(
|
||||
unit(&game.board, path[0].0, path[0].1).get_coords(),
|
||||
path[0]
|
||||
);
|
||||
game.tick_update();
|
||||
assert!(
|
||||
unit(&game.board, path[1].0, path[1].1)
|
||||
.get_ref_tasks()
|
||||
.is_empty()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn queued_tasks_execute_in_order_and_keep_unit_coordinates_current() {
|
||||
let mut game = game();
|
||||
tunnel(&mut game.board, 5, 3);
|
||||
tunnel(&mut game.board, 5, 4);
|
||||
queue(&mut game.board, (5, 2), &[(5, 3)]);
|
||||
queue(&mut game.board, (5, 2), &[(5, 4)]);
|
||||
advance(&mut game, travel_ticks());
|
||||
assert_eq!(unit(&game.board, 5, 3).get_ref_tasks().len(), 1);
|
||||
advance(&mut game, travel_ticks());
|
||||
assert_eq!(unit(&game.board, 5, 4).get_coords(), (5, 4));
|
||||
assert!(unit(&game.board, 5, 4).get_ref_tasks().is_empty());
|
||||
queue(&mut game.board, (5, 4), &[(5, 3), (5, 2)]);
|
||||
advance(&mut game, 2 * travel_ticks() - 1);
|
||||
assert_eq!(unit(&game.board, 5, 3).get_coords(), (5, 3));
|
||||
game.tick_update();
|
||||
assert_eq!(unit(&game.board, 5, 2).get_coords(), (5, 2));
|
||||
assert!(matches!(
|
||||
game.board.cells[5][2].get_structure(),
|
||||
Structures::Base(_)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn occupied_tunnel_waits_without_losing_units_or_task() {
|
||||
let mut game = game();
|
||||
tunnel(&mut game.board, 5, 3);
|
||||
*game.board.cells[5][3].get_mut_option_unit() =
|
||||
Some(Units::Miner(MinerUnit::new(Players::Player, (5, 3))));
|
||||
queue(&mut game.board, (5, 2), &[(5, 3)]);
|
||||
for _ in 0..3 {
|
||||
game.tick_update();
|
||||
}
|
||||
assert_eq!(unit(&game.board, 5, 2).get_ref_tasks().len(), 1);
|
||||
assert_eq!(unit(&game.board, 5, 3).get_coords(), (5, 3));
|
||||
game.board.cells[5][3].get_mut_option_unit().take();
|
||||
advance(&mut game, travel_ticks() - 1);
|
||||
assert_eq!(unit(&game.board, 5, 2).get_coords(), (5, 2));
|
||||
game.tick_update();
|
||||
assert_eq!(unit(&game.board, 5, 3).get_coords(), (5, 3));
|
||||
assert!(unit(&game.board, 5, 3).get_ref_tasks().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ore_is_not_destroyed_by_digging_task() {
|
||||
let mut game = game();
|
||||
let ore = game.board.cells[5][1].get_structure();
|
||||
queue(&mut game.board, (5, 2), &[(5, 1)]);
|
||||
for _ in 0..200 {
|
||||
game.tick_update();
|
||||
}
|
||||
assert_eq!(game.board.cells[5][1].get_structure(), ore);
|
||||
assert_eq!(unit(&game.board, 5, 2).get_coords(), (5, 2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_or_empty_routes_do_not_teleport_or_remove_miner() {
|
||||
for path in [vec![], vec![(usize::MAX, usize::MAX)], vec![(0, 0)]] {
|
||||
let mut game = game();
|
||||
queue(&mut game.board, (5, 2), &path);
|
||||
game.tick_update();
|
||||
assert_eq!(unit(&game.board, 5, 2).get_coords(), (5, 2));
|
||||
assert!(unit(&game.board, 5, 2).get_ref_tasks().is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_player_miner_can_start_drawing_route() {
|
||||
let mut game = game();
|
||||
game.board.toggle_marking();
|
||||
assert!(!game.board.marked_cells.marking_cells);
|
||||
for _ in 0..36 {
|
||||
game.board.change_focused_cell(MoveFocusedCell::Right);
|
||||
}
|
||||
assert_eq!(game.board.get_focused_cell().get_coords(), (5, 39));
|
||||
game.board.toggle_marking();
|
||||
assert!(!game.board.marked_cells.marking_cells);
|
||||
assert!(game.board.get_marked_cells().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drawing_route_pauses_selected_miner_and_boundary_moves_do_not_repeat_cells() {
|
||||
let mut game = game();
|
||||
game.board.change_focused_cell(MoveFocusedCell::Left);
|
||||
queue(&mut game.board, (5, 2), &[(5, 3)]);
|
||||
game.board.toggle_marking();
|
||||
game.tick_update();
|
||||
assert_eq!(
|
||||
game.board.cells[5][3].get_structure().get_durability(),
|
||||
1000
|
||||
);
|
||||
for _ in 0..10 {
|
||||
game.board.change_focused_cell(MoveFocusedCell::Up);
|
||||
}
|
||||
let coords: Vec<_> = game.board.get_marked_cells().iter().copied().collect();
|
||||
assert!(coords.windows(2).all(|pair| pair[0] != pair[1]));
|
||||
game.board.end_marking_cells(true);
|
||||
game.tick_update();
|
||||
assert!(game.board.cells[5][3].get_structure().get_durability() < 1000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drawing_another_task_extends_the_end_of_queued_work() {
|
||||
let mut game = game();
|
||||
for col in [3, 4, 5] {
|
||||
tunnel(&mut game.board, 5, col);
|
||||
}
|
||||
game.board.change_focused_cell(MoveFocusedCell::Left);
|
||||
game.board.toggle_marking();
|
||||
game.board.change_focused_cell(MoveFocusedCell::Right);
|
||||
game.board.change_focused_cell(MoveFocusedCell::Right);
|
||||
game.board.toggle_marking();
|
||||
game.board.change_focused_cell(MoveFocusedCell::Left);
|
||||
game.board.change_focused_cell(MoveFocusedCell::Left);
|
||||
game.board.toggle_marking();
|
||||
assert_eq!(game.board.get_focused_cell().get_coords(), (5, 4));
|
||||
game.board.change_focused_cell(MoveFocusedCell::Right);
|
||||
game.board.toggle_marking();
|
||||
assert_eq!(unit(&game.board, 5, 2).get_ref_tasks().len(), 2);
|
||||
advance(&mut game, 3 * travel_ticks());
|
||||
assert_eq!(unit(&game.board, 5, 5).get_coords(), (5, 5));
|
||||
assert!(unit(&game.board, 5, 5).get_ref_tasks().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn freshly_dug_cell_is_entered_immediately_then_tunnel_travel_takes_three_seconds() {
|
||||
let mut game = game();
|
||||
tunnel(&mut game.board, 5, 4);
|
||||
queue(&mut game.board, (5, 2), &[(5, 3), (5, 4)]);
|
||||
for _ in 0..200 {
|
||||
game.tick_update();
|
||||
if matches!(
|
||||
game.board.cells[5][3].get_structure(),
|
||||
Structures::Tunnel(_)
|
||||
) {
|
||||
assert_eq!(unit(&game.board, 5, 3).get_coords(), (5, 3));
|
||||
advance(&mut game, travel_ticks() - 1);
|
||||
assert_eq!(unit(&game.board, 5, 3).get_coords(), (5, 3));
|
||||
game.tick_update();
|
||||
assert!(unit(&game.board, 5, 4).get_ref_tasks().is_empty());
|
||||
return;
|
||||
}
|
||||
}
|
||||
panic!("miner did not finish digging the stone");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn miner_symbol_in_tunnel_uses_owner_color_even_when_selected_or_marked() {
|
||||
for owner in [Players::Player, Players::Enemy] {
|
||||
for zoom in [ZoomLevel::Default, ZoomLevel::ZoomedIn] {
|
||||
for (selected, marked) in [(false, false), (true, false), (false, true), (true, true)] {
|
||||
let mut game = game();
|
||||
let cell = &mut game.board.cells[5][3];
|
||||
cell.set_structure(Structures::Tunnel(Tunnel::new()));
|
||||
cell
|
||||
.set_selected(selected)
|
||||
.set_marked(marked)
|
||||
.set_zoom_level(zoom);
|
||||
*cell.get_mut_option_unit() = Some(Units::Miner(MinerUnit::new(owner, (5, 3))));
|
||||
let area = Rect::new(
|
||||
0,
|
||||
0,
|
||||
zoom.get_cell_size(CellSizes::Width) as u16,
|
||||
zoom.get_cell_size(CellSizes::Height) as u16,
|
||||
);
|
||||
let mut buffer = Buffer::empty(area);
|
||||
cell.clone().render(area, &mut buffer);
|
||||
let marker = buffer
|
||||
.content
|
||||
.iter()
|
||||
.find(|cell| cell.symbol() == "M")
|
||||
.unwrap();
|
||||
assert_eq!(marker.fg, owner.get_color(), "{owner:?} {zoom:?}");
|
||||
assert!(buffer.content.iter().any(|cell| cell.symbol() == "T"));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tui_shows_movement_percentage_and_clears_it_after_each_step() {
|
||||
for zoom in [
|
||||
ZoomLevel::ZoomedIn,
|
||||
ZoomLevel::Default,
|
||||
ZoomLevel::ZoomedOut,
|
||||
] {
|
||||
let mut game = game();
|
||||
for col in [2, 3, 4] {
|
||||
tunnel(&mut game.board, 5, col);
|
||||
}
|
||||
queue(&mut game.board, (5, 2), &[(5, 3), (5, 4)]);
|
||||
let render = |board: &BoardState, col: usize| {
|
||||
let area = Rect::new(
|
||||
0,
|
||||
0,
|
||||
zoom.get_cell_size(CellSizes::Width) as u16,
|
||||
zoom.get_cell_size(CellSizes::Height) as u16,
|
||||
);
|
||||
let mut buffer = Buffer::empty(area);
|
||||
let mut cell = board.get_ref_cell(5, col).clone();
|
||||
cell.set_zoom_level(zoom);
|
||||
cell.render(area, &mut buffer);
|
||||
buffer
|
||||
};
|
||||
let text =
|
||||
|buffer: &Buffer| -> String { buffer.content.iter().map(|cell| cell.symbol()).collect() };
|
||||
assert!(!text(&render(&game.board, 2)).contains('%'));
|
||||
advance(&mut game, 46);
|
||||
let buffer = render(&game.board, 2);
|
||||
assert!(text(&buffer).contains("50%"), "{zoom:?}");
|
||||
if zoom != ZoomLevel::ZoomedOut {
|
||||
let marker = buffer
|
||||
.content
|
||||
.iter()
|
||||
.find(|cell| cell.symbol() == "M")
|
||||
.unwrap();
|
||||
assert_eq!(marker.fg, Players::Player.get_color());
|
||||
}
|
||||
advance(&mut game, travel_ticks() - 46);
|
||||
assert!(!text(&render(&game.board, 2)).contains('%'));
|
||||
assert!(!text(&render(&game.board, 3)).contains('%'));
|
||||
game.tick_update();
|
||||
assert!(text(&render(&game.board, 3)).contains("1%"));
|
||||
advance(&mut game, travel_ticks() - 1);
|
||||
assert!(!text(&render(&game.board, 4)).contains('%'));
|
||||
assert!(unit(&game.board, 5, 4).get_ref_tasks().is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn confirmed_route_splits_into_moving_and_digging_segments() {
|
||||
let mut game = game();
|
||||
for col in [3, 4, 7] {
|
||||
tunnel(&mut game.board, 5, col);
|
||||
}
|
||||
game.board.change_focused_cell(MoveFocusedCell::Left);
|
||||
game.board.toggle_marking();
|
||||
for _ in 0..5 {
|
||||
game.board.change_focused_cell(MoveFocusedCell::Right);
|
||||
}
|
||||
game.board.toggle_marking();
|
||||
let tasks = unit(&game.board, 5, 2).get_ref_tasks();
|
||||
assert_eq!(tasks.len(), 3);
|
||||
assert!(matches!(tasks[0], Tasks::Moving(_)));
|
||||
assert_eq!(
|
||||
tasks[0].get_path().iter().copied().collect::<Vec<_>>(),
|
||||
[(5, 3), (5, 4)]
|
||||
);
|
||||
assert!(matches!(tasks[1], Tasks::Digging(_)));
|
||||
assert_eq!(
|
||||
tasks[1].get_path().iter().copied().collect::<Vec<_>>(),
|
||||
[(5, 5), (5, 6)]
|
||||
);
|
||||
assert!(matches!(tasks[2], Tasks::Moving(_)));
|
||||
assert_eq!(tasks[0].get_icon(), '→');
|
||||
assert_eq!(tasks[1].get_icon(), '⛏');
|
||||
advance(&mut game, 2 * travel_ticks());
|
||||
assert!(matches!(
|
||||
unit(&game.board, 5, 4).get_ref_tasks()[0],
|
||||
Tasks::Digging(_)
|
||||
));
|
||||
advance(&mut game, 2 * 182);
|
||||
assert!(matches!(
|
||||
unit(&game.board, 5, 6).get_ref_tasks()[0],
|
||||
Tasks::Moving(_)
|
||||
));
|
||||
advance(&mut game, travel_ticks());
|
||||
assert!(unit(&game.board, 5, 7).get_ref_tasks().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn moving_task_does_not_damage_walkable_structure() {
|
||||
let mut game = game();
|
||||
tunnel(&mut game.board, 5, 3);
|
||||
let structure = game.board.cells[5][3].get_structure();
|
||||
game.board.cells[5][2]
|
||||
.get_mut_option_unit()
|
||||
.set_task(Tasks::Moving(MovingTask::new([(5, 3)].into())));
|
||||
advance(&mut game, travel_ticks());
|
||||
assert_eq!(game.board.cells[5][3].get_structure(), structure);
|
||||
assert!(unit(&game.board, 5, 3).get_ref_tasks().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn previously_dug_route_becomes_moving_without_instant_steps() {
|
||||
let mut game = game();
|
||||
queue(&mut game.board, (5, 2), &[(5, 3), (5, 4), (5, 3)]);
|
||||
advance(&mut game, 2 * 182);
|
||||
assert_eq!(unit(&game.board, 5, 4).get_coords(), (5, 4));
|
||||
game.tick_update();
|
||||
assert!(matches!(
|
||||
unit(&game.board, 5, 4).get_ref_tasks()[0],
|
||||
Tasks::Moving(_)
|
||||
));
|
||||
advance(&mut game, travel_ticks() - 2);
|
||||
assert_eq!(unit(&game.board, 5, 4).get_coords(), (5, 4));
|
||||
game.tick_update();
|
||||
assert!(unit(&game.board, 5, 3).get_ref_tasks().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn route_reclassifies_as_digging_if_target_is_stone() {
|
||||
let mut game = game();
|
||||
game.board.cells[5][2]
|
||||
.get_mut_option_unit()
|
||||
.set_task(Tasks::Moving(MovingTask::new([(5, 3)].into())));
|
||||
game.tick_update();
|
||||
assert!(matches!(
|
||||
unit(&game.board, 5, 2).get_ref_tasks()[0],
|
||||
Tasks::Digging(_)
|
||||
));
|
||||
assert!(game.board.cells[5][3].get_structure().get_durability() < 1000);
|
||||
advance(&mut game, 181);
|
||||
assert!(unit(&game.board, 5, 3).get_ref_tasks().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tab_panel_shows_current_step_and_total_for_active_and_pending_tasks() -> () {
|
||||
let args: Cli = Cli::try_parse_from(["game", "--disable-audio", "--view", "skirmish"]).unwrap();
|
||||
let channels: (Sender<AudioCmd>, Receiver<AudioCmd>) = channel();
|
||||
let mut app: App = App::new(args, channels.0);
|
||||
app.window_area = Rect::new(0, 0, 120, 60);
|
||||
app.states = Some(GameStates::new(&app.args, &app.window_area));
|
||||
let state: &mut SkirmishState = &mut app.states_mut().unwrap().skirmish;
|
||||
tunnel(&mut state.board, 5, 3);
|
||||
tunnel(&mut state.board, 5, 4);
|
||||
state.board.cells[5][2]
|
||||
.get_mut_option_unit()
|
||||
.set_task(Tasks::Moving(MovingTask::new([(5, 3), (5, 4)].into())));
|
||||
state.board.cells[5][2]
|
||||
.get_mut_option_unit()
|
||||
.set_task(Tasks::Digging(DiggingTask::new([(5, 5)].into(), 0.55)));
|
||||
advance(state, travel_ticks() + 46);
|
||||
handle_keybindings(&mut app, KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
|
||||
let mut terminal: Terminal<TestBackend> = Terminal::new(TestBackend::new(120, 60)).unwrap();
|
||||
terminal
|
||||
.draw(|frame: &mut ratatui::Frame<'_>| -> () {
|
||||
frame.render_widget(&mut app, frame.area());
|
||||
})
|
||||
.unwrap();
|
||||
let text: String = terminal
|
||||
.backend()
|
||||
.buffer()
|
||||
.content
|
||||
.iter()
|
||||
.map(|cell: &ratatui::buffer::Cell| -> &str { cell.symbol() })
|
||||
.collect();
|
||||
assert!(text.contains("Step: 50% | Total: 75%"));
|
||||
assert!(text.contains("Step: 0% | Total: 0%"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn space_route_tick_and_tui_show_remaining_durability_at_every_zoom() {
|
||||
let args = Cli::try_parse_from(["game", "--disable-audio", "--view", "skirmish"]).unwrap();
|
||||
let (tx, _) = channel();
|
||||
let mut app = App::new(args, tx);
|
||||
app.window_area = Rect::new(0, 0, 120, 40);
|
||||
app.states = Some(GameStates::new(&app.args, &app.window_area));
|
||||
for code in [
|
||||
KeyCode::Left,
|
||||
KeyCode::Char(' '),
|
||||
KeyCode::Right,
|
||||
KeyCode::Char(' '),
|
||||
] {
|
||||
handle_keybindings(&mut app, KeyEvent::new(code, KeyModifiers::NONE));
|
||||
}
|
||||
app.states_mut().unwrap().skirmish.tick_update();
|
||||
let durability = app.states_ref().unwrap().skirmish.board.cells[5][3]
|
||||
.get_structure()
|
||||
.get_durability();
|
||||
let mut terminal = Terminal::new(TestBackend::new(120, 40)).unwrap();
|
||||
for zoom in [
|
||||
ZoomLevel::ZoomedIn,
|
||||
ZoomLevel::Default,
|
||||
ZoomLevel::ZoomedOut,
|
||||
] {
|
||||
let state = &mut app.states_mut().unwrap().skirmish;
|
||||
state.board.change_zoom(zoom);
|
||||
state.board.vertical_offset.set_value(5);
|
||||
terminal
|
||||
.draw(|frame| frame.render_widget(&mut app, frame.area()))
|
||||
.unwrap();
|
||||
let text: String = terminal
|
||||
.backend()
|
||||
.buffer()
|
||||
.content
|
||||
.iter()
|
||||
.map(|cell| cell.symbol())
|
||||
.collect();
|
||||
assert!(text.contains("99%"), "{zoom:?}");
|
||||
}
|
||||
handle_keybindings(&mut app, KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
|
||||
terminal
|
||||
.draw(|frame| frame.render_widget(&mut app, frame.area()))
|
||||
.unwrap();
|
||||
let text: String = terminal
|
||||
.backend()
|
||||
.buffer()
|
||||
.content
|
||||
.iter()
|
||||
.map(|cell| cell.symbol())
|
||||
.collect();
|
||||
assert!(text.contains(&format!("{durability}/1000")));
|
||||
assert_eq!(app.view, View::Skirmish);
|
||||
}
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
use war_in_tunnels::app::states::skirmish_states::{
|
||||
Players,
|
||||
tasks::{DiggingTask, MovingTask, Task, Tasks},
|
||||
units::{MinerUnit, Unit},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn digging_progress_accumulates_and_stops_at_completion() {
|
||||
let mut task = DiggingTask::new([(5, 3)].into(), 40.0);
|
||||
assert!(!task.is_finished());
|
||||
task.progress();
|
||||
assert_eq!(task.get_progress(), 40.0);
|
||||
assert!(!task.is_finished());
|
||||
for _ in 0..3 {
|
||||
task.progress();
|
||||
}
|
||||
assert_eq!(task.get_progress(), 100.0);
|
||||
assert!(task.is_finished());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn advancing_path_resets_progress_and_handles_empty_path() {
|
||||
let mut task = DiggingTask::new([(5, 3), (5, 4)].into(), 25.0);
|
||||
task.progress();
|
||||
task.pop_front();
|
||||
assert_eq!(task.current_target(), Some((5, 4)));
|
||||
assert_eq!(task.get_progress(), 0.0);
|
||||
task.pop_front();
|
||||
task.pop_front();
|
||||
assert_eq!(task.current_target(), None);
|
||||
assert!(task.get_path().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn miners_keep_tasks_in_submission_order() {
|
||||
let mut miner = MinerUnit::new(Players::Player, (5, 2));
|
||||
for target in [(5, 3), (6, 3)] {
|
||||
miner.set_task(Tasks::Digging(DiggingTask::new([target].into(), 1.0)));
|
||||
}
|
||||
assert_eq!(miner.get_ref_tasks().len(), 2);
|
||||
assert_eq!(
|
||||
*miner
|
||||
.get_mut_tasks()
|
||||
.pop_front()
|
||||
.unwrap()
|
||||
.get_path_front_coords(),
|
||||
(5, 3)
|
||||
);
|
||||
assert_eq!(*miner.get_ref_tasks()[0].get_path_front_coords(), (6, 3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn movement_progress_is_separate_from_digging_and_resets_for_next_cell() {
|
||||
let mut task = MovingTask::new([(5, 3), (5, 4)].into());
|
||||
assert_eq!(task.get_movement_progress(), None);
|
||||
assert_eq!(task.get_progress(), 0.0);
|
||||
assert!(!task.is_finished());
|
||||
for _ in 0..46 {
|
||||
task.progress();
|
||||
}
|
||||
assert_eq!(task.get_movement_progress(), Some(50));
|
||||
for _ in 46..91 {
|
||||
task.progress();
|
||||
}
|
||||
assert_eq!(task.get_movement_progress(), Some(100));
|
||||
assert_eq!(task.get_progress(), 100.0);
|
||||
assert!(task.is_finished());
|
||||
task.progress();
|
||||
assert_eq!(task.get_progress(), 100.0);
|
||||
task.pop_front();
|
||||
assert_eq!(task.get_movement_progress(), None);
|
||||
assert_eq!(task.get_progress(), 0.0);
|
||||
assert!(!task.is_finished());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn overall_digging_progress_counts_completed_cells_and_current_step() -> () {
|
||||
let mut task: DiggingTask = DiggingTask::new([(5, 3), (5, 4)].into(), 25.0);
|
||||
assert_eq!(task.get_total_progress(), 0.0);
|
||||
task.progress();
|
||||
assert_eq!(task.get_progress(), 25.0);
|
||||
assert_eq!(task.get_total_progress(), 12.5);
|
||||
task.pop_front();
|
||||
assert_eq!(task.get_progress(), 0.0);
|
||||
assert_eq!(task.get_total_progress(), 50.0);
|
||||
task.progress();
|
||||
assert_eq!(task.get_total_progress(), 62.5);
|
||||
task.pop_front();
|
||||
assert_eq!(task.get_total_progress(), 100.0);
|
||||
let empty: DiggingTask = DiggingTask::new([].into(), 1.0);
|
||||
assert_eq!(empty.get_total_progress(), 100.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn task_conversion_keeps_completed_steps_and_original_length() -> () {
|
||||
let mut digging: DiggingTask = DiggingTask::new([(5, 3), (5, 4)].into(), 25.0);
|
||||
digging.pop_front();
|
||||
let mut moving: MovingTask = MovingTask::from_digging(digging);
|
||||
assert_eq!(moving.get_total_steps(), 2);
|
||||
assert_eq!(moving.get_total_progress(), 50.0);
|
||||
for _ in 0..91 {
|
||||
moving.progress();
|
||||
}
|
||||
assert_eq!(moving.get_total_progress(), 100.0);
|
||||
let mut digging: DiggingTask = DiggingTask::from_moving(moving, 25.0);
|
||||
assert_eq!(digging.get_total_steps(), 2);
|
||||
assert_eq!(digging.get_total_progress(), 50.0);
|
||||
digging.progress();
|
||||
assert_eq!(digging.get_total_progress(), 62.5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn digging_queue_formats_step_total_and_pending_progress() -> () {
|
||||
let mut miner: MinerUnit = MinerUnit::new(Players::Player, (5, 2));
|
||||
let mut task: DiggingTask = DiggingTask::new([(5, 3), (5, 4)].into(), 25.0);
|
||||
task.pop_front();
|
||||
task.progress();
|
||||
miner.set_task(Tasks::Digging(task));
|
||||
miner.set_task(Tasks::Moving(MovingTask::new([(5, 5)].into())));
|
||||
let lines: Vec<ratatui::text::Line<'_>> = miner.get_tasks_formatted();
|
||||
assert!(lines[1].to_string().contains("Step: 25% | Total: 62%"));
|
||||
assert!(lines[3].to_string().contains("Step: 0% | Total: 0%"));
|
||||
}
|
||||
Reference in new issue
Block a user