simplify light mask, gradient shadows, torches (without light)

This commit is contained in:
Norman Köhring 2025-03-19 16:49:44 +01:00
parent 8f281fbdf9
commit 72c5cd1d19
11 changed files with 100 additions and 97 deletions

BIN
public/Items/torch.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -5,7 +5,7 @@ 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'))
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))
@ -46,6 +47,8 @@ const lightBarrier = computed<number[]>(() => {
return level.sunLight(floorX.value)
})
const lightSources = computed(() => [])
const mapUpdateCount = ref(0)
const mapGrid = computed<Block[][]>(() => {
const _update = mapUpdateCount.value // reactivity trigger
@ -127,20 +130,29 @@ 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', blockX, blockY, block.type, inventorySelection.value.type, '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'
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
console.log({ canBuild, hasSpace, hasTool, blockInHand })
// 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 +226,11 @@ 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, floorX, floorY,
tx, ty, time,
lightBarrier, lightSources,
)
} else {
console.warn('lightmap deactivated')
}
@ -265,7 +281,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,7 @@
.block.brickWall {
background-image: url(/Tiles/brick_grey.png);
}
.block.torch { background-image: url("/Items/torch.png"); }
#field {
user-select: none;
@ -112,17 +113,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 +133,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/torch.png"); }

View file

@ -26,9 +26,13 @@ export const blockTypes: Record<BlockType, Block> = {
stone: { type: 'stone', hp: 10, walkable: false, drops: 'block_stone' },
bedrock: { type: 'bedrock', hp: 25, walkable: false, drops: 'block_stone' },
// Built Blocks
brickWall: { type: 'brickWall', hp: 25, walkable: false, drops: 'block_gravel' },
brickWall: { type: 'brickWall', hp: 25, walkable: false, drops: 'block_gravel', fixture: true },
torch: { type: 'torch', hp: 1, walkable: true, transparent: true, drops: 'fixture_torch', illuminated: true, fixture: true },
}
export const softTerrain: BlockType[] = ['grass', 'soil', 'soilGravel', 'torch']
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

10
src/types.d.ts vendored
View file

@ -1,6 +1,6 @@
import 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
@ -28,6 +28,7 @@ export type BlockType =
| 'stone' | 'stoneGravel'
| 'bedrock' | 'cave'
| 'brickWall'
| 'torch'
export type Block = {
type: BlockType, // what is it?
@ -36,6 +37,7 @@ export type Block = {
climbable?: boolean, // can I climb it?
transparent?: boolean, // can I see through it?
illuminated?: boolean, // is it glowing?
fixture?: boolean, // is it built by the player?
drops?: ItemId, // what do I get, when loot it?
}
@ -78,3 +80,9 @@ export interface Player extends Moveable {
export type Direction = 'at' | 'left' | 'right' | 'up' | 'down'
export interface LightSource {
x: number
y: number
strength: number
}

View file

@ -1,4 +1,5 @@
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>
@ -11,37 +12,17 @@ export default function useLightMask(
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
@ -50,67 +31,53 @@ export default function useLightMask(
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 light = ctx.createRadialGradient(x, y, 0, x, y, src.strength)
light.addColorStop(0.0, "#CC8F")
light.addColorStop(0.4, "#CC8A")
light.addColorStop(1, "#CC80")
}
}
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

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