Add multi-input tiles (broken now)
This commit is contained in:
parent
d46e98dda0
commit
aa0e84de69
Binary file not shown.
|
After Width: | Height: | Size: 206 B |
Binary file not shown.
|
After Width: | Height: | Size: 157 B |
Binary file not shown.
|
After Width: | Height: | Size: 195 B |
Binary file not shown.
|
After Width: | Height: | Size: 196 B |
|
|
@ -11,6 +11,7 @@ export default class Game {
|
||||||
private ui;
|
private ui;
|
||||||
private prevWorldPos: Point | null = null;
|
private prevWorldPos: Point | null = null;
|
||||||
private prevFrame: number = performance.now();
|
private prevFrame: number = performance.now();
|
||||||
|
private paused = false;
|
||||||
|
|
||||||
constructor(private canvas: HTMLCanvasElement, controls: HTMLElement) {
|
constructor(private canvas: HTMLCanvasElement, controls: HTMLElement) {
|
||||||
window.addEventListener('resize', this.onResize);
|
window.addEventListener('resize', this.onResize);
|
||||||
|
|
@ -91,6 +92,12 @@ export default class Game {
|
||||||
this.prevWorldPos = worldPos;
|
this.prevWorldPos = worldPos;
|
||||||
}
|
}
|
||||||
this.graphics.highlight(mousePos);
|
this.graphics.highlight(mousePos);
|
||||||
|
const tile = this.world.getTile(worldPos);
|
||||||
|
if (tile) {
|
||||||
|
this.graphics.showTooltip(mousePos, JSON.stringify(tile, null, 2));
|
||||||
|
} else {
|
||||||
|
this.graphics.showTooltip(mousePos, '');
|
||||||
|
}
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -98,6 +105,11 @@ export default class Game {
|
||||||
const key = event.key.toLowerCase();
|
const key = event.key.toLowerCase();
|
||||||
if (key === 'h') {
|
if (key === 'h') {
|
||||||
this.graphics.resetView();
|
this.graphics.resetView();
|
||||||
|
} else if (key === ' ') {
|
||||||
|
this.paused = !this.paused;
|
||||||
|
} else if (key.length === 1 && key >= '1' && key <= '9') {
|
||||||
|
const slot = parseInt(key);
|
||||||
|
this.ui.useSlot(slot);
|
||||||
} else {
|
} else {
|
||||||
console.log(`Pressed: ${key}`);
|
console.log(`Pressed: ${key}`);
|
||||||
}
|
}
|
||||||
|
|
@ -113,7 +125,9 @@ export default class Game {
|
||||||
const dt = now - this.prevFrame;
|
const dt = now - this.prevFrame;
|
||||||
this.prevFrame = now;
|
this.prevFrame = now;
|
||||||
|
|
||||||
|
if (!this.paused) {
|
||||||
this.world.update(dt);
|
this.world.update(dt);
|
||||||
|
}
|
||||||
|
|
||||||
this.graphics.clear();
|
this.graphics.clear();
|
||||||
|
|
||||||
|
|
@ -122,6 +136,7 @@ export default class Game {
|
||||||
|
|
||||||
this.graphics.drawHighlight();
|
this.graphics.drawHighlight();
|
||||||
|
|
||||||
|
this.graphics.drawTooltip();
|
||||||
this.graphics.debug();
|
this.graphics.debug();
|
||||||
|
|
||||||
if (this.running) {
|
if (this.running) {
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { renderers, type NullRenderer, type Renderer } from "./renderer";
|
import { defaultRenderer, renderPorts, renderResource, renderers, type NullRenderer, type Renderer } from "./renderer";
|
||||||
import { exp, trunc } from "./utils";
|
import { ALL_DIRECTIONS, Direction, exp, trunc } from "./utils";
|
||||||
import type World from "./world";
|
import type World from "./world";
|
||||||
import { type Tile } from "./world";
|
import { type Tile } from "./world";
|
||||||
|
|
||||||
|
|
@ -10,9 +10,15 @@ export default class Graphics {
|
||||||
private tileSize = initialTileSize;
|
private tileSize = initialTileSize;
|
||||||
private offset: Point = [0, 0];
|
private offset: Point = [0, 0];
|
||||||
private highlighted: Point = [0, 0];
|
private highlighted: Point = [0, 0];
|
||||||
|
private tooltip: [Point, string] | null = null;
|
||||||
|
|
||||||
constructor(private canvas: HTMLCanvasElement) {
|
constructor(private canvas: HTMLCanvasElement) {
|
||||||
this.context = canvas.getContext('2d')!;
|
this.context = canvas.getContext('2d')!;
|
||||||
|
this.context.imageSmoothingEnabled = false;
|
||||||
|
this.context.font = 'bold 0.4px sans-serif';
|
||||||
|
this.context.textRendering = 'optimizeSpeed';
|
||||||
|
this.context.textAlign = 'center';
|
||||||
|
|
||||||
this.resetView();
|
this.resetView();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -54,11 +60,50 @@ export default class Graphics {
|
||||||
}
|
}
|
||||||
|
|
||||||
debug() {
|
debug() {
|
||||||
// const p00 = this.worldToScreen([0, 0]);
|
|
||||||
// const p11 = exp`${this.worldToScreen([1, 1])} - ${p00}`;
|
|
||||||
|
|
||||||
// this.context.fillStyle = 'red';
|
}
|
||||||
// this.context.fillRect(...p00, ...p11);
|
|
||||||
|
showTooltip(pos: Point, text: string) {
|
||||||
|
if (text) {
|
||||||
|
this.tooltip = [pos, text];
|
||||||
|
} else {
|
||||||
|
this.tooltip = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
drawTooltip() {
|
||||||
|
if (this.tooltip) {
|
||||||
|
this.context.save();
|
||||||
|
// this.context.reset();
|
||||||
|
this.context.font = '16px sans-serif';
|
||||||
|
this.context.textAlign = 'left';
|
||||||
|
|
||||||
|
const [pos, text] = this.tooltip;
|
||||||
|
const lines = text.split('\n');
|
||||||
|
|
||||||
|
let maxWidth = 0;
|
||||||
|
let maxHeight = 10;
|
||||||
|
for (const line of lines) {
|
||||||
|
const measure = this.context.measureText(line);
|
||||||
|
maxWidth = Math.max(maxWidth, measure.width);
|
||||||
|
maxHeight += 18;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.context.fillStyle = 'white';
|
||||||
|
this.context.fillRect(...pos, maxWidth + 10, maxHeight + 3);
|
||||||
|
this.context.strokeStyle = 'black';
|
||||||
|
this.context.strokeRect(...pos, maxWidth + 10, maxHeight + 3);
|
||||||
|
this.context.font = '16px';
|
||||||
|
this.context.fillStyle = 'black';
|
||||||
|
|
||||||
|
let y = pos[1] + 18;
|
||||||
|
for (const line of lines) {
|
||||||
|
const measure = this.context.measureText(line);
|
||||||
|
this.context.fillText(line, pos[0] + 5, y);
|
||||||
|
y += 18;
|
||||||
|
}
|
||||||
|
this.context.restore();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
drawGrid() {
|
drawGrid() {
|
||||||
|
|
@ -86,9 +131,9 @@ export default class Graphics {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
drawTile<T extends Tile>(position: Point, renderer: Renderer<T>, tile: T): void;
|
drawTile<T extends Tile>(position: Point, renderer: Renderer<T>, tile: T, drawPorts?: boolean): void;
|
||||||
drawTile<T extends null>(position: Point, renderer: NullRenderer): void;
|
drawTile<T extends null>(position: Point, renderer: NullRenderer): void;
|
||||||
drawTile<T extends Tile>(position: Point, renderer: Renderer<T> | NullRenderer, tile?: T): void {
|
drawTile<T extends Tile>(position: Point, renderer: Renderer<T> | NullRenderer, tile?: T, drawPorts: boolean = true): void {
|
||||||
this.context.save();
|
this.context.save();
|
||||||
|
|
||||||
// TODO skip drawing if outside screen
|
// TODO skip drawing if outside screen
|
||||||
|
|
@ -97,6 +142,9 @@ export default class Graphics {
|
||||||
this.context.scale(this.tileSize, this.tileSize);
|
this.context.scale(this.tileSize, this.tileSize);
|
||||||
|
|
||||||
if (tile) {
|
if (tile) {
|
||||||
|
if (drawPorts) {
|
||||||
|
renderPorts(this.context, tile);
|
||||||
|
}
|
||||||
renderer(this.context, tile);
|
renderer(this.context, tile);
|
||||||
} else {
|
} else {
|
||||||
(renderer as NullRenderer)(this.context);
|
(renderer as NullRenderer)(this.context);
|
||||||
|
|
@ -106,21 +154,31 @@ export default class Graphics {
|
||||||
}
|
}
|
||||||
|
|
||||||
drawWorld(world: World) {
|
drawWorld(world: World) {
|
||||||
this.context.font = 'bold 0.4px sans-serif';
|
|
||||||
this.context.textRendering = 'optimizeSpeed';
|
|
||||||
this.context.textAlign = 'center';
|
|
||||||
const [x0, y0, x1, y1] = this.visibleWorld;
|
const [x0, y0, x1, y1] = this.visibleWorld;
|
||||||
|
const resourceTiles: [Point, Tile][] = [];
|
||||||
for (let y = y0; y <= y1; y++) {
|
for (let y = y0; y <= y1; y++) {
|
||||||
for (let x = x0; x <= x1; x++) {
|
for (let x = x0; x <= x1; x++) {
|
||||||
const tile = world.getTile([x, y]);
|
const pos: Point = [x, y];
|
||||||
|
const tile = world.getTile(pos);
|
||||||
if (tile) {
|
if (tile) {
|
||||||
const renderer = renderers[tile.type] as Renderer<Tile>;
|
let renderer = renderers[tile.type] as Renderer<Tile>;
|
||||||
this.drawTile([x, y], renderer, tile);
|
if (!renderer) {
|
||||||
|
renderer = defaultRenderer;
|
||||||
|
}
|
||||||
|
this.drawTile(pos, renderer, tile);
|
||||||
|
if ([...ALL_DIRECTIONS, Direction.NONE].some(d => tile.inv[d] != null)) {
|
||||||
|
resourceTiles.push([pos, tile]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.context.fillStyle = 'blue';
|
||||||
|
for (const [pos, tile] of resourceTiles) {
|
||||||
|
this.drawTile(pos, renderResource, tile, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
get visibleWorld(): Rect {
|
get visibleWorld(): Rect {
|
||||||
const [x0, y0] = this.screenToWorld([0, 0]);
|
const [x0, y0] = this.screenToWorld([0, 0]);
|
||||||
const [x1, y1] = this.screenToWorld([this.width, this.height]);
|
const [x1, y1] = this.screenToWorld([this.width, this.height]);
|
||||||
|
|
|
||||||
|
|
@ -1,40 +1,59 @@
|
||||||
import { type Tile, TileType, getPortDirections, PortDirection } from "./world";
|
import { type Tile, TileType, getPortDirections, PortDirection, LIMITS, type Resource } from "./world";
|
||||||
|
|
||||||
import extractorIcon from '../assets/img/extractor.png';
|
import { ALL_DIRECTIONS, Direction, makeImage, movePoint } from "./utils";
|
||||||
import { Direction, makeImage } from "./utils";
|
|
||||||
|
import emptySrc from '../assets/img/empty.png';
|
||||||
|
import extractorSrc from '../assets/img/extractor.png';
|
||||||
|
import notSrc from '../assets/img/not.png';
|
||||||
|
import andSrc from '../assets/img/and.png';
|
||||||
|
import orSrc from '../assets/img/or.png';
|
||||||
|
|
||||||
export type Renderer<T extends Tile> = (ctx: CanvasRenderingContext2D, tile: T) => void;
|
export type Renderer<T extends Tile> = (ctx: CanvasRenderingContext2D, tile: T) => void;
|
||||||
export type NullRenderer = (ctx: CanvasRenderingContext2D) => void;
|
export type NullRenderer = (ctx: CanvasRenderingContext2D) => void;
|
||||||
|
|
||||||
type Renderers = {
|
type Renderers = {
|
||||||
[K in Tile['type']]: Renderer<Extract<Tile, { type: K }>>
|
[K in Tile['type']]?: Renderer<Extract<Tile, { type: K }>>
|
||||||
}
|
}
|
||||||
|
|
||||||
const extractorImage = makeImage(extractorIcon);
|
const emptyImage = makeImage(emptySrc);
|
||||||
|
const extractorImage = makeImage(extractorSrc);
|
||||||
|
const notImage = makeImage(notSrc);
|
||||||
|
const andImage = makeImage(andSrc);
|
||||||
|
const orImage = makeImage(orSrc);
|
||||||
|
|
||||||
export const renderers: Renderers = {
|
export const renderResource = (ctx: CanvasRenderingContext2D, tile: Tile, resource?: Resource) => {
|
||||||
[TileType.SOURCE]: (ctx, tile) => {
|
const resources: [Direction, Resource | undefined][] = resource
|
||||||
ctx.fillStyle = '#bbffff7f';
|
? [[Direction.NONE, resource]]
|
||||||
ctx.fillRect(0, 0, 1, 1);
|
: [...ALL_DIRECTIONS, Direction.NONE].map((d) => [d, tile.inv[d]]);
|
||||||
ctx.fillStyle = 'black';
|
|
||||||
ctx.fillText(tile.resource.toString(2), 0.5, 0.65);
|
const oldStyle = ctx.fillStyle;
|
||||||
},
|
const px = 1 / 32;
|
||||||
[TileType.DESTINATION]: (ctx, tile) => {
|
|
||||||
if (tile.center) {
|
let wasOtherDrawn = false;
|
||||||
ctx.fillStyle = '#bbffbb';
|
for (const [direction, res] of resources) {
|
||||||
ctx.fillRect(-2, -2, 5, 5);
|
if (direction === Direction.NONE && wasOtherDrawn) continue;
|
||||||
ctx.fillStyle = 'black';
|
if (res) {
|
||||||
ctx.fillText('Deploy', 0.5, 0.65);
|
const str = res.toString(2);
|
||||||
|
const amount = (tile.animationTimer ?? 0) / LIMITS[tile.type].cooldown;
|
||||||
|
|
||||||
|
const [x, y] = movePoint([0.5, 0.65], direction, amount);
|
||||||
|
|
||||||
|
ctx.fillStyle = 'white';
|
||||||
|
ctx.fillText(str, x - px, y - px);
|
||||||
|
ctx.fillText(str, x - px, y + px);
|
||||||
|
ctx.fillText(str, x + px, y - px);
|
||||||
|
ctx.fillText(str, x + px, y + px);
|
||||||
|
ctx.fillStyle = oldStyle;
|
||||||
|
ctx.fillText(str, x, y);
|
||||||
|
wasOtherDrawn = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const renderPorts = (ctx: CanvasRenderingContext2D, tile: Tile) => {
|
||||||
|
if (tile.type === TileType.DESTINATION || tile.type === TileType.SOURCE) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
},
|
|
||||||
[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)) {
|
for (const direction of getPortDirections(tile.ports)) {
|
||||||
const portDirection = tile.ports[direction]?.direction;
|
const portDirection = tile.ports[direction]?.direction;
|
||||||
if (portDirection === PortDirection.INPUT) {
|
if (portDirection === PortDirection.INPUT) {
|
||||||
|
|
@ -52,9 +71,40 @@ export const renderers: Renderers = {
|
||||||
ctx.fillRect(0.8, 0.2, 0.2, 0.6);
|
ctx.fillRect(0.8, 0.2, 0.2, 0.6);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (tile.resource) {
|
|
||||||
ctx.fillStyle = 'blue';
|
|
||||||
ctx.fillText(tile.resource.toString(2), 0.5, 0.65);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const defaultRenderer = (ctx: CanvasRenderingContext2D) => {
|
||||||
|
ctx.drawImage(emptyImage, 0, 0, 1, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const imageRenderer = <T extends Tile>(image: HTMLImageElement): Renderer<T> => (ctx, tile) => {
|
||||||
|
ctx.drawImage(image, 0, 0, 1, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const renderers: Renderers = {
|
||||||
|
[TileType.SOURCE]: (ctx, tile) => {
|
||||||
|
ctx.fillStyle = '#bbffff7f';
|
||||||
|
ctx.fillRect(0, 0, 1, 1);
|
||||||
|
ctx.fillStyle = 'black';
|
||||||
|
renderResource(ctx, tile, tile.resource);
|
||||||
|
},
|
||||||
|
[TileType.DESTINATION]: (ctx, tile) => {
|
||||||
|
if (tile.center) {
|
||||||
|
ctx.fillStyle = '#bbffbb';
|
||||||
|
ctx.fillRect(-2, -2, 5, 5);
|
||||||
|
ctx.fillStyle = 'black';
|
||||||
|
ctx.fillText('Deploy', 0.5, 0.65);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[TileType.EXTRACTOR]: (ctx, tile) => {
|
||||||
|
renderers[TileType.SOURCE]?.(ctx, tile.source);
|
||||||
|
ctx.drawImage(extractorImage, 0, 0, 1, 1);
|
||||||
|
},
|
||||||
|
[TileType.CONVEYOR]: (ctx, tile) => {
|
||||||
|
ctx.fillStyle = 'lightgray';
|
||||||
|
ctx.fillRect(0.2, 0.2, 0.6, 0.6);
|
||||||
|
},
|
||||||
|
[TileType.NOT]: imageRenderer(notImage),
|
||||||
|
[TileType.AND]: imageRenderer(andImage),
|
||||||
|
[TileType.OR]: imageRenderer(orImage),
|
||||||
};
|
};
|
||||||
|
|
@ -4,16 +4,19 @@ import { range } from './utils';
|
||||||
import { TileType } from './world';
|
import { TileType } from './world';
|
||||||
|
|
||||||
import styles from '../assets/ui.module.css';
|
import styles from '../assets/ui.module.css';
|
||||||
import selectIcon from '../assets/img/select.png';
|
import conveyorSrc from '../assets/img/conveyor.png';
|
||||||
import conveyorIcon from '../assets/img/conveyor.png';
|
import extractorSrc from '../assets/img/extractor.png';
|
||||||
import extractorIcon from '../assets/img/extractor.png';
|
import notSrc from '../assets/img/not.png';
|
||||||
import trashIcon from '../assets/img/trash.png';
|
import andSrc from '../assets/img/and.png';
|
||||||
|
import orSrc from '../assets/img/or.png';
|
||||||
|
|
||||||
export enum ToolType {
|
export enum ToolType {
|
||||||
SELECT,
|
SELECT,
|
||||||
EXTRACTOR,
|
EXTRACTOR,
|
||||||
CONVEYOR,
|
CONVEYOR,
|
||||||
DELETE = 9,
|
NOT,
|
||||||
|
OR,
|
||||||
|
AND,
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Tool {
|
interface Tool {
|
||||||
|
|
@ -24,33 +27,40 @@ interface Tool {
|
||||||
}
|
}
|
||||||
|
|
||||||
const TOOLS: (Tool | null)[] = [
|
const TOOLS: (Tool | null)[] = [
|
||||||
{
|
|
||||||
type: ToolType.SELECT,
|
|
||||||
title: 'Select',
|
|
||||||
icon: selectIcon,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
type: ToolType.EXTRACTOR,
|
type: ToolType.EXTRACTOR,
|
||||||
title: 'Extractor',
|
title: 'Extractor',
|
||||||
icon: extractorIcon,
|
icon: extractorSrc,
|
||||||
tileType: TileType.EXTRACTOR,
|
tileType: TileType.EXTRACTOR,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
type: ToolType.CONVEYOR,
|
type: ToolType.CONVEYOR,
|
||||||
title: 'Conveyor',
|
title: 'Conveyor',
|
||||||
icon: conveyorIcon,
|
icon: conveyorSrc,
|
||||||
tileType: TileType.CONVEYOR,
|
tileType: TileType.CONVEYOR,
|
||||||
},
|
},
|
||||||
null, // 4
|
{
|
||||||
null, // 5
|
type: ToolType.NOT,
|
||||||
|
title: 'Logical NOT',
|
||||||
|
icon: notSrc,
|
||||||
|
tileType: TileType.NOT,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: ToolType.AND,
|
||||||
|
title: 'Logical AND',
|
||||||
|
icon: andSrc,
|
||||||
|
tileType: TileType.AND,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: ToolType.OR,
|
||||||
|
title: 'Logical OR',
|
||||||
|
icon: orSrc,
|
||||||
|
tileType: TileType.OR,
|
||||||
|
},
|
||||||
null, // 6
|
null, // 6
|
||||||
null, // 7
|
null, // 7
|
||||||
null, // 8
|
null, // 8
|
||||||
{
|
null, // 9
|
||||||
type: ToolType.DELETE,
|
|
||||||
title: 'Delete',
|
|
||||||
icon: trashIcon,
|
|
||||||
},
|
|
||||||
];
|
];
|
||||||
|
|
||||||
export default class UI {
|
export default class UI {
|
||||||
|
|
@ -74,13 +84,17 @@ export default class UI {
|
||||||
render(fragment, this.root);
|
render(fragment, this.root);
|
||||||
}
|
}
|
||||||
|
|
||||||
onToolSelect = (tool: Tool | null) => {
|
onToolSelect(tool: Tool | null) {
|
||||||
if (tool) {
|
if (tool) {
|
||||||
this.currentTool = tool;
|
this.currentTool = tool;
|
||||||
this.render();
|
this.render();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
useSlot(slot: number) {
|
||||||
|
this.onToolSelect(TOOLS[slot - 1]);
|
||||||
|
}
|
||||||
|
|
||||||
get selectedTool() {
|
get selectedTool() {
|
||||||
return this.currentTool;
|
return this.currentTool;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,11 @@ export enum Direction {
|
||||||
WEST,
|
WEST,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isDirection(obj: any): obj is Direction {
|
||||||
|
if (typeof obj !== 'number') return false;
|
||||||
|
return Direction.NONE <= obj && obj <= Direction.WEST;
|
||||||
|
}
|
||||||
|
|
||||||
type Operand = Point | number;
|
type Operand = Point | number;
|
||||||
type Operation = (a: number, b: number) => number;
|
type Operation = (a: number, b: number) => number;
|
||||||
function op(a: Operand, b: Operand, fn: Operation): Operand {
|
function op(a: Operand, b: Operand, fn: Operation): Operand {
|
||||||
|
|
@ -149,9 +154,9 @@ export const NEXT_DIRECTION: Record<Direction, Direction> = {
|
||||||
[Direction.EAST]: Direction.SOUTH,
|
[Direction.EAST]: Direction.SOUTH,
|
||||||
};
|
};
|
||||||
export const ALL_DIRECTIONS = [Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST];
|
export const ALL_DIRECTIONS = [Direction.NORTH, Direction.EAST, Direction.SOUTH, Direction.WEST];
|
||||||
export const movePoint = (point: Point, direction: Direction): Point => [
|
export const movePoint = (point: Point, direction: Direction, scale: number = 1): Point => [
|
||||||
point[0] + DIRECTION_VECTORS[direction][0],
|
point[0] + DIRECTION_VECTORS[direction][0] * scale,
|
||||||
point[1] + DIRECTION_VECTORS[direction][1],
|
point[1] + DIRECTION_VECTORS[direction][1] * scale,
|
||||||
];
|
];
|
||||||
|
|
||||||
export const makeImage = (src: string): HTMLImageElement => {
|
export const makeImage = (src: string): HTMLImageElement => {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
import { ALL_DIRECTIONS, Direction, NEXT_DIRECTION, cyrb32, exp, getDirection, getOppositeDirection, movePoint, pointsEquals, trunc } from "./utils";
|
import { ALL_DIRECTIONS, Direction, NEXT_DIRECTION, cyrb32, exp, getDirection, getOppositeDirection, isDirection, movePoint, pointsEquals, trunc } from "./utils";
|
||||||
|
|
||||||
export enum TileType {
|
export enum TileType {
|
||||||
DESTINATION,
|
DESTINATION,
|
||||||
SOURCE,
|
SOURCE,
|
||||||
EXTRACTOR,
|
EXTRACTOR,
|
||||||
CONVEYOR,
|
CONVEYOR,
|
||||||
|
NOT,
|
||||||
|
AND,
|
||||||
|
OR,
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum PortDirection {
|
export enum PortDirection {
|
||||||
|
|
@ -12,19 +15,41 @@ export enum PortDirection {
|
||||||
OUTPUT
|
OUTPUT
|
||||||
}
|
}
|
||||||
|
|
||||||
type Resource = number;
|
interface TileLimit {
|
||||||
|
cooldown: number;
|
||||||
|
resourcesRequired: number;
|
||||||
|
capacity: number;
|
||||||
|
maxOutputs: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const LIMITS: Record<TileType, TileLimit> = {
|
||||||
|
[TileType.DESTINATION]: { cooldown: 0, resourcesRequired: 1, capacity: 4, maxOutputs: 0 },
|
||||||
|
[TileType.SOURCE]: { cooldown: 10000, resourcesRequired: 0, capacity: 0, maxOutputs: 0 },
|
||||||
|
[TileType.EXTRACTOR]: { cooldown: 5000, resourcesRequired: 0, capacity: 0, maxOutputs: 4 },
|
||||||
|
[TileType.CONVEYOR]: { cooldown: 3000, resourcesRequired: 1, capacity: 1, maxOutputs: 4 },
|
||||||
|
[TileType.NOT]: { cooldown: 3000, resourcesRequired: 1, capacity: 1, maxOutputs: 1 },
|
||||||
|
[TileType.AND]: { cooldown: 3000, resourcesRequired: 2, capacity: 2, maxOutputs: 1 },
|
||||||
|
[TileType.OR]: { cooldown: 3000, resourcesRequired: 2, capacity: 2, maxOutputs: 1 },
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Resource = number;
|
||||||
|
|
||||||
interface Port {
|
interface Port {
|
||||||
direction: PortDirection;
|
direction: PortDirection;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Ports = Partial<Record<Direction, Port>>;
|
type Ports = Partial<Record<Direction, Port>>;
|
||||||
|
type Inventory = Partial<Record<Direction, Resource>>;
|
||||||
|
|
||||||
interface BaseTile {
|
interface BaseTile {
|
||||||
ports: Ports;
|
ports: Ports;
|
||||||
|
inv: Inventory;
|
||||||
|
|
||||||
|
bufferedDirections?: Direction[];
|
||||||
|
nextInput?: Direction;
|
||||||
nextOutput?: Direction;
|
nextOutput?: Direction;
|
||||||
resource?: Resource;
|
|
||||||
timer?: number;
|
timer?: number;
|
||||||
|
animationTimer?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TileDestination extends BaseTile {
|
interface TileDestination extends BaseTile {
|
||||||
|
|
@ -46,15 +71,65 @@ interface TileConveyor extends BaseTile {
|
||||||
type: TileType.CONVEYOR;
|
type: TileType.CONVEYOR;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Tile = TileDestination | TileSource | TileExtractor | TileConveyor;
|
interface TileUnaryLogic extends BaseTile {
|
||||||
|
type: TileType.NOT;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface TileBinaryLogic extends BaseTile {
|
||||||
|
type: TileType.AND | TileType.OR;
|
||||||
|
// TODO internal buffers
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Tile = TileDestination | TileSource | TileExtractor | TileConveyor | TileUnaryLogic | TileBinaryLogic;
|
||||||
|
|
||||||
const id = (point: Point) => ((Math.floor(point[0]) & 0xFFFF) << 16) | Math.floor(point[1]) & 0xFFFF;
|
const id = (point: Point) => ((Math.floor(point[0]) & 0xFFFF) << 16) | Math.floor(point[1]) & 0xFFFF;
|
||||||
const deid = (pid: number): Point => [(pid >> 16) & 0xFFFF, pid & 0xFFFF];
|
const deid = (pid: number): Point => [(pid >> 16) & 0xFFFF, pid & 0xFFFF];
|
||||||
|
|
||||||
export const getPortDirections = (ports: Ports) => Object.keys(ports).map(k => +k as Direction);
|
export const getPortDirections = (ports: Ports) => Object.keys(ports).map(k => +k as Direction);
|
||||||
|
|
||||||
const findNextOutput = (ports: Ports, prevDirection?: Direction): Direction => {
|
export const getTileInputs = (tile: Tile) => ALL_DIRECTIONS.map(d => tile.inv[d]).filter(r => r != null);
|
||||||
const outputs = getPortDirections(ports).filter(d => d && ports[d]?.direction === PortDirection.OUTPUT);
|
export const getTileOutput = (tile: Tile): [Resource | undefined, Direction[]] => {
|
||||||
|
let bufferedResource = tile.inv[Direction.NONE];
|
||||||
|
let inputDirections: Direction[] = tile.bufferedDirections ?? [];
|
||||||
|
const availableResources = getTileInputs(tile);
|
||||||
|
const limits = LIMITS[tile.type];
|
||||||
|
if (availableResources.length < limits.resourcesRequired) {
|
||||||
|
return [undefined, []];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bufferedResource == null) {
|
||||||
|
if (tile.type === TileType.EXTRACTOR) {
|
||||||
|
bufferedResource = tile.source.resource;
|
||||||
|
} else if (limits.resourcesRequired === 1) {
|
||||||
|
const inputDirection = ALL_DIRECTIONS.find(d => typeof tile.inv[d] !== 'undefined');
|
||||||
|
if (inputDirection) {
|
||||||
|
let resource = tile.inv[inputDirection];
|
||||||
|
if (tile.type === TileType.NOT && resource) {
|
||||||
|
resource = ~(resource) & 0xF;
|
||||||
|
}
|
||||||
|
inputDirections = [inputDirection];
|
||||||
|
bufferedResource = resource;
|
||||||
|
}
|
||||||
|
} else if (limits.resourcesRequired === 2) {
|
||||||
|
inputDirections = ALL_DIRECTIONS.filter(d => typeof tile.inv[d] !== 'undefined').slice(0, 2);
|
||||||
|
const [x, y] = inputDirections.map(d => tile.inv[d]!);
|
||||||
|
switch (tile.type) {
|
||||||
|
case TileType.AND:
|
||||||
|
bufferedResource = (x & y) & 0xF;
|
||||||
|
break;
|
||||||
|
case TileType.OR:
|
||||||
|
bufferedResource = (x | y) & 0xF;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tile.inv[Direction.NONE] = bufferedResource;
|
||||||
|
tile.bufferedDirections = inputDirections;
|
||||||
|
}
|
||||||
|
return [bufferedResource, [Direction.NONE, ...inputDirections]];
|
||||||
|
}
|
||||||
|
|
||||||
|
const findNextPort = (ports: Ports, portDirection: PortDirection, prevDirection: Direction | undefined): Direction => {
|
||||||
|
const outputs = getPortDirections(ports).filter(d => d && ports[d]?.direction === portDirection);
|
||||||
if (outputs.length === 0) return Direction.NONE;
|
if (outputs.length === 0) return Direction.NONE;
|
||||||
|
|
||||||
if (prevDirection) {
|
if (prevDirection) {
|
||||||
|
|
@ -85,6 +160,7 @@ export default class World {
|
||||||
type: TileType.DESTINATION,
|
type: TileType.DESTINATION,
|
||||||
ports,
|
ports,
|
||||||
center: x == 2 && y == 2,
|
center: x == 2 && y == 2,
|
||||||
|
inv: {},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -99,6 +175,9 @@ export default class World {
|
||||||
const existingTile = this.getTile(position);
|
const existingTile = this.getTile(position);
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case TileType.CONVEYOR:
|
case TileType.CONVEYOR:
|
||||||
|
case TileType.NOT:
|
||||||
|
case TileType.AND:
|
||||||
|
case TileType.OR:
|
||||||
if (prevPosition) {
|
if (prevPosition) {
|
||||||
const prevPositionTile = this.getTile(prevPosition);
|
const prevPositionTile = this.getTile(prevPosition);
|
||||||
const ports: Ports = {};
|
const ports: Ports = {};
|
||||||
|
|
@ -117,32 +196,44 @@ export default class World {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
tile = {
|
tile = {
|
||||||
type: TileType.CONVEYOR,
|
type,
|
||||||
ports,
|
ports,
|
||||||
|
inv: {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (!existingTile) {
|
||||||
|
const ports = this.connectPorts(position);
|
||||||
|
tile = {
|
||||||
|
type,
|
||||||
|
ports,
|
||||||
|
inv: {},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case TileType.EXTRACTOR:
|
case TileType.EXTRACTOR:
|
||||||
if (existingTile?.type === TileType.SOURCE) {
|
if (existingTile?.type === TileType.SOURCE) {
|
||||||
const ports: Ports = {};
|
const ports = this.connectPorts(position, PortDirection.OUTPUT);
|
||||||
for (const direction of ALL_DIRECTIONS) {
|
|
||||||
const [neighbour, oppositeDirection] = this.getNeighbour(position, direction);
|
|
||||||
if (neighbour) {
|
|
||||||
if (!neighbour.ports[oppositeDirection]) {
|
|
||||||
neighbour.ports[oppositeDirection] = { direction: PortDirection.INPUT };
|
|
||||||
}
|
|
||||||
ports[direction] = { direction: PortDirection.OUTPUT };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tile = {
|
tile = {
|
||||||
type: TileType.EXTRACTOR,
|
type: TileType.EXTRACTOR,
|
||||||
ports,
|
ports,
|
||||||
source: existingTile,
|
source: existingTile,
|
||||||
|
inv: {},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
case TileType.DESTINATION:
|
||||||
|
case TileType.SOURCE:
|
||||||
|
// Naturally generated only
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
|
if (!existingTile) {
|
||||||
|
tile = {
|
||||||
|
type,
|
||||||
|
ports: {},
|
||||||
|
inv: {},
|
||||||
|
}
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -155,6 +246,24 @@ export default class World {
|
||||||
this.world.set(id(position), tile);
|
this.world.set(id(position), tile);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private connectPorts(position: Point, portDirection = PortDirection.INPUT): Ports {
|
||||||
|
const ports: Ports = {};
|
||||||
|
for (const direction of ALL_DIRECTIONS) {
|
||||||
|
const [neighbour, oppositeDirection] = this.getNeighbour(position, direction);
|
||||||
|
if (neighbour && neighbour.type !== TileType.SOURCE && neighbour.type !== TileType.DESTINATION) {
|
||||||
|
if (!neighbour.ports[oppositeDirection]) {
|
||||||
|
neighbour.ports[oppositeDirection] = {
|
||||||
|
direction: portDirection === PortDirection.OUTPUT
|
||||||
|
? PortDirection.INPUT
|
||||||
|
: PortDirection.OUTPUT
|
||||||
|
};
|
||||||
|
}
|
||||||
|
ports[direction] = { direction: portDirection };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ports;
|
||||||
|
}
|
||||||
|
|
||||||
removeTile(position: Point) {
|
removeTile(position: Point) {
|
||||||
position = trunc(position);
|
position = trunc(position);
|
||||||
const pid = id(position);
|
const pid = id(position);
|
||||||
|
|
@ -168,6 +277,13 @@ export default class World {
|
||||||
const [neighbour, oppositeDirection] = this.getNeighbour(position, direction);
|
const [neighbour, oppositeDirection] = this.getNeighbour(position, direction);
|
||||||
if (neighbour) {
|
if (neighbour) {
|
||||||
delete neighbour.ports[oppositeDirection];
|
delete neighbour.ports[oppositeDirection];
|
||||||
|
delete neighbour.inv[oppositeDirection];
|
||||||
|
if (neighbour.nextInput === oppositeDirection) {
|
||||||
|
neighbour.nextInput = findNextPort(neighbour.ports, PortDirection.INPUT, neighbour.nextInput);
|
||||||
|
}
|
||||||
|
if (neighbour.nextOutput === oppositeDirection) {
|
||||||
|
neighbour.nextOutput = findNextPort(neighbour.ports, PortDirection.OUTPUT, neighbour.nextOutput);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.world.delete(pid);
|
this.world.delete(pid);
|
||||||
|
|
@ -181,7 +297,7 @@ export default class World {
|
||||||
if (tile) return tile;
|
if (tile) return tile;
|
||||||
|
|
||||||
if (Math.abs(x) >= 5 && Math.abs(y) >= 5) {
|
if (Math.abs(x) >= 5 && Math.abs(y) >= 5) {
|
||||||
return this.genTile(position);
|
return this.genTile(trunc(position));
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -192,7 +308,7 @@ export default class World {
|
||||||
|
|
||||||
if ((hash & 0xFF) === 42) {
|
if ((hash & 0xFF) === 42) {
|
||||||
const resource = (hash >> 12) & 0xF || 1;
|
const resource = (hash >> 12) & 0xF || 1;
|
||||||
const newTile: Tile = { type: TileType.SOURCE, resource, ports: {} };
|
const newTile: Tile = { type: TileType.SOURCE, resource, ports: {}, inv: {} };
|
||||||
return newTile;
|
return newTile;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -203,47 +319,75 @@ export default class World {
|
||||||
return this.world.entries();
|
return this.world.entries();
|
||||||
}
|
}
|
||||||
|
|
||||||
private getNeighbour(position: Point, direction: Direction): [Tile | null, Direction] {
|
private getNeighbour(position: Point, direction: Direction | undefined): [Tile | null, Direction, Point] {
|
||||||
|
if (!direction) {
|
||||||
|
return [this.genTile(position), Direction.NONE, position];
|
||||||
|
}
|
||||||
const neighbourPosition = movePoint(position, direction);
|
const neighbourPosition = movePoint(position, direction);
|
||||||
const neighbour = this.getTile(neighbourPosition);
|
const neighbour = this.getTile(neighbourPosition);
|
||||||
const oppositeDirection = getOppositeDirection(direction);
|
const oppositeDirection = getOppositeDirection(direction);
|
||||||
|
|
||||||
return [neighbour, oppositeDirection];
|
return [neighbour, oppositeDirection, neighbourPosition];
|
||||||
}
|
}
|
||||||
|
|
||||||
update(dt: number) {
|
update(dt: number) {
|
||||||
for (const [pid, tile] of this.tiles) {
|
for (const [pid, tile] of this.tiles) {
|
||||||
|
if (!tile.nextInput) {
|
||||||
|
tile.nextInput = findNextPort(tile.ports, PortDirection.INPUT, tile.nextInput);
|
||||||
|
}
|
||||||
|
if (!tile.nextOutput) {
|
||||||
|
tile.nextOutput = findNextPort(tile.ports, PortDirection.OUTPUT, tile.nextOutput);
|
||||||
|
}
|
||||||
const position = deid(pid);
|
const position = deid(pid);
|
||||||
if (tile.timer && tile.timer > 0) {
|
if (tile.timer && tile.timer > 0) {
|
||||||
tile.timer -= dt;
|
tile.timer -= dt;
|
||||||
}
|
}
|
||||||
|
if (tile.animationTimer && tile.animationTimer > 0) {
|
||||||
|
tile.animationTimer -= dt;
|
||||||
|
}
|
||||||
|
|
||||||
if (!tile.timer || tile.timer <= 0) {
|
if (!tile.timer || tile.timer <= 0) {
|
||||||
switch (tile.type) {
|
switch (tile.type) {
|
||||||
case TileType.EXTRACTOR:
|
|
||||||
case TileType.CONVEYOR:
|
|
||||||
if (tile.resource) {
|
|
||||||
tile.nextOutput = findNextOutput(tile.ports, tile.nextOutput);
|
|
||||||
if (tile.nextOutput) {
|
|
||||||
const [neighbour, oppositeDirection] = this.getNeighbour(position, tile.nextOutput);
|
|
||||||
|
|
||||||
if (neighbour?.ports[oppositeDirection]?.direction === PortDirection.INPUT && !neighbour.resource) {
|
|
||||||
neighbour.resource = tile.resource;
|
|
||||||
neighbour.timer = 300; // TODO remove hardcode
|
|
||||||
tile.resource = undefined;
|
|
||||||
tile.timer = 500;
|
|
||||||
} else {
|
|
||||||
tile.timer = 100;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (tile.type === TileType.EXTRACTOR && !tile.resource) {
|
|
||||||
tile.resource = tile.source.resource;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case TileType.DESTINATION:
|
case TileType.DESTINATION:
|
||||||
// TODO count gathered
|
// TODO count gathered
|
||||||
tile.resource = undefined;
|
tile.inv = {};
|
||||||
|
break;
|
||||||
|
case TileType.SOURCE:
|
||||||
|
break; // source itself does nothing
|
||||||
|
default:
|
||||||
|
const [resource, inputDirections] = getTileOutput(tile);
|
||||||
|
if (resource != null) {
|
||||||
|
tile.nextOutput = findNextPort(tile.ports, PortDirection.OUTPUT, tile.nextOutput);
|
||||||
|
if (tile.nextOutput) {
|
||||||
|
const [neighbour, inputDirection, neighbourPosition] = this.getNeighbour(position, tile.nextOutput);
|
||||||
|
if (neighbour) {
|
||||||
|
const [priorityPusher,] = this.getNeighbour(neighbourPosition, neighbour.nextInput);
|
||||||
|
const limits = LIMITS[neighbour.type];
|
||||||
|
|
||||||
|
if (
|
||||||
|
neighbour.ports[inputDirection]?.direction === PortDirection.INPUT
|
||||||
|
&& neighbour.inv[inputDirection] == null
|
||||||
|
&& getTileInputs(neighbour).length < limits.capacity
|
||||||
|
&& (
|
||||||
|
neighbour.nextInput == null
|
||||||
|
|| neighbour.nextInput === inputDirection
|
||||||
|
|| !priorityPusher
|
||||||
|
|| getTileOutput(priorityPusher)[0] == null
|
||||||
|
|| limits.resourcesRequired > 1
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
neighbour.inv[inputDirection] = resource;
|
||||||
|
if (getTileInputs(neighbour).length >= limits.capacity) {
|
||||||
|
neighbour.timer = limits.cooldown;
|
||||||
|
neighbour.nextInput = findNextPort(neighbour.ports, PortDirection.INPUT, neighbour.nextInput);
|
||||||
|
}
|
||||||
|
|
||||||
|
inputDirections.forEach(inputDirection => delete tile.inv[inputDirection]);
|
||||||
|
tile.timer = LIMITS[tile.type].cooldown;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue