Compare commits

...

544 Commits

Author SHA1 Message Date
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 a246d1a2f9
Switch homebrew port notes 2022-12-01 15:20:02 +01:00
Alula e567dd6296
Use correct branch for cpal 2022-12-01 14:41:57 +01:00
Alula 066389a29f
thanks dawn 2022-12-01 14:39:07 +01:00
Alula 67979a03ea
Mostly-working Horizon port 2022-12-01 14:30:59 +01:00
Alula f91513edd2
small oops 2022-11-21 15:16:57 +01:00
Alula 5d92cafe67
Initial Horizon OS bringup 2022-11-21 15:15:51 +01:00
Alula d87bbf2b46 abstract gamepad away from SDL 2022-11-21 15:13:46 +01:00
Alula 2860938b9a
abstract gamepad away from SDL 2022-11-21 15:12:45 +01:00
Alula e74b586dd1
Font rendering refactor 2022-11-20 20:38:49 +01:00
alula 0fca898c54
Update README.md 2022-11-20 10:19:01 +01:00
Alula df467e8764
gitignore moment 2022-11-19 18:27:28 +01:00
Alula 17e1156850
Refactoring time 2022-11-19 18:20:03 +01:00
Alula 9fd04ed47a
remove this stupid feature 2022-11-19 12:21:10 +01:00
Alula c9e6dd7181
add teleport_player command and use if let() instead of unwrapping 2022-11-19 12:10:02 +01:00
dawnDus 0330cf3b2b
Fix MYB bumping away from NPC when direction >4 2022-11-12 17:11:03 -05:00
dawnDus 2be1c422d6
Adjust horizontal booster caret (Fixes #173) 2022-11-03 20:29:24 -04:00
Awesomegamer6566 0f79474aae Fix downward facing fans pointing upwards 2022-11-01 22:42:52 +01:00
dawnDus 713e704b9b
Fix seasonal mychar offsets 2022-10-26 17:04:06 -04:00
dawnDus d42632f973
Fix Curly NPC 2022-10-21 12:07:43 -04:00
dawnDus 57b2be5211
Fix stuck popups with Spur 2022-10-17 17:40:30 -04:00
Alula 3a756e0ac4
abuse ASLR for RNG seeding 2022-10-15 14:13:04 +02:00
József Sallai 6607d2fc15 play get item sfx on AM+ 2022-10-10 10:44:51 +03:00
József Sallai 8ebe210105 play get item sound on <I+N 2022-10-06 19:31:10 +03:00
József Sallai f0949f49cf fix a few NPC inaccuracies from #165 2022-09-24 23:27:27 +03:00
dawnDus 2b9a0198cb
Safely get current stage name for Save Menu 2022-09-17 18:05:10 -04:00
József Sallai 8684dd8448 some runtime optimizations 2022-09-17 13:57:37 +03:00
Awesomegamer6566 72c268647f
Fix changing BGM Interpolation setting with left and right keys (#161)
* Fix changing BGM Interpolation setting with left and right keys

* Actually fix changing BGM Interpolation Setting with left and right keys
2022-09-16 22:08:19 +03:00
Daedliy e9d2099f42
Add Links Menu (#159)
* Add Links Menu

* Suggested Changes
2022-08-29 00:06:58 +03:00
Sallai József f7d635a3d7 add switch boss health bar 2022-08-28 16:17:54 +03:00
Sallai József 029d6d52e4 add/drop player 2 from pause menu 2022-08-28 15:00:53 +03:00
Sallai József dec913dd65 make vanilla extractor input and output configurable 2022-08-28 14:07:01 +03:00
Sallai József 028c60157d make localization system dynamic 2022-08-26 03:17:45 +03:00
dawnDus 3d86995feb
Add fast-forward cutscene skip option 2022-08-23 18:27:39 -04:00
Sallai József 670e6891c1 run arbitrary TSC from debug command line 2022-08-22 03:08:57 +03:00
dawnDus a25dc297ef
Add spawn_npc command 2022-08-21 18:32:51 -04:00
Sallai József 4ed7ba66b8 add in-game debug command line 2022-08-22 01:10:33 +03:00
Sallai József b1b3b131e2 process debug keys in game scene and add quick save shortcut 2022-08-21 22:06:06 +03:00
Sallai József f1b3c680c9 throttle number popup on NPC damage 2022-08-21 18:57:01 +03:00
Sallai József ca1fa7b7c0 fix egg corridor inaccuracies (fixes #155) 2022-08-21 13:20:04 +03:00
dawnDus 3cc9d75681
Fix spur damage inaccuracy 2022-08-20 21:22:45 -04:00
dawnDus 92bc887663
Fixed order of operations for bullets (Fixes #131) 2022-08-20 18:43:48 -04:00
Sallai József 59da01b7b9 even more rust
sprites by @RedCoder09 and @Krunchy0920
2022-08-20 22:08:15 +03:00
Sallai József e07207b40c more rust
crab headband sprites by @RedCoder09
2022-08-16 14:47:11 +03:00
Daedliy 1688f4bcbc
Proper wiiware DEMO support (#153)
* Fixed Crashing

* Add Demo Splash Rect + Fixes

* FIX newgame pos (Joe)
2022-08-14 18:36:50 +03:00
Sallai József 8c70e1a13d fix missing muscle doctor action and sue teleport sound 2022-08-14 16:49:11 +03:00
Sallai József b1d578c0b4 record last challenge attempts 2022-08-14 14:19:46 +03:00
Sallai József 290068dd37 add gamepad rumble 2022-08-13 17:54:05 +03:00
Sallai József 7b359ae4c1 main menu improvements 2022-08-07 22:54:23 +03:00
Sallai József 56631201c8 fix igor inaccuracies (fixes #132) 2022-08-06 20:41:18 +03:00
dawnDus 81755d4ad9
Require control to look up/down while falling 2022-08-06 12:44:14 -04:00
Sallai József f1542246c6 make curly's weapon respect light cone setting (fixes #109) 2022-08-06 17:06:17 +03:00
Sallai József b29c375a7a fix broken new game menu after deleting save 2022-08-06 14:47:02 +03:00
Sallai József bfd9c8c343 oops 2022-08-06 14:10:21 +03:00
Sallai József 1883045f75 make controller selection its own menu 2022-08-06 14:07:00 +03:00
Sallai József 4cfbcc50ac add gamecontrollerdb, update default inputs, add reset controls option 2022-08-01 03:18:43 +03:00
Sallai József f74ec19cb5 make menu ok/back remappable and fix jump/shoot rebind bug (#150) 2022-07-31 14:50:46 +03:00
Sallai József c68fedaa50 fix menu controller swap issues and handle jump/shoot rebind edge case 2022-07-31 03:00:19 +03:00
Sallai József 8a4201f381 fix some controls menu bugs 2022-07-31 02:16:34 +03:00
Sallai József ffaf12cca8 add controls settings menu 2022-07-30 23:20:53 +03:00
Sallai József 03e9c9db0c make menus scrollable 2022-07-30 15:01:22 +03:00
dawnDus c9ba05c948
Ballos inaccuracies (Fixes #149) 2022-07-27 21:27:26 -04:00
Sallai József 914555eac0 don't disable keyboard input when using gamepad 2022-07-27 14:14:05 +03:00
Sallai József fee63f2600 make pausing on focus loss toggleable 2022-07-25 11:41:20 +03:00
alula 2a792db797
Update README.md 2022-07-25 06:09:22 +02:00
Sallai József a598f31716 don't pause the game loop when the window loses focus 2022-07-24 19:38:39 +03:00
Sallai József ef040a393c refactor menus to use enums instead of indices 2022-07-24 17:52:51 +03:00
Sallai József 4a6b2c4400 add gamepad jukebox indicators 2022-07-23 23:29:58 +03:00
Sallai József 398f610c09 skip after Y/N prompts 2022-07-23 20:55:58 +03:00
Sallai József fb4ac0dae8 add gamepad input icons 2022-07-23 18:45:08 +03:00
Sallai József 0415f917f8 render = as white circle (closes #110) 2022-07-23 14:57:40 +03:00
Sallai József 6f95e6109c add methods for rendering text with sprite rects 2022-07-23 13:59:13 +03:00
dawnDus cdd3a37754
Fixed snack tile rendering (Fixes #147) 2022-07-22 17:32:16 -04:00
József Sallai f00f35ca2c
actually bump the version number 2022-07-21 16:49:45 +03:00
Sallai József eee0f9eff9 bump settings version to fix skip keybinding and gamepad mappings 2022-07-21 14:38:44 +03:00
Sallai József 84d9dbf877 remove gilrs dependency for controller support 2022-07-21 03:19:23 +03:00
Sallai József 9932b1209f very initial gamepad support 2022-07-20 16:07:24 +03:00
dawnDus 6d8e58090a
Allow switch data to use original portraits 2022-07-17 22:37:35 -04:00
Sallai József 93e567981e teleport second player when off-screen 2022-07-18 01:57:49 +03:00
József Sallai 7693a4ff20
Merge pull request #146 from Daedliy/patch-1
re-add wiiware extraction guide
2022-07-17 21:31:46 +03:00
Daedliy 86f89e7522
re-add wiiware extraction guide
using the more complete one from the CSMC
2022-07-17 14:48:02 -03:00
Sallai József 3297b151ad update extraction guide link on android 2022-07-17 17:57:33 +03:00
Sallai József aaecafcb72 stop shipping data files in appveyor builds and update readme 2022-07-17 17:38:35 +03:00
Sallai József 444539405a detect and extract vanilla exe resources 2022-07-16 15:33:31 +03:00
Sallai József 2177382b5a add fullscreen toggle 2022-07-13 17:03:17 +03:00
Sallai József a1d0f2dc63 fix 2P behavior in TSC commands and ironhead fight 2022-07-09 20:01:31 +03:00
Sallai József 75b077c772 add screen shake intensity setting (closes #129) 2022-07-09 18:06:22 +03:00
Sallai József d8636bc693 refactor sound manager to prevent audio crashes (fixes #112) 2022-07-09 16:49:56 +03:00
József Sallai 9d7c63571d
Merge pull request #145 from jozsefsallai/bugfix/filesystem-errors
fix filesystem errors and UB warnings on rust >= 1.62.0
2022-07-07 17:02:18 +03:00
Sallai József 3faf99b535 fix filesystem errors and UB warnings on rust >= 1.62.0 2022-07-07 16:32:33 +03:00
Sallai József 1440a91fba trigger appveyor rebuild 2022-07-07 12:21:35 +03:00
dawnDus 6d08eb716e
Add missing fireplace smoke 2022-06-30 11:40:24 -04:00
dawnDus 69fdc7d3d2
Fix warnings from #141 2022-06-30 11:39:56 -04:00
IruzzArcana 18a7670248
add coop menu (#141) 2022-06-22 18:08:36 -04:00
József Sallai d7face2544
fix zombie curly corpses not despawning (fixes #137) (#140) 2022-06-07 14:48:43 -04:00
dawnDus 987c857b1c
show missing mod.txt 2022-05-22 09:17:55 -04:00
dawnDus 3f8c66db0f
fix undead minicore rotation 2022-05-14 13:48:58 -04:00
dawnDus dc2476c9dd
fix volume rounding 2022-05-14 09:18:50 -04:00
dawnDus 8a2e9fa569
bugfixes for #120 and #122 2022-05-11 20:19:43 -04:00
dawndus 588b0d53dd
Update README.md 2022-05-10 18:31:57 -04:00
dawnDus 7c07986b5d
Minor rendering tweak [ci skip] 2022-05-02 19:34:16 -04:00
dawnDus 02a9cac305
Added Android cutscene skip 2022-05-02 18:54:57 -04:00
dawnDus af39130fed
undo android restriction 2022-05-02 16:36:27 -04:00
dawnDus 2d2e712eab
fix loading order 2022-04-30 11:54:48 -04:00
dawnDus acad65d233
enable cs+ japanese font when valid 2022-04-30 11:48:47 -04:00
Alula 24762a1c45
enable cs+ japanese font 2022-04-29 20:38:02 +02:00
dawnDus 75f5e9f364
Cutscene skip tutorial prompt 2022-04-26 23:23:55 -04:00
dawnDus 7e793e09a8
Bug Fixes 2022-04-26 23:23:14 -04:00
dawnDus 7dcf30f854
Android pause menu fix 2022-04-24 18:25:37 -04:00
dawnDus 5fdd9676cb
Refire fix 2022-04-24 17:45:44 -04:00
dawnDus f672ff6f24
Trigger CPS and CSS when going to title 2022-04-24 11:55:37 -04:00
dawnDus e4ec69b6dc
Assorted Fixes 2022-04-24 11:32:26 -04:00
Daedliy 934df79f85
debug menu additions (#124) 2022-04-24 11:01:31 -04:00
dawnDus c97ed04fea
Inventory inaccuracies (fixes #119) 2022-04-21 13:24:44 -04:00
dawnDus daea35381b
Delete replay menu option 2022-04-20 08:47:31 -04:00
dawnDus 28a3f160c3
RNG Tweaks 2022-04-20 08:07:27 -04:00
dawnDus b94b20bf76
Fixed Curly/Toroko+/Undead Core bugs 2022-04-19 19:39:36 -04:00
dawnDus b626472f10
Debug mode config toggle 2022-04-19 18:50:04 -04:00
dawnDus 0c97d554ae
Cleanup 2022-04-19 18:20:18 -04:00
dawnDus 9b572190de
Weapon XP and missile damage (fixes #116) 2022-04-18 14:12:26 -04:00
Daedliy 90900f01e1
Menu titles (#115) 2022-04-17 20:21:57 -04:00
dawnDus adfc768a8f
Remove blank space on sound menu 2022-04-16 17:15:30 -04:00
dawnDus 11f335acc4
Puu black fix and reword Vsync options 2022-04-14 22:15:25 -04:00
Alula ca1d7a8642
Add frame cap configuration 2022-04-15 02:51:48 +02:00
dawnDus 339f822a80
Better Save Menu UX (fixes #111) 2022-04-10 15:57:19 -04:00
dawnDus 68318e3a69
Wind fortress NPCs 2022-04-08 16:37:36 -04:00
dawnDus 2d9840c901
adjust background for freeware widescreen 2022-04-02 16:45:53 -04:00
dawnDus 858abae42d
better widescreen support for cloud backgrounds (fixes #105) 2022-04-02 16:36:16 -04:00
Daedliy 730d1cb5d8
Game Speed debug text (#103) 2022-03-27 17:11:32 -04:00
dawndus 7c8f2ac60a
Update README.md 2022-03-27 09:05:41 -04:00
dawnDus 2bce0136ff
Use game_rng for NPC splash effect 2022-03-27 08:40:14 -04:00
dawnDus 8b31d0a9ab
NPCs trigger plash effect with dynamic water (fixes #102) 2022-03-27 00:31:10 -04:00
dawnDus 299ef053f4
Less dumb fullscreen 2022-03-26 18:00:20 -04:00
dawnDus 77fdd19ec2
Simple fullscreen on Maximize 2022-03-26 17:01:16 -04:00
dawnDus 55e80b4c69
Add max fall speed function for NPCs 2022-03-26 11:05:56 -04:00
dawnDus b0958749f6
More NPC refactoring 2022-03-26 10:53:03 -04:00
Alula a0df539b7b
some refactorings, behemot smoke fix 2022-03-26 10:21:08 +01:00
dawnDus fb17edea7a
Fixed <ESC skipping intro cutscene 2022-03-25 08:58:01 -04:00
dawnDus 11454183a1
Changed debug cheat display 2022-03-23 22:12:57 -04:00
dawnDus 0dbee1e854
Add debugger window for invincibility 2022-03-23 18:09:32 -04:00
dawnDus 890c297437
Add offset for Balfrog adds (Fixes #99) 2022-03-23 08:58:51 -04:00
Alula e27d555bc6
add sue banner [ci skip] 2022-03-23 02:37:02 +01:00
alula 51d384aee8
Update README.md 2022-03-23 02:36:46 +01:00
Daedliy 06709ae031
Better wiiware instructions (#98) 2022-03-22 20:30:53 -04:00
dawnDus ecdc84fba7
Update Sue 2022-03-22 13:49:25 -04:00
dawnDus 8fbab06192
update mac build 2022-03-22 13:18:53 -04:00
megumin bd00c8a372
codesign macOS builds (#97) 2022-03-22 12:56:57 -04:00
Alula d69e158b41
switch to winres crate 2022-03-22 05:26:38 +01:00
Alula 91f6f19914
put windres in build-dependencies 2022-03-22 04:51:13 +01:00
Alula fb253b7573
add windows .exe icon 2022-03-22 04:47:02 +01:00
alula 04fecdeee0
Update README.md 2022-03-22 03:38:08 +01:00
dawnDus 2e1188d854
cleanup readme [ci skip] 2022-03-21 22:27:10 -04:00
Alula cd8a23391a
Add Apple M1 builds 2022-03-22 03:15:54 +01:00
dawnDus 703413dcb6
Gaudi projectiles and red demon exp (Fixes #95) 2022-03-21 22:08:08 -04:00
Alula f60440f877
try to fix m1 build v2 2022-03-22 03:01:08 +01:00
Alula 42edfd7fd6
Add macOS ARM64 (M1) build configuration 2022-03-22 00:36:03 +01:00
alula 6408ffcf21
Update README.md 2022-03-22 00:30:29 +01:00
alula 01d88c2d33
Update README.md 2022-03-22 00:27:15 +01:00
dawnDus 675e321d8f
Fixed basu spawn location 2022-03-19 22:21:21 -04:00
dawnDus 99d7ef67e0
Missing barking puppy action 2022-03-19 09:14:41 -04:00
dawnDus 9dba30d360
Allow TSC to keep running when no proper end is reached 2022-03-17 18:26:48 -04:00
dawnDus 1e7da276ab
HP/Exp bar flash with player damage (Fixes #92) 2022-03-16 22:22:22 -04:00
József Sallai 7e3fef8d41
add challenge unlocking (#90) 2022-03-15 18:18:25 -04:00
József Sallai 1795d71b37
Basic i18n support (#82) 2022-03-14 21:54:03 -04:00
dawnDus 500f53bebb
Fix for interacting while shooting (#89) 2022-03-14 21:52:32 -04:00
dawnDus ad2beacf40
Removed extra song from jukebox 2022-03-13 18:06:08 -04:00
dawnDus c0efcbef71
Slight changes to water fill logic 2022-03-13 17:45:21 -04:00
József Sallai 2dadbdb905
add ikachan title theme (fixes #85) (#87) 2022-03-13 15:58:22 -04:00
dawnDus 8fd0a814e9
Add schema for texture_sizes.json [ci skip] 2022-03-12 12:38:58 -05:00
dawnDus 38ea01d605
Actually fix press damage 2022-03-12 10:00:46 -05:00
dawnDus 5795015059
Press damage fix 2022-03-12 09:35:06 -05:00
Daedliy df2663d9fb
Unique Data detection (#81) 2022-03-11 17:13:11 -05:00
dawnDus 2415d74a46
Challenge original graphics fix (#83) 2022-03-11 17:11:25 -05:00
dawnDus f50760d9c6
Switch offset for Quote in ending cutscene 2022-03-10 21:54:26 -05:00
dawnDus ef99809c95
Fix Switch dog rendering and add springy effect 2022-03-10 17:37:02 -05:00
dawnDus b2ae281483
Co-op cutscene handling for NPCs 2022-03-10 17:35:22 -05:00
alula 42f35b673d
Update README.md 2022-03-10 18:52:56 +01:00
dawnDus b079488c27
Fix graphics toggle breaking NPCs and TSC 2022-03-08 21:37:44 -05:00
dawnDus 06b4aeead9
Initial P2 Quote NPC 2022-03-08 21:08:53 -05:00
dawnDus 4b0b667ed5
Properly fix balrog cutscene and undo NPC 0 change 2022-03-07 08:51:12 -05:00
József Sallai d2a671e04c
refactor CS+ soundtrack loading (#79) 2022-03-07 08:47:37 -05:00
dawnDus 62efbf0cc3
Fix upward wind tile's gravity 2022-03-06 19:04:15 -05:00
József Sallai 465825797e
fix bleeding pixels in cutscene skip box (#78) 2022-03-06 17:50:02 -05:00
dawnDus e1f1dd4554
Custom SFX support 2022-03-06 17:49:25 -05:00
dawnDus cb7b2bd402
Don't draw NPC 0 2022-03-06 11:15:39 -05:00
dawnDus 067bcc5c8b
Improved texture scaling logic 2022-03-06 11:01:23 -05:00
József Sallai 05b9d9ebe0
fix menu selection snapping on first tick (#77) 2022-03-06 11:00:50 -05:00
József Sallai bc56271174
prevent stray bytes from disallowing subsequent events (fixes #73) (#74) 2022-03-05 13:03:58 -05:00
József Sallai d45e611466
fix cutscene skip speeding up title screen (#75) 2022-03-05 13:03:02 -05:00
dawnDus 0db6b60251
Display Nicalis copyright with nicalis data 2022-03-05 13:02:09 -05:00
dawnDus 657be6159e
Fix Sisters and Undead Core bugs 2022-03-05 13:01:08 -05:00
dawnDus 4b5d70ea3f
Update readme [ci skip] 2022-03-04 20:39:24 -05:00
dawnDus bba0313824
Assorted bug fixes 2022-03-04 20:38:13 -05:00
dawnDus 15010e54c2
Initial challenge replay support 2022-03-04 18:37:25 -05:00
dawnDus 0387a450ce
Fix soundtrack texture bleed 2022-03-01 18:45:10 -05:00
József Sallai 71b39cdadc
make jukebox start with selected soundtrack + chevrons (#72) 2022-03-01 18:44:03 -05:00
dawnDus 4055fef911
Difficulty changes Quote NPC and original graphics sprites 2022-02-28 21:45:43 -05:00
dawnDus 7a580fdf44
Revert fade changes 2022-02-28 21:24:24 -05:00
Alula 4cdadfc505
fix linking 2022-02-28 23:33:29 +01:00
Alula b6712409ab
strip linux binary, fix data files not being put in linux .zip 2022-02-28 20:07:25 +01:00
Alula 7f35dbf19e
some tweaks for faster builds 2022-02-28 19:31:04 +01:00
Alula b673d5a33d
fix macOS build 2022-02-28 19:13:27 +01:00
Sallai József 657b73aefc refactor stuff to make code cleaner 2022-02-28 19:03:57 +01:00
Sallai József 0c33795356 fix difficulties interfering with challenges 2022-02-28 19:03:57 +01:00
Sallai József 6b7b6b7032 add CS+ game difficulties 2022-02-28 19:03:57 +01:00
Alula a6272476ba
add screenshots [ci skip] 2022-02-28 19:01:41 +01:00
Alula 3867dd58c4
Add Linux AppVeyor builds, remove GitHub Actions. 2022-02-28 18:53:38 +01:00
Alula 390cc45153
updated readme [ci skip] 2022-02-28 18:53:18 +01:00
Alula c5e73fac91
Relicense under modified MIT (#70) 2022-02-28 09:00:18 +01:00
Alula 765d520d70
merge dawns water speed patch 2022-02-28 08:58:13 +01:00
Alula fc2f26db91
use references to slices in draw_triangle_list() 2022-02-28 08:42:04 +01:00
dawnDus efd1729ce5
Fix fade in/out for widescreen 2022-02-27 23:15:35 -05:00
dawnDus 74a5cddeaf
Fix water effect speed 2022-02-27 20:25:10 -05:00
Alula 464ea6f194
remove old shaders 2022-02-27 20:26:44 +01:00
Alula e216110864
water improvements 2022-02-27 20:26:42 +01:00
dawnDus d61602b7bb
formatting 2022-02-26 23:54:15 -05:00
dawnDus 287e06e24b
Boss bugs #68 2022-02-26 23:54:04 -05:00
dawnDus b29fe87e76
Boss damage popups 2022-02-26 23:53:33 -05:00
dawnDus ccd4030dc1
Add texture size lookup file 2022-02-26 14:07:35 -05:00
dawnDus 9b3e2837b7
Fixed falling block spawn trigger 2022-02-25 22:32:05 -05:00
dawnDus 7db42e86e6
Booster strafing 2022-02-25 21:49:14 -05:00
dawnDus f26f019584
Added strafing 2022-02-25 20:51:10 -05:00
dawnDus befac5db85
Challenge fixes: saves, nikumaru timer, menu 2022-02-25 17:00:14 -05:00
Alula 49d14b58a3
Make NPC rect arrays tolerant for invalid indexes 2022-02-25 10:51:27 +01:00
Alula 39171cc9a9 prevent zero division if 0 as wait is passed to <FON/<FOM 2022-02-25 06:06:30 +01:00
Alula 19dad43d7a tweak defaults 2022-02-25 06:03:05 +01:00
Alula 1c2eaae4d1 macOS fixes 2022-02-25 06:03:05 +01:00
dawnDus a2ebaaaab6
Further super missile fix 2022-02-24 22:40:50 -05:00
dawnDus 5909fedf33
Fixed fireball sfx and missiles going through walls (#37) 2022-02-24 21:00:10 -05:00
dawnDus 5ee580cb35
Enable challenges outside of CS+ 2022-02-23 23:21:06 -05:00
dawnDus 1070e67af4
Start event #0000 on profile load 2022-02-23 19:05:19 -05:00
József Sallai 8a94c841c6
Soundtrack menu improvements (#66) 2022-02-23 16:37:02 -05:00
dawnDus 5cf63660ef
Add save slots for challenges 2022-02-22 19:46:49 -05:00
dawnDus 41bf965937
Prevent vanish showing incorrect sheet on 1st tick 2022-02-21 17:59:18 -05:00
dawnDus 5ed2883954
Fixed ProjectileDissipation 1st frame missing 2022-02-21 17:22:14 -05:00
dawnDus d49c261a17
Even more bugfixes 2022-02-17 19:54:22 -05:00
dawnDus 0d20b1a209
Bug fixes for #63 2022-02-16 23:16:25 -05:00
Alula 44f478be75
remove clay hanson from credits and .pxchar support 2022-02-17 04:45:21 +01:00
dawnDus b771005816
New Android app icon by Daedliy 2022-02-15 17:22:07 -05:00
dawnDus c43d822c44
formatting 2022-02-14 20:50:57 -05:00
dawnDus 6a8d0bfd22
Disable exp popup for Spur 2022-02-14 20:50:18 -05:00
dawnDus e6632a845d
Drowning fixes 2022-02-14 20:03:24 -05:00
dawnDus 38efa5ded9
Fixes for #63
- Booster caret location
- Game should start tick/control (flag 3)
- Extend fade timer for Switch
2022-02-13 22:39:53 -05:00
dawnDus d7a25f2681
Parse mod names and string table 2022-02-13 14:39:28 -05:00
dawnDus dfcf2e2f3f
Initial jukebox scene 2022-02-12 13:32:48 -05:00
dawnDus 1fe00d25c6
Move broken animation fix 2022-02-12 13:31:36 -05:00
Alula 37cb574907
special treatment for cs+ challenges 2022-02-12 10:12:30 +01:00
Alula 3cd95b4427
switch like look for map system gui 2022-02-12 09:28:09 +01:00
Alula 680294def8
adjust design of skip cutscene popup 2022-02-12 09:25:13 +01:00
Alula 99c4798bed
switch: use 427px image for upper part 2022-02-12 09:19:12 +01:00
Alula e109db81e6
tsc newline fix 2022-02-12 09:18:32 +01:00
Alula c82c65c39f
round viewport width on android 2022-02-12 08:57:06 +01:00
dawnDus c4f1c60e35
Initial Android pause menu support 2022-02-11 22:28:30 -05:00
dawnDus b457f5dd6f
Fixed left/right in menu on Android 2022-02-11 19:07:27 -05:00
dawnDus 8cd22b932b
Set width of soundtrack menu to longest 2022-02-10 18:37:08 -05:00
dawnDus 2e3245c654
Use CS+ menu assets 2022-02-10 18:23:14 -05:00
dawnDus fe3e60ecbf
Fix challenge's incomplete stage.tbl 2022-02-10 18:02:45 -05:00
Alula 8e2088adb4
initial cs+ challenge support 2022-02-10 10:21:28 +01:00
Alula c127ee4bd4
use path list for resource loading 2022-02-10 08:54:20 +01:00
Alula 3374f13c2b
soundtrack switching menu 2022-02-10 07:15:28 +01:00
Alula e09ea37bda
Add support for triangle rendering using SDL 2.0.17+ APIs 2022-02-10 06:01:58 +01:00
Alula 338dbe4bc8
use git version of rust-sdl2 2022-02-10 06:01:58 +01:00
Alula 7bc8e18310
implement remaining switch TSC opcodes 2022-02-10 06:01:57 +01:00
dawnDus c722582ff2
Assorted bugs #60 2022-02-09 23:00:02 -05:00
dawnDus 0369b37d10
Save, load, and display item counts 2022-02-08 19:04:36 -05:00
dawnDus e53d4c7f43
Fix balrog missile trail 2022-02-08 17:26:21 -05:00
dawnDus 79d28822e8 Animated portrait support 2022-02-07 17:33:09 +01:00
dawndus 693155ca6a
Added save select menu (#58) 2022-02-06 12:23:24 -05:00
dawnDus 2223358991
<FAC slide-in animation 2022-02-06 12:22:26 -05:00
dawnDus a359a756d0
Include BMFont config file 2022-02-05 13:25:25 -05:00
dawnDus 32526f92f8
TSC Fixes:
- <CLR should not reset <FAC
- Last character \n bugfix
2022-02-04 19:31:16 -05:00
dawndus bd0762f812
Added volume settings (#57) 2022-02-02 22:09:29 -05:00
dawnDus 88fdb7b0ce
Load bullet.tbl and arms_level.tbl from plus files 2022-02-02 22:08:20 -05:00
dawnDus ff79957145
Enable closing map while it draws 2022-01-30 15:40:59 -05:00
dawnDus 807cc305b9
Use skin sheet for whimsical star 2022-01-29 12:48:45 -05:00
dawnDus d3d77b58e3
Refactor whimsical star 2022-01-29 11:14:39 -05:00
dawnDus ff5cf7359b
Reduce flickering and whimsical star fix 2022-01-28 19:44:03 -05:00
dawnDus 9f530ce6a5
Main menu music and cursor for nikumaru time 2022-01-28 16:17:00 -05:00
dawnDus 5725948f85
Adjust polar star caret 2022-01-28 12:52:30 -05:00
dawnDus db50f67876
Fix FacingPlayer condition 2022-01-28 12:46:33 -05:00
dawnDus a02284c439
Missing bubble caret 2022-01-27 09:43:03 -05:00
dawnDus 6e7191e5d3
Missing carets (Bubbler, Empty!, Push Jump) 2022-01-26 23:56:32 -05:00
dawnDus 654cbfb814
Universal offset for hardcoded bosses (Switch) 2022-01-26 18:51:05 -05:00
dawnDus b99cb8a34d
Use set_flag function to handle OOB flags 2022-01-25 23:41:21 -05:00
dawnDus 7448ce0e59
Fixed Sister's spawn location on Switch 2022-01-25 23:32:47 -05:00
dawnDus fba36467ea
FPS counter 2022-01-25 20:37:45 -05:00
dawnDus b7f226b322
Pause menu background dimming 2022-01-25 19:35:41 -05:00
dawnDus 698d694c43
Missed tilemap calls in editor 2022-01-25 00:16:07 -05:00
dawnDus 3b307c7c9d
Wrong dimension on clip width 2022-01-25 00:13:51 -05:00
dawnDus 95fd91b8a0
Better control over tilemap ticks 2022-01-25 00:10:13 -05:00
dawnDus 6302258817
Push tile animations 2022-01-24 23:07:23 -05:00
dawnDus d32cd87532
Pause menu improvements 2022-01-24 22:34:21 -05:00
dawnDus 7b5d4c367c
Correct layering on damage numbers 2022-01-23 19:51:18 -05:00
dawnDus e1b33aa0e9
Persistent damage numbers 2022-01-23 19:29:41 -05:00
Alula 12d7758ea7
Adjust lighting effects 2022-01-23 18:50:10 +01:00
Alula a098095c13
change version to 0.99.0 in appveyor builds 2022-01-23 18:17:01 +01:00
Alula 59b2e9954a
Fix a big mistake in ogg resampling code 2022-01-23 18:01:35 +01:00
dawnDus bb07919505
fixed core ball direciton and shock state 2022-01-23 10:09:04 -05:00
Alula 4c20234d5e fix stack overflows in audio code by moving huge structs to heap 2022-01-23 15:00:10 +01:00
Alula 0e06113738
Add Map System 2022-01-23 05:56:10 +01:00
alula 3054d07213
fix credit list appearance 2022-01-23 05:05:44 +01:00
dawnDus 1c7e4c9f65 SSS operand implemented 2022-01-22 23:32:40 +01:00
Alula b880fee8e7
Switch to upsteam cpal and avoid creating unnecessary threads 2022-01-22 05:08:53 +01:00
Alula dba6789b0a
Consistent menu spacing 2022-01-22 05:08:13 +01:00
Alula dcd33d943e
Compliant BMFont renderer 2022-01-22 05:07:53 +01:00
Alula 4325dcad86
Add support for GOG version 2022-01-22 02:19:18 +01:00
Alula d6df4640ab
Merge branch 'master' of github.com:alula/doukutsu-rs 2022-01-22 02:18:26 +01:00
Alula bc3906e39d
SDL/KAGE -> Original/Remastered because Nicalis surprised us a little 2022-01-22 02:18:12 +01:00
dawnDus 203bacb1a0 reset cutscene skip speed in main menu 2022-01-21 23:23:57 +01:00
dawnDus eadeedae6b STC command 2022-01-21 23:23:57 +01:00
dawnDus af1f9f5d89 Loading counter 2022-01-21 23:23:57 +01:00
Daedliy ce6de7c0a0
decorative border fix + spacing (#52)
* decorative border fix + spacing

* extra shifting around
2022-01-21 15:00:36 -05:00
dawnDus 6226df68ca
F2 for quick retry 2022-01-20 22:30:25 -05:00
dawndus fb5a72c565
Merge pull request #50 from dawndus/master
Ironhead letterbox
2022-01-20 21:22:17 -05:00
dawnDus c9f11d6c9f
fix jittery whimsical star during pause 2022-01-20 21:21:34 -05:00
dawnDus b181f9293f
Initial pause menu 2022-01-20 21:14:12 -05:00
dawnDus c63c520553
ghost puppy particles 2022-01-20 21:00:44 -05:00
dawnDus 53fdb890b1
curly lightcone 2022-01-19 19:23:31 -05:00
dawnDus dd8f2dcf67
Added missing core action 2022-01-19 18:50:33 -05:00
dawnDus 924f23154b
Ironhead Switch fixes 2022-01-19 00:05:34 -05:00
Vinícius Miguel f676b73c0e
typo: `rect_deserialze` -> `rect_deserialize` (#51) 2022-01-19 04:58:44 +01:00
dawnDus 46045c2a7e
Ironhead letterbox 2022-01-17 22:46:27 -05:00
Marcin Puc c8115df285
Add various refactorings (#48) 2022-01-17 23:29:30 +01:00
alula 529a1c122d
Merge pull request #49 from Daedliy/master
small start menu fixes
2022-01-17 21:51:56 +01:00
Daedliy df297819fe applying suggested changes 2022-01-17 16:34:37 -03:00
Daedliy 3d1ebf76a3 -consistent 14/7 pixel spacing between option and selected value
-fixed small bug in audio menu where cursor would select the first entry, despite being un-selectable
-gave MenuEntry:Options an additional Vec<String> for hopefully better descriptions
-removed "Player's" from "Player's Weapon Light Cone" over redundancy and so it'd fit
2022-01-16 14:52:08 -03:00
alula 824ba2c287
Update credits 2022-01-16 03:18:07 +01:00
alula 8786787249
Remove dead link to CSE2 repo 2022-01-16 03:12:16 +01:00
alula ef1d2a320e
Update README.md 2022-01-16 03:10:21 +01:00
Alula 4f00d439f3
fix android build 2022-01-16 02:57:12 +01:00
Alula 1b702d1a5a
bump version to 0.99.0, add dawn to authors 2022-01-16 02:52:42 +01:00
alula 58406aae71
Merge pull request #45 from dawndus/whimsical-star
Adding whimsical star
2022-01-16 02:46:30 +01:00
Alula 1ae5ecdbbf
fix CS+ font rendering inaccuracies 2022-01-16 02:45:17 +01:00
alula 87792972d0
Merge pull request #46 from dawndus/fixes
Minor fixes
2022-01-16 02:36:22 +01:00
Alula 87bab5fca9
add text box scrolling and fix opengl cliprects 2022-01-16 02:35:36 +01:00
dawnDus 27003f4e64
fix ballos leaving animation 2022-01-15 17:28:29 -05:00
dawnDus 2fed0928d8
Fixed butes getting stuck in walls 2022-01-15 16:36:24 -05:00
dawnDus 375c72cfbd
Fixed heavy press cutscene on switch 2022-01-15 16:35:57 -05:00
dawnDus cd049fd378
don't allow inventory while scripts are running 2022-01-15 16:13:21 -05:00
dawnDus fa0695b546
add whimsical star 2022-01-15 10:09:41 -05:00
alula e57bf29703
Merge pull request #44 from dawndus/ballos
Implemented all remaining NPCs
2022-01-15 08:04:49 +01:00
dawndus 85b1b71a0f
Merge branch 'doukutsu-rs:master' into ballos 2022-01-14 21:30:30 -05:00
dawnDus 6cef63bd0e
Fixed curly walking into death in cutscene 2022-01-14 21:30:14 -05:00
dawnDus b492be2203
All remaning NPCs 2022-01-14 21:29:39 -05:00
dawnDus 39267048c7
More ballos 2022-01-14 09:09:39 -05:00
dawnDus d5743ac972
reset ground collision check for <WAS 2022-01-13 11:34:36 -05:00
dawnDus 1f8690263b
add super_quake 2022-01-13 11:32:33 -05:00
dawnDus 10b7b5536d
Primary ballos NPC+Boss 2022-01-12 22:47:41 -05:00
Alula 89771844cc
fix black bars margin 2022-01-11 16:50:51 +01:00
Alula 0804fe86ce
fix ogg resampling bug 2022-01-11 16:01:49 +01:00
Alula e27cde67c7
Reset invicibility timer (#30) 2022-01-11 15:26:22 +01:00
alula be91851952
Merge pull request #43 from dawndus/master
Don't animate up/down in ironhead fight
2022-01-10 05:30:50 +01:00
dawnDus c4d80eecb3
Don't animate up/down in ironhead fight 2022-01-09 23:11:48 -05:00
Alula 4c8ec46864
make menu background scale better 2022-01-10 04:37:03 +01:00
dawndus 87ddcc1324
Implemented Heavy Press boss fight (#40)
* heavy press

* fixed incorrect scale for death smoke call

* Shoot caret animates at correct speed

* formatting
2022-01-10 04:02:27 +01:00
dawnDus e7b666b4cc implemented scrolling background type 2022-01-10 04:02:13 +01:00
dawnDus 6339a612b5 ironhead bugfixes 2022-01-10 04:02:13 +01:00
dawnDus cb52935d9f added ikachan 2022-01-10 04:02:13 +01:00
dawnDus 25a098145d format ma pignon 2022-01-10 04:02:02 +01:00
dawnDus af6e36ef3b control_enabled check on nikumaru 2022-01-10 04:02:02 +01:00
Alula 7d92b55b58
fix nikumaru z index and behavior on <TRA 2022-01-09 13:11:25 +01:00
Alula 124b2e2c82
hell cleanup 2022-01-09 12:56:06 +01:00
alula d572aebe32
Tyrone lied 2022-01-09 09:14:32 +01:00
Alula 0cad0b0762
document remaining flags 2022-01-08 15:43:46 +01:00
Alula 27b439f2cb
document bullet flags 2022-01-08 15:20:41 +01:00
Alula 3e6adbe3c1
fix unused variable warning in nikumaru.rs 2022-01-08 15:19:57 +01:00
Alula 7ac4346012
add nikumaru counter 2022-01-08 12:39:17 +01:00
Alula 75a1b3f9f4
<rmu fix 2022-01-08 11:36:33 +01:00
Alula 4a91448067
oops 2022-01-08 06:59:16 +01:00
Alula d484e8a183
get rid of some warnings 2022-01-08 06:57:04 +01:00
Alula 4d6768c015
fix few warnings 2022-01-08 06:15:30 +01:00
Alula b80f57ae49
fix macOS builds 2022-01-08 05:57:53 +01:00
Alula 4f3312d9ca
try to fix linux builds 2022-01-08 05:54:57 +01:00
Alula a10a5f138c
remove unreachable in curly boss bullet npc 2022-01-08 05:54:57 +01:00
Alula 94514d3b68
44100hz drums fix 2022-01-08 05:54:56 +01:00
dawndus d15fd43c84
Adding Ma Pignon NPCs (#38)
* add ma pignon

Co-authored-by: alula <6276139+alula@users.noreply.github.com>
2022-01-08 05:53:29 +01:00
dawndus 77fc4ca5b9
Hell enemies + small fixes (#36)
* Added curly_carried (320)

* add curly_carried property

* add Curly's nemesis

* add lightcone for Curly

* fixed caret spawn locations

* B2

* Butes

* Arrow collision

* Mesas

* Hell NPCs

* suggested changes

* lighting -> lightning and hell lighting

* curly lighting to NPC
2022-01-08 05:52:26 +01:00
Alula 3d912f6fbb
sound system improvements 2022-01-06 19:51:21 +01:00
Alula c8dc2d3443
remove credit.tsc debug print 2022-01-06 19:51:11 +01:00
Alula d09e0f8a91
update sdl2 crate 2022-01-06 19:49:44 +01:00
Alula cdfa550110
pretty print settings.json 2022-01-06 19:49:20 +01:00
Alula ef84379b62
editor shit 2022-01-06 02:11:17 +01:00
Alula e2afafdfa3
graphics::imgui_texture_id and soundness fixes 2022-01-05 11:42:39 +01:00
Alula ebe3c2f2af
refactor: fade 2022-01-05 06:59:47 +01:00
Alula b7680019ee
refactor: split background and fix outside scaling 2022-01-05 06:40:50 +01:00
Alula 575dcc7a6d
refactor: split text boxes 2022-01-05 06:08:36 +01:00
Alula 3b1a5f149e
refactor: split tilemap out 2022-01-05 05:50:16 +01:00
Alula ac58602ed0
add out of bounds black bars 2022-01-05 04:17:22 +01:00
Alula 4134d4754e
some editor barebones 2022-01-05 04:16:29 +01:00
Alula 5a6f7dec59
reformat 2022-01-05 04:15:19 +01:00
alula 00309a73f0
Merge pull request #35 from dawndus/master
fix Yes/No animation
2022-01-02 02:08:44 +01:00
dawnDus 9b4fd7d1a0 fix Yes/No animation 2022-01-01 17:20:06 -05:00
300 changed files with 37533 additions and 19782 deletions

View File

@ -1,23 +1,44 @@
version: "0.1.0.{build}-{branch}"
version: "0.101.0-{build}-{branch}"
skip_commits:
files:
- README.md
- LICENSE
- app/
- drsandroid/
- drshorizon/
environment:
global:
PROJECT_NAME: doukutsu-rs
matrix:
- channel: nightly
- 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: nightly
- 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
job_name: linux-x64
appveyor_build_worker_image: Ubuntu
- channel: stable
target: x86_64-apple-darwin
target_name: macos
target_name: mac-intel
job_name: mac-x64
appveyor_build_worker_image: macos
appveyor_build_worker_image: macos-monterey
- channel: stable
target: aarch64-apple-darwin
target_name: mac-m1
job_name: mac-arm64
appveyor_build_worker_image: macos-monterey
matrix:
fast_finish: true
@ -38,49 +59,100 @@ for:
- cargo -vV
cache:
- '%USERPROFILE%\.cache\sccache -> Cargo.toml'
- '%USERPROFILE%\.cargo -> 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 target\release\doukutsu-rs.exe release
- copy LICENSE release\LICENSE
- copy target\release\doukutsu-rs.exe release\doukutsu-rs.%arch_name%.exe
- cd release
- appveyor DownloadFile https://github.com/doukutsu-rs/game-data/archive/master.zip -FileName ../game-data.zip
- 7z x ../game-data.zip
- rename game-data-master data
- 7z a ../doukutsu-rs_%target_name%.zip *
- appveyor PushArtifact ../doukutsu-rs_%target_name%.zip
-
matrix:
only:
- appveyor_build_worker_image: macos
- 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
- rustup update
- rustup default $channel
- rustup target add $target
- rustc -vV
- cargo -vV
- cargo install cargo-bundle --force
cache:
- '$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
- 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
- cp -a target/$target/release/bundle/osx/doukutsu-rs.app ./release/doukutsu-rs.app
- cd release
- codesign -s - -f ./doukutsu-rs.app/Contents/MacOS/doukutsu-rs
- 7z a ../doukutsu-rs_$target_name.zip *
- appveyor PushArtifact ../doukutsu-rs_$target_name.zip
-
matrix:
only:
- appveyor_build_worker_image: Ubuntu
install:
- sudo apt-get update && sudo apt-get -y install libasound2-dev libudev-dev libgl1-mesa-dev pkg-config
- curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -yv --default-toolchain $channel --default-host $target
- export PATH=$PATH:$HOME/.cargo/bin
- rustup update
- rustup default $channel
- rustc -vV
- cargo -vV
- cargo install cargo-bundle
cache:
- '$HOME/.cache/sccache -> Cargo.toml'
- '$HOME/.cargo -> 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
- appveyor DownloadFile https://github.com/doukutsu-rs/game-data/archive/master.zip -FileName ../game-data.zip
- 7z x ../game-data.zip
- mv game-data-master data
- cargo bundle --release
#- 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 -a target/release/bundle/osx/doukutsu-rs.app ./release/doukutsu-rs.app
- cp LICENSE ./release/LICENSE
- cp -a target/release/doukutsu-rs ./release/doukutsu-rs.x86_64.elf
- 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

View File

@ -1,185 +1,46 @@
name: Release
on: [push]
jobs:
build-linux-x86_64:
runs-on: ubuntu-latest
steps:
- name: Install dependencies
run: sudo apt-get update && sudo apt-get install libasound2-dev libudev-dev pkg-config
- name: Cache Cargo registry
uses: actions/cache@v2
with:
path: ~/.cargo/registry
key: ${{ runner.os }}-stable-cargo-registry
restore-keys: |
${{ runner.os }}-stable-cargo-registry-
- name: Cache Cargo index
uses: actions/cache@v2
with:
path: ~/.cargo/git
key: ${{ runner.os }}-stable-cargo-index
restore-keys: |
${{ runner.os }}-stable-cargo-index-
- name: Cache Cargo build
uses: actions/cache@v2
with:
path: target
key: ${{ runner.os }}-stable-target
restore-keys: |
${{ runner.os }}-stable-target
- name: Checkout sources
uses: actions/checkout@v2
- name: Install latest stable Rust toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: stable
default: true
override: true
- name: Build
run: cargo build --all --release && strip target/release/doukutsu-rs
- name: Copy executable to root directory
run: cp target/release/doukutsu-rs doukutsu-rs.x86_64
- name: Upload artifacts
uses: actions/upload-artifact@v2
with:
name: doukutsu-rs_linux_x86_64
path: doukutsu-rs.x86_64
if-no-files-found: error
- name: Release
uses: softprops/action-gh-release@v1
if: startsWith(github.ref, 'refs/tags/')
with:
files: doukutsu-rs.x86_64
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
build-windows-x86:
runs-on: windows-latest
steps:
- name: Cache Cargo registry
uses: actions/cache@v2
with:
path: ~/.cargo/registry
key: ${{ runner.os }}-stable-cargo-registry
restore-keys: |
${{ runner.os }}-stable-cargo-registry-
- name: Cache Cargo index
uses: actions/cache@v2
with:
path: ~/.cargo/git
key: ${{ runner.os }}-stable-cargo-index
restore-keys: |
${{ runner.os }}-stable-cargo-index-
- name: Cache Cargo build
uses: actions/cache@v2
with:
path: target
key: ${{ runner.os }}-stable-target
restore-keys: |
${{ runner.os }}-stable-target
- name: Checkout sources
uses: actions/checkout@v2
- name: Install latest stable Rust toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: stable
target: i686-pc-windows-msvc
default: true
override: true
- name: Build
run: cargo build --target i686-pc-windows-msvc --all --release
- name: Copy executable to root directory
run: cp target/i686-pc-windows-msvc/release/doukutsu-rs.exe doukutsu-rs.x86.exe
- name: Upload artifacts
uses: actions/upload-artifact@v2
with:
name: doukutsu-rs_windows_x86
path: doukutsu-rs.x86.exe
if-no-files-found: error
- name: Release
uses: softprops/action-gh-release@v1
if: startsWith(github.ref, 'refs/tags/')
with:
files: doukutsu-rs.x86.exe
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
build-windows-x64:
runs-on: windows-latest
steps:
- name: Cache Cargo registry
uses: actions/cache@v2
with:
path: ~/.cargo/registry
key: ${{ runner.os }}-stable-cargo-registry
restore-keys: |
${{ runner.os }}-stable-cargo-registry-
- name: Cache Cargo index
uses: actions/cache@v2
with:
path: ~/.cargo/git
key: ${{ runner.os }}-stable-cargo-index
restore-keys: |
${{ runner.os }}-stable-cargo-index-
- name: Cache Cargo build
uses: actions/cache@v2
with:
path: target
key: ${{ runner.os }}-stable-target
restore-keys: |
${{ runner.os }}-stable-target
- name: Checkout sources
uses: actions/checkout@v2
- name: Install latest stable Rust toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: stable
default: true
override: true
- name: Build
run: cargo build --all --release
- name: Copy executable to root directory
run: cp target/release/doukutsu-rs.exe doukutsu-rs.x86_64.exe
- name: Upload artifacts
uses: actions/upload-artifact@v2
with:
name: doukutsu-rs_windows_x64
path: doukutsu-rs.x86_64.exe
if-no-files-found: error
- name: Release
uses: softprops/action-gh-release@v1
if: startsWith(github.ref, 'refs/tags/')
with:
files: doukutsu-rs.x86_64.exe
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
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

7
.gitignore vendored
View File

@ -2,13 +2,16 @@
.vscode/
# Cave Story (copyrighted) data files
data/
/data/
# Generated by Cargo
# will have compiled files and executables
debug/
target/
# Shader binary files
*.dksh
# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries
# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html
Cargo.lock
@ -60,3 +63,5 @@ ehthumbs_vista.db
# Recycle Bin used on file shares
$RECYCLE.BIN/
*.log

View File

@ -1,12 +1,12 @@
[package]
name = "doukutsu-rs"
description = "A re-implementation of Cave Story (Doukutsu Monogatari) engine"
version = "0.1.0"
authors = ["Alula"]
edition = "2018"
version = "0.101.0"
authors = ["Alula", "dawnDus"]
edition = "2021"
rust-version = "1.65"
[lib]
#crate-type = ["lib", "cdylib"]
crate-type = ["lib"]
[[bin]]
@ -19,83 +19,102 @@ required-features = ["exe"]
[profile.release]
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.1.0"
version = "0.101.0"
resources = ["data"]
copyright = "Copyright (c) 2020-2021 Alula and 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"]
default-base = ["scripting-lua", "ogg-playback", "netplay"]
default = ["default-base", "backend-sdl", "render-opengl", "exe", "webbrowser", "discord-rpc"]
default-base = ["ogg-playback"]
ogg-playback = ["lewton"]
backend-sdl = ["sdl2", "sdl2-sys"]
backend-glutin = ["winit", "glutin", "render-opengl"]
backend-horizon = []
render-opengl = []
scripting-lua = ["lua-ffi"]
netplay = ["tokio", "serde_cbor"]
discord-rpc = ["discord-rich-presence"]
netplay = ["serde_cbor"]
editor = []
hooks = ["libc"]
exe = []
android = []
[dependencies]
#cpal = { path = "./3rdparty/cpal" }
#glutin = { path = "./3rdparty/glutin/glutin", optional = true }
#lua-ffi = { path = "./3rdparty/luajit-rs", optional = true }
#winit = { path = "./3rdparty/winit", optional = true, default_features = false, features = ["x11"] }
bitvec = "0.20"
#sdl2 = { path = "./3rdparty/rust-sdl2", optional = true, features = ["unsafe_textures", "bundled", "static-link"] }
#sdl2-sys = { path = "./3rdparty/rust-sdl2/sdl2-sys", optional = true, features = ["bundled", "static-link"] }
#cpal = { path = "./3rdparty/cpal" }
byteorder = "1.4"
case_insensitive_hashmap = "1.0.0"
chrono = "0.4"
cpal = { git = "https://github.com/doukutsu-rs/cpal.git", rev = "4218ff23242834d36bcdcc0c2e3883985c15b5e0" }
chrono = { version = "0.4", default-features = false, features = ["clock", "std"] }
cpal = { git = "https://github.com/doukutsu-rs/cpal", rev = "9d269d8724102404e73a61e9def0c0cbc921b676" }
directories = "3"
funty = "=1.1.0" # https://github.com/bitvecto-rs/bitvec/issues/105
glutin = { git = "https://github.com/doukutsu-rs/glutin.git", rev = "8dd457b9adb7dbac7ade337246b6356c784272d9", optional = true, default_features = false, features = ["x11"] }
imgui = "0.8.0"
image = { version = "0.23", default-features = false, features = ["png", "bmp"] }
discord-rich-presence = { version = "0.2", optional = true }
downcast = "0.11"
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"
lazy_static = "1.4.0"
lewton = { version = "0.10.2", optional = true }
libc = { version = "0.2", optional = true }
lazy_static = "1.4"
lewton = { version = "0.10", optional = true }
log = "0.4"
lua-ffi = { git = "https://github.com/doukutsu-rs/lua-ffi.git", rev = "e0b2ff5960f7ef9974aa9675cebe4907bee0134f", optional = true }
num-derive = "0.3.2"
num-traits = "0.2.12"
paste = "1.0.0"
sdl2 = { version = "=0.34.2", optional = true, features = ["unsafe_textures", "bundled", "static-link"] }
sdl2-sys = { version = "=0.34.2", optional = true, features = ["bundled", "static-link"] }
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 = "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.2", optional = true }
serde_cbor = { version = "0.11", optional = true }
serde_json = "1.0"
simple_logger = { version = "1.13" }
strum = "0.20"
strum_macros = "0.20"
tokio = { version = "1.12.0", features = ["net"], optional = true }
strum = "0.24"
strum_macros = "0.24"
# remove and replace when drain_filter is in stable
vec_mut_scan = "0.4"
webbrowser = "0.5.5"
winit = { git = "https://github.com/alula/winit.git", rev = "6acf76ff192dd8270aaa119b9f35716c03685f9f", 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"
#[build-dependencies]
#gl_generator = { version = "0.14.0", optional = true }
#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"] }
[target.'cfg(target_os = "windows")'.build-dependencies]
winres = "0.1"
[target.'cfg(target_os = "macos")'.dependencies]
objc = "0.2.7"
objc = "0.2"
[target.'cfg(target_os = "android")'.dependencies]
ndk = "0.3"
ndk-glue = "0.3"
ndk-sys = "0.2"
jni = "0.19"
ndk = "0.7"
ndk-glue = "0.7"
ndk-sys = "0.4"
jni = "0.20"
[target.'cfg(target_os = "horizon")'.dependencies]
#deko3d = { path = "./3rdparty/deko3d" }
deko3d = { git = "https://github.com/doukutsu-rs/deko3d-rs", branch = "master" }

22
LICENSE
View File

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

249
README.md
View File

@ -1,59 +1,240 @@
# doukutsu-rs
![doukutsu-rs](./res/sue_crab_banner_github.png)
![Release](https://github.com/doukutsu-rs/doukutsu-rs/workflows/Release/badge.svg)
[Download latest Nightly builds](https://github.com/doukutsu-rs/doukutsu-rs/actions) (Requires being logged in to GitHub)
A re-implementation of Cave Story (Doukutsu Monogatari) engine written in [Rust](https://www.rust-lang.org/).
**The project is still incomplete and not fully playable yet.** [Click here to see the current status of the project](https://github.com/doukutsu-rs/doukutsu-rs/issues/10).
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)
[![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](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 (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)
- [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.
> [!NOTE]
> If you get issues with Epic Games Store version, scroll down for instructions.
#### Data files
This repository does not contain any copyrighted files.
In order to work doukutsu-rs needs to be paired with supported data files. This repository does not contain any data
files.
For better user experience, pre-built binaries are distributed with slightly modified freeware game 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+).
*doukutsu-rs* should work fine with pre-extracted and tweaked data files from [this repository](https://github.com/doukutsu-rs/game-data), [NXEngine(-evo)](https://github.com/nxengine/nxengine-evo) extracted freeware data files and [Cave Story+](https://www.nicalis.com/games/cavestory+) data files.
<details>
<summary>How to set up data files on Android</summary>
If your phone has an app called **"Files"**:
Vanilla Cave Story does not work yet because some important data files have been embedded inside the executable. and we don't have a loader/extractor implemented yet.
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.
##### Where to get them?
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**
- https://github.com/doukutsu-rs/game-data - Freeware game data distributed with CI builds, based on those two below.
- ~~https://github.com/Clownacy/CSE2/archive/enhanced.zip - copy `game_english/data` from archive to the runtime directory (place you run the executable from, usually project root)~~
- https://github.com/nxengine/nxengine-evo/releases/download/v2.6.4/NXEngine-v2.6.4-Win32.zip - copy `NXEngine-evo-2.6.4-xxx/data` from the archive to runtime directory
doukutsu-rs works out of the box when it's placed in the same directory as the original Doukutsu.exe executable. On the initial
startup, doukutsu-rs will automatically extract the additional resources that are embedded in the vanilla game into the `data`
directory. Until that is done, both doukutsu-rs and the vanilla executable have to exist in the directory.
<details>
<summary>Example root directory</summary>
![example root directory with doukutsu-rs and vanilla Cave Story](https://i.imgur.com/3dJ7WMB.png)
</details>
**Cave Story+**
- SDL version (first released in 2011 on Steam)
- PC release (Steam) - Copy `data` folder from installation directory ([guide for Steam](https://steamcommunity.com/sharedfiles/filedetails/?id=760447682)) to the runtime directory.
- PC release (Epic Games Store) - Essentially the same thing as with Steam version.
- PC release (Humble Bundle) - Essentially the same thing as with Steam version.
- KAGE version (first released in 2017 on Switch)
- PC release - Sadly not there yet but Tyrone mentioned it's coming in 2021.
- Switch release - (tested only with eShop version) Extract `data` directory from romfs. Requires a hacked console and a recent and legal copy of the game. If you don't know how, look in Google how to exactly do that because the methods really differ.
doukutsu-rs can be used as drop-in replacement for `CaveStory+.exe`. No modifications to game files are needed.
#### Mandatory screenshots
**Original version (first released in 2011 on Steam)** - expand for instructions
**Freeware data files:**
<details>
<summary>Steam release (Win/Mac/Linux)</summary>
![Japanese Freeware](https://i.imgur.com/eZ0V5rK.png)
The `data` folder is in the same place across all platforms.
**Cave Story+ data files:**
If you want to use doukutsu-rs as a substitute for Mac version of Cave Story+ (which at moment of writing doesn't work
on 10.15+ anymore), do the following:
![CS+ with enhanced graphics](https://i.imgur.com/YaPAs70.png)
1. Find the doukutsu-rs executable:
- In AppVeyor builds, it's in `doukutsu-rs.app/Contents/MacOS/doukutsu-rs`
- In your own builds, it's in `target/(release|debug)/doukutsu-rs`
2. Open Steam Library, select `Cave Story+`, press the `Manage` button (gear icon) and select `Properties...`
3. Select `Local Files` and press `Browse...`.
4. Open the `Cave Story+.app` bundle and navigate to `Contents/MacOS` directory.
5. Rename the `Cave Story+` executable to something else or delete it.
6. Copy the doukutsu-rs executable and rename it to `Cave Story+`.
7. Launch the game from Steam and enjoy!
![image](https://user-images.githubusercontent.com/53099651/155904982-eb6032d8-7a4d-4af7-ae6f-b69041ecfaa4.png)
</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>
Check your default installation directory.
![image](https://user-images.githubusercontent.com/53099651/155905035-0080eace-bd98-4cf5-9628-c98334ea768c.png)
</details>
<details>
<summary>GOG</summary>
Check your default installation directory.
![image](https://user-images.githubusercontent.com/53099651/155906494-1e53f174-f12f-41be-ab53-8745cdf735b5.png)
</details>
<details>
<summary>Humble Bundle</summary>
The archive from Humble Bundle contains the necessary `data` folder, in the same folder as `CaveStory+.exe`.
![image](https://user-images.githubusercontent.com/96957561/156861929-7fa03951-442b-4277-b673-474189411103.png)
</details>
<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)
</details>
**Remastered version (first released in 2017 on Switch)**
> [!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>
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
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 run it in setup described above.
</details>
#### Controls
Same controls as the default for freeware and Cave Story+ keyboard.
To change, use the control customization menu or edit `doukutsu-rs\data\settings.json` within your user directory.
| | P1 | P2 |
| ------------------------- | --------- | --------- |
| Movement | `← ↑ ↓ →` | `, L . /` |
| Jump | `Z` | `B` |
| Shoot | `X` | `N` |
| Cycle Weapon | `A and S` | `G and H` |
| Inventory / Skip cutscene | `Q` | `T` |
| Map | `W` | `Y` |
| Strafe | `LShift` | `RShift` |
- `Alt + Enter` - Toggle Fullscreen
- `F2` (While paused) - Quick Restart
#### Screenshots
<details>
<summary>Freeware</summary>
![JP Freeware 2](https://user-images.githubusercontent.com/53099651/155924461-c63afc93-a41f-4cfd-ac9f-8f021cebcb04.png)
![Toroko Fight Freeware](https://user-images.githubusercontent.com/53099651/155924215-d492907a-ed0e-4323-bd46-61745b8fb32a.png)
![No Lighting Freeware](https://user-images.githubusercontent.com/53099651/155923814-621cf29e-bb20-4680-a96d-f049aaef1f88.png)
</details>
<details>
<summary>Original CS+</summary>
![CS+ Sand Zone](https://user-images.githubusercontent.com/53099651/155923620-db230077-0df5-4de4-b086-be6b4dcbc6df.png)
![CS+ Showoff Outer Wall](https://user-images.githubusercontent.com/53099651/155920013-3967cd03-8d69-4fc5-8f1d-fe659ff2e953.png)
![CS+ Challenge](https://user-images.githubusercontent.com/53099651/155919381-7e8159a0-a7cf-461a-8be2-2ce864631299.png)
</details>
<details>
<summary>Remastered CS+</summary>
![Balcony Switch](https://user-images.githubusercontent.com/53099651/155918810-063c0f06-2d48-485f-8367-6337525deab7.png)
![Dogs Switch](https://github.com/doukutsu-rs/doukutsu-rs/assets/6276139/30ba01ae-375d-4488-98c4-98e3e8c7f187)
![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)
</details>
#### Credits
- Studio Pixel/Nicalis for Cave Story
- [ggez](https://github.com/ggez/ggez) - we took few bits from it while moving away to SDL2.
- [@ClayHanson_](https://twitter.com/ClayHanson_) - for letting us use his .pxchar skin format from Cave Story Multiplayer mod.
- [Cave Story Tribute Site](https://cavestory.org) - has lots of useful resources related to the game.
- Studio Pixel/Nicalis for Cave Story
- [@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) - which is being used by us as `.org` playback engine.
- [@uselesscalcium](https://twitter.com/uselesscalcium) - Android port icon artwork
- [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)

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

After

Width:  |  Height:  |  Size: 178 KiB

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,7 +28,9 @@ public class DownloadActivity extends AppCompatActivity {
setContentView(R.layout.activity_download);
txtProgress = findViewById(R.id.txtProgress);
progressBar = findViewById(R.id.progressBar);
basePath = getFilesDir().getAbsolutePath() + "/data/";
ActivityUtils.hideSystemBars(this);
basePath = getFilesDir().getAbsolutePath() + "/";
downloadThread = new DownloadThread();
downloadThread.start();
@ -39,23 +43,37 @@ public class DownloadActivity extends AppCompatActivity {
}
private class DownloadThread extends Thread {
private static final String DOWNLOAD_URL = "https://github.com/doukutsu-rs/game-data/archive/refs/heads/master.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("game-data-master/")) {
entryName = entryName.substring("game-data-master/".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,119 +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>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 7.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 7.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 34 KiB

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#8DA5F8</color>
<color name="ic_launcher_background">#FD7F44</color>
</resources>

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

@ -1,7 +1,7 @@
use std::env;
// #[cfg(feature = "generate-gl")]
// use gl_generator::{Api, Fallbacks, Profile, Registry};
#[cfg(target_os = "windows")]
extern crate winres;
fn main() {
// let dest = PathBuf::from(&env::var("OUT_DIR").unwrap());
@ -9,18 +9,26 @@ fn main() {
let is_android = cfg!(target_os = "android") || (cfg!(target_os = "linux") && target.contains("android")); // hack
println!("cargo:rerun-if-changed=build.rs");
//
// #[cfg(feature = "generate-gl")]
// {
// let mut file = File::create(&dest.join("gl_bindings.rs")).unwrap();
//
// Registry::new(Api::Gles2, (3, 0), Profile::Core, Fallbacks::All, [])
// .write_bindings(gl_generator::StructGenerator, &mut file)
// .unwrap();
// }
if target.contains("macos") {
println!("cargo:rustc-env=MACOSX_DEPLOYMENT_TARGET=10.12")
#[cfg(target_os = "windows")]
{
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") {
println!("cargo:rustc-env=MACOSX_DEPLOYMENT_TARGET=10.15");
println!("cargo:rustc-link-arg=-weak_framework");
println!("cargo:rustc-link-arg=GameController");
println!("cargo:rustc-link-arg=-weak_framework");
println!("cargo:rustc-link-arg=CoreHaptics");
}
if is_android {

View File

@ -1,14 +1,20 @@
[package]
name = "drsandroid"
description = "doukutsu-rs targeted for Android"
version = "0.1.0"
authors = ["Alula"]
edition = "2018"
edition = "2021"
[profile.release]
opt-level = 3
lto = "off"
codegen-units = 256
incremental = true
[lib]
crate-type = ["cdylib"]
[dependencies]
ndk = "0.3"
ndk-glue = "0.3"
ndk-sys = "0.2"
doukutsu-rs = { path = "../", default-features = false, features = ["default-base", "backend-glutin"] }
ndk = "0.7"
ndk-glue = "0.7"
ndk-sys = "0.4"
doukutsu-rs = { path = "../", default-features = false, features = ["default-base", "backend-glutin", "webbrowser"] }

View File

@ -1,7 +1,11 @@
#[cfg(target_os = "android")]
#[cfg_attr(target_os = "android", ndk_glue::main())]
pub fn android_main() {
let options = doukutsu_rs::LaunchOptions { server_mode: false };
let resource_dir = std::path::PathBuf::from(ndk_glue::native_activity().internal_data_path().to_string_lossy().to_string());
doukutsu_rs::init(options).unwrap();
std::env::set_current_dir(&resource_dir).unwrap();
let options = doukutsu_rs::game::LaunchOptions { server_mode: false, editor: false };
doukutsu_rs::game::init(options).unwrap();
}

9
drshorizon/.cargo/config Normal file
View File

@ -0,0 +1,9 @@
[build]
target = ["aarch64-nintendo-switch"]
[target.aarch64-nintendo-switch]
cc = {path = "/opt/devkitpro/devkitA64/bin/aarch64-none-elf-gcc"}
cxx = {path = "/opt/devkitpro/devkitA64/bin/aarch64-none-elf-g++"}
ar = "/opt/devkitpro/devkitA64/bin/aarch64-none-elf-ar"
ranlib = {path = "/opt/devkitpro/devkitA64/bin/aarch64-none-elf-ranlib"}
linker = "/opt/devkitpro/devkitA64/bin/aarch64-none-elf-gcc"

31
drshorizon/Cargo.toml Normal file
View File

@ -0,0 +1,31 @@
[package]
name = "drshorizon"
description = "doukutsu-rs targeted for Nintendo Switch"
version = "0.1.0"
edition = "2021"
[profile.release]
opt-level = 3
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]
doukutsu-rs = { path = "../", default-features = false, features = ["default-base", "backend-horizon"] }

3
drshorizon/README.md Normal file
View File

@ -0,0 +1,3 @@
Experimental.
ld script and .specs taken from devkitPro

View File

@ -0,0 +1,45 @@
{
"arch": "aarch64",
"data-layout": "e-m:e-i8:8:32-i16:16:32-i64:64-i128:128-n32:64-S128",
"dynamic-linking": true,
"disable-redzone": true,
"env": "newlib",
"executables": true,
"exe-suffix": ".elf",
"features": "+a57,+strict-align,+crc,+crypto",
"has-rpath": false,
"has-thread-local": false,
"linker": "/opt/devkitpro/devkitA64/bin/aarch64-none-elf-gcc",
"linker-flavor": "gcc",
"llvm-target": "aarch64-unknown-none",
"max-atomic-width": 128,
"no-default-libraries": false,
"os": "horizon",
"panic-strategy": "abort",
"position-independent-executables": true,
"pre-link-args": {
"gcc": [
"-fPIC",
"-specs",
"aarch64_nintendo_switch.specs",
"-T",
"aarch64_nintendo_switch.ld",
"-L",
"/opt/devkitpro/portlibs/switch/lib",
"-L",
"/opt/devkitpro/libnx/lib",
"-I",
"/opt/devkitpro/libnx/include"
]
},
"relocation-model": "pic",
"requires-uwtable": true,
"target-c-int-width": "32",
"target-endian": "little",
"target-family": [
"unix"
],
"target-pointer-width": "64",
"trap-unreachable": true,
"vendor": "nintendo"
}

View File

@ -0,0 +1,200 @@
OUTPUT_ARCH(aarch64)
ENTRY(_start)
PHDRS
{
code PT_LOAD FLAGS(5) /* Read | Execute */;
rodata PT_LOAD FLAGS(4) /* Read */;
data PT_LOAD FLAGS(6) /* Read | Write */;
dyn PT_DYNAMIC;
}
SECTIONS
{
/* =========== CODE section =========== */
PROVIDE(__start__ = 0x0);
. = __start__;
__code_start = . ;
.text :
{
HIDDEN(__text_start = .);
KEEP (*(.crt0))
*(.text.unlikely .text.*_unlikely .text.unlikely.*)
*(.text.exit .text.exit.*)
*(.text.startup .text.startup.*)
*(.text.hot .text.hot.*)
*(.text .stub .text.* .gnu.linkonce.t.*)
. = ALIGN(8);
} :code
.init :
{
KEEP( *(.init) )
. = ALIGN(8);
} :code
.plt :
{
*(.plt)
*(.iplt)
. = ALIGN(8);
} :code
.fini :
{
KEEP( *(.fini) )
. = ALIGN(8);
} :code
/* =========== RODATA section =========== */
. = ALIGN(0x1000);
__rodata_start = . ;
.nx-module-name : { KEEP (*(.nx-module-name)) } :rodata
.rodata :
{
*(.rodata .rodata.* .gnu.linkonce.r.*)
. = ALIGN(8);
} :rodata
.eh_frame_hdr : { __eh_frame_hdr_start = .; *(.eh_frame_hdr) *(.eh_frame_entry .eh_frame_entry.*) __eh_frame_hdr_end = .; } :rodata
.eh_frame : ONLY_IF_RO { KEEP (*(.eh_frame)) *(.eh_frame.*) } :rodata
.gcc_except_table : ONLY_IF_RO { *(.gcc_except_table .gcc_except_table.*) } :rodata
.gnu_extab : ONLY_IF_RO { *(.gnu_extab*) } : rodata
HIDDEN(__dynamic_start = .);
.dynamic : { *(.dynamic) } :rodata :dyn
.dynsym : { *(.dynsym) } :rodata
.dynstr : { *(.dynstr) } :rodata
.rela.dyn : { *(.rela.*) } :rodata
.interp : { *(.interp) } :rodata
.hash : { *(.hash) } :rodata
.gnu.hash : { *(.gnu.hash) } :rodata
.gnu.version : { *(.gnu.version) } :rodata
.gnu.version_d : { *(.gnu.version_d) } :rodata
.gnu.version_r : { *(.gnu.version_r) } :rodata
.note.gnu.build-id : { *(.note.gnu.build-id) } :rodata
/* =========== DATA section =========== */
. = ALIGN(0x1000);
__data_start = . ;
.eh_frame : ONLY_IF_RW { KEEP (*(.eh_frame)) *(.eh_frame.*) } :data
.gcc_except_table : ONLY_IF_RW { *(.gcc_except_table .gcc_except_table.*) } :data
.gnu_extab : ONLY_IF_RW { *(.gnu_extab*) } : data
.exception_ranges : ONLY_IF_RW { *(.exception_ranges .exception_ranges*) } :data
.tdata ALIGN(8) :
{
__tdata_lma = .;
*(.tdata .tdata.* .gnu.linkonce.td.*)
. = ALIGN(8);
__tdata_lma_end = .;
} :data
.tbss ALIGN(8) :
{
*(.tbss .tbss.* .gnu.linkonce.tb.*) *(.tcommon)
. = ALIGN(8);
} :data
.preinit_array ALIGN(8) :
{
PROVIDE (__preinit_array_start = .);
KEEP (*(.preinit_array))
PROVIDE (__preinit_array_end = .);
} :data
.init_array ALIGN(8) :
{
PROVIDE (__init_array_start = .);
KEEP( *(SORT_BY_INIT_PRIORITY(.init_array.*) SORT_BY_INIT_PRIORITY(.ctors.*)) )
KEEP( *(.init_array .ctors) )
PROVIDE (__init_array_end = .);
} :data
.fini_array ALIGN(8) :
{
PROVIDE (__fini_array_start = .);
KEEP( *(SORT_BY_INIT_PRIORITY(.fini_array.*) SORT_BY_INIT_PRIORITY(.dtors.*)) )
KEEP( *(.fini_array .dtors) )
PROVIDE (__fini_array_end = .);
} :data
__got_start__ = .;
.got : { *(.got) *(.igot) } :data
.got.plt : { *(.got.plt) *(.igot.plt) } :data
__got_end__ = .;
.data ALIGN(8) :
{
*(.data .data.* .gnu.linkonce.d.*)
SORT(CONSTRUCTORS)
} :data
__bss_start__ = .;
.bss ALIGN(8) :
{
HIDDEN(__bss_start = .);
*(.dynbss)
*(.bss .bss.* .gnu.linkonce.b.*)
*(COMMON)
. = ALIGN(8);
/* Reserve space for the TLS segment of the main thread */
__tls_start = .;
. += + SIZEOF(.tdata) + SIZEOF(.tbss);
__tls_end = .;
HIDDEN(__bss_end = .);
} :data
__bss_end__ = .;
__end__ = ABSOLUTE(.) ;
. = ALIGN(0x1000);
__argdata__ = ABSOLUTE(.) ;
/* ==================
==== Metadata ====
================== */
/* Discard sections that difficult post-processing */
/DISCARD/ : { *(.group .comment .note) }
/* Stabs debugging sections. */
.stab 0 : { *(.stab) }
.stabstr 0 : { *(.stabstr) }
.stab.excl 0 : { *(.stab.excl) }
.stab.exclstr 0 : { *(.stab.exclstr) }
.stab.index 0 : { *(.stab.index) }
.stab.indexstr 0 : { *(.stab.indexstr) }
/* DWARF debug sections.
Symbols in the DWARF debugging sections are relative to the beginning
of the section so we begin them at 0. */
/* DWARF 1 */
.debug 0 : { *(.debug) }
.line 0 : { *(.line) }
/* GNU DWARF 1 extensions */
.debug_srcinfo 0 : { *(.debug_srcinfo) }
.debug_sfnames 0 : { *(.debug_sfnames) }
/* DWARF 1.1 and DWARF 2 */
.debug_aranges 0 : { *(.debug_aranges) }
.debug_pubnames 0 : { *(.debug_pubnames) }
/* DWARF 2 */
.debug_info 0 : { *(.debug_info) }
.debug_abbrev 0 : { *(.debug_abbrev) }
.debug_line 0 : { *(.debug_line) }
.debug_frame 0 : { *(.debug_frame) }
.debug_str 0 : { *(.debug_str) }
.debug_loc 0 : { *(.debug_loc) }
.debug_macinfo 0 : { *(.debug_macinfo) }
}

View File

@ -0,0 +1,8 @@
%rename link old_link
*link:
%(old_link) -pie --no-dynamic-linker --spare-dynamic-tags=0 -z text -z nodynamic-undefined-weak --build-id=sha1 --nx-module-name
*startfile:
crti%O%s crtbegin%O%s --require-defined=main

5
drshorizon/build.rs Normal file
View File

@ -0,0 +1,5 @@
fn main() {
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rustc-link-lib=dylib=nx");
}

33
drshorizon/build_debug.sh Executable file
View File

@ -0,0 +1,33 @@
#!/bin/bash
cd "$(dirname "$0")" || exit
set -e
DARK_GRAY=$(tput setaf 8)
YELLOW=$(tput bold)$(tput setaf 3)
RESET=$(tput sgr0)
function message() {
echo "${DARK_GRAY}----${RESET} ${YELLOW}$*${RESET}"
}
message "Compiling shaders..."
uam -s vert -o ../src/framework/shaders/deko3d/vertex_basic.dksh ../src/framework/shaders/deko3d/vertex_basic.glsl
uam -s frag -o ../src/framework/shaders/deko3d/fragment_textured.dksh ../src/framework/shaders/deko3d/fragment_textured.glsl
uam -s frag -o ../src/framework/shaders/deko3d/fragment_color.dksh ../src/framework/shaders/deko3d/fragment_color.glsl
message "Building crate..."
rustup run rust-switch cargo build -Z build-std=core,alloc,std,panic_abort --target aarch64-nintendo-switch.json
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.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 \
--icon=../res/nx_icon.jpg \
--nacp=target/aarch64-nintendo-switch/debug/drshorizon.nacp
message "done!"

33
drshorizon/build_release.sh Executable file
View File

@ -0,0 +1,33 @@
#!/bin/bash
cd "$(dirname "$0")" || exit
set -e
DARK_GRAY=$(tput setaf 8)
YELLOW=$(tput bold)$(tput setaf 3)
RESET=$(tput sgr0)
function message() {
echo "${DARK_GRAY}----${RESET} ${YELLOW}$*${RESET}"
}
message "Compiling shaders..."
uam -s vert -o ../src/framework/shaders/deko3d/vertex_basic.dksh ../src/framework/shaders/deko3d/vertex_basic.glsl
uam -s frag -o ../src/framework/shaders/deko3d/fragment_textured.dksh ../src/framework/shaders/deko3d/fragment_textured.glsl
uam -s frag -o ../src/framework/shaders/deko3d/fragment_color.dksh ../src/framework/shaders/deko3d/fragment_color.glsl
message "Building crate..."
rustup run rust-switch cargo build -Z build-std=core,alloc,std,panic_abort --target aarch64-nintendo-switch.json --release
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.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 \
--icon=../res/nx_icon.jpg \
--nacp=target/aarch64-nintendo-switch/release/drshorizon.nacp
message "done!"

47
drshorizon/src/main.rs Normal file
View File

@ -0,0 +1,47 @@
//#![feature(restricted_std)]
#[repr(C)]
pub struct PrintConsole {}
#[repr(C)]
#[derive(Copy, Clone, Eq, PartialEq)]
pub enum ApmCpuBoostMode {
Normal = 0,
FastLoad = 1,
}
extern "C" {
fn consoleInit(unk: *mut PrintConsole) -> *mut PrintConsole;
fn consoleUpdate(unk: *mut PrintConsole);
fn socketInitialize(unk: *const std::ffi::c_void) -> u32;
fn nxlinkConnectToHost(redir_stdout: bool, redir_stderr: bool) -> i32;
fn appletSetCpuBoostMode(mode: ApmCpuBoostMode) -> u32;
static __text_start: u32;
}
fn main() {
unsafe {
// if socketInitialize(std::ptr::null()) == 0 {
// nxlinkConnectToHost(true, true);
// }
// appletSetCpuBoostMode(ApmCpuBoostMode::FastLoad);
std::env::set_var("RUST_BACKTRACE", "full");
println!("__text_start = {:#x}", (&__text_start) as *const _ as usize);
let options = doukutsu_rs::game::LaunchOptions { server_mode: false, editor: false };
let result = doukutsu_rs::game::init(options);
if let Err(e) = result {
println!("Initialization error: {}", e);
loop {
std::thread::sleep(std::time::Duration::from_millis(100));
}
}
}
}

51
drshorizon/symbolize.js Normal file
View File

@ -0,0 +1,51 @@
// <reference types="node" />
const readline = require('readline');
const childProcess = require('child_process');
const rl = readline.createInterface({
input: process.stdin,
terminal: false
});
let textStart = 0;
const textStartRegex = /__text_start = 0x([0-9a-f]+)/i;
let symbolize = false;
if (process.argv.length <= 2) {
console.error('Usage: node symbolize.js <path to ELF file>');
process.exit(1);
}
const elfPath = process.argv[2];
rl.on('line', (line) => {
if (textStart === 0) {
const match = textStartRegex.exec(line);
if (match) {
textStart = parseInt(match[1], 16);
}
}
if (line.includes("stack backtrace:")) {
symbolize = true;
}
if (symbolize) {
const match = /0x([0-9a-f]+) - \<unknown\>/.exec(line);
if (match) {
const addr = parseInt(match[1], 16);
const relative = addr - textStart;
// run addr2line on the address
const addr2line = childProcess.spawnSync('addr2line', ['-e', elfPath, '-j', '.text', '-f', '-C', '0x' + relative.toString(16)]);
if (addr2line.status === 0) {
const output = addr2line.stdout.toString();
const lines = output.split('\n');
const [func, file] = lines;
line = line.replace(match[0], `0x${addr.toString(16)} - ${func} (${file})`);
}
}
}
console.log(line);
});

View File

@ -0,0 +1,33 @@
{
"$schema": "https://json-schema.org/draft/2019-09/schema",
"title": "JSON schema for texture sizes",
"type": "object",
"properties": {
"sizes": {
"description": "List of textures and their sizes to prevent accidental scaling being applied",
"type": "object",
"patternProperties": {
"^.*$": {
"description": "Location and Width/Height of texture",
"type": "array",
"minItems": 2,
"maxItems": 2,
"items": [
{
"type": "number",
"minimum": 0,
"maximum": 65535
},
{
"type": "number",
"minimum": 0,
"maximum": 65535
}
]
}
},
"additionalProperties": false
}
},
"additionalProperties": false
}

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/nx_icon.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

BIN
res/nx_icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

BIN
res/sue.bmp Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

BIN
res/sue.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 764 KiB

Binary file not shown.

View File

@ -1,4 +1,4 @@
edition = "2018"
edition = "2021"
max_width = 120
use_small_heuristics = "Max"
newline_style = "Unix"

View File

@ -1,94 +0,0 @@
use std::collections::HashMap;
use std::io;
use byteorder::{ReadBytesExt, LE};
use crate::framework::error::GameError::ResourceLoadError;
use crate::framework::error::GameResult;
#[derive(Debug)]
pub struct BmChar {
pub x: u16,
pub y: u16,
pub width: u16,
pub height: u16,
pub xoffset: i16,
pub yoffset: i16,
pub xadvance: i16,
pub page: u8,
pub chnl: u8,
}
#[derive(Debug)]
pub struct BMFont {
pub pages: u16,
pub font_size: i16,
pub line_height: u16,
pub base: u16,
pub chars: HashMap<char, BmChar>,
}
const MAGIC: [u8; 4] = [b'B', b'M', b'F', 3];
impl BMFont {
pub fn load_from<R: io::Read + io::Seek>(mut data: R) -> GameResult<Self> {
let mut magic = [0u8; 4];
let mut pages = 0u16;
let mut chars = HashMap::with_capacity(128);
let mut font_size = 0i16;
let mut line_height = 0u16;
let mut base = 0u16;
data.read_exact(&mut magic)?;
if magic != MAGIC {
return Err(ResourceLoadError("Invalid magic".to_owned()));
}
while let Ok(block_type) = data.read_u8() {
let length = data.read_u32::<LE>()?;
match block_type {
1 => {
font_size = data.read_i16::<LE>()?;
data.seek(io::SeekFrom::Current(length as i64 - 2))?;
}
2 => {
line_height = data.read_u16::<LE>()?;
base = data.read_u16::<LE>()?;
data.seek(io::SeekFrom::Current(4))?;
pages = data.read_u16::<LE>()?;
data.seek(io::SeekFrom::Current(length as i64 - 10))?;
}
3 | 5 => {
data.seek(io::SeekFrom::Current(length as i64))?;
}
4 => {
let count = length / 20;
for _ in 0..count {
let id = data.read_u32::<LE>()?;
let x = data.read_u16::<LE>()?;
let y = data.read_u16::<LE>()?;
let width = data.read_u16::<LE>()?;
let height = data.read_u16::<LE>()?;
let xoffset = data.read_i16::<LE>()?;
let yoffset = data.read_i16::<LE>()?;
let xadvance = data.read_i16::<LE>()?;
let page = data.read_u8()?;
let chnl = data.read_u8()?;
if let Some(chr) = std::char::from_u32(id) {
chars.insert(chr, BmChar { x, y, width, height, xoffset, yoffset, xadvance, page, chnl });
}
}
}
_ => {
return Err(ResourceLoadError("Unknown block type.".to_owned()));
}
}
}
Ok(Self { pages, font_size, line_height, base, chars })
}
}

View File

@ -1,205 +0,0 @@
use std::collections::HashSet;
use std::path::PathBuf;
use crate::bmfont::BMFont;
use crate::common::{Rect, FILE_TYPES};
use crate::engine_constants::EngineConstants;
use crate::framework::context::Context;
use crate::framework::error::GameError::ResourceLoadError;
use crate::framework::error::GameResult;
use crate::framework::filesystem;
use crate::texture_set::TextureSet;
pub struct BMFontRenderer {
font: BMFont,
pages: Vec<String>,
}
impl BMFontRenderer {
pub fn load(root: &str, desc_path: &str, ctx: &mut Context) -> GameResult<BMFontRenderer> {
let root = PathBuf::from(root);
let full_path = &root.join(PathBuf::from(desc_path));
let desc_stem =
full_path.file_stem().ok_or_else(|| ResourceLoadError("Cannot extract the file stem.".to_owned()))?;
let stem = full_path.parent().unwrap_or(full_path).join(desc_stem);
let font = BMFont::load_from(filesystem::open(ctx, &full_path)?)?;
let mut pages = Vec::new();
let (zeros, _, _) = FILE_TYPES
.iter()
.map(|ext| (1, ext, format!("{}_0{}", stem.to_string_lossy(), ext)))
.find(|(_, _, path)| filesystem::exists(ctx, &path))
.or_else(|| {
FILE_TYPES
.iter()
.map(|ext| (2, ext, format!("{}_00{}", stem.to_string_lossy(), ext)))
.find(|(_, _, path)| filesystem::exists(ctx, &path))
})
.ok_or_else(|| ResourceLoadError(format!("Cannot find glyph atlas 0 for font: {:?}", desc_path)))?;
for i in 0..font.pages {
let page_path = format!("{}_{:02$}", stem.to_string_lossy(), i, zeros);
pages.push(page_path);
}
Ok(Self { font, pages })
}
pub fn line_height(&self, constants: &EngineConstants) -> f32 {
self.font.line_height as f32 * constants.font_scale
}
pub fn text_width<I: Iterator<Item = char>>(&self, iter: I, constants: &EngineConstants) -> f32 {
let mut offset_x = 0.0;
for chr in iter {
if let Some(glyph) = self.font.chars.get(&chr) {
offset_x += ((glyph.width as f32 + glyph.xoffset as f32) * constants.font_scale).floor()
+ if chr != ' ' { 1.0 } else { constants.font_space_offset };
}
}
offset_x
}
pub fn draw_text<I: Iterator<Item = char>>(
&self,
iter: I,
x: f32,
y: f32,
constants: &EngineConstants,
texture_set: &mut TextureSet,
ctx: &mut Context,
) -> GameResult {
self.draw_colored_text(iter, x, y, (255, 255, 255, 255), constants, texture_set, ctx)
}
pub fn draw_text_with_shadow<I: Iterator<Item = char> + Clone>(
&self,
iter: I,
x: f32,
y: f32,
constants: &EngineConstants,
texture_set: &mut TextureSet,
ctx: &mut Context,
) -> GameResult {
self.draw_colored_text(iter.clone(), x + 1.0, y + 1.0, (0, 0, 0, 150), constants, texture_set, ctx)?;
self.draw_colored_text(iter, x, y, (255, 255, 255, 255), constants, texture_set, ctx)
}
pub fn draw_colored_text_with_shadow_scaled<I: Iterator<Item = char> + Clone>(
&self,
iter: I,
x: f32,
y: f32,
scale: f32,
color: (u8, u8, u8, u8),
constants: &EngineConstants,
texture_set: &mut TextureSet,
ctx: &mut Context,
) -> GameResult {
self.draw_colored_text_scaled(
iter.clone(),
x + scale,
y + scale,
scale,
(0, 0, 0, 150),
constants,
texture_set,
ctx,
)?;
self.draw_colored_text_scaled(iter, x, y, scale, color, constants, texture_set, ctx)
}
pub fn draw_colored_text_scaled<I: Iterator<Item = char>>(
&self,
iter: I,
x: f32,
y: f32,
scale: f32,
color: (u8, u8, u8, u8),
constants: &EngineConstants,
texture_set: &mut TextureSet,
ctx: &mut Context,
) -> GameResult {
if self.pages.len() == 1 {
let batch = texture_set.get_or_load_batch(ctx, constants, self.pages.get(0).unwrap())?;
let mut offset_x = x;
for chr in iter {
if let Some(glyph) = self.font.chars.get(&chr) {
batch.add_rect_scaled_tinted(
offset_x,
y + (glyph.yoffset as f32 * constants.font_scale).floor(),
color,
constants.font_scale,
constants.font_scale,
&Rect::new_size(glyph.x as u16, glyph.y as u16, glyph.width as u16, glyph.height as u16),
);
offset_x += ((glyph.width as f32 + glyph.xoffset as f32) * constants.font_scale).floor()
+ if chr != ' ' { 1.0 } else { constants.font_space_offset };
}
}
batch.draw(ctx)?;
} else {
let mut pages = HashSet::new();
let mut chars = Vec::new();
for chr in iter {
if let Some(glyph) = self.font.chars.get(&chr) {
pages.insert(glyph.page);
chars.push((chr, glyph));
}
}
for page in pages {
let page_tex = if let Some(p) = self.pages.get(page as usize) {
p
} else {
continue;
};
let batch = texture_set.get_or_load_batch(ctx, constants, page_tex)?;
let mut offset_x = x;
for (chr, glyph) in chars.iter() {
if glyph.page == page {
batch.add_rect_scaled_tinted(
offset_x,
y + (glyph.yoffset as f32 * constants.font_scale).floor(),
color,
constants.font_scale * scale,
constants.font_scale * scale,
&Rect::new_size(glyph.x as u16, glyph.y as u16, glyph.width as u16, glyph.height as u16),
);
}
offset_x += scale
* (((glyph.width as f32 + glyph.xoffset as f32) * constants.font_scale).floor()
+ if *chr != ' ' { 1.0 } else { constants.font_space_offset });
}
batch.draw(ctx)?;
}
}
Ok(())
}
pub fn draw_colored_text<I: Iterator<Item = char>>(
&self,
iter: I,
x: f32,
y: f32,
color: (u8, u8, u8, u8),
constants: &EngineConstants,
texture_set: &mut TextureSet,
ctx: &mut Context,
) -> GameResult {
self.draw_colored_text_scaled(iter, x, y, 1.0, color, constants, texture_set, ctx)
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.0 KiB

View File

@ -1,8 +0,0 @@
#version 330
varying vec4 color;
varying vec4 frag_color;
void main() {
frag_color = color;
}

View File

@ -1,28 +0,0 @@
#version 150 core
uniform sampler2D t_Texture;
in vec2 v_Uv;
in vec4 v_Color;
out vec4 Target0;
layout (std140) uniform Globals {
mat4 u_MVP;
};
layout (std140) uniform WaterShaderParams {
vec2 u_Resolution;
vec2 u_FramePos;
float u_Tick;
};
void main() {
vec2 wave = v_Uv;
wave.x += sin((-u_FramePos.y / u_Resolution.y + v_Uv.x * 16.0) + u_Tick / 20.0) * 2.0 / u_Resolution.x;
wave.y -= cos((-u_FramePos.x / u_Resolution.x + v_Uv.y * 16.0) + u_Tick / 5.0) * 2.0 / u_Resolution.y;
float off = 0.4 / u_Resolution.y;
vec4 color = texture(t_Texture, wave);
color.r = texture(t_Texture, wave + off).r;
color.b = texture(t_Texture, wave - off).b;
Target0 = (vec4(0.4, 0.6, 0.8, 1.0) * 0.3) + (color * v_Color * 0.7);
}

View File

@ -1,54 +0,0 @@
#version 300 es
precision mediump float;
uniform sampler2D t_Texture;
in vec2 v_Uv;
in vec4 v_Color;
out vec4 Target0;
layout (std140) uniform Globals {
mat4 u_MVP;
};
layout (std140) uniform WaterShaderParams {
vec2 u_Resolution;
vec2 u_FramePos;
float u_Tick;
};
void main() {
vec2 wave = v_Uv;
wave.x += sin((-u_FramePos.y / u_Resolution.y + v_Uv.x * 16.0) + u_Tick / 20.0) * 2.0 / u_Resolution.x;
wave.y -= cos((-u_FramePos.x / u_Resolution.x + v_Uv.y * 16.0) + u_Tick / 5.0) * 2.0 / u_Resolution.y;
float off = 0.4 / u_Resolution.y;
vec4 color = texture(t_Texture, wave);
color.r = texture(t_Texture, wave + off).r;
color.b = texture(t_Texture, wave - off).b;
Target0 = (vec4(0.4, 0.6, 0.8, 1.0) * 0.3) + (color * v_Color * 0.7);
}
/*
precision mediump float;
uniform sampler2D t_Texture;
varying vec2 v_Uv;
varying vec4 v_Color;
uniform mat4 u_MVP;
uniform vec2 u_Resolution;
uniform vec2 u_FramePos;
uniform float u_Tick;
void main() {
vec2 wave = v_Uv;
wave.x += sin((-u_FramePos.y / u_Resolution.y + v_Uv.x * 16.0) + u_Tick / 20.0) * 2.0 / u_Resolution.x;
wave.y -= cos((-u_FramePos.x / u_Resolution.x + v_Uv.y * 16.0) + u_Tick / 5.0) * 2.0 / u_Resolution.y;
float off = 0.4 / u_Resolution.y;
vec4 color = texture2D(t_Texture, wave);
color.r = texture2D(t_Texture, wave + off).r;
color.b = texture2D(t_Texture, wave - off).b;
gl_FragColor = (vec4(0.4, 0.6, 0.8, 1.0) * 0.3) + (color * v_Color * 0.7);
}*/

Binary file not shown.

Before

Width:  |  Height:  |  Size: 789 B

View File

@ -1,254 +0,0 @@
use std::{fmt, io};
use std::fmt::Debug;
use std::io::Cursor;
use std::io::ErrorKind;
use std::io::SeekFrom;
use std::path::{Component, Path, PathBuf};
use crate::framework::error::GameError::FilesystemError;
use crate::framework::error::GameResult;
use crate::framework::vfs::{OpenOptions, VFile, VMetadata, VFS};
#[derive(Debug)]
pub struct BuiltinFile(Cursor<&'static [u8]>);
impl BuiltinFile {
pub fn from(buf: &'static [u8]) -> Box<dyn VFile> {
Box::new(BuiltinFile(Cursor::new(buf)))
}
}
impl io::Read for BuiltinFile {
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
self.0.read(buf)
}
}
impl io::Seek for BuiltinFile {
fn seek(&mut self, pos: SeekFrom) -> io::Result<u64> {
self.0.seek(pos)
}
}
impl io::Write for BuiltinFile {
fn write(&mut self, _buf: &[u8]) -> io::Result<usize> {
Err(io::Error::new(ErrorKind::PermissionDenied, "Built-in file system is read-only."))
}
fn flush(&mut self) -> io::Result<()> {
Err(io::Error::new(ErrorKind::PermissionDenied, "Built-in file system is read-only."))
}
}
struct BuiltinMetadata {
is_dir: bool,
size: u64,
}
impl VMetadata for BuiltinMetadata {
fn is_dir(&self) -> bool {
self.is_dir
}
fn is_file(&self) -> bool {
!self.is_dir
}
fn len(&self) -> u64 {
self.size
}
}
#[derive(Clone, Debug)]
enum FSNode {
File(&'static str, &'static [u8]),
Directory(&'static str, Vec<FSNode>),
}
impl FSNode {
fn get_name(&self) -> &'static str {
match self {
FSNode::File(name, _) => { name }
FSNode::Directory(name, _) => { name }
}
}
fn to_file(&self) -> GameResult<Box<dyn VFile>> {
match self {
FSNode::File(_, buf) => { Ok(BuiltinFile::from(buf)) }
FSNode::Directory(name, _) => { Err(FilesystemError(format!("{} is a directory.", name))) }
}
}
fn to_metadata(&self) -> Box<dyn VMetadata> {
match self {
FSNode::File(_, buf) => {
Box::new(BuiltinMetadata {
is_dir: false,
size: buf.len() as u64,
})
}
FSNode::Directory(_, _) => {
Box::new(BuiltinMetadata {
is_dir: true,
size: 0,
})
}
}
}
}
pub struct BuiltinFS {
root: Vec<FSNode>,
}
impl BuiltinFS {
pub fn new() -> Self {
Self {
root: vec![
FSNode::Directory("builtin", vec![
FSNode::File("builtin_font.fnt", include_bytes!("builtin/builtin_font.fnt")),
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("organya-wavetable-doukutsu.bin", include_bytes!("builtin/organya-wavetable-doukutsu.bin")),
FSNode::File("touch.png", include_bytes!("builtin/touch.png")),
FSNode::Directory("shaders", vec![
// FSNode::File("basic_150.vert.glsl", include_bytes!("builtin/shaders/basic_150.vert.glsl")),
// FSNode::File("water_150.frag.glsl", include_bytes!("builtin/shaders/water_150.frag.glsl")),
// FSNode::File("basic_es300.vert.glsl", include_bytes!("builtin/shaders/basic_es300.vert.glsl")),
// FSNode::File("water_es300.frag.glsl", include_bytes!("builtin/shaders/water_es300.frag.glsl")),
]),
FSNode::Directory("lightmap", vec![
FSNode::File("spot.png", include_bytes!("builtin/lightmap/spot.png")),
]),
])
],
}
}
fn get_node(&self, path: &Path) -> GameResult<FSNode> {
let mut iter = path.components().peekable();
if let Some(Component::RootDir) = iter.next() {
let mut curr_dir = &self.root;
if iter.peek().is_none() {
return Ok(FSNode::Directory("", self.root.clone()));
}
while let Some(comp) = iter.next() {
let comp_name = comp.as_os_str().to_string_lossy();
for file in curr_dir {
match file {
FSNode::File(name, _) if comp_name.eq(name) => {
return if iter.peek().is_some() {
Err(FilesystemError(format!("Expected a directory, found a file: {:?}", path)))
} else {
Ok(file.clone())
};
}
FSNode::Directory(name, contents) if comp_name.eq(name) => {
if iter.peek().is_some() {
curr_dir = contents;
break;
} else {
return Ok(file.clone());
}
}
_ => {}
}
}
}
} else {
return Err(FilesystemError("Path must be absolute.".to_string()));
}
Err(FilesystemError("File not found.".to_string()))
}
}
impl Debug for BuiltinFS {
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
write!(f, "<BuiltinFS>")
}
}
impl VFS for BuiltinFS {
fn open_options(&self, path: &Path, open_options: OpenOptions) -> GameResult<Box<dyn VFile>> {
if open_options.write || open_options.create || open_options.append || open_options.truncate {
let msg = format!(
"Cannot alter file {:?} in root {:?}, filesystem read-only",
path, self
);
return Err(FilesystemError(msg));
}
self.get_node(path)?.to_file()
}
fn mkdir(&self, _path: &Path) -> GameResult<()> {
Err(FilesystemError("Tried to make directory {} but FS is read-only".to_string()))
}
fn rm(&self, _path: &Path) -> GameResult<()> {
Err(FilesystemError("Tried to remove file {} but FS is read-only".to_string()))
}
fn rmrf(&self, _path: &Path) -> GameResult<()> {
Err(FilesystemError("Tried to remove file/dir {} but FS is read-only".to_string()))
}
fn exists(&self, path: &Path) -> bool {
self.get_node(path).is_ok()
}
fn metadata(&self, path: &Path) -> GameResult<Box<dyn VMetadata>> {
self.get_node(path).map(|v| v.to_metadata())
}
fn read_dir(&self, path: &Path) -> GameResult<Box<dyn Iterator<Item=GameResult<PathBuf>>>> {
match self.get_node(path) {
Ok(FSNode::Directory(_, contents)) => {
let mut vec = Vec::new();
for node in contents {
vec.push(Ok(PathBuf::from(node.get_name())))
}
Ok(Box::new(vec.into_iter()))
}
Ok(FSNode::File(_, _)) => {
Err(FilesystemError(format!("Expected a directory, found a file: {:?}", path)))
}
Err(e) => {
Err(e)
}
}
}
fn to_path_buf(&self) -> Option<PathBuf> {
None
}
}
#[test]
fn test_builtin_fs() {
let fs = BuiltinFS {
root: vec![
FSNode::File("test.txt", &[]),
FSNode::Directory("memes", vec![
FSNode::File("nothing.txt", &[]),
FSNode::Directory("secret stuff", vec![
FSNode::File("passwords.txt", b"12345678"),
]),
]),
FSNode::File("test2.txt", &[]),
],
};
println!("{:?}", fs.get_node(Path::new("/")).unwrap());
println!("{:?}", fs.get_node(Path::new("/test.txt")).unwrap());
println!("{:?}", fs.get_node(Path::new("/memes")).unwrap());
println!("{:?}", fs.get_node(Path::new("/memes/nothing.txt")).unwrap());
println!("{:?}", fs.get_node(Path::new("/memes/secret stuff/passwords.txt")).unwrap());
}

View File

@ -1,13 +1,14 @@
use std::fmt;
use std::time::{SystemTime, UNIX_EPOCH};
use lazy_static::lazy_static;
use num_traits::{abs, Num};
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
use serde::de::{SeqAccess, Visitor};
use serde::ser::SerializeTupleStruct;
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
use crate::bitfield;
use crate::texture_set::G_MAG;
use crate::graphics::texture_set::G_MAG;
/// Multiply cave story degrees (0-255, which corresponds to 0°-360°) with this constant to get
/// respective value in radians.
@ -38,15 +39,15 @@ bitfield! {
pub hit_right_slope, set_hit_right_slope: 4;
/// Set if entity stays on left slope. (corresponds to flag & 0x20)
pub hit_left_slope, set_hit_left_slope: 5;
/// Unknown purpose (corresponds to flag & 0x40)
pub flag_x40, set_flag_x40: 6;
/// Unknown purpose (corresponds to flag & 0x80)
pub flag_x80, set_flag_x80: 7;
/// Used only in bullet code, set if a bullet hits upper right slope (corresponds to flag & 0x40)
pub hit_upper_right_slope, set_hit_upper_right_slope: 6;
/// Used only in bullet code, set if a bullet hits upper left slope (corresponds to flag & 0x80)
pub hit_upper_left_slope, set_hit_upper_left_slope: 7;
/// Set if entity is in water. (corresponds to flag & 0x100)
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
@ -123,7 +124,10 @@ bitfield! {
pub control_enabled, set_control_enabled: 1; // 0x02
pub interactions_disabled, set_interactions_disabled: 2; // 0x04
pub credits_running, set_credits_running: 3; // 0x08
// cs+ switch specific, according to peri:
// Flag 0x10 prevents the OK button from restarting the item description event (resets when the cursor is moved)
// (it does not prevent the cancel button from exiting the inventory, however)
pub ok_button_disabled, set_ok_button_disabled: 4; // 0x10
// engine specific flags
pub friendly_fire, set_friendly_fire: 14;
}
@ -131,16 +135,21 @@ bitfield! {
bitfield! {
#[derive(Clone, Copy)]
#[repr(C)]
pub struct BulletFlag(u16);
pub struct BulletFlag(u8);
impl Debug;
pub flag_x01, set_flag_x01: 0; // 0x01
pub flag_x02, set_flag_x02: 1; // 0x02
pub no_collision_checks, set_no_collision_checks: 2; // 0x04
pub bounce_from_walls, set_bounce_from_walls: 3; // 0x08
pub flag_x10, set_flag_x10: 4; // 0x10
pub flag_x20, set_flag_x20: 5; // 0x20
pub can_destroy_snack, set_can_destroy_snack: 6; // 0x40
pub flag_x80, set_flag_x80: 7; // 0x80
pub flag_x01, set_flag_x01: 0; // 0x01, nowhere in code?
pub flag_x02, set_flag_x02: 1; // 0x02, nowhere in code?
/// Corresponds to flag & 0x04. If set, bullet will pass through blocks.
pub no_collision_checks, set_no_collision_checks: 2;
/// Corresponds to flag & 0x08. IF set, bullet will bounce off walls.
pub bounce_from_walls, set_bounce_from_walls: 3;
/// Corresponds to flag & 0x10. IF set, bullet will not produce projectile dissipation effect when it hits a NPC or boss.
pub no_proj_dissipation, set_no_proj_dissipation: 4;
/// Corresponds to flag & 0x20. If set, performs checks in block collision check procedure. Kills the bullet if flag 0x40 isn't set.
pub check_block_hit, set_check_block_hit: 5;
/// Corresponds to flag & 0x40. If set, bullet will destroy snack blocks on hit.
pub can_destroy_snack, set_can_destroy_snack: 6;
pub flag_x80, set_flag_x80: 7; // 0x80, nowhere in code?
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@ -154,7 +163,7 @@ pub enum FadeDirection {
}
impl FadeDirection {
pub fn from_int(val: usize) -> Option<FadeDirection> {
pub const fn from_int(val: usize) -> Option<FadeDirection> {
match val {
0 => Some(FadeDirection::Left),
1 => Some(FadeDirection::Up),
@ -165,7 +174,7 @@ impl FadeDirection {
}
}
pub fn opposite(&self) -> FadeDirection {
pub const fn opposite(&self) -> FadeDirection {
match self {
FadeDirection::Left => FadeDirection::Right,
FadeDirection::Up => FadeDirection::Down,
@ -198,7 +207,7 @@ pub enum Direction {
pub const FILE_TYPES: [&str; 3] = [".png", ".bmp", ".pbm"];
impl Direction {
pub fn from_int(val: usize) -> Option<Direction> {
pub const fn from_int(val: usize) -> Option<Direction> {
match val {
0 => Some(Direction::Left),
1 => Some(Direction::Up),
@ -208,7 +217,7 @@ impl Direction {
}
}
pub fn from_int_facing(val: usize) -> Option<Direction> {
pub const fn from_int_facing(val: usize) -> Option<Direction> {
match val {
0 => Some(Direction::Left),
1 => Some(Direction::Up),
@ -219,33 +228,33 @@ impl Direction {
}
}
pub fn opposite(&self) -> Direction {
pub const fn opposite(&self) -> Direction {
match self {
Direction::Left => Direction::Right,
Direction::Up => Direction::Bottom,
Direction::Right => Direction::Left,
Direction::Bottom => Direction::Up,
Direction::FacingPlayer => unreachable!(),
Direction::FacingPlayer => Direction::FacingPlayer,
}
}
pub fn vector_x(&self) -> i32 {
pub const fn vector_x(&self) -> i32 {
match self {
Direction::Left => -1,
Direction::Up => 0,
Direction::Right => 1,
Direction::Bottom => 0,
Direction::FacingPlayer => unreachable!(),
Direction::FacingPlayer => 0,
}
}
pub fn vector_y(&self) -> i32 {
pub const fn vector_y(&self) -> i32 {
match self {
Direction::Left => 0,
Direction::Up => -1,
Direction::Right => 0,
Direction::Bottom => 1,
Direction::FacingPlayer => unreachable!(),
Direction::FacingPlayer => 0,
}
}
}
@ -293,8 +302,8 @@ impl<T: Num + PartialOrd + Copy> Rect<T> {
impl<T: Num + PartialOrd + Copy + Serialize> Serialize for Rect<T> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
where
S: Serializer,
{
let mut state = serializer.serialize_tuple_struct("Rect", 4)?;
state.serialize_field(&self.left)?;
@ -316,7 +325,7 @@ impl<T: Num + PartialOrd + Copy + Serialize> Default for Rect<T> {
}
}
macro_rules! rect_deserialze {
macro_rules! rect_deserialize {
($num_type: ident) => {
impl<'de> Deserialize<'de> for Rect<$num_type> {
fn deserialize<D>(deserializer: D) -> Result<Rect<$num_type>, D::Error>
@ -353,11 +362,11 @@ macro_rules! rect_deserialze {
};
}
rect_deserialze!(u8);
rect_deserialze!(u16);
rect_deserialze!(i32);
rect_deserialze!(isize);
rect_deserialze!(usize);
rect_deserialize!(u8);
rect_deserialize!(u16);
rect_deserialize!(i32);
rect_deserialize!(isize);
rect_deserialize!(usize);
#[inline(always)]
pub fn fix9_scale(val: i32) -> f32 {
@ -384,6 +393,11 @@ pub fn interpolate_fix9_scale(old_val: i32, val: i32, frame_delta: f64) -> f32 {
}
}
pub fn get_timestamp() -> u64 {
let now = SystemTime::now();
now.duration_since(UNIX_EPOCH).unwrap().as_secs() as u64
}
/// A RGBA color in the `sRGB` color space represented as `f32`'s in the range `[0.0-1.0]`
///
/// For convenience, [`WHITE`](constant.WHITE.html) and [`BLACK`](constant.BLACK.html) are provided.
@ -541,18 +555,14 @@ impl<T> SliceExt for [T] {
type Item = T;
fn get_two_mut(&mut self, a: usize, b: usize) -> Option<(&mut Self::Item, &mut Self::Item)> {
if a == b {
None
} else {
if a >= self.len() || b >= self.len() {
None
} else {
unsafe {
let ar = &mut *(self.get_unchecked_mut(a) as *mut _);
let br = &mut *(self.get_unchecked_mut(b) as *mut _);
Some((ar, br))
}
}
if a == b || a >= self.len() || b >= self.len() {
return None;
}
unsafe {
let ar = &mut *(self.get_unchecked_mut(a) as *mut _);
let br = &mut *(self.get_unchecked_mut(b) as *mut _);
Some((ar, br))
}
}
}

View File

@ -0,0 +1,176 @@
use crate::common::{Color, Rect};
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::framework::graphics;
use crate::game::frame::Frame;
use crate::game::shared_game_state::SharedGameState;
use crate::game::stage::{BackgroundType, Stage, StageTexturePaths};
pub struct Background {
pub tick: usize,
pub prev_tick: usize,
}
impl Background {
pub fn new() -> Self {
Background { tick: 0, prev_tick: 0 }
}
pub fn tick(&mut self) -> GameResult<()> {
self.tick = self.tick.wrapping_add(1);
Ok(())
}
pub fn draw_tick(&mut self) -> GameResult<()> {
self.prev_tick = self.tick;
Ok(())
}
pub fn draw(
&self,
state: &mut SharedGameState,
ctx: &mut Context,
frame: &Frame,
textures: &StageTexturePaths,
stage: &Stage,
) -> GameResult {
let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, &textures.background)?;
let scale = state.scale;
let (frame_x, frame_y) = frame.xy_interpolated(state.frame_time);
match stage.data.background_type {
BackgroundType::TiledStatic => {
graphics::clear(ctx, stage.data.background_color);
let (bg_width, bg_height) = (batch.width() as i32, batch.height() as i32);
let count_x = state.canvas_size.0 as i32 / bg_width + 1;
let count_y = state.canvas_size.1 as i32 / bg_height + 1;
for y in -1..count_y {
for x in -1..count_x {
batch.add((x * bg_width) as f32, (y * bg_height) as f32);
}
}
}
BackgroundType::TiledParallax | BackgroundType::Tiled | BackgroundType::Waterway => {
graphics::clear(ctx, stage.data.background_color);
let (off_x, off_y) = if stage.data.background_type == BackgroundType::Tiled {
(frame_x % (batch.width() as f32), frame_y % (batch.height() as f32))
} else {
(
((frame_x / 2.0 * scale).floor() / scale) % (batch.width() as f32),
((frame_y / 2.0 * scale).floor() / scale) % (batch.height() as f32),
)
};
let (bg_width, bg_height) = (batch.width() as i32, batch.height() as i32);
let count_x = state.canvas_size.0 as i32 / bg_width + 2;
let count_y = state.canvas_size.1 as i32 / bg_height + 2;
for y in -1..count_y {
for x in -1..count_x {
batch.add((x * bg_width) as f32 - off_x, (y * bg_height) as f32 - off_y);
}
}
}
BackgroundType::Water => {
graphics::clear(ctx, stage.data.background_color);
}
BackgroundType::Black => {
graphics::clear(ctx, stage.data.background_color);
}
BackgroundType::Scrolling => {
graphics::clear(ctx, stage.data.background_color);
let (bg_width, bg_height) = (batch.width() as i32, batch.height() as i32);
let offset_x = self.tick as f32 % (bg_width as f32 / 3.0);
let interp_x = (offset_x * (1.0 - state.frame_time as f32)
+ (offset_x + 1.0) * state.frame_time as f32)
* 3.0
* scale;
let count_x = state.canvas_size.0 as i32 / bg_width + 6;
let count_y = state.canvas_size.1 as i32 / bg_height + 1;
for y in -1..count_y {
for x in -1..count_x {
batch.add((x * bg_width) as f32 - interp_x, (y * bg_height) as f32);
}
}
}
BackgroundType::OutsideWind | BackgroundType::Outside | BackgroundType::OutsideUnknown => {
graphics::clear(ctx, Color::from_rgb(0, 0, 0));
let offset_x = (self.tick % 640) as i32;
let offset_y = ((state.canvas_size.1 - 240.0) / 2.0).floor();
// Sun/Moon with 100px buffers on either side
let (start, width, center) = if state.constants.is_switch {
(0, 427, ((state.canvas_size.0 - 427.0) / 2.0).floor())
} else {
(144, 320, ((state.canvas_size.0 - 320.0) / 2.0).floor())
};
for x in (0..(center as i32)).step_by(100) {
batch.add_rect(x as f32, offset_y, &Rect::new_size(start, 0, 100, 88));
}
batch.add_rect(center, offset_y, &Rect::new_size(0, 0, width, 88));
for x in (center as i32 + width as i32..(state.canvas_size.0 as i32)).step_by(100) {
batch.add_rect(x as f32, offset_y, &Rect::new_size(start, 0, 100, 88));
}
// top / bottom edges
if offset_y > 0.0 {
let scale = offset_y;
for x in (0..(state.canvas_size.0 as i32)).step_by(100) {
batch.add_rect_scaled(x as f32, 0.0, 1.0, scale, &Rect::new_size(128, 0, 100, 1));
}
batch.add_rect_scaled(
(state.canvas_size.0 - 320.0) / 2.0,
0.0,
1.0,
scale,
&Rect::new_size(0, 0, 320, 1),
);
for x in ((-offset_x * 4)..(state.canvas_size.0 as i32)).step_by(320) {
batch.add_rect_scaled(
x as f32,
offset_y + 240.0,
1.0,
scale + 4.0,
&Rect::new_size(0, 239, 320, 1),
);
}
}
for x in ((-offset_x / 2)..(state.canvas_size.0 as i32)).step_by(320) {
batch.add_rect(x as f32, offset_y + 88.0, &Rect::new_size(0, 88, 320, 35));
}
for x in ((-offset_x % 320)..(state.canvas_size.0 as i32)).step_by(320) {
batch.add_rect(x as f32, offset_y + 123.0, &Rect::new_size(0, 123, 320, 23));
}
for x in ((-offset_x * 2)..(state.canvas_size.0 as i32)).step_by(320) {
batch.add_rect(x as f32, offset_y + 146.0, &Rect::new_size(0, 146, 320, 30));
}
for x in ((-offset_x * 4)..(state.canvas_size.0 as i32)).step_by(320) {
batch.add_rect(x as f32, offset_y + 176.0, &Rect::new_size(0, 176, 320, 64));
}
}
}
batch.draw(ctx)?;
Ok(())
}
}

View File

@ -1,11 +1,11 @@
use crate::common::Rect;
use crate::entity::GameEntity;
use crate::frame::Frame;
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::npc::boss::BossNPC;
use crate::npc::list::NPCList;
use crate::shared_game_state::SharedGameState;
use crate::game::frame::Frame;
use crate::game::shared_game_state::SharedGameState;
use crate::game::npc::boss::BossNPC;
use crate::game::npc::list::NPCList;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
@ -25,13 +25,7 @@ pub struct BossLifeBar {
impl BossLifeBar {
pub fn new() -> BossLifeBar {
BossLifeBar {
target: BossLifeTarget::None,
life: 0,
max_life: 0,
prev_life: 0,
counter: 0,
}
BossLifeBar { target: BossLifeTarget::None, life: 0, max_life: 0, prev_life: 0, counter: 0 }
}
pub fn set_npc_target(&mut self, npc_id: u16, npc_list: &NPCList) {
@ -49,6 +43,91 @@ impl BossLifeBar {
self.max_life = self.life;
self.prev_life = self.life;
}
fn draw_regular(&self, state: &mut SharedGameState, ctx: &mut Context, _frame: &Frame) -> GameResult {
let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, "TextBox")?;
let box_length = 256;
let bar_length = box_length - 58;
let text_rect = Rect::new_size(0, 48, 32, 8);
let box_rect1 = Rect::new_size(0, 0, 244, 8);
let box_rect2 = Rect::new_size(0, 16, 244, 8);
let mut rect_prev_bar = Rect::new_size(0, 32, 232, 8);
let mut rect_life_bar = Rect::new_size(0, 24, 232, 8);
rect_prev_bar.right = ((self.prev_life as u32 * bar_length) / self.max_life as u32).min(bar_length) as u16;
rect_life_bar.right = ((self.life as u32 * bar_length) / self.max_life as u32).min(bar_length) as u16;
batch.add_rect(
((state.canvas_size.0 - box_length as f32) / 2.0).floor(),
state.canvas_size.1 - 20.0,
&box_rect1,
);
batch.add_rect(
((state.canvas_size.0 - box_length as f32) / 2.0).floor(),
state.canvas_size.1 - 12.0,
&box_rect2,
);
batch.add_rect(
((state.canvas_size.0 - box_length as f32) / 2.0).floor(),
state.canvas_size.1 - 20.0,
&box_rect1,
);
batch.add_rect(
((state.canvas_size.0 - box_length as f32) / 2.0 + 40.0).floor(),
state.canvas_size.1 - 16.0,
&rect_prev_bar,
);
batch.add_rect(
((state.canvas_size.0 - box_length as f32) / 2.0 + 40.0).floor(),
state.canvas_size.1 - 16.0,
&rect_life_bar,
);
batch.add_rect(
((state.canvas_size.0 - box_length as f32) / 2.0 + 8.0).floor(),
state.canvas_size.1 - 16.0,
&text_rect,
);
batch.draw(ctx)?;
Ok(())
}
fn draw_nx(&self, state: &mut SharedGameState, ctx: &mut Context, _frame: &Frame) -> GameResult {
let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, "TextBox")?;
let box_length = 148;
let bar_length = box_length - 52;
let text_rect = Rect::new_size(0, 48, 32, 8);
let box_rect1 = Rect::new_size(0, 0, 136, 8);
let box_rect2 = Rect::new_size(0, 16, 136, 8);
let box_rect3 = Rect::new_size(238, 0, 6, 8);
let box_rect4 = Rect::new_size(238, 16, 6, 8);
let mut rect_prev_bar = Rect::new_size(0, 32, 124, 8);
let mut rect_life_bar = Rect::new_size(0, 24, 124, 8);
rect_prev_bar.right = ((self.prev_life as u32 * bar_length) / self.max_life as u32).min(bar_length) as u16;
rect_life_bar.right = ((self.life as u32 * bar_length) / self.max_life as u32).min(bar_length) as u16;
let base_x = state.canvas_size.0 - box_length as f32;
batch.add_rect((base_x - 6.0).floor(), state.canvas_size.1 - 20.0, &box_rect1);
batch.add_rect((base_x - 6.0).floor(), state.canvas_size.1 - 12.0, &box_rect2);
batch.add_rect((base_x - 6.0).floor(), state.canvas_size.1 - 20.0, &box_rect1);
batch.add_rect((state.canvas_size.0 - 18.0).floor(), state.canvas_size.1 - 20.0, &box_rect3);
batch.add_rect((state.canvas_size.0 - 18.0).floor(), state.canvas_size.1 - 12.0, &box_rect4);
batch.add_rect((state.canvas_size.0 - 18.0).floor(), state.canvas_size.1 - 20.0, &box_rect3);
batch.add_rect((base_x + 34.0).floor(), state.canvas_size.1 - 16.0, &rect_prev_bar);
batch.add_rect((base_x + 34.0).floor(), state.canvas_size.1 - 16.0, &rect_life_bar);
batch.add_rect((base_x + 2.0).floor(), state.canvas_size.1 - 16.0, &text_rect);
batch.draw(ctx)?;
Ok(())
}
}
impl GameEntity<(&NPCList, &BossNPC)> for BossLifeBar {
@ -86,35 +165,9 @@ impl GameEntity<(&NPCList, &BossNPC)> for BossLifeBar {
return Ok(());
}
let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, "TextBox")?;
let box_length = 256;
let bar_length = box_length - 58;
let text_rect = Rect::new_size(0, 48, 32, 8);
let box_rect1 = Rect::new_size(0, 0, 244, 8);
let box_rect2 = Rect::new_size(0, 16, 244, 8);
let mut rect_prev_bar = Rect::new_size(0, 32, 232, 8);
let mut rect_life_bar = Rect::new_size(0, 24, 232, 8);
rect_prev_bar.right = ((self.prev_life as u32 * bar_length) / self.max_life as u32).min(bar_length) as u16;
rect_life_bar.right = ((self.life as u32 * bar_length) / self.max_life as u32).min(bar_length) as u16;
batch.add_rect(((state.canvas_size.0 - box_length as f32) / 2.0).floor(),
state.canvas_size.1 - 20.0, &box_rect1);
batch.add_rect(((state.canvas_size.0 - box_length as f32) / 2.0).floor(),
state.canvas_size.1 - 12.0, &box_rect2);
batch.add_rect(((state.canvas_size.0 - box_length as f32) / 2.0).floor(),
state.canvas_size.1 - 20.0, &box_rect1);
batch.add_rect(((state.canvas_size.0 - box_length as f32) / 2.0 + 40.0).floor(),
state.canvas_size.1 - 16.0, &rect_prev_bar);
batch.add_rect(((state.canvas_size.0 - box_length as f32) / 2.0 + 40.0).floor(),
state.canvas_size.1 - 16.0, &rect_life_bar);
batch.add_rect(((state.canvas_size.0 - box_length as f32) / 2.0 + 8.0).floor(),
state.canvas_size.1 - 16.0, &text_rect);
batch.draw(ctx)?;
Ok(())
match state.constants.is_switch {
true => self.draw_nx(state, ctx, _frame),
false => self.draw_regular(state, ctx, _frame),
}
}
}

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

@ -1,11 +1,12 @@
use crate::common::{Color, Rect};
use crate::entity::GameEntity;
use crate::frame::Frame;
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::framework::graphics;
use crate::scripting::tsc::text_script::IllustrationState;
use crate::shared_game_state::SharedGameState;
use crate::game::frame::Frame;
use crate::game::scripting::tsc::text_script::IllustrationState;
use crate::game::shared_game_state::SharedGameState;
use crate::graphics::font::Font;
pub struct Credits {}
@ -60,23 +61,64 @@ impl GameEntity<()> for Credits {
}
let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, "Casts")?;
for line in state.creditscript_vm.lines.iter() {
for line in &state.creditscript_vm.lines {
let x = (line.cast_id % 13) * 24;
let y = ((line.cast_id / 13) & 0xff) * 24;
let rect = Rect::new_size(x, y, 24, 24);
batch.add_rect(line.pos_x - 24.0, line.pos_y - 8.0, &rect);
if state.more_rust && line.cast_id == 1 {
// sue with more rust
batch.add_rect_tinted(line.pos_x - 24.0, line.pos_y - 8.0, (200, 200, 255, 255), &rect);
} else {
batch.add_rect(line.pos_x - 24.0, line.pos_y - 8.0, &rect);
}
}
batch.draw(ctx)?;
for line in state.creditscript_vm.lines.iter() {
state.font.draw_text_with_shadow(
line.text.chars(),
line.pos_x,
line.pos_y,
if state.more_rust {
// draw sue's headband separately because rust doesn't let me mutate the texture set multiple times at once
let headband_spritesheet = {
let base = if state.settings.original_textures { "ogph" } else { "plus" };
format!("headband/{}/Casts", base)
};
let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, headband_spritesheet.as_str())?;
for line in &state.creditscript_vm.lines {
if line.cast_id != 1 {
continue;
}
let x = (line.cast_id % 13) * 24;
let y = ((line.cast_id / 13) & 0xff) * 24;
let rect = Rect::new_size(x, y, 24, 24);
batch.add_rect(line.pos_x - 24.0, line.pos_y - 8.0, &rect);
break;
}
batch.draw(ctx)?;
}
for line in &state.creditscript_vm.lines {
let mut text_ovr = None;
if state.more_rust {
text_ovr = Some(line.text.replace("Sue Sakamoto", "Crabby Sue"));
}
let mut text = line.text.as_str();
if let Some(ovr) = text_ovr.as_ref() {
text = ovr.as_str();
}
state.font.builder().position(line.pos_x, line.pos_y).shadow(true).draw(
text,
ctx,
&state.constants,
&mut state.texture_set,
ctx,
)?;
}

View File

@ -1,7 +1,7 @@
use crate::common::Rect;
use crate::shared_game_state::SharedGameState;
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::game::shared_game_state::SharedGameState;
#[derive(Debug, EnumIter, PartialEq, Eq, Hash, Copy, Clone)]
pub enum Alignment {
@ -23,3 +23,18 @@ pub fn draw_number(x: f32, y: f32, val: usize, align: Alignment, state: &mut Sha
batch.draw(ctx)?;
Ok(())
}
pub fn draw_number_zeros(x: f32, y: f32, val: usize, align: Alignment, zeros: usize, state: &mut SharedGameState, ctx: &mut Context) -> GameResult {
let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, "TextBox")?;
let n = format!("{:01$}", val, zeros);
let align_offset = if align == Alignment::Right { n.len() as f32 * 8.0 } else { 0.0 };
for (offset, chr) in n.chars().enumerate() {
let idx = chr as u16 - '0' as u16;
batch.add_rect(x - align_offset + offset as f32 * 8.0, y, &Rect::new_size(idx * 8, 56, 8, 8));
}
batch.draw(ctx)?;
Ok(())
}

152
src/components/fade.rs Normal file
View File

@ -0,0 +1,152 @@
use crate::common::{FadeDirection, FadeState, Rect};
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;
pub struct Fade;
impl Fade {
pub fn new() -> Self {
Fade
}
}
impl GameEntity<()> for Fade {
fn tick(&mut self, state: &mut SharedGameState, _custom: ()) -> GameResult {
let fade_ticks = state.constants.textscript.fade_ticks;
match state.fade_state {
FadeState::FadeOut(tick, direction) if tick < fade_ticks => {
state.fade_state = FadeState::FadeOut(tick + 1, direction);
}
FadeState::FadeOut(tick, _) if tick == fade_ticks => {
state.fade_state = FadeState::Hidden;
}
FadeState::FadeIn(tick, direction) if tick > -fade_ticks => {
state.fade_state = FadeState::FadeIn(tick - 1, direction);
}
FadeState::FadeIn(tick, _) if tick == -fade_ticks => {
state.fade_state = FadeState::Visible;
}
_ => {}
}
Ok(())
}
fn draw(&self, state: &mut SharedGameState, ctx: &mut Context, _frame: &Frame) -> GameResult {
match state.fade_state {
FadeState::Visible => {
return Ok(());
}
FadeState::Hidden => {
let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, "Fade")?;
let mut rect = Rect::new(0, 0, 16, 16);
let frame = 15;
rect.left = frame * 16;
rect.right = rect.left + 16;
for x in 0..(state.canvas_size.0 as i32 / 16 + 1) {
for y in 0..(state.canvas_size.1 as i32 / 16 + 1) {
batch.add_rect(x as f32 * 16.0, y as f32 * 16.0, &rect);
}
}
batch.draw(ctx)?;
}
FadeState::FadeIn(tick, direction) | FadeState::FadeOut(tick, direction) => {
let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, "Fade")?;
let mut rect = Rect::new(0, 0, 16, 16);
match direction {
FadeDirection::Left | FadeDirection::Right => {
let mut frame = tick;
for x in 0..(state.canvas_size.0 as i32 / 16 + 1) {
if frame >= 15 {
frame = 15;
} else {
frame += 1;
}
if frame >= 0 {
rect.left = frame.abs() as u16 * 16;
rect.right = rect.left + 16;
for y in 0..(state.canvas_size.1 as i32 / 16 + 1) {
if direction == FadeDirection::Left {
batch.add_rect(
state.canvas_size.0 - x as f32 * 16.0 - 16.0,
y as f32 * 16.0,
&rect,
);
} else {
batch.add_rect(x as f32 * 16.0, y as f32 * 16.0, &rect);
}
}
}
}
}
FadeDirection::Up | FadeDirection::Down => {
let mut frame = tick;
for y in 0..(state.canvas_size.1 as i32 / 16 + 1) {
if frame >= 15 {
frame = 15;
} else {
frame += 1;
}
if frame >= 0 {
rect.left = frame.abs() as u16 * 16;
rect.right = rect.left + 16;
for x in 0..(state.canvas_size.0 as i32 / 16 + 1) {
if direction == FadeDirection::Down {
batch.add_rect(x as f32 * 16.0, y as f32 * 16.0, &rect);
} else {
batch.add_rect(x as f32 * 16.0, state.canvas_size.1 - y as f32 * 16.0, &rect);
}
}
}
}
}
FadeDirection::Center => {
let center_x = (state.canvas_size.0 / 2.0 - 8.0) as i32;
let center_y = (state.canvas_size.1 / 2.0 - 8.0) as i32;
let mut start_frame = tick;
for x in 0..(center_x / 16 + 2) {
let mut frame = start_frame;
for y in 0..(center_y / 16 + 2) {
if frame >= 15 {
frame = 15;
} else {
frame += 1;
}
if frame >= 0 {
rect.left = frame.abs() as u16 * 16;
rect.right = rect.left + 16;
batch.add_rect((center_x - x * 16) as f32, (center_y + y * 16) as f32, &rect);
batch.add_rect((center_x - x * 16) as f32, (center_y - y * 16) as f32, &rect);
batch.add_rect((center_x + x * 16) as f32, (center_y + y * 16) as f32, &rect);
batch.add_rect((center_x + x * 16) as f32, (center_y - y * 16) as f32, &rect);
}
}
start_frame += 1;
}
}
}
batch.draw(ctx)?;
}
}
Ok(())
}
}

View File

@ -1,11 +1,13 @@
use std::ops::Deref;
use crate::common::{Color, Rect};
use crate::entity::GameEntity;
use crate::frame::Frame;
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::graphics;
use crate::scripting::tsc::text_script::TextScriptExecutionState;
use crate::shared_game_state::SharedGameState;
use crate::framework::graphics;
use crate::game::frame::Frame;
use crate::game::shared_game_state::SharedGameState;
use crate::game::scripting::tsc::text_script::TextScriptExecutionState;
pub struct FallingIsland {}
@ -16,7 +18,7 @@ impl FallingIsland {
}
impl GameEntity<()> for FallingIsland {
fn tick(&mut self, state: &mut SharedGameState, custom: ()) -> GameResult {
fn tick(&mut self, _state: &mut SharedGameState, _custom: ()) -> GameResult {
Ok(())
}
@ -43,7 +45,11 @@ impl GameEntity<()> for FallingIsland {
static RECT_ISLAND: Rect<u16> = Rect { left: 160, top: 0, right: 200, bottom: 24 };
static RECT_TERRAIN: Rect<u16> = Rect { left: 160, top: 48, right: 320, bottom: 80 };
let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, &state.npc_table.tex_npc1_name)?;
let batch = state.texture_set.get_or_load_batch(
ctx,
&state.constants,
&state.npc_table.stage_textures.deref().borrow().npc1,
)?;
batch.add_rect(off_x + 80.0, 80.0, &RECT_BG);
batch.add_rect(off_x + (pos_x as f32 / 512.0) - 20.0, (pos_y as f32 / 512.0) - 12.0, &RECT_ISLAND);
batch.add_rect(off_x + 80.0, 128.0, &RECT_TERRAIN);

View File

@ -1,10 +1,10 @@
use crate::common::{Color, Rect};
use crate::entity::GameEntity;
use crate::frame::Frame;
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::framework::graphics;
use crate::shared_game_state::SharedGameState;
use crate::game::frame::Frame;
use crate::game::shared_game_state::SharedGameState;
pub enum FlashState {
None,
@ -83,7 +83,7 @@ impl GameEntity<()> for Flash {
left: 0,
top: ((cen_y - width) * state.scale) as isize,
right: (state.canvas_size.0 * state.scale) as isize,
bottom: ((cen_y + width) * state.scale) as isize
bottom: ((cen_y + width) * state.scale) as isize,
};
graphics::draw_rect(ctx, rect, WHITE)?;
@ -93,7 +93,7 @@ impl GameEntity<()> for Flash {
left: ((cen_x - width) * state.scale) as isize,
top: 0,
right: ((cen_x + width) * state.scale) as isize,
bottom: (state.canvas_size.1 * state.scale) as isize
bottom: (state.canvas_size.1 * state.scale) as isize,
};
graphics::draw_rect(ctx, rect, WHITE)?;

View File

@ -1,13 +1,14 @@
use crate::common::Rect;
use crate::components::draw_common::{draw_number, Alignment};
use crate::components::draw_common::{Alignment, draw_number};
use crate::entity::GameEntity;
use crate::frame::Frame;
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::framework::graphics::screen_insets_scaled;
use crate::inventory::Inventory;
use crate::player::Player;
use crate::shared_game_state::SharedGameState;
use crate::game::frame::Frame;
use crate::game::inventory::Inventory;
use crate::game::shared_game_state::SharedGameState;
use crate::game::player::Player;
use crate::game::weapon::WeaponType;
pub struct HUD {
pub alignment: Alignment,
@ -30,6 +31,7 @@ pub struct HUD {
weapon_count: usize,
current_weapon: isize,
weapon_types: [u8; 16],
shock: bool,
}
impl HUD {
@ -55,6 +57,7 @@ impl HUD {
weapon_count: 0,
current_weapon: 0,
weapon_types: [0; 16],
shock: false,
}
}
}
@ -75,6 +78,7 @@ impl GameEntity<(&Player, &mut Inventory)> for HUD {
self.max_life = player.max_life;
self.air = player.air;
self.air_counter = player.air_counter;
self.shock = player.shock_counter / 2 % 2 != 0;
self.weapon_count = inventory.get_weapon_count();
self.current_weapon = inventory.get_current_weapon_idx() as isize;
@ -108,12 +112,26 @@ impl GameEntity<(&Player, &mut Inventory)> for HUD {
if player.controller.trigger_next_weapon() {
state.sound_manager.play_sfx(4);
inventory.next_weapon();
if let Some(weapon) = inventory.get_current_weapon_mut() {
if weapon.wtype == WeaponType::Spur {
weapon.reset_xp();
}
}
self.weapon_x_pos = 32;
}
if player.controller.trigger_prev_weapon() {
state.sound_manager.play_sfx(4);
inventory.prev_weapon();
if let Some(weapon) = inventory.get_current_weapon_mut() {
if weapon.wtype == WeaponType::Spur {
weapon.reset_xp();
}
}
self.weapon_x_pos = 0;
}
}
@ -143,7 +161,7 @@ impl GameEntity<(&Player, &mut Inventory)> for HUD {
let wtype = self.weapon_types[a];
if wtype != 0 {
rect = Rect::new_size(pos_x + weapon_offset - 4, 16 - 4, 24, 24);
rect = Rect::new_size(pos_x + weapon_offset - 4, 16 + (4 * state.scale as isize), 24, 24);
if state.touch_controls.consume_click_in(rect) {
state.sound_manager.play_sfx(4);
@ -190,38 +208,40 @@ impl GameEntity<(&Player, &mut Inventory)> for HUD {
batch.add_rect(bar_offset + weap_x + 48.0, 24.0 + top, &Rect::new_size(80, 48, 16, 8));
}
// per
batch.add_rect(bar_offset + weap_x + 32.0, 24.0 + top, &Rect::new_size(72, 48, 8, 8));
// lv
batch.add_rect(num_offset + weap_x, 32.0 + top, &Rect::new_size(80, 80, 16, 8));
// xp box
batch.add_rect(bar_offset + weap_x + 24.0, 32.0 + top, &Rect::new_size(0, 72, 40, 8));
if !self.shock {
// per
batch.add_rect(bar_offset + weap_x + 32.0, 24.0 + top, &Rect::new_size(72, 48, 8, 8));
// lv
batch.add_rect(num_offset + weap_x, 32.0 + top, &Rect::new_size(80, 80, 16, 8));
// xp box
batch.add_rect(bar_offset + weap_x + 24.0, 32.0 + top, &Rect::new_size(0, 72, 40, 8));
if self.max_level {
batch.add_rect(bar_offset + weap_x + 24.0, 32.0 + top, &Rect::new_size(40, 72, 40, 8));
} else if self.max_xp > 0 {
// xp bar
let bar_width = (self.xp as f32 / self.max_xp as f32 * 40.0) as u16;
if self.max_level {
batch.add_rect(bar_offset + weap_x + 24.0, 32.0 + top, &Rect::new_size(40, 72, 40, 8));
} else if self.max_xp > 0 {
// xp bar
let bar_width = (self.xp as f32 / self.max_xp as f32 * 40.0) as u16;
batch.add_rect(bar_offset + weap_x + 24.0, 32.0 + top, &Rect::new_size(0, 80, bar_width, 8));
}
batch.add_rect(bar_offset + weap_x + 24.0, 32.0 + top, &Rect::new_size(0, 80, bar_width, 8));
}
if (self.xp_bar_counter & 0x02) != 0 {
batch.add_rect(bar_offset + weap_x + 24.0, 32.0 + top, &Rect::new_size(40, 80, 40, 8));
}
if (self.xp_bar_counter & 0x02) != 0 {
batch.add_rect(bar_offset + weap_x + 24.0, 32.0 + top, &Rect::new_size(40, 80, 40, 8));
}
if self.max_life != 0 {
let yellow_bar_width = (self.life_bar as f32 / self.max_life as f32 * 39.0) as u16;
let bar_width = (self.life as f32 / self.max_life as f32 * 39.0) as u16;
if self.max_life != 0 {
let yellow_bar_width = (self.life_bar as f32 / self.max_life as f32 * 39.0) as u16;
let bar_width = (self.life as f32 / self.max_life as f32 * 39.0) as u16;
// heart/hp number box
batch.add_rect(num_offset + 16.0, 40.0 + top, &Rect::new_size(0, 40, 24, 8));
// life box
batch.add_rect(bar_offset + 40.0, 40.0 + top, &Rect::new_size(24, 40, 40, 8));
// yellow bar
batch.add_rect(bar_offset + 40.0, 40.0 + top, &Rect::new_size(0, 32, yellow_bar_width, 8));
// life
batch.add_rect(bar_offset + 40.0, 40.0 + top, &Rect::new_size(0, 24, bar_width, 8));
// heart/hp number box
batch.add_rect(num_offset + 16.0, 40.0 + top, &Rect::new_size(0, 40, 24, 8));
// life box
batch.add_rect(bar_offset + 40.0, 40.0 + top, &Rect::new_size(24, 40, 40, 8));
// yellow bar
batch.add_rect(bar_offset + 40.0, 40.0 + top, &Rect::new_size(0, 32, yellow_bar_width, 8));
// life
batch.add_rect(bar_offset + 40.0, 40.0 + top, &Rect::new_size(0, 24, bar_width, 8));
}
}
if self.air_counter > 0 {
@ -240,8 +260,18 @@ impl GameEntity<(&Player, &mut Inventory)> for HUD {
if self.weapon_count != 0 {
let mut rect = Rect::new(0, 0, 0, 16);
// First frame of animation is off by one weapon
// There's probably a more elegant solution than this
let first_frame_offset = if self.weapon_x_pos == 32 {
-1
} else if self.weapon_x_pos == 0 {
1
} else {
0
};
for a in 0..self.weapon_count {
let mut pos_x = ((a as isize - self.current_weapon) as f32 * 16.0) + weap_x;
let mut pos_x = ((a as isize - self.current_weapon + first_frame_offset) as f32 * 16.0) + weap_x;
if pos_x < 8.0 {
pos_x += 48.0 + self.weapon_count as f32 * 16.0;
@ -282,11 +312,13 @@ impl GameEntity<(&Player, &mut Inventory)> for HUD {
}
if self.max_ammo != 0 {
draw_number(num_offset + weap_x + 64.0, 16.0 + top, self.ammo as usize, Alignment::Right, state, ctx)?;
draw_number(num_offset + weap_x + 64.0, 24.0 + top, self.max_ammo as usize, Alignment::Right, state, ctx)?;
draw_number(bar_offset + weap_x + 64.0, 16.0 + top, self.ammo as usize, Alignment::Right, state, ctx)?;
draw_number(bar_offset + weap_x + 64.0, 24.0 + top, self.max_ammo as usize, Alignment::Right, state, ctx)?;
}
if !self.shock {
draw_number(num_offset + weap_x + 24.0, 32.0 + top, self.current_level, Alignment::Right, state, ctx)?;
draw_number(num_offset + 40.0, 40.0 + top, self.life_bar as usize, Alignment::Right, state, ctx)?;
}
draw_number(num_offset + weap_x + 24.0, 32.0 + top, self.current_level, Alignment::Right, state, ctx)?;
draw_number(num_offset + 40.0, 40.0 + top, self.life_bar as usize, Alignment::Right, state, ctx)?;
Ok(())
}

View File

@ -1,15 +1,16 @@
use crate::common::Rect;
use crate::components::draw_common::{draw_number, Alignment};
use crate::components::draw_common::{Alignment, draw_number};
use crate::components::hud::HUD;
use crate::entity::GameEntity;
use crate::frame::Frame;
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::game::frame::Frame;
use crate::game::inventory::Inventory;
use crate::game::shared_game_state::SharedGameState;
use crate::input::touch_controls::TouchControlType;
use crate::inventory::Inventory;
use crate::player::Player;
use crate::shared_game_state::SharedGameState;
use crate::scripting::tsc::text_script::ScriptMode;
use crate::weapon::{WeaponLevel, WeaponType};
use crate::game::player::Player;
use crate::game::scripting::tsc::text_script::{ScriptMode, TextScriptExecutionState};
use crate::game::weapon::{WeaponLevel, WeaponType};
#[derive(Copy, Clone, PartialEq, Eq)]
#[repr(u8)]
@ -62,21 +63,25 @@ impl InventoryUI {
inventory.get_item_idx(self.selected_item as usize).map(|i| i.0 + 6000).unwrap_or(6000)
}
fn exit(&mut self, state: &mut SharedGameState, player: &mut Player, inventory: &mut Inventory) {
fn get_weapon_event_number(&self, inventory: &Inventory) -> u16 {
inventory.get_current_weapon().map(|w| w.wtype as u16 + 1000).unwrap_or(1000)
}
fn exit(&mut self, state: &mut SharedGameState, _player: &mut Player, inventory: &mut Inventory, hud: &mut HUD) {
self.focus = InventoryFocus::None;
inventory.current_item = 0;
self.text_y_pos = 16;
hud.weapon_x_pos = 32;
state.textscript_vm.reset();
state.textscript_vm.set_mode(ScriptMode::Map);
player.controller.update_trigger();
}
}
impl GameEntity<(&mut Context, &mut Player, &mut Inventory)> for InventoryUI {
impl GameEntity<(&mut Context, &mut Player, &mut Inventory, &mut HUD)> for InventoryUI {
fn tick(
&mut self,
state: &mut SharedGameState,
(ctx, player, inventory): (&mut Context, &mut Player, &mut Inventory),
(ctx, player, inventory, hud): (&mut Context, &mut Player, &mut Inventory, &mut HUD),
) -> GameResult<()> {
let (off_left, off_top, off_right, _) = crate::framework::graphics::screen_insets_scaled(ctx, state.scale);
let mut slot_rect =
@ -86,10 +91,12 @@ impl GameEntity<(&mut Context, &mut Player, &mut Inventory)> for InventoryUI {
if state.control_flags.control_enabled()
&& (player.controller.trigger_inventory()
|| player.controller.trigger_menu_back()
|| (state.settings.touch_controls && state.touch_controls.consume_click_in(slot_rect)))
|| player.controller.trigger_menu_back()
|| (player.controller.trigger_menu_ok() && self.focus == InventoryFocus::Weapons)
|| (state.settings.touch_controls && state.touch_controls.consume_click_in(slot_rect)))
{
self.exit(state, player, inventory);
state.control_flags.set_ok_button_disabled(false);
self.exit(state, player, inventory, hud);
return Ok(());
}
@ -124,10 +131,6 @@ impl GameEntity<(&mut Context, &mut Player, &mut Inventory)> for InventoryUI {
}
}
fn get_weapon_event_number(inventory: &Inventory) -> u16 {
inventory.get_current_weapon().map(|w| w.wtype as u16 + 1000).unwrap_or(1000)
}
self.selected_item = inventory.current_item;
self.selected_weapon = inventory.current_weapon;
@ -136,27 +139,45 @@ impl GameEntity<(&mut Context, &mut Player, &mut Inventory)> for InventoryUI {
match self.focus {
InventoryFocus::None => {
self.focus = InventoryFocus::Weapons;
state.textscript_vm.start_script(get_weapon_event_number(inventory));
state.control_flags.set_ok_button_disabled(false);
// 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.textscript_vm.start_script(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.textscript_vm.start_script(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));
}
}
InventoryFocus::Items if self.item_count != 0 && state.control_flags.control_enabled() => {
let mut moved_cursor = false;
if player.controller.trigger_left() {
state.sound_manager.play_sfx(1);
@ -166,22 +187,24 @@ impl GameEntity<(&mut Context, &mut Player, &mut Inventory)> for InventoryUI {
self.selected_item += count_x - 1;
}
state.textscript_vm.start_script(self.get_item_event_number(inventory));
state.control_flags.set_ok_button_disabled(false);
moved_cursor = true;
}
if player.controller.trigger_right() {
match () {
_ if self.selected_item == self.item_count + 1 => {
_ if self.selected_item + 1 == self.item_count => {
self.selected_item = count_x * (self.selected_item / count_x);
}
_ if (self.selected_item % count_x) + 1 == count_x => {
self.selected_item = self.selected_item.saturating_sub(count_x) + 1;
self.selected_item = self.selected_item.saturating_sub(count_x - 1);
}
_ => self.selected_item += 1,
}
state.sound_manager.play_sfx(1);
state.textscript_vm.start_script(self.get_item_event_number(inventory));
state.control_flags.set_ok_button_disabled(false);
moved_cursor = true;
}
if player.controller.trigger_up() {
@ -189,12 +212,14 @@ impl GameEntity<(&mut Context, &mut Player, &mut Inventory)> for InventoryUI {
self.focus = InventoryFocus::Weapons;
state.sound_manager.play_sfx(4);
state.textscript_vm.start_script(get_weapon_event_number(inventory));
state.control_flags.set_ok_button_disabled(false);
state.textscript_vm.start_script(self.get_weapon_event_number(inventory));
} else {
self.selected_item -= count_x;
state.sound_manager.play_sfx(1);
state.textscript_vm.start_script(self.get_item_event_number(inventory));
state.control_flags.set_ok_button_disabled(false);
moved_cursor = true;
}
}
@ -203,20 +228,27 @@ impl GameEntity<(&mut Context, &mut Player, &mut Inventory)> for InventoryUI {
self.focus = InventoryFocus::Weapons;
state.sound_manager.play_sfx(4);
state.textscript_vm.start_script(get_weapon_event_number(inventory));
state.control_flags.set_ok_button_disabled(false);
moved_cursor = true;
} else {
self.selected_item += count_x;
state.sound_manager.play_sfx(1);
state.textscript_vm.start_script(self.get_item_event_number(inventory));
state.control_flags.set_ok_button_disabled(false);
moved_cursor = true;
}
}
if player.controller.trigger_menu_ok() {
self.selected_item = self.selected_item.min(self.item_count - 1);
if moved_cursor {
state.textscript_vm.start_script(self.get_item_event_number(inventory));
}
if !state.control_flags.ok_button_disabled() && player.controller.trigger_menu_ok() {
state.textscript_vm.start_script(self.get_item_event_number_action(inventory));
}
self.selected_item = self.selected_item.min(self.item_count - 1);
inventory.current_item = self.selected_item;
}
_ => {}
@ -234,8 +266,8 @@ impl GameEntity<(&mut Context, &mut Player, &mut Inventory)> for InventoryUI {
state.sound_manager.play_sfx(4);
self.selected_weapon = i;
inventory.current_weapon = i;
state.textscript_vm.start_script(get_weapon_event_number(inventory));
self.exit(state, player, inventory);
state.textscript_vm.start_script(self.get_weapon_event_number(inventory));
self.exit(state, player, inventory, hud);
}
}
@ -264,6 +296,10 @@ impl GameEntity<(&mut Context, &mut Player, &mut Inventory)> for InventoryUI {
}
fn draw(&self, state: &mut SharedGameState, ctx: &mut Context, _frame: &Frame) -> GameResult<()> {
if state.textscript_vm.state == TextScriptExecutionState::MapSystem {
return Ok(());
}
let mut tmp_rect = Rect { left: 0, top: 0, right: 0, bottom: 0 };
let (off_left, off_top, off_right, _) = crate::framework::graphics::screen_insets_scaled(ctx, state.scale);
let x = (((state.canvas_size.0 - off_left - off_right) - 244.0) / 2.0).floor() + off_left;
@ -286,8 +322,8 @@ impl GameEntity<(&mut Context, &mut Player, &mut Inventory)> for InventoryUI {
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(
@ -358,6 +394,23 @@ impl GameEntity<(&mut Context, &mut Player, &mut Inventory)> for InventoryUI {
batch.draw(ctx)?;
for (idx, (item_id, amount)) in self.item_data.iter().enumerate() {
if *item_id == 0 || *amount == 0 {
break;
}
if *amount > 1 {
draw_number(
x + 12.0 + (idx % count_x) as f32 * 32.0 + 32.0,
y + 68.0 + (idx / count_x) as f32 * 16.0,
*amount as usize,
Alignment::Right,
state,
ctx,
)?;
}
}
for (idx, weapon) in self.weapon_data.iter().enumerate() {
if weapon.wtype == WeaponType::None {
break;

View File

@ -0,0 +1,286 @@
use std::cell::RefCell;
use crate::common::{Color, Rect};
use crate::framework::backend::{BackendTexture, SpriteBatchCommand};
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::framework::graphics;
use crate::game::player::Player;
use crate::game::scripting::tsc::text_script::TextScriptExecutionState;
use crate::game::shared_game_state::SharedGameState;
use crate::game::stage::Stage;
use crate::graphics::font::Font;
use crate::input::touch_controls::TouchControlType;
#[derive(Copy, Clone, Eq, PartialEq)]
pub enum MapSystemState {
Hidden,
FadeInBox(u16),
FadeInLine(u16),
Visible,
FadeOutBox(u16),
}
pub struct MapSystem {
texture: RefCell<Option<Box<dyn BackendTexture>>>,
has_map_data: RefCell<bool>,
last_size: (u16, u16),
tick: u16,
state: MapSystemState,
}
impl MapSystem {
pub fn new() -> MapSystem {
MapSystem {
texture: RefCell::new(None),
has_map_data: RefCell::new(false),
last_size: (0, 0),
tick: 0,
state: MapSystemState::Hidden,
}
}
fn render_map(&self, state: &mut SharedGameState, ctx: &mut Context, stage: &Stage) -> GameResult {
if self.texture.borrow().is_none() {
*self.has_map_data.borrow_mut() = false;
return Ok(());
}
*self.has_map_data.borrow_mut() = true;
graphics::set_render_target(ctx, self.texture.borrow().as_ref())?;
graphics::clear(ctx, Color::new(0.0, 0.0, 0.0, 1.0));
let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, "TextBox")?;
for y in 0..stage.map.height {
for x in 0..stage.map.width {
const RECTS: [Rect<u16>; 4] = [
Rect { left: 240, top: 24, right: 241, bottom: 25 },
Rect { left: 241, top: 24, right: 242, bottom: 25 },
Rect { left: 242, top: 24, right: 243, bottom: 25 },
Rect { left: 243, top: 24, right: 244, bottom: 25 },
];
let attr = stage.map.get_attribute(x as _, y as _);
let layer = match attr {
0 => 0,
0x01 | 0x02 | 0x40 | 0x44 | 0x51 | 0x52 | 0x55 | 0x56 | 0x60 | 0x71 | 0x72 | 0x75 | 0x76 | 0x80
| 0x81 | 0x82 | 0x83 | 0xA0 | 0xA1 | 0xA2 | 0xA3 => 1,
0x43 | 0x50 | 0x53 | 0x54 | 0x57 | 0x63 | 0x70 | 0x73 | 0x74 | 0x77 => 2,
_ => 3,
};
batch.add_rect(x as _, y as _, &RECTS[layer]);
}
}
batch.draw(ctx)?;
graphics::set_render_target(ctx, None)?;
Ok(())
}
pub fn tick(
&mut self,
state: &mut SharedGameState,
ctx: &mut Context,
stage: &Stage,
players: [&Player; 2],
) -> GameResult {
let touch_rect = Rect::new_size(0, 0, state.canvas_size.0 as isize, state.canvas_size.1 as isize);
if state.textscript_vm.state == TextScriptExecutionState::MapSystem {
if self.state == MapSystemState::Hidden {
state.touch_controls.control_type = TouchControlType::None;
state.control_flags.set_control_enabled(false);
self.state = MapSystemState::FadeInBox(0);
}
} else {
self.state = MapSystemState::Hidden;
}
if self.state == MapSystemState::Hidden {
self.tick = 0;
*self.has_map_data.borrow_mut() = false;
return Ok(());
}
self.tick = self.tick.wrapping_add(1);
let width = (stage.map.width as f32 * state.scale) as u16;
let height = (stage.map.height as f32 * state.scale) as u16;
if self.last_size != (width, height) {
self.last_size = (width, height);
*self.texture.borrow_mut() = graphics::create_texture_mutable(ctx, width, height).ok();
*self.has_map_data.borrow_mut() = false;
}
match self.state {
MapSystemState::FadeInBox(tick) => {
if tick >= 8 {
self.state = MapSystemState::FadeInLine(0);
} else {
self.state = MapSystemState::FadeInBox(tick + 1);
}
}
MapSystemState::FadeOutBox(tick) => {
if tick == 0 {
state.control_flags.set_tick_world(true);
state.control_flags.set_control_enabled(true);
state.textscript_vm.state = TextScriptExecutionState::Ended;
self.state = MapSystemState::Hidden;
} else {
self.state = MapSystemState::FadeOutBox(tick - 1);
}
}
MapSystemState::FadeInLine(tick) => {
if (tick + 2) < stage.map.height {
self.state = MapSystemState::FadeInLine(tick + 2);
} else {
self.state = MapSystemState::Visible;
}
for player in &players {
if player.controller.trigger_jump()
|| player.controller.trigger_shoot()
|| state.touch_controls.consume_click_in(touch_rect)
{
self.state = MapSystemState::FadeOutBox(8);
break;
}
}
}
MapSystemState::Visible => {
for player in &players {
if player.controller.trigger_jump()
|| player.controller.trigger_shoot()
|| state.touch_controls.consume_click_in(touch_rect)
{
self.state = MapSystemState::FadeOutBox(8);
break;
}
}
}
_ => (),
}
Ok(())
}
pub fn draw(
&self,
state: &mut SharedGameState,
ctx: &mut Context,
stage: &Stage,
players: [&Player; 2],
) -> GameResult {
if self.state == MapSystemState::Hidden {
return Ok(());
}
if !*self.has_map_data.borrow() {
self.render_map(state, ctx, stage)?;
}
let (scr_w, scr_h) = (state.canvas_size.0 * state.scale, state.canvas_size.1 * state.scale);
let text_height = state.font.line_height();
let rect_black_bar = Rect::new_size(
0,
(7.0 * state.scale) as _,
state.screen_size.0 as _,
((text_height + 4.0) * state.scale) as _,
);
if !state.constants.is_switch {
graphics::draw_rect(ctx, rect_black_bar, Color::new(0.0, 0.0, 0.0, 1.0))?;
}
let map_name = if state.constants.is_cs_plus && state.settings.locale == "jp" {
stage.data.name_jp.as_str()
} else {
stage.data.name.as_str()
};
state.font.builder().center(state.canvas_size.0).y(9.0).draw(
map_name,
ctx,
&state.constants,
&mut state.texture_set,
)?;
let mut map_rect = Rect::new(0.0, 0.0, self.last_size.0 as f32, self.last_size.1 as f32);
match self.state {
MapSystemState::FadeInBox(tick) | MapSystemState::FadeOutBox(tick) => {
let width = (state.scale * tick as f32 * stage.map.width as f32 / 16.0) as isize;
let height = (state.scale * tick as f32 * stage.map.height as f32 / 16.0) as isize;
let rect = Rect::new_size(
(scr_w / 2.0) as isize - width,
(scr_h / 2.0) as isize - height,
width * 2,
height * 2,
);
graphics::draw_rect(ctx, rect, Color::new(0.0, 0.0, 0.0, 1.0))?;
return Ok(());
}
MapSystemState::FadeInLine(line) => {
map_rect.bottom = state.scale * (line as f32 + 1.0);
}
_ => (),
}
let width_border = state.scale * (stage.map.width as f32 + 2.0);
let height_border = state.scale * (stage.map.height as f32 + 2.0);
let rect = Rect::new_size(
((scr_w - width_border) / 2.0) as isize,
((scr_h - height_border) / 2.0) as isize,
width_border as isize,
height_border as isize,
);
graphics::draw_rect(ctx, rect, Color::new(0.0, 0.0, 0.0, 1.0))?;
if let Some(tex) = self.texture.borrow_mut().as_mut() {
let width = state.scale * stage.map.width as f32;
let height = state.scale * stage.map.height as f32;
tex.clear();
tex.add(SpriteBatchCommand::DrawRect(
map_rect,
Rect::new_size((scr_w - width) / 2.0, (scr_h - height) / 2.0, map_rect.width(), map_rect.height()),
));
tex.draw()?;
}
if (self.tick & 8) != 0 {
const PLAYER_RECT: Rect<u16> = Rect { left: 0, top: 57, right: 1, bottom: 58 };
let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, "TextBox")?;
let x_offset = (state.canvas_size.0 - stage.map.width as f32) / 2.0;
let y_offset = (state.canvas_size.1 - stage.map.height as f32) / 2.0;
let tile_div = stage.map.tile_size.as_int() * 0x200;
for player in &players {
if !player.cond.alive() {
continue;
}
let plr_x = x_offset + (player.x / tile_div) as f32;
let plr_y = y_offset + (player.y / tile_div) as f32;
batch.add_rect(plr_x, plr_y, &PLAYER_RECT);
}
batch.draw(ctx)?;
}
Ok(())
}
}

View File

@ -1,10 +1,19 @@
pub mod background;
pub mod boss_life_bar;
pub mod compact_jukebox;
pub mod credits;
pub mod draw_common;
pub mod fade;
pub mod falling_island;
pub mod flash;
pub mod hud;
pub mod inventory;
pub mod map_system;
pub mod nikumaru;
pub mod number_popup;
pub mod replay;
pub mod stage_select;
pub mod text_boxes;
pub mod tilemap;
pub mod water_renderer;
pub mod whimsical_star;

166
src/components/nikumaru.rs Normal file
View File

@ -0,0 +1,166 @@
use byteorder::{LE, ReadBytesExt, WriteBytesExt};
use crate::common::Rect;
use crate::components::draw_common::{Alignment, draw_number, draw_number_zeros};
use crate::entity::GameEntity;
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::framework::filesystem;
use crate::framework::vfs::OpenOptions;
use crate::game::frame::Frame;
use crate::game::shared_game_state::{SharedGameState, TimingMode};
use crate::game::player::Player;
use crate::game::scripting::tsc::text_script::TextScriptExecutionState;
use crate::util::rng::RNG;
#[derive(Clone, Copy)]
pub struct NikumaruCounter {
pub tick: usize,
pub shown: bool,
}
impl NikumaruCounter {
pub fn new() -> NikumaruCounter {
NikumaruCounter { tick: 0, shown: false }
}
fn load_time(&mut self, state: &mut SharedGameState, ctx: &mut Context) -> GameResult<u32> {
if let Ok(mut data) = filesystem::user_open(ctx, [state.get_rec_filename(), ".rec".to_string()].join("")) {
let mut ticks: [u32; 4] = [0; 4];
for iter in 0..=3 {
ticks[iter] = data.read_u32::<LE>()?;
}
let random = data.read_u32::<LE>()?;
let random_list: [u8; 4] = random.to_le_bytes();
for iter in 0..=3 {
ticks[iter] = u32::from_le_bytes([
ticks[iter].to_le_bytes()[0].wrapping_sub(random_list[iter]),
ticks[iter].to_le_bytes()[1].wrapping_sub(random_list[iter]),
ticks[iter].to_le_bytes()[2].wrapping_sub(random_list[iter]),
ticks[iter].to_le_bytes()[3].wrapping_sub(random_list[iter] / 2),
]);
}
if ticks[0] == ticks[1] && ticks[0] == ticks[2] {
return Ok(ticks[0]);
}
} else {
log::warn!("Failed to open 290 record.");
}
Ok(0)
}
fn save_time(&mut self, new_time: u32, state: &mut SharedGameState, ctx: &mut Context) -> GameResult {
if let Ok(mut data) = filesystem::open_options(
ctx,
[state.get_rec_filename(), ".rec".to_string()].join(""),
OpenOptions::new().write(true).create(true),
) {
let mut ticks: [u32; 4] = [new_time; 4];
let mut random_list: [u8; 4] = [0; 4];
for iter in 0..=3 {
random_list[iter] = state.effect_rng.range(0..250) as u8 + iter as u8;
ticks[iter] = u32::from_le_bytes([
ticks[iter].to_le_bytes()[0].wrapping_add(random_list[iter]),
ticks[iter].to_le_bytes()[1].wrapping_add(random_list[iter]),
ticks[iter].to_le_bytes()[2].wrapping_add(random_list[iter]),
ticks[iter].to_le_bytes()[3].wrapping_add(random_list[iter] / 2),
]);
data.write_u32::<LE>(ticks[iter])?;
}
data.write_u32::<LE>(u32::from_le_bytes(random_list))?;
} else {
log::warn!("Failed to write 290 record.");
}
Ok(())
}
pub fn load_counter(&mut self, state: &mut SharedGameState, ctx: &mut Context) -> GameResult {
self.tick = self.load_time(state, ctx)? as usize;
if self.tick > 0 {
self.shown = true;
} else {
self.shown = false;
}
Ok(())
}
pub fn save_counter(&mut self, state: &mut SharedGameState, ctx: &mut Context) -> GameResult<bool> {
let old_record = self.load_time(state, ctx)? as usize;
if self.tick < old_record || old_record == 0 {
self.save_time(self.tick as u32, state, ctx)?;
return Ok(true);
}
Ok(false)
}
}
impl GameEntity<&Player> for NikumaruCounter {
fn tick(&mut self, state: &mut SharedGameState, player: &Player) -> GameResult {
if !player.equip.has_nikumaru() {
self.tick = 0;
self.shown = false;
return Ok(());
}
self.shown = true;
if state.control_flags.control_enabled() {
self.tick += 1;
}
if self.tick >= 300000 {
self.tick = 300000;
}
Ok(())
}
fn draw(&self, state: &mut SharedGameState, ctx: &mut Context, _frame: &Frame) -> GameResult {
if !self.shown {
return Ok(());
}
if state.textscript_vm.state == TextScriptExecutionState::MapSystem {
return Ok(());
}
let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, "TextBox")?;
let x = 16.0;
let y = 8.0;
const CLOCK_RECTS: [Rect<u16>; 2] = [
Rect { left: 112, top: 104, right: 120, bottom: 112 },
Rect { left: 120, top: 104, right: 128, bottom: 112 },
];
const PRIME: Rect<u16> = Rect { left: 128, top: 104, right: 160, bottom: 112 };
let (one_tenth, second, minute) = match state.settings.timing_mode {
TimingMode::_60Hz => (6, 60, 3600),
_ => (5, 50, 3000),
};
if self.tick % 30 <= 10 {
batch.add_rect(x, y, &CLOCK_RECTS[1]);
} else {
batch.add_rect(x, y, &CLOCK_RECTS[0]);
}
batch.add_rect(x + 30.0, y, &PRIME);
batch.draw(ctx)?;
draw_number(x + 32.0, y, self.tick / minute, Alignment::Right, state, ctx)?;
draw_number_zeros(x + 52.0, y, (self.tick / second) % 60, Alignment::Right, 2, state, ctx)?;
draw_number(x + 64.0, y, (self.tick / one_tenth) % 10, Alignment::Right, state, ctx)?;
Ok(())
}
}

View File

@ -1,9 +1,9 @@
use crate::common::{interpolate_fix9_scale, Rect};
use crate::entity::GameEntity;
use crate::frame::Frame;
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::shared_game_state::SharedGameState;
use crate::game::frame::Frame;
use crate::game::shared_game_state::SharedGameState;
#[derive(Debug, Copy, Clone)]
pub struct NumberPopup {
@ -13,43 +13,48 @@ pub struct NumberPopup {
pub prev_x: i32,
pub prev_y: i32,
counter: 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 }
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;
}
pub fn add_value(&mut self, value: i16) {
self.set_value(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.value == 0 {
if self.value_display == 0 {
return Ok(());
}
self.counter += 1;
if self.counter == 80 {
self.counter = 0;
self.value = 0;
self.value_display = 0;
}
Ok(())
}
fn draw(&self, state: &mut SharedGameState, ctx: &mut Context, frame: &Frame) -> GameResult<()> {
if self.value == 0 {
if self.value_display == 0 {
return Ok(());
}
@ -63,12 +68,10 @@ 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 mut n = self.value.to_string();
if self.value > 0 {
n = "+".to_owned() + n.as_str();
};
let n = format!("{:+}", self.value_display);
let x = x - n.len() as f32 * 4.0;
@ -81,7 +84,7 @@ impl GameEntity<()> for NumberPopup {
batch.add_rect(x + offset as f32 * 8.0, y, &Rect::new_size(40, 48 + clip, 8, 8 - clip));
}
'0'..='9' => {
let number_set = if self.value < 0 { 64 } else { 56 };
let number_set = if self.value_display < 0 { 64 } else { 56 };
let idx = chr as u16 - '0' as u16;
batch.add_rect(
x + offset as f32 * 8.0,

188
src/components/replay.rs Normal file
View File

@ -0,0 +1,188 @@
use std::io::{Cursor, Read};
use byteorder::{LE, ReadBytesExt, WriteBytesExt};
use crate::entity::GameEntity;
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::framework::filesystem;
use crate::framework::keyboard::ScanCode;
use crate::framework::vfs::OpenOptions;
use crate::game::frame::Frame;
use crate::game::shared_game_state::{ReplayKind, ReplayState, SharedGameState};
use crate::input::replay_player_controller::{KeyState, ReplayController};
use crate::game::player::Player;
use crate::graphics::font::Font;
#[derive(Clone)]
pub struct Replay {
replay_version: u16,
keylist: Vec<u16>,
last_input: KeyState,
rng_seed: u64,
pub controller: ReplayController,
tick: usize,
resume_tick: usize,
is_active: bool,
}
impl Replay {
pub fn new() -> Replay {
Replay {
replay_version: 0,
keylist: Vec::new(),
last_input: KeyState(0),
rng_seed: 0,
controller: ReplayController::new(),
tick: 0,
resume_tick: 0,
is_active: false,
}
}
pub fn initialize_recording(&mut self, state: &mut SharedGameState) {
if !self.is_active {
self.rng_seed = state.game_rng.dump_state();
self.is_active = true;
}
}
pub fn stop_recording(
&mut self,
state: &mut SharedGameState,
ctx: &mut Context,
is_new_record: bool,
) -> GameResult {
state.replay_state = ReplayState::None;
self.write_replay(state, ctx, ReplayKind::Last)?;
if is_new_record {
self.write_replay(state, ctx, ReplayKind::Best)?;
}
Ok(())
}
pub fn initialize_playback(
&mut self,
state: &mut SharedGameState,
ctx: &mut Context,
replay_kind: ReplayKind,
) -> GameResult {
if !self.is_active {
state.replay_state = ReplayState::Playback(replay_kind);
self.read_replay(state, ctx, replay_kind)?;
state.game_rng.load_state(self.rng_seed);
self.is_active = true;
}
Ok(())
}
fn write_replay(&mut self, state: &mut SharedGameState, ctx: &mut Context, replay_kind: ReplayKind) -> GameResult {
if let Ok(mut file) = filesystem::open_options(
ctx,
[state.get_rec_filename(), replay_kind.get_suffix()].join(""),
OpenOptions::new().write(true).create(true),
) {
file.write_u16::<LE>(0)?; // Space for versioning replay files
file.write_u64::<LE>(self.rng_seed)?;
for input in &self.keylist {
file.write_u16::<LE>(*input)?;
}
}
Ok(())
}
fn read_replay(&mut self, state: &mut SharedGameState, ctx: &mut Context, replay_kind: ReplayKind) -> GameResult {
if let Ok(mut file) = filesystem::user_open(ctx, [state.get_rec_filename(), replay_kind.get_suffix()].join(""))
{
self.replay_version = file.read_u16::<LE>()?;
self.rng_seed = file.read_u64::<LE>()?;
let mut data = Vec::new();
file.read_to_end(&mut data)?;
let count = data.len() / 2;
let mut inputs = Vec::new();
let mut f = Cursor::new(data);
for _ in 0..count {
inputs.push(f.read_u16::<LE>()?);
}
self.keylist = inputs;
}
Ok(())
}
}
impl GameEntity<(&mut Context, &mut Player)> for Replay {
fn tick(&mut self, state: &mut SharedGameState, (ctx, player): (&mut Context, &mut Player)) -> GameResult {
match state.replay_state {
ReplayState::Recording => {
// This mimics the KeyState bitfield
let inputs = player.controller.move_left() as u16
+ ((player.controller.move_right() as u16) << 1)
+ ((player.controller.move_up() as u16) << 2)
+ ((player.controller.move_down() as u16) << 3)
+ ((player.controller.trigger_map() as u16) << 4)
+ ((player.controller.trigger_inventory() as u16) << 5)
+ (((player.controller.jump() || player.controller.trigger_menu_ok()) as u16) << 6)
+ (((player.controller.shoot() || player.controller.trigger_menu_back()) as u16) << 7)
+ ((player.controller.next_weapon() as u16) << 8)
+ ((player.controller.prev_weapon() as u16) << 9)
+ ((player.controller.trigger_menu_ok() as u16) << 11)
+ ((player.controller.skip() as u16) << 12)
+ ((player.controller.strafe() as u16) << 13);
self.keylist.push(inputs);
}
ReplayState::Playback(_) => {
let pause = ctx.keyboard_context.is_key_pressed(ScanCode::Escape) && (self.tick - self.resume_tick > 3);
let next_input = if pause { 1 << 10 } else { *self.keylist.get(self.tick).unwrap_or(&0) };
self.controller.state = KeyState(next_input);
self.controller.old_state = self.last_input;
player.controller = Box::new(self.controller);
if !pause {
self.last_input = KeyState(next_input);
self.tick += 1;
} else {
self.resume_tick = self.tick;
};
if self.tick >= self.keylist.len() {
state.replay_state = ReplayState::None;
player.controller = state.settings.create_player1_controller();
}
}
ReplayState::None => {}
}
Ok(())
}
fn draw(&self, state: &mut SharedGameState, ctx: &mut Context, _frame: &Frame) -> GameResult {
let x = state.canvas_size.0 - 32.0;
let y = 8.0 + if state.settings.fps_counter { 12.0 } else { 0.0 };
match state.replay_state {
ReplayState::None => {}
ReplayState::Playback(_) => {
state.font.builder()
.position(x, y)
.draw("PLAY", ctx, &state.constants, &mut state.texture_set)?;
}
ReplayState::Recording => {
state.font.builder()
.position(x, y)
.draw("REC", ctx, &state.constants, &mut state.texture_set)?;
}
}
Ok(())
}
}

View File

@ -1,13 +1,12 @@
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::common::Rect;
use crate::entity::GameEntity;
use crate::frame::Frame;
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::game::frame::Frame;
use crate::game::shared_game_state::SharedGameState;
use crate::input::touch_controls::TouchControlType;
use crate::player::Player;
use crate::shared_game_state::SharedGameState;
use crate::scripting::tsc::text_script::ScriptMode;
use crate::game::player::Player;
use crate::game::scripting::tsc::text_script::ScriptMode;
pub struct StageSelect {
pub current_teleport_slot: u8,

View File

@ -0,0 +1,278 @@
use crate::common::{Color, Rect};
use crate::engine_constants::AnimatedFace;
use crate::entity::GameEntity;
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::framework::graphics;
use crate::game::frame::Frame;
use crate::game::scripting::tsc::text_script::{ConfirmSelection, TextScriptExecutionState, TextScriptLine};
use crate::game::shared_game_state::SharedGameState;
use crate::graphics::font::{Font, Symbols};
pub struct TextBoxes {
pub slide_in: u8,
pub anim_counter: usize,
animated_face: AnimatedFace,
}
const FACE_TEX: &str = "Face";
const SWITCH_FACE_TEX: [&str; 5] = ["Face1", "Face2", "Face3", "Face4", "Face5"];
impl TextBoxes {
pub fn new() -> TextBoxes {
TextBoxes {
slide_in: 7,
anim_counter: 0,
animated_face: AnimatedFace { face_id: 0, anim_id: 0, anim_frames: vec![(0, 0)] },
}
}
}
impl GameEntity<()> for TextBoxes {
fn tick(&mut self, state: &mut SharedGameState, _custom: ()) -> GameResult {
if state.textscript_vm.face != 0 {
self.slide_in = self.slide_in.saturating_sub(1);
self.anim_counter = self.anim_counter.wrapping_add(1);
let face_num = state.textscript_vm.face % 100;
let animation = state.textscript_vm.face % 1000 / 100;
if state.constants.textscript.animated_face_pics
&& !state.settings.original_textures
&& (self.animated_face.anim_id != animation || self.animated_face.face_id != face_num)
{
self.animated_face = state
.constants
.animated_face_table
.clone()
.into_iter()
.find(|face| face.face_id == face_num && face.anim_id == animation)
.unwrap_or_else(|| AnimatedFace { face_id: face_num, anim_id: 0, anim_frames: vec![(0, 0)] });
}
if self.anim_counter > self.animated_face.anim_frames.first().unwrap().1 as usize {
self.animated_face.anim_frames.rotate_left(1);
self.anim_counter = 0;
}
}
Ok(())
}
fn draw(&self, state: &mut SharedGameState, ctx: &mut Context, _frame: &Frame) -> GameResult {
if !state.textscript_vm.flags.render() {
return Ok(());
}
let (off_left, off_top, off_right, off_bottom) =
crate::framework::graphics::screen_insets_scaled(ctx, state.scale);
let center = ((state.canvas_size.0 - off_left - off_right) / 2.0).floor();
let top_pos = if state.textscript_vm.flags.position_top() {
32.0 + off_top
} else {
state.canvas_size.1 as f32 - off_bottom - 66.0
};
let left_pos = off_left + center - 122.0;
{
let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, "TextBox")?;
if state.textscript_vm.flags.background_visible() {
batch.add_rect(left_pos, top_pos, &state.constants.textscript.textbox_rect_top);
for i in 1..7 {
batch.add_rect(left_pos, top_pos + i as f32 * 8.0, &state.constants.textscript.textbox_rect_middle);
}
batch.add_rect(left_pos, top_pos + 56.0, &state.constants.textscript.textbox_rect_bottom);
}
if state.textscript_vm.item != 0 {
batch.add_rect(
center - 40.0,
state.canvas_size.1 - off_bottom - 112.0,
&state.constants.textscript.get_item_top_left,
);
batch.add_rect(
center - 40.0,
state.canvas_size.1 - off_bottom - 96.0,
&state.constants.textscript.get_item_bottom_left,
);
batch.add_rect(
center + 32.0,
state.canvas_size.1 - off_bottom - 112.0,
&state.constants.textscript.get_item_top_right,
);
batch.add_rect(
center + 32.0,
state.canvas_size.1 - off_bottom - 104.0,
&state.constants.textscript.get_item_right,
);
batch.add_rect(
center + 32.0,
state.canvas_size.1 - off_bottom - 96.0,
&state.constants.textscript.get_item_right,
);
batch.add_rect(
center + 32.0,
state.canvas_size.1 - off_bottom - 88.0,
&state.constants.textscript.get_item_bottom_right,
);
}
if let TextScriptExecutionState::WaitConfirmation(_, _, _, wait, selection) = state.textscript_vm.state {
let pos_y = if wait > 14 {
state.canvas_size.1 - off_bottom - 96.0 + 4.0 * (17 - wait) as f32
} else {
state.canvas_size.1 - off_bottom - 96.0
};
batch.add_rect(center + 56.0, pos_y, &state.constants.textscript.textbox_rect_yes_no);
if wait == 0 {
let pos_x = if selection == ConfirmSelection::No { 41.0 } else { 0.0 };
batch.add_rect(
center + 51.0 + pos_x,
pos_y + 10.0,
&state.constants.textscript.textbox_rect_cursor,
);
}
}
batch.draw(ctx)?;
}
if state.textscript_vm.face != 0 {
let clip_rect = Rect::new_size(
((left_pos + 14.0) * state.scale) as isize,
((top_pos + 8.0) * state.scale) as isize,
(48.0 * state.scale) as isize,
(48.0 * state.scale) as isize,
);
graphics::set_clip_rect(ctx, Some(clip_rect))?;
// switch version uses 1xxx flag to show a flipped version of face
let flip = state.textscript_vm.face > 1000;
let face_num = state.textscript_vm.face % 100;
let animation_frame = self.animated_face.anim_frames.first().unwrap().0 as usize;
let tex_name = if state.constants.textscript.animated_face_pics && !state.settings.original_textures {
SWITCH_FACE_TEX[animation_frame]
} else {
FACE_TEX
};
let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, tex_name)?;
let face_x = (4.0 + (6 - self.slide_in) as f32 * 8.0) - 52.0;
let final_x = left_pos + 14.0 + face_x;
let final_y = top_pos + 8.0;
let rect = Rect::new_size((face_num as u16 % 6) * 48, (face_num as u16 / 6) * 48, 48, 48);
if face_num >= 1 && face_num <= 4 && state.more_rust {
// sue
batch.add_rect_flip_tinted(final_x, final_y, flip, false, (200, 200, 255, 255), &rect);
} else {
batch.add_rect_flip(final_x, final_y, flip, false, &rect);
}
batch.draw(ctx)?;
graphics::set_clip_rect(ctx, None)?;
}
if state.textscript_vm.item != 0 {
let mut rect = Rect::new(0, 0, 0, 0);
if state.textscript_vm.item < 1000 {
let item_id = state.textscript_vm.item as u16;
rect.left = (item_id % 16) * 16;
rect.right = rect.left + 16;
rect.top = (item_id / 16) * 16;
rect.bottom = rect.top + 16;
let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, "ArmsImage")?;
batch.add_rect((center - 12.0).floor(), state.canvas_size.1 - off_bottom - 104.0, &rect);
batch.draw(ctx)?;
} else {
let item_id = state.textscript_vm.item as u16 - 1000;
rect.left = (item_id % 8) * 32;
rect.right = rect.left + 32;
rect.top = (item_id / 8) * 16;
rect.bottom = rect.top + 16;
let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, "ItemImage")?;
batch.add_rect((center - 20.0).floor(), state.canvas_size.1 - off_bottom - 104.0, &rect);
batch.draw(ctx)?;
}
}
let text_offset = if state.textscript_vm.face == 0 { 0.0 } else { 56.0 };
let y_offset = if let TextScriptExecutionState::MsgNewLine(_, _, _, _, counter) = state.textscript_vm.state {
16.0 - counter as f32 * 4.0
} else {
0.0
};
let lines = [&state.textscript_vm.line_1, &state.textscript_vm.line_2, &state.textscript_vm.line_3];
let clip_rect = Rect::new_size(
0,
((top_pos + 6.0) * state.scale) as isize,
state.screen_size.0 as isize,
(48.0 * state.scale) as isize,
);
graphics::set_clip_rect(ctx, Some(clip_rect))?;
for (idx, line) in lines.iter().enumerate() {
if !line.is_empty() {
let symbols = Symbols { symbols: &state.textscript_vm.substitution_rect_map, texture: "TextBox" };
state
.font
.builder()
.position(left_pos + text_offset + 14.0, top_pos + 10.0 + idx as f32 * 16.0 - y_offset)
.shadow(state.constants.textscript.text_shadow)
.with_symbols(Some(symbols))
.draw_iter(line.iter().copied(), ctx, &state.constants, &mut state.texture_set)?;
}
}
graphics::set_clip_rect(ctx, None)?;
if let TextScriptExecutionState::WaitInput(_, _, tick) = state.textscript_vm.state {
if tick > 10 {
let builder = state
.font
.builder()
.with_symbols(Some(Symbols { symbols: &state.textscript_vm.substitution_rect_map, texture: "" }));
let (mut x, y) = match state.textscript_vm.current_line {
TextScriptLine::Line1 => {
(builder.compute_width_iter(state.textscript_vm.line_1.iter().copied()), top_pos + 10.0)
}
TextScriptLine::Line2 => {
(builder.compute_width_iter(state.textscript_vm.line_2.iter().copied()), top_pos + 10.0 + 16.0)
}
TextScriptLine::Line3 => {
(builder.compute_width_iter(state.textscript_vm.line_3.iter().copied()), top_pos + 10.0 + 32.0)
}
};
x += left_pos + text_offset + 14.0;
graphics::draw_rect(
ctx,
Rect::new_size(
(x * state.scale) as isize,
(y * state.scale) as isize,
(5.0 * state.scale) as isize,
(state.font.line_height() * state.scale) as isize,
),
Color::from_rgb(255, 255, 255),
)?;
}
}
Ok(())
}
}

234
src/components/tilemap.rs Normal file
View File

@ -0,0 +1,234 @@
use crate::common::Rect;
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::game::frame::Frame;
use crate::game::shared_game_state::{SharedGameState, TileSize};
use crate::game::stage::{BackgroundType, Stage, StageTexturePaths};
pub struct Tilemap {
tick: u32,
prev_tick: u32,
pub no_water: bool,
}
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
pub enum TileLayer {
Background,
Middleground,
Foreground,
Snack,
}
impl Tilemap {
pub fn new() -> Self {
Tilemap { tick: 0, prev_tick: 0, no_water: false }
}
pub fn tick(&mut self) -> GameResult {
self.tick = self.tick.wrapping_add(1);
Ok(())
}
pub fn set_prev(&mut self) -> GameResult {
self.prev_tick = self.tick;
Ok(())
}
pub fn draw(
&self,
state: &mut SharedGameState,
ctx: &mut Context,
frame: &Frame,
layer: TileLayer,
textures: &StageTexturePaths,
stage: &Stage,
) -> GameResult {
if stage.map.tile_size == TileSize::Tile8x8 && layer == TileLayer::Snack {
return Ok(());
}
let tex = match layer {
TileLayer::Snack => "Npc/NpcSym",
TileLayer::Background => &textures.tileset_bg,
TileLayer::Middleground => &textures.tileset_mg,
TileLayer::Foreground => &textures.tileset_fg,
};
let (layer_offset, layer_width, layer_height, uses_layers) = if let Some(pxpack_data) = &stage.data.pxpack_data
{
match layer {
TileLayer::Background => {
(pxpack_data.offset_bg as usize, pxpack_data.size_bg.0, pxpack_data.size_bg.1, true)
}
TileLayer::Middleground => {
(pxpack_data.offset_mg as usize, pxpack_data.size_mg.0, pxpack_data.size_mg.1, true)
}
_ => (0, pxpack_data.size_fg.0, pxpack_data.size_fg.1, true),
}
} else {
(0, stage.map.width, stage.map.height, false)
};
if !uses_layers && layer == TileLayer::Middleground {
return Ok(());
}
let tile_size = state.tile_size.as_int();
let tile_sizef = state.tile_size.as_float();
let halft = tile_size / 2;
let halftf = tile_sizef / 2.0;
let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, tex)?;
let mut rect = Rect::new(0, 0, tile_size as u16, tile_size as u16);
let (mut frame_x, mut frame_y) = frame.xy_interpolated(state.frame_time);
if let Some(pxpack_data) = &stage.data.pxpack_data {
let (fx, fy) = match layer {
TileLayer::Background => pxpack_data.scroll_bg.transform_camera_pos(frame_x, frame_y),
TileLayer::Middleground => pxpack_data.scroll_mg.transform_camera_pos(frame_x, frame_y),
_ => pxpack_data.scroll_fg.transform_camera_pos(frame_x, frame_y),
};
frame_x = fx;
frame_y = fy;
}
let tile_start_x = (frame_x as i32 / tile_size).clamp(0, layer_width as i32) as usize;
let tile_start_y = (frame_y as i32 / tile_size).clamp(0, layer_height as i32) as usize;
let tile_end_x =
((frame_x as i32 + 8 + state.canvas_size.0 as i32) / tile_size + 1).clamp(0, layer_width as i32) as usize;
let tile_end_y = ((frame_y as i32 + halft + state.canvas_size.1 as i32) / tile_size + 1)
.clamp(0, layer_height as i32) as usize;
if layer == TileLayer::Snack {
rect = state.constants.world.snack_rect;
}
for y in tile_start_y..tile_end_y {
for x in tile_start_x..tile_end_x {
let tile = *stage.map.tiles.get((y * layer_width as usize) + x + layer_offset).unwrap();
match layer {
_ if uses_layers => {
if tile == 0 {
continue;
}
let tile_size = tile_size as u16;
rect.left = (tile as u16 % 16) * tile_size;
rect.top = (tile as u16 / 16) * tile_size;
rect.right = rect.left + tile_size;
rect.bottom = rect.top + tile_size;
}
TileLayer::Background => {
if stage.map.attrib[tile as usize] >= 0x20 {
continue;
}
let tile_size = tile_size as u16;
rect.left = (tile as u16 % 16) * tile_size;
rect.top = (tile as u16 / 16) * tile_size;
rect.right = rect.left + tile_size;
rect.bottom = rect.top + tile_size;
}
TileLayer::Foreground => {
let attr = stage.map.attrib[tile as usize];
if attr < 0x40 || attr >= 0x80 {
continue;
}
let tile_size = tile_size as u16;
rect.left = (tile as u16 % 16) * tile_size;
rect.top = (tile as u16 / 16) * tile_size;
rect.right = rect.left + tile_size;
rect.bottom = rect.top + tile_size;
}
TileLayer::Snack => {
if stage.map.attrib[tile as usize] != 0x43 {
continue;
}
}
_ => {}
}
batch.add_rect(
(x as f32 * tile_sizef - halftf) - frame_x,
(y as f32 * tile_sizef - halftf) - frame_y,
&rect,
);
}
}
batch.draw(ctx)?;
if !self.no_water && layer == TileLayer::Foreground && stage.data.background_type == BackgroundType::Water {
let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, &textures.background)?;
let rect_top = Rect { left: 0, top: 0, right: 32, bottom: 16 };
let rect_middle = Rect { left: 0, top: 16, right: 32, bottom: 48 };
let tile_start_x = frame_x as i32 / 32;
let tile_end_x = (frame_x + 16.0 + state.canvas_size.0) as i32 / 32 + 1;
let water_y = state.water_level as f32 / 512.0;
let tile_count_y = (frame_y + 16.0 + state.canvas_size.1 - water_y) as i32 / 32 + 1;
for x in tile_start_x..tile_end_x {
batch.add_rect((x as f32 * 32.0) - frame_x, water_y - frame_y, &rect_top);
for y in 0..tile_count_y {
batch.add_rect((x as f32 * 32.0) - frame_x, (y as f32 * 32.0) + water_y - frame_y, &rect_middle);
}
}
batch.draw(ctx)?;
}
if layer == TileLayer::Foreground {
let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, "Caret")?;
for y in tile_start_y..tile_end_y {
for x in tile_start_x..tile_end_x {
let tile = *stage.map.tiles.get((y * layer_width as usize) + x + layer_offset).unwrap();
let attr = stage.map.attrib[tile as usize];
if ![0x80, 0x81, 0x82, 0x83, 0xA0, 0xA1, 0xA2, 0xA3].contains(&attr) {
continue;
}
let shift =
((self.tick as f64 + (self.tick - self.prev_tick) as f64 * state.frame_time) * 2.0) as u16 % 16;
let mut push_rect = state.constants.world.water_push_rect;
match attr {
0x80 | 0xA0 => {
push_rect.left = push_rect.left + shift;
push_rect.right = push_rect.right + shift;
}
0x81 | 0xA1 => {
push_rect.top = push_rect.top + shift;
push_rect.bottom = push_rect.bottom + shift;
}
0x82 | 0xA2 => {
push_rect.left = push_rect.left - shift + state.tile_size.as_int() as u16;
push_rect.right = push_rect.right - shift + state.tile_size.as_int() as u16;
}
0x83 | 0xA3 => {
push_rect.top = push_rect.top - shift + state.tile_size.as_int() as u16;
push_rect.bottom = push_rect.bottom - shift + state.tile_size.as_int() as u16;
}
_ => (),
}
batch.add_rect(
(x as f32 * tile_sizef - halftf) - frame_x,
(y as f32 * tile_sizef - halftf) - frame_y,
&push_rect,
);
}
}
batch.draw(ctx)?;
}
Ok(())
}
}

View File

@ -1,20 +1,29 @@
use std::cell::RefCell;
use crate::common::{Color, Rect};
use crate::entity::GameEntity;
use crate::frame::Frame;
use crate::framework::backend::{BackendShader, VertexData};
use crate::framework::backend::{BackendShader, SpriteBatchCommand, VertexData};
use crate::framework::context::Context;
use crate::framework::error::GameResult;
use crate::framework::graphics;
use crate::map::WaterRegionType;
use crate::player::Player;
use crate::shared_game_state::SharedGameState;
use crate::physics::PhysicalEntity;
use crate::npc::list::NPCList;
use crate::framework::graphics::BlendMode;
use crate::game::frame::Frame;
use crate::game::map::{WaterParamEntry, WaterParams, WaterRegionType};
use crate::game::physics::PhysicalEntity;
use crate::game::shared_game_state::SharedGameState;
use crate::game::stage::{BackgroundType, Stage};
use crate::game::npc::list::NPCList;
use crate::game::player::Player;
const TENSION: f32 = 0.03;
const DAMPENING: f32 = 0.01;
const SPREAD: f32 = 0.02;
#[derive(Copy, Clone, Eq, PartialEq)]
pub enum WaterLayer {
Front,
Back,
}
struct DynamicWaterColumn {
target_height: f32,
height: f32,
@ -33,14 +42,15 @@ impl DynamicWaterColumn {
}
pub struct DynamicWater {
x: u16,
y: u16,
end_x: u16,
x: f32,
y: f32,
end_x: f32,
columns: Vec<DynamicWaterColumn>,
color: WaterParamEntry,
}
impl DynamicWater {
pub fn new(x: u16, y: u16, length: u16) -> DynamicWater {
pub fn new(x: u16, y: u16, length: u16, color: WaterParamEntry) -> DynamicWater {
let mut columns = Vec::new();
let count = length as usize * 8 + 1;
@ -48,11 +58,11 @@ impl DynamicWater {
columns.push(DynamicWaterColumn::new());
}
DynamicWater { x, y, end_x: x + length, columns }
DynamicWater { x: x as f32 * 16.0, y: y as f32 * 16.0, end_x: (x + length) as f32 * 16.0, columns, color }
}
pub fn tick(&mut self) {
for col in self.columns.iter_mut() {
for col in &mut self.columns {
col.tick(DAMPENING, TENSION);
}
@ -89,156 +99,259 @@ impl DynamicWater {
}
}
}
pub fn interact(&mut self, players: &[&Player], npc_list: &NPCList) {
let cols_i32 = self.columns.len() as i32;
let mut tick_object = |obj: &dyn PhysicalEntity| {
let obj_x = obj.x() as f32 / 512.0 + 8.0;
let obj_y = obj.y() as f32 / 512.0 + 8.0;
if (obj.vel_y() > 0x80 || obj.vel_y() < -0x80)
&& obj_x > self.x
&& obj_x < self.end_x as f32
&& obj_y > self.y - 5.0
&& obj_y < self.y + 4.0
{
let col_idx_center = (((obj_x - self.x) / 2.0) as i32).clamp(0, cols_i32);
let col_idx_left =
(col_idx_center - (obj.hit_bounds().left as i32 / (8 * 0x200))).clamp(0, cols_i32) as usize;
let col_idx_right =
(col_idx_center + (obj.hit_bounds().left as i32 / (8 * 0x200))).clamp(0, cols_i32) as usize;
for col in &mut self.columns[col_idx_left..=col_idx_right] {
col.speed = (obj.vel_y() as f32 / 512.0) * (obj.hit_rect_size() as f32 * 0.25).clamp(0.1, 1.0);
}
}
};
for player in players {
tick_object(*player);
}
for npc in npc_list.iter_alive() {
static NO_COLL_NPCS: [u16; 6] = [0, 3, 4, 18, 191, 195];
if NO_COLL_NPCS.contains(&npc.npc_type) {
continue;
}
tick_object(npc);
}
}
}
pub struct DepthRegion {
rect: Rect<f32>,
color: WaterParamEntry,
}
impl DepthRegion {
pub fn new_tile(rect: Rect<u16>, color: WaterParamEntry) -> DepthRegion {
DepthRegion {
rect: Rect {
left: rect.left as f32 * 16.0,
top: rect.top as f32 * 16.0,
right: rect.right as f32 * 16.0,
bottom: rect.bottom as f32 * 16.0,
},
color,
}
}
pub fn new(rect: Rect<f32>, color: WaterParamEntry) -> DepthRegion {
DepthRegion { rect, color }
}
}
pub struct WaterRenderer {
depth_regions: Vec<Rect<u16>>,
surf_regions: Vec<Rect<u16>>,
depth_regions: Vec<DepthRegion>,
water_surfaces: Vec<DynamicWater>,
core_water: Option<(DynamicWater, DepthRegion)>,
t: RefCell<u32>,
}
impl WaterRenderer {
pub fn new() -> WaterRenderer {
WaterRenderer { depth_regions: Vec::new(), surf_regions: Vec::new(), water_surfaces: Vec::new() }
WaterRenderer { depth_regions: Vec::new(), water_surfaces: Vec::new(), core_water: None, t: RefCell::new(0) }
}
pub fn initialize(&mut self, regions: Vec<(WaterRegionType, Rect<u16>)>) {
for (reg_type, bounds) in regions {
pub fn initialize(
&mut self,
regions: Vec<(WaterRegionType, Rect<u16>, u8)>,
water_params: &WaterParams,
stage: &Stage,
) {
for (reg_type, bounds, color_idx) in regions {
let color = water_params.get_entry(color_idx);
match reg_type {
WaterRegionType::WaterLine => {
self.surf_regions.push(bounds);
self.water_surfaces.push(DynamicWater::new(bounds.left, bounds.top, bounds.width() + 1));
self.water_surfaces.push(DynamicWater::new(bounds.left, bounds.top, bounds.width() + 1, *color));
}
WaterRegionType::WaterDepth => {
self.depth_regions.push(bounds);
self.depth_regions.push(DepthRegion::new_tile(bounds, *color));
}
}
}
if stage.data.background_type == BackgroundType::Water {
let core_water_color = water_params.get_entry(0);
self.core_water = Some((
DynamicWater::new(0, 32768, stage.map.width, *core_water_color),
DepthRegion::new(
Rect {
left: 0.0,
top: stage.map.height as f32 * 16.0,
right: stage.map.width as f32 * 16.0,
bottom: stage.map.height as f32 * 16.0 + 1.0,
},
*core_water_color,
),
));
}
}
}
impl GameEntity<(&[&Player], &NPCList)> for WaterRenderer {
fn tick(&mut self, state: &mut SharedGameState, (players, npc_list): (&[&Player], &NPCList)) -> GameResult<()> {
if !state.settings.shader_effects {
return Ok(());
}
for surf in self.water_surfaces.iter_mut() {
let line_x = surf.x as f32 * 16.0;
let line_y = surf.y as f32 * 16.0;
let mut tick_object = |obj: &dyn PhysicalEntity| {
let obj_x = obj.x() as f32 / 512.0 + 8.0;
let obj_y = obj.y() as f32 / 512.0 + 8.0;
if (obj.vel_y() > 0x80 || obj.vel_y() < -0x80)
&& obj_x > line_x
&& obj_x < surf.end_x as f32 * 16.0
&& obj_y > line_y - 5.0
&& obj_y < line_y + 4.0
{
let col_idx_center = (((obj_x - line_x) / 2.0) as i32).clamp(0, surf.columns.len() as i32);
let col_idx_left = (col_idx_center - (obj.hit_bounds().left as i32 / (8 * 0x200)))
.clamp(0, surf.columns.len() as i32) as usize;
let col_idx_right = (col_idx_center + (obj.hit_bounds().left as i32 / (8 * 0x200)))
.clamp(0, surf.columns.len() as i32) as usize;
for col in surf.columns[col_idx_left..=col_idx_right].iter_mut() {
col.speed = (obj.vel_y() as f32 / 512.0) * (obj.hit_rect_size() as f32 * 0.25).clamp(0.1, 1.0);
}
}
};
for player in players {
tick_object(*player);
}
for npc in npc_list.iter_alive() {
static NO_COLL_NPCS: [u16; 3] = [0, 3, 4];
if NO_COLL_NPCS.contains(&npc.npc_type) {
continue;
}
tick_object(npc);
}
pub fn tick(&mut self, state: &mut SharedGameState, (players, npc_list): (&[&Player], &NPCList)) -> GameResult<()> {
for surf in &mut self.water_surfaces {
surf.interact(players, npc_list);
surf.tick();
}
if let Some((ref mut core_water, ref mut core_depth)) = &mut self.core_water {
let level = state.water_level as f32 / 512.0 + 8.0;
core_water.y = level;
core_depth.rect.top = (level + 16.0).min(core_depth.rect.bottom);
core_water.interact(players, npc_list);
core_water.tick();
}
let mut t_ref = self.t.borrow_mut();
*t_ref = t_ref.wrapping_add(1);
Ok(())
}
fn draw(&self, state: &mut SharedGameState, ctx: &mut Context, frame: &Frame) -> GameResult<()> {
let mut out_rect = Rect::new(0, 0, 0, 0);
let (o_x, o_y) = frame.xy_interpolated(state.frame_time);
let water_color_top = Color::from_rgba(102, 153, 204, 150);
let water_color = Color::from_rgba(102, 153, 204, 75);
for region in self.depth_regions.iter() {
out_rect.left = ((region.left as f32 * 16.0 - o_x - 8.0) * state.scale) as isize;
out_rect.top = ((region.top as f32 * 16.0 - o_y - 8.0) * state.scale) as isize;
out_rect.right = ((region.right as f32 * 16.0 - o_x + 8.0) * state.scale) as isize;
out_rect.bottom = ((region.bottom as f32 * 16.0 - o_y + 8.0) * state.scale) as isize;
graphics::draw_rect(ctx, out_rect, water_color)?;
}
if !state.settings.shader_effects || !graphics::supports_vertex_draw(ctx)? {
for region in self.surf_regions.iter() {
out_rect.left = ((region.left as f32 * 16.0 - o_x - 8.0) * state.scale) as isize;
out_rect.top = ((region.top as f32 * 16.0 - o_y - 5.0) * state.scale) as isize;
out_rect.right = ((region.right as f32 * 16.0 - o_x + 8.0) * state.scale) as isize;
out_rect.bottom = ((region.bottom as f32 * 16.0 - o_y + 8.0) * state.scale) as isize;
graphics::draw_rect(ctx, out_rect, water_color)?;
}
pub fn draw(
&self,
state: &mut SharedGameState,
ctx: &mut Context,
frame: &Frame,
layer: WaterLayer,
) -> GameResult<()> {
if !graphics::supports_vertex_draw(ctx)? {
return Ok(());
}
graphics::set_render_target(ctx, state.lightmap_canvas.as_ref())?;
graphics::clear(ctx, Color::from_rgba(0, 0, 0, 0));
graphics::set_blend_mode(ctx, BlendMode::None)?;
let (o_x, o_y) = frame.xy_interpolated(state.frame_time);
let uv = (0.0, 0.0);
let color_top_rgba = water_color_top.to_rgba();
let color_mid_rgba = water_color.to_rgba();
let color_btm_rgba = water_color.to_rgba();
let t = *self.t.borrow_mut() as f32 + state.frame_time as f32;
let shader = BackendShader::WaterFill(state.scale, t, (o_x, o_y));
let mut vertices = Vec::new();
for surf in self.water_surfaces.iter() {
let pos_x = surf.x as f32 * 16.0;
let pos_y = surf.y as f32 * 16.0;
{
let mut draw_region = |region: &DepthRegion| -> GameResult {
let color_mid_rgba = region.color.color_middle.to_rgba();
let color_btm_rgba = region.color.color_bottom.to_rgba();
vertices.clear();
vertices.reserve(6);
if (pos_x - o_x - 16.0) > state.canvas_size.0
|| (pos_x - o_x + 16.0 + surf.end_x as f32 * 16.0) < 0.0
|| (pos_y - o_y - 16.0) > state.canvas_size.1
|| (pos_y - o_y + 16.0) < 0.0
{
continue;
let left = (region.rect.left - o_x - 8.0) * state.scale;
let top = (region.rect.top - o_y - 8.0) * state.scale;
let right = (region.rect.right - o_x + 8.0) * state.scale;
let bottom = (region.rect.bottom - o_y + 8.0) * state.scale;
vertices.push(VertexData { position: (left, bottom), uv, color: color_btm_rgba });
vertices.push(VertexData { position: (left, top), uv, color: color_mid_rgba });
vertices.push(VertexData { position: (right, top), uv, color: color_mid_rgba });
vertices.push(VertexData { position: (left, bottom), uv, color: color_btm_rgba });
vertices.push(VertexData { position: (right, top), uv, color: color_mid_rgba });
vertices.push(VertexData { position: (right, bottom), uv, color: color_btm_rgba });
graphics::draw_triangle_list(ctx, &vertices, None, shader)?;
Ok(())
};
if layer == WaterLayer::Back {
for region in &self.depth_regions {
draw_region(region)?;
}
} else if let Some((_, ref core_depth)) = &self.core_water {
draw_region(core_depth)?;
}
}
let mut vertices = vec![];
vertices.reserve(12 * surf.columns.len());
{
let mut draw_region = |surf: &DynamicWater| -> GameResult {
let pos_x = surf.x;
let pos_y = surf.y;
let color_top_rgba = surf.color.color_top.to_rgba();
let color_mid_rgba = surf.color.color_middle.to_rgba();
let color_btm_rgba = surf.color.color_bottom.to_rgba();
let bottom = (pos_y - o_y + 8.0) * state.scale;
for i in 1..surf.columns.len() {
let x_right = (pos_x - 8.0 - o_x + i as f32 * 2.0) * state.scale;
let x_left = x_right - 2.0 * state.scale;
let top_left = (pos_y - o_y - 13.0 + surf.columns[i - 1].height) * state.scale;
let top_right = (pos_y - o_y - 13.0 + surf.columns[i].height) * state.scale;
let middle_left = top_left + 6.0 * state.scale;
let middle_right = top_left + 6.0 * state.scale;
if (pos_x - o_x - 16.0) > state.canvas_size.0
|| (pos_x - o_x + 16.0 + surf.end_x) < 0.0
|| (pos_y - o_y - 16.0) > state.canvas_size.1
|| (pos_y - o_y + 16.0) < 0.0
{
return Ok(());
}
vertices.push(VertexData { position: (x_left, middle_left), uv, color: color_mid_rgba });
vertices.push(VertexData { position: (x_left, top_left), uv, color: color_top_rgba });
vertices.push(VertexData { position: (x_right, top_right), uv, color: color_top_rgba });
vertices.push(VertexData { position: (x_left, middle_left), uv, color: color_mid_rgba });
vertices.push(VertexData { position: (x_right, top_right), uv, color: color_top_rgba });
vertices.push(VertexData { position: (x_right, middle_right), uv, color: color_mid_rgba });
vertices.clear();
vertices.reserve(12 * surf.columns.len());
vertices.push(VertexData { position: (x_left, bottom), uv, color: color_btm_rgba });
vertices.push(VertexData { position: (x_left, middle_left), uv, color: color_mid_rgba });
vertices.push(VertexData { position: (x_right, middle_right), uv, color: color_mid_rgba });
vertices.push(VertexData { position: (x_left, bottom), uv, color: color_btm_rgba });
vertices.push(VertexData { position: (x_right, middle_right), uv, color: color_mid_rgba });
vertices.push(VertexData { position: (x_right, bottom), uv, color: color_btm_rgba });
let bottom = (pos_y - o_y + 8.0) * state.scale;
for i in 1..surf.columns.len() {
let x_right = (pos_x - 8.0 - o_x + i as f32 * 2.0) * state.scale;
let x_left = x_right - 2.0 * state.scale;
let top_left = (pos_y - o_y - 13.0 + surf.columns[i - 1].height) * state.scale;
let top_right = (pos_y - o_y - 13.0 + surf.columns[i].height) * state.scale;
let middle_left = top_left + 6.0 * state.scale;
let middle_right = top_left + 6.0 * state.scale;
vertices.push(VertexData { position: (x_left, middle_left), uv, color: color_mid_rgba });
vertices.push(VertexData { position: (x_left, top_left), uv, color: color_top_rgba });
vertices.push(VertexData { position: (x_right, top_right), uv, color: color_top_rgba });
vertices.push(VertexData { position: (x_left, middle_left), uv, color: color_mid_rgba });
vertices.push(VertexData { position: (x_right, top_right), uv, color: color_top_rgba });
vertices.push(VertexData { position: (x_right, middle_right), uv, color: color_mid_rgba });
vertices.push(VertexData { position: (x_left, bottom), uv, color: color_btm_rgba });
vertices.push(VertexData { position: (x_left, middle_left), uv, color: color_mid_rgba });
vertices.push(VertexData { position: (x_right, middle_right), uv, color: color_mid_rgba });
vertices.push(VertexData { position: (x_left, bottom), uv, color: color_btm_rgba });
vertices.push(VertexData { position: (x_right, middle_right), uv, color: color_mid_rgba });
vertices.push(VertexData { position: (x_right, bottom), uv, color: color_btm_rgba });
}
graphics::draw_triangle_list(ctx, &vertices, None, shader)?;
Ok(())
};
if layer == WaterLayer::Back {
for surf in &self.water_surfaces {
draw_region(surf)?;
}
} else if let Some((ref surf, _)) = &self.core_water {
draw_region(surf)?;
}
}
graphics::draw_triangle_list(ctx, vertices, None, BackendShader::Fill)?;
graphics::set_blend_mode(ctx, BlendMode::Alpha)?;
graphics::set_render_target(ctx, None)?;
{
let canvas = state.lightmap_canvas.as_mut().unwrap();
let rect = Rect { left: 0.0, top: 0.0, right: state.screen_size.0, bottom: state.screen_size.1 };
canvas.clear();
canvas.add(SpriteBatchCommand::DrawRect(rect, rect));
canvas.draw()?;
}
Ok(())

View File

@ -0,0 +1,138 @@
use crate::common::{Direction, interpolate_fix9_scale, Rect};
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::game::player::{Player, TargetPlayer};
use crate::game::weapon::bullet::{Bullet, BulletManager};
pub struct WhimsicalStar {
pub star: [Star; 3],
pub tex: String,
pub star_count: u8,
pub equipped: bool,
pub active_star: u8,
}
pub struct Star {
pub x: i32,
pub y: i32,
pub prev_x: i32,
pub prev_y: i32,
pub vel_x: i32,
pub vel_y: i32,
pub rect: Rect<u16>,
}
impl Star {
fn new(vel_x: i32, vel_y: i32) -> Star {
Star { x: 0, y: 0, vel_x, vel_y, prev_x: 0, prev_y: 0, rect: Rect::new(0, 0, 0, 0) }
}
}
impl WhimsicalStar {
pub fn new() -> WhimsicalStar {
WhimsicalStar {
star: [Star::new(0x400, -0x200), Star::new(-0x200, 0x400), Star::new(0x200, 0x200)],
tex: "MyChar".to_string(),
star_count: 0,
equipped: false,
active_star: 0,
}
}
pub fn init(&mut self, player: &Player) {
self.tex = player.skin.get_skin_texture_name().to_string();
for (iter, star) in &mut self.star.iter_mut().enumerate() {
star.rect = player.skin.get_whimsical_star_rect(iter);
}
}
pub fn set_prev(&mut self) {
for star in &mut self.star {
star.prev_x = star.x;
star.prev_y = star.y;
}
}
}
impl GameEntity<(&Player, &mut BulletManager)> for WhimsicalStar {
fn tick(
&mut self,
state: &mut SharedGameState,
(player, bullet_manager): (&Player, &mut BulletManager),
) -> GameResult {
if !self.equipped && player.equip.has_whimsical_star() {
for star in &mut self.star {
star.x = player.x;
star.y = player.y;
}
self.equipped = true;
}
if !player.equip.has_whimsical_star() {
self.equipped = false;
return Ok(());
}
self.star_count = player.stars;
let mut prev_x = player.x;
let mut prev_y = player.y;
for star in &mut self.star {
star.vel_x += if prev_x >= star.x { 0x80 } else { -0x80 };
star.vel_y += if prev_y >= star.y { 0xAA } else { -0xAA };
star.vel_x = star.vel_x.clamp(-0xA00, 0xA00);
star.vel_y = star.vel_y.clamp(-0xA00, 0xA00);
star.x += star.vel_x;
star.y += star.vel_y;
prev_x = star.x;
prev_y = star.y;
}
// Only one star can deal damage per tick
self.active_star += 1;
self.active_star %= 3;
if self.active_star < self.star_count && state.control_flags.control_enabled() {
let bullet = Bullet::new(
self.star[self.active_star as usize].x,
self.star[self.active_star as usize].y,
45,
TargetPlayer::Player1,
Direction::Left,
&state.constants,
);
bullet_manager.push_bullet(bullet);
}
Ok(())
}
fn draw(&self, state: &mut SharedGameState, ctx: &mut Context, frame: &Frame) -> GameResult {
if !self.equipped {
return Ok(());
}
let (frame_x, frame_y) = frame.xy_interpolated(state.frame_time);
let batch = state.texture_set.get_or_load_batch(ctx, &state.constants, &self.tex)?;
let (active_stars, _) = self.star.split_at(self.star_count as usize);
for star in active_stars {
let x = interpolate_fix9_scale(star.prev_x as i32, star.x as i32, state.frame_time) - frame_x;
let y = interpolate_fix9_scale(star.prev_y as i32, star.y as i32, state.frame_time) - frame_y;
batch.add_rect(x, y, &star.rect);
}
batch.draw(ctx)?;
Ok(())
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 276 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 447 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 456 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 562 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 621 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Some files were not shown because too many files have changed in this diff Show More