Compare commits
817 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 74e04cb086 | |||
| 25ec896545 | |||
| 8240205254 | |||
| 55616fc03d | |||
| 675971068e | |||
| cfa8406306 | |||
| 0ea224d5db | |||
| c2e1db0f89 | |||
| 7b1e796c48 | |||
| 4c13770e70 | |||
| cf45cbbf92 | |||
| 4e1ab1879b | |||
| 917735a41c | |||
| c877ea80a7 | |||
| 7d1ddf9b7c | |||
| faf82c2cec | |||
| 5bc6036735 | |||
| 809628f0fa | |||
| 4817ff8bf9 | |||
| 4f8739c029 | |||
| c655217120 | |||
| fbdfc1c053 | |||
| 72c244b433 | |||
| e0e030003b | |||
| ce2afb83f1 | |||
| 28f741f72f | |||
| 22bcd95da1 | |||
| ea28f886f3 | |||
| 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 | |||
| 6fb5aa08ff | |||
| ba8aa7b263 | |||
| 243cb6a477 | |||
| c987fefede | |||
| 2a038f7112 | |||
| 548e75a15e | |||
| 339c85b590 | |||
| 0f0420314b | |||
| a08b5e65f7 | |||
| 99b89c4e5e | |||
| 20586e0c48 | |||
| b54d0c605a | |||
| 2f75af902c | |||
| 9147a6413e | |||
| 9a467d4bf1 | |||
| e40f9337b6 | |||
| 1f0712d2d5 | |||
| 345bc9519a | |||
| 0b30bb2eff | |||
| f0e88aa581 | |||
| 317893bd61 | |||
| bd181271d9 | |||
| 4df09010a8 | |||
| 1a5b2b96dc | |||
| 43d89813fc | |||
| 5bc76ce6fe | |||
| 1fe35335a5 | |||
| 8654444d1a | |||
| 772c39edb8 | |||
| 74e7474349 | |||
| 3aefe41ea1 | |||
| 2223c31e93 | |||
| 568f0fa9eb | |||
| e91cf0f0c2 | |||
| 8a1f583d88 | |||
| 7518319c80 | |||
| 304df44742 | |||
| e92bab6f4f | |||
| 43755c5457 | |||
| d4455c508f | |||
| 7d01242cd1 | |||
| 9d77189cec | |||
| c8adbdcc64 | |||
| ac6dfe7134 | |||
| b1ad7c7254 | |||
| 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 |
@@ -22,12 +22,14 @@ body:
|
|||||||
id: game
|
id: game
|
||||||
attributes:
|
attributes:
|
||||||
label: Which game were you playing
|
label: Which game were you playing
|
||||||
description: Pick every version you saw the bug in.
|
description: Pick every version you saw the bug in. Use N/A if it isn't game-specific.
|
||||||
multiple: true
|
multiple: true
|
||||||
options:
|
options:
|
||||||
- Red
|
- Red
|
||||||
- Blue
|
- Blue
|
||||||
- Yellow
|
- Yellow
|
||||||
|
- Gold
|
||||||
|
- N/A
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
@@ -35,12 +37,16 @@ body:
|
|||||||
id: os
|
id: os
|
||||||
attributes:
|
attributes:
|
||||||
label: Which build are you running
|
label: Which build are you running
|
||||||
|
description: Official release targets. Pick Multiple platforms if you saw it on more than one.
|
||||||
options:
|
options:
|
||||||
- macOS
|
- macOS
|
||||||
- Windows
|
- Windows
|
||||||
- Linux
|
- Linux
|
||||||
- Android
|
- Android
|
||||||
- iOS
|
- iOS
|
||||||
|
- Nintendo Switch
|
||||||
|
- Xbox
|
||||||
|
- Anbernic RG34XXSP
|
||||||
- Multiple platforms
|
- Multiple platforms
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|||||||
@@ -30,13 +30,14 @@ body:
|
|||||||
id: game
|
id: game
|
||||||
attributes:
|
attributes:
|
||||||
label: Which game is this about
|
label: Which game is this about
|
||||||
description: Pick every version it applies to.
|
description: Pick every version it applies to. Use N/A if it isn't game-specific.
|
||||||
multiple: true
|
multiple: true
|
||||||
options:
|
options:
|
||||||
- Red
|
- Red
|
||||||
- Blue
|
- Blue
|
||||||
- Yellow
|
- Yellow
|
||||||
- Not version-specific
|
- Gold
|
||||||
|
- N/A
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
|
|||||||
@@ -27,13 +27,14 @@ body:
|
|||||||
id: game
|
id: game
|
||||||
attributes:
|
attributes:
|
||||||
label: Which game is this for
|
label: Which game is this for
|
||||||
description: Pick every version the mod should cover.
|
description: Pick every version the mod should cover. Use N/A if it isn't game-specific.
|
||||||
multiple: true
|
multiple: true
|
||||||
options:
|
options:
|
||||||
- Red
|
- Red
|
||||||
- Blue
|
- Blue
|
||||||
- Yellow
|
- Yellow
|
||||||
- Not version-specific
|
- Gold
|
||||||
|
- N/A
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
# The T3 content tier asserts Pokemon Red facts; scripts/test.sh detects
|
||||||
# data/generated/ is absent and skips it rather than failing.
|
# data/generated/ is absent and skips it rather than failing.
|
||||||
#
|
#
|
||||||
# Runs alongside release.yml, which is untouched by this file.
|
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
# Integration branch + release branch. PRs already run via pull_request
|
# Integration branch + release branch. PRs already run via pull_request
|
||||||
@@ -24,12 +22,363 @@ concurrency:
|
|||||||
group: ci-${{ github.ref }}
|
group: ci-${{ github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
jobs:
|
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|cache_fs_gold_nx_load)_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
|
||||||
|
luajit tests/engine/cache_fs_gold_nx_load_test.lua
|
||||||
|
luajit tests/engine/cache_fs_blue_mount_test.lua
|
||||||
|
luajit tests/engine/switch_diagnostics_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:
|
headless:
|
||||||
name: headless suites (no ROM)
|
name: headless suites (no ROM)
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v7
|
||||||
|
|
||||||
# LuaJIT, not lua5.4: LOVE 11.x embeds LuaJIT 2.1 and the engine is
|
# 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
|
# written to Lua 5.1 semantics, so CI must run the interpreter the
|
||||||
@@ -37,6 +386,9 @@ jobs:
|
|||||||
- name: install luajit
|
- name: install luajit
|
||||||
run: sudo apt-get update && sudo apt-get install -y 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
|
- name: interpreter version
|
||||||
run: luajit -v
|
run: luajit -v
|
||||||
|
|
||||||
@@ -47,7 +399,7 @@ jobs:
|
|||||||
name: fixture dataset integrity
|
name: fixture dataset integrity
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v7
|
||||||
- run: sudo apt-get update && sudo apt-get install -y luajit
|
- run: sudo apt-get update && sudo apt-get install -y luajit
|
||||||
- run: python3 -m pip install --upgrade pillow
|
- run: python3 -m pip install --upgrade pillow
|
||||||
|
|
||||||
@@ -89,7 +441,7 @@ jobs:
|
|||||||
name: screenshot differ (capture not yet wired)
|
name: screenshot differ (capture not yet wired)
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v7
|
||||||
- run: python3 -m pip install --upgrade pillow
|
- run: python3 -m pip install --upgrade pillow
|
||||||
|
|
||||||
# 21-testing-and-ci §"Testing & acceptance criteria": compare_shots
|
# 21-testing-and-ci §"Testing & acceptance criteria": compare_shots
|
||||||
@@ -137,7 +489,7 @@ jobs:
|
|||||||
name: mod lint (no ROM-derived content)
|
name: mod lint (no ROM-derived content)
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v7
|
||||||
|
|
||||||
# the MK305 dump check key-diffs shipped tables through luajit, and
|
# the MK305 dump check key-diffs shipped tables through luajit, and
|
||||||
# modkit treats a missing interpreter as a fatal MK100 -- without
|
# 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
|
name: Release
|
||||||
|
|
||||||
# Builds the macOS, Windows, and Linux desktop apps, an Android APK, an iOS
|
# Builds the macOS, Windows, and Linux desktop apps, an Android APK, an iOS
|
||||||
# IPA, and the Anbernic RG34XXSP (Stock OS 64-bit MOD / PortMaster) port on
|
# IPA, a Nintendo Switch SD-ready zip (experimental), Xbox UWP, the Anbernic
|
||||||
# the self-hosted Mac runner, and publishes them as a GitHub Release.
|
# 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:
|
# Versioning:
|
||||||
# - First ever release is 0.1.0.
|
# - First ever release is 0.1.0.
|
||||||
@@ -24,6 +26,7 @@ on:
|
|||||||
paths-ignore:
|
paths-ignore:
|
||||||
- '.github/**'
|
- '.github/**'
|
||||||
- '**.md'
|
- '**.md'
|
||||||
|
- 'mobile/ios/app-repo.json'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
version:
|
version:
|
||||||
@@ -41,22 +44,17 @@ concurrency:
|
|||||||
cancel-in-progress: false
|
cancel-in-progress: false
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
version:
|
||||||
runs-on: [self-hosted, macOS]
|
name: determine release version
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
outputs:
|
||||||
|
version: ${{ steps.ver.outputs.version }}
|
||||||
|
tag: ${{ steps.ver.outputs.tag }}
|
||||||
steps:
|
steps:
|
||||||
# The self-hosted runner lives under the machine owner's home
|
- uses: actions/checkout@v7
|
||||||
# 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@v4
|
|
||||||
with:
|
with:
|
||||||
fetch-depth: 0
|
fetch-depth: 0
|
||||||
fetch-tags: true
|
fetch-tags: true
|
||||||
|
|
||||||
- name: Determine version
|
- name: Determine version
|
||||||
id: ver
|
id: ver
|
||||||
env:
|
env:
|
||||||
@@ -64,7 +62,6 @@ jobs:
|
|||||||
GH_TOKEN: ${{ github.token }}
|
GH_TOKEN: ${{ github.token }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
semver_re='^[0-9]+\.[0-9]+\.[0-9]+$'
|
semver_re='^[0-9]+\.[0-9]+\.[0-9]+$'
|
||||||
|
|
||||||
# 1) Explicit override from a manual run.
|
# 1) Explicit override from a manual run.
|
||||||
@@ -94,7 +91,6 @@ jobs:
|
|||||||
| grep -E "$semver_re" \
|
| grep -E "$semver_re" \
|
||||||
| sort -t. -k1,1n -k2,2n -k3,3n \
|
| sort -t. -k1,1n -k2,2n -k3,3n \
|
||||||
| tail -1 || true)"
|
| tail -1 || true)"
|
||||||
|
|
||||||
if [ -z "$latest" ]; then
|
if [ -z "$latest" ]; then
|
||||||
version="0.1.0"
|
version="0.1.0"
|
||||||
echo "No existing release tag; starting at $version"
|
echo "No existing release tag; starting at $version"
|
||||||
@@ -122,11 +118,197 @@ jobs:
|
|||||||
echo "::error::Release $tag already exists. Pick a different version."
|
echo "::error::Release $tag already exists. Pick a different version."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
echo "version=$version" >> "$GITHUB_OUTPUT"
|
echo "version=$version" >> "$GITHUB_OUTPUT"
|
||||||
echo "tag=$tag" >> "$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
|
- name: Import signing certificate into a temporary keychain
|
||||||
|
if: github.repository == 'bryanthaboi/gen1recomp'
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
KEYCHAIN_PATH="$RUNNER_TEMP/pokemon-signing.keychain-db"
|
KEYCHAIN_PATH="$RUNNER_TEMP/pokemon-signing.keychain-db"
|
||||||
@@ -163,18 +345,26 @@ jobs:
|
|||||||
security find-identity -v -p codesigning "$KEYCHAIN_PATH"
|
security find-identity -v -p codesigning "$KEYCHAIN_PATH"
|
||||||
|
|
||||||
- name: Build macOS + Windows + Linux
|
- name: Build macOS + Windows + Linux
|
||||||
|
env:
|
||||||
|
GEN1TLS_DLL: ${{ github.workspace }}/dist/native/win-x64/gen1tls.dll
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
# Sign in-build (identity auto-detected from the temp keychain);
|
# Sign in-build (identity auto-detected from the temp keychain);
|
||||||
# notarize separately below so it uses secret credentials, not a
|
# notarize separately below so it uses secret credentials, not a
|
||||||
# login-keychain profile. "all" also builds the Linux AppImage,
|
# login-keychain profile. "all" also builds the Linux AppImage,
|
||||||
# which needs no signing/notarization.
|
# 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
|
- name: Build Android
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
scripts/build_android.sh --version "${{ steps.ver.outputs.version }}"
|
scripts/build_android.sh --version "${{ needs.version.outputs.version }}"
|
||||||
|
|
||||||
- name: Install xcbeautify
|
- name: Install xcbeautify
|
||||||
run: |
|
run: |
|
||||||
@@ -182,22 +372,52 @@ jobs:
|
|||||||
brew list xcbeautify >/dev/null 2>&1 || brew install xcbeautify
|
brew list xcbeautify >/dev/null 2>&1 || brew install xcbeautify
|
||||||
|
|
||||||
- name: Build iOS
|
- name: Build iOS
|
||||||
|
env:
|
||||||
|
CANONICAL_REPOSITORY: ${{ github.repository == 'bryanthaboi/gen1recomp' }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
# Device Release IPA; signs with the Apple Development identity on
|
if [ "$CANONICAL_REPOSITORY" = true ]; then
|
||||||
# the runner (auto team detection). Users on other Apple IDs still
|
scripts/build_ios.sh --fetch --device --release \
|
||||||
# re-sign or build via docs/ios-install.md.
|
--version "${{ needs.version.outputs.version }}"
|
||||||
scripts/build_ios.sh --fetch --device --release \
|
else
|
||||||
--version "${{ steps.ver.outputs.version }}"
|
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
|
- name: Build Anbernic RG34XXSP port
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
# Self-contained aarch64 PortMaster-style pack; pulls the LÖVE 11.5
|
# Self-contained aarch64 PortMaster-style pack; pulls the LÖVE 11.5
|
||||||
# runtime from PortMaster-GUI, so it needs no signing/notarization.
|
# 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
|
- name: Notarize & staple macOS app
|
||||||
|
if: github.repository == 'bryanthaboi/gen1recomp'
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
ci_dir="${POKEMON_CI_DIR:-$HOME/.config/pokemon-ci}"
|
ci_dir="${POKEMON_CI_DIR:-$HOME/.config/pokemon-ci}"
|
||||||
@@ -231,24 +451,58 @@ jobs:
|
|||||||
ditto -c -k --sequesterRsrc --keepParent "$app" "$zip"
|
ditto -c -k --sequesterRsrc --keepParent "$app" "$zip"
|
||||||
echo "Notarized + stapled ✓"
|
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
|
- name: Stage release assets
|
||||||
|
if: github.repository == 'bryanthaboi/gen1recomp'
|
||||||
id: assets
|
id: assets
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
v="${{ steps.ver.outputs.version }}"
|
v="${{ needs.version.outputs.version }}"
|
||||||
outdir="dist/release"
|
outdir="dist/release"
|
||||||
rm -rf "$outdir"
|
rm -rf "$outdir"
|
||||||
mkdir -p "$outdir"
|
mkdir -p "$outdir"
|
||||||
cp "dist/mac/gen1recomp-macos.zip" "$outdir/gen1recomp-${v}-macos.zip"
|
cp "dist/mac/gen1recomp-macos.zip" "$outdir/gen1recomp-${v}-macos.zip"
|
||||||
cp "dist/win/gen1recomp-win64.zip" "$outdir/gen1recomp-${v}-windows.zip"
|
cp "dist/win/gen1recomp-win64.zip" "$outdir/gen1recomp-${v}-windows.zip"
|
||||||
cp "dist/linux/gen1recomp-linux.zip" "$outdir/gen1recomp-${v}-linux.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)"
|
apk="$(find dist/android/debug -name '*.apk' | head -1)"
|
||||||
[ -n "$apk" ] || { echo "::error::no Android APK found under dist/android/debug"; exit 1; }
|
[ -n "$apk" ] || { echo "::error::no Android APK found under dist/android/debug"; exit 1; }
|
||||||
cp "$apk" "$outdir/gen1recomp-${v}-android.apk"
|
cp "$apk" "$outdir/gen1recomp-${v}-android.apk"
|
||||||
|
|
||||||
ipa="dist/ios/gen1recomp.ipa"
|
ipa="dist/ios/gen1recomp++.ipa"
|
||||||
[ -f "$ipa" ] || { echo "::error::$ipa not found (expected from scripts/build_ios.sh --device)"; exit 1; }
|
[ -f "$ipa" ] || { echo "::error::$ipa not found (expected from scripts/build_ios.sh --device)"; exit 1; }
|
||||||
cp "$ipa" "$outdir/gen1recomp-${v}-ios.ipa"
|
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
|
# Anbernic handheld port (suffix names the CFW it targets, so a
|
||||||
# future RG35XX/other-CFW pack can ship alongside it).
|
# future RG35XX/other-CFW pack can ship alongside it).
|
||||||
@@ -256,6 +510,11 @@ jobs:
|
|||||||
[ -f "$rg34" ] || { echo "::error::$rg34 not found (expected from ./build-rg34xxsp.sh)"; exit 1; }
|
[ -f "$rg34" ] || { echo "::error::$rg34 not found (expected from ./build-rg34xxsp.sh)"; exit 1; }
|
||||||
cp "$rg34" "$outdir/gen1recomp-${v}-rg34xxsp-stockos64-mod.zip"
|
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
|
# Platform-independent update payload, built alongside the desktop
|
||||||
# apps above (same game.love that gets fused into each of them).
|
# apps above (same game.love that gets fused into each of them).
|
||||||
love_file=".bazinga/work/game.love"
|
love_file=".bazinga/work/game.love"
|
||||||
@@ -271,12 +530,13 @@ jobs:
|
|||||||
cat "$outdir/sha256sums.txt"
|
cat "$outdir/sha256sums.txt"
|
||||||
|
|
||||||
- name: Publish GitHub Release
|
- name: Publish GitHub Release
|
||||||
|
if: github.repository == 'bryanthaboi/gen1recomp'
|
||||||
env:
|
env:
|
||||||
GH_TOKEN: ${{ github.token }}
|
GH_TOKEN: ${{ github.token }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
v="${{ steps.ver.outputs.version }}"
|
v="${{ needs.version.outputs.version }}"
|
||||||
tag="${{ steps.ver.outputs.tag }}"
|
tag="${{ needs.version.outputs.tag }}"
|
||||||
|
|
||||||
# Issues this release closes. Three sources, deduped by number:
|
# Issues this release closes. Three sources, deduped by number:
|
||||||
# 1. GitHub's own "closing issues" links on every PR whose
|
# 1. GitHub's own "closing issues" links on every PR whose
|
||||||
@@ -360,22 +620,96 @@ jobs:
|
|||||||
fi
|
fi
|
||||||
printf 'Release notes:\n%s\n' "$notes"
|
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" \
|
gh release create "$tag" \
|
||||||
--target "$GITHUB_SHA" \
|
--target "$GITHUB_SHA" \
|
||||||
--title "$v" \
|
--title "$v" \
|
||||||
--notes "$notes" \
|
--notes "$notes" \
|
||||||
"dist/release/gen1recomp-${v}-macos.zip" \
|
"${release_files[@]}"
|
||||||
"dist/release/gen1recomp-${v}-windows.zip" \
|
|
||||||
"dist/release/gen1recomp-${v}-linux.zip" \
|
|
||||||
"dist/release/gen1recomp-${v}-android.apk" \
|
|
||||||
"dist/release/gen1recomp-${v}-ios.ipa" \
|
|
||||||
"dist/release/gen1recomp-${v}-rg34xxsp-stockos64-mod.zip" \
|
|
||||||
"dist/release/gen1recomp-${v}.love" \
|
|
||||||
"dist/release/sha256sums.txt"
|
|
||||||
|
|
||||||
echo "Published release $tag"
|
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
|
- name: Clean up signing keychain
|
||||||
if: always()
|
if: ${{ always() && github.repository == 'bryanthaboi/gen1recomp' }}
|
||||||
run: |
|
run: |
|
||||||
security delete-keychain "$RUNNER_TEMP/pokemon-signing.keychain-db" 2>/dev/null || true
|
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/
|
data/generated/
|
||||||
assets/generated/
|
assets/generated/
|
||||||
|
|
||||||
# LÖVE packages
|
# LÖVE packages & archives
|
||||||
*.love
|
*.love
|
||||||
|
*.zip
|
||||||
|
|
||||||
# Local saves (LÖVE writes to its save dir, but keep the repo clean anyway)
|
# Local saves (LÖVE writes to its save dir, but keep the repo clean anyway)
|
||||||
save/
|
save/
|
||||||
@@ -31,9 +32,29 @@ mobile/ios/love-src/
|
|||||||
mobile/ios/cache/
|
mobile/ios/cache/
|
||||||
mobile/ios/build/
|
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/
|
/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/)
|
# Legacy manual convenience-copy location (superseded by /dist/android/)
|
||||||
mobile/dist/
|
mobile/dist/
|
||||||
|
|
||||||
@@ -43,3 +64,24 @@ mobile/dist/
|
|||||||
|
|
||||||
# per-machine iOS bundle-id pin (see scripts/build_ios.sh)
|
# per-machine iOS bundle-id pin (see scripts/build_ios.sh)
|
||||||
mobile/ios/bundle_id.local
|
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,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 |
|
| `QUEST` | New story, NPCs, dialogue, cutscenes | content |
|
||||||
| `MECHANIC` | New or changed battle/field mechanics via hooks/effects | overhaul |
|
| `MECHANIC` | New or changed battle/field mechanics via hooks/effects | overhaul |
|
||||||
| `GRAPHICS` | Sprite / tileset / palette / font changes | content |
|
| `GRAPHICS` | Sprite / tileset / palette / font changes | content |
|
||||||
|
| `LANGUAGE` | A translation: `text`, `strings` and the glyphs it needs | content |
|
||||||
| `AUDIO` | Music, sfx, cries | content |
|
| `AUDIO` | Music, sfx, cries | content |
|
||||||
| `UI` | New or modified screens, menus, overlays | content / overhaul |
|
| `UI` | New or modified screens, menus, overlays | content / overhaul |
|
||||||
| `TOOL` | Dev/QoL utilities, overlays, inter-mod libraries | content |
|
| `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
|
keeps validating with the value it has shipped since before the taxonomy
|
||||||
existed.
|
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,
|
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
|
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
|
merge — only by tooling and the manager's detail pane — so an absent or
|
||||||
malformed card can never break a load.
|
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
|
distributed mod never carries ROM-derived bytes, not even in its preview
|
||||||
images.
|
images.
|
||||||
|
|
||||||
### 5. Tags
|
### 7. Tags
|
||||||
|
|
||||||
Lowercase kebab strings, open vocabulary. The showcase generator
|
Lowercase kebab strings, open vocabulary. The showcase generator
|
||||||
lowercases and de-dupes. A recommended starting set: `beginner`,
|
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:
|
and the generator runs clean:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
luajit tools/gen_registry_docs.lua # in-repo default
|
luajit tools/gen_registry_docs.lua # docs/modding/reference/registries.md
|
||||||
luajit tools/gen_registry_docs.lua ../project.wiki # the wiki checkout
|
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
|
With no argument it writes inside the repo, which is the copy `python3
|
||||||
written into a checkout of it, so they cannot drift from the engine.
|
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
|
### 5. Deprecation etiquette
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,13 @@ 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
|
behavior are hand-written Lua; game data and graphics are decoded from a ROM
|
||||||
supplied by the player.
|
supplied by the player.
|
||||||
|
|
||||||
|
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>
|
<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)
|
**SUPPORT / ANNOUNCEMENTS / MODS:** [Discord](https://bois.icu)
|
||||||
@@ -50,13 +57,14 @@ supplied by the player.
|
|||||||
|
|
||||||
|
|
||||||
This project does not include a ROM, emulate the Game Boy, transpile assembly,
|
This project does not include a ROM, emulate the Game Boy, transpile assembly,
|
||||||
or download a disassembly. A canonical US Poke Red, Blue, or Yellow ROM is the
|
or download a disassembly. A canonical US Poke Red, Blue, Yellow, or Gold ROM
|
||||||
only game content input.
|
is the only game content input.
|
||||||
|
|
||||||
The ROM is verified, used during import, and then released from memory. It is
|
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
|
not copied into the cache. Later launches load the private generated cache and
|
||||||
do not ask for the ROM again. Red, Blue, and Yellow can all be imported and
|
do not ask for the ROM again. Red, Blue, Yellow, and Gold can all be imported
|
||||||
played side by side.
|
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
|
## Quick Start
|
||||||
|
|
||||||
@@ -64,17 +72,31 @@ 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
|
`.gbc` file or drop it onto the window. Import takes a few seconds and the
|
||||||
game starts automatically.
|
game starts automatically.
|
||||||
|
|
||||||
Only the canonical 1 MiB US Red, Blue, and Yellow ROMs are accepted. The
|
Only the canonical US Red, Blue, Yellow (1 MiB), and Gold (2 MiB) ROMs are
|
||||||
importer verifies SHA-1 before creating any game data:
|
accepted. The importer verifies SHA-1 before creating any game data:
|
||||||
|
|
||||||
- Red: `ea9bcae617fdf159b045185467ae58b2e4a48b9a`
|
- Red: `ea9bcae617fdf159b045185467ae58b2e4a48b9a`
|
||||||
- Blue: `d7037c83e1ae5b39bde3c30787637ba1d4c48ce2`
|
- Blue: `d7037c83e1ae5b39bde3c30787637ba1d4c48ce2`
|
||||||
- Yellow: `cc7d03262ebfaf2f06772c1a480c7d9d5f4a38e1`
|
- Yellow: `cc7d03262ebfaf2f06772c1a480c7d9d5f4a38e1`
|
||||||
|
- Gold: `d8b8a3600a465308c9953dfa04f0081c05bdcb94`
|
||||||
|
|
||||||
The packaged app contains neither a ROM nor pre-extracted game data. Music,
|
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
|
sound effects, and cries are synthesized while the game runs from compact
|
||||||
audio channel programs copied out of the verified ROM.
|
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
|
## Controls
|
||||||
|
|
||||||
|
|
||||||
@@ -96,6 +118,7 @@ supported out of the box.
|
|||||||
| Key | What it does |
|
| Key | What it does |
|
||||||
| --------- | ---------------------------------------------------- |
|
| --------- | ---------------------------------------------------- |
|
||||||
| `-` / `=` | Zoom out / in (overworld; also mouse wheel) |
|
| `-` / `=` | Zoom out / in (overworld; also mouse wheel) |
|
||||||
|
| `1` | Cycle GAME SPEED up (controller: R2 faster, L2 slower) |
|
||||||
| `2` | Cycle COLORS |
|
| `2` | Cycle COLORS |
|
||||||
| `3` | Cycle TILT (free-roam overworld) |
|
| `3` | Cycle TILT (free-roam overworld) |
|
||||||
| `4` | Cycle ZOOM through every level (free-roam overworld) |
|
| `4` | Cycle ZOOM through every level (free-roam overworld) |
|
||||||
@@ -105,8 +128,8 @@ supported out of the box.
|
|||||||
| `F10` | Open / close the mod manager |
|
| `F10` | Open / close the mod manager |
|
||||||
|
|
||||||
|
|
||||||
COLORS, TILT, ZOOM, GBC FX, and VOID FILL are also in the Options menu
|
COLORS, TILT, ZOOM, GBC FX, GAME SPEED, and VOID FILL are also in the
|
||||||
and persist in `options.lua`.
|
Options menu and persist in `options.lua`.
|
||||||
|
|
||||||
### Low-end devices
|
### Low-end devices
|
||||||
|
|
||||||
@@ -188,13 +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
|
already written to either location is touched automatically, so copy files
|
||||||
over yourself if you want to carry existing progress across the switch.
|
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
|
## iOS
|
||||||
|
|
||||||
Every release ships `gen1recomp-*-ios.ipa`. Sideload it with AltStore
|
Every release ships `gen1recomp++-*-ios.ipa`. Sideload it with AltStore
|
||||||
(Windows or Mac) — see [docs/ios-sideload.md](docs/ios-sideload.md). To
|
(Windows or Mac) — see [docs/ios-sideload.md](docs/ios-sideload.md). To
|
||||||
build and install from source on a Mac instead, see
|
build and install from source on a Mac instead, see
|
||||||
[docs/ios-install.md](docs/ios-install.md).
|
[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
|
## Handhelds
|
||||||
|
|
||||||
A PortMaster-style port for the **Anbernic RG34XXSP** on Stock OS 64-bit MOD
|
A PortMaster-style port for the **Anbernic RG34XXSP** on Stock OS 64-bit MOD
|
||||||
@@ -202,6 +302,21 @@ ships with every release as `gen1recomp-*-rg34xxsp-stockos64-mod.zip`.
|
|||||||
Install steps, controls, and troubleshooting live in
|
Install steps, controls, and troubleshooting live in
|
||||||
[docs/anbernic-rg34xxsp.md](docs/anbernic-rg34xxsp.md).
|
[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
|
## Modding
|
||||||
|
|
||||||
The game ships a native mod platform: content registries, events and hooks,
|
The game ships a native mod platform: content registries, events and hooks,
|
||||||
@@ -239,7 +354,8 @@ request with real detail is one that can actually get built.
|
|||||||
- [Save editor](https://github.com/bryanthaboi/gen1recomp/wiki/Guide-Save-Editor)
|
- [Save editor](https://github.com/bryanthaboi/gen1recomp/wiki/Guide-Save-Editor)
|
||||||
— edit party, boxes, items, events, and Pokédex flags outside the game.
|
— edit party, boxes, items, events, and Pokédex flags outside the game.
|
||||||
- `docs/architecture.md` — runtime details;
|
- `docs/architecture.md` — runtime details;
|
||||||
`docs/behavior-porting-notes.md` — formula provenance.
|
`docs/behavior-porting-notes.md` — formula provenance;
|
||||||
|
`docs/link-security.md` — what link play defends against, and what it doesn't.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
# tools/save-editor is part of that payload: the launcher's Edit button on a
|
||||||
# save row opens it in-process (main.lua).
|
# save row opens it in-process (main.lua).
|
||||||
(cd "$ROOT" && zip -q -9 -r "$WORK/game-payload.zip" \
|
(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.json tools/rom_manifest_blue.json \
|
||||||
|
tools/rom_manifest_yellow.json tools/rom_manifest_gold.json \
|
||||||
-x '*.DS_Store' 'data/generated/*' 'assets/generated/*')
|
-x '*.DS_Store' 'data/generated/*' 'assets/generated/*')
|
||||||
if unzip -Z1 "$WORK/game-payload.zip" \
|
payload_list="$(unzip -Z1 "$WORK/game-payload.zip")"
|
||||||
| grep -Eq '^(data|assets)/generated/[^/]+|^(data|assets)/generated/.+/'; then
|
printf '%s\n' "$payload_list" \
|
||||||
fail "payload unexpectedly contains generated ROM data"
|
| grep -Eq '^(data|assets)/generated/[^/]+|^(data|assets)/generated/.+/' \
|
||||||
fi
|
&& 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"
|
unzip -q "$WORK/game-payload.zip" -d "$GAME_SRC"
|
||||||
rm -f "$WORK/game-payload.zip"
|
rm -f "$WORK/game-payload.zip"
|
||||||
|
|
||||||
|
|||||||
@@ -6,18 +6,30 @@ function love.conf(t)
|
|||||||
|
|
||||||
local editor = os.getenv("POKEPORT_EDITOR") == "1"
|
local editor = os.getenv("POKEPORT_EDITOR") == "1"
|
||||||
local developer = os.getenv("POKEPORT_DEV") == "1"
|
local developer = os.getenv("POKEPORT_DEV") == "1"
|
||||||
|
local companion = nil
|
||||||
if arg then
|
if arg then
|
||||||
for _, a in ipairs(arg) do
|
for _, a in ipairs(arg) do
|
||||||
if a == "--editor" then editor = true end
|
if a == "--editor" then editor = true end
|
||||||
if a == "--developer" then developer = 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
|
||||||
end
|
end
|
||||||
-- main.lua runs in the same Lua state right after conf.lua; stash the
|
-- 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`.
|
-- decision in a global so it doesn't need to reparse `arg`.
|
||||||
_G.POKEPORT_EDITOR_MODE = editor
|
_G.POKEPORT_EDITOR_MODE = editor
|
||||||
_G.POKEPORT_DEV_MODE = developer
|
_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
|
-- 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
|
-- 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
|
-- folder. A private editor identity would point love.filesystem at an
|
||||||
@@ -51,14 +63,25 @@ function love.conf(t)
|
|||||||
end
|
end
|
||||||
t.version = love._os == "iOS" and "12.0" or "11.5"
|
t.version = love._os == "iOS" and "12.0" or "11.5"
|
||||||
t.window.vsync = 1
|
t.window.vsync = 1
|
||||||
t.modules.joystick = true
|
t.modules.audio = not companion
|
||||||
|
t.modules.joystick = not companion
|
||||||
t.modules.physics = false
|
t.modules.physics = false
|
||||||
|
|
||||||
-- love.system is not loaded during love.conf; love._os is set by the
|
-- love.system is not loaded during love.conf; love._os is set by the
|
||||||
-- engine before conf runs (LÖVE 11.x / 11.5).
|
-- engine before conf runs (LÖVE 11.x / 11.5).
|
||||||
local osName = love._os
|
local osName = love._os
|
||||||
local mobile = osName == "Android" or osName == "iOS"
|
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
|
-- resizable is what unlocks orientation. SDL's Android backend, given no
|
||||||
-- SDL_HINT_ORIENTATIONS (LÖVE sets none), calls setRequestedOrientation
|
-- SDL_HINT_ORIENTATIONS (LÖVE sets none), calls setRequestedOrientation
|
||||||
-- at window creation -- FULL_SENSOR when the window is resizable (rotates
|
-- at window creation -- FULL_SENSOR when the window is resizable (rotates
|
||||||
@@ -70,6 +93,10 @@ function love.conf(t)
|
|||||||
-- just work. FULL_SENSOR ignores the device's rotation lock, so
|
-- just work. FULL_SENSOR ignores the device's rotation lock, so
|
||||||
-- GameActivity.setOrientationBis remaps it to FULL_USER after SDL has
|
-- GameActivity.setOrientationBis remaps it to FULL_USER after SDL has
|
||||||
-- run: same orientations allowed, but auto-rotate being off now wins.
|
-- 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
|
-- iOS follows the Info.plist orientations
|
||||||
-- (see mobile/ios/overlays/love-ios.plist, now portrait + landscape).
|
-- (see mobile/ios/overlays/love-ios.plist, now portrait + landscape).
|
||||||
t.window.resizable = true
|
t.window.resizable = true
|
||||||
|
|||||||
@@ -1683,158 +1683,172 @@ return {
|
|||||||
{ 0, 0, 0 },
|
{ 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 = {
|
pokemon = {
|
||||||
ABRA = "ABRA",
|
ABRA = "YELLOWMON",
|
||||||
AERODACTYL = "AERODACTYL",
|
AERODACTYL = "GRAYMON",
|
||||||
ALAKAZAM = "ALAKAZAM",
|
ALAKAZAM = "YELLOWMON",
|
||||||
ARBOK = "ARBOK",
|
ARBOK = "PURPLEMON",
|
||||||
ARCANINE = "ARCANINE",
|
ARCANINE = "REDMON",
|
||||||
ARTICUNO = "ARTICUNO",
|
ARTICUNO = "BLUEMON",
|
||||||
BEEDRILL = "BEEDRILL",
|
BEEDRILL = "YELLOWMON",
|
||||||
BELLSPROUT = "BELLSPROUT",
|
BELLSPROUT = "GREENMON",
|
||||||
BLASTOISE = "BLASTOISE",
|
BLASTOISE = "CYANMON",
|
||||||
BULBASAUR = "BULBASAUR",
|
BULBASAUR = "GREENMON",
|
||||||
BUTTERFREE = "BUTTERFREE",
|
BUTTERFREE = "CYANMON",
|
||||||
CATERPIE = "CATERPIE",
|
CATERPIE = "GREENMON",
|
||||||
CHANSEY = "CHANSEY",
|
CHANSEY = "PINKMON",
|
||||||
CHARIZARD = "CHARIZARD",
|
CHARIZARD = "REDMON",
|
||||||
CHARMANDER = "CHARMANDER",
|
CHARMANDER = "REDMON",
|
||||||
CHARMELEON = "CHARMELEON",
|
CHARMELEON = "REDMON",
|
||||||
CLEFABLE = "CLEFABLE",
|
CLEFABLE = "PINKMON",
|
||||||
CLEFAIRY = "CLEFAIRY",
|
CLEFAIRY = "PINKMON",
|
||||||
CLOYSTER = "CLOYSTER",
|
CLOYSTER = "GRAYMON",
|
||||||
CUBONE = "CUBONE",
|
CUBONE = "GRAYMON",
|
||||||
DEWGONG = "DEWGONG",
|
DEWGONG = "BLUEMON",
|
||||||
DIGLETT = "DIGLETT",
|
DIGLETT = "BROWNMON",
|
||||||
DITTO = "DITTO",
|
DITTO = "GRAYMON",
|
||||||
DODRIO = "DODRIO",
|
DODRIO = "BROWNMON",
|
||||||
DODUO = "DODUO",
|
DODUO = "BROWNMON",
|
||||||
DRAGONAIR = "DRAGONAIR",
|
DRAGONAIR = "BLUEMON",
|
||||||
DRAGONITE = "DRAGONITE",
|
DRAGONITE = "BROWNMON",
|
||||||
DRATINI = "DRATINI",
|
DRATINI = "GRAYMON",
|
||||||
DROWZEE = "DROWZEE",
|
DROWZEE = "YELLOWMON",
|
||||||
DUGTRIO = "DUGTRIO",
|
DUGTRIO = "BROWNMON",
|
||||||
EEVEE = "EEVEE",
|
EEVEE = "GRAYMON",
|
||||||
EKANS = "EKANS",
|
EKANS = "PURPLEMON",
|
||||||
ELECTABUZZ = "ELECTABUZZ",
|
ELECTABUZZ = "YELLOWMON",
|
||||||
ELECTRODE = "ELECTRODE",
|
ELECTRODE = "YELLOWMON",
|
||||||
EXEGGCUTE = "EXEGGCUTE",
|
EXEGGCUTE = "PINKMON",
|
||||||
EXEGGUTOR = "EXEGGUTOR",
|
EXEGGUTOR = "GREENMON",
|
||||||
FARFETCHD = "FARFETCH_D",
|
FARFETCHD = "BROWNMON",
|
||||||
FEAROW = "FEAROW",
|
FEAROW = "BROWNMON",
|
||||||
FLAREON = "FLAREON",
|
FLAREON = "REDMON",
|
||||||
GASTLY = "GASTLY",
|
GASTLY = "PURPLEMON",
|
||||||
GENGAR = "GENGAR",
|
GENGAR = "PURPLEMON",
|
||||||
GEODUDE = "GEODUDE",
|
GEODUDE = "GRAYMON",
|
||||||
GLOOM = "GLOOM",
|
GLOOM = "REDMON",
|
||||||
GOLBAT = "GOLBAT",
|
GOLBAT = "BLUEMON",
|
||||||
GOLDEEN = "GOLDEEN",
|
GOLDEEN = "REDMON",
|
||||||
GOLDUCK = "GOLDUCK",
|
GOLDUCK = "CYANMON",
|
||||||
GOLEM = "GOLEM",
|
GOLEM = "GRAYMON",
|
||||||
GRAVELER = "GRAVELER",
|
GRAVELER = "GRAYMON",
|
||||||
GRIMER = "GRIMER",
|
GRIMER = "PURPLEMON",
|
||||||
GROWLITHE = "GROWLITHE",
|
GROWLITHE = "BROWNMON",
|
||||||
GYARADOS = "GYARADOS",
|
GYARADOS = "BLUEMON",
|
||||||
HAUNTER = "HAUNTER",
|
HAUNTER = "PURPLEMON",
|
||||||
HITMONCHAN = "HITMONCHAN",
|
HITMONCHAN = "BROWNMON",
|
||||||
HITMONLEE = "HITMONLEE",
|
HITMONLEE = "BROWNMON",
|
||||||
HORSEA = "HORSEA",
|
HORSEA = "CYANMON",
|
||||||
HYPNO = "HYPNO",
|
HYPNO = "YELLOWMON",
|
||||||
IVYSAUR = "IVYSAUR",
|
IVYSAUR = "GREENMON",
|
||||||
JIGGLYPUFF = "JIGGLYPUFF",
|
JIGGLYPUFF = "PINKMON",
|
||||||
JOLTEON = "JOLTEON",
|
JOLTEON = "YELLOWMON",
|
||||||
JYNX = "JYNX",
|
JYNX = "MEWMON",
|
||||||
KABUTO = "KABUTO",
|
KABUTO = "BROWNMON",
|
||||||
KABUTOPS = "KABUTOPS",
|
KABUTOPS = "BROWNMON",
|
||||||
KADABRA = "KADABRA",
|
KADABRA = "YELLOWMON",
|
||||||
KAKUNA = "KAKUNA",
|
KAKUNA = "YELLOWMON",
|
||||||
KANGASKHAN = "KANGASKHAN",
|
KANGASKHAN = "BROWNMON",
|
||||||
KINGLER = "KINGLER",
|
KINGLER = "REDMON",
|
||||||
KOFFING = "KOFFING",
|
KOFFING = "PURPLEMON",
|
||||||
KRABBY = "KRABBY",
|
KRABBY = "REDMON",
|
||||||
LAPRAS = "LAPRAS",
|
LAPRAS = "CYANMON",
|
||||||
LICKITUNG = "LICKITUNG",
|
LICKITUNG = "PINKMON",
|
||||||
MACHAMP = "MACHAMP",
|
MACHAMP = "GRAYMON",
|
||||||
MACHOKE = "MACHOKE",
|
MACHOKE = "GRAYMON",
|
||||||
MACHOP = "MACHOP",
|
MACHOP = "GRAYMON",
|
||||||
MAGIKARP = "MAGIKARP",
|
MAGIKARP = "REDMON",
|
||||||
MAGMAR = "MAGMAR",
|
MAGMAR = "REDMON",
|
||||||
MAGNEMITE = "MAGNEMITE",
|
MAGNEMITE = "GRAYMON",
|
||||||
MAGNETON = "MAGNETON",
|
MAGNETON = "GRAYMON",
|
||||||
MANKEY = "MANKEY",
|
MANKEY = "BROWNMON",
|
||||||
MAROWAK = "MAROWAK",
|
MAROWAK = "GRAYMON",
|
||||||
MEOWTH = "MEOWTH",
|
MEOWTH = "YELLOWMON",
|
||||||
METAPOD = "METAPOD",
|
METAPOD = "GREENMON",
|
||||||
MEW = "MEW",
|
MEW = "MEWMON",
|
||||||
MEWTWO = "MEWTWO",
|
MEWTWO = "MEWMON",
|
||||||
MOLTRES = "MOLTRES",
|
MOLTRES = "REDMON",
|
||||||
MR_MIME = "MR_MIME",
|
MR_MIME = "PINKMON",
|
||||||
MUK = "MUK",
|
MUK = "PURPLEMON",
|
||||||
NIDOKING = "NIDOKING",
|
NIDOKING = "PURPLEMON",
|
||||||
NIDOQUEEN = "NIDOQUEEN",
|
NIDOQUEEN = "BLUEMON",
|
||||||
NIDORAN_F = "NIDORAN_F",
|
NIDORAN_F = "BLUEMON",
|
||||||
NIDORAN_M = "NIDORAN_M",
|
NIDORAN_M = "PURPLEMON",
|
||||||
NIDORINA = "NIDORINA",
|
NIDORINA = "BLUEMON",
|
||||||
NIDORINO = "NIDORINO",
|
NIDORINO = "PURPLEMON",
|
||||||
NINETALES = "NINETALES",
|
NINETALES = "YELLOWMON",
|
||||||
ODDISH = "ODDISH",
|
ODDISH = "GREENMON",
|
||||||
OMANYTE = "OMANYTE",
|
OMANYTE = "BLUEMON",
|
||||||
OMASTAR = "OMASTAR",
|
OMASTAR = "BLUEMON",
|
||||||
ONIX = "ONIX",
|
ONIX = "GRAYMON",
|
||||||
PARAS = "PARAS",
|
PARAS = "REDMON",
|
||||||
PARASECT = "PARASECT",
|
PARASECT = "REDMON",
|
||||||
PERSIAN = "PERSIAN",
|
PERSIAN = "YELLOWMON",
|
||||||
PIDGEOT = "PIDGEOT",
|
PIDGEOT = "BROWNMON",
|
||||||
PIDGEOTTO = "PIDGEOTTO",
|
PIDGEOTTO = "BROWNMON",
|
||||||
PIDGEY = "PIDGEY",
|
PIDGEY = "BROWNMON",
|
||||||
PIKACHU = "PIKACHU",
|
PIKACHU = "YELLOWMON",
|
||||||
PINSIR = "PINSIR",
|
PINSIR = "BROWNMON",
|
||||||
POLIWAG = "POLIWAG",
|
POLIWAG = "BLUEMON",
|
||||||
POLIWHIRL = "POLIWHIRL",
|
POLIWHIRL = "BLUEMON",
|
||||||
POLIWRATH = "POLIWRATH",
|
POLIWRATH = "BLUEMON",
|
||||||
PONYTA = "PONYTA",
|
PONYTA = "REDMON",
|
||||||
PORYGON = "PORYGON",
|
PORYGON = "GRAYMON",
|
||||||
PRIMEAPE = "PRIMEAPE",
|
PRIMEAPE = "BROWNMON",
|
||||||
PSYDUCK = "PSYDUCK",
|
PSYDUCK = "YELLOWMON",
|
||||||
RAICHU = "RAICHU",
|
RAICHU = "YELLOWMON",
|
||||||
RAPIDASH = "RAPIDASH",
|
RAPIDASH = "REDMON",
|
||||||
RATICATE = "RATICATE",
|
RATICATE = "GRAYMON",
|
||||||
RATTATA = "RATTATA",
|
RATTATA = "GRAYMON",
|
||||||
RHYDON = "RHYDON",
|
RHYDON = "GRAYMON",
|
||||||
RHYHORN = "RHYHORN",
|
RHYHORN = "GRAYMON",
|
||||||
SANDSHREW = "SANDSHREW",
|
SANDSHREW = "BROWNMON",
|
||||||
SANDSLASH = "SANDSLASH",
|
SANDSLASH = "BROWNMON",
|
||||||
SCYTHER = "SCYTHER",
|
SCYTHER = "GREENMON",
|
||||||
SEADRA = "SEADRA",
|
SEADRA = "CYANMON",
|
||||||
SEAKING = "SEAKING",
|
SEAKING = "REDMON",
|
||||||
SEEL = "SEEL",
|
SEEL = "BLUEMON",
|
||||||
SHELLDER = "SHELLDER",
|
SHELLDER = "GRAYMON",
|
||||||
SLOWBRO = "SLOWBRO",
|
SLOWBRO = "PINKMON",
|
||||||
SLOWPOKE = "SLOWPOKE",
|
SLOWPOKE = "PINKMON",
|
||||||
SNORLAX = "SNORLAX",
|
SNORLAX = "PINKMON",
|
||||||
SPEAROW = "SPEAROW",
|
SPEAROW = "BROWNMON",
|
||||||
SQUIRTLE = "SQUIRTLE",
|
SQUIRTLE = "CYANMON",
|
||||||
STARMIE = "STARMIE",
|
STARMIE = "GRAYMON",
|
||||||
STARYU = "STARYU",
|
STARYU = "REDMON",
|
||||||
TANGELA = "TANGELA",
|
TANGELA = "BLUEMON",
|
||||||
TAUROS = "TAUROS",
|
TAUROS = "GRAYMON",
|
||||||
TENTACOOL = "TENTACOOL",
|
TENTACOOL = "CYANMON",
|
||||||
TENTACRUEL = "TENTACRUEL",
|
TENTACRUEL = "CYANMON",
|
||||||
VAPOREON = "VAPOREON",
|
VAPOREON = "CYANMON",
|
||||||
VENOMOTH = "VENOMOTH",
|
VENOMOTH = "PURPLEMON",
|
||||||
VENONAT = "VENONAT",
|
VENONAT = "PURPLEMON",
|
||||||
VENUSAUR = "VENUSAUR",
|
VENUSAUR = "GREENMON",
|
||||||
VICTREEBEL = "VICTREEBEL",
|
VICTREEBEL = "GREENMON",
|
||||||
VILEPLUME = "VILEPLUME",
|
VILEPLUME = "REDMON",
|
||||||
VOLTORB = "VOLTORB",
|
VOLTORB = "YELLOWMON",
|
||||||
VULPIX = "VULPIX",
|
VULPIX = "REDMON",
|
||||||
WARTORTLE = "WARTORTLE",
|
WARTORTLE = "CYANMON",
|
||||||
WEEDLE = "WEEDLE",
|
WEEDLE = "YELLOWMON",
|
||||||
WEEPINBELL = "WEEPINBELL",
|
WEEPINBELL = "GREENMON",
|
||||||
WEEZING = "WEEZING",
|
WEEZING = "PURPLEMON",
|
||||||
WIGGLYTUFF = "WIGGLYTUFF",
|
WIGGLYTUFF = "PINKMON",
|
||||||
ZAPDOS = "ZAPDOS",
|
ZAPDOS = "YELLOWMON",
|
||||||
ZUBAT = "ZUBAT",
|
ZUBAT = "BLUEMON",
|
||||||
},
|
},
|
||||||
source = "pokered-gbc data/super_palettes.asm + data/mon_palettes.asm + color/**",
|
source = "pokered-gbc data/super_palettes.asm + data/mon_palettes.asm + color/**",
|
||||||
world = {
|
world = {
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ return {
|
|||||||
{ "set_flag", "EVENT_GOT_EEVEE" }, -- 7
|
{ "set_flag", "EVENT_GOT_EEVEE" }, -- 7
|
||||||
{ "hide_object", "CELADON_MANSION_ROOF_HOUSE",
|
{ "hide_object", "CELADON_MANSION_ROOF_HOUSE",
|
||||||
"CELADONMANSION_ROOF_HOUSE_EEVEE_POKEBALL" }, -- 8
|
"CELADONMANSION_ROOF_HOUSE_EEVEE_POKEBALL" }, -- 8
|
||||||
{ "play_sound", "Get_Item1" }, -- 9 (GotMonText jingle)
|
{ "text_sound", "Get_Item1" }, -- 9 (GotMonText jingle)
|
||||||
{ "show_text", "_GotMonText", { RAM = "EEVEE" } }, -- 10
|
{ "show_text", "_GotMonText", { RAM = "EEVEE" } }, -- 10
|
||||||
{ "jump", 13 }, -- 11
|
{ "jump", 13 }, -- 11
|
||||||
{ "show_text", "_BoxIsFullText" }, -- 12
|
{ "show_text", "_BoxIsFullText" }, -- 12
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ return {
|
|||||||
-- else -> .WhatsLostIsLostText (player has TM_DIG)
|
-- else -> .WhatsLostIsLostText (player has TM_DIG)
|
||||||
TEXT_CERULEANTRASHEDHOUSE_FISHING_GURU = {
|
TEXT_CERULEANTRASHEDHOUSE_FISHING_GURU = {
|
||||||
{ "check_item", "TM_DIG" },
|
{ "check_item", "TM_DIG" },
|
||||||
{ "jump_if_true", 4 },
|
{ "jump_if_true", 5 },
|
||||||
{ "show_text", "_CeruleanTrashedHouseFishingGuruTheyStoleATMText" },
|
{ "show_text", "_CeruleanTrashedHouseFishingGuruTheyStoleATMText" },
|
||||||
{ "jump", 5 },
|
{ "jump", "end" },
|
||||||
{ "show_text", "_CeruleanTrashedHouseFishingGuruWhatsLostIsLostText" },
|
{ "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)
|
local function coinGiver(opts)
|
||||||
return function(game, ow, npc, done)
|
return function(game, ow, npc, done)
|
||||||
local TextBox = require("src.render.TextBox")
|
local TextBox = require("src.render.TextBox")
|
||||||
local Sound = require("src.core.Sound")
|
|
||||||
local t = game.data.text
|
local t = game.data.text
|
||||||
local function push(label, fallback, onDone)
|
local function push(label, fallback, onDone, popts)
|
||||||
game.stack:push(TextBox.new(game, t[label] or fallback, onDone or done))
|
game.stack:push(TextBox.new(game, t[label] or fallback, onDone or done,
|
||||||
|
popts))
|
||||||
end
|
end
|
||||||
if game.save.flags[opts.event] then
|
if game.save.flags[opts.event] then
|
||||||
push(opts.alreadyGotLabel, opts.alreadyGotFallback)
|
push(opts.alreadyGotLabel, opts.alreadyGotFallback)
|
||||||
@@ -30,9 +30,10 @@ local function coinGiver(opts)
|
|||||||
end
|
end
|
||||||
game.save.coins = math.min(9999, (game.save.coins or 0) + opts.amount)
|
game.save.coins = math.min(9999, (game.save.coins or 0) + opts.amount)
|
||||||
game.save.flags[opts.event] = true
|
game.save.flags[opts.event] = true
|
||||||
Sound.play(game.data, "Get_Item1")
|
-- the ReceivedNCoinsText strings carry sound_get_item_1
|
||||||
push(opts.receivedLabel,
|
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
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -8,9 +8,9 @@ return {
|
|||||||
TEXT_LAVENDERMART_COOLTRAINER_M = {
|
TEXT_LAVENDERMART_COOLTRAINER_M = {
|
||||||
{ "face_player" },
|
{ "face_player" },
|
||||||
{ "check_flag", "EVENT_RESCUED_MR_FUJI" },
|
{ "check_flag", "EVENT_RESCUED_MR_FUJI" },
|
||||||
{ "jump_if_true", 5 },
|
{ "jump_if_true", 6 },
|
||||||
{ "show_text", "_LavenderMartCooltrainerMReviveText" },
|
{ "show_text", "_LavenderMartCooltrainerMReviveText" },
|
||||||
{ "jump", 6 },
|
{ "jump", "end" },
|
||||||
{ "show_text", "_LavenderMartCooltrainerMNuggetText" },
|
{ "show_text", "_LavenderMartCooltrainerMNuggetText" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -13,9 +13,9 @@ M.MR_FUJIS_HOUSE = {
|
|||||||
TEXT_MRFUJISHOUSE_SUPER_NERD = {
|
TEXT_MRFUJISHOUSE_SUPER_NERD = {
|
||||||
{ "face_player" }, -- 1
|
{ "face_player" }, -- 1
|
||||||
{ "check_flag", "EVENT_RESCUED_MR_FUJI" }, -- 2
|
{ "check_flag", "EVENT_RESCUED_MR_FUJI" }, -- 2
|
||||||
{ "jump_if_true", 5 }, -- 3
|
{ "jump_if_true", 6 }, -- 3
|
||||||
{ "show_text", "_MrFujisHouseSuperNerdMrFujiIsntHereText" }, -- 4
|
{ "show_text", "_MrFujisHouseSuperNerdMrFujiIsntHereText" }, -- 4
|
||||||
{ "jump", 6 }, -- 5
|
{ "jump", "end" }, -- 5
|
||||||
{ "show_text", "_MrFujisHouseSuperNerdMrFujiHadBeenPrayingText" }, -- 6
|
{ "show_text", "_MrFujisHouseSuperNerdMrFujiHadBeenPrayingText" }, -- 6
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -25,9 +25,9 @@ M.MR_FUJIS_HOUSE = {
|
|||||||
TEXT_MRFUJISHOUSE_LITTLE_GIRL = {
|
TEXT_MRFUJISHOUSE_LITTLE_GIRL = {
|
||||||
{ "face_player" }, -- 1
|
{ "face_player" }, -- 1
|
||||||
{ "check_flag", "EVENT_RESCUED_MR_FUJI" }, -- 2
|
{ "check_flag", "EVENT_RESCUED_MR_FUJI" }, -- 2
|
||||||
{ "jump_if_true", 5 }, -- 3
|
{ "jump_if_true", 6 }, -- 3
|
||||||
{ "show_text", "_MrFujisHouseLittleGirlThisIsMrFujisHouseText" }, -- 4
|
{ "show_text", "_MrFujisHouseLittleGirlThisIsMrFujisHouseText" }, -- 4
|
||||||
{ "jump", 6 }, -- 5
|
{ "jump", "end" }, -- 5
|
||||||
{ "show_text", "_MrFujisHouseLittleGirlPokemonAreNiceToHugText" }, -- 6
|
{ "show_text", "_MrFujisHouseLittleGirlPokemonAreNiceToHugText" }, -- 6
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ return {
|
|||||||
local TextBox = require("src.render.TextBox")
|
local TextBox = require("src.render.TextBox")
|
||||||
local Commands = require("src.script.Commands")
|
local Commands = require("src.script.Commands")
|
||||||
local t = game.data.text
|
local t = game.data.text
|
||||||
local function say(label, cb)
|
local function say(label, cb, sopts)
|
||||||
game.stack:push(TextBox.new(game, t[label] or label, cb))
|
game.stack:push(TextBox.new(game, t[label] or label, cb, sopts))
|
||||||
end
|
end
|
||||||
|
|
||||||
if game.save.flags.EVENT_GOT_OLD_AMBER then
|
if game.save.flags.EVENT_GOT_OLD_AMBER then
|
||||||
@@ -39,8 +39,9 @@ return {
|
|||||||
game.save.flags.EVENT_GOT_OLD_AMBER = true
|
game.save.flags.EVENT_GOT_OLD_AMBER = true
|
||||||
Commands.hide_object({ save = game.save, overworld = ow, game = game },
|
Commands.hide_object({ save = game.save, overworld = ow, game = game },
|
||||||
"MUSEUM_1F", "MUSEUM1F_OLD_AMBER")
|
"MUSEUM_1F", "MUSEUM1F_OLD_AMBER")
|
||||||
require("src.core.Sound").play(game.data, "Get_Item1")
|
-- .ReceivedOldAmberText carries sound_get_item_1
|
||||||
say("_Museum1FScientist2ReceivedOldAmberText", done)
|
say("_Museum1FScientist2ReceivedOldAmberText", done,
|
||||||
|
TextBox.soundOpts(game, "Get_Item1"))
|
||||||
end)
|
end)
|
||||||
end,
|
end,
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,39 @@
|
|||||||
-- Hand-ported flavor text for OaksLab (registry id OAKS_LAB).
|
-- Hand-ported OAKS_LAB flavor: the simple talk texts (scripts/OaksLab.asm;
|
||||||
-- Source: pokered/scripts/OaksLab.asm. These five text_asm bodies are
|
-- OAK1, the starter balls and RIVAL live in data/scripts/oaks_lab.lua).
|
||||||
-- all simple "PrintText; jp TextScriptEnd" -- no flag branches, no
|
|
||||||
-- YES/NO menu -- so a one-row talk script showing the real extracted
|
local TextBox = require("src.render.TextBox")
|
||||||
-- 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.)
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
OAKS_LAB = {
|
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 = {
|
talk = {
|
||||||
-- OaksLabGirlText (scripts/OaksLab.asm)
|
-- OaksLabGirlText (scripts/OaksLab.asm)
|
||||||
TEXT_OAKSLAB_GIRL = {
|
TEXT_OAKSLAB_GIRL = {
|
||||||
|
|||||||
@@ -3,9 +3,8 @@
|
|||||||
-- guide and SUPER_NERD2 garden nerd.
|
-- guide and SUPER_NERD2 garden nerd.
|
||||||
--
|
--
|
||||||
-- The YOUNGSTER's gym escort (talk + east-exit onStep) lives in
|
-- The YOUNGSTER's gym escort (talk + east-exit onStep) lives in
|
||||||
-- story5.lua so the lockstep RLE walk is not overwritten by this
|
-- story5.lua; SUPER_NERD1's museum escort (scripts/PewterCity.asm:47-113)
|
||||||
-- flavor merge. SUPER_NERD1's museum escort is not ported; only the
|
-- is below.
|
||||||
-- YES/NO-branched flavor text is here.
|
|
||||||
|
|
||||||
local M = {}
|
local M = {}
|
||||||
|
|
||||||
@@ -16,16 +15,147 @@ local function push(game, s, done)
|
|||||||
game.stack:push(TextBox.new(game, s, done))
|
game.stack:push(TextBox.new(game, s, done))
|
||||||
end
|
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 function ask(game, s, cb)
|
||||||
local ChoiceBox = require("src.ui.ChoiceBox")
|
local TextBox = require("src.render.TextBox")
|
||||||
push(game, s, function() game.stack:push(ChoiceBox.new(game, cb)) end)
|
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
|
end
|
||||||
|
|
||||||
M.PEWTER_CITY = {
|
M.PEWTER_CITY = {
|
||||||
|
museumEscort = { plan = museumPlan, guySteps = museumGuySteps },
|
||||||
talk = {
|
talk = {
|
||||||
-- PewterCitySuperNerd1Text (scripts/PewterCity.asm): asks if you
|
-- PewterCitySuperNerd1Text (scripts/PewterCity.asm:209-237): YES ->
|
||||||
-- checked out the museum; YES -> fossils comment, NO -> "you have
|
-- fossils comment, NO -> "you have to go" and the museum escort
|
||||||
-- to go" (which in pokered also kicks off the escort script).
|
|
||||||
TEXT_PEWTERCITY_SUPER_NERD1 = function(game, ow, npc, done)
|
TEXT_PEWTERCITY_SUPER_NERD1 = function(game, ow, npc, done)
|
||||||
local t = text(game)
|
local t = text(game)
|
||||||
ask(game, t._PewterCitySuperNerd1DidYouCheckOutMuseumText
|
ask(game, t._PewterCitySuperNerd1DidYouCheckOutMuseumText
|
||||||
@@ -35,7 +165,10 @@ M.PEWTER_CITY = {
|
|||||||
or "Weren't those\nfossils from MT.\nMOON amazing?", done)
|
or "Weren't those\nfossils from MT.\nMOON amazing?", done)
|
||||||
else
|
else
|
||||||
push(game, t._PewterCitySuperNerd1YouHaveToGoText
|
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)
|
end)
|
||||||
end,
|
end,
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ return {
|
|||||||
TEXT_ROUTE16GATE1F_GUARD = {
|
TEXT_ROUTE16GATE1F_GUARD = {
|
||||||
{ "face_player" },
|
{ "face_player" },
|
||||||
{ "check_item", "BICYCLE" },
|
{ "check_item", "BICYCLE" },
|
||||||
{ "jump_if_true", 5 },
|
{ "jump_if_true", 6 },
|
||||||
{ "show_text", "_Route16Gate1FGuardNoPedestriansAllowedText" },
|
{ "show_text", "_Route16Gate1FGuardNoPedestriansAllowedText" },
|
||||||
{ "jump", 6 },
|
{ "jump", "end" },
|
||||||
{ "show_text", "_Route16Gate1FGuardCyclingRoadExplanationText" },
|
{ "show_text", "_Route16Gate1FGuardCyclingRoadExplanationText" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ return {
|
|||||||
TEXT_ROUTE18GATE1F_GUARD = {
|
TEXT_ROUTE18GATE1F_GUARD = {
|
||||||
{ "face_player" },
|
{ "face_player" },
|
||||||
{ "check_item", "BICYCLE" },
|
{ "check_item", "BICYCLE" },
|
||||||
{ "jump_if_true", 5 },
|
{ "jump_if_true", 6 },
|
||||||
{ "show_text", "_Route18Gate1FGuardYouNeedABicycleText" },
|
{ "show_text", "_Route18Gate1FGuardYouNeedABicycleText" },
|
||||||
{ "jump", 6 },
|
{ "jump", "end" },
|
||||||
{ "show_text", "_Route18Gate1FGuardCyclingRoadUphillText" },
|
{ "show_text", "_Route18Gate1FGuardCyclingRoadUphillText" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ return {
|
|||||||
TEXT_SILPHCO10F_SILPH_WORKER_F = {
|
TEXT_SILPHCO10F_SILPH_WORKER_F = {
|
||||||
{ "face_player" },
|
{ "face_player" },
|
||||||
{ "check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI" },
|
{ "check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI" },
|
||||||
{ "jump_if_true", 5 },
|
{ "jump_if_true", 6 },
|
||||||
{ "show_text", "_SilphCo10FSilphWorkerFImScaredText" },
|
{ "show_text", "_SilphCo10FSilphWorkerFImScaredText" },
|
||||||
{ "jump", 6 },
|
{ "jump", "end" },
|
||||||
{ "show_text", "_SilphCo10FSilphWorkerFQuietAboutMyCryingText" },
|
{ "show_text", "_SilphCo10FSilphWorkerFQuietAboutMyCryingText" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ return {
|
|||||||
-- not set: _SilphCo3FSilphWorkerMWhatShouldIDoText
|
-- not set: _SilphCo3FSilphWorkerMWhatShouldIDoText
|
||||||
TEXT_SILPHCO3F_SILPH_WORKER_M = {
|
TEXT_SILPHCO3F_SILPH_WORKER_M = {
|
||||||
{ "check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI" },
|
{ "check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI" },
|
||||||
{ "jump_if_true", 4 },
|
{ "jump_if_true", 5 },
|
||||||
{ "show_text", "_SilphCo3FSilphWorkerMWhatShouldIDoText" },
|
{ "show_text", "_SilphCo3FSilphWorkerMWhatShouldIDoText" },
|
||||||
{ "jump", 5 },
|
{ "jump", "end" },
|
||||||
{ "show_text", "_SilphCo3FSilphWorkerMYouSavedUsText" },
|
{ "show_text", "_SilphCo3FSilphWorkerMYouSavedUsText" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ return {
|
|||||||
TEXT_SILPHCO4F_SILPH_WORKER_M = {
|
TEXT_SILPHCO4F_SILPH_WORKER_M = {
|
||||||
{"face_player"},
|
{"face_player"},
|
||||||
{"check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI"},
|
{"check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI"},
|
||||||
{"jump_if_true", 5},
|
{"jump_if_true", 6},
|
||||||
{"show_text", "_SilphCo4FSilphWorkerMImHidingText"},
|
{"show_text", "_SilphCo4FSilphWorkerMImHidingText"},
|
||||||
{"jump", 6},
|
{"jump", "end"},
|
||||||
{"show_text", "_SilphCo4FSilphWorkerMTeamRocketIsGoneText"},
|
{"show_text", "_SilphCo4FSilphWorkerMTeamRocketIsGoneText"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ return {
|
|||||||
TEXT_SILPHCO5F_SILPH_WORKER_M = {
|
TEXT_SILPHCO5F_SILPH_WORKER_M = {
|
||||||
{"face_player"},
|
{"face_player"},
|
||||||
{"check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI"},
|
{"check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI"},
|
||||||
{"jump_if_true", 5},
|
{"jump_if_true", 6},
|
||||||
{"show_text", "_SilphCo5FSilphWorkerMThatsYouRightText"},
|
{"show_text", "_SilphCo5FSilphWorkerMThatsYouRightText"},
|
||||||
{"jump", 6},
|
{"jump", "end"},
|
||||||
{"show_text", "_SilphCo5FSilphWorkerMYoureOurHeroText"},
|
{"show_text", "_SilphCo5FSilphWorkerMYoureOurHeroText"},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ return {
|
|||||||
TEXT_SILPHCO6F_SILPH_WORKER_M1 = {
|
TEXT_SILPHCO6F_SILPH_WORKER_M1 = {
|
||||||
{ "face_player" },
|
{ "face_player" },
|
||||||
{ "check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI" },
|
{ "check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI" },
|
||||||
{ "jump_if_true", 5 },
|
{ "jump_if_true", 6 },
|
||||||
{ "show_text", "_SilphCo6FSilphWorkerM1TookOverTheBuildingText" },
|
{ "show_text", "_SilphCo6FSilphWorkerM1TookOverTheBuildingText" },
|
||||||
{ "jump", 6 },
|
{ "jump", "end" },
|
||||||
{ "show_text", "_SilphCo6FSilphWorkerM1BackToWorkText" },
|
{ "show_text", "_SilphCo6FSilphWorkerM1BackToWorkText" },
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -21,9 +21,9 @@ return {
|
|||||||
TEXT_SILPHCO6F_SILPH_WORKER_M2 = {
|
TEXT_SILPHCO6F_SILPH_WORKER_M2 = {
|
||||||
{ "face_player" },
|
{ "face_player" },
|
||||||
{ "check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI" },
|
{ "check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI" },
|
||||||
{ "jump_if_true", 5 },
|
{ "jump_if_true", 6 },
|
||||||
{ "show_text", "_SilphCo6FSilphWorkerMHelpMePleaseText" },
|
{ "show_text", "_SilphCo6FSilphWorkerMHelpMePleaseText" },
|
||||||
{ "jump", 6 },
|
{ "jump", "end" },
|
||||||
{ "show_text", "_SilphCo6FSilphWorkerMWeGotEngagedText" },
|
{ "show_text", "_SilphCo6FSilphWorkerMWeGotEngagedText" },
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -31,9 +31,9 @@ return {
|
|||||||
TEXT_SILPHCO6F_SILPH_WORKER_F1 = {
|
TEXT_SILPHCO6F_SILPH_WORKER_F1 = {
|
||||||
{ "face_player" },
|
{ "face_player" },
|
||||||
{ "check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI" },
|
{ "check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI" },
|
||||||
{ "jump_if_true", 5 },
|
{ "jump_if_true", 6 },
|
||||||
{ "show_text", "_SilphCo6FSilphWorkerF1SuchACowardText" },
|
{ "show_text", "_SilphCo6FSilphWorkerF1SuchACowardText" },
|
||||||
{ "jump", 6 },
|
{ "jump", "end" },
|
||||||
{ "show_text", "_SilphCo6FSilphWorkerF1HaveToMarryHimText" },
|
{ "show_text", "_SilphCo6FSilphWorkerF1HaveToMarryHimText" },
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -41,9 +41,9 @@ return {
|
|||||||
TEXT_SILPHCO6F_SILPH_WORKER_F2 = {
|
TEXT_SILPHCO6F_SILPH_WORKER_F2 = {
|
||||||
{ "face_player" },
|
{ "face_player" },
|
||||||
{ "check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI" },
|
{ "check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI" },
|
||||||
{ "jump_if_true", 5 },
|
{ "jump_if_true", 6 },
|
||||||
{ "show_text", "_SilphCo6FSilphWorkerF2TeamRocketConquerWorldText" },
|
{ "show_text", "_SilphCo6FSilphWorkerF2TeamRocketConquerWorldText" },
|
||||||
{ "jump", 6 },
|
{ "jump", "end" },
|
||||||
{ "show_text", "_SilphCo6FSilphWorkerF2TeamRocketRanText" },
|
{ "show_text", "_SilphCo6FSilphWorkerF2TeamRocketRanText" },
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -51,9 +51,9 @@ return {
|
|||||||
TEXT_SILPHCO6F_SILPH_WORKER_M3 = {
|
TEXT_SILPHCO6F_SILPH_WORKER_M3 = {
|
||||||
{ "face_player" },
|
{ "face_player" },
|
||||||
{ "check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI" },
|
{ "check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI" },
|
||||||
{ "jump_if_true", 5 },
|
{ "jump_if_true", 6 },
|
||||||
{ "show_text", "_SilphCo6FSilphWorkerM3TargetedSilphText" },
|
{ "show_text", "_SilphCo6FSilphWorkerM3TargetedSilphText" },
|
||||||
{ "jump", 6 },
|
{ "jump", "end" },
|
||||||
{ "show_text", "_SilphCo6FSilphWorkerM3WorkForSilphText" },
|
{ "show_text", "_SilphCo6FSilphWorkerM3WorkForSilphText" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ return {
|
|||||||
-- set: _SilphCo7FSilphWorkerM2CancelledMasterBallText
|
-- set: _SilphCo7FSilphWorkerM2CancelledMasterBallText
|
||||||
TEXT_SILPHCO7F_SILPH_WORKER_M2 = {
|
TEXT_SILPHCO7F_SILPH_WORKER_M2 = {
|
||||||
{ "check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI" },
|
{ "check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI" },
|
||||||
{ "jump_if_true", 4 },
|
{ "jump_if_true", 5 },
|
||||||
{ "show_text", "_SilphCo7FSilphWorkerM2AfterTheMasterBallText" },
|
{ "show_text", "_SilphCo7FSilphWorkerM2AfterTheMasterBallText" },
|
||||||
{ "jump", 5 },
|
{ "jump", "end" },
|
||||||
{ "show_text", "_SilphCo7FSilphWorkerM2CancelledMasterBallText" },
|
{ "show_text", "_SilphCo7FSilphWorkerM2CancelledMasterBallText" },
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -22,9 +22,9 @@ return {
|
|||||||
-- set: _SilphCo7FSilphWorkerM3YouChasedOffTeamRocketText
|
-- set: _SilphCo7FSilphWorkerM3YouChasedOffTeamRocketText
|
||||||
TEXT_SILPHCO7F_SILPH_WORKER_M3 = {
|
TEXT_SILPHCO7F_SILPH_WORKER_M3 = {
|
||||||
{ "check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI" },
|
{ "check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI" },
|
||||||
{ "jump_if_true", 4 },
|
{ "jump_if_true", 5 },
|
||||||
{ "show_text", "_SilphCo7FSilphWorkerM3ItWouldBeBadText" },
|
{ "show_text", "_SilphCo7FSilphWorkerM3ItWouldBeBadText" },
|
||||||
{ "jump", 5 },
|
{ "jump", "end" },
|
||||||
{ "show_text", "_SilphCo7FSilphWorkerM3YouChasedOffTeamRocketText" },
|
{ "show_text", "_SilphCo7FSilphWorkerM3YouChasedOffTeamRocketText" },
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -34,9 +34,9 @@ return {
|
|||||||
-- set: _SilphCo7FSilphWorkerM4SafeAtLastText
|
-- set: _SilphCo7FSilphWorkerM4SafeAtLastText
|
||||||
TEXT_SILPHCO7F_SILPH_WORKER_M4 = {
|
TEXT_SILPHCO7F_SILPH_WORKER_M4 = {
|
||||||
{ "check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI" },
|
{ "check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI" },
|
||||||
{ "jump_if_true", 4 },
|
{ "jump_if_true", 5 },
|
||||||
{ "show_text", "_SilphCo7FSilphWorkerM4ItsReallyDangerousHereText" },
|
{ "show_text", "_SilphCo7FSilphWorkerM4ItsReallyDangerousHereText" },
|
||||||
{ "jump", 5 },
|
{ "jump", "end" },
|
||||||
{ "show_text", "_SilphCo7FSilphWorkerM4SafeAtLastText" },
|
{ "show_text", "_SilphCo7FSilphWorkerM4SafeAtLastText" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ return {
|
|||||||
-- set: _SilphCo8FSilphWorkerMThanksForSavingUsText
|
-- set: _SilphCo8FSilphWorkerMThanksForSavingUsText
|
||||||
TEXT_SILPHCO8F_SILPH_WORKER_M = {
|
TEXT_SILPHCO8F_SILPH_WORKER_M = {
|
||||||
{ "check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI" },
|
{ "check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI" },
|
||||||
{ "jump_if_true", 4 },
|
{ "jump_if_true", 5 },
|
||||||
{ "show_text", "_SilphCo8FSilphWorkerMSilphIsFinishedText" },
|
{ "show_text", "_SilphCo8FSilphWorkerMSilphIsFinishedText" },
|
||||||
{ "jump", 5 },
|
{ "jump", "end" },
|
||||||
{ "show_text", "_SilphCo8FSilphWorkerMThanksForSavingUsText" },
|
{ "show_text", "_SilphCo8FSilphWorkerMThanksForSavingUsText" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -23,9 +23,13 @@ local function push(game, s, done)
|
|||||||
game.stack:push(TextBox.new(game, s, done))
|
game.stack:push(TextBox.new(game, s, done))
|
||||||
end
|
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 function ask(game, s, cb)
|
||||||
local ChoiceBox = require("src.ui.ChoiceBox")
|
local TextBox = require("src.render.TextBox")
|
||||||
push(game, s, function() game.stack:push(ChoiceBox.new(game, cb)) end)
|
game.stack:push(TextBox.new(game, s, nil, { choice = cb }))
|
||||||
end
|
end
|
||||||
|
|
||||||
M.VIRIDIAN_CITY = {
|
M.VIRIDIAN_CITY = {
|
||||||
|
|||||||
@@ -9,7 +9,8 @@
|
|||||||
-- hidden_text_predef spends the facing byte on the tx_pre id, so neither
|
-- hidden_text_predef spends the facing byte on the tx_pre id, so neither
|
||||||
-- tile gates on facing.
|
-- tile gates on facing.
|
||||||
|
|
||||||
local Menu = require("src.ui.Menu")
|
local Font = require("src.render.Font")
|
||||||
|
local Theme = require("src.ui.Theme")
|
||||||
local TextBox = require("src.render.TextBox")
|
local TextBox = require("src.render.TextBox")
|
||||||
|
|
||||||
-- ViridianSchoolBlackboard (engine/events/hidden_events/school_blackboard.asm):
|
-- ViridianSchoolBlackboard (engine/events/hidden_events/school_blackboard.asm):
|
||||||
@@ -25,33 +26,112 @@ local STATUS_LABELS = {
|
|||||||
{ " FRZ", "_ViridianBlackboardFrozenText" },
|
{ " 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 function blackboard(game)
|
||||||
local text = game.data.text or {}
|
local text = game.data.text or {}
|
||||||
local items, showMenu, askHeading
|
local openBoard
|
||||||
function showMenu()
|
-- wCurrentMenuItem / wMenuItemOffset are zeroed once, above .blackboardLoop,
|
||||||
game.stack:push(Menu.new(game, items,
|
-- and nothing inside the loop clears them again: after a status blurb
|
||||||
{ tx = 0, ty = 0, tw = 12, th = 8, rowStep = 1 }))
|
-- `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
|
end
|
||||||
-- ViridianSchoolBlackboardText2 is reprinted on every .blackboardLoop
|
local function pick(i)
|
||||||
-- pass, immediately before HandleMenuInput
|
closeBoard()
|
||||||
function askHeading()
|
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,
|
game.stack:push(TextBox.new(game,
|
||||||
text._ViridianSchoolBlackboardText2 or "Which heading do\nyou want to read?",
|
text._ViridianSchoolBlackboardText2 or "Which heading do\nyou want to read?",
|
||||||
showMenu))
|
nil, { stay = { onShown = function()
|
||||||
|
board = board or StatusBoard.new(game, pick, closeBoard)
|
||||||
|
game.stack:push(board)
|
||||||
|
end } }))
|
||||||
end
|
end
|
||||||
items = {}
|
|
||||||
for i, row in ipairs(STATUS_LABELS) do
|
|
||||||
local label, key = row[1], row[2]
|
|
||||||
items[i] = { label = label, onSelect = function()
|
|
||||||
game.stack:push(TextBox.new(game, text[key] or label, askHeading))
|
|
||||||
end }
|
|
||||||
end
|
|
||||||
-- no onSelect: Menu's own pop closes the box, matching .exitBlackboard
|
|
||||||
items[#items + 1] = { label = " QUIT" }
|
|
||||||
game.stack:push(TextBox.new(game,
|
game.stack:push(TextBox.new(game,
|
||||||
text._ViridianSchoolBlackboardText1
|
text._ViridianSchoolBlackboardText1
|
||||||
or "The blackboard\ndescribes POKéMON\vSTATUS changes\vduring battles.",
|
or "The blackboard\ndescribes POKéMON\vSTATUS changes\vduring battles.",
|
||||||
askHeading))
|
openBoard))
|
||||||
end
|
end
|
||||||
|
|
||||||
-- ViridianSchoolNotebook (engine/events/hidden_events/school_notebooks.asm):
|
-- ViridianSchoolNotebook (engine/events/hidden_events/school_notebooks.asm):
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ local files = {
|
|||||||
"data.scripts.flavor.cerulean_trashed_house",
|
"data.scripts.flavor.cerulean_trashed_house",
|
||||||
"data.scripts.flavor.copycats_house_1f",
|
"data.scripts.flavor.copycats_house_1f",
|
||||||
"data.scripts.flavor.copycats_house_2f",
|
"data.scripts.flavor.copycats_house_2f",
|
||||||
|
"data.scripts.flavor.fuchsia_city",
|
||||||
"data.scripts.flavor.game_corner",
|
"data.scripts.flavor.game_corner",
|
||||||
"data.scripts.flavor.lavender_cubone_house",
|
"data.scripts.flavor.lavender_cubone_house",
|
||||||
"data.scripts.flavor.lavender_mart",
|
"data.scripts.flavor.lavender_mart",
|
||||||
|
|||||||
@@ -18,6 +18,26 @@ local M = {
|
|||||||
VIRIDIAN_GYM = { city = "VIRIDIAN CITY", leader = "GIOVANNI", badge = "EARTHBADGE" },
|
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
|
-- scripts/PewterGym.asm PewterGymBrockText (text_asm): CheckEvent
|
||||||
-- EVENT_BEAT_BROCK branches his dialogue. Before the badge he prints
|
-- EVENT_BEAT_BROCK branches his dialogue. Before the badge he prints
|
||||||
-- _PewterGymBrockPreBattleText and engages the leader battle
|
-- _PewterGymBrockPreBattleText and engages the leader battle
|
||||||
@@ -25,12 +45,14 @@ local M = {
|
|||||||
-- badge/TM34 rewards and EVENT_BEAT_BROCK come from
|
-- badge/TM34 rewards and EVENT_BEAT_BROCK come from
|
||||||
-- data/scripts/victories.lua OPP_BROCK#1). After the badge his
|
-- data/scripts/victories.lua OPP_BROCK#1). After the badge his
|
||||||
-- .afterBeat branch prints _PewterGymBrockPostBattleAdviceText ("Go to
|
-- .afterBeat branch prints _PewterGymBrockPostBattleAdviceText ("Go to
|
||||||
-- the GYM in CERULEAN..."). The original's middle branch (beat but
|
-- the GYM in CERULEAN..."). The middle branch (beat but TM34 not yet
|
||||||
-- TM34 not yet handed over, CheckEventReuseA EVENT_GOT_TM34) is
|
-- handed over, CheckEventReuseA EVENT_GOT_TM34 -> call
|
||||||
-- unreachable in the port: the TM is granted with the victory.
|
-- PewterGymScriptReceiveTM34) retries the TM give when the bag was full
|
||||||
|
-- at the victory (#797).
|
||||||
M.PEWTER_GYM.talk = {
|
M.PEWTER_GYM.talk = {
|
||||||
TEXT_PEWTERGYM_BROCK = function(game, ow, npc, done)
|
TEXT_PEWTERGYM_BROCK = function(game, ow, npc, done)
|
||||||
if game.save.flags.EVENT_BEAT_BROCK then
|
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")
|
local TextBox = require("src.render.TextBox")
|
||||||
game.stack:push(TextBox.new(game,
|
game.stack:push(TextBox.new(game,
|
||||||
game.data.text._PewterGymBrockPostBattleAdviceText
|
game.data.text._PewterGymBrockPostBattleAdviceText
|
||||||
@@ -48,16 +70,17 @@ M.PEWTER_GYM.talk = {
|
|||||||
-- (engageTrainer shows that same pre-battle text via resolveText; the
|
-- (engageTrainer shows that same pre-battle text via resolveText; the
|
||||||
-- badge/TM rewards and the beat flag come from data/scripts/victories.lua)
|
-- 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,
|
-- -- and once beaten print the post-battle advice text. As with Brock,
|
||||||
-- the originals' middle branch (beaten but the TM not yet handed over,
|
-- the middle branch (beaten but the TM not yet handed over,
|
||||||
-- CheckEventReuseA EVENT_GOT_TM*) is unreachable in the port: the TM is
|
-- CheckEventReuseA EVENT_GOT_TM*) retries the TM give when the bag was
|
||||||
-- granted with the victory.
|
-- full at the victory.
|
||||||
-- afterAdvice, when given, takes over `done`: it is handed (game, ow, npc,
|
-- 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
|
-- done) and must call done() itself once whatever it's doing (e.g. a fade
|
||||||
-- around a HideObject) finishes, rather than having it invoked
|
-- around a HideObject) finishes, rather than having it invoked
|
||||||
-- automatically. Only Giovanni's farewell uses this.
|
-- 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)
|
return function(game, ow, npc, done)
|
||||||
if game.save.flags[beatFlag] then
|
if game.save.flags[beatFlag] then
|
||||||
|
if victoryKey and retryTmGive(game, ow, victoryKey, done) then return end
|
||||||
local TextBox = require("src.render.TextBox")
|
local TextBox = require("src.render.TextBox")
|
||||||
local finish = done
|
local finish = done
|
||||||
if afterAdvice then
|
if afterAdvice then
|
||||||
@@ -79,42 +102,42 @@ end
|
|||||||
M.CERULEAN_GYM.talk = {
|
M.CERULEAN_GYM.talk = {
|
||||||
TEXT_CERULEANGYM_MISTY = leaderTalk("EVENT_BEAT_MISTY",
|
TEXT_CERULEANGYM_MISTY = leaderTalk("EVENT_BEAT_MISTY",
|
||||||
"_CeruleanGymMistyTM11ExplanationText",
|
"_CeruleanGymMistyTM11ExplanationText",
|
||||||
"TM11 teaches\nBUBBLEBEAM!"),
|
"TM11 teaches\nBUBBLEBEAM!", nil, "OPP_MISTY#1"),
|
||||||
}
|
}
|
||||||
|
|
||||||
-- scripts/VermilionGym.asm VermilionGymLTSurgeText .got_tm24_already
|
-- scripts/VermilionGym.asm VermilionGymLTSurgeText .got_tm24_already
|
||||||
M.VERMILION_GYM.talk = {
|
M.VERMILION_GYM.talk = {
|
||||||
TEXT_VERMILIONGYM_LT_SURGE = leaderTalk("EVENT_BEAT_LT_SURGE",
|
TEXT_VERMILIONGYM_LT_SURGE = leaderTalk("EVENT_BEAT_LT_SURGE",
|
||||||
"_VermilionGymLTSurgePostBattleAdviceText",
|
"_VermilionGymLTSurgePostBattleAdviceText",
|
||||||
"A little word of\nadvice, kid!"),
|
"A little word of\nadvice, kid!", nil, "OPP_LT_SURGE#1"),
|
||||||
}
|
}
|
||||||
|
|
||||||
-- scripts/CeladonGym.asm CeladonGymErikaText .afterBeat
|
-- scripts/CeladonGym.asm CeladonGymErikaText .afterBeat
|
||||||
M.CELADON_GYM.talk = {
|
M.CELADON_GYM.talk = {
|
||||||
TEXT_CELADONGYM_ERIKA = leaderTalk("EVENT_BEAT_ERIKA",
|
TEXT_CELADONGYM_ERIKA = leaderTalk("EVENT_BEAT_ERIKA",
|
||||||
"_CeladonGymErikaPostBattleAdviceText",
|
"_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
|
-- scripts/FuchsiaGym.asm FuchsiaGymKogaText .afterBeat
|
||||||
M.FUCHSIA_GYM.talk = {
|
M.FUCHSIA_GYM.talk = {
|
||||||
TEXT_FUCHSIAGYM_KOGA = leaderTalk("EVENT_BEAT_KOGA",
|
TEXT_FUCHSIAGYM_KOGA = leaderTalk("EVENT_BEAT_KOGA",
|
||||||
"_FuchsiaGymKogaPostBattleAdviceText",
|
"_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
|
-- scripts/SaffronGym.asm SaffronGymSabrinaText .afterBeat
|
||||||
M.SAFFRON_GYM.talk = {
|
M.SAFFRON_GYM.talk = {
|
||||||
TEXT_SAFFRONGYM_SABRINA = leaderTalk("EVENT_BEAT_SABRINA",
|
TEXT_SAFFRONGYM_SABRINA = leaderTalk("EVENT_BEAT_SABRINA",
|
||||||
"_SaffronGymSabrinaPostBattleAdviceText",
|
"_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
|
-- scripts/CinnabarGym.asm CinnabarGymBlaineText .afterBeat
|
||||||
M.CINNABAR_GYM.talk = {
|
M.CINNABAR_GYM.talk = {
|
||||||
TEXT_CINNABARGYM_BLAINE = leaderTalk("EVENT_BEAT_BLAINE",
|
TEXT_CINNABARGYM_BLAINE = leaderTalk("EVENT_BEAT_BLAINE",
|
||||||
"_CinnabarGymBlainePostBattleAdviceText",
|
"_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
|
-- scripts/ViridianGym.asm ViridianGymGiovanniText .afterBeat: after the
|
||||||
@@ -142,7 +165,7 @@ M.VIRIDIAN_GYM.talk = {
|
|||||||
"VIRIDIAN_GYM", "VIRIDIANGYM_GIOVANNI")
|
"VIRIDIAN_GYM", "VIRIDIANGYM_GIOVANNI")
|
||||||
end
|
end
|
||||||
end, done))
|
end, done))
|
||||||
end),
|
end, "OPP_GIOVANNI#3"),
|
||||||
}
|
}
|
||||||
|
|
||||||
return M
|
return M
|
||||||
|
|||||||
@@ -48,7 +48,8 @@ end
|
|||||||
if GameVersion.isYellow() then
|
if GameVersion.isYellow() then
|
||||||
for _, file in ipairs({ "data.scripts.yellow_gifts",
|
for _, file in ipairs({ "data.scripts.yellow_gifts",
|
||||||
"data.scripts.yellow_jessie_james",
|
"data.scripts.yellow_jessie_james",
|
||||||
"data.scripts.yellow_beach_house" }) do
|
"data.scripts.yellow_beach_house",
|
||||||
|
"data.scripts.yellow_viridian_old_man" }) do
|
||||||
for mapId, mod in pairs(require(file)) do
|
for mapId, mod in pairs(require(file)) do
|
||||||
MapScripts.attachBase(mapId, mod)
|
MapScripts.attachBase(mapId, mod)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -22,10 +22,10 @@ local function starterBall(askText, species, choseFlag, ownBall,
|
|||||||
rivalBallX, rivalBall)
|
rivalBallX, rivalBall)
|
||||||
return {
|
return {
|
||||||
{ "check_flag", "EVENT_GOT_STARTER" }, -- 1
|
{ "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)
|
-- no picking until Oak has walked you in (OaksLabScript gating)
|
||||||
{ "check_flag", "EVENT_FOLLOWED_OAK_INTO_LAB" }, -- 3
|
{ "check_flag", "EVENT_FOLLOWED_OAK_INTO_LAB" }, -- 3
|
||||||
{ "jump_if_false", 22 }, -- 4
|
{ "jump_if_false", 26 }, -- 4
|
||||||
-- the Pokédex "new species" entry shows before the ask (predef
|
-- the Pokédex "new species" entry shows before the ask (predef
|
||||||
-- StarterDex ahead of OaksLabYouWant...Text). StarterDex temporarily
|
-- StarterDex ahead of OaksLabYouWant...Text). StarterDex temporarily
|
||||||
-- sets the owned bits so ShowPokedexData prints height/weight/text;
|
-- sets the owned bits so ShowPokedexData prints height/weight/text;
|
||||||
@@ -34,36 +34,38 @@ local function starterBall(askText, species, choseFlag, ownBall,
|
|||||||
{ species = species, forceOwned = true } }, -- 5
|
{ species = species, forceOwned = true } }, -- 5
|
||||||
{ "ask", askText }, -- 6
|
{ "ask", askText }, -- 6
|
||||||
{ "jump_if_false", "end" }, -- 7
|
{ "jump_if_false", "end" }, -- 7
|
||||||
-- OaksLab.asm prints ReceivedMon then AddPartyMon (AskName lives
|
-- scripts/OaksLab.asm:919
|
||||||
-- inside give_pokemon). Show the received text first so the
|
{ "show_text", "_OaksLabMonEnergeticText" }, -- 8
|
||||||
-- nickname prompt follows "you got X", matching Gen1.
|
-- OaksLab.asm: ReceivedMon (sound_get_key_item) then AddPartyMon; the
|
||||||
{ "show_text", "_OaksLabReceivedMonText", { RAM = species } }, -- 8
|
-- jingle fires once the box has typed and holds it (#668)
|
||||||
{ "give_pokemon", species, 5 }, -- 9
|
{ "text_sound", "Get_Key_Item" }, -- 9
|
||||||
{ "set_flag", "EVENT_GOT_STARTER" }, -- 10
|
{ "show_text", "_OaksLabReceivedMonText", { RAM = species } }, -- 10
|
||||||
{ "set_flag", choseFlag }, -- 11
|
{ "give_pokemon", species, 5 }, -- 11
|
||||||
-- POKé BALLs are not handed out here in the original -- Oak gives
|
{ "set_flag", "EVENT_GOT_STARTER" }, -- 12
|
||||||
-- them later, at OaksLabOak1Text's .give_poke_balls beat once the
|
{ "set_flag", choseFlag }, -- 13
|
||||||
-- player has beaten the Route 22 rival (see TEXT_OAKSLAB_OAK1 below)
|
-- POKé BALLs come later, at OaksLabOak1Text's .give_poke_balls beat
|
||||||
{ "hide_object", "OAKS_LAB", ownBall }, -- 12
|
-- 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)
|
-- the rival walks to the countering ball (around the furniture)
|
||||||
{ "move_npc_to", 1, rivalBallX, 4 }, -- 13
|
{ "move_npc_to", 1, rivalBallX, 4 }, -- 15
|
||||||
{ "face_object", 1, "up" }, -- 14
|
{ "face_object", 1, "up" }, -- 16
|
||||||
{ "show_text", "_OaksLabRivalIllTakeThisOneText" }, -- 15
|
{ "show_text", "_OaksLabRivalIllTakeThisOneText" }, -- 17
|
||||||
{ "hide_object", "OAKS_LAB", rivalBall }, -- 16
|
{ "hide_object", "OAKS_LAB", rivalBall }, -- 18
|
||||||
|
{ "text_sound", "Get_Key_Item" }, -- 19 (sound_get_key_item)
|
||||||
{ "show_text", "_OaksLabRivalReceivedMonText",
|
{ "show_text", "_OaksLabRivalReceivedMonText",
|
||||||
{ RAM = rivalBall == "OAKSLAB_CHARMANDER_POKE_BALL" and "CHARMANDER"
|
{ RAM = rivalBall == "OAKSLAB_CHARMANDER_POKE_BALL" and "CHARMANDER"
|
||||||
or rivalBall == "OAKSLAB_SQUIRTLE_POKE_BALL" and "SQUIRTLE"
|
or rivalBall == "OAKSLAB_SQUIRTLE_POKE_BALL" and "SQUIRTLE"
|
||||||
or "BULBASAUR" } }, -- 17
|
or "BULBASAUR" } }, -- 20
|
||||||
{ "jump", "end" }, -- 18
|
{ "jump", "end" }, -- 21
|
||||||
{ "jump", "end" }, -- 19 (spacer)
|
{ "jump", "end" }, -- 22 (spacer)
|
||||||
-- a leftover ball after the player's pick: Oak turns to face the
|
-- leftover ball: Oak reads the last-mon line (scripts/OaksLab.asm
|
||||||
-- player and reads the last-mon line instead of re-offering the
|
-- OaksLabSelectedPokeBallScript -> OaksLabLastMonScript, #601)
|
||||||
-- starter (scripts/OaksLab.asm OaksLabSelectedPokeBallScript ->
|
{ "face_object", 5, "down" }, -- 23
|
||||||
-- OaksLabLastMonScript; #601). The ROM's "#MON" ligature is spelled
|
{ "show_text", "That's PROF.OAK's\nlast Pokémon!" }, -- 24
|
||||||
-- out as Pokémon here.
|
-- OaksLabLastMonScript ends at TextScriptEnd; the port used to fall
|
||||||
{ "face_object", 5, "down" }, -- 20
|
-- through into the pre-pick line below (#601 remnant, reported on #600)
|
||||||
{ "show_text", "That's PROF.OAK's\nlast Pokémon!" }, -- 21
|
{ "jump", "end" }, -- 25
|
||||||
{ "show_text", "_OaksLabThoseArePokeBallsText" }, -- 22
|
{ "show_text", "_OaksLabThoseArePokeBallsText" }, -- 26
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -71,10 +73,22 @@ return {
|
|||||||
talk = {
|
talk = {
|
||||||
-- Oak: OaksLabOak1Text. Parcel delivery kicks SCRIPT_OAKSLAB_RIVAL_
|
-- Oak: OaksLabOak1Text. Parcel delivery kicks SCRIPT_OAKSLAB_RIVAL_
|
||||||
-- ARRIVES_AT_OAKS_REQUEST + OaksLabOakGivesPokedexScript (rival walk-
|
-- ARRIVES_AT_OAKS_REQUEST + OaksLabOakGivesPokedexScript (rival walk-
|
||||||
-- in, full Pokédex speech, rival exit, Route 22 arm). Dex-rating
|
-- in, full Pokédex speech, rival exit, Route 22 arm).
|
||||||
-- (DisplayDexRating) is still skipped.
|
|
||||||
TEXT_OAKSLAB_OAK1 = {
|
TEXT_OAKSLAB_OAK1 = {
|
||||||
{ "face_player" },
|
{ "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" },
|
{ "check_item", "POKE_BALL" },
|
||||||
{ "jump_if_true", "come_see" },
|
{ "jump_if_true", "come_see" },
|
||||||
{ "check_flag", "EVENT_BEAT_ROUTE22_RIVAL_1ST_BATTLE" },
|
{ "check_flag", "EVENT_BEAT_ROUTE22_RIVAL_1ST_BATTLE" },
|
||||||
@@ -86,8 +100,8 @@ return {
|
|||||||
{ "check_item", "OAKS_PARCEL" },
|
{ "check_item", "OAKS_PARCEL" },
|
||||||
{ "jump_if_false", "raise_young" },
|
{ "jump_if_false", "raise_young" },
|
||||||
-- OaksLabOak1Text.got_parcel → RivalArrives + OakGivesPokedex
|
-- OaksLabOak1Text.got_parcel → RivalArrives + OakGivesPokedex
|
||||||
|
{ "text_sound", "Get_Key_Item" },
|
||||||
{ "show_text", "_OaksLabOak1DeliverParcelText" },
|
{ "show_text", "_OaksLabOak1DeliverParcelText" },
|
||||||
{ "play_sound", "Get_Key_Item" },
|
|
||||||
{ "show_text", "_OaksLabOak1ParcelThanksText" },
|
{ "show_text", "_OaksLabOak1ParcelThanksText" },
|
||||||
{ "take_item", "OAKS_PARCEL", 1 },
|
{ "take_item", "OAKS_PARCEL", 1 },
|
||||||
{ "stop_music" },
|
{ "stop_music" },
|
||||||
@@ -108,8 +122,8 @@ return {
|
|||||||
{ "face_object", 1, "up" },
|
{ "face_object", 1, "up" },
|
||||||
{ "face_object", 5, "down" },
|
{ "face_object", 5, "down" },
|
||||||
{ "show_text", "_OaksLabOakMyInventionPokedexText" },
|
{ "show_text", "_OaksLabOakMyInventionPokedexText" },
|
||||||
|
{ "text_sound", "Get_Key_Item" },
|
||||||
{ "show_text", "_OaksLabOakGotPokedexText" },
|
{ "show_text", "_OaksLabOakGotPokedexText" },
|
||||||
{ "play_sound", "Get_Key_Item" },
|
|
||||||
{ "hide_object", "OAKS_LAB", "OAKSLAB_POKEDEX1" },
|
{ "hide_object", "OAKS_LAB", "OAKSLAB_POKEDEX1" },
|
||||||
{ "hide_object", "OAKS_LAB", "OAKSLAB_POKEDEX2" },
|
{ "hide_object", "OAKS_LAB", "OAKSLAB_POKEDEX2" },
|
||||||
{ "face_object", 1, "up" },
|
{ "face_object", 1, "up" },
|
||||||
@@ -160,6 +174,14 @@ return {
|
|||||||
|
|
||||||
{ "label", "come_see" },
|
{ "label", "come_see" },
|
||||||
{ "show_text", "_OaksLabOak1ComeSeeMeSometimesText" },
|
{ "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 =
|
TEXT_OAKSLAB_CHARMANDER_POKE_BALL =
|
||||||
@@ -218,9 +240,17 @@ return {
|
|||||||
-- the table sprites; re-entering the lab applies the same HideObject
|
-- the table sprites; re-entering the lab applies the same HideObject
|
||||||
-- the gift script now does (OaksLab.asm OakGivesPokedex).
|
-- the gift script now does (OaksLab.asm OakGivesPokedex).
|
||||||
onEnter = function(game, ow)
|
onEnter = function(game, ow)
|
||||||
if not (game.save.flags and game.save.flags.EVENT_GOT_POKEDEX) then
|
local flags = game.save.flags or {}
|
||||||
return
|
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
|
end
|
||||||
|
if not flags.EVENT_GOT_POKEDEX then return end
|
||||||
local Commands = require("src.script.Commands")
|
local Commands = require("src.script.Commands")
|
||||||
local ctx = { save = game.save, game = game, overworld = ow }
|
local ctx = { save = game.save, game = game, overworld = ow }
|
||||||
Commands.hide_object(ctx, "OAKS_LAB", "OAKSLAB_POKEDEX1")
|
Commands.hide_object(ctx, "OAKS_LAB", "OAKSLAB_POKEDEX1")
|
||||||
@@ -254,7 +284,14 @@ return {
|
|||||||
and y >= 6 then
|
and y >= 6 then
|
||||||
local rival = ow:npcByIndex(1)
|
local rival = ow:npcByIndex(1)
|
||||||
if not rival then return false end
|
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 = {
|
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
|
{ "show_text", "_OaksLabRivalIllTakeYouOnText" }, -- 1
|
||||||
}
|
}
|
||||||
-- the rival routes to a free cell beside the player
|
-- the rival routes to a free cell beside the player
|
||||||
@@ -276,24 +313,22 @@ return {
|
|||||||
local base = #rows
|
local base = #rows
|
||||||
local party = flags.EVENT_CHOSE_BULBASAUR and 3
|
local party = flags.EVENT_CHOSE_BULBASAUR and 3
|
||||||
or flags.EVENT_CHOSE_SQUIRTLE and 2 or 1
|
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 })
|
table.insert(rows, { "start_battle", "trainer", "OPP_RIVAL1", party })
|
||||||
-- OaksLabRivalEndBattleScript: heal + flag on win or loss; no blackout
|
-- OaksLabRivalEndBattleScript: heal + flag on win or loss; no blackout
|
||||||
table.insert(rows, { "heal_party" })
|
table.insert(rows, { "heal_party" })
|
||||||
table.insert(rows, { "set_flag", "EVENT_BATTLED_RIVAL_IN_OAKS_LAB" })
|
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, { "jump_if_false", base + 6 })
|
||||||
table.insert(rows, { "show_text", "_OaksLabRivalIPickedTheWrongPokemonText" })
|
|
||||||
table.insert(rows, { "show_text", "_OaksLabRivalSmellYouLaterText" })
|
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, { "move_npc_to", 1, 4, 11 })
|
||||||
table.insert(rows, { "hide_object", "OAKS_LAB", "OAKSLAB_RIVAL" })
|
table.insert(rows, { "hide_object", "OAKS_LAB", "OAKSLAB_RIVAL" })
|
||||||
|
table.insert(rows, { "play_music", "Music_OaksLab" })
|
||||||
ow.runner:run(rows, { npc = rival })
|
ow.runner:run(rows, { npc = rival })
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -38,13 +38,15 @@ return {
|
|||||||
{ "jump_if_false", "raise_young" },
|
{ "jump_if_false", "raise_young" },
|
||||||
-- .DeliverParcelText: parcel handover, then the Pokédex scene
|
-- .DeliverParcelText: parcel handover, then the Pokédex scene
|
||||||
-- (OaksLabRivalArrivesAtOaksRequestScript -> OakGivesPokedexScript)
|
-- (OaksLabRivalArrivesAtOaksRequestScript -> OakGivesPokedexScript)
|
||||||
|
{ "text_sound", "Get_Key_Item" },
|
||||||
{ "show_text", "_OaksLabOak1DeliverParcelText" },
|
{ "show_text", "_OaksLabOak1DeliverParcelText" },
|
||||||
{ "play_sound", "Get_Key_Item" },
|
|
||||||
{ "show_text", "_OaksLabOak1ParcelThanksText" },
|
{ "show_text", "_OaksLabOak1ParcelThanksText" },
|
||||||
{ "take_item", "OAKS_PARCEL", 1 },
|
{ "take_item", "OAKS_PARCEL", 1 },
|
||||||
{ "stop_music" },
|
{ "stop_music" },
|
||||||
{ "play_music", "Music_MeetRival" },
|
{ "play_music", "Music_MeetRival" },
|
||||||
{ "show_text", "_OaksLabRivalGrampsText" },
|
{ "show_text", "_OaksLabRivalGrampsText" },
|
||||||
|
-- callfar OaksLabPikachuMovementScript, before ShowObject (#1021)
|
||||||
|
{ "pikachu_make_way" },
|
||||||
{ "show_object", "OAKS_LAB", "OAKSLAB_RIVAL" },
|
{ "show_object", "OAKS_LAB", "OAKSLAB_RIVAL" },
|
||||||
{ "place_npc", RIVAL, 4, 7, "up" },
|
{ "place_npc", RIVAL, 4, 7, "up" },
|
||||||
{ "move_npc_to", RIVAL, 4, 3 },
|
{ "move_npc_to", RIVAL, 4, 3 },
|
||||||
@@ -60,8 +62,8 @@ return {
|
|||||||
{ "face_object", RIVAL, "up" },
|
{ "face_object", RIVAL, "up" },
|
||||||
{ "face_object", OAK1, "down" },
|
{ "face_object", OAK1, "down" },
|
||||||
{ "show_text", "_OaksLabOakMyInventionPokedexText" },
|
{ "show_text", "_OaksLabOakMyInventionPokedexText" },
|
||||||
|
{ "text_sound", "Get_Key_Item" },
|
||||||
{ "show_text", "_OaksLabOakGotPokedexText" },
|
{ "show_text", "_OaksLabOakGotPokedexText" },
|
||||||
{ "play_sound", "Get_Key_Item" },
|
|
||||||
{ "hide_object", "OAKS_LAB", "OAKSLAB_POKEDEX1" },
|
{ "hide_object", "OAKS_LAB", "OAKSLAB_POKEDEX1" },
|
||||||
{ "hide_object", "OAKS_LAB", "OAKSLAB_POKEDEX2" },
|
{ "hide_object", "OAKS_LAB", "OAKSLAB_POKEDEX2" },
|
||||||
{ "face_object", RIVAL, "up" },
|
{ "face_object", RIVAL, "up" },
|
||||||
@@ -71,8 +73,12 @@ return {
|
|||||||
{ "show_text", "_OaksLabRivalLeaveItAllToMeText" },
|
{ "show_text", "_OaksLabRivalLeaveItAllToMeText" },
|
||||||
{ "set_flag", "EVENT_GOT_POKEDEX" },
|
{ "set_flag", "EVENT_GOT_POKEDEX" },
|
||||||
{ "set_flag", "EVENT_OAK_GOT_PARCEL" },
|
{ "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" },
|
{ "hide_object", "VIRIDIAN_CITY", "VIRIDIANCITY_OLD_MAN_SLEEPY" },
|
||||||
{ "show_object", "VIRIDIAN_CITY", "VIRIDIANCITY_OLD_MAN" },
|
{ "show_object", "VIRIDIAN_CITY", "VIRIDIANCITY_OLD_MAN2" },
|
||||||
{ "stop_music" },
|
{ "stop_music" },
|
||||||
{ "play_music", "Music_MeetRival" },
|
{ "play_music", "Music_MeetRival" },
|
||||||
{ "move_npc_to", RIVAL, 4, 7 },
|
{ "move_npc_to", RIVAL, 4, 7 },
|
||||||
@@ -107,8 +113,8 @@ return {
|
|||||||
{ "jump_if_true", "come_see" },
|
{ "jump_if_true", "come_see" },
|
||||||
{ "set_flag", "EVENT_GOT_POKEBALLS_FROM_OAK" },
|
{ "set_flag", "EVENT_GOT_POKEBALLS_FROM_OAK" },
|
||||||
{ "give_item", "POKE_BALL", 5, false },
|
{ "give_item", "POKE_BALL", 5, false },
|
||||||
|
{ "text_sound", "Get_Key_Item" },
|
||||||
{ "show_text", "_OaksLabOak1ReceivedPokeballsText" },
|
{ "show_text", "_OaksLabOak1ReceivedPokeballsText" },
|
||||||
{ "play_sound", "Get_Key_Item" },
|
|
||||||
{ "show_text", "_OaksLabGivePokeballsExplanationText" },
|
{ "show_text", "_OaksLabGivePokeballsExplanationText" },
|
||||||
{ "jump", "end" },
|
{ "jump", "end" },
|
||||||
|
|
||||||
@@ -170,7 +176,7 @@ return {
|
|||||||
-- rival starter baseline (RIVAL_STARTER_JOLTEON) at snatch time
|
-- rival starter baseline (RIVAL_STARTER_JOLTEON) at snatch time
|
||||||
rows[#rows + 1] = { "set_field", "rivalStarter", 1 }
|
rows[#rows + 1] = { "set_field", "rivalStarter", 1 }
|
||||||
rows[#rows + 1] = { "show_text", "_OaksLabRivalTakesText1" }
|
rows[#rows + 1] = { "show_text", "_OaksLabRivalTakesText1" }
|
||||||
rows[#rows + 1] = { "play_sound", "Get_Key_Item" }
|
rows[#rows + 1] = { "text_sound", "Get_Key_Item" }
|
||||||
rows[#rows + 1] = { "show_text", "_OaksLabRivalTakesText2" }
|
rows[#rows + 1] = { "show_text", "_OaksLabRivalTakesText2" }
|
||||||
rows[#rows + 1] = { "show_text", "_OaksLabRivalTakesText3" }
|
rows[#rows + 1] = { "show_text", "_OaksLabRivalTakesText3" }
|
||||||
rows[#rows + 1] = { "show_text", "_OaksLabRivalTakesText4" }
|
rows[#rows + 1] = { "show_text", "_OaksLabRivalTakesText4" }
|
||||||
@@ -188,15 +194,17 @@ return {
|
|||||||
end
|
end
|
||||||
rows[#rows + 1] = { "face_player_dir", "up" }
|
rows[#rows + 1] = { "face_player_dir", "up" }
|
||||||
rows[#rows + 1] = { "face_object", OAK1, "down" }
|
rows[#rows + 1] = { "face_object", OAK1, "down" }
|
||||||
-- OaksLabPlayerReceivedMonText: no nickname prompt -- the starter
|
-- OaksLabPlayerReceivedMonText clears wMonDataLocation, so AskName runs (#1013)
|
||||||
-- Pikachu keeps its species name
|
|
||||||
rows[#rows + 1] = { "show_text", "_OaksLabOakGivesText" }
|
rows[#rows + 1] = { "show_text", "_OaksLabOakGivesText" }
|
||||||
rows[#rows + 1] = { "play_sound", "Get_Key_Item" }
|
rows[#rows + 1] = { "text_sound", "Get_Key_Item" }
|
||||||
rows[#rows + 1] = { "show_text", "_OaksLabReceivedText", { RAM = "PIKACHU" } }
|
rows[#rows + 1] = { "show_text", "_OaksLabReceivedText", { RAM = "PIKACHU" } }
|
||||||
rows[#rows + 1] = { "give_pokemon", "PIKACHU", 5, true }
|
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_GOT_STARTER" }
|
||||||
rows[#rows + 1] = { "set_flag", "EVENT_CHOSE_PIKACHU" }
|
rows[#rows + 1] = { "set_flag", "EVENT_CHOSE_PIKACHU" }
|
||||||
ow.runner:run(rows, { npc = npc, onDone = done })
|
ow.runner:run(rows, { npc = npc, onDone = done,
|
||||||
|
checkpointOnDone = "release_npc" })
|
||||||
end,
|
end,
|
||||||
|
|
||||||
TEXT_OAKSLAB_RIVAL = {
|
TEXT_OAKSLAB_RIVAL = {
|
||||||
@@ -218,9 +226,15 @@ return {
|
|||||||
},
|
},
|
||||||
|
|
||||||
onEnter = function(game, ow)
|
onEnter = function(game, ow)
|
||||||
if not (game.save.flags and game.save.flags.EVENT_GOT_POKEDEX) then
|
local flags = game.save.flags or {}
|
||||||
return
|
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
|
end
|
||||||
|
if not flags.EVENT_GOT_POKEDEX then return end
|
||||||
local Commands = require("src.script.Commands")
|
local Commands = require("src.script.Commands")
|
||||||
local ctx = { save = game.save, game = game, overworld = ow }
|
local ctx = { save = game.save, game = game, overworld = ow }
|
||||||
Commands.hide_object(ctx, "OAKS_LAB", "OAKSLAB_POKEDEX1")
|
Commands.hide_object(ctx, "OAKS_LAB", "OAKSLAB_POKEDEX1")
|
||||||
@@ -249,6 +263,7 @@ return {
|
|||||||
local rival = ow:npcByIndex(RIVAL)
|
local rival = ow:npcByIndex(RIVAL)
|
||||||
if not rival then return false end
|
if not rival then return false end
|
||||||
local rows = {
|
local rows = {
|
||||||
|
{ "face_object", RIVAL, "down" }, -- pokeyellow scripts/OaksLab.asm:311-315
|
||||||
{ "face_player_dir", "up" },
|
{ "face_player_dir", "up" },
|
||||||
{ "stop_music" },
|
{ "stop_music" },
|
||||||
{ "play_music", "Music_MeetRival" },
|
{ "play_music", "Music_MeetRival" },
|
||||||
@@ -283,16 +298,19 @@ return {
|
|||||||
table.insert(rows, { "label", "lost_lab" })
|
table.insert(rows, { "label", "lost_lab" })
|
||||||
table.insert(rows, { "set_field", "rivalStarter", 3 })
|
table.insert(rows, { "set_field", "rivalStarter", 3 })
|
||||||
table.insert(rows, { "label", "exit" })
|
table.insert(rows, { "label", "exit" })
|
||||||
-- OaksLabRivalStartsExitScript: parting shot, walk out past the
|
-- OaksLabRivalStartsExitScript: parting shot, rival exit fanfare, then
|
||||||
-- player, restore the lab theme
|
-- walk out past the player (#683).
|
||||||
table.insert(rows, { "wait", 20 })
|
table.insert(rows, { "wait", 20 })
|
||||||
table.insert(rows, { "show_text", "_OaksLabRivalSmellYouLaterText" })
|
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, { "move_npc_to", RIVAL, 4, 11 })
|
||||||
table.insert(rows, { "hide_object", "OAKS_LAB", "OAKSLAB_RIVAL" })
|
table.insert(rows, { "hide_object", "OAKS_LAB", "OAKSLAB_RIVAL" })
|
||||||
table.insert(rows, { "play_music", "Music_OaksLab" })
|
table.insert(rows, { "play_music", "Music_OaksLab" })
|
||||||
-- OaksLabPikachuEscapesPokeballScript: Pikachu hates its ball.
|
-- OaksLabPikachuEscapesPokeballScript: the follower reaches the map (#1009)
|
||||||
-- The overworld follower itself is still an open port
|
table.insert(rows, { "face_player_dir", "up" })
|
||||||
-- (docs/yellow-version.md runtime backlog); the story beat plays.
|
table.insert(rows, { "set_field", "pikachuInBall", false })
|
||||||
|
table.insert(rows, { "spawn_pikachu_follower" })
|
||||||
table.insert(rows, { "play_cry", "PIKACHU" })
|
table.insert(rows, { "play_cry", "PIKACHU" })
|
||||||
table.insert(rows, { "show_text", "_OaksLabPikachuDislikesPokeballsText1" })
|
table.insert(rows, { "show_text", "_OaksLabPikachuDislikesPokeballsText1" })
|
||||||
table.insert(rows, { "show_text", "_OaksLabPikachuDislikesPokeballsText2" })
|
table.insert(rows, { "show_text", "_OaksLabPikachuDislikesPokeballsText2" })
|
||||||
|
|||||||
@@ -4,9 +4,11 @@
|
|||||||
-- (.PlayerNextToSafariZoneWorker1CoordsArray). Paying ¥500 hands over
|
-- (.PlayerNextToSafariZoneWorker1CoordsArray). Paying ¥500 hands over
|
||||||
-- 30 SAFARI BALLs and starts the 502-step game
|
-- 30 SAFARI BALLs and starts the 502-step game
|
||||||
-- (SafariZoneGateWouldYouLikeToJoinScript: wSafariSteps = 502,
|
-- (SafariZoneGateWouldYouLikeToJoinScript: wSafariSteps = 502,
|
||||||
-- wNumSafariBalls = SAFARI_BALLS_RECEIVED). Declining walks you back
|
-- wNumSafariBalls = SAFARI_BALLS_RECEIVED), then auto-walks the player
|
||||||
-- so you can't slip past. Returning to the gate ends the game and the
|
-- up through the north warp into the zone. Declining walks you back
|
||||||
-- worker takes the leftover balls 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
|
-- Step/ball bookkeeping lives in src/world/OverworldController.lua
|
||||||
-- (safariStep/safariGameOver, from
|
-- (safariStep/safariGameOver, from
|
||||||
@@ -19,7 +21,33 @@ local FEE = 500
|
|||||||
local BALLS = 30
|
local BALLS = 30
|
||||||
local STEPS = 502
|
local STEPS = 502
|
||||||
|
|
||||||
local function startGame(game, t, done, balls, introText)
|
-- 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.safari = { balls = balls or BALLS, steps = STEPS }
|
||||||
game.save.safariNags = nil
|
game.save.safariNags = nil
|
||||||
local TextBox = require("src.render.TextBox")
|
local TextBox = require("src.render.TextBox")
|
||||||
@@ -31,7 +59,10 @@ local function startGame(game, t, done, balls, introText)
|
|||||||
local pa = t._SafariZoneGateSafariZoneWorker1CallYouOnThePAText
|
local pa = t._SafariZoneGateSafariZoneWorker1CallYouOnThePAText
|
||||||
or "\fWe'll call you on\nthe PA when you\nrun out of time\nor SAFARI BALLs!"
|
or "\fWe'll call you on\nthe PA when you\nrun out of time\nor SAFARI BALLs!"
|
||||||
local luck = t._SafariZoneGateSafariZoneWorker1GoodLuckText or "Good Luck!"
|
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
|
end
|
||||||
|
|
||||||
-- Yellow's soft-lock fix (scripts/SafariZoneGate_2.asm): a player short of
|
-- Yellow's soft-lock fix (scripts/SafariZoneGate_2.asm): a player short of
|
||||||
@@ -52,7 +83,7 @@ local function yellowLowCost(game, ow, t, done, back)
|
|||||||
or "\fOh, all right, pay\nme what you have.")
|
or "\fOh, all right, pay\nme what you have.")
|
||||||
.. "\f" .. (t._SafariZoneLowCostText2
|
.. "\f" .. (t._SafariZoneLowCostText2
|
||||||
or "But, I can't give\nyou all 30 BALLs.")
|
or "But, I can't give\nyou all 30 BALLs.")
|
||||||
startGame(game, t, done, balls, intro)
|
startGame(game, ow, t, done, balls, intro)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
local nag = game.save.safariNags or 0
|
local nag = game.save.safariNags or 0
|
||||||
@@ -62,7 +93,7 @@ local function yellowLowCost(game, ow, t, done, back)
|
|||||||
(t._SafariZoneLowCostText8 or "Read my lips, NO!\nGet it?")
|
(t._SafariZoneLowCostText8 or "Read my lips, NO!\nGet it?")
|
||||||
.. (t._SafariZoneLowCostText3
|
.. (t._SafariZoneLowCostText3
|
||||||
or "\fYou're persistent,\naren't you?\fOK, you can go in\nfor free, but\njust this once!")
|
or "\fYou're persistent,\naren't you?\fOK, you can go in\nfor free, but\njust this once!")
|
||||||
startGame(game, t, done, 1, intro)
|
startGame(game, ow, t, done, 1, intro)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
local nags = {
|
local nags = {
|
||||||
@@ -100,7 +131,7 @@ local function joinPrompt(game, ow, done)
|
|||||||
end
|
end
|
||||||
else
|
else
|
||||||
game.save.money = game.save.money - FEE
|
game.save.money = game.save.money - FEE
|
||||||
startGame(game, t, done)
|
startGame(game, ow, t, done)
|
||||||
end
|
end
|
||||||
end))
|
end))
|
||||||
end))
|
end))
|
||||||
@@ -135,27 +166,38 @@ M.SAFARI_ZONE_GATE = {
|
|||||||
-- no walks you back into the zone
|
-- no walks you back into the zone
|
||||||
onEnter = function(game, ow)
|
onEnter = function(game, ow)
|
||||||
if not game.save.safari or ow.player.cellY > 1 then return end
|
if not game.save.safari or ow.player.cellY > 1 then return end
|
||||||
local TextBox = require("src.render.TextBox")
|
-- QUEUED, never pushed: onEnter runs inside the arriving warp's
|
||||||
local ChoiceBox = require("src.ui.ChoiceBox")
|
-- Transition midpoint, and Transition:finish pops whatever is on top
|
||||||
local t = game.data.text
|
-- the same frame (Timing.WARP_FADE_IN is 0) -- so a box pushed here is
|
||||||
game.stack:push(TextBox.new(game,
|
-- swallowed, and on a build where it survived it drew over a screen
|
||||||
t._SafariZoneGateSafariZoneWorker1LeavingEarlyText or "Leaving early?",
|
-- still faded to black (#540). Same contract as M.HALL_OF_FAME in
|
||||||
function()
|
-- data/scripts/story.lua.
|
||||||
game.stack:push(ChoiceBox.new(game, function(yes)
|
--
|
||||||
if not yes then
|
-- SafariZoneGateLeavingSafariScript .leaving_early: YES prints the
|
||||||
-- back into the zone through the entrance warp
|
-- return-balls text, faces the player down and runs
|
||||||
local w = game.data.maps.SAFARI_ZONE_CENTER.warps[1]
|
-- SafariZoneEntranceAutoWalk with `PAD_DOWN, c = 3`, landing on the
|
||||||
ow:startWarpTo("SAFARI_ZONE_CENTER", w.x, w.y, "up")
|
-- counter row 3 cells below the warp you came in by; NO prints
|
||||||
return
|
-- "Good Luck!" and walks one step back up through that same warp.
|
||||||
end
|
local rightSide = ow.player.cellX ~= 3
|
||||||
game.save.safari = nil
|
local dest = game.data.maps.SAFARI_ZONE_CENTER.warps[rightSide and 2 or 1]
|
||||||
game.stack:push(TextBox.new(game,
|
ow:queueScript({
|
||||||
(t._SafariZoneGateSafariZoneWorker1ReturnSafariBallsText
|
{ "ask", "_SafariZoneGateSafariZoneWorker1LeavingEarlyText" },
|
||||||
or "Please return any\nSAFARI BALLs you\nhave left.")
|
{ "jump_if_false", "stay" },
|
||||||
.. "\f" .. (t._SafariZoneGateSafariZoneWorker1GoodHaulComeAgainText
|
-- the port never reaches SafariZoneGateLeavingSafariScript's own
|
||||||
or "Did you get a\ngood haul?\fCome again!")))
|
-- GOOD_HAUL_COME_AGAIN branch (safariGameOver warps straight to the
|
||||||
end))
|
-- counter), so the sign-off rides on this path
|
||||||
end))
|
{ "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,
|
end,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
-- field.seafoam (SEAFOAM_ISLANDS_1F/B1F/B3F holes+holeDestination and
|
-- field.seafoam (SEAFOAM_ISLANDS_1F/B1F/B3F holes+holeDestination and
|
||||||
-- B3F's pluggedByHolesOn) plus the generic
|
-- B3F's pluggedByHolesOn) plus the generic
|
||||||
-- OverworldState:boulderIntoHole in src/world/OverworldController.lua;
|
-- 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 = {}
|
local M = {}
|
||||||
|
|
||||||
@@ -23,4 +23,46 @@ M.VERMILION_GYM = {
|
|||||||
end,
|
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
|
return M
|
||||||
|
|||||||
@@ -140,22 +140,29 @@ M.VIRIDIAN_CITY = {
|
|||||||
-- Daisy hands over the TOWN MAP once Oak's errand is under way
|
-- Daisy hands over the TOWN MAP once Oak's errand is under way
|
||||||
-- (scripts/BluesHouse.asm BluesHouseDaisySittingText)
|
-- (scripts/BluesHouse.asm BluesHouseDaisySittingText)
|
||||||
M.BLUES_HOUSE = {
|
M.BLUES_HOUSE = {
|
||||||
|
-- scripts/BluesHouse.asm:12-16
|
||||||
|
onEnter = function(game, ow)
|
||||||
|
game.save.flags.EVENT_ENTERED_BLUES_HOUSE = true
|
||||||
|
end,
|
||||||
talk = {
|
talk = {
|
||||||
TEXT_BLUESHOUSE_DAISY_SITTING = {
|
TEXT_BLUESHOUSE_DAISY_SITTING = {
|
||||||
{ "face_player" }, -- 1
|
{ "face_player" },
|
||||||
{ "check_flag", "EVENT_GOT_TOWN_MAP" }, -- 2
|
{ "check_flag", "EVENT_GOT_TOWN_MAP" },
|
||||||
{ "jump_if_true", 10 }, -- 3
|
{ "jump_if_true", "got_map" },
|
||||||
{ "check_flag", "EVENT_GOT_STARTER" }, -- 4
|
{ "check_flag", "EVENT_GOT_POKEDEX" },
|
||||||
{ "jump_if_false", 12 }, -- 5
|
{ "jump_if_false", "too_early" },
|
||||||
{ "show_text", "_BluesHouseDaisyOfferMapText" }, -- 6
|
{ "show_text", "_BluesHouseDaisyOfferMapText" },
|
||||||
-- _GotMapText: "{PLAYER} got a\n{RAM:wStringBuffer}!" -- the
|
-- _GotMapText: "{PLAYER} got a\n{RAM:wStringBuffer}!" -- the
|
||||||
-- buffer supplies "TOWN MAP" (scripts/BluesHouse.asm GotMapText)
|
-- buffer supplies "TOWN MAP" (scripts/BluesHouse.asm GotMapText)
|
||||||
{ "give_item", "TOWN_MAP", 1, "_GotMapText" }, -- 7
|
{ "give_item", "TOWN_MAP", 1, "_GotMapText" },
|
||||||
{ "set_flag", "EVENT_GOT_TOWN_MAP" }, -- 8
|
{ "hide_object", "BLUES_HOUSE", "BLUESHOUSE_TOWN_MAP" },
|
||||||
{ "jump", 13 }, -- 9
|
{ "set_flag", "EVENT_GOT_TOWN_MAP" },
|
||||||
{ "show_text", "_BluesHouseDaisyUseMapText" }, -- 10
|
{ "jump", "end" },
|
||||||
{ "jump", 13 }, -- 11
|
{ "label", "got_map" },
|
||||||
{ "show_text", "_BluesHouseDaisyRivalAtLabText" }, -- 12
|
{ "show_text", "_BluesHouseDaisyUseMapText" },
|
||||||
|
{ "jump", "end" },
|
||||||
|
{ "label", "too_early" },
|
||||||
|
{ "show_text", "_BluesHouseDaisyRivalAtLabText" },
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -188,7 +195,12 @@ M.BILLS_HOUSE = {
|
|||||||
end
|
end
|
||||||
if ow.player.facing == "down" then
|
if ow.player.facing == "down" then
|
||||||
-- the player is standing on his straight path: walk around
|
-- 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, "right", 1, function()
|
||||||
ow:scriptMove(npc, "up", 2, function()
|
ow:scriptMove(npc, "up", 2, function()
|
||||||
ow:scriptMove(npc, "left", 1, function()
|
ow:scriptMove(npc, "left", 1, function()
|
||||||
@@ -289,6 +301,7 @@ M.BILLS_HOUSE = {
|
|||||||
|
|
||||||
M.ROUTE_25 = {
|
M.ROUTE_25 = {
|
||||||
onEnter = function(game, ow)
|
onEnter = function(game, ow)
|
||||||
|
game.save.pikachuMapScriptActive = nil
|
||||||
local flags = game.save.flags
|
local flags = game.save.flags
|
||||||
if flags.EVENT_LEFT_BILLS_HOUSE_AFTER_HELPING then return end
|
if flags.EVENT_LEFT_BILLS_HOUSE_AFTER_HELPING then return end
|
||||||
local Commands = require("src.script.Commands")
|
local Commands = require("src.script.Commands")
|
||||||
@@ -321,6 +334,7 @@ M.VERMILION_CITY = {
|
|||||||
-- only read while EVENT_1ST_LOCK_OPENED is unset (the gym is only
|
-- only read while EVENT_1ST_LOCK_OPENED is unset (the gym is only
|
||||||
-- reachable through this map, so a fresh visit always re-rolls).
|
-- reachable through this map, so a fresh visit always re-rolls).
|
||||||
onEnter = function(game, ow)
|
onEnter = function(game, ow)
|
||||||
|
game.save.pikachuMapScriptActive = nil
|
||||||
local puz = game.save.trashPuzzle or {}
|
local puz = game.save.trashPuzzle or {}
|
||||||
game.save.trashPuzzle = puz
|
game.save.trashPuzzle = puz
|
||||||
puz.first = love.math.random(0, 7) * 2
|
puz.first = love.math.random(0, 7) * 2
|
||||||
@@ -600,12 +614,34 @@ local function snorlaxWake(mapId, objName, beatFlag, wokeUpText, calmedText)
|
|||||||
}
|
}
|
||||||
end
|
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
|
-- snorlaxWake is looked up by ItemEffects.lua/BagMenu.lua (via
|
||||||
-- data/scripts/init.lua's M.get) and run when the flute wakes Snorlax;
|
-- 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
|
-- objName/beatFlag let ItemEffects find the NPC and check whether it's
|
||||||
-- already been beaten before allowing the wake.
|
-- already been beaten before allowing the wake.
|
||||||
M.ROUTE_12 = {
|
M.ROUTE_12 = {
|
||||||
talk = { TEXT_ROUTE12_SNORLAX = { { "show_text", "_Route12SnorlaxText" } } },
|
talk = { TEXT_ROUTE12_SNORLAX = { { "show_text", "_Route12SnorlaxText" } } },
|
||||||
|
onEnter = hideBeatenSnorlax("ROUTE_12", "ROUTE12_SNORLAX",
|
||||||
|
"EVENT_BEAT_ROUTE12_SNORLAX"),
|
||||||
snorlaxWake = {
|
snorlaxWake = {
|
||||||
objName = "ROUTE12_SNORLAX", beatFlag = "EVENT_BEAT_ROUTE12_SNORLAX",
|
objName = "ROUTE12_SNORLAX", beatFlag = "EVENT_BEAT_ROUTE12_SNORLAX",
|
||||||
script = snorlaxWake("ROUTE_12", "ROUTE12_SNORLAX", "EVENT_BEAT_ROUTE12_SNORLAX",
|
script = snorlaxWake("ROUTE_12", "ROUTE12_SNORLAX", "EVENT_BEAT_ROUTE12_SNORLAX",
|
||||||
@@ -614,6 +650,8 @@ M.ROUTE_12 = {
|
|||||||
}
|
}
|
||||||
M.ROUTE_16 = {
|
M.ROUTE_16 = {
|
||||||
talk = { TEXT_ROUTE16_SNORLAX = { { "show_text", "_Route16Text7" } } },
|
talk = { TEXT_ROUTE16_SNORLAX = { { "show_text", "_Route16Text7" } } },
|
||||||
|
onEnter = hideBeatenSnorlax("ROUTE_16", "ROUTE16_SNORLAX",
|
||||||
|
"EVENT_BEAT_ROUTE16_SNORLAX"),
|
||||||
snorlaxWake = {
|
snorlaxWake = {
|
||||||
objName = "ROUTE16_SNORLAX", beatFlag = "EVENT_BEAT_ROUTE16_SNORLAX",
|
objName = "ROUTE16_SNORLAX", beatFlag = "EVENT_BEAT_ROUTE16_SNORLAX",
|
||||||
script = snorlaxWake("ROUTE_16", "ROUTE16_SNORLAX", "EVENT_BEAT_ROUTE16_SNORLAX",
|
script = snorlaxWake("ROUTE_16", "ROUTE16_SNORLAX", "EVENT_BEAT_ROUTE16_SNORLAX",
|
||||||
@@ -645,16 +683,18 @@ M.SAFARI_ZONE_SECRET_HOUSE = {
|
|||||||
M.WARDENS_HOUSE = {
|
M.WARDENS_HOUSE = {
|
||||||
talk = {
|
talk = {
|
||||||
TEXT_WARDENSHOUSE_WARDEN = {
|
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
|
{ "face_player" }, -- 1
|
||||||
{ "check_flag", "EVENT_GOT_HM04" }, -- 2
|
{ "check_flag", "EVENT_GOT_HM04" }, -- 2
|
||||||
-- #535: previously jumped to the same silent-end target as the
|
-- #535: previously jumped to the same silent-end target as the
|
||||||
-- give-then-thank fallthrough (row 13), so the warden said nothing
|
-- give-then-thank fallthrough, so the warden said nothing on every
|
||||||
-- on every visit after the trade. pokered's .got_item branch
|
-- visit after the trade. pokered's .got_item branch
|
||||||
-- (scripts/WardensHouse.asm) instead prints HM04ExplanationText,
|
-- (scripts/WardensHouse.asm) instead prints HM04ExplanationText.
|
||||||
-- so route here to the new row 17 that does the same.
|
{ "jump_if_true", "got_hm04" }, -- 3
|
||||||
{ "jump_if_true", 17 }, -- 3
|
|
||||||
{ "check_item", "GOLD_TEETH" }, -- 4
|
{ "check_item", "GOLD_TEETH" }, -- 4
|
||||||
{ "jump_if_false", 15 }, -- 5
|
{ "jump_if_false", "no_teeth" }, -- 5
|
||||||
{ "show_text", "_WardensHouseWardenGaveTheGoldTeethText" }, -- 6
|
{ "show_text", "_WardensHouseWardenGaveTheGoldTeethText" }, -- 6
|
||||||
{ "take_item", "GOLD_TEETH", 1 }, -- 7
|
{ "take_item", "GOLD_TEETH", 1 }, -- 7
|
||||||
{ "set_flag", "EVENT_GAVE_GOLD_TEETH" }, -- 8
|
{ "set_flag", "EVENT_GAVE_GOLD_TEETH" }, -- 8
|
||||||
@@ -663,15 +703,27 @@ M.WARDENS_HOUSE = {
|
|||||||
{ "give_item", "HM_STRENGTH", 1, false }, -- 10
|
{ "give_item", "HM_STRENGTH", 1, false }, -- 10
|
||||||
{ "show_text", "_WardensHouseWardenReceivedHM04Text" }, -- 11
|
{ "show_text", "_WardensHouseWardenReceivedHM04Text" }, -- 11
|
||||||
{ "set_flag", "EVENT_GOT_HM04" }, -- 12
|
{ "set_flag", "EVENT_GOT_HM04" }, -- 12
|
||||||
{ "jump", 18 }, -- 13 (already got it this convo; jp .done)
|
{ "jump", "end" }, -- 13 (jp .done)
|
||||||
{ "jump", 18 }, -- 14 (unused)
|
|
||||||
{ "show_text", "_WardensHouseWardenGibberish1Text" }, -- 15
|
-- #645: WardensHouseWardenText prints Gibberish1, then YesNoChoice,
|
||||||
{ "jump", 18 }, -- 16 (#535: skip the new explanation row below)
|
-- 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) --
|
-- #535: pokered .got_item branch (scripts/WardensHouse.asm) --
|
||||||
-- printed on every subsequent talk once EVENT_GOT_HM04 is set.
|
-- printed on every subsequent talk once EVENT_GOT_HM04 is set.
|
||||||
-- Text is _WardensHouseWardenHM04ExplanationText (text/WardensHouse.asm):
|
-- Text is _WardensHouseWardenHM04ExplanationText (text/WardensHouse.asm):
|
||||||
-- HM04 teaches Strength, and hints at the Safari Zone secret house.
|
-- HM04 teaches Strength, and hints at the Safari Zone secret house.
|
||||||
{ "show_text", "_WardensHouseWardenHM04ExplanationText" }, -- 17
|
{ "label", "got_hm04" }, -- 22
|
||||||
|
{ "show_text", "_WardensHouseWardenHM04ExplanationText" }, -- 23
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -723,6 +775,28 @@ local function silphRocketsLeave(game, ow, onlyMap)
|
|||||||
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 = {
|
M.SILPH_CO_11F = {
|
||||||
-- Giovanni's battle is a COORDINATE TRIGGER, not a talk.
|
-- Giovanni's battle is a COORDINATE TRIGGER, not a talk.
|
||||||
-- SilphCo11FDefaultScript (scripts/SilphCo11F.asm) checks
|
-- SilphCo11FDefaultScript (scripts/SilphCo11F.asm) checks
|
||||||
@@ -735,9 +809,13 @@ M.SILPH_CO_11F = {
|
|||||||
-- line) would touch, and the whole Silph ending -- the flag, the Master
|
-- line) would touch, and the whole Silph ending -- the flag, the Master
|
||||||
-- Ball, the Saffron streets clearing -- silently never happened.
|
-- Ball, the Saffron streets clearing -- silently never happened.
|
||||||
--
|
--
|
||||||
-- engageTrainer shows TEXT_SILPHCO11F_GIOVANNI as the battle text and,
|
-- SilphCo11FDefaultScript orders it DisplayTextID TEXT_SILPHCO11F_GIOVANNI
|
||||||
-- via victories.lua OPP_GIOVANNI#2, sets the event on a win; a loss
|
-- FIRST, then MoveSprite .GiovanniMovement: he speaks from behind the desk
|
||||||
-- sets nothing, so the trigger re-arms exactly as vanilla does.
|
-- 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)
|
onStep = function(game, ow, x, y)
|
||||||
if game.save.flags.EVENT_BEAT_SILPH_CO_GIOVANNI then return false end
|
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
|
if not ((x == 6 and y == 13) or (x == 7 and y == 12)) then return false end
|
||||||
@@ -746,17 +824,28 @@ M.SILPH_CO_11F = {
|
|||||||
if npc.def and npc.def.name == "SILPHCO11F_GIOVANNI" then gio = npc break end
|
if npc.def and npc.def.name == "SILPHCO11F_GIOVANNI" then gio = npc break end
|
||||||
end
|
end
|
||||||
if not gio or ow:trainerDefeated(gio) then return false end
|
if not gio or ow:trainerDefeated(gio) then return false end
|
||||||
ow:scriptMove(gio, "down", 3, function()
|
local TextBox = require("src.render.TextBox")
|
||||||
gio:facePlayer(ow.player)
|
game.stack:push(TextBox.new(game,
|
||||||
ow:engageTrainer(gio, function()
|
game.data.text._SilphCo11FGiovanniText
|
||||||
-- SilphCo11FTeamRocketLeavesScript: every Silph rocket leaves
|
or "Ah {PLAYER}!\nSo we meet again!",
|
||||||
-- after the loss (the street rockets are handled by
|
function()
|
||||||
-- M.SAFFRON_CITY.onEnter in story4.lua).
|
ow:scriptMove(gio, "down", 3, function()
|
||||||
if game.save.flags.EVENT_BEAT_SILPH_CO_GIOVANNI then
|
gio:facePlayer(ow.player)
|
||||||
silphRocketsLeave(game, ow)
|
ow:engageTrainer(gio, function()
|
||||||
end
|
-- SilphCo11FGiovanniAfterBattleScript: the "Blast it all!"
|
||||||
end)
|
-- speech, then SilphCo11FTeamRocketLeavesScript behind a fade so
|
||||||
end)
|
-- 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
|
return true
|
||||||
end,
|
end,
|
||||||
onEnter = function(game, ow)
|
onEnter = function(game, ow)
|
||||||
@@ -899,6 +988,7 @@ M.VICTORY_ROAD_3F = {
|
|||||||
-- fall is onStep, not a collision block.
|
-- fall is onStep, not a collision block.
|
||||||
onStep = function(game, ow, x, y)
|
onStep = function(game, ow, x, y)
|
||||||
if x == 23 and y == 15 then
|
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)
|
ow:startWarpTo("VICTORY_ROAD_2F", 22, 16, ow.player.facing)
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
@@ -937,40 +1027,63 @@ M.VICTORY_ROAD_3F = {
|
|||||||
local championsRoomRivalScript = {
|
local championsRoomRivalScript = {
|
||||||
{ "face_player" }, -- 1
|
{ "face_player" }, -- 1
|
||||||
{ "check_flag", "EVENT_BEAT_CHAMPION_RIVAL_THIS_RUN" }, -- 2
|
{ "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
|
{ "show_text", "_ChampionsRoomRivalIntroText" }, -- 4
|
||||||
{ "rival_battle", "OPP_RIVAL3", 1 }, -- 5
|
-- ChampionsRoomRivalReadyToBattleScript plays MUSIC_FINAL_BATTLE after
|
||||||
{ "jump_if_false", 25 }, -- 6 past end
|
-- the intro text, before the battle itself (#706); pushBattle's wipe-time
|
||||||
{ "set_flag", "EVENT_BEAT_CHAMPION_RIVAL_THIS_RUN" }, -- 7
|
-- playBattle("final") then no-ops on the same song, so the theme stays
|
||||||
{ "set_flag", "EVENT_BEAT_CHAMPION_RIVAL" }, -- 8
|
-- 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,
|
-- ChampionsRoomRivalDefeatedScript re-displays TEXT_CHAMPIONSROOM_RIVAL,
|
||||||
-- whose text_asm takes the EVENT_BEAT_CHAMPION_RIVAL branch =
|
-- whose text_asm takes the EVENT_BEAT_CHAMPION_RIVAL branch =
|
||||||
-- _ChampionsRoomRivalAfterBattleText (the in-battle _RivalDefeatedText
|
-- _ChampionsRoomRivalAfterBattleText (the in-battle _RivalDefeatedText
|
||||||
-- is the port's generic "<PLAYER> defeated BLUE!" engine line instead).
|
-- is the port's generic "<PLAYER> defeated BLUE!" engine line instead).
|
||||||
{ "show_text", "_ChampionsRoomRivalAfterBattleText" }, -- 9
|
{ "show_text", "_ChampionsRoomRivalAfterBattleText" }, -- 10
|
||||||
-- ChampionsRoomOakArrivesScript: Music_Cities1AlternateTempo
|
-- ChampionsRoomOakArrivesScript: Music_Cities1AlternateTempo
|
||||||
-- (Cities1, kept into HALL_OF_FAME like BIT_NO_MAP_MUSIC after
|
-- (Cities1, kept into HALL_OF_FAME like BIT_NO_MAP_MUSIC after
|
||||||
-- defeating RIVAL3), then Oak's "{PLAYER}!" + reveal + walk in
|
-- defeating RIVAL3), then Oak's "{PLAYER}!" + reveal + walk in.
|
||||||
{ "play_music", "Music_Cities1", { keep = true } }, -- 10
|
-- audio/alternate_tempo.asm Music_Cities1AlternateTempo is not a plain
|
||||||
{ "show_text", "_ChampionsRoomOakText" }, -- 11
|
-- PlayMusic: it fades the current song out (wAudioFadeOutControl = 10),
|
||||||
{ "show_object", "CHAMPIONS_ROOM", "CHAMPIONSROOM_OAK" }, -- 12
|
-- waits 100 frames for the fade, then restarts Cities1 with channel 1
|
||||||
{ "move_npc", 2, "up", 5 }, -- 13 OakEntranceAfterVictoryMovement
|
-- 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
|
-- OakCongratulatesPlayerScript: rival faces left, Oak faces down
|
||||||
{ "face_object", 1, "left" }, -- 14
|
{ "face_object", 1, "left" }, -- 17
|
||||||
{ "face_object", 2, "down" }, -- 15
|
{ "face_object", 2, "down" }, -- 18
|
||||||
{ "show_text", "_ChampionsRoomOakCongratulatesPlayerText" }, -- 16
|
{ "load_player_starter_name" },
|
||||||
|
{ "show_text", "_ChampionsRoomOakCongratulatesPlayerText" }, -- 19
|
||||||
-- OakDisappointedWithRivalScript: Oak turns to the rival (right)
|
-- OakDisappointedWithRivalScript: Oak turns to the rival (right)
|
||||||
{ "face_object", 2, "right" }, -- 17
|
{ "face_object", 2, "right" }, -- 20
|
||||||
{ "show_text", "_ChampionsRoomOakDisappointedWithRivalText" }, -- 18
|
{ "show_text", "_ChampionsRoomOakDisappointedWithRivalText" }, -- 21
|
||||||
-- OakComeWithMeScript: Oak faces down again, then exits up
|
-- OakComeWithMeScript: Oak faces down again, then exits up
|
||||||
{ "face_object", 2, "down" }, -- 19
|
{ "face_object", 2, "down" }, -- 22
|
||||||
{ "show_text", "_ChampionsRoomOakComeWithMeText" }, -- 20
|
{ "show_text", "_ChampionsRoomOakComeWithMeText" }, -- 23
|
||||||
{ "move_npc", 2, "up", 2 }, -- 21 OakExitChampionsRoomMovement
|
{ "move_npc", 2, "up", 2 }, -- 24 OakExitChampionsRoomMovement
|
||||||
{ "hide_object", "CHAMPIONS_ROOM", "CHAMPIONSROOM_OAK" }, -- 22
|
{ "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
|
-- 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)
|
-- onEnter), then warp up into it (destWarp 1 lands at (4,7) facing up)
|
||||||
{ "set_field", "pendingHallOfFame", true }, -- 23
|
{ "set_field", "pendingHallOfFame", true }, -- 28
|
||||||
{ "warp", "HALL_OF_FAME", 4, 7, "up" }, -- 24
|
{ "warp", "HALL_OF_FAME", 4, 7, "up" }, -- 29
|
||||||
}
|
}
|
||||||
|
|
||||||
M.CHAMPIONS_ROOM = {
|
M.CHAMPIONS_ROOM = {
|
||||||
@@ -1130,6 +1243,7 @@ local function pokemonTower2FRivalScript(playerX)
|
|||||||
{ "jump_if_false", "end" }, -- 6 loss: stay
|
{ "jump_if_false", "end" }, -- 6 loss: stay
|
||||||
{ "set_flag", "EVENT_BEAT_POKEMON_TOWER_RIVAL" }, -- 7
|
{ "set_flag", "EVENT_BEAT_POKEMON_TOWER_RIVAL" }, -- 7
|
||||||
{ "show_text", "_PokemonTower2FRivalDefeatedText" }, -- 8
|
{ "show_text", "_PokemonTower2FRivalDefeatedText" }, -- 8
|
||||||
|
{ "play_music", "Music_MeetRival", { start = "rival" } },
|
||||||
{ "walk_npc", 1, exitDirs }, -- 9
|
{ "walk_npc", 1, exitDirs }, -- 9
|
||||||
{ "hide_object", "POKEMON_TOWER_2F", "POKEMONTOWER2F_RIVAL" }, -- 10
|
{ "hide_object", "POKEMON_TOWER_2F", "POKEMONTOWER2F_RIVAL" }, -- 10
|
||||||
{ "jump", "end" }, -- 11
|
{ "jump", "end" }, -- 11
|
||||||
|
|||||||
@@ -87,6 +87,18 @@ end
|
|||||||
M.PALLET_TOWN = {
|
M.PALLET_TOWN = {
|
||||||
talk = require("data.scripts.pallet_town").talk,
|
talk = require("data.scripts.pallet_town").talk,
|
||||||
escort = escort,
|
escort = escort,
|
||||||
|
-- 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),
|
-- 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
|
-- then a wild Pikachu battle before the lab escort (pokeyellow
|
||||||
-- PalletTownPikachuBattleScript).
|
-- PalletTownPikachuBattleScript).
|
||||||
@@ -178,7 +190,8 @@ M.PALLET_TOWN = {
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function enterLab()
|
local function enterLab(oak)
|
||||||
|
if oak then oak.stepFrames = nil end
|
||||||
Commands.hide_object(ctx, "PALLET_TOWN", "PALLETTOWN_OAK")
|
Commands.hide_object(ctx, "PALLET_TOWN", "PALLETTOWN_OAK")
|
||||||
Commands.show_object(ctx, "OAKS_LAB", "OAKSLAB_OAK2")
|
Commands.show_object(ctx, "OAKS_LAB", "OAKSLAB_OAK2")
|
||||||
ow.doorWarp = true
|
ow.doorWarp = true
|
||||||
@@ -187,12 +200,17 @@ M.PALLET_TOWN = {
|
|||||||
end
|
end
|
||||||
|
|
||||||
local function walkToLab(oak)
|
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
|
local i = 0
|
||||||
|
if oak then
|
||||||
|
oak.stepFrames = ow.player.stepFramesCur or ow.player.stepFrames
|
||||||
|
end
|
||||||
local function tick()
|
local function tick()
|
||||||
i = i + 1
|
i = i + 1
|
||||||
local playerStep = escort.playerSteps[i]
|
local playerStep = escort.playerSteps[i]
|
||||||
if not playerStep then
|
if not playerStep then
|
||||||
enterLab()
|
enterLab(oak)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
if oak and escort.oakSteps[i] then
|
if oak and escort.oakSteps[i] then
|
||||||
@@ -206,6 +224,13 @@ M.PALLET_TOWN = {
|
|||||||
end
|
end
|
||||||
|
|
||||||
local function escortToLab(oak)
|
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
|
local numSteps = x - 10
|
||||||
if oak and numSteps > 0 then
|
if oak and numSteps > 0 then
|
||||||
ow:scriptMove(oak, "left", numSteps, function()
|
ow:scriptMove(oak, "left", numSteps, function()
|
||||||
@@ -249,14 +274,24 @@ M.PALLET_TOWN = {
|
|||||||
function()
|
function()
|
||||||
-- Oak turns toward the horizontally adjacent grass (left exit
|
-- Oak turns toward the horizontally adjacent grass (left exit
|
||||||
-- looks right, right exit looks left -- the
|
-- looks right, right exit looks left -- the
|
||||||
-- EVENT_PLAYER_AT_RIGHT_EXIT_TO_PALLET_TOWN branch)
|
-- 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
|
if oak then oak.facing = x == 10 and "right" or "left" end
|
||||||
local battle = BattleState.newWild(game, "PIKACHU", 5)
|
hold(2, nil, function()
|
||||||
battle:makeOldManDemo("PROF.OAK")
|
local battle = BattleState.newWild(game, "PIKACHU", 5)
|
||||||
battle.onFinish = function()
|
battle:makeOldManDemo("PROF.OAK")
|
||||||
afterPikaBattle()
|
battle.onFinish = function()
|
||||||
end
|
afterPikaBattle()
|
||||||
game.stack:push(battle)
|
end
|
||||||
|
-- Use the standard wild-battle entry transition.
|
||||||
|
Commands.pushBattle(ctx, battle)
|
||||||
|
end)
|
||||||
end))
|
end))
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -396,19 +431,31 @@ M.ROUTE_8_GATE = saffronGate("TEXT_ROUTE8GATE_GUARD", { { 2, 3 }, { 2, 4 } }, tr
|
|||||||
-- -------------------------------------------------------------------
|
-- -------------------------------------------------------------------
|
||||||
|
|
||||||
M.POKEMON_FAN_CLUB = {
|
M.POKEMON_FAN_CLUB = {
|
||||||
|
onEnter = function(game, ow)
|
||||||
|
require("src.world.PikachuFollower").onFanClubEntered(game, ow)
|
||||||
|
end,
|
||||||
talk = {
|
talk = {
|
||||||
TEXT_POKEMONFANCLUB_CHAIRMAN = {
|
TEXT_POKEMONFANCLUB_CHAIRMAN = {
|
||||||
{ "face_player" }, -- 1
|
{ "face_player" }, -- 1
|
||||||
{ "check_flag", "EVENT_RECEIVED_BIKE_VOUCHER" }, -- 2
|
{ "check_flag", "EVENT_RECEIVED_BIKE_VOUCHER" }, -- 2
|
||||||
{ "jump_if_true", 9 }, -- 3
|
{ "jump_if_true", "nothing_left" }, -- 3
|
||||||
{ "show_text", "_PokemonFanClubChairmanIntroText" }, -- 4
|
-- YesNoChoice (scripts/PokemonFanClub.asm): NO forfeits the voucher (#1050)
|
||||||
{ "show_text", "_PokemonFanClubChairmanStoryText" }, -- 5
|
{ "ask", "_PokemonFanClubChairmanIntroText" }, -- 4
|
||||||
|
{ "jump_if_false", "no_story" }, -- 5
|
||||||
|
{ "show_text", "_PokemonFanClubChairmanStoryText" }, -- 6
|
||||||
-- give-then-print like scripts/PokemonFanClub.asm (GiveItem
|
-- give-then-print like scripts/PokemonFanClub.asm (GiveItem
|
||||||
-- fills wStringBuffer; the received text reads it)
|
-- fills wStringBuffer; the received text reads it)
|
||||||
{ "give_item", "BIKE_VOUCHER", 1, false }, -- 6
|
{ "give_item", "BIKE_VOUCHER", 1, false }, -- 7
|
||||||
{ "show_text", "_PokemonFanClubReceivedBikeVoucherText" }, -- 7
|
{ "show_text", "_PokemonFanClubReceivedBikeVoucherText" }, -- 8
|
||||||
{ "set_flag", "EVENT_RECEIVED_BIKE_VOUCHER" }, -- 8
|
{ "set_flag", "EVENT_RECEIVED_BIKE_VOUCHER" }, -- 9
|
||||||
{ "show_text", "_PokemonFanClubExplainBikeVoucherText" }, -- 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
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -619,8 +666,10 @@ local function mtMoonFossil(itemId, otherName, gotFlag)
|
|||||||
end
|
end
|
||||||
local idef = game.data.items[itemId]
|
local idef = game.data.items[itemId]
|
||||||
game.stringBuffer = idef and idef.name or 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)
|
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,
|
game.stack:push(TextBox.new(game,
|
||||||
t._MtMoonB2FReceivedFossilText
|
t._MtMoonB2FReceivedFossilText
|
||||||
or ("{PLAYER} got the\n" .. game.stringBuffer .. "!"),
|
or ("{PLAYER} got the\n" .. game.stringBuffer .. "!"),
|
||||||
@@ -633,11 +682,11 @@ local function mtMoonFossil(itemId, otherName, gotFlag)
|
|||||||
ow.runner:run({
|
ow.runner:run({
|
||||||
{ "walk_npc", 1, dirs },
|
{ "walk_npc", 1, dirs },
|
||||||
{ "text_opts", { auto = true } },
|
{ "text_opts", { auto = true } },
|
||||||
|
{ "text_sound", "Get_Key_Item" },
|
||||||
{ "show_text", "_MtMoonB2FSuperNerdThenThisIsMineText" },
|
{ "show_text", "_MtMoonB2FSuperNerdThenThisIsMineText" },
|
||||||
{ "play_sound", "Get_Key_Item" },
|
|
||||||
{ "hide_object", "MT_MOON_B2F", otherName },
|
{ "hide_object", "MT_MOON_B2F", otherName },
|
||||||
}, { onDone = done })
|
}, { onDone = done })
|
||||||
end))
|
end, TextBox.soundOpts(game, "Get_Key_Item")))
|
||||||
end }))
|
end }))
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -653,6 +702,23 @@ M.MT_MOON_B2F = {
|
|||||||
return false
|
return false
|
||||||
end,
|
end,
|
||||||
talk = {
|
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(
|
TEXT_MTMOONB2F_DOME_FOSSIL = mtMoonFossil(
|
||||||
"DOME_FOSSIL", "MTMOONB2F_HELIX_FOSSIL", "EVENT_GOT_DOME_FOSSIL"),
|
"DOME_FOSSIL", "MTMOONB2F_HELIX_FOSSIL", "EVENT_GOT_DOME_FOSSIL"),
|
||||||
TEXT_MTMOONB2F_HELIX_FOSSIL = mtMoonFossil(
|
TEXT_MTMOONB2F_HELIX_FOSSIL = mtMoonFossil(
|
||||||
@@ -660,35 +726,40 @@ M.MT_MOON_B2F = {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
-- The ticket clerk (scripts/Museum1F.asm Museum1FScientist1Text):
|
-- The ticket clerk (scripts/Museum1F.asm Museum1FScientist1Text): Y50, once.
|
||||||
-- Y50, once. Declining at the rope shoves the player one tile SOUTH back off
|
-- Declining at the rope shoves the player one tile south (#151)
|
||||||
-- the exhibit rope they crossed heading north (#151); the museum floor has no
|
|
||||||
-- ledges, so a plain scriptMove("down",1) is the correct primitive.
|
|
||||||
local function museumClerk(game, ow, done, onDecline)
|
local function museumClerk(game, ow, done, onDecline)
|
||||||
local TextBox = require("src.render.TextBox")
|
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
|
if game.save.flags.EVENT_BOUGHT_MUSEUM_TICKET then
|
||||||
game.stack:push(TextBox.new(game,
|
game.stack:push(TextBox.new(game,
|
||||||
"Take your time,\nand enjoy it all!", done))
|
t._Museum1FScientist1TakePlentyOfTimeText
|
||||||
|
or "Take your time,\nand enjoy it all!", done))
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
|
-- scripts/Museum1F.asm:72
|
||||||
|
local money = function() return game.save.money end
|
||||||
game.stack:push(TextBox.new(game,
|
game.stack:push(TextBox.new(game,
|
||||||
"It's ¥50 for a\nchild's ticket.\fWould you like to\ncome in?", function()
|
t._Museum1FScientist1WouldYouLikeToComeInText
|
||||||
game.stack:push(ChoiceBox.new(game, function(yes)
|
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
|
if yes and game.save.money >= 50 then
|
||||||
game.save.money = game.save.money - 50
|
game.save.money = game.save.money - 50
|
||||||
game.save.flags.EVENT_BOUGHT_MUSEUM_TICKET = true
|
game.save.flags.EVENT_BOUGHT_MUSEUM_TICKET = true
|
||||||
|
-- scripts/Museum1F.asm:106
|
||||||
game.stack:push(TextBox.new(game,
|
game.stack:push(TextBox.new(game,
|
||||||
"Right, ¥50!\nThank you!", done))
|
t._Museum1FScientist1ThankYouText or "Right, ¥50!\nThank you!", done,
|
||||||
|
{ money = money }))
|
||||||
elseif yes then
|
elseif yes then
|
||||||
game.stack:push(TextBox.new(game,
|
game.stack:push(TextBox.new(game,
|
||||||
"You don't have\nenough money.", onDecline or done))
|
t._Museum1FScientist1DontHaveEnoughMoneyText
|
||||||
|
or "You don't have\nenough money.", onDecline or done, { money = money }))
|
||||||
else
|
else
|
||||||
game.stack:push(TextBox.new(game,
|
game.stack:push(TextBox.new(game,
|
||||||
"Come again!", onDecline or done))
|
t._Museum1FScientist1ComeAgainText
|
||||||
|
or "Come again!", onDecline or done, { money = money }))
|
||||||
end
|
end
|
||||||
end))
|
end }))
|
||||||
end))
|
|
||||||
end
|
end
|
||||||
|
|
||||||
M.MUSEUM_1F = {
|
M.MUSEUM_1F = {
|
||||||
|
|||||||
@@ -151,9 +151,27 @@ M.POKEMON_TOWER_6F = {
|
|||||||
-- trick, and the speedrun route this bot follows depends on it.
|
-- trick, and the speedrun route this bot follows depends on it.
|
||||||
if result == "win" or battle.pokeDollEscape then
|
if result == "win" or battle.pokeDollEscape then
|
||||||
game.save.flags.EVENT_BEAT_GHOST_MAROWAK = true
|
game.save.flags.EVENT_BEAT_GHOST_MAROWAK = true
|
||||||
game.stack:push(TextBox.new(game,
|
-- PokemonTower6FMarowakDepartedText (scripts/PokemonTower6F.asm)
|
||||||
t._PokemonTower6FSoulWasCalmedText
|
-- is two texts, not one: the CUBONE's-mother line first, then
|
||||||
or "The mother's soul\nwas calmed.\012It departed to\nthe afterlife!"))
|
-- 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
|
elseif result ~= "lose" then
|
||||||
-- .did_not_defeat: one simulated step right, off the trigger,
|
-- .did_not_defeat: one simulated step right, off the trigger,
|
||||||
-- so fleeing does not leave you standing on a cell that
|
-- so fleeing does not leave you standing on a cell that
|
||||||
@@ -517,6 +535,14 @@ M.GAME_CORNER = {
|
|||||||
done()
|
done()
|
||||||
return
|
return
|
||||||
end
|
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()
|
ow:engageTrainer(npc, function()
|
||||||
if not ow:trainerDefeated(npc) then
|
if not ow:trainerDefeated(npc) then
|
||||||
done()
|
done()
|
||||||
@@ -527,19 +553,44 @@ M.GAME_CORNER = {
|
|||||||
game.data.text._GameCornerRocketAfterBattleText
|
game.data.text._GameCornerRocketAfterBattleText
|
||||||
or "Our hideout might\nbe discovered! I\nbetter tell BOSS!",
|
or "Our hideout might\nbe discovered! I\nbetter tell BOSS!",
|
||||||
function()
|
function()
|
||||||
-- #198: GameCornerRocketExitScript (scripts/GameCorner.asm)
|
-- #198/#862: GameCornerRocketBattleScript (scripts/GameCorner.asm)
|
||||||
-- ApplyMovementData walks the grunt one tile UP into the poster
|
-- picks the exit walk from where the player is standing, because
|
||||||
-- (the hideout's secret entrance at 9,4) before HideObject, so
|
-- the grunt on (9,5) has to get past him: wYCoord == 6 (talked to
|
||||||
-- he leaves the floor rather than popping out of existence on
|
-- from the south) or wXCoord == 8 (from the west) leaves the row
|
||||||
-- (9,5). scriptMove locks player input (#scriptMoves>0) and
|
-- clear and takes GameCornerMovement_Rocket_WalkDirect, five steps
|
||||||
-- ignores collision, so we despawn + unfreeze (done) only once
|
-- RIGHT; otherwise the player is east of him on (10,5) and
|
||||||
-- the step lands.
|
-- GameCornerMovement_Rocket_WalkAroundPlayer steps DOWN, right, UP
|
||||||
ow:scriptMove(npc, "up", 1, function()
|
-- and right again to go AROUND him. pokeyellow's copy of the
|
||||||
hideRocket()
|
-- around-path takes one extra RIGHT on the lower row before coming
|
||||||
done()
|
-- back up (it also has to clear Pikachu); both versions end on
|
||||||
end)
|
-- (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)
|
end, game.data.text._GameCornerRocketBattleEndText or "Dang!")
|
||||||
end,
|
end,
|
||||||
-- GameCornerClerk1Text (scripts/GameCorner.asm): the offer, a
|
-- GameCornerClerk1Text (scripts/GameCorner.asm): the offer, a
|
||||||
-- YesNoChoice, then ¥1000 for 50 coins. Yellow drops the "1" from the
|
-- YesNoChoice, then ¥1000 for 50 coins. Yellow drops the "1" from the
|
||||||
@@ -549,21 +600,57 @@ M.GAME_CORNER = {
|
|||||||
-- handler is bound to both text ids just below (#552).
|
-- handler is bound to both text ids just below (#552).
|
||||||
TEXT_GAMECORNER_CLERK1 = function(game, ow, npc, done)
|
TEXT_GAMECORNER_CLERK1 = function(game, ow, npc, done)
|
||||||
local TextBox = require("src.render.TextBox")
|
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 t = game.data.text
|
||||||
local function line(suffix, fallback)
|
local function line(suffix, fallback)
|
||||||
return t["_GameCornerClerk1" .. suffix]
|
return t["_GameCornerClerk1" .. suffix]
|
||||||
or t["_GameCornerClerk" .. suffix]
|
or t["_GameCornerClerk" .. suffix]
|
||||||
or fallback
|
or fallback
|
||||||
end
|
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,
|
game.stack:push(TextBox.new(game,
|
||||||
line("DoYouNeedSomeGameCoinsText",
|
line("DoYouNeedSomeGameCoinsText",
|
||||||
"Do you need some\ngame coins?\f¥1000 for 50."), function()
|
"Do you need some\ngame coins?\f¥1000 for 50."),
|
||||||
game.stack:push(ChoiceBox.new(game, function(yes)
|
nil, { choice = function(yes)
|
||||||
if not yes then
|
if not yes then
|
||||||
game.stack:push(TextBox.new(game,
|
game.stack:push(TextBox.new(game,
|
||||||
line("PleaseComePlaySometimeText",
|
line("PleaseComePlaySometimeText",
|
||||||
"No? Please come\nplay sometime!"), done))
|
"No? Please come\nplay sometime!"), finish))
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
-- scripts/GameCorner.asm GameCornerClerk1Text: coins need
|
-- scripts/GameCorner.asm GameCornerClerk1Text: coins need
|
||||||
@@ -571,29 +658,30 @@ M.GAME_CORNER = {
|
|||||||
if not game.save.inventory.COIN_CASE then
|
if not game.save.inventory.COIN_CASE then
|
||||||
game.stack:push(TextBox.new(game,
|
game.stack:push(TextBox.new(game,
|
||||||
line("DontHaveCoinCaseText",
|
line("DontHaveCoinCaseText",
|
||||||
"You don't have a\nCOIN CASE!"), done))
|
"You don't have a\nCOIN CASE!"), finish))
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
if (game.save.coins or 0) >= 9990 then
|
if (game.save.coins or 0) >= 9990 then
|
||||||
game.stack:push(TextBox.new(game,
|
game.stack:push(TextBox.new(game,
|
||||||
line("CoinCaseIsFullText",
|
line("CoinCaseIsFullText",
|
||||||
"Oops! Your COIN\nCASE is full."), done))
|
"Oops! Your COIN\nCASE is full."), finish))
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
if game.save.money < 1000 then
|
if game.save.money < 1000 then
|
||||||
game.stack:push(TextBox.new(game,
|
game.stack:push(TextBox.new(game,
|
||||||
line("CantAffordTheCoinsText",
|
line("CantAffordTheCoinsText",
|
||||||
"You can't afford\nthe coins!"), done))
|
"You can't afford\nthe coins!"), finish))
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
game.save.money = game.save.money - 1000
|
game.save.money = game.save.money - 1000
|
||||||
game.save.coins = math.min(9999, (game.save.coins or 0) + 50)
|
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,
|
game.stack:push(TextBox.new(game,
|
||||||
line("ThanksHereAre50CoinsText",
|
line("ThanksHereAre50CoinsText",
|
||||||
"Thanks! Here are\nyour 50 coins!")
|
"Thanks! Here are\nyour 50 coins!"), finish))
|
||||||
.. ("\fCOINS: %d"):format(game.save.coins), done))
|
end }))
|
||||||
end))
|
|
||||||
end))
|
|
||||||
end,
|
end,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -606,99 +694,188 @@ M.GAME_CORNER.talk.TEXT_GAMECORNER_CLERK =
|
|||||||
M.GAME_CORNER.talk.TEXT_GAMECORNER_CLERK1
|
M.GAME_CORNER.talk.TEXT_GAMECORNER_CLERK1
|
||||||
|
|
||||||
-- Game Corner prize lists (data/events/prizes.asm, prize_mon_levels.asm).
|
-- 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
|
-- Each counter owns ONE window of three prizes, not the whole catalogue:
|
||||||
-- identical, so they are shared and appended to each version's mon list.
|
-- 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 = {
|
local PRIZE_TMS = {
|
||||||
{ kind = "item", item = "TM_DRAGON_RAGE", cost = 3300 },
|
{ kind = "item", item = "TM_DRAGON_RAGE", cost = 3300 },
|
||||||
{ kind = "item", item = "TM_HYPER_BEAM", cost = 5500 },
|
{ kind = "item", item = "TM_HYPER_BEAM", cost = 5500 },
|
||||||
{ kind = "item", item = "TM_SUBSTITUTE", cost = 7700 },
|
{ kind = "item", item = "TM_SUBSTITUTE", cost = 7700 },
|
||||||
}
|
}
|
||||||
local RED_PRIZES = {
|
local RED_PRIZE_WINDOWS = {
|
||||||
{ kind = "mon", species = "ABRA", level = 9, cost = 180 },
|
{
|
||||||
{ kind = "mon", species = "CLEFAIRY", level = 8, cost = 500 },
|
{ kind = "mon", species = "ABRA", level = 9, cost = 180 },
|
||||||
{ kind = "mon", species = "NIDORINA", level = 17, cost = 1200 },
|
{ kind = "mon", species = "CLEFAIRY", level = 8, cost = 500 },
|
||||||
{ kind = "mon", species = "DRATINI", level = 18, cost = 2800 },
|
{ kind = "mon", species = "NIDORINA", level = 17, cost = 1200 },
|
||||||
{ 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],
|
{ 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 = {
|
local BLUE_PRIZE_WINDOWS = {
|
||||||
{ kind = "mon", species = "ABRA", level = 6, cost = 120 },
|
{
|
||||||
{ kind = "mon", species = "CLEFAIRY", level = 12, cost = 750 },
|
{ kind = "mon", species = "ABRA", level = 6, cost = 120 },
|
||||||
{ kind = "mon", species = "NIDORINO", level = 17, cost = 1200 },
|
{ kind = "mon", species = "CLEFAIRY", level = 12, cost = 750 },
|
||||||
{ kind = "mon", species = "PINSIR", level = 20, cost = 2500 },
|
{ kind = "mon", species = "NIDORINO", level = 17, cost = 1200 },
|
||||||
{ 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],
|
{ 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()
|
local function prizeWindow(n)
|
||||||
return require("src.core.GameVersion").isBlue() and BLUE_PRIZES or RED_PRIZES
|
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
|
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
|
-- 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
|
-- 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
|
-- with no case prints RequireCoinCaseText and returns without ever opening a
|
||||||
-- window; only with the case does it print ExchangeCoinsForPrizesText and then
|
-- window; only with the case does it print ExchangeCoinsForPrizesText and then
|
||||||
-- show the prizes. #194: the port used to open the window unconditionally and
|
-- show the prizes. #194: the port used to open the window unconditionally and
|
||||||
-- skip both text boxes.
|
-- skip both text boxes. wMaxMenuItem is 3, i.e. this window's three prizes
|
||||||
local function prizeCounter(game, ow, npc, done)
|
-- plus the NO THANKS row, and HandlePrizeChoice confirms the pick with
|
||||||
local ListMenu = require("src.ui.ListMenu")
|
-- SoYouWantPrizeText + YesNoChoice before any coins move; every branch then
|
||||||
local Commands = require("src.script.Commands")
|
-- rets out of CeladonPrizeMenu, so one transaction ends the conversation and
|
||||||
local TextBox = require("src.render.TextBox")
|
-- buying again means talking to the counter again (#623).
|
||||||
local t = game.data.text
|
local function prizeCounter(window)
|
||||||
-- IsItemInBag COIN_CASE: without the case, deny and open no window
|
return function(game, ow, npc, done)
|
||||||
-- (COIN_CASE is a numeric count in save.inventory, nil when absent).
|
local ListMenu = require("src.ui.ListMenu")
|
||||||
if not game.save.inventory.COIN_CASE then
|
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,
|
game.stack:push(TextBox.new(game,
|
||||||
t._RequireCoinCaseText or "A COIN CASE is\nrequired!", done))
|
t._ExchangeCoinsForPrizesText or "We exchange your\ncoins for prizes.",
|
||||||
return
|
function()
|
||||||
end
|
local items = {}
|
||||||
-- ExchangeCoinsForPrizesText plays before the prize window opens.
|
for _, p in ipairs(prizeWindow(window)) do
|
||||||
game.stack:push(TextBox.new(game,
|
local label
|
||||||
t._ExchangeCoinsForPrizesText or "We exchange your\ncoins for prizes.",
|
if p.kind == "mon" then
|
||||||
function()
|
label = ("%s L%d"):format(game.data.pokemon[p.species].name, p.level)
|
||||||
local items = {}
|
else
|
||||||
for _, p in ipairs(activePrizes()) do
|
label = game.data.items[p.item].name
|
||||||
local label
|
end
|
||||||
if p.kind == "mon" then
|
table.insert(items,
|
||||||
label = ("%s L%d"):format(game.data.pokemon[p.species].name, p.level)
|
{ label = label, right = tostring(p.cost), value = p })
|
||||||
else
|
|
||||||
label = game.data.items[p.item].name
|
|
||||||
end
|
end
|
||||||
table.insert(items,
|
-- NoThanksText (data/events/prizes.asm) sits under the three prizes
|
||||||
{ label = label, right = tostring(p.cost), value = p })
|
table.insert(items, { label = "NO THANKS" })
|
||||||
end
|
local list
|
||||||
local list
|
-- close the window first: every ending in HandlePrizeChoice leaves
|
||||||
list = ListMenu.new(game, "PRIZES (COINS)", items, {
|
-- the menu for good, and the closing line belongs over the map
|
||||||
footer = ("COINS %d"):format(game.save.coins or 0),
|
local function finish(msg)
|
||||||
onChoose = function(item)
|
list:close()
|
||||||
local p = item.value
|
game.stack:push(TextBox.new(game, msg, done))
|
||||||
|
end
|
||||||
|
local function buy(p)
|
||||||
if (game.save.coins or 0) < p.cost then
|
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
|
return
|
||||||
end
|
end
|
||||||
game.save.coins = game.save.coins - p.cost
|
game.save.coins = game.save.coins - p.cost
|
||||||
if p.kind == "mon" then
|
-- no thank-you line: HereYouGoText is unreferenced in the asm,
|
||||||
Commands.give_pokemon({ save = game.save, game = game },
|
-- which just redraws the coin box (PrintPrizePrice) and returns
|
||||||
p.species, p.level)
|
list:close()
|
||||||
else
|
done()
|
||||||
game.save.inventory[p.item] = (game.save.inventory[p.item] or 0) + 1
|
end
|
||||||
end
|
list = ListMenu.new(game, "PRIZES (COINS)", items, {
|
||||||
list.footer = ("Got it! COINS %d"):format(game.save.coins)
|
footer = ("COINS %d"):format(game.save.coins or 0),
|
||||||
end,
|
onChoose = function(item)
|
||||||
onCancel = done,
|
local p = item.value
|
||||||
})
|
if not p then -- NO THANKS is the B exit (cp 3 -> .noChoice)
|
||||||
game.stack:push(list)
|
list:close()
|
||||||
end))
|
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
|
end
|
||||||
|
|
||||||
M.GAME_CORNER_PRIZE_ROOM = {
|
M.GAME_CORNER_PRIZE_ROOM = {
|
||||||
talk = { -- the three prize counters are bg events
|
talk = { -- the three prize counters are bg events, one window each
|
||||||
TEXT_GAMECORNERPRIZEROOM_PRIZE_VENDOR_1 = prizeCounter,
|
TEXT_GAMECORNERPRIZEROOM_PRIZE_VENDOR_1 = prizeCounter(1),
|
||||||
TEXT_GAMECORNERPRIZEROOM_PRIZE_VENDOR_2 = prizeCounter,
|
TEXT_GAMECORNERPRIZEROOM_PRIZE_VENDOR_2 = prizeCounter(2),
|
||||||
TEXT_GAMECORNERPRIZEROOM_PRIZE_VENDOR_3 = prizeCounter,
|
TEXT_GAMECORNERPRIZEROOM_PRIZE_VENDOR_3 = prizeCounter(3),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -760,15 +937,10 @@ M.VERMILION_DOCK = {
|
|||||||
ow:startDustAnim(cx, 1, function() puff(n - 1, cx + 2) end)
|
ow:startDustAnim(cx, 1, function() puff(n - 1, cx + 2) end)
|
||||||
end
|
end
|
||||||
puff(3, 15)
|
puff(3, 15)
|
||||||
-- VermilionDock_EraseSSAnne deliberately leaves the blocks under the
|
-- scripts/VermilionDock.asm:182-203
|
||||||
-- player alone ("south of the player and won't be redrawn"), so skip
|
|
||||||
-- his own block: he must not spend the walk-out standing on water
|
|
||||||
local pbx = math.floor(ow.player.cellX / 2)
|
|
||||||
local pby = math.floor(ow.player.cellY / 2)
|
|
||||||
local rows = {}
|
local rows = {}
|
||||||
local function setBlock(bx, by, block)
|
local function setBlock(bx, by, block)
|
||||||
if bx < 1 or bx > 8 then return end
|
if bx < 1 or bx > 8 then return end
|
||||||
if bx == pbx and by == pby then return end
|
|
||||||
rows[#rows + 1] = { "replace_block", bx, by, block }
|
rows[#rows + 1] = { "replace_block", bx, by, block }
|
||||||
end
|
end
|
||||||
rows[#rows + 1] = { "wait", 120 }
|
rows[#rows + 1] = { "wait", 120 }
|
||||||
|
|||||||
@@ -8,21 +8,35 @@ local M = {}
|
|||||||
|
|
||||||
local function text(game) return game.data.text end
|
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")
|
local TextBox = require("src.render.TextBox")
|
||||||
game.stack:push(TextBox.new(game, s, done))
|
game.stack:push(TextBox.new(game, s, done, opts))
|
||||||
end
|
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 function ask(game, s, cb)
|
||||||
local ChoiceBox = require("src.ui.ChoiceBox")
|
local TextBox = require("src.render.TextBox")
|
||||||
push(game, s, function() game.stack:push(ChoiceBox.new(game, cb)) end)
|
game.stack:push(TextBox.new(game, s, nil, { choice = cb }))
|
||||||
end
|
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)
|
local function fill(s, subs)
|
||||||
s = s:gsub("{PLAYER}", subs.player or "")
|
s = s:gsub("{PLAYER}", subs.player or "")
|
||||||
s = s:gsub("{NUM:[^}]*}", function() return tostring(subs.num or "") end)
|
s = s:gsub("{NUM:([%w_]*)[^}]*}", function(name)
|
||||||
s = s:gsub("{RAM:[^}]*}", function() return subs.ram or "" end)
|
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
|
return s
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -73,9 +87,12 @@ local function oaksAide(threshold, itemId, repeatText)
|
|||||||
{ ram = itemName, player = game.save.player.name }), done)
|
{ ram = itemName, player = game.save.player.name }), done)
|
||||||
end)
|
end)
|
||||||
else
|
else
|
||||||
|
-- .notEnoughOwnedMons prints owned then requirement, two counts
|
||||||
push(game, fill(t._OaksAideUhOhText or
|
push(game, fill(t._OaksAideUhOhText or
|
||||||
"You have only\ncaught {NUM:}!",
|
"You have only\ncaught {NUM:}!",
|
||||||
{ num = owned, ram = itemName }), done)
|
{ num = owned, ram = itemName,
|
||||||
|
hOaksAideNumMonsOwned = owned,
|
||||||
|
hOaksAideRequirement = threshold }), done)
|
||||||
end
|
end
|
||||||
end)
|
end)
|
||||||
end
|
end
|
||||||
@@ -101,33 +118,36 @@ M.ROUTE_15_GATE_2F = {
|
|||||||
|
|
||||||
M.MT_MOON_POKECENTER = {
|
M.MT_MOON_POKECENTER = {
|
||||||
talk = {
|
talk = {
|
||||||
TEXT_MTMOONPOKECENTER_MAGIKARP_SALESMAN = function(game, ow, npc, done)
|
-- command rows, not a Lua handler: give_pokemon needs a runner to AskName (#1407)
|
||||||
local t = text(game)
|
TEXT_MTMOONPOKECENTER_MAGIKARP_SALESMAN = {
|
||||||
if game.save.flags.EVENT_BOUGHT_MAGIKARP then
|
{ "check_flag", "EVENT_BOUGHT_MAGIKARP" },
|
||||||
push(game, t._MtMoonPokecenterMagikarpSalesmanNoRefundsText
|
{ "jump_if_true", "no_refunds" },
|
||||||
or "Well, I don't\ngive refunds!", done)
|
-- MONEY_BOX goes up between the offer and YesNoChoice -- MtMoonPokecenter.asm:31
|
||||||
return
|
{ "text_opts", { money = true } },
|
||||||
end
|
{ "ask", "_MtMoonPokecenterMagikarpSalesmanIGotADealText" },
|
||||||
ask(game, t._MtMoonPokecenterMagikarpSalesmanOfferText
|
{ "jump_if_false", "declined" },
|
||||||
or "MAGIKARP! A\nsteal at ¥500!\nWant one?", function(yes)
|
{ "check_money", 500 },
|
||||||
if not yes then
|
{ "jump_if_false", "no_money" },
|
||||||
push(game, t._MtMoonPokecenterMagikarpSalesmanNoText
|
{ "give_pokemon", "MAGIKARP", 5 },
|
||||||
or "No? I'm only\nselling today!", done)
|
-- MtMoonPokecenter.asm:49 `jr nc, .done`: a refused gift is never charged
|
||||||
return
|
{ "jump_if_false", "box_full" },
|
||||||
end
|
{ "take_money", 500 },
|
||||||
if game.save.money < 500 then
|
{ "set_flag", "EVENT_BOUGHT_MAGIKARP" },
|
||||||
push(game, t._MtMoonPokecenterMagikarpSalesmanNoMoneyText
|
{ "text_sound", "Get_Item1" },
|
||||||
or "You'll need more\nmoney than that!", done)
|
{ "show_text", "_GotMonText", { RAM = "MAGIKARP" } },
|
||||||
return
|
{ "jump", "end" },
|
||||||
end
|
{ "label", "box_full" },
|
||||||
game.save.money = game.save.money - 500
|
{ "show_text", "_BoxIsFullText" },
|
||||||
game.save.flags.EVENT_BOUGHT_MAGIKARP = true
|
{ "jump", "end" },
|
||||||
local Commands = require("src.script.Commands")
|
{ "label", "declined" },
|
||||||
Commands.give_pokemon({ save = game.save, game = game, overworld = ow },
|
{ "show_text", "_MtMoonPokecenterMagikarpSalesmanNoText" },
|
||||||
"MAGIKARP", 5)
|
{ "jump", "end" },
|
||||||
push(game, ("%s got a\nMAGIKARP!"):format(game.save.player.name), done)
|
{ "label", "no_money" },
|
||||||
end)
|
{ "show_text", "_MtMoonPokecenterMagikarpSalesmanNoMoneyText" },
|
||||||
end,
|
{ "jump", "end" },
|
||||||
|
{ "label", "no_refunds" },
|
||||||
|
{ "show_text", "_MtMoonPokecenterMagikarpSalesmanNoRefundsText" },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,19 +175,26 @@ local function dojoBall(species, ownBall, otherBall, askKey)
|
|||||||
push(game, "You'll have to\nbeat the master\nfirst!", done)
|
push(game, "You'll have to\nbeat the master\nfirst!", done)
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
ask(game, t[askKey] or ("You want\n" .. species .. "?"), function(yes)
|
-- Examining a ball shows that species' POKéDEX entry first
|
||||||
if not yes then done() return end
|
-- (DisplayPokedex in FightingDojo.asm, which also marks it seen),
|
||||||
flags["EVENT_GOT_" .. species] = true
|
-- then the yes/no take-it prompt (#853).
|
||||||
flags.EVENT_DEFEATED_FIGHTING_DOJO = true
|
local Commands = require("src.script.Commands")
|
||||||
local Commands = require("src.script.Commands")
|
local ctx = { save = game.save, game = game, overworld = ow }
|
||||||
local ctx = { save = game.save, game = game, overworld = ow }
|
Commands.mark_seen(ctx, species)
|
||||||
Commands.give_pokemon(ctx, species, 30)
|
local DexEntryMenu = require("src.ui.DexEntryMenu")
|
||||||
-- Hide ONLY the chosen ball; the other stays (FightingDojo.asm hides
|
game.stack:push(DexEntryMenu.new(game, species, function()
|
||||||
-- just the picked object's index) and routes to the greedy line above
|
ask(game, t[askKey] or ("You want\n" .. species .. "?"), function(yes)
|
||||||
-- when talked to (#197).
|
if not yes then done() return end
|
||||||
Commands.hide_object(ctx, "FIGHTING_DOJO", ownBall)
|
flags["EVENT_GOT_" .. species] = true
|
||||||
push(game, ("%s got\n%s!"):format(game.save.player.name, species), done)
|
flags.EVENT_DEFEATED_FIGHTING_DOJO = true
|
||||||
end)
|
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
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -229,27 +256,32 @@ M.FIGHTING_DOJO = {
|
|||||||
|
|
||||||
M.SILPH_CO_7F = {
|
M.SILPH_CO_7F = {
|
||||||
talk = {
|
talk = {
|
||||||
TEXT_SILPHCO7F_SILPH_WORKER_M1 = function(game, ow, npc, done)
|
-- command rows, not a Lua handler: give_pokemon needs a runner to AskName (#1049)
|
||||||
local t = text(game)
|
TEXT_SILPHCO7F_SILPH_WORKER_M1 = {
|
||||||
if game.save.flags.EVENT_GOT_LAPRAS then
|
{ "face_player" },
|
||||||
push(game, t._SilphCo7FSilphWorkerM1LaprasDescriptionText
|
{ "check_flag", "EVENT_GOT_LAPRAS" },
|
||||||
or "How is LAPRAS\ndoing?", done)
|
{ "jump_if_true", "has_lapras" },
|
||||||
return
|
{ "show_text", "_SilphCo7FSilphWorkerM1HaveThisPokemonText" },
|
||||||
end
|
{ "give_pokemon", "LAPRAS", 15 },
|
||||||
push(game, t._SilphCo7FSilphWorkerM1ThankYouText
|
{ "jump_if_false", "box_full" },
|
||||||
or "Thank you for\nsaving us!\fI want you to\nhave this LAPRAS!",
|
-- flag ahead of the jingle, like the Celadon EEVEE (#426)
|
||||||
function()
|
{ "set_flag", "EVENT_GOT_LAPRAS" },
|
||||||
game.save.flags.EVENT_GOT_LAPRAS = true
|
{ "text_sound", "Get_Item1" },
|
||||||
local Commands = require("src.script.Commands")
|
{ "show_text", "_GotMonText", { RAM = "LAPRAS" } },
|
||||||
Commands.give_pokemon({ save = game.save, game = game, overworld = ow },
|
{ "show_text", "_SilphCo7FSilphWorkerM1LaprasDescriptionText" },
|
||||||
"LAPRAS", 15)
|
{ "jump", "end" },
|
||||||
push(game, ("%s got\nLAPRAS!"):format(game.save.player.name),
|
{ "label", "box_full" },
|
||||||
function()
|
{ "show_text", "_BoxIsFullText" },
|
||||||
push(game, t._SilphCo7FSilphWorkerM1LaprasDescriptionText
|
{ "jump", "end" },
|
||||||
or "It's a good\nswimmer!", done)
|
-- SilphCo7F.asm .saved_silph gates the thanks on Giovanni
|
||||||
end)
|
{ "label", "has_lapras" },
|
||||||
end)
|
{ "check_flag", "EVENT_BEAT_SILPH_CO_GIOVANNI" },
|
||||||
end,
|
{ "jump_if_true", "saved" },
|
||||||
|
{ "show_text", "_SilphCo7FSilphWorkerM1IsOurPresidentOkText" },
|
||||||
|
{ "jump", "end" },
|
||||||
|
{ "label", "saved" },
|
||||||
|
{ "show_text", "_SilphCo7FSilphWorkerM1SavedText" },
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -278,12 +310,11 @@ M.COPYCATS_HOUSE_2F = {
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
game.stringBuffer = game.data.items.TM_MIMIC.name
|
game.stringBuffer = game.data.items.TM_MIMIC.name
|
||||||
require("src.core.Sound").play(game.data, "Get_Item1")
|
|
||||||
Bag.remove(game.save, "POKE_DOLL", 1)
|
Bag.remove(game.save, "POKE_DOLL", 1)
|
||||||
game.save.flags.EVENT_GOT_TM31 = true
|
game.save.flags.EVENT_GOT_TM31 = true
|
||||||
push(game, t._CopycatsHouse2FCopycatReceivedTM31Text, function()
|
push(game, t._CopycatsHouse2FCopycatReceivedTM31Text, function()
|
||||||
push(game, t._CopycatsHouse2FCopycatTM31Explanation1Text, done)
|
push(game, t._CopycatsHouse2FCopycatTM31Explanation1Text, done)
|
||||||
end)
|
end, require("src.render.TextBox").soundOpts(game, "Get_Item1"))
|
||||||
end)
|
end)
|
||||||
end)
|
end)
|
||||||
end,
|
end,
|
||||||
@@ -449,7 +480,6 @@ M.CELADON_MART_ROOF = {
|
|||||||
return
|
return
|
||||||
end
|
end
|
||||||
game.save.flags[g.flag] = true
|
game.save.flags[g.flag] = true
|
||||||
require("src.core.Sound").play(game.data, "Get_Item1")
|
|
||||||
local subs = { player = game.save.player.name,
|
local subs = { player = game.save.player.name,
|
||||||
ram = game.data.items[g.tm].name }
|
ram = game.data.items[g.tm].name }
|
||||||
local explain = fill(t[g.explain] or "", subs)
|
local explain = fill(t[g.explain] or "", subs)
|
||||||
@@ -461,7 +491,7 @@ M.CELADON_MART_ROOF = {
|
|||||||
else
|
else
|
||||||
done()
|
done()
|
||||||
end
|
end
|
||||||
end)
|
end, require("src.render.TextBox").soundOpts(game, "Get_Item1"))
|
||||||
end)
|
end)
|
||||||
end,
|
end,
|
||||||
onCancel = done,
|
onCancel = done,
|
||||||
@@ -483,25 +513,28 @@ M.ROUTE_24 = {
|
|||||||
local flags = game.save.flags
|
local flags = game.save.flags
|
||||||
local function battleOrDone()
|
local function battleOrDone()
|
||||||
if ow:trainerDefeated(npc) then
|
if ow:trainerDefeated(npc) then
|
||||||
push(game, "I hate this!\nMy dreams of\nTEAM ROCKET...", done)
|
push(game, text(game)._Route24CooltrainerM1YouCouldBecomeATopLeaderText,
|
||||||
|
done)
|
||||||
else
|
else
|
||||||
ow:engageTrainer(npc, done)
|
ow:engageTrainer(npc, done)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
if not flags.EVENT_GOT_NUGGET then
|
if not flags.EVENT_GOT_NUGGET then
|
||||||
push(game, "Congratulations!\nYou beat our 5\ncontest trainers!\f"
|
local t = text(game)
|
||||||
.. "You just earned a\nfabulous prize!", function()
|
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
|
flags.EVENT_GOT_NUGGET = true
|
||||||
require("src.inventory.Bag").add(game.save, "NUGGET", 1)
|
game.stringBuffer = game.data.items.NUGGET.name
|
||||||
push(game, ("%s received\na NUGGET!"):format(game.save.player.name),
|
push(game, t._Route24CooltrainerM1ReceivedNuggetText, function()
|
||||||
function()
|
push(game, t._Route24CooltrainerM1JoinTeamRocketText,
|
||||||
ask(game, "By the way, would\nyou like to join\nTEAM ROCKET?",
|
battleOrDone)
|
||||||
function()
|
end, require("src.render.TextBox").soundOpts(game, "Get_Item1"))
|
||||||
push(game, "Arrgh! You are\nnot convinced?\fThen I'll show\n"
|
end, require("src.render.TextBox").soundOpts(game, "Get_Item1"))
|
||||||
.. "you my power!", battleOrDone)
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
end)
|
|
||||||
return
|
return
|
||||||
end
|
end
|
||||||
battleOrDone()
|
battleOrDone()
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ local M = {}
|
|||||||
|
|
||||||
local function text(game) return game.data.text end
|
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")
|
local TextBox = require("src.render.TextBox")
|
||||||
game.stack:push(TextBox.new(game, s, done))
|
game.stack:push(TextBox.new(game, s, done, opts))
|
||||||
end
|
end
|
||||||
|
|
||||||
-- fill the extracted text placeholders ({RAM:...}, {PLAYER})
|
-- fill the extracted text placeholders ({RAM:...}, {PLAYER})
|
||||||
@@ -25,8 +25,8 @@ local function gift(opts)
|
|||||||
local t = text(game)
|
local t = text(game)
|
||||||
local itemName = game.data.items[opts.item].name
|
local itemName = game.data.items[opts.item].name
|
||||||
local subs = { ram = itemName, player = game.save.player.name }
|
local subs = { ram = itemName, player = game.save.player.name }
|
||||||
local function say(label, fallback, cb)
|
local function say(label, fallback, cb, sopts)
|
||||||
push(game, fill(t[label] or fallback, subs), cb)
|
push(game, fill(t[label] or fallback, subs), cb, sopts)
|
||||||
end
|
end
|
||||||
if game.save.flags[opts.flag] then
|
if game.save.flags[opts.flag] then
|
||||||
say(opts.already or opts.explain, "It's a useful\nitem, isn't it?", done)
|
say(opts.already or opts.explain, "It's a useful\nitem, isn't it?", done)
|
||||||
@@ -39,17 +39,18 @@ local function gift(opts)
|
|||||||
end
|
end
|
||||||
game.save.flags[opts.flag] = true
|
game.save.flags[opts.flag] = true
|
||||||
local idef = game.data.items[opts.item]
|
local idef = game.data.items[opts.item]
|
||||||
require("src.core.Sound").play(game.data,
|
-- the received texts carry sound_get_item_1 / sound_get_key_item, so
|
||||||
(idef and idef.keyItem) and "Get_Key_Item" or "Get_Item1")
|
-- the jingle only fires once that box has typed out
|
||||||
say(opts.received, "{PLAYER} received\n{RAM:}!", function()
|
say(opts.received, "{PLAYER} received\n{RAM:}!", function()
|
||||||
if opts.explain then
|
if opts.explain then
|
||||||
say(opts.explain, "", done)
|
say(opts.explain, "", done)
|
||||||
else
|
else
|
||||||
done()
|
done()
|
||||||
end
|
end
|
||||||
end)
|
end, require("src.render.TextBox").soundOpts(game,
|
||||||
|
(idef and idef.keyItem) and "Get_Key_Item" or "Get_Item1"))
|
||||||
end
|
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
|
||||||
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 = {
|
M.VIRIDIAN_CITY = {
|
||||||
talk = {
|
talk = {
|
||||||
TEXT_VIRIDIANCITY_FISHER = gift({
|
TEXT_VIRIDIANCITY_FISHER = gift({
|
||||||
flag = "EVENT_GOT_TM42", item = "TM_DREAM_EATER",
|
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",
|
received = "_ViridianCityFisherReceivedTM42Text",
|
||||||
explain = "_ViridianCityFisherTM42ExplanationText",
|
explain = "_ViridianCityFisherTM42ExplanationText",
|
||||||
noRoom = "_ViridianCityFisherTM42NoRoomText",
|
noRoom = "_ViridianCityFisherTM42NoRoomText",
|
||||||
@@ -176,6 +187,20 @@ M.ROUTE_18_GATE_2F = {
|
|||||||
{ "face_player" },
|
{ "face_player" },
|
||||||
{ "trade", 6, "EVENT_TRADED_SLOWBRO_FOR_LICKITUNG" }, -- MARC
|
{ "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)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -421,7 +446,7 @@ local function pewterGymEscort(game, ow)
|
|||||||
end
|
end
|
||||||
|
|
||||||
local function afterWalk()
|
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")
|
Music.playMap(game.data, "PEWTER_CITY")
|
||||||
push(game, t._PewterCityYoungsterGoTakeOnBrockText
|
push(game, t._PewterCityYoungsterGoTakeOnBrockText
|
||||||
or "Go take on BROCK\nat the GYM first!", walkHome)
|
or "Go take on BROCK\nat the GYM first!", walkHome)
|
||||||
@@ -444,6 +469,11 @@ local function pewterGymEscort(game, ow)
|
|||||||
end
|
end
|
||||||
|
|
||||||
local function beginWalk()
|
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")
|
Music.play(game.data, "Music_MuseumGuy")
|
||||||
if guy and head > 0 then
|
if guy and head > 0 then
|
||||||
local h = 0
|
local h = 0
|
||||||
@@ -484,12 +514,12 @@ M.PEWTER_CITY = {
|
|||||||
-- Rival ambush: show the hidden rival, walk him up to the player, run
|
-- 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
|
-- the battle rows, march him back and hide him. On a loss the walk is
|
||||||
-- skipped (the blackout rebuilds the map mid-script).
|
-- 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
|
if ow.runner:isRunning() then return false end
|
||||||
ow.player.facing = playerFacing
|
ow.player.facing = playerFacing
|
||||||
-- the rival encounter sting (MUSIC_MEET_RIVAL); the battle music
|
-- the rival encounter sting (MUSIC_MEET_RIVAL); the battle music
|
||||||
-- takes over and the map theme returns after the victory jingle
|
-- 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)
|
ow.runner:run(rows)
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
@@ -543,12 +573,15 @@ local function route22Scene(n, objIndex, objName, oppClass, baseParty, beatFlag,
|
|||||||
{ "face_object", objIndex, rivalFacing }, -- 3
|
{ "face_object", objIndex, rivalFacing }, -- 3
|
||||||
{ "show_text", "_Route22RivalBeforeBattleText" .. n }, -- 4
|
{ "show_text", "_Route22RivalBeforeBattleText" .. n }, -- 4
|
||||||
{ "rival_battle", oppClass, baseParty }, -- 5
|
{ "rival_battle", oppClass, baseParty }, -- 5
|
||||||
{ "jump_if_false", 11 }, -- 6
|
{ "jump_if_false", 13 }, -- 6
|
||||||
{ "set_flag", beatFlag }, -- 7
|
{ "set_flag", beatFlag }, -- 7
|
||||||
{ "show_text", "_Route22Rival" .. n .. "DefeatedText" }, -- 8
|
{ "show_text", "_Route22Rival" .. n .. "DefeatedText" }, -- 8
|
||||||
{ "show_text", "_Route22RivalAfterBattleText" .. n }, -- 9
|
{ "show_text", "_Route22RivalAfterBattleText" .. n }, -- 9
|
||||||
{ "walk_npc", objIndex, route22ExitDirs(n, py) }, -- 10
|
{ "play_music", "Music_MeetRival", { start = "rival",
|
||||||
{ "hide_object", "ROUTE_22", objName }, -- 11
|
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
|
end
|
||||||
|
|
||||||
@@ -570,7 +603,8 @@ M.ROUTE_22 = {
|
|||||||
if f.EVENT_BEAT_GIOVANNI and not f.EVENT_BEAT_ROUTE22_RIVAL_2ND_BATTLE then
|
if f.EVENT_BEAT_GIOVANNI and not f.EVENT_BEAT_ROUTE22_RIVAL_2ND_BATTLE then
|
||||||
return runAmbush(game, ow,
|
return runAmbush(game, ow,
|
||||||
route22Scene(2, 2, "ROUTE22_RIVAL2", "OPP_RIVAL2", 10,
|
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
|
end
|
||||||
return false
|
return false
|
||||||
end,
|
end,
|
||||||
@@ -594,12 +628,14 @@ local function ceruleanRivalScene(px, py)
|
|||||||
{ "face_object", 1, "down" }, -- 3
|
{ "face_object", 1, "down" }, -- 3
|
||||||
{ "show_text", "_CeruleanCityRivalPreBattleText" }, -- 4
|
{ "show_text", "_CeruleanCityRivalPreBattleText" }, -- 4
|
||||||
{ "rival_battle", "OPP_RIVAL1", 7 }, -- 5
|
{ "rival_battle", "OPP_RIVAL1", 7 }, -- 5
|
||||||
{ "jump_if_false", 11 }, -- 6
|
{ "jump_if_false", 13 }, -- 6
|
||||||
{ "set_flag", "EVENT_BEAT_CERULEAN_RIVAL" }, -- 7
|
{ "set_flag", "EVENT_BEAT_CERULEAN_RIVAL" }, -- 7
|
||||||
{ "show_text", "_CeruleanCityRivalDefeatedText" }, -- 8
|
{ "show_text", "_CeruleanCityRivalDefeatedText" }, -- 8
|
||||||
{ "show_text", "_CeruleanCityRivalIWentToBillsText" }, -- 9
|
{ "show_text", "_CeruleanCityRivalIWentToBillsText" }, -- 9
|
||||||
{ "walk_npc", 1, ceruleanRivalExitDirs(px) }, -- 10
|
{ "play_music", "Music_MeetRival", { start = "rival" } }, -- 10
|
||||||
{ "hide_object", "CERULEAN_CITY", "CERULEANCITY_RIVAL" }, -- 11
|
{ "walk_npc", 1, ceruleanRivalExitDirs(px) }, -- 11
|
||||||
|
{ "play_default_music" }, -- scripts/CeruleanCity.asm:230
|
||||||
|
{ "hide_object", "CERULEAN_CITY", "CERULEANCITY_RIVAL" }, -- 13
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -706,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
|
-- 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
|
-- page has typed out (PrintText returning), and auto.tick then runs once
|
||||||
-- per frame while the gate it returns still reads as playing.
|
-- 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")
|
local Music = require("src.core.Music")
|
||||||
-- .findMatchingFacingDirectionLoop: the rotation picks up at the entry
|
-- .findMatchingFacingDirectionLoop: the rotation picks up at the entry
|
||||||
-- matching the sprite's current facing (showMapText has just turned it
|
-- matching the sprite's current facing (showMapText has just turned it
|
||||||
@@ -752,7 +788,13 @@ local function jigglypuffDance(game, npc)
|
|||||||
if npc then npc.facing = JIGGLYPUFF_SPIN[step] end
|
if npc then npc.facing = JIGGLYPUFF_SPIN[step] end
|
||||||
return
|
return
|
||||||
end
|
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,
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
@@ -765,7 +807,7 @@ M.PEWTER_POKECENTER = {
|
|||||||
local TextBox = require("src.render.TextBox")
|
local TextBox = require("src.render.TextBox")
|
||||||
game.stack:push(TextBox.new(game,
|
game.stack:push(TextBox.new(game,
|
||||||
text(game)._PewterPokecenterJigglypuffText or "JIGGLYPUFF: Puu\npupuu!",
|
text(game)._PewterPokecenterJigglypuffText or "JIGGLYPUFF: Puu\npupuu!",
|
||||||
done, { auto = jigglypuffDance(game, npc) }))
|
done, { auto = jigglypuffDance(game, npc, ow) }))
|
||||||
end,
|
end,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -840,12 +882,14 @@ M.SILPH_CO_7F = {
|
|||||||
{ "face_object", 9, "up" }, -- 4
|
{ "face_object", 9, "up" }, -- 4
|
||||||
{ "show_text", "_SilphCo7FRivalWaitedHereText" }, -- 5
|
{ "show_text", "_SilphCo7FRivalWaitedHereText" }, -- 5
|
||||||
{ "rival_battle", "OPP_RIVAL2", 7 }, -- 6
|
{ "rival_battle", "OPP_RIVAL2", 7 }, -- 6
|
||||||
{ "jump_if_false", 12 }, -- 7
|
{ "jump_if_false", 14 }, -- 7
|
||||||
{ "set_flag", "EVENT_BEAT_SILPH_CO_RIVAL" }, -- 8
|
{ "set_flag", "EVENT_BEAT_SILPH_CO_RIVAL" }, -- 8
|
||||||
{ "show_text", "_SilphCo7FRivalDefeatedText" }, -- 9
|
{ "show_text", "_SilphCo7FRivalDefeatedText" }, -- 9
|
||||||
{ "show_text", "_SilphCo7FRivalGoodLuckToYouText" }, -- 10
|
{ "show_text", "_SilphCo7FRivalGoodLuckToYouText" }, -- 10
|
||||||
{ "move_npc_to", 9, 5, y + 1 }, -- 11
|
{ "play_music", "Music_MeetRival", { start = "rival" } }, -- 11
|
||||||
{ "hide_object", "SILPH_CO_7F", "SILPHCO7F_RIVAL" }, -- 12
|
{ "move_npc_to", 9, 5, y + 1 }, -- 12
|
||||||
|
{ "play_default_music" }, -- scripts/SilphCo7F.asm:261
|
||||||
|
{ "hide_object", "SILPH_CO_7F", "SILPHCO7F_RIVAL" }, -- 14
|
||||||
}, "down")
|
}, "down")
|
||||||
end,
|
end,
|
||||||
}
|
}
|
||||||
@@ -875,12 +919,14 @@ M.SS_ANNE_2F = {
|
|||||||
{ "face_object", 2, onLeft and "down" or "right" }, -- 3
|
{ "face_object", 2, onLeft and "down" or "right" }, -- 3
|
||||||
{ "show_text", "_SSAnne2FRivalText" }, -- 4
|
{ "show_text", "_SSAnne2FRivalText" }, -- 4
|
||||||
{ "rival_battle", "OPP_RIVAL2", 1 }, -- 5
|
{ "rival_battle", "OPP_RIVAL2", 1 }, -- 5
|
||||||
{ "jump_if_false", 11 }, -- 6
|
{ "jump_if_false", 13 }, -- 6
|
||||||
{ "set_flag", "EVENT_BEAT_SS_ANNE_RIVAL" }, -- 7
|
{ "set_flag", "EVENT_BEAT_SS_ANNE_RIVAL" }, -- 7
|
||||||
{ "show_text", "_SSAnne2FRivalDefeatedText" }, -- 8
|
{ "show_text", "_SSAnne2FRivalDefeatedText" }, -- 8
|
||||||
{ "show_text", "_SSAnne2FRivalCutMasterText" }, -- 9
|
{ "show_text", "_SSAnne2FRivalCutMasterText" }, -- 9
|
||||||
{ "walk_npc", 2, ssAnne2FRivalExitDirs(onLeft) }, -- 10
|
{ "play_music", "Music_MeetRival", { start = "rival" } }, -- 10
|
||||||
{ "hide_object", "SS_ANNE_2F", "SSANNE2F_RIVAL" }, -- 11
|
{ "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")
|
}, onLeft and "up" or "left")
|
||||||
end,
|
end,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,9 +12,13 @@ local function push(game, s, done)
|
|||||||
game.stack:push(TextBox.new(game, s, done))
|
game.stack:push(TextBox.new(game, s, done))
|
||||||
end
|
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 function ask(game, s, cb)
|
||||||
local ChoiceBox = require("src.ui.ChoiceBox")
|
local TextBox = require("src.render.TextBox")
|
||||||
push(game, s, function() game.stack:push(ChoiceBox.new(game, cb)) end)
|
game.stack:push(TextBox.new(game, s, nil, { choice = cb }))
|
||||||
end
|
end
|
||||||
|
|
||||||
-- -------------------------------------------------------------------
|
-- -------------------------------------------------------------------
|
||||||
@@ -118,6 +122,7 @@ local MANSION_HOLES = {
|
|||||||
M.POKEMON_MANSION_3F.onStep = function(game, ow, x, y)
|
M.POKEMON_MANSION_3F.onStep = function(game, ow, x, y)
|
||||||
for _, h in ipairs(MANSION_HOLES) do
|
for _, h in ipairs(MANSION_HOLES) do
|
||||||
if x == h[1] and y == h[2] then
|
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)
|
ow:startWarpTo(h[3], h[4], h[5], ow.player.facing)
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -18,6 +18,24 @@
|
|||||||
-- script). Leaders are not def_trainers entries, so engageTrainer has
|
-- script). Leaders are not def_trainers entries, so engageTrainer has
|
||||||
-- no header.won -- checkVictoryRewards shows this chain instead of a
|
-- no header.won -- checkVictoryRewards shows this chain instead of a
|
||||||
-- synthetic "received badge/TM" stub.
|
-- 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 function range(prefix, first, last)
|
||||||
local t = {}
|
local t = {}
|
||||||
@@ -33,83 +51,136 @@ return {
|
|||||||
-- escort NPC and the first Route 22 rival stay gone after the badge.
|
-- escort NPC and the first Route 22 rival stay gone after the badge.
|
||||||
["OPP_BROCK#1"] = { badge = "BOULDERBADGE", flag = "EVENT_BEAT_BROCK",
|
["OPP_BROCK#1"] = { badge = "BOULDERBADGE", flag = "EVENT_BEAT_BROCK",
|
||||||
item = "TM_BIDE",
|
item = "TM_BIDE",
|
||||||
|
gotFlag = "EVENT_GOT_TM34",
|
||||||
|
noRoom = "_PewterGymTM34NoRoomText",
|
||||||
deactivate = { "EVENT_BEAT_PEWTER_GYM_TRAINER_0" },
|
deactivate = { "EVENT_BEAT_PEWTER_GYM_TRAINER_0" },
|
||||||
hide = {
|
hide = {
|
||||||
{ "PEWTER_CITY", "PEWTERCITY_YOUNGSTER" },
|
{ "PEWTER_CITY", "PEWTERCITY_YOUNGSTER" },
|
||||||
{ "ROUTE_22", "ROUTE22_RIVAL1" },
|
{ "ROUTE_22", "ROUTE22_RIVAL1" },
|
||||||
},
|
},
|
||||||
|
badgeSound = "Get_Item1", -- sound_level_up
|
||||||
|
tmSound = "Get_Item1",
|
||||||
dialogue = {
|
dialogue = {
|
||||||
"_PewterGymBrockReceivedBoulderBadgeText",
|
"_PewterGymBrockReceivedBoulderBadgeText",
|
||||||
"_PewterGymBrockBoulderBadgeInfoText",
|
"_PewterGymBrockBoulderBadgeInfoText",
|
||||||
"_PewterGymBrockWaitTakeThisText",
|
},
|
||||||
|
tmPre = { "_PewterGymBrockWaitTakeThisText" },
|
||||||
|
tmDialogue = {
|
||||||
"_PewterGymReceivedTM34Text",
|
"_PewterGymReceivedTM34Text",
|
||||||
"_TM34ExplanationText",
|
"_TM34ExplanationText",
|
||||||
} },
|
} },
|
||||||
["OPP_MISTY#1"] = { badge = "CASCADEBADGE", flag = "EVENT_BEAT_MISTY",
|
["OPP_MISTY#1"] = { badge = "CASCADEBADGE", flag = "EVENT_BEAT_MISTY",
|
||||||
item = "TM_BUBBLEBEAM",
|
item = "TM_BUBBLEBEAM",
|
||||||
|
gotFlag = "EVENT_GOT_TM11",
|
||||||
|
noRoom = "_CeruleanGymMistyTM11NoRoomText",
|
||||||
deactivate = range("EVENT_BEAT_CERULEAN_GYM_TRAINER_", 0, 1),
|
deactivate = range("EVENT_BEAT_CERULEAN_GYM_TRAINER_", 0, 1),
|
||||||
|
badgeSound = "Get_Key_Item",
|
||||||
|
tmSound = "Get_Item1",
|
||||||
dialogue = {
|
dialogue = {
|
||||||
"_CeruleanGymMistyReceivedCascadeBadgeText",
|
"_CeruleanGymMistyReceivedCascadeBadgeText",
|
||||||
"_CeruleanGymMistyCascadeBadgeInfoText",
|
},
|
||||||
|
tmPre = { "_CeruleanGymMistyCascadeBadgeInfoText" },
|
||||||
|
tmDialogue = {
|
||||||
"_CeruleanGymMistyReceivedTM11Text",
|
"_CeruleanGymMistyReceivedTM11Text",
|
||||||
} },
|
} },
|
||||||
["OPP_LT_SURGE#1"] = { badge = "THUNDERBADGE", flag = "EVENT_BEAT_LT_SURGE",
|
["OPP_LT_SURGE#1"] = { badge = "THUNDERBADGE", flag = "EVENT_BEAT_LT_SURGE",
|
||||||
item = "TM_THUNDERBOLT",
|
item = "TM_THUNDERBOLT",
|
||||||
|
gotFlag = "EVENT_GOT_TM24",
|
||||||
|
noRoom = "_VermilionGymLTSurgeTM24NoRoomText",
|
||||||
deactivate = range("EVENT_BEAT_VERMILION_GYM_TRAINER_", 0, 2),
|
deactivate = range("EVENT_BEAT_VERMILION_GYM_TRAINER_", 0, 2),
|
||||||
|
tmSound = "Get_Key_Item",
|
||||||
dialogue = {
|
dialogue = {
|
||||||
"_VermilionGymLTSurgeReceivedThunderBadgeText",
|
"_VermilionGymLTSurgeReceivedThunderBadgeText",
|
||||||
"_VermilionGymLTSurgeThunderBadgeInfoText",
|
},
|
||||||
|
tmPre = { "_VermilionGymLTSurgeThunderBadgeInfoText" },
|
||||||
|
tmDialogue = {
|
||||||
"_VermilionGymLTSurgeReceivedTM24Text",
|
"_VermilionGymLTSurgeReceivedTM24Text",
|
||||||
"_TM24ExplanationText",
|
"_TM24ExplanationText",
|
||||||
} },
|
} },
|
||||||
["OPP_ERIKA#1"] = { badge = "RAINBOWBADGE", flag = "EVENT_BEAT_ERIKA",
|
["OPP_ERIKA#1"] = { badge = "RAINBOWBADGE", flag = "EVENT_BEAT_ERIKA",
|
||||||
item = "TM_MEGA_DRAIN",
|
item = "TM_MEGA_DRAIN",
|
||||||
|
gotFlag = "EVENT_GOT_TM21",
|
||||||
|
noRoom = "_CeladonGymTM21NoRoomText",
|
||||||
deactivate = range("EVENT_BEAT_CELADON_GYM_TRAINER_", 0, 6),
|
deactivate = range("EVENT_BEAT_CELADON_GYM_TRAINER_", 0, 6),
|
||||||
|
tmSound = "Get_Item1",
|
||||||
dialogue = {
|
dialogue = {
|
||||||
"_CeladonGymErikaReceivedRainbowBadgeText",
|
"_CeladonGymErikaReceivedRainbowBadgeText",
|
||||||
"_CeladonGymRainbowBadgeInfoText",
|
},
|
||||||
|
tmPre = { "_CeladonGymRainbowBadgeInfoText" },
|
||||||
|
tmDialogue = {
|
||||||
"_CeladonGymReceivedTM21Text",
|
"_CeladonGymReceivedTM21Text",
|
||||||
"_TM21ExplanationText",
|
"_TM21ExplanationText",
|
||||||
} },
|
} },
|
||||||
["OPP_KOGA#1"] = { badge = "SOULBADGE", flag = "EVENT_BEAT_KOGA",
|
["OPP_KOGA#1"] = { badge = "SOULBADGE", flag = "EVENT_BEAT_KOGA",
|
||||||
item = "TM_TOXIC",
|
item = "TM_TOXIC",
|
||||||
|
gotFlag = "EVENT_GOT_TM06",
|
||||||
|
noRoom = "_FuchsiaGymKogaTM06NoRoomText",
|
||||||
deactivate = range("EVENT_BEAT_FUCHSIA_GYM_TRAINER_", 0, 5),
|
deactivate = range("EVENT_BEAT_FUCHSIA_GYM_TRAINER_", 0, 5),
|
||||||
|
tmSound = "Get_Key_Item",
|
||||||
dialogue = {
|
dialogue = {
|
||||||
"_FuchsiaGymKogaReceivedSoulBadgeText",
|
"_FuchsiaGymKogaReceivedSoulBadgeText",
|
||||||
"_FuchsiaGymKogaSoulBadgeInfoText",
|
},
|
||||||
|
tmPre = { "_FuchsiaGymKogaSoulBadgeInfoText" },
|
||||||
|
tmDialogue = {
|
||||||
"_FuchsiaGymKogaReceivedTM06Text",
|
"_FuchsiaGymKogaReceivedTM06Text",
|
||||||
"_FuchsiaGymKogaTM06ExplanationText",
|
"_FuchsiaGymKogaTM06ExplanationText",
|
||||||
} },
|
} },
|
||||||
["OPP_SABRINA#1"] = { badge = "MARSHBADGE", flag = "EVENT_BEAT_SABRINA",
|
["OPP_SABRINA#1"] = { badge = "MARSHBADGE", flag = "EVENT_BEAT_SABRINA",
|
||||||
item = "TM_PSYWAVE",
|
item = "TM_PSYWAVE",
|
||||||
|
gotFlag = "EVENT_GOT_TM46",
|
||||||
|
noRoom = "_SaffronGymSabrinaTM46NoRoomText",
|
||||||
deactivate = range("EVENT_BEAT_SAFFRON_GYM_TRAINER_", 0, 6),
|
deactivate = range("EVENT_BEAT_SAFFRON_GYM_TRAINER_", 0, 6),
|
||||||
|
badgeSound = "Get_Key_Item",
|
||||||
|
tmSound = "Get_Item1",
|
||||||
dialogue = {
|
dialogue = {
|
||||||
"_SaffronGymSabrinaReceivedMarshBadgeText",
|
"_SaffronGymSabrinaReceivedMarshBadgeText",
|
||||||
"_SaffronGymSabrinaMarshBadgeInfoText",
|
},
|
||||||
|
tmPre = { "_SaffronGymSabrinaMarshBadgeInfoText" },
|
||||||
|
tmDialogue = {
|
||||||
"_SaffronGymSabrinaReceivedTM46Text",
|
"_SaffronGymSabrinaReceivedTM46Text",
|
||||||
"_TM46ExplanationText",
|
"_TM46ExplanationText",
|
||||||
} },
|
} },
|
||||||
["OPP_BLAINE#1"] = { badge = "VOLCANOBADGE", flag = "EVENT_BEAT_BLAINE",
|
["OPP_BLAINE#1"] = { badge = "VOLCANOBADGE", flag = "EVENT_BEAT_BLAINE",
|
||||||
item = "TM_FIRE_BLAST",
|
item = "TM_FIRE_BLAST",
|
||||||
|
gotFlag = "EVENT_GOT_TM38",
|
||||||
|
noRoom = "_CinnabarGymBlaineTM38NoRoomText",
|
||||||
deactivate = range("EVENT_BEAT_CINNABAR_GYM_TRAINER_", 0, 6),
|
deactivate = range("EVENT_BEAT_CINNABAR_GYM_TRAINER_", 0, 6),
|
||||||
|
badgeSound = "Get_Key_Item",
|
||||||
|
tmSound = "Get_Item1",
|
||||||
dialogue = {
|
dialogue = {
|
||||||
"_CinnabarGymBlaineReceivedVolcanoBadgeText",
|
"_CinnabarGymBlaineReceivedVolcanoBadgeText",
|
||||||
"_CinnabarGymBlaineVolcanoBadgeInfoText",
|
},
|
||||||
|
tmPre = { "_CinnabarGymBlaineVolcanoBadgeInfoText" },
|
||||||
|
tmDialogue = {
|
||||||
"_CinnabarGymBlaineReceivedTM38Text",
|
"_CinnabarGymBlaineReceivedTM38Text",
|
||||||
"_CinnabarGymBlaineTM38ExplanationText",
|
"_CinnabarGymBlaineTM38ExplanationText",
|
||||||
} },
|
} },
|
||||||
["OPP_GIOVANNI#3"] = { badge = "EARTHBADGE", flag = "EVENT_BEAT_GIOVANNI",
|
["OPP_GIOVANNI#3"] = { badge = "EARTHBADGE", flag = "EVENT_BEAT_GIOVANNI",
|
||||||
item = "TM_FISSURE",
|
item = "TM_FISSURE",
|
||||||
|
gotFlag = "EVENT_GOT_TM27",
|
||||||
|
noRoom = "_ViridianGymGiovanniTM27NoRoomText",
|
||||||
deactivate = range("EVENT_BEAT_VIRIDIAN_GYM_TRAINER_", 0, 7),
|
deactivate = range("EVENT_BEAT_VIRIDIAN_GYM_TRAINER_", 0, 7),
|
||||||
|
badgeSound = "Get_Item1", -- sound_level_up
|
||||||
|
tmSound = "Get_Item1",
|
||||||
dialogue = {
|
dialogue = {
|
||||||
"_ViridianGymGiovanniReceivedEarthBadgeText",
|
"_ViridianGymGiovanniReceivedEarthBadgeText",
|
||||||
"_ViridianGymGiovanniEarthBadgeInfoText",
|
},
|
||||||
|
tmPre = { "_ViridianGymGiovanniEarthBadgeInfoText" },
|
||||||
|
tmDialogue = {
|
||||||
"_ViridianGymGiovanniReceivedTM27Text",
|
"_ViridianGymGiovanniReceivedTM27Text",
|
||||||
"_ViridianGymGiovanniTM27ExplanationText",
|
"_ViridianGymGiovanniTM27ExplanationText",
|
||||||
} },
|
} },
|
||||||
|
|
||||||
-- Silph Co. Giovanni: unlocks the president's Master Ball gift
|
-- Silph Co. Giovanni: unlocks the president's Master Ball gift.
|
||||||
["OPP_GIOVANNI#2"] = { flag = "EVENT_BEAT_SILPH_CO_GIOVANNI" },
|
-- 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
|
-- Fighting Dojo Karate Master (scripts/FightingDojo.asm
|
||||||
-- FightingDojoKarateMasterPostBattleScript sets EVENT_BEAT_KARATE_MASTER,
|
-- FightingDojoKarateMasterPostBattleScript sets EVENT_BEAT_KARATE_MASTER,
|
||||||
|
|||||||
@@ -36,7 +36,8 @@ M.CERULEAN_MELANIES_HOUSE = {
|
|||||||
rows[#rows + 1] = { "label", "declined" }
|
rows[#rows + 1] = { "label", "declined" }
|
||||||
rows[#rows + 1] = { "show_text", "MelanieText5" }
|
rows[#rows + 1] = { "show_text", "MelanieText5" }
|
||||||
end
|
end
|
||||||
ow.runner:run(rows, { npc = npc, onDone = done })
|
ow.runner:run(rows, { npc = npc, onDone = done,
|
||||||
|
checkpointOnDone = "release_npc" })
|
||||||
end,
|
end,
|
||||||
-- pet flavor: the text with the species' cry over it
|
-- pet flavor: the text with the species' cry over it
|
||||||
TEXT_CERULEANMELANIESHOUSE_BULBASAUR = {
|
TEXT_CERULEANMELANIESHOUSE_BULBASAUR = {
|
||||||
@@ -105,7 +106,8 @@ M.VERMILION_CITY = {
|
|||||||
rows[#rows + 1] = { "label", "declined" }
|
rows[#rows + 1] = { "label", "declined" }
|
||||||
rows[#rows + 1] = { "show_text", "_OfficerJennyText4" }
|
rows[#rows + 1] = { "show_text", "_OfficerJennyText4" }
|
||||||
end
|
end
|
||||||
ow.runner:run(rows, { npc = npc, onDone = done })
|
ow.runner:run(rows, { npc = npc, onDone = done,
|
||||||
|
checkpointOnDone = "release_npc" })
|
||||||
end,
|
end,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -62,10 +62,15 @@ M.MT_MOON_B2F = {
|
|||||||
{ "walk_npc", 6, { "left", "left", "left", "left", "left" } },
|
{ "walk_npc", 6, { "left", "left", "left", "left", "left" } },
|
||||||
{ "face_object", 6, "left" },
|
{ "face_object", 6, "left" },
|
||||||
{ "show_text", "_MtMoonJessieJamesText2" },
|
{ "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 },
|
{ "start_battle", "trainer", "OPP_ROCKET", 42 },
|
||||||
{ "check_battle_result", "win" },
|
{ "check_battle_result", "win" },
|
||||||
{ "jump_if_false", "end" },
|
{ "jump_if_false", "end" },
|
||||||
{ "show_text", "_MtMoonJessieJamesText3" },
|
|
||||||
{ "show_text", "_MtMoonJessieJamesText4" },
|
{ "show_text", "_MtMoonJessieJamesText4" },
|
||||||
{ "stop_music" },
|
{ "stop_music" },
|
||||||
{ "play_music", "Music_MeetJessieJames" },
|
{ "play_music", "Music_MeetJessieJames" },
|
||||||
@@ -85,7 +90,8 @@ M.MT_MOON_B2F = {
|
|||||||
-- motto plays from off-screen FIRST, then the duo pops in at (25,10) /
|
-- 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
|
-- (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
|
-- $19=25, EVENT_ROCKET_HIDEOUT_4_JESSIE_JAMES_ON_LEFT) walks the three
|
||||||
-- tiles down to loom over the player while the other steps one. A loss
|
-- 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
|
-- re-hides them (RocketHideoutB4FResetScripts via EVENT_6A0), so the
|
||||||
-- trigger re-arms clean.
|
-- trigger re-arms clean.
|
||||||
-- -------------------------------------------------------------------
|
-- -------------------------------------------------------------------
|
||||||
@@ -106,7 +112,7 @@ M.ROCKET_HIDEOUT_B4F = {
|
|||||||
if f.EVENT_BEAT_ROCKET_HIDEOUT_4_JESSIE_JAMES 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
|
-- ON_LEFT: player under James's column (25); movement data pairs
|
||||||
-- RocketHideoutB4FJessieJamesMovementData_45605/45606 swap so the
|
-- RocketHideoutB4FJessieJamesMovementData_45605/45606 swap so the
|
||||||
-- column-mate walks 3, the other 1.
|
-- column-mate walks 3, the other 4.
|
||||||
local onLeft = (x == 25)
|
local onLeft = (x == 25)
|
||||||
ow.runner:run({
|
ow.runner:run({
|
||||||
{ "stop_music" },
|
{ "stop_music" },
|
||||||
@@ -116,16 +122,30 @@ M.ROCKET_HIDEOUT_B4F = {
|
|||||||
{ "emote", "player", "shock", 30 },
|
{ "emote", "player", "shock", 30 },
|
||||||
{ "show_object", "ROCKET_HIDEOUT_B4F", "ROCKETHIDEOUTB4F_JAMES" },
|
{ "show_object", "ROCKET_HIDEOUT_B4F", "ROCKETHIDEOUTB4F_JAMES" },
|
||||||
{ "show_object", "ROCKET_HIDEOUT_B4F", "ROCKETHIDEOUTB4F_JESSIE" },
|
{ "show_object", "ROCKET_HIDEOUT_B4F", "ROCKETHIDEOUTB4F_JESSIE" },
|
||||||
-- James (object 2) then Jessie (object 3), Script4..Script9 order
|
-- James (object 2) then Jessie (object 3), Script4..Script9 order.
|
||||||
{ "walk_npc", 2, onLeft and { "down", "down", "down" } or { "down" } },
|
-- 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" },
|
{ "face_object", 2, onLeft and "down" or "left" },
|
||||||
{ "walk_npc", 3, onLeft and { "down" } or { "down", "down", "down" } },
|
{ "walk_npc", 3, onLeft and { "down", "down", "down", "down" }
|
||||||
|
or { "down", "down", "down" } },
|
||||||
{ "face_object", 3, onLeft and "right" or "down" },
|
{ "face_object", 3, onLeft and "right" or "down" },
|
||||||
{ "show_text", "_RocketHideoutJessieJamesText2" },
|
{ "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 },
|
{ "start_battle", "trainer", "OPP_ROCKET", 43 },
|
||||||
{ "check_battle_result", "win" },
|
{ "check_battle_result", "win" },
|
||||||
{ "jump_if_false", "lost" },
|
{ "jump_if_false", "lost" },
|
||||||
{ "show_text", "_RocketHideoutJessieJamesText3" },
|
|
||||||
{ "show_text", "_RocketHideoutJessieJamesText4" },
|
{ "show_text", "_RocketHideoutJessieJamesText4" },
|
||||||
{ "stop_music" },
|
{ "stop_music" },
|
||||||
{ "play_music", "Music_MeetJessieJames" },
|
{ "play_music", "Music_MeetJessieJames" },
|
||||||
@@ -175,16 +195,27 @@ M.POKEMON_TOWER_7F = {
|
|||||||
{ "show_text", "_PokemonTowerJessieJamesText1" },
|
{ "show_text", "_PokemonTowerJessieJamesText1" },
|
||||||
{ "face_player_dir", "up" },
|
{ "face_player_dir", "up" },
|
||||||
{ "emote", "player", "shock", 30 },
|
{ "emote", "player", "shock", 30 },
|
||||||
-- Jessie (object 1) then James (object 2), Script1..Script6 order
|
-- Jessie (object 1) then James (object 2), Script1..Script6 order.
|
||||||
{ "walk_npc", 1, onLeft and { "down" } or { "down", "down", "down" } },
|
-- 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" },
|
{ "face_object", 1, onLeft and "right" or "down" },
|
||||||
{ "walk_npc", 2, onLeft and { "down", "down", "down" } or { "down" } },
|
{ "walk_npc", 2, onLeft and { "down", "down", "down" }
|
||||||
|
or { "down", "down", "down", "down" } },
|
||||||
{ "face_object", 2, onLeft and "down" or "left" },
|
{ "face_object", 2, onLeft and "down" or "left" },
|
||||||
{ "show_text", "_PokemonTowerJessieJamesText2" },
|
{ "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 },
|
{ "start_battle", "trainer", "OPP_ROCKET", 44 },
|
||||||
{ "check_battle_result", "win" },
|
{ "check_battle_result", "win" },
|
||||||
{ "jump_if_false", "end" },
|
{ "jump_if_false", "end" },
|
||||||
{ "show_text", "_PokemonTowerJessieJamesText3" },
|
|
||||||
{ "show_text", "_PokemonTowerJessieJamesText4" },
|
{ "show_text", "_PokemonTowerJessieJamesText4" },
|
||||||
{ "stop_music" },
|
{ "stop_music" },
|
||||||
{ "play_music", "Music_MeetJessieJames" },
|
{ "play_music", "Music_MeetJessieJames" },
|
||||||
@@ -254,10 +285,12 @@ M.SILPH_CO_11F = {
|
|||||||
{ "walk_npc", 6, jessieDirs },
|
{ "walk_npc", 6, jessieDirs },
|
||||||
{ "face_object", 6, jessieFace },
|
{ "face_object", 6, jessieFace },
|
||||||
{ "show_text", "_SilphCoJessieJamesText2" },
|
{ "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 },
|
{ "start_battle", "trainer", "OPP_ROCKET", 45 },
|
||||||
{ "check_battle_result", "win" },
|
{ "check_battle_result", "win" },
|
||||||
{ "jump_if_false", "end" },
|
{ "jump_if_false", "end" },
|
||||||
{ "show_text", "_SilphCoJessieJamesText3" },
|
|
||||||
{ "show_text", "_SilphCoJessieJamesText4" },
|
{ "show_text", "_SilphCoJessieJamesText4" },
|
||||||
{ "stop_music" },
|
{ "stop_music" },
|
||||||
{ "play_music", "Music_MeetJessieJames" },
|
{ "play_music", "Music_MeetJessieJames" },
|
||||||
|
|||||||
@@ -0,0 +1,152 @@
|
|||||||
|
-- The Viridian City old-man catch tutorial, Yellow's way
|
||||||
|
-- (pokeyellow scripts/ViridianCity.asm, scripts/ViridianCity_2.asm,
|
||||||
|
-- scripts/OaksLab.asm). Registered on top of the shared tables by
|
||||||
|
-- data/scripts/init.lua on a Yellow boot.
|
||||||
|
--
|
||||||
|
-- Yellow has TWO gambler objects where Red/Blue have one:
|
||||||
|
-- * VIRIDIANCITY_OLD_MAN at (17,5) -- a Red/Blue leftover; its
|
||||||
|
-- toggle stays OFF forever in Yellow, no script ever shows it.
|
||||||
|
-- * VIRIDIANCITY_OLD_MAN2 at (18,9) -- replaces the sleeper the
|
||||||
|
-- moment the Pokédex is given (OaksLabOakGivesPokedexScript:
|
||||||
|
-- HideObject TOGGLE_LYING_OLD_MAN / ShowObject TOGGLE_OLD_MAN_2).
|
||||||
|
--
|
||||||
|
-- This is the tutorial old man. The Red/Blue "Are you in a hurry?"
|
||||||
|
-- yes/no script must NOT run against him: Yellow's
|
||||||
|
-- _ViridianCityOldManHadMyCoffeeNowText is the apology speech ("I've had
|
||||||
|
-- my coffee now ... I'll show you how to catch POKéMON as my apology"),
|
||||||
|
-- and the shared story.lua TEXT_VIRIDIANCITY_OLD_MAN rows hang an
|
||||||
|
-- invented yes/no over it -- YES printed the TimeIsMoney alias
|
||||||
|
-- (_ViridianCityOldManLosingMyTouchText) and NO ran the demo, every
|
||||||
|
-- talk, forever (#617).
|
||||||
|
--
|
||||||
|
-- The real flow (ViridianCityCheckWaitingOldMan + ViridianCityOldMan2Text
|
||||||
|
-- + ViridianCityOldManInitialCatchTrainingScript + ...EndInitial... +
|
||||||
|
-- ViridianCityPostInitialCatchTraining): stepping into (19,9) -- the gap
|
||||||
|
-- east of the sleeper's cell -- faces the old man right and the player
|
||||||
|
-- left, prints the apology, and without any choice runs the demo battle
|
||||||
|
-- (BATTLE_TYPE_OLD_MAN, RATTATA lvl 5), which he FAILS -- the ball shakes
|
||||||
|
-- three times and breaks open. After it, the same text pointer
|
||||||
|
-- now prints _ViridianCityOldManLosingMyTouchText ("That didn't work!
|
||||||
|
-- I must be losing my touch."), the old man walks off (down 6 with the
|
||||||
|
-- player on (19,9), right 1 otherwise, Pikachu nudged out of the way
|
||||||
|
-- first) and TOGGLE_OLD_MAN_2 hides. A direct talk does the same.
|
||||||
|
|
||||||
|
local M = {}
|
||||||
|
|
||||||
|
local OLD_MAN2 = "VIRIDIANCITY_OLD_MAN2"
|
||||||
|
|
||||||
|
-- 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.
|
||||||
|
-- story5's VIRIDIAN_CITY.onStep chains story.lua's sleeping-old-man
|
||||||
|
-- gate and its own gym-lock step (same pattern as yellow_jessie_james).
|
||||||
|
local baseViridianStep = require("data.scripts.story5").VIRIDIAN_CITY.onStep
|
||||||
|
|
||||||
|
-- pokeyellow text/ViridianCity.asm, _ViridianCityOldManHadMyCoffeeNowText
|
||||||
|
-- and _ViridianCityOldManLosingMyTouchText, spelled with the extractor's
|
||||||
|
-- markers (line -> \n, cont -> \v, para -> \f)
|
||||||
|
local function text(game)
|
||||||
|
return {
|
||||||
|
apology = game.data.text._ViridianCityOldManHadMyCoffeeNowText
|
||||||
|
or "Ahh, I've had my\ncoffee now and I\vfeel great!\fSure, you can go\n"
|
||||||
|
.. "through!\fI'm sorry I was\nso rude to you!\fI see you're using\n"
|
||||||
|
.. "a POKéDEX.\fI'll show you how\nto catch POKéMON\vas my apology.",
|
||||||
|
losingMyTouch = game.data.text._ViridianCityOldManLosingMyTouchText
|
||||||
|
or "That didn't work!\nI must be losing\vmy touch.\fI've run out of\n"
|
||||||
|
.. "POKé BALLs too.\fI have to get some\nat POKéMON MART.",
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
-- The row list for the initial-tutorial branch, keyed by where the
|
||||||
|
-- player stands when the battle ends (ViridianCityPostInitialCatchTraining
|
||||||
|
-- reads wXCoord: (19,9) walks the old man down the corridor, anywhere
|
||||||
|
-- else walks him right 1 after moving the follower Pikachu aside).
|
||||||
|
local function oldMan2Rows(game, ow, npc)
|
||||||
|
local rows = {
|
||||||
|
{ "show_text", "_ViridianCityOldManHadMyCoffeeNowText" },
|
||||||
|
-- ViridianCityOldManInitialCatchTrainingScript sets
|
||||||
|
-- EVENT_INITIAL_CATCH_TRAINING before the battle runs, and
|
||||||
|
-- ItemUseBall's .oldManBattle branch turns that event into anim data
|
||||||
|
-- $63: three shakes, then the ball breaks open. The losing-my-touch
|
||||||
|
-- line below only follows a throw that failed (#636).
|
||||||
|
{ "old_man_demo", "fail" },
|
||||||
|
{ "set_flag", "EVENT_COMPLETED_CATCH_TRAINING" },
|
||||||
|
{ "show_text", "_ViridianCityOldManLosingMyTouchText" },
|
||||||
|
}
|
||||||
|
if ow.player and ow.player.cellX == 19 then
|
||||||
|
rows[#rows + 1] =
|
||||||
|
{ "walk_npc", npc.def.index,
|
||||||
|
{ "down", "down", "down", "down", "down", "down" } }
|
||||||
|
else
|
||||||
|
-- ViridianCityMovePikachu (scripts/ViridianCity_2.asm): Pikachu
|
||||||
|
-- steps out of the old man's way before he turns right
|
||||||
|
local PikachuFollower = require("src.world.PikachuFollower")
|
||||||
|
local pika = ow and PikachuFollower.current(ow)
|
||||||
|
if pika then
|
||||||
|
rows[#rows + 1] = { "walk_npc", pika.def.index, { "right" } }
|
||||||
|
end
|
||||||
|
rows[#rows + 1] = { "walk_npc", npc.def.index, { "right" } }
|
||||||
|
end
|
||||||
|
rows[#rows + 1] =
|
||||||
|
{ "hide_object", "VIRIDIAN_CITY", OLD_MAN2 }
|
||||||
|
return rows
|
||||||
|
end
|
||||||
|
|
||||||
|
-- The shared talk handler: TEXT_VIRIDIANCITY_OLD_MAN2's text_asm branch
|
||||||
|
-- (ViridianCityOldMan2Text) on EVENT_COMPLETED_CATCH_TRAINING.
|
||||||
|
local function oldMan2Talk(game, ow, npc, done)
|
||||||
|
if game.save.flags and game.save.flags.EVENT_COMPLETED_CATCH_TRAINING then
|
||||||
|
local TextBox = require("src.render.TextBox")
|
||||||
|
game.stack:push(TextBox.new(game, text(game).losingMyTouch, done))
|
||||||
|
return
|
||||||
|
end
|
||||||
|
ow.runner:run(oldMan2Rows(game, ow, npc), { npc = npc, onDone = done,
|
||||||
|
checkpointOnDone = "release_npc" })
|
||||||
|
end
|
||||||
|
|
||||||
|
M.VIRIDIAN_CITY = {
|
||||||
|
talk = {
|
||||||
|
TEXT_VIRIDIANCITY_OLD_MAN2 = oldMan2Talk,
|
||||||
|
},
|
||||||
|
|
||||||
|
-- Re-apply the Pokédex swap for a save that already holds the flag but
|
||||||
|
-- was never standing here when it fired (converted .sav imports, same
|
||||||
|
-- shape as story.lua's VIRIDIAN_CITY.onEnter). Yellow shows OLD_MAN2,
|
||||||
|
-- not the Red/Blue OLD_MAN at (17,5), and also puts away a stray
|
||||||
|
-- OLD_MAN a save made by the pre-#617 build left standing.
|
||||||
|
onEnter = function(game, ow)
|
||||||
|
if not (game.save.flags and game.save.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, "VIRIDIAN_CITY", "VIRIDIANCITY_OLD_MAN_SLEEPY")
|
||||||
|
Commands.hide_object(ctx, "VIRIDIAN_CITY", "VIRIDIANCITY_OLD_MAN")
|
||||||
|
Commands.show_object(ctx, "VIRIDIAN_CITY", OLD_MAN2)
|
||||||
|
end,
|
||||||
|
|
||||||
|
-- ViridianCityCheckWaitingOldMan: with the Pokédex held and the
|
||||||
|
-- tutorial undone, (19,9) -- the gap east of the old man, the same
|
||||||
|
-- cell the sleeper used to gate -- faces him right, turns the player
|
||||||
|
-- left and starts the OLD_MAN2 flow with no choice.
|
||||||
|
onStep = function(game, ow, x, y)
|
||||||
|
if baseViridianStep and baseViridianStep(game, ow, x, y) then
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
local flags = game.save.flags
|
||||||
|
if not flags.EVENT_GOT_POKEDEX then return false end
|
||||||
|
if flags.EVENT_COMPLETED_CATCH_TRAINING then return false end
|
||||||
|
if x ~= 19 or y ~= 9 then return false end
|
||||||
|
local man
|
||||||
|
for _, npc in ipairs(ow.npcs) do
|
||||||
|
if npc.def and npc.def.name == OLD_MAN2 then man = npc break end
|
||||||
|
end
|
||||||
|
if not man then return false end
|
||||||
|
man.facing = "right"
|
||||||
|
ow.player.facing = "left"
|
||||||
|
oldMan2Talk(game, ow, man, nil)
|
||||||
|
return true
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
|
||||||
|
return M
|
||||||
@@ -38,7 +38,7 @@ the same core data and graphics into the source tree for verification.
|
|||||||
| | `src/core/SaveData.lua` | Lua-serialized save in the LÖVE save dir |
|
| | `src/core/SaveData.lua` | Lua-serialized save in the LÖVE save dir |
|
||||||
| render | `src/render/Renderer.lua` | 160x144 canvas, integer nearest scaling |
|
| render | `src/render/Renderer.lua` | 160x144 canvas, integer nearest scaling |
|
||||||
| | `src/render/TileRenderer.lua` | one SpriteBatch per map (8x8 quads) + border-block ring |
|
| | `src/render/TileRenderer.lua` | one SpriteBatch per map (8x8 quads) + border-block ring |
|
||||||
| | `src/render/SpriteRenderer.lua` | 6-frame walker sheets, flipped right facing |
|
| | `src/render/SpriteRenderer.lua` | variable-size anchored sprite sheets, 6-frame walkers and flipped right facing |
|
||||||
| | `src/render/Font.lua` | glyph rendering via charmap (greedy longest match) |
|
| | `src/render/Font.lua` | glyph rendering via charmap (greedy longest match) |
|
||||||
| | `src/render/TextBox.lua` | dialogue box: typewriter, `\n` line, `\v` scroll, `\f` page |
|
| | `src/render/TextBox.lua` | dialogue box: typewriter, `\n` line, `\v` scroll, `\f` page |
|
||||||
| | `src/render/Camera.lua`, `Transition.lua` | follow camera, warp fades |
|
| | `src/render/Camera.lua`, `Transition.lua` | follow camera, warp fades |
|
||||||
|
|||||||
@@ -1,515 +0,0 @@
|
|||||||
# Behavior porting notes
|
|
||||||
|
|
||||||
What was ported from pokered's engine code and where it came from.
|
|
||||||
|
|
||||||
## Overworld
|
|
||||||
|
|
||||||
- **Collision rule** (`home/overworld.asm` tile-in-front checks): a 16x16
|
|
||||||
cell is passable when its bottom-left 8x8 tile is in the tileset's
|
|
||||||
`coll_tiles` list. Verified against Pallet Town's fences/houses/water
|
|
||||||
and Oak's Lab furniture.
|
|
||||||
- **Warp activation** (`home/overworld.asm` CheckWarpsNoCollision /
|
|
||||||
ExtraWarpCheck): a warp fires when arriving on a warp whose standing
|
|
||||||
tile is in the tileset's door or warp tile list, or when standing on a
|
|
||||||
warp and walking off the map edge (interior exit mats). Both paths are
|
|
||||||
data-driven from `door_tile_ids.asm` / `warp_tile_ids.asm`.
|
|
||||||
- **LAST_MAP warps** return to the remembered outdoor map/position, like
|
|
||||||
`wLastMap`.
|
|
||||||
- **Connections** (`map_header` connection directives): crossing an edge
|
|
||||||
places the player at `destCoord = curCoord - offset*2` cells on the
|
|
||||||
destination's opposite edge.
|
|
||||||
- **Movement**: tile-by-tile, 1 px/frame at 60 fps (16 frames per step),
|
|
||||||
tap-to-turn without stepping, hold-to-walk, input locked mid-step.
|
|
||||||
- **Wild encounters** (`engine/battle/wild_encounters.asm`): per grass
|
|
||||||
step, encounter iff `rand(0..255) < rate`; slot picked via the
|
|
||||||
cumulative buckets 51/102/141/166/191/216/229/242/253/256.
|
|
||||||
- **Initial object visibility** from `toggleable_objects.asm` (e.g. Oak
|
|
||||||
hidden in his lab), with `show_object`/`hide_object` script commands
|
|
||||||
persisting to the save like the missable-object bits.
|
|
||||||
|
|
||||||
## Pokémon math (`engine/pokemon/calc_stats.asm`, `experience.asm`)
|
|
||||||
|
|
||||||
- `stat = floor(((base + DV)*2 + floor(sqrt(statExp)/4)) * L / 100) + 5`
|
|
||||||
(HP: `+ L + 10`); HP DV from the low bits of the other four DVs.
|
|
||||||
- Growth curves use the exact cubic coefficients (MEDIUM_SLOW =
|
|
||||||
1.2n^3 - 15n^2 + 100n - 140, etc).
|
|
||||||
- Exp gain = `floor(baseExp * level / 7)` (x1.5 for trainer battles);
|
|
||||||
defeated species' base stats accumulate as stat experience.
|
|
||||||
|
|
||||||
## Battle core (`engine/battle/core.asm`)
|
|
||||||
|
|
||||||
- Damage: `floor(floor(2L(x2 crit)/5 + 2) * power * atk / def / 50)`
|
|
||||||
capped at 997, `+2`, STAB x1.5, per-matchup type multipliers applied
|
|
||||||
sequentially (x10 fixed point), then `rand(217..255)/255` when
|
|
||||||
damage > 1.
|
|
||||||
- Critical hits: `rand(0..255) < baseSpeed/2` (x4 for Karate Chop, Razor
|
|
||||||
Leaf, Crabhammer, Slash, capped 255); crits double level and ignore
|
|
||||||
stat stages (gen1_faithful ruleset).
|
|
||||||
- Accuracy: `rand(0..255) < floor(acc*255/100)` after accuracy/evasion
|
|
||||||
stages, including the 1/256 miss at 100% accuracy (toggleable via the
|
|
||||||
`modern_clean` ruleset).
|
|
||||||
- Stat stages use the 25/28/33/40/50/66/100/150/.../400 multiplier table
|
|
||||||
(`data/battle/stat_modifiers.asm`).
|
|
||||||
- Physical/special split by type (special = Water/Grass/Fire/Ice/
|
|
||||||
Electric/Psychic/Dragon).
|
|
||||||
- Status: paralysis speed/4 and 25% full para, burn halves physical
|
|
||||||
attack, poison/burn residual = maxHP/16, sleep 1-7 turns waking on the
|
|
||||||
lost turn, freeze permanent (as in Gen 1).
|
|
||||||
- Turn order: effective speed, coin-flip ties; Quick Attack first,
|
|
||||||
Counter last (Gen 1's only priorities).
|
|
||||||
- Run formula (`TryRunningFromBattle`): always escape if faster,
|
|
||||||
otherwise `floor(pSpd*32 / (eSpd/4)) + 30*attempts` vs `rand(0..255)`.
|
|
||||||
- Catching (`ItemUseBall`): ball-specific rand ranges (255/200/150),
|
|
||||||
status bonus 25/12, second roll `floor(maxHP*255/ballFactor) /
|
|
||||||
floor(HP/4)` capped 255.
|
|
||||||
- Prize money: class base money x last defeated mon's level
|
|
||||||
(`pic_pointers_money.asm`).
|
|
||||||
|
|
||||||
## Battle move effects (engine/battle/core.asm, move_effects/*)
|
|
||||||
|
|
||||||
- Mimic via Metronome (effects.asm:1203-1273): MimicEffect's
|
|
||||||
.letPlayerChooseMove branch snapshots wCurrentMenuItem before the
|
|
||||||
copy-picker menu opens and restores it afterward as the write index
|
|
||||||
into wBattleMonMoves. Since SelectMenuItem always writes
|
|
||||||
wCurrentMenuItem/wPlayerMoveListIndex together at the FIGHT-menu
|
|
||||||
confirm and nothing (including MetronomePickMove) touches either
|
|
||||||
variable during mid-move resolution, the reused value is always the
|
|
||||||
calling move's own slot, BattleState.lua's applyMimic fallback uses
|
|
||||||
self.moveIndex, frozen the same way, so a called Mimic (e.g. from
|
|
||||||
METRONOME in slot 3) overwrites the calling move's own slot, keeping
|
|
||||||
its PP, matching the Gen 1 quirk exactly.
|
|
||||||
- Multi-hit distribution 2/2/2/3/3/3/4/5 over rand(0..7); all hits reuse
|
|
||||||
the first damage roll (faithful).
|
|
||||||
- Recoil = damage/4 (Struggle /2); drain/Dream Eater heal = damage/2;
|
|
||||||
Dream Eater requires sleep.
|
|
||||||
- Fixed damage: SonicBoom 20, Dragon Rage 40, Seismic Toss/Night Shade =
|
|
||||||
level, Psywave rand(1 .. 1.5xlevel-1).
|
|
||||||
- OHKO deals 65535, fails against faster targets; Swift skips accuracy;
|
|
||||||
Jump Kick crash = 1 damage on miss; Explosion halves defense and
|
|
||||||
faints the user even on a miss; Hyper Beam skips recharge if it KOs.
|
|
||||||
- Charge moves (incl. Fly's invulnerable turn), trapping moves locking
|
|
||||||
the victim out of its turns, Thrash's 3-4 turn lock ending in
|
|
||||||
confusion, Bide's 2-3 turn store-and-double, Rage's permanent lock
|
|
||||||
with attack-up on being hit, Counter/Quick Attack priority.
|
|
||||||
- Side-effect chances: 26/256 (10%), 77/256 (30%), stat-down side
|
|
||||||
effects 85/256; Twineedle 20% poison.
|
|
||||||
- Substitute costs 1/4 max HP, absorbs damage, blocks status/stat/side
|
|
||||||
effects; screens double effective defense (bypassed by crits); Focus
|
|
||||||
Energy keeps the Gen 1 quarter-rate bug under gen1_faithful.
|
|
||||||
- Status: sleep 1-7 turns (wake turn is lost), freeze permanent, burn
|
|
||||||
halves physical attack, paralysis speed/4 + 25% full para, Toxic's
|
|
||||||
rising counter, Leech Seed transfer, confusion 2-5 turns with 50%
|
|
||||||
40-power typeless self-hit.
|
|
||||||
- Trainer Pokémon use fixed DVs 9/8/8/8 (TrainerAI.asm convention).
|
|
||||||
|
|
||||||
## Items (engine/items/item_effects.asm)
|
|
||||||
|
|
||||||
- Potion family 20/50/200/full; drinks 50/60/80; status heals per item;
|
|
||||||
Revive half HP; Rare Candy = exact next-level exp with HP delta kept;
|
|
||||||
evolution stones use the extracted evos data; TMs single-use / HMs
|
|
||||||
reusable, gated by the species' real tmhm list; Repel 100/200/250
|
|
||||||
steps blocking wilds below the lead's level; Escape Rope returns to
|
|
||||||
the last heal point.
|
|
||||||
- Snorlax (Route 12/16) only wakes via `ItemUsePokeFlute` (item-use
|
|
||||||
menu, adjacent to it, not yet beaten), talking to it with the POKé
|
|
||||||
FLUTE merely in the bag has no effect (`engine/items/item_effects.asm`,
|
|
||||||
`scripts/Route12.asm`/`Route16.asm`).
|
|
||||||
- Mart inventories come from the script_mart lists per clerk; selling
|
|
||||||
pays half price; TM prices from tm_prices.asm.
|
|
||||||
|
|
||||||
## Overworld field systems
|
|
||||||
|
|
||||||
- Ledges from ledge_tiles.asm (facing + standing tile + ledge tile +
|
|
||||||
input direction -> two-cell hop).
|
|
||||||
- Counter talk-through uses the tileset's counter tiles
|
|
||||||
(tileset_headers.asm), which is how mart clerks and nurses work.
|
|
||||||
- Trainer sight (`home/trainers.asm` CheckFightingMapTrainers +
|
|
||||||
`engine/overworld/trainer_sight.asm`): extracted per-trainer range,
|
|
||||||
inclusive tiles along the facing line; detection runs only on
|
|
||||||
tile-aligned frames, before input handling, so on detection the d-pad
|
|
||||||
is dead (wJoyIgnore) and the player freezes on the spotted tile; the
|
|
||||||
"!" holds 60 frames (EmotionBubble), then the trainer walks
|
|
||||||
distance−1 steps to the adjacent tile (none if already adjacent) and
|
|
||||||
uses the real battle/won/after dialogue from the trainer headers.
|
|
||||||
Sight is a pure screen-coordinate comparison with no line-of-sight
|
|
||||||
obstruction check (TrainerEngage / CheckSpriteCanSeePlayer): an
|
|
||||||
aligned in-range trainer engages through interposed NPCs and
|
|
||||||
unwalkable tiles, and the walk-up (TrainerWalkUpToPlayer, a fixed
|
|
||||||
distance−1 MoveSprite_ script) has no collision either, so the
|
|
||||||
trainer simply walks/overlaps through anything on the line, as OAM
|
|
||||||
sprites overlap on hardware.
|
|
||||||
- Elevator rides (`engine/overworld/elevator.asm` ShakeElevator →
|
|
||||||
`src/world/ElevatorShake.lua`): choosing a floor stops the music,
|
|
||||||
bounces the BG scroll ±1 px around rest for 100 two-frame cycles with
|
|
||||||
SFX_COLLISION retriggered every cycle, restores the scroll, plays
|
|
||||||
SFX_SAFARI_ZONE_PA to completion, and restarts the map theme before
|
|
||||||
the floor warp. Lead-in delays kept per script: 9 frames of Delay3s
|
|
||||||
inside ShakeElevator (Celadon farjps in), 12 with the Silph/Rocket
|
|
||||||
scripts' extra Delay3. The offset applies to the BG layer only,
|
|
||||||
sprites are OAM and stay put. After the ride the port no longer
|
|
||||||
jump-cuts: choosing a floor rewrites the car's own exit-warp entries
|
|
||||||
to that floor (`engine/events/elevator.asm` DisplayElevatorFloorMenu
|
|
||||||
.UpdateWarp, per scripts/SilphCoElevator.asm /
|
|
||||||
CeladonMartElevator.asm / RocketHideoutElevator.asm), then the player
|
|
||||||
is walked out through the doorway onto that warp (ow:scriptMove →
|
|
||||||
ow:takeWarp), like the original.
|
|
||||||
- Field-move gates (engine/overworld/field_move_messages.asm +
|
|
||||||
start_sub_menus.asm): IsSurfingAllowed ported exactly, SURF refuses
|
|
||||||
with _CyclingIsFunText while the Cycling Road's BIT_ALWAYS_ON_BIKE is
|
|
||||||
armed (save.forcedBike: set on the Route 16/18 forced-bike tiles,
|
|
||||||
cleared by the gates, Fly, dungeon/blackout warps; the forced mount
|
|
||||||
itself is silent, as in CheckForceBikeOrSurf) and with
|
|
||||||
_CurrentTooFastText on Seafoam B4F's stairs square (7,11) until both
|
|
||||||
EVENT_SEAFOAM4 boulders are down. Re-selecting SURF while surfing is
|
|
||||||
ItemUseSurfboard's dismount attempt: steps ashore silently if the
|
|
||||||
facing tile is land-passable and unoccupied, else "There's no place
|
|
||||||
to get off!", and the menu closes either way (wActionResult stays 1).
|
|
||||||
STRENGTH's first page auto-advances after the cry + Delay3 (no
|
|
||||||
prompt); "can move boulders." prompts. The GBPalWhiteOutWithDelay3
|
|
||||||
white blink plays on every .goBackToMap closer: Strength, surf
|
|
||||||
mount/dismount/no-place, Flash (after its text), and Dig/Teleport
|
|
||||||
(Cut closes without a blink, per the asm).
|
|
||||||
- Wild slot table + rate per map; water encounter tables used while
|
|
||||||
surfing.
|
|
||||||
- Cut-tree block swaps from cut_tree_blocks.asm; surfable tilesets from
|
|
||||||
water_tilesets.asm (water tile $14, plus $32 on SHIP_PORT).
|
|
||||||
|
|
||||||
## Story events (data/scripts/story.lua and friends)
|
|
||||||
|
|
||||||
- Every hand-ported script cites its scripts/*.asm source and reuses the
|
|
||||||
real extracted text and event-flag names.
|
|
||||||
- Custom flag names (audited equivalent): three port-internal flag
|
|
||||||
families have no pokered EVENT constant but mirror the original's
|
|
||||||
state exactly. EVENT_TRADED_* are per-trade names for
|
|
||||||
wCompletedInGameTradeFlags bits (engine/events/in_game_trades.asm:
|
|
||||||
FLAG_TEST before the offer → after-trade text, FLAG_SET on completion;
|
|
||||||
dialogset text families, party-menu pick, the received mon joins the
|
|
||||||
end of the party, ConnectCable→anim→TradedFor→Thanks all ported).
|
|
||||||
EVENT_GOT_EEVEE is bookkeeping alongside the real guard, the hidden
|
|
||||||
ball object (scripts/CeladonMansionRoofHouse.asm HideObject, ≡
|
|
||||||
save.objectToggles), and self-heals older saves; a full party+box
|
|
||||||
keeps the ball claimable (_BoxIsFullText). EVENT_BEAT_SS_ANNE_RIVAL
|
|
||||||
stands in for scripts/SSAnne2F.asm's saved wSSAnne2FCurScript NOOP
|
|
||||||
progression, including the lose-and-retrigger path (flag only set on
|
|
||||||
victory). Names are kept for save compatibility. Coverage:
|
|
||||||
tests/parity_trade_gift.lua.
|
|
||||||
- The Pallet Town intro follows pokered exactly: the trigger is
|
|
||||||
PalletTownDefaultScript's wYCoord==1 check, Oak appears at (8,5) and
|
|
||||||
takes FindPathToPlayer's zigzag to one tile below the player, and the
|
|
||||||
escort is RLEList_ProfOakWalkToLab against the reverse-order playback
|
|
||||||
of RLEList_PlayerWalkToLab (the 17th simulated press is eaten by the
|
|
||||||
door-warp frame), followed by the OaksLab walk-in and choose-mon
|
|
||||||
exchange with map music deferred like BIT_NO_MAP_MUSIC. Oak's speech
|
|
||||||
ends with the real shrink: RedPicFront collapses through the extracted
|
|
||||||
ShrinkPic1/ShrinkPic2 into the overworld walking sprite on
|
|
||||||
OakSpeech.asm's frame timings (SFX_SHRINK, 4/4/20/50-frame beats, fade
|
|
||||||
to white), with the closing text box held on screen. The escort's
|
|
||||||
scripted steps run 16 frames/tile (chained single-tile scriptMoves
|
|
||||||
start back-to-back, no idle frame); Oak marches in place on the door
|
|
||||||
mat for RLEList_ProfOakWalkToLab's trailing NPC_CHANGE_FACING beat
|
|
||||||
(movement.asm ChangeFacingDirection → zero-delta TryWalking); the "!"
|
|
||||||
EmotionBubble overlaps the still-shown "Hey! Wait!" box
|
|
||||||
(PalletTownOakText prints without a button wait, then DelayFrames 10 →
|
|
||||||
EmotionBubble before the box clears); and the shrink beat ramps the
|
|
||||||
music to silence over ~70 frames (wAudioFadeOutControl = 10;
|
|
||||||
home/fade_audio.asm FadeOutAudio steps rAUDVOL 7→0) rather than
|
|
||||||
hard-stopping.
|
|
||||||
- The 12 disguised static wild battles (Power Plant Voltorb/Electrode +
|
|
||||||
Zapdos, Articuno, Moltres, Mewtwo) follow TalkToTrainer/
|
|
||||||
EndTrainerBattle exactly: cry + battle text, after-battle text without
|
|
||||||
a rematch once EVENT_BEAT_* is set, and the flag/HideObject on any
|
|
||||||
non-blackout result (fleeing loses the legendary, as in Gen 1).
|
|
||||||
Snorlax hides before its battle and only shows the calmed-down/
|
|
||||||
returned line when not caught. Zapdos/Articuno/Moltres/Mewtwo's
|
|
||||||
battle text is a text_far string ending in a bare "...@" terminator
|
|
||||||
(no <DONE>/<PROMPT>) followed by text_asm PlayCry + WaitForSoundToFinish:
|
|
||||||
the box types with no ▼ prompt and auto-closes only once the cry
|
|
||||||
finishes, never on a button press, ported via `Commands.play_cry`
|
|
||||||
stashing the pending cry for the following `Commands.show_text` to
|
|
||||||
consume as the TextBox's auto-close sound. Voltorb/Electrode's battle
|
|
||||||
text has no PlayCry call in the ROM at all and keeps the ordinary
|
|
||||||
button-wait close.
|
|
||||||
- Gym leader repeat dialogue (data/scripts/gyms.lua): each leader's
|
|
||||||
text_asm branches on EVENT_BEAT_<LEADER>, pre-badge talk prints the
|
|
||||||
pre-battle text and engages the leader battle (badge/TM via
|
|
||||||
data/scripts/victories.lua); post-badge talk prints the leader's
|
|
||||||
post-battle advice text (Misty's is her TM11 explanation). The
|
|
||||||
originals' middle branch (beaten but TM not handed over) is
|
|
||||||
unreachable since the TM is granted with the victory. Giovanni's
|
|
||||||
farewell (`ViridianGymGiovanniText` .afterBeat) hides him inside a
|
|
||||||
fade-to-black/fade-in Transition matching ViridianGym.asm's
|
|
||||||
GBFadeOutToBlack → HideObject → GBFadeInFromBlack, persisted
|
|
||||||
permanently via TOGGLE_VIRIDIAN_GYM_GIOVANNI in save.objectToggles.
|
|
||||||
- Cable Club receptionists (TX_SCRIPT_CABLE_CLUB_RECEPTIONIST →
|
|
||||||
CableClubNPC, all 12 Pokémon Centers): welcome, pre-Pokédex "making
|
|
||||||
preparations" brush-off, and the apply/save YES-NO are ported;
|
|
||||||
accepting saves the game and opens the link menu, declining prints
|
|
||||||
"Please come again!".
|
|
||||||
- Cinnabar fossil deposit follows GiveFossilToCinnabarLab: a menu of
|
|
||||||
carried fossils (FossilsList order), SeesFossilText with a Yes/No
|
|
||||||
confirm, ComeAgainText on either cancel.
|
|
||||||
- Hall of Fame induction: each party mon's front sprite scrolls in from
|
|
||||||
the left at 4px/frame, matching HoFShowMonOrPlayer's .ScrollPic
|
|
||||||
front-pic phase (engine/movie/hall_of_fame.asm); the back-pic's
|
|
||||||
enlarged/blurred pre-wipe is a VRAM-scroll-register trick not
|
|
||||||
replicated in this sprite-based renderer. The finale
|
|
||||||
(HoFDisplayPlayerStats) shows trainer name, play time, money, POKéDEX
|
|
||||||
seen/owned, and Prof. Oak's rating text (engine/events/
|
|
||||||
pokedex_rating.asm DexRatingsTable) from real save data.
|
|
||||||
- End credits + post-game reset (engine/movie/credits.asm,
|
|
||||||
scripts/HallOfFame.asm): screen-by-screen CreditsOrder pages (hlcoord
|
|
||||||
9,6 + signed columns), FadeInCredits' 4x5-frame ramp, 90/110/120/140-
|
|
||||||
frame holds, DisplayCreditsMon's 27-frame 8px/frame silhouette wipe,
|
|
||||||
LoadCopyrightTiles' three-row block, THE END at (4,8). While THE END
|
|
||||||
is up the HoF script autosaves (wLastBlackoutMap := PALLET_TOWN; the
|
|
||||||
player is saved in the HALL_OF_FAME room), waits 600 frames, then A/B
|
|
||||||
triggers `jp Init`, the boot sequence replays into the title screen.
|
|
||||||
- Victory Road's boulder switches replicate the original's
|
|
||||||
ReplaceTileBlock data: 1F boulder at (17,13) -> block $1D at (4,6);
|
|
||||||
2F boulders at (1,16)/(9,16) -> $15 at (3,4) and $1D at (11,7); 3F
|
|
||||||
boulder at (3,5) -> $1D at (3,5), and the (23,15) hole drops the
|
|
||||||
boulder to 2F (hide/show toggle). Barriers are re-applied from flags
|
|
||||||
on map entry, exactly like the originals' map-load scripts.
|
|
||||||
- Item balls, static legendary encounters and trainer rewards
|
|
||||||
(badges + gym TMs, the Silph Giovanni flag) are generic systems driven
|
|
||||||
by the extracted object args and a hand-ported reward table
|
|
||||||
(data/scripts/victories.lua).
|
|
||||||
- In-game trades use the real data/events/trades.asm table (species in,
|
|
||||||
species out, original nickname).
|
|
||||||
|
|
||||||
## Safari game (engine/events/hidden_events/safari_game.asm + engine/battle)
|
|
||||||
|
|
||||||
- ¥500 buys 30 SAFARI BALLs and 502 steps (scripts/SafariZoneGate.asm
|
|
||||||
sets `wSafariSteps = 502`); steps count down on the four outdoor zone
|
|
||||||
maps and hitting 0 (or throwing the last ball) ends the game at the
|
|
||||||
gate.
|
|
||||||
- Safari battles offer BALL / BAIT / ROCK / RUN; no player Pokémon
|
|
||||||
acts. The working catch rate starts at the species rate; BAIT halves
|
|
||||||
it and adds 1-5 to the bait factor (zeroing the escape factor); ROCK
|
|
||||||
doubles it (cap 255) and adds 1-5 to the escape factor (zeroing bait)
|
|
||||||
-- ItemUseBait/ItemUseRock in engine/items/item_effects.asm.
|
|
||||||
- Each turn one factor decays ("is eating!" / "is angry!"); when the
|
|
||||||
escape factor decays to 0 the catch rate resets to the species rate
|
|
||||||
(PrintSafariZoneBattleText, engine/battle/safari_zone.asm).
|
|
||||||
- Flee check (engine/battle/core.asm): `b = 2 * (speed % 256)`; the mon
|
|
||||||
always flees when speed > 127; while eating `b /= 4`, while angry
|
|
||||||
`b = min(255, 2b)`; it flees when `rand(0,255) < b`.
|
|
||||||
- The SAFARI BALL rolls the ULTRA_BALL rand range (0-150) in the Gen 1
|
|
||||||
catch formula, against the BAIT/ROCK-modified rate.
|
|
||||||
|
|
||||||
## Slot machines (engine/slots/slot_machine.asm)
|
|
||||||
|
|
||||||
- The three reels are the extracted 18-symbol wheel sequences
|
|
||||||
(data/events/slot_machine_wheels.asm); bet 1 plays the middle row,
|
|
||||||
bet 2 adds top+bottom, bet 3 adds both diagonals.
|
|
||||||
- Payouts: 7-7-7 = 300, BAR = 100, CHERRY = 8, MOUSE/FISH/BIRD = 15
|
|
||||||
(SlotRewardPointers).
|
|
||||||
- Per-wheel stop/slip rules ported exactly: wheel 1 spends up to 4 slip
|
|
||||||
charges, slipping past a centred CHERRY (in seven-and-bar mode it
|
|
||||||
always slips all 4 via pokered's `cp HIGH(SLOTS7)` bug); wheel 2 stops
|
|
||||||
as soon as wheels 1+2 line up any potential match (pairs checked b/b,
|
|
||||||
b/m, m/m, t/m, t/t) or, in seven-and-bar mode, on 7/BAR; wheel 3 rolls
|
|
||||||
past forbidden matches free and burns wSlotMachineRerollCounter
|
|
||||||
charges on winnable no-match spins, animated tile-by-tile. Luck flags
|
|
||||||
(SetFlags): seven-and-bar mode is sticky across spins; r==0 arms 60
|
|
||||||
allow-matches charges; a BAR win clears flags; a 300 win zeroes the
|
|
||||||
counter and clears flags with probability 128/256; 8/15 wins burn one
|
|
||||||
charge. Lines are checked in asm order with the first match taken;
|
|
||||||
A-presses are ignored while a prior wheel's slip counter is nonzero.
|
|
||||||
Machine and COIN CASE texts are byte-identical
|
|
||||||
(_GameCorner*Text; AbleToPlaySlotsCheck's no-coins gate included).
|
|
||||||
- Flow brackets: PromptUserToPlaySlots "A slot machine! Want to play?"
|
|
||||||
(YesNoChoice) and MainSlotMachineLoop's "One more go?" (TwoOptionMenu);
|
|
||||||
the x3/x2/x1 coin menu (CoinMultiplierSlotMachineText) defaults its
|
|
||||||
cursor to x3, bet = 3 - menu item. Static frame: the real
|
|
||||||
SlotMachineMap (gfx/slots/slots.tilemap, 20x12 tile ids < $25) blitted
|
|
||||||
from red_slots_1.png, extracted as field.slotSymbols.tilemap
|
|
||||||
(tools/extract/gfx.py extract_slots). Win flash:
|
|
||||||
SlotMachine_CheckForMatches.flashScreenLoop flips rBGP (shade 3->2) b
|
|
||||||
times at 5 frames each, b = 20/8/4/2 for the 300/100/15/8 rewards
|
|
||||||
(SlotReward{300,100,8,15}Func). Payout drip:
|
|
||||||
SlotMachine_PayCoinsToPlayer credits one coin every 8 frames (4 for a
|
|
||||||
7/BAR), SFX_SLOTS_REWARD per coin, rOBP0 symbol flicker every 5 coins.
|
|
||||||
|
|
||||||
## Spinner arrow tiles (scripts/*.asm arrow movement tables)
|
|
||||||
|
|
||||||
- Viridian Gym and Rocket Hideout B2F/B3F keep per-coordinate RLE
|
|
||||||
movement lists (map_coord_movement); each list executes backwards
|
|
||||||
from its terminator (DecodeArrowMovementRLE), sliding the player and
|
|
||||||
chaining onto further arrows.
|
|
||||||
|
|
||||||
## Cries (data/pokemon/cries.asm, audio/engine_1.asm)
|
|
||||||
|
|
||||||
- Each species = a base cry (one of 38 SFX_CryXX streams) + a frequency
|
|
||||||
modifier added to every note's frequency register
|
|
||||||
(Audio1_ApplyFrequencyModifier) + a tempo modifier
|
|
||||||
(`sfx tempo = $80 + length`, Audio1_SetSfxTempo). All 151 cries are
|
|
||||||
rendered offline with those modifiers applied and play on battle
|
|
||||||
entry and Pokédex pages.
|
|
||||||
|
|
||||||
## Hidden events & facility puzzles
|
|
||||||
|
|
||||||
- Card key doors (engine/events/card_key.asm): door tiles $18/$24
|
|
||||||
(SILPH_CO_11F: $5e) replaced with block $0e ($03 on 11F).
|
|
||||||
- Vermilion trash cans
|
|
||||||
(engine/events/hidden_events/vermilion_gym_trash.asm): the first-lock
|
|
||||||
can re-rolls on every Vermilion City map load (VermilionCity_Script's
|
|
||||||
Random & $e, even cans) and after every failed second-can guess; the
|
|
||||||
second lock uses the GymTrashCans table verbatim, including the
|
|
||||||
underflow bug that can place it in can 0 regardless of adjacency; a
|
|
||||||
wrong pick resets EVENT_1ST_LOCK_OPENED and re-rolls immediately; only
|
|
||||||
SuccessText3 prints on completion; the gym door block at (2,2) is
|
|
||||||
$24 closed / $5 open (scripts/VermilionGym.asm). SuccessText1/
|
|
||||||
SuccessText3/FailText play SFX_SWITCH/GO_INSIDE/DENIED from each
|
|
||||||
text's text_asm tail after the text prints (DisplayTextID's
|
|
||||||
WaitForTextScrollButtonPress then holds the box), so the port fires
|
|
||||||
them from an onDone on the TextBox, landing the beep as the box
|
|
||||||
closes rather than as it opens.
|
|
||||||
- Menu close-keys follow pokered's per-menu wMenuWatchedKeys mask, not
|
|
||||||
a single global rule: the shared Menu base (src/ui/Menu.lua) closes
|
|
||||||
on B only, and START-close is opt-in via opts.startCloses. Only the
|
|
||||||
start menu sets it, matching engine/menus/draw_start_menu.asm's
|
|
||||||
PAD_DOWN|PAD_UP|PAD_START|PAD_B|PAD_A; OptionsMenu also closes on
|
|
||||||
START via its own loop, matching engine/menus/main_menu.asm
|
|
||||||
DisplayOptionMenu's explicit B_PAD_B/B_PAD_START checks. Every other
|
|
||||||
menu (bag/PC item lists PAD_A|PAD_B|PAD_SELECT, party menu /
|
|
||||||
BUY-SELL-QUIT / USE-TOSS submenu / PC menus / Pokedex side menu
|
|
||||||
PAD_A|PAD_B) leaves PAD_START unwatched, so START does not close
|
|
||||||
them. START never replays SFX_PRESS_AB (HandleMenuInput_ beeps only
|
|
||||||
for the PAD_A|PAD_B branch).
|
|
||||||
- Old man tutorial hollow cursor: the item list is itself scripted in
|
|
||||||
pokered (DisplayListMenuID's old-man branch, home/list_menu.asm:65-91)
|
|
||||||
, no input is read; the filled '▶' hovers POKé BALL for 80 frames,
|
|
||||||
auto-presses A, then PlaceUnfilledArrowMenuCursor leaves the hollow
|
|
||||||
'▷' on that row until ItemUseBall tears the list down for the throw.
|
|
||||||
Ported via ListMenu's opts.script hook (src/ui/ListMenu.lua) and
|
|
||||||
BattleState:openOldManBag driving the same beats. The MissingNo./
|
|
||||||
wGrassRate side effects of the OLD MAN name swap are not modeled,
|
|
||||||
see docs/gameboy-hardware-limitations.md.
|
|
||||||
- Gym statues (gym_statues.asm): plaque with the city/leader from each
|
|
||||||
gym's script; the player joins WINNING TRAINERS with the badge.
|
|
||||||
- Route 22 gate / Route 23 guards: real trigger rows, badge order
|
|
||||||
(EARTH down to CASCADE) and EVENT_PASSED_*_CHECK skip flags.
|
|
||||||
- Game Corner poster (scripts/GameCorner.asm): block (8,2) $2a -> $43
|
|
||||||
on EVENT_FOUND_ROCKET_HIDEOUT.
|
|
||||||
- Seafoam Islands (scripts/SeafoamIslandsB3F/B4F.asm): reversed-RLE
|
|
||||||
current paths, Seafoam4HolesCoords boulder holes setting the
|
|
||||||
EVENT_SEAFOAM*_BOULDER*_DOWN_HOLE pairs, the forced pool exit rows.
|
|
||||||
- Rock Tunnel darkness: wMapPalOffset = 6 on entry, cleared by Flash
|
|
||||||
(BOULDERBADGE) or leaving (home/overworld.asm).
|
|
||||||
|
|
||||||
## Battle extras
|
|
||||||
|
|
||||||
- GROWL/ROAR (GetMoveSound/IsCryMove, engine/battle/animations.asm
|
|
||||||
~2196): the move's own MoveSoundTable tempo byte (Growl $c0, Roar
|
|
||||||
$40, both pitch $00) layers onto the cry via `Sound.playMoveCry`'s
|
|
||||||
`Source:setPitch(256/(128+tempoMod))`. Transform (engine/gfx/
|
|
||||||
palettes.asm DeterminePaletteID, bit TRANSFORMED): the swapped-in pic
|
|
||||||
is tinted PAL_GRAYMON via `PaletteFX.monPal(data, species,
|
|
||||||
transformed)`, not the copied species' own palette, in
|
|
||||||
`BattleState:speciesSprite`. Growl (DoGrowlSpecialEffects,
|
|
||||||
animations.asm ~928): AnimPlayer's GROWL frame-block branch keeps a
|
|
||||||
`growlNoteTrail` snapshot so each block's emitted sprites include the
|
|
||||||
previous block's note copy alongside the current one (GROWL skips
|
|
||||||
AnimationCleanOAM between blocks per the `cp GROWL` check ~line 145);
|
|
||||||
ROAR is unaffected since the asm never applies this quirk to it.
|
|
||||||
- Master/Ultra ball tosses flicker the OBJ palette: DoBallTossSpecial
|
|
||||||
Effects (engine/battle/animations.asm:685) XORs rOBP0 with %00111100
|
|
||||||
after every frame block while wCurItem <= ULTRA_BALL, so the 11 toss
|
|
||||||
blocks alternate the $F0/$CC shade maps starting normal; PlayAnimation
|
|
||||||
pushes/pops rOBP0 around each subanimation row, so the ambient
|
|
||||||
palette returns when the toss ends. GREAT/POKE/SAFARI balls never
|
|
||||||
flicker, and the toss arc always follows wCurItem via
|
|
||||||
TossBallAnimation, including the ghost-dodge throw.
|
|
||||||
- Anim-layer OBJ colorization is per 8x8 attribute cell: the SGB's
|
|
||||||
ATTR_BLK regions color the composited DMG picture per cell, not per
|
|
||||||
OAM entry, so an anim sprite overlapping a zone boundary takes each
|
|
||||||
cell's palette on the pixels inside it, AnimPlayer samples the zone
|
|
||||||
under every cell an 8x8 tile touches and repaints differing cells
|
|
||||||
through a cell-clipped scissor (aligned tiles stay one draw).
|
|
||||||
- Ball wobbles (ItemUseBall): Z = X*Y/255 + status2 with
|
|
||||||
Y = rate*100/ballFactor2; <10/<30/<70 -> 0/1/2 shakes, else 3, with
|
|
||||||
the matching ItemUseBallText01-04 lines.
|
|
||||||
- Trainer class AI (data/trainers/ai_pointers.asm +
|
|
||||||
engine/battle/trainer_ai.asm): per-class item/switch routines with
|
|
||||||
wAICount uses per Pokémon, ported to data/scripts/ai_classes.lua.
|
|
||||||
- Exp (engine/battle/experience.asm): baseExp*level/7 divided by the
|
|
||||||
participant count, x1.5 for trainers, x1.5 for traded mons; stat exp
|
|
||||||
in full to each participant.
|
|
||||||
- Move sounds: data/moves/sfx.asm (sound + pitch/tempo per move). The
|
|
||||||
pitch/tempo modifiers are applied at synthesis time
|
|
||||||
(Audio2_ApplyFrequencyModifier adds pitch to every frequency write;
|
|
||||||
Audio2_SetSfxTempo scales tone-channel note lengths, noise skips it),
|
|
||||||
128 variant WAVs keyed "<sfx>@<pitch><tempo>" that Sound.playMove
|
|
||||||
selects, exact rather than a playback-rate approximation. Per-row
|
|
||||||
sounds fire as PlayAnimation does; GROWL/ROAR (IsCryMove) play the
|
|
||||||
attacker's cry. Hit sounds by effectiveness (Damage/Super/NotVery).
|
|
||||||
- Screen-effect animations (engine/battle/animations.asm +
|
|
||||||
engine/gfx/screen_effects.asm): every SE_* is implemented per-routine,
|
|
||||||
FlashScreen/FlashScreenLong (the FlashScreenLongSGB 12-entry table),
|
|
||||||
Dark/Light/DarkenMon/Reset palette ops (shade-map permutations of the
|
|
||||||
SGB zone palettes), all SlideMon variants, ShakeBackAndForth,
|
|
||||||
BoundUpAndDown, SquishMonPic, Minimize (real MinimizedMonSprite),
|
|
||||||
spiral/shoot-balls/water-droplets/leaves emitters compiled from the
|
|
||||||
asm trajectories, per-animation-id frame-block flashes (Explosion,
|
|
||||||
Rock Slide's rumbles, Blizzard's cadence...), AnimationWavyScreen with
|
|
||||||
true per-scanline offsets, PredefShakeScreenHorizontally/Vertically
|
|
||||||
and ShakeEnemyHUD. SE rows carry the faithful blocking durations.
|
|
||||||
- SGB battle colorization (SetPal_Battle, BlkPacket_Battle,
|
|
||||||
SetAnimationPalette): the battle screen is colorized by zone, player
|
|
||||||
HUD, enemy HUD, player mon + message box, enemy mon; trainer front
|
|
||||||
pics and the player/old-man back pics take PAL_MEWMON (both species
|
|
||||||
IDs are zero at the intro, so MonsterPalettes[0]); the ghost keeps the
|
|
||||||
disguised species' palette; attack animation sprites and thrown balls
|
|
||||||
are colored through the OBJ palettes (wAnimPalette $F0 on SGB, ambient
|
|
||||||
$E4, OBP1 $6C). Headless/no-shader environments fall back to the flat
|
|
||||||
pipeline.
|
|
||||||
- Mimic resolves mid-move (MimicEffect): accuracy first, then the
|
|
||||||
player's copy menu (enemy/link copy a random slot); the copy
|
|
||||||
overwrites only the slot's move ID, PP is shared with Mimic's slot,
|
|
||||||
and reverts on switch/battle end.
|
|
||||||
- Old man tutorial (DisplayBattleMenu's BATTLE_TYPE_OLD_MAN branch): the
|
|
||||||
real scripted cursor, ▶ beside FIGHT for 80 frames, beside ITEM for
|
|
||||||
50, ITEM force-selected into the POKé BALL x50 list; the throw always
|
|
||||||
catches at full HP (item_effects.asm jumps straight to .captured, 3
|
|
||||||
shakes, no party/dex add, no ball consumed); backing out of the bag
|
|
||||||
replays the script. The old man never attacks, the original tutorial
|
|
||||||
is menu navigation + a guaranteed catch, nothing more.
|
|
||||||
|
|
||||||
## Link battles (lockstep)
|
|
||||||
|
|
||||||
- Both sides simulate with a shared Park-Miller RNG stream (host deals
|
|
||||||
the seed), identical pack/unpack-clamped party copies, no badge
|
|
||||||
boosts, and a mirrored speed-tie roll (the guest inverts it); a
|
|
||||||
canonical host-side-first state hash is exchanged per turn and any
|
|
||||||
mismatch ends the match as a draw.
|
|
||||||
|
|
||||||
## Music (audio/engine_1.asm)
|
|
||||||
|
|
||||||
- Note duration: `frames = length * speed * tempo / 0x100` with
|
|
||||||
fractional carry, at 60 fps (Audio1_note_length / CalculateDelay).
|
|
||||||
- Frequency: `reg = pitches[note] asr (octave - 1)` (CalculateFrequency;
|
|
||||||
the octave byte stores `8 - octave`), `f = 131072/(2048 - reg)` for
|
|
||||||
squares, halved for channel 3.
|
|
||||||
- note_type volume/fade renders as an NRx2-style envelope (step every
|
|
||||||
`fade/64` s); duty_cycle maps to 12.5/25/50/75% pulse widths;
|
|
||||||
sound_call/sound_loop honor the engine's one-level call stack and
|
|
||||||
loop counters.
|
|
||||||
|
|
||||||
## Text & font
|
|
||||||
|
|
||||||
- The Pokédex height row uses the real ′/″ tiles: gfx/pokedex/pokedex.png
|
|
||||||
tiles 0/1 are patched over font-extra slots $60/$61 exactly as
|
|
||||||
engine/gfx/load_pokedex_tiles.asm loads them over vChars2 (they replace
|
|
||||||
glyphs charmap.asm marks unused); ASCII `"` aliases to the closing-
|
|
||||||
quote glyph $73 so stray hand-written quotes render.
|
|
||||||
|
|
||||||
## Validation against the original
|
|
||||||
|
|
||||||
- `tests/run_tests.lua` pins hand-checked values: L5 Bulbasaur 19 HP /
|
|
||||||
9 Atk at 0 DVs, L100 Mewtwo 415 HP / 406 Spc at max DVs+statExp,
|
|
||||||
MEDIUM_SLOW(5) = 135, type chart spot checks, deterministic damage
|
|
||||||
rolls, Route 1 slot 1 = L3 Pidgey.
|
|
||||||
- The autopilot run reproduces the original's early flow on real map
|
|
||||||
data: Pallet sign text, lab door warp target (5,11), Oak's Lab exit by
|
|
||||||
walking off the mat, connection into Route 1 at matching x.
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
# ROM Extraction Notes
|
|
||||||
|
|
||||||
There are two ROM-only extraction paths:
|
|
||||||
|
|
||||||
- The packaged app uses `src/import/RomImporter.lua` and
|
|
||||||
`src/import/RomExtractor.lua` on first boot.
|
|
||||||
- Developers can run `tools/build_data.py --rom <path> [--clean]` to generate
|
|
||||||
data in the source tree for audit and parity work.
|
|
||||||
|
|
||||||
Both paths read only the supplied ROM and the checked-in
|
|
||||||
`tools/rom_manifest.json`. Neither invokes RGBDS, Git, or a disassembly.
|
|
||||||
|
|
||||||
## Validation
|
|
||||||
|
|
||||||
Only the canonical US Pokemon Red ROM is supported. SHA-1 is checked before
|
|
||||||
any cached output is removed or written.
|
|
||||||
|
|
||||||
## Decoded Data
|
|
||||||
|
|
||||||
| Area | ROM data |
|
|
||||||
| --- | --- |
|
|
||||||
| world | map headers, block maps, connections, warps, signs, objects |
|
|
||||||
| tiles | tileset graphics, blocksets, collision, door and warp tile lists |
|
|
||||||
| text | 2,584 text command streams and RAM/number substitutions |
|
|
||||||
| Pokemon | names, stats, evolutions, learnsets, Dex data, compressed pictures |
|
|
||||||
| battle | moves, detailed animations, OAM frames/tiles, effects, type chart, palettes, trainer parties/AI/pictures |
|
|
||||||
| inventory | item names, prices, key-item flags, TM/HM data |
|
|
||||||
| encounters | grass and water wild tables |
|
|
||||||
| UI | fonts, icons, title/intro, trainer card, town map, slots, field effects |
|
|
||||||
| audio | music, SFX and cry headers, channel programs, wave instruments |
|
|
||||||
|
|
||||||
The Python and Lua picture decompressors implement the Gen 1 `pic` format.
|
|
||||||
Graphics are converted to RGBA PNGs. OAM artwork uses transparent color 0;
|
|
||||||
battle pictures use edge-connected white matting so white interior details
|
|
||||||
remain visible.
|
|
||||||
|
|
||||||
The in-app importer stores three audio ROM banks as a 48 KiB
|
|
||||||
`programs.bin`. `src/core/ChipAudio.lua` interprets the channel bytecode and
|
|
||||||
synthesizes music as a queueable stream; SFX and cries are synthesized on
|
|
||||||
demand. This avoids shipping or generating a large WAV/OGG tree.
|
|
||||||
|
|
||||||
## Metadata Boundary
|
|
||||||
|
|
||||||
Names, dimensions, enum ordering, Lua script hooks, and hand-ported field
|
|
||||||
behavior do not survive compilation in a form the Lua runtime can infer.
|
|
||||||
Those relationships are bundled in `rom_manifest.json`. The manifest stores
|
|
||||||
no dialogue strings, images, audio samples, or ROM bytes.
|
|
||||||
|
|
||||||
`tools/make_rom_manifest.py` and `tools/verify_rom_data.py` are developer audit
|
|
||||||
tools. They are not used by the packaged game.
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
# Sideload the iOS build with AltStore
|
|
||||||
|
|
||||||
Every GitHub Release ships an IPA (`gen1recomp-*-ios.ipa`). Install it on
|
|
||||||
your iPhone or iPad with [AltStore Classic](https://altstore.io/) — AltStore
|
|
||||||
re-signs the app with **your** free Apple ID so you do not need a Mac or
|
|
||||||
Xcode.
|
|
||||||
|
|
||||||
## 1. Install AltStore
|
|
||||||
|
|
||||||
Follow the official guide for your computer:
|
|
||||||
|
|
||||||
- [How to Install (Windows)](https://faq.altstore.io/altstore-classic/how-to-install-altstore-windows)
|
|
||||||
- [How to Install (macOS)](https://faq.altstore.io/altstore-classic/how-to-install-altstore-macos)
|
|
||||||
|
|
||||||
You will install **AltServer** on the computer, then use it to put AltStore
|
|
||||||
on the phone. What AltServer is and why it needs to stay running:
|
|
||||||
|
|
||||||
- [AltServer](https://faq.altstore.io/altstore-classic/altserver)
|
|
||||||
|
|
||||||
Stuck? Start here:
|
|
||||||
|
|
||||||
- [Troubleshooting Guide](https://faq.altstore.io/altstore-classic/troubleshooting-guide)
|
|
||||||
|
|
||||||
## 2. Install the game
|
|
||||||
|
|
||||||
1. Download `gen1recomp-*-ios.ipa` from
|
|
||||||
[Releases](https://github.com/bryanthaboi/gen1recomp/releases).
|
|
||||||
2. Open **AltStore** on the phone (AltServer must be running on the same
|
|
||||||
Wi‑Fi, or keep the phone plugged into the computer).
|
|
||||||
3. Tap **My Apps → +** (or share the IPA into AltStore) and pick the file.
|
|
||||||
4. Sign in with your Apple ID when prompted. Wait for the install to finish.
|
|
||||||
5. On first launch: Settings → **Privacy & Security → Developer Mode** (iOS
|
|
||||||
16+), and Settings → **General → VPN & Device Management** → Trust your
|
|
||||||
Apple ID if asked.
|
|
||||||
|
|
||||||
Then open the app, import your own legal `.gb` ROM on the Red/Blue tab, and
|
|
||||||
play.
|
|
||||||
|
|
||||||
## Refresh / 7-day limit
|
|
||||||
|
|
||||||
With a free Apple ID, sideloaded apps stop launching after **7 days**. Keep
|
|
||||||
AltServer running so AltStore can refresh them, or open AltStore and refresh
|
|
||||||
manually before they expire. Saves on the phone are kept across refreshes.
|
|
||||||
|
|
||||||
## Prefer building it yourself?
|
|
||||||
|
|
||||||
Building from source on a Mac (no AltStore) is covered in
|
|
||||||
[ios-install.md](ios-install.md).
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
# Known differences from the original game
|
|
||||||
|
|
||||||
Only genuine remaining divergences live here: behavior that is still
|
|
||||||
**missing, wrong, or approximated for convenience** and would need more
|
|
||||||
work for true parity. Faithfully-ported behavior is documented in
|
|
||||||
docs/behavior-porting-notes.md; deliberate additions beyond the original
|
|
||||||
are in docs/new-features.md.
|
|
||||||
|
|
||||||
## Reimplemented unused Prof. Oak and Rocket Chief battles
|
|
||||||
|
|
||||||
The original ROM defines trainer data for `PROF_OAK` and `CHIEF`
|
|
||||||
(`data/trainers/parties.asm`) but never attaches either to an NPC, so
|
|
||||||
both battles are unreachable in the real game. This project makes them
|
|
||||||
fightable after the Hall of Fame:
|
|
||||||
|
|
||||||
- Prof. Oak battles you in Pallet Town once `EVENT_BEAT_CHAMPION_RIVAL`
|
|
||||||
is set, using `ProfOakData`'s three starter-matched teams (the team is
|
|
||||||
picked by the type that counters your starter, mirroring the rival).
|
|
||||||
- The Celadon Game Corner Chief battles you in his house post-game.
|
|
||||||
`ChiefData` is empty in the ROM, so `OPP_CHIEF` is given a
|
|
||||||
reconstructed party.
|
|
||||||
|
|
||||||
This is an intentional divergence: neither battle can be triggered in the
|
|
||||||
original game.
|
|
||||||
|
|
||||||
## Reimplemented unused Silph Co. card-key doors
|
|
||||||
|
|
||||||
`engine/events/card_key.asm` and the unused `CardKeyTable1/2/3` coordinate
|
|
||||||
lists (`data/events/card_key_coords.asm`) describe locked doors for Silph
|
|
||||||
Co. floors 2F-11F, but no retail `.blk` map layout ever places the closed
|
|
||||||
door block at those coordinates, so the card key check is dead code in
|
|
||||||
the original game. This project stamps the closed door block (`$54`/`$5f`
|
|
||||||
on floors 2F-10F, `$20` on 11F) over each of the 20 door coordinates on
|
|
||||||
map load, and swaps it for the open block once that door's
|
|
||||||
`EVENT_SILPH_CO_n_UNLOCKED_DOORn` flag is set (using the key from a Team
|
|
||||||
Rocket grunt, as in the original's unused design).
|
|
||||||
|
|
||||||
This is an intentional divergence: the doors are not visible or
|
|
||||||
functional in the original game. The door layout lives in
|
|
||||||
`tools/rom_manifest.json` (`field.cardKeyDoors.closedDoors`), hand-ported
|
|
||||||
since no retail ROM data encodes it; `src/import/RomExtractor.lua` copies
|
|
||||||
it straight through on ROM import.
|
|
||||||
@@ -1,265 +0,0 @@
|
|||||||
# Launcher
|
|
||||||
|
|
||||||
The launcher is `src/import/RomImporter.lua`, the first-run / title screen
|
|
||||||
that runs before `Game:load`. Besides ROM import (see the file's own header)
|
|
||||||
it hosts a tabbed shell covering per-game save slots and a mod manager. This
|
|
||||||
file documents the runtime model; the visual spec lives separately.
|
|
||||||
|
|
||||||
## Android multi-ROM / mod / save import
|
|
||||||
|
|
||||||
On Android, `love.system.pickFile([kind])` opens the Storage Access Framework
|
|
||||||
picker (`GameActivity.showFilePicker`); the chosen file is copied into the app
|
|
||||||
save directory as:
|
|
||||||
|
|
||||||
| `kind` | Destination |
|
|
||||||
| --- | --- |
|
|
||||||
| nil / `"rom"` | `picked_rom.gb` (open) |
|
|
||||||
| `"mod"` | `picked_mod.zip` (open) |
|
|
||||||
| `"sav"` / `"save"` | `picked_save.sav` (open) |
|
|
||||||
|
|
||||||
Export uses a separate API: `love.system.createFile(suggestedName)` →
|
|
||||||
`GameActivity.showCreateDocument` (`ACTION_CREATE_DOCUMENT`), which copies
|
|
||||||
staged `pending_export.sav` to the user-chosen URI and writes `export_done.flag`
|
|
||||||
for the launcher to acknowledge on refocus.
|
|
||||||
|
|
||||||
`RomImporter` then imports on refocus / Choose:
|
|
||||||
|
|
||||||
- **ROMs** via `findPendingRom`: only a 1 MiB `.gb` whose SHA-1 maps to a
|
|
||||||
version that is **not** yet ready counts as pending. A leftover
|
|
||||||
`picked_rom.gb` from Red therefore cannot block Blue's Choose (issue #167).
|
|
||||||
- **Mods** via `findPendingMod`: Prefer `picked_mod.zip`, or (on Choose) any
|
|
||||||
other `.zip` at the save-dir root (USB copy).
|
|
||||||
- **Saves** via `findPendingSav`: Prefer `picked_save.sav`, or (on Choose) any
|
|
||||||
other `.sav` at the save-dir root.
|
|
||||||
|
|
||||||
After a successful import the consumed save-dir file is removed.
|
|
||||||
|
|
||||||
**Manual check (device/emulator):** import Red → switch to Blue → Choose →
|
|
||||||
system file picker must appear (not a silent Red re-extract) → pick Blue →
|
|
||||||
Blue becomes ready beside Red. On the MODS tab, Import mod .zip must open the
|
|
||||||
same system picker and install the chosen archive on return.
|
|
||||||
|
|
||||||
## Tab structure
|
|
||||||
|
|
||||||
`self.tab` is one of `"red"`, `"blue"`, `"yellow"`, `"mods"`. The tab bar
|
|
||||||
draws one chip per game plus a MODS chip and rebuilds `self.tabRects` every
|
|
||||||
frame so `mousepressed` can dispatch clicks; switching tabs mid-import is
|
|
||||||
allowed (a dropped ROM still routes by SHA-1 regardless of which tab shows).
|
|
||||||
|
|
||||||
- A game tab (`_drawGamePanel`) shows the ROM card, the SAVE FILES card, the
|
|
||||||
Play button, and the SAVE SLOT card in a responsive two-column grid (see
|
|
||||||
Responsiveness). The MODS tab (`_drawModsPanel`) shows the mod list instead.
|
|
||||||
- The self-updater banner (`self.Check`, see `docs/updater.md`) draws as a
|
|
||||||
centered pill in a reserved band just above the footer, on every tab. That
|
|
||||||
position is unchanged by this redesign, so `docs/updater.md` needed no edits.
|
|
||||||
|
|
||||||
## Save slot model
|
|
||||||
|
|
||||||
All slot I/O lives in `src/core/SaveData.lua` and goes through the same fs
|
|
||||||
abstraction (`persistFs`) every other save/options call uses, so portable
|
|
||||||
mode (an `io.*` filesystem used when `portable.txt` marks the install)
|
|
||||||
keeps working unchanged.
|
|
||||||
|
|
||||||
- **Files.** A version's playthroughs live under `saves/<version>/`, one file
|
|
||||||
per slot: `saves/<version>/slot1.lua` plus a rolling `.bak` and staged
|
|
||||||
`.tmp` witness (`slotNames`), mirroring the write/recovery discipline
|
|
||||||
`SaveData.save`/`load` already use for the flat legacy file. Slot ids match
|
|
||||||
`slot%d+`; `createSlot` allocates one past the highest existing number so a
|
|
||||||
reused id can never collide with a lingering file.
|
|
||||||
- **Registry.** The ordered slot list and which one is active persist in
|
|
||||||
`options.lua` (via the existing `SaveData.loadOptions`/`saveOptions`):
|
|
||||||
`options.saveSlots = { [version] = { list = {"slot1", ...}, active = "slot1" } }`.
|
|
||||||
Custom slot labels (#205) live alongside them in the same registry:
|
|
||||||
`options.saveSlots[version].names = { slot1 = "Nuzlocke" }`, written by
|
|
||||||
`SaveData.renameSlot` (trimmed; an empty label clears it) and surfaced on
|
|
||||||
each `listSlots` row as `label` (the launcher row shows `label`, falling
|
|
||||||
back to the player name). `deleteSlot` drops the label with the slot.
|
|
||||||
Renaming never touches the save file, so an empty slot can be labeled.
|
|
||||||
On desktop, right-clicking a slot row opens the inline rename modal
|
|
||||||
(Enter commits, Esc cancels); touch has no secondary button, so the
|
|
||||||
affordance is desktop-only.
|
|
||||||
- **Active slot resolution.** `saveNames(version)`, the function every
|
|
||||||
existing caller (`TitleState` hasSave/load/save, recovery order) already
|
|
||||||
goes through, now resolves the *active* slot instead of a fixed flat name.
|
|
||||||
Resolved once per version per process (`ensureVersionSlots`, cached in
|
|
||||||
`activeSlotCache`/`slotsChecked`): a registry entry wins; otherwise a lazy
|
|
||||||
legacy migration may create one; otherwise the flat legacy path is used
|
|
||||||
(`save.lua` / `save_blue.lua`), so a pre-slots install keeps working as before.
|
|
||||||
- **Legacy migration.** One-time per version, lazy on first
|
|
||||||
`listSlots`/`load`/`saveNames` call (`tryMigrateLegacy`): if a flat legacy
|
|
||||||
file exists and no `saves/<version>/` registry does, its main + `.bak` are
|
|
||||||
copied into `saves/<version>/slot1.lua(.bak)`, verified readable
|
|
||||||
(`decodeSlot`: main, then `.tmp`, then `.bak`), and only then are the
|
|
||||||
originals removed and `slot1` registered as active. A copy that fails to
|
|
||||||
verify leaves the originals in place; migration never loses data.
|
|
||||||
|
|
||||||
The launcher-facing API:
|
|
||||||
- `SaveData.listSlots(version)` -> array of `{id, exists, name, meta}` for
|
|
||||||
every registered slot. `name` is the save's player name, or `nil` for an
|
|
||||||
empty slot; `meta` is `{badges, timeText, dexCount}` (the same fields the
|
|
||||||
title screen's `ContinueInfo` shows) or `nil`. The pure part,
|
|
||||||
`SaveData.slotSummary(save)`, is unit-testable with no filesystem.
|
|
||||||
- `SaveData.setActiveSlot(version, slotId)` registers the id if new, persists
|
|
||||||
it as active, and updates the process cache so the very next save/load
|
|
||||||
lands there. The launcher calls this the moment a slot row is clicked
|
|
||||||
(`RomImporter:_selectSlot`); pressing Play needs no signature change, since
|
|
||||||
`Game.lua`/`main.lua` still just call `SaveData.load()`/`save()`.
|
|
||||||
- `SaveData.createSlot(version)` -> new slot id, registered but with **no
|
|
||||||
save file written**. An empty slot means the title screen offers NEW GAME
|
|
||||||
only, which needs no further changes.
|
|
||||||
- `SaveData.deleteSlot(version, slotId)` removes the slot's
|
|
||||||
main/`.bak`/`.tmp` files, drops it from the registry, and if it was active
|
|
||||||
points active at another remaining slot (or clears active when the list is
|
|
||||||
empty). The launcher's SAVE SLOT panel Delete control calls this.
|
|
||||||
|
|
||||||
## Launcher mod manager
|
|
||||||
|
|
||||||
`src/mods/LauncherMods.lua` is a launcher-only read of the mod set. It runs
|
|
||||||
before `Game:load`, so **it never loads a mod's entry chunk**; only
|
|
||||||
`manifest.json` is read and validated (`src/mods/Manifest.validate`), the way
|
|
||||||
`Loader:_discover` finds mods without running them. The real loader
|
|
||||||
(`src/mods/Loader.lua`) still owns the actual load at boot.
|
|
||||||
|
|
||||||
- `LauncherMods.list()` scans `mods/` one level deep (first id wins on a
|
|
||||||
duplicate) and returns one row per mod:
|
|
||||||
`{id, name, version, badge, description, enabled, status, statusDetail}`.
|
|
||||||
`badge` is the manifest's `category`, falling back to `profile`, then
|
|
||||||
`"MOD"`, uppercased. `enabled` reads `options.mods[id]` (missing means
|
|
||||||
enabled, matching the loader's own default).
|
|
||||||
- `status` is `"ok"`, `"warn"`, or `"conflict"`, computed by the pure
|
|
||||||
`LauncherMods.deriveList`/`statusFor` against `ManagerState.resolveToggle`
|
|
||||||
and the validated manifests: `conflict` when enabling this mod collides
|
|
||||||
with another enabled one; `warn` for an out-of-range `game_version` or an
|
|
||||||
absent/disabled/wrong-version hard dependency; `ok` otherwise. Having no
|
|
||||||
`love.*` calls, this half is table-driven by the test suite on its own.
|
|
||||||
- `LauncherMods.setEnabled(id, bool)` persists `options.mods[id]` as a plain
|
|
||||||
boolean, the exact shape `Loader:_saveState` writes, so the running game
|
|
||||||
and the in-game `ManagerState` see the change on next boot. The mods panel
|
|
||||||
calls this on every toggle and re-derives the list right away
|
|
||||||
(`RomImporter:_refreshMods`) so a status change (e.g. a new conflict)
|
|
||||||
shows without waiting for a reload.
|
|
||||||
- `LauncherMods.installZip(path)` mounts the archive with
|
|
||||||
`love.filesystem.mount`, locates the mod root via `locateRoot` (manifest at
|
|
||||||
the zip root, or inside one top-level folder), validates its manifest, and
|
|
||||||
copies the tree into the save-dir `mods/<id>/` before unmounting. Rejects a
|
|
||||||
duplicate of an already-installed mod id, and accepts either an external
|
|
||||||
path string or a LOVE `DroppedFile`, staging a dropped file into a save-dir
|
|
||||||
temp first (mount only reaches save-dir-relative paths), the same way
|
|
||||||
`RomImporter` handles a dropped ROM. A failed copy rolls its partial tree
|
|
||||||
back, and every path unmounts and clears the staged temp file.
|
|
||||||
- `LauncherMods.uninstall(id)` removes `mods/<id>/` and clears
|
|
||||||
`options.mods[id]` so a later reinstall starts from the loader's default
|
|
||||||
(enabled). The mods panel Delete control calls this and re-derives the list.
|
|
||||||
|
|
||||||
## Import / Export save
|
|
||||||
|
|
||||||
The SAVE FILES card wires a raw Gen1 `.sav` battery image to the save slots
|
|
||||||
through `src/import/SaveFileIO.lua`, which sits on top of
|
|
||||||
`src/save_convert/SaveConvert.lua` and the slot API in `SaveData`.
|
|
||||||
|
|
||||||
- **Import save** is live once the game's ROM is imported (playable). It opens
|
|
||||||
a native `.sav` picker (`chooseSav` on desktop; on Android,
|
|
||||||
`love.system.pickFile("sav")` → `picked_save.sav`, same SAF path as ROMs).
|
|
||||||
`SaveFileIO.importToSlot` reads the bytes (an absolute path, a save-dir
|
|
||||||
relative name, a dropped LOVE file, or raw bytes),
|
|
||||||
guards the 32768-byte size, runs `SaveConvert.importSav` (which also rejects
|
|
||||||
a bad main-data checksum), then registers a fresh slot (`SaveData.createSlot`),
|
|
||||||
writes it (`SaveData.writeSlot`), and makes it active (`SaveData.setActiveSlot`).
|
|
||||||
The meta stamp is re-stamped off `gen1_import` to the current numeric format
|
|
||||||
so `SaveData.load`'s migration pass accepts the slot. On success the SAVE SLOT
|
|
||||||
panel is refreshed with the new slot selected.
|
|
||||||
- **Export save** is live only when the active slot actually holds a save
|
|
||||||
(checked against `listSlots`). `SaveFileIO.exportActiveSlot` loads the active
|
|
||||||
slot, encodes it back with `SaveConvert.exportSav` (a slot never keeps
|
|
||||||
`rawImport`, so this is a zero-filled template export, which is valid), and
|
|
||||||
writes `exports/gen1recomp-<version>-<slotId>.sav` in the save directory
|
|
||||||
(`love.filesystem.createDirectory("exports")`). On desktop it returns the
|
|
||||||
absolute path (`love.filesystem.getSaveDirectory()`), which the notice line
|
|
||||||
shows with an "Open folder" affordance (`love.system.openURL("file://" .. dir)`).
|
|
||||||
On Android the bytes are also staged as `pending_export.sav` and
|
|
||||||
`love.system.createFile(suggestedName)` opens `ACTION_CREATE_DOCUMENT` so the
|
|
||||||
player can save to Downloads / Drive / etc.; on return `export_done.flag`
|
|
||||||
makes focus show "Save exported."
|
|
||||||
- **Drag-drop.** `filedropped` routes a `.sav` to the import path for the
|
|
||||||
currently active game tab; when a non-game tab (mods, or the locked yellow
|
|
||||||
placeholder) is showing it defaults to red, the always-present first game
|
|
||||||
(`_savedropTarget`). `.gb` (ROM) and `.zip` (mod) routing is unchanged.
|
|
||||||
- **Failure UX.** Every error path (wrong size, bad checksum, write failure,
|
|
||||||
nothing to export, ROM not imported yet) surfaces as a red notice line on the
|
|
||||||
card. Nothing raises and nothing silently no-ops.
|
|
||||||
|
|
||||||
`SaveFileIO` is love-free enough to unit-test through the same in-memory
|
|
||||||
filesystem stub the slot backend uses (`tests/engine/save_file_io_tests.lua`).
|
|
||||||
|
|
||||||
## Responsiveness
|
|
||||||
|
|
||||||
Every measurement derives from `love.graphics.getDimensions()` each frame
|
|
||||||
plus the existing global scale `s = clamp(height / 768, 0.7, 1.6)`; nothing
|
|
||||||
assumes a fixed window size. The game panel's two-column grid (ROM/SAVE
|
|
||||||
FILES/Play on the left, SAVE SLOT on the right) collapses to one stacked
|
|
||||||
column, slot card below Play, when the window is too narrow for both
|
|
||||||
`~300 * s`-wide columns. The save-slot list and the mod list both scroll
|
|
||||||
(wheel, or drag on touch/desktop) clamped to their own content extent,
|
|
||||||
recomputed every draw. The tab bar labels only the active chip so it stays
|
|
||||||
narrow-safe, and content caps out at `~1440 * s` wide, centered.
|
|
||||||
|
|
||||||
The desktop window has a floor of 480x360 (`conf.lua` `minwidth`/`minheight`),
|
|
||||||
under which the cards stop being readable at all. Mobile ignores it: those
|
|
||||||
windows are fullscreen.
|
|
||||||
|
|
||||||
### Page scroll
|
|
||||||
|
|
||||||
Two columns fit any window the launcher is likely to open in; one stacked
|
|
||||||
column does not. On a phone-shaped window the ROM card, SAVE FILES, Play and
|
|
||||||
SAVE SLOT together run past the bottom, and a footer pinned to the window
|
|
||||||
bottom painted over them with the overflow unreachable.
|
|
||||||
|
|
||||||
So the whole column under the tab bar -- panel, updater banner, footer --
|
|
||||||
scrolls as one page whenever it is taller than the room below the tab bar:
|
|
||||||
|
|
||||||
- The strip, logo and tab bar stay pinned, so navigation is always on screen.
|
|
||||||
Everything else draws at `contentTop - pageScroll` inside a scissor, and the
|
|
||||||
footer is laid out downward from `footerTop` right after the content instead
|
|
||||||
of upward from the window bottom.
|
|
||||||
- `RomImporter.pageScrollFor(naturalH, viewportH, scroll)` is the whole
|
|
||||||
decision, pure and pinned by `tests/engine/launcher_page_scroll.lua`. A
|
|
||||||
window that grows back drags the offset down with it, so the page can never
|
|
||||||
stay parked past its own end.
|
|
||||||
- The panels report their natural height as they draw (`_drawGamePanel` and
|
|
||||||
`_drawModsPanel` return it), so the decision reads the previous frame's
|
|
||||||
measurement -- the same one-frame settle the two lists already rely on.
|
|
||||||
- **One scroll axis at a time.** While the page scrolls, the panels draw
|
|
||||||
`paged`: the slot and mod lists take their natural height, keep no inner
|
|
||||||
scroll region and report a max of 0, so the wheel, the right stick and a drag
|
|
||||||
all move the page and never fight a list for the same gesture. Two-column
|
|
||||||
layouts do not overflow, `paged` stays false, and every one of these behaves
|
|
||||||
exactly as it did before.
|
|
||||||
- Hit testing follows the clip: `inside` (clicks) and `_ptIn` (hover) reject a
|
|
||||||
rect that scrolled out of the viewport, so a control that slid under the tab
|
|
||||||
bar cannot be clicked through it. Tab chips carry `pinned = true` and are
|
|
||||||
exempt. `pageScroll` resets on a tab change, each tab being a different
|
|
||||||
length.
|
|
||||||
- A press on empty background pans the page, resolved in `_updateSlotDrag` like
|
|
||||||
every other drag here.
|
|
||||||
|
|
||||||
### Dragging on Android
|
|
||||||
|
|
||||||
The launcher is handed no move events on any platform: `main.lua` forwards
|
|
||||||
neither `touchmoved` nor `mousemoved` while it is up, which is why every drag
|
|
||||||
here is resolved by polling inside `draw` instead. Desktop polls the mouse;
|
|
||||||
Android used to poll nothing at all ("no reliable pointer polling" meant its
|
|
||||||
mouse emulation), so it had no scroll gesture whatsoever -- fine while every
|
|
||||||
scroll region was an inner list with a wheel alternative, useless the moment
|
|
||||||
the page itself became the thing that scrolls, since a phone is exactly where
|
|
||||||
it overflows.
|
|
||||||
|
|
||||||
`love.touch` is pollable, so `_pointerHold` reads the first active touch there
|
|
||||||
and hands `_updateSlotDrag` the same (held, y) pair the mouse gives on desktop.
|
|
||||||
Consequences:
|
|
||||||
|
|
||||||
- Slot rows and mod toggles ARM on press and commit on release on Android too,
|
|
||||||
matching desktop, so a swipe that starts on a card scrolls instead of
|
|
||||||
selecting the row it started on.
|
|
||||||
- `touchPollable` (set once in `new`) gates all of it. Where `love.touch` is
|
|
||||||
missing, every Android path is exactly what it was: act on press, never arm,
|
|
||||||
no drag.
|
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
# Link play: threat model and what the code actually guarantees
|
||||||
|
|
||||||
|
Link play is the only part of this game that reads bytes written by
|
||||||
|
somebody else. This is what it defends against, what it does not, and
|
||||||
|
where each guarantee lives.
|
||||||
|
|
||||||
|
## The boundary
|
||||||
|
|
||||||
|
Everything a peer or the relay sends arrives as one JSON object per line.
|
||||||
|
There is exactly one place it becomes a message:
|
||||||
|
|
||||||
|
src/link/Net.lua reads bytes, frames lines, decodes JSON
|
||||||
|
src/link/Wire.lua rebuilds each line as a typed message
|
||||||
|
src/link/Session.lua the only path from a transport into a mode
|
||||||
|
|
||||||
|
`Session:update` runs `Wire.sanitize` on every message before anything
|
||||||
|
else sees it. A schema returns a **new** table holding only the fields it
|
||||||
|
names, at the Lua types it names, so the rest of `src/link/` can read
|
||||||
|
`msg.slot`, `msg.parts.actives` or `msg.mons[i].dvs.hp` directly and be
|
||||||
|
right by construction. A message with no schema (a mod's, or a future
|
||||||
|
build's) keeps a bounded, scalar-only copy of its payload instead of
|
||||||
|
being dropped.
|
||||||
|
|
||||||
|
A message that fails its schema is **dropped and logged**, never fatal.
|
||||||
|
Latching a terminal failure would hand a hostile peer a cheaper
|
||||||
|
disconnect than sending nothing at all.
|
||||||
|
|
||||||
|
### Why the bounds are loose
|
||||||
|
|
||||||
|
Wire's numeric bounds are deliberately wider than the game's own clamps in
|
||||||
|
`Protocol.unpackMon`. Both peers run identical clamps over identical
|
||||||
|
packets; a bound that bit an honest value would change one side's copy of
|
||||||
|
a mon and desync the lockstep. Wire's job is types and sizes. Rules are
|
||||||
|
`Protocol`'s job, and it keeps its own clamps for the callers that reach
|
||||||
|
it without a Session (the mod API, `tests/`).
|
||||||
|
|
||||||
|
### Containment behind it
|
||||||
|
|
||||||
|
Assume something still gets through:
|
||||||
|
|
||||||
|
- `Game:step` pcalls the link pump, and pcalls `stack:update` **only
|
||||||
|
while a link session is active**. On a throw, `Game:breakLink` closes
|
||||||
|
the connection, unwinds to the overworld and says "The link was
|
||||||
|
broken." Outside link play the stack is unguarded on purpose: a blanket
|
||||||
|
pcall would swallow real engine bugs and leave the game silently wrong
|
||||||
|
instead of loudly broken.
|
||||||
|
- `Net` caps `rxBuf` at 256KB and its per-frame read at 512KB, so a peer
|
||||||
|
that never sends a newline ends as a clean disconnect.
|
||||||
|
- `Json.decode` refuses documents nested past 64 levels, and takes an
|
||||||
|
optional length cap that the link path passes and the mod-manifest path
|
||||||
|
does not.
|
||||||
|
|
||||||
|
## The relay (`../pokeserver`)
|
||||||
|
|
||||||
|
- A line that is not a JSON **object** with a string `type` is dropped
|
||||||
|
before any handler runs, and `onLine` is wrapped in try/catch.
|
||||||
|
`server.js` installs `uncaughtException`/`unhandledRejection` handlers:
|
||||||
|
one bad packet must never take every live match down with the process.
|
||||||
|
- Line buffers are capped, lines per second are capped, connections per
|
||||||
|
IP and in total are capped, and an unbound connection that never hosts
|
||||||
|
or joins is swept after 30s.
|
||||||
|
- `SERVER_ONLY` is the set of message types the server is the only
|
||||||
|
legitimate author of (`peer_gone`, `bracket_update`, `match_start`,
|
||||||
|
`tournament_over`, `spectate`, ...). A peer that sends one has them
|
||||||
|
dropped rather than forwarded, so a bracket opponent cannot forge a
|
||||||
|
tournament result or fake "your opponent left".
|
||||||
|
- Trainer names are reduced to a printable subset and capped at the same
|
||||||
|
10 characters the game enforces, on the way in, because they are
|
||||||
|
rendered by the dashboard and broadcast to every participant.
|
||||||
|
|
||||||
|
`pokeserver/test/hostile.js` is the regression net for all of that.
|
||||||
|
|
||||||
|
## What is NOT defended
|
||||||
|
|
||||||
|
**Party legality is trust-the-client.** Online play meets strangers, and
|
||||||
|
`Handshake.onlineAllowed` is a Lua function in the same VM the mods load
|
||||||
|
into. It cannot be made tamper-proof in-process, and pretending otherwise
|
||||||
|
would only cost honest mod authors. What lockstep and
|
||||||
|
`Protocol.unpackMon`'s recompute-from-species-data *do* guarantee is that
|
||||||
|
a cheater cannot invent stats, moves, or a shiny: every derived value is
|
||||||
|
rebuilt locally from real species data. They can send a legal party they
|
||||||
|
farmed or edited. That is the honest boundary.
|
||||||
|
|
||||||
|
What the relay does instead is **observe and record**. It already sees
|
||||||
|
every `hello`, so it keeps each connection's self-reported
|
||||||
|
`engineVersion`, `fingerprint` and `linkModified`, compares the two sides
|
||||||
|
of a room or a live tournament match, and logs and surfaces a
|
||||||
|
`modded` / `fingerprint_mismatch` / `version_skew` flag on the dashboard.
|
||||||
|
A patched client can still lie; what it cannot do is lie without the
|
||||||
|
tournament organizer having a record of it.
|
||||||
|
|
||||||
|
Client-side attestation is deliberately not built. This is an
|
||||||
|
open-source Lua game: it would be theater, and it would break honest
|
||||||
|
mods.
|
||||||
|
|
||||||
|
**The relay has no TLS.** Port 7778 is plaintext, so party contents,
|
||||||
|
trades and trainer names are visible to anyone on the network path. There
|
||||||
|
is nothing secret in a Pokemon party, but it is a real property of the
|
||||||
|
system and not an oversight. Fixing it means a TLS terminator in front of
|
||||||
|
the relay and a client that speaks it, which is a version break for every
|
||||||
|
shipped build.
|
||||||
|
|
||||||
|
**The dashboard has no default password.** `DASHBOARD_PASSWORD` is
|
||||||
|
required; with it unset the relay runs and the dashboard simply does not
|
||||||
|
start. It is still Basic Auth over plain HTTP, so it belongs behind an
|
||||||
|
IP restriction or an SSH tunnel (`pokeserver/DEPLOY.md`).
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
luajit tests/link_hostile.lua every message type x every wrong type
|
||||||
|
luajit tests/link_desync_fuzz.lua lockstep fuzz, plus a mutation mode
|
||||||
|
luajit tests/run_link_tests.lua both of the above, plus the rest
|
||||||
|
cd ../pokeserver && npm test relay smoke, 16-player bracket, hostile
|
||||||
|
|
||||||
|
`tests/link_hostile.lua` builds its corpus from a template per message
|
||||||
|
type, replaces each field (and several nested ones) with every wrong Lua
|
||||||
|
type, and drives the survivors through the real trade session, a real
|
||||||
|
lockstep battle, a real spectator battle, and the tournament screen
|
||||||
|
**including its draw** -- because the two nastiest payloads are
|
||||||
|
delayed-fuse ones that crash on render rather than on receipt.
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
# Linux ARM SBC Handhelds (PortMaster)
|
||||||
|
|
||||||
|
Download `gen1recomp-*-sbc-portmaster.zip` from the [Gen1Recomp releases](https://github.com/bryanthaboi/gen1recomp/releases). This build targets 64-bit Linux ARM handhelds with PortMaster, including compatible H700 devices.
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
|
1. Unzip the release. It contains `gen1recomp-sbc.sh` and a `gen1recomp-sbc/` folder.
|
||||||
|
2. Copy both as siblings into your device's PortMaster ports directory, commonly `Roms/Ports (PORTS)/` or `Roms/PORTS/`.
|
||||||
|
3. Install PortMaster for your firmware and refresh the Ports list.
|
||||||
|
4. Copy your legally owned canonical US Red or Blue `.gb` file into `gen1recomp-sbc/lovegame/`.
|
||||||
|
5. Launch **gen1recomp-sbc** from Ports and choose the ROM.
|
||||||
|
|
||||||
|
The pack includes `portable.txt`, so saves and ROM-derived cache remain beside the game on the SD card. The build never ships ROM-derived bytes.
|
||||||
|
|
||||||
|
Canonical US cart SHA-1 values:
|
||||||
|
|
||||||
|
- Red: `ea9bcae617fdf159b045185467ae58b2e4a48b9a`
|
||||||
|
- Blue: `d7037c83e1ae5b39bde3c30787637ba1d4c48ce2`
|
||||||
|
|
||||||
|
## Controls
|
||||||
|
|
||||||
|
| Input | Action |
|
||||||
|
| --- | --- |
|
||||||
|
| D-pad | Move cursor |
|
||||||
|
| A | Click / confirm |
|
||||||
|
| L1 / R1 | Switch tabs |
|
||||||
|
| Start / Select | Play or choose ROM |
|
||||||
|
|
||||||
|
In-game controls use the normal PortMaster/SDL mapping and can be rebound in **OPTIONS → CONTROLS**.
|
||||||
|
|
||||||
|
## Runtime and suspend
|
||||||
|
|
||||||
|
The package bundles PortMaster's LÖVE 11.5 aarch64 runtime. The launcher sources `control.txt`, calls `get_controls`, applies an optional CFW override, invokes `pm_platform_helper`, and calls `pm_finish` on exit. Paths are relative to the launcher, allowing different firmware mount points.
|
||||||
|
|
||||||
|
Suspend/resume uses the existing LÖVE focus/visibility lifecycle: input is reset on focus loss and the game resumes when the window becomes visible again. Exact power-button behavior remains firmware-dependent; hardware validation has been performed on the TrimUI Brick, not every SBC or H700 device.
|
||||||
|
|
||||||
|
## Building
|
||||||
|
|
||||||
|
Release workflows build this automatically. Standalone builds resolve the latest published Gen1Recomp release by default:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
./build-linux-arm-sbc.sh --version 0.1.75
|
||||||
|
```
|
||||||
|
|
||||||
|
For development, package a local checkout explicitly:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
GEN1RECOMP_SOURCE_DIR="$PWD" ./build-linux-arm-sbc.sh --version 0.1.0
|
||||||
|
# or: ./build-linux-arm-sbc.sh --source "$PWD" --version 0.1.0
|
||||||
|
```
|
||||||
|
|
||||||
|
The generated `port.json` records the source release tag. `install-linux-arm-sbc.sh` is a macOS helper for copying a built pack to a mounted SD card.
|
||||||
|
|
||||||
|
PortMaster device support and runtime integration are maintained in the [PortMaster](https://github.com/PortsMaster/PortMaster-New) ecosystem.
|
||||||