Compare commits
947 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6cd8f0ddea | |||
| 7e25da70f0 | |||
| df3d3e7600 | |||
| 8c9af95598 | |||
| 45519ad550 | |||
| 3cca70608f | |||
| fb4eaeda10 | |||
| 69100301a1 | |||
| a5b674f9da | |||
| 4356b94483 | |||
| c8bd205d0c | |||
| 995444774b | |||
| 360b692963 | |||
| 051040371f | |||
| c5ff95edcf | |||
| d6627eda4c | |||
| 6e28cd5dca | |||
| 08ddb882af | |||
| 2f6f094559 | |||
| 6ac425144d | |||
| 73f561e256 | |||
| b739fa76c0 | |||
| 0b00faf38e | |||
| b20b1370ab | |||
| 467566c799 | |||
| d6ddf23f97 | |||
| 9423337bcc | |||
| c280119d03 | |||
| 24d0c6528d | |||
| 1fc34da2a9 | |||
| 70b9def0b0 | |||
| d03d2af5f8 | |||
| c23f85cba9 | |||
| 455ff21aff | |||
| 2da2168dac | |||
| f62b1268c8 | |||
| 82ae667611 | |||
| 2b5229e73f | |||
| 881670db91 | |||
| a542ed90ba | |||
| e9a4a592a4 | |||
| 70f7d5c028 | |||
| 46f73b7bb3 | |||
| 99d9908017 | |||
| b72d1d34b5 | |||
| f3619a00c2 | |||
| 524138ff27 | |||
| fdffb12571 | |||
| 0c941cecd4 | |||
| dd59175c71 | |||
| 114352b75f | |||
| 0e40a7a1f4 | |||
| fe6a580e20 | |||
| a910b65434 | |||
| 1f3d13adaf | |||
| 393a1013e4 | |||
| a3a20a07e1 | |||
| 1151c188a7 | |||
| b39e11b7cd | |||
| 65128e13a4 | |||
| d74662ba99 | |||
| 5f2b2c616e | |||
| 32e6e4cef0 | |||
| fc841f7525 | |||
| 12fdfa1e88 | |||
| 90eb53b00c | |||
| a34d8611bf | |||
| 73fbaaa250 | |||
| 9469e39926 | |||
| 179048a58e | |||
| e1f5c2b217 | |||
| 1d8ac1e692 | |||
| 39df5bdfa6 | |||
| 4b7a4daf2c | |||
| e1d233d026 | |||
| c22888a7fd | |||
| 6780393f45 | |||
| 530f2bdd15 | |||
| 0e4fc3c54a | |||
| 82b91e36ca | |||
| 3588a5f3fe | |||
| 992dc80aa7 | |||
| 6e7073d32b | |||
| 000e691966 | |||
| fdbc51c5d1 | |||
| 3e3566d1b4 | |||
| fcb5d1d348 | |||
| b45d783dee | |||
| d99072b44e | |||
| 0ac55b0f9e | |||
| ab48572852 | |||
| 5d2c13ed2b | |||
| d87f6b8ad1 | |||
| 24cf367758 | |||
| 829d398a94 | |||
| 3ee50a27c5 | |||
| 871087a16b | |||
| 180ce6b2e7 | |||
| cf335f67de | |||
| 7e0d81a431 | |||
| 10314bcdcf | |||
| b8ec4fe6b5 | |||
| 099a4266a8 | |||
| cec1f196be | |||
| e24410f0fb | |||
| b29b6fd7bd | |||
| f06c4d4584 | |||
| 3a997e8a62 | |||
| e6ccdd57eb | |||
| 927507f8f7 | |||
| 9bf15c33fd | |||
| 52efdabf61 | |||
| ef208035ec | |||
| 18d61779eb | |||
| d573878a2f | |||
| a66efe207d | |||
| 5198b35945 | |||
| 40977337b1 | |||
| 1598f34954 | |||
| 43cbc554c3 | |||
| 3c3e2c54c5 | |||
| 00c72c441b | |||
| 7804ef9793 | |||
| 673d8b3ad8 | |||
| 62e1296ced | |||
| a3bbd78e7b | |||
| 8dfbd1daae | |||
| 4046b28a8c | |||
| a86d57ac44 | |||
| 72f126ae26 | |||
| 5d7a8c9291 | |||
| c48fc578ca | |||
| dfc216f974 | |||
| 542856c83d | |||
| 545a99d86d | |||
| 2b6473ae03 | |||
| df0be1cba6 | |||
| c9d67582ae | |||
| 1cfd91a503 | |||
| fb738fa1ce | |||
| c0ad6d0328 | |||
| 84de8c9cb1 | |||
| 6307bc92f6 | |||
| 052dd26b3e | |||
| c8f6c7241b | |||
| b3928388ef | |||
| a38fae5a97 | |||
| 9fab992d42 | |||
| dcc388a942 | |||
| 797a6bebfe | |||
| 97a9c0f58f | |||
| 407f649e9d | |||
| 84635cdfdf | |||
| 3de45b671c | |||
| 4395792226 | |||
| a77210799f | |||
| 6f67292b0e | |||
| 78e8a31ead | |||
| b6388013ec | |||
| ba7cd8fabf | |||
| c8f7f396ce | |||
| a94fecfec8 | |||
| 2010ba71a6 | |||
| e90dff6764 | |||
| f4658b89ae | |||
| da63163216 | |||
| fceaa2b54c | |||
| be2ef46ebc | |||
| d8e86e6878 | |||
| 7e57e3174e | |||
| 5192106730 | |||
| dfeacc36b0 | |||
| 1586aec9f6 | |||
| 35d44efb8b | |||
| 500d8c2c07 | |||
| 09204daa28 | |||
| 01329758ff | |||
| 7b8cb127a8 | |||
| 8e4d5b6807 | |||
| 26e9e1d597 | |||
| f7f32d2786 | |||
| c5192eaea4 | |||
| c2906744ce | |||
| 8e6b5ae73d | |||
| 0b96ac0ab6 | |||
| f6a035947f | |||
| 941181d31c | |||
| 9ceb1a8940 | |||
| 37051a26b5 | |||
| b12a0dac91 | |||
| e4786a78d9 | |||
| bde606f966 | |||
| a722d24759 | |||
| 833388c235 | |||
| 2ac527ae3c | |||
| df35193ad3 | |||
| 9b6365c9cc | |||
| 285a98533a | |||
| f952edf77a | |||
| ae8360b2ae | |||
| 52e36ad7e4 | |||
| 62b9f04191 | |||
| 1010d0584a | |||
| 6864173f35 | |||
| e9d431b3ff | |||
| f138827917 | |||
| a68c47e7e1 | |||
| d648990a81 | |||
| 6ff7dd0c38 | |||
| ee00728e9e | |||
| 0aab11b690 | |||
| c2d9af693d | |||
| 1642113d1f | |||
| d64627e062 | |||
| 5fea0b05a4 | |||
| c3136bf8f7 | |||
| 517170f1c8 | |||
| e5bebea437 | |||
| 3320eacc26 | |||
| b40ff167e7 | |||
| c0f654f8c2 | |||
| f4497b4dbb | |||
| b9684ef162 | |||
| f394f56395 | |||
| 6731937841 | |||
| e44769a48a | |||
| b2819d04e0 | |||
| 52fac8ced6 | |||
| d38faab03c | |||
| 83682f011d | |||
| 1176a269e5 | |||
| 96af652d3b | |||
| 29a2b9a123 | |||
| 238af263d6 | |||
| dbd8abe14c | |||
| 04490c9b9a | |||
| 49d094b14d | |||
| cd7985e4ac | |||
| 61edf3470f | |||
| cd02cd6f33 | |||
| 127e3da909 | |||
| 8fa702e715 | |||
| 3aaaf9936e | |||
| af33c6e810 | |||
| e8eccfd4df | |||
| 241c3345bd | |||
| 20e0692486 | |||
| 0136429d3e | |||
| 01aab1d763 | |||
| 3d2d53362d | |||
| 03838ee1d4 | |||
| 2fabc03841 | |||
| ae6cac89e1 | |||
| 41f02ecfbc | |||
| cef286b170 | |||
| ab94b96a79 | |||
| cc43bd77d2 | |||
| d4dc72d0f4 | |||
| 6d841526b1 | |||
| 8e48bb4e32 | |||
| a0279f5be2 | |||
| 2c8c800584 | |||
| ccb5358aad | |||
| 4db97164bb | |||
| 882763cfe1 | |||
| 798f3c25c0 | |||
| d21a0edffc | |||
| 02024fef58 | |||
| a4c51eccea | |||
| a9767e5df1 | |||
| e59175fc89 | |||
| 59725c0ead | |||
| 4e20f4585f | |||
| 1c37867e52 | |||
| 67491e2dac | |||
| 2d85c9595d | |||
| b8138ef850 | |||
| e3fc8380cf | |||
| b9afaaae60 | |||
| 4d03a38067 | |||
| 79ed37699e | |||
| abf0f9e98a | |||
| 3955721bfc | |||
| c3855cebec | |||
| 41e6507665 | |||
| 5482ac590d | |||
| 28440eb936 | |||
| f606840216 | |||
| 6a990bf79f | |||
| ab5fed60fe | |||
| e3a215e081 | |||
| 7fa758602a | |||
| bb979911c8 | |||
| f225f8a6e7 | |||
| ee891fb8fa | |||
| 9d73ff4110 | |||
| 0b7ecf1599 | |||
| aa45f05c63 | |||
| 9234f12ed0 | |||
| 26e0bd227e | |||
| 64ce66a619 | |||
| 9302bbcbf3 | |||
| 1855f838ef | |||
| 11af06a752 | |||
| 05668591ef | |||
| e74beec32d | |||
| 3efb2c9a6c | |||
| d3af63e013 | |||
| ca6bee64b9 | |||
| b52d183dba | |||
| 23e1877a62 | |||
| d8736fe33e | |||
| a74365a4fd | |||
| 1c2163bb5f | |||
| 12c2677dc2 | |||
| ae1b59ce7f | |||
| 02c4de897d | |||
| 3c6242717e | |||
| 3c71afb9fa | |||
| e79107c644 | |||
| 9f54734f62 | |||
| 0afce96c8d | |||
| d881997acd | |||
| 8906133b93 | |||
| b9e3c4a689 | |||
| 81f18e244d | |||
| ff91481018 | |||
| fdb3184c71 | |||
| aa3b2a18ec | |||
| d498016a47 | |||
| 492e0344b5 | |||
| a710c64909 | |||
| 943ba5dcbf | |||
| 371edd1a02 | |||
| 9fc6ba5b01 | |||
| 983bea61aa | |||
| ab74f7adf7 | |||
| bdd0bf45bd | |||
| 4b950fc23d | |||
| af183d95ad | |||
| 1d4d5d2337 | |||
| 653b72d86a | |||
| 12b6ef6350 | |||
| c979685a1f | |||
| 5b1d0261ff | |||
| 5bffc89a2f | |||
| 2ce612d5b1 | |||
| 6999e5aecf | |||
| 44a7910c69 | |||
| 85c6fde443 | |||
| 67b6dcc293 | |||
| 24d9f279ec | |||
| 83f93c27a1 | |||
| d756b7cd43 | |||
| 1e6f504a84 | |||
| abb4f3f9f0 | |||
| 5b0209a7ce | |||
| c1e0685e97 | |||
| 3c57b28c6d | |||
| cab62ff7b3 | |||
| 5b6dfed359 | |||
| 91a85a065b | |||
| 32e2c51365 | |||
| 1992fdf398 | |||
| 59ac27971a | |||
| cd0ace2a39 | |||
| 274c728e77 | |||
| 8547127f54 | |||
| 45d799aa98 | |||
| 660c90626d | |||
| d9dc42895f | |||
| 02fd21bcdb | |||
| af00d6ad14 | |||
| 0d4a518a08 | |||
| 05b43ca258 | |||
| 9d6ea845d7 | |||
| bbca4e8e39 | |||
| 49954ec4ad | |||
| 6e94625f2a | |||
| 0399ad040f | |||
| 726ed1102e | |||
| 5bc7d203a5 | |||
| ce204aaf16 | |||
| ed8a89c5ce | |||
| 66e7d9432e | |||
| d59a9522ee | |||
| f1661b62b3 | |||
| a98562f46b | |||
| 365bd24740 | |||
| d50ea4d067 | |||
| 52c96437fa | |||
| 2a7c8ec81b | |||
| bd6c606303 | |||
| 112120e8fe | |||
| 00f6e3a7b4 | |||
| 9cc72f951b | |||
| 0f484682d8 | |||
| 60cf07fb0a | |||
| 493caee715 | |||
| aa6217e581 | |||
| a640f5504f | |||
| e01672b97d | |||
| 9e3d487a08 | |||
| 506cd2c5c5 | |||
| 6cb0ac5666 | |||
| b7d0ff2e16 | |||
| 984cefdc7b | |||
| 6c892cb7c1 | |||
| 52d1ddc5af | |||
| 75cd28435c | |||
| 9aa8b4e371 | |||
| e94d8e32ef | |||
| 3fae3088ca | |||
| cbb08f76ae | |||
| dd503706b0 | |||
| eeb181bcc0 | |||
| 287534cf26 | |||
| d08fce5fd5 | |||
| 0ae29744c8 | |||
| 84e1a5177b | |||
| c7e72149f1 | |||
| ecfca19f11 | |||
| 90a53e509a | |||
| 15fc97a996 | |||
| 35ed326dda | |||
| f4a1dd9e16 | |||
| 6f75a64c46 | |||
| dc04a4e848 | |||
| cd37aa175c | |||
| cb6cfb5556 | |||
| 439e97aee6 | |||
| 5528525888 | |||
| a49ba66c36 | |||
| 9de4db8531 | |||
| dbc48f377a | |||
| 1d77d42fe2 | |||
| 683fee8028 | |||
| a83d18fc51 | |||
| b9e8b00af0 | |||
| e2820e02c5 | |||
| f92364a002 | |||
| 9696c8d3e2 | |||
| 18b2bcd0a7 | |||
| f56970350a | |||
| 863f371e68 | |||
| 1bfc89fdd4 | |||
| 24c5114745 | |||
| 7def560726 | |||
| 6cbd0de77d | |||
| 9a8101df6a | |||
| 78744a6853 | |||
| efce1bb116 | |||
| 104c95a942 | |||
| f6392e8932 | |||
| 88e34ef65c | |||
| 8f0117a145 | |||
| 1f878c3098 | |||
| ae5276b2d1 | |||
| 4dd4c5c463 | |||
| cafa6f3d61 | |||
| f0ed2efe07 | |||
| bbf48c7e9e | |||
| 99d54b82b6 | |||
| 49776ca1eb | |||
| 272305f3a4 | |||
| 89f023c7dd | |||
| afc5a52978 | |||
| 12a04f4188 | |||
| 0fe7a96781 | |||
| a06a49bfed | |||
| 86b90fd4b8 | |||
| d18824cfae | |||
| 12b4c93279 | |||
| af47e19e1a | |||
| 4e666303d0 | |||
| 0fa8206321 | |||
| 78ac154998 | |||
| bbcaac7b71 | |||
| 719ba49a85 | |||
| 54ae20c69b | |||
| f864837cbe | |||
| 21191b8e80 | |||
| 6426e913b6 | |||
| 3d3e42c6a3 | |||
| 6fe106f55c | |||
| ba8ac3d143 | |||
| 1963a5453e | |||
| 59a383736c | |||
| 1820f411ae | |||
| 8327942a9c | |||
| aec7f6001a | |||
| d377518632 | |||
| ad9b03fe56 | |||
| 3b490bc42f | |||
| dacd9abe73 | |||
| f74e21782b | |||
| 8fbe819493 | |||
| 626080d228 | |||
| f56e82de81 | |||
| eec396e388 | |||
| 7d3950658a | |||
| 3c1246ee06 | |||
| d69965e3b1 | |||
| 30edac03a6 | |||
| dd4aface93 | |||
| b1350fdecd | |||
| f2d9b74490 | |||
| 84d09d0894 | |||
| e61f231316 | |||
| 33998aab5d | |||
| 1ef0d8c2c0 | |||
| 0f24105686 | |||
| d35b9e5c3e | |||
| ee5c168983 | |||
| 9ce7379946 | |||
| b692474e24 | |||
| 6cfc85ca83 | |||
| 7b060c19f4 | |||
| 604b9338f9 | |||
| 3a6557ffe2 | |||
| fe139ca7f4 | |||
| 5c617d3e32 | |||
| 8d11c04d43 | |||
| 92f73d8bed | |||
| 8da51aa7ba | |||
| 78f87111de | |||
| e3fcdd0776 | |||
| 6ea59f1a79 | |||
| 51fb4db82e | |||
| 7f78de8718 | |||
| 76aab74bf1 | |||
| a2bf08c6ff | |||
| b831076839 | |||
| 45575a1fd8 | |||
| 56d92246cb | |||
| 4a00f2335c | |||
| e9cb830d71 | |||
| c2666d2b43 | |||
| 9ceec3070d | |||
| f89977a8e7 | |||
| 8d512b95cb | |||
| 0914387c16 | |||
| 105672c132 | |||
| 0dd6ecc219 | |||
| 9ef5c8dead | |||
| 12ff4dba2e | |||
| ff9992dff8 | |||
| 98c08e4b22 | |||
| 0bd3b26acc | |||
| 59b55a8f37 | |||
| 67e6b1fb04 | |||
| 17243a7d8b | |||
| 16b6b98ede | |||
| e4ce1063a1 | |||
| f099593136 | |||
| f944507520 | |||
| 6824949247 | |||
| 4f54255518 | |||
| cfe482e1fc | |||
| 0e5ea26e36 | |||
| d5886e69aa | |||
| 32fa215243 | |||
| dd5d8afd82 | |||
| f19f4e9341 | |||
| 3aec2ca89b | |||
| 0f7261dd92 | |||
| 0886573d34 | |||
| 5c041ea857 | |||
| 30cd9afc92 | |||
| cceb74a025 | |||
| 42540076c1 | |||
| a528868656 | |||
| c82af28fd4 | |||
| d72e5db0a5 | |||
| 9907cfb0a0 | |||
| a3cd5a132e | |||
| 0971b47bec | |||
| f2ea250364 | |||
| d86ac70d9e | |||
| be8023a994 | |||
| a0ab949399 | |||
| 30ed68ae22 | |||
| f2aa376304 | |||
| 1b48862923 | |||
| 25417ffac8 | |||
| 4dfbc5a828 | |||
| f0f5bc7551 | |||
| f002db2929 | |||
| c5791574f5 | |||
| 8c1fbfb429 | |||
| 8e5501a23b | |||
| 50fd0e9c7d | |||
| 17843c2f7a | |||
| 54c8d2706b | |||
| 44bfb1b93f | |||
| 7a78992881 | |||
| 6ed41c3b87 | |||
| 7414c176d9 | |||
| d7581dbece | |||
| eeb8c89d3d | |||
| 2591fde764 | |||
| a8f3a3155c | |||
| 2a927651c5 | |||
| 62ef647811 | |||
| fe4491a87b | |||
| 105bf65e02 | |||
| 8120f113af | |||
| 7e0c64cb78 | |||
| 4afb54c54f | |||
| 74f6b68034 | |||
| 5d1e7ff3c1 | |||
| 3c628140f7 | |||
| 01f68d4038 | |||
| 8bf99c3318 | |||
| f0829ed54f | |||
| 21276e20d1 | |||
| 9fb9648e4c | |||
| af7689a1bc | |||
| 59dba325b9 | |||
| d13064ee64 | |||
| 669c9f4d8b | |||
| 228306f883 | |||
| 0a2ef854e6 | |||
| 61c471343a | |||
| 175bff4b29 | |||
| 95eb1c3a35 | |||
| cf50976d65 | |||
| 3fcee7f951 | |||
| 17a4c83970 | |||
| 9712fb1e9b | |||
| cbf622f4aa | |||
| 7c1cdea2eb | |||
| 2bfa491b6a | |||
| 41fbfc9ba6 | |||
| 1fa7c0eb43 | |||
| 839b238e74 | |||
| ec22d57cd1 | |||
| 23ef283272 | |||
| 327579f9b6 | |||
| e5f9d2903d | |||
| 1f3aa4e111 | |||
| 3f88d1c49a | |||
| 5348ba1c65 | |||
| 2f35fd44a1 | |||
| 470fe70f07 | |||
| 399bf557f3 | |||
| fc2ca6cc26 | |||
| d9d42ec956 | |||
| 5e41c74682 | |||
| 02ad846dfa | |||
| 4e7eda65ed | |||
| 3fabe4f591 | |||
| 81234ef2ab | |||
| 4bd28390ea | |||
| fa886899fc | |||
| a81126a03a | |||
| 164c555bb4 | |||
| e5926893a9 | |||
| dbcaf705c2 | |||
| f6809be81d | |||
| 3ddf70888e | |||
| fe7dcf33ec | |||
| 733450bf86 | |||
| 1dae9622e1 | |||
| da0fa5c9ad | |||
| e576dea676 | |||
| 4cac51a831 | |||
| fef844e220 | |||
| 0f45bb5792 | |||
| c195c5da7d | |||
| cb26367f46 | |||
| 31dc89c0d7 | |||
| c95c3003f7 | |||
| 35b3fa6d9c | |||
| e217d62f42 | |||
| e62801caaf | |||
| e6008bb299 | |||
| 9a49646d8a | |||
| 9934e4d765 | |||
| b6a397460e | |||
| 2b75c07571 | |||
| ebbc55c4d0 | |||
| 4e6ee3629f | |||
| 46bd0f6709 | |||
| e6c1ed8753 | |||
| 2b92562538 | |||
| 078bb1ade6 | |||
| f205464aa4 | |||
| b820d3917c | |||
| fea81f03fc | |||
| 75aa71a886 | |||
| 398eefc380 | |||
| b549e319c2 | |||
| 1a57b67e14 | |||
| 378246728b | |||
| 41e326f376 | |||
| f9adeca5ca | |||
| 838f87c224 | |||
| 5b519165fc | |||
| 0bb0518093 | |||
| 41f549b271 | |||
| 6b012a31ce | |||
| 1a2b23a066 | |||
| f109530c5f | |||
| 4cb132199c | |||
| fe16b3259c | |||
| 2e46954fe0 | |||
| aedc63c40d | |||
| 00d13d3c35 | |||
| 1b8b3ad538 | |||
| 5f75cfd691 | |||
| 5f89def2ce | |||
| 7c26eb9a24 | |||
| d951fe8fc5 | |||
| ac01135ca8 | |||
| 5e89e35e02 | |||
| 7e7afeaf82 | |||
| c6fa6d294b | |||
| fc2d17fc78 | |||
| 6fb5aa08ff | |||
| 9f072285a6 | |||
| c58ebe4b9b | |||
| 0dd187fe30 | |||
| ea78792c03 | |||
| ba8aa7b263 | |||
| 243cb6a477 | |||
| c987fefede | |||
| 9954225701 | |||
| 2a038f7112 | |||
| 6bb2e078c0 | |||
| 323c59e54f | |||
| d26d63ed38 | |||
| 2009df3dd1 | |||
| 548e75a15e | |||
| 339c85b590 | |||
| 0f0420314b | |||
| a08b5e65f7 | |||
| 99b89c4e5e | |||
| 20586e0c48 | |||
| b54d0c605a | |||
| 2f75af902c | |||
| 9147a6413e | |||
| 9a467d4bf1 | |||
| e40f9337b6 | |||
| 1f0712d2d5 | |||
| 345bc9519a | |||
| 0b30bb2eff | |||
| f0e88aa581 | |||
| 317893bd61 | |||
| bd181271d9 | |||
| 4df09010a8 | |||
| 1a5b2b96dc | |||
| 43d89813fc | |||
| 5bc76ce6fe | |||
| f0f3e9634b | |||
| 1fe35335a5 | |||
| ca1beaefe7 | |||
| 24cf1b3317 | |||
| 88e2ec2042 | |||
| be60c7c1d5 | |||
| 9be21380c7 | |||
| 22fd196045 | |||
| d5293dffb8 | |||
| 9326b07583 | |||
| 90437a5d8d | |||
| c4ec5dc1c5 | |||
| 3b19826f1e | |||
| 95e4715bc3 | |||
| 81edc97d86 | |||
| 92c8bac714 | |||
| 1df04aa18d | |||
| 8654444d1a | |||
| 772c39edb8 | |||
| 74e7474349 | |||
| 3aefe41ea1 | |||
| 2223c31e93 | |||
| 568f0fa9eb | |||
| e91cf0f0c2 | |||
| 8a1f583d88 | |||
| 7518319c80 | |||
| 304df44742 | |||
| e92bab6f4f | |||
| 43755c5457 | |||
| d4455c508f | |||
| 7d01242cd1 | |||
| 9d77189cec | |||
| c8adbdcc64 | |||
| ac6dfe7134 | |||
| b1ad7c7254 | |||
| 218b9249a2 | |||
| 6fb5602cb0 | |||
| b942d37944 | |||
| b7ee191b6c | |||
| 4ea24a0a6f | |||
| 6ad8c8d73d | |||
| 8982952fa4 | |||
| b135878824 | |||
| e9463d39aa | |||
| dcee7ca8e4 | |||
| 1653aa0a5d | |||
| 2699c9a2f9 | |||
| efd81d8e34 | |||
| 925b224eda | |||
| 7504753ea8 | |||
| da60f40dfc | |||
| 560ebc5997 | |||
| 361d4b81df | |||
| 83cf1fdb00 | |||
| 228a0baf81 | |||
| df7cea4387 | |||
| 13b55a797e | |||
| 1ec33b1374 | |||
| c06991e03d | |||
| 2fcd7abbfd | |||
| 5ca17cfc99 | |||
| 05f1e3852c | |||
| 3b22a45a23 | |||
| f172d3129f | |||
| a7a84b6fab | |||
| d4afa09e03 | |||
| 50947eec2a | |||
| 2624bb0210 | |||
| 6e9685f302 | |||
| 898bf0c71e | |||
| 5a7add8eaa | |||
| 66334dfacd | |||
| a33f3b1ceb | |||
| 6e724cedf7 | |||
| e949c78639 | |||
| 9bcfdb1f0d | |||
| fccb122c59 | |||
| e2b2a5fb9f | |||
| f2a3e5f05b | |||
| 5f855568c8 | |||
| 24696e3be2 | |||
| b17dce232e | |||
| e1b20723fd | |||
| fdab15a6dc | |||
| 33899c237e | |||
| 0fad222569 | |||
| 9c5f33e187 | |||
| ddcce5b95a | |||
| 6fd9741fab | |||
| 4916fc8f4b | |||
| fc19e58678 | |||
| 9610bcfb72 | |||
| 1bc252741a | |||
| caa65182dc | |||
| 97e3fb296c | |||
| 74eeb411ba | |||
| 040ca3f332 | |||
| 5ec440792e | |||
| 9d3ee6a597 | |||
| 6c4c9d14e2 | |||
| d165189032 | |||
| 84bf25fcd5 | |||
| 3945b9d078 | |||
| 6796804593 | |||
| 858c5bc9b8 | |||
| 71bdbb7e48 | |||
| c346c78697 | |||
| ba2bac5bed | |||
| bcfc94d448 | |||
| bebfc87cd4 | |||
| c8e035d332 | |||
| e58929428c | |||
| a389416b7b | |||
| 0a8bca1a6d | |||
| efad8b325b | |||
| 4c5f776128 | |||
| c1495e0d5e | |||
| a97168a4f0 | |||
| a50ad30176 | |||
| c1f65106de | |||
| 4250e3aa32 | |||
| cecb24bb0e | |||
| b99b497893 | |||
| e16a45d6ba | |||
| 0e29046c31 | |||
| 5cbada493a | |||
| 4c79db3d5d | |||
| c7e46b6563 | |||
| fe1714d50e | |||
| 2765e3d145 | |||
| c62f1334bc | |||
| eea0dec4a6 | |||
| 25d241f12c | |||
| 0b55ab083a | |||
| 6b9d4f2470 | |||
| b9a89c1fd2 | |||
| 8a8340ffc8 | |||
| 0af69e2df6 | |||
| 4b9a326f6b | |||
| a92761dc13 | |||
| 8a649e8d95 | |||
| 5c5c970e81 | |||
| 7b208043b9 | |||
| 26dfad0682 | |||
| 902f0d73d4 | |||
| f6ae967796 | |||
| 29afbdaec7 | |||
| 350402b179 | |||
| 0f581e2f69 | |||
| 17eee1ca0e | |||
| 7aa06942be | |||
| d3010dc060 | |||
| 53e1a535dd | |||
| e63fa70700 | |||
| 27fb6eedc6 | |||
| 5c0a7d1456 | |||
| e10f6e2032 | |||
| c61643edaa | |||
| 0b7b0b48d1 | |||
| 2ff459e2c4 | |||
| 8c877a64f0 | |||
| 8fdfd5c843 | |||
| 579c1744bd | |||
| 118c45f8f6 | |||
| beaa502ff9 | |||
| 730b7b842c | |||
| 784832ca95 | |||
| bbe894fb4f | |||
| 4c17e606fd | |||
| 73d81a72b8 | |||
| 8b0f770c57 | |||
| a89d43f315 | |||
| 96a64d439e | |||
| 401f79877c | |||
| c52a35288a | |||
| a37d1a0676 | |||
| 0e56e0a539 | |||
| cc67be341f | |||
| ff829569b0 | |||
| bc4afcba71 | |||
| dde25ec7d0 | |||
| 9eb80575e5 | |||
| 590e082328 | |||
| 2f52a0651a | |||
| e393ec06e3 | |||
| cd33858020 | |||
| 0dddb32305 | |||
| f3165ceb71 | |||
| d6e36d457f | |||
| 52e3a0d23f | |||
| fb58fa788b | |||
| d09cf80da5 | |||
| aab980b05a |
@@ -18,19 +18,59 @@ body:
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: game
|
||||
attributes:
|
||||
label: Which game were you playing
|
||||
description: Pick every version you saw the bug in. Use N/A if it isn't game-specific.
|
||||
multiple: true
|
||||
options:
|
||||
- Red
|
||||
- Blue
|
||||
- Yellow
|
||||
- Gold
|
||||
- N/A
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: os
|
||||
attributes:
|
||||
label: Which build are you running
|
||||
description: Official release targets. Pick Multiple platforms if you saw it on more than one.
|
||||
options:
|
||||
- macOS
|
||||
- Windows
|
||||
- Linux
|
||||
- Android
|
||||
- iOS
|
||||
- Nintendo Switch
|
||||
- Xbox
|
||||
- Anbernic RG34XXSP
|
||||
- Multiple platforms
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: mods_enabled
|
||||
attributes:
|
||||
label: Were any mods on
|
||||
description: Check the MODS tab in the launcher if you're not sure.
|
||||
options:
|
||||
- "No"
|
||||
- "Yes"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: mods_which
|
||||
attributes:
|
||||
label: Which mods (if any were on)
|
||||
description: List the enabled mods. Leave blank if none were on.
|
||||
placeholder: nuzlocke 1.0.0, running-shoes 0.3
|
||||
validations:
|
||||
required: false
|
||||
|
||||
- type: input
|
||||
id: version
|
||||
attributes:
|
||||
|
||||
@@ -26,6 +26,21 @@ body:
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: game
|
||||
attributes:
|
||||
label: Which game is this about
|
||||
description: Pick every version it applies to. Use N/A if it isn't game-specific.
|
||||
multiple: true
|
||||
options:
|
||||
- Red
|
||||
- Blue
|
||||
- Yellow
|
||||
- Gold
|
||||
- N/A
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: discord
|
||||
attributes:
|
||||
|
||||
@@ -23,6 +23,21 @@ body:
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: game
|
||||
attributes:
|
||||
label: Which game is this for
|
||||
description: Pick every version the mod should cover. Use N/A if it isn't game-specific.
|
||||
multiple: true
|
||||
options:
|
||||
- Red
|
||||
- Blue
|
||||
- Yellow
|
||||
- Gold
|
||||
- N/A
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: input
|
||||
id: discord
|
||||
attributes:
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: github-actions
|
||||
directory: /
|
||||
schedule:
|
||||
interval: weekly
|
||||
|
After Width: | Height: | Size: 626 KiB |
|
After Width: | Height: | Size: 582 KiB |
|
After Width: | Height: | Size: 472 KiB |
|
After Width: | Height: | Size: 730 KiB |
@@ -10,8 +10,6 @@ name: ci
|
||||
# The T3 content tier asserts Pokemon Red facts; scripts/test.sh detects
|
||||
# data/generated/ is absent and skips it rather than failing.
|
||||
#
|
||||
# Runs alongside release.yml, which is untouched by this file.
|
||||
|
||||
on:
|
||||
push:
|
||||
# Integration branch + release branch. PRs already run via pull_request
|
||||
@@ -24,12 +22,360 @@ concurrency:
|
||||
group: ci-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
ios-changes:
|
||||
name: detect iOS changes
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
changed: ${{ steps.paths.outputs.changed }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- id: paths
|
||||
env:
|
||||
BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }}
|
||||
HEAD_SHA: ${{ github.sha }}
|
||||
run: |
|
||||
if [ -z "$BASE_SHA" ] || [ "$BASE_SHA" = "0000000000000000000000000000000000000000" ]; then
|
||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
if git diff --name-only "$BASE_SHA" "$HEAD_SHA" | grep -Eq '^(mobile/ios/|scripts/build_ios\.sh$)'; then
|
||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "changed=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
ios-build:
|
||||
name: iOS build
|
||||
needs: ios-changes
|
||||
if: needs.ios-changes.outputs.changed == 'true'
|
||||
runs-on: ${{ fromJSON(github.repository == 'bryanthaboi/gen1recomp' && '["self-hosted", "macOS"]' || '"macos-latest"') }}
|
||||
outputs:
|
||||
ipa_url: ${{ steps.upload-ipa.outputs.artifact-url }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: import signing certificate
|
||||
if: github.repository == 'bryanthaboi/gen1recomp'
|
||||
run: |
|
||||
keychain_path="$RUNNER_TEMP/gen1recomp-ci-signing.keychain-db"
|
||||
ci_dir="${POKEMON_CI_DIR:-$HOME/.config/pokemon-ci}"
|
||||
p12="$ci_dir/signing.p12"
|
||||
passfile="$ci_dir/signing.pass"
|
||||
[ -f "$p12" ] && [ -f "$passfile" ] || exit 1
|
||||
p12pw="$(cat "$passfile")"
|
||||
kcpw="$(openssl rand -base64 24)"
|
||||
echo "::add-mask::$kcpw"
|
||||
security delete-keychain "$keychain_path" 2>/dev/null || true
|
||||
security create-keychain -p "$kcpw" "$keychain_path"
|
||||
security set-keychain-settings "$keychain_path"
|
||||
security unlock-keychain -p "$kcpw" "$keychain_path"
|
||||
security import "$p12" -P "$p12pw" -k "$keychain_path" -T /usr/bin/codesign -T /usr/bin/security
|
||||
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$kcpw" "$keychain_path" >/dev/null
|
||||
existing="$(security list-keychains -d user | sed -e 's/^[[:space:]]*//' -e 's/"//g')"
|
||||
security list-keychains -d user -s "$keychain_path" $existing
|
||||
- name: install xcbeautify
|
||||
run: brew list xcbeautify >/dev/null 2>&1 || brew install xcbeautify
|
||||
- name: build iOS release
|
||||
env:
|
||||
CANONICAL_REPOSITORY: ${{ github.repository == 'bryanthaboi/gen1recomp' }}
|
||||
run: |
|
||||
if [ "$CANONICAL_REPOSITORY" = true ]; then
|
||||
scripts/build_ios.sh --fetch --device --release
|
||||
else
|
||||
scripts/build_ios.sh --fetch --release
|
||||
fi
|
||||
- name: upload iOS release artifact
|
||||
id: upload-ipa
|
||||
if: github.repository == 'bryanthaboi/gen1recomp'
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: gen1recomp++-ios-ipa
|
||||
path: dist/ios/gen1recomp++.ipa
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
- name: clean up signing keychain
|
||||
if: ${{ always() && github.repository == 'bryanthaboi/gen1recomp' }}
|
||||
run: security delete-keychain "$RUNNER_TEMP/gen1recomp-ci-signing.keychain-db" 2>/dev/null || true
|
||||
|
||||
switch-changes:
|
||||
name: detect Switch changes
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
changed: ${{ steps.paths.outputs.changed }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- id: paths
|
||||
env:
|
||||
BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }}
|
||||
HEAD_SHA: ${{ github.sha }}
|
||||
run: |
|
||||
if [ -z "$BASE_SHA" ] || [ "$BASE_SHA" = "0000000000000000000000000000000000000000" ]; then
|
||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
if git diff --name-only "$BASE_SHA" "$HEAD_SHA" | grep -Eq '^(scripts/build_switch\.sh$|scripts/switch/|docs/switch-.*\.md$|tests/switch_ci_workflows_test\.lua$|tests/switch_transfer_docs_test\.lua$|\.github/workflows/(ci|release|switch-artifact-comment)\.yml$|src/core/(NxAssetOverlay|Platform|GameVersion)\.lua$|src/import/CacheFs\.lua$|tests/engine/(assets_version_fallback|nx_generated_guard|nx_yellow_boot|switch_diagnostics)_test\.lua$|tests/engine/platform_nx)'; then
|
||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "changed=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
switch-selftest:
|
||||
name: Switch offline selftest
|
||||
needs: switch-changes
|
||||
if: needs.switch-changes.outputs.changed == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: install luajit
|
||||
run: sudo apt-get update && sudo apt-get install -y luajit
|
||||
- name: Switch offline selftest
|
||||
run: bash scripts/switch/selftest_build_switch.sh
|
||||
- name: verify_payload self-test
|
||||
run: bash scripts/switch/verify_payload.sh --self-test
|
||||
- name: Switch CI workflow content gate
|
||||
run: luajit tests/switch_ci_workflows_test.lua
|
||||
- name: Switch transfer docs content gate
|
||||
run: luajit tests/switch_transfer_docs_test.lua
|
||||
# NX runtime regressions gate this job via switch-changes; run the NX
|
||||
# engine suites here too so a PR touching them gets feedback on the
|
||||
# fork-safe ubuntu runner before the self-hosted Mac build.
|
||||
- name: NX engine suites (headless)
|
||||
run: |
|
||||
luajit tests/engine/assets_version_fallback_test.lua
|
||||
luajit tests/engine/nx_generated_guard_test.lua
|
||||
luajit tests/engine/nx_yellow_boot_test.lua
|
||||
|
||||
switch-build:
|
||||
name: Switch fused build
|
||||
needs: [switch-changes, switch-selftest]
|
||||
if: |
|
||||
always()
|
||||
&& needs.switch-changes.outputs.changed == 'true'
|
||||
&& needs.switch-selftest.result == 'success'
|
||||
&& github.repository == 'bryanthaboi/gen1recomp'
|
||||
&& (github.event_name != 'pull_request'
|
||||
|| github.event.pull_request.head.repo.full_name == github.repository)
|
||||
runs-on: ["self-hosted", "macOS"]
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: Build Switch fused NRO
|
||||
run: |
|
||||
set -euo pipefail
|
||||
VER="$(printf '%s' "$GITHUB_SHA" | cut -c1-7)"
|
||||
scripts/build_switch.sh --fetch --fused --version "$VER"
|
||||
echo "SWITCH_VER=$VER" >> "$GITHUB_ENV"
|
||||
- name: upload Switch NRO artifact
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: gen1recomp-switch-nro
|
||||
path: |
|
||||
dist/switch/gen1recomp-${{ env.SWITCH_VER }}-switch.nro
|
||||
dist/switch/gen1recomp-${{ env.SWITCH_VER }}-switch.nro.sha256
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
|
||||
xbox-uwp-changes:
|
||||
name: detect Xbox UWP changes
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
changed: ${{ steps.paths.outputs.changed }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- id: paths
|
||||
env:
|
||||
BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }}
|
||||
HEAD_SHA: ${{ github.sha }}
|
||||
run: |
|
||||
if [ -z "$BASE_SHA" ] || [ "$BASE_SHA" = "0000000000000000000000000000000000000000" ]; then
|
||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
if git diff --name-only "$BASE_SHA" "$HEAD_SHA" | grep -Eq '^(ports/uwp/|scripts/build_xbox_uwp\.sh$|scripts/xbox-uwp/|scripts/pack_love\.sh$|\.github/workflows/(ci|release)\.yml$|src/core/Platform\.lua$|src/import/(CacheFs|LauncherView|RomImporter)\.lua$|src/update/Check\.lua$|tests/engine/(platform_nx|uwp_baseroms|uwp_native_picker)_test\.lua$|tests/rom_importer_double_pick_test\.lua$)'; then
|
||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "changed=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
xbox-uwp-selftest:
|
||||
name: Xbox UWP offline selftest
|
||||
needs: xbox-uwp-changes
|
||||
if: needs.xbox-uwp-changes.outputs.changed == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: Xbox UWP offline selftest
|
||||
run: bash scripts/xbox-uwp/selftest_build_xbox_uwp.sh
|
||||
- name: Build shared payload
|
||||
run: |
|
||||
scripts/pack_love.sh \
|
||||
--output .bazinga/work/ci-game.love \
|
||||
--listing .bazinga/work/ci-love-listing.txt \
|
||||
--version 0.0.0
|
||||
- name: Upload shared payload
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: gen1recomp-xbox-uwp-payload
|
||||
path: .bazinga/work/ci-game.love
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
xbox-uwp-build:
|
||||
name: Xbox UWP build
|
||||
needs: [xbox-uwp-changes, xbox-uwp-selftest]
|
||||
if: |
|
||||
always()
|
||||
&& needs.xbox-uwp-changes.outputs.changed == 'true'
|
||||
&& needs.xbox-uwp-selftest.result == 'success'
|
||||
runs-on: windows-2022
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: Download shared payload
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: gen1recomp-xbox-uwp-payload
|
||||
path: .bazinga/work
|
||||
- name: Build Xbox UWP package
|
||||
shell: bash
|
||||
run: |
|
||||
bash scripts/build_xbox_uwp.sh \
|
||||
--release \
|
||||
--version 0.0.0 \
|
||||
--game-love .bazinga/work/ci-game.love
|
||||
- name: Upload Xbox UWP package
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: gen1recomp-xbox-uwp
|
||||
path: |
|
||||
dist/xbox-uwp/gen1recomp-0.0.0-xbox-uwp.zip
|
||||
dist/xbox-uwp/gen1recomp-0.0.0-xbox-uwp.zip.sha256
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
|
||||
linux-arm64-changes:
|
||||
name: detect Linux arm64 changes
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
changed: ${{ steps.paths.outputs.changed }}
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- id: paths
|
||||
env:
|
||||
BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }}
|
||||
HEAD_SHA: ${{ github.sha }}
|
||||
run: |
|
||||
if [ -z "$BASE_SHA" ] || [ "$BASE_SHA" = "0000000000000000000000000000000000000000" ]; then
|
||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||
exit 0
|
||||
fi
|
||||
if git diff --name-only "$BASE_SHA" "$HEAD_SHA" | grep -Eq '^(scripts/build_linux_arm64\.sh$|scripts/linux-arm64/|scripts/pack_love\.sh$|docs/linux-arm64-build\.md$|\.github/workflows/(ci|release)\.yml$)'; then
|
||||
echo "changed=true" >> "$GITHUB_OUTPUT"
|
||||
else
|
||||
echo "changed=false" >> "$GITHUB_OUTPUT"
|
||||
fi
|
||||
|
||||
linux-arm64-selftest:
|
||||
name: Linux arm64 offline selftest
|
||||
needs: linux-arm64-changes
|
||||
if: needs.linux-arm64-changes.outputs.changed == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
# Deliberately on x86_64: everything this gate checks (pins, the
|
||||
# host-arch guard, the dependency exclude list, the AppRun fusion
|
||||
# contract) is answerable without a container or an aarch64 machine,
|
||||
# so the slow native job below only ever starts on a sane tree.
|
||||
- name: Linux arm64 offline selftest
|
||||
run: bash scripts/linux-arm64/selftest_build_linux_arm64.sh
|
||||
|
||||
linux-arm64-build:
|
||||
name: Linux arm64 AppImage build
|
||||
needs: [linux-arm64-changes, linux-arm64-selftest]
|
||||
if: |
|
||||
always()
|
||||
&& needs.linux-arm64-changes.outputs.changed == 'true'
|
||||
&& needs.linux-arm64-selftest.result == 'success'
|
||||
# No fork restriction, unlike switch-build: this needs no secrets and no
|
||||
# self-hosted hardware, just GitHub's free arm64 runner for public repos,
|
||||
# so contributors get the same coverage on their own PRs.
|
||||
runs-on: ubuntu-24.04-arm
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: Build the aarch64 AppImage
|
||||
run: |
|
||||
set -euo pipefail
|
||||
scripts/build_linux_arm64.sh --version 0.0.0
|
||||
- name: Verify the AppImage is self-contained and bullseye-compatible
|
||||
run: |
|
||||
set -euo pipefail
|
||||
image="dist/linux-arm64/gen1recomp-0.0.0-linux-arm64.AppImage"
|
||||
|
||||
# --appimage-extract needs no FUSE, so this works on a runner
|
||||
# without /dev/fuse and still exercises the real payload.
|
||||
"$image" --appimage-extract >/dev/null
|
||||
for required in AppRun bin/love game.love lib/liblove-11.5.so; do
|
||||
[ -e "squashfs-root/$required" ] \
|
||||
|| { echo "::error::AppImage is missing $required"; exit 1; }
|
||||
done
|
||||
|
||||
# Every bundled object must resolve once AppRun's LD_LIBRARY_PATH is
|
||||
# applied; an unresolved soname here is a user-visible launch crash.
|
||||
#
|
||||
# This runs on a HEADLESS runner on purpose, and that is the point.
|
||||
# The first version of this build bundled Debian's SDL2, which
|
||||
# hard-links libpulse/libasound/libX11/libwayland, so it only ever
|
||||
# started on a full desktop -- a bare runner is what exposed it.
|
||||
missing="$(LD_LIBRARY_PATH="$PWD/squashfs-root/lib" \
|
||||
ldd squashfs-root/bin/love squashfs-root/lib/*.so* 2>/dev/null \
|
||||
| grep 'not found' || true)"
|
||||
[ -z "$missing" ] || { echo "::error::unresolved deps:"; echo "$missing"; exit 1; }
|
||||
|
||||
# Nothing may hard-link a driver, session or audio-stack library:
|
||||
# those must be reached through dlopen so the AppImage runs on a box
|
||||
# with only ALSA, only Wayland, or only KMSDRM.
|
||||
linked="$(for f in squashfs-root/bin/love squashfs-root/lib/*.so*; do
|
||||
objdump -p "$f" 2>/dev/null | awk '/NEEDED/{print $2}'
|
||||
done | sort -u | grep -E '^lib(pulse|asound|X11|wayland|GL|EGL|drm|gbm|xcb|cairo|sndio|dbus)' || true)"
|
||||
[ -z "$linked" ] \
|
||||
|| { echo "::error::these must be dlopened, not linked:"; echo "$linked"; exit 1; }
|
||||
|
||||
# The whole point of compiling on bullseye. If a future change moves
|
||||
# the builder to a newer base, the glibc floor silently rises and
|
||||
# every user on an older distro gets "GLIBC_2.xx not found" -- catch
|
||||
# it here instead of in a release.
|
||||
floor="$(objdump -T squashfs-root/bin/love squashfs-root/lib/*.so* 2>/dev/null \
|
||||
| grep -o 'GLIBC_[0-9.]*' | sort -V | tail -1)"
|
||||
echo "highest required glibc symbol version: $floor"
|
||||
[ -n "$floor" ] \
|
||||
|| { echo "::error::found no versioned glibc symbols -- objdump read nothing"; exit 1; }
|
||||
highest="$(printf '%s\n' "$floor" "GLIBC_2.31" | sort -V | tail -1)"
|
||||
[ "$highest" = "GLIBC_2.31" ] \
|
||||
|| { echo "::error::AppImage requires $floor, above the bullseye 2.31 floor"; exit 1; }
|
||||
- name: Upload the AppImage
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: gen1recomp-linux-arm64
|
||||
path: |
|
||||
dist/linux-arm64/gen1recomp-0.0.0-linux-arm64.AppImage
|
||||
dist/linux-arm64/gen1recomp-0.0.0-linux-arm64.AppImage.sha256
|
||||
if-no-files-found: error
|
||||
retention-days: 7
|
||||
|
||||
headless:
|
||||
name: headless suites (no ROM)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
# LuaJIT, not lua5.4: LOVE 11.x embeds LuaJIT 2.1 and the engine is
|
||||
# written to Lua 5.1 semantics, so CI must run the interpreter the
|
||||
@@ -37,6 +383,9 @@ jobs:
|
||||
- name: install luajit
|
||||
run: sudo apt-get update && sudo apt-get install -y luajit
|
||||
|
||||
- name: install Pillow
|
||||
run: python3 -m pip install --upgrade pillow
|
||||
|
||||
- name: interpreter version
|
||||
run: luajit -v
|
||||
|
||||
@@ -47,7 +396,7 @@ jobs:
|
||||
name: fixture dataset integrity
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v7
|
||||
- run: sudo apt-get update && sudo apt-get install -y luajit
|
||||
- run: python3 -m pip install --upgrade pillow
|
||||
|
||||
@@ -89,7 +438,7 @@ jobs:
|
||||
name: screenshot differ (capture not yet wired)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v7
|
||||
- run: python3 -m pip install --upgrade pillow
|
||||
|
||||
# 21-testing-and-ci §"Testing & acceptance criteria": compare_shots
|
||||
@@ -137,7 +486,7 @@ jobs:
|
||||
name: mod lint (no ROM-derived content)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
# the MK305 dump check key-diffs shipped tables through luajit, and
|
||||
# modkit treats a missing interpreter as a fatal MK100 -- without
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
name: iOS artifact comment
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: [ci]
|
||||
types: [completed]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
comment:
|
||||
if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- id: artifact
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
RUN_ID: ${{ github.event.workflow_run.id }}
|
||||
HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }}
|
||||
HEAD_REPOSITORY: ${{ github.event.workflow_run.head_repository.full_name }}
|
||||
run: |
|
||||
artifact_id="$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$RUN_ID/artifacts" --jq '.artifacts[] | select(.name == "gen1recomp++-ios-ipa") | .id')"
|
||||
[ -n "$artifact_id" ] || exit 0
|
||||
head_owner="${HEAD_REPOSITORY%%/*}"
|
||||
pr_number="$(gh api "repos/$GITHUB_REPOSITORY/pulls?state=open&head=$head_owner:$HEAD_BRANCH" --jq '.[0].number // empty')"
|
||||
[ -n "$pr_number" ] || exit 0
|
||||
echo "artifact_url=https://github.com/$GITHUB_REPOSITORY/actions/runs/$RUN_ID/artifacts/$artifact_id" >> "$GITHUB_OUTPUT"
|
||||
echo "pr_number=$pr_number" >> "$GITHUB_OUTPUT"
|
||||
# Upsert via comment-tag only — do not delete-all bot comments (clobbers Switch).
|
||||
- name: Get build info
|
||||
id: build-info
|
||||
env:
|
||||
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
|
||||
run: |
|
||||
commit_hash="$(printf '%s' "$HEAD_SHA" | cut -c1-7)"
|
||||
build_time="$(date "+%Y-%m-%d %H:%M:%S")"
|
||||
echo "hash=$commit_hash" >> "$GITHUB_OUTPUT"
|
||||
echo "time=$build_time" >> "$GITHUB_OUTPUT"
|
||||
- name: comment iOS artifact
|
||||
if: steps.artifact.outputs.pr_number != ''
|
||||
uses: thollander/actions-comment-pull-request@v3
|
||||
with:
|
||||
message: |
|
||||
[gen1recomp++.ipa](${{ steps.artifact.outputs.artifact_url }})
|
||||
|
||||
**Commit**: [#${{ steps.build-info.outputs.hash }}](https://github.com/${{ github.event.workflow_run.head_repository.full_name }}/commit/${{ github.event.workflow_run.head_sha }})
|
||||
**Build Time**: `${{ steps.build-info.outputs.time }}`
|
||||
|
||||
<sub>This comment was automatically generated. [View workflow run](https://github.com/${{ github.repository }}/actions/runs/${{ github.event.workflow_run.id }})</sub>
|
||||
pr-number: ${{ steps.artifact.outputs.pr_number }}
|
||||
comment-tag: ios-build-result
|
||||
github-token: ${{ github.token }}
|
||||
@@ -1,8 +1,10 @@
|
||||
name: Release
|
||||
|
||||
# Builds the macOS, Windows, and Linux desktop apps, an Android APK, and the
|
||||
# Anbernic RG34XXSP (Stock OS 64-bit MOD / PortMaster) port on the self-hosted
|
||||
# Mac runner, and publishes them as a GitHub Release.
|
||||
# Builds the macOS, Windows, and Linux desktop apps, an Android APK, an iOS
|
||||
# IPA, a Nintendo Switch SD-ready zip (experimental), Xbox UWP, the Anbernic
|
||||
# RG34XXSP (Stock OS 64-bit MOD / PortMaster) and Linux ARM SBC PortMaster
|
||||
# handheld ports on the self-hosted Mac runner, and publishes them as a
|
||||
# GitHub Release.
|
||||
#
|
||||
# Versioning:
|
||||
# - First ever release is 0.1.0.
|
||||
@@ -24,6 +26,7 @@ on:
|
||||
paths-ignore:
|
||||
- '.github/**'
|
||||
- '**.md'
|
||||
- 'mobile/ios/app-repo.json'
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version:
|
||||
@@ -41,16 +44,17 @@ concurrency:
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: [self-hosted, macOS]
|
||||
|
||||
version:
|
||||
name: determine release version
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version: ${{ steps.ver.outputs.version }}
|
||||
tag: ${{ steps.ver.outputs.tag }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
|
||||
- name: Determine version
|
||||
id: ver
|
||||
env:
|
||||
@@ -58,7 +62,6 @@ jobs:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
semver_re='^[0-9]+\.[0-9]+\.[0-9]+$'
|
||||
|
||||
# 1) Explicit override from a manual run.
|
||||
@@ -88,7 +91,6 @@ jobs:
|
||||
| grep -E "$semver_re" \
|
||||
| sort -t. -k1,1n -k2,2n -k3,3n \
|
||||
| tail -1 || true)"
|
||||
|
||||
if [ -z "$latest" ]; then
|
||||
version="0.1.0"
|
||||
echo "No existing release tag; starting at $version"
|
||||
@@ -116,11 +118,197 @@ jobs:
|
||||
echo "::error::Release $tag already exists. Pick a different version."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "version=$version" >> "$GITHUB_OUTPUT"
|
||||
echo "tag=$tag" >> "$GITHUB_OUTPUT"
|
||||
|
||||
love-payload:
|
||||
name: build release game.love
|
||||
needs: version
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: Build shared payload
|
||||
run: |
|
||||
scripts/pack_love.sh \
|
||||
--output dist/payload/game.love \
|
||||
--listing dist/payload/love-listing.txt \
|
||||
--version "${{ needs.version.outputs.version }}"
|
||||
- name: Upload shared payload
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: gen1recomp-release-love
|
||||
path: dist/payload/game.love
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
linux-arm64:
|
||||
name: build Linux arm64 AppImage
|
||||
needs: [version, love-payload]
|
||||
# GitHub's free arm64 runner for public repos. It has to be arm64: the
|
||||
# AppImage compiles LÖVE natively inside a Debian bullseye arm64
|
||||
# container, and the qemu-emulated alternative takes hours.
|
||||
runs-on: ubuntu-24.04-arm
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: Download shared payload
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: gen1recomp-release-love
|
||||
path: .bazinga/work
|
||||
- name: Build Linux arm64 AppImage
|
||||
run: |
|
||||
set -euo pipefail
|
||||
scripts/build_linux_arm64.sh \
|
||||
--version "${{ needs.version.outputs.version }}" \
|
||||
--game-love .bazinga/work/game.love
|
||||
- name: Upload Linux arm64 release
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: gen1recomp-linux-arm64-release
|
||||
path: |
|
||||
dist/linux-arm64/gen1recomp-${{ needs.version.outputs.version }}-linux-arm64.AppImage
|
||||
dist/linux-arm64/gen1recomp-${{ needs.version.outputs.version }}-linux-arm64.AppImage.sha256
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
xbox-uwp:
|
||||
name: build Xbox UWP release
|
||||
needs: [version, love-payload]
|
||||
runs-on: windows-2022
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: Download shared payload
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: gen1recomp-release-love
|
||||
path: .bazinga/work
|
||||
- name: Prepare signing certificate
|
||||
shell: pwsh
|
||||
env:
|
||||
CERTIFICATE_BASE64: ${{ secrets.XBOX_UWP_SIGNING_CERTIFICATE }}
|
||||
CERTIFICATE_PASSWORD: ${{ secrets.XBOX_UWP_SIGNING_PASSWORD }}
|
||||
CANONICAL_REPOSITORY: ${{ github.repository == 'bryanthaboi/gen1recomp' }}
|
||||
run: |
|
||||
if ($env:CANONICAL_REPOSITORY -eq 'true' -and
|
||||
[string]::IsNullOrWhiteSpace($env:CERTIFICATE_BASE64)) {
|
||||
throw 'XBOX_UWP_SIGNING_CERTIFICATE is not configured.'
|
||||
}
|
||||
if ([string]::IsNullOrWhiteSpace($env:CERTIFICATE_BASE64)) {
|
||||
"UWP_PUBLISHER=CN=Gen1Recomp" | Out-File $env:GITHUB_ENV -Append
|
||||
exit 0
|
||||
}
|
||||
$pfx = Join-Path $env:RUNNER_TEMP 'gen1recomp-uwp.pfx'
|
||||
[IO.File]::WriteAllBytes($pfx, [Convert]::FromBase64String($env:CERTIFICATE_BASE64))
|
||||
$flags = [Security.Cryptography.X509Certificates.X509KeyStorageFlags]::EphemeralKeySet
|
||||
$cert = [Security.Cryptography.X509Certificates.X509Certificate2]::new(
|
||||
$pfx, $env:CERTIFICATE_PASSWORD, $flags)
|
||||
$cer = Join-Path $env:RUNNER_TEMP 'gen1recomp-uwp.cer'
|
||||
[IO.File]::WriteAllBytes(
|
||||
$cer,
|
||||
$cert.Export([Security.Cryptography.X509Certificates.X509ContentType]::Cert))
|
||||
Import-Certificate -FilePath $cer -CertStoreLocation Cert:\LocalMachine\TrustedPeople | Out-Null
|
||||
"UWP_PFX=$pfx" | Out-File $env:GITHUB_ENV -Append
|
||||
"UWP_CERT_THUMBPRINT=$($cert.Thumbprint)" | Out-File $env:GITHUB_ENV -Append
|
||||
"UWP_PUBLISHER=$($cert.Subject)" | Out-File $env:GITHUB_ENV -Append
|
||||
- name: Build Xbox UWP package
|
||||
shell: bash
|
||||
run: |
|
||||
bash scripts/build_xbox_uwp.sh \
|
||||
--release \
|
||||
--version "${{ needs.version.outputs.version }}" \
|
||||
--publisher "$UWP_PUBLISHER" \
|
||||
--game-love .bazinga/work/game.love
|
||||
- name: Sign and stage Xbox UWP release
|
||||
shell: pwsh
|
||||
env:
|
||||
CERTIFICATE_PASSWORD: ${{ secrets.XBOX_UWP_SIGNING_PASSWORD }}
|
||||
run: |
|
||||
if (-not $env:UWP_PFX) {
|
||||
exit 0
|
||||
}
|
||||
scripts/xbox-uwp/stage_release.ps1 `
|
||||
-Version '${{ needs.version.outputs.version }}' `
|
||||
-Configuration Release `
|
||||
-BuildInfo .bazinga/work/xbox-uwp-build-info.json `
|
||||
-CertificatePath $env:UWP_PFX `
|
||||
-CertificatePassword $env:CERTIFICATE_PASSWORD
|
||||
- name: Upload Xbox UWP release
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: gen1recomp-xbox-uwp-release
|
||||
path: |
|
||||
dist/xbox-uwp/gen1recomp-${{ needs.version.outputs.version }}-xbox-uwp.zip
|
||||
dist/xbox-uwp/gen1recomp-${{ needs.version.outputs.version }}-xbox-uwp.zip.sha256
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
- name: Remove signing certificate
|
||||
if: always()
|
||||
shell: pwsh
|
||||
run: |
|
||||
if ($env:UWP_CERT_THUMBPRINT) {
|
||||
Remove-Item "Cert:\LocalMachine\TrustedPeople\$env:UWP_CERT_THUMBPRINT" -ErrorAction SilentlyContinue
|
||||
}
|
||||
if ($env:UWP_PFX) {
|
||||
Remove-Item $env:UWP_PFX -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
# Windows Native AOT TLS dialer. The Mac release runner fuses the win64 zip
|
||||
# from LÖVE's prebuilt binaries and cannot cross-compile this DLL, so build
|
||||
# it here and inject it in the release job before scripts/build.sh win.
|
||||
native-tls-win:
|
||||
name: build Windows gen1tls.dll
|
||||
needs: version
|
||||
runs-on: windows-2022
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- name: Setup .NET 8
|
||||
uses: actions/setup-dotnet@v6
|
||||
with:
|
||||
dotnet-version: "8.0.x"
|
||||
- name: Publish gen1tls (win-x64 Native AOT)
|
||||
shell: pwsh
|
||||
run: |
|
||||
$out = "dist/native/win-x64"
|
||||
New-Item -ItemType Directory -Force -Path $out | Out-Null
|
||||
dotnet publish native/tls_dial/Gen1Tls.csproj `
|
||||
-c Release -r win-x64 -o $out
|
||||
if (-not (Test-Path "$out/gen1tls.dll")) {
|
||||
throw "gen1tls.dll missing after publish"
|
||||
}
|
||||
Get-Item "$out/gen1tls.dll" | Format-List Name, Length, LastWriteTime
|
||||
- name: Upload gen1tls.dll
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: gen1tls-win-x64
|
||||
path: dist/native/win-x64/gen1tls.dll
|
||||
if-no-files-found: error
|
||||
retention-days: 1
|
||||
|
||||
release:
|
||||
needs: [version, xbox-uwp, linux-arm64, native-tls-win]
|
||||
runs-on: ${{ fromJSON(github.repository == 'bryanthaboi/gen1recomp' && '["self-hosted", "macOS"]' || '"macos-latest"') }}
|
||||
|
||||
steps:
|
||||
# The self-hosted runner lives under the machine owner's home
|
||||
# directory; mask it first so absolute paths in every later step's
|
||||
# output show up as *** in the public workflow logs.
|
||||
- name: Mask runner paths
|
||||
run: echo "::add-mask::$HOME"
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v7
|
||||
with:
|
||||
fetch-depth: 0
|
||||
fetch-tags: true
|
||||
|
||||
- name: Download Windows gen1tls dialer
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: gen1tls-win-x64
|
||||
path: dist/native/win-x64
|
||||
|
||||
- name: Import signing certificate into a temporary keychain
|
||||
if: github.repository == 'bryanthaboi/gen1recomp'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
KEYCHAIN_PATH="$RUNNER_TEMP/pokemon-signing.keychain-db"
|
||||
@@ -157,27 +345,79 @@ jobs:
|
||||
security find-identity -v -p codesigning "$KEYCHAIN_PATH"
|
||||
|
||||
- name: Build macOS + Windows + Linux
|
||||
env:
|
||||
GEN1TLS_DLL: ${{ github.workspace }}/dist/native/win-x64/gen1tls.dll
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Sign in-build (identity auto-detected from the temp keychain);
|
||||
# notarize separately below so it uses secret credentials, not a
|
||||
# login-keychain profile. "all" also builds the Linux AppImage,
|
||||
# which needs no signing/notarization.
|
||||
scripts/build.sh all --version "${{ steps.ver.outputs.version }}" --no-notarize
|
||||
if [ ! -f "$GEN1TLS_DLL" ]; then
|
||||
echo "::error::gen1tls.dll missing at $GEN1TLS_DLL (native-tls-win job)"
|
||||
exit 1
|
||||
fi
|
||||
scripts/build.sh all --version "${{ needs.version.outputs.version }}" --no-notarize
|
||||
unzip -l dist/win/gen1recomp-win64.zip | grep -F gen1tls.dll \
|
||||
|| { echo "::error::Windows zip is missing gen1tls.dll"; exit 1; }
|
||||
|
||||
- name: Build Android
|
||||
run: |
|
||||
set -euo pipefail
|
||||
scripts/build_android.sh --version "${{ steps.ver.outputs.version }}"
|
||||
scripts/build_android.sh --version "${{ needs.version.outputs.version }}"
|
||||
|
||||
- name: Install xcbeautify
|
||||
run: |
|
||||
set -euo pipefail
|
||||
brew list xcbeautify >/dev/null 2>&1 || brew install xcbeautify
|
||||
|
||||
- name: Build iOS
|
||||
env:
|
||||
CANONICAL_REPOSITORY: ${{ github.repository == 'bryanthaboi/gen1recomp' }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ "$CANONICAL_REPOSITORY" = true ]; then
|
||||
scripts/build_ios.sh --fetch --device --release \
|
||||
--version "${{ needs.version.outputs.version }}"
|
||||
else
|
||||
scripts/build_ios.sh --fetch --release \
|
||||
--version "${{ needs.version.outputs.version }}"
|
||||
fi
|
||||
|
||||
- name: Build Switch
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Hard-fail gate: Switch ships with every release (never soft-fail).
|
||||
# PR CI is path-gated (ubuntu selftest + canonical fused); release
|
||||
# always builds Switch regardless of which files changed.
|
||||
# Needs native switch-tools (nacptool/elf2nro) and/or Docker on the
|
||||
# Mac self-hosted runner; see docs/switch-build.md.
|
||||
scripts/build_switch.sh --fetch --fused \
|
||||
--version "${{ needs.version.outputs.version }}"
|
||||
|
||||
- name: Build Anbernic RG34XXSP port
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Self-contained aarch64 PortMaster-style pack; pulls the LÖVE 11.5
|
||||
# runtime from PortMaster-GUI, so it needs no signing/notarization.
|
||||
./build-rg34xxsp.sh --version "${{ steps.ver.outputs.version }}"
|
||||
./build-rg34xxsp.sh --version "${{ needs.version.outputs.version }}"
|
||||
|
||||
- name: Build Linux ARM SBC PortMaster port
|
||||
env:
|
||||
# The release workflow must package the commit being released. The
|
||||
# script defaults to the latest published release for standalone
|
||||
# builds, while this explicit local override keeps CI source-aligned.
|
||||
GEN1RECOMP_SOURCE_DIR: ${{ github.workspace }}
|
||||
GEN1RECOMP_RELEASE_TAG: v${{ needs.version.outputs.version }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
# Same aarch64 PortMaster-style pack for Linux ARM SBC PortMaster. The build
|
||||
# keeps its own cache because the two scripts use different staging
|
||||
# layouts and runtime package paths.
|
||||
./build-linux-arm-sbc.sh --version "${{ needs.version.outputs.version }}"
|
||||
|
||||
- name: Notarize & staple macOS app
|
||||
if: github.repository == 'bryanthaboi/gen1recomp'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
ci_dir="${POKEMON_CI_DIR:-$HOME/.config/pokemon-ci}"
|
||||
@@ -211,27 +451,70 @@ jobs:
|
||||
ditto -c -k --sequesterRsrc --keepParent "$app" "$zip"
|
||||
echo "Notarized + stapled ✓"
|
||||
|
||||
- name: Download Xbox UWP release
|
||||
if: github.repository == 'bryanthaboi/gen1recomp'
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: gen1recomp-xbox-uwp-release
|
||||
path: dist/xbox-uwp
|
||||
|
||||
- name: Download Linux arm64 release
|
||||
if: github.repository == 'bryanthaboi/gen1recomp'
|
||||
uses: actions/download-artifact@v8
|
||||
with:
|
||||
name: gen1recomp-linux-arm64-release
|
||||
path: dist/linux-arm64
|
||||
|
||||
- name: Stage release assets
|
||||
if: github.repository == 'bryanthaboi/gen1recomp'
|
||||
id: assets
|
||||
run: |
|
||||
set -euo pipefail
|
||||
v="${{ steps.ver.outputs.version }}"
|
||||
v="${{ needs.version.outputs.version }}"
|
||||
outdir="dist/release"
|
||||
rm -rf "$outdir"
|
||||
mkdir -p "$outdir"
|
||||
cp "dist/mac/gen1recomp-macos.zip" "$outdir/gen1recomp-${v}-macos.zip"
|
||||
cp "dist/win/gen1recomp-win64.zip" "$outdir/gen1recomp-${v}-windows.zip"
|
||||
cp "dist/linux/gen1recomp-linux.zip" "$outdir/gen1recomp-${v}-linux.zip"
|
||||
|
||||
# arm64 desktop Linux (Raspberry Pi, Armbian, arm64 VMs). Built on
|
||||
# its own runner because LÖVE publishes no aarch64 binary and the
|
||||
# AppImage has to be compiled natively; ships as a runnable
|
||||
# AppImage rather than a zip so `chmod +x && ./it` just works.
|
||||
arm64_appimage="dist/linux-arm64/gen1recomp-${v}-linux-arm64.AppImage"
|
||||
[ -f "$arm64_appimage" ] || { echo "::error::$arm64_appimage not found (expected from the linux-arm64 job)"; exit 1; }
|
||||
cp "$arm64_appimage" "$outdir/gen1recomp-${v}-linux-arm64.AppImage"
|
||||
chmod +x "$outdir/gen1recomp-${v}-linux-arm64.AppImage"
|
||||
apk="$(find dist/android/debug -name '*.apk' | head -1)"
|
||||
[ -n "$apk" ] || { echo "::error::no Android APK found under dist/android/debug"; exit 1; }
|
||||
cp "$apk" "$outdir/gen1recomp-${v}-android.apk"
|
||||
|
||||
ipa="dist/ios/gen1recomp++.ipa"
|
||||
[ -f "$ipa" ] || { echo "::error::$ipa not found (expected from scripts/build_ios.sh --device)"; exit 1; }
|
||||
cp "$ipa" "$outdir/gen1recomp++-${v}-ios.ipa"
|
||||
|
||||
swzip="dist/switch/gen1recomp-${v}-switch.zip"
|
||||
[ -f "$swzip" ] || { echo "::error::$swzip not found (expected from scripts/build_switch.sh --fused → pack_sd_zip.sh)"; exit 1; }
|
||||
cp "$swzip" "$outdir/gen1recomp-${v}-switch.zip"
|
||||
# Local fused .nro stays under dist/switch/ for PR CI / debug; release
|
||||
# publishes the SD-ready zip only.
|
||||
|
||||
uwp="dist/xbox-uwp/gen1recomp-${v}-xbox-uwp.zip"
|
||||
[ -f "$uwp" ] || { echo "::error::$uwp not found (expected from the Xbox UWP job)"; exit 1; }
|
||||
cp "$uwp" "$outdir/gen1recomp-${v}-xbox-uwp.zip"
|
||||
|
||||
# Anbernic handheld port (suffix names the CFW it targets, so a
|
||||
# future RG35XX/other-CFW pack can ship alongside it).
|
||||
rg34="dist/rg34xxsp/gen1recomp-rg34xxsp-stockos64-mod.zip"
|
||||
[ -f "$rg34" ] || { echo "::error::$rg34 not found (expected from ./build-rg34xxsp.sh)"; exit 1; }
|
||||
cp "$rg34" "$outdir/gen1recomp-${v}-rg34xxsp-stockos64-mod.zip"
|
||||
|
||||
# Linux ARM SBC PortMaster handheld port.
|
||||
sbc="dist/linux-arm-sbc/gen1recomp-sbc-portmaster.zip"
|
||||
[ -f "$sbc" ] || { echo "::error::$sbc not found (expected from ./build-linux-arm-sbc.sh)"; exit 1; }
|
||||
cp "$sbc" "$outdir/gen1recomp-${v}-sbc-portmaster.zip"
|
||||
|
||||
# Platform-independent update payload, built alongside the desktop
|
||||
# apps above (same game.love that gets fused into each of them).
|
||||
love_file=".bazinga/work/game.love"
|
||||
@@ -247,26 +530,45 @@ jobs:
|
||||
cat "$outdir/sha256sums.txt"
|
||||
|
||||
- name: Publish GitHub Release
|
||||
if: github.repository == 'bryanthaboi/gen1recomp'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
v="${{ steps.ver.outputs.version }}"
|
||||
tag="${{ steps.ver.outputs.tag }}"
|
||||
v="${{ needs.version.outputs.version }}"
|
||||
tag="${{ needs.version.outputs.tag }}"
|
||||
|
||||
# Issues this release closes, per GitHub's own "closing issues" links
|
||||
# (works for squash, rebase, and merge commits alike). Scans every
|
||||
# commit since the previous tag so a skipped release run doesn't drop
|
||||
# issues on the floor.
|
||||
# Issues this release closes. Three sources, deduped by number:
|
||||
# 1. GitHub's own "closing issues" links on every PR whose
|
||||
# commits are in the range (works for squash, rebase, and
|
||||
# merge commits alike) -- including PRs that were merged
|
||||
# into a branch this release PR is itself merging in.
|
||||
# 2. CLOSES/fixes/resolves text in those PRs' titles + bodies
|
||||
# that GitHub didn't turn into a closing link (a PR into a
|
||||
# non-default branch never gets one).
|
||||
# 3. The same text in raw commit messages, so a direct push
|
||||
# with "CLOSES #N #M" still lands in the notes.
|
||||
# Scans every commit since the previous tag so a skipped release
|
||||
# run doesn't drop issues on the floor.
|
||||
prev_tag="$(git tag -l 'v*' --sort=-v:refname | grep -v "^${tag}$" | head -1 || true)"
|
||||
range="${prev_tag:+${prev_tag}..}$GITHUB_SHA"
|
||||
|
||||
closed=""
|
||||
for pr in $(git log --pretty=%H "$range" \
|
||||
| xargs -I{} gh api "repos/$GITHUB_REPOSITORY/commits/{}/pulls" \
|
||||
--jq '.[].number' 2>/dev/null \
|
||||
| sort -un || true); do
|
||||
closed+="$(gh api graphql \
|
||||
# every #N on a line that carries a closing keyword (handles the
|
||||
# multi-issue "CLOSES #1 #2 #3" form the tracker uses)
|
||||
scan_closes() {
|
||||
grep -iE '\b(close[sd]?|fix(es|ed)?|resolve[sd]?)\b' \
|
||||
| grep -oE '#[0-9]+' | tr -d '#' || true
|
||||
}
|
||||
|
||||
nums=""
|
||||
nums+=" $(git log --pretty=%B "$range" | scan_closes | tr '\n' ' ')"
|
||||
|
||||
prs="$(git log --pretty=%H "$range" \
|
||||
| xargs -I{} gh api "repos/$GITHUB_REPOSITORY/commits/{}/pulls" \
|
||||
--jq '.[].number' 2>/dev/null \
|
||||
| sort -un || true)"
|
||||
for pr in $prs; do
|
||||
nums+=" $(gh api graphql \
|
||||
-f owner="${GITHUB_REPOSITORY%/*}" \
|
||||
-f name="${GITHUB_REPOSITORY#*/}" \
|
||||
-F pr="$pr" \
|
||||
@@ -274,36 +576,140 @@ jobs:
|
||||
query($owner:String!, $name:String!, $pr:Int!) {
|
||||
repository(owner:$owner, name:$name) {
|
||||
pullRequest(number:$pr) {
|
||||
closingIssuesReferences(first:50) { nodes { number title } }
|
||||
closingIssuesReferences(first:50) { nodes { number } }
|
||||
}
|
||||
}
|
||||
}' \
|
||||
--jq '.data.repository.pullRequest.closingIssuesReferences.nodes[]
|
||||
| "- #\(.number) \(.title)"' 2>/dev/null || true)"$'\n'
|
||||
--jq '.data.repository.pullRequest.closingIssuesReferences.nodes[].number' \
|
||||
2>/dev/null | grep -E '^[0-9]+$' | tr '\n' ' ' || true)"
|
||||
nums+=" $(gh api "repos/$GITHUB_REPOSITORY/pulls/$pr" \
|
||||
--jq '.title + " " + (.body // "")' 2>/dev/null \
|
||||
| scan_closes | tr '\n' ' ' || true)"
|
||||
done
|
||||
|
||||
# dedupe, drop anything that is a PR or does not exist, keep
|
||||
# titles (gh api prints the response body to stdout on an HTTP
|
||||
# error, so only a zero exit counts)
|
||||
closed=""
|
||||
for n in $(printf '%s' "$nums" | tr ' ' '\n' | grep -E '^[0-9]+$' | sort -un); do
|
||||
if title="$(gh api "repos/$GITHUB_REPOSITORY/issues/$n" \
|
||||
--jq 'if .pull_request then empty else .title end' \
|
||||
2>/dev/null)"; then
|
||||
[ -n "$title" ] && closed+="- #$n $title"$'\n'
|
||||
fi
|
||||
done
|
||||
closed="$(printf '%s' "$closed" | grep . | sort -t'#' -k2 -n || true)"
|
||||
|
||||
# Everyone whose commits are in the range: GitHub login when the
|
||||
# commit is linked to an account, the raw git author name when not;
|
||||
# CI bots filtered out.
|
||||
base="${prev_tag:-$(git rev-list --max-parents=0 "$GITHUB_SHA" | tail -1)}"
|
||||
contributors="$(gh api --paginate \
|
||||
"repos/$GITHUB_REPOSITORY/compare/${base}...${GITHUB_SHA}" \
|
||||
--jq '.commits[] | if .author and .author.login
|
||||
then "@" + .author.login
|
||||
else .commit.author.name end' 2>/dev/null \
|
||||
| grep -viE '\[bot\]$' | sort -uf | sed 's/^/- /' || true)"
|
||||
|
||||
notes="Download the correct version for your computer below."
|
||||
if [ -n "$closed" ]; then
|
||||
notes+=$'\n\n## Issues closed\n\n'"$closed"
|
||||
fi
|
||||
if [ -n "$contributors" ]; then
|
||||
notes+=$'\n\n## Contributors\n\n'"$contributors"
|
||||
fi
|
||||
printf 'Release notes:\n%s\n' "$notes"
|
||||
|
||||
release_files=(
|
||||
"dist/release/gen1recomp-${v}-macos.zip"
|
||||
"dist/release/gen1recomp-${v}-windows.zip"
|
||||
"dist/release/gen1recomp-${v}-linux.zip"
|
||||
"dist/release/gen1recomp-${v}-linux-arm64.AppImage"
|
||||
"dist/release/gen1recomp-${v}-android.apk"
|
||||
"dist/release/gen1recomp++-${v}-ios.ipa"
|
||||
"dist/release/gen1recomp-${v}-switch.zip"
|
||||
"dist/release/gen1recomp-${v}-xbox-uwp.zip"
|
||||
"dist/release/gen1recomp-${v}-rg34xxsp-stockos64-mod.zip"
|
||||
"dist/release/gen1recomp-${v}-sbc-portmaster.zip"
|
||||
"dist/release/gen1recomp-${v}.love"
|
||||
"dist/release/sha256sums.txt"
|
||||
)
|
||||
|
||||
gh release create "$tag" \
|
||||
--target "$GITHUB_SHA" \
|
||||
--title "$v" \
|
||||
--notes "$notes" \
|
||||
"dist/release/gen1recomp-${v}-macos.zip" \
|
||||
"dist/release/gen1recomp-${v}-windows.zip" \
|
||||
"dist/release/gen1recomp-${v}-linux.zip" \
|
||||
"dist/release/gen1recomp-${v}-android.apk" \
|
||||
"dist/release/gen1recomp-${v}-rg34xxsp-stockos64-mod.zip" \
|
||||
"dist/release/gen1recomp-${v}.love" \
|
||||
"dist/release/sha256sums.txt"
|
||||
"${release_files[@]}"
|
||||
|
||||
echo "Published release $tag"
|
||||
|
||||
- name: Update iOS app repository
|
||||
if: github.repository == 'bryanthaboi/gen1recomp'
|
||||
run: |
|
||||
set -euo pipefail
|
||||
v="${{ needs.version.outputs.version }}"
|
||||
ipa="dist/release/gen1recomp++-${v}-ios.ipa"
|
||||
app_repo="mobile/ios/app-repo.json"
|
||||
[ -f "$ipa" ] || { echo "::error::$ipa not found"; exit 1; }
|
||||
[ -f "$app_repo" ] || { echo "::error::$app_repo not found"; exit 1; }
|
||||
|
||||
date="$(date -u +"%Y-%m-%d")"
|
||||
size="$(wc -c < "$ipa" | tr -d '[:space:]')"
|
||||
download_url="https://github.com/${GITHUB_REPOSITORY}/releases/download/v${v}/gen1recomp++-${v}-ios.ipa"
|
||||
bundle_id="com.theboisclub.gen1recompplusplus"
|
||||
localized_description="Gen1Recomp - A native Lua / LÖVE2D recreation of Gen 1 Poke"
|
||||
release_notes="$(GH_TOKEN="${{ github.token }}" gh release view "v${v}" --json body --jq '.body // ""' 2>/dev/null || true)"
|
||||
if [ -n "$release_notes" ]; then
|
||||
localized_description="$release_notes"
|
||||
fi
|
||||
entry="$(jq -n \
|
||||
--arg version "$v" \
|
||||
--arg date "$date" \
|
||||
--arg download_url "$download_url" \
|
||||
--arg localized_description "$localized_description" \
|
||||
--argjson size "$size" \
|
||||
'{version: $version, date: $date, size: $size, downloadURL: $download_url, localizedDescription: $localized_description}')"
|
||||
|
||||
if jq -e --arg bundle_id "$bundle_id" --arg version "$v" \
|
||||
'any(.apps[] | select(.bundleIdentifier == $bundle_id).versions[]?; .version == $version)' \
|
||||
"$app_repo" >/dev/null; then
|
||||
jq --arg bundle_id "$bundle_id" --arg version "$v" --argjson entry "$entry" \
|
||||
'(.apps[] | select(.bundleIdentifier == $bundle_id).versions) |= map(if .version == $version then $entry else . end)' \
|
||||
"$app_repo" > "$app_repo.tmp"
|
||||
else
|
||||
jq --arg bundle_id "$bundle_id" --argjson entry "$entry" \
|
||||
'(.apps[] | select(.bundleIdentifier == $bundle_id).versions) |= [$entry] + .' \
|
||||
"$app_repo" > "$app_repo.tmp"
|
||||
fi
|
||||
mv "$app_repo.tmp" "$app_repo"
|
||||
|
||||
# main is PR-only for everyone except deploy keys (the "main protection"
|
||||
# ruleset's bypass actor), so this push must authenticate with the
|
||||
# RELEASE_DEPLOY_KEY deploy key over SSH; the workflow's GITHUB_TOKEN
|
||||
# would be rejected by the branch protection.
|
||||
- name: Commit iOS app repository
|
||||
if: github.repository == 'bryanthaboi/gen1recomp'
|
||||
env:
|
||||
DEPLOY_KEY: ${{ secrets.RELEASE_DEPLOY_KEY }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
git add mobile/ios/app-repo.json
|
||||
if git diff --cached --quiet; then
|
||||
echo "app-repo.json unchanged; nothing to push"
|
||||
exit 0
|
||||
fi
|
||||
key="$RUNNER_TEMP/release-deploy-key"
|
||||
printf '%s\n' "$DEPLOY_KEY" > "$key"
|
||||
chmod 600 "$key"
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
||||
git commit -m "chore(ios): update app-repo.json [skip ci]"
|
||||
git -c core.sshCommand="ssh -i $key -o IdentitiesOnly=yes -o StrictHostKeyChecking=accept-new" \
|
||||
push "git@github.com:${GITHUB_REPOSITORY}.git" "HEAD:${GITHUB_REF_NAME}"
|
||||
rm -f "$key"
|
||||
|
||||
- name: Clean up signing keychain
|
||||
if: always()
|
||||
if: ${{ always() && github.repository == 'bryanthaboi/gen1recomp' }}
|
||||
run: |
|
||||
security delete-keychain "$RUNNER_TEMP/pokemon-signing.keychain-db" 2>/dev/null || true
|
||||
rm -f "$RUNNER_TEMP/release-deploy-key"
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
name: Switch artifact comment
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: [ci]
|
||||
types: [completed]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
comment:
|
||||
if: github.event.workflow_run.event == 'pull_request' && github.event.workflow_run.conclusion == 'success'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- id: artifact
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
RUN_ID: ${{ github.event.workflow_run.id }}
|
||||
HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }}
|
||||
HEAD_REPOSITORY: ${{ github.event.workflow_run.head_repository.full_name }}
|
||||
run: |
|
||||
artifact_id="$(gh api "repos/$GITHUB_REPOSITORY/actions/runs/$RUN_ID/artifacts" --jq '.artifacts[] | select(.name == "gen1recomp-switch-nro") | .id')"
|
||||
[ -n "$artifact_id" ] || exit 0
|
||||
head_owner="${HEAD_REPOSITORY%%/*}"
|
||||
pr_number="$(gh api "repos/$GITHUB_REPOSITORY/pulls?state=open&head=$head_owner:$HEAD_BRANCH" --jq '.[0].number // empty')"
|
||||
[ -n "$pr_number" ] || exit 0
|
||||
echo "artifact_url=https://github.com/$GITHUB_REPOSITORY/actions/runs/$RUN_ID/artifacts/$artifact_id" >> "$GITHUB_OUTPUT"
|
||||
echo "pr_number=$pr_number" >> "$GITHUB_OUTPUT"
|
||||
# Upsert via comment-tag only — do not delete-all bot comments (clobbers iOS).
|
||||
- name: Get build info
|
||||
id: build-info
|
||||
env:
|
||||
HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
|
||||
run: |
|
||||
commit_hash="$(printf '%s' "$HEAD_SHA" | cut -c1-7)"
|
||||
build_time="$(date "+%Y-%m-%d %H:%M:%S")"
|
||||
echo "hash=$commit_hash" >> "$GITHUB_OUTPUT"
|
||||
echo "time=$build_time" >> "$GITHUB_OUTPUT"
|
||||
- name: comment Switch artifact
|
||||
if: steps.artifact.outputs.pr_number != ''
|
||||
uses: thollander/actions-comment-pull-request@v3
|
||||
with:
|
||||
message: |
|
||||
[gen1recomp-switch.nro](${{ steps.artifact.outputs.artifact_url }})
|
||||
|
||||
**Commit**: [#${{ steps.build-info.outputs.hash }}](https://github.com/${{ github.event.workflow_run.head_repository.full_name }}/commit/${{ github.event.workflow_run.head_sha }})
|
||||
**Build Time**: `${{ steps.build-info.outputs.time }}`
|
||||
|
||||
<sub>This comment was automatically generated. [View workflow run](https://github.com/${{ github.repository }}/actions/runs/${{ github.event.workflow_run.id }})</sub>
|
||||
pr-number: ${{ steps.artifact.outputs.pr_number }}
|
||||
comment-tag: switch-build-result
|
||||
github-token: ${{ github.token }}
|
||||
@@ -3,8 +3,9 @@
|
||||
data/generated/
|
||||
assets/generated/
|
||||
|
||||
# LÖVE packages
|
||||
# LÖVE packages & archives
|
||||
*.love
|
||||
*.zip
|
||||
|
||||
# Local saves (LÖVE writes to its save dir, but keep the repo clean anyway)
|
||||
save/
|
||||
@@ -16,6 +17,7 @@ __pycache__/
|
||||
.*
|
||||
!.github/
|
||||
!.gitignore
|
||||
!.luacheckrc
|
||||
|
||||
# Android build outputs / local SDK path / packaged payload (keep love-android sources)
|
||||
mobile/android/app/build/
|
||||
@@ -30,8 +32,56 @@ mobile/ios/love-src/
|
||||
mobile/ios/cache/
|
||||
mobile/ios/build/
|
||||
|
||||
# Final packaged build artifacts (mac/win/web/android/ios) — see scripts/build.sh
|
||||
# love-nx vendor binaries (fetch per docs/switch-build.md; also covered by .*)
|
||||
.bazinga/love-nx/
|
||||
|
||||
# Final packaged build artifacts (mac/win/web/android/ios/switch) — see scripts/build.sh
|
||||
/dist/
|
||||
|
||||
# Switch OTA launcher build outputs
|
||||
ports/switch/ota-launcher/build/
|
||||
ports/switch/ota-launcher/build-host/
|
||||
ports/switch/ota-launcher/*.nro
|
||||
ports/switch/ota-launcher/*.nacp
|
||||
ports/switch/ota-launcher/*.elf
|
||||
ports/switch/ota-launcher/*.map
|
||||
ports/switch/ota-launcher/romfs/logo.rgba
|
||||
ports/switch/ota-launcher/romfs/logo.png
|
||||
ports/switch/ota-launcher/romfs/cacert.pem
|
||||
ports/switch/ota-launcher/romfs/ota-bootstrap.nro
|
||||
ports/switch/ota-bootstrap/build/
|
||||
ports/switch/ota-bootstrap/*.nro
|
||||
ports/switch/ota-bootstrap/*.nacp
|
||||
ports/switch/ota-bootstrap/*.elf
|
||||
ports/switch/ota-bootstrap/*.map
|
||||
|
||||
# Legacy manual convenience-copy location (superseded by /dist/android/)
|
||||
mobile/dist/
|
||||
|
||||
# Tiled map-editing workspace (tools/tiled_export.py). Derived from the imported
|
||||
# ROM cache exactly like data/generated/, so it is never committable.
|
||||
/build/tiled/
|
||||
|
||||
# per-machine iOS bundle-id pin (see scripts/build_ios.sh)
|
||||
mobile/ios/bundle_id.local
|
||||
|
||||
# Xbox UWP build output
|
||||
/ports/uwp/build/
|
||||
/ports/uwp/third_party/*/source/
|
||||
/ports/uwp/third_party/angle/depot_tools/
|
||||
|
||||
# Native TLS dialer build output (dotnet publish)
|
||||
/native/tls_dial/bin/
|
||||
/native/tls_dial/obj/
|
||||
/dist/native/
|
||||
/dist/win/
|
||||
/.bazinga/
|
||||
|
||||
# Local options / preferences
|
||||
/options.lua*
|
||||
|
||||
# User-owned ROMs imported for individual mods. Manifests declare the
|
||||
# destinations, but source checkouts and packaged mods never ship the files.
|
||||
/mods/*/baseroms/
|
||||
/imports/baseroms/
|
||||
/imports/baseroms-recovery/
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
-- Static-analysis config for `luacheck` (https://luacheck.readthedocs.io).
|
||||
--
|
||||
-- Run it over the engine with: luacheck src (or scripts/lint.sh)
|
||||
--
|
||||
-- The point is a high-signal baseline: the categories left on are the ones
|
||||
-- that catch real defects -- undefined globals/locals (the class that hid a
|
||||
-- `music.volume` crash: applyVolume read a `state` that was still the nil
|
||||
-- global), unused values, unreachable code, redefinitions. The cosmetic
|
||||
-- categories the codebase deliberately lives with (a `self`/`dt` an
|
||||
-- interface requires but a given method ignores, documented empty
|
||||
-- fall-through branches, the odd long line) are muted so they don't drown
|
||||
-- the signal.
|
||||
|
||||
std = "luajit"
|
||||
|
||||
-- LÖVE exposes `love` as a mutable table: games assign their callbacks onto
|
||||
-- it (love.wheelmoved, love.run, ...), so it is a regular global, not
|
||||
-- read-only -- otherwise every callback registration reads as a violation.
|
||||
globals = { "love" }
|
||||
|
||||
read_globals = {
|
||||
"jit",
|
||||
-- LuaJIT 2.1 ships table.unpack even though the bare 5.1 `table` std lacks
|
||||
-- it; without this, every `table.unpack` reads as an undefined field.
|
||||
table = { fields = { "unpack" } },
|
||||
}
|
||||
|
||||
-- Vendored/native trees and the test suites have their own conventions.
|
||||
exclude_files = {
|
||||
"mobile/",
|
||||
"tests/",
|
||||
"tools/save-editor/",
|
||||
}
|
||||
|
||||
ignore = {
|
||||
"212", -- unused argument -- self/dt kept for a shared method signature
|
||||
"213", -- unused loop variable -- `for _, v in` where only v is wanted
|
||||
"421", -- shadowing a local -- deliberate re-use in a few tight scopes
|
||||
"431", -- shadowing an upvalue
|
||||
"432", -- shadowing an argument
|
||||
"542", -- empty if branch -- documented fall-throughs, not gaps
|
||||
"631", -- line is too long
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
# AI Disclosure
|
||||
|
||||
This is a disclosure of the use of AI in this project.
|
||||
|
||||
## AI Use
|
||||
|
||||
Anyone who demands the dislosure of how AI was used in an engineering project,
|
||||
has no idea what AI is, or how it works.
|
||||
|
||||
AI was used in this project as a tool. Several contributors used AI in their
|
||||
commits, and so you will see like 7 commits by Claude or Codex or Cursor.
|
||||
|
||||
However those commits were reviewed by human beings, and it was declared that
|
||||
the exact same fix would have been done by a human, so they were accepted.
|
||||
|
||||
AI was not used to make decisions, or to create the project.
|
||||
|
||||
If you would like to read more, well then continue reading:
|
||||
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Quisque ante leo, luctus in semper a, maximus ut est. Vivamus nec magna vitae quam luctus suscipit nec eu orci. Vestibulum ut felis a dolor cursus vulputate. Phasellus pharetra elementum sollicitudin. Aenean elementum imperdiet ultrices. In risus mauris, scelerisque sed viverra in, iaculis non eros. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin quam justo, maximus sit amet fringilla in, tristique eget sapien.
|
||||
|
||||
Pellentesque rhoncus, erat nec elementum ultricies, augue purus suscipit augue, in condimentum nisl enim vel velit. Etiam at semper turpis. Vestibulum ipsum magna, ultrices non sem ut, hendrerit bibendum mi. Curabitur in varius sapien. Morbi posuere bibendum ex, at ultrices orci. Fusce feugiat ultrices varius. Suspendisse sed ante ligula. Sed dignissim lorem est, nec tristique arcu commodo sed. Cras consectetur sapien dolor, vitae finibus enim lacinia id.
|
||||
|
||||
Donec quis magna est. Maecenas dui arcu, venenatis sit amet libero nec, lacinia eleifend leo. Quisque lobortis vulputate lacus a elementum. Proin nec metus lectus. Donec eu auctor sem, at finibus ipsum. Curabitur eget dignissim justo. Donec lobortis leo eu arcu tristique, in volutpat augue eleifend. Morbi lacinia a risus in suscipit. Maecenas suscipit est eu interdum dictum. Cras in nulla imperdiet, dapibus mauris posuere, facilisis velit. Nunc dapibus, leo quis interdum tempor, elit mi mattis dolor, sagittis dictum mi urna sed lectus. Maecenas elementum, mauris id molestie dapibus, diam arcu egestas erat, at tempor justo orci vitae nibh. Etiam sagittis facilisis erat a vulputate. Praesent condimentum ac odio quis sollicitudin.
|
||||
|
||||
Fusce vitae orci vestibulum, sagittis dolor non, cursus urna. Morbi eleifend pretium pellentesque. Pellentesque ornare elementum sem in imperdiet. Maecenas dapibus, erat et lobortis porttitor, velit magna auctor odio, quis interdum elit est eu justo. In posuere euismod odio, in porttitor magna iaculis eget. In id quam pulvinar, ultrices dolor in, pellentesque dolor. Nunc varius ante at felis dictum, id porttitor sem efficitur. Integer pretium dignissim commodo. Suspendisse in est a arcu blandit faucibus. Donec quis lacus mollis, tincidunt nunc quis, suscipit nunc. Nunc non arcu dignissim, dignissim sem in, finibus neque. Aliquam non porta eros. Donec et pretium augue, non cursus eros.
|
||||
|
||||
Nunc at dignissim nisi. Nam nec metus augue. Proin nulla sapien, tristique a purus vel, vulputate commodo mi. Sed id erat leo. Quisque ullamcorper a nisl id molestie. Aliquam erat volutpat. Donec eget hendrerit mauris. Fusce tincidunt nisl a lorem tincidunt dapibus. Nam volutpat rhoncus tortor.
|
||||
|
||||
Nunc et sapien enim. Proin at nunc a nulla maximus consectetur nec eget tortor. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Mauris orci odio, sodales et elementum laoreet, porta at lacus. Maecenas vestibulum lectus risus, pulvinar scelerisque dolor posuere viverra. Proin gravida tellus vitae accumsan dignissim. Nunc non sapien aliquet ex cursus ultricies ac quis diam. Sed luctus feugiat risus eu tincidunt. Duis auctor lacinia fringilla. Donec pretium cursus magna a feugiat. Duis tristique, leo vulputate semper iaculis, est ipsum dapibus lacus, et molestie nulla enim nec ex. Nunc non feugiat neque.
|
||||
|
||||
Fusce euismod egestas elit ut pretium. Nulla eros quam, auctor sit amet faucibus eu, scelerisque eu neque. Sed nisi felis, lobortis in sapien a, tempor efficitur nunc. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Pellentesque ut justo est. Sed maximus, tellus in bibendum posuere, quam augue finibus eros, sed gravida arcu diam quis lectus. Donec eu placerat ligula. Ut quis imperdiet lorem. Maecenas a mi ac augue semper sodales. Curabitur in justo velit. Praesent et felis quis enim porttitor sagittis.
|
||||
|
||||
Nulla sed sagittis felis, sit amet placerat tortor. Ut metus est, sollicitudin ac turpis quis, aliquet congue lorem. Fusce auctor erat non convallis aliquet. Nullam sodales rutrum tellus ac malesuada. Quisque sem diam, iaculis in ultricies sit amet, fermentum quis sem. Integer condimentum placerat purus non lacinia. Integer hendrerit ultricies tellus, at dignissim nibh. Suspendisse accumsan eget tortor nec cursus. Proin accumsan rhoncus leo, eget pretium est tristique ac.
|
||||
|
||||
Sed feugiat sed diam a porta. Nullam varius lacus at fermentum fringilla. Morbi pharetra scelerisque pharetra. Nulla placerat vitae ligula non efficitur. Suspendisse quam dui, rutrum eget nulla eu, semper eleifend ante. Aenean ut condimentum arcu. Suspendisse auctor metus non sem ornare, vel tincidunt odio vehicula. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos.
|
||||
|
||||
Donec vel metus ut libero sollicitudin posuere a nec nulla. Vivamus a scelerisque nisi. Aliquam eu sollicitudin tortor. Aliquam purus eros, blandit et quam et, pretium porttitor sem. Nunc iaculis arcu enim, et maximus sem malesuada in. Morbi nec nunc volutpat, semper diam sit amet, gravida elit. Vestibulum eu turpis vel lacus imperdiet congue. Donec rhoncus auctor sem.
|
||||
|
||||
Suspendisse eu lorem non dolor pretium finibus euismod quis dolor. Cras finibus egestas velit, commodo rutrum est placerat sit amet. Pellentesque vitae semper diam, sit amet auctor metus. Sed porttitor porttitor nunc, vel imperdiet neque volutpat quis. Sed hendrerit sapien et lacus imperdiet, nec hendrerit lorem sodales. Integer lobortis rutrum odio at ullamcorper. Aliquam tincidunt magna a tellus suscipit, at porta turpis dapibus. Ut ultricies auctor felis eu feugiat. Sed tempus sem et dictum fringilla. Nunc non pellentesque tortor. Suspendisse pulvinar, arcu ut imperdiet gravida, eros ex mattis mauris, vel ultricies est erat et dui. Praesent porttitor tortor et erat interdum efficitur. Phasellus et luctus lectus, et egestas ante. Praesent ex ipsum, rutrum id efficitur et, vulputate non tortor. Aenean maximus nunc ac purus sodales, et venenatis lacus laoreet.
|
||||
|
||||
Cras egestas ultrices dui, at tempor leo varius vitae. Donec porta, nisl nec ornare maximus, est arcu auctor mauris, varius elementum nisl arcu venenatis neque. Aenean metus quam, vestibulum eget justo non, hendrerit dapibus nunc. Vivamus diam ante, mattis sed nulla at, iaculis elementum magna. Sed massa diam, efficitur vel nunc sed, malesuada interdum tortor. Aliquam non neque aliquet ex imperdiet finibus eget ac neque. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae;
|
||||
|
||||
Pellentesque fringilla tortor metus, luctus commodo leo gravida et. Fusce nec turpis at lorem rutrum porta vel in justo. Curabitur mattis suscipit felis, id dapibus arcu ornare ut. Proin sapien felis, pulvinar ut tristique vitae, aliquet ac dui. Vestibulum a erat tellus. Vestibulum sagittis dolor eget augue egestas fringilla. Fusce et purus a nunc auctor dapibus vel sed dui. Fusce interdum, libero vel pellentesque rutrum, urna massa iaculis tellus, et aliquam lectus diam vel sem. Donec a nunc et dui semper gravida. Donec posuere, eros eu consequat efficitur, justo metus ullamcorper lectus, et molestie massa ipsum eu sapien. Mauris eu suscipit neque. Morbi convallis sit amet leo a scelerisque. Cras ultrices libero ac mattis accumsan. Nam gravida ligula id erat semper ornare. Duis consequat ut ipsum eu volutpat. Quisque egestas sollicitudin ullamcorper.
|
||||
|
||||
Cras pellentesque quam non neque porta fringilla. Integer elementum, augue mattis blandit consequat, enim ipsum finibus ex, quis finibus neque eros eu ex. Fusce at urna justo. Donec erat eros, maximus id mauris vel, rutrum rutrum sapien. Morbi sed rutrum ex. Suspendisse lacus velit, varius ut elementum vitae, finibus non enim. Suspendisse vehicula euismod ipsum, id consequat nulla sodales vel. Morbi eu sem id leo congue dapibus a nec velit. Sed nec neque quam. Etiam rhoncus id nulla id volutpat. Nulla facilisi. Donec non maximus enim. Aenean consequat, sapien sit amet malesuada rutrum, erat sem euismod sapien, et feugiat lectus mauris id velit.
|
||||
|
||||
Fusce sodales porttitor gravida. Proin placerat ante nec nibh tempor aliquam. Sed ut diam eu sem fringilla malesuada. Maecenas aliquam risus vel quam dictum, at iaculis nibh pretium. Mauris convallis quam vitae dolor varius suscipit. Etiam nec fermentum dui. Aliquam in magna tincidunt, consectetur quam eget, aliquam purus. Ut dictum aliquet finibus. Pellentesque vel lacinia felis. Nulla malesuada vestibulum varius. Sed quam diam, efficitur id felis in, volutpat bibendum erat. Praesent luctus vulputate urna at interdum. Aenean ac aliquam eros. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Morbi mollis, nisl vel consequat vehicula, massa tellus porta arcu, vel dictum sapien mauris sit amet nunc. Integer sed maximus neque, ac iaculis urna.
|
||||
|
||||
Integer non erat a leo euismod convallis quis eget magna. Morbi gravida ac urna sed ornare. Nunc vehicula mauris accumsan, ornare sem in, egestas mauris. Vestibulum vel vulputate felis. Nulla eu scelerisque diam. Suspendisse ac odio tempor nunc pellentesque hendrerit at a magna. Vivamus ultrices nunc ut orci fermentum pharetra. Nullam laoreet hendrerit ligula ut gravida. Proin scelerisque magna sit amet arcu malesuada, pharetra ultrices est molestie. Nullam pulvinar placerat dui, vitae hendrerit tortor luctus sed. Pellentesque elementum tellus eget arcu pulvinar varius.
|
||||
|
||||
Ut placerat, magna vitae tincidunt ultricies, est orci aliquet urna, at luctus augue erat vel ipsum. Fusce odio sem, venenatis vel consequat nec, bibendum sed dolor. Cras a sodales eros. Nullam eget dui congue, vehicula purus ut, condimentum dui. Maecenas libero ipsum, condimentum tincidunt nisi in, sodales lacinia ex. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus.
|
||||
|
||||
Duis gravida velit ac euismod suscipit. Proin sed ligula erat. Nullam eu ornare massa, non fringilla felis. Curabitur eu erat ex. Quisque sit amet dolor id arcu mattis scelerisque at at eros. In arcu nulla, fermentum non maximus eget, rhoncus in lacus. Nulla sit amet augue eu tortor vulputate congue. Vivamus laoreet condimentum tempus. Maecenas tempor, diam sed laoreet venenatis, mauris arcu lacinia enim, quis facilisis nunc turpis a ligula. Pellentesque quis placerat nisi, sit amet ullamcorper diam. Suspendisse a elementum elit, vel tristique dolor. Nam pretium ante tortor, vel tristique ipsum ultricies ut. Quisque non lectus imperdiet, placerat erat ac, pharetra tellus. In condimentum at magna a posuere. Aliquam et fringilla ipsum. Sed facilisis, nulla a finibus gravida, elit elit vulputate velit, dictum ornare est sapien a nisl.
|
||||
|
||||
Mauris eleifend vulputate felis sed mattis. Praesent id velit vitae ex porta pretium. Cras mollis malesuada justo, ut ornare quam placerat ac. Donec lobortis arcu tellus, luctus tempor mi malesuada quis. Maecenas condimentum libero vitae finibus malesuada. Vestibulum sollicitudin fringilla diam eget egestas. Sed vulputate urna nec ipsum maximus hendrerit. Maecenas blandit ex ut massa sodales, vitae tincidunt lorem ullamcorper. Phasellus vitae nisl ornare, cursus sem a, pulvinar arcu. Vestibulum faucibus risus nec tincidunt pellentesque. Pellentesque vel porttitor ex. Vivamus sollicitudin gravida lacus in suscipit. Aliquam urna neque, sodales quis quam ac, suscipit condimentum ante.
|
||||
|
||||
Morbi id arcu sit amet sapien ornare gravida eget quis sapien. In hac habitasse platea dictumst. In quis interdum ligula. Donec sed mi vulputate, scelerisque turpis vitae, interdum odio. Proin tristique condimentum arcu, et malesuada tellus convallis in. Fusce egestas maximus magna, sit amet convallis velit porttitor ut. Curabitur venenatis lacus ut blandit convallis. Phasellus scelerisque congue turpis eget vehicula. Nam venenatis mi sit amet rhoncus pretium. Nulla sed odio purus. Phasellus cursus id sapien ut feugiat. Duis et ipsum vel dui tempus porta. Curabitur non tortor consectetur, sollicitudin tellus mollis, sollicitudin lorem.
|
||||
|
||||
Ut quis ornare justo. Nunc aliquam, leo sit amet placerat placerat, dui nulla luctus dui, ac iaculis nisl orci id metus. Vestibulum nunc nunc, porta nec dictum id, feugiat et ante. Quisque lobortis, lacus tristique vestibulum rhoncus, massa nulla dignissim massa, at scelerisque massa nunc at velit. Donec eu ipsum nec dui luctus pulvinar et et turpis. Quisque eu neque erat. Donec varius egestas nunc, ut pretium libero tempor ac. Vestibulum pellentesque mi erat, et semper dolor semper vitae. Morbi enim dui, laoreet non venenatis sit amet, dignissim a orci. Sed id odio turpis. Phasellus non rutrum magna. Maecenas placerat arcu ultricies ultrices congue. Nulla quis neque ligula. Etiam in diam commodo, pharetra mauris ac, pretium nisi. Praesent sed nibh nec odio condimentum commodo vel vel lacus.
|
||||
|
||||
Pellentesque id libero vitae ex egestas pharetra placerat nec augue. Ut eget lobortis lorem, at vehicula sapien. Aliquam eu tincidunt ligula. Aenean et vestibulum dui, quis porttitor dui. Nullam quis dolor libero. Sed accumsan eros vitae nisi ornare congue. Aliquam nisi sapien, sollicitudin quis odio vel, pharetra maximus urna.
|
||||
|
||||
Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Morbi accumsan felis id urna malesuada, non vulputate velit maximus. Proin urna velit, viverra a metus vel, sollicitudin faucibus nulla. Vivamus ipsum lectus, pharetra sit amet varius vel, ullamcorper nec neque. Suspendisse potenti. Vivamus sit amet justo ac augue placerat hendrerit in eu felis. Integer luctus ex quam, in pellentesque eros interdum sed. Fusce finibus quis neque vitae efficitur. Pellentesque vulputate consectetur egestas. Integer a neque fermentum, tincidunt ligula id, gravida urna. Pellentesque ultrices, leo et suscipit accumsan, lorem nunc porta dui, non congue ligula leo ut urna. Duis vehicula risus in mi eleifend luctus. Duis convallis, mi faucibus pellentesque cursus, libero mauris varius sem, sit amet fermentum massa metus nec tellus. In tortor ligula, faucibus eu nibh id, lobortis viverra erat. Morbi non nisi suscipit, mollis enim at, convallis velit.
|
||||
|
||||
Pellentesque consequat imperdiet felis quis scelerisque. Duis aliquam mollis nibh quis tincidunt. Vivamus elit odio, blandit quis volutpat at, blandit nec tortor. Cras maximus ex at odio maximus, dapibus condimentum risus malesuada. Pellentesque viverra orci at ante commodo, quis posuere sapien efficitur. Nunc tristique imperdiet diam elementum lobortis. Fusce velit dui, ultrices id ante pharetra, fermentum egestas augue. Curabitur ante augue, vestibulum non magna quis, feugiat pulvinar diam. Suspendisse sagittis dui a tellus scelerisque, ut tincidunt neque accumsan. Cras pharetra metus vel eros tincidunt, vel tincidunt lacus egestas. Donec eget pellentesque sapien. Cras condimentum in justo pulvinar feugiat. Quisque malesuada ac odio eget rhoncus. Fusce posuere justo sed finibus ornare.
|
||||
|
||||
Duis a auctor tortor. Pellentesque lobortis auctor risus, ultrices varius mi cursus quis. Sed dui nulla, mattis sit amet justo mattis, condimentum commodo justo. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Pellentesque hendrerit diam vel lacinia volutpat. Sed et luctus nunc, vitae congue arcu. Aenean placerat tincidunt ipsum. Ut molestie orci eu dapibus viverra. Cras sodales ullamcorper augue, at aliquam ex. Sed et justo augue.
|
||||
|
||||
Nullam feugiat risus et turpis faucibus, vel tincidunt nulla consequat. Aliquam libero erat, pellentesque sit amet tellus in, tempor ornare nisl. Donec viverra eget magna non pharetra. Cras sollicitudin, justo ut porttitor venenatis, risus nulla auctor est, at commodo sem urna in mi. Vestibulum mattis sapien vel nibh pulvinar, vel dignissim lacus cursus. Maecenas vel dictum tortor, ac ultricies justo. Praesent quis venenatis nisl. Morbi a diam fringilla, auctor lectus sed, varius est. Praesent faucibus auctor dolor, a commodo nisi mattis id. Fusce porta molestie ultrices. Sed pulvinar, leo ac consectetur hendrerit, erat velit gravida enim, eu blandit ligula justo sit amet neque.
|
||||
|
||||
Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Aenean libero risus, porttitor in congue vel, consequat sed felis. Vivamus eget blandit ante. Vivamus vitae mattis massa. In ut dolor sit amet tellus sollicitudin mattis. Mauris iaculis nisl neque, in gravida lectus ultricies nec. Fusce vehicula vehicula lacinia. Fusce viverra sed nisl id rhoncus. Donec sed porta mi. Nam tempus purus non massa tincidunt iaculis. Morbi viverra massa ut gravida vestibulum.
|
||||
|
||||
Proin dapibus mi a libero sagittis, id vehicula nulla iaculis. Proin a enim in tortor tincidunt egestas. Integer finibus neque eu nibh pretium, et pellentesque urna finibus. Integer a sollicitudin mauris, at convallis erat. Nullam sit amet lectus sed turpis commodo efficitur. Nulla nec turpis dapibus, suscipit diam quis, vulputate urna. Aenean sed posuere justo.
|
||||
|
||||
Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Sed luctus purus nibh, id imperdiet risus pulvinar in. Quisque auctor sem lacinia turpis mollis, nec pretium ipsum suscipit. Aliquam sed metus sagittis, mollis nisi eget, hendrerit libero. Nulla sodales erat semper nisl condimentum, ultricies rhoncus lacus commodo. Ut suscipit libero augue, non vulputate dui tristique vel. Praesent convallis efficitur est, sed tincidunt mauris aliquet in. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos.
|
||||
|
||||
Etiam a vehicula purus. Curabitur lacus erat, ultrices et dictum id, posuere ut risus. Etiam at auctor leo. Pellentesque eleifend est a metus lacinia rutrum. Nullam justo ligula, tempor non facilisis vel, volutpat sed nulla. Integer non dolor consequat, dapibus lectus eu, luctus turpis. Aenean enim erat, sagittis vitae ornare bibendum, faucibus sit amet magna. Phasellus suscipit ultrices faucibus. Sed at risus molestie, viverra ipsum vel, bibendum lectus. Pellentesque molestie vitae risus non viverra. Phasellus eleifend massa id odio sagittis mattis. Nullam velit mauris, viverra quis fermentum sit amet, vestibulum ut ex. Suspendisse imperdiet, sapien sed sagittis pharetra, nisi nibh vulputate metus, quis mattis dolor nisi ut lorem. Praesent vestibulum nibh vulputate lacus pulvinar tempor. Vestibulum vulputate diam ligula, vitae efficitur enim dapibus non. Etiam at ornare enim.
|
||||
|
||||
Curabitur aliquet velit enim, euismod faucibus urna euismod sit amet. Vivamus viverra vulputate nulla, ut gravida neque rutrum in. Suspendisse potenti. Nulla vitae neque felis. Etiam eu erat ac nulla ornare volutpat. Quisque ut diam dui. Sed ut massa quis dolor volutpat eleifend. Duis posuere dolor sit amet varius auctor. Donec mollis malesuada erat, eu luctus libero viverra feugiat. Curabitur fermentum velit eu purus fringilla, consequat tincidunt diam rutrum. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc volutpat vel est in vehicula. Morbi euismod, tortor ut posuere ullamcorper, velit justo ultricies lorem, vitae tincidunt erat ante vitae sem. Praesent semper, quam eget condimentum finibus, metus leo imperdiet augue, nec fringilla sem nunc id sapien.
|
||||
|
||||
Aliquam vestibulum ante porta sem finibus, ut rhoncus elit sodales. Phasellus a lacus congue, sagittis nulla eget, cursus libero. Duis laoreet fringilla faucibus. Aenean gravida lorem sed fringilla facilisis. Pellentesque sodales urna lorem, non rutrum tortor vulputate eget. Duis a enim semper, iaculis sem ac, facilisis urna. Donec iaculis nulla sit amet dignissim volutpat. Mauris cursus dui id feugiat suscipit. Fusce tempor placerat nulla vitae vestibulum. Vivamus imperdiet blandit nulla, in aliquet justo viverra in. Cras malesuada molestie ligula sit amet volutpat. Praesent ornare orci sit amet rutrum eleifend. Ut placerat metus felis, id malesuada justo mollis eget. Etiam mi turpis, pulvinar in pulvinar in, tincidunt in neque.
|
||||
|
||||
In venenatis euismod neque, eu convallis diam semper ac. Mauris auctor mi non massa vestibulum viverra. Aenean non turpis sapien. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Praesent gravida feugiat interdum. Vivamus sit amet consequat ligula. Praesent dictum nunc ac sapien ullamcorper consectetur. Ut malesuada blandit neque.
|
||||
|
||||
Duis tincidunt mauris sit amet odio dictum accumsan. Suspendisse efficitur nibh magna, ac dapibus augue vulputate nec. Etiam lectus neque, sollicitudin quis sapien vitae, aliquet fermentum mi. Aenean interdum interdum rhoncus. Donec eu libero urna. Donec semper lacus eu nunc scelerisque, vitae viverra quam consectetur. Integer sagittis, nulla congue venenatis auctor, purus justo mollis metus, at rutrum magna arcu nec est. Maecenas felis lorem, consequat non cursus vitae, lobortis vitae nisi. Cras eget magna justo. Nullam sagittis tellus id luctus ultricies. Etiam a arcu efficitur, consectetur libero non, imperdiet turpis. Donec ac velit et nisl semper semper. Duis iaculis interdum nunc sed tempor.
|
||||
|
||||
Sed diam odio, sagittis non dignissim nec, accumsan ac diam. Fusce sit amet dui sit amet justo ultrices viverra. Sed vel massa suscipit nibh porttitor laoreet in id nunc. Nam quis libero vitae nunc blandit sollicitudin et a lorem. Duis urna arcu, accumsan sed dignissim sit amet, vulputate at ex. Mauris porttitor libero mauris, vel fringilla diam euismod quis. Sed varius placerat tellus vel efficitur. Phasellus pulvinar gravida magna. Nulla dignissim consectetur finibus.
|
||||
|
||||
Nunc quis aliquet nisi. Cras luctus bibendum eros ac dignissim. Aenean suscipit felis vitae elementum eleifend. Proin commodo nunc non diam dignissim, in tincidunt nulla ultrices. Vivamus faucibus quam scelerisque interdum finibus. Sed porttitor vehicula urna, in laoreet arcu condimentum non. Praesent ac lacus diam. Vivamus aliquam euismod risus, luctus dictum lectus sodales et. Proin quis velit ac massa tristique scelerisque.
|
||||
|
||||
Sed non dolor efficitur, tincidunt mi eget, sagittis tortor. Quisque at varius felis, at finibus sem. Vestibulum vel lectus tincidunt, pharetra diam sit amet, interdum nulla. Sed ut elit tortor. Nam tincidunt tempus aliquam. Vivamus rhoncus faucibus sapien eget facilisis. Aliquam erat volutpat. Phasellus placerat aliquam lacus, eget ultrices orci pretium hendrerit. Fusce vitae dolor sit amet ante condimentum placerat. Nunc varius risus id tellus mollis, euismod luctus sapien viverra. Donec sodales est vel massa suscipit, eu sollicitudin ante convallis. Curabitur eu condimentum velit. Sed pharetra euismod tincidunt.
|
||||
|
||||
Nam at libero eros. Quisque bibendum, ligula quis sagittis ullamcorper, eros leo consectetur ex, quis elementum dolor justo vitae mi. Maecenas et elementum erat, et auctor enim. Nunc at nibh fermentum, ullamcorper mi elementum, facilisis erat. Vestibulum vestibulum leo ut pellentesque placerat. Suspendisse imperdiet nisl vitae justo sodales pellentesque. Interdum et malesuada fames ac ante ipsum primis in faucibus. In faucibus pretium nunc, sed interdum lectus vestibulum quis. Vestibulum luctus viverra ex at efficitur. Etiam ac est lorem. Maecenas mollis, orci at rhoncus congue, nulla leo rutrum dui, et pellentesque orci ligula eget ipsum. Suspendisse fermentum nisi turpis, ut sollicitudin purus imperdiet non. Maecenas vitae quam ornare, porta sem quis, rhoncus neque. Donec mattis purus a erat tristique, ac mollis est convallis. Duis vitae ipsum viverra, condimentum ante vel, sagittis ex. Maecenas placerat odio libero, id interdum turpis fermentum at.
|
||||
|
||||
Praesent faucibus nulla eget vehicula accumsan. Nulla elementum ante a nibh venenatis hendrerit. Proin nec nunc mattis, imperdiet nisl rutrum, sollicitudin libero. Nullam bibendum dignissim faucibus. Sed eget tortor vitae sapien cursus faucibus nec et lectus. Duis pharetra non odio id consectetur. Suspendisse at est sem. Nunc mauris ligula, ultrices id ante non, venenatis mattis erat. Vivamus sit amet viverra nisl.
|
||||
|
||||
Sed cursus vel nisi in mattis. Nunc porttitor dictum leo ac euismod. Sed blandit ornare nunc id lobortis. Aenean convallis ligula at volutpat commodo. Vestibulum sit amet laoreet urna. Donec et pellentesque orci, ac egestas nulla. In accumsan venenatis porta. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Donec scelerisque, metus nec viverra pharetra, dolor libero dictum velit, id ullamcorper enim nisi eu nibh. Aliquam nec dapibus quam. Curabitur vulputate, libero sit amet tempor ullamcorper, libero purus congue quam, nec sollicitudin orci erat non massa. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas.
|
||||
|
||||
Aenean faucibus mollis placerat. Praesent lacinia venenatis turpis eu scelerisque. Nam tempus tortor a varius posuere. Cras ut viverra tortor. Cras facilisis mauris ut ante imperdiet, a malesuada justo luctus. Ut eu enim sit amet arcu porttitor pulvinar ac ac odio. Ut odio neque, molestie vitae ligula quis, dignissim viverra erat. In ullamcorper erat sed elementum varius. Sed in lacus maximus, euismod nisi vitae, tempus mi. Nunc tellus justo, auctor at luctus ac, feugiat sit amet dui.
|
||||
|
||||
Donec imperdiet purus lorem, sed venenatis dolor finibus non. Aenean lacus nunc, elementum nec arcu eget, faucibus elementum turpis. Aliquam lacinia massa ac quam efficitur, et tincidunt eros pretium. Fusce condimentum mi vel pharetra egestas. Quisque consectetur nibh vel leo dignissim sollicitudin. Duis ultrices felis ipsum, sed maximus arcu ornare vitae. Curabitur porttitor ligula in turpis facilisis, id venenatis augue ultricies. Phasellus vel dolor id tellus finibus sodales ut quis nisi. Integer id orci cursus erat tincidunt sagittis non in nunc. Pellentesque ligula lacus, vestibulum eu ante vel, facilisis viverra massa. Sed ut tincidunt metus, vel tristique est. Ut et cursus justo. Ut ac porttitor eros, at dictum felis. Phasellus ornare nisi sit amet risus varius, sed sollicitudin nulla ornare. Donec aliquam ipsum urna. Aliquam id bibendum magna, quis venenatis diam.
|
||||
|
||||
Duis tempor odio id iaculis egestas. Cras consequat neque ac posuere iaculis. Nulla tempus et nisi eu auctor. Vestibulum metus massa, dignissim ut metus eget, ullamcorper consectetur turpis. Integer vel est tellus. Ut ac vestibulum massa. Pellentesque nec venenatis erat. Nam vel pellentesque lectus.
|
||||
|
||||
Morbi a placerat est. Ut eleifend ante ut placerat porta. Donec sagittis semper leo, ut scelerisque nisi imperdiet feugiat. Mauris purus turpis, consequat ut fringilla ac, cursus eget augue. Fusce arcu dolor, sagittis et facilisis ut, scelerisque non lacus. Aliquam sit amet eleifend tellus. Mauris id est luctus, iaculis tortor eget, gravida justo. Suspendisse at tellus nisl. Nullam felis erat, vehicula eu porttitor bibendum, pulvinar et dui. Sed molestie lacus nec sagittis rutrum. Aliquam erat volutpat. Nullam ut aliquet eros. Sed feugiat, massa id pharetra auctor, leo turpis condimentum purus, sit amet volutpat sem nunc sed nisi.
|
||||
|
||||
Pellentesque feugiat ipsum at accumsan iaculis. Morbi et dui in lorem commodo hendrerit. Mauris tempor ex mollis mollis blandit. Cras eu turpis feugiat, suscipit velit quis, volutpat magna. Vestibulum varius ligula ut quam mollis, a volutpat nibh lobortis. Sed sodales euismod leo non suscipit. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Nullam ultricies sagittis justo, sit amet lacinia eros tincidunt tristique. Etiam faucibus turpis in lacus efficitur, vitae rutrum magna porttitor. Cras finibus eros vel ante semper, facilisis vestibulum erat accumsan. Ut mollis dui ut commodo varius. In et sem malesuada erat egestas pellentesque. Maecenas pulvinar sodales risus, at euismod ligula aliquet in. Quisque fringilla malesuada dui vel cursus. Curabitur eu ex vulputate, pretium elit non, sodales sapien. Integer egestas facilisis odio et pretium.
|
||||
|
||||
Integer eleifend, felis vitae faucibus tempus, tellus lectus placerat nunc, eu efficitur lacus mi vel nisl. Mauris commodo pretium feugiat. In aliquet nibh diam, ac egestas mauris consequat ut. Integer cursus, tortor pharetra pellentesque pulvinar, neque risus ultricies felis, et consequat felis eros at eros. Pellentesque fermentum velit ac sodales facilisis. Suspendisse vestibulum metus quis convallis lacinia. Donec in pharetra magna. Proin gravida dolor eget ligula lobortis sagittis.
|
||||
|
||||
Nullam consectetur ut massa id ultrices. Fusce consectetur at eros at mollis. Donec nec nibh fringilla, porttitor ipsum eget, aliquam neque. Quisque suscipit tortor in dui commodo, sed venenatis augue cursus. Etiam feugiat purus id justo elementum placerat. Sed interdum dictum nibh at sodales. Maecenas lobortis, metus ac sagittis lacinia, elit arcu varius felis, quis facilisis magna elit in leo. Proin condimentum orci sit amet dignissim imperdiet. Fusce sed iaculis felis. Maecenas sodales non magna vitae rutrum.
|
||||
|
||||
Nulla id ex massa. Sed vehicula sed quam non elementum. Aliquam luctus, enim vel molestie posuere, sem arcu laoreet justo, quis finibus nisl justo et magna. Vivamus malesuada elit in aliquam dignissim. Sed at tellus in orci vulputate ullamcorper. Integer magna sem, mattis id hendrerit non, tincidunt in est. Praesent posuere aliquet lobortis. Quisque euismod leo ut nisl pellentesque, et imperdiet dui dapibus.
|
||||
|
||||
Sed a erat nec risus pulvinar venenatis. Integer ultrices eros at aliquet efficitur. Orci varius natoque penatibus et magnis dis parturient montes, nascetur ridiculus mus. Quisque ac maximus ante. Sed sodales, nisi et sagittis accumsan, diam odio consequat elit, et mollis dolor turpis tincidunt metus. Proin justo quam, tincidunt id convallis a, molestie hendrerit massa. Etiam pharetra turpis eu ultrices mollis.
|
||||
|
||||
Proin fermentum libero in purus cursus molestie. In varius magna eu ante maximus, eget rutrum felis iaculis. Maecenas hendrerit, diam eget vestibulum vehicula, est quam porta magna, a dictum urna mi ut libero. Vestibulum dictum quis lacus vitae eleifend. Integer sapien libero, pretium vitae euismod eget, semper eget ante. Vivamus mollis elementum odio vel hendrerit. Phasellus tristique, metus eget luctus tincidunt, ex enim faucibus ipsum, at dictum eros urna ac mi. Aenean imperdiet felis eu ultricies egestas. Vivamus fermentum convallis nisi, non sollicitudin felis posuere nec. Ut commodo sit amet felis semper dictum. Aenean accumsan, tellus id blandit aliquet, lorem nibh pellentesque mi, sit amet volutpat erat ligula ut est. Vivamus non posuere velit. Sed vel rutrum diam, non pulvinar nulla. Suspendisse quis gravida lectus, ac accumsan justo. Sed lobortis neque ante, a imperdiet nisl iaculis nec.
|
||||
@@ -118,6 +118,7 @@ not a hard error, so the list can grow without breaking old mods.
|
||||
| `QUEST` | New story, NPCs, dialogue, cutscenes | content |
|
||||
| `MECHANIC` | New or changed battle/field mechanics via hooks/effects | overhaul |
|
||||
| `GRAPHICS` | Sprite / tileset / palette / font changes | content |
|
||||
| `LANGUAGE` | A translation: `text`, `strings` and the glyphs it needs | content |
|
||||
| `AUDIO` | Music, sfx, cries | content |
|
||||
| `UI` | New or modified screens, menus, overlays | content / overhaul |
|
||||
| `TOOL` | Dev/QoL utilities, overlays, inter-mod libraries | content |
|
||||
@@ -128,10 +129,195 @@ not a hard error, so the list can grow without breaking old mods.
|
||||
keeps validating with the value it has shipped since before the taxonomy
|
||||
existed.
|
||||
|
||||
### 4. `mod.card`
|
||||
A translation may also set `"language": true` in the manifest. That is the
|
||||
one claim online play acts on: an install running nothing but verified
|
||||
translations may take an ONLINE MATCH or a TOURNAMENT instead of being
|
||||
asked to restart vanilla. The claim is checked, not taken -- the mod
|
||||
qualifies only if every record it writes lands in `text`, `strings` or
|
||||
`font`, it wraps no hook, subscribes to no event and requests no
|
||||
permission. Anything else and it is an ordinary content mod that happens to
|
||||
ship text.
|
||||
|
||||
### 4. `games` (and the legacy `gen2compat`)
|
||||
|
||||
Pokemon Gold is Gen 2, and it runs its own battle engine, overworld, script
|
||||
VM and save format. The mod API is shared across both generations (same hook
|
||||
names, same event names, same registry names) but Gold cannot serve all of it
|
||||
yet, so Gen 2 is opt-in. Say which games the mod is for:
|
||||
|
||||
```json
|
||||
"games": ["gen1", "gen2"]
|
||||
```
|
||||
|
||||
Each entry is a version id (`"red"`, `"blue"`, `"yellow"`, `"gold"`), a
|
||||
generation (`"gen1"`, `"gen2"`) or `"all"`;
|
||||
`src/mods/ModTargets.lua` resolves them off `GameVersion.ORDER` so nothing
|
||||
restates the game list. `python3 tools/modkit.py scaffold my_mod --games
|
||||
gen1,gen2` writes the key for you. The mod still installs to one directory,
|
||||
`mods/<id>/`, shared by every game -- targeting is declared, never filed.
|
||||
|
||||
Absent means Gen 1 only, which is what every mod written before the key existed
|
||||
was tested as. `"gen2compat": true` is the legacy spelling, still accepted and
|
||||
purely additive (it *adds* the Gen 2 games), so no manifest can lose a game it
|
||||
already ran on. On a Gold boot a mod claiming no Gen 2 game is not loaded at
|
||||
all: the manager lists it as `ENABLED (NOT THIS GAME)` and says why, because a
|
||||
mod that half-applies reads as a broken mod. Claim Gen 2 once you have actually
|
||||
run your mod on Gold.
|
||||
|
||||
Every token is enforced, per game: the loader gates on the same
|
||||
`ModTargets.supports` answer both mod surfaces draw, so `"games": ["blue"]`
|
||||
really does not load on Red and the skip line is the launcher's line, `For
|
||||
Blue, not Red`, and `"games": ["gold"]` alone does not load on Red either. A
|
||||
manifest with neither key still covers every Gen 1 game, so nothing written
|
||||
before the key existed changes behavior; list both generations or say `"all"`
|
||||
when you mean everywhere.
|
||||
|
||||
`docs/mod-api-gen2-compat.md` is the compatibility matrix: what works on Gold
|
||||
today (40 of the 46 registries, 40 event and 43 hook names shared with Gen 1,
|
||||
and 24 Gen 2-only ones), which registries have no Gen 2 home and drop their
|
||||
writes with a report, and which hooks and events are still to come.
|
||||
`docs/preparing-your-mod-for-gen2.md` is the step-by-step migration guide for a
|
||||
Gen 1 mod, and it is the one to start from.
|
||||
|
||||
Two consequences worth knowing before you claim Gen 2.
|
||||
|
||||
**Dependencies are contagious.** A mod whose hard dependency does not run here
|
||||
is left out too, with the dependency's own wording (`depends on X, which does
|
||||
not run here (For Blue, not Red)`). It is reported as a skip, not as a failure,
|
||||
and neither mod lands on the boot error list, but the mod does not run, so
|
||||
every hard dependency has to cover the same games.
|
||||
|
||||
**The player can override you.** The claim is yours, and a mod written before
|
||||
the key existed can never carry one, so the manager's detail pane offers
|
||||
`TRY HERE ANYWAY` for any mod that does not claim the game being played. It
|
||||
persists per game in `options.modsGen2[id][version]` and takes effect on the
|
||||
next boot; forcing a mod onto Red does not force it onto Gold. A forced mod
|
||||
loads normally and keeps a note saying its author never verified it here.
|
||||
|
||||
**Prefer the API on Gold, but the Gen 1 names still work.** Gen 2 is a
|
||||
parallel module tree behind `src/core/Game2.lua`. In new code take the live
|
||||
game from `mod.game` (or the `game.ready` payload, or any `ui.*` hook's first
|
||||
argument) and the world from `mod.world`; both resolve per generation, and
|
||||
neither needs `engine_internals`.
|
||||
|
||||
For the mods written before Gold existed, a require made from a mod's own file
|
||||
is answered on a Gold boot by an adapter presenting the Gen 1 API over Gen 2
|
||||
internals. Fifteen names are served -- `src.core.Game`,
|
||||
`src.world.OverworldController`, `src.world.Map`, `src.world.NPC`,
|
||||
`src.world.Collision`, `src.world.WorldAPI`, `src.world.PikachuFollower`,
|
||||
`src.world.FieldDefaults`, `src.pokemon.Boxes`, `src.script.ScriptRunner`,
|
||||
`src.ui.PartyMenu`, `src.ui.StartMenu`, `src.ui.OptionsMenu`, `src.ui.BoxMenu`
|
||||
and `src.battle.BattleState`. `src/mods/Gen2Compat.lua` is the full table and
|
||||
publishes what it covers through `Gen2Compat.coverage(name)`, whose members are
|
||||
`backed`, `warned` or `absent`. A name with no adapter (`src.script.Commands`,
|
||||
`src.ui.OptionRows`) is reported against the mod that required it, and a member
|
||||
an adapter cannot back is absent or logs once rather than answering wrongly.
|
||||
|
||||
Things no adapter can fix, all mod-side: a hardcoded version allow-list
|
||||
(`GameVersion.get() == "red" or ...`) excludes you from Gold by construction;
|
||||
Gold's builtin screen ids carry a `Gen2` prefix, so a string match on
|
||||
`"BoxMenu"` matches nothing there; a write to a field on a live Gen 2 menu
|
||||
instance is inert; and `map.warpAt` is a table on Gen 1 and a method on Gold,
|
||||
so indexing it raises. Each has a route that works on both generations, in
|
||||
`docs/preparing-your-mod-for-gen2.md`.
|
||||
|
||||
Check it statically, then load it headless:
|
||||
|
||||
```sh
|
||||
python3 tools/modkit.py gen2check mods/my_mod
|
||||
```
|
||||
|
||||
```lua
|
||||
local run = T.sdk.loadMod("mods/my_mod", { generation = 2 })
|
||||
T.eq(run.mod and run.mod.state, "loaded",
|
||||
"runs on gen 2: " .. tostring(run.mod and run.mod.skipReason))
|
||||
T.eq(#run.errors, 0, "and loads with no boot errors")
|
||||
```
|
||||
|
||||
Assert the state, not only the error count: a gate skip is deliberately not an
|
||||
error, so `#run.errors == 0` passes for a mod that never ran a line.
|
||||
|
||||
`gen2check` answers `will load`, `will load but degrade` or `will not work`,
|
||||
with a `MK4xx` finding per site and an `unresolved:` note, with a file and a
|
||||
line, for every reach a static scan could not follow. Neither substitutes for a
|
||||
real Gold boot.
|
||||
|
||||
### 5. What a mod's code can reach
|
||||
|
||||
Your code runs in a sandbox (`src/mods/Sandbox.lua`), not against the
|
||||
engine's globals. Every chunk you author gets it: `main.lua`, your
|
||||
`options_schema`, and anything you `load()` yourself.
|
||||
|
||||
The globals the sandbox took away are still *reachable*, as compat
|
||||
stand-ins (`src/mods/LegacyCompat.lua`) that answer with the new API
|
||||
underneath. A mod written before the sandbox keeps working; it logs one
|
||||
warning per call it should migrate, and the mod manager lists them. What
|
||||
each stand-in actually does:
|
||||
|
||||
| Pre-sandbox call | What it does now | Migrate to |
|
||||
| --- | --- | --- |
|
||||
| `io.open`, `io.lines`, `love.filesystem.read`/`lines`/`newFile` | reads your own shipped files, then your overlay, then `mod.storage` | `mod:read`, `mod.storage` |
|
||||
| `love.filesystem.write`/`append`, `io.open(…, "w")`, `os.remove`, `os.rename` | writes to a private per-mod overlay under `mod_compat/<your id>/` | `mod.storage` |
|
||||
| `love.filesystem.getDirectoryItems`/`getInfo` | your own directory plus your overlay | `mod:list`, `mod:info` |
|
||||
| `love.filesystem.getSaveDirectory` and friends | a virtual root; anything joined to it lands in your overlay | `mod.storage` |
|
||||
| `os.getenv` | `nil`, except home-like names, which answer with that same virtual root | nothing |
|
||||
| `love.filesystem.load`, `dofile`, `loadfile` | compiles the chunk into your sandbox | `require`, `mod:read` plus `load` |
|
||||
| `love.system` | `getOS`/`getPowerInfo`/`getProcessorCount` read through; clipboard and `openURL` do nothing | `mod.device:powerInfo()`, `mod.steps` |
|
||||
| `love.event` | passes through, except `quit`, which does nothing | `mod.events`, `mod.hooks` |
|
||||
| `love.mousemoved = fn` and the other callbacks | installs on the real `love` table, the way it always did | `mod.hooks`, `mod.events` |
|
||||
| `package` | an inert stub, so `package.path = …` does not crash | `require` |
|
||||
|
||||
What has no stand-in, because there is nothing honest to reroute it to:
|
||||
|
||||
| Still refused | Why |
|
||||
| --- | --- |
|
||||
| `love.thread` | a LÖVE thread is a fresh Lua state with the full standard library, which no environment-based sandbox in this state can reach. Use `mod.fetch` for background HTTP (`network`) or `mod.job` for background compute (`background`) — both run your code inside the sandbox instead of outside it |
|
||||
| `require("ffi")` | arbitrary C |
|
||||
| `debug`, `getfenv`, `setfenv` | each one undoes the sandbox from inside |
|
||||
| `io.popen`, `os.execute` | spawning a process |
|
||||
| `love.run`, `love.errorhandler` | the engine's own loop and its crash path |
|
||||
| replacing a `love` module table (`love.filesystem = {}`) | the engine reads those tables too |
|
||||
|
||||
The rest of `love` passes through unchanged, so graphics, audio, timers and
|
||||
input work as they always have.
|
||||
|
||||
Three consequences worth knowing before you write against it:
|
||||
|
||||
- **Your globals are yours.** `_G` inside a mod is that mod's own table. Two
|
||||
mods no longer share a namespace, and neither can reach the engine's. To
|
||||
publish something to another mod, put it on `mod.exports` and let them
|
||||
`mod.find("your_id").exports` — the channel that was always the intended
|
||||
one. The same goes for the standard library: `string`, `table` and `math`
|
||||
are per-mod copies, so patching one is a local decision.
|
||||
- **Paths cannot climb.** `mod:read`, `mod:list`, `mod:info`, `mod.assets:path`
|
||||
and `mod.assets:image` join to your own directory, and `..`, absolute paths
|
||||
and drive letters are refused. So are `entry` and `options_schema` in your
|
||||
manifest. `mod:list("assets")` is the sandboxed `getDirectoryItems` for a
|
||||
folder you shipped; `mod:info` tells file from directory so a walk can
|
||||
recurse.
|
||||
- **Ship source, not bytecode.** A precompiled entry file is refused.
|
||||
|
||||
`permissions` in the manifest is still a disclosure the manager shows the
|
||||
player. `network` gates `require("socket")` and friends plus `mod.fetch`
|
||||
(non-blocking HTTP), and `background` gates `mod.job` (compute on a worker
|
||||
thread). Those two are the sanctioned ways to work off the main thread now
|
||||
that `love.thread` is refused. There is no
|
||||
permission that grants raw filesystem access, because no mod needs one:
|
||||
everything a mod legitimately writes is already scoped by
|
||||
`mod.storage` or the asset-transform derived root.
|
||||
|
||||
If your mod used one of the rerouted globals, the fix is almost always
|
||||
`mod.storage`. The overlay is a compatibility floor, not a second storage
|
||||
system: it is not scoped per playthrough, it does not migrate, and it is
|
||||
the first thing that will be dropped once the mods on the index have
|
||||
moved off it. Open an issue if you have a case `mod.storage` does not
|
||||
cover.
|
||||
|
||||
### 6. `mod.card`
|
||||
|
||||
The manifest is the *engine's* contract: identity, load order, dependencies,
|
||||
permissions, profile. The card is the *human-facing* one: who made this,
|
||||
permissions, profile (see [Manifest specification](docs/modding.md#manifest-specification-manifestjson)).
|
||||
The card is the *human-facing* one: who made this,
|
||||
what it changes, what it does not do yet. It is never read by the loader's
|
||||
merge — only by tooling and the manager's detail pane — so an absent or
|
||||
malformed card can never break a load.
|
||||
@@ -149,7 +335,7 @@ Two fields deserve their own note:
|
||||
distributed mod never carries ROM-derived bytes, not even in its preview
|
||||
images.
|
||||
|
||||
### 5. Tags
|
||||
### 7. Tags
|
||||
|
||||
Lowercase kebab strings, open vocabulary. The showcase generator
|
||||
lowercases and de-dupes. A recommended starting set: `beginner`,
|
||||
@@ -207,12 +393,15 @@ registry or a new schema field lands with its catalog entry in the same PR
|
||||
and the generator runs clean:
|
||||
|
||||
```sh
|
||||
luajit tools/gen_registry_docs.lua # in-repo default
|
||||
luajit tools/gen_registry_docs.lua ../project.wiki # the wiki checkout
|
||||
luajit tools/gen_registry_docs.lua # docs/modding/reference/registries.md
|
||||
luajit tools/gen_registry_docs.lua ../project.wiki # Reference-Registries.md in a wiki checkout
|
||||
```
|
||||
|
||||
The prose reference lives in the GitHub wiki; the generated pages are
|
||||
written into a checkout of it, so they cannot drift from the engine.
|
||||
With no argument it writes inside the repo, which is the copy `python3
|
||||
tools/modkit.py docs` regenerates and `--out` copies from. Pass a directory
|
||||
(or set `POKEPORT_DOCS_DIR`) to write the wiki's flat page name into a wiki
|
||||
checkout instead. The prose reference lives in the GitHub wiki; both copies
|
||||
come off `src/mods/Schemas.lua`, so neither can drift from the engine.
|
||||
|
||||
### 5. Deprecation etiquette
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
Copyright 2026 BOIS CLUB GAMES, LLC
|
||||
|
||||
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 above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@@ -1,36 +1,102 @@
|
||||
# Gen1Recomp
|
||||
|
||||
A native LÖVE2D recreation of Poke Red and Blue. The engine and map
|
||||
A native LÖVE2D recreation of Poke Red, Blue and Yellow. The engine and map
|
||||
behavior are hand-written Lua; game data and graphics are decoded from a ROM
|
||||
supplied by the player.
|
||||
|
||||
SUPPORT AND ANNOUNCEMENTS: [Discord](https://bois.icu)
|
||||
And before you say, "that's not a recomp", you're wrong. Recomp is an acronym. ***Reverse Engineering Causes Obsessive Mental Problems***
|
||||
|
||||
[Click Here for the AI Use Disclosure!](AIDisclosure.md)
|
||||
|
||||
> [!CAUTION]
|
||||
> **We are NOT affiliated with the website `gen1recomp[.]com`** That website is not run by this project, was not authorized by us, and we have no idea who operates it. It is impersonating this project; do not download anything from it, and treat anything it hosts or claims as untrustworthy. Even if the site currently links back to this repository, the people behind it can change its content at any time, so nothing on it should ever be trusted. This GitHub repository and the Discord linked below are the only official sources for this project.
|
||||
|
||||
<p align="center"><img src="https://raw.githubusercontent.com/bryanthaboi/gen1recomp/refs/heads/dev/assets/logo/logo.png"></p>
|
||||
|
||||
**SUPPORT / ANNOUNCEMENTS / MODS:** [Discord](https://bois.icu)
|
||||
|
||||
<p align="center">
|
||||
|
||||
<a href="https://www.youtube.com/@bryanthaboi">
|
||||
<img src="https://img.shields.io/badge/YouTube-FF0000?style=for-the-badge&logo=youtube&logoColor=white" alt="YouTube">
|
||||
</a>
|
||||
<a href="https://www.tiktok.com/@bryanthaboi">
|
||||
<img src="https://img.shields.io/badge/TikTok-000000?style=for-the-badge&logo=tiktok&logoColor=white" alt="TikTok">
|
||||
</a>
|
||||
<a href="https://x.com/bryanthaboi">
|
||||
<img src="https://img.shields.io/badge/X-000000?style=for-the-badge&logo=x&logoColor=white" alt="X">
|
||||
</a>
|
||||
<a href="https://bsky.app/profile/bryanthaboi.live">
|
||||
<img src="https://img.shields.io/badge/Bluesky-0285FF?style=for-the-badge&logo=bluesky&logoColor=white" alt="Bluesky">
|
||||
</a>
|
||||
<a href="https://www.instagram.com/bryanthaboi">
|
||||
<img src="https://img.shields.io/badge/Instagram-E4405F?style=for-the-badge&logo=instagram&logoColor=white" alt="Instagram">
|
||||
</a>
|
||||
|
||||
</p>
|
||||
|
||||
|
||||
<p align="center"> <a href="https://www.polygon.com/pokemon-red-blue-3d-voxel-mod-battle-pixels-gameplay-footage-remake/"> <img src="https://img.shields.io/badge/AS%20SEEN%20ON-POLYGON-ea2e49?style=for-the-badge" alt="As seen on Polygon"> </a>
|
||||
<a href="https://kotaku.com/pokemon-red-blue-recompilation-project-voxel-3d-mod-2000720281"> <img src="https://img.shields.io/badge/AS%20SEEN%20ON-KOTAKU-ea2e49?style=for-the-badge" alt="As seen on KOTAKU"> </a>
|
||||
|
||||
<a href="https://www.digitalfoundry.net/news/2026/07/pokemon-yellow-voxel-mod-turns-the-original-gameboy-code-into-a-stunning-world">
|
||||
<img src="https://img.shields.io/badge/AS%20SEEN%20ON-DIGITAL%20FOUNDRY-ea2e49?style=for-the-badge" alt="As seen on Digital Foundry">
|
||||
</a>
|
||||
<a href="https://www.androidauthority.com/unofficial-android-port-pokemon-red-blue-yellow-3692724/">
|
||||
<img src="https://img.shields.io/badge/AS%20SEEN%20ON-ANDROID%20AUTHORITY-ea2e49?style=for-the-badge" alt="As seen on Android Authority">
|
||||
</a>
|
||||
|
||||
<a href="https://www.xda-developers.com/this-amazing-pokemon-red-and-blue-voxel-mod-adds-a-3d-perspective-without-an-emulator/">
|
||||
<img src="https://img.shields.io/badge/AS%20SEEN%20ON-XDA%20DEVELOPERS-ea2e49?style=for-the-badge" alt="As seen on XDA Developers">
|
||||
</a>
|
||||
</p>
|
||||
|
||||
### Watch the latest update video
|
||||
|
||||
[](https://www.youtube.com/watch?v=8IOgqbe4YvA)
|
||||
|
||||
|
||||
This project does not include a ROM, emulate the Game Boy, transpile assembly,
|
||||
or download a disassembly. A canonical US Poke Red or Blue ROM is the only
|
||||
game content input.
|
||||
or download a disassembly. A canonical US Poke Red, Blue, Yellow, or Gold ROM
|
||||
is the only game content input.
|
||||
|
||||
The ROM is verified, used during import, and then released from memory. It is
|
||||
not copied into the cache. Later launches load the private generated cache and
|
||||
do not ask for the ROM again. Red and Blue can both be imported and played
|
||||
side by side.
|
||||
do not ask for the ROM again. Red, Blue, Yellow, and Gold can all be imported
|
||||
side by side. Gold is Gen 2 Phase 1 (import + launcher; see
|
||||
`docs/gold-phase1.md`): the Gen 2 engine is still under construction.
|
||||
|
||||
## Quick Start
|
||||
|
||||
Open the desktop app. On first boot, choose your legally obtained `.gb` file
|
||||
or drop it onto the window. Import takes a few seconds and the game starts
|
||||
automatically.
|
||||
Open the desktop app. On first boot, choose your legally obtained `.gb` /
|
||||
`.gbc` file or drop it onto the window. Import takes a few seconds and the
|
||||
game starts automatically.
|
||||
|
||||
Only the canonical 1 MiB US Red and Blue ROMs are accepted. The importer
|
||||
verifies SHA-1 before creating any game data:
|
||||
Only the canonical US Red, Blue, Yellow (1 MiB), and Gold (2 MiB) ROMs are
|
||||
accepted. The importer verifies SHA-1 before creating any game data:
|
||||
|
||||
- Red: `ea9bcae617fdf159b045185467ae58b2e4a48b9a`
|
||||
- Blue: `d7037c83e1ae5b39bde3c30787637ba1d4c48ce2`
|
||||
- Yellow: `cc7d03262ebfaf2f06772c1a480c7d9d5f4a38e1`
|
||||
- Gold: `d8b8a3600a465308c9953dfa04f0081c05bdcb94`
|
||||
|
||||
The packaged app contains neither a ROM nor pre-extracted game data. Music,
|
||||
sound effects, and cries are synthesized while the game runs from compact
|
||||
audio channel programs copied out of the verified ROM.
|
||||
|
||||
### A note on Windows Defender warnings
|
||||
|
||||
Windows Defender sometimes flags the Windows build with a generic
|
||||
machine-learning detection such as `Trojan:Win32/Wacatac!ml` (#621). This is
|
||||
a known false positive: the exe is the official LÖVE runtime with the game
|
||||
archive appended (the standard way LÖVE games ship), and Defender's
|
||||
heuristics distrust unsigned executables with appended data. Every release
|
||||
publishes SHA-256 checksums (`sha256sums.txt`) so you can verify your
|
||||
download, and you can confirm a flagged file yourself on
|
||||
[VirusTotal](https://www.virustotal.com), where these builds come back clean
|
||||
on every engine except Defender's heuristic. False positives are reported to
|
||||
Microsoft as they come up.
|
||||
|
||||
## Controls
|
||||
|
||||
|
||||
@@ -52,6 +118,7 @@ supported out of the box.
|
||||
| Key | What it does |
|
||||
| --------- | ---------------------------------------------------- |
|
||||
| `-` / `=` | Zoom out / in (overworld; also mouse wheel) |
|
||||
| `1` | Cycle GAME SPEED up (controller: R2 faster, L2 slower) |
|
||||
| `2` | Cycle COLORS |
|
||||
| `3` | Cycle TILT (free-roam overworld) |
|
||||
| `4` | Cycle ZOOM through every level (free-roam overworld) |
|
||||
@@ -61,8 +128,19 @@ supported out of the box.
|
||||
| `F10` | Open / close the mod manager |
|
||||
|
||||
|
||||
COLORS, TILT, ZOOM, GBC FX, and VOID FILL are also in the Options menu
|
||||
and persist in `options.lua`.
|
||||
COLORS, TILT, ZOOM, GBC FX, GAME SPEED, and VOID FILL are also in the
|
||||
Options menu and persist in `options.lua`.
|
||||
|
||||
### Low-end devices
|
||||
|
||||
**OPTIONS → PERFORMANCE** scales the port's optional extras for weaker
|
||||
hardware: **HIGH** (everything on), **BALANCED** (no 3D tilt or GBC FX),
|
||||
**LOW** (also no survey zoom, FPS capped), or **AUTO** — the default, which
|
||||
picks a tier from your device (ARM handhelds → LOW, phones → BALANCED,
|
||||
normal desktops → HIGH, unchanged). It only scales presentation; the
|
||||
fixed-step game logic is identical on every tier, and a lower tier hides
|
||||
your tilt/zoom/GBC-FX preferences without forgetting them. Details in
|
||||
[docs/new-features.md](docs/new-features.md#performance-tier-low-end-devices).
|
||||
|
||||
### Rulesets
|
||||
|
||||
@@ -98,11 +176,11 @@ famous bugs included:
|
||||
|
||||
## Running From Source
|
||||
|
||||
Requires LÖVE 11.x. Place a Red or Blue ROM in the project folder and
|
||||
Requires LÖVE 11.x. Place a Red, Blue, or Yellow ROM in the project folder and
|
||||
double-click `Play-Mac.command` or `Play-Windows.bat`, or run:
|
||||
|
||||
```sh
|
||||
scripts/setup.sh --rom "/path/to/Poke Red.gb" # or Poke Blue.gb
|
||||
scripts/setup.sh --rom "/path/to/Poke Red.gb" # or Blue.gb / Yellow.gbc
|
||||
scripts/run.sh
|
||||
```
|
||||
|
||||
@@ -133,6 +211,90 @@ even on a different computer, as long as the same folder comes along.
|
||||
already written to either location is touched automatically, so copy files
|
||||
over yourself if you want to carry existing progress across the switch.
|
||||
|
||||
## Launch Options
|
||||
|
||||
By default the app opens the launcher so you can pick a game. Launch options
|
||||
skip it and start one game directly, which is what you want for a one-click
|
||||
entry: a desktop shortcut per game, a Steam entry, or a handheld frontend.
|
||||
|
||||
| Option | Effect |
|
||||
| --- | --- |
|
||||
| `--game=red` | boot Red, skipping the launcher (`blue` and `yellow` too, or just `r` / `b` / `y`) |
|
||||
| `--slot=2` | load that save slot; takes a slot number or a slot id |
|
||||
| `--launcher` | open the launcher anyway, so you can edit a shortcut you already made |
|
||||
|
||||
|
||||
## Linux on arm64 (Raspberry Pi)
|
||||
|
||||
Alongside the x86_64 `gen1recomp-*-linux.zip`, every release ships
|
||||
`gen1recomp-*-linux-arm64.AppImage` for 64-bit ARM desktop Linux — Raspberry
|
||||
Pi 4/5, Armbian and other SBC distros, and arm64 VMs on Apple Silicon:
|
||||
|
||||
```sh
|
||||
chmod +x gen1recomp-*-linux-arm64.AppImage
|
||||
./gen1recomp-*-linux-arm64.AppImage
|
||||
```
|
||||
|
||||
LÖVE publishes no aarch64 binary of any kind, so this artifact compiles the
|
||||
engine — and SDL2, OpenAL and the codecs — from source inside a Debian
|
||||
bullseye arm64 container. It needs only glibc 2.29+, libstdc++, freetype and
|
||||
zlib on the host; OpenGL, X11, Wayland, KMSDRM, ALSA and PulseAudio are all
|
||||
dlopened, so the same image runs on a full desktop, a Wayland-only session or
|
||||
a KMSDRM handheld with no X server. Build instructions and the reasoning are
|
||||
in [docs/linux-arm64-build.md](docs/linux-arm64-build.md).
|
||||
|
||||
|
||||
## iOS
|
||||
|
||||
Every release ships `gen1recomp++-*-ios.ipa`. Sideload it with AltStore
|
||||
(Windows or Mac) — see [docs/ios-sideload.md](docs/ios-sideload.md). To
|
||||
build and install from source on a Mac instead, see
|
||||
[docs/ios-install.md](docs/ios-install.md).
|
||||
|
||||
<div>
|
||||
<a href="https://intradeus.github.io/http-protocol-redirector?r=sidestore://source?url=https://github.com/bryanthaboi/gen1recomp/raw/refs/heads/main/mobile/ios/app-repo.json"><img src="./.github/resources/sidestore-badge.png" alt="Add to SideStore" height="60"></a>
|
||||
|
||||
<a href="https://intradeus.github.io/http-protocol-redirector?r=feather://source/https://github.com/bryanthaboi/gen1recomp/raw/refs/heads/main/mobile/ios/app-repo.json"><img src="./.github/resources/feather-badge.png" alt="Add to Feather" height="60"></a>
|
||||
|
||||
<a href="https://intradeus.github.io/http-protocol-redirector?r=altstore://source?url=https://github.com/bryanthaboi/gen1recomp/raw/refs/heads/main/mobile/ios/app-repo.json"><img src="./.github/resources/altstore-badge.png" alt="Add to AltStore" height="60"></a>
|
||||
|
||||
<a href="https://github.com/bryanthaboi/gen1recomp/releases/latest"><img src="./.github/resources/github-badge.png" alt="Download from GitHub" height="60"></a>
|
||||
</div>
|
||||
|
||||
## Xbox Dev Mode
|
||||
|
||||
Every release ships `gen1recomp-*-xbox-uwp.zip` for Xbox One and Xbox Series
|
||||
consoles in Developer Mode. It cannot be installed in retail mode.
|
||||
|
||||
Extract the archive, then use Xbox Device Portal to install the `.msix` and
|
||||
the x64 package under `Dependencies`.
|
||||
|
||||
### External setup
|
||||
|
||||
1. Put your legally obtained Red, Blue, or Yellow ROMs on an external drive.
|
||||
Mod ZIPs can go on the same drive.
|
||||
2. Connect the drive to the Xbox and open Gen1Recomp.
|
||||
3. Select **Import ROM** or **Import Mod**, then choose the file with the Xbox
|
||||
file picker.
|
||||
4. Repeat the ROM import for each version you want to use.
|
||||
|
||||
### Internal setup
|
||||
|
||||
1. Create a folder named `baseroms` on your PC and place your legally obtained
|
||||
Red, Blue, or Yellow ROMs inside it.
|
||||
2. ZIP the folder, keeping `baseroms` at the top level of the archive.
|
||||
3. Launch Gen1Recomp once, then close it.
|
||||
4. Open Xbox Device Portal and upload the ZIP to
|
||||
`Gen1Recomp/LocalState/pokemon-love2d/`.
|
||||
5. Choose **Yes** when Device Portal asks whether to extract the archive.
|
||||
6. Open Gen1Recomp. The launcher checks baseroms once at startup. When it finds a compatible ROM, that game’s tab shows ROM FOUND and an Import detected ROM button.
|
||||
|
||||
ROMs, generated game data, saves, and mods remain in LocalState and are not
|
||||
included in the app.
|
||||
|
||||
Source builds and package details are covered in
|
||||
[the Xbox UWP build notes](ports/uwp/BUILD.md).
|
||||
|
||||
## Handhelds
|
||||
|
||||
A PortMaster-style port for the **Anbernic RG34XXSP** on Stock OS 64-bit MOD
|
||||
@@ -140,6 +302,21 @@ ships with every release as `gen1recomp-*-rg34xxsp-stockos64-mod.zip`.
|
||||
Install steps, controls, and troubleshooting live in
|
||||
[docs/anbernic-rg34xxsp.md](docs/anbernic-rg34xxsp.md).
|
||||
|
||||
## Nintendo Switch
|
||||
|
||||
Releases ship an SD-ready `gen1recomp-*-switch.zip`. Runtime target is pinned
|
||||
[love-nx](https://github.com/retronx-team/love-nx) `11.5-nx1`. Requires a
|
||||
console that can run Switch homebrew.
|
||||
|
||||
- Players: [docs/switch-install.md](docs/switch-install.md). Download the
|
||||
zip, extract at the microSD root (install or update), title-override
|
||||
launch, import your own legal ROM, Joy-Con controls and shortcuts.
|
||||
- Builders: [docs/switch-build.md](docs/switch-build.md). `--fetch` /
|
||||
`--loose` / `--fused`, toolchain, Docker fallback, and CI vs release
|
||||
(path-gated ubuntu selftest, fused PR artifact on the main repo, release
|
||||
hard-fail).
|
||||
- File transfer (MTP / SD / FTP): [docs/switch-transfer.md](docs/switch-transfer.md).
|
||||
|
||||
## Modding
|
||||
|
||||
The game ships a native mod platform: content registries, events and hooks,
|
||||
@@ -150,6 +327,11 @@ reference — lives on the
|
||||
|
||||
Shipped example mods, one per kind of author, live in `[mods/](mods/)`.
|
||||
|
||||
Maps can be edited in our own build of [Tiled](https://www.mapeditor.org),
|
||||
[bryanthaboi/tiled_gen1recomp](https://github.com/bryanthaboi/tiled_gen1recomp/releases),
|
||||
and exported back out as a mod; see
|
||||
[docs/tiled-map-editing.md](docs/tiled-map-editing.md).
|
||||
|
||||
## Bugs and Ideas
|
||||
|
||||
Found a bug? A warp dropping you somewhere it shouldn't, a battle doing math
|
||||
@@ -180,4 +362,6 @@ request with real detail is one that can actually get built.
|
||||
|
||||
This project would not be possible without [pret](https://github.com/pret) >
|
||||
the pret band of decompiling maniacs > and their
|
||||
[pokered](https://github.com/pret/pokered) disassembly.
|
||||
[pokered](https://github.com/pret/pokered) disassembly.
|
||||
|
||||
<p align="center"><a href="https://boisclub.games"><img src="https://raw.githubusercontent.com/bryanthaboi/gen1recomp/refs/heads/dev/assets/logo/bcg.png"></a></p>
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
# Plain Pixel font
|
||||
|
||||
"Plain Pixel Font" by Douglas Vautour (Burpy Fresh) is licensed under
|
||||
CC-BY 4.0: https://burpyfresh.itch.io
|
||||
|
||||
Version 0.009 (CJK character additions), unmodified. Characters for most
|
||||
languages have a 5x11 base but can extend vertically; double-width
|
||||
characters such as Hiragana and Katakana are 11x11.
|
||||
|
||||
Bundled so a translation mod can opt into TTF text rendering
|
||||
(`mod.content.font:register("ttf", {})`; see the Translation support
|
||||
section of docs/new-features.md) instead of drawing hundreds of glyph-page
|
||||
tiles. The tile font extracted from the player's ROM stays the default.
|
||||
|
After Width: | Height: | Size: 243 KiB |
|
After Width: | Height: | Size: 217 KiB |
|
After Width: | Height: | Size: 236 KiB |
|
After Width: | Height: | Size: 242 KiB |
|
After Width: | Height: | Size: 166 KiB |
|
After Width: | Height: | Size: 251 KiB |
|
After Width: | Height: | Size: 2.4 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 96 KiB |
|
After Width: | Height: | Size: 18 KiB |
@@ -0,0 +1,10 @@
|
||||
# gb_anim -- bundled touch skin
|
||||
|
||||
Bezel art and overlay layout from libretro's `common-overlays`
|
||||
(`gamepads/gb_anim_portrait`), licensed CC-BY-4.0:
|
||||
https://github.com/libretro/common-overlays
|
||||
|
||||
`overlay.cfg` is the upstream `gb_big.cfg`, unmodified. It ships as the
|
||||
reference skin for the RetroArch-overlay loader in
|
||||
`src/core/TouchSkin.lua`: a full-device bezel, per-button press art, a
|
||||
screen viewport, and page switching between the DMG and Color shells.
|
||||
|
After Width: | Height: | Size: 973 B |
|
After Width: | Height: | Size: 109 KiB |
|
After Width: | Height: | Size: 649 B |
|
After Width: | Height: | Size: 645 B |
|
After Width: | Height: | Size: 636 B |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 3.3 KiB |
|
After Width: | Height: | Size: 3.2 KiB |
|
After Width: | Height: | Size: 3.4 KiB |
|
After Width: | Height: | Size: 85 KiB |
|
After Width: | Height: | Size: 1.1 KiB |
|
After Width: | Height: | Size: 1.2 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 1.3 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
|
After Width: | Height: | Size: 2.1 KiB |
@@ -0,0 +1,89 @@
|
||||
overlays = 2
|
||||
|
||||
overlay0_name = "GameBoy"
|
||||
overlay0_overlay = img/gb_back.png
|
||||
overlay0_full_screen = true
|
||||
overlay0_normalized = true
|
||||
overlay0_range_mod = 1.0
|
||||
overlay0_alpha_mod = 0.001
|
||||
overlay0_viewport = "0.0,0.0,1.0,0.505"
|
||||
overlay0_viewport_fill = true
|
||||
|
||||
overlay1_name = "GameBoyColor"
|
||||
overlay1_overlay = img/gbc_back.png
|
||||
overlay1_full_screen = true
|
||||
overlay1_normalized = true
|
||||
overlay1_range_mod = 1.0
|
||||
overlay1_alpha_mod = 0.001
|
||||
overlay1_viewport = "0.0,0.0,1.0,0.505"
|
||||
overlay1_viewport_fill = true
|
||||
|
||||
# GameBoy
|
||||
overlay0_descs = 18
|
||||
|
||||
overlay0_desc0 = "left,0.12778,0.73417,radial,0.09630,0.04635"
|
||||
overlay0_desc0_overlay = img/gb_left.png
|
||||
overlay0_desc1 = "right,0.35370,0.73417,radial,0.09630,0.04635"
|
||||
overlay0_desc1_overlay = img/gb_right.png
|
||||
overlay0_desc2 = "up,0.24074,0.67063,radial,0.08241,0.05417"
|
||||
overlay0_desc2_overlay = img/gb_up.png
|
||||
overlay0_desc3 = "down,0.24074,0.79771,radial,0.08241,0.05417"
|
||||
overlay0_desc3_overlay = img/gb_down.png
|
||||
overlay0_desc4 = "left|up,0.09259,0.65188,rect,0.06481,0.03646"
|
||||
overlay0_desc5 = "right|up,0.38704,0.65188,rect,0.06481,0.03646"
|
||||
overlay0_desc6 = "left|down,0.09259,0.81750,rect,0.06481,0.03646"
|
||||
overlay0_desc7 = "right|down,0.38704,0.81750,rect,0.06481,0.03646"
|
||||
overlay0_desc8 = "a,0.87407,0.72417,radial,0.08889,0.05000"
|
||||
overlay0_desc8_overlay = img/gb_a_b.png
|
||||
overlay0_desc9 = "b,0.68148,0.76584,radial,0.08889,0.05000"
|
||||
overlay0_desc9_overlay = img/gb_a_b.png
|
||||
overlay0_desc10 = "a|b,0.77037,0.73417,radial,0.02963,0.01667"
|
||||
overlay0_desc11 = "a|b,0.78518,0.75584,radial,0.02963,0.01667"
|
||||
overlay0_desc12 = "start,0.66666,0.93000,radial,0.07037,0.03958"
|
||||
overlay0_desc12_overlay = img/gb_start_select.png
|
||||
overlay0_desc13 = "select,0.33333,0.93000,radial,0.07037,0.03958"
|
||||
overlay0_desc13_overlay = img/gb_start_select.png
|
||||
overlay0_desc14 = "menu_toggle,0.05000,0.52800,radial,0.041296,0.02323"
|
||||
overlay0_desc14_overlay = img/menu.png
|
||||
overlay0_desc15 = "overlay_next,0.95000,0.52800,radial,0.041296,0.02323"
|
||||
overlay0_desc15_overlay = img/rotate.png
|
||||
overlay0_desc15_next_target = "GameBoyColor"
|
||||
overlay0_desc16 = "rewind,0.05000,0.97500,radial,0.041296,0.02323"
|
||||
overlay0_desc16_overlay =
|
||||
overlay0_desc17 = "hold_fast_forward,0.95000,0.97500,radial,0.041296,0.02323"
|
||||
overlay0_desc17_overlay =
|
||||
|
||||
# GameBoyColor
|
||||
overlay1_descs = 18
|
||||
|
||||
overlay1_desc0 = "left,0.14078,0.73417,radial,0.08530,0.04635"
|
||||
overlay1_desc0_overlay = img/gbc_left.png
|
||||
overlay1_desc1 = "right,0.34270,0.73417,radial,0.08530,0.04635"
|
||||
overlay1_desc1_overlay = img/gbc_right.png
|
||||
overlay1_desc2 = "up,0.24074,0.67863,radial,0.08241,0.04617"
|
||||
overlay1_desc2_overlay = img/gbc_up.png
|
||||
overlay1_desc3 = "down,0.24074,0.78971,radial,0.08241,0.04617"
|
||||
overlay1_desc3_overlay = img/gbc_down.png
|
||||
overlay1_desc4 = "left|up,0.09259,0.65188,rect,0.06481,0.03646"
|
||||
overlay1_desc5 = "right|up,0.38704,0.65188,rect,0.06481,0.03646"
|
||||
overlay1_desc6 = "left|down,0.09259,0.81750,rect,0.06481,0.03646"
|
||||
overlay1_desc7 = "right|down,0.38704,0.81750,rect,0.06481,0.03646"
|
||||
overlay1_desc8 = "a,0.87407,0.72417,radial,0.08889,0.05000"
|
||||
overlay1_desc8_overlay = img/gbc_a.png
|
||||
overlay1_desc9 = "b,0.68148,0.76584,radial,0.08889,0.05000"
|
||||
overlay1_desc9_overlay = img/gbc_b.png
|
||||
overlay1_desc10 = "a|b,0.77037,0.73417,radial,0.02963,0.01667"
|
||||
overlay1_desc11 = "a|b,0.78518,0.75584,radial,0.02963,0.01667"
|
||||
overlay1_desc12 = "start,0.66666,0.93000,radial,0.07037,0.03958"
|
||||
overlay1_desc12_overlay = img/gbc_start_select.png
|
||||
overlay1_desc13 = "select,0.33333,0.93000,radial,0.07037,0.03958"
|
||||
overlay1_desc13_overlay = img/gbc_start_select.png
|
||||
overlay1_desc14 = "menu_toggle,0.05000,0.52800,radial,0.041296,0.02323"
|
||||
overlay1_desc14_overlay = img/menu.png
|
||||
overlay1_desc15 = "overlay_next,0.95000,0.52800,radial,0.041296,0.02323"
|
||||
overlay1_desc15_overlay = img/rotate.png
|
||||
overlay1_desc15_next_target = "GameBoy"
|
||||
overlay1_desc16 = "rewind,0.05000,0.97500,radial,0.041296,0.02323"
|
||||
overlay1_desc16_overlay =
|
||||
overlay1_desc17 = "hold_fast_forward,0.95000,0.97500,radial,0.041296,0.02323"
|
||||
overlay1_desc17_overlay =
|
||||
@@ -0,0 +1,10 @@
|
||||
# tv_crt -- bundled desktop bezel
|
||||
|
||||
CRT television border from libretro's `common-overlays`
|
||||
(`borders/tv-integer.cfg` + `borders/img/tv-integer.png`), licensed
|
||||
CC-BY-4.0: https://github.com/libretro/common-overlays
|
||||
|
||||
`overlay.cfg` is the upstream file, unmodified. It is the reference
|
||||
DESKTOP skin: 1920x1080, `descs = 0` (pure decoration, no touch buttons),
|
||||
and a `viewport` naming the transparent screen hole, so the Game Boy
|
||||
picture is fitted into the TV's tube instead of the whole window.
|
||||
|
After Width: | Height: | Size: 1.9 MiB |
@@ -0,0 +1,6 @@
|
||||
overlays = 1
|
||||
overlay0_overlay = img/tv-integer.png
|
||||
overlay0_full_screen = true
|
||||
overlay0_descs = 0
|
||||
overlay0_viewport = "0.2335,0.0855,0.5335,0.830"
|
||||
overlay0_viewport_fill = true
|
||||
@@ -0,0 +1,366 @@
|
||||
#!/usr/bin/env bash
|
||||
# Build a PortMaster aarch64 port of gen1recomp for Linux ARM SBC handhelds.
|
||||
# The package uses PortMaster control hooks and a self-contained LÖVE runtime,
|
||||
# while keeping paths relative to the launcher for broad CFW compatibility.
|
||||
#
|
||||
# The launcher uses SHDIR-relative paths and bundles the LÖVE 11.5 aarch64
|
||||
# runtime so the device does not need a separate runtime download on first launch.
|
||||
#
|
||||
# Usage:
|
||||
# ./build-linux-arm-sbc.sh [--version X.Y.Z]
|
||||
# GEN1RECOMP_SOURCE_DIR="$PWD" ./build-linux-arm-sbc.sh --version X.Y.Z
|
||||
# ./build-linux-arm-sbc.sh --source /path/to/gen1recomp --version X.Y.Z
|
||||
#
|
||||
# Output:
|
||||
# dist/linux-arm-sbc/gen1recomp-sbc-portmaster.zip
|
||||
#
|
||||
# Install on device:
|
||||
# 1. Install PortMaster for the handheld firmware.
|
||||
# 2. Unzip into the device's PortMaster ports folder so you have:
|
||||
# Roms/Ports (PORTS)/gen1recomp-sbc.sh
|
||||
# Roms/Ports (PORTS)/gen1recomp-sbc/...
|
||||
# 3. Copy a legal US Red or Blue .gb into Roms/Ports (PORTS)/gen1recomp-sbc/lovegame/
|
||||
# 4. Launch "gen1recomp-sbc" from the Ports list; press Choose ROM (scans that
|
||||
# folder when zenity is missing).
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")" && pwd)"
|
||||
HERE="$ROOT/.bazinga"
|
||||
CACHE="$HERE/cache/linux-arm-sbc"
|
||||
WORK="$HERE/work/linux-arm-sbc"
|
||||
DIST="$ROOT/dist/linux-arm-sbc"
|
||||
|
||||
APP_NAME="gen1recomp-sbc"
|
||||
# Artifact suffix identifies this as the generic PortMaster SBC package.
|
||||
# Release uploads stage it as gen1recomp-<ver>-sbc-portmaster.zip.
|
||||
ARTIFACT_SUFFIX="portmaster"
|
||||
PORT_DIR_NAME="gen1recomp-sbc"
|
||||
LAUNCHER_NAME="gen1recomp-sbc.sh"
|
||||
LOVE_VERSION="11.5"
|
||||
# By default the pack is reproducible from the latest published GitHub release,
|
||||
# not whatever happens to be in the caller's checkout. Development builds can
|
||||
# point this at a local checkout with GEN1RECOMP_SOURCE_DIR=/path/to/repo.
|
||||
SOURCE_DIR_OVERRIDE="${GEN1RECOMP_SOURCE_DIR:-}"
|
||||
SOURCE_TAG_OVERRIDE="${GEN1RECOMP_RELEASE_TAG:-}"
|
||||
VERSION="${GEN1RECOMP_VERSION:-}"
|
||||
|
||||
# Official PortMaster LÖVE 11.5 aarch64 runtime (small love stub + liblove).
|
||||
PM_RUNTIME_BASE="https://raw.githubusercontent.com/PortsMaster/PortMaster-GUI/main/PortMaster/runtimes/love_${LOVE_VERSION}"
|
||||
RELEASES_LATEST_URL="https://github.com/bryanthaboi/gen1recomp/releases/latest"
|
||||
RELEASE_TARBALL_BASE="https://github.com/bryanthaboi/gen1recomp/archive/refs/tags"
|
||||
|
||||
say() { printf '\033[1;32m==>\033[0m %s\n' "$*"; }
|
||||
warn() { printf '\033[1;33mwarn:\033[0m %s\n' "$*" >&2; }
|
||||
fail() { printf '\033[1;31merror:\033[0m %s\n' "$*" >&2; exit 1; }
|
||||
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--version) [ $# -ge 2 ] || fail "--version needs X.Y.Z"; VERSION="$2"; shift ;;
|
||||
--source) [ $# -ge 2 ] || fail "--source needs a directory"; SOURCE_DIR_OVERRIDE="$2"; shift ;;
|
||||
--release-tag) [ $# -ge 2 ] || fail "--release-tag needs a tag"; SOURCE_TAG_OVERRIDE="$2"; shift ;;
|
||||
-h|--help)
|
||||
sed -n '2,24p' "$0"
|
||||
exit 0
|
||||
;;
|
||||
*) fail "unknown argument: $1" ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
|
||||
command -v curl >/dev/null || fail "curl is required"
|
||||
command -v zip >/dev/null || fail "zip is required"
|
||||
command -v unzip >/dev/null || fail "unzip is required"
|
||||
command -v tar >/dev/null || fail "tar is required"
|
||||
|
||||
mkdir -p "$CACHE" "$WORK" "$DIST"
|
||||
|
||||
download() {
|
||||
local url="$1" dest="$2"
|
||||
if [ -f "$dest" ] && [ -s "$dest" ]; then
|
||||
return 0
|
||||
fi
|
||||
say "downloading $(basename "$dest")"
|
||||
curl -fL --progress-bar "$url" -o "$dest.tmp" \
|
||||
|| fail "download failed: $url"
|
||||
mv "$dest.tmp" "$dest"
|
||||
}
|
||||
|
||||
# --------------------------------------------------------------- source + game tree
|
||||
# Release builds use the latest published source archive. A local checkout is
|
||||
# an explicit override for development and for CI's just-built release source.
|
||||
if [ -n "$SOURCE_DIR_OVERRIDE" ]; then
|
||||
SOURCE_DIR_OVERRIDE="$(cd "$SOURCE_DIR_OVERRIDE" 2>/dev/null && pwd)" \
|
||||
|| fail "source directory does not exist: $SOURCE_DIR_OVERRIDE"
|
||||
SOURCE_DIR="$SOURCE_DIR_OVERRIDE"
|
||||
SOURCE_TAG="${SOURCE_TAG_OVERRIDE:-local}"
|
||||
if [ "$SOURCE_TAG" != "local" ]; then
|
||||
printf '%s' "$SOURCE_TAG" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+$' \
|
||||
|| fail "release tag must look like vX.Y.Z: $SOURCE_TAG"
|
||||
fi
|
||||
if [ -z "$VERSION" ]; then
|
||||
VERSION="$(git -C "$SOURCE_DIR" rev-parse --short HEAD 2>/dev/null || echo dev)"
|
||||
fi
|
||||
else
|
||||
if [ -z "$SOURCE_TAG_OVERRIDE" ]; then
|
||||
latest_location="$(curl -fsSI "$RELEASES_LATEST_URL" \
|
||||
| awk 'tolower($1) == "location:" { print $2 }' | tail -1 | tr -d '\r')" \
|
||||
|| fail "could not resolve latest published release"
|
||||
SOURCE_TAG_OVERRIDE="${latest_location##*/}"
|
||||
fi
|
||||
printf '%s' "$SOURCE_TAG_OVERRIDE" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+$' \
|
||||
|| fail "release tag must look like vX.Y.Z: $SOURCE_TAG_OVERRIDE"
|
||||
SOURCE_TAG="$SOURCE_TAG_OVERRIDE"
|
||||
SOURCE_ARCHIVE="$CACHE/gen1recomp-${SOURCE_TAG}.tar.gz"
|
||||
download "$RELEASE_TARBALL_BASE/$SOURCE_TAG.tar.gz" "$SOURCE_ARCHIVE"
|
||||
SOURCE_EXTRACT="$WORK/source-$SOURCE_TAG"
|
||||
rm -rf "$SOURCE_EXTRACT"
|
||||
mkdir -p "$SOURCE_EXTRACT"
|
||||
tar -xzf "$SOURCE_ARCHIVE" -C "$SOURCE_EXTRACT"
|
||||
SOURCE_DIR="$(find "$SOURCE_EXTRACT" -mindepth 1 -maxdepth 1 -type d -print -quit)"
|
||||
[ -n "$SOURCE_DIR" ] || fail "release archive had no source directory"
|
||||
if [ -z "$VERSION" ]; then VERSION="${SOURCE_TAG#v}"; fi
|
||||
fi
|
||||
|
||||
say "staging lovegame/ from $SOURCE_TAG"
|
||||
GAME_SRC="$WORK/lovegame"
|
||||
rm -rf "$GAME_SRC"
|
||||
mkdir -p "$GAME_SRC"
|
||||
|
||||
# Same payload as scripts/build.sh's game.love — never ship ROM-derived cache.
|
||||
# tools/save-editor is part of that payload: the launcher's Edit button on a
|
||||
# save row opens it in-process (main.lua).
|
||||
(cd "$SOURCE_DIR" && zip -q -9 -r "$WORK/game-payload.zip" \
|
||||
main.lua conf.lua src libs data assets tools/save-editor \
|
||||
tools/rom_manifest.json tools/rom_manifest_blue.json \
|
||||
-x '*.DS_Store' 'data/generated/*' 'assets/generated/*')
|
||||
if unzip -Z1 "$WORK/game-payload.zip" \
|
||||
| grep -Eq '^(data|assets)/generated/[^/]+|^(data|assets)/generated/.+/'; then
|
||||
fail "payload unexpectedly contains generated ROM data"
|
||||
fi
|
||||
unzip -q "$WORK/game-payload.zip" -d "$GAME_SRC"
|
||||
rm -f "$WORK/game-payload.zip"
|
||||
|
||||
# Stamp release version into the staged tree only (never the working tree).
|
||||
if printf '%s' "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then
|
||||
say "stamping engine version $VERSION"
|
||||
sed -E "s/(engine[[:space:]]*=[[:space:]]*\")[^\"]*(\")/\1$VERSION\2/" \
|
||||
"$SOURCE_DIR/src/core/Version.lua" > "$GAME_SRC/src/core/Version.lua"
|
||||
version_re="$(printf '%s' "$VERSION" | sed 's/\./\\./g')"
|
||||
grep -Eq "engine[[:space:]]*=[[:space:]]*\"$version_re\"" \
|
||||
"$GAME_SRC/src/core/Version.lua" \
|
||||
|| fail "version stamp failed"
|
||||
else
|
||||
say "version '$VERSION' is not X.Y.Z — shipping default engine (no stamp)"
|
||||
fi
|
||||
|
||||
# Portable marker: saves + ROM cache live next to the game on the SD card.
|
||||
: > "$GAME_SRC/portable.txt"
|
||||
|
||||
# --------------------------------------------------------------- love runtime
|
||||
say "fetching LÖVE $LOVE_VERSION aarch64 runtime"
|
||||
LOVE_BIN="$CACHE/love.aarch64"
|
||||
LOVE_LIB="$CACHE/liblove-11.5.so"
|
||||
LUAJIT_LIB="$CACHE/libluajit-5.1.so.2"
|
||||
MODPLUG_LIB="$CACHE/libmodplug.so.1"
|
||||
OGG_LIB="$CACHE/libogg.so.0"
|
||||
|
||||
download "$PM_RUNTIME_BASE/love.aarch64" "$LOVE_BIN"
|
||||
download "$PM_RUNTIME_BASE/libs.aarch64/liblove-11.5.so" "$LOVE_LIB"
|
||||
download "$PM_RUNTIME_BASE/libs.aarch64/libluajit-5.1.so.2" "$LUAJIT_LIB"
|
||||
download "$PM_RUNTIME_BASE/libs.aarch64/libmodplug.so.1" "$MODPLUG_LIB"
|
||||
download "$PM_RUNTIME_BASE/libs.aarch64/libogg.so.0" "$OGG_LIB"
|
||||
|
||||
# Sanity: love stub must be an aarch64 ELF.
|
||||
file "$LOVE_BIN" | grep -qi 'aarch64\|ARM aarch64' \
|
||||
|| fail "love.aarch64 does not look like an aarch64 ELF (got: $(file "$LOVE_BIN"))"
|
||||
|
||||
# --------------------------------------------------------------- port tree
|
||||
say "assembling port package"
|
||||
PORT_ROOT="$WORK/port"
|
||||
rm -rf "$PORT_ROOT"
|
||||
mkdir -p "$PORT_ROOT/$PORT_DIR_NAME/bin" \
|
||||
"$PORT_ROOT/$PORT_DIR_NAME/libs.aarch64" \
|
||||
"$PORT_ROOT/$PORT_DIR_NAME/licenses" \
|
||||
"$PORT_ROOT/$PORT_DIR_NAME/conf"
|
||||
|
||||
cp -R "$GAME_SRC" "$PORT_ROOT/$PORT_DIR_NAME/lovegame"
|
||||
cp "$LOVE_BIN" "$PORT_ROOT/$PORT_DIR_NAME/bin/love.aarch64"
|
||||
chmod +x "$PORT_ROOT/$PORT_DIR_NAME/bin/love.aarch64"
|
||||
cp "$LOVE_LIB" "$LUAJIT_LIB" "$MODPLUG_LIB" "$OGG_LIB" \
|
||||
"$PORT_ROOT/$PORT_DIR_NAME/libs.aarch64/"
|
||||
|
||||
# Drop a short license pointer for the bundled LÖVE bits.
|
||||
cat > "$PORT_ROOT/$PORT_DIR_NAME/licenses/LICENSE.love2d.txt" <<'EOF'
|
||||
This port bundles the LÖVE 11.5 aarch64 runtime from PortMaster
|
||||
(https://github.com/PortsMaster/PortMaster-GUI). LÖVE is zlib-licensed;
|
||||
see https://love2d.org/ for full terms.
|
||||
EOF
|
||||
|
||||
# --------------------------------------------------------------- launcher
|
||||
# Resolve the game directory from the launcher so this works with both
|
||||
# PortMaster-managed ports directories.
|
||||
cat > "$PORT_ROOT/$LAUNCHER_NAME" <<'EOF'
|
||||
#!/bin/bash
|
||||
# gen1recomp-sbc — Linux ARM SBC / PortMaster launcher
|
||||
# Uses SHDIR-relative paths so firmware-specific mount points do not matter.
|
||||
|
||||
export HOME="${HOME:-/root}"
|
||||
XDG_DATA_HOME="${XDG_DATA_HOME:-$HOME/.local/share}"
|
||||
SHDIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
|
||||
if [ -d "/mnt/SDCARD/Apps/PortMaster/PortMaster/" ]; then
|
||||
controlfolder="/mnt/SDCARD/Apps/PortMaster/PortMaster"
|
||||
elif [ -d "/mnt/SDCARD/Roms/ports/PortMaster" ]; then
|
||||
controlfolder="/mnt/SDCARD/Roms/ports/PortMaster"
|
||||
elif [ -d "/mnt/SDCARD/Data/PortMaster/" ]; then
|
||||
controlfolder="/mnt/SDCARD/Data/PortMaster"
|
||||
elif [ -d "$SHDIR/PortMaster" ]; then
|
||||
controlfolder="$SHDIR/PortMaster"
|
||||
elif [ -d "/opt/system/Tools/PortMaster/" ]; then
|
||||
controlfolder="/opt/system/Tools/PortMaster"
|
||||
elif [ -d "/opt/tools/PortMaster/" ]; then
|
||||
controlfolder="/opt/tools/PortMaster"
|
||||
elif [ -d "$XDG_DATA_HOME/PortMaster/" ]; then
|
||||
controlfolder="$XDG_DATA_HOME/PortMaster"
|
||||
elif [ -d "/roms/ports/PortMaster" ]; then
|
||||
controlfolder="/roms/ports/PortMaster"
|
||||
else
|
||||
controlfolder="/mnt/SDCARD/Roms/PORTS/PortMaster"
|
||||
fi
|
||||
|
||||
if [ ! -f "$controlfolder/control.txt" ]; then
|
||||
echo "PortMaster control.txt not found under $controlfolder" >&2
|
||||
exit 1
|
||||
fi
|
||||
# shellcheck disable=SC1090
|
||||
source "$controlfolder/control.txt"
|
||||
get_controls
|
||||
if [ -n "${CFW_NAME:-}" ] && [ -f "${controlfolder}/mod_${CFW_NAME}.txt" ]; then
|
||||
# shellcheck disable=SC1090
|
||||
source "${controlfolder}/mod_${CFW_NAME}.txt"
|
||||
fi
|
||||
|
||||
GAMEDIR="$SHDIR/gen1recomp-sbc"
|
||||
CONFDIR="$GAMEDIR/conf"
|
||||
mkdir -p "$CONFDIR"
|
||||
|
||||
cd "$GAMEDIR" || exit 1
|
||||
> "$GAMEDIR/log.txt" && exec > >(tee "$GAMEDIR/log.txt") 2>&1
|
||||
|
||||
export XDG_DATA_HOME="$CONFDIR"
|
||||
export XDG_CONFIG_HOME="$CONFDIR"
|
||||
export LD_LIBRARY_PATH="$GAMEDIR/libs.aarch64:${LD_LIBRARY_PATH:-}"
|
||||
export SDL_GAMECONTROLLERCONFIG="${sdl_controllerconfig:-}"
|
||||
# GLES is the common path on ARM SBC handhelds; firmware may override it.
|
||||
export LOVE_GRAPHICS_USE_OPENGLES="${LOVE_GRAPHICS_USE_OPENGLES:-1}"
|
||||
|
||||
$ESUDO chmod a+x ./bin/love.aarch64 2>/dev/null || chmod a+x ./bin/love.aarch64
|
||||
$ESUDO chmod 666 /dev/uinput 2>/dev/null || true
|
||||
|
||||
if [ -n "${GPTOKEYB:-}" ]; then
|
||||
$GPTOKEYB "love.aarch64" &
|
||||
fi
|
||||
if type pm_platform_helper >/dev/null 2>&1; then
|
||||
pm_platform_helper "$GAMEDIR/bin/love.aarch64"
|
||||
fi
|
||||
|
||||
./bin/love.aarch64 "$GAMEDIR/lovegame"
|
||||
|
||||
if type pm_finish >/dev/null 2>&1; then
|
||||
pm_finish
|
||||
else
|
||||
if [ -n "${ESUDO:-}" ]; then
|
||||
$ESUDO kill -9 $(pidof gptokeyb) 2>/dev/null || true
|
||||
else
|
||||
kill -9 $(pidof gptokeyb) 2>/dev/null || true
|
||||
fi
|
||||
fi
|
||||
EOF
|
||||
chmod +x "$PORT_ROOT/$LAUNCHER_NAME"
|
||||
|
||||
# --------------------------------------------------------------- metadata
|
||||
cat > "$PORT_ROOT/port.json" <<EOF
|
||||
{
|
||||
"version": 2,
|
||||
"name": "gen1recomp-sbc.zip",
|
||||
"items": [
|
||||
"$LAUNCHER_NAME",
|
||||
"$PORT_DIR_NAME"
|
||||
],
|
||||
"items_opt": null,
|
||||
"attr": {
|
||||
"title": "gen1recomp-sbc",
|
||||
"desc": "Native LÖVE2D recreation of Pokemon Red and Blue. Supply your own legal US Red or Blue ROM.",
|
||||
"source": "https://github.com/bryanthaboi/gen1recomp/releases/tag/$SOURCE_TAG",
|
||||
"inst": "Requires a 64-bit Linux ARM handheld with PortMaster. Copy a canonical US Red or Blue .gb into gen1recomp-sbc/lovegame/, then launch and press Choose ROM.",
|
||||
"genres": ["adventure", "rpg"],
|
||||
"porter": ["gen1recomp-sbc"],
|
||||
"image": {},
|
||||
"rtr": true,
|
||||
"runtime": null,
|
||||
"reqs": [],
|
||||
"arch": ["aarch64"]
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
||||
cat > "$PORT_ROOT/gameinfo.xml" <<EOF
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<gameList>
|
||||
<game>
|
||||
<path>./$LAUNCHER_NAME</path>
|
||||
<name>gen1recomp-sbc</name>
|
||||
<desc>Native LÖVE2D recreation of Pokemon Red and Blue. Requires your own legal US Red or Blue ROM.</desc>
|
||||
<releasedate>20250101T000000</releasedate>
|
||||
<developer>the bois club</developer>
|
||||
<publisher>the bois club</publisher>
|
||||
<genre>RPG</genre>
|
||||
</game>
|
||||
</gameList>
|
||||
EOF
|
||||
|
||||
cat > "$PORT_ROOT/README.md" <<'EOF'
|
||||
## gen1recomp-sbc (Linux ARM SBC / PortMaster)
|
||||
|
||||
Native LÖVE 11.5 aarch64 PortMaster port of gen1recomp for compatible Linux ARM SBC handhelds, including H700-class devices. This pack was built from source release **__SOURCE_TAG__**.
|
||||
|
||||
### Install
|
||||
|
||||
1. Install PortMaster for your handheld firmware.
|
||||
2. Unzip so `gen1recomp-sbc.sh` and the `gen1recomp-sbc/` folder are siblings in the device's PortMaster ports directory.
|
||||
3. Copy a legal US Pokémon Red or Blue `.gb` into `gen1recomp-sbc/lovegame/`.
|
||||
4. Refresh the launcher and launch **gen1recomp-sbc** from Ports.
|
||||
|
||||
### Controls
|
||||
|
||||
| Input | Action |
|
||||
|--|--|
|
||||
| D-pad | Move cursor |
|
||||
| A | Click |
|
||||
| L1 / R1 | Switch tabs |
|
||||
| Start / Select | Play or choose ROM |
|
||||
|
||||
Controls use the normal PortMaster / SDL pad map. Device-specific power/suspend behavior is supplied by the firmware and PortMaster runtime.
|
||||
|
||||
### First run
|
||||
|
||||
Put the `.gb` in `lovegame/`, then press **Choose ROM**. After import, the ROM-derived cache and saves stay beside the game (`portable.txt`).
|
||||
|
||||
### Thanks
|
||||
|
||||
LÖVE runtime binaries from [PortMaster](https://portmaster.games/). PortMaster device support and runtime integration are maintained by the PortMaster team.
|
||||
EOF
|
||||
sed -i.bak "s/__SOURCE_TAG__/$SOURCE_TAG/g" "$PORT_ROOT/README.md"
|
||||
rm -f "$PORT_ROOT/README.md.bak"
|
||||
|
||||
# --------------------------------------------------------------- zip
|
||||
ZIP_OUT="$DIST/$APP_NAME-$ARTIFACT_SUFFIX.zip"
|
||||
rm -f "$ZIP_OUT"
|
||||
say "packing $ZIP_OUT"
|
||||
(cd "$PORT_ROOT" && zip -q -9 -r "$ZIP_OUT" \
|
||||
"$LAUNCHER_NAME" "$PORT_DIR_NAME" port.json gameinfo.xml README.md)
|
||||
|
||||
say "done."
|
||||
say "artifact: $ZIP_OUT ($(du -h "$ZIP_OUT" | cut -f1))"
|
||||
say "copy into the device PortMaster ports folder, then drop your .gb into gen1recomp-sbc/lovegame/"
|
||||
@@ -89,13 +89,16 @@ mkdir -p "$GAME_SRC"
|
||||
# tools/save-editor is part of that payload: the launcher's Edit button on a
|
||||
# save row opens it in-process (main.lua).
|
||||
(cd "$ROOT" && zip -q -9 -r "$WORK/game-payload.zip" \
|
||||
main.lua conf.lua src data assets tools/save-editor \
|
||||
main.lua conf.lua src libs data assets tools/save-editor \
|
||||
tools/rom_manifest.json tools/rom_manifest_blue.json \
|
||||
tools/rom_manifest_yellow.json tools/rom_manifest_gold.json \
|
||||
-x '*.DS_Store' 'data/generated/*' 'assets/generated/*')
|
||||
if unzip -Z1 "$WORK/game-payload.zip" \
|
||||
| grep -Eq '^(data|assets)/generated/[^/]+|^(data|assets)/generated/.+/'; then
|
||||
fail "payload unexpectedly contains generated ROM data"
|
||||
fi
|
||||
payload_list="$(unzip -Z1 "$WORK/game-payload.zip")"
|
||||
printf '%s\n' "$payload_list" \
|
||||
| grep -Eq '^(data|assets)/generated/[^/]+|^(data|assets)/generated/.+/' \
|
||||
&& fail "payload unexpectedly contains generated ROM data"
|
||||
printf '%s\n' "$payload_list" | grep -qxF "tools/rom_manifest_gold.json" \
|
||||
|| fail "payload is missing tools/rom_manifest_gold.json"
|
||||
unzip -q "$WORK/game-payload.zip" -d "$GAME_SRC"
|
||||
rm -f "$WORK/game-payload.zip"
|
||||
|
||||
|
||||
@@ -1,18 +1,35 @@
|
||||
function love.conf(t)
|
||||
-- PhysFS ignores symlinks unless told otherwise, so a mod dev-linked into
|
||||
-- mods/ (ln -s, matching the mklink /J workflow on Windows) is invisible
|
||||
-- to love.filesystem.getDirectoryItems without this.
|
||||
love.filesystem.setSymlinksEnabled(true)
|
||||
|
||||
local editor = os.getenv("POKEPORT_EDITOR") == "1"
|
||||
local developer = os.getenv("POKEPORT_DEV") == "1"
|
||||
local companion = nil
|
||||
if arg then
|
||||
for _, a in ipairs(arg) do
|
||||
if a == "--editor" then editor = true end
|
||||
if a == "--developer" then developer = true end
|
||||
local port, token = a:match("^%-%-display%-companion=(%d+),([%w]+)$")
|
||||
if port then companion = { port = tonumber(port), token = token } end
|
||||
end
|
||||
end
|
||||
-- main.lua runs in the same Lua state right after conf.lua; stash the
|
||||
-- decision in a global so it doesn't need to reparse `arg`.
|
||||
_G.POKEPORT_EDITOR_MODE = editor
|
||||
_G.POKEPORT_DEV_MODE = developer
|
||||
_G.POKEPORT_DISPLAY_COMPANION = companion
|
||||
|
||||
if editor then
|
||||
if companion then
|
||||
t.identity = "pokemon-love2d-companion"
|
||||
t.window.title = "gen1recomp Secondary Display"
|
||||
t.window.width = 640
|
||||
t.window.height = 576
|
||||
t.window.minwidth = 160
|
||||
t.window.minheight = 144
|
||||
t.window.resizable = true
|
||||
elseif editor then
|
||||
-- Same identity as the game, deliberately: the editor edits the game's
|
||||
-- saves and reads the game's ROM cache, both of which live under this
|
||||
-- folder. A private editor identity would point love.filesystem at an
|
||||
@@ -35,17 +52,36 @@ function love.conf(t)
|
||||
-- starting size, not the game's resolution.
|
||||
t.window.width = 1024
|
||||
t.window.height = 768
|
||||
-- Floor for the resizable desktop window. The launcher's single-column
|
||||
-- layout is laid out against ~420 logical px of content, and the game
|
||||
-- canvas letterboxes fine below that, so this only stops a drag that would
|
||||
-- squeeze the cards past the point where their buttons still read. Kept
|
||||
-- well under the smallest supported desktop display; mobile ignores it
|
||||
-- (fullscreen), so the handheld ports are unaffected.
|
||||
t.window.minwidth = 480
|
||||
t.window.minheight = 360
|
||||
end
|
||||
t.version = "11.5"
|
||||
t.version = love._os == "iOS" and "12.0" or "11.5"
|
||||
t.window.vsync = 1
|
||||
t.modules.joystick = true
|
||||
t.modules.audio = not companion
|
||||
t.modules.joystick = not companion
|
||||
t.modules.physics = false
|
||||
|
||||
-- love.system is not loaded during love.conf; love._os is set by the
|
||||
-- engine before conf runs (LÖVE 11.x / 11.5).
|
||||
local osName = love._os
|
||||
local mobile = osName == "Android" or osName == "iOS"
|
||||
if mobile then
|
||||
local nx = osName == "NX"
|
||||
if nx then
|
||||
-- Switch (love-nx): hint handheld 720p. SDL auto-switches portable↔dock
|
||||
-- (720p↔1080p) only when the window is resizable and not exclusive
|
||||
-- fullscreen; NxDisplay.sync also applies the size on boot and dock change.
|
||||
t.window.width = 1280
|
||||
t.window.height = 720
|
||||
t.window.fullscreen = false
|
||||
t.window.resizable = true
|
||||
t.window.highdpi = false
|
||||
elseif mobile then
|
||||
-- resizable is what unlocks orientation. SDL's Android backend, given no
|
||||
-- SDL_HINT_ORIENTATIONS (LÖVE sets none), calls setRequestedOrientation
|
||||
-- at window creation -- FULL_SENSOR when the window is resizable (rotates
|
||||
@@ -54,7 +90,14 @@ function love.conf(t)
|
||||
-- the game follow the device. The renderer letterboxes the 160x144
|
||||
-- viewport into whatever size results, and the on-screen touch controls
|
||||
-- re-lay themselves out from the new window size, so both orientations
|
||||
-- just work. iOS follows the Info.plist orientations
|
||||
-- just work. FULL_SENSOR ignores the device's rotation lock, so
|
||||
-- GameActivity.setOrientationBis remaps it to FULL_USER after SDL has
|
||||
-- run: same orientations allowed, but auto-rotate being off now wins.
|
||||
-- A persisted ORIENTATION lock (#592) overrides all of this after boot:
|
||||
-- src/core/Orientation.lua sets SDL_HINT_ORIENTATIONS over the FFI and
|
||||
-- re-triggers the request, from main.lua for the launcher and from
|
||||
-- Game:applyOptions in game.
|
||||
-- iOS follows the Info.plist orientations
|
||||
-- (see mobile/ios/overlays/love-ios.plist, now portrait + landscape).
|
||||
t.window.resizable = true
|
||||
-- Starting size is a tall portrait hint; the OS resizes to the real
|
||||
@@ -70,6 +113,14 @@ function love.conf(t)
|
||||
-- player to copy their ROM there instead of needing a native file
|
||||
-- picker (LOVE 11.5 on Android has none -- see src/import/RomImporter.lua).
|
||||
t.externalstorage = osName == "Android"
|
||||
-- LOVE 11.x exposes the phone's accelerometer as a 3-axis joystick by
|
||||
-- default. Gravity keeps one axis pinned near +/-1.0 whenever the phone
|
||||
-- is held upright, so it streams past-deadzone joystickaxis events that
|
||||
-- both hid the touch overlay every instant (it reads as "a controller is
|
||||
-- in use") and, via the generic-joystick axis mapping (#459), walked the
|
||||
-- player around by tilt. Nothing in the game reads the accelerometer,
|
||||
-- so drop the device entirely (#468).
|
||||
t.accelerometerjoystick = false
|
||||
else
|
||||
t.window.resizable = true
|
||||
end
|
||||
|
||||
@@ -1683,158 +1683,172 @@ return {
|
||||
{ 0, 0, 0 },
|
||||
},
|
||||
},
|
||||
-- Species -> palette NAME, taken from pokered-gbc data/pokemon/palettes.asm
|
||||
-- ELSE branch -- the one that is NOT gated on GEN_2_GRAPHICS.
|
||||
--
|
||||
-- That file has two tables. The IF GEN_2_GRAPHICS branch assigns a
|
||||
-- PER-SPECIES palette (PAL_BULBASAUR, PAL_SQUIRTLE, ...) authored for Gen 2
|
||||
-- sprite art; the ELSE branch keeps Gen 1's own assignments (GREENMON,
|
||||
-- CYANMON, ...). This port extracts Gen 1 pics from the ROM, so the Gen 1
|
||||
-- branch is the matching one. The per-species table was imported here by
|
||||
-- mistake, which pointed every mon at colours shaded for different art:
|
||||
-- Bulbasaur wore PAL_BULBASAUR's red-orange over a sprite that has no red
|
||||
-- on it, and Squirtle wore PAL_SQUIRTLE's shell brown on his head.
|
||||
--
|
||||
-- The palette VALUES below are still pokered-gbc's, so ADVANCED keeps its
|
||||
-- richer colours -- only which palette each species points at changed.
|
||||
pokemon = {
|
||||
ABRA = "ABRA",
|
||||
AERODACTYL = "AERODACTYL",
|
||||
ALAKAZAM = "ALAKAZAM",
|
||||
ARBOK = "ARBOK",
|
||||
ARCANINE = "ARCANINE",
|
||||
ARTICUNO = "ARTICUNO",
|
||||
BEEDRILL = "BEEDRILL",
|
||||
BELLSPROUT = "BELLSPROUT",
|
||||
BLASTOISE = "BLASTOISE",
|
||||
BULBASAUR = "BULBASAUR",
|
||||
BUTTERFREE = "BUTTERFREE",
|
||||
CATERPIE = "CATERPIE",
|
||||
CHANSEY = "CHANSEY",
|
||||
CHARIZARD = "CHARIZARD",
|
||||
CHARMANDER = "CHARMANDER",
|
||||
CHARMELEON = "CHARMELEON",
|
||||
CLEFABLE = "CLEFABLE",
|
||||
CLEFAIRY = "CLEFAIRY",
|
||||
CLOYSTER = "CLOYSTER",
|
||||
CUBONE = "CUBONE",
|
||||
DEWGONG = "DEWGONG",
|
||||
DIGLETT = "DIGLETT",
|
||||
DITTO = "DITTO",
|
||||
DODRIO = "DODRIO",
|
||||
DODUO = "DODUO",
|
||||
DRAGONAIR = "DRAGONAIR",
|
||||
DRAGONITE = "DRAGONITE",
|
||||
DRATINI = "DRATINI",
|
||||
DROWZEE = "DROWZEE",
|
||||
DUGTRIO = "DUGTRIO",
|
||||
EEVEE = "EEVEE",
|
||||
EKANS = "EKANS",
|
||||
ELECTABUZZ = "ELECTABUZZ",
|
||||
ELECTRODE = "ELECTRODE",
|
||||
EXEGGCUTE = "EXEGGCUTE",
|
||||
EXEGGUTOR = "EXEGGUTOR",
|
||||
FARFETCHD = "FARFETCH_D",
|
||||
FEAROW = "FEAROW",
|
||||
FLAREON = "FLAREON",
|
||||
GASTLY = "GASTLY",
|
||||
GENGAR = "GENGAR",
|
||||
GEODUDE = "GEODUDE",
|
||||
GLOOM = "GLOOM",
|
||||
GOLBAT = "GOLBAT",
|
||||
GOLDEEN = "GOLDEEN",
|
||||
GOLDUCK = "GOLDUCK",
|
||||
GOLEM = "GOLEM",
|
||||
GRAVELER = "GRAVELER",
|
||||
GRIMER = "GRIMER",
|
||||
GROWLITHE = "GROWLITHE",
|
||||
GYARADOS = "GYARADOS",
|
||||
HAUNTER = "HAUNTER",
|
||||
HITMONCHAN = "HITMONCHAN",
|
||||
HITMONLEE = "HITMONLEE",
|
||||
HORSEA = "HORSEA",
|
||||
HYPNO = "HYPNO",
|
||||
IVYSAUR = "IVYSAUR",
|
||||
JIGGLYPUFF = "JIGGLYPUFF",
|
||||
JOLTEON = "JOLTEON",
|
||||
JYNX = "JYNX",
|
||||
KABUTO = "KABUTO",
|
||||
KABUTOPS = "KABUTOPS",
|
||||
KADABRA = "KADABRA",
|
||||
KAKUNA = "KAKUNA",
|
||||
KANGASKHAN = "KANGASKHAN",
|
||||
KINGLER = "KINGLER",
|
||||
KOFFING = "KOFFING",
|
||||
KRABBY = "KRABBY",
|
||||
LAPRAS = "LAPRAS",
|
||||
LICKITUNG = "LICKITUNG",
|
||||
MACHAMP = "MACHAMP",
|
||||
MACHOKE = "MACHOKE",
|
||||
MACHOP = "MACHOP",
|
||||
MAGIKARP = "MAGIKARP",
|
||||
MAGMAR = "MAGMAR",
|
||||
MAGNEMITE = "MAGNEMITE",
|
||||
MAGNETON = "MAGNETON",
|
||||
MANKEY = "MANKEY",
|
||||
MAROWAK = "MAROWAK",
|
||||
MEOWTH = "MEOWTH",
|
||||
METAPOD = "METAPOD",
|
||||
MEW = "MEW",
|
||||
MEWTWO = "MEWTWO",
|
||||
MOLTRES = "MOLTRES",
|
||||
MR_MIME = "MR_MIME",
|
||||
MUK = "MUK",
|
||||
NIDOKING = "NIDOKING",
|
||||
NIDOQUEEN = "NIDOQUEEN",
|
||||
NIDORAN_F = "NIDORAN_F",
|
||||
NIDORAN_M = "NIDORAN_M",
|
||||
NIDORINA = "NIDORINA",
|
||||
NIDORINO = "NIDORINO",
|
||||
NINETALES = "NINETALES",
|
||||
ODDISH = "ODDISH",
|
||||
OMANYTE = "OMANYTE",
|
||||
OMASTAR = "OMASTAR",
|
||||
ONIX = "ONIX",
|
||||
PARAS = "PARAS",
|
||||
PARASECT = "PARASECT",
|
||||
PERSIAN = "PERSIAN",
|
||||
PIDGEOT = "PIDGEOT",
|
||||
PIDGEOTTO = "PIDGEOTTO",
|
||||
PIDGEY = "PIDGEY",
|
||||
PIKACHU = "PIKACHU",
|
||||
PINSIR = "PINSIR",
|
||||
POLIWAG = "POLIWAG",
|
||||
POLIWHIRL = "POLIWHIRL",
|
||||
POLIWRATH = "POLIWRATH",
|
||||
PONYTA = "PONYTA",
|
||||
PORYGON = "PORYGON",
|
||||
PRIMEAPE = "PRIMEAPE",
|
||||
PSYDUCK = "PSYDUCK",
|
||||
RAICHU = "RAICHU",
|
||||
RAPIDASH = "RAPIDASH",
|
||||
RATICATE = "RATICATE",
|
||||
RATTATA = "RATTATA",
|
||||
RHYDON = "RHYDON",
|
||||
RHYHORN = "RHYHORN",
|
||||
SANDSHREW = "SANDSHREW",
|
||||
SANDSLASH = "SANDSLASH",
|
||||
SCYTHER = "SCYTHER",
|
||||
SEADRA = "SEADRA",
|
||||
SEAKING = "SEAKING",
|
||||
SEEL = "SEEL",
|
||||
SHELLDER = "SHELLDER",
|
||||
SLOWBRO = "SLOWBRO",
|
||||
SLOWPOKE = "SLOWPOKE",
|
||||
SNORLAX = "SNORLAX",
|
||||
SPEAROW = "SPEAROW",
|
||||
SQUIRTLE = "SQUIRTLE",
|
||||
STARMIE = "STARMIE",
|
||||
STARYU = "STARYU",
|
||||
TANGELA = "TANGELA",
|
||||
TAUROS = "TAUROS",
|
||||
TENTACOOL = "TENTACOOL",
|
||||
TENTACRUEL = "TENTACRUEL",
|
||||
VAPOREON = "VAPOREON",
|
||||
VENOMOTH = "VENOMOTH",
|
||||
VENONAT = "VENONAT",
|
||||
VENUSAUR = "VENUSAUR",
|
||||
VICTREEBEL = "VICTREEBEL",
|
||||
VILEPLUME = "VILEPLUME",
|
||||
VOLTORB = "VOLTORB",
|
||||
VULPIX = "VULPIX",
|
||||
WARTORTLE = "WARTORTLE",
|
||||
WEEDLE = "WEEDLE",
|
||||
WEEPINBELL = "WEEPINBELL",
|
||||
WEEZING = "WEEZING",
|
||||
WIGGLYTUFF = "WIGGLYTUFF",
|
||||
ZAPDOS = "ZAPDOS",
|
||||
ZUBAT = "ZUBAT",
|
||||
ABRA = "YELLOWMON",
|
||||
AERODACTYL = "GRAYMON",
|
||||
ALAKAZAM = "YELLOWMON",
|
||||
ARBOK = "PURPLEMON",
|
||||
ARCANINE = "REDMON",
|
||||
ARTICUNO = "BLUEMON",
|
||||
BEEDRILL = "YELLOWMON",
|
||||
BELLSPROUT = "GREENMON",
|
||||
BLASTOISE = "CYANMON",
|
||||
BULBASAUR = "GREENMON",
|
||||
BUTTERFREE = "CYANMON",
|
||||
CATERPIE = "GREENMON",
|
||||
CHANSEY = "PINKMON",
|
||||
CHARIZARD = "REDMON",
|
||||
CHARMANDER = "REDMON",
|
||||
CHARMELEON = "REDMON",
|
||||
CLEFABLE = "PINKMON",
|
||||
CLEFAIRY = "PINKMON",
|
||||
CLOYSTER = "GRAYMON",
|
||||
CUBONE = "GRAYMON",
|
||||
DEWGONG = "BLUEMON",
|
||||
DIGLETT = "BROWNMON",
|
||||
DITTO = "GRAYMON",
|
||||
DODRIO = "BROWNMON",
|
||||
DODUO = "BROWNMON",
|
||||
DRAGONAIR = "BLUEMON",
|
||||
DRAGONITE = "BROWNMON",
|
||||
DRATINI = "GRAYMON",
|
||||
DROWZEE = "YELLOWMON",
|
||||
DUGTRIO = "BROWNMON",
|
||||
EEVEE = "GRAYMON",
|
||||
EKANS = "PURPLEMON",
|
||||
ELECTABUZZ = "YELLOWMON",
|
||||
ELECTRODE = "YELLOWMON",
|
||||
EXEGGCUTE = "PINKMON",
|
||||
EXEGGUTOR = "GREENMON",
|
||||
FARFETCHD = "BROWNMON",
|
||||
FEAROW = "BROWNMON",
|
||||
FLAREON = "REDMON",
|
||||
GASTLY = "PURPLEMON",
|
||||
GENGAR = "PURPLEMON",
|
||||
GEODUDE = "GRAYMON",
|
||||
GLOOM = "REDMON",
|
||||
GOLBAT = "BLUEMON",
|
||||
GOLDEEN = "REDMON",
|
||||
GOLDUCK = "CYANMON",
|
||||
GOLEM = "GRAYMON",
|
||||
GRAVELER = "GRAYMON",
|
||||
GRIMER = "PURPLEMON",
|
||||
GROWLITHE = "BROWNMON",
|
||||
GYARADOS = "BLUEMON",
|
||||
HAUNTER = "PURPLEMON",
|
||||
HITMONCHAN = "BROWNMON",
|
||||
HITMONLEE = "BROWNMON",
|
||||
HORSEA = "CYANMON",
|
||||
HYPNO = "YELLOWMON",
|
||||
IVYSAUR = "GREENMON",
|
||||
JIGGLYPUFF = "PINKMON",
|
||||
JOLTEON = "YELLOWMON",
|
||||
JYNX = "MEWMON",
|
||||
KABUTO = "BROWNMON",
|
||||
KABUTOPS = "BROWNMON",
|
||||
KADABRA = "YELLOWMON",
|
||||
KAKUNA = "YELLOWMON",
|
||||
KANGASKHAN = "BROWNMON",
|
||||
KINGLER = "REDMON",
|
||||
KOFFING = "PURPLEMON",
|
||||
KRABBY = "REDMON",
|
||||
LAPRAS = "CYANMON",
|
||||
LICKITUNG = "PINKMON",
|
||||
MACHAMP = "GRAYMON",
|
||||
MACHOKE = "GRAYMON",
|
||||
MACHOP = "GRAYMON",
|
||||
MAGIKARP = "REDMON",
|
||||
MAGMAR = "REDMON",
|
||||
MAGNEMITE = "GRAYMON",
|
||||
MAGNETON = "GRAYMON",
|
||||
MANKEY = "BROWNMON",
|
||||
MAROWAK = "GRAYMON",
|
||||
MEOWTH = "YELLOWMON",
|
||||
METAPOD = "GREENMON",
|
||||
MEW = "MEWMON",
|
||||
MEWTWO = "MEWMON",
|
||||
MOLTRES = "REDMON",
|
||||
MR_MIME = "PINKMON",
|
||||
MUK = "PURPLEMON",
|
||||
NIDOKING = "PURPLEMON",
|
||||
NIDOQUEEN = "BLUEMON",
|
||||
NIDORAN_F = "BLUEMON",
|
||||
NIDORAN_M = "PURPLEMON",
|
||||
NIDORINA = "BLUEMON",
|
||||
NIDORINO = "PURPLEMON",
|
||||
NINETALES = "YELLOWMON",
|
||||
ODDISH = "GREENMON",
|
||||
OMANYTE = "BLUEMON",
|
||||
OMASTAR = "BLUEMON",
|
||||
ONIX = "GRAYMON",
|
||||
PARAS = "REDMON",
|
||||
PARASECT = "REDMON",
|
||||
PERSIAN = "YELLOWMON",
|
||||
PIDGEOT = "BROWNMON",
|
||||
PIDGEOTTO = "BROWNMON",
|
||||
PIDGEY = "BROWNMON",
|
||||
PIKACHU = "YELLOWMON",
|
||||
PINSIR = "BROWNMON",
|
||||
POLIWAG = "BLUEMON",
|
||||
POLIWHIRL = "BLUEMON",
|
||||
POLIWRATH = "BLUEMON",
|
||||
PONYTA = "REDMON",
|
||||
PORYGON = "GRAYMON",
|
||||
PRIMEAPE = "BROWNMON",
|
||||
PSYDUCK = "YELLOWMON",
|
||||
RAICHU = "YELLOWMON",
|
||||
RAPIDASH = "REDMON",
|
||||
RATICATE = "GRAYMON",
|
||||
RATTATA = "GRAYMON",
|
||||
RHYDON = "GRAYMON",
|
||||
RHYHORN = "GRAYMON",
|
||||
SANDSHREW = "BROWNMON",
|
||||
SANDSLASH = "BROWNMON",
|
||||
SCYTHER = "GREENMON",
|
||||
SEADRA = "CYANMON",
|
||||
SEAKING = "REDMON",
|
||||
SEEL = "BLUEMON",
|
||||
SHELLDER = "GRAYMON",
|
||||
SLOWBRO = "PINKMON",
|
||||
SLOWPOKE = "PINKMON",
|
||||
SNORLAX = "PINKMON",
|
||||
SPEAROW = "BROWNMON",
|
||||
SQUIRTLE = "CYANMON",
|
||||
STARMIE = "GRAYMON",
|
||||
STARYU = "REDMON",
|
||||
TANGELA = "BLUEMON",
|
||||
TAUROS = "GRAYMON",
|
||||
TENTACOOL = "CYANMON",
|
||||
TENTACRUEL = "CYANMON",
|
||||
VAPOREON = "CYANMON",
|
||||
VENOMOTH = "PURPLEMON",
|
||||
VENONAT = "PURPLEMON",
|
||||
VENUSAUR = "GREENMON",
|
||||
VICTREEBEL = "GREENMON",
|
||||
VILEPLUME = "REDMON",
|
||||
VOLTORB = "YELLOWMON",
|
||||
VULPIX = "REDMON",
|
||||
WARTORTLE = "CYANMON",
|
||||
WEEDLE = "YELLOWMON",
|
||||
WEEPINBELL = "GREENMON",
|
||||
WEEZING = "PURPLEMON",
|
||||
WIGGLYTUFF = "PINKMON",
|
||||
ZAPDOS = "YELLOWMON",
|
||||
ZUBAT = "BLUEMON",
|
||||
},
|
||||
source = "pokered-gbc data/super_palettes.asm + data/mon_palettes.asm + color/**",
|
||||
world = {
|
||||
|
||||
@@ -0,0 +1,686 @@
|
||||
-- Generated by tools/build_data.py. DO NOT EDIT.
|
||||
-- Pokemon Yellow palettes (COLORS=OG YELLOW): SuperPalettes +
|
||||
-- CGBBasePalettes (authentic GBC look) as 4 8-bit RGB colors
|
||||
-- per palette (color 0 first), plus MonsterPalettes.
|
||||
return {
|
||||
cgbBase = {
|
||||
["0F"] = {
|
||||
{ 255, 255, 255 },
|
||||
{ 107, 8, 255 },
|
||||
{ 0, 74, 255 },
|
||||
{ 8, 8, 8 },
|
||||
},
|
||||
BADGE = {
|
||||
{ 255, 255, 255 },
|
||||
{ 189, 66, 0 },
|
||||
{ 140, 115, 90 },
|
||||
{ 25, 25, 25 },
|
||||
},
|
||||
BLACK = {
|
||||
{ 255, 255, 255 },
|
||||
{ 25, 25, 25 },
|
||||
{ 25, 25, 25 },
|
||||
{ 25, 25, 25 },
|
||||
},
|
||||
BLUEMON = {
|
||||
{ 255, 255, 255 },
|
||||
{ 132, 148, 255 },
|
||||
{ 0, 8, 206 },
|
||||
{ 25, 25, 25 },
|
||||
},
|
||||
BROWNMON = {
|
||||
{ 255, 255, 255 },
|
||||
{ 239, 148, 82 },
|
||||
{ 140, 74, 41 },
|
||||
{ 25, 25, 25 },
|
||||
},
|
||||
CAVE = {
|
||||
{ 255, 255, 255 },
|
||||
{ 189, 66, 0 },
|
||||
{ 140, 115, 90 },
|
||||
{ 25, 25, 25 },
|
||||
},
|
||||
CELADON = {
|
||||
{ 255, 255, 255 },
|
||||
{ 41, 255, 41 },
|
||||
{ 90, 189, 255 },
|
||||
{ 25, 25, 25 },
|
||||
},
|
||||
CERULEAN = {
|
||||
{ 255, 255, 255 },
|
||||
{ 41, 66, 255 },
|
||||
{ 90, 189, 255 },
|
||||
{ 25, 25, 25 },
|
||||
},
|
||||
CINNABAR = {
|
||||
{ 255, 255, 255 },
|
||||
{ 255, 66, 66 },
|
||||
{ 90, 189, 255 },
|
||||
{ 25, 25, 25 },
|
||||
},
|
||||
CYANMON = {
|
||||
{ 255, 255, 255 },
|
||||
{ 132, 214, 255 },
|
||||
{ 0, 140, 255 },
|
||||
{ 25, 25, 25 },
|
||||
},
|
||||
FUCHSIA = {
|
||||
{ 255, 255, 255 },
|
||||
{ 255, 123, 123 },
|
||||
{ 90, 189, 255 },
|
||||
{ 25, 25, 25 },
|
||||
},
|
||||
GAMEFREAK = {
|
||||
{ 255, 255, 255 },
|
||||
{ 255, 156, 0 },
|
||||
{ 156, 156, 0 },
|
||||
{ 25, 25, 25 },
|
||||
},
|
||||
GRAYMON = {
|
||||
{ 255, 255, 255 },
|
||||
{ 165, 189, 82 },
|
||||
{ 90, 90, 41 },
|
||||
{ 25, 25, 25 },
|
||||
},
|
||||
GREENBAR = {
|
||||
{ 255, 255, 255 },
|
||||
{ 255, 255, 0 },
|
||||
{ 0, 255, 0 },
|
||||
{ 25, 25, 25 },
|
||||
},
|
||||
GREENMON = {
|
||||
{ 255, 255, 255 },
|
||||
{ 140, 255, 90 },
|
||||
{ 8, 181, 49 },
|
||||
{ 25, 25, 25 },
|
||||
},
|
||||
INDIGO = {
|
||||
{ 255, 255, 255 },
|
||||
{ 90, 66, 255 },
|
||||
{ 90, 189, 255 },
|
||||
{ 25, 25, 25 },
|
||||
},
|
||||
LAVENDER = {
|
||||
{ 255, 255, 255 },
|
||||
{ 206, 33, 255 },
|
||||
{ 90, 189, 255 },
|
||||
{ 25, 25, 25 },
|
||||
},
|
||||
LOGO1 = {
|
||||
{ 255, 255, 255 },
|
||||
{ 255, 255, 0 },
|
||||
{ 255, 0, 0 },
|
||||
{ 255, 0, 0 },
|
||||
},
|
||||
LOGO2 = {
|
||||
{ 255, 255, 255 },
|
||||
{ 255, 255, 0 },
|
||||
{ 58, 58, 206 },
|
||||
{ 0, 0, 140 },
|
||||
},
|
||||
MEWMON = {
|
||||
{ 255, 255, 255 },
|
||||
{ 255, 255, 0 },
|
||||
{ 255, 8, 8 },
|
||||
{ 25, 25, 25 },
|
||||
},
|
||||
PALLET = {
|
||||
{ 255, 255, 255 },
|
||||
{ 189, 140, 255 },
|
||||
{ 90, 189, 255 },
|
||||
{ 25, 25, 25 },
|
||||
},
|
||||
PEWTER = {
|
||||
{ 255, 255, 255 },
|
||||
{ 148, 148, 123 },
|
||||
{ 90, 189, 255 },
|
||||
{ 25, 25, 25 },
|
||||
},
|
||||
PIKACHUS_BEACH = {
|
||||
{ 255, 255, 255 },
|
||||
{ 255, 255, 0 },
|
||||
{ 90, 189, 255 },
|
||||
{ 25, 25, 25 },
|
||||
},
|
||||
PIKACHUS_BEACH_TITLE = {
|
||||
{ 255, 255, 255 },
|
||||
{ 74, 74, 74 },
|
||||
{ 255, 173, 0 },
|
||||
{ 25, 25, 25 },
|
||||
},
|
||||
PIKACHU_PORTRAIT = {
|
||||
{ 255, 255, 255 },
|
||||
{ 255, 148, 0 },
|
||||
{ 156, 58, 8 },
|
||||
{ 25, 25, 25 },
|
||||
},
|
||||
PINKMON = {
|
||||
{ 255, 255, 255 },
|
||||
{ 255, 123, 148 },
|
||||
{ 255, 0, 49 },
|
||||
{ 25, 25, 25 },
|
||||
},
|
||||
PURPLEMON = {
|
||||
{ 255, 255, 255 },
|
||||
{ 206, 123, 255 },
|
||||
{ 156, 0, 181 },
|
||||
{ 25, 25, 25 },
|
||||
},
|
||||
REDBAR = {
|
||||
{ 255, 255, 255 },
|
||||
{ 255, 255, 0 },
|
||||
{ 255, 0, 0 },
|
||||
{ 25, 25, 25 },
|
||||
},
|
||||
REDMON = {
|
||||
{ 255, 255, 255 },
|
||||
{ 255, 140, 0 },
|
||||
{ 255, 0, 0 },
|
||||
{ 25, 25, 25 },
|
||||
},
|
||||
ROUTE = {
|
||||
{ 255, 255, 255 },
|
||||
{ 132, 255, 33 },
|
||||
{ 90, 189, 255 },
|
||||
{ 25, 25, 25 },
|
||||
},
|
||||
SAFFRON = {
|
||||
{ 255, 255, 255 },
|
||||
{ 255, 255, 0 },
|
||||
{ 90, 189, 255 },
|
||||
{ 25, 25, 25 },
|
||||
},
|
||||
SLOTS1 = {
|
||||
{ 255, 255, 255 },
|
||||
{ 206, 8, 255 },
|
||||
{ 255, 0, 0 },
|
||||
{ 25, 25, 25 },
|
||||
},
|
||||
SLOTS2 = {
|
||||
{ 255, 255, 255 },
|
||||
{ 255, 33, 156 },
|
||||
{ 255, 255, 0 },
|
||||
{ 25, 25, 25 },
|
||||
},
|
||||
SLOTS3 = {
|
||||
{ 255, 255, 255 },
|
||||
{ 66, 255, 0 },
|
||||
{ 255, 255, 0 },
|
||||
{ 25, 25, 25 },
|
||||
},
|
||||
SLOTS4 = {
|
||||
{ 255, 255, 255 },
|
||||
{ 0, 255, 255 },
|
||||
{ 255, 255, 0 },
|
||||
{ 25, 25, 25 },
|
||||
},
|
||||
TOWNMAP = {
|
||||
{ 255, 255, 255 },
|
||||
{ 0, 173, 255 },
|
||||
{ 82, 230, 0 },
|
||||
{ 8, 8, 8 },
|
||||
},
|
||||
VERMILION = {
|
||||
{ 255, 255, 255 },
|
||||
{ 255, 156, 0 },
|
||||
{ 90, 189, 255 },
|
||||
{ 25, 25, 25 },
|
||||
},
|
||||
VIRIDIAN = {
|
||||
{ 255, 255, 255 },
|
||||
{ 156, 255, 0 },
|
||||
{ 90, 189, 255 },
|
||||
{ 25, 25, 25 },
|
||||
},
|
||||
YELLOWBAR = {
|
||||
{ 255, 255, 255 },
|
||||
{ 255, 255, 0 },
|
||||
{ 255, 148, 0 },
|
||||
{ 25, 25, 25 },
|
||||
},
|
||||
YELLOWMON = {
|
||||
{ 255, 255, 255 },
|
||||
{ 255, 255, 0 },
|
||||
{ 230, 115, 0 },
|
||||
{ 25, 25, 25 },
|
||||
},
|
||||
},
|
||||
order = {
|
||||
"ROUTE",
|
||||
"PALLET",
|
||||
"VIRIDIAN",
|
||||
"PEWTER",
|
||||
"CERULEAN",
|
||||
"LAVENDER",
|
||||
"VERMILION",
|
||||
"CELADON",
|
||||
"FUCHSIA",
|
||||
"CINNABAR",
|
||||
"INDIGO",
|
||||
"SAFFRON",
|
||||
"TOWNMAP",
|
||||
"LOGO1",
|
||||
"LOGO2",
|
||||
"0F",
|
||||
"MEWMON",
|
||||
"BLUEMON",
|
||||
"REDMON",
|
||||
"CYANMON",
|
||||
"PURPLEMON",
|
||||
"BROWNMON",
|
||||
"GREENMON",
|
||||
"PINKMON",
|
||||
"YELLOWMON",
|
||||
"GRAYMON",
|
||||
"SLOTS1",
|
||||
"SLOTS2",
|
||||
"SLOTS3",
|
||||
"SLOTS4",
|
||||
"BLACK",
|
||||
"GREENBAR",
|
||||
"YELLOWBAR",
|
||||
"REDBAR",
|
||||
"BADGE",
|
||||
"CAVE",
|
||||
"GAMEFREAK",
|
||||
"PIKACHUS_BEACH",
|
||||
"PIKACHU_PORTRAIT",
|
||||
"PIKACHUS_BEACH_TITLE",
|
||||
},
|
||||
palettes = {
|
||||
["0F"] = {
|
||||
{ 255, 255, 247 },
|
||||
{ 197, 165, 247 },
|
||||
{ 90, 165, 247 },
|
||||
{ 25, 16, 16 },
|
||||
},
|
||||
BADGE = {
|
||||
{ 255, 255, 247 },
|
||||
{ 165, 123, 90 },
|
||||
{ 181, 173, 165 },
|
||||
{ 49, 49, 49 },
|
||||
},
|
||||
BLACK = {
|
||||
{ 255, 255, 247 },
|
||||
{ 49, 49, 49 },
|
||||
{ 49, 49, 49 },
|
||||
{ 49, 49, 49 },
|
||||
},
|
||||
BLUEMON = {
|
||||
{ 255, 255, 247 },
|
||||
{ 173, 181, 255 },
|
||||
{ 74, 82, 165 },
|
||||
{ 49, 49, 49 },
|
||||
},
|
||||
BROWNMON = {
|
||||
{ 255, 255, 247 },
|
||||
{ 214, 189, 148 },
|
||||
{ 148, 115, 82 },
|
||||
{ 49, 49, 49 },
|
||||
},
|
||||
CAVE = {
|
||||
{ 255, 255, 247 },
|
||||
{ 165, 123, 90 },
|
||||
{ 181, 173, 165 },
|
||||
{ 49, 49, 49 },
|
||||
},
|
||||
CELADON = {
|
||||
{ 255, 255, 247 },
|
||||
{ 181, 255, 181 },
|
||||
{ 189, 222, 255 },
|
||||
{ 49, 49, 49 },
|
||||
},
|
||||
CERULEAN = {
|
||||
{ 255, 255, 247 },
|
||||
{ 181, 189, 255 },
|
||||
{ 189, 222, 255 },
|
||||
{ 49, 49, 49 },
|
||||
},
|
||||
CINNABAR = {
|
||||
{ 255, 255, 247 },
|
||||
{ 255, 123, 115 },
|
||||
{ 189, 222, 255 },
|
||||
{ 49, 49, 49 },
|
||||
},
|
||||
CYANMON = {
|
||||
{ 255, 255, 247 },
|
||||
{ 214, 230, 255 },
|
||||
{ 58, 197, 230 },
|
||||
{ 49, 49, 49 },
|
||||
},
|
||||
FUCHSIA = {
|
||||
{ 255, 255, 247 },
|
||||
{ 255, 214, 214 },
|
||||
{ 189, 222, 255 },
|
||||
{ 49, 49, 49 },
|
||||
},
|
||||
GAMEFREAK = {
|
||||
{ 255, 255, 247 },
|
||||
{ 230, 197, 115 },
|
||||
{ 165, 165, 90 },
|
||||
{ 49, 49, 49 },
|
||||
},
|
||||
GRAYMON = {
|
||||
{ 255, 255, 247 },
|
||||
{ 206, 206, 148 },
|
||||
{ 132, 132, 115 },
|
||||
{ 49, 49, 49 },
|
||||
},
|
||||
GREENBAR = {
|
||||
{ 255, 255, 247 },
|
||||
{ 255, 255, 156 },
|
||||
{ 0, 173, 0 },
|
||||
{ 49, 49, 49 },
|
||||
},
|
||||
GREENMON = {
|
||||
{ 255, 255, 247 },
|
||||
{ 197, 230, 148 },
|
||||
{ 107, 173, 123 },
|
||||
{ 49, 49, 49 },
|
||||
},
|
||||
INDIGO = {
|
||||
{ 255, 255, 247 },
|
||||
{ 140, 140, 206 },
|
||||
{ 189, 222, 255 },
|
||||
{ 49, 49, 49 },
|
||||
},
|
||||
LAVENDER = {
|
||||
{ 255, 255, 247 },
|
||||
{ 222, 189, 239 },
|
||||
{ 189, 222, 255 },
|
||||
{ 49, 49, 49 },
|
||||
},
|
||||
LOGO1 = {
|
||||
{ 255, 255, 247 },
|
||||
{ 247, 247, 140 },
|
||||
{ 173, 0, 33 },
|
||||
{ 173, 0, 33 },
|
||||
},
|
||||
LOGO2 = {
|
||||
{ 255, 255, 247 },
|
||||
{ 247, 247, 140 },
|
||||
{ 148, 148, 197 },
|
||||
{ 58, 58, 132 },
|
||||
},
|
||||
MEWMON = {
|
||||
{ 255, 255, 247 },
|
||||
{ 255, 247, 181 },
|
||||
{ 222, 132, 132 },
|
||||
{ 49, 49, 49 },
|
||||
},
|
||||
PALLET = {
|
||||
{ 255, 255, 247 },
|
||||
{ 230, 222, 255 },
|
||||
{ 189, 222, 255 },
|
||||
{ 49, 49, 49 },
|
||||
},
|
||||
PEWTER = {
|
||||
{ 255, 255, 247 },
|
||||
{ 189, 189, 181 },
|
||||
{ 189, 222, 255 },
|
||||
{ 49, 49, 49 },
|
||||
},
|
||||
PIKACHUS_BEACH = {
|
||||
{ 255, 255, 247 },
|
||||
{ 255, 247, 181 },
|
||||
{ 189, 222, 255 },
|
||||
{ 49, 49, 49 },
|
||||
},
|
||||
PIKACHUS_BEACH_TITLE = {
|
||||
{ 255, 255, 247 },
|
||||
{ 132, 132, 132 },
|
||||
{ 255, 206, 74 },
|
||||
{ 49, 49, 49 },
|
||||
},
|
||||
PIKACHU_PORTRAIT = {
|
||||
{ 255, 255, 247 },
|
||||
{ 230, 189, 74 },
|
||||
{ 148, 115, 82 },
|
||||
{ 49, 49, 49 },
|
||||
},
|
||||
PINKMON = {
|
||||
{ 255, 255, 247 },
|
||||
{ 255, 197, 214 },
|
||||
{ 255, 148, 173 },
|
||||
{ 49, 49, 49 },
|
||||
},
|
||||
PURPLEMON = {
|
||||
{ 255, 255, 247 },
|
||||
{ 222, 181, 247 },
|
||||
{ 181, 123, 189 },
|
||||
{ 49, 49, 49 },
|
||||
},
|
||||
REDBAR = {
|
||||
{ 255, 255, 247 },
|
||||
{ 255, 255, 156 },
|
||||
{ 214, 74, 49 },
|
||||
{ 49, 49, 49 },
|
||||
},
|
||||
REDMON = {
|
||||
{ 255, 255, 247 },
|
||||
{ 255, 197, 90 },
|
||||
{ 214, 74, 49 },
|
||||
{ 49, 49, 49 },
|
||||
},
|
||||
ROUTE = {
|
||||
{ 255, 255, 247 },
|
||||
{ 189, 214, 156 },
|
||||
{ 189, 222, 255 },
|
||||
{ 49, 49, 49 },
|
||||
},
|
||||
SAFFRON = {
|
||||
{ 255, 255, 247 },
|
||||
{ 255, 255, 156 },
|
||||
{ 189, 222, 255 },
|
||||
{ 49, 49, 49 },
|
||||
},
|
||||
SLOTS1 = {
|
||||
{ 255, 255, 247 },
|
||||
{ 222, 181, 247 },
|
||||
{ 214, 74, 49 },
|
||||
{ 49, 49, 49 },
|
||||
},
|
||||
SLOTS2 = {
|
||||
{ 255, 255, 247 },
|
||||
{ 255, 189, 214 },
|
||||
{ 239, 239, 66 },
|
||||
{ 49, 49, 49 },
|
||||
},
|
||||
SLOTS3 = {
|
||||
{ 255, 255, 247 },
|
||||
{ 189, 255, 165 },
|
||||
{ 239, 239, 66 },
|
||||
{ 49, 49, 49 },
|
||||
},
|
||||
SLOTS4 = {
|
||||
{ 255, 255, 247 },
|
||||
{ 189, 239, 255 },
|
||||
{ 239, 239, 66 },
|
||||
{ 49, 49, 49 },
|
||||
},
|
||||
TOWNMAP = {
|
||||
{ 255, 255, 247 },
|
||||
{ 165, 214, 255 },
|
||||
{ 140, 189, 82 },
|
||||
{ 25, 16, 16 },
|
||||
},
|
||||
VERMILION = {
|
||||
{ 255, 255, 247 },
|
||||
{ 255, 206, 132 },
|
||||
{ 189, 222, 255 },
|
||||
{ 49, 49, 49 },
|
||||
},
|
||||
VIRIDIAN = {
|
||||
{ 255, 255, 247 },
|
||||
{ 214, 255, 173 },
|
||||
{ 189, 222, 255 },
|
||||
{ 49, 49, 49 },
|
||||
},
|
||||
YELLOWBAR = {
|
||||
{ 255, 255, 247 },
|
||||
{ 255, 255, 156 },
|
||||
{ 230, 189, 74 },
|
||||
{ 49, 49, 49 },
|
||||
},
|
||||
YELLOWMON = {
|
||||
{ 255, 255, 247 },
|
||||
{ 255, 255, 156 },
|
||||
{ 230, 189, 74 },
|
||||
{ 49, 49, 49 },
|
||||
},
|
||||
},
|
||||
pokemon = {
|
||||
ABRA = "YELLOWMON",
|
||||
AERODACTYL = "GRAYMON",
|
||||
ALAKAZAM = "YELLOWMON",
|
||||
ARBOK = "PURPLEMON",
|
||||
ARCANINE = "REDMON",
|
||||
ARTICUNO = "BLUEMON",
|
||||
BEEDRILL = "YELLOWMON",
|
||||
BELLSPROUT = "GREENMON",
|
||||
BLASTOISE = "CYANMON",
|
||||
BULBASAUR = "GREENMON",
|
||||
BUTTERFREE = "CYANMON",
|
||||
CATERPIE = "GREENMON",
|
||||
CHANSEY = "PINKMON",
|
||||
CHARIZARD = "REDMON",
|
||||
CHARMANDER = "REDMON",
|
||||
CHARMELEON = "REDMON",
|
||||
CLEFABLE = "PINKMON",
|
||||
CLEFAIRY = "PINKMON",
|
||||
CLOYSTER = "GRAYMON",
|
||||
CUBONE = "GRAYMON",
|
||||
DEWGONG = "BLUEMON",
|
||||
DIGLETT = "BROWNMON",
|
||||
DITTO = "GRAYMON",
|
||||
DODRIO = "BROWNMON",
|
||||
DODUO = "BROWNMON",
|
||||
DRAGONAIR = "BLUEMON",
|
||||
DRAGONITE = "BROWNMON",
|
||||
DRATINI = "GRAYMON",
|
||||
DROWZEE = "YELLOWMON",
|
||||
DUGTRIO = "BROWNMON",
|
||||
EEVEE = "GRAYMON",
|
||||
EKANS = "PURPLEMON",
|
||||
ELECTABUZZ = "YELLOWMON",
|
||||
ELECTRODE = "YELLOWMON",
|
||||
EXEGGCUTE = "PINKMON",
|
||||
EXEGGUTOR = "GREENMON",
|
||||
FARFETCHD = "BROWNMON",
|
||||
FEAROW = "BROWNMON",
|
||||
FLAREON = "REDMON",
|
||||
GASTLY = "PURPLEMON",
|
||||
GENGAR = "PURPLEMON",
|
||||
GEODUDE = "GRAYMON",
|
||||
GLOOM = "REDMON",
|
||||
GOLBAT = "BLUEMON",
|
||||
GOLDEEN = "REDMON",
|
||||
GOLDUCK = "CYANMON",
|
||||
GOLEM = "GRAYMON",
|
||||
GRAVELER = "GRAYMON",
|
||||
GRIMER = "PURPLEMON",
|
||||
GROWLITHE = "BROWNMON",
|
||||
GYARADOS = "BLUEMON",
|
||||
HAUNTER = "PURPLEMON",
|
||||
HITMONCHAN = "BROWNMON",
|
||||
HITMONLEE = "BROWNMON",
|
||||
HORSEA = "CYANMON",
|
||||
HYPNO = "YELLOWMON",
|
||||
IVYSAUR = "GREENMON",
|
||||
JIGGLYPUFF = "PINKMON",
|
||||
JOLTEON = "YELLOWMON",
|
||||
JYNX = "MEWMON",
|
||||
KABUTO = "BROWNMON",
|
||||
KABUTOPS = "BROWNMON",
|
||||
KADABRA = "YELLOWMON",
|
||||
KAKUNA = "YELLOWMON",
|
||||
KANGASKHAN = "BROWNMON",
|
||||
KINGLER = "REDMON",
|
||||
KOFFING = "PURPLEMON",
|
||||
KRABBY = "REDMON",
|
||||
LAPRAS = "CYANMON",
|
||||
LICKITUNG = "PINKMON",
|
||||
MACHAMP = "GRAYMON",
|
||||
MACHOKE = "GRAYMON",
|
||||
MACHOP = "GRAYMON",
|
||||
MAGIKARP = "REDMON",
|
||||
MAGMAR = "REDMON",
|
||||
MAGNEMITE = "GRAYMON",
|
||||
MAGNETON = "GRAYMON",
|
||||
MANKEY = "BROWNMON",
|
||||
MAROWAK = "GRAYMON",
|
||||
MEOWTH = "YELLOWMON",
|
||||
METAPOD = "GREENMON",
|
||||
MEW = "MEWMON",
|
||||
MEWTWO = "MEWMON",
|
||||
MOLTRES = "REDMON",
|
||||
MR_MIME = "PINKMON",
|
||||
MUK = "PURPLEMON",
|
||||
NIDOKING = "PURPLEMON",
|
||||
NIDOQUEEN = "BLUEMON",
|
||||
NIDORAN_F = "BLUEMON",
|
||||
NIDORAN_M = "PURPLEMON",
|
||||
NIDORINA = "BLUEMON",
|
||||
NIDORINO = "PURPLEMON",
|
||||
NINETALES = "YELLOWMON",
|
||||
ODDISH = "GREENMON",
|
||||
OMANYTE = "BLUEMON",
|
||||
OMASTAR = "BLUEMON",
|
||||
ONIX = "GRAYMON",
|
||||
PARAS = "REDMON",
|
||||
PARASECT = "REDMON",
|
||||
PERSIAN = "YELLOWMON",
|
||||
PIDGEOT = "BROWNMON",
|
||||
PIDGEOTTO = "BROWNMON",
|
||||
PIDGEY = "BROWNMON",
|
||||
PIKACHU = "YELLOWMON",
|
||||
PINSIR = "BROWNMON",
|
||||
POLIWAG = "BLUEMON",
|
||||
POLIWHIRL = "BLUEMON",
|
||||
POLIWRATH = "BLUEMON",
|
||||
PONYTA = "REDMON",
|
||||
PORYGON = "GRAYMON",
|
||||
PRIMEAPE = "BROWNMON",
|
||||
PSYDUCK = "YELLOWMON",
|
||||
RAICHU = "YELLOWMON",
|
||||
RAPIDASH = "REDMON",
|
||||
RATICATE = "GRAYMON",
|
||||
RATTATA = "GRAYMON",
|
||||
RHYDON = "GRAYMON",
|
||||
RHYHORN = "GRAYMON",
|
||||
SANDSHREW = "BROWNMON",
|
||||
SANDSLASH = "BROWNMON",
|
||||
SCYTHER = "GREENMON",
|
||||
SEADRA = "CYANMON",
|
||||
SEAKING = "REDMON",
|
||||
SEEL = "BLUEMON",
|
||||
SHELLDER = "GRAYMON",
|
||||
SLOWBRO = "PINKMON",
|
||||
SLOWPOKE = "PINKMON",
|
||||
SNORLAX = "PINKMON",
|
||||
SPEAROW = "BROWNMON",
|
||||
SQUIRTLE = "CYANMON",
|
||||
STARMIE = "GRAYMON",
|
||||
STARYU = "REDMON",
|
||||
TANGELA = "BLUEMON",
|
||||
TAUROS = "GRAYMON",
|
||||
TENTACOOL = "CYANMON",
|
||||
TENTACRUEL = "CYANMON",
|
||||
VAPOREON = "CYANMON",
|
||||
VENOMOTH = "PURPLEMON",
|
||||
VENONAT = "PURPLEMON",
|
||||
VENUSAUR = "GREENMON",
|
||||
VICTREEBEL = "GREENMON",
|
||||
VILEPLUME = "REDMON",
|
||||
VOLTORB = "YELLOWMON",
|
||||
VULPIX = "REDMON",
|
||||
WARTORTLE = "CYANMON",
|
||||
WEEDLE = "YELLOWMON",
|
||||
WEEPINBELL = "GREENMON",
|
||||
WEEZING = "PURPLEMON",
|
||||
WIGGLYTUFF = "PINKMON",
|
||||
ZAPDOS = "YELLOWMON",
|
||||
ZUBAT = "BLUEMON",
|
||||
},
|
||||
source = "pokeyellow data/sgb/sgb_palettes.asm + data/pokemon/palettes.asm",
|
||||
}
|
||||
@@ -11,8 +11,70 @@
|
||||
-- equivalent, kept for save compatibility: rows 1-4 also self-heal
|
||||
-- older saves (flag set before the port hid the ball) by hiding the
|
||||
-- leftover ball on the next interaction.
|
||||
--
|
||||
-- The room's two readables (data/events/hidden_events.asm,
|
||||
-- hidden_events_for CELADON_MANSION_ROOF_HOUSE):
|
||||
-- hidden_text_predef 3, 0 / 4, 0 PrintBlackboardLinkCableText, LinkCableHelp
|
||||
-- hidden_text_predef 3, 4 PrintNotebookText, TMNotebook
|
||||
-- tools/extract/field.py only parses `hidden_event` rows, so no
|
||||
-- hidden_text_predef row reaches data/generated/field.lua and both tiles
|
||||
-- were dead A presses (#391). Same hook shape as the bedroom SNES in
|
||||
-- data/scripts/flavor/reds_house_2f.lua (#135). hidden_text_predef puts
|
||||
-- the predef id in the facing byte, so neither tile gates on facing.
|
||||
|
||||
local Menu = require("src.ui.Menu")
|
||||
local TextBox = require("src.render.TextBox")
|
||||
|
||||
-- TMNotebookText (data/text/text_2.asm) has no leading underscore, so the
|
||||
-- extractor never collects it and the pamphlet's text is inlined.
|
||||
local TM_NOTEBOOK_TEXT = "It's a pamphlet\non TMs.\f...\f"
|
||||
.. "There are 50 TMs\nin all.\f"
|
||||
.. "There are also 5\nHMs that can be\vused repeatedly.\f"
|
||||
.. "SILPH CO."
|
||||
|
||||
-- LinkCableHelp (engine/events/hidden_events/school_blackboard.asm):
|
||||
-- HowToLinkText's four headings in the 15x10 box at the top left; picking
|
||||
-- a heading prints its LinkCableInfoText and returns to the menu, B or
|
||||
-- STOP READING closes.
|
||||
local LINK_HEADINGS = { "HOW TO LINK", "COLOSSEUM", "TRADE CENTER" }
|
||||
|
||||
local function linkCableHelp(game)
|
||||
local text = game.data.text or {}
|
||||
local items, showMenu, askHeading
|
||||
function showMenu()
|
||||
game.stack:push(Menu.new(game, items,
|
||||
{ tx = 0, ty = 0, tw = 15, th = 10, rowStep = 1 }))
|
||||
end
|
||||
function askHeading()
|
||||
game.stack:push(TextBox.new(game,
|
||||
text._LinkCableHelpText2 or "Which heading do\nyou want to read?",
|
||||
showMenu))
|
||||
end
|
||||
items = {}
|
||||
for i, label in ipairs(LINK_HEADINGS) do
|
||||
items[i] = { label = label, onSelect = function()
|
||||
game.stack:push(TextBox.new(game,
|
||||
text["_LinkCableInfoText" .. i] or label, askHeading))
|
||||
end }
|
||||
end
|
||||
items[#items + 1] = { label = "STOP READING" }
|
||||
game.stack:push(TextBox.new(game,
|
||||
text._LinkCableHelpText1 or "TRAINER TIPS\fUsing a Game Link\nCable",
|
||||
askHeading))
|
||||
end
|
||||
|
||||
return {
|
||||
onInteract = function(game, ow, fx, fy)
|
||||
if fy == 0 and (fx == 3 or fx == 4) then
|
||||
linkCableHelp(game)
|
||||
return true
|
||||
end
|
||||
if fx == 3 and fy == 4 then
|
||||
game.stack:push(TextBox.new(game, TM_NOTEBOOK_TEXT))
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end,
|
||||
talk = {
|
||||
TEXT_CELADONMANSION_ROOF_HOUSE_EEVEE_POKEBALL = {
|
||||
{ "check_flag", "EVENT_GOT_EEVEE" }, -- 1
|
||||
@@ -22,11 +84,16 @@ return {
|
||||
{ "jump", 13 }, -- 4
|
||||
{ "give_pokemon", "EEVEE", 25 }, -- 5
|
||||
{ "jump_if_false", 12 }, -- 6 (party+box full)
|
||||
{ "play_sound", "Get_Item1" }, -- 7 (GotMonText jingle)
|
||||
{ "show_text", "_GotMonText", { RAM = "EEVEE" } }, -- 8
|
||||
{ "set_flag", "EVENT_GOT_EEVEE" }, -- 9
|
||||
-- flag + HideObject before the jingle and GotMonText: the nickname
|
||||
-- prompt inside give_pokemon and the text row both yield, and a
|
||||
-- script that dies there would leave the EEVEE taken with the ball
|
||||
-- still on the table and the gift claimable again (#426). The row
|
||||
-- count is unchanged, so the numeric jump targets still hold.
|
||||
{ "set_flag", "EVENT_GOT_EEVEE" }, -- 7
|
||||
{ "hide_object", "CELADON_MANSION_ROOF_HOUSE",
|
||||
"CELADONMANSION_ROOF_HOUSE_EEVEE_POKEBALL" }, -- 10
|
||||
"CELADONMANSION_ROOF_HOUSE_EEVEE_POKEBALL" }, -- 8
|
||||
{ "text_sound", "Get_Item1" }, -- 9 (GotMonText jingle)
|
||||
{ "show_text", "_GotMonText", { RAM = "EEVEE" } }, -- 10
|
||||
{ "jump", 13 }, -- 11
|
||||
{ "show_text", "_BoxIsFullText" }, -- 12
|
||||
},
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
-- BikeShop (BIKE_SHOP) flavor dialogue
|
||||
-- Source: pokered/scripts/BikeShop.asm, pokered/text/BikeShop.asm
|
||||
--
|
||||
-- TEXT_BIKESHOP_CLERK is skipped: it drives the actual voucher-for-bicycle
|
||||
-- exchange (YesNoChoice purchase menu, GiveItem, RemoveItemByID, SetEvent
|
||||
-- EVENT_GOT_BICYCLE). That is a significant standalone feature outside the
|
||||
-- scope of these two flavor NPCs and is left unported here.
|
||||
-- TEXT_BIKESHOP_CLERK lives in data/scripts/story2.lua (M.BIKE_SHOP): the
|
||||
-- voucher exchange and the BICYCLE/CANCEL price window need more than
|
||||
-- command rows (#568).
|
||||
|
||||
return {
|
||||
BIKE_SHOP = {
|
||||
@@ -20,12 +19,15 @@ return {
|
||||
-- CheckEvent EVENT_GOT_BICYCLE ; jr nz, .gotBike
|
||||
-- before the player owns a bike -> TheseBikesAreExpensiveText
|
||||
-- after the player owns a bike -> CoolBikeText
|
||||
-- The check reads the bag, not the event: the port hands out the
|
||||
-- BICYCLE itself (a key item, so it cannot be tossed), and that also
|
||||
-- reads right on saves written before the clerk set the event (#567).
|
||||
TEXT_BIKESHOP_YOUNGSTER = {
|
||||
{ "face_player" }, -- 1
|
||||
{ "check_flag", "EVENT_GOT_BICYCLE" }, -- 2
|
||||
{ "jump_if_true", 5 }, -- 3
|
||||
{ "check_item", "BICYCLE" }, -- 2
|
||||
{ "jump_if_true", 6 }, -- 3
|
||||
{ "show_text", "_BikeShopYoungsterTheseBikesAreExpensiveText" }, -- 4
|
||||
{ "jump", 6 }, -- 5
|
||||
{ "jump", "end" }, -- 5
|
||||
{ "show_text", "_BikeShopYoungsterCoolBikeText" }, -- 6
|
||||
},
|
||||
},
|
||||
|
||||
@@ -1,33 +1,88 @@
|
||||
-- Celadon Mansion 3F Game Designer (pokered/scripts/CeladonMansion3F.asm
|
||||
-- CeladonMansion3FGameDesignerText): text_asm counts set bits in
|
||||
-- wPokedexOwned and compares against NUM_POKEMON - 1 (discounts Mew).
|
||||
-- If the player owns >= 150 species, shows the "completed" text
|
||||
-- (originally followed by DisplayDiploma, which has no equivalent UI
|
||||
-- in this port, so we just show the congratulatory text); otherwise
|
||||
-- shows the normal encouragement text.
|
||||
-- Celadon Mansion 3F, the GAME FREAK dev floor
|
||||
-- (pokered+pokeyellow/scripts/CeladonMansion3F.asm). Every dev's
|
||||
-- text_asm counts set bits in wPokedexOwned against NUM_POKEMON - 1
|
||||
-- (150, discounting Mew). The game designer shows the diploma
|
||||
-- (DisplayDiploma -> src/ui/Diploma.lua) on a completed dex; Yellow's
|
||||
-- graphic artist then offers the printed copy (PrintDiploma, stood in
|
||||
-- by src/core/Printer.lua's PNG export like the Pokédex PRNT item).
|
||||
|
||||
local function ownedCount(game)
|
||||
local dex = game.save.pokedex
|
||||
local owned = 0
|
||||
if dex and dex.owned then
|
||||
for _ in pairs(dex.owned) do owned = owned + 1 end
|
||||
end
|
||||
return owned
|
||||
end
|
||||
|
||||
return {
|
||||
CELADON_MANSION_3F = {
|
||||
talk = {
|
||||
TEXT_CELADONMANSION3F_GAME_DESIGNER = function(game, ow, npc, done)
|
||||
local t = game.data.text
|
||||
local dex = game.save.pokedex
|
||||
local owned = 0
|
||||
if dex and dex.owned then
|
||||
for _ in pairs(dex.owned) do
|
||||
owned = owned + 1
|
||||
end
|
||||
end
|
||||
-- NUM_POKEMON - 1 = 150 (discounts Mew, per pokered)
|
||||
local label, fallback
|
||||
if owned >= 150 then
|
||||
label, fallback = "_CeladonMansion3FGameDesignerCompletedDexText",
|
||||
"Wow! Excellent!\nYou completed\nyour POKeDEX!\nCongratulations!"
|
||||
else
|
||||
label, fallback = "_CeladonMansion3FGameDesignerText",
|
||||
"Is that right?\nI'm the game\ndesigner!\nFilling up your\nPOKeDEX is tough,\nbut don't quit!\nWhen you finish,\ncome tell me!"
|
||||
end
|
||||
local TextBox = require("src.render.TextBox")
|
||||
game.stack:push(TextBox.new(game, t[label] or fallback, done))
|
||||
if ownedCount(game) < 150 then
|
||||
game.stack:push(TextBox.new(game,
|
||||
t._CeladonMansion3FGameDesignerText
|
||||
or "Is that right?\nI'm the game\ndesigner!\fFilling up your\nPOKéDEX is tough,\nbut don't quit!",
|
||||
done))
|
||||
return
|
||||
end
|
||||
game.stack:push(TextBox.new(game,
|
||||
t._CeladonMansion3FGameDesignerCompletedDexText
|
||||
or "Wow! Excellent!\nYou completed\nyour POKéDEX!\nCongratulations!",
|
||||
function()
|
||||
local Diploma = require("src.ui.Diploma")
|
||||
game.stack:push(Diploma.new(game, function()
|
||||
-- Yellow tags on the unlocked-printing line (CompletedDexText2)
|
||||
local after = require("src.core.GameVersion").isYellow()
|
||||
and (t._CeladonMansion3FGameDesignerCompletedDexText2
|
||||
or "You can print out\nyour diploma with\nthe GAME BOY\nPrinter!")
|
||||
or nil
|
||||
if after then
|
||||
game.stack:push(TextBox.new(game, after, done))
|
||||
else
|
||||
done()
|
||||
end
|
||||
end))
|
||||
end))
|
||||
end,
|
||||
|
||||
TEXT_CELADONMANSION3F_GRAPHIC_ARTIST = function(game, ow, npc, done)
|
||||
local t = game.data.text
|
||||
local TextBox = require("src.render.TextBox")
|
||||
local yellow = require("src.core.GameVersion").isYellow()
|
||||
if not (yellow and ownedCount(game) >= 150) then
|
||||
game.stack:push(TextBox.new(game,
|
||||
t._CeladonMansion3FGraphicArtistText
|
||||
or "I'm the graphic\nartist!\nI drew you!", done))
|
||||
return
|
||||
end
|
||||
-- _CeladonMansion3FGraphicArtistText2: offer to print the diploma
|
||||
game.stack:push(TextBox.new(game,
|
||||
t._CeladonMansion3FGraphicArtistText2
|
||||
or "I'm the graphic\nartist!\fShould I print\nyour diploma?",
|
||||
function()
|
||||
local ChoiceBox = require("src.ui.ChoiceBox")
|
||||
game.stack:push(ChoiceBox.new(game, function(yes)
|
||||
if not yes then
|
||||
game.stack:push(TextBox.new(game,
|
||||
t._CeladonMansion3FGraphicArtistText3
|
||||
or "Oh. But it's a\nspecial diploma!", done))
|
||||
return
|
||||
end
|
||||
local Printer = require("src.core.Printer")
|
||||
local Diploma = require("src.ui.Diploma")
|
||||
local Strings = require("src.core.Strings")
|
||||
local saved, err = Printer.save("diploma", 160, 144, function()
|
||||
Diploma.render(game)
|
||||
end)
|
||||
-- the PRNT stand-in always reports where the PNG landed
|
||||
game.stack:push(TextBox.new(game, saved
|
||||
and Strings("There you go!\fSaved as\n%s\vin the save\nfolder.", saved)
|
||||
or Strings("Printer error!\n%s", tostring(err)), done))
|
||||
end))
|
||||
end))
|
||||
end,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -35,13 +35,25 @@ local function middleAgedMan(game, ow, npc, done)
|
||||
-- .loop: print WhichBadgeText, then show the badge list menu again
|
||||
push(game, t._CeruleanBadgeHouseMiddleAgedManWhichBadgeText, function()
|
||||
local ListMenu = require("src.ui.ListMenu")
|
||||
local Strings = require("src.core.Strings")
|
||||
local items = {}
|
||||
for _, id in ipairs(BADGE_ORDER) do
|
||||
local idef = game.data.items[id]
|
||||
items[#items + 1] = { label = idef and idef.name or id, value = id }
|
||||
end
|
||||
-- PrintListMenuEntries prints ListMenuCancelText once it hits the
|
||||
-- list's $FF terminator (home/list_menu.asm), so every
|
||||
-- DisplayListMenuID list ends in an on-screen CANCEL row; picking it
|
||||
-- takes DisplayListMenuIDLoop's ExitListMenu path, the same .done
|
||||
-- exit as B (#569)
|
||||
items[#items + 1] = { label = Strings("CANCEL") }
|
||||
local menu = ListMenu.new(game, "", items, {
|
||||
onChoose = function(item)
|
||||
game.stack:pop()
|
||||
if not item.value then
|
||||
push(game, t._CeruleanBadgeHouseMiddleAgedManVisitAnyTimeText, done)
|
||||
return
|
||||
end
|
||||
push(game, t[BADGE_TEXT[item.value]], loop)
|
||||
end,
|
||||
onCancel = function()
|
||||
|
||||
@@ -3,6 +3,10 @@
|
||||
-- Both NPCs use text_asm with an hRandomAdd roll to pick one of several
|
||||
-- flavor lines (no flags, no branching outcome) -- ported as a weighted
|
||||
-- math.random pick each time the NPC is talked to.
|
||||
--
|
||||
-- Yellow renames Slowbro -> Electrode in the text labels (see
|
||||
-- tools/yellow_symbol_aliases.py); look up both so Red/Blue and Yellow
|
||||
-- share this script.
|
||||
|
||||
local M = {}
|
||||
|
||||
@@ -12,6 +16,29 @@ local function push(game, ow, npc, done, text)
|
||||
game.stack:push(TextBox.new(game, text, done))
|
||||
end
|
||||
|
||||
-- CeruleanCitySlowbroText / CeruleanCityElectrodeText
|
||||
-- cp 180 -> 76/256 chance of 1st; cp 120 -> 60/256 chance of 2nd;
|
||||
-- cp 60 -> 60/256 chance of 3rd; else 60/256 chance of 4th.
|
||||
local function talkMon(game, ow, npc, done)
|
||||
local t = game.data.text
|
||||
local roll = math.random(0, 255)
|
||||
local text
|
||||
if roll >= 180 then
|
||||
text = t._CeruleanCitySlowbroTookASnoozeText
|
||||
or t._CeruleanCityElectrodeTookASnoozeText
|
||||
elseif roll >= 120 then
|
||||
text = t._CeruleanCitySlowbroIsLoafingAroundText
|
||||
or t._CeruleanCityElectrodeIsLoafingAroundText
|
||||
elseif roll >= 60 then
|
||||
text = t._CeruleanCitySlowbroTurnedAwayText
|
||||
or t._CeruleanCityElectrodeTurnedAwayText
|
||||
else
|
||||
text = t._CeruleanCitySlowbroIgnoredOrdersText
|
||||
or t._CeruleanCityElectrodeIgnoredOrdersText
|
||||
end
|
||||
push(game, ow, npc, done, text)
|
||||
end
|
||||
|
||||
M.CERULEAN_CITY = {
|
||||
talk = {
|
||||
-- CeruleanCityCooltrainerF1Text (scripts/CeruleanCity.asm:362-393)
|
||||
@@ -23,32 +50,20 @@ M.CERULEAN_CITY = {
|
||||
local text
|
||||
if roll >= 180 then
|
||||
text = t._CeruleanCityCooltrainerF1SlowbroUseSonicboomText
|
||||
or t._CeruleanCityCooltrainerF1ElectrodeUseSonicboomText
|
||||
elseif roll >= 100 then
|
||||
text = t._CeruleanCityCooltrainerF1SlowbroPunchText
|
||||
or t._CeruleanCityCooltrainerF1ElectrodePunchText
|
||||
else
|
||||
text = t._CeruleanCityCooltrainerF1SlowbroWithdrawText
|
||||
or t._CeruleanCityCooltrainerF1ElectrodeWithdrawText
|
||||
end
|
||||
push(game, ow, npc, done, text)
|
||||
end,
|
||||
|
||||
-- CeruleanCitySlowbroText (scripts/CeruleanCity.asm:395-436)
|
||||
-- cp 180 -> 76/256 chance of 1st; cp 120 -> 60/256 chance of 2nd;
|
||||
-- cp 60 -> 60/256 chance of 3rd; else 60/256 chance of 4th.
|
||||
TEXT_CERULEANCITY_SLOWBRO = function(game, ow, npc, done)
|
||||
local t = game.data.text
|
||||
local roll = math.random(0, 255)
|
||||
local text
|
||||
if roll >= 180 then
|
||||
text = t._CeruleanCitySlowbroTookASnoozeText
|
||||
elseif roll >= 120 then
|
||||
text = t._CeruleanCitySlowbroIsLoafingAroundText
|
||||
elseif roll >= 60 then
|
||||
text = t._CeruleanCitySlowbroTurnedAwayText
|
||||
else
|
||||
text = t._CeruleanCitySlowbroIgnoredOrdersText
|
||||
end
|
||||
push(game, ow, npc, done, text)
|
||||
end,
|
||||
TEXT_CERULEANCITY_SLOWBRO = talkMon,
|
||||
-- Yellow map object uses TEXT_CERULEANCITY_ELECTRODE
|
||||
TEXT_CERULEANCITY_ELECTRODE = talkMon,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -14,9 +14,9 @@ return {
|
||||
-- else -> .WhatsLostIsLostText (player has TM_DIG)
|
||||
TEXT_CERULEANTRASHEDHOUSE_FISHING_GURU = {
|
||||
{ "check_item", "TM_DIG" },
|
||||
{ "jump_if_true", 4 },
|
||||
{ "jump_if_true", 5 },
|
||||
{ "show_text", "_CeruleanTrashedHouseFishingGuruTheyStoleATMText" },
|
||||
{ "jump", 5 },
|
||||
{ "jump", "end" },
|
||||
{ "show_text", "_CeruleanTrashedHouseFishingGuruWhatsLostIsLostText" },
|
||||
},
|
||||
},
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
-- Fuchsia City (pokered/scripts/FuchsiaCity.asm)
|
||||
--
|
||||
-- The exhibit signs are text_asm bodies: PrintText of a single text_far,
|
||||
-- then DisplayPokedex for the exhibited species. The preview marks the
|
||||
-- species seen but not owned, same as the S.S. Anne passenger's Snorlax.
|
||||
--
|
||||
-- The fossil sign branches on the Mt. Moon fossil events. The exhibit
|
||||
-- holds the fossil the player did NOT take: taking the Dome Fossil puts
|
||||
-- Omanyte on display, taking the Helix Fossil puts Kabuto. With neither
|
||||
-- event set the sign only prints its undetermined line and no dex entry
|
||||
-- opens.
|
||||
--
|
||||
-- The other text pointers (city sign, Safari Game signs, mart/center/gym
|
||||
-- and warden signs, the four NPCs, the exhibited-mon FuchsiaCityPokemonText
|
||||
-- rows) are plain text_far wrappers that resolve through Data:resolveText,
|
||||
-- so they are not ported here.
|
||||
return {
|
||||
FUCHSIA_CITY = {
|
||||
talk = {
|
||||
-- FuchsiaCityChanseySignText: PrintText(_FuchsiaCityChanseySignText),
|
||||
-- then DisplayPokedex CHANSEY.
|
||||
TEXT_FUCHSIACITY_CHANSEY_SIGN = {
|
||||
{ "show_text", "_FuchsiaCityChanseySignText" },
|
||||
{ "mark_seen", "CHANSEY" },
|
||||
{ "push_screen", "DexEntryMenu", "CHANSEY" },
|
||||
},
|
||||
|
||||
-- FuchsiaCityVoltorbSignText: PrintText(_FuchsiaCityVoltorbSignText),
|
||||
-- then DisplayPokedex VOLTORB.
|
||||
TEXT_FUCHSIACITY_VOLTORB_SIGN = {
|
||||
{ "show_text", "_FuchsiaCityVoltorbSignText" },
|
||||
{ "mark_seen", "VOLTORB" },
|
||||
{ "push_screen", "DexEntryMenu", "VOLTORB" },
|
||||
},
|
||||
|
||||
-- FuchsiaCityKangaskhanSignText: PrintText(_FuchsiaCityKangaskhanSignText),
|
||||
-- then DisplayPokedex KANGASKHAN.
|
||||
TEXT_FUCHSIACITY_KANGASKHAN_SIGN = {
|
||||
{ "show_text", "_FuchsiaCityKangaskhanSignText" },
|
||||
{ "mark_seen", "KANGASKHAN" },
|
||||
{ "push_screen", "DexEntryMenu", "KANGASKHAN" },
|
||||
},
|
||||
|
||||
-- FuchsiaCitySlowpokeSignText: PrintText(_FuchsiaCitySlowpokeSignText),
|
||||
-- then DisplayPokedex SLOWPOKE.
|
||||
TEXT_FUCHSIACITY_SLOWPOKE_SIGN = {
|
||||
{ "show_text", "_FuchsiaCitySlowpokeSignText" },
|
||||
{ "mark_seen", "SLOWPOKE" },
|
||||
{ "push_screen", "DexEntryMenu", "SLOWPOKE" },
|
||||
},
|
||||
|
||||
-- FuchsiaCityLaprasSignText: PrintText(_FuchsiaCityLaprasSignText),
|
||||
-- then DisplayPokedex LAPRAS.
|
||||
TEXT_FUCHSIACITY_LAPRAS_SIGN = {
|
||||
{ "show_text", "_FuchsiaCityLaprasSignText" },
|
||||
{ "mark_seen", "LAPRAS" },
|
||||
{ "push_screen", "DexEntryMenu", "LAPRAS" },
|
||||
},
|
||||
|
||||
-- FuchsiaCityFossilSignText: CheckEvent EVENT_GOT_DOME_FOSSIL /
|
||||
-- CheckEventReuseA EVENT_GOT_HELIX_FOSSIL pick the text and the
|
||||
-- displayed entry; with neither set only the undetermined line prints.
|
||||
TEXT_FUCHSIACITY_FOSSIL_SIGN = {
|
||||
{ "check_flag", "EVENT_GOT_DOME_FOSSIL" }, -- 1
|
||||
{ "jump_if_true", 7 }, -- 2
|
||||
{ "check_flag", "EVENT_GOT_HELIX_FOSSIL" }, -- 3
|
||||
{ "jump_if_true", 11 }, -- 4
|
||||
{ "show_text", "_FuchsiaCityFossilSignUndeterminedText" }, -- 5
|
||||
{ "jump", "end" }, -- 6
|
||||
{ "show_text", "_FuchsiaCityFossilSignOmanyteText" }, -- 7
|
||||
{ "mark_seen", "OMANYTE" }, -- 8
|
||||
{ "push_screen", "DexEntryMenu", "OMANYTE" }, -- 9
|
||||
{ "jump", "end" }, -- 10
|
||||
{ "show_text", "_FuchsiaCityFossilSignKabutoText" }, -- 11
|
||||
{ "mark_seen", "KABUTO" }, -- 12
|
||||
{ "push_screen", "DexEntryMenu", "KABUTO" }, -- 13
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -10,10 +10,10 @@
|
||||
local function coinGiver(opts)
|
||||
return function(game, ow, npc, done)
|
||||
local TextBox = require("src.render.TextBox")
|
||||
local Sound = require("src.core.Sound")
|
||||
local t = game.data.text
|
||||
local function push(label, fallback, onDone)
|
||||
game.stack:push(TextBox.new(game, t[label] or fallback, onDone or done))
|
||||
local function push(label, fallback, onDone, popts)
|
||||
game.stack:push(TextBox.new(game, t[label] or fallback, onDone or done,
|
||||
popts))
|
||||
end
|
||||
if game.save.flags[opts.event] then
|
||||
push(opts.alreadyGotLabel, opts.alreadyGotFallback)
|
||||
@@ -30,9 +30,10 @@ local function coinGiver(opts)
|
||||
end
|
||||
game.save.coins = math.min(9999, (game.save.coins or 0) + opts.amount)
|
||||
game.save.flags[opts.event] = true
|
||||
Sound.play(game.data, "Get_Item1")
|
||||
-- the ReceivedNCoinsText strings carry sound_get_item_1
|
||||
push(opts.receivedLabel,
|
||||
("{PLAYER} received\n%d coins!"):format(opts.amount))
|
||||
("{PLAYER} received\n%d coins!"):format(opts.amount), nil,
|
||||
TextBox.soundOpts(game, "Get_Item1"))
|
||||
end)
|
||||
end
|
||||
end
|
||||
@@ -85,6 +86,47 @@ return {
|
||||
alreadyGotLabel = "_GameCornerGentlemanCloselyWatchTheReelsText",
|
||||
alreadyGotFallback = "The trick is to\nwatch the reels\vclosely!",
|
||||
}),
|
||||
|
||||
-- Yellow keeps all three giveaways with the same events and amounts
|
||||
-- but renames the objects and their text labels: FISHING_GURU ->
|
||||
-- FISHING_GURU1, CLERK2 -> MIDDLE_AGED_MAN2, GENTLEMAN ->
|
||||
-- FISHING_GURU2 (pokeyellow/scripts/GameCorner.asm). The Red/Blue
|
||||
-- keys above never match there, so Yellow needs its own three (#552).
|
||||
TEXT_GAMECORNER_FISHING_GURU1 = coinGiver({
|
||||
event = "EVENT_GOT_10_COINS",
|
||||
amount = 10,
|
||||
askLabel = "_GameCornerFishingGuru1WantToPlayText",
|
||||
askFallback = "Kid, do you want\nto play?",
|
||||
receivedLabel = "_GameCornerFishingGuru1Received10CoinsText",
|
||||
coinCaseFullLabel = "_GameCornerFishingGuru1DontNeedMyCoinsText",
|
||||
coinCaseFullFallback = "You don't need my\ncoins!",
|
||||
alreadyGotLabel = "_GameCornerFishingGuru1WinsComeAndGoText",
|
||||
alreadyGotFallback = "Wins seem to come\nand go.",
|
||||
}),
|
||||
|
||||
TEXT_GAMECORNER_MIDDLE_AGED_MAN2 = coinGiver({
|
||||
event = "EVENT_GOT_20_COINS_2",
|
||||
amount = 20,
|
||||
askLabel = "_GameCornerMiddleAgedMan2WantSomeCoinsText",
|
||||
askFallback = "What's up? Want\nsome coins?",
|
||||
receivedLabel = "_GameCornerMiddleAgedMan2Received20CoinsText",
|
||||
coinCaseFullLabel = "_GameCornerMiddleAgedMan2YouHaveLotsOfCoinsText",
|
||||
coinCaseFullFallback = "You have lots of\ncoins!",
|
||||
alreadyGotLabel = "_GameCornerMiddleAgedMan2INeedMoreCoinsText",
|
||||
alreadyGotFallback = "Darn! I need more\ncoins for the\vPOKéMON I want!",
|
||||
}),
|
||||
|
||||
TEXT_GAMECORNER_FISHING_GURU2 = coinGiver({
|
||||
event = "EVENT_GOT_20_COINS",
|
||||
amount = 20,
|
||||
askLabel = "_GameCornerFishingGuru2ThrowingMeOffText",
|
||||
askFallback = "Hey, what? You're\nthrowing me off!\vHere are some\vcoins, shoo!",
|
||||
receivedLabel = "_GameCornerFishingGuru2Received20CoinsText",
|
||||
coinCaseFullLabel = "_GameCornerFishingGuru2YouGotYourOwnCoinsText",
|
||||
coinCaseFullFallback = "You've got your\nown coins!",
|
||||
alreadyGotLabel = "_GameCornerFishingGuru2CloselyWatchTheReelsText",
|
||||
alreadyGotFallback = "The trick is to\nwatch the reels\vclosely!",
|
||||
}),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -8,9 +8,9 @@ return {
|
||||
TEXT_LAVENDERMART_COOLTRAINER_M = {
|
||||
{ "face_player" },
|
||||
{ "check_flag", "EVENT_RESCUED_MR_FUJI" },
|
||||
{ "jump_if_true", 5 },
|
||||
{ "jump_if_true", 6 },
|
||||
{ "show_text", "_LavenderMartCooltrainerMReviveText" },
|
||||
{ "jump", 6 },
|
||||
{ "jump", "end" },
|
||||
{ "show_text", "_LavenderMartCooltrainerMNuggetText" },
|
||||
},
|
||||
},
|
||||
|
||||
@@ -13,9 +13,9 @@ M.MR_FUJIS_HOUSE = {
|
||||
TEXT_MRFUJISHOUSE_SUPER_NERD = {
|
||||
{ "face_player" }, -- 1
|
||||
{ "check_flag", "EVENT_RESCUED_MR_FUJI" }, -- 2
|
||||
{ "jump_if_true", 5 }, -- 3
|
||||
{ "jump_if_true", 6 }, -- 3
|
||||
{ "show_text", "_MrFujisHouseSuperNerdMrFujiIsntHereText" }, -- 4
|
||||
{ "jump", 6 }, -- 5
|
||||
{ "jump", "end" }, -- 5
|
||||
{ "show_text", "_MrFujisHouseSuperNerdMrFujiHadBeenPrayingText" }, -- 6
|
||||
},
|
||||
|
||||
@@ -25,9 +25,9 @@ M.MR_FUJIS_HOUSE = {
|
||||
TEXT_MRFUJISHOUSE_LITTLE_GIRL = {
|
||||
{ "face_player" }, -- 1
|
||||
{ "check_flag", "EVENT_RESCUED_MR_FUJI" }, -- 2
|
||||
{ "jump_if_true", 5 }, -- 3
|
||||
{ "jump_if_true", 6 }, -- 3
|
||||
{ "show_text", "_MrFujisHouseLittleGirlThisIsMrFujisHouseText" }, -- 4
|
||||
{ "jump", 6 }, -- 5
|
||||
{ "jump", "end" }, -- 5
|
||||
{ "show_text", "_MrFujisHouseLittleGirlPokemonAreNiceToHugText" }, -- 6
|
||||
},
|
||||
|
||||
|
||||
@@ -22,8 +22,8 @@ return {
|
||||
local TextBox = require("src.render.TextBox")
|
||||
local Commands = require("src.script.Commands")
|
||||
local t = game.data.text
|
||||
local function say(label, cb)
|
||||
game.stack:push(TextBox.new(game, t[label] or label, cb))
|
||||
local function say(label, cb, sopts)
|
||||
game.stack:push(TextBox.new(game, t[label] or label, cb, sopts))
|
||||
end
|
||||
|
||||
if game.save.flags.EVENT_GOT_OLD_AMBER then
|
||||
@@ -39,8 +39,9 @@ return {
|
||||
game.save.flags.EVENT_GOT_OLD_AMBER = true
|
||||
Commands.hide_object({ save = game.save, overworld = ow, game = game },
|
||||
"MUSEUM_1F", "MUSEUM1F_OLD_AMBER")
|
||||
require("src.core.Sound").play(game.data, "Get_Item1")
|
||||
say("_Museum1FScientist2ReceivedOldAmberText", done)
|
||||
-- .ReceivedOldAmberText carries sound_get_item_1
|
||||
say("_Museum1FScientist2ReceivedOldAmberText", done,
|
||||
TextBox.soundOpts(game, "Get_Item1"))
|
||||
end)
|
||||
end,
|
||||
|
||||
|
||||
@@ -1,13 +1,39 @@
|
||||
-- Hand-ported flavor text for OaksLab (registry id OAKS_LAB).
|
||||
-- Source: pokered/scripts/OaksLab.asm. These five text_asm bodies are
|
||||
-- all simple "PrintText; jp TextScriptEnd" -- no flag branches, no
|
||||
-- YES/NO menu -- so a one-row talk script showing the real extracted
|
||||
-- text is a faithful port. (The rest of OaksLab.asm's TEXT_OAKSLAB_*
|
||||
-- constants -- OAK1, the three starter poke balls, RIVAL -- are already
|
||||
-- ported with full branching logic in data/scripts/oaks_lab.lua.)
|
||||
-- Hand-ported OAKS_LAB flavor: the simple talk texts (scripts/OaksLab.asm;
|
||||
-- OAK1, the starter balls and RIVAL live in data/scripts/oaks_lab.lua).
|
||||
|
||||
local TextBox = require("src.render.TextBox")
|
||||
|
||||
return {
|
||||
OAKS_LAB = {
|
||||
-- data/events/hidden_events.asm:147
|
||||
onInteract = function(game, ow, fx, fy)
|
||||
local t = game.data.text or {}
|
||||
-- engine/events/hidden_events/oaks_lab_posters.asm:1
|
||||
if fy == 0 and fx == 4 then
|
||||
game.stack:push(TextBox.new(game,
|
||||
t._PushStartText or "Push START to\nopen the MENU!"))
|
||||
return true
|
||||
end
|
||||
if fy == 0 and fx == 5 then
|
||||
local owned = 0
|
||||
for _ in pairs(game.save.pokedex.owned or {}) do owned = owned + 1 end
|
||||
game.stack:push(TextBox.new(game,
|
||||
owned >= 2
|
||||
and (t._StrengthsAndWeaknessesText
|
||||
or "All POKéMON types\nhave strong and\vweak points\vagainst others.")
|
||||
or (t._SaveOptionText
|
||||
or "The SAVE option is\non the MENU\vscreen.")))
|
||||
return true
|
||||
end
|
||||
-- engine/events/hidden_events/oaks_lab_email.asm:1
|
||||
if fy == 1 and (fx == 0 or fx == 1) then
|
||||
if ow.player.facing ~= "up" then return false end
|
||||
game.stack:push(TextBox.new(game,
|
||||
t._OakLabEmailText or "There's an e-mail\nmessage here!"))
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end,
|
||||
talk = {
|
||||
-- OaksLabGirlText (scripts/OaksLab.asm)
|
||||
TEXT_OAKSLAB_GIRL = {
|
||||
|
||||
@@ -3,9 +3,8 @@
|
||||
-- guide and SUPER_NERD2 garden nerd.
|
||||
--
|
||||
-- The YOUNGSTER's gym escort (talk + east-exit onStep) lives in
|
||||
-- story5.lua so the lockstep RLE walk is not overwritten by this
|
||||
-- flavor merge. SUPER_NERD1's museum escort is not ported; only the
|
||||
-- YES/NO-branched flavor text is here.
|
||||
-- story5.lua; SUPER_NERD1's museum escort (scripts/PewterCity.asm:47-113)
|
||||
-- is below.
|
||||
|
||||
local M = {}
|
||||
|
||||
@@ -16,16 +15,147 @@ local function push(game, s, done)
|
||||
game.stack:push(TextBox.new(game, s, done))
|
||||
end
|
||||
|
||||
-- PrintText on a text_end string returns with the box still drawn and
|
||||
-- YesNoChoice then draws the menu above it (InitYesNoTextBoxParameters,
|
||||
-- engine/menus/text_box.asm); no A press clears the question first. Ride
|
||||
-- TextBox's opts.choice, the same as Commands.ask (#854).
|
||||
local function ask(game, s, cb)
|
||||
local ChoiceBox = require("src.ui.ChoiceBox")
|
||||
push(game, s, function() game.stack:push(ChoiceBox.new(game, cb)) end)
|
||||
local TextBox = require("src.render.TextBox")
|
||||
game.stack:push(TextBox.new(game, s, nil, { choice = cb }))
|
||||
end
|
||||
|
||||
-- RLEList_PewterMuseumGuy (engine/overworld/auto_movement.asm:199-204)
|
||||
local museumGuySteps = {
|
||||
"up", "up", "up", "up", "up", "up",
|
||||
"left", "left", "left", "left", "left", "left", "left", "left",
|
||||
"left", "left", "left", "left", "left",
|
||||
"up", "up", "up",
|
||||
"left",
|
||||
}
|
||||
|
||||
-- RLEList_PewterMuseumPlayer (engine/overworld/auto_movement.asm:192-197)
|
||||
local museumPlayerRle = {
|
||||
"NO",
|
||||
"up", "up", "up",
|
||||
"left", "left", "left", "left", "left", "left", "left", "left",
|
||||
"left", "left", "left", "left", "left",
|
||||
"up", "up", "up", "up", "up", "up",
|
||||
}
|
||||
|
||||
-- PewterMuseumGuyCoords (engine/events/pewter_guys.asm:58-75)
|
||||
local museumPreambles = {
|
||||
["27,18"] = { "up", "up" },
|
||||
["27,16"] = { "right", "left" },
|
||||
["26,17"] = { "up", "right" },
|
||||
["28,17"] = { "up", "left" },
|
||||
}
|
||||
|
||||
-- PewterGuys (engine/events/pewter_guys.asm:1-49), same transform as
|
||||
-- pewterEscort.playerPlan in story5.lua
|
||||
local function museumPlan(x, y)
|
||||
local pre = museumPreambles[x .. "," .. y]
|
||||
if not pre then return nil end
|
||||
local buf = {}
|
||||
for i, d in ipairs(museumPlayerRle) do buf[i] = d end
|
||||
buf[#buf] = pre[1]
|
||||
for i = 2, #pre do buf[#buf + 1] = pre[i] end
|
||||
local path = {}
|
||||
for i = #buf, 1, -1 do path[#path + 1] = buf[i] end
|
||||
local head = 0
|
||||
while path[head + 1] == "NO" do head = head + 1 end
|
||||
local tail = #path
|
||||
while tail > head and path[tail] == "NO" do tail = tail - 1 end
|
||||
local steps = {}
|
||||
for i = head + 1, tail do steps[#steps + 1] = path[i] end
|
||||
return { steps = steps, guyHeadStart = math.floor(head / 8) }
|
||||
end
|
||||
|
||||
-- PewterCitySuperNerd1ShowsPlayerMuseumScript (scripts/PewterCity.asm:47-113)
|
||||
local function museumEscortWalk(game, ow)
|
||||
if ow.runner:isRunning() or #ow.scriptMoves > 0 then return false end
|
||||
local plan = museumPlan(ow.player.cellX, ow.player.cellY)
|
||||
if not plan then return false end
|
||||
local Music = require("src.core.Music")
|
||||
local t = text(game)
|
||||
local guy = ow:npcByIndex(3) -- PEWTERCITY_SUPER_NERD1
|
||||
local head = plan.guyHeadStart
|
||||
|
||||
-- SetSpritePosition2 + ShowObject back on his spawn (27,17), the same
|
||||
-- snap walkHome does in story5.lua (scripts/PewterCity.asm:102-113)
|
||||
local function walkOut()
|
||||
if not guy then return end
|
||||
local i = 0
|
||||
local function tick()
|
||||
i = i + 1
|
||||
if i > 4 then
|
||||
guy.cellX, guy.cellY = 27, 17
|
||||
guy.px, guy.py = 27 * 16, 17 * 16
|
||||
guy.moving = false
|
||||
guy.targetX, guy.targetY = nil, nil
|
||||
guy.facing = "down"
|
||||
return
|
||||
end
|
||||
ow:scriptMove(guy, "down", 1, tick)
|
||||
end
|
||||
tick()
|
||||
end
|
||||
|
||||
-- SetSpritePosition1 pins him beside the museum door (map (17,12) minus
|
||||
-- the +4 border offset = (13,8)), then MovementData_PewterMuseumGuyExit
|
||||
local function afterWalk()
|
||||
if guy then
|
||||
guy.stepFrames = nil
|
||||
guy.cellX, guy.cellY = 13, 8
|
||||
guy.px, guy.py = 13 * 16, 8 * 16
|
||||
guy.moving = false
|
||||
guy.targetX, guy.targetY = nil, nil
|
||||
guy.facing = "up"
|
||||
end
|
||||
Music.playMap(game.data, "PEWTER_CITY")
|
||||
push(game, t._PewterCitySuperNerd1ItsRightHereText
|
||||
or "It's right here!", walkOut)
|
||||
end
|
||||
|
||||
local function lockstep()
|
||||
local i = 0
|
||||
local function tick()
|
||||
i = i + 1
|
||||
local ps = plan.steps[i]
|
||||
if not ps then
|
||||
afterWalk()
|
||||
return
|
||||
end
|
||||
local gs = museumGuySteps[head + i]
|
||||
if guy and gs then ow:scriptMove(guy, gs, 1) end
|
||||
ow:scriptMove(ow.player, ps, 1, tick)
|
||||
end
|
||||
tick()
|
||||
end
|
||||
|
||||
-- engine/overworld/movement.asm:737 (DoScriptedNPCMovement)
|
||||
if guy then
|
||||
guy.stepFrames = ow.player.stepFramesCur or ow.player.stepFrames
|
||||
end
|
||||
Music.play(game.data, "Music_MuseumGuy")
|
||||
if guy and head > 0 then
|
||||
local h = 0
|
||||
local function headTick()
|
||||
h = h + 1
|
||||
if h > head then lockstep(); return end
|
||||
ow:scriptMove(guy, museumGuySteps[h], 1, headTick)
|
||||
end
|
||||
headTick()
|
||||
else
|
||||
lockstep()
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
M.PEWTER_CITY = {
|
||||
museumEscort = { plan = museumPlan, guySteps = museumGuySteps },
|
||||
talk = {
|
||||
-- PewterCitySuperNerd1Text (scripts/PewterCity.asm): asks if you
|
||||
-- checked out the museum; YES -> fossils comment, NO -> "you have
|
||||
-- to go" (which in pokered also kicks off the escort script).
|
||||
-- PewterCitySuperNerd1Text (scripts/PewterCity.asm:209-237): YES ->
|
||||
-- fossils comment, NO -> "you have to go" and the museum escort
|
||||
TEXT_PEWTERCITY_SUPER_NERD1 = function(game, ow, npc, done)
|
||||
local t = text(game)
|
||||
ask(game, t._PewterCitySuperNerd1DidYouCheckOutMuseumText
|
||||
@@ -35,7 +165,10 @@ M.PEWTER_CITY = {
|
||||
or "Weren't those\nfossils from MT.\nMOON amazing?", done)
|
||||
else
|
||||
push(game, t._PewterCitySuperNerd1YouHaveToGoText
|
||||
or "Really?\nYou absolutely\nhave to go!", done)
|
||||
or "Really?\nYou absolutely\nhave to go!", function()
|
||||
museumEscortWalk(game, ow)
|
||||
if done then done() end
|
||||
end)
|
||||
end
|
||||
end)
|
||||
end,
|
||||
|
||||
@@ -12,9 +12,9 @@ return {
|
||||
TEXT_ROUTE16GATE1F_GUARD = {
|
||||
{ "face_player" },
|
||||
{ "check_item", "BICYCLE" },
|
||||
{ "jump_if_true", 5 },
|
||||
{ "jump_if_true", 6 },
|
||||
{ "show_text", "_Route16Gate1FGuardNoPedestriansAllowedText" },
|
||||
{ "jump", 6 },
|
||||
{ "jump", "end" },
|
||||
{ "show_text", "_Route16Gate1FGuardCyclingRoadExplanationText" },
|
||||
},
|
||||
},
|
||||
|
||||
@@ -14,9 +14,9 @@ return {
|
||||
TEXT_ROUTE18GATE1F_GUARD = {
|
||||
{ "face_player" },
|
||||
{ "check_item", "BICYCLE" },
|
||||
{ "jump_if_true", 5 },
|
||||
{ "jump_if_true", 6 },
|
||||
{ "show_text", "_Route18Gate1FGuardYouNeedABicycleText" },
|
||||
{ "jump", 6 },
|
||||
{ "jump", "end" },
|
||||
{ "show_text", "_Route18Gate1FGuardCyclingRoadUphillText" },
|
||||
},
|
||||
},
|
||||
|
||||
@@ -12,9 +12,9 @@ return {
|
||||
TEXT_SILPHCO10F_SILPH_WORKER_F = {
|
||||
{ "face_player" },
|
||||
{ "check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI" },
|
||||
{ "jump_if_true", 5 },
|
||||
{ "jump_if_true", 6 },
|
||||
{ "show_text", "_SilphCo10FSilphWorkerFImScaredText" },
|
||||
{ "jump", 6 },
|
||||
{ "jump", "end" },
|
||||
{ "show_text", "_SilphCo10FSilphWorkerFQuietAboutMyCryingText" },
|
||||
},
|
||||
},
|
||||
|
||||
@@ -10,9 +10,9 @@ return {
|
||||
-- not set: _SilphCo3FSilphWorkerMWhatShouldIDoText
|
||||
TEXT_SILPHCO3F_SILPH_WORKER_M = {
|
||||
{ "check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI" },
|
||||
{ "jump_if_true", 4 },
|
||||
{ "jump_if_true", 5 },
|
||||
{ "show_text", "_SilphCo3FSilphWorkerMWhatShouldIDoText" },
|
||||
{ "jump", 5 },
|
||||
{ "jump", "end" },
|
||||
{ "show_text", "_SilphCo3FSilphWorkerMYouSavedUsText" },
|
||||
},
|
||||
},
|
||||
|
||||
@@ -7,9 +7,9 @@ return {
|
||||
TEXT_SILPHCO4F_SILPH_WORKER_M = {
|
||||
{"face_player"},
|
||||
{"check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI"},
|
||||
{"jump_if_true", 5},
|
||||
{"jump_if_true", 6},
|
||||
{"show_text", "_SilphCo4FSilphWorkerMImHidingText"},
|
||||
{"jump", 6},
|
||||
{"jump", "end"},
|
||||
{"show_text", "_SilphCo4FSilphWorkerMTeamRocketIsGoneText"},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -7,9 +7,9 @@ return {
|
||||
TEXT_SILPHCO5F_SILPH_WORKER_M = {
|
||||
{"face_player"},
|
||||
{"check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI"},
|
||||
{"jump_if_true", 5},
|
||||
{"jump_if_true", 6},
|
||||
{"show_text", "_SilphCo5FSilphWorkerMThatsYouRightText"},
|
||||
{"jump", 6},
|
||||
{"jump", "end"},
|
||||
{"show_text", "_SilphCo5FSilphWorkerMYoureOurHeroText"},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -11,9 +11,9 @@ return {
|
||||
TEXT_SILPHCO6F_SILPH_WORKER_M1 = {
|
||||
{ "face_player" },
|
||||
{ "check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI" },
|
||||
{ "jump_if_true", 5 },
|
||||
{ "jump_if_true", 6 },
|
||||
{ "show_text", "_SilphCo6FSilphWorkerM1TookOverTheBuildingText" },
|
||||
{ "jump", 6 },
|
||||
{ "jump", "end" },
|
||||
{ "show_text", "_SilphCo6FSilphWorkerM1BackToWorkText" },
|
||||
},
|
||||
|
||||
@@ -21,9 +21,9 @@ return {
|
||||
TEXT_SILPHCO6F_SILPH_WORKER_M2 = {
|
||||
{ "face_player" },
|
||||
{ "check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI" },
|
||||
{ "jump_if_true", 5 },
|
||||
{ "jump_if_true", 6 },
|
||||
{ "show_text", "_SilphCo6FSilphWorkerMHelpMePleaseText" },
|
||||
{ "jump", 6 },
|
||||
{ "jump", "end" },
|
||||
{ "show_text", "_SilphCo6FSilphWorkerMWeGotEngagedText" },
|
||||
},
|
||||
|
||||
@@ -31,9 +31,9 @@ return {
|
||||
TEXT_SILPHCO6F_SILPH_WORKER_F1 = {
|
||||
{ "face_player" },
|
||||
{ "check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI" },
|
||||
{ "jump_if_true", 5 },
|
||||
{ "jump_if_true", 6 },
|
||||
{ "show_text", "_SilphCo6FSilphWorkerF1SuchACowardText" },
|
||||
{ "jump", 6 },
|
||||
{ "jump", "end" },
|
||||
{ "show_text", "_SilphCo6FSilphWorkerF1HaveToMarryHimText" },
|
||||
},
|
||||
|
||||
@@ -41,9 +41,9 @@ return {
|
||||
TEXT_SILPHCO6F_SILPH_WORKER_F2 = {
|
||||
{ "face_player" },
|
||||
{ "check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI" },
|
||||
{ "jump_if_true", 5 },
|
||||
{ "jump_if_true", 6 },
|
||||
{ "show_text", "_SilphCo6FSilphWorkerF2TeamRocketConquerWorldText" },
|
||||
{ "jump", 6 },
|
||||
{ "jump", "end" },
|
||||
{ "show_text", "_SilphCo6FSilphWorkerF2TeamRocketRanText" },
|
||||
},
|
||||
|
||||
@@ -51,9 +51,9 @@ return {
|
||||
TEXT_SILPHCO6F_SILPH_WORKER_M3 = {
|
||||
{ "face_player" },
|
||||
{ "check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI" },
|
||||
{ "jump_if_true", 5 },
|
||||
{ "jump_if_true", 6 },
|
||||
{ "show_text", "_SilphCo6FSilphWorkerM3TargetedSilphText" },
|
||||
{ "jump", 6 },
|
||||
{ "jump", "end" },
|
||||
{ "show_text", "_SilphCo6FSilphWorkerM3WorkForSilphText" },
|
||||
},
|
||||
},
|
||||
|
||||
@@ -10,9 +10,9 @@ return {
|
||||
-- set: _SilphCo7FSilphWorkerM2CancelledMasterBallText
|
||||
TEXT_SILPHCO7F_SILPH_WORKER_M2 = {
|
||||
{ "check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI" },
|
||||
{ "jump_if_true", 4 },
|
||||
{ "jump_if_true", 5 },
|
||||
{ "show_text", "_SilphCo7FSilphWorkerM2AfterTheMasterBallText" },
|
||||
{ "jump", 5 },
|
||||
{ "jump", "end" },
|
||||
{ "show_text", "_SilphCo7FSilphWorkerM2CancelledMasterBallText" },
|
||||
},
|
||||
|
||||
@@ -22,9 +22,9 @@ return {
|
||||
-- set: _SilphCo7FSilphWorkerM3YouChasedOffTeamRocketText
|
||||
TEXT_SILPHCO7F_SILPH_WORKER_M3 = {
|
||||
{ "check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI" },
|
||||
{ "jump_if_true", 4 },
|
||||
{ "jump_if_true", 5 },
|
||||
{ "show_text", "_SilphCo7FSilphWorkerM3ItWouldBeBadText" },
|
||||
{ "jump", 5 },
|
||||
{ "jump", "end" },
|
||||
{ "show_text", "_SilphCo7FSilphWorkerM3YouChasedOffTeamRocketText" },
|
||||
},
|
||||
|
||||
@@ -34,9 +34,9 @@ return {
|
||||
-- set: _SilphCo7FSilphWorkerM4SafeAtLastText
|
||||
TEXT_SILPHCO7F_SILPH_WORKER_M4 = {
|
||||
{ "check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI" },
|
||||
{ "jump_if_true", 4 },
|
||||
{ "jump_if_true", 5 },
|
||||
{ "show_text", "_SilphCo7FSilphWorkerM4ItsReallyDangerousHereText" },
|
||||
{ "jump", 5 },
|
||||
{ "jump", "end" },
|
||||
{ "show_text", "_SilphCo7FSilphWorkerM4SafeAtLastText" },
|
||||
},
|
||||
},
|
||||
|
||||
@@ -10,9 +10,9 @@ return {
|
||||
-- set: _SilphCo8FSilphWorkerMThanksForSavingUsText
|
||||
TEXT_SILPHCO8F_SILPH_WORKER_M = {
|
||||
{ "check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI" },
|
||||
{ "jump_if_true", 4 },
|
||||
{ "jump_if_true", 5 },
|
||||
{ "show_text", "_SilphCo8FSilphWorkerMSilphIsFinishedText" },
|
||||
{ "jump", 5 },
|
||||
{ "jump", "end" },
|
||||
{ "show_text", "_SilphCo8FSilphWorkerMThanksForSavingUsText" },
|
||||
},
|
||||
},
|
||||
|
||||
@@ -7,19 +7,19 @@
|
||||
-- CheckFightingMapTrainers/TalkToTrainer via the existing trainer
|
||||
-- system, and the item pickups carry no talk text of their own here.
|
||||
--
|
||||
-- TEXT_SSANNE2FROOMS_GENTLEMAN3 also opens the POKéDEX entry for
|
||||
-- SNORLAX after the text box (DisplayPokedex in the original), which
|
||||
-- has no equivalent talk-script hook in this port (DexEntryMenu has
|
||||
-- no done-callback the way other pushed UI states do) -- only the
|
||||
-- flavor line is ported here.
|
||||
-- TEXT_SSANNE2FROOMS_GENTLEMAN3 opens SNORLAX's POKéDEX entry after its
|
||||
-- text box, like DisplayPokedex in the original. The preview marks it seen
|
||||
-- but does not mark it owned.
|
||||
return {
|
||||
SS_ANNE_2F_ROOMS = {
|
||||
talk = {
|
||||
-- SSAnne2FRoomsGentleman3Text: PrintText(_SSAnne2FRoomsGentleman3Text)
|
||||
-- (then DisplayPokedex SNORLAX -- not ported, see note above)
|
||||
-- SSAnne2FRoomsGentleman3Text: PrintText(_SSAnne2FRoomsGentleman3Text),
|
||||
-- then DisplayPokedex SNORLAX.
|
||||
TEXT_SSANNE2FROOMS_GENTLEMAN3 = {
|
||||
{ "face_player" },
|
||||
{ "show_text", "_SSAnne2FRoomsGentleman3Text" },
|
||||
{ "mark_seen", "SNORLAX" },
|
||||
{ "push_screen", "DexEntryMenu", "SNORLAX" },
|
||||
},
|
||||
|
||||
-- SSAnne2FRoomsGentleman4Text: PrintText(_SSAnne2FRoomsGentleman4Text)
|
||||
|
||||
@@ -23,9 +23,13 @@ local function push(game, s, done)
|
||||
game.stack:push(TextBox.new(game, s, done))
|
||||
end
|
||||
|
||||
-- PrintText on a text_end string returns with the box still drawn and
|
||||
-- YesNoChoice then draws the menu above it (InitYesNoTextBoxParameters,
|
||||
-- engine/menus/text_box.asm); no A press clears the question first. Ride
|
||||
-- TextBox's opts.choice, the same as Commands.ask (#854).
|
||||
local function ask(game, s, cb)
|
||||
local ChoiceBox = require("src.ui.ChoiceBox")
|
||||
push(game, s, function() game.stack:push(ChoiceBox.new(game, cb)) end)
|
||||
local TextBox = require("src.render.TextBox")
|
||||
game.stack:push(TextBox.new(game, s, nil, { choice = cb }))
|
||||
end
|
||||
|
||||
M.VIRIDIAN_CITY = {
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
-- Viridian School House's two readables (data/events/hidden_events.asm,
|
||||
-- hidden_events_for VIRIDIAN_SCHOOL_HOUSE):
|
||||
-- hidden_text_predef 3, 0 PrintBlackboardLinkCableText, ViridianSchoolBlackboard
|
||||
-- hidden_text_predef 3, 4 PrintNotebookText, ViridianSchoolNotebook
|
||||
-- tools/extract/field.py only parses `hidden_event` rows, so neither
|
||||
-- hidden_text_predef row reaches data/generated/field.lua and both tiles
|
||||
-- were dead A presses (#503). Same hook shape, and the same sibling asm
|
||||
-- file, as the Celadon roof house in data/scripts/celadon_eevee.lua (#391);
|
||||
-- hidden_text_predef spends the facing byte on the tx_pre id, so neither
|
||||
-- tile gates on facing.
|
||||
|
||||
local Font = require("src.render.Font")
|
||||
local Theme = require("src.ui.Theme")
|
||||
local TextBox = require("src.render.TextBox")
|
||||
|
||||
-- ViridianSchoolBlackboard (engine/events/hidden_events/school_blackboard.asm):
|
||||
-- StatusAilmentText1/2 are the two columns of the 12x8 box at the top left
|
||||
-- (hlcoord 0, 0 + `lb bc, 6, 10`); picking a status prints its
|
||||
-- ViridianBlackboardStatusPointers entry and jumps back to .blackboardLoop,
|
||||
-- QUIT or B falls through to .exitBlackboard.
|
||||
local STATUS_LABELS = {
|
||||
{ " SLP", "_ViridianBlackboardSleepText" },
|
||||
{ " PSN", "_ViridianBlackboardPoisonText" },
|
||||
{ " PAR", "_ViridianBlackboardPrlzText" },
|
||||
{ " BRN", "_ViridianBlackboardBurnText" },
|
||||
{ " FRZ", "_ViridianBlackboardFrozenText" },
|
||||
}
|
||||
|
||||
-- The headings list is a two-column menu, which src/ui/Menu.lua does not do
|
||||
-- (it stacks one column), so the layout lives here (#591). .blackboardLoop:
|
||||
-- TextBoxBorder at hlcoord 0, 0 with `lb bc, 6, 10` is the 12x8 box,
|
||||
-- StatusAilmentText1 (" SLP"/" PSN"/" PAR") is placed at hlcoord 1, 2 and
|
||||
-- StatusAilmentText2 (" BRN"/" FRZ"/" QUIT") at hlcoord 6, 2. LEFT/RIGHT
|
||||
-- move wTopMenuItemX between those two columns and swap wMenuItemOffset
|
||||
-- between 0 and 3 while leaving wCurrentMenuItem (the row) alone; UP/DOWN
|
||||
-- are not in wMenuWatchedKeys, so they only slide the cursor and loop.
|
||||
local BOARD_LABELS = {}
|
||||
for i, row in ipairs(STATUS_LABELS) do BOARD_LABELS[i] = row[1] end
|
||||
BOARD_LABELS[#BOARD_LABELS + 1] = " QUIT"
|
||||
local BOARD_COL_X = { 1, 6 }
|
||||
local BOARD_ROW_Y = 2
|
||||
local BOARD_ROWS = 3
|
||||
|
||||
local StatusBoard = {}
|
||||
StatusBoard.__index = StatusBoard
|
||||
|
||||
function StatusBoard.new(game, onPick, onQuit)
|
||||
return setmetatable({ game = game, col = 1, row = 1, labels = BOARD_LABELS,
|
||||
onPick = onPick, onQuit = onQuit }, StatusBoard)
|
||||
end
|
||||
|
||||
-- flat index = pokered's wMenuItemOffset (0 or 3) + wCurrentMenuItem (0..2),
|
||||
-- so 1..5 are the statuses in ViridianBlackboardStatusPointers order and 6
|
||||
-- is QUIT
|
||||
function StatusBoard:selection()
|
||||
return (self.col - 1) * BOARD_ROWS + self.row
|
||||
end
|
||||
|
||||
function StatusBoard:update()
|
||||
local input = self.game.input
|
||||
if input:wasPressed("up") then
|
||||
-- wMenuWrappingEnabled is never set here, so both ends are hard stops
|
||||
if self.row > 1 then self.row = self.row - 1 end
|
||||
elseif input:wasPressed("down") then
|
||||
if self.row < BOARD_ROWS then self.row = self.row + 1 end
|
||||
elseif input:wasPressed("left") then
|
||||
self.col = 1
|
||||
elseif input:wasPressed("right") then
|
||||
self.col = 2
|
||||
elseif input:wasPressed("a") or input:wasPressed("b") then
|
||||
-- HandleMenuInput_ (home/window.asm) beeps for the PAD_A | PAD_B branch,
|
||||
-- and B and QUIT share .exitBlackboard
|
||||
require("src.core.Sound").play(self.game.data, "Press_AB")
|
||||
local sel = self:selection()
|
||||
if input:wasPressed("b") or sel > #STATUS_LABELS then
|
||||
self.onQuit()
|
||||
else
|
||||
self.onPick(sel)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function StatusBoard:draw()
|
||||
Font.drawBox(0, 0, 12, 8)
|
||||
love.graphics.setColor(0, 0, 0, 1)
|
||||
for i, label in ipairs(BOARD_LABELS) do
|
||||
local col = i <= BOARD_ROWS and 1 or 2
|
||||
local row = i - (col - 1) * BOARD_ROWS
|
||||
Font.draw(label, BOARD_COL_X[col] * 8, (BOARD_ROW_Y + row - 1) * 8)
|
||||
end
|
||||
-- wTopMenuItemX equals the column PlaceString started at, so the cursor
|
||||
-- covers the blank each label leads with
|
||||
Font.drawCode(Theme.cursor, BOARD_COL_X[self.col] * 8,
|
||||
(BOARD_ROW_Y + self.row - 1) * 8)
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
end
|
||||
|
||||
local function blackboard(game)
|
||||
local text = game.data.text or {}
|
||||
local openBoard
|
||||
-- wCurrentMenuItem / wMenuItemOffset are zeroed once, above .blackboardLoop,
|
||||
-- and nothing inside the loop clears them again: after a status blurb
|
||||
-- `jp .blackboardLoop` comes back with the cursor still on the row and
|
||||
-- column the player just picked. One StatusBoard lives for the whole
|
||||
-- reading and is re-pushed each pass, so only entering the blackboard
|
||||
-- resets to the left column / top row (#591).
|
||||
local board
|
||||
-- .blackboardLoop reprints ViridianSchoolBlackboardText2 and only then
|
||||
-- calls HandleMenuInput, so the prompt is on screen for exactly as long as
|
||||
-- the headings list is. That text ends in `done`, not `prompt`
|
||||
-- (data/text/text_2.asm:646), so PrintText returns with the box still up
|
||||
-- and never waits for a button: TextBox opts.stay holds it open under the
|
||||
-- list and these callbacks pop the pair together (#591).
|
||||
local function closeBoard()
|
||||
game.stack:pop() -- the headings list
|
||||
game.stack:pop() -- the held "Which heading" box under it
|
||||
end
|
||||
local function pick(i)
|
||||
closeBoard()
|
||||
game.stack:push(TextBox.new(game,
|
||||
text[STATUS_LABELS[i][2]] or STATUS_LABELS[i][1], openBoard))
|
||||
end
|
||||
function openBoard()
|
||||
game.stack:push(TextBox.new(game,
|
||||
text._ViridianSchoolBlackboardText2 or "Which heading do\nyou want to read?",
|
||||
nil, { stay = { onShown = function()
|
||||
board = board or StatusBoard.new(game, pick, closeBoard)
|
||||
game.stack:push(board)
|
||||
end } }))
|
||||
end
|
||||
game.stack:push(TextBox.new(game,
|
||||
text._ViridianSchoolBlackboardText1
|
||||
or "The blackboard\ndescribes POKéMON\vSTATUS changes\vduring battles.",
|
||||
openBoard))
|
||||
end
|
||||
|
||||
-- ViridianSchoolNotebook (engine/events/hidden_events/school_notebooks.asm):
|
||||
-- pages 1-3 each end in TurnPageSchoolNotebook (TurnPageText + YesNoChoice)
|
||||
-- and NO stops the read; page 4 turns without asking and runs straight into
|
||||
-- page 5, the girl catching you at it.
|
||||
local function notebook(game)
|
||||
local text = game.data.text or {}
|
||||
local function page(n, after)
|
||||
return TextBox.new(game, text["_ViridianSchoolNotebookText" .. n] or "", after)
|
||||
end
|
||||
local function turnPage(nextPage)
|
||||
return function()
|
||||
game.stack:push(TextBox.new(game, text._TurnPageText or "Turn the page?",
|
||||
nil, { choice = function(yes)
|
||||
if yes then game.stack:push(nextPage()) end
|
||||
end }))
|
||||
end
|
||||
end
|
||||
local function page5() return page(5) end
|
||||
local function page4() return page(4, function() game.stack:push(page5()) end) end
|
||||
local function page3() return page(3, turnPage(page4)) end
|
||||
local function page2() return page(2, turnPage(page3)) end
|
||||
game.stack:push(page(1, turnPage(page2)))
|
||||
end
|
||||
|
||||
return {
|
||||
VIRIDIAN_SCHOOL_HOUSE = {
|
||||
onInteract = function(game, ow, fx, fy)
|
||||
if fx == 3 and fy == 0 then
|
||||
blackboard(game)
|
||||
return true
|
||||
end
|
||||
if fx == 3 and fy == 4 then
|
||||
notebook(game)
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end,
|
||||
},
|
||||
}
|
||||
@@ -15,6 +15,7 @@ local files = {
|
||||
"data.scripts.flavor.cerulean_trashed_house",
|
||||
"data.scripts.flavor.copycats_house_1f",
|
||||
"data.scripts.flavor.copycats_house_2f",
|
||||
"data.scripts.flavor.fuchsia_city",
|
||||
"data.scripts.flavor.game_corner",
|
||||
"data.scripts.flavor.lavender_cubone_house",
|
||||
"data.scripts.flavor.lavender_mart",
|
||||
@@ -60,6 +61,7 @@ local files = {
|
||||
"data.scripts.flavor.victory_road_2f",
|
||||
"data.scripts.flavor.viridian_city",
|
||||
"data.scripts.flavor.viridian_nickname_house",
|
||||
"data.scripts.flavor.viridian_school_house", -- #503
|
||||
"data.scripts.flavor.wardens_house",
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,26 @@ local M = {
|
||||
VIRIDIAN_GYM = { city = "VIRIDIAN CITY", leader = "GIOVANNI", badge = "EARTHBADGE" },
|
||||
}
|
||||
|
||||
-- The originals' middle branch shared by every leader's text_asm: beaten
|
||||
-- but EVENT_GOT_TM* unset means the bag was full when the victory script
|
||||
-- ran GiveItem, so talking to the leader re-runs the ReceiveTM script.
|
||||
-- Returns true when the retry took over the talk. A save from before
|
||||
-- #797 already holds the TM without the flag; treat the owned TM as
|
||||
-- received so those saves fall through to the advice text instead of
|
||||
-- collecting a second copy.
|
||||
local function retryTmGive(game, ow, victoryKey, done)
|
||||
local reward = require("data.scripts.victories")[victoryKey]
|
||||
if not (reward and reward.gotFlag) then return false end
|
||||
if game.save.flags[reward.gotFlag] then return false end
|
||||
local owned = game.save.inventory and game.save.inventory[reward.item] or 0
|
||||
if owned > 0 then
|
||||
game.save.flags[reward.gotFlag] = true
|
||||
return false
|
||||
end
|
||||
ow:offerGymTm(reward, done)
|
||||
return true
|
||||
end
|
||||
|
||||
-- scripts/PewterGym.asm PewterGymBrockText (text_asm): CheckEvent
|
||||
-- EVENT_BEAT_BROCK branches his dialogue. Before the badge he prints
|
||||
-- _PewterGymBrockPreBattleText and engages the leader battle
|
||||
@@ -25,12 +45,14 @@ local M = {
|
||||
-- badge/TM34 rewards and EVENT_BEAT_BROCK come from
|
||||
-- data/scripts/victories.lua OPP_BROCK#1). After the badge his
|
||||
-- .afterBeat branch prints _PewterGymBrockPostBattleAdviceText ("Go to
|
||||
-- the GYM in CERULEAN..."). The original's middle branch (beat but
|
||||
-- TM34 not yet handed over, CheckEventReuseA EVENT_GOT_TM34) is
|
||||
-- unreachable in the port: the TM is granted with the victory.
|
||||
-- the GYM in CERULEAN..."). The middle branch (beat but TM34 not yet
|
||||
-- handed over, CheckEventReuseA EVENT_GOT_TM34 -> call
|
||||
-- PewterGymScriptReceiveTM34) retries the TM give when the bag was full
|
||||
-- at the victory (#797).
|
||||
M.PEWTER_GYM.talk = {
|
||||
TEXT_PEWTERGYM_BROCK = function(game, ow, npc, done)
|
||||
if game.save.flags.EVENT_BEAT_BROCK then
|
||||
if retryTmGive(game, ow, "OPP_BROCK#1", done) then return end
|
||||
local TextBox = require("src.render.TextBox")
|
||||
game.stack:push(TextBox.new(game,
|
||||
game.data.text._PewterGymBrockPostBattleAdviceText
|
||||
@@ -48,16 +70,17 @@ M.PEWTER_GYM.talk = {
|
||||
-- (engageTrainer shows that same pre-battle text via resolveText; the
|
||||
-- badge/TM rewards and the beat flag come from data/scripts/victories.lua)
|
||||
-- -- and once beaten print the post-battle advice text. As with Brock,
|
||||
-- the originals' middle branch (beaten but the TM not yet handed over,
|
||||
-- CheckEventReuseA EVENT_GOT_TM*) is unreachable in the port: the TM is
|
||||
-- granted with the victory.
|
||||
-- the middle branch (beaten but the TM not yet handed over,
|
||||
-- CheckEventReuseA EVENT_GOT_TM*) retries the TM give when the bag was
|
||||
-- full at the victory.
|
||||
-- afterAdvice, when given, takes over `done`: it is handed (game, ow, npc,
|
||||
-- done) and must call done() itself once whatever it's doing (e.g. a fade
|
||||
-- around a HideObject) finishes, rather than having it invoked
|
||||
-- automatically. Only Giovanni's farewell uses this.
|
||||
local function leaderTalk(beatFlag, adviceLabel, fallback, afterAdvice)
|
||||
local function leaderTalk(beatFlag, adviceLabel, fallback, afterAdvice, victoryKey)
|
||||
return function(game, ow, npc, done)
|
||||
if game.save.flags[beatFlag] then
|
||||
if victoryKey and retryTmGive(game, ow, victoryKey, done) then return end
|
||||
local TextBox = require("src.render.TextBox")
|
||||
local finish = done
|
||||
if afterAdvice then
|
||||
@@ -79,42 +102,42 @@ end
|
||||
M.CERULEAN_GYM.talk = {
|
||||
TEXT_CERULEANGYM_MISTY = leaderTalk("EVENT_BEAT_MISTY",
|
||||
"_CeruleanGymMistyTM11ExplanationText",
|
||||
"TM11 teaches\nBUBBLEBEAM!"),
|
||||
"TM11 teaches\nBUBBLEBEAM!", nil, "OPP_MISTY#1"),
|
||||
}
|
||||
|
||||
-- scripts/VermilionGym.asm VermilionGymLTSurgeText .got_tm24_already
|
||||
M.VERMILION_GYM.talk = {
|
||||
TEXT_VERMILIONGYM_LT_SURGE = leaderTalk("EVENT_BEAT_LT_SURGE",
|
||||
"_VermilionGymLTSurgePostBattleAdviceText",
|
||||
"A little word of\nadvice, kid!"),
|
||||
"A little word of\nadvice, kid!", nil, "OPP_LT_SURGE#1"),
|
||||
}
|
||||
|
||||
-- scripts/CeladonGym.asm CeladonGymErikaText .afterBeat
|
||||
M.CELADON_GYM.talk = {
|
||||
TEXT_CELADONGYM_ERIKA = leaderTalk("EVENT_BEAT_ERIKA",
|
||||
"_CeladonGymErikaPostBattleAdviceText",
|
||||
"You are cataloging\nPOKéMON? I must\nsay I'm impressed."),
|
||||
"You are cataloging\nPOKéMON? I must\nsay I'm impressed.", nil, "OPP_ERIKA#1"),
|
||||
}
|
||||
|
||||
-- scripts/FuchsiaGym.asm FuchsiaGymKogaText .afterBeat
|
||||
M.FUCHSIA_GYM.talk = {
|
||||
TEXT_FUCHSIAGYM_KOGA = leaderTalk("EVENT_BEAT_KOGA",
|
||||
"_FuchsiaGymKogaPostBattleAdviceText",
|
||||
"When afflicted by\nTOXIC, POKéMON\nsuffer more and\nmore as battle\nprogresses!"),
|
||||
"When afflicted by\nTOXIC, POKéMON\nsuffer more and\nmore as battle\nprogresses!", nil, "OPP_KOGA#1"),
|
||||
}
|
||||
|
||||
-- scripts/SaffronGym.asm SaffronGymSabrinaText .afterBeat
|
||||
M.SAFFRON_GYM.talk = {
|
||||
TEXT_SAFFRONGYM_SABRINA = leaderTalk("EVENT_BEAT_SABRINA",
|
||||
"_SaffronGymSabrinaPostBattleAdviceText",
|
||||
"Everyone has\npsychic power!\nPeople just don't\nrealize it!"),
|
||||
"Everyone has\npsychic power!\nPeople just don't\nrealize it!", nil, "OPP_SABRINA#1"),
|
||||
}
|
||||
|
||||
-- scripts/CinnabarGym.asm CinnabarGymBlaineText .afterBeat
|
||||
M.CINNABAR_GYM.talk = {
|
||||
TEXT_CINNABARGYM_BLAINE = leaderTalk("EVENT_BEAT_BLAINE",
|
||||
"_CinnabarGymBlainePostBattleAdviceText",
|
||||
"FIRE BLAST is the\nultimate fire\ntechnique!"),
|
||||
"FIRE BLAST is the\nultimate fire\ntechnique!", nil, "OPP_BLAINE#1"),
|
||||
}
|
||||
|
||||
-- scripts/ViridianGym.asm ViridianGymGiovanniText .afterBeat: after the
|
||||
@@ -142,7 +165,7 @@ M.VIRIDIAN_GYM.talk = {
|
||||
"VIRIDIAN_GYM", "VIRIDIANGYM_GIOVANNI")
|
||||
end
|
||||
end, done))
|
||||
end),
|
||||
end, "OPP_GIOVANNI#3"),
|
||||
}
|
||||
|
||||
return M
|
||||
|
||||
@@ -12,11 +12,18 @@
|
||||
-- different files can each add NPCs to the same map), and mod
|
||||
-- contributions from the map_scripts registry compose on top of it.
|
||||
|
||||
local GameVersion = require("src.core.GameVersion")
|
||||
local MapScripts = require("src.script.MapScripts")
|
||||
|
||||
-- OaksLab is a full Yellow rewrite (one Eevee ball + forced Pikachu);
|
||||
-- Red/Blue keep the three-starter choose flow.
|
||||
local oaksLab = GameVersion.isYellow()
|
||||
and "data.scripts.oaks_lab_yellow"
|
||||
or "data.scripts.oaks_lab"
|
||||
|
||||
for _, mapEntry in ipairs({
|
||||
{ "PALLET_TOWN", "data.scripts.pallet_town" },
|
||||
{ "OAKS_LAB", "data.scripts.oaks_lab" },
|
||||
{ "OAKS_LAB", oaksLab },
|
||||
{ "REDS_HOUSE_1F", "data.scripts.reds_house" },
|
||||
{ "CELADON_MANSION_ROOF_HOUSE", "data.scripts.celadon_eevee" },
|
||||
}) do
|
||||
@@ -36,6 +43,19 @@ for _, file in ipairs({ "data.scripts.story", "data.scripts.story2",
|
||||
end
|
||||
end
|
||||
|
||||
-- Yellow-only content on top of the shared tables (talk keys merge per
|
||||
-- TEXT constant): the Kanto-starter gift quests and Jessie & James.
|
||||
if GameVersion.isYellow() then
|
||||
for _, file in ipairs({ "data.scripts.yellow_gifts",
|
||||
"data.scripts.yellow_jessie_james",
|
||||
"data.scripts.yellow_beach_house",
|
||||
"data.scripts.yellow_viridian_old_man" }) do
|
||||
for mapId, mod in pairs(require(file)) do
|
||||
MapScripts.attachBase(mapId, mod)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local M = {}
|
||||
|
||||
function M.get(mapId)
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
-- takes it ("I'll take this one, then!") and both balls disappear.
|
||||
-- Source: scripts/OaksLab.asm OaksLabCharmanderPokeBallText /
|
||||
-- OaksLabRivalTakePokeBallScript.
|
||||
-- * Leftover ball (after the pick): Oak turns and reads the last-mon
|
||||
-- line instead of re-offering the starter (OaksLabLastMonScript, #601).
|
||||
-- * Rival (object 1): before starter -> "go ahead and choose" once Oak
|
||||
-- has walked you in, else "gramps isn't around" (#218); with
|
||||
-- starter -> taunt + battle OPP_RIVAL1 with the counter-pick party
|
||||
@@ -20,10 +22,10 @@ local function starterBall(askText, species, choseFlag, ownBall,
|
||||
rivalBallX, rivalBall)
|
||||
return {
|
||||
{ "check_flag", "EVENT_GOT_STARTER" }, -- 1
|
||||
{ "jump_if_true", 20 }, -- 2
|
||||
{ "jump_if_true", 23 }, -- 2
|
||||
-- no picking until Oak has walked you in (OaksLabScript gating)
|
||||
{ "check_flag", "EVENT_FOLLOWED_OAK_INTO_LAB" }, -- 3
|
||||
{ "jump_if_false", 20 }, -- 4
|
||||
{ "jump_if_false", 26 }, -- 4
|
||||
-- the Pokédex "new species" entry shows before the ask (predef
|
||||
-- StarterDex ahead of OaksLabYouWant...Text). StarterDex temporarily
|
||||
-- sets the owned bits so ShowPokedexData prints height/weight/text;
|
||||
@@ -31,30 +33,39 @@ local function starterBall(askText, species, choseFlag, ownBall,
|
||||
{ "push_screen", "DexEntryMenu",
|
||||
{ species = species, forceOwned = true } }, -- 5
|
||||
{ "ask", askText }, -- 6
|
||||
{ "jump_if_false", 21 }, -- 7
|
||||
-- OaksLab.asm prints ReceivedMon then AddPartyMon (AskName lives
|
||||
-- inside give_pokemon). Show the received text first so the
|
||||
-- nickname prompt follows "you got X", matching Gen1.
|
||||
{ "show_text", "_OaksLabReceivedMonText", { RAM = species } }, -- 8
|
||||
{ "give_pokemon", species, 5 }, -- 9
|
||||
{ "set_flag", "EVENT_GOT_STARTER" }, -- 10
|
||||
{ "set_flag", choseFlag }, -- 11
|
||||
-- POKé BALLs are not handed out here in the original -- Oak gives
|
||||
-- them later, at OaksLabOak1Text's .give_poke_balls beat once the
|
||||
-- player has beaten the Route 22 rival (see TEXT_OAKSLAB_OAK1 below)
|
||||
{ "hide_object", "OAKS_LAB", ownBall }, -- 12
|
||||
{ "jump_if_false", "end" }, -- 7
|
||||
-- scripts/OaksLab.asm:919
|
||||
{ "show_text", "_OaksLabMonEnergeticText" }, -- 8
|
||||
-- OaksLab.asm: ReceivedMon (sound_get_key_item) then AddPartyMon; the
|
||||
-- jingle fires once the box has typed and holds it (#668)
|
||||
{ "text_sound", "Get_Key_Item" }, -- 9
|
||||
{ "show_text", "_OaksLabReceivedMonText", { RAM = species } }, -- 10
|
||||
{ "give_pokemon", species, 5 }, -- 11
|
||||
{ "set_flag", "EVENT_GOT_STARTER" }, -- 12
|
||||
{ "set_flag", choseFlag }, -- 13
|
||||
-- POKé BALLs come later, at OaksLabOak1Text's .give_poke_balls beat
|
||||
-- once the Route 22 rival is beaten (see TEXT_OAKSLAB_OAK1 below)
|
||||
{ "hide_object", "OAKS_LAB", ownBall }, -- 14
|
||||
-- the rival walks to the countering ball (around the furniture)
|
||||
{ "move_npc_to", 1, rivalBallX, 4 }, -- 13
|
||||
{ "face_object", 1, "up" }, -- 14
|
||||
{ "show_text", "_OaksLabRivalIllTakeThisOneText" }, -- 15
|
||||
{ "hide_object", "OAKS_LAB", rivalBall }, -- 16
|
||||
{ "move_npc_to", 1, rivalBallX, 4 }, -- 15
|
||||
{ "face_object", 1, "up" }, -- 16
|
||||
{ "show_text", "_OaksLabRivalIllTakeThisOneText" }, -- 17
|
||||
{ "hide_object", "OAKS_LAB", rivalBall }, -- 18
|
||||
{ "text_sound", "Get_Key_Item" }, -- 19 (sound_get_key_item)
|
||||
{ "show_text", "_OaksLabRivalReceivedMonText",
|
||||
{ RAM = rivalBall == "OAKSLAB_CHARMANDER_POKE_BALL" and "CHARMANDER"
|
||||
or rivalBall == "OAKSLAB_SQUIRTLE_POKE_BALL" and "SQUIRTLE"
|
||||
or "BULBASAUR" } }, -- 17
|
||||
{ "jump", 21 }, -- 18
|
||||
{ "jump", 21 }, -- 19 (spacer)
|
||||
{ "show_text", "_OaksLabThoseArePokeBallsText" }, -- 20
|
||||
or "BULBASAUR" } }, -- 20
|
||||
{ "jump", "end" }, -- 21
|
||||
{ "jump", "end" }, -- 22 (spacer)
|
||||
-- leftover ball: Oak reads the last-mon line (scripts/OaksLab.asm
|
||||
-- OaksLabSelectedPokeBallScript -> OaksLabLastMonScript, #601)
|
||||
{ "face_object", 5, "down" }, -- 23
|
||||
{ "show_text", "That's PROF.OAK's\nlast Pokémon!" }, -- 24
|
||||
-- OaksLabLastMonScript ends at TextScriptEnd; the port used to fall
|
||||
-- through into the pre-pick line below (#601 remnant, reported on #600)
|
||||
{ "jump", "end" }, -- 25
|
||||
{ "show_text", "_OaksLabThoseArePokeBallsText" }, -- 26
|
||||
}
|
||||
end
|
||||
|
||||
@@ -62,10 +73,22 @@ return {
|
||||
talk = {
|
||||
-- Oak: OaksLabOak1Text. Parcel delivery kicks SCRIPT_OAKSLAB_RIVAL_
|
||||
-- ARRIVES_AT_OAKS_REQUEST + OaksLabOakGivesPokedexScript (rival walk-
|
||||
-- in, full Pokédex speech, rival exit, Route 22 arm). Dex-rating
|
||||
-- (DisplayDexRating) is still skipped.
|
||||
-- in, full Pokédex speech, rival exit, Route 22 arm).
|
||||
TEXT_OAKSLAB_OAK1 = {
|
||||
{ "face_player" },
|
||||
-- OaksLabOak1Text leads with the dex-rating branch (#600): with
|
||||
-- EVENT_PALLET_AFTER_GETTING_POKEBALLS set (converted saves), or
|
||||
-- 2+ species owned once the Pokédex is in hand, Oak asks how it is
|
||||
-- coming and rates it (predef DisplayDexRating). Red keeps the
|
||||
-- GOT_POKEDEX gate that Yellow's copy of this text drops
|
||||
-- (data/scripts/oaks_lab_yellow.lua).
|
||||
{ "check_flag", "EVENT_PALLET_AFTER_GETTING_POKEBALLS" },
|
||||
{ "jump_if_true", "dex_rating" },
|
||||
{ "check_dex_owned", 2 },
|
||||
{ "jump_if_false", "no_rating" },
|
||||
{ "check_flag", "EVENT_GOT_POKEDEX" },
|
||||
{ "jump_if_true", "dex_rating" },
|
||||
{ "label", "no_rating" },
|
||||
{ "check_item", "POKE_BALL" },
|
||||
{ "jump_if_true", "come_see" },
|
||||
{ "check_flag", "EVENT_BEAT_ROUTE22_RIVAL_1ST_BATTLE" },
|
||||
@@ -77,8 +100,8 @@ return {
|
||||
{ "check_item", "OAKS_PARCEL" },
|
||||
{ "jump_if_false", "raise_young" },
|
||||
-- OaksLabOak1Text.got_parcel → RivalArrives + OakGivesPokedex
|
||||
{ "text_sound", "Get_Key_Item" },
|
||||
{ "show_text", "_OaksLabOak1DeliverParcelText" },
|
||||
{ "play_sound", "Get_Key_Item" },
|
||||
{ "show_text", "_OaksLabOak1ParcelThanksText" },
|
||||
{ "take_item", "OAKS_PARCEL", 1 },
|
||||
{ "stop_music" },
|
||||
@@ -99,8 +122,8 @@ return {
|
||||
{ "face_object", 1, "up" },
|
||||
{ "face_object", 5, "down" },
|
||||
{ "show_text", "_OaksLabOakMyInventionPokedexText" },
|
||||
{ "text_sound", "Get_Key_Item" },
|
||||
{ "show_text", "_OaksLabOakGotPokedexText" },
|
||||
{ "play_sound", "Get_Key_Item" },
|
||||
{ "hide_object", "OAKS_LAB", "OAKSLAB_POKEDEX1" },
|
||||
{ "hide_object", "OAKS_LAB", "OAKSLAB_POKEDEX2" },
|
||||
{ "face_object", 1, "up" },
|
||||
@@ -151,6 +174,14 @@ return {
|
||||
|
||||
{ "label", "come_see" },
|
||||
{ "show_text", "_OaksLabOak1ComeSeeMeSometimesText" },
|
||||
{ "jump", "end" },
|
||||
|
||||
-- .HowIsYourPokedexComingText ends on `prompt` and OaksLabOak1Text
|
||||
-- sets wDoNotWaitForButtonPressAfterDisplayingText, so the seen/owned
|
||||
-- tally follows with no button wait (engine/events/pokedex_rating.asm)
|
||||
{ "label", "dex_rating" },
|
||||
{ "show_text", "_OaksLabOak1HowIsYourPokedexComingText" },
|
||||
{ "dex_rating" },
|
||||
},
|
||||
|
||||
TEXT_OAKSLAB_CHARMANDER_POKE_BALL =
|
||||
@@ -209,9 +240,17 @@ return {
|
||||
-- the table sprites; re-entering the lab applies the same HideObject
|
||||
-- the gift script now does (OaksLab.asm OakGivesPokedex).
|
||||
onEnter = function(game, ow)
|
||||
if not (game.save.flags and game.save.flags.EVENT_GOT_POKEDEX) then
|
||||
return
|
||||
local flags = game.save.flags or {}
|
||||
if flags.EVENT_GOT_STARTER and not flags.EVENT_BATTLED_RIVAL_IN_OAKS_LAB then
|
||||
local rival = ow:npcByIndex(1)
|
||||
if rival then
|
||||
rival.cellX = flags.EVENT_CHOSE_CHARMANDER and 7
|
||||
or flags.EVENT_CHOSE_SQUIRTLE and 8 or 6
|
||||
rival.cellY = 4
|
||||
rival.px, rival.py = rival.cellX * 16, rival.cellY * 16
|
||||
end
|
||||
end
|
||||
if not flags.EVENT_GOT_POKEDEX then return end
|
||||
local Commands = require("src.script.Commands")
|
||||
local ctx = { save = game.save, game = game, overworld = ow }
|
||||
Commands.hide_object(ctx, "OAKS_LAB", "OAKSLAB_POKEDEX1")
|
||||
@@ -245,7 +284,14 @@ return {
|
||||
and y >= 6 then
|
||||
local rival = ow:npcByIndex(1)
|
||||
if not rival then return false end
|
||||
-- OaksLabRivalChallengesPlayerScript swaps in the rival encounter
|
||||
-- fanfare for the taunt/challenge exchange, same as the Yellow port
|
||||
-- (oaks_lab_yellow.lua); it was silently dropped here (#596).
|
||||
local rows = {
|
||||
{ "face_object", 1, "down" }, -- scripts/OaksLab.asm:347-351
|
||||
{ "face_player_dir", "up" },
|
||||
{ "stop_music" },
|
||||
{ "play_music", "Music_MeetRival" },
|
||||
{ "show_text", "_OaksLabRivalIllTakeYouOnText" }, -- 1
|
||||
}
|
||||
-- the rival routes to a free cell beside the player
|
||||
@@ -267,24 +313,22 @@ return {
|
||||
local base = #rows
|
||||
local party = flags.EVENT_CHOSE_BULBASAUR and 3
|
||||
or flags.EVENT_CHOSE_SQUIRTLE and 2 or 1
|
||||
table.insert(rows, { "save_end_battle_text", "_OaksLabRivalIPickedTheWrongPokemonText" })
|
||||
table.insert(rows, { "start_battle", "trainer", "OPP_RIVAL1", party })
|
||||
-- OaksLabRivalEndBattleScript: heal + flag on win or loss; no blackout
|
||||
table.insert(rows, { "heal_party" })
|
||||
table.insert(rows, { "set_flag", "EVENT_BATTLED_RIVAL_IN_OAKS_LAB" })
|
||||
-- OaksLabRivalEndBattleScript: on WIN, print the "picked the wrong
|
||||
-- POKéMON!" gloat, then BOTH win and loss print the shared exit line
|
||||
-- _OaksLabRivalSmellYouLaterText ("OK! I'll make my POKéMON fight to
|
||||
-- toughen it up!\012<PLAYER>! Gramps! Smell you later!") before Blue
|
||||
-- marches out. A loss skips only the gloat (that taunt was already
|
||||
-- shown in-battle via Rival1WinText), never the exit line (#231). The
|
||||
-- jump_if_false convergence point is the exit line: base+6 indexes the
|
||||
-- SmellYouLater row below, so WIN falls IPicked -> SmellYouLater and
|
||||
-- LOSS jumps straight to SmellYouLater (both then walk-out + hide).
|
||||
table.insert(rows, { "jump_if_false", base + 6 })
|
||||
table.insert(rows, { "show_text", "_OaksLabRivalIPickedTheWrongPokemonText" })
|
||||
table.insert(rows, { "show_text", "_OaksLabRivalSmellYouLaterText" })
|
||||
-- OaksLabRivalStartsExitScript: parting shot, rival exit fanfare, then
|
||||
-- walk out past the player. The fanfare was dropped here (#683) -- the
|
||||
-- parcel scene above already plays Music_MeetRival on both arrival and
|
||||
-- departure (lines 144-146), and this exit should match (#596).
|
||||
table.insert(rows, { "stop_music" })
|
||||
table.insert(rows, { "play_music", "Music_MeetRival", { start = "rival" } })
|
||||
table.insert(rows, { "move_npc_to", 1, 4, 11 })
|
||||
table.insert(rows, { "hide_object", "OAKS_LAB", "OAKSLAB_RIVAL" })
|
||||
table.insert(rows, { "play_music", "Music_OaksLab" })
|
||||
ow.runner:run(rows, { npc = rival })
|
||||
return true
|
||||
end
|
||||
|
||||
@@ -0,0 +1,322 @@
|
||||
-- Hand-ported from pret/pokeyellow scripts/OaksLab.asm.
|
||||
-- Yellow: one Eevee ball on the table. Rival snatches it; Oak then
|
||||
-- gives the player the wild Pikachu he caught earlier (STARTER_PIKACHU).
|
||||
-- Object indices differ from Red (no Charmander/Squirtle/Bulbasaur balls):
|
||||
-- 1 RIVAL (4,3), 2 EEVEE_POKE_BALL (7,3), 3 OAK1 (5,2),
|
||||
-- 4-5 POKEDEX (2,1)/(3,1), 6 OAK2 (door), 7 GIRL, 8-9 SCIENTIST.
|
||||
--
|
||||
-- wRivalStarter rides in save.rivalStarter (RIVAL_STARTER_* 1 JOLTEON /
|
||||
-- 2 FLAREON / 3 VAPOREON): JOLTEON baseline at the snatch
|
||||
-- (OaksLabRivalTakesPokeballScript), FLAREON on a lab win / VAPOREON on
|
||||
-- a lab loss (OaksLabRivalEndBattleScript), and Route 22's first battle
|
||||
-- upgrades FLAREON back to JOLTEON (Route22Rival1AfterBattleScript).
|
||||
|
||||
local OAK1 = 3
|
||||
local RIVAL = 1
|
||||
|
||||
return {
|
||||
talk = {
|
||||
TEXT_OAKSLAB_OAK1 = {
|
||||
{ "face_player" },
|
||||
-- Yellow's OaksLabOak1Text leads with the dex-rating branch: once
|
||||
-- EVENT_PALLET_AFTER_GETTING_POKEBALLS is set (converted saves) or
|
||||
-- 2+ species are owned, Oak asks how the Pokédex is coming and
|
||||
-- rates it (predef DisplayDexRating)
|
||||
{ "check_flag", "EVENT_PALLET_AFTER_GETTING_POKEBALLS" },
|
||||
{ "jump_if_true", "dex_rating" },
|
||||
{ "check_dex_owned", 2 },
|
||||
{ "jump_if_true", "dex_rating" },
|
||||
{ "check_item", "POKE_BALL" },
|
||||
{ "jump_if_true", "come_see" },
|
||||
{ "check_flag", "EVENT_BEAT_ROUTE22_RIVAL_1ST_BATTLE" },
|
||||
{ "jump_if_true", "give_balls" },
|
||||
{ "check_flag", "EVENT_GOT_POKEDEX" },
|
||||
{ "jump_if_true", "around_world" },
|
||||
{ "check_flag", "EVENT_BATTLED_RIVAL_IN_OAKS_LAB" },
|
||||
{ "jump_if_false", "pre_lab_battle" },
|
||||
{ "check_item", "OAKS_PARCEL" },
|
||||
{ "jump_if_false", "raise_young" },
|
||||
-- .DeliverParcelText: parcel handover, then the Pokédex scene
|
||||
-- (OaksLabRivalArrivesAtOaksRequestScript -> OakGivesPokedexScript)
|
||||
{ "text_sound", "Get_Key_Item" },
|
||||
{ "show_text", "_OaksLabOak1DeliverParcelText" },
|
||||
{ "show_text", "_OaksLabOak1ParcelThanksText" },
|
||||
{ "take_item", "OAKS_PARCEL", 1 },
|
||||
{ "stop_music" },
|
||||
{ "play_music", "Music_MeetRival" },
|
||||
{ "show_text", "_OaksLabRivalGrampsText" },
|
||||
-- callfar OaksLabPikachuMovementScript, before ShowObject (#1021)
|
||||
{ "pikachu_make_way" },
|
||||
{ "show_object", "OAKS_LAB", "OAKSLAB_RIVAL" },
|
||||
{ "place_npc", RIVAL, 4, 7, "up" },
|
||||
{ "move_npc_to", RIVAL, 4, 3 },
|
||||
{ "play_music", "Music_OaksLab" },
|
||||
{ "face_object", RIVAL, "up" },
|
||||
{ "face_object", OAK1, "down" },
|
||||
-- Yellow opens with the rival bragging, not Red's "what did you
|
||||
-- call me for" (OaksLabOakGivesPokedexScript text order)
|
||||
{ "show_text", "_OaksLabRivalMyPokemonHasGrownStrongerText" },
|
||||
{ "face_object", RIVAL, "up" },
|
||||
{ "face_object", OAK1, "down" },
|
||||
{ "show_text", "_OaksLabOakIHaveARequestText" },
|
||||
{ "face_object", RIVAL, "up" },
|
||||
{ "face_object", OAK1, "down" },
|
||||
{ "show_text", "_OaksLabOakMyInventionPokedexText" },
|
||||
{ "text_sound", "Get_Key_Item" },
|
||||
{ "show_text", "_OaksLabOakGotPokedexText" },
|
||||
{ "hide_object", "OAKS_LAB", "OAKSLAB_POKEDEX1" },
|
||||
{ "hide_object", "OAKS_LAB", "OAKSLAB_POKEDEX2" },
|
||||
{ "face_object", RIVAL, "up" },
|
||||
{ "face_object", OAK1, "down" },
|
||||
{ "show_text", "_OaksLabOakThatWasMyDreamText" },
|
||||
{ "face_object", RIVAL, "right" },
|
||||
{ "show_text", "_OaksLabRivalLeaveItAllToMeText" },
|
||||
{ "set_flag", "EVENT_GOT_POKEDEX" },
|
||||
{ "set_flag", "EVENT_OAK_GOT_PARCEL" },
|
||||
-- OaksLabOakGivesPokedexScript: HideObject TOGGLE_LYING_OLD_MAN /
|
||||
-- ShowObject TOGGLE_OLD_MAN_2 -- Yellow's tutorial old man stands
|
||||
-- on the sleeper's cell (18,9); the Red/Blue walker OLD_MAN at
|
||||
-- (17,5) never appears in Yellow (#617)
|
||||
{ "hide_object", "VIRIDIAN_CITY", "VIRIDIANCITY_OLD_MAN_SLEEPY" },
|
||||
{ "show_object", "VIRIDIAN_CITY", "VIRIDIANCITY_OLD_MAN2" },
|
||||
{ "stop_music" },
|
||||
{ "play_music", "Music_MeetRival" },
|
||||
{ "move_npc_to", RIVAL, 4, 7 },
|
||||
{ "hide_object", "OAKS_LAB", "OAKSLAB_RIVAL" },
|
||||
{ "play_music", "Music_OaksLab" },
|
||||
{ "set_flag", "EVENT_1ST_ROUTE22_RIVAL_BATTLE" },
|
||||
{ "clear_flag", "EVENT_2ND_ROUTE22_RIVAL_BATTLE" },
|
||||
{ "set_flag", "EVENT_ROUTE22_RIVAL_WANTS_BATTLE" },
|
||||
{ "show_object", "ROUTE_22", "ROUTE22_RIVAL1" },
|
||||
{ "jump", "end" },
|
||||
|
||||
{ "label", "raise_young" },
|
||||
-- Yellow: talk-to-it (starter Pikachu) instead of Red raise-young line.
|
||||
{ "show_text", "_OaksLabOak1YouShouldTalkToIt" },
|
||||
{ "jump", "end" },
|
||||
|
||||
{ "label", "pre_lab_battle" },
|
||||
{ "check_flag", "EVENT_GOT_STARTER" },
|
||||
{ "jump_if_true", "can_fight" },
|
||||
{ "show_text", "_OaksLabOak1GoAheadItsYours" },
|
||||
{ "jump", "end" },
|
||||
{ "label", "can_fight" },
|
||||
{ "show_text", "_OaksLabOak1YourPokemonCanFightText" },
|
||||
{ "jump", "end" },
|
||||
|
||||
{ "label", "around_world" },
|
||||
{ "show_text", "_OaksLabOak1PokemonAroundTheWorldText" },
|
||||
{ "jump", "end" },
|
||||
|
||||
{ "label", "give_balls" },
|
||||
{ "check_flag", "EVENT_GOT_POKEBALLS_FROM_OAK" },
|
||||
{ "jump_if_true", "come_see" },
|
||||
{ "set_flag", "EVENT_GOT_POKEBALLS_FROM_OAK" },
|
||||
{ "give_item", "POKE_BALL", 5, false },
|
||||
{ "text_sound", "Get_Key_Item" },
|
||||
{ "show_text", "_OaksLabOak1ReceivedPokeballsText" },
|
||||
{ "show_text", "_OaksLabGivePokeballsExplanationText" },
|
||||
{ "jump", "end" },
|
||||
|
||||
{ "label", "come_see" },
|
||||
{ "show_text", "_OaksLabOak1ComeSeeMeSometimesText" },
|
||||
{ "jump", "end" },
|
||||
|
||||
{ "label", "dex_rating" },
|
||||
{ "show_text", "_OaksLabOak1HowIsYourPokedexComingText" },
|
||||
{ "dex_rating" },
|
||||
},
|
||||
|
||||
-- OaksLabEeveePokeBallText / OaksLabRivalExclamationScript ->
|
||||
-- OaksLabChoseStarterScript..OaksLabPlayerReceivesPikachuScript:
|
||||
-- before Oak's choose speech the ball is just flavor; after it, the
|
||||
-- rival "!"s, shoves the player off the table, snatches the ball,
|
||||
-- then the player is walked over to Oak and handed Pikachu.
|
||||
TEXT_OAKSLAB_EEVEE_POKE_BALL = function(game, ow, npc, done)
|
||||
local flags = game.save.flags
|
||||
if flags.EVENT_GOT_STARTER then
|
||||
done()
|
||||
return
|
||||
end
|
||||
if not flags.EVENT_OAK_ASKED_TO_CHOOSE_MON then
|
||||
ow.runner:run({
|
||||
{ "show_text", "_OaksLabThatsAPokeball" },
|
||||
}, { onDone = done })
|
||||
return
|
||||
end
|
||||
local px, py = ow.player.cellX, ow.player.cellY
|
||||
local rows = {
|
||||
-- OaksLabRivalExclamationScript: "!" over the rival
|
||||
{ "emote", RIVAL, "shock" },
|
||||
}
|
||||
-- .RivalPushesPlayerAwayFromEeveeBall is DOWN then RIGHT x3 (the $07
|
||||
-- bytes are Yellow's own step-right encoding, decoded by
|
||||
-- engine/overworld/movement.asm Func_5288 -> Func_532b), and the
|
||||
-- PAD_RIGHT x2 shove is NOT queued alongside it:
|
||||
-- OaksLabRivalTakesPokeballScript .asm_1c564 polls every frame and
|
||||
-- only simulates the pair once wNPCNumScriptedSteps reads 1 -- i.e.
|
||||
-- as the rival begins the LAST byte, the step onto the tile the
|
||||
-- player is standing on. Starting both on one row had Red stroll
|
||||
-- off the Eevee while the rival was still at the top of the table
|
||||
-- (#559).
|
||||
if py == 4 then
|
||||
rows[#rows + 1] = { "walk_npc", RIVAL, { "down", "right", "right" } }
|
||||
-- this one runs concurrently with the shove below
|
||||
rows[#rows + 1] = { "walk_npc", RIVAL, { "right" }, { wait = false } }
|
||||
rows[#rows + 1] = { "face_player_dir", "left" }
|
||||
rows[#rows + 1] = { "move_player", "right", 2 }
|
||||
-- move_player blocks for both tiles, so the rival has already
|
||||
-- landed on (7,4); this is just the beat before he turns up
|
||||
rows[#rows + 1] = { "wait", 20 }
|
||||
else
|
||||
rows[#rows + 1] = { "move_npc_to", RIVAL, 7, 4 }
|
||||
end
|
||||
rows[#rows + 1] = { "face_object", RIVAL, "up" }
|
||||
rows[#rows + 1] = { "hide_object", "OAKS_LAB", "OAKSLAB_EEVEE_POKE_BALL" }
|
||||
-- rival starter baseline (RIVAL_STARTER_JOLTEON) at snatch time
|
||||
rows[#rows + 1] = { "set_field", "rivalStarter", 1 }
|
||||
rows[#rows + 1] = { "show_text", "_OaksLabRivalTakesText1" }
|
||||
rows[#rows + 1] = { "text_sound", "Get_Key_Item" }
|
||||
rows[#rows + 1] = { "show_text", "_OaksLabRivalTakesText2" }
|
||||
rows[#rows + 1] = { "show_text", "_OaksLabRivalTakesText3" }
|
||||
rows[#rows + 1] = { "show_text", "_OaksLabRivalTakesText4" }
|
||||
rows[#rows + 1] = { "show_text", "_OaksLabRivalTakesText5" }
|
||||
-- OaksLabRLE_PlayerWalksToOak: the simulated-joypad buffer plays
|
||||
-- its RLE list BACKWARDS (StartSimulatingJoypadStates consumes
|
||||
-- from wSimulatedJoypadStatesEnd), so the real order is LEFT 1,
|
||||
-- DOWN 1, LEFT 3, UP 2 -- from the shove spot (9,4) the player
|
||||
-- rounds the BOTTOM of the table to (5,3), directly below Oak
|
||||
if py == 4 then
|
||||
rows[#rows + 1] = { "walk_npc", "player",
|
||||
{ "left", "down", "left", "left", "left", "up", "up" } }
|
||||
else
|
||||
rows[#rows + 1] = { "walk_npc", "player", { "left" } }
|
||||
end
|
||||
rows[#rows + 1] = { "face_player_dir", "up" }
|
||||
rows[#rows + 1] = { "face_object", OAK1, "down" }
|
||||
-- OaksLabPlayerReceivedMonText clears wMonDataLocation, so AskName runs (#1013)
|
||||
rows[#rows + 1] = { "show_text", "_OaksLabOakGivesText" }
|
||||
rows[#rows + 1] = { "text_sound", "Get_Key_Item" }
|
||||
rows[#rows + 1] = { "show_text", "_OaksLabReceivedText", { RAM = "PIKACHU" } }
|
||||
rows[#rows + 1] = { "give_pokemon", "PIKACHU", 5 }
|
||||
-- DisablePikachuOverworldSpriteDrawing keeps it in the ball (#1009)
|
||||
rows[#rows + 1] = { "set_field", "pikachuInBall", true }
|
||||
rows[#rows + 1] = { "set_flag", "EVENT_GOT_STARTER" }
|
||||
rows[#rows + 1] = { "set_flag", "EVENT_CHOSE_PIKACHU" }
|
||||
ow.runner:run(rows, { npc = npc, onDone = done,
|
||||
checkpointOnDone = "release_npc" })
|
||||
end,
|
||||
|
||||
TEXT_OAKSLAB_RIVAL = {
|
||||
{ "face_player" },
|
||||
{ "check_flag", "EVENT_GOT_STARTER" },
|
||||
{ "jump_if_false", "pre_starter" },
|
||||
{ "show_text", "_OaksLabRivalMyPokemonLooksStrongerText" },
|
||||
{ "jump", "end" },
|
||||
|
||||
{ "label", "pre_starter" },
|
||||
{ "check_flag", "EVENT_FOLLOWED_OAK_INTO_LAB_2" },
|
||||
{ "jump_if_false", "gramps_gone" },
|
||||
{ "show_text", "_OaksLabRivalIllGetABetterPokemonThanYou" },
|
||||
{ "jump", "end" },
|
||||
|
||||
{ "label", "gramps_gone" },
|
||||
{ "show_text", "_OaksLabRivalGrampsIsntAroundText" },
|
||||
},
|
||||
},
|
||||
|
||||
onEnter = function(game, ow)
|
||||
local flags = game.save.flags or {}
|
||||
if flags.EVENT_GOT_STARTER and not flags.EVENT_BATTLED_RIVAL_IN_OAKS_LAB then
|
||||
local rival = ow:npcByIndex(RIVAL)
|
||||
if rival then
|
||||
rival.cellX, rival.cellY = 7, 4
|
||||
rival.px, rival.py = 7 * 16, 4 * 16
|
||||
end
|
||||
end
|
||||
if not flags.EVENT_GOT_POKEDEX then return end
|
||||
local Commands = require("src.script.Commands")
|
||||
local ctx = { save = game.save, game = game, overworld = ow }
|
||||
Commands.hide_object(ctx, "OAKS_LAB", "OAKSLAB_POKEDEX1")
|
||||
Commands.hide_object(ctx, "OAKS_LAB", "OAKSLAB_POKEDEX2")
|
||||
end,
|
||||
|
||||
onStep = function(game, ow, x, y)
|
||||
local flags = game.save.flags
|
||||
-- OaksLabPlayerDontGoAwayScript: y==6 without the starter walks the
|
||||
-- player back up a tile
|
||||
if flags.EVENT_FOLLOWED_OAK_INTO_LAB and not flags.EVENT_GOT_STARTER
|
||||
and y >= 6 then
|
||||
ow.runner:run({
|
||||
{ "face_object", OAK1, "down" },
|
||||
{ "face_object", RIVAL, "down" },
|
||||
{ "show_text", "_OaksLabOakDontGoAwayYetText" },
|
||||
{ "move_player", "up", 1 },
|
||||
}, {})
|
||||
return true
|
||||
end
|
||||
-- OaksLabRivalChallengesPlayerScript..OaksLabPikachuDislikesPokeballsScript:
|
||||
-- heading for the door with Pikachu starts the rival battle, his
|
||||
-- exit walk, then Pikachu pops out of its ball.
|
||||
if flags.EVENT_GOT_STARTER and not flags.EVENT_BATTLED_RIVAL_IN_OAKS_LAB
|
||||
and y >= 6 then
|
||||
local rival = ow:npcByIndex(RIVAL)
|
||||
if not rival then return false end
|
||||
local rows = {
|
||||
{ "face_object", RIVAL, "down" }, -- pokeyellow scripts/OaksLab.asm:311-315
|
||||
{ "face_player_dir", "up" },
|
||||
{ "stop_music" },
|
||||
{ "play_music", "Music_MeetRival" },
|
||||
{ "show_text", "_OaksLabRivalIllTakeYouOnText" },
|
||||
}
|
||||
-- FindPathToPlayer with the Y distance decremented: the rival
|
||||
-- stops one tile above the player
|
||||
local target
|
||||
for _, c in ipairs({ { x, y - 1 }, { x - 1, y }, { x + 1, y },
|
||||
{ x, y + 1 } }) do
|
||||
if ow.map:inBounds(c[1], c[2]) and ow.map:isWalkableCell(c[1], c[2]) then
|
||||
target = c
|
||||
break
|
||||
end
|
||||
end
|
||||
if target then
|
||||
table.insert(rows, { "move_npc_to", RIVAL, target[1], target[2] })
|
||||
end
|
||||
table.insert(rows, { "face_object", RIVAL,
|
||||
target and target[2] < y and "down"
|
||||
or target and target[2] > y and "up"
|
||||
or target and target[1] < x and "right" or "left" })
|
||||
table.insert(rows, { "start_battle", "trainer", "OPP_RIVAL1", 1 })
|
||||
table.insert(rows, { "heal_party" })
|
||||
table.insert(rows, { "set_flag", "EVENT_BATTLED_RIVAL_IN_OAKS_LAB" })
|
||||
-- OaksLabRivalEndBattleScript: his Eevee's future evolution is
|
||||
-- decided here -- FLAREON if the player won, VAPOREON otherwise
|
||||
table.insert(rows, { "check_battle_result", "win" })
|
||||
table.insert(rows, { "jump_if_false", "lost_lab" })
|
||||
table.insert(rows, { "set_field", "rivalStarter", 2 })
|
||||
table.insert(rows, { "jump", "exit" })
|
||||
table.insert(rows, { "label", "lost_lab" })
|
||||
table.insert(rows, { "set_field", "rivalStarter", 3 })
|
||||
table.insert(rows, { "label", "exit" })
|
||||
-- OaksLabRivalStartsExitScript: parting shot, rival exit fanfare, then
|
||||
-- walk out past the player (#683).
|
||||
table.insert(rows, { "wait", 20 })
|
||||
table.insert(rows, { "show_text", "_OaksLabRivalSmellYouLaterText" })
|
||||
table.insert(rows, { "stop_music" })
|
||||
table.insert(rows, { "play_music", "Music_MeetRival", { start = "rival" } })
|
||||
table.insert(rows, { "move_npc_to", RIVAL, 4, 11 })
|
||||
table.insert(rows, { "hide_object", "OAKS_LAB", "OAKSLAB_RIVAL" })
|
||||
table.insert(rows, { "play_music", "Music_OaksLab" })
|
||||
-- OaksLabPikachuEscapesPokeballScript: the follower reaches the map (#1009)
|
||||
table.insert(rows, { "face_player_dir", "up" })
|
||||
table.insert(rows, { "set_field", "pikachuInBall", false })
|
||||
table.insert(rows, { "spawn_pikachu_follower" })
|
||||
table.insert(rows, { "play_cry", "PIKACHU" })
|
||||
table.insert(rows, { "show_text", "_OaksLabPikachuDislikesPokeballsText1" })
|
||||
table.insert(rows, { "show_text", "_OaksLabPikachuDislikesPokeballsText2" })
|
||||
ow.runner:run(rows, { npc = rival })
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end,
|
||||
}
|
||||
@@ -4,9 +4,11 @@
|
||||
-- (.PlayerNextToSafariZoneWorker1CoordsArray). Paying ¥500 hands over
|
||||
-- 30 SAFARI BALLs and starts the 502-step game
|
||||
-- (SafariZoneGateWouldYouLikeToJoinScript: wSafariSteps = 502,
|
||||
-- wNumSafariBalls = SAFARI_BALLS_RECEIVED). Declining walks you back
|
||||
-- so you can't slip past. Returning to the gate ends the game and the
|
||||
-- worker takes the leftover balls back.
|
||||
-- wNumSafariBalls = SAFARI_BALLS_RECEIVED), then auto-walks the player
|
||||
-- up through the north warp into the zone. Declining walks you back
|
||||
-- so you can't slip past. Returning to the gate ends the game, the
|
||||
-- worker takes the leftover balls back and the auto-walk drops you 3
|
||||
-- cells below the warp you came in by (#540).
|
||||
--
|
||||
-- Step/ball bookkeeping lives in src/world/OverworldController.lua
|
||||
-- (safariStep/safariGameOver, from
|
||||
@@ -19,17 +21,87 @@ local FEE = 500
|
||||
local BALLS = 30
|
||||
local STEPS = 502
|
||||
|
||||
local function startGame(game, t, done)
|
||||
game.save.money = game.save.money - FEE
|
||||
game.save.safari = { balls = BALLS, steps = STEPS }
|
||||
-- SafariZoneGateSafariZoneWorker1WouldYouLikeToJoinText .success closes with
|
||||
-- `ld a, PAD_UP / ld c, 3 / SafariZoneEntranceAutoWalk`: paying walks the
|
||||
-- player up out of the gate and through the north warp, it is never left to
|
||||
-- the player. EVENT_IN_SAFARI_ZONE is already set when that walk runs, so
|
||||
-- the two gate steps taken before the warp fires are charged against
|
||||
-- wSafariSteps (home/overworld.asm:307-310) -- which is why the counter
|
||||
-- reads 500/500 on arrival even though the script wrote 502 (#540). The
|
||||
-- port's counter only runs on the nine interior maps (FieldDefaults
|
||||
-- safari.stepMaps, OverworldState:inSafariStepZone), so charge those two
|
||||
-- steps here instead.
|
||||
local function walkIntoZone(game, ow)
|
||||
local p = ow.player
|
||||
-- only from the two trigger cells in front of the worker, which are the
|
||||
-- columns the north warps sit on; a player who paid after TALKING to him
|
||||
-- from somewhere else walks in on their own, as they do today
|
||||
local w = p.cellY == 2 and ow.map:warpAtCell(p.cellX, 0) or nil
|
||||
if not w then return end
|
||||
ow:scriptMove(p, "up", 2, function()
|
||||
local st = game.save.safari
|
||||
if st then st.steps = st.steps - 2 end
|
||||
-- scripted steps skip onStepComplete (and with it CheckWarpsNoCollision),
|
||||
-- so take that warp explicitly once the walk lands on it
|
||||
ow:takeWarp(w.def)
|
||||
end)
|
||||
end
|
||||
|
||||
local function startGame(game, ow, t, done, balls, introText)
|
||||
game.save.safari = { balls = balls or BALLS, steps = STEPS }
|
||||
game.save.safariNags = nil
|
||||
local TextBox = require("src.render.TextBox")
|
||||
local paid = (t._SafariZoneGateSafariZoneWorker1ThatllBe500PleaseText
|
||||
or "That'll be ¥500\nplease!\f{PLAYER} received\n30 SAFARI BALLs!")
|
||||
:gsub("{PLAYER}", game.save.player.name)
|
||||
local paid = introText
|
||||
or (t._SafariZoneGateSafariZoneWorker1ThatllBe500PleaseText
|
||||
or "That'll be ¥500\nplease!\f{PLAYER} received\n30 SAFARI BALLs!")
|
||||
:gsub("{NUM:[^}]*}", "500")
|
||||
paid = paid:gsub("{PLAYER}", game.save.player.name)
|
||||
local pa = t._SafariZoneGateSafariZoneWorker1CallYouOnThePAText
|
||||
or "\fWe'll call you on\nthe PA when you\nrun out of time\nor SAFARI BALLs!"
|
||||
local luck = t._SafariZoneGateSafariZoneWorker1GoodLuckText or "Good Luck!"
|
||||
game.stack:push(TextBox.new(game, paid .. pa .. "\f" .. luck, done))
|
||||
game.stack:push(TextBox.new(game, paid .. pa .. "\f" .. luck, function()
|
||||
if done then done() end
|
||||
walkIntoZone(game, ow)
|
||||
end))
|
||||
end
|
||||
|
||||
-- Yellow's soft-lock fix (scripts/SafariZoneGate_2.asm): a player short of
|
||||
-- the full fee still gets in.
|
||||
-- 0 < money < 500: SafariZoneEntranceCalculateLowCostAdmission takes
|
||||
-- everything and hands over min(money/23 + 1, 29) balls.
|
||||
-- money == 0: SafariZoneEntranceGetLowCostAdmissionText nags four times
|
||||
-- (LowCostText5/6/7/8), then relents -- free entry, one ball.
|
||||
local function yellowLowCost(game, ow, t, done, back)
|
||||
local TextBox = require("src.render.TextBox")
|
||||
if game.save.money > 0 then
|
||||
local balls = math.min(math.floor(game.save.money / 23) + 1, 29)
|
||||
game.save.money = 0
|
||||
local intro =
|
||||
(t._SafariZoneGateSafariZoneWorker1NotEnoughMoneyText
|
||||
or "Oops! Not enough\nmoney!")
|
||||
.. (t._SafariZoneLowCostText1
|
||||
or "\fOh, all right, pay\nme what you have.")
|
||||
.. "\f" .. (t._SafariZoneLowCostText2
|
||||
or "But, I can't give\nyou all 30 BALLs.")
|
||||
startGame(game, ow, t, done, balls, intro)
|
||||
return
|
||||
end
|
||||
local nag = game.save.safariNags or 0
|
||||
game.save.safariNags = nag + 1
|
||||
if nag >= 3 then
|
||||
local intro =
|
||||
(t._SafariZoneLowCostText8 or "Read my lips, NO!\nGet it?")
|
||||
.. (t._SafariZoneLowCostText3
|
||||
or "\fYou're persistent,\naren't you?\fOK, you can go in\nfor free, but\njust this once!")
|
||||
startGame(game, ow, t, done, 1, intro)
|
||||
return
|
||||
end
|
||||
local nags = {
|
||||
t._SafariZoneLowCostText5 or "I'm sorry, but you\nhave to pay to\nenter.",
|
||||
t._SafariZoneLowCostText6 or "You can't enter\nwithout paying!",
|
||||
t._SafariZoneLowCostText7 or "I said, no money,\nno entry!",
|
||||
}
|
||||
back(nags[nag + 1])
|
||||
end
|
||||
|
||||
local function joinPrompt(game, ow, done)
|
||||
@@ -51,10 +123,15 @@ local function joinPrompt(game, ow, done)
|
||||
back(t._SafariZoneGateSafariZoneWorker1PleaseComeAgainText
|
||||
or "OK! Please come\nagain!")
|
||||
elseif game.save.money < FEE then
|
||||
back(t._SafariZoneGateSafariZoneWorker1NotEnoughMoneyText
|
||||
or "Oops! Not enough\nmoney!")
|
||||
if require("src.core.GameVersion").isYellow() then
|
||||
yellowLowCost(game, ow, t, done, back)
|
||||
else
|
||||
back(t._SafariZoneGateSafariZoneWorker1NotEnoughMoneyText
|
||||
or "Oops! Not enough\nmoney!")
|
||||
end
|
||||
else
|
||||
startGame(game, t, done)
|
||||
game.save.money = game.save.money - FEE
|
||||
startGame(game, ow, t, done)
|
||||
end
|
||||
end))
|
||||
end))
|
||||
@@ -89,27 +166,38 @@ M.SAFARI_ZONE_GATE = {
|
||||
-- no walks you back into the zone
|
||||
onEnter = function(game, ow)
|
||||
if not game.save.safari or ow.player.cellY > 1 then return end
|
||||
local TextBox = require("src.render.TextBox")
|
||||
local ChoiceBox = require("src.ui.ChoiceBox")
|
||||
local t = game.data.text
|
||||
game.stack:push(TextBox.new(game,
|
||||
t._SafariZoneGateSafariZoneWorker1LeavingEarlyText or "Leaving early?",
|
||||
function()
|
||||
game.stack:push(ChoiceBox.new(game, function(yes)
|
||||
if not yes then
|
||||
-- back into the zone through the entrance warp
|
||||
local w = game.data.maps.SAFARI_ZONE_CENTER.warps[1]
|
||||
ow:startWarpTo("SAFARI_ZONE_CENTER", w.x, w.y, "up")
|
||||
return
|
||||
end
|
||||
game.save.safari = nil
|
||||
game.stack:push(TextBox.new(game,
|
||||
(t._SafariZoneGateSafariZoneWorker1ReturnSafariBallsText
|
||||
or "Please return any\nSAFARI BALLs you\nhave left.")
|
||||
.. "\f" .. (t._SafariZoneGateSafariZoneWorker1GoodHaulComeAgainText
|
||||
or "Did you get a\ngood haul?\fCome again!")))
|
||||
end))
|
||||
end))
|
||||
-- QUEUED, never pushed: onEnter runs inside the arriving warp's
|
||||
-- Transition midpoint, and Transition:finish pops whatever is on top
|
||||
-- the same frame (Timing.WARP_FADE_IN is 0) -- so a box pushed here is
|
||||
-- swallowed, and on a build where it survived it drew over a screen
|
||||
-- still faded to black (#540). Same contract as M.HALL_OF_FAME in
|
||||
-- data/scripts/story.lua.
|
||||
--
|
||||
-- SafariZoneGateLeavingSafariScript .leaving_early: YES prints the
|
||||
-- return-balls text, faces the player down and runs
|
||||
-- SafariZoneEntranceAutoWalk with `PAD_DOWN, c = 3`, landing on the
|
||||
-- counter row 3 cells below the warp you came in by; NO prints
|
||||
-- "Good Luck!" and walks one step back up through that same warp.
|
||||
local rightSide = ow.player.cellX ~= 3
|
||||
local dest = game.data.maps.SAFARI_ZONE_CENTER.warps[rightSide and 2 or 1]
|
||||
ow:queueScript({
|
||||
{ "ask", "_SafariZoneGateSafariZoneWorker1LeavingEarlyText" },
|
||||
{ "jump_if_false", "stay" },
|
||||
-- the port never reaches SafariZoneGateLeavingSafariScript's own
|
||||
-- GOOD_HAUL_COME_AGAIN branch (safariGameOver warps straight to the
|
||||
-- counter), so the sign-off rides on this path
|
||||
{ "show_text", "_SafariZoneGateSafariZoneWorker1ReturnSafariBallsText" },
|
||||
{ "show_text", "_SafariZoneGateSafariZoneWorker1GoodHaulComeAgainText" },
|
||||
-- no value: set_field assigns nil, which is how save.safari is cleared
|
||||
{ "set_field", "safari" },
|
||||
-- move_player runs through scriptMove, which skips onStepComplete, so
|
||||
-- walking back down past (x,2) cannot re-fire the join trigger
|
||||
{ "move_player", "down", 3 },
|
||||
{ "jump", "end" },
|
||||
{ "label", "stay" },
|
||||
{ "show_text", "_SafariZoneGateSafariZoneWorker1GoodLuckText" },
|
||||
{ "warp", "SAFARI_ZONE_CENTER", dest.x, dest.y, "up" },
|
||||
})
|
||||
end,
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
-- field.seafoam (SEAFOAM_ISLANDS_1F/B1F/B3F holes+holeDestination and
|
||||
-- B3F's pluggedByHolesOn) plus the generic
|
||||
-- OverworldState:boulderIntoHole in src/world/OverworldController.lua;
|
||||
-- no per-map onEnter hook is needed here.
|
||||
-- no per-map onEnter hook is needed for the boulders.
|
||||
|
||||
local M = {}
|
||||
|
||||
@@ -23,4 +23,46 @@ M.VERMILION_GYM = {
|
||||
end,
|
||||
}
|
||||
|
||||
-- The PLAYER falling down those same holes (#599). field.seafoam's holes
|
||||
-- carry only the BOULDER's object cell on the floor below (landsAt), not
|
||||
-- the player's landing, so the player's landing is spelled out here: it
|
||||
-- comes from data/maps/special_warps.asm DungeonWarpList/DungeonWarpData,
|
||||
-- which the importer does not extract. Same shape as MANSION_HOLES in
|
||||
-- data/scripts/story6.lua and VICTORY_ROAD_3F.onStep in
|
||||
-- data/scripts/story.lua; CAVERN $22 is a walkable tile, so the fall has
|
||||
-- to be an onStep, not a collision block.
|
||||
--
|
||||
-- Sources: scripts/SeafoamIslands1F.asm Seafoam1HolesCoords (17,6)/(24,6),
|
||||
-- B1F.asm Seafoam2HolesCoords (18,6)/(23,6), B2F.asm Seafoam3HolesCoords
|
||||
-- (19,6)/(22,6), B3F.asm Seafoam4HolesCoords (3,16)/(6,16). Each floor
|
||||
-- sets wDungeonWarpDestinationMap and calls IsPlayerOnDungeonWarp, and
|
||||
-- wCoordIndex picks that floor's DungeonWarpData row. Unconditional in
|
||||
-- the original: a plugged hole still drops the player. The B3F and B4F
|
||||
-- landings are water; setMap's CheckForceBikeOrSurf pass
|
||||
-- (OverworldState:checkForcedMovement) mounts SURF on arrival.
|
||||
local HOLE_FALLS = {
|
||||
SEAFOAM_ISLANDS_1F = { { 17, 6, "SEAFOAM_ISLANDS_B1F", 18, 7 },
|
||||
{ 24, 6, "SEAFOAM_ISLANDS_B1F", 23, 7 } },
|
||||
SEAFOAM_ISLANDS_B1F = { { 18, 6, "SEAFOAM_ISLANDS_B2F", 19, 7 },
|
||||
{ 23, 6, "SEAFOAM_ISLANDS_B2F", 22, 7 } },
|
||||
SEAFOAM_ISLANDS_B2F = { { 19, 6, "SEAFOAM_ISLANDS_B3F", 18, 7 },
|
||||
{ 22, 6, "SEAFOAM_ISLANDS_B3F", 19, 7 } },
|
||||
SEAFOAM_ISLANDS_B3F = { { 3, 16, "SEAFOAM_ISLANDS_B4F", 4, 14 },
|
||||
{ 6, 16, "SEAFOAM_ISLANDS_B4F", 5, 14 } },
|
||||
}
|
||||
|
||||
for mapId, holes in pairs(HOLE_FALLS) do
|
||||
M[mapId] = M[mapId] or {}
|
||||
M[mapId].onStep = function(game, ow, x, y)
|
||||
for _, h in ipairs(holes) do
|
||||
if x == h[1] and y == h[2] then
|
||||
require("src.core.Sound").play(game.data, "Faint_Fall")
|
||||
ow:startWarpTo(h[3], h[4], h[5], ow.player.facing)
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
return M
|
||||
|
||||
@@ -140,22 +140,29 @@ M.VIRIDIAN_CITY = {
|
||||
-- Daisy hands over the TOWN MAP once Oak's errand is under way
|
||||
-- (scripts/BluesHouse.asm BluesHouseDaisySittingText)
|
||||
M.BLUES_HOUSE = {
|
||||
-- scripts/BluesHouse.asm:12-16
|
||||
onEnter = function(game, ow)
|
||||
game.save.flags.EVENT_ENTERED_BLUES_HOUSE = true
|
||||
end,
|
||||
talk = {
|
||||
TEXT_BLUESHOUSE_DAISY_SITTING = {
|
||||
{ "face_player" }, -- 1
|
||||
{ "check_flag", "EVENT_GOT_TOWN_MAP" }, -- 2
|
||||
{ "jump_if_true", 10 }, -- 3
|
||||
{ "check_flag", "EVENT_GOT_STARTER" }, -- 4
|
||||
{ "jump_if_false", 12 }, -- 5
|
||||
{ "show_text", "_BluesHouseDaisyOfferMapText" }, -- 6
|
||||
{ "face_player" },
|
||||
{ "check_flag", "EVENT_GOT_TOWN_MAP" },
|
||||
{ "jump_if_true", "got_map" },
|
||||
{ "check_flag", "EVENT_GOT_POKEDEX" },
|
||||
{ "jump_if_false", "too_early" },
|
||||
{ "show_text", "_BluesHouseDaisyOfferMapText" },
|
||||
-- _GotMapText: "{PLAYER} got a\n{RAM:wStringBuffer}!" -- the
|
||||
-- buffer supplies "TOWN MAP" (scripts/BluesHouse.asm GotMapText)
|
||||
{ "give_item", "TOWN_MAP", 1, "_GotMapText" }, -- 7
|
||||
{ "set_flag", "EVENT_GOT_TOWN_MAP" }, -- 8
|
||||
{ "jump", 13 }, -- 9
|
||||
{ "show_text", "_BluesHouseDaisyUseMapText" }, -- 10
|
||||
{ "jump", 13 }, -- 11
|
||||
{ "show_text", "_BluesHouseDaisyRivalAtLabText" }, -- 12
|
||||
{ "give_item", "TOWN_MAP", 1, "_GotMapText" },
|
||||
{ "hide_object", "BLUES_HOUSE", "BLUESHOUSE_TOWN_MAP" },
|
||||
{ "set_flag", "EVENT_GOT_TOWN_MAP" },
|
||||
{ "jump", "end" },
|
||||
{ "label", "got_map" },
|
||||
{ "show_text", "_BluesHouseDaisyUseMapText" },
|
||||
{ "jump", "end" },
|
||||
{ "label", "too_early" },
|
||||
{ "show_text", "_BluesHouseDaisyRivalAtLabText" },
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -183,11 +190,17 @@ M.BILLS_HOUSE = {
|
||||
overworld = ow },
|
||||
"BILLS_HOUSE", "BILLSHOUSE_BILL_POKEMON")
|
||||
game.save.flags.EVENT_BILL_SAID_USE_CELL_SEPARATOR = true
|
||||
require("src.world.PikachuFollower").onBillEnteredMachine(game, ow)
|
||||
done()
|
||||
end
|
||||
if ow.player.facing == "down" then
|
||||
-- the player is standing on his straight path: walk around
|
||||
-- (.PokemonWalkAroundPlayerMovement)
|
||||
-- (.PokemonWalkAroundPlayerMovement). BillsHouseScript2 runs
|
||||
-- BillsHousePikachuWatchPlayer first on this branch, so a
|
||||
-- Pikachu that is still following steps clear of Bill's detour
|
||||
-- and turns to watch the player (#455).
|
||||
require("src.world.PikachuFollower")
|
||||
.onBillWalksAroundPlayer(game, ow)
|
||||
ow:scriptMove(npc, "right", 1, function()
|
||||
ow:scriptMove(npc, "up", 2, function()
|
||||
ow:scriptMove(npc, "left", 1, function()
|
||||
@@ -273,6 +286,9 @@ M.BILLS_HOUSE = {
|
||||
Commands.hide_object(ctx, "BILLS_HOUSE", "BILLSHOUSE_BILL1")
|
||||
Commands.show_object(ctx, "BILLS_HOUSE", "BILLSHOUSE_BILL2")
|
||||
end
|
||||
if not flags.EVENT_MET_BILL_2 then
|
||||
require("src.world.PikachuFollower").onBillsHouseEnter(game, ow)
|
||||
end
|
||||
end,
|
||||
}
|
||||
|
||||
@@ -285,6 +301,7 @@ M.BILLS_HOUSE = {
|
||||
|
||||
M.ROUTE_25 = {
|
||||
onEnter = function(game, ow)
|
||||
game.save.pikachuMapScriptActive = nil
|
||||
local flags = game.save.flags
|
||||
if flags.EVENT_LEFT_BILLS_HOUSE_AFTER_HELPING then return end
|
||||
local Commands = require("src.script.Commands")
|
||||
@@ -317,6 +334,7 @@ M.VERMILION_CITY = {
|
||||
-- only read while EVENT_1ST_LOCK_OPENED is unset (the gym is only
|
||||
-- reachable through this map, so a fresh visit always re-rolls).
|
||||
onEnter = function(game, ow)
|
||||
game.save.pikachuMapScriptActive = nil
|
||||
local puz = game.save.trashPuzzle or {}
|
||||
game.save.trashPuzzle = puz
|
||||
puz.first = love.math.random(0, 7) * 2
|
||||
@@ -596,12 +614,34 @@ local function snorlaxWake(mapId, objName, beatFlag, wokeUpText, calmedText)
|
||||
}
|
||||
end
|
||||
|
||||
-- A beaten Snorlax is gone for good: Route12/Route16DefaultScript run
|
||||
-- HideObject in the same breath as the battle that sets
|
||||
-- EVENT_BEAT_ROUTEnn_SNORLAX, so "event set, object still on the map" is a
|
||||
-- state the asm cannot produce. Here it can (a mod's world:toggleObject, a
|
||||
-- save edited or migrated from a build older than the flag), and it is a
|
||||
-- dead end: ItemEffects' adjacentSleepingSnorlax refuses to wake a Snorlax
|
||||
-- whose beat flag is set (ItemUsePokeFlute's CheckEvent, engine/items/
|
||||
-- item_effects.asm), so the sleeper sits in the road forever and Cycling
|
||||
-- Road is unreachable (#585). Reconcile the toggle from the flag on every
|
||||
-- entry -- the mirror of SaveData.lua's toggle -> flag backfill, and the
|
||||
-- same repair shape the Silph Co. floors use below.
|
||||
local function hideBeatenSnorlax(mapId, objName, beatFlag)
|
||||
return function(game, ow)
|
||||
if not game.save.flags[beatFlag] then return end
|
||||
local Commands = require("src.script.Commands")
|
||||
Commands.hide_object({ game = game, save = game.save, overworld = ow },
|
||||
mapId, objName)
|
||||
end
|
||||
end
|
||||
|
||||
-- snorlaxWake is looked up by ItemEffects.lua/BagMenu.lua (via
|
||||
-- data/scripts/init.lua's M.get) and run when the flute wakes Snorlax;
|
||||
-- objName/beatFlag let ItemEffects find the NPC and check whether it's
|
||||
-- already been beaten before allowing the wake.
|
||||
M.ROUTE_12 = {
|
||||
talk = { TEXT_ROUTE12_SNORLAX = { { "show_text", "_Route12SnorlaxText" } } },
|
||||
onEnter = hideBeatenSnorlax("ROUTE_12", "ROUTE12_SNORLAX",
|
||||
"EVENT_BEAT_ROUTE12_SNORLAX"),
|
||||
snorlaxWake = {
|
||||
objName = "ROUTE12_SNORLAX", beatFlag = "EVENT_BEAT_ROUTE12_SNORLAX",
|
||||
script = snorlaxWake("ROUTE_12", "ROUTE12_SNORLAX", "EVENT_BEAT_ROUTE12_SNORLAX",
|
||||
@@ -610,6 +650,8 @@ M.ROUTE_12 = {
|
||||
}
|
||||
M.ROUTE_16 = {
|
||||
talk = { TEXT_ROUTE16_SNORLAX = { { "show_text", "_Route16Text7" } } },
|
||||
onEnter = hideBeatenSnorlax("ROUTE_16", "ROUTE16_SNORLAX",
|
||||
"EVENT_BEAT_ROUTE16_SNORLAX"),
|
||||
snorlaxWake = {
|
||||
objName = "ROUTE16_SNORLAX", beatFlag = "EVENT_BEAT_ROUTE16_SNORLAX",
|
||||
script = snorlaxWake("ROUTE_16", "ROUTE16_SNORLAX", "EVENT_BEAT_ROUTE16_SNORLAX",
|
||||
@@ -641,11 +683,18 @@ M.SAFARI_ZONE_SECRET_HOUSE = {
|
||||
M.WARDENS_HOUSE = {
|
||||
talk = {
|
||||
TEXT_WARDENSHOUSE_WARDEN = {
|
||||
-- Labelled rather than hand-numbered: the branches here have been
|
||||
-- re-pointed twice now (#535, #645), and every insert used to mean
|
||||
-- renumbering three jumps that had no way of announcing they were stale.
|
||||
{ "face_player" }, -- 1
|
||||
{ "check_flag", "EVENT_GOT_HM04" }, -- 2
|
||||
{ "jump_if_true", 13 }, -- 3
|
||||
-- #535: previously jumped to the same silent-end target as the
|
||||
-- give-then-thank fallthrough, so the warden said nothing on every
|
||||
-- visit after the trade. pokered's .got_item branch
|
||||
-- (scripts/WardensHouse.asm) instead prints HM04ExplanationText.
|
||||
{ "jump_if_true", "got_hm04" }, -- 3
|
||||
{ "check_item", "GOLD_TEETH" }, -- 4
|
||||
{ "jump_if_false", 15 }, -- 5
|
||||
{ "jump_if_false", "no_teeth" }, -- 5
|
||||
{ "show_text", "_WardensHouseWardenGaveTheGoldTeethText" }, -- 6
|
||||
{ "take_item", "GOLD_TEETH", 1 }, -- 7
|
||||
{ "set_flag", "EVENT_GAVE_GOLD_TEETH" }, -- 8
|
||||
@@ -654,9 +703,27 @@ M.WARDENS_HOUSE = {
|
||||
{ "give_item", "HM_STRENGTH", 1, false }, -- 10
|
||||
{ "show_text", "_WardensHouseWardenReceivedHM04Text" }, -- 11
|
||||
{ "set_flag", "EVENT_GOT_HM04" }, -- 12
|
||||
{ "jump", 16 }, -- 13 (already got it)
|
||||
{ "jump", 16 }, -- 14 (unused)
|
||||
{ "show_text", "_WardensHouseWardenGibberish1Text" }, -- 15
|
||||
{ "jump", "end" }, -- 13 (jp .done)
|
||||
|
||||
-- #645: WardensHouseWardenText prints Gibberish1, then YesNoChoice,
|
||||
-- and the warden answers the same gibberish either way -- Gibberish2
|
||||
-- on yes, Gibberish3 on no (scripts/WardensHouse.asm). The port
|
||||
-- printed the question and walked off before the answer.
|
||||
{ "label", "no_teeth" }, -- 14
|
||||
{ "ask", "_WardensHouseWardenGibberish1Text" }, -- 15
|
||||
{ "jump_if_true", "gibberish_yes" }, -- 16
|
||||
{ "show_text", "_WardensHouseWardenGibberish3Text" }, -- 17
|
||||
{ "jump", "end" }, -- 18
|
||||
{ "label", "gibberish_yes" }, -- 19
|
||||
{ "show_text", "_WardensHouseWardenGibberish2Text" }, -- 20
|
||||
{ "jump", "end" }, -- 21
|
||||
|
||||
-- #535: pokered .got_item branch (scripts/WardensHouse.asm) --
|
||||
-- printed on every subsequent talk once EVENT_GOT_HM04 is set.
|
||||
-- Text is _WardensHouseWardenHM04ExplanationText (text/WardensHouse.asm):
|
||||
-- HM04 teaches Strength, and hints at the Safari Zone secret house.
|
||||
{ "label", "got_hm04" }, -- 22
|
||||
{ "show_text", "_WardensHouseWardenHM04ExplanationText" }, -- 23
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -666,6 +733,70 @@ M.WARDENS_HOUSE = {
|
||||
-- generic OPP_GIOVANNI#2 battle)
|
||||
-- -------------------------------------------------------------------
|
||||
|
||||
-- SilphCo11FTeamRocketLeavesScript (scripts/SilphCo11F.asm) hides every
|
||||
-- ROCKET and rocket-aligned SCIENTIST toggle on 2F-11F the moment Giovanni
|
||||
-- falls; the item balls, the 2F/10F rescued workers and the 7F rival keep
|
||||
-- theirs. Names and order are data/maps/toggleable_objects.asm's
|
||||
-- TOGGLE_SILPH_CO_<floor>_n entries. Saffron City's half of the same
|
||||
-- script lives in M.SAFFRON_CITY (story4.lua) (#392).
|
||||
local SILPH_ROCKET_OBJECTS = {
|
||||
{ "SILPH_CO_2F", { "SILPHCO2F_SCIENTIST1", "SILPHCO2F_SCIENTIST2",
|
||||
"SILPHCO2F_ROCKET1", "SILPHCO2F_ROCKET2" } },
|
||||
{ "SILPH_CO_3F", { "SILPHCO3F_ROCKET", "SILPHCO3F_SCIENTIST" } },
|
||||
{ "SILPH_CO_4F", { "SILPHCO4F_ROCKET1", "SILPHCO4F_SCIENTIST",
|
||||
"SILPHCO4F_ROCKET2" } },
|
||||
{ "SILPH_CO_5F", { "SILPHCO5F_ROCKET1", "SILPHCO5F_SCIENTIST",
|
||||
"SILPHCO5F_ROCKER", "SILPHCO5F_ROCKET2" } },
|
||||
{ "SILPH_CO_6F", { "SILPHCO6F_ROCKET1", "SILPHCO6F_SCIENTIST",
|
||||
"SILPHCO6F_ROCKET2" } },
|
||||
{ "SILPH_CO_7F", { "SILPHCO7F_ROCKET1", "SILPHCO7F_SCIENTIST",
|
||||
"SILPHCO7F_ROCKET2", "SILPHCO7F_ROCKET3" } },
|
||||
{ "SILPH_CO_8F", { "SILPHCO8F_ROCKET1", "SILPHCO8F_SCIENTIST",
|
||||
"SILPHCO8F_ROCKET2" } },
|
||||
{ "SILPH_CO_9F", { "SILPHCO9F_ROCKET1", "SILPHCO9F_SCIENTIST",
|
||||
"SILPHCO9F_ROCKET2" } },
|
||||
{ "SILPH_CO_10F", { "SILPHCO10F_ROCKET", "SILPHCO10F_SCIENTIST" } },
|
||||
{ "SILPH_CO_11F", { "SILPHCO11F_GIOVANNI", "SILPHCO11F_ROCKET1",
|
||||
"SILPHCO11F_ROCKET2" } },
|
||||
}
|
||||
|
||||
-- hide_object writes save.objectToggles, so the single pass at the win
|
||||
-- covers floors the player is not standing on; onlyMap is the per-floor
|
||||
-- repair path below.
|
||||
local function silphRocketsLeave(game, ow, onlyMap)
|
||||
local Commands = require("src.script.Commands")
|
||||
local ctx = { game = game, save = game.save, overworld = ow }
|
||||
for _, floor in ipairs(SILPH_ROCKET_OBJECTS) do
|
||||
if not onlyMap or onlyMap == floor[1] then
|
||||
for _, name in ipairs(floor[2]) do
|
||||
Commands.hide_object(ctx, floor[1], name)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- SilphCo11FGiovanniAfterBattleScript (scripts/SilphCo11F.asm) is the whole
|
||||
-- aftermath: DisplayTextID TEXT_SILPHCO11F_GIOVANNI_YOU_RUINED_OUR_PLANS,
|
||||
-- GBFadeOutToBlack, SilphCo11FTeamRocketLeavesScript, Delay3,
|
||||
-- GBFadeInFromBlack, then SetEvent. The port had only the hide pass, so the
|
||||
-- speech never played and every rocket blinked out in front of the player
|
||||
-- (#722). Same hide list as silphRocketsLeave, spelled as script rows so the
|
||||
-- fade can hold over it.
|
||||
local function silphAftermathRows()
|
||||
local rows = {
|
||||
{ "show_text", "_SilphCo11FGiovanniYouRuinedOurPlansText" },
|
||||
{ "fade", "out" },
|
||||
}
|
||||
for _, floor in ipairs(SILPH_ROCKET_OBJECTS) do
|
||||
for _, name in ipairs(floor[2]) do
|
||||
rows[#rows + 1] = { "hide_object", floor[1], name }
|
||||
end
|
||||
end
|
||||
rows[#rows + 1] = { "wait", 3 } -- Delay3
|
||||
rows[#rows + 1] = { "fade", "in" }
|
||||
return rows
|
||||
end
|
||||
|
||||
M.SILPH_CO_11F = {
|
||||
-- Giovanni's battle is a COORDINATE TRIGGER, not a talk.
|
||||
-- SilphCo11FDefaultScript (scripts/SilphCo11F.asm) checks
|
||||
@@ -678,9 +809,13 @@ M.SILPH_CO_11F = {
|
||||
-- line) would touch, and the whole Silph ending -- the flag, the Master
|
||||
-- Ball, the Saffron streets clearing -- silently never happened.
|
||||
--
|
||||
-- engageTrainer shows TEXT_SILPHCO11F_GIOVANNI as the battle text and,
|
||||
-- via victories.lua OPP_GIOVANNI#2, sets the event on a win; a loss
|
||||
-- sets nothing, so the trigger re-arms exactly as vanilla does.
|
||||
-- SilphCo11FDefaultScript orders it DisplayTextID TEXT_SILPHCO11F_GIOVANNI
|
||||
-- FIRST, then MoveSprite .GiovanniMovement: he speaks from behind the desk
|
||||
-- and only then walks the three tiles down. Moving him before the box made
|
||||
-- him cross the room in silence and deliver the speech point-blank (#869),
|
||||
-- so the box comes first here and engageTrainer skips its own battle text.
|
||||
-- victories.lua OPP_GIOVANNI#2 sets the event on a win; a loss sets
|
||||
-- nothing, so the trigger re-arms exactly as vanilla does.
|
||||
onStep = function(game, ow, x, y)
|
||||
if game.save.flags.EVENT_BEAT_SILPH_CO_GIOVANNI then return false end
|
||||
if not ((x == 6 and y == 13) or (x == 7 and y == 12)) then return false end
|
||||
@@ -689,45 +824,72 @@ M.SILPH_CO_11F = {
|
||||
if npc.def and npc.def.name == "SILPHCO11F_GIOVANNI" then gio = npc break end
|
||||
end
|
||||
if not gio or ow:trainerDefeated(gio) then return false end
|
||||
ow:scriptMove(gio, "down", 3, function()
|
||||
gio:facePlayer(ow.player)
|
||||
ow:engageTrainer(gio, function()
|
||||
-- SilphCo11FTeamRocketLeavesScript: Giovanni leaves the floor
|
||||
-- after the loss (the street rockets are handled by
|
||||
-- M.SAFFRON_CITY.onEnter in story4.lua).
|
||||
if game.save.flags.EVENT_BEAT_SILPH_CO_GIOVANNI then
|
||||
local Commands = require("src.script.Commands")
|
||||
local ctx = { game = game, save = game.save, overworld = ow }
|
||||
Commands.hide_object(ctx, "SILPH_CO_11F", "SILPHCO11F_GIOVANNI")
|
||||
end
|
||||
end)
|
||||
end)
|
||||
local TextBox = require("src.render.TextBox")
|
||||
game.stack:push(TextBox.new(game,
|
||||
game.data.text._SilphCo11FGiovanniText
|
||||
or "Ah {PLAYER}!\nSo we meet again!",
|
||||
function()
|
||||
ow:scriptMove(gio, "down", 3, function()
|
||||
gio:facePlayer(ow.player)
|
||||
ow:engageTrainer(gio, function()
|
||||
-- SilphCo11FGiovanniAfterBattleScript: the "Blast it all!"
|
||||
-- speech, then SilphCo11FTeamRocketLeavesScript behind a fade so
|
||||
-- every Silph rocket leaves off-screen (the street rockets are
|
||||
-- handled by M.SAFFRON_CITY.onEnter in story4.lua). Queued, not
|
||||
-- run here: the battle's own callbacks are still unwinding, so
|
||||
-- queueScript starts it on the first idle overworld frame --
|
||||
-- after the end-battle "Arrgh!!" box victories.lua OPP_GIOVANNI#2
|
||||
-- pushes (#722).
|
||||
if game.save.flags.EVENT_BEAT_SILPH_CO_GIOVANNI then
|
||||
ow:queueScript(silphAftermathRows())
|
||||
end
|
||||
end, nil, true)
|
||||
end)
|
||||
end))
|
||||
return true
|
||||
end,
|
||||
onEnter = function(game, ow)
|
||||
if game.save.flags.EVENT_BEAT_SILPH_CO_GIOVANNI then
|
||||
local Commands = require("src.script.Commands")
|
||||
local ctx = { game = game, save = game.save, overworld = ow }
|
||||
Commands.hide_object(ctx, "SILPH_CO_11F", "SILPHCO11F_GIOVANNI")
|
||||
silphRocketsLeave(game, ow)
|
||||
end
|
||||
end,
|
||||
talk = {
|
||||
-- SilphCo11FSilphPresidentText branches on EVENT_GOT_MASTER_BALL only:
|
||||
-- the teleport pads reach him without passing Giovanni's trigger, and
|
||||
-- afterwards he describes the ball forever. The old rows fell off the
|
||||
-- end of the script on both branches, so a second talk was silence
|
||||
-- (#392).
|
||||
TEXT_SILPHCO11F_SILPH_PRESIDENT = {
|
||||
{ "face_player" }, -- 1
|
||||
{ "check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI" }, -- 2
|
||||
{ "jump_if_false", 10 }, -- 3
|
||||
{ "check_flag", "EVENT_GOT_MASTER_BALL" }, -- 4
|
||||
{ "jump_if_true", 10 }, -- 5
|
||||
{ "show_text", "_SilphCo11FSilphPresidentText" }, -- 6
|
||||
{ "check_flag", "EVENT_GOT_MASTER_BALL" }, -- 2
|
||||
{ "jump_if_true", 9 }, -- 3
|
||||
{ "show_text", "_SilphCo11FSilphPresidentText" }, -- 4
|
||||
-- give-then-print like scripts/SilphCo11F.asm
|
||||
{ "give_item", "MASTER_BALL", 1, false }, -- 7
|
||||
{ "show_text", "_SilphCo11FSilphPresidentReceivedMasterBallText" }, -- 8
|
||||
{ "set_flag", "EVENT_GOT_MASTER_BALL" }, -- 9
|
||||
{ "jump", 11 }, -- 10
|
||||
{ "give_item", "MASTER_BALL", 1, false }, -- 5
|
||||
{ "show_text", "_SilphCo11FSilphPresidentReceivedMasterBallText" }, -- 6
|
||||
{ "set_flag", "EVENT_GOT_MASTER_BALL" }, -- 7
|
||||
{ "jump", "end" }, -- 8
|
||||
{ "show_text",
|
||||
"_SilphCo11FSilphPresidentMasterBallDescriptionText" }, -- 9
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
-- Floors 2F-10F carry no other onEnter, so this repairs saves that beat
|
||||
-- Giovanni before the hide list existed (same shape as
|
||||
-- M.POKEMON_TOWER_7F.onEnter); 11F hides its own inside the table above.
|
||||
for _, floor in ipairs(SILPH_ROCKET_OBJECTS) do
|
||||
local mapId = floor[1]
|
||||
if mapId ~= "SILPH_CO_11F" then
|
||||
M[mapId] = M[mapId] or {}
|
||||
M[mapId].onEnter = function(game, ow)
|
||||
if game.save.flags.EVENT_BEAT_SILPH_CO_GIOVANNI then
|
||||
silphRocketsLeave(game, ow, mapId)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- -------------------------------------------------------------------
|
||||
-- Victory Road boulder switches (scripts/VictoryRoad1F/2F/3F.asm):
|
||||
-- a boulder resting on a switch removes a barrier block; the 3F hole
|
||||
@@ -826,6 +988,7 @@ M.VICTORY_ROAD_3F = {
|
||||
-- fall is onStep, not a collision block.
|
||||
onStep = function(game, ow, x, y)
|
||||
if x == 23 and y == 15 then
|
||||
require("src.core.Sound").play(game.data, "Faint_Fall")
|
||||
ow:startWarpTo("VICTORY_ROAD_2F", 22, 16, ow.player.facing)
|
||||
return true
|
||||
end
|
||||
@@ -864,40 +1027,63 @@ M.VICTORY_ROAD_3F = {
|
||||
local championsRoomRivalScript = {
|
||||
{ "face_player" }, -- 1
|
||||
{ "check_flag", "EVENT_BEAT_CHAMPION_RIVAL_THIS_RUN" }, -- 2
|
||||
{ "jump_if_true", 25 }, -- 3 past end
|
||||
-- "end" rather than a row number past the tail: this script grew by a row
|
||||
-- when the follow-Oak walk landed (#704), which silently turned the old
|
||||
-- numeric 26 into a jump ONTO the closing HALL_OF_FAME warp instead of past
|
||||
-- it, so a returning champion warped straight into the induction.
|
||||
{ "jump_if_true", "end" }, -- 3
|
||||
{ "show_text", "_ChampionsRoomRivalIntroText" }, -- 4
|
||||
{ "rival_battle", "OPP_RIVAL3", 1 }, -- 5
|
||||
{ "jump_if_false", 25 }, -- 6 past end
|
||||
{ "set_flag", "EVENT_BEAT_CHAMPION_RIVAL_THIS_RUN" }, -- 7
|
||||
{ "set_flag", "EVENT_BEAT_CHAMPION_RIVAL" }, -- 8
|
||||
-- ChampionsRoomRivalReadyToBattleScript plays MUSIC_FINAL_BATTLE after
|
||||
-- the intro text, before the battle itself (#706); pushBattle's wipe-time
|
||||
-- playBattle("final") then no-ops on the same song, so the theme stays
|
||||
-- continuous into the fight
|
||||
{ "play_music", "Music_FinalBattle" }, -- 5
|
||||
{ "rival_battle", "OPP_RIVAL3", 1 }, -- 6
|
||||
-- losing halts here; the numeric target this replaced pointed at the
|
||||
-- closing warp, which inducted a player who had just lost the fight (#704)
|
||||
{ "jump_if_false", "end" }, -- 7
|
||||
{ "set_flag", "EVENT_BEAT_CHAMPION_RIVAL_THIS_RUN" }, -- 8
|
||||
{ "set_flag", "EVENT_BEAT_CHAMPION_RIVAL" }, -- 9
|
||||
-- ChampionsRoomRivalDefeatedScript re-displays TEXT_CHAMPIONSROOM_RIVAL,
|
||||
-- whose text_asm takes the EVENT_BEAT_CHAMPION_RIVAL branch =
|
||||
-- _ChampionsRoomRivalAfterBattleText (the in-battle _RivalDefeatedText
|
||||
-- is the port's generic "<PLAYER> defeated BLUE!" engine line instead).
|
||||
{ "show_text", "_ChampionsRoomRivalAfterBattleText" }, -- 9
|
||||
{ "show_text", "_ChampionsRoomRivalAfterBattleText" }, -- 10
|
||||
-- ChampionsRoomOakArrivesScript: Music_Cities1AlternateTempo
|
||||
-- (Cities1, kept into HALL_OF_FAME like BIT_NO_MAP_MUSIC after
|
||||
-- defeating RIVAL3), then Oak's "{PLAYER}!" + reveal + walk in
|
||||
{ "play_music", "Music_Cities1", { keep = true } }, -- 10
|
||||
{ "show_text", "_ChampionsRoomOakText" }, -- 11
|
||||
{ "show_object", "CHAMPIONS_ROOM", "CHAMPIONSROOM_OAK" }, -- 12
|
||||
{ "move_npc", 2, "up", 5 }, -- 13 OakEntranceAfterVictoryMovement
|
||||
-- defeating RIVAL3), then Oak's "{PLAYER}!" + reveal + walk in.
|
||||
-- audio/alternate_tempo.asm Music_Cities1AlternateTempo is not a plain
|
||||
-- PlayMusic: it fades the current song out (wAudioFadeOutControl = 10),
|
||||
-- waits 100 frames for the fade, then restarts Cities1 with channel 1
|
||||
-- pointed at Music_Cities1_Ch1_AlternateTempo -- `tempo 232` where the
|
||||
-- normal Music_Cities1_Ch1 opens `tempo 144`, i.e. the slower, heavier
|
||||
-- reading of the town theme this scene is known for (#847).
|
||||
{ "fade_music", 10 }, -- 11
|
||||
{ "wait", 100 }, -- 12
|
||||
{ "play_music", "Music_Cities1", { keep = true, tempo = 232 } }, -- 13
|
||||
{ "show_text", "_ChampionsRoomOakText" }, -- 14
|
||||
{ "show_object", "CHAMPIONS_ROOM", "CHAMPIONSROOM_OAK" }, -- 15
|
||||
{ "move_npc", 2, "up", 5 }, -- 16 OakEntranceAfterVictoryMovement
|
||||
-- OakCongratulatesPlayerScript: rival faces left, Oak faces down
|
||||
{ "face_object", 1, "left" }, -- 14
|
||||
{ "face_object", 2, "down" }, -- 15
|
||||
{ "show_text", "_ChampionsRoomOakCongratulatesPlayerText" }, -- 16
|
||||
{ "face_object", 1, "left" }, -- 17
|
||||
{ "face_object", 2, "down" }, -- 18
|
||||
{ "load_player_starter_name" },
|
||||
{ "show_text", "_ChampionsRoomOakCongratulatesPlayerText" }, -- 19
|
||||
-- OakDisappointedWithRivalScript: Oak turns to the rival (right)
|
||||
{ "face_object", 2, "right" }, -- 17
|
||||
{ "show_text", "_ChampionsRoomOakDisappointedWithRivalText" }, -- 18
|
||||
{ "face_object", 2, "right" }, -- 20
|
||||
{ "show_text", "_ChampionsRoomOakDisappointedWithRivalText" }, -- 21
|
||||
-- OakComeWithMeScript: Oak faces down again, then exits up
|
||||
{ "face_object", 2, "down" }, -- 19
|
||||
{ "show_text", "_ChampionsRoomOakComeWithMeText" }, -- 20
|
||||
{ "move_npc", 2, "up", 2 }, -- 21 OakExitChampionsRoomMovement
|
||||
{ "hide_object", "CHAMPIONS_ROOM", "CHAMPIONSROOM_OAK" }, -- 22
|
||||
{ "face_object", 2, "down" }, -- 22
|
||||
{ "show_text", "_ChampionsRoomOakComeWithMeText" }, -- 23
|
||||
{ "move_npc", 2, "up", 2 }, -- 24 OakExitChampionsRoomMovement
|
||||
{ "hide_object", "CHAMPIONS_ROOM", "CHAMPIONSROOM_OAK" }, -- 25
|
||||
-- scripts/ChampionsRoom.asm WalkToHallOfFame_RLEMovement
|
||||
{ "move_player", "left", 1 },
|
||||
{ "move_player", "up", 3 }, -- 27
|
||||
-- hand the induction off to the HALL_OF_FAME room (consumed by its
|
||||
-- onEnter), then warp up into it (destWarp 1 lands at (4,7) facing up)
|
||||
{ "set_field", "pendingHallOfFame", true }, -- 23
|
||||
{ "warp", "HALL_OF_FAME", 4, 7, "up" }, -- 24
|
||||
{ "set_field", "pendingHallOfFame", true }, -- 28
|
||||
{ "warp", "HALL_OF_FAME", 4, 7, "up" }, -- 29
|
||||
}
|
||||
|
||||
M.CHAMPIONS_ROOM = {
|
||||
@@ -1057,6 +1243,7 @@ local function pokemonTower2FRivalScript(playerX)
|
||||
{ "jump_if_false", "end" }, -- 6 loss: stay
|
||||
{ "set_flag", "EVENT_BEAT_POKEMON_TOWER_RIVAL" }, -- 7
|
||||
{ "show_text", "_PokemonTower2FRivalDefeatedText" }, -- 8
|
||||
{ "play_music", "Music_MeetRival", { start = "rival" } },
|
||||
{ "walk_npc", 1, exitDirs }, -- 9
|
||||
{ "hide_object", "POKEMON_TOWER_2F", "POKEMONTOWER2F_RIVAL" }, -- 10
|
||||
{ "jump", "end" }, -- 11
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
-- More hand-ported events: the Pallet Town intro, the thirsty Saffron
|
||||
-- gate guards, the Bike Voucher chain, fossils and the day-care.
|
||||
-- Registered via data/scripts/init.lua; each cites its pokered source.
|
||||
-- Registered via data/scripts/init.lua; Red/Blue cite pokered, Yellow
|
||||
-- cites pokeyellow (Pallet stop row, Oak spawn, walk RLE, and the
|
||||
-- wild-Pikachu beat before the lab escort).
|
||||
|
||||
local GameVersion = require("src.core.GameVersion")
|
||||
|
||||
local M = {}
|
||||
|
||||
@@ -35,72 +39,91 @@ function escort.findPath(fromX, fromY, toX, toY)
|
||||
return path
|
||||
end
|
||||
|
||||
-- PalletTownOakWalksToPlayerScript: Oak appears at his object spot
|
||||
-- (8,5) and zigzags to one tile below the player (hNPCPlayerYDistance
|
||||
-- is pre-decremented before predef FindPathToPlayer).
|
||||
-- Red: Oak object (8,5) -> one tile below the player at y=1.
|
||||
-- Yellow: Oak object (10,4); stop fires at y=0 (pokeyellow PalletTown).
|
||||
function escort.oakApproach(playerX)
|
||||
if GameVersion.isYellow() then
|
||||
return escort.findPath(10, 4, playerX, 1)
|
||||
end
|
||||
return escort.findPath(8, 5, playerX, 2)
|
||||
end
|
||||
|
||||
-- RLEList_ProfOakWalkToLab (engine/overworld/auto_movement.asm):
|
||||
-- DOWN x5, LEFT, DOWN x5, RIGHT x3, UP -- Oak's last step lands on the
|
||||
-- lab door (12,11). (The trailing NPC_CHANGE_FACING is a march-in-
|
||||
-- place beat on the mat; here Oak just stands his final beat.)
|
||||
escort.oakSteps = {
|
||||
"down", "down", "down", "down", "down",
|
||||
"left",
|
||||
"down", "down", "down", "down", "down",
|
||||
"right", "right", "right",
|
||||
"up",
|
||||
}
|
||||
-- RLEList_ProfOakWalkToLab (engine/overworld/auto_movement.asm).
|
||||
-- Yellow differs: first DOWN is x6 (Oak starts one tile farther north).
|
||||
local function buildOakSteps()
|
||||
if GameVersion.isYellow() then
|
||||
return {
|
||||
"down", "down", "down", "down", "down", "down",
|
||||
"left",
|
||||
"down", "down", "down", "down", "down",
|
||||
"right", "right", "right",
|
||||
"up",
|
||||
}
|
||||
end
|
||||
return {
|
||||
"down", "down", "down", "down", "down",
|
||||
"left",
|
||||
"down", "down", "down", "down", "down",
|
||||
"right", "right", "right",
|
||||
"up",
|
||||
}
|
||||
end
|
||||
|
||||
-- RLEList_PlayerWalkToLab decodes to UP x2, RIGHT x3, DOWN x5, LEFT,
|
||||
-- DOWN x6 and plays in REVERSE buffer order (wSimulatedJoypadStatesEnd
|
||||
-- grows downward): DOWN x6, LEFT, DOWN x5, RIGHT x3, UP x2 -- Oak's
|
||||
-- exact path one step behind. The 17th press (the second UP) is eaten
|
||||
-- by the door-warp frame (WarpFound clears hJoyHeld via EnterMap), so
|
||||
-- only 16 real steps happen; the walk ends on the door at (12,11).
|
||||
escort.oakSteps = buildOakSteps()
|
||||
|
||||
-- Player stays one step behind Oak (simplified reverse-RLE port).
|
||||
escort.playerSteps = { "down" }
|
||||
for _, d in ipairs(escort.oakSteps) do
|
||||
escort.playerSteps[#escort.playerSteps + 1] = d
|
||||
end
|
||||
|
||||
local function npcNamed(ow, name)
|
||||
for _, n in ipairs(ow.npcs or {}) do
|
||||
if n.def and n.def.name == name then return n end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
M.PALLET_TOWN = {
|
||||
talk = require("data.scripts.pallet_town").talk,
|
||||
escort = escort,
|
||||
-- Oak stops you at the north row (PalletTownDefaultScript's
|
||||
-- `wYCoord == 1` check), walks up from (8,5), and leads you to his
|
||||
-- lab with the player one step behind (scripts/PalletTown.asm +
|
||||
-- PalletMovementScriptPointerTable in
|
||||
-- engine/overworld/auto_movement.asm), then the lab walk-in and the
|
||||
-- choose-mon exchange (scripts/OaksLab.asm OaksLabDefaultScript ..
|
||||
-- OaksLabOakChooseMonSpeechScript).
|
||||
-- scripts/PalletTown.asm:133-144
|
||||
onEnter = function(game, ow)
|
||||
local f = game.save.flags
|
||||
if f.EVENT_GOT_TOWN_MAP and f.EVENT_ENTERED_BLUES_HOUSE
|
||||
and not f.EVENT_DAISY_WALKING then
|
||||
f.EVENT_DAISY_WALKING = true
|
||||
local Commands = require("src.script.Commands")
|
||||
local ctx = { save = game.save, game = game, overworld = ow }
|
||||
Commands.hide_object(ctx, "BLUES_HOUSE", "BLUESHOUSE_DAISY1")
|
||||
Commands.show_object(ctx, "BLUES_HOUSE", "BLUESHOUSE_DAISY2")
|
||||
end
|
||||
end,
|
||||
-- Red: stop at y==1 from (8,5). Yellow: stop at y==0 from (10,4),
|
||||
-- then a wild Pikachu battle before the lab escort (pokeyellow
|
||||
-- PalletTownPikachuBattleScript).
|
||||
onStep = function(game, ow, x, y)
|
||||
if y ~= 1 or game.save.flags.EVENT_FOLLOWED_OAK_INTO_LAB
|
||||
local yellow = GameVersion.isYellow()
|
||||
local stopY = yellow and 0 or 1
|
||||
if y ~= stopY or game.save.flags.EVENT_FOLLOWED_OAK_INTO_LAB
|
||||
or game.save.flags.EVENT_GOT_STARTER then
|
||||
return false
|
||||
end
|
||||
local TextBox = require("src.render.TextBox")
|
||||
local Commands = require("src.script.Commands")
|
||||
local Music = require("src.core.Music")
|
||||
local BattleState = require("src.battle.BattleState")
|
||||
local t = game.data.text
|
||||
local ctx = { save = game.save, game = game, overworld = ow }
|
||||
|
||||
-- PalletTownDefaultScript: stop the player, turn them around
|
||||
-- (wPlayerMovingDirection = PLAYER_DIR_DOWN applies on the very
|
||||
-- next frame, before the text box opens) and strike up the "oak
|
||||
-- appears" theme (MUSIC_MEET_PROF_OAK)
|
||||
ow.player.facing = "down"
|
||||
-- Red turns the player down; Yellow faces up at the north exit.
|
||||
ow.player.facing = yellow and "up" or "down"
|
||||
Music.play(game.data, "Music_MeetProfOak")
|
||||
|
||||
-- DelayFrames-style hold: the world pauses (input stays locked)
|
||||
-- for `frames` frames, then cb runs. Reuses the emote pause slot;
|
||||
-- with an `npc` the "!" bubble draws above it (EmotionBubble).
|
||||
local function hold(frames, npc, cb)
|
||||
ow.emote = { frames = frames, npc = npc, onDone = cb }
|
||||
end
|
||||
|
||||
-- chain single-tile scriptMoves through a direction list
|
||||
local function walkList(entity, steps, done)
|
||||
local i = 0
|
||||
local function nextStep()
|
||||
@@ -114,10 +137,8 @@ M.PALLET_TOWN = {
|
||||
nextStep()
|
||||
end
|
||||
|
||||
-- ---- Oak's Lab side (scripts/OaksLab.asm) ----------------------
|
||||
|
||||
-- OaksLabOakChooseMonSpeechScript: the fed-up / choose-mon /
|
||||
-- what-about-me / be-patient exchange, Delay3 between boxes
|
||||
-- OaksLabOakChooseMonSpeechScript. Yellow's OakChooseMon text is
|
||||
-- the single-ball speech, not Red's "there are 3 POKéMON".
|
||||
local function chooseMonSpeech()
|
||||
local function say(key, fb, next)
|
||||
game.stack:push(TextBox.new(game, t[key] or fb, next))
|
||||
@@ -126,7 +147,9 @@ M.PALLET_TOWN = {
|
||||
"{RIVAL}: Gramps!\nI'm fed up with\nwaiting!", function()
|
||||
hold(3, nil, function()
|
||||
say("_OaksLabOakChooseMonText",
|
||||
"OAK: Here, {PLAYER}!\fThere are 3\nPOKéMON here!\fYou can have one!\nChoose!", function()
|
||||
yellow
|
||||
and "OAK: Look, {PLAYER}! Do\nyou see that ball\non the table?"
|
||||
or "OAK: Here, {PLAYER}!\fThere are 3\nPOKéMON here!\fYou can have one!\nChoose!", function()
|
||||
hold(3, nil, function()
|
||||
say("_OaksLabRivalWhatAboutMeText",
|
||||
"{RIVAL}: Hey!\nGramps! What\nabout me?", function()
|
||||
@@ -143,30 +166,18 @@ M.PALLET_TOWN = {
|
||||
end)
|
||||
end
|
||||
|
||||
-- entering the lab: the door Oak (OAKSLAB_OAK2, (5,10)) walks up 3
|
||||
-- ahead of the player (OaksLabOakEntersLabScript OakEntryMovement),
|
||||
-- swaps for the desk Oak (OAKSLAB_OAK1, (5,2)), then the player
|
||||
-- walks up 8 from the mat (PlayerEntryMovementRLE) while the rival
|
||||
-- and Oak turn with them (OaksLabPlayerEntersLabScript /
|
||||
-- OaksLabFollowedOakScript)
|
||||
-- Door Oak is OAKSLAB_OAK2: Red index 8, Yellow index 6 (one ball).
|
||||
local function labWalkIn()
|
||||
local oak2 = ow:npcByIndex(8)
|
||||
local oak2 = npcNamed(ow, "OAKSLAB_OAK2") or ow:npcByIndex(yellow and 6 or 8)
|
||||
local function swapOaks()
|
||||
Commands.hide_object(ctx, "OAKS_LAB", "OAKSLAB_OAK2")
|
||||
Commands.show_object(ctx, "OAKS_LAB", "OAKSLAB_OAK1")
|
||||
hold(3, nil, function() -- Delay3
|
||||
Commands.face_object(ctx, 1, "down") -- rival watches you pass
|
||||
hold(3, nil, function()
|
||||
Commands.face_object(ctx, 1, "down")
|
||||
ow:scriptMove(ow.player, "up", 8, function()
|
||||
-- OaksLabFollowedOakScript: flags only after the walk-in, so a
|
||||
-- stray step on the door mat can't fire the "don't go away"
|
||||
-- push-up (oaks_lab.lua onStep) mid-cutscene. Outdoor escort
|
||||
-- still re-arms on F1 mid-escort -- these flags stay clear
|
||||
-- until the lab walk finishes.
|
||||
game.save.flags.EVENT_FOLLOWED_OAK_INTO_LAB = true
|
||||
game.save.flags.EVENT_FOLLOWED_OAK_INTO_LAB_2 = true
|
||||
Commands.face_object(ctx, 1, "up")
|
||||
-- res BIT_NO_MAP_MUSIC + PlayDefaultMusic: the lab theme
|
||||
-- only starts once the walk-in is done
|
||||
Music.playMap(game.data, "OAKS_LAB")
|
||||
chooseMonSpeech()
|
||||
end)
|
||||
@@ -179,10 +190,8 @@ M.PALLET_TOWN = {
|
||||
end
|
||||
end
|
||||
|
||||
-- PalletMovementScript_Done + the door warp: Oak is hidden as the
|
||||
-- player steps into the doorway; PALLET_TOWN warp 3 -> OAKS_LAB
|
||||
-- warp 2 = (5,11), with the door SFX (WarpFound -> SFX_GO_INSIDE)
|
||||
local function enterLab()
|
||||
local function enterLab(oak)
|
||||
if oak then oak.stepFrames = nil end
|
||||
Commands.hide_object(ctx, "PALLET_TOWN", "PALLETTOWN_OAK")
|
||||
Commands.show_object(ctx, "OAKS_LAB", "OAKSLAB_OAK2")
|
||||
ow.doorWarp = true
|
||||
@@ -190,25 +199,23 @@ M.PALLET_TOWN = {
|
||||
{ keepMusic = true })
|
||||
end
|
||||
|
||||
-- PalletMovementScript_WalkToLab: Oak's NPC movement and the
|
||||
-- player's simulated joypad run simultaneously, in lockstep; the
|
||||
-- player retraces Oak's path one step behind and follows him into
|
||||
-- the doorway on the final beat
|
||||
local function walkToLab(oak)
|
||||
-- lockstep half runs Oak on the player's own frames per cell
|
||||
-- engine/overworld/movement.asm:737 (DoScriptedNPCMovement)
|
||||
local i = 0
|
||||
if oak then
|
||||
oak.stepFrames = ow.player.stepFramesCur or ow.player.stepFrames
|
||||
end
|
||||
local function tick()
|
||||
i = i + 1
|
||||
local playerStep = escort.playerSteps[i]
|
||||
if not playerStep then
|
||||
enterLab()
|
||||
enterLab(oak)
|
||||
return
|
||||
end
|
||||
if oak and escort.oakSteps[i] then
|
||||
ow:scriptMove(oak, escort.oakSteps[i], 1)
|
||||
elseif oak then
|
||||
-- RLEList_ProfOakWalkToLab's trailing NPC_CHANGE_FACING beat:
|
||||
-- Oak marches in place on the door mat while the player takes
|
||||
-- the final step up behind him (movement.asm ChangeFacingDirection)
|
||||
ow:marchInPlace(oak)
|
||||
end
|
||||
ow:scriptMove(ow.player, playerStep, 1, tick)
|
||||
@@ -216,10 +223,14 @@ M.PALLET_TOWN = {
|
||||
tick()
|
||||
end
|
||||
|
||||
-- PalletMovementScript_OakMoveLeft/_PlayerMoveLeft: from the right
|
||||
-- tile (x == 11) Oak sidesteps left first, then the player follows
|
||||
-- left (wNumStepsToTake = wXCoord - 10), and only then both walk
|
||||
local function escortToLab(oak)
|
||||
-- PalletMovementScript_OakMoveLeft
|
||||
-- (engine/overworld/auto_movement.asm) starts MUSIC_MUSEUM_GUY
|
||||
-- when the escort begins in Yellow. Until then, Pallet Town plays
|
||||
-- after the battle; Red/Blue leave MUSIC_MEET_PROF_OAK playing.
|
||||
if yellow then
|
||||
Music.play(game.data, "Music_MuseumGuy")
|
||||
end
|
||||
local numSteps = x - 10
|
||||
if oak and numSteps > 0 then
|
||||
ow:scriptMove(oak, "left", numSteps, function()
|
||||
@@ -232,37 +243,75 @@ M.PALLET_TOWN = {
|
||||
end
|
||||
end
|
||||
|
||||
-- ---- Pallet Town side (scripts/PalletTown.asm) -----------------
|
||||
-- After Oak reaches the player: Red goes straight to "It's unsafe!".
|
||||
-- Yellow (PalletTownOakGreetsPlayerScript..AfterPikachuBattleScript):
|
||||
-- ThatWasClose -> Oak faces the grass patch -> BATTLE_TYPE_PIKACHU
|
||||
-- (the old-man-style simulated battle where PROF.OAK throws the ball
|
||||
-- and always catches the lv5 Pikachu) -> Whew -> ComeWithMe.
|
||||
local function afterOakArrives(oak)
|
||||
if not yellow then
|
||||
game.stack:push(TextBox.new(game,
|
||||
t._PalletTownOakItsUnsafeText
|
||||
or "OAK: It's unsafe!\nWild POKéMON\nlive in tall grass!",
|
||||
function() escortToLab(oak) end))
|
||||
return
|
||||
end
|
||||
local function comeWithMe()
|
||||
game.stack:push(TextBox.new(game,
|
||||
t._PalletTownOakComeWithMe
|
||||
or "OAK: Here, come with\nme!",
|
||||
function() escortToLab(oak) end))
|
||||
end
|
||||
local function afterPikaBattle()
|
||||
if oak then oak.facing = "up" end
|
||||
game.stack:push(TextBox.new(game,
|
||||
t._PalletTownOakWhewText or "OAK: Whew...",
|
||||
comeWithMe))
|
||||
end
|
||||
game.stack:push(TextBox.new(game,
|
||||
t._PalletTownOakThatWasCloseText
|
||||
or "OAK: That was\nclose!\fWild POKéMON live\nin tall grass!",
|
||||
function()
|
||||
-- Oak turns toward the horizontally adjacent grass (left exit
|
||||
-- looks right, right exit looks left -- the
|
||||
-- EVENT_PLAYER_AT_RIGHT_EXIT_TO_PALLET_TOWN branch).
|
||||
-- In pokeyellow, PalletTownOakGreetsPlayerScript turns Oak and
|
||||
-- PalletTownPikachuBattleScript arms the battle on the next
|
||||
-- overworld iteration. OverworldLoopLessDelay
|
||||
-- (home/overworld.asm) burns two DelayFrame calls at the top
|
||||
-- of each iteration and calls RunMapScript before checking
|
||||
-- wCurOpponent, so those two DelayFrame calls are what keep
|
||||
-- Oak's turn on screen before the battle check fires.
|
||||
if oak then oak.facing = x == 10 and "right" or "left" end
|
||||
hold(2, nil, function()
|
||||
local battle = BattleState.newWild(game, "PIKACHU", 5)
|
||||
battle:makeOldManDemo("PROF.OAK")
|
||||
battle.onFinish = function()
|
||||
afterPikaBattle()
|
||||
end
|
||||
-- Use the standard wild-battle entry transition.
|
||||
Commands.pushBattle(ctx, battle)
|
||||
end)
|
||||
end))
|
||||
end
|
||||
|
||||
-- PalletTownOakWalksToPlayerScript: Oak appears at (8,5), faces up
|
||||
-- (SetSpriteFacingDirectionAndDelay + Delay3), then zigzags to the
|
||||
-- player; the "It's unsafe!" text follows and the escort begins
|
||||
local function oakAppearsAndWalks()
|
||||
Commands.show_object(ctx, "PALLET_TOWN", "PALLETTOWN_OAK")
|
||||
local oak = ow:npcByIndex(1)
|
||||
local oak = npcNamed(ow, "PALLETTOWN_OAK") or ow:npcByIndex(1)
|
||||
if oak then oak.facing = "up" end
|
||||
hold(6, nil, function()
|
||||
walkList(oak, escort.oakApproach(x), function()
|
||||
-- PalletTownOakNotSafeComeWithMeScript: the second text waits
|
||||
-- for a button, then the escort starts
|
||||
game.stack:push(TextBox.new(game,
|
||||
t._PalletTownOakItsUnsafeText
|
||||
or "OAK: It's unsafe!\nWild POKéMON\nlive in tall grass!",
|
||||
function() escortToLab(oak) end))
|
||||
afterOakArrives(oak)
|
||||
end)
|
||||
end)
|
||||
end
|
||||
|
||||
-- The "Hey! Wait!" box ends without a button wait (auto), then the
|
||||
-- "!" bubble shows over the player WHILE the box is still on screen
|
||||
-- (PalletTownOakText: DelayFrames 10 then EmotionBubble, box not yet
|
||||
-- cleared). onOverlap sets the bubble during the box's last frames;
|
||||
-- the box pops after `overlap`, and the bubble's 60-frame hold then
|
||||
-- runs to Oak's appearance (the bubble is static while the box is up,
|
||||
-- since the overworld pauses under it, so 10 overlap + 50 = 60).
|
||||
game.stack:push(TextBox.new(game,
|
||||
t._PalletTownOakHeyWaitDontGoOutText or "OAK: Hey! Wait!\nDon't go out!",
|
||||
nil, { auto = { delay = 10, overlap = 10, onOverlap = function()
|
||||
-- .HeyWaitDontGoOutText turns the player to face down (toward
|
||||
-- the approaching Oak) before the exclamation bubble
|
||||
ow.player.facing = "down"
|
||||
ow.emote = { npc = ow.player, frames = 50, onDone = oakAppearsAndWalks }
|
||||
end } }))
|
||||
return true
|
||||
@@ -382,39 +431,170 @@ M.ROUTE_8_GATE = saffronGate("TEXT_ROUTE8GATE_GUARD", { { 2, 3 }, { 2, 4 } }, tr
|
||||
-- -------------------------------------------------------------------
|
||||
|
||||
M.POKEMON_FAN_CLUB = {
|
||||
onEnter = function(game, ow)
|
||||
require("src.world.PikachuFollower").onFanClubEntered(game, ow)
|
||||
end,
|
||||
talk = {
|
||||
TEXT_POKEMONFANCLUB_CHAIRMAN = {
|
||||
{ "face_player" }, -- 1
|
||||
{ "check_flag", "EVENT_RECEIVED_BIKE_VOUCHER" }, -- 2
|
||||
{ "jump_if_true", 9 }, -- 3
|
||||
{ "show_text", "_PokemonFanClubChairmanIntroText" }, -- 4
|
||||
{ "show_text", "_PokemonFanClubChairmanStoryText" }, -- 5
|
||||
{ "jump_if_true", "nothing_left" }, -- 3
|
||||
-- YesNoChoice (scripts/PokemonFanClub.asm): NO forfeits the voucher (#1050)
|
||||
{ "ask", "_PokemonFanClubChairmanIntroText" }, -- 4
|
||||
{ "jump_if_false", "no_story" }, -- 5
|
||||
{ "show_text", "_PokemonFanClubChairmanStoryText" }, -- 6
|
||||
-- give-then-print like scripts/PokemonFanClub.asm (GiveItem
|
||||
-- fills wStringBuffer; the received text reads it)
|
||||
{ "give_item", "BIKE_VOUCHER", 1, false }, -- 6
|
||||
{ "show_text", "_PokemonFanClubReceivedBikeVoucherText" }, -- 7
|
||||
{ "set_flag", "EVENT_RECEIVED_BIKE_VOUCHER" }, -- 8
|
||||
{ "show_text", "_PokemonFanClubExplainBikeVoucherText" }, -- 9
|
||||
{ "give_item", "BIKE_VOUCHER", 1, false }, -- 7
|
||||
{ "show_text", "_PokemonFanClubReceivedBikeVoucherText" }, -- 8
|
||||
{ "set_flag", "EVENT_RECEIVED_BIKE_VOUCHER" }, -- 9
|
||||
{ "show_text", "_PokemonFanClubExplainBikeVoucherText" }, -- 10
|
||||
{ "jump", "end" }, -- 11
|
||||
{ "label", "no_story" }, -- 12
|
||||
{ "show_text", "_PokemonFanClubNoStoryText" }, -- 13
|
||||
{ "jump", "end" }, -- 14
|
||||
-- .nothingleft: the gift is done, he only reminisces now
|
||||
{ "label", "nothing_left" }, -- 15
|
||||
{ "show_text", "_PokemonFanClubChairFinalText" }, -- 16
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
-- BikeShopClerkText (scripts/BikeShop.asm) runs three ways: the BICYCLE is
|
||||
-- already yours, you are carrying the BIKE VOUCHER, or you get the sales
|
||||
-- pitch. The pitch draws its own window (TextBoxBorder hlcoord 0,0, b=4
|
||||
-- c=15) holding BikeShopMenuText and BikeShopMenuPrice, and leaves it up
|
||||
-- while the clerk keeps talking in the bottom box: the original never
|
||||
-- erases it before TextScriptEnd (#568).
|
||||
local BikeShopWindow = {}
|
||||
BikeShopWindow.__index = BikeShopWindow
|
||||
|
||||
function BikeShopWindow.new(game, footer, onChoose)
|
||||
local self = setmetatable({}, BikeShopWindow)
|
||||
self.game = game
|
||||
self.onChoose = onChoose
|
||||
self.index = 1
|
||||
self.active = true
|
||||
-- BikeShopClerkDoYouLikeItText stays on screen under the window for as
|
||||
-- long as the menu is up. The text box pushed on top types it out and
|
||||
-- pops itself; this copy of its last page takes over from there, so the
|
||||
-- bottom box never blanks between the pitch and the answer. Paginated
|
||||
-- with the themed column budget so the copy breaks where the box did.
|
||||
local TextBox = require("src.render.TextBox")
|
||||
local box = require("src.ui.Theme").textBox or {}
|
||||
local pages = TextBox.paginate(TextBox.substitute(game, footer), box.maxCols)
|
||||
self.footer = pages[#pages]
|
||||
return self
|
||||
end
|
||||
|
||||
function BikeShopWindow:update()
|
||||
-- one answer only: the boxes pushed by onChoose sit on top of this
|
||||
-- state, but a second A on the same frame must not fire it twice
|
||||
if not self.active then return end
|
||||
local input = self.game.input
|
||||
-- HandleMenuInput with wMenuWrappingEnabled clear: the two rows clamp
|
||||
if input:wasPressed("up") then
|
||||
self.index = 1
|
||||
elseif input:wasPressed("down") then
|
||||
self.index = 2
|
||||
elseif input:wasPressed("a") or input:wasPressed("b") then
|
||||
local cancelled = input:wasPressed("b") -- bit B_PAD_B -> .cancel
|
||||
require("src.core.Sound").play(self.game.data, "Press_AB")
|
||||
self.active = false
|
||||
self.onChoose(not cancelled and self.index == 1)
|
||||
end
|
||||
end
|
||||
|
||||
function BikeShopWindow:draw()
|
||||
local Font = require("src.render.Font")
|
||||
local Strings = require("src.core.Strings")
|
||||
local Theme = require("src.ui.Theme")
|
||||
Font.drawBox(0, 0, 17, 6)
|
||||
love.graphics.setColor(0, 0, 0, 1)
|
||||
local bike = self.game.data.items.BICYCLE
|
||||
Font.draw(bike and bike.name or "BICYCLE", 16, 16) -- hlcoord 2, 2
|
||||
Font.draw("¥1000000", 64, 24) -- hlcoord 8, 3
|
||||
Font.draw(Strings("CANCEL"), 16, 32) -- `next` skips a row
|
||||
-- wTopMenuItemX 1, wTopMenuItemY 2, rows two apart
|
||||
Font.drawCode(Theme.cursor, 8, self.index == 1 and 16 or 32)
|
||||
if self.footer then
|
||||
-- same geometry TextBox resolves against, so a themed box matches
|
||||
local box = Theme.textBox or {}
|
||||
local tx, ty = box.tx or 0, box.ty or 12
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
Font.drawBox(tx, ty, box.tw or 20, box.th or 6)
|
||||
love.graphics.setColor(0, 0, 0, 1)
|
||||
for i, line in ipairs(self.footer) do
|
||||
Font.draw(line, (tx + 1) * 8, (ty + 2 * i) * 8)
|
||||
end
|
||||
end
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
end
|
||||
|
||||
M.BIKE_SHOP = {
|
||||
talk = {
|
||||
TEXT_BIKESHOP_CLERK = function(game, ow, npc, done)
|
||||
local Bag = require("src.inventory.Bag")
|
||||
local Flags = require("src.script.Flags")
|
||||
local TextBox = require("src.render.TextBox")
|
||||
if (game.save.inventory.BICYCLE or 0) > 0 then
|
||||
game.stack:push(TextBox.new(game, "How's the\nBICYCLE treating\nyou?", done))
|
||||
elseif (game.save.inventory.BIKE_VOUCHER or 0) > 0 then
|
||||
game.save.inventory.BIKE_VOUCHER = nil
|
||||
game.save.inventory.BICYCLE = 1
|
||||
game.stack:push(TextBox.new(game,
|
||||
("Oh, that's a\nBIKE VOUCHER!\f%s exchanged\nit for a BICYCLE!")
|
||||
:format(game.save.player.name), done))
|
||||
else
|
||||
game.stack:push(TextBox.new(game,
|
||||
"A BICYCLE costs\n¥1000000. Sorry,\nno instalments!", done))
|
||||
local t = game.data.text
|
||||
local function say(text, after, opts)
|
||||
game.stack:push(TextBox.new(game, text, after, opts))
|
||||
end
|
||||
|
||||
-- CheckEvent EVENT_GOT_BICYCLE. Saves made before the clerk started
|
||||
-- setting the event still have the bike in the bag, so either counts.
|
||||
if (game.save.inventory.BICYCLE or 0) > 0
|
||||
or Flags.get(game.save, "EVENT_GOT_BICYCLE") then
|
||||
say(t._BikeShopClerkHowDoYouLikeYourBicycleText, done)
|
||||
return
|
||||
end
|
||||
|
||||
-- .dontHaveBike: IsItemInBag BIKE_VOUCHER
|
||||
if (game.save.inventory.BIKE_VOUCHER or 0) > 0 then
|
||||
say(t._BikeShopClerkOhThatsAVoucherText, function()
|
||||
-- GiveItem's `jr nc, .BagFull`: the voucher is only spent once
|
||||
-- the BICYCLE is actually in the bag
|
||||
if not Bag.add(game.save, "BICYCLE", 1) then
|
||||
say(t._BikeShopBagFullText, done)
|
||||
return
|
||||
end
|
||||
Bag.remove(game.save, "BIKE_VOUCHER", 1)
|
||||
Flags.set(game.save, "EVENT_GOT_BICYCLE")
|
||||
-- BikeShopExchangedVoucherText carries sound_get_key_item; the
|
||||
-- map runs EnableAutoTextBoxDrawing, so the box still waits for
|
||||
-- a button once the jingle has played (auto.wait, #247)
|
||||
say(t._BikeShopExchangedVoucherText, done, {
|
||||
auto = { wait = true, sound = function()
|
||||
return require("src.core.Sound").play(game.data, "Get_Key_Item")
|
||||
end },
|
||||
})
|
||||
end)
|
||||
return
|
||||
end
|
||||
|
||||
-- .dontHaveVoucher: welcome, then the BICYCLE/CANCEL window
|
||||
say(t._BikeShopClerkWelcomeText, function()
|
||||
local pitch = t._BikeShopClerkDoYouLikeItText
|
||||
game.stack:push(BikeShopWindow.new(game, pitch, function(bought)
|
||||
local function comeAgain()
|
||||
say(t._BikeShopComeAgainText, function()
|
||||
game.stack:pop() -- the window, still up under the text
|
||||
done()
|
||||
end)
|
||||
end
|
||||
if bought then
|
||||
-- a million is out of anyone's reach: BikeShopCantAffordText
|
||||
say(t._BikeShopCantAffordText, comeAgain)
|
||||
else
|
||||
comeAgain()
|
||||
end
|
||||
end))
|
||||
-- PrintText BikeShopClerkDoYouLikeItText, then straight into
|
||||
-- HandleMenuInput: the box types out and hands over without
|
||||
-- waiting, leaving the window's copy of the line on screen
|
||||
say(pitch, nil, { auto = { delay = 0 } })
|
||||
end)
|
||||
end,
|
||||
},
|
||||
}
|
||||
@@ -486,8 +666,10 @@ local function mtMoonFossil(itemId, otherName, gotFlag)
|
||||
end
|
||||
local idef = game.data.items[itemId]
|
||||
game.stringBuffer = idef and idef.name or itemId
|
||||
require("src.core.Sound").play(game.data, "Get_Key_Item")
|
||||
local dirs = mtMoonNerdWalk(ow.player.cellX, ow.player.cellY, itemId)
|
||||
-- MtMoonB2FReceivedFossilText: text_far, sound_get_key_item,
|
||||
-- text_waitbutton -- the jingle plays after the box has typed and
|
||||
-- the button wait comes after it
|
||||
game.stack:push(TextBox.new(game,
|
||||
t._MtMoonB2FReceivedFossilText
|
||||
or ("{PLAYER} got the\n" .. game.stringBuffer .. "!"),
|
||||
@@ -500,11 +682,11 @@ local function mtMoonFossil(itemId, otherName, gotFlag)
|
||||
ow.runner:run({
|
||||
{ "walk_npc", 1, dirs },
|
||||
{ "text_opts", { auto = true } },
|
||||
{ "text_sound", "Get_Key_Item" },
|
||||
{ "show_text", "_MtMoonB2FSuperNerdThenThisIsMineText" },
|
||||
{ "play_sound", "Get_Key_Item" },
|
||||
{ "hide_object", "MT_MOON_B2F", otherName },
|
||||
}, { onDone = done })
|
||||
end))
|
||||
end, TextBox.soundOpts(game, "Get_Key_Item")))
|
||||
end }))
|
||||
end
|
||||
end
|
||||
@@ -520,6 +702,23 @@ M.MT_MOON_B2F = {
|
||||
return false
|
||||
end,
|
||||
talk = {
|
||||
-- MtMoonB2FSuperNerdText: once beaten his line turns on the fossils
|
||||
-- (scripts/MtMoonB2F.asm:187), which the header's flat `after` can't hold
|
||||
TEXT_MTMOONB2F_SUPER_NERD = function(game, ow, npc, done)
|
||||
if not superNerdBeaten(ow) then
|
||||
engageSuperNerd(game, ow, done)
|
||||
return
|
||||
end
|
||||
local TextBox = require("src.render.TextBox")
|
||||
local t = game.data.text
|
||||
local flags = game.save.flags
|
||||
local line = (flags.EVENT_GOT_DOME_FOSSIL or flags.EVENT_GOT_HELIX_FOSSIL)
|
||||
and (t._MtMoonB2FSuperNerdTheresAPokemonLabText
|
||||
or "Far away, on\nCINNABAR ISLAND,\nthere's a POKéMON\nLAB.")
|
||||
or (t._MtMoonB2fSuperNerdEachTakeOneText
|
||||
or "We'll each take\none!\nNo being greedy!")
|
||||
game.stack:push(TextBox.new(game, line, done))
|
||||
end,
|
||||
TEXT_MTMOONB2F_DOME_FOSSIL = mtMoonFossil(
|
||||
"DOME_FOSSIL", "MTMOONB2F_HELIX_FOSSIL", "EVENT_GOT_DOME_FOSSIL"),
|
||||
TEXT_MTMOONB2F_HELIX_FOSSIL = mtMoonFossil(
|
||||
@@ -527,35 +726,37 @@ M.MT_MOON_B2F = {
|
||||
},
|
||||
}
|
||||
|
||||
-- The ticket clerk (scripts/Museum1F.asm Museum1FScientist1Text):
|
||||
-- Y50, once. Declining at the rope shoves the player one tile SOUTH back off
|
||||
-- the exhibit rope they crossed heading north (#151); the museum floor has no
|
||||
-- ledges, so a plain scriptMove("down",1) is the correct primitive.
|
||||
-- The ticket clerk (scripts/Museum1F.asm Museum1FScientist1Text): Y50, once.
|
||||
-- Declining at the rope shoves the player one tile south (#151)
|
||||
local function museumClerk(game, ow, done, onDecline)
|
||||
local TextBox = require("src.render.TextBox")
|
||||
local ChoiceBox = require("src.ui.ChoiceBox")
|
||||
local t = game.data.text or {}
|
||||
if game.save.flags.EVENT_BOUGHT_MUSEUM_TICKET then
|
||||
game.stack:push(TextBox.new(game,
|
||||
"Take your time,\nand enjoy it all!", done))
|
||||
return
|
||||
end
|
||||
-- scripts/Museum1F.asm:72
|
||||
local money = function() return game.save.money end
|
||||
game.stack:push(TextBox.new(game,
|
||||
"It's ¥50 for a\nchild's ticket.\fWould you like to\ncome in?", function()
|
||||
game.stack:push(ChoiceBox.new(game, function(yes)
|
||||
t._Museum1FScientist1WouldYouLikeToComeInText
|
||||
or "It's ¥50 for a\nchild's ticket.\fWould you like to\ncome in?",
|
||||
nil, { money = money, choice = function(yes)
|
||||
if yes and game.save.money >= 50 then
|
||||
game.save.money = game.save.money - 50
|
||||
game.save.flags.EVENT_BOUGHT_MUSEUM_TICKET = true
|
||||
-- scripts/Museum1F.asm:106
|
||||
game.stack:push(TextBox.new(game,
|
||||
"Right, ¥50!\nThank you!", done))
|
||||
t._Museum1FScientist1ThankYouText or "Right, ¥50!\nThank you!", done,
|
||||
{ money = money }))
|
||||
elseif yes then
|
||||
game.stack:push(TextBox.new(game,
|
||||
"You don't have\nenough money.", onDecline or done))
|
||||
"You don't have\nenough money.", onDecline or done, { money = money }))
|
||||
else
|
||||
game.stack:push(TextBox.new(game,
|
||||
"Come again!", onDecline or done))
|
||||
"Come again!", onDecline or done, { money = money }))
|
||||
end
|
||||
end))
|
||||
end))
|
||||
end }))
|
||||
end
|
||||
|
||||
M.MUSEUM_1F = {
|
||||
|
||||
@@ -111,8 +111,9 @@ M.POKEMON_TOWER_5F = {
|
||||
--
|
||||
-- PokemonTower6FDefaultScript starts the RESTLESS SOUL battle with NO
|
||||
-- Silph Scope check at the trigger -- the scope only decides whether the
|
||||
-- battle is disguised (IsGhostBattle -> makeGhost: "too scared to move",
|
||||
-- balls dodged). An earlier version of this port turned the player back
|
||||
-- disguise sticks (IsGhostBattle -> makeGhost: "too scared to move", balls
|
||||
-- dodged) or comes off in the unveil (makeUnveiledGhost, #492). An earlier
|
||||
-- version of this port turned the player back
|
||||
-- without the scope and never opened the battle, which made 6F
|
||||
-- impassable on any route that skips Rocket Hideout; vanilla lets the
|
||||
-- battle open and a POKE_DOLL end it (see wBattleResult below).
|
||||
@@ -128,7 +129,18 @@ M.POKEMON_TOWER_6F = {
|
||||
t._PokemonTower6FBeGoneText or "Be gone...\nIntruders...", function()
|
||||
local BattleState = require("src.battle.BattleState")
|
||||
local battle = BattleState.newWild(game, "MAROWAK", 30)
|
||||
if not game.save.inventory.SILPH_SCOPE then
|
||||
-- ItemUseBall .notOldManBattle (item_effects.asm:166-175): a ball
|
||||
-- thrown on POKEMON_TOWER_6F at the RESTLESS SOUL is dodged whether
|
||||
-- or not the scope revealed it, so the "can't be caught" state rides
|
||||
-- the battle instead of IsGhostBattle alone (#444)
|
||||
battle.noCatch = true
|
||||
-- InitWildBattle enters disguised for the RESTLESS SOUL either way
|
||||
-- (core.asm:6698-6700). The scope does not skip the disguise, it
|
||||
-- buys the unveil PrintBeginningBattleText .isMarowak plays over it
|
||||
-- before the battle proceeds as an ordinary wild one (#492).
|
||||
if game.save.inventory.SILPH_SCOPE then
|
||||
battle:makeUnveiledGhost()
|
||||
else
|
||||
battle:makeGhost()
|
||||
end
|
||||
battle.onFinish = function(result)
|
||||
@@ -139,9 +151,27 @@ M.POKEMON_TOWER_6F = {
|
||||
-- trick, and the speedrun route this bot follows depends on it.
|
||||
if result == "win" or battle.pokeDollEscape then
|
||||
game.save.flags.EVENT_BEAT_GHOST_MAROWAK = true
|
||||
game.stack:push(TextBox.new(game,
|
||||
t._PokemonTower6FSoulWasCalmedText
|
||||
or "The mother's soul\nwas calmed.\012It departed to\nthe afterlife!"))
|
||||
-- PokemonTower6FMarowakDepartedText (scripts/PokemonTower6F.asm)
|
||||
-- is two texts, not one: the CUBONE's-mother line first, then
|
||||
-- PlayCry RESTLESS_SOUL (EQU MAROWAK, constants/pokemon_constants
|
||||
-- .asm:209) + WaitForSoundToFinish + DelayFrames 30 before the
|
||||
-- calmed line; the port dropped the first text and the cry
|
||||
-- (#867). play_cry arms the next show_text, so the cry rides
|
||||
-- the calmed box's open with the button prompt kept, and the
|
||||
-- wait row stands in for the asm's 30-frame gap.
|
||||
local rows = {
|
||||
{ "show_text", t._PokemonTower6FGhostWasCubonesMotherText
|
||||
or "The GHOST was the\nrestless soul of\vCUBONE's mother!" },
|
||||
{ "play_cry", "MAROWAK", true },
|
||||
{ "wait", 30 },
|
||||
{ "show_text", t._PokemonTower6FSoulWasCalmedText
|
||||
or "The mother's soul\nwas calmed.\012It departed to\nthe afterlife!" },
|
||||
}
|
||||
if ow.runner then
|
||||
ow.runner:run(rows)
|
||||
elseif ow.queueScript then
|
||||
ow:queueScript(rows)
|
||||
end
|
||||
elseif result ~= "lose" then
|
||||
-- .did_not_defeat: one simulated step right, off the trigger,
|
||||
-- so fleeing does not leave you standing on a cell that
|
||||
@@ -182,17 +212,16 @@ M.POKEMON_TOWER_6F = {
|
||||
-- preFrames: the shake's lead-in delays -- ShakeElevator's own Delay3s
|
||||
-- come to 9 frames; Silph/Rocket's ...ShakeScript prefixes another
|
||||
-- Delay3 (12) while CeladonMartElevatorShakeScript farjps straight in
|
||||
-- The panel is a bg_event, not a map-entry script: data/maps/objects/
|
||||
-- CeladonMartElevator.asm has `bg_event 3, 0, TEXT_CELADONMARTELEVATOR`
|
||||
-- and CeladonMartElevatorText is what runs DisplayElevatorFloorMenu, so
|
||||
-- the menu waits for the player to face the panel and press A (#395).
|
||||
-- Map entry only stores the car's exit warps (CeladonMartElevator
|
||||
-- StoreWarpEntriesScript), which is elevatorSeedExit below.
|
||||
-- After the ride the original does NOT jump-cut to the floor: choosing a
|
||||
-- floor in engine/events/elevator.asm DisplayElevatorFloorMenu rewrites
|
||||
-- the elevator car's own warp entries (wWarpEntries, via .UpdateWarp) to
|
||||
-- the chosen floor's exit warp, then the player walks out of the car onto
|
||||
-- that warp themselves (scripts/SilphCoElevator.asm etc.). Reproduce
|
||||
-- that here: rewrite the car's exit warps, then drive a short scripted
|
||||
-- walk-out onto an exit tile and take the (now rewritten) warp, reusing
|
||||
-- ow:scriptMove / ow:takeWarp (the Oak-escort primitives).
|
||||
local WALK_DIRVEC = { up = { 0, -1 }, down = { 0, 1 }, left = { -1, 0 }, right = { 1, 0 } }
|
||||
local WALK_OPP = { up = "down", down = "up", left = "right", right = "left" }
|
||||
local WALK_ORDER = { "up", "down", "left", "right" }
|
||||
-- floor rewrites the elevator car's own warp entries (wWarpEntries, via
|
||||
-- .UpdateWarp) to the chosen floor's exit warp and returns control, and
|
||||
-- the player walks out of the car onto that warp themselves.
|
||||
|
||||
-- .UpdateWarp: point EVERY car exit warp at the same floor's elevator
|
||||
-- door (warp id, map id). Shared generated map data, but the car's
|
||||
@@ -205,46 +234,6 @@ local function elevatorSetExit(ow, floor)
|
||||
end
|
||||
end
|
||||
|
||||
local function elevatorWalkOut(ow, floor)
|
||||
local m, p = ow.map, ow.player
|
||||
elevatorSetExit(ow, floor)
|
||||
-- leave by the exit tile under the player (they warped in onto one),
|
||||
-- else the nearest
|
||||
local door
|
||||
for _, w in ipairs(m.def.warps) do
|
||||
if w.x == p.cellX and w.y == p.cellY then door = w break end
|
||||
end
|
||||
if not door then
|
||||
local best
|
||||
for _, w in ipairs(m.def.warps) do
|
||||
local d = math.abs(w.x - p.cellX) + math.abs(w.y - p.cellY)
|
||||
if not best or d < best then best, door = d, w end
|
||||
end
|
||||
end
|
||||
-- the car interior sits on the door's walkable side; "out" is the
|
||||
-- doorway direction (the map edge for Silph/Celadon, the top doorway
|
||||
-- for the Rocket car). Fixed direction order keeps the step
|
||||
-- deterministic when a door tile has several walkable neighbours
|
||||
-- (Silph/Celadon step up into the car, the Rocket car steps down).
|
||||
local into
|
||||
for _, dir in ipairs(WALK_ORDER) do
|
||||
local v = WALK_DIRVEC[dir]
|
||||
local nx, ny = door.x + v[1], door.y + v[2]
|
||||
if m:inBounds(nx, ny) and m:isWalkableCell(nx, ny) then into = dir break end
|
||||
end
|
||||
into = into or "up"
|
||||
local out = WALK_OPP[into]
|
||||
local function leave()
|
||||
ow:takeWarp(door) -- door SFX + warp to the rewritten floor target
|
||||
end
|
||||
-- step one tile into the car, then walk back through the doorway onto
|
||||
-- the exit tile and take the warp: a visible walk-out on valid tiles
|
||||
-- for either door orientation, instead of a jump cut
|
||||
ow:scriptMove(p, into, 1, function()
|
||||
ow:scriptMove(p, out, 1, leave)
|
||||
end)
|
||||
end
|
||||
|
||||
local function elevatorFloors(elevatorMapId, game)
|
||||
local floors = {}
|
||||
for mapId, def in pairs(game.data.maps) do
|
||||
@@ -272,9 +261,9 @@ local function elevatorFloors(elevatorMapId, game)
|
||||
end
|
||||
|
||||
local function elevatorSeedExit(ow, floors, fromMapId)
|
||||
-- Seed a walk-out destination before the menu (or key-gate text):
|
||||
-- entry floor when known, else the first listed floor (1F). Choosing
|
||||
-- a floor still rewrites via elevatorWalkOut; B-cancel / no-key leave
|
||||
-- Seed a walk-out destination on entry (before the panel is ever
|
||||
-- read): entry floor when known, else the first listed floor (1F).
|
||||
-- Choosing a floor rewrites it again; B-cancel / no-key leave
|
||||
-- keeps this seed so walking out of the car cannot hit a missing ROM
|
||||
-- placeholder (#123) or the car's static default floor (#90: Rocket
|
||||
-- Hideout defaults to B1F even when entered from B2F/B4F).
|
||||
@@ -288,68 +277,68 @@ local function elevatorSeedExit(ow, floors, fromMapId)
|
||||
return exitFloor
|
||||
end
|
||||
|
||||
local function elevator(elevatorMapId, keyGate, preFrames)
|
||||
local function elevator(elevatorMapId, panelText, keyGate, preFrames)
|
||||
-- the panel bg_event: DisplayElevatorFloorMenu runs from this text
|
||||
-- script, never from map entry (#395)
|
||||
local function panel(game, ow, npc, done)
|
||||
done = done or function() end
|
||||
local floors = elevatorFloors(elevatorMapId, game)
|
||||
-- Rocket Hideout: without LIFT_KEY the panel only prints the need-
|
||||
-- a-key line and shows no floor menu
|
||||
-- (scripts/RocketHideoutElevator.asm).
|
||||
if keyGate and not game.save.inventory[keyGate.item] then
|
||||
local TextBox = require("src.render.TextBox")
|
||||
game.stack:push(TextBox.new(game,
|
||||
game.data.text[keyGate.text] or "It appears to\nneed a key.", done))
|
||||
return
|
||||
end
|
||||
local items = {}
|
||||
for _, f in ipairs(floors) do
|
||||
table.insert(items, { label = f.token, value = f })
|
||||
end
|
||||
local ListMenu = require("src.ui.ListMenu")
|
||||
game.stack:push(ListMenu.new(game, "WHICH FLOOR?", items, {
|
||||
onChoose = function(item, list)
|
||||
list:close()
|
||||
-- the whole ShakeElevator ride runs in place -- music stop, 100
|
||||
-- collision-thud scroll bounces, the PA chime -- and only then
|
||||
-- does .UpdateWarp's rewrite land, with the player still stood
|
||||
-- at the panel: they walk out to the car door themselves
|
||||
local ElevatorShake = require("src.world.ElevatorShake")
|
||||
game.stack:push(ElevatorShake.new(game, ow, {
|
||||
preFrames = preFrames,
|
||||
onDone = function()
|
||||
elevatorSetExit(ow, item.value)
|
||||
done()
|
||||
end,
|
||||
}))
|
||||
end,
|
||||
onCancel = function()
|
||||
-- DisplayElevatorFloorMenu: `ret c` on B -- no warp, nothing
|
||||
-- happens, the player just stays in the car (exit warps were
|
||||
-- already seeded on entry)
|
||||
done()
|
||||
end,
|
||||
}))
|
||||
end
|
||||
return {
|
||||
-- fromMapId: the floor the player just left (setMap passes it), so a
|
||||
-- B-cancel can still walk out onto a real map. Silph's ROM car warps
|
||||
-- default to UNUSED_MAP_ED, which is not in Data.maps -- Warp.resolve
|
||||
-- asserted and hard-crashed (#123).
|
||||
onEnter = function(game, ow, fromMapId)
|
||||
local floors = elevatorFloors(elevatorMapId, game)
|
||||
elevatorSeedExit(ow, floors, fromMapId)
|
||||
-- Rocket Hideout: without LIFT_KEY the panel only prints the need-
|
||||
-- a-key line (scripts/RocketHideoutElevator.asm). Exit warps are
|
||||
-- still seeded above so walking out returns to the entry floor
|
||||
-- instead of the car's ROM default (B1F) -- #90 / #105.
|
||||
if keyGate and not game.save.inventory[keyGate.item] then
|
||||
local TextBox = require("src.render.TextBox")
|
||||
game.stack:push(TextBox.new(game,
|
||||
game.data.text[keyGate.text] or "It appears to\nneed a key."))
|
||||
return
|
||||
end
|
||||
local items = {}
|
||||
for _, f in ipairs(floors) do
|
||||
table.insert(items, { label = f.token, value = f })
|
||||
end
|
||||
local ListMenu = require("src.ui.ListMenu")
|
||||
game.stack:push(ListMenu.new(game, "WHICH FLOOR?", items, {
|
||||
onChoose = function(item, list)
|
||||
list:close()
|
||||
-- the map-entry Transition (startWarpTo) calls onEnter from
|
||||
-- its OWN midpoint callback, so this menu was pushed on top
|
||||
-- of that still-active Transition; only the top state
|
||||
-- updates, so it froze mid-fade instead of finishing. Left
|
||||
-- alone it would resurface after the ride and play a stray
|
||||
-- fade at the wrong time -- pop it now, before it can happen.
|
||||
local Transition = require("src.render.Transition")
|
||||
if getmetatable(game.stack:top()) == Transition then
|
||||
game.stack:pop()
|
||||
end
|
||||
-- the whole ShakeElevator ride runs in place -- music stop,
|
||||
-- 100 collision-thud scroll bounces, the PA chime -- and only
|
||||
-- then does the player walk out of the car onto the chosen
|
||||
-- floor (elevatorWalkOut rewrites the car's exit warps first)
|
||||
local ElevatorShake = require("src.world.ElevatorShake")
|
||||
game.stack:push(ElevatorShake.new(game, ow, {
|
||||
preFrames = preFrames,
|
||||
onDone = function()
|
||||
elevatorWalkOut(ow, item.value)
|
||||
end,
|
||||
}))
|
||||
end,
|
||||
onCancel = function()
|
||||
-- DisplayElevatorFloorMenu: `ret c` on B -- no warp, nothing
|
||||
-- happens, the player just stays in the car (exit warps were
|
||||
-- already seeded to the entry floor above)
|
||||
end,
|
||||
}))
|
||||
elevatorSeedExit(ow, elevatorFloors(elevatorMapId, game), fromMapId)
|
||||
end,
|
||||
talk = { [panelText] = panel },
|
||||
}
|
||||
end
|
||||
|
||||
M.SILPH_CO_ELEVATOR = elevator("SILPH_CO_ELEVATOR")
|
||||
M.CELADON_MART_ELEVATOR = elevator("CELADON_MART_ELEVATOR", nil, 9)
|
||||
M.SILPH_CO_ELEVATOR = elevator("SILPH_CO_ELEVATOR",
|
||||
"TEXT_SILPHCOELEVATOR_ELEVATOR")
|
||||
M.CELADON_MART_ELEVATOR = elevator("CELADON_MART_ELEVATOR",
|
||||
"TEXT_CELADONMARTELEVATOR", nil, 9)
|
||||
M.ROCKET_HIDEOUT_ELEVATOR = elevator("ROCKET_HIDEOUT_ELEVATOR",
|
||||
"TEXT_ROCKETHIDEOUTELEVATOR",
|
||||
{ item = "LIFT_KEY", text = "_RocketHideoutElevatorAppearsToNeedKeyText" })
|
||||
|
||||
-- -------------------------------------------------------------------
|
||||
@@ -389,6 +378,38 @@ M.ROCKET_HIDEOUT_B4F = {
|
||||
end))
|
||||
end,
|
||||
|
||||
-- Yellow swaps Rocket1/Rocket2 for Jessie & James and keeps a single
|
||||
-- grunt, spelled ROCKETHIDEOUTB4F_ROCKET / TEXT_ROCKETHIDEOUTB4F_ROCKET
|
||||
-- (pokeyellow/scripts/RocketHideoutB4F.asm), so the Red/Blue key above
|
||||
-- never matched there and the LIFT KEY never dropped (#552, a
|
||||
-- regression of #105). Yellow also moves the drop off the second
|
||||
-- talk: RocketHideoutB4FRocketEndBattleText is text_promptbutton +
|
||||
-- text_asm, so its SetEvent EVENT_ROCKET_DROPPED_LIFT_KEY / ShowObject
|
||||
-- TOGGLE_ROCKET_HIDEOUT_B4F_ITEM_5 run the instant the battle ends.
|
||||
TEXT_ROCKETHIDEOUTB4F_ROCKET = function(game, ow, npc, done)
|
||||
if not ow:trainerDefeated(npc) then
|
||||
ow:engageTrainer(npc, function()
|
||||
-- engageTrainer records the win before it calls back, so this
|
||||
-- is the end-battle text's SetEvent + ShowObject
|
||||
if ow:trainerDefeated(npc)
|
||||
and not game.save.flags.EVENT_ROCKET_DROPPED_LIFT_KEY then
|
||||
game.save.flags.EVENT_ROCKET_DROPPED_LIFT_KEY = true
|
||||
local Commands = require("src.script.Commands")
|
||||
Commands.show_object(
|
||||
{ game = game, save = game.save, overworld = ow },
|
||||
"ROCKET_HIDEOUT_B4F", "ROCKETHIDEOUTB4F_LIFT_KEY")
|
||||
end
|
||||
done()
|
||||
end)
|
||||
return
|
||||
end
|
||||
-- RocketHideoutB4FRocketAfterBattleText: later talks only reprint
|
||||
local TextBox = require("src.render.TextBox")
|
||||
game.stack:push(TextBox.new(game,
|
||||
game.data.text._RocketHideoutB4FRocketAfterBattleText
|
||||
or "Oh no! I dropped\nthe LIFT KEY!", done))
|
||||
end,
|
||||
|
||||
TEXT_ROCKETHIDEOUTB4F_GIOVANNI = function(game, ow, npc, done)
|
||||
-- Giovanni has no trainer-header row (def_trainers 2); his text_asm
|
||||
-- owns both the engage and the BeatGiovanniScript aftermath.
|
||||
@@ -514,6 +535,14 @@ M.GAME_CORNER = {
|
||||
done()
|
||||
return
|
||||
end
|
||||
-- GameCornerRocketText hands the battle its own loss line through
|
||||
-- SaveEndBattleTextPointers (.BattleEndText ->
|
||||
-- _GameCornerRocketBattleEndText, "Dang!"), and PrintEndBattleText
|
||||
-- prints it ON the battle screen between TrainerDefeatedText and
|
||||
-- MoneyForWinningText (engine/battle/core.asm TrainerBattleVictory).
|
||||
-- He is a text_asm trainer with no def_trainers header, so there is no
|
||||
-- header.won for engageTrainer to find and the line has to be handed
|
||||
-- over here or it never shows at all (#862).
|
||||
ow:engageTrainer(npc, function()
|
||||
if not ow:trainerDefeated(npc) then
|
||||
done()
|
||||
@@ -524,160 +553,329 @@ M.GAME_CORNER = {
|
||||
game.data.text._GameCornerRocketAfterBattleText
|
||||
or "Our hideout might\nbe discovered! I\nbetter tell BOSS!",
|
||||
function()
|
||||
-- #198: GameCornerRocketExitScript (scripts/GameCorner.asm)
|
||||
-- ApplyMovementData walks the grunt one tile UP into the poster
|
||||
-- (the hideout's secret entrance at 9,4) before HideObject, so
|
||||
-- he leaves the floor rather than popping out of existence on
|
||||
-- (9,5). scriptMove locks player input (#scriptMoves>0) and
|
||||
-- ignores collision, so we despawn + unfreeze (done) only once
|
||||
-- the step lands.
|
||||
ow:scriptMove(npc, "up", 1, function()
|
||||
hideRocket()
|
||||
done()
|
||||
end)
|
||||
-- #198/#862: GameCornerRocketBattleScript (scripts/GameCorner.asm)
|
||||
-- picks the exit walk from where the player is standing, because
|
||||
-- the grunt on (9,5) has to get past him: wYCoord == 6 (talked to
|
||||
-- from the south) or wXCoord == 8 (from the west) leaves the row
|
||||
-- clear and takes GameCornerMovement_Rocket_WalkDirect, five steps
|
||||
-- RIGHT; otherwise the player is east of him on (10,5) and
|
||||
-- GameCornerMovement_Rocket_WalkAroundPlayer steps DOWN, right, UP
|
||||
-- and right again to go AROUND him. pokeyellow's copy of the
|
||||
-- around-path takes one extra RIGHT on the lower row before coming
|
||||
-- back up (it also has to clear Pikachu); both versions end on
|
||||
-- (15,5). He never steps UP: (9,4) is the poster wall, which is
|
||||
-- where the old single UP step sent him.
|
||||
local px = ow.player and ow.player.cellX
|
||||
local py = ow.player and ow.player.cellY
|
||||
local path
|
||||
if py == 6 or px == 8 then
|
||||
path = { { "right", 5 } }
|
||||
elseif require("src.core.GameVersion").isYellow() then
|
||||
path = { { "down", 1 }, { "right", 3 }, { "up", 1 }, { "right", 3 } }
|
||||
else
|
||||
path = { { "down", 1 }, { "right", 2 }, { "up", 1 }, { "right", 4 } }
|
||||
end
|
||||
-- GameCornerRocketExitScript only HideObjects him once
|
||||
-- BIT_SCRIPTED_NPC_MOVEMENT clears, i.e. after the last step.
|
||||
-- scriptMove locks player input (#scriptMoves>0) and ignores
|
||||
-- collision, so the despawn + unfreeze (done) ride the final step.
|
||||
local function step(i)
|
||||
if i > #path then
|
||||
hideRocket()
|
||||
done()
|
||||
return
|
||||
end
|
||||
ow:scriptMove(npc, path[i][1], path[i][2],
|
||||
function() step(i + 1) end)
|
||||
end
|
||||
step(1)
|
||||
end))
|
||||
end)
|
||||
end, game.data.text._GameCornerRocketBattleEndText or "Dang!")
|
||||
end,
|
||||
-- GameCornerClerk1Text (scripts/GameCorner.asm): the offer, a
|
||||
-- YesNoChoice, then ¥1000 for 50 coins. Yellow drops the "1" from the
|
||||
-- object const and from every one of his text labels
|
||||
-- (GameCornerClerkText, pokeyellow/scripts/GameCorner.asm) with an
|
||||
-- identical body, so each line resolves under both spellings and the
|
||||
-- handler is bound to both text ids just below (#552).
|
||||
TEXT_GAMECORNER_CLERK1 = function(game, ow, npc, done)
|
||||
local TextBox = require("src.render.TextBox")
|
||||
local ChoiceBox = require("src.ui.ChoiceBox")
|
||||
local Font = require("src.render.Font")
|
||||
local Strings = require("src.core.Strings")
|
||||
local t = game.data.text
|
||||
local function line(suffix, fallback)
|
||||
return t["_GameCornerClerk1" .. suffix]
|
||||
or t["_GameCornerClerk" .. suffix]
|
||||
or fallback
|
||||
end
|
||||
-- GameCornerDrawCoinBox (scripts/GameCorner.asm; pokeyellow's copy is
|
||||
-- identical): TextBoxBorder at hlcoord 11,0 with b=5 c=7, a 9x7-tile
|
||||
-- window in the top right holding MONEY at (12,2) over the amount on
|
||||
-- row 3 and COIN at (12,4) over the count on row 5. Both
|
||||
-- PrintBCDNumber calls pass LEADING_ZEROES, whose bit 7 SUPPRESSES
|
||||
-- leading zeroes (home/print_bcd.asm), and neither passes LEFT_ALIGN,
|
||||
-- so both numbers read plain and right-aligned against the inner edge
|
||||
-- at column 18. The asm draws the box before the offer and redraws it
|
||||
-- after the purchase, so it stands for the whole exchange: a draw-only
|
||||
-- state under the dialogue gets that lifetime, since StateStack draws
|
||||
-- every state above the last opaque one and updates only the top
|
||||
-- (src/core/StateStack.lua), and reading save each frame is the
|
||||
-- redraw (#624).
|
||||
local coinBox = { draw = function()
|
||||
Font.drawBox(11, 0, 9, 7)
|
||||
love.graphics.setColor(0, 0, 0, 1)
|
||||
Font.draw(Strings("MONEY"), 96, 16)
|
||||
local money = ("¥%d"):format(game.save.money or 0)
|
||||
Font.draw(money, 152 - Font.width(money), 24)
|
||||
Font.draw(Strings("COIN"), 96, 32)
|
||||
local coins = ("%d"):format(game.save.coins or 0)
|
||||
Font.draw(coins, 152 - Font.width(coins), 40)
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
end }
|
||||
game.stack:push(coinBox)
|
||||
-- Every branch below finishes here. A TextBox pops itself before its
|
||||
-- onDone runs, so the coin box is top of the stack again by then and
|
||||
-- this pop takes it down, never someone else's state.
|
||||
local function finish()
|
||||
game.stack:pop()
|
||||
done()
|
||||
end
|
||||
-- YesNoChoice is called with the offer still printed, so the prompt
|
||||
-- has to ride the open text box (opts.choice) instead of being pushed
|
||||
-- after it closes, which is what made the question vanish (#624).
|
||||
game.stack:push(TextBox.new(game,
|
||||
(t._GameCornerClerk1DoYouNeedSomeGameCoinsText
|
||||
or "Do you need some\ngame coins?\f¥1000 for 50."), function()
|
||||
game.stack:push(ChoiceBox.new(game, function(yes)
|
||||
line("DoYouNeedSomeGameCoinsText",
|
||||
"Do you need some\ngame coins?\f¥1000 for 50."),
|
||||
nil, { choice = function(yes)
|
||||
if not yes then
|
||||
game.stack:push(TextBox.new(game,
|
||||
t._GameCornerClerk1PleaseComePlaySometimeText
|
||||
or "No? Please come\nplay sometime!", done))
|
||||
line("PleaseComePlaySometimeText",
|
||||
"No? Please come\nplay sometime!"), finish))
|
||||
return
|
||||
end
|
||||
-- scripts/GameCorner.asm GameCornerClerk1Text: coins need
|
||||
-- the COIN CASE and room for at least 9 coins (Has9990Coins)
|
||||
if not game.save.inventory.COIN_CASE then
|
||||
game.stack:push(TextBox.new(game,
|
||||
t._GameCornerClerk1DontHaveCoinCaseText
|
||||
or "You don't have a\nCOIN CASE!", done))
|
||||
line("DontHaveCoinCaseText",
|
||||
"You don't have a\nCOIN CASE!"), finish))
|
||||
return
|
||||
end
|
||||
if (game.save.coins or 0) >= 9990 then
|
||||
game.stack:push(TextBox.new(game,
|
||||
t._GameCornerClerk1CoinCaseIsFullText
|
||||
or "Oops! Your COIN\nCASE is full.", done))
|
||||
line("CoinCaseIsFullText",
|
||||
"Oops! Your COIN\nCASE is full."), finish))
|
||||
return
|
||||
end
|
||||
if game.save.money < 1000 then
|
||||
game.stack:push(TextBox.new(game,
|
||||
t._GameCornerClerk1CantAffordTheCoinsText
|
||||
or "You can't afford\nthe coins!", done))
|
||||
line("CantAffordTheCoinsText",
|
||||
"You can't afford\nthe coins!"), finish))
|
||||
return
|
||||
end
|
||||
game.save.money = game.save.money - 1000
|
||||
game.save.coins = math.min(9999, (game.save.coins or 0) + 50)
|
||||
-- the thanks text is the plain _GameCornerClerk1ThanksHereAre50-
|
||||
-- CoinsText; the new count belongs in the coin box the asm
|
||||
-- redraws here, not appended to the line (#624)
|
||||
game.stack:push(TextBox.new(game,
|
||||
(t._GameCornerClerk1ThanksHereAre50CoinsText
|
||||
or "Thanks! Here are\nyour 50 coins!")
|
||||
.. ("\fCOINS: %d"):format(game.save.coins), done))
|
||||
end))
|
||||
end))
|
||||
line("ThanksHereAre50CoinsText",
|
||||
"Thanks! Here are\nyour 50 coins!"), finish))
|
||||
end }))
|
||||
end,
|
||||
},
|
||||
}
|
||||
|
||||
-- Yellow's coin clerk is GAMECORNER_CLERK / TEXT_GAMECORNER_CLERK where Red
|
||||
-- and Blue spell him CLERK1, and the two text_asm bodies are identical, so
|
||||
-- one handler answers both ids. Without the alias the Yellow clerk fell
|
||||
-- through to his extracted offer line with no yes/no box behind it (#552).
|
||||
M.GAME_CORNER.talk.TEXT_GAMECORNER_CLERK =
|
||||
M.GAME_CORNER.talk.TEXT_GAMECORNER_CLERK1
|
||||
|
||||
-- Game Corner prize lists (data/events/prizes.asm, prize_mon_levels.asm).
|
||||
-- The six mon prizes differ between Red and Blue; the three TM prizes are
|
||||
-- identical, so they are shared and appended to each version's mon list.
|
||||
-- Each counter owns ONE window of three prizes, not the whole catalogue:
|
||||
-- GetPrizeMenuId (engine/events/prize_menu.asm) subtracts
|
||||
-- TEXT_GAMECORNERPRIZEROOM_PRIZE_VENDOR_1 from hTextID and indexes
|
||||
-- PrizeDifferentMenuPtrs with the result, so vendor 1 sells
|
||||
-- PrizeMenuMon1Entries, vendor 2 PrizeMenuMon2Entries and vendor 3
|
||||
-- PrizeMenuTMsEntries (#623). The mon windows and their levels differ per
|
||||
-- version; the TM window is identical in all three, so it is shared.
|
||||
local PRIZE_TMS = {
|
||||
{ kind = "item", item = "TM_DRAGON_RAGE", cost = 3300 },
|
||||
{ kind = "item", item = "TM_HYPER_BEAM", cost = 5500 },
|
||||
{ kind = "item", item = "TM_SUBSTITUTE", cost = 7700 },
|
||||
}
|
||||
local RED_PRIZES = {
|
||||
{ kind = "mon", species = "ABRA", level = 9, cost = 180 },
|
||||
{ kind = "mon", species = "CLEFAIRY", level = 8, cost = 500 },
|
||||
{ kind = "mon", species = "NIDORINA", level = 17, cost = 1200 },
|
||||
{ kind = "mon", species = "DRATINI", level = 18, cost = 2800 },
|
||||
{ kind = "mon", species = "SCYTHER", level = 25, cost = 5500 },
|
||||
{ kind = "mon", species = "PORYGON", level = 26, cost = 9999 },
|
||||
PRIZE_TMS[1], PRIZE_TMS[2], PRIZE_TMS[3],
|
||||
local RED_PRIZE_WINDOWS = {
|
||||
{
|
||||
{ kind = "mon", species = "ABRA", level = 9, cost = 180 },
|
||||
{ kind = "mon", species = "CLEFAIRY", level = 8, cost = 500 },
|
||||
{ kind = "mon", species = "NIDORINA", level = 17, cost = 1200 },
|
||||
},
|
||||
{
|
||||
{ kind = "mon", species = "DRATINI", level = 18, cost = 2800 },
|
||||
{ kind = "mon", species = "SCYTHER", level = 25, cost = 5500 },
|
||||
{ kind = "mon", species = "PORYGON", level = 26, cost = 9999 },
|
||||
},
|
||||
PRIZE_TMS,
|
||||
}
|
||||
local BLUE_PRIZES = {
|
||||
{ kind = "mon", species = "ABRA", level = 6, cost = 120 },
|
||||
{ kind = "mon", species = "CLEFAIRY", level = 12, cost = 750 },
|
||||
{ kind = "mon", species = "NIDORINO", level = 17, cost = 1200 },
|
||||
{ kind = "mon", species = "PINSIR", level = 20, cost = 2500 },
|
||||
{ kind = "mon", species = "DRATINI", level = 24, cost = 4600 },
|
||||
{ kind = "mon", species = "PORYGON", level = 18, cost = 6500 },
|
||||
PRIZE_TMS[1], PRIZE_TMS[2], PRIZE_TMS[3],
|
||||
local BLUE_PRIZE_WINDOWS = {
|
||||
{
|
||||
{ kind = "mon", species = "ABRA", level = 6, cost = 120 },
|
||||
{ kind = "mon", species = "CLEFAIRY", level = 12, cost = 750 },
|
||||
{ kind = "mon", species = "NIDORINO", level = 17, cost = 1200 },
|
||||
},
|
||||
{
|
||||
{ kind = "mon", species = "PINSIR", level = 20, cost = 2500 },
|
||||
{ kind = "mon", species = "DRATINI", level = 24, cost = 4600 },
|
||||
{ kind = "mon", species = "PORYGON", level = 18, cost = 6500 },
|
||||
},
|
||||
PRIZE_TMS,
|
||||
}
|
||||
-- Yellow keeps the three windows but restocks both mon counters
|
||||
-- (pokeyellow/data/events/prizes.asm, prize_mon_levels.asm)
|
||||
local YELLOW_PRIZE_WINDOWS = {
|
||||
{
|
||||
{ kind = "mon", species = "ABRA", level = 15, cost = 230 },
|
||||
{ kind = "mon", species = "VULPIX", level = 18, cost = 1000 },
|
||||
{ kind = "mon", species = "WIGGLYTUFF", level = 22, cost = 2680 },
|
||||
},
|
||||
{
|
||||
{ kind = "mon", species = "SCYTHER", level = 30, cost = 6500 },
|
||||
{ kind = "mon", species = "PINSIR", level = 30, cost = 6500 },
|
||||
{ kind = "mon", species = "PORYGON", level = 26, cost = 9999 },
|
||||
},
|
||||
PRIZE_TMS,
|
||||
}
|
||||
|
||||
local function activePrizes()
|
||||
return require("src.core.GameVersion").isBlue() and BLUE_PRIZES or RED_PRIZES
|
||||
local function prizeWindow(n)
|
||||
local GameVersion = require("src.core.GameVersion")
|
||||
local windows = RED_PRIZE_WINDOWS
|
||||
if GameVersion.isBlue() then
|
||||
windows = BLUE_PRIZE_WINDOWS
|
||||
elseif GameVersion.isYellow() then
|
||||
windows = YELLOW_PRIZE_WINDOWS
|
||||
end
|
||||
return windows[n]
|
||||
end
|
||||
|
||||
-- Prize counters (engine/menus/prize_menu.asm CeladonPrizeMenu; the prize
|
||||
-- Prize counters (engine/events/prize_menu.asm CeladonPrizeMenu; the prize
|
||||
-- list itself is data/events/prizes.asm, prize_mon_levels.asm). Gen1 gates
|
||||
-- the prize window on the COIN CASE: it does IsItemInBag COIN_CASE first, and
|
||||
-- with no case prints RequireCoinCaseText and returns without ever opening a
|
||||
-- window; only with the case does it print ExchangeCoinsForPrizesText and then
|
||||
-- show the prizes. #194: the port used to open the window unconditionally and
|
||||
-- skip both text boxes.
|
||||
local function prizeCounter(game, ow, npc, done)
|
||||
local ListMenu = require("src.ui.ListMenu")
|
||||
local Commands = require("src.script.Commands")
|
||||
local TextBox = require("src.render.TextBox")
|
||||
local t = game.data.text
|
||||
-- IsItemInBag COIN_CASE: without the case, deny and open no window
|
||||
-- (COIN_CASE is a numeric count in save.inventory, nil when absent).
|
||||
if not game.save.inventory.COIN_CASE then
|
||||
-- skip both text boxes. wMaxMenuItem is 3, i.e. this window's three prizes
|
||||
-- plus the NO THANKS row, and HandlePrizeChoice confirms the pick with
|
||||
-- SoYouWantPrizeText + YesNoChoice before any coins move; every branch then
|
||||
-- rets out of CeladonPrizeMenu, so one transaction ends the conversation and
|
||||
-- buying again means talking to the counter again (#623).
|
||||
local function prizeCounter(window)
|
||||
return function(game, ow, npc, done)
|
||||
local ListMenu = require("src.ui.ListMenu")
|
||||
local Commands = require("src.script.Commands")
|
||||
local TextBox = require("src.render.TextBox")
|
||||
local t = game.data.text
|
||||
-- IsItemInBag COIN_CASE: without the case, deny and open no window
|
||||
-- (COIN_CASE is a numeric count in save.inventory, nil when absent).
|
||||
if not game.save.inventory.COIN_CASE then
|
||||
game.stack:push(TextBox.new(game,
|
||||
t._RequireCoinCaseText or "A COIN CASE is\nrequired!", done))
|
||||
return
|
||||
end
|
||||
-- ExchangeCoinsForPrizesText plays before the prize window opens.
|
||||
game.stack:push(TextBox.new(game,
|
||||
t._RequireCoinCaseText or "A COIN CASE is\nrequired!", done))
|
||||
return
|
||||
end
|
||||
-- ExchangeCoinsForPrizesText plays before the prize window opens.
|
||||
game.stack:push(TextBox.new(game,
|
||||
t._ExchangeCoinsForPrizesText or "We exchange your\ncoins for prizes.",
|
||||
function()
|
||||
local items = {}
|
||||
for _, p in ipairs(activePrizes()) do
|
||||
local label
|
||||
if p.kind == "mon" then
|
||||
label = ("%s L%d"):format(game.data.pokemon[p.species].name, p.level)
|
||||
else
|
||||
label = game.data.items[p.item].name
|
||||
t._ExchangeCoinsForPrizesText or "We exchange your\ncoins for prizes.",
|
||||
function()
|
||||
local items = {}
|
||||
for _, p in ipairs(prizeWindow(window)) do
|
||||
local label
|
||||
if p.kind == "mon" then
|
||||
label = ("%s L%d"):format(game.data.pokemon[p.species].name, p.level)
|
||||
else
|
||||
label = game.data.items[p.item].name
|
||||
end
|
||||
table.insert(items,
|
||||
{ label = label, right = tostring(p.cost), value = p })
|
||||
end
|
||||
table.insert(items,
|
||||
{ label = label, right = tostring(p.cost), value = p })
|
||||
end
|
||||
local list
|
||||
list = ListMenu.new(game, "PRIZES (COINS)", items, {
|
||||
footer = ("COINS %d"):format(game.save.coins or 0),
|
||||
onChoose = function(item)
|
||||
local p = item.value
|
||||
-- NoThanksText (data/events/prizes.asm) sits under the three prizes
|
||||
table.insert(items, { label = "NO THANKS" })
|
||||
local list
|
||||
-- close the window first: every ending in HandlePrizeChoice leaves
|
||||
-- the menu for good, and the closing line belongs over the map
|
||||
local function finish(msg)
|
||||
list:close()
|
||||
game.stack:push(TextBox.new(game, msg, done))
|
||||
end
|
||||
local function buy(p)
|
||||
if (game.save.coins or 0) < p.cost then
|
||||
list.footer = "Not enough coins!"
|
||||
finish(t._SorryNeedMoreCoinsText or "Sorry, you need\nmore coins.")
|
||||
return
|
||||
end
|
||||
-- HasEnoughCoins passed, so hand the prize over first and only
|
||||
-- subtract once it landed: the asm rets before .subtractCoins when
|
||||
-- the bag is full, or when both the party and every box are full
|
||||
local roomless = t._OopsYouDontHaveEnoughRoomText
|
||||
or "Oops! You don't\nhave enough room."
|
||||
if p.kind == "mon" then
|
||||
-- no runner here, so give_pokemon reports through ctx.lastCheck
|
||||
-- and skips the AskName prompt (Commands.give_pokemon)
|
||||
local ctx = { save = game.save, game = game }
|
||||
Commands.give_pokemon(ctx, p.species, p.level)
|
||||
if not ctx.lastCheck then
|
||||
finish(roomless)
|
||||
return
|
||||
end
|
||||
elseif not require("src.inventory.Bag").add(
|
||||
game.save, p.item, 1, game.data) then
|
||||
finish(roomless)
|
||||
return
|
||||
end
|
||||
game.save.coins = game.save.coins - p.cost
|
||||
if p.kind == "mon" then
|
||||
Commands.give_pokemon({ save = game.save, game = game },
|
||||
p.species, p.level)
|
||||
else
|
||||
game.save.inventory[p.item] = (game.save.inventory[p.item] or 0) + 1
|
||||
end
|
||||
list.footer = ("Got it! COINS %d"):format(game.save.coins)
|
||||
end,
|
||||
onCancel = done,
|
||||
})
|
||||
game.stack:push(list)
|
||||
end))
|
||||
-- no thank-you line: HereYouGoText is unreferenced in the asm,
|
||||
-- which just redraws the coin box (PrintPrizePrice) and returns
|
||||
list:close()
|
||||
done()
|
||||
end
|
||||
list = ListMenu.new(game, "PRIZES (COINS)", items, {
|
||||
footer = ("COINS %d"):format(game.save.coins or 0),
|
||||
onChoose = function(item)
|
||||
local p = item.value
|
||||
if not p then -- NO THANKS is the B exit (cp 3 -> .noChoice)
|
||||
list:close()
|
||||
done()
|
||||
return
|
||||
end
|
||||
local name = (p.kind == "mon")
|
||||
and game.data.pokemon[p.species].name
|
||||
or game.data.items[p.item].name
|
||||
-- SoYouWantPrizeText names the prize out of wNameBuffer, which
|
||||
-- is not one of TextBox's RAM tokens, so fill it in here
|
||||
local ask = (t._SoYouWantPrizeText
|
||||
or "So, you want\n{RAM:wNameBuffer}?")
|
||||
:gsub("{RAM:wNameBuffer}", name)
|
||||
game.stack:push(TextBox.new(game, ask, nil, {
|
||||
choice = function(yes)
|
||||
if not yes then
|
||||
finish(t._OhFineThenText or "Oh, fine then.")
|
||||
return
|
||||
end
|
||||
buy(p)
|
||||
end,
|
||||
}))
|
||||
end,
|
||||
onCancel = done,
|
||||
})
|
||||
game.stack:push(list)
|
||||
end))
|
||||
end
|
||||
end
|
||||
|
||||
M.GAME_CORNER_PRIZE_ROOM = {
|
||||
talk = { -- the three prize counters are bg events
|
||||
TEXT_GAMECORNERPRIZEROOM_PRIZE_VENDOR_1 = prizeCounter,
|
||||
TEXT_GAMECORNERPRIZEROOM_PRIZE_VENDOR_2 = prizeCounter,
|
||||
TEXT_GAMECORNERPRIZEROOM_PRIZE_VENDOR_3 = prizeCounter,
|
||||
talk = { -- the three prize counters are bg events, one window each
|
||||
TEXT_GAMECORNERPRIZEROOM_PRIZE_VENDOR_1 = prizeCounter(1),
|
||||
TEXT_GAMECORNERPRIZEROOM_PRIZE_VENDOR_2 = prizeCounter(2),
|
||||
TEXT_GAMECORNERPRIZEROOM_PRIZE_VENDOR_3 = prizeCounter(3),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -695,6 +893,16 @@ local DOCK_SHIP_BLOCKS = {
|
||||
{ bx = 7, by = 2, water = 13 }, { bx = 8, by = 2, water = 13 },
|
||||
}
|
||||
|
||||
-- her four hull columns bow-to-stern (upper-half / lower-half block ids)
|
||||
-- and the open-water ids of the rows she sits in
|
||||
local DOCK_SHIP_COLUMNS = {
|
||||
{ bx = 5, top = 4, bottom = 8 },
|
||||
{ bx = 6, top = 5, bottom = 9 },
|
||||
{ bx = 7, top = 6, bottom = 10 },
|
||||
{ bx = 8, top = 7, bottom = 11 },
|
||||
}
|
||||
local DOCK_WATER_TOP, DOCK_WATER_BOTTOM = 1, 13
|
||||
|
||||
M.VERMILION_DOCK = {
|
||||
onEnter = function(game, ow)
|
||||
local Flags = require("src.script.Flags")
|
||||
@@ -729,17 +937,34 @@ M.VERMILION_DOCK = {
|
||||
ow:startDustAnim(cx, 1, function() puff(n - 1, cx + 2) end)
|
||||
end
|
||||
puff(3, 15)
|
||||
-- scripts/VermilionDock.asm:182-203
|
||||
local rows = {}
|
||||
rows[#rows + 1] = { "wait", 100 }
|
||||
rows[#rows + 1] = { "play_sound", "SS_Anne_Horn" }
|
||||
for _, b in ipairs(DOCK_SHIP_BLOCKS) do
|
||||
rows[#rows + 1] = { "replace_block", b.bx, b.by, b.water }
|
||||
local function setBlock(bx, by, block)
|
||||
if bx < 1 or bx > 8 then return end
|
||||
rows[#rows + 1] = { "replace_block", bx, by, block }
|
||||
end
|
||||
rows[#rows + 1] = { "wait", 30 }
|
||||
rows[#rows + 1] = { "wait", 120 }
|
||||
rows[#rows + 1] = { "play_sound", "SS_Anne_Horn" }
|
||||
-- .shift_columns_up slides her tile columns west behind a mid-frame
|
||||
-- rSCX split; with no split scroll here she sails one block per beat
|
||||
-- and the water closes in astern (#360)
|
||||
for step = 1, 8 do
|
||||
for _, col in ipairs(DOCK_SHIP_COLUMNS) do
|
||||
setBlock(col.bx - step, 1, col.top)
|
||||
setBlock(col.bx - step, 2, col.bottom)
|
||||
end
|
||||
setBlock(9 - step, 1, DOCK_WATER_TOP)
|
||||
setBlock(9 - step, 2, DOCK_WATER_BOTTOM)
|
||||
rows[#rows + 1] = { "wait", 20 }
|
||||
end
|
||||
-- the second horn as she clears the dock, then EraseSSAnne's 120
|
||||
-- frames before the walk out
|
||||
rows[#rows + 1] = { "play_sound", "SS_Anne_Horn" }
|
||||
rows[#rows + 1] = { "wait", 120 }
|
||||
rows[#rows + 1] = { "move_player", "up", 2 }
|
||||
-- keep Music_Surfing across the city warp (pokered stays on it
|
||||
-- through the exit-ship walk)
|
||||
rows[#rows + 1] = { "play_music", "Music_Surfing", { keep = true } }
|
||||
-- no keepMusic on this warp: Music_Surfing belongs to the dock's
|
||||
-- cutscene, and VERMILION_CITY's own theme has to take over as the
|
||||
-- player crosses in (EnterMap's PlayDefaultMusic)
|
||||
rows[#rows + 1] = { "warp", "VERMILION_CITY", 18, 31, "up" }
|
||||
rows[#rows + 1] = { "move_player", "up", 2 }
|
||||
ow:queueScript(rows)
|
||||
|
||||
@@ -8,21 +8,35 @@ local M = {}
|
||||
|
||||
local function text(game) return game.data.text end
|
||||
|
||||
local function push(game, s, done)
|
||||
local function push(game, s, done, opts)
|
||||
local TextBox = require("src.render.TextBox")
|
||||
game.stack:push(TextBox.new(game, s, done))
|
||||
game.stack:push(TextBox.new(game, s, done, opts))
|
||||
end
|
||||
|
||||
-- The question stays on screen under the YES/NO menu. The dojo prize
|
||||
-- balls are the clearest case: FightingDojoHitmonleePokeBallText
|
||||
-- (scripts/FightingDojo.asm) is `call PrintText` on a text_end string --
|
||||
-- no prompt, so no WaitForTextScrollButtonPress -- immediately followed
|
||||
-- by `call YesNoChoice`, and InitYesNoTextBoxParameters
|
||||
-- (engine/menus/text_box.asm) puts the menu above the dialogue box
|
||||
-- rather than replacing it. Ride TextBox's opts.choice, the same as
|
||||
-- Commands.ask, instead of popping the box with an A press and leaving a
|
||||
-- bare ChoiceBox over the overworld (#854).
|
||||
local function ask(game, s, cb)
|
||||
local ChoiceBox = require("src.ui.ChoiceBox")
|
||||
push(game, s, function() game.stack:push(ChoiceBox.new(game, cb)) end)
|
||||
local TextBox = require("src.render.TextBox")
|
||||
game.stack:push(TextBox.new(game, s, nil, { choice = cb }))
|
||||
end
|
||||
|
||||
-- fill the extracted text placeholders ({NUM:...}, {RAM:...}, {PLAYER})
|
||||
-- fill text placeholders; key on the hram/wram symbol first, since one
|
||||
-- string can carry two different NUM slots (#1006)
|
||||
local function fill(s, subs)
|
||||
s = s:gsub("{PLAYER}", subs.player or "")
|
||||
s = s:gsub("{NUM:[^}]*}", function() return tostring(subs.num or "") end)
|
||||
s = s:gsub("{RAM:[^}]*}", function() return subs.ram or "" end)
|
||||
s = s:gsub("{NUM:([%w_]*)[^}]*}", function(name)
|
||||
return tostring(subs[name] or subs.num or "")
|
||||
end)
|
||||
s = s:gsub("{RAM:([%w_]*)[^}]*}", function(name)
|
||||
return subs[name] or subs.ram or ""
|
||||
end)
|
||||
return s
|
||||
end
|
||||
|
||||
@@ -37,16 +51,15 @@ local function countOwned(save)
|
||||
return n
|
||||
end
|
||||
|
||||
local function oaksAide(threshold, itemId)
|
||||
local function oaksAide(threshold, itemId, repeatText)
|
||||
return function(game, ow, npc, done)
|
||||
local t = text(game)
|
||||
local flags = game.save.flags
|
||||
local itemName = game.data.items[itemId].name
|
||||
local flag = "EVENT_GOT_" .. itemId
|
||||
if flags[flag] then
|
||||
push(game, fill(t._OaksAideComeBackText or
|
||||
"I already gave\nyou the {RAM:}!",
|
||||
{ num = threshold, ram = itemName }), done)
|
||||
push(game, t[repeatText] or
|
||||
fill("I already gave\nyou the {RAM:}!", { ram = itemName }), done)
|
||||
return
|
||||
end
|
||||
ask(game, fill(t._OaksAideHiText or
|
||||
@@ -74,22 +87,28 @@ local function oaksAide(threshold, itemId)
|
||||
{ ram = itemName, player = game.save.player.name }), done)
|
||||
end)
|
||||
else
|
||||
-- .notEnoughOwnedMons prints owned then requirement, two counts
|
||||
push(game, fill(t._OaksAideUhOhText or
|
||||
"You have only\ncaught {NUM:}!",
|
||||
{ num = owned, ram = itemName }), done)
|
||||
{ num = owned, ram = itemName,
|
||||
hOaksAideNumMonsOwned = owned,
|
||||
hOaksAideRequirement = threshold }), done)
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
M.ROUTE_2_GATE = {
|
||||
talk = { TEXT_ROUTE2GATE_OAKS_AIDE = oaksAide(10, "HM_FLASH") },
|
||||
talk = { TEXT_ROUTE2GATE_OAKS_AIDE = oaksAide(10, "HM_FLASH",
|
||||
"_Route2GateOaksAideFlashExplanationText") },
|
||||
}
|
||||
M.ROUTE_11_GATE_2F = {
|
||||
talk = { TEXT_ROUTE11GATE2F_OAKS_AIDE = oaksAide(30, "ITEMFINDER") },
|
||||
talk = { TEXT_ROUTE11GATE2F_OAKS_AIDE = oaksAide(30, "ITEMFINDER",
|
||||
"_Route11Gate2FOaksAideItemfinderDescriptionText") },
|
||||
}
|
||||
M.ROUTE_15_GATE_2F = {
|
||||
talk = { TEXT_ROUTE15GATE2F_OAKS_AIDE = oaksAide(50, "EXP_ALL") },
|
||||
talk = { TEXT_ROUTE15GATE2F_OAKS_AIDE = oaksAide(50, "EXP_ALL",
|
||||
"_Route15Gate2FOaksAideExpAllText") },
|
||||
}
|
||||
|
||||
-- -------------------------------------------------------------------
|
||||
@@ -99,33 +118,36 @@ M.ROUTE_15_GATE_2F = {
|
||||
|
||||
M.MT_MOON_POKECENTER = {
|
||||
talk = {
|
||||
TEXT_MTMOONPOKECENTER_MAGIKARP_SALESMAN = function(game, ow, npc, done)
|
||||
local t = text(game)
|
||||
if game.save.flags.EVENT_BOUGHT_MAGIKARP then
|
||||
push(game, t._MtMoonPokecenterMagikarpSalesmanNoRefundsText
|
||||
or "Well, I don't\ngive refunds!", done)
|
||||
return
|
||||
end
|
||||
ask(game, t._MtMoonPokecenterMagikarpSalesmanOfferText
|
||||
or "MAGIKARP! A\nsteal at ¥500!\nWant one?", function(yes)
|
||||
if not yes then
|
||||
push(game, t._MtMoonPokecenterMagikarpSalesmanNoText
|
||||
or "No? I'm only\nselling today!", done)
|
||||
return
|
||||
end
|
||||
if game.save.money < 500 then
|
||||
push(game, t._MtMoonPokecenterMagikarpSalesmanNoMoneyText
|
||||
or "You'll need more\nmoney than that!", done)
|
||||
return
|
||||
end
|
||||
game.save.money = game.save.money - 500
|
||||
game.save.flags.EVENT_BOUGHT_MAGIKARP = true
|
||||
local Commands = require("src.script.Commands")
|
||||
Commands.give_pokemon({ save = game.save, game = game, overworld = ow },
|
||||
"MAGIKARP", 5)
|
||||
push(game, ("%s got a\nMAGIKARP!"):format(game.save.player.name), done)
|
||||
end)
|
||||
end,
|
||||
-- command rows, not a Lua handler: give_pokemon needs a runner to AskName (#1407)
|
||||
TEXT_MTMOONPOKECENTER_MAGIKARP_SALESMAN = {
|
||||
{ "check_flag", "EVENT_BOUGHT_MAGIKARP" },
|
||||
{ "jump_if_true", "no_refunds" },
|
||||
-- MONEY_BOX goes up between the offer and YesNoChoice -- MtMoonPokecenter.asm:31
|
||||
{ "text_opts", { money = true } },
|
||||
{ "ask", "_MtMoonPokecenterMagikarpSalesmanIGotADealText" },
|
||||
{ "jump_if_false", "declined" },
|
||||
{ "check_money", 500 },
|
||||
{ "jump_if_false", "no_money" },
|
||||
{ "give_pokemon", "MAGIKARP", 5 },
|
||||
-- MtMoonPokecenter.asm:49 `jr nc, .done`: a refused gift is never charged
|
||||
{ "jump_if_false", "box_full" },
|
||||
{ "take_money", 500 },
|
||||
{ "set_flag", "EVENT_BOUGHT_MAGIKARP" },
|
||||
{ "text_sound", "Get_Item1" },
|
||||
{ "show_text", "_GotMonText", { RAM = "MAGIKARP" } },
|
||||
{ "jump", "end" },
|
||||
{ "label", "box_full" },
|
||||
{ "show_text", "_BoxIsFullText" },
|
||||
{ "jump", "end" },
|
||||
{ "label", "declined" },
|
||||
{ "show_text", "_MtMoonPokecenterMagikarpSalesmanNoText" },
|
||||
{ "jump", "end" },
|
||||
{ "label", "no_money" },
|
||||
{ "show_text", "_MtMoonPokecenterMagikarpSalesmanNoMoneyText" },
|
||||
{ "jump", "end" },
|
||||
{ "label", "no_refunds" },
|
||||
{ "show_text", "_MtMoonPokecenterMagikarpSalesmanNoRefundsText" },
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -153,22 +175,51 @@ local function dojoBall(species, ownBall, otherBall, askKey)
|
||||
push(game, "You'll have to\nbeat the master\nfirst!", done)
|
||||
return
|
||||
end
|
||||
ask(game, t[askKey] or ("You want\n" .. species .. "?"), function(yes)
|
||||
if not yes then done() return end
|
||||
flags["EVENT_GOT_" .. species] = true
|
||||
flags.EVENT_DEFEATED_FIGHTING_DOJO = true
|
||||
local Commands = require("src.script.Commands")
|
||||
local ctx = { save = game.save, game = game, overworld = ow }
|
||||
Commands.give_pokemon(ctx, species, 30)
|
||||
-- Hide ONLY the chosen ball; the other stays (FightingDojo.asm hides
|
||||
-- just the picked object's index) and routes to the greedy line above
|
||||
-- when talked to (#197).
|
||||
Commands.hide_object(ctx, "FIGHTING_DOJO", ownBall)
|
||||
push(game, ("%s got\n%s!"):format(game.save.player.name, species), done)
|
||||
end)
|
||||
-- Examining a ball shows that species' POKéDEX entry first
|
||||
-- (DisplayPokedex in FightingDojo.asm, which also marks it seen),
|
||||
-- then the yes/no take-it prompt (#853).
|
||||
local Commands = require("src.script.Commands")
|
||||
local ctx = { save = game.save, game = game, overworld = ow }
|
||||
Commands.mark_seen(ctx, species)
|
||||
local DexEntryMenu = require("src.ui.DexEntryMenu")
|
||||
game.stack:push(DexEntryMenu.new(game, species, function()
|
||||
ask(game, t[askKey] or ("You want\n" .. species .. "?"), function(yes)
|
||||
if not yes then done() return end
|
||||
flags["EVENT_GOT_" .. species] = true
|
||||
flags.EVENT_DEFEATED_FIGHTING_DOJO = true
|
||||
Commands.give_pokemon(ctx, species, 30)
|
||||
-- Hide ONLY the chosen ball; the other stays (FightingDojo.asm hides
|
||||
-- just the picked object's index) and routes to the greedy line above
|
||||
-- when talked to (#197).
|
||||
Commands.hide_object(ctx, "FIGHTING_DOJO", ownBall)
|
||||
push(game, ("%s got\n%s!"):format(game.save.player.name, species), done)
|
||||
end)
|
||||
end))
|
||||
end
|
||||
end
|
||||
|
||||
-- FightingDojoDefaultScript does not use the Master's facing direction for
|
||||
-- this battle. It checks exactly the cell left of him, turns both sprites
|
||||
-- toward one another, and then displays his trainer text. Keeping this out
|
||||
-- of trainer sight also lets him keep his original downward-facing pose.
|
||||
local function dojoMasterGate(game, ow, x, y)
|
||||
if x ~= 4 or y ~= 3 or game.save.flags.EVENT_BEAT_KARATE_MASTER then
|
||||
return false
|
||||
end
|
||||
local master
|
||||
for _, npc in ipairs(ow.npcs) do
|
||||
if npc.def and npc.def.name == "FIGHTINGDOJO_KARATE_MASTER" then
|
||||
master = npc
|
||||
break
|
||||
end
|
||||
end
|
||||
if not master or ow:trainerDefeated(master) then return false end
|
||||
ow.player.facing = "right"
|
||||
master:facePlayer(ow.player)
|
||||
ow:engageTrainer(master)
|
||||
return true
|
||||
end
|
||||
|
||||
M.FIGHTING_DOJO = {
|
||||
talk = {
|
||||
TEXT_FIGHTINGDOJO_HITMONLEE_POKE_BALL =
|
||||
@@ -195,6 +246,7 @@ M.FIGHTING_DOJO = {
|
||||
end
|
||||
return false
|
||||
end,
|
||||
onStep = dojoMasterGate,
|
||||
}
|
||||
|
||||
-- -------------------------------------------------------------------
|
||||
@@ -204,27 +256,32 @@ M.FIGHTING_DOJO = {
|
||||
|
||||
M.SILPH_CO_7F = {
|
||||
talk = {
|
||||
TEXT_SILPHCO7F_SILPH_WORKER_M1 = function(game, ow, npc, done)
|
||||
local t = text(game)
|
||||
if game.save.flags.EVENT_GOT_LAPRAS then
|
||||
push(game, t._SilphCo7FSilphWorkerM1LaprasDescriptionText
|
||||
or "How is LAPRAS\ndoing?", done)
|
||||
return
|
||||
end
|
||||
push(game, t._SilphCo7FSilphWorkerM1ThankYouText
|
||||
or "Thank you for\nsaving us!\fI want you to\nhave this LAPRAS!",
|
||||
function()
|
||||
game.save.flags.EVENT_GOT_LAPRAS = true
|
||||
local Commands = require("src.script.Commands")
|
||||
Commands.give_pokemon({ save = game.save, game = game, overworld = ow },
|
||||
"LAPRAS", 15)
|
||||
push(game, ("%s got\nLAPRAS!"):format(game.save.player.name),
|
||||
function()
|
||||
push(game, t._SilphCo7FSilphWorkerM1LaprasDescriptionText
|
||||
or "It's a good\nswimmer!", done)
|
||||
end)
|
||||
end)
|
||||
end,
|
||||
-- command rows, not a Lua handler: give_pokemon needs a runner to AskName (#1049)
|
||||
TEXT_SILPHCO7F_SILPH_WORKER_M1 = {
|
||||
{ "face_player" },
|
||||
{ "check_flag", "EVENT_GOT_LAPRAS" },
|
||||
{ "jump_if_true", "has_lapras" },
|
||||
{ "show_text", "_SilphCo7FSilphWorkerM1HaveThisPokemonText" },
|
||||
{ "give_pokemon", "LAPRAS", 15 },
|
||||
{ "jump_if_false", "box_full" },
|
||||
-- flag ahead of the jingle, like the Celadon EEVEE (#426)
|
||||
{ "set_flag", "EVENT_GOT_LAPRAS" },
|
||||
{ "text_sound", "Get_Item1" },
|
||||
{ "show_text", "_GotMonText", { RAM = "LAPRAS" } },
|
||||
{ "show_text", "_SilphCo7FSilphWorkerM1LaprasDescriptionText" },
|
||||
{ "jump", "end" },
|
||||
{ "label", "box_full" },
|
||||
{ "show_text", "_BoxIsFullText" },
|
||||
{ "jump", "end" },
|
||||
-- SilphCo7F.asm .saved_silph gates the thanks on Giovanni
|
||||
{ "label", "has_lapras" },
|
||||
{ "check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI" },
|
||||
{ "jump_if_true", "saved" },
|
||||
{ "show_text", "_SilphCo7FSilphWorkerM1IsOurPresidentOkText" },
|
||||
{ "jump", "end" },
|
||||
{ "label", "saved" },
|
||||
{ "show_text", "_SilphCo7FSilphWorkerM1SavedText" },
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -253,12 +310,11 @@ M.COPYCATS_HOUSE_2F = {
|
||||
return
|
||||
end
|
||||
game.stringBuffer = game.data.items.TM_MIMIC.name
|
||||
require("src.core.Sound").play(game.data, "Get_Item1")
|
||||
Bag.remove(game.save, "POKE_DOLL", 1)
|
||||
game.save.flags.EVENT_GOT_TM31 = true
|
||||
push(game, t._CopycatsHouse2FCopycatReceivedTM31Text, function()
|
||||
push(game, t._CopycatsHouse2FCopycatTM31Explanation1Text, done)
|
||||
end)
|
||||
end, require("src.render.TextBox").soundOpts(game, "Get_Item1"))
|
||||
end)
|
||||
end)
|
||||
end,
|
||||
@@ -424,7 +480,6 @@ M.CELADON_MART_ROOF = {
|
||||
return
|
||||
end
|
||||
game.save.flags[g.flag] = true
|
||||
require("src.core.Sound").play(game.data, "Get_Item1")
|
||||
local subs = { player = game.save.player.name,
|
||||
ram = game.data.items[g.tm].name }
|
||||
local explain = fill(t[g.explain] or "", subs)
|
||||
@@ -436,7 +491,7 @@ M.CELADON_MART_ROOF = {
|
||||
else
|
||||
done()
|
||||
end
|
||||
end)
|
||||
end, require("src.render.TextBox").soundOpts(game, "Get_Item1"))
|
||||
end)
|
||||
end,
|
||||
onCancel = done,
|
||||
@@ -458,25 +513,28 @@ M.ROUTE_24 = {
|
||||
local flags = game.save.flags
|
||||
local function battleOrDone()
|
||||
if ow:trainerDefeated(npc) then
|
||||
push(game, "I hate this!\nMy dreams of\nTEAM ROCKET...", done)
|
||||
push(game, text(game)._Route24CooltrainerM1YouCouldBecomeATopLeaderText,
|
||||
done)
|
||||
else
|
||||
ow:engageTrainer(npc, done)
|
||||
end
|
||||
end
|
||||
if not flags.EVENT_GOT_NUGGET then
|
||||
push(game, "Congratulations!\nYou beat our 5\ncontest trainers!\f"
|
||||
.. "You just earned a\nfabulous prize!", function()
|
||||
local t = text(game)
|
||||
push(game, t._Route24CooltrainerM1YouBeatOurContestText .. "\f"
|
||||
.. t._Route24CooltrainerM1YouJustEarnedAPrizeText, function()
|
||||
if not require("src.inventory.Bag").add(game.save, "NUGGET", 1,
|
||||
game.data) then
|
||||
push(game, t._Route24CooltrainerM1NoRoomText, done)
|
||||
return
|
||||
end
|
||||
flags.EVENT_GOT_NUGGET = true
|
||||
require("src.inventory.Bag").add(game.save, "NUGGET", 1)
|
||||
push(game, ("%s received\na NUGGET!"):format(game.save.player.name),
|
||||
function()
|
||||
ask(game, "By the way, would\nyou like to join\nTEAM ROCKET?",
|
||||
function()
|
||||
push(game, "Arrgh! You are\nnot convinced?\fThen I'll show\n"
|
||||
.. "you my power!", battleOrDone)
|
||||
end)
|
||||
end)
|
||||
end)
|
||||
game.stringBuffer = game.data.items.NUGGET.name
|
||||
push(game, t._Route24CooltrainerM1ReceivedNuggetText, function()
|
||||
push(game, t._Route24CooltrainerM1JoinTeamRocketText,
|
||||
battleOrDone)
|
||||
end, require("src.render.TextBox").soundOpts(game, "Get_Item1"))
|
||||
end, require("src.render.TextBox").soundOpts(game, "Get_Item1"))
|
||||
return
|
||||
end
|
||||
battleOrDone()
|
||||
|
||||
@@ -4,9 +4,9 @@ local M = {}
|
||||
|
||||
local function text(game) return game.data.text end
|
||||
|
||||
local function push(game, s, done)
|
||||
local function push(game, s, done, opts)
|
||||
local TextBox = require("src.render.TextBox")
|
||||
game.stack:push(TextBox.new(game, s, done))
|
||||
game.stack:push(TextBox.new(game, s, done, opts))
|
||||
end
|
||||
|
||||
-- fill the extracted text placeholders ({RAM:...}, {PLAYER})
|
||||
@@ -25,8 +25,8 @@ local function gift(opts)
|
||||
local t = text(game)
|
||||
local itemName = game.data.items[opts.item].name
|
||||
local subs = { ram = itemName, player = game.save.player.name }
|
||||
local function say(label, fallback, cb)
|
||||
push(game, fill(t[label] or fallback, subs), cb)
|
||||
local function say(label, fallback, cb, sopts)
|
||||
push(game, fill(t[label] or fallback, subs), cb, sopts)
|
||||
end
|
||||
if game.save.flags[opts.flag] then
|
||||
say(opts.already or opts.explain, "It's a useful\nitem, isn't it?", done)
|
||||
@@ -39,17 +39,18 @@ local function gift(opts)
|
||||
end
|
||||
game.save.flags[opts.flag] = true
|
||||
local idef = game.data.items[opts.item]
|
||||
require("src.core.Sound").play(game.data,
|
||||
(idef and idef.keyItem) and "Get_Key_Item" or "Get_Item1")
|
||||
-- the received texts carry sound_get_item_1 / sound_get_key_item, so
|
||||
-- the jingle only fires once that box has typed out
|
||||
say(opts.received, "{PLAYER} received\n{RAM:}!", function()
|
||||
if opts.explain then
|
||||
say(opts.explain, "", done)
|
||||
else
|
||||
done()
|
||||
end
|
||||
end)
|
||||
end, require("src.render.TextBox").soundOpts(game,
|
||||
(idef and idef.keyItem) and "Get_Key_Item" or "Get_Item1"))
|
||||
end
|
||||
if opts.pre then say(opts.pre, "", give) else give() end
|
||||
if opts.pre then say(opts.pre, opts.preFallback or "", give) else give() end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -118,11 +119,21 @@ M.CINNABAR_LAB_METRONOME_ROOM = {
|
||||
},
|
||||
}
|
||||
|
||||
-- TM42 Dream Eater (scripts/ViridianCity.asm, the fisher; no pre text)
|
||||
-- TM42 Dream Eater (scripts/ViridianCity.asm, the fisher). The fisher's
|
||||
-- YouCanHaveThisText prints before GiveItem, so this gift needs a pre
|
||||
-- text (#775). Like the SilphCo2F worker (#393) that label carries no
|
||||
-- leading underscore, and on Red it sits outside the extractor's symbol
|
||||
-- set, so the literal from text/ViridianCity.asm rides along as the
|
||||
-- fallback; Yellow resolves the ROM string instead.
|
||||
M.VIRIDIAN_CITY = {
|
||||
talk = {
|
||||
TEXT_VIRIDIANCITY_FISHER = gift({
|
||||
flag = "EVENT_GOT_TM42", item = "TM_DREAM_EATER",
|
||||
pre = "ViridianCityFisherYouCanHaveThisText",
|
||||
preFallback = "Yawn!\nI must have dozed\voff in the sun."
|
||||
.. "\fI had this dream\nabout a DROWZEE\veating my dream."
|
||||
.. "\vWhat's this?\vWhere did this TM\vcome from?"
|
||||
.. "\fThis is spooky!\nHere, you can\vhave this TM.",
|
||||
received = "_ViridianCityFisherReceivedTM42Text",
|
||||
explain = "_ViridianCityFisherTM42ExplanationText",
|
||||
noRoom = "_ViridianCityFisherTM42NoRoomText",
|
||||
@@ -135,6 +146,9 @@ M.SILPH_CO_2F = {
|
||||
talk = {
|
||||
TEXT_SILPHCO2F_SILPH_WORKER_F = gift({
|
||||
flag = "EVENT_GOT_TM36", item = "TM_SELFDESTRUCT",
|
||||
-- the label carries no leading underscore: pokered keeps this one in
|
||||
-- the script bank, not the far-text bank (#393)
|
||||
pre = "SilphCo2FSilphWorkerFPleaseTakeThisText",
|
||||
received = "_SilphCo2FSilphWorkerFReceivedTM36Text",
|
||||
explain = "_SilphCo2FSilphWorkerFTM36ExplanationText",
|
||||
noRoom = "_SilphCo2FSilphWorkerFTM36NoRoomText",
|
||||
@@ -173,6 +187,20 @@ M.ROUTE_18_GATE_2F = {
|
||||
{ "face_player" },
|
||||
{ "trade", 6, "EVENT_TRADED_SLOWBRO_FOR_LICKITUNG" }, -- MARC
|
||||
},
|
||||
-- Yellow replaces the youngster with a cook trading SPIKE
|
||||
-- (TANGELA -> PARASECT): pokeyellow/scripts/Route18Gate2F.asm
|
||||
-- Route18Gate2FCookText runs TRADE_FOR_SPIKE, index 6 in the Yellow
|
||||
-- TradeMons table that Data:applyVersionedFieldData swaps in. Red
|
||||
-- maps have no COOK object here and Yellow maps have no YOUNGSTER,
|
||||
-- so each version only ever fires its own row (#651). Both rows
|
||||
-- share the Red-flavoured done flag on purpose: a .sav tracks
|
||||
-- "trade slot 6 completed" in one wCompletedInGameTradeFlags bit
|
||||
-- either version reads, and the save codec maps that bit to this
|
||||
-- flag name (src/save_convert/GenSave.lua EXTRA_FLAG_BITS).
|
||||
TEXT_ROUTE18GATE2F_COOK = {
|
||||
{ "face_player" },
|
||||
{ "trade", 6, "EVENT_TRADED_SLOWBRO_FOR_LICKITUNG" }, -- SPIKE (Yellow)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -418,7 +446,7 @@ local function pewterGymEscort(game, ow)
|
||||
end
|
||||
|
||||
local function afterWalk()
|
||||
if guy then guy.facing = "left" end
|
||||
if guy then guy.stepFrames, guy.facing = nil, "left" end
|
||||
Music.playMap(game.data, "PEWTER_CITY")
|
||||
push(game, t._PewterCityYoungsterGoTakeOnBrockText
|
||||
or "Go take on BROCK\nat the GYM first!", walkHome)
|
||||
@@ -441,6 +469,11 @@ local function pewterGymEscort(game, ow)
|
||||
end
|
||||
|
||||
local function beginWalk()
|
||||
-- the escort runs the youngster on the player's own frames per cell
|
||||
-- engine/overworld/movement.asm:737 (DoScriptedNPCMovement)
|
||||
if guy then
|
||||
guy.stepFrames = ow.player.stepFramesCur or ow.player.stepFrames
|
||||
end
|
||||
Music.play(game.data, "Music_MuseumGuy")
|
||||
if guy and head > 0 then
|
||||
local h = 0
|
||||
@@ -481,12 +514,12 @@ M.PEWTER_CITY = {
|
||||
-- Rival ambush: show the hidden rival, walk him up to the player, run
|
||||
-- the battle rows, march him back and hide him. On a loss the walk is
|
||||
-- skipped (the blackout rebuilds the map mid-script).
|
||||
local function runAmbush(game, ow, rows, playerFacing)
|
||||
local function runAmbush(game, ow, rows, playerFacing, musicOpts)
|
||||
if ow.runner:isRunning() then return false end
|
||||
ow.player.facing = playerFacing
|
||||
-- the rival encounter sting (MUSIC_MEET_RIVAL); the battle music
|
||||
-- takes over and the map theme returns after the victory jingle
|
||||
require("src.core.Music").play(game.data, "Music_MeetRival")
|
||||
require("src.core.Music").play(game.data, "Music_MeetRival", nil, musicOpts)
|
||||
ow.runner:run(rows)
|
||||
return true
|
||||
end
|
||||
@@ -540,12 +573,15 @@ local function route22Scene(n, objIndex, objName, oppClass, baseParty, beatFlag,
|
||||
{ "face_object", objIndex, rivalFacing }, -- 3
|
||||
{ "show_text", "_Route22RivalBeforeBattleText" .. n }, -- 4
|
||||
{ "rival_battle", oppClass, baseParty }, -- 5
|
||||
{ "jump_if_false", 11 }, -- 6
|
||||
{ "jump_if_false", 13 }, -- 6
|
||||
{ "set_flag", beatFlag }, -- 7
|
||||
{ "show_text", "_Route22Rival" .. n .. "DefeatedText" }, -- 8
|
||||
{ "show_text", "_Route22RivalAfterBattleText" .. n }, -- 9
|
||||
{ "walk_npc", objIndex, route22ExitDirs(n, py) }, -- 10
|
||||
{ "hide_object", "ROUTE_22", objName }, -- 11
|
||||
{ "play_music", "Music_MeetRival", { start = "rival",
|
||||
tempo = n == 2 and 100 or nil } }, -- 10
|
||||
{ "walk_npc", objIndex, route22ExitDirs(n, py) }, -- 11
|
||||
{ "play_default_music" }, -- scripts/Route22.asm:230
|
||||
{ "hide_object", "ROUTE_22", objName }, -- 13
|
||||
}
|
||||
end
|
||||
|
||||
@@ -567,7 +603,8 @@ M.ROUTE_22 = {
|
||||
if f.EVENT_BEAT_GIOVANNI and not f.EVENT_BEAT_ROUTE22_RIVAL_2ND_BATTLE then
|
||||
return runAmbush(game, ow,
|
||||
route22Scene(2, 2, "ROUTE22_RIVAL2", "OPP_RIVAL2", 10,
|
||||
"EVENT_BEAT_ROUTE22_RIVAL_2ND_BATTLE", y), playerFacing)
|
||||
"EVENT_BEAT_ROUTE22_RIVAL_2ND_BATTLE", y), playerFacing,
|
||||
{ tempo = 100 })
|
||||
end
|
||||
return false
|
||||
end,
|
||||
@@ -591,12 +628,14 @@ local function ceruleanRivalScene(px, py)
|
||||
{ "face_object", 1, "down" }, -- 3
|
||||
{ "show_text", "_CeruleanCityRivalPreBattleText" }, -- 4
|
||||
{ "rival_battle", "OPP_RIVAL1", 7 }, -- 5
|
||||
{ "jump_if_false", 11 }, -- 6
|
||||
{ "jump_if_false", 13 }, -- 6
|
||||
{ "set_flag", "EVENT_BEAT_CERULEAN_RIVAL" }, -- 7
|
||||
{ "show_text", "_CeruleanCityRivalDefeatedText" }, -- 8
|
||||
{ "show_text", "_CeruleanCityRivalIWentToBillsText" }, -- 9
|
||||
{ "walk_npc", 1, ceruleanRivalExitDirs(px) }, -- 10
|
||||
{ "hide_object", "CERULEAN_CITY", "CERULEANCITY_RIVAL" }, -- 11
|
||||
{ "play_music", "Music_MeetRival", { start = "rival" } }, -- 10
|
||||
{ "walk_npc", 1, ceruleanRivalExitDirs(px) }, -- 11
|
||||
{ "play_default_music" }, -- scripts/CeruleanCity.asm:230
|
||||
{ "hide_object", "CERULEAN_CITY", "CERULEANCITY_RIVAL" }, -- 13
|
||||
}
|
||||
end
|
||||
|
||||
@@ -703,7 +742,7 @@ local JIGGLYPUFF_SILENCE, JIGGLYPUFF_STEP, JIGGLYPUFF_TAIL = 32, 24, 48
|
||||
-- Built as a TextBox `auto` table: auto.sound fires the frame the last
|
||||
-- page has typed out (PrintText returning), and auto.tick then runs once
|
||||
-- per frame while the gate it returns still reads as playing.
|
||||
local function jigglypuffDance(game, npc)
|
||||
local function jigglypuffDance(game, npc, ow)
|
||||
local Music = require("src.core.Music")
|
||||
-- .findMatchingFacingDirectionLoop: the rotation picks up at the entry
|
||||
-- matching the sprite's current facing (showMapText has just turned it
|
||||
@@ -749,7 +788,13 @@ local function jigglypuffDance(game, npc)
|
||||
if npc then npc.facing = JIGGLYPUFF_SPIN[step] end
|
||||
return
|
||||
end
|
||||
if frames >= JIGGLYPUFF_TAIL then phase = "done" end
|
||||
if frames >= JIGGLYPUFF_TAIL then
|
||||
phase = "done"
|
||||
if require("src.core.GameVersion").isYellow()
|
||||
and require("src.world.PikachuFollower").starterInParty(game.save) then
|
||||
ow.pikachuPewterSleepScene = true
|
||||
end
|
||||
end
|
||||
end,
|
||||
}
|
||||
end
|
||||
@@ -762,7 +807,7 @@ M.PEWTER_POKECENTER = {
|
||||
local TextBox = require("src.render.TextBox")
|
||||
game.stack:push(TextBox.new(game,
|
||||
text(game)._PewterPokecenterJigglypuffText or "JIGGLYPUFF: Puu\npupuu!",
|
||||
done, { auto = jigglypuffDance(game, npc) }))
|
||||
done, { auto = jigglypuffDance(game, npc, ow) }))
|
||||
end,
|
||||
},
|
||||
}
|
||||
@@ -787,8 +832,19 @@ local function bikeGateGuard(coords, stopText, explainText)
|
||||
local t = text(game)
|
||||
push(game, t[stopText] or "Hey! Wait up!", function()
|
||||
push(game, t[explainText] or "You need a\nBICYCLE for\nCYCLING ROAD!", function()
|
||||
-- pokered's Route16Gate1FGuardScript / Route18Gate1FGuardScript
|
||||
-- (scripts/Route16Gate1F.asm, Route18Gate1F.asm) simulate one
|
||||
-- PAD_RIGHT step after the refusal text, and only clear
|
||||
-- wJoyIgnore / hand control back once that step finishes
|
||||
-- (PlayerMovingRightScript). Without it the player was left
|
||||
-- parked beside the guard's counter with no way past. #518
|
||||
local function shoveRight()
|
||||
ow:scriptMove(ow.player, "right", 1)
|
||||
end
|
||||
if dist > 0 then
|
||||
ow:scriptMove(ow.player, "up", dist)
|
||||
ow:scriptMove(ow.player, "up", dist, shoveRight)
|
||||
else
|
||||
shoveRight()
|
||||
end
|
||||
end)
|
||||
end)
|
||||
@@ -826,16 +882,31 @@ M.SILPH_CO_7F = {
|
||||
{ "face_object", 9, "up" }, -- 4
|
||||
{ "show_text", "_SilphCo7FRivalWaitedHereText" }, -- 5
|
||||
{ "rival_battle", "OPP_RIVAL2", 7 }, -- 6
|
||||
{ "jump_if_false", 12 }, -- 7
|
||||
{ "jump_if_false", 14 }, -- 7
|
||||
{ "set_flag", "EVENT_BEAT_SILPH_CO_RIVAL" }, -- 8
|
||||
{ "show_text", "_SilphCo7FRivalDefeatedText" }, -- 9
|
||||
{ "show_text", "_SilphCo7FRivalGoodLuckToYouText" }, -- 10
|
||||
{ "move_npc_to", 9, 5, y + 1 }, -- 11
|
||||
{ "hide_object", "SILPH_CO_7F", "SILPHCO7F_RIVAL" }, -- 12
|
||||
{ "play_music", "Music_MeetRival", { start = "rival" } }, -- 11
|
||||
{ "move_npc_to", 9, 5, y + 1 }, -- 12
|
||||
{ "play_default_music" }, -- scripts/SilphCo7F.asm:261
|
||||
{ "hide_object", "SILPH_CO_7F", "SILPHCO7F_RIVAL" }, -- 14
|
||||
}, "down")
|
||||
end,
|
||||
}
|
||||
|
||||
-- SSAnne2FRivalAfterBattleScript's exit walk is keyed on the player's X,
|
||||
-- not on where the rival stopped: from (37,8) the rival stands below him on
|
||||
-- (36,8) and takes .RivalDownFourMovement straight down out of the room;
|
||||
-- from (36,8) he stands above him on (36,7) and takes
|
||||
-- .RivalWalkAroundPlayerMovement, which steps RIGHT and then falls through
|
||||
-- into the same four DOWNs, so five downs in all (#360).
|
||||
local function ssAnne2FRivalExitDirs(onLeft)
|
||||
if onLeft then
|
||||
return { "right", "down", "down", "down", "down", "down" }
|
||||
end
|
||||
return { "down", "down", "down", "down" }
|
||||
end
|
||||
|
||||
-- S.S. Anne 2F rival ambush (scripts/SSAnne2F.asm; coords 36/37,8)
|
||||
M.SS_ANNE_2F = {
|
||||
onStep = function(game, ow, x, y)
|
||||
@@ -848,11 +919,14 @@ M.SS_ANNE_2F = {
|
||||
{ "face_object", 2, onLeft and "down" or "right" }, -- 3
|
||||
{ "show_text", "_SSAnne2FRivalText" }, -- 4
|
||||
{ "rival_battle", "OPP_RIVAL2", 1 }, -- 5
|
||||
{ "jump_if_false", 10 }, -- 6
|
||||
{ "jump_if_false", 13 }, -- 6
|
||||
{ "set_flag", "EVENT_BEAT_SS_ANNE_RIVAL" }, -- 7
|
||||
{ "show_text", "_SSAnne2FRivalDefeatedText" }, -- 8
|
||||
{ "move_npc_to", 2, 36, 4 }, -- 9
|
||||
{ "hide_object", "SS_ANNE_2F", "SSANNE2F_RIVAL" }, -- 10
|
||||
{ "show_text", "_SSAnne2FRivalCutMasterText" }, -- 9
|
||||
{ "play_music", "Music_MeetRival", { start = "rival" } }, -- 10
|
||||
{ "walk_npc", 2, ssAnne2FRivalExitDirs(onLeft) }, -- 11
|
||||
{ "play_default_music" }, -- scripts/SSAnne2F.asm:175
|
||||
{ "hide_object", "SS_ANNE_2F", "SSANNE2F_RIVAL" }, -- 13
|
||||
}, onLeft and "up" or "left")
|
||||
end,
|
||||
}
|
||||
|
||||
@@ -12,9 +12,13 @@ local function push(game, s, done)
|
||||
game.stack:push(TextBox.new(game, s, done))
|
||||
end
|
||||
|
||||
-- PrintText on a text_end string returns with the box still drawn and
|
||||
-- YesNoChoice then draws the menu above it (InitYesNoTextBoxParameters,
|
||||
-- engine/menus/text_box.asm); no A press clears the question first. Ride
|
||||
-- TextBox's opts.choice, the same as Commands.ask (#854).
|
||||
local function ask(game, s, cb)
|
||||
local ChoiceBox = require("src.ui.ChoiceBox")
|
||||
push(game, s, function() game.stack:push(ChoiceBox.new(game, cb)) end)
|
||||
local TextBox = require("src.render.TextBox")
|
||||
game.stack:push(TextBox.new(game, s, nil, { choice = cb }))
|
||||
end
|
||||
|
||||
-- -------------------------------------------------------------------
|
||||
@@ -118,6 +122,7 @@ local MANSION_HOLES = {
|
||||
M.POKEMON_MANSION_3F.onStep = function(game, ow, x, y)
|
||||
for _, h in ipairs(MANSION_HOLES) do
|
||||
if x == h[1] and y == h[2] then
|
||||
require("src.core.Sound").play(game.data, "Faint_Fall")
|
||||
ow:startWarpTo(h[3], h[4], h[5], ow.player.facing)
|
||||
return true
|
||||
end
|
||||
@@ -173,7 +178,46 @@ local function syncGymGatesAfterBattle(game, ow)
|
||||
applyGymGates(game, ow)
|
||||
end
|
||||
|
||||
-- Yellow's quiz-first rule (scripts/CinnabarGym.asm SuperNerd2..7): a
|
||||
-- gate guardian refuses to battle until his quiz was attempted -- a
|
||||
-- wrong answer sics him on you (the onInteract path below), a right one
|
||||
-- opens his gate; walking up and talking first just gets the room's
|
||||
-- CinnabarGymText_N lecture (Func_f2150's pointer table).
|
||||
local function yellowQuizTalk(machineIndex)
|
||||
return function(game, ow, npc, done)
|
||||
local yellow = require("src.core.GameVersion").isYellow()
|
||||
local defeated = ow:trainerDefeated(npc)
|
||||
local gateOpen = game.save.flags[gymGateFlag(machineIndex)]
|
||||
if yellow and not defeated and not gateOpen then
|
||||
local TextBox = require("src.render.TextBox")
|
||||
local t = game.data.text
|
||||
game.stack:push(TextBox.new(game,
|
||||
t["_CinnabarGymText_" .. machineIndex]
|
||||
or "You have to take\nthe quiz first!", done))
|
||||
return
|
||||
end
|
||||
-- gate open (or Red/Blue): the ordinary trainer engagement /
|
||||
-- after-battle line, mirroring talkTo's generic trainer branch
|
||||
npc:facePlayer(ow.player)
|
||||
if not defeated then
|
||||
ow:engageTrainer(npc, done)
|
||||
return
|
||||
end
|
||||
local header = game.data:trainerHeader(ow.map.def.label, npc.def.index)
|
||||
local after = header and header.after and game.data.text[header.after]
|
||||
local TextBox = require("src.render.TextBox")
|
||||
game.stack:push(TextBox.new(game, after or "...", done))
|
||||
end
|
||||
end
|
||||
|
||||
M.CINNABAR_GYM = {
|
||||
talk = (function()
|
||||
local talk = {}
|
||||
for i = 1, 6 do
|
||||
talk["TEXT_CINNABARGYM_SUPER_NERD" .. (i + 1)] = yellowQuizTalk(i)
|
||||
end
|
||||
return talk
|
||||
end)(),
|
||||
onEnter = applyGymGates,
|
||||
onVictory = syncGymGatesAfterBattle,
|
||||
onInteract = function(game, ow, fx, fy)
|
||||
|
||||
@@ -18,6 +18,24 @@
|
||||
-- script). Leaders are not def_trainers entries, so engageTrainer has
|
||||
-- no header.won -- checkVictoryRewards shows this chain instead of a
|
||||
-- synthetic "received badge/TM" stub.
|
||||
--
|
||||
-- Gym entries split the TM hand-over out of `dialogue`, mirroring the
|
||||
-- originals' GiveItem check (`call GiveItem` / `jr nc, .BagFull`):
|
||||
-- `tmPre` is the ReceiveTM script's lead-in (badge info / "Wait! Take
|
||||
-- this!"), shown at the victory and again when a beaten leader retries
|
||||
-- the hand-over; `tmDialogue` shows only when the TM actually goes in
|
||||
-- the bag; `noRoom` is the "make room" line shown instead when the bag
|
||||
-- is full; `gotFlag` (pokered's EVENT_GOT_TM*) is set only on a
|
||||
-- successful give, which is what makes the leader's talk script retry
|
||||
-- later (gyms.lua).
|
||||
--
|
||||
-- `badgeSound` / `tmSound` are the text sound command each gym's reward
|
||||
-- text carries right after its FIRST label -- home/text.asm TextCommand_SOUND
|
||||
-- plays it once that page has typed out and then blocks on
|
||||
-- WaitForSoundToFinish, so the jingle sits between the pages rather than
|
||||
-- under them. macros/scripts/text.asm defines sound_level_up as
|
||||
-- sound_get_item_1, so Pewter's and Viridian's badge lines are Get_Item1
|
||||
-- too. Vermilion, Celadon and Fuchsia carry no sound on the badge text.
|
||||
|
||||
local function range(prefix, first, last)
|
||||
local t = {}
|
||||
@@ -33,83 +51,136 @@ return {
|
||||
-- escort NPC and the first Route 22 rival stay gone after the badge.
|
||||
["OPP_BROCK#1"] = { badge = "BOULDERBADGE", flag = "EVENT_BEAT_BROCK",
|
||||
item = "TM_BIDE",
|
||||
gotFlag = "EVENT_GOT_TM34",
|
||||
noRoom = "_PewterGymTM34NoRoomText",
|
||||
deactivate = { "EVENT_BEAT_PEWTER_GYM_TRAINER_0" },
|
||||
hide = {
|
||||
{ "PEWTER_CITY", "PEWTERCITY_YOUNGSTER" },
|
||||
{ "ROUTE_22", "ROUTE22_RIVAL1" },
|
||||
},
|
||||
badgeSound = "Get_Item1", -- sound_level_up
|
||||
tmSound = "Get_Item1",
|
||||
dialogue = {
|
||||
"_PewterGymBrockReceivedBoulderBadgeText",
|
||||
"_PewterGymBrockBoulderBadgeInfoText",
|
||||
"_PewterGymBrockWaitTakeThisText",
|
||||
},
|
||||
tmPre = { "_PewterGymBrockWaitTakeThisText" },
|
||||
tmDialogue = {
|
||||
"_PewterGymReceivedTM34Text",
|
||||
"_TM34ExplanationText",
|
||||
} },
|
||||
["OPP_MISTY#1"] = { badge = "CASCADEBADGE", flag = "EVENT_BEAT_MISTY",
|
||||
item = "TM_BUBBLEBEAM",
|
||||
gotFlag = "EVENT_GOT_TM11",
|
||||
noRoom = "_CeruleanGymMistyTM11NoRoomText",
|
||||
deactivate = range("EVENT_BEAT_CERULEAN_GYM_TRAINER_", 0, 1),
|
||||
badgeSound = "Get_Key_Item",
|
||||
tmSound = "Get_Item1",
|
||||
dialogue = {
|
||||
"_CeruleanGymMistyReceivedCascadeBadgeText",
|
||||
"_CeruleanGymMistyCascadeBadgeInfoText",
|
||||
},
|
||||
tmPre = { "_CeruleanGymMistyCascadeBadgeInfoText" },
|
||||
tmDialogue = {
|
||||
"_CeruleanGymMistyReceivedTM11Text",
|
||||
} },
|
||||
["OPP_LT_SURGE#1"] = { badge = "THUNDERBADGE", flag = "EVENT_BEAT_LT_SURGE",
|
||||
item = "TM_THUNDERBOLT",
|
||||
gotFlag = "EVENT_GOT_TM24",
|
||||
noRoom = "_VermilionGymLTSurgeTM24NoRoomText",
|
||||
deactivate = range("EVENT_BEAT_VERMILION_GYM_TRAINER_", 0, 2),
|
||||
tmSound = "Get_Key_Item",
|
||||
dialogue = {
|
||||
"_VermilionGymLTSurgeReceivedThunderBadgeText",
|
||||
"_VermilionGymLTSurgeThunderBadgeInfoText",
|
||||
},
|
||||
tmPre = { "_VermilionGymLTSurgeThunderBadgeInfoText" },
|
||||
tmDialogue = {
|
||||
"_VermilionGymLTSurgeReceivedTM24Text",
|
||||
"_TM24ExplanationText",
|
||||
} },
|
||||
["OPP_ERIKA#1"] = { badge = "RAINBOWBADGE", flag = "EVENT_BEAT_ERIKA",
|
||||
item = "TM_MEGA_DRAIN",
|
||||
gotFlag = "EVENT_GOT_TM21",
|
||||
noRoom = "_CeladonGymTM21NoRoomText",
|
||||
deactivate = range("EVENT_BEAT_CELADON_GYM_TRAINER_", 0, 6),
|
||||
tmSound = "Get_Item1",
|
||||
dialogue = {
|
||||
"_CeladonGymErikaReceivedRainbowBadgeText",
|
||||
"_CeladonGymRainbowBadgeInfoText",
|
||||
},
|
||||
tmPre = { "_CeladonGymRainbowBadgeInfoText" },
|
||||
tmDialogue = {
|
||||
"_CeladonGymReceivedTM21Text",
|
||||
"_TM21ExplanationText",
|
||||
} },
|
||||
["OPP_KOGA#1"] = { badge = "SOULBADGE", flag = "EVENT_BEAT_KOGA",
|
||||
item = "TM_TOXIC",
|
||||
gotFlag = "EVENT_GOT_TM06",
|
||||
noRoom = "_FuchsiaGymKogaTM06NoRoomText",
|
||||
deactivate = range("EVENT_BEAT_FUCHSIA_GYM_TRAINER_", 0, 5),
|
||||
tmSound = "Get_Key_Item",
|
||||
dialogue = {
|
||||
"_FuchsiaGymKogaReceivedSoulBadgeText",
|
||||
"_FuchsiaGymKogaSoulBadgeInfoText",
|
||||
},
|
||||
tmPre = { "_FuchsiaGymKogaSoulBadgeInfoText" },
|
||||
tmDialogue = {
|
||||
"_FuchsiaGymKogaReceivedTM06Text",
|
||||
"_FuchsiaGymKogaTM06ExplanationText",
|
||||
} },
|
||||
["OPP_SABRINA#1"] = { badge = "MARSHBADGE", flag = "EVENT_BEAT_SABRINA",
|
||||
item = "TM_PSYWAVE",
|
||||
gotFlag = "EVENT_GOT_TM46",
|
||||
noRoom = "_SaffronGymSabrinaTM46NoRoomText",
|
||||
deactivate = range("EVENT_BEAT_SAFFRON_GYM_TRAINER_", 0, 6),
|
||||
badgeSound = "Get_Key_Item",
|
||||
tmSound = "Get_Item1",
|
||||
dialogue = {
|
||||
"_SaffronGymSabrinaReceivedMarshBadgeText",
|
||||
"_SaffronGymSabrinaMarshBadgeInfoText",
|
||||
},
|
||||
tmPre = { "_SaffronGymSabrinaMarshBadgeInfoText" },
|
||||
tmDialogue = {
|
||||
"_SaffronGymSabrinaReceivedTM46Text",
|
||||
"_TM46ExplanationText",
|
||||
} },
|
||||
["OPP_BLAINE#1"] = { badge = "VOLCANOBADGE", flag = "EVENT_BEAT_BLAINE",
|
||||
item = "TM_FIRE_BLAST",
|
||||
gotFlag = "EVENT_GOT_TM38",
|
||||
noRoom = "_CinnabarGymBlaineTM38NoRoomText",
|
||||
deactivate = range("EVENT_BEAT_CINNABAR_GYM_TRAINER_", 0, 6),
|
||||
badgeSound = "Get_Key_Item",
|
||||
tmSound = "Get_Item1",
|
||||
dialogue = {
|
||||
"_CinnabarGymBlaineReceivedVolcanoBadgeText",
|
||||
"_CinnabarGymBlaineVolcanoBadgeInfoText",
|
||||
},
|
||||
tmPre = { "_CinnabarGymBlaineVolcanoBadgeInfoText" },
|
||||
tmDialogue = {
|
||||
"_CinnabarGymBlaineReceivedTM38Text",
|
||||
"_CinnabarGymBlaineTM38ExplanationText",
|
||||
} },
|
||||
["OPP_GIOVANNI#3"] = { badge = "EARTHBADGE", flag = "EVENT_BEAT_GIOVANNI",
|
||||
item = "TM_FISSURE",
|
||||
gotFlag = "EVENT_GOT_TM27",
|
||||
noRoom = "_ViridianGymGiovanniTM27NoRoomText",
|
||||
deactivate = range("EVENT_BEAT_VIRIDIAN_GYM_TRAINER_", 0, 7),
|
||||
badgeSound = "Get_Item1", -- sound_level_up
|
||||
tmSound = "Get_Item1",
|
||||
dialogue = {
|
||||
"_ViridianGymGiovanniReceivedEarthBadgeText",
|
||||
"_ViridianGymGiovanniEarthBadgeInfoText",
|
||||
},
|
||||
tmPre = { "_ViridianGymGiovanniEarthBadgeInfoText" },
|
||||
tmDialogue = {
|
||||
"_ViridianGymGiovanniReceivedTM27Text",
|
||||
"_ViridianGymGiovanniTM27ExplanationText",
|
||||
} },
|
||||
|
||||
-- Silph Co. Giovanni: unlocks the president's Master Ball gift
|
||||
["OPP_GIOVANNI#2"] = { flag = "EVENT_BEAT_SILPH_CO_GIOVANNI" },
|
||||
-- Silph Co. Giovanni: unlocks the president's Master Ball gift.
|
||||
-- SilphCo11FGiovanniStartBattleScript (scripts/SilphCo11F.asm) hands the
|
||||
-- battle SilphCo10FGiovanniILostAgainText through SaveEndBattleTextPointers,
|
||||
-- but he has no def_trainers header on 11F, so engageTrainer finds no
|
||||
-- header.won to give it -- this chain is the port's stand-in for that loss
|
||||
-- line (#722). The "Blast it all!" speech, the fade and the rockets
|
||||
-- leaving are SilphCo11FGiovanniAfterBattleScript, ported in M.SILPH_CO_11F
|
||||
-- (data/scripts/story.lua).
|
||||
["OPP_GIOVANNI#2"] = { flag = "EVENT_BEAT_SILPH_CO_GIOVANNI",
|
||||
dialogue = { "_SilphCo10FGiovanniILostAgainText" } },
|
||||
|
||||
-- Fighting Dojo Karate Master (scripts/FightingDojo.asm
|
||||
-- FightingDojoKarateMasterPostBattleScript sets EVENT_BEAT_KARATE_MASTER,
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
-- Summer Beach House (scripts/SummerBeachHouse.asm), Yellow's Route 19
|
||||
-- surf shack. The Surfin' Dude only lets a party Pikachu that knows
|
||||
-- SURF ride (IsSurfingPikachuInParty, home/map_objects.asm); saying yes
|
||||
-- runs the Surfing Pikachu minigame (src/ui/SurfingMinigame.lua). The
|
||||
-- corner printer shows/prints the high score once you have surfed this
|
||||
-- visit (BIT_PIKACHU_MAP_SURF_SELECT is a per-map-load flag, so the
|
||||
-- session markers live on the overworld state, not the save).
|
||||
|
||||
local function surfingPikachu(game)
|
||||
for _, mon in ipairs(game.save.party or {}) do
|
||||
if mon.species == "PIKACHU" then
|
||||
for _, mv in ipairs(mon.moves or {}) do
|
||||
if mv.id == "SURF" then return mon end
|
||||
end
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function push(game, text, done)
|
||||
local TextBox = require("src.render.TextBox")
|
||||
game.stack:push(TextBox.new(game, text, done))
|
||||
end
|
||||
|
||||
-- the two-variant posters: the surf-capable line once a surfing
|
||||
-- Pikachu is along, the plain one otherwise
|
||||
local function poster(n)
|
||||
return function(game, ow, npc, done)
|
||||
local t = game.data.text
|
||||
local key = ("_SummerBeachHousePoster%dText%d"):format(
|
||||
n, surfingPikachu(game) and 1 or 2)
|
||||
push(game, t[key] or "A surfing poster.", done)
|
||||
end
|
||||
end
|
||||
|
||||
return {
|
||||
SUMMER_BEACH_HOUSE = {
|
||||
talk = {
|
||||
TEXT_SUMMERBEACHHOUSE_SURFINDUDE = function(game, ow, npc, done)
|
||||
local t = game.data.text
|
||||
if not surfingPikachu(game) then
|
||||
push(game, t._SummerBeachHouseSurfinDudeText4
|
||||
or "Dogs and burgers\non special today!", done)
|
||||
return
|
||||
end
|
||||
-- Text1 on the first ask each visit, the short Text3 after
|
||||
local ask = ow.surfinDudeAsked
|
||||
and (t._SummerBeachHouseSurfinDudeText3 or "Wanna go SURF?")
|
||||
or (t._SummerBeachHouseSurfinDudeText1
|
||||
or "Whoa!\nYour PIKACHU knows\nhow to SURF!\fGive it a go?")
|
||||
ow.surfinDudeAsked = true
|
||||
push(game, ask, function()
|
||||
local ChoiceBox = require("src.ui.ChoiceBox")
|
||||
game.stack:push(ChoiceBox.new(game, function(yes)
|
||||
if not yes then
|
||||
push(game, t._SummerBeachHouseSurfinDudeText2
|
||||
or "Come SURF anytime,\nmy friend!", done)
|
||||
return
|
||||
end
|
||||
local SurfingMinigame = require("src.ui.SurfingMinigame")
|
||||
game.stack:push(SurfingMinigame.new(game, function()
|
||||
ow.surfedThisVisit = true -- BIT_PIKACHU_MAP_SURF_SELECT
|
||||
require("src.core.Music").playMap(game.data, ow.map.id)
|
||||
done()
|
||||
end))
|
||||
end))
|
||||
end)
|
||||
end,
|
||||
|
||||
TEXT_SUMMERBEACHHOUSE_PIKACHU = function(game, ow, npc, done)
|
||||
local t = game.data.text
|
||||
push(game, t._SummerBeachHousePikachuText or "PIKACHU: Pikaa!",
|
||||
function()
|
||||
require("src.core.Sound").playCry(game.data, "PIKACHU")
|
||||
done()
|
||||
end)
|
||||
end,
|
||||
|
||||
TEXT_SUMMERBEACHHOUSE_POSTER1 = poster(1),
|
||||
TEXT_SUMMERBEACHHOUSE_POSTER2 = poster(2),
|
||||
TEXT_SUMMERBEACHHOUSE_POSTER3 = poster(3),
|
||||
|
||||
TEXT_SUMMERBEACHHOUSE_PRINTER = function(game, ow, npc, done)
|
||||
local t = game.data.text
|
||||
if not surfingPikachu(game) then
|
||||
push(game, t._SummerBeachHousePrinterText1
|
||||
or "It's some sort of\na machine...", done)
|
||||
return
|
||||
end
|
||||
push(game, t._SummerBeachHousePrinterText2
|
||||
or "SUMMER BEACH HOUSE\nPRINTER, it says.", function()
|
||||
if not ow.surfedThisVisit then
|
||||
done()
|
||||
return
|
||||
end
|
||||
push(game, t._SummerBeachHousePrinterText3
|
||||
or "The Hi-Score is\nshown.\fPRINT it out?", function()
|
||||
local ChoiceBox = require("src.ui.ChoiceBox")
|
||||
game.stack:push(ChoiceBox.new(game, function(yes)
|
||||
if not yes then
|
||||
done()
|
||||
return
|
||||
end
|
||||
-- PrintSurfingMinigameHighScore -> PNG stand-in
|
||||
local Printer = require("src.core.Printer")
|
||||
local Font = require("src.render.Font")
|
||||
local Strings = require("src.core.Strings")
|
||||
local hi = game.save.surfingHighScore or 0
|
||||
local name = game.save.player.name or "RED"
|
||||
local saved, err = Printer.save("surf_hiscore", 160, 64,
|
||||
function()
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.rectangle("fill", 0, 0, 160, 64)
|
||||
love.graphics.setColor(0, 0, 0, 1)
|
||||
love.graphics.rectangle("line", 2.5, 2.5, 155, 59)
|
||||
Font.draw(Strings("SUMMER BEACH HOUSE"), 8, 10)
|
||||
Font.draw(Strings("SURFING Hi-Score"), 8, 24)
|
||||
Font.draw(name, 8, 40)
|
||||
Font.draw(Strings("%d pts", hi), 96, 40)
|
||||
end)
|
||||
push(game, saved
|
||||
and Strings("Printed!\fSaved as\n%s\vin the save\nfolder.",
|
||||
saved)
|
||||
or Strings("Printer error!\n%s", tostring(err)), done)
|
||||
end))
|
||||
end)
|
||||
end)
|
||||
end,
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
-- Yellow's three Kanto-starter side-quests, hand-ported from
|
||||
-- scripts/CeruleanMelaniesHouse.asm, scripts/Route24.asm
|
||||
-- (Route24CooltrainerM4Text) and scripts/VermilionCity_2.asm
|
||||
-- (VermilionCityPrintOfficerJennyText). Registered on top of the shared
|
||||
-- Red map scripts by data/scripts/init.lua on a Yellow boot only, so the
|
||||
-- table keys compose with story.lua / story4.lua's existing entries.
|
||||
|
||||
local M = {}
|
||||
|
||||
-- Melanie hands over her Bulbasaur once the starter Pikachu trusts you
|
||||
-- (wPikachuHappiness >= 147).
|
||||
M.CERULEAN_MELANIES_HOUSE = {
|
||||
talk = {
|
||||
TEXT_CERULEANMELANIESHOUSE_MELANIE = function(game, ow, npc, done)
|
||||
local rows = { { "face_player" } }
|
||||
if game.save.flags.EVENT_GOT_BULBASAUR_IN_CERULEAN then
|
||||
rows[#rows + 1] = { "show_text", "MelanieText4" }
|
||||
elseif (game.save.pikachuHappiness or 90) < 147 then
|
||||
rows[#rows + 1] = { "show_text", "MelanieText1" }
|
||||
else
|
||||
rows[#rows + 1] = { "show_text", "MelanieText1" }
|
||||
rows[#rows + 1] = { "ask", "MelanieText2" }
|
||||
rows[#rows + 1] = { "jump_if_false", "declined" }
|
||||
rows[#rows + 1] = { "give_pokemon", "BULBASAUR", 10 }
|
||||
rows[#rows + 1] = { "jump_if_false", "end" } -- party + box full
|
||||
-- Event and HideObject before MelanieText3: give_pokemon's nickname
|
||||
-- prompt and the received text both yield, and a script that dies
|
||||
-- in that window used to leave the BULBASAUR given with the event
|
||||
-- clear, so Melanie offered a second one (#426). The flag is only
|
||||
-- read at script entry, so the order is invisible in play.
|
||||
rows[#rows + 1] = { "set_flag", "EVENT_GOT_BULBASAUR_IN_CERULEAN" }
|
||||
rows[#rows + 1] = { "hide_object", "CERULEAN_MELANIES_HOUSE",
|
||||
"CERULEANMELANIESHOUSE_BULBASAUR" }
|
||||
rows[#rows + 1] = { "show_text", "MelanieText3" }
|
||||
rows[#rows + 1] = { "jump", "end" }
|
||||
rows[#rows + 1] = { "label", "declined" }
|
||||
rows[#rows + 1] = { "show_text", "MelanieText5" }
|
||||
end
|
||||
ow.runner:run(rows, { npc = npc, onDone = done,
|
||||
checkpointOnDone = "release_npc" })
|
||||
end,
|
||||
-- pet flavor: the text with the species' cry over it
|
||||
TEXT_CERULEANMELANIESHOUSE_BULBASAUR = {
|
||||
{ "play_cry", "BULBASAUR" },
|
||||
{ "show_text", "MelanieBulbasaurText" },
|
||||
},
|
||||
TEXT_CERULEANMELANIESHOUSE_ODDISH = {
|
||||
{ "play_cry", "ODDISH" },
|
||||
{ "show_text", "MelanieOddishText" },
|
||||
},
|
||||
TEXT_CERULEANMELANIESHOUSE_SANDSHREW = {
|
||||
{ "play_cry", "SANDSHREW" },
|
||||
{ "show_text", "MelanieSandshrewText" },
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
-- Damian gives away the Charmander he thinks is too weak (EVENT_54F).
|
||||
M.ROUTE_24 = {
|
||||
talk = {
|
||||
TEXT_ROUTE24_COOLTRAINER_M4 = {
|
||||
{ "face_player" },
|
||||
{ "check_flag", "EVENT_54F" },
|
||||
{ "jump_if_true", "after" },
|
||||
{ "ask", "_Route24DamianText1" },
|
||||
{ "jump_if_false", "declined" },
|
||||
{ "give_pokemon", "CHARMANDER", 10 },
|
||||
{ "jump_if_false", "end" },
|
||||
-- SetEvent hoisted ahead of the received text (Route24.asm writes it
|
||||
-- after Route24Text_515e3, where nothing in between can fail): the
|
||||
-- nickname prompt and the text both yield, and a script death in that
|
||||
-- window left the CHARMANDER given with EVENT_54F clear, so Damian
|
||||
-- handed out a second one on the next talk (#426).
|
||||
{ "set_flag", "EVENT_54F" },
|
||||
{ "show_text", "_Route24DamianText2" },
|
||||
{ "jump", "end" },
|
||||
{ "label", "declined" },
|
||||
{ "show_text", "_Route24DamianText3" },
|
||||
{ "jump", "end" },
|
||||
{ "label", "after" },
|
||||
{ "show_text", "_Route24DamianText4" },
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
-- Officer Jenny's Squirtle: kept until you carry the Thunder Badge.
|
||||
M.VERMILION_CITY = {
|
||||
talk = {
|
||||
TEXT_VERMILIONCITY_OFFICER_JENNY = function(game, ow, npc, done)
|
||||
local rows = { { "face_player" } }
|
||||
if game.save.flags.EVENT_GOT_SQUIRTLE_FROM_OFFICER_JENNY then
|
||||
rows[#rows + 1] = { "show_text", "_OfficerJennyText5" }
|
||||
elseif not (game.save.inventory
|
||||
and game.save.inventory.THUNDERBADGE) then
|
||||
rows[#rows + 1] = { "show_text", "_OfficerJennyText1" }
|
||||
else
|
||||
rows[#rows + 1] = { "ask", "_OfficerJennyText2" }
|
||||
rows[#rows + 1] = { "jump_if_false", "declined" }
|
||||
rows[#rows + 1] = { "give_pokemon", "SQUIRTLE", 10 }
|
||||
rows[#rows + 1] = { "jump_if_false", "end" }
|
||||
-- event before the received text, as above (#426)
|
||||
rows[#rows + 1] = { "set_flag",
|
||||
"EVENT_GOT_SQUIRTLE_FROM_OFFICER_JENNY" }
|
||||
rows[#rows + 1] = { "show_text", "_OfficerJennyText3" }
|
||||
rows[#rows + 1] = { "jump", "end" }
|
||||
rows[#rows + 1] = { "label", "declined" }
|
||||
rows[#rows + 1] = { "show_text", "_OfficerJennyText4" }
|
||||
end
|
||||
ow.runner:run(rows, { npc = npc, onDone = done,
|
||||
checkpointOnDone = "release_npc" })
|
||||
end,
|
||||
},
|
||||
}
|
||||
|
||||
return M
|
||||
@@ -0,0 +1,308 @@
|
||||
-- Jessie & James, Yellow's Team Rocket duo, at all four ambush sites:
|
||||
-- Mt Moon B2F (scripts/MtMoonB2F.asm), Rocket Hideout B4F
|
||||
-- (scripts/RocketHideoutB4F.asm), Pokemon Tower 7F
|
||||
-- (scripts/PokemonTower7F.asm) and Silph Co 11F (scripts/SilphCo11F.asm).
|
||||
-- Every site shares one shape: a coordinate trigger swaps the map theme
|
||||
-- for Music_MeetJessieJames, the motto plays, the duo closes in, one
|
||||
-- battle against the shared OPP_ROCKET party fights them both, and after
|
||||
-- their parting lines they vanish together under a second sting of the
|
||||
-- theme before the map theme resumes (PlayDefaultMusic ->
|
||||
-- play_default_music).
|
||||
--
|
||||
-- Registered on top of the shared tables by data/scripts/init.lua on a
|
||||
-- Yellow boot; MT_MOON_B2F's onStep chains story2's Super Nerd / fossil
|
||||
-- trigger and SILPH_CO_11F's chains story's Giovanni trigger, since
|
||||
-- non-talk hooks replace rather than merge.
|
||||
|
||||
local M = {}
|
||||
|
||||
-- Capture the FUNCTION, not the table: attachBase stores the module
|
||||
-- table itself, so once this file's onStep is attached the table's slot
|
||||
-- points back here -- delegating through the table would self-recurse.
|
||||
local baseMtMoonStep = require("data.scripts.story2").MT_MOON_B2F.onStep
|
||||
local baseSilph11Step = require("data.scripts.story").SILPH_CO_11F.onStep
|
||||
|
||||
M.MT_MOON_B2F = {
|
||||
talk = {
|
||||
TEXT_MTMOONB2F_JESSIE = {
|
||||
{ "face_player" }, { "show_text", "_MtMoonJessieJamesText1" },
|
||||
},
|
||||
TEXT_MTMOONB2F_JAMES = {
|
||||
{ "face_player" }, { "show_text", "_MtMoonJessieJamesText1" },
|
||||
},
|
||||
},
|
||||
|
||||
onStep = function(game, ow, x, y)
|
||||
if baseMtMoonStep and baseMtMoonStep(game, ow, x, y) then
|
||||
return true
|
||||
end
|
||||
local f = game.save.flags
|
||||
if x ~= 3 or y ~= 5 then return false end
|
||||
if f.EVENT_BEAT_MT_MOON_3_JESSIE_JAMES then return false end
|
||||
if not (f.EVENT_GOT_DOME_FOSSIL or f.EVENT_GOT_HELIX_FOSSIL) then
|
||||
return false
|
||||
end
|
||||
ow.runner:run({
|
||||
{ "stop_music" },
|
||||
{ "play_music", "Music_MeetJessieJames" },
|
||||
{ "show_object", "MT_MOON_B2F", "MTMOONB2F_JESSIE" },
|
||||
{ "show_object", "MT_MOON_B2F", "MTMOONB2F_JAMES" },
|
||||
{ "show_text", "_MtMoonJessieJamesText1" },
|
||||
{ "face_player_dir", "up" },
|
||||
{ "emote", "player", "shock", 30 },
|
||||
-- MtMoonB2FScript_49e15 simulates PAD_UP for one player step, then
|
||||
-- Script6/Script9 walk Jessie (object 2, MovementData_f9e65: six
|
||||
-- $06) and James (object 6, f9e66: five $06); both objects' movement
|
||||
-- byte 2 is LEFT, so .determineDirection makes those LEFT steps --
|
||||
-- Jessie (9,3)->(3,3) above the player at (3,4), James (9,4)->(4,4)
|
||||
-- beside him. #423
|
||||
{ "walk_npc", "player", { "up" } },
|
||||
{ "walk_npc", 2, { "left", "left", "left", "left", "left", "left" } },
|
||||
{ "face_object", 2, "down" },
|
||||
{ "walk_npc", 6, { "left", "left", "left", "left", "left" } },
|
||||
{ "face_object", 6, "left" },
|
||||
{ "show_text", "_MtMoonJessieJamesText2" },
|
||||
-- MtMoonB2FScript12 arms _MtMoonJessieJamesText3 with
|
||||
-- SaveEndBattleTextPointers before it sets wCurOpponent, so
|
||||
-- TrainerBattleVictory prints it on the battle screen as "ROCKET: A
|
||||
-- brat beat us?" between TrainerDefeatedText and MoneyForWinningText.
|
||||
-- Its one-word first line only reads right behind that tag (#866).
|
||||
{ "save_end_battle_text", "_MtMoonJessieJamesText3" },
|
||||
{ "start_battle", "trainer", "OPP_ROCKET", 42 },
|
||||
{ "check_battle_result", "win" },
|
||||
{ "jump_if_false", "end" },
|
||||
{ "show_text", "_MtMoonJessieJamesText4" },
|
||||
{ "stop_music" },
|
||||
{ "play_music", "Music_MeetJessieJames" },
|
||||
{ "fade", "out" },
|
||||
{ "hide_object", "MT_MOON_B2F", "MTMOONB2F_JESSIE" },
|
||||
{ "hide_object", "MT_MOON_B2F", "MTMOONB2F_JAMES" },
|
||||
{ "fade", "in" },
|
||||
{ "play_default_music" },
|
||||
{ "set_flag", "EVENT_BEAT_MT_MOON_3_JESSIE_JAMES" },
|
||||
}, {})
|
||||
return true
|
||||
end,
|
||||
}
|
||||
|
||||
-- -------------------------------------------------------------------
|
||||
-- Rocket Hideout B4F (RocketHideoutB4FScript_455a5..Script13): the
|
||||
-- motto plays from off-screen FIRST, then the duo pops in at (25,10) /
|
||||
-- (24,10) and whichever of them shares the player's column ($18=24 or
|
||||
-- $19=25, EVENT_ROCKET_HIDEOUT_4_JESSIE_JAMES_ON_LEFT) walks the three
|
||||
-- tiles down to loom over the player while the other walks four and ends
|
||||
-- up beside him. A loss
|
||||
-- re-hides them (RocketHideoutB4FResetScripts via EVENT_6A0), so the
|
||||
-- trigger re-arms clean.
|
||||
-- -------------------------------------------------------------------
|
||||
|
||||
M.ROCKET_HIDEOUT_B4F = {
|
||||
talk = {
|
||||
TEXT_ROCKETHIDEOUTB4F_JESSIE = {
|
||||
{ "face_player" }, { "show_text", "_RocketHideoutJessieJamesText1" },
|
||||
},
|
||||
TEXT_ROCKETHIDEOUTB4F_JAMES = {
|
||||
{ "face_player" }, { "show_text", "_RocketHideoutJessieJamesText1" },
|
||||
},
|
||||
},
|
||||
|
||||
onStep = function(game, ow, x, y)
|
||||
local f = game.save.flags
|
||||
if y ~= 14 or (x ~= 24 and x ~= 25) then return false end
|
||||
if f.EVENT_BEAT_ROCKET_HIDEOUT_4_JESSIE_JAMES then return false end
|
||||
-- ON_LEFT: player under James's column (25); movement data pairs
|
||||
-- RocketHideoutB4FJessieJamesMovementData_45605/45606 swap so the
|
||||
-- column-mate walks 3, the other 4.
|
||||
local onLeft = (x == 25)
|
||||
ow.runner:run({
|
||||
{ "stop_music" },
|
||||
{ "play_music", "Music_MeetJessieJames" },
|
||||
{ "show_text", "_RocketHideoutJessieJamesText1" },
|
||||
{ "face_player_dir", "up" },
|
||||
{ "emote", "player", "shock", 30 },
|
||||
{ "show_object", "ROCKET_HIDEOUT_B4F", "ROCKETHIDEOUTB4F_JAMES" },
|
||||
{ "show_object", "ROCKET_HIDEOUT_B4F", "ROCKETHIDEOUTB4F_JESSIE" },
|
||||
-- James (object 2) then Jessie (object 3), Script4..Script9 order.
|
||||
-- RocketHideoutB4FJessieJamesMovementData_45605 is a lone $4 that FALLS
|
||||
-- THROUGH into _45606 ($4 $4 $4 $ff), so MoveSprite_ (home/pathfinding.asm)
|
||||
-- reads _45605 as FOUR steps and _45606 as three; $4 is DOWN in Yellow's
|
||||
-- Func_5288 lookup (engine/overworld/movement.asm), which walks with no
|
||||
-- collision test. From (25,10)/(24,10) against a player on y=14 the
|
||||
-- column-mate stops three down, right above him, and the other walks the
|
||||
-- full four to stand alongside -- which is what the facings below assume.
|
||||
-- Reading _45605 as a single step stranded whoever was off-column three
|
||||
-- tiles away, so James never reached the player (#865).
|
||||
{ "walk_npc", 2, onLeft and { "down", "down", "down" }
|
||||
or { "down", "down", "down", "down" } },
|
||||
{ "face_object", 2, onLeft and "down" or "left" },
|
||||
{ "walk_npc", 3, onLeft and { "down", "down", "down", "down" }
|
||||
or { "down", "down", "down" } },
|
||||
{ "face_object", 3, onLeft and "right" or "down" },
|
||||
{ "show_text", "_RocketHideoutJessieJamesText2" },
|
||||
-- RocketHideoutB4FScript10 saves _RocketHideoutJessieJamesText3 as the
|
||||
-- end-battle text, so it prints as "ROCKET: Such a dreadful twerp!" on
|
||||
-- the battle screen ahead of MoneyForWinningText (#866).
|
||||
{ "save_end_battle_text", "_RocketHideoutJessieJamesText3" },
|
||||
{ "start_battle", "trainer", "OPP_ROCKET", 43 },
|
||||
{ "check_battle_result", "win" },
|
||||
{ "jump_if_false", "lost" },
|
||||
{ "show_text", "_RocketHideoutJessieJamesText4" },
|
||||
{ "stop_music" },
|
||||
{ "play_music", "Music_MeetJessieJames" },
|
||||
{ "fade", "out" },
|
||||
{ "hide_object", "ROCKET_HIDEOUT_B4F", "ROCKETHIDEOUTB4F_JAMES" },
|
||||
{ "hide_object", "ROCKET_HIDEOUT_B4F", "ROCKETHIDEOUTB4F_JESSIE" },
|
||||
{ "fade", "in" },
|
||||
{ "play_default_music" },
|
||||
{ "set_flag", "EVENT_BEAT_ROCKET_HIDEOUT_4_JESSIE_JAMES" },
|
||||
{ "jump", "end" },
|
||||
{ "label", "lost" },
|
||||
{ "hide_object", "ROCKET_HIDEOUT_B4F", "ROCKETHIDEOUTB4F_JAMES" },
|
||||
{ "hide_object", "ROCKET_HIDEOUT_B4F", "ROCKETHIDEOUTB4F_JESSIE" },
|
||||
}, {})
|
||||
return true
|
||||
end,
|
||||
}
|
||||
|
||||
-- -------------------------------------------------------------------
|
||||
-- Pokemon Tower 7F (PokemonTower7FScript_60d2a..Script10): same beat
|
||||
-- one floor below Fuji, except the duo pops in BEFORE the motto and
|
||||
-- Jessie ($a=10 column) leads the walk-down; ON_LEFT ($b=11) hands the
|
||||
-- three-tile walk to James instead. On a loss vanilla only resets the
|
||||
-- script counter (the blackout warp reloads the map anyway).
|
||||
-- -------------------------------------------------------------------
|
||||
|
||||
M.POKEMON_TOWER_7F = {
|
||||
talk = {
|
||||
TEXT_POKEMONTOWER7F_JESSIE = {
|
||||
{ "face_player" }, { "show_text", "_PokemonTowerJessieJamesText1" },
|
||||
},
|
||||
TEXT_POKEMONTOWER7F_JAMES = {
|
||||
{ "face_player" }, { "show_text", "_PokemonTowerJessieJamesText1" },
|
||||
},
|
||||
},
|
||||
|
||||
onStep = function(game, ow, x, y)
|
||||
local f = game.save.flags
|
||||
if y ~= 12 or (x ~= 10 and x ~= 11) then return false end
|
||||
if f.EVENT_BEAT_POKEMONTOWER_7_JESSIE_JAMES then return false end
|
||||
local onLeft = (x == 11) -- EVENT_POKEMONTOWER_7_JESSIE_JAMES_ON_LEFT
|
||||
ow.runner:run({
|
||||
{ "stop_music" },
|
||||
{ "play_music", "Music_MeetJessieJames" },
|
||||
{ "show_object", "POKEMON_TOWER_7F", "POKEMONTOWER7F_JESSIE" },
|
||||
{ "show_object", "POKEMON_TOWER_7F", "POKEMONTOWER7F_JAMES" },
|
||||
{ "show_text", "_PokemonTowerJessieJamesText1" },
|
||||
{ "face_player_dir", "up" },
|
||||
{ "emote", "player", "shock", 30 },
|
||||
-- Jessie (object 1) then James (object 2), Script1..Script6 order.
|
||||
-- Same fall-through blob as the hideout: PokemonTower7FMovementData_60d7a
|
||||
-- is a lone $4 running into _60d7b ($4 $4 $4 $FF), so _60d7a is FOUR
|
||||
-- steps and _60d7b is three. From (10,8)/(11,8) against a player on
|
||||
-- y=12 the column-mate halts one tile above him and the other closes the
|
||||
-- full four to his side; the single-step reading is why James only
|
||||
-- "moved a bit" here (#865).
|
||||
{ "walk_npc", 1, onLeft and { "down", "down", "down", "down" }
|
||||
or { "down", "down", "down" } },
|
||||
{ "face_object", 1, onLeft and "right" or "down" },
|
||||
{ "walk_npc", 2, onLeft and { "down", "down", "down" }
|
||||
or { "down", "down", "down", "down" } },
|
||||
{ "face_object", 2, onLeft and "down" or "left" },
|
||||
{ "show_text", "_PokemonTowerJessieJamesText2" },
|
||||
-- PokemonTower7FScript7 saves _PokemonTowerJessieJamesText3 as the
|
||||
-- end-battle text: "ROCKET: You will regret this!" on the battle screen,
|
||||
-- before the prize money (#866).
|
||||
{ "save_end_battle_text", "_PokemonTowerJessieJamesText3" },
|
||||
{ "start_battle", "trainer", "OPP_ROCKET", 44 },
|
||||
{ "check_battle_result", "win" },
|
||||
{ "jump_if_false", "end" },
|
||||
{ "show_text", "_PokemonTowerJessieJamesText4" },
|
||||
{ "stop_music" },
|
||||
{ "play_music", "Music_MeetJessieJames" },
|
||||
{ "fade", "out" },
|
||||
{ "hide_object", "POKEMON_TOWER_7F", "POKEMONTOWER7F_JESSIE" },
|
||||
{ "hide_object", "POKEMON_TOWER_7F", "POKEMONTOWER7F_JAMES" },
|
||||
{ "fade", "in" },
|
||||
{ "play_default_music" },
|
||||
{ "set_flag", "EVENT_BEAT_POKEMONTOWER_7_JESSIE_JAMES" },
|
||||
}, {})
|
||||
return true
|
||||
end,
|
||||
}
|
||||
|
||||
-- -------------------------------------------------------------------
|
||||
-- Silph Co 11F (SilphCo11FScript_6229c..Script14): the only site where
|
||||
-- the duo starts VISIBLE (toggleable_objects.asm keeps SILPHCO11F_JAMES
|
||||
-- / _JESSIE ON), flanking Giovanni at (2,8)/(3,8), so they are talkable
|
||||
-- before the ambush (SilphCo11FJessieJamesText = the full motto). The
|
||||
-- trigger is the top row (y=3, x<4); EVENT_780/EVENT_781 pick one of
|
||||
-- three approach paths (SilphCo11FMovementData_622f5..62311, $5=up
|
||||
-- $6=left) that route James then Jessie up to the player without ever
|
||||
-- crossing the player's tile. Their duo text lives in
|
||||
-- text/SilphCo10F.asm (_SilphCoJessieJamesText*).
|
||||
-- -------------------------------------------------------------------
|
||||
|
||||
M.SILPH_CO_11F = {
|
||||
talk = {
|
||||
TEXT_SILPHCO11F_JESSIE = {
|
||||
{ "face_player" }, { "show_text", "_SilphCoJessieJamesText1" },
|
||||
},
|
||||
TEXT_SILPHCO11F_JAMES = {
|
||||
{ "face_player" }, { "show_text", "_SilphCoJessieJamesText1" },
|
||||
},
|
||||
},
|
||||
|
||||
onStep = function(game, ow, x, y)
|
||||
if baseSilph11Step and baseSilph11Step(game, ow, x, y) then
|
||||
return true
|
||||
end
|
||||
local f = game.save.flags
|
||||
if y ~= 3 or x > 3 then return false end
|
||||
if f.EVENT_BEAT_SILPH_CO_11F_JESSIE_JAMES then return false end
|
||||
-- x==3 -> base path, x==2 -> EVENT_780 variant, x<=1 -> EVENT_781
|
||||
local jamesDirs, jamesFace, jessieDirs, jessieFace
|
||||
if x == 3 then
|
||||
jamesDirs, jamesFace = { "up", "up", "up", "up", "up" }, "right"
|
||||
jessieDirs, jessieFace = { "up", "up", "up", "up" }, "up"
|
||||
elseif x == 2 then
|
||||
jamesDirs, jamesFace = { "up", "up", "up", "up" }, "up"
|
||||
jessieDirs, jessieFace = { "up", "up", "up", "up", "up" }, "left"
|
||||
else
|
||||
jamesDirs = { "up", "up", "left", "up", "up" }
|
||||
jamesFace = "up"
|
||||
jessieDirs = { "up", "up", "up", "left", "up", "up" }
|
||||
jessieFace = "left"
|
||||
end
|
||||
ow.runner:run({
|
||||
{ "stop_music" },
|
||||
{ "play_music", "Music_MeetJessieJames" },
|
||||
{ "show_text", "_SilphCoJessieJamesText1" },
|
||||
{ "face_player_dir", "down" },
|
||||
{ "emote", "player", "shock", 30 },
|
||||
-- James (object 4) then Jessie (object 6), Script5..Script10 order
|
||||
{ "walk_npc", 4, jamesDirs },
|
||||
{ "face_object", 4, jamesFace },
|
||||
{ "walk_npc", 6, jessieDirs },
|
||||
{ "face_object", 6, jessieFace },
|
||||
{ "show_text", "_SilphCoJessieJamesText2" },
|
||||
-- SilphCo11FScript11 saves _SilphCoJessieJamesText3 (SilphCo11FText_624c2)
|
||||
-- as the end-battle text: "ROCKET: Like always..." before the money (#866).
|
||||
{ "save_end_battle_text", "_SilphCoJessieJamesText3" },
|
||||
{ "start_battle", "trainer", "OPP_ROCKET", 45 },
|
||||
{ "check_battle_result", "win" },
|
||||
{ "jump_if_false", "end" },
|
||||
{ "show_text", "_SilphCoJessieJamesText4" },
|
||||
{ "stop_music" },
|
||||
{ "play_music", "Music_MeetJessieJames" },
|
||||
{ "fade", "out" },
|
||||
{ "hide_object", "SILPH_CO_11F", "SILPHCO11F_JAMES" },
|
||||
{ "hide_object", "SILPH_CO_11F", "SILPHCO11F_JESSIE" },
|
||||
{ "fade", "in" },
|
||||
{ "play_default_music" },
|
||||
{ "set_flag", "EVENT_BEAT_SILPH_CO_11F_JESSIE_JAMES" },
|
||||
}, {})
|
||||
return true
|
||||
end,
|
||||
}
|
||||
|
||||
return M
|
||||