Compare commits

..

No commits in common. "2025" and "main" have entirely different histories.
2025 ... main

76 changed files with 2388 additions and 5388 deletions

View file

@ -1,9 +1,9 @@
[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue,css,scss,sass,less,styl}] root = true
[*]
charset = utf-8 charset = utf-8
indent_size = 2
indent_style = space indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true insert_final_newline = true
trim_trailing_whitespace = true trim_trailing_whitespace = true
end_of_line = lf
max_line_length = 100

48
.eslintrc.cjs Normal file
View 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
View file

@ -1 +0,0 @@
* text=auto eol=lf

9
.gitignore vendored
View file

@ -8,23 +8,20 @@ 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?
*.tsbuildinfo # old source code for reference?!
src/old

4
.prettierrc Normal file
View file

@ -0,0 +1,4 @@
{
"semi": false,
"singleQuote": true
}

View file

@ -1,6 +0,0 @@
{
"$schema": "https://json.schemastore.org/prettierrc",
"semi": false,
"singleQuote": true,
"printWidth": 100
}

View file

@ -1,9 +0,0 @@
{
"recommendations": [
"Vue.volar",
"vitest.explorer",
"dbaeumer.vscode-eslint",
"EditorConfig.EditorConfig",
"esbenp.prettier-vscode"
]
}

View file

@ -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 # build for production with minification
yarn build
See [Vite Configuration Reference](https://vite.dev/config/).
## Project Setup
```sh
pnpm install
``` ```
### Compile and Hot-Reload for Development
```sh
pnpm dev
```
### Type-Check, Compile and Minify for Production ## Credits
```sh Art, Music and all from [OpenGameArt](https://opengameart.org/). More specifically:
pnpm build
```
### 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 and partially adapted to my needs.
pnpm test:unit
```
### Lint with [ESLint](https://eslint.org/)
```sh
pnpm lint
```

BIN
dist/bedrock.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

7
dist/build.js vendored Normal file

File diff suppressed because one or more lines are too long

1
dist/build.js.map vendored Normal file

File diff suppressed because one or more lines are too long

BIN
dist/dwarf_left.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 607 B

BIN
dist/dwarf_right.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 629 B

BIN
dist/grass01.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 614 B

BIN
dist/rock.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
dist/soil01.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

BIN
dist/soil_gravel01.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 940 B

BIN
dist/tree_crown_left.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 719 B

BIN
dist/tree_crown_left_mixed.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
dist/tree_crown_middle.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

BIN
dist/tree_crown_right.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

BIN
dist/tree_crown_right_mixed.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
dist/tree_root_left.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 B

BIN
dist/tree_root_left_mixed.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 539 B

BIN
dist/tree_root_middle.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 938 B

BIN
dist/tree_root_right.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 280 B

BIN
dist/tree_root_right_mixed.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 539 B

BIN
dist/tree_top_left.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 B

BIN
dist/tree_top_left_mixed.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 B

BIN
dist/tree_top_middle.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 747 B

BIN
dist/tree_top_right.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 B

BIN
dist/tree_top_right_mixed.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 257 B

BIN
dist/tree_trunk_left.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 735 B

BIN
dist/tree_trunk_left_mixed.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
dist/tree_trunk_middle.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

BIN
dist/tree_trunk_right.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 750 B

BIN
dist/tree_trunk_right_mixed.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

7
env.d.ts vendored
View file

@ -1 +1,8 @@
/// <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;
}

View file

@ -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
View file

@ -0,0 +1,3 @@
{
"recommendations": ["Vue.volar", "Vue.vscode-typescript-vue-plugin"]
}

18
foo/README.md Normal file
View 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
View 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
View 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
View 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
View 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

View 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
View 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;
}
}

View file

@ -1,8 +1,7 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang=""> <html lang="en">
<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" />
@ -16,8 +15,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>Vue Shovel</title> <title>Dig!</title>
<style> <style>
:root { :root {
--block-size: 64px; --block-size: 64px;

View file

@ -1,55 +1,33 @@
{ {
"name": "vue-shovel", "name": "DIG",
"version": "0.0.0", "description": "A blocky, side-scrolling, digging and exploration game",
"version": "0.0.1",
"author": "koehr <n@koehr.in>",
"license": "MIT",
"private": true, "private": true,
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "run-p type-check \"build-only {@}\" --", "build": "vue-tsc && vite build",
"preview": "vite preview", "preview": "vite preview",
"test:unit": "vitest", "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix"
"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.3", "simplex-noise": "^4.0.1",
"vue": "^3.5.13" "vue": "^3.4.22"
}, },
"devDependencies": { "devDependencies": {
"@tsconfig/node22": "^22.0.0", "@rushstack/eslint-patch": "^1.10.2",
"@types/jsdom": "^21.1.7", "@typescript-eslint/parser": "^7.7.0",
"@types/node": "^22.13.9", "@vitejs/plugin-vue": "^5.0.4",
"@vitejs/plugin-vue": "^5.2.1", "@vue/eslint-config-prettier": "^9.0.0",
"@vitest/eslint-plugin": "^1.1.36", "@vue/eslint-config-typescript": "^13.0.0",
"@vue/eslint-config-prettier": "^10.2.0", "eslint": "^9.0.0",
"@vue/eslint-config-typescript": "^14.5.0", "eslint-plugin-vue": "^9.25.0",
"@vue/test-utils": "^2.4.6", "typescript": "^5.4.5",
"@vue/tsconfig": "^0.7.0", "unplugin-vue-macros": "^2.9.1",
"eslint": "^9.21.0", "vite": "^5.2.9",
"eslint-plugin-oxlint": "^0.15.13", "vue-tsc": "^2.0.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

File diff suppressed because it is too large Load diff

14
simplex.html Normal file
View 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>

View file

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue' import { ref, computed, onMounted } 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'
@ -31,23 +31,10 @@ 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 fracX = computed(() => x.value - floorX.value) const tx = computed(() => (x.value - floorX.value) * -BLOCK_SIZE)
const fracY = computed(() => y.value - floorY.value) const ty = computed(() => (y.value - floorY.value) * -BLOCK_SIZE)
const tx = computed(() => fracX.value * -BLOCK_SIZE) const rows = computed(() => level.grid(floorX.value, floorY.value))
const ty = computed(() => fracY.value * -BLOCK_SIZE) const lightBarrier = computed(() => level.sunLight(floorX.value))
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 arriving = ref(true) const arriving = ref(true)
const walking = ref(false) const walking = ref(false)
@ -61,32 +48,25 @@ type Surroundings = {
down: Block, down: Block,
} }
const surroundings = computed<Surroundings>(() => { const surroundings = computed<Surroundings>(() => {
const _update = mapUpdateCount.value // reactivity trigger const px = player.x
const x = px.value const py = player.y
const y = py.value const row = rows.value
const rows = mapGrid.value
const rowY = rows[y]
const rowYp = rows[y - 1]
const rowYn = rows[y + 1]
return { return {
at: rowY[x], at: row[py][px],
left: rowY[x - 1], left: row[py][px - 1],
right: rowY[x + 1], right: row[py][px + 1],
up: rowYp[x], up: row[py - 1][px],
down: rowYn[x], down: row[py + 1][px],
} }
}) })
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 && fx < 0.8 && fx > 0.7, left: !left.walkable,
right: !right.walkable && fx > 0.2 && fx < 0.3, right: !right.walkable,
up: !up.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, 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) {
@ -124,14 +103,12 @@ function build(blockX: number, blockY: number, block: InventoryItem) {
y: floorY.value + blockY, y: floorY.value + blockY,
newType: blockToBuild newType: blockToBuild
}) })
mapUpdateCount.value = mapUpdateCount.value + 1
const newAmount = unpocket(block) const newAmount = unpocket(block)
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) {
console.log('interact with', blockX, blockY, block.type)
// § 4 ArbZG // § 4 ArbZG
if (paused.value) return if (paused.value) return
@ -146,6 +123,12 @@ function interactWith(blockX: number, blockY: number, block: Block) {
} else if (toolInHand && !emptyBlock) { } else if (toolInHand && !emptyBlock) {
dig(blockX, blockY, block) 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
@ -213,16 +196,12 @@ function selectTool(item: InventoryItem) {
} }
onMounted(() => { onMounted(() => {
if (lightMapEl.value) { const canvas = lightMapEl.value!
const canvas = lightMapEl.value
canvas.height = (BLOCK_SIZE + 2) * STAGE_HEIGHT canvas.height = (BLOCK_SIZE + 2) * STAGE_HEIGHT
canvas.width = (BLOCK_SIZE + 2) * STAGE_WIDTH canvas.width = (BLOCK_SIZE + 2) * STAGE_WIDTH
const ctx = canvas.getContext('2d')! const ctx = canvas.getContext('2d')!
updateLightMap = useLightMap(ctx, floorX, floorY, tx, ty, time, lightBarrier) updateLightMap = useLightMap(ctx, floorX, floorY, tx, ty, time, lightBarrier)
} else {
console.warn('lightmap deactivated')
updateLightMap = (() => {}) as ReturnType<typeof useLightMap>
}
lastTick = performance.now() lastTick = performance.now()
move(lastTick) move(lastTick)
}) })
@ -230,19 +209,19 @@ onMounted(() => {
<template> <template>
<div id="field" :class="timeOfDay"> <div id="field" :class="timeOfDay">
<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 mapGrid"> <template v-for="(row, y) in rows">
<div v-for="(block, x) in row" <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)" @click="interactWith(x, y, block)"
/> />
</template> </template>
</div> </div>
<div id="player" <div id="player" :class="[direction, { walking }]" @click="inventory = !inventory">
:class="[direction, { 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">
@ -262,8 +241,6 @@ onMounted(() => {
x:{{ floorX }}, y:{{ floorY }} x:{{ floorX }}, y:{{ floorY }}
<template v-if="paused">(PAUSED)</template> <template v-if="paused">(PAUSED)</template>
<template v-else>({{ clock }})</template> <template v-else>({{ clock }})</template>
<br/>
Map Changes: {{ mapUpdateCount }}
</div> </div>
<Inventory :shown="inventory" <Inventory :shown="inventory"

View file

@ -101,7 +101,7 @@
#field .block:hover, #field .block:hover,
#field .block.block.highlight { #field .block.block.highlight {
filter: brightness(1.2) saturate(1.2); filter: brightness(1.2) grayscale(1.0);
outline: 1px solid white; outline: 1px solid white;
z-index: 10; z-index: 10;
} }

View file

@ -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); top: calc(var(--field-height) / 2 - 10px);
display: flex; display: flex;
flex-flow: column nowrap; flex-flow: column nowrap;
width: var(--player-width); width: var(--player-width);
@ -58,21 +58,6 @@
#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;
@ -85,9 +70,6 @@
#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;
@ -107,14 +89,6 @@
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; }

View file

@ -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 // 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): void { function calcLightBarrier(columnOffset: number) {
let previousBlock: Block = T.air let previousBlock: Block = T.air
for (let col = 0; col < width; col++) { 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++) { 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)
@ -98,21 +98,13 @@ export default function createLevel(width: number, height: number, seed = 'extre
applyPlayerChanges(columnOffset, levelOffset) applyPlayerChanges(columnOffset, levelOffset)
} }
function sunLight(columnOffset: number): number[] { function sunLight(columnOffset: number) {
calcLightBarrier(columnOffset) calcLightBarrier(columnOffset)
return _lightBarrier return _lightBarrier
} }
let lastGenX = 0 function grid(x: number, y: number) {
let lastGenY = 0
generate(0, 0)
function grid(x: number, y: number, force: false): Block[][] {
if (force || lastGenX !== x || lastGenY !== y) {
generate(x, y) generate(x, y)
lastGenX = x
lastGenY = y
}
return _grid return _grid
} }

View file

@ -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");

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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
}

View file

@ -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
}
}

View file

@ -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
}
}
}

View file

@ -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
}
}

View file

@ -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
}
}
}

View file

@ -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)
})

View file

@ -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'
}
}

View file

@ -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
View file

@ -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[]
}

View file

@ -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/*"]
}
}
}

View file

@ -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": [ "references": [
{ {
"path": "./tsconfig.node.json" "path": "./tsconfig.node.json"
},
{
"path": "./tsconfig.app.json"
},
{
"path": "./tsconfig.vitest.json"
} }
] ]
} }

View file

@ -1,19 +1,9 @@
{ {
"extends": "@tsconfig/node22/tsconfig.json",
"include": [
"vite.config.*",
"vitest.config.*",
"cypress.config.*",
"nightwatch.conf.*",
"playwright.config.*",
"eslint.config.*"
],
"compilerOptions": { "compilerOptions": {
"noEmit": true, "composite": true,
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"module": "ESNext", "module": "ESNext",
"moduleResolution": "Bundler", "moduleResolution": "Node",
"types": ["node"] "allowSyntheticDefaultImports": true
} },
"include": ["vite.config.ts"]
} }

View file

@ -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"]
}
}

View file

@ -1,21 +1,14 @@
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite' import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue' import VueMacros from 'unplugin-vue-macros/vite'
import vueDevTools from 'vite-plugin-vue-devtools' import Vue from '@vitejs/plugin-vue'
// https://vite.dev/config/ // https://vitejs.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [ plugins: [
vue(), VueMacros({
vueDevTools(), 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)),
},
},
}) })

View file

@ -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)),
},
}),
)

1978
yarn.lock Normal file

File diff suppressed because it is too large Load diff