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
|
charset = utf-8
|
||||||
indent_style = space
|
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
end_of_line = lf
|
indent_style = space
|
||||||
insert_final_newline = true
|
insert_final_newline = true
|
||||||
trim_trailing_whitespace = 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*
|
lerna-debug.log*
|
||||||
|
|
||||||
node_modules
|
node_modules
|
||||||
|
.DS_Store
|
||||||
dist
|
dist
|
||||||
dist-ssr
|
dist-ssr
|
||||||
|
coverage
|
||||||
*.local
|
*.local
|
||||||
|
|
||||||
|
/cypress/videos/
|
||||||
|
/cypress/screenshots/
|
||||||
|
|
||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
.vscode/*
|
.vscode/*
|
||||||
!.vscode/extensions.json
|
!.vscode/extensions.json
|
||||||
.idea
|
.idea
|
||||||
.DS_Store
|
|
||||||
*.suo
|
*.suo
|
||||||
*.ntvs*
|
*.ntvs*
|
||||||
*.njsproj
|
*.njsproj
|
||||||
*.sln
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
|
|
||||||
# old source code for reference?!
|
*.tsbuildinfo
|
||||||
src/old
|
|
||||||
|
|
|
@ -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
|
## Build Setup
|
||||||
|
|
||||||
``` bash
|
```bash
|
||||||
# install dependencies
|
# install dependencies
|
||||||
yarn
|
pnpm
|
||||||
|
|
||||||
# serve with hot reload at localhost:8080
|
# serve with hot reload at localhost:5173
|
||||||
yarn dev
|
pnpm dev
|
||||||
|
|
||||||
# build for production with minification
|
# 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" />
|
/// <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>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="">
|
||||||
<head>
|
<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="icon" type="image/png" href="/favicon.png" />
|
||||||
<link rel="preload" as="image" href="/Tiles/dirt.png" />
|
<link rel="preload" as="image" href="/Tiles/dirt.png" />
|
||||||
<link rel="preload" as="image" href="/Tiles/dirt_grass.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_mid.png" />
|
||||||
<link rel="preload" as="image" href="/Tiles/trunk_bottom.png" />
|
<link rel="preload" as="image" href="/Tiles/trunk_bottom.png" />
|
||||||
<link rel="preload" as="image" href="/Tiles/leaves_transparent.png" />
|
<link rel="preload" as="image" href="/Tiles/leaves_transparent.png" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>Dig!</title>
|
<title>Vue Shovel</title>
|
||||||
<style>
|
<style>
|
||||||
:root {
|
:root {
|
||||||
--block-size: 64px;
|
--block-size: 64px;
|
||||||
|
|
61
package.json
|
@ -1,33 +1,58 @@
|
||||||
{
|
{
|
||||||
"name": "DIG",
|
"name": "vue-shovel",
|
||||||
"description": "A blocky, side-scrolling, digging and exploration game",
|
"description": "A blocky, side-scrolling, digging and exploration game",
|
||||||
"version": "0.0.1",
|
"version": "0.1.0",
|
||||||
"author": "koehr <n@koehr.in>",
|
"author": "koehr <n@koehr.in>",
|
||||||
"license": "MIT",
|
"license": "GPLv3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vue-tsc && vite build",
|
"build": "run-p type-check \"build-only {@}\" --",
|
||||||
"preview": "vite preview",
|
"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": {
|
"dependencies": {
|
||||||
"alea": "^1.0.1",
|
"alea": "^1.0.1",
|
||||||
"simplex-noise": "^4.0.1",
|
"simplex-noise": "^4.0.3",
|
||||||
"vue": "^3.4.22"
|
"vue": "^3.5.13"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rushstack/eslint-patch": "^1.10.2",
|
"@tsconfig/node22": "^22.0.0",
|
||||||
"@typescript-eslint/parser": "^7.7.0",
|
"@types/jsdom": "^21.1.7",
|
||||||
"@vitejs/plugin-vue": "^5.0.4",
|
"@types/node": "^22.13.9",
|
||||||
"@vue/eslint-config-prettier": "^9.0.0",
|
"@vitejs/plugin-vue": "^5.2.1",
|
||||||
"@vue/eslint-config-typescript": "^13.0.0",
|
"@vitest/eslint-plugin": "^1.1.36",
|
||||||
"eslint": "^9.0.0",
|
"@vue/eslint-config-prettier": "^10.2.0",
|
||||||
"eslint-plugin-vue": "^9.25.0",
|
"@vue/eslint-config-typescript": "^14.5.0",
|
||||||
"typescript": "^5.4.5",
|
"@vue/test-utils": "^2.4.6",
|
||||||
"unplugin-vue-macros": "^2.9.1",
|
"@vue/tsconfig": "^0.7.0",
|
||||||
"vite": "^5.2.9",
|
"eslint": "^9.21.0",
|
||||||
"vue-tsc": "^2.0.13"
|
"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">
|
<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 Help from './screens/help.vue'
|
||||||
import Inventory from './screens/inventory.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 { getItem, getItemClass } from './level/items'
|
||||||
import createLevel from './level'
|
import createLevel from './level'
|
||||||
|
|
||||||
|
import usePlayer from './util/usePlayer'
|
||||||
import useTime from './util/useTime'
|
import useTime from './util/useTime'
|
||||||
import useInput from './util/useInput'
|
import useInput from './util/useInput'
|
||||||
import usePlayer, { type InventoryItem } from './util/usePlayer'
|
import useLightMask from './util/useLightMask'
|
||||||
import useLightMap from './util/useLightMap'
|
|
||||||
|
|
||||||
const { updateTime, time, timeOfDay, clock } = useTime()
|
const { updateTime, time, timeOfDay, clock } = useTime()
|
||||||
const { player, direction, dx, dy, pocket, unpocket } = usePlayer()
|
const { player, direction, dx, dy, pocket, unpocket } = usePlayer()
|
||||||
const { inputX, inputY, running, paused, help, inventory } = useInput()
|
const { inputX, inputY, running, paused, help, inventory } = useInput()
|
||||||
const level = createLevel(STAGE_WIDTH + 2, STAGE_HEIGHT + 2)
|
const level = createLevel(STAGE_WIDTH + 2, STAGE_HEIGHT + 2)
|
||||||
|
|
||||||
const lightMapEl = ref<HTMLCanvasElement | undefined>(undefined)
|
const lightMaskEl = useTemplateRef<HTMLCanvasElement>('light-mask')
|
||||||
let updateLightMap: ReturnType<typeof useLightMap>
|
let updateLightMap = (() => {}) as ReturnType<typeof useLightMask>
|
||||||
|
|
||||||
pocket({ name: 'Shovel', type: 'tool', icon: 'shovel', quality: 'wood' })
|
pocket(getItem('tool_shovel_wood'))
|
||||||
pocket({ name: 'Sword', type: 'weapon', icon: 'sword', quality: 'wood' })
|
pocket(getItem('tool_sword_wood'))
|
||||||
pocket({ name: 'Pick Axe', type: 'tool', icon: 'pick', quality: 'wood' })
|
pocket(getItem('tool_pickaxe_wood'))
|
||||||
|
pocket(getItem('fixture_torch'), 5)
|
||||||
|
|
||||||
let animationFrame = 0
|
let animationFrame = 0
|
||||||
let lastTick = 0
|
let lastTick = 0
|
||||||
|
|
||||||
|
const debug = ref(true)
|
||||||
const x = ref(0)
|
const x = ref(0)
|
||||||
const y = ref(0)
|
const y = ref(0)
|
||||||
const floorX = computed(() => Math.floor(x.value))
|
const floorX = computed(() => Math.floor(x.value))
|
||||||
const floorY = computed(() => Math.floor(y.value))
|
const floorY = computed(() => Math.floor(y.value))
|
||||||
const tx = computed(() => (x.value - floorX.value) * -BLOCK_SIZE)
|
const fracX = computed(() => x.value - floorX.value)
|
||||||
const ty = computed(() => (y.value - floorY.value) * -BLOCK_SIZE)
|
const fracY = computed(() => y.value - floorY.value)
|
||||||
const rows = computed(() => level.grid(floorX.value, floorY.value))
|
const tx = computed(() => fracX.value * -BLOCK_SIZE)
|
||||||
const lightBarrier = computed(() => level.sunLight(floorX.value))
|
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 arriving = ref(true)
|
||||||
const walking = ref(false)
|
const walking = ref(false)
|
||||||
const inventorySelection = ref<InventoryItem>(player.inventory[0])
|
const inventorySelection = ref<InventoryItem>(player.inventory[0])
|
||||||
|
|
||||||
type Surroundings = {
|
const getSurroundings = (x: number, y: number) => {
|
||||||
at: Block,
|
const rows = mapGrid.value
|
||||||
left: Block,
|
|
||||||
right: Block,
|
const rowY = rows[y]
|
||||||
up: Block,
|
const rowYp = rows[y - 1]
|
||||||
down: Block,
|
const rowYn = rows[y + 1]
|
||||||
}
|
|
||||||
const surroundings = computed<Surroundings>(() => {
|
|
||||||
const px = player.x
|
|
||||||
const py = player.y
|
|
||||||
const row = rows.value
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
at: row[py][px],
|
at: rowY[x],
|
||||||
left: row[py][px - 1],
|
left: rowY[x - 1],
|
||||||
right: row[py][px + 1],
|
right: rowY[x + 1],
|
||||||
up: row[py - 1][px],
|
up: rowYp[x],
|
||||||
down: row[py + 1][px],
|
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 blocked = computed(() => {
|
||||||
const { left, right, up, down } = surroundings.value
|
const { left, right, up, down } = surroundings.value
|
||||||
|
const fx = fracX.value
|
||||||
|
const fy = fracY.value
|
||||||
return {
|
return {
|
||||||
left: !left.walkable,
|
left: !left.walkable && fx < 0.8 && fx > 0.7,
|
||||||
right: !right.walkable,
|
right: !right.walkable && fx > 0.2 && fx < 0.3,
|
||||||
up: !up.walkable,
|
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
|
// finally dig that block
|
||||||
// TODO: damage blocks first
|
// TODO: damage blocks first
|
||||||
level.change({
|
level.change({
|
||||||
type: 'exchange',
|
change: 'exchange',
|
||||||
x: floorX.value + blockX,
|
x: floorX.value + blockX,
|
||||||
y: floorY.value + blockY,
|
y: floorY.value + blockY,
|
||||||
newType: 'air'
|
newType: 'air'
|
||||||
})
|
})
|
||||||
|
mapUpdateCount.value = mapUpdateCount.value + 1
|
||||||
|
|
||||||
// anything to pick up?
|
// anything to pick up?
|
||||||
if (block.drops) {
|
if (block.drops) {
|
||||||
|
@ -93,42 +136,66 @@ function dig(blockX: number, blockY: number, block: Block) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function build(blockX: number, blockY: number, block: InventoryItem) {
|
function build(blockX: number, blockY: number, block: InventoryItem) {
|
||||||
const blockToBuild = block.builds
|
let blockToBuild = block.builds
|
||||||
// the block doesn't do anything
|
if (!blockToBuild) return // the block doesn't do anything?!
|
||||||
if (!blockToBuild) return
|
|
||||||
|
// 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({
|
level.change({
|
||||||
type: 'exchange',
|
change: 'exchange',
|
||||||
x: floorX.value + blockX,
|
x: floorX.value + blockX,
|
||||||
y: floorY.value + blockY,
|
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]
|
if (newAmount < 1) inventorySelection.value = player.inventory[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
function interactWith(blockX: number, blockY: number, block: Block) {
|
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
|
// § 4 ArbZG
|
||||||
if (paused.value) return
|
if (paused.value) return
|
||||||
|
|
||||||
const blockInHand = inventorySelection.value.type === 'block'
|
// no spooky interaction at a distance
|
||||||
const toolInHand = inventorySelection.value.type === 'tool'
|
const distanceX = ~~(px.value - blockX)
|
||||||
const emptyBlock = block.type === 'air' || block.type === 'cave'
|
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
|
// put the selected block
|
||||||
if (blockInHand && emptyBlock) {
|
if (canBuild && hasSpace) {
|
||||||
build(blockX, blockY, inventorySelection.value)
|
build(blockX, blockY, blockInHand)
|
||||||
// dig a block with shovel or pick axe
|
// dig a block with shovel or pick axe
|
||||||
} else if (toolInHand && !emptyBlock) {
|
} else if (hasTool && !hasSpace) {
|
||||||
dig(blockX, blockY, block)
|
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
|
let lastTimeUpdate = 0
|
||||||
|
@ -196,12 +263,18 @@ function selectTool(item: InventoryItem) {
|
||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
const canvas = lightMapEl.value!
|
if (lightMaskEl.value) {
|
||||||
canvas.height = (BLOCK_SIZE + 2) * STAGE_HEIGHT
|
const canvas = lightMaskEl.value
|
||||||
canvas.width = (BLOCK_SIZE + 2) * STAGE_WIDTH
|
canvas.height = (BLOCK_SIZE + 2) * STAGE_HEIGHT
|
||||||
const ctx = canvas.getContext('2d')!
|
canvas.width = (BLOCK_SIZE + 2) * STAGE_WIDTH
|
||||||
|
const ctx = canvas.getContext('2d')!
|
||||||
updateLightMap = useLightMap(ctx, floorX, floorY, tx, ty, time, lightBarrier)
|
updateLightMap = useLightMask(
|
||||||
|
ctx, floorY, tx, ty,
|
||||||
|
lightBarrier, lightSources,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
console.warn('lightmap deactivated')
|
||||||
|
}
|
||||||
lastTick = performance.now()
|
lastTick = performance.now()
|
||||||
move(lastTick)
|
move(lastTick)
|
||||||
})
|
})
|
||||||
|
@ -209,19 +282,27 @@ onMounted(() => {
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div id="field" :class="timeOfDay">
|
<div id="field" :class="timeOfDay">
|
||||||
|
<Background :time :x />
|
||||||
|
<div id="parallax">
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="blocks" :style="{transform: `translate(${tx}px, ${ty}px)`}">
|
<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"
|
<div v-for="(block, x) in row"
|
||||||
:class="['block', block.type, {
|
:class="['block', block.type, { highlight: debug && x === px && y === py }]"
|
||||||
highlight: x === player.x && y == player.y
|
|
||||||
}]"
|
|
||||||
@click="interactWith(x, y, block)"
|
@click="interactWith(x, y, block)"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</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="head"></div>
|
||||||
<div class="body"></div>
|
<div class="body"></div>
|
||||||
<div class="legs">
|
<div class="legs">
|
||||||
|
@ -235,11 +316,19 @@ onMounted(() => {
|
||||||
</div>
|
</div>
|
||||||
</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="beam" v-if="arriving"></div>
|
||||||
<div id="level-indicator">
|
<div id="level-indicator">
|
||||||
x:{{ floorX }}, y:{{ floorY }}
|
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>
|
<template v-else>({{ clock }})</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
import { ref, computed, onMounted, watch } from 'vue'
|
import { ref, computed, onMounted, watch } from 'vue'
|
||||||
import useBackground from './util/useBackground'
|
import useBackground from './util/useBackground'
|
||||||
import { BLOCK_SIZE, STAGE_WIDTH, STAGE_HEIGHT } from './level/def'
|
import { BLOCK_SIZE, STAGE_WIDTH, STAGE_HEIGHT } from './level/def'
|
||||||
|
import { calcSunAngle } from './util/useTime'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
time: number
|
time: number
|
||||||
|
@ -11,8 +12,7 @@ export interface Props {
|
||||||
const props = defineProps<Props>()
|
const props = defineProps<Props>()
|
||||||
const canvas = ref<HTMLCanvasElement | null>(null)
|
const canvas = ref<HTMLCanvasElement | null>(null)
|
||||||
|
|
||||||
const p = Math.PI / -10
|
const sunY = computed(() => calcSunAngle(props.time))
|
||||||
const sunY = computed(() => Math.sin(props.time * p))
|
|
||||||
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
@ -21,8 +21,8 @@ onMounted(() => {
|
||||||
|
|
||||||
const drawBackground = useBackground(
|
const drawBackground = useBackground(
|
||||||
canvasEl,
|
canvasEl,
|
||||||
~~(STAGE_WIDTH * BLOCK_SIZE / 2.0),
|
~~(STAGE_WIDTH * BLOCK_SIZE / 1.0),
|
||||||
~~(STAGE_HEIGHT * BLOCK_SIZE / 2.0),
|
~~(STAGE_HEIGHT * BLOCK_SIZE / 1.0),
|
||||||
)
|
)
|
||||||
|
|
||||||
watch(props, () => drawBackground(props.x, sunY.value), { immediate: true })
|
watch(props, () => drawBackground(props.x, sunY.value), { immediate: true })
|
||||||
|
@ -31,5 +31,5 @@ onMounted(() => {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<canvas ref="canvas" id="background"></canvas>
|
<canvas ref="canvas" id="background" />
|
||||||
</template>
|
</template>
|
||||||
|
|
|
@ -98,27 +98,35 @@
|
||||||
.block.brickWall {
|
.block.brickWall {
|
||||||
background-image: url(/Tiles/brick_grey.png);
|
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:hover,
|
||||||
#field .block.block.highlight {
|
#field .block.block.highlight {
|
||||||
filter: brightness(1.2) grayscale(1.0);
|
filter: brightness(1.2) saturate(1.2);
|
||||||
outline: 1px solid white;
|
outline: 1px solid white;
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
.morning0 .block,
|
.morning0 .block,
|
||||||
.morning0 #player {
|
.morning0 #player {
|
||||||
filter: saturate(50%);
|
filter: saturate(50%) brightness(0.6);
|
||||||
}
|
}
|
||||||
|
|
||||||
.morning1 .block,
|
.morning1 .block,
|
||||||
.morning1 #player {
|
.morning1 #player {
|
||||||
filter: saturate(100%);
|
filter: saturate(100%) brightness(0.8);
|
||||||
}
|
}
|
||||||
|
|
||||||
.morning2 .block,
|
.morning2 .block,
|
||||||
.morning2 #player {
|
.morning2 #player {
|
||||||
filter: saturate(120%);
|
filter: saturate(120%) brightness(0.9);
|
||||||
}
|
}
|
||||||
|
|
||||||
.evening0 .block,
|
.evening0 .block,
|
||||||
|
@ -128,17 +136,17 @@
|
||||||
|
|
||||||
.evening1 .block,
|
.evening1 .block,
|
||||||
.evening1 #player {
|
.evening1 #player {
|
||||||
filter: saturate(70%);
|
filter: saturate(70%) brightness(0.8);
|
||||||
}
|
}
|
||||||
|
|
||||||
.evening2 .block,
|
.evening2 .block,
|
||||||
.evening2 #player {
|
.evening2 #player {
|
||||||
filter: saturate(50%);
|
filter: saturate(50%) brightness(0.6);
|
||||||
}
|
}
|
||||||
|
|
||||||
.night .block,
|
.night .block,
|
||||||
.night #player {
|
.night #player {
|
||||||
filter: saturate(30%);
|
filter: saturate(30%) brightness(0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
#blocks {
|
#blocks {
|
||||||
|
@ -151,12 +159,14 @@
|
||||||
flex-flow: row wrap;
|
flex-flow: row wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
#light-mask {
|
#parallax, #background, #light-mask {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: calc(var(--block-size) * -1);
|
top: calc(var(--block-size) * -1);
|
||||||
left: calc(var(--block-size) * -1);
|
left: calc(var(--block-size) * -1);
|
||||||
width: calc(100% + var(--block-size) * 2);
|
width: calc(100% + var(--block-size) * 2);
|
||||||
height: calc(100% + var(--block-size) * 2);
|
height: calc(100% + var(--block-size) * 2);
|
||||||
mix-blend-mode: multiply;
|
|
||||||
pointer-events: none;
|
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.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-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-wood { background-image: url("/Tiles/wood.png"); }
|
||||||
.item.block-dirt { background-image: url("/Tiles/dirt.png"); }
|
.item.block-dirt { background-image: url("/Tiles/dirt.png"); }
|
||||||
.item.block-stone { background-image: url("/Tiles/stone.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;
|
--player-height: 76px;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: calc(var(--field-width) / 2);
|
left: calc(var(--field-width) / 2);
|
||||||
top: calc(var(--field-height) / 2 - 10px);
|
top: calc(var(--field-height) / 2);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-flow: column nowrap;
|
flex-flow: column nowrap;
|
||||||
width: var(--player-width);
|
width: var(--player-width);
|
||||||
|
@ -58,6 +58,21 @@
|
||||||
#player.walking > .legs > div.left {
|
#player.walking > .legs > div.left {
|
||||||
animation: dangle .3s linear infinite alternate;
|
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 {
|
#player > .arms {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 8px;
|
width: 8px;
|
||||||
|
@ -70,6 +85,9 @@
|
||||||
#player.walking > .arms {
|
#player.walking > .arms {
|
||||||
animation: dangle .3s linear infinite alternate;
|
animation: dangle .3s linear infinite alternate;
|
||||||
}
|
}
|
||||||
|
#player.running > .arms {
|
||||||
|
animation: gallop .2s linear infinite alternate;
|
||||||
|
}
|
||||||
#player > .arms > .item {
|
#player > .arms > .item {
|
||||||
width: 32px;
|
width: 32px;
|
||||||
height: 32px;
|
height: 32px;
|
||||||
|
@ -89,6 +107,14 @@
|
||||||
from { transform: rotate(-20deg); }
|
from { transform: rotate(-20deg); }
|
||||||
to { 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 {
|
@keyframes pulse {
|
||||||
from { opacity: .3; }
|
from { opacity: .3; }
|
||||||
to { opacity: 1.0; }
|
to { opacity: 1.0; }
|
||||||
|
|
|
@ -1,9 +1,14 @@
|
||||||
import type { NoiseFunction2D } from 'simplex-noise'
|
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,
|
||||||
function trees(r: number, i: number, row: Block[], previousRow: Block[]) {
|
i: number,
|
||||||
|
row: Block[],
|
||||||
|
previousRow: Block[],
|
||||||
|
nextRow: Block[],
|
||||||
|
) {
|
||||||
const max = row.length - 1
|
const max = row.length - 1
|
||||||
const h = i - 1
|
const h = i - 1
|
||||||
const j = 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 above = previousRow[i]
|
||||||
const aboveLeft = previousRow[h]
|
const aboveLeft = previousRow[h]
|
||||||
const aboveRight = previousRow[j]
|
const aboveRight = previousRow[j]
|
||||||
|
const below = nextRow[i]
|
||||||
|
|
||||||
if (current === T.treeCrown) {
|
if (current === T.treeCrown) {
|
||||||
if (i > 0) row[h] = T.treeLeaves
|
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
|
if (i < max) row[j] = T.treeLeaves
|
||||||
|
|
||||||
} else if (above === T.treeTrunk) {
|
} else if (above === T.treeTrunk) {
|
||||||
if (current === T.air) row[i] = T.treeTrunk
|
if (below === T.soil || below === T.grass) row[i] = T.treeRoot
|
||||||
else row[i] = T.treeRoot
|
else row[i] = T.treeTrunk
|
||||||
} else if (above === T.treeRoot) {
|
} else if (above === T.treeRoot) {
|
||||||
row[i] = T.soil
|
row[i] = T.soil
|
||||||
if (i > 0) row[h] = T.treeRoot
|
if (i > 0) row[h] = T.treeRoot
|
||||||
|
@ -44,7 +50,8 @@ function ground(r: number, i: number, current: Block, above: Block) {
|
||||||
return current
|
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 rootParts = [T.treeRootLeft, T.treeRootMiddle, T.treeRootRight]
|
||||||
const prevBlock = previousRow[i]
|
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
|
if (row[i] === T.soil) row[i] = T.grass
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
*/
|
||||||
|
|
||||||
function rock(r: number, i: number, current: Block, above: Block) {
|
function rock(r: number, i: number, current: Block, above: Block) {
|
||||||
if (above === T.soil && r < P.fray) return T.soil
|
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) {
|
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)
|
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) {
|
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)
|
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++) {
|
for (let column = 0; column < row.length; column++) {
|
||||||
if (level < L.ground) {
|
if (level < L.ground) {
|
||||||
// growTrees(level, column, columnOffset, row, previousRow)
|
growTrees(level, column, columnOffset, row, previousRow, nextRow)
|
||||||
} else {
|
} else {
|
||||||
row[column] = extendBlock(level, column, columnOffset, row[column], previousRow[column])
|
row[column] = extendBlock(level, column, columnOffset, row[column], previousRow[column])
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import type { NoiseFunction2D } from 'simplex-noise'
|
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) {
|
export default function createBlockGenerator(rand: NoiseFunction2D) {
|
||||||
// randomly generate a block
|
// 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 RECIPROCAL = 1 / BLOCK_SIZE
|
||||||
|
|
||||||
export const STAGE_WIDTH = 20 // 20*64 = 1280 pixel wide stage
|
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 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> = {
|
export const blockTypes: Record<BlockType, Block> = {
|
||||||
// Transparent Blocks
|
// Transparent Blocks
|
||||||
air: { type: 'air', hp: Infinity, walkable: true, transparent: true },
|
air: { type: 'air', hp: Infinity, walkable: true, transparent: true },
|
||||||
cave: { type: 'cave', hp: Infinity, walkable: true, transparent: true },
|
cave: { type: 'cave', hp: Infinity, walkable: true, transparent: true },
|
||||||
// Tree Parts
|
// Tree Parts
|
||||||
treeCrown: { type: 'treeCrown', hp: 1, walkable: true, transparent: true, drops: 'leaves' },
|
treeCrown: { type: 'treeCrown', hp: 1, walkable: true, transparent: true, drops: 'block_leaves' },
|
||||||
treeLeaves: { type: 'treeLeaves', hp: 1, walkable: true, transparent: true, drops: '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: 'wood' },
|
treeTrunk: { type: 'treeTrunk', hp: 10, walkable: true, climbable: true, transparent: true, drops: 'block_wood' },
|
||||||
treeRoot: { type: 'treeRoot', hp: 10, walkable: true, climbable: true, drops: 'wood' },
|
treeRoot: { type: 'treeRoot', hp: 10, walkable: true, climbable: true, drops: 'block_wood' },
|
||||||
// Opaque Natural Blocks
|
// Opaque Natural Blocks
|
||||||
grass: { type: 'grass', hp: 5, walkable: false, drops: 'dirt' },
|
grass: { type: 'grass', hp: 5, walkable: false, drops: 'block_dirt' },
|
||||||
soil: { type: 'soil', hp: 5, walkable: false, drops: 'dirt' },
|
soil: { type: 'soil', hp: 5, walkable: false, drops: 'block_dirt' },
|
||||||
soilGravel: { type: 'soilGravel', hp: 5, walkable: false, drops: 'gravel' },
|
soilGravel: { type: 'soilGravel', hp: 5, walkable: false, drops: 'block_gravel' },
|
||||||
stoneGravel: { type: 'stoneGravel', hp: 10, walkable: false, drops: 'gravel' },
|
stoneGravel: { type: 'stoneGravel', hp: 10, walkable: false, drops: 'block_gravel' },
|
||||||
stone: { type: 'stone', hp: 10, walkable: false, drops: 'stone' },
|
stone: { type: 'stone', hp: 10, walkable: false, drops: 'block_stone' },
|
||||||
bedrock: { type: 'bedrock', hp: 25, walkable: false, drops: 'stone' },
|
bedrock: { type: 'bedrock', hp: 25, walkable: false, drops: 'block_stone' },
|
||||||
// Built Blocks
|
// 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 = {
|
export const level = {
|
||||||
treeTop: 9,
|
treeTop: 9,
|
||||||
ground: 14,
|
ground: 14,
|
||||||
|
@ -55,7 +49,7 @@ export const level = {
|
||||||
}
|
}
|
||||||
|
|
||||||
export const probability = {
|
export const probability = {
|
||||||
tree: 0.3,
|
tree: 0.2,
|
||||||
soilHole: 0.3,
|
soilHole: 0.3,
|
||||||
soilGravel: 0.2,
|
soilGravel: 0.2,
|
||||||
stoneGravel: 0.1,
|
stoneGravel: 0.1,
|
||||||
|
|
|
@ -3,24 +3,10 @@ import { createNoise2D, type NoiseFunction2D } from 'simplex-noise'
|
||||||
import createBlockGenerator from './blockGen'
|
import createBlockGenerator from './blockGen'
|
||||||
import createBlockExtender from './blockExt'
|
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
|
const MAX_LIGHT = L.underground // maximum level where light shines
|
||||||
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
|
|
||||||
|
|
||||||
export default function createLevel(width: number, height: number, seed = 'extremely random seed') {
|
export default function createLevel(width: number, height: number, seed = 'extremely random seed') {
|
||||||
const prng = alea(seed)
|
const prng = alea(seed)
|
||||||
|
@ -50,7 +36,7 @@ export default function createLevel(width: number, height: number, seed = 'extre
|
||||||
if (changes) {
|
if (changes) {
|
||||||
const maxLevel = levelOffset + height
|
const maxLevel = levelOffset + height
|
||||||
changes.forEach(c => {
|
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]
|
_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
|
// 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
|
// until a block is generated that blocks light. The height of that block is
|
||||||
// stored in the lightBarrier list
|
// stored in the lightBarrier list
|
||||||
function calcLightBarrier(columnOffset: number) {
|
function calcLightBarrier(columnOffset: number): void {
|
||||||
let previousBlock: Block = T.air
|
let previousBlock: Block = T.air
|
||||||
|
|
||||||
for (let col = 0; col < width; col++) {
|
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]
|
const changes = _changes[columnOffset + col]
|
||||||
if (changes) {
|
if (changes) {
|
||||||
const change = changes.find(c => c.y === level)
|
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
|
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++) {
|
for (let i = 0; i < height; i++) {
|
||||||
const level = levelOffset + i
|
const level = levelOffset + i
|
||||||
const row: Block[] = Array(width)
|
const row: Block[] = Array(width)
|
||||||
const previousRow = i ? _grid[i-1] : [] as Block[]
|
const previousRow = i ? _grid[i-1] : [] as Block[]
|
||||||
|
const nextRow = _grid[i+1] ?? [] as Block[]
|
||||||
|
|
||||||
blockGen.fillRow(level, columnOffset, row)
|
blockGen.fillRow(level, columnOffset, row)
|
||||||
blockExt.extendRow(level, columnOffset, row, previousRow)
|
blockExt.extendRow(level, columnOffset, row, previousRow, nextRow)
|
||||||
|
|
||||||
_grid[i] = row
|
_grid[i] = row
|
||||||
}
|
}
|
||||||
applyPlayerChanges(columnOffset, levelOffset)
|
applyPlayerChanges(columnOffset, levelOffset)
|
||||||
}
|
}
|
||||||
|
|
||||||
function sunLight(columnOffset: number) {
|
function sunLight(columnOffset: number): number[] {
|
||||||
calcLightBarrier(columnOffset)
|
calcLightBarrier(columnOffset)
|
||||||
return _lightBarrier
|
return _lightBarrier
|
||||||
}
|
}
|
||||||
|
|
||||||
function grid(x: number, y: number) {
|
let lastGenX = 0
|
||||||
generate(x, y)
|
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
|
return _grid
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,58 +1,45 @@
|
||||||
import type { BlockType } from './def'
|
import type { ItemQuality, Item, InventoryItem } from '../types.d'
|
||||||
import type { InventoryItem } from '../util/usePlayer'
|
|
||||||
|
|
||||||
export type ItemQuality = 'wood' | 'iron' | 'silver' | 'gold' | 'diamond'
|
export const items = {
|
||||||
export type ItemType = 'tool' | 'weapon' | 'block' | 'ore'
|
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 =
|
tool_shovel_iron: { id: 'tool_shovel_iron', name: 'Iron Shovel', type: 'tool', icon: 'shovel', quality: 'iron' } as Item,
|
||||||
| 'Shovel' | 'Pick Axe' | 'Sword'
|
tool_pickaxe_iron: { id: 'tool_pickaxe_iron', name: 'Iron Pick Axe', type: 'tool', icon: 'pick', quality: 'iron' } as Item,
|
||||||
| 'leaves' | 'dirt' | 'wood' | 'stone' | 'gravel'
|
tool_sword_iron: { id: 'tool_sword_iron', name: 'Iron Sword', type: 'tool', icon: 'sword', quality: 'iron' } as Item,
|
||||||
| 'coal' | 'iron' | 'silver' | 'gold' | 'ruby' | 'diamond' | 'emerald'
|
|
||||||
|
|
||||||
export interface Item {
|
tool_shovel_diamond: { id: 'tool_shovel_diamond', name: 'Diamond Shovel', type: 'tool', icon: 'shovel', quality: 'diamond' } as Item,
|
||||||
name: DropItem
|
tool_pickaxe_diamond: { id: 'tool_pickaxe_diamond', name: 'Diamond Pick Axe', type: 'tool', icon: 'pick', quality: 'diamond' } as Item,
|
||||||
type: ItemType
|
tool_sword_diamond: { id: 'tool_sword_diamond', name: 'Diamond Sword', type: 'tool', icon: 'sword', quality: 'diamond' } as Item,
|
||||||
icon: string
|
|
||||||
hasQuality?: boolean
|
|
||||||
builds?: BlockType
|
|
||||||
}
|
|
||||||
|
|
||||||
export const items: Item[] = [
|
block_leaves: { id: 'block_leaves', name: 'leaves', type: 'block', icon: 'leaves', builds: 'treeLeaves' } as Item,
|
||||||
{ name: 'Shovel', type: 'tool', icon: 'shovel', hasQuality: true },
|
block_dirt: { id: 'block_dirt', name: 'dirt', type: 'block', icon: 'dirt', builds: 'soil' } as Item,
|
||||||
{ name: 'Pick Axe', type: 'tool', icon: 'pick', hasQuality: true },
|
block_wood: { id: 'block_wood', name: 'wood', type: 'block', icon: 'wood', builds: 'treeTrunk' } as Item,
|
||||||
{ name: 'Sword', type: 'weapon', icon: 'sword', hasQuality: true },
|
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' },
|
ore_coal: { id: 'ore_coal', name: 'coal', type: 'ore', icon: 'ore_coal' } as Item,
|
||||||
{ name: 'dirt', type: 'block', icon: 'dirt', builds: 'soil' },
|
ore_iron: { id: 'ore_iron', name: 'iron', type: 'ore', icon: 'ore_iron' } as Item,
|
||||||
{ name: 'wood', type: 'block', icon: 'wood', builds: 'treeTrunk' },
|
ore_silver: { id: 'ore_silver', name: 'silver', type: 'ore', icon: 'ore_silver' } as Item,
|
||||||
{ name: 'stone', type: 'block', icon: 'stone', builds: 'brickWall' },
|
ore_gold: { id: 'ore_gold', name: 'gold', type: 'ore', icon: 'ore_gold' } as Item,
|
||||||
{ name: 'gravel', type: 'block', icon: 'stone' }, // TODO
|
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' },
|
fixture_torch: { id: 'fixture_torch', name: 'Torch', type: 'fixture', icon: 'torch', builds: 'torch' } as Item,
|
||||||
{ name: 'iron', type: 'ore', icon: 'ore_iron' },
|
} as const
|
||||||
{ name: 'silver', type: 'ore', icon: 'ore_silver' },
|
|
||||||
{ name: 'gold', type: 'ore', icon: 'ore_gold' },
|
export type ItemId = keyof typeof items
|
||||||
{ name: 'ruby', type: 'ore', icon: 'ore_ruby' },
|
|
||||||
{ name: 'diamond', type: 'ore', icon: 'ore_diamond' },
|
|
||||||
{ name: 'emerald', type: 'ore', icon: 'ore_emerald' },
|
|
||||||
]
|
|
||||||
|
|
||||||
export const damage: Record<ItemQuality, number> = {
|
export const damage: Record<ItemQuality, number> = {
|
||||||
wood: 1,
|
wood: 1,
|
||||||
iron: 2,
|
iron: 3,
|
||||||
silver: 3,
|
diamond: 5,
|
||||||
gold: 5,
|
} as const
|
||||||
diamond: 8,
|
|
||||||
}
|
|
||||||
|
|
||||||
export function getItem(name: string, quality = null) {
|
export function getItem(id: ItemId) {
|
||||||
const item = items.find(i => i.name === name)
|
return items[id]
|
||||||
if (item) {
|
|
||||||
return {
|
|
||||||
...item,
|
|
||||||
quality,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getItemClass(item: InventoryItem) {
|
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 { createApp } from "vue"
|
||||||
import "./assets/field.css";
|
import "./assets/field.css"
|
||||||
import "./assets/player.css";
|
import "./assets/player.css"
|
||||||
import "./assets/items.css";
|
import "./assets/items.css"
|
||||||
import App from "./App.vue";
|
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">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onUnmounted } from 'vue'
|
import { ref, computed, onUnmounted } from 'vue'
|
||||||
import { getItemClass } from '../level/items'
|
import { getItemClass } from '../level/items'
|
||||||
import type { InventoryItem } from '../util/usePlayer'
|
import type { InventoryItem } from '../types.d'
|
||||||
|
|
||||||
export interface Props {
|
export interface Props {
|
||||||
items: InventoryItem[]
|
items: InventoryItem[]
|
||||||
|
@ -10,7 +10,7 @@ export interface Props {
|
||||||
|
|
||||||
const props = defineProps<Props>()
|
const props = defineProps<Props>()
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
(event: 'selection', value: InventoryItem | null): void
|
(event: 'selection', value: InventoryItem): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
// inventory size is 15
|
// 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"
|
* @param r: number [44] - the radius of the "sun"
|
||||||
* @returns emissionStrength: number - emission intensity blends over the mountains
|
* @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 w = ctx.canvas.width
|
||||||
const h = ctx.canvas.height
|
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)
|
if (sunY > -30) emissionStrength -= Math.max((30 + sunY) / 5, 0.0)
|
||||||
else if (sunY < -60) emissionStrength += Math.min(1 + (60 + 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).
|
// 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).
|
emissionGradient.addColorStop(.1, hsl(30, 50, 3.1 * emissionStrength))
|
||||||
// Now paint the gradient all over our godrays canvas.
|
emissionGradient.addColorStop(.2, hsl(12, 71, 1.4 * emissionStrength))
|
||||||
|
|
||||||
|
// Now paint the gradient all over our godrays canvas
|
||||||
ctx.fillRect(0, 0, w, h)
|
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'
|
ctx.fillStyle = '#000'
|
||||||
|
|
||||||
return emissionStrength
|
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
|
* Mountains are made by summing up sine waves with varying frequencies and amplitudes
|
||||||
* The frequencies are prime, to avoid extra repetitions
|
* The frequencies are prime, to avoid extra repetitions
|
||||||
*/
|
*/
|
||||||
function calcMountainHeight(pos: number, roughness: number, frequencies = [1721, 947, 547, 233, 73, 31, 7]) {
|
function calcMountainHeight(
|
||||||
return frequencies.reduce((height, freq) => height * roughness - Math.cos(freq * pos), 0)
|
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 layers: number - amount of mountain layers for parallax effect
|
||||||
* @param emissionStrength: number - intensity of the godrays
|
* @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 w = ctx.canvas.width
|
||||||
const h = ctx.canvas.height
|
const h = ctx.canvas.height
|
||||||
const grDiv = w / grCtx.canvas.width
|
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 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?
|
* @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.width = w
|
||||||
canvasEl.height = h
|
canvasEl.height = h
|
||||||
|
|
||||||
const grW = w / rayQuality
|
const grW = w / rayQuality
|
||||||
const grH = h / 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 grCanvasEl = document.createElement('canvas')
|
||||||
|
|
||||||
|
const ctx = canvasEl.getContext('2d')
|
||||||
const grCtx = grCanvasEl.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.width = grW
|
||||||
grCanvasEl.height = grH
|
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
|
* @param sunY: number - the position (height) of the sun in the sky
|
||||||
*/
|
*/
|
||||||
return function drawFrame (frame: number, sunY: number) {
|
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)
|
const emissionStrength = renderGodrays(grCtx, sunCenterX, sunCenterY, sunY)
|
||||||
renderSky(ctx, sunY)
|
renderSky(ctx, sunY)
|
||||||
renderMountains(ctx, grCtx, frame, sunY, mountainLayers, emissionStrength)
|
renderMountains(ctx, grCtx, frame, sunY, mountainLayers, emissionStrength)
|
||||||
|
|
|
@ -28,18 +28,18 @@ export default function useInput() {
|
||||||
inputX.value = -1
|
inputX.value = -1
|
||||||
break
|
break
|
||||||
case 'p':
|
case 'p':
|
||||||
if (!help.value) paused.value = !paused.value
|
if (!help.value && !inventory.value) {
|
||||||
if (wasPaused && !paused.value) wasPaused = false
|
paused.value = !paused.value
|
||||||
|
wasPaused = paused.value
|
||||||
|
}
|
||||||
break
|
break
|
||||||
case '?':
|
case '?':
|
||||||
if (paused.value && !help.value) wasPaused = true
|
if (!wasPaused) paused.value = !help.value
|
||||||
help.value = !help.value
|
help.value = !help.value
|
||||||
paused.value = help.value || wasPaused
|
|
||||||
break
|
break
|
||||||
case 'i':
|
case 'i':
|
||||||
if (paused.value && !inventory.value) wasPaused = true
|
if (!wasPaused) paused.value = !inventory.value
|
||||||
inventory.value = !inventory.value
|
inventory.value = !inventory.value
|
||||||
paused.value = inventory.value || wasPaused
|
|
||||||
break
|
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 { computed, reactive } from 'vue'
|
||||||
import { RECIPROCAL, STAGE_WIDTH, STAGE_HEIGHT } from '../level/def'
|
import { RECIPROCAL, STAGE_WIDTH, STAGE_HEIGHT } from '../level/def'
|
||||||
import type { Item, ItemQuality } from '../level/items'
|
import type { Item, Player } from '../types.d'
|
||||||
|
|
||||||
export interface InventoryItem extends Item {
|
|
||||||
amount: number
|
|
||||||
quality: ItemQuality | null
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Player extends Moveable {
|
|
||||||
inventory: InventoryItem[]
|
|
||||||
}
|
|
||||||
|
|
||||||
const player = reactive<Player>({
|
const player = reactive<Player>({
|
||||||
x: Math.round((STAGE_WIDTH + 2) / 2),
|
x: Math.round((STAGE_WIDTH + 2) / 2),
|
||||||
|
@ -17,10 +8,10 @@ const player = reactive<Player>({
|
||||||
lastDir: 0,
|
lastDir: 0,
|
||||||
vx: 0,
|
vx: 0,
|
||||||
vy: 1, // always falling, because of gravity
|
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)
|
const existing = player.inventory.find(item => item.name === newItem.name)
|
||||||
|
|
||||||
if (existing) {
|
if (existing) {
|
||||||
|
@ -28,9 +19,8 @@ const pocket = (newItem: Item) => {
|
||||||
return existing.amount
|
return existing.amount
|
||||||
}
|
}
|
||||||
player.inventory.push({
|
player.inventory.push({
|
||||||
quality: null,
|
...newItem,
|
||||||
amount: 1,
|
amount,
|
||||||
...newItem
|
|
||||||
})
|
})
|
||||||
return 1
|
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'
|
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() {
|
export default function useTime() {
|
||||||
// the day is split in 1000 parts, so we start in the morning
|
// the day is split in 1000 parts, so we start in the morning
|
||||||
const time = ref(230)
|
const time = ref(240)
|
||||||
|
|
||||||
function updateTime() {
|
function updateTime() {
|
||||||
time.value = (time.value + 0.1) % 1000
|
time.value = (time.value + 0.1) % 1000
|
||||||
}
|
}
|
||||||
|
|
||||||
const timeOfDay = computed(() => {
|
const timeOfDay = computed(() => calcTimeOfDay(time.value))
|
||||||
if (time.value >= 900 || time.value < 80) return 'night'
|
const clock = computed(() => renderClock(time.value))
|
||||||
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}`
|
|
||||||
})
|
|
||||||
|
|
||||||
return { time, updateTime, timeOfDay, clock }
|
return { time, updateTime, timeOfDay, clock }
|
||||||
}
|
}
|
||||||
|
|
12
src/vite-env.d.ts
vendored
|
@ -1,18 +1,6 @@
|
||||||
/// <reference types="vite/client" />
|
/// <reference types="vite/client" />
|
||||||
|
|
||||||
declare global {
|
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 {}
|
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": {
|
"files": [],
|
||||||
"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",
|
|
||||||
],
|
|
||||||
"references": [
|
"references": [
|
||||||
{
|
{
|
||||||
"path": "./tsconfig.node.json"
|
"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": {
|
"compilerOptions": {
|
||||||
"composite": true,
|
"noEmit": true,
|
||||||
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
|
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"moduleResolution": "Node",
|
"moduleResolution": "Bundler",
|
||||||
"allowSyntheticDefaultImports": true
|
"types": ["node"]
|
||||||
},
|
}
|
||||||
"include": ["vite.config.ts"]
|
|
||||||
}
|
}
|
||||||
|
|
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 { fileURLToPath, URL } from 'node:url'
|
||||||
import VueMacros from 'unplugin-vue-macros/vite'
|
|
||||||
import Vue from '@vitejs/plugin-vue'
|
|
||||||
|
|
||||||
// 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({
|
export default defineConfig({
|
||||||
plugins: [
|
plugins: [
|
||||||
VueMacros({
|
vue(),
|
||||||
plugins: {
|
vueDevTools(),
|
||||||
vue: Vue(),
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
],
|
],
|
||||||
|
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)),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|