1
0
mirror of https://github.com/tobspr/shapez.io.git synced 2026-03-02 03:39:21 +00:00

Initial commit

This commit is contained in:
Tobias Springer
2020-05-09 16:45:23 +02:00
commit 93c6ea683d
304 changed files with 56031 additions and 0 deletions

View File

@@ -0,0 +1,92 @@
import { Component } from "../component";
import { types } from "../../savegame/serialization";
import { gItemRegistry } from "../../core/global_registries";
import { BaseItem } from "../base_item";
import { Vector, enumDirection } from "../../core/vector";
import { Math_PI, Math_sin, Math_cos } from "../../core/builtins";
import { globalConfig } from "../../core/config";
export class BeltComponent extends Component {
static getId() {
return "Belt";
}
static getSchema() {
return {
direction: types.string,
sortedItems: types.array(types.pair(types.ufloat, types.obj(gItemRegistry))),
};
}
/**
*
* @param {object} param0
* @param {enumDirection=} param0.direction The direction of the belt
*/
constructor({ direction = enumDirection.top }) {
super();
this.direction = direction;
/** @type {Array<[number, BaseItem]>} */
this.sortedItems = [];
}
/**
* Converts from belt space (0 = start of belt ... 1 = end of belt) to the local
* belt coordinates (-0.5|-0.5 to 0.5|0.5)
* @param {number} progress
* @returns {Vector}
*/
transformBeltToLocalSpace(progress) {
switch (this.direction) {
case enumDirection.top:
return new Vector(0, 0.5 - progress);
case enumDirection.right: {
const arcProgress = progress * 0.5 * Math_PI;
return new Vector(0.5 - 0.5 * Math_cos(arcProgress), 0.5 - 0.5 * Math_sin(arcProgress));
}
case enumDirection.left: {
const arcProgress = progress * 0.5 * Math_PI;
return new Vector(-0.5 + 0.5 * Math_cos(arcProgress), 0.5 - 0.5 * Math_sin(arcProgress));
}
default:
assertAlways(false, "Invalid belt direction: " + this.direction);
return new Vector(0, 0);
}
}
/**
* Returns if the belt can currently accept an item from the given direction
* @param {enumDirection} direction
*/
canAcceptNewItem(direction) {
const firstItem = this.sortedItems[0];
if (!firstItem) {
return true;
}
return firstItem[0] > globalConfig.itemSpacingOnBelts;
}
/**
* Pushes a new item to the belt
* @param {BaseItem} item
* @param {enumDirection} direction
*/
takeNewItem(item, direction) {
this.sortedItems.unshift([0, item]);
}
/**
* Returns how much space there is to the first item
*/
getDistanceToFirstItemCenter() {
const firstItem = this.sortedItems[0];
if (!firstItem) {
return 1;
}
return firstItem[0];
}
}

View File

@@ -0,0 +1,25 @@
import { Component } from "../component";
import { ShapeDefinition } from "../shape_definition";
export class HubComponent extends Component {
static getId() {
return "Hub";
}
constructor() {
super();
/**
* Shape definitions in queue to be analyzed and counted towards the goal
* @type {Array<ShapeDefinition>}
*/
this.definitionsToAnalyze = [];
}
/**
* @param {ShapeDefinition} definition
*/
queueShapeDefinition(definition) {
this.definitionsToAnalyze.push(definition);
}
}

View File

@@ -0,0 +1,129 @@
import { Component } from "../component";
import { Vector, enumDirection, enumDirectionToAngle, enumInvertedDirections } from "../../core/vector";
import { BaseItem } from "../base_item";
import { ShapeItem } from "../items/shape_item";
import { ColorItem } from "../items/color_item";
/**
* @enum {string?}
*/
export const enumItemAcceptorItemFilter = {
shape: "shape",
color: "color",
none: null,
};
/** @typedef {{
* pos: Vector,
* directions: enumDirection[],
* filter?: enumItemAcceptorItemFilter
* }} ItemAcceptorSlot */
export class ItemAcceptorComponent extends Component {
static getId() {
return "ItemAcceptor";
}
static getSchema() {
return {
// slots: "TODO",
};
}
/**
*
* @param {object} param0
* @param {Array<{pos: Vector, directions: enumDirection[], filter?: enumItemAcceptorItemFilter}>} param0.slots The slots from which we accept items
*/
constructor({ slots }) {
super();
this.setSlots(slots);
}
/**
*
* @param {Array<{pos: Vector, directions: enumDirection[], filter?: enumItemAcceptorItemFilter}>} slots
*/
setSlots(slots) {
/** @type {Array<{pos: Vector, directions: enumDirection[], filter?: enumItemAcceptorItemFilter}>} */
this.slots = [];
for (let i = 0; i < slots.length; ++i) {
const slot = slots[i];
this.slots.push({
pos: slot.pos,
directions: slot.directions,
// Which type of item to accept (shape | color | all) @see enumItemAcceptorItemFilter
filter: slot.filter,
});
}
}
/**
* Returns if this acceptor can accept a new item at slot N
* @param {number} slotIndex
* @param {BaseItem=} item
*/
canAcceptItem(slotIndex, item) {
const slot = this.slots[slotIndex];
switch (slot.filter) {
case enumItemAcceptorItemFilter.shape: {
return item instanceof ShapeItem;
}
case enumItemAcceptorItemFilter.color: {
return item instanceof ColorItem;
}
default:
return true;
}
}
/**
* Tries to find a slot which accepts the current item
* @param {Vector} targetLocalTile
* @param {enumDirection} fromLocalDirection
* @returns {{
* slot: ItemAcceptorSlot,
* index: number,
* acceptedDirection: enumDirection
* }|null}
*/
findMatchingSlot(targetLocalTile, fromLocalDirection) {
// We need to invert our direction since the acceptor specifies *from* which direction
// it accepts items, but the ejector specifies *into* which direction it ejects items.
// E.g.: Ejector ejects into "right" direction but acceptor accepts from "left" direction.
const desiredDirection = enumInvertedDirections[fromLocalDirection];
// Go over all slots and try to find a target slot
for (let slotIndex = 0; slotIndex < this.slots.length; ++slotIndex) {
const slot = this.slots[slotIndex];
// const acceptorLocalPosition = targetStaticComp.applyRotationToVector(
// slot.pos
// );
// const acceptorGlobalPosition = acceptorLocalPosition.add(targetStaticComp.origin);
// Make sure the acceptor slot is on the right position
if (!slot.pos.equals(targetLocalTile)) {
continue;
}
// Check if the acceptor slot accepts items from our direction
for (let i = 0; i < slot.directions.length; ++i) {
// const localDirection = targetStaticComp.localDirectionToWorld(slot.directions[l]);
if (desiredDirection === slot.directions[i]) {
return {
slot,
index: slotIndex,
acceptedDirection: desiredDirection,
};
}
}
}
// && this.canAcceptItem(slotIndex, ejectingItem)
return null;
}
}

View File

@@ -0,0 +1,162 @@
import { globalConfig } from "../../core/config";
import { Vector, enumDirection, enumDirectionToVector } from "../../core/vector";
import { BaseItem } from "../base_item";
import { Component } from "../component";
/**
* @typedef {{
* pos: Vector,
* direction: enumDirection,
* item: BaseItem,
* progress: number?
* }} ItemEjectorSlot
*/
export class ItemEjectorComponent extends Component {
static getId() {
return "ItemEjector";
}
static getSchema() {
return {
// slots: "TODO"
};
}
/**
*
* @param {object} param0
* @param {Array<{pos: Vector, direction: enumDirection}>} param0.slots The slots to eject on
* @param {boolean=} param0.instantEject If the ejection is instant
*/
constructor({ slots, instantEject = false }) {
super();
// How long items take to eject
this.instantEject = instantEject;
this.setSlots(slots);
}
/**
* @param {Array<{pos: Vector, direction: enumDirection}>} slots The slots to eject on
*/
setSlots(slots) {
/** @type {Array<ItemEjectorSlot>} */
this.slots = [];
for (let i = 0; i < slots.length; ++i) {
const slot = slots[i];
this.slots.push({
pos: slot.pos,
direction: slot.direction,
item: null,
progress: 0,
});
}
}
/**
* Returns the amount of slots
*/
getNumSlots() {
return this.slots.length;
}
/**
* Returns where this slot ejects to
* @param {number} index
* @returns {Vector}
*/
getSlotTargetLocalTile(index) {
const slot = this.slots[index];
const directionVector = enumDirectionToVector[slot.direction];
return slot.pos.add(directionVector);
}
/**
* Returns whether any slot ejects to the given local tile
* @param {Vector} tile
*/
anySlotEjectsToLocalTile(tile) {
for (let i = 0; i < this.slots.length; ++i) {
if (this.getSlotTargetLocalTile(i).equals(tile)) {
return true;
}
}
return false;
}
/**
* Returns if slot # is currently ejecting
* @param {number} slotIndex
* @returns {boolean}
*/
isSlotEjecting(slotIndex) {
assert(slotIndex >= 0 && slotIndex < this.slots.length, "Invalid ejector slot: " + slotIndex);
return !!this.slots[slotIndex].item;
}
/**
* Returns if we can eject on a given slot
* @param {number} slotIndex
* @returns {boolean}
*/
canEjectOnSlot(slotIndex) {
assert(slotIndex >= 0 && slotIndex < this.slots.length, "Invalid ejector slot: " + slotIndex);
return !this.slots[slotIndex].item;
}
/**
* Returns the first free slot on this ejector or null if there is none
* @returns {number?}
*/
getFirstFreeSlot() {
for (let i = 0; i < this.slots.length; ++i) {
if (this.canEjectOnSlot(i)) {
return i;
}
}
return null;
}
/**
* Returns if any slot is ejecting
* @returns {boolean}
*/
isAnySlotEjecting() {
for (let i = 0; i < this.slots.length; ++i) {
if (this.slots[i].item) {
return true;
}
}
return false;
}
/**
* Returns if any slot is free
* @returns {boolean}
*/
hasAnySlotFree() {
for (let i = 0; i < this.slots.length; ++i) {
if (this.canEjectOnSlot(i)) {
return true;
}
}
return false;
}
/**
* Tries to eject a given item
* @param {number} slotIndex
* @param {BaseItem} item
* @returns {boolean}
*/
tryEject(slotIndex, item) {
if (!this.canEjectOnSlot(slotIndex)) {
return false;
}
this.slots[slotIndex].item = item;
this.slots[slotIndex].progress = this.instantEject ? 1 : 0;
return true;
}
}

View File

@@ -0,0 +1,106 @@
import { BaseItem } from "../base_item";
import { Component } from "../component";
import { enumDirection, Vector } from "../../core/vector";
/** @enum {string} */
export const enumItemProcessorTypes = {
splitter: "splitter",
cutter: "cutter",
rotater: "rotater",
stacker: "stacker",
trash: "trash",
mixer: "mixer",
painter: "painter",
hub: "hub",
};
export class ItemProcessorComponent extends Component {
static getId() {
return "ItemProcessor";
}
static getSchema() {
return {
// TODO
};
}
/**
*
* @param {object} param0
* @param {enumItemProcessorTypes} param0.processorType Which type of processor this is
* @param {number} param0.inputsPerCharge How many items this machine needs until it can start working
* @param {Array<{pos: Vector, direction: enumDirection}>=} param0.beltUnderlays Where to render belt underlays
*
*/
constructor({ processorType = enumItemProcessorTypes.splitter, inputsPerCharge, beltUnderlays = [] }) {
super();
// Which slot to emit next, this is only a preference and if it can't emit
// it will take the other one. Some machines ignore this (e.g. the splitter) to make
// sure the outputs always match
this.nextOutputSlot = 0;
// Type of the processor
this.type = processorType;
// How many inputs we need for one charge
this.inputsPerCharge = inputsPerCharge;
// Which belt underlays to render
this.beltUnderlays = beltUnderlays;
/**
* Our current inputs
* @type {Array<{ item: BaseItem, sourceSlot: number }>}
*/
this.inputSlots = [];
/**
* What we are currently processing, empty if we don't produce anything rn
* requiredSlot: Item *must* be ejected on this slot
* preferredSlot: Item *can* be ejected on this slot, but others are fine too if the one is not usable
* @type {Array<{item: BaseItem, requiredSlot?: number, preferredSlot?: number}>}
*/
this.itemsToEject = [];
/**
* How long it takes until we are done with the current items
*/
this.secondsUntilEject = 0;
/**
* Fixes belt animations
* @type {Array<{ item: BaseItem, slotIndex: number, animProgress: number, direction: enumDirection}>}
*/
this.itemConsumptionAnimations = [];
}
/**
* Tries to take the item
* @param {BaseItem} item
*/
tryTakeItem(item, sourceSlot, sourceDirection) {
if (this.inputSlots.length >= this.inputsPerCharge) {
// Already full
return false;
}
// Check that we only take one item per slot
for (let i = 0; i < this.inputSlots.length; ++i) {
const slot = this.inputSlots[i];
if (slot.sourceSlot === sourceSlot) {
return false;
}
}
this.inputSlots.push({ item, sourceSlot });
this.itemConsumptionAnimations.push({
item,
slotIndex: sourceSlot,
direction: sourceDirection,
animProgress: 0.0,
});
return true;
}
}

View File

@@ -0,0 +1,23 @@
import { globalConfig } from "../../core/config";
import { types } from "../../savegame/serialization";
import { Component } from "../component";
export class MinerComponent extends Component {
static getId() {
return "Miner";
}
static getSchema() {
return {
lastMiningTime: types.ufloat,
};
}
/**
* @param {object} param0
*/
constructor({}) {
super();
this.lastMiningTime = 0;
}
}

View File

@@ -0,0 +1,11 @@
import { Component } from "../component";
/**
* Marks an entity as replaceable, so that when other buildings are placed above him it
* simply gets deleted
*/
export class ReplaceableMapEntityComponent extends Component {
static getId() {
return "ReplaceableMapEntity";
}
}

View File

@@ -0,0 +1,184 @@
import { Math_radians } from "../../core/builtins";
import { globalConfig } from "../../core/config";
import { DrawParameters } from "../../core/draw_parameters";
import { Rectangle } from "../../core/rectangle";
import { AtlasSprite } from "../../core/sprites";
import { enumDirection, Vector } from "../../core/vector";
import { types } from "../../savegame/serialization";
import { Component } from "../component";
export class StaticMapEntityComponent extends Component {
static getId() {
return "StaticMapEntity";
}
static getSchema() {
return {
origin: types.tileVector,
tileSize: types.tileVector,
rotationDegrees: types.uint,
spriteKey: types.string,
};
}
/**
*
* @param {object} param0
* @param {Vector=} param0.origin Origin (Top Left corner) of the entity
* @param {Vector=} param0.tileSize Size of the entity in tiles
* @param {number=} param0.rotationDegrees Rotation in degrees. Must be multiple of 90
* @param {string=} param0.spriteKey Optional sprite
* @param {string=} param0.silhouetteColor Optional silhouette color override
*/
constructor({
origin = new Vector(),
tileSize = new Vector(1, 1),
rotationDegrees = 0,
spriteKey = null,
silhouetteColor = null,
}) {
super();
assert(
rotationDegrees % 90 === 0,
"Rotation of static map entity must be multiple of 90 (was " + rotationDegrees + ")"
);
this.origin = origin;
this.tileSize = tileSize;
this.spriteKey = spriteKey;
this.rotationDegrees = rotationDegrees;
this.silhouetteColor = silhouetteColor;
}
/**
* Returns the effective rectangle of this entity in tile space
* @returns {Rectangle}
*/
getTileSpaceBounds() {
switch (this.rotationDegrees) {
case 0:
return new Rectangle(this.origin.x, this.origin.y, this.tileSize.x, this.tileSize.y);
case 90:
return new Rectangle(
this.origin.x - this.tileSize.y + 1,
this.origin.y,
this.tileSize.y,
this.tileSize.x
);
case 180:
return new Rectangle(
this.origin.x - this.tileSize.x + 1,
this.origin.y - this.tileSize.y + 1,
this.tileSize.x,
this.tileSize.y
);
case 270:
return new Rectangle(
this.origin.x,
this.origin.y - this.tileSize.x + 1,
this.tileSize.y,
this.tileSize.x
);
default:
assert(false, "Invalid rotation");
}
}
/**
* Transforms the given vector/rotation from local space to world space
* @param {Vector} vector
* @returns {Vector}
*/
applyRotationToVector(vector) {
return vector.rotateFastMultipleOf90(this.rotationDegrees);
}
/**
* Transforms the given vector/rotation from world space to local space
* @param {Vector} vector
* @returns {Vector}
*/
unapplyRotationToVector(vector) {
return vector.rotateFastMultipleOf90(360 - this.rotationDegrees);
}
/**
* Transforms the given direction from local space
* @param {enumDirection} direction
* @returns {enumDirection}
*/
localDirectionToWorld(direction) {
return Vector.transformDirectionFromMultipleOf90(direction, this.rotationDegrees);
}
/**
* Transforms the given direction from world to local space
* @param {enumDirection} direction
* @returns {enumDirection}
*/
worldDirectionToLocal(direction) {
return Vector.transformDirectionFromMultipleOf90(direction, 360 - this.rotationDegrees);
}
/**
* Transforms from local tile space to global tile space
* @param {Vector} localTile
* @returns {Vector}
*/
localTileToWorld(localTile) {
const result = this.applyRotationToVector(localTile);
result.addInplace(this.origin);
return result;
}
/**
* Transforms from world space to local space
* @param {Vector} worldTile
*/
worldToLocalTile(worldTile) {
const localUnrotated = worldTile.sub(this.origin);
return this.unapplyRotationToVector(localUnrotated);
}
/**
* Draws a sprite over the whole space of the entity
* @param {DrawParameters} parameters
* @param {AtlasSprite} sprite
* @param {number=} extrudePixels How many pixels to extrude the sprite
* @param {boolean=} clipping Whether to clip
*/
drawSpriteOnFullEntityBounds(parameters, sprite, extrudePixels = 0, clipping = true) {
const worldX = this.origin.x * globalConfig.tileSize;
const worldY = this.origin.y * globalConfig.tileSize;
if (this.rotationDegrees === 0) {
// Early out, is faster
sprite.drawCached(
parameters,
worldX - extrudePixels * this.tileSize.x,
worldY - extrudePixels * this.tileSize.y,
globalConfig.tileSize * this.tileSize.x + 2 * extrudePixels * this.tileSize.x,
globalConfig.tileSize * this.tileSize.y + 2 * extrudePixels * this.tileSize.y,
clipping
);
} else {
const rotationCenterX = worldX + globalConfig.halfTileSize;
const rotationCenterY = worldY + globalConfig.halfTileSize;
parameters.context.translate(rotationCenterX, rotationCenterY);
parameters.context.rotate(Math_radians(this.rotationDegrees));
sprite.drawCached(
parameters,
-globalConfig.halfTileSize - extrudePixels * this.tileSize.x,
-globalConfig.halfTileSize - extrudePixels * this.tileSize.y,
globalConfig.tileSize * this.tileSize.x + 2 * extrudePixels * this.tileSize.x,
globalConfig.tileSize * this.tileSize.y + 2 * extrudePixels * this.tileSize.y,
false
);
parameters.context.rotate(-Math_radians(this.rotationDegrees));
parameters.context.translate(-rotationCenterX, -rotationCenterY);
}
}
}

View File

@@ -0,0 +1,88 @@
import { BaseItem } from "../base_item";
import { Component } from "../component";
import { globalConfig } from "../../core/config";
/** @enum {string} */
export const enumUndergroundBeltMode = {
sender: "sender",
receiver: "receiver",
};
export class UndergroundBeltComponent extends Component {
static getId() {
return "UndergroundBelt";
}
/**
*
* @param {object} param0
* @param {enumUndergroundBeltMode=} param0.mode As which type of belt the entity acts
*/
constructor({ mode = enumUndergroundBeltMode.sender }) {
super();
this.mode = mode;
/**
* Used on both receiver and sender.
* Reciever: Used to store the next item to transfer, and to block input while doing this
* Sender: Used to store which items are currently "travelling"
* @type {Array<[BaseItem, number]>} Format is [Item, remaining seconds until transfer/ejection]
*/
this.pendingItems = [];
}
/**
* Tries to accept an item from an external source like a regular belt or building
* @param {BaseItem} item
* @param {number} beltSpeed How fast this item travels
*/
tryAcceptExternalItem(item, beltSpeed) {
if (this.mode !== enumUndergroundBeltMode.sender) {
// Only senders accept external items
return false;
}
if (this.pendingItems.length > 0) {
// We currently have a pending item
return false;
}
console.log("Takes", 1 / beltSpeed);
this.pendingItems.push([item, 1 / beltSpeed]);
return true;
}
/**
* Tries to accept a tunneled item
* @param {BaseItem} item
* @param {number} travelDistance How many tiles this item has to travel
* @param {number} beltSpeed How fast this item travels
*/
tryAcceptTunneledItem(item, travelDistance, beltSpeed) {
if (this.mode !== enumUndergroundBeltMode.receiver) {
// Only receivers can accept tunneled items
return false;
}
// Notice: We assume that for all items the travel distance is the same
const maxItemsInTunnel = (1 + travelDistance) / globalConfig.itemSpacingOnBelts;
if (this.pendingItems.length >= maxItemsInTunnel) {
// Simulate a real belt which gets full at some point
return false;
}
// NOTICE:
// This corresponds to the item ejector - it needs 0.5 additional tiles to eject the item.
// So instead of adding 1 we add 0.5 only.
const travelDuration = (travelDistance + 0.5) / beltSpeed;
console.log(travelDistance, "->", travelDuration);
this.pendingItems.push([item, travelDuration]);
// Sort so we can only look at the first ones
this.pendingItems.sort((a, b) => a[1] - b[1]);
return true;
}
}

View File

@@ -0,0 +1,7 @@
import { Component } from "../component";
export class UnremovableComponent extends Component {
static getId() {
return "Unremovable";
}
}