Compare commits

...

155 Commits

Author SHA1 Message Date
biroder c33d387031 Add Ridiculon soundtrack name 2024-05-20 01:20:30 +03:00
Laura K b572fdfd47
Fix dead Discord CDN urls [ci skip] 2024-04-27 20:00:37 +02:00
Laura K 94b9f930ce
ci: fix windows targets 2024-04-27 00:24:32 +02:00
莯凛 01ec93dd27
support custom text encoding for `.tsc` and stage table (#259)
* feat: support optional custom encoding
* feat: support custom encoding for stage table
2024-04-10 13:06:14 +03:00
biroder ca5361cc58 Add panic logging 2024-03-25 16:06:47 +00:00
poly000 08f086bfc4
localize difficulty name in save menu (#263) 2024-03-25 11:41:21 +00:00
poly000 1f288e2a39
replace `impl Clone` with `#[derive(Clone)]` (#262) [ci skip] 2024-03-25 11:04:13 +00:00
poly000 0e0cd66564
ci: migrate to node 20 actions (#261) 2024-03-24 14:58:32 +02:00
József Sallai 9c95b20f5c use localized soundtrack names 2024-03-24 00:57:56 +02:00
Alula 7630a9b60e
Update gamecontrollerdb 2024-03-11 07:57:07 +01:00
Alula ae909878c4
Parse machine code to determine vanilla stage table offset 2024-03-08 02:46:21 +01:00
Alula 7785513e8b
Fix inaccuracy: bloody water droplets 2024-03-08 01:44:30 +01:00
Alula 7f33f7b6c8
Support for loading 0.9.x.x beta saves 2024-03-08 01:39:48 +01:00
József Sallai 6c9b4d9a54
fix minor grammar mistake in readme 2024-03-04 00:12:12 +02:00
Edward Stuckey af9947b931 Fix ORG sampling bug
Also included the full org2 wavetable.
2024-02-29 19:54:09 +01:00
Laura K b275816e76
Update README.md [ci skip] 2024-02-23 11:44:25 +01:00
Laura K 8e3ccea8a1
Add font credits [ci skip] 2024-02-19 22:21:21 +01:00
biroder 76ec4771b3 Add local.properties file [ci skip] 2024-02-09 21:07:29 +00:00
biroder 3b77cdf0c5 Change the search directory for Lua scripts back to data dir[ci skip] 2024-01-26 10:30:08 +02:00
biroder 397cd8f584 Make some changes to the Android port
- Add application category and description
- Hide system navigation bar
- Change documents provider name to application name
- Disable multi-window mode support because touch controls don't work in this mode
2024-01-16 12:17:52 +00:00
biroder 21c255efb4
Make discord-rich-presence an optional dependency [ci skip] 2024-01-09 11:58:05 +00:00
Sobakin e1fb118910
Added cutscene auto skip option (#249) 2024-01-09 11:17:08 +00:00
Laura K c56bd2e8ae
Add EGS warning + some cleanup [ci skip] 2023-12-29 03:22:00 +01:00
periwinkle e09fbf5c63 Make all occurrences of Stage::change_tile spawn an accurate amount of smoke
Wouldn't it be better to have change_tile itself make the smoke?
2023-12-29 03:08:46 +01:00
biroder c4cffd54a8
Update sdl2 2023-12-28 17:12:38 +00:00
biroder 99f13a746e
Dummy change to trigger CI build 2023-12-27 03:07:33 +02:00
biroder 690c97e44d
Add nightly builds link for Android 2023-12-26 20:37:22 +02:00
biroder 311ca8b12f
Update ci.yml [ci skip] 2023-12-14 20:04:44 +02:00
biroder 152e31966a
Update ci.yml
This will need to be fixed later
2023-12-14 19:43:30 +02:00
biroder b4ede5bcad
Enable Android nightly builds 2023-12-14 19:18:44 +02:00
biroder a5f49c07e4 Fix #241 2023-12-08 15:07:54 +00:00
biroder fc66b84d8f
Add commit sha to nightly builds metadata 2023-11-27 09:13:14 +00:00
biroder 87cc7f12e3
Fix nightly build links [ci skip] 2023-11-26 15:56:49 +02:00
biroder e9cf548cc2 Add builds metadata saving
And remove the `pkg-config` dependency installation for Linux builds.
2023-11-26 15:31:09 +02:00
biroder 3af1d61e5b
Fix CI 2023-11-12 12:34:38 +02:00
Edward Stuckey 3b14adf949
Fix Player gun desync bug and inventory inconsistencies (#245)
* add inventory and player render fixes
2023-11-12 10:02:59 +00:00
biroder 46710dd31a Make some changes to README and CI config[ci skip]
Change links to nightly builds in README.

Change the version of nightly builds (use the build number at the end of the version string for master and part of the commit hash for other branches). Make it so that the workflow can be run manually. Disable cache saving for non-master branches, because in this case the cache is always created new and doesn't update the existing one, which wastes cache space. Temporarily disable build for Android
2023-11-11 00:38:47 +02:00
biroder 10e4d3efff
Change build number 2023-11-06 16:14:48 +00:00
biroder 2b1411787b Switch to Github Actions and add Android nightly builds 2023-11-06 17:48:00 +00:00
biroder 0e33bcaaf9
Prevent "Missing data files" message box from closing 2023-11-02 13:11:36 +00:00
biroder 45443dfa23 Fix #221 2023-10-30 09:35:35 +00:00
biroder 06ae269b7b Implement "Open game/user data directory" menus on Android[ci skip] 2023-10-29 17:17:01 +00:00
periwinkle 12a305becb
Accuracy fixes for misc NPCs (#242) 2023-10-06 13:14:05 +03:00
periwinkle bd203cfddb
Fix Misery bubble going the wrong way (oops sorry) (#240) 2023-09-28 11:18:51 +03:00
periwinkle c2ad3dd643
Accuracy fixes for Waterway through Hell (#237)
* Accuracy fixes for Waterway through Balcony

* Hell accuracy fixes
2023-09-27 22:14:26 -04:00
biroder 3468bcf5fd Disable debug hotkeys in non-debug mode, fix Balrog text scrolling sound from #165 2023-09-25 14:30:12 +03:00
biroder 21221d80e7 Fix #233 2023-09-03 11:40:00 +03:00
József Sallai a45c630116 add compact jukebox for non-switch CS+ (#232) 2023-08-23 02:50:03 +03:00
biroder 73295a6351 <FMU opcode fix 2023-08-10 13:39:40 +03:00
biroder ab87862646
Fix #222 [ci skip]
In general this isn't a bug or inaccuracy, because in the vanilla it's normal behaviour. But since most of the pause menu entries don't working during the intro, this had to be fixed anyway
2023-07-14 18:15:21 +03:00
József Sallai 5f24ee52b0 make strafing toggleable (closes #220) 2023-07-11 10:20:13 +03:00
biroder 02e1763e1d Fix a couple of warnings[ci skip] 2023-07-05 11:58:54 +03:00
biroder 44c6af2146
Add guide to setting up game data on Android 2023-07-05 11:37:58 +03:00
periwinkle d1d008edda Balfrog accuracy fixes 2023-07-04 08:39:58 +02:00
periwinkle 4d7dfd0266 Accuracy fixes for First Cave through Grasstown 2023-07-04 08:39:58 +02:00
biroder b70f0007b1
add japanese translation of display touch controls option[ci skip] 2023-06-28 14:39:18 +03:00
biroder f7148edd96 This implementation is better[ci skip] 2023-06-28 12:15:54 +03:00
biroder 425a26b3a0 Add display touch controls option[ci skip] 2023-06-28 11:37:05 +03:00
periwinkle f6caffd624
Accuracy fixes for Sand Zone and Labyrinth (#215)
* Accuracy fixes for Sand Zone and Labyrinth
And a couple of smaller things as well

* Remove NPCList::remove_by_type (kill_npcs_by_type does the same thing)
Also rename remove_by_event to kill_npcs_by_event for consistency
2023-06-22 17:26:51 +03:00
biroder b294f65656
fix of fix [ci skip] 2023-06-21 12:15:08 +03:00
karnak a5ed1a370e Fix fullscreen mode on glutin backend[ci skip] 2023-06-21 10:54:52 +03:00
karnak f07228895c Add some tsc settings to the Lua API[ci skip] 2023-06-19 16:05:24 +03:00
periwinkle b3007c10e3 Better number popup behavior (fixes #163) 2023-06-17 15:04:26 +02:00
periwinkle 7caff84e04
Fix LOTS of bugs and inaccuracies (#213)
- Player vs. soft solid NPCs used the wrong collision size
- Curly w/ Nemesis attached too high above the player when standing facing up
- Butes from Bute spawners started following the player at the wrong conditions
- Small pignons in Cemetery never turned around
- Refill stations spawned with alt direction didn't spawn with upward velocity
- Misery blocks spawned with glitchy framerects
- Various issues with possessed Misery
- Various issues with possessed Sue
- Ballos (phase 1) didn't face the player at certain points
- Ballos orbiting eyes (phase 4) didn't spawn smoke when killed
- A part of Ballos phase 2 had the wrong collision type
  (apparently this was intentional due to a bug with soft solid collision;
  hopefully that's also fixed by these changes?)
- A few missing sound effects during the Ballos fight
- Green devil generators weren't spawning enough enemies
- Balrog missile trails were in the wrong direction
- Core blade projectiles had the wrong check for water collision
- Monster X fish missiles moved at the wrong initial angle
- Sisters fireball attacks shot in the wrong direction
- Sisters missing smoke when shot
- Undead Core missing smoke in some cases
- <DNA'd NPCs spawned the wrong amount of smoke
  (fixes Heavy Press lightning despawn for the speedrun ;) )
- Player jumping and falling animations were flipped
- Implemented <MYDxxxx facing towards entity xxxx if xxxx >= 10
  (also <MYD0003 should not set the player's direction to down)
- <TRA resets invincibility in freeware (due to starting a new event)
- Bubbler level 1 bullets spawned at the wrong offset
- Bubbler and Machine Gun autofire timers shouldn't reset when fire button is pressed
- Fixed level 3 missile and Super Missile spawn offsets and trajectories
- Nemesis carets spawned at wrong offsets
- Various small typos/logic errors
2023-06-16 12:51:22 +03:00
Alula dcdb4e9d39
Fix <FOB emitting wrong code 2023-06-15 21:24:14 +02:00
biroder 596c7b8aff Lua api fixes[ci skip] 2023-06-15 13:01:09 +03:00
biroder ed2c5f510a Fixed game crash when disabing Discord Rich Presense setting and Discord isn't running. Fixed crash o exit when Discord isn't running 2023-05-30 12:37:41 +03:00
biroder 0ba5aad8af Merge branch 'master' of https://github.com/doukutsu-rs/doukutsu-rs 2023-05-30 11:35:07 +03:00
biroder 50ff888141 Fix #208 and add a different log level for file logging 2023-05-29 10:42:34 +03:00
biroder 72e74e2113 Merge branch 'master' of https://github.com/doukutsu-rs/doukutsu-rs 2023-05-29 10:38:28 +03:00
biroder 47a43467d0 Fix #208 and add different log level for file logging 2023-05-29 10:32:27 +03:00
biroder 19e0da519f
Fix of.fix(again) [ci skip] 2023-05-24 15:04:56 +03:00
biroder 721a0c907a Fix og logo offset 2023-05-24 13:59:50 +03:00
biroder 2a3341e3c5 Update android port strings 2023-05-23 10:10:12 +03:00
biroder 63f903104a Fix of #192 2023-05-16 17:00:17 +03:00
biroder 277f5d957b Add logging to file 2023-05-11 17:40:11 +03:00
biroder 9409bb35fe
The final change to the save point smoke [ci skip]
Hope this is the last change:/
2023-04-30 12:21:49 +03:00
biroder 9e09d56b76
Some changes to creating smoke when save point spawned in a shelter 2023-04-26 18:22:09 +03:00
biroder 40767c021b Fix innacuracy [ci skip] 2023-04-26 12:07:20 +03:00
biroder 0e164a44df
Revert changes [ci skip] 2023-04-23 15:04:41 +03:00
biroder 38338ae551
let's try again 2023-04-23 14:22:37 +03:00
biroder dbb72ff6c1
yet another fix 2023-04-23 14:12:07 +03:00
biroder 0a45332f3f
fix 2023-04-23 12:45:08 +03:00
biroder aa27680baf
Trying to add android CI builds 2023-04-23 12:42:45 +03:00
biroder 67fb32499f Partial fixes of bosses 2023-04-17 16:51:52 +03:00
biroder 5821f06928
Dehardcode menu toogles text [ci skip] 2023-04-14 16:39:16 +03:00
biroder 5f93658f0f
Fix of fix textscript encoding [ci skip] 2023-04-13 18:02:41 +03:00
biroder cd671cce48 * i18n improving for Android [ci skip] 2023-04-13 17:10:27 +03:00
biroder 2a162d948f
Fix textscript encoding 2023-04-12 21:36:32 +03:00
biroder 72a2ada0b2
Minor refactoring of TSC decryption [ci skip] 2023-04-10 16:23:46 +03:00
biroder 0ccbbefbed
Fix `skip_commits` rule in CI config [ci skip] 2023-04-08 11:46:13 +03:00
biroder bc91d9f366 Minor icon change 2023-04-03 17:35:22 +03:00
Alula beb290dafb
no more april fools 2023-04-02 11:52:32 +02:00
biroder 5f7bae8f4f
Fix a linker error on Windows 2023-04-02 11:52:12 +03:00
biroder 11eafb8d03
Add window icons for non-Windows systems (#206) 2023-04-01 21:18:25 +03:00
biroder f99b452073
Fix sound volume change and inventory overflow on Android (#207)
* Android changing volume fix. Added arrows to indicate volume buttons

* Fix inventory overflowing on Android
2023-04-01 20:27:30 +03:00
Alula 51834be404
yeah 2023-04-01 13:56:52 +02:00
Alula b2fb548584
april fools 2023-04-01 13:53:52 +02:00
biroder 1633eef3dd
Skip appveyor builds for android and horizon (#205)
Builds for Android and Horizon isn't currently configured, so starting the build after changing these files is useless
2023-03-25 12:42:44 +02:00
biroder e57c9cdb27
Minor editor fixes (#204) 2023-03-24 12:52:55 +02:00
biroder 07b1550cf4 Revoke remove of kotlin builtins
They may be used by one of dependencies, so maybe it's better to leave them
2023-03-17 13:22:33 +01:00
biroder a15fd4d190 Remove useless files from output android build 2023-03-17 13:22:33 +01:00
biroder b8af9aed25 Fix display of picked up expirience when player took damage 2023-03-15 15:15:56 +01:00
biroder 8992889e94 Maybe this implementation would be better 2023-03-15 15:15:56 +01:00
biroder f91e793062 Add damage amount display when taking fatal damage
Fix doukutsu-rs/doukutsu-rs#163
2023-03-15 15:15:56 +01:00
biroder 3fbe94ecd1 Remove use of MakePortable enrty on unsupported targets (#201)
`MakePortable` not supported on `android` and `horizon` targets
2023-03-03 20:28:43 +02:00
biroder 5bd0dcd564
DocumentsProvider refreshing after changing files (#197)
* DocumentsProvider refreshing after changing files

* Add a flags for notifyChange function
2023-03-03 17:46:00 +02:00
József Sallai 41b840d13f reset game difficulty on title screen (fixes #196) 2023-02-25 13:45:36 +02:00
József Sallai 95e09ded99 fix game crashing when discord isn't open (lol) 2023-02-24 13:42:47 +02:00
József Sallai 6bccf59f5b improve quick discord RPC updates and prevent crashes 2023-02-23 15:23:59 +02:00
József Sallai 890c0596ed add portable user directory setting 2023-02-23 14:27:53 +02:00
József Sallai b22ca8b35e add difficulty icons to discord rich presence 2023-02-18 22:13:14 +02:00
József Sallai e77241cd56 improve command line appearance and autofocus 2023-02-18 21:05:30 +02:00
József Sallai 3dbe56690a add experimental discord rich presence support 2023-02-18 20:42:00 +02:00
József Sallai 1810bf6d5b fix missing `as_any` methods in the glutin backend 2023-02-17 16:06:11 +02:00
József Sallai d6715bccea add menu options for opening user and data dirs 2023-02-17 16:05:38 +02:00
alula 30c85c2f8b
Merge pull request #193 from biroder/patch-1 2023-02-07 09:16:34 +01:00
biroder 979909faa8
Fix 'render_opengl' import
Import 'render_opengl' only when feature 'render-opengl' is enabled
2023-02-06 10:23:39 +02:00
Alula 90df8faa7a
fix a nasty type confusion bug in SDL backend 2023-01-30 23:18:16 +01:00
Daedliy 6d3c127912
Minor missing texture rewording (#190)
* Minor missing texture rewording

* ababa

---------

Co-authored-by: Daedliy <missari.bb@gmail.com>
2023-01-29 19:40:42 +02:00
Alula 515a0a7fe7
Well, there's something to see there now... [ci skip] 2023-01-28 00:47:50 +01:00
Alula 7fe96780de
oops I pushed and forgot to save [ci skip] 2023-01-28 00:44:13 +01:00
Alula f1e5d11b2a
Switch to 0.101 and introduce new versioning scheme 2023-01-28 00:42:56 +01:00
Alula 3739e9f170
do not hardcode x86_64 as arch name on windows 2023-01-28 00:41:07 +01:00
József Sallai 3c79582fed bump webbrowser 2023-01-26 17:42:16 +02:00
József Sallai a187942cff remove 2P options from pause menu on android 2023-01-26 17:34:35 +02:00
József Sallai 4ae085f7e2 add engine constant flags for difficulty and 2P menus 2023-01-26 17:29:53 +02:00
József Sallai 153b1b4962 fix P2 skin OOB error 2023-01-26 16:56:13 +02:00
Alula 2774eae66f
revert [ci skip] 2023-01-25 20:46:16 +01:00
Alula 4928ce4682
set version to 0.100.0-beta5 for just this commit for gh release builds 2023-01-25 20:45:34 +01:00
Alula 64290ae5a3
h 2023-01-25 20:08:20 +01:00
Alula 1457aa3caa
preserve cache 2023-01-25 19:52:24 +01:00
Alula ee58c1c8af
revert [ci skip] 2023-01-25 19:40:48 +01:00
Alula a5bb130e73
this is stupid as fuck but I don't want to remake the release once again 2023-01-25 19:40:19 +01:00
Alula bbd20a003b
bruh [ci skip] 2023-01-25 19:38:21 +01:00
Alula ef1c2a5930
hotfix time 2023-01-25 18:23:15 +01:00
Alula 074af609bc
fix syntax error [ci skip] 2023-01-25 15:53:40 +01:00
Alula efa8a47b8d
use tag for CI builds if present [ci skip] 2023-01-25 15:48:39 +01:00
Alula bc3616d073
android + hos fixes 2023-01-25 15:25:51 +01:00
Devon W 510442490e
Map System Equip fix (#185)
Changing map key enable from hardcoded to item 2 to the actual vanilla behavior of checking for equip flag.
2023-01-23 17:51:40 -05:00
József Sallai 0a7fd4dc47 add win32 build to readme [skip ci] 2023-01-23 13:27:29 +02:00
József Sallai 4fa7069e82 fix 32-bit windows builds 2023-01-22 21:13:19 +02:00
József Sallai 212d7b915b don't try to render fps counter on no data scene 2023-01-22 20:27:46 +02:00
József Sallai cb95db1e89 fix text wrapping on no data scene 2023-01-22 19:42:40 +02:00
József Sallai 334e64f499
kurwa mać 2023-01-22 18:19:13 +02:00
József Sallai 90e58649a7
fuck yaml 2023-01-22 18:06:24 +02:00
Alula e845c87738
let's see if win32 builds work 2023-01-22 16:19:26 +01:00
alula 2cb36de715
Merge pull request #183 from doukutsu-rs/horizon-os
merge Horizon branch with portability fixes
2023-01-18 00:12:48 +01:00
alula c2a8bf52e9
Merge branch 'master' into horizon-os 2023-01-17 17:57:55 +01:00
József Sallai e975a75ec4 sue 2023-01-06 10:01:45 +02:00
Alula 5854735392
i forgor to push 2023-01-01 17:36:16 +01:00
József Sallai 356f4230b5 refactor co-op skins 2022-12-23 00:26:46 +02:00
József Sallai 4be3dd518b add basic support for switch P2 skins 2022-12-22 22:59:24 +02:00
József Sallai 5ed2d40e23 rumble failure shouldn't crash game 2022-12-18 19:07:37 +02:00
Alula d87bbf2b46 abstract gamepad away from SDL 2022-11-21 15:13:46 +01:00
135 changed files with 4914 additions and 9490 deletions

View File

@ -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

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

@ -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

46
.github/workflows/release.yml vendored Normal file
View File

@ -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

View File

@ -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" }

View File

@ -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.

1
app/.gitignore vendored
View File

@ -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/

View File

@ -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"
]
}
}
}

View File

@ -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" />

View File

@ -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)

View File

@ -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());
}
}

View File

@ -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();
}
}
}
}

View File

@ -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;
}
}
}

View File

@ -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();
}
}
}

View File

@ -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();
});
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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()
}
}

View File

@ -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

0
app/local.properties Normal file
View File

View File

@ -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") {

View File

@ -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"]

View File

@ -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();
}

View File

@ -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]

View File

@ -1,3 +1,3 @@
Experimental. Nothing to see there yet.
Experimental.
ld script and .specs taken from devkitPro

View File

@ -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 \

View File

@ -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 \

View File

@ -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");

View File

@ -0,0 +1 @@
curl -o ./src/data/builtin/gamecontrollerdb.txt https://raw.githubusercontent.com/mdqinc/SDL_GameControllerDB/master/gamecontrollerdb.txt

BIN
res/nx_icon.bmp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
res/sue.bmp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -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

View File

@ -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(())
}
}

View File

@ -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(

View File

@ -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;

View File

@ -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);

View File

@ -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"

View File

@ -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.

View File

@ -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"),

View File

@ -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())),
};

View File

@ -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() {

189
src/discord/mod.rs Normal file
View File

@ -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();
}
}

View File

@ -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 {

View File

@ -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) {

View File

@ -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 {

View File

@ -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> {

View File

@ -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<()> {

View File

@ -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
}
}

View File

@ -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 {

View File

@ -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)
}
}

View File

@ -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)
}

View File

@ -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 {

View File

@ -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 {

View File

@ -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))
})
}
}

View File

@ -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())),

View File

@ -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()));

View File

@ -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];

View File

@ -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(),
);
}

View File

@ -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;
}
_ => (),

View File

@ -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);
}
}
_ => (),

View File

@ -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;
}

View File

@ -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;

View File

@ -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;

View File

@ -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 };
}

View File

@ -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);

View File

@ -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;

View File

@ -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 };

View File

@ -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;

View File

@ -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);

View File

@ -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(());
}
}
_ => (),

View File

@ -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;

View File

@ -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();

View File

@ -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;
}

View File

@ -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;

View File

@ -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;

View File

@ -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);
}
}

View File

@ -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;

View File

@ -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);
}
}

View File

@ -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);

View File

@ -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;

View File

@ -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];
}

View File

@ -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);
}
}
}

View File

@ -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;
}
_ => (),
}

View File

@ -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);
}
}

View File

@ -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, ())?;

View File

@ -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(

View File

@ -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);
}
}
}

View File

@ -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);

View File

@ -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);
}
_ => {}
}

View File

@ -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 };
}
}

View File

@ -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;

View File

@ -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()));
}

View File

@ -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)

View File

@ -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.

View File

@ -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;
}
}
_ => {}
}
}

View File

@ -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);
}

View File

@ -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

View File

@ -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