first step towards complete rewrite

This commit is contained in:
Norman Köhring 2023-02-10 12:58:09 +01:00
parent 6abd225eb5
commit e146052f33
50 changed files with 2635 additions and 8060 deletions

View file

@ -1,6 +0,0 @@
{
"presets": [
["env", { "modules": false }],
"stage-3"
]
}

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

21
.gitignore vendored
View file

@ -1,11 +1,24 @@
.DS_Store
node_modules/
npm-debug.log
yarn-error.log
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

4
.prettierrc Normal file
View file

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

View file

@ -2,20 +2,22 @@
> A blocky, side-scrolling, building and exploration game
This version of DIG! is reimplemented with Vue3 and Typescript. To see the old (and probably broken) version, check the vue2 branch.
## Build Setup
``` bash
# install dependencies
npm install
yarn
# serve with hot reload at localhost:8080
npm run dev
yarn dev
# build for production with minification
npm run build
yarn build
```
For detailed explanation on how things work, consult the [docs for vue-loader](http://vuejs.github.io/vue-loader).
## Credits

8
dist/build.js vendored

File diff suppressed because one or more lines are too long

2
dist/build.js.map vendored

File diff suppressed because one or more lines are too long

8
env.d.ts vendored Normal file
View file

@ -0,0 +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;
}

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,11 +1,58 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>building-game</title>
<meta charset="UTF-8" />
<link rel="icon" type="image/png" href="/favicon.png" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Dig!</title>
<style>
:root {
--block-size: 32px;
--blocks-x: 32;
--blocks-y: 18;
--spare-blocks: 2;
--field-width: calc(var(--block-size) * var(--blocks-x));
--field-height: calc(var(--block-size) * var(--blocks-y));
--spare-blocks: 2;
}
html,body,#app {
display: flex;
flex-flow: column nowrap;
justify-content: center;
width: 100vw;
height: 100vh;
background: black;
margin: 0;
padding: 0;
overflow: hidden;
}
#field {
position: relative;
width: var(--field-width);
height: var(--field-height);
margin: auto;
overflow: hidden;
background-color: #56F;
}
#input {
position: absolute;
opacity: 0;
display: block;
width: 1px;
height: 1px;
top: 0;
left: 0;
}
#level-indicator {
position: absolute;
top: 0;
right: 0;
color: white;
}
</style>
</head>
<body>
<div id="app"></div>
<script src="dist/build.js"></script>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

6800
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,36 +1,33 @@
{
"name": "digging-game",
"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": "cross-env NODE_ENV=development webpack-dev-server --open --hot",
"build": "cross-env NODE_ENV=production webpack --progress --hide-modules"
"dev": "vite",
"build": "vue-tsc && vite build",
"preview": "vite preview",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix"
},
"dependencies": {
"seedrandom": "^2.4.4",
"vue": "^2.6.10"
"alea": "^1.0.1",
"simplex-noise": "^4.0.1",
"vue": "^3.2.45"
},
"browserslist": [
"> 1%",
"last 2 versions",
"not ie <= 10"
],
"devDependencies": {
"babel-core": "^6.26.3",
"babel-loader": "^7.1.5",
"babel-preset-env": "^1.7.0",
"babel-preset-stage-3": "^6.24.1",
"cross-env": "^5.2.0",
"css-loader": "^3.0.0",
"file-loader": "^1.1.4",
"lodash": "^4.17.11",
"open-simplex-noise": "^1.6.0",
"vue-loader": "^13.7.3",
"vue-template-compiler": "^2.6.10",
"webpack": "^3.12.0",
"webpack-dev-server": "^2.11.5"
"@rushstack/eslint-patch": "^1.2.0",
"@typescript-eslint/parser": "^5.50.0",
"@vitejs/plugin-vue": "^4.0.0",
"@vue/eslint-config-prettier": "^7.0.0",
"@vue/eslint-config-typescript": "^11.0.2",
"eslint": "^8.33.0",
"eslint-plugin-vue": "^9.9.0",
"typescript": "^4.9.3",
"unplugin-vue-macros": "^1.7.3",
"vite": "^4.1.0",
"vue-tsc": "^1.0.24"
}
}

BIN
public/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 629 B

View file

@ -1,32 +1,93 @@
<template>
<div id="building-game">
<Field />
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { BLOCK_SIZE, STAGE_WIDTH, STAGE_HEIGHT } from './level/def'
import createLevel from './level'
<script>
import Field from './Field'
import useTime from './util/useTime'
import useInput from './util/useInput'
import usePlayer from './util/usePlayer'
export default {
name: 'building-game',
components: { Field },
data () {
return {
}
}
const { updateTime, timeOfDay, clock } = useTime()
const { player, direction, dx, dy } = usePlayer()
const { inputX, inputY, digging, paused } = useInput(player)
const level = createLevel(STAGE_WIDTH + 2, STAGE_HEIGHT + 2)
let animationFrame = 0
let lastTick = 0
let x = ref(0)
let y = ref(12)
const floorX = computed(() => Math.floor(x.value))
const floorY = computed(() => Math.floor(y.value))
const tx = computed(() => (x.value - floorX.value) * -BLOCK_SIZE)
const ty = computed(() => (y.value - floorY.value) * -BLOCK_SIZE)
const rows = computed(() => level.grid(floorX.value, floorY.value))
// TODO: mock
const blocked = {
left: false,
right: false,
up: false,
down: false,
}
function dig() {
console.warn('digging not yet implemented')
}
function move(thisTick) {
animationFrame = requestAnimationFrame(move)
// do nothing when paused, otherwise keep roughly 20 fps
if (paused.value || thisTick - lastTick < 50) return
updateTime()
player.vx = inputX.value
player.vy = inputY.value
if (inputX.value) player.lastDir = inputX.value
let dx_ = dx.value
let dy_ = dy.value
if (dx > 0 && blocked.right) dx_ = 0
else if (dx < 0 && blocked.left) dx_ = 0
if (dy > 0 && blocked.down) dy_ = 0
else if (dy < 0 && blocked.up) dy_ = 0
if (!inputY.value && digging.value) {
dx_ = 0
dig()
}
x.value += dx_ * 32
y.value += dy_ * 32
lastTick = thisTick
}
onMounted(() => {
lastTick = performance.now()
move(lastTick)
})
</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>
<template>
<div id="field" :class="timeOfDay">
<div id="blocks" :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="direction" />
<div id="level-indicator">
x:{{ floorX }}, y:{{ floorY }}
<template v-if="paused">(PAUSED)</template>
<template v-else>({{ clock }})</template>
<div>{{ inputX }}, {{ inputY }}, {{ player.lastDir }}</div>
<div>{{ dx }}, {{ dy }}, {{ direction }}</div>
</div>
</div>
</template>

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 +0,0 @@
dwarf_left.png

Before

Width:  |  Height:  |  Size: 14 B

After

Width:  |  Height:  |  Size: 607 B

BIN
src/assets/dwarf_back.png Normal file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 B

After

Width:  |  Height:  |  Size: 607 B

View file

@ -1,49 +1,34 @@
#field {
position: relative;
width: var(--field-width);
height: var(--field-height);
margin: auto;
overflow: hidden;
background-color: #56F;
}
#field > input {
position: absolute;
opacity: 0;
display: block;
width: 1px;
height: 1px;
}
.block.grass { background-image: url(./grass01.png); }
.block.tree_top_left { background-image: url(./tree_top_left.png); }
.block.tree_top_middle { background-image: url(./tree_top_middle.png); }
.block.tree_top_right { background-image: url(./tree_top_right.png); }
.block.treeTopLeft { background-image: url(./tree_top_left.png); }
.block.treeTopMiddle { background-image: url(./tree_top_middle.png); }
.block.treeTopRight { background-image: url(./tree_top_right.png); }
.block.tree_crown_left { background-image: url(./tree_crown_left.png); }
.block.tree_crown_middle { background-image: url(./tree_crown_middle.png); }
.block.tree_crown_right { background-image: url(./tree_crown_right.png); }
.block.treeCrownLeft { background-image: url(./tree_crown_left.png); }
.block.treeCrownMiddle { background-image: url(./tree_crown_middle.png); }
.block.treeCrownRight { background-image: url(./tree_crown_right.png); }
.block.tree_trunk_left { background-image: url(./tree_trunk_left.png); }
.block.tree_trunk_middle { background-image: url(./tree_trunk_middle.png); }
.block.tree_trunk_right { background-image: url(./tree_trunk_right.png); }
.block.treeTrunkLeft { background-image: url(./tree_trunk_left.png); }
.block.treeTrunkMiddle { background-image: url(./tree_trunk_middle.png); }
.block.treeTrunkRight { background-image: url(./tree_trunk_right.png); }
.block.tree_root_left { background-image: url(./tree_root_left.png); }
.block.tree_root_middle { background-image: url(./tree_root_middle.png); }
.block.tree_root_right { background-image: url(./tree_root_right.png); }
.block.treeRootLeft { background-image: url(./tree_root_left.png); }
.block.treeRootMiddle { background-image: url(./tree_root_middle.png); }
.block.treeRootRight { background-image: url(./tree_root_right.png); }
.block.tree_top_left_mixed { background-image: url(./tree_top_left_mixed.png); }
.block.tree_crown_left_mixed { background-image: url(./tree_crown_left_mixed.png); }
.block.tree_trunk_left_mixed { background-image: url(./tree_trunk_left_mixed.png); }
.block.tree_root_left_mixed { background-image: url(./tree_root_left_mixed.png); }
.block.treeTopLeftMixed { background-image: url(./tree_top_left_mixed.png); }
.block.treeCrownLeftMixed { background-image: url(./tree_crown_left_mixed.png); }
.block.treeTrunkLeftMixed { background-image: url(./tree_trunk_left_mixed.png); }
.block.treeRootLeftMixed { background-image: url(./tree_root_left_mixed.png); }
.block.tree_top_right_mixed { background-image: url(./tree_top_right_mixed.png); }
.block.tree_crown_right_mixed { background-image: url(./tree_crown_right_mixed.png); }
.block.tree_trunk_right_mixed { background-image: url(./tree_trunk_right_mixed.png); }
.block.tree_root_right_mixed { background-image: url(./tree_root_right_mixed.png); }
.block.treeTopRightMixed { background-image: url(./tree_top_right_mixed.png); }
.block.treeCrownRightMixed { background-image: url(./tree_crown_right_mixed.png); }
.block.treeTrunkRightMixed { background-image: url(./tree_trunk_right_mixed.png); }
.block.treeRootRightMixed { background-image: url(./tree_root_right_mixed.png); }
.block.soil { background-image: url(./soil.png); }
.block.soil_gravel { background-image: url(./soil_gravel.png); }
.block.stone_gravel { background-image: url(./rock_gravel.png); }
.block.soilGravel { background-image: url(./soil_gravel.png); }
.block.stoneGravel { background-image: url(./rock_gravel.png); }
.block.stone { background-image: url(./rock.png); }
.block.bedrock { background-image: url(./bedrock.png); }
.block.cave { background-color: #000; }
@ -58,3 +43,32 @@
.evening2 .block, .evening2 #player { filter: brightness(0.4) hue-rotate(-10deg) saturate(50%); }
.night .block, .night #player { filter: brightness(0.3) saturate(30%); }
#player {
position: absolute;
left: calc(var(--field-width) / 2);
top: calc(var(--field-height) / 2);
background-image: url(./dwarf_right.png);
}
#player.right { background-image: url(./dwarf_right.png); }
#player.left { background-image: url(./dwarf_left.png); }
#player.up { background-image: url(./dwarf_back.png); }
#player.down { background-image: url(./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;
}
#blocks {
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;
}

52
src/level/blockGen.ts Normal file
View file

@ -0,0 +1,52 @@
import type { NoiseFunction2D } from 'simplex-noise'
import {blockTypes as T, level as L, probability as P, type Block} from './def'
export default function createBlockGenerator(noise2D: NoiseFunction2D) {
const rand: NoiseFunction2D = (x, y) => 0.5 + 0.5 * noise2D(x, y)
// randomly generate a block
// level: number, smaller is "higher"
// column: number, the x-axis
// before: Block, the block type left of (before) this block
// above: Block, the block type above this block
const generateBlock = (level: number, column: number, before: Block, above: Block): Block => {
// no randomness needed, there is always air above the trees
if (level < L.treeTop) return T.air
const r = rand(level, column)
// Air layer: mostly air, sometimes trees
if (level < L.ground) {
if (level === L.treeTop && r < P.tree) return T.treeTopMiddle
return T.air
}
// Soil layer: Mostly soil, sometimes gravel
if (level < L.rock) {
if (r < P.soilGravel) return T.soilGravel
else return T.soil
}
// Rock level: Mostly stone, sometimes gravel
if (level < L.underground) {
if (r < P.stoneGravel) return T.stoneGravel
else return T.stone
}
// Underground: Mostly bedrock, sometimes caves
// the probability for a cave rises with the level
const a = P.cave / L.caveMax**2
const p = Math.min(P.cave, a * level**2)
if (r < p) return T.cave
return T.bedrock
}
const fillRow = (level: number, column: number, row: Block[], previousRow: Block[]) => {
for (let i = 0; i < row.length; i++) {
row[i] = generateBlock(level, column + i, row[i - 1], previousRow[i])
}
}
return fillRow
}

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
}

73
src/level/def.ts Normal file
View file

@ -0,0 +1,73 @@
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 type Block = {
type: string,
hp: number,
walkable: boolean,
climbable?: boolean,
}
export const blockTypes: Record<string, Block> = {
air: { type: 'air', hp: Infinity, walkable: true },
grass: { type: 'grass', hp: 1, walkable: false },
treeTopLeft: { type: 'treeTopLeft', hp: 5, walkable: true },
treeTopMiddle: { type: 'treeTopMiddle', hp: 5, walkable: true },
treeTopRight: { type: 'treeTopRight', hp: 5, walkable: true },
treeCrownLeft: { type: 'treeCrownLeft', hp: 5, walkable: true },
treeCrownMiddle: { type: 'treeCrownMiddle', hp: 5, walkable: true, climbable: true },
treeCrownRight: { type: 'treeCrownRight', hp: 5, walkable: true },
treeTrunkLeft: { type: 'treeTrunkLeft', hp: 5, walkable: true },
treeTrunkMiddle: { type: 'treeTrunkMiddle', hp: 5, walkable: true, climbable: true },
treeTrunkRight: { type: 'treeTrunkRight', hp: 5, walkable: true },
treeRootLeft: { type: 'treeRootLeft', hp: 5, walkable: true },
treeRootMiddle: { type: 'treeRootMiddle', hp: 5, walkable: true, climbable: true },
treeRootRight: { type: 'treeRootRight', hp: 5, walkable: true },
treeTopLeftMixed: { type: 'treeTopLeftMixed', hp: 5, walkable: true },
treeCrownLeftMixed: { type: 'treeCrownLeftMixed', hp: 5, walkable: true },
treeTrunkLeftMixed: { type: 'treeTrunkLeftMixed', hp: 5, walkable: true },
treeRootLeftMixed: { type: 'treeRootLeftMixed', hp: 5, walkable: true },
treeTopRightMixed: { type: 'treeTopRightMixed', hp: 5, walkable: true },
treeCrownRightMixed: { type: 'treeCrownRightMixed', hp: 5, walkable: true },
treeTrunkRightMixed: { type: 'treeTrunkRightMixed', hp: 5, walkable: true },
treeRootRightMixed: { type: 'treeRootRightMixed', hp: 5, walkable: true },
soil: { type: 'soil', hp: 2, walkable: false },
soilGravel: { type: 'soilGravel', hp: 5, walkable: false },
stoneGravel: { type: 'stoneGravel', 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,
caveMax: 250,
}
export const probability = {
tree: 0.2,
soilHole: 0.3,
soilGravel: 0.2,
stoneGravel: 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.