This commit is contained in:
bryanthaboi
2026-09-18 16:49:48 -04:00
parent bdfac727aa
commit 661d75ef1c
44 changed files with 5621 additions and 860 deletions
+744 -4
View File
@@ -1,7 +1,747 @@
Copyright 2026 BOIS CLUB GAMES, LLC
# License
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
Copyright (C) 2026 BOIS CLUB GAMES, LLC
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
This program is free software: you can redistribute it and/or modify it under
the terms of the GNU General Public License as published by the Free Software
Foundation, version 3 of the License, with the Additional Terms below, which
are added under Section 7 of that License.
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
This program is distributed in the hope that it will be useful, but WITHOUT
ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
## Additional Terms (GPLv3 Section 7)
### 1. Attribution (Section 7(b))
Any copy, fork, modified version, or work based on this program, in source or
object form, and for any purpose, must:
- keep this license file and every copyright and author notice in the source;
- give clear, visible credit to the original project in its README (or
equivalent project page) and in any in-app credits, "about", or title
screen, reading at minimum:
"Based on the Pokemon Gen 1 Recompilation Project by BOIS CLUB GAMES, LLC
(https://github.com/bryanthaboi/gen1recomp)";
- show that same credit, visibly, inside any launcher, front end, menu shell,
or other program used to start or select the game, including any launcher
written to replace the one excluded in term 2.
Using this program, in whole or in part, for other projects is permitted, as
long as this credit is always given.
### 2. The Launcher is not licensed under the GPL
The project launcher (the "Launcher") is NOT part of the program licensed
above. It is proprietary: Copyright (C) 2026 BOIS CLUB GAMES, LLC, all rights
reserved. The Launcher may not be copied, modified, redistributed, or used in
any fork, modified version, or other project, in whole or in part. A fork or
derivative must provide its own launcher, which must carry the credit
required by term 1.
The Launcher consists of the following files, and any later version of them:
- `src/import/LauncherView.lua`
- `src/import/LauncherSettings.lua`
- `src/import/OnlinePanel.lua`
- `src/import/CartLabelArt.lua`
- `src/import/CartShape.lua`
- `src/mods/LauncherMods.lua`
- the launcher user interface code in `src/import/RomImporter.lua` (the
ROM import, verification, and cache extraction logic in that file remains
under the GPL)
- `assets/launcher/` and `assets/labels/`
Distributing the Launcher together with this program is permitted only in
unmodified official releases from BOIS CLUB GAMES, LLC.
### 3. Names and marks (Section 7(e))
This license grants no rights to use the names, logos, or trademarks of BOIS
CLUB GAMES, LLC or of the project, except as needed to give the credit
required by term 1.
### 4. Third-party rights
This project contains no ROM data and no extracted game content. Pokemon and
all related names and content are the property of Nintendo, Creatures Inc.,
and GAME FREAK inc. This license covers only the original code and assets of
this project.
---
GNU GENERAL PUBLIC LICENSE
Version 3, 29 June 2007
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
Everyone is permitted to copy and distribute verbatim copies
of this license document, but changing it is not allowed.
Preamble
The GNU General Public License is a free, copyleft license for
software and other kinds of works.
The licenses for most software and other practical works are designed
to take away your freedom to share and change the works. By contrast,
the GNU General Public License is intended to guarantee your freedom to
share and change all versions of a program--to make sure it remains free
software for all its users. We, the Free Software Foundation, use the
GNU General Public License for most of our software; it applies also to
any other work released this way by its authors. You can apply it to
your programs, too.
When we speak of free software, we are referring to freedom, not
price. Our General Public Licenses are designed to make sure that you
have the freedom to distribute copies of free software (and charge for
them if you wish), that you receive source code or can get it if you
want it, that you can change the software or use pieces of it in new
free programs, and that you know you can do these things.
To protect your rights, we need to prevent others from denying you
these rights or asking you to surrender the rights. Therefore, you have
certain responsibilities if you distribute copies of the software, or if
you modify it: responsibilities to respect the freedom of others.
For example, if you distribute copies of such a program, whether
gratis or for a fee, you must pass on to the recipients the same
freedoms that you received. You must make sure that they, too, receive
or can get the source code. And you must show them these terms so they
know their rights.
Developers that use the GNU GPL protect your rights with two steps:
(1) assert copyright on the software, and (2) offer you this License
giving you legal permission to copy, distribute and/or modify it.
For the developers' and authors' protection, the GPL clearly explains
that there is no warranty for this free software. For both users' and
authors' sake, the GPL requires that modified versions be marked as
changed, so that their problems will not be attributed erroneously to
authors of previous versions.
Some devices are designed to deny users access to install or run
modified versions of the software inside them, although the manufacturer
can do so. This is fundamentally incompatible with the aim of
protecting users' freedom to change the software. The systematic
pattern of such abuse occurs in the area of products for individuals to
use, which is precisely where it is most unacceptable. Therefore, we
have designed this version of the GPL to prohibit the practice for those
products. If such problems arise substantially in other domains, we
stand ready to extend this provision to those domains in future versions
of the GPL, as needed to protect the freedom of users.
Finally, every program is threatened constantly by software patents.
States should not allow patents to restrict development and use of
software on general-purpose computers, but in those that do, we wish to
avoid the special danger that patents applied to a free program could
make it effectively proprietary. To prevent this, the GPL assures that
patents cannot be used to render the program non-free.
The precise terms and conditions for copying, distribution and
modification follow.
TERMS AND CONDITIONS
0. Definitions.
"This License" refers to version 3 of the GNU General Public License.
"Copyright" also means copyright-like laws that apply to other kinds of
works, such as semiconductor masks.
"The Program" refers to any copyrightable work licensed under this
License. Each licensee is addressed as "you". "Licensees" and
"recipients" may be individuals or organizations.
To "modify" a work means to copy from or adapt all or part of the work
in a fashion requiring copyright permission, other than the making of an
exact copy. The resulting work is called a "modified version" of the
earlier work or a work "based on" the earlier work.
A "covered work" means either the unmodified Program or a work based
on the Program.
To "propagate" a work means to do anything with it that, without
permission, would make you directly or secondarily liable for
infringement under applicable copyright law, except executing it on a
computer or modifying a private copy. Propagation includes copying,
distribution (with or without modification), making available to the
public, and in some countries other activities as well.
To "convey" a work means any kind of propagation that enables other
parties to make or receive copies. Mere interaction with a user through
a computer network, with no transfer of a copy, is not conveying.
An interactive user interface displays "Appropriate Legal Notices"
to the extent that it includes a convenient and prominently visible
feature that (1) displays an appropriate copyright notice, and (2)
tells the user that there is no warranty for the work (except to the
extent that warranties are provided), that licensees may convey the
work under this License, and how to view a copy of this License. If
the interface presents a list of user commands or options, such as a
menu, a prominent item in the list meets this criterion.
1. Source Code.
The "source code" for a work means the preferred form of the work
for making modifications to it. "Object code" means any non-source
form of a work.
A "Standard Interface" means an interface that either is an official
standard defined by a recognized standards body, or, in the case of
interfaces specified for a particular programming language, one that
is widely used among developers working in that language.
The "System Libraries" of an executable work include anything, other
than the work as a whole, that (a) is included in the normal form of
packaging a Major Component, but which is not part of that Major
Component, and (b) serves only to enable use of the work with that
Major Component, or to implement a Standard Interface for which an
implementation is available to the public in source code form. A
"Major Component", in this context, means a major essential component
(kernel, window system, and so on) of the specific operating system
(if any) on which the executable work runs, or a compiler used to
produce the work, or an object code interpreter used to run it.
The "Corresponding Source" for a work in object code form means all
the source code needed to generate, install, and (for an executable
work) run the object code and to modify the work, including scripts to
control those activities. However, it does not include the work's
System Libraries, or general-purpose tools or generally available free
programs which are used unmodified in performing those activities but
which are not part of the work. For example, Corresponding Source
includes interface definition files associated with source files for
the work, and the source code for shared libraries and dynamically
linked subprograms that the work is specifically designed to require,
such as by intimate data communication or control flow between those
subprograms and other parts of the work.
The Corresponding Source need not include anything that users
can regenerate automatically from other parts of the Corresponding
Source.
The Corresponding Source for a work in source code form is that
same work.
2. Basic Permissions.
All rights granted under this License are granted for the term of
copyright on the Program, and are irrevocable provided the stated
conditions are met. This License explicitly affirms your unlimited
permission to run the unmodified Program. The output from running a
covered work is covered by this License only if the output, given its
content, constitutes a covered work. This License acknowledges your
rights of fair use or other equivalent, as provided by copyright law.
You may make, run and propagate covered works that you do not
convey, without conditions so long as your license otherwise remains
in force. You may convey covered works to others for the sole purpose
of having them make modifications exclusively for you, or provide you
with facilities for running those works, provided that you comply with
the terms of this License in conveying all material for which you do
not control copyright. Those thus making or running the covered works
for you must do so exclusively on your behalf, under your direction
and control, on terms that prohibit them from making any copies of
your copyrighted material outside their relationship with you.
Conveying under any other circumstances is permitted solely under
the conditions stated below. Sublicensing is not allowed; section 10
makes it unnecessary.
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
No covered work shall be deemed part of an effective technological
measure under any applicable law fulfilling obligations under article
11 of the WIPO copyright treaty adopted on 20 December 1996, or
similar laws prohibiting or restricting circumvention of such
measures.
When you convey a covered work, you waive any legal power to forbid
circumvention of technological measures to the extent such circumvention
is effected by exercising rights under this License with respect to
the covered work, and you disclaim any intention to limit operation or
modification of the work as a means of enforcing, against the work's
users, your or third parties' legal rights to forbid circumvention of
technological measures.
4. Conveying Verbatim Copies.
You may convey verbatim copies of the Program's source code as you
receive it, in any medium, provided that you conspicuously and
appropriately publish on each copy an appropriate copyright notice;
keep intact all notices stating that this License and any
non-permissive terms added in accord with section 7 apply to the code;
keep intact all notices of the absence of any warranty; and give all
recipients a copy of this License along with the Program.
You may charge any price or no price for each copy that you convey,
and you may offer support or warranty protection for a fee.
5. Conveying Modified Source Versions.
You may convey a work based on the Program, or the modifications to
produce it from the Program, in the form of source code under the
terms of section 4, provided that you also meet all of these conditions:
a) The work must carry prominent notices stating that you modified
it, and giving a relevant date.
b) The work must carry prominent notices stating that it is
released under this License and any conditions added under section
7. This requirement modifies the requirement in section 4 to
"keep intact all notices".
c) You must license the entire work, as a whole, under this
License to anyone who comes into possession of a copy. This
License will therefore apply, along with any applicable section 7
additional terms, to the whole of the work, and all its parts,
regardless of how they are packaged. This License gives no
permission to license the work in any other way, but it does not
invalidate such permission if you have separately received it.
d) If the work has interactive user interfaces, each must display
Appropriate Legal Notices; however, if the Program has interactive
interfaces that do not display Appropriate Legal Notices, your
work need not make them do so.
A compilation of a covered work with other separate and independent
works, which are not by their nature extensions of the covered work,
and which are not combined with it such as to form a larger program,
in or on a volume of a storage or distribution medium, is called an
"aggregate" if the compilation and its resulting copyright are not
used to limit the access or legal rights of the compilation's users
beyond what the individual works permit. Inclusion of a covered work
in an aggregate does not cause this License to apply to the other
parts of the aggregate.
6. Conveying Non-Source Forms.
You may convey a covered work in object code form under the terms
of sections 4 and 5, provided that you also convey the
machine-readable Corresponding Source under the terms of this License,
in one of these ways:
a) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by the
Corresponding Source fixed on a durable physical medium
customarily used for software interchange.
b) Convey the object code in, or embodied in, a physical product
(including a physical distribution medium), accompanied by a
written offer, valid for at least three years and valid for as
long as you offer spare parts or customer support for that product
model, to give anyone who possesses the object code either (1) a
copy of the Corresponding Source for all the software in the
product that is covered by this License, on a durable physical
medium customarily used for software interchange, for a price no
more than your reasonable cost of physically performing this
conveying of source, or (2) access to copy the
Corresponding Source from a network server at no charge.
c) Convey individual copies of the object code with a copy of the
written offer to provide the Corresponding Source. This
alternative is allowed only occasionally and noncommercially, and
only if you received the object code with such an offer, in accord
with subsection 6b.
d) Convey the object code by offering access from a designated
place (gratis or for a charge), and offer equivalent access to the
Corresponding Source in the same way through the same place at no
further charge. You need not require recipients to copy the
Corresponding Source along with the object code. If the place to
copy the object code is a network server, the Corresponding Source
may be on a different server (operated by you or a third party)
that supports equivalent copying facilities, provided you maintain
clear directions next to the object code saying where to find the
Corresponding Source. Regardless of what server hosts the
Corresponding Source, you remain obligated to ensure that it is
available for as long as needed to satisfy these requirements.
e) Convey the object code using peer-to-peer transmission, provided
you inform other peers where the object code and Corresponding
Source of the work are being offered to the general public at no
charge under subsection 6d.
A separable portion of the object code, whose source code is excluded
from the Corresponding Source as a System Library, need not be
included in conveying the object code work.
A "User Product" is either (1) a "consumer product", which means any
tangible personal property which is normally used for personal, family,
or household purposes, or (2) anything designed or sold for incorporation
into a dwelling. In determining whether a product is a consumer product,
doubtful cases shall be resolved in favor of coverage. For a particular
product received by a particular user, "normally used" refers to a
typical or common use of that class of product, regardless of the status
of the particular user or of the way in which the particular user
actually uses, or expects or is expected to use, the product. A product
is a consumer product regardless of whether the product has substantial
commercial, industrial or non-consumer uses, unless such uses represent
the only significant mode of use of the product.
"Installation Information" for a User Product means any methods,
procedures, authorization keys, or other information required to install
and execute modified versions of a covered work in that User Product from
a modified version of its Corresponding Source. The information must
suffice to ensure that the continued functioning of the modified object
code is in no case prevented or interfered with solely because
modification has been made.
If you convey an object code work under this section in, or with, or
specifically for use in, a User Product, and the conveying occurs as
part of a transaction in which the right of possession and use of the
User Product is transferred to the recipient in perpetuity or for a
fixed term (regardless of how the transaction is characterized), the
Corresponding Source conveyed under this section must be accompanied
by the Installation Information. But this requirement does not apply
if neither you nor any third party retains the ability to install
modified object code on the User Product (for example, the work has
been installed in ROM).
The requirement to provide Installation Information does not include a
requirement to continue to provide support service, warranty, or updates
for a work that has been modified or installed by the recipient, or for
the User Product in which it has been modified or installed. Access to a
network may be denied when the modification itself materially and
adversely affects the operation of the network or violates the rules and
protocols for communication across the network.
Corresponding Source conveyed, and Installation Information provided,
in accord with this section must be in a format that is publicly
documented (and with an implementation available to the public in
source code form), and must require no special password or key for
unpacking, reading or copying.
7. Additional Terms.
"Additional permissions" are terms that supplement the terms of this
License by making exceptions from one or more of its conditions.
Additional permissions that are applicable to the entire Program shall
be treated as though they were included in this License, to the extent
that they are valid under applicable law. If additional permissions
apply only to part of the Program, that part may be used separately
under those permissions, but the entire Program remains governed by
this License without regard to the additional permissions.
When you convey a copy of a covered work, you may at your option
remove any additional permissions from that copy, or from any part of
it. (Additional permissions may be written to require their own
removal in certain cases when you modify the work.) You may place
additional permissions on material, added by you to a covered work,
for which you have or can give appropriate copyright permission.
Notwithstanding any other provision of this License, for material you
add to a covered work, you may (if authorized by the copyright holders of
that material) supplement the terms of this License with terms:
a) Disclaiming warranty or limiting liability differently from the
terms of sections 15 and 16 of this License; or
b) Requiring preservation of specified reasonable legal notices or
author attributions in that material or in the Appropriate Legal
Notices displayed by works containing it; or
c) Prohibiting misrepresentation of the origin of that material, or
requiring that modified versions of such material be marked in
reasonable ways as different from the original version; or
d) Limiting the use for publicity purposes of names of licensors or
authors of the material; or
e) Declining to grant rights under trademark law for use of some
trade names, trademarks, or service marks; or
f) Requiring indemnification of licensors and authors of that
material by anyone who conveys the material (or modified versions of
it) with contractual assumptions of liability to the recipient, for
any liability that these contractual assumptions directly impose on
those licensors and authors.
All other non-permissive additional terms are considered "further
restrictions" within the meaning of section 10. If the Program as you
received it, or any part of it, contains a notice stating that it is
governed by this License along with a term that is a further
restriction, you may remove that term. If a license document contains
a further restriction but permits relicensing or conveying under this
License, you may add to a covered work material governed by the terms
of that license document, provided that the further restriction does
not survive such relicensing or conveying.
If you add terms to a covered work in accord with this section, you
must place, in the relevant source files, a statement of the
additional terms that apply to those files, or a notice indicating
where to find the applicable terms.
Additional terms, permissive or non-permissive, may be stated in the
form of a separately written license, or stated as exceptions;
the above requirements apply either way.
8. Termination.
You may not propagate or modify a covered work except as expressly
provided under this License. Any attempt otherwise to propagate or
modify it is void, and will automatically terminate your rights under
this License (including any patent licenses granted under the third
paragraph of section 11).
However, if you cease all violation of this License, then your
license from a particular copyright holder is reinstated (a)
provisionally, unless and until the copyright holder explicitly and
finally terminates your license, and (b) permanently, if the copyright
holder fails to notify you of the violation by some reasonable means
prior to 60 days after the cessation.
Moreover, your license from a particular copyright holder is
reinstated permanently if the copyright holder notifies you of the
violation by some reasonable means, this is the first time you have
received notice of violation of this License (for any work) from that
copyright holder, and you cure the violation prior to 30 days after
your receipt of the notice.
Termination of your rights under this section does not terminate the
licenses of parties who have received copies or rights from you under
this License. If your rights have been terminated and not permanently
reinstated, you do not qualify to receive new licenses for the same
material under section 10.
9. Acceptance Not Required for Having Copies.
You are not required to accept this License in order to receive or
run a copy of the Program. Ancillary propagation of a covered work
occurring solely as a consequence of using peer-to-peer transmission
to receive a copy likewise does not require acceptance. However,
nothing other than this License grants you permission to propagate or
modify any covered work. These actions infringe copyright if you do
not accept this License. Therefore, by modifying or propagating a
covered work, you indicate your acceptance of this License to do so.
10. Automatic Licensing of Downstream Recipients.
Each time you convey a covered work, the recipient automatically
receives a license from the original licensors, to run, modify and
propagate that work, subject to this License. You are not responsible
for enforcing compliance by third parties with this License.
An "entity transaction" is a transaction transferring control of an
organization, or substantially all assets of one, or subdividing an
organization, or merging organizations. If propagation of a covered
work results from an entity transaction, each party to that
transaction who receives a copy of the work also receives whatever
licenses to the work the party's predecessor in interest had or could
give under the previous paragraph, plus a right to possession of the
Corresponding Source of the work from the predecessor in interest, if
the predecessor has it or can get it with reasonable efforts.
You may not impose any further restrictions on the exercise of the
rights granted or affirmed under this License. For example, you may
not impose a license fee, royalty, or other charge for exercise of
rights granted under this License, and you may not initiate litigation
(including a cross-claim or counterclaim in a lawsuit) alleging that
any patent claim is infringed by making, using, selling, offering for
sale, or importing the Program or any portion of it.
11. Patents.
A "contributor" is a copyright holder who authorizes use under this
License of the Program or a work on which the Program is based. The
work thus licensed is called the contributor's "contributor version".
A contributor's "essential patent claims" are all patent claims
owned or controlled by the contributor, whether already acquired or
hereafter acquired, that would be infringed by some manner, permitted
by this License, of making, using, or selling its contributor version,
but do not include claims that would be infringed only as a
consequence of further modification of the contributor version. For
purposes of this definition, "control" includes the right to grant
patent sublicenses in a manner consistent with the requirements of
this License.
Each contributor grants you a non-exclusive, worldwide, royalty-free
patent license under the contributor's essential patent claims, to
make, use, sell, offer for sale, import and otherwise run, modify and
propagate the contents of its contributor version.
In the following three paragraphs, a "patent license" is any express
agreement or commitment, however denominated, not to enforce a patent
(such as an express permission to practice a patent or covenant not to
sue for patent infringement). To "grant" such a patent license to a
party means to make such an agreement or commitment not to enforce a
patent against the party.
If you convey a covered work, knowingly relying on a patent license,
and the Corresponding Source of the work is not available for anyone
to copy, free of charge and under the terms of this License, through a
publicly available network server or other readily accessible means,
then you must either (1) cause the Corresponding Source to be so
available, or (2) arrange to deprive yourself of the benefit of the
patent license for this particular work, or (3) arrange, in a manner
consistent with the requirements of this License, to extend the patent
license to downstream recipients. "Knowingly relying" means you have
actual knowledge that, but for the patent license, your conveying the
covered work in a country, or your recipient's use of the covered work
in a country, would infringe one or more identifiable patents in that
country that you have reason to believe are valid.
If, pursuant to or in connection with a single transaction or
arrangement, you convey, or propagate by procuring conveyance of, a
covered work, and grant a patent license to some of the parties
receiving the covered work authorizing them to use, propagate, modify
or convey a specific copy of the covered work, then the patent license
you grant is automatically extended to all recipients of the covered
work and works based on it.
A patent license is "discriminatory" if it does not include within
the scope of its coverage, prohibits the exercise of, or is
conditioned on the non-exercise of one or more of the rights that are
specifically granted under this License. You may not convey a covered
work if you are a party to an arrangement with a third party that is
in the business of distributing software, under which you make payment
to the third party based on the extent of your activity of conveying
the work, and under which the third party grants, to any of the
parties who would receive the covered work from you, a discriminatory
patent license (a) in connection with copies of the covered work
conveyed by you (or copies made from those copies), or (b) primarily
for and in connection with specific products or compilations that
contain the covered work, unless you entered into that arrangement,
or that patent license was granted, prior to 28 March 2007.
Nothing in this License shall be construed as excluding or limiting
any implied license or other defenses to infringement that may
otherwise be available to you under applicable patent law.
12. No Surrender of Others' Freedom.
If conditions are imposed on you (whether by court order, agreement or
otherwise) that contradict the conditions of this License, they do not
excuse you from the conditions of this License. If you cannot convey a
covered work so as to satisfy simultaneously your obligations under this
License and any other pertinent obligations, then as a consequence you may
not convey it at all. For example, if you agree to terms that obligate you
to collect a royalty for further conveying from those to whom you convey
the Program, the only way you could satisfy both those terms and this
License would be to refrain entirely from conveying the Program.
13. Use with the GNU Affero General Public License.
Notwithstanding any other provision of this License, you have
permission to link or combine any covered work with a work licensed
under version 3 of the GNU Affero General Public License into a single
combined work, and to convey the resulting work. The terms of this
License will continue to apply to the part which is the covered work,
but the special requirements of the GNU Affero General Public License,
section 13, concerning interaction through a network will apply to the
combination as such.
14. Revised Versions of this License.
The Free Software Foundation may publish revised and/or new versions of
the GNU General Public License from time to time. Such new versions will
be similar in spirit to the present version, but may differ in detail to
address new problems or concerns.
Each version is given a distinguishing version number. If the
Program specifies that a certain numbered version of the GNU General
Public License "or any later version" applies to it, you have the
option of following the terms and conditions either of that numbered
version or of any later version published by the Free Software
Foundation. If the Program does not specify a version number of the
GNU General Public License, you may choose any version ever published
by the Free Software Foundation.
If the Program specifies that a proxy can decide which future
versions of the GNU General Public License can be used, that proxy's
public statement of acceptance of a version permanently authorizes you
to choose that version for the Program.
Later license versions may give you additional or different
permissions. However, no additional obligations are imposed on any
author or copyright holder as a result of your choosing to follow a
later version.
15. Disclaimer of Warranty.
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
16. Limitation of Liability.
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGES.
17. Interpretation of Sections 15 and 16.
If the disclaimer of warranty and limitation of liability provided
above cannot be given local legal effect according to their terms,
reviewing courts shall apply local law that most closely approximates
an absolute waiver of all civil liability in connection with the
Program, unless a warranty or assumption of liability accompanies a
copy of the Program in return for a fee.
END OF TERMS AND CONDITIONS
How to Apply These Terms to Your New Programs
If you develop a new program, and you want it to be of the greatest
possible use to the public, the best way to achieve this is to make it
free software which everyone can redistribute and change under these terms.
To do so, attach the following notices to the program. It is safest
to attach them to the start of each source file to most effectively
state the exclusion of warranty; and each file should have at least
the "copyright" line and a pointer to where the full notice is found.
<one line to give the program's name and a brief idea of what it does.>
Copyright (C) <year> <name of author>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
Also add information on how to contact you by electronic and paper mail.
If the program does terminal interaction, make it output a short
notice like this when it starts in an interactive mode:
<program> Copyright (C) <year> <name of author>
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
This is free software, and you are welcome to redistribute it
under certain conditions; type `show c' for details.
The hypothetical commands `show w' and `show c' should show the appropriate
parts of the General Public License. Of course, your program's commands
might be different; for a GUI interface, you would use an "about box".
You should also get your employer (if you work as a programmer) or school,
if any, to sign a "copyright disclaimer" for the program, if necessary.
For more information on this, and how to apply and follow the GNU GPL, see
<https://www.gnu.org/licenses/>.
The GNU General Public License does not permit incorporating your program
into proprietary programs. If your program is a subroutine library, you
may consider it more useful to permit linking proprietary applications with
the library. If this is what you want to do, use the GNU Lesser General
Public License instead of this License. But first, please read
<https://www.gnu.org/licenses/why-not-lgpl.html>.
+9 -1
View File
@@ -339,7 +339,15 @@ function Game3:_handleRegisteredItem()
session.registeredItem = nil
return
end
ItemUse.useField(session, session.bag, item, nil)
local ok, kind, text = ItemUse.useField(session, session.bag, item, nil)
if kind == "vs_seeker" then
-- pokefirered/src/item_use.c:712
if ok then
require("src.core.game3.vs_seeker").use(session, self)
elseif text then
require("src.ui.game3.hud").openMessage(self, text)
end
end
end
function Game3:_handleBootAction(action)
+16 -3
View File
@@ -24,6 +24,7 @@ Anim._packLoaded = false
Anim._hpTweening = false
Anim._expTweening = false
Anim._introTweening = 0
Anim._stageTasks = {}
Anim._seqBusy = false
Anim._statusQueue = {}
Anim._present = AnimCoords.idTable()
@@ -239,6 +240,8 @@ end
function Anim.reset(opts)
opts = opts or {}
for id in pairs(Anim._stageTasks) do Task.cancel(id) end
Anim._stageTasks = {}
Anim._headless = opts.headless and true or false
Anim._hpTweening = false
Anim._expTweening = false
@@ -457,10 +460,14 @@ function Anim.tweenStage(frames, onStep, onComplete)
return nil
end
Anim._introTweening = (Anim._introTweening or 0) + 1
return Task.tween(frames, onStep, function()
local t
t = Task.tween(frames, onStep, function()
Anim._stageTasks[t.id] = nil
Anim._introTweening = math.max(0, (Anim._introTweening or 1) - 1)
if onComplete then onComplete() end
end)
Anim._stageTasks[t.id] = true
return t
end
function Anim.seqBusy()
@@ -513,13 +520,16 @@ function Anim.tweenHp(side, fromHp, toHp, maxHp, opts)
if opts.frames then frames = opts.frames end
Anim._hpTweening = true
p.displayHp = fromHp
Task.tween(frames, function(u)
local t
t = Task.tween(frames, function(u)
p.displayHp = fromHp + (toHp - fromHp) * u
end, function()
Anim._stageTasks[t.id] = nil
p.displayHp = toHp
Anim._hpTweening = false
if opts.onComplete then opts.onComplete() end
end)
Anim._stageTasks[t.id] = true
end
--- Lerp player EXP bar ratio 0..1 within current level band.
@@ -550,7 +560,8 @@ function Anim.tweenExp(side, fromRatio, toRatio, opts)
end
Anim._expTweening = true
p.displayExp = fromRatio
Task.tween(totalFrames, function(_, t)
local task
task = Task.tween(totalFrames, function(_, t)
local curFrame = t.frames or 0
if curFrame <= leadIn then
p.displayExp = fromRatio
@@ -559,10 +570,12 @@ function Anim.tweenExp(side, fromRatio, toRatio, opts)
p.displayExp = fromRatio + (toRatio - fromRatio) * u
end
end, function()
Anim._stageTasks[task.id] = nil
p.displayExp = toRatio
Anim._expTweening = false
if opts.onComplete then opts.onComplete() end
end)
Anim._stageTasks[task.id] = true
end
function Anim.displayExpRatio(side, battler)
File diff suppressed because it is too large Load Diff
+7 -1
View File
@@ -227,9 +227,11 @@ local function drawFieldPlane(game, vw, vh, Renderer)
local Oam = require("src.core.game3.oam")
local FieldView = require("src.core.game3.field_view")
local Tilt = require("src.render.Tilt")
local Transition = package.loaded["src.core.game3.battle_transition"]
local transitioning = Transition and Transition.isActive and Transition.isActive()
Oam.resetFrame()
local prev = Oam.setLayer("world")
if Tilt.active() and Renderer and Renderer.beginUprightPass then
if Tilt.active() and not transitioning and Renderer and Renderer.beginUprightPass then
FieldView.draw(game, vw, vh, { skipActors = true })
Renderer:beginUprightPass()
FieldView.draw(game, vw, vh, { actorsOnly = true, billboard = true })
@@ -304,6 +306,10 @@ local function presentPlanes(game)
local vw, vh = Renderer:worldViewSize()
drawFieldPlane(game, vw, vh, Renderer)
love.graphics.pop()
local Transition = package.loaded["src.core.game3.battle_transition"]
if Transition and Transition.isActive and Transition.isActive() then
Transition.drawWorld(Renderer.worldCanvas, vw, vh)
end
Renderer:endWorldPass()
love.graphics.push("all")
+4 -3
View File
@@ -441,12 +441,13 @@ local function drawGame3Actors(game, camX, camY, px, py, facing, walkPhase, step
kind = "npc",
i = eo.localId,
obj = eo.def,
x = eo.px or (eo.cellX * CELL),
y = eo.py or (eo.cellY * CELL),
x = (eo.px or (eo.cellX * CELL)) + (eo.raiseX or 0),
y = (eo.py or (eo.cellY * CELL)) + (eo.raiseY or 0),
sortY = eo.py or (eo.cellY * CELL),
facing = eo.facing or "down",
walkPhase = Objects.walkPhase(eo),
stepFlip = eo.stepFlip and true or false,
bow = eo.bowFrames and eo.bowFrames > 0,
bow = (eo.bowFrames and eo.bowFrames > 0) or eo.raiseHand == true,
frame = eo.customFrame,
sprite = eo.sprite or spriteNameForObj(eo.def or {}),
graphicsId = eo.graphicsId or (eo.def and (eo.def.graphicsId or eo.def.graphics)),
+5 -3
View File
@@ -442,8 +442,10 @@ local function useField(session, bag, id, partySlot)
if use == "vs_seeker" or id == ItemsData.ITEM_VS_SEEKER or id == "VS_SEEKER"
or ItemsData.toNumericId(id) == ItemsData.ITEM_VS_SEEKER then
local VsSeeker = require("src.core.game3.vs_seeker")
local ok = VsSeeker.use(session, nil)
return ok, "vs_seeker", nil
if not VsSeeker.canUseHere(session) then
return false, "vs_seeker", VsSeeker.notTimeText(session)
end
return true, "vs_seeker", nil
end
if id == ItemsData.ITEM_TM_CASE or id == "TM_CASE"
@@ -549,7 +551,7 @@ function ItemUse.useField(session,bag,id,partySlot)
else
ok,kind,text=useField(session,bag,id,partySlot)
end
if ok and kind~="tm" and kind~="tm_case" and kind~="berry_pouch" then
if ok and kind~="tm" and kind~="tm_case" and kind~="berry_pouch" and kind~="vs_seeker" then
local Items=require("src.core.game3.items")
local Pokemon=require("src.core.game3.pokemon")
local mon=partySlot and session and session.party and session.party[partySlot]
+4
View File
@@ -385,6 +385,10 @@ function Map.load(mod, game, mapId, opts)
end
local Objects = require("src.core.game3.objects")
-- pokefirered/src/overworld.c:800
if fromMapId and (fromMapId ~= mapId or opts.heal) then
require("src.core.game3.vs_seeker").mapReset(session)
end
if Space and Space.activate then
Space.activate(mod or Runtime._mod, mapId, game, world)
end
+96 -2
View File
@@ -29,6 +29,7 @@ Objects._bounds = nil
-- Permanent template overrides from setobjectxyperm / setobjectmovementtype.
-- Survives loadMap within a session (pret objectEventTemplates).
Objects._perm = {} -- [mapId] = { [localId] = { x=, y=, movementType= } }
Objects._templateMt = {}
Objects._logged = false
local function log(msg)
@@ -276,6 +277,7 @@ function Objects.loadMap(game, mapId, mapDef)
-- Never wipe in-flight applymovement on a same-map rebind (host setMap echo).
if not sameMap then
Objects._tracks = {}
Objects._templateMt = {}
end
Objects._byId = {}
Objects._order = {}
@@ -308,6 +310,12 @@ function Objects.loadMap(game, mapId, mapDef)
local eo = newEventObject(def)
if eo.localId > 0 then
applyPerm(eo, mapId)
local tmt = Objects._templateMt[eo.localId]
if tmt then
Objects.setTrainerMovementType(eo, tmt)
local face = ({ [7] = "up", [8] = "down", [9] = "left", [10] = "right" })[tmt]
if face then eo.facing = face end
end
Objects._byId[eo.localId] = eo
Objects._order[#Objects._order + 1] = eo.localId
if announce then
@@ -609,10 +617,58 @@ function Objects.clearMovements()
Objects._tracks = {}
end
local function sine(i)
local v = 256 * math.sin((i % 256) * math.pi / 128)
return v >= 0 and math.floor(v + 0.5) or -math.floor(-v + 0.5)
end
-- src/event_object_movement.c:7812
local function raiseHandTick(eo)
local rh = eo.raiseHandState
if not rh then
rh = { mode = 0, angle = 0, hops = 0, timer = 0, swing = 0 }
eo.raiseHandState = rh
eo.raiseHand = true
end
local mt = tonumber(eo.movementType) or 0
if mt == 0x4F then
rh.swing = (rh.swing + 4) % 256
eo.raiseX = math.floor(sine(rh.swing) / 128)
return
end
if mt ~= 0x4E then return end
if rh.mode == 0 then
rh.angle = rh.angle + 10
if rh.angle > 127 then
rh.angle = 0
rh.hops = rh.hops + 1
rh.mode = rh.hops
eo.raiseHand = false
end
eo.raiseY = -math.floor(3 * sine(rh.angle) / 128)
elseif rh.mode == 1 then
rh.timer = rh.timer + 1
if rh.timer > 16 then
rh.timer = 0
eo.raiseHand = true
rh.mode = 0
end
else
rh.timer = rh.timer + 1
if rh.timer > 80 then
eo.raiseHandState = nil
end
end
end
local function idleTick(eo, game, ctx)
if eo.frozen or eo.scriptBusy or eo.moving or eo.hidden or not eo.visible then
return
end
if eo.movement == "RAISE_HAND" then
raiseHandTick(eo)
return
end
eo.idleTimer = (eo.idleTimer or 0) - 1
if eo.idleTimer > 0 then return end
@@ -884,6 +940,23 @@ function Objects.setMovementType(localId, mt)
local mapKey = (Sp and Sp.mapId) or Objects._mapId
rememberPerm(mapKey, lid, { movementType = mt })
if not eo then return end
Objects.setTrainerMovementType(eo, mt)
local face = ({ [7] = "up", [8] = "down", [9] = "left", [10] = "right" })[mt]
if face then eo.facing = face end
end
local function clearRaiseHand(eo)
eo.raiseHandState = nil
eo.raiseHand = nil
eo.raiseX = nil
eo.raiseY = nil
end
-- src/event_object_movement.c:4806
function Objects.setTrainerMovementType(localId, mt)
local eo = type(localId) == "table" and localId or Objects._byId[tonumber(localId) or 0]
if not eo then return end
mt = tonumber(mt) or 0
eo.movementType = mt
local hostMv = GfxIds.hostMovement and GfxIds.hostMovement(mt)
if hostMv then
@@ -891,8 +964,29 @@ function Objects.setMovementType(localId, mt)
eo.range = hostMv.range
if hostMv.radius then eo.radius = hostMv.radius end
end
local face = ({ [7] = "up", [8] = "down", [9] = "left", [10] = "right" })[mt]
if face then eo.facing = face end
clearRaiseHand(eo)
if eo.movement == "RAISE_HAND" then
eo.facing = "down"
end
end
-- src/event_object_movement.c:2640
function Objects.overrideTemplateMovementType(localId, mt)
local lid = tonumber(localId) or 0
if lid <= 0 then return end
Objects._templateMt[lid] = tonumber(mt)
end
function Objects.templateMovementType(localId)
local lid = tonumber(localId) or 0
local o = Objects._templateMt[lid]
if o then return o end
for _, def in ipairs(Objects._defs or {}) do
if tonumber(def.localId or def.index) == lid then
return tonumber(def.movementType) or 0
end
end
return nil
end
function Objects.facePlayer(localId, game)
+6
View File
@@ -98,6 +98,12 @@ function Options.lEqualsA(session)
return tonumber(o.buttonMode) == 2
end
-- src/menu_helpers.c:72
function Options.lrMode(session)
local o = Options.ensure(session)
return tonumber(o.buttonMode) == 1
end
-- pokefirered/src/battle_main.c
function Options.battleStyle(session)
local o = Options.ensure(session)
+3
View File
@@ -60,6 +60,7 @@ function Schema.newGame(opts)
move_overlay = {},
trainerId = nil,
rng = nil,
vsSeeker = { steps = 0, charging = 0, rematches = {} },
}
-- pret new_game.c: SeedWildEncounterRng(Random()) after title SeedRngAndSetTrainerId.
local Rng = require("src.core.game3.rng")
@@ -119,6 +120,7 @@ function Schema.toSaveTable(session)
move_overlay = session.move_overlay or {},
trainerId = session.trainerId,
rng = session.rng,
vsSeeker = session.vsSeeker,
questLog = require("src.core.game3.quest_log").export(session),
modData = session.modData,
meta = session.meta,
@@ -162,6 +164,7 @@ function Schema.fromSaveTable(save)
move_overlay = save.move_overlay or {},
trainerId = save.trainerId,
rng = save.rng,
vsSeeker = type(save.vsSeeker) == "table" and save.vsSeeker or { steps = 0, charging = 0, rematches = {} },
questLog = require("src.core.game3.quest_log").restore(save.questLog),
modData = type(save.modData) == "table" and save.modData or {},
meta = save.meta,
+37 -28
View File
@@ -135,6 +135,39 @@ function Adapters.stub(opts)
return a
end
function Adapters.resolveNpcColor(ctx, store)
local Ctx = require("src.core.game3.scripting.ctx")
local sv = ctx and ctx.specialVars or {}
local tc = sv[Ctx.VAR_TEXT_COLOR]
if tc == nil then tc = Ctx.TEXT_COLOR_DEFAULT end
-- src/field_specials.c:1548
if tc ~= Ctx.TEXT_COLOR_DEFAULT then return tc end
local sel = ctx and tonumber(ctx.selectedLocalId) or 0
if sel == 0 then return 3 end
local gfx = nil
local Objects = package.loaded["src.core.game3.objects"]
local obj = Objects and Objects.find and not (Objects.isPlayer and Objects.isPlayer(sel)) and Objects.find(sel)
if obj then
gfx = obj.graphicsId or (obj.def and (obj.def.graphicsId or obj.def.graphics))
end
gfx = tonumber(gfx) or tonumber(ctx.selectedGfx)
if gfx and gfx >= 240 and gfx <= 255 then
local Flags = require("src.core.game3.scripting.flags")
local Space = package.loaded["src.core.game3.scripting.space"]
local v = Flags.getVar(store or (Space and Space.store), ctx, Ctx.GFX_VAR_LO + (gfx - 240))
gfx = (type(v) == "number" and v > 0) and v or gfx
end
local FrlgFont = require("src.ui.game3.frlg_font")
return FrlgFont.getNpcTextColor(gfx)
end
local function current_npc_color()
local Space = package.loaded["src.core.game3.scripting.space"]
local ctx = Space and Space.vm and Space.vm.ctx
if not ctx then return nil end
return Adapters.resolveNpcColor(ctx, Space.store)
end
local function tick_vm()
local Space = package.loaded["src.core.game3.scripting.space"]
if Space and Space.vm then Space.vm:tick() end
@@ -297,21 +330,9 @@ function Adapters.host(mod, game, world)
local Hud = require("src.ui.game3.hud")
local g = resolveGame()
a.log("[game3] dialog via game3 HUD (not Gen2 showText)")
local gfxId = nil
local Space = package.loaded["src.core.game3.scripting.space"]
local Ctx = package.loaded["src.core.game3.scripting.context"]
local Objects = package.loaded["src.core.game3.objects"]
if Space and Space.vm and Space.vm.ctx and Ctx and Objects and Objects.get then
local lid = Space.vm.ctx.specialVars[Ctx.VAR_LAST_TALKED]
if lid and lid > 0 then
local obj = Objects.get(lid)
if obj and obj.def then
gfxId = obj.def.graphicsId or obj.def.gfx
end
end
end
local npcColor = current_npc_color()
Hud.openMessage(g, text, {
gfxId = gfxId,
npcColor = npcColor,
done = function()
boxOpen = false
if done then done() end
@@ -344,21 +365,9 @@ function Adapters.host(mod, game, world)
local Hud = require("src.ui.game3.hud")
local g = resolveGame()
a.log("[game3] stay-dialog via game3 HUD")
local gfxId = nil
local Space = package.loaded["src.core.game3.scripting.space"]
local Ctx = package.loaded["src.core.game3.scripting.context"]
local Objects = package.loaded["src.core.game3.objects"]
if Space and Space.vm and Space.vm.ctx and Ctx and Objects and Objects.get then
local lid = Space.vm.ctx.specialVars[Ctx.VAR_LAST_TALKED]
if lid and lid > 0 then
local obj = Objects.get(lid)
if obj and obj.def then
gfxId = obj.def.graphicsId or obj.def.gfx
end
end
end
local npcColor = current_npc_color()
Hud.openMessageStay(g, text, {
gfxId = gfxId,
npcColor = npcColor,
done = function()
if done then done() end
tick_vm()
+17 -2
View File
@@ -13,6 +13,9 @@ Ctx.VAR_FACING = 0x800C
Ctx.VAR_RESULT = 0x800D
Ctx.VAR_ITEM_ID = 0x800E
Ctx.VAR_LAST_TALKED = 0x800F
Ctx.VAR_TEXT_COLOR = 0x8012
Ctx.VAR_PREV_TEXT_COLOR = 0x8013
Ctx.TEXT_COLOR_DEFAULT = 255
function Ctx.isSpecial(id)
id = tonumber(id) or 0
@@ -38,7 +41,7 @@ function Ctx.new(opts)
comparisonResult = 0,
data = { [0] = 0, [1] = 0, [2] = 0, [3] = 0 },
stringVars = { [1] = "", [2] = "", [3] = "" },
specialVars = {},
specialVars = { [Ctx.VAR_TEXT_COLOR] = Ctx.TEXT_COLOR_DEFAULT },
lockSnapshots = {},
lockKind = nil, -- "single" | "all" | nil
activeMoves = {},
@@ -53,7 +56,18 @@ function Ctx.new(opts)
end
function Ctx.wipeSpecial(ctx)
ctx.specialVars = {}
ctx.specialVars = { [Ctx.VAR_TEXT_COLOR] = Ctx.TEXT_COLOR_DEFAULT } -- src/field_specials.c:1542
end
function Ctx.selectObject(ctx, localId)
localId = tonumber(localId) or 0
ctx.selectedLocalId = localId ~= 0 and localId or nil
ctx.selectedGfx = nil
if ctx.selectedLocalId then
local Objects = package.loaded["src.core.game3.objects"]
local obj = Objects and Objects.find and Objects.find(localId)
ctx.selectedGfx = obj and (obj.graphicsId or (obj.def and (obj.def.graphicsId or obj.def.graphics))) or nil
end
end
function Ctx.clearLocks(ctx)
@@ -68,6 +82,7 @@ end
function Ctx.haltCleanup(ctx)
Ctx.wipeSpecial(ctx)
Ctx.selectObject(ctx, 0)
Ctx.clearLocks(ctx)
Ctx.clearMoves(ctx)
ctx.messageOpen = false
+3
View File
@@ -66,6 +66,9 @@ GfxIds.MOVEMENT = {
[0x1A] = { movement = "WALK", range = "UP_DOWN" },
[0x1B] = { movement = "WALK", range = "LEFT_RIGHT" },
[0x1C] = { movement = "WALK", range = "LEFT_RIGHT" },
[0x4D] = { movement = "RAISE_HAND", range = "DOWN" },
[0x4E] = { movement = "RAISE_HAND", range = "DOWN" },
[0x4F] = { movement = "RAISE_HAND", range = "DOWN" },
}
function GfxIds.spriteFor(graphicsId)
+84
View File
@@ -18,7 +18,91 @@ local function yield_host(ctx, adapters, startFn)
return not finished -- true = caller should yield
end
local function vsSeeker()
return require("src.core.game3.vs_seeker")
end
local function flagsMod()
return require("src.core.game3.scripting.flags")
end
local function lastTalked(ctx)
return flagsMod().getVar(nil, ctx, 0x800F)
end
local function setResult(ctx, v)
flagsMod().setVar(nil, ctx, 0x800D, v)
end
Natives.ALLOW = {
-- pokefirered/src/battle_setup.c:865
["special:" .. Std.SPECIAL.Script_HasTrainerBeenFought] = function(ctx)
local Flags = flagsMod()
local fid = Flags.trainerFlagId(ctx.trainerBattleOpponentA or 0)
setResult(ctx, Flags.getFlag(vsSeeker().store(), ctx, fid) and 1 or 0)
return false
end,
-- pokefirered/src/battle_setup.c:1007
["special:" .. Std.SPECIAL.PlayTrainerEncounterMusic] = function(ctx)
if ctx.trainerBattleMode == 1 or ctx.trainerBattleMode == 8 then return false end
local Trainers = require("src.core.game3.scripting.trainers")
local song = Trainers.getEncounterMusic and Trainers.getEncounterMusic(ctx.trainerBattleOpponentA or 0)
local okA, Audio = pcall(require, "src.core.game3.audio")
if okA and Audio and Audio.playSong and song then Audio.playSong(song) end
return false
end,
-- pokefirered/src/vs_seeker.c:1013
["special:" .. Std.SPECIAL.ShouldTryRematchBattle] = function(ctx)
local VsSeeker = vsSeeker()
local ok = VsSeeker.shouldTryRematchBattle(ctx.trainerBattleOpponentA or 0, lastTalked(ctx), VsSeeker.store())
setResult(ctx, ok and 1 or 0)
return false
end,
-- pokefirered/src/vs_seeker.c:1086
["special:" .. Std.SPECIAL.IsTrainerReadyForRematch] = function(ctx)
local ok = vsSeeker().isTrainerReadyForRematch(ctx.trainerBattleOpponentA or 0, lastTalked(ctx))
setResult(ctx, ok and 1 or 0)
return false
end,
-- pokefirered/src/script_pokemon_util.c:90
["special:" .. Std.SPECIAL.HasEnoughMonsForDoubleBattle] = function(ctx)
local Party = require("src.core.game3.party")
local rt = package.loaded["src.core.game3.runtime"]
local session = rt and rt.getSession and rt.getSession()
setResult(ctx, Party.monsStateToDoubles(session and session.party))
return false
end,
-- pokefirered/src/battle_setup.c:848
["special:" .. Std.SPECIAL.SetUpTrainerMovement] = function(ctx)
local Objects = package.loaded["src.core.game3.objects"]
local lid = lastTalked(ctx)
local eo = Objects and not Objects.isPlayer(lid) and Objects.find(lid)
if eo and Objects.setTrainerMovementType then
Objects.setTrainerMovementType(eo, vsSeeker().faceTypeFor(eo.facing))
end
return false
end,
-- pokefirered/src/vs_seeker.c:636
["special:" .. Std.SPECIAL.VsSeekerResetObjectMovementAfterChargeComplete] = function()
vsSeeker().resetObjectMovementAfterChargeComplete()
return false
end,
-- pokefirered/src/vs_seeker.c:598
["special:" .. Std.SPECIAL.VsSeekerFreezeObjectsAfterChargeComplete] = function()
local Objects = package.loaded["src.core.game3.objects"]
for _, lid in ipairs(Objects and Objects._order or {}) do
local eo = Objects._byId[lid]
if eo then eo.frozen = true end
end
return false
end,
-- pokefirered/src/battle_setup.c:870
["special:" .. Std.SPECIAL.SetBattledTrainerFlag] = function(ctx)
local Flags = flagsMod()
local store = vsSeeker().store()
if store then Flags.setFlag(store, ctx, Flags.trainerFlagId(ctx.trainerBattleOpponentA or 0), true) end
return false
end,
["special:" .. Std.SPECIAL.SetUsedPkmnCenterQuestLogEvent] = function()
local rt=package.loaded["src.core.game3.runtime"]
require("src.core.game3.quest_log_recorder").event(rt and rt.getSession(),"MonsWereFullyRestoredAtCenter",{})
+51 -8
View File
@@ -433,7 +433,8 @@ local function dispatch(vm, row)
Flags.setVar(store, ctx, Ctx.VAR_RESULT, ok and 0 or 2) -- 0=party, 2=fail
return false
elseif op == "textcolor" then
Flags.setVar(store, ctx, 0x8012, row.color or row[1] or 0)
Flags.setVar(store, ctx, Ctx.VAR_PREV_TEXT_COLOR, Flags.getVar(store, ctx, Ctx.VAR_TEXT_COLOR)) -- src/scrcmd.c:1257
Flags.setVar(store, ctx, Ctx.VAR_TEXT_COLOR, row.color or row[1] or 0)
return false
elseif op == "signmsg" or op == "normalmsg" then
local Message = package.loaded["src.ui.game3.message"]
@@ -823,6 +824,24 @@ local function dispatch(vm, row)
local earlyRival = (battleType == 9) -- TRAINER_BATTLE_EARLY_RIVAL
local eventScript = row.eventScript
local trainerFlag = Flags.trainerFlagId(trainerId)
local VsSeeker = require("src.core.game3.vs_seeker")
local isRematch = op == "trainerbattle" and (battleType == 5 or battleType == 7)
local trainerLocalId = tonumber(row.localId) or 0
if op == "trainerbattle" and battleType ~= 3 and not earlyRival and trainerLocalId ~= 0 then
-- src/battle_setup.c:778
Flags.setVar(store, ctx, Ctx.VAR_LAST_TALKED, trainerLocalId)
Ctx.selectObject(ctx, trainerLocalId)
end
local lastTalked = Flags.getVar(store, ctx, Ctx.VAR_LAST_TALKED)
local opponentA = trainerId
if op == "trainerbattle" then
if isRematch then
-- pokefirered/src/battle_setup.c:814
opponentA = VsSeeker.rematchTrainerId(trainerId, store)
end
ctx.trainerBattleMode = battleType
ctx.trainerBattleOpponentA = opponentA
end
-- Remember post-battle / beaten scripts for gotopost/gotobeaten.
-- VM already advanced PC past this op → current PC is post-battle addr.
@@ -832,10 +851,14 @@ local function dispatch(vm, row)
}
ctx.trainerBattleBeatenScript = eventScript
if op == "trainerbattle" and not earlyRival and Flags.getFlag(store, ctx, trainerFlag) then
if op == "trainerbattle" and not earlyRival and not isRematch and Flags.getFlag(store, ctx, trainerFlag) then
-- Already defeated → fall through (gotopostbattlescript).
return false
end
-- pokefirered/data/scripts/trainer_battle.inc:52
if op == "trainerbattle" and isRematch and not VsSeeker.isTrainerReadyForRematch(opponentA, lastTalked) then
return false
end
local isDouble = battleType == 4 or battleType == 6 or battleType == 7 or battleType == 8
if op == "trainerbattle" and isDouble and a.startTrainerBattle then
@@ -869,19 +892,19 @@ local function dispatch(vm, row)
end
if a.startTrainerBattle then
local foe = Trainers.foeFromId(trainerId)
local foe = Trainers.foeFromId(opponentA)
if not foe then
local sp = tonumber(row.species)
if not sp or sp < 1 then sp = nil end
foe = {
species = sp or 4,
level = tonumber(row.level) or 5,
trainerId = trainerId,
trainerId = opponentA,
}
end
foe.moves = foe.moves or row.moves
local dialogs = Trainers.dialogs(trainerId) or {}
local dialogs = Trainers.dialogs(opponentA) or Trainers.dialogs(trainerId) or {}
local introText = nil
if battleType ~= 3 and battleType ~= 9 then
introText = (row.introText and resolve_text(vm, row.introText)) or dialogs.intro
@@ -915,7 +938,18 @@ local function dispatch(vm, row)
if earlyRival then
Flags.setVar(store, ctx, Ctx.VAR_RESULT, lost and 1 or 0)
end
if not lost then
-- pokefirered/src/battle_main.c:3848
VsSeeker.clearRematchStateByTrainerId(opponentA, lastTalked, store)
if isRematch then
if not lost then
-- pokefirered/src/battle_setup.c:967
local rematchFlag = Flags.trainerFlagId(opponentA)
Flags.setFlag(store, ctx, rematchFlag, true)
if a.onFlagChanged then a.onFlagChanged(rematchFlag, true) end
VsSeeker.clearRematchStateOfLastTalked(lastTalked, opponentA, store)
end
shouldHalt = true
elseif not lost then
Flags.setFlag(store, ctx, trainerFlag, true)
if a.onFlagChanged then a.onFlagChanged(trainerFlag, true) end
-- CONTINUE_SCRIPT*: gotobeatenscript after battle.
@@ -929,7 +963,7 @@ local function dispatch(vm, row)
end
done = true
end, {
trainerId = trainerId,
trainerId = opponentA,
earlyRival = earlyRival,
rivalFlags = rivalFlags,
noWhiteout = earlyRival and (rivalFlags % 2 == 1),
@@ -939,9 +973,18 @@ local function dispatch(vm, row)
})
end
if isRematch then
-- pokefirered/src/battle_setup.c:848
local Objects = package.loaded["src.core.game3.objects"]
local eo = Objects and Objects.find and Objects.find(lastTalked)
if eo and Objects.setTrainerMovementType and not (Objects.isPlayer and Objects.isPlayer(lastTalked)) then
Objects.setTrainerMovementType(eo, VsSeeker.faceTypeFor(eo.facing))
end
end
if introText and introText ~= "" and a.openMessageAsync then
-- Play trainer encounter music if not already playing (pret PlayTrainerEncounterMusic / EventScript_TryDoNormalTrainerBattle)
local song = Trainers.getEncounterMusic and Trainers.getEncounterMusic(trainerId)
local song = Trainers.getEncounterMusic and Trainers.getEncounterMusic(opponentA)
local okA, Audio = pcall(require, "src.core.game3.audio")
if okA and Audio and Audio.playSong and song then
Audio.playSong(song)
+2 -1
View File
@@ -386,7 +386,8 @@ function Space.startScript(scriptKey, localId, facing)
if localId then
return Space.vm:startTalk(scriptKey, localId, facing)
end
return Space.vm:start(scriptKey, facing)
-- src/field_control_avatar.c:200
return Space.vm:startTalk(scriptKey, 0, facing)
end
function Space.install(mod)
+23 -1
View File
@@ -40,6 +40,15 @@ Std.SPECIAL = {
HelpSystem_Disable = 0x198,
HelpSystem_Enable = 0x199,
StartMarowakBattle = 0x156, -- pokefirered/data/specials.inc:353
Script_HasTrainerBeenFought = 0x36, -- pokefirered/data/specials.inc:65
PlayTrainerEncounterMusic = 0x38, -- pokefirered/data/specials.inc:67
ShouldTryRematchBattle = 0x39, -- pokefirered/data/specials.inc:68
IsTrainerReadyForRematch = 0x3A, -- pokefirered/data/specials.inc:69
HasEnoughMonsForDoubleBattle = 0x3D, -- pokefirered/data/specials.inc:72
SetUpTrainerMovement = 0x13A, -- pokefirered/data/specials.inc:325
VsSeekerResetObjectMovementAfterChargeComplete = 0x164, -- pokefirered/data/specials.inc:367
VsSeekerFreezeObjectsAfterChargeComplete = 0x172, -- pokefirered/data/specials.inc:381
SetBattledTrainerFlag = 0x18F, -- pokefirered/data/specials.inc:410
-- Engine-extension specials (not cart indices) for shared primitives.
FadeScreen = 0xF001,
OpenNaming = 0xF002,
@@ -137,9 +146,20 @@ Std.SCRIPTS = {
{ op = "return" },
},
-- Economy / item stds (pret obtain_item.inc). Pocket name → STR_VAR_3.
["std:0"] = { -- STD_OBTAIN_ITEM
EventScript_RestorePrevTextColor = { -- data/scripts/obtain_item.inc:6
{ op = "copyvar", [1] = 0x8012, [2] = 0x8013 },
{ op = "return" },
},
["std:0"] = { -- STD_OBTAIN_ITEM, data/scripts/obtain_item.inc:10
{ op = "copyvar", [1] = 0x8013, [2] = 0x8012 },
{ op = "textcolor", color = 3, [1] = 3 },
{ op = "additem", [1] = 0x8000, [2] = 0x8001 },
{ op = "copyvar", [1] = 0x8007, [2] = 0x800D },
{ op = "call", target = "EventScript_ObtainItemMessage" },
{ op = "copyvar", [1] = 0x8012, [2] = 0x8013 },
{ op = "return" },
},
EventScript_ObtainItemMessage = {
{ op = "bufferitemname", dest = 1, src = 0x8000 }, -- STR_VAR_2
{ op = "checkitemtype", [1] = 0x8000 },
{ op = "call", target = "EventScript_BufferPocketName" },
@@ -242,6 +262,7 @@ Std.SCRIPTS = {
{ op = "return" },
},
["std:9"] = { -- STD_RECEIVED_ITEM (msgreceiveditem)
{ op = "textcolor", color = 3, [1] = 3 }, -- data/scripts/std_msgbox.inc:30
{ op = "compare_var_to_value", var = 0x8002, value = 318 }, -- MUS_OBTAIN_KEY_ITEM
{ op = "goto_if", cond = 1, target = "EventScript_ReceivedItemFanfareKeyItem" },
{ op = "compare_var_to_value", var = 0x8002, value = 258 }, -- MUS_OBTAIN_ITEM
@@ -271,6 +292,7 @@ Std.SCRIPTS = {
{ op = "waitfanfare" },
{ op = "waitmessage" },
{ op = "callstd", std = 8 }, -- STD_PUT_ITEM_AWAY
{ op = "call", target = "EventScript_RestorePrevTextColor" },
{ op = "return" },
},
}
+1
View File
@@ -78,6 +78,7 @@ end
--- Stamp LAST_TALKED and VAR_FACING then start script.
function Vm:startTalk(scriptKey, localId, facing)
Flags.setVar(self.store, self.ctx, Ctx.VAR_LAST_TALKED, localId or 0)
Ctx.selectObject(self.ctx, localId) -- src/field_control_avatar.c:426
if facing then
Flags.setVar(self.store, self.ctx, Ctx.VAR_FACING, facing)
end
+40 -14
View File
@@ -2,7 +2,7 @@
-- Features:
-- 1. Lockstep Event Queue: Prevents simultaneous tick collisions; flushes on party white-out.
-- 2. Happiness Step Counter (128 steps): +1 friendship to all party Pokémon.
-- 3. VS Seeker Battery (100 steps): Increments on all movement states (Walk, Run, Bike, Surf).
-- 3. VS Seeker Battery (100 steps): Increments while the VS SEEKER is in the bag.
-- 4. Overworld Poison (4 steps): 4-frame reddish screen flash, SE_FIELD_POISON, lethal faint at 0 HP.
-- 5. Egg Cycles & Daycare (256 steps): Decrements egg cycles -> EggHatchScene; +1 EXP per step in Daycare.
-- 6. Repel Counter: Decrements steps -> "Repel's effect wore off..." on expiration.
@@ -44,6 +44,18 @@ end
local push_event = StepEvents.queueEvent
-- pokefirered/src/metatile_behavior.c:266
local function forced_step()
local Player = package.loaded["src.core.game3.player"]
local Collision = package.loaded["src.core.game3.collision"]
if not (Player and Collision and Collision.behavior) then return false end
local ok, mb = pcall(Collision.behavior, Player.cellX, Player.cellY)
mb = ok and tonumber(mb) or nil
if not mb then return false end
return (mb >= 0x40 and mb <= 0x48) or (mb >= 0x50 and mb <= 0x53)
or mb == 0x13 or mb == 0x23 or (mb >= 0x54 and mb <= 0x57)
end
local function party_is_wiped(party)
if not party or #party == 0 then return false end
local hasAlive = false
@@ -73,8 +85,8 @@ local function trigger_white_out(session, game)
local playerName = (session and (session.name or session.playerName)) or "PLAYER"
local msg = playerName .. " is out of usable\nPOKéMON!\n\n" .. playerName .. " whited out!"
Hud.showDialogue(msg, {
onDone = function()
Hud.openMessage(game, msg, {
done = function()
-- 2. Fade to black and warp to last heal location (or Pallet Town player house)
Fade.begin(Fade.MODE.TO_BLACK, 0.5, function()
local healMap = (session and session.healMap) or "FR_PALLET_TOWN_PLAYERS_HOUSE_2F"
@@ -133,12 +145,18 @@ function StepEvents.onStepTaken(session, game)
session.vars[0x403F] = hapSteps
session.happinessSteps = hapSteps
-- 2. VS Seeker Battery Counter (up to 100 steps on any movement mode)
local vsCharge = tonumber(session.vsSeekerCharge or session.vars[0x4044]) or 0
if vsCharge < 100 then
vsCharge = math.min(100, vsCharge + 1)
session.vsSeekerCharge = vsCharge
session.vars[0x4044] = vsCharge
-- pokefirered/src/field_control_avatar.c:658
local vsChargeDone = false
if not forced_step() then
local VsSeeker = require("src.core.game3.vs_seeker")
if VsSeeker.onStep(session) then
vsChargeDone = true
push_event(VsSeeker.chargingDoneEvent())
end
end
if vsChargeDone then
StepEvents.onRepelStep(session, game)
return
end
-- 3. Overworld Poison Counter (every 4 steps, pret field_poison.c)
@@ -188,8 +206,8 @@ function StepEvents.onStepTaken(session, game)
end)
local Hud = require("src.ui.game3.hud")
Hud.showDialogue(fainted.name .. " fainted...", {
onDone = function()
Hud.openMessage(game, fainted.name .. " fainted...", {
done = function()
if party_is_wiped(party) then
trigger_white_out(session, game)
else
@@ -242,6 +260,10 @@ function StepEvents.onStepTaken(session, game)
end
session.eggSteps = eggSteps
StepEvents.onRepelStep(session, game)
end
function StepEvents.onRepelStep(session, game)
-- 5. Repel Step Counter (VAR_REPEL_STEP_COUNT)
local repelSteps = tonumber(session.repelSteps or session.vars[0x4021]) or 0
if repelSteps > 0 then
@@ -255,8 +277,8 @@ function StepEvents.onStepTaken(session, game)
run = function(onDone)
se(67) -- SE_REPEL
local Hud = require("src.ui.game3.hud")
Hud.showDialogue("Repel's effect wore off...", {
onDone = onDone,
Hud.openMessage(game, "Repel's effect wore off...", {
done = onDone,
})
end,
})
@@ -270,7 +292,11 @@ function StepEvents.update(dt, game)
StepEvents._poisonFlashTimer = math.max(0, StepEvents._poisonFlashTimer - (dt or 1 / 60))
end
if StepEvents._activeEvent then return end
if StepEvents._activeEvent then
local active = StepEvents._activeEvent
if active.tick then active.tick(dt, game) end
return
end
if #StepEvents._queue == 0 then return end
local ev = table.remove(StepEvents._queue, 1)
+603 -81
View File
@@ -1,12 +1,70 @@
-- VS Seeker Overworld Radar & Battery Engine (pret vs_seeker.c).
-- Features:
-- 1. Battery charge management: 100 steps on any locomotion mode (Walk, Run, Bike, Surf).
-- 2. Hard indoor/cave validation rejection: Preserves 100 charge.
-- 3. Outdoor radar ping: Emits signal, drains battery to 0 (even on "no response").
-- 4. Trainer detection (8-tile radius) with animated '!!' alert emoticons and rematch party progression.
local Data = require("src.core.game3.vs_seeker_data")
local Flags = require("src.core.game3.scripting.flags")
local Rng = require("src.core.game3.rng")
local VsSeeker = {}
VsSeeker.ITEM_VS_SEEKER = 374
VsSeeker.MAX_CHARGE = 100
VsSeeker.FLAG_SYS_VS_SEEKER_CHARGING = 0x801
-- src/vs_seeker.c:973
VsSeeker.TIER_FLAGS = { 0x292, 0x896, 0x897, 0x82C, 0x844 }
VsSeeker.MOVEMENT_TYPE_FACE_UP = 0x07
VsSeeker.MOVEMENT_TYPE_FACE_DOWN = 0x08
VsSeeker.MOVEMENT_TYPE_FACE_LEFT = 0x09
VsSeeker.MOVEMENT_TYPE_FACE_RIGHT = 0x0A
VsSeeker.MOVEMENT_TYPE_RAISE_HAND_AND_STOP = 0x4D
VsSeeker.MOVEMENT_TYPE_RAISE_HAND_AND_JUMP = 0x4E
VsSeeker.MOVEMENT_TYPE_RAISE_HAND_AND_SWIM = 0x4F
VsSeeker.NOT_CHARGED = 0
VsSeeker.NO_ONE_IN_RANGE = 1
VsSeeker.CAN_USE = 2
VsSeeker.RESPONSE_NO_RESPONSE = 0
VsSeeker.RESPONSE_UNFOUGHT_TRAINERS = 1
VsSeeker.RESPONSE_FOUND_REMATCHES = 2
local RESP_RAND, RESP_NO, RESP_YES = 0, 1, 2
local SE_PIN = 21
local SE_CONTEST_MONS_TURN = 94
VsSeeker.EFFECT_FRAMES = 89
VsSeeker.WAIT_FRAMES = 48
-- src/vs_seeker.c:565
VsSeeker.MOVEMENT_UNFOUGHT = { 0x62, 0xFE }
VsSeeker.MOVEMENT_NO_REMATCH = { 0x64, 0xFE }
VsSeeker.MOVEMENT_REMATCH = { 0x2D, 0x65, 0xFE }
-- data/text/trainers.inc:1
VsSeeker.TEXT = {
notCharged = "The battery isn't charged enough.\fNo. of steps required to fully\ncharge the battery: %d",
noTrainers = "There are no TRAINERS within range\nwho can battle…\fThe VS SEEKER was turned off.",
notReady = "The other TRAINERS don't appear\nto be ready for battle.\fLet's wait till later.",
}
-- src/item_use.c:712
VsSeeker.EXCLUDED_PRET_MAPS = { "ViridianForest", "MtEmber_Exterior", "ThreeIsland_BerryForest", "SixIsland_PatternBush" }
local RAISE_HAND = {
[0x4D] = true,
[0x4E] = true,
[0x4F] = true,
}
-- src/vs_seeker.c:1132
local RAISE_HAND_AND_JUMP_GFX = {
[17] = true, [18] = true, [19] = true, [20] = true, [22] = true, [23] = true,
[24] = true, [25] = true, [26] = true, [28] = true, [29] = true, [30] = true,
[37] = true, [39] = true, [40] = true, [41] = true, [42] = true, [45] = true,
[46] = true, [54] = true, [56] = true, [62] = true,
}
local RAISE_HAND_AND_SWIM_GFX = { [36] = true, [43] = true, [44] = true }
local FACE_TYPE_BY_FACING = { up = 0x07, down = 0x08, left = 0x09, right = 0x0A }
local function se(id)
pcall(function()
local Audio = require("src.core.game3.audio")
@@ -14,97 +72,561 @@ local function se(id)
end)
end
local function runtimeSession()
local Runtime = package.loaded["src.core.game3.runtime"]
return Runtime and Runtime.getSession and Runtime.getSession() or nil
end
local function objectsMod()
return package.loaded["src.core.game3.objects"] or require("src.core.game3.objects")
end
function VsSeeker.isRaiseHand(mt)
return RAISE_HAND[tonumber(mt) or -1] == true
end
function VsSeeker.faceTypeFor(facing)
return FACE_TYPE_BY_FACING[facing or "down"] or VsSeeker.MOVEMENT_TYPE_FACE_DOWN
end
function VsSeeker.state(session)
session = session or runtimeSession()
if not session then return { steps = 0, charging = 0, rematches = {} } end
local s = session.vsSeeker
if type(s) ~= "table" then
s = {}
session.vsSeeker = s
end
s.steps = tonumber(s.steps) or 0
s.charging = tonumber(s.charging) or 0
if type(s.rematches) ~= "table" then s.rematches = {} end
return s
end
function VsSeeker.store(session)
local Space = package.loaded["src.core.game3.scripting.space"]
if Space and Space.store then return Space.store end
session = session or runtimeSession()
if session then
session.flags = session.flags or {}
session.vars = session.vars or {}
return { flags = session.flags, vars = session.vars }
end
return nil
end
local function getFlag(id, st)
return Flags.getFlag(st or VsSeeker.store(), nil, id)
end
local function setFlag(id, on, st)
st = st or VsSeeker.store()
if st then Flags.setFlag(st, nil, id, on) end
end
local function fought(trainerId, st)
return getFlag(Flags.trainerFlagId(trainerId), st)
end
function VsSeeker.getRematch(s, localId)
local v = s.rematches[tonumber(localId) or -1] or s.rematches[tostring(localId)]
return tonumber(v) or 0
end
local function setRematch(s, localId, v)
localId = tonumber(localId) or 0
s.rematches[tostring(localId)] = nil
s.rematches[localId] = (v and v ~= 0) and v or nil
end
function VsSeeker.getBattery(session)
if not session then return 0 end
return tonumber(session.vsSeekerCharge or (session.vars and session.vars[0x4044])) or 0
return VsSeeker.state(session).steps
end
function VsSeeker.setBattery(session, charge)
if not session then return end
charge = math.min(100, math.max(0, math.floor(tonumber(charge) or 0)))
session.vsSeekerCharge = charge
if session.vars then session.vars[0x4044] = charge end
local s = VsSeeker.state(session)
s.steps = math.min(VsSeeker.MAX_CHARGE, math.max(0, math.floor(tonumber(charge) or 0)))
end
local function is_valid_map(mapId)
mapId = tostring(mapId or ""):upper()
-- Caves, gyms, buildings, and indoor rooms reject VS Seeker
if mapId:find("_HOUSE") or mapId:find("_POKECENTER") or mapId:find("_MART")
or mapId:find("_GYM") or mapId:find("_CAVE") or mapId:find("_TUNNEL")
or mapId:find("MT_MOON") or mapId:find("VIRIDIAN_FOREST")
or mapId:find("ROCK_TUNNEL") or mapId:find("SEAFOAM")
or mapId:find("VICTORY_ROAD") or mapId:find("POKEMON_MANSION")
or mapId:find("POKEMON_TOWER") or mapId:find("POWER_PLANT")
or mapId:find("SAFARI_ZONE") or mapId:find("_1F") or mapId:find("_2F") or mapId:find("_3F")
or mapId:find("_B1F") or mapId:find("_B2F") then
return false
-- src/vs_seeker.c:1209
function VsSeeker.clearAllRematchStates(session)
VsSeeker.state(session).rematches = {}
end
-- src/vs_seeker.c:664
function VsSeeker.onStep(session, st)
local s = VsSeeker.state(session)
local Bag = require("src.core.game3.bag")
if session and session.bag and Bag.has(session.bag, VsSeeker.ITEM_VS_SEEKER, 1) then
if s.steps < VsSeeker.MAX_CHARGE then s.steps = s.steps + 1 end
end
st = st or VsSeeker.store(session)
if getFlag(VsSeeker.FLAG_SYS_VS_SEEKER_CHARGING, st) then
if s.charging < VsSeeker.MAX_CHARGE then s.charging = s.charging + 1 end
if s.charging == VsSeeker.MAX_CHARGE then
setFlag(VsSeeker.FLAG_SYS_VS_SEEKER_CHARGING, false, st)
s.charging = 0
s.rematches = {}
return true
end
end
return false
end
-- src/vs_seeker.c:1235
function VsSeeker.nextAvailable(trainerId, st)
local ri = Data.byBase[tonumber(trainerId) or -1]
if not ri then return 0, nil end
local row = Data.REMATCHES[ri]
for j = 1, Data.MAX_REMATCH_PARTIES - 1 do
local id = row[j + 1]
if id == nil or id == Data.TRAINER_NONE then return j - 1, ri end
if id ~= Data.SKIP and not fought(id, st) then return j, ri end
end
return Data.MAX_REMATCH_PARTIES - 1, ri
end
-- src/vs_seeker.c:1002
local function stepDownTier(row, j)
j = j - 1
while j ~= 0 do
if row[j + 1] ~= Data.SKIP then return j end
j = j - 1
end
return 0
end
-- src/vs_seeker.c:1075
function VsSeeker.rematchTrainerId(trainerId, st)
local j, ri = VsSeeker.nextAvailable(trainerId, st)
if j == 0 then return 0 end
local row = Data.REMATCHES[ri]
local tier = VsSeeker.TIER_FLAGS[j]
if tier and not getFlag(tier, st) then
j = stepDownTier(row, j)
end
return row[j + 1] or 0
end
-- src/vs_seeker.c:1013
function VsSeeker.shouldTryRematchBattle(opponentA, lastTalked, st, session)
local ri = Data.byBase[tonumber(opponentA) or -1]
if not ri then return false end
if VsSeeker.getRematch(VsSeeker.state(session), lastTalked) ~= 0 then return true end
return fought(Data.REMATCHES[ri][1], st)
end
-- src/vs_seeker.c:1086
function VsSeeker.isTrainerReadyForRematch(opponentA, lastTalked, session)
if not Data.byAny[tonumber(opponentA) or -1] then return false end
return VsSeeker.getRematch(VsSeeker.state(session), lastTalked) ~= 0
end
-- src/vs_seeker.c:1047
function VsSeeker.clearRematchStateOfLastTalked(lastTalked, opponentA, st, session)
setRematch(VsSeeker.state(session), lastTalked, 0)
setFlag(Flags.trainerFlagId(opponentA), true, st)
end
-- src/vs_seeker.c:1113
function VsSeeker.randomFaceType()
local r = Rng.Random() % 4
if r == 0 then return VsSeeker.MOVEMENT_TYPE_FACE_UP end
if r == 2 then return VsSeeker.MOVEMENT_TYPE_FACE_LEFT end
if r == 3 then return VsSeeker.MOVEMENT_TYPE_FACE_RIGHT end
return VsSeeker.MOVEMENT_TYPE_FACE_DOWN
end
function VsSeeker.runningBehavior(graphicsId)
graphicsId = tonumber(graphicsId) or -1
if RAISE_HAND_AND_JUMP_GFX[graphicsId] then return VsSeeker.MOVEMENT_TYPE_RAISE_HAND_AND_JUMP end
if RAISE_HAND_AND_SWIM_GFX[graphicsId] then return VsSeeker.MOVEMENT_TYPE_RAISE_HAND_AND_SWIM end
return VsSeeker.MOVEMENT_TYPE_RAISE_HAND_AND_STOP
end
local function templateTrainerId(def, eo)
if def and def.trainerId then return tonumber(def.trainerId) end
local okT, TrainerSight = pcall(require, "src.core.game3.trainer_sight")
if not okT then return nil end
return TrainerSight.getTrainerId(eo or { def = def, scriptKey = def and def.scriptKey })
end
local function isSane(eo)
return eo ~= nil and eo.visible ~= false and not eo.hidden
end
local function trainerTemplates(Objects)
local out = {}
for _, def in ipairs(Objects._defs or {}) do
local tt = tonumber(def.trainerType) or 0
local lid = tonumber(def.localId or def.index) or 0
if (tt == 1 or tt == 3) and lid > 0 then
out[#out + 1] = { def = def, localId = lid, eo = Objects._byId and Objects._byId[lid] }
end
end
return out
end
-- src/vs_seeker.c:806
function VsSeeker.gather(Objects)
Objects = Objects or objectsMod()
local infos = {}
for _, t in ipairs(trainerTemplates(Objects)) do
local eo = t.eo
infos[#infos + 1] = {
localId = t.localId,
trainerIdx = templateTrainerId(t.def, eo) or 0,
x = eo and eo.cellX or 0,
y = eo and eo.cellY or 0,
graphicsId = tonumber(t.def.graphicsId or t.def.graphics) or 0,
sane = isSane(eo),
}
end
return infos
end
-- src/vs_seeker.c:1217
function VsSeeker.isVisible(info, px, py)
if not info.sane then return false end
return math.abs((info.x or 0) - px) <= 7 and math.abs((info.y or 0) - py) <= 5
end
-- src/vs_seeker.c:852
function VsSeeker.canUse(infos, px, py, s, st)
if s.steps ~= VsSeeker.MAX_CHARGE then
return VsSeeker.NOT_CHARGED, VsSeeker.MAX_CHARGE - s.steps
end
for _, info in ipairs(infos) do
if VsSeeker.isVisible(info, px, py) then
if not fought(info.trainerIdx, st) or VsSeeker.nextAvailable(info.trainerIdx, st) ~= 0 then
return VsSeeker.CAN_USE
end
end
end
return VsSeeker.NO_ONE_IN_RANGE
end
-- src/vs_seeker.c:1285
local function curResponse(infos, idx, trainerIdx, responders, px, py)
for i = 1, idx - 1 do
local other = infos[i]
if VsSeeker.isVisible(other, px, py) and other.trainerIdx == trainerIdx then
for _, r in ipairs(responders) do
if r.trainerIdx == other.trainerIdx then return RESP_YES end
end
return RESP_NO
end
end
return RESP_RAND
end
-- src/vs_seeker.c:869
function VsSeeker.computeResponse(infos, px, py, s, st)
local actions, responders = {}, {}
local unfought, wants = false, false
for i, info in ipairs(infos) do
if VsSeeker.isVisible(info, px, py) then
local tid = info.trainerIdx
if not fought(tid, st) then
actions[#actions + 1] = { localId = info.localId, movement = VsSeeker.MOVEMENT_UNFOUGHT }
unfought = true
else
local j = VsSeeker.nextAvailable(tid, st)
if j == 0 then
actions[#actions + 1] = { localId = info.localId, movement = VsSeeker.MOVEMENT_NO_REMATCH }
else
local rval = Rng.Random() % 100
local resp = curResponse(infos, i, tid, responders, px, py)
if resp == RESP_YES then
rval = 100
elseif resp == RESP_NO then
rval = 0
end
if rval < 30 then
actions[#actions + 1] = { localId = info.localId, movement = VsSeeker.MOVEMENT_NO_REMATCH }
else
setRematch(s, info.localId, j)
actions[#actions + 1] = { localId = info.localId, movement = VsSeeker.MOVEMENT_REMATCH, rematch = true }
responders[#responders + 1] = { trainerIdx = tid, behavior = VsSeeker.runningBehavior(info.graphicsId) }
wants = true
end
end
end
end
end
if wants then
se(SE_PIN)
setFlag(VsSeeker.FLAG_SYS_VS_SEEKER_CHARGING, true, st)
s.charging = 0
return VsSeeker.RESPONSE_FOUND_REMATCHES, actions, responders
end
if unfought then
return VsSeeker.RESPONSE_UNFOUGHT_TRAINERS, actions, responders
end
return VsSeeker.RESPONSE_NO_RESPONSE, actions, responders
end
-- src/vs_seeker.c:1305
function VsSeeker.startAllRespondantIdleMovements(infos, responders, s, st, Objects)
Objects = Objects or objectsMod()
for _, r in ipairs(responders) do
for _, info in ipairs(infos) do
if info.trainerIdx == r.trainerIdx then
if info.sane and Objects.setTrainerMovementType then
Objects.setTrainerMovementType(info.localId, r.behavior)
end
if Objects.overrideTemplateMovementType then
Objects.overrideTemplateMovementType(info.localId, r.behavior)
end
setRematch(s, info.localId, (VsSeeker.nextAvailable(info.trainerIdx, st)))
end
end
end
end
-- src/vs_seeker.c:636
function VsSeeker.resetObjectMovementAfterChargeComplete(Objects)
Objects = Objects or objectsMod()
for _, t in ipairs(trainerTemplates(Objects)) do
local mt = Objects.templateMovementType and Objects.templateMovementType(t.localId)
or tonumber(t.def.movementType)
if VsSeeker.isRaiseHand(mt) then
local face = VsSeeker.randomFaceType()
if isSane(t.eo) and Objects.setTrainerMovementType then
Objects.setTrainerMovementType(t.localId, face)
end
if Objects.overrideTemplateMovementType then
Objects.overrideTemplateMovementType(t.localId, face)
end
end
end
end
-- src/vs_seeker.c:693
function VsSeeker.mapReset(session, st, Objects)
local s = VsSeeker.state(session)
setFlag(VsSeeker.FLAG_SYS_VS_SEEKER_CHARGING, false, st or VsSeeker.store(session))
s.charging = 0
s.rematches = {}
Objects = Objects or package.loaded["src.core.game3.objects"]
if not Objects then return end
for _, lid in ipairs(Objects._order or {}) do
local eo = Objects._byId and Objects._byId[lid]
if eo and VsSeeker.isRaiseHand(eo.movementType) then
local face = VsSeeker.randomFaceType()
if Objects.setTrainerMovementType then
Objects.setTrainerMovementType(lid, face)
end
end
end
end
-- src/vs_seeker.c:937
function VsSeeker.clearRematchStateByTrainerId(opponentA, selectedLocalId, st, session, Objects)
local ri = Data.byAny[tonumber(opponentA) or -1]
if not ri then return end
Objects = Objects or package.loaded["src.core.game3.objects"]
if not Objects then return end
local s = VsSeeker.state(session)
selectedLocalId = tonumber(selectedLocalId) or -1
for _, t in ipairs(trainerTemplates(Objects)) do
local tid = templateTrainerId(t.def, t.eo)
if tid and Data.byAny[tid] == ri then
Rng.Random()
local eo = t.eo
local faceType = VsSeeker.faceTypeFor(eo and eo.facing)
if Objects.overrideTemplateMovementType then
Objects.overrideTemplateMovementType(t.localId, faceType)
end
setRematch(s, t.localId, 0)
if eo and Objects.setTrainerMovementType then
Objects.setTrainerMovementType(t.localId,
t.localId == selectedLocalId and faceType or VsSeeker.MOVEMENT_TYPE_FACE_DOWN)
end
end
end
end
function VsSeeker.mapAllowed(mapId, mapType)
mapType = tonumber(mapType) or 0
if mapType ~= 1 and mapType ~= 2 and mapType ~= 3 then return false end
local okC, MapCatalog = pcall(require, "src.import.gba.map_catalog")
for _, pret in ipairs(VsSeeker.EXCLUDED_PRET_MAPS) do
if mapId == pret then return false end
if okC and MapCatalog.pretToEngine and MapCatalog.pretToEngine(pret) == mapId then return false end
end
return true
end
--- Use the VS Seeker in the overworld.
function VsSeeker.use(session, game, onDone)
local Hud = require("src.ui.game3.hud")
local currentMap = (session and session.map) or ""
-- 1. Location check
if not is_valid_map(currentMap) then
Hud.showDialogue("This can't be used here.", { onDone = onDone })
return false
end
-- 2. Battery check
local battery = VsSeeker.getBattery(session)
if battery < 100 then
local needed = 100 - battery
local msg = string.format("The battery has run down!\nNo. of steps to charge: %d", needed)
Hud.showDialogue(msg, { onDone = onDone })
return false
end
-- 3. Battery drain on valid search signal
VsSeeker.setBattery(session, 0)
se(43) -- SE_PIN
-- 4. Scan nearby NPC trainers in 8-tile radius
local Objects = require("src.core.game3.objects")
local Player = require("src.core.game3.player")
local px, py = Player.cellX, Player.cellY
local rematchTrainers = {}
for _, obj in ipairs(Objects.all and Objects.all() or {}) do
local def = obj.def or {}
local isTrainer = def.trainerType ~= nil and def.trainerType > 0
if isTrainer then
local dx = math.abs((obj.x or 0) - px)
local dy = math.abs((obj.y or 0) - py)
if dx <= 8 and dy <= 8 then
rematchTrainers[#rematchTrainers + 1] = obj
end
function VsSeeker.currentMapType(session)
local mapId = session and session.map
local Map = package.loaded["src.core.game3.map"]
local def = Map and Map.currentDef and (Map.current == nil or Map.current == mapId) and Map.currentDef() or nil
if not (def and def.mapType) and mapId then
local okD, Dataset = pcall(require, "src.core.game3.dataset")
if okD and Dataset and Dataset.map then
local okM, d = pcall(Dataset.map, mapId)
if okM and d then def = d end
end
end
return def and tonumber(def.mapType) or 0
end
if #rematchTrainers > 0 then
-- Trigger rematch alert emoticons
pcall(function()
local Audio = require("src.core.game3.audio")
if Audio and Audio.playSong then Audio.playSong(299, { loop = false }) end -- MUS_VS_SEEKER_SEARCH
end)
-- src/item_use.c:712
function VsSeeker.canUseHere(session)
return VsSeeker.mapAllowed(session and session.map, VsSeeker.currentMapType(session))
end
local FieldEffects = require("src.core.game3.field_effects")
for _, tr in ipairs(rematchTrainers) do
if FieldEffects and FieldEffects.spawnEmoticon then
FieldEffects.spawnEmoticon(tr, 1) -- Double Exclamation / Alert
end
tr._rematchReady = true
end
-- src/strings.c:188
function VsSeeker.notTimeText(session)
local name = tostring((session and (session.name or session.playerName)) or "RED")
return "OAK: " .. name .. "!\nThis isn't the time to use that!"
end
if onDone then onDone(true, #rematchTrainers) end
return true
else
Hud.showDialogue("There is no response...", { onDone = onDone })
return false
local function fieldLock(on)
local Field = package.loaded["src.core.game3.field"]
if not Field then return end
if on then Field.lock() else Field.unlock() end
end
local function showMessage(game, text, done)
fieldLock(true)
local Hud = package.loaded["src.ui.game3.hud"] or require("src.ui.game3.hud")
Hud.openMessage(game, text, {
done = function()
fieldLock(false)
if done then done() end
end,
})
end
local function playerCoords()
local Player = package.loaded["src.core.game3.player"]
if not Player then return 0, 0 end
if Player.moving and Player.targetX then return Player.targetX, Player.targetY end
return Player.cellX or 0, Player.cellY or 0
end
local function freezeAll(Objects)
for _, lid in ipairs(Objects._order or {}) do
local eo = Objects._byId[lid]
if eo then eo.frozen = true end
end
end
local function unfreezeAll(Objects)
for _, lid in ipairs(Objects._order or {}) do
local eo = Objects._byId[lid]
if eo and not eo.scriptBusy then eo.frozen = false end
end
end
local function queue(ev)
local StepEvents = package.loaded["src.core.game3.step_events"] or require("src.core.game3.step_events")
StepEvents.queueEvent(ev)
end
-- src/vs_seeker.c:745
function VsSeeker.use(session, game, onDone)
session = session or runtimeSession()
local st = VsSeeker.store(session)
local s = VsSeeker.state(session)
local Objects = objectsMod()
local infos = VsSeeker.gather(Objects)
local px, py = playerCoords()
local code, need = VsSeeker.canUse(infos, px, py, s, st)
if code == VsSeeker.NOT_CHARGED then
showMessage(game, string.format(VsSeeker.TEXT.notCharged, need), onDone)
return false, code
elseif code == VsSeeker.NO_ONE_IN_RANGE then
showMessage(game, VsSeeker.TEXT.noTrainers, onDone)
return false, code
end
pcall(function()
local Items = require("src.core.game3.items")
require("src.core.game3.quest_log_recorder").event(session, "UsedTheItem",
{ Items.displayName(VsSeeker.ITEM_VS_SEEKER) })
end)
local seq = { t = 0, finished = false }
local function finish()
seq.finished = true
unfreezeAll(Objects)
fieldLock(false)
if seq.evDone then seq.evDone() end
if onDone then onDone(true, seq.response) end
end
queue({
type = "vs_seeker",
run = function(evDone)
seq.evDone = evDone
fieldLock(true)
freezeAll(Objects)
local Player = package.loaded["src.core.game3.player"]
if Player and Player.startFieldMove then Player.startFieldMove(VsSeeker.EFFECT_FRAMES) end
end,
tick = function()
if seq.finished or seq.waitingText then return end
seq.t = seq.t + 1
local t = seq.t
if t == 31 or t == 42 then se(SE_CONTEST_MONS_TURN) end
if t == VsSeeker.EFFECT_FRAMES then
s.steps = 0
local ppx, ppy = playerCoords()
local response, actions, responders = VsSeeker.computeResponse(infos, ppx, ppy, s, st)
seq.response, seq.responders = response, responders
for _, a in ipairs(actions) do
local eo = Objects._byId[a.localId]
if eo then eo.frozen = false end
Objects.applyMovement(a.localId, a.movement, function()
if seq.finished and eo and not eo.scriptBusy then eo.frozen = false end
end)
end
seq.waitUntil = t + VsSeeker.WAIT_FRAMES
end
if seq.waitUntil and t >= seq.waitUntil then
if seq.response == VsSeeker.RESPONSE_NO_RESPONSE then
seq.waitingText = true
local Hud = package.loaded["src.ui.game3.hud"] or require("src.ui.game3.hud")
Hud.openMessage(game, VsSeeker.TEXT.notReady, { done = finish })
else
if seq.response == VsSeeker.RESPONSE_FOUND_REMATCHES then
VsSeeker.startAllRespondantIdleMovements(infos, seq.responders, s, st, Objects)
end
finish()
end
end
end,
})
return true, code
end
-- data/event_scripts.s:1228
function VsSeeker.chargingDoneEvent()
local Objects = objectsMod()
local ev = { type = "vs_seeker_charged" }
ev.run = function(done)
ev.done = done
end
ev.tick = function()
if not ev.done then return end
local Player = package.loaded["src.core.game3.player"]
if Player and Player.moving then return end
for _, lid in ipairs(Objects._order or {}) do
local eo = Objects._byId[lid]
if eo and (eo.moving or (eo.raiseY or 0) ~= 0) then return end
end
if Objects.hasActiveTracks and Objects.hasActiveTracks() then return end
freezeAll(Objects)
VsSeeker.resetObjectMovementAfterChargeComplete(Objects)
unfreezeAll(Objects)
local cb = ev.done
ev.done = nil
cb()
end
return ev
end
return VsSeeker
+245
View File
@@ -0,0 +1,245 @@
local SKIP = 0xFFFF
local Data = {}
Data.SKIP = SKIP
Data.TRAINER_NONE = 0
Data.MAX_REMATCH_PARTIES = 6
-- src/vs_seeker.c:123
Data.REMATCHES = {
{ 89, 101, SKIP, 498, 499 },
{ 90, 90 },
{ 105, 729, SKIP, 730, SKIP, 731 },
{ 106, 106 },
{ 107, 107 },
{ 116, 507, SKIP, 508 },
{ 117, 117 },
{ 118, 118 },
{ 119, 119 },
{ 92, 503, SKIP, 504, SKIP, 505 },
{ 110, 110 },
{ 122, 501, SKIP, 502 },
{ 123, 123 },
{ 143, 143 },
{ 144, 144 },
{ 93, 93 },
{ 94, 94 },
{ 95, 500, SKIP, 506, 509 },
{ 153, 621, SKIP, 622, 623 },
{ 125, 125 },
{ 182, SKIP, 510 },
{ 183, 183 },
{ 184, 184 },
{ 471, 471 },
{ 111, 111 },
{ 112, 112 },
{ 145, 624, SKIP, 625, SKIP, 626 },
{ 146, 627, SKIP, 628, SKIP, 629 },
{ 151, 151 },
{ 152, 630, SKIP, 631, 632 },
{ 97, 97 },
{ 98, 98 },
{ 99, SKIP, 633, SKIP, 634 },
{ 100, 100 },
{ 221, 221 },
{ 222, SKIP, SKIP, 635 },
{ 258, 258 },
{ 259, 259 },
{ 260, 260 },
{ 261, SKIP, SKIP, 636 },
{ 114, 114 },
{ 115, 115 },
{ 148, SKIP, 637, 638, SKIP, 639 },
{ 149, 149 },
{ 154, SKIP, 640, 641, SKIP, 642 },
{ 155, 155 },
{ 185, 185 },
{ 186, 186 },
{ 465, SKIP, SKIP, 643 },
{ 156, 156 },
{ 157, 157 },
{ 162, SKIP, SKIP, 594, SKIP, 644 },
{ 163, SKIP, SKIP, 645, SKIP, 646 },
{ 187, 187 },
{ 188, SKIP, SKIP, 647 },
{ 128, 128 },
{ 129, 129 },
{ 130, SKIP, 648, SKIP, 649 },
{ 131, 131 },
{ 171, 171 },
{ 172, SKIP, SKIP, 650 },
{ 173, 173 },
{ 262, 262 },
{ 264, SKIP, SKIP, 651 },
{ 484, SKIP, SKIP, 533 },
{ 535, 535 },
{ 536, SKIP, SKIP, 652 },
{ 225, 225 },
{ 226, 226 },
{ 227, 227 },
{ 228, SKIP, SKIP, 653 },
{ 233, 233 },
{ 285, SKIP, SKIP, 654 },
{ 477, 477 },
{ 486, SKIP, SKIP, 706, SKIP, 707 },
{ 195, 195 },
{ 268, 268 },
{ 269, SKIP, SKIP, SKIP, 655 },
{ 300, 300 },
{ 301, 301 },
{ 302, SKIP, SKIP, 656, 657 },
{ 466, 466 },
{ 467, SKIP, SKIP, 658, 659, 660 },
{ 468, 468 },
{ 469, 469 },
{ 196, 196 },
{ 207, SKIP, SKIP, SKIP, 661 },
{ 208, 208 },
{ 209, 209 },
{ 303, 303 },
{ 304, SKIP, SKIP, 662, 663 },
{ 313, 313 },
{ 314, 314 },
{ 315, 315 },
{ 316, SKIP, SKIP, 664, 665 },
{ 487, 487 },
{ 197, 197 },
{ 198, 198 },
{ 273, SKIP, SKIP, SKIP, 666 },
{ 274, 274 },
{ 305, 305 },
{ 306, SKIP, SKIP, 667, 668 },
{ 478, 478 },
{ 479, 479 },
{ 480, SKIP, SKIP, 669, 670, 671 },
{ 481, 481 },
{ 488, SKIP, SKIP, 672, 673, 674 },
{ 199, 199 },
{ 201, 201 },
{ 202, SKIP, SKIP, SKIP, 675 },
{ 249, 249 },
{ 250, 250 },
{ 251, SKIP, SKIP, SKIP, 676 },
{ 489, 489 },
{ 203, 203 },
{ 204, 204 },
{ 205, SKIP, SKIP, SKIP, 677 },
{ 206, 206 },
{ 252, 252 },
{ 253, SKIP, SKIP, SKIP, 678 },
{ 254, 254 },
{ 255, 255 },
{ 256, SKIP, SKIP, SKIP, 679 },
{ 470, 470 },
{ 307, 307 },
{ 308, 308 },
{ 309, SKIP, SKIP, 680, 681 },
{ 235, 235 },
{ 236, 236 },
{ 237, SKIP, SKIP, SKIP, 732 },
{ 238, 238 },
{ 239, 239 },
{ 240, SKIP, SKIP, SKIP, 733 },
{ 241, 241 },
{ 276, 276 },
{ 277, SKIP, SKIP, SKIP, 682 },
{ 278, 278 },
{ 490, 490 },
{ 242, 242 },
{ 243, 243 },
{ 244, SKIP, SKIP, SKIP, SKIP, 683 },
{ 270, 270 },
{ 271, 271 },
{ 272, SKIP, SKIP, SKIP, 734 },
{ 279, 279 },
{ 310, 310 },
{ 472, SKIP, SKIP, SKIP, 684, 685 },
{ 473, 473 },
{ 229, 229 },
{ 230, 230 },
{ 231, SKIP, SKIP, SKIP, 686 },
{ 232, 232 },
{ 245, 245 },
{ 246, SKIP, SKIP, SKIP, 687 },
{ 247, 247 },
{ 248, 248 },
{ 491, SKIP, SKIP, SKIP, 688, 689 },
{ 547, 547 },
{ 548, 548 },
{ 549, SKIP, SKIP, SKIP, 690 },
{ 550, 550 },
{ 551, 551 },
{ 518, SKIP, SKIP, SKIP, 691, 692 },
{ 552, SKIP, SKIP, SKIP, 693, 694 },
{ 553, SKIP, SKIP, SKIP, 695, 696 },
{ 554, SKIP, SKIP, SKIP, 697, 698 },
{ 555, 555 },
{ 556, 556 },
{ 557, SKIP, SKIP, SKIP, 699, 700 },
{ 546, 546 },
{ 523, 523 },
{ 558, 558 },
{ 519, SKIP, SKIP, SKIP, 701 },
{ 559, 559 },
{ 561, 561 },
{ 560, SKIP, SKIP, SKIP, 702 },
{ 526, 526 },
{ 562, 562 },
{ 563, SKIP, SKIP, SKIP, SKIP, 703 },
{ 525, 525 },
{ 564, 564 },
{ 565, SKIP, SKIP, SKIP, SKIP, 704 },
{ 566, 566 },
{ 520, SKIP, SKIP, SKIP, SKIP, 705 },
{ 570, SKIP, SKIP, SKIP, SKIP, 708 },
{ 571, SKIP, SKIP, SKIP, SKIP, 709 },
{ 572, SKIP, SKIP, SKIP, SKIP, 710 },
{ 573, 573 },
{ 574, 574 },
{ 575, SKIP, SKIP, SKIP, SKIP, 711 },
{ 576, 576 },
{ 517, SKIP, SKIP, SKIP, SKIP, 712 },
{ 577, 577 },
{ 291, 291 },
{ 578, SKIP, SKIP, SKIP, SKIP, 713 },
{ 579, 579 },
{ 580, 580 },
{ 581, SKIP, SKIP, SKIP, SKIP, 714 },
{ 524, 524 },
{ 582, 582 },
{ 583, SKIP, SKIP, SKIP, SKIP, 715 },
{ 584, 584 },
{ 585, SKIP, SKIP, SKIP, SKIP, 716 },
{ 586, SKIP, SKIP, SKIP, SKIP, 717 },
{ 587, SKIP, SKIP, SKIP, SKIP, 718 },
{ 588, 588 },
{ 589, 589 },
{ 590, SKIP, SKIP, SKIP, SKIP, 719 },
{ 521, SKIP, SKIP, SKIP, SKIP, 720 },
{ 522, SKIP, SKIP, SKIP, SKIP, 721 },
{ 591, SKIP, SKIP, SKIP, SKIP, 722 },
{ 593, SKIP, SKIP, SKIP, SKIP, 723 },
{ 596, SKIP, SKIP, SKIP, SKIP, 724 },
{ 598, SKIP, SKIP, SKIP, SKIP, 725 },
{ 599, SKIP, SKIP, SKIP, SKIP, 726 },
{ 600, SKIP, SKIP, SKIP, SKIP, 727 },
{ 601, SKIP, SKIP, SKIP, SKIP, 728 },
{ 602, 602 },
{ 603, 603 },
{ 604, 604 },
{ 605, 605 },
}
Data.byBase = {}
Data.byAny = {}
for i, row in ipairs(Data.REMATCHES) do
if Data.byBase[row[1]] == nil then Data.byBase[row[1]] = i end
for j = 1, Data.MAX_REMATCH_PARTIES do
local id = row[j]
if id == nil or id == 0 then break end
if id ~= SKIP and Data.byAny[id] == nil then Data.byAny[id] = i end
end
end
return Data
+8 -1
View File
@@ -20,7 +20,7 @@ CacheContract.VERSION_FORMAT = {
-- engine/overworld/map_sprites.asm:181
yellow = "rom-cache-v11-yellow1:",
-- v8: M4A tracks retain reachable patterns and explicit entry offsets.
firered = "rom-cache-v12-firered:",
firered = "rom-cache-v13-firered:",
}
CacheContract.MARKER_PATH = "rom-cache.complete"
@@ -205,6 +205,13 @@ CacheContract.VERSION_REQUIRED_FILES_OVERRIDE = {
"data/generated/gba/pokemon/party/slot_main.rgba",
"data/generated/gba/items/bag/manifest.lua",
"data/generated/gba/items/bag/bg.rgba",
"data/generated/gba/items/bag/bg_female.rgba",
"data/generated/gba/items/bag/list.rgba",
"data/generated/gba/items/bag/list_female.rgba",
"data/generated/gba/items/bag/list_blank.rgba",
"data/generated/gba/items/bag/list_blank_female.rgba",
"data/generated/gba/items/bag/desc_sel.rgba",
"data/generated/gba/items/bag/red_arrow.rgba",
"data/generated/gba/items/shop/manifest.lua",
"data/generated/gba/items/shop/bg.rgba",
"data/generated/gba/doors/manifest.lua",
+81 -30
View File
@@ -7,7 +7,7 @@ local Lz77 = require("src.import.gba.lz77")
local BagChromeExtract = {}
BagChromeExtract.CACHE_SUB = "items/bag"
BagChromeExtract.FORMAT_VERSION = 1
BagChromeExtract.FORMAT_VERSION = 2
local function default_cache_root()
local ok, Extract = pcall(require, "src.import.gba.extract_island1")
@@ -65,51 +65,76 @@ local function load_pal_banks(bytes, count)
return banks
end
local function bake_bg_rgba(gfx, palBytes, map, W, H)
local function bake_tiles_rgba(gfx, banks, W, H, entryAt)
local tileCount = math.floor(byte_len(gfx) / 32)
local banks = load_pal_banks(palBytes)
local mapW = 32
local indices, pals = {}, {}
for i = 1, W * H do indices[i] = 0; pals[i] = 0 end
local tilesH = math.min(32, math.floor(H / 8))
local tilesW = math.min(32, math.floor(W / 8))
for ty = 0, tilesH - 1 do
for tx = 0, tilesW - 1 do
local mi = (ty * mapW + tx) * 2 + 1
local entry = (map[mi] or 0) + (map[mi + 1] or 0) * 256
local chunks = {}
local tmp = {}
for ty = 0, math.floor(H / 8) - 1 do
for tx = 0, math.floor(W / 8) - 1 do
local entry = entryAt(tx, ty) or 0
local tileId = entry % 1024
local hflip = math.floor(entry / 1024) % 2 == 1
local vflip = math.floor(entry / 2048) % 2 == 1
local palNum = math.floor(entry / 4096) % 16
local bank = banks[math.floor(entry / 4096) % 16] or banks[0]
if tileId >= tileCount then tileId = 0 end
local tile = {}
local base = tileId * 32
for i = 1, 32 do tile[i] = gfx[base + i] or 0 end
local tmp = {}
for i = 1, 64 do tmp[i] = 0 end
decode_tile_4bpp(tile, tmp, 0, 0, 8, hflip, vflip)
for row = 0, 7 do
for col = 0, 7 do
local px, py = tx * 8 + col, ty * 8 + row
if px < W and py < H then
local di = py * W + px + 1
indices[di] = tmp[row * 8 + col + 1] or 0
pals[di] = palNum
end
local c = bank and bank[tmp[row * 8 + col + 1] or 0] or 0
local r, g, b = bgr555_to_rgb8(c)
chunks[(ty * 8 + row) * W + tx * 8 + col + 1] = string.char(r, g, b, 255)
end
end
end
end
return table.concat(chunks)
end
local function map_entry(map, tx, ty)
local mi = (ty * 32 + tx) * 2 + 1
return (map[mi] or 0) + (map[mi + 1] or 0) * 256
end
local function bake_bg_rgba(gfx, banks, map, W, H)
return bake_tiles_rgba(gfx, banks, W, H, function(tx, ty)
return map_entry(map, tx, ty)
end)
end
local function female_banks(palBytes, femaleBytes)
local banks = load_pal_banks(palBytes)
local over = load_pal_banks(femaleBytes, 1)
if over[0] then banks[0] = over[0] end
return banks
end
local function bake_red_arrow(gfx, palBytes)
local W, H = 16, 32
local pal = load_pal_banks(palBytes, 1)[0] or {}
local pixels = {}
for i = 1, W * H do pixels[i] = 0 end
local ti = 0
for ty = 0, 3 do
for tx = 0, 1 do
local tile = {}
for i = 1, 32 do tile[i] = gfx[ti * 32 + i] or 0 end
decode_tile_4bpp(tile, pixels, tx * 8, ty * 8, W, false, false)
ti = ti + 1
end
end
local chunks = {}
for i = 1, W * H do
local idx = indices[i] or 0
local bank = banks[pals[i] or 0] or banks[0]
local c = bank and bank[idx] or 0
local r, g, b = bgr555_to_rgb8(c)
local a = (idx == 0) and 0 or 255
-- Bag BG uses opaque black for index0 in most cells; keep opaque for full-screen.
a = 255
chunks[i] = string.char(r, g, b, a)
local idx = pixels[i] or 0
if idx == 0 then
chunks[i] = string.char(0, 0, 0, 0)
else
local r, g, b = bgr555_to_rgb8(pal[idx] or 0)
chunks[i] = string.char(r, g, b, 255)
end
end
return table.concat(chunks)
end
@@ -200,7 +225,31 @@ function BagChromeExtract.run(rom, cache, opts)
local gfx = Lz77.decompress(get, Versions.BAG_BG_GFX)
local pal = Lz77.decompress(get, Versions.BAG_BG_PAL)
local map = Lz77.decompress(get, Versions.BAG_BG_TILEMAP)
cache:write(root .. "/bg.rgba", bake_bg_rgba(gfx, pal, map, W, H))
local femalePal = Lz77.decompress(get, Versions.BAG_BG_PAL_FEMALE)
local maleBanks = load_pal_banks(pal)
local femaleBanks = female_banks(pal, femalePal)
cache:write(root .. "/bg.rgba", bake_bg_rgba(gfx, maleBanks, map, W, H))
cache:write(root .. "/bg_female.rgba", bake_bg_rgba(gfx, femaleBanks, map, W, H))
local LW, LH = Versions.BAG_LIST_TILES_W, Versions.BAG_LIST_TILES_H
local listMap = rom:readBytes(Versions.BAG_LIST_TILEMAP, LW * LH * 2)
local function list_entry(tx, ty)
local i = (ty * LW + tx) * 2 + 1
return (listMap[i] or 0) + (listMap[i + 1] or 0) * 256
end
local function blank_entry() return Versions.BAG_LIST_BLANK_TILE end
cache:write(root .. "/list.rgba", bake_tiles_rgba(gfx, maleBanks, LW * 8, LH * 8, list_entry))
cache:write(root .. "/list_female.rgba", bake_tiles_rgba(gfx, femaleBanks, LW * 8, LH * 8, list_entry))
cache:write(root .. "/list_blank.rgba", bake_tiles_rgba(gfx, maleBanks, LW * 8, LH * 8, blank_entry))
cache:write(root .. "/list_blank_female.rgba", bake_tiles_rgba(gfx, femaleBanks, LW * 8, LH * 8, blank_entry))
-- src/item_menu.c:1118
cache:write(root .. "/desc_sel.rgba", bake_tiles_rgba(gfx, maleBanks, W, 48, function(tx, ty)
return map_entry(map, tx, ty + 14) % 4096 + 2 * 4096
end))
local arrowGfx = Lz77.decompress(get, Versions.RED_ARROW_OTHER_GFX)
local arrowPal = rom:readBytes(Versions.RED_ARROW_PAL, 32)
cache:write(root .. "/red_arrow.rgba", bake_red_arrow(arrowGfx, arrowPal))
local maleGfx = Lz77.decompress(get, Versions.BAG_MALE_GFX)
local femaleGfx = Lz77.decompress(get, Versions.BAG_FEMALE_GFX)
@@ -258,6 +307,8 @@ return {
bagW = %d,
bagH = %d,
bagFrames = %d,
listW = %d,
listH = %d,
iconW = 24,
iconH = 24,
itemCount = %d,
@@ -266,7 +317,7 @@ return {
}
]],
BagChromeExtract.FORMAT_VERSION, W, H, bw, bh, frames or 4,
itemCount, baked, failed)
LW * 8, LH * 8, itemCount, baked, failed)
cache:write(root .. "/manifest.lua", manifest)
print(string.format("[bag_chrome] bg + bag sprites + %d icons (%d fail) → %s",
+1 -1
View File
@@ -394,7 +394,7 @@ function ExtractScripts.loadBundle(cache, root, opts)
end
-- Overlay stdscripts always (nurse/PC host arms).
for k, v in pairs(merge_std_scripts()) do
if scripts[k] == nil then scripts[k] = v end
scripts[k] = v
end
local stdText = merge_std_text()
text = text or {}
+6
View File
@@ -1290,6 +1290,12 @@ Versions.BAG_FEMALE_GFX = 0xE83DBC -- gBagFemale_Gfx
Versions.BAG_SPRITE_PAL = 0xE84560 -- gBag_Pal
Versions.BAG_SWAP_GFX = 0xE84588 -- gSwapLine_Gfx
Versions.BAG_SWAP_PAL = 0xE845C8 -- gSwapLine_Pal
Versions.BAG_LIST_TILEMAP = 0x452D08 -- sItemListTilemap (raw u16 18x12)
Versions.BAG_LIST_TILES_W = 18
Versions.BAG_LIST_TILES_H = 12
Versions.BAG_LIST_BLANK_TILE = 0x02D -- src/item_menu.c:1163
Versions.RED_ARROW_PAL = 0x463308 -- sRedArrowPal (raw 32 bytes)
Versions.RED_ARROW_OTHER_GFX = 0x463328 -- sRedArrowOtherGfx (LZ 4bpp 16x32)
-- TM Case (LZ-compressed & raw; FireRed USA 1.0).
Versions.TM_CASE_BG_GFX = 0xE845D8 -- gTMCase_Gfx (LZ 4bpp, 2912 bytes)
+148 -14
View File
@@ -10,6 +10,9 @@ BagChrome._bg = nil
BagChrome._bagMale = nil
BagChrome._bagFemale = nil
BagChrome._bagQuads = {}
BagChrome._bagData = {}
BagChrome._rotated = {}
BagChrome._images = {}
BagChrome._icons = {} -- id → Image
BagChrome._manifest = nil
BagChrome._logged = false
@@ -76,6 +79,14 @@ local function load_lua(rel)
return nil
end
local function rgba_to_imagedata(rgba, w, h)
if not (love and love.image) then return nil end
if not rgba or #rgba < w * h * 4 then return nil end
local ok, imageData = pcall(love.image.newImageData, w, h, "rgba8", rgba)
if ok and imageData then return imageData end
return nil
end
local function rgba_to_image(rgba, w, h)
if not (love and love.image and love.graphics) then return nil end
if not rgba or #rgba < w * h * 4 then return nil end
@@ -111,6 +122,9 @@ function BagChrome.install(cache)
BagChrome._bagMale = nil
BagChrome._bagFemale = nil
BagChrome._bagQuads = {}
BagChrome._bagData = {}
BagChrome._rotated = {}
BagChrome._images = {}
BagChrome._icons = {}
BagChrome._manifest = nil
BagChrome._logged = false
@@ -162,40 +176,160 @@ local function bag_quad(frame)
return q
end
--- Pocket index 1..5 → bag sprite frame 0..3 (TM/Berry share last frames).
local function frame_for_pocket(pocketIdx)
pocketIdx = tonumber(pocketIdx) or 1
if pocketIdx <= 1 then return 0 end
if pocketIdx == 2 then return 1 end
if pocketIdx == 3 then return 2 end
return 3
-- src/item_menu_icons.c:52
local POCKET_FRAME = { 2, 3, 1 }
function BagChrome.frameForPocket(pocketIdx)
return POCKET_FRAME[tonumber(pocketIdx) or 1] or 2
end
function BagChrome.drawBg(x, y)
local img = ensure_bg()
local function named_image(name, w, h)
local img = BagChrome._images[name]
if img ~= nil then return img or nil end
img = rgba_to_image(read_bytes(bag_root() .. "/" .. name .. ".rgba"), w, h)
BagChrome._images[name] = img or false
return img
end
function BagChrome.drawBg(x, y, opts)
local img = (opts and opts.female) and named_image("bg_female", 240, 160) or nil
img = img or ensure_bg()
if not img then return false end
love.graphics.setColor(1, 1, 1, 1)
love.graphics.draw(img, x or 0, y or 0)
return true
end
-- src/item_menu.c:1163, 1332
function BagChrome.drawListFrame(rows, female)
local suffix = female and "_female" or ""
local blank = named_image("list_blank" .. suffix, 144, 96) or named_image("list_blank", 144, 96)
local list = named_image("list" .. suffix, 144, 96) or named_image("list", 144, 96)
if not (blank and list) then return false end
rows = math.max(0, math.min(12, tonumber(rows) or 0))
love.graphics.setColor(1, 1, 1, 1)
love.graphics.draw(blank, 88, 8)
if rows > 0 then
local key = "list_rows_" .. rows
local q = BagChrome._bagQuads[key]
if not q then
q = love.graphics.newQuad(0, (12 - rows) * 8, 144, rows * 8, 144, 96)
BagChrome._bagQuads[key] = q
end
love.graphics.draw(list, q, 88, 8 + (12 - rows) * 8)
end
return true
end
-- src/item_menu.c:1118
function BagChrome.drawDescSelected()
local img = named_image("desc_sel", 240, 48)
if not img then return false end
love.graphics.setColor(1, 1, 1, 1)
love.graphics.draw(img, 0, 112)
return true
end
-- src/menu_indicators.c:259
function BagChrome.drawArrow(dir, x, y)
local img = named_image("red_arrow", 16, 32)
if not img then return false end
local key = "arrow_" .. tostring(dir)
local q = BagChrome._bagQuads[key]
if not q then
local top = (dir == "up" or dir == "down") and 16 or 0
q = love.graphics.newQuad(0, top, 16, 16, 16, 32)
BagChrome._bagQuads[key] = q
end
local sx = (dir == "right") and -1 or 1
local sy = (dir == "down") and -1 or 1
love.graphics.setColor(1, 1, 1, 1)
love.graphics.draw(img, q, x + (sx < 0 and 16 or 0), y + (sy < 0 and 16 or 0), 0, sx, sy)
return true
end
local BIOS_SIN = {}
for i = 0, 255 do
BIOS_SIN[i] = math.floor(math.sin(i * 2 * math.pi / 256) * 16384 + 0.5)
end
local function bag_data(female)
local key = female and "f" or "m"
local d = BagChrome._bagData[key]
if d ~= nil then return d or nil end
local man = BagChrome._manifest or load_lua(bag_root() .. "/manifest.lua")
BagChrome._manifest = man
local w = (man and man.bagW) or 64
local h = (man and man.bagH) or 256
d = rgba_to_imagedata(read_bytes(bag_root() .. (female and "/bag_female.rgba" or "/bag_male.rgba")), w, h)
BagChrome._bagData[key] = d or false
return d
end
-- src/sprite.c:1292
local function rotated_image(female, frame, rot)
local key = (female and "f" or "m") .. frame .. ":" .. rot
local img = BagChrome._rotated[key]
if img ~= nil then return img or nil end
local src = bag_data(female)
if not src then
BagChrome._rotated[key] = false
return nil
end
local idx = rot % 256
local sn, cs = BIOS_SIN[idx], BIOS_SIN[(idx + 64) % 256]
local pa = math.floor(256 * cs / 16384)
local pb = math.floor(-(256 * sn) / 16384)
local pc = math.floor(256 * sn / 16384)
local pd = math.floor(256 * cs / 16384)
local out = love.image.newImageData(64, 64)
local base = frame * 64
for y = 0, 63 do
local iy = y - 32
for x = 0, 63 do
local ix = x - 32
local tx = math.floor((pa * ix + pb * iy) / 256) + 32
local ty = math.floor((pc * ix + pd * iy) / 256) + 32
if tx >= 0 and tx < 64 and ty >= 0 and ty < 64 then
out:setPixel(x, y, src:getPixel(tx, base + ty))
else
out:setPixel(x, y, 0, 0, 0, 0)
end
end
end
img = love.graphics.newImage(out)
img:setFilter("nearest", "nearest")
BagChrome._rotated[key] = img
return img
end
--- Draw bag sprite. pret field position ≈ (40, 68).
function BagChrome.drawBag(px, py, opts)
opts = opts or {}
local female = opts.female == true
local img = ensure_bag_sheet(female)
if not img then
img = ensure_bag_sheet(not female)
female = not female
img = ensure_bag_sheet(female)
end
if not img then return false end
local frame = opts.frame
if frame == nil then frame = frame_for_pocket(opts.pocketIdx) end
local q = bag_quad(frame)
if frame == nil then frame = BagChrome.frameForPocket(opts.pocketIdx) end
frame = math.max(0, math.min(3, math.floor(frame)))
local rot = math.floor(tonumber(opts.rotation) or 0)
love.graphics.setColor(1, 1, 1, 1)
if rot % 256 ~= 0 then
local r = rotated_image(female, frame, rot)
if r then
love.graphics.draw(r, px or 8, py or 36)
return true
end
end
local q = bag_quad(frame)
if q then
love.graphics.draw(img, q, px or 40, py or 68)
love.graphics.draw(img, q, px or 8, py or 36)
else
love.graphics.draw(img, px or 40, py or 68)
love.graphics.draw(img, px or 8, py or 36)
end
return true
end
+576 -194
View File
@@ -7,6 +7,8 @@ local FrlgFont = require("src.ui.game3.frlg_font")
local ItemsData = require("src.core.game3.items_data")
local Bag = require("src.core.game3.bag")
local ItemUse = require("src.core.game3.item_use")
local Options = require("src.core.game3.options")
local Trig = require("src.core.game3.trig")
local BagMenu = {}
@@ -27,9 +29,38 @@ local LIST_LEFT = 11
local LIST_W = 18
local LIST_H = 13
-- include/constants/songs.h:251
local SE_BAG_CURSOR = 245
local SE_BAG_POCKET = 246
local SE_SELECT = 5
-- src/item_menu_icons.c:81
local SHAKE_ROT = { -2, -4, -2, 0, 2, 4, 2, 0, -2, -4, -2, 0 }
-- src/bag.c:13
local WIN_WHITE = { fg = FrlgFont.STDPAL[1], shadow = FrlgFont.STDPAL[2], bg = FrlgFont.STDPAL[0] }
local CURSOR_SELECTED = { fg = FrlgFont.STDPAL[3], shadow = FrlgFont.STDPAL[2], bg = FrlgFont.STDPAL[0] }
-- src/item_menu.c:285
local ITEM_BLUE = { fg = FrlgFont.STDPAL[8], shadow = FrlgFont.STDPAL[9], bg = FrlgFont.STDPAL[0] }
local sessionState = setmetatable({}, { __mode = "k" })
local function se(id)
pcall(function() require("src.core.game3.audio").playSe(id) end)
end
local function actions_for_pocket(pocket, row)
if BagMenu._battle then
return { "USE", "CANCEL" }
-- src/item_menu.c:1344
local num = row and ItemsData.toNumericId(row.id)
if num == ItemsData.ITEM_BERRY_POUCH then
return { "OPEN", "CANCEL" }
end
local BattleItems = require("src.core.game3.battle.items")
if row and BattleItems.isBattleUsable(row.id) then
return { "USE", "CANCEL" }
end
return { "CANCEL" }
end
pocket = pocket or "ITEMS"
local info = row and (row.info or ItemsData.info(row.id))
@@ -82,42 +113,99 @@ function BagMenu.list(pocket)
return tostring(a.name) < tostring(b.name)
end)
end
if BagMenu._battle then
if pocket == "KEY_ITEMS" or pocket == "TM_CASE" then
return {}
end
local BattleItems = require("src.core.game3.battle.items")
local filtered = {}
for _, r in ipairs(rows) do
if BattleItems.isBattleUsable(r.id) then
filtered[#filtered + 1] = r
end
end
return filtered
end
return rows
end
local function max_showed(total)
-- src/item_menu.c:1005
return math.min(VISIBLE, total)
end
local function clamp_cursor()
local rows = BagMenu.list()
local n = #rows
if n < 1 then
BagMenu.cursor = 1
BagMenu.scroll = 0
return rows
end
if BagMenu.cursor > n then BagMenu.cursor = n end
local total = #rows + 1
local shown = max_showed(total)
if BagMenu.cursor > total then BagMenu.cursor = total end
if BagMenu.cursor < 1 then BagMenu.cursor = 1 end
if BagMenu.scroll > total - shown then BagMenu.scroll = total - shown end
if BagMenu.cursor <= BagMenu.scroll then
BagMenu.scroll = BagMenu.cursor - 1
end
if BagMenu.cursor > BagMenu.scroll + VISIBLE then
BagMenu.scroll = BagMenu.cursor - VISIBLE
if BagMenu.cursor > BagMenu.scroll + shown then
BagMenu.scroll = BagMenu.cursor - shown
end
if BagMenu.scroll < 0 then BagMenu.scroll = 0 end
return rows
end
local function bag_state()
local key = BagMenu._session or BagMenu._bag or BagMenu
local st = sessionState[key]
if not st then
st = { pocket = 1, pos = {} }
sessionState[key] = st
end
return st
end
local function save_pos()
local st = bag_state()
st.pocket = BagMenu.pocketIdx
st.pos[BagMenu.pocketIdx] = { cursor = BagMenu.cursor, scroll = BagMenu.scroll }
end
local function load_pos(pocketIdx)
local p = bag_state().pos[pocketIdx]
BagMenu.cursor = p and p.cursor or 1
BagMenu.scroll = p and p.scroll or 0
end
-- src/item_menu.c:866
local function settle_scroll()
local st = bag_state()
for p, pos in pairs(st.pos) do
local total = #BagMenu.list(ItemsData.BAG_POCKET_ORDER[p]) + 1
local shown = max_showed(total)
if pos.cursor > total then pos.cursor = total end
if pos.scroll > total - shown then pos.scroll = total - shown end
if pos.scroll < 0 then pos.scroll = 0 end
local row = pos.cursor - pos.scroll - 1
if row > 3 then
local j = 0
while j <= row - 3 do
if pos.scroll + shown == total then break end
row = row - 1
pos.scroll = pos.scroll + 1
j = j + 1
end
end
end
end
local function field_fade_in()
local okF, Fade = pcall(require, "src.ui.game3.fade")
if okF and Fade and Fade.begin then
Fade.begin(Fade.MODE.FROM_BLACK, 1)
end
end
-- src/item_menu.c:915
local function begin_open(curtain)
BagMenu._exit = nil
BagMenu._open = { k = 0, curtain = curtain }
end
-- src/item_menu.c:893, 941
local function begin_exit(curtain, cb)
BagMenu._open = nil
BagMenu._switch = nil
BagMenu._exit = { k = 0, curtain = curtain, cb = cb }
end
local function reshow()
if BagMenu.open then begin_open(false) end
end
function BagMenu.show(sessionBag, opts)
opts = opts or {}
BagMenu.open = true
@@ -133,9 +221,8 @@ function BagMenu.show(sessionBag, opts)
end
BagMenu._battle = opts.battle and true or false
BagMenu._onBattleUse = opts.onBattleUse
BagMenu.cursor = 1
BagMenu.pocketIdx = opts.pocketIdx or 1
BagMenu.scroll = 0
local st = bag_state()
BagMenu.pocketIdx = opts.pocketIdx or st.pocket or 1
BagMenu.mode = "list"
BagMenu.partyPurpose = "use"
BagMenu.tossQty = 1
@@ -144,14 +231,25 @@ function BagMenu.show(sessionBag, opts)
for i, p in ipairs(ItemsData.BAG_POCKET_ORDER) do
if p == opts.pocket then BagMenu.pocketIdx = i; break end
end
elseif BagMenu._battle then
BagMenu.pocketIdx = 1
end
if not ItemsData.BAG_POCKET_ORDER[BagMenu.pocketIdx] then BagMenu.pocketIdx = 1 end
settle_scroll()
load_pos(BagMenu.pocketIdx)
clamp_cursor()
BagMenu._switch = nil
BagMenu._shake = nil
BagMenu._arrowK = 0
BagMenu._heldKey = nil
BagMenu._bagAnim = { n = 0 }
begin_open(true)
Stack.push("bag", BagMenu, { hideBelow = not BagMenu._battle })
end
function BagMenu.close()
if BagMenu.open then save_pos() end
BagMenu._open = nil
BagMenu._exit = nil
BagMenu._switch = nil
BagMenu.open = false
local battleCb = BagMenu._onBattleUse
local wasBattle = BagMenu._battle
@@ -167,6 +265,12 @@ function BagMenu.close()
BagMenu._battleUsed = nil
end
local function close_to_field()
local battle = BagMenu._battle
BagMenu.close()
if not battle then field_fade_in() end
end
local function refresh_actions()
local rows = BagMenu.list()
local row = rows[BagMenu.cursor]
@@ -176,11 +280,106 @@ local function refresh_actions()
end
end
function BagMenu.handleInput(input)
local function se(id)
pcall(function() require("src.core.game3.audio").playSe(id) end)
local function pocket_switch_dir(input, pocketIdx)
-- src/item_menu.c:1124
local lr = Options.lrMode(BagMenu._session)
if input:wasPressed("left") or (lr and input:wasPressed("l")) then
if pocketIdx <= 1 then return 0 end
se(SE_BAG_POCKET)
return -1
end
if input:wasPressed("right") or (lr and input:wasPressed("r")) then
if pocketIdx >= #ItemsData.BAG_POCKET_ORDER then return 0 end
se(SE_BAG_POCKET)
return 1
end
return 0
end
-- src/item_menu.c:1147
local function start_switch(dir)
save_pos()
BagMenu.pocketIdx = BagMenu.pocketIdx + dir
load_pos(BagMenu.pocketIdx)
clamp_cursor()
BagMenu._switch = { dir = dir, k = 0 }
BagMenu._bagAnim = { n = 0 }
if BagMenu._shake then BagMenu._shake.cb = false end
end
local function shake_ended()
local s = BagMenu._shake
return s == nil or (s.phase == "shake" and s.j >= 13)
end
-- src/item_menu.c:677
local function cursor_moved()
se(SE_BAG_CURSOR)
if shake_ended() then
BagMenu._shake = { phase = "shake", j = 0, cb = true }
end
end
-- src/list_menu.c:438
local function move_cursor(down)
local total = #BagMenu.list() + 1
local shown = max_showed(total)
local scroll = BagMenu.scroll
local row = BagMenu.cursor - scroll - 1
local newRow
if not down then
newRow = (shown == 1) and 0 or (shown - (math.floor(shown / 2) + shown % 2) - 1)
if scroll == 0 then
if row == 0 then return false end
row = row - 1
elseif row > newRow then
row = row - 1
else
row = newRow
scroll = scroll - 1
end
else
newRow = (shown == 1) and 0 or (math.floor(shown / 2) + shown % 2)
if scroll == total - shown then
if row >= shown - 1 then return false end
row = row + 1
elseif row < newRow then
row = row + 1
else
row = newRow
scroll = scroll + 1
end
end
BagMenu.scroll = scroll
BagMenu.cursor = scroll + row + 1
return true
end
local function held_repeat(input, key)
if input:wasPressed(key) then return true end
-- src/main.c:309
return BagMenu._heldKey == key and BagMenu._heldFrames >= 40
and (BagMenu._heldFrames - 40) % 5 == 0
end
local function track_held(input)
local key
if input.isDown then
if input:isDown("up") then key = "up" elseif input:isDown("down") then key = "down" end
end
if key ~= BagMenu._heldKey or input:wasPressed(key or "") then
BagMenu._heldKey = key
BagMenu._heldFrames = 0
elseif key then
BagMenu._heldFrames = (BagMenu._heldFrames or 0) + 1
end
end
local function open_submenu(fn)
begin_exit(false, fn)
end
local function handle_menu_input(input)
if BagMenu.mode == "toss" then
local rows = BagMenu.list()
local row = rows[BagMenu.cursor]
@@ -252,13 +451,17 @@ function BagMenu.handleInput(input)
end,
})
else
local cb = BagMenu._onBattleUse
BagMenu._battleUsed = true
BagMenu.open = false
BagMenu._battle = false
BagMenu._onBattleUse = nil
Stack.pop("bag")
cb(row.id, nil)
-- src/item_use.c:742
begin_exit(true, function()
save_pos()
local cb = BagMenu._onBattleUse
BagMenu._battleUsed = true
BagMenu.open = false
BagMenu._battle = false
BagMenu._onBattleUse = nil
Stack.pop("bag")
cb(row.id, nil)
end)
return
end
else
@@ -266,32 +469,38 @@ function BagMenu.handleInput(input)
if numId == ItemsData.ITEM_TM_CASE or row.id == "TM_CASE" then
local savedState = { pocketIdx = BagMenu.pocketIdx, cursor = BagMenu.cursor, scroll = BagMenu.scroll }
local TmCase = require("src.ui.game3.tm_case")
TmCase.show(BagMenu._session, BagMenu._bag, {
session = BagMenu._session,
bag = BagMenu._bag,
onClose = function()
BagMenu.pocketIdx = savedState.pocketIdx
BagMenu.cursor = savedState.cursor
BagMenu.scroll = savedState.scroll
BagMenu.mode = "list"
clamp_cursor()
end,
})
open_submenu(function()
TmCase.show(BagMenu._session, BagMenu._bag, {
session = BagMenu._session,
bag = BagMenu._bag,
onClose = function()
BagMenu.pocketIdx = savedState.pocketIdx
BagMenu.cursor = savedState.cursor
BagMenu.scroll = savedState.scroll
BagMenu.mode = "list"
clamp_cursor()
reshow()
end,
})
end)
return
elseif numId == ItemsData.ITEM_BERRY_POUCH or row.id == "BERRY_POUCH" then
local savedState = { pocketIdx = BagMenu.pocketIdx, cursor = BagMenu.cursor, scroll = BagMenu.scroll }
local BerryPouch = require("src.ui.game3.berry_pouch")
BerryPouch.show(BagMenu._session, BagMenu._bag, {
session = BagMenu._session,
bag = BagMenu._bag,
onClose = function()
BagMenu.pocketIdx = savedState.pocketIdx
BagMenu.cursor = savedState.cursor
BagMenu.scroll = savedState.scroll
BagMenu.mode = "list"
clamp_cursor()
end,
})
open_submenu(function()
BerryPouch.show(BagMenu._session, BagMenu._bag, {
session = BagMenu._session,
bag = BagMenu._bag,
onClose = function()
BagMenu.pocketIdx = savedState.pocketIdx
BagMenu.cursor = savedState.cursor
BagMenu.scroll = savedState.scroll
BagMenu.mode = "list"
clamp_cursor()
reshow()
end,
})
end)
return
elseif ItemUse.needsPartyTarget(row.id) then
if #party == 0 then
@@ -299,21 +508,44 @@ function BagMenu.handleInput(input)
BagMenu.messageText = "There is no POKéMON."
else
local PartyMenu = require("src.ui.game3.party_menu")
PartyMenu.show(party, BagMenu._session and BagMenu._session.moveOverlay, {
session = BagMenu._session,
bag = BagMenu._bag,
item = row.id,
mode = "use",
onClose = function()
BagMenu.mode = "list"
clamp_cursor()
end,
})
open_submenu(function()
PartyMenu.show(party, BagMenu._session and BagMenu._session.moveOverlay, {
session = BagMenu._session,
bag = BagMenu._bag,
item = row.id,
mode = "use",
onClose = function()
BagMenu.mode = "list"
clamp_cursor()
reshow()
end,
})
end)
end
else
ItemUse.useField(BagMenu._session, BagMenu._bag, row.id, nil)
BagMenu.mode = "list"
clamp_cursor()
local ok, kind, text = ItemUse.useField(BagMenu._session, BagMenu._bag, row.id, nil)
if kind == "vs_seeker" and not ok then
BagMenu.mode = "message"
BagMenu.messageText = text
elseif kind == "vs_seeker" then
-- pokefirered/src/item_use.c:727
local session = BagMenu._session
begin_exit(true, function()
BagMenu.close()
local StartMenu = package.loaded["src.ui.game3.start_menu"]
if StartMenu and StartMenu.isOpen and StartMenu.isOpen() then
StartMenu.open = false
StartMenu._onClose = nil
Stack.pop("start")
end
field_fade_in()
require("src.core.game3.vs_seeker").use(session, nil)
end)
return
else
BagMenu.mode = "list"
clamp_cursor()
end
end
end
elseif act == "GIVE" then
@@ -326,17 +558,35 @@ function BagMenu.handleInput(input)
BagMenu.messageText = "There is no POKéMON."
else
local PartyMenu = require("src.ui.game3.party_menu")
PartyMenu.show(party, BagMenu._session and BagMenu._session.moveOverlay, {
-- src/item_menu.c:1620
open_submenu(function()
PartyMenu.show(party, BagMenu._session and BagMenu._session.moveOverlay, {
session = BagMenu._session,
bag = BagMenu._bag,
item = row.id,
mode = "give",
onClose = function()
BagMenu.mode = "list"
clamp_cursor()
reshow()
end,
})
end)
end
elseif act == "OPEN" then
local BerryPouch = require("src.ui.game3.berry_pouch")
open_submenu(function()
BerryPouch.show(BagMenu._session, BagMenu._bag, {
session = BagMenu._session,
bag = BagMenu._bag,
item = row.id,
mode = "give",
onClose = function()
BagMenu.mode = "list"
clamp_cursor()
reshow()
end,
})
end
end)
return
elseif act == "TOSS" then
BagMenu.mode = "toss"
BagMenu.tossQty = 1
@@ -357,70 +607,13 @@ function BagMenu.handleInput(input)
return
end
-- Battle pocket navigation restriction
if BagMenu._battle then
local order = { "ITEMS", "POKE_BALLS", "BERRY_POUCH" }
if input:wasPressed("left") or input:wasPressed("l") then
local cur = BagMenu.currentPocket()
local idx = 1
for i, p in ipairs(order) do if p == cur then idx = i break end end
idx = ((idx - 2) % #order) + 1
for i, p in ipairs(ItemsData.POCKET_ORDER) do
if p == order[idx] then BagMenu.pocketIdx = i break end
end
BagMenu.cursor = 1
BagMenu.scroll = 0
se(5)
return
elseif input:wasPressed("right") or input:wasPressed("r") then
local cur = BagMenu.currentPocket()
local idx = 1
for i, p in ipairs(order) do if p == cur then idx = i break end end
idx = (idx % #order) + 1
for i, p in ipairs(ItemsData.POCKET_ORDER) do
if p == order[idx] then BagMenu.pocketIdx = i break end
end
BagMenu.cursor = 1
BagMenu.scroll = 0
se(5)
return
end
-- src/item_menu.c:1050
local dir = pocket_switch_dir(input, BagMenu.pocketIdx)
if dir ~= 0 then
start_switch(dir)
return
end
-- list mode
if input:wasPressed("left") or input:wasPressed("l") then
BagMenu.pocketIdx = ((BagMenu.pocketIdx - 2) % #ItemsData.BAG_POCKET_ORDER) + 1
BagMenu.cursor = 1
BagMenu.scroll = 0
se(5)
elseif input:wasPressed("right") or input:wasPressed("r") then
BagMenu.pocketIdx = (BagMenu.pocketIdx % #ItemsData.BAG_POCKET_ORDER) + 1
BagMenu.cursor = 1
BagMenu.scroll = 0
se(5)
elseif input:wasPressed("up") then
local rows = clamp_cursor()
if #rows > 0 then
BagMenu.cursor = ((BagMenu.cursor - 2) % #rows) + 1
clamp_cursor()
se(5)
end
elseif input:wasPressed("down") then
local rows = clamp_cursor()
if #rows > 0 then
BagMenu.cursor = (BagMenu.cursor % #rows) + 1
clamp_cursor()
se(5)
end
elseif input:wasPressed("a") then
local rows = clamp_cursor()
if #rows > 0 then
BagMenu.mode = "action"
BagMenu.actionCursor = 1
refresh_actions()
se(5)
end
elseif input:wasPressed("select") and not BagMenu._battle then
if input:wasPressed("select") and not BagMenu._battle then
local rows = clamp_cursor()
local row = rows[BagMenu.cursor]
if row and BagMenu.currentPocket() == "KEY_ITEMS" then
@@ -435,9 +628,134 @@ function BagMenu.handleInput(input)
se(5)
end
end
elseif input:wasPressed("b") or input:wasPressed("start") then
se(9)
BagMenu.close()
return
end
local rows = clamp_cursor()
if input:wasPressed("a") then
se(SE_SELECT)
if BagMenu.cursor > #rows then
-- src/item_menu.c:1085
begin_exit(true, close_to_field)
else
BagMenu.mode = "action"
BagMenu.actionCursor = 1
refresh_actions()
end
elseif input:wasPressed("b") then
se(SE_SELECT)
begin_exit(true, close_to_field)
elseif held_repeat(input, "up") then
if move_cursor(false) then cursor_moved() end
elseif held_repeat(input, "down") then
if move_cursor(true) then cursor_moved() end
end
end
-- src/item_menu.c:1169
local function run_transitions(input)
local ex = BagMenu._exit
if ex then
ex.k = ex.k + 1
if ex.k >= 15 then
BagMenu._exit = nil
if ex.cb then ex.cb() end
end
return true
end
local op = BagMenu._open
if op then
op.k = op.k + 1
if op.k < 23 then return true end
BagMenu._open = nil
end
local sw = BagMenu._switch
if sw then
local dir = pocket_switch_dir(input, BagMenu.pocketIdx)
if dir ~= 0 then
start_switch(dir)
return true
end
sw.k = sw.k + 1
if sw.k >= 13 then
BagMenu._switch = nil
clamp_cursor()
end
return true
end
return false
end
local function arrows_live()
return BagMenu.mode == "list" and not BagMenu._switch
end
-- src/sprite.c:304
local function animate_sprites()
if BagMenu._bagAnim then
BagMenu._bagAnim.n = BagMenu._bagAnim.n + 1
if BagMenu._bagAnim.n > 6 then BagMenu._bagAnim = nil end
end
local s = BagMenu._shake
if s then
if s.phase == "shake" then
if s.j < 13 then
s.j = s.j + 1
elseif s.cb then
s.phase = "idle"
else
BagMenu._shake = nil
end
else
BagMenu._shake = nil
end
end
if arrows_live() then
BagMenu._arrowK = (BagMenu._arrowK or -1) + 1
else
BagMenu._arrowK = nil
end
end
function BagMenu.handleInput(input)
if BagMenu._battle then
local top = Stack.top()
if top and top.mod ~= BagMenu and top.mod and top.mod.handleInput then
return top.mod.handleInput(input)
end
end
track_held(input)
if not run_transitions(input) then
handle_menu_input(input)
end
animate_sprites()
end
local NO_INPUT = { wasPressed = function() return false end }
function BagMenu.settle()
for _ = 1, 64 do
if not (BagMenu._open or BagMenu._exit or BagMenu._switch) then return end
BagMenu.handleInput(NO_INPUT)
end
end
local function bob(k, freq)
if not k or k < 1 then return 0 end
local v = Trig.sin(((k - 1) * freq) % 256) * 2 / 256
return v < 0 and math.ceil(v) or math.floor(v)
end
local function draw_spaced(text, x, y, colors)
-- src/item_menu.c:756
local cx, cy = x, y
for ttype, val in FrlgFont.scanTokens(text) do
if ttype == "nl" or ttype == "page" then
cx = x
cy = cy + 14
elseif ttype == "char" then
FrlgFont.draw(val, cx, cy, { colors = colors })
cx = cx + FrlgFont.advance(FrlgFont.glyphId(val)) + 2
end
end
end
@@ -445,80 +763,124 @@ function BagMenu.draw()
if not BagMenu.open then return end
local pocket = BagMenu.currentPocket()
local rows = clamp_cursor()
local total = #rows + 1
local switching = BagMenu._switch ~= nil
local selected = BagMenu.mode ~= "list"
local female = false
local session = BagMenu._session
if session and (session.gender == 1 or session.gender == "female"
or session.playerGender == 1) then
female = true
end
local okC, BagChrome = pcall(require, "src.ui.game3.bag_chrome")
local chrome = okC and BagChrome and BagChrome.ready and BagChrome.ready()
if chrome then
BagChrome.drawBg(0, 0)
local female = false
local session = BagMenu._session
if session and (session.gender == 1 or session.gender == "female"
or session.playerGender == 1) then
female = true
BagChrome.drawBg(0, 0, { female = female })
if switching then
BagChrome.drawListFrame(math.min(12, BagMenu._switch.k), female)
end
BagChrome.drawBag(8, 36, { female = female, pocketIdx = BagMenu.pocketIdx })
if selected then
BagChrome.drawDescSelected()
end
local anim = BagMenu._bagAnim
local frame, y2 = nil, 0
if anim then
y2 = math.min(0, anim.n - 5)
if anim.n <= 5 then frame = 0 end
end
local rot = 0
local s = BagMenu._shake
if s and s.phase == "shake" and s.j >= 1 and s.j <= 12 then rot = SHAKE_ROT[s.j] end
BagChrome.drawBag(8, 36 + y2, {
female = female, pocketIdx = BagMenu.pocketIdx, frame = frame, rotation = rot,
})
end
-- Pocket Name Header (left pane: 72px wide box at x=8, y=8, text centered at y=9)
local pLabel = ItemsData.POCKET_LABEL[pocket] or pocket
local tw = FrlgFont.measure(pLabel)
local px = 8 + math.floor((72 - tw) / 2)
FrlgFont.draw(pLabel, px, 9, { colors = FrlgFont.COLOR.NORMAL })
-- Pocket switch indicators (GBA OAM scroll arrows at x=8 and x=72, y=70)
FrlgFont.draw("◀", 8, 68, { colors = FrlgFont.COLOR.DARK_GRAY })
FrlgFont.draw("▶", 74, 68, { colors = FrlgFont.COLOR.DARK_GRAY })
if not switching then
-- src/bag.c:226
local pLabel = ItemsData.POCKET_LABEL[pocket] or pocket
local tw = FrlgFont.measure(pLabel)
FrlgFont.draw(pLabel, 8 + math.floor((72 - tw) / 2), 9, { colors = WIN_WHITE })
end
-- Right List Pane (Window 0: x=88, y=8, item rows at y = 10 + (i-1)*16)
if not chrome then
Window.stdFrame(Window.template(LIST_LEFT, LIST_TOP, LIST_W, LIST_H))
end
if #rows < 1 then
Window.printPx("No items.", 97, 10)
else
-- Scroll Up indicator (x=224, y=10)
if BagMenu.scroll > 0 then
FrlgFont.draw("▲", 224, 10, { colors = FrlgFont.COLOR.DARK_GRAY })
end
-- Scroll Down indicator (x=224, y=90)
if BagMenu.scroll + VISIBLE < #rows then
FrlgFont.draw("▼", 224, 90, { colors = FrlgFont.COLOR.DARK_GRAY })
end
for i = 1, VISIBLE do
if not switching then
local shown = max_showed(total)
for i = 1, shown do
local idx = BagMenu.scroll + i
local r = rows[idx]
if not r then break end
if idx > total then break end
local y = 10 + (i - 1) * 16
if idx == BagMenu.cursor and BagMenu.mode == "list" then
Window.cursorPx(89, y)
if idx == BagMenu.cursor then
if selected then
FrlgFont.drawGlyph(FrlgFont.CHAR_SELECTOR_ARROW, 89, y, { colors = CURSOR_SELECTED })
else
Window.cursorPx(89, y)
end
end
local label = r.name
local session = BagMenu._session
if session and session.registeredItem
and ItemsData.toNumericId(session.registeredItem) == ItemsData.toNumericId(r.id) then
label = "►" .. label
end
FrlgFont.draw(label, 97, y, { maxWidth = 96, colors = FrlgFont.COLOR.NORMAL })
if pocket ~= "KEY_ITEMS" and pocket ~= "TM_CASE" then
FrlgFont.draw(string.format("×%2d", r.qty or 1), 198, y, { colors = FrlgFont.COLOR.NORMAL })
local r = rows[idx]
if not r then
FrlgFont.draw("CANCEL", 97, y, { colors = FrlgFont.COLOR.NORMAL })
else
local label = r.name
if session and session.registeredItem
and ItemsData.toNumericId(session.registeredItem) == ItemsData.toNumericId(r.id) then
label = "►" .. label
end
local num = ItemsData.toNumericId(r.id)
local colors = (num == ItemsData.ITEM_TM_CASE or num == ItemsData.ITEM_BERRY_POUCH)
and ITEM_BLUE or FrlgFont.COLOR.NORMAL
FrlgFont.draw(label, 97, y, { maxWidth = 96, colors = colors })
local info = r.info or ItemsData.info(r.id)
local important = info and (tonumber(info.importance) or 0) ~= 0
if pocket ~= "KEY_ITEMS" and pocket ~= "TM_CASE" and not important then
-- src/item_menu.c:716
FrlgFont.draw(string.format("×%3d", r.qty or 1), 198, y,
{ small = true, colors = FrlgFont.COLOR.NORMAL })
end
end
end
end
-- Bottom-left Item Icon Viewport (top-left of 32x32 viewport at (8,124))
local sel = rows[BagMenu.cursor]
if chrome and sel then
BagChrome.drawItemIcon(sel.id, 8, 124)
if chrome and BagMenu._arrowK and BagMenu._arrowK >= 1 then
-- src/item_menu.c:287, 759
local k = BagMenu._arrowK
if BagMenu.pocketIdx > 1 then
BagChrome.drawArrow("left", 0 + bob(k, 8), 64)
end
if BagMenu.pocketIdx < #ItemsData.BAG_POCKET_ORDER then
BagChrome.drawArrow("right", 64 + bob(k, -8), 64)
end
local shown = max_showed(total)
if BagMenu.scroll > 0 then
BagChrome.drawArrow("up", 152, 0 + bob(k, 8))
end
if BagMenu.scroll < total - shown then
BagChrome.drawArrow("down", 152, 96 + bob(k, -8))
end
end
-- Bottom Description Window (Window 1: tile (5,14) -> px (40,112), text at (40,115))
if BagMenu.mode ~= "action" then
local sel = rows[BagMenu.cursor]
if chrome and not switching then
if sel then
BagChrome.drawItemIcon(sel.id, 8, 124)
else
-- src/data/item_icon_table.h:402
BagChrome.drawItemIcon(ItemsData.ITEMS_COUNT or 375, 8, 124)
end
end
if BagMenu.mode ~= "action" and not switching then
if not chrome then
Window.stdFrame(Window.template(5, 14, 25, 6))
end
if sel and sel.description then
local desc = FrlgFont.wrap(sel.description, 192)
FrlgFont.draw(desc, 40, 115, { maxWidth = 192, linePitch = 15, colors = FrlgFont.COLOR.NORMAL })
local desc = sel and sel.description
if not sel then desc = "CLOSE BAG" end
if desc then
draw_spaced(desc, 40, 115, WIN_WHITE)
end
end
@@ -563,6 +925,26 @@ function BagMenu.draw()
local wrapped = FrlgFont.wrap(BagMenu.messageText, 192)
FrlgFont.draw(wrapped, 40, 115, { maxWidth = 192, linePitch = 15, colors = FrlgFont.COLOR.NORMAL })
end
local level, curtain = 0, 0
local op, ex = BagMenu._open, BagMenu._exit
if op then
-- src/item_menu.c:915, src/palette.c:393
level = math.max(0, 16 - 2 * math.floor(op.k / 2))
if op.curtain then curtain = math.max(0, math.min(160, 192 - 16 * op.k)) end
elseif ex then
level = math.min(16, 4 * math.floor(ex.k / 2))
if ex.curtain then curtain = math.min(160, 16 * ex.k) end
end
if level > 0 then
love.graphics.setColor(0, 0, 0, level / 16)
love.graphics.rectangle("fill", 0, 0, 240, 160)
end
if curtain > 0 then
love.graphics.setColor(0, 0, 0, 1)
love.graphics.rectangle("fill", 0, 0, 240, curtain)
end
love.graphics.setColor(1, 1, 1, 1)
end
return BagMenu
+36 -4
View File
@@ -11,6 +11,7 @@ BattleTransitionChrome._bigPokeball = nil
BattleTransitionChrome._slidingPokeball = nil
BattleTransitionChrome._gridSquare = nil
BattleTransitionChrome._gridQuads = {}
BattleTransitionChrome._gridFrames = {}
BattleTransitionChrome._vsbars = {}
BattleTransitionChrome._banners = {}
BattleTransitionChrome._logged = false
@@ -73,16 +74,33 @@ local function load_lua(rel)
return nil
end
local function rgba_to_image(rgba, w, h)
local function rgba_to_data(rgba, w, h, keyed)
if not (love and love.image and love.graphics) then return nil end
if not rgba or #rgba < w * h * 4 then return nil end
local ok, imageData = pcall(love.image.newImageData, w, h, "rgba8", rgba)
if not ok or not imageData then return nil end
if keyed then
local kr, kg, kb = imageData:getPixel(0, 0)
imageData:mapPixel(function(_, _, r, g, b, a)
if r == kr and g == kg and b == kb then return r, g, b, 0 end
return r, g, b, a
end)
end
return imageData
end
local function data_to_image(imageData, wrap)
if not imageData then return nil end
local image = love.graphics.newImage(imageData)
if image.setFilter then image:setFilter("nearest", "nearest") end
if wrap and image.setWrap then pcall(image.setWrap, image, "repeat", "repeat") end
return image
end
local function rgba_to_image(rgba, w, h, keyed, wrap)
return data_to_image(rgba_to_data(rgba, w, h, keyed), wrap)
end
function BattleTransitionChrome.install(cache)
BattleTransitionChrome._cache = resolve_cache(cache)
BattleTransitionChrome._manifest = nil
@@ -90,6 +108,7 @@ function BattleTransitionChrome.install(cache)
BattleTransitionChrome._slidingPokeball = nil
BattleTransitionChrome._gridSquare = nil
BattleTransitionChrome._gridQuads = {}
BattleTransitionChrome._gridFrames = {}
BattleTransitionChrome._vsbars = {}
BattleTransitionChrome._banners = {}
BattleTransitionChrome._logged = false
@@ -101,9 +120,17 @@ function BattleTransitionChrome.install(cache)
local sp = read_bytes(root .. "/sliding_pokeball.rgba")
local gs = read_bytes(root .. "/grid_square.rgba")
BattleTransitionChrome._bigPokeball = rgba_to_image(bp, 240, 160)
BattleTransitionChrome._bigPokeball = rgba_to_image(bp, 240, 160, true)
BattleTransitionChrome._slidingPokeball = rgba_to_image(sp, 32, 32)
BattleTransitionChrome._gridSquare = rgba_to_image(gs, 8, 120)
local gridData = rgba_to_data(gs, 8, 120, true)
BattleTransitionChrome._gridSquare = data_to_image(gridData)
if gridData and love.image.newImageData then
for frame = 0, 14 do
local fd = love.image.newImageData(8, 8)
fd:paste(gridData, 0, 0, 0, frame * 8, 8, 8)
BattleTransitionChrome._gridFrames[frame] = data_to_image(fd, true)
end
end
if BattleTransitionChrome._gridSquare and love and love.graphics and love.graphics.newQuad then
for frame = 0, 14 do
@@ -118,7 +145,7 @@ function BattleTransitionChrome.install(cache)
local vsKey = key .. "_" .. gender
local vsRgba = read_bytes(root .. "/vsbar_" .. vsKey .. ".rgba")
if vsRgba then
BattleTransitionChrome._vsbars[vsKey] = rgba_to_image(vsRgba, 256, 160)
BattleTransitionChrome._vsbars[vsKey] = rgba_to_image(vsRgba, 256, 160, true, true)
end
end
local bRgba = read_bytes(root .. "/banner_" .. key .. ".rgba")
@@ -158,6 +185,11 @@ function BattleTransitionChrome.gridSquare()
return BattleTransitionChrome._gridSquare, BattleTransitionChrome._gridQuads
end
function BattleTransitionChrome.gridFrame(stage)
BattleTransitionChrome.ensureInstalled()
return BattleTransitionChrome._gridFrames[stage]
end
function BattleTransitionChrome.vsbar(mugshotKey, genderKey)
BattleTransitionChrome.ensureInstalled()
genderKey = (genderKey == "female" or genderKey == 1) and "female" or "male"
+9
View File
@@ -363,6 +363,15 @@ function Chrome.stdFrame(tx, ty, tw, th)
fillRect(tx * T, ty * T, tw * T, th * T, 1, 1, 1, 1)
end
-- src/text_window.c:35
function Chrome.fixedStdFrame(tx, ty, tw, th)
local atlas = ensureStd()
if atlas then return drawNineSlice(atlas, tx, ty, tw, th) end
fillRect(tx * T - 8, ty * T - 8, (tw + 2) * T, (th + 2) * T, 98 / 255, 115 / 255, 123 / 255, 1)
fillRect(tx * T - 6, ty * T - 6, (tw + 2) * T - 4, (th + 2) * T - 4, 205 / 255, 213 / 255, 213 / 255, 1)
fillRect(tx * T, ty * T, tw * T, th * T, 1, 1, 1, 1)
end
drawNineSlice = function(atlas, tx, ty, tw, th)
love.graphics.setColor(1, 1, 1, 1)
local L, Top, W, H = tx, ty, tw, th
+35 -34
View File
@@ -79,12 +79,13 @@ FrlgFont.COLOR = {
DARK = { fg = FrlgFont.STDPAL[2], shadow = FrlgFont.STDPAL[3], bg = FrlgFont.STDPAL[0] },
}
-- NPC Text Color Enums matching pokefirered/include/constants/event_objects.h
-- include/constants/vars.h:340
FrlgFont.NPC_TEXT_COLOR = {
MALE = 0,
FEMALE = 1,
NEUTRAL = 2,
MON = 3,
MON = 2,
NEUTRAL = 3,
DEFAULT = 255,
}
-- 152-element sTextColorTable from pokefirered/src/dynamic_placeholder_text_util.c
@@ -136,39 +137,39 @@ local sTextColorTable = {
[43] = 0x00, -- OBJ_EVENT_GFX_BLAINE / OBJ_EVENT_GFX_GIOVANNI
[44] = 0x01, -- OBJ_EVENT_GFX_MOM / OBJ_EVENT_GFX_CELIO
[45] = 0x00, -- OBJ_EVENT_GFX_TEACHY_TV_HOST / OBJ_EVENT_GFX_GYM_GUY
[46] = 0x22, -- OBJ_EVENT_GFX_ITEM_BALL / OBJ_EVENT_GFX_TOWN_MAP
[47] = 0x22, -- OBJ_EVENT_GFX_POKEDEX / OBJ_EVENT_GFX_CUT_TREE
[48] = 0x22, -- OBJ_EVENT_GFX_ROCK_SMASH_ROCK / OBJ_EVENT_GFX_PUSHABLE_BOULDER
[49] = 0x22, -- OBJ_EVENT_GFX_FOSSIL / OBJ_EVENT_GFX_RUBY
[50] = 0x22, -- OBJ_EVENT_GFX_SAPPHIRE / OBJ_EVENT_GFX_OLD_AMBER
[51] = 0x22, -- OBJ_EVENT_GFX_GYM_SIGN / OBJ_EVENT_GFX_SIGN
[52] = 0x22, -- OBJ_EVENT_GFX_TRAINER_TIPS / OBJ_EVENT_GFX_CLIPBOARD
[53] = 0x22, -- OBJ_EVENT_GFX_METEORITE / OBJ_EVENT_GFX_LAPRAS_DOLL
[54] = 0x32, -- OBJ_EVENT_GFX_SEAGALLOP / OBJ_EVENT_GFX_SNORLAX
[55] = 0x33, -- OBJ_EVENT_GFX_SPEAROW / OBJ_EVENT_GFX_CUBONE
[56] = 0x33, -- OBJ_EVENT_GFX_POLIWRATH / OBJ_EVENT_GFX_CLEFAIRY
[57] = 0x33, -- OBJ_EVENT_GFX_PIDGEOT / OBJ_EVENT_GFX_JIGGLYPUFF
[58] = 0x33, -- OBJ_EVENT_GFX_PIDGEY / OBJ_EVENT_GFX_CHANSEY
[59] = 0x33, -- OBJ_EVENT_GFX_OMANYTE / OBJ_EVENT_GFX_KANGASKHAN
[60] = 0x33, -- OBJ_EVENT_GFX_PIKACHU / OBJ_EVENT_GFX_PSYDUCK
[61] = 0x33, -- OBJ_EVENT_GFX_NIDORAN_F / OBJ_EVENT_GFX_NIDORAN_M
[62] = 0x33, -- OBJ_EVENT_GFX_NIDORINO / OBJ_EVENT_GFX_MEOWTH
[63] = 0x33, -- OBJ_EVENT_GFX_SEEL / OBJ_EVENT_GFX_VOLTORB
[64] = 0x33, -- OBJ_EVENT_GFX_SLOWPOKE / OBJ_EVENT_GFX_SLOWBRO
[65] = 0x33, -- OBJ_EVENT_GFX_MACHOP / OBJ_EVENT_GFX_WIGGLYTUFF
[66] = 0x33, -- OBJ_EVENT_GFX_DODUO / OBJ_EVENT_GFX_FEAROW
[67] = 0x33, -- OBJ_EVENT_GFX_MACHOKE / OBJ_EVENT_GFX_LAPRAS
[68] = 0x33, -- OBJ_EVENT_GFX_ZAPDOS / OBJ_EVENT_GFX_MOLTRES
[69] = 0x33, -- OBJ_EVENT_GFX_ARTICUNO / OBJ_EVENT_GFX_MEWTWO
[70] = 0x33, -- OBJ_EVENT_GFX_MEW / OBJ_EVENT_GFX_ENTEI
[71] = 0x33, -- OBJ_EVENT_GFX_SUICUNE / OBJ_EVENT_GFX_RAIKOU
[72] = 0x33, -- OBJ_EVENT_GFX_LUGIA / OBJ_EVENT_GFX_HO_OH
[73] = 0x33, -- OBJ_EVENT_GFX_CELEBI / OBJ_EVENT_GFX_KABUTO
[74] = 0x33, -- OBJ_EVENT_GFX_DEOXYS_D / OBJ_EVENT_GFX_DEOXYS_A
[75] = 0x23, -- OBJ_EVENT_GFX_DEOXYS_N / OBJ_EVENT_GFX_SS_ANNE
[46] = 0x33, -- OBJ_EVENT_GFX_ITEM_BALL / OBJ_EVENT_GFX_TOWN_MAP
[47] = 0x33, -- OBJ_EVENT_GFX_POKEDEX / OBJ_EVENT_GFX_CUT_TREE
[48] = 0x33, -- OBJ_EVENT_GFX_ROCK_SMASH_ROCK / OBJ_EVENT_GFX_PUSHABLE_BOULDER
[49] = 0x33, -- OBJ_EVENT_GFX_FOSSIL / OBJ_EVENT_GFX_RUBY
[50] = 0x33, -- OBJ_EVENT_GFX_SAPPHIRE / OBJ_EVENT_GFX_OLD_AMBER
[51] = 0x33, -- OBJ_EVENT_GFX_GYM_SIGN / OBJ_EVENT_GFX_SIGN
[52] = 0x33, -- OBJ_EVENT_GFX_TRAINER_TIPS / OBJ_EVENT_GFX_CLIPBOARD
[53] = 0x33, -- OBJ_EVENT_GFX_METEORITE / OBJ_EVENT_GFX_LAPRAS_DOLL
[54] = 0x23, -- OBJ_EVENT_GFX_SEAGALLOP / OBJ_EVENT_GFX_SNORLAX
[55] = 0x22, -- OBJ_EVENT_GFX_SPEAROW / OBJ_EVENT_GFX_CUBONE
[56] = 0x22, -- OBJ_EVENT_GFX_POLIWRATH / OBJ_EVENT_GFX_CLEFAIRY
[57] = 0x22, -- OBJ_EVENT_GFX_PIDGEOT / OBJ_EVENT_GFX_JIGGLYPUFF
[58] = 0x22, -- OBJ_EVENT_GFX_PIDGEY / OBJ_EVENT_GFX_CHANSEY
[59] = 0x22, -- OBJ_EVENT_GFX_OMANYTE / OBJ_EVENT_GFX_KANGASKHAN
[60] = 0x22, -- OBJ_EVENT_GFX_PIKACHU / OBJ_EVENT_GFX_PSYDUCK
[61] = 0x22, -- OBJ_EVENT_GFX_NIDORAN_F / OBJ_EVENT_GFX_NIDORAN_M
[62] = 0x22, -- OBJ_EVENT_GFX_NIDORINO / OBJ_EVENT_GFX_MEOWTH
[63] = 0x22, -- OBJ_EVENT_GFX_SEEL / OBJ_EVENT_GFX_VOLTORB
[64] = 0x22, -- OBJ_EVENT_GFX_SLOWPOKE / OBJ_EVENT_GFX_SLOWBRO
[65] = 0x22, -- OBJ_EVENT_GFX_MACHOP / OBJ_EVENT_GFX_WIGGLYTUFF
[66] = 0x22, -- OBJ_EVENT_GFX_DODUO / OBJ_EVENT_GFX_FEAROW
[67] = 0x22, -- OBJ_EVENT_GFX_MACHOKE / OBJ_EVENT_GFX_LAPRAS
[68] = 0x22, -- OBJ_EVENT_GFX_ZAPDOS / OBJ_EVENT_GFX_MOLTRES
[69] = 0x22, -- OBJ_EVENT_GFX_ARTICUNO / OBJ_EVENT_GFX_MEWTWO
[70] = 0x22, -- OBJ_EVENT_GFX_MEW / OBJ_EVENT_GFX_ENTEI
[71] = 0x22, -- OBJ_EVENT_GFX_SUICUNE / OBJ_EVENT_GFX_RAIKOU
[72] = 0x22, -- OBJ_EVENT_GFX_LUGIA / OBJ_EVENT_GFX_HO_OH
[73] = 0x22, -- OBJ_EVENT_GFX_CELEBI / OBJ_EVENT_GFX_KABUTO
[74] = 0x22, -- OBJ_EVENT_GFX_DEOXYS_D / OBJ_EVENT_GFX_DEOXYS_A
[75] = 0x32, -- OBJ_EVENT_GFX_DEOXYS_N / OBJ_EVENT_GFX_SS_ANNE
}
--- Lookup NPC text color enum from graphicsId (0=Male, 1=Female, 2=Neutral, 3=Mon).
--- Lookup NPC text color enum from graphicsId (0=Male, 1=Female, 2=Mon, 3=Neutral).
function FrlgFont.getNpcTextColor(graphicId)
if not graphicId then return FrlgFont.NPC_TEXT_COLOR.NEUTRAL end
graphicId = tonumber(graphicId)
+2 -2
View File
@@ -151,10 +151,10 @@ local function composeGfWindow(text, logo)
love.graphics.setShader()
love.graphics.clear(0, 0, 0, 0)
love.graphics.setColor(1, 1, 1, 1)
-- pokefirered/src/intro.c:1127
if text then love.graphics.draw(text, 48, 72) end
-- pokefirered/src/intro.c:1251
if logo then love.graphics.draw(logo, 104, 38) end
-- pokefirered/src/intro.c:1127
if text then love.graphics.draw(text, 48, 72) end
love.graphics.pop()
return canvas
end
+79 -16
View File
@@ -8,10 +8,15 @@ local OptionMenu = {}
OptionMenu.open = false
OptionMenu.cursor = 1
local FIRST_ROW = 4
local VISIBLE = 6
local LABEL_COL = 3
local VALUE_COL = 17
local VISIBLE = 7
local WIN_X, WIN_Y, WIN_W, WIN_H = 16, 56, 208, 96
local ROW_Y0 = WIN_Y + 2
local ROW_STEP = 13
local ROW_H = 14
local LABEL_X = WIN_X + 8
local VALUE_X = WIN_X + 0x82
local HELP_BG = { 0 / 255, 123 / 255, 197 / 255, 1 }
local HELP_TEXT = "{DPAD_UPDOWN}PICK {DPAD_LEFTRIGHT}SWITCH {A_BUTTON}{B_BUTTON}CANCEL"
local function ctx()
return OptionMenu._ctx
@@ -150,46 +155,104 @@ function OptionMenu.handleInput(input)
end
end
function OptionMenu.update()
if OptionMenu.open then
OptionMenu._arrowK = (OptionMenu._arrowK or 0) + 1
end
end
local function valueColors()
local FrlgFont = require("src.ui.game3.frlg_font")
return { fg = FrlgFont.STDPAL[5], shadow = FrlgFont.STDPAL[4], bg = FrlgFont.STDPAL[0] } -- src/option_menu.c:180
end
-- src/menu_indicators.c:289
local function bob(k, freq)
local Trig = require("src.core.game3.trig")
local v = Trig.sin(((k or 0) * freq) % 256) * 2 / 256
return v < 0 and math.ceil(v) or math.floor(v)
end
local function scrollArrow(dir, x, y)
local ok, BagChrome = pcall(require, "src.ui.game3.bag_chrome")
if ok and BagChrome and BagChrome.drawArrow then
local drew, res = pcall(BagChrome.drawArrow, dir, x, y)
if drew and res then return end
end
local FrlgFont = require("src.ui.game3.frlg_font")
FrlgFont.drawGlyph(dir == "up" and FrlgFont.CHAR_UP_ARROW or FrlgFont.CHAR_DOWN_ARROW,
x + 4, y + 1, { colors = FrlgFont.COLOR.RED })
end
-- src/option_menu.c:316
local function drawHelpBar()
love.graphics.setColor(HELP_BG)
love.graphics.rectangle("fill", 0, 0, 240, 16)
love.graphics.setColor(1, 1, 1, 1)
local PokedexChrome = require("src.ui.game3.pokedex_chrome")
PokedexChrome.drawControlInfo(HELP_TEXT, 0xE4, 0)
end
function OptionMenu.draw()
if not OptionMenu.open then return end
local p = page()
if not p then return end
local c = ctx()
local Chrome = require("src.ui.game3.chrome")
local FrlgFont = require("src.ui.game3.frlg_font")
love.graphics.setColor(0, 0, 0, 1)
love.graphics.rectangle("fill", 0, 0, 240, 160)
love.graphics.setColor(1, 1, 1, 1)
drawHelpBar()
Chrome.fixedStdFrame(2, 3, 26, 2) -- src/option_menu.c:537
Window.printPx(p.title or "OPTION", 16 + 8, 24 + 1, { colors = FrlgFont.COLOR.NORMAL })
local frameType = tonumber(Options.block(c.options).frameType) or 0
Window.userFrame(Window.template(1, 1, 28, 17), frameType)
Window.print(p.title or "OPTION", 2, 2)
Window.userFrame(Window.template(2, 7, 26, 12), frameType)
local total = rowCount(p)
clampScroll(p)
local vcol = valueColors()
for slot = 1, VISIBLE do
local idx = p.scroll + slot
if idx <= total then
local y = Window.menuRowY(FIRST_ROW, slot)
if idx == p.index then Window.cursor(2, y) end
local y = ROW_Y0 + (slot - 1) * ROW_STEP -- src/option_menu.c:563
if idx > #p.rows then
Window.print("BACK", LABEL_COL, y)
Window.printPx("CANCEL", LABEL_X, y, { colors = FrlgFont.COLOR.NORMAL })
else
local row = p.rows[idx]
Window.print(row.label or "?", LABEL_COL, y)
Window.printPx(row.label or "?", LABEL_X, y, { colors = FrlgFont.COLOR.NORMAL })
if row.value then
local ok, text = pcall(row.value, c)
Window.print(ok and tostring(text) or "----", VALUE_COL, y)
Window.printPx(ok and tostring(text) or "----", VALUE_X, y, { colors = vcol })
end
end
end
end
-- src/option_menu.c:572
local selTop = ROW_Y0 + (p.index - p.scroll - 1) * ROW_STEP
local selBot = selTop + ROW_H
love.graphics.setColor(0, 0, 0, 2 / 16)
if selTop > WIN_Y then
love.graphics.rectangle("fill", WIN_X, WIN_Y, WIN_W, selTop - WIN_Y)
end
if selBot < WIN_Y + WIN_H then
love.graphics.rectangle("fill", WIN_X, selBot, WIN_W, WIN_Y + WIN_H - selBot)
end
love.graphics.setColor(1, 1, 1, 1)
if total > VISIBLE then
local FrlgFont = require("src.ui.game3.frlg_font")
local k = OptionMenu._arrowK or 0
if p.scroll > 0 then
FrlgFont.drawGlyph(FrlgFont.CHAR_UP_ARROW, 224, 26)
scrollArrow("up", 208, WIN_Y + bob(k, 8))
end
if p.scroll + VISIBLE < total then
FrlgFont.drawGlyph(FrlgFont.CHAR_DOWN_ARROW, 224, 106)
scrollArrow("down", 208, WIN_Y + WIN_H - 16 + bob(k, -8))
end
end
Window.print("LEFT/RIGHT:CHANGE B:BACK", 2, 16)
end
return OptionMenu
+3 -1
View File
@@ -124,7 +124,9 @@ function Rows.build(ctx)
})
add({
id = "frameType", label = Strings("FRAME"),
value = function(c) return tostring((tonumber(cart(c).frameType) or 0) + 1) end,
value = function(c)
return Strings("TYPE") .. string.format("%2d", (tonumber(cart(c).frameType) or 0) + 1) -- src/option_menu.c:496
end,
step = function(c, dir)
cartCycle(c, "frameType", 10, dir)
local okC, Chrome = pcall(require, "src.ui.game3.chrome")
+327
View File
@@ -0,0 +1,327 @@
local U = require("tests.drivers.util")
local DIR = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/game3_vs_seeker"
local ITEM_VS_SEEKER = 374
local LASS_MEGAN, LASS_MEGAN_2 = 130, 648
local TWINS_ELI_ANNE, TWINS_ELI_ANNE_2 = 484, 533
local IVYSAUR, WARTORTLE = 2, 8
local CLEFAIRY, JIGGLYPUFF = 35, 39
local FLAG_GOT_VS_SEEKER, FLAG_CELADON, FLAG_FUCHSIA = 0x292, 0x896, 0x897
local FLAG_CHARGING = 0x801
local EMOTE_EXCL, EMOTE_DOUBLE = 0, 6
local failures = 0
local function result(ok, label)
print((ok and "PASS " or "FAIL ") .. label)
if not ok then failures = failures + 1 end
return ok
end
local function finish()
if failures == 0 then
print("PASS game3_vs_seeker")
love.event.quit(0)
else
print("FAIL game3_vs_seeker failures=" .. failures)
love.event.quit(1)
end
end
local function raise_hand(mt)
mt = tonumber(mt) or 0
return mt >= 0x4D and mt <= 0x4F
end
return function(game)
for _ = 1, 600 do
if game.phase == "boot" and game.boot then break end
U.wait(1)
end
game:_handleBootAction({ action = "new_game", name = "RED" })
U.wait(180)
local Runtime = require("src.core.game3.runtime")
local Map = require("src.core.game3.map")
local Space = require("src.core.game3.scripting.space")
local Flags = require("src.core.game3.scripting.flags")
local Party = require("src.core.game3.party")
local Bag = require("src.core.game3.bag")
local Objects = require("src.core.game3.objects")
local Collision = require("src.core.game3.collision")
local TrainerSight = require("src.core.game3.trainer_sight")
local FieldEffects = require("src.core.game3.field_effects")
local ItemUse = require("src.core.game3.item_use")
local StepEvents = require("src.core.game3.step_events")
local Rng = require("src.core.game3.rng")
local VsSeeker = require("src.core.game3.vs_seeker")
local Battle = require("src.core.game3.battle")
local Message = package.loaded["src.ui.game3.message"] or require("src.ui.game3.message")
local session = Runtime.getSession()
if not result(session ~= nil, "new game reached the field") then return finish() end
session.party = {}
Party.giveMon(session, IVYSAUR, 45)
Party.giveMon(session, WARTORTLE, 45)
session.bag = session.bag or Bag.new()
Bag.add(session.bag, ITEM_VS_SEEKER, 1)
local function setFlag(id, on) Flags.setFlag(Space.store, nil, id, on ~= false) end
local function getFlag(id) return Flags.getFlag(Space.store, nil, id) end
setFlag(FLAG_GOT_VS_SEEKER)
setFlag(FLAG_CELADON)
setFlag(FLAG_FUCHSIA)
local okC, MapCatalog = pcall(require, "src.import.gba.map_catalog")
local mapId = (okC and MapCatalog.pretToEngine and MapCatalog.pretToEngine("Route8")) or "FR_ROUTE_8"
Map.load(nil, game, mapId, { x = 40, y = 6, facing = "up" })
game.session.x, game.session.y, game.session.facing = 40, 6, "up"
U.wait(90)
session = Runtime.getSession()
setFlag(Flags.trainerFlagId(LASS_MEGAN))
setFlag(Flags.trainerFlagId(TWINS_ELI_ANNE))
local function find_trainers(tid)
local found = {}
for _, lid in ipairs(Objects.listActive()) do
local eo = Objects.find(lid)
if eo and TrainerSight.getTrainerId(eo) == tid then found[#found + 1] = eo end
end
table.sort(found, function(a, b) return (a.cellX or 0) < (b.cellX or 0) end)
return found[1], found[2]
end
local function stand_facing(tid)
local eo = find_trainers(tid)
if not eo then return nil end
local tx, ty = eo.cellX, eo.cellY
local cands = { { 1, 0, "left" }, { 0, 1, "up" }, { -1, 0, "right" }, { 0, -1, "down" } }
for _, c in ipairs(cands) do
local x, y = tx + c[1], ty + c[2]
if Collision.canEnter(game, x, y, {}) and not Objects.at(x, y) then
Map.load(nil, game, mapId, { x = x, y = y, facing = c[3] })
game.session.x, game.session.y, game.session.facing = x, y, c[3]
U.wait(45)
return find_trainers(tid)
end
end
return nil
end
local function accept_seed()
for seed = 1, 65535 do
Rng.SeedRng(seed)
if Rng.Random() % 100 >= 30 then return seed end
end
return 1
end
local forcedSeed = nil
local origCompute = VsSeeker.computeResponse
VsSeeker.computeResponse = function(...)
if forcedSeed then Rng.SeedRng(forcedSeed) end
return origCompute(...)
end
local function use_seeker(label, shotName)
local ok, kind, text = ItemUse.useField(session, session.bag, ITEM_VS_SEEKER)
result(ok and kind == "vs_seeker", label .. ": VS SEEKER accepted on Route 8 (" .. tostring(text) .. ")")
local seen = {}
local done, response = false, nil
forcedSeed = accept_seed()
VsSeeker.use(session, game, function(_, resp)
done = true
response = resp
end)
local shot = false
for i = 1, 600 do
for _, a in ipairs(FieldEffects._anims or {}) do
if a.kind == "emote" and a.targetObj and a.targetObj.localId then
seen[a.targetObj.localId] = a.baseFrame
end
end
if not shot and i == VsSeeker.EFFECT_FRAMES + 20 and shotName then
shot = true
result(U.shot(game, DIR .. "/" .. shotName), "screenshot " .. shotName)
end
if done then break end
if Message.isOpen and Message.isOpen() then U.tap(game, "a") end
U.wait(1)
end
forcedSeed = nil
result(done, label .. ": VS SEEKER sequence finished")
return response, seen
end
local function diag(label)
local Hud = package.loaded["src.ui.game3.hud"]
local Stack = package.loaded["src.ui.game3.stack"]
local Choice = package.loaded["src.ui.game3.choice"]
local Field = package.loaded["src.core.game3.field"]
local Fade = package.loaded["src.ui.game3.fade"]
local BT = package.loaded["src.core.game3.battle_transition"]
local ctx = Space.vm and Space.vm.ctx or {}
local layers = {}
for _, l in ipairs(Stack and Stack._layers or {}) do layers[#layers + 1] = tostring(l.id) end
print(string.format("DIAG %s phase=%s rt=%s vm=%s/%s pc=%s:%s msg=%s stay=%s waiting=%s page=%s/%s waitBtn=%s stack=[%s] choice=%s locked=%s running=%s fade=%s bt=%s battle=%s steps=%s",
label, tostring(game.phase), tostring(Runtime.isActive and Runtime.isActive()),
tostring(ctx.status), tostring(ctx.mode), tostring(ctx.pc and ctx.pc.listKey), tostring(ctx.pc and ctx.pc.index),
tostring(Message.isOpen and Message.isOpen()), tostring(Message._stay), tostring(Message._waiting),
tostring(Message._page), tostring(Message._pages and #Message._pages),
tostring(Hud and Hud._waitButton ~= nil), table.concat(layers, ","), tostring(Choice and Choice.active),
tostring(Field and Field.locked), tostring(Field and Field.running),
tostring(Fade and Fade.isActive and Fade.isActive()), tostring(BT and BT.isActive and BT.isActive()),
tostring(Battle.isActive()), tostring(StepEvents.busy())))
end
local function wants_a()
local Hud = package.loaded["src.ui.game3.hud"]
return (Message.isOpen and Message.isOpen()) or (Hud and Hud._waitButton ~= nil)
end
local function talk_until_battle()
local sawMsg = false
local openFor = 0
U.tap(game, "a")
for _ = 1, 600 do
if Battle.isActive() then break end
if wants_a() then
sawMsg = true
openFor = openFor + 1
if openFor == 100 then diag("waiting on A for 100 taps") end
U.tap(game, "a")
else
openFor = 0
if sawMsg and not (Space.vm and Space.vm:isRunning()) then break end
end
U.wait(3)
end
return sawMsg
end
local function settle()
for _ = 1, 600 do
local busy = (Space.vm and Space.vm:isRunning()) or wants_a()
or Battle.isActive() or StepEvents.busy()
if not busy then break end
if wants_a() then U.tap(game, "a") end
U.wait(3)
end
U.wait(30)
end
local s = VsSeeker.state(session)
local megan = stand_facing(LASS_MEGAN)
if not result(megan ~= nil, "found LASS MEGAN and stood next to her") then return finish() end
local meganLid = megan.localId
s.steps = 99
local _, uncharged = VsSeeker.use(session, game)
result(uncharged == VsSeeker.NOT_CHARGED and s.steps == 99, "99 steps: not charged, no drain")
settle()
s.steps = 100
local response, seen = use_seeker("single", "00_response.png")
megan = Objects.find(meganLid)
result(response == VsSeeker.RESPONSE_FOUND_REMATCHES, "response is FOUND_REMATCHES")
result(VsSeeker.getRematch(s, meganLid) == 2, "Megan stamped with rematch column 2 (got " .. VsSeeker.getRematch(s, meganLid) .. ")")
result(getFlag(FLAG_CHARGING), "FLAG_SYS_VS_SEEKER_CHARGING set")
result(s.steps == 0 and s.charging == 0, "battery drained, charging counter reset")
result(megan and raise_hand(megan.movementType), "Megan raised her hand (movementType=" .. tostring(megan and megan.movementType) .. ")")
result(seen[meganLid] == EMOTE_DOUBLE, "Megan showed !!")
local sawExcl = false
for lid, base in pairs(seen) do
if lid ~= meganLid and base == EMOTE_EXCL then sawExcl = true end
end
result(sawExcl, "an unfought trainer in range showed !")
settle()
talk_until_battle()
if not result(Battle.isActive(), "talking to Megan started the rematch") then return finish() end
local st = Battle._st
result(st and st.double ~= true, "Megan rematch is a single battle")
result(st and st.trainerId == LASS_MEGAN_2, "tier check: CELADON set picks LASS_MEGAN_2 (got " .. tostring(st and st.trainerId) .. ")")
U.wait(30)
Battle.abort("win")
local sawPost = false
for _ = 1, 200 do
if Message.isOpen and Message.isOpen() and not Battle.isActive() then sawPost = true end
if not (Space.vm and Space.vm:isRunning()) and not Battle.isActive() then break end
U.wait(3)
end
U.wait(60)
result(not sawPost and not (Message.isOpen and Message.isOpen()), "rematch script ends without the post-battle msgbox")
result(getFlag(Flags.trainerFlagId(LASS_MEGAN_2)), "FLAG for LASS_MEGAN_2 set after the win")
result(VsSeeker.getRematch(s, meganLid) == 0, "Megan rematch state cleared")
megan = Objects.find(meganLid)
result(megan and not raise_hand(megan.movementType), "Megan stopped raising her hand")
result(U.shot(game, DIR .. "/01_single_after.png"), "screenshot 01_single_after.png")
settle()
talk_until_battle()
result(not Battle.isActive(), "talking again without a VS SEEKER response gives no battle")
settle()
local eli, anne = find_trainers(TWINS_ELI_ANNE)
if not result(eli ~= nil and anne ~= nil, "found both twins") then return finish() end
local eliLid, anneLid = eli.localId, anne.localId
local ex, ey = eli.cellX, eli.cellY + 1
Map.load(nil, game, mapId, { x = ex, y = ey, facing = "up" })
game.session.x, game.session.y, game.session.facing = ex, ey, "up"
U.wait(45)
s.steps = 100
response, seen = use_seeker("double", "02_double_response.png")
result(response == VsSeeker.RESPONSE_FOUND_REMATCHES, "twins response is FOUND_REMATCHES")
result(VsSeeker.getRematch(s, eliLid) == 3 and VsSeeker.getRematch(s, anneLid) == 3, "both twins stamped with column 3")
result(seen[eliLid] == EMOTE_DOUBLE and seen[anneLid] == EMOTE_DOUBLE, "both twins showed !!")
settle()
talk_until_battle()
if not result(Battle.isActive(), "talking to Eli started the double rematch") then return finish() end
st = Battle._st
result(st and st.double == true, "twins rematch is a double battle")
result(st and st.battlersCount == 4, "battlersCount is 4")
result(st and st.trainerId == TWINS_ELI_ANNE_2, "foe is TWINS_ELI_ANNE_2 (got " .. tostring(st and st.trainerId) .. ")")
local fp = st and st.foeParty or {}
result(fp[1] and fp[2] and fp[1].species == CLEFAIRY and fp[2].species == JIGGLYPUFF
and fp[1].level == 28 and fp[2].level == 28, "foe party is Clefairy L28 + Jigglypuff L28")
U.wait(30)
Battle.abort("win")
for _ = 1, 200 do
if not (Space.vm and Space.vm:isRunning()) and not Battle.isActive() then break end
U.wait(3)
end
U.wait(60)
result(getFlag(Flags.trainerFlagId(TWINS_ELI_ANNE_2)), "FLAG for TWINS_ELI_ANNE_2 set after the win")
result(VsSeeker.getRematch(s, eliLid) == 0 and VsSeeker.getRematch(s, anneLid) == 0, "both twin rematch states cleared")
anne = Objects.find(anneLid)
eli = Objects.find(eliLid)
result(anne and anne.movementType == 8, "Anne set to FACE_DOWN (got " .. tostring(anne and anne.movementType) .. ")")
result(eli and not raise_hand(eli.movementType), "Eli stopped raising her hand")
result(U.shot(game, DIR .. "/03_double_after.png"), "screenshot 03_double_after.png")
settle()
megan = stand_facing(LASS_MEGAN)
if not result(megan ~= nil, "back next to Megan") then return finish() end
s.steps = 100
response = use_seeker("charge", nil)
megan = Objects.find(meganLid)
result(VsSeeker.getRematch(s, meganLid) == 4 and megan and raise_hand(megan.movementType),
"Megan wants the next tier (column 4) and raises her hand")
settle()
s.charging = 99
StepEvents.onStepTaken(session, game)
for _ = 1, 120 do
if not StepEvents.busy() then break end
U.wait(1)
end
megan = Objects.find(meganLid)
result(not getFlag(FLAG_CHARGING) and s.charging == 0, "100 charging steps clear the charging flag")
result(VsSeeker.getRematch(s, meganLid) == 0, "charging done clears the rematch states")
result(megan and megan.movementType >= 7 and megan.movementType <= 10,
"raise-hand replaced by a random face type (got " .. tostring(megan and megan.movementType) .. ")")
result(U.shot(game, DIR .. "/04_charge_reset.png"), "screenshot 04_charge_reset.png")
VsSeeker.computeResponse = origCompute
finish()
end
+2 -1
View File
@@ -220,13 +220,14 @@ check(Bag.get(testBag, 139) == 5, "BERRY + ORAN_BERRY merged to 5 Oran berries")
local BagMenu = require("src.ui.game3.bag_menu")
BagMenu.show(nil, testBag)
BagMenu.settle()
check(BagMenu.currentPocket() == "ITEMS", "BagMenu initial pocket is ITEMS")
BagMenu.handleInput({ wasPressed = function(s, k) return k == "right" end })
check(BagMenu.currentPocket() == "KEY_ITEMS", "BagMenu second pocket is KEY_ITEMS")
BagMenu.handleInput({ wasPressed = function(s, k) return k == "right" end })
check(BagMenu.currentPocket() == "POKE_BALLS", "BagMenu third pocket is POKE_BALLS")
BagMenu.handleInput({ wasPressed = function(s, k) return k == "right" end })
check(BagMenu.currentPocket() == "ITEMS", "BagMenu wraps back to ITEMS (3 pockets only)")
check(BagMenu.currentPocket() == "POKE_BALLS", "BagMenu does not wrap past POKE_BALLS (3 pockets only)")
BagMenu.close()
if failed > 0 then
+49 -5
View File
@@ -121,21 +121,65 @@ end)
check(BattleTransition.isActive() == true, "transition active on start")
check(BattleTransition._phase == "intro", "starts in intro phase")
-- Tick intro phase (32 frames)
for f = 1, 32 do
-- src/battle_transition.c:703
for f = 1, 33 do
BattleTransition.tick(1 / 60)
end
check(BattleTransition._phase == "main", "advances to main phase after 32 intro frames")
check(BattleTransition._phase == "intro", "intro still running after 33 frames")
BattleTransition.tick(1 / 60)
check(BattleTransition._phase == "main", "advances to main phase after 34 intro frames")
-- Tick main phase to completion
local ticks = 0
while BattleTransition.isActive() and ticks < 100 do
while BattleTransition.isActive() and ticks < 300 do
BattleTransition.tick(1 / 60)
ticks = ticks + 1
end
check(doneCalled == true, "transition completes and invokes done callback")
check(BattleTransition.isActive() == false, "transition inactive after finish")
print("[test] 8. Coverage never stops short or reverses")
local function run(id)
local covs, finished = {}, false
BattleTransition.start(id, { skipIntro = true }, function() finished = true end)
local guard = 0
while not finished and guard < 600 do
BattleTransition.tick(1 / 60)
if BattleTransition._fx then covs[#covs + 1] = BattleTransition.screenCoverage() end
guard = guard + 1
end
return covs, finished
end
local covs, finished = run(ID.SLICE)
local monotonic = true
for i = 2, #covs do
if covs[i] < covs[i - 1] then monotonic = false end
end
check(finished, "SLICE finishes")
check(covs[1] < 0.02, "SLICE starts uncovered (no instant black)")
check(monotonic, "SLICE coverage never reverses")
check(covs[#covs] == 1, "SLICE ends fully covered")
local firstFull
for i, c in ipairs(covs) do
if c == 1 and not firstFull then firstFull = i end
end
check(firstFull ~= nil and #covs - firstFull <= 2, "SLICE hands off right after full coverage")
for _, id in ipairs({ ID.WAVE, ID.ANGLED_WIPES, ID.CLOCKWISE_WIPE, ID.GRID_SQUARES, ID.WHITE_BARS_FADE,
ID.BLUR, ID.SWIRL, ID.SHUFFLE, ID.RIPPLE, ID.BIG_POKEBALL, ID.POKEBALLS_TRAIL, ID.BLUE }) do
local c, done = run(id)
check(done and c[#c] == 1, "transition " .. id .. " ends on a black screen")
end
print("[test] 9. SLICE rows extend to the full window width")
BattleTransition.start(ID.SLICE, { skipIntro = true })
for _ = 1, 20 do BattleTransition.tick(1 / 60) end
local even = BattleTransition.blackSpans(0, -120, 360)
local odd = BattleTransition.blackSpans(1, -120, 360)
check(even[1] == -120 and even[2] > -120, "even rows grow from the left window edge")
check(odd[2] == 360 and odd[1] < 360, "odd rows grow from the right window edge")
BattleTransition.abort()
if failed > 0 then
print(string.format("[FAIL] %d test(s) failed", failed))
os.exit(1)
+2
View File
@@ -207,10 +207,12 @@ testParty[1].hp = 5
Bag.add(testBag, "POTION", 1)
BagMenu.show({ party = testParty, bag = testBag }, { bag = testBag })
assert(BagMenu.isOpen() == true, "BagMenu is open")
BagMenu.settle()
BagMenu.mode = "action"
BagMenu.actionCursor = 1 -- USE
mockInput:press("a")
BagMenu.handleInput(mockInput)
BagMenu.settle()
assert(PartyMenu.isOpen() == true, "PartyMenu opened from BagMenu USE")
assert(PartyMenu.mode == "use", "PartyMenu is in use mode")
-- Select Mon 1 with A
+1
View File
@@ -573,6 +573,7 @@ do
session = session,
pocket = "KEY_ITEMS",
})
BagMenu.settle()
check(BagMenu.isOpen() == true, "BagMenu is open in KEY_ITEMS pocket")
check(BagMenu.currentPocket() == "KEY_ITEMS", "current pocket is KEY_ITEMS")
+10 -1
View File
@@ -288,7 +288,7 @@ assert(Pokedex.isOpen() == false, "Registration screen closed on A press")
assert(regClosed == true, "onDone callback invoked")
print("[ok] Pokédex UI navigation, chrome entries, and registration view passed")
print("[test] 6. Battle Bag pocket navigation (ITEMS <-> POKE_BALLS) and action back-out")
print("[test] 6. Battle Bag pocket navigation (ITEMS -> KEY_ITEMS -> POKE_BALLS) and action back-out")
local BagMenu = require("src.ui.game3.bag_menu")
local battleBag = Bag.new()
Bag.add(battleBag, "POTION", 3)
@@ -301,10 +301,15 @@ BagMenu.show(battleBag, {
assert(BagMenu.isOpen() == true, "BagMenu is open in battle")
assert(BagMenu.mode == "list", "BagMenu starts in list mode (not action mode)")
assert(BagMenu.currentPocket() == "ITEMS", "Starts on ITEMS pocket")
BagMenu.settle()
-- Swap to POKé_BALLS pocket with right arrow
mockInput:press("right")
BagMenu.handleInput(mockInput)
assert(BagMenu.currentPocket() == "KEY_ITEMS", "Swapped to KEY_ITEMS pocket")
mockInput:press("right")
BagMenu.handleInput(mockInput)
BagMenu.settle()
assert(BagMenu.currentPocket() == "POKE_BALLS", "Swapped to POKE_BALLS pocket")
assert(#BagMenu.list() == 1, "1 ball item in pocket")
assert(BagMenu.list()[1].id == 4 or BagMenu.list()[1].name == "POKé BALL", "Poke ball in list")
@@ -324,11 +329,15 @@ assert(BagMenu.currentPocket() == "POKE_BALLS", "Still in POKE_BALLS pocket")
-- Swap back to ITEMS pocket with left arrow
mockInput:press("left")
BagMenu.handleInput(mockInput)
mockInput:press("left")
BagMenu.handleInput(mockInput)
BagMenu.settle()
assert(BagMenu.currentPocket() == "ITEMS", "Swapped back to ITEMS pocket")
-- Close bag with B in list mode
mockInput:press("b")
BagMenu.handleInput(mockInput)
BagMenu.settle()
assert(BagMenu.isOpen() == false, "BagMenu closed on B in list mode")
print("[ok] Battle bag pocket navigation and action back-out passed")
+3
View File
@@ -58,6 +58,7 @@ BagMenu.show(testBag, {
session = session,
onClose = function() closed = true end,
})
BagMenu.settle()
check(BagMenu.isOpen() == true, "bag menu is open")
check(BagMenu.currentPocket() == "ITEMS", "starts in ITEMS pocket")
@@ -83,8 +84,10 @@ mockInput:set("right")
BagMenu.handleInput(mockInput)
check(BagMenu.currentPocket() == "POKE_BALLS", "right advances to POKE_BALLS pocket")
BagMenu.settle()
mockInput:set("b")
BagMenu.handleInput(mockInput)
BagMenu.settle()
check(BagMenu.isOpen() == false, "B closes bag menu")
check(closed == true, "onClose called")
+47 -32
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env luajit
-- Comprehensive unit and integration test for:
-- 1. StepEvents Engine (lockstep queue, lethal poison, whiteout flush, repel, happiness, egg cycles, vs seeker charge)
-- 2. VS Seeker Engine (battery charge across movement modes, indoor rejection, outdoor ping, battery drain)
-- 2. VS Seeker Engine (battery charge only with the item in the bag, map gate, uncharged use)
-- 3. TM Case & Berry Pouch Sub-Containers & Bag state persistence
package.path = "./?.lua;./?/init.lua;" .. package.path
@@ -64,13 +64,20 @@ check(not StepEvents.busy(), "Queue drained, StepEvents is no longer busy")
print("=== 2. VS Seeker Battery Management across Locomotion Modes ===")
local session = {
vsSeekerCharge = 0,
repelSteps = 10,
bag = Bag.new(),
party = {
{ species = 25, nickname = "PIKACHU", hp = 20, maxHp = 20, friendship = 70 },
},
}
for i = 1, 5 do
StepEvents.onStep(session, {})
end
check(VsSeeker.getBattery(session) == 0, "VS Seeker does not charge while it is not in the bag")
session.repelSteps = 10
Bag.add(session.bag, ItemsData.ITEM_VS_SEEKER, 1)
-- Step counting across locomotion modes
for i = 1, 50 do
StepEvents.onStep(session, { running = false, biking = false, surfing = false })
@@ -98,11 +105,11 @@ check(session.repelSteps == 0, "Repel wore off after 10 steps (started at 10)")
check(StepEvents.busy(), "Repel wear-off event queued")
local handledRepel = false
local mockHud = {
showDialogue = function(msg, opts)
openMessage = function(_game, msg, opts)
if msg:find("Repel's effect wore off") then
handledRepel = true
end
if opts and opts.onDone then opts.onDone() end
if opts and opts.done then opts.done() end
end
}
package.loaded["src.ui.game3.hud"] = mockHud
@@ -144,11 +151,11 @@ check(poisonedMon2.hp == 0, "Poisoned mon 2 dropped to 0 HP and fainted")
-- Process queued poison faint dialogue
local faintedMsg = false
mockHud.showDialogue = function(msg, opts)
mockHud.openMessage = function(_game, msg, opts)
if msg:find("CHAR fainted") or msg:find("fainted") then
faintedMsg = true
end
if opts and opts.onDone then opts.onDone() end
if opts and opts.done then opts.done() end
end
StepEvents.update(0.1)
check(faintedMsg, "Lethal poison faint event processed")
@@ -167,36 +174,42 @@ StepEvents.update(0.1)
check(whiteoutTriggered, "Whiteout triggered and StepEvents queue cleanly flushed")
check(not StepEvents.busy(), "StepEvents queue completely empty after whiteout")
print("=== 6. VS Seeker Outdoor Ping & Indoor Rejection ===")
print("=== 6. VS Seeker Map Gate & Uncharged Use ===")
local mapType = 8
package.loaded["src.core.game3.map"] = {
current = "FR_ROUTE_8",
currentDef = function() return { mapType = mapType } end,
}
session.map = "FR_ROUTE_8"
session.name = "RED"
session.bag = Bag.new()
Bag.add(session.bag, ItemsData.ITEM_VS_SEEKER, 1)
VsSeeker.setBattery(session, 100)
session.map = "VIRIDIAN_FOREST" -- Invalid indoor/cave map
local indoorDenied = false
mockHud.showDialogue = function(msg, opts)
if msg:find("can't be used here") then
indoorDenied = true
end
if opts and opts.onDone then opts.onDone() end
local okIndoor, kindIndoor, textIndoor = ItemUse.useField(session, session.bag, ItemsData.ITEM_VS_SEEKER)
check(not okIndoor and kindIndoor == "vs_seeker", "VS Seeker refused on an indoor map type")
check(textIndoor == "OAK: RED!\nThis isn't the time to use that!", "refusal uses the OAK forbids text")
check(VsSeeker.getBattery(session) == 100, "Battery preserved at 100 on refusal")
mapType = 3
local okRoute, kindRoute = ItemUse.useField(session, session.bag, ItemsData.ITEM_VS_SEEKER)
check(okRoute and kindRoute == "vs_seeker", "VS Seeker accepted on a ROUTE map type")
check(VsSeeker.mapAllowed("FR_ROUTE_8", 1) and VsSeeker.mapAllowed("FR_ROUTE_8", 2), "TOWN and CITY map types allowed")
check(not VsSeeker.mapAllowed("FR_ROUTE_8", 4), "UNDERGROUND map type refused")
check(not VsSeeker.mapAllowed("ViridianForest", 3), "Viridian Forest excluded by name")
VsSeeker.setBattery(session, 40)
local notChargedMsg
mockHud.openMessage = function(_game, msg, opts)
notChargedMsg = msg
if opts and opts.done then opts.done() end
end
local okUse = VsSeeker.use(session, nil)
check(not okUse and indoorDenied, "VS Seeker rejected in indoor/forest map")
check(VsSeeker.getBattery(session) == 100, "Battery preserved at 100 on indoor rejection")
-- Outdoor route with no trainers
session.map = "MAP_ROUTE_1"
local noResponseMsg = false
mockHud.showDialogue = function(msg, opts)
if msg:find("no response") then
noResponseMsg = true
end
if opts and opts.onDone then opts.onDone() end
end
local okOutdoor = VsSeeker.use(session, nil)
check(not okOutdoor and noResponseMsg, "Outdoor ping returned 'There is no response...'")
check(VsSeeker.getBattery(session) == 0, "Battery drained to 0 on valid outdoor radar ping")
local okUse, code = VsSeeker.use(session, nil)
check(not okUse and code == VsSeeker.NOT_CHARGED, "uncharged VS Seeker reports NOT_CHARGED")
check(notChargedMsg and notChargedMsg:find("charge the battery: 60", 1, true) ~= nil, "uncharged text prints 100 - charge")
check(VsSeeker.getBattery(session) == 40, "uncharged use does not drain the battery")
package.loaded["src.core.game3.map"] = nil
print("=== 7. TM Case & Berry Pouch Sub-Containers & Bag State Persistence ===")
@@ -229,6 +242,7 @@ check(not BerryPouch.isOpen(), "BerryPouch closed cleanly")
-- BagMenu sub-container transition & state preservation
BagMenu.show(session, { bag = bag })
BagMenu.settle()
BagMenu.pocketIdx = 2 -- KEY_ITEMS
BagMenu.cursor = 1
BagMenu.scroll = 0
@@ -246,6 +260,7 @@ local mockInput = {
end
}
BagMenu.handleInput(mockInput)
BagMenu.settle()
check(TmCase.isOpen(), "Using TM CASE from BagMenu opens TmCase UI")
-- Close TM Case and verify BagMenu state restored
+183
View File
@@ -175,4 +175,187 @@ assert_eq(Message._colors.fg, FrlgFont.STDPAL[1], "Battle message gets WHITE tex
Message.close()
print("[ok] Message.show color resolution verified")
print("=== [TEST 7] NPC_TEXT_COLOR enum + full sTextColorTable (pret numbering) ===")
-- include/constants/vars.h:340
assert_eq(FrlgFont.NPC_TEXT_COLOR.MALE, 0, "MALE is 0")
assert_eq(FrlgFont.NPC_TEXT_COLOR.FEMALE, 1, "FEMALE is 1")
assert_eq(FrlgFont.NPC_TEXT_COLOR.MON, 2, "MON is 2")
assert_eq(FrlgFont.NPC_TEXT_COLOR.NEUTRAL, 3, "NEUTRAL is 3")
assert_eq(FrlgFont.NPC_TEXT_COLOR.DEFAULT, 255, "DEFAULT is 255")
-- src/dynamic_placeholder_text_util.c:9
local PRET_TABLE = {
0x00, 0x00, 0x00, 0x10, 0x11, 0x11, 0x11, 0x10, 0x10, 0x00, 0x00, 0x11,
0x01, 0x00, 0x11, 0x10, 0x00, 0x10, 0x10, 0x00, 0x01, 0x01, 0x01, 0x01,
0x01, 0x01, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x11, 0x01, 0x00, 0x00,
0x00, 0x10, 0x11, 0x00, 0x10, 0x10, 0x10, 0x00, 0x01, 0x00, 0x33, 0x33,
0x33, 0x33, 0x33, 0x33, 0x33, 0x33, 0x23, 0x22, 0x22, 0x22, 0x22, 0x22,
0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22, 0x22,
0x22, 0x22, 0x22, 0x32,
}
assert_eq(#PRET_TABLE, 76, "pret table has 76 bytes")
for gfx = 0, 151 do
local b = PRET_TABLE[math.floor(gfx / 2) + 1]
local want = (gfx % 2 == 0) and (b % 16) or math.floor(b / 16)
assert_eq(FrlgFont.getNpcTextColor(gfx), want, "gfx " .. gfx .. " color")
end
assert_eq(FrlgFont.getNpcTextColor(152), 3, "past table is NEUTRAL (3)")
assert_eq(FrlgFont.getNpcTextColor(108), FrlgFont.NPC_TEXT_COLOR.NEUTRAL, "SEAGALLOP is NEUTRAL")
assert_eq(FrlgFont.getNpcTextColor(150), FrlgFont.NPC_TEXT_COLOR.MON, "DEOXYS_N is MON")
assert_eq(FrlgFont.getNpcTextColor(151), FrlgFont.NPC_TEXT_COLOR.NEUTRAL, "SS_ANNE is NEUTRAL")
assert_eq(FrlgFont.getNpcTextColor(95), FrlgFont.NPC_TEXT_COLOR.NEUTRAL, "CUT_TREE is NEUTRAL")
print("[ok] enum and all 152 table entries match pret")
print("=== [TEST 8] Message.show npcColor (field_message_box.c:103) ===")
Message.show("x", { npcColor = 0 })
assert_eq(Message._colors.fg, FrlgFont.STDPAL[8], "npcColor MALE is BLUE")
Message.show("x", { npcColor = 1 })
assert_eq(Message._colors.fg, FrlgFont.STDPAL[4], "npcColor FEMALE is RED")
Message.show("x", { npcColor = 2 })
assert_eq(Message._colors.fg, FrlgFont.STDPAL[2], "npcColor MON is DARK_GRAY")
Message.show("x", { npcColor = 3 })
assert_eq(Message._colors.fg, FrlgFont.STDPAL[2], "npcColor NEUTRAL is DARK_GRAY")
Message.close()
print("[ok] npcColor maps to pret printer colors")
print("=== [TEST 9] VAR_TEXT_COLOR / ContextNpcGetTextColor resolver ===")
pcall(function() require("src.core.GameVersion").set("firered") end)
local Ctx = require("src.core.game3.scripting.ctx")
local Flags = require("src.core.game3.scripting.flags")
local Adapters = require("src.core.game3.scripting.adapters")
local Vm = require("src.core.game3.scripting.vm")
local Std = require("src.core.game3.scripting.stdscripts")
local c0 = Ctx.new()
assert_eq(c0.specialVars[0x8012], 255, "fresh ctx VAR_TEXT_COLOR is DEFAULT")
c0.specialVars[0x8012] = 1
Ctx.wipeSpecial(c0)
assert_eq(c0.specialVars[0x8012], 255, "wipeSpecial reseeds DEFAULT")
local realObjects = package.loaded["src.core.game3.objects"]
local fakeObjs = {
[1] = { localId = 1, graphicsId = 64 }, -- NURSE
[2] = { localId = 2, graphicsId = 71 }, -- PROF_OAK
[3] = { localId = 3, graphicsId = 92 }, -- ITEM_BALL
[4] = { localId = 4, graphicsId = 120 }, -- PIKACHU
[5] = { localId = 5, graphicsId = 240 }, -- OBJ_EVENT_GFX_VAR_0
}
package.loaded["src.core.game3.objects"] = {
find = function(lid) return fakeObjs[tonumber(lid)] end,
isPlayer = function(lid) return tonumber(lid) == 255 end,
}
local r = Ctx.new()
assert_eq(Adapters.resolveNpcColor(r), 3, "no selection + DEFAULT is NEUTRAL")
Ctx.selectObject(r, 1)
assert_eq(Adapters.resolveNpcColor(r), 1, "nurse selected is FEMALE")
Ctx.selectObject(r, 2)
assert_eq(Adapters.resolveNpcColor(r), 0, "oak selected is MALE")
Ctx.selectObject(r, 3)
assert_eq(Adapters.resolveNpcColor(r), 3, "item ball selected is NEUTRAL")
Ctx.selectObject(r, 4)
assert_eq(Adapters.resolveNpcColor(r), 2, "pikachu selected is MON")
Ctx.selectObject(r, 1)
r.specialVars[0x8012] = 0
assert_eq(Adapters.resolveNpcColor(r), 0, "explicit textcolor beats sprite")
r.specialVars[0x8012] = 255
Ctx.selectObject(r, 5)
local gfxStore = Flags.newStore()
Flags.setVar(gfxStore, r, 0x4010, 64)
assert_eq(Adapters.resolveNpcColor(r, gfxStore), 1, "OBJ_EVENT_GFX_VAR_0 resolves through VAR_OBJ_GFX_ID_0")
Ctx.selectObject(r, 1)
fakeObjs[1] = nil
assert_eq(Adapters.resolveNpcColor(r), 1, "removed object keeps its cached sprite color")
fakeObjs[1] = { localId = 1, graphicsId = 64 }
Ctx.selectObject(r, 0)
assert_eq(Adapters.resolveNpcColor(r), 3, "cleared selection is NEUTRAL")
print("[ok] resolver mirrors field_specials.c:1548")
print("=== [TEST 10] textcolor saves prev, restore, release keeps color ===")
local TextIR2 = require("src.core.game3.scripting.text_ir")
local seen = {}
local vmRef
local adapters = Adapters.stub({
onMessage = function() seen[#seen + 1] = Adapters.resolveNpcColor(vmRef.ctx) end,
})
local scripts = {}
for k, v in pairs(Std.SCRIPTS) do scripts[k] = v end
scripts.test_npc = {
{ op = "message", ptr = "T" },
{ op = "textcolor", color = 3 },
{ op = "message", ptr = "T" },
{ op = "call", target = "EventScript_RestorePrevTextColor" },
{ op = "message", ptr = "T" },
{ op = "textcolor", color = 0 },
{ op = "closemessage" },
{ op = "release" },
{ op = "message", ptr = "T" },
{ op = "end" },
}
scripts.test_sign = {
{ op = "message", ptr = "T" },
{ op = "end" },
}
vmRef = Vm.new({ scripts = scripts, text = { T = TextIR2.fromAscii("hi") }, adapters = adapters })
vmRef:startTalk("test_npc", 1, 1)
assert_eq(seen[1], 1, "nurse line is FEMALE")
assert_eq(seen[2], 3, "forced NEUTRAL line")
assert_eq(seen[3], 1, "RestorePrevTextColor returns to DEFAULT, nurse FEMALE again")
assert_eq(seen[4], 0, "closemessage/release keep textcolor")
assert_eq(vmRef.ctx.specialVars[0x8012], 255, "script end resets VAR_TEXT_COLOR to DEFAULT")
assert_eq(vmRef.ctx.selectedLocalId, nil, "script end clears selection")
seen = {}
vmRef:startTalk("test_sign", 0, 1)
assert_eq(seen[1], 3, "sign/trigger (LAST_TALKED 0) is NEUTRAL")
print("[ok] textcolor / RestorePrevTextColor / reset rules")
print("=== [TEST 11] std bookends (obtain_item.inc:10, std_msgbox.inc:29) ===")
local s0 = Std.SCRIPTS["std:0"]
assert_eq(s0[1].op, "copyvar", "std:0 saves text color first")
assert_eq(s0[1][1], 0x8013, "std:0 copies into VAR_PREV_TEXT_COLOR")
assert_eq(s0[1][2], 0x8012, "std:0 copies from VAR_TEXT_COLOR")
assert_eq(s0[2].op, "textcolor", "std:0 forces textcolor")
assert_eq(s0[2].color, 3, "std:0 forces NEUTRAL")
assert_eq(s0[#s0].op, "return", "std:0 returns")
assert_eq(s0[#s0 - 1].op, "copyvar", "std:0 restores before return")
assert_eq(s0[#s0 - 1][1], 0x8012, "std:0 restores VAR_TEXT_COLOR")
assert_eq(s0[#s0 - 1][2], 0x8013, "std:0 restores from VAR_PREV_TEXT_COLOR")
local s9 = Std.SCRIPTS["std:9"]
assert_eq(s9[1].op, "textcolor", "std:9 starts with textcolor")
assert_eq(s9[1].color, 3, "std:9 forces NEUTRAL")
local show = Std.SCRIPTS.EventScript_ReceivedItemShowMsg
assert_eq(show[#show - 1].target, "EventScript_RestorePrevTextColor", "std:9 restores after putitemaway")
seen = {}
scripts.test_received = {
{ op = "textcolor", color = 0 },
{ op = "message", ptr = "T" },
{ op = "textcolor", color = 3 },
{ op = "message", ptr = "T" },
{ op = "call", target = "EventScript_RestorePrevTextColor" },
{ op = "message", ptr = "T" },
{ op = "end" },
}
vmRef:startTalk("test_received", 3, 1)
assert_eq(seen[1], 0, "cutscene speaker MALE")
assert_eq(seen[2], 3, "{PLAYER} received line NEUTRAL")
assert_eq(seen[3], 0, "speaker MALE restored")
print("[ok] std bookends match pret")
print("=== [TEST 12] trainerbattle stamps trainer as selected (battle_setup.c:778) ===")
local store = vmRef.store
Flags.setFlag(store, vmRef.ctx, Flags.trainerFlagId(102), true)
scripts.test_trainer = {
{ op = "trainerbattle", type = 0, trainer = 102, localId = 1, [1] = 102, [2] = 1 },
{ op = "message", ptr = "T" },
{ op = "end" },
}
seen = {}
vmRef:startTalk("test_trainer", 0, 1)
assert_eq(seen[1], 1, "post-battle text keeps the trainer's sprite color")
print("[ok] trainerbattle keeps trainer color")
package.loaded["src.core.game3.objects"] = realObjects
print("\nALL TEXT COLORS & ROM PARITY TESTS PASSED CLEANLY!")
+225
View File
@@ -0,0 +1,225 @@
#!/usr/bin/env luajit
package.path = "./?.lua;./?/init.lua;" .. package.path
local failed = 0
local function check(cond, msg)
if cond then
print("[ok] " .. msg)
else
failed = failed + 1
print("[FAIL] " .. msg)
end
end
local Flags = require("src.core.game3.scripting.flags")
local store = Flags.newStore()
package.loaded["src.core.game3.scripting.space"] = { store = store }
local Rng = require("src.core.game3.rng")
local Data = require("src.core.game3.vs_seeker_data")
local VsSeeker = require("src.core.game3.vs_seeker")
local session = { flags = {}, vars = {} }
local s = VsSeeker.state(session)
local function fightFlag(id, on) Flags.setFlag(store, nil, Flags.trainerFlagId(id), on ~= false) end
local function sysFlag(id, on) Flags.setFlag(store, nil, id, on ~= false) end
local function resetFlags()
store.flags = {}
end
local BEN, BEN_2, BEN_3 = 89, 101, 498
local MEGAN, MEGAN_2 = 130, 648
local TWINS, TWINS_2 = 484, 533
print("=== table ===")
check(#Data.REMATCHES == 221, "sRematches has 221 rows")
check(Data.REMATCHES[Data.byBase[TWINS]][4] == TWINS_2, "twins row carries TWINS_ELI_ANNE_2 in column 3")
check(Data.byAny[TWINS_2] == Data.byBase[TWINS], "rematch id maps back to its base row")
print("=== GetNextAvailableRematchTrainer ===")
local calvin = Data.REMATCHES[2][1]
check(Data.REMATCHES[2][2] == calvin, "row 2 is a {X, X} row")
fightFlag(calvin)
check(VsSeeker.nextAvailable(calvin, store) == 1, "{X, X} row returns 1")
check(VsSeeker.rematchTrainerId(calvin, store) == calvin, "{X, X} rematch without the VS SEEKER flag steps down to column 0")
sysFlag(0x292)
check(VsSeeker.rematchTrainerId(calvin, store) == calvin, "{X, X} rematch with VS SEEKER flag uses the same id")
fightFlag(BEN)
check(VsSeeker.nextAvailable(BEN, store) == 1, "BEN row returns 1 after the base fight")
fightFlag(BEN_2)
check(VsSeeker.nextAvailable(BEN, store) == 3, "BEN row skips SKIP and returns 3 after BEN_2")
check(VsSeeker.rematchTrainerId(BEN, store) == BEN_2, "FUCHSIA flag unset steps tier 3 down to BEN_2")
sysFlag(0x897)
check(VsSeeker.rematchTrainerId(BEN, store) == BEN_3, "FUCHSIA flag set gives BEN_3")
check(VsSeeker.nextAvailable(9999, store) == 0, "trainer outside the table returns 0")
fightFlag(MEGAN)
sysFlag(0x896)
check(VsSeeker.rematchTrainerId(MEGAN, store) == MEGAN_2, "MEGAN tier 2 with CELADON flag is MEGAN_2")
sysFlag(0x896, false)
check(VsSeeker.rematchTrainerId(MEGAN, store) == MEGAN, "MEGAN tier 2 without CELADON steps down to the base party")
print("=== ShouldTryRematchBattle / IsTrainerReadyForRematch ===")
resetFlags()
check(not VsSeeker.shouldTryRematchBattle(BEN, 5, store, session), "unfought, not ready: no rematch branch")
fightFlag(BEN)
check(VsSeeker.shouldTryRematchBattle(BEN, 5, store, session), "base flag set: rematch branch taken")
check(not VsSeeker.shouldTryRematchBattle(9999, 5, store, session), "trainer outside the table never branches")
check(not VsSeeker.shouldTryRematchBattle(BEN_2, 5, store, session), "only column-0 ids branch")
s.rematches = { [5] = 1 }
check(VsSeeker.isTrainerReadyForRematch(BEN, 5, session), "ready when lastTalked has a rematch state")
check(not VsSeeker.isTrainerReadyForRematch(BEN, 6, session), "not ready for another localId")
check(VsSeeker.isTrainerReadyForRematch(BEN_2, 5, session), "rematch ids hit the table in any column")
VsSeeker.clearRematchStateOfLastTalked(5, BEN_2, store, session)
check(VsSeeker.getRematch(s, 5) == 0, "ClearRematchStateOfLastTalked clears the state")
check(Flags.getFlag(store, nil, Flags.trainerFlagId(BEN_2)), "and sets the rematch trainer flag")
print("=== GetVsSeekerResponseInArea ===")
local function firstRoll(target)
for seed = 0, 65535 do
Rng.SeedRng(seed)
if Rng.Random() % 100 == target then return seed end
end
end
resetFlags()
fightFlag(BEN)
sysFlag(0x292)
local single = { { localId = 3, trainerIdx = BEN, x = 2, y = 2, graphicsId = 18, sane = true } }
local seed29, seed30 = firstRoll(29), firstRoll(30)
s.rematches = {}
Rng.SeedRng(seed29)
local code = VsSeeker.computeResponse(single, 0, 0, s, store)
check(code == VsSeeker.RESPONSE_NO_RESPONSE and VsSeeker.getRematch(s, 3) == 0, "rval 29 refuses")
check(not Flags.getFlag(store, nil, 0x801), "refusal does not set the charging flag")
s.rematches = {}
s.charging = 7
Rng.SeedRng(seed30)
local actions, responders
code, actions, responders = VsSeeker.computeResponse(single, 0, 0, s, store)
check(code == VsSeeker.RESPONSE_FOUND_REMATCHES and VsSeeker.getRematch(s, 3) == 1, "rval 30 accepts and stamps j")
check(Flags.getFlag(store, nil, 0x801) and s.charging == 0, "accept sets the charging flag and resets the counter")
check(actions[1].rematch and responders[1].behavior == 0x4E, "youngster responds with RAISE_HAND_AND_JUMP")
local far = { { localId = 3, trainerIdx = BEN, x = 8, y = 0, graphicsId = 18, sane = true },
{ localId = 4, trainerIdx = BEN, x = 0, y = 6, graphicsId = 18, sane = true } }
Rng.SeedRng(seed30)
code = VsSeeker.computeResponse(far, 0, 0, s, store)
check(code == VsSeeker.RESPONSE_NO_RESPONSE and Rng._value == seed30, "outside the 7x5 box: skipped, no RNG")
resetFlags()
local unfought = { { localId = 3, trainerIdx = BEN, x = 1, y = 1, graphicsId = 18, sane = true } }
Rng.SeedRng(1)
code, actions = VsSeeker.computeResponse(unfought, 0, 0, s, store)
check(code == VsSeeker.RESPONSE_UNFOUGHT_TRAINERS and actions[1].movement[1] == 0x62, "unfought trainer gets ! and no RNG")
check(Rng._value == 1, "unfought branch does not consume RNG")
resetFlags()
fightFlag(TWINS)
sysFlag(0x292)
sysFlag(0x896)
sysFlag(0x897)
local twins = {
{ localId = 10, trainerIdx = TWINS, x = 0, y = -2, graphicsId = 17, sane = true },
{ localId = 11, trainerIdx = TWINS, x = 1, y = -2, graphicsId = 17, sane = true },
}
local mirrored, sawYes, sawNo = true, false, false
for seed = 1, 40 do
s.rematches = {}
Rng.SeedRng(seed)
local c, acts = VsSeeker.computeResponse(twins, 0, 0, s, store)
local a1, a2 = acts[1].rematch == true, acts[2].rematch == true
if a1 ~= a2 then mirrored = false end
if a1 then
sawYes = true
if VsSeeker.getRematch(s, 10) ~= 3 or VsSeeker.getRematch(s, 11) ~= 3 then mirrored = false end
if c ~= VsSeeker.RESPONSE_FOUND_REMATCHES then mirrored = false end
else
sawNo = true
end
Rng.SeedRng(seed)
Rng.Random()
Rng.Random()
local expect = Rng._value
Rng.SeedRng(seed)
s.rematches = {}
VsSeeker.computeResponse(twins, 0, 0, s, store)
if Rng._value ~= expect then mirrored = false end
end
check(mirrored, "twin pair mirrors the first twin's answer and consumes one Random() each")
check(sawYes and sawNo, "seed sweep covered both answers")
print("=== CanUseVsSeeker ===")
s.steps = 99
check(VsSeeker.canUse(twins, 0, 0, s, store) == VsSeeker.NOT_CHARGED, "99 steps is not charged")
s.steps = 100
check(VsSeeker.canUse(twins, 0, 0, s, store) == VsSeeker.CAN_USE, "charged with a rematchable twin in range")
check(VsSeeker.canUse(twins, 20, 0, s, store) == VsSeeker.NO_ONE_IN_RANGE, "no one in range")
print("=== step counter ===")
local Bag = require("src.core.game3.bag")
session.bag = Bag.new()
Bag.add(session.bag, 374, 1)
s.steps = 99
s.charging = 98
s.rematches = { [10] = 3 }
sysFlag(0x801)
check(not VsSeeker.onStep(session, store) and s.steps == 100 and s.charging == 99, "step 99 charges both bytes")
check(VsSeeker.onStep(session, store), "charging reaches 100 and fires the charging-done event")
check(not Flags.getFlag(store, nil, 0x801) and s.charging == 0, "charging flag and counter cleared")
check(next(s.rematches) == nil, "all rematch states cleared")
check(s.steps == 100, "in-bag charge caps at 100")
print("=== object movement ===")
check(VsSeeker.runningBehavior(22) == 0x4E and VsSeeker.runningBehavior(43) == 0x4F
and VsSeeker.runningBehavior(5) == 0x4D, "GetRunningBehaviorFromGraphicsId")
local live, template = {}, {}
local stubObjects = {
_defs = {
{ localId = 10, trainerType = 1, trainerId = TWINS, movementType = 8, graphicsId = 17 },
{ localId = 11, trainerType = 1, trainerId = TWINS, movementType = 8, graphicsId = 17 },
{ localId = 12, trainerType = 0, trainerId = TWINS, movementType = 8, graphicsId = 17 },
},
_byId = {
[10] = { localId = 10, facing = "left", visible = true },
[11] = { localId = 11, facing = "down", visible = true },
},
_order = { 10, 11 },
setTrainerMovementType = function(lid, mt)
if type(lid) == "table" then lid = lid.localId end
live[lid] = mt
end,
overrideTemplateMovementType = function(lid, mt) template[lid] = mt end,
templateMovementType = function(lid) return template[lid] or 8 end,
}
s.rematches = { [10] = 3, [11] = 3 }
Rng.SeedRng(77)
VsSeeker.clearRematchStateByTrainerId(TWINS_2, 10, store, session, stubObjects)
Rng.SeedRng(77)
Rng.Random()
Rng.Random()
local afterTwo = Rng._value
Rng.SeedRng(77)
VsSeeker.clearRematchStateByTrainerId(TWINS_2, 10, store, session, stubObjects)
check(Rng._value == afterTwo, "ClearRematchStateByTrainerId consumes one Random() per matching trainer template")
check(VsSeeker.getRematch(s, 10) == 0 and VsSeeker.getRematch(s, 11) == 0, "both twins cleared by the rematch id")
check(live[10] == 9 and live[11] == 8, "selected twin keeps its facing type, the other gets FACE_DOWN")
check(template[10] == 9 and template[11] == 8, "templates take each twin's facing type")
live, template = {}, { [10] = 0x4E, [11] = 0x4E }
Rng.SeedRng(5)
VsSeeker.resetObjectMovementAfterChargeComplete(stubObjects)
local face = { [7] = true, [8] = true, [9] = true, [10] = true }
check(face[live[10]] and face[live[11]] and face[template[10]] and face[template[11]],
"charge-complete reset replaces raise-hand types with random face types")
if failed == 0 then
print("ALL TESTS PASSED")
else
print(string.format("TESTS FAILED: %d", failed))
os.exit(1)
end