diff --git a/src/game/game.ts b/src/game/game.ts index 6a976c1..1640495 100644 --- a/src/game/game.ts +++ b/src/game/game.ts @@ -1,7 +1,7 @@ import Graphics from "./graphics"; import UI from "./ui"; import World from "./world"; -import { prevent } from "./utils"; +import { pointsEquals, prevent } from "./utils"; export default class Game { private running = false; @@ -9,6 +9,7 @@ export default class Game { private graphics; private world; private ui; + private prevWorldPos: Point | null = null; constructor(private canvas: HTMLCanvasElement, controls: HTMLElement) { window.addEventListener('resize', this.onResize); @@ -42,7 +43,7 @@ export default class Game { if (direction < 0) { scale = 1.1; } else if (direction > 0) { - scale = 1/ 1.1; + scale = 1 / 1.1; } this.graphics.applyScale(scale, [event.clientX, event.clientY]); @@ -58,22 +59,37 @@ export default class Game { private onMouseUp = (event: MouseEvent) => { this.canvas.style.cursor = 'default'; this.mouseDown = false; + this.prevWorldPos = null; + const mousePos: Point = [event.clientX, event.clientY]; + const worldPos = this.graphics.screenToWorld(mousePos); - const pos = this.graphics.screenToWorld([event.clientX, event.clientY]); - if (event.button === 0) { - // this.world.placeTile(pos, TileType.CONVEYOR); TODO place selected tile from hotbar + if (event.button === 0 && this.ui.selectedTool.tileType != null) { + const tileType = this.ui.selectedTool.tileType; + this.world.placeTile(worldPos, tileType); } else if (event.button === 2) { - this.world.removeTile(pos); + this.world.removeTile(worldPos); } event.preventDefault(); } private onMouseMove = (event: MouseEvent) => { + const mousePos: Point = [event.clientX, event.clientY]; + const mouseDelta: Point = [event.movementX, event.movementY]; + const worldPos = this.graphics.screenToWorld(mousePos); + if (this.mouseDown === 1) { this.canvas.style.cursor = 'grabbing'; - this.graphics.pan([event.movementX, event.movementY]); + this.graphics.pan(mouseDelta); + } else if (this.mouseDown === 0 && this.ui.selectedTool.tileType != null) { + const tileType = this.ui.selectedTool.tileType; + this.world.placeTile(worldPos, tileType, this.prevWorldPos); + } else if (this.mouseDown === 2) { + this.world.removeTile(worldPos); } - this.graphics.highlight([event.clientX, event.clientY]) + if (!this.prevWorldPos || !pointsEquals(this.prevWorldPos, worldPos)) { + this.prevWorldPos = worldPos; + } + this.graphics.highlight(mousePos); event.preventDefault(); } @@ -92,6 +108,8 @@ export default class Game { } private loop = () => { + this.world.update(); + this.graphics.clear(); this.graphics.drawGrid(); diff --git a/src/game/graphics.ts b/src/game/graphics.ts index 684a18b..b61bb62 100644 --- a/src/game/graphics.ts +++ b/src/game/graphics.ts @@ -29,7 +29,7 @@ export default class Graphics { } applyScale(scale: number, point: Point) { - const newTileSize = Math.min(Math.max(this.tileSize * scale, 2), this.width / 2, this.height / 2); + const newTileSize = Math.min(Math.max(this.tileSize * scale, 16), this.width / 2, this.height / 2); const realScale = newTileSize / this.tileSize; this.tileSize = newTileSize; diff --git a/src/game/renderer.ts b/src/game/renderer.ts index 5e205f4..182ffaa 100644 --- a/src/game/renderer.ts +++ b/src/game/renderer.ts @@ -1,4 +1,7 @@ -import { type Tile, TileType } from "./world"; +import { type Tile, TileType, getPortDirections, PortDirection } from "./world"; + +import extractorIcon from '../assets/img/extractor.png'; +import { Direction, makeImage } from "./utils"; export type Renderer = (ctx: CanvasRenderingContext2D, tile: T) => void; export type NullRenderer = (ctx: CanvasRenderingContext2D) => void; @@ -7,6 +10,8 @@ type Renderers = { [K in Tile['type']]: Renderer> } +const extractorImage = makeImage(extractorIcon); + export const renderers: Renderers = { [TileType.SOURCE]: (ctx, tile) => { ctx.fillStyle = '#bbffff7f'; @@ -23,7 +28,29 @@ export const renderers: Renderers = { } }, [TileType.EXTRACTOR]: (ctx, tile) => { + renderers[TileType.SOURCE](ctx, tile.source); + ctx.imageSmoothingEnabled = false; + ctx.drawImage(extractorImage, 0, 0, 1, 1); }, [TileType.CONVEYOR]: (ctx, tile) => { + ctx.fillStyle = 'lightgray'; + ctx.fillRect(0.2, 0.2, 0.6, 0.6); + for (const direction of getPortDirections(tile.ports)) { + const portDirection = tile.ports[direction]?.direction; + if (portDirection === PortDirection.INPUT) { + ctx.fillStyle = 'lightgreen'; + } else if (portDirection === PortDirection.OUTPUT) { + ctx.fillStyle = 'red'; + } + if (direction === Direction.NORTH) { + ctx.fillRect(0.2, 0, 0.6, 0.2); + } else if (direction === Direction.SOUTH) { + ctx.fillRect(0.2, 0.8, 0.6, 0.2); + } else if (direction === Direction.WEST) { + ctx.fillRect(0, 0.2, 0.2, 0.6); + } else if (direction === Direction.EAST) { + ctx.fillRect(0.8, 0.2, 0.2, 0.6); + } + } } }; \ No newline at end of file diff --git a/src/game/ui.tsx b/src/game/ui.tsx index 8529814..fc101a9 100644 --- a/src/game/ui.tsx +++ b/src/game/ui.tsx @@ -9,7 +9,7 @@ import conveyorIcon from '../assets/img/conveyor.png'; import extractorIcon from '../assets/img/extractor.png'; import trashIcon from '../assets/img/trash.png'; -enum ToolType { +export enum ToolType { SELECT, EXTRACTOR, CONVEYOR, diff --git a/src/game/utils.ts b/src/game/utils.ts index d1ec270..aaf2aeb 100644 --- a/src/game/utils.ts +++ b/src/game/utils.ts @@ -1,12 +1,20 @@ +export enum Direction { + NONE, + NORTH, + EAST, + SOUTH, + WEST, +} + type Operand = Point | number; type Operation = (a: number, b: number) => number; function op(a: Operand, b: Operand, fn: Operation): Operand { const aArray = Array.isArray(a); const bArray = Array.isArray(b); if (aArray) { - return (bArray ? a.map((x, i) => fn(x, b[i])): a.map((x) => fn(x, b))) as Point; + return (bArray ? a.map((x, i) => fn(x, b[i])) : a.map((x) => fn(x, b))) as Point; } - return (bArray ? b.map((x) => fn(a, x)): fn(a, b)) as Point; + return (bArray ? b.map((x) => fn(a, x)) : fn(a, b)) as Point; } const operations: Record = { @@ -89,6 +97,8 @@ export function exp(strings: TemplateStringsArray, ...args: Operand[]): Operand export function trunc(input: Point): Point { return op(input, 0, (x) => Math.floor(x)) as Point; } +const EPS = 0.001; +export const pointsEquals = (a: Point, b: Point) => Math.abs(a[0] - b[0]) < EPS && Math.abs(a[1] - b[1]) < EPS; export const prevent = (e: Event) => (e.preventDefault(), false); @@ -104,4 +114,41 @@ export const cyrb32 = (seed: number, ...parts: number[]) => { return h1; }; export const sinHash = (...data: number[]) => data.reduce((hash, n) => Math.sin((hash * 123.12 + n) * 756.12), 0) / 2 + 0.5; -export const range = (size: number | string) => Object.keys((new Array(+size)).fill(0)).map(k => +k); \ No newline at end of file +export const range = (size: number | string) => Object.keys((new Array(+size)).fill(0)).map(k => +k); + +export const getDirection = (point: Point): Direction => { + const [x, y] = point; + const absX = Math.abs(x); + const absY = Math.abs(y); + if (absX === 0 && absY === 0) return Direction.NONE; + else if (absX > absY) return x < 0 ? Direction.WEST : Direction.EAST; + else if (absY > absX) return y < 0 ? Direction.NORTH : Direction.SOUTH; + else return Direction.NONE; +} +export const getOppositeDirection = (dir: Direction): Direction => { + switch (dir) { + case Direction.NORTH: return Direction.SOUTH; + case Direction.SOUTH: return Direction.NORTH; + case Direction.WEST: return Direction.EAST; + case Direction.EAST: return Direction.WEST; + default: return Direction.NONE; + } +} +export const DIRECTION_VECTORS: Record = { + [Direction.NONE]: [0, 0], + [Direction.NORTH]: [0, -1], + [Direction.SOUTH]: [0, 1], + [Direction.WEST]: [-1, 0], + [Direction.EAST]: [1, 0], +}; +export const ALL_DIRECTIONS = [Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST]; +export const movePoint = (point: Point, direction: Direction): Point => [ + point[0] + DIRECTION_VECTORS[direction][0], + point[1] + DIRECTION_VECTORS[direction][1], +]; + +export const makeImage = (src: string): HTMLImageElement => { + const image = new Image(); + image.src = src; + return image; +} \ No newline at end of file diff --git a/src/game/world.ts b/src/game/world.ts index 92a1c05..e0af166 100644 --- a/src/game/world.ts +++ b/src/game/world.ts @@ -1,4 +1,4 @@ -import { cyrb32 } from "./utils"; +import { ALL_DIRECTIONS, Direction, cyrb32, exp, getDirection, getOppositeDirection, movePoint, pointsEquals, trunc } from "./utils"; export enum TileType { DESTINATION, @@ -7,24 +7,22 @@ export enum TileType { CONVEYOR, } -export enum Direction { - NONE, - NORTH, - EAST, - SOUTH, - WEST, +export enum PortDirection { + INPUT, + OUTPUT } type Resource = number; interface Port { - direction: Direction; + direction: PortDirection; buffer?: Resource; } +type Ports = Partial>; + interface BaseTile { - outputs: Port[]; - inputs: Port[]; + ports: Ports; } interface TileDestination extends BaseTile { @@ -32,7 +30,7 @@ interface TileDestination extends BaseTile { center: boolean; } -interface TileSource { +interface TileSource extends BaseTile { type: TileType.SOURCE; resource: Resource; } @@ -50,36 +48,117 @@ export type Tile = TileDestination | TileSource | TileExtractor | TileConveyor; const id = (point: Point) => ((Math.floor(point[0]) & 0xFFFF) << 16) | Math.floor(point[1]) & 0xFFFF; +export const getPortDirections = (ports: Partial>) => Object.keys(ports).map(k => +k as Direction); + export default class World { private world = new Map(); constructor(private seed: number = Math.random() * 2e9) { for (let x = 0; x < 5; x++) { for (let y = 0; y < 5; y++) { - const inputs: Port[] = []; - if (y == 0) inputs.push({ direction: Direction.NORTH }); - if (y == 4) inputs.push({ direction: Direction.SOUTH }); - if (x == 0) inputs.push({ direction: Direction.WEST }); - if (x == 4) inputs.push({ direction: Direction.EAST }); + const ports: Ports = {}; + if (y == 0) ports[Direction.NORTH] = { direction: PortDirection.INPUT }; + if (y == 4) ports[Direction.SOUTH] = { direction: PortDirection.INPUT }; + if (x == 0) ports[Direction.WEST] = { direction: PortDirection.INPUT }; + if (x == 4) ports[Direction.EAST] = { direction: PortDirection.INPUT }; - this.placeTile([x - 2, y - 2], { + this.setTile([x - 2, y - 2], { type: TileType.DESTINATION, - outputs: [], - inputs, + ports, center: x == 2 && y == 2, }); } } } - placeTile(position: Point, tile: Tile) { - // TODO select correct type + placeTile(position: Point, type: TileType, prevPosition: Point | null = null) { + position = trunc(position); + if (prevPosition) { + prevPosition = trunc(prevPosition); + } + let tile: Tile | undefined; + const existingTile = this.getTile(position); + switch (type) { + case TileType.CONVEYOR: + if (prevPosition) { + const prevPositionTile = this.getTile(prevPosition); + const ports: Ports = {}; + const direction = getDirection(exp`${position} - ${prevPosition}`); + const oppositeDirection = getOppositeDirection(direction); + + if (prevPositionTile) { + if (!prevPositionTile.ports[direction]) { + prevPositionTile.ports[direction] = { direction: PortDirection.OUTPUT }; + } + ports[oppositeDirection] = { direction: PortDirection.INPUT }; + } + if (existingTile) { + if (!existingTile.ports[oppositeDirection]) { + existingTile.ports[oppositeDirection] = { direction: PortDirection.INPUT }; + } + } else { + tile = { + type: TileType.CONVEYOR, + ports, + } + } + } + break; + case TileType.EXTRACTOR: + if (existingTile?.type === TileType.SOURCE) { + const ports: Ports = {}; + for (const direction of ALL_DIRECTIONS) { + const oppositeDirection = getOppositeDirection(direction); + const neighbourPos = movePoint(position, direction); + const neighbour = this.getTile(neighbourPos); + if (neighbour && !neighbour.ports[oppositeDirection]) { + neighbour.ports[oppositeDirection] = { direction: PortDirection.INPUT }; + ports[direction] = { direction: PortDirection.OUTPUT }; + } + } + tile = { + type: TileType.EXTRACTOR, + ports, + source: existingTile, + }; + } + break; + default: + break; + } + + if (tile) { + this.setTile(position, tile); + } + } + + private setTile(position: Point, tile: Tile) { this.world.set(id(position), tile); } removeTile(position: Point) { - // TODO restore correct type if needed - this.world.delete(id(position)); + position = trunc(position); + const pid = id(position); + const existingTile = this.world.get(pid); + const type = existingTile?.type; + if (type === TileType.DESTINATION || type === TileType.SOURCE) { + return; + } + if (existingTile) { + for (const direction of getPortDirections(existingTile.ports)) { + const oppositeDirection = getOppositeDirection(direction); + const neighbourPos = movePoint(position, direction); + const neighbour = this.getTile(neighbourPos); + if (neighbour) { + delete neighbour.ports[oppositeDirection]; + } + } + } + if (existingTile?.type === TileType.EXTRACTOR) { + this.world.set(pid, existingTile.source); + } else if (existingTile) { + this.world.delete(pid); + } } getTile(position: Point): Tile | null { @@ -89,16 +168,27 @@ export default class World { if (tile) return tile; if (Math.abs(x) >= 5 && Math.abs(y) >= 5) { - const hash = cyrb32(this.seed, ...position); - - if ((hash & 0xFF) === 42) { - const resource = (hash >> 12) & 0xF; - const newTile: Tile = { type: TileType.SOURCE, resource }; - this.world.set(pid, newTile); - return newTile; - } + return this.genTile(position); } return null; } + + private genTile(position: Point): Tile | null { + const pid = id(position); + const hash = cyrb32(this.seed, ...position); + + if ((hash & 0xFF) === 42) { + const resource = (hash >> 12) & 0xF || 1; + const newTile: Tile = { type: TileType.SOURCE, resource, ports: {} }; + this.world.set(pid, newTile); + return newTile; + } + + return null; + } + + update() { + + } } \ No newline at end of file