Compare commits
No commits in common. "2025" and "main" have entirely different histories.
|
@ -1,9 +1,9 @@
|
|||
[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue,css,scss,sass,less,styl}]
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
indent_size = 2
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
end_of_line = lf
|
||||
max_line_length = 100
|
||||
|
|
48
.eslintrc.cjs
Normal file
|
@ -0,0 +1,48 @@
|
|||
/* 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
|
@ -1 +0,0 @@
|
|||
* text=auto eol=lf
|
9
.gitignore
vendored
|
@ -8,23 +8,20 @@ pnpm-debug.log*
|
|||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
|
||||
/cypress/videos/
|
||||
/cypress/screenshots/
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
*.tsbuildinfo
|
||||
# old source code for reference?!
|
||||
src/old
|
||||
|
|
4
.prettierrc
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"semi": false,
|
||||
"singleQuote": true
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
{
|
||||
"$schema": "https://json.schemastore.org/prettierrc",
|
||||
"semi": false,
|
||||
"singleQuote": true,
|
||||
"printWidth": 100
|
||||
}
|
9
.vscode/extensions.json
vendored
|
@ -1,9 +0,0 @@
|
|||
{
|
||||
"recommendations": [
|
||||
"Vue.volar",
|
||||
"vitest.explorer",
|
||||
"dbaeumer.vscode-eslint",
|
||||
"EditorConfig.EditorConfig",
|
||||
"esbenp.prettier-vscode"
|
||||
]
|
||||
}
|
48
README.md
|
@ -1,45 +1,29 @@
|
|||
# vue-shovel
|
||||
# building-game
|
||||
|
||||
This template should help get you started developing with Vue 3 in Vite.
|
||||
> A blocky, side-scrolling, building and exploration game
|
||||
|
||||
## Recommended IDE Setup
|
||||
This version of DIG! is reimplemented with Vue3 and Typescript. To see the old (and probably broken) version, check the vue2 branch.
|
||||
|
||||
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
|
||||
## Build Setup
|
||||
|
||||
## Type Support for `.vue` Imports in TS
|
||||
``` bash
|
||||
# install dependencies
|
||||
yarn
|
||||
|
||||
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 [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) to make the TypeScript language service aware of `.vue` types.
|
||||
# serve with hot reload at localhost:8080
|
||||
yarn dev
|
||||
|
||||
## Customize configuration
|
||||
|
||||
See [Vite Configuration Reference](https://vite.dev/config/).
|
||||
|
||||
## Project Setup
|
||||
|
||||
```sh
|
||||
pnpm install
|
||||
# build for production with minification
|
||||
yarn build
|
||||
```
|
||||
|
||||
### Compile and Hot-Reload for Development
|
||||
|
||||
```sh
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
### Type-Check, Compile and Minify for Production
|
||||
## Credits
|
||||
|
||||
```sh
|
||||
pnpm build
|
||||
```
|
||||
Art, Music and all from [OpenGameArt](https://opengameart.org/). More specifically:
|
||||
|
||||
### Run Unit Tests with [Vitest](https://vitest.dev/)
|
||||
* https://opengameart.org/content/voxel-pack (Kenneys Game Art is just incredible!)
|
||||
* https://opengameart.org/content/oves-essential-game-audio-pack-collection-160-files-updated
|
||||
|
||||
```sh
|
||||
pnpm test:unit
|
||||
```
|
||||
|
||||
### Lint with [ESLint](https://eslint.org/)
|
||||
|
||||
```sh
|
||||
pnpm lint
|
||||
```
|
||||
and partially adapted to my needs.
|
||||
|
|
BIN
dist/bedrock.png
vendored
Normal file
After Width: | Height: | Size: 1.2 KiB |
7
dist/build.js
vendored
Normal file
1
dist/build.js.map
vendored
Normal file
BIN
dist/dwarf_left.png
vendored
Normal file
After Width: | Height: | Size: 607 B |
BIN
dist/dwarf_right.png
vendored
Normal file
After Width: | Height: | Size: 629 B |
BIN
dist/grass01.png
vendored
Normal file
After Width: | Height: | Size: 614 B |
BIN
dist/rock.png
vendored
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
dist/soil01.png
vendored
Normal file
After Width: | Height: | Size: 586 B |
BIN
dist/soil_gravel01.png
vendored
Normal file
After Width: | Height: | Size: 940 B |
BIN
dist/tree_crown_left.png
vendored
Normal file
After Width: | Height: | Size: 719 B |
BIN
dist/tree_crown_left_mixed.png
vendored
Normal file
After Width: | Height: | Size: 1.4 KiB |
BIN
dist/tree_crown_middle.png
vendored
Normal file
After Width: | Height: | Size: 1.5 KiB |
BIN
dist/tree_crown_right.png
vendored
Normal file
After Width: | Height: | Size: 1 KiB |
BIN
dist/tree_crown_right_mixed.png
vendored
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
dist/tree_root_left.png
vendored
Normal file
After Width: | Height: | Size: 340 B |
BIN
dist/tree_root_left_mixed.png
vendored
Normal file
After Width: | Height: | Size: 539 B |
BIN
dist/tree_root_middle.png
vendored
Normal file
After Width: | Height: | Size: 938 B |
BIN
dist/tree_root_right.png
vendored
Normal file
After Width: | Height: | Size: 280 B |
BIN
dist/tree_root_right_mixed.png
vendored
Normal file
After Width: | Height: | Size: 539 B |
BIN
dist/tree_top_left.png
vendored
Normal file
After Width: | Height: | Size: 139 B |
BIN
dist/tree_top_left_mixed.png
vendored
Normal file
After Width: | Height: | Size: 257 B |
BIN
dist/tree_top_middle.png
vendored
Normal file
After Width: | Height: | Size: 747 B |
BIN
dist/tree_top_right.png
vendored
Normal file
After Width: | Height: | Size: 205 B |
BIN
dist/tree_top_right_mixed.png
vendored
Normal file
After Width: | Height: | Size: 257 B |
BIN
dist/tree_trunk_left.png
vendored
Normal file
After Width: | Height: | Size: 735 B |
BIN
dist/tree_trunk_left_mixed.png
vendored
Normal file
After Width: | Height: | Size: 1.1 KiB |
BIN
dist/tree_trunk_middle.png
vendored
Normal file
After Width: | Height: | Size: 1.2 KiB |
BIN
dist/tree_trunk_right.png
vendored
Normal file
After Width: | Height: | Size: 750 B |
BIN
dist/tree_trunk_right_mixed.png
vendored
Normal file
After Width: | Height: | Size: 1.1 KiB |
7
env.d.ts
vendored
|
@ -1 +1,8 @@
|
|||
/// <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;
|
||||
}
|
||||
|
|
|
@ -1,32 +0,0 @@
|
|||
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
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
|
||||
}
|
18
foo/README.md
Normal file
|
@ -0,0 +1,18 @@
|
|||
# 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.
|
20
foo/package.json
Normal file
|
@ -0,0 +1,20 @@
|
|||
{
|
||||
"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
foo/public/vite.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<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>
|
After Width: | Height: | Size: 1.5 KiB |
30
foo/src/App.vue
Normal file
|
@ -0,0 +1,30 @@
|
|||
<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
foo/src/assets/vue.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<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>
|
After Width: | Height: | Size: 496 B |
38
foo/src/components/HelloWorld.vue
Normal file
|
@ -0,0 +1,38 @@
|
|||
<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>
|
80
foo/src/style.css
Normal file
|
@ -0,0 +1,80 @@
|
|||
: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,8 +1,7 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="">
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<link rel="icon" href="/favicon.ico">
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/png" href="/favicon.png" />
|
||||
<link rel="preload" as="image" href="/Tiles/dirt.png" />
|
||||
<link rel="preload" as="image" href="/Tiles/dirt_grass.png" />
|
||||
|
@ -16,8 +15,8 @@
|
|||
<link rel="preload" as="image" href="/Tiles/trunk_mid.png" />
|
||||
<link rel="preload" as="image" href="/Tiles/trunk_bottom.png" />
|
||||
<link rel="preload" as="image" href="/Tiles/leaves_transparent.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Vue Shovel</title>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Dig!</title>
|
||||
<style>
|
||||
:root {
|
||||
--block-size: 64px;
|
||||
|
|
62
package.json
|
@ -1,55 +1,33 @@
|
|||
{
|
||||
"name": "vue-shovel",
|
||||
"version": "0.0.0",
|
||||
"name": "DIG",
|
||||
"description": "A blocky, side-scrolling, digging and exploration game",
|
||||
"version": "0.0.1",
|
||||
"author": "koehr <n@koehr.in>",
|
||||
"license": "MIT",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "run-p type-check \"build-only {@}\" --",
|
||||
"build": "vue-tsc && vite build",
|
||||
"preview": "vite preview",
|
||||
"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/"
|
||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix"
|
||||
},
|
||||
"dependencies": {
|
||||
"alea": "^1.0.1",
|
||||
"simplex-noise": "^4.0.3",
|
||||
"vue": "^3.5.13"
|
||||
"simplex-noise": "^4.0.1",
|
||||
"vue": "^3.4.22"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tsconfig/node22": "^22.0.0",
|
||||
"@types/jsdom": "^21.1.7",
|
||||
"@types/node": "^22.13.9",
|
||||
"@vitejs/plugin-vue": "^5.2.1",
|
||||
"@vitest/eslint-plugin": "^1.1.36",
|
||||
"@vue/eslint-config-prettier": "^10.2.0",
|
||||
"@vue/eslint-config-typescript": "^14.5.0",
|
||||
"@vue/test-utils": "^2.4.6",
|
||||
"@vue/tsconfig": "^0.7.0",
|
||||
"eslint": "^9.21.0",
|
||||
"eslint-plugin-oxlint": "^0.15.13",
|
||||
"eslint-plugin-vue": "~10.0.0",
|
||||
"jiti": "^2.4.2",
|
||||
"jsdom": "^26.0.0",
|
||||
"npm-run-all2": "^7.0.2",
|
||||
"oxlint": "^0.15.13",
|
||||
"prettier": "3.5.3",
|
||||
"typescript": "~5.8.0",
|
||||
"vite": "^6.2.1",
|
||||
"vite-plugin-vue-devtools": "^7.7.2",
|
||||
"vitest": "^3.0.8",
|
||||
"vue-tsc": "^2.2.8"
|
||||
},
|
||||
"pnpm": {
|
||||
"ignoredBuiltDependencies": [
|
||||
"esbuild"
|
||||
],
|
||||
"onlyBuiltDependencies": [
|
||||
"esbuild"
|
||||
]
|
||||
"@rushstack/eslint-patch": "^1.10.2",
|
||||
"@typescript-eslint/parser": "^7.7.0",
|
||||
"@vitejs/plugin-vue": "^5.0.4",
|
||||
"@vue/eslint-config-prettier": "^9.0.0",
|
||||
"@vue/eslint-config-typescript": "^13.0.0",
|
||||
"eslint": "^9.0.0",
|
||||
"eslint-plugin-vue": "^9.25.0",
|
||||
"typescript": "^5.4.5",
|
||||
"unplugin-vue-macros": "^2.9.1",
|
||||
"vite": "^5.2.9",
|
||||
"vue-tsc": "^2.0.13"
|
||||
}
|
||||
}
|
||||
|
|
4232
pnpm-lock.yaml
generated
14
simplex.html
Normal file
|
@ -0,0 +1,14 @@
|
|||
<!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>
|
91
src/App.vue
|
@ -1,5 +1,5 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, computed, watch, onMounted } from 'vue'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import Help from './screens/help.vue'
|
||||
import Inventory from './screens/inventory.vue'
|
||||
|
||||
|
@ -31,23 +31,10 @@ const x = ref(0)
|
|||
const y = ref(0)
|
||||
const floorX = computed(() => Math.floor(x.value))
|
||||
const floorY = computed(() => Math.floor(y.value))
|
||||
const fracX = computed(() => x.value - floorX.value)
|
||||
const fracY = computed(() => y.value - floorY.value)
|
||||
const tx = computed(() => fracX.value * -BLOCK_SIZE)
|
||||
const ty = computed(() => fracY.value * -BLOCK_SIZE)
|
||||
const px = computed(() => Math.round(player.x + fracX.value))
|
||||
const py = computed(() => Math.round(player.y + fracY.value))
|
||||
|
||||
const lightBarrier = computed<number[]>(() => {
|
||||
const _update = mapUpdateCount.value // reactivity trigger
|
||||
return level.sunLight(floorX.value)
|
||||
})
|
||||
|
||||
const mapUpdateCount = ref(0)
|
||||
const mapGrid = computed<Block[][]>(() => {
|
||||
const _update = mapUpdateCount.value // reactivity trigger
|
||||
return level.grid(floorX.value, floorY.value, true)
|
||||
})
|
||||
const tx = computed(() => (x.value - floorX.value) * -BLOCK_SIZE)
|
||||
const ty = computed(() => (y.value - floorY.value) * -BLOCK_SIZE)
|
||||
const rows = computed(() => level.grid(floorX.value, floorY.value))
|
||||
const lightBarrier = computed(() => level.sunLight(floorX.value))
|
||||
|
||||
const arriving = ref(true)
|
||||
const walking = ref(false)
|
||||
|
@ -61,32 +48,25 @@ type Surroundings = {
|
|||
down: Block,
|
||||
}
|
||||
const surroundings = computed<Surroundings>(() => {
|
||||
const _update = mapUpdateCount.value // reactivity trigger
|
||||
const x = px.value
|
||||
const y = py.value
|
||||
const rows = mapGrid.value
|
||||
|
||||
const rowY = rows[y]
|
||||
const rowYp = rows[y - 1]
|
||||
const rowYn = rows[y + 1]
|
||||
const px = player.x
|
||||
const py = player.y
|
||||
const row = rows.value
|
||||
|
||||
return {
|
||||
at: rowY[x],
|
||||
left: rowY[x - 1],
|
||||
right: rowY[x + 1],
|
||||
up: rowYp[x],
|
||||
down: rowYn[x],
|
||||
at: row[py][px],
|
||||
left: row[py][px - 1],
|
||||
right: row[py][px + 1],
|
||||
up: row[py - 1][px],
|
||||
down: row[py + 1][px],
|
||||
}
|
||||
})
|
||||
const blocked = computed(() => {
|
||||
const { left, right, up, down } = surroundings.value
|
||||
const fx = fracX.value
|
||||
const fy = fracY.value
|
||||
return {
|
||||
left: !left.walkable && fx < 0.8 && fx > 0.7,
|
||||
right: !right.walkable && fx > 0.2 && fx < 0.3,
|
||||
left: !left.walkable,
|
||||
right: !right.walkable,
|
||||
up: !up.walkable,
|
||||
down: !down.walkable && fy > 0.8,
|
||||
down: !down.walkable,
|
||||
}
|
||||
})
|
||||
|
||||
|
@ -104,7 +84,6 @@ function dig(blockX: number, blockY: number, block: Block) {
|
|||
y: floorY.value + blockY,
|
||||
newType: 'air'
|
||||
})
|
||||
mapUpdateCount.value = mapUpdateCount.value + 1
|
||||
|
||||
// anything to pick up?
|
||||
if (block.drops) {
|
||||
|
@ -124,14 +103,12 @@ function build(blockX: number, blockY: number, block: InventoryItem) {
|
|||
y: floorY.value + blockY,
|
||||
newType: blockToBuild
|
||||
})
|
||||
mapUpdateCount.value = mapUpdateCount.value + 1
|
||||
|
||||
const newAmount = unpocket(block)
|
||||
if (newAmount < 1) inventorySelection.value = player.inventory[0]
|
||||
}
|
||||
|
||||
function interactWith(blockX: number, blockY: number, block: Block) {
|
||||
console.log('interact with', blockX, blockY, block.type)
|
||||
// § 4 ArbZG
|
||||
if (paused.value) return
|
||||
|
||||
|
@ -146,6 +123,12 @@ function interactWith(blockX: number, blockY: number, block: Block) {
|
|||
} else if (toolInHand && !emptyBlock) {
|
||||
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
|
||||
|
@ -213,16 +196,12 @@ function selectTool(item: InventoryItem) {
|
|||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (lightMapEl.value) {
|
||||
const canvas = lightMapEl.value
|
||||
canvas.height = (BLOCK_SIZE + 2) * STAGE_HEIGHT
|
||||
canvas.width = (BLOCK_SIZE + 2) * STAGE_WIDTH
|
||||
const ctx = canvas.getContext('2d')!
|
||||
updateLightMap = useLightMap(ctx, floorX, floorY, tx, ty, time, lightBarrier)
|
||||
} else {
|
||||
console.warn('lightmap deactivated')
|
||||
updateLightMap = (() => {}) as ReturnType<typeof useLightMap>
|
||||
}
|
||||
const canvas = lightMapEl.value!
|
||||
canvas.height = (BLOCK_SIZE + 2) * STAGE_HEIGHT
|
||||
canvas.width = (BLOCK_SIZE + 2) * STAGE_WIDTH
|
||||
const ctx = canvas.getContext('2d')!
|
||||
|
||||
updateLightMap = useLightMap(ctx, floorX, floorY, tx, ty, time, lightBarrier)
|
||||
lastTick = performance.now()
|
||||
move(lastTick)
|
||||
})
|
||||
|
@ -230,19 +209,19 @@ onMounted(() => {
|
|||
|
||||
<template>
|
||||
<div id="field" :class="timeOfDay">
|
||||
|
||||
<div id="blocks" :style="{transform: `translate(${tx}px, ${ty}px)`}">
|
||||
<template v-for="(row, y) in mapGrid">
|
||||
<template v-for="(row, y) in rows">
|
||||
<div v-for="(block, x) in row"
|
||||
:class="['block', block.type, { highlight: x === px && y === py }]"
|
||||
:class="['block', block.type, {
|
||||
highlight: x === player.x && y == player.y
|
||||
}]"
|
||||
@click="interactWith(x, y, block)"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div id="player"
|
||||
:class="[direction, { walking, running, sitting: paused }]"
|
||||
@click="inventory = !inventory"
|
||||
>
|
||||
<div id="player" :class="[direction, { walking }]" @click="inventory = !inventory">
|
||||
<div class="head"></div>
|
||||
<div class="body"></div>
|
||||
<div class="legs">
|
||||
|
@ -262,8 +241,6 @@ onMounted(() => {
|
|||
x:{{ floorX }}, y:{{ floorY }}
|
||||
<template v-if="paused">(PAUSED)</template>
|
||||
<template v-else>({{ clock }})</template>
|
||||
<br/>
|
||||
Map Changes: {{ mapUpdateCount }}
|
||||
</div>
|
||||
|
||||
<Inventory :shown="inventory"
|
||||
|
|
|
@ -101,7 +101,7 @@
|
|||
|
||||
#field .block:hover,
|
||||
#field .block.block.highlight {
|
||||
filter: brightness(1.2) saturate(1.2);
|
||||
filter: brightness(1.2) grayscale(1.0);
|
||||
outline: 1px solid white;
|
||||
z-index: 10;
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
--player-height: 76px;
|
||||
position: absolute;
|
||||
left: calc(var(--field-width) / 2);
|
||||
top: calc(var(--field-height) / 2);
|
||||
top: calc(var(--field-height) / 2 - 10px);
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
width: var(--player-width);
|
||||
|
@ -58,21 +58,6 @@
|
|||
#player.walking > .legs > div.left {
|
||||
animation: dangle .3s linear infinite alternate;
|
||||
}
|
||||
#player.running > .legs > div.right {
|
||||
animation: gillop .2s linear infinite alternate;
|
||||
}
|
||||
#player.running > .legs > div.left {
|
||||
animation: gallop .2s linear infinite alternate;
|
||||
}
|
||||
#player.sitting {
|
||||
transform: translateY(10px);
|
||||
}
|
||||
#player.right.sitting {
|
||||
transform: scaleX(-1) translateY(10px);
|
||||
}
|
||||
#player.sitting > .legs > div {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
#player > .arms {
|
||||
position: absolute;
|
||||
width: 8px;
|
||||
|
@ -85,9 +70,6 @@
|
|||
#player.walking > .arms {
|
||||
animation: dangle .3s linear infinite alternate;
|
||||
}
|
||||
#player.running > .arms {
|
||||
animation: gallop .2s linear infinite alternate;
|
||||
}
|
||||
#player > .arms > .item {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
|
@ -107,14 +89,6 @@
|
|||
from { transform: rotate(-20deg); }
|
||||
to { transform: rotate(20deg); }
|
||||
}
|
||||
@keyframes gillop {
|
||||
from { transform: rotate(35deg); }
|
||||
to { transform: rotate(-35deg); }
|
||||
}
|
||||
@keyframes gallop {
|
||||
from { transform: rotate(-35deg); }
|
||||
to { transform: rotate(35deg); }
|
||||
}
|
||||
@keyframes pulse {
|
||||
from { opacity: .3; }
|
||||
to { opacity: 1.0; }
|
||||
|
|
|
@ -60,7 +60,7 @@ export default function createLevel(width: number, height: number, seed = 'extre
|
|||
// takes the current columnOffset and generates all blocks from the very top
|
||||
// until a block is generated that blocks light. The height of that block is
|
||||
// stored in the lightBarrier list
|
||||
function calcLightBarrier(columnOffset: number): void {
|
||||
function calcLightBarrier(columnOffset: number) {
|
||||
let previousBlock: Block = T.air
|
||||
|
||||
for (let col = 0; col < width; col++) {
|
||||
|
@ -84,7 +84,7 @@ export default function createLevel(width: number, height: number, seed = 'extre
|
|||
}
|
||||
}
|
||||
|
||||
function generate(columnOffset: number, levelOffset: number): void {
|
||||
function generate(columnOffset: number, levelOffset: number) {
|
||||
for (let i = 0; i < height; i++) {
|
||||
const level = levelOffset + i
|
||||
const row: Block[] = Array(width)
|
||||
|
@ -98,21 +98,13 @@ export default function createLevel(width: number, height: number, seed = 'extre
|
|||
applyPlayerChanges(columnOffset, levelOffset)
|
||||
}
|
||||
|
||||
function sunLight(columnOffset: number): number[] {
|
||||
function sunLight(columnOffset: number) {
|
||||
calcLightBarrier(columnOffset)
|
||||
return _lightBarrier
|
||||
}
|
||||
|
||||
let lastGenX = 0
|
||||
let lastGenY = 0
|
||||
generate(0, 0)
|
||||
|
||||
function grid(x: number, y: number, force: false): Block[][] {
|
||||
if (force || lastGenX !== x || lastGenY !== y) {
|
||||
generate(x, y)
|
||||
lastGenX = x
|
||||
lastGenY = y
|
||||
}
|
||||
function grid(x: number, y: number) {
|
||||
generate(x, y)
|
||||
return _grid
|
||||
}
|
||||
|
||||
|
|
12
src/main.ts
|
@ -1,7 +1,7 @@
|
|||
import { createApp } from "vue"
|
||||
import "./assets/field.css"
|
||||
import "./assets/player.css"
|
||||
import "./assets/items.css"
|
||||
import App from "./App.vue"
|
||||
import { createApp } from "vue";
|
||||
import "./assets/field.css";
|
||||
import "./assets/player.css";
|
||||
import "./assets/items.css";
|
||||
import App from "./App.vue";
|
||||
|
||||
createApp(App).mount("#app")
|
||||
createApp(App).mount("#app");
|
||||
|
|
|
@ -1,32 +0,0 @@
|
|||
<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>
|
|
@ -1,72 +0,0 @@
|
|||
<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>
|
|
@ -1,35 +0,0 @@
|
|||
<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>
|
|
@ -1,216 +0,0 @@
|
|||
<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>
|
|
@ -1,66 +0,0 @@
|
|||
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
|
||||
}
|
|
@ -1,58 +0,0 @@
|
|||
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
|
||||
}
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,70 +0,0 @@
|
|||
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
|
||||
}
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
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)
|
||||
})
|
|
@ -1,22 +0,0 @@
|
|||
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'
|
||||
}
|
||||
}
|
|
@ -1,130 +0,0 @@
|
|||
/* 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);
|
||||
}
|
54
src/types.d.ts
vendored
|
@ -1,54 +0,0 @@
|
|||
export type ItemQuality = 'wood' | 'iron' | 'silver' | 'gold' | 'diamond'
|
||||
export type ItemType = 'tool' | 'weapon' | 'block' | 'ore'
|
||||
|
||||
export type DropItem =
|
||||
| 'Shovel' | 'Pick Axe' | 'Sword'
|
||||
| 'leaves' | 'dirt' | 'wood' | 'stone' | 'gravel'
|
||||
| 'coal' | 'iron' | 'silver' | 'gold' | 'ruby' | 'diamond' | 'emerald'
|
||||
|
||||
export interface Item {
|
||||
name: DropItem
|
||||
type: ItemType
|
||||
icon: string
|
||||
hasQuality?: boolean
|
||||
builds?: BlockType
|
||||
}
|
||||
|
||||
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 interface InventoryItem extends Item {
|
||||
amount: number
|
||||
quality: ItemQuality | null
|
||||
}
|
||||
|
||||
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[]
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
{
|
||||
"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,14 +1,36 @@
|
|||
{
|
||||
"files": [],
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"useDefineForClassFields": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Node",
|
||||
"strict": true,
|
||||
"jsx": "preserve",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"esModuleInterop": true,
|
||||
"lib": [
|
||||
"ESNext",
|
||||
"DOM"
|
||||
],
|
||||
"skipLibCheck": true,
|
||||
"noEmit": true,
|
||||
"types": [
|
||||
"unplugin-vue-macros/macros-global"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/**/*.d.ts",
|
||||
"src/**/*.tsx",
|
||||
"src/**/*.vue"
|
||||
],
|
||||
"exclude": [
|
||||
"src/old",
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "./tsconfig.node.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.app.json"
|
||||
},
|
||||
{
|
||||
"path": "./tsconfig.vitest.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -1,19 +1,9 @@
|
|||
{
|
||||
"extends": "@tsconfig/node22/tsconfig.json",
|
||||
"include": [
|
||||
"vite.config.*",
|
||||
"vitest.config.*",
|
||||
"cypress.config.*",
|
||||
"nightwatch.conf.*",
|
||||
"playwright.config.*",
|
||||
"eslint.config.*"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"noEmit": true,
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
|
||||
"composite": true,
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"types": ["node"]
|
||||
}
|
||||
"moduleResolution": "Node",
|
||||
"allowSyntheticDefaultImports": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
|
|
|
@ -1,11 +0,0 @@
|
|||
{
|
||||
"extends": "./tsconfig.app.json",
|
||||
"include": ["src/**/__tests__/*", "env.d.ts"],
|
||||
"exclude": [],
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.vitest.tsbuildinfo",
|
||||
|
||||
"lib": [],
|
||||
"types": ["node", "jsdom"]
|
||||
}
|
||||
}
|
|
@ -1,21 +1,14 @@
|
|||
import { fileURLToPath, URL } from 'node:url'
|
||||
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import vueDevTools from 'vite-plugin-vue-devtools'
|
||||
import VueMacros from 'unplugin-vue-macros/vite'
|
||||
import Vue from '@vitejs/plugin-vue'
|
||||
|
||||
// https://vite.dev/config/
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
vue(),
|
||||
vueDevTools(),
|
||||
VueMacros({
|
||||
plugins: {
|
||||
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)),
|
||||
},
|
||||
},
|
||||
})
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
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)),
|
||||
},
|
||||
}),
|
||||
)
|