1
0
Fork 0

Tile placing

This commit is contained in:
Pabloader 2024-06-26 19:18:02 +00:00
parent cdf915a779
commit 12136bacf6
6 changed files with 227 additions and 45 deletions

View File

@ -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();

View File

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

View File

@ -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<T extends Tile> = (ctx: CanvasRenderingContext2D, tile: T) => void;
export type NullRenderer = (ctx: CanvasRenderingContext2D) => void;
@ -7,6 +10,8 @@ type Renderers = {
[K in Tile['type']]: Renderer<Extract<Tile, { type: K }>>
}
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);
}
}
}
};

View File

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

View File

@ -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<string, [Operation, number]> = {
@ -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);
@ -105,3 +115,40 @@ export const cyrb32 = (seed: number, ...parts: number[]) => {
};
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);
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, Point> = {
[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;
}

View File

@ -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<Record<Direction, Port>>;
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<Record<Direction, any>>) => Object.keys(ports).map(k => +k as Direction);
export default class World {
private world = new Map<number, Tile>();
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() {
}
}