diff --git a/src/common/display/text.ts b/src/common/display/text.ts index a999e8d..8a97b29 100644 --- a/src/common/display/text.ts +++ b/src/common/display/text.ts @@ -17,7 +17,7 @@ const generateColors = () => { const colors: string[] = []; for (let i = 0; i < 16; i++) { const h = (i & 0b1000) ? 'ff' : '7f'; - + const r = (i & 0b0100) ? h : '00'; const g = (i & 0b0010) ? h : '00'; const b = (i & 0b0001) ? h : '00'; @@ -188,7 +188,9 @@ export class TextDisplay { } private setCharRaw(x: number, y: number, char: string, fg: IColorLike, bg: IColorLike) { - if (x < this.clipLeft || y < this.clipTop || x >= this.clipRight || y >= this.clipBottom || !char) return; + if (x < this.clipLeft || y < this.clipTop || x >= this.clipRight || y >= this.clipBottom) return; + if (!char || char === '\0') return; + const i = (y | 0) * this.width + (x | 0); let dirty = false; if (this.chars[i] !== char) { @@ -239,7 +241,9 @@ export class TextDisplay { const dispBase = row * this.width + x0; const regRowStr = chars[regRow]; for (let i = 0; i < copyW; i++) { - this.chars[dispBase + i] = regRowStr[regColOffset + i]; + const ch = regRowStr[regColOffset + i]; + if (ch === '\0') continue; + this.chars[dispBase + i] = ch; this.fgs[dispBase + i] = fgs[regBase + i]; this.bgs[dispBase + i] = bgs[regBase + i]; this.drawCell(x0 + i, row); @@ -452,6 +456,18 @@ export class TextRegion { } } + get(x: number, y: number): IDefinedChar { + const i = y * this.width + x; + return [this.#chars[y][x], this.#fgs[i], this.#bgs[i]]; + } + + set(x: number, y: number, char: IChar) { + const ch = parseChar(char); + this.#chars[y] = this.#chars[y].slice(0, x) + ch[0] + this.#chars[y].slice(x + 1); + this.#fgs[y * this.width + x] = ch[1]; + this.#bgs[y * this.width + x] = ch[2]; + } + get [REGION_DATA]() { return { chars: this.#chars, diff --git a/src/common/rpg/components/position.ts b/src/common/rpg/components/position.ts index 60ae3a0..c6a6dd2 100644 --- a/src/common/rpg/components/position.ts +++ b/src/common/rpg/components/position.ts @@ -2,9 +2,9 @@ import { Component, Entity } from "../core/world"; import { component } from "../utils/decorators"; @component({ variables: ['x', 'y', 'z'] }) -export class Position extends Component<{ x: number, y: number, z: number }> { - constructor(x = 0, y = 0, z = 0) { - super({ x, y, z }); +export class Position extends Component<{ x: number, y: number, z: number, absolute: boolean }> { + constructor(x = 0, y = 0, z = 0, absolute = false) { + super({ x, y, z, absolute }); } get x() { @@ -25,6 +25,19 @@ export class Position extends Component<{ x: number, y: number, z: number }> { set z(z: number) { this.state.z = z; } + get absolute() { + return this.state.absolute; + } + set absolute(absolute: boolean) { + this.state.absolute = absolute; + } } export const getPosition = (entity?: Entity, key?: string) => entity?.get(Position, key)?.state; +export const move = (entity: Entity, dx: number, dy: number, dz = 0) => { + const pos = getPosition(entity); + if (!pos) return; + pos.x += dx; + pos.y += dy; + pos.z += dz; +}; \ No newline at end of file diff --git a/src/common/rpg/components/render/viewport.ts b/src/common/rpg/components/render/viewport.ts index 76e5f3e..bdb5260 100644 --- a/src/common/rpg/components/render/viewport.ts +++ b/src/common/rpg/components/render/viewport.ts @@ -37,14 +37,6 @@ export class Viewport extends Component { get worldY(): number { return Math.round(getPosition(this.entity)?.y ?? 0); } - - move(dx: number, dy: number) { - const pos = getPosition(this.entity); - if (!pos) return; - - pos.x += dx; - pos.y += dy; - } } export const createViewport = (world: World, viewportData: ViewportData) => { diff --git a/src/common/rpg/systems/render/text.ts b/src/common/rpg/systems/render/text.ts index d24a913..995b426 100644 --- a/src/common/rpg/systems/render/text.ts +++ b/src/common/rpg/systems/render/text.ts @@ -21,16 +21,12 @@ export class TextDisplaySystem extends System { x: viewport.worldX - viewport.screenX, y: viewport.worldY - viewport.screenY, } : { x: 0, y: 0 }; - const clipRect = this.display.getClipRect(); - if (viewport) { - this.display.setClipRect({ x: viewport.screenX, y: viewport.screenY, width: viewport.width, height: viewport.height }); - } const sprites = Array.from(world.query(Sprite, Position)).sort((a, b) => a[2].state.z - b[2].state.z); for (const [e, sprite, pos] of sprites) { if (e.has(Hidden)) continue; const image = sprite.image; - const { x, y } = pos.state; + const { x, y, absolute } = pos; const data = Resources.get(TextRegion, image) @@ -38,9 +34,17 @@ export class TextDisplaySystem extends System { ?? image; const region = data instanceof TextRegion ? data : new TextRegion(data); - this.display.setRegion(x - offset.x, y - offset.y, region); - } - this.display.setClipRect(clipRect); + if (absolute) { + this.display.setRegion(x, y, region); + } else { + const clipRect = this.display.getClipRect(); + if (viewport) { + this.display.setClipRect({ x: viewport.screenX, y: viewport.screenY, width: viewport.width, height: viewport.height }); + } + this.display.setRegion(x - offset.x, y - offset.y, region); + this.display.setClipRect(clipRect); + } + } } } \ No newline at end of file diff --git a/src/games/crawler/index.ts b/src/games/crawler/index.ts index 6c5dd8f..135471f 100644 --- a/src/games/crawler/index.ts +++ b/src/games/crawler/index.ts @@ -1,60 +1,170 @@ -import { TextRegion } from "@common/display/text"; +import { Color, TextRegion } from "@common/display/text"; import { gameLoop } from "@common/game"; +import type { Point } from "@common/geometry"; import Input from "@common/input"; import { BSP } from "@common/level/bsp"; +import { bresenhamCircleGen } from "@common/navigation/bresenham"; import { SeededRandom } from "@common/random"; -import { Position } from "@common/rpg/components/position"; -import { createViewport, Viewport } from "@common/rpg/components/render/viewport"; +import { getPosition, move, Position } from "@common/rpg/components/position"; +import { createViewport } from "@common/rpg/components/render/viewport"; import { Sprite } from "@common/rpg/components/sprite"; import { World } from "@common/rpg/core/world"; import { TextDisplaySystem } from "@common/rpg/systems/render/text"; import { Resources } from "@common/rpg/utils/resources"; +const WALL = '#'; +const PLAYER = '@'; function createMap(world: World, random: SeededRandom) { - const mapSize = 100; - const mapData = new Array(mapSize * mapSize).fill('#'); + const MAP_SIZE = 100; + const mapData = new Array(MAP_SIZE * MAP_SIZE).fill(WALL); - BSP.generateLevel(mapSize, mapSize, (x, y) => { mapData[x + y * mapSize] = '.'; }, { + BSP.generateLevel(MAP_SIZE, MAP_SIZE, (x, y) => { mapData[x + y * MAP_SIZE] = '.'; }, { minWidth: 8, minHeight: 8, depth: 8, random, }); + const mapDataString: string[] = []; + const maskDataString: string[] = []; + for (let i = 0; i < MAP_SIZE; i++) { + mapDataString.push(mapData.slice(i * MAP_SIZE, (i + 1) * MAP_SIZE).join('')); + } + for (let i = 0; i < MAP_SIZE * 3; i++) { + maskDataString.push(Array.from({ length: MAP_SIZE * 3 }, () => ' ').join('')); + } const map = world.createEntity('map'); - const mapDataString: string[] = []; - for (let i = 0; i < mapSize; i++) { - mapDataString.push(mapData.slice(i * mapSize, (i + 1) * mapSize).join('')); - } - Resources.add('map', new TextRegion(mapDataString)); - map.add(new Sprite('map')); - map.add(new Position(0, 0)); + map.add(new Sprite(Resources.add('map', new TextRegion(mapDataString)))); + map.add(new Position(0, 0, -2)); - return map; + const mask = world.createEntity('mask'); + mask.add(new Sprite(Resources.add('mask', new TextRegion(maskDataString)))); + mask.add(new Position(-MAP_SIZE, -MAP_SIZE, 9)); + + return { map, mask }; +} + +function createPlayer(world: World, x = 0, y = 0) { + const player = world.createEntity('player'); + player.add(new Position(x, y, 10)); + player.add(new Sprite(Resources.add('player', new TextRegion(PLAYER, Color.YELLOW)))); + ; + return player; +} + +function createFPS(world: World, x = 0, y = 0) { + const fps = world.createEntity('fps'); + fps.add(new Position(x, y, 100, true)); + fps.add(new Sprite(Resources.add('fps', '60'))); + return fps; } export default gameLoop(() => { const world = new World(); const display = world.addSystem(new TextDisplaySystem(100, 25)).display; - const random = new SeededRandom('awoorwa'); - const map = createMap(world, random); - const viewportEntity = createViewport(world, { - width: display.width >> 1, + const random = new SeededRandom(); + const { map, mask } = createMap(world, random); + + const mapData = Resources.get(TextRegion, map.get(Sprite)!.image)!; + const emptyCells: Point[] = []; + for (let x = 0; x < mapData.width; x++) { + for (let y = 0; y < mapData.height; y++) { + if (mapData.get(x, y)[0] === '.') { + emptyCells.push({ x, y }); + } + } + } + + const maskData = Resources.get(TextRegion, mask.get(Sprite)!.image)!; + const startCell = random.choice(emptyCells); + + const viewport = createViewport(world, { + width: display.width, height: display.height, - worldX: 0, - worldY: 0, - screenX: display.width >> 1, + worldX: startCell.x - (display.width >> 1), + worldY: startCell.y - (display.height >> 1), + screenX: 0, screenY: 0, }); - const viewport = viewportEntity.get(Viewport)!; - return { world, map, random, viewport }; -}, (dt, { world, viewport }) => { - const dx = Input.getHorizontal(); - const dy = Input.getVertical(); + const player = createPlayer(world, startCell.x, startCell.y); + createFPS(world, 0, 0); - viewport.move(dx * dt * 32, dy * dt * 16); + return { + world, + map, mapData, + mask, maskData, maskDirty: true, + fps: 0, + fpsTimer: 0, + random, + viewport, + player, + lastMove: 0, now: 0, + isWall: (x: number, y: number): boolean => { + const [ch] = mapData.get(x, y); + return ch === WALL; + }, + }; +}, (dt, state) => { + const { world, viewport, player, lastMove, now, mapData, maskData, isWall } = state; + + let dx = -Math.sign(Input.getHorizontal()); + let dy = -Math.sign(Input.getVertical()); + const playerPos = getPosition(player)!; + + if (now - lastMove > 0.03) { + if (isWall(playerPos.x + dx, playerPos.y)) { + dx = 0; + } + if (isWall(playerPos.x, playerPos.y + dy)) { + dy = 0; + } + if (isWall(playerPos.x + dx, playerPos.y + dy)) { + dx = 0 + dy = 0; + } + } else { + dx = 0; + dy = 0; + } + + if (dx || dy) { + move(player, dx, dy); + move(viewport, dx, dy); + + playerPos.x + dx; + playerPos.y + dy; + + state.maskDirty = true; + state.lastMove = now; + } + + if (state.maskDirty) { + for (let x = 0; x < mapData.width; x++) { + for (let y = 0; y < mapData.height; y++) { + const [ch, fg] = maskData.get(x + mapData.width, y + mapData.height); + if (ch !== ' ' && fg !== Color.GRAY) { + maskData.set(x + mapData.width, y + mapData.height, [mapData.get(x, y)[0], Color.GRAY]); + } + } + } + for (const { x, y } of bresenhamCircleGen(playerPos.x, playerPos.y, 10, { fill: 'shadow', breaker: isWall })) { + maskData.set(x + mapData.width, y + mapData.height, '\0'); + } + state.maskDirty = false; + } world.update(dt); + + state.fpsTimer += dt; + if (state.fpsTimer >= 0.5) { + Resources.update('fps', (state.fps * 2).toString()); + state.fps = 0; + state.fpsTimer = 0; + } else { + state.fps++; + }; + + state.now += dt; });