Compare commits

...

2 commits

Author SHA1 Message Date
Norman Köhring
3266ddb217 directional fixtures 2025-03-22 22:43:27 +01:00
Norman Köhring
72c5cd1d19 simplify light mask, gradient shadows, torches (without light) 2025-03-19 16:49:44 +01:00
15 changed files with 186 additions and 123 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

BIN
public/Items/torchFloor.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
public/Items/torchLeft.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

BIN
public/Items/torchRight.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -1,11 +1,11 @@
<script setup lang="ts">
import type { Block, Direction, Item, InventoryItem } from './types.d'
import type { Block, BlockType, Direction, Item, InventoryItem, LightSource } from './types.d'
import { ref, computed, watch, onMounted, useTemplateRef } from 'vue'
import Help from './screens/help.vue'
import Inventory from './screens/inventory.vue'
import Background from './Background.vue'
import { BLOCK_SIZE, STAGE_WIDTH, STAGE_HEIGHT } from './level/def'
import { BLOCK_SIZE, STAGE_WIDTH, STAGE_HEIGHT, softTerrain, hardTerrain } from './level/def'
import { getItem, getItemClass } from './level/items'
import createLevel from './level'
@ -25,11 +25,12 @@ let updateLightMap = (() => {}) as ReturnType<typeof useLightMask>
pocket(getItem('tool_shovel_wood'))
pocket(getItem('tool_sword_wood'))
pocket(getItem('tool_pickaxe_wood'))
pocket(getItem('fixture_torch'), 5)
let animationFrame = 0
let lastTick = 0
const debug = ref(false)
const debug = ref(true)
const x = ref(0)
const y = ref(0)
const floorX = computed(() => Math.floor(x.value))
@ -51,15 +52,35 @@ const mapGrid = computed<Block[][]>(() => {
const _update = mapUpdateCount.value // reactivity trigger
return level.grid(floorX.value, floorY.value, true)
})
const lightSources = computed(() => {
const _update = mapUpdateCount.value // reactivity trigger
const _floorX = floorX.value // reactivity trigger
const _floorY = floorY.value // reactivity trigger
const lightSources: LightSource[] = []
const grid = mapGrid.value
for (let y = 0; y < grid.length; y++) {
const row = grid[y]
for (let x = 0; x < row.length; x++) {
const block = row[x]
if (block.illumination) {
lightSources.push({
x, y,
strength: block.illumination,
color: block.color ?? '#FFE'
})
}
}
}
return lightSources
})
const arriving = ref(true)
const walking = ref(false)
const inventorySelection = ref<InventoryItem>(player.inventory[0])
const surroundings = computed<Record<Direction, Block>>(() => {
const _update = mapUpdateCount.value // reactivity trigger
const x = px.value
const y = py.value
const getSurroundings = (x: number, y: number) => {
const rows = mapGrid.value
const rowY = rows[y]
@ -73,6 +94,11 @@ const surroundings = computed<Record<Direction, Block>>(() => {
up: rowYp[x],
down: rowYn[x],
}
}
const surroundings = computed<Record<Direction, Block>>(() => {
const _update = mapUpdateCount.value // reactivity trigger
return getSurroundings(px.value, py.value)
})
const blocked = computed(() => {
const { left, right, up, down } = surroundings.value
@ -110,15 +136,26 @@ function dig(blockX: number, blockY: number, block: Block) {
}
function build(blockX: number, blockY: number, block: InventoryItem) {
const blockToBuild = block.builds
// the block doesn't do anything
if (!blockToBuild) return
let blockToBuild = block.builds
if (!blockToBuild) return // the block doesn't do anything?!
// While blocks are just filling the space completely, fixtures are attached
// to the closest surface. We check the surroundings, starting at with left
// and right, then bottom and top.
if (block.type === 'fixture') {
const { left, right, up, down } = getSurroundings(blockX, blockY)
if (!left.transparent) blockToBuild = `${blockToBuild}Left`
else if (!right.transparent) blockToBuild = `${blockToBuild}Right`
else if (!up.transparent) blockToBuild = `${blockToBuild}Ceiling`
else if (!down.transparent) blockToBuild = `${blockToBuild}Floor`
}
level.change({
change: 'exchange',
x: floorX.value + blockX,
y: floorY.value + blockY,
newType: blockToBuild
newType: blockToBuild as BlockType
})
mapUpdateCount.value = mapUpdateCount.value + 1
@ -127,20 +164,37 @@ function build(blockX: number, blockY: number, block: InventoryItem) {
}
function interactWith(blockX: number, blockY: number, block: Block) {
if (debug) console.debug('interact with', blockX, blockY, block.type)
if (debug) {
console.debug(
`interact with ${block.type} at ${blockX},${blockY},`,
`with a ${inventorySelection.value.id} in hand`
)
}
// § 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'
// no spooky interaction at a distance
const distanceX = ~~(px.value - blockX)
const distanceY = ~~(py.value - blockY)
if (distanceX > 1 || distanceY > 1) return
const blockInHand = inventorySelection.value
const shovelInHand = blockInHand.id.indexOf('shovel') >= 0
const pickaxeInHand = blockInHand.id.indexOf('pickaxe') >= 0
const canBuild = !!blockInHand.builds
const hasTool = blockInHand.type === 'tool'
const hasSpace = block.type === 'air' || block.type === 'cave'
const canUseShovel = softTerrain.indexOf(block.type) >= 0
const canUsePickaxe = hardTerrain.indexOf(block.type) >= 0
// put the selected block
if (blockInHand && emptyBlock) {
build(blockX, blockY, inventorySelection.value)
if (canBuild && hasSpace) {
build(blockX, blockY, blockInHand)
// dig a block with shovel or pick axe
} else if (toolInHand && !emptyBlock) {
dig(blockX, blockY, block)
} else if (hasTool && !hasSpace) {
if (shovelInHand && canUseShovel) dig(blockX, blockY, block)
else if (pickaxeInHand && canUsePickaxe) dig(blockX, blockY, block)
}
}
@ -214,7 +268,10 @@ onMounted(() => {
canvas.height = (BLOCK_SIZE + 2) * STAGE_HEIGHT
canvas.width = (BLOCK_SIZE + 2) * STAGE_WIDTH
const ctx = canvas.getContext('2d')!
updateLightMap = useLightMask(ctx, floorX, floorY, tx, ty, time, lightBarrier)
updateLightMap = useLightMask(
ctx, floorY, tx, ty,
lightBarrier, lightSources,
)
} else {
console.warn('lightmap deactivated')
}
@ -265,7 +322,13 @@ onMounted(() => {
<div id="beam" v-if="arriving"></div>
<div id="level-indicator">
x:{{ floorX }}, y:{{ floorY }}
<template v-if="paused">(PAUSED)</template>
<template v-if="paused">
<template v-if="debug">
({{ clock }})<br/>
time: <input type="number" max="0" min="1000" v-model="time" />
</template>
<template v-else>(PAUSED)</template>
</template>
<template v-else>({{ clock }})</template>
</div>

View file

@ -21,18 +21,15 @@ onMounted(() => {
const drawBackground = useBackground(
canvasEl,
~~(STAGE_WIDTH * BLOCK_SIZE / 2.0),
~~(STAGE_HEIGHT * BLOCK_SIZE / 2.0),
~~(STAGE_WIDTH * BLOCK_SIZE / 1.0),
~~(STAGE_HEIGHT * BLOCK_SIZE / 1.0),
)
watch(props, () => {
console.log('drawing background', sunY.value)
drawBackground(props.x, sunY.value)
}, { immediate: true })
watch(props, () => drawBackground(props.x, sunY.value), { immediate: true })
})
</script>
<template>
<canvas ref="canvas" id="background"></canvas>
<canvas ref="canvas" id="background" />
</template>

View file

@ -98,6 +98,10 @@
.block.brickWall {
background-image: url(/Tiles/brick_grey.png);
}
.block.torchLeft { background-image: url("/Items/torchLeft.png"); }
.block.torchRight { background-image: url("/Items/torchRight.png"); }
.block.torchFloor { background-image: url("/Items/torchFloor.png"); }
.block.torchCeiling { background-image: url("/Items/torchCeiling.png"); }
#field {
user-select: none;
@ -112,17 +116,17 @@
.morning0 .block,
.morning0 #player {
filter: saturate(50%);
filter: saturate(50%) brightness(0.6);
}
.morning1 .block,
.morning1 #player {
filter: saturate(100%);
filter: saturate(100%) brightness(0.8);
}
.morning2 .block,
.morning2 #player {
filter: saturate(120%);
filter: saturate(120%) brightness(0.9);
}
.evening0 .block,
@ -132,17 +136,17 @@
.evening1 .block,
.evening1 #player {
filter: saturate(70%);
filter: saturate(70%) brightness(0.8);
}
.evening2 .block,
.evening2 #player {
filter: saturate(50%);
filter: saturate(50%) brightness(0.6);
}
.night .block,
.night #player {
filter: saturate(30%);
filter: saturate(30%) brightness(0.4);
}
#blocks {

View file

@ -14,3 +14,5 @@
.item.block-dirt { background-image: url("/Tiles/dirt.png"); }
.item.block-stone { background-image: url("/Tiles/stone.png"); }
.item.block-gravel { background-image: url("/Tiles/gravel_stone.png"); }
.item.fixture-torch { background-image: url("/Items/torchFloor.png"); }

View file

@ -27,8 +27,19 @@ export const blockTypes: Record<BlockType, Block> = {
bedrock: { type: 'bedrock', hp: 25, walkable: false, drops: 'block_stone' },
// Built Blocks
brickWall: { type: 'brickWall', hp: 25, walkable: false, drops: 'block_gravel' },
torchLeft: { type: 'torchLeft', hp: 1, walkable: true, transparent: true, drops: 'fixture_torch', illumination: 1.0, color: '#FFE', fixture: true },
torchRight: { type: 'torchRight', hp: 1, walkable: true, transparent: true, drops: 'fixture_torch', illumination: 1.0, color: '#FFE', fixture: true },
torchCeiling: { type: 'torchCeiling', hp: 1, walkable: true, transparent: true, drops: 'fixture_torch', illumination: 1.0, color: '#FFE', fixture: true },
torchFloor: { type: 'torchFloor', hp: 1, walkable: true, transparent: true, drops: 'fixture_torch', illumination: 1.0, color: '#FEB', fixture: true },
}
export const softTerrain: BlockType[] = [
'grass', 'soil', 'soilGravel',
'torchLeft', 'torchRight', 'torchCeiling', 'torchFloor',
]
export const hardTerrain: BlockType[] = ['stone', 'stoneGravel', 'bedrock', 'brickWall']
export const level = {
treeTop: 9,
ground: 14,

View file

@ -3,10 +3,10 @@ import { createNoise2D, type NoiseFunction2D } from 'simplex-noise'
import createBlockGenerator from './blockGen'
import createBlockExtender from './blockExt'
import { blockTypes, blockTypes as T } from './def'
import { level as L, blockTypes, blockTypes as T } from './def'
import type { Block, Change } from '../types.d'
const MAX_LIGHT = 100 // maximum level where light shines
const MAX_LIGHT = L.underground // maximum level where light shines
export default function createLevel(width: number, height: number, seed = 'extremely random seed') {
const prng = alea(seed)

View file

@ -26,6 +26,8 @@ export const items = {
ore_ruby: { id: 'ore_ruby', name: 'ruby', type: 'ore', icon: 'ore_ruby' } as Item,
ore_diamond: { id: 'ore_diamond', name: 'diamond', type: 'ore', icon: 'ore_diamond' } as Item,
ore_emerald: { id: 'ore_emerald', name: 'emerald', type: 'ore', icon: 'ore_emerald' } as Item,
fixture_torch: { id: 'fixture_torch', name: 'Torch', type: 'fixture', icon: 'torch', builds: 'torch' } as Item,
} as const
export type ItemId = keyof typeof items

36
src/types.d.ts vendored
View file

@ -1,6 +1,7 @@
import type { ItemId } from './level/items'
export type { ItemId } from './level/items'
export type ItemQuality = 'wood' | 'iron' | 'diamond'
export type ItemType = 'tool' | 'block' | 'ore'
export type ItemType = 'tool' | 'block' | 'ore' | 'fixture'
export interface Item {
id: string
@ -8,7 +9,9 @@ export interface Item {
type: ItemType
icon: string
quality?: ItemQuality
builds?: BlockType
// this should be ItemId | BlockType, but has to be string to avoid
// a circular type reference
builds?: string
}
export interface ToolItem extends Item {
@ -21,6 +24,8 @@ export interface BlockItem extends Item {
builds: BlockType
}
type Fixture<T extends string> = `${T}Left` | `${T}Right` | `${T}Ceiling` | `${T}Floor`;
export type BlockType =
| 'air' | 'grass'
| 'treeCrown' | 'treeLeaves' | 'treeTrunk' | 'treeRoot'
@ -28,15 +33,18 @@ export type BlockType =
| 'stone' | 'stoneGravel'
| 'bedrock' | 'cave'
| 'brickWall'
| Fixture<'torch'>
export type Block = {
type: BlockType, // what is it?
hp: number, // how long do I need to hit it?
walkable: boolean, // can I walk through it?
climbable?: boolean, // can I climb it?
transparent?: boolean, // can I see through it?
illuminated?: boolean, // is it glowing?
drops?: ItemId, // what do I get, when loot it?
type: BlockType // what is it?
hp: number // how long do I need to hit it?
walkable: boolean // can I walk through it?
climbable?: boolean // can I climb it?
transparent?: boolean // can I see through it?
fixture?: boolean // is it built by the player?
illumination?: number // How many blocks wide is it glowing?
color?: string // How is it coloured?
drops?: ItemId // what do I get, when loot it?
}
// describes a changed block, eg digged or placed by the player
@ -52,11 +60,10 @@ type ChangedBlock = {
y: number
newType: BlockType
}
type Change = DamagedBlock | ChangedBlock
export type Change = DamagedBlock | ChangedBlock
export interface InventoryItem extends Item {
amount: number
quality: ItemQuality | null
}
export interface Moveable {
@ -78,3 +85,10 @@ export interface Player extends Moveable {
export type Direction = 'at' | 'left' | 'right' | 'up' | 'down'
export interface LightSource {
x: number
y: number
strength: number
color: string
}

View file

@ -1,116 +1,87 @@
import type { Ref, ComputedRef } from 'vue'
import type { LightSource } from '../types.d'
import { BLOCK_SIZE, STAGE_WIDTH, STAGE_HEIGHT } from '../level/def'
type RefOrComputed<T> = Ref<T> | ComputedRef<T>
export default function useLightMask(
ctx: CanvasRenderingContext2D,
x: RefOrComputed<number>,
y: RefOrComputed<number>,
tx: RefOrComputed<number>,
ty: RefOrComputed<number>,
time: RefOrComputed<number>,
lightBarrier: RefOrComputed<number[]>,
lightSources: RefOrComputed<LightSource[]>,
) {
const W = ((STAGE_WIDTH + 2) * BLOCK_SIZE)
const H = ((STAGE_HEIGHT + 2) * BLOCK_SIZE)
const B = BLOCK_SIZE - 4 // no idea why there is a difference, but it is 4px
const BHalf = B / 2
const playerX = (W - B) / 2 + B / 4
const playerY = H / 2 - B / 2
const playerY = H / 2 - BHalf
const playerLightSize = B * 1.8
function getAmbientLightColor() {
const t = time.value
// Night time (pale bluish dark: hslpicker.com/#2b293d )
if (t > 900 || t < 100) {
return `hsl(245, 20%, 20%)`
}
// Morning hours (gradually more reddish hue)
if (t < 250) {
const s = Math.round((t - 100) / 1.5) // 0-100%
const l = Math.round((t - 100) / 1.875) + 20 // 20-100%
return `hsl(0, ${s}%, ${l}%)`
}
// Evening hours (from neutral white to bluish hue with low saturation)
if (t > 700) {
const s = 100 - Math.round((t - 700) / 2.5) // 100-20%
return `hsl(245, ${s}%, ${s}%)`
}
// day (neutral white)
return `hsl(0, 0%, 100%)`
}
function drawPlayerLight(sizeMul:number) {
const t = time.value
const playerLight = ctx.createRadialGradient(
playerX - tx.value, playerY - ty.value, 0,
playerX - tx.value, playerY - ty.value, playerLightSize * sizeMul
)
// Add color stops for a light around the player
// Night time, lets tone down the light a bit
if (t > 900 || t < 100) {
playerLight.addColorStop(0.7, "#AA7A");
playerLight.addColorStop(1, "#AA70");
// Morning
} else if (t < 150) {
playerLight.addColorStop(0.7, "#CCAA");
playerLight.addColorStop(1, "#CCA0");
// Day (neutral white)
} else {
playerLight.addColorStop(0.7, "#FFFA");
playerLight.addColorStop(1, "#FFF0");
}
playerLight.addColorStop(0.7, "#FFFA");
playerLight.addColorStop(1, "#FFF0");
// Set the fill style and draw a rectangle
ctx.fillStyle = playerLight;
ctx.fillRect(0, 0, W, H)
}
function drawLights() {
// used for everything above ground
const ambientLight = getAmbientLightColor()
function drawLightSources() {
for (const src of lightSources.value) {
const x = src.x * B
const y = src.y * B
const strength = src.strength + (src.strength * Math.random()) / 10
const light = ctx.createRadialGradient(
x + BHalf, y - BHalf, 0,
x + BHalf, y - BHalf, strength * B,
)
const color = src.color
light.addColorStop(0.0, color)
light.addColorStop(0.4, `${color}A`)
light.addColorStop(1, `${color}0`)
ctx.fillStyle = light
ctx.fillRect(0, 0, W, H)
}
}
function drawShadows() {
const barrier = lightBarrier.value
ctx.fillStyle = ambientLight
for (let col = 0; col < W / B; col++) {
for (let col = 0; col < barrier.length; col++) {
const level = (barrier[col] - y.value) * B
const sw = B
const sh = level
const sx = col * sw
const sy = 0
const x = B*col
ctx.fillRect(sx, sy, sw, sh)
// gradient for the shadow that is cast down from the surface
const gradient = ctx.createLinearGradient(0, 0, 0, H)
gradient.addColorStop(0, '#FFF')
gradient.addColorStop(Math.min(level / H, 1), '#FFF')
gradient.addColorStop(Math.min((level + B) / H, 1), '#000')
ctx.fillStyle = gradient
ctx.fillRect(x, 0, B, H)
}
// make light columns wider to illuminate surrounding blocks
const extra = Math.floor(B / 2)
ctx.fillStyle = ambientLight.slice(0, -1) + ', 40%)'
for (let col = 0; col < W / B; col++) {
const level = (barrier[col] - y.value) * B
const sw = B
const sh = level
const sx = col * sw
const sy = 0
ctx.fillRect(sx - extra, sy - extra, sw + extra * 2, sh + extra * 2)
}
// TODO: draw light for candles and torches
}
return function update() {
// first, throw the world in complete darkness
ctx.fillStyle = '#000000'
ctx.fillStyle = '#FFFFFF'
ctx.fillRect(0, 0, W, H)
// second, find and bring light into the world
drawLights()
// second, hide what is beneath
drawShadows()
// third, fight the darkness
drawLightSources()
// finally, draw the players light
// with a size multiplicator which might be later used to

View file

@ -11,7 +11,7 @@ const player = reactive<Player>({
inventory: [],
})
const pocket = (newItem: Item) => {
const pocket = (newItem: Item, amount = 1) => {
const existing = player.inventory.find(item => item.name === newItem.name)
if (existing) {
@ -19,9 +19,8 @@ const pocket = (newItem: Item) => {
return existing.amount
}
player.inventory.push({
quality: null,
amount: 1,
...newItem
...newItem,
amount,
})
return 1
}

View file

@ -46,7 +46,7 @@ export function calcSunAngle(tick: number): number {
export default function useTime() {
// the day is split in 1000 parts, so we start in the morning
const time = ref(250)
const time = ref(240)
function updateTime() {
time.value = (time.value + 0.1) % 1000