You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
253 lines
6.9 KiB
Vue
253 lines
6.9 KiB
Vue
<script setup lang="ts">
|
|
import { ref, computed, onMounted } from 'vue'
|
|
import Help from './screens/help.vue'
|
|
import Inventory from './screens/inventory.vue'
|
|
|
|
import { BLOCK_SIZE, STAGE_WIDTH, STAGE_HEIGHT, type Block } from './level/def'
|
|
import { getItem, getItemClass } from './level/items'
|
|
import createLevel from './level'
|
|
|
|
import useTime from './util/useTime'
|
|
import useInput from './util/useInput'
|
|
import usePlayer, { type InventoryItem } from './util/usePlayer'
|
|
import useLightMap from './util/useLightMap'
|
|
|
|
const { updateTime, time, timeOfDay, clock } = useTime()
|
|
const { player, direction, dx, dy, pocket, unpocket } = usePlayer()
|
|
const { inputX, inputY, running, paused, help, inventory } = useInput()
|
|
const level = createLevel(STAGE_WIDTH + 2, STAGE_HEIGHT + 2)
|
|
|
|
const lightMapEl = ref<HTMLCanvasElement | undefined>(undefined)
|
|
let updateLightMap: ReturnType<typeof useLightMap>
|
|
|
|
pocket({ name: 'Shovel', type: 'tool', icon: 'shovel', quality: 'wood' })
|
|
pocket({ name: 'Sword', type: 'weapon', icon: 'sword', quality: 'wood' })
|
|
pocket({ name: 'Pick Axe', type: 'tool', icon: 'pick', quality: 'wood' })
|
|
|
|
let animationFrame = 0
|
|
let lastTick = 0
|
|
|
|
const x = ref(0)
|
|
const y = ref(0)
|
|
const floorX = computed(() => Math.floor(x.value))
|
|
const floorY = computed(() => Math.floor(y.value))
|
|
const tx = computed(() => (x.value - floorX.value) * -BLOCK_SIZE)
|
|
const ty = computed(() => (y.value - floorY.value) * -BLOCK_SIZE)
|
|
const rows = computed(() => level.grid(floorX.value, floorY.value))
|
|
const lightBarrier = computed(() => level.sunLight(floorX.value))
|
|
|
|
const arriving = ref(true)
|
|
const walking = ref(false)
|
|
const inventorySelection = ref<InventoryItem>(player.inventory[0])
|
|
|
|
type Surroundings = {
|
|
at: Block,
|
|
left: Block,
|
|
right: Block,
|
|
up: Block,
|
|
down: Block,
|
|
}
|
|
const surroundings = computed<Surroundings>(() => {
|
|
const px = player.x
|
|
const py = player.y
|
|
const row = rows.value
|
|
|
|
return {
|
|
at: row[py][px],
|
|
left: row[py][px - 1],
|
|
right: row[py][px + 1],
|
|
up: row[py - 1][px],
|
|
down: row[py + 1][px],
|
|
}
|
|
})
|
|
const blocked = computed(() => {
|
|
const { left, right, up, down } = surroundings.value
|
|
return {
|
|
left: !left.walkable,
|
|
right: !right.walkable,
|
|
up: !up.walkable,
|
|
down: !down.walkable,
|
|
}
|
|
})
|
|
|
|
// TODO
|
|
const damagedBlocks = ref([])
|
|
|
|
function dig(blockX: number, blockY: number, block: Block) {
|
|
// can only dig in player proximity
|
|
if (Math.abs(player.x - blockX) > 2 || Math.abs(player.y - blockY) > 2) return
|
|
// finally dig that block
|
|
// TODO: damage blocks first
|
|
level.change({
|
|
type: 'exchange',
|
|
x: floorX.value + blockX,
|
|
y: floorY.value + blockY,
|
|
newType: 'air'
|
|
})
|
|
|
|
// anything to pick up?
|
|
if (block.drops) {
|
|
const dropItem = getItem(block.drops)
|
|
if (dropItem) pocket(dropItem)
|
|
}
|
|
}
|
|
|
|
function build(blockX: number, blockY: number, block: InventoryItem) {
|
|
const blockToBuild = block.builds
|
|
// the block doesn't do anything
|
|
if (!blockToBuild) return
|
|
|
|
level.change({
|
|
type: 'exchange',
|
|
x: floorX.value + blockX,
|
|
y: floorY.value + blockY,
|
|
newType: blockToBuild
|
|
})
|
|
|
|
const newAmount = unpocket(block)
|
|
if (newAmount < 1) inventorySelection.value = player.inventory[0]
|
|
}
|
|
|
|
function interactWith(blockX: number, blockY: number, block: Block) {
|
|
// § 4 ArbZG
|
|
if (paused.value) return
|
|
|
|
const blockInHand = inventorySelection.value.type === 'block'
|
|
const toolInHand = inventorySelection.value.type === 'tool'
|
|
const emptyBlock = block.type === 'air' || block.type === 'cave'
|
|
|
|
// put the selected block
|
|
if (blockInHand && emptyBlock) {
|
|
build(blockX, blockY, inventorySelection.value)
|
|
// dig a block with shovel or pick axe
|
|
} else if (toolInHand && !emptyBlock) {
|
|
dig(blockX, blockY, block)
|
|
}
|
|
|
|
// This feels like cheating, but it makes Vue recalculate floorX
|
|
// which then recalculates the blocks, so that the changes are
|
|
// applied. Otherwise, they wouldn't be visible before moving
|
|
x.value = x.value + 0.01
|
|
x.value = x.value - 0.01
|
|
}
|
|
|
|
let lastTimeUpdate = 0
|
|
|
|
const move = (thisTick: number): void => {
|
|
animationFrame = requestAnimationFrame(move)
|
|
|
|
// do nothing when paused
|
|
if (paused.value) {
|
|
// reset tick, to avoid tickDelta and resulting character teleport
|
|
lastTick = thisTick
|
|
return
|
|
}
|
|
|
|
const tickDelta = thisTick - lastTick
|
|
lastTimeUpdate += tickDelta
|
|
|
|
// update in-game time every 60ms by 0.1
|
|
// then a day needs 10000 updates, and it takes about 10 minutes
|
|
if (lastTimeUpdate > 60) {
|
|
updateTime()
|
|
lastTimeUpdate = 0
|
|
}
|
|
|
|
player.vx = inputX.value
|
|
player.vy = inputY.value
|
|
|
|
if (inputX.value) player.lastDir = inputX.value
|
|
|
|
let dx_ = dx.value
|
|
let dy_ = dy.value
|
|
|
|
if (running.value) dx_ *= 2
|
|
|
|
if (dx_ > 0 && blocked.value.right) dx_ = 0
|
|
else if (dx_ < 0 && blocked.value.left) dx_ = 0
|
|
|
|
if (dy_ > 0 && blocked.value.down) dy_ = 0
|
|
else if (dy_ < 0 && blocked.value.up) dy_ = 0
|
|
|
|
const optimal = 16 // 16ms per tick => 60 FPS
|
|
const movementMultiplier = (tickDelta / optimal) * 2
|
|
const fallMultiplier = movementMultiplier * 2 // TODO: accelerated fall?
|
|
|
|
if (arriving.value && dy_ === 0) {
|
|
arriving.value = false
|
|
}
|
|
|
|
walking.value = !!dx_
|
|
|
|
if (dy_ <= 0) x.value += dx_ * movementMultiplier
|
|
|
|
if (dy_ < 0 || arriving.value) {
|
|
y.value += dy_ * movementMultiplier
|
|
} else {
|
|
y.value += dy_ * fallMultiplier
|
|
}
|
|
|
|
updateLightMap()
|
|
lastTick = thisTick
|
|
}
|
|
|
|
function selectTool(item: InventoryItem) {
|
|
inventorySelection.value = item
|
|
}
|
|
|
|
onMounted(() => {
|
|
const canvas = lightMapEl.value!
|
|
canvas.height = (BLOCK_SIZE + 2) * STAGE_HEIGHT
|
|
canvas.width = (BLOCK_SIZE + 2) * STAGE_WIDTH
|
|
const ctx = canvas.getContext('2d')!
|
|
|
|
updateLightMap = useLightMap(ctx, floorX, floorY, tx, ty, time, lightBarrier)
|
|
lastTick = performance.now()
|
|
move(lastTick)
|
|
})
|
|
</script>
|
|
|
|
<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', block.type, {
|
|
highlight: x === player.x && y == player.y
|
|
}]"
|
|
@click="interactWith(x, y, block)"
|
|
/>
|
|
</template>
|
|
</div>
|
|
|
|
<div id="player" :class="[direction, { walking }]" @click="inventory = !inventory">
|
|
<div class="head"></div>
|
|
<div class="body"></div>
|
|
<div class="legs">
|
|
<div class="left"></div>
|
|
<div class="right"></div>
|
|
</div>
|
|
<div class="arms">
|
|
<div v-if="inventorySelection"
|
|
:class="['item', getItemClass(inventorySelection)]"
|
|
></div>
|
|
</div>
|
|
</div>
|
|
|
|
<canvas id="light-mask" ref="lightMapEl" :style="{transform: `translate(${tx}px, ${ty}px)`}" />
|
|
<div id="beam" v-if="arriving"></div>
|
|
<div id="level-indicator">
|
|
x:{{ floorX }}, y:{{ floorY }}
|
|
<template v-if="paused">(PAUSED)</template>
|
|
<template v-else>({{ clock }})</template>
|
|
</div>
|
|
|
|
<Inventory :shown="inventory"
|
|
:items="player.inventory"
|
|
@selection="selectTool($event)"
|
|
/>
|
|
<Help v-show="help" />
|
|
</div>
|
|
</template>
|