Compare commits
No commits in common. "5796f914e3e4fa0b671c59259e24582b1d711ca2" and "0077def410a788622862f257fff0ed5848de441e" have entirely different histories.
5796f914e3
...
0077def410
|
|
@ -1,7 +1,6 @@
|
|||
import '@common/assets/fonts/vga.font.css';
|
||||
import { randInt } from "@common/utils";
|
||||
import { createCanvas } from './canvas';
|
||||
import type { Rect } from '@common/geometry';
|
||||
|
||||
export type IColorLike = string | number | Color;
|
||||
export type IChar = [string, IColorLike?, IColorLike?] | string;
|
||||
|
|
@ -108,10 +107,6 @@ export class TextDisplay {
|
|||
private ctx: CanvasRenderingContext2D;
|
||||
private font = FALLBACK_FONT;
|
||||
private letterboxColor: string;
|
||||
private clipLeft: number = 0;
|
||||
private clipTop: number = 0;
|
||||
private clipRight: number;
|
||||
private clipBottom: number;
|
||||
|
||||
constructor(
|
||||
public width = GAME_WIDTH,
|
||||
|
|
@ -143,9 +138,6 @@ export class TextDisplay {
|
|||
this.font = loaded ? NATIVE_FONT : FALLBACK_FONT;
|
||||
this.redraw();
|
||||
});
|
||||
|
||||
this.clipRight = width;
|
||||
this.clipBottom = height;
|
||||
}
|
||||
|
||||
private updateScale() {
|
||||
|
|
@ -188,7 +180,7 @@ 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 < 0 || y < 0 || y >= this.height || x >= this.width || !char) return;
|
||||
const i = (y | 0) * this.width + (x | 0);
|
||||
let dirty = false;
|
||||
if (this.chars[i] !== char) {
|
||||
|
|
@ -214,7 +206,7 @@ export class TextDisplay {
|
|||
|
||||
getChar(x: number, y: number): IDefinedChar {
|
||||
x = x | 0; y = y | 0;
|
||||
if (x < this.clipLeft || y < this.clipTop || x >= this.clipRight || y >= this.clipBottom) {
|
||||
if (x < 0 || y < 0 || y >= this.height || x >= this.width) {
|
||||
return [' ', DEFAULT_FG, DEFAULT_BG];
|
||||
}
|
||||
return [this.chars[y * this.width + x], this.fgs[y * this.width + x], this.bgs[y * this.width + x]];
|
||||
|
|
@ -225,10 +217,10 @@ export class TextDisplay {
|
|||
y = y | 0;
|
||||
const { chars, fgs, bgs } = region[REGION_DATA];
|
||||
const rw = region.width;
|
||||
const x0 = Math.max(this.clipLeft, x);
|
||||
const y0 = Math.max(this.clipTop, y);
|
||||
const x1 = Math.min(this.clipRight, x + rw);
|
||||
const y1 = Math.min(this.clipBottom, y + region.height);
|
||||
const x0 = Math.max(0, x);
|
||||
const y0 = Math.max(0, y);
|
||||
const x1 = Math.min(this.width, x + rw);
|
||||
const y1 = Math.min(this.height, y + region.height);
|
||||
const copyW = x1 - x0;
|
||||
if (copyW <= 0) return;
|
||||
|
||||
|
|
@ -260,7 +252,7 @@ export class TextDisplay {
|
|||
const bgRow: IColorLike[] = [];
|
||||
for (let col = 0; col < w; col++) {
|
||||
const dispCol = x + col;
|
||||
if (dispCol < this.clipLeft || dispRow < this.clipTop || dispCol >= this.clipRight || dispRow >= this.clipBottom) {
|
||||
if (dispRow < 0 || dispRow >= this.height || dispCol < 0 || dispCol >= this.width) {
|
||||
rowStr += ' ';
|
||||
fgRow.push(DEFAULT_FG);
|
||||
bgRow.push(DEFAULT_BG);
|
||||
|
|
@ -285,7 +277,7 @@ export class TextDisplay {
|
|||
for (let row = 0; row < lines.length; row++) {
|
||||
const line = lines[row];
|
||||
const ry = y + row;
|
||||
if (ry < this.clipTop || ry >= this.clipBottom) continue;
|
||||
if (ry < 0 || ry >= this.height) continue;
|
||||
for (let col = 0; col < line.length; col++) {
|
||||
this.setCharRaw(x + col, ry, line[col], fg, bg);
|
||||
}
|
||||
|
|
@ -296,9 +288,9 @@ export class TextDisplay {
|
|||
x = x | 0; y1 = y1 | 0; y2 = y2 | 0;
|
||||
if (y2 < y1) { const t = y2; y2 = y1; y1 = t; }
|
||||
|
||||
y1 = Math.max(this.clipTop, y1);
|
||||
y2 = Math.min(this.clipBottom - 1, y2);
|
||||
if (x < this.clipLeft || x >= this.clipRight) return;
|
||||
y1 = Math.max(0, y1);
|
||||
y2 = Math.min(this.height - 1, y2);
|
||||
if (x < 0 || x >= this.width) return;
|
||||
|
||||
const ch = parseChar(char);
|
||||
for (let y = y1; y <= y2; y++) {
|
||||
|
|
@ -309,9 +301,9 @@ export class TextDisplay {
|
|||
drawHLine(x1: number, x2: number, y: number, char: IChar = '─') {
|
||||
x1 = x1 | 0; x2 = x2 | 0; y = y | 0;
|
||||
if (x2 < x1) { const t = x2; x2 = x1; x1 = t; }
|
||||
x1 = Math.max(this.clipLeft, x1);
|
||||
x2 = Math.min(this.clipRight - 1, x2);
|
||||
if (y < this.clipTop || y >= this.clipBottom) return;
|
||||
x1 = Math.max(0, x1);
|
||||
x2 = Math.min(this.width - 1, x2);
|
||||
if (y < 0 || y >= this.height) return;
|
||||
|
||||
const ch = parseChar(char);
|
||||
for (let x = x1; x <= x2; x++) {
|
||||
|
|
@ -378,22 +370,6 @@ export class TextDisplay {
|
|||
this.drawHLine(x, x + width - 1, i, char);
|
||||
}
|
||||
}
|
||||
|
||||
getClipRect(): Rect {
|
||||
return {
|
||||
x: this.clipLeft,
|
||||
y: this.clipTop,
|
||||
width: this.clipRight - this.clipLeft,
|
||||
height: this.clipBottom - this.clipTop,
|
||||
};
|
||||
}
|
||||
|
||||
setClipRect(rect: Rect) {
|
||||
this.clipLeft = Math.max(0, rect.x);
|
||||
this.clipTop = Math.max(0, rect.y);
|
||||
this.clipRight = Math.min(this.width, rect.x + rect.width);
|
||||
this.clipBottom = Math.min(this.height, rect.y + rect.height);
|
||||
}
|
||||
}
|
||||
|
||||
export class TextRegion {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { nextFrame } from "./utils";
|
|||
|
||||
type Setup<T> = () => Promise<T> | T;
|
||||
type Frame<T> = (dt: number, state: T) => Promise<T | void> | T | void;
|
||||
type GameMain = () => Promise<void>;
|
||||
type GameMain = () => void;
|
||||
|
||||
export function gameLoop<T>(frame: Frame<T>): GameMain;
|
||||
export function gameLoop<T>(setup: Setup<T>, frame: Frame<T>): GameMain;
|
||||
|
|
|
|||
|
|
@ -1,15 +0,0 @@
|
|||
export interface Point {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export interface Rect {
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export const inRect = (x: number, y: number, rect: Rect) => {
|
||||
return x >= rect.x && x < rect.x + rect.width && y >= rect.y && y < rect.y + rect.height;
|
||||
}
|
||||
|
|
@ -1,216 +0,0 @@
|
|||
import { inRect } from "@common/geometry";
|
||||
import { SeededRandom } from "@common/random";
|
||||
|
||||
export namespace BSP {
|
||||
export interface BSPOptions {
|
||||
depth?: number;
|
||||
minWidth?: number;
|
||||
minHeight?: number;
|
||||
seed?: string | number;
|
||||
random?: SeededRandom;
|
||||
}
|
||||
interface Node {
|
||||
id: string;
|
||||
x: number;
|
||||
y: number;
|
||||
width: number;
|
||||
height: number;
|
||||
depth: number;
|
||||
children: Node[];
|
||||
}
|
||||
type Carver = (x: number, y: number, node?: Node) => void;
|
||||
export function generateLevel(
|
||||
width: number,
|
||||
height: number,
|
||||
carver: Carver,
|
||||
options?: BSPOptions,
|
||||
) {
|
||||
const minWidth = options?.minWidth ?? 4;
|
||||
const minHeight = options?.minHeight ?? 4;
|
||||
const depth = options?.depth ?? 4;
|
||||
const random = options?.random ?? new SeededRandom(options?.seed);
|
||||
|
||||
const root: Node = {
|
||||
id: '/',
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: width,
|
||||
height: height,
|
||||
depth: 0,
|
||||
children: []
|
||||
};
|
||||
|
||||
const carved = new Set<string>();
|
||||
|
||||
const dedupCarver = (x: number, y: number, node?: Node) => {
|
||||
if (carved.has(`${x},${y}`)) return;
|
||||
carved.add(`${x},${y}`);
|
||||
carver(x, y, node);
|
||||
}
|
||||
|
||||
const stack = [root];
|
||||
|
||||
while (stack.length > 0) {
|
||||
const node = stack.pop()!;
|
||||
|
||||
if (node.depth >= depth) continue;
|
||||
|
||||
const splitHorizontally = node.width > node.height;
|
||||
|
||||
if (splitHorizontally && node.width > minWidth * 2) {
|
||||
const splitX = random.randInt(minWidth, node.width - minWidth);
|
||||
|
||||
const nodeA: Node = {
|
||||
id: node.id + '/A',
|
||||
x: node.x,
|
||||
y: node.y,
|
||||
width: splitX,
|
||||
height: node.height,
|
||||
depth: node.depth + 1,
|
||||
children: []
|
||||
};
|
||||
const nodeB: Node = {
|
||||
id: node.id + '/B',
|
||||
x: node.x + splitX,
|
||||
y: node.y,
|
||||
width: node.width - splitX,
|
||||
height: node.height,
|
||||
depth: node.depth + 1,
|
||||
children: []
|
||||
};
|
||||
node.children = [nodeA, nodeB];
|
||||
stack.push(nodeA, nodeB);
|
||||
} else if (!splitHorizontally && node.height > minHeight * 2) {
|
||||
const splitY = random.randInt(minHeight, node.height - minHeight);
|
||||
|
||||
const nodeA: Node = {
|
||||
id: node.id + '/A',
|
||||
x: node.x,
|
||||
y: node.y,
|
||||
width: node.width,
|
||||
height: splitY,
|
||||
depth: node.depth + 1,
|
||||
children: []
|
||||
};
|
||||
const nodeB: Node = {
|
||||
id: node.id + '/B',
|
||||
x: node.x,
|
||||
y: node.y + splitY,
|
||||
width: node.width,
|
||||
height: node.height - splitY,
|
||||
depth: node.depth + 1,
|
||||
children: []
|
||||
};
|
||||
node.children = [nodeA, nodeB];
|
||||
stack.push(nodeA, nodeB);
|
||||
} else {
|
||||
// Leaf node
|
||||
node.children = [];
|
||||
}
|
||||
}
|
||||
|
||||
// Carve rooms
|
||||
|
||||
stack.push(root);
|
||||
|
||||
while (stack.length > 0) {
|
||||
const node = stack.pop()!;
|
||||
if (node.children.length === 0) {
|
||||
const width = random.randInt(minWidth, node.width) - 2;
|
||||
const height = random.randInt(minHeight, node.height) - 2;
|
||||
const x = random.randInt(node.x + 1, node.x + node.width - width - 1);
|
||||
const y = random.randInt(node.y + 1, node.y + node.height - height - 1);
|
||||
|
||||
node.x = x;
|
||||
node.y = y;
|
||||
node.width = width;
|
||||
node.height = height;
|
||||
|
||||
carveRoom(x, y, width, height, node, dedupCarver);
|
||||
} else {
|
||||
stack.push(...node.children);
|
||||
}
|
||||
}
|
||||
|
||||
stack.push(root);
|
||||
|
||||
// Carve corridors
|
||||
while (stack.length > 0) {
|
||||
const node = stack.pop()!;
|
||||
|
||||
if (node.children.length === 0) continue;
|
||||
|
||||
const [nodeA, nodeB] = node.children;
|
||||
|
||||
const leafA = findLeaf(nodeA, nodeB);
|
||||
const leafB = findLeaf(nodeB, nodeA);
|
||||
|
||||
carveCorridor(leafA, leafB, dedupCarver, random);
|
||||
stack.push(...node.children);
|
||||
}
|
||||
}
|
||||
|
||||
function carveRoom(x: number, y: number, width: number, height: number, node: Node, carver: Carver) {
|
||||
for (let i = x; i < x + width; i++) {
|
||||
for (let j = y; j < y + height; j++) {
|
||||
carver(i, j, node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function carveCorridor(nodeA: Node, nodeB: Node, carver: Carver, random: SeededRandom) {
|
||||
let ax = random.randInt(nodeA.x, nodeA.x + nodeA.width);
|
||||
let ay = random.randInt(nodeA.y, nodeA.y + nodeA.height);
|
||||
|
||||
const bx = random.randInt(nodeB.x, nodeB.x + nodeB.width);
|
||||
const by = random.randInt(nodeB.y, nodeB.y + nodeB.height);
|
||||
|
||||
const dx = Math.sign(bx - ax);
|
||||
const dy = Math.sign(by - ay);
|
||||
|
||||
for (; ax !== bx; ax += dx) {
|
||||
if (inRect(ax, ay, nodeA)) {
|
||||
continue;
|
||||
}
|
||||
if (inRect(ax, ay, nodeB)) {
|
||||
break;
|
||||
}
|
||||
carver(ax, ay);
|
||||
}
|
||||
for (; ay != by; ay += dy) {
|
||||
if (inRect(ax, ay, nodeA)) {
|
||||
continue;
|
||||
}
|
||||
if (inRect(ax, ay, nodeB)) {
|
||||
break;
|
||||
}
|
||||
carver(ax, ay);
|
||||
}
|
||||
}
|
||||
|
||||
const center = (node: Node) => ({
|
||||
x: node.x + node.width / 2,
|
||||
y: node.y + node.height / 2
|
||||
});
|
||||
|
||||
function findLeaf(node: Node, closestNode: Node): Node {
|
||||
if (node.children.length === 0) {
|
||||
return node;
|
||||
}
|
||||
|
||||
let minDist = Infinity;
|
||||
let closest = null;
|
||||
const closestCenter = center(closestNode);
|
||||
|
||||
for (const child of node.children.map(child => findLeaf(child, closestNode))) {
|
||||
const childCenter = center(child);
|
||||
const dist = Math.abs(childCenter.x - closestCenter.x) + Math.abs(childCenter.y - closestCenter.y);
|
||||
if (dist < minDist) {
|
||||
minDist = dist;
|
||||
closest = child;
|
||||
}
|
||||
}
|
||||
|
||||
return closest ?? node;
|
||||
}
|
||||
}
|
||||
|
|
@ -263,7 +263,7 @@ export const bresenhamLine = (
|
|||
* to that point and yields every cell along the ray. You can pass `breaker`
|
||||
* to signal an obstacle — the rest of that ray is then
|
||||
* skipped and the next outline point's ray begins. The obstacle cell itself
|
||||
* is yielded (obstacle is visible).
|
||||
* is not yielded (obstacle is hidden).
|
||||
*
|
||||
* **Bounds:** The outline is always generated without clipping so that rays
|
||||
* extend to the full circle perimeter. The individual rays are clipped to
|
||||
|
|
@ -299,8 +299,8 @@ export function* bresenhamCircleGen(
|
|||
};
|
||||
for (const { x: ox, y: oy } of bresenhamCircleGen(x, y, r, { fill: false })) {
|
||||
for (const linePoint of bresenhamLineGen(x, y, ox, oy, lineBounds)) {
|
||||
yield linePoint;
|
||||
if (options?.breaker?.(linePoint.x, linePoint.y)) break;
|
||||
yield linePoint;
|
||||
}
|
||||
}
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -10,9 +10,11 @@ export function* shadowCast(
|
|||
const seen = new Set<string>();
|
||||
const keyOf = (px: number, py: number) => `${px},${py}`;
|
||||
|
||||
// Yield the source tile itself if it is not blocked.
|
||||
if (!breaker(x, y)) {
|
||||
seen.add(keyOf(x, y));
|
||||
yield { x, y };
|
||||
if (breaker(x, y)) return;
|
||||
}
|
||||
|
||||
type Frame = {
|
||||
octant: number;
|
||||
|
|
@ -66,7 +68,7 @@ export function* shadowCast(
|
|||
|
||||
const opaque = breaker(mapX, mapY);
|
||||
|
||||
if (dx * dx + dy * dy <= radiusSq) {
|
||||
if (dx * dx + dy * dy <= radiusSq && !opaque) {
|
||||
const key = keyOf(mapX, mapY);
|
||||
if (!seen.has(key)) {
|
||||
seen.add(key);
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@ export class SeededRandom {
|
|||
throw new RangeError(`randInt: bounds must be integers, got (${min}, ${hi})`);
|
||||
}
|
||||
if (min >= hi) {
|
||||
return hi;
|
||||
throw new RangeError(`randInt: min (${min}) must be strictly less than max (${hi})`);
|
||||
}
|
||||
|
||||
const range = hi - min;
|
||||
|
|
|
|||
|
|
@ -1,73 +0,0 @@
|
|||
import { Component, World } from "@common/rpg/core/world";
|
||||
import { component } from "@common/rpg/utils/decorators";
|
||||
import { getPosition, Position } from "../position";
|
||||
|
||||
export interface ViewportData {
|
||||
screenX: number;
|
||||
screenY: number;
|
||||
width: number;
|
||||
height: number;
|
||||
worldX: number;
|
||||
worldY: number;
|
||||
}
|
||||
|
||||
@component
|
||||
export class Viewport extends Component<void> {
|
||||
|
||||
get screenX(): number {
|
||||
return Math.round(getPosition(this.entity, 'screen')?.x ?? 0);
|
||||
}
|
||||
|
||||
get screenY(): number {
|
||||
return Math.round(getPosition(this.entity, 'screen')?.y ?? 0);
|
||||
}
|
||||
|
||||
get width(): number {
|
||||
return Math.round(getPosition(this.entity, 'size')?.x ?? Infinity);
|
||||
}
|
||||
|
||||
get height(): number {
|
||||
return Math.round(getPosition(this.entity, 'size')?.y ?? Infinity);
|
||||
}
|
||||
|
||||
get worldX(): number {
|
||||
return Math.round(getPosition(this.entity)?.x ?? 0);
|
||||
}
|
||||
|
||||
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) => {
|
||||
const viewport = world.createEntity();
|
||||
viewport.add(new Viewport());
|
||||
viewport.add(new Position(viewportData.worldX, viewportData.worldY));
|
||||
viewport.add(new Position(viewportData.width, viewportData.height), 'size');
|
||||
viewport.add(new Position(viewportData.screenX, viewportData.screenY), 'screen');
|
||||
|
||||
return viewport;
|
||||
}
|
||||
|
||||
export const getViewport = (world: World): ViewportData | null => {
|
||||
for (const [, , viewport] of world.query(Viewport)) {
|
||||
return {
|
||||
screenX: viewport.screenX,
|
||||
screenY: viewport.screenY,
|
||||
width: viewport.width,
|
||||
height: viewport.height,
|
||||
worldX: viewport.worldX,
|
||||
worldY: viewport.worldY,
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
|
@ -364,10 +364,9 @@ export class World {
|
|||
}
|
||||
}
|
||||
|
||||
addSystem<T extends System>(system: T): T {
|
||||
addSystem(system: System): void {
|
||||
this.#systems.push(system);
|
||||
system.onAdd(this);
|
||||
return system;
|
||||
}
|
||||
|
||||
removeSystem(system: System): void {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { TextRegion, TextDisplay } from "@common/display/text";
|
||||
import { Position } from "@common/rpg/components/position";
|
||||
import { getViewport } from "@common/rpg/components/render/viewport";
|
||||
import { Hidden, Sprite } from "@common/rpg/components/sprite";
|
||||
import { System, World } from "@common/rpg/core/world";
|
||||
import { Resources } from "@common/rpg/utils/resources";
|
||||
|
|
@ -14,16 +13,6 @@ export class TextDisplaySystem extends System {
|
|||
}
|
||||
|
||||
override update(world: World) {
|
||||
const viewport = getViewport(world);
|
||||
const offset = viewport ? {
|
||||
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;
|
||||
|
|
@ -36,9 +25,7 @@ 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);
|
||||
this.display.setRegion(x, y, region);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -32,12 +32,4 @@ export namespace Resources {
|
|||
resources.set(ctor, namespace);
|
||||
return id;
|
||||
}
|
||||
|
||||
export function update<T>(id: string, value: NonNullable<T>): string {
|
||||
const ctor = value.constructor;
|
||||
const namespace: Map<string, T> = resources.get(ctor) ?? new Map();
|
||||
namespace.set(id, value);
|
||||
resources.set(ctor, namespace);
|
||||
return id;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
import { TextRegion } from "@common/display/text";
|
||||
import { gameLoop } from "@common/game";
|
||||
import Input from "@common/input";
|
||||
import { BSP } from "@common/level/bsp";
|
||||
import { SeededRandom } from "@common/random";
|
||||
import { Position } from "@common/rpg/components/position";
|
||||
import { createViewport, Viewport } 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";
|
||||
|
||||
|
||||
function createMap(world: World, random: SeededRandom) {
|
||||
const mapSize = 100;
|
||||
const mapData = new Array(mapSize * mapSize).fill('#');
|
||||
|
||||
BSP.generateLevel(mapSize, mapSize, (x, y) => { mapData[x + y * mapSize] = '.'; }, {
|
||||
minWidth: 8,
|
||||
minHeight: 8,
|
||||
depth: 8,
|
||||
random,
|
||||
});
|
||||
|
||||
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));
|
||||
|
||||
return map;
|
||||
}
|
||||
|
||||
export default gameLoop(() => {
|
||||
const world = new World();
|
||||
const display = world.addSystem(new TextDisplaySystem()).display;
|
||||
|
||||
const random = new SeededRandom('awoorwa');
|
||||
const map = createMap(world, random);
|
||||
const viewportEntity = createViewport(world, {
|
||||
width: display.width >> 1,
|
||||
height: display.height,
|
||||
worldX: 0,
|
||||
worldY: 0,
|
||||
screenX: display.width >> 1,
|
||||
screenY: 0,
|
||||
});
|
||||
const viewport = viewportEntity.get(Viewport)!;
|
||||
return { world, map, random, viewport };
|
||||
}, (dt, { world, viewport }) => {
|
||||
const dx = Input.getHorizontal();
|
||||
const dy = Input.getVertical();
|
||||
|
||||
viewport.move(dx * dt * 32, dy * dt * 16);
|
||||
|
||||
world.update(dt);
|
||||
});
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import { createCanvas } from "@common/display/canvas";
|
||||
import { gameLoop } from "@common/game";
|
||||
import { bresenhamCircleGen } from "@common/navigation/bresenham";
|
||||
import { circleGen } from "@common/navigation/bresenham";
|
||||
import Physics from "@common/physics";
|
||||
import { mapNumber } from "@common/utils";
|
||||
|
||||
|
|
@ -162,7 +162,7 @@ const frame = (dt: number, state: State) => {
|
|||
|
||||
function fillCircle(ctx: CanvasRenderingContext2D, xc: number, yc: number, r: number) {
|
||||
// because default circle fill is antialiased
|
||||
for (const { x, y } of bresenhamCircleGen(xc, yc, r, { fill: true })) {
|
||||
for (const { x, y } of circleGen(xc, yc, r, { fill: true })) {
|
||||
ctx.fillRect(x, y, 1, 1);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import { createCanvas } from "@common/display/canvas";
|
||||
import { BSP } from "@common/level/bsp";
|
||||
import { stringHash } from "@common/utils";
|
||||
import { bresenhamCircleGen } from "@common/navigation/bresenham";
|
||||
|
||||
const S = 512;
|
||||
const S = 20;
|
||||
|
||||
const breaker = (x: number, y: number) => Math.hypot(x - S * 0.3, y - S * 0.3) <= S / 16;
|
||||
|
||||
export default async function main() {
|
||||
const canvas = createCanvas(S, S);
|
||||
|
|
@ -10,10 +11,16 @@ export default async function main() {
|
|||
const ctx = canvas.getContext("2d");
|
||||
if (!ctx) return;
|
||||
|
||||
BSP.generateLevel(S, S, (x, y, node) => {
|
||||
ctx.fillStyle = node?.id ? `hsl(${stringHash(node.id) % 360}, 100%, 20%)`: 'black';
|
||||
ctx.fillRect(x, y, 1, 1);
|
||||
}, { depth: 16, minWidth: 8, minHeight: 8 });
|
||||
ctx.fillStyle = 'black';
|
||||
for (const fill of ['fov', 'shadow'] as const) {
|
||||
let i = 0;
|
||||
|
||||
console.log('done');
|
||||
console.time(`fill=${fill}`);
|
||||
for (const { x, y } of bresenhamCircleGen(S / 2, S / 2, S * 0.4, { fill, breaker })) {
|
||||
i++;
|
||||
ctx.fillRect(x, y, 1, 1);
|
||||
}
|
||||
console.timeEnd(`fill=${fill}`);
|
||||
console.log(`i=${i}`)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
type Point = [number, number];
|
||||
type Rect = [number, number, number, number];
|
||||
|
||||
type RunGame = () => Promise<void>;
|
||||
|
||||
declare const GAME: string;
|
||||
|
|
|
|||
Loading…
Reference in New Issue