diff --git a/src/game.ts b/src/game.ts index 555cb82..12516e5 100644 --- a/src/game.ts +++ b/src/game.ts @@ -1,10 +1,12 @@ import Graphics from "./graphics"; import { prevent } from "./utils"; +import World, { TileType } from "./world"; export default class Game { private running = false; - private mouseDown = false; + private mouseDown: false | number = false; private graphics; + private world; constructor(private canvas: HTMLCanvasElement) { window.addEventListener('resize', this.onResize); @@ -19,6 +21,7 @@ export default class Game { document.addEventListener('contextmenu', prevent); this.graphics = new Graphics(canvas); + this.world = new World(); } async load() { @@ -46,17 +49,27 @@ export default class Game { } private onMouseDown = (event: MouseEvent) => { - this.mouseDown = true; + this.mouseDown = event.button; event.preventDefault(); } private onMouseUp = (event: MouseEvent) => { + this.canvas.style.cursor = 'default'; this.mouseDown = false; + + const pos = this.graphics.screenToWorld([event.clientX, event.clientY]); + if (event.button === 0) { + this.world.placeTile(pos, TileType.CONVEYOR); + } else if (event.button === 2) { + this.world.removeTile(pos); + } event.preventDefault(); } private onMouseMove = (event: MouseEvent) => { - if (this.mouseDown) { + if (this.mouseDown === 1) { + this.canvas.style.cursor = 'grabbing'; this.graphics.pan([event.movementX, event.movementY]); } + this.graphics.highlight([event.clientX, event.clientY]) event.preventDefault(); } @@ -69,6 +82,10 @@ export default class Game { this.graphics.clear(); this.graphics.drawGrid(); + this.graphics.drawWorld(this.world); + + this.graphics.drawHighlight(); + this.graphics.debug(); if (this.running) { diff --git a/src/graphics.ts b/src/graphics.ts index 71dd658..7871c3a 100644 --- a/src/graphics.ts +++ b/src/graphics.ts @@ -1,9 +1,11 @@ -import { exp } from "./utils"; +import { exp, trunc } from "./utils"; +import type World from "./world"; export default class Graphics { private context: CanvasRenderingContext2D; private tileSize = 32; private offset: Point = [0, 0]; + private highlighted: Point = [0, 0]; constructor(private canvas: HTMLCanvasElement) { this.context = canvas.getContext('2d')!; @@ -33,6 +35,10 @@ export default class Graphics { this.offset = exp`${this.offset} + ${amount}`; } + highlight(screenPoint: Point) { + this.highlighted = this.screenToWorld(screenPoint); + } + clear() { this.context.clearRect(0, 0, this.width, this.height); } @@ -49,8 +55,8 @@ export default class Graphics { this.context.beginPath(); this.context.strokeStyle = 'gray'; let [x0, y0, x1, y1] = this.visibleWorld; - [x0, y0] = this.worldToScreen([Math.floor(x0), Math.floor(y0)]); - [x1, y1] = this.worldToScreen([Math.ceil(x1), Math.ceil(y1)]); + [x0, y0] = this.worldToScreen([x0, y0]); + [x1, y1] = this.worldToScreen([x1, y1]); for (let x = x0; x < x1; x += this.tileSize) { this.context.moveTo(x, 0); @@ -63,11 +69,46 @@ export default class Graphics { this.context.stroke(); } - get visibleWorld(): Rect { - const topLeft = this.screenToWorld([0, 0]); - const bottomRight = this.screenToWorld([this.width, this.height]); + drawHighlight() { + this.drawTile(this.highlighted, ctx => { + ctx.fillStyle = 'rgba(0, 0, 0, 0.1)'; + ctx.fillRect(0, 0, 1, 1); + }); + } - return [...topLeft, ...bottomRight]; + drawTile(position: Point, renderer: (ctx: CanvasRenderingContext2D) => void) { + this.context.save(); + + // TODO skip drawing if outside screen + const screenPosition = this.worldToScreen(trunc(position)); + this.context.translate(screenPosition[0], screenPosition[1]); + this.context.scale(this.tileSize, this.tileSize); + + renderer(this.context); + + this.context.restore(); + } + + drawWorld(world: World) { + const [x0, y0, x1, y1] = this.visibleWorld; + for (let y = y0; y <= y1; y++) { + for (let x = x0; x <= x1; x++) { + const tile = world.getTile([x, y]); + if (tile) { + this.drawTile([x, y], ctx => { + ctx.fillStyle = 'green'; + ctx.fillRect(0, 0, 1, 1); + }); + } + } + } + } + + get visibleWorld(): Rect { + const [x0, y0] = this.screenToWorld([0, 0]); + const [x1, y1] = this.screenToWorld([this.width, this.height]); + + return [Math.floor(x0), Math.floor(y0), Math.ceil(x1), Math.ceil(y1)]; } worldToScreen(point: Point): Point { diff --git a/src/utils.ts b/src/utils.ts index e4b8a4d..041ab41 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -20,7 +20,7 @@ const operations: Record = { export function exp(strings: TemplateStringsArray, ...args: number[]): number; export function exp(strings: TemplateStringsArray, ...args: Operand[]): Point; -export function exp(strings: TemplateStringsArray, ...args: Operand[]): Point | number { +export function exp(strings: TemplateStringsArray, ...args: Operand[]): Operand { const input: (string | Operand)[] = []; const output: (Operation | Operand)[] = []; const stack: string[] = []; @@ -86,5 +86,8 @@ export function exp(strings: TemplateStringsArray, ...args: Operand[]): Point | return calcStack[0]; } +export function trunc(input: Point): Point { + return op(input, 0, (x) => Math.floor(x)) as Point; +} export const prevent = (e: Event) => (e.preventDefault(), false); \ No newline at end of file diff --git a/src/world.ts b/src/world.ts new file mode 100644 index 0000000..56a4600 --- /dev/null +++ b/src/world.ts @@ -0,0 +1,58 @@ +import { trunc } from "./utils"; + +export enum TileType { + SOURCE, + EXTRACTOR, + CONVEYOR, +} + +export enum Direction { + NONE, + NORTH, + EAST, + SOUTH, + WEST, +} + +interface BaseTile { + outputs: Direction[]; + inputs: Direction[]; +} + +interface TileSource extends BaseTile { + type: TileType.SOURCE; +} + +interface TileExtractor extends BaseTile { + type: TileType.EXTRACTOR; + source: TileSource; +} + +interface TileConveyor extends BaseTile { + type: TileType.CONVEYOR; +} + +type Tile = TileExtractor | TileSource | TileConveyor; + +const id = (point: Point) => `${Math.trunc(point[0])}-${Math.trunc(point[1])}`; + +export default class World { + private world = new Map(); + constructor() { + + } + + placeTile(position: Point, type: TileType) { + // TODO select correct type + this.world.set(id(position), { type: TileType.SOURCE, inputs: [], outputs: [] }); + } + + removeTile(position: Point) { + // TODO restore correct type if needed + this.world.delete(id(position)); + } + + getTile(position: Point) { + return this.world.get(id(position)); + } +} \ No newline at end of file