Compare commits
No commits in common. "11beffc537c11cc38a13487dc2b714351bfa24e5" and "3e773e77c6bbc0c17c6bed8ef1019645d1965e91" have entirely different histories.
11beffc537
...
3e773e77c6
|
@ -1,9 +1,9 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue,css,scss,sass,less,styl}]
|
||||
charset = utf-8
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
end_of_line = lf
|
||||
max_line_length = 100
|
||||
|
|
|
@ -1,48 +0,0 @@
|
|||
/* eslint-env node */
|
||||
require('@rushstack/eslint-patch/modern-module-resolution')
|
||||
|
||||
module.exports = {
|
||||
root: true,
|
||||
env: {
|
||||
browser: true,
|
||||
node: true,
|
||||
},
|
||||
extends: [
|
||||
'eslint:recommended',
|
||||
'plugin:vue/vue3-essential',
|
||||
'@vue/eslint-config-typescript/recommended',
|
||||
'@vue/eslint-config-prettier',
|
||||
],
|
||||
overrides: [
|
||||
{
|
||||
files: ['cypress/e2e/**.{cy,spec}.{js,ts,jsx,tsx}'],
|
||||
extends: ['plugin:cypress/recommended'],
|
||||
},
|
||||
{
|
||||
// https://typescript-eslint.io/docs/linting/troubleshooting/#i-get-errors-from-the-no-undef-rule-about-global-variables-not-being-defined-even-though-there-are-no-typescript-errors
|
||||
files: ['*.vue'],
|
||||
rules: {
|
||||
'no-undef': 'off',
|
||||
},
|
||||
},
|
||||
{
|
||||
files: ['*.story.vue', '*.story.controls.vue'],
|
||||
rules: {
|
||||
'@typescript-eslint/no-empty-function': 'off',
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
'vue/require-v-for-key': 'off',
|
||||
'vue/no-mutating-props': 'off',
|
||||
},
|
||||
},
|
||||
],
|
||||
rules: {
|
||||
// see https://vuejs.org/guide/extras/reactivity-transform.html
|
||||
'vue/no-setup-props-destructure': 'off',
|
||||
// TODO: discuss if we want to force this
|
||||
'vue/multi-word-component-names': 'off',
|
||||
// see https://eslint.org/docs/latest/rules/no-prototype-builtins#when-not-to-use-it
|
||||
'no-prototype-builtins': 'off',
|
||||
// as long as it is explicit, it is fine to use any
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
},
|
||||
}
|
1
.gitattributes
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
* text=auto eol=lf
|
9
.gitignore
vendored
|
@ -8,20 +8,23 @@ pnpm-debug.log*
|
|||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
# old source code for reference?!
|
||||
src/old
|
||||
*.tsbuildinfo
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
{
|
||||
"semi": false,
|
||||
"singleQuote": true
|
||||
}
|
6
.prettierrc.json
Normal file
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/prettierrc",
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"printWidth": 100
|
||||
}
|
9
.vscode/extensions.json
vendored
Normal file
|
@ -0,0 +1,9 @@
|
|||
{
|
||||
"recommendations": [
|
||||
"Vue.volar",
|
||||
"vitest.explorer",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"EditorConfig.EditorConfig",
|
||||
"esbenp.prettier-vscode"
|
||||
]
|
||||
}
|
674
LICENSE
Normal 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>.
|
19
README.md
|
@ -1,20 +1,23 @@
|
|||
# building-game
|
||||
# Vue Shovel
|
||||
|
||||
> A blocky, side-scrolling, building and exploration game
|
||||
> A blocky, side-scrolling, building and exploration game ...phew, what a pile of words.
|
||||
|
||||
This version of DIG! is reimplemented with Vue3 and Typescript. To see the old (and probably broken) version, check the vue2 branch.
|
||||
This is Vue Shovel, a complete rewrite of DIG! in Typescript, with Vue3.
|
||||
|
||||
## Build Setup
|
||||
|
||||
``` bash
|
||||
```bash
|
||||
# install dependencies
|
||||
yarn
|
||||
pnpm
|
||||
|
||||
# serve with hot reload at localhost:8080
|
||||
yarn dev
|
||||
# serve with hot reload at localhost:5173
|
||||
pnpm dev
|
||||
|
||||
# build for production with minification
|
||||
yarn build
|
||||
pnpm build
|
||||
|
||||
# the production build can be run locally with
|
||||
pnpm preview
|
||||
```
|
||||
|
||||
|
||||
|
|
BIN
dist/bedrock.png
vendored
Before Width: | Height: | Size: 1.2 KiB |
7
dist/build.js
vendored
1
dist/build.js.map
vendored
BIN
dist/dwarf_left.png
vendored
Before Width: | Height: | Size: 607 B |
BIN
dist/dwarf_right.png
vendored
Before Width: | Height: | Size: 629 B |
BIN
dist/grass01.png
vendored
Before Width: | Height: | Size: 614 B |
BIN
dist/rock.png
vendored
Before Width: | Height: | Size: 1.2 KiB |
BIN
dist/soil01.png
vendored
Before Width: | Height: | Size: 586 B |
BIN
dist/soil_gravel01.png
vendored
Before Width: | Height: | Size: 940 B |
BIN
dist/tree_crown_left.png
vendored
Before Width: | Height: | Size: 719 B |
BIN
dist/tree_crown_left_mixed.png
vendored
Before Width: | Height: | Size: 1.4 KiB |
BIN
dist/tree_crown_middle.png
vendored
Before Width: | Height: | Size: 1.5 KiB |
BIN
dist/tree_crown_right.png
vendored
Before Width: | Height: | Size: 1 KiB |
BIN
dist/tree_crown_right_mixed.png
vendored
Before Width: | Height: | Size: 1.3 KiB |
BIN
dist/tree_root_left.png
vendored
Before Width: | Height: | Size: 340 B |
BIN
dist/tree_root_left_mixed.png
vendored
Before Width: | Height: | Size: 539 B |
BIN
dist/tree_root_middle.png
vendored
Before Width: | Height: | Size: 938 B |
BIN
dist/tree_root_right.png
vendored
Before Width: | Height: | Size: 280 B |
BIN
dist/tree_root_right_mixed.png
vendored
Before Width: | Height: | Size: 539 B |
BIN
dist/tree_top_left.png
vendored
Before Width: | Height: | Size: 139 B |
BIN
dist/tree_top_left_mixed.png
vendored
Before Width: | Height: | Size: 257 B |
BIN
dist/tree_top_middle.png
vendored
Before Width: | Height: | Size: 747 B |
BIN
dist/tree_top_right.png
vendored
Before Width: | Height: | Size: 205 B |
BIN
dist/tree_top_right_mixed.png
vendored
Before Width: | Height: | Size: 257 B |
BIN
dist/tree_trunk_left.png
vendored
Before Width: | Height: | Size: 735 B |
BIN
dist/tree_trunk_left_mixed.png
vendored
Before Width: | Height: | Size: 1.1 KiB |
BIN
dist/tree_trunk_middle.png
vendored
Before Width: | Height: | Size: 1.2 KiB |
BIN
dist/tree_trunk_right.png
vendored
Before Width: | Height: | Size: 750 B |
BIN
dist/tree_trunk_right_mixed.png
vendored
Before Width: | Height: | Size: 1.1 KiB |
7
env.d.ts
vendored
|
@ -1,8 +1 @@
|
|||
/// <reference types="vite/client" />
|
||||
|
||||
declare module "*.vue" {
|
||||
import { DefineComponent } from "vue";
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
|
||||
const component: DefineComponent<{}, {}, any>;
|
||||
export default component;
|
||||
}
|
||||
|
|
32
eslint.config.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
import pluginVue from 'eslint-plugin-vue'
|
||||
import { defineConfigWithVueTs, vueTsConfigs } from '@vue/eslint-config-typescript'
|
||||
import pluginVitest from '@vitest/eslint-plugin'
|
||||
import oxlint from 'eslint-plugin-oxlint'
|
||||
import skipFormatting from '@vue/eslint-config-prettier/skip-formatting'
|
||||
|
||||
// To allow more languages other than `ts` in `.vue` files, uncomment the following lines:
|
||||
// import { configureVueProject } from '@vue/eslint-config-typescript'
|
||||
// configureVueProject({ scriptLangs: ['ts', 'tsx'] })
|
||||
// More info at https://github.com/vuejs/eslint-config-typescript/#advanced-setup
|
||||
|
||||
export default defineConfigWithVueTs(
|
||||
{
|
||||
name: 'app/files-to-lint',
|
||||
files: ['**/*.{ts,mts,tsx,vue}'],
|
||||
},
|
||||
|
||||
{
|
||||
name: 'app/files-to-ignore',
|
||||
ignores: ['**/dist/**', '**/dist-ssr/**', '**/coverage/**'],
|
||||
},
|
||||
|
||||
pluginVue.configs['flat/essential'],
|
||||
vueTsConfigs.recommended,
|
||||
|
||||
{
|
||||
...pluginVitest.configs.recommended,
|
||||
files: ['src/**/__tests__/*'],
|
||||
},
|
||||
...oxlint.configs['flat/recommended'],
|
||||
skipFormatting,
|
||||
)
|
3
foo/.vscode/extensions.json
vendored
|
@ -1,3 +0,0 @@
|
|||
{
|
||||
"recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
# Vue 3 + TypeScript + Vite
|
||||
|
||||
This template should help get you started developing with Vue 3 and TypeScript in Vite. The template uses Vue 3 `<script setup>` SFCs, check out the [script setup docs](https://v3.vuejs.org/api/sfc-script-setup.html#sfc-script-setup) to learn more.
|
||||
|
||||
## Recommended IDE Setup
|
||||
|
||||
- [VS Code](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
|
||||
|
||||
## Type Support For `.vue` Imports in TS
|
||||
|
||||
TypeScript cannot handle type information for `.vue` imports by default, so we replace the `tsc` CLI with `vue-tsc` for type checking. In editors, we need [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin) to make the TypeScript language service aware of `.vue` types.
|
||||
|
||||
If the standalone TypeScript plugin doesn't feel fast enough to you, Volar has also implemented a [Take Over Mode](https://github.com/johnsoncodehk/volar/discussions/471#discussioncomment-1361669) that is more performant. You can enable it by the following steps:
|
||||
|
||||
1. Disable the built-in TypeScript Extension
|
||||
1. Run `Extensions: Show Built-in Extensions` from VSCode's command palette
|
||||
2. Find `TypeScript and JavaScript Language Features`, right click and select `Disable (Workspace)`
|
||||
2. Reload the VSCode window by running `Developer: Reload Window` from the command palette.
|
|
@ -1,20 +0,0 @@
|
|||
{
|
||||
"name": "foo",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc && vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"vue": "^3.2.45"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-vue": "^4.0.0",
|
||||
"typescript": "^4.9.3",
|
||||
"vite": "^4.1.0",
|
||||
"vue-tsc": "^1.0.24"
|
||||
}
|
||||
}
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
Before Width: | Height: | Size: 1.5 KiB |
|
@ -1,30 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
import HelloWorld from './components/HelloWorld.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<a href="https://vitejs.dev" target="_blank">
|
||||
<img src="/vite.svg" class="logo" alt="Vite logo" />
|
||||
</a>
|
||||
<a href="https://vuejs.org/" target="_blank">
|
||||
<img src="./assets/vue.svg" class="logo vue" alt="Vue logo" />
|
||||
</a>
|
||||
</div>
|
||||
<HelloWorld msg="Vite + Vue" />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.logo {
|
||||
height: 6em;
|
||||
padding: 1.5em;
|
||||
will-change: filter;
|
||||
transition: filter 300ms;
|
||||
}
|
||||
.logo:hover {
|
||||
filter: drop-shadow(0 0 2em #646cffaa);
|
||||
}
|
||||
.logo.vue:hover {
|
||||
filter: drop-shadow(0 0 2em #42b883aa);
|
||||
}
|
||||
</style>
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
Before Width: | Height: | Size: 496 B |
|
@ -1,38 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
defineProps<{ msg: string }>()
|
||||
|
||||
const count = ref(0)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<h1>{{ msg }}</h1>
|
||||
|
||||
<div class="card">
|
||||
<button type="button" @click="count++">count is {{ count }}</button>
|
||||
<p>
|
||||
Edit
|
||||
<code>components/HelloWorld.vue</code> to test HMR
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p>
|
||||
Check out
|
||||
<a href="https://vuejs.org/guide/quick-start.html#local" target="_blank"
|
||||
>create-vue</a
|
||||
>, the official Vue + Vite starter
|
||||
</p>
|
||||
<p>
|
||||
Install
|
||||
<a href="https://github.com/johnsoncodehk/volar" target="_blank">Volar</a>
|
||||
in your IDE for a better DX
|
||||
</p>
|
||||
<p class="read-the-docs">Click on the Vite and Vue logos to learn more</p>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.read-the-docs {
|
||||
color: #888;
|
||||
}
|
||||
</style>
|
|
@ -1,80 +0,0 @@
|
|||
:root {
|
||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
#app {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
|
@ -1,7 +1,8 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<html lang="">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||
<link rel="preload" as="image" href="/Tiles/dirt.png" />
|
||||
<link rel="preload" as="image" href="/Tiles/dirt_grass.png" />
|
||||
|
@ -15,8 +16,8 @@
|
|||
<link rel="preload" as="image" href="/Tiles/trunk_mid.png" />
|
||||
<link rel="preload" as="image" href="/Tiles/trunk_bottom.png" />
|
||||
<link rel="preload" as="image" href="/Tiles/leaves_transparent.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Dig!</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Vue Shovel</title>
|
||||
<style>
|
||||
:root {
|
||||
--block-size: 64px;
|
||||
|
|
61
package.json
|
@ -1,33 +1,58 @@
|
|||
{
|
||||
"name": "DIG",
|
||||
"name": "vue-shovel",
|
||||
"description": "A blocky, side-scrolling, digging and exploration game",
|
||||
"version": "0.0.1",
|
||||
"version": "0.1.0",
|
||||
"author": "koehr <n@koehr.in>",
|
||||
"license": "MIT",
|
||||
"license": "GPLv3",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc && vite build",
|
||||
"build": "run-p type-check \"build-only {@}\" --",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix"
|
||||
"test:unit": "vitest",
|
||||
"build-only": "vite build",
|
||||
"type-check": "vue-tsc --build",
|
||||
"lint:oxlint": "oxlint . --fix -D correctness --ignore-path .gitignore",
|
||||
"lint:eslint": "eslint . --fix",
|
||||
"lint": "run-s lint:*",
|
||||
"format": "prettier --write src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"alea": "^1.0.1",
|
||||
"simplex-noise": "^4.0.1",
|
||||
"vue": "^3.4.22"
|
||||
"simplex-noise": "^4.0.3",
|
||||
"vue": "^3.5.13"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rushstack/eslint-patch": "^1.10.2",
|
||||
"@typescript-eslint/parser": "^7.7.0",
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"@vue/eslint-config-prettier": "^9.0.0",
|
||||
"@vue/eslint-config-typescript": "^13.0.0",
|
||||
"eslint": "^9.0.0",
|
||||
"eslint-plugin-vue": "^9.25.0",
|
||||
"typescript": "^5.4.5",
|
||||
"unplugin-vue-macros": "^2.9.1",
|
||||
"vite": "^5.2.9",
|
||||
"vue-tsc": "^2.0.13"
|
||||
"@tsconfig/node22": "^22.0.0",
|
||||
"@types/jsdom": "^21.1.7",
|
||||
"@types/node": "^22.13.9",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"@vitest/eslint-plugin": "^1.1.36",
|
||||
"@vue/eslint-config-prettier": "^10.2.0",
|
||||
"@vue/eslint-config-typescript": "^14.5.0",
|
||||
"@vue/test-utils": "^2.4.6",
|
||||
"@vue/tsconfig": "^0.7.0",
|
||||
"eslint": "^9.21.0",
|
||||
"eslint-plugin-oxlint": "^0.15.13",
|
||||
"eslint-plugin-vue": "~10.0.0",
|
||||
"jiti": "^2.4.2",
|
||||
"jsdom": "^26.0.0",
|
||||
"npm-run-all2": "^7.0.2",
|
||||
"oxlint": "^0.15.13",
|
||||
"prettier": "3.5.3",
|
||||
"typescript": "~5.8.0",
|
||||
"vite": "^6.2.1",
|
||||
"vite-plugin-vue-devtools": "^7.7.2",
|
||||
"vitest": "^3.0.8",
|
||||
"vue-tsc": "^2.2.8"
|
||||
},
|
||||
"pnpm": {
|
||||
"ignoredBuiltDependencies": [
|
||||
"esbuild"
|
||||
],
|
||||
"onlyBuiltDependencies": [
|
||||
"esbuild"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
4232
pnpm-lock.yaml
generated
Normal file
BIN
public/Items/torchCeiling.png
Normal file
After Width: | Height: | Size: 2.1 KiB |
BIN
public/Items/torchFloor.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
public/Items/torchLeft.png
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
public/Items/torchRight.png
Normal file
After Width: | Height: | Size: 1.2 KiB |
14
simplex.html
|
@ -1,14 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Simplex Noise Test</title>
|
||||
<style>
|
||||
body, html { width: 100%; height: 100%; padding: 0; margin: 0; overflow: hidden; background: black; }
|
||||
canvas { display: block; width: 1024px; height: 768px; margin: calc(50vh - 768px / 2) auto 0; border: 2px solid #333; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<canvas></canvas>
|
||||
<script type="module" src="/src/simplex-demo.ts"></script>
|
||||
</body>
|
||||
</html>
|
219
src/App.vue
|
@ -1,72 +1,114 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import type { Block, BlockType, Direction, Item, InventoryItem, LightSource } from './types.d'
|
||||
import { ref, computed, watch, onMounted, useTemplateRef } from 'vue'
|
||||
import Help from './screens/help.vue'
|
||||
import Inventory from './screens/inventory.vue'
|
||||
import Background from './Background.vue'
|
||||
|
||||
import { BLOCK_SIZE, STAGE_WIDTH, STAGE_HEIGHT, type Block } from './level/def'
|
||||
import { BLOCK_SIZE, STAGE_WIDTH, STAGE_HEIGHT, softTerrain, hardTerrain } from './level/def'
|
||||
import { getItem, getItemClass } from './level/items'
|
||||
import createLevel from './level'
|
||||
|
||||
import usePlayer from './util/usePlayer'
|
||||
import useTime from './util/useTime'
|
||||
import useInput from './util/useInput'
|
||||
import usePlayer, { type InventoryItem } from './util/usePlayer'
|
||||
import useLightMap from './util/useLightMap'
|
||||
import useLightMask from './util/useLightMask'
|
||||
|
||||
const { updateTime, time, timeOfDay, clock } = useTime()
|
||||
const { player, direction, dx, dy, pocket, unpocket } = usePlayer()
|
||||
const { inputX, inputY, running, paused, help, inventory } = useInput()
|
||||
const level = createLevel(STAGE_WIDTH + 2, STAGE_HEIGHT + 2)
|
||||
|
||||
const lightMapEl = ref<HTMLCanvasElement | undefined>(undefined)
|
||||
let updateLightMap: ReturnType<typeof useLightMap>
|
||||
const lightMaskEl = useTemplateRef<HTMLCanvasElement>('light-mask')
|
||||
let updateLightMap = (() => {}) as ReturnType<typeof useLightMask>
|
||||
|
||||
pocket({ name: 'Shovel', type: 'tool', icon: 'shovel', quality: 'wood' })
|
||||
pocket({ name: 'Sword', type: 'weapon', icon: 'sword', quality: 'wood' })
|
||||
pocket({ name: 'Pick Axe', type: 'tool', icon: 'pick', quality: 'wood' })
|
||||
pocket(getItem('tool_shovel_wood'))
|
||||
pocket(getItem('tool_sword_wood'))
|
||||
pocket(getItem('tool_pickaxe_wood'))
|
||||
pocket(getItem('fixture_torch'), 5)
|
||||
|
||||
let animationFrame = 0
|
||||
let lastTick = 0
|
||||
|
||||
const debug = ref(true)
|
||||
const x = ref(0)
|
||||
const y = ref(0)
|
||||
const floorX = computed(() => Math.floor(x.value))
|
||||
const floorY = computed(() => Math.floor(y.value))
|
||||
const tx = computed(() => (x.value - floorX.value) * -BLOCK_SIZE)
|
||||
const ty = computed(() => (y.value - floorY.value) * -BLOCK_SIZE)
|
||||
const rows = computed(() => level.grid(floorX.value, floorY.value))
|
||||
const lightBarrier = computed(() => level.sunLight(floorX.value))
|
||||
const fracX = computed(() => x.value - floorX.value)
|
||||
const fracY = computed(() => y.value - floorY.value)
|
||||
const tx = computed(() => fracX.value * -BLOCK_SIZE)
|
||||
const ty = computed(() => fracY.value * -BLOCK_SIZE)
|
||||
const px = computed(() => Math.round(player.x + fracX.value))
|
||||
const py = computed(() => Math.round(player.y + fracY.value))
|
||||
|
||||
const lightBarrier = computed<number[]>(() => {
|
||||
const _update = mapUpdateCount.value // reactivity trigger
|
||||
return level.sunLight(floorX.value)
|
||||
})
|
||||
|
||||
const mapUpdateCount = ref(0)
|
||||
const mapGrid = computed<Block[][]>(() => {
|
||||
const _update = mapUpdateCount.value // reactivity trigger
|
||||
return level.grid(floorX.value, floorY.value, true)
|
||||
})
|
||||
const lightSources = computed(() => {
|
||||
const _update = mapUpdateCount.value // reactivity trigger
|
||||
const _floorX = floorX.value // reactivity trigger
|
||||
const _floorY = floorY.value // reactivity trigger
|
||||
|
||||
const lightSources: LightSource[] = []
|
||||
const grid = mapGrid.value
|
||||
|
||||
for (let y = 0; y < grid.length; y++) {
|
||||
const row = grid[y]
|
||||
for (let x = 0; x < row.length; x++) {
|
||||
const block = row[x]
|
||||
if (block.illumination) {
|
||||
lightSources.push({
|
||||
x, y,
|
||||
strength: block.illumination,
|
||||
color: block.color ?? '#FFE'
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return lightSources
|
||||
})
|
||||
|
||||
const arriving = ref(true)
|
||||
const walking = ref(false)
|
||||
const inventorySelection = ref<InventoryItem>(player.inventory[0])
|
||||
|
||||
type Surroundings = {
|
||||
at: Block,
|
||||
left: Block,
|
||||
right: Block,
|
||||
up: Block,
|
||||
down: Block,
|
||||
}
|
||||
const surroundings = computed<Surroundings>(() => {
|
||||
const px = player.x
|
||||
const py = player.y
|
||||
const row = rows.value
|
||||
const getSurroundings = (x: number, y: number) => {
|
||||
const rows = mapGrid.value
|
||||
|
||||
const rowY = rows[y]
|
||||
const rowYp = rows[y - 1]
|
||||
const rowYn = rows[y + 1]
|
||||
|
||||
return {
|
||||
at: row[py][px],
|
||||
left: row[py][px - 1],
|
||||
right: row[py][px + 1],
|
||||
up: row[py - 1][px],
|
||||
down: row[py + 1][px],
|
||||
at: rowY[x],
|
||||
left: rowY[x - 1],
|
||||
right: rowY[x + 1],
|
||||
up: rowYp[x],
|
||||
down: rowYn[x],
|
||||
}
|
||||
}
|
||||
|
||||
const surroundings = computed<Record<Direction, Block>>(() => {
|
||||
const _update = mapUpdateCount.value // reactivity trigger
|
||||
return getSurroundings(px.value, py.value)
|
||||
})
|
||||
const blocked = computed(() => {
|
||||
const { left, right, up, down } = surroundings.value
|
||||
const fx = fracX.value
|
||||
const fy = fracY.value
|
||||
return {
|
||||
left: !left.walkable,
|
||||
right: !right.walkable,
|
||||
left: !left.walkable && fx < 0.8 && fx > 0.7,
|
||||
right: !right.walkable && fx > 0.2 && fx < 0.3,
|
||||
up: !up.walkable,
|
||||
down: !down.walkable,
|
||||
down: !down.walkable && fy > 0.8,
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -79,11 +121,12 @@ function dig(blockX: number, blockY: number, block: Block) {
|
|||
// finally dig that block
|
||||
// TODO: damage blocks first
|
||||
level.change({
|
||||
type: 'exchange',
|
||||
change: 'exchange',
|
||||
x: floorX.value + blockX,
|
||||
y: floorY.value + blockY,
|
||||
newType: 'air'
|
||||
})
|
||||
mapUpdateCount.value = mapUpdateCount.value + 1
|
||||
|
||||
// anything to pick up?
|
||||
if (block.drops) {
|
||||
|
@ -93,42 +136,66 @@ function dig(blockX: number, blockY: number, block: Block) {
|
|||
}
|
||||
|
||||
function build(blockX: number, blockY: number, block: InventoryItem) {
|
||||
const blockToBuild = block.builds
|
||||
// the block doesn't do anything
|
||||
if (!blockToBuild) return
|
||||
let blockToBuild = block.builds
|
||||
if (!blockToBuild) return // the block doesn't do anything?!
|
||||
|
||||
// While blocks are just filling the space completely, fixtures are attached
|
||||
// to the closest surface. We check the surroundings, starting at with left
|
||||
// and right, then bottom and top.
|
||||
if (block.type === 'fixture') {
|
||||
const { left, right, up, down } = getSurroundings(blockX, blockY)
|
||||
|
||||
if (!left.transparent) blockToBuild = `${blockToBuild}Left`
|
||||
else if (!right.transparent) blockToBuild = `${blockToBuild}Right`
|
||||
else if (!up.transparent) blockToBuild = `${blockToBuild}Ceiling`
|
||||
else if (!down.transparent) blockToBuild = `${blockToBuild}Floor`
|
||||
}
|
||||
|
||||
level.change({
|
||||
type: 'exchange',
|
||||
change: 'exchange',
|
||||
x: floorX.value + blockX,
|
||||
y: floorY.value + blockY,
|
||||
newType: blockToBuild
|
||||
newType: blockToBuild as BlockType
|
||||
})
|
||||
mapUpdateCount.value = mapUpdateCount.value + 1
|
||||
|
||||
const newAmount = unpocket(block)
|
||||
const newAmount = unpocket(block as Item)
|
||||
if (newAmount < 1) inventorySelection.value = player.inventory[0]
|
||||
}
|
||||
|
||||
function interactWith(blockX: number, blockY: number, block: Block) {
|
||||
if (debug) {
|
||||
console.debug(
|
||||
`interact with ${block.type} at ${blockX},${blockY},`,
|
||||
`with a ${inventorySelection.value.id} in hand`
|
||||
)
|
||||
}
|
||||
// § 4 ArbZG
|
||||
if (paused.value) return
|
||||
|
||||
const blockInHand = inventorySelection.value.type === 'block'
|
||||
const toolInHand = inventorySelection.value.type === 'tool'
|
||||
const emptyBlock = block.type === 'air' || block.type === 'cave'
|
||||
// no spooky interaction at a distance
|
||||
const distanceX = ~~(px.value - blockX)
|
||||
const distanceY = ~~(py.value - blockY)
|
||||
if (distanceX > 1 || distanceY > 1) return
|
||||
|
||||
const blockInHand = inventorySelection.value
|
||||
const shovelInHand = blockInHand.id.indexOf('shovel') >= 0
|
||||
const pickaxeInHand = blockInHand.id.indexOf('pickaxe') >= 0
|
||||
|
||||
const canBuild = !!blockInHand.builds
|
||||
const hasTool = blockInHand.type === 'tool'
|
||||
const hasSpace = block.type === 'air' || block.type === 'cave'
|
||||
const canUseShovel = softTerrain.indexOf(block.type) >= 0
|
||||
const canUsePickaxe = hardTerrain.indexOf(block.type) >= 0
|
||||
|
||||
// put the selected block
|
||||
if (blockInHand && emptyBlock) {
|
||||
build(blockX, blockY, inventorySelection.value)
|
||||
if (canBuild && hasSpace) {
|
||||
build(blockX, blockY, blockInHand)
|
||||
// dig a block with shovel or pick axe
|
||||
} else if (toolInHand && !emptyBlock) {
|
||||
dig(blockX, blockY, block)
|
||||
} else if (hasTool && !hasSpace) {
|
||||
if (shovelInHand && canUseShovel) dig(blockX, blockY, block)
|
||||
else if (pickaxeInHand && canUsePickaxe) dig(blockX, blockY, block)
|
||||
}
|
||||
|
||||
// This feels like cheating, but it makes Vue recalculate floorX
|
||||
// which then recalculates the blocks, so that the changes are
|
||||
// applied. Otherwise, they wouldn't be visible before moving
|
||||
x.value = x.value + 0.01
|
||||
x.value = x.value - 0.01
|
||||
}
|
||||
|
||||
let lastTimeUpdate = 0
|
||||
|
@ -196,12 +263,18 @@ function selectTool(item: InventoryItem) {
|
|||
}
|
||||
|
||||
onMounted(() => {
|
||||
const canvas = lightMapEl.value!
|
||||
canvas.height = (BLOCK_SIZE + 2) * STAGE_HEIGHT
|
||||
canvas.width = (BLOCK_SIZE + 2) * STAGE_WIDTH
|
||||
const ctx = canvas.getContext('2d')!
|
||||
|
||||
updateLightMap = useLightMap(ctx, floorX, floorY, tx, ty, time, lightBarrier)
|
||||
if (lightMaskEl.value) {
|
||||
const canvas = lightMaskEl.value
|
||||
canvas.height = (BLOCK_SIZE + 2) * STAGE_HEIGHT
|
||||
canvas.width = (BLOCK_SIZE + 2) * STAGE_WIDTH
|
||||
const ctx = canvas.getContext('2d')!
|
||||
updateLightMap = useLightMask(
|
||||
ctx, floorY, tx, ty,
|
||||
lightBarrier, lightSources,
|
||||
)
|
||||
} else {
|
||||
console.warn('lightmap deactivated')
|
||||
}
|
||||
lastTick = performance.now()
|
||||
move(lastTick)
|
||||
})
|
||||
|
@ -209,19 +282,27 @@ onMounted(() => {
|
|||
|
||||
<template>
|
||||
<div id="field" :class="timeOfDay">
|
||||
<Background :time :x />
|
||||
<div id="parallax">
|
||||
</div>
|
||||
|
||||
<div id="blocks" :style="{transform: `translate(${tx}px, ${ty}px)`}">
|
||||
<template v-for="(row, y) in rows">
|
||||
<template v-for="(row, y) in mapGrid">
|
||||
<div v-for="(block, x) in row"
|
||||
:class="['block', block.type, {
|
||||
highlight: x === player.x && y == player.y
|
||||
}]"
|
||||
:class="['block', block.type, { highlight: debug && x === px && y === py }]"
|
||||
@click="interactWith(x, y, block)"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div id="player" :class="[direction, { walking }]" @click="inventory = !inventory">
|
||||
<div id="player"
|
||||
:class="[direction, {
|
||||
walking,
|
||||
running: walking && running,
|
||||
sitting: paused
|
||||
}]"
|
||||
@click="inventory = !inventory"
|
||||
>
|
||||
<div class="head"></div>
|
||||
<div class="body"></div>
|
||||
<div class="legs">
|
||||
|
@ -235,11 +316,19 @@ onMounted(() => {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<canvas id="light-mask" ref="lightMapEl" :style="{transform: `translate(${tx}px, ${ty}px)`}" />
|
||||
<canvas id="light-mask" ref="light-mask"
|
||||
:style="{ transform: `translate(${tx}px, ${ty}px)` }"
|
||||
/>
|
||||
<div id="beam" v-if="arriving"></div>
|
||||
<div id="level-indicator">
|
||||
x:{{ floorX }}, y:{{ floorY }}
|
||||
<template v-if="paused">(PAUSED)</template>
|
||||
<template v-if="paused">
|
||||
<template v-if="debug">
|
||||
({{ clock }})<br/>
|
||||
time: <input type="number" max="0" min="1000" v-model="time" />
|
||||
</template>
|
||||
<template v-else>(PAUSED)</template>
|
||||
</template>
|
||||
<template v-else>({{ clock }})</template>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
import { ref, computed, onMounted, watch } from 'vue'
|
||||
import useBackground from './util/useBackground'
|
||||
import { BLOCK_SIZE, STAGE_WIDTH, STAGE_HEIGHT } from './level/def'
|
||||
import { calcSunAngle } from './util/useTime'
|
||||
|
||||
export interface Props {
|
||||
time: number
|
||||
|
@ -11,8 +12,7 @@ export interface Props {
|
|||
const props = defineProps<Props>()
|
||||
const canvas = ref<HTMLCanvasElement | null>(null)
|
||||
|
||||
const p = Math.PI / -10
|
||||
const sunY = computed(() => Math.sin(props.time * p))
|
||||
const sunY = computed(() => calcSunAngle(props.time))
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
|
@ -21,8 +21,8 @@ onMounted(() => {
|
|||
|
||||
const drawBackground = useBackground(
|
||||
canvasEl,
|
||||
~~(STAGE_WIDTH * BLOCK_SIZE / 2.0),
|
||||
~~(STAGE_HEIGHT * BLOCK_SIZE / 2.0),
|
||||
~~(STAGE_WIDTH * BLOCK_SIZE / 1.0),
|
||||
~~(STAGE_HEIGHT * BLOCK_SIZE / 1.0),
|
||||
)
|
||||
|
||||
watch(props, () => drawBackground(props.x, sunY.value), { immediate: true })
|
||||
|
@ -31,5 +31,5 @@ onMounted(() => {
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<canvas ref="canvas" id="background"></canvas>
|
||||
<canvas ref="canvas" id="background" />
|
||||
</template>
|
||||
|
|
|
@ -98,27 +98,35 @@
|
|||
.block.brickWall {
|
||||
background-image: url(/Tiles/brick_grey.png);
|
||||
}
|
||||
.block.torchLeft { background-image: url("/Items/torchLeft.png"); }
|
||||
.block.torchRight { background-image: url("/Items/torchRight.png"); }
|
||||
.block.torchFloor { background-image: url("/Items/torchFloor.png"); }
|
||||
.block.torchCeiling { background-image: url("/Items/torchCeiling.png"); }
|
||||
|
||||
#field {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
#field .block:hover,
|
||||
#field .block.block.highlight {
|
||||
filter: brightness(1.2) grayscale(1.0);
|
||||
filter: brightness(1.2) saturate(1.2);
|
||||
outline: 1px solid white;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.morning0 .block,
|
||||
.morning0 #player {
|
||||
filter: saturate(50%);
|
||||
filter: saturate(50%) brightness(0.6);
|
||||
}
|
||||
|
||||
.morning1 .block,
|
||||
.morning1 #player {
|
||||
filter: saturate(100%);
|
||||
filter: saturate(100%) brightness(0.8);
|
||||
}
|
||||
|
||||
.morning2 .block,
|
||||
.morning2 #player {
|
||||
filter: saturate(120%);
|
||||
filter: saturate(120%) brightness(0.9);
|
||||
}
|
||||
|
||||
.evening0 .block,
|
||||
|
@ -128,17 +136,17 @@
|
|||
|
||||
.evening1 .block,
|
||||
.evening1 #player {
|
||||
filter: saturate(70%);
|
||||
filter: saturate(70%) brightness(0.8);
|
||||
}
|
||||
|
||||
.evening2 .block,
|
||||
.evening2 #player {
|
||||
filter: saturate(50%);
|
||||
filter: saturate(50%) brightness(0.6);
|
||||
}
|
||||
|
||||
.night .block,
|
||||
.night #player {
|
||||
filter: saturate(30%);
|
||||
filter: saturate(30%) brightness(0.4);
|
||||
}
|
||||
|
||||
#blocks {
|
||||
|
@ -151,12 +159,14 @@
|
|||
flex-flow: row wrap;
|
||||
}
|
||||
|
||||
#light-mask {
|
||||
#parallax, #background, #light-mask {
|
||||
position: absolute;
|
||||
top: calc(var(--block-size) * -1);
|
||||
left: calc(var(--block-size) * -1);
|
||||
width: calc(100% + var(--block-size) * 2);
|
||||
height: calc(100% + var(--block-size) * 2);
|
||||
mix-blend-mode: multiply;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
#light-mask {
|
||||
mix-blend-mode: multiply;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,18 @@
|
|||
.item.tool-shovel-wood { background-image: url("/Items/shovel_bronze.png"); }
|
||||
.item.weapon-sword-wood { background-image: url("/Items/sword_bronze.png"); }
|
||||
.item.tool-sword-wood { background-image: url("/Items/sword_bronze.png"); }
|
||||
.item.tool-pick-wood { background-image: url("/Items/pick_bronze.png"); }
|
||||
|
||||
.item.tool-shovel-iron { background-image: url("/Items/shovel_iron.png"); }
|
||||
.item.tool-sword-iron { background-image: url("/Items/sword_iron.png"); }
|
||||
.item.tool-pick-iron { background-image: url("/Items/pick_iron.png"); }
|
||||
|
||||
.item.tool-shovel-diamond { background-image: url("/Items/shovel_diamond.png"); }
|
||||
.item.tool-sword-diamond { background-image: url("/Items/sword_diamond.png"); }
|
||||
.item.tool-pick-diamond { background-image: url("/Items/pick_diamond.png"); }
|
||||
|
||||
.item.block-wood { background-image: url("/Tiles/wood.png"); }
|
||||
.item.block-dirt { background-image: url("/Tiles/dirt.png"); }
|
||||
.item.block-stone { background-image: url("/Tiles/stone.png"); }
|
||||
.item.block-gravel { background-image: url("/Tiles/gravel_stone.png"); }
|
||||
|
||||
.item.fixture-torch { background-image: url("/Items/torchFloor.png"); }
|
||||
|
|
Before Width: | Height: | Size: 72 KiB |
|
@ -14,7 +14,7 @@
|
|||
--player-height: 76px;
|
||||
position: absolute;
|
||||
left: calc(var(--field-width) / 2);
|
||||
top: calc(var(--field-height) / 2 - 10px);
|
||||
top: calc(var(--field-height) / 2);
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
width: var(--player-width);
|
||||
|
@ -58,6 +58,21 @@
|
|||
#player.walking > .legs > div.left {
|
||||
animation: dangle .3s linear infinite alternate;
|
||||
}
|
||||
#player.running > .legs > div.right {
|
||||
animation: gillop .2s linear infinite alternate;
|
||||
}
|
||||
#player.running > .legs > div.left {
|
||||
animation: gallop .2s linear infinite alternate;
|
||||
}
|
||||
#player.sitting {
|
||||
transform: translateY(10px);
|
||||
}
|
||||
#player.right.sitting {
|
||||
transform: scaleX(-1) translateY(10px);
|
||||
}
|
||||
#player.sitting > .legs > div {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
#player > .arms {
|
||||
position: absolute;
|
||||
width: 8px;
|
||||
|
@ -70,6 +85,9 @@
|
|||
#player.walking > .arms {
|
||||
animation: dangle .3s linear infinite alternate;
|
||||
}
|
||||
#player.running > .arms {
|
||||
animation: gallop .2s linear infinite alternate;
|
||||
}
|
||||
#player > .arms > .item {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
|
@ -89,6 +107,14 @@
|
|||
from { transform: rotate(-20deg); }
|
||||
to { transform: rotate(20deg); }
|
||||
}
|
||||
@keyframes gillop {
|
||||
from { transform: rotate(35deg); }
|
||||
to { transform: rotate(-35deg); }
|
||||
}
|
||||
@keyframes gallop {
|
||||
from { transform: rotate(-35deg); }
|
||||
to { transform: rotate(35deg); }
|
||||
}
|
||||
@keyframes pulse {
|
||||
from { opacity: .3; }
|
||||
to { opacity: 1.0; }
|
||||
|
|
|
@ -1,9 +1,14 @@
|
|||
import type { NoiseFunction2D } from 'simplex-noise'
|
||||
import {blockTypes as T, level as L, probability as P, type Block} from './def'
|
||||
import { blockTypes as T, level as L, probability as P } from './def'
|
||||
import type { Block } from '../types.d'
|
||||
|
||||
const TREE_ROOT = [T.treeRootLeft, T.treeRootMiddle, T.treeRootRight]
|
||||
|
||||
function trees(r: number, i: number, row: Block[], previousRow: Block[]) {
|
||||
function trees(
|
||||
r: number,
|
||||
i: number,
|
||||
row: Block[],
|
||||
previousRow: Block[],
|
||||
nextRow: Block[],
|
||||
) {
|
||||
const max = row.length - 1
|
||||
const h = i - 1
|
||||
const j = i + 1
|
||||
|
@ -11,6 +16,7 @@ function trees(r: number, i: number, row: Block[], previousRow: Block[]) {
|
|||
const above = previousRow[i]
|
||||
const aboveLeft = previousRow[h]
|
||||
const aboveRight = previousRow[j]
|
||||
const below = nextRow[i]
|
||||
|
||||
if (current === T.treeCrown) {
|
||||
if (i > 0) row[h] = T.treeLeaves
|
||||
|
@ -27,8 +33,8 @@ function trees(r: number, i: number, row: Block[], previousRow: Block[]) {
|
|||
if (i < max) row[j] = T.treeLeaves
|
||||
|
||||
} else if (above === T.treeTrunk) {
|
||||
if (current === T.air) row[i] = T.treeTrunk
|
||||
else row[i] = T.treeRoot
|
||||
if (below === T.soil || below === T.grass) row[i] = T.treeRoot
|
||||
else row[i] = T.treeTrunk
|
||||
} else if (above === T.treeRoot) {
|
||||
row[i] = T.soil
|
||||
if (i > 0) row[h] = T.treeRoot
|
||||
|
@ -44,7 +50,8 @@ function ground(r: number, i: number, current: Block, above: Block) {
|
|||
return current
|
||||
}
|
||||
|
||||
function ground_(r: number, i: number, row: Block[], previousRow: Block[]) {
|
||||
/*
|
||||
function ground(r: number, i: number, row: Block[], previousRow: Block[]) {
|
||||
const rootParts = [T.treeRootLeft, T.treeRootMiddle, T.treeRootRight]
|
||||
const prevBlock = previousRow[i]
|
||||
|
||||
|
@ -55,6 +62,7 @@ function ground_(r: number, i: number, row: Block[], previousRow: Block[]) {
|
|||
if (row[i] === T.soil) row[i] = T.grass
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
function rock(r: number, i: number, current: Block, above: Block) {
|
||||
if (above === T.soil && r < P.fray) return T.soil
|
||||
|
@ -67,9 +75,16 @@ function underground(r: number, i: number, current: Block, above: Block) {
|
|||
}
|
||||
|
||||
export default function createBlockExtender(rand: NoiseFunction2D) {
|
||||
function growTrees(level: number, column: number, offset: number, row: Block[], previousRow: Block[]) {
|
||||
function growTrees(
|
||||
level: number,
|
||||
column: number,
|
||||
offset: number,
|
||||
row: Block[],
|
||||
previousRow: Block[],
|
||||
nextRow: Block[],
|
||||
) {
|
||||
const r = rand(level, column + offset)
|
||||
trees(r, column, row, previousRow)
|
||||
trees(r, column, row, previousRow, nextRow)
|
||||
}
|
||||
|
||||
function extendBlock(level: number, column: number, offset: number, current: Block, above: Block) {
|
||||
|
@ -80,10 +95,16 @@ export default function createBlockExtender(rand: NoiseFunction2D) {
|
|||
return underground(r, column, current, above)
|
||||
}
|
||||
|
||||
function extendRow(level: number, columnOffset: number, row: Block[], previousRow: Block[]) {
|
||||
function extendRow(
|
||||
level: number,
|
||||
columnOffset: number,
|
||||
row: Block[],
|
||||
previousRow: Block[],
|
||||
nextRow: Block[],
|
||||
) {
|
||||
for (let column = 0; column < row.length; column++) {
|
||||
if (level < L.ground) {
|
||||
// growTrees(level, column, columnOffset, row, previousRow)
|
||||
growTrees(level, column, columnOffset, row, previousRow, nextRow)
|
||||
} else {
|
||||
row[column] = extendBlock(level, column, columnOffset, row[column], previousRow[column])
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import type { NoiseFunction2D } from 'simplex-noise'
|
||||
import {blockTypes as T, level as L, probability as P, type Block} from './def'
|
||||
import { blockTypes as T, level as L, probability as P } from './def'
|
||||
import type { Block } from '../types.d'
|
||||
|
||||
export default function createBlockGenerator(rand: NoiseFunction2D) {
|
||||
// randomly generate a block
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import type { DropItem } from './items'
|
||||
import type { Block, BlockType } from '../types.d'
|
||||
|
||||
export const BLOCK_SIZE = 64 // each block is 64̨̣̌̇x64 pixel in size and equals 1m
|
||||
export const BLOCK_SIZE = 64 // each block is 64x64 px in size and equals 1m
|
||||
export const RECIPROCAL = 1 / BLOCK_SIZE
|
||||
|
||||
export const STAGE_WIDTH = 20 // 20*64 = 1280 pixel wide stage
|
||||
|
@ -9,43 +9,37 @@ export const STAGE_HEIGHT = 12 // 12*64 = 768 pixel high stage
|
|||
|
||||
export const GRAVITY = 10 // blocks per second
|
||||
|
||||
export type Block = {
|
||||
type: string, // what is it?
|
||||
hp: number, // how long do I need to hit it?
|
||||
walkable: boolean, // can I walk through it?
|
||||
climbable?: boolean, // can I climb it?
|
||||
transparent?: boolean, // can I see through it?
|
||||
illuminated?: boolean, // is it glowing?
|
||||
drops?: DropItem, // what do I get, when loot it?
|
||||
}
|
||||
|
||||
export type BlockType =
|
||||
| 'air' | 'grass'
|
||||
| 'treeCrown' | 'treeLeaves' | 'treeTrunk' | 'treeRoot'
|
||||
| 'soil' | 'soilGravel' | 'stone' | 'stoneGravel'
|
||||
| 'bedrock' | 'cave'
|
||||
| 'brickWall'
|
||||
|
||||
export const blockTypes: Record<BlockType, Block> = {
|
||||
// Transparent Blocks
|
||||
air: { type: 'air', hp: Infinity, walkable: true, transparent: true },
|
||||
cave: { type: 'cave', hp: Infinity, walkable: true, transparent: true },
|
||||
// Tree Parts
|
||||
treeCrown: { type: 'treeCrown', hp: 1, walkable: true, transparent: true, drops: 'leaves' },
|
||||
treeLeaves: { type: 'treeLeaves', hp: 1, walkable: true, transparent: true, drops: 'leaves' },
|
||||
treeTrunk: { type: 'treeTrunk', hp: 10, walkable: true, climbable: true, transparent: true, drops: 'wood' },
|
||||
treeRoot: { type: 'treeRoot', hp: 10, walkable: true, climbable: true, drops: 'wood' },
|
||||
treeCrown: { type: 'treeCrown', hp: 1, walkable: true, transparent: true, drops: 'block_leaves' },
|
||||
treeLeaves: { type: 'treeLeaves', hp: 1, walkable: true, transparent: true, drops: 'block_leaves' },
|
||||
treeTrunk: { type: 'treeTrunk', hp: 10, walkable: true, climbable: true, transparent: true, drops: 'block_wood' },
|
||||
treeRoot: { type: 'treeRoot', hp: 10, walkable: true, climbable: true, drops: 'block_wood' },
|
||||
// Opaque Natural Blocks
|
||||
grass: { type: 'grass', hp: 5, walkable: false, drops: 'dirt' },
|
||||
soil: { type: 'soil', hp: 5, walkable: false, drops: 'dirt' },
|
||||
soilGravel: { type: 'soilGravel', hp: 5, walkable: false, drops: 'gravel' },
|
||||
stoneGravel: { type: 'stoneGravel', hp: 10, walkable: false, drops: 'gravel' },
|
||||
stone: { type: 'stone', hp: 10, walkable: false, drops: 'stone' },
|
||||
bedrock: { type: 'bedrock', hp: 25, walkable: false, drops: 'stone' },
|
||||
grass: { type: 'grass', hp: 5, walkable: false, drops: 'block_dirt' },
|
||||
soil: { type: 'soil', hp: 5, walkable: false, drops: 'block_dirt' },
|
||||
soilGravel: { type: 'soilGravel', hp: 5, walkable: false, drops: 'block_gravel' },
|
||||
stoneGravel: { type: 'stoneGravel', hp: 10, walkable: false, drops: 'block_gravel' },
|
||||
stone: { type: 'stone', hp: 10, walkable: false, drops: 'block_stone' },
|
||||
bedrock: { type: 'bedrock', hp: 25, walkable: false, drops: 'block_stone' },
|
||||
// Built Blocks
|
||||
brickWall: { type: 'brickWall', hp: 25, walkable: false, drops: 'stone' },
|
||||
brickWall: { type: 'brickWall', hp: 25, walkable: false, drops: 'block_gravel' },
|
||||
|
||||
torchLeft: { type: 'torchLeft', hp: 1, walkable: true, transparent: true, drops: 'fixture_torch', illumination: 1.0, color: '#FFE', fixture: true },
|
||||
torchRight: { type: 'torchRight', hp: 1, walkable: true, transparent: true, drops: 'fixture_torch', illumination: 1.0, color: '#FFE', fixture: true },
|
||||
torchCeiling: { type: 'torchCeiling', hp: 1, walkable: true, transparent: true, drops: 'fixture_torch', illumination: 1.0, color: '#FFE', fixture: true },
|
||||
torchFloor: { type: 'torchFloor', hp: 1, walkable: true, transparent: true, drops: 'fixture_torch', illumination: 1.0, color: '#FEB', fixture: true },
|
||||
}
|
||||
|
||||
export const softTerrain: BlockType[] = [
|
||||
'grass', 'soil', 'soilGravel',
|
||||
'torchLeft', 'torchRight', 'torchCeiling', 'torchFloor',
|
||||
]
|
||||
export const hardTerrain: BlockType[] = ['stone', 'stoneGravel', 'bedrock', 'brickWall']
|
||||
|
||||
export const level = {
|
||||
treeTop: 9,
|
||||
ground: 14,
|
||||
|
@ -55,7 +49,7 @@ export const level = {
|
|||
}
|
||||
|
||||
export const probability = {
|
||||
tree: 0.3,
|
||||
tree: 0.2,
|
||||
soilHole: 0.3,
|
||||
soilGravel: 0.2,
|
||||
stoneGravel: 0.1,
|
||||
|
|
|
@ -3,24 +3,10 @@ import { createNoise2D, type NoiseFunction2D } from 'simplex-noise'
|
|||
import createBlockGenerator from './blockGen'
|
||||
import createBlockExtender from './blockExt'
|
||||
|
||||
import {blockTypes, blockTypes as T, level as L, type Block, type BlockType} from './def'
|
||||
import { level as L, blockTypes, blockTypes as T } from './def'
|
||||
import type { Block, Change } from '../types.d'
|
||||
|
||||
// describes a changed block, eg digged or placed by the player
|
||||
type DamagedBlock = {
|
||||
type: 'damage'
|
||||
x: number
|
||||
y: number
|
||||
damage: number
|
||||
}
|
||||
type ChangedBlock = {
|
||||
type: 'exchange'
|
||||
x: number
|
||||
y: number
|
||||
newType: BlockType
|
||||
}
|
||||
type Change = DamagedBlock | ChangedBlock
|
||||
|
||||
const MAX_LIGHT = 100 // maximum level where light shines
|
||||
const MAX_LIGHT = L.underground // maximum level where light shines
|
||||
|
||||
export default function createLevel(width: number, height: number, seed = 'extremely random seed') {
|
||||
const prng = alea(seed)
|
||||
|
@ -50,7 +36,7 @@ export default function createLevel(width: number, height: number, seed = 'extre
|
|||
if (changes) {
|
||||
const maxLevel = levelOffset + height
|
||||
changes.forEach(c => {
|
||||
if (c.type !== 'exchange' || c.y < levelOffset || c.y >= maxLevel) return
|
||||
if (c.change !== 'exchange' || c.y < levelOffset || c.y >= maxLevel) return
|
||||
_grid[c.y - levelOffset][c.x - columnOffset] = blockTypes[c.newType]
|
||||
})
|
||||
}
|
||||
|
@ -60,7 +46,7 @@ export default function createLevel(width: number, height: number, seed = 'extre
|
|||
// takes the current columnOffset and generates all blocks from the very top
|
||||
// until a block is generated that blocks light. The height of that block is
|
||||
// stored in the lightBarrier list
|
||||
function calcLightBarrier(columnOffset: number) {
|
||||
function calcLightBarrier(columnOffset: number): void {
|
||||
let previousBlock: Block = T.air
|
||||
|
||||
for (let col = 0; col < width; col++) {
|
||||
|
@ -71,7 +57,7 @@ export default function createLevel(width: number, height: number, seed = 'extre
|
|||
const changes = _changes[columnOffset + col]
|
||||
if (changes) {
|
||||
const change = changes.find(c => c.y === level)
|
||||
if (change && change.type === 'exchange') block = blockTypes[change.newType]
|
||||
if (change && change.change === 'exchange') block = blockTypes[change.newType]
|
||||
}
|
||||
|
||||
previousBlock = block
|
||||
|
@ -84,27 +70,36 @@ export default function createLevel(width: number, height: number, seed = 'extre
|
|||
}
|
||||
}
|
||||
|
||||
function generate(columnOffset: number, levelOffset: number) {
|
||||
function generate(columnOffset: number, levelOffset: number): void {
|
||||
for (let i = 0; i < height; i++) {
|
||||
const level = levelOffset + i
|
||||
const row: Block[] = Array(width)
|
||||
const previousRow = i ? _grid[i-1] : [] as Block[]
|
||||
const nextRow = _grid[i+1] ?? [] as Block[]
|
||||
|
||||
blockGen.fillRow(level, columnOffset, row)
|
||||
blockExt.extendRow(level, columnOffset, row, previousRow)
|
||||
blockExt.extendRow(level, columnOffset, row, previousRow, nextRow)
|
||||
|
||||
_grid[i] = row
|
||||
}
|
||||
applyPlayerChanges(columnOffset, levelOffset)
|
||||
}
|
||||
|
||||
function sunLight(columnOffset: number) {
|
||||
function sunLight(columnOffset: number): number[] {
|
||||
calcLightBarrier(columnOffset)
|
||||
return _lightBarrier
|
||||
}
|
||||
|
||||
function grid(x: number, y: number) {
|
||||
generate(x, y)
|
||||
let lastGenX = 0
|
||||
let lastGenY = 0
|
||||
generate(0, 0)
|
||||
|
||||
function grid(x: number, y: number, force = false): Block[][] {
|
||||
if (force || lastGenX !== x || lastGenY !== y) {
|
||||
generate(x, y)
|
||||
lastGenX = x
|
||||
lastGenY = y
|
||||
}
|
||||
return _grid
|
||||
}
|
||||
|
||||
|
|
|
@ -1,58 +1,45 @@
|
|||
import type { BlockType } from './def'
|
||||
import type { InventoryItem } from '../util/usePlayer'
|
||||
import type { ItemQuality, Item, InventoryItem } from '../types.d'
|
||||
|
||||
export type ItemQuality = 'wood' | 'iron' | 'silver' | 'gold' | 'diamond'
|
||||
export type ItemType = 'tool' | 'weapon' | 'block' | 'ore'
|
||||
export const items = {
|
||||
tool_shovel_wood: { id: 'tool_shovel_wood', name: 'Wooden Shovel', type: 'tool', icon: 'shovel', quality: 'wood' } as Item,
|
||||
tool_pickaxe_wood: { id: 'tool_pickaxe_wood', name: 'Wooden Pick Axe', type: 'tool', icon: 'pick', quality: 'wood' } as Item,
|
||||
tool_sword_wood: { id: 'tool_sword_wood', name: 'Wooden Sword', type: 'tool', icon: 'sword', quality: 'wood' } as Item,
|
||||
|
||||
export type DropItem =
|
||||
| 'Shovel' | 'Pick Axe' | 'Sword'
|
||||
| 'leaves' | 'dirt' | 'wood' | 'stone' | 'gravel'
|
||||
| 'coal' | 'iron' | 'silver' | 'gold' | 'ruby' | 'diamond' | 'emerald'
|
||||
tool_shovel_iron: { id: 'tool_shovel_iron', name: 'Iron Shovel', type: 'tool', icon: 'shovel', quality: 'iron' } as Item,
|
||||
tool_pickaxe_iron: { id: 'tool_pickaxe_iron', name: 'Iron Pick Axe', type: 'tool', icon: 'pick', quality: 'iron' } as Item,
|
||||
tool_sword_iron: { id: 'tool_sword_iron', name: 'Iron Sword', type: 'tool', icon: 'sword', quality: 'iron' } as Item,
|
||||
|
||||
export interface Item {
|
||||
name: DropItem
|
||||
type: ItemType
|
||||
icon: string
|
||||
hasQuality?: boolean
|
||||
builds?: BlockType
|
||||
}
|
||||
tool_shovel_diamond: { id: 'tool_shovel_diamond', name: 'Diamond Shovel', type: 'tool', icon: 'shovel', quality: 'diamond' } as Item,
|
||||
tool_pickaxe_diamond: { id: 'tool_pickaxe_diamond', name: 'Diamond Pick Axe', type: 'tool', icon: 'pick', quality: 'diamond' } as Item,
|
||||
tool_sword_diamond: { id: 'tool_sword_diamond', name: 'Diamond Sword', type: 'tool', icon: 'sword', quality: 'diamond' } as Item,
|
||||
|
||||
export const items: Item[] = [
|
||||
{ name: 'Shovel', type: 'tool', icon: 'shovel', hasQuality: true },
|
||||
{ name: 'Pick Axe', type: 'tool', icon: 'pick', hasQuality: true },
|
||||
{ name: 'Sword', type: 'weapon', icon: 'sword', hasQuality: true },
|
||||
block_leaves: { id: 'block_leaves', name: 'leaves', type: 'block', icon: 'leaves', builds: 'treeLeaves' } as Item,
|
||||
block_dirt: { id: 'block_dirt', name: 'dirt', type: 'block', icon: 'dirt', builds: 'soil' } as Item,
|
||||
block_wood: { id: 'block_wood', name: 'wood', type: 'block', icon: 'wood', builds: 'treeTrunk' } as Item,
|
||||
block_stone: { id: 'block_stone', name: 'stone', type: 'block', icon: 'stone', builds: 'brickWall' } as Item,
|
||||
block_gravel: { id: 'block_gravel', name: 'gravel', type: 'block', icon: 'gravel' /*, builds??? TODO */ } as Item,
|
||||
|
||||
{ name: 'leaves', type: 'block', icon: 'leaves', builds: 'treeLeaves' },
|
||||
{ name: 'dirt', type: 'block', icon: 'dirt', builds: 'soil' },
|
||||
{ name: 'wood', type: 'block', icon: 'wood', builds: 'treeTrunk' },
|
||||
{ name: 'stone', type: 'block', icon: 'stone', builds: 'brickWall' },
|
||||
{ name: 'gravel', type: 'block', icon: 'stone' }, // TODO
|
||||
ore_coal: { id: 'ore_coal', name: 'coal', type: 'ore', icon: 'ore_coal' } as Item,
|
||||
ore_iron: { id: 'ore_iron', name: 'iron', type: 'ore', icon: 'ore_iron' } as Item,
|
||||
ore_silver: { id: 'ore_silver', name: 'silver', type: 'ore', icon: 'ore_silver' } as Item,
|
||||
ore_gold: { id: 'ore_gold', name: 'gold', type: 'ore', icon: 'ore_gold' } as Item,
|
||||
ore_ruby: { id: 'ore_ruby', name: 'ruby', type: 'ore', icon: 'ore_ruby' } as Item,
|
||||
ore_diamond: { id: 'ore_diamond', name: 'diamond', type: 'ore', icon: 'ore_diamond' } as Item,
|
||||
ore_emerald: { id: 'ore_emerald', name: 'emerald', type: 'ore', icon: 'ore_emerald' } as Item,
|
||||
|
||||
{ name: 'coal', type: 'ore', icon: 'ore_coal' },
|
||||
{ name: 'iron', type: 'ore', icon: 'ore_iron' },
|
||||
{ name: 'silver', type: 'ore', icon: 'ore_silver' },
|
||||
{ name: 'gold', type: 'ore', icon: 'ore_gold' },
|
||||
{ name: 'ruby', type: 'ore', icon: 'ore_ruby' },
|
||||
{ name: 'diamond', type: 'ore', icon: 'ore_diamond' },
|
||||
{ name: 'emerald', type: 'ore', icon: 'ore_emerald' },
|
||||
]
|
||||
fixture_torch: { id: 'fixture_torch', name: 'Torch', type: 'fixture', icon: 'torch', builds: 'torch' } as Item,
|
||||
} as const
|
||||
|
||||
export type ItemId = keyof typeof items
|
||||
|
||||
export const damage: Record<ItemQuality, number> = {
|
||||
wood: 1,
|
||||
iron: 2,
|
||||
silver: 3,
|
||||
gold: 5,
|
||||
diamond: 8,
|
||||
}
|
||||
iron: 3,
|
||||
diamond: 5,
|
||||
} as const
|
||||
|
||||
export function getItem(name: string, quality = null) {
|
||||
const item = items.find(i => i.name === name)
|
||||
if (item) {
|
||||
return {
|
||||
...item,
|
||||
quality,
|
||||
}
|
||||
}
|
||||
export function getItem(id: ItemId) {
|
||||
return items[id]
|
||||
}
|
||||
|
||||
export function getItemClass(item: InventoryItem) {
|
||||
|
|
|
@ -1,80 +0,0 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, onMounted, watch, computed } from 'vue'
|
||||
import { BLOCK_SIZE, STAGE_WIDTH, STAGE_HEIGHT } from './level/def'
|
||||
|
||||
export interface Props {
|
||||
x: number,
|
||||
y: number,
|
||||
tx: number,
|
||||
ty: number,
|
||||
lightBarrier: number[],
|
||||
time: number,
|
||||
}
|
||||
const props = defineProps<Props>()
|
||||
|
||||
// TODO: use OffscreenCanvas and a WebWorker?
|
||||
const lightMapEl = ref<HTMLCanvasElement | undefined>(undefined)
|
||||
const W = ((STAGE_WIDTH + 2) * BLOCK_SIZE) / 2
|
||||
const H = ((STAGE_HEIGHT + 2) * BLOCK_SIZE) / 2
|
||||
|
||||
const playerX = (W - BLOCK_SIZE) / 4
|
||||
const playerY = H / 4 - BLOCK_SIZE / 2
|
||||
const playerLightSize = BLOCK_SIZE / 3
|
||||
|
||||
function drawPlayerLight(ctx: CanvasRenderingContext2D) {
|
||||
|
||||
const playerLight = ctx.createRadialGradient(
|
||||
playerX - props.tx / 4, playerY - props.ty / 4, 0,
|
||||
playerX - props.tx / 4, playerY - props.ty / 4, playerLightSize
|
||||
)
|
||||
|
||||
// Add three color stops
|
||||
playerLight.addColorStop(0.0, "#FFFF");
|
||||
playerLight.addColorStop(1, "#F0F0");
|
||||
|
||||
// Set the fill style and draw a rectangle
|
||||
ctx.fillStyle = playerLight;
|
||||
ctx.fillRect(0, 0, W, H)
|
||||
}
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
const canvas = lightMapEl.value
|
||||
const ctx = canvas?.getContext('2d')
|
||||
if (!ctx) return
|
||||
|
||||
watch(props, () => {
|
||||
const t = props.time
|
||||
|
||||
if (t > 900 || t < 100) {
|
||||
ctx.fillStyle = `hsl(0, 0%, 20%)`
|
||||
} else if (t < 250) {
|
||||
const s = Math.round((t - 100) / 1.5) // 0-100%
|
||||
const l = Math.round((t - 100) / 1.875) + 20 // 20-100%
|
||||
ctx.fillStyle = `hsl(0, ${s}%, ${l}%)`
|
||||
// } else if (t < 700) {
|
||||
// ctx.fillStyle = `hsl(0, ${}%, ${}%)`
|
||||
} else {
|
||||
ctx.fillStyle = `hsl(0, 0%, 100%)`
|
||||
}
|
||||
ctx.fillRect(0, 0, W, H)
|
||||
drawPlayerLight(ctx)
|
||||
})
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<canvas ref="lightMapEl" :style="{transform: `translate(${tx}px, ${ty}px)`}" />
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
canvas {
|
||||
position: absolute;
|
||||
top: -64px;
|
||||
left: -64px;
|
||||
width: calc(100% + 128px);
|
||||
height: calc(100% + 128px);
|
||||
mix-blend-mode: multiply;
|
||||
}
|
||||
</style>
|
12
src/main.ts
|
@ -1,7 +1,7 @@
|
|||
import { createApp } from "vue";
|
||||
import "./assets/field.css";
|
||||
import "./assets/player.css";
|
||||
import "./assets/items.css";
|
||||
import App from "./App.vue";
|
||||
import { createApp } from "vue"
|
||||
import "./assets/field.css"
|
||||
import "./assets/player.css"
|
||||
import "./assets/items.css"
|
||||
import App from "./App.vue"
|
||||
|
||||
createApp(App).mount("#app");
|
||||
createApp(App).mount("#app")
|
||||
|
|
32
src/old/App.vue
Normal file
|
@ -0,0 +1,32 @@
|
|||
<template>
|
||||
<div id="building-game">
|
||||
<Field />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import Field from './Field'
|
||||
|
||||
export default {
|
||||
name: 'building-game',
|
||||
components: { Field },
|
||||
data () {
|
||||
return {
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
html,body,#app {
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
justify-content: center;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: black;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
72
src/old/Background.vue
Normal file
|
@ -0,0 +1,72 @@
|
|||
<template>
|
||||
<canvas ref="canvas" id="background"></canvas>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import solarQuartet from './solar-quartet'
|
||||
import { BLOCK_SIZE, STAGE_WIDTH, STAGE_HEIGHT } from './level/def'
|
||||
|
||||
export default {
|
||||
name: 'background',
|
||||
props: {
|
||||
x: Number,
|
||||
time: Number
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
redraw: null
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
// x () { this.refresh() },
|
||||
time () { this.refresh() }
|
||||
},
|
||||
mounted () {
|
||||
const canvas = this.$refs.canvas
|
||||
const godraysCanvas = document.createElement('canvas')
|
||||
canvas.width = STAGE_WIDTH * BLOCK_SIZE
|
||||
canvas.height = STAGE_HEIGHT * BLOCK_SIZE
|
||||
godraysCanvas.width = ~~(canvas.width / 8.0)
|
||||
godraysCanvas.height = ~~(canvas.height / 8.0)
|
||||
this.redraw = solarQuartet.bind(
|
||||
null,
|
||||
canvas, canvas.getContext('2d'), ~~(canvas.width / 2.0), ~~(canvas.height / 2.0),
|
||||
godraysCanvas, godraysCanvas.getContext('2d'), godraysCanvas.width, godraysCanvas.height,
|
||||
)
|
||||
this.refresh()
|
||||
},
|
||||
computed: {
|
||||
/* time value to sun position conversion
|
||||
*
|
||||
* The time value rotates from 0 to 1000
|
||||
* sunY convertes it to values between 0 and -100,
|
||||
* while -100 is high sun position (aka day)
|
||||
* and 0 is low (aka night).
|
||||
* My adaption of Solar Quartet renders a static night sky from -30 upwards
|
||||
* and a static day at -70 or lower
|
||||
*/
|
||||
sunY () {
|
||||
// time is between 0 and 1000
|
||||
const p = Math.PI / 1000
|
||||
return Math.sin(this.time * p) * -100
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
refresh () {
|
||||
// console.time('draw background')
|
||||
this.redraw(this.x, this.sunY)
|
||||
// console.timeEnd('draw background')
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#background {
|
||||
display: block;
|
||||
width: var(--field-width);
|
||||
height: var(--field-height);
|
||||
object-fit: contain;
|
||||
background: black;
|
||||
}
|
||||
</style>
|
35
src/old/Block.vue
Normal file
|
@ -0,0 +1,35 @@
|
|||
<template>
|
||||
<div class="block" :class="type"></div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: 'block',
|
||||
props: {
|
||||
type: String
|
||||
},
|
||||
data () {
|
||||
return {
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.block {
|
||||
flex: 0 0 auto;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
background-color: #6DA956;
|
||||
border: 1px solid transparent;
|
||||
}
|
||||
.block.air { background-color: #33A; }
|
||||
.block.grass { background-color: #33A; height: 28px; border-bottom: 2px solid #0A0; }
|
||||
.block.soil { background-color: #543; }
|
||||
.block.gravel { background-color: #665; }
|
||||
.block.stone { background-color: #555; }
|
||||
.block.bedrock { background-color: #444; }
|
||||
.block:hover {
|
||||
border-color: rgba(255,255,255,0.2);
|
||||
}
|
||||
</style>
|
216
src/old/Field.vue
Normal file
|
@ -0,0 +1,216 @@
|
|||
<template>
|
||||
<div id="field" :class="daytimeClass">
|
||||
<input v-keep-focussed type="text"
|
||||
@keydown.up="inputY = -1"
|
||||
@keydown.down="inputY = 1"
|
||||
@keydown.right="inputX = -1"
|
||||
@keydown.left="inputX = 1"
|
||||
@keyup.up="inputY = inputY === -1 ? 0 : 1"
|
||||
@keyup.down="inputY = inputY === 1 ? 0 : 1"
|
||||
@keyup.right="inputX = inputX === -1 ? 0 : 1"
|
||||
@keyup.left="inputX = inputX === 1 ? 0: -1"
|
||||
@keypress.p="togglePause"
|
||||
@keydown.space="digging = true"
|
||||
@keyup.space="digging = false"
|
||||
/>
|
||||
<mountain-background :x="128 + x / 8" :time="time" />
|
||||
<div id="wrap" :style="{transform: `translate(${tx}px, ${ty}px)`}">
|
||||
<template v-for="(row, y) in rows">
|
||||
<div v-for="(block, x) in row" class="block" :class="[block.type]" />
|
||||
</template>
|
||||
</div>
|
||||
<div id="player" :class="[player.direction]" />
|
||||
<div id="level-indicator">
|
||||
x:{{ floorX }}, y:{{ floorY }}
|
||||
<template v-if="moving !== false">({{clock}})</template>
|
||||
<template v-else>(PAUSED)</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
// import throttle from 'lodash/throttle'
|
||||
import MountainBackground from './Background'
|
||||
import Level from './level'
|
||||
import { Moveable } from './physics'
|
||||
import {
|
||||
BLOCK_SIZE,
|
||||
RECIPROCAL,
|
||||
STAGE_WIDTH,
|
||||
STAGE_HEIGHT,
|
||||
PLAYER_X,
|
||||
PLAYER_Y
|
||||
} from './level/def'
|
||||
|
||||
const level = new Level(STAGE_WIDTH + 2, STAGE_HEIGHT + 2)
|
||||
const player = new Moveable(PLAYER_X, PLAYER_Y)
|
||||
|
||||
export default {
|
||||
name: 'field',
|
||||
components: { MountainBackground },
|
||||
data () {
|
||||
return {
|
||||
player,
|
||||
x: 0,
|
||||
y: 12,
|
||||
inputX: 0,
|
||||
inputY: 0,
|
||||
time: 250,
|
||||
moving: false,
|
||||
lastTick: 0
|
||||
}
|
||||
},
|
||||
mounted () {
|
||||
this.lastTick = performance.now()
|
||||
this.move(this.lastTick)
|
||||
},
|
||||
computed: {
|
||||
rows () { return level.grid(this.floorX, this.floorY) },
|
||||
surroundings () {
|
||||
const px = PLAYER_X
|
||||
const py = PLAYER_Y
|
||||
const at = this.rows[py][px]
|
||||
const left = this.rows[py][px]
|
||||
const right = this.rows[py][px + 1]
|
||||
const up = this.rows[py - 1][px] || at
|
||||
const down = this.rows[py + 1][px]
|
||||
|
||||
return { at, left, right, up, down }
|
||||
},
|
||||
blocked () {
|
||||
const { at, left, right, up, down } = this.surroundings
|
||||
|
||||
return {
|
||||
at: !at.walkable,
|
||||
left: !left.walkable,
|
||||
right: !right.walkable,
|
||||
up: !up.walkable,
|
||||
down: !down.walkable
|
||||
}
|
||||
},
|
||||
floorX () { return Math.floor(this.x) },
|
||||
floorY () { return Math.floor(this.y) },
|
||||
tx () { return (this.x - this.floorX) * -BLOCK_SIZE },
|
||||
ty () { return (this.y - this.floorY) * -BLOCK_SIZE },
|
||||
daytimeClass () {
|
||||
const t = this.time
|
||||
if (t >= 900 || t < 80) return "night"
|
||||
|
||||
if (t >= 80 && t < 120) return "morning0"
|
||||
if (t >= 120 && t < 150) return "morning1"
|
||||
if (t >= 150 && t < 240) return "morning2"
|
||||
|
||||
if (t >= 700 && t < 800) return "evening0"
|
||||
if (t >= 800 && t < 850) return "evening1"
|
||||
if (t >= 850 && t < 900) return "evening2"
|
||||
|
||||
return "day"
|
||||
},
|
||||
clock () {
|
||||
const t = this.time * 86.4 // 1000 ticks to 86400 seconds (per day)
|
||||
const h = ~~(t / 3600.0)
|
||||
const m = ~~((t / 3600.0 - h) * 60.0)
|
||||
return `${(h + 2) % 24}:${m < 10 ? '0' : ''}${m}`
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
move (thisTick) {
|
||||
this.moving = requestAnimationFrame(this.move)
|
||||
|
||||
// keep roughly 20 fps
|
||||
if (thisTick - this.lastTick < 50) return
|
||||
|
||||
// set time of day in ticks
|
||||
this.time = (this.time + 0.1) % 1000
|
||||
|
||||
const player = this.player
|
||||
const x = player.x
|
||||
const y = player.y
|
||||
|
||||
let dx = player.vx * player.dir * RECIPROCAL
|
||||
let dy = player.vy * RECIPROCAL
|
||||
|
||||
// don't walk / fall into blocks
|
||||
if (dx > 0 && this.blocked.right) dx = 0
|
||||
if (dx < 0 && this.blocked.left) dx = 0
|
||||
if (dy > 0 && this.blocked.down) dy = 0
|
||||
if (dy < 0 && this.blocked.up) dy = 0
|
||||
|
||||
// don't walk, work!
|
||||
if (!this.inputY && this.digging) {
|
||||
dx = 0
|
||||
this.dig()
|
||||
}
|
||||
|
||||
this.x += dx
|
||||
this.y += dy
|
||||
this.lastTick = thisTick
|
||||
},
|
||||
dig () {
|
||||
console.log('dig', this.playerDirection, this.surroundings[this.playerDirection])
|
||||
// lets not bother with invincible blocks (like air or cave)
|
||||
if (this.surroundings[this.playerDirection].hp >= Infinity) return
|
||||
|
||||
const px = this.floorX + PLAYER_X
|
||||
const py = this.floorY + PLAYER_Y
|
||||
const block = {...this.surroundings[this.playerDirection]}
|
||||
|
||||
block.hp--
|
||||
level.change(py, px, block)
|
||||
},
|
||||
togglePause () {
|
||||
if (this.moving === false) { // is paused
|
||||
this.move()
|
||||
} else {
|
||||
cancelAnimationFrame(this.moving)
|
||||
this.moving = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style src="./assets/field.css" />
|
||||
<style>
|
||||
:root {
|
||||
--block-size: 32px;
|
||||
--field-width: 1024px;
|
||||
--field-height: 576px;
|
||||
--spare-blocks: 2;
|
||||
}
|
||||
|
||||
#level-indicator {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
color: white;
|
||||
}
|
||||
#player {
|
||||
position: absolute;
|
||||
left: calc(var(--field-width) / 2);
|
||||
top: calc(var(--field-height) / 2);
|
||||
background-image: url(./assets/dwarf_right.png);
|
||||
}
|
||||
#player.right { background-image: url(./assets/dwarf_right.png); }
|
||||
#player.left { background-image: url(./assets/dwarf_left.png); }
|
||||
#player.up { background-image: url(./assets/dwarf_back.png); }
|
||||
#player.down { background-image: url(./assets/dwarf_back.png); }
|
||||
#player, .block {
|
||||
flex: 0 0 auto;
|
||||
width: var(--block-size);
|
||||
height: var(--block-size);
|
||||
background-color: transparent;
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
#wrap {
|
||||
position: absolute;
|
||||
top: calc(var(--block-size) * (var(--spare-blocks) / -2));
|
||||
left: calc(var(--block-size) * (var(--spare-blocks) / -2));
|
||||
width: calc(var(--field-width) + var(--spare-blocks) * var(--block-size));
|
||||
height: calc(var(--field-height) + var(--spare-blocks) * var(--block-size));
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
}
|
||||
</style>
|
66
src/old/level/def.js
Normal file
|
@ -0,0 +1,66 @@
|
|||
export const BLOCK_SIZE = 32 // each block is 32̨̣̌̇x32 pixel in size and equals 1m
|
||||
export const RECIPROCAL = 1 / BLOCK_SIZE
|
||||
|
||||
export const STAGE_WIDTH = 32 // 32*32 = 1024 pixel wide stage
|
||||
export const STAGE_HEIGHT = ~~(STAGE_WIDTH * 0.5625) // 16:9 😎
|
||||
|
||||
// the player position is fixed to the middle of the x axis
|
||||
export const PLAYER_X = ~~(STAGE_WIDTH / 2) + 1
|
||||
export const PLAYER_Y = ~~(STAGE_HEIGHT * 0.5) // fall from the center
|
||||
|
||||
export const GRAVITY = 10 // blocks per second
|
||||
|
||||
export const type = {
|
||||
air: {type: 'air', hp: Infinity, walkable: true},
|
||||
grass: {type: 'grass', hp: 1, walkable: false},
|
||||
|
||||
tree_top_left: {type: 'tree_top_left', hp: 5, walkable: true},
|
||||
tree_top_middle: {type: 'tree_top_middle', hp: 5, walkable: true},
|
||||
tree_top_right: {type: 'tree_top_right', hp: 5, walkable: true},
|
||||
|
||||
tree_crown_left: {type: 'tree_crown_left', hp: 5, walkable: true},
|
||||
tree_crown_middle: {type: 'tree_crown_middle', hp: 5, walkable: true, climbable: true},
|
||||
tree_crown_right: {type: 'tree_crown_right', hp: 5, walkable: true},
|
||||
|
||||
tree_trunk_left: {type: 'tree_trunk_left', hp: 5, walkable: true},
|
||||
tree_trunk_middle: {type: 'tree_trunk_middle', hp: 5, walkable: true, climbable: true},
|
||||
tree_trunk_right: {type: 'tree_trunk_right', hp: 5, walkable: true},
|
||||
|
||||
tree_root_left: {type: 'tree_root_left', hp: 5, walkable: true},
|
||||
tree_root_middle: {type: 'tree_root_middle', hp: 5, walkable: true, climbable: true},
|
||||
tree_root_right: {type: 'tree_root_right', hp: 5, walkable: true},
|
||||
|
||||
tree_top_left_mixed: {type: 'tree_top_left_mixed', hp: 5, walkable: true},
|
||||
tree_crown_left_mixed: {type: 'tree_crown_left_mixed', hp: 5, walkable: true},
|
||||
tree_trunk_left_mixed: {type: 'tree_trunk_left_mixed', hp: 5, walkable: true},
|
||||
tree_root_left_mixed: {type: 'tree_root_left_mixed', hp: 5, walkable: true},
|
||||
|
||||
tree_top_right_mixed: {type: 'tree_top_right_mixed', hp: 5, walkable: true},
|
||||
tree_crown_right_mixed: {type: 'tree_crown_right_mixed', hp: 5, walkable: true},
|
||||
tree_trunk_right_mixed: {type: 'tree_trunk_right_mixed', hp: 5, walkable: true},
|
||||
tree_root_right_mixed: {type: 'tree_root_right_mixed', hp: 5, walkable: true},
|
||||
|
||||
soil: {type: 'soil', hp: 2, walkable: false},
|
||||
soil_gravel: {type: 'soil_gravel', hp: 5, walkable: false},
|
||||
stone_gravel: {type: 'stone_gravel', hp: 5, walkable: false},
|
||||
stone: {type: 'stone', hp: 10, walkable: false},
|
||||
bedrock: {type: 'bedrock', hp: 25, walkable: false},
|
||||
cave: {type: 'cave', hp: Infinity, walkable: true}
|
||||
}
|
||||
|
||||
export const level = {
|
||||
treeTop: 24,
|
||||
ground: 28,
|
||||
rock: 32,
|
||||
underground: 48,
|
||||
cave_max: 250
|
||||
}
|
||||
|
||||
export const probability = {
|
||||
tree: 0.2,
|
||||
soil_hole: 0.3,
|
||||
soil_gravel: 0.2,
|
||||
stone_gravel: 0.1,
|
||||
cave: 0.5,
|
||||
fray: 0.4
|
||||
}
|
58
src/old/level/first-iteration.js
Normal file
|
@ -0,0 +1,58 @@
|
|||
import {type as T, level as L, probability as P} from './def'
|
||||
|
||||
export default class BlockGen {
|
||||
constructor (noiseGen) {
|
||||
this.rand = (x, y) => 0.5 + 0.5 * noiseGen.noise2D(x, y)
|
||||
}
|
||||
|
||||
level (level, column, row, previousRow) {
|
||||
for (let i = 0; i < row.length; i++) {
|
||||
row[i] = this.block(level, column + i, row[i], row[i - 1], previousRow[i])
|
||||
}
|
||||
}
|
||||
|
||||
block (level, column, current, before, above) {
|
||||
if (level < L.peak) return this.air()
|
||||
|
||||
const r = this.rand(level, column)
|
||||
if (level < L.ground) {
|
||||
if (level === L.treeTop) return this.treeTop(r)
|
||||
return this.air()
|
||||
}
|
||||
if (level < L.rock) return this.ground(r)
|
||||
if (level < L.underground) return this.rock(r)
|
||||
return this.underground(r, above, before, level - L.underground)
|
||||
}
|
||||
|
||||
// always returns air
|
||||
air () {
|
||||
return T.air
|
||||
}
|
||||
|
||||
// returns mostly air, but sometimes starts a tree
|
||||
treeTop (r) {
|
||||
if (r < P.tree) return T.tree_top_middle
|
||||
return T.air
|
||||
}
|
||||
|
||||
// returns mostly soil and grass, sometimes gravel and sometimes air
|
||||
ground (r) {
|
||||
if (r < P.soil_gravel) return T.soil_gravel
|
||||
return T.soil
|
||||
}
|
||||
|
||||
// returns mostly stones, sometimes gravel
|
||||
rock (r) {
|
||||
return r < P.stone_gravel ? T.stone_gravel : T.stone
|
||||
}
|
||||
|
||||
// return mostly bedrock, sometimes caves, depending on the level
|
||||
underground (r, above, before, level) {
|
||||
// the probability for a cave rises with the level
|
||||
const a = P.cave / L.cave_max**2
|
||||
const p = Math.min(P.cave, a * level**2)
|
||||
|
||||
if (r < p) return T.cave
|
||||
return T.bedrock
|
||||
}
|
||||
}
|
44
src/old/level/index.js
Normal file
|
@ -0,0 +1,44 @@
|
|||
import SeedRng from 'seedrandom'
|
||||
import SimplexNoise from 'open-simplex-noise'
|
||||
|
||||
import {type as T, level as L} from './def'
|
||||
import BlockGen from './first-iteration'
|
||||
import BlockExt from './second-iteration'
|
||||
import PlayerChanges from './third-iteration'
|
||||
|
||||
export default class Level {
|
||||
constructor (width, height, seed = 'super random seed') {
|
||||
const random = SeedRng(seed)
|
||||
const noiseGen = new SimplexNoise(parseInt(seed, 32))
|
||||
this._w = width
|
||||
this._h = height
|
||||
this._grid = new Array(this._h)
|
||||
this.blockGen = new BlockGen(noiseGen)
|
||||
this.blockExt = new BlockExt(noiseGen)
|
||||
this.playerChanges = new PlayerChanges()
|
||||
}
|
||||
|
||||
change (level, column, newBlock) {
|
||||
if (newBlock.hp <= 0) {
|
||||
newBlock = level > L.rock ? { ...T.cave } : { ...T.air }
|
||||
}
|
||||
this.playerChanges.apply(level, column, newBlock)
|
||||
}
|
||||
|
||||
grid (x, y) {
|
||||
this.generate(x, y, this._w, this._h)
|
||||
return this._grid
|
||||
}
|
||||
|
||||
generate (column, y, width, height) {
|
||||
for (let i = 0; i < height; i++) {
|
||||
const level = y + i
|
||||
const row = Array(width)
|
||||
const previousRow = this._grid[i - 1] || Array()
|
||||
this.blockGen.level(level, column, row, previousRow)
|
||||
this.blockExt.level(level, column, row, previousRow)
|
||||
this.playerChanges.level(level, column, row)
|
||||
this._grid[i] = row
|
||||
}
|
||||
}
|
||||
}
|
70
src/old/level/second-iteration.js
Normal file
|
@ -0,0 +1,70 @@
|
|||
import {type as T, level as L, probability as P} from './def'
|
||||
|
||||
export default class BlockExt {
|
||||
constructor (noiseGen) {
|
||||
this.rand = (x, y) => 0.5 + 0.5 * noiseGen.noise2D(x, y)
|
||||
}
|
||||
|
||||
level (level, column, row, previousRow) {
|
||||
for (let i = 0; i < row.length; i++) {
|
||||
const r = Math.abs(this.rand(level, column + i))
|
||||
|
||||
if (level < L.ground) this.trees(r, i, row, previousRow, level)
|
||||
else if (level < L.rock) this.ground(r, i, row, previousRow)
|
||||
else if (level < L.underground) this.rock(r, i, row, previousRow)
|
||||
else this.underground(r, i, row, previousRow)
|
||||
}
|
||||
}
|
||||
|
||||
trees (r, i, row, previousRow, level) {
|
||||
const max = row.length - 1
|
||||
|
||||
if (row[i] === T.tree_top_middle) {
|
||||
if (i) {
|
||||
if (row[i - 1] === T.tree_top_right) row[i - 1] = T.tree_top_left_mixed
|
||||
else row[i - 1] = T.tree_top_left
|
||||
}
|
||||
if (i < max) row[i + 1] = T.tree_top_right
|
||||
|
||||
} else if (previousRow[i] === T.tree_top_middle) {
|
||||
row[i] = T.tree_crown_middle
|
||||
if (i) {
|
||||
if (row[i - 1] === T.tree_crown_right) row[i - 1] = T.tree_crown_left_mixed
|
||||
else row[i - 1] = T.tree_crown_left
|
||||
}
|
||||
if (i < max) row[i + 1] = T.tree_crown_right
|
||||
} else if (previousRow[i] === T.tree_crown_middle) {
|
||||
row[i] = T.tree_trunk_middle
|
||||
if (i) {
|
||||
if (row[i - 1] === T.tree_trunk_right) row[i - 1] = T.tree_trunk_left_mixed
|
||||
else row[i - 1] = T.tree_trunk_left
|
||||
}
|
||||
if (i < max) row[i + 1] = T.tree_trunk_right
|
||||
} else if (previousRow[i] === T.tree_trunk_middle) {
|
||||
row[i] = T.tree_root_middle
|
||||
if (i) {
|
||||
if (row[i - 1] === T.tree_root_right) row[i - 1] = T.tree_root_left_mixed
|
||||
else row[i - 1] = T.tree_root_left
|
||||
}
|
||||
if (i < max) row[i + 1] = T.tree_root_right
|
||||
}
|
||||
}
|
||||
|
||||
ground (r, i, row, previousRow) {
|
||||
const tree_parts = [T.tree_root_left, T.tree_root_middle, T.tree_root_right]
|
||||
if (previousRow[i] === T.air) {
|
||||
if (r < P.soil_hole) row[i] = T.air
|
||||
if (row[i] === T.soil) row[i] = T.grass
|
||||
} else if (tree_parts.indexOf(previousRow[i]) >= 0) {
|
||||
if (row[i] === T.soil) row[i] = T.grass
|
||||
}
|
||||
}
|
||||
|
||||
rock (r, i, row, previousRow) {
|
||||
if (previousRow[i] === T.soil && r < P.fray) row[i] = T.soil
|
||||
}
|
||||
|
||||
underground (r, i, row, previousRow) {
|
||||
if (previousRow[i] === T.stone && r < P.fray) row[i] = T.stone
|
||||
}
|
||||
}
|
23
src/old/level/third-iteration.js
Normal file
|
@ -0,0 +1,23 @@
|
|||
export default class PlayerChanges {
|
||||
constructor () {
|
||||
this.changes = {}
|
||||
}
|
||||
|
||||
getKey (level, column) {
|
||||
return `${column}.${level}`
|
||||
}
|
||||
|
||||
apply (level, column, newBlock) {
|
||||
const key = this.getKey(level, column)
|
||||
this.changes[key] = newBlock
|
||||
console.log('applied', level, column, newBlock, this.changes)
|
||||
}
|
||||
|
||||
level (level, column, row) {
|
||||
for (let i = 0; i < row.length; i++) {
|
||||
const key = this.getKey(level - 1, column + i)
|
||||
const change = this.changes[key]
|
||||
if (change) row[i] = change
|
||||
}
|
||||
}
|
||||
}
|
14
src/old/main.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
import Vue from 'vue'
|
||||
import App from './App.vue'
|
||||
|
||||
Vue.directive('keep-focussed', {
|
||||
inserted (el, binding) {
|
||||
el.focus()
|
||||
el.addEventListener('blur', () => el.focus())
|
||||
}
|
||||
})
|
||||
|
||||
new Vue({
|
||||
el: '#app',
|
||||
render: h => h(App)
|
||||
})
|
22
src/old/physics.js
Normal file
|
@ -0,0 +1,22 @@
|
|||
import { GRAVITY } from './level/def'
|
||||
|
||||
/** physics gets input like
|
||||
instance of Moveable,
|
||||
position: [x, y],
|
||||
surroundings: [top, right, bottom, left] where each is a block type
|
||||
and updates the Moveable instance values accordingly
|
||||
*/
|
||||
|
||||
export class Moveable {
|
||||
constructor (x, y, direction = 1) {
|
||||
this.x = x
|
||||
this.y = y
|
||||
this.dir = direction
|
||||
this.vx = 0
|
||||
this.vy = 0
|
||||
}
|
||||
|
||||
get direction () {
|
||||
return this.dir > 0 ? 'left' : 'right'
|
||||
}
|
||||
}
|
130
src/old/solar-quartet.js
Normal file
|
@ -0,0 +1,130 @@
|
|||
/* Adapted from the original "Solar Quartet" by y0natan
|
||||
* https://codepen.io/y0natan/pen/MVvxBM
|
||||
* https://js1k.com/2018-coins/demo/3075
|
||||
*/
|
||||
|
||||
// sunY sets the height of the sun and with this the time of the day
|
||||
// where 0 is lowest (night) and -100 is highest (day), other values are possible
|
||||
// but don't make much sense / difference
|
||||
export default function drawFrame (canvas, ctx, width, height, grCanvas, grCtx, grWidth, grHeight, frame, sunY) {
|
||||
// reset canvas state
|
||||
canvas.width = width
|
||||
canvas.height = height
|
||||
grCanvas.width = grWidth
|
||||
grCanvas.height = grHeight
|
||||
|
||||
const sunCenterX = grWidth / 2
|
||||
const sunCenterY = grHeight / 2 + sunY
|
||||
// Set the godrays' context fillstyle to a newly created gradient
|
||||
// which we also run through our abbreviator.
|
||||
let emissionGradient = grCtx.createRadialGradient(
|
||||
sunCenterX, sunCenterY, // The sun's center.
|
||||
0, // Start radius.
|
||||
sunCenterX, sunCenterY, // The sun's center.
|
||||
44 // End radius.
|
||||
)
|
||||
grCtx.fillStyle = emissionGradient
|
||||
|
||||
// Now we addColorStops. This needs to be a dark gradient because our
|
||||
// godrays effect will basically overlay it on top of itself many many times,
|
||||
// so anything lighter will result in lots of white.
|
||||
// If you're not space-bound you can add another stop or two, maybe fade out to black,
|
||||
// but this actually looks good enough.
|
||||
|
||||
// a black "gradient" means no emission, so we fade to black as transition to night or day
|
||||
let emissionStrength = 1.0
|
||||
if (sunY > -30) emissionStrength -= Math.max((30 + sunY) / 5, 0.0)
|
||||
else if (sunY < -60) emissionStrength += Math.min(1 + (60 + sunY) / 5, 0.0)
|
||||
|
||||
emissionGradient.addColorStop(.1, `hsl(30,50%,${3.1 * emissionStrength}%)`) // pixels in radius 0 to 4.4 (44 * .1).
|
||||
emissionGradient.addColorStop(.2, `hsl(12,71%,${1.4 * emissionStrength}%)`) // pixels in radius 0 to 4.4 (44 * .1).
|
||||
|
||||
// Now paint the gradient all over our godrays canvas.
|
||||
grCtx.fillRect(0, 0, grWidth, grHeight)
|
||||
|
||||
// And set the fillstyle to black, we'll use it to paint our occlusion (mountains).
|
||||
grCtx.fillStyle = '#000'
|
||||
|
||||
// Paint the sky
|
||||
const skyGradient = ctx.createLinearGradient(0, 0, 0, height)
|
||||
|
||||
// hue from blue to red depending on the suns position
|
||||
const skyHue = 360 + sunY
|
||||
// lesser saturation at day so that the red fades away
|
||||
const skySaturation = 100 + sunY
|
||||
// darker at night
|
||||
const skyLightness = Math.min(sunY * -1 - 10, 55)
|
||||
|
||||
const skyHSLTop = `hsl(220, 70%, ${skyLightness}%)`
|
||||
const skyHSLBottom = `hsl(${skyHue}, ${skySaturation}%, ${skyLightness}%)`
|
||||
skyGradient.addColorStop(0, skyHSLTop)
|
||||
skyGradient.addColorStop(.7, skyHSLBottom)
|
||||
|
||||
ctx.fillStyle = skyGradient
|
||||
ctx.fillRect(0, 0, width, height)
|
||||
|
||||
// Our mountains will be made by summing up sine waves of varying frequencies and amplitudes.
|
||||
function mountainHeight(position, roughness) {
|
||||
// Our frequencies (prime numbers to avoid extra repetitions).
|
||||
// TODO: play with the numbers
|
||||
let frequencies = [1721, 947, 547, 233, 73, 31, 7]
|
||||
// Add them up.
|
||||
return frequencies.reduce((height, freq) => height * roughness - Math.cos(freq * position), 0)
|
||||
}
|
||||
|
||||
// Draw 4 layers of mountains.
|
||||
for(let i = 0; i < 4; i++) {
|
||||
// Set the main canvas fillStyle to a shade of green-brown with variable lightness
|
||||
// depending on sunY and depth
|
||||
|
||||
if (sunY > -60) {
|
||||
ctx.fillStyle = `hsl(5, 23%, ${33*emissionStrength - i*6*emissionStrength}%)`
|
||||
} else {
|
||||
ctx.fillStyle = `hsl(${220 - i*40}, 23%, ${33-i*6}%)`
|
||||
}
|
||||
|
||||
// For each column in our canvas...
|
||||
for(let x = width; x--;) {
|
||||
// Ok, I don't really remember the details here, basically the (frame+frame*i*i) makes the
|
||||
// near mountains move faster than the far ones. We divide by large numbers because our
|
||||
// mountains repeat at position 1/7*Math.PI*2 or something like that...
|
||||
let mountainPosition = (frame * 2 * i**2) / 1000 + x / 2000;
|
||||
// Make further mountains more jagged, adds a bit of realism and also makes the godrays
|
||||
// look nicer.
|
||||
let mountainRoughness = i / 19 - .5;
|
||||
// 128 is the middle, i * 25 moves the nearer mountains lower on the screen.
|
||||
let y = 128 + i * 25 + mountainHeight(mountainPosition, mountainRoughness) * 45;
|
||||
// Paint a 1px-wide rectangle from the mountain's top to below the bottom of the canvas.
|
||||
ctx.fillRect(x, y, 1, 999); // 999 can be any large number...
|
||||
// Paint the same thing in black on the godrays emission canvas, which is 1/4 the size,
|
||||
// and move it one pixel down (otherwise there can be a tiny underlit space between the
|
||||
// mountains and the sky).
|
||||
grCtx.fillRect(x/4, y/4+1, 1, 999);
|
||||
}
|
||||
}
|
||||
|
||||
// The godrays are generated by adding up RGB values, gCt is the bane of all js golfers -
|
||||
// globalCompositeOperation. Set it to 'lighter' on both canvases.
|
||||
ctx.globalCompositeOperation = grCtx.globalCompositeOperation = 'lighter';
|
||||
|
||||
// NOW - let's light this motherfucker up! We'll make several passes over our emission canvas,
|
||||
// each time adding an enlarged copy of it to itself so at the first pass we get 2 copies, then 4,
|
||||
// then 8, then 16 etc... We square our scale factor at each iteration.
|
||||
for (let scaleFactor = 1.07; scaleFactor < 5; scaleFactor *= scaleFactor) {
|
||||
// The x, y, width and height arguments for drawImage keep the light source in the same
|
||||
// spot on the enlarged copy. It basically boils down to multiplying a 2D matrix by itself.
|
||||
// There's probably a better way to do this, but I couldn't figure it out.
|
||||
// For reference, here's an AS3 version (where BitmapData:draw takes a matrix argument):
|
||||
// https://github.com/yonatan/volumetrics/blob/d3849027213e9499742cc4dfd2838c6032f4d9d3/src/org/zozuar/volumetrics/EffectContainer.as#L208-L209
|
||||
grCtx.drawImage(
|
||||
grCanvas,
|
||||
(grWidth - grWidth * scaleFactor) / 2,
|
||||
(grHeight - grHeight * scaleFactor) / 2 - sunY * scaleFactor + sunY,
|
||||
grWidth * scaleFactor,
|
||||
grHeight * scaleFactor
|
||||
)
|
||||
}
|
||||
|
||||
// Draw godrays to output canvas (whose globalCompositeOperation is already set to 'lighter').
|
||||
ctx.drawImage(grCanvas, 0, 0, width, height);
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, computed, onUnmounted } from 'vue'
|
||||
import { getItemClass } from '../level/items'
|
||||
import type { InventoryItem } from '../util/usePlayer'
|
||||
import type { InventoryItem } from '../types.d'
|
||||
|
||||
export interface Props {
|
||||
items: InventoryItem[]
|
||||
|
@ -10,7 +10,7 @@ export interface Props {
|
|||
|
||||
const props = defineProps<Props>()
|
||||
const emit = defineEmits<{
|
||||
(event: 'selection', value: InventoryItem | null): void
|
||||
(event: 'selection', value: InventoryItem): void
|
||||
}>()
|
||||
|
||||
// inventory size is 15
|
||||
|
|
94
src/types.d.ts
vendored
Normal file
|
@ -0,0 +1,94 @@
|
|||
import type { ItemId } from './level/items'
|
||||
export type { ItemId } from './level/items'
|
||||
export type ItemQuality = 'wood' | 'iron' | 'diamond'
|
||||
export type ItemType = 'tool' | 'block' | 'ore' | 'fixture'
|
||||
|
||||
export interface Item {
|
||||
id: string
|
||||
name: string
|
||||
type: ItemType
|
||||
icon: string
|
||||
quality?: ItemQuality
|
||||
// this should be ItemId | BlockType, but has to be string to avoid
|
||||
// a circular type reference
|
||||
builds?: string
|
||||
}
|
||||
|
||||
export interface ToolItem extends Item {
|
||||
type: 'tool'
|
||||
quality: ItemQuality
|
||||
}
|
||||
|
||||
export interface BlockItem extends Item {
|
||||
type: 'block'
|
||||
builds: BlockType
|
||||
}
|
||||
|
||||
type Fixture<T extends string> = `${T}Left` | `${T}Right` | `${T}Ceiling` | `${T}Floor`;
|
||||
|
||||
export type BlockType =
|
||||
| 'air' | 'grass'
|
||||
| 'treeCrown' | 'treeLeaves' | 'treeTrunk' | 'treeRoot'
|
||||
| 'soil' | 'soilGravel'
|
||||
| 'stone' | 'stoneGravel'
|
||||
| 'bedrock' | 'cave'
|
||||
| 'brickWall'
|
||||
| Fixture<'torch'>
|
||||
|
||||
export type Block = {
|
||||
type: BlockType // what is it?
|
||||
hp: number // how long do I need to hit it?
|
||||
walkable: boolean // can I walk through it?
|
||||
climbable?: boolean // can I climb it?
|
||||
transparent?: boolean // can I see through it?
|
||||
fixture?: boolean // is it built by the player?
|
||||
illumination?: number // How many blocks wide is it glowing?
|
||||
color?: string // How is it coloured?
|
||||
drops?: ItemId // what do I get, when loot it?
|
||||
}
|
||||
|
||||
// describes a changed block, eg digged or placed by the player
|
||||
type DamagedBlock = {
|
||||
change: 'damage'
|
||||
x: number
|
||||
y: number
|
||||
damage: number
|
||||
}
|
||||
type ChangedBlock = {
|
||||
change: 'exchange'
|
||||
x: number
|
||||
y: number
|
||||
newType: BlockType
|
||||
}
|
||||
export type Change = DamagedBlock | ChangedBlock
|
||||
|
||||
export interface InventoryItem extends Item {
|
||||
amount: number
|
||||
}
|
||||
|
||||
export interface Moveable {
|
||||
x: number // position on x-axis (fixed for the player)
|
||||
y: number // position on y-axis (fixed for the player)
|
||||
lastDir: number // store last face direction
|
||||
vx: number // velocity on the x-axis
|
||||
vy: number // velocity on the y-axis
|
||||
}
|
||||
|
||||
export interface Npc extends Moveable {
|
||||
hostile: boolean
|
||||
inventory: InventoryItem[]
|
||||
}
|
||||
|
||||
export interface Player extends Moveable {
|
||||
inventory: InventoryItem[]
|
||||
}
|
||||
|
||||
export type Direction = 'at' | 'left' | 'right' | 'up' | 'down'
|
||||
|
||||
|
||||
export interface LightSource {
|
||||
x: number
|
||||
y: number
|
||||
strength: number
|
||||
color: string
|
||||
}
|
|
@ -18,7 +18,13 @@ function hsl(h: number, s: number, l: number): string {
|
|||
* @param r: number [44] - the radius of the "sun"
|
||||
* @returns emissionStrength: number - emission intensity blends over the mountains
|
||||
*/
|
||||
function renderGodrays(ctx: CanvasRenderingContext2D, cx: number, cy: number, sunY: number, r = 44) {
|
||||
function renderGodrays(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
cx: number,
|
||||
cy: number,
|
||||
sunY: number,
|
||||
r = 44,
|
||||
) {
|
||||
const w = ctx.canvas.width
|
||||
const h = ctx.canvas.height
|
||||
|
||||
|
@ -36,11 +42,14 @@ function renderGodrays(ctx: CanvasRenderingContext2D, cx: number, cy: number, su
|
|||
if (sunY > -30) emissionStrength -= Math.max((30 + sunY) / 5, 0.0)
|
||||
else if (sunY < -60) emissionStrength += Math.min(1 + (60 + sunY) / 5, 0.0)
|
||||
|
||||
emissionGradient.addColorStop(.1, hsl(30, 50, 3.1 * emissionStrength)) // pixels in radius 0 to 4.4 (44 * .1).
|
||||
emissionGradient.addColorStop(.2, hsl(12, 71, 1.4 * emissionStrength)) // pixels in radius 0 to 4.4 (44 * .1).
|
||||
// Now paint the gradient all over our godrays canvas.
|
||||
// pixels in radius 0 to 4.4 (44 * .1)
|
||||
emissionGradient.addColorStop(.1, hsl(30, 50, 3.1 * emissionStrength))
|
||||
emissionGradient.addColorStop(.2, hsl(12, 71, 1.4 * emissionStrength))
|
||||
|
||||
// Now paint the gradient all over our godrays canvas
|
||||
ctx.fillRect(0, 0, w, h)
|
||||
// And set the fillstyle to black, we'll use it to paint our occlusion (mountains).
|
||||
|
||||
// And set the fillstyle to black, we'll use it to paint our occlusion (mountains)
|
||||
ctx.fillStyle = '#000'
|
||||
|
||||
return emissionStrength
|
||||
|
@ -51,8 +60,12 @@ function renderGodrays(ctx: CanvasRenderingContext2D, cx: number, cy: number, su
|
|||
* Mountains are made by summing up sine waves with varying frequencies and amplitudes
|
||||
* The frequencies are prime, to avoid extra repetitions
|
||||
*/
|
||||
function calcMountainHeight(pos: number, roughness: number, frequencies = [1721, 947, 547, 233, 73, 31, 7]) {
|
||||
return frequencies.reduce((height, freq) => height * roughness - Math.cos(freq * pos), 0)
|
||||
function calcMountainHeight(
|
||||
pos: number,
|
||||
roughness: number,
|
||||
freqs = [1721, 947, 547, 233, 73, 31, 7],
|
||||
) {
|
||||
return freqs.reduce((height, freq) => height * roughness - Math.cos(freq * pos), 0)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -64,7 +77,14 @@ function calcMountainHeight(pos: number, roughness: number, frequencies = [1721,
|
|||
* @param layers: number - amount of mountain layers for parallax effect
|
||||
* @param emissionStrength: number - intensity of the godrays
|
||||
*/
|
||||
function renderMountains(ctx: CanvasRenderingContext2D, grCtx: CanvasRenderingContext2D, frame: number, sunY: number, layers: number, emissionStrength: number) {
|
||||
function renderMountains(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
grCtx: CanvasRenderingContext2D,
|
||||
frame: number,
|
||||
sunY: number,
|
||||
layers: number,
|
||||
emissionStrength: number,
|
||||
) {
|
||||
const w = ctx.canvas.width
|
||||
const h = ctx.canvas.height
|
||||
const grDiv = w / grCtx.canvas.width
|
||||
|
@ -127,19 +147,27 @@ function renderSky(ctx: CanvasRenderingContext2D, sunY: number) {
|
|||
* @param rayQuality: number [8] - The quality of the sunrays (divides the resolution, so higher value means lower quality)
|
||||
* @param mountainLayers: number [4] - How many layers of mountains are used for parallax effect?
|
||||
*/
|
||||
export default function useBackground (canvasEl: HTMLCanvasElement, w: number, h: number, rayQuality = 8, mountainLayers = 4) {
|
||||
export default function useBackground(
|
||||
canvasEl: HTMLCanvasElement,
|
||||
w: number,
|
||||
h: number,
|
||||
rayQuality = 8,
|
||||
mountainLayers = 4,
|
||||
) {
|
||||
canvasEl.width = w
|
||||
canvasEl.height = h
|
||||
|
||||
const grW = w / rayQuality
|
||||
const grH = h / rayQuality
|
||||
|
||||
const ctx = canvasEl.getContext('2d')
|
||||
if (ctx === null) return // like, how old is your browser?
|
||||
|
||||
const grCanvasEl = document.createElement('canvas')
|
||||
|
||||
const ctx = canvasEl.getContext('2d')
|
||||
const grCtx = grCanvasEl.getContext('2d')
|
||||
if (grCtx === null) return // like, how old is your browser?
|
||||
if (ctx === null || grCtx === null) {
|
||||
console.error('BACKGROUND CANVAS ERROR: Failed to set up canvas?!')
|
||||
return () => {} // for real, how old is it?
|
||||
}
|
||||
|
||||
grCanvasEl.width = grW
|
||||
grCanvasEl.height = grH
|
||||
|
@ -153,7 +181,10 @@ export default function useBackground (canvasEl: HTMLCanvasElement, w: number, h
|
|||
* @param sunY: number - the position (height) of the sun in the sky
|
||||
*/
|
||||
return function drawFrame (frame: number, sunY: number) {
|
||||
console.log('drawing frame', frame, sunY)
|
||||
ctx.globalCompositeOperation = grCtx.globalCompositeOperation = 'source-over'
|
||||
ctx.clearRect(0, 0, w, h)
|
||||
grCtx.clearRect(0, 0, grW, grH)
|
||||
|
||||
const emissionStrength = renderGodrays(grCtx, sunCenterX, sunCenterY, sunY)
|
||||
renderSky(ctx, sunY)
|
||||
renderMountains(ctx, grCtx, frame, sunY, mountainLayers, emissionStrength)
|
||||
|
|
|
@ -28,18 +28,18 @@ export default function useInput() {
|
|||
inputX.value = -1
|
||||
break
|
||||
case 'p':
|
||||
if (!help.value) paused.value = !paused.value
|
||||
if (wasPaused && !paused.value) wasPaused = false
|
||||
if (!help.value && !inventory.value) {
|
||||
paused.value = !paused.value
|
||||
wasPaused = paused.value
|
||||
}
|
||||
break
|
||||
case '?':
|
||||
if (paused.value && !help.value) wasPaused = true
|
||||
if (!wasPaused) paused.value = !help.value
|
||||
help.value = !help.value
|
||||
paused.value = help.value || wasPaused
|
||||
break
|
||||
case 'i':
|
||||
if (paused.value && !inventory.value) wasPaused = true
|
||||
if (!wasPaused) paused.value = !inventory.value
|
||||
inventory.value = !inventory.value
|
||||
paused.value = inventory.value || wasPaused
|
||||
break
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,105 +0,0 @@
|
|||
import type { ComputedRef } from 'vue'
|
||||
import { BLOCK_SIZE, STAGE_WIDTH, STAGE_HEIGHT } from '../level/def'
|
||||
|
||||
export default function useLightMap(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
x: ComputedRef<number>,
|
||||
y: ComputedRef<number>,
|
||||
tx: ComputedRef<number>,
|
||||
ty: ComputedRef<number>,
|
||||
time: ComputedRef<number>,
|
||||
lightBarrier: ComputedRef<number[]>,
|
||||
) {
|
||||
const W = ((STAGE_WIDTH + 2) * BLOCK_SIZE)
|
||||
const H = ((STAGE_HEIGHT + 2) * BLOCK_SIZE)
|
||||
const B = BLOCK_SIZE - 4 // no idea why there is a difference, but it is 4px
|
||||
|
||||
const playerX = (W - B) / 2 + B / 4
|
||||
const playerY = H / 2 - B / 2
|
||||
const playerLightSize = B * 1.8
|
||||
|
||||
function getAmbientLightColor() {
|
||||
const t = time.value
|
||||
|
||||
// Night time (pale bluish dark: hslpicker.com/#2b293d )
|
||||
if (t > 900 || t < 100) {
|
||||
return `hsl(245, 20%, 20%)`
|
||||
}
|
||||
// Morning hours (gradually more reddish hue)
|
||||
if (t < 250) {
|
||||
const s = Math.round((t - 100) / 1.5) // 0-100%
|
||||
const l = Math.round((t - 100) / 1.875) + 20 // 20-100%
|
||||
return `hsl(0, ${s}%, ${l}%)`
|
||||
}
|
||||
// Evening hours (from neutral white to bluish hue with low saturation)
|
||||
if (t > 700) {
|
||||
const s = 100 - Math.round((t - 700) / 2.5) // 100-20%
|
||||
return `hsl(245, ${s}%, ${s}%)`
|
||||
}
|
||||
// day (neutral white)
|
||||
return `hsl(0, 0%, 100%)`
|
||||
}
|
||||
|
||||
function drawPlayerLight(sizeMul:number) {
|
||||
const playerLight = ctx.createRadialGradient(
|
||||
playerX - tx.value, playerY - ty.value, 0,
|
||||
playerX - tx.value, playerY - ty.value, playerLightSize * sizeMul
|
||||
)
|
||||
|
||||
// Add color stops: white in the center to transparent white
|
||||
playerLight.addColorStop(0.0, "#FFFF");
|
||||
playerLight.addColorStop(1, "#FFF0");
|
||||
|
||||
// Set the fill style and draw a rectangle
|
||||
ctx.fillStyle = playerLight;
|
||||
ctx.fillRect(0, 0, W, H)
|
||||
}
|
||||
|
||||
function drawLights() {
|
||||
// used for everything above ground
|
||||
const ambientLight = getAmbientLightColor()
|
||||
const surroundingLight = ambientLight.slice(-2)
|
||||
const barrier = lightBarrier.value
|
||||
|
||||
ctx.fillStyle = ambientLight
|
||||
for (let col = 0; col < W / B; col++) {
|
||||
const level = (barrier[col] - y.value) * B
|
||||
const sw = B
|
||||
const sh = level
|
||||
const sx = col * sw
|
||||
const sy = 0
|
||||
|
||||
ctx.fillRect(sx, sy, sw, sh)
|
||||
}
|
||||
|
||||
// make light columns wider to illuminate surrounding blocks
|
||||
const extra = Math.floor(B / 2)
|
||||
const reflectedLight = ambientLight.slice(0, -1) + ', 50%)'
|
||||
ctx.fillStyle = reflectedLight
|
||||
for (let col = 0; col < W / B; col++) {
|
||||
const level = (barrier[col] - y.value) * B
|
||||
const sw = B
|
||||
const sh = level
|
||||
const sx = col * sw
|
||||
const sy = 0
|
||||
|
||||
ctx.fillRect(sx - extra, sy - extra, sw + extra * 2, sh + extra * 2)
|
||||
}
|
||||
|
||||
// TODO: draw light for candles and torches
|
||||
}
|
||||
|
||||
return function update() {
|
||||
// first, throw the world in complete darkness
|
||||
ctx.fillStyle = '#000'
|
||||
ctx.fillRect(0, 0, W, H)
|
||||
|
||||
// second, find and bring light into the world
|
||||
drawLights()
|
||||
|
||||
// finally, draw the players light
|
||||
// with a size multiplicator which might be later used to
|
||||
// simulate greater illumination with candles or torches
|
||||
drawPlayerLight(1)
|
||||
}
|
||||
}
|
91
src/util/useLightMask.ts
Normal file
|
@ -0,0 +1,91 @@
|
|||
import type { Ref, ComputedRef } from 'vue'
|
||||
import type { LightSource } from '../types.d'
|
||||
import { BLOCK_SIZE, STAGE_WIDTH, STAGE_HEIGHT } from '../level/def'
|
||||
|
||||
type RefOrComputed<T> = Ref<T> | ComputedRef<T>
|
||||
|
||||
export default function useLightMask(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
y: RefOrComputed<number>,
|
||||
tx: RefOrComputed<number>,
|
||||
ty: RefOrComputed<number>,
|
||||
lightBarrier: RefOrComputed<number[]>,
|
||||
lightSources: RefOrComputed<LightSource[]>,
|
||||
) {
|
||||
const W = ((STAGE_WIDTH + 2) * BLOCK_SIZE)
|
||||
const H = ((STAGE_HEIGHT + 2) * BLOCK_SIZE)
|
||||
const B = BLOCK_SIZE - 4 // no idea why there is a difference, but it is 4px
|
||||
const BHalf = B / 2
|
||||
|
||||
const playerX = (W - B) / 2 + B / 4
|
||||
const playerY = H / 2 - BHalf
|
||||
const playerLightSize = B * 1.8
|
||||
|
||||
function drawPlayerLight(sizeMul:number) {
|
||||
const playerLight = ctx.createRadialGradient(
|
||||
playerX - tx.value, playerY - ty.value, 0,
|
||||
playerX - tx.value, playerY - ty.value, playerLightSize * sizeMul
|
||||
)
|
||||
|
||||
playerLight.addColorStop(0.7, "#FFFA");
|
||||
playerLight.addColorStop(1, "#FFF0");
|
||||
|
||||
// Set the fill style and draw a rectangle
|
||||
ctx.fillStyle = playerLight;
|
||||
ctx.fillRect(0, 0, W, H)
|
||||
}
|
||||
|
||||
function drawLightSources() {
|
||||
for (const src of lightSources.value) {
|
||||
const x = src.x * B
|
||||
const y = src.y * B
|
||||
const strength = src.strength + (src.strength * Math.random()) / 10
|
||||
const light = ctx.createRadialGradient(
|
||||
x + BHalf, y - BHalf, 0,
|
||||
x + BHalf, y - BHalf, strength * B,
|
||||
)
|
||||
const color = src.color
|
||||
|
||||
light.addColorStop(0.0, color)
|
||||
light.addColorStop(0.4, `${color}A`)
|
||||
light.addColorStop(1, `${color}0`)
|
||||
|
||||
ctx.fillStyle = light
|
||||
ctx.fillRect(0, 0, W, H)
|
||||
}
|
||||
}
|
||||
|
||||
function drawShadows() {
|
||||
const barrier = lightBarrier.value
|
||||
|
||||
for (let col = 0; col < barrier.length; col++) {
|
||||
const level = (barrier[col] - y.value) * B
|
||||
const x = B*col
|
||||
|
||||
// gradient for the shadow that is cast down from the surface
|
||||
const gradient = ctx.createLinearGradient(0, 0, 0, H)
|
||||
gradient.addColorStop(0, '#FFF')
|
||||
gradient.addColorStop(Math.min(level / H, 1), '#FFF')
|
||||
gradient.addColorStop(Math.min((level + B) / H, 1), '#000')
|
||||
ctx.fillStyle = gradient
|
||||
ctx.fillRect(x, 0, B, H)
|
||||
}
|
||||
}
|
||||
|
||||
return function update() {
|
||||
// first, throw the world in complete darkness
|
||||
ctx.fillStyle = '#FFFFFF'
|
||||
ctx.fillRect(0, 0, W, H)
|
||||
|
||||
// second, hide what is beneath
|
||||
drawShadows()
|
||||
|
||||
// third, fight the darkness
|
||||
drawLightSources()
|
||||
|
||||
// finally, draw the players light
|
||||
// with a size multiplicator which might be later used to
|
||||
// simulate greater illumination with candles or torches
|
||||
drawPlayerLight(1)
|
||||
}
|
||||
}
|
|
@ -1,15 +1,6 @@
|
|||
import { computed, reactive } from 'vue'
|
||||
import { RECIPROCAL, STAGE_WIDTH, STAGE_HEIGHT } from '../level/def'
|
||||
import type { Item, ItemQuality } from '../level/items'
|
||||
|
||||
export interface InventoryItem extends Item {
|
||||
amount: number
|
||||
quality: ItemQuality | null
|
||||
}
|
||||
|
||||
export interface Player extends Moveable {
|
||||
inventory: InventoryItem[]
|
||||
}
|
||||
import type { Item, Player } from '../types.d'
|
||||
|
||||
const player = reactive<Player>({
|
||||
x: Math.round((STAGE_WIDTH + 2) / 2),
|
||||
|
@ -17,10 +8,10 @@ const player = reactive<Player>({
|
|||
lastDir: 0,
|
||||
vx: 0,
|
||||
vy: 1, // always falling, because of gravity
|
||||
inventory: [], // not yet in use
|
||||
inventory: [],
|
||||
})
|
||||
|
||||
const pocket = (newItem: Item) => {
|
||||
const pocket = (newItem: Item, amount = 1) => {
|
||||
const existing = player.inventory.find(item => item.name === newItem.name)
|
||||
|
||||
if (existing) {
|
||||
|
@ -28,9 +19,8 @@ const pocket = (newItem: Item) => {
|
|||
return existing.amount
|
||||
}
|
||||
player.inventory.push({
|
||||
quality: null,
|
||||
amount: 1,
|
||||
...newItem
|
||||
...newItem,
|
||||
amount,
|
||||
})
|
||||
return 1
|
||||
}
|
||||
|
|
58
src/util/useTime.test.ts
Normal file
|
@ -0,0 +1,58 @@
|
|||
import { describe, it, expect } from 'vitest'
|
||||
import { calcTimeOfDay, renderClock, calcSunAngle } from './useTime'
|
||||
|
||||
describe('useTime composable', () => {
|
||||
describe('clock', () => {
|
||||
it('renders 2:00 at tick 0', () => {
|
||||
expect(renderClock(0)).toEqual('2:00')
|
||||
})
|
||||
it('renders 14:00 at tick 500', () => {
|
||||
expect(renderClock(500)).toEqual('14:00')
|
||||
})
|
||||
it('does not break on meanless values', () => {
|
||||
expect(renderClock(9500)).toBeDefined()
|
||||
expect(renderClock(-500)).toBeDefined()
|
||||
})
|
||||
})
|
||||
describe('time of day', () => {
|
||||
it('sets the correct time of day', () => {
|
||||
expect(calcTimeOfDay(0)).toEqual('night')
|
||||
expect(calcTimeOfDay(900)).toEqual('night')
|
||||
expect(calcTimeOfDay(100)).toEqual('morning0')
|
||||
expect(calcTimeOfDay(145)).toEqual('morning1')
|
||||
expect(calcTimeOfDay(200)).toEqual('morning2')
|
||||
expect(calcTimeOfDay(700)).toEqual('evening0')
|
||||
expect(calcTimeOfDay(800)).toEqual('evening1')
|
||||
expect(calcTimeOfDay(850)).toEqual('evening2')
|
||||
expect(calcTimeOfDay(300)).toEqual('day')
|
||||
expect(calcTimeOfDay(400)).toEqual('day')
|
||||
expect(calcTimeOfDay(555)).toEqual('day')
|
||||
})
|
||||
})
|
||||
describe('sun angle', () => {
|
||||
it('returns -10 degrees over night', () => {
|
||||
expect(calcSunAngle(900)).toEqual(-10)
|
||||
expect(calcSunAngle(945)).toEqual(-10)
|
||||
expect(calcSunAngle(0)).toEqual(-10)
|
||||
expect(calcSunAngle(57)).toEqual(-10)
|
||||
})
|
||||
it('returns -90 degrees over day', () => {
|
||||
expect(calcSunAngle(250)).toEqual(-90)
|
||||
expect(calcSunAngle(300)).toEqual(-90)
|
||||
expect(calcSunAngle(557)).toEqual(-90)
|
||||
expect(Math.round(calcSunAngle(699))).toEqual(-90)
|
||||
})
|
||||
it('raises in the morning', () => {
|
||||
expect(calcSunAngle(100)).toEqual(-20)
|
||||
expect(calcSunAngle(150)).toEqual(-45)
|
||||
expect(calcSunAngle(200)).toEqual(-70)
|
||||
expect(calcSunAngle(240)).toEqual(-90)
|
||||
})
|
||||
it('sinks in the evening', () => {
|
||||
expect(calcSunAngle(700)).toEqual(-90)
|
||||
expect(calcSunAngle(750)).toEqual(-70)
|
||||
expect(calcSunAngle(800)).toEqual(-50)
|
||||
expect(Math.round(calcSunAngle(899))).toEqual(-10)
|
||||
})
|
||||
})
|
||||
})
|
|
@ -1,30 +1,59 @@
|
|||
import { ref, computed } from 'vue'
|
||||
|
||||
type TimeOfDay =
|
||||
| 'day'
|
||||
| 'night'
|
||||
| 'morning0'
|
||||
| 'morning1'
|
||||
| 'morning2'
|
||||
| 'evening0'
|
||||
| 'evening1'
|
||||
| 'evening2'
|
||||
|
||||
export function calcTimeOfDay(tick: number): TimeOfDay {
|
||||
if (tick >= 900 || tick < 80) return 'night'
|
||||
if (tick >= 80 && tick < 120) return 'morning0'
|
||||
if (tick >= 120 && tick < 150) return 'morning1'
|
||||
if (tick >= 150 && tick < 240) return 'morning2'
|
||||
if (tick >= 700 && tick < 800) return 'evening0'
|
||||
if (tick >= 800 && tick < 850) return 'evening1'
|
||||
if (tick >= 850 && tick < 900) return 'evening2'
|
||||
return 'day'
|
||||
}
|
||||
|
||||
export function renderClock(tick: number): string {
|
||||
const t = tick * 86.4 // 1000 ticks to 86400 seconds (per day)
|
||||
const h = ~~(t / 3600.0)
|
||||
const m = ~~((t / 3600.0 - h) * 60.0)
|
||||
return `${(h + 2) % 24}:${m < 10 ? '0' : ''}${m}`
|
||||
}
|
||||
|
||||
function calcProgress(tick: number, start: number, end: number, span: number): number {
|
||||
return (tick - start) / (end - start) * span
|
||||
}
|
||||
|
||||
// calculates the suns angle from -90 (top, at noon), to 0 (at midnight)
|
||||
export function calcSunAngle(tick: number): number {
|
||||
// night time: -10 degrees fixed
|
||||
if (tick >= 900 || tick < 80) return -10
|
||||
// sunrise: gradually move from -10 to -90, by mapping 80 -> 240 to -10 -> -90
|
||||
if (tick >= 80 && tick < 240) return -10 - calcProgress(tick, 80, 240, 80)
|
||||
// sundawn: gradually move from -90 to -10, by mapping 700 -> 900 to -90 -> -10
|
||||
if (tick >= 700 && tick < 900) return -90 + calcProgress(tick, 700, 900, 80)
|
||||
// day time: -90 degrees fixed
|
||||
return -90
|
||||
}
|
||||
|
||||
export default function useTime() {
|
||||
// the day is split in 1000 parts, so we start in the morning
|
||||
const time = ref(230)
|
||||
const time = ref(240)
|
||||
|
||||
function updateTime() {
|
||||
time.value = (time.value + 0.1) % 1000
|
||||
}
|
||||
|
||||
const timeOfDay = computed(() => {
|
||||
if (time.value >= 900 || time.value < 80) return 'night'
|
||||
if (time.value >= 80 && time.value < 120) return 'morning0'
|
||||
if (time.value >= 120 && time.value < 150) return 'morning1'
|
||||
if (time.value >= 150 && time.value < 240) return 'morning2'
|
||||
if (time.value >= 700 && time.value < 800) return 'evening0'
|
||||
if (time.value >= 800 && time.value < 850) return 'evening1'
|
||||
if (time.value >= 850 && time.value < 900) return 'evening2'
|
||||
return 'day'
|
||||
})
|
||||
|
||||
const clock = computed(() => {
|
||||
const t = time.value * 86.4 // 1000 ticks to 86400 seconds (per day)
|
||||
const h = ~~(t / 3600.0)
|
||||
const m = ~~((t / 3600.0 - h) * 60.0)
|
||||
return `${(h + 2) % 24}:${m < 10 ? '0' : ''}${m}`
|
||||
})
|
||||
const timeOfDay = computed(() => calcTimeOfDay(time.value))
|
||||
const clock = computed(() => renderClock(time.value))
|
||||
|
||||
return { time, updateTime, timeOfDay, clock }
|
||||
}
|
||||
|
|
12
src/vite-env.d.ts
vendored
|
@ -1,18 +1,6 @@
|
|||
/// <reference types="vite/client" />
|
||||
|
||||
declare global {
|
||||
interface Moveable {
|
||||
x: number // position on x-axis (fixed for the player)
|
||||
y: number // position on y-axis (fixed for the player)
|
||||
lastDir: number // store last face direction
|
||||
vx: number // velocity on the x-axis
|
||||
vy: number // velocity on the y-axis
|
||||
}
|
||||
|
||||
interface Npc extends Moveable {
|
||||
hostile: boolean
|
||||
inventory: InventoryItem[]
|
||||
}
|
||||
}
|
||||
|
||||
export {}
|
||||
|
|
15
tsconfig.app.json
Normal file
|
@ -0,0 +1,15 @@
|
|||
{
|
||||
"extends": "@vue/tsconfig/tsconfig.dom.json",
|
||||
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
|
||||
"exclude": ["src/**/__tests__/*"],
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
|
||||
"paths": {
|
||||
"~/*": ["./src/*"],
|
||||
"~cmp/*": ["./src/components/*"],
|
||||
"~use/*": ["./src/composables/*"],
|
||||
"~asset/*": ["./src/assets/*"]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,36 +1,14 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"strict": true,
|
||||
"jsx": "preserve",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"esModuleInterop": true,
|
||||
"lib": [
|
||||
"ESNext",
|
||||
"DOM"
|
||||
],
|
||||
"skipLibCheck": true,
|
||||
"noEmit": true,
|
||||
"types": [
|
||||
"unplugin-vue-macros/macros-global"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.d.ts",
|
||||
"src/**/*.tsx",
|
||||
"src/**/*.vue"
|
||||
],
|
||||
"exclude": [
|
||||
"src/old",
|
||||
],
|
||||
"files": [],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.vitest.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -1,9 +1,19 @@
|
|||
{
|
||||
"extends": "@tsconfig/node22/tsconfig.json",
|
||||
"include": [
|
||||
"vite.config.*",
|
||||
"vitest.config.*",
|
||||
"cypress.config.*",
|
||||
"nightwatch.conf.*",
|
||||
"playwright.config.*",
|
||||
"eslint.config.*"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"noEmit": true,
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
"moduleResolution": "Bundler",
|
||||
"types": ["node"]
|
||||
}
|
||||
}
|
||||
|
|
11
tsconfig.vitest.json
Normal file
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"extends": "./tsconfig.app.json",
|
||||
"include": ["src/**/__tests__/*", "env.d.ts"],
|
||||
"exclude": [],
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.vitest.tsbuildinfo",
|
||||
|
||||
"lib": [],
|
||||
"types": ["node", "jsdom"]
|
||||
}
|
||||
}
|
|
@ -1,14 +1,21 @@
|
|||
import { defineConfig } from 'vite'
|
||||
import VueMacros from 'unplugin-vue-macros/vite'
|
||||
import Vue from '@vitejs/plugin-vue'
|
||||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import vueDevTools from 'vite-plugin-vue-devtools'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
VueMacros({
|
||||
plugins: {
|
||||
vue: Vue(),
|
||||
},
|
||||
}),
|
||||
vue(),
|
||||
vueDevTools(),
|
||||
],
|
||||
resolve: {
|
||||
alias: {
|
||||
'~': fileURLToPath(new URL('./src', import.meta.url)),
|
||||
'~cmp': fileURLToPath(new URL('./src/components', import.meta.url)),
|
||||
'~use': fileURLToPath(new URL('./src/composables', import.meta.url)),
|
||||
'~asset': fileURLToPath(new URL('./src/assets', import.meta.url)),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
14
vitest.config.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
import { fileURLToPath } from 'node:url'
|
||||
import { mergeConfig, defineConfig, configDefaults } from 'vitest/config'
|
||||
import viteConfig from './vite.config'
|
||||
|
||||
export default mergeConfig(
|
||||
viteConfig,
|
||||
defineConfig({
|
||||
test: {
|
||||
environment: 'jsdom',
|
||||
exclude: [...configDefaults.exclude, 'e2e/**'],
|
||||
root: fileURLToPath(new URL('./', import.meta.url)),
|
||||
},
|
||||
}),
|
||||
)
|