Delivery counting, some graphical fixes
This commit is contained in:
parent
688f548ef3
commit
99ed5dd8c8
|
|
@ -14,8 +14,6 @@ export default class Game {
|
|||
private paused = false;
|
||||
|
||||
constructor(private canvas: HTMLCanvasElement, controls: HTMLElement) {
|
||||
window.addEventListener('resize', this.onResize);
|
||||
this.onResize();
|
||||
|
||||
canvas.focus();
|
||||
|
||||
|
|
@ -31,11 +29,17 @@ export default class Game {
|
|||
this.graphics = new Graphics(canvas);
|
||||
this.world = new World();
|
||||
this.ui = new UI(controls);
|
||||
|
||||
window.addEventListener('resize', this.onResize);
|
||||
this.onResize();
|
||||
this.graphics.resetView();
|
||||
}
|
||||
|
||||
private onResize = () => {
|
||||
this.canvas.width = window.innerWidth;
|
||||
this.canvas.height = window.innerHeight;
|
||||
|
||||
this.graphics.resetStyle();
|
||||
}
|
||||
|
||||
private onScroll = (event: WheelEvent) => {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { defaultRenderer, renderPorts, renderResource, renderers, type NullRenderer, type Renderer } from "./renderer";
|
||||
import { defaultRenderer, renderPorts, renderResource, renderers, type NullRenderer, type Renderer, type ViewConfig } from "./renderer";
|
||||
import { ALL_DIRECTIONS, Direction, exp, trunc } from "./utils";
|
||||
import type World from "./world";
|
||||
import { type Tile } from "./world";
|
||||
|
|
@ -11,15 +11,10 @@ export default class Graphics {
|
|||
private offset: Point = [0, 0];
|
||||
private highlighted: Point = [0, 0];
|
||||
private tooltip: [Point, string] | null = null;
|
||||
private firstStyleReset = false;
|
||||
|
||||
constructor(private canvas: HTMLCanvasElement) {
|
||||
this.context = canvas.getContext('2d')!;
|
||||
this.context.imageSmoothingEnabled = false;
|
||||
this.context.font = 'bold 0.3px sans-serif';
|
||||
this.context.textRendering = 'optimizeSpeed';
|
||||
this.context.textAlign = 'center';
|
||||
|
||||
this.resetView();
|
||||
this.context = this.canvas.getContext('2d')!;
|
||||
}
|
||||
|
||||
get width() {
|
||||
|
|
@ -46,6 +41,15 @@ export default class Graphics {
|
|||
this.offset = exp`${this.offset} + ${amount}`;
|
||||
}
|
||||
|
||||
resetStyle() {
|
||||
this.context = this.canvas.getContext('2d')!;
|
||||
this.context.imageSmoothingEnabled = false;
|
||||
this.context.font = 'bold 0.3px sans-serif';
|
||||
this.context.textRendering = 'optimizeSpeed';
|
||||
this.context.textAlign = 'center';
|
||||
this.context.textBaseline = 'middle';
|
||||
}
|
||||
|
||||
resetView() {
|
||||
this.tileSize = initialTileSize;
|
||||
this.offset = exp`(${this.size} - ${this.tileSize}) / ${2}`;
|
||||
|
|
@ -135,6 +139,9 @@ export default class Graphics {
|
|||
drawTile<T extends null>(position: Point, renderer: NullRenderer): void;
|
||||
drawTile<T extends Tile>(position: Point, renderer: Renderer<T> | NullRenderer, tile?: T, drawPorts: boolean = true): void {
|
||||
this.context.save();
|
||||
const viewConfig: ViewConfig = {
|
||||
tileSize: this.tileSize,
|
||||
}
|
||||
|
||||
// TODO skip drawing if outside screen
|
||||
const screenPosition = this.worldToScreen(trunc(position));
|
||||
|
|
@ -143,11 +150,11 @@ export default class Graphics {
|
|||
|
||||
if (tile) {
|
||||
if (drawPorts) {
|
||||
renderPorts(this.context, tile);
|
||||
renderPorts(this.context, viewConfig, tile);
|
||||
}
|
||||
renderer(this.context, tile);
|
||||
renderer(this.context, viewConfig, tile);
|
||||
} else {
|
||||
(renderer as NullRenderer)(this.context);
|
||||
(renderer as NullRenderer)(this.context, viewConfig);
|
||||
}
|
||||
|
||||
this.context.restore();
|
||||
|
|
@ -174,7 +181,7 @@ export default class Graphics {
|
|||
}
|
||||
|
||||
this.context.fillStyle = 'blue';
|
||||
for (const [pos, tile] of resourceTiles) {
|
||||
for (const [pos, tile] of resourceTiles) {
|
||||
this.drawTile(pos, renderResource, tile, false);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { type Tile, TileType, getPortDirections, PortDirection, LIMITS, type Resource, getTileOutput } from "./world";
|
||||
import { type Tile, TileType, getPortDirections, PortDirection, LIMITS, type Resource, getTileOutput, ResourceType } from "./world";
|
||||
|
||||
import { ALL_DIRECTIONS, Direction, makeImage, movePoint } from "./utils";
|
||||
|
||||
|
|
@ -8,8 +8,12 @@ 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 NullRenderer = (ctx: CanvasRenderingContext2D) => void;
|
||||
export interface ViewConfig {
|
||||
tileSize: number;
|
||||
}
|
||||
|
||||
export type Renderer<T extends Tile> = (ctx: CanvasRenderingContext2D, view: ViewConfig, tile: T) => void;
|
||||
export type NullRenderer = (ctx: CanvasRenderingContext2D, view: ViewConfig) => void;
|
||||
|
||||
type Renderers = {
|
||||
[K in Tile['type']]?: Renderer<Extract<Tile, { type: K }>>
|
||||
|
|
@ -21,30 +25,42 @@ const notImage = makeImage(notSrc);
|
|||
const andImage = makeImage(andSrc);
|
||||
const orImage = makeImage(orSrc);
|
||||
|
||||
const px = 1 / 32;
|
||||
export const renderResource = (ctx: CanvasRenderingContext2D, tile: Tile, resource?: Resource) => {
|
||||
let resources: [Direction, Resource | undefined][] = resource
|
||||
? [[Direction.NONE, resource]]
|
||||
: [...ALL_DIRECTIONS, Direction.NONE].map((d) => [d, tile.inv[d]]);
|
||||
|
||||
const [output] = getTileOutput(tile);
|
||||
if (output && (tile.timer ?? 0) <= 0) {
|
||||
resources = [[Direction.NONE, output]];
|
||||
export const renderResource = (ctx: CanvasRenderingContext2D, view: ViewConfig, tile: Tile) => {
|
||||
let resources: [Direction, Resource | undefined][] | undefined;
|
||||
if (tile.type === TileType.SOURCE) {
|
||||
resources = [[Direction.NONE, tile.resource]]
|
||||
} else if (tile.type === TileType.EXTRACTOR) {
|
||||
resources = [[Direction.NONE, tile.source.resource]]
|
||||
} else if ((tile.timer ?? 0) <= 0 && tile.type !== TileType.DESTINATION) { // when resource was precessed by machine
|
||||
const [output] = getTileOutput(tile);
|
||||
if (output && (tile.timer ?? 0) <= 0) {
|
||||
resources = [[Direction.NONE, output]];
|
||||
}
|
||||
}
|
||||
|
||||
if (!resources) {
|
||||
resources = [...ALL_DIRECTIONS, Direction.NONE].map((d) => [d, tile.inv[d]]);
|
||||
}
|
||||
|
||||
const fontScale = Math.max(Math.pow(32 / view.tileSize, 1.5), 1);
|
||||
const px = 1 / 32;
|
||||
|
||||
let wasOtherDrawn = false;
|
||||
for (const [direction, res] of resources) {
|
||||
if (direction === Direction.NONE && wasOtherDrawn) continue;
|
||||
if (res) {
|
||||
if (res?.type === ResourceType.NUMBER) {
|
||||
const str = res.value.toString(2);
|
||||
const [timer, timerMax] = tile.inputAnimations?.get(direction) ?? [0, 1];
|
||||
const amount = timer / timerMax;
|
||||
|
||||
const [x, y] = movePoint([0.5, 0.62], direction, amount);
|
||||
const [x, y] = movePoint([0.5, 0.52], direction, amount);
|
||||
|
||||
ctx.strokeStyle = 'white';
|
||||
ctx.lineWidth = px * 2;
|
||||
ctx.miterLimit = 2;
|
||||
if (tile.type === TileType.SOURCE) {
|
||||
ctx.font = `bold ${0.3 * fontScale}px sans-serif`;
|
||||
}
|
||||
ctx.strokeText(str, x, y);
|
||||
ctx.fillText(str, x, y);
|
||||
wasOtherDrawn = true;
|
||||
|
|
@ -52,7 +68,7 @@ export const renderResource = (ctx: CanvasRenderingContext2D, tile: Tile, resour
|
|||
}
|
||||
}
|
||||
|
||||
export const renderPorts = (ctx: CanvasRenderingContext2D, tile: Tile) => {
|
||||
export const renderPorts = (ctx: CanvasRenderingContext2D, _view: ViewConfig, tile: Tile) => {
|
||||
if (tile.type === TileType.DESTINATION || tile.type === TileType.SOURCE) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -84,13 +100,13 @@ const imageRenderer = <T extends Tile>(image: HTMLImageElement): Renderer<T> =>
|
|||
}
|
||||
|
||||
export const renderers: Renderers = {
|
||||
[TileType.SOURCE]: (ctx, tile) => {
|
||||
[TileType.SOURCE]: (ctx, view, tile) => {
|
||||
ctx.fillStyle = '#bbffff7f';
|
||||
ctx.fillRect(0, 0, 1, 1);
|
||||
ctx.fillStyle = 'black';
|
||||
renderResource(ctx, tile, tile.resource);
|
||||
renderResource(ctx, view, tile);
|
||||
},
|
||||
[TileType.DESTINATION]: (ctx, tile) => {
|
||||
[TileType.DESTINATION]: (ctx, _view, tile) => {
|
||||
if (tile.center) {
|
||||
ctx.fillStyle = '#bbffbb';
|
||||
ctx.fillRect(-2, -2, 5, 5);
|
||||
|
|
@ -98,11 +114,11 @@ export const renderers: Renderers = {
|
|||
ctx.fillText('Deploy', 0.5, 0.65);
|
||||
}
|
||||
},
|
||||
[TileType.EXTRACTOR]: (ctx, tile) => {
|
||||
renderers[TileType.SOURCE]?.(ctx, tile.source);
|
||||
[TileType.EXTRACTOR]: (ctx, view, tile) => {
|
||||
renderers[TileType.SOURCE]?.(ctx, view, tile.source);
|
||||
ctx.drawImage(extractorImage, 0, 0, 1, 1);
|
||||
},
|
||||
[TileType.CONVEYOR]: (ctx, tile) => {
|
||||
[TileType.CONVEYOR]: (ctx, _view, _tile) => {
|
||||
ctx.fillStyle = 'lightgray';
|
||||
ctx.fillRect(0.2, 0.2, 0.6, 0.6);
|
||||
},
|
||||
|
|
|
|||
|
|
@ -15,6 +15,15 @@ export enum PortDirection {
|
|||
OUTPUT
|
||||
}
|
||||
|
||||
export enum ResourceType {
|
||||
NUMBER,
|
||||
ITEM,
|
||||
}
|
||||
|
||||
export enum ResourceItemType {
|
||||
|
||||
}
|
||||
|
||||
interface TileLimit {
|
||||
cooldown: number;
|
||||
resourcesRequired: number;
|
||||
|
|
@ -27,13 +36,26 @@ 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: 600, resourcesRequired: 0, capacity: 0, maxOutputs: 4 },
|
||||
[TileType.EXTRACTOR]: { cooldown: 300, resourcesRequired: 0, capacity: 0, maxOutputs: 4 },
|
||||
[TileType.NOT]: { cooldown: 3000, resourcesRequired: 1, capacity: 1, maxOutputs: 1 },
|
||||
[TileType.AND]: { cooldown: 6000, resourcesRequired: 2, capacity: 2, maxOutputs: 1 },
|
||||
[TileType.OR]: { cooldown: 3000, resourcesRequired: 2, capacity: 2, maxOutputs: 1 },
|
||||
};
|
||||
|
||||
export type Resource = { readonly value: number };
|
||||
interface BaseResource {
|
||||
}
|
||||
|
||||
interface ResourceNumber extends BaseResource {
|
||||
type: ResourceType.NUMBER;
|
||||
value: number;
|
||||
}
|
||||
|
||||
interface ResourceItem extends BaseResource {
|
||||
type: ResourceType.ITEM;
|
||||
itemType: ResourceItemType;
|
||||
}
|
||||
|
||||
export type Resource = ResourceNumber | ResourceItem;
|
||||
|
||||
interface Port {
|
||||
direction: PortDirection;
|
||||
|
|
@ -46,6 +68,9 @@ type AnimationTimers = Map<Direction, [number, number]>;
|
|||
interface BaseTile {
|
||||
ports: Ports;
|
||||
inv: Inventory;
|
||||
|
||||
acceptedResources?: ResourceType[];
|
||||
|
||||
inputAnimations?: AnimationTimers;
|
||||
|
||||
bufferedDirections?: Direction[];
|
||||
|
|
@ -87,6 +112,16 @@ export type Tile = TileDestination | TileSource | TileExtractor | TileConveyor |
|
|||
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 rid = (resource: Resource): number => {
|
||||
const idPart = (resource.type & 0xFFFF) << 16;
|
||||
if (resource.type === ResourceType.NUMBER) {
|
||||
return idPart | (resource.value) & 0xFFFF;
|
||||
} else if (resource.type === ResourceType.ITEM) {
|
||||
return idPart | (resource.itemType) & 0xFFFF;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
export const getPortDirections = (ports: Ports) => Object.keys(ports).map(k => +k as Direction);
|
||||
|
||||
export const getTileInputs = (tile: Tile) => ALL_DIRECTIONS.map(d => tile.inv[d]).filter(r => r != null);
|
||||
|
|
@ -106,22 +141,24 @@ export const getTileOutput = (tile: Tile): [Resource | undefined, Direction[]] =
|
|||
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 = { value: ~(resource.value) & 0xF };
|
||||
if (tile.type === TileType.NOT && resource?.type === ResourceType.NUMBER) {
|
||||
resource = { ...resource, value: ~(resource.value) & 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 = { value: (x.value & y.value) & 0xF };
|
||||
break;
|
||||
case TileType.OR:
|
||||
bufferedResource = { value: (x.value | y.value) & 0xF };
|
||||
break;
|
||||
const [x, y] = inputDirections.map(d => tile.inv[d]);
|
||||
if (x?.type === ResourceType.NUMBER && y?.type === ResourceType.NUMBER) {
|
||||
switch (tile.type) {
|
||||
case TileType.AND:
|
||||
bufferedResource = { type: ResourceType.NUMBER, value: (x.value & y.value) & 0xF };
|
||||
break;
|
||||
case TileType.OR:
|
||||
bufferedResource = { type: ResourceType.NUMBER, value: (x.value | y.value) & 0xF };
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
tile.inv[Direction.NONE] = bufferedResource;
|
||||
|
|
@ -148,8 +185,8 @@ const findNextPort = (ports: Ports, portDirection: PortDirection, prevDirection:
|
|||
|
||||
export default class World {
|
||||
private world = new Map<number, Tile>();
|
||||
private deliveredResources = new Map<number, number>();
|
||||
constructor(private seed: number = Math.random() * 2e9) {
|
||||
|
||||
for (let x = 0; x < 5; x++) {
|
||||
for (let y = 0; y < 5; y++) {
|
||||
const ports: Ports = {};
|
||||
|
|
@ -308,9 +345,14 @@ export default class World {
|
|||
private genTile(position: Point): Tile | null {
|
||||
const hash = cyrb32(this.seed, ...position);
|
||||
|
||||
if ((hash & 0xFF) === 42) {
|
||||
const value = (hash >> 12) & 0xF || 1;
|
||||
const newTile: Tile = { type: TileType.SOURCE, resource: { value }, ports: {}, inv: {} };
|
||||
if ([42, 69, 0x42, 0x69].includes(hash & 0xFF)) {
|
||||
let mask = 1;
|
||||
const dist = Math.log10(Math.hypot(...position));
|
||||
for (let i = 1; i < dist; i++) {
|
||||
mask = (mask << 1) | 1;
|
||||
}
|
||||
const value = (hash >> 12) & mask;
|
||||
const newTile: Tile = { type: TileType.SOURCE, resource: { type: ResourceType.NUMBER, value }, ports: {}, inv: {} };
|
||||
return newTile;
|
||||
}
|
||||
|
||||
|
|
@ -352,8 +394,15 @@ export default class World {
|
|||
if (!tile.timer || tile.timer <= 0) {
|
||||
switch (tile.type) {
|
||||
case TileType.DESTINATION:
|
||||
// TODO count gathered
|
||||
tile.inv = {};
|
||||
const isInputting = Array.from(tile.inputAnimations?.values() ?? []).some(a => a[0] > 0);
|
||||
if (!isInputting) {
|
||||
for (const resource of Object.values(tile.inv)) {
|
||||
const resourceId = rid(resource);
|
||||
const gatheredAmount = this.deliveredResources.get(resourceId) ?? 0;
|
||||
this.deliveredResources.set(resourceId, gatheredAmount + 1);
|
||||
}
|
||||
tile.inv = {};
|
||||
}
|
||||
break;
|
||||
case TileType.SOURCE:
|
||||
break; // source itself does nothing
|
||||
|
|
|
|||
Loading…
Reference in New Issue