Compare commits
155 Commits
0.99.0-hor
...
master
Author | SHA1 | Date |
---|---|---|
biroder | c33d387031 | |
Laura K | b572fdfd47 | |
Laura K | 94b9f930ce | |
莯凛 | 01ec93dd27 | |
biroder | ca5361cc58 | |
poly000 | 08f086bfc4 | |
poly000 | 1f288e2a39 | |
poly000 | 0e0cd66564 | |
József Sallai | 9c95b20f5c | |
Alula | 7630a9b60e | |
Alula | ae909878c4 | |
Alula | 7785513e8b | |
Alula | 7f33f7b6c8 | |
József Sallai | 6c9b4d9a54 | |
Edward Stuckey | af9947b931 | |
Laura K | b275816e76 | |
Laura K | 8e3ccea8a1 | |
biroder | 76ec4771b3 | |
biroder | 3b77cdf0c5 | |
biroder | 397cd8f584 | |
biroder | 21c255efb4 | |
Sobakin | e1fb118910 | |
Laura K | c56bd2e8ae | |
periwinkle | e09fbf5c63 | |
biroder | c4cffd54a8 | |
biroder | 99f13a746e | |
biroder | 690c97e44d | |
biroder | 311ca8b12f | |
biroder | 152e31966a | |
biroder | b4ede5bcad | |
biroder | a5f49c07e4 | |
biroder | fc66b84d8f | |
biroder | 87cc7f12e3 | |
biroder | e9cf548cc2 | |
biroder | 3af1d61e5b | |
Edward Stuckey | 3b14adf949 | |
biroder | 46710dd31a | |
biroder | 10e4d3efff | |
biroder | 2b1411787b | |
biroder | 0e33bcaaf9 | |
biroder | 45443dfa23 | |
biroder | 06ae269b7b | |
periwinkle | 12a305becb | |
periwinkle | bd203cfddb | |
periwinkle | c2ad3dd643 | |
biroder | 3468bcf5fd | |
biroder | 21221d80e7 | |
József Sallai | a45c630116 | |
biroder | 73295a6351 | |
biroder | ab87862646 | |
József Sallai | 5f24ee52b0 | |
biroder | 02e1763e1d | |
biroder | 44c6af2146 | |
periwinkle | d1d008edda | |
periwinkle | 4d7dfd0266 | |
biroder | b70f0007b1 | |
biroder | f7148edd96 | |
biroder | 425a26b3a0 | |
periwinkle | f6caffd624 | |
biroder | b294f65656 | |
karnak | a5ed1a370e | |
karnak | f07228895c | |
periwinkle | b3007c10e3 | |
periwinkle | 7caff84e04 | |
Alula | dcdb4e9d39 | |
biroder | 596c7b8aff | |
biroder | ed2c5f510a | |
biroder | 0ba5aad8af | |
biroder | 50ff888141 | |
biroder | 72e74e2113 | |
biroder | 47a43467d0 | |
biroder | 19e0da519f | |
biroder | 721a0c907a | |
biroder | 2a3341e3c5 | |
biroder | 63f903104a | |
biroder | 277f5d957b | |
biroder | 9409bb35fe | |
biroder | 9e09d56b76 | |
biroder | 40767c021b | |
biroder | 0e164a44df | |
biroder | 38338ae551 | |
biroder | dbb72ff6c1 | |
biroder | 0a45332f3f | |
biroder | aa27680baf | |
biroder | 67fb32499f | |
biroder | 5821f06928 | |
biroder | 5f93658f0f | |
biroder | cd671cce48 | |
biroder | 2a162d948f | |
biroder | 72a2ada0b2 | |
biroder | 0ccbbefbed | |
biroder | bc91d9f366 | |
Alula | beb290dafb | |
biroder | 5f7bae8f4f | |
biroder | 11eafb8d03 | |
biroder | f99b452073 | |
Alula | 51834be404 | |
Alula | b2fb548584 | |
biroder | 1633eef3dd | |
biroder | e57c9cdb27 | |
biroder | 07b1550cf4 | |
biroder | a15fd4d190 | |
biroder | b8af9aed25 | |
biroder | 8992889e94 | |
biroder | f91e793062 | |
biroder | 3fbe94ecd1 | |
biroder | 5bd0dcd564 | |
József Sallai | 41b840d13f | |
József Sallai | 95e09ded99 | |
József Sallai | 6bccf59f5b | |
József Sallai | 890c0596ed | |
József Sallai | b22ca8b35e | |
József Sallai | e77241cd56 | |
József Sallai | 3dbe56690a | |
József Sallai | 1810bf6d5b | |
József Sallai | d6715bccea | |
alula | 30c85c2f8b | |
biroder | 979909faa8 | |
Alula | 90df8faa7a | |
Daedliy | 6d3c127912 | |
Alula | 515a0a7fe7 | |
Alula | 7fe96780de | |
Alula | f1e5d11b2a | |
Alula | 3739e9f170 | |
József Sallai | 3c79582fed | |
József Sallai | a187942cff | |
József Sallai | 4ae085f7e2 | |
József Sallai | 153b1b4962 | |
Alula | 2774eae66f | |
Alula | 4928ce4682 | |
Alula | 64290ae5a3 | |
Alula | 1457aa3caa | |
Alula | ee58c1c8af | |
Alula | a5bb130e73 | |
Alula | bbd20a003b | |
Alula | ef1c2a5930 | |
Alula | 074af609bc | |
Alula | efa8a47b8d | |
Alula | bc3616d073 | |
Devon W | 510442490e | |
József Sallai | 0a7fd4dc47 | |
József Sallai | 4fa7069e82 | |
József Sallai | 212d7b915b | |
József Sallai | cb95db1e89 | |
József Sallai | 334e64f499 | |
József Sallai | 90e58649a7 | |
Alula | e845c87738 | |
alula | 2cb36de715 | |
alula | c2a8bf52e9 | |
József Sallai | e975a75ec4 | |
Alula | 5854735392 | |
József Sallai | 356f4230b5 | |
József Sallai | 4be3dd518b | |
József Sallai | 5ed2d40e23 | |
Alula | d87bbf2b46 |
|
@ -1,8 +1,12 @@
|
|||
version: "0.99.0.{build}-{branch}"
|
||||
version: "0.101.0-{build}-{branch}"
|
||||
|
||||
skip_commits:
|
||||
files:
|
||||
- README.md
|
||||
- LICENSE
|
||||
- app/
|
||||
- drsandroid/
|
||||
- drshorizon/
|
||||
|
||||
environment:
|
||||
global:
|
||||
|
@ -11,12 +15,15 @@ environment:
|
|||
- channel: stable
|
||||
target: x86_64-pc-windows-msvc
|
||||
target_name: win64
|
||||
arch_name: x86_64
|
||||
job_name: windows-x64
|
||||
appveyor_build_worker_image: Visual Studio 2019
|
||||
# - channel: stable
|
||||
# target: i686-pc-windows-msvc
|
||||
# target_name: win32
|
||||
# job_name: windows-x32
|
||||
- channel: stable
|
||||
target: i686-pc-windows-msvc
|
||||
target_name: win32
|
||||
arch_name: i686
|
||||
job_name: windows-x32
|
||||
appveyor_build_worker_image: Visual Studio 2019
|
||||
- channel: stable
|
||||
target: x86_64-unknown-linux-gnu
|
||||
target_name: linux
|
||||
|
@ -52,21 +59,22 @@ for:
|
|||
- cargo -vV
|
||||
|
||||
cache:
|
||||
- '%USERPROFILE%\.cache -> Cargo.toml'
|
||||
- '%USERPROFILE%\.cargo\bin -> Cargo.toml'
|
||||
- '%USERPROFILE%\.cargo\registry\index -> Cargo.toml'
|
||||
- '%USERPROFILE%\.cargo\registry\cache -> Cargo.toml'
|
||||
- '%USERPROFILE%\.cargo\git\db -> Cargo.toml'
|
||||
- '%USERPROFILE%\.rustup -> Cargo.toml'
|
||||
- 'target -> Cargo.toml'
|
||||
- '%USERPROFILE%\.cache'
|
||||
- '%USERPROFILE%\.cargo\bin'
|
||||
- '%USERPROFILE%\.cargo\registry\index'
|
||||
- '%USERPROFILE%\.cargo\registry\cache'
|
||||
- '%USERPROFILE%\.cargo\git\db'
|
||||
- '%USERPROFILE%\.rustup'
|
||||
- 'target'
|
||||
|
||||
build_script:
|
||||
- set DRS_BUILD_VERSION_OVERRIDE=%APPVEYOR_BUILD_VERSION%
|
||||
#- set DRS_BUILD_VERSION_OVERRIDE=%APPVEYOR_BUILD_VERSION%
|
||||
- if "%APPVEYOR_REPO_TAG%" == "true" (set DRS_BUILD_VERSION_OVERRIDE=%APPVEYOR_REPO_TAG_NAME%) else (set DRS_BUILD_VERSION_OVERRIDE=%APPVEYOR_BUILD_VERSION%)
|
||||
- set CARGO_INCREMENTAL=1
|
||||
- cargo build --release --bin doukutsu-rs
|
||||
- mkdir release
|
||||
- copy LICENSE release\LICENSE
|
||||
- copy target\release\doukutsu-rs.exe release\doukutsu-rs.x86_64.exe
|
||||
- copy target\release\doukutsu-rs.exe release\doukutsu-rs.%arch_name%.exe
|
||||
- cd release
|
||||
- 7z a ../doukutsu-rs_%target_name%.zip *
|
||||
- appveyor PushArtifact ../doukutsu-rs_%target_name%.zip
|
||||
|
@ -75,7 +83,14 @@ for:
|
|||
matrix:
|
||||
only:
|
||||
- appveyor_build_worker_image: macos-monterey
|
||||
|
||||
|
||||
init:
|
||||
- ps: |
|
||||
if ($env:APPVEYOR_REPO_TAG -eq "true")
|
||||
{
|
||||
Update-AppveyorBuild -Version "$env:APPVEYOR_REPO_TAG_NAME"
|
||||
}
|
||||
|
||||
install:
|
||||
- curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -yv --default-toolchain $channel
|
||||
- export PATH=$PATH:$HOME/.cargo/bin
|
||||
|
@ -87,16 +102,17 @@ for:
|
|||
- cargo install cargo-bundle --force
|
||||
|
||||
cache:
|
||||
- '$HOME/.cache -> Cargo.toml'
|
||||
- '$HOME/.cargo/bin -> Cargo.toml'
|
||||
- '$HOME/.cargo/registry/index -> Cargo.toml'
|
||||
- '$HOME/.cargo/registry/cache -> Cargo.toml'
|
||||
- '$HOME/.cargo/git/db -> Cargo.toml'
|
||||
- '$HOME/.rustup -> Cargo.toml'
|
||||
- 'target -> Cargo.toml'
|
||||
- '$HOME/.cache'
|
||||
- '$HOME/.cargo/bin'
|
||||
- '$HOME/.cargo/registry/index'
|
||||
- '$HOME/.cargo/registry/cache'
|
||||
- '$HOME/.cargo/git/db'
|
||||
- '$HOME/.rustup'
|
||||
- 'target'
|
||||
|
||||
build_script:
|
||||
- export DRS_BUILD_VERSION_OVERRIDE=$APPVEYOR_BUILD_VERSION
|
||||
#- export DRS_BUILD_VERSION_OVERRIDE=$APPVEYOR_BUILD_VERSION
|
||||
- if [ "$APPVEYOR_REPO_TAG" = "true" ]; then export DRS_BUILD_VERSION_OVERRIDE=$APPVEYOR_REPO_TAG_NAME; else export DRS_BUILD_VERSION_OVERRIDE=$APPVEYOR_BUILD_VERSION; fi
|
||||
- CARGO_INCREMENTAL=1 cargo bundle --release --target $target
|
||||
- mkdir release
|
||||
- cp LICENSE ./release/LICENSE
|
||||
|
@ -121,16 +137,17 @@ for:
|
|||
- cargo -vV
|
||||
|
||||
cache:
|
||||
- '$HOME/.cache -> Cargo.toml'
|
||||
- '$HOME/.cargo/bin -> Cargo.toml'
|
||||
- '$HOME/.cargo/registry/index -> Cargo.toml'
|
||||
- '$HOME/.cargo/registry/cache -> Cargo.toml'
|
||||
- '$HOME/.cargo/git/db -> Cargo.toml'
|
||||
- '$HOME/.rustup -> Cargo.toml'
|
||||
- 'target -> Cargo.toml'
|
||||
- '$HOME/.cache'
|
||||
- '$HOME/.cargo/bin'
|
||||
- '$HOME/.cargo/registry/index'
|
||||
- '$HOME/.cargo/registry/cache'
|
||||
- '$HOME/.cargo/git/db'
|
||||
- '$HOME/.rustup'
|
||||
- 'target'
|
||||
|
||||
build_script:
|
||||
- export DRS_BUILD_VERSION_OVERRIDE=$APPVEYOR_BUILD_VERSION
|
||||
#- export DRS_BUILD_VERSION_OVERRIDE=$APPVEYOR_BUILD_VERSION
|
||||
- if [ "$APPVEYOR_REPO_TAG" = "true" ]; then export DRS_BUILD_VERSION_OVERRIDE=$APPVEYOR_REPO_TAG_NAME; else export DRS_BUILD_VERSION_OVERRIDE=$APPVEYOR_BUILD_VERSION; fi
|
||||
- RUSTFLAGS="-C link-arg=-s" CARGO_INCREMENTAL=1 cargo build --release --bin doukutsu-rs
|
||||
- mkdir release
|
||||
- cp LICENSE ./release/LICENSE
|
||||
|
@ -138,3 +155,4 @@ for:
|
|||
- cd release
|
||||
- 7z a ../doukutsu-rs_$target_name.zip *
|
||||
- appveyor PushArtifact ../doukutsu-rs_$target_name.zip
|
||||
|
||||
|
|
|
@ -0,0 +1,258 @@
|
|||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches-ignore:
|
||||
- cpp-rewrite
|
||||
- horizon-os
|
||||
paths-ignore:
|
||||
- '.gitignore'
|
||||
- '.github/*'
|
||||
- '**.md'
|
||||
- 'LICENSE'
|
||||
- 'drshorizon/**'
|
||||
- 'res/**'
|
||||
workflow_dispatch:
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
env:
|
||||
VERSION: "0.101.0"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: ${{ matrix.name }}
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: true
|
||||
matrix:
|
||||
include:
|
||||
- name: Linux x86_64
|
||||
os: ubuntu-latest
|
||||
channel: stable
|
||||
target: x86_64-unknown-linux-gnu
|
||||
target_name: linux-x64
|
||||
arch_name: x86_64
|
||||
- name: Windows x64
|
||||
os: windows-latest
|
||||
channel: stable
|
||||
target: x86_64-pc-windows-msvc
|
||||
target_name: windows-x64
|
||||
arch_name: x86_64
|
||||
- name: Windows x32
|
||||
os: windows-latest
|
||||
channel: stable
|
||||
target: i686-pc-windows-msvc
|
||||
target_name: windows-x32
|
||||
arch_name: i686
|
||||
- name: macOS x64 (Intel Macs)
|
||||
os: macos-latest
|
||||
channel: stable
|
||||
target: x86_64-apple-darwin
|
||||
target_name: mac-x64
|
||||
- name: macOS ARM64 (M1 Macs)
|
||||
os: macos-latest
|
||||
channel: stable
|
||||
target: aarch64-apple-darwin
|
||||
target_name: mac-arm64
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install dependencies
|
||||
if: ${{ matrix.os == 'ubuntu-latest' }}
|
||||
run: sudo apt install libasound2-dev libudev-dev libgl1-mesa-dev
|
||||
|
||||
- name: Restore cache
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo
|
||||
~/.rustup
|
||||
target
|
||||
key: ${{ matrix.target_name }}-cargo
|
||||
|
||||
- name: Setup rust toolchain
|
||||
run: |
|
||||
rustup default ${{ matrix.channel }}
|
||||
rustup target add ${{ matrix.target }}
|
||||
|
||||
rustc -vV
|
||||
cargo -vV
|
||||
|
||||
if [ "${{ runner.os }}" == "macOS" ]; then
|
||||
cargo install cargo-bundle
|
||||
fi
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
if [ "${{ github.ref_type }}" == "tag" ]; then
|
||||
export DRS_BUILD_VERSION_OVERRIDE="{{ github.ref_name }}"
|
||||
elif [ "${{ github.ref_name }}" == "master"]; then
|
||||
export DRS_BUILD_VERSION_OVERRIDE="${{ env.VERSION }}-$((${{ github.run_number }} + 654))"
|
||||
else
|
||||
export DRS_BUILD_VERSION_OVERRIDE="${{ env.VERSION }}-${GITHUB_SHA:0:7}"
|
||||
fi
|
||||
|
||||
mkdir release
|
||||
cp LICENSE release
|
||||
|
||||
if [ "${{ runner.os }}" == "macOS" ]; then
|
||||
CARGO_INCREMENTAL=1 cargo bundle --release --target ${{ matrix.target }}
|
||||
cp -a ./target/${{ matrix.target }}/release/bundle/osx/doukutsu-rs.app release/doukutsu-rs.app
|
||||
codesign -s - -f ./release/doukutsu-rs.app/Contents/MacOS/doukutsu-rs
|
||||
elif [ "${{ runner.os }}" == "Windows" ]; then
|
||||
CARGO_INCREMENTAL=1 cargo build --release --bin doukutsu-rs --target ${{ matrix.target }}
|
||||
cp ./target/${{ matrix.target }}/release/doukutsu-rs.exe release/doukutsu-rs.${{ matrix.arch_name }}.exe
|
||||
elif [ "${{ runner.os }}" == "Linux" ]; then
|
||||
RUSTFLAGS="-C link-args=-s" CARGO_INCREMENTAL=1 cargo build --release --bin doukutsu-rs
|
||||
cp -a ./target/release/doukutsu-rs release/doukutsu-rs.${{ matrix.arch_name }}.elf
|
||||
fi
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: doukutsu-rs_${{ matrix.target_name }}
|
||||
path: ./release/*
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Save cache
|
||||
if: ${{ github.ref_name == 'master' || github.ref_type == 'tag' }}
|
||||
uses: actions/cache/save@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cargo
|
||||
~/.rustup
|
||||
target
|
||||
key: ${{ matrix.target_name }}-cargo
|
||||
|
||||
build_android:
|
||||
name: Android build
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
APP_OUTPUTS_DIR: "app/app/build/outputs/apk/release"
|
||||
strategy:
|
||||
fail-fast: true
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Restore cache
|
||||
uses: actions/cache/restore@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cache
|
||||
~/.cargo
|
||||
~/.rustup
|
||||
~/.gradle
|
||||
app/app/.cxx
|
||||
app/app/build
|
||||
drsandroid/target
|
||||
key: android-cargo
|
||||
|
||||
- name: Setup rust toolchain
|
||||
run: |
|
||||
rustup default stable
|
||||
rustup target add aarch64-linux-android armv7-linux-androideabi i686-linux-android
|
||||
rustc -vV
|
||||
cargo -vV
|
||||
cargo install cargo-ndk
|
||||
|
||||
- name: Build
|
||||
run: |
|
||||
if [ "${{ github.ref_type }}" == "tag" ]; then
|
||||
export DRS_BUILD_VERSION_OVERRIDE="{{ github.ref_name }}"
|
||||
elif [ "${{ github.ref_name }}" == "master"]; then
|
||||
export DRS_BUILD_VERSION_OVERRIDE="${{ env.VERSION }}-$((${{ github.run_number }} + 654))"
|
||||
else
|
||||
export DRS_BUILD_VERSION_OVERRIDE="${{ env.VERSION }}-${GITHUB_SHA:0:7}"
|
||||
fi
|
||||
|
||||
cd app
|
||||
chmod +x ./gradlew
|
||||
./gradlew assembleRelease
|
||||
|
||||
- name: Sign app
|
||||
run: |
|
||||
BUILD_TOOLS=$ANDROID_HOME/build-tools/33.0.0
|
||||
|
||||
echo "${{ secrets.ANDROID_SIGNING_KEYSTORE }}" | base64 --decode > keystore.jks
|
||||
if [ "${{ secrets.ANDROID_SIGNING_KEY_PASS }}" != "" ]; then
|
||||
$BUILD_TOOLS/apksigner sign --ks ./keystore.jks --ks-key-alias "${{ secrets.ANDROID_SIGNING_ALIAS }}" --ks-pass "pass:${{ secrets.ANDROID_SIGNING_KEYSTORE_PASS }}" --key-pass "pass:${{ secrets.ANDROID_SIGNING_KEY_PASS }}" --out $APP_OUTPUTS_DIR/app-signed.apk $APP_OUTPUTS_DIR/app-release-unsigned.apk
|
||||
else
|
||||
$BUILD_TOOLS/apksigner sign --ks ./keystore.jks --ks-key-alias "${{ secrets.ANDROID_SIGNING_ALIAS }}" --ks-pass "pass:${{ secrets.ANDROID_SIGNING_KEYSTORE_PASS }}" --out $APP_OUTPUTS_DIR/app-signed.apk $APP_OUTPUTS_DIR/app-release-unsigned.apk
|
||||
fi
|
||||
|
||||
rm keystore.jks
|
||||
|
||||
- name: Prepare artifact
|
||||
run: |
|
||||
mkdir release
|
||||
mv $APP_OUTPUTS_DIR/app-signed.apk release/doukutsu-rs.apk
|
||||
cp LICENSE ./release
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: doukutsu-rs_android
|
||||
path: ./release/*
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Save cache
|
||||
if: ${{ github.ref_name == 'master' || github.ref_type == 'tag' }}
|
||||
uses: actions/cache/save@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cache
|
||||
~/.cargo
|
||||
~/.rustup
|
||||
~/.gradle
|
||||
app/app/.cxx
|
||||
app/app/build
|
||||
drsandroid/target
|
||||
key: android-cargo
|
||||
|
||||
update_metadata:
|
||||
name: Update metadata
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.ref_type != 'tag' && always() }}
|
||||
needs: [build, build_android]
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
repository: doukutsu-rs/metadata
|
||||
token: ${{ secrets.METADATA_USER_TOKEN }}
|
||||
|
||||
- name: Update metadata
|
||||
id: metadata
|
||||
run: |
|
||||
export FILE="./metadata/nightly.json"
|
||||
if [ "${{ github.ref_name }}" == "master" ]; then
|
||||
export VERSION="${{ env.VERSION }}-$((${{ github.run_number }} + 654))"
|
||||
else
|
||||
export VERSION="${{ env.VERSION }}-${GITHUB_SHA:0:7}"
|
||||
fi
|
||||
|
||||
if [ "${{ needs.build.result }}" == "success" ]; then
|
||||
node ./metadata.js --os linux --arch x86_64 --version $VERSION --commit $GITHUB_SHA --link https://nightly.link/doukutsu-rs/doukutsu-rs/actions/runs/${{ github.run_id }}/doukutsu-rs_linux-x64.zip $FILE
|
||||
node ./metadata.js --os windows --arch x86_64 --version $VERSION --commit $GITHUB_SHA --link https://nightly.link/doukutsu-rs/doukutsu-rs/actions/runs/${{ github.run_id }}/doukutsu-rs_windows-x64.zip $FILE
|
||||
node ./metadata.js --os windows --arch i686 --version $VERSION --commit $GITHUB_SHA --link https://nightly.link/doukutsu-rs/doukutsu-rs/actions/runs/${{ github.run_id }}/doukutsu-rs_windows-x32.zip $FILE
|
||||
node ./metadata.js --os macos --arch x64 --version $VERSION --commit $GITHUB_SHA --link https://nightly.link/doukutsu-rs/doukutsu-rs/actions/runs/${{ github.run_id }}/doukutsu-rs_mac-x64.zip $FILE
|
||||
node ./metadata.js --os macos --arch arm64 --version $VERSION --commit $GITHUB_SHA --link https://nightly.link/doukutsu-rs/doukutsu-rs/actions/runs/${{ github.run_id }}/doukutsu-rs_mac-arm64.zip $FILE
|
||||
fi
|
||||
|
||||
if [ "${{ needs.build_android.result }}" == "success" ]; then
|
||||
node ./metadata.js --os android --version $VERSION --commit $GITHUB_SHA --link https://nightly.link/doukutsu-rs/doukutsu-rs/actions/runs/${{ github.run_id }}/doukutsu-rs_android.zip $FILE
|
||||
fi
|
||||
|
||||
echo "file=$FILE" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Upload metadata
|
||||
run: |
|
||||
git config user.name ${{ vars.METADATA_USER_NAME }}
|
||||
git config user.email ${{ vars.METADATA_USER_EMAIL }}
|
||||
|
||||
git add ${{ steps.metadata.outputs.file }}
|
||||
git commit -m "Update nightly builds metadata(CI)"
|
||||
git push
|
|
@ -0,0 +1,46 @@
|
|||
name: Release
|
||||
|
||||
on:
|
||||
release:
|
||||
types:
|
||||
- released
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: bash
|
||||
|
||||
jobs:
|
||||
update_metadata:
|
||||
name: Update metadata
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
repository: doukutsu-rs/metadata
|
||||
token: ${{ secrets.METADATA_USER_TOKEN }}
|
||||
- name: Update metadata
|
||||
id: metadata
|
||||
run: |
|
||||
export VERSION="${{ github.event.release.tag_name }}"
|
||||
export FILE="./metadata/stable.json"
|
||||
|
||||
node ./metadata.js --os windows --arch x86_64 --version $VERSION --link https://github.com/doukutsu-rs/doukutsu-rs/releases/download/$VERSION/doukutsu-rs.windows.x86_64.$VERSION.exe $FILE
|
||||
node ./metadata.js --os windows --arch i686 --version $VERSION --link https://github.com/doukutsu-rs/doukutsu-rs/releases/download/$VERSION/doukutsu-rs.windows.i686.$VERSION.exe $FILE
|
||||
node ./metadata.js --os macos --arch x86_64 --version $VERSION --link https://github.com/doukutsu-rs/doukutsu-rs/releases/download/$VERSION/doukutsu-rs.macos.x86_64.$VERSION.zip $FILE
|
||||
node ./metadata.js --os macos --arch arm64 --version $VERSION --link https://github.com/doukutsu-rs/doukutsu-rs/releases/download/$VERSION/doukutsu-rs.macos.arm64.$VERSION.zip $FILE
|
||||
node ./metadata.js --os linux --arch x86_64 --version $VERSION --link https://github.com/doukutsu-rs/doukutsu-rs/releases/download/$VERSION/doukutsu-rs.linux.x86_64.$VERSION.elf $FILE
|
||||
node ./metadata.js --os android --version $VERSION --link https://github.com/doukutsu-rs/doukutsu-rs/releases/download/$VERSION/doukutsu-rs.android.$VERSION.apk $FILE
|
||||
|
||||
echo "file=$FILE" >> "$GITHUB_OUTPUT"
|
||||
- name: Upload metadata
|
||||
run: |
|
||||
git config user.name ${{ vars.METADATA_USER_NAME }}
|
||||
git config user.email ${{ vars.METADATA_USER_EMAIL }}
|
||||
|
||||
git add ${{ steps.metadata.outputs.file }}
|
||||
git commit -m "Update stable builds metadata(CI)"
|
||||
git push
|
||||
|
||||
|
41
Cargo.toml
41
Cargo.toml
|
@ -1,9 +1,10 @@
|
|||
[package]
|
||||
name = "doukutsu-rs"
|
||||
description = "A re-implementation of Cave Story (Doukutsu Monogatari) engine"
|
||||
version = "0.100.0"
|
||||
version = "0.101.0"
|
||||
authors = ["Alula", "dawnDus"]
|
||||
edition = "2021"
|
||||
rust-version = "1.65"
|
||||
|
||||
[lib]
|
||||
crate-type = ["lib"]
|
||||
|
@ -16,24 +17,28 @@ bench = false
|
|||
required-features = ["exe"]
|
||||
|
||||
[profile.release]
|
||||
lto = "thin"
|
||||
lto = "off"
|
||||
panic = "abort"
|
||||
codegen-units = 256
|
||||
incremental = true
|
||||
split-debuginfo = "packed"
|
||||
|
||||
[profile.dev.package."*"]
|
||||
opt-level = 3
|
||||
overflow-checks = false
|
||||
codegen-units = 256
|
||||
|
||||
[package.metadata.bundle]
|
||||
name = "doukutsu-rs"
|
||||
identifier = "io.github.doukutsu_rs"
|
||||
version = "0.100.0"
|
||||
version = "0.101.0"
|
||||
resources = ["data"]
|
||||
copyright = "Copyright (c) 2020-2022 doukutsu-rs contributors"
|
||||
copyright = "Copyright (c) 2020-2023 doukutsu-rs contributors"
|
||||
category = "Game"
|
||||
osx_minimum_system_version = "10.12"
|
||||
|
||||
[features]
|
||||
default = ["default-base", "backend-sdl", "render-opengl", "exe", "webbrowser"]
|
||||
default = ["default-base", "backend-sdl", "render-opengl", "exe", "webbrowser", "discord-rpc"]
|
||||
default-base = ["ogg-playback"]
|
||||
ogg-playback = ["lewton"]
|
||||
backend-sdl = ["sdl2", "sdl2-sys"]
|
||||
|
@ -41,6 +46,7 @@ backend-glutin = ["winit", "glutin", "render-opengl"]
|
|||
backend-horizon = []
|
||||
render-opengl = []
|
||||
scripting-lua = ["lua-ffi"]
|
||||
discord-rpc = ["discord-rich-presence"]
|
||||
netplay = ["serde_cbor"]
|
||||
editor = []
|
||||
exe = []
|
||||
|
@ -56,11 +62,13 @@ android = []
|
|||
byteorder = "1.4"
|
||||
case_insensitive_hashmap = "1.0.0"
|
||||
chrono = { version = "0.4", default-features = false, features = ["clock", "std"] }
|
||||
#cpal = "0.14"
|
||||
cpal = { git = "https://github.com/doukutsu-rs/cpal", rev = "9d269d8724102404e73a61e9def0c0cbc921b676" }
|
||||
directories = "3"
|
||||
discord-rich-presence = { version = "0.2", optional = true }
|
||||
downcast = "0.11"
|
||||
#glutin = { git = "https://github.com/doukutsu-rs/glutin.git", rev = "8dd457b9adb7dbac7ade337246b6356c784272d9", optional = true, default_features = false, features = ["x11"] }
|
||||
glutin = { version = "0.30", optional = true, default_features = false, features = ["x11"] }
|
||||
encoding_rs = "0.8.33"
|
||||
fern = "0.6.2"
|
||||
glutin = { git = "https://github.com/doukutsu-rs/glutin.git", rev = "2dd95f042e6e090d36f577cbea125560dd99bd27", optional = true, default_features = false, features = ["x11"] }
|
||||
imgui = "0.8"
|
||||
image = { version = "0.24", default-features = false, features = ["png", "bmp"] }
|
||||
itertools = "0.10"
|
||||
|
@ -70,27 +78,27 @@ log = "0.4"
|
|||
lua-ffi = { git = "https://github.com/doukutsu-rs/lua-ffi.git", rev = "e0b2ff5960f7ef9974aa9675cebe4907bee0134f", optional = true }
|
||||
num-derive = "0.3"
|
||||
num-traits = "0.2"
|
||||
open = "3.2"
|
||||
paste = "1.0"
|
||||
pelite = { version = ">=0.9.2", default-features = false, features = ["std"] }
|
||||
sdl2 = { git = "https://github.com/doukutsu-rs/rust-sdl2.git", rev = "95bcf63768abf422527f86da41da910649b9fcc9", optional = true, features = ["unsafe_textures", "bundled", "static-link"] }
|
||||
sdl2-sys = { git = "https://github.com/doukutsu-rs/rust-sdl2.git", rev = "95bcf63768abf422527f86da41da910649b9fcc9", optional = true, features = ["bundled", "static-link"] }
|
||||
sdl2 = { git = "https://github.com/doukutsu-rs/rust-sdl2.git", rev = "f2f1e29a416bcc22f2faf411866db2c8d9536308", optional = true, features = ["unsafe_textures", "bundled", "static-link"] }
|
||||
sdl2-sys = { git = "https://github.com/doukutsu-rs/rust-sdl2.git", rev = "f2f1e29a416bcc22f2faf411866db2c8d9536308", optional = true, features = ["bundled", "static-link"] }
|
||||
rc-box = "1.2.0"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_derive = "1"
|
||||
serde_cbor = { version = "0.11", optional = true }
|
||||
serde_json = "1.0"
|
||||
simple_logger = { version = "1.16", features = ["colors", "threads"] }
|
||||
strum = "0.24"
|
||||
strum_macros = "0.24"
|
||||
# remove and replace when drain_filter is in stable
|
||||
vec_mut_scan = "0.4"
|
||||
webbrowser = { version = "0.8", optional = true }
|
||||
#winit = { git = "https://github.com/alula/winit.git", rev = "6acf76ff192dd8270aaa119b9f35716c03685f9f", optional = true, default_features = false, features = ["x11"] }
|
||||
winit = { version = "0.27", optional = true, default_features = false, features = ["x11"] }
|
||||
webbrowser = { version = "0.8.6", optional = true }
|
||||
winit = { git = "https://github.com/doukutsu-rs/winit.git", rev = "878f206d19af01b0977277929eee5e32667453c0", optional = true, default_features = false, features = ["x11"] }
|
||||
xmltree = "0.10"
|
||||
|
||||
[target.'cfg(not(target_os = "horizon"))'.dependencies]
|
||||
cpal = "0.14"
|
||||
#hack to not link SDL_image on Windows(causes a linker error)
|
||||
[target.'cfg(not(target_os = "windows"))'.dependencies]
|
||||
sdl2 = { git = "https://github.com/doukutsu-rs/rust-sdl2.git", rev = "f2f1e29a416bcc22f2faf411866db2c8d9536308", optional = true, features = ["image", "unsafe_textures", "bundled", "static-link"] }
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
winapi = { version = "0.3", features = ["winuser"] }
|
||||
|
@ -109,5 +117,4 @@ jni = "0.20"
|
|||
|
||||
[target.'cfg(target_os = "horizon")'.dependencies]
|
||||
#deko3d = { path = "./3rdparty/deko3d" }
|
||||
cpal = { git = "https://github.com/doukutsu-rs/cpal", branch = "horizon" }
|
||||
deko3d = { git = "https://github.com/doukutsu-rs/deko3d-rs", branch = "master" }
|
||||
|
|
83
README.md
83
README.md
|
@ -1,24 +1,30 @@
|
|||
![doukutsu-rs](./res/sue_crab_banner_github.png)
|
||||
|
||||
A fully playable re-implementation of Cave Story (Doukutsu Monogatari) engine written
|
||||
A fully playable re-implementation of the Cave Story (Doukutsu Monogatari) engine written
|
||||
in [Rust](https://www.rust-lang.org/).
|
||||
|
||||
[Join the Discord server](https://discord.gg/fbRsNNB)
|
||||
|
||||
![https://ci.appveyor.com/api/projects/status/github/doukutsu-rs/doukutsu-rs](https://ci.appveyor.com/api/projects/status/github/doukutsu-rs/doukutsu-rs)
|
||||
[![CI](https://github.com/doukutsu-rs/doukutsu-rs/actions/workflows/ci.yml/badge.svg?branch=master)](https://nightly.link/doukutsu-rs/doukutsu-rs/workflows/ci/master?preview)
|
||||
|
||||
- [Get nightly builds from AppVeyor](https://ci.appveyor.com/project/alula/doukutsu-rs) (recommended for now, has latest fixes and improvements)
|
||||
- [Get nightly builds](https://nightly.link/doukutsu-rs/doukutsu-rs/workflows/ci/master?preview) (recommended for now, has latest fixes and improvements)
|
||||
|
||||
Permalinks to latest builds from `master` branch:
|
||||
|
||||
- [Windows (x86_64)](https://ci.appveyor.com/api/projects/alula/doukutsu-rs/artifacts/doukutsu-rs_win64.zip?branch=master&job=windows-x64)
|
||||
- [macOS (Intel, 64-bit, 10.14+)](https://ci.appveyor.com/api/projects/alula/doukutsu-rs/artifacts/doukutsu-rs_mac-intel.zip?branch=master&job=mac-x64)
|
||||
- [macOS (Apple M1, 11.0+)](https://ci.appveyor.com/api/projects/alula/doukutsu-rs/artifacts/doukutsu-rs_mac-m1.zip?branch=master&job=mac-arm64)
|
||||
- [Linux (x86_64)](https://ci.appveyor.com/api/projects/alula/doukutsu-rs/artifacts/doukutsu-rs_linux.zip?branch=master&job=linux-x64)
|
||||
- [Windows (64-bit)](https://nightly.link/doukutsu-rs/doukutsu-rs/workflows/ci/master/doukutsu-rs_windows-x64.zip)
|
||||
- [Windows (32-bit)](https://nightly.link/doukutsu-rs/doukutsu-rs/workflows/ci/master/doukutsu-rs_windows-x32.zip)
|
||||
- [macOS (Intel, 64-bit, 10.14+)](https://nightly.link/doukutsu-rs/doukutsu-rs/workflows/ci/master/doukutsu-rs_mac-x64.zip)
|
||||
- [macOS (Apple M1, 11.0+)](https://nightly.link/doukutsu-rs/doukutsu-rs/workflows/ci/master/doukutsu-rs_mac-arm64.zip)
|
||||
- [Linux (64-bit)](https://nightly.link/doukutsu-rs/doukutsu-rs/workflows/ci/master/doukutsu-rs_linux-x64.zip)
|
||||
- [Android (armv7/arm64/x86)](https://nightly.link/doukutsu-rs/doukutsu-rs/workflows/ci/master/doukutsu-rs_android.zip)
|
||||
|
||||
**macOS note:** If you get a `"doukutsu-rs" can't be opened` message, right-click doukutsu-rs.app and click open.
|
||||
- [Get stable/beta builds from GitHub Releases](https://github.com/doukutsu-rs/doukutsu-rs/releases)
|
||||
|
||||
> [!NOTE]
|
||||
> macOS note: If you get a `"doukutsu-rs" can't be opened` message, right-click doukutsu-rs.app and click open.
|
||||
|
||||
- [Get stable/beta builds from GitHub Releases](https://github.com/doukutsu-rs/doukutsu-rs/releases) (Includes Android builds)
|
||||
> [!NOTE]
|
||||
> If you get issues with Epic Games Store version, scroll down for instructions.
|
||||
|
||||
#### Data files
|
||||
|
||||
|
@ -28,6 +34,31 @@ files.
|
|||
doukutsu-rs works fine with freeware data files or [NXEngine(-evo)](https://github.com/nxengine/nxengine-evo) or from a
|
||||
supported copy of [Cave Story+](https://www.nicalis.com/games/cavestory+).
|
||||
|
||||
<details>
|
||||
<summary>How to set up data files on Android</summary>
|
||||
|
||||
If your phone has an app called **"Files"**:
|
||||
|
||||
1. Launch this app.
|
||||
2. Press **☰** on the top left corner.
|
||||
3. Tap on **"doukutsu-rs game data"**.
|
||||
4. Copy your game data files to the opened folder.
|
||||
|
||||
|
||||
If your phone does not have this app:
|
||||
|
||||
1. Install the **"Material Files"** app from *Hai Zhang* and launch it([Google Play](https://play.google.com/store/apps/details?id=me.zhanghai.android.files) | [F-Droid](https://f-droid.org/en/packages/me.zhanghai.android.files/) | [Github Releases](https://github.com/zhanghai/MaterialFiles/releases)).
|
||||
2. Press **☰** on the top left corner.
|
||||
3. Press **"+ Add storage"**.
|
||||
4. In the window that pops up, press **"External storage"**.
|
||||
5. Press **☰** on the top left corner.
|
||||
6. Tap on **"doukutsu-rs game data"**.
|
||||
7. Press the large blue button at the bottom labelled **"USE THIS FOLDER"**.
|
||||
8. Then click on **☰** in the top left corner again and open.
|
||||
9. Tap on **"files"** above **"+ Add storage"**.
|
||||
10. Copy your game data files to the opened folder.
|
||||
</details>
|
||||
|
||||
#### Supported game editions and data file acquisition guides
|
||||
|
||||
**Freeware**
|
||||
|
@ -71,6 +102,15 @@ on 10.15+ anymore), do the following:
|
|||
|
||||
</details>
|
||||
|
||||
> [!WARNING]
|
||||
> **EPIC GAMES STORE VERSION WARNING**
|
||||
>
|
||||
> Nicalis for some reason ships a stray `opengl32.dll` DLL from Windows 7 with the Epic Games Store copies of Cave Story+.
|
||||
>
|
||||
> However as the game is 32-bit and the dll is 64-bit it has no effect on the original version, but as it's a core Windows DLL and doukutsu-rs ships 64-bit builds and uses OpenGL, it's makes the game crash on startup.
|
||||
>
|
||||
> The fix is to simply delete `opengl32.dll`, as it's not used anyway.
|
||||
|
||||
<details>
|
||||
<summary>Epic Games Store</summary>
|
||||
|
||||
|
@ -101,17 +141,18 @@ The archive from Humble Bundle contains the necessary `data` folder, in the same
|
|||
<details>
|
||||
<summary>WiiWare</summary>
|
||||
|
||||
1. [Dump Your WiiWare ``.wad``](https://wii.guide/dump-wads.html)
|
||||
2. [Extract and decompress the ``data`` folder](https://docs.google.com/document/d/1hDNDgNl0cUDlFOQ_BUOq3QCGb7S0xfUxRoob-hfM-DY)
|
||||
Example of a [valid uncompressed ``data`` folder](https://user-images.githubusercontent.com/53099651/159585593-43fead24-b041-48f4-8332-be50d712310d.png)
|
||||
1. [Dump Your WiiWare `.wad`](https://wii.guide/dump-wads.html)
|
||||
2. [Extract and decompress the `data` folder](https://docs.google.com/document/d/1hDNDgNl0cUDlFOQ_BUOq3QCGb7S0xfUxRoob-hfM-DY)
|
||||
Example of a [valid uncompressed `data` folder](https://user-images.githubusercontent.com/53099651/159585593-43fead24-b041-48f4-8332-be50d712310d.png)
|
||||
|
||||
</details>
|
||||
|
||||
**Remastered version (first released in 2017 on Switch)**
|
||||
|
||||
Note that this version is **incompatible** with saves from the original version.
|
||||
|
||||
Interchanging the save files may result in spawning in wrong locations, softlocks, graphical glitches, or other issues.
|
||||
> [!NOTE]
|
||||
> This version is **incompatible** with saves from the original version.
|
||||
>
|
||||
> Interchanging the save files may result in spawning in wrong locations, softlocks, graphical glitches, or other issues.
|
||||
|
||||
<details>
|
||||
<summary>Nintendo Switch</summary>
|
||||
|
@ -119,15 +160,16 @@ Interchanging the save files may result in spawning in wrong locations, softlock
|
|||
Extract the `data` folder (contained in `romfs`) from your console using tool such as [nxdumptool](https://github.com/DarkMatterCore/nxdumptool).
|
||||
|
||||
**Important notes:**
|
||||
|
||||
- doukutsu-rs doesn't rely on the original ROM or executable, you just need the data files, go to `RomFS options` menu to just extract the files to SD card so you don't need to do any extra steps.
|
||||
- Ensure you're dumping the files **with update included** (`Use update/DLC` option), as 1.0 isn't supported.
|
||||
|
||||
**Nintendo Switch homebrew port specific info**
|
||||
|
||||
If you're running the homebrew port (drshorizon.nro) on your Switch, you can avoid the dumping step, doukutsu-rs will
|
||||
If you're running the homebrew port (drshorizon.nro) on your Switch, you can avoid the dumping step, doukutsu-rs will
|
||||
automatically detect and mount the data files if you run it over Cave Story+ in Title Override mode (hold `R` while starting CS+ and launch d-rs from hbmenu).
|
||||
|
||||
You can put your own data files in `/switch/doukutsu-rs/data` directory on SD Card, which will be overlayed over RomFS if
|
||||
You can put your own data files in `/switch/doukutsu-rs/data` directory on SD Card, which will be overlayed over RomFS if
|
||||
you run it in setup described above.
|
||||
|
||||
</details>
|
||||
|
@ -180,9 +222,9 @@ To change, use the control customization menu or edit `doukutsu-rs\data\settings
|
|||
|
||||
![Balcony Switch](https://user-images.githubusercontent.com/53099651/155918810-063c0f06-2d48-485f-8367-6337525deab7.png)
|
||||
|
||||
![Dogs Switch](https://media.discordapp.net/attachments/745322954660905103/947895408196202617/unknown.png)
|
||||
![Dogs Switch](https://github.com/doukutsu-rs/doukutsu-rs/assets/6276139/30ba01ae-375d-4488-98c4-98e3e8c7f187)
|
||||
|
||||
![Almond Switch](https://media.discordapp.net/attachments/745322954660905103/947898268631826492/unknown.png)
|
||||
![Almond Switch](https://github.com/doukutsu-rs/doukutsu-rs/assets/6276139/42d4b6a3-4fc5-4aaf-9535-462c4c484dc7)
|
||||
|
||||
![Hell Switch](https://user-images.githubusercontent.com/53099651/155918602-62268274-c529-41c2-a87e-0c31e7874b94.png)
|
||||
|
||||
|
@ -194,4 +236,5 @@ To change, use the control customization menu or edit `doukutsu-rs\data\settings
|
|||
- [@Daedily](https://twitter.com/Daedliy) - brand artwork (Icon / Banner / Server), screenshots for this guide.
|
||||
- [ggez](https://github.com/ggez/ggez) - parts of it are used in `crate::framework`, notably the VFS code.
|
||||
- [Clownacy](https://github.com/Clownacy) - widescreen camera code.
|
||||
- [LunarLambda for organism](https://gitdab.com/LunarLambda/organism) - used as basis for our Organya playback engine.
|
||||
- [LunarLambda for organism](https://github.com/doukutsu-rs/organism) - used as basis for our Organya playback engine.
|
||||
- [Zoroyoshi](http://z.apps.atjp.jp/k12x10/) - k12x10 font we use as built-in font.
|
||||
|
|
|
@ -231,3 +231,4 @@ fabric.properties
|
|||
!/gradle/wrapper/gradle-wrapper.jar
|
||||
|
||||
# End of https://www.toptal.com/developers/gitignore/api/androidstudio,gradle,android
|
||||
app/release/
|
|
@ -4,21 +4,23 @@ plugins {
|
|||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 30
|
||||
buildToolsVersion "30.0.3"
|
||||
ndkVersion "22.1.7171670"
|
||||
namespace "io.github.doukutsu_rs"
|
||||
compileSdkVersion 33
|
||||
buildToolsVersion "33.0.0"
|
||||
ndkVersion "25.2.9519653"
|
||||
|
||||
defaultConfig {
|
||||
applicationId "io.github.doukutsu_rs"
|
||||
minSdkVersion 24
|
||||
targetSdkVersion 30
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
targetSdkVersion 33
|
||||
versionCode 2
|
||||
versionName "0.101.0"
|
||||
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
ndk {
|
||||
abiFilters 'x86', 'arm64-v8a', 'armeabi-v7a'
|
||||
abiFilters 'arm64-v8a'
|
||||
stl = "c++_shared"
|
||||
}
|
||||
|
||||
externalNativeBuild {
|
||||
|
@ -41,6 +43,15 @@ android {
|
|||
release {
|
||||
minifyEnabled false
|
||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||
|
||||
ndk {
|
||||
abiFilters 'x86', 'arm64-v8a', 'armeabi-v7a'
|
||||
stl = "c++_shared"
|
||||
}
|
||||
}
|
||||
debug {
|
||||
jniDebuggable true
|
||||
renderscriptDebuggable true
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -58,28 +69,28 @@ android {
|
|||
path "src/main/cpp/CMakeLists.txt"
|
||||
}
|
||||
}
|
||||
|
||||
packagingOptions {
|
||||
jniLibs {
|
||||
excludes += "**/dummy.so"
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'com.android.support:support-annotations:28.0.0'
|
||||
implementation 'com.android.support:appcompat-v7:28.0.0'
|
||||
implementation 'com.android.support:design:28.0.0'
|
||||
implementation 'com.android.support.constraint:constraint-layout:2.0.1'
|
||||
implementation 'android.arch.navigation:navigation-fragment:1.0.0'
|
||||
implementation 'android.arch.navigation:navigation-ui:1.0.0'
|
||||
implementation 'androidx.annotation:annotation:1.5.0'
|
||||
implementation 'androidx.appcompat:appcompat:1.6.0'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.1'
|
||||
implementation 'androidx.core:core:1.9.0'
|
||||
implementation 'com.google.android.material:material:1.8.0'
|
||||
}
|
||||
|
||||
println("cargo target: ${project.buildDir.getAbsolutePath()}/rust-target")
|
||||
println("ndk dir: ${android.ndkDirectory}")
|
||||
|
||||
cargoNdk {
|
||||
targets = [
|
||||
"x86",
|
||||
"arm",
|
||||
"arm64"
|
||||
]
|
||||
librariesNames = ["libdrsandroid.so"]
|
||||
//targetDirectory = "${project.buildDir.getAbsolutePath()}/rust-target"
|
||||
module = "../drsandroid/"
|
||||
extraCargoEnv = ["ANDROID_NDK_HOME": android.ndkDirectory]
|
||||
extraCargoBuildArguments = []
|
||||
|
@ -88,9 +99,17 @@ cargoNdk {
|
|||
buildTypes {
|
||||
release {
|
||||
buildType = "release"
|
||||
targets = [
|
||||
"x86",
|
||||
"arm",
|
||||
"arm64"
|
||||
]
|
||||
}
|
||||
debug {
|
||||
buildType = "debug"
|
||||
targets = [
|
||||
"arm64"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,26 +1,59 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="io.github.doukutsu_rs">
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
|
||||
<application android:allowBackup="true" android:extractNativeLibs="true" android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true"
|
||||
<application
|
||||
android:allowBackup="true"
|
||||
android:appCategory="game"
|
||||
android:description="@string/app_description"
|
||||
android:extractNativeLibs="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:isGame="true"
|
||||
android:label="@string/app_name"
|
||||
android:resizeableActivity="false"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.Doukutsurs">
|
||||
<activity android:name=".DownloadActivity" android:label="Download" android:screenOrientation="sensorLandscape"
|
||||
android:theme="@style/Theme.Doukutsurs.NoActionBar"></activity>
|
||||
<activity android:name=".MainActivity" android:configChanges="orientation|keyboardHidden|screenSize"
|
||||
android:label="doukutsu-rs" android:launchMode="standard" android:screenOrientation="sensorLandscape">
|
||||
|
||||
<activity
|
||||
android:name=".MainActivity"
|
||||
android:configChanges="orientation|keyboardHidden|screenSize"
|
||||
android:exported="true"
|
||||
android:label="doukutsu-rs"
|
||||
android:launchMode="standard"
|
||||
android:screenOrientation="sensorLandscape"
|
||||
android:theme="@style/Theme.Doukutsurs.NoActionBar">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data android:name="android.app.lib_name" android:value="drsandroid" />
|
||||
</activity>
|
||||
|
||||
<provider android:name=".DoukutsuDocumentsProvider" android:authorities="${documentsAuthority}"
|
||||
android:exported="true" android:grantUriPermissions="true"
|
||||
<activity
|
||||
android:name=".DownloadActivity"
|
||||
android:label="Download"
|
||||
android:screenOrientation="sensorLandscape"
|
||||
android:theme="@style/Theme.Doukutsurs.NoActionBar" />
|
||||
|
||||
<activity
|
||||
android:name=".GameActivity"
|
||||
android:configChanges="orientation|keyboardHidden|screenSize"
|
||||
android:exported="true"
|
||||
android:launchMode="standard"
|
||||
android:screenOrientation="sensorLandscape">
|
||||
<meta-data
|
||||
android:name="android.app.lib_name"
|
||||
android:value="drsandroid" />
|
||||
</activity>
|
||||
|
||||
<provider
|
||||
android:name=".DoukutsuDocumentsProvider"
|
||||
android:authorities="${documentsAuthority}"
|
||||
android:exported="true"
|
||||
android:grantUriPermissions="true"
|
||||
android:permission="android.permission.MANAGE_DOCUMENTS">
|
||||
<intent-filter>
|
||||
<action android:name="android.content.action.DOCUMENTS_PROVIDER" />
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
# Sets the minimum version of CMake required to build your native library.
|
||||
# This ensures that a certain set of CMake features is available to
|
||||
# your build.
|
||||
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
project(doukutsu-rs)
|
||||
cmake_minimum_required(VERSION 3.18)
|
||||
|
||||
# Copy shared STL files to Android Studio output directory so they can be
|
||||
# packaged in the APK.
|
||||
|
@ -21,7 +21,7 @@ cmake_minimum_required(VERSION 3.10)
|
|||
function(configure_shared_stl lib_path so_base)
|
||||
message("Configuring STL ${so_base} for ${ANDROID_ABI}")
|
||||
configure_file(
|
||||
"${ANDROID_NDK}/sources/cxx-stl/${lib_path}/libs/${ANDROID_ABI}/lib${so_base}.so"
|
||||
"${ANDROID_NDK}/toolchains/llvm/prebuilt/${ANDROID_HOST_TAG}/sysroot/usr/lib/${CMAKE_LIBRARY_ARCHITECTURE}/lib${so_base}.so"
|
||||
"${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/lib${so_base}.so"
|
||||
COPYONLY)
|
||||
endfunction()
|
||||
|
@ -50,4 +50,4 @@ endif()
|
|||
# and CMake builds them for you. When you build your app, Gradle
|
||||
# automatically packages shared libraries with your APK.
|
||||
|
||||
#add_library(dummy SHARED dummy.cpp)
|
||||
add_library(dummy SHARED dummy.cpp)
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
package io.github.doukutsu_rs;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.view.Window;
|
||||
import androidx.core.view.WindowCompat;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
import androidx.core.view.WindowInsetsControllerCompat;
|
||||
|
||||
public class ActivityUtils {
|
||||
public static void hideSystemBars(Activity activity) {
|
||||
Window window = activity.getWindow();
|
||||
WindowInsetsControllerCompat windowInsetsController =
|
||||
WindowCompat.getInsetsController(window, window.getDecorView());
|
||||
windowInsetsController.hide(WindowInsetsCompat.Type.systemBars());
|
||||
}
|
||||
}
|
|
@ -1,21 +1,31 @@
|
|||
package io.github.doukutsu_rs;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.database.Cursor;
|
||||
import android.database.MatrixCursor;
|
||||
import android.database.MatrixCursor.RowBuilder;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.CancellationSignal;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.provider.DocumentsContract;
|
||||
import android.provider.DocumentsContract.Document;
|
||||
import android.provider.DocumentsContract.Path;
|
||||
import android.provider.DocumentsContract.Root;
|
||||
import android.provider.DocumentsProvider;
|
||||
import android.util.Log;
|
||||
import android.webkit.MimeTypeMap;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.util.LinkedList;
|
||||
|
||||
import static android.os.Build.VERSION.SDK_INT;
|
||||
|
||||
public class DoukutsuDocumentsProvider extends DocumentsProvider {
|
||||
private final static String[] DEFAULT_ROOT_PROJECTION =
|
||||
|
@ -54,7 +64,7 @@ public class DoukutsuDocumentsProvider extends DocumentsProvider {
|
|||
row.add(Root.COLUMN_ROOT_ID, id);
|
||||
row.add(Root.COLUMN_ICON, R.mipmap.ic_launcher);
|
||||
row.add(Root.COLUMN_TITLE,
|
||||
getContext().getString(R.string.document_provider_name));
|
||||
getContext().getString(R.string.app_name));
|
||||
row.add(Root.COLUMN_MIME_TYPES, "*/*");
|
||||
row.add(Root.COLUMN_AVAILABLE_BYTES, file.getFreeSpace());
|
||||
row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_IS_CHILD | Root.FLAG_SUPPORTS_CREATE);
|
||||
|
@ -95,7 +105,9 @@ public class DoukutsuDocumentsProvider extends DocumentsProvider {
|
|||
pushFile(result, file);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
result.setNotificationUri(getContext().getContentResolver(), DocumentsContract.buildDocumentUri(BuildConfig.DOCUMENTS_AUTHORITY, parentDocumentId));
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
@ -134,7 +146,16 @@ public class DoukutsuDocumentsProvider extends DocumentsProvider {
|
|||
} catch (IOException e) {
|
||||
throw new FileNotFoundException("Couldn't create file: " + e.getMessage());
|
||||
}
|
||||
|
||||
|
||||
Uri uri = DocumentsContract.buildDocumentUri(BuildConfig.DOCUMENTS_AUTHORITY, file.getParent());
|
||||
|
||||
if (SDK_INT >= Build.VERSION_CODES.R) {
|
||||
getContext().getContentResolver().notifyChange(uri, null, ContentResolver.NOTIFY_INSERT);
|
||||
} else {
|
||||
getContext().getContentResolver().notifyChange(uri, null);
|
||||
}
|
||||
|
||||
|
||||
return file.getAbsolutePath();
|
||||
}
|
||||
|
||||
|
@ -147,8 +168,38 @@ public class DoukutsuDocumentsProvider extends DocumentsProvider {
|
|||
}
|
||||
|
||||
deleteRecursive(file);
|
||||
// todo refresh this shit
|
||||
// getContext().getContentResolver().refresh()
|
||||
|
||||
|
||||
Uri uri = DocumentsContract.buildDocumentUri(BuildConfig.DOCUMENTS_AUTHORITY, file.getParent());
|
||||
|
||||
if (SDK_INT >= Build.VERSION_CODES.R) {
|
||||
getContext().getContentResolver().notifyChange(uri, null, ContentResolver.NOTIFY_DELETE);
|
||||
} else {
|
||||
getContext().getContentResolver().notifyChange(uri, null);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
public Path findDocumentPath(@Nullable String parentDocumentId, String childDocumentId) throws FileNotFoundException {
|
||||
if (parentDocumentId == null) {
|
||||
parentDocumentId = getContext().getFilesDir().getAbsolutePath();
|
||||
}
|
||||
|
||||
File childFile = new File(childDocumentId);
|
||||
if (!childFile.exists()) {
|
||||
throw new FileNotFoundException(childFile.getAbsolutePath()+" doesn't exist");
|
||||
} else if (!isChildDocument(parentDocumentId, childDocumentId)) {
|
||||
throw new FileNotFoundException(childDocumentId+" is not child of "+parentDocumentId);
|
||||
}
|
||||
|
||||
LinkedList<String> path = new LinkedList<>();
|
||||
while (childFile != null && isChildDocument(parentDocumentId, childFile.getAbsolutePath())) {
|
||||
path.addFirst(childFile.getAbsolutePath());
|
||||
childFile = childFile.getParentFile();
|
||||
}
|
||||
|
||||
return new Path(parentDocumentId, path);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -187,11 +238,26 @@ public class DoukutsuDocumentsProvider extends DocumentsProvider {
|
|||
File newPath = new File(file.getParentFile().getAbsolutePath() + "/" + displayName);
|
||||
|
||||
try {
|
||||
Files.move(file.toPath(), newPath.toPath());
|
||||
if (SDK_INT >= Build.VERSION_CODES.O) {
|
||||
Files.move(file.toPath(), newPath.toPath());
|
||||
} else {
|
||||
if (!file.renameTo(newPath)) {
|
||||
throw new IOException("Couldn't rename file: " + file.getAbsolutePath());
|
||||
}
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new FileNotFoundException(e.getMessage());
|
||||
}
|
||||
|
||||
|
||||
Uri uri = DocumentsContract.buildDocumentUri(BuildConfig.DOCUMENTS_AUTHORITY, file.getParent());
|
||||
|
||||
if (SDK_INT >= Build.VERSION_CODES.R) {
|
||||
getContext().getContentResolver().notifyChange(uri, null, ContentResolver.NOTIFY_UPDATE);
|
||||
} else {
|
||||
getContext().getContentResolver().notifyChange(uri, null);
|
||||
}
|
||||
|
||||
return newPath.getAbsolutePath();
|
||||
}
|
||||
|
||||
|
@ -205,8 +271,18 @@ public class DoukutsuDocumentsProvider extends DocumentsProvider {
|
|||
File[] files = file.listFiles();
|
||||
if (files != null) {
|
||||
for (File f : files) {
|
||||
if (!Files.isSymbolicLink(f.toPath())) {
|
||||
deleteRecursive(f);
|
||||
if (SDK_INT >= Build.VERSION_CODES.O) {
|
||||
if (!Files.isSymbolicLink(f.toPath())) {
|
||||
deleteRecursive(f);
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
if (!f.getAbsolutePath().equals(f.getCanonicalPath())) {
|
||||
deleteRecursive(f);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,14 +2,15 @@ package io.github.doukutsu_rs;
|
|||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.os.Handler;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
import java.io.*;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.util.Locale;
|
||||
import java.util.ArrayList;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipInputStream;
|
||||
|
||||
|
@ -18,6 +19,7 @@ public class DownloadActivity extends AppCompatActivity {
|
|||
private ProgressBar progressBar;
|
||||
private DownloadThread downloadThread;
|
||||
private String basePath;
|
||||
private Handler handler = new Handler();
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
|
@ -26,6 +28,8 @@ public class DownloadActivity extends AppCompatActivity {
|
|||
setContentView(R.layout.activity_download);
|
||||
txtProgress = findViewById(R.id.txtProgress);
|
||||
progressBar = findViewById(R.id.progressBar);
|
||||
ActivityUtils.hideSystemBars(this);
|
||||
|
||||
basePath = getFilesDir().getAbsolutePath() + "/";
|
||||
|
||||
downloadThread = new DownloadThread();
|
||||
|
@ -39,23 +43,37 @@ public class DownloadActivity extends AppCompatActivity {
|
|||
}
|
||||
|
||||
private class DownloadThread extends Thread {
|
||||
private static final String DOWNLOAD_URL = "https://www.cavestory.org/downloads/cavestoryen.zip";
|
||||
private final ArrayList<DownloadEntry> urls = new ArrayList<>();
|
||||
|
||||
private final ArrayList<String> filesWhitelist = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
this.filesWhitelist.add("data/");
|
||||
this.filesWhitelist.add("Doukutsu.exe");
|
||||
|
||||
// DON'T SET `true` VALUE FOR TRANSLATIONS
|
||||
this.urls.add(new DownloadEntry(R.string.download_entries_base, "https://www.cavestory.org/downloads/cavestoryen.zip", true));
|
||||
|
||||
for (DownloadEntry entry : this.urls) {
|
||||
this.download(entry);
|
||||
}
|
||||
}
|
||||
|
||||
private void download(DownloadEntry downloadEntry) {
|
||||
HttpURLConnection connection = null;
|
||||
try {
|
||||
URL url = new URL(DOWNLOAD_URL);
|
||||
URL url = new URL(downloadEntry.url);
|
||||
connection = (HttpURLConnection) url.openConnection();
|
||||
connection.connect();
|
||||
|
||||
if (connection.getResponseCode() != HttpURLConnection.HTTP_OK) {
|
||||
throw new IllegalStateException("Bad HTTP response code: " + connection.getResponseCode());
|
||||
throw new IllegalStateException(getString(R.string.download_status_error_http, connection.getResponseCode()));
|
||||
}
|
||||
|
||||
int fileLength = connection.getContentLength();
|
||||
if (fileLength == 0) {
|
||||
progressBar.setIndeterminate(true);
|
||||
handler.post(() -> progressBar.setIndeterminate(true));
|
||||
}
|
||||
|
||||
byte[] zipFile;
|
||||
|
@ -78,10 +96,10 @@ public class DownloadActivity extends AppCompatActivity {
|
|||
if (last + 1000 >= now) {
|
||||
int speed = (int) ((downloaded - downloadedLast) / 1024.0);
|
||||
String text = (fileLength > 0)
|
||||
? String.format(Locale.ENGLISH, "Downloading... %d%% (%d/%d KiB, %d KiB/s)", downloaded * 100 / fileLength, downloaded / 1024, fileLength / 1024, speed)
|
||||
: String.format(Locale.ENGLISH, "Downloading... --%% (%d KiB, %d KiB/s)", downloaded / 1024, speed);
|
||||
? getString(R.string.download_status_downloading, downloadEntry.name, downloaded * 100 / fileLength, downloaded / 1024, fileLength / 1024, speed)
|
||||
: getString(R.string.download_status_downloading_null, downloadEntry.name, downloaded / 1024, speed);
|
||||
|
||||
txtProgress.setText(text);
|
||||
handler.post(() -> txtProgress.setText(text));
|
||||
|
||||
downloadedLast = downloaded;
|
||||
last = now;
|
||||
|
@ -94,46 +112,90 @@ public class DownloadActivity extends AppCompatActivity {
|
|||
}
|
||||
|
||||
new File(basePath).mkdirs();
|
||||
try (ZipInputStream in = new ZipInputStream(new ByteArrayInputStream(zipFile))) {
|
||||
ZipEntry entry;
|
||||
byte[] buffer = new byte[4096];
|
||||
while ((entry = in.getNextEntry()) != null) {
|
||||
String entryName = entry.getName();
|
||||
this.unpack(zipFile, downloadEntry.isBase);
|
||||
|
||||
// strip prefix
|
||||
if (entryName.startsWith("CaveStory/")) {
|
||||
entryName = entryName.substring("CaveStory/".length());
|
||||
}
|
||||
handler.post(() -> txtProgress.setText(getString(R.string.download_status_done)));
|
||||
|
||||
txtProgress.setText("Unpacking: " + entryName);
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
new File(basePath + entryName).mkdirs();
|
||||
} else {
|
||||
try (FileOutputStream fos = new FileOutputStream(basePath + entryName)) {
|
||||
int count;
|
||||
while ((count = in.read(buffer)) != -1) {
|
||||
fos.write(buffer, 0, count);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
in.closeEntry();
|
||||
}
|
||||
}
|
||||
|
||||
txtProgress.setText("Done!");
|
||||
|
||||
Intent intent = new Intent(DownloadActivity.this, MainActivity.class);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_TASK_ON_HOME);
|
||||
startActivity(intent);
|
||||
DownloadActivity.this.finish();
|
||||
handler.post(() -> {
|
||||
Intent intent = new Intent(DownloadActivity.this, GameActivity.class);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
startActivity(intent);
|
||||
DownloadActivity.this.finish();
|
||||
});
|
||||
} catch (Exception e) {
|
||||
if (txtProgress != null) txtProgress.setText(e.getMessage());
|
||||
handler.post(() -> {
|
||||
if (txtProgress != null)
|
||||
txtProgress.setText(e.getMessage());
|
||||
});
|
||||
e.printStackTrace();
|
||||
} finally {
|
||||
if (connection != null) connection.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
private void unpack(byte[] zipFile, boolean isBase) throws IOException {
|
||||
ZipInputStream in = new ZipInputStream(new ByteArrayInputStream(zipFile));
|
||||
ZipEntry entry;
|
||||
byte[] buffer = new byte[4096];
|
||||
while ((entry = in.getNextEntry()) != null) {
|
||||
String entryName = entry.getName();
|
||||
|
||||
// strip prefix
|
||||
if (entryName.startsWith("CaveStory/")) {
|
||||
entryName = entryName.substring("CaveStory/".length());
|
||||
}
|
||||
|
||||
if (!this.entryInWhitelist(entryName)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
final String s = entryName;
|
||||
handler.post(() -> txtProgress.setText(
|
||||
getString(R.string.download_status_unpacking, s)
|
||||
));
|
||||
|
||||
if (entry.isDirectory()) {
|
||||
new File(basePath + entryName).mkdirs();
|
||||
} else {
|
||||
try (FileOutputStream fos = new FileOutputStream(basePath + entryName)) {
|
||||
int count;
|
||||
while ((count = in.read(buffer)) != -1) {
|
||||
fos.write(buffer, 0, count);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
in.closeEntry();
|
||||
}
|
||||
}
|
||||
|
||||
private boolean entryInWhitelist(String entry) {
|
||||
for (String file : this.filesWhitelist) {
|
||||
if (entry.startsWith(file)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private class DownloadEntry {
|
||||
public String name; //e.g. "Polish translation", "Base data files"
|
||||
public String url;
|
||||
public boolean isBase = false;
|
||||
|
||||
DownloadEntry(String name, String url, boolean isBase) {
|
||||
this.name = name;
|
||||
this.url = url;
|
||||
this.isBase = isBase;
|
||||
}
|
||||
|
||||
DownloadEntry(int name, String url, boolean isBase) {
|
||||
this.name = getString(name);
|
||||
this.url = url;
|
||||
this.isBase = isBase;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,118 @@
|
|||
package io.github.doukutsu_rs;
|
||||
|
||||
import android.app.NativeActivity;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Configuration;
|
||||
import android.hardware.SensorManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.provider.DocumentsContract;
|
||||
import android.view.OrientationEventListener;
|
||||
import android.view.WindowInsets;
|
||||
import android.widget.Toast;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import static android.os.Build.VERSION.SDK_INT;
|
||||
|
||||
public class GameActivity extends NativeActivity {
|
||||
private int[] displayInsets = new int[]{0, 0, 0, 0};
|
||||
private OrientationEventListener listener;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
ActivityUtils.hideSystemBars(this);
|
||||
|
||||
listener = new OrientationEventListener(this, SensorManager.SENSOR_DELAY_UI) {
|
||||
@Override
|
||||
public void onOrientationChanged(int orientation) {
|
||||
GameActivity.this.updateCutouts();
|
||||
}
|
||||
};
|
||||
|
||||
if (listener.canDetectOrientation()) {
|
||||
listener.enable();
|
||||
} else {
|
||||
listener = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
|
||||
if (listener != null) {
|
||||
listener.disable();
|
||||
|
||||
listener = null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttachedToWindow() {
|
||||
super.onAttachedToWindow();
|
||||
|
||||
this.updateCutouts();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
|
||||
this.updateCutouts();
|
||||
}
|
||||
|
||||
private void updateCutouts() {
|
||||
this.displayInsets[0] = 0;
|
||||
this.displayInsets[1] = 0;
|
||||
this.displayInsets[2] = 0;
|
||||
this.displayInsets[3] = 0;
|
||||
|
||||
WindowInsets insets = getWindow().getDecorView().getRootWindowInsets();
|
||||
|
||||
if (insets != null) {
|
||||
this.displayInsets[0] = Math.max(this.displayInsets[0], insets.getStableInsetLeft());
|
||||
this.displayInsets[1] = Math.max(this.displayInsets[1], insets.getStableInsetTop());
|
||||
this.displayInsets[2] = Math.max(this.displayInsets[2], insets.getStableInsetRight());
|
||||
this.displayInsets[3] = Math.max(this.displayInsets[3], insets.getStableInsetBottom());
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
if (SDK_INT >= Build.VERSION_CODES.P) {
|
||||
android.view.DisplayCutout cutout = insets.getDisplayCutout();
|
||||
|
||||
if (cutout != null) {
|
||||
this.displayInsets[0] = Math.max(this.displayInsets[0], cutout.getSafeInsetLeft());
|
||||
this.displayInsets[1] = Math.max(this.displayInsets[0], cutout.getSafeInsetTop());
|
||||
this.displayInsets[2] = Math.max(this.displayInsets[0], cutout.getSafeInsetRight());
|
||||
this.displayInsets[3] = Math.max(this.displayInsets[0], cutout.getSafeInsetBottom());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void openDir(String path) {
|
||||
Uri uri = DocumentsContract.buildDocumentUri(BuildConfig.DOCUMENTS_AUTHORITY, path);
|
||||
|
||||
File file = new File(path);
|
||||
if (!file.isDirectory()) {
|
||||
Toast.makeText(getApplicationContext(), R.string.dir_not_found, Toast.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW);
|
||||
intent.addCategory(Intent.CATEGORY_DEFAULT);
|
||||
intent.setDataAndType(uri, DocumentsContract.Document.MIME_TYPE_DIR);
|
||||
intent.setFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION | Intent.FLAG_GRANT_PREFIX_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
|
||||
|
||||
try {
|
||||
startActivity(intent);
|
||||
} catch(ActivityNotFoundException e) {
|
||||
Toast.makeText(getApplicationContext(), R.string.no_app_found_to_open_dir, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,118 +1,50 @@
|
|||
package io.github.doukutsu_rs;
|
||||
|
||||
import android.app.AlertDialog;
|
||||
import android.app.NativeActivity;
|
||||
import android.content.Intent;
|
||||
import android.content.res.Configuration;
|
||||
import android.hardware.SensorManager;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.view.OrientationEventListener;
|
||||
import android.view.WindowInsets;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import static android.os.Build.VERSION.SDK_INT;
|
||||
|
||||
public class MainActivity extends NativeActivity {
|
||||
private int[] displayInsets = new int[]{0, 0, 0, 0};
|
||||
private OrientationEventListener listener;
|
||||
public class MainActivity extends AppCompatActivity {
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
setContentView(R.layout.activity_main);
|
||||
|
||||
ActivityUtils.hideSystemBars(this);
|
||||
|
||||
File f = new File(getFilesDir().getAbsolutePath() + "/data/");
|
||||
String[] list = f.list();
|
||||
if (!f.exists() || (list != null && list.length == 0)) {
|
||||
messageBox("Missing data files", "No data files found, would you like to download them?", () -> {
|
||||
messageBox(getString(R.string.missing_data_title), getString(R.string.missing_data_desc), () -> {
|
||||
Intent intent = new Intent(this, DownloadActivity.class);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_TASK_ON_HOME);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
startActivity(intent);
|
||||
this.finish();
|
||||
});
|
||||
}
|
||||
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
listener = new OrientationEventListener(this, SensorManager.SENSOR_DELAY_UI) {
|
||||
@Override
|
||||
public void onOrientationChanged(int orientation) {
|
||||
MainActivity.this.updateCutouts();
|
||||
}
|
||||
};
|
||||
|
||||
if (listener.canDetectOrientation()) {
|
||||
listener.enable();
|
||||
}, this::launchGame);
|
||||
} else {
|
||||
listener = null;
|
||||
launchGame();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
|
||||
if (listener != null) {
|
||||
listener.disable();
|
||||
|
||||
listener = null;
|
||||
}
|
||||
private void launchGame() {
|
||||
Intent intent = new Intent(this, GameActivity.class);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
startActivity(intent);
|
||||
this.finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAttachedToWindow() {
|
||||
super.onAttachedToWindow();
|
||||
|
||||
this.updateCutouts();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(Configuration newConfig) {
|
||||
super.onConfigurationChanged(newConfig);
|
||||
|
||||
this.updateCutouts();
|
||||
}
|
||||
|
||||
private void updateCutouts() {
|
||||
this.displayInsets[0] = 0;
|
||||
this.displayInsets[1] = 0;
|
||||
this.displayInsets[2] = 0;
|
||||
this.displayInsets[3] = 0;
|
||||
|
||||
WindowInsets insets = getWindow().getDecorView().getRootWindowInsets();
|
||||
|
||||
if (insets != null) {
|
||||
this.displayInsets[0] = Math.max(this.displayInsets[0], insets.getStableInsetLeft());
|
||||
this.displayInsets[1] = Math.max(this.displayInsets[1], insets.getStableInsetTop());
|
||||
this.displayInsets[2] = Math.max(this.displayInsets[2], insets.getStableInsetRight());
|
||||
this.displayInsets[3] = Math.max(this.displayInsets[3], insets.getStableInsetBottom());
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
if (SDK_INT >= Build.VERSION_CODES.P) {
|
||||
android.view.DisplayCutout cutout = insets.getDisplayCutout();
|
||||
|
||||
if (cutout != null) {
|
||||
this.displayInsets[0] = Math.max(this.displayInsets[0], cutout.getSafeInsetLeft());
|
||||
this.displayInsets[1] = Math.max(this.displayInsets[0], cutout.getSafeInsetTop());
|
||||
this.displayInsets[2] = Math.max(this.displayInsets[0], cutout.getSafeInsetRight());
|
||||
this.displayInsets[3] = Math.max(this.displayInsets[0], cutout.getSafeInsetBottom());
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private void messageBox(String title, String message, Runnable callback) {
|
||||
private void messageBox(String title, String message, Runnable yesCallback, Runnable noCallback) {
|
||||
this.runOnUiThread(() -> {
|
||||
AlertDialog.Builder alert = new AlertDialog.Builder(this);
|
||||
alert.setTitle(title);
|
||||
alert.setMessage(message);
|
||||
alert.setPositiveButton(android.R.string.yes, (dialog, whichButton) -> {
|
||||
callback.run();
|
||||
});
|
||||
alert.setNegativeButton(android.R.string.no, (dialog, whichButton) -> {
|
||||
// hide
|
||||
});
|
||||
alert.setPositiveButton(android.R.string.yes, (dialog, whichButton) -> yesCallback.run());
|
||||
alert.setNegativeButton(android.R.string.no, (dialog, whichButton) -> noCallback.run());
|
||||
alert.setCancelable(false);
|
||||
alert.show();
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
|
@ -21,7 +22,7 @@
|
|||
android:id="@+id/txtTitle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Downloading game data"
|
||||
android:text="@string/download_title"
|
||||
android:textAlignment="center"
|
||||
android:textAppearance="@style/TextAppearance.AppCompat.Display1" />
|
||||
|
||||
|
@ -39,4 +40,4 @@
|
|||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
</LinearLayout>
|
||||
</android.support.constraint.ConstraintLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
tools:context=".MainActivity">
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -1,4 +1,21 @@
|
|||
<resources>
|
||||
<string name="app_name">doukutsu-rs</string>
|
||||
<string name="document_provider_name">doukutsu-rs game data</string>
|
||||
<string name="app_name" translatable="false">doukutsu-rs</string>
|
||||
<string name="app_description">A faithful and open-source remake of Cave Story engine written in Rust.</string>
|
||||
|
||||
<string name="missing_data_title">Missing data files</string>
|
||||
<string name="missing_data_desc">No data files found, would you like to download them?</string>
|
||||
|
||||
<string name="download_title">Downloading game data</string>
|
||||
<string name="download_status_error_http">Bad HTTP response code: %d</string>
|
||||
<!-- Downloading {entry_name}… {progress}% ({downloaded_size}/{total_size} KiB, {speed} KiB/s) -->
|
||||
<string name="download_status_downloading">Downloading %1$s… %2$d%% (%3$d/%4$d KiB, %5$d KiB/s)</string>
|
||||
<string name="download_status_downloading_null">Downloading %1$s… --%% (%2$d KiB, %3$d KiB/s)</string>
|
||||
<string name="download_status_unpacking">Unpacking: %s</string>
|
||||
<string name="download_status_done">Done!</string>
|
||||
|
||||
<!-- Look {entry_name} on 9th line -->
|
||||
<string name="download_entries_base">base game files</string>
|
||||
|
||||
<string name="dir_not_found">Dir not found</string>
|
||||
<string name="no_app_found_to_open_dir">No app found to open dir</string>
|
||||
</resources>
|
||||
|
|
|
@ -2,13 +2,13 @@
|
|||
buildscript {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
maven {
|
||||
url "https://plugins.gradle.org/m2/"
|
||||
}
|
||||
}
|
||||
dependencies {
|
||||
classpath "com.android.tools.build:gradle:4.2.0"
|
||||
classpath "com.android.tools.build:gradle:7.3.1"
|
||||
classpath "gradle.plugin.com.github.willir.rust:plugin:0.3.4"
|
||||
|
||||
// NOTE: Do not place your application dependencies here; they belong
|
||||
|
@ -19,7 +19,7 @@ buildscript {
|
|||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
#Wed Feb 17 23:16:31 CET 2021
|
||||
distributionBase=GRADLE_USER_HOME
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-bin.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
|
||||
distributionPath=wrapper/dists
|
||||
zipStorePath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
|
|
6
build.rs
6
build.rs
|
@ -15,6 +15,12 @@ fn main() {
|
|||
let mut res = winres::WindowsResource::new();
|
||||
res.set_icon("res/sue.ico");
|
||||
res.compile().unwrap();
|
||||
|
||||
if target.contains("i686") {
|
||||
// yet another hack
|
||||
println!("cargo:rustc-link-arg=/FORCE:MULTIPLE");
|
||||
println!("cargo:rustc-link-lib=shlwapi");
|
||||
}
|
||||
}
|
||||
|
||||
if target.contains("darwin") {
|
||||
|
|
|
@ -4,6 +4,12 @@ description = "doukutsu-rs targeted for Android"
|
|||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[profile.release]
|
||||
opt-level = 3
|
||||
lto = "off"
|
||||
codegen-units = 256
|
||||
incremental = true
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
|
|
|
@ -1,7 +1,11 @@
|
|||
#[cfg(target_os = "android")]
|
||||
#[cfg_attr(target_os = "android", ndk_glue::main())]
|
||||
pub fn android_main() {
|
||||
let resource_dir = std::path::PathBuf::from(ndk_glue::native_activity().internal_data_path().to_string_lossy().to_string());
|
||||
|
||||
std::env::set_current_dir(&resource_dir).unwrap();
|
||||
|
||||
let options = doukutsu_rs::game::LaunchOptions { server_mode: false, editor: false };
|
||||
|
||||
doukutsu_rs::init(options).unwrap();
|
||||
doukutsu_rs::game::init(options).unwrap();
|
||||
}
|
||||
|
|
|
@ -6,15 +6,25 @@ edition = "2021"
|
|||
|
||||
[profile.release]
|
||||
opt-level = 3
|
||||
#incremental = true
|
||||
codegen-units = 256
|
||||
incremental = true
|
||||
|
||||
[profile.dev]
|
||||
opt-level = 3
|
||||
lto = "off"
|
||||
overflow-checks = false
|
||||
codegen-units = 256
|
||||
incremental = true
|
||||
|
||||
[profile.dev.package."*"]
|
||||
opt-level = 3
|
||||
overflow-checks = false
|
||||
incremental = true
|
||||
|
||||
[profile.dev.package."doukutsu-rs"]
|
||||
opt-level = 3
|
||||
overflow-checks = false
|
||||
codegen-units = 256
|
||||
incremental = true
|
||||
|
||||
[dependencies]
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
Experimental. Nothing to see there yet.
|
||||
Experimental.
|
||||
|
||||
ld script and .specs taken from devkitPro
|
||||
|
|
|
@ -23,7 +23,7 @@ rm -f target/aarch64-nintendo-switch/debug/drshorizon.nro
|
|||
rm -f target/aarch64-nintendo-switch/debug/drshorizon.nacp
|
||||
|
||||
message "Creating NACP..."
|
||||
nacptool --create 'doukutsu-rs' 'doukutsu-rs contributors' '0.100.0' target/aarch64-nintendo-switch/debug/drshorizon.nacp
|
||||
nacptool --create 'doukutsu-rs' 'doukutsu-rs contributors' '0.101.0' target/aarch64-nintendo-switch/debug/drshorizon.nacp
|
||||
|
||||
message "Running elf2nro..."
|
||||
elf2nro target/aarch64-nintendo-switch/debug/drshorizon.elf target/aarch64-nintendo-switch/debug/drshorizon.nro \
|
||||
|
|
|
@ -23,7 +23,7 @@ rm -f target/aarch64-nintendo-switch/release/drshorizon.nro
|
|||
rm -f target/aarch64-nintendo-switch/release/drshorizon.nacp
|
||||
|
||||
message "Creating NACP..."
|
||||
nacptool --create 'doukutsu-rs' 'doukutsu-rs contributors' '0.100.0' target/aarch64-nintendo-switch/release/drshorizon.nacp
|
||||
nacptool --create 'doukutsu-rs' 'doukutsu-rs contributors' '0.101.0' target/aarch64-nintendo-switch/release/drshorizon.nacp
|
||||
|
||||
message "Running elf2nro..."
|
||||
elf2nro target/aarch64-nintendo-switch/release/drshorizon.elf target/aarch64-nintendo-switch/release/drshorizon.nro \
|
||||
|
|
|
@ -24,11 +24,11 @@ extern "C" {
|
|||
|
||||
fn main() {
|
||||
unsafe {
|
||||
if socketInitialize(std::ptr::null()) == 0 {
|
||||
nxlinkConnectToHost(true, true);
|
||||
}
|
||||
// if socketInitialize(std::ptr::null()) == 0 {
|
||||
// nxlinkConnectToHost(true, true);
|
||||
// }
|
||||
|
||||
appletSetCpuBoostMode(ApmCpuBoostMode::FastLoad);
|
||||
// appletSetCpuBoostMode(ApmCpuBoostMode::FastLoad);
|
||||
|
||||
std::env::set_var("RUST_BACKTRACE", "full");
|
||||
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
curl -o ./src/data/builtin/gamecontrollerdb.txt https://raw.githubusercontent.com/mdqinc/SDL_GameControllerDB/master/gamecontrollerdb.txt
|
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
|
@ -47,7 +47,7 @@ bitfield! {
|
|||
pub in_water, set_in_water: 8;
|
||||
pub weapon_hit_block, set_weapon_hit_block: 9; // 0x200
|
||||
pub hit_by_spike, set_hit_by_spike: 10; // 0x400
|
||||
pub water_splash_facing_right, set_water_splash_facing_right: 11; // 0x800
|
||||
pub bloody_droplets, set_bloody_droplets: 11; // 0x800
|
||||
pub force_left, set_force_left: 12; // 0x1000
|
||||
pub force_up, set_force_up: 13; // 0x2000
|
||||
pub force_right, set_force_right: 14; // 0x4000
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
use crate::entity::GameEntity;
|
||||
use crate::framework::context::Context;
|
||||
use crate::framework::error::GameResult;
|
||||
use crate::game::frame::Frame;
|
||||
use crate::game::shared_game_state::SharedGameState;
|
||||
use crate::graphics::font::Font;
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct CompactJukebox {
|
||||
song_id: usize,
|
||||
shown: bool,
|
||||
}
|
||||
|
||||
impl CompactJukebox {
|
||||
pub fn new() -> CompactJukebox {
|
||||
CompactJukebox { song_id: 0, shown: false }
|
||||
}
|
||||
|
||||
pub fn change_song(&mut self, song_id: usize, state: &mut SharedGameState, ctx: &mut Context) -> GameResult {
|
||||
self.song_id = song_id;
|
||||
|
||||
if self.song_id == state.sound_manager.current_song() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
return state.sound_manager.play_song(song_id, &state.constants, &state.settings, ctx, false);
|
||||
}
|
||||
|
||||
pub fn next_song(&mut self, state: &mut SharedGameState, ctx: &mut Context) -> GameResult {
|
||||
let mut new_song_id = if self.song_id == state.constants.music_table.len() - 1 { 1 } else { self.song_id + 1 };
|
||||
|
||||
// skip ika if soundtrack is not set to new
|
||||
if self.is_ika_unavailable(new_song_id, state) {
|
||||
new_song_id = 1;
|
||||
}
|
||||
|
||||
self.change_song(new_song_id, state, ctx)
|
||||
}
|
||||
|
||||
pub fn prev_song(&mut self, state: &mut SharedGameState, ctx: &mut Context) -> GameResult {
|
||||
let mut new_song_id = if self.song_id == 1 { state.constants.music_table.len() - 1 } else { self.song_id - 1 };
|
||||
|
||||
// skip ika if soundtrack is not set to new
|
||||
if self.is_ika_unavailable(new_song_id, state) {
|
||||
new_song_id = 42;
|
||||
}
|
||||
|
||||
self.change_song(new_song_id, state, ctx)
|
||||
}
|
||||
|
||||
pub fn show(&mut self) {
|
||||
self.shown = true;
|
||||
}
|
||||
|
||||
pub fn is_shown(&self) -> bool {
|
||||
self.shown
|
||||
}
|
||||
|
||||
fn is_ika_unavailable(&self, song_id: usize, state: &SharedGameState) -> bool {
|
||||
song_id == 43 && state.settings.soundtrack != "New"
|
||||
}
|
||||
}
|
||||
|
||||
impl GameEntity<&mut Context> for CompactJukebox {
|
||||
fn tick(&mut self, _state: &mut SharedGameState, _ctx: &mut Context) -> GameResult {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw(&self, state: &mut SharedGameState, ctx: &mut Context, _frame: &Frame) -> GameResult {
|
||||
if !self.shown {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let text = format!("< {:02} >", self.song_id);
|
||||
|
||||
let font_builder = state.font.builder();
|
||||
let text_width = font_builder.compute_width(&text);
|
||||
|
||||
let x = state.canvas_size.0 as f32 - text_width - 15.0;
|
||||
let y = state.canvas_size.1 as f32 - 15.0;
|
||||
|
||||
font_builder.x(x).y(y).shadow(true).draw(&text, ctx, &state.constants, &mut state.texture_set)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -140,24 +140,36 @@ impl GameEntity<(&mut Context, &mut Player, &mut Inventory, &mut HUD)> for Inven
|
|||
InventoryFocus::None => {
|
||||
self.focus = InventoryFocus::Weapons;
|
||||
state.control_flags.set_ok_button_disabled(false);
|
||||
state.textscript_vm.start_script(self.get_weapon_event_number(inventory));
|
||||
// check weapon count (0 count means we run item script)
|
||||
let event = if self.weapon_count > 0 {
|
||||
self.get_weapon_event_number(inventory)
|
||||
} else {
|
||||
self.get_item_event_number(inventory)
|
||||
};
|
||||
state.textscript_vm.start_script(event);
|
||||
}
|
||||
InventoryFocus::Weapons if state.control_flags.control_enabled() => {
|
||||
if player.controller.trigger_left() {
|
||||
state.sound_manager.play_sfx(4);
|
||||
inventory.prev_weapon();
|
||||
state.control_flags.set_ok_button_disabled(false);
|
||||
state.textscript_vm.start_script(self.get_weapon_event_number(inventory));
|
||||
|
||||
// if we have no weapons, the TSC should not be refreshed with L/R keystrokes
|
||||
if self.weapon_count > 0
|
||||
{
|
||||
if player.controller.trigger_left() {
|
||||
state.sound_manager.play_sfx(4);
|
||||
inventory.prev_weapon();
|
||||
state.control_flags.set_ok_button_disabled(false);
|
||||
state.textscript_vm.start_script(self.get_weapon_event_number(inventory));
|
||||
}
|
||||
|
||||
if player.controller.trigger_right() {
|
||||
state.sound_manager.play_sfx(4);
|
||||
inventory.next_weapon();
|
||||
state.control_flags.set_ok_button_disabled(false);
|
||||
state.textscript_vm.start_script(self.get_weapon_event_number(inventory));
|
||||
}
|
||||
}
|
||||
|
||||
if player.controller.trigger_right() {
|
||||
state.sound_manager.play_sfx(4);
|
||||
inventory.next_weapon();
|
||||
state.control_flags.set_ok_button_disabled(false);
|
||||
state.textscript_vm.start_script(self.get_weapon_event_number(inventory));
|
||||
}
|
||||
|
||||
if player.controller.trigger_up() || player.controller.trigger_down() {
|
||||
// we should not move from the weapon row if there are no items
|
||||
if (player.controller.trigger_up() || player.controller.trigger_down()) && self.item_count > 0 {
|
||||
self.focus = InventoryFocus::Items;
|
||||
state.control_flags.set_ok_button_disabled(false);
|
||||
state.textscript_vm.start_script(self.get_item_event_number(inventory));
|
||||
|
@ -310,8 +322,8 @@ impl GameEntity<(&mut Context, &mut Player, &mut Inventory, &mut HUD)> for Inven
|
|||
|
||||
let (item_cursor_frame, weapon_cursor_frame) = match self.focus {
|
||||
InventoryFocus::None => (1, 1),
|
||||
InventoryFocus::Weapons => (1, self.tick & 1),
|
||||
InventoryFocus::Items => (self.tick & 1, 1),
|
||||
InventoryFocus::Weapons => (1, (self.tick / 2) % 2), //every-other frame (& 1): this is not what we want, we want every 2 frames.
|
||||
InventoryFocus::Items => ((self.tick / 2) % 2, 1),
|
||||
};
|
||||
|
||||
batch.add_rect(
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
pub mod background;
|
||||
pub mod boss_life_bar;
|
||||
pub mod compact_jukebox;
|
||||
pub mod credits;
|
||||
pub mod draw_common;
|
||||
pub mod fade;
|
||||
|
|
|
@ -13,20 +13,15 @@ pub struct NumberPopup {
|
|||
pub prev_x: i32,
|
||||
pub prev_y: i32,
|
||||
counter: u16,
|
||||
throttle: u16,
|
||||
value_display: i16,
|
||||
}
|
||||
|
||||
impl NumberPopup {
|
||||
pub fn new() -> NumberPopup {
|
||||
NumberPopup { value: 0, x: 0, y: 0, prev_x: 0, prev_y: 0, counter: 0, throttle: 0, value_display: 0 }
|
||||
NumberPopup { value: 0, x: 0, y: 0, prev_x: 0, prev_y: 0, counter: 0, value_display: 0 }
|
||||
}
|
||||
|
||||
pub fn set_value(&mut self, value: i16) {
|
||||
if self.counter > 32 {
|
||||
self.counter = 32;
|
||||
}
|
||||
|
||||
self.value = value;
|
||||
}
|
||||
|
||||
|
@ -34,32 +29,24 @@ impl NumberPopup {
|
|||
self.set_value(self.value + value);
|
||||
}
|
||||
|
||||
pub fn set_value_throttled(&mut self, value: i16) {
|
||||
self.throttle = 16;
|
||||
self.set_value(value);
|
||||
}
|
||||
|
||||
pub fn add_value_throttled(&mut self, value: i16) {
|
||||
self.set_value_throttled(self.value + value);
|
||||
pub fn update_displayed_value(&mut self) {
|
||||
if self.counter > 32 {
|
||||
self.counter = 32;
|
||||
}
|
||||
self.value_display += self.value;
|
||||
self.value = 0;
|
||||
}
|
||||
}
|
||||
|
||||
impl GameEntity<()> for NumberPopup {
|
||||
fn tick(&mut self, _state: &mut SharedGameState, _custom: ()) -> GameResult<()> {
|
||||
if self.throttle > 0 {
|
||||
self.throttle = self.throttle.saturating_sub(1);
|
||||
}
|
||||
|
||||
if self.value == 0 || self.throttle > 0 {
|
||||
if self.value_display == 0 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.value_display = self.value;
|
||||
|
||||
self.counter += 1;
|
||||
if self.counter == 80 {
|
||||
self.counter = 0;
|
||||
self.value = 0;
|
||||
self.value_display = 0;
|
||||
}
|
||||
|
||||
|
@ -81,7 +68,8 @@ impl GameEntity<()> for NumberPopup {
|
|||
|
||||
let (frame_x, frame_y) = frame.xy_interpolated(state.frame_time);
|
||||
let x = interpolate_fix9_scale(self.prev_x, self.x, state.frame_time) - frame_x;
|
||||
let y = interpolate_fix9_scale(self.prev_y, self.y, state.frame_time) - frame_y - y_offset;
|
||||
let y = interpolate_fix9_scale(self.prev_y, self.y, state.frame_time) - frame_y - y_offset
|
||||
- 3.0f32; // This is supposed to be -4, but for some reason -3 looks more accurate
|
||||
|
||||
let n = format!("{:+}", self.value_display);
|
||||
|
||||
|
|
|
@ -40,7 +40,9 @@
|
|||
"title": "Select Difficulty",
|
||||
"easy": "Easy",
|
||||
"normal": "Normal",
|
||||
"hard": "Hard"
|
||||
"hard": "Hard",
|
||||
"difficulty_name": "Difficulty: {difficulty}",
|
||||
"unknown": "(unknown)"
|
||||
},
|
||||
"coop_menu": {
|
||||
"title": "Select Number of Players",
|
||||
|
@ -113,6 +115,9 @@
|
|||
"soundtrack": "Soundtrack: {soundtrack}"
|
||||
},
|
||||
"controls": "Controls...",
|
||||
"controls_menu": {
|
||||
"display_touch_controls": "Display touch controls:"
|
||||
},
|
||||
"language": "Language...",
|
||||
"behavior": "Behavior...",
|
||||
"behavior_menu": {
|
||||
|
@ -125,10 +130,25 @@
|
|||
"cutscene_skip_method": {
|
||||
"entry": "Cutscene Skip:",
|
||||
"hold": "Hold to Skip",
|
||||
"fastforward": "Fast-Forward"
|
||||
}
|
||||
"fastforward": "Fast-Forward",
|
||||
"auto": "Auto"
|
||||
},
|
||||
"discord_rpc": "Discord Rich Presence:",
|
||||
"allow_strafe": "Allow strafe:"
|
||||
},
|
||||
"links": "Links..."
|
||||
"links": "Links...",
|
||||
"advanced": "Advanced...",
|
||||
"advanced_menu": {
|
||||
"open_user_data": "Open user data directory",
|
||||
"open_game_data": "Open game data directory",
|
||||
"make_portable": "Make portable user directory"
|
||||
},
|
||||
"portable_menu": {
|
||||
"explanation": "This will create a local user data directory and copy your settings and save files.",
|
||||
"restart_question": "Reload the game to use the new location?",
|
||||
"restart": "Save and return to title",
|
||||
"cancel": "Cancel"
|
||||
}
|
||||
},
|
||||
"controls_menu": {
|
||||
"select_player": {
|
||||
|
@ -170,7 +190,8 @@
|
|||
"organya": "Organya",
|
||||
"remastered": "Remastered",
|
||||
"new": "New",
|
||||
"famitracks": "Famitracks"
|
||||
"famitracks": "Famitracks",
|
||||
"ridiculon": "Ridiculon"
|
||||
},
|
||||
"game": {
|
||||
"cutscene_skip": "Hold {key} to skip the cutscene"
|
||||
|
|
|
@ -40,7 +40,9 @@
|
|||
"title": "難易度選択",
|
||||
"easy": "簡単",
|
||||
"normal": "普通",
|
||||
"hard": "難しい"
|
||||
"hard": "難しい",
|
||||
"difficulty_name": "難易度: {difficulty}",
|
||||
"unknown": "(未知)"
|
||||
},
|
||||
"coop_menu": {
|
||||
"title": "プレイヤー数を選択",
|
||||
|
@ -113,6 +115,9 @@
|
|||
"soundtrack": "サウンドトラック: {soundtrack}"
|
||||
},
|
||||
"controls": "ボタン変更",
|
||||
"controls_menu": {
|
||||
"display_touch_controls": "タッチコントロールを表示する: "
|
||||
},
|
||||
"language": "言語",
|
||||
"behavior": "動作",
|
||||
"behavior_menu": {
|
||||
|
@ -126,9 +131,23 @@
|
|||
"entry": "カットシーンをスキップ",
|
||||
"hold": "を押し続け",
|
||||
"fastforward": "はやおくり"
|
||||
}
|
||||
},
|
||||
"discord_rpc": "Discord Rich Presence:",
|
||||
"allow_strafe": "ストレイフを許可する:"
|
||||
},
|
||||
"links": "リンク"
|
||||
"links": "リンク",
|
||||
"advanced": "詳細設定",
|
||||
"advanced_menu": {
|
||||
"open_user_data": "ユーザープロファイルを開く",
|
||||
"open_game_data": "ゲームファイルを開く",
|
||||
"make_portable": "ポータブルユーザーディレクトリを作成する"
|
||||
},
|
||||
"portable_menu": {
|
||||
"explanation": "ローカルのユーザーデータディレクトリが作成され、設定とセーブファイルがそこにコピーされます。",
|
||||
"restart_question": "新しい場所を使うには、ゲームを再起動しますか?",
|
||||
"restart": "保存してタイトルに戻る",
|
||||
"cancel": "キャンセル"
|
||||
}
|
||||
},
|
||||
"controls_menu": {
|
||||
"select_player": {
|
||||
|
@ -170,7 +189,8 @@
|
|||
"organya": "オルガーニャ",
|
||||
"remastered": "リマスター",
|
||||
"new": "新",
|
||||
"famitracks": "ファミトラック"
|
||||
"famitracks": "ファミトラック",
|
||||
"ridiculon": "リディキュロン"
|
||||
},
|
||||
"game": {
|
||||
"cutscene_skip": "{key} を押し続け、カットシーンをスキップ"
|
||||
|
|
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
@ -102,6 +102,7 @@ impl BuiltinFS {
|
|||
FSNode::File("builtin_font_0.png", include_bytes!("builtin/builtin_font_0.png")),
|
||||
FSNode::File("builtin_font_1.png", include_bytes!("builtin/builtin_font_1.png")),
|
||||
FSNode::File("gamecontrollerdb.txt", include_bytes!("builtin/gamecontrollerdb.txt")),
|
||||
FSNode::File("icon.bmp", include_bytes!("../../res/sue.bmp")),
|
||||
FSNode::File(
|
||||
"organya-wavetable-doukutsu.bin",
|
||||
include_bytes!("builtin/organya-wavetable-doukutsu.bin"),
|
||||
|
|
|
@ -33,6 +33,7 @@ impl ExeResourceDirectory {
|
|||
}
|
||||
|
||||
pub struct ExeParser<'a> {
|
||||
pub image_base: u32,
|
||||
pub resources: Resources<'a>,
|
||||
pub section_headers: Box<&'a SectionHeaders>,
|
||||
}
|
||||
|
@ -50,8 +51,13 @@ impl<'a> ExeParser<'a> {
|
|||
}
|
||||
|
||||
let section_headers = pe.section_headers();
|
||||
let image_base = pe.nt_headers().OptionalHeader.ImageBase;
|
||||
|
||||
Ok(Self { resources: resources.unwrap(), section_headers: Box::new(section_headers) })
|
||||
Ok(Self {
|
||||
image_base,
|
||||
resources: resources.unwrap(),
|
||||
section_headers: Box::new(section_headers)
|
||||
})
|
||||
}
|
||||
Err(_) => Err(ParseError("Failed to parse PE file".to_string())),
|
||||
};
|
||||
|
|
|
@ -17,23 +17,52 @@ use crate::framework::{
|
|||
pub struct VanillaExtractor {
|
||||
exe_buffer: Vec<u8>,
|
||||
data_base_dir: String,
|
||||
root: PathBuf,
|
||||
}
|
||||
|
||||
const VANILLA_STAGE_COUNT: u32 = 95;
|
||||
const VANILLA_STAGE_ENTRY_SIZE: u32 = 0xC8;
|
||||
const VANILLA_STAGE_OFFSET: u32 = 0x937B0;
|
||||
const VANILLA_STAGE_TABLE_SIZE: u32 = VANILLA_STAGE_COUNT * VANILLA_STAGE_ENTRY_SIZE;
|
||||
|
||||
trait RangeExt {
|
||||
fn to_usize(&self) -> std::ops::Range<usize>;
|
||||
}
|
||||
|
||||
impl RangeExt for Range<u32> {
|
||||
fn to_usize(&self) -> std::ops::Range<usize> {
|
||||
(self.start as usize)..(self.end as usize)
|
||||
}
|
||||
}
|
||||
|
||||
impl VanillaExtractor {
|
||||
pub fn from(ctx: &mut Context, exe_name: String, data_base_dir: String) -> Option<Self> {
|
||||
let mut vanilla_exe_path = env::current_exe().unwrap();
|
||||
vanilla_exe_path.pop();
|
||||
vanilla_exe_path.push(exe_name);
|
||||
#[cfg(not(any(target_os = "android", target_os = "horizon")))]
|
||||
let mut vanilla_exe_path = env::current_dir().unwrap();
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
let mut vanilla_exe_path = PathBuf::from(ndk_glue::native_activity().internal_data_path().to_string_lossy().to_string());
|
||||
|
||||
#[cfg(target_os = "horizon")]
|
||||
let mut vanilla_exe_path = PathBuf::from("sdmc:/switch/doukutsu-rs/");
|
||||
|
||||
vanilla_exe_path.push(&exe_name);
|
||||
|
||||
log::info!("Looking for vanilla game executable at {:?}", vanilla_exe_path);
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "horizon")))]
|
||||
if !vanilla_exe_path.is_file() {
|
||||
vanilla_exe_path = env::current_exe().unwrap();
|
||||
vanilla_exe_path.pop();
|
||||
vanilla_exe_path.push(&exe_name);
|
||||
}
|
||||
|
||||
if !vanilla_exe_path.is_file() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let mut root = vanilla_exe_path.clone();
|
||||
root.pop();
|
||||
|
||||
log::info!("Found vanilla game executable, attempting to extract resources.");
|
||||
|
||||
if filesystem::exists(ctx, format!("{}/stage.sect", data_base_dir.clone())) {
|
||||
|
@ -54,7 +83,7 @@ impl VanillaExtractor {
|
|||
return None;
|
||||
}
|
||||
|
||||
Some(Self { exe_buffer, data_base_dir })
|
||||
Some(Self { exe_buffer, data_base_dir, root })
|
||||
}
|
||||
|
||||
pub fn extract_data(&self) -> GameResult {
|
||||
|
@ -93,8 +122,7 @@ impl VanillaExtractor {
|
|||
}
|
||||
|
||||
for org in orgs.unwrap().data_files {
|
||||
let mut org_path = env::current_exe().unwrap();
|
||||
org_path.pop();
|
||||
let mut org_path = self.root.clone();
|
||||
org_path.push(self.data_base_dir.clone());
|
||||
org_path.push("Org/");
|
||||
|
||||
|
@ -130,8 +158,7 @@ impl VanillaExtractor {
|
|||
}
|
||||
|
||||
for bitmap in bitmaps.unwrap().data_files {
|
||||
let mut data_path = env::current_exe().unwrap();
|
||||
data_path.pop();
|
||||
let mut data_path = self.root.clone();
|
||||
data_path.push(self.data_base_dir.clone());
|
||||
|
||||
if self.deep_create_dir_if_not_exists(data_path.clone()).is_err() {
|
||||
|
@ -164,24 +191,58 @@ impl VanillaExtractor {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn extract_stage_table(&self, parser: &ExeParser) -> GameResult {
|
||||
fn find_stage_table_offset(&self, parser: &ExeParser) -> Option<Range<u32>> {
|
||||
let range = parser.get_named_section_byte_range(".csmap".to_string());
|
||||
if range.is_err() {
|
||||
return Err(ParseError("Failed to retrieve stage table from executable.".to_string()));
|
||||
return None;
|
||||
}
|
||||
|
||||
let range = match range.unwrap() {
|
||||
let pattern = [
|
||||
// add esp, 8
|
||||
0x83u8, 0xc4, 0x08,
|
||||
// mov eax, [ebp+arg_0]
|
||||
0x8b, 0x45, 0x08,
|
||||
// imul eax, 0C8h
|
||||
0x69, 0xc0, 0xc8, 0x00, 0x00, 0x00,
|
||||
// add eax, offset gTMT
|
||||
0x05, // 0x??, 0x??, 0x??, 0x??
|
||||
];
|
||||
|
||||
let text = parser.section_headers.by_name(".text")?;
|
||||
let text_range = text.file_range().to_usize();
|
||||
let text_range_start = text_range.start;
|
||||
let offset = self.exe_buffer[text_range]
|
||||
.windows(pattern.len())
|
||||
.position(|window| window == pattern)?;
|
||||
let offset = text_range_start + offset;
|
||||
let offset = u32::from_le_bytes([
|
||||
self.exe_buffer[offset + 13],
|
||||
self.exe_buffer[offset + 14],
|
||||
self.exe_buffer[offset + 15],
|
||||
self.exe_buffer[offset + 16],
|
||||
]);
|
||||
log::info!("Found stage table offset: 0x{:X}", offset);
|
||||
|
||||
let section = parser.section_headers.by_rva(offset - parser.image_base)?;
|
||||
let offset_inside_range = offset.checked_sub(section.VirtualAddress + parser.image_base)?;
|
||||
let range = section.file_range();
|
||||
|
||||
let data_start = range.start + offset_inside_range;
|
||||
let data_end = data_start + VANILLA_STAGE_TABLE_SIZE;
|
||||
Some(data_start..data_end)
|
||||
}
|
||||
|
||||
fn extract_stage_table(&self, parser: &ExeParser) -> GameResult {
|
||||
let range = self.find_stage_table_offset(parser);
|
||||
let range = match range {
|
||||
Some(range) => range,
|
||||
None => Range { start: VANILLA_STAGE_OFFSET, end: VANILLA_STAGE_OFFSET + VANILLA_STAGE_TABLE_SIZE },
|
||||
None => return Err(ParseError("Failed to retrieve stage table from executable.".to_string())),
|
||||
};
|
||||
let range = range.to_usize();
|
||||
|
||||
let start = range.start as usize;
|
||||
let end = range.end as usize;
|
||||
let byte_slice = &self.exe_buffer[range];
|
||||
|
||||
let byte_slice = &self.exe_buffer[start..end];
|
||||
|
||||
let mut stage_tbl_path = env::current_exe().unwrap();
|
||||
stage_tbl_path.pop();
|
||||
let mut stage_tbl_path = self.root.clone();
|
||||
stage_tbl_path.push(self.data_base_dir.clone());
|
||||
|
||||
if self.deep_create_dir_if_not_exists(stage_tbl_path.clone()).is_err() {
|
||||
|
|
|
@ -0,0 +1,189 @@
|
|||
use std::sync::Mutex;
|
||||
|
||||
use discord_rich_presence::{
|
||||
activity::{Activity, Assets, Button},
|
||||
DiscordIpc, DiscordIpcClient,
|
||||
};
|
||||
|
||||
use crate::framework::error::GameResult;
|
||||
use crate::game::{player::Player, shared_game_state::GameDifficulty, stage::StageData};
|
||||
|
||||
pub enum DiscordRPCState {
|
||||
Initializing,
|
||||
Idling,
|
||||
InGame,
|
||||
Jukebox,
|
||||
}
|
||||
|
||||
pub struct DiscordRPC {
|
||||
pub enabled: bool,
|
||||
pub ready: bool,
|
||||
|
||||
client: DiscordIpcClient,
|
||||
state: DiscordRPCState,
|
||||
life: u16,
|
||||
max_life: u16,
|
||||
stage_name: String,
|
||||
difficulty: Option<GameDifficulty>,
|
||||
|
||||
can_update: Mutex<bool>,
|
||||
}
|
||||
|
||||
impl DiscordRPC {
|
||||
pub fn new(app_id: &str) -> Self {
|
||||
Self {
|
||||
enabled: false,
|
||||
ready: false,
|
||||
|
||||
client: DiscordIpcClient::new(app_id).unwrap(),
|
||||
state: DiscordRPCState::Idling,
|
||||
life: 0,
|
||||
max_life: 0,
|
||||
stage_name: String::new(),
|
||||
difficulty: None,
|
||||
|
||||
can_update: Mutex::new(true),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn start(&mut self) -> GameResult {
|
||||
log::info!("Starting Discord RPC client...");
|
||||
|
||||
let mut can_update = self.can_update.lock().unwrap();
|
||||
*can_update = false;
|
||||
|
||||
match self.client.connect() {
|
||||
Ok(_) => {
|
||||
self.ready = true;
|
||||
*can_update = true;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
log::warn!("Failed to start Discord RPC client (maybe Discord is not running?): {}", e);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn update(&mut self) -> GameResult {
|
||||
if !self.enabled || !self.ready {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut can_update = self.can_update.lock().unwrap();
|
||||
|
||||
if !*can_update {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
*can_update = false;
|
||||
|
||||
let (state, details) = match self.state {
|
||||
DiscordRPCState::Initializing => ("Initializing...".to_owned(), "Just started playing".to_owned()),
|
||||
DiscordRPCState::Idling => ("In the menus".to_owned(), "Idling".to_owned()),
|
||||
DiscordRPCState::InGame => {
|
||||
(format!("Currently in: {}", self.stage_name), format!("HP: {} / {}", self.life, self.max_life))
|
||||
}
|
||||
DiscordRPCState::Jukebox => ("In the menus".to_owned(), "Listening to the soundtrack".to_owned()),
|
||||
};
|
||||
|
||||
log::debug!("Updating Discord RPC state: {} - {}", state, details);
|
||||
|
||||
let mut activity_assets = Assets::new().large_image("drs");
|
||||
|
||||
if self.difficulty.is_some() {
|
||||
let difficulty = self.difficulty.unwrap();
|
||||
|
||||
let asset_name = match difficulty {
|
||||
GameDifficulty::Easy => "deasy",
|
||||
GameDifficulty::Normal => "dnormal",
|
||||
GameDifficulty::Hard => "dhard",
|
||||
};
|
||||
|
||||
let asset_label = match difficulty {
|
||||
GameDifficulty::Easy => "Easy",
|
||||
GameDifficulty::Normal => "Normal",
|
||||
GameDifficulty::Hard => "Hard",
|
||||
};
|
||||
|
||||
activity_assets = activity_assets.small_image(asset_name).small_text(asset_label);
|
||||
}
|
||||
|
||||
let activity = Activity::new()
|
||||
.state(state.as_str())
|
||||
.details(details.as_str())
|
||||
.assets(activity_assets)
|
||||
.buttons(vec![Button::new("doukutsu-rs on GitHub", "https://github.com/doukutsu-rs/doukutsu-rs")]);
|
||||
|
||||
match self.client.set_activity(activity) {
|
||||
Ok(()) => {
|
||||
*can_update = true;
|
||||
log::debug!("Discord RPC state updated successfully");
|
||||
}
|
||||
Err(e) => log::error!("Failed to update Discord RPC state: {}", e),
|
||||
};
|
||||
|
||||
Ok(()) // whatever
|
||||
}
|
||||
|
||||
pub fn update_stage(&mut self, stage: &StageData) -> GameResult {
|
||||
self.stage_name = stage.name.clone();
|
||||
self.update()
|
||||
}
|
||||
|
||||
pub fn update_hp(&mut self, player: &Player) -> GameResult {
|
||||
self.life = player.life;
|
||||
self.max_life = player.max_life;
|
||||
self.update()
|
||||
}
|
||||
|
||||
pub fn update_difficulty(&mut self, difficulty: GameDifficulty) -> GameResult {
|
||||
self.difficulty = Some(difficulty);
|
||||
self.update()
|
||||
}
|
||||
|
||||
pub fn set_initializing(&mut self) -> GameResult {
|
||||
self.set_state(DiscordRPCState::Initializing)
|
||||
}
|
||||
|
||||
pub fn set_idling(&mut self) -> GameResult {
|
||||
self.difficulty = None;
|
||||
self.set_state(DiscordRPCState::Idling)
|
||||
}
|
||||
|
||||
pub fn set_in_game(&mut self) -> GameResult {
|
||||
self.set_state(DiscordRPCState::InGame)
|
||||
}
|
||||
|
||||
pub fn set_in_jukebox(&mut self) -> GameResult {
|
||||
self.set_state(DiscordRPCState::Jukebox)
|
||||
}
|
||||
|
||||
pub fn set_state(&mut self, state: DiscordRPCState) -> GameResult {
|
||||
self.state = state;
|
||||
self.update()
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) -> GameResult {
|
||||
if !self.ready {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let _ = self.client.clear_activity();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn dispose(&mut self) {
|
||||
if !self.ready {
|
||||
return;
|
||||
}
|
||||
|
||||
let can_update = self.can_update.lock();
|
||||
if can_update.is_ok() {
|
||||
*can_update.unwrap() = false;
|
||||
}
|
||||
|
||||
let _ = self.client.close();
|
||||
}
|
||||
}
|
|
@ -2,14 +2,18 @@ use std::cell::RefCell;
|
|||
use std::ops::Deref;
|
||||
use std::rc::Rc;
|
||||
|
||||
use imgui::{Image, MouseButton, Window, WindowFlags};
|
||||
use imgui::{Image, MouseButton, Window};
|
||||
|
||||
use crate::{Context, GameResult, graphics, I_MAG, SharedGameState};
|
||||
use crate::common::{Color, Rect};
|
||||
use crate::components::background::Background;
|
||||
use crate::components::tilemap::{TileLayer, Tilemap};
|
||||
use crate::framework::context::Context;
|
||||
use crate::framework::error::GameResult;
|
||||
use crate::framework::graphics;
|
||||
use crate::game::shared_game_state::SharedGameState;
|
||||
use crate::game::frame::Frame;
|
||||
use crate::game::stage::{Stage, StageTexturePaths};
|
||||
use crate::graphics::texture_set::I_MAG;
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq)]
|
||||
pub enum CurrentTool {
|
||||
|
|
|
@ -67,7 +67,7 @@ pub struct GameConsts {
|
|||
pub tile_offset_x: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CaretConsts {
|
||||
pub offsets: [(i32, i32); 18],
|
||||
pub bubble_left_rects: Vec<Rect<u16>>,
|
||||
|
@ -97,34 +97,6 @@ pub struct TextureSizeTable {
|
|||
sizes: HashMap<String, (u16, u16)>,
|
||||
}
|
||||
|
||||
impl Clone for CaretConsts {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
offsets: self.offsets,
|
||||
bubble_left_rects: self.bubble_left_rects.clone(),
|
||||
bubble_right_rects: self.bubble_right_rects.clone(),
|
||||
projectile_dissipation_left_rects: self.projectile_dissipation_left_rects.clone(),
|
||||
projectile_dissipation_right_rects: self.projectile_dissipation_right_rects.clone(),
|
||||
projectile_dissipation_up_rects: self.projectile_dissipation_up_rects.clone(),
|
||||
shoot_rects: self.shoot_rects.clone(),
|
||||
zzz_rects: self.zzz_rects.clone(),
|
||||
drowned_quote_left_rect: self.drowned_quote_left_rect,
|
||||
drowned_quote_right_rect: self.drowned_quote_right_rect,
|
||||
level_up_rects: self.level_up_rects.clone(),
|
||||
level_down_rects: self.level_down_rects.clone(),
|
||||
hurt_particles_rects: self.hurt_particles_rects.clone(),
|
||||
explosion_rects: self.explosion_rects.clone(),
|
||||
little_particles_rects: self.little_particles_rects.clone(),
|
||||
exhaust_rects: self.exhaust_rects.clone(),
|
||||
question_left_rect: self.question_left_rect,
|
||||
question_right_rect: self.question_right_rect,
|
||||
small_projectile_dissipation: self.small_projectile_dissipation.clone(),
|
||||
empty_text: self.empty_text.clone(),
|
||||
push_jump_key: self.push_jump_key.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct BulletData {
|
||||
pub damage: u8,
|
||||
|
@ -175,23 +147,13 @@ pub struct BulletRects {
|
|||
pub b042_spur_trail_l3: [Rect<u16>; 6],
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct WeaponConsts {
|
||||
pub bullet_table: Vec<BulletData>,
|
||||
pub bullet_rects: BulletRects,
|
||||
pub level_table: [[u16; 3]; 14],
|
||||
}
|
||||
|
||||
impl Clone for WeaponConsts {
|
||||
fn clone(&self) -> WeaponConsts {
|
||||
WeaponConsts {
|
||||
bullet_table: self.bullet_table.clone(),
|
||||
bullet_rects: self.bullet_rects,
|
||||
level_table: self.level_table,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub struct WorldConsts {
|
||||
pub snack_rect: Rect<u16>,
|
||||
|
@ -207,7 +169,7 @@ pub struct AnimatedFace {
|
|||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ExtraSoundtrack {
|
||||
pub name: String,
|
||||
pub id: String,
|
||||
pub path: String,
|
||||
pub available: bool,
|
||||
}
|
||||
|
@ -245,7 +207,7 @@ pub struct TextScriptConsts {
|
|||
pub fade_ticks: i8,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct TitleConsts {
|
||||
pub intro_text: String,
|
||||
pub logo_rect: Rect<u16>,
|
||||
|
@ -266,60 +228,32 @@ pub struct TitleConsts {
|
|||
pub cursor_sue: [Rect<u16>; 4],
|
||||
}
|
||||
|
||||
impl Clone for TitleConsts {
|
||||
fn clone(&self) -> TitleConsts {
|
||||
TitleConsts {
|
||||
intro_text: self.intro_text.clone(),
|
||||
logo_rect: self.logo_rect,
|
||||
logo_splash_rect: self.logo_splash_rect,
|
||||
menu_left_top: self.menu_left_top,
|
||||
menu_right_top: self.menu_right_top,
|
||||
menu_left_bottom: self.menu_left_bottom,
|
||||
menu_right_bottom: self.menu_right_bottom,
|
||||
menu_top: self.menu_top,
|
||||
menu_bottom: self.menu_bottom,
|
||||
menu_middle: self.menu_middle,
|
||||
menu_left: self.menu_left,
|
||||
menu_right: self.menu_right,
|
||||
cursor_quote: self.cursor_quote,
|
||||
cursor_curly: self.cursor_curly,
|
||||
cursor_toroko: self.cursor_toroko,
|
||||
cursor_king: self.cursor_king,
|
||||
cursor_sue: self.cursor_sue,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct GamepadConsts {
|
||||
pub button_rects: HashMap<Button, [Rect<u16>; 4]>,
|
||||
pub axis_rects: HashMap<Axis, [Rect<u16>; 4]>,
|
||||
}
|
||||
|
||||
impl Clone for GamepadConsts {
|
||||
fn clone(&self) -> GamepadConsts {
|
||||
GamepadConsts { button_rects: self.button_rects.clone(), axis_rects: self.axis_rects.clone() }
|
||||
}
|
||||
}
|
||||
|
||||
impl GamepadConsts {
|
||||
fn rects(base: Rect<u16>) -> [Rect<u16>; 4] {
|
||||
[
|
||||
base,
|
||||
Rect::new(base.left + 64, base.top, base.right + 64, base.bottom),
|
||||
Rect::new(base.left + 128, base.top, base.right + 128, base.bottom),
|
||||
Rect::new(base.left + 192, base.top, base.right + 192, base.bottom),
|
||||
Rect::new(base.left + 64, base.top + 128, base.right + 64, base.bottom + 128),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct EngineConstants {
|
||||
pub base_paths: Vec<String>,
|
||||
pub is_cs_plus: bool,
|
||||
pub is_switch: bool,
|
||||
pub is_demo: bool,
|
||||
pub supports_og_textures: bool,
|
||||
pub has_difficulty_menu: bool,
|
||||
pub supports_two_player: bool,
|
||||
pub game: GameConsts,
|
||||
pub player: PlayerConsts,
|
||||
pub booster: BoosterConsts,
|
||||
|
@ -337,45 +271,13 @@ pub struct EngineConstants {
|
|||
pub music_table: Vec<String>,
|
||||
pub organya_paths: Vec<String>,
|
||||
pub credit_illustration_paths: Vec<String>,
|
||||
pub player_skin_paths: Vec<String>,
|
||||
pub animated_face_table: Vec<AnimatedFace>,
|
||||
pub string_table: HashMap<String, String>,
|
||||
pub missile_flags: Vec<u16>,
|
||||
pub locales: Vec<Locale>,
|
||||
pub gamepad: GamepadConsts,
|
||||
}
|
||||
|
||||
impl Clone for EngineConstants {
|
||||
fn clone(&self) -> EngineConstants {
|
||||
EngineConstants {
|
||||
base_paths: self.base_paths.clone(),
|
||||
is_cs_plus: self.is_cs_plus,
|
||||
is_switch: self.is_switch,
|
||||
is_demo: self.is_demo,
|
||||
supports_og_textures: self.supports_og_textures,
|
||||
game: self.game,
|
||||
player: self.player,
|
||||
booster: self.booster,
|
||||
caret: self.caret.clone(),
|
||||
world: self.world,
|
||||
npc: self.npc,
|
||||
weapon: self.weapon.clone(),
|
||||
tex_sizes: self.tex_sizes.clone(),
|
||||
textscript: self.textscript,
|
||||
title: self.title.clone(),
|
||||
inventory_dim_color: self.inventory_dim_color,
|
||||
font_path: self.font_path.clone(),
|
||||
font_space_offset: self.font_space_offset,
|
||||
soundtracks: self.soundtracks.clone(),
|
||||
music_table: self.music_table.clone(),
|
||||
organya_paths: self.organya_paths.clone(),
|
||||
credit_illustration_paths: self.credit_illustration_paths.clone(),
|
||||
animated_face_table: self.animated_face_table.clone(),
|
||||
string_table: self.string_table.clone(),
|
||||
missile_flags: self.missile_flags.clone(),
|
||||
locales: self.locales.clone(),
|
||||
gamepad: self.gamepad.clone(),
|
||||
}
|
||||
}
|
||||
pub stage_encoding: Option<TextScriptEncoding>,
|
||||
}
|
||||
|
||||
impl EngineConstants {
|
||||
|
@ -386,6 +288,8 @@ impl EngineConstants {
|
|||
is_switch: false,
|
||||
is_demo: false,
|
||||
supports_og_textures: false,
|
||||
has_difficulty_menu: true,
|
||||
supports_two_player: cfg!(not(target_os = "android")),
|
||||
game: GameConsts {
|
||||
intro_stage: 72,
|
||||
intro_event: 100,
|
||||
|
@ -1433,6 +1337,7 @@ impl EngineConstants {
|
|||
"ItemImage" => (256, 128),
|
||||
"Loading" => (64, 8),
|
||||
"MyChar" => (200, 64),
|
||||
"mychar_p2" => (200, 384), // switch
|
||||
"Npc/Npc0" => (32, 32),
|
||||
"Npc/NpcAlmo1" => (320, 240),
|
||||
"Npc/NpcAlmo2" => (320, 240),
|
||||
|
@ -1604,10 +1509,10 @@ impl EngineConstants {
|
|||
font_path: "csfont.fnt".to_owned(),
|
||||
font_space_offset: 0.0,
|
||||
soundtracks: vec![
|
||||
ExtraSoundtrack { name: "Remastered".to_owned(), path: "/base/Ogg11/".to_owned(), available: false },
|
||||
ExtraSoundtrack { name: "New".to_owned(), path: "/base/Ogg/".to_owned(), available: false },
|
||||
ExtraSoundtrack { name: "Famitracks".to_owned(), path: "/base/ogg17/".to_owned(), available: false },
|
||||
ExtraSoundtrack { name: "Ridiculon".to_owned(), path: "/base/ogg_ridic/".to_owned(), available: false },
|
||||
ExtraSoundtrack { id: "remastered".to_owned(), path: "/base/Ogg11/".to_owned(), available: false },
|
||||
ExtraSoundtrack { id: "new".to_owned(), path: "/base/Ogg/".to_owned(), available: false },
|
||||
ExtraSoundtrack { id: "famitracks".to_owned(), path: "/base/ogg17/".to_owned(), available: false },
|
||||
ExtraSoundtrack { id: "ridiculon".to_owned(), path: "/base/ogg_ridic/".to_owned(), available: false },
|
||||
],
|
||||
music_table: vec![
|
||||
"xxxx".to_owned(),
|
||||
|
@ -1665,6 +1570,7 @@ impl EngineConstants {
|
|||
"Resource/BITMAP/".to_owned(), // CSE2E
|
||||
"endpic/".to_owned(), // NXEngine
|
||||
],
|
||||
player_skin_paths: vec!["MyChar".to_owned()],
|
||||
animated_face_table: vec![AnimatedFace { face_id: 0, anim_id: 0, anim_frames: vec![(0, 0)] }],
|
||||
string_table: HashMap::new(),
|
||||
missile_flags: vec![200, 201, 202, 218, 550, 766, 880, 920, 1551],
|
||||
|
@ -1695,6 +1601,7 @@ impl EngineConstants {
|
|||
(Axis::TriggerRight, GamepadConsts::rects(Rect::new(32, 80, 64, 96))),
|
||||
]),
|
||||
},
|
||||
stage_encoding: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1749,6 +1656,10 @@ impl EngineConstants {
|
|||
let _ = sound_manager.set_sample_params(2, typewriter_sample);
|
||||
}
|
||||
|
||||
pub fn is_base(&self) -> bool {
|
||||
!self.is_switch && !self.is_cs_plus && !self.is_demo
|
||||
}
|
||||
|
||||
pub fn apply_csplus_nx_patches(&mut self) {
|
||||
log::info!("Applying Switch-specific Cave Story+ constants patches...");
|
||||
|
||||
|
@ -1769,6 +1680,7 @@ impl EngineConstants {
|
|||
self.textscript.fade_ticks = 21;
|
||||
self.game.tile_offset_x = 3;
|
||||
self.game.new_game_player_pos = (13, 8);
|
||||
self.player_skin_paths.push("mychar_p2".to_owned());
|
||||
}
|
||||
|
||||
pub fn apply_csdemo_patches(&mut self) {
|
||||
|
|
|
@ -26,12 +26,16 @@ pub enum BackendShader {
|
|||
|
||||
pub trait Backend {
|
||||
fn create_event_loop(&self, ctx: &Context) -> GameResult<Box<dyn BackendEventLoop>>;
|
||||
|
||||
fn as_any(&self) -> &dyn Any;
|
||||
}
|
||||
|
||||
pub trait BackendEventLoop {
|
||||
fn run(&mut self, game: &mut Game, ctx: &mut Context);
|
||||
|
||||
fn new_renderer(&self, ctx: *mut Context) -> GameResult<Box<dyn BackendRenderer>>;
|
||||
|
||||
fn as_any(&self) -> &dyn Any;
|
||||
}
|
||||
|
||||
pub trait BackendRenderer {
|
||||
|
@ -81,6 +85,8 @@ pub trait BackendRenderer {
|
|||
texture: Option<&Box<dyn BackendTexture>>,
|
||||
shader: BackendShader,
|
||||
) -> GameResult;
|
||||
|
||||
fn as_any(&self) -> &dyn Any;
|
||||
}
|
||||
|
||||
pub trait BackendTexture {
|
||||
|
|
|
@ -1,23 +1,29 @@
|
|||
use std::any::Any;
|
||||
use std::cell::{RefCell, UnsafeCell};
|
||||
use std::ffi::c_void;
|
||||
use std::io::Read;
|
||||
use std::mem;
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
use std::vec::Vec;
|
||||
|
||||
use glutin::{Api, ContextBuilder, GlProfile, GlRequest, PossiblyCurrent, WindowedContext};
|
||||
use glutin::event::{ElementState, Event, TouchPhase, VirtualKeyCode, WindowEvent};
|
||||
use glutin::event_loop::{ControlFlow, EventLoop};
|
||||
use glutin::window::WindowBuilder;
|
||||
use glutin::{Api, ContextBuilder, GlProfile, GlRequest, PossiblyCurrent, WindowedContext};
|
||||
use imgui::{DrawCmdParams, DrawData, DrawIdx, DrawVert};
|
||||
use winit::window::Icon;
|
||||
|
||||
use crate::{Game, GAME_SUSPENDED};
|
||||
use crate::common::Rect;
|
||||
use crate::framework::backend::{Backend, BackendEventLoop, BackendRenderer, BackendTexture, SpriteBatchCommand};
|
||||
use crate::framework::context::Context;
|
||||
use crate::framework::error::GameResult;
|
||||
use crate::framework::filesystem;
|
||||
use crate::framework::gl;
|
||||
use crate::framework::keyboard::ScanCode;
|
||||
use crate::framework::render_opengl::{GLContext, OpenGLRenderer};
|
||||
use crate::game::Game;
|
||||
use crate::game::GAME_SUSPENDED;
|
||||
use crate::input::touch_controls::TouchPoint;
|
||||
|
||||
pub struct GlutinBackend;
|
||||
|
@ -43,6 +49,10 @@ impl Backend for GlutinBackend {
|
|||
|
||||
Ok(Box::new(GlutinEventLoop { refs: Rc::new(UnsafeCell::new(None)) }))
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub struct GlutinEventLoop {
|
||||
|
@ -50,15 +60,16 @@ pub struct GlutinEventLoop {
|
|||
}
|
||||
|
||||
impl GlutinEventLoop {
|
||||
fn get_context(&self, event_loop: &EventLoop<()>) -> &mut WindowedContext<PossiblyCurrent> {
|
||||
fn get_context(&self, ctx: &Context, event_loop: &EventLoop<()>) -> &mut WindowedContext<PossiblyCurrent> {
|
||||
let mut refs = unsafe { &mut *self.refs.get() };
|
||||
|
||||
if refs.is_none() {
|
||||
let mut window = WindowBuilder::new();
|
||||
|
||||
let windowed_context = ContextBuilder::new();
|
||||
let windowed_context = windowed_context.with_gl(GlRequest::Specific(Api::OpenGl, (3, 0)));
|
||||
#[cfg(target_os = "android")]
|
||||
let windowed_context = windowed_context.with_gl(GlRequest::Specific(Api::OpenGlEs, (2, 0)));
|
||||
let windowed_context = windowed_context.with_gl(GlRequest::Specific(Api::OpenGlEs, (2, 0)));
|
||||
|
||||
let windowed_context = windowed_context
|
||||
.with_gl_profile(GlProfile::Core)
|
||||
|
@ -67,13 +78,30 @@ impl GlutinEventLoop {
|
|||
.with_vsync(true);
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use glutin::platform::windows::WindowBuilderExtWindows;
|
||||
window = window.with_drag_and_drop(false);
|
||||
}
|
||||
{
|
||||
use glutin::platform::windows::WindowBuilderExtWindows;
|
||||
window = window.with_drag_and_drop(false);
|
||||
}
|
||||
|
||||
window = window.with_title("doukutsu-rs");
|
||||
|
||||
|
||||
#[cfg(not(any(target_os = "windows", target_os = "android", target_os = "horizon")))]
|
||||
{
|
||||
let mut file = filesystem::open(&ctx, "/builtin/icon.bmp").unwrap();
|
||||
let mut buf: Vec<u8> = Vec::new();
|
||||
file.read_to_end(&mut buf);
|
||||
|
||||
let mut img = match image::load_from_memory_with_format(buf.as_slice(), image::ImageFormat::Bmp) {
|
||||
Ok(image) => image.into_rgba8(),
|
||||
Err(e) => panic!("Cannot set window icon")
|
||||
};
|
||||
|
||||
let (width, height) = img.dimensions();
|
||||
let icon = Icon::from_rgba(img.into_raw(), width, height).unwrap();
|
||||
|
||||
window = window.with_window_icon(Some(icon));
|
||||
}
|
||||
|
||||
let windowed_context = windowed_context.build_windowed(window, event_loop).unwrap();
|
||||
|
||||
let windowed_context = unsafe { windowed_context.make_current().unwrap() };
|
||||
|
@ -112,18 +140,24 @@ fn request_android_redraw() {
|
|||
#[cfg(target_os = "android")]
|
||||
fn get_insets() -> GameResult<(f32, f32, f32, f32)> {
|
||||
unsafe {
|
||||
use jni::objects::JObject;
|
||||
use jni::JavaVM;
|
||||
|
||||
let vm_ptr = ndk_glue::native_activity().vm();
|
||||
let vm = unsafe { jni::JavaVM::from_raw(vm_ptr) }?;
|
||||
let vm = JavaVM::from_raw(vm_ptr)?;
|
||||
let vm_env = vm.attach_current_thread()?;
|
||||
|
||||
//let class = vm_env.find_class("io/github/doukutsu_rs/MainActivity")?;
|
||||
let class = vm_env.new_global_ref(ndk_glue::native_activity().activity())?;
|
||||
let class = vm_env.new_global_ref(JObject::from_raw(ndk_glue::native_activity().activity()))?;
|
||||
let field = vm_env.get_field(class.as_obj(), "displayInsets", "[I")?.to_jni().l as jni::sys::jintArray;
|
||||
|
||||
let mut elements = [0; 4];
|
||||
vm_env.get_int_array_region(field, 0, &mut elements)?;
|
||||
|
||||
vm_env.delete_local_ref(field.into());
|
||||
vm_env.delete_local_ref(JObject::from_raw(field));
|
||||
|
||||
//Game always runs with horizontal orientation so top and bottom cutouts not needed and only wastes piece of the screen
|
||||
elements[1] = 0;
|
||||
elements[3] = 0;
|
||||
|
||||
Ok((elements[0] as f32, elements[1] as f32, elements[2] as f32, elements[3] as f32))
|
||||
}
|
||||
|
@ -141,8 +175,7 @@ impl BackendEventLoop for GlutinEventLoop {
|
|||
let event_loop = EventLoop::new();
|
||||
let state_ref = unsafe { &mut *game.state.get() };
|
||||
let window: &'static mut WindowedContext<PossiblyCurrent> =
|
||||
unsafe { std::mem::transmute(self.get_context(&event_loop)) };
|
||||
|
||||
unsafe { std::mem::transmute(self.get_context(&ctx, &event_loop)) };
|
||||
{
|
||||
let size = window.window().inner_size();
|
||||
ctx.real_screen_size = (size.width, size.height);
|
||||
|
@ -159,10 +192,10 @@ impl BackendEventLoop for GlutinEventLoop {
|
|||
|
||||
match event {
|
||||
Event::WindowEvent { event: WindowEvent::CloseRequested, window_id }
|
||||
if window_id == window.window().id() =>
|
||||
{
|
||||
state_ref.shutdown();
|
||||
}
|
||||
if window_id == window.window().id() =>
|
||||
{
|
||||
state_ref.shutdown();
|
||||
}
|
||||
Event::Resumed => {
|
||||
{
|
||||
let mut mutex = GAME_SUSPENDED.lock().unwrap();
|
||||
|
@ -187,74 +220,74 @@ impl BackendEventLoop for GlutinEventLoop {
|
|||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
unsafe {
|
||||
unsafe {
|
||||
window.surface_destroyed();
|
||||
}
|
||||
|
||||
state_ref.sound_manager.pause();
|
||||
}
|
||||
Event::WindowEvent { event: WindowEvent::Resized(size), window_id }
|
||||
if window_id == window.window().id() =>
|
||||
{
|
||||
if let Some(renderer) = &ctx.renderer {
|
||||
if let Ok(imgui) = renderer.imgui() {
|
||||
imgui.io_mut().display_size = [size.width as f32, size.height as f32];
|
||||
}
|
||||
|
||||
ctx.real_screen_size = (size.width, size.height);
|
||||
ctx.screen_size = get_scaled_size(size.width.max(1), size.height.max(1));
|
||||
state_ref.handle_resize(ctx).unwrap();
|
||||
if window_id == window.window().id() =>
|
||||
{
|
||||
if let Some(renderer) = &ctx.renderer {
|
||||
if let Ok(imgui) = renderer.imgui() {
|
||||
imgui.io_mut().display_size = [size.width as f32, size.height as f32];
|
||||
}
|
||||
|
||||
ctx.real_screen_size = (size.width, size.height);
|
||||
ctx.screen_size = get_scaled_size(size.width.max(1), size.height.max(1));
|
||||
state_ref.handle_resize(ctx).unwrap();
|
||||
}
|
||||
}
|
||||
Event::WindowEvent { event: WindowEvent::Touch(touch), window_id }
|
||||
if window_id == window.window().id() =>
|
||||
{
|
||||
let mut controls = &mut state_ref.touch_controls;
|
||||
let scale = state_ref.scale as f64;
|
||||
let loc_x = (touch.location.x * ctx.screen_size.0 as f64 / ctx.real_screen_size.0 as f64) / scale;
|
||||
let loc_y = (touch.location.y * ctx.screen_size.1 as f64 / ctx.real_screen_size.1 as f64) / scale;
|
||||
if window_id == window.window().id() =>
|
||||
{
|
||||
let mut controls = &mut state_ref.touch_controls;
|
||||
let scale = state_ref.scale as f64;
|
||||
let loc_x = (touch.location.x * ctx.screen_size.0 as f64 / ctx.real_screen_size.0 as f64) / scale;
|
||||
let loc_y = (touch.location.y * ctx.screen_size.1 as f64 / ctx.real_screen_size.1 as f64) / scale;
|
||||
|
||||
match touch.phase {
|
||||
TouchPhase::Started | TouchPhase::Moved => {
|
||||
if let Some(point) = controls.points.iter_mut().find(|p| p.id == touch.id) {
|
||||
point.last_position = point.position;
|
||||
point.position = (loc_x, loc_y);
|
||||
} else {
|
||||
controls.touch_id_counter = controls.touch_id_counter.wrapping_add(1);
|
||||
match touch.phase {
|
||||
TouchPhase::Started | TouchPhase::Moved => {
|
||||
if let Some(point) = controls.points.iter_mut().find(|p| p.id == touch.id) {
|
||||
point.last_position = point.position;
|
||||
point.position = (loc_x, loc_y);
|
||||
} else {
|
||||
controls.touch_id_counter = controls.touch_id_counter.wrapping_add(1);
|
||||
|
||||
let point = TouchPoint {
|
||||
id: touch.id,
|
||||
touch_id: controls.touch_id_counter,
|
||||
position: (loc_x, loc_y),
|
||||
last_position: (0.0, 0.0),
|
||||
};
|
||||
controls.points.push(point);
|
||||
let point = TouchPoint {
|
||||
id: touch.id,
|
||||
touch_id: controls.touch_id_counter,
|
||||
position: (loc_x, loc_y),
|
||||
last_position: (0.0, 0.0),
|
||||
};
|
||||
controls.points.push(point);
|
||||
|
||||
if touch.phase == TouchPhase::Started {
|
||||
controls.clicks.push(point);
|
||||
}
|
||||
if touch.phase == TouchPhase::Started {
|
||||
controls.clicks.push(point);
|
||||
}
|
||||
}
|
||||
TouchPhase::Ended | TouchPhase::Cancelled => {
|
||||
controls.points.retain(|p| p.id != touch.id);
|
||||
controls.clicks.retain(|p| p.id != touch.id);
|
||||
}
|
||||
}
|
||||
TouchPhase::Ended | TouchPhase::Cancelled => {
|
||||
controls.points.retain(|p| p.id != touch.id);
|
||||
controls.clicks.retain(|p| p.id != touch.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
Event::WindowEvent { event: WindowEvent::KeyboardInput { input, .. }, window_id }
|
||||
if window_id == window.window().id() =>
|
||||
{
|
||||
if let Some(keycode) = input.virtual_keycode {
|
||||
if let Some(drs_scan) = conv_keycode(keycode) {
|
||||
let key_state = match input.state {
|
||||
ElementState::Pressed => true,
|
||||
ElementState::Released => false,
|
||||
};
|
||||
if window_id == window.window().id() =>
|
||||
{
|
||||
if let Some(keycode) = input.virtual_keycode {
|
||||
if let Some(drs_scan) = conv_keycode(keycode) {
|
||||
let key_state = match input.state {
|
||||
ElementState::Pressed => true,
|
||||
ElementState::Released => false,
|
||||
};
|
||||
|
||||
ctx.keyboard_context.set_key(drs_scan, key_state);
|
||||
}
|
||||
ctx.keyboard_context.set_key(drs_scan, key_state);
|
||||
}
|
||||
}
|
||||
}
|
||||
Event::RedrawRequested(id) if id == window.window().id() => {
|
||||
{
|
||||
let mutex = GAME_SUSPENDED.lock().unwrap();
|
||||
|
@ -264,16 +297,16 @@ impl BackendEventLoop for GlutinEventLoop {
|
|||
}
|
||||
|
||||
#[cfg(not(target_os = "android"))]
|
||||
{
|
||||
if let Err(err) = game.draw(ctx) {
|
||||
log::error!("Failed to draw frame: {}", err);
|
||||
}
|
||||
|
||||
window.window().request_redraw();
|
||||
{
|
||||
if let Err(err) = game.draw(ctx) {
|
||||
log::error!("Failed to draw frame: {}", err);
|
||||
}
|
||||
|
||||
window.window().request_redraw();
|
||||
}
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
request_android_redraw();
|
||||
request_android_redraw();
|
||||
}
|
||||
Event::MainEventsCleared => {
|
||||
if state_ref.shutdown {
|
||||
|
@ -289,24 +322,35 @@ impl BackendEventLoop for GlutinEventLoop {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "horizon")))]
|
||||
{
|
||||
if state_ref.settings.window_mode.get_glutin_fullscreen_type() != window.window().fullscreen() {
|
||||
let fullscreen_type = state_ref.settings.window_mode.get_glutin_fullscreen_type();
|
||||
let cursor_visible = state_ref.settings.window_mode.should_display_mouse_cursor();
|
||||
|
||||
window.window().set_fullscreen(fullscreen_type);
|
||||
window.window().set_cursor_visible(cursor_visible);
|
||||
}
|
||||
}
|
||||
|
||||
game.update(ctx).unwrap();
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
match get_insets() {
|
||||
Ok(insets) => {
|
||||
ctx.screen_insets = insets;
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!("Failed to update insets: {}", e);
|
||||
}
|
||||
{
|
||||
match get_insets() {
|
||||
Ok(insets) => {
|
||||
ctx.screen_insets = insets;
|
||||
}
|
||||
|
||||
if let Err(err) = game.draw(ctx) {
|
||||
log::error!("Failed to draw frame: {}", err);
|
||||
Err(e) => {
|
||||
log::error!("Failed to update insets: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(err) = game.draw(ctx) {
|
||||
log::error!("Failed to draw frame: {}", err);
|
||||
}
|
||||
}
|
||||
|
||||
if state_ref.next_scene.is_some() {
|
||||
mem::swap(&mut game.scene, &mut state_ref.next_scene);
|
||||
state_ref.next_scene = None;
|
||||
|
@ -363,6 +407,10 @@ impl BackendEventLoop for GlutinEventLoop {
|
|||
|
||||
Ok(Box::new(OpenGLRenderer::new(gl_context, UnsafeCell::new(imgui))))
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
fn conv_keycode(code: VirtualKeyCode) -> Option<ScanCode> {
|
||||
|
|
|
@ -283,6 +283,10 @@ impl Backend for HorizonBackend {
|
|||
|
||||
Ok(Box::new(HorizonEventLoop { gamepads, active: [false; 8] }))
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub struct HorizonEventLoop {
|
||||
|
@ -293,8 +297,8 @@ pub struct HorizonEventLoop {
|
|||
const GAMEPAD_KEYMAP: [Button; 16] = [
|
||||
Button::South,
|
||||
Button::East,
|
||||
Button::North,
|
||||
Button::West,
|
||||
Button::North,
|
||||
Button::LeftStick,
|
||||
Button::RightStick,
|
||||
Button::LeftShoulder,
|
||||
|
@ -351,6 +355,14 @@ impl HorizonEventLoop {
|
|||
let button = GAMEPAD_KEYMAP[i];
|
||||
let mask = 1 << i;
|
||||
|
||||
if i == 8 {
|
||||
ctx.gamepad_context.set_axis_value(id as u32, Axis::TriggerLeft, if buttons_down & mask != 0 { 1.0 } else { 0.0 });
|
||||
continue;
|
||||
} else if i == 9 {
|
||||
ctx.gamepad_context.set_axis_value(id as u32, Axis::TriggerRight, if buttons_down & mask != 0 { 1.0 } else { 0.0 });
|
||||
continue;
|
||||
}
|
||||
|
||||
if buttons_down & mask != 0 {
|
||||
ctx.gamepad_context.set_button(id as u32, button, true);
|
||||
}
|
||||
|
@ -363,10 +375,11 @@ impl HorizonEventLoop {
|
|||
let analog_x = pad.sticks[0].x as f64 / 32768.0;
|
||||
let analog_y = -pad.sticks[0].y as f64 / 32768.0;
|
||||
|
||||
ctx.gamepad_context.set_axis_value(id as u32, Axis::LeftX, analog_x.clamp(0.0, 1.0));
|
||||
ctx.gamepad_context.set_axis_value(id as u32, Axis::LeftY, analog_y.clamp(0.0, 1.0));
|
||||
ctx.gamepad_context.set_axis_value(id as u32, Axis::RightX, (-analog_x).clamp(0.0, 1.0));
|
||||
ctx.gamepad_context.set_axis_value(id as u32, Axis::RightY, (-analog_y).clamp(0.0, 1.0));
|
||||
ctx.gamepad_context.set_axis_value(id as u32, Axis::LeftX, (analog_x).clamp(-1.0, 1.0));
|
||||
ctx.gamepad_context.set_axis_value(id as u32, Axis::LeftY, (analog_y).clamp(-1.0, 1.0));
|
||||
ctx.gamepad_context.set_axis_value(id as u32, Axis::RightX, (analog_x).clamp(-1.0, 1.0));
|
||||
ctx.gamepad_context.set_axis_value(id as u32, Axis::RightY, (analog_y).clamp(-1.0, 1.0));
|
||||
ctx.gamepad_context.update_axes(id as u32);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -411,6 +424,10 @@ impl BackendEventLoop for HorizonEventLoop {
|
|||
|
||||
Deko3DRenderer::new(device, imgui)
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub struct HorizonGamepad {
|
||||
|
@ -1322,6 +1339,10 @@ impl BackendRenderer for Deko3DRenderer {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub fn web_open(url: &str) -> std::io::Result<()> {
|
||||
|
|
|
@ -25,6 +25,10 @@ impl Backend for NullBackend {
|
|||
fn create_event_loop(&self, _ctx: &Context) -> GameResult<Box<dyn BackendEventLoop>> {
|
||||
Ok(Box::new(NullEventLoop))
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub struct NullEventLoop;
|
||||
|
@ -64,6 +68,10 @@ impl BackendEventLoop for NullEventLoop {
|
|||
|
||||
Ok(Box::new(NullRenderer(RefCell::new(imgui))))
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub struct NullTexture(u16, u16);
|
||||
|
@ -151,4 +159,8 @@ impl BackendRenderer for NullRenderer {
|
|||
) -> GameResult<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,10 +2,12 @@ use core::mem;
|
|||
use std::any::Any;
|
||||
use std::cell::{RefCell, UnsafeCell};
|
||||
use std::ffi::c_void;
|
||||
use std::io::Read;
|
||||
use std::ops::Deref;
|
||||
use std::ptr::{null, null_mut};
|
||||
use std::rc::Rc;
|
||||
use std::time::{Duration, Instant};
|
||||
use std::vec::Vec;
|
||||
|
||||
use imgui::internal::RawWrapper;
|
||||
use imgui::sys::{ImGuiKey_Backspace, ImGuiKey_Delete, ImGuiKey_Enter};
|
||||
|
@ -16,6 +18,8 @@ use sdl2::keyboard::Scancode;
|
|||
use sdl2::mouse::{Cursor, SystemCursor};
|
||||
use sdl2::pixels::PixelFormatEnum;
|
||||
use sdl2::render::{Texture, TextureCreator, TextureQuery, WindowCanvas};
|
||||
use sdl2::rwops::RWops;
|
||||
use sdl2::surface::Surface;
|
||||
use sdl2::video::GLProfile;
|
||||
use sdl2::video::Window;
|
||||
use sdl2::video::WindowContext;
|
||||
|
@ -32,6 +36,7 @@ use crate::framework::filesystem;
|
|||
use crate::framework::gamepad::{Axis, Button, GamepadType};
|
||||
use crate::framework::graphics::BlendMode;
|
||||
use crate::framework::keyboard::ScanCode;
|
||||
#[cfg(feature = "render-opengl")]
|
||||
use crate::framework::render_opengl::{GLContext, OpenGLRenderer};
|
||||
use crate::framework::ui::init_imgui;
|
||||
use crate::game::shared_game_state::WindowMode;
|
||||
|
@ -59,6 +64,10 @@ impl Backend for SDL2Backend {
|
|||
fn create_event_loop(&self, ctx: &Context) -> GameResult<Box<dyn BackendEventLoop>> {
|
||||
SDL2EventLoop::new(&self.context, self.size_hint, ctx)
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
enum WindowOrCanvas {
|
||||
|
@ -163,14 +172,27 @@ impl SDL2EventLoop {
|
|||
gl_attr.set_context_profile(GLProfile::Compatibility);
|
||||
gl_attr.set_context_version(2, 1);
|
||||
|
||||
let mut window = video.window("Cave Story (doukutsu-rs)", size_hint.0 as _, size_hint.1 as _);
|
||||
window.position_centered();
|
||||
window.resizable();
|
||||
let mut win_builder = video.window("Cave Story (doukutsu-rs)", size_hint.0 as _, size_hint.1 as _);
|
||||
win_builder.position_centered();
|
||||
win_builder.resizable();
|
||||
|
||||
#[cfg(feature = "render-opengl")]
|
||||
window.opengl();
|
||||
win_builder.opengl();
|
||||
|
||||
let window = window.build().map_err(|e| GameError::WindowError(e.to_string()))?;
|
||||
let mut window = win_builder.build().map_err(|e| GameError::WindowError(e.to_string()))?;
|
||||
#[cfg(not(any(target_os = "windows", target_os = "android", target_os = "horizon")))]
|
||||
{
|
||||
let mut file = filesystem::open(&ctx, "/builtin/icon.bmp").unwrap();
|
||||
let mut buf: Vec<u8> = Vec::new();
|
||||
file.read_to_end(&mut buf)?;
|
||||
|
||||
let mut rwops = RWops::from_bytes(buf.as_slice()).unwrap();
|
||||
let icon = Surface::load_bmp_rw(&mut rwops).unwrap();
|
||||
|
||||
window.set_icon(icon);
|
||||
}
|
||||
|
||||
|
||||
let opengl_available = if let Ok(v) = std::env::var("CAVESTORY_NO_OPENGL") { v != "1" } else { true };
|
||||
|
||||
let event_loop = SDL2EventLoop {
|
||||
|
@ -194,11 +216,10 @@ impl BackendEventLoop for SDL2EventLoop {
|
|||
fn run(&mut self, game: &mut Game, ctx: &mut Context) {
|
||||
let state = unsafe { &mut *game.state.get() };
|
||||
|
||||
let (imgui, imgui_sdl2) = unsafe {
|
||||
let renderer: &Box<SDL2Renderer> = std::mem::transmute(ctx.renderer.as_ref().unwrap());
|
||||
|
||||
(&mut *renderer.imgui.as_ptr(), &mut *renderer.imgui_event.as_ptr())
|
||||
let imgui = unsafe {
|
||||
(&*(ctx.renderer.as_ref().unwrap() as *const Box<dyn BackendRenderer>)).imgui().unwrap()
|
||||
};
|
||||
let mut imgui_sdl2 = ImguiSdl2::new(imgui, self.refs.deref().borrow().window.window());
|
||||
|
||||
{
|
||||
let (width, height) = self.refs.deref().borrow().window.window().size();
|
||||
|
@ -375,13 +396,14 @@ impl BackendEventLoop for SDL2EventLoop {
|
|||
let window = refs.window.window_mut();
|
||||
|
||||
let fullscreen_type = state.settings.window_mode.get_sdl2_fullscreen_type();
|
||||
let show_cursor = state.settings.window_mode.should_display_mouse_cursor();
|
||||
|
||||
window.set_fullscreen(fullscreen_type);
|
||||
window
|
||||
.subsystem()
|
||||
.sdl()
|
||||
.mouse()
|
||||
.show_cursor(state.settings.window_mode.should_display_mouse_cursor());
|
||||
.show_cursor(show_cursor);
|
||||
|
||||
refs.fullscreen_type = fullscreen_type;
|
||||
}
|
||||
|
@ -471,6 +493,10 @@ impl BackendEventLoop for SDL2EventLoop {
|
|||
|
||||
SDL2Renderer::new(self.refs.clone())
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
fn get_game_controller_type(ctype: sdl2_sys::SDL_GameControllerType) -> GamepadType {
|
||||
|
@ -497,14 +523,15 @@ struct SDL2Gamepad {
|
|||
}
|
||||
|
||||
impl SDL2Gamepad {
|
||||
fn new(inner: GameController) -> Box<dyn BackendGamepad> {
|
||||
pub fn new(inner: GameController) -> Box<dyn BackendGamepad> {
|
||||
Box::new(SDL2Gamepad { inner })
|
||||
}
|
||||
}
|
||||
|
||||
impl BackendGamepad for SDL2Gamepad {
|
||||
fn set_rumble(&mut self, low_freq: u16, high_freq: u16, duration_ms: u32) -> GameResult {
|
||||
self.inner.set_rumble(low_freq, high_freq, duration_ms).map_err(|e| GameError::GamepadError(e.to_string()))
|
||||
let _ = self.inner.set_rumble(low_freq, high_freq, duration_ms);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn instance_id(&self) -> u32 {
|
||||
|
@ -515,7 +542,6 @@ impl BackendGamepad for SDL2Gamepad {
|
|||
struct SDL2Renderer {
|
||||
refs: Rc<RefCell<SDL2Context>>,
|
||||
imgui: Rc<RefCell<imgui::Context>>,
|
||||
imgui_event: Rc<RefCell<ImguiSdl2>>,
|
||||
#[allow(unused)] // the rendering pipeline uses pointers to SDL_Texture, and we manually manage the lifetimes
|
||||
imgui_font_tex: SDL2Texture,
|
||||
}
|
||||
|
@ -565,15 +591,9 @@ impl SDL2Renderer {
|
|||
};
|
||||
imgui.fonts().tex_id = TextureId::new(imgui_font_tex.texture.as_ref().unwrap().raw() as usize);
|
||||
|
||||
let imgui_sdl2 = unsafe {
|
||||
let refs = &mut *refs.as_ptr();
|
||||
ImguiSdl2::new(&mut imgui, refs.window.window())
|
||||
};
|
||||
|
||||
Ok(Box::new(SDL2Renderer {
|
||||
refs,
|
||||
imgui: Rc::new(RefCell::new(imgui)),
|
||||
imgui_event: Rc::new(RefCell::new(imgui_sdl2)),
|
||||
imgui_font_tex,
|
||||
}))
|
||||
}
|
||||
|
@ -827,8 +847,8 @@ impl BackendRenderer for SDL2Renderer {
|
|||
}
|
||||
|
||||
fn prepare_imgui(&mut self, ui: &Ui) -> GameResult {
|
||||
let refs = self.refs.borrow_mut();
|
||||
self.imgui_event.borrow_mut().prepare_render(ui, refs.window.window());
|
||||
// let refs = self.refs.borrow_mut();
|
||||
// self.imgui_event.borrow_mut().prepare_render(ui, refs.window.window());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -926,6 +946,10 @@ impl BackendRenderer for SDL2Renderer {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
struct SDL2Texture {
|
||||
|
|
|
@ -3,8 +3,8 @@
|
|||
use std::error::Error;
|
||||
use std::fmt;
|
||||
use std::string::FromUtf8Error;
|
||||
use std::sync::{Arc, PoisonError};
|
||||
use std::sync::mpsc::SendError;
|
||||
use std::sync::{Arc, PoisonError};
|
||||
|
||||
/// An enum containing all kinds of game framework errors.
|
||||
#[derive(Debug, Clone)]
|
||||
|
@ -44,6 +44,8 @@ pub enum GameError {
|
|||
InvalidValue(String),
|
||||
/// Something went wrong while executing a debug command line command.
|
||||
CommandLineError(String),
|
||||
/// Something went wrong while initializing logger
|
||||
LoggerError(String),
|
||||
}
|
||||
|
||||
impl fmt::Display for GameError {
|
||||
|
@ -147,3 +149,10 @@ impl<T> From<SendError<T>> for GameError {
|
|||
GameError::EventLoopError(errstr)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<log::SetLoggerError> for GameError {
|
||||
fn from(s: log::SetLoggerError) -> GameError {
|
||||
let errstr = format!("Set logger error: {}", s);
|
||||
GameError::LoggerError(errstr)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -160,7 +160,7 @@ impl Filesystem {
|
|||
pub(crate) fn user_read_dir<P: AsRef<path::Path>>(
|
||||
&self,
|
||||
path: P,
|
||||
) -> GameResult<Box<dyn Iterator<Item=path::PathBuf>>> {
|
||||
) -> GameResult<Box<dyn Iterator<Item = path::PathBuf>>> {
|
||||
let itr = self
|
||||
.user_vfs
|
||||
.read_dir(path.as_ref())?
|
||||
|
@ -175,7 +175,7 @@ impl Filesystem {
|
|||
pub(crate) fn read_dir<P: AsRef<path::Path>>(
|
||||
&self,
|
||||
path: P,
|
||||
) -> GameResult<Box<dyn Iterator<Item=path::PathBuf>>> {
|
||||
) -> GameResult<Box<dyn Iterator<Item = path::PathBuf>>> {
|
||||
let itr = self
|
||||
.vfs
|
||||
.read_dir(path.as_ref())?
|
||||
|
@ -221,6 +221,14 @@ impl Filesystem {
|
|||
pub fn mount_user_vfs(&mut self, vfs: Box<dyn vfs::VFS>) {
|
||||
self.user_vfs.push_back(vfs);
|
||||
}
|
||||
|
||||
pub fn unmount_vfs(&mut self, root: &PathBuf) {
|
||||
self.vfs.remove(root);
|
||||
}
|
||||
|
||||
pub fn unmount_user_vfs(&mut self, root: &PathBuf) {
|
||||
self.user_vfs.remove(root);
|
||||
}
|
||||
}
|
||||
|
||||
/// Opens the given path and returns the resulting `File`
|
||||
|
@ -303,7 +311,7 @@ pub fn user_is_dir<P: AsRef<path::Path>>(ctx: &Context, path: P) -> bool {
|
|||
pub fn user_read_dir<P: AsRef<path::Path>>(
|
||||
ctx: &Context,
|
||||
path: P,
|
||||
) -> GameResult<Box<dyn Iterator<Item=path::PathBuf>>> {
|
||||
) -> GameResult<Box<dyn Iterator<Item = path::PathBuf>>> {
|
||||
ctx.filesystem.user_read_dir(path)
|
||||
}
|
||||
|
||||
|
@ -339,7 +347,7 @@ pub fn is_dir<P: AsRef<path::Path>>(ctx: &Context, path: P) -> bool {
|
|||
/// in no particular order.
|
||||
///
|
||||
/// Lists the base directory if an empty path is given.
|
||||
pub fn read_dir<P: AsRef<path::Path>>(ctx: &Context, path: P) -> GameResult<Box<dyn Iterator<Item=path::PathBuf>>> {
|
||||
pub fn read_dir<P: AsRef<path::Path>>(ctx: &Context, path: P) -> GameResult<Box<dyn Iterator<Item = path::PathBuf>>> {
|
||||
ctx.filesystem.read_dir(path)
|
||||
}
|
||||
|
||||
|
@ -347,7 +355,7 @@ pub fn read_dir_find<P: AsRef<path::Path>>(
|
|||
ctx: &Context,
|
||||
roots: &Vec<String>,
|
||||
path: P,
|
||||
) -> GameResult<Box<dyn Iterator<Item=path::PathBuf>>> {
|
||||
) -> GameResult<Box<dyn Iterator<Item = path::PathBuf>>> {
|
||||
let mut files = Vec::new();
|
||||
|
||||
for root in roots {
|
||||
|
@ -383,3 +391,13 @@ pub fn mount_vfs(ctx: &mut Context, vfs: Box<dyn vfs::VFS>) {
|
|||
pub fn mount_user_vfs(ctx: &mut Context, vfs: Box<dyn vfs::VFS>) {
|
||||
ctx.filesystem.mount_user_vfs(vfs)
|
||||
}
|
||||
|
||||
/// Unmounts a VFS with a provided root path.
|
||||
pub fn unmount_vfs(ctx: &mut Context, root: &PathBuf) {
|
||||
ctx.filesystem.unmount_vfs(root)
|
||||
}
|
||||
|
||||
/// Unmounts a user VFS with a provided root path.
|
||||
pub fn unmount_user_vfs(ctx: &mut Context, root: &PathBuf) {
|
||||
ctx.filesystem.unmount_user_vfs(root)
|
||||
}
|
||||
|
|
|
@ -1207,6 +1207,10 @@ impl BackendRenderer for OpenGLRenderer {
|
|||
) -> GameResult<()> {
|
||||
self.draw_arrays(gl::TRIANGLES, vertices, texture, shader)
|
||||
}
|
||||
|
||||
fn as_any(&self) -> &dyn Any {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl OpenGLRenderer {
|
||||
|
|
|
@ -441,6 +441,11 @@ impl OverlayFS {
|
|||
pub fn roots(&self) -> &VecDeque<Box<dyn VFS>> {
|
||||
&self.roots
|
||||
}
|
||||
|
||||
/// Removes a VFS with a provided root.
|
||||
pub fn remove(&mut self, root: &PathBuf) {
|
||||
self.roots.iter().position(|fs| fs.to_path_buf() == Some(root.clone())).map(|i| self.roots.remove(i));
|
||||
}
|
||||
}
|
||||
|
||||
impl VFS for OverlayFS {
|
||||
|
|
|
@ -0,0 +1,221 @@
|
|||
use std::path::PathBuf;
|
||||
|
||||
use crate::{
|
||||
data::builtin_fs::BuiltinFS,
|
||||
framework::{
|
||||
context::Context,
|
||||
error::GameResult,
|
||||
filesystem::{mount_user_vfs, mount_vfs, unmount_user_vfs},
|
||||
vfs::PhysicalFS,
|
||||
},
|
||||
};
|
||||
|
||||
pub struct FilesystemContainer {
|
||||
pub user_path: PathBuf,
|
||||
pub game_path: PathBuf,
|
||||
|
||||
pub is_portable: bool,
|
||||
}
|
||||
|
||||
impl FilesystemContainer {
|
||||
pub fn new() -> Self {
|
||||
Self { user_path: PathBuf::new(), game_path: PathBuf::new(), is_portable: false }
|
||||
}
|
||||
|
||||
pub fn mount_fs(&mut self, context: &mut Context) -> GameResult {
|
||||
#[cfg(not(any(target_os = "android", target_os = "horizon")))]
|
||||
let resource_dir = if let Ok(data_dir) = std::env::var("CAVESTORY_DATA_DIR") {
|
||||
PathBuf::from(data_dir)
|
||||
} else {
|
||||
let mut resource_dir = std::env::current_exe()?;
|
||||
if resource_dir.file_name().is_some() {
|
||||
let _ = resource_dir.pop();
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let mut bundle_dir = resource_dir.clone();
|
||||
let _ = bundle_dir.pop();
|
||||
let mut bundle_exec_dir = bundle_dir.clone();
|
||||
let mut csplus_data_dir = bundle_dir.clone();
|
||||
let _ = csplus_data_dir.pop();
|
||||
let _ = csplus_data_dir.pop();
|
||||
let mut csplus_data_base_dir = csplus_data_dir.clone();
|
||||
csplus_data_base_dir.push("data");
|
||||
csplus_data_base_dir.push("base");
|
||||
|
||||
bundle_exec_dir.push("MacOS");
|
||||
bundle_dir.push("Resources");
|
||||
|
||||
if bundle_exec_dir.is_dir() && bundle_dir.is_dir() {
|
||||
log::info!("Running in macOS bundle mode");
|
||||
|
||||
if csplus_data_base_dir.is_dir() {
|
||||
log::info!("Cave Story+ Steam detected");
|
||||
resource_dir = csplus_data_dir;
|
||||
} else {
|
||||
resource_dir = bundle_dir;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource_dir.push("data");
|
||||
resource_dir
|
||||
};
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "horizon")))]
|
||||
log::info!("Resource directory: {:?}", resource_dir);
|
||||
|
||||
log::info!("Initializing engine...");
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "horizon")))]
|
||||
{
|
||||
mount_vfs(context, Box::new(PhysicalFS::new(&resource_dir, true)));
|
||||
self.game_path = resource_dir.clone();
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "horizon")))]
|
||||
let project_dirs = match directories::ProjectDirs::from("", "", "doukutsu-rs") {
|
||||
Some(dirs) => dirs,
|
||||
None => {
|
||||
use crate::framework::error::GameError;
|
||||
return Err(GameError::FilesystemError(String::from(
|
||||
"No valid home directory path could be retrieved.",
|
||||
)));
|
||||
}
|
||||
};
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
let mut data_path =
|
||||
PathBuf::from(ndk_glue::native_activity().internal_data_path().to_string_lossy().to_string());
|
||||
let mut user_path = data_path.clone();
|
||||
|
||||
data_path.push("data");
|
||||
user_path.push("saves");
|
||||
|
||||
let _ = std::fs::create_dir_all(&data_path);
|
||||
let _ = std::fs::create_dir_all(&user_path);
|
||||
|
||||
log::info!("Android data directories: data_path={:?} user_path={:?}", &data_path, &user_path);
|
||||
|
||||
mount_vfs(context, Box::new(PhysicalFS::new(&data_path, true)));
|
||||
mount_user_vfs(context, Box::new(PhysicalFS::new(&user_path, false)));
|
||||
|
||||
self.user_path = user_path.clone();
|
||||
self.game_path = data_path.clone();
|
||||
}
|
||||
#[cfg(target_os = "horizon")]
|
||||
{
|
||||
let mut data_path = PathBuf::from("sdmc:/switch/doukutsu-rs/data");
|
||||
let mut user_path = PathBuf::from("sdmc:/switch/doukutsu-rs/user");
|
||||
|
||||
let _ = std::fs::create_dir_all(&data_path);
|
||||
let _ = std::fs::create_dir_all(&user_path);
|
||||
|
||||
log::info!("Mounting VFS");
|
||||
mount_vfs(context, Box::new(PhysicalFS::new(&data_path, true)));
|
||||
if crate::framework::backend_horizon::mount_romfs() {
|
||||
mount_vfs(context, Box::new(PhysicalFS::new_lowercase(&PathBuf::from("romfs:/data"))));
|
||||
}
|
||||
log::info!("Mounting user VFS");
|
||||
mount_user_vfs(context, Box::new(PhysicalFS::new(&user_path, false)));
|
||||
log::info!("ok");
|
||||
|
||||
self.user_path = user_path.clone();
|
||||
self.game_path = data_path.clone();
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "horizon")))]
|
||||
{
|
||||
let mut user_dir = resource_dir.clone();
|
||||
user_dir.pop();
|
||||
user_dir.push("user");
|
||||
|
||||
if user_dir.is_dir() {
|
||||
// portable mode
|
||||
mount_user_vfs(context, Box::new(PhysicalFS::new(&user_dir, false)));
|
||||
self.user_path = user_dir.clone();
|
||||
self.is_portable = true;
|
||||
} else {
|
||||
let user_dir = project_dirs.data_local_dir();
|
||||
mount_user_vfs(context, Box::new(PhysicalFS::new(user_dir, false)));
|
||||
|
||||
self.user_path = user_dir.to_path_buf();
|
||||
}
|
||||
}
|
||||
|
||||
log::info!("Mounting built-in FS");
|
||||
mount_vfs(context, Box::new(BuiltinFS::new()));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn open_user_directory(&self) -> GameResult {
|
||||
self.open_directory(self.user_path.clone())
|
||||
}
|
||||
|
||||
pub fn open_game_directory(&self) -> GameResult {
|
||||
self.open_directory(self.game_path.clone())
|
||||
}
|
||||
|
||||
pub fn make_portable_user_directory(&mut self, ctx: &mut Context) -> GameResult {
|
||||
let mut user_dir = self.game_path.clone();
|
||||
user_dir.pop();
|
||||
user_dir.push("user");
|
||||
|
||||
if user_dir.is_dir() {
|
||||
return Ok(()); // portable directory already exists
|
||||
}
|
||||
|
||||
let _ = std::fs::create_dir_all(user_dir.clone());
|
||||
|
||||
// copy user data from current user dir
|
||||
for entry in std::fs::read_dir(&self.user_path)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
let file_name = path.file_name().unwrap().to_str().unwrap();
|
||||
let mut new_path = user_dir.clone();
|
||||
new_path.push(file_name);
|
||||
std::fs::copy(path, new_path)?;
|
||||
}
|
||||
|
||||
// unmount old user dir
|
||||
unmount_user_vfs(ctx, &self.user_path);
|
||||
|
||||
// mount new user dir
|
||||
mount_user_vfs(ctx, Box::new(PhysicalFS::new(&user_dir, false)));
|
||||
|
||||
self.user_path = user_dir.clone();
|
||||
self.is_portable = true;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn open_directory(&self, path: PathBuf) -> GameResult {
|
||||
#[cfg(target_os = "horizon")]
|
||||
return Ok(()); // can't open directories on switch
|
||||
|
||||
#[cfg(target_os = "android")]
|
||||
unsafe {
|
||||
use jni::objects::{JObject, JValue};
|
||||
use jni::JavaVM;
|
||||
|
||||
let vm_ptr = ndk_glue::native_activity().vm();
|
||||
let vm = JavaVM::from_raw(vm_ptr)?;
|
||||
let vm_env = vm.attach_current_thread()?;
|
||||
|
||||
let class = vm_env.new_global_ref(JObject::from_raw(ndk_glue::native_activity().activity()))?;
|
||||
let method = vm_env.call_method(class.as_obj(), "openDir", "(Ljava/lang/String;)V", &[
|
||||
JValue::from(vm_env.new_string(path.to_str().unwrap()).unwrap())
|
||||
])?;
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "horizon")))]
|
||||
open::that(path).map_err(|e| {
|
||||
use crate::framework::error::GameError;
|
||||
GameError::FilesystemError(format!("Failed to open directory: {}", e))
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,18 +1,17 @@
|
|||
use std::collections::HashMap;
|
||||
use std::io;
|
||||
use std::io::{BufRead, BufReader, Cursor, Read};
|
||||
use std::io::{BufRead, BufReader, Read};
|
||||
use std::sync::Arc;
|
||||
|
||||
use byteorder::{LE, ReadBytesExt};
|
||||
use byteorder::{ReadBytesExt, LE};
|
||||
|
||||
use crate::common::{Color, Rect};
|
||||
use crate::framework::context::Context;
|
||||
use crate::framework::error::{GameError, GameResult};
|
||||
use crate::framework::error::GameError::ResourceLoadError;
|
||||
use crate::framework::error::{GameError, GameResult};
|
||||
use crate::framework::filesystem;
|
||||
use crate::game::shared_game_state::TileSize;
|
||||
use crate::game::stage::{PxPackScroll, PxPackStageData, StageData};
|
||||
use crate::util::encoding::read_cur_shift_jis;
|
||||
|
||||
static SUPPORTED_PXM_VERSIONS: [u8; 1] = [0x10];
|
||||
static SUPPORTED_PXE_VERSIONS: [u8; 2] = [0, 0x10];
|
||||
|
@ -84,22 +83,11 @@ impl Map {
|
|||
}
|
||||
|
||||
fn read_string<R: io::Read>(map_data: &mut R) -> GameResult<String> {
|
||||
let mut bytes = map_data.read_u8()? as u32;
|
||||
let bytes = map_data.read_u8()? as u32;
|
||||
let mut raw_chars = Vec::new();
|
||||
raw_chars.resize(bytes as usize, 0u8);
|
||||
map_data.read(&mut raw_chars)?;
|
||||
let mut raw_chars = Cursor::new(raw_chars);
|
||||
|
||||
let mut chars = Vec::new();
|
||||
chars.reserve(bytes as usize);
|
||||
|
||||
while bytes > 0 {
|
||||
let (consumed, chr) = read_cur_shift_jis(&mut raw_chars, bytes);
|
||||
chars.push(chr);
|
||||
bytes -= consumed;
|
||||
}
|
||||
|
||||
Ok(chars.iter().collect())
|
||||
Ok(encoding_rs::SHIFT_JIS.decode_without_bom_handling(&raw_chars).0.into_owned())
|
||||
}
|
||||
|
||||
fn skip_string<R: io::Read>(map_data: &mut R) -> GameResult {
|
||||
|
@ -211,7 +199,7 @@ impl Map {
|
|||
log::warn!("Map attribute data is shorter than 256 bytes!");
|
||||
}
|
||||
} else if let Ok(mut attrib_data) =
|
||||
filesystem::open_find(ctx, roots, ["Stage/", &tileset_fg, ".pxattr"].join(""))
|
||||
filesystem::open_find(ctx, roots, ["Stage/", &tileset_fg, ".pxattr"].join(""))
|
||||
{
|
||||
attrib_data.read_exact(&mut magic)?;
|
||||
|
||||
|
@ -549,7 +537,7 @@ impl WaterParams {
|
|||
}
|
||||
|
||||
pub fn load_from<R: io::Read>(&mut self, data: R) -> GameResult {
|
||||
fn next_u8<'a>(s: &mut impl Iterator<Item=&'a str>, error_msg: &str) -> GameResult<u8> {
|
||||
fn next_u8<'a>(s: &mut impl Iterator<Item = &'a str>, error_msg: &str) -> GameResult<u8> {
|
||||
match s.next() {
|
||||
None => Err(GameError::ParseError("Out of range.".to_string())),
|
||||
Some(v) => v.trim().parse::<u8>().map_err(|_| GameError::ParseError(error_msg.to_string())),
|
||||
|
|
203
src/game/mod.rs
203
src/game/mod.rs
|
@ -1,4 +1,6 @@
|
|||
use std::backtrace::Backtrace;
|
||||
use std::cell::UnsafeCell;
|
||||
use std::panic::PanicInfo;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Mutex;
|
||||
use std::time::{Duration, Instant};
|
||||
|
@ -7,20 +9,19 @@ use lazy_static::lazy_static;
|
|||
|
||||
use scripting::tsc::text_script::ScriptMode;
|
||||
|
||||
use crate::data::builtin_fs::BuiltinFS;
|
||||
use crate::framework::context::Context;
|
||||
use crate::framework::error::GameResult;
|
||||
use crate::framework::filesystem::{mount_user_vfs, mount_vfs};
|
||||
use crate::framework::graphics;
|
||||
use crate::framework::graphics::VSyncMode;
|
||||
use crate::framework::ui::UI;
|
||||
use crate::framework::vfs::PhysicalFS;
|
||||
use crate::game::filesystem_container::FilesystemContainer;
|
||||
use crate::game::shared_game_state::{Fps, SharedGameState, TimingMode};
|
||||
use crate::graphics::texture_set::{G_MAG, I_MAG};
|
||||
use crate::scene::loading_scene::LoadingScene;
|
||||
use crate::scene::Scene;
|
||||
|
||||
pub mod caret;
|
||||
pub mod filesystem_container;
|
||||
pub mod frame;
|
||||
pub mod inventory;
|
||||
pub mod map;
|
||||
|
@ -191,7 +192,7 @@ impl Game {
|
|||
|
||||
if let Some(scene) = &mut self.scene {
|
||||
scene.draw(state_ref, ctx)?;
|
||||
if state_ref.settings.touch_controls {
|
||||
if state_ref.settings.touch_controls && state_ref.settings.display_touch_controls {
|
||||
state_ref.touch_controls.draw(
|
||||
state_ref.canvas_size,
|
||||
state_ref.scale,
|
||||
|
@ -214,119 +215,103 @@ impl Game {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn init(options: LaunchOptions) -> GameResult {
|
||||
let _ = simple_logger::SimpleLogger::new()
|
||||
.without_timestamps()
|
||||
.with_colors(true)
|
||||
.with_level(log::Level::Info.to_level_filter())
|
||||
.init();
|
||||
// For the most part this is just a copy-paste of the code from FilesystemContainer because it logs
|
||||
// some messages during init, but the default logger cannot be replaced with another
|
||||
// one or deinited(so we can't create the console-only logger and replace it by the
|
||||
// console&file logger after FilesystemContainer has been initialized)
|
||||
fn get_logs_dir() -> GameResult<PathBuf> {
|
||||
let mut logs_dir: PathBuf;
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "horizon")))]
|
||||
let resource_dir = if let Ok(data_dir) = std::env::var("CAVESTORY_DATA_DIR") {
|
||||
PathBuf::from(data_dir)
|
||||
} else {
|
||||
let mut resource_dir = std::env::current_exe()?;
|
||||
if resource_dir.file_name().is_some() {
|
||||
let _ = resource_dir.pop();
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let mut bundle_dir = resource_dir.clone();
|
||||
let _ = bundle_dir.pop();
|
||||
let mut bundle_exec_dir = bundle_dir.clone();
|
||||
let mut csplus_data_dir = bundle_dir.clone();
|
||||
let _ = csplus_data_dir.pop();
|
||||
let _ = csplus_data_dir.pop();
|
||||
let mut csplus_data_base_dir = csplus_data_dir.clone();
|
||||
csplus_data_base_dir.push("data");
|
||||
csplus_data_base_dir.push("base");
|
||||
|
||||
bundle_exec_dir.push("MacOS");
|
||||
bundle_dir.push("Resources");
|
||||
|
||||
if bundle_exec_dir.is_dir() && bundle_dir.is_dir() {
|
||||
log::info!("Running in macOS bundle mode");
|
||||
|
||||
if csplus_data_base_dir.is_dir() {
|
||||
log::info!("Cave Story+ Steam detected");
|
||||
resource_dir = csplus_data_dir;
|
||||
} else {
|
||||
resource_dir = bundle_dir;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
resource_dir.push("data");
|
||||
resource_dir
|
||||
};
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "horizon")))]
|
||||
log::info!("Resource directory: {:?}", resource_dir);
|
||||
log::info!("Initializing engine...");
|
||||
|
||||
let mut context = Box::pin(Context::new());
|
||||
#[cfg(not(any(target_os = "android", target_os = "horizon")))]
|
||||
mount_vfs(&mut context, Box::new(PhysicalFS::new(&resource_dir, true)));
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "horizon")))]
|
||||
let project_dirs = match directories::ProjectDirs::from("", "", "doukutsu-rs") {
|
||||
Some(dirs) => dirs,
|
||||
None => {
|
||||
use crate::framework::error::GameError;
|
||||
return Err(GameError::FilesystemError(String::from("No valid home directory path could be retrieved.")));
|
||||
}
|
||||
};
|
||||
#[cfg(target_os = "android")]
|
||||
{
|
||||
let mut data_path =
|
||||
PathBuf::from(ndk_glue::native_activity().internal_data_path().to_string_lossy().to_string());
|
||||
let mut user_path = data_path.clone();
|
||||
|
||||
data_path.push("data");
|
||||
user_path.push("saves");
|
||||
|
||||
let _ = std::fs::create_dir_all(&data_path);
|
||||
let _ = std::fs::create_dir_all(&user_path);
|
||||
|
||||
log::info!("Android data directories: data_path={:?} user_path={:?}", &data_path, &user_path);
|
||||
|
||||
mount_vfs(&mut context, Box::new(PhysicalFS::new(&data_path, true)));
|
||||
mount_user_vfs(&mut context, Box::new(PhysicalFS::new(&user_path, false)));
|
||||
logs_dir = PathBuf::from(ndk_glue::native_activity().internal_data_path().to_string_lossy().to_string());
|
||||
}
|
||||
|
||||
#[cfg(target_os = "horizon")]
|
||||
{
|
||||
let mut data_path = PathBuf::from("sdmc:/switch/doukutsu-rs/data");
|
||||
let mut user_path = PathBuf::from("sdmc:/switch/doukutsu-rs/user");
|
||||
|
||||
let _ = std::fs::create_dir_all(&data_path);
|
||||
let _ = std::fs::create_dir_all(&user_path);
|
||||
|
||||
log::info!("Mounting VFS");
|
||||
mount_vfs(&mut context, Box::new(PhysicalFS::new(&data_path, true)));
|
||||
if crate::framework::backend_horizon::mount_romfs() {
|
||||
mount_vfs(&mut context, Box::new(PhysicalFS::new_lowercase(&PathBuf::from("romfs:/data"))));
|
||||
}
|
||||
log::info!("Mounting user VFS");
|
||||
mount_user_vfs(&mut context, Box::new(PhysicalFS::new(&user_path, false)));
|
||||
log::info!("ok");
|
||||
logs_dir = PathBuf::from("sdmc:/switch/doukutsu-rs");
|
||||
}
|
||||
|
||||
#[cfg(not(any(target_os = "android", target_os = "horizon")))]
|
||||
{
|
||||
if crate::framework::filesystem::open(&context, "/.drs_localstorage").is_ok() {
|
||||
let mut user_dir = resource_dir.clone();
|
||||
user_dir.push("_drs_profile");
|
||||
let project_dirs = match directories::ProjectDirs::from("", "", "doukutsu-rs") {
|
||||
Some(dirs) => dirs,
|
||||
None => {
|
||||
use crate::framework::error::GameError;
|
||||
return Err(GameError::FilesystemError(String::from(
|
||||
"No valid home directory path could be retrieved.",
|
||||
)));
|
||||
}
|
||||
};
|
||||
|
||||
let _ = std::fs::create_dir_all(&user_dir);
|
||||
mount_user_vfs(&mut context, Box::new(PhysicalFS::new(&user_dir, false)));
|
||||
} else {
|
||||
mount_user_vfs(&mut context, Box::new(PhysicalFS::new(project_dirs.data_local_dir(), false)));
|
||||
}
|
||||
logs_dir = project_dirs.data_local_dir().to_path_buf();
|
||||
}
|
||||
|
||||
log::info!("Mounting built-in FS");
|
||||
mount_vfs(&mut context, Box::new(BuiltinFS::new()));
|
||||
logs_dir.push("logs");
|
||||
|
||||
|
||||
Ok(logs_dir)
|
||||
}
|
||||
|
||||
fn init_logger() -> GameResult {
|
||||
let logs_dir = get_logs_dir()?;
|
||||
let _ = std::fs::create_dir_all(&logs_dir);
|
||||
|
||||
|
||||
let mut dispatcher = fern::Dispatch::new()
|
||||
.format(|out, message, record| {
|
||||
out.finish(format_args!(
|
||||
"{} [{}] {}",
|
||||
record.level(),
|
||||
record.module_path().unwrap().to_owned(),
|
||||
message
|
||||
))
|
||||
})
|
||||
.level(log::LevelFilter::Debug)
|
||||
.chain(
|
||||
fern::Dispatch::new()
|
||||
.chain(std::io::stderr())
|
||||
);
|
||||
|
||||
|
||||
let date = chrono::Utc::now();
|
||||
let mut file = logs_dir.clone();
|
||||
file.push(format!("log_{}", date.format("%Y-%m-%d")));
|
||||
file.set_extension("txt");
|
||||
|
||||
dispatcher = dispatcher.chain(
|
||||
fern::Dispatch::new()
|
||||
.level(log::LevelFilter::Info)
|
||||
.chain(fern::log_file(file).unwrap())
|
||||
);
|
||||
dispatcher.apply()?;
|
||||
|
||||
//log::info!("===GAME LAUNCH===");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn panic_hook(info: &PanicInfo<'_>) {
|
||||
let backtrace = Backtrace::force_capture();
|
||||
let msg = info.payload().downcast_ref::<&str>().unwrap_or(&"");
|
||||
let location = info.location();
|
||||
|
||||
if location.is_some() {
|
||||
log::error!("Panic occurred in {} with message: '{msg}'\n {backtrace:#}", location.unwrap().to_string());
|
||||
} else {
|
||||
log::error!("Panic occurred with message: '{msg}'\n {backtrace:#}");
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init(options: LaunchOptions) -> GameResult {
|
||||
let _ = init_logger();
|
||||
std::panic::set_hook(Box::new(panic_hook));
|
||||
|
||||
let mut context = Box::pin(Context::new());
|
||||
|
||||
let mut fs_container = FilesystemContainer::new();
|
||||
fs_container.mount_fs(&mut context)?;
|
||||
|
||||
if options.server_mode {
|
||||
log::info!("Running in server mode...");
|
||||
|
@ -335,8 +320,16 @@ pub fn init(options: LaunchOptions) -> GameResult {
|
|||
|
||||
let mut game = Box::pin(Game::new(&mut context)?);
|
||||
#[cfg(feature = "scripting-lua")]
|
||||
{
|
||||
game.state.get().lua.update_refs(unsafe { &mut *game.state.get() }, &mut context as *mut Context);
|
||||
unsafe {
|
||||
(*game.state.get()).lua.update_refs(&mut *game.state.get(), &mut *context);
|
||||
}
|
||||
|
||||
game.state.get_mut().fs_container = Some(fs_container);
|
||||
|
||||
#[cfg(feature = "discord-rpc")]
|
||||
if game.state.get_mut().settings.discord_rpc {
|
||||
game.state.get_mut().discord_rpc.enabled = true;
|
||||
game.state.get_mut().discord_rpc.start()?;
|
||||
}
|
||||
|
||||
game.state.get_mut().next_scene = Some(Box::new(LoadingScene::new()));
|
||||
|
|
|
@ -64,7 +64,7 @@ impl NPC {
|
|||
let _ = npc_list.spawn(0x100, npc);
|
||||
|
||||
// chaco
|
||||
let mut npc = NPC::create(223, &state.npc_table);
|
||||
let mut npc = NPC::create(93, &state.npc_table);
|
||||
npc.cond.set_alive(true);
|
||||
npc.x = self.x - 0x4600;
|
||||
npc.y = self.y - 0x1c00;
|
||||
|
@ -116,6 +116,16 @@ impl NPC {
|
|||
_ => (),
|
||||
}
|
||||
|
||||
if let Some(parent) = self.get_parent_ref_mut(npc_list) {
|
||||
if self.direction == Direction::Left {
|
||||
self.x = parent.x + 0x2400;
|
||||
self.y = parent.y - 0x7200;
|
||||
} else {
|
||||
self.x = parent.x - 0x4000;
|
||||
self.y = parent.y - 0x6800;
|
||||
}
|
||||
}
|
||||
|
||||
let dir_offset = if self.direction == Direction::Left { 0 } else { 4 };
|
||||
|
||||
self.anim_rect = state.constants.npc.n255_helicopter_blades[self.anim_num as usize + dir_offset];
|
||||
|
|
|
@ -452,9 +452,8 @@ impl NPC {
|
|||
self.vel_y = -0x800;
|
||||
self.npc_flags.set_ignore_solidity(true);
|
||||
|
||||
for npc in npc_list.iter_alive().filter(|npc| npc.npc_type == 117 || npc.npc_type == 150) {
|
||||
npc.cond.set_alive(false)
|
||||
}
|
||||
npc_list.kill_npcs_by_type(150, false, state);
|
||||
npc_list.kill_npcs_by_type(117, false, state);
|
||||
|
||||
let mut npc = NPC::create(355, &state.npc_table);
|
||||
npc.cond.set_alive(true);
|
||||
|
@ -484,19 +483,22 @@ impl NPC {
|
|||
npc.x = x as i32 * 0x2000;
|
||||
npc.y = y as i32 * 0x2000;
|
||||
|
||||
let _ = npc_list.spawn(0x100, npc.clone());
|
||||
let _ = npc_list.spawn(0x100, npc.clone());
|
||||
let _ = npc_list.spawn(0, npc.clone());
|
||||
let _ = npc_list.spawn(0, npc.clone());
|
||||
let _ = npc_list.spawn(0, npc.clone());
|
||||
|
||||
if x > 0 && stage.change_tile(x - 1, y, 0) {
|
||||
npc.x = (x - 1) as i32 * 0x2000;
|
||||
let _ = npc_list.spawn(0x100, npc.clone());
|
||||
let _ = npc_list.spawn(0x100, npc.clone());
|
||||
let _ = npc_list.spawn(0, npc.clone());
|
||||
let _ = npc_list.spawn(0, npc.clone());
|
||||
let _ = npc_list.spawn(0, npc.clone());
|
||||
}
|
||||
|
||||
if x < stage.map.width as usize && stage.change_tile(x + 1, y, 0) {
|
||||
npc.x = (x + 1) as i32 * 0x2000;
|
||||
let _ = npc_list.spawn(0x100, npc.clone());
|
||||
let _ = npc_list.spawn(0x100, npc);
|
||||
let _ = npc_list.spawn(0, npc.clone());
|
||||
let _ = npc_list.spawn(0, npc.clone());
|
||||
let _ = npc_list.spawn(0, npc);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -679,7 +681,7 @@ impl NPC {
|
|||
self.vel_x2 -= 1;
|
||||
self.action_counter = 0;
|
||||
|
||||
let angle = f64::atan2((self.y - player.y) as f64, (self.x - player.x) as f64)
|
||||
let angle = f64::atan2((self.y + 0x800 - player.y) as f64, (self.x - player.x) as f64)
|
||||
+ self.rng.range(-16..16) as f64 * CDEG_RAD;
|
||||
|
||||
let mut npc = NPC::create(11, &state.npc_table);
|
||||
|
@ -739,7 +741,7 @@ impl NPC {
|
|||
self.anim_num = 3;
|
||||
}
|
||||
|
||||
self.vel_y += ((self.target_y - self.y).signum() | 1) * 0x40;
|
||||
self.vel_y += if self.y < self.target_y { 0x40 } else { -0x40 };
|
||||
self.vel_y = clamp(self.vel_y, -0x200, 0x200);
|
||||
}
|
||||
6 => {
|
||||
|
@ -1016,9 +1018,8 @@ impl NPC {
|
|||
}
|
||||
}
|
||||
|
||||
if self.action_counter > 0 {
|
||||
self.action_counter -= 1;
|
||||
} else {
|
||||
self.action_counter = self.action_counter.saturating_sub(1);
|
||||
if self.action_counter == 0 {
|
||||
self.action_num = 2;
|
||||
self.action_counter3 += 1;
|
||||
}
|
||||
|
@ -1247,7 +1248,7 @@ impl NPC {
|
|||
self.x + 0x1000 * self.direction.opposite().vector_x(),
|
||||
self.y,
|
||||
CaretType::Exhaust,
|
||||
self.direction,
|
||||
self.direction.opposite(),
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -865,57 +865,47 @@ impl NPC {
|
|||
players: [&mut Player; 2],
|
||||
npc_list: &NPCList,
|
||||
) -> GameResult {
|
||||
match self.action_num {
|
||||
0 => {
|
||||
if self.action_num == 0 {
|
||||
let player = &players[0];
|
||||
self.x = player.x;
|
||||
self.y = player.y;
|
||||
let player = &players[0];
|
||||
|
||||
let mut npc = NPC::create(321, &state.npc_table);
|
||||
npc.cond.set_alive(true);
|
||||
npc.parent_id = self.id;
|
||||
let _ = npc_list.spawn(0x100, npc);
|
||||
if self.action_num == 0 {
|
||||
self.x = player.x;
|
||||
self.y = player.y;
|
||||
|
||||
self.action_num = 1;
|
||||
}
|
||||
}
|
||||
1 => {
|
||||
let player = &players[0];
|
||||
let mut npc = NPC::create(321, &state.npc_table);
|
||||
npc.cond.set_alive(true);
|
||||
npc.parent_id = self.id;
|
||||
let _ = npc_list.spawn(0x100, npc);
|
||||
|
||||
self.direction = player.direction.opposite();
|
||||
let grounded = player.flags.hit_bottom_wall();
|
||||
|
||||
self.target_x = player.x;
|
||||
|
||||
if player.up {
|
||||
self.target_y = player.y + if grounded { -0x1800 } else { 0x1000 };
|
||||
self.anim_num = if grounded { 1 } else { 2 };
|
||||
self.direction = if grounded { Direction::Up } else { Direction::Bottom };
|
||||
} else if player.down && !grounded {
|
||||
self.target_y = player.y - 0x1000;
|
||||
self.anim_num = 1;
|
||||
self.direction = Direction::Up;
|
||||
} else {
|
||||
self.target_x += if self.direction == Direction::Right { 0xE00 } else { -0xE00 };
|
||||
self.target_y = player.y - 0x600;
|
||||
self.anim_num = 0;
|
||||
}
|
||||
|
||||
self.x += (self.target_x - self.x) / 2;
|
||||
self.y += (self.target_y - self.y) / 2;
|
||||
|
||||
if (player.anim_num & 1) != 0 {
|
||||
self.y -= 0x200
|
||||
};
|
||||
|
||||
let dir_offset = if player.direction.opposite() == Direction::Left { 0 } else { 3 };
|
||||
|
||||
self.anim_rect = state.constants.npc.n320_curly_carried[self.anim_num as usize + dir_offset];
|
||||
}
|
||||
_ => (),
|
||||
self.action_num = 1;
|
||||
}
|
||||
|
||||
let grounded = player.flags.hit_bottom_wall();
|
||||
|
||||
self.target_x = player.x;
|
||||
|
||||
if player.up {
|
||||
self.target_y = player.y + if grounded { -0x1400 } else { 0x1000 };
|
||||
self.anim_num = if grounded { 1 } else { 2 };
|
||||
} else if player.down && !grounded {
|
||||
self.target_y = player.y - 0x1000;
|
||||
self.anim_num = 1;
|
||||
} else {
|
||||
self.target_x += if player.direction == Direction::Left { 0xE00 } else { -0xE00 };
|
||||
self.target_y = player.y - 0x600;
|
||||
self.anim_num = 0;
|
||||
}
|
||||
|
||||
self.x += (self.target_x - self.x) / 2;
|
||||
self.y += (self.target_y - self.y) / 2;
|
||||
|
||||
if (player.anim_num & 1) != 0 {
|
||||
self.y -= 0x200
|
||||
};
|
||||
|
||||
let dir_offset = if player.direction.opposite() == Direction::Left { 0 } else { 3 };
|
||||
|
||||
self.anim_rect = state.constants.npc.n320_curly_carried[self.anim_num as usize + dir_offset];
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -929,21 +919,20 @@ impl NPC {
|
|||
if let Some(npc) = self.get_parent_ref_mut(npc_list) {
|
||||
let player = &players[0];
|
||||
|
||||
self.direction = npc.direction;
|
||||
self.x = npc.x;
|
||||
self.y = npc.y;
|
||||
|
||||
match self.direction {
|
||||
Direction::Right => {
|
||||
self.x += 0x1000;
|
||||
match npc.anim_num {
|
||||
0 => {
|
||||
self.direction = player.direction.opposite();
|
||||
self.x += 0x1000 * self.direction.vector_x();
|
||||
}
|
||||
Direction::Left => {
|
||||
self.x -= 0x1000;
|
||||
}
|
||||
Direction::Up => {
|
||||
1 => {
|
||||
self.direction = Direction::Up;
|
||||
self.y -= 0x1400;
|
||||
}
|
||||
Direction::Bottom => {
|
||||
2 => {
|
||||
self.direction = Direction::Bottom;
|
||||
self.y += 0x1400;
|
||||
}
|
||||
_ => (),
|
||||
|
|
|
@ -238,7 +238,15 @@ impl NPC {
|
|||
self.y += self.vel_y;
|
||||
}
|
||||
|
||||
self.animate(3, 0, 1);
|
||||
self.anim_counter += 1;
|
||||
if self.anim_counter > 3 {
|
||||
self.anim_counter = 0;
|
||||
self.anim_num += 1;
|
||||
}
|
||||
// Not using self.animate because this check needs to be outside of the previous if statement
|
||||
if self.anim_num > 1 {
|
||||
self.anim_num = 0;
|
||||
}
|
||||
|
||||
if self.direction == Direction::Left && self.vel_x > 0 {
|
||||
self.anim_num = 2;
|
||||
|
@ -364,8 +372,8 @@ impl NPC {
|
|||
npc.y = self.y;
|
||||
|
||||
for i in (8..256).step_by(16) {
|
||||
npc.vel_x = ((i as f64 * CDEG_RAD).cos() * -1024.0) as i32;
|
||||
npc.vel_y = ((i as f64 * CDEG_RAD).sin() * -1024.0) as i32;
|
||||
npc.vel_x = ((i as f64 * CDEG_RAD).cos() * 1024.0) as i32;
|
||||
npc.vel_y = ((i as f64 * CDEG_RAD).sin() * 1024.0) as i32;
|
||||
|
||||
let _ = npc_list.spawn(0x100, npc.clone());
|
||||
}
|
||||
|
@ -375,7 +383,6 @@ impl NPC {
|
|||
self.action_counter += 1;
|
||||
if self.action_counter > 50 {
|
||||
self.action_num = 100;
|
||||
self.anim_num = 0;
|
||||
}
|
||||
}
|
||||
100 | 101 => {
|
||||
|
@ -507,8 +514,8 @@ impl NPC {
|
|||
self.target_x += self.vel_x;
|
||||
|
||||
let angle = self.action_counter2 as f64 * CDEG_RAD;
|
||||
self.x = self.target_x + self.action_counter as i32 * (angle.cos() * -512.0) as i32 / 8;
|
||||
self.y = self.target_y + self.action_counter as i32 * (angle.sin() * -512.0) as i32 / 2;
|
||||
self.x = self.target_x + self.action_counter as i32 * (angle.cos() * 512.0) as i32 / 8;
|
||||
self.y = self.target_y + self.action_counter as i32 * (angle.sin() * 512.0) as i32 / 2;
|
||||
|
||||
let mut npc = NPC::create(265, &state.npc_table);
|
||||
npc.cond.set_alive(true);
|
||||
|
@ -652,9 +659,9 @@ impl NPC {
|
|||
self.direction = if self.x > player.x { Direction::Left } else { Direction::Right };
|
||||
|
||||
if self.flags.hit_bottom_wall() {
|
||||
if self.life + 20 > self.action_counter3 {
|
||||
if self.life + 20 >= self.action_counter3 {
|
||||
self.animate(10, 1, 2);
|
||||
} else if player.flags.hit_bottom_wall() && player.x > self.x - 0x6000 && player.x < self.x + 0x6000
|
||||
} else if player.flags.hit_bottom_wall() && player.x > self.x - 0x6000 && player.x < self.x + 0x6000 && self.anim_num != 6
|
||||
{
|
||||
self.anim_num = 6;
|
||||
state.quake_counter = 10;
|
||||
|
@ -900,9 +907,8 @@ impl NPC {
|
|||
}
|
||||
}
|
||||
103 => {
|
||||
if self.action_counter > 0 {
|
||||
self.action_counter -= 2;
|
||||
} else {
|
||||
self.action_counter = self.action_counter.saturating_sub(2);
|
||||
if self.action_counter == 0 {
|
||||
self.action_num = 16;
|
||||
self.vel_x = 0;
|
||||
self.vel_y = -0x200;
|
||||
|
@ -936,7 +942,7 @@ impl NPC {
|
|||
if self.action_counter / 2 % 2 != 0 {
|
||||
self.x = self.target_x;
|
||||
} else {
|
||||
self.x = self.x.wrapping_add(0x200);
|
||||
self.x = self.target_x.wrapping_add(0x200);
|
||||
}
|
||||
}
|
||||
510 | 511 => {
|
||||
|
@ -1072,7 +1078,7 @@ impl NPC {
|
|||
|
||||
self.x += self.vel_x;
|
||||
self.y += self.vel_y;
|
||||
if self.action_counter > 50 || self.flags.any_flag() {
|
||||
if self.action_counter > 50 || self.flags.hit_anything() {
|
||||
self.cond.set_alive(false)
|
||||
}
|
||||
} else if self.direction == Direction::Right {
|
||||
|
@ -1160,7 +1166,7 @@ impl NPC {
|
|||
self.action_counter += 1;
|
||||
if self.action_counter > 250 {
|
||||
self.action_num = 22;
|
||||
npc_list.remove_by_type(270, state);
|
||||
npc_list.kill_npcs_by_type(270, false, state);
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
|
|
|
@ -110,10 +110,6 @@ impl NPC {
|
|||
let player = self.get_closest_player_ref(&players);
|
||||
self.face_player(player);
|
||||
|
||||
if self.target_x < 100 {
|
||||
self.target_x += 1;
|
||||
}
|
||||
|
||||
if self.action_counter >= 8
|
||||
&& self.x - 0xe000 < player.x
|
||||
&& self.x + 0xe000 > player.x
|
||||
|
@ -137,11 +133,10 @@ impl NPC {
|
|||
}
|
||||
|
||||
if self.action_counter >= 8
|
||||
&& self.target_x >= 100
|
||||
&& self.x - 0x6000 < player.x
|
||||
&& self.x + 0x6000 > player.x
|
||||
&& self.y - 0xa000 < player.y
|
||||
&& self.y + 0xa000 > player.y
|
||||
&& self.y + 0x6000 > player.y
|
||||
{
|
||||
self.action_num = 2;
|
||||
self.action_counter = 0;
|
||||
|
@ -527,7 +522,7 @@ impl NPC {
|
|||
self.vel_x = -0x200;
|
||||
}
|
||||
|
||||
self.vel_y += ((self.target_y - self.y).signum() | 1) * 0x08;
|
||||
self.vel_y -= ((self.y - self.target_y).signum() | 1) * 0x08;
|
||||
|
||||
self.vel_x = clamp(self.vel_x, -0x2ff, 0x2ff);
|
||||
self.vel_y = clamp(self.vel_y, -0x100, 0x100);
|
||||
|
@ -587,7 +582,7 @@ impl NPC {
|
|||
}
|
||||
}
|
||||
|
||||
if self.action_counter > 120 && self.action_counter & 0x02 == 1 && self.anim_num == 1 {
|
||||
if self.action_counter > 120 && self.action_counter & 0x02 != 0 && self.anim_num == 1 {
|
||||
self.anim_num = 2;
|
||||
}
|
||||
|
||||
|
@ -665,7 +660,7 @@ impl NPC {
|
|||
}
|
||||
|
||||
self.action_counter += 1;
|
||||
if (self.action_counter & 1) != 0 {
|
||||
if (self.action_counter & 2) != 0 {
|
||||
self.anim_num = 2;
|
||||
} else {
|
||||
self.anim_num = 3;
|
||||
|
@ -770,10 +765,6 @@ impl NPC {
|
|||
self.direction = Direction::Right;
|
||||
}
|
||||
|
||||
if self.target_x < 100 {
|
||||
self.target_x += 1;
|
||||
}
|
||||
|
||||
if self.action_counter >= 8
|
||||
&& self.x - 0xe000 < player.x
|
||||
&& self.x + 0xe000 > player.x
|
||||
|
@ -781,8 +772,11 @@ impl NPC {
|
|||
&& self.y + 0xa000 > player.y
|
||||
{
|
||||
self.anim_num = 1;
|
||||
} else if self.action_counter < 8 {
|
||||
self.action_counter += 1;
|
||||
} else {
|
||||
if self.action_counter < 8 {
|
||||
self.action_counter += 1;
|
||||
}
|
||||
self.anim_num = 0;
|
||||
}
|
||||
|
||||
if self.shock > 0 {
|
||||
|
@ -792,11 +786,10 @@ impl NPC {
|
|||
}
|
||||
|
||||
if self.action_counter >= 8
|
||||
&& self.target_x >= 100
|
||||
&& self.x - 0x6000 < player.x
|
||||
&& self.x + 0x6000 > player.x
|
||||
&& self.y - 0xa000 < player.y
|
||||
&& self.y + 0xa000 > player.y
|
||||
&& self.y + 0x6000 > player.y
|
||||
{
|
||||
self.action_num = 2;
|
||||
self.action_counter = 0;
|
||||
|
@ -810,7 +803,11 @@ impl NPC {
|
|||
self.anim_num = 2;
|
||||
|
||||
self.vel_y = -0x5ff;
|
||||
state.sound_manager.play_sfx(30);
|
||||
|
||||
let player = self.get_closest_player_mut(players);
|
||||
if !player.cond.hidden() {
|
||||
state.sound_manager.play_sfx(30);
|
||||
}
|
||||
|
||||
if self.direction == Direction::Left {
|
||||
self.vel_x = -0x100;
|
||||
|
@ -826,7 +823,10 @@ impl NPC {
|
|||
self.action_num = 1;
|
||||
self.anim_num = 0;
|
||||
|
||||
state.sound_manager.play_sfx(23);
|
||||
let player = self.get_closest_player_mut(players);
|
||||
if !player.cond.hidden() {
|
||||
state.sound_manager.play_sfx(23);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
|
@ -954,7 +954,7 @@ impl NPC {
|
|||
self.vel_y += 0x20;
|
||||
self.action_counter += 1;
|
||||
|
||||
if self.action_counter > 8 && self.flags.any_flag() {
|
||||
if self.action_counter > 8 && self.flags.hit_anything() {
|
||||
self.action_num = 4;
|
||||
self.action_counter = 0;
|
||||
self.damage = 0;
|
||||
|
@ -1185,10 +1185,10 @@ impl NPC {
|
|||
self.vel_x = -0x200;
|
||||
}
|
||||
|
||||
self.vel_y += ((self.target_y - self.y).signum() | 1) * 0x08;
|
||||
self.vel_y -= ((self.y - self.target_y).signum() | 1) * 0x08;
|
||||
|
||||
self.vel_x = clamp(self.vel_x, -0x2ff, 0x2ff);
|
||||
self.vel_y = clamp(self.vel_y, -0x100, 0x100);
|
||||
self.vel_y = clamp(self.vel_y, -0x200, 0x200);
|
||||
|
||||
if self.shock > 0 {
|
||||
self.x += self.vel_x / 2;
|
||||
|
@ -1245,7 +1245,7 @@ impl NPC {
|
|||
}
|
||||
}
|
||||
|
||||
if self.action_counter > 120 && self.action_counter & 0x02 == 1 && self.anim_num == 1 {
|
||||
if self.action_counter > 120 && self.action_counter & 0x02 != 0 && self.anim_num == 1 {
|
||||
self.anim_num = 2;
|
||||
}
|
||||
|
||||
|
|
|
@ -116,7 +116,7 @@ impl NPC {
|
|||
&& self.x - 0x8000 < player.x
|
||||
&& self.x + 0x8000 > player.x
|
||||
&& self.y - 0xa000 < player.y
|
||||
&& self.y + 0xa000 > player.y
|
||||
&& self.y + 0x6000 > player.y
|
||||
{
|
||||
self.action_num = 2;
|
||||
self.action_counter = 0;
|
||||
|
@ -177,7 +177,7 @@ impl NPC {
|
|||
}
|
||||
|
||||
self.action_counter += 1;
|
||||
if self.action_counter > 50 {
|
||||
if self.action_counter >= 50 {
|
||||
self.action_num = 2;
|
||||
self.action_counter = 0;
|
||||
self.vel_y = 0x300;
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
use num_traits::abs;
|
||||
use num_traits::clamp;
|
||||
|
||||
use crate::common::{Direction, Rect};
|
||||
use crate::common::{Direction, Rect, CDEG_RAD};
|
||||
use crate::framework::error::GameResult;
|
||||
use crate::game::caret::CaretType;
|
||||
use crate::game::npc::{NPC, NPCList};
|
||||
|
@ -151,15 +151,15 @@ impl NPC {
|
|||
match self.action_num {
|
||||
0 | 1 => {
|
||||
if self.action_num == 0 {
|
||||
let angle = self.rng.range(0..0xff);
|
||||
self.vel_x = ((angle as f64 * 1.40625).cos() * 512.0) as i32;
|
||||
let angle = self.rng.range(0..0xff) as f64 * CDEG_RAD;
|
||||
self.vel_x = (angle.cos() * 512.0) as i32;
|
||||
self.target_x =
|
||||
self.x + ((angle as f64 * 1.40625 + std::f64::consts::FRAC_2_PI).cos() * 8.0 * 512.0) as i32;
|
||||
self.x + ((angle + 64.0 * CDEG_RAD).cos() * 8.0 * 512.0) as i32;
|
||||
|
||||
let angle = self.rng.range(0..0xff);
|
||||
self.vel_y = ((angle as f64 * 1.40625).sin() * 512.0) as i32;
|
||||
let angle = self.rng.range(0..0xff) as f64 * CDEG_RAD;
|
||||
self.vel_y = (angle.sin() * 512.0) as i32;
|
||||
self.target_y =
|
||||
self.y + ((angle as f64 * 1.40625 + std::f64::consts::FRAC_2_PI).sin() * 8.0 * 512.0) as i32;
|
||||
self.y + ((angle + 64.0 * CDEG_RAD).sin() * 8.0 * 512.0) as i32;
|
||||
|
||||
self.action_num = 1;
|
||||
self.action_counter2 = 120;
|
||||
|
@ -228,7 +228,7 @@ impl NPC {
|
|||
}
|
||||
|
||||
if self.action_counter >= 8
|
||||
&& abs(self.x - player.x) < 0xc000
|
||||
&& abs(self.x - player.x) < 0x10000
|
||||
&& self.y - 0x10000 < player.y
|
||||
&& self.y + 0x6000 > player.y
|
||||
{
|
||||
|
@ -288,7 +288,7 @@ impl NPC {
|
|||
}
|
||||
}
|
||||
4 => {
|
||||
if self.x > player.x {
|
||||
if self.x >= player.x {
|
||||
self.direction = Direction::Left;
|
||||
} else {
|
||||
self.direction = Direction::Right;
|
||||
|
@ -305,7 +305,7 @@ impl NPC {
|
|||
self.damage = 3;
|
||||
} else {
|
||||
if self.action_counter % 4 == 1 {
|
||||
state.sound_manager.play_sfx(110);
|
||||
state.sound_manager.play_sfx(109);
|
||||
}
|
||||
|
||||
if self.flags.hit_bottom_wall() {
|
||||
|
@ -392,7 +392,7 @@ impl NPC {
|
|||
self.clamp_fall_speed();
|
||||
|
||||
self.action_counter += 1;
|
||||
if self.action_counter >= 20 && (self.flags.hit_bottom_wall() || self.y > player.y - 0x2000) {
|
||||
if self.flags.hit_bottom_wall() || (self.action_counter >= 20 && self.y > player.y - 0x2000) {
|
||||
self.action_num = 5;
|
||||
self.anim_num = 2;
|
||||
self.anim_counter = 0;
|
||||
|
@ -609,9 +609,8 @@ impl NPC {
|
|||
}
|
||||
|
||||
if self.action_num == 1 {
|
||||
if self.action_counter > 0 {
|
||||
self.action_counter -= 1;
|
||||
} else {
|
||||
self.action_counter = self.action_counter.saturating_sub(1);
|
||||
if self.action_counter == 0 {
|
||||
self.action_num = 10;
|
||||
}
|
||||
}
|
||||
|
@ -765,15 +764,15 @@ impl NPC {
|
|||
self.npc_flags.set_ignore_solidity(true);
|
||||
} else {
|
||||
self.npc_flags.set_ignore_solidity(false);
|
||||
}
|
||||
|
||||
self.action_counter += 1;
|
||||
self.action_counter += 1;
|
||||
|
||||
if self.rng.range(0..50) == 1 {
|
||||
self.action_num = 2;
|
||||
self.action_counter = 0;
|
||||
self.anim_num = 0;
|
||||
self.anim_counter = 0;
|
||||
if self.rng.range(0..50) == 1 {
|
||||
self.action_num = 2;
|
||||
self.action_counter = 0;
|
||||
self.anim_num = 0;
|
||||
self.anim_counter = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
1 => {
|
||||
|
@ -834,7 +833,7 @@ impl NPC {
|
|||
&& self.action_num != 3
|
||||
&& self.action_counter > 10
|
||||
&& ((self.shock > 0)
|
||||
|| (abs(self.x - player.x) < 0x14000 && abs(self.y - player.y) < 0x8000) && self.rng.range(0..50) == 2)
|
||||
|| (abs(self.x - player.x) <= 0x14000 && abs(self.y - player.y) <= 0x8000) && self.rng.range(0..50) == 2)
|
||||
{
|
||||
self.direction = if self.x >= player.x { Direction::Left } else { Direction::Right };
|
||||
self.action_num = 10;
|
||||
|
@ -1029,9 +1028,8 @@ impl NPC {
|
|||
match self.action_num {
|
||||
0 | 1 => {
|
||||
if self.action_num == 0 {
|
||||
self.action_counter = self.action_counter.saturating_sub(1);
|
||||
if self.action_counter > 0 {
|
||||
self.action_counter -= 1;
|
||||
} else {
|
||||
self.action_num = 1;
|
||||
}
|
||||
|
||||
|
@ -1106,15 +1104,15 @@ impl NPC {
|
|||
self.npc_flags.set_ignore_solidity(true);
|
||||
} else {
|
||||
self.npc_flags.set_ignore_solidity(false);
|
||||
}
|
||||
|
||||
self.action_counter += 1;
|
||||
self.action_counter += 1;
|
||||
|
||||
if self.rng.range(0..50) == 1 {
|
||||
self.action_num = 2;
|
||||
self.action_counter = 0;
|
||||
self.anim_num = 0;
|
||||
self.anim_counter = 0;
|
||||
if self.rng.range(0..50) == 1 {
|
||||
self.action_num = 2;
|
||||
self.action_counter = 0;
|
||||
self.anim_num = 0;
|
||||
self.anim_counter = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
1 => {
|
||||
|
@ -1176,7 +1174,7 @@ impl NPC {
|
|||
&& self.action_num != 3
|
||||
&& self.action_counter > 10
|
||||
&& ((self.shock > 0)
|
||||
|| (abs(self.x - player.x) < 0x14000 && abs(self.y - player.y) < 0x8000) && self.rng.range(0..50) == 2)
|
||||
|| (abs(self.x - player.x) <= 0x14000 && abs(self.y - player.y) <= 0x8000) && self.rng.range(0..50) == 2)
|
||||
{
|
||||
self.direction = if self.x >= player.x { Direction::Left } else { Direction::Right };
|
||||
self.action_num = 10;
|
||||
|
@ -1184,9 +1182,7 @@ impl NPC {
|
|||
self.vel_x = self.direction.vector_x() * 0x100;
|
||||
self.vel_y = -0x2ff;
|
||||
|
||||
if !player.cond.hidden() {
|
||||
state.sound_manager.play_sfx(30);
|
||||
}
|
||||
state.sound_manager.play_sfx(6);
|
||||
}
|
||||
|
||||
self.vel_y = (self.vel_y + 0x80).min(0x5ff);
|
||||
|
@ -1230,7 +1226,7 @@ impl NPC {
|
|||
let player = self.get_closest_player_mut(players);
|
||||
|
||||
self.anim_num = 1;
|
||||
self.direction = if self.x >= player.x { Direction::Left } else { Direction::Right };
|
||||
self.direction = if self.x > player.x { Direction::Left } else { Direction::Right };
|
||||
|
||||
self.action_counter += 1;
|
||||
if self.action_counter > 20 {
|
||||
|
@ -1249,7 +1245,7 @@ impl NPC {
|
|||
if self.anim_num > 2 {
|
||||
let player = self.get_closest_player_mut(players);
|
||||
|
||||
self.direction = if self.x >= player.x { Direction::Left } else { Direction::Right };
|
||||
self.direction = if self.x > player.x { Direction::Left } else { Direction::Right };
|
||||
|
||||
self.vel_x = self.direction.vector_x() * 0x200;
|
||||
self.action_counter2 += 1;
|
||||
|
|
|
@ -83,7 +83,7 @@ impl NPC {
|
|||
self.action_num = 1;
|
||||
|
||||
if (self.direction == Direction::Left && player.x > self.x - 0x24000 && player.x < self.x - 0x22000)
|
||||
|| (player.x < self.x + 0x24000 && player.x > self.x + 0x22000)
|
||||
|| (self.direction != Direction::Left && player.x < self.x + 0x24000 && player.x > self.x + 0x22000)
|
||||
{
|
||||
self.action_num = 10;
|
||||
}
|
||||
|
@ -488,47 +488,44 @@ impl NPC {
|
|||
state: &mut SharedGameState,
|
||||
players: [&mut Player; 2],
|
||||
) -> GameResult {
|
||||
match self.action_num {
|
||||
0 | 1 => {
|
||||
if self.action_num == 0 {
|
||||
self.action_num = 1;
|
||||
if self.action_num == 0 || self.action_num == 1 {
|
||||
if self.action_num == 0 {
|
||||
self.action_num = 1;
|
||||
|
||||
self.vel_x = 0x600 * self.direction.vector_x();
|
||||
self.vel_y = 0x600 * self.direction.vector_y();
|
||||
}
|
||||
|
||||
self.action_counter += 1;
|
||||
if self.action_counter == 16 {
|
||||
self.npc_flags.set_ignore_solidity(false)
|
||||
};
|
||||
|
||||
self.x += self.vel_x;
|
||||
self.y += self.vel_y;
|
||||
|
||||
if self.flags.hit_anything() {
|
||||
self.action_num = 10
|
||||
};
|
||||
|
||||
let player = self.get_closest_player_ref(&players);
|
||||
if self.action_counter > 20
|
||||
&& ((self.direction == Direction::Left && self.x <= player.x + 0x4000)
|
||||
|| (self.direction == Direction::Up && self.y <= player.y + 0x4000)
|
||||
|| (self.direction == Direction::Right && self.x <= player.x - 0x4000)
|
||||
|| (self.direction == Direction::Bottom && self.y <= player.y - 0x4000))
|
||||
{
|
||||
self.action_num = 10
|
||||
}
|
||||
self.vel_x = 0x600 * self.direction.vector_x();
|
||||
self.vel_y = 0x600 * self.direction.vector_y();
|
||||
}
|
||||
10 => {
|
||||
self.npc_type = 309;
|
||||
self.anim_num = 0;
|
||||
self.action_num = 11;
|
||||
self.npc_flags.set_shootable(true);
|
||||
self.npc_flags.set_ignore_solidity(false);
|
||||
self.damage = 5;
|
||||
self.display_bounds.top = 0x1000;
|
||||
|
||||
self.action_counter += 1;
|
||||
if self.action_counter == 16 {
|
||||
self.npc_flags.set_ignore_solidity(false)
|
||||
};
|
||||
|
||||
self.x += self.vel_x;
|
||||
self.y += self.vel_y;
|
||||
|
||||
if self.flags.hit_anything() {
|
||||
self.action_num = 10
|
||||
};
|
||||
|
||||
let player = self.get_closest_player_ref(&players);
|
||||
if self.action_counter > 20
|
||||
&& ((self.direction == Direction::Left && self.x <= player.x + 0x4000)
|
||||
|| (self.direction == Direction::Up && self.y <= player.y + 0x4000)
|
||||
|| (self.direction == Direction::Right && self.x >= player.x - 0x4000)
|
||||
|| (self.direction == Direction::Bottom && self.y >= player.y - 0x4000))
|
||||
{
|
||||
self.action_num = 10
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
if self.action_num == 10 {
|
||||
self.npc_type = 309;
|
||||
self.anim_num = 0;
|
||||
self.action_num = 11;
|
||||
self.npc_flags.set_shootable(true);
|
||||
self.npc_flags.set_ignore_solidity(false);
|
||||
self.damage = 5;
|
||||
self.display_bounds.top = 0x1000;
|
||||
}
|
||||
|
||||
self.animate(3, 0, 3);
|
||||
|
@ -733,7 +730,7 @@ impl NPC {
|
|||
_ => (),
|
||||
}
|
||||
|
||||
self.animate(1, 0, 2);
|
||||
self.animate(0, 0, 2);
|
||||
self.anim_rect = state.constants.npc.n319_mesa_block[self.anim_num as usize];
|
||||
|
||||
Ok(())
|
||||
|
@ -816,12 +813,23 @@ impl NPC {
|
|||
let x = (self.x / (state.tile_size.as_int() * 0x100)) as usize;
|
||||
let y = (self.y / (state.tile_size.as_int() * 0x100)) as usize;
|
||||
|
||||
let mut change_tile_with_smoke = |x, y| {
|
||||
if stage.change_tile(x, y, 0) {
|
||||
let mut npc = NPC::create(4, &state.npc_table);
|
||||
npc.cond.set_alive(true);
|
||||
npc.x = (x as i32) * state.tile_size.as_int() * 0x200;
|
||||
npc.y = (y as i32) * state.tile_size.as_int() * 0x200;
|
||||
let _ = npc_list.spawn(0, npc.clone());
|
||||
let _ = npc_list.spawn(0, npc.clone());
|
||||
let _ = npc_list.spawn(0, npc);
|
||||
}
|
||||
};
|
||||
if self.direction == Direction::Left {
|
||||
stage.change_tile(x / 2, (y + 1) / 2, 0);
|
||||
stage.change_tile(x / 2, (y - 1) / 2, 0);
|
||||
change_tile_with_smoke(x / 2, (y + 1) / 2);
|
||||
change_tile_with_smoke(x / 2, (y - 1) / 2);
|
||||
} else {
|
||||
stage.change_tile((x + 1) / 2, y / 2, 0);
|
||||
stage.change_tile((x - 1) / 2, y / 2, 0);
|
||||
change_tile_with_smoke((x + 1) / 2, y / 2);
|
||||
change_tile_with_smoke((x - 1) / 2, y / 2);
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
|
@ -836,12 +844,20 @@ impl NPC {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn tick_n330_rolling(&mut self, state: &mut SharedGameState, stage: &mut Stage) -> GameResult {
|
||||
pub(crate) fn tick_n330_rolling(&mut self, state: &mut SharedGameState, npc_list: &NPCList, stage: &mut Stage) -> GameResult {
|
||||
match self.action_num {
|
||||
0 => {
|
||||
let x = (self.x / (state.tile_size.as_int() * 0x200)) as usize;
|
||||
let y = (self.y / (state.tile_size.as_int() * 0x200)) as usize;
|
||||
stage.change_tile(x, y, 0);
|
||||
if stage.change_tile(x, y, 0) {
|
||||
let mut npc = NPC::create(4, &state.npc_table);
|
||||
npc.cond.set_alive(true);
|
||||
npc.x = (x as i32) * state.tile_size.as_int() * 0x200;
|
||||
npc.y = (y as i32) * state.tile_size.as_int() * 0x200;
|
||||
let _ = npc_list.spawn(0, npc.clone());
|
||||
let _ = npc_list.spawn(0, npc.clone());
|
||||
let _ = npc_list.spawn(0, npc);
|
||||
}
|
||||
|
||||
self.action_num = if self.direction == Direction::Left { 10 } else { 30 };
|
||||
}
|
||||
|
|
|
@ -280,6 +280,7 @@ impl NPC {
|
|||
0 | 1 => {
|
||||
if self.action_num == 0 {
|
||||
self.action_num = 1;
|
||||
state.sound_manager.play_sfx(72);
|
||||
|
||||
let player = self.get_closest_player_mut(players);
|
||||
self.direction = if self.x > player.x { Direction::Left } else { Direction::Right };
|
||||
|
@ -428,7 +429,7 @@ impl NPC {
|
|||
self.action_counter = 0;
|
||||
self.anim_num = 0;
|
||||
self.action_counter = 0;
|
||||
self.direction = if self.x < player.x { Direction::Right } else { Direction::Left };
|
||||
self.direction = if self.x <= player.x { Direction::Right } else { Direction::Left };
|
||||
}
|
||||
|
||||
self.vel_x = self.direction.vector_x() * 0x200;
|
||||
|
@ -495,8 +496,8 @@ impl NPC {
|
|||
let deg = (if self.direction == Direction::Left { 0x88 } else { 0xf8 } + self.rng.range(-16..16))
|
||||
as f64
|
||||
* CDEG_RAD;
|
||||
let vel_x = (deg.cos() * 1536.0) as i32;
|
||||
let vel_y = (deg.sin() * 1536.0) as i32;
|
||||
let vel_x = (deg.cos() * 2560.0) as i32;
|
||||
let vel_y = (deg.sin() * 2560.0) as i32;
|
||||
|
||||
let mut npc = NPC::create(11, &state.npc_table);
|
||||
|
||||
|
|
|
@ -5,6 +5,7 @@ use crate::game::npc::list::NPCList;
|
|||
use crate::game::npc::NPC;
|
||||
use crate::game::player::Player;
|
||||
use crate::game::shared_game_state::SharedGameState;
|
||||
use crate::game::stage::Stage;
|
||||
use crate::util::rng::RNG;
|
||||
|
||||
impl NPC {
|
||||
|
@ -103,7 +104,11 @@ impl NPC {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn tick_n242_bat_last_cave(&mut self, state: &mut SharedGameState) -> GameResult {
|
||||
pub(crate) fn tick_n242_bat_last_cave(&mut self, state: &mut SharedGameState, stage: &mut Stage) -> GameResult {
|
||||
if self.x < 0 || self.x > stage.map.width as i32 * state.tile_size.as_int() * 0x200 {
|
||||
self.vanish(state);
|
||||
return Ok(());
|
||||
}
|
||||
loop {
|
||||
match self.action_num {
|
||||
0 => {
|
||||
|
@ -183,7 +188,7 @@ impl NPC {
|
|||
|
||||
if hit {
|
||||
for _ in 0..3 {
|
||||
state.create_caret(self.x, self.y, CaretType::Bubble, Direction::Right);
|
||||
state.create_caret(self.x, self.y + 0x800, CaretType::Bubble, Direction::Right);
|
||||
}
|
||||
|
||||
let player = self.get_closest_player_ref(&players);
|
||||
|
@ -289,7 +294,7 @@ impl NPC {
|
|||
10 | 11 => {
|
||||
if self.action_num == 10 {
|
||||
self.action_num = 11;
|
||||
self.anim_num = 2;
|
||||
self.anim_num = 3;
|
||||
self.action_counter = 0;
|
||||
self.npc_flags.set_shootable(true);
|
||||
}
|
||||
|
@ -297,6 +302,7 @@ impl NPC {
|
|||
self.action_counter += 1;
|
||||
match self.action_counter {
|
||||
30 | 40 | 50 => {
|
||||
self.anim_num = 4;
|
||||
let player = self.get_closest_player_ref(&players);
|
||||
|
||||
let mut npc = NPC::create(277, &state.npc_table);
|
||||
|
@ -341,14 +347,15 @@ impl NPC {
|
|||
self.action_counter += 1;
|
||||
match self.action_counter {
|
||||
30 | 40 | 50 => {
|
||||
self.anim_num = 6;
|
||||
let player = self.get_closest_player_ref(&players);
|
||||
|
||||
let mut npc = NPC::create(277, &state.npc_table);
|
||||
npc.cond.set_alive(true);
|
||||
npc.x = self.x;
|
||||
npc.y = self.y;
|
||||
npc.y = self.y - 0x1400;
|
||||
|
||||
let angle = f64::atan2((self.y - player.y) as f64, (self.x - player.x) as f64);
|
||||
let angle = f64::atan2((self.y - 0x1400 - player.y) as f64, (self.x - player.x) as f64);
|
||||
npc.vel_x = (-2048.0 * angle.cos()) as i32;
|
||||
npc.vel_y = (-2048.0 * angle.sin()) as i32;
|
||||
|
||||
|
|
|
@ -399,12 +399,12 @@ impl NPC {
|
|||
0 | 1 => {
|
||||
if self.action_num == 0 {
|
||||
let deg = self.rng.range(0..255) as f64 * CDEG_RAD;
|
||||
self.vel_y = (deg.cos() * -512.0) as i32;
|
||||
self.target_x = self.x + 8 * ((deg + 64.0 * CDEG_RAD).cos() * -512.0) as i32;
|
||||
self.vel_x = (deg.cos() * 512.0) as i32;
|
||||
self.target_x = self.x + 8 * ((deg + 64.0 * CDEG_RAD).cos() * 512.0) as i32;
|
||||
|
||||
let deg = self.rng.range(0..255) as f64 * CDEG_RAD;
|
||||
self.vel_y = (deg.sin() * -512.0) as i32;
|
||||
self.target_y = self.y + 8 * ((deg + 64.0 * CDEG_RAD).sin() * -512.0) as i32;
|
||||
self.vel_y = (deg.sin() * 512.0) as i32;
|
||||
self.target_y = self.y + 8 * ((deg + 64.0 * CDEG_RAD).sin() * 512.0) as i32;
|
||||
|
||||
self.action_num = 1;
|
||||
self.action_counter3 = 120;
|
||||
|
@ -886,9 +886,9 @@ impl NPC {
|
|||
state.sound_manager.play_sfx(25);
|
||||
}
|
||||
|
||||
self.vel_y += 0x10;
|
||||
self.x += self.vel_x;
|
||||
self.y += self.vel_y;
|
||||
self.vel_y += 0x10;
|
||||
|
||||
if self.action_counter != 0 && self.flags.hit_bottom_wall() {
|
||||
state.sound_manager.play_sfx(35);
|
||||
|
@ -915,6 +915,9 @@ impl NPC {
|
|||
players: [&mut Player; 2],
|
||||
npc_list: &NPCList,
|
||||
) -> GameResult {
|
||||
let player = self.get_closest_player_mut(players);
|
||||
self.face_player(player);
|
||||
|
||||
if self.action_num == 0 {
|
||||
self.action_num = 1;
|
||||
self.action_counter = self.rng.range(0..50) as u16;
|
||||
|
@ -937,14 +940,6 @@ impl NPC {
|
|||
self.vel_y = self.vel_y.clamp(-0x200, 0x200);
|
||||
self.y += self.vel_y;
|
||||
|
||||
let player = self.get_closest_player_mut(players);
|
||||
|
||||
if self.x <= player.x {
|
||||
self.direction = Direction::Right;
|
||||
} else {
|
||||
self.direction = Direction::Left;
|
||||
}
|
||||
|
||||
if self.direction != Direction::Left {
|
||||
if player.y < self.y + 0xA000
|
||||
&& player.y > self.y - 0xA000
|
||||
|
@ -1048,12 +1043,12 @@ impl NPC {
|
|||
self.action_num = 25;
|
||||
self.action_counter = 0;
|
||||
self.anim_num = 2;
|
||||
self.vel_y = -0x5ff;
|
||||
self.vel_y = -0x600;
|
||||
|
||||
if self.x >= self.target_x {
|
||||
self.vel_x = -0x100;
|
||||
self.vel_x = -0x80;
|
||||
} else {
|
||||
self.vel_x = 0x100;
|
||||
self.vel_x = 0x80;
|
||||
}
|
||||
} else {
|
||||
state.sound_manager.play_sfx(30);
|
||||
|
@ -1151,6 +1146,10 @@ impl NPC {
|
|||
self.action_num = 2;
|
||||
}
|
||||
|
||||
// So we're going to move *before* checking collisions, huh?
|
||||
self.x += self.vel_x;
|
||||
self.y += self.vel_y;
|
||||
|
||||
let mut hit = false;
|
||||
|
||||
if self.flags.hit_left_wall() {
|
||||
|
@ -1178,6 +1177,8 @@ impl NPC {
|
|||
}
|
||||
2 => {
|
||||
self.vel_y += 0x40;
|
||||
self.x += self.vel_x;
|
||||
self.y += self.vel_y;
|
||||
|
||||
if self.flags.hit_bottom_wall() {
|
||||
self.action_counter2 += 1;
|
||||
|
@ -1190,9 +1191,6 @@ impl NPC {
|
|||
_ => (),
|
||||
}
|
||||
|
||||
self.x += self.vel_x;
|
||||
self.y += self.vel_y;
|
||||
|
||||
self.vel_y = self.vel_y.clamp(-0x5ff, 0x5ff);
|
||||
|
||||
self.anim_num += 1;
|
||||
|
@ -1277,9 +1275,7 @@ impl NPC {
|
|||
self.action_num = 2;
|
||||
self.action_counter = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if self.action_num == 2 {
|
||||
} else if self.action_num == 2 {
|
||||
self.animate(3, 0, 1);
|
||||
|
||||
self.action_counter += 1;
|
||||
|
@ -1415,7 +1411,7 @@ impl NPC {
|
|||
|
||||
for _ in 0..4 {
|
||||
npc.x = self.x + self.rng.range(-12..12) as i32 * 0x200;
|
||||
npc.y = self.y + 0x2000 + self.rng.range(-12..12) as i32 * 0x200;
|
||||
npc.y = self.y + 0x2000;
|
||||
npc.vel_x = self.rng.range(-0x155..0x155) as i32;
|
||||
npc.vel_y = self.rng.range(-0x600..0) as i32;
|
||||
|
||||
|
@ -1524,9 +1520,7 @@ impl NPC {
|
|||
self.action_num = 2;
|
||||
self.vel_y = 0x300;
|
||||
}
|
||||
}
|
||||
|
||||
if self.action_num == 2 {
|
||||
} else if self.action_num == 2 {
|
||||
let player = self.get_closest_player_mut(players);
|
||||
|
||||
self.action_counter3 += 4;
|
||||
|
@ -1559,24 +1553,22 @@ impl NPC {
|
|||
self.action_counter3 = self.tsc_direction;
|
||||
}
|
||||
|
||||
let player = self.get_closest_player_mut(players);
|
||||
|
||||
if self.action_num == 1 {
|
||||
if let Some(parent) = self.get_parent_ref_mut(npc_list) {
|
||||
if parent.npc_type == 187 && parent.cond.alive() {
|
||||
let deg = (self.action_counter3.wrapping_add(parent.action_counter3) & 0xff) as f64 * CDEG_RAD;
|
||||
|
||||
self.x = parent.x + 20 * (deg.sin() * -512.0) as i32;
|
||||
self.y = parent.y + 32 * (deg.cos() * -512.0) as i32;
|
||||
self.x = parent.x + 20 * (deg.sin() * 512.0) as i32;
|
||||
self.y = parent.y + 32 * (deg.cos() * 512.0) as i32;
|
||||
} else {
|
||||
self.vel_x = self.rng.range(-512..512);
|
||||
self.vel_y = self.rng.range(-512..512);
|
||||
self.action_num = 10;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let player = self.get_closest_player_mut(players);
|
||||
|
||||
if self.action_num == 10 {
|
||||
} else if self.action_num == 10 {
|
||||
self.vel_x += if player.x >= self.x { 0x20 } else { -0x20 };
|
||||
|
||||
self.vel_y += if player.y >= self.y { 0x20 } else { -0x20 };
|
||||
|
|
|
@ -27,12 +27,15 @@ impl NPC {
|
|||
self.action_num = 2;
|
||||
self.action_counter = 0;
|
||||
self.anim_num = 1;
|
||||
}
|
||||
|
||||
if self.rng.range(0..150) == 1 {
|
||||
self.action_num = 3;
|
||||
self.action_counter = 50;
|
||||
self.anim_num = 0;
|
||||
} else {
|
||||
if self.rng.range(0..150) == 1 {
|
||||
self.direction = self.direction.opposite();
|
||||
}
|
||||
if self.rng.range(0..150) == 1 {
|
||||
self.action_num = 3;
|
||||
self.action_counter = 50;
|
||||
self.anim_num = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
2 => {
|
||||
|
@ -49,9 +52,8 @@ impl NPC {
|
|||
self.anim_counter = 0;
|
||||
}
|
||||
|
||||
if self.action_counter > 0 {
|
||||
self.action_counter -= 1;
|
||||
} else {
|
||||
self.action_counter = self.action_counter.saturating_sub(1);
|
||||
if self.action_counter == 0 {
|
||||
self.action_num = 0;
|
||||
}
|
||||
|
||||
|
@ -139,7 +141,7 @@ impl NPC {
|
|||
if (self.x - 0x6000 < player.x)
|
||||
&& (self.x + 0x6000 > player.x)
|
||||
&& (self.y - 0x6000 < player.y)
|
||||
&& (self.y + 0x6000 > player.y)
|
||||
&& (self.y + 0x2000 > player.y)
|
||||
{
|
||||
self.anim_num = 1;
|
||||
} else {
|
||||
|
@ -371,9 +373,8 @@ impl NPC {
|
|||
self.anim_counter = 0;
|
||||
}
|
||||
|
||||
if self.action_counter > 0 {
|
||||
self.action_counter -= 1;
|
||||
} else {
|
||||
self.action_counter = self.action_counter.saturating_sub(1);
|
||||
if self.action_counter == 0 {
|
||||
self.action_num = 0;
|
||||
}
|
||||
|
||||
|
@ -442,6 +443,7 @@ impl NPC {
|
|||
0 | 1 => {
|
||||
if self.action_num == 0 {
|
||||
self.action_num = 1;
|
||||
self.anim_num = 0;
|
||||
self.y += 0x800;
|
||||
}
|
||||
|
||||
|
@ -806,6 +808,7 @@ impl NPC {
|
|||
}
|
||||
|
||||
self.face_player(player);
|
||||
self.anim_num = 0;
|
||||
self.action_counter += 1;
|
||||
if self.action_counter > 4 {
|
||||
self.action_num = 120;
|
||||
|
|
|
@ -92,26 +92,30 @@ impl NPC {
|
|||
}
|
||||
|
||||
pub(crate) fn tick_n013_forcefield(&mut self, state: &mut SharedGameState) -> GameResult {
|
||||
self.anim_counter = (self.anim_counter + 1) % 2;
|
||||
if self.anim_counter == 1 {
|
||||
self.anim_num = (self.anim_num + 1) % 4;
|
||||
self.anim_rect = state.constants.npc.n013_forcefield[self.anim_num as usize];
|
||||
}
|
||||
self.anim_num = (self.anim_num + 1) % 4;
|
||||
self.anim_rect = state.constants.npc.n013_forcefield[self.anim_num as usize];
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn tick_n014_key(&mut self, state: &mut SharedGameState) -> GameResult {
|
||||
pub(crate) fn tick_n014_key(&mut self, state: &mut SharedGameState, npc_list: &NPCList) -> GameResult {
|
||||
if self.action_num == 0 {
|
||||
self.action_num = 1;
|
||||
|
||||
if self.direction == Direction::Right {
|
||||
self.vel_y = -0x200;
|
||||
}
|
||||
}
|
||||
|
||||
if self.flags.hit_bottom_wall() {
|
||||
self.npc_flags.set_interactable(true);
|
||||
let mut npc = NPC::create(4, &state.npc_table);
|
||||
npc.cond.set_alive(true);
|
||||
for _ in 0..4 {
|
||||
npc.x = self.x + self.rng.range(-12..12) as i32 * 0x200;
|
||||
npc.y = self.y + self.rng.range(-12..12) as i32 * 0x200;
|
||||
npc.vel_x = self.rng.range(-0x155..0x155) as i32;
|
||||
npc.vel_y = self.rng.range(-0x600..0) as i32;
|
||||
|
||||
let _ = npc_list.spawn(0x100, npc.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.anim_counter += 1;
|
||||
|
@ -164,7 +168,7 @@ impl NPC {
|
|||
}
|
||||
|
||||
self.anim_num = 0;
|
||||
if self.rng.range(0..30) == 10 {
|
||||
if self.rng.range(0..30) == 0 {
|
||||
self.action_num = 2;
|
||||
}
|
||||
}
|
||||
|
@ -188,17 +192,31 @@ impl NPC {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn tick_n016_save_point(&mut self, state: &mut SharedGameState) -> GameResult {
|
||||
pub(crate) fn tick_n016_save_point(&mut self, state: &mut SharedGameState, npc_list: &NPCList) -> GameResult {
|
||||
if self.action_num == 0 {
|
||||
self.npc_flags.set_interactable(true);
|
||||
self.action_num = 1;
|
||||
|
||||
|
||||
if self.direction == Direction::Right {
|
||||
self.npc_flags.set_interactable(false);
|
||||
self.vel_y = -0x200;
|
||||
|
||||
//Creates smoke
|
||||
let mut npc = NPC::create(4, &state.npc_table);
|
||||
npc.cond.set_alive(true);
|
||||
|
||||
for _ in 0..4 {
|
||||
npc.x = self.x + self.rng.range(-12..12) as i32 * 0x200;
|
||||
npc.y = self.y + self.rng.range(-12..12) as i32 * 0x200;
|
||||
npc.vel_x = self.rng.range(-341..341) as i32;
|
||||
npc.vel_y = self.rng.range(-0x600..0) as i32;
|
||||
|
||||
let _ = npc_list.spawn(0x100, npc.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if self.flags.hit_bottom_wall() {
|
||||
if self.action_num == 1 && self.flags.hit_bottom_wall() {
|
||||
self.npc_flags.set_interactable(true);
|
||||
}
|
||||
|
||||
|
@ -214,9 +232,27 @@ impl NPC {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn tick_n017_health_refill(&mut self, state: &mut SharedGameState) -> GameResult {
|
||||
pub(crate) fn tick_n017_health_refill(&mut self, state: &mut SharedGameState, npc_list: &NPCList) -> GameResult {
|
||||
if self.action_num == 0 {
|
||||
self.action_num = 1;
|
||||
|
||||
//Creates smoke when spawned in a shelter
|
||||
if self.direction == Direction::Right {
|
||||
self.vel_y = -0x200;
|
||||
|
||||
//Creates smoke
|
||||
let mut npc = NPC::create(4, &state.npc_table);
|
||||
npc.cond.set_alive(true);
|
||||
|
||||
for _ in 0..4 {
|
||||
npc.x = self.x + self.rng.range(-12..12) as i32 * 0x200;
|
||||
npc.y = self.y + self.rng.range(-12..12) as i32 * 0x200;
|
||||
npc.vel_x = self.rng.range(-341..341) as i32;
|
||||
npc.vel_y = self.rng.range(-0x600..0) as i32;
|
||||
|
||||
let _ = npc_list.spawn(0x100, npc.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match self.action_num {
|
||||
|
@ -238,9 +274,8 @@ impl NPC {
|
|||
self.anim_rect = state.constants.npc.n017_health_refill[0];
|
||||
self.anim_num = 0;
|
||||
|
||||
if self.action_counter > 0 {
|
||||
self.action_counter -= 1;
|
||||
} else {
|
||||
self.action_counter = self.action_counter.saturating_sub(1);
|
||||
if self.action_counter == 0 {
|
||||
self.action_num = 1;
|
||||
}
|
||||
}
|
||||
|
@ -255,9 +290,8 @@ impl NPC {
|
|||
self.anim_rect = state.constants.npc.n017_health_refill[0];
|
||||
}
|
||||
|
||||
if self.action_counter > 0 {
|
||||
self.action_counter -= 1;
|
||||
} else {
|
||||
self.action_counter = self.action_counter.saturating_sub(1);
|
||||
if self.action_counter == 0 {
|
||||
self.action_num = 1;
|
||||
}
|
||||
}
|
||||
|
@ -265,9 +299,8 @@ impl NPC {
|
|||
self.anim_num = 1;
|
||||
self.anim_rect = state.constants.npc.n017_health_refill[1];
|
||||
|
||||
if self.action_counter > 0 {
|
||||
self.action_counter -= 1;
|
||||
} else {
|
||||
self.action_counter = self.action_counter.saturating_sub(1);
|
||||
if self.action_counter == 0 {
|
||||
self.action_num = 1;
|
||||
}
|
||||
}
|
||||
|
@ -390,24 +423,23 @@ impl NPC {
|
|||
pub(crate) fn tick_n030_gunsmith(&mut self, state: &mut SharedGameState) -> GameResult {
|
||||
if self.direction == Direction::Left {
|
||||
match self.action_num {
|
||||
0 => {
|
||||
self.action_num = 1;
|
||||
self.anim_counter = 0;
|
||||
self.anim_rect = state.constants.npc.n030_hermit_gunsmith[0];
|
||||
}
|
||||
1 => {
|
||||
self.action_num = 1;
|
||||
self.anim_counter = 0;
|
||||
0 | 1 => {
|
||||
if self.action_num == 0 {
|
||||
self.action_num = 1;
|
||||
self.anim_counter = 0;
|
||||
self.anim_rect = state.constants.npc.n030_hermit_gunsmith[0];
|
||||
}
|
||||
|
||||
if self.rng.range(0..120) == 10 {
|
||||
self.action_num = 2;
|
||||
self.action_counter = 8;
|
||||
self.action_counter = 0;
|
||||
self.anim_rect = state.constants.npc.n030_hermit_gunsmith[1];
|
||||
}
|
||||
}
|
||||
2 => {
|
||||
if self.action_counter > 0 {
|
||||
self.action_counter -= 1;
|
||||
self.action_counter += 1;
|
||||
if self.action_counter > 8 {
|
||||
self.action_num = 1;
|
||||
self.anim_rect = state.constants.npc.n030_hermit_gunsmith[0];
|
||||
}
|
||||
}
|
||||
|
@ -577,11 +609,14 @@ impl NPC {
|
|||
npc_list: &NPCList,
|
||||
) -> GameResult {
|
||||
if self.direction == Direction::Left {
|
||||
self.anim_counter = (self.anim_counter + 1) % 4;
|
||||
self.anim_num = self.anim_counter / 2;
|
||||
self.animate(1, 0, 2);
|
||||
if self.anim_num > 1 {
|
||||
self.anim_num = 0;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let player = self.get_closest_player_mut(players);
|
||||
if self.anim_num % 2 == 0 && (player.x - self.x).abs() < 0x3c000 {
|
||||
if (player.x - self.x).abs() < 0x28000 && (player.y - self.y).abs() < 0x1E000 {
|
||||
self.action_counter = self.action_counter.wrapping_add(1);
|
||||
|
||||
let mut droplet = NPC::create(73, &state.npc_table);
|
||||
|
@ -593,7 +628,7 @@ impl NPC {
|
|||
droplet.vel_y = 3 * self.rng.range(-0x200..0x80) as i32;
|
||||
let _ = npc_list.spawn(0x100, droplet.clone());
|
||||
|
||||
if self.action_counter % 2 == 0 {
|
||||
if self.action_counter % 2 == 1 {
|
||||
droplet.vel_x = 2 * self.rng.range(-0x200..0x200) as i32;
|
||||
droplet.vel_y = 3 * self.rng.range(-0x200..0x80) as i32;
|
||||
let _ = npc_list.spawn(0x100, droplet);
|
||||
|
@ -715,7 +750,7 @@ impl NPC {
|
|||
self.action_num = 2;
|
||||
}
|
||||
|
||||
self.anim_num = 1;
|
||||
self.anim_num = 0;
|
||||
}
|
||||
2 => {
|
||||
self.anim_counter += 1;
|
||||
|
@ -729,7 +764,7 @@ impl NPC {
|
|||
|
||||
{
|
||||
let i = self.get_closest_player_idx_mut(&players);
|
||||
if (players[i].x - self.x).abs() < 0x3c000
|
||||
if (players[i].x - self.x).abs() < 0x28000
|
||||
&& (players[i].y - self.y).abs() < 0x1e000
|
||||
&& self.rng.range(0..5) == 1
|
||||
{
|
||||
|
@ -775,7 +810,7 @@ impl NPC {
|
|||
self.action_num = 2;
|
||||
}
|
||||
|
||||
self.anim_num = 1;
|
||||
self.anim_num = 0;
|
||||
}
|
||||
2 => {
|
||||
self.anim_counter += 1;
|
||||
|
@ -789,7 +824,7 @@ impl NPC {
|
|||
|
||||
{
|
||||
let i = self.get_closest_player_idx_mut(&players);
|
||||
if (players[i].x - self.x).abs() < 0x3c000
|
||||
if (players[i].x - self.x).abs() < 0x28000
|
||||
&& (players[i].y - self.y).abs() < 0x1e000
|
||||
&& self.rng.range(0..5) == 1
|
||||
{
|
||||
|
@ -834,7 +869,7 @@ impl NPC {
|
|||
self.action_num = 2;
|
||||
}
|
||||
|
||||
self.anim_num = 1;
|
||||
self.anim_num = 0;
|
||||
}
|
||||
2 => {
|
||||
self.anim_counter += 1;
|
||||
|
@ -848,7 +883,7 @@ impl NPC {
|
|||
|
||||
{
|
||||
let i = self.get_closest_player_idx_mut(&players);
|
||||
if (players[i].x - self.x).abs() < 0x3c000
|
||||
if (players[i].x - self.x).abs() < 0x28000
|
||||
&& (players[i].y - self.y).abs() < 0x1e000
|
||||
&& self.rng.range(0..5) == 1
|
||||
{
|
||||
|
@ -890,7 +925,7 @@ impl NPC {
|
|||
self.action_num = 2;
|
||||
}
|
||||
|
||||
self.anim_num = 1;
|
||||
self.anim_num = 0;
|
||||
}
|
||||
2 => {
|
||||
self.anim_counter += 1;
|
||||
|
@ -904,7 +939,7 @@ impl NPC {
|
|||
|
||||
{
|
||||
let i = self.get_closest_player_idx_mut(&players);
|
||||
if (players[i].x - self.x).abs() < 0x3c000
|
||||
if (players[i].x - self.x).abs() < 0x28000
|
||||
&& (players[i].y - self.y).abs() < 0x1e000
|
||||
&& self.rng.range(0..5) == 1
|
||||
{
|
||||
|
@ -1191,8 +1226,8 @@ impl NPC {
|
|||
|
||||
let mut npc = NPC::create(4, &state.npc_table);
|
||||
npc.cond.set_alive(true);
|
||||
for _ in 0..3 {
|
||||
npc.x = self.x + self.rng.range(-12..12) as i32 * 0x200;
|
||||
for _ in 0..4 {
|
||||
npc.x = self.x - 0x2000;
|
||||
npc.y = self.y + self.rng.range(-12..12) as i32 * 0x200;
|
||||
npc.vel_x = self.rng.range(-0x155..0x155) as i32;
|
||||
npc.vel_y = self.rng.range(-0x600..0) as i32;
|
||||
|
@ -1243,8 +1278,8 @@ impl NPC {
|
|||
|
||||
let mut npc = NPC::create(4, &state.npc_table);
|
||||
npc.cond.set_alive(true);
|
||||
for _ in 0..3 {
|
||||
npc.x = self.x + self.rng.range(-12..12) as i32 * 0x200;
|
||||
for _ in 0..4 {
|
||||
npc.x = self.x + 0x2000;
|
||||
npc.y = self.y + self.rng.range(-12..12) as i32 * 0x200;
|
||||
npc.vel_x = self.rng.range(-0x155..0x155) as i32;
|
||||
npc.vel_y = self.rng.range(-0x600..0) as i32;
|
||||
|
@ -1337,9 +1372,9 @@ impl NPC {
|
|||
|
||||
let mut npc = NPC::create(4, &state.npc_table);
|
||||
npc.cond.set_alive(true);
|
||||
for _ in 0..3 {
|
||||
for _ in 0..4 {
|
||||
npc.x = self.x + self.rng.range(-12..12) as i32 * 0x200;
|
||||
npc.y = self.y + self.rng.range(-12..12) as i32 * 0x200;
|
||||
npc.y = self.y - 0x2000;
|
||||
npc.vel_x = self.rng.range(-0x155..0x155) as i32;
|
||||
npc.vel_y = self.rng.range(-0x600..0) as i32;
|
||||
|
||||
|
@ -1389,9 +1424,9 @@ impl NPC {
|
|||
|
||||
let mut npc = NPC::create(4, &state.npc_table);
|
||||
npc.cond.set_alive(true);
|
||||
for _ in 0..3 {
|
||||
for _ in 0..4 {
|
||||
npc.x = self.x + self.rng.range(-12..12) as i32 * 0x200;
|
||||
npc.y = self.y + self.rng.range(-12..12) as i32 * 0x200;
|
||||
npc.y = self.y + 0x2000;
|
||||
npc.vel_x = self.rng.range(-0x155..0x155) as i32;
|
||||
npc.vel_y = self.rng.range(-0x600..0) as i32;
|
||||
|
||||
|
@ -1889,8 +1924,9 @@ impl NPC {
|
|||
state.sound_manager.play_sfx(26);
|
||||
}
|
||||
|
||||
self.action_num = 1;
|
||||
self.action_num = 20;
|
||||
self.anim_num = 0;
|
||||
self.anim_counter = 0;
|
||||
self.damage = 0;
|
||||
self.npc_flags.set_solid_hard(true);
|
||||
}
|
||||
|
@ -1949,7 +1985,7 @@ impl NPC {
|
|||
if self.vel_x < 0 && self.x < -0x2000
|
||||
|| self.vel_x > 0 && self.x > stage.map.width as i32 * state.tile_size.as_int() * 0x200 + 0x2000
|
||||
{
|
||||
self.cond.set_alive(false);
|
||||
self.vanish(state);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
|
@ -2027,7 +2063,7 @@ impl NPC {
|
|||
stage: &mut Stage,
|
||||
) -> GameResult {
|
||||
match self.action_num {
|
||||
0 => {
|
||||
0 | 10 | 11 => {
|
||||
if self.action_num == 0 {
|
||||
match self.direction {
|
||||
Direction::Left => {
|
||||
|
@ -2050,31 +2086,19 @@ impl NPC {
|
|||
}
|
||||
}
|
||||
|
||||
if self.direction == Direction::Up {
|
||||
self.action_num = 11;
|
||||
self.action_counter = 16;
|
||||
|
||||
if self.action_counter > 0 {
|
||||
self.action_counter = self.action_counter.saturating_sub(2);
|
||||
} else {
|
||||
if self.action_num != 0 || self.direction == Direction::Up {
|
||||
if self.action_num == 10 {
|
||||
self.action_num = 11;
|
||||
self.action_counter = 16;
|
||||
}
|
||||
|
||||
self.action_counter = self.action_counter.saturating_sub(2);
|
||||
if self.action_counter == 0 {
|
||||
self.action_num = 100;
|
||||
self.npc_flags.set_invulnerable(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
10 | 11 => {
|
||||
if self.action_num == 10 {
|
||||
self.action_num = 11;
|
||||
self.action_counter = 16;
|
||||
}
|
||||
|
||||
if self.action_counter > 0 {
|
||||
self.action_counter = self.action_counter.saturating_sub(2);
|
||||
} else {
|
||||
self.action_num = 100;
|
||||
self.npc_flags.set_invulnerable(true);
|
||||
}
|
||||
}
|
||||
100 => {
|
||||
self.vel_y += 0x40;
|
||||
if self.vel_y > 0x700 {
|
||||
|
@ -2125,7 +2149,7 @@ impl NPC {
|
|||
|
||||
if self.action_num == 11 {
|
||||
self.anim_rect.top += self.action_counter;
|
||||
self.anim_rect.bottom += self.action_counter;
|
||||
self.anim_rect.bottom -= self.action_counter;
|
||||
self.display_bounds.top = (16u32).saturating_sub(self.action_counter as u32) * 0x200;
|
||||
}
|
||||
|
||||
|
@ -2247,9 +2271,7 @@ impl NPC {
|
|||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
if self.action_num == 1 {
|
||||
} else if self.action_num == 1 {
|
||||
self.x += self.vel_x;
|
||||
self.y += self.vel_y;
|
||||
|
||||
|
@ -2327,24 +2349,28 @@ impl NPC {
|
|||
pub(crate) fn tick_n302_camera_focus_marker(
|
||||
&mut self,
|
||||
state: &mut SharedGameState,
|
||||
players: [&mut Player; 2],
|
||||
mut players: [&mut Player; 2],
|
||||
npc_list: &NPCList,
|
||||
boss: &mut BossNPC,
|
||||
) -> GameResult {
|
||||
let player = &players[state.textscript_vm.executor_player.index()];
|
||||
let player = &mut players[state.textscript_vm.executor_player.index()];
|
||||
|
||||
match self.action_num {
|
||||
10 => {
|
||||
self.x = player.x;
|
||||
self.y = player.y - 0x4000;
|
||||
}
|
||||
20 => match self.direction {
|
||||
Direction::Left => self.x -= 0x400,
|
||||
Direction::Up => self.y -= 0x400,
|
||||
Direction::Right => self.x += 0x400,
|
||||
Direction::Bottom => self.y += 0x400,
|
||||
_ => (),
|
||||
},
|
||||
20 => {
|
||||
match self.direction {
|
||||
Direction::Left => self.x -= 0x400,
|
||||
Direction::Up => self.y -= 0x400,
|
||||
Direction::Right => self.x += 0x400,
|
||||
Direction::Bottom => self.y += 0x400,
|
||||
_ => (),
|
||||
}
|
||||
player.x = self.x;
|
||||
player.y = self.y;
|
||||
}
|
||||
30 => {
|
||||
self.x = player.x;
|
||||
self.y = player.y + 0xa000;
|
||||
|
@ -2524,17 +2550,19 @@ impl NPC {
|
|||
}
|
||||
}
|
||||
|
||||
self.vel_y += 0x40;
|
||||
self.clamp_fall_speed();
|
||||
if self.action_num == 1 {
|
||||
self.vel_y += 0x40;
|
||||
self.clamp_fall_speed();
|
||||
|
||||
if self.flags.hit_bottom_wall() {
|
||||
self.action_num = 2;
|
||||
self.anim_num = 1;
|
||||
self.vel_y = 0;
|
||||
if self.flags.hit_bottom_wall() {
|
||||
self.action_num = 2;
|
||||
self.anim_num = 1;
|
||||
self.vel_y = 0;
|
||||
}
|
||||
|
||||
self.y += self.vel_y;
|
||||
}
|
||||
|
||||
self.y += self.vel_y;
|
||||
|
||||
self.anim_rect = state.constants.npc.n352_ending_characters
|
||||
[(2 * self.action_counter2 as usize + self.anim_num as usize) % 28];
|
||||
|
||||
|
@ -2655,7 +2683,7 @@ impl NPC {
|
|||
let player = self.get_closest_player_mut(players);
|
||||
if (player.x - self.x).abs() < 0x28000
|
||||
&& player.y < self.y + 0x28000
|
||||
&& player.y > self.y - 0x12c00
|
||||
&& player.y > self.y - 0x14000
|
||||
&& self.rng.range(0..100) == 2
|
||||
{
|
||||
let mut npc = NPC::create(73, &state.npc_table);
|
||||
|
|
|
@ -24,9 +24,9 @@ impl NPC {
|
|||
self.target_x = npc.x;
|
||||
self.target_y = npc.y;
|
||||
|
||||
let angle = ((self.y - self.target_y) as f64 / (self.x - self.target_x) as f64).atan();
|
||||
self.vel_x = (angle.cos() * 1024.0) as i32;
|
||||
self.vel_y = (angle.sin() * 1024.0) as i32;
|
||||
let angle = f64::atan2((self.y - self.target_y) as f64, (self.x - self.target_x) as f64);
|
||||
self.vel_x = (angle.cos() * -1024.0) as i32;
|
||||
self.vel_y = (angle.sin() * -1024.0) as i32;
|
||||
}
|
||||
|
||||
if self.action_counter2 == 0 {
|
||||
|
@ -144,6 +144,7 @@ impl NPC {
|
|||
npc.cond.set_alive(true);
|
||||
npc.x = self.x;
|
||||
npc.y = self.y - 0x2000;
|
||||
npc.parent_id = self.id; // This NPC doesn't do anything with its parent...but we'll set it anyways
|
||||
|
||||
let _ = npc_list.spawn(0, npc);
|
||||
}
|
||||
|
@ -270,6 +271,7 @@ impl NPC {
|
|||
let mut npc = NPC::create(66, &state.npc_table);
|
||||
npc.x = self.x;
|
||||
npc.y = self.y - 0x2000;
|
||||
npc.parent_id = self.id; // This NPC doesn't do anything with its parent...but we'll set it anyways
|
||||
npc.cond.set_alive(true);
|
||||
|
||||
let _ = npc_list.spawn(0, npc);
|
||||
|
@ -318,7 +320,8 @@ impl NPC {
|
|||
27 => {
|
||||
self.action_counter += 1;
|
||||
if self.action_counter == 50 {
|
||||
self.action_num = 14;
|
||||
self.action_num = 0;
|
||||
self.anim_num = 0;
|
||||
}
|
||||
}
|
||||
30 | 31 => {
|
||||
|
@ -363,6 +366,10 @@ impl NPC {
|
|||
|
||||
let _ = npc_list.spawn(0x100, npc);
|
||||
}
|
||||
|
||||
if self.action_counter > 50 {
|
||||
self.action_num = 0;
|
||||
}
|
||||
}
|
||||
50 => {
|
||||
self.anim_num = 8;
|
||||
|
@ -607,7 +614,7 @@ impl NPC {
|
|||
}
|
||||
|
||||
let player = self.get_closest_player_ref(&players);
|
||||
self.action_num = if player.x > self.x - 0xe000 && player.x <= self.x + 0xe000 { 100 } else { 160 };
|
||||
self.action_num = if player.x >= self.x - 0xe000 && player.x <= self.x + 0xe000 { 100 } else { 160 };
|
||||
}
|
||||
}
|
||||
160 | 161 => {
|
||||
|
@ -657,7 +664,7 @@ impl NPC {
|
|||
self.vel_x = 0;
|
||||
self.vel_y = 0;
|
||||
|
||||
npc_list.remove_by_type(252, state);
|
||||
npc_list.kill_npcs_by_type(252, true, state);
|
||||
|
||||
let mut npc = NPC::create(4, &state.npc_table);
|
||||
npc.cond.set_alive(true);
|
||||
|
@ -696,7 +703,7 @@ impl NPC {
|
|||
}
|
||||
|
||||
pub(crate) fn tick_n248_misery_boss_vanishing(&mut self, state: &mut SharedGameState) -> GameResult {
|
||||
if self.flags.any_flag() {
|
||||
if self.flags.hit_anything() {
|
||||
self.cond.set_alive(false);
|
||||
state.create_caret(self.x, self.y, CaretType::ProjectileDissipation, Direction::Left);
|
||||
}
|
||||
|
@ -810,7 +817,7 @@ impl NPC {
|
|||
self.anim_num = (self.anim_num + 1) & 1;
|
||||
self.y += 0x1000;
|
||||
|
||||
if self.flags.any_flag() {
|
||||
if self.flags.hit_anything() {
|
||||
npc_list.create_death_smoke(self.x, self.y, self.display_bounds.right as usize, 3, state, &self.rng);
|
||||
self.cond.set_alive(false);
|
||||
}
|
||||
|
@ -844,10 +851,10 @@ impl NPC {
|
|||
|
||||
if let Some(parent) = self.get_parent_ref_mut(npc_list) {
|
||||
self.x = parent.x
|
||||
+ self.action_counter as i32 * ((self.action_counter2 as f64 * CDEG_RAD).cos() * -512.0) as i32
|
||||
+ self.action_counter as i32 * ((self.action_counter2 as f64 * CDEG_RAD).cos() * 512.0) as i32
|
||||
/ 4;
|
||||
self.y = parent.y
|
||||
+ self.action_counter as i32 * ((self.action_counter2 as f64 * CDEG_RAD).sin() * -512.0) as i32
|
||||
+ self.action_counter as i32 * ((self.action_counter2 as f64 * CDEG_RAD).sin() * 512.0) as i32
|
||||
/ 4;
|
||||
|
||||
if parent.action_num == 151 {
|
||||
|
@ -919,7 +926,7 @@ impl NPC {
|
|||
|
||||
match self.action_num {
|
||||
0 | 1 => {
|
||||
if self.action_num == 9 {
|
||||
if self.action_num == 0 {
|
||||
self.action_num = 1;
|
||||
self.y -= 0x1000;
|
||||
state.sound_manager.play_sfx(29);
|
||||
|
@ -933,7 +940,7 @@ impl NPC {
|
|||
self.anim_num = 9;
|
||||
}
|
||||
20 | 21 => {
|
||||
if self.action_num == 21 {
|
||||
if self.action_num == 20 {
|
||||
self.action_num = 21;
|
||||
self.action_counter = 0;
|
||||
self.anim_num = 0;
|
||||
|
@ -954,10 +961,10 @@ impl NPC {
|
|||
|
||||
let player = self.get_closest_player_ref(&players);
|
||||
|
||||
self.direction = if player.x > self.x { Direction::Left } else { Direction::Right };
|
||||
self.direction = if player.x > self.x { Direction::Right } else { Direction::Left };
|
||||
}
|
||||
30 | 31 => {
|
||||
if self.action_num == 31 {
|
||||
if self.action_num == 30 {
|
||||
self.action_num = 31;
|
||||
self.action_counter = 0;
|
||||
self.anim_num = 2;
|
||||
|
@ -997,6 +1004,8 @@ impl NPC {
|
|||
self.vel_x = 0;
|
||||
self.vel_y = 0;
|
||||
|
||||
state.sound_manager.play_sfx(103);
|
||||
|
||||
let player = self.get_closest_player_ref(&players);
|
||||
self.direction = if self.x > player.x { Direction::Left } else { Direction::Right };
|
||||
self.action_counter3 = if player.y >= 0x14000 { 289 } else { 290 };
|
||||
|
@ -1045,7 +1054,7 @@ impl NPC {
|
|||
if self.action_counter > 50 {
|
||||
self.action_num = 30;
|
||||
self.vel_y = -0x200;
|
||||
self.vel_x = self.direction.vector_x() * 0x200;
|
||||
self.vel_x = self.direction.opposite().vector_x() * 0x200;
|
||||
}
|
||||
}
|
||||
50 | 51 => {
|
||||
|
@ -1070,17 +1079,17 @@ impl NPC {
|
|||
|
||||
let mut npc = NPC::create(301, &state.npc_table);
|
||||
npc.cond.set_alive(true);
|
||||
npc.x = self.x + 0x1400 * self.direction.vector_x();
|
||||
npc.x = self.x + 0x1400 * self.direction.opposite().vector_x();
|
||||
npc.y = self.y;
|
||||
npc.tsc_direction = match ((self.action_counter / 6) & 3, self.direction) {
|
||||
(0, Direction::Left) => 0x58,
|
||||
(1, Direction::Left) => 0x6C,
|
||||
(2, Direction::Left) => 0x94,
|
||||
(3, Direction::Left) => 0xA8,
|
||||
(0, _) => 0xD8,
|
||||
(1, _) => 0xEC,
|
||||
(2, _) => 0x14,
|
||||
(3, _) => 0x28,
|
||||
(0, Direction::Left) => 0xD8,
|
||||
(1, Direction::Left) => 0xEC,
|
||||
(2, Direction::Left) => 0x14,
|
||||
(3, Direction::Left) => 0x28,
|
||||
(0, _) => 0x58,
|
||||
(1, _) => 0x6C,
|
||||
(2, _) => 0x94,
|
||||
(3, _) => 0xA8,
|
||||
_ => unsafe {
|
||||
unreachable_unchecked();
|
||||
},
|
||||
|
@ -1191,6 +1200,7 @@ impl NPC {
|
|||
|
||||
if self.y > stage.map.height as i32 * state.tile_size.as_int() * 0x200 {
|
||||
self.vanish(state);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
|
|
|
@ -537,8 +537,8 @@ impl NPC {
|
|||
0 | 1 => {
|
||||
if self.action_num == 0 {
|
||||
self.action_num = 1;
|
||||
self.vel_x = ((self.rng.range(0..255) as f64 * CDEG_RAD).cos() * -512.0) as i32;
|
||||
self.vel_y = ((self.rng.range(0..255) as f64 * CDEG_RAD).sin() * -512.0) as i32;
|
||||
self.vel_x = ((self.rng.range(0..255) as f64 * CDEG_RAD).cos() * 512.0) as i32;
|
||||
self.vel_y = ((self.rng.range(0..255) as f64 * CDEG_RAD).sin() * 512.0) as i32;
|
||||
|
||||
self.action_counter2 = 120;
|
||||
self.vel_y2 = self.rng.range(-32..32) * 0x200;
|
||||
|
@ -902,7 +902,7 @@ impl NPC {
|
|||
self.x += self.vel_x;
|
||||
self.y += self.vel_y;
|
||||
|
||||
if self.flags.any_flag() {
|
||||
if self.flags.hit_anything() {
|
||||
let mut npc = NPC::create(4, &state.npc_table);
|
||||
npc.cond.set_alive(true);
|
||||
npc.x = self.x;
|
||||
|
@ -994,9 +994,9 @@ impl NPC {
|
|||
let mut npc = NPC::create(273, &state.npc_table);
|
||||
npc.cond.set_alive(true);
|
||||
npc.x = self.x;
|
||||
npc.y = self.y;
|
||||
npc.y = self.y - 0x1400;
|
||||
|
||||
let angle = f64::atan2((player.y - 0x1400 - self.y) as f64, (player.x - self.x) as f64);
|
||||
let angle = f64::atan2((player.y + 0x1400 - self.y) as f64, (player.x - self.x) as f64);
|
||||
npc.vel_x = (2048.0 * angle.cos()) as i32;
|
||||
npc.vel_y = (2048.0 * angle.sin()) as i32;
|
||||
|
||||
|
|
|
@ -13,15 +13,15 @@ use crate::util::rng::RNG;
|
|||
impl NPC {
|
||||
pub(crate) fn tick_n044_polish(&mut self, state: &mut SharedGameState, npc_list: &NPCList) -> GameResult {
|
||||
match self.action_num {
|
||||
0 | 1 => {
|
||||
self.anim_num = 0;
|
||||
self.action_num = match self.direction {
|
||||
Direction::Left => 8,
|
||||
Direction::Right => 2,
|
||||
_ => 8,
|
||||
};
|
||||
}
|
||||
2 => {
|
||||
0 | 1 | 2 => {
|
||||
if self.action_num <= 1 {
|
||||
self.anim_num = 0;
|
||||
self.action_num = match self.direction {
|
||||
Direction::Left => 8,
|
||||
Direction::Right => 2,
|
||||
_ => 8,
|
||||
};
|
||||
}
|
||||
self.vel_y += 0x20;
|
||||
if self.vel_y > 0 && self.flags.hit_bottom_wall() {
|
||||
self.vel_y = -0x100;
|
||||
|
@ -575,7 +575,7 @@ impl NPC {
|
|||
|
||||
let parent = parent.unwrap();
|
||||
|
||||
let angle = self.vel_x + parent.vel_y2;
|
||||
let angle = (self.vel_x + parent.vel_y2) & 0xFF;
|
||||
|
||||
if self.action_num < 2 {
|
||||
if self.action_num == 0 {
|
||||
|
@ -773,22 +773,25 @@ impl NPC {
|
|||
self.anim_counter = self.rng.range(0..4) as u16;
|
||||
self.action_counter2 = 120;
|
||||
|
||||
let mut angle = self.rng.range(0..255);
|
||||
let angle = self.rng.range(0..255);
|
||||
|
||||
self.vel_x = ((angle as f64 * CDEG_RAD).cos() * -512.0) as i32;
|
||||
angle += 0x40;
|
||||
self.target_x = self.x + 8 * ((angle as f64 * CDEG_RAD).cos() * -512.0) as i32;
|
||||
self.vel_y = ((angle as f64 * CDEG_RAD).sin() * -512.0) as i32;
|
||||
angle += 0x40;
|
||||
self.target_y = self.y + 8 * ((angle as f64 * CDEG_RAD).sin() * -512.0) as i32;
|
||||
self.vel_x = ((angle as f64 * CDEG_RAD).cos() * 512.0) as i32;
|
||||
self.target_x = self.x + 8 * (((angle + 0x40) as f64 * CDEG_RAD).cos() * 512.0) as i32;
|
||||
|
||||
let angle = self.rng.range(0..255);
|
||||
self.vel_y = ((angle as f64 * CDEG_RAD).sin() * 512.0) as i32;
|
||||
self.target_y = self.y + 8 * (((angle + 0x40) as f64 * CDEG_RAD).sin() * 512.0) as i32;
|
||||
}
|
||||
|
||||
let player = self.get_closest_player_mut(players);
|
||||
|
||||
self.direction = if self.x > player.x { Direction::Left } else { Direction::Right };
|
||||
|
||||
self.vel_x += ((self.target_x - self.x).signum() * 0x10).clamp(-0x200, 0x200);
|
||||
self.vel_y += ((self.target_y - self.y).signum() * 0x10).clamp(-0x200, 0x200);
|
||||
self.vel_x += (self.target_x - self.x).signum() * 0x10;
|
||||
self.vel_y += (self.target_y - self.y).signum() * 0x10;
|
||||
|
||||
self.vel_x = clamp(self.vel_x, -0x200, 0x200);
|
||||
self.vel_y = clamp(self.vel_y, -0x200, 0x200);
|
||||
|
||||
if self.shock != 0 {
|
||||
self.action_num = 2;
|
||||
|
@ -802,7 +805,7 @@ impl NPC {
|
|||
let player = self.get_closest_player_mut(players);
|
||||
|
||||
self.direction = if self.x > player.x { Direction::Left } else { Direction::Right };
|
||||
self.vel_x += if self.y <= player.y + 0x4000 {
|
||||
self.vel_x += if self.y <= player.y + 0x6000 {
|
||||
(player.x - self.x).signum() * 0x10
|
||||
} else {
|
||||
(self.x - player.x).signum() * 0x10
|
||||
|
@ -1020,7 +1023,7 @@ impl NPC {
|
|||
}
|
||||
self.vel_y += 32;
|
||||
|
||||
self.vel_x = self.vel_x.clamp(-0x200, 0x200);
|
||||
self.vel_x = self.vel_x.clamp(-0x1FF, 0x1FF);
|
||||
|
||||
self.clamp_fall_speed();
|
||||
self.y += self.vel_y;
|
||||
|
@ -1223,23 +1226,24 @@ impl NPC {
|
|||
}
|
||||
}
|
||||
}
|
||||
2 => {
|
||||
self.action_counter += 1;
|
||||
if self.action_counter > 8 {
|
||||
self.action_num = 1;
|
||||
self.anim_num = 0;
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
self.action_counter += 1;
|
||||
if self.action_counter > 8 {
|
||||
self.action_num = 1;
|
||||
self.anim_num = 0;
|
||||
}
|
||||
|
||||
self.vel_y += 0x40;
|
||||
self.clamp_fall_speed();
|
||||
self.x += self.vel_x;
|
||||
self.y += self.vel_y;
|
||||
|
||||
let anim = if self.direction == Direction::Left { 0 } else { 4 };
|
||||
let dir_offset = if self.direction == Direction::Left { 0 } else { 4 };
|
||||
|
||||
self.anim_rect = state.constants.npc.n130_puppy_sitting[anim];
|
||||
self.anim_rect = state.constants.npc.n130_puppy_sitting[self.anim_num as usize + dir_offset];
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
@ -1263,6 +1267,7 @@ impl NPC {
|
|||
state: &mut SharedGameState,
|
||||
players: [&mut Player; 2],
|
||||
) -> GameResult {
|
||||
let player = self.get_closest_player_mut(players);
|
||||
match self.action_num {
|
||||
0 | 1 => {
|
||||
if self.action_num == 0 {
|
||||
|
@ -1277,13 +1282,16 @@ impl NPC {
|
|||
self.anim_num = 1;
|
||||
}
|
||||
|
||||
let player = self.get_closest_player_mut(players);
|
||||
if (self.x - player.x).abs() < 0x8000 && (self.y - player.y).abs() < 0x2000 {
|
||||
self.animate(4, 2, 4);
|
||||
|
||||
if self.anim_num == 4 && self.anim_counter == 0 {
|
||||
state.sound_manager.play_sfx(105);
|
||||
}
|
||||
} else {
|
||||
if self.anim_num == 4 {
|
||||
self.anim_num = 2;
|
||||
}
|
||||
}
|
||||
}
|
||||
2 => {
|
||||
|
@ -1343,6 +1351,10 @@ impl NPC {
|
|||
_ => (),
|
||||
}
|
||||
|
||||
if self.action_num < 100 {
|
||||
self.face_player(player);
|
||||
}
|
||||
|
||||
self.vel_y += 0x40;
|
||||
self.clamp_fall_speed();
|
||||
|
||||
|
|
|
@ -340,7 +340,7 @@ impl NPC {
|
|||
stage: &mut Stage,
|
||||
boss: &mut BossNPC,
|
||||
) -> GameResult {
|
||||
if self.action_num < 100 && (!boss.parts[0].cond.alive() || self.life < 400) {
|
||||
if self.action_num < 100 && (!boss.parts[0].cond.alive() || self.life < 500) {
|
||||
self.action_num = 100;
|
||||
}
|
||||
|
||||
|
@ -403,7 +403,7 @@ impl NPC {
|
|||
|
||||
let player = self.get_closest_player_ref(&players);
|
||||
|
||||
self.direction = if player.x > self.x { Direction::Left } else { Direction::Right };
|
||||
self.direction = if player.x > self.x { Direction::Right } else { Direction::Left };
|
||||
|
||||
if self.life + 50 < self.action_counter3 {
|
||||
self.action_counter3 = self.life;
|
||||
|
@ -451,7 +451,7 @@ impl NPC {
|
|||
let half_h = stage.map.height as i32 * state.tile_size.as_int() * 0x200 / 2;
|
||||
|
||||
if ((self.x < half_w && self.vel_x > 0) || (self.x > half_w && self.vel_x < 0))
|
||||
|| ((self.y < half_h && self.vel_y > 0) || (self.y > half_h && self.vel_y < 0))
|
||||
&& ((self.y < half_h && self.vel_y > 0) || (self.y > half_h && self.vel_y < 0))
|
||||
{
|
||||
self.npc_flags.set_ignore_solidity(true);
|
||||
}
|
||||
|
@ -486,7 +486,7 @@ impl NPC {
|
|||
let half_h = stage.map.height as i32 * state.tile_size.as_int() * 0x200 / 2;
|
||||
|
||||
if ((self.x < half_w && self.vel_x > 0) || (self.x > half_w && self.vel_x < 0))
|
||||
|| ((self.y < half_h && self.vel_y > 0) || (self.y > half_h && self.vel_y < 0))
|
||||
&& ((self.y < half_h && self.vel_y > 0) || (self.y > half_h && self.vel_y < 0))
|
||||
{
|
||||
self.npc_flags.set_ignore_solidity(true);
|
||||
}
|
||||
|
@ -524,7 +524,7 @@ impl NPC {
|
|||
self.action_num = 42;
|
||||
self.action_counter = 0;
|
||||
|
||||
self.vel_x = self.direction.vector_x() * 0x200;
|
||||
self.vel_x = self.direction.opposite().vector_x() * 0x200;
|
||||
self.vel_y = -0x200;
|
||||
}
|
||||
}
|
||||
|
@ -561,6 +561,7 @@ impl NPC {
|
|||
self.damage = 0;
|
||||
self.npc_flags.set_shootable(false);
|
||||
self.npc_flags.set_ignore_solidity(true);
|
||||
self.vel_y = -0x200;
|
||||
self.shock += 50;
|
||||
boss.parts[0].anim_num += 1;
|
||||
}
|
||||
|
|
|
@ -193,10 +193,10 @@ impl NPC {
|
|||
|
||||
self.vel_x = 0x100 * self.direction.vector_x();
|
||||
|
||||
self.action_counter += 1;
|
||||
if self.action_counter != 0 && self.flags.hit_bottom_wall() {
|
||||
self.action_num = 2;
|
||||
}
|
||||
self.action_counter += 1;
|
||||
}
|
||||
2 | 3 => {
|
||||
if self.action_num == 2 {
|
||||
|
@ -240,11 +240,11 @@ impl NPC {
|
|||
4 => {
|
||||
self.vel_x = 0x100 * self.direction.vector_x();
|
||||
|
||||
self.action_counter += 1;
|
||||
if self.action_counter != 0 && self.flags.hit_bottom_wall() {
|
||||
self.action_num = 5;
|
||||
self.npc_flags.set_interactable(true);
|
||||
}
|
||||
self.action_counter += 1;
|
||||
}
|
||||
5 => {
|
||||
self.vel_x = 0;
|
||||
|
@ -590,88 +590,89 @@ impl NPC {
|
|||
players: [&mut Player; 2],
|
||||
npc_list: &NPCList,
|
||||
) -> GameResult {
|
||||
if self.action_num != 1 {
|
||||
if 1 < self.action_num {
|
||||
if self.action_num == 10 {
|
||||
if (self.flags.0 & 0xf) == 0 {
|
||||
self.x += self.vel_x;
|
||||
self.y += self.vel_y;
|
||||
match self.action_num {
|
||||
0 | 1 => {
|
||||
if self.action_num == 0 {
|
||||
self.action_num = 1;
|
||||
self.action_counter = 0;
|
||||
}
|
||||
let parent = self.get_parent_ref_mut(npc_list);
|
||||
if let Some(parent) = parent {
|
||||
let player = self.get_closest_player_mut(players);
|
||||
|
||||
if parent.direction == Direction::Left {
|
||||
self.x = parent.x + 0x1400;
|
||||
} else {
|
||||
self.action_num = 0x14;
|
||||
self.action_counter = 0;
|
||||
|
||||
state.create_caret(self.x, self.y, CaretType::ProjectileDissipation, Direction::Left);
|
||||
state.sound_manager.play_sfx(0xc);
|
||||
|
||||
let mut npc = NPC::create(4, &state.npc_table);
|
||||
npc.cond.set_alive(true);
|
||||
for _ in 0..4 {
|
||||
npc.x = self.x;
|
||||
npc.y = self.y;
|
||||
npc.vel_x = self.rng.range(-0x200..0x200);
|
||||
npc.vel_y = self.rng.range(-0x200..0x200);
|
||||
|
||||
let _ = npc_list.spawn(0x100, npc.clone());
|
||||
}
|
||||
self.x = parent.x + -0x1400;
|
||||
}
|
||||
} else if self.action_num == 0x14 {
|
||||
|
||||
self.y = parent.y + -0x1000;
|
||||
if (parent.action_num == 0x18) || (parent.action_num == 0x34) {
|
||||
self.action_num = 10;
|
||||
if parent.direction == Direction::Left {
|
||||
self.x = parent.x + -0x2000;
|
||||
} else {
|
||||
self.x = parent.x + 0x2000;
|
||||
}
|
||||
self.y = parent.y;
|
||||
|
||||
let angle = f64::atan2((self.y - player.y) as f64, (self.x - player.x) as f64);
|
||||
|
||||
self.vel_x = (angle.cos() * -2048.0) as i32;
|
||||
self.vel_y = (angle.sin() * -2048.0) as i32;
|
||||
state.sound_manager.play_sfx(0x27);
|
||||
}
|
||||
}
|
||||
}
|
||||
10 => {
|
||||
if (self.flags.0 & 0xf) == 0 {
|
||||
self.x += self.vel_x;
|
||||
self.y += self.vel_y;
|
||||
self.action_counter += 1;
|
||||
if 4 < self.action_counter {
|
||||
let mut npc = NPC::create(4, &state.npc_table);
|
||||
npc.cond.set_alive(true);
|
||||
for _ in 0..4 {
|
||||
npc.x = self.x;
|
||||
npc.y = self.y;
|
||||
npc.vel_x = self.rng.range(-0x200..0x200);
|
||||
npc.vel_y = self.rng.range(-0x200..0x200);
|
||||
|
||||
let _ = npc_list.spawn(0x100, npc.clone());
|
||||
}
|
||||
|
||||
self.npc_type = 0x8e;
|
||||
self.anim_num = 0;
|
||||
self.action_num = 0x14;
|
||||
self.vel_x = 0;
|
||||
self.npc_flags.set_invulnerable(false);
|
||||
self.npc_flags.set_shootable(true);
|
||||
self.damage = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
if self.action_num == 0 {
|
||||
self.action_num = 1;
|
||||
self.action_counter = 0;
|
||||
}
|
||||
} else {
|
||||
let parent = self.get_parent_ref_mut(npc_list);
|
||||
if let Some(parent) = parent {
|
||||
let player = self.get_closest_player_mut(players);
|
||||
|
||||
if parent.direction == Direction::Left {
|
||||
self.x = parent.x + 0x1400;
|
||||
} else {
|
||||
self.x = parent.x + -0x1400;
|
||||
}
|
||||
self.action_num = 0x14;
|
||||
self.action_counter = 0;
|
||||
|
||||
self.y = parent.y + -0x1000;
|
||||
if (parent.action_num == 0x18) || (parent.action_num == 0x34) {
|
||||
self.action_num = 10;
|
||||
if parent.direction == Direction::Left {
|
||||
self.x = parent.x + -0x2000;
|
||||
} else {
|
||||
self.x = parent.x + 0x2000;
|
||||
state.create_caret(self.x, self.y, CaretType::ProjectileDissipation, Direction::Left);
|
||||
state.sound_manager.play_sfx(0xc);
|
||||
|
||||
let mut npc = NPC::create(4, &state.npc_table);
|
||||
npc.cond.set_alive(true);
|
||||
for _ in 0..4 {
|
||||
npc.x = self.x;
|
||||
npc.y = self.y;
|
||||
npc.vel_x = self.rng.range(-0x200..0x200);
|
||||
npc.vel_y = self.rng.range(-0x200..0x200);
|
||||
|
||||
let _ = npc_list.spawn(0x100, npc.clone());
|
||||
}
|
||||
self.y = parent.y;
|
||||
|
||||
let angle = f64::atan2((self.y - player.y) as f64, (self.x - player.x) as f64);
|
||||
|
||||
self.vel_x = (angle.cos() * -2048.0) as i32;
|
||||
self.vel_y = (angle.sin() * -2048.0) as i32;
|
||||
state.sound_manager.play_sfx(0x27);
|
||||
}
|
||||
}
|
||||
20 => {
|
||||
self.x += self.vel_x;
|
||||
self.y += self.vel_y;
|
||||
self.action_counter += 1;
|
||||
if 4 < self.action_counter {
|
||||
let mut npc = NPC::create(4, &state.npc_table);
|
||||
npc.cond.set_alive(true);
|
||||
for _ in 0..4 {
|
||||
npc.x = self.x;
|
||||
npc.y = self.y;
|
||||
npc.vel_x = self.rng.range(-0x200..0x200);
|
||||
npc.vel_y = self.rng.range(-0x200..0x200);
|
||||
|
||||
let _ = npc_list.spawn(0x100, npc.clone());
|
||||
}
|
||||
|
||||
self.npc_type = 0x8e;
|
||||
self.anim_num = 0;
|
||||
self.action_num = 0x14;
|
||||
self.vel_x = 0;
|
||||
self.npc_flags.set_invulnerable(false);
|
||||
self.npc_flags.set_shootable(true);
|
||||
self.damage = 1;
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
self.anim_num += 1;
|
||||
|
|
|
@ -38,7 +38,7 @@ impl BossNPC {
|
|||
0 => {
|
||||
self.hurt_sound[0] = 52;
|
||||
self.parts[0].x = 6 * 0x2000 + state.constants.game.tile_offset_x * 0x2000;
|
||||
self.parts[0].y = 12 * 0x2000;
|
||||
self.parts[0].y = 12 * 0x2000 + 0x1000;
|
||||
self.parts[0].direction = Direction::Right;
|
||||
self.parts[0].display_bounds =
|
||||
Rect { left: 48 * 0x200, top: 48 * 0x200, right: 32 * 0x200, bottom: 0x2000 };
|
||||
|
@ -168,7 +168,7 @@ impl BossNPC {
|
|||
npc.cond.set_alive(true);
|
||||
npc.direction = Direction::Left;
|
||||
npc.x = self.parts[0].x + self.parts[0].rng.range(-12..12) as i32 * 0x200;
|
||||
npc.y = self.parts[0].y + self.parts[0].rng.range(-12..12) as i32 * 0x200;
|
||||
npc.y = self.parts[0].y + self.parts[0].hit_bounds.bottom as i32;
|
||||
npc.vel_x = self.parts[0].rng.range(-0x155..0x155) as i32;
|
||||
npc.vel_y = self.parts[0].rng.range(-0x600..0) as i32;
|
||||
|
||||
|
@ -211,12 +211,12 @@ impl BossNPC {
|
|||
}
|
||||
113 => {
|
||||
if self.parts[0].shock != 0 {
|
||||
self.parts[0].action_counter2 += 1;
|
||||
if (self.parts[0].action_counter2 / 2) & 1 != 0 {
|
||||
self.parts[0].anim_num = 4;
|
||||
} else {
|
||||
self.parts[0].anim_num = 3;
|
||||
}
|
||||
self.parts[0].action_counter2 += 1;
|
||||
} else {
|
||||
self.parts[0].action_counter2 = 0;
|
||||
self.parts[0].anim_num = 3;
|
||||
|
|
|
@ -40,7 +40,7 @@ impl NPC {
|
|||
self.anim_counter = 0;
|
||||
let anim = self.anim_num as i32 + (self.direction.vector_x() * -1);
|
||||
if anim < 0 {
|
||||
self.anim_num = 4;
|
||||
self.anim_num = 3;
|
||||
} else if anim > 3 {
|
||||
self.anim_num = 0;
|
||||
} else {
|
||||
|
@ -106,7 +106,7 @@ impl NPC {
|
|||
state.sound_manager.play_sfx(103);
|
||||
}
|
||||
self.action_counter += 1;
|
||||
self.anim_num = (self.action_counter + 1) % 2;
|
||||
self.anim_num = 1 - (self.action_counter & 2) / 2;
|
||||
if self.direction == Direction::Left && self.action_counter == 20 {
|
||||
let mut npc = NPC::create(146, &state.npc_table);
|
||||
npc.cond.set_alive(true);
|
||||
|
@ -149,8 +149,8 @@ impl NPC {
|
|||
|
||||
if self.x < 0
|
||||
|| self.y < 0
|
||||
|| self.x > (stage.map.width as i32) * state.tile_size.as_int() * 0x200 + 0x4000
|
||||
|| self.y > (stage.map.height as i32) * state.tile_size.as_int() * 0x200 + 0x4000
|
||||
|| self.x > (stage.map.width as i32) * state.tile_size.as_int() * 0x200
|
||||
|| self.y > (stage.map.height as i32) * state.tile_size.as_int() * 0x200
|
||||
{
|
||||
self.vanish(state);
|
||||
return Ok(());
|
||||
|
@ -176,6 +176,7 @@ impl NPC {
|
|||
match self.action_num {
|
||||
0 | 1 => {
|
||||
if self.action_num == 0 {
|
||||
self.action_num = 1;
|
||||
self.action_counter = self.rng.range(0..40) as u16;
|
||||
}
|
||||
if self.action_counter > 0 {
|
||||
|
@ -303,6 +304,7 @@ impl NPC {
|
|||
self.action_counter = 0;
|
||||
self.anim_num = 6;
|
||||
self.anim_counter = 0;
|
||||
self.vel_y = 0;
|
||||
self.damage = 10;
|
||||
|
||||
self.face_player(player);
|
||||
|
@ -351,6 +353,7 @@ impl NPC {
|
|||
}
|
||||
self.vel_y = if self.action_num == 221 { -0x800 } else { 0x800 };
|
||||
|
||||
self.action_counter += 1;
|
||||
self.anim_num = if self.action_counter & 0x02 != 0 { 8 } else { 9 };
|
||||
|
||||
if (self.y < 0x6000 && self.action_num == 221)
|
||||
|
@ -362,13 +365,13 @@ impl NPC {
|
|||
}
|
||||
|
||||
if self.action_num == 231 {
|
||||
self.direction = if self.action_num == 220 { Direction::Left } else { Direction::Right };
|
||||
self.face_player(player);
|
||||
}
|
||||
|
||||
self.action_num += 1;
|
||||
self.action_counter = 0;
|
||||
self.damage = 3;
|
||||
let sign = self.direction.vector_x();
|
||||
let sign = if self.action_num == 221 { -1 } else { 1 };
|
||||
|
||||
for _ in 0..8 {
|
||||
let mut npc = NPC::create(4, &state.npc_table);
|
||||
|
@ -435,7 +438,7 @@ impl NPC {
|
|||
self.action_counter = 0;
|
||||
self.anim_num = 3;
|
||||
|
||||
self.direction = if self.action_num == 220 { Direction::Left } else { Direction::Right };
|
||||
self.face_player(player);
|
||||
}
|
||||
}
|
||||
242 => {
|
||||
|
@ -520,12 +523,15 @@ impl NPC {
|
|||
self.target_x = self.x;
|
||||
self.vel_x = 0;
|
||||
self.npc_flags.set_shootable(false);
|
||||
// I think Pixel meant for the smoke radius to be 16 pixels (0x2000) instead of 16 units,
|
||||
// because as it is, this just gets divided by 0x200 units/px and becomes 0
|
||||
npc_list.create_death_smoke(self.x, self.y, 16, 16, state, &self.rng);
|
||||
state.sound_manager.play_sfx(72);
|
||||
}
|
||||
self.vel_y += 0x20;
|
||||
self.clamp_fall_speed();
|
||||
|
||||
self.action_counter;
|
||||
self.action_counter += 1;
|
||||
self.x = self.target_x + if self.action_counter & 0x02 != 0 { 0x200 } else { -0x200 };
|
||||
if self.flags.hit_bottom_wall() {
|
||||
self.action_num = 1002;
|
||||
|
@ -657,7 +663,7 @@ impl NPC {
|
|||
}
|
||||
|
||||
if self.action_counter2 < 2 {
|
||||
self.action_counter2 += 512;
|
||||
self.action_counter2 += 512 - 2; // Still have to subtract 2 first :)
|
||||
} else {
|
||||
self.action_counter2 -= 2;
|
||||
}
|
||||
|
@ -672,7 +678,7 @@ impl NPC {
|
|||
if self.life < 900 {
|
||||
self.action_num = 22;
|
||||
self.npc_flags.set_shootable(false);
|
||||
npc_list.create_death_smoke(self.x, self.y, 16, 32, state, &self.rng);
|
||||
npc_list.create_death_smoke(self.x, self.y, 0x2000, 32, state, &self.rng);
|
||||
state.sound_manager.play_sfx(71);
|
||||
}
|
||||
|
||||
|
@ -686,7 +692,7 @@ impl NPC {
|
|||
self.anim_num = 2;
|
||||
|
||||
if self.action_counter2 < 2 {
|
||||
self.action_counter2 += 512;
|
||||
self.action_counter2 += 512 - 2;
|
||||
} else {
|
||||
self.action_counter2 -= 2;
|
||||
}
|
||||
|
@ -699,7 +705,7 @@ impl NPC {
|
|||
self.anim_num = 2;
|
||||
|
||||
if self.action_counter2 < 4 {
|
||||
self.action_counter2 += 512;
|
||||
self.action_counter2 += 512 - 4;
|
||||
} else {
|
||||
self.action_counter2 -= 4;
|
||||
}
|
||||
|
@ -753,6 +759,8 @@ impl NPC {
|
|||
self.damage = 5;
|
||||
self.npc_flags.set_ignore_solidity(false);
|
||||
self.npc_flags.set_shootable(false);
|
||||
npc_list.create_death_smoke(self.x, self.y, 0x2000, 32, state, &self.rng);
|
||||
state.sound_manager.play_sfx(71);
|
||||
}
|
||||
|
||||
if self.flags.hit_left_wall() {
|
||||
|
@ -793,7 +801,7 @@ impl NPC {
|
|||
self.anim_num = 0;
|
||||
}
|
||||
} else {
|
||||
npc_list.create_death_smoke(self.x, self.y, 16, 32, state, &self.rng);
|
||||
npc_list.create_death_smoke(self.x, self.y, 0x2000, 32, state, &self.rng);
|
||||
state.sound_manager.play_sfx(71);
|
||||
self.vanish(state);
|
||||
return Ok(());
|
||||
|
@ -816,6 +824,8 @@ impl NPC {
|
|||
npc.x = self.x - 0x1000;
|
||||
npc.y = self.y + 0x1800;
|
||||
let _ = npc_list.spawn(0x100, npc);
|
||||
|
||||
state.sound_manager.play_sfx(26);
|
||||
}
|
||||
(Direction::Up, 268) => {
|
||||
let mut npc = NPC::create(4, &state.npc_table);
|
||||
|
@ -829,6 +839,8 @@ impl NPC {
|
|||
npc.x = self.x - 0x1800;
|
||||
npc.y = self.y - 0x1000;
|
||||
let _ = npc_list.spawn(0x100, npc);
|
||||
|
||||
state.sound_manager.play_sfx(26);
|
||||
}
|
||||
(Direction::Right, 396) => {
|
||||
let mut npc = NPC::create(4, &state.npc_table);
|
||||
|
@ -848,6 +860,8 @@ impl NPC {
|
|||
npc.x = self.x - 0x1000;
|
||||
npc.y = self.y - 0x1800;
|
||||
let _ = npc_list.spawn(0x100, npc);
|
||||
|
||||
state.sound_manager.play_sfx(26);
|
||||
}
|
||||
(Direction::Bottom, 12) => {
|
||||
let mut npc = NPC::create(4, &state.npc_table);
|
||||
|
@ -861,6 +875,8 @@ impl NPC {
|
|||
npc.x = self.x + 0x1800;
|
||||
npc.y = self.y - 0x1000;
|
||||
let _ = npc_list.spawn(0x100, npc);
|
||||
|
||||
state.sound_manager.play_sfx(26);
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
@ -937,7 +953,6 @@ impl NPC {
|
|||
self.npc_flags.set_ignore_solidity(false);
|
||||
}
|
||||
|
||||
self.action_counter += 1;
|
||||
if self.action_counter & 0x02 != 0 {
|
||||
let mut npc = NPC::create(4, &state.npc_table);
|
||||
npc.cond.set_alive(true);
|
||||
|
@ -945,6 +960,7 @@ impl NPC {
|
|||
npc.y = self.y;
|
||||
let _ = npc_list.spawn(0x100, npc);
|
||||
}
|
||||
self.action_counter += 1; // This gets incremented after the previous check for some reason
|
||||
|
||||
if self.flags.hit_bottom_wall() {
|
||||
self.action_num = 110;
|
||||
|
@ -968,7 +984,7 @@ impl NPC {
|
|||
110 => {
|
||||
self.vel_y += 0x40;
|
||||
|
||||
if self.y > (stage.map.height as i32) * state.tile_size.as_int() * 0x200 + 0x4000 {
|
||||
if self.y > (stage.map.height as i32 + 2) * state.tile_size.as_int() * 0x200 {
|
||||
self.cond.set_alive(false);
|
||||
return Ok(());
|
||||
}
|
||||
|
@ -1000,7 +1016,9 @@ impl NPC {
|
|||
0 | 10 => {
|
||||
if self.action_num == 0 {
|
||||
self.action_num = 10;
|
||||
// self.action_counter2 set by Ballos code, no need to set it here
|
||||
self.action_counter3 = 192;
|
||||
self.vel_y2 = 0;
|
||||
}
|
||||
if self.action_counter3 >= 448 {
|
||||
self.action_num = 11;
|
||||
|
@ -1017,7 +1035,7 @@ impl NPC {
|
|||
if self.action_counter2 > 0 {
|
||||
self.action_counter2 -= 1;
|
||||
} else {
|
||||
self.action_counter2 += 0x400;
|
||||
self.action_counter2 += 0x400 - 1;
|
||||
}
|
||||
|
||||
if boss.parts[0].action_num == 421 {
|
||||
|
@ -1040,7 +1058,7 @@ impl NPC {
|
|||
if self.action_counter2 > 1 {
|
||||
self.action_counter2 -= 2;
|
||||
} else {
|
||||
self.action_counter2 += 0x400;
|
||||
self.action_counter2 += 0x400 - 2;
|
||||
}
|
||||
|
||||
if boss.parts[0].action_num == 422 {
|
||||
|
@ -1056,6 +1074,7 @@ impl NPC {
|
|||
}
|
||||
}
|
||||
100 => {
|
||||
self.vel_y2 = 0;
|
||||
if boss.parts[0].action_num == 424 {
|
||||
self.action_num = 30;
|
||||
} else if boss.parts[0].action_num == 428 {
|
||||
|
@ -1096,20 +1115,19 @@ impl NPC {
|
|||
match self.action_num {
|
||||
20 | 30 => {
|
||||
if self.action_counter2 % 4 == 0 {
|
||||
self.vel_y = (self.target_y - self.y) / 4;
|
||||
self.vel_y2 = (self.target_y - self.y) / 4;
|
||||
}
|
||||
}
|
||||
40 | 50 => {
|
||||
if self.action_counter2 & 0x02 == 0 {
|
||||
self.vel_y = (self.target_y - self.y) / 2;
|
||||
self.vel_y2 = (self.target_y - self.y) / 2;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
self.vel_y = self.target_y - self.y;
|
||||
self.vel_y2 = self.target_y - self.y;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.vel_y = 0;
|
||||
self.vel_y = self.vel_y2;
|
||||
}
|
||||
|
||||
self.x += self.vel_x;
|
||||
|
@ -1380,7 +1398,9 @@ impl NPC {
|
|||
if stage.change_tile(x as usize, y as usize, 109) {
|
||||
npc.x = x * state.tile_size.as_int() * 0x200;
|
||||
npc.y = y * state.tile_size.as_int() * 0x200;
|
||||
let _ = npc_list.spawn(0x100, npc.clone());
|
||||
let _ = npc_list.spawn(0, npc.clone());
|
||||
let _ = npc_list.spawn(0, npc.clone());
|
||||
let _ = npc_list.spawn(0, npc.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1432,7 +1452,7 @@ impl BossNPC {
|
|||
|
||||
self.parts[3].cond.set_alive(true);
|
||||
self.parts[3].cond.set_damage_boss(true);
|
||||
self.parts[3].npc_flags.set_solid_hard(true); // This should be soft -- investigate bug with large soft collision boxes?
|
||||
self.parts[3].npc_flags.set_solid_soft(true);
|
||||
self.parts[3].npc_flags.set_invulnerable(true);
|
||||
self.parts[3].npc_flags.set_ignore_solidity(true);
|
||||
self.parts[3].display_bounds = Rect { left: 0x7800, top: 0x7800, right: 0x7800, bottom: 0x7800 };
|
||||
|
@ -1669,7 +1689,7 @@ impl BossNPC {
|
|||
let mut npc = NPC::create(344, &state.npc_table);
|
||||
npc.cond.set_alive(true);
|
||||
npc.x = self.parts[0].x + 0x3000 * dir.vector_x();
|
||||
npc.y = self.parts[0].y + 0x4800;
|
||||
npc.y = self.parts[0].y - 0x4800;
|
||||
npc.direction = dir;
|
||||
let _ = npc_list.spawn(0x20, npc);
|
||||
}
|
||||
|
@ -1743,7 +1763,7 @@ impl BossNPC {
|
|||
self.parts[0].action_counter = 0;
|
||||
self.parts[0].vel_x = 0;
|
||||
self.parts[0].vel_y = 0;
|
||||
npc_list.kill_npcs_by_type(339, true, state);
|
||||
npc_list.kill_npcs_by_type(339, false, state);
|
||||
}
|
||||
|
||||
self.parts[0].y += (0x13E00 - self.parts[0].y) / 8;
|
||||
|
@ -1775,7 +1795,7 @@ impl BossNPC {
|
|||
let mut npc = NPC::create(344, &state.npc_table);
|
||||
npc.cond.set_alive(true);
|
||||
npc.x = self.parts[0].x + 0x3000 * dir.vector_x();
|
||||
npc.y = self.parts[0].y + 0x4800;
|
||||
npc.y = self.parts[0].y - 0x4800;
|
||||
npc.direction = dir;
|
||||
let _ = npc_list.spawn(0x20, npc);
|
||||
}
|
||||
|
@ -1797,7 +1817,7 @@ impl BossNPC {
|
|||
npc.cond.set_alive(true);
|
||||
npc.x = (((self.parts[0].action_counter as i32 / 30) * 2) + 2) * 0x2000;
|
||||
npc.y = 0x2A000;
|
||||
let _ = npc_list.spawn(0x100, npc);
|
||||
let _ = npc_list.spawn(0x180, npc);
|
||||
}
|
||||
|
||||
if (self.parts[0].action_counter / 3 % 2) > 0 {
|
||||
|
@ -1951,7 +1971,7 @@ impl BossNPC {
|
|||
npc.x = self.parts[0].x + self.parts[0].rng.range(-40..40) * 0x200;
|
||||
npc.y = self.parts[0].y + self.parts[0].rng.range(0..40) * 0x200;
|
||||
npc.direction = Direction::Bottom;
|
||||
let _ = npc_list.spawn(0x100, npc);
|
||||
let _ = npc_list.spawn(0, npc);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -16,7 +16,7 @@ impl NPC {
|
|||
self.cond.set_alive(false);
|
||||
}
|
||||
|
||||
if (self.y - 0x800) > state.water_level {
|
||||
if self.flags.in_water() {
|
||||
self.x += self.vel_x / 2;
|
||||
self.y += self.vel_y / 2;
|
||||
} else {
|
||||
|
@ -180,6 +180,10 @@ impl BossNPC {
|
|||
self.parts[7].x = self.parts[0].x - 0x6000;
|
||||
self.parts[7].y = self.parts[0].y + 0x4000;
|
||||
|
||||
for i in [2,3,6,7] {
|
||||
self.hurt_sound[i] = 54;
|
||||
}
|
||||
|
||||
for part in &mut self.parts {
|
||||
part.prev_x = part.x;
|
||||
part.prev_y = part.y;
|
||||
|
|
|
@ -162,12 +162,12 @@ impl BossNPC {
|
|||
// This relies heavily on the map not being changed
|
||||
// Need to calculate offset from the default starting location
|
||||
for i in 0..5 {
|
||||
stage.change_tile(i + 8, self.parts[0].action_counter3 as usize, 0);
|
||||
let extra_smoke = if stage.change_tile(i + 8, self.parts[0].action_counter3 as usize, 0) { 3 } else { 0 };
|
||||
npc_list.create_death_smoke(
|
||||
(i as i32 + 8) * 0x2000,
|
||||
self.parts[0].action_counter3 as i32 * 0x2000,
|
||||
0,
|
||||
4,
|
||||
4 + extra_smoke,
|
||||
state,
|
||||
&self.parts[0].rng,
|
||||
);
|
||||
|
@ -250,7 +250,8 @@ impl BossNPC {
|
|||
// Need to calculate offset from the default starting location
|
||||
for i in 0..7 {
|
||||
stage.change_tile(i + 7, 14, 0);
|
||||
npc_list.create_death_smoke((i as i32 + 7) * 0x2000, 0x1C000, 0, 1, state, &self.parts[0].rng);
|
||||
// This should be called with an amount of 0, but change_tile also needs to make smoke
|
||||
npc_list.create_death_smoke((i as i32 + 7) * 0x2000, 0x1C000, 0, 3, state, &self.parts[0].rng);
|
||||
state.sound_manager.play_sfx(12);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ impl NPC {
|
|||
pub(crate) fn tick_n196_ironhead_wall(&mut self, state: &mut SharedGameState) -> GameResult {
|
||||
self.x -= 0xC00;
|
||||
if self.x <= if !state.constants.is_switch { 0x26000 } else { 0x1E000 } {
|
||||
self.x += if !state.constants.is_switch { 0x2C000 } else { 0x3C000 };
|
||||
self.x += if !state.constants.is_switch { 0x2C000 } else { 0x3B400 };
|
||||
}
|
||||
|
||||
let dir_offset = if self.direction == Direction::Left { 0 } else { 1 };
|
||||
|
@ -158,7 +158,7 @@ impl NPC {
|
|||
}
|
||||
10 => {
|
||||
self.action_counter += 1;
|
||||
if self.action_counter % 6 == 0 {
|
||||
if self.action_counter % 4 == 1 {
|
||||
let mut npc = NPC::create(335, &state.npc_table);
|
||||
npc.cond.set_alive(true);
|
||||
npc.x = self.x;
|
||||
|
@ -269,19 +269,21 @@ impl BossNPC {
|
|||
self.parts[0].action_num = 100;
|
||||
}
|
||||
|
||||
self.parts[0].action_counter += 1;
|
||||
if [300, 310, 320].contains(&self.parts[0].action_counter) {
|
||||
state.sound_manager.play_sfx(39);
|
||||
if self.parts[0].direction == Direction::Left {
|
||||
self.parts[0].action_counter += 1;
|
||||
if [300, 310, 320].contains(&self.parts[0].action_counter) {
|
||||
state.sound_manager.play_sfx(39);
|
||||
|
||||
let mut npc = NPC::create(198, &state.npc_table);
|
||||
npc.cond.set_alive(true);
|
||||
npc.x = self.parts[0].x + 0x1400;
|
||||
npc.y = self.parts[0].y + 0x200;
|
||||
npc.vel_x = self.parts[0].rng.range(-3..0) * 0x200;
|
||||
npc.vel_y = self.parts[0].rng.range(-3..3) * 0x200;
|
||||
npc.direction = Direction::Right;
|
||||
let mut npc = NPC::create(198, &state.npc_table);
|
||||
npc.cond.set_alive(true);
|
||||
npc.x = self.parts[0].x + 0x1400;
|
||||
npc.y = self.parts[0].y + 0x200;
|
||||
npc.vel_x = self.parts[0].rng.range(-3..0) * 0x200;
|
||||
npc.vel_y = self.parts[0].rng.range(-3..3) * 0x200;
|
||||
npc.direction = Direction::Right;
|
||||
|
||||
let _ = npc_list.spawn(0x100, npc);
|
||||
let _ = npc_list.spawn(0x100, npc);
|
||||
}
|
||||
}
|
||||
|
||||
self.parts[0].animate(2, 0, 7);
|
||||
|
|
|
@ -81,6 +81,8 @@ impl GameEntity<([&mut Player; 2], &NPCList, &mut Stage, &BulletManager, &mut Fl
|
|||
),
|
||||
) -> GameResult {
|
||||
if !self.parts[0].cond.alive() {
|
||||
// Kind of hacky but fixes Monster X's damage popup being stuck on screen
|
||||
self.parts[0].popup.tick(state, ())?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
|
@ -100,6 +102,9 @@ impl GameEntity<([&mut Player; 2], &NPCList, &mut Stage, &BulletManager, &mut Fl
|
|||
for part in &mut self.parts {
|
||||
if part.shock > 0 {
|
||||
part.shock -= 1;
|
||||
} else if part.npc_flags.show_damage() && part.popup.value != 0 {
|
||||
// I don't know where the best place to put this is, but let's try putting it here
|
||||
part.popup.update_displayed_value();
|
||||
}
|
||||
part.popup.x = part.x;
|
||||
part.popup.y = part.y;
|
||||
|
|
|
@ -38,7 +38,8 @@ impl NPC {
|
|||
self.y += self.vel_y;
|
||||
|
||||
let player = self.get_closest_player_mut(players);
|
||||
let direction = f64::atan2(-(self.y - player.y) as f64, -(self.x - player.x) as f64);
|
||||
// Get angle between 0 and 2*PI
|
||||
let direction = f64::atan2((self.y - player.y) as f64, (self.x - player.x) as f64) + std::f64::consts::PI;
|
||||
|
||||
if direction < radians {
|
||||
if radians - direction < std::f64::consts::PI {
|
||||
|
@ -179,11 +180,9 @@ impl BossNPC {
|
|||
self.parts[3].hit_bounds =
|
||||
Rect { left: 5 * 0x200, top: 5 * 0x200, right: 5 * 0x200, bottom: 5 * 0x200 };
|
||||
self.parts[3].npc_flags.set_ignore_solidity(true);
|
||||
self.hurt_sound[3] = 54;
|
||||
self.death_sound[3] = 71;
|
||||
|
||||
self.parts[4] = self.parts[3].clone();
|
||||
self.parts[3].target_x = 1;
|
||||
self.parts[4].target_x = 1;
|
||||
|
||||
self.parts[5] = self.parts[3].clone();
|
||||
self.parts[6] = self.parts[3].clone();
|
||||
|
@ -192,6 +191,11 @@ impl BossNPC {
|
|||
self.parts[5].life = 100;
|
||||
self.parts[6].life = 100;
|
||||
|
||||
for i in 3..7 {
|
||||
self.hurt_sound[i] = 54;
|
||||
self.death_sound[i] = 71;
|
||||
}
|
||||
|
||||
self.parts[7].cond.set_alive(true);
|
||||
self.parts[7].x = self.parts[0].x;
|
||||
self.parts[7].y = self.parts[0].y;
|
||||
|
@ -201,6 +205,7 @@ impl BossNPC {
|
|||
Rect { left: 52 * 0x200, top: 24 * 0x200, right: 52 * 0x200, bottom: 24 * 0x200 };
|
||||
self.parts[7].hit_bounds = Rect { left: 0x1000, top: 24 * 0x200, right: 0x1000, bottom: 0x2000 };
|
||||
self.parts[7].npc_flags.set_ignore_solidity(true);
|
||||
self.hurt_sound[7] = 52;
|
||||
|
||||
self.parts[9].cond.set_alive(true);
|
||||
self.parts[9].x = self.parts[0].x - 64 * 0x200;
|
||||
|
@ -463,11 +468,9 @@ impl BossNPC {
|
|||
state.sound_manager.play_sfx(52);
|
||||
}
|
||||
|
||||
let mut npc = NPC::create(4, &state.npc_table);
|
||||
npc.cond.set_alive(true);
|
||||
npc.x = self.parts[0].x + self.parts[0].rng.range(-72..72) as i32 * 0x200;
|
||||
npc.y = self.parts[0].y + self.parts[0].rng.range(-64..64) as i32 * 0x200;
|
||||
let _ = npc_list.spawn(0x100, npc);
|
||||
let x = self.parts[0].x + self.parts[0].rng.range(-72..72) as i32 * 0x200;
|
||||
let y = self.parts[0].y + self.parts[0].rng.range(-64..64) as i32 * 0x200;
|
||||
npc_list.create_death_smoke(x, y, 1, 1, state, &self.parts[0].rng);
|
||||
|
||||
if self.parts[0].action_counter > 100 {
|
||||
self.parts[0].action_num = 1001;
|
||||
|
@ -486,16 +489,12 @@ impl BossNPC {
|
|||
part.cond.set_alive(false);
|
||||
}
|
||||
|
||||
for npc in npc_list.iter_alive() {
|
||||
if npc.npc_type == 158 {
|
||||
npc.cond.set_alive(false);
|
||||
}
|
||||
}
|
||||
npc_list.kill_npcs_by_type(158, true, state);
|
||||
|
||||
let mut npc = NPC::create(159, &state.npc_table);
|
||||
npc.cond.set_alive(true);
|
||||
npc.x = self.parts[0].x;
|
||||
npc.y = self.parts[1].y - 24 * 0x200;
|
||||
npc.y = self.parts[0].y - 24 * 0x200;
|
||||
|
||||
let _ = npc_list.spawn(0, npc);
|
||||
}
|
||||
|
@ -549,18 +548,17 @@ impl BossNPC {
|
|||
match self.parts[i].action_num {
|
||||
0 => {
|
||||
self.parts[0].npc_flags.set_shootable(false);
|
||||
self.parts[i].anim_num = 2;
|
||||
self.parts[i].anim_num = 0;
|
||||
}
|
||||
10 | 11 => {
|
||||
if self.parts[i].action_num == 10 {
|
||||
self.parts[i].action_num = 11;
|
||||
self.parts[i].action_counter = (self.parts[i].target_x * 10 + 40) as u16;
|
||||
self.parts[i].anim_num = 2;
|
||||
self.parts[0].npc_flags.set_shootable(true);
|
||||
}
|
||||
|
||||
if self.parts[0].shock > 0 {
|
||||
self.parts[i].action_counter2 += 1;
|
||||
self.parts[i].action_counter2 = self.parts[i].action_counter2.wrapping_add(1);
|
||||
if self.parts[i].action_counter2 / 2 % 2 != 0 {
|
||||
self.parts[i].anim_num = 1;
|
||||
} else {
|
||||
|
@ -573,8 +571,12 @@ impl BossNPC {
|
|||
_ => {}
|
||||
}
|
||||
|
||||
self.parts[7].x = self.parts[0].x;
|
||||
self.parts[7].y = self.parts[0].y;
|
||||
self.parts[i].x = self.parts[0].x;
|
||||
self.parts[i].y = self.parts[0].y;
|
||||
|
||||
if self.parts[0].action_num <= 10 {
|
||||
self.parts[i].anim_num = 2;
|
||||
}
|
||||
|
||||
self.parts[i].anim_rect = state.constants.npc.b03_monster_x[self.parts[i].anim_num as usize];
|
||||
}
|
||||
|
|
|
@ -75,6 +75,7 @@ impl BossNPC {
|
|||
self.parts[0].display_bounds =
|
||||
Rect { left: 40 * 0x200, top: 40 * 0x200, right: 40 * 0x200, bottom: 0x2000 };
|
||||
self.parts[0].hit_bounds = Rect { left: 0x1000, top: 24 * 0x200, right: 0x1000, bottom: 0x2000 };
|
||||
self.hurt_sound[0] = 52;
|
||||
|
||||
self.parts[1].cond.set_alive(true);
|
||||
self.parts[1].display_bounds =
|
||||
|
@ -100,6 +101,7 @@ impl BossNPC {
|
|||
self.parts[4].display_bounds = self.parts[3].display_bounds;
|
||||
self.parts[4].hit_bounds = self.parts[3].hit_bounds;
|
||||
self.parts[4].npc_flags = self.parts[3].npc_flags;
|
||||
self.parts[4].x = self.parts[0].x - 0x2000;
|
||||
self.parts[4].y = self.parts[3].y;
|
||||
self.parts[4].direction = Direction::Right;
|
||||
self.hurt_sound[4] = 52;
|
||||
|
@ -166,7 +168,7 @@ impl BossNPC {
|
|||
}
|
||||
60 => {
|
||||
self.parts[0].action_counter += 1;
|
||||
if self.parts[0].action_counter % 3 == 0 && (20..80).contains(&self.parts[0].action_counter) {
|
||||
if self.parts[0].action_counter % 3 == 0 && (21..80).contains(&self.parts[0].action_counter) {
|
||||
let mut npc = NPC::create(48, &state.npc_table);
|
||||
npc.cond.set_alive(true);
|
||||
npc.x = self.parts[0].x;
|
||||
|
@ -195,6 +197,8 @@ impl BossNPC {
|
|||
match self.parts[0].anim_num {
|
||||
1 => self.parts[0].damage = 20,
|
||||
0 => {
|
||||
state.sound_manager.stop_sfx(102);
|
||||
state.sound_manager.play_sfx(12);
|
||||
self.parts[0].action_num = 80;
|
||||
self.parts[0].action_counter = 0;
|
||||
self.parts[0].npc_flags.set_shootable(false);
|
||||
|
@ -297,7 +301,7 @@ impl BossNPC {
|
|||
|
||||
state.sound_manager.play_sfx(12);
|
||||
state.sound_manager.play_sfx(25);
|
||||
state.sound_manager.play_sfx(102);
|
||||
state.sound_manager.stop_sfx(102);
|
||||
}
|
||||
1 => {
|
||||
self.parts[0].damage = 20;
|
||||
|
@ -427,13 +431,9 @@ impl BossNPC {
|
|||
self.parts[0].action_num = 150;
|
||||
self.parts[0].action_counter = 0;
|
||||
self.parts[0].damage = 0;
|
||||
self.parts[5].damage = 5;
|
||||
self.parts[5].damage = 0;
|
||||
|
||||
for npc in npc_list.iter_alive() {
|
||||
if npc.npc_type == 48 {
|
||||
npc.cond.set_alive(false);
|
||||
}
|
||||
}
|
||||
npc_list.kill_npcs_by_type(48, true, state);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -261,7 +261,7 @@ impl BossNPC {
|
|||
} else {
|
||||
return;
|
||||
};
|
||||
|
||||
|
||||
match part.action_num {
|
||||
0 => {
|
||||
part.action_num = 1;
|
||||
|
@ -316,7 +316,15 @@ impl BossNPC {
|
|||
part.hit_bounds.left = 0x2000;
|
||||
|
||||
state.sound_manager.play_sfx(51);
|
||||
npc_list.remove_by_type(211, state);
|
||||
|
||||
npc_list.create_death_smoke(
|
||||
part.x,
|
||||
part.y,
|
||||
part.display_bounds.right as usize,
|
||||
4,
|
||||
state,
|
||||
&part.rng
|
||||
);
|
||||
}
|
||||
}
|
||||
220 => {
|
||||
|
@ -329,11 +337,11 @@ impl BossNPC {
|
|||
npc.y = part.y;
|
||||
|
||||
let player = part.get_closest_player_ref(players);
|
||||
let angle = f64::atan2((player.y - npc.y) as f64, (player.x - npc.x) as f64)
|
||||
let angle = f64::atan2((part.y - player.y) as f64, (part.x - player.x) as f64)
|
||||
+ (part.rng.range(-6..6) as f64 * CDEG_RAD);
|
||||
|
||||
npc.vel_x = (angle.cos() * 512.0) as i32;
|
||||
npc.vel_y = (angle.sin() * 512.0) as i32;
|
||||
npc.vel_x = (angle.cos() * -512.0) as i32;
|
||||
npc.vel_y = (angle.sin() * -512.0) as i32;
|
||||
|
||||
let _ = npc_list.spawn(0x100, npc);
|
||||
|
||||
|
@ -376,20 +384,24 @@ impl BossNPC {
|
|||
npc.cond.set_alive(true);
|
||||
|
||||
let player = part.get_closest_player_ref(players);
|
||||
let angle = f64::atan2((player.y - npc.y) as f64, (player.x - npc.x) as f64)
|
||||
let angle = f64::atan2((part.y - player.y) as f64, (part.x - player.x) as f64)
|
||||
+ (part.rng.range(-6..6) as f64 * CDEG_RAD);
|
||||
|
||||
npc.x = part.x + 0x1000 * part.direction.vector_x();
|
||||
npc.y = part.y;
|
||||
|
||||
npc.vel_x = (angle.cos() * 512.0) as i32;
|
||||
npc.vel_y = (angle.sin() * 512.0) as i32;
|
||||
npc.vel_x = (angle.cos() * -512.0) as i32;
|
||||
npc.vel_y = (angle.sin() * -512.0) as i32;
|
||||
|
||||
let _ = npc_list.spawn(0x100, npc);
|
||||
|
||||
state.sound_manager.play_sfx(33);
|
||||
}
|
||||
}
|
||||
1000 => {
|
||||
part.npc_flags.set_shootable(false);
|
||||
part.anim_num = 3;
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
|
|
|
@ -20,7 +20,7 @@ impl NPC {
|
|||
if self.action_num == 0 {
|
||||
self.action_num = 20;
|
||||
self.target_y = self.y;
|
||||
self.vel_y = if self.rng.range(0..100) & 1 == 0 { -0x100 } else { 0x100 };
|
||||
self.vel_y = if self.rng.range(0..100) & 1 != 0 { -0x100 } else { 0x100 };
|
||||
}
|
||||
|
||||
if self.action_num == 20 {
|
||||
|
@ -170,8 +170,8 @@ impl NPC {
|
|||
self.vel_x += self.direction.vector_x() * 0x15;
|
||||
self.target_x += self.vel_x;
|
||||
|
||||
self.x = self.target_x + 4 * ((self.action_counter2 as f64).cos() * -512.0) as i32;
|
||||
self.y = self.target_y + 6 * ((self.action_counter2 as f64).sin() * -512.0) as i32;
|
||||
self.x = self.target_x + 4 * ((self.action_counter2 as f64).cos() * 512.0) as i32;
|
||||
self.y = self.target_y + 6 * ((self.action_counter2 as f64).sin() * 512.0) as i32;
|
||||
|
||||
let mut npc = NPC::create(286, &state.npc_table);
|
||||
npc.cond.set_alive(true);
|
||||
|
@ -292,12 +292,12 @@ impl NPC {
|
|||
|
||||
npc.x = self.x;
|
||||
npc.y = self.y;
|
||||
npc.vel_y = self.direction.vector_y() * 0x400;
|
||||
npc.vel_y = self.direction.opposite().vector_y() * 0x400;
|
||||
|
||||
let _ = npc_list.spawn(0x100, npc);
|
||||
}
|
||||
|
||||
if self.x < 0x2000 || self.x > (stage.map.width as i32 + 1) * state.tile_size.as_int() * 0x200 {
|
||||
if self.x < 0x2000 || self.x > (stage.map.width as i32 - 1) * state.tile_size.as_int() * 0x200 {
|
||||
self.cond.set_alive(false);
|
||||
}
|
||||
}
|
||||
|
@ -400,6 +400,10 @@ impl BossNPC {
|
|||
self.parts[7].action_counter2 = 1;
|
||||
self.parts[7].action_counter3 = 128;
|
||||
|
||||
for i in [2, 6, 7] {
|
||||
self.hurt_sound[i] = self.hurt_sound[1];
|
||||
}
|
||||
|
||||
self.parts[19].action_counter = self.parts[0].life;
|
||||
|
||||
for i in 0u16..20 {
|
||||
|
@ -525,6 +529,7 @@ impl BossNPC {
|
|||
self.parts[10].npc_flags.set_invulnerable(true);
|
||||
self.parts[11].npc_flags.set_shootable(true);
|
||||
self.parts[19].action_counter = self.parts[0].life;
|
||||
v19 = true;
|
||||
|
||||
state.quake_counter = 100;
|
||||
state.quake_rumble_counter = 100;
|
||||
|
@ -1042,7 +1047,7 @@ impl BossNPC {
|
|||
|
||||
part.vel_x += 0x20;
|
||||
part.x += part.vel_x;
|
||||
if part.x > (stage.map.width as i32) * state.tile_size.as_int() * 0x200 + 0x4000 {
|
||||
if part.x > (stage.map.width as i32 + 2) * state.tile_size.as_int() * 0x200 {
|
||||
part.cond.set_alive(false);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -265,10 +265,10 @@ impl GameEntity<([&mut Player; 2], &NPCList, &mut Stage, &mut BulletManager, &mu
|
|||
11 => self.tick_n011_balrogs_projectile(state),
|
||||
12 => self.tick_n012_balrog_cutscene(state, players, npc_list, stage),
|
||||
13 => self.tick_n013_forcefield(state),
|
||||
14 => self.tick_n014_key(state),
|
||||
14 => self.tick_n014_key(state, npc_list),
|
||||
15 => self.tick_n015_chest_closed(state, npc_list),
|
||||
16 => self.tick_n016_save_point(state),
|
||||
17 => self.tick_n017_health_refill(state),
|
||||
16 => self.tick_n016_save_point(state, npc_list),
|
||||
17 => self.tick_n017_health_refill(state, npc_list),
|
||||
18 => self.tick_n018_door(state, npc_list),
|
||||
19 => self.tick_n019_balrog_bust_in(state, npc_list),
|
||||
20 => self.tick_n020_computer(state),
|
||||
|
@ -493,7 +493,7 @@ impl GameEntity<([&mut Player; 2], &NPCList, &mut Stage, &mut BulletManager, &mu
|
|||
239 => self.tick_n239_cage_bars(state),
|
||||
240 => self.tick_n240_mimiga_jailed(state),
|
||||
241 => self.tick_n241_critter_red(state, players),
|
||||
242 => self.tick_n242_bat_last_cave(state),
|
||||
242 => self.tick_n242_bat_last_cave(state, stage),
|
||||
243 => self.tick_n243_bat_generator(state, npc_list),
|
||||
244 => self.tick_n244_lava_drop(state, players),
|
||||
245 => self.tick_n245_lava_drop_generator(state, npc_list),
|
||||
|
@ -581,7 +581,7 @@ impl GameEntity<([&mut Player; 2], &NPCList, &mut Stage, &mut BulletManager, &mu
|
|||
327 => self.tick_n327_sneeze(state, npc_list),
|
||||
328 => self.tick_n328_human_transform_machine(state),
|
||||
329 => self.tick_n329_laboratory_fan(state),
|
||||
330 => self.tick_n330_rolling(state, stage),
|
||||
330 => self.tick_n330_rolling(state, npc_list, stage),
|
||||
331 => self.tick_n331_ballos_bone_projectile(state),
|
||||
332 => self.tick_n332_ballos_shockwave(state, npc_list),
|
||||
333 => self.tick_n333_ballos_lightning(state, players, npc_list),
|
||||
|
@ -625,6 +625,10 @@ impl GameEntity<([&mut Player; 2], &NPCList, &mut Stage, &mut BulletManager, &mu
|
|||
_ => Ok(()),
|
||||
}?;
|
||||
|
||||
// I don't know where the best place to put this is, but let's try putting it here
|
||||
if self.shock == 0 && self.npc_flags.show_damage() && self.popup.value != 0 {
|
||||
self.popup.update_displayed_value();
|
||||
}
|
||||
self.popup.x = self.x;
|
||||
self.popup.y = self.y;
|
||||
self.popup.tick(state, ())?;
|
||||
|
|
|
@ -266,13 +266,13 @@ impl NPCList {
|
|||
|
||||
match npc.size {
|
||||
1 => {
|
||||
self.create_death_smoke(npc.x, npc.y, npc.display_bounds.right as usize, 3, state, &npc.rng);
|
||||
self.create_death_smoke(npc.x, npc.y, npc.display_bounds.right as usize, 4, state, &npc.rng);
|
||||
}
|
||||
2 => {
|
||||
self.create_death_smoke(npc.x, npc.y, npc.display_bounds.right as usize, 7, state, &npc.rng);
|
||||
self.create_death_smoke(npc.x, npc.y, npc.display_bounds.right as usize, 8, state, &npc.rng);
|
||||
}
|
||||
3 => {
|
||||
self.create_death_smoke(npc.x, npc.y, npc.display_bounds.right as usize, 12, state, &npc.rng);
|
||||
self.create_death_smoke(npc.x, npc.y, npc.display_bounds.right as usize, 16, state, &npc.rng);
|
||||
}
|
||||
_ => {}
|
||||
};
|
||||
|
@ -332,6 +332,9 @@ impl NPCList {
|
|||
state.set_flag(npc.flag_num as usize, true);
|
||||
|
||||
if npc.npc_flags.show_damage() {
|
||||
if npc.popup.value != 0 {
|
||||
npc.popup.update_displayed_value();
|
||||
}
|
||||
if vanish {
|
||||
npc.vanish(state);
|
||||
}
|
||||
|
@ -345,7 +348,7 @@ impl NPCList {
|
|||
}
|
||||
|
||||
/// Removes NPCs whose event number matches the provided one.
|
||||
pub fn remove_by_event(&self, event_num: u16, state: &mut SharedGameState) {
|
||||
pub fn kill_npcs_by_event(&self, event_num: u16, state: &mut SharedGameState) {
|
||||
for npc in self.iter_alive() {
|
||||
if npc.event_num == event_num {
|
||||
npc.cond.set_alive(false);
|
||||
|
@ -354,23 +357,6 @@ impl NPCList {
|
|||
}
|
||||
}
|
||||
|
||||
/// Removes NPCs (and creates a smoke effect) whose type IDs match the provided one.
|
||||
pub fn remove_by_type(&self, npc_type: u16, state: &mut SharedGameState) {
|
||||
for npc in self.iter_alive() {
|
||||
if npc.npc_type == npc_type {
|
||||
npc.cond.set_alive(false);
|
||||
state.set_flag(npc.flag_num as usize, true);
|
||||
|
||||
match npc.size {
|
||||
1 => self.create_death_smoke(npc.x, npc.y, npc.display_bounds.right as usize, 3, state, &npc.rng),
|
||||
2 => self.create_death_smoke(npc.x, npc.y, npc.display_bounds.right as usize, 7, state, &npc.rng),
|
||||
3 => self.create_death_smoke(npc.x, npc.y, npc.display_bounds.right as usize, 12, state, &npc.rng),
|
||||
_ => {}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates NPC death smoke diffusing in random directions.
|
||||
#[inline]
|
||||
pub fn create_death_smoke(
|
||||
|
|
|
@ -708,6 +708,7 @@ pub trait PhysicalEntity {
|
|||
self.flags().set_hit_by_spike(true);
|
||||
if water {
|
||||
self.flags().set_in_water(true);
|
||||
self.flags().set_bloody_droplets(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,7 @@ use std::clone::Clone;
|
|||
use num_derive::FromPrimitive;
|
||||
use num_traits::clamp;
|
||||
|
||||
use crate::common::{Condition, Direction, Equipment, Flag, interpolate_fix9_scale, Rect};
|
||||
use crate::common::{interpolate_fix9_scale, Condition, Direction, Equipment, Flag, Rect};
|
||||
use crate::components::number_popup::NumberPopup;
|
||||
use crate::entity::GameEntity;
|
||||
use crate::framework::context::Context;
|
||||
|
@ -12,8 +12,8 @@ use crate::game::caret::CaretType;
|
|||
use crate::game::frame::Frame;
|
||||
use crate::game::npc::list::NPCList;
|
||||
use crate::game::npc::NPC;
|
||||
use crate::game::player::skin::{PlayerAnimationState, PlayerAppearanceState, PlayerSkin};
|
||||
use crate::game::player::skin::basic::BasicPlayerSkin;
|
||||
use crate::game::player::skin::{PlayerAnimationState, PlayerAppearanceState, PlayerSkin};
|
||||
use crate::game::shared_game_state::SharedGameState;
|
||||
use crate::input::dummy_player_controller::DummyPlayerController;
|
||||
use crate::input::player_controller::PlayerController;
|
||||
|
@ -108,14 +108,13 @@ pub struct Player {
|
|||
pub air: u16,
|
||||
pub skin: Box<dyn PlayerSkin>,
|
||||
pub controller: Box<dyn PlayerController>,
|
||||
pub popup: NumberPopup,
|
||||
pub damage_popup: NumberPopup,
|
||||
pub exp_popup: NumberPopup,
|
||||
strafe_up: bool,
|
||||
weapon_offset_y: i8,
|
||||
splash: bool,
|
||||
tick: u8,
|
||||
booster_switch: BoosterSwitch,
|
||||
damage_counter: u16,
|
||||
damage_taken: i16,
|
||||
pub anim_num: u16,
|
||||
anim_counter: u16,
|
||||
anim_rect: Rect<u16>,
|
||||
|
@ -167,10 +166,9 @@ impl Player {
|
|||
air: 0,
|
||||
skin,
|
||||
controller: Box::new(DummyPlayerController::new()),
|
||||
popup: NumberPopup::new(),
|
||||
damage_popup: NumberPopup::new(),
|
||||
exp_popup: NumberPopup::new(),
|
||||
strafe_up: false,
|
||||
damage_counter: 0,
|
||||
damage_taken: 0,
|
||||
anim_num: 0,
|
||||
anim_counter: 0,
|
||||
anim_rect: constants.player.frames_right[0],
|
||||
|
@ -189,6 +187,12 @@ impl Player {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn load_skin(&mut self, texture_name: String, state: &mut SharedGameState, ctx: &mut Context) {
|
||||
self.skin = Box::new(BasicPlayerSkin::new(texture_name, state, ctx));
|
||||
self.display_bounds = self.skin.get_display_bounds();
|
||||
self.hit_bounds = self.skin.get_hit_bounds();
|
||||
}
|
||||
|
||||
fn tick_normal(&mut self, state: &mut SharedGameState, npc_list: &NPCList) -> GameResult {
|
||||
if !state.control_flags.interactions_disabled() && state.control_flags.control_enabled() {
|
||||
if self.equip.has_air_tank() {
|
||||
|
@ -233,7 +237,7 @@ impl Player {
|
|||
self.booster_switch = BoosterSwitch::None;
|
||||
}
|
||||
|
||||
if state.control_flags.control_enabled() {
|
||||
if state.control_flags.control_enabled() && state.settings.allow_strafe {
|
||||
if self.controller.trigger_strafe() {
|
||||
if self.controller.move_up() {
|
||||
self.strafe_up = true;
|
||||
|
@ -258,19 +262,20 @@ impl Player {
|
|||
}
|
||||
|
||||
if state.control_flags.control_enabled() {
|
||||
let trigger_only_down = self.controller.trigger_down()
|
||||
&& !self.controller.trigger_up()
|
||||
&& !self.controller.trigger_left()
|
||||
&& !self.controller.trigger_right()
|
||||
&& !self.controller.trigger_shoot();
|
||||
|
||||
let only_down = self.controller.move_down()
|
||||
&& !self.controller.move_up()
|
||||
&& !self.controller.move_left()
|
||||
&& !self.controller.move_right()
|
||||
&& !self.controller.shoot();
|
||||
&& !self.controller.shoot()
|
||||
&& !self.controller.jump()
|
||||
&& !self.controller.prev_weapon()
|
||||
&& !self.controller.next_weapon()
|
||||
&& !self.controller.map()
|
||||
&& !self.controller.inventory()
|
||||
&& !self.controller.strafe();
|
||||
// Leaving the skip button unchecked as a "feature" :)
|
||||
|
||||
if trigger_only_down
|
||||
if self.controller.trigger_down()
|
||||
&& only_down
|
||||
&& !self.cond.interacted()
|
||||
&& !state.control_flags.interactions_disabled()
|
||||
|
@ -286,7 +291,7 @@ impl Player {
|
|||
self.vel_x += physics.dash_ground;
|
||||
}
|
||||
|
||||
if !self.controller.strafe() {
|
||||
if !self.controller.strafe() || !state.settings.allow_strafe {
|
||||
if self.controller.move_left() {
|
||||
self.direction = Direction::Left;
|
||||
}
|
||||
|
@ -358,7 +363,7 @@ impl Player {
|
|||
self.vel_x += physics.dash_air;
|
||||
}
|
||||
|
||||
if !self.controller.strafe() {
|
||||
if !self.controller.strafe() || !state.settings.allow_strafe {
|
||||
if self.controller.look_left() {
|
||||
self.direction = Direction::Left;
|
||||
}
|
||||
|
@ -402,10 +407,10 @@ impl Player {
|
|||
// stop interacting when moved
|
||||
if state.control_flags.control_enabled()
|
||||
&& (self.controller.move_left()
|
||||
|| self.controller.move_right()
|
||||
|| self.controller.move_up()
|
||||
|| self.controller.jump()
|
||||
|| self.controller.shoot())
|
||||
|| self.controller.move_right()
|
||||
|| self.controller.move_up()
|
||||
|| self.controller.jump()
|
||||
|| self.controller.shoot())
|
||||
{
|
||||
self.cond.set_interacted(false);
|
||||
}
|
||||
|
@ -441,7 +446,7 @@ impl Player {
|
|||
|
||||
let mut booster_dir = self.direction;
|
||||
|
||||
if self.controller.strafe() {
|
||||
if self.controller.strafe() && state.settings.allow_strafe {
|
||||
if self.controller.move_left() {
|
||||
self.booster_switch = BoosterSwitch::Left;
|
||||
} else if self.controller.move_right() {
|
||||
|
@ -524,8 +529,8 @@ impl Player {
|
|||
if (self.flags.hit_bottom_wall() && self.flags.hit_right_higher_half() && self.vel_x < 0)
|
||||
|| (self.flags.hit_bottom_wall() && self.flags.hit_left_higher_half() && self.vel_x > 0)
|
||||
|| (self.flags.hit_bottom_wall()
|
||||
&& self.flags.hit_left_lower_half()
|
||||
&& self.flags.hit_right_lower_half())
|
||||
&& self.flags.hit_left_lower_half()
|
||||
&& self.flags.hit_right_lower_half())
|
||||
{
|
||||
self.vel_y = 0x400; // 2.0fix9
|
||||
}
|
||||
|
@ -533,9 +538,9 @@ impl Player {
|
|||
|
||||
let max_move = if self.flags.in_water()
|
||||
&& !(self.flags.force_left()
|
||||
|| self.flags.force_up()
|
||||
|| self.flags.force_right()
|
||||
|| self.flags.force_down())
|
||||
|| self.flags.force_up()
|
||||
|| self.flags.force_right()
|
||||
|| self.flags.force_down())
|
||||
{
|
||||
state.constants.player.water_physics.max_move
|
||||
} else {
|
||||
|
@ -554,7 +559,7 @@ impl Player {
|
|||
droplet.cond.set_alive(true);
|
||||
droplet.y = self.y;
|
||||
droplet.direction =
|
||||
if self.flags.water_splash_facing_right() { Direction::Right } else { Direction::Left };
|
||||
if self.flags.bloody_droplets() { Direction::Right } else { Direction::Left };
|
||||
|
||||
for _ in 0..7 {
|
||||
droplet.x = self.x + (state.game_rng.range(-8..8) * 0x200) as i32;
|
||||
|
@ -739,15 +744,15 @@ impl Player {
|
|||
|
||||
if self.flags.hit_bottom_wall() {
|
||||
if self.cond.interacted() {
|
||||
self.skin.set_state(PlayerAnimationState::Examining);
|
||||
self.anim_num = 0;
|
||||
self.anim_num = 11;
|
||||
self.anim_counter = 0;
|
||||
self.skin.set_state(PlayerAnimationState::Examining, self.anim_counter);
|
||||
} else if state.control_flags.control_enabled()
|
||||
&& (self.controller.move_up() || self.strafe_up)
|
||||
&& (self.controller.move_left() || self.controller.move_right())
|
||||
{
|
||||
self.cond.set_fallen(true);
|
||||
self.skin.set_state(PlayerAnimationState::WalkingUp);
|
||||
self.skin.set_state(PlayerAnimationState::WalkingUp, self.anim_counter);
|
||||
|
||||
self.anim_counter += 1;
|
||||
if self.anim_counter > 4 {
|
||||
|
@ -766,7 +771,7 @@ impl Player {
|
|||
&& (self.controller.move_left() || self.controller.move_right())
|
||||
{
|
||||
self.cond.set_fallen(true);
|
||||
self.skin.set_state(PlayerAnimationState::Walking);
|
||||
self.skin.set_state(PlayerAnimationState::Walking, self.anim_counter);
|
||||
|
||||
self.anim_counter += 1;
|
||||
if self.anim_counter > 4 {
|
||||
|
@ -787,8 +792,8 @@ impl Player {
|
|||
}
|
||||
|
||||
self.cond.set_fallen(false);
|
||||
self.skin.set_state(PlayerAnimationState::LookingUp);
|
||||
self.anim_num = 0;
|
||||
self.skin.set_state(PlayerAnimationState::LookingUp, self.anim_counter);
|
||||
self.anim_num = 5;
|
||||
self.anim_counter = 0;
|
||||
} else {
|
||||
if self.cond.fallen() {
|
||||
|
@ -796,30 +801,24 @@ impl Player {
|
|||
}
|
||||
|
||||
self.cond.set_fallen(false);
|
||||
self.skin.set_state(PlayerAnimationState::Idle);
|
||||
self.skin.set_state(PlayerAnimationState::Idle, self.anim_counter);
|
||||
self.anim_num = 0;
|
||||
self.anim_counter = 0;
|
||||
}
|
||||
} else if state.control_flags.control_enabled()
|
||||
&& (self.controller.look_up() || self.strafe_up)
|
||||
&& self.control_mode == ControlMode::Normal
|
||||
{
|
||||
self.skin.set_state(PlayerAnimationState::FallingLookingUp);
|
||||
self.anim_num = 0;
|
||||
} else if self.up {
|
||||
self.skin.set_state(PlayerAnimationState::FallingLookingUp, self.anim_counter);
|
||||
self.anim_num = 6;
|
||||
self.anim_counter = 0;
|
||||
} else if state.control_flags.control_enabled()
|
||||
&& self.controller.look_down()
|
||||
&& self.control_mode == ControlMode::Normal
|
||||
{
|
||||
self.skin.set_state(PlayerAnimationState::FallingLookingDown);
|
||||
self.anim_num = 0;
|
||||
} else if self.down {
|
||||
self.skin.set_state(PlayerAnimationState::FallingLookingDown, self.anim_counter);
|
||||
self.anim_num = 10;
|
||||
self.anim_counter = 0;
|
||||
} else {
|
||||
if self.vel_y > 0 {
|
||||
self.skin.set_state(PlayerAnimationState::Falling);
|
||||
self.skin.set_state(PlayerAnimationState::Falling, self.anim_counter);
|
||||
self.anim_num = 1;
|
||||
} else {
|
||||
self.skin.set_state(PlayerAnimationState::Jumping);
|
||||
self.skin.set_state(PlayerAnimationState::Jumping, self.anim_counter);
|
||||
self.anim_num = 3;
|
||||
}
|
||||
self.anim_counter = 0;
|
||||
|
@ -860,7 +859,7 @@ impl Player {
|
|||
|
||||
if state.constants.is_switch && self.air == 0 && self.flags.in_water() && !state.get_flag(4000) {
|
||||
self.skin.set_appearance(PlayerAppearanceState::Default);
|
||||
self.skin.set_state(PlayerAnimationState::Drowned);
|
||||
self.skin.set_state(PlayerAnimationState::Drowned, self.anim_counter);
|
||||
}
|
||||
|
||||
self.anim_rect = self.skin.animation_frame();
|
||||
|
@ -894,11 +893,8 @@ impl Player {
|
|||
self.controller.set_rumble(rumble_intensity, rumble_intensity, 20);
|
||||
|
||||
self.damage = self.damage.saturating_add(final_hp as u16);
|
||||
if self.popup.value > 0 {
|
||||
self.popup.set_value(-(self.damage as i16));
|
||||
} else {
|
||||
self.popup.add_value(-(self.damage as i16));
|
||||
}
|
||||
self.damage_popup.add_value(-(self.damage as i16));
|
||||
self.damage_popup.update_displayed_value();
|
||||
|
||||
if self.life == 0 {
|
||||
state.sound_manager.play_sfx(17);
|
||||
|
@ -917,6 +913,9 @@ impl Player {
|
|||
let _ = npc_list.spawn(0x100, npc.clone());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "discord-rpc")]
|
||||
let _ = state.discord_rpc.update_hp(&self);
|
||||
}
|
||||
|
||||
pub fn update_teleport_counter(&mut self, state: &SharedGameState) {
|
||||
|
@ -931,6 +930,12 @@ impl Player {
|
|||
impl GameEntity<&NPCList> for Player {
|
||||
fn tick(&mut self, state: &mut SharedGameState, npc_list: &NPCList) -> GameResult {
|
||||
if !self.cond.alive() {
|
||||
if self.life == 0 {
|
||||
self.damage_popup.x = self.x;
|
||||
self.damage_popup.y = self.y - self.display_bounds.top as i32 + 0x1000;
|
||||
self.damage_popup.tick(state, ())?;
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
|
@ -938,18 +943,14 @@ impl GameEntity<&NPCList> for Player {
|
|||
self.shock_counter = 0;
|
||||
}
|
||||
|
||||
if self.damage_counter != 0 {
|
||||
self.damage_counter -= 1;
|
||||
}
|
||||
|
||||
if self.xp_counter != 0 {
|
||||
self.xp_counter -= 1;
|
||||
}
|
||||
|
||||
if self.shock_counter != 0 {
|
||||
self.shock_counter -= 1;
|
||||
} else if self.damage_taken != 0 {
|
||||
self.damage_taken = 0;
|
||||
} else if self.exp_popup.value != 0 {
|
||||
self.exp_popup.update_displayed_value();
|
||||
}
|
||||
|
||||
match (self.control_mode, state.settings.noclip) {
|
||||
|
@ -958,9 +959,12 @@ impl GameEntity<&NPCList> for Player {
|
|||
(ControlMode::IronHead, _) => self.tick_ironhead(state)?,
|
||||
}
|
||||
|
||||
self.popup.x = self.x;
|
||||
self.popup.y = self.y - self.display_bounds.top as i32 + 0x1000;
|
||||
self.popup.tick(state, ())?;
|
||||
self.damage_popup.x = self.x;
|
||||
self.damage_popup.y = self.y - self.display_bounds.top as i32 + 0x1000;
|
||||
self.damage_popup.tick(state, ())?;
|
||||
self.exp_popup.x = self.x;
|
||||
self.exp_popup.y = self.y - self.display_bounds.top as i32 + 0x1000;
|
||||
self.exp_popup.tick(state, ())?;
|
||||
|
||||
self.cond.set_increase_acceleration(false);
|
||||
self.tick_animation(state);
|
||||
|
|
|
@ -106,7 +106,7 @@ impl Player {
|
|||
let mut flags = Flag(0);
|
||||
|
||||
if ((self.y - self.hit_bounds.top as i32) < (npc.y + npc.hit_bounds.bottom as i32 - 0x600))
|
||||
&& ((self.y + self.hit_bounds.bottom as i32) > (npc.y - npc.hit_bounds.bottom as i32 + 0x600))
|
||||
&& ((self.y + self.hit_bounds.bottom as i32) > (npc.y - npc.hit_bounds.top as i32 + 0x600))
|
||||
&& ((self.x - self.hit_bounds.right as i32) < (npc.x + npc.hit_bounds.right as i32))
|
||||
&& ((self.x - self.hit_bounds.right as i32) > npc.x)
|
||||
{
|
||||
|
@ -118,7 +118,7 @@ impl Player {
|
|||
}
|
||||
|
||||
if ((self.y - self.hit_bounds.top as i32) < (npc.y + npc.hit_bounds.bottom as i32 - 0x600))
|
||||
&& ((self.y + self.hit_bounds.bottom as i32) > (npc.y - npc.hit_bounds.bottom as i32 + 0x600))
|
||||
&& ((self.y + self.hit_bounds.bottom as i32) > (npc.y - npc.hit_bounds.top as i32 + 0x600))
|
||||
&& ((self.x + self.hit_bounds.right as i32 - 0x200) > (npc.x - npc.hit_bounds.right as i32))
|
||||
&& ((self.x + self.hit_bounds.right as i32 - 0x200) < npc.x)
|
||||
{
|
||||
|
@ -287,14 +287,8 @@ impl Player {
|
|||
inventory.add_xp(npc.exp, self, state);
|
||||
|
||||
if let Some(weapon) = inventory.get_current_weapon() {
|
||||
if weapon.wtype == WeaponType::Spur {
|
||||
npc.exp = 0;
|
||||
} else {
|
||||
if self.popup.value > 0 {
|
||||
self.popup.add_value(npc.exp as i16);
|
||||
} else {
|
||||
self.popup.set_value(npc.exp as i16);
|
||||
}
|
||||
if weapon.wtype != WeaponType::Spur {
|
||||
self.exp_popup.add_value(npc.exp as i16);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -319,6 +313,9 @@ impl Player {
|
|||
npc.cond.set_alive(false);
|
||||
|
||||
state.sound_manager.play_sfx(20);
|
||||
|
||||
#[cfg(feature = "discord-rpc")]
|
||||
let _ = state.discord_rpc.update_hp(&self);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
|
|
@ -142,8 +142,8 @@ impl PlayerSkin for BasicPlayerSkin {
|
|||
PlayerAnimationState::Examining => 7,
|
||||
PlayerAnimationState::Sitting => 8,
|
||||
PlayerAnimationState::Collapsed => 9,
|
||||
PlayerAnimationState::Jumping => 1,
|
||||
PlayerAnimationState::Falling => 2,
|
||||
PlayerAnimationState::Jumping => 2,
|
||||
PlayerAnimationState::Falling => 1,
|
||||
PlayerAnimationState::FallingLookingUp => 4,
|
||||
PlayerAnimationState::FallingLookingDown => 6,
|
||||
PlayerAnimationState::FallingUpsideDown => 10,
|
||||
|
@ -179,10 +179,15 @@ impl PlayerSkin for BasicPlayerSkin {
|
|||
}
|
||||
}
|
||||
|
||||
fn set_state(&mut self, state: PlayerAnimationState) {
|
||||
fn set_state(&mut self, state: PlayerAnimationState, tick: u16) {
|
||||
if self.state != state {
|
||||
self.state = state;
|
||||
self.tick = 0;
|
||||
|
||||
//self.tick = 0; // this should not happen
|
||||
//self.tick = curr_tick; // this should happen instead, but there's a problem with ticking on 4 that results in an instant 1st frame animation.
|
||||
// this dirty hack should fix that.
|
||||
self.tick = if tick % 5 == 4 { u16::MAX } else { tick };
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -53,7 +53,7 @@ pub trait PlayerSkin: PlayerSkinClone {
|
|||
fn tick(&mut self);
|
||||
|
||||
/// Sets the current animation state.
|
||||
fn set_state(&mut self, state: PlayerAnimationState);
|
||||
fn set_state(&mut self, state: PlayerAnimationState, tick: u16);
|
||||
|
||||
/// Returns current animation state.
|
||||
fn get_state(&self) -> PlayerAnimationState;
|
||||
|
|
|
@ -7,7 +7,7 @@ use crate::common::{Direction, FadeState, get_timestamp};
|
|||
use crate::framework::context::Context;
|
||||
use crate::framework::error::GameError::ResourceLoadError;
|
||||
use crate::framework::error::GameResult;
|
||||
use crate::game::player::ControlMode;
|
||||
use crate::game::player::{ControlMode, TargetPlayer};
|
||||
use crate::game::shared_game_state::{GameDifficulty, SharedGameState};
|
||||
use crate::game::weapon::{WeaponLevel, WeaponType};
|
||||
use crate::scene::game_scene::GameScene;
|
||||
|
@ -54,7 +54,7 @@ impl GameProfile {
|
|||
state.control_flags.set_tick_world(true);
|
||||
state.control_flags.set_control_enabled(true);
|
||||
|
||||
let _ = state.sound_manager.play_song(self.current_song as usize, &state.constants, &state.settings, ctx);
|
||||
let _ = state.sound_manager.play_song(self.current_song as usize, &state.constants, &state.settings, ctx, false);
|
||||
|
||||
game_scene.inventory_player1.current_weapon = self.current_weapon as u16;
|
||||
game_scene.inventory_player1.current_item = self.current_item as u16;
|
||||
|
@ -157,19 +157,29 @@ impl GameProfile {
|
|||
game_scene.player2.skin.apply_gamestate(state);
|
||||
}
|
||||
|
||||
pub fn dump(state: &mut SharedGameState, game_scene: &mut GameScene) -> GameProfile {
|
||||
pub fn dump(state: &mut SharedGameState, game_scene: &mut GameScene, target_player: Option<TargetPlayer>) -> GameProfile {
|
||||
let player = match target_player.unwrap_or(TargetPlayer::Player1) {
|
||||
TargetPlayer::Player1 => &game_scene.player1,
|
||||
TargetPlayer::Player2 => &game_scene.player2,
|
||||
};
|
||||
|
||||
let inventory_player = match target_player.unwrap_or(TargetPlayer::Player1) {
|
||||
TargetPlayer::Player1 => &game_scene.inventory_player1,
|
||||
TargetPlayer::Player2 => &game_scene.inventory_player2,
|
||||
};
|
||||
|
||||
let current_map = game_scene.stage_id as u32;
|
||||
let current_song = state.sound_manager.current_song() as u32;
|
||||
let pos_x = game_scene.player1.x as i32;
|
||||
let pos_y = game_scene.player1.y as i32;
|
||||
let direction = game_scene.player1.direction;
|
||||
let max_life = game_scene.player1.max_life;
|
||||
let stars = game_scene.player1.stars as u16;
|
||||
let life = game_scene.player1.life;
|
||||
let current_weapon = game_scene.inventory_player1.current_weapon as u32;
|
||||
let current_item = game_scene.inventory_player1.current_item as u32;
|
||||
let equipment = game_scene.player1.equip.0 as u32;
|
||||
let control_mode = game_scene.player1.control_mode as u32;
|
||||
let pos_x = player.x as i32;
|
||||
let pos_y = player.y as i32;
|
||||
let direction = player.direction;
|
||||
let max_life = player.max_life;
|
||||
let stars = player.stars as u16;
|
||||
let life = player.life;
|
||||
let current_weapon = inventory_player.current_weapon as u32;
|
||||
let current_item = inventory_player.current_item as u32;
|
||||
let equipment = player.equip.0 as u32;
|
||||
let control_mode = player.control_mode as u32;
|
||||
let counter = 0; // TODO
|
||||
let mut weapon_data = [
|
||||
WeaponData { weapon_id: 0, level: 0, exp: 0, max_ammo: 0, ammo: 0 },
|
||||
|
@ -194,7 +204,7 @@ impl GameProfile {
|
|||
];
|
||||
|
||||
for (idx, weap) in weapon_data.iter_mut().enumerate() {
|
||||
if let Some(weapon) = game_scene.inventory_player1.get_weapon(idx) {
|
||||
if let Some(weapon) = inventory_player.get_weapon(idx) {
|
||||
weap.weapon_id = weapon.wtype as u32;
|
||||
weap.level = weapon.level as u32;
|
||||
weap.exp = weapon.experience as u32;
|
||||
|
@ -204,7 +214,7 @@ impl GameProfile {
|
|||
}
|
||||
|
||||
for (idx, item) in items.iter_mut().enumerate() {
|
||||
if let Some(sitem) = game_scene.inventory_player1.get_item_idx(idx) {
|
||||
if let Some(sitem) = inventory_player.get_item_idx(idx) {
|
||||
*item = sitem.0 as u32 + (((sitem.1 - 1) as u32) << 16);
|
||||
}
|
||||
}
|
||||
|
@ -305,8 +315,9 @@ impl GameProfile {
|
|||
}
|
||||
|
||||
pub fn load_from_save<R: io::Read>(mut data: R) -> GameResult<GameProfile> {
|
||||
// Do041220
|
||||
if data.read_u64::<BE>()? != 0x446f303431323230 {
|
||||
let magic = data.read_u64::<BE>()?;
|
||||
// Do041220, Do041115
|
||||
if magic != 0x446f303431323230 && magic != 0x446f303431313135 {
|
||||
return Err(ResourceLoadError("Invalid magic".to_owned()));
|
||||
}
|
||||
|
||||
|
|
|
@ -56,7 +56,12 @@ __doukutsu_rs_runtime_dont_touch._known_settings = {
|
|||
["doukutsu-rs.new_game.event_id"] = 0x1003,
|
||||
["doukutsu-rs.new_game.stage_id"] = 0x1004,
|
||||
["doukutsu-rs.new_game.pos"] = 0x1005,
|
||||
["doukutsu-rs.window.height"] = 0x1100,
|
||||
["doukutsu-rs.window.width"] = 0x1101,
|
||||
["doukutsu-rs.window.title"] = 0x1102,
|
||||
["doukutsu-rs.font_scale"] = 0x2000,
|
||||
["doukutsu-rs.tsc.encoding"] = 0x3000,
|
||||
["doukutsu-rs.tsc.encrypted"] = 0x3001,
|
||||
}
|
||||
|
||||
__doukutsu_rs_runtime_dont_touch._requires = {}
|
||||
|
@ -385,8 +390,8 @@ function doukutsu.playSfxLoop(id)
|
|||
__doukutsu_rs:playSfxLoop(id)
|
||||
end
|
||||
|
||||
function doukutsu.playSong(id)
|
||||
__doukutsu_rs:playSong(id)
|
||||
function doukutsu.playSong(id, fadeout)
|
||||
__doukutsu_rs:playSong(id, fadeout)
|
||||
end
|
||||
|
||||
function doukutsu.players()
|
||||
|
@ -518,7 +523,7 @@ function ModCS.SkipFlag.Get(id)
|
|||
end
|
||||
|
||||
function ModCS.Organya.Play(id)
|
||||
__doukutsu_rs:playSong(id)
|
||||
__doukutsu_rs:playSong(id, false)
|
||||
end
|
||||
|
||||
function ModCS.Sound.Play(id, loop)
|
||||
|
|
|
@ -229,8 +229,9 @@ declare namespace doukutsu {
|
|||
/**
|
||||
* Changes current music to one with specified ID.
|
||||
* If ID equals 0, the music is stopped.
|
||||
* If ID equals 0 and fadeout is true, the music is faded out.
|
||||
*/
|
||||
function playMusic(id: number): void;
|
||||
function playMusic(id: number, fadeout: boolean = false): void;
|
||||
|
||||
/**
|
||||
* Returns the value of a certain TSC flag.
|
||||
|
|
|
@ -8,6 +8,7 @@ use lua_ffi::lua_method;
|
|||
use crate::common::{Direction, Rect};
|
||||
use crate::framework::filesystem;
|
||||
use crate::game::scripting::lua::{check_status, DRS_RUNTIME_GLOBAL, LuaScriptingState};
|
||||
use crate::game::scripting::tsc::text_script::TextScriptEncoding;
|
||||
use crate::scene::game_scene::LightingMode;
|
||||
use crate::util::rng::RNG;
|
||||
|
||||
|
@ -46,8 +47,10 @@ impl Doukutsu {
|
|||
let game_state = &mut (*(*self.ptr).state_ptr);
|
||||
let ctx = &mut (*(*self.ptr).ctx_ptr);
|
||||
|
||||
let fadeout = if let Some(fadeout_flag) = state.to_bool(3) { fadeout_flag } else { false };
|
||||
|
||||
let _ =
|
||||
game_state.sound_manager.play_song(index as usize, &game_state.constants, &game_state.settings, ctx);
|
||||
game_state.sound_manager.play_song(index as usize, &game_state.constants, &game_state.settings, ctx, fadeout);
|
||||
}
|
||||
|
||||
0
|
||||
|
@ -156,14 +159,45 @@ impl Doukutsu {
|
|||
game_state.constants.game.new_game_player_pos = (ng_x as i16, ng_y as i16);
|
||||
}
|
||||
}
|
||||
0x1100 => {
|
||||
// window height
|
||||
//TODO
|
||||
}
|
||||
0x1101 => {
|
||||
// window width
|
||||
//TODO
|
||||
}
|
||||
0x1102 => {
|
||||
// window title
|
||||
//TODO
|
||||
}
|
||||
0x2000 => {
|
||||
// font scale
|
||||
if let Some(font_scale) = state.to_float(3) {
|
||||
if font_scale > 0.0 {
|
||||
game_state.constants.font_scale = font_scale;
|
||||
game_state.font.scale(font_scale);
|
||||
}
|
||||
}
|
||||
}
|
||||
0x3000 => {
|
||||
// tsc encoding
|
||||
if let Some(encoding) = state.to_str(3) {
|
||||
let enc = TextScriptEncoding::from(encoding.clone());
|
||||
|
||||
if TextScriptEncoding::invalid_encoding(enc, &game_state) {
|
||||
log::warn!("{} encoding is invalid", encoding.clone());
|
||||
} else {
|
||||
game_state.constants.textscript.encoding = enc;
|
||||
log::debug!("{} encoding is set", encoding.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
0x3001 => {
|
||||
// tsc encrypted
|
||||
if let Some(encrypted) = state.to_bool(3) {
|
||||
game_state.constants.textscript.encrypted = encrypted;
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@ use std::io::{Cursor, Read};
|
|||
use crate::framework::error::GameError::ParseError;
|
||||
use crate::framework::error::GameResult;
|
||||
use crate::game::scripting::tsc::text_script::TextScriptEncoding;
|
||||
use crate::util::encoding::{read_cur_shift_jis, read_cur_wtf8};
|
||||
|
||||
pub fn put_varint(val: i32, out: &mut Vec<u8>) {
|
||||
let mut x = ((val as u32) >> 31) ^ ((val as u32) << 1);
|
||||
|
@ -43,7 +42,7 @@ pub fn read_cur_varint(cursor: &mut Cursor<&[u8]>) -> GameResult<i32> {
|
|||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn read_varint<I: Iterator<Item=u8>>(iter: &mut I) -> GameResult<i32> {
|
||||
pub fn read_varint<I: Iterator<Item = u8>>(iter: &mut I) -> GameResult<i32> {
|
||||
let mut result = 0u32;
|
||||
|
||||
for o in 0..5 {
|
||||
|
@ -62,27 +61,21 @@ pub fn put_string(buffer: &mut Vec<u8>, out: &mut Vec<u8>, encoding: TextScriptE
|
|||
if buffer.is_empty() {
|
||||
return;
|
||||
}
|
||||
let mut chars_count = 0;
|
||||
|
||||
let mut cursor: Cursor<&Vec<u8>> = Cursor::new(buffer);
|
||||
let mut tmp_buf = Vec::new();
|
||||
let mut remaining = buffer.len() as u32;
|
||||
let mut chars = 0;
|
||||
|
||||
while remaining > 0 {
|
||||
let (consumed, chr) = match encoding {
|
||||
TextScriptEncoding::UTF8 => read_cur_wtf8(&mut cursor, remaining),
|
||||
TextScriptEncoding::ShiftJIS => read_cur_shift_jis(&mut cursor, remaining),
|
||||
};
|
||||
let encoding: &encoding_rs::Encoding = encoding.into();
|
||||
|
||||
remaining -= consumed;
|
||||
chars += 1;
|
||||
|
||||
put_varint(chr as i32, &mut tmp_buf);
|
||||
let decoded_text = encoding.decode_without_bom_handling(&buffer).0;
|
||||
for chr in decoded_text.chars() {
|
||||
chars_count += 1;
|
||||
put_varint(chr as _, &mut tmp_buf);
|
||||
}
|
||||
|
||||
buffer.clear();
|
||||
|
||||
put_varint(chars, out);
|
||||
put_varint(chars_count, out);
|
||||
out.append(&mut tmp_buf);
|
||||
}
|
||||
|
||||
|
|
|
@ -185,7 +185,6 @@ impl TextScript {
|
|||
// One operand codes
|
||||
TSCOpCode::BOA
|
||||
| TSCOpCode::BSL
|
||||
| TSCOpCode::FOB
|
||||
| TSCOpCode::FOM
|
||||
| TSCOpCode::QUA
|
||||
| TSCOpCode::UNI
|
||||
|
@ -229,6 +228,7 @@ impl TextScript {
|
|||
}
|
||||
// Two operand codes
|
||||
TSCOpCode::FON
|
||||
| TSCOpCode::FOB
|
||||
| TSCOpCode::MOV
|
||||
| TSCOpCode::AMp
|
||||
| TSCOpCode::NCJ
|
||||
|
|
|
@ -158,7 +158,7 @@ impl CreditScriptVM {
|
|||
CreditOpCode::ChangeMusic => {
|
||||
let song = read_cur_varint(&mut cursor)? as u16;
|
||||
|
||||
state.sound_manager.play_song(song as usize, &state.constants, &state.settings, ctx)?;
|
||||
state.sound_manager.play_song(song as usize, &state.constants, &state.settings, ctx, false)?;
|
||||
|
||||
state.creditscript_vm.state = CreditScriptExecutionState::Running(cursor.position() as u32);
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue