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

Rework quad painter and filter, rework logic gates

This commit is contained in:
tobspr
2020-08-29 09:35:14 +02:00
parent 674d8a139e
commit bb739c80fa
24 changed files with 3016 additions and 2857 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -1,86 +1,91 @@
import { enumDirection, Vector } from "../../core/vector";
import { enumPinSlotType, WiredPinsComponent } from "../components/wired_pins";
import { Entity } from "../entity";
import { MetaBuilding } from "../meta_building";
import { GameRoot } from "../root";
import { LeverComponent } from "../components/lever";
import { ItemAcceptorComponent } from "../components/item_acceptor";
import { ItemEjectorComponent } from "../components/item_ejector";
import { ItemProcessorComponent, enumItemProcessorTypes } from "../components/item_processor";
export class MetaFilterBuilding extends MetaBuilding {
constructor() {
super("filter");
}
getSilhouetteColor() {
return "#c45c2e";
}
/**
* @param {GameRoot} root
*/
getIsUnlocked(root) {
// @todo
return true;
}
getDimensions() {
return new Vector(2, 1);
}
getShowWiresLayerPreview() {
return true;
}
/**
* Creates the entity at the given location
* @param {Entity} entity
*/
setupEntityComponents(entity) {
entity.addComponent(
new WiredPinsComponent({
slots: [
{
pos: new Vector(0, 0),
direction: enumDirection.left,
type: enumPinSlotType.logicalAcceptor,
},
],
})
);
entity.addComponent(
new ItemAcceptorComponent({
slots: [
{
pos: new Vector(0, 0),
directions: [enumDirection.bottom],
},
],
})
);
entity.addComponent(
new ItemEjectorComponent({
slots: [
{
pos: new Vector(0, 0),
direction: enumDirection.top,
},
{
pos: new Vector(1, 0),
direction: enumDirection.right,
},
],
})
);
entity.addComponent(
new ItemProcessorComponent({
processorType: enumItemProcessorTypes.filter,
inputsPerCharge: 1,
})
);
}
}
import { enumDirection, Vector } from "../../core/vector";
import { enumPinSlotType, WiredPinsComponent } from "../components/wired_pins";
import { Entity } from "../entity";
import { MetaBuilding } from "../meta_building";
import { GameRoot } from "../root";
import { LeverComponent } from "../components/lever";
import { ItemAcceptorComponent } from "../components/item_acceptor";
import { ItemEjectorComponent } from "../components/item_ejector";
import {
ItemProcessorComponent,
enumItemProcessorTypes,
enumItemProcessorRequirements,
} from "../components/item_processor";
export class MetaFilterBuilding extends MetaBuilding {
constructor() {
super("filter");
}
getSilhouetteColor() {
return "#c45c2e";
}
/**
* @param {GameRoot} root
*/
getIsUnlocked(root) {
// @todo
return true;
}
getDimensions() {
return new Vector(2, 1);
}
getShowWiresLayerPreview() {
return true;
}
/**
* Creates the entity at the given location
* @param {Entity} entity
*/
setupEntityComponents(entity) {
entity.addComponent(
new WiredPinsComponent({
slots: [
{
pos: new Vector(0, 0),
direction: enumDirection.left,
type: enumPinSlotType.logicalAcceptor,
},
],
})
);
entity.addComponent(
new ItemAcceptorComponent({
slots: [
{
pos: new Vector(0, 0),
directions: [enumDirection.bottom],
},
],
})
);
entity.addComponent(
new ItemEjectorComponent({
slots: [
{
pos: new Vector(0, 0),
direction: enumDirection.top,
},
{
pos: new Vector(1, 0),
direction: enumDirection.right,
},
],
})
);
entity.addComponent(
new ItemProcessorComponent({
processorType: enumItemProcessorTypes.filter,
inputsPerCharge: 1,
processingRequirement: enumItemProcessorRequirements.filter,
})
);
}
}

View File

@@ -3,7 +3,11 @@ import { enumDirection, Vector } from "../../core/vector";
import { T } from "../../translations";
import { ItemAcceptorComponent } from "../components/item_acceptor";
import { ItemEjectorComponent } from "../components/item_ejector";
import { enumItemProcessorTypes, ItemProcessorComponent, enumItemProcessorRequirements } from "../components/item_processor";
import {
enumItemProcessorTypes,
ItemProcessorComponent,
enumItemProcessorRequirements,
} from "../components/item_processor";
import { Entity } from "../entity";
import { defaultBuildingVariant, MetaBuilding } from "../meta_building";
import { GameRoot } from "../root";
@@ -120,8 +124,10 @@ export class MetaPainterBuilding extends MetaBuilding {
switch (variant) {
case defaultBuildingVariant:
case enumPainterVariants.mirrored: {
// REGULAR PAINTER
if (entity.components.WiredPins) {
entity.removeComponent(WiredPinsComponent)
entity.removeComponent(WiredPinsComponent);
}
entity.components.ItemAcceptor.setSlots([
@@ -139,18 +145,22 @@ export class MetaPainterBuilding extends MetaBuilding {
},
]);
entity.components.ItemEjector.setSlots([
{ pos: new Vector(1, 0), direction: enumDirection.right },
]);
entity.components.ItemProcessor.type = enumItemProcessorTypes.painter;
entity.components.ItemProcessor.processingRequirement = null;
entity.components.ItemProcessor.inputsPerCharge = 2;
entity.components.ItemEjector.setSlots([
{ pos: new Vector(1, 0), direction: enumDirection.right },
]);
break;
}
case enumPainterVariants.double: {
// DOUBLE PAINTER
if (entity.components.WiredPins) {
entity.removeComponent(WiredPinsComponent)
entity.removeComponent(WiredPinsComponent);
}
entity.components.ItemAcceptor.setSlots([
@@ -171,43 +181,46 @@ export class MetaPainterBuilding extends MetaBuilding {
},
]);
entity.components.ItemProcessor.type = enumItemProcessorTypes.painterDouble;
entity.components.ItemProcessor.processingRequirement = null;
entity.components.ItemProcessor.inputsPerCharge = 3;
entity.components.ItemEjector.setSlots([
{ pos: new Vector(1, 0), direction: enumDirection.right },
]);
entity.components.ItemProcessor.type = enumItemProcessorTypes.painterDouble;
entity.components.ItemProcessor.processingRequirement = null;
entity.components.ItemProcessor.inputsPerCharge = 3;
break;
}
case enumPainterVariants.quad: {
// QUAD PAINTER
if (!entity.components.WiredPins) {
entity.addComponent(new WiredPinsComponent({
slots: [
{
pos: new Vector(0, 0),
direction: enumDirection.bottom,
type: enumPinSlotType.logicalAcceptor
},
{
pos: new Vector(1, 0),
direction: enumDirection.bottom,
type: enumPinSlotType.logicalAcceptor
},
{
pos: new Vector(2, 0),
direction: enumDirection.bottom,
type: enumPinSlotType.logicalAcceptor
},
{
pos: new Vector(3, 0),
direction: enumDirection.bottom,
type: enumPinSlotType.logicalAcceptor
},
]
}));
entity.addComponent(new WiredPinsComponent({ slots: [] }));
}
entity.components.WiredPins.setSlots([
{
pos: new Vector(0, 0),
direction: enumDirection.bottom,
type: enumPinSlotType.logicalAcceptor,
},
{
pos: new Vector(1, 0),
direction: enumDirection.bottom,
type: enumPinSlotType.logicalAcceptor,
},
{
pos: new Vector(2, 0),
direction: enumDirection.bottom,
type: enumPinSlotType.logicalAcceptor,
},
{
pos: new Vector(3, 0),
direction: enumDirection.bottom,
type: enumPinSlotType.logicalAcceptor,
},
]);
entity.components.ItemAcceptor.setSlots([
{
pos: new Vector(0, 0),
@@ -236,15 +249,18 @@ export class MetaPainterBuilding extends MetaBuilding {
},
]);
entity.components.ItemProcessor.type = enumItemProcessorTypes.painterQuad;
entity.components.ItemProcessor.processingRequirement = enumItemProcessorRequirements.painterQuad;
entity.components.ItemProcessor.inputsPerCharge = 5;
entity.components.ItemEjector.setSlots([
{ pos: new Vector(0, 0), direction: enumDirection.top },
]);
entity.components.ItemProcessor.type = enumItemProcessorTypes.painterQuad;
entity.components.ItemProcessor.processingRequirement =
enumItemProcessorRequirements.painterQuad;
entity.components.ItemProcessor.inputsPerCharge = 5;
break;
}
default:
assertAlways(false, "Unknown painter variant: " + variant);
}

View File

@@ -43,7 +43,7 @@ export function initComponentRegistry() {
assert(
// @ts-ignore
require.context("./components", false, /.*\.js/i).keys().length ===
gComponentRegistry.getNumEntries(),
gComponentRegistry.getNumEntries(),
"Not all components are registered"
);

View File

@@ -24,7 +24,8 @@ export const enumItemProcessorTypes = {
/** @enum {string} */
export const enumItemProcessorRequirements = {
painterQuad: "painterQuad"
painterQuad: "painterQuad",
filter: "filter",
};
export class ItemProcessorComponent extends Component {
@@ -71,7 +72,7 @@ export class ItemProcessorComponent extends Component {
constructor({
processorType = enumItemProcessorTypes.splitter,
processingRequirement = null,
inputsPerCharge = 1
inputsPerCharge = 1,
}) {
super();

View File

@@ -1,165 +1,171 @@
/* typehints:start */
import { GameRoot } from "./root";
/* typehints:end */
import { createLogger } from "../core/logging";
import { BeltSystem } from "./systems/belt";
import { ItemEjectorSystem } from "./systems/item_ejector";
import { MapResourcesSystem } from "./systems/map_resources";
import { MinerSystem } from "./systems/miner";
import { ItemProcessorSystem } from "./systems/item_processor";
import { UndergroundBeltSystem } from "./systems/underground_belt";
import { HubSystem } from "./systems/hub";
import { StaticMapEntitySystem } from "./systems/static_map_entity";
import { ItemAcceptorSystem } from "./systems/item_acceptor";
import { StorageSystem } from "./systems/storage";
import { WiredPinsSystem } from "./systems/wired_pins";
import { BeltUnderlaysSystem } from "./systems/belt_underlays";
import { WireSystem } from "./systems/wire";
import { ConstantSignalSystem } from "./systems/constant_signal";
import { LogicGateSystem } from "./systems/logic_gate";
import { LeverSystem } from "./systems/lever";
import { DisplaySystem } from "./systems/display";
const logger = createLogger("game_system_manager");
export class GameSystemManager {
/**
*
* @param {GameRoot} root
*/
constructor(root) {
this.root = root;
this.systems = {
/* typehints:start */
/** @type {BeltSystem} */
belt: null,
/** @type {ItemEjectorSystem} */
itemEjector: null,
/** @type {MapResourcesSystem} */
mapResources: null,
/** @type {MinerSystem} */
miner: null,
/** @type {ItemProcessorSystem} */
itemProcessor: null,
/** @type {UndergroundBeltSystem} */
undergroundBelt: null,
/** @type {HubSystem} */
hub: null,
/** @type {StaticMapEntitySystem} */
staticMapEntities: null,
/** @type {ItemAcceptorSystem} */
itemAcceptor: null,
/** @type {StorageSystem} */
storage: null,
/** @type {WiredPinsSystem} */
wiredPins: null,
/** @type {BeltUnderlaysSystem} */
beltUnderlays: null,
/** @type {WireSystem} */
wire: null,
/** @type {ConstantSignalSystem} */
constantSignal: null,
/** @type {LogicGateSystem} */
logicGate: null,
/** @type {LeverSystem} */
lever: null,
/** @type {DisplaySystem} */
display: null,
/* typehints:end */
};
this.systemUpdateOrder = [];
this.internalInitSystems();
}
/**
* Initializes all systems
*/
internalInitSystems() {
const add = (id, systemClass) => {
this.systems[id] = new systemClass(this.root);
this.systemUpdateOrder.push(id);
};
// Order is important!
add("belt", BeltSystem);
add("undergroundBelt", UndergroundBeltSystem);
add("miner", MinerSystem);
add("storage", StorageSystem);
add("itemProcessor", ItemProcessorSystem);
add("itemEjector", ItemEjectorSystem);
add("mapResources", MapResourcesSystem);
add("hub", HubSystem);
add("staticMapEntities", StaticMapEntitySystem);
add("wiredPins", WiredPinsSystem);
add("beltUnderlays", BeltUnderlaysSystem);
add("constantSignal", ConstantSignalSystem);
// IMPORTANT: Must be after belt system since belt system can change the
// orientation of an entity after it is placed -> the item acceptor cache
// then would be invalid
add("itemAcceptor", ItemAcceptorSystem);
// WIRES section
add("lever", LeverSystem);
// IMPORTANT: We have 2 phases: In phase 1 we compute the output values of all gates,
// processors etc. In phase 2 we propagate it through the wires network
add("logicGate", LogicGateSystem);
// Wires must be after all gate, signal etc logic!
add("wire", WireSystem);
add("display", DisplaySystem);
logger.log("📦 There are", this.systemUpdateOrder.length, "game systems");
}
/**
* Updates all systems
*/
update() {
for (let i = 0; i < this.systemUpdateOrder.length; ++i) {
const system = this.systems[this.systemUpdateOrder[i]];
system.update();
}
}
refreshCaches() {
for (let i = 0; i < this.systemUpdateOrder.length; ++i) {
const system = this.systems[this.systemUpdateOrder[i]];
system.refreshCaches();
}
}
}
/* typehints:start */
import { GameRoot } from "./root";
/* typehints:end */
import { createLogger } from "../core/logging";
import { BeltSystem } from "./systems/belt";
import { ItemEjectorSystem } from "./systems/item_ejector";
import { MapResourcesSystem } from "./systems/map_resources";
import { MinerSystem } from "./systems/miner";
import { ItemProcessorSystem } from "./systems/item_processor";
import { UndergroundBeltSystem } from "./systems/underground_belt";
import { HubSystem } from "./systems/hub";
import { StaticMapEntitySystem } from "./systems/static_map_entity";
import { ItemAcceptorSystem } from "./systems/item_acceptor";
import { StorageSystem } from "./systems/storage";
import { WiredPinsSystem } from "./systems/wired_pins";
import { BeltUnderlaysSystem } from "./systems/belt_underlays";
import { WireSystem } from "./systems/wire";
import { ConstantSignalSystem } from "./systems/constant_signal";
import { LogicGateSystem } from "./systems/logic_gate";
import { LeverSystem } from "./systems/lever";
import { DisplaySystem } from "./systems/display";
import { ItemProcessorOverlaysSystem } from "./systems/item_processor_overlays";
const logger = createLogger("game_system_manager");
export class GameSystemManager {
/**
*
* @param {GameRoot} root
*/
constructor(root) {
this.root = root;
this.systems = {
/* typehints:start */
/** @type {BeltSystem} */
belt: null,
/** @type {ItemEjectorSystem} */
itemEjector: null,
/** @type {MapResourcesSystem} */
mapResources: null,
/** @type {MinerSystem} */
miner: null,
/** @type {ItemProcessorSystem} */
itemProcessor: null,
/** @type {UndergroundBeltSystem} */
undergroundBelt: null,
/** @type {HubSystem} */
hub: null,
/** @type {StaticMapEntitySystem} */
staticMapEntities: null,
/** @type {ItemAcceptorSystem} */
itemAcceptor: null,
/** @type {StorageSystem} */
storage: null,
/** @type {WiredPinsSystem} */
wiredPins: null,
/** @type {BeltUnderlaysSystem} */
beltUnderlays: null,
/** @type {WireSystem} */
wire: null,
/** @type {ConstantSignalSystem} */
constantSignal: null,
/** @type {LogicGateSystem} */
logicGate: null,
/** @type {LeverSystem} */
lever: null,
/** @type {DisplaySystem} */
display: null,
/** @type {ItemProcessorOverlaysSystem} */
itemProcessorOverlays: null,
/* typehints:end */
};
this.systemUpdateOrder = [];
this.internalInitSystems();
}
/**
* Initializes all systems
*/
internalInitSystems() {
const add = (id, systemClass) => {
this.systems[id] = new systemClass(this.root);
this.systemUpdateOrder.push(id);
};
// Order is important!
add("belt", BeltSystem);
add("undergroundBelt", UndergroundBeltSystem);
add("miner", MinerSystem);
add("storage", StorageSystem);
add("itemProcessor", ItemProcessorSystem);
add("itemEjector", ItemEjectorSystem);
add("mapResources", MapResourcesSystem);
add("hub", HubSystem);
add("staticMapEntities", StaticMapEntitySystem);
add("wiredPins", WiredPinsSystem);
add("beltUnderlays", BeltUnderlaysSystem);
add("constantSignal", ConstantSignalSystem);
// IMPORTANT: Must be after belt system since belt system can change the
// orientation of an entity after it is placed -> the item acceptor cache
// then would be invalid
add("itemAcceptor", ItemAcceptorSystem);
// WIRES section
add("lever", LeverSystem);
// IMPORTANT: We have 2 phases: In phase 1 we compute the output values of all gates,
// processors etc. In phase 2 we propagate it through the wires network
add("logicGate", LogicGateSystem);
// Wires must be after all gate, signal etc logic!
add("wire", WireSystem);
add("display", DisplaySystem);
add("itemProcessorOverlays", ItemProcessorOverlaysSystem);
logger.log("📦 There are", this.systemUpdateOrder.length, "game systems");
}
/**
* Updates all systems
*/
update() {
for (let i = 0; i < this.systemUpdateOrder.length; ++i) {
const system = this.systems[this.systemUpdateOrder[i]];
system.update();
}
}
refreshCaches() {
for (let i = 0; i < this.systemUpdateOrder.length; ++i) {
const system = this.systems[this.systemUpdateOrder[i]];
system.refreshCaches();
}
}
}

View File

@@ -1,62 +1,70 @@
import { DrawParameters } from "../../core/draw_parameters";
import { Loader } from "../../core/loader";
import { types } from "../../savegame/serialization";
import { BaseItem } from "../base_item";
import { globalConfig } from "../../core/config";
export class BooleanItem extends BaseItem {
static getId() {
return "boolean_item";
}
static getSchema() {
return types.uint;
}
serialize() {
return this.value;
}
deserialize(data) {
this.value = data;
}
/** @returns {"boolean"} **/
getItemType() {
return "boolean";
}
/**
* @param {number} value
*/
constructor(value) {
super();
this.value = value ? 1 : 0;
}
/**
* @param {BaseItem} other
*/
equalsImpl(other) {
return this.value === /** @type {BooleanItem} */ (other).value;
}
/**
* @param {number} x
* @param {number} y
* @param {number} diameter
* @param {DrawParameters} parameters
*/
drawItemCenteredImpl(x, y, parameters, diameter = globalConfig.defaultItemDiameter) {
let sprite;
if (this.value) {
sprite = Loader.getSprite("sprites/wires/boolean_true.png");
} else {
sprite = Loader.getSprite("sprites/wires/boolean_false.png");
}
sprite.drawCachedCentered(parameters, x, y, diameter);
}
}
export const BOOL_FALSE_SINGLETON = new BooleanItem(0);
export const BOOL_TRUE_SINGLETON = new BooleanItem(1);
import { DrawParameters } from "../../core/draw_parameters";
import { Loader } from "../../core/loader";
import { types } from "../../savegame/serialization";
import { BaseItem } from "../base_item";
import { globalConfig } from "../../core/config";
export class BooleanItem extends BaseItem {
static getId() {
return "boolean_item";
}
static getSchema() {
return types.uint;
}
serialize() {
return this.value;
}
deserialize(data) {
this.value = data;
}
/** @returns {"boolean"} **/
getItemType() {
return "boolean";
}
/**
* @param {number} value
*/
constructor(value) {
super();
this.value = value ? 1 : 0;
}
/**
* @param {BaseItem} other
*/
equalsImpl(other) {
return this.value === /** @type {BooleanItem} */ (other).value;
}
/**
* @param {number} x
* @param {number} y
* @param {number} diameter
* @param {DrawParameters} parameters
*/
drawItemCenteredImpl(x, y, parameters, diameter = globalConfig.defaultItemDiameter) {
let sprite;
if (this.value) {
sprite = Loader.getSprite("sprites/wires/boolean_true.png");
} else {
sprite = Loader.getSprite("sprites/wires/boolean_false.png");
}
sprite.drawCachedCentered(parameters, x, y, diameter);
}
}
export const BOOL_FALSE_SINGLETON = new BooleanItem(0);
export const BOOL_TRUE_SINGLETON = new BooleanItem(1);
/**
*
* @param {BaseItem} item
*/
export function isTrueItem(item) {
return item && item.getItemType() === "boolean" && /** @type {BooleanItem} */ (item).value;
}

View File

@@ -69,6 +69,7 @@ export class MapChunkView extends MapChunk {
systems.lever.drawChunk(parameters, this);
systems.display.drawChunk(parameters, this);
systems.storage.drawChunk(parameters, this);
systems.itemProcessorOverlays.drawChunk(parameters, this);
}
/**

View File

@@ -1,99 +1,97 @@
import { globalConfig } from "../../core/config";
import { Loader } from "../../core/loader";
import { BaseItem } from "../base_item";
import { enumColors } from "../colors";
import { DisplayComponent } from "../components/display";
import { GameSystemWithFilter } from "../game_system_with_filter";
import { ColorItem, COLOR_ITEM_SINGLETONS } from "../items/color_item";
import { MapChunkView } from "../map_chunk_view";
import { BooleanItem } from "../items/boolean_item";
export class DisplaySystem extends GameSystemWithFilter {
constructor(root) {
super(root, [DisplayComponent]);
/** @type {Object<string, import("../../core/draw_utils").AtlasSprite>} */
this.displaySprites = {};
for (const colorId in enumColors) {
if (colorId === enumColors.uncolored) {
continue;
}
this.displaySprites[colorId] = Loader.getSprite("sprites/wires/display/" + colorId + ".png");
}
}
/**
* Returns the color / value a display should show
* @param {BaseItem} value
* @returns {BaseItem}
*/
getDisplayItem(value) {
if (!value) {
return null;
}
switch (value.getItemType()) {
case "boolean": {
return /** @type {BooleanItem} */ (value).value
? COLOR_ITEM_SINGLETONS[enumColors.white]
: null;
}
case "color": {
const item = /**@type {ColorItem} */ (value);
return item.color === enumColors.uncolored ? null : item;
}
case "shape": {
return value;
}
default:
assertAlways(false, "Unknown item type: " + value.getItemType());
}
}
/**
* Draws a given chunk
* @param {import("../../core/draw_utils").DrawParameters} parameters
* @param {MapChunkView} chunk
*/
drawChunk(parameters, chunk) {
const contents = chunk.containedEntitiesByLayer.regular;
for (let i = 0; i < contents.length; ++i) {
const entity = contents[i];
if (entity && entity.components.Display) {
const pinsComp = entity.components.WiredPins;
const network = pinsComp.slots[0].linkedNetwork;
if (!network || !network.currentValue) {
continue;
}
const value = this.getDisplayItem(network.currentValue);
if (!value) {
continue;
}
const origin = entity.components.StaticMapEntity.origin;
if (value.getItemType() === "color") {
this.displaySprites[/** @type {ColorItem} */ (value).color].drawCachedCentered(
parameters,
(origin.x + 0.5) * globalConfig.tileSize,
(origin.y + 0.5) * globalConfig.tileSize,
globalConfig.tileSize
);
} else if (value.getItemType() === "shape") {
value.drawItemCenteredClipped(
(origin.x + 0.5) * globalConfig.tileSize,
(origin.y + 0.5) * globalConfig.tileSize,
parameters,
30
);
}
}
}
}
}
import { globalConfig } from "../../core/config";
import { Loader } from "../../core/loader";
import { BaseItem } from "../base_item";
import { enumColors } from "../colors";
import { DisplayComponent } from "../components/display";
import { GameSystemWithFilter } from "../game_system_with_filter";
import { isTrueItem } from "../items/boolean_item";
import { ColorItem, COLOR_ITEM_SINGLETONS } from "../items/color_item";
import { MapChunkView } from "../map_chunk_view";
export class DisplaySystem extends GameSystemWithFilter {
constructor(root) {
super(root, [DisplayComponent]);
/** @type {Object<string, import("../../core/draw_utils").AtlasSprite>} */
this.displaySprites = {};
for (const colorId in enumColors) {
if (colorId === enumColors.uncolored) {
continue;
}
this.displaySprites[colorId] = Loader.getSprite("sprites/wires/display/" + colorId + ".png");
}
}
/**
* Returns the color / value a display should show
* @param {BaseItem} value
* @returns {BaseItem}
*/
getDisplayItem(value) {
if (!value) {
return null;
}
switch (value.getItemType()) {
case "boolean": {
return isTrueItem(value) ? COLOR_ITEM_SINGLETONS[enumColors.white] : null;
}
case "color": {
const item = /**@type {ColorItem} */ (value);
return item.color === enumColors.uncolored ? null : item;
}
case "shape": {
return value;
}
default:
assertAlways(false, "Unknown item type: " + value.getItemType());
}
}
/**
* Draws a given chunk
* @param {import("../../core/draw_utils").DrawParameters} parameters
* @param {MapChunkView} chunk
*/
drawChunk(parameters, chunk) {
const contents = chunk.containedEntitiesByLayer.regular;
for (let i = 0; i < contents.length; ++i) {
const entity = contents[i];
if (entity && entity.components.Display) {
const pinsComp = entity.components.WiredPins;
const network = pinsComp.slots[0].linkedNetwork;
if (!network || !network.currentValue) {
continue;
}
const value = this.getDisplayItem(network.currentValue);
if (!value) {
continue;
}
const origin = entity.components.StaticMapEntity.origin;
if (value.getItemType() === "color") {
this.displaySprites[/** @type {ColorItem} */ (value).color].drawCachedCentered(
parameters,
(origin.x + 0.5) * globalConfig.tileSize,
(origin.y + 0.5) * globalConfig.tileSize,
globalConfig.tileSize
);
} else if (value.getItemType() === "shape") {
value.drawItemCenteredClipped(
(origin.x + 0.5) * globalConfig.tileSize,
(origin.y + 0.5) * globalConfig.tileSize,
parameters,
30
);
}
}
}
}
}

View File

@@ -1,388 +1,381 @@
import { globalConfig } from "../../core/config";
import { DrawParameters } from "../../core/draw_parameters";
import { createLogger } from "../../core/logging";
import { Rectangle } from "../../core/rectangle";
import { enumDirection, enumDirectionToVector, Vector } from "../../core/vector";
import { BaseItem } from "../base_item";
import { ItemEjectorComponent } from "../components/item_ejector";
import { Entity } from "../entity";
import { GameSystemWithFilter } from "../game_system_with_filter";
import { enumItemProcessorTypes } from "../components/item_processor";
import { MapChunkView } from "../map_chunk_view";
const logger = createLogger("systems/ejector");
export class ItemEjectorSystem extends GameSystemWithFilter {
constructor(root) {
super(root, [ItemEjectorComponent]);
this.root.signals.entityAdded.add(this.checkForCacheInvalidation, this);
this.root.signals.entityDestroyed.add(this.checkForCacheInvalidation, this);
this.root.signals.postLoadHook.add(this.recomputeCache, this);
/**
* @type {Rectangle}
*/
this.areaToRecompute = null;
}
/**
*
* @param {Entity} entity
*/
checkForCacheInvalidation(entity) {
if (!this.root.gameInitialized) {
return;
}
if (!entity.components.StaticMapEntity) {
return;
}
// Optimize for the common case: adding or removing one building at a time. Clicking
// and dragging can cause up to 4 add/remove signals.
const staticComp = entity.components.StaticMapEntity;
const bounds = staticComp.getTileSpaceBounds();
const expandedBounds = bounds.expandedInAllDirections(2);
if (this.areaToRecompute) {
this.areaToRecompute = this.areaToRecompute.getUnion(expandedBounds);
} else {
this.areaToRecompute = expandedBounds;
}
}
/**
* Precomputes the cache, which makes up for a huge performance improvement
*/
recomputeCache() {
if (this.areaToRecompute) {
logger.log("Recomputing cache using rectangle");
if (G_IS_DEV && globalConfig.debug.renderChanges) {
this.root.hud.parts.changesDebugger.renderChange(
"ejector-area",
this.areaToRecompute,
"#fe50a6"
);
}
this.recomputeAreaCache();
this.areaToRecompute = null;
} else {
logger.log("Full cache recompute");
if (G_IS_DEV && globalConfig.debug.renderChanges) {
this.root.hud.parts.changesDebugger.renderChange(
"ejector-full",
new Rectangle(-1000, -1000, 2000, 2000),
"#fe50a6"
);
}
// Try to find acceptors for every ejector
for (let i = 0; i < this.allEntities.length; ++i) {
const entity = this.allEntities[i];
this.recomputeSingleEntityCache(entity);
}
}
}
/**
* Recomputes the cache in the given area
*/
recomputeAreaCache() {
const area = this.areaToRecompute;
let entryCount = 0;
logger.log("Recomputing area:", area.x, area.y, "/", area.w, area.h);
// Store the entities we already recomputed, so we don't do work twice
const recomputedEntities = new Set();
for (let x = area.x; x < area.right(); ++x) {
for (let y = area.y; y < area.bottom(); ++y) {
const entities = this.root.map.getLayersContentsMultipleXY(x, y);
for (let i = 0; i < entities.length; ++i) {
const entity = entities[i];
// Recompute the entity in case its relevant for this system and it
// hasn't already been computed
if (!recomputedEntities.has(entity.uid) && entity.components.ItemEjector) {
recomputedEntities.add(entity.uid);
this.recomputeSingleEntityCache(entity);
}
}
}
}
return entryCount;
}
/**
* @param {Entity} entity
*/
recomputeSingleEntityCache(entity) {
const ejectorComp = entity.components.ItemEjector;
const staticComp = entity.components.StaticMapEntity;
for (let slotIndex = 0; slotIndex < ejectorComp.slots.length; ++slotIndex) {
const ejectorSlot = ejectorComp.slots[slotIndex];
// Clear the old cache.
ejectorSlot.cachedDestSlot = null;
ejectorSlot.cachedTargetEntity = null;
ejectorSlot.cachedBeltPath = null;
// Figure out where and into which direction we eject items
const ejectSlotWsTile = staticComp.localTileToWorld(ejectorSlot.pos);
const ejectSlotWsDirection = staticComp.localDirectionToWorld(ejectorSlot.direction);
const ejectSlotWsDirectionVector = enumDirectionToVector[ejectSlotWsDirection];
const ejectSlotTargetWsTile = ejectSlotWsTile.add(ejectSlotWsDirectionVector);
// Try to find the given acceptor component to take the item
// Since there can be cross layer dependencies, check on all layers
const targetEntities = this.root.map.getLayersContentsMultipleXY(
ejectSlotTargetWsTile.x,
ejectSlotTargetWsTile.y
);
for (let i = 0; i < targetEntities.length; ++i) {
const targetEntity = targetEntities[i];
const targetStaticComp = targetEntity.components.StaticMapEntity;
const targetBeltComp = targetEntity.components.Belt;
// Check for belts (special case)
if (targetBeltComp) {
const beltAcceptingDirection = targetStaticComp.localDirectionToWorld(enumDirection.top);
if (ejectSlotWsDirection === beltAcceptingDirection) {
ejectorSlot.cachedTargetEntity = targetEntity;
ejectorSlot.cachedBeltPath = targetBeltComp.assignedPath;
break;
}
}
// Check for item acceptors
const targetAcceptorComp = targetEntity.components.ItemAcceptor;
if (!targetAcceptorComp) {
// Entity doesn't accept items
continue;
}
const matchingSlot = targetAcceptorComp.findMatchingSlot(
targetStaticComp.worldToLocalTile(ejectSlotTargetWsTile),
targetStaticComp.worldDirectionToLocal(ejectSlotWsDirection)
);
if (!matchingSlot) {
// No matching slot found
continue;
}
// A slot can always be connected to one other slot only
ejectorSlot.cachedTargetEntity = targetEntity;
ejectorSlot.cachedDestSlot = matchingSlot;
break;
}
}
}
update() {
if (this.areaToRecompute) {
this.recomputeCache();
}
// Precompute effective belt speed
let progressGrowth = 2 * this.root.dynamicTickrate.deltaSeconds;
if (G_IS_DEV && globalConfig.debug.instantBelts) {
progressGrowth = 1;
}
// Go over all cache entries
for (let i = 0; i < this.allEntities.length; ++i) {
const sourceEntity = this.allEntities[i];
const sourceEjectorComp = sourceEntity.components.ItemEjector;
if (!sourceEjectorComp.enabled) {
continue;
}
const slots = sourceEjectorComp.slots;
for (let j = 0; j < slots.length; ++j) {
const sourceSlot = slots[j];
const item = sourceSlot.item;
if (!item) {
// No item available to be ejected
continue;
}
const targetEntity = sourceSlot.cachedTargetEntity;
// Advance items on the slot
sourceSlot.progress = Math.min(
1,
sourceSlot.progress +
progressGrowth *
this.root.hubGoals.getBeltBaseSpeed() *
globalConfig.itemSpacingOnBelts
);
// Check if we are still in the process of ejecting, can't proceed then
if (sourceSlot.progress < 1.0) {
continue;
}
// Check if we are ejecting to a belt path
const destPath = sourceSlot.cachedBeltPath;
if (destPath) {
// Try passing the item over
if (destPath.tryAcceptItem(item)) {
sourceSlot.item = null;
}
// Always stop here, since there can *either* be a belt path *or*
// a slot
continue;
}
// Check if the target acceptor can actually accept this item
const destSlot = sourceSlot.cachedDestSlot;
if (destSlot) {
const targetAcceptorComp = targetEntity.components.ItemAcceptor;
if (!targetAcceptorComp.canAcceptItem(destSlot.index, item)) {
continue;
}
// Try to hand over the item
if (this.tryPassOverItem(item, targetEntity, destSlot.index)) {
// Handover successful, clear slot
targetAcceptorComp.onItemAccepted(destSlot.index, destSlot.acceptedDirection, item);
sourceSlot.item = null;
continue;
}
}
}
}
}
/**
*
* @param {BaseItem} item
* @param {Entity} receiver
* @param {number} slotIndex
*/
tryPassOverItem(item, receiver, slotIndex) {
// Try figuring out how what to do with the item
// TODO: Kinda hacky. How to solve this properly? Don't want to go through inheritance hell.
// Also its just a few cases (hope it stays like this .. :x).
const beltComp = receiver.components.Belt;
if (beltComp) {
const path = beltComp.assignedPath;
assert(path, "belt has no path");
if (path.tryAcceptItem(item)) {
return true;
}
// Belt can have nothing else
return false;
}
const itemProcessorComp = receiver.components.ItemProcessor;
if (itemProcessorComp) {
// @todo HACK
// Check if there are pins, and if so if they are connected
if (itemProcessorComp.type === enumItemProcessorTypes.filter) {
const pinsComp = receiver.components.WiredPins;
if (pinsComp && pinsComp.slots.length === 1) {
const network = pinsComp.slots[0].linkedNetwork;
if (!network || !network.currentValue) {
return false;
}
}
}
// Its an item processor ..
if (itemProcessorComp.tryTakeItem(item, slotIndex)) {
return true;
}
// Item processor can have nothing else
return false;
}
const undergroundBeltComp = receiver.components.UndergroundBelt;
if (undergroundBeltComp) {
// Its an underground belt. yay.
if (
undergroundBeltComp.tryAcceptExternalItem(
item,
this.root.hubGoals.getUndergroundBeltBaseSpeed()
)
) {
return true;
}
// Underground belt can have nothing else
return false;
}
const storageComp = receiver.components.Storage;
if (storageComp) {
// It's a storage
if (storageComp.canAcceptItem(item)) {
storageComp.takeItem(item);
return true;
}
// Storage can't have anything else
return false;
}
return false;
}
/**
* @param {DrawParameters} parameters
* @param {MapChunkView} chunk
*/
drawChunk(parameters, chunk) {
const contents = chunk.containedEntitiesByLayer.regular;
for (let i = 0; i < contents.length; ++i) {
const entity = contents[i];
const ejectorComp = entity.components.ItemEjector;
if (!ejectorComp) {
continue;
}
const staticComp = entity.components.StaticMapEntity;
for (let i = 0; i < ejectorComp.slots.length; ++i) {
const slot = ejectorComp.slots[i];
const ejectedItem = slot.item;
if (!ejectedItem) {
// No item
continue;
}
const realPosition = staticComp.localTileToWorld(slot.pos);
if (!chunk.tileSpaceRectangle.containsPoint(realPosition.x, realPosition.y)) {
// Not within this chunk
continue;
}
const realDirection = staticComp.localDirectionToWorld(slot.direction);
const realDirectionVector = enumDirectionToVector[realDirection];
const tileX = realPosition.x + 0.5 + realDirectionVector.x * 0.5 * slot.progress;
const tileY = realPosition.y + 0.5 + realDirectionVector.y * 0.5 * slot.progress;
const worldX = tileX * globalConfig.tileSize;
const worldY = tileY * globalConfig.tileSize;
ejectedItem.drawItemCenteredClipped(
worldX,
worldY,
parameters,
globalConfig.defaultItemDiameter
);
}
}
}
}
import { globalConfig } from "../../core/config";
import { DrawParameters } from "../../core/draw_parameters";
import { createLogger } from "../../core/logging";
import { Rectangle } from "../../core/rectangle";
import { enumDirection, enumDirectionToVector, Vector } from "../../core/vector";
import { BaseItem } from "../base_item";
import { ItemEjectorComponent } from "../components/item_ejector";
import { Entity } from "../entity";
import { GameSystemWithFilter } from "../game_system_with_filter";
import { enumItemProcessorTypes } from "../components/item_processor";
import { MapChunkView } from "../map_chunk_view";
const logger = createLogger("systems/ejector");
export class ItemEjectorSystem extends GameSystemWithFilter {
constructor(root) {
super(root, [ItemEjectorComponent]);
this.root.signals.entityAdded.add(this.checkForCacheInvalidation, this);
this.root.signals.entityDestroyed.add(this.checkForCacheInvalidation, this);
this.root.signals.postLoadHook.add(this.recomputeCache, this);
/**
* @type {Rectangle}
*/
this.areaToRecompute = null;
}
/**
*
* @param {Entity} entity
*/
checkForCacheInvalidation(entity) {
if (!this.root.gameInitialized) {
return;
}
if (!entity.components.StaticMapEntity) {
return;
}
// Optimize for the common case: adding or removing one building at a time. Clicking
// and dragging can cause up to 4 add/remove signals.
const staticComp = entity.components.StaticMapEntity;
const bounds = staticComp.getTileSpaceBounds();
const expandedBounds = bounds.expandedInAllDirections(2);
if (this.areaToRecompute) {
this.areaToRecompute = this.areaToRecompute.getUnion(expandedBounds);
} else {
this.areaToRecompute = expandedBounds;
}
}
/**
* Precomputes the cache, which makes up for a huge performance improvement
*/
recomputeCache() {
if (this.areaToRecompute) {
logger.log("Recomputing cache using rectangle");
if (G_IS_DEV && globalConfig.debug.renderChanges) {
this.root.hud.parts.changesDebugger.renderChange(
"ejector-area",
this.areaToRecompute,
"#fe50a6"
);
}
this.recomputeAreaCache();
this.areaToRecompute = null;
} else {
logger.log("Full cache recompute");
if (G_IS_DEV && globalConfig.debug.renderChanges) {
this.root.hud.parts.changesDebugger.renderChange(
"ejector-full",
new Rectangle(-1000, -1000, 2000, 2000),
"#fe50a6"
);
}
// Try to find acceptors for every ejector
for (let i = 0; i < this.allEntities.length; ++i) {
const entity = this.allEntities[i];
this.recomputeSingleEntityCache(entity);
}
}
}
/**
* Recomputes the cache in the given area
*/
recomputeAreaCache() {
const area = this.areaToRecompute;
let entryCount = 0;
logger.log("Recomputing area:", area.x, area.y, "/", area.w, area.h);
// Store the entities we already recomputed, so we don't do work twice
const recomputedEntities = new Set();
for (let x = area.x; x < area.right(); ++x) {
for (let y = area.y; y < area.bottom(); ++y) {
const entities = this.root.map.getLayersContentsMultipleXY(x, y);
for (let i = 0; i < entities.length; ++i) {
const entity = entities[i];
// Recompute the entity in case its relevant for this system and it
// hasn't already been computed
if (!recomputedEntities.has(entity.uid) && entity.components.ItemEjector) {
recomputedEntities.add(entity.uid);
this.recomputeSingleEntityCache(entity);
}
}
}
}
return entryCount;
}
/**
* @param {Entity} entity
*/
recomputeSingleEntityCache(entity) {
const ejectorComp = entity.components.ItemEjector;
const staticComp = entity.components.StaticMapEntity;
for (let slotIndex = 0; slotIndex < ejectorComp.slots.length; ++slotIndex) {
const ejectorSlot = ejectorComp.slots[slotIndex];
// Clear the old cache.
ejectorSlot.cachedDestSlot = null;
ejectorSlot.cachedTargetEntity = null;
ejectorSlot.cachedBeltPath = null;
// Figure out where and into which direction we eject items
const ejectSlotWsTile = staticComp.localTileToWorld(ejectorSlot.pos);
const ejectSlotWsDirection = staticComp.localDirectionToWorld(ejectorSlot.direction);
const ejectSlotWsDirectionVector = enumDirectionToVector[ejectSlotWsDirection];
const ejectSlotTargetWsTile = ejectSlotWsTile.add(ejectSlotWsDirectionVector);
// Try to find the given acceptor component to take the item
// Since there can be cross layer dependencies, check on all layers
const targetEntities = this.root.map.getLayersContentsMultipleXY(
ejectSlotTargetWsTile.x,
ejectSlotTargetWsTile.y
);
for (let i = 0; i < targetEntities.length; ++i) {
const targetEntity = targetEntities[i];
const targetStaticComp = targetEntity.components.StaticMapEntity;
const targetBeltComp = targetEntity.components.Belt;
// Check for belts (special case)
if (targetBeltComp) {
const beltAcceptingDirection = targetStaticComp.localDirectionToWorld(enumDirection.top);
if (ejectSlotWsDirection === beltAcceptingDirection) {
ejectorSlot.cachedTargetEntity = targetEntity;
ejectorSlot.cachedBeltPath = targetBeltComp.assignedPath;
break;
}
}
// Check for item acceptors
const targetAcceptorComp = targetEntity.components.ItemAcceptor;
if (!targetAcceptorComp) {
// Entity doesn't accept items
continue;
}
const matchingSlot = targetAcceptorComp.findMatchingSlot(
targetStaticComp.worldToLocalTile(ejectSlotTargetWsTile),
targetStaticComp.worldDirectionToLocal(ejectSlotWsDirection)
);
if (!matchingSlot) {
// No matching slot found
continue;
}
// A slot can always be connected to one other slot only
ejectorSlot.cachedTargetEntity = targetEntity;
ejectorSlot.cachedDestSlot = matchingSlot;
break;
}
}
}
update() {
if (this.areaToRecompute) {
this.recomputeCache();
}
// Precompute effective belt speed
let progressGrowth = 2 * this.root.dynamicTickrate.deltaSeconds;
if (G_IS_DEV && globalConfig.debug.instantBelts) {
progressGrowth = 1;
}
// Go over all cache entries
for (let i = 0; i < this.allEntities.length; ++i) {
const sourceEntity = this.allEntities[i];
const sourceEjectorComp = sourceEntity.components.ItemEjector;
if (!sourceEjectorComp.enabled) {
continue;
}
const slots = sourceEjectorComp.slots;
for (let j = 0; j < slots.length; ++j) {
const sourceSlot = slots[j];
const item = sourceSlot.item;
if (!item) {
// No item available to be ejected
continue;
}
const targetEntity = sourceSlot.cachedTargetEntity;
// Advance items on the slot
sourceSlot.progress = Math.min(
1,
sourceSlot.progress +
progressGrowth *
this.root.hubGoals.getBeltBaseSpeed() *
globalConfig.itemSpacingOnBelts
);
// Check if we are still in the process of ejecting, can't proceed then
if (sourceSlot.progress < 1.0) {
continue;
}
// Check if we are ejecting to a belt path
const destPath = sourceSlot.cachedBeltPath;
if (destPath) {
// Try passing the item over
if (destPath.tryAcceptItem(item)) {
sourceSlot.item = null;
}
// Always stop here, since there can *either* be a belt path *or*
// a slot
continue;
}
// Check if the target acceptor can actually accept this item
const destSlot = sourceSlot.cachedDestSlot;
if (destSlot) {
const targetAcceptorComp = targetEntity.components.ItemAcceptor;
if (!targetAcceptorComp.canAcceptItem(destSlot.index, item)) {
continue;
}
// Try to hand over the item
if (this.tryPassOverItem(item, targetEntity, destSlot.index)) {
// Handover successful, clear slot
targetAcceptorComp.onItemAccepted(destSlot.index, destSlot.acceptedDirection, item);
sourceSlot.item = null;
continue;
}
}
}
}
}
/**
*
* @param {BaseItem} item
* @param {Entity} receiver
* @param {number} slotIndex
*/
tryPassOverItem(item, receiver, slotIndex) {
// Try figuring out how what to do with the item
// TODO: Kinda hacky. How to solve this properly? Don't want to go through inheritance hell.
// Also its just a few cases (hope it stays like this .. :x).
const beltComp = receiver.components.Belt;
if (beltComp) {
const path = beltComp.assignedPath;
assert(path, "belt has no path");
if (path.tryAcceptItem(item)) {
return true;
}
// Belt can have nothing else
return false;
}
const itemProcessorComp = receiver.components.ItemProcessor;
if (itemProcessorComp) {
// Check for potential filters
if (!this.root.systemMgr.systems.itemProcessor.checkRequirements(receiver, item, slotIndex)) {
return false;
}
// Its an item processor ..
if (itemProcessorComp.tryTakeItem(item, slotIndex)) {
return true;
}
// Item processor can have nothing else
return false;
}
const undergroundBeltComp = receiver.components.UndergroundBelt;
if (undergroundBeltComp) {
// Its an underground belt. yay.
if (
undergroundBeltComp.tryAcceptExternalItem(
item,
this.root.hubGoals.getUndergroundBeltBaseSpeed()
)
) {
return true;
}
// Underground belt can have nothing else
return false;
}
const storageComp = receiver.components.Storage;
if (storageComp) {
// It's a storage
if (storageComp.canAcceptItem(item)) {
storageComp.takeItem(item);
return true;
}
// Storage can't have anything else
return false;
}
return false;
}
/**
* @param {DrawParameters} parameters
* @param {MapChunkView} chunk
*/
drawChunk(parameters, chunk) {
const contents = chunk.containedEntitiesByLayer.regular;
for (let i = 0; i < contents.length; ++i) {
const entity = contents[i];
const ejectorComp = entity.components.ItemEjector;
if (!ejectorComp) {
continue;
}
const staticComp = entity.components.StaticMapEntity;
for (let i = 0; i < ejectorComp.slots.length; ++i) {
const slot = ejectorComp.slots[i];
const ejectedItem = slot.item;
if (!ejectedItem) {
// No item
continue;
}
const realPosition = staticComp.localTileToWorld(slot.pos);
if (!chunk.tileSpaceRectangle.containsPoint(realPosition.x, realPosition.y)) {
// Not within this chunk
continue;
}
const realDirection = staticComp.localDirectionToWorld(slot.direction);
const realDirectionVector = enumDirectionToVector[realDirection];
const tileX = realPosition.x + 0.5 + realDirectionVector.x * 0.5 * slot.progress;
const tileY = realPosition.y + 0.5 + realDirectionVector.y * 0.5 * slot.progress;
const worldX = tileX * globalConfig.tileSize;
const worldY = tileY * globalConfig.tileSize;
ejectedItem.drawItemCenteredClipped(
worldX,
worldY,
parameters,
globalConfig.defaultItemDiameter
);
}
}
}
}

View File

@@ -1,10 +1,14 @@
import { globalConfig } from "../../core/config";
import { BaseItem } from "../base_item";
import { enumColors, enumColorMixingResults } from "../colors";
import { enumItemProcessorTypes, ItemProcessorComponent, enumItemProcessorRequirements } from "../components/item_processor";
import { enumColorMixingResults, enumColors } from "../colors";
import {
enumItemProcessorRequirements,
enumItemProcessorTypes,
ItemProcessorComponent,
} from "../components/item_processor";
import { Entity } from "../entity";
import { GameSystemWithFilter } from "../game_system_with_filter";
import { BOOL_TRUE_SINGLETON, BOOL_FALSE_SINGLETON } from "../items/boolean_item";
import { BOOL_TRUE_SINGLETON, isTrueItem } from "../items/boolean_item";
import { ColorItem, COLOR_ITEM_SINGLETONS } from "../items/color_item";
import { ShapeItem } from "../items/shape_item";
@@ -68,78 +72,154 @@ export class ItemProcessorSystem extends GameSystemWithFilter {
}
}
// Check if we have an empty queue and can start a new charge
if (processorComp.itemsToEject.length === 0) {
if (entity.components.ItemProcessor.processingRequirement) {
if (this.canProcess(entity)) {
this.startNewCharge(entity);
}
} else if (processorComp.inputSlots.length >= processorComp.inputsPerCharge) {
if (this.canProcess(entity)) {
this.startNewCharge(entity);
}
}
}
}
/**
* Returns true if the entity should accept the given item on the given slot.
* This should only be called with matching items! I.e. if a color item is expected
* on the given slot, then only a color item must be passed.
* @param {Entity} entity
* @param {BaseItem} item The item to accept
* @param {number} slotIndex The slot index
* @returns {boolean}
*/
checkRequirements(entity, item, slotIndex) {
const itemProcessorComp = entity.components.ItemProcessor;
const pinsComp = entity.components.WiredPins;
switch (itemProcessorComp.processingRequirement) {
case enumItemProcessorRequirements.painterQuad: {
if (slotIndex === 0) {
// Always accept the shape
return true;
}
// Check the network value at the given slot
const network = pinsComp.slots[slotIndex - 1].linkedNetwork;
const slotIsEnabled = network && isTrueItem(network.currentValue);
if (!slotIsEnabled) {
return false;
}
return true;
}
case enumItemProcessorRequirements.filter: {
const network = pinsComp.slots[0].linkedNetwork;
if (!network || !network.currentValue) {
// Item filter is not connected
return false;
}
// Check if "false" was passed in
const item = network.currentValue;
if (item.getItemType() === "boolean") {
if (!(/** @type {BooleanItem} */ (item).value)) {
return false;
}
}
// Otherwise, all good
return true;
}
// By default, everything is accepted
default:
return true;
}
}
/**
* Checks whether it's possible to process something
* @param {Entity} entity
*/
canProcess(entity) {
switch (entity.components.ItemProcessor.processingRequirement) {
case enumItemProcessorRequirements.painterQuad: {
// For quad-painter, pins match slots
// boolean true means "disable input"
// a color means "disable if not matched"
const processorComp = entity.components.ItemProcessor;
const processorComp = entity.components.ItemProcessor;
switch (processorComp.processingRequirement) {
// DEFAULT
// By default, we can start processing once all inputs are there
case null: {
return processorComp.inputSlots.length >= processorComp.inputsPerCharge;
}
// QUAD PAINTER
// For the quad painter, it might be possible to start processing earlier
case enumItemProcessorRequirements.painterQuad: {
const pinsComp = entity.components.WiredPins;
/** @type {Object.<string, { item: BaseItem, sourceSlot: number }>} */
/** @type {Object.<number, { item: BaseItem, sourceSlot: number }>} */
const itemsBySlot = {};
for (let i = 0; i < processorComp.inputSlots.length; ++i) {
itemsBySlot[processorComp.inputSlots[i].sourceSlot] = processorComp.inputSlots[i];
}
// first slot is the shape
if (!itemsBySlot[0]) return false;
// First slot is the shape, so if it's not there we can't do anything
if (!itemsBySlot[0]) {
return false;
}
const shapeItem = /** @type {ShapeItem} */ (itemsBySlot[0].item);
const slotStatus = [];
// Here we check just basic things`
// Stop processing if anything except TRUE is
// set and there is no item.
// Check which slots are enabled
for (let i = 0; i < 4; ++i) {
const netValue = pinsComp.slots[i].linkedNetwork ?
pinsComp.slots[i].linkedNetwork.currentValue :
null;
// Extract the network value on the Nth pin
const networkValue = pinsComp.slots[i].linkedNetwork
? pinsComp.slots[i].linkedNetwork.currentValue
: null;
const currentItem = itemsBySlot[i + 1];
// If there is no "1" on that slot, don't paint there
if (!isTrueItem(networkValue)) {
slotStatus.push(false);
continue;
}
if ((netValue == null || !netValue.equals(BOOL_TRUE_SINGLETON)) && currentItem == null) {
let quadCount = 0;
slotStatus.push(true);
}
// All slots are disabled
if (!slotStatus.includes(true)) {
return false;
}
// Check if all colors of the enabled slots are there
for (let i = 0; i < slotStatus.length; ++i) {
if (slotStatus[i] && !itemsBySlot[1 + i]) {
// A slot which is enabled wasn't enabled. Make sure if there is anything on the quadrant,
// it is not possible to paint, but if there is nothing we can ignore it
for (let j = 0; j < 4; ++j) {
const layer = shapeItem.definition.layers[j];
if (layer && layer[i]) {
quadCount++;
return false;
}
}
if (quadCount > 0) {
return false;
}
}
}
return true;
}
// FILTER
// Double check with linked network
case enumItemProcessorRequirements.filter: {
const network = entity.components.WiredPins.slots[0].linkedNetwork;
if (!network || !network.currentValue) {
// Item filter is not connected
return false;
}
return processorComp.inputSlots.length >= processorComp.inputsPerCharge;
}
default:
assertAlways(
false,
"Unknown requirement for " + entity.components.ItemProcessor.processingRequirement
);
assertAlways(false, "Unknown requirement for " + processorComp.processingRequirement);
}
}
@@ -375,60 +455,22 @@ export class ItemProcessorSystem extends GameSystemWithFilter {
const shapeItem = /** @type {ShapeItem} */ (itemsBySlot[0].item);
assert(shapeItem instanceof ShapeItem, "Input for painter is not a shape");
/** @type {Array<ColorItem>} */
const colorItems = [].fill(null, 0, 4);
/** @type {Array<enumColors>} */
const colors = [null, null, null, null];
for (let i = 0; i < 4; ++i) {
if (itemsBySlot[i + 1]) {
colorItems[i] = /** @type {ColorItem} */ (itemsBySlot[i + 1].item);
assert(colorItems[i] instanceof ColorItem, "Input for painter is not a color");
colors[i] = /** @type {ColorItem} */ (itemsBySlot[i + 1].item).color;
}
}
const pinValues = entity.components.WiredPins.slots
.map(slot => slot.linkedNetwork ? slot.linkedNetwork.currentValue : BOOL_FALSE_SINGLETON);
// @todo cleanup
const colorTL = colorItems[0];
const colorTR = colorItems[1];
const colorBR = colorItems[2];
const colorBL = colorItems[3];
/** @type {Array<boolean>} */
let skipped = [];
for (let i = 0; i < 4; ++i) {
skipped[i] = pinValues[i] ? pinValues[i].equals(BOOL_TRUE_SINGLETON) : false;
}
for (let i = 0; i < 4; ++i) {
if (colorItems[i] == null) {
skipped[i] = false; // make sure we never insert null item back
} else if (pinValues[i] instanceof ColorItem) {
// if pin value is a color, skip anything except that color
// but still require any color, because it would not work on
// slow factories.
if (!colorItems[i].equals(pinValues[i])) {
skipped[i] = true;
}
}
}
const toColor = [
(!skipped[0] && colorTL) ? colorTL.color : null,
(!skipped[1] && colorTR) ? colorTR.color : null,
(!skipped[2] && colorBR) ? colorBR.color : null,
(!skipped[3] && colorBL) ? colorBL.color : null,
];
const colorizedDefinition = this.root.shapeDefinitionMgr.shapeActionPaintWith4Colors(
shapeItem.definition,
/** @type {[enumColors, enumColors, enumColors, enumColors]} */(toColor)
/** @type {[string, string, string, string]} */ (colors)
);
outItems.push({
item: this.root.shapeDefinitionMgr.getShapeItemFromDefinition(colorizedDefinition),
});
break;
}

View File

@@ -0,0 +1,101 @@
import { GameSystem } from "../game_system";
import { MapChunkView } from "../map_chunk_view";
import { enumItemProcessorRequirements } from "../components/item_processor";
import { Entity } from "../entity";
import { isTrueItem } from "../items/boolean_item";
import { globalConfig } from "../../core/config";
import { Loader } from "../../core/loader";
import { smoothPulse } from "../../core/utils";
export class ItemProcessorOverlaysSystem extends GameSystem {
constructor(root) {
super(root);
this.spriteDisabled = Loader.getSprite("sprites/misc/processor_disabled.png");
this.spriteDisconnected = Loader.getSprite("sprites/misc/processor_disconnected.png");
this.drawnUids = new Set();
this.root.signals.gameFrameStarted.add(this.clearDrawnUids, this);
}
clearDrawnUids() {
this.drawnUids.clear();
}
/**
*
* @param {import("../../core/draw_utils").DrawParameters} parameters
* @param {MapChunkView} chunk
*/
drawChunk(parameters, chunk) {
const contents = chunk.containedEntitiesByLayer.regular;
for (let i = 0; i < contents.length; ++i) {
const entity = contents[i];
const processorComp = entity.components.ItemProcessor;
if (!processorComp) {
continue;
}
const requirement = processorComp.processingRequirement;
if (!requirement) {
continue;
}
if (this.drawnUids.has(entity.uid)) {
continue;
}
this.drawnUids.add(entity.uid);
switch (requirement) {
case enumItemProcessorRequirements.painterQuad: {
this.drawConnectedSlotRequirement(parameters, entity);
break;
}
case enumItemProcessorRequirements.filter: {
this.drawConnectedSlotRequirement(parameters, entity);
break;
}
}
}
}
/**
*
* @param {import("../../core/draw_utils").DrawParameters} parameters
* @param {Entity} entity
*/
drawConnectedSlotRequirement(parameters, entity) {
const staticComp = entity.components.StaticMapEntity;
const pinsComp = entity.components.WiredPins;
let anySlotConnected = false;
// Check if any slot has a value
for (let i = 0; i < pinsComp.slots.length; ++i) {
const slot = pinsComp.slots[i];
const network = slot.linkedNetwork;
if (network && network.currentValue) {
anySlotConnected = true;
if (isTrueItem(network.currentValue)) {
// No need to draw anything
return;
}
}
}
const pulse = smoothPulse(this.root.time.now());
parameters.context.globalAlpha = 0.6 + 0.4 * pulse;
const sprite = anySlotConnected ? this.spriteDisabled : this.spriteDisconnected;
sprite.drawCachedCentered(
parameters,
(staticComp.origin.x + 0.5) * globalConfig.tileSize,
(staticComp.origin.y + 0.5) * globalConfig.tileSize,
globalConfig.tileSize * (0.7 + 0.2 * pulse)
);
parameters.context.globalAlpha = 1;
}
}

View File

@@ -3,7 +3,7 @@ import { enumColors } from "../colors";
import { enumLogicGateType, LogicGateComponent } from "../components/logic_gate";
import { enumPinSlotType } from "../components/wired_pins";
import { GameSystemWithFilter } from "../game_system_with_filter";
import { BOOL_FALSE_SINGLETON, BOOL_TRUE_SINGLETON, BooleanItem } from "../items/boolean_item";
import { BOOL_FALSE_SINGLETON, BOOL_TRUE_SINGLETON, isTrueItem } from "../items/boolean_item";
import { COLOR_ITEM_SINGLETONS } from "../items/color_item";
import { ShapeDefinition } from "../shape_definition";
import { ShapeItem } from "../items/shape_item";
@@ -76,28 +76,9 @@ export class LogicGateSystem extends GameSystemWithFilter {
*/
compute_AND(parameters) {
assert(parameters.length === 2, "bad parameter count for AND");
const param1 = parameters[0];
const param2 = parameters[1];
if (!param1 || !param2) {
// Not enough params
return BOOL_FALSE_SINGLETON;
}
const itemType = param1.getItemType();
if (itemType !== param2.getItemType()) {
// Differing type
return BOOL_FALSE_SINGLETON;
}
if (itemType === "boolean") {
return /** @type {BooleanItem} */ (param1).value && /** @type {BooleanItem} */ (param2).value
? BOOL_TRUE_SINGLETON
: BOOL_FALSE_SINGLETON;
}
return BOOL_FALSE_SINGLETON;
return isTrueItem(parameters[0]) && isTrueItem(parameters[1])
? BOOL_TRUE_SINGLETON
: BOOL_FALSE_SINGLETON;
}
/**
@@ -105,18 +86,7 @@ export class LogicGateSystem extends GameSystemWithFilter {
* @returns {BaseItem}
*/
compute_NOT(parameters) {
const item = parameters[0];
if (!item) {
return BOOL_TRUE_SINGLETON;
}
if (item.getItemType() !== "boolean") {
// Not a boolean actually
return BOOL_FALSE_SINGLETON;
}
const value = /** @type {BooleanItem} */ (item).value;
return value ? BOOL_FALSE_SINGLETON : BOOL_TRUE_SINGLETON;
return isTrueItem(parameters[0]) ? BOOL_FALSE_SINGLETON : BOOL_TRUE_SINGLETON;
}
/**
@@ -125,27 +95,9 @@ export class LogicGateSystem extends GameSystemWithFilter {
*/
compute_XOR(parameters) {
assert(parameters.length === 2, "bad parameter count for XOR");
const param1 = parameters[0];
const param2 = parameters[1];
if (!param1 && !param2) {
// Not enough params
return BOOL_FALSE_SINGLETON;
}
// Check for the right types
if (param1 && param1.getItemType() !== "boolean") {
return BOOL_FALSE_SINGLETON;
}
if (param2 && param2.getItemType() !== "boolean") {
return BOOL_FALSE_SINGLETON;
}
const valueParam1 = param1 ? /** @type {BooleanItem} */ (param1).value : 0;
const valueParam2 = param2 ? /** @type {BooleanItem} */ (param2).value : 0;
return valueParam1 ^ valueParam2 ? BOOL_TRUE_SINGLETON : BOOL_FALSE_SINGLETON;
return isTrueItem(parameters[0]) ^ isTrueItem(parameters[1])
? BOOL_TRUE_SINGLETON
: BOOL_FALSE_SINGLETON;
}
/**
@@ -154,20 +106,9 @@ export class LogicGateSystem extends GameSystemWithFilter {
*/
compute_OR(parameters) {
assert(parameters.length === 2, "bad parameter count for OR");
const param1 = parameters[0];
const param2 = parameters[1];
if (!param1 && !param2) {
// Not enough params
return BOOL_FALSE_SINGLETON;
}
const valueParam1 =
param1 && param1.getItemType() === "boolean" ? /** @type {BooleanItem} */ (param1).value : 0;
const valueParam2 =
param2 && param2.getItemType() === "boolean" ? /** @type {BooleanItem} */ (param2).value : 0;
return valueParam1 || valueParam2 ? BOOL_TRUE_SINGLETON : BOOL_FALSE_SINGLETON;
return isTrueItem(parameters[0]) || isTrueItem(parameters[1])
? BOOL_TRUE_SINGLETON
: BOOL_FALSE_SINGLETON;
}
/**
@@ -176,21 +117,11 @@ export class LogicGateSystem extends GameSystemWithFilter {
*/
compute_IF(parameters) {
assert(parameters.length === 2, "bad parameter count for IF");
const flag = parameters[0];
const value = parameters[1];
if (!flag || !value) {
// Not enough params
return null;
}
if (flag.getItemType() !== "boolean") {
// Flag is not a boolean
return null;
}
// pass through item
if (/** @type {BooleanItem} */ (flag).value) {
if (isTrueItem(flag)) {
return value;
}

File diff suppressed because it is too large Load Diff