First commit 🎉

This commit is contained in:
Tony Bark 2025-07-17 01:49:18 -04:00
commit 43ea213f9b
728 changed files with 37080 additions and 0 deletions

4
.editorconfig Normal file
View file

@ -0,0 +1,4 @@
root = true
[*]
charset = utf-8

2
.gitattributes vendored Normal file
View file

@ -0,0 +1,2 @@
# Normalize EOL for all files that Git considers text files.
* text=auto eol=lf

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
# Godot 4+ specific ignores
.godot/
/android/

674
LICENSE Normal file
View file

@ -0,0 +1,674 @@
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>.

21
addons/popochiu/LICENSE Normal file
View file

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

View file

@ -0,0 +1,332 @@
@tool
extends HBoxContainer
## Used to show new buttons in the EditorPlugin.CONTAINER_CANVAS_EDITOR_MENU (the top bar in the
## 2D editor) to select specific nodes in PopochiuClickable objects.
var _active_popochiu_object: Node = null
var _shown_helpers := []
@onready var btn_baseline: Button = %BtnBaseline
@onready var btn_walk_to_point: Button = %BtnWalkToPoint
@onready var btn_look_at_point: Button = %BtnLookAtPoint
@onready var btn_dialog_pos: Button = %BtnDialogPos
@onready var btn_interaction_polygon: Button = %BtnInteractionPolygon
#region Godot ######################################################################################
func _ready() -> void:
# Gizmos are always visible at editor load, so we'll set the buttons down
# to sync the status (hardcoded, not very good but enough for now)
_reset_buttons_state()
# Connect to child signals
btn_baseline.pressed.connect(_toggle_baseline_visibility)
btn_walk_to_point.pressed.connect(_toggle_walk_to_point_visibility)
btn_look_at_point.pressed.connect(_toggle_look_at_point_visibility)
btn_dialog_pos.pressed.connect(_toggle_dialog_pos_visibility)
btn_interaction_polygon.pressed.connect(_select_interaction_polygon)
# Connect to singleton signals
EditorInterface.get_selection().selection_changed.connect(_on_selection_changed)
EditorInterface.get_editor_settings().settings_changed.connect(_on_gizmo_settings_changed)
_set_toolbar_buttons_color()
hide()
#endregion
#region Private ####################################################################################
func _toggle_walk_to_point_visibility() -> void:
PopochiuEditorHelper.signal_bus.gizmo_visibility_changed.emit(
PopochiuGizmoClickablePlugin.WALK_TO_POINT,
btn_walk_to_point.button_pressed
)
func _toggle_look_at_point_visibility() -> void:
PopochiuEditorHelper.signal_bus.gizmo_visibility_changed.emit(
PopochiuGizmoClickablePlugin.LOOK_AT_POINT,
btn_look_at_point.button_pressed
)
func _toggle_baseline_visibility() -> void:
PopochiuEditorHelper.signal_bus.gizmo_visibility_changed.emit(
PopochiuGizmoClickablePlugin.BASELINE,
btn_baseline.button_pressed
)
func _toggle_dialog_pos_visibility() -> void:
PopochiuEditorHelper.signal_bus.gizmo_visibility_changed.emit(
PopochiuGizmoClickablePlugin.DIALOG_POS,
btn_dialog_pos.button_pressed
)
func _select_interaction_polygon() -> void:
# Since we are going to select the interaction polygon node
# inside the node, let's hide the gizmos buttons
btn_walk_to_point.hide()
btn_baseline.hide()
# If we are editing the polygon, go back and select the parent node
# then stop execution.
var selected_node := EditorInterface.get_selection().get_selected_nodes()[0]
if PopochiuEditorHelper.is_popochiu_obj_polygon(
selected_node
):
EditorInterface.get_selection().add_node(selected_node.get_parent())
_on_selection_changed()
return
# If we are editing a popochiu object holding a polygon, let's move on.
# This variable will hold the reference to the polygon we need to edit.
var obj_polygon: Node2D = null
# Let's find the node holding the polygon
# Since different Popochiu Objects have different polygons (NavigationRegion2D
# for Walkable Areas, InteractionPolygon2D for props, etc...) we tagged them
# by a special metadata
obj_polygon = PopochiuEditorHelper.get_first_child_by_group(
_active_popochiu_object,
PopochiuEditorHelper.POPOCHIU_OBJECT_POLYGON_GROUP
)
if obj_polygon == null:
return
EditorInterface.get_selection().clear()
EditorInterface.get_selection().add_node(obj_polygon)
obj_polygon.show()
func _on_gizmo_settings_changed() -> void:
# Pretty self explanatory
_set_walkable_areas_visibility()
_set_toolbar_buttons_color()
func _on_selection_changed() -> void:
# Always reset the walkable areas visibility depending on the user preferences
# Doing this immediately so, if this function exits early, the visibility is conditioned
# by the editor settings (partially fixes #325).
_set_walkable_areas_visibility()
# Make sure this function works only if the user is editing a
# supported scene
if not PopochiuEditorHelper.is_popochiu_object(
EditorInterface.get_edited_scene_root()
):
hide()
return
# If we have no selection in the tree (the user clicked on an
# empty area or pressed ESC), we hide the toolbar.
if EditorInterface.get_selection().get_selected_nodes().is_empty():
if _active_popochiu_object != null:
# TODO: this is not a helper function, because we want to get
# rid of this ASAP. The same logic is also in the function
# _set_polygons_visibility() in the base Popochiu object
# factory, and should be removed as well.
for node in _active_popochiu_object.get_children():
if PopochiuEditorHelper.is_popochiu_obj_polygon(node):
node.hide()
# This "if" solves "!p_node->is_inside_tree()" internal Godot error
# The line inside is the logic we need to make this block work
if EditorInterface.get_edited_scene_root() == _active_popochiu_object:
EditorInterface.get_selection().add_node.call_deferred(_active_popochiu_object)
# Reset the clickable reference and hide the toolbar
# (restart from a blank state)
_active_popochiu_object = null
hide()
# NOTE: Here we used to pop all the buttons up, by invoking _reset_buttons_state() but
# this is undesirable, since it overrides the user's visibility choices for the session.
# Leaving this comment here for future reference.
# Reset the walkable areas visibility depending on the user preferences
# Doing here because clicking on an empty area would hide the walkable areas
# ignoring the editor settings (fixes #325)
_set_walkable_areas_visibility()
return
# We identify which PopochiuClickable we are working on in the editor.
# Case 1:
# There is only one selected node in the editor. It can be anything the user
# clicked on, or the polygon selected by clicking the toolbar button.
# (The user can never select the polygon directly because the node is not visible
# in the scene tree)
if EditorInterface.get_selection().get_selected_nodes().size() == 1:
var selected_node = EditorInterface.get_selection().get_selected_nodes()[0]
if PopochiuEditorHelper.is_popochiu_obj_polygon(selected_node):
_active_popochiu_object = selected_node.get_parent()
elif PopochiuEditorHelper.is_popochiu_room_object(selected_node):
var polygon = null
if is_instance_valid(_active_popochiu_object):
polygon = PopochiuEditorHelper.get_first_child_by_group(
_active_popochiu_object,
PopochiuEditorHelper.POPOCHIU_OBJECT_POLYGON_GROUP
)
if (polygon != null):
polygon.hide()
btn_interaction_polygon.set_pressed_no_signal(false)
_active_popochiu_object = selected_node
else:
_active_popochiu_object = null
# Case 2:
# We have more than one node selected. This can happen because the user selected
# more than one node explicitly (holding shift, or ctrl), or because the user selected
# one node in the scene while editing the polygon.
# In this case, since the polygon was selected programmatically and it's not in the scene
# tree, Godot will NOT remove it from selection and we need to do it by hand.
elif EditorInterface.get_selection().get_selected_nodes().size() > 1:
for node in EditorInterface.get_selection().get_selected_nodes():
if PopochiuEditorHelper.is_popochiu_obj_polygon(node):
node.hide()
EditorInterface.get_selection().remove_node.call_deferred(node)
btn_interaction_polygon.set_pressed_no_signal(false)
# Reset the walkable areas visibility depending on the user preferences
# Doing this also at the end because the state can be reset by one of the steps
# above.
_set_walkable_areas_visibility()
# Always reset the button visibility depending on the state of the internal variables
_set_buttons_visibility()
## Handles the editor config that allows the WAs polygons to be always visible,
## not only during editing.
func _set_walkable_areas_visibility() -> void:
for child in PopochiuEditorHelper.get_all_children(
EditorInterface.get_edited_scene_root().find_child("WalkableAreas")
):
# Not a polygon? Skip
if not PopochiuEditorHelper.is_popochiu_obj_polygon(child):
continue
# Should we show all the polygons? Show and go to the next one
if PopochiuEditorConfig.get_editor_setting(
PopochiuEditorConfig.GIZMOS_ALWAYS_SHOW_WA
):
child.show()
continue
# If we are editing the polygon, make sure it stays visible!
if child in EditorInterface.get_selection().get_selected_nodes():
child.show()
continue
# OK, we know we must hide this polygon now!
child.hide()
## Sets all the buttons color so that they are the same as the gizmos
## or make them theme-standard if the use so prefer (see editor settings)
func _set_toolbar_buttons_color() -> void:
if not PopochiuEditorConfig.get_editor_setting(PopochiuEditorConfig.GIZMOS_COLOR_TOOLBAR_BUTTONS):
# Reset button colors
_reset_toolbar_button_color(btn_baseline)
_reset_toolbar_button_color(btn_walk_to_point)
_reset_toolbar_button_color(btn_look_at_point)
_reset_toolbar_button_color(btn_dialog_pos)
_reset_toolbar_button_color(btn_interaction_polygon)
# Done
return
_set_toolbar_button_color(
btn_baseline,
PopochiuEditorConfig.get_editor_setting(
PopochiuEditorConfig.GIZMOS_BASELINE_COLOR)
)
_set_toolbar_button_color(
btn_walk_to_point,
PopochiuEditorConfig.get_editor_setting(
PopochiuEditorConfig.GIZMOS_WALK_TO_POINT_COLOR)
)
_set_toolbar_button_color(
btn_look_at_point,
PopochiuEditorConfig.get_editor_setting(
PopochiuEditorConfig.GIZMOS_LOOK_AT_POINT_COLOR)
)
_set_toolbar_button_color(
btn_dialog_pos,
PopochiuEditorConfig.get_editor_setting(
PopochiuEditorConfig.GIZMOS_DIALOG_POS_COLOR)
)
_set_toolbar_button_color(
btn_interaction_polygon,
Color.RED # no config for this at the moment
)
## Internal helper to reduce code duplication
func _set_toolbar_button_color(btn, color) -> void:
btn.add_theme_color_override("icon_normal_color", color)
btn.add_theme_color_override("icon_hover_color", color.lightened(1.0))
btn.add_theme_color_override("icon_focused_color", color.lightened(1.0))
btn.add_theme_color_override("icon_pressed_color", color.darkened(0.2))
btn.add_theme_color_override("icon_hover_pressed_color", color.lightened(1.0))
## Internal helper to reduce code duplication
func _reset_toolbar_button_color(btn) -> void:
btn.remove_theme_color_override("icon_normal_color")
btn.remove_theme_color_override("icon_hover_color")
btn.remove_theme_color_override("icon_focused_color")
btn.remove_theme_color_override("icon_pressed_color")
btn.remove_theme_color_override("icon_hover_pressed_color")
func _set_buttons_visibility() -> void:
# Let's assume the buttons are all hidden...
hide()
btn_baseline.hide()
btn_walk_to_point.hide()
btn_look_at_point.hide()
btn_dialog_pos.hide()
btn_interaction_polygon.hide()
# If we are not editing a Popochiu object, nothing to do
if not PopochiuEditorHelper.is_popochiu_room_object(_active_popochiu_object):
return
# Now we know we have to show the toolbar
show()
# Every Popochiu Object always shows the polygon editing button when edited
# unless we are in a room scene and selected a character
if not (
PopochiuEditorHelper.is_character(_active_popochiu_object)
and PopochiuEditorHelper.is_editing_room()
):
btn_interaction_polygon.show()
# If the selected node in the editor is actually a popochiu object polygon
# We don't have to show the other buttons, only the polygon editing toggle
if PopochiuEditorHelper.is_popochiu_obj_polygon(
EditorInterface.get_selection().get_selected_nodes()[0]
):
return
# If we are in a room scene, we may have selected a room object of sort, so check
# for the various types and hide the ones we don't need
if PopochiuEditorHelper.is_room(EditorInterface.get_edited_scene_root()):
# If we are editing a clickable object, let's show gizmos buttons too
if _active_popochiu_object is PopochiuClickable:
btn_baseline.show()
btn_walk_to_point.show()
btn_look_at_point.show()
# If we are in a Character scene, show polygon and dialogpos gizmo button
elif PopochiuEditorHelper.is_character(EditorInterface.get_edited_scene_root()):
btn_dialog_pos.show()
# Make all buttons pop-up
func _reset_buttons_state() -> void:
btn_baseline.set_pressed_no_signal(true)
btn_walk_to_point.set_pressed_no_signal(true)
btn_look_at_point.set_pressed_no_signal(true)
btn_dialog_pos.set_pressed_no_signal(true)

View file

@ -0,0 +1 @@
uid://15ys464umb3h

View file

@ -0,0 +1,65 @@
[gd_scene load_steps=7 format=3 uid="uid://wd748u1vdybq"]
[ext_resource type="Script" path="res://addons/popochiu/editor/canvas_editor_menu/popochiu_canvas_editor_menu.gd" id="1_vs7c6"]
[ext_resource type="Texture2D" uid="uid://b3sku5v1n23ni" path="res://addons/popochiu/icons/btn_baseline.svg" id="2_w3cau"]
[ext_resource type="Texture2D" uid="uid://dmt2epjmlpv56" path="res://addons/popochiu/icons/btn_walk_to_point.svg" id="3_ifql1"]
[ext_resource type="Texture2D" uid="uid://skjlvpct7ah7" path="res://addons/popochiu/icons/btn_look_at_point.svg" id="4_bge33"]
[ext_resource type="Texture2D" uid="uid://cekffh7bsuanp" path="res://addons/popochiu/icons/btn_dialog_position.svg" id="5_803mj"]
[ext_resource type="Texture2D" uid="uid://cyun4rylrtrvm" path="res://addons/popochiu/icons/btn_interaction_polygon.svg" id="6_6oxl8"]
[node name="PopochiuCanvasEditorMenu" type="HBoxContainer"]
visible = false
offset_right = 40.0
offset_bottom = 40.0
script = ExtResource("1_vs7c6")
[node name="Label" type="Label" parent="."]
layout_mode = 2
text = "Popochiu "
vertical_alignment = 1
[node name="BtnBaseline" type="Button" parent="."]
unique_name_in_owner = true
visible = false
layout_mode = 2
tooltip_text = "Baseline"
theme_type_variation = &"FlatButton"
toggle_mode = true
button_pressed = true
icon = ExtResource("2_w3cau")
[node name="BtnWalkToPoint" type="Button" parent="."]
unique_name_in_owner = true
visible = false
layout_mode = 2
tooltip_text = "Walk-To Point"
theme_type_variation = &"FlatButton"
toggle_mode = true
button_pressed = true
icon = ExtResource("3_ifql1")
[node name="BtnLookAtPoint" type="Button" parent="."]
unique_name_in_owner = true
layout_mode = 2
tooltip_text = "Look-At Point"
theme_type_variation = &"FlatButton"
toggle_mode = true
button_pressed = true
icon = ExtResource("4_bge33")
[node name="BtnDialogPos" type="Button" parent="."]
unique_name_in_owner = true
layout_mode = 2
tooltip_text = "Dialog Position"
theme_type_variation = &"FlatButton"
toggle_mode = true
button_pressed = true
icon = ExtResource("5_803mj")
[node name="BtnInteractionPolygon" type="Button" parent="."]
unique_name_in_owner = true
layout_mode = 2
tooltip_text = "Interaction Polygon"
theme_type_variation = &"FlatButton"
toggle_mode = true
icon = ExtResource("6_6oxl8")

View file

@ -0,0 +1,291 @@
@tool
class_name PopochiuConfig
extends RefCounted
enum DialogStyle {
ABOVE_CHARACTER,
PORTRAIT,
CAPTION,
#PORTRAIT_ABOVE_CHARACTER, # TODO: Create a GUI node to make this option available
#BUBBLE_ABOVE_CHARACTER, # TODO: Create a GUI node to make this option available
}
# Thanks to @drbloop for providing the bases of the new approach for moving the popochiu settings to
# Godot's ProjectSettings instead of using a Resource file.
# ---- GUI -----------------------------------------------------------------------------------------
const SCALE_GUI = "popochiu/gui/experimental_scale_gui"
const FADE_COLOR = "popochiu/gui/fade_color"
const SKIP_CUTSCENE_TIME = "popochiu/gui/skip_cutscene_time"
const TL_FIRST_ROOM = "popochiu/gui/show_transition_layer_in_first_room"
# ---- Dialogs -------------------------------------------------------------------------------------
const TEXT_SPEED = "popochiu/dialogs/text_speed"
const AUTO_CONTINUE_TEXT = "popochiu/dialogs/auto_continue_text"
const USE_TRANSLATIONS = "popochiu/dialogs/use_translations"
const GIBBERISH_SPOKEN_TEXT = 'popochiu/dialogs/gibberish_spoken_text'
const GIBBERISH_DIALOG_OPTIONS = 'popochiu/dialogs/gibberish_dialog_options'
const DIALOG_STYLE = "popochiu/dialogs/dialog_style"
# ---- Inventory -----------------------------------------------------------------------------------
const INVENTORY_LIMIT = "popochiu/inventory/inventory_limit"
const INVENTORY_ITEMS_ON_START = "popochiu/inventory/items_on_start"
# ---- Aseprite Importing --------------------------------------------------------------------------
const ASEPRITE_IMPORT_ANIMATION = "popochiu/aseprite_import/import_animation_by_default"
const ASEPRITE_LOOP_ANIMATION = "popochiu/aseprite_import/loop_animation_by_default"
const ASEPRITE_PROPS_VISIBLE = "popochiu/aseprite_import/new_props_visible_by_default"
const ASEPRITE_PROPS_CLICKABLE = "popochiu/aseprite_import/new_props_clickable_by_default"
const ASEPRITE_WIPE_OLD_ANIMATIONS = "popochiu/aseprite_import/wipe_old_animations"
# ---- Pixel game ----------------------------------------------------------------------------------
const PIXEL_ART_TEXTURES = "popochiu/pixel/pixel_art_textures"
const PIXEL_PERFECT = "popochiu/pixel/pixel_perfect"
# ---- Audio ---------------------------------------------------------------------------------------
const PREFIX_CHARACTER = "popochiu/audio/prefix_character"
const MUSIC_PREFIXES = "popochiu/audio/music_prefixes"
const SOUND_EFFECT_PREFIXES = "popochiu/audio/sound_effect_prefixes"
const VOICE_PREFIXES = "popochiu/audio/voice_prefixes"
const UI_PREFIXES = "popochiu/audio/ui_prefixes"
# ---- DEV -----------------------------------------------------------------------------------------
const DEV_USE_ADDON_TEMPLATE = "popochiu/dev/use_addon_template"
static var defaults := {
SCALE_GUI: false,
FADE_COLOR: Color.BLACK,
SKIP_CUTSCENE_TIME: 0.2,
TL_FIRST_ROOM: false,
TEXT_SPEED: 0.1,
AUTO_CONTINUE_TEXT: false,
USE_TRANSLATIONS: false,
GIBBERISH_SPOKEN_TEXT: false,
GIBBERISH_DIALOG_OPTIONS: false,
DIALOG_STYLE: DialogStyle.ABOVE_CHARACTER,
INVENTORY_LIMIT: 0,
INVENTORY_ITEMS_ON_START: [],
ASEPRITE_IMPORT_ANIMATION: true,
ASEPRITE_LOOP_ANIMATION: true,
ASEPRITE_PROPS_VISIBLE: true,
ASEPRITE_PROPS_CLICKABLE: true,
ASEPRITE_WIPE_OLD_ANIMATIONS: true,
PIXEL_ART_TEXTURES: false,
PIXEL_PERFECT: false,
PREFIX_CHARACTER: "_",
MUSIC_PREFIXES: "mx,",
SOUND_EFFECT_PREFIXES: "sfx,",
VOICE_PREFIXES: "vo,",
UI_PREFIXES: "ui,",
DEV_USE_ADDON_TEMPLATE: false,
}
#region Public #####################################################################################
static func initialize_project_settings():
# ---- GUI -------------------------------------------------------------------------------------
_initialize_project_setting(SCALE_GUI, TYPE_BOOL)
_initialize_project_setting(FADE_COLOR, TYPE_COLOR)
_initialize_project_setting(SKIP_CUTSCENE_TIME, TYPE_FLOAT)
_initialize_project_setting(TL_FIRST_ROOM, TYPE_BOOL)
# ---- Dialogs ---------------------------------------------------------------------------------
_initialize_project_setting(TEXT_SPEED, TYPE_FLOAT, PROPERTY_HINT_RANGE, "0.0,0.1")
_initialize_project_setting(AUTO_CONTINUE_TEXT, TYPE_BOOL)
#_initialize_project_setting(USE_TRANSLATIONS, TYPE_BOOL)
#_initialize_project_setting(
#DIALOG_STYLE,
#TYPE_INT,
#PROPERTY_HINT_ENUM,
## TODO: Add other options: Portrait Above Character, Bubble Above Character
#"Above Character,Portrait,Caption"
#)
_initialize_project_setting(GIBBERISH_SPOKEN_TEXT, TYPE_BOOL)
_initialize_project_setting(GIBBERISH_DIALOG_OPTIONS, TYPE_BOOL)
# ---- Inventory -------------------------------------------------------------------------------
_initialize_project_setting(INVENTORY_LIMIT, TYPE_INT)
_initialize_project_setting(
INVENTORY_ITEMS_ON_START,
TYPE_ARRAY,
PROPERTY_HINT_TYPE_STRING,
"%d:" % [TYPE_STRING]
)
# ---- Aseprite Importing ----------------------------------------------------------------------
_initialize_project_setting(ASEPRITE_IMPORT_ANIMATION, TYPE_BOOL)
_initialize_project_setting(ASEPRITE_LOOP_ANIMATION, TYPE_BOOL)
_initialize_project_setting(ASEPRITE_PROPS_VISIBLE, TYPE_BOOL)
_initialize_project_setting(ASEPRITE_PROPS_CLICKABLE, TYPE_BOOL)
_initialize_project_setting(ASEPRITE_WIPE_OLD_ANIMATIONS, TYPE_BOOL)
# ---- Pixel game ------------------------------------------------------------------------------
_initialize_project_setting(PIXEL_ART_TEXTURES, TYPE_BOOL)
_initialize_project_setting(PIXEL_PERFECT, TYPE_BOOL)
# ---- Audio -----------------------------------------------------------------------------------
_initialize_project_setting(PREFIX_CHARACTER, TYPE_STRING)
_initialize_project_setting(MUSIC_PREFIXES, TYPE_STRING)
_initialize_project_setting(SOUND_EFFECT_PREFIXES, TYPE_STRING)
_initialize_project_setting(VOICE_PREFIXES, TYPE_STRING)
_initialize_project_setting(UI_PREFIXES, TYPE_STRING)
# ---- DEV -------------------------------------------------------------------------------------
_initialize_advanced_project_setting(DEV_USE_ADDON_TEMPLATE, TYPE_BOOL)
ProjectSettings.save()
static func set_project_setting(key: String, value) -> void:
ProjectSettings.set_setting(key, value)
ProjectSettings.save()
# ---- GUI -----------------------------------------------------------------------------------------
static func is_scale_gui() -> bool:
return _get_project_setting(SCALE_GUI)
static func get_fade_color() -> Color:
return _get_project_setting(FADE_COLOR)
static func get_skip_cutscene_time() -> float:
return _get_project_setting(SKIP_CUTSCENE_TIME)
static func should_show_tl_in_first_room() -> bool:
return _get_project_setting(TL_FIRST_ROOM)
# ---- Dialogs -------------------------------------------------------------------------------------
static func get_text_speed() -> float:
return _get_project_setting(TEXT_SPEED)
static func is_auto_continue_text() -> bool:
return _get_project_setting(AUTO_CONTINUE_TEXT)
static func is_use_translations() -> bool:
return _get_project_setting(USE_TRANSLATIONS)
static func get_dialog_style() -> int:
return _get_project_setting(DIALOG_STYLE)
static func should_talk_gibberish() -> bool:
return _get_project_setting(GIBBERISH_SPOKEN_TEXT)
static func should_dialog_options_be_gibberish() -> bool:
return _get_project_setting(GIBBERISH_DIALOG_OPTIONS)
# ---- Inventory -----------------------------------------------------------------------------------
static func get_inventory_limit() -> int:
return _get_project_setting(INVENTORY_LIMIT)
static func set_inventory_items_on_start(items: Array) -> void:
set_project_setting(INVENTORY_ITEMS_ON_START, items)
static func get_inventory_items_on_start() -> Array:
return _get_project_setting(INVENTORY_ITEMS_ON_START)
# ---- Aseprite Importing --------------------------------------------------------------------------
static func is_default_animation_import_enabled() -> bool:
return _get_project_setting(ASEPRITE_IMPORT_ANIMATION)
static func is_default_animation_loop_enabled() -> bool:
return _get_project_setting(ASEPRITE_LOOP_ANIMATION)
static func is_default_animation_prop_visible() -> bool:
return _get_project_setting(ASEPRITE_PROPS_VISIBLE)
static func is_default_animation_prop_clickable() -> bool:
return _get_project_setting(ASEPRITE_PROPS_CLICKABLE)
static func is_default_wipe_old_anims_enabled() -> bool:
return _get_project_setting(ASEPRITE_WIPE_OLD_ANIMATIONS)
# ---- Pixel game ----------------------------------------------------------------------------------
static func set_pixel_art_textures(use_pixel_art_textures: bool) -> void:
set_project_setting(PIXEL_ART_TEXTURES, use_pixel_art_textures)
static func is_pixel_art_textures() -> bool:
return _get_project_setting(PIXEL_ART_TEXTURES)
static func is_pixel_perfect() -> bool:
return _get_project_setting(PIXEL_PERFECT)
# ---- Audio ---------------------------------------------------------------------------------------
static func get_prefix_character() -> String:
return _get_project_setting(PREFIX_CHARACTER)
static func get_music_prefixes() -> String:
return _get_project_setting(MUSIC_PREFIXES)
static func get_sound_effect_prefixes() -> String:
return _get_project_setting(SOUND_EFFECT_PREFIXES)
static func get_voice_prefixes() -> String:
return _get_project_setting(VOICE_PREFIXES)
static func get_ui_prefixes() -> String:
return _get_project_setting(UI_PREFIXES)
# ---- DEV -----------------------------------------------------------------------------------------
static func is_use_addon_template() -> bool:
return _get_project_setting(DEV_USE_ADDON_TEMPLATE)
#endregion
#region Private ####################################################################################
static func _initialize_project_setting(
key: String, type: int, hint := PROPERTY_HINT_NONE, hint_string := ""
) -> void:
_create_setting(key, type, hint, hint_string)
ProjectSettings.set_as_basic(key, true)
static func _initialize_advanced_project_setting(
key: String, type: int, hint := PROPERTY_HINT_NONE, hint_string := ""
) -> void:
_create_setting(key, type, hint, hint_string)
static func _create_setting(
key: String, type: int, hint := PROPERTY_HINT_NONE, hint_string := ""
) -> void:
ProjectSettings.set_setting(key, ProjectSettings.get_setting(key, defaults[key]))
ProjectSettings.set_initial_value(key, defaults[key])
ProjectSettings.add_property_info({
"name": key,
"type": type,
"hint": hint,
"hint_string": hint_string,
})
static func _get_project_setting(key: String):
var p = ProjectSettings.get_setting(key)
return p if p != null else defaults[key]
#endregion

View file

@ -0,0 +1 @@
uid://blq0g140jdg7d

View file

@ -0,0 +1,128 @@
@tool
class_name PopochiuEditorConfig
extends RefCounted
enum Icons { COLLAPSED, EXPANDED }
# ASEPRITE IMPORTER --------------------------------------------------------------------------------
const ASEPRITE_IMPORTER_ENABLED = "popochiu/import/aseprite/enable_aseprite_importer"
const ASEPRITE_COMMAND_PATH = "popochiu/import/aseprite/command_path"
const ASEPRITE_REMOVE_JSON_FILE = "popochiu/import/aseprite/remove_json_file"
# GIZMOS -------------------------------------------------------------------------------------------
const GIZMOS_FONT_SIZE = "popochiu/gizmos/font_size"
const GIZMOS_BASELINE_COLOR = "popochiu/gizmos/baseline_color"
const GIZMOS_WALK_TO_POINT_COLOR = "popochiu/gizmos/walk_to_point_color"
const GIZMOS_LOOK_AT_POINT_COLOR = "popochiu/gizmos/look_at_point_color"
const GIZMOS_DIALOG_POS_COLOR = "popochiu/gizmos/dialog_position_color"
const GIZMOS_COLOR_TOOLBAR_BUTTONS = "popochiu/gizmos/apply_colors_to_toolbar_buttons"
const GIZMOS_HANDLER_SIZE = "popochiu/gizmos/handler_size"
const GIZMOS_SHOW_CONNECTORS = "popochiu/gizmos/show_connectors"
const GIZMOS_SHOW_OUTLINE = "popochiu/gizmos/show_handler_outline"
const GIZMOS_SHOW_NODE_NAME = "popochiu/gizmos/show_node_name"
const GIZMOS_ALWAYS_SHOW_WA = "popochiu/gizmos/always_show_walkable_areas"
# Settings default values
static var defaults := {
ASEPRITE_IMPORTER_ENABLED: false,
ASEPRITE_COMMAND_PATH: _default_aseprite_command(),
ASEPRITE_REMOVE_JSON_FILE: true,
GIZMOS_FONT_SIZE: _default_font_size(),
GIZMOS_BASELINE_COLOR: Color.CYAN,
GIZMOS_WALK_TO_POINT_COLOR: Color.GREEN,
GIZMOS_LOOK_AT_POINT_COLOR: Color.RED,
GIZMOS_DIALOG_POS_COLOR: Color.MAGENTA,
GIZMOS_COLOR_TOOLBAR_BUTTONS: true,
GIZMOS_HANDLER_SIZE: 32,
GIZMOS_SHOW_CONNECTORS: true,
GIZMOS_SHOW_OUTLINE: true,
GIZMOS_SHOW_NODE_NAME: true,
GIZMOS_ALWAYS_SHOW_WA: false,
}
static var editor_settings: EditorSettings
#region Public #####################################################################################
static func initialize_editor_settings():
editor_settings = EditorInterface.get_editor_settings()
# Aseprite importer
_initialize_editor_setting(ASEPRITE_IMPORTER_ENABLED, TYPE_BOOL)
_initialize_editor_setting(ASEPRITE_COMMAND_PATH, TYPE_STRING)
_initialize_editor_setting(ASEPRITE_REMOVE_JSON_FILE, TYPE_BOOL)
# Gizmos
_initialize_editor_setting(GIZMOS_BASELINE_COLOR, TYPE_COLOR)
_initialize_editor_setting(GIZMOS_WALK_TO_POINT_COLOR, TYPE_COLOR)
_initialize_editor_setting(GIZMOS_LOOK_AT_POINT_COLOR, TYPE_COLOR)
_initialize_editor_setting(GIZMOS_DIALOG_POS_COLOR, TYPE_COLOR)
_initialize_editor_setting(GIZMOS_COLOR_TOOLBAR_BUTTONS, TYPE_BOOL)
_initialize_editor_setting(GIZMOS_HANDLER_SIZE, TYPE_INT, PROPERTY_HINT_RANGE, "4,64")
_initialize_editor_setting(GIZMOS_FONT_SIZE, TYPE_INT, PROPERTY_HINT_RANGE, "4,64")
_initialize_editor_setting(GIZMOS_SHOW_CONNECTORS, TYPE_BOOL)
_initialize_editor_setting(GIZMOS_SHOW_OUTLINE, TYPE_BOOL)
_initialize_editor_setting(GIZMOS_SHOW_NODE_NAME, TYPE_BOOL)
_initialize_editor_setting(GIZMOS_ALWAYS_SHOW_WA, TYPE_BOOL)
static func get_icon(icon: Icons) -> Texture2D:
match icon:
Icons.COLLAPSED:
return EditorInterface.get_base_control().get_theme_icon(
"GuiTreeArrowRight", "EditorIcons"
)
Icons.EXPANDED:
return EditorInterface.get_base_control().get_theme_icon(
"GuiTreeArrowDown", "EditorIcons"
)
return null
# ASEPRITE IMPORTER --------------------------------------------------------------------------------
static func aseprite_importer_enabled() -> bool:
return get_editor_setting(ASEPRITE_IMPORTER_ENABLED)
static func get_command() -> String:
return get_editor_setting(ASEPRITE_COMMAND_PATH)
static func should_remove_source_files() -> bool:
return get_editor_setting(ASEPRITE_REMOVE_JSON_FILE)
#endregion
#region Private ####################################################################################
static func _default_aseprite_command() -> String:
return 'aseprite'
static func _default_font_size() -> int:
if Engine.is_editor_hint():
return EditorInterface.get_editor_theme().default_font_size
return 16
static func _initialize_editor_setting(
key: String, type: int, hint: int = PROPERTY_HINT_NONE, hint_string : String = ""
) -> void:
if editor_settings.has_setting(key): return
editor_settings.set_setting(key, defaults[key])
editor_settings.set_initial_value(key, defaults[key], false)
editor_settings.add_property_info({
"name": key,
"type": type,
"hint": hint,
"hint_string": hint_string
})
static func get_editor_setting(key: String):
var e = editor_settings.get_setting(key)
return e if e != null else defaults[e]
#endregion

View file

@ -0,0 +1 @@
uid://c2xn0h6paw2xy

View file

@ -0,0 +1,70 @@
@tool
extends RefCounted
const LOCAL_OBJ_CONFIG_META_NAME = "_popochiu_aseprite_config_"
const LOCAL_OBJ_CONFIG_MARKER = "popochiu_aseprite_config"
const SEPARATOR = "|="
# ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ PUBLIC ░░░░
static func encode(object: Dictionary):
var text = "%s\n" % LOCAL_OBJ_CONFIG_MARKER
for prop in object:
text += "%s%s%s\n" % [prop, SEPARATOR, object[prop]]
return Marshalls.utf8_to_base64(text)
static func decode(string: String):
var decoded = _decode_base64(string)
if not _is_valid_config(decoded):
return null
print(decoded)
var cfg = decoded.split("\n")
var config = {}
for c in cfg:
var parts = c.split(SEPARATOR, 1)
if parts.size() == 2:
var key = parts[0].strip_edges()
var value = parts[1].strip_edges()
#Convert bool properties
if key in ["only_visible_layers", "wipe_old_anims", "op_exp"]:
match value:
"True":
config[key] = true
"False":
config[key] = false
_:
config[key] = false
else:
config[key] = value
return config
static func load_config(node:Node):
# Check if node is not null to avoid showing error messages in Output when inspecting nodes in
# the Debugger
if node and node.has_meta(LOCAL_OBJ_CONFIG_META_NAME):
return node.get_meta(LOCAL_OBJ_CONFIG_META_NAME)
static func save_config(node:Node, cfg:Dictionary):
node.set_meta(LOCAL_OBJ_CONFIG_META_NAME, cfg)
# ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ PRIVATE ░░░░
static func _decode_base64(string: String):
if string != "":
return Marshalls.base64_to_utf8(string)
return null
static func _is_valid_config(cfg) -> bool:
return cfg != null and cfg.begins_with(LOCAL_OBJ_CONFIG_MARKER)

View file

@ -0,0 +1 @@
uid://d26rqfocnhh7v

View file

@ -0,0 +1,75 @@
@tool
extends RefCounted
class_name ResultCodes
enum {
## Base codes
FAILURE, # generic failure state
SUCCESS, # generic success state
## Aseprite importer errors
ERR_ASEPRITE_CMD_NOT_FULL_PATH,
ERR_ASEPRITE_CMD_NOT_FOUND,
ERR_SOURCE_FILE_NOT_FOUND,
ERR_OUTPUT_FOLDER_NOT_FOUND,
ERR_ASEPRITE_EXPORT_FAILED,
ERR_UNKNOWN_EXPORT_MODE,
ERR_NO_VALID_LAYERS_FOUND,
ERR_INVALID_ASEPRITE_SPRITESHEET,
ERR_NO_ANIMATION_PLAYER_FOUND,
ERR_NO_SPRITE_FOUND,
ERR_UNNAMED_TAG_DETECTED,
ERR_TAGS_OPTIONS_ARRAY_EMPTY,
## Popochiu Object factories errors
ERR_CANT_CREATE_OBJ_FOLDER,
ERR_CANT_CREATE_OBJ_STATE,
ERR_CANT_OPEN_OBJ_SCRIPT_TEMPLATE,
ERR_CANT_CREATE_OBJ_SCRIPT,
ERR_CANT_SAVE_OBJ_SCENE,
ERR_CANT_SAVE_OBJ_RESOURCE,
}
# ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ PUBLIC ░░░░
static func get_error_message(code: int):
## TODO: these messages are a bit dull, having params would be better.
## Maybe add a param argument
match code:
# Aseprite importers error messages
ERR_ASEPRITE_CMD_NOT_FULL_PATH:
return "Aseprite command not found at given path. Please check \"Editor Settings > Popochiu > Import > Command Path\" to hold the FULL path to a valid Aseprite executable."
ERR_ASEPRITE_CMD_NOT_FOUND:
return "Aseprite command failed. Please, check if the right command is in your PATH or configured through \"Editor Settings > Popochiu > Import > Command Path\"."
ERR_SOURCE_FILE_NOT_FOUND:
return "Source file does not exist"
ERR_OUTPUT_FOLDER_NOT_FOUND:
return "Output location does not exist"
ERR_ASEPRITE_EXPORT_FAILED:
return "Unable to import file"
ERR_INVALID_ASEPRITE_SPRITESHEET:
return "Aseprite generated invalid data file"
ERR_NO_VALID_LAYERS_FOUND:
return "No valid layers found"
ERR_NO_ANIMATION_PLAYER_FOUND:
return "No AnimationPlayer found in target node"
ERR_NO_SPRITE_FOUND:
return "No sprite found in target node"
ERR_UNNAMED_TAG_DETECTED:
return "Unnamed tag detected"
ERR_TAGS_OPTIONS_ARRAY_EMPTY:
return "Tags options array is empty"
# Popochiu object factories error messages
ERR_CANT_CREATE_OBJ_FOLDER:
return "Can't create folder to host new Popochiu object"
ERR_CANT_CREATE_OBJ_STATE:
return "Can't create new Popochiu object's state resource (_state.tres, _state.gd)"
ERR_CANT_OPEN_OBJ_SCRIPT_TEMPLATE:
return "Can't open script template for new Popochiu object"
ERR_CANT_CREATE_OBJ_SCRIPT:
return "Can't create new Popochiu object's script file (.gd)"
ERR_CANT_SAVE_OBJ_SCENE:
return "Can't create new Popochiu object's scene (.tscn)"
ERR_CANT_SAVE_OBJ_RESOURCE:
return "Can't create new Popochiu object's resource (.tres)"
# Generic error message
_:
return "Import failed with code %d" % code

View file

@ -0,0 +1 @@
uid://cbxik5o1u00hf

View file

@ -0,0 +1,237 @@
extends RefCounted
const BASE_STATE_TEMPLATE := "res://addons/popochiu/engine/templates/%s_state_template.gd"
const BASE_SCRIPT_TEMPLATE := "res://addons/popochiu/engine/templates/%s_template.gd"
const BASE_SCENE_PATH := "res://addons/popochiu/engine/objects/%s/popochiu_%s.tscn"
const EMPTY_SCRIPT := "res://addons/popochiu/engine/templates/empty_script_template.gd"
# The following variables are setup on creation Names variants and name parameter passed to the
# create method.
var _path_template := "" # always set by child class
var _snake_name := ""
var _pascal_name := ""
var _path_base := ""
var _path_scene = ""
var _path_resource = ""
var _path_state = ""
var _path_script := ""
# The following variables are setup by the sub-class constructor to define the type of object to be
# processed
# TODO: reduce this to just "type", too much redundancy
var _type := -1
var _type_label := ""
var _type_target := ""
var _type_method: Callable
# The following variables are references to the elements generated for the creation of the new
# Popochiu object, such as resources, scenes, scripts, state scripts, etc
var _scene: Node
var _resource: Resource
var _state_resource: Resource
var _script: Resource
#region Public #####################################################################################
func get_obj_scene() -> Node:
return _scene
func get_snake_name() -> String:
return _snake_name
func get_obj_resource() -> Resource:
return _resource
func get_state_resource() -> Resource:
return _state_resource
func get_obj_script() -> Resource:
return _script
func get_scene_path() -> String:
return _path_scene
func get_type() -> int:
return _type
func get_type_method() -> Callable:
return _type_method
#endregion
#region Private ####################################################################################
func _setup_name(obj_name: String) -> void:
_pascal_name = obj_name.to_pascal_case()
_snake_name = obj_name.to_snake_case()
_path_base = _path_template % [_snake_name, _snake_name]
_path_script = _path_base + ".gd"
_path_state = _path_base + "_state.gd"
_path_resource = _path_base + ".tres"
_path_scene = _path_base + ".tscn"
func _create_obj_folder() -> int:
# TODO: Remove created files if the creation process failed.
if DirAccess.make_dir_recursive_absolute(_path_base.get_base_dir()) != OK:
PopochiuUtils.print_error(
"Could not create %s directory: %s" %
[_path_base.get_base_dir(), _pascal_name]
)
return ResultCodes.ERR_CANT_CREATE_OBJ_FOLDER
return ResultCodes.SUCCESS
func _create_state_resource() -> int:
var state_template: Script = load(
BASE_STATE_TEMPLATE % _type_label
).duplicate()
if ResourceSaver.save(state_template, _path_state) != OK:
PopochiuUtils.print_error(
"Could not create %s state script: %s" %
[_type_label, _pascal_name]
)
return ResultCodes.FAILURE
_state_resource = load(_path_state).new()
_state_resource.script_name = _pascal_name
_state_resource.scene = _path_scene
_state_resource.resource_name = _pascal_name
if ResourceSaver.save(_state_resource, _path_resource) != OK:
PopochiuUtils.print_error(
"Could not create state resource for %s: %s" %
[_type_label, _pascal_name]
)
return ResultCodes.ERR_CANT_CREATE_OBJ_STATE
return ResultCodes.SUCCESS
func _copy_script_template() -> int:
var _script: Script = load(
BASE_SCRIPT_TEMPLATE % _type_label
).duplicate()
if ResourceSaver.save( _script, _path_script) != OK:
PopochiuUtils.print_error(
"Could not create %s script: %s" %
[_type_label, _path_script]
)
return ResultCodes.ERR_CANT_CREATE_OBJ_SCRIPT
return ResultCodes.SUCCESS
## Create the script for the object based on the template of its type.
func _create_script_from_template() -> int:
var script_template_file = FileAccess.open(
BASE_SCRIPT_TEMPLATE % _type_label, FileAccess.READ
)
if script_template_file == null:
PopochiuUtils.print_error(
"Could not read script template from %s" %
[BASE_SCRIPT_TEMPLATE % _type_label]
)
return ResultCodes.ERR_CANT_OPEN_OBJ_SCRIPT_TEMPLATE
var new_code: String = script_template_file.get_as_text()
script_template_file.close()
new_code = new_code.replace(
"%s_state_template" % _type_label,
"%s_%s_state" % [_type_label, _snake_name]
)
new_code = new_code.replace(
"Data = null",
'Data = load("%s.tres")' % _path_base
)
new_code = new_code.replace("PopochiuUtils.e", "E")
_script = load(EMPTY_SCRIPT).duplicate()
_script.source_code = new_code
if ResourceSaver.save( _script, _path_script) != OK:
PopochiuUtils.print_error(
"Could not create %s script: %s" %
[_type_label, _path_script]
)
return ResultCodes.ERR_CANT_CREATE_OBJ_SCRIPT
return ResultCodes.SUCCESS
func _save_obj_scene(obj: Node) -> int:
var packed_scene: PackedScene = PackedScene.new()
packed_scene.pack(obj)
if ResourceSaver.save(packed_scene, _path_scene) != OK:
PopochiuUtils.print_error(
"Could not create %s: %s" %
[_type_label, _path_script]
)
return ResultCodes.ERR_CANT_SAVE_OBJ_SCENE
# Load the scene to be get by the calling code
# Instancing the created .tscn file fixes #58
_scene = (load(_path_scene) as PackedScene).instantiate(PackedScene.GEN_EDIT_STATE_INSTANCE)
return ResultCodes.SUCCESS
func _save_obj_resource(obj: Resource) -> int:
if ResourceSaver.save(obj, _path_resource) != OK:
PopochiuUtils.print_error(
"Could not create %s: %s" %
[_type_label, _pascal_name]
)
return ResultCodes.ERR_CANT_SAVE_OBJ_RESOURCE
# Load the resource to be get by the calling code
_resource = load(_path_resource)
return ResultCodes.SUCCESS
## Makes a copy of the base scene for the object (e.g. popochiu_room.tscn,
## popochiu_inventory_item.tscn, popochiu_prop.tscn).
func _load_obj_base_scene() -> Node:
var obj = (
load(BASE_SCENE_PATH % [_type_label, _type_label]) as PackedScene
).instantiate(PackedScene.GEN_EDIT_STATE_MAIN_INHERITED)
# The script is assigned first so that other properties will not be
# overwritten by that assignment.
if _script != null:
obj.set_script(load(_path_script))
return obj
func _add_resource_to_popochiu() -> void:
# Add the created obj to Popochiu's correct list
var resource := ResourceLoader.load(_path_resource)
if PopochiuResources.set_data_value(
_type_target,
resource.script_name,
resource.resource_path
) != OK:
PopochiuUtils.print_error(
"Could not add the created %s to Popochiu: %s" %
[_type_label, _pascal_name]
)
return
# Add the object to the proper singleton
PopochiuResources.update_autoloads(true)
# Update the related list in the dock
PopochiuEditorHelper.signal_bus.main_object_added.emit(_type, _pascal_name)
#endregion

View file

@ -0,0 +1 @@
uid://dsauwqcvbelb1

View file

@ -0,0 +1,98 @@
class_name PopochiuRoomObjFactory
extends "res://addons/popochiu/editor/factories/factory_base_popochiu_obj.gd"
const CHILD_VISIBLE_IN_ROOM_META = "_popochiu_obj_factory_child_visible_in_room_"
# The following variable is setup by the sub-class constructor to
# define the holder node for the new room object (Props, Hotspots, etc)
var _obj_room_group := ""
# The following variables are setup by the _setup_room method
var _room: Node2D = null
var _room_path := ""
var _room_dir := ""
#region Public #####################################################################################
func get_group() -> String:
return _obj_room_group
func create_from(node: Node, room: PopochiuRoom) -> int:
_setup_room(room)
_setup_name(node.name)
var param := _get_param(node)
param.room = room
param.obj_name = node.name
param.is_visible = node.visible
param.should_setup_room_and_name = false
param.should_add_to_room = false
param.should_create_script = !FileAccess.file_exists(_path_script)
return call("create", param)
func get_new_instance() -> PopochiuRoomObjFactory:
return new()
#endregion
#region Private ####################################################################################
func _setup_room(room: PopochiuRoom) -> void:
_room = room
_room_path = _room.scene_file_path
_room_dir = _room_path.get_base_dir()
# Adding room path to room object path template
_path_template = _room_dir + _path_template
# This function adds a child to the new object scene
# marking it as "visible in room scene"
func _add_visible_child(child: Node) -> void:
child.set_meta(CHILD_VISIBLE_IN_ROOM_META, true)
_scene.add_child(child)
func _add_resource_to_room() -> void:
# Add the newly created obj to its room
_room.get_node(_obj_room_group).add_child(_scene)
# Set the ownership for the node plus all it's children
# (this address colliders, polygons, etc)
_scene.owner = _room
for child in _scene.get_children():
if child.has_meta(CHILD_VISIBLE_IN_ROOM_META):
child.owner = _room
child.remove_meta(CHILD_VISIBLE_IN_ROOM_META)
# Center the object on the scene
_scene.position = Vector2(
ProjectSettings.get_setting(PopochiuResources.DISPLAY_WIDTH),
ProjectSettings.get_setting(PopochiuResources.DISPLAY_HEIGHT)
) / 2.0
# Save the room scene (it's open in the editor)
EditorInterface.save_scene()
func _get_param(_node: Node) -> PopochiuRoomObjFactoryParam:
return PopochiuRoomObjFactoryParam.new()
#endregion
#region Subclass ###################################################################################
class PopochiuRoomObjFactoryParam extends RefCounted:
var obj_name: String
var room: PopochiuRoom
var is_visible := true
var should_setup_room_and_name := true
var should_create_script := true
var should_add_to_room := true
## Property used to store the vectors stored in the [member CollisionPolygon2D.polygon] for
## [PopochiuProp], [PopochiuHotspot], and [PopochiuRegion].
var interaction_polygon := PackedVector2Array()
#endregion

View file

@ -0,0 +1 @@
uid://ch67numd388l

View file

@ -0,0 +1,64 @@
extends "res://addons/popochiu/editor/factories/factory_base_popochiu_obj.gd"
class_name PopochiuCharacterFactory
#region Godot ######################################################################################
func _init() -> void:
_type = PopochiuResources.Types.CHARACTER
_type_label = "character"
_type_target = "characters"
_path_template = PopochiuResources.CHARACTERS_PATH.path_join("%s/character_%s")
#endregion
#region Public #####################################################################################
func create(obj_name: String, is_pc := false) -> int:
# If everything goes well, this won't change.
var result_code := ResultCodes.SUCCESS
# Setup the class variables that depends on the object name
_setup_name(obj_name)
# Create the folder
result_code = _create_obj_folder()
if result_code != ResultCodes.SUCCESS: return result_code
# Create the state Resource and a script
# so devs can add extra properties to that state
result_code = _create_state_resource()
if result_code != ResultCodes.SUCCESS: return result_code
# Create the script populating the template with the right references
result_code = _create_script_from_template()
if result_code != ResultCodes.SUCCESS: return result_code
# ---- LOCAL CODE ------------------------------------------------------------------------------
# Create the instance
var new_obj: PopochiuCharacter = _load_obj_base_scene()
new_obj.name = "Character" + _pascal_name
new_obj.script_name = _pascal_name
new_obj.description = _pascal_name.capitalize()
new_obj.cursor = PopochiuResources.CURSOR_TYPE.TALK
if PopochiuConfig.is_pixel_art_textures():
new_obj.get_node("Sprite2D").texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
# ---- END OF LOCAL CODE -----------------------------------------------------------------------
# Save the scene (.tscn)
result_code = _save_obj_scene(new_obj)
if result_code != ResultCodes.SUCCESS: return result_code
# Add the object to Popochiu dock list, plus open it in the editor
_add_resource_to_popochiu()
# ---- LOCAL CODE ------------------------------------------------------------------------------
# Set as PC
if is_pc:
PopochiuEditorHelper.signal_bus.pc_changed.emit(new_obj.script_name)
# ---- END OF LOCAL CODE -----------------------------------------------------------------------
return result_code
#endregion

View file

@ -0,0 +1 @@
uid://dstbnm6v0t6ot

View file

@ -0,0 +1,49 @@
extends "res://addons/popochiu/editor/factories/factory_base_popochiu_obj.gd"
class_name PopochiuDialogFactory
#region Godot ######################################################################################
func _init() -> void:
_type = PopochiuResources.Types.DIALOG
_type_label = "dialog"
_type_target = "dialogs"
_path_template = PopochiuResources.DIALOGS_PATH.path_join("%s/dialog_%s")
#endregion
#region Public #####################################################################################
func create(obj_name: String) -> int:
# If everything goes well, this won't change.
var result_code := ResultCodes.SUCCESS
# Setup the class variables that depends on the object name
_setup_name(obj_name)
# Create the folder
result_code = _create_obj_folder()
if result_code != ResultCodes.SUCCESS: return result_code
# Create the script
result_code = _copy_script_template()
if result_code != ResultCodes.SUCCESS: return result_code
# ---- LOCAL CODE ------------------------------------------------------------------------------
# Create the resource (dialogs are not scenes)
var new_obj := PopochiuDialog.new()
new_obj.set_script(load(_path_script))
new_obj.script_name = _pascal_name
new_obj.resource_name = _pascal_name
# ---- END OF LOCAL CODE -----------------------------------------------------------------------
# Save resource (dialogs are not scenes)
result_code = _save_obj_resource(new_obj)
if result_code != ResultCodes.SUCCESS: return result_code
# Add the object to Popochiu dock list, plus open it in the editor
_add_resource_to_popochiu()
return result_code
#endregion

View file

@ -0,0 +1 @@
uid://ddk6pykrms2tq

View file

@ -0,0 +1,76 @@
class_name PopochiuHotspotFactory
extends PopochiuRoomObjFactory
#region Godot ######################################################################################
func _init() -> void:
_type = PopochiuResources.Types.HOTSPOT
_type_label = "hotspot"
_type_method = PopochiuEditorHelper.is_hotspot
_obj_room_group = "Hotspots"
_path_template = "/hotspots/%s/hotspot_%s"
#endregion
#region Public #####################################################################################
func create(param: PopochiuHotspotFactoryParam) -> int:
# If everything goes well, this won't change.
var result_code := ResultCodes.SUCCESS
if param.should_setup_room_and_name:
_setup_room(param.room)
_setup_name(param.obj_name)
# Create the folder
result_code = _create_obj_folder()
if result_code != ResultCodes.SUCCESS: return result_code
# Create the script
if param.should_create_script:
result_code = _copy_script_template()
if result_code != ResultCodes.SUCCESS: return result_code
# ---- LOCAL CODE ------------------------------------------------------------------------------
# Create the instance
var new_obj: PopochiuHotspot = _load_obj_base_scene()
new_obj.set_script(ResourceLoader.load(_path_script))
new_obj.name = _pascal_name
new_obj.script_name = _pascal_name
new_obj.description = _snake_name.capitalize()
new_obj.cursor = PopochiuResources.CURSOR_TYPE.ACTIVE
new_obj.interaction_polygon = param.interaction_polygon
# Save the hotspot scene (.tscn) and put it into _scene class property
result_code = _save_obj_scene(new_obj)
if result_code != ResultCodes.SUCCESS: return result_code
# ---- END OF LOCAL CODE -----------------------------------------------------------------------
if param.should_add_to_room:
# Add the object to its room
_add_resource_to_room()
return result_code
#endregion
#region Private ####################################################################################
func _get_param(node: Node) -> PopochiuRoomObjFactoryParam:
var param := PopochiuHotspotFactoryParam.new()
param.is_interactive = node.clickable
# TODO: Remove this line once the last gizmos PR is merged
param.interaction_polygon = node.interaction_polygon
return param
#endregion
#region Subclass ###################################################################################
class PopochiuHotspotFactoryParam extends PopochiuRoomObjFactory.PopochiuRoomObjFactoryParam:
var is_interactive := true
#endregion

View file

@ -0,0 +1 @@
uid://bcnd7an67ulyf

View file

@ -0,0 +1,60 @@
extends "res://addons/popochiu/editor/factories/factory_base_popochiu_obj.gd"
class_name PopochiuInventoryItemFactory
#region Godot ######################################################################################
func _init() -> void:
_type = PopochiuResources.Types.INVENTORY_ITEM
_type_label = "inventory_item"
_type_target = "inventory_items"
_path_template = PopochiuResources.INVENTORY_ITEMS_PATH.path_join("%s/inventory_item_%s")
#endregion
#region Public #####################################################################################
func create(obj_name: String) -> int:
# If everything goes well, this won't change.
var result_code := ResultCodes.SUCCESS
# Setup the class variables that depends on the object name
_setup_name(obj_name)
# Create the folder
result_code = _create_obj_folder()
if result_code != ResultCodes.SUCCESS: return result_code
# Create the state Resource and a script
# so devs can add extra properties to that state
result_code = _create_state_resource()
if result_code != ResultCodes.SUCCESS: return result_code
# Create the script populating the template with the right references
result_code = _create_script_from_template()
if result_code != ResultCodes.SUCCESS: return result_code
# ---- LOCAL CODE ------------------------------------------------------------------------------
# Create the instance
var new_obj: PopochiuInventoryItem = _load_obj_base_scene()
new_obj.name = "Item" + _pascal_name
new_obj.script_name = _pascal_name
new_obj.description = _pascal_name.capitalize()
new_obj.cursor = PopochiuResources.CURSOR_TYPE.USE
new_obj.size_flags_vertical = new_obj.SIZE_SHRINK_CENTER
if PopochiuConfig.is_pixel_art_textures():
new_obj.texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
# ---- END OF LOCAL CODE -----------------------------------------------------------------------
# Save the scene (.tscn)
result_code = _save_obj_scene(new_obj)
if result_code != ResultCodes.SUCCESS: return result_code
# Add the object to Popochiu dock list, plus open it in the editor
_add_resource_to_popochiu()
return result_code
#endregion

View file

@ -0,0 +1 @@
uid://bym4xs81a2ph4

View file

@ -0,0 +1,44 @@
class_name PopochiuMarkerFactory
extends PopochiuRoomObjFactory
#region Godot ######################################################################################
func _init() -> void:
_type = PopochiuResources.Types.MARKER
_type_label = "marker"
_type_method = PopochiuEditorHelper.is_marker
_obj_room_group = "Markers"
_path_template = "/markers/%s/marker_%s"
#endregion
#region Public #####################################################################################
func create(param: PopochiuRoomObjFactoryParam) -> int:
# If everything goes well, this won't change.
var result_code := ResultCodes.SUCCESS
if param.should_setup_room_and_name:
_setup_room(param.room)
_setup_name(param.obj_name)
# Create the folder
result_code = _create_obj_folder()
if result_code != ResultCodes.SUCCESS: return result_code
# ---- LOCAL CODE ------------------------------------------------------------------------------
# Create the instance
var new_obj: Marker2D = Marker2D.new()
new_obj.name = _pascal_name
# Save the marker scene (.tscn) and put it into _scene class property
result_code = _save_obj_scene(new_obj)
if result_code != ResultCodes.SUCCESS: return result_code
# ---- END OF LOCAL CODE -----------------------------------------------------------------------
if param.should_add_to_room:
# Add the object to its room
_add_resource_to_room()
return result_code
#endregion

View file

@ -0,0 +1 @@
uid://ckjo1lcmxjlh6

View file

@ -0,0 +1,86 @@
class_name PopochiuPropFactory
extends PopochiuRoomObjFactory
#region Godot ######################################################################################
func _init() -> void:
_type = PopochiuResources.Types.PROP
_type_label = "prop"
_type_method = PopochiuEditorHelper.is_prop
_obj_room_group = "Props"
_path_template = "/props/%s/prop_%s"
#endregion
#region Public #####################################################################################
func create(param: PopochiuPropFactoryParam) -> int:
# If everything goes well, this won't change.
var result_code := ResultCodes.SUCCESS
if param.should_setup_room_and_name:
_setup_room(param.room)
_setup_name(param.obj_name)
# Create the folder
result_code = _create_obj_folder()
if result_code != ResultCodes.SUCCESS: return result_code
# Create the script (if the prop is interactive)
if param.should_create_script:
result_code = _copy_script_template()
if result_code != ResultCodes.SUCCESS: return result_code
# ---- LOCAL CODE ------------------------------------------------------------------------------
# Create the instance
var new_obj: PopochiuProp = _load_obj_base_scene()
new_obj.set_script(ResourceLoader.load(_path_script))
new_obj.name = _pascal_name
new_obj.script_name = _pascal_name
new_obj.description = _snake_name.capitalize()
new_obj.cursor = PopochiuResources.CURSOR_TYPE.ACTIVE
new_obj.clickable = param.is_interactive
new_obj.visible = param.is_visible
new_obj.interaction_polygon = param.interaction_polygon
if PopochiuConfig.is_pixel_art_textures():
new_obj.get_node("Sprite2D").texture_filter = CanvasItem.TEXTURE_FILTER_NEAREST
if _snake_name in ["bg", "background"]:
new_obj.baseline =\
-ProjectSettings.get_setting(PopochiuResources.DISPLAY_HEIGHT) / 2.0
new_obj.z_index = -1
# Save the scene (.tscn) and put it into _scene class property
result_code = _save_obj_scene(new_obj)
if result_code != ResultCodes.SUCCESS: return result_code
# ---- END OF LOCAL CODE -----------------------------------------------------------------------
if param.should_add_to_room:
# Add the object to its room
_add_resource_to_room()
return result_code
#endregion
#region Private ####################################################################################
func _get_param(node: Node) -> PopochiuRoomObjFactoryParam:
var param := PopochiuPropFactoryParam.new()
param.is_interactive = node.clickable
# TODO: Remove this line once the last gizmos PR is merged
param.interaction_polygon = node.interaction_polygon
return param
#endregion
#region Subclass ###################################################################################
class PopochiuPropFactoryParam extends PopochiuRoomObjFactory.PopochiuRoomObjFactoryParam:
var is_interactive := false
#endregion

View file

@ -0,0 +1 @@
uid://b8ajrxi1mp5up

View file

@ -0,0 +1,71 @@
class_name PopochiuRegionFactory
extends PopochiuRoomObjFactory
#region Godot ######################################################################################
func _init() -> void:
_type = PopochiuResources.Types.REGION
_type_label = "region"
_type_method = PopochiuEditorHelper.is_region
_obj_room_group = "Regions"
_path_template = "/regions/%s/region_%s"
#endregion
#region Public #####################################################################################
func create(param: PopochiuRoomObjFactoryParam) -> int:
# If everything goes well, this won't change.
var result_code := ResultCodes.SUCCESS
if param.should_setup_room_and_name:
_setup_room(param.room)
_setup_name(param.obj_name)
# Create the folder
result_code = _create_obj_folder()
if result_code != ResultCodes.SUCCESS: return result_code
# Create the script
if param.should_create_script:
result_code = _copy_script_template()
if result_code != ResultCodes.SUCCESS: return result_code
# ---- LOCAL CODE ------------------------------------------------------------------------------
# Create the instance
var new_obj: PopochiuRegion = _load_obj_base_scene()
new_obj.set_script(ResourceLoader.load(_path_script))
new_obj.name = _pascal_name
new_obj.script_name = _pascal_name
new_obj.description = _snake_name.capitalize()
new_obj.interaction_polygon = param.interaction_polygon
# Save the scene (.tscn) and put it into _scene class property
result_code = _save_obj_scene(new_obj)
if result_code != ResultCodes.SUCCESS: return result_code
# ---- END OF LOCAL CODE -----------------------------------------------------------------------
if param.should_add_to_room:
# Add the object to its room
_add_resource_to_room()
return result_code
#endregion
#region Private ####################################################################################
func _get_param(node: Node) -> PopochiuRoomObjFactoryParam:
var param := PopochiuRegionFactoryParam.new()
param.interaction_polygon = node.interaction_polygon
return param
#endregion
#region Subclass ###################################################################################
class PopochiuRegionFactoryParam extends PopochiuRoomObjFactory.PopochiuRoomObjFactoryParam:
var should_create_interaction_polygon := true
#endregion

View file

@ -0,0 +1 @@
uid://dphtxosh168sb

View file

@ -0,0 +1,63 @@
extends "res://addons/popochiu/editor/factories/factory_base_popochiu_obj.gd"
class_name PopochiuRoomFactory
#region Godot ######################################################################################
func _init() -> void:
_type = PopochiuResources.Types.ROOM
_type_label = "room"
_type_target = "rooms"
_path_template = PopochiuResources.ROOMS_PATH.path_join("%s/room_%s")
#endregion
#region Public #####################################################################################
func create(obj_name: String, set_as_main := false) -> int:
# If everything goes well, this won't change.
var result_code := ResultCodes.SUCCESS
# Setup the class variables that depends on the object name
_setup_name(obj_name)
# Create the folder
result_code = _create_obj_folder()
if result_code != ResultCodes.SUCCESS: return result_code
# Create the state Resource and a script
# so devs can add extra properties to that state
result_code = _create_state_resource()
if result_code != ResultCodes.SUCCESS: return result_code
# Create the script populating the template with the right references
result_code = _create_script_from_template()
if result_code != ResultCodes.SUCCESS: return result_code
# ---- LOCAL CODE ------------------------------------------------------------------------------
# Create the instance
var new_obj: PopochiuRoom = _load_obj_base_scene()
new_obj.name = "Room" + _pascal_name
new_obj.script_name = _pascal_name
new_obj.width = ProjectSettings.get_setting(PopochiuResources.DISPLAY_WIDTH)
new_obj.height = ProjectSettings.get_setting(PopochiuResources.DISPLAY_HEIGHT)
# ---- END OF LOCAL CODE -----------------------------------------------------------------------
# Save the scene (.tscn)
result_code = _save_obj_scene(new_obj)
if result_code != ResultCodes.SUCCESS: return result_code
# Add the object to Popochiu dock list, plus open it in the editor
_add_resource_to_popochiu()
# ---- LOCAL CODE ------------------------------------------------------------------------------
# Set as main room
# Changed _set_as_main_check.pressed to _set_as_main_check.button_pressed
# in order to fix #56
if set_as_main:
PopochiuEditorHelper.signal_bus.main_scene_changed.emit(_scene.scene_file_path)
# ---- END OF LOCAL CODE -----------------------------------------------------------------------
return result_code
#endregion

View file

@ -0,0 +1 @@
uid://b7kd0q631qqt6

View file

@ -0,0 +1,94 @@
class_name PopochiuWalkableAreaFactory
extends PopochiuRoomObjFactory
#region Godot ######################################################################################
func _init() -> void:
_type = PopochiuResources.Types.WALKABLE_AREA
_type_label = "walkable_area"
_type_method = PopochiuEditorHelper.is_walkable_area
_obj_room_group = "WalkableAreas"
_path_template = "/walkable_areas/%s/walkable_area_%s"
#endregion
#region Public #####################################################################################
func create(param: PopochiuWalkableAreaFactoryParam) -> int:
# If everything goes well, this won't change.
var result_code := ResultCodes.SUCCESS
if param.should_setup_room_and_name:
_setup_room(param.room)
_setup_name(param.obj_name)
# Create the folder
result_code = _create_obj_folder()
if result_code != ResultCodes.SUCCESS: return result_code
# Create the script
if param.should_create_script:
result_code = _copy_script_template()
if result_code != ResultCodes.SUCCESS: return result_code
# ---- LOCAL CODE ------------------------------------------------------------------------------
# Create the instance
var new_obj: PopochiuWalkableArea = _load_obj_base_scene()
new_obj.set_script(ResourceLoader.load(_path_script))
new_obj.name = _pascal_name
new_obj.script_name = _pascal_name
new_obj.description = _snake_name.capitalize()
# Find the NavigationRegion2D for the WA and populate it with a default rectangle polygon
var perimeter := new_obj.find_child("Perimeter")
var polygon := NavigationPolygon.new()
polygon.add_outline(PackedVector2Array([
Vector2(-10, -10), Vector2(10, -10), Vector2(10, 10), Vector2(-10, 10)
]))
NavigationServer2D.bake_from_source_geometry_data(
polygon, NavigationMeshSourceGeometryData2D.new()
)
polygon.agent_radius = 0.0
perimeter.navigation_polygon = polygon
if not param.navigation_polygon.is_empty():
new_obj.interaction_polygon = param.navigation_polygon
new_obj.clear_and_bake(perimeter.navigation_polygon)
# Show the WA perimeter, depending on user prefs
perimeter.visible = PopochiuEditorConfig.get_editor_setting(
PopochiuEditorConfig.GIZMOS_ALWAYS_SHOW_WA
)
# Save the scene (.tscn) and put it into _scene class property
result_code = _save_obj_scene(new_obj)
if result_code != ResultCodes.SUCCESS: return result_code
# ---- END OF LOCAL CODE -----------------------------------------------------------------------
if param.should_add_to_room:
# Add the object to its room
_add_resource_to_room()
return result_code
#endregion
#region Private ####################################################################################
func _get_param(node: Node) -> PopochiuRoomObjFactoryParam:
var param := PopochiuWalkableAreaFactoryParam.new()
param.navigation_polygon = node.interaction_polygon
return param
#endregion
#region Subclass ###################################################################################
class PopochiuWalkableAreaFactoryParam extends PopochiuRoomObjFactory.PopochiuRoomObjFactoryParam:
var navigation_polygon := []
#endregion

View file

@ -0,0 +1 @@
uid://c66i7vwbgxym8

View file

@ -0,0 +1,274 @@
@tool
class_name Gizmo2D
extends RefCounted
# Gizmo types
enum {
GIZMO_POS, # square marker that represents (x,y) coordinates
GIZMO_HPOS, # vertical line that represents a horizontal coordinate
GIZMO_VPOS # horizontal line that represents a vertical coordinate
}
# Public vars
# Convenience accessors
var target_node: Node2D:
set = set_target_node,
get = get_target_node
var target_property: String:
set = set_target_property,
get = get_target_property
var position:
get = get_position
# Behavior flags
var show_connector: bool = true # Show gizmo-to-node connectors
var show_outlines: bool = true
var show_target_name: bool = true # Show target node name
var visible: bool = true # Gizmo visibility
# Private vars
# Context
var _type: int
var _target_node: Node2D
var _target_property: String
# Appearance
var _size: Vector2 # Gizmo width and height
var _color: Color # Gizmo color
var _label: String # A label to be painted near the Gizmo
var _font: Font # Label font
var _font_size: int # Label font size
# State
var _handle: Rect2 # Gizmo handle
var _current_position: Vector2 # The position the gizmo is representing in every moment
var _current_color: Color
var _is_grabbed: bool = false # Gizmo is moving
var _grab_center_pos: Vector2 # Starting center position when grabbing
var _grab_mouse_pos: Vector2 # Starting mouse position when grabbing
#region Virtual ####################################################################################
func _init(
node: Node,
property: String,
label: String,
type: int,
):
_target_node = node
_target_property = property
_type = type
_label = label
set_theme(
Color.AQUA,
24,
EditorInterface.get_editor_theme().default_font,
EditorInterface.get_editor_theme().default_font_size
)
_current_color = _color
#endregion
#region SetGet #####################################################################################
func set_theme(
color: Color,
size: int,
font: Font,
font_size: int
):
_color = color
_size = Vector2(size, size)
_font = font
_font_size = font_size
func set_target_node(node: Node2D):
_target_node = node
func get_target_node() -> Node2D:
return _target_node
func set_target_property(property: String):
_target_property = property
func get_target_property() -> String:
if _target_property:
return _target_property
return ""
#endregion
#region Private ####################################################################################
func _draw_outlines(viewport: Control):
viewport.draw_rect(
_handle,
Color.BLACK, false, 4
)
viewport.draw_string_outline(
_font,
_handle.position + Vector2(0, _size.y + 2 + _font.get_ascent(_font_size)),
_label, HORIZONTAL_ALIGNMENT_CENTER,
- 1,
_font_size,
6,
Color.BLACK
)
if show_target_name:
viewport.draw_string_outline(
_font,
_handle.position + Vector2(0, -_font.get_descent(_font_size)),
_target_node.name,
HORIZONTAL_ALIGNMENT_CENTER,
- 1,
_font_size,
6,
Color.BLACK
)
func _draw_gizmo(viewport: Control):
# Draw the handle (on top of the line, if it's present)
viewport.draw_rect(
_handle,
_current_color
)
# Draw gizmo-to-node connector, if active
if show_connector:
viewport.draw_dashed_line(
(_target_node.get_viewport_transform() * _target_node.get_global_transform()).origin,
_handle.get_center(),
_current_color.darkened(0.2),
2,
4
)
viewport.draw_circle(
_handle.get_center(),
3,
_current_color.darkened(0.2)
)
# Draw the label, if it's set and non empty
if _label:
viewport.draw_string(
_font,
_handle.position + Vector2(0, _size.y + 2 + _font.get_ascent(_font_size)),
_label, HORIZONTAL_ALIGNMENT_CENTER,
- 1,
_font_size,
_current_color
)
if show_target_name:
viewport.draw_string(
_font,
_handle.position + Vector2(0, -_font.get_descent(_font_size)),
_target_node.name,
HORIZONTAL_ALIGNMENT_CENTER,
- 1,
_font_size,
_current_color
)
func _can_draw():
return (visible and _target_node != null and _target_node.is_visible_in_tree())
#endregion
#region Public #####################################################################################
func draw(viewport: Control, coord: Variant) -> void:
# Handmade coordinates type overloading
if not (coord is Vector2 or coord is int):
return
# Check if the gizmo can be drawn
if not _can_draw():
return
# Coordinates normalization (to vector) for horizontal or vertical gizmos
# Both axis are set to the same value, then ignore one or the other
# depending on the gizmo type
if coord is int:
coord = Vector2(coord, coord)
# Calculate the GLOBAL coordinates of the center of the square handle
# This only takes into account the node offset discarding its transform basis
# (representing rotation, skew and scale) then it applies the viewport transform
# to take into account the zoom level
var center = _target_node.get_viewport_transform() * (_target_node.get_global_transform().origin + Vector2(coord))
# Set handle color
_current_color = _color
# Highlight handle if held by the mouse click
if _is_grabbed:
_current_color = _color.lightened(0.5)
# Draw an horizontal or vertical line if the gizmo is one-dimensional
match _type:
GIZMO_VPOS:
var viewport_width = EditorInterface.get_editor_viewport_2d().size.x
center.x = viewport_width / 2
viewport.draw_line(
Vector2(0, center.y),
Vector2(viewport_width, center.y),
_current_color,
2
)
GIZMO_HPOS:
var viewport_height = EditorInterface.get_editor_viewport_2d().size.y
center.y = viewport_height / 2
viewport.draw_line(
Vector2(center.x, 0),
Vector2(center.x, viewport_height),
_current_color,
2
)
# Initialize the handle in the right position
_handle = Rect2(center - _size / 2, _size)
if show_outlines:
_draw_outlines(viewport)
_draw_gizmo(viewport)
func drag_to(pos: Vector2):
# Distance between the mouse position and the gizmo center
var d = _grab_center_pos - _grab_mouse_pos
# Gizmo center position in global coordinates
var current_gizmo_pos = pos + d
# Distance between gizmo center position in 2D world node coordinates and
# node position ignoring its transform basis (representing rotation, skew and scale)
_current_position = _target_node.get_viewport_transform().affine_inverse() * current_gizmo_pos - (target_node.get_global_transform().origin)
func release():
_is_grabbed = false
func grab(pos: Vector2):
_is_grabbed = true
_grab_mouse_pos = pos
_grab_center_pos = _handle.get_center()
func cancel():
_is_grabbed = false
func has_point(pos: Vector2):
return visible and _handle.abs().has_point(pos)
func get_position():
match _type:
GIZMO_POS:
return _current_position
GIZMO_HPOS:
return _current_position.x
GIZMO_VPOS:
return _current_position.y
#endregion

View file

@ -0,0 +1 @@
uid://71g54gbvyv7l

View file

@ -0,0 +1,278 @@
@tool
class_name PopochiuGizmoClickablePlugin
extends EditorPlugin
# TODO: move these out of the plugin and into Popochiu (enums) or PopochiuClickable
enum {
WALK_TO_POINT,
LOOK_AT_POINT,
BASELINE,
DIALOG_POS
}
# Private vars
# State
var _target_node: Node2D
var _undo: EditorUndoRedoManager
var _gizmos: Array
var _active_gizmos: Array
var _grabbed_gizmo: Gizmo2D
#region Godot ######################################################################################
func _enter_tree() -> void:
# TODO: remove the following 2 lines when the plugin is connected to the appropriate signal
# e.g. popochiu_ready
PopochiuEditorConfig.initialize_editor_settings()
PopochiuConfig.initialize_project_settings()
# Initialization of the plugin goes here.
_undo = get_undo_redo()
_gizmos.insert(WALK_TO_POINT, _init_popochiu_gizmo(WALK_TO_POINT))
_gizmos.insert(LOOK_AT_POINT, _init_popochiu_gizmo(LOOK_AT_POINT))
_gizmos.insert(BASELINE, _init_popochiu_gizmo(BASELINE))
_gizmos.insert(DIALOG_POS, _init_popochiu_gizmo(DIALOG_POS))
EditorInterface.get_editor_settings().settings_changed.connect(_on_gizmo_settings_changed)
PopochiuEditorHelper.signal_bus.gizmo_visibility_changed.connect(_on_gizmo_visibility_changed)
#endregion
#region Virtual ####################################################################################
func _edit(object: Object) -> void:
if object == null or object.get_class() == "EditorDebuggerRemoteObject":
return
_target_node = object
_active_gizmos.clear()
if EditorInterface.get_edited_scene_root() is PopochiuCharacter:
_active_gizmos.append(_gizmos[DIALOG_POS])
elif EditorInterface.get_edited_scene_root() is PopochiuRoom:
_active_gizmos.append(_gizmos[WALK_TO_POINT])
_active_gizmos.append(_gizmos[LOOK_AT_POINT])
_active_gizmos.append(_gizmos[BASELINE])
for gizmo in _active_gizmos:
gizmo.set_target_node(_target_node)
if not EditorInterface.get_inspector().property_edited.is_connected(_on_property_changed):
EditorInterface.get_inspector().property_edited.connect(_on_property_changed)
update_overlays()
func _forward_canvas_draw_over_viewport(viewport_control: Control) -> void:
for gizmo in _active_gizmos:
gizmo.draw(viewport_control, _target_node.get(gizmo.target_property))
func _handles(object: Object) -> bool:
return object is PopochiuClickable
func _forward_canvas_gui_input(event: InputEvent) -> bool:
if not _target_node or not _target_node.is_visible_in_tree():
return false
# For left mouse buttons, try to grab or release, depending on state
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT:
# Grab
if not _grabbed_gizmo and event.is_pressed():
return _try_grab_gizmo(event)
# Release
elif _grabbed_gizmo and event.is_released():
return _release_gizmo(event)
# For mouse movement, drag the grabbed gizmo
if event is InputEventMouseMotion:
return _drag_gizmo(event)
# For ESC key or comparable events, cancel the dragging if in place
if event.is_action_pressed("ui_cancel"):
return _cancel_dragging_gizmo(event)
## Nothing to handle outside the cases above
return false
#endregion
#region Private ####################################################################################
func _on_property_changed(property: String):
update_overlays()
func _on_gizmo_settings_changed() -> void:
var gizmo_id = 0
var default_font = EditorInterface.get_editor_theme().default_font
for gizmo in _gizmos:
match gizmo_id:
WALK_TO_POINT:
gizmo.set_theme(
PopochiuEditorConfig.get_editor_setting(PopochiuEditorConfig.GIZMOS_WALK_TO_POINT_COLOR),
PopochiuEditorConfig.get_editor_setting(PopochiuEditorConfig.GIZMOS_HANDLER_SIZE),
default_font,
PopochiuEditorConfig.get_editor_setting(PopochiuEditorConfig.GIZMOS_FONT_SIZE)
)
LOOK_AT_POINT:
gizmo.set_theme(
PopochiuEditorConfig.get_editor_setting(PopochiuEditorConfig.GIZMOS_LOOK_AT_POINT_COLOR),
PopochiuEditorConfig.get_editor_setting(PopochiuEditorConfig.GIZMOS_HANDLER_SIZE),
default_font,
PopochiuEditorConfig.get_editor_setting(PopochiuEditorConfig.GIZMOS_FONT_SIZE)
)
BASELINE:
gizmo.set_theme(
PopochiuEditorConfig.get_editor_setting(PopochiuEditorConfig.GIZMOS_BASELINE_COLOR),
PopochiuEditorConfig.get_editor_setting(PopochiuEditorConfig.GIZMOS_HANDLER_SIZE),
default_font,
PopochiuEditorConfig.get_editor_setting(PopochiuEditorConfig.GIZMOS_FONT_SIZE)
)
DIALOG_POS:
gizmo.set_theme(
PopochiuEditorConfig.get_editor_setting(PopochiuEditorConfig.GIZMOS_DIALOG_POS_COLOR),
PopochiuEditorConfig.get_editor_setting(PopochiuEditorConfig.GIZMOS_HANDLER_SIZE),
default_font,
PopochiuEditorConfig.get_editor_setting(PopochiuEditorConfig.GIZMOS_FONT_SIZE)
)
gizmo.show_connector = PopochiuEditorConfig.get_editor_setting(PopochiuEditorConfig.GIZMOS_SHOW_CONNECTORS)
gizmo.show_outlines = PopochiuEditorConfig.get_editor_setting(PopochiuEditorConfig.GIZMOS_SHOW_OUTLINE)
gizmo.show_target_name = PopochiuEditorConfig.get_editor_setting(PopochiuEditorConfig.GIZMOS_SHOW_NODE_NAME)
gizmo_id += 1
update_overlays()
func _on_gizmo_visibility_changed(gizmo_id:int, visibility:bool):
if gizmo_id < _gizmos.size():
_gizmos[gizmo_id].visible = visibility
update_overlays()
func _update_properties():
if _grabbed_gizmo and _grabbed_gizmo.target_property:
_target_node.set(
_grabbed_gizmo.target_property,
_grabbed_gizmo.get_position()
)
func _init_popochiu_gizmo(gizmo_id: int) -> Gizmo2D:
var gizmo: Gizmo2D
var default_font = EditorInterface.get_editor_theme().default_font
match gizmo_id:
WALK_TO_POINT:
gizmo = Gizmo2D.new(_target_node, "walk_to_point", "Walk To Point", Gizmo2D.GIZMO_POS)
gizmo.set_theme(
PopochiuEditorConfig.get_editor_setting(PopochiuEditorConfig.GIZMOS_WALK_TO_POINT_COLOR),
PopochiuEditorConfig.get_editor_setting(PopochiuEditorConfig.GIZMOS_HANDLER_SIZE),
default_font,
PopochiuEditorConfig.get_editor_setting(PopochiuEditorConfig.GIZMOS_FONT_SIZE)
)
LOOK_AT_POINT:
gizmo = Gizmo2D.new(_target_node, "look_at_point", "Look At Point", Gizmo2D.GIZMO_POS)
gizmo.set_theme(
PopochiuEditorConfig.get_editor_setting(PopochiuEditorConfig.GIZMOS_LOOK_AT_POINT_COLOR),
PopochiuEditorConfig.get_editor_setting(PopochiuEditorConfig.GIZMOS_HANDLER_SIZE),
default_font,
PopochiuEditorConfig.get_editor_setting(PopochiuEditorConfig.GIZMOS_FONT_SIZE)
)
BASELINE:
gizmo = Gizmo2D.new(_target_node, "baseline", "Baseline", Gizmo2D.GIZMO_VPOS)
gizmo.set_theme(
PopochiuEditorConfig.get_editor_setting(PopochiuEditorConfig.GIZMOS_BASELINE_COLOR),
PopochiuEditorConfig.get_editor_setting(PopochiuEditorConfig.GIZMOS_HANDLER_SIZE),
default_font,
PopochiuEditorConfig.get_editor_setting(PopochiuEditorConfig.GIZMOS_FONT_SIZE)
)
DIALOG_POS:
gizmo = Gizmo2D.new(_target_node, "dialog_pos", "Dialog Position", Gizmo2D.GIZMO_POS)
gizmo.set_theme(
PopochiuEditorConfig.get_editor_setting(PopochiuEditorConfig.GIZMOS_DIALOG_POS_COLOR),
PopochiuEditorConfig.get_editor_setting(PopochiuEditorConfig.GIZMOS_HANDLER_SIZE),
default_font,
PopochiuEditorConfig.get_editor_setting(PopochiuEditorConfig.GIZMOS_FONT_SIZE)
)
return gizmo
func _try_grab_gizmo(event: InputEventMouseButton) -> bool:
# Check if the mouse click happened on a gizmo
# The order is reversed to the topmost gizmo
# (the last been drawn) is selected
for i in range(_active_gizmos.size() - 1, -1, -1):
if not _active_gizmos[i].has_point(event.position):
continue
_grabbed_gizmo = _active_gizmos[i]
break
# If user clicked on no gizmos
# ignore the event
if not _grabbed_gizmo:
return false
# hold the gizmo with the mouse
_grabbed_gizmo.grab(event.position)
_undo.create_action("Move gizmo")
_undo.add_undo_property(
_grabbed_gizmo.target_node,
_grabbed_gizmo.target_property,
_grabbed_gizmo.target_node.get(_grabbed_gizmo.target_property)
)
update_overlays()
return true
func _release_gizmo(event: InputEvent) -> bool:
# If there is no gizmo to release
# ignore the event
if not _grabbed_gizmo:
return false
_grabbed_gizmo.release()
_undo.add_do_property(
_grabbed_gizmo.target_node,
_grabbed_gizmo.target_property,
_grabbed_gizmo.target_node.get(_grabbed_gizmo.target_property)
)
_undo.commit_action()
update_overlays()
_grabbed_gizmo = null
return true
func _drag_gizmo(event: InputEvent) -> bool:
# If no gizmo to drag
# ignore the event
if not _grabbed_gizmo:
return false
# Drag the gizmo
_grabbed_gizmo.drag_to(event.position)
_update_properties()
update_overlays()
return true
func _cancel_dragging_gizmo(event: InputEvent) -> bool:
# If ESC/Cancel happens but we're not dragging
# ignore the event
if not _grabbed_gizmo:
return false
# Cancel the action
_grabbed_gizmo.cancel()
_undo.commit_action()
_undo.get_history_undo_redo(_undo.get_object_history_id(_grabbed_gizmo.target_node)).undo()
update_overlays()
_grabbed_gizmo = null
return true
#endregion

View file

@ -0,0 +1 @@
uid://1t0c81s6wd7c

View file

@ -0,0 +1,7 @@
[plugin]
name="PopochiuGizmoClickable"
description="Provides viewport-drawn interactive 2D gizmos to Popochiu objects in the editor."
author="Carenalgas Dev Team"
version="2.0"
script="gizmo_clickable_plugin.gd"

View file

@ -0,0 +1,323 @@
@tool
class_name PopochiuEditorHelper
extends Resource
## Utils class for Editor related things.
# ---- Strings, paths, scenes, and other values ----------------------------------------------------
const POPUPS_FOLDER = "res://addons/popochiu/editor/popups/"
const CREATE_OBJECT_FOLDER = "res://addons/popochiu/editor/popups/create_object/"
const CREATE_ROOM = preload(CREATE_OBJECT_FOLDER + "create_room/create_room.tscn")
const CREATE_CHARACTER = preload(CREATE_OBJECT_FOLDER + "create_character/create_character.tscn")
const CREATE_INVENTORY_ITEM = preload(
CREATE_OBJECT_FOLDER + "create_inventory_item/create_inventory_item.tscn"
)
const CREATE_DIALOG = preload(CREATE_OBJECT_FOLDER + "create_dialog/create_dialog.tscn")
const CREATE_PROP = preload(CREATE_OBJECT_FOLDER + "create_prop/create_prop.tscn")
const CREATE_HOTSPOT = preload(CREATE_OBJECT_FOLDER + "create_hotspot/create_hotspot.tscn")
const CREATE_WALKABLE_AREA = preload(
CREATE_OBJECT_FOLDER + "create_walkable_area/create_walkable_area.tscn"
)
const CREATE_REGION = preload(CREATE_OBJECT_FOLDER + "create_region/create_region.tscn")
const CREATE_MARKER = preload(CREATE_OBJECT_FOLDER + "create_marker/create_marker.tscn")
const DELETE_CONFIRMATION_SCENE = preload(
POPUPS_FOLDER + "delete_confirmation/delete_confirmation.tscn"
)
const PROGRESS_DIALOG_SCENE = preload(POPUPS_FOLDER + "progress/progress.tscn")
const SETUP_SCENE = preload("res://addons/popochiu/editor/popups/setup/setup.tscn")
# ---- Identifiers ---------------------------------------------------------------------------------
const POPOCHIU_OBJECT_POLYGON_GROUP = "popochiu_object_polygon"
const MIGRATIONS_PANEL_SCENE = preload(
"res://addons/popochiu/editor/popups/migrations_panel/migrations_panel.tscn"
)
# ---- Classes -------------------------------------------------------------------------------------
const PopochiuSignalBus = preload("res://addons/popochiu/editor/helpers/popochiu_signal_bus.gd")
const DeleteConfirmation = preload(POPUPS_FOLDER + "delete_confirmation/delete_confirmation.gd")
const Progress = preload(POPUPS_FOLDER + "progress/progress.gd")
const CreateObject = preload(CREATE_OBJECT_FOLDER + "create_object.gd")
const MigrationsPanel = preload(
"res://addons/popochiu/editor/popups/migrations_panel/migrations_panel.gd"
)
static var signal_bus := PopochiuSignalBus.new()
static var ei := EditorInterface
static var undo_redo: EditorUndoRedoManager = null
static var dock: Panel = null
static var _room_scene_path_template := PopochiuResources.ROOMS_PATH.path_join("%s/room_%s.tscn")
#region Public #####################################################################################
static func select_node(node: Node) -> void:
ei.get_selection().clear()
ei.get_selection().add_node(node)
static func show_popup(popup_name: String) -> void:
PopochiuUtils.print_normal(popup_name)
static func add_resource_to_popochiu(target: String, resource: Resource) -> int:
return PopochiuResources.set_data_value(target, resource.script_name, resource.resource_path)
static func show_delete_confirmation(
content: DeleteConfirmation, min_size := Vector2i(640, 160)
) -> void:
var dialog := ConfirmationDialog.new()
dialog.title = content.title
dialog.confirmed.connect(
func () -> void:
if content.on_confirmed:
content.on_confirmed.call()
dialog.queue_free()
)
dialog.canceled.connect(
func () -> void:
if content.on_canceled:
content.on_canceled.call()
dialog.queue_free()
)
dialog.about_to_popup.connect(content.on_about_to_popup)
dialog.add_child(content)
await show_dialog(dialog, min_size)
static func show_progress(min_size := Vector2i(640, 80)) -> Progress:
var dialog := AcceptDialog.new()
var content: Progress = PROGRESS_DIALOG_SCENE.instantiate()
dialog.borderless = true
dialog.add_child(content)
dialog.get_ok_button().hide()
await show_dialog(dialog, min_size)
return content
static func show_creation_popup(scene: PackedScene, min_size := Vector2i(640, 180)) -> void:
var content: CreateObject = scene.instantiate()
var dialog := ConfirmationDialog.new()
content.content_changed.connect(
func () -> void:
content.custom_minimum_size = content.get_child(0).size
content.size = content.get_child(0).size
dialog.reset_size()
dialog.move_to_center()
)
dialog.confirmed.connect(content.create)
dialog.canceled.connect(dialog.queue_free)
dialog.about_to_popup.connect(content.on_about_to_popup)
dialog.add_child(content)
await show_dialog(dialog, min_size)
dialog.register_text_enter(content.input)
static func show_setup(is_welcome := false) -> void:
var dialog := ConfirmationDialog.new()
var content := SETUP_SCENE.instantiate()
dialog.title = "Setup"
dialog.confirmed.connect(content.on_close)
dialog.close_requested.connect(content.on_close)
dialog.about_to_popup.connect(content.on_about_to_popup)
dialog.add_child(content)
dock.add_child.call_deferred(dialog)
await dialog.ready
content.define_content(is_welcome)
content.size_calculated.connect(
func () -> void:
dialog.reset_size()
dialog.move_to_center()
)
await show_dialog(dialog, content.custom_minimum_size)
static func show_migrations(
content: MigrationsPanel, min_size := Vector2i(640, 640)
) -> AcceptDialog:
var dialog := AcceptDialog.new()
dialog.title = "Migration Tool"
content.anchors_preset = Control.PRESET_FULL_RECT
dialog.add_child(content)
await show_dialog(dialog, min_size)
return dialog
static func show_dialog(dialog: Window, min_size := Vector2i.ZERO) -> void:
if not dialog.is_inside_tree():
dock.add_child.call_deferred(dialog)
await dialog.ready
dialog.popup_centered(min_size * EditorInterface.get_editor_scale())
# Type-checking functions
static func is_popochiu_clickable(node: Node) -> bool:
return node is PopochiuCharacter \
or node is PopochiuProp \
or node is PopochiuHotspot
static func is_popochiu_object(node: Node) -> bool:
return node is PopochiuRoom \
or is_popochiu_room_object(node)
static func is_popochiu_room_object(node: Node) -> bool:
return node is PopochiuCharacter \
or node is PopochiuProp \
or node is PopochiuHotspot \
or node is PopochiuWalkableArea \
or node is PopochiuRegion
static func is_room(node: Node) -> bool:
return node is PopochiuRoom
static func is_character(node: Node) -> bool:
return node is PopochiuCharacter
static func is_prop(node: Node) -> bool:
return node is PopochiuProp
static func is_hotspot(node: Node) -> bool:
return node is PopochiuHotspot
static func is_walkable_area(node: Node) -> bool:
return node is PopochiuWalkableArea
static func is_region(node: Node) -> bool:
return node is PopochiuRegion
static func is_marker(node: Node) -> bool:
return node is Marker2D
static func is_popochiu_obj_polygon(node: Node):
return node.is_in_group(POPOCHIU_OBJECT_POLYGON_GROUP)
# Context-checking functions
static func is_editing_room() -> bool:
# If the open scene in the editor is a PopochiuRoom, return true
return is_room(ei.get_edited_scene_root())
# Quick-access functions
static func get_first_child_by_group(node: Node, group: StringName) -> Node:
if (node == null):
return null
for n in node.get_children():
if n.is_in_group(group):
return n
return null
static func get_all_children(node, children := []) -> Array:
if node == null:
return []
children.push_back(node)
for child in node.get_children():
children = get_all_children(child, children)
return children
## Overrides the font [param font_name] in [param node] by the theme [Font] identified by
## [param editor_font_name].
static func override_font(node: Control, font_name: String, editor_font_name: String) -> void:
node.add_theme_font_override(font_name, node.get_theme_font(editor_font_name, "EditorFonts"))
static func frame_processed() -> void:
await EditorInterface.get_base_control().get_tree().process_frame
static func secs_passed(secs := 1.0) -> void:
await EditorInterface.get_base_control().get_tree().create_timer(secs).timeout
static func filesystem_scanned() -> void:
EditorInterface.get_resource_filesystem().scan.call_deferred()
await EditorInterface.get_resource_filesystem().filesystem_changed
static func pack_scene(node: Node, path := "") -> int:
var packed_scene := PackedScene.new()
packed_scene.pack(node)
if path.is_empty():
path = node.scene_file_path
return ResourceSaver.save(packed_scene, path)
## Helper function to recursively remove all folders and files inside [param folder_path].
static func remove_recursive(folder_path: String) -> bool:
if DirAccess.dir_exists_absolute(folder_path):
# Delete subfolders and their contents recursively in folder_path
for subfolder_path: String in get_absolute_directory_paths_at(folder_path):
remove_recursive(subfolder_path)
# Delete all files in folder_path
for file_path: String in get_absolute_file_paths_at(folder_path):
if DirAccess.remove_absolute(file_path) != OK:
return false
# Once all files are deleted in folder_path, remove folder_path
if DirAccess.remove_absolute(folder_path) != OK:
return false
return true
## Helper function to get the absolute directory paths for all folders under [param folder_path].
static func get_absolute_directory_paths_at(folder_path: String) -> Array:
var dir_array : PackedStringArray = []
if DirAccess.dir_exists_absolute(folder_path):
for folder in DirAccess.get_directories_at(folder_path):
dir_array.append(folder_path.path_join(folder))
return Array(dir_array)
## Helper function to get the absolute file paths for all files under [param folder_path].
static func get_absolute_file_paths_at(folder_path: String) -> PackedStringArray:
var file_array : PackedStringArray = []
if DirAccess.dir_exists_absolute(folder_path):
for file in DirAccess.get_files_at(folder_path):
file_array.append(folder_path.path_join(file))
return file_array
## Returns an array of [PopochiuRoom] (instances) for all the rooms in the project.
static func get_rooms() -> Array[PopochiuRoom]:
var rooms: Array[PopochiuRoom] = []
rooms.assign(PopochiuResources.get_section_keys("rooms").map(
func (room_name: String) -> PopochiuRoom:
var scene_path := _room_scene_path_template.replace("%s", room_name.to_snake_case())
return (load(scene_path) as PackedScene).instantiate(PackedScene.GEN_EDIT_STATE_INSTANCE)
))
return rooms
#endregion

View file

@ -0,0 +1 @@
uid://bgogdq51wvqyn

View file

@ -0,0 +1,359 @@
@tool
extends Resource
## Helper class for operations related to the GUI templates
static var _template_id := ""
static var _template_theme_path := ""
#region Public #####################################################################################
## Creates a copy of the selected template, including its components. Also generate the necessary
## scripts to define custom logic for the graphical interface and its commands.
static func copy_gui_template(
template_name: String, on_progress: Callable, on_complete: Callable
) -> void:
if (
DirAccess.dir_exists_absolute(PopochiuResources.GUI_GAME_FOLDER)
and template_name == PopochiuResources.get_data_value("ui", "template", "")
):
PopochiuUtils.print_normal("No changes in GUI template.")
on_complete.call()
return
on_progress.call(0, "Starting GUI template application")
_template_theme_path = ""
var scene_path := PopochiuResources.GUI_CUSTOM_SCENE
var commands_template_path := PopochiuResources.GUI_CUSTOM_TEMPLATE
_template_id = template_name.to_snake_case()
if _template_id != PopochiuResources.GUI_CUSTOM:
scene_path = PopochiuResources.GUI_TEMPLATES_FOLDER.path_join(
"%s/%s_gui.tscn" % [_template_id, _template_id]
)
commands_template_path = PopochiuResources.GUI_SCRIPT_TEMPLATES_FOLDER.path_join(
"%s_commands_template.gd" % _template_id
)
var script_path := PopochiuResources.GUI_GAME_SCENE.replace(".tscn", ".gd")
await _wait()
on_progress.call(5, "Creating Graphic Interface scene")
# ---- Make a copy of the selected GUI template ------------------------------------------------
if _create_scene(scene_path) != OK:
PopochiuUtils.print_error("Couldn't create %s file" % PopochiuResources.GUI_GAME_SCENE)
return
await _wait(2.0)
on_progress.call(10, "Copying a bunch of components")
# Copy the components used by the GUI template to the res://game/gui/components
# folder so devs can play with them freely -----------------------------------------------------
await copy_components(scene_path, true)
on_progress.call(60, "Creating scripts")
# Create a copy of the corresponding commands template -----------------------------------------
_copy_commands_and_gui_scripts(
commands_template_path, PopochiuResources.GUI_COMMANDS, script_path, scene_path
)
await _wait(1.5)
on_progress.call(80, "Assigning scripts")
# Update the script of the created gui.tscn so it uses the copy created above ------------------
if _update_scene_script(script_path) != OK:
PopochiuUtils.print_error("Couldn't update gui.tscn script")
return
await _wait()
on_progress.call(90, "Updating config file")
# Update the info related to the GUI template and the GUI commands script
# in the popochiu_data.cfg file ----------------------------------------------------------------
PopochiuResources.set_data_value("ui", "template", template_name)
await _wait(0.8)
on_progress.call(100, "All in place. Thanks for your patience.")
PopochiuUtils.print_normal("[wave]Selected GUI template successfully applied[/wave]")
await _wait()
await PopochiuEditorHelper.filesystem_scanned()
on_complete.call()
static func copy_component(source_scene_path: String) -> String:
var file_name := source_scene_path.get_file()
var source_folder := source_scene_path.get_base_dir()
var target_folder := source_folder.replace(PopochiuResources.GUI_ADDON_FOLDER, "")
target_folder = PopochiuResources.GUI_GAME_FOLDER + target_folder
if not DirAccess.dir_exists_absolute(target_folder):
DirAccess.make_dir_recursive_absolute(target_folder)
# Make a copy of the component and save it in the graphic interface components folder
var component_instance := (load(source_scene_path) as PackedScene).instantiate()
var target_scene_file := "%s/%s" % [target_folder, file_name]
var packed_scene := PackedScene.new()
packed_scene.pack(component_instance)
packed_scene.resource_path = target_scene_file
var err := ResourceSaver.save(packed_scene, target_scene_file)
if err != OK:
PopochiuUtils.print_error(
"Couldn't create instance of %s component. Error code: %d." % [
file_name.get_slice(".", 0).capitalize(),
err
]
)
return ""
# Move the dependencies of the source scene to the graphic interface folder
await copy_components(source_scene_path)
return target_scene_file
## Makes a copy of the components used by the original GUI template to the
## **res://game/gui/components/** folder so devs can play with those scenes without
## affecting the ones in the plugin's folder.
static func copy_components(source_scene_path: String, is_gui_game_scene := false) -> void:
var dependencies_to_update: Array[Dictionary] = []
# Make a copy of the dependencies of the graphic interface
for dep: String in ResourceLoader.get_dependencies(source_scene_path):
var source_file_path := dep.get_slice("::", 2)
if (
is_gui_game_scene and source_file_path.get_extension() == "gd"
and source_scene_path.get_base_dir() == source_file_path.get_base_dir()
):
# Ignore the script of the GUI template scene file
continue
var source_file_uid := ResourceUID.id_to_text(
ResourceLoader.get_resource_uid(source_file_path)
)
var dependency_data := {
source_path = source_file_path,
source_uid = source_file_uid,
target_path = "",
}
# ---- Create the folder of the file -------------------------------------------------------
var file_name := source_file_path.get_file()
var source_folder := source_file_path.get_base_dir()
var target_folder := source_folder.replace(PopochiuResources.GUI_ADDON_FOLDER, "")
target_folder = target_folder.replace("templates/%s/" % _template_id, "")
target_folder = PopochiuResources.GUI_GAME_FOLDER + target_folder
dependency_data.target_path = "%s/%s" % [target_folder, file_name]
# Make sure all the copied components share the copied GUI theme
if source_file_path.get_extension() == "tres" and "_theme" in source_file_path:
if is_gui_game_scene and _template_theme_path.is_empty():
# Change the name of the GUI template theme so it differs from the original one
dependency_data.target_path = "%s/%s" % [
target_folder, "gui_theme.tres"
]
_template_theme_path = dependency_data.target_path
elif not is_gui_game_scene:
dependency_data.target_path = _template_theme_path
dependencies_to_update.append(dependency_data)
if FileAccess.file_exists(dependency_data.target_path):
# Ignore any file that has already been copied
continue
if not DirAccess.dir_exists_absolute(target_folder):
DirAccess.make_dir_recursive_absolute(target_folder)
# --- Make a copy of the original file -----------------------------------------------------
if source_file_path.get_extension() == "gd":
_copy_component_script(source_file_path, dependency_data.target_path)
else:
_copy_file(source_file_path, target_folder, dependency_data.target_path)
EditorInterface.get_resource_filesystem().scan()
await EditorInterface.get_resource_filesystem().filesystem_changed
# Repeat the process for each of the dependencies of the .tscn resources
if source_file_path.get_extension() == "tscn":
await copy_components(source_file_path)
if is_gui_game_scene:
_update_dependencies(PopochiuResources.GUI_GAME_SCENE, dependencies_to_update)
else:
var game_scene_path := source_scene_path.replace(PopochiuResources.GUI_ADDON_FOLDER, "")
game_scene_path = game_scene_path.replace("templates/%s/" % _template_id, "")
game_scene_path = PopochiuResources.GUI_GAME_FOLDER + game_scene_path
_update_dependencies(game_scene_path, dependencies_to_update)
EditorInterface.get_resource_filesystem().scan()
await EditorInterface.get_resource_filesystem().filesystem_changed
#endregion
#region Private ####################################################################################
## Create the **gui.tscn** file as a copy of the selected GUI template scene.
## If a template change is being made, all components of the previous template are removed along
## with the **.tscn** file before copying the new one.
static func _create_scene(scene_path: String) -> int:
# Create the res://game/gui/ folder
if not FileAccess.file_exists(PopochiuResources.GUI_GAME_SCENE):
DirAccess.make_dir_recursive_absolute(PopochiuResources.GUI_GAME_FOLDER)
else:
# Remove the gui.tscn file
DirAccess.remove_absolute(PopochiuResources.GUI_GAME_SCENE)
EditorInterface.get_resource_filesystem().scan()
for dir_name: String in DirAccess.get_directories_at(PopochiuResources.GUI_GAME_FOLDER):
_remove_components(PopochiuResources.GUI_GAME_FOLDER + dir_name)
# Make a copy of the selected GUI template (.tscn) and save it in
# res://game/gui/gui.tscn ------------------------------------------
var gi_scene := load(scene_path).duplicate(true)
gi_scene.resource_path = PopochiuResources.GUI_GAME_SCENE
return ResourceSaver.save(gi_scene, PopochiuResources.GUI_GAME_SCENE)
static func _remove_components(dir_path: String) -> void:
for file_name: String in DirAccess.get_files_at(dir_path):
DirAccess.remove_absolute(dir_path.path_join(file_name))
EditorInterface.get_resource_filesystem().scan()
for dir_name: String in DirAccess.get_directories_at(dir_path):
var sub_dir_path := dir_path.path_join(dir_name)
_remove_components(sub_dir_path)
# Once the directory is empty, remove it
DirAccess.remove_absolute(dir_path)
EditorInterface.get_resource_filesystem().scan()
## Makes a copy of a GUI component's script.
static func _copy_component_script(
source_file_path: String, target_file_path: String
) -> void:
# Make a copy of the original script -----------------------------------------------------------
var source_file := FileAccess.open(source_file_path, FileAccess.READ)
var source_code := source_file.get_as_text()
source_file.close()
if "class_name " in source_code:
source_code = source_code.replace("class_name Popochiu", "class_name GUI")
var file_write := FileAccess.open(target_file_path, FileAccess.WRITE)
file_write.store_string(source_code)
file_write.close()
# Create a script for devs to overwrite the functionality of the original's copy script --------
file_write = FileAccess.open(target_file_path.replace(".gd", "_custom.gd"), FileAccess.WRITE)
if "@tool" in source_code:
file_write.store_string("@tool\n")
file_write.store_string('extends "%s"' % target_file_path.get_file())
static func _copy_file(
source_file_path: String, target_folder: String, target_file_path: String
) -> void:
# ---- Create a copy of the scene file ---------------------------------------------------------
if source_file_path.get_extension() in ["tscn", "tres"]:
var file_resource := load(source_file_path).duplicate(true)
file_resource.resource_path = target_file_path
if ResourceSaver.save(file_resource, target_file_path) != OK:
DirAccess.remove_absolute(target_folder)
else:
DirAccess.copy_absolute(source_file_path, target_file_path)
## Replace the UID and paths of the components in the graphic interface scene
static func _update_dependencies(scene_path: String, dependencies_to_update: Array) -> void:
if dependencies_to_update.is_empty():
return
# ---- Update the UID and paths of the copied components ---------------------------------------
var file_read = FileAccess.open(scene_path, FileAccess.READ)
var text := file_read.get_as_text()
file_read.close()
for dic: Dictionary in dependencies_to_update:
var target_path: String = dic.target_path
if ".gd" in target_path:
target_path = target_path.replace(".gd", "_custom.gd")
text = text.replace(dic.source_path, target_path)
var target_uid := ResourceUID.id_to_text(ResourceLoader.get_resource_uid(target_path))
if "invalid" in target_uid: continue
text = text.replace(dic.source_uid, target_uid)
var file_write = FileAccess.open(scene_path, FileAccess.WRITE)
file_write.store_string(text)
file_write.close()
## Copy the commands and graphic interface scripts of the chosen GUI template. The new graphic
## interface scripts inherits from the one originally assigned to the .tscn file of the selected
## template.
static func _copy_commands_and_gui_scripts(
commands_template_path: String, commands_path: String, script_path: String, scene_path: String
) -> void:
DirAccess.copy_absolute(commands_template_path, commands_path)
# Create a copy of the graphic interface script template ---------------------------------------
var template_path := (
PopochiuResources.GUI_SCRIPT_TEMPLATES_FOLDER + "gui_template.gd"
)
var script_file := FileAccess.open(template_path, FileAccess.READ)
var source_code := script_file.get_as_text()
script_file.close()
source_code = source_code.replace(
"extends PopochiuGraphicInterface",
'extends "%s"' % scene_path.replace(".tscn", ".gd")
)
script_file = FileAccess.open(script_path, FileAccess.WRITE)
script_file.store_string(source_code)
script_file.close()
## Updates the script of the created [b]res://game/gui/gui.tscn[/b] file so it uses the one created
## in [method _copy_commands_and_gui_scripts].
static func _update_scene_script(script_path: String) -> int:
# Update the script of the GUI -----------------------------------------------------------------
var scene := (load(
PopochiuResources.GUI_GAME_SCENE
) as PackedScene).instantiate()
scene.set_script(load(script_path))
# Set the name of the root node
scene.name = "GUI"
var packed_scene: PackedScene = PackedScene.new()
packed_scene.pack(scene)
packed_scene.resource_path = PopochiuResources.GUI_GAME_SCENE
return ResourceSaver.save(packed_scene, PopochiuResources.GUI_GAME_SCENE)
static func _wait(max := 1.0) -> void:
await PopochiuEditorHelper.secs_passed(randf_range(0.5, max))
#endregion

View file

@ -0,0 +1 @@
uid://cn6emnkso14qe

View file

@ -0,0 +1,9 @@
extends RefCounted
## Helper Editor class to emit and connect to signals across different components in the plugin
signal main_scene_changed(scene_path: String)
signal pc_changed(script_name: String)
signal audio_cues_deleted(cue_file_paths: Array)
signal main_object_added(type: int, name_to_add: String)
signal gizmo_visibility_changed(gizmo: int, visible: bool)
signal migrations_done

View file

@ -0,0 +1 @@
uid://p35cn6n5xi6r

View file

@ -0,0 +1,390 @@
@tool
# This logic has been taken almost as-is from Vinicius Gerevini's
# Aseprite Wizard plugin. Credits goes to him for the real magic.
# See: https://godotengine.org/asset-library/asset/713
extends RefCounted
const RESULT_CODE = preload("res://addons/popochiu/editor/config/result_codes.gd")
const _DEFAULT_AL = "" # Empty string equals default "Global" animation library
# Vars configured on initialization
var _file_system: EditorFileSystem
var _aseprite: RefCounted
# Vars configured on animations creation
var _target_node: Node
var _player: AnimationPlayer
var _options: Dictionary
# Class-logic vars
var _spritesheet_metadata = {}
var _target_sprite: Sprite2D
var _output: Dictionary
#region Public #####################################################################################
func init(aseprite: RefCounted, editor_file_system: EditorFileSystem = null):
_file_system = editor_file_system
_aseprite = aseprite
## Public interfaces, dedicated to specific popochiu objects
func create_character_animations(character: Node, player: AnimationPlayer, options: Dictionary):
# Chores
_target_node = character
_player = player
_options = options
# Duly check everything is valid and cleanup animations
var result = _perform_common_checks()
if result != RESULT_CODE.SUCCESS:
return result
# Create the spritesheet
result = await _create_spritesheet_from_file()
if result != RESULT_CODE.SUCCESS:
return result
# Load tags information
result = await _load_spritesheet_metadata()
if result != RESULT_CODE.SUCCESS:
return result
# Set the texture in the sprite and configure
# the animations in the AnimationPlayer
_setup_texture()
result = _configure_animations()
return result
func create_prop_animations(prop: Node, aseprite_tag: String, options: Dictionary):
# Chores
_target_node = prop
# TODO: if the prop has no AnimationPlayer, add one!
_player = prop.get_node("AnimationPlayer")
_options = options
var prop_animation_name = aseprite_tag.to_snake_case()
# Duly check everything is valid and cleanup animations
var result = _perform_common_checks()
if result != RESULT_CODE.SUCCESS:
return result
# Create the spritesheet
result = await _create_spritesheet_from_tag(aseprite_tag)
if result != RESULT_CODE.SUCCESS:
return result
# Load tags information
result = await _load_spritesheet_metadata(aseprite_tag)
if result != RESULT_CODE.SUCCESS:
return result
# Set the texture in the sprite and configure
# the animations in the AnimationPlayer
_setup_texture()
result = _configure_animations()
# Sorry, mom...
_player.autoplay = prop.name.to_snake_case()
return result
#endregion
#region Private ####################################################################################
## This function creates a spritesheet with the whole file content
func _create_spritesheet_from_file():
## TODO: See _aseprite.export_layer() when the time comes to add layers selection
_output = _aseprite.export_file(_options.source, _options.output_folder, _options)
if _output.is_empty():
return RESULT_CODE.ERR_ASEPRITE_EXPORT_FAILED
return RESULT_CODE.SUCCESS
## This function creates a spritesheet with the frames of a specific tag
## WARNING: it's case sensitive
func _create_spritesheet_from_tag(selected_tag: String):
## TODO: See _aseprite.export_layer() when the time comes to add layers selection
_output = _aseprite.export_tag(_options.source, selected_tag, _options.output_folder, _options)
if _output.is_empty():
return RESULT_CODE.ERR_ASEPRITE_EXPORT_FAILED
return RESULT_CODE.SUCCESS
func _load_spritesheet_metadata(selected_tag: String = ""):
_spritesheet_metadata = {
tags = {},
frames = {},
meta = {},
sprite_sheet = {}
}
# Refresh filesystem
await _scan_filesystem()
# Collect all needed info
var source_file = _output.data_file
var sprite_sheet = _output.sprite_sheet
# Try to access, decode and validate Aseprite JSON output
var file = FileAccess.open(source_file, FileAccess.READ)
if file == null:
return file.get_open_error()
var test_json_conv = JSON.new()
test_json_conv.parse(file.get_as_text())
var content = test_json_conv.get_data()
if not _aseprite.is_valid_spritesheet(content):
return RESULT_CODE.ERR_INVALID_ASEPRITE_SPRITESHEET
# Save image metadata from JSON data
_spritesheet_metadata.meta = content.meta
# Save frames metadata from JSON data
_spritesheet_metadata.frames = _aseprite.get_content_frames(content)
# Save tags metadata, starting from user's selection, and retrieving
# other information from JSON data
var tags = _options.get("tags").filter(func(tag): return tag.get("import"))
for t in tags:
# If a tag is specified, ignore every other ones
if not selected_tag.is_empty() and selected_tag != t.tag_name: continue
# Create a lookup table for tags
_spritesheet_metadata.tags[t.tag_name] = t
for ft in _aseprite.get_content_meta_tags(content):
if not _spritesheet_metadata.tags.has(ft.name): continue
_spritesheet_metadata.tags.get(ft.name).merge({
from = ft.from,
to = ft.to,
direction = ft.direction,
})
# If a tag is specified, the tags lookup table should contain
# a single tag information. In this case the to and from properties
# must be shifted back in the [1 - tag_length] range.
if not selected_tag.is_empty():
# Using a temp variable to make this readable
var t = _spritesheet_metadata.tags[selected_tag]
# NOTE: imagine this goes from 34 to 54, we need to shift
# the range back of a 33 amount, so it goes from 1 to (54 - 33)
t.to = t.to - t.from + 1
t.from = 0
_spritesheet_metadata.tags[selected_tag] = t
# Save spritesheet path from the command output
_spritesheet_metadata.sprite_sheet = sprite_sheet
# Remove the JSON file if config says so
if PopochiuEditorConfig.should_remove_source_files():
DirAccess.remove_absolute(_output.data_file)
await _scan_filesystem()
return RESULT_CODE.SUCCESS
func _configure_animations():
if not _player.has_animation_library(_DEFAULT_AL):
_player.add_animation_library(_DEFAULT_AL, AnimationLibrary.new())
if _spritesheet_metadata.tags.size() > 0:
var result = RESULT_CODE.SUCCESS
# RESTART_FROM_HERE: WARNING: in case of prop and inventory, the JSON file contains
# the whole set of tags, so we must take the tag.from and tag.to and remap the range
# from "1" to "tag.to +1 - tag.from + 1" (do the math an you'll see that's correct)
for tag in _spritesheet_metadata.tags.values():
var selected_frames = _spritesheet_metadata.frames.slice(tag.from, tag.to + 1) # slice is [)
result = _add_animation_frames(tag.tag_name, selected_frames, tag.direction)
if result != RESULT_CODE.SUCCESS:
break
return result
else:
return _add_animation_frames("default", _spritesheet_metadata.frames)
func _add_animation_frames(anim_name: String, frames: Array, direction = 'forward'):
# TODO: ATM there is no way to assign a walk/talk/grab/idle animation
# with a different name than the standard ones. The engine is searching for
# lowercase names in the AnimationPlayer, thus we are forcing snake_case
# animations name conversion.
# We have to add methods or properties to the Character to assign different
# animations (but maybe we can do with anim_prefix or other strategies).
var animation_name = anim_name.to_snake_case()
var is_loopable = _spritesheet_metadata.tags.get(anim_name).get("loops")
# Create animation library if it doesn't exist
# This is always true if the user selected to wipe old animations.
# See _remove_animations_from_player() function.
if not _player.has_animation_library(_DEFAULT_AL):
_player.add_animation_library(_DEFAULT_AL, AnimationLibrary.new())
if not _player.get_animation_library(_DEFAULT_AL).has_animation(animation_name):
_player.get_animation_library(_DEFAULT_AL).add_animation(animation_name, Animation.new())
# Here is where animations are created.
# TODO: we need to "fork" the logic so that Character has a single spritesheet
# containing all tags, while Rooms/Props and Inventory Items has a single spritesheet
# for each tag, so that you can have each prop with its own animation (PnC)
var animation = _player.get_animation(animation_name)
_create_meta_tracks(animation)
var frame_track = _get_property_track_path("frame")
var frame_track_index = _create_track(_target_sprite, animation, frame_track)
if direction == 'reverse':
frames.reverse()
var animation_length = 0
for frame in frames:
var frame_key = _get_frame_key(frame)
animation.track_insert_key(frame_track_index, animation_length, frame_key)
animation_length += frame.duration / 1000 ## NOTE: animation_length is in seconds
if direction == 'pingpong':
frames.remove_at(frames.size() - 1)
if is_loopable:
frames.remove_at(0)
frames.reverse()
for frame in frames:
var frame_key = _get_frame_key(frame)
animation.track_insert_key(frame_track_index, animation_length, frame_key)
animation_length += frame.duration / 1000 ## NOTE: animation_length is in seconds
animation.length = animation_length
animation.loop_mode = Animation.LOOP_LINEAR if is_loopable else Animation.LOOP_NONE
return RESULT_CODE.SUCCESS
## TODO: insert validate tokens in animation name
func _create_track(target_sprite: Node, animation: Animation, track: String):
var track_index = animation.find_track(track, Animation.TYPE_VALUE)
if track_index != -1:
animation.remove_track(track_index)
track_index = animation.add_track(Animation.TYPE_VALUE)
## Here we set a label for the track in the sprite_path:property_changed format
## so that _get_property_track_path can rebuild it by naming convention
animation.track_set_path(track_index, track)
animation.track_set_interpolation_loop_wrap(track_index, false)
animation.value_track_set_update_mode(track_index, Animation.UPDATE_DISCRETE)
return track_index
func _get_property_track_path(prop: String) -> String:
var node_path = _player.get_node(_player.root_node).get_path_to(_target_sprite)
return "%s:%s" % [node_path, prop]
func _scan_filesystem():
_file_system.scan()
await _file_system.filesystem_changed
func _remove_properties_from_path(path: NodePath) -> NodePath:
var string_path := path as String
if !(":" in string_path):
return string_path as NodePath
var property_path := path.get_concatenated_subnames() as String
string_path = string_path.substr(0, string_path.length() - property_path.length() - 1)
return string_path as NodePath
# ---- SPRITE NODE LOGIC ---------------------------------------------------------------------------
## What follow is logic specifically gathered for Sprite elements. TextureRect should
## be treated in a different way (see texture_rect_animation_creator.gd file in
## original Aseprite Wizard plugin by Vinicius Gerevini)
func _setup_texture():
# Load texture in target sprite (ignoring cache and forcing a refres)
var texture = ResourceLoader.load(
_spritesheet_metadata.sprite_sheet, 'Image', ResourceLoader.CACHE_MODE_IGNORE
)
texture.take_over_path(_spritesheet_metadata.sprite_sheet)
_target_sprite.texture = texture
if _spritesheet_metadata.frames.is_empty():
return
_target_sprite.hframes = (
_spritesheet_metadata.meta.size.w / _spritesheet_metadata.frames[0].sourceSize.w
)
_target_sprite.vframes = (
_spritesheet_metadata.meta.size.h / _spritesheet_metadata.frames[0].sourceSize.h
)
func _create_meta_tracks(animation: Animation):
var hframes_track = _get_property_track_path("hframes")
var hframes_track_index = _create_track(_target_sprite, animation, hframes_track)
animation.track_insert_key(hframes_track_index, 0, _target_sprite.hframes)
var vframes_track = _get_property_track_path("vframes")
var vframes_track_index = _create_track(_target_sprite, animation, vframes_track)
animation.track_insert_key(vframes_track_index, 0, _target_sprite.vframes)
var visible_track = _get_property_track_path("visible")
var visible_track_index = _create_track(_target_sprite, animation, visible_track)
animation.track_insert_key(visible_track_index, 0, true)
func _get_frame_key(frame: Dictionary):
return _calculate_frame_index(_target_sprite,frame)
func _calculate_frame_index(sprite: Node, frame: Dictionary) -> int:
var column = floor(frame.frame.x * sprite.hframes / sprite.texture.get_width())
var row = floor(frame.frame.y * sprite.vframes / sprite.texture.get_height())
return (row * sprite.hframes) + column
func _perform_common_checks():
# Checks
if not _aseprite.check_command_path():
return RESULT_CODE.ERR_ASEPRITE_CMD_NOT_FULL_PATH
if not _aseprite.test_command():
return RESULT_CODE.ERR_ASEPRITE_CMD_NOT_FOUND
if not FileAccess.file_exists(_options.source):
return RESULT_CODE.ERR_SOURCE_FILE_NOT_FOUND
if not DirAccess.dir_exists_absolute(_options.output_folder):
return RESULT_CODE.ERR_OUTPUT_FOLDER_NOT_FOUND
_target_sprite = _find_sprite_in_target()
if _target_sprite == null:
return RESULT_CODE.ERR_NO_SPRITE_FOUND
if typeof(_options.get("tags")) != TYPE_ARRAY:
return RESULT_CODE.ERR_TAGS_OPTIONS_ARRAY_EMPTY
if (_options.wipe_old_animations):
_remove_animations_from_player(_player)
return RESULT_CODE.SUCCESS
func _find_sprite_in_target() -> Node:
if not _target_node.has_node("Sprite2D"):
return null
return _target_node.get_node("Sprite2D")
func _remove_animations_from_player(player: AnimationPlayer):
if player.has_animation_library(_DEFAULT_AL):
player.remove_animation_library(_DEFAULT_AL)
#endregion

View file

@ -0,0 +1 @@
uid://c44sonibms74d

View file

@ -0,0 +1,249 @@
@tool
extends RefCounted
# ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ PUBLIC ░░░░
func export_file(file_name: String, output_folder: String, options: Dictionary) -> Dictionary:
var exception_pattern = options.get('exception_pattern', "")
var only_visible_layers = options.get('only_visible_layers', false)
var output_name = (
file_name if options.get('output_filename') == ""
else options.get('output_filename', file_name)
)
var basename = _get_file_basename(output_name)
var output_dir = output_folder.replace("res://", "./")
var data_file = "%s/%s.json" % [output_dir, basename]
var sprite_sheet = "%s/%s.png" % [output_dir, basename]
var output = []
var arguments = _export_command_common_arguments(file_name, data_file, sprite_sheet)
if not only_visible_layers:
arguments.push_front("--all-layers")
_add_sheet_type_arguments(arguments, options)
_add_ignore_layer_arguments(file_name, arguments, exception_pattern)
var exit_code = _execute(arguments, output)
if exit_code != 0:
printerr('[Popochiu] Aseprite: failed to export spritesheet')
printerr(output)
return {}
return {
'data_file': data_file.replace("./", "res://"),
"sprite_sheet": sprite_sheet.replace("./", "res://")
}
func export_layers(file_name: String, output_folder: String, options: Dictionary) -> Array:
var exception_pattern = options.get('exception_pattern', "")
var only_visible_layers = options.get('only_visible_layers', false)
var basename = _get_file_basename(file_name)
var layers = list_layers(file_name, only_visible_layers)
var exception_regex = _compile_regex(exception_pattern)
var output = []
for layer in layers:
if layer != "" and (not exception_regex or exception_regex.search(layer) == null):
output.push_back(export_layer(file_name, layer, output_folder, options))
return output
func export_layer(file_name: String, layer_name: String, output_folder: String, options: Dictionary) -> Dictionary:
var output_prefix = options.get('output_filename', "").strip_edges()
var output_dir = output_folder.replace("res://", "./").strip_edges()
var data_file = "%s/%s%s.json" % [output_dir, output_prefix, layer_name]
var sprite_sheet = "%s/%s%s.png" % [output_dir, output_prefix, layer_name]
var output = []
var arguments = _export_command_common_arguments(file_name, data_file, sprite_sheet)
arguments.push_front(layer_name)
arguments.push_front("--layer")
_add_sheet_type_arguments(arguments, options)
var exit_code = _execute(arguments, output)
if exit_code != 0:
printerr('[Popochiu] Aseprite: Failed to export layer spritesheet. Command output follows:')
print(output)
return {}
return {
'data_file': data_file.replace("./", "res://"),
"sprite_sheet": sprite_sheet.replace("./", "res://")
}
# IMPROVE: See if we can extract JSON data limited to the single tag
# (so we don't have to reckon offset framerange)
func export_tag(file_name: String, tag_name: String, output_folder: String, options: Dictionary) -> Dictionary:
var output_prefix = options.get('output_filename', "").strip_edges()
var output_dir = output_folder.replace("res://", "./").strip_edges()
var data_file = "%s/%s%s.json" % [output_dir, output_prefix, tag_name]
var sprite_sheet = "%s/%s%s.png" % [output_dir, output_prefix, tag_name]
var output = []
var arguments = _export_command_common_arguments(file_name, data_file, sprite_sheet)
arguments.push_front(tag_name)
arguments.push_front("--tag")
_add_sheet_type_arguments(arguments, options)
var exit_code = _execute(arguments, output)
if exit_code != 0:
printerr('[Popochiu] Aseprite: Failed to export tag spritesheet. Command output follows:')
print(output)
return {}
return {
'data_file': data_file.replace("./", "res://"),
"sprite_sheet": sprite_sheet.replace("./", "res://")
}
func list_layers(file_name: String, only_visible = false) -> Array:
var output = []
var arguments = ["-b", "--list-layers", file_name]
if not only_visible:
arguments.push_front("--all-layers")
var exit_code = _execute(arguments, output)
if exit_code != 0:
printerr('[Popochiu] Aseprite: failed listing layers')
printerr(output)
return []
return _sanitize_list_output(output)
func list_tags(file_name: String) -> Array:
var output = []
var arguments = ["-b", "--list-tags", file_name]
var exit_code = _execute(arguments, output)
if exit_code != 0:
printerr('[Popochiu] Aseprite: failed listing tags')
printerr(output)
return []
return _sanitize_list_output(output)
func is_valid_spritesheet(content):
return content.has("frames") and content.has("meta") and content.meta.has('image')
func get_content_frames(content):
return content.frames if typeof(content.frames) == TYPE_ARRAY else content.frames.values()
func get_content_meta_tags(content):
return content.meta.frameTags if content.meta.has("frameTags") else []
func check_command_path():
# On Linux, MacOS or other *nix platforms, nothing to do
if not OS.get_name() in ["Windows", "UWP"]:
return true
# On Windows, OS.Execute() calls trigger an uncatchable
# internal error if the invoked executable is not found.
# Since the error is unclear, we have to check that the aseprite
# command is given as a full path and return an error if it's not.
var regex = RegEx.new()
regex.compile("^[A-Z|a-z]:[\\\\|\\/].+\\.exe$")
return \
regex.search(_get_aseprite_command()) \
and \
FileAccess.file_exists(_get_aseprite_command())
func test_command():
var exit_code = OS.execute(_get_aseprite_command(), ['--version'], [], true)
return exit_code == 0
# ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ PRIVATE ░░░░
func _add_ignore_layer_arguments(file_name: String, arguments: Array, exception_pattern: String):
var layers = _get_exception_layers(file_name, exception_pattern)
if not layers.is_empty():
for l in layers:
arguments.push_front(l)
arguments.push_front('--ignore-layer')
func _add_sheet_type_arguments(arguments: Array, options : Dictionary):
var column_count : int = options.get("column_count", 0)
if column_count > 0:
arguments.push_back("--merge-duplicates") # Yes, this is undocumented
arguments.push_back("--sheet-columns")
arguments.push_back(column_count)
else:
arguments.push_back("--sheet-pack")
func _get_exception_layers(file_name: String, exception_pattern: String) -> Array:
var layers = list_layers(file_name)
var regex = _compile_regex(exception_pattern)
if regex == null:
return []
var exception_layers = []
for layer in layers:
if regex.search(layer) != null:
exception_layers.push_back(layer)
return exception_layers
func _sanitize_list_output(output) -> Array:
if output.is_empty():
return output
var raw = output[0].split('\n')
var sanitized = []
for s in raw:
sanitized.append(s.strip_edges())
return sanitized
func _export_command_common_arguments(source_name: String, data_path: String, spritesheet_path: String) -> Array:
return [
"-b",
"--list-tags",
"--data",
data_path,
"--format",
"json-array",
"--sheet",
spritesheet_path,
source_name
]
func _execute(arguments, output):
return OS.execute(_get_aseprite_command(), arguments, output, true, true)
func _get_aseprite_command() -> String:
return PopochiuEditorConfig.get_command()
func _get_file_basename(file_path: String) -> String:
return file_path.get_file().trim_suffix('.%s' % file_path.get_extension())
func _compile_regex(pattern):
if pattern == "":
return
var rgx = RegEx.new()
if rgx.compile(pattern) == OK:
return rgx
printerr('[Popochiu] exception regex error')

View file

@ -0,0 +1 @@
uid://d1vhl7uqwadfx

View file

@ -0,0 +1,94 @@
@tool
extends HBoxContainer
signal tag_state_changed
const RESULT_CODE = preload("res://addons/popochiu/editor/config/result_codes.gd")
var _anim_tag_state: Dictionary = {}
@onready var tag_name_label = $HBoxContainer/TagName
@onready var import_toggle = $Panel/HBoxContainer/Import
@onready var loops_toggle = $Panel/HBoxContainer/Loops
@onready var separator = $Panel/HBoxContainer/Separator
@onready var visible_toggle = $Panel/HBoxContainer/Visible
@onready var clickable_toggle = $Panel/HBoxContainer/Clickable
#region Godot ######################################################################################
func _ready():
# Common toggle icons
import_toggle.icon = get_theme_icon('Load', 'EditorIcons')
loops_toggle.icon = get_theme_icon('Loop', 'EditorIcons')
# Room-related toggle icons
visible_toggle.icon = get_theme_icon('GuiVisibilityVisible', 'EditorIcons')
clickable_toggle.icon = get_theme_icon('ToolSelect', 'EditorIcons')
#endregion
#region Public #####################################################################################
func init(tag_cfg: Dictionary):
if tag_cfg.tag_name == null or tag_cfg.tag_name == "":
printerr(RESULT_CODE.get_error_message(RESULT_CODE.ERR_UNNAMED_TAG_DETECTED))
return false
_anim_tag_state = _load_default_tag_state()
_anim_tag_state.merge(tag_cfg, true)
_setup_scene()
func show_prop_buttons():
separator.visible = true
visible_toggle.visible = true
clickable_toggle.visible = true
#endregion
#region SetGet #####################################################################################
func get_cfg() -> Dictionary:
return _anim_tag_state
#endregion
#region Private ####################################################################################
func _setup_scene():
import_toggle.button_pressed = _anim_tag_state.import
loops_toggle.button_pressed = _anim_tag_state.loops
tag_name_label.text = _anim_tag_state.tag_name
visible_toggle.button_pressed = _anim_tag_state.prop_visible
clickable_toggle.button_pressed = _anim_tag_state.prop_clickable
emit_signal("tag_state_changed")
func _load_default_tag_state() -> Dictionary:
return {
"tag_name": "",
"import": PopochiuConfig.is_default_animation_import_enabled(),
"loops": PopochiuConfig.is_default_animation_loop_enabled(),
"prop_visible": PopochiuConfig.is_default_animation_prop_visible(),
"prop_clickable": PopochiuConfig.is_default_animation_prop_clickable(),
}
func _on_import_toggled(button_pressed):
_anim_tag_state.import = button_pressed
emit_signal("tag_state_changed")
func _on_loops_toggled(button_pressed):
_anim_tag_state.loops = button_pressed
emit_signal("tag_state_changed")
func _on_visible_toggled(button_pressed):
_anim_tag_state.prop_visible = button_pressed
emit_signal("tag_state_changed")
func _on_clickable_toggled(button_pressed):
_anim_tag_state.prop_clickable = button_pressed
emit_signal("tag_state_changed")
#endregion

View file

@ -0,0 +1 @@
uid://krf8u35pkjn3

View file

@ -0,0 +1,94 @@
[gd_scene load_steps=6 format=3 uid="uid://rphyltbm12m4"]
[ext_resource type="Script" path="res://addons/popochiu/editor/importers/aseprite/docks/animation_tag_row.gd" id="1"]
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_77wem"]
[sub_resource type="Image" id="Image_vdhps"]
data = {
"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
"format": "RGBA8",
"height": 16,
"mipmaps": false,
"width": 16
}
[sub_resource type="ImageTexture" id="ImageTexture_c80ss"]
image = SubResource("Image_vdhps")
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_sd1l8"]
[node name="AnimationTagRow" type="HBoxContainer"]
offset_right = 320.0
offset_bottom = 20.0
script = ExtResource("1")
[node name="HBoxContainer" type="HBoxContainer" parent="."]
layout_mode = 2
[node name="TagName" type="Label" parent="HBoxContainer"]
layout_mode = 2
text = "Tag Name"
[node name="Panel" type="Panel" parent="."]
layout_mode = 2
size_flags_horizontal = 3
theme_override_styles/panel = SubResource("StyleBoxEmpty_77wem")
[node name="HBoxContainer" type="HBoxContainer" parent="Panel"]
layout_mode = 1
anchors_preset = 1
anchor_left = 1.0
anchor_right = 1.0
offset_left = -60.0
offset_bottom = 20.0
grow_horizontal = 0
[node name="Visible" type="Button" parent="Panel/HBoxContainer"]
visible = false
layout_mode = 2
size_flags_horizontal = 4
size_flags_vertical = 3
tooltip_text = "This prop will be visible"
toggle_mode = true
icon = SubResource("ImageTexture_c80ss")
flat = true
[node name="Clickable" type="Button" parent="Panel/HBoxContainer"]
visible = false
layout_mode = 2
size_flags_horizontal = 4
size_flags_vertical = 3
tooltip_text = "This prop will be clickable"
toggle_mode = true
icon = SubResource("ImageTexture_c80ss")
flat = true
[node name="Separator" type="Panel" parent="Panel/HBoxContainer"]
visible = false
custom_minimum_size = Vector2(1, 0)
layout_mode = 2
theme_override_styles/panel = SubResource("StyleBoxFlat_sd1l8")
[node name="Import" type="Button" parent="Panel/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 4
size_flags_vertical = 3
tooltip_text = "Import this animation"
toggle_mode = true
icon = SubResource("ImageTexture_c80ss")
flat = true
[node name="Loops" type="Button" parent="Panel/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 4
size_flags_vertical = 3
tooltip_text = "Set animation as looping"
toggle_mode = true
icon = SubResource("ImageTexture_c80ss")
flat = true
[connection signal="toggled" from="Panel/HBoxContainer/Visible" to="." method="_on_visible_toggled"]
[connection signal="toggled" from="Panel/HBoxContainer/Clickable" to="." method="_on_clickable_toggled"]
[connection signal="toggled" from="Panel/HBoxContainer/Import" to="." method="_on_import_toggled"]
[connection signal="toggled" from="Panel/HBoxContainer/Loops" to="." method="_on_loops_toggled"]

View file

@ -0,0 +1,429 @@
@tool
extends PanelContainer
# TODO: review coding standards for those constants
const RESULT_CODE = preload("res://addons/popochiu/editor/config/result_codes.gd")
const LOCAL_OBJ_CONFIG = preload("res://addons/popochiu/editor/config/local_obj_config.gd")
# TODO: this can be specialized, even if for a two buttons... ?
const AnimationTagRow =\
preload("res://addons/popochiu/editor/importers/aseprite/docks/animation_tag_row.gd")
var scene: Node
var target_node: Node
var file_system: EditorFileSystem
# ---- External logic
var _animation_tag_row_scene: PackedScene =\
preload("res://addons/popochiu/editor/importers/aseprite/docks/animation_tag_row.tscn")
var _aseprite = preload("../aseprite_controller.gd").new() ## TODO: should be absolute?
# ---- References for children scripts
var _root_node: Node
var _options: Dictionary
# ---- Importer parameters variables
var _source: String = ""
var _tags_cache: Array = []
var _file_dialog_aseprite: FileDialog
var _output_folder_dialog: FileDialog
var _importing := false
var _output_folder := ""
var _out_folder_default := "[Same as scene]"
#region Godot ######################################################################################
func _ready():
_set_elements_styles()
if not PopochiuEditorConfig.aseprite_importer_enabled():
_show_info()
return
# Check access to Aseprite executable
var result = _check_aseprite()
if result == RESULT_CODE.SUCCESS:
_show_importer()
else:
PopochiuUtils.print_error(RESULT_CODE.get_error_message(result))
_show_warning()
# Load inspector dock configuration from node
var cfg = LOCAL_OBJ_CONFIG.load_config(target_node)
if cfg == null:
_load_default_config()
_set_options_visible(true)
else:
_load_config(cfg)
_set_tags_visible(cfg.get("tags_exp"))
_set_options_visible(cfg.get("op_exp"))
#endregion
#region Private ####################################################################################
func _check_aseprite() -> int:
if not _aseprite.check_command_path():
return RESULT_CODE.ERR_ASEPRITE_CMD_NOT_FULL_PATH
if not _aseprite.test_command():
return RESULT_CODE.ERR_ASEPRITE_CMD_NOT_FOUND
return RESULT_CODE.SUCCESS
func _list_tags(file: String):
if not _aseprite.check_command_path():
return RESULT_CODE.ERR_ASEPRITE_CMD_NOT_FULL_PATH
if not _aseprite.test_command():
return RESULT_CODE.ERR_ASEPRITE_CMD_NOT_FOUND
return _aseprite.list_tags(file)
## TODO: Currently unused. keeping this as reference
## to populate a checkable list of layers
func _list_layers(file: String, only_visibles = false):
if not _aseprite.check_command_path():
return RESULT_CODE.ERR_ASEPRITE_CMD_NOT_FULL_PATH
if not _aseprite.test_command():
return RESULT_CODE.ERR_ASEPRITE_CMD_NOT_FOUND
return _aseprite.list_layers(file, only_visibles)
func _load_config(cfg):
if cfg.has("source"):
_set_source(cfg.source)
_output_folder = cfg.get("o_folder", "")
get_node("%OutFolderButton").text = (
_output_folder if _output_folder != "" else _out_folder_default
)
get_node("%OutFileName").text = cfg.get("o_name", "")
get_node("%VisibleLayersCheckButton").set_pressed_no_signal(
cfg.get("only_visible_layers", false)
)
get_node("%WipeOldAnimationsCheckButton").set_pressed_no_signal(
cfg.get("wipe_old_anims", false)
)
_set_tags_visible(cfg.get("tags_exp", false))
_set_options_visible(cfg.get("op_exp", false))
_populate_tags(cfg.get("tags", []))
func _save_config():
_update_tags_cache()
var cfg := {
"source": _source,
"tags": _tags_cache,
"tags_exp": get_node("%Tags").visible,
"op_exp": get_node("%Options").visible,
"o_folder": _output_folder,
"o_name": get_node("%OutFileName").text,
"only_visible_layers": get_node("%VisibleLayersCheckButton").is_pressed(),
"wipe_old_anims": get_node("%WipeOldAnimationsCheckButton").is_pressed(),
}
LOCAL_OBJ_CONFIG.save_config(target_node, cfg)
func _load_default_config():
# Reset variables
_source = ""
_tags_cache = []
_output_folder = ""
# Empty tags list
_empty_tags_container()
# Reset inspector fields
get_node("%SourceButton").text = "[empty]"
get_node("%SourceButton").tooltip_text = ""
get_node("%OutFolderButton").text = "[empty]"
get_node("%OutFileName").clear()
get_node("%VisibleLayersCheckButton").set_pressed_no_signal(false)
get_node("%WipeOldAnimationsCheckButton").set_pressed_no_signal(
PopochiuConfig.is_default_wipe_old_anims_enabled()
)
func _set_source(source):
_source = source
get_node("%SourceButton").text = _source
get_node("%SourceButton").tooltip_text = _source
func _on_source_pressed():
_open_source_dialog()
func _on_aseprite_file_selected(path):
_set_source(ProjectSettings.localize_path(path))
_populate_tags(_get_tags_from_source())
_save_config()
_file_dialog_aseprite.queue_free()
func _on_rescan_pressed():
_populate_tags(\
_merge_with_cache(_get_tags_from_source())\
)
_save_config()
func _on_import_pressed():
if _importing:
return
_importing = true
_root_node = get_tree().get_edited_scene_root()
if _source == "":
_show_message("Aseprite file not selected")
_importing = false
return
_options = {
"source": ProjectSettings.globalize_path(_source),
"tags": _tags_cache,
"output_folder": (
_output_folder if _output_folder != "" else _root_node.scene_file_path.get_base_dir()
),
"output_filename": get_node("%OutFileName").text,
"only_visible_layers": get_node("%VisibleLayersCheckButton").is_pressed(),
"wipe_old_animations": get_node("%WipeOldAnimationsCheckButton").is_pressed(),
}
_save_config()
func _on_reset_pressed():
var _confirmation_dialog = _show_confirmation(\
"This will reset the importer preferences." + \
"This cannot be undone! Are you sure?", "Confirmation required!")
_confirmation_dialog.get_ok_button().connect("pressed", Callable(self, "_reset_prefs_metadata"))
func _reset_prefs_metadata():
if target_node.has_meta(LOCAL_OBJ_CONFIG.LOCAL_OBJ_CONFIG_META_NAME):
target_node.remove_meta(LOCAL_OBJ_CONFIG.LOCAL_OBJ_CONFIG_META_NAME)
_load_default_config()
notify_property_list_changed()
func _open_source_dialog():
_file_dialog_aseprite = _create_aseprite_file_selection()
get_parent().add_child(_file_dialog_aseprite)
if _source != "":
_file_dialog_aseprite.set_current_dir(
ProjectSettings.globalize_path(
_source.get_base_dir()
)
)
_file_dialog_aseprite.popup_centered_ratio()
func _create_aseprite_file_selection():
var file_dialog = FileDialog.new()
file_dialog.file_mode = FileDialog.FILE_MODE_OPEN_FILE
file_dialog.access = FileDialog.ACCESS_FILESYSTEM
file_dialog.title = "Select Aseprite animation file"
file_dialog.connect("file_selected", Callable(self, "_on_aseprite_file_selected"))
file_dialog.set_filters(PackedStringArray(["*.ase","*.aseprite"]))
return file_dialog
func _populate_tags(tags: Array):
## reset tags container
_empty_tags_container()
# Add each tag found
for t in tags:
if t.tag_name == "":
continue
var tag_row: AnimationTagRow = _animation_tag_row_scene.instantiate()
get_node("%Tags").add_child(tag_row)
tag_row.init(t)
tag_row.connect("tag_state_changed", Callable(self, "_save_config"))
_customize_tag_ui(tag_row)
# Invoke customization hook implementable in child classes
_update_tags_cache()
func _customize_tag_ui(tagrow: AnimationTagRow):
## This can be implemented by child classes if necessary
pass
func _empty_tags_container():
# Clean the inspector tags container empty
for tl in get_node("%Tags").get_children():
get_node("%Tags").remove_child(tl)
tl.queue_free()
func _update_tags_cache():
_tags_cache = _get_tags_from_ui()
func _merge_with_cache(tags: Array) -> Array:
var tags_cache_index = {}
var result = []
for t in _tags_cache:
tags_cache_index[t.tag_name] = t
for i in tags.size():
result.push_back(
tags_cache_index[tags[i].tag_name]
if tags_cache_index.has(tags[i].tag_name)
else tags[i]
)
return result
func _get_tags_from_ui() -> Array:
var tags_list = []
for tag_row in get_node("%Tags").get_children():
var tag_row_cfg = tag_row.get_cfg()
if tag_row_cfg.tag_name == "":
continue
tags_list.push_back(tag_row_cfg)
return tags_list
func _get_tags_from_source() -> Array:
var tags_found = _list_tags(ProjectSettings.globalize_path(_source))
if typeof(tags_found) == TYPE_INT:
PopochiuUtils.print_error(RESULT_CODE.get_error_message(tags_found))
return []
var tags_list = []
for t in tags_found:
if t == "":
continue
tags_list.push_back({
tag_name = t
})
return tags_list
func _show_message(
message: String, title: String = "", object: Object = null, method := ""
):
var warning_dialog = AcceptDialog.new()
if title != "":
warning_dialog.title = title
warning_dialog.dialog_text = message
warning_dialog.popup_window = true
var callback := Callable(warning_dialog, "queue_free")
if is_instance_valid(object) and not method.is_empty():
callback = func():
object.call(method)
warning_dialog.confirmed.connect(callback)
warning_dialog.close_requested.connect(callback)
PopochiuEditorHelper.show_dialog(warning_dialog)
func _show_confirmation(message: String, title: String = ""):
var _confirmation_dialog = ConfirmationDialog.new()
get_parent().add_child(_confirmation_dialog)
if title != "":
_confirmation_dialog.title = title
_confirmation_dialog.dialog_text = message
_confirmation_dialog.popup_centered()
_confirmation_dialog.connect("close_requested", Callable(_confirmation_dialog, "queue_free"))
return _confirmation_dialog
func _on_options_title_toggled(button_pressed):
_set_options_visible(button_pressed)
_save_config()
func _set_options_visible(is_visible):
get_node("%Options").visible = is_visible
get_node("%OptionsTitle").icon = (
PopochiuEditorConfig.get_icon(PopochiuEditorConfig.Icons.EXPANDED) if is_visible
else PopochiuEditorConfig.get_icon(PopochiuEditorConfig.Icons.COLLAPSED)
)
func _on_tags_title_toggled(button_pressed: bool) -> void:
_set_tags_visible(button_pressed)
_save_config()
func _set_tags_visible(is_visible: bool) -> void:
get_node("%Tags").visible = is_visible
get_node("%TagsTitle").icon = (
PopochiuEditorConfig.get_icon(PopochiuEditorConfig.Icons.EXPANDED) if is_visible
else PopochiuEditorConfig.get_icon(PopochiuEditorConfig.Icons.COLLAPSED)
)
func _on_out_folder_pressed():
_output_folder_dialog = _create_output_folder_selection()
get_parent().add_child(_output_folder_dialog)
if _output_folder != _out_folder_default:
_output_folder_dialog.current_dir = _output_folder
_output_folder_dialog.popup_centered_ratio()
func _create_output_folder_selection():
var file_dialog = FileDialog.new()
file_dialog.file_mode = FileDialog.FILE_MODE_OPEN_DIR
file_dialog.access = FileDialog.ACCESS_RESOURCES
file_dialog.title = "Select destination folder"
file_dialog.connect("dir_selected", Callable(self, "_on_output_folder_selected"))
return file_dialog
func _on_output_folder_selected(path):
_output_folder = path
get_node("%OutFolderButton").text = (
_output_folder if _output_folder != "" else _out_folder_default
)
_output_folder_dialog.queue_free()
_save_config()
func _set_elements_styles():
# Set sections title colors according to current theme
var section_color = get_theme_color("prop_section", "Editor")
var section_style = StyleBoxFlat.new()
section_style.set_bg_color(section_color)
get_node("%TagsTitleBar").set("theme_override_styles/panel", section_style)
get_node("%OptionsTitleBar").set("theme_override_styles/panel", section_style)
# Set style of warning panel
get_node("%WarningPanel").add_theme_stylebox_override(
"panel",
get_node("%WarningPanel").get_theme_stylebox("sub_inspector_bg11", "Editor")
)
get_node("%WarningLabel").add_theme_color_override("font_color", Color("c46c71"))
func _show_info():
get_node("%Info").visible = true
get_node("%Warning").visible = false
get_node("%Importer").visible = false
func _show_warning():
get_node("%Info").visible = false
get_node("%Warning").visible = true
get_node("%Importer").visible = false
func _show_importer():
get_node("%Info").visible = false
get_node("%Warning").visible = false
get_node("%Importer").visible = true
# TODO: Introduce layer selection list, more or less as tags
#endregion

View file

@ -0,0 +1 @@
uid://c5o55inhq2abl

View file

@ -0,0 +1,225 @@
[gd_scene load_steps=4 format=3 uid="uid://bcanby6n3eahm"]
[sub_resource type="StyleBoxEmpty" id="1"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_wwoxk"]
bg_color = Color(0, 0, 0, 1)
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_ctsm1"]
content_margin_left = 4.0
content_margin_top = 4.0
content_margin_right = 4.0
content_margin_bottom = 4.0
bg_color = Color(1, 0.364706, 0.364706, 1)
draw_center = false
corner_detail = 1
[node name="AsepriteImporterInspectorDock" type="PanelContainer"]
offset_right = 14.0
offset_bottom = 14.0
theme_override_styles/panel = SubResource("1")
[node name="Margin" type="MarginContainer" parent="."]
layout_mode = 2
theme_override_constants/margin_top = 2
theme_override_constants/margin_bottom = 2
[node name="Importer" type="VBoxContainer" parent="Margin"]
unique_name_in_owner = true
visible = false
layout_mode = 2
[node name="Source" type="HBoxContainer" parent="Margin/Importer"]
layout_mode = 2
tooltip_text = "Location of the Aseprite (*.ase, *.aseprite) source file."
[node name="Label" type="Label" parent="Margin/Importer/Source"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_stretch_ratio = 2.0
text = "Aseprite File"
[node name="SourceButton" type="Button" parent="Margin/Importer/Source"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
size_flags_stretch_ratio = 2.0
text = "[empty]"
clip_text = true
[node name="RescanButton" type="Button" parent="Margin/Importer/Source"]
unique_name_in_owner = true
layout_mode = 2
text = "Rescan"
[node name="TagsTitleBar" type="PanelContainer" parent="Margin/Importer"]
unique_name_in_owner = true
layout_mode = 2
theme_override_styles/panel = SubResource("StyleBoxFlat_wwoxk")
[node name="TagsTitle" type="Button" parent="Margin/Importer/TagsTitleBar"]
unique_name_in_owner = true
layout_mode = 2
theme_override_colors/font_pressed_color = Color(0.8, 0.807843, 0.827451, 1)
toggle_mode = true
text = "Animation tags"
[node name="Tags" type="VBoxContainer" parent="Margin/Importer"]
unique_name_in_owner = true
layout_mode = 2
[node name="OptionsTitleBar" type="PanelContainer" parent="Margin/Importer"]
unique_name_in_owner = true
layout_mode = 2
theme_override_styles/panel = SubResource("StyleBoxFlat_wwoxk")
[node name="OptionsTitle" type="Button" parent="Margin/Importer/OptionsTitleBar"]
unique_name_in_owner = true
layout_mode = 2
theme_override_colors/font_pressed_color = Color(0.8, 0.807843, 0.827451, 1)
toggle_mode = true
text = "Options"
[node name="Options" type="VBoxContainer" parent="Margin/Importer"]
unique_name_in_owner = true
layout_mode = 2
[node name="OutFolder" type="HBoxContainer" parent="Margin/Importer/Options"]
layout_mode = 2
tooltip_text = "Location where the spritesheet file should be saved."
[node name="Label" type="Label" parent="Margin/Importer/Options/OutFolder"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_stretch_ratio = 2.0
text = "Output folder"
[node name="OutFolderButton" type="Button" parent="Margin/Importer/Options/OutFolder"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
size_flags_stretch_ratio = 2.0
text = "[empty]"
clip_text = true
[node name="OutFile" type="HBoxContainer" parent="Margin/Importer/Options"]
layout_mode = 2
tooltip_text = "Base filename for spritesheet. In case the layer option is used, this works as a prefix to the layer name."
[node name="Label" type="Label" parent="Margin/Importer/Options/OutFile"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_stretch_ratio = 2.0
text = "Output file name"
[node name="OutFileName" type="LineEdit" parent="Margin/Importer/Options/OutFile"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
size_flags_stretch_ratio = 2.0
[node name="VisibleLayers" type="HBoxContainer" parent="Margin/Importer/Options"]
layout_mode = 2
tooltip_text = "If active, layers not visible in the source file won't be included in the final image."
[node name="Label" type="Label" parent="Margin/Importer/Options/VisibleLayers"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_stretch_ratio = 2.0
text = "Only visible layers"
[node name="VisibleLayersCheckButton" type="CheckButton" parent="Margin/Importer/Options/VisibleLayers"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
size_flags_stretch_ratio = 2.0
[node name="WipeOldAnimations" type="HBoxContainer" parent="Margin/Importer/Options"]
layout_mode = 2
tooltip_text = "If active, layers not visible in the source file won't be included in the final image."
[node name="Label" type="Label" parent="Margin/Importer/Options/WipeOldAnimations"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_stretch_ratio = 2.0
tooltip_text = "Set this to OFF if you want to add new animations on top of old ones. Anims with same name will be updated."
mouse_filter = 0
text = "Wipe old animations"
[node name="WipeOldAnimationsCheckButton" type="CheckButton" parent="Margin/Importer/Options/WipeOldAnimations"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
size_flags_stretch_ratio = 2.0
[node name="Import" type="Button" parent="Margin/Importer"]
layout_mode = 2
text = "Import"
[node name="Reset" type="Button" parent="Margin/Importer"]
layout_mode = 2
text = "Reset Preferences"
[node name="Warning" type="VBoxContainer" parent="Margin"]
unique_name_in_owner = true
visible = false
layout_mode = 2
[node name="HBoxContainer" type="HBoxContainer" parent="Margin/Warning"]
layout_mode = 2
[node name="WarningPanel" type="Panel" parent="Margin/Warning/HBoxContainer"]
unique_name_in_owner = true
custom_minimum_size = Vector2(222, 50)
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
theme_override_styles/panel = SubResource("StyleBoxFlat_ctsm1")
[node name="WarningLabel" type="Label" parent="Margin/Warning/HBoxContainer/WarningPanel"]
unique_name_in_owner = true
custom_minimum_size = Vector2(0, 42)
layout_mode = 0
anchor_right = 1.0
anchor_bottom = 1.0
size_flags_horizontal = 3
size_flags_vertical = 6
theme_override_colors/font_color = Color(0.768627, 0.423529, 0.443137, 1)
text = "Error loading Aseprite Importer!
Check Output panel for details."
horizontal_alignment = 1
[node name="Info" type="VBoxContainer" parent="Margin"]
unique_name_in_owner = true
layout_mode = 2
[node name="HBoxContainer" type="HBoxContainer" parent="Margin/Info"]
layout_mode = 2
[node name="InfoPanel" type="Panel" parent="Margin/Info/HBoxContainer"]
custom_minimum_size = Vector2(222, 50)
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
theme_override_styles/panel = SubResource("StyleBoxFlat_ctsm1")
[node name="InfoLabel" type="Label" parent="Margin/Info/HBoxContainer/InfoPanel"]
custom_minimum_size = Vector2(0, 42)
layout_mode = 0
anchor_right = 1.0
anchor_bottom = 1.0
size_flags_horizontal = 3
size_flags_vertical = 6
text = "Aseprite Importer disabled.
Can be enabled in Editor Settings."
[connection signal="pressed" from="Margin/Importer/Source/SourceButton" to="." method="_on_source_pressed"]
[connection signal="pressed" from="Margin/Importer/Source/RescanButton" to="." method="_on_rescan_pressed"]
[connection signal="toggled" from="Margin/Importer/TagsTitleBar/TagsTitle" to="." method="_on_tags_title_toggled"]
[connection signal="toggled" from="Margin/Importer/OptionsTitleBar/OptionsTitle" to="." method="_on_options_title_toggled"]
[connection signal="pressed" from="Margin/Importer/Options/OutFolder/OutFolderButton" to="." method="_on_out_folder_pressed"]
[connection signal="focus_exited" from="Margin/Importer/Options/OutFile/OutFileName" to="." method="_save_config"]
[connection signal="pressed" from="Margin/Importer/Options/VisibleLayers/VisibleLayersCheckButton" to="." method="_save_config"]
[connection signal="pressed" from="Margin/Importer/Options/WipeOldAnimations/WipeOldAnimationsCheckButton" to="." method="_save_config"]
[connection signal="pressed" from="Margin/Importer/Import" to="." method="_on_import_pressed"]
[connection signal="pressed" from="Margin/Importer/Reset" to="." method="_on_reset_pressed"]

View file

@ -0,0 +1,55 @@
@tool
extends "res://addons/popochiu/editor/importers/aseprite/docks/aseprite_importer_inspector_dock.gd"
var _animation_player_path: String
var _animation_creator = preload(
"res://addons/popochiu/editor/importers/aseprite/animation_creator.gd"
).new()
#region Godot ######################################################################################
func _ready():
if not target_node.has_node("AnimationPlayer"):
PopochiuUtils.print_error(
RESULT_CODE.get_error_message(RESULT_CODE.ERR_NO_ANIMATION_PLAYER_FOUND)
)
return
_animation_player_path = target_node.get_node("AnimationPlayer").get_path()
# Instantiate animation creator
_animation_creator.init(_aseprite, file_system)
super()
#endregion
#region Private ####################################################################################
func _on_import_pressed():
# Set everything up
# This will populate _root_node and _options class variables
super()
if _animation_player_path == "" or not _root_node.has_node(_animation_player_path):
_show_message("AnimationPlayer not found")
_importing = false
return
var result = await _animation_creator.create_character_animations(
target_node, _root_node.get_node(_animation_player_path), _options
)
_importing = false
if typeof(result) == TYPE_INT and result != RESULT_CODE.SUCCESS:
PopochiuUtils.print_error(RESULT_CODE.get_error_message(result))
_show_message("Some errors occurred. Please check output panel.", "Warning!")
else:
_show_message("%d animation tags processed." % [_tags_cache.size()], "Done!")
func _customize_tag_ui(tag_row: AnimationTagRow):
# Nothing special has to be done for Character tags
pass
#endregion

View file

@ -0,0 +1,117 @@
@tool
extends "res://addons/popochiu/editor/importers/aseprite/docks/aseprite_importer_inspector_dock.gd"
var _animation_creator = preload(\
"res://addons/popochiu/editor/importers/aseprite/animation_creator.gd").new()
#region Godot ######################################################################################
func _ready():
# Instantiate animation creator
_animation_creator.init(_aseprite, file_system)
super()
#endregion
#region Private ####################################################################################
func _on_import_pressed():
# Set everything up
# This will populate _root_node and _options class variables
super()
var props_container = _root_node.get_node("Props")
var result: int = RESULT_CODE.SUCCESS
# Create a prop for each tag that must be imported
# and populate it with the right sprite
for tag in _options.get("tags"):
# Ignore unwanted tags
if not tag.import: continue
# Always convert to PascalCase as a standard
# TODO: check Godot 4 standards, I can't find info
var prop_name: String = tag.tag_name.to_pascal_case()
# In case the prop is there, use the one we already have
var prop = props_container.get_node_or_null(prop_name)
if prop == null:
# Create a new prop if necessary, specifying the
# interaction flags.
prop = _create_prop(prop_name, tag.prop_clickable, tag.prop_visible)
else:
# Force flags (a bit redundant but they may have been changed
# in the Importer interface, for already imported props)
prop.clickable = tag.prop_clickable
prop.visible = tag.prop_visible
prop.set_meta("ANIM_NAME", tag.tag_name)
for prop in props_container.get_children():
if not prop.has_meta("ANIM_NAME"): continue
# TODO: check if animation player exists in prop, if not add it
# same for Sprite2D even if it should be there...
# Make the output folder match the prop's folder
_options.output_folder = prop.scene_file_path.get_base_dir()
# Import a single tag animation
result = await _animation_creator.create_prop_animations(
prop,
prop.get_meta("ANIM_NAME"),
_options
)
for prop in props_container.get_children():
if not prop.has_meta("ANIM_NAME"): continue
# Save the prop
result = await _save_prop(prop)
# TODO: maybe check if this is better done with signals
_importing = false
if typeof(result) == TYPE_INT and result != RESULT_CODE.SUCCESS:
PopochiuUtils.print_error(RESULT_CODE.get_error_message(result))
_show_message("Some errors occurred. Please check output panel.", "Warning!")
else:
await get_tree().create_timer(0.1).timeout
# Once the popup is closed, call _clean_props()
_show_message(
"%d animation tags processed." % [_tags_cache.size()],
"Done!"
)
func _customize_tag_ui(tag_row: AnimationTagRow):
# Show props-related buttons if we are in a room
tag_row.show_prop_buttons()
func _create_prop(name: String, is_clickable: bool = true, is_visible: bool = true):
var factory = PopochiuPropFactory.new()
var param := PopochiuPropFactory.PopochiuPropFactoryParam.new()
param.obj_name = name
param.room = _root_node
param.is_interactive = is_clickable
param.is_visible = is_visible
if factory.create(param) != ResultCodes.SUCCESS:
return
return factory.get_obj_scene()
func _save_prop(prop: PopochiuProp):
var packed_scene: PackedScene = PackedScene.new()
packed_scene.pack(prop)
if ResourceSaver.save(packed_scene, prop.scene_file_path) != OK:
PopochiuUtils.print_error(
"Couldn't save animations for prop %s at %s" %
[prop.name, prop.scene_file_path]
)
return ResultCodes.ERR_CANT_SAVE_OBJ_SCENE
return ResultCodes.SUCCESS
#endregion

View file

@ -0,0 +1,46 @@
extends EditorInspectorPlugin ## TODO: create a base class with pointer variables
const DOCKS_PATH := "res://addons/popochiu/editor/importers/aseprite/docks/"
const INSPECTOR_DOCK = preload(DOCKS_PATH + "aseprite_importer_inspector_dock.tscn")
const CONFIG_SCRIPT = preload("res://addons/popochiu/editor/config/config.gd")
const INSPECTOR_DOCK_CHARACTER := DOCKS_PATH + "aseprite_importer_inspector_dock_character.gd"
const INSPECTOR_DOCK_ROOM := DOCKS_PATH + "aseprite_importer_inspector_dock_room.gd"
var _target_node: Node
#region Godot ######################################################################################
func _can_handle(object):
if object.has_method("get_parent") and object.get_parent() is Node2D:
return false
return object is PopochiuCharacter || object is PopochiuRoom #|| object is PopochiuInventoryItem
func _parse_begin(object: Object):
# Fix showing error messages in Output when inspecting nodes in the Debugger
if not object is Node: return
_target_node = object
func _parse_property(object, type, name, hint_type, hint_string, usage_flags, wide) -> bool:
if object.get_class() == "EditorDebuggerRemoteObject":
return false
if name != 'popochiu_placeholder':
return false
# Instantiate and configure the dock
var dock = INSPECTOR_DOCK.instantiate()
# Load the specific script in the dock
if object is PopochiuCharacter:
dock.set_script(load(INSPECTOR_DOCK_CHARACTER))
if object is PopochiuRoom:
dock.set_script(load(INSPECTOR_DOCK_ROOM))
dock.target_node = object
dock.file_system = EditorInterface.get_resource_filesystem()
# Add the dock to the inspector
add_custom_control(dock)
return true
#endregion

View file

@ -0,0 +1 @@
uid://dpianc4xo6kcl

View file

@ -0,0 +1,52 @@
extends EditorInspectorPlugin
#region Godot ######################################################################################
func _can_handle(object: Object) -> bool:
return object is PopochiuAudioCue
func _parse_property(
object: Object,
type,
path: String,
hint,
hint_text: String,
usage,
wide: bool
) -> bool:
if not object is PopochiuAudioCue or path != "bus":
return false
var ep := EditorProperty.new()
var ob := OptionButton.new()
_update_buses_list(ob, object)
ob.item_selected.connect(_update_audio_cue_bus.bind(object))
ob.pressed.connect(_update_buses_list.bind(ob, object))
ep.add_child(ob)
add_property_editor(path, ep)
return true
#endregion
#region Private ####################################################################################
func _update_audio_cue_bus(idx: int, audio_cue: PopochiuAudioCue) -> void:
audio_cue.bus = AudioServer.get_bus_name(idx)
ResourceSaver.save(audio_cue, audio_cue.resource_path)
func _update_buses_list(ob: OptionButton, pac: PopochiuAudioCue) -> void:
ob.clear()
for idx in AudioServer.bus_count:
ob.add_item(AudioServer.get_bus_name(idx), idx)
ob.selected = AudioServer.get_bus_index(pac.bus)
#endregion

View file

@ -0,0 +1 @@
uid://d0emjgqvommw4

View file

@ -0,0 +1,83 @@
extends EditorInspectorPlugin
#region Virtual ####################################################################################
func _can_handle(object: Object) -> bool:
if object is PopochiuCharacter:
return true
return false
func _parse_begin(object: Object) -> void:
if object.get_class() == "EditorDebuggerRemoteObject":
return
if not object.get_parent() is Node2D: return
var panel := PanelContainer.new()
var hbox := HBoxContainer.new()
var button := Button.new()
hbox.custom_minimum_size.y = 42.0
button.text = "* Open Node' scene to edit its properties"
button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
button.alignment = HORIZONTAL_ALIGNMENT_CENTER
button.mouse_default_cursor_shape = Control.CURSOR_POINTING_HAND
panel.add_theme_stylebox_override(
"panel",
panel.get_theme_stylebox("sub_inspector_bg11", "Editor")
)
button.add_theme_color_override("font_color", Color("c46c71"))
button.add_theme_color_override("font_color_hover", Color("c46c71"))
button.add_theme_color_override("font_color_pressed", Color("c46c71"))
button.pressed.connect(
_open_scene.bind((object as PopochiuCharacter).scene_file_path),
CONNECT_DEFERRED
)
hbox.add_child(button)
panel.add_child(hbox)
add_custom_control(panel)
func _parse_property(
object: Object,
type,
path: String,
hint,
hint_text: String,
usage,
wide: bool
) -> bool:
if object.get_class() == "EditorDebuggerRemoteObject":
return false
# NOTE: We could add this as an option of the plugin settings. So devs can add extra properties
# if needed.
if object and object.get_parent() is Node2D and not path in [
"baseline",
"walk_to_point",
"look_at_point",
"position",
"visible",
"modulate",
"self_modulate",
"light_mask",
]:
return true
return false
#endregion
#region Private ####################################################################################
func _open_scene(path: String) -> void:
EditorInterface.set_main_screen_editor("2D")
EditorInterface.open_scene_from_path(path)
#endregion

View file

@ -0,0 +1 @@
uid://m7hkqdx4jhem

View file

@ -0,0 +1,63 @@
extends EditorInspectorPlugin
#region Virtual ####################################################################################
func _can_handle(object: Object) -> bool:
if object is PopochiuProp:
return true
return false
func _parse_property(
object: Object,
type,
path: String,
hint,
hint_text: String,
usage,
wide: bool
) -> bool:
if (
object.get_class() == "EditorDebuggerRemoteObject"
or object is not PopochiuProp
or path != "link_to_item"
):
return false
var ep := EditorProperty.new()
var ob := OptionButton.new()
_update_items_list(ob, object)
ob.item_selected.connect(_update_link_to_item.bind(ob, object))
ob.pressed.connect(_update_items_list.bind(ob, object))
ep.add_child(ob)
add_property_editor(path, ep)
return true
#endregion
#region Private ####################################################################################
func _update_items_list(ob: OptionButton, prop: PopochiuProp) -> void:
ob.clear()
var inventory_items := PopochiuResources.get_section_keys("inventory_items")
var keys_ids_map := {}
inventory_items.sort()
ob.add_item("")
for key: String in inventory_items:
keys_ids_map[key] = ob.item_count
ob.add_item(key)
if keys_ids_map.has(prop.link_to_item):
ob.selected = ob.get_item_index(keys_ids_map[prop.link_to_item])
func _update_link_to_item(idx: int, ob: OptionButton, prop: PopochiuProp) -> void:
prop.link_to_item = ob.get_item_text(idx)
#endregion

View file

@ -0,0 +1 @@
uid://dsuhln67g2d8k

View file

@ -0,0 +1,117 @@
@tool
extends Panel
signal move_folders_pressed
@onready var tab_container: TabContainer = %TabContainer
@onready var tab_main: VBoxContainer = %Main
@onready var tab_room: VBoxContainer = %Room
@onready var tab_audio: VBoxContainer = %Audio
@onready var tab_gui: VBoxContainer = %GUI
# ---- FOOTER --------------------------------------------------------------------------------------
@onready var version: Label = %Version
@onready var btn_setup: Button = %BtnSetup
@onready var btn_docs: Button = %BtnDocs
#region Godot ######################################################################################
func _ready() -> void:
version.text = "v" + PopochiuResources.get_version()
btn_setup.icon = get_theme_icon("Edit", "EditorIcons")
btn_docs.icon = get_theme_icon("HelpSearch", "EditorIcons")
# Set the Main tab selected by default
tab_container.current_tab = 0
# Hide the GUI tab while we decide how it will work based on devs feedback
tab_container.set_tab_hidden(tab_gui.get_index(), true)
# Connect to children's signals
tab_container.tab_changed.connect(_on_tab_changed)
btn_setup.pressed.connect(open_setup)
btn_docs.pressed.connect(OS.shell_open.bind(PopochiuResources.DOCUMENTATION))
# Connect to parent signals
get_tree().node_added.connect(_check_node)
#endregion
#region Public #####################################################################################
func fill_data() -> void:
tab_main.fill_data()
tab_audio.fill_data()
func scene_changed(scene_root: Node) -> void:
if not is_instance_valid(tab_room): return
tab_room.scene_changed(scene_root)
# TODO: Uncomment these lines when working on the GUI tab again
#if not is_instance_valid(tab_gui): return
#tab_gui.on_scene_changed(scene_root)
if (
not scene_root
or (
not scene_root is PopochiuRoom
# TODO: Uncomment this line when working on the GUI tab again
#and not scene_root.scene_file_path == PopochiuResources.GUI_GAME_SCENE
)
):
# Open the Popochiu Main tab if the opened scene in the Editor2D is not a PopochiuRoom nor
# the GUI scene
tab_container.current_tab = 0
func scene_closed(filepath: String) -> void:
if not is_instance_valid(tab_room): return
tab_room.scene_closed(filepath)
check_open_scenes()
func search_audio_files() -> void:
if not is_instance_valid(tab_audio): return
tab_audio.search_audio_files()
func open_setup() -> void:
PopochiuEditorHelper.show_setup()
## If there are no other opened scenes in the Editor, this function connects to
## [signal EditorSelection.selection_changed] in order to make sure the Popochiu dock behaves as
## expected when the [signal EditorPlugin.scene_changed] signal is not emitted.
func check_open_scenes() -> void:
# Fixes #273: Since Godot is not triggering the EditorPlugin.scene_changed signal when opening a
# scene when no other scenes are opened, listen to the EditorSelection.selection_changed signal
await get_tree().process_frame
if EditorInterface.get_open_scenes().is_empty():
EditorInterface.get_selection().selection_changed.connect(_on_editor_selection_changed)
#endregion
#region Private ####################################################################################
func _on_tab_changed(tab: int) -> void:
if tab == tab_main.get_index():
tab_main.check_data()
if tab == tab_gui.get_index():
tab_gui.open_gui_scene()
func _check_node(node: Node) -> void:
if node is PopochiuCharacter and node.get_parent() is Node2D:
# The node is a PopochiuCharacter in a room
node.set_name.call_deferred("Character%s *" % node.script_name)
func _on_editor_selection_changed() -> void:
if EditorInterface.get_edited_scene_root():
EditorInterface.get_selection().selection_changed.disconnect(_on_editor_selection_changed)
scene_changed(EditorInterface.get_edited_scene_root())
#endregion

View file

@ -0,0 +1 @@
uid://bcwtfohnkna8t

View file

@ -0,0 +1,100 @@
[gd_scene load_steps=8 format=3 uid="uid://bardo4kb80rvg"]
[ext_resource type="PackedScene" uid="uid://bynwdds8o3tcx" path="res://addons/popochiu/editor/main_dock/tab_main/tab_main.tscn" id="2_oxyje"]
[ext_resource type="Script" path="res://addons/popochiu/editor/main_dock/popochiu_dock.gd" id="7"]
[ext_resource type="PackedScene" uid="uid://4etgd0rwjgct" path="res://addons/popochiu/editor/main_dock/tab_gui/tab_gui.tscn" id="10_82goo"]
[ext_resource type="PackedScene" uid="uid://q1bjkxavt2ay" path="res://addons/popochiu/editor/main_dock/tab_room/tab_room.tscn" id="12"]
[ext_resource type="PackedScene" uid="uid://bpj8jlet25coy" path="res://addons/popochiu/editor/main_dock/tab_audio/tab_audio.tscn" id="13"]
[sub_resource type="Image" id="Image_sgf6r"]
data = {
"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
"format": "RGBA8",
"height": 16,
"mipmaps": false,
"width": 16
}
[sub_resource type="ImageTexture" id="ImageTexture_32xut"]
image = SubResource("Image_sgf6r")
[node name="Popochiu" type="Panel"]
clip_contents = true
custom_minimum_size = Vector2(340, 0)
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
script = ExtResource("7")
[node name="MarginContainer" type="MarginContainer" parent="."]
layout_mode = 0
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="VBoxContainer" type="VBoxContainer" parent="MarginContainer"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="TabContainer" type="TabContainer" parent="MarginContainer/VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
current_tab = 0
[node name="Main" parent="MarginContainer/VBoxContainer/TabContainer" instance=ExtResource("2_oxyje")]
unique_name_in_owner = true
layout_mode = 2
[node name="Room" parent="MarginContainer/VBoxContainer/TabContainer" instance=ExtResource("12")]
unique_name_in_owner = true
visible = false
layout_mode = 2
focus_mode = 2
metadata/_tab_index = 1
[node name="Audio" parent="MarginContainer/VBoxContainer/TabContainer" instance=ExtResource("13")]
unique_name_in_owner = true
visible = false
layout_mode = 2
focus_mode = 2
metadata/_tab_index = 2
[node name="GUI" parent="MarginContainer/VBoxContainer/TabContainer" instance=ExtResource("10_82goo")]
unique_name_in_owner = true
visible = false
layout_mode = 2
metadata/_tab_index = 3
[node name="FooterPanel" type="PanelContainer" parent="MarginContainer/VBoxContainer"]
layout_mode = 2
[node name="HBoxContainer" type="HBoxContainer" parent="MarginContainer/VBoxContainer/FooterPanel"]
layout_mode = 2
size_flags_vertical = 3
alignment = 2
[node name="Version" type="Label" parent="MarginContainer/VBoxContainer/FooterPanel/HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
text = "v2.0.1"
[node name="BtnSetup" type="Button" parent="MarginContainer/VBoxContainer/FooterPanel/HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
tooltip_text = "Opens wiki in web browser"
text = "Setup"
icon = SubResource("ImageTexture_32xut")
[node name="BtnDocs" type="Button" parent="MarginContainer/VBoxContainer/FooterPanel/HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
tooltip_text = "Opens wiki in web browser"
text = "Documentation"
icon = SubResource("ImageTexture_32xut")

View file

@ -0,0 +1,51 @@
@tool
extends LineEdit
var groups := {}: set = set_groups
# ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ GODOT ░░░░
func _ready() -> void:
right_icon = get_theme_icon('Search', 'EditorIcons')
# ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ SET & GET ░░░░
func set_groups(value: Dictionary) -> void:
groups = value
if groups:
text_changed.connect(
_filter_rows.bind(groups),
CONNECT_REFERENCE_COUNTED
)
# ░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░ PRIVATE ░░░░
# `source` is one of the `_types` dictionaries in PopochiuDock, TabRoom and
# TabAudio
func _filter_rows(new_text: String, source: Dictionary) -> void:
for type_dic in source.values():
type_dic.group.show()
var title_in_filter := false
if type_dic.group.title.findn(new_text) > -1:
title_in_filter = true
var hidden_rows := 0
# type_dic.group is a PopochiuGroup
var rows: Array = type_dic.group.get_elements()
for row in rows:
row.show()
if new_text.is_empty(): continue
if (row as Control).name.findn(new_text) < 0\
and not title_in_filter:
hidden_rows += 1
row.hide()
if hidden_rows == rows.size() and not new_text.is_empty():
type_dic.group.hide()

View file

@ -0,0 +1 @@
uid://l0id2f7cqbkj

View file

@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 16 16" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;">
<g id="PopochiuGroup" transform="matrix(0.5,0,0,0.5,0,0)">
<rect x="0" y="0" width="32" height="32" style="fill:none;"/>
<g transform="matrix(3.25,0,0,3.25,-32.75,-39.25)">
<path d="M19,13.615C19,13.276 18.724,13 18.385,13L11.615,13C11.276,13 11,13.276 11,13.615L11,20.385C11,20.724 11.276,21 11.615,21L18.385,21C18.724,21 19,20.724 19,20.385L19,13.615Z" style="fill:none;stroke:rgb(112,109,235);stroke-width:1.23px;"/>
</g>
<g transform="matrix(0.673999,0,0,0.570028,13,-2.18068)">
<path d="M0,24L8.902,31.902L-0,39.789" style="fill:none;stroke:rgb(112,109,235);stroke-width:6.41px;"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View file

@ -0,0 +1,37 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://uwjkfsrpfx3e"
path="res://.godot/imported/popochiu_group.svg-55d153202c16f767328f8314e84a5c37.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/popochiu/editor/main_dock/popochiu_group/images/popochiu_group.svg"
dest_files=["res://.godot/imported/popochiu_group.svg-55d153202c16f767328f8314e84a5c37.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

View file

@ -0,0 +1,199 @@
@tool
@icon("res://addons/popochiu/editor/main_dock/popochiu_group/images/popochiu_group.svg")
class_name PopochiuGroup
extends PanelContainer
signal create_clicked
const PopochiuRow := preload("res://addons/popochiu/editor/main_dock/popochiu_row/popochiu_row.gd")
@export var icon: Texture2D : set = set_icon
@export var is_open := true : set = set_is_open
@export var color: Color = Color("999999") : set = set_color
@export var title := "Group" : set = set_title
@export var can_create := true
@export var create_text := ""
@export var target_list: NodePath = ""
@export var custom_title_count := false
var _external_list: VBoxContainer = null
@onready var header: PanelContainer = %Header
@onready var arrow: TextureRect = %Arrow
@onready var trt_icon: TextureRect = %Icon
@onready var lbl_title: Label = %Title
@onready var body: Container = %Body
@onready var btn_create: Button = %BtnCreate
@onready var list: VBoxContainer = %List
#region Godot ######################################################################################
func _ready() -> void:
# Establecer estado inicial
add_theme_stylebox_override("panel", get_theme_stylebox("panel").duplicate())
(get_theme_stylebox("panel") as StyleBoxFlat).border_color = color
if is_instance_valid(icon):
trt_icon.texture = icon
lbl_title.text = title
btn_create.icon = get_theme_icon("Add", "EditorIcons")
btn_create.text = create_text
self.is_open = list.get_child_count() > 0
if not can_create:
btn_create.hide()
header.gui_input.connect(_on_input)
list.resized.connect(_update_child_count)
btn_create.pressed.connect(emit_signal.bind("create_clicked"))
if target_list:
_external_list = get_node(target_list) as VBoxContainer
self.is_open = _external_list.get_child_count() > 0
#endregion
#region Public #####################################################################################
func clear_list() -> void:
for c in list.get_children():
# Fix #216: Delete the row immediately so that it does not interfere with the creation of
# other rows that may have the same name as it
c.free()
func add(node: Node, sort := false) -> void:
if sort:
node.ready.connect(_order_list.bind(node))
list.add_child(node)
btn_create.disabled = false
if not is_open:
self.is_open = true
func clear_favs() -> void:
for popochiu_row: PopochiuRow in list.get_children():
popochiu_row.clear_tag()
func disable_create() -> void:
btn_create.disabled = true
func enable_create() -> void:
btn_create.disabled = false
func get_elements() -> Array:
return list.get_children()
func remove_by_name(node_name: String) -> void:
if list.has_node(node_name):
var node: HBoxContainer = list.get_node(node_name)
list.remove_child(node)
node.free()
func add_header_button(btn: Button) -> void:
btn_create.add_sibling(btn)
func set_title_count(count: int, max_count := 0) -> void:
if max_count > 0:
lbl_title.text = "%s (%d/%d)" % [title, count, max_count]
else:
lbl_title.text = "%s (%d)" % [title, count]
func get_by_name(node_name: String) -> HBoxContainer:
if list.has_node(node_name):
return list.get_node(node_name)
return null
#endregion
#region SetGet #####################################################################################
func set_icon(value: Texture2D) -> void:
icon = value
if is_instance_valid(trt_icon):
trt_icon.texture = value
func set_is_open(value: bool) -> void:
is_open = value
_toggled(value)
func set_color(value: Color) -> void:
color = value
if is_instance_valid(header):
(get_theme_stylebox("panel") as StyleBoxFlat).border_color = value
func set_title(value: String) -> void:
title = value
if is_instance_valid(lbl_title):
lbl_title.text = value
#endregion
#region Private ####################################################################################
func _on_input(event: InputEvent) -> void:
var mouse_event: = event as InputEventMouseButton
if mouse_event and mouse_event.button_index == MOUSE_BUTTON_LEFT \
and mouse_event.pressed:
is_open = !is_open
_toggled(is_open)
func _toggled(button_pressed: bool) -> void:
if is_instance_valid(arrow):
arrow.texture = (
get_theme_icon("GuiTreeArrowDown", "EditorIcons") if button_pressed
else get_theme_icon("GuiTreeArrowRight", "EditorIcons")
)
if is_instance_valid(body):
if button_pressed: body.show()
else: body.hide()
if is_instance_valid(_external_list):
_external_list.visible = button_pressed
func _update_child_count() -> void:
if custom_title_count: return
if is_instance_valid(lbl_title):
var children := list.get_child_count()
lbl_title.text = title + (" (%d)" % children) if children > 1 else title
func _order_list(node: Node) -> void:
node.ready.disconnect(_order_list)
# Place the new row in its place alphabetically
var place_before: Node = null
for row in list.get_children():
if str(node.name) < str(row.name):
place_before = row
break
if not place_before: return
list.move_child(node, place_before.get_index())
#endregion

View file

@ -0,0 +1 @@
uid://dfejejptym3da

View file

@ -0,0 +1,91 @@
[gd_scene load_steps=6 format=3 uid="uid://b55ialbvpilxv"]
[ext_resource type="Script" path="res://addons/popochiu/editor/main_dock/popochiu_group/popochiu_group.gd" id="1_lumyt"]
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_qhsn2"]
content_margin_left = 8.0
content_margin_right = 8.0
draw_center = false
border_width_left = 2
border_width_top = 2
border_width_right = 2
border_width_bottom = 2
border_color = Color(0.6, 0.6, 0.6, 1)
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_t8mu1"]
content_margin_left = 0.0
content_margin_top = 0.0
content_margin_right = 0.0
content_margin_bottom = 0.0
bg_color = Color(0.6, 0.6, 0.6, 0.211765)
draw_center = false
corner_detail = 5
expand_margin_left = 4.0
expand_margin_right = 4.0
[sub_resource type="Image" id="Image_e0ep0"]
data = {
"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
"format": "RGBA8",
"height": 16,
"mipmaps": false,
"width": 16
}
[sub_resource type="ImageTexture" id="ImageTexture_uhpk4"]
image = SubResource("Image_e0ep0")
[node name="PopochiuGroup" type="PanelContainer"]
offset_right = 320.0
offset_bottom = 24.0
theme_override_styles/panel = SubResource("StyleBoxFlat_qhsn2")
script = ExtResource("1_lumyt")
is_open = false
[node name="VBoxContainer" type="VBoxContainer" parent="."]
layout_mode = 2
[node name="Header" type="PanelContainer" parent="VBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
theme_override_styles/panel = SubResource("StyleBoxFlat_t8mu1")
[node name="HBoxContainer" type="HBoxContainer" parent="VBoxContainer/Header"]
layout_mode = 2
[node name="Arrow" type="TextureRect" parent="VBoxContainer/Header/HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 4
size_flags_vertical = 4
texture = SubResource("ImageTexture_uhpk4")
stretch_mode = 4
[node name="Icon" type="TextureRect" parent="VBoxContainer/Header/HBoxContainer"]
unique_name_in_owner = true
texture_filter = 1
layout_mode = 2
size_flags_horizontal = 4
size_flags_vertical = 4
[node name="Title" type="Label" parent="VBoxContainer/Header/HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
text = "Group"
[node name="Body" type="VBoxContainer" parent="VBoxContainer"]
unique_name_in_owner = true
visible = false
layout_mode = 2
[node name="BtnCreate" type="Button" parent="VBoxContainer/Body"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 8
icon = SubResource("ImageTexture_uhpk4")
[node name="List" type="VBoxContainer" parent="VBoxContainer/Body"]
unique_name_in_owner = true
layout_mode = 2

View file

@ -0,0 +1,237 @@
@tool
extends "res://addons/popochiu/editor/main_dock/popochiu_row/popochiu_row.gd"
signal group_selected(type: int)
signal deleted(file_path: String)
enum AudioOptions {
DELETE = MenuOptions.DELETE,
ADD_TO_MUSIC,
ADD_TO_SFX,
ADD_TO_VOICE,
ADD_TO_UI
}
const DELETE_AUDIO_CUE_MSG = "This will remove the [b]%s[/b] resource. Calls to this audio in \
scripts will not work anymore. This action cannot be reversed. Continue?"
const DELETE_AUDIO_CUE_ASK = "Delete [b]%s[/b] file too? (cannot be reversed)"
const DELETE_AUDIO_FILE_MSG = "[b]%s[/b] will be deleted in the file system. This action cannot be \
reversed. Continue?"
# Only used by rows that represent an audio file
var file_name: String
var audio_cue: AudioCue
var cue_group: String
var stream_player: AudioStreamPlayer
var audio_tab: VBoxContainer = null
var is_playing := false :
set = set_is_playing
var current_playback_position := 0.0
@onready var play_btn: Button = %Play
@onready var stop_btn: Button = %Stop
#region Godot ######################################################################################
func _ready() -> void:
super()
# Assign icons
play_btn.icon = get_theme_icon("MainPlay", "EditorIcons")
stop_btn.icon = get_theme_icon("Stop", "EditorIcons")
# Connect to children's signals
play_btn.pressed.connect(play)
stop_btn.pressed.connect(stop)
# Remove group options if this is a PopochiuAudioCue
if is_instance_valid(audio_cue):
menu_popup.remove_item(menu_popup.get_item_index(AudioOptions.ADD_TO_MUSIC))
menu_popup.remove_item(menu_popup.get_item_index(AudioOptions.ADD_TO_SFX))
menu_popup.remove_item(menu_popup.get_item_index(AudioOptions.ADD_TO_VOICE))
menu_popup.remove_item(menu_popup.get_item_index(AudioOptions.ADD_TO_UI))
else:
label.text = file_name
#endregion
#region Virtual ####################################################################################
func _remove_object() -> void:
_delete_dialog = PopochiuEditorHelper.DELETE_CONFIRMATION_SCENE.instantiate()
if is_instance_valid(audio_cue):
_delete_dialog.title = "Remove %s cue" % audio_cue.resource_name
_delete_dialog.message = DELETE_AUDIO_CUE_MSG % audio_cue.resource_name
_delete_dialog.ask = DELETE_AUDIO_CUE_ASK % audio_cue.audio.resource_path
_delete_dialog.on_confirmed = _remove_from_popochiu
else:
_delete_dialog.title = "Delete %s" % file_name
_delete_dialog.message = DELETE_AUDIO_FILE_MSG % path
_delete_dialog.on_confirmed = _delete_from_file_system
PopochiuEditorHelper.show_delete_confirmation(_delete_dialog)
#endregion
#region Public #####################################################################################
func select() -> void:
EditorInterface.edit_resource(audio_cue)
super()
func play() -> void:
if is_playing:
# Pause the audio stream
is_playing = false
return
if is_instance_valid(audio_tab.last_played):
# Stop the currently playing row (which is different from this one)
audio_tab.last_played.stop()
if not is_instance_valid(audio_cue):
# If the row does not have a [PopochiuAudioCue] assigned, then it is the row of an audio
# file. Therefore, the [AudioStream] to play will be its own [path]
var stream: AudioStream = load(path)
stream.loop = false
stream_player.stream = stream
else:
# Otherwise, the [AudioStream] to play will be that of the audio file associated with this
# [PopochiuAudioCue.audio]
stream_player.stream = audio_cue.audio
# The values of [AudioStream.pitch_scale] and [AudioStream.volume_db] should be taken from
# the information stored in the [PopochiuAudioCue].
stream_player.pitch_scale = audio_cue.get_pitch_scale()
stream_player.volume_db = audio_cue.volume
is_playing = true
func stop() -> void:
is_playing = false
current_playback_position = 0.0
label.add_theme_color_override("font_color", dflt_font_color)
stream_player.stream = null
audio_tab.last_played = null
#endregion
#region SetGet #####################################################################################
func set_is_playing(value: bool) -> void:
is_playing = value
if is_playing:
if not stream_player.finished.is_connected(stop):
stream_player.finished.connect(stop)
stream_player.play(current_playback_position)
audio_tab.last_played = self
else:
current_playback_position = stream_player.get_playback_position()
if stream_player.playing:
stream_player.stop()
stream_player.finished.disconnect(stop)
play_btn.icon = play_btn.get_theme_icon("Pause" if is_playing else "MainPlay", "EditorIcons")
#endregion
#region Private ####################################################################################
func _get_menu_cfg() -> Array:
return [
{
id = AudioOptions.ADD_TO_MUSIC,
icon = preload("res://addons/popochiu/icons/music.png"),
label = "Add to Music"
},
{
id = AudioOptions.ADD_TO_SFX,
icon = preload("res://addons/popochiu/icons/sfx.png"),
label = "Add to Sound Effects"
},
{
id = AudioOptions.ADD_TO_VOICE,
icon = preload("res://addons/popochiu/icons/voice.png"),
label = "Add to Voices"
},
{
id = AudioOptions.ADD_TO_UI,
icon = preload("res://addons/popochiu/icons/ui.png"),
label = "Add to Graphic Interface"
}
] + super()
func _menu_item_pressed(id: int) -> void:
match id:
AudioOptions.ADD_TO_MUSIC:
group_selected.emit(PopochiuResources.AudioTypes.MUSIC)
AudioOptions.ADD_TO_SFX:
group_selected.emit(PopochiuResources.AudioTypes.SOUND_EFFECT)
AudioOptions.ADD_TO_VOICE:
group_selected.emit(PopochiuResources.AudioTypes.VOICE)
AudioOptions.ADD_TO_UI:
group_selected.emit(PopochiuResources.AudioTypes.UI)
_:
super(id)
func _remove_from_popochiu() -> void:
# Remove the AudioCue from popochiu_data.cfg ---------------------------------------------------
var group_data: Array = PopochiuResources.get_data_value(
"audio", cue_group, []
)
if group_data:
group_data.erase(audio_cue.resource_path)
if group_data.is_empty():
PopochiuResources.erase_data_value("audio", cue_group)
else:
group_data.sort_custom(
func (a: String, b: String) -> bool:
return PopochiuUtils.sort_by_file_name(a, b)
)
PopochiuResources.set_data_value("audio", cue_group, group_data)
# Remove the AudioCue from the A singleton -----------------------------------------------------
PopochiuResources.remove_audio_autoload(cue_group, name, audio_cue.resource_path)
# Delete the file in its corresponding group in Audio tab
deleted.emit(audio_cue.audio.resource_path)
if _delete_dialog.check_box.button_pressed:
_delete_from_file_system()
else:
queue_free()
func _delete_from_file_system() -> void:
# Delete the .tres file from the file system
var err: int = DirAccess.remove_absolute(path)
if err != OK:
PopochiuUtils.print_error("Couldn't delete audio cue %s (err_code: %d)" % [path, err])
return
# Delete the audio file linked to the cue
var audio_file_path := audio_cue.audio.resource_path
err = DirAccess.remove_absolute(audio_file_path)
if err != OK:
PopochiuUtils.print_error(
"Couldn't delete audio file %s (err_code: %d)" % [audio_file_path, err]
)
return
# Do this so Godot removes the .import file of the audio file
EditorInterface.get_resource_filesystem().update_file(audio_file_path)
EditorInterface.get_resource_filesystem().scan()
EditorInterface.get_resource_filesystem().scan_sources()
queue_free()
#endregion

View file

@ -0,0 +1 @@
uid://lhhhuwe3ye1f

View file

@ -0,0 +1,62 @@
[gd_scene load_steps=9 format=3 uid="uid://ds6ojs55q50ud"]
[ext_resource type="PackedScene" uid="uid://dwtwuqw2hpdpe" path="res://addons/popochiu/editor/main_dock/popochiu_row/popochiu_row.tscn" id="1_i2mx0"]
[ext_resource type="Script" path="res://addons/popochiu/editor/main_dock/popochiu_row/audio_row/popochiu_audio_row.gd" id="2_24kri"]
[ext_resource type="Texture2D" uid="uid://d1dnmfkhscb7r" path="res://addons/popochiu/icons/music.png" id="3_hi2e1"]
[ext_resource type="Texture2D" uid="uid://cfh1uxtaff0ks" path="res://addons/popochiu/icons/sfx.png" id="4_1iw68"]
[ext_resource type="Texture2D" uid="uid://6ewpl4v0td2h" path="res://addons/popochiu/icons/voice.png" id="5_ray7p"]
[ext_resource type="Texture2D" uid="uid://528j2rksws2c" path="res://addons/popochiu/icons/ui.png" id="6_1bl3m"]
[sub_resource type="Image" id="Image_dygia"]
data = {
"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
"format": "RGBA8",
"height": 16,
"mipmaps": false,
"width": 16
}
[sub_resource type="ImageTexture" id="ImageTexture_v80gh"]
image = SubResource("Image_dygia")
[node name="PopochiuAudioRow" instance=ExtResource("1_i2mx0")]
script = ExtResource("2_24kri")
[node name="Label" parent="HBoxContainer" index="0"]
text = ""
[node name="Play" type="Button" parent="Panel/ButtonsContainer" index="0"]
unique_name_in_owner = true
layout_mode = 2
mouse_filter = 1
icon = SubResource("ImageTexture_v80gh")
flat = true
[node name="Stop" type="Button" parent="Panel/ButtonsContainer" index="1"]
unique_name_in_owner = true
layout_mode = 2
mouse_filter = 1
icon = SubResource("ImageTexture_v80gh")
flat = true
[node name="BtnMenu" parent="Panel/ButtonsContainer" index="2"]
icon = SubResource("ImageTexture_v80gh")
item_count = 6
popup/item_0/text = "Add to Music"
popup/item_0/icon = ExtResource("3_hi2e1")
popup/item_0/id = 1
popup/item_1/text = "Add to Sound effects"
popup/item_1/icon = ExtResource("4_1iw68")
popup/item_1/id = 2
popup/item_2/text = "Add to Voices"
popup/item_2/icon = ExtResource("5_ray7p")
popup/item_2/id = 3
popup/item_3/text = "Add to Graphic interface"
popup/item_3/icon = ExtResource("6_1bl3m")
popup/item_3/id = 4
popup/item_4/text = ""
popup/item_4/id = -1
popup/item_4/separator = true
popup/item_5/text = "Remove"
popup/item_5/icon = SubResource("ImageTexture_v80gh")
popup/item_5/id = 0

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 B

View file

@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bw3ie8wfwa2i2"
path="res://.godot/imported/add_to_core.png-52def14ca6e499df1e292c93f01c4349.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/popochiu/editor/main_dock/popochiu_row/images/add_to_core.png"
dest_files=["res://.godot/imported/add_to_core.png-52def14ca6e499df1e292c93f01c4349.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 B

View file

@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bahipxbrrss0o"
path="res://.godot/imported/delete.png-27dd9adc116bbf3fc8b20a99d1331933.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/popochiu/editor/main_dock/popochiu_row/images/delete.png"
dest_files=["res://.godot/imported/delete.png-27dd9adc116bbf3fc8b20a99d1331933.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 B

View file

@ -0,0 +1,34 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://wm7qycjntmfr"
path="res://.godot/imported/open.png-eb4e739212f91fcaedbead9efc5f731f.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/popochiu/editor/main_dock/popochiu_row/images/open.png"
dest_files=["res://.godot/imported/open.png-eb4e739212f91fcaedbead9efc5f731f.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1

View file

@ -0,0 +1,79 @@
@tool
extends "res://addons/popochiu/editor/main_dock/popochiu_row/object_row/popochiu_object_row.gd"
enum CharacterOptions {
DELETE = MenuOptions.DELETE,
ADD_TO_CORE = Options.ADD_TO_CORE,
SET_AS_PC,
}
const TAG_ICON = preload("res://addons/popochiu/icons/player_character.png")
const STATE_TEMPLATE = "res://addons/popochiu/engine/templates/character_state_template.gd"
var is_pc := false : set = set_is_pc
#region Godot ######################################################################################
func _ready() -> void:
super()
# Assign icons
tag.texture = TAG_ICON
#endregion
#region Virtual ####################################################################################
func _get_state_template() -> Script:
return load(STATE_TEMPLATE)
func _clear_tag() -> void:
if is_pc:
is_pc = false
#endregion
#region SetGet #####################################################################################
func set_is_pc(value: bool) -> void:
is_pc = value
if is_pc:
PopochiuEditorHelper.signal_bus.pc_changed.emit(name)
tag.visible = value
menu_popup.set_item_disabled(menu_popup.get_item_index(CharacterOptions.SET_AS_PC), value)
#endregion
#region Private ####################################################################################
func _get_menu_cfg() -> Array:
return [
{
id = CharacterOptions.SET_AS_PC,
icon = TAG_ICON,
label = "Set as Player-controlled Character (PC)",
},
] + super()
func _menu_item_pressed(id: int) -> void:
match id:
CharacterOptions.SET_AS_PC:
self.is_pc = true
_:
super(id)
func _remove_from_core() -> void:
# Delete the object from Popochiu
PopochiuResources.remove_autoload_obj(PopochiuResources.C_SNGL, name)
PopochiuResources.erase_data_value("characters", str(name))
# Continue with the deletion flow
super()
#endregion

View file

@ -0,0 +1,15 @@
@tool
extends "res://addons/popochiu/editor/main_dock/popochiu_row/object_row/popochiu_object_row.gd"
#region Private ####################################################################################
func _remove_from_core() -> void:
# Delete the object from Popochiu
PopochiuResources.remove_autoload_obj(PopochiuResources.D_SNGL, name)
PopochiuResources.erase_data_value("dialogs", str(name))
# Continue with the deletion flow
super()
#endregion

View file

@ -0,0 +1,78 @@
@tool
extends "res://addons/popochiu/editor/main_dock/popochiu_row/object_row/popochiu_object_row.gd"
enum InventoryItemOptions {
DELETE = MenuOptions.DELETE,
ADD_TO_CORE = Options.ADD_TO_CORE,
START_WITH_IT,
}
const TAG_ICON = preload("res://addons/popochiu/icons/inventory_item_start.png")
const STATE_TEMPLATE = "res://addons/popochiu/engine/templates/inventory_item_state_template.gd"
var is_on_start := false : set = set_is_on_start
#region Godot ######################################################################################
func _ready() -> void:
super()
# Assign icons
tag.texture = TAG_ICON
#endregion
#region Virtual ####################################################################################
func _get_state_template() -> Script:
return load(STATE_TEMPLATE)
#endregion
#region SetGet #####################################################################################
func set_is_on_start(value: bool) -> void:
is_on_start = value
tag.visible = value
#endregion
#region Private ####################################################################################
func _get_menu_cfg() -> Array:
return [
{
id = InventoryItemOptions.START_WITH_IT,
icon = TAG_ICON,
label = "Start with it",
},
] + super()
func _menu_item_pressed(id: int) -> void:
match id:
InventoryItemOptions.START_WITH_IT:
var items: Array = PopochiuConfig.get_inventory_items_on_start()
var script_name := str(name)
if script_name in items:
items.erase(script_name)
else:
items.append(script_name)
PopochiuConfig.set_inventory_items_on_start(items)
self.is_on_start = script_name in items
_:
super(id)
func _remove_from_core() -> void:
# Delete the object from Popochiu
PopochiuResources.remove_autoload_obj(PopochiuResources.I_SNGL, name)
PopochiuResources.erase_data_value("inventory_items", str(name))
# Continue with the deletion flow
super()
#endregion

View file

@ -0,0 +1,334 @@
@tool
extends "res://addons/popochiu/editor/main_dock/popochiu_row/popochiu_row.gd"
## Row for the main object types: Room, Character, Inventory item, Dialog
enum Options {
DELETE = MenuOptions.DELETE,
ADD_TO_CORE,
}
const DELETE_MESSAGE = "This will remove the [b]%s[/b] object in [b]%s[/b] scene. Uses of this \
object in scripts will not work anymore. This action cannot be undone. Continue?"
const DELETE_ASK_MESSAGE = "Do you want to delete the [b]%s[/b] folder too?%s (cannot be reversed)"
const ADD_TO_CORE_ICON = preload(
"res://addons/popochiu/editor/main_dock/popochiu_row/images/add_to_core.png"
)
const AUDIO_FILE_TYPES = ["AudioStreamOggVorbis", "AudioStreamMP3", "AudioStreamWAV"]
@onready var btn_open: Button = %BtnOpen
@onready var btn_script: Button = %BtnScript
@onready var btn_state: Button = %BtnState
@onready var btn_state_script: Button = %BtnStateScript
#region Godot ######################################################################################
func _ready() -> void:
# Assign icons
btn_open.icon = get_theme_icon("InstanceOptions", "EditorIcons")
btn_script.icon = get_theme_icon("Script", "EditorIcons")
btn_state.icon = get_theme_icon("Object", "EditorIcons")
btn_state_script.icon = get_theme_icon("GDScript", "EditorIcons")
# Connect to signals and create the options for the menu
super()
# Connect to children's signals
btn_open.pressed.connect(_open)
btn_script.pressed.connect(_open_script)
btn_state.pressed.connect(_edit_state)
btn_state_script.pressed.connect(_open_state_script)
# Disable some options by default
var add_to_core_idx := menu_popup.get_item_index(Options.ADD_TO_CORE)
if add_to_core_idx >= 0:
menu_popup.set_item_disabled(add_to_core_idx, true)
#endregion
#region Virtual ####################################################################################
## Shows a confirmation popup to ask the developer if the Popochiu object should be removed only
## from the core, or from the file system too.
func _remove_object() -> void:
var location := _get_location()
# Look into the Object"s folder for audio files and AudioCues to show the developer that those
# files will be removed too.
var audio_files := _search_audio_files(
EditorInterface.get_resource_filesystem().get_filesystem_path(path.get_base_dir())
)
_delete_dialog = PopochiuEditorHelper.DELETE_CONFIRMATION_SCENE.instantiate()
_delete_dialog.title = "Remove %s from %s" % [name, location]
_delete_dialog.message = DELETE_MESSAGE % [name, location]
_delete_dialog.ask = DELETE_ASK_MESSAGE % [
path.get_base_dir(),
"" if audio_files.is_empty()
else " ([b]%d[/b] audio cues will be deleted)" % audio_files.size()
]
_delete_dialog.on_confirmed = _remove_from_core
PopochiuEditorHelper.show_delete_confirmation(_delete_dialog)
func _get_state_template() -> Script:
return null
func _get_location() -> String:
return "Popochiu"
#endregion
#region Public #####################################################################################
## Called to make the row appear semitransparent to indicate that the object is in the project
## (has a folder with files inside) but is not part of the [code]popochiu_data.cfg[/code] file nor
## its corresponding autoload (e.g., R, C, I, D). This can happen when one removes an object from
## the project without removing its files, or when adding objects from another project.
func show_as_not_in_core() -> void:
label.modulate.a = 0.5
menu_popup.set_item_disabled(menu_popup.get_item_index(Options.ADD_TO_CORE), false)
#endregion
#region Private ####################################################################################
func _get_menu_cfg() -> Array:
return [
{
id = Options.ADD_TO_CORE,
icon = ADD_TO_CORE_ICON,
label = "Add to Popochiu",
types = PopochiuResources.MAIN_TYPES
},
] + super()
func _menu_item_pressed(id: int) -> void:
match id:
Options.ADD_TO_CORE:
_add_object_to_core()
_:
super(id)
## Add this Object (Room, Character, InventoryItem, Dialog) to popochiu_data.cfg so it can be used
## by Popochiu.
func _add_object_to_core() -> void:
var target_array := ""
var resource: Resource
if ".tscn" in path:
resource = load(path.replace(".tscn", ".tres"))
else:
resource = load(path)
match type:
PopochiuResources.Types.ROOM:
target_array = "rooms"
PopochiuResources.Types.CHARACTER:
target_array = "characters"
PopochiuResources.Types.INVENTORY_ITEM:
target_array = "inventory_items"
PopochiuResources.Types.DIALOG:
target_array = "dialogs"
if PopochiuEditorHelper.add_resource_to_popochiu(target_array, resource) != OK:
PopochiuUtils.print_error("Couldn't add Object [b]%s[/b] to Popochiu." % str(name))
return
# Add the object to its corresponding singleton
PopochiuResources.update_autoloads(true)
label.modulate.a = 1.0
menu_popup.set_item_disabled(menu_popup.get_item_index(Options.ADD_TO_CORE), true)
## Selects the main file of the object in the FileSystem and opens it so that it can be edited.
func _open() -> void:
EditorInterface.select_file(path)
if ".tres" in path:
EditorInterface.edit_resource(load(path))
else:
EditorInterface.set_main_screen_editor("2D")
EditorInterface.open_scene_from_path(path)
select()
func _open_script() -> void:
var script_path := path
if ".tscn" in path:
# A room, character, inventory item, or prop
script_path = path.replace(".tscn", ".gd")
elif ".tres" in path:
# A dialog
script_path = path.replace(".tres", ".gd")
elif not ".gd" in path:
return
EditorInterface.select_file(script_path)
EditorInterface.set_main_screen_editor("Script")
EditorInterface.edit_script(load(script_path))
select()
func _edit_state() -> void:
EditorInterface.select_file(path.replace(".tscn", ".tres"))
EditorInterface.edit_resource(load(path.replace(".tscn", ".tres")))
select()
func _open_state_script() -> void:
var state := load(path.replace(".tscn", ".tres"))
EditorInterface.select_file(state.get_script().resource_path)
EditorInterface.set_main_screen_editor("Script")
EditorInterface.edit_resource(state.get_script())
select()
func _search_audio_files(dir: EditorFileSystemDirectory) -> Array:
var files := []
for idx in dir.get_subdir_count():
files.append_array(_search_audio_files(dir.get_subdir(idx)))
for idx in dir.get_file_count():
match dir.get_file_type(idx):
AUDIO_FILE_TYPES:
files.append(dir.get_file_path(idx))
return files
func _remove_from_core() -> void:
# Check if the files should be deleted in the file system
if _delete_dialog.check_box.button_pressed:
_delete_from_file_system()
elif type in PopochiuResources.MAIN_TYPES:
show_as_not_in_core()
var edited_scene: Node = EditorInterface.get_edited_scene_root()
if edited_scene and edited_scene.get("script_name") and edited_scene.script_name == name:
# If the open scene matches the object being deleted, skip saving the scene
queue_free()
return
EditorInterface.save_scene()
queue_free()
## Remove this object's directory (subfolders included) from the file system.
func _delete_from_file_system() -> void:
var object_dir: EditorFileSystemDirectory = \
EditorInterface.get_resource_filesystem().get_filesystem_path(path.get_base_dir())
# Remove files, sub folders and its files.
_recursive_delete(object_dir)
## Remove the `dir` directory from the system. For Godot to be able to delete a directory, it has to
## be empty, so this method first deletes the files from from the directory and each of its
## subdirectories.
func _recursive_delete(dir: EditorFileSystemDirectory) -> void:
if dir.get_file_count() > 0:
assert(
_delete_files(dir) == OK,
"[Popochiu] Error removing files in recursive elimination of %s" % dir.get_path()
)
if dir.get_subdir_count() > 0:
for folder_idx in dir.get_subdir_count():
# Check if there are more folders inside the folder or delete the files inside it before
# deleting the folder itself
_recursive_delete(dir.get_subdir(folder_idx))
assert(
DirAccess.remove_absolute(dir.get_path()) == OK,
"[Popochiu] Error removing folder in recursive elimination of %s" % dir.get_path()
)
EditorInterface.get_resource_filesystem().scan()
## Delete files within [param dir] directory. First, get the paths to each file, then delete them
## one by one calling [method EditorFileSystem.update_file], so that in case it's an imported file,
## its [b].import[/b] is also deleted.
func _delete_files(dir: EditorFileSystemDirectory) -> int:
# Stores the paths of the files to be deleted.
var files_paths := []
# Stores the paths of the audio resources to delete
var deleted_audios := []
for file_idx: int in dir.get_file_count():
match dir.get_file_type(file_idx):
AUDIO_FILE_TYPES:
deleted_audios.append(dir.get_file_path(file_idx))
"Resource":
var resource: Resource = load(dir.get_file_path(file_idx))
if not resource is AudioCue:
# If the resource is not an AudioCue, then it should be ignored for deletion
# in the game data
continue
# Delete the [PopochiuAudioCue] in the project data file and the A singleton
assert(
_delete_audio_cue_in_data(resource) == true,
"[Popochiu] Couldn't remove [b]%s[/b] during deletion of [b]%s[/b]." %
[resource.resource_path, dir.get_path()]
)
deleted_audios.append(resource.audio.resource_path)
files_paths.append(dir.get_file_path(file_idx))
for fp: String in files_paths:
var err: int = DirAccess.remove_absolute(fp)
if err != OK:
PopochiuUtils.print_error("Couldn't delete file %s. err_code:%d" % [err, fp])
return err
EditorInterface.get_resource_filesystem().scan()
# Delete the rows of audio files and the deleted AudioCues in the Audio tab
if not deleted_audios.is_empty():
PopochiuEditorHelper.signal_bus.audio_cues_deleted.emit(deleted_audios)
# Remove extra files (like .import)
for file_name: String in DirAccess.get_files_at(dir.get_path()):
DirAccess.remove_absolute(dir.get_path() + "/" + file_name)
EditorInterface.get_resource_filesystem().scan()
return OK
## Looks to which audio group corresponds [param audio_cue] and deletes it both from
## [code]popochiu_data.cfg[/code] and the [b]A[/b] singleton (which is the one used to allow code
## autocompletion related to [PopochiuAudioCue]s).
func _delete_audio_cue_in_data(audio_cue: AudioCue) -> bool:
# TODO: This could be improved a lot if each PopochiuAudioCue has a variable to store the group
# to which it corresponds to.
# Delete the [PopochiuAudioCue] in the popochiu_data.cfg
for cue_group in ["mx_cues", "sfx_cues", "vo_cues", "ui_cues"]:
var cues: Array = PopochiuResources.get_data_value("audio", cue_group, [])
if not cues.has(audio_cue.resource_path): continue
cues.erase(audio_cue.resource_path)
if PopochiuResources.set_data_value("audio", cue_group, cues) != OK:
return false
# Fix #59 : remove the [PopochiuAudioCue] from the [A] singleton
PopochiuResources.remove_audio_autoload(
cue_group, audio_cue.resource_name, audio_cue.resource_path
)
break
return true
#endregion

View file

@ -0,0 +1 @@
uid://dj0dw4jcynb3p

View file

@ -0,0 +1,58 @@
[gd_scene load_steps=5 format=3 uid="uid://dwbo3pl372ugo"]
[ext_resource type="PackedScene" uid="uid://dwtwuqw2hpdpe" path="res://addons/popochiu/editor/main_dock/popochiu_row/popochiu_row.tscn" id="1_xi41g"]
[ext_resource type="Script" path="res://addons/popochiu/editor/main_dock/popochiu_row/object_row/popochiu_object_row.gd" id="2_g05pm"]
[sub_resource type="Image" id="Image_15l6n"]
data = {
"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 44, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 94, 94, 234, 255, 95, 95, 43, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
"format": "RGBA8",
"height": 16,
"mipmaps": false,
"width": 16
}
[sub_resource type="ImageTexture" id="ImageTexture_12sj2"]
image = SubResource("Image_15l6n")
[node name="PopochiuMainObjectRow" instance=ExtResource("1_xi41g")]
script = ExtResource("2_g05pm")
[node name="Label" parent="HBoxContainer" index="0"]
text = "PopochiuMainObjectRow"
[node name="BtnOpen" type="Button" parent="Panel/ButtonsContainer" index="0"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 4
size_flags_vertical = 3
tooltip_text = "Open in Editor"
icon = SubResource("ImageTexture_12sj2")
flat = true
[node name="BtnScript" type="Button" parent="Panel/ButtonsContainer" index="1"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 4
size_flags_vertical = 3
tooltip_text = "Open in Script"
icon = SubResource("ImageTexture_12sj2")
flat = true
[node name="BtnState" type="Button" parent="Panel/ButtonsContainer" index="2"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 4
size_flags_vertical = 3
tooltip_text = "Open state"
icon = SubResource("ImageTexture_12sj2")
flat = true
[node name="BtnStateScript" type="Button" parent="Panel/ButtonsContainer" index="3"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 4
size_flags_vertical = 3
tooltip_text = "Open state Script"
icon = SubResource("ImageTexture_12sj2")
flat = true

View file

@ -0,0 +1,60 @@
@tool
extends "res://addons/popochiu/editor/main_dock/popochiu_row/object_row/popochiu_object_row.gd"
const PROP_TEMPLATE = "res://addons/popochiu/engine/templates/prop_template.gd"
var node_path := ""
#region Godot ######################################################################################
func _ready() -> void:
super()
if not FileAccess.file_exists(path.replace(".tscn", ".gd")):
btn_script.hide()
btn_state.hide()
btn_state_script.hide()
#endregion
#region Virtual ####################################################################################
func _get_location() -> String:
# Structure of path: "res://game/rooms/room_name/props/prop_name/"
# path split: [res:, popochiu, rooms, room_name, props, prop_name]
return "Room%s" % (path.split("/", false)[3]).to_pascal_case()
#endregion
#region Private ####################################################################################
func _remove_from_core() -> void:
var room_child_to_free: Node = null
if EditorInterface.get_edited_scene_root() is PopochiuRoom:
var opened_room: PopochiuRoom = EditorInterface.get_edited_scene_root()
match type:
PopochiuResources.Types.PROP:
room_child_to_free = opened_room.get_prop(str(name))
PopochiuResources.Types.HOTSPOT:
room_child_to_free = opened_room.get_hotspot(str(name))
PopochiuResources.Types.MARKER:
room_child_to_free = opened_room.get_marker(str(name))
PopochiuResources.Types.REGION:
room_child_to_free = opened_room.get_region(str(name))
PopochiuResources.Types.WALKABLE_AREA:
room_child_to_free = opened_room.get_walkable_area(str(name))
# Continue with the deletion flow
super()
# Fix #196: Remove the Node from the Room tree once the folder of the object has been deleted
# from the FileSystem (this applies to Props, Hotspots, Walkable areas and Regions).
if room_child_to_free:
room_child_to_free.queue_free()
EditorInterface.save_scene()
#endregion

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