From 27db6fe693fc764412a45c025a8d7dccafaa6d7a Mon Sep 17 00:00:00 2001 From: tobspr Date: Tue, 18 Jan 2022 10:13:25 +0100 Subject: [PATCH 01/17] Add notification blocks mod example --- mod_examples/notification_blocks.js | 312 ++++++++++++++++++ res/ui/icons/notification_error.png | Bin 0 -> 5509 bytes res/ui/icons/notification_info.png | Bin 0 -> 1899 bytes res/ui/icons/notification_warning.png | Bin 0 -> 4168 bytes src/css/resources.scss | 3 +- src/js/game/hud/hud.js | 2 + src/js/game/hud/parts/constant_signal_edit.js | 181 +++++++++- src/js/game/hud/parts/notifications.js | 9 +- src/js/game/systems/constant_signal.js | 188 +---------- src/js/mods/mod_interface.js | 12 + src/js/mods/mod_signals.js | 2 + 11 files changed, 522 insertions(+), 187 deletions(-) create mode 100644 mod_examples/notification_blocks.js create mode 100644 res/ui/icons/notification_error.png create mode 100644 res/ui/icons/notification_info.png create mode 100644 res/ui/icons/notification_warning.png diff --git a/mod_examples/notification_blocks.js b/mod_examples/notification_blocks.js new file mode 100644 index 00000000..a8116849 --- /dev/null +++ b/mod_examples/notification_blocks.js @@ -0,0 +1,312 @@ +// @ts-nocheck +const METADATA = { + website: "https://tobspr.io", + author: "tobspr", + name: "Mod Example: Notification Blocks", + version: "1", + id: "notification-blocks", + description: + "Adds a new building to the wires layer, 'Notification Blocks' which show a custom notification when they get a truthy signal.", +}; + +//////////////////////////////////////////////////////////////////////// +// This is the component storing which text the block should show as +// a notification. +class NotificationBlockComponent extends shapez.Component { + static getId() { + return "NotificationBlock"; + } + + static getSchema() { + // Here you define which properties should be saved to the savegame + // and get automatically restored + return { + notificationText: shapez.types.string, + lastStoredInput: shapez.types.bool, + }; + } + + constructor() { + super(); + this.notificationText = "Test"; + this.lastStoredInput = false; + } +} + +//////////////////////////////////////////////////////////////////////// +// The game system to trigger notifications when the signal changes +class NotificationBlocksSystem extends shapez.GameSystemWithFilter { + constructor(root) { + // By specifying the list of components, `this.allEntities` will only + // contain entities which have *all* of the specified components + super(root, [NotificationBlockComponent]); + + // Ask for a notification text once an entity is placed + this.root.signals.entityManuallyPlaced.add(entity => { + const editorHud = this.root.hud.parts.notificationBlockEdit; + if (editorHud) { + editorHud.editNotificationText(entity, { deleteOnCancel: true }); + } + }); + } + + update() { + if (!this.root.gameInitialized) { + // Do not start updating before the wires network was + // computed to avoid dispatching all notifications + return; + } + + // Go over all notification blocks and check if the signal changed + for (let i = 0; i < this.allEntities.length; ++i) { + const entity = this.allEntities[i]; + + // Compute if the bottom pin currently has a truthy input + const pinsComp = entity.components.WiredPins; + const network = pinsComp.slots[0].linkedNetwork; + + let currentInput = false; + + if (network && network.hasValue()) { + const value = network.currentValue; + if (value && shapez.isTruthyItem(value)) { + currentInput = true; + } + } + + // If the value changed, show the notification if its truthy + const notificationComp = entity.components.NotificationBlock; + if (currentInput !== notificationComp.lastStoredInput) { + notificationComp.lastStoredInput = currentInput; + if (currentInput) { + this.root.hud.signals.notification.dispatch( + notificationComp.notificationText, + shapez.enumNotificationType.info + ); + } + } + } + } +} + +//////////////////////////////////////////////////////////////////////// +// The actual notification block building +class MetaNotificationBlockBuilding extends shapez.ModMetaBuilding { + constructor() { + super("notification_block"); + } + + static getAllVariantCombinations() { + return [ + { + variant: shapez.defaultBuildingVariant, + name: "Notification Block", + description: "Shows a predefined notification on screen when receiving a truthy signal", + + regularImageBase64: RESOURCES["notification_block.png"], + blueprintImageBase64: RESOURCES["notification_block.png"], + tutorialImageBase64: RESOURCES["notification_block.png"], + }, + ]; + } + + getSilhouetteColor() { + return "#daff89"; + } + + getIsUnlocked(root) { + return root.hubGoals.isRewardUnlocked(shapez.enumHubGoalRewards.reward_wires_painter_and_levers); + } + + getLayer() { + return "wires"; + } + + getDimensions() { + return new shapez.Vector(1, 1); + } + + getRenderPins() { + // Do not show pin overlays since it would hide our building icon + return false; + } + + setupEntityComponents(entity) { + // Accept logical input from the bottom + entity.addComponent( + new shapez.WiredPinsComponent({ + slots: [ + { + pos: new shapez.Vector(0, 0), + direction: shapez.enumDirection.bottom, + type: shapez.enumPinSlotType.logicalAcceptor, + }, + ], + }) + ); + + // Add your notification component to identify the building as a notification block + entity.addComponent(new NotificationBlockComponent()); + } +} + +//////////////////////////////////////////////////////////////////////// +// HUD Component to be able to edit notification blocks by clicking them +class HUDNotificationBlockEdit extends shapez.BaseHUDPart { + initialize() { + this.root.camera.downPreHandler.add(this.downPreHandler, this); + } + + /** + * @param {Vector} pos + * @param {enumMouseButton} button + */ + downPreHandler(pos, button) { + if (this.root.currentLayer !== "wires") { + return; + } + + const tile = this.root.camera.screenToWorld(pos).toTileSpace(); + const contents = this.root.map.getLayerContentXY(tile.x, tile.y, "wires"); + if (contents) { + const notificationComp = contents.components.NotificationBlock; + if (notificationComp) { + if (button === shapez.enumMouseButton.left) { + this.editNotificationText(contents, { + deleteOnCancel: false, + }); + return shapez.STOP_PROPAGATION; + } + } + } + } + + /** + * Asks the player to enter a notification text + * @param {Entity} entity + * @param {object} param0 + * @param {boolean=} param0.deleteOnCancel + */ + editNotificationText(entity, { deleteOnCancel = true }) { + const notificationComp = entity.components.NotificationBlock; + if (!notificationComp) { + return; + } + + // save the uid because it could get stale + const uid = entity.uid; + + // create an input field to query the text + const textInput = new shapez.FormElementInput({ + id: "notificationText", + placeholder: "", + defaultValue: notificationComp.notificationText, + validator: val => val.length > 0, + }); + + // create the dialog & show it + const dialog = new shapez.DialogWithForm({ + app: this.root.app, + title: shapez.T.mods.notificationBlocks.dialogTitle, + desc: shapez.T.mods.notificationBlocks.enterNotificationText, + formElements: [textInput], + buttons: ["cancel:bad:escape", "ok:good:enter"], + closeButton: false, + }); + this.root.hud.parts.dialogs.internalShowDialog(dialog); + + // When confirmed, set the text + dialog.buttonSignals.ok.add(() => { + if (!this.root || !this.root.entityMgr) { + // Game got stopped + return; + } + + const entityRef = this.root.entityMgr.findByUid(uid, false); + if (!entityRef) { + // outdated + return; + } + + const notificationComp = entityRef.components.NotificationBlock; + if (!notificationComp) { + // no longer interesting + return; + } + + // set the text + notificationComp.notificationText = textInput.getValue(); + }); + + // When cancelled, destroy the entity again + if (deleteOnCancel) { + dialog.buttonSignals.cancel.add(() => { + if (!this.root || !this.root.entityMgr) { + // Game got stopped + return; + } + + const entityRef = this.root.entityMgr.findByUid(uid, false); + if (!entityRef) { + // outdated + return; + } + + const notificationComp = entityRef.components.NotificationBlock; + if (!notificationComp) { + // no longer interesting + return; + } + + this.root.logic.tryDeleteBuilding(entityRef); + }); + } + } +} + +//////////////////////////////////////////////////////////////////////// +// The actual mod logic +class Mod extends shapez.Mod { + init() { + // Register the component + this.modInterface.registerComponent(NotificationBlockComponent); + + // Register the new building + this.modInterface.registerNewBuilding({ + metaClass: MetaNotificationBlockBuilding, + buildingIconBase64: RESOURCES["notification_block.png"], + }); + + // Add it to the regular toolbar + this.modInterface.addNewBuildingToToolbar({ + toolbar: "wires", + location: "secondary", + metaClass: MetaNotificationBlockBuilding, + }); + + // Register our game system so we can dispatch the notifications + this.modInterface.registerGameSystem({ + id: "notificationBlocks", + systemClass: NotificationBlocksSystem, + before: "constantSignal", + }); + + // Register our hud element to be able to edit the notification texts + this.modInterface.registerHudElement("notificationBlockEdit", HUDNotificationBlockEdit); + + // This mod also supports translations + this.modInterface.registerTranslations("en", { + mods: { + notificationBlocks: { + enterNotificationText: + "Enter the notification text to show once the signal switches from 0 to 1:", + }, + }, + }); + } +} + +const RESOURCES = { + "notification_block.png": + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMAAAADACAYAAABS3GwHAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAA4VpVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDcuMS1jMDAwIDc5LmRhYmFjYmIsIDIwMjEvMDQvMTQtMDA6Mzk6NDQgICAgICAgICI+IDxyZGY6UkRGIHhtbG5zOnJkZj0iaHR0cDovL3d3dy53My5vcmcvMTk5OS8wMi8yMi1yZGYtc3ludGF4LW5zIyI+IDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiIHhtbG5zOnhtcE1NPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvbW0vIiB4bWxuczpzdFJlZj0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL3NUeXBlL1Jlc291cmNlUmVmIyIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bXBNTTpPcmlnaW5hbERvY3VtZW50SUQ9InhtcC5kaWQ6NGQwZTY5MmYtOTRlNy00MDQyLWFjY2ItNmU3OGEzMGU1N2ZjIiB4bXBNTTpEb2N1bWVudElEPSJ4bXAuZGlkOjAyQTMyQTVGNzA1RTExRUNBQ0EzQTZCNEYxQjM5MkFBIiB4bXBNTTpJbnN0YW5jZUlEPSJ4bXAuaWlkOjAyQTMyQTVFNzA1RTExRUNBQ0EzQTZCNEYxQjM5MkFBIiB4bXA6Q3JlYXRvclRvb2w9IkFkb2JlIFBob3Rvc2hvcCBDQyAyMDE5IChXaW5kb3dzKSI+IDx4bXBNTTpEZXJpdmVkRnJvbSBzdFJlZjppbnN0YW5jZUlEPSJ4bXAuaWlkOjY3NDVhMzZlLTkyMWUtNDViYS04NDFjLWVjOWZjZTc0Y2MyNSIgc3RSZWY6ZG9jdW1lbnRJRD0iYWRvYmU6ZG9jaWQ6cGhvdG9zaG9wOjk3MDYxZWYyLTY2ZGMtZjI0Zi1iZTMy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", +}; diff --git a/res/ui/icons/notification_error.png b/res/ui/icons/notification_error.png new file mode 100644 index 0000000000000000000000000000000000000000..cd9099910fa9e5f9882d15bdcefc095b18d11704 GIT binary patch literal 5509 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 UsxqbV`^TuR)@{vl^@m~q1HkPG`Tzg` literal 0 HcmV?d00001 diff --git a/res/ui/icons/notification_info.png b/res/ui/icons/notification_info.png new file mode 100644 index 0000000000000000000000000000000000000000..04afd526036b87a90b99131a1b70c1d79fab3312 GIT binary patch literal 1899 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 this.parseSignalCode(entity, val), + }); + + const items = [...Object.values(COLOR_ITEM_SINGLETONS)]; + + if (entity.components.WiredPins) { + items.unshift(BOOL_FALSE_SINGLETON, BOOL_TRUE_SINGLETON); + items.push( + this.root.shapeDefinitionMgr.getShapeItemFromShortKey( + this.root.gameMode.getBlueprintShapeKey() + ) + ); + } else { + // producer which can produce virtually anything + const shapes = ["CuCuCuCu", "RuRuRuRu", "WuWuWuWu", "SuSuSuSu"]; + items.unshift( + ...shapes.reverse().map(key => this.root.shapeDefinitionMgr.getShapeItemFromShortKey(key)) + ); + } + + if (this.root.gameMode.hasHub()) { + items.push( + this.root.shapeDefinitionMgr.getShapeItemFromDefinition( + this.root.hubGoals.currentGoal.definition + ) + ); + } + + if (this.root.hud.parts.pinnedShapes) { + items.push( + ...this.root.hud.parts.pinnedShapes.pinnedShapes.map(key => + this.root.shapeDefinitionMgr.getShapeItemFromShortKey(key) + ) + ); + } + + const itemInput = new FormElementItemChooser({ + id: "signalItem", + label: null, + items, + }); + + const dialog = new DialogWithForm({ + app: this.root.app, + title: T.dialogs.editConstantProducer.title, + desc: T.dialogs.editSignal.descItems, + formElements: [itemInput, signalValueInput], + buttons: ["cancel:bad:escape", "ok:good:enter"], + closeButton: false, + }); + this.root.hud.parts.dialogs.internalShowDialog(dialog); + + // When confirmed, set the signal + const closeHandler = () => { + if (!this.root || !this.root.entityMgr) { + // Game got stopped + return; + } + + const entityRef = this.root.entityMgr.findByUid(uid, false); + if (!entityRef) { + // outdated + return; + } + + const constantComp = entityRef.components.ConstantSignal; + if (!constantComp) { + // no longer interesting + return; + } + + if (itemInput.chosenItem) { + constantComp.signal = itemInput.chosenItem; + } else { + constantComp.signal = this.parseSignalCode(entity, signalValueInput.getValue()); + } + }; + + dialog.buttonSignals.ok.add(() => { + closeHandler(); + }); + dialog.valueChosen.add(() => { + dialog.closeRequested.dispatch(); + closeHandler(); + }); + + // When cancelled, destroy the entity again + if (deleteOnCancel) { + dialog.buttonSignals.cancel.add(() => { + if (!this.root || !this.root.entityMgr) { + // Game got stopped + return; + } + + const entityRef = this.root.entityMgr.findByUid(uid, false); + if (!entityRef) { + // outdated + return; + } + + const constantComp = entityRef.components.ConstantSignal; + if (!constantComp) { + // no longer interesting + return; + } + + this.root.logic.tryDeleteBuilding(entityRef); + }); + } + } + + /** + * Tries to parse a signal code + * @param {Entity} entity + * @param {string} code + * @returns {BaseItem} + */ + parseSignalCode(entity, code) { + if (!this.root || !this.root.shapeDefinitionMgr) { + // Stale reference + return null; + } + + code = trim(code); + const codeLower = code.toLowerCase(); + + if (enumColors[codeLower]) { + return COLOR_ITEM_SINGLETONS[codeLower]; + } + + if (entity.components.WiredPins) { + if (code === "1" || codeLower === "true") { + return BOOL_TRUE_SINGLETON; + } + + if (code === "0" || codeLower === "false") { + return BOOL_FALSE_SINGLETON; + } + } + + if (ShapeDefinition.isValidShortKey(code)) { + return this.root.shapeDefinitionMgr.getShapeItemFromShortKey(code); + } + + return null; + } } diff --git a/src/js/game/hud/parts/notifications.js b/src/js/game/hud/parts/notifications.js index bef8dd0f..abeab205 100644 --- a/src/js/game/hud/parts/notifications.js +++ b/src/js/game/hud/parts/notifications.js @@ -7,6 +7,9 @@ export const enumNotificationType = { saved: "saved", upgrade: "upgrade", success: "success", + info: "info", + warning: "warning", + error: "error", }; const notificationDuration = 3; @@ -17,14 +20,14 @@ export class HUDNotifications extends BaseHUDPart { } initialize() { - this.root.hud.signals.notification.add(this.onNotification, this); + this.root.hud.signals.notification.add(this.internalShowNotification, this); /** @type {Array<{ element: HTMLElement, expireAt: number}>} */ this.notificationElements = []; // Automatic notifications this.root.signals.gameSaved.add(() => - this.onNotification(T.ingame.notifications.gameSaved, enumNotificationType.saved) + this.internalShowNotification(T.ingame.notifications.gameSaved, enumNotificationType.saved) ); } @@ -32,7 +35,7 @@ export class HUDNotifications extends BaseHUDPart { * @param {string} message * @param {enumNotificationType} type */ - onNotification(message, type) { + internalShowNotification(message, type) { const element = makeDiv(this.element, null, ["notification", "type-" + type], message); element.setAttribute("data-icon", "icons/notification_" + type + ".png"); diff --git a/src/js/game/systems/constant_signal.js b/src/js/game/systems/constant_signal.js index 29079825..75a4dbdd 100644 --- a/src/js/game/systems/constant_signal.js +++ b/src/js/game/systems/constant_signal.js @@ -1,25 +1,16 @@ -import trim from "trim"; -import { THIRDPARTY_URLS } from "../../core/config"; -import { DialogWithForm } from "../../core/modal_dialog_elements"; -import { FormElementInput, FormElementItemChooser } from "../../core/modal_dialog_forms"; -import { fillInLinkIntoTranslation } from "../../core/utils"; -import { T } from "../../translations"; -import { BaseItem } from "../base_item"; -import { enumColors } from "../colors"; import { ConstantSignalComponent } from "../components/constant_signal"; -import { Entity } from "../entity"; import { GameSystemWithFilter } from "../game_system_with_filter"; -import { BOOL_FALSE_SINGLETON, BOOL_TRUE_SINGLETON } from "../items/boolean_item"; -import { COLOR_ITEM_SINGLETONS } from "../items/color_item"; -import { ShapeDefinition } from "../shape_definition"; export class ConstantSignalSystem extends GameSystemWithFilter { constructor(root) { super(root, [ConstantSignalComponent]); - this.root.signals.entityManuallyPlaced.add(entity => - this.editConstantSignal(entity, { deleteOnCancel: true }) - ); + this.root.signals.entityManuallyPlaced.add(entity => { + const editorHud = this.root.hud.parts.constantSignalEdit; + if (editorHud) { + editorHud.editConstantSignal(entity, { deleteOnCancel: true }); + } + }); } update() { @@ -34,171 +25,4 @@ export class ConstantSignalSystem extends GameSystemWithFilter { } } } - - /** - * Asks the entity to enter a valid signal code - * @param {Entity} entity - * @param {object} param0 - * @param {boolean=} param0.deleteOnCancel - */ - editConstantSignal(entity, { deleteOnCancel = true }) { - if (!entity.components.ConstantSignal) { - return; - } - - // Ok, query, but also save the uid because it could get stale - const uid = entity.uid; - - const signal = entity.components.ConstantSignal.signal; - const signalValueInput = new FormElementInput({ - id: "signalValue", - label: fillInLinkIntoTranslation(T.dialogs.editSignal.descShortKey, THIRDPARTY_URLS.shapeViewer), - placeholder: "", - defaultValue: signal ? signal.getAsCopyableKey() : "", - validator: val => this.parseSignalCode(entity, val), - }); - - const items = [...Object.values(COLOR_ITEM_SINGLETONS)]; - - if (entity.components.WiredPins) { - items.unshift(BOOL_FALSE_SINGLETON, BOOL_TRUE_SINGLETON); - items.push( - this.root.shapeDefinitionMgr.getShapeItemFromShortKey( - this.root.gameMode.getBlueprintShapeKey() - ) - ); - } else { - // producer which can produce virtually anything - const shapes = ["CuCuCuCu", "RuRuRuRu", "WuWuWuWu", "SuSuSuSu"]; - items.unshift( - ...shapes.reverse().map(key => this.root.shapeDefinitionMgr.getShapeItemFromShortKey(key)) - ); - } - - if (this.root.gameMode.hasHub()) { - items.push( - this.root.shapeDefinitionMgr.getShapeItemFromDefinition( - this.root.hubGoals.currentGoal.definition - ) - ); - } - - if (this.root.hud.parts.pinnedShapes) { - items.push( - ...this.root.hud.parts.pinnedShapes.pinnedShapes.map(key => - this.root.shapeDefinitionMgr.getShapeItemFromShortKey(key) - ) - ); - } - - const itemInput = new FormElementItemChooser({ - id: "signalItem", - label: null, - items, - }); - - const dialog = new DialogWithForm({ - app: this.root.app, - title: T.dialogs.editConstantProducer.title, - desc: T.dialogs.editSignal.descItems, - formElements: [itemInput, signalValueInput], - buttons: ["cancel:bad:escape", "ok:good:enter"], - closeButton: false, - }); - this.root.hud.parts.dialogs.internalShowDialog(dialog); - - // When confirmed, set the signal - const closeHandler = () => { - if (!this.root || !this.root.entityMgr) { - // Game got stopped - return; - } - - const entityRef = this.root.entityMgr.findByUid(uid, false); - if (!entityRef) { - // outdated - return; - } - - const constantComp = entityRef.components.ConstantSignal; - if (!constantComp) { - // no longer interesting - return; - } - - if (itemInput.chosenItem) { - constantComp.signal = itemInput.chosenItem; - } else { - constantComp.signal = this.parseSignalCode(entity, signalValueInput.getValue()); - } - }; - - dialog.buttonSignals.ok.add(() => { - closeHandler(); - }); - dialog.valueChosen.add(() => { - dialog.closeRequested.dispatch(); - closeHandler(); - }); - - // When cancelled, destroy the entity again - if (deleteOnCancel) { - dialog.buttonSignals.cancel.add(() => { - if (!this.root || !this.root.entityMgr) { - // Game got stopped - return; - } - - const entityRef = this.root.entityMgr.findByUid(uid, false); - if (!entityRef) { - // outdated - return; - } - - const constantComp = entityRef.components.ConstantSignal; - if (!constantComp) { - // no longer interesting - return; - } - - this.root.logic.tryDeleteBuilding(entityRef); - }); - } - } - - /** - * Tries to parse a signal code - * @param {Entity} entity - * @param {string} code - * @returns {BaseItem} - */ - parseSignalCode(entity, code) { - if (!this.root || !this.root.shapeDefinitionMgr) { - // Stale reference - return null; - } - - code = trim(code); - const codeLower = code.toLowerCase(); - - if (enumColors[codeLower]) { - return COLOR_ITEM_SINGLETONS[codeLower]; - } - - if (entity.components.WiredPins) { - if (code === "1" || codeLower === "true") { - return BOOL_TRUE_SINGLETON; - } - - if (code === "0" || codeLower === "false") { - return BOOL_FALSE_SINGLETON; - } - } - - if (ShapeDefinition.isValidShortKey(code)) { - return this.root.shapeDefinitionMgr.getShapeItemFromShortKey(code); - } - - return null; - } } diff --git a/src/js/mods/mod_interface.js b/src/js/mods/mod_interface.js index 904362e7..acf47caf 100644 --- a/src/js/mods/mod_interface.js +++ b/src/js/mods/mod_interface.js @@ -25,6 +25,7 @@ import { KEYMAPPINGS } from "../game/key_action_mapper"; import { HUDModalDialogs } from "../game/hud/parts/modal_dialogs"; import { THEMES } from "../game/theme"; import { ModMetaBuilding } from "./mod_meta_building"; +import { BaseHUDPart } from "../game/hud/base_hud_part"; export class ModInterface { /** @@ -416,4 +417,15 @@ export class ModInterface { extendClass(classHandle, extender) { this.extendObject(classHandle.prototype, extender); } + + /** + * + * @param {string} id + * @param {new (...args) => BaseHUDPart} element + */ + registerHudElement(id, element) { + this.modLoader.signals.hudInitializer.add(root => { + root.hud.parts[id] = new element(root); + }); + } } diff --git a/src/js/mods/mod_signals.js b/src/js/mods/mod_signals.js index 220cb424..c311af29 100644 --- a/src/js/mods/mod_signals.js +++ b/src/js/mods/mod_signals.js @@ -19,6 +19,8 @@ export const MOD_SIGNALS = { hudElementInitialized: /** @type {TypedSignal<[BaseHUDPart]>} */ (new Signal()), hudElementFinalized: /** @type {TypedSignal<[BaseHUDPart]>} */ (new Signal()), + hudInitializer: /** @type {TypedSignal<[GameRoot]>} */ (new Signal()), + gameInitialized: /** @type {TypedSignal<[GameRoot]>} */ (new Signal()), gameLoadingStageEntered: /** @type {TypedSignal<[InGameState, string]>} */ (new Signal()), From 9311ffe4e3a8f5c6d2e9ffaa282623eef75f761c Mon Sep 17 00:00:00 2001 From: tobspr Date: Tue, 18 Jan 2022 11:03:10 +0100 Subject: [PATCH 02/17] Add small tutorial --- mod_examples/README.md | 48 +++++++ mod_examples/combined.js | 223 ------------------------------ mod_examples/custom_keybinding.js | 27 ++++ mod_examples/modify_ui.js | 41 ++++++ 4 files changed, 116 insertions(+), 223 deletions(-) create mode 100644 mod_examples/README.md delete mode 100644 mod_examples/combined.js create mode 100644 mod_examples/custom_keybinding.js create mode 100644 mod_examples/modify_ui.js diff --git a/mod_examples/README.md b/mod_examples/README.md new file mode 100644 index 00000000..cadc3b17 --- /dev/null +++ b/mod_examples/README.md @@ -0,0 +1,48 @@ +# shapez.io Modding + +## General Instructions + +Currently there are two options to develop mods for shapez.io: + +1. Writing single file mods, which doesn't require any additional tools and can be loaded directly in the game +2. Using the `create-shapezio-mod` package. This package is still in development but allows you to pack multiple files and images into a single mod file, so you don't have to base64 encode your images etc. + +Since the `create-shapezio-mod` package is still in development, the current recommended way is to write single file mods, which I'll explain now. + +## Setting up your development environment + +The simplest way of developing mods is by just creating a `mymod.js` file and putting it in the `mods/` folder of the standalone (You can find the `mods/` folder by clicking "Open Mods Folder" in the shapez.io Standalone). + +You can then add `--dev` to the launch options on Steam. This adds an application menu where you can click "Restart" to reload your mod, and will also show the developer console where you can see any potential errors. + +## Getting started + +To get into shapez.io modding, I highly recommend checking out all of the examples in this folder. Here's a list of examples and what features of the modloader they show: + +| Example | Description | Demonstrates | +| ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | +| [base.js](base.js) | The most basic mod | Base structure of a mod | +| [class_extensions.js](class_extensions.js) | Shows how to extend multiple methods of one class at once, useful for overriding a lot of methods | Overriding and extending builtin methods | +| [custom_css.js](custom_css.js) | Modifies the Main Menu State look | Modifying the UI styles with CSS | +| [replace_builtin_sprites.js](replace_builtin_sprites.js) | Replaces all color sprites with icons | Replacing builtin sprites | +| [translations.js](translations.js) | Shows how to replace and add new translations in multiple languages | Adding and replacing translations | +| [add_building_basic.js](add_building_basic.js) | Shows how to add a new building | Registering a new building | +| [add_building_flipper.js](add_building_flipper.js) | Adds a "flipper" building which mirrors shapes from top to bottom | Registering a new building, Adding a custom shape and item processing operation (flip) | +| [custom_drawing.js](custom_drawing.js) | Displays a a small indicator on every item processing building whether it is currently working | Adding a new GameSystem and drawing overlays | +| [custom_keybinding.js](custom_keybinding.js) | Adds a new customizable ingame keybinding (Shift+F) | Adding a new keybinding | +| [custom_sub_shapes.js](custom_sub_shapes.js) | Adds a new type of sub-shape (Line) | Adding a new sub shape and drawing it, making it spawn on the map, modifying the builtin levels | +| [modify_theme.js](modify_theme.js) | Modifies the default game themes | Modifying the builtin themes | +| [custom_theme.js](custom_theme.js) | Adds a new UI and map theme | Adding a new game theme | +| [mod_settings.js](mod_settings.js) | Shows a dialog counting how often the mod has been launched | Reading and storing mod settings | +| [modify_existing_building.js](modify_existing_building.js) | Makes the rotator building always unlocked and adds a new statistic to the building panel | Modifying a builtin building, replacing builtin methods | +| [modify_ui.js](modify_ui.js) | Shows how to add custom IU elements to builtin game states (the Main Menu in this case) | Extending builtin UI states, Adding CSS | +| [pasting.js](pasting.js) | Shows a dialog when pasting text in the game | Listening to paste events | + +### Advanced Examples + +| Example | Description | Demonstrates | +| ------------------------------------------------ | ---------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [notification_blocks.js](notification_blocks.js) | Adds a notification block building, which shows a user defined notification when receiving a truthy signal | Adding a new Component, Adding a new GameSystem, Working with wire networks, Adding a new building, Adding a new HUD part, Using Input Dialogs, Adding Translations | +| [usage_statistics.js](usage_statistics.js) | Displays a percentage on every building showing its utilization | Adding a new component, Adding a new GameSystem, Drawing within a GameSystem, Modifying builtin buildings, Adding custom game logic | +| [new_item_type.js](new_item_type.js) | Adds a new type of items to the map (fluids) | Adding a new item type, modifying map generation | +| [buildings_have_cost.js](buildings_have_cost.js) | Adds a new currency, and belts cost 1 of that currency | Extending and replacing builtin methods, Adding CSS and custom sprites | diff --git a/mod_examples/combined.js b/mod_examples/combined.js deleted file mode 100644 index 57d2f218..00000000 --- a/mod_examples/combined.js +++ /dev/null @@ -1,223 +0,0 @@ -class DemoModComponent extends shapez.Component { - static getId() { - return "DemoMod"; - } - - static getSchema() { - return { - magicNumber: shapez.types.uint, - }; - } - - constructor(magicNumber) { - super(); - - this.magicNumber = magicNumber; - } -} - -class MetaDemoModBuilding extends shapez.MetaBuilding { - constructor() { - super("demoModBuilding"); - } - - getSilhouetteColor() { - return "red"; - } - - setupEntityComponents(entity) { - entity.addComponent(new DemoModComponent(Math.floor(Math.random() * 100.0))); - } -} - -class DemoModSystem extends shapez.GameSystemWithFilter { - constructor(root) { - super(root, [DemoModComponent]); - } - - update() { - // nothing to do here - } - - drawChunk(parameters, chunk) { - const contents = chunk.containedEntitiesByLayer.regular; - for (let i = 0; i < contents.length; ++i) { - const entity = contents[i]; - const demoComp = entity.components.DemoMod; - if (!demoComp) { - continue; - } - - const staticComp = entity.components.StaticMapEntity; - - const context = parameters.context; - const center = staticComp.getTileSpaceBounds().getCenter().toWorldSpace(); - - // Culling for better performance - if (parameters.visibleRect.containsCircle(center.x, center.y, 40)) { - // Circle - context.fillStyle = "#53cf47"; - context.strokeStyle = "#000"; - context.lineWidth = 2; - - const timeFactor = 5.23 * this.root.time.now(); - context.beginCircle( - center.x + Math.cos(timeFactor) * 10, - center.y + Math.sin(timeFactor) * 10, - 7 - ); - context.fill(); - context.stroke(); - - // Text - context.fillStyle = "#fff"; - context.textAlign = "center"; - context.font = "12px GameFont"; - context.fillText(demoComp.magicNumber, center.x, center.y + 4); - } - } - } -} - -class Mod extends shapez.Mod { - constructor(app, modLoader) { - super( - app, - { - website: "https://tobspr.io", - author: "tobspr", - name: "Demo Mod", - version: "1", - id: "demo-mod", - description: "A simple mod to demonstrate the capatibilities of the mod loader.", - }, - modLoader - ); - } - - init() { - // Add some custom css - // this.modInterface.registerCss(` - // * { - // font-family: "Comic Sans", "Comic Sans MS", Tahoma !important; - // } - // `); - - // Add an atlas - this.modInterface.registerAtlas(RESOURCES["demoAtlas.png"], RESOURCES["demoAtlas.json"]); - - // Register a new component - this.modInterface.registerComponent(DemoModComponent); - - // Register a new game system which can update and draw stuff - this.modInterface.registerGameSystem({ - id: "demo_mod", - systemClass: DemoModSystem, - before: "belt", - drawHooks: ["staticAfter"], - }); - - // // Register the new building - // this.modInterface.registerNewBuilding({ - // metaClass: MetaDemoModBuilding, - // buildingIconBase64: RESOURCES["demoBuilding.png"], - - // variantsAndRotations: [ - // { - // description: "A test building", - // name: "A test name", - - // regularImageBase64: RESOURCES["demoBuilding.png"], - // blueprintImageBase64: RESOURCES["demoBuildingBlueprint.png"], - // tutorialImageBase64: RESOURCES["demoBuildingBlueprint.png"], - // }, - // ], - // }); - - // Add it to the regular toolbar - // this.modInterface.addNewBuildingToToolbar({ - // toolbar: "regular", - // location: "primary", - // metaClass: MetaDemoModBuilding, - // }); - - // Register keybinding - this.modInterface.registerIngameKeybinding({ - id: "demo_mod_binding", - keyCode: shapez.keyToKeyCode("F"), - translation: "mymod: Do something (always with SHIFT)", - modifiers: { - shift: true, - }, - handler: root => { - this.dialogs.showInfo("Mod Message", "It worked!"); - return shapez.STOP_PROPAGATION; - }, - }); - - // Add fancy sign to main menu - this.signals.stateEntered.add(state => { - if (state.key === "MainMenuState") { - const element = document.createElement("div"); - element.id = "demo_mod_hello_world_element"; - document.body.appendChild(element); - - const button = document.createElement("button"); - button.classList.add("styledButton"); - button.innerText = "Hello!"; - button.addEventListener("click", () => { - this.dialogs.showInfo("Mod Message", "Button clicked!"); - }); - element.appendChild(button); - } - }); - - this.modInterface.registerCss(` - #demo_mod_hello_world_element { - position: absolute; - top: calc(10px * var(--ui-scale)); - left: calc(10px * var(--ui-scale)); - color: red; - z-index: 0; - } - - `); - } -} - -//////////////////////////////////////////////////////////////////////// -// @notice: Later this part will be autogenerated - -const RESOURCES = { - "red.png": - "data:image/png;base64,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", - - "green.png": - "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAIwwAACMMBoDLh7AAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAABP2SURBVHic7Z15eFTV3cc/ZybJJJmQsEOQoqAVBaFVX7Uu5VXBpdW3uHZ56dviW1uL7VOXautSKy61Vay7Vn1tVV6tWkQUFI0RVFQsIPuO1hBMwhLJMskkmSQzv/5xb5K7zWSSzJKB+3me+yRz7nbO/f7uWX7n3HOUiOASP0opP3AhcC7wTaAeeEJEHk1rxHqJcg2ge5RSCk3smcClQIHlkDbgayKyNcVR6zNZ6Y5Af0cp9QPgTmBcjMOyAV9qYpRYXAOIglLqq8BjwLQ4Dl8EbExujJKDawAWlFI+4LfATcR+q3egCV8iIqWpiFsycA3AgFLqG8CzwJFRDgkA/wCeFpHlKYtYEnENQEcpNRVYCOQ77G4B/gTMEZGmlEYsybgGACilvg3MB3Iddi8ErhaRstTGKjUc9M1ApdQxwCrs4pcBvxSRxamPVeo4qHMAvcL3PHbx1wLniEh16mOVWjzpjkCauQuYbAn7J3DGwSA+HMRFgFJqNFCO+SXYD0wWkar0xCr19MscQCmVinhdhj39P02W+ClKU49JSw6glBoBnAAcAozS/xr/HwzUAVWWrVL/u1xE9vbh/gr4HDjMELxEROLx+kW75gjgFLrSMMqyDQRq6EpDpeX/VX1JU68RkZRswFjgWuADIAxIH7Yw8CHwa2BcL+IyzeGaP+jFdcbpafowQWn6QL/e2JTpkmTRJwO3Auv6+HC629bp95kUZ7xus5xfC+TGee6kFKdpcsYZAPA1oDTJDyja9lZ3Dw3N3Ws8Z0GcxvxWmtJUitbdnHCtEuoHUEoVo3WdzqSbCqZS4B/soXCEhwHDvRSO6Pjfg3+wh2BNhPo9EQJ7ItTvDhPYq/0NBaW7aJwDnKWUmgvcIiIVDsccavm9K0aaRgN3AD/qLk0APr+iqFhLT1Gxl8KRHopGdqWpYV+EwF5ta9inpStYE0FiJ2sasEYp9QzwOxHZ3V084iUhlUClVD5wvb75ox3n8cKhx+cw8RwfR0/zUTii5xXjYE2Ere+E2Lg4RNnKViQS8/Bm4AHgTyISMMR3J2Yj+I2IzLGkqRC4AbgayIt2A+WBsSfmMOnbWpr8g3uepsBeLU2bS0KUr24lEo55eBCYQ4L6JfpsAEqp6Wj95qOc9nuzYNzJOUw828dRU3v3gKLR+GWETW+F2PhGC1+sa4v1Fn2J5s9/Xo9zLVqtvIOZIvJsxw+l1Aw0wxnqdDGlYPTXspl8fi7HnOujYGji0hSsibBtSYjNb4f4/ONWwu1RD60CrhSR1/pyvz4ZgFLqRuAPgLLvg0nfzuWsX/sZOMrbhyjGR11VmI/nNvPP/2+K9QbdBfwO2ARMMITfICJ3683DO9HGAtjweOEb/5PPyT/KS1maSv8cZOPilmjGLcDNIvLH3t6jVwaglMoB/g+tXLRx6PHZnPvbAkZPzo7reu2tQmN1hIbqCIF9EYaO9TLiq72rnuzd0c7C2Q3sWtMW7ZB/AEOAqYawx4Fr0CqH33U6acxx2Xxn9gBGHJn67pOKDW28dXcj5aujpmkumhOrtafX7rEBKKWGAguA06z7Bo/xcvZ1BUw8O/pAmkgYdq1pY9vSEP/6uJX63RGa6+0F+Td+mMd5vxvQo7h1IAJrF7RQMqeRplrHSkI1MMzweyla3eUk64H5gzycc30Bx16Yi7Llc6ll89sh3r63kZpdjlnch8CFIvJlT67ZIwNQSk0AXkdz6pg49sJcpt8+AG+2/SmF22H7uyG2Lgmx471Wmupi19wAsnyKG5YPxefv/VNvro/w9p+DrJ7X3F0tOwyY8nSl4PhL8zj7137yivqPFzfcJrz2+wbWLmhx2l0GnC8iW+K9XtwGoJQ6EliBufKEUjDtmgKm/Mw+kEYENr3ZwjsPBKNZbVQGj/FydcmQhLx1m94M8fL19bEqVCa8WXDJnCKO+Vb/Hei77Mkm3rm/0cmw64CTRGRHPNeJywD0JtEK4ChjeHau4pI5hUw4y/6gyla0UjKnkcpNcT51HW8WHHZiDqfP8nPYCfHVIeJhx/utvPCretpDsdOb5VP84KEijvzPnITdO1lsKQ3x8vUB2lpsadqGZgQBh9NMdGsAes34NeC/jOEDhnv44V8GMmqiuVLUVBvhlRsb2P5eKOZ18woV48/0MfaEHApHeCgY5mHAMA/5gzxJK2vLVrby/Kz6qM4kn18x4y9FjD2x/4vfQdXmdp6bVUfDPluxugiYLt0IHI8B3A7cYgzzD/Hw83mDbE2hvZ+28/ysemornLN7/xAPx5zrY8I0H4edmIMn+S0pGxUb2ph7eR3NAXu6f/zXgRxxauaI30FdVZjHL60luN9mBHeIyO9jnRvTAJRSFwEvY2jne7Ng5tODbNnztndDvHxdwPHt8vkVp/0kn1MuyycnL81VabSm4jOX1dFoeWA3rRxGXmH649cbdq5q45nLaq31HAEuEZFXop0X1QD0Gv8KLN/BnX/LAE6aYfaMfvBUE6X3Ndrcst4s+I/v5XHGL/wJ9QAmgtZmYf5vA2wtDSECU6/yc/qsqF7sjGDF8828fkeDNbgRrT7g2DKIZQBLgDONYcddlMuFdxWajlv592YW3W67KQOGefjvR4vidgali6ZarTNm2OEHxvjYBTcFWPOKrYm4VESmOh3vaABKqWloXZCdHDIpm8ufH0hWTlcW+fnHrTx7eZ3N9XrIpGxmPFrEgOH9660/GGhvFZ6aUUflRpvX8CwReccaGE2hu4w/snIU33+w0CR+za4wL14dsIk/+bxcLn9uoCt+mnDSSucup+NtKimlLkYbr9fJSTPMnR8tDcJzP6+zuXCPnubjknsLyfJlZkXqQGHgKK+tngacoGtrwmQASikvWm9YJ74CxZQrzF6+0vsbqf7c/OqPHJ/FJfcUpt1f7qIx5Yp8fAU2Me7UNe7EmgP8GIu377Sf5JM/sOuw/eVhPnmp2XSSf4iHGX8pIiffVb+/kD/Qw2k/sbnnj0LTuBOrAdxo/FEwxMMpMy1v/32NtnL/e/cVpqR/3KVnnDIzn4IhtlLepHHnXv0jySOMO0+/0m9y3FRsaGNzidnFe9SZPsaelHnes4OBnDzF6VfafBtH6FoD5hxguvGoLJ/iuIvN30y+fW/Q9NvjhbOuzWznyYHOcRfnOlXKO7U2GsB3jEccfnI22bldJ9ZVhilbaR5w8vULchl+xIHhQDlQyc5VHH6yzRnXqbUHOodzm5p+488wd/FuXWLO+pUHzvyl+/ZnAlYt0ZqExdCVA5yPZWDn+NOtBmB++7/y9WyKit2KXyZg1RJN6/OhywBM5f+oCVmmMfvNAaH8E7MBOA0CcemfFI7wMGqCraieDuDRHQOmjoLxZ5rF3fF+yNb0mzDNNYBMwqopMFUp5fUAw7FMkTLO0qz7Yp25Y2Hk+CwGfcXN/jMJq6Zomg/3AMXWPUUjzc6Dxmqzz3/k0W7NP9OwaqpT7GgA1p68gGW8WaHb05dxROmdtRtAXqGyOQ6sAw4LhrnZf6aR5VNOw93sBjBguF3cxi/NBjBgmJsDZCIO2joZgFncUFBobzWPGioqdg0gE3EoBuwGYC3ffX7Focd3uRIHjvIyelL/Hufn4oxD3a04CzC1D7z2oURMv6OQZU8ECQWFKT/Np39OeObSHQ7a5mShTVHWSWCv/aOOYeO8XHx3oS3cJbNw0LbSg9UA9nT/5a5LZuKgbaUHME2iVO8awAGLg7YVthyguT7i9LWpS4bT1iJOE3HYcwDQZq1yObCIoqk9BwAI7OnZZA4u/Z8omlZ6RKQWMM0359YDDjwcNG0SkdqOFr2pGHCLgAMPB00roGtEUJlxz57tPZvWxaX/46BpGXQZwEfGPTtX9ni6OZd+joOmH0GXAbxv3NNQHWF/uVsRPFDYXx6modpWBLwPXQawAm1xxE7cXODAwUHLFjTNNQMQkVBHQAdlq6JOS+qSYThouULX3PRlkKkY2OkawAGDg5adWkc1gPrdYWq/cOsBmU7tF2Hqd9t0dDSAjwFTYeEWA5mPg4ataFoDBgMQkWa0NXQ7cSuCmY+Dhqt0rQH7BBGmYsDNATIfBw1NGsc0gLpKx/LDJUOo3x2mrjJ6+Q92A1gOmHyG25a6xUCm4qBdO5rGnZgMQEQa0VbP6GTDG44LE7hkAA7aLdU17sRpfO+Lxh9frG1zi4EMpH53mC/W2sr/F60BTgawAENzUAQ2Lo49979L/2Pj4pB1NZFWNG1N2AxAROqAEtPF3GIg43DQrETX1kS0TzxeMv6o2tLu9g5mEPvLw1RtsfX/v+R0bDQDWIi27Gonbi6QOTho1YymqQ1HAxCRBmCx6aJuPSBjcNBqsa6pjVhf+ZmyjH2ftbtDxTKAPdvb2fdZfNk/xDaA19GWG+nELQb6Pw4aNaJp6UhUA9A7DBaZLu4WA/0eB40WGTt/rHT3obfJcVBbEaZivdtB1F+pWN/mtGSfzfljpDsDKAHqjQGr/hHVmFzSjIM29Vh8OlZiGoA+bmyeMWzD66FoK3K7pJGm2ggbXrdl//M6xv5FI565Ph4x/mgPCatecnOB/saql5qd1kV+xOlYI90agIisx9KHvOLvzXGvxO2SfMLtmiYW3te1i0m8s/08aPzRsC/C5hK3Sdhf2FzS4rR49INOx1qJ1wAWAjuNAR/PdYuB/oKDFjuJ4vq1EpcBiEgYeNQYVrG+zW0S9gOi6PCorlm39GTCt6cA06JBbi6Qfhw0CKJpFRdxG4DelzzXGBal7HFJEVHqYnOd+v2j0dMpHx9CW5MeiFr7dEkRDq0xQdMobnpkACKyDcuq4lHany5JJoo/plTXKG56M+mrqXnRVBth/SK3SZhq1i9qcfLIxtX0M9IbA3gT+NQYsOzJJtcxlELC7dozt/ApmjY9oscGICK2cqZmV5hVL7p1gVSx6sVmanbZWnkP6dr0iN7O+/1XoMoY8N5jQUKNbl0g2YQahfceC1qDq9A06TG9MgB9gMFsY1iwJsIHT9myJZcE88FTTQRrbGX/7FiDPmLRl5n//wZsNwYsf6bJaTIilwTRUB1h+TO2l2w7mha9otcGoLsabzaGtbUISx+2ZU8uCWLpw0Gnibxvjtft60Sf1v4QkfnASmPYmvnNVP/LbRIkmup/tbNmvi2XX6lr0GsSsfjLDcYfkTCU3ufmAomm9L6gbfleLM++N/TZAETkXSzjzrYuCVG+2u0pTBTlq9vYusQ2sqtEf/Z9IlHLP92IoY8AoGROY5RDXXqKw7MUtGfeZxJiACKyFuu8Auva2FLqfkfQV7aUhmyLdwMv6s+8z6heOI+cL6TUOGAL0LlOeVGxl1++Nphc+5KlLnHQEhAemV5jnaAjBEwQkc8TcY+ErQCoR8jULKzfHea1WwOJusVBx2u3BpxmZ7k5UeJDAg1A535gmTFg05sh1i5wewt7ytoFLWx601aEvo/2jBNGwoqAzgsqNRbYABR0hOXkK37x6mAGj3FXHY+Hml1hHr2ghtYmkzYNwGQR2ZnIeyV8EVgRKQOuMYa1Ngnzrgs4tWNdLETCMO+6gFV8gKsSLT4kwQAAROQp4A1jWMWGNtdNHAdLHw5SscFW618oIk8n437JXAb6cmC/MWDZk0HKP3EdRNEo/6SNZU/aXpJq4KfJumfSDEBE9gCzTGERmHd9gJaAO27ASktAmHd9ALF3pv5MRPYl675JXQheROYBLxjD6neHWTjbcbqag5qFsxucmnzPisirybxvUg1A5xdYRg9tXNzCh39zB4908OHfmti42NZULgd+lex7J90A9JVJ/xdrX8E9jax8Ib3jCEONkvZhbCtfaKbkHpuvPwLMFJGke9ES7geIeiOlrgIeMIfBhX8s5NgLclMShw7qqsKU3hdkc4nmaJl4jo+zrvUzcFRq/RRrX21hwY0B65SuALNE5PFUxCFlBgCglLoTi7vY44WL7ylk8nnJN4JQUFj2RJDlz9o/ZsnyKU75cR5TrvDj8ye/72LDGy3M/42jb2S2iNyW9AjopNQAAJRST2Jp1igFU67wM/VXflQSCqVIGNa+0sw7DwZp/DL2mMWCoR6mXeXn2Ivy8CQhQ5AILHkoyLIngk5v/uMiMsvhtKSRDgPwoM07dJF135FTcrj03qKE9R5+WRZmzfxm1r7a0q3wVgqGejj2glyOuziPoWMTYwktAWHedfXsWOa4CMd84LsiDg3BJJJyAwBQSvmAZ4DvW/cNHuPlWzcWcNQZPtt58RAKCpvfCrH65WZ22efLt7JB/zs51kFjjs3m+EvymHiur9fFw7Z3Q7z5x0anDzpAG0sxs7sJnZJBWgyg8+ZK3QzcAdie6ujJ2Uy9ys8Rp+bEvEZrs7BrTRtlK1opW9lG1aa2eD5T+xS4la5BLN8HbgO+GuskbxaMOiabsSdmM/akHMYcl01OXmyD+OyjVpY86OjeBa1ldIuI/KHbGCeJtBoAgFLqAuA5wO+0f/AYL8VHZTFifBZDDvPSVCvUV4Wp2x2mtiLCnq1xCd5BBXA78LSImM5SSmUBlwG/B0bHczFvFow8OptBoz0MLPZSNMpL/iDF/rIwe7a3s3tbe6zFN4PAD5Pt6OkWEUn7hpYFf4b2RiR6i6CNUfgZ4IsjLj792GX6ucmI02doXbvpf/bpjoDhwWej9R1UJughrwN+A4zpQ5zG6NdYl6A4VeppzE738+7Y0l4EWFFK5QJXoo15H9aDU2uAj4APgTdEZHOC4zUROA84DTgVGNyD06uBPwGPiUi/Gh7V7wygA6VUPnAi8HXDdiTa/Le79a0SWA18AGyVFCVGKaWAo4FvAscDhwDF+laE9r3eOmC9/neliPTLzo9/Ayif0cMIPHkWAAAAAElFTkSuQmCC", - - "purple.png": - "data:image/png;base64,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", - - "blue.png": - "data:image/png;base64,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", - - "yellow.png": - "data:image/png;base64,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", - - "cyan.png": - "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAACXBIWXMAAAOxAAADsQH1g+1JAAAE8mlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4gPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNy4xLWMwMDAgNzkuZGFiYWNiYiwgMjAyMS8wNC8xNC0wMDozOTo0NCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvIiB4bWxuczpkYz0iaHR0cDovL3B1cmwub3JnL2RjL2VsZW1lbnRzLzEuMS8iIHhtbG5zOnBob3Rvc2hvcD0iaHR0cDovL25zLmFkb2JlLmNvbS9waG90b3Nob3AvMS4wLyIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0RXZ0PSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VFdmVudCMiIHhtcDpDcmVhdG9yVG9vbD0iQWRvYmUgUGhvdG9zaG9wIDIzLjAgKE1hY2ludG9zaCkiIHhtcDpDcmVhdGVEYXRlPSIyMDIyLTAxLTE0VDEwOjMwOjMyKzAxOjAwIiB4bXA6TW9kaWZ5RGF0ZT0iMjAyMi0wMS0xNFQxMDozMDo1MCswMTowMCIgeG1wOk1ldGFkYXRhRGF0ZT0iMjAyMi0wMS0xNFQxMDozMDo1MCswMTowMCIgZGM6Zm9ybWF0PSJpbWFnZS9wbmciIHBob3Rvc2hvcDpDb2xvck1vZGU9IjMiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6ZTFiZTU2MjEtMTU1ZC00YmFmLWFiNzgtYjM2Y2QzMmIzNTNkIiB4bXBNTTpEb2N1bWVudElE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", - - "white.png": - "data:image/png;base64,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", - "demoBuilding.png": - "data:image/png;base64,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", - - "demoBuildingBlueprint.png": - "data:image/png;base64,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", - - "demoAtlas.png": - "data:image/png;base64,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", - - "demoAtlas.json": - '{"frames":{"enum_selector.png":{"frame":{"x":1,"y":1,"w":38,"h":23},"rotated":false,"trimmed":true,"spriteSourceSize":{"x":5,"y":12,"w":38,"h":23},"sourceSize":{"w":48,"h":48}},"enum_selector_white.png":{"frame":{"x":387,"y":101,"w":38,"h":23},"rotated":false,"trimmed":true,"spriteSourceSize":{"x":5,"y":12,"w":38,"h":23},"sourceSize":{"w":48,"h":48}},"icon.png":{"frame":{"x":387,"y":126,"w":284,"h":284},"rotated":false,"trimmed":true,"spriteSourceSize":{"x":0,"y":0,"w":284,"h":284},"sourceSize":{"w":284,"h":284}},"sprites/blueprints/miner-chainable.png":{"frame":{"x":673,"y":267,"w":137,"h":143},"rotated":false,"trimmed":true,"spriteSourceSize":{"x":4,"y":0,"w":137,"h":143},"sourceSize":{"w":144,"h":144}},"sprites/buildings/miner-chainable.png":{"frame":{"x":812,"y":268,"w":136,"h":142},"rotated":false,"trimmed":true,"spriteSourceSize":{"x":5,"y":0,"w":136,"h":142},"sourceSize":{"w":144,"h":144}},"test.png":{"frame":{"x":1,"y":26,"w":384,"h":384},"rotated":false,"trimmed":true,"spriteSourceSize":{"x":0,"y":0,"w":384,"h":384},"sourceSize":{"w":384,"h":384}}},"meta":{"image":"atlas0_hq.png","format":"RGBA8888","size":{"w":1024,"h":512},"scale":"0.75"}}', -}; diff --git a/mod_examples/custom_keybinding.js b/mod_examples/custom_keybinding.js new file mode 100644 index 00000000..650065f0 --- /dev/null +++ b/mod_examples/custom_keybinding.js @@ -0,0 +1,27 @@ +// @ts-nocheck +const METADATA = { + website: "https://tobspr.io", + author: "tobspr", + name: "Mod Example: Custom Keybindings", + version: "1", + id: "base", + description: "Shows how to add a new keybinding", +}; + +class Mod extends shapez.Mod { + init() { + // Register keybinding + this.modInterface.registerIngameKeybinding({ + id: "demo_mod_binding", + keyCode: shapez.keyToKeyCode("F"), + translation: "Do something (always with SHIFT)", + modifiers: { + shift: true, + }, + handler: root => { + this.dialogs.showInfo("Mod Message", "It worked!"); + return shapez.STOP_PROPAGATION; + }, + }); + } +} diff --git a/mod_examples/modify_ui.js b/mod_examples/modify_ui.js new file mode 100644 index 00000000..749e191e --- /dev/null +++ b/mod_examples/modify_ui.js @@ -0,0 +1,41 @@ +// @ts-nocheck +const METADATA = { + website: "https://tobspr.io", + author: "tobspr", + name: "Mod Example: Modify UI", + version: "1", + id: "modify-ui", + description: "Shows how to modify a builtin game state, in this case the main menu", +}; + +class Mod extends shapez.Mod { + init() { + // Add fancy sign to main menu + this.signals.stateEntered.add(state => { + if (state.key === "MainMenuState") { + const element = document.createElement("div"); + element.id = "demo_mod_hello_world_element"; + document.body.appendChild(element); + + const button = document.createElement("button"); + button.classList.add("styledButton"); + button.innerText = "Hello!"; + button.addEventListener("click", () => { + this.dialogs.showInfo("Mod Message", "Button clicked!"); + }); + element.appendChild(button); + } + }); + + this.modInterface.registerCss(` + #demo_mod_hello_world_element { + position: absolute; + top: calc(10px * var(--ui-scale)); + left: calc(10px * var(--ui-scale)); + color: red; + z-index: 0; + } + + `); + } +} From dc95ece45b203315397543248090e84bdd944d0c Mon Sep 17 00:00:00 2001 From: tobspr Date: Tue, 18 Jan 2022 17:51:56 +0100 Subject: [PATCH 03/17] Update readme --- mod_examples/README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/mod_examples/README.md b/mod_examples/README.md index cadc3b17..53a4309e 100644 --- a/mod_examples/README.md +++ b/mod_examples/README.md @@ -9,6 +9,10 @@ Currently there are two options to develop mods for shapez.io: Since the `create-shapezio-mod` package is still in development, the current recommended way is to write single file mods, which I'll explain now. +## Mod Developer Discord + +A great place to get help with mod development is the official [shapez.io modloader discord]https://discord.gg/xq5v8uyMue). + ## Setting up your development environment The simplest way of developing mods is by just creating a `mymod.js` file and putting it in the `mods/` folder of the standalone (You can find the `mods/` folder by clicking "Open Mods Folder" in the shapez.io Standalone). From f3a939b071ba5119bb02385e76242f71af246309 Mon Sep 17 00:00:00 2001 From: tobspr Date: Tue, 18 Jan 2022 18:23:23 +0100 Subject: [PATCH 04/17] Add better instructions --- mod_examples/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mod_examples/README.md b/mod_examples/README.md index 53a4309e..1545a46b 100644 --- a/mod_examples/README.md +++ b/mod_examples/README.md @@ -15,7 +15,7 @@ A great place to get help with mod development is the official [shapez.io modloa ## Setting up your development environment -The simplest way of developing mods is by just creating a `mymod.js` file and putting it in the `mods/` folder of the standalone (You can find the `mods/` folder by clicking "Open Mods Folder" in the shapez.io Standalone). +The simplest way of developing mods is by just creating a `mymod.js` file and putting it in the `mods/` folder of the standalone (You can find the `mods/` folder by clicking "Open Mods Folder" in the shapez.io Standalone, be sure to select the 1.5.0-modloader branch on Steam). You can then add `--dev` to the launch options on Steam. This adds an application menu where you can click "Restart" to reload your mod, and will also show the developer console where you can see any potential errors. From e686ad2fe1ae1bbf22864dc68644650219c294d6 Mon Sep 17 00:00:00 2001 From: Bagel03 <70449196+Bagel03@users.noreply.github.com> Date: Tue, 18 Jan 2022 12:37:52 -0500 Subject: [PATCH 05/17] Update JSDoc for Replacing Methods (#1336) * upgraded types for overriding methods * updated comments Co-authored-by: Edward Badel --- src/js/mods/mod_interface.js | 61 +++++++++++++++++++++++++++++++++--- 1 file changed, 56 insertions(+), 5 deletions(-) diff --git a/src/js/mods/mod_interface.js b/src/js/mods/mod_interface.js index acf47caf..6fb54be1 100644 --- a/src/js/mods/mod_interface.js +++ b/src/js/mods/mod_interface.js @@ -27,6 +27,27 @@ import { THEMES } from "../game/theme"; import { ModMetaBuilding } from "./mod_meta_building"; import { BaseHUDPart } from "../game/hud/base_hud_part"; +/** + * @typedef {{new(...args: any[]): any, prototype: any}} constructable + */ + +/** + * @template {(...args: any[]) => any} F + * @template P + * @typedef {(...args: [P, Parameters]) => ReturnType} beforePrams IMPORTANT: this puts the original parameters into an array + */ + +/** + * @template {(...args: any[]) => any} F + * @template P + * @typedef {(...args: [...Parameters, P]) => ReturnType} afterPrams + */ + +/** + * @template {(...args: any[]) => any} F + * @typedef {(...args: [...Parameters, ...any]) => ReturnType} extendsPrams + */ + export class ModInterface { /** * @@ -364,28 +385,58 @@ export class ModInterface { } /** - * Patches a method on a given object + * Patches a method on a given class + * @template {constructable} C the class + * @template {C["prototype"]} P the prototype of said class + * @template {keyof P} M the name of the method we are overriding + * @template {extendsPrams} O the method that will override the old one + * @param {C} classHandle + * @param {M} methodName + * @param {beforePrams} override */ replaceMethod(classHandle, methodName, override) { const oldMethod = classHandle.prototype[methodName]; classHandle.prototype[methodName] = function () { + //@ts-ignore This is true I just cant tell it that arguments will be Arguments return override.call(this, oldMethod.bind(this), arguments); }; } + /** + * Runs before a method on a given class + * @template {constructable} C the class + * @template {C["prototype"]} P the prototype of said class + * @template {keyof P} M the name of the method we are overriding + * @template {extendsPrams} O the method that will run before the old one + * @param {C} classHandle + * @param {M} methodName + * @param {O} executeBefore + */ runBeforeMethod(classHandle, methodName, executeBefore) { const oldHandle = classHandle.prototype[methodName]; classHandle.prototype[methodName] = function () { - executeBefore.apply(this, arguments); - return oldHandle.apply(this, arguments); + //@ts-ignore Same as above + executeBefore.apply(this, ...arguments); + return oldHandle.apply(this, ...arguments); }; } + /** + * Runs after a method on a given class + * @template {constructable} C the class + * @template {C["prototype"]} P the prototype of said class + * @template {keyof P} M the name of the method we are overriding + * @template {extendsPrams} O the method that will run before the old one + * @param {C} classHandle + * @param {M} methodName + * @param {O} executeAfter + */ runAfterMethod(classHandle, methodName, executeAfter) { const oldHandle = classHandle.prototype[methodName]; classHandle.prototype[methodName] = function () { - const returnValue = oldHandle.apply(this, arguments); - executeAfter.apply(this, arguments); + const returnValue = oldHandle.apply(this, ...arguments); + //@ts-ignore + executeAfter.apply(this, ...arguments); return returnValue; }; } From 4a76135d575d250515781531077a7b9d00fcb3ec Mon Sep 17 00:00:00 2001 From: tobspr Date: Tue, 18 Jan 2022 20:38:20 +0100 Subject: [PATCH 06/17] Direction lock now indicates when there is a building inbetween --- mod_examples/buildings_have_cost.js | 4 +- mod_examples/custom_theme.js | 4 + src/js/game/blueprint.js | 6 +- src/js/game/hud/parts/building_placer.js | 138 ++++++++++++++++------- src/js/game/logic.js | 10 +- src/js/game/themes/dark.json | 4 + src/js/game/themes/light.json | 4 + 7 files changed, 119 insertions(+), 51 deletions(-) diff --git a/mod_examples/buildings_have_cost.js b/mod_examples/buildings_have_cost.js index b97197f9..79061d35 100644 --- a/mod_examples/buildings_have_cost.js +++ b/mod_examples/buildings_have_cost.js @@ -65,10 +65,10 @@ class Mod extends shapez.Mod { // Only allow placing an entity when there is enough currency this.modInterface.replaceMethod(shapez.GameLogic, "checkCanPlaceEntity", function ( $original, - [entity, offset] + [entity, options] ) { const storedCurrency = this.root.hubGoals.storedShapes[CURRENCY] || 0; - return storedCurrency > 0 && $original(entity, offset); + return storedCurrency > 0 && $original(entity, options); }); // Take shapes when placing a building diff --git a/mod_examples/custom_theme.js b/mod_examples/custom_theme.js index fd2e063a..a596799c 100644 --- a/mod_examples/custom_theme.js +++ b/mod_examples/custom_theme.js @@ -40,6 +40,10 @@ const RESOURCES = { color: "rgb(74, 237, 134)", background: "rgba(74, 237, 134, 0.2)", }, + error: { + color: "rgb(255, 137, 137)", + background: "rgba(255, 137, 137, 0.2)", + }, }, colorBlindPickerTile: "rgba(50, 50, 50, 0.4)", diff --git a/src/js/game/blueprint.js b/src/js/game/blueprint.js index 795b27c3..14848485 100644 --- a/src/js/game/blueprint.js +++ b/src/js/game/blueprint.js @@ -82,7 +82,7 @@ export class Blueprint { const rect = staticComp.getTileSpaceBounds(); rect.moveBy(tile.x, tile.y); - if (!parameters.root.logic.checkCanPlaceEntity(entity, tile)) { + if (!parameters.root.logic.checkCanPlaceEntity(entity, { offset: tile })) { parameters.context.globalAlpha = 0.3; } else { parameters.context.globalAlpha = 1; @@ -131,7 +131,7 @@ export class Blueprint { for (let i = 0; i < this.entities.length; ++i) { const entity = this.entities[i]; - if (root.logic.checkCanPlaceEntity(entity, tile)) { + if (root.logic.checkCanPlaceEntity(entity, { offset: tile })) { anyPlaceable = true; } } @@ -160,7 +160,7 @@ export class Blueprint { let count = 0; for (let i = 0; i < this.entities.length; ++i) { const entity = this.entities[i]; - if (!root.logic.checkCanPlaceEntity(entity, tile)) { + if (!root.logic.checkCanPlaceEntity(entity, { offset: tile })) { continue; } diff --git a/src/js/game/hud/parts/building_placer.js b/src/js/game/hud/parts/building_placer.js index ee7bc804..e4ecfe7e 100644 --- a/src/js/game/hud/parts/building_placer.js +++ b/src/js/game/hud/parts/building_placer.js @@ -61,7 +61,7 @@ export class HUDBuildingPlacer extends HUDBuildingPlacerLogic { this.currentInterpolatedCornerTile = new Vector(); this.lockIndicatorSprites = {}; - layers.forEach(layer => { + [...layers, "error"].forEach(layer => { this.lockIndicatorSprites[layer] = this.makeLockIndicatorSprite(layer); }); @@ -76,7 +76,7 @@ export class HUDBuildingPlacer extends HUDBuildingPlacerLogic { /** * Makes the lock indicator sprite for the given layer - * @param {Layer} layer + * @param {string} layer */ makeLockIndicatorSprite(layer) { const dims = 48; @@ -358,7 +358,7 @@ export class HUDBuildingPlacer extends HUDBuildingPlacerLogic { rotationVariant ); - const canBuild = this.root.logic.checkCanPlaceEntity(this.fakeEntity); + const canBuild = this.root.logic.checkCanPlaceEntity(this.fakeEntity, {}); // Fade in / out parameters.context.lineWidth = 1; @@ -397,6 +397,42 @@ export class HUDBuildingPlacer extends HUDBuildingPlacerLogic { } } + /** + * Checks if there are any entities in the way, returns true if there are + * @param {Vector} from + * @param {Vector} to + * @returns + */ + checkForObstales(from, to) { + assert(from.x === to.x || from.y === to.y, "Must be a straight line"); + + const prop = from.x === to.x ? "y" : "x"; + const current = from.copy(); + + const metaBuilding = this.currentMetaBuilding.get(); + this.fakeEntity.layer = metaBuilding.getLayer(); + const staticComp = this.fakeEntity.components.StaticMapEntity; + staticComp.origin = current; + staticComp.rotation = 0; + metaBuilding.updateVariants(this.fakeEntity, 0, this.currentVariant.get()); + staticComp.code = getCodeFromBuildingData( + this.currentMetaBuilding.get(), + this.currentVariant.get(), + 0 + ); + + const start = Math.min(from[prop], to[prop]); + const end = Math.max(from[prop], to[prop]); + + for (let i = start; i <= end; i++) { + current[prop] = i; + if (!this.root.logic.checkCanPlaceEntity(this.fakeEntity, { allowReplaceBuildings: false })) { + return true; + } + } + return false; + } + /** * @param {DrawParameters} parameters */ @@ -407,55 +443,73 @@ export class HUDBuildingPlacer extends HUDBuildingPlacerLogic { return; } + const applyStyles = look => { + parameters.context.fillStyle = THEME.map.directionLock[look].color; + parameters.context.strokeStyle = THEME.map.directionLock[look].background; + parameters.context.lineWidth = 10; + }; + + if (!this.lastDragTile) { + // Not dragging yet + applyStyles(this.root.currentLayer); + const mouseWorld = this.root.camera.screenToWorld(mousePosition); + parameters.context.beginCircle(mouseWorld.x, mouseWorld.y, 4); + parameters.context.fill(); + return; + } + const mouseWorld = this.root.camera.screenToWorld(mousePosition); const mouseTile = mouseWorld.toTileSpace(); - parameters.context.fillStyle = THEME.map.directionLock[this.root.currentLayer].color; - parameters.context.strokeStyle = THEME.map.directionLock[this.root.currentLayer].background; - parameters.context.lineWidth = 10; + const startLine = this.lastDragTile.toWorldSpaceCenterOfTile(); + const endLine = mouseTile.toWorldSpaceCenterOfTile(); + const midLine = this.currentDirectionLockCorner.toWorldSpaceCenterOfTile(); + const anyObstacle = + this.checkForObstales(this.lastDragTile, this.currentDirectionLockCorner) || + this.checkForObstales(this.currentDirectionLockCorner, mouseTile); + + if (anyObstacle) { + applyStyles("error"); + } else { + applyStyles(this.root.currentLayer); + } parameters.context.beginCircle(mouseWorld.x, mouseWorld.y, 4); parameters.context.fill(); - if (this.lastDragTile) { - const startLine = this.lastDragTile.toWorldSpaceCenterOfTile(); - const endLine = mouseTile.toWorldSpaceCenterOfTile(); - const midLine = this.currentDirectionLockCorner.toWorldSpaceCenterOfTile(); + parameters.context.beginCircle(startLine.x, startLine.y, 8); + parameters.context.fill(); - parameters.context.beginCircle(startLine.x, startLine.y, 8); - parameters.context.fill(); + parameters.context.beginPath(); + parameters.context.moveTo(startLine.x, startLine.y); + parameters.context.lineTo(midLine.x, midLine.y); + parameters.context.lineTo(endLine.x, endLine.y); + parameters.context.stroke(); - parameters.context.beginPath(); - parameters.context.moveTo(startLine.x, startLine.y); - parameters.context.lineTo(midLine.x, midLine.y); - parameters.context.lineTo(endLine.x, endLine.y); - parameters.context.stroke(); + parameters.context.beginCircle(endLine.x, endLine.y, 5); + parameters.context.fill(); - parameters.context.beginCircle(endLine.x, endLine.y, 5); - parameters.context.fill(); + // Draw arrow + const arrowSprite = this.lockIndicatorSprites[anyObstacle ? "error" : this.root.currentLayer]; + const path = this.computeDirectionLockPath(); + for (let i = 0; i < path.length - 1; i += 1) { + const { rotation, tile } = path[i]; + const worldPos = tile.toWorldSpaceCenterOfTile(); + const angle = Math.radians(rotation); - // Draw arrow - const arrowSprite = this.lockIndicatorSprites[this.root.currentLayer]; - const path = this.computeDirectionLockPath(); - for (let i = 0; i < path.length - 1; i += 1) { - const { rotation, tile } = path[i]; - const worldPos = tile.toWorldSpaceCenterOfTile(); - const angle = Math.radians(rotation); - - parameters.context.translate(worldPos.x, worldPos.y); - parameters.context.rotate(angle); - parameters.context.drawImage( - arrowSprite, - -6, - -globalConfig.halfTileSize - - clamp((this.root.time.realtimeNow() * 1.5) % 1.0, 0, 1) * 1 * globalConfig.tileSize + - globalConfig.halfTileSize - - 6, - 12, - 12 - ); - parameters.context.rotate(-angle); - parameters.context.translate(-worldPos.x, -worldPos.y); - } + parameters.context.translate(worldPos.x, worldPos.y); + parameters.context.rotate(angle); + parameters.context.drawImage( + arrowSprite, + -6, + -globalConfig.halfTileSize - + clamp((this.root.time.realtimeNow() * 1.5) % 1.0, 0, 1) * 1 * globalConfig.tileSize + + globalConfig.halfTileSize - + 6, + 12, + 12 + ); + parameters.context.rotate(-angle); + parameters.context.translate(-worldPos.x, -worldPos.y); } } diff --git a/src/js/game/logic.js b/src/js/game/logic.js index 79104958..49bfb416 100644 --- a/src/js/game/logic.js +++ b/src/js/game/logic.js @@ -53,10 +53,12 @@ export class GameLogic { /** * Checks if the given entity can be placed * @param {Entity} entity - * @param {Vector=} offset Optional, move the entity by the given offset first + * @param {Object} param0 + * @param {boolean=} param0.allowReplaceBuildings + * @param {Vector=} param0.offset Optional, move the entity by the given offset first * @returns {boolean} true if the entity could be placed there */ - checkCanPlaceEntity(entity, offset = null) { + checkCanPlaceEntity(entity, { allowReplaceBuildings = false, offset = null }) { // Compute area of the building const rect = entity.components.StaticMapEntity.getTileSpaceBounds(); if (offset) { @@ -71,7 +73,7 @@ export class GameLogic { const otherEntity = this.root.map.getLayerContentXY(x, y, entity.layer); if (otherEntity) { const metaClass = otherEntity.components.StaticMapEntity.getMetaBuilding(); - if (!metaClass.getIsReplaceable()) { + if (!allowReplaceBuildings || !metaClass.getIsReplaceable()) { // This one is a direct blocker return false; } @@ -116,7 +118,7 @@ export class GameLogic { rotationVariant, variant, }); - if (this.checkCanPlaceEntity(entity)) { + if (this.checkCanPlaceEntity(entity, {})) { this.freeEntityAreaBeforeBuild(entity); this.root.map.placeStaticEntity(entity); this.root.entityMgr.registerEntity(entity); diff --git a/src/js/game/themes/dark.json b/src/js/game/themes/dark.json index 25571700..786cfda2 100644 --- a/src/js/game/themes/dark.json +++ b/src/js/game/themes/dark.json @@ -18,6 +18,10 @@ "wires": { "color": "rgb(74, 237, 134)", "background": "rgba(74, 237, 134, 0.2)" + }, + "error": { + "color": "rgb(255, 137, 137)", + "background": "rgba(255, 137, 137, 0.2)" } }, diff --git a/src/js/game/themes/light.json b/src/js/game/themes/light.json index 3bea6a80..1236d43d 100644 --- a/src/js/game/themes/light.json +++ b/src/js/game/themes/light.json @@ -18,6 +18,10 @@ "wires": { "color": "rgb(74, 237, 134)", "background": "rgba(74, 237, 134, 0.2)" + }, + "error": { + "color": "rgb(255, 137, 137)", + "background": "rgba(255, 137, 137, 0.2)" } }, From 4bfd423efb6fe46eb2016449b1624c824ba2d4c0 Mon Sep 17 00:00:00 2001 From: tobspr Date: Tue, 18 Jan 2022 20:49:11 +0100 Subject: [PATCH 07/17] Fix mod examples --- mod_examples/custom_sub_shapes.js | 2 ++ src/js/changelog.js | 1 + 2 files changed, 3 insertions(+) diff --git a/mod_examples/custom_sub_shapes.js b/mod_examples/custom_sub_shapes.js index d44b84d9..afb901c0 100644 --- a/mod_examples/custom_sub_shapes.js +++ b/mod_examples/custom_sub_shapes.js @@ -32,6 +32,8 @@ class Mod extends shapez.Mod { 0 ); context.closePath(); + context.fill(); + context.stroke(); }, }); diff --git a/src/js/changelog.js b/src/js/changelog.js index a68c8ce4..9165be18 100644 --- a/src/js/changelog.js +++ b/src/js/changelog.js @@ -4,6 +4,7 @@ export const CHANGELOG = [ date: "unreleased", entries: [ "This version adds an official modloader! You can now load mods by placing it in the mods/ folder of the game.", + "When holding shift while placing a belt, the indicator now becomes red when crossing buildings", ], }, { From 5c9cbcd8ad74fe9e2c9d48ae033bafdd184789ba Mon Sep 17 00:00:00 2001 From: tobspr Date: Tue, 18 Jan 2022 21:10:52 +0100 Subject: [PATCH 08/17] Fix linter error --- src/js/mods/mod_interface.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/js/mods/mod_interface.js b/src/js/mods/mod_interface.js index 6fb54be1..5e07c719 100644 --- a/src/js/mods/mod_interface.js +++ b/src/js/mods/mod_interface.js @@ -59,7 +59,7 @@ export class ModInterface { registerCss(cssString) { // Preprocess css - cssString = cssString.replace(/\$scaled\(([^\)]*)\)/gim, (substr, expression) => { + cssString = cssString.replace(/\$scaled\(([^)]*)\)/gim, (substr, expression) => { return "calc((" + expression + ") * var(--ui-scale))"; }); const element = document.createElement("style"); From e16f13034a05b312b5d8fa824cfe0766013bc7cc Mon Sep 17 00:00:00 2001 From: "Thomas (DJ1TJOO)" <44841260+DJ1TJOO@users.noreply.github.com> Date: Tue, 18 Jan 2022 21:11:21 +0100 Subject: [PATCH 09/17] Game state register (#1341) * Added a gamestate register helper Added a gamestate register helper * Update mod_interface.js --- src/js/mods/mod_interface.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/js/mods/mod_interface.js b/src/js/mods/mod_interface.js index 5e07c719..5df35563 100644 --- a/src/js/mods/mod_interface.js +++ b/src/js/mods/mod_interface.js @@ -367,6 +367,14 @@ export class ModInterface { }); } + /** + * Registers a new state class, should be a GameState derived class + * @param {typeof GameState} stateClass + */ + registerGameState(stateClass) { + this.modLoader.app.stateMgr.register(stateClass); + } + /** * @param {object} param0 * @param {"regular"|"wires"} param0.toolbar From 6bf07a43aa667ea9da517e0835379bfc0a12d453 Mon Sep 17 00:00:00 2001 From: tobspr Date: Tue, 18 Jan 2022 21:19:00 +0100 Subject: [PATCH 10/17] export build options --- src/js/core/globals.js | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/js/core/globals.js b/src/js/core/globals.js index 15197880..c47abfed 100644 --- a/src/js/core/globals.js +++ b/src/js/core/globals.js @@ -15,3 +15,20 @@ export function setGlobalApp(app) { assert(!GLOBAL_APP, "Create application twice!"); GLOBAL_APP = app; } + +export const BUILD_OPTIONS = { + HAVE_ASSERT: G_HAVE_ASSERT, + APP_ENVIRONMENT: G_APP_ENVIRONMENT, + TRACKING_ENDPOINT: G_TRACKING_ENDPOINT, + CHINA_VERSION: G_CHINA_VERSION, + WEGAME_VERSION: G_WEGAME_VERSION, + IS_DEV: G_IS_DEV, + IS_RELEASE: G_IS_RELEASE, + IS_MOBILE_APP: G_IS_MOBILE_APP, + IS_BROWSER: G_IS_BROWSER, + IS_STANDALONE: G_IS_STANDALONE, + BUILD_TIME: G_BUILD_TIME, + BUILD_COMMIT_HASH: G_BUILD_COMMIT_HASH, + BUILD_VERSION: G_BUILD_VERSION, + ALL_UI_IMAGES: G_ALL_UI_IMAGES, +}; From 66f085250a5eb69311126f7763ce8fff5f994bde Mon Sep 17 00:00:00 2001 From: tobspr Date: Thu, 20 Jan 2022 14:59:25 +0100 Subject: [PATCH 11/17] Fix runBeforeMethod and runAfterMethod --- src/js/mods/mod_interface.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/js/mods/mod_interface.js b/src/js/mods/mod_interface.js index 5df35563..949e4301 100644 --- a/src/js/mods/mod_interface.js +++ b/src/js/mods/mod_interface.js @@ -424,8 +424,8 @@ export class ModInterface { const oldHandle = classHandle.prototype[methodName]; classHandle.prototype[methodName] = function () { //@ts-ignore Same as above - executeBefore.apply(this, ...arguments); - return oldHandle.apply(this, ...arguments); + executeBefore.apply(this, arguments); + return oldHandle.apply(this, arguments); }; } @@ -442,9 +442,9 @@ export class ModInterface { runAfterMethod(classHandle, methodName, executeAfter) { const oldHandle = classHandle.prototype[methodName]; classHandle.prototype[methodName] = function () { - const returnValue = oldHandle.apply(this, ...arguments); + const returnValue = oldHandle.apply(this, arguments); //@ts-ignore - executeAfter.apply(this, ...arguments); + executeAfter.apply(this, arguments); return returnValue; }; } From 38cca33985993022faff1951110ec4ee7357a8be Mon Sep 17 00:00:00 2001 From: tobspr Date: Thu, 20 Jan 2022 16:05:39 +0100 Subject: [PATCH 12/17] Minor game system code cleanup --- src/js/game/game_system_manager.js | 1 + src/js/game/systems/belt.js | 11 +++++++---- src/js/game/systems/belt_underlays.js | 6 +++--- src/js/game/systems/constant_producer.js | 2 -- src/js/game/systems/display.js | 7 +++---- src/js/game/systems/goal_acceptor.js | 2 -- src/js/game/systems/item_ejector.js | 1 - src/js/game/systems/item_producer.js | 5 ----- src/js/game/systems/lever.js | 9 ++++----- src/js/game/systems/storage.js | 8 ++++---- src/js/game/systems/wire.js | 5 +++-- 11 files changed, 25 insertions(+), 32 deletions(-) diff --git a/src/js/game/game_system_manager.js b/src/js/game/game_system_manager.js index 05085b70..f2d44e52 100644 --- a/src/js/game/game_system_manager.js +++ b/src/js/game/game_system_manager.js @@ -187,6 +187,7 @@ export class GameSystemManager { // 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); + add("beltReader", BeltReaderSystem); add("display", DisplaySystem); diff --git a/src/js/game/systems/belt.js b/src/js/game/systems/belt.js index 00491eff..c19715a4 100644 --- a/src/js/game/systems/belt.js +++ b/src/js/game/systems/belt.js @@ -11,6 +11,7 @@ import { arrayBeltVariantToRotation, MetaBeltBuilding } from "../buildings/belt" import { getCodeFromBuildingData } from "../building_codes"; import { BeltComponent } from "../components/belt"; import { Entity } from "../entity"; +import { GameSystem } from "../game_system"; import { GameSystemWithFilter } from "../game_system_with_filter"; import { MapChunkView } from "../map_chunk_view"; import { defaultBuildingVariant } from "../meta_building"; @@ -22,9 +23,9 @@ const logger = createLogger("belt"); /** * Manages all belts */ -export class BeltSystem extends GameSystemWithFilter { +export class BeltSystem extends GameSystem { constructor(root) { - super(root, [BeltComponent]); + super(root); /** * @type {Object.>} */ @@ -425,8 +426,10 @@ export class BeltSystem extends GameSystemWithFilter { const result = []; - for (let i = 0; i < this.allEntities.length; ++i) { - const entity = this.allEntities[i]; + const beltEntities = this.root.entityMgr.getAllWithComponent(BeltComponent); + + for (let i = 0; i < beltEntities.length; ++i) { + const entity = beltEntities[i]; if (visitedUids.has(entity.uid)) { continue; } diff --git a/src/js/game/systems/belt_underlays.js b/src/js/game/systems/belt_underlays.js index c5c69d26..02707525 100644 --- a/src/js/game/systems/belt_underlays.js +++ b/src/js/game/systems/belt_underlays.js @@ -16,7 +16,7 @@ import { BeltUnderlaysComponent, enumClippedBeltUnderlayType } from "../componen import { ItemAcceptorComponent } from "../components/item_acceptor"; import { ItemEjectorComponent } from "../components/item_ejector"; import { Entity } from "../entity"; -import { GameSystemWithFilter } from "../game_system_with_filter"; +import { GameSystem } from "../game_system"; import { MapChunkView } from "../map_chunk_view"; import { BELT_ANIM_COUNT } from "./belt"; @@ -31,9 +31,9 @@ const enumUnderlayTypeToClipRect = { [enumClippedBeltUnderlayType.bottomOnly]: new Rectangle(0, 0.5, 1, 0.5), }; -export class BeltUnderlaysSystem extends GameSystemWithFilter { +export class BeltUnderlaysSystem extends GameSystem { constructor(root) { - super(root, [BeltUnderlaysComponent]); + super(root); this.underlayBeltSprites = []; diff --git a/src/js/game/systems/constant_producer.js b/src/js/game/systems/constant_producer.js index 5c10b409..a95efdb0 100644 --- a/src/js/game/systems/constant_producer.js +++ b/src/js/game/systems/constant_producer.js @@ -5,10 +5,8 @@ import { ConstantSignalComponent } from "../components/constant_signal"; import { ItemProducerComponent } from "../components/item_producer"; import { GameSystemWithFilter } from "../game_system_with_filter"; import { MapChunk } from "../map_chunk"; -import { GameRoot } from "../root"; export class ConstantProducerSystem extends GameSystemWithFilter { - /** @param {GameRoot} root */ constructor(root) { super(root, [ConstantSignalComponent, ItemProducerComponent]); } diff --git a/src/js/game/systems/display.js b/src/js/game/systems/display.js index f11091b9..65cb3a5c 100644 --- a/src/js/game/systems/display.js +++ b/src/js/game/systems/display.js @@ -2,15 +2,14 @@ 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 { GameSystem } from "../game_system"; 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 { +export class DisplaySystem extends GameSystem { constructor(root) { - super(root, [DisplayComponent]); + super(root); /** @type {Object} */ this.displaySprites = {}; diff --git a/src/js/game/systems/goal_acceptor.js b/src/js/game/systems/goal_acceptor.js index 60d4a984..2ffc3b52 100644 --- a/src/js/game/systems/goal_acceptor.js +++ b/src/js/game/systems/goal_acceptor.js @@ -5,10 +5,8 @@ import { Vector } from "../../core/vector"; import { GoalAcceptorComponent } from "../components/goal_acceptor"; import { GameSystemWithFilter } from "../game_system_with_filter"; import { MapChunk } from "../map_chunk"; -import { GameRoot } from "../root"; export class GoalAcceptorSystem extends GameSystemWithFilter { - /** @param {GameRoot} root */ constructor(root) { super(root, [GoalAcceptorComponent]); diff --git a/src/js/game/systems/item_ejector.js b/src/js/game/systems/item_ejector.js index db37455a..4eef11aa 100644 --- a/src/js/game/systems/item_ejector.js +++ b/src/js/game/systems/item_ejector.js @@ -4,7 +4,6 @@ import { createLogger } from "../../core/logging"; import { Rectangle } from "../../core/rectangle"; import { StaleAreaDetector } from "../../core/stale_area_detector"; import { enumDirection, enumDirectionToVector } from "../../core/vector"; -import { ACHIEVEMENTS } from "../../platform/achievement_provider"; import { BaseItem } from "../base_item"; import { BeltComponent } from "../components/belt"; import { ItemAcceptorComponent } from "../components/item_acceptor"; diff --git a/src/js/game/systems/item_producer.js b/src/js/game/systems/item_producer.js index 0a385907..8ca29ae1 100644 --- a/src/js/game/systems/item_producer.js +++ b/src/js/game/systems/item_producer.js @@ -1,12 +1,7 @@ -/* typehints:start */ -import { GameRoot } from "../root"; -/* typehints:end */ - import { ItemProducerComponent } from "../components/item_producer"; import { GameSystemWithFilter } from "../game_system_with_filter"; export class ItemProducerSystem extends GameSystemWithFilter { - /** @param {GameRoot} root */ constructor(root) { super(root, [ItemProducerComponent]); this.item = null; diff --git a/src/js/game/systems/lever.js b/src/js/game/systems/lever.js index 75b6cf28..343894ae 100644 --- a/src/js/game/systems/lever.js +++ b/src/js/game/systems/lever.js @@ -1,9 +1,8 @@ -import { GameSystemWithFilter } from "../game_system_with_filter"; -import { LeverComponent } from "../components/lever"; -import { BOOL_TRUE_SINGLETON, BOOL_FALSE_SINGLETON } from "../items/boolean_item"; -import { MapChunkView } from "../map_chunk_view"; -import { globalConfig } from "../../core/config"; import { Loader } from "../../core/loader"; +import { LeverComponent } from "../components/lever"; +import { GameSystemWithFilter } from "../game_system_with_filter"; +import { BOOL_FALSE_SINGLETON, BOOL_TRUE_SINGLETON } from "../items/boolean_item"; +import { MapChunkView } from "../map_chunk_view"; export class LeverSystem extends GameSystemWithFilter { constructor(root) { diff --git a/src/js/game/systems/storage.js b/src/js/game/systems/storage.js index 80affac9..cd204448 100644 --- a/src/js/game/systems/storage.js +++ b/src/js/game/systems/storage.js @@ -1,9 +1,9 @@ -import { GameSystemWithFilter } from "../game_system_with_filter"; -import { StorageComponent } from "../components/storage"; import { DrawParameters } from "../../core/draw_parameters"; -import { formatBigNumber, lerp } from "../../core/utils"; import { Loader } from "../../core/loader"; -import { BOOL_TRUE_SINGLETON, BOOL_FALSE_SINGLETON } from "../items/boolean_item"; +import { formatBigNumber, lerp } from "../../core/utils"; +import { StorageComponent } from "../components/storage"; +import { GameSystemWithFilter } from "../game_system_with_filter"; +import { BOOL_FALSE_SINGLETON, BOOL_TRUE_SINGLETON } from "../items/boolean_item"; import { MapChunkView } from "../map_chunk_view"; export class StorageSystem extends GameSystemWithFilter { diff --git a/src/js/game/systems/wire.js b/src/js/game/systems/wire.js index 0491def6..4a255866 100644 --- a/src/js/game/systems/wire.js +++ b/src/js/game/systems/wire.js @@ -21,6 +21,7 @@ import { enumWireType, enumWireVariant, WireComponent } from "../components/wire import { enumPinSlotType, WiredPinsComponent } from "../components/wired_pins"; import { WireTunnelComponent } from "../components/wire_tunnel"; import { Entity } from "../entity"; +import { GameSystem } from "../game_system"; import { GameSystemWithFilter } from "../game_system_with_filter"; import { isTruthyItem } from "../items/boolean_item"; import { MapChunkView } from "../map_chunk_view"; @@ -90,9 +91,9 @@ export class WireNetwork { } } -export class WireSystem extends GameSystemWithFilter { +export class WireSystem extends GameSystem { constructor(root) { - super(root, [WireComponent]); + super(root); /** * @type {Object>} From 670c07cba8a0d8a9b58df22bf7fa3c1eeee289be Mon Sep 17 00:00:00 2001 From: tobspr Date: Thu, 20 Jan 2022 17:07:03 +0100 Subject: [PATCH 13/17] Belt path drawing optimization --- src/js/game/belt_path.js | 162 +++++++++++++++++++++++++++++++++++++-- src/js/game/logic.js | 2 +- 2 files changed, 156 insertions(+), 8 deletions(-) diff --git a/src/js/game/belt_path.js b/src/js/game/belt_path.js index 8ad4f7e3..badcf3cb 100644 --- a/src/js/game/belt_path.js +++ b/src/js/game/belt_path.js @@ -1,7 +1,9 @@ import { globalConfig } from "../core/config"; +import { smoothenDpi } from "../core/dpi_manager"; import { DrawParameters } from "../core/draw_parameters"; import { createLogger } from "../core/logging"; import { Rectangle } from "../core/rectangle"; +import { ORIGINAL_SPRITE_SCALE } from "../core/sprites"; import { clamp, epsilonCompare, round4Digits } from "../core/utils"; import { enumDirection, enumDirectionToVector, enumInvertedDirections, Vector } from "../core/vector"; import { BasicSerializableObject, types } from "../savegame/serialization"; @@ -1430,6 +1432,12 @@ export class BeltPath extends BasicSerializableObject { let trackPos = 0.0; + /** + * @type {Array<[Vector, BaseItem]>} + */ + let drawStack = []; + let drawStackProp = ""; + // Iterate whole track and check items for (let i = 0; i < this.entityPath.length; ++i) { const entity = this.entityPath[i]; @@ -1449,25 +1457,165 @@ export class BeltPath extends BasicSerializableObject { const worldPos = staticComp.localTileToWorld(localPos).toWorldSpaceCenterOfTile(); const distanceAndItem = this.items[currentItemIndex]; + const item = distanceAndItem[1 /* item */]; + const nextItemDistance = distanceAndItem[0 /* nextDistance */]; - distanceAndItem[1 /* item */].drawItemCenteredClipped( - worldPos.x, - worldPos.y, - parameters, - globalConfig.defaultItemDiameter - ); + if (drawStack.length > 1) { + // Check if we can append to the stack, since its already a stack of two same items + const referenceItem = drawStack[0]; + if (Math.abs(referenceItem[0][drawStackProp] - worldPos[drawStackProp]) < 0.001) { + // Will continue stack + } else { + // Start a new stack, since item doesn't follow in row + this.drawDrawStack(drawStack, parameters, drawStackProp); + drawStack = []; + drawStackProp = ""; + } + } else if (drawStack.length === 1) { + const firstItem = drawStack[0]; + + // Check if we can make it a stack + if (firstItem[1 /* item */].equals(item)) { + // Same item, check if it is either horizontal or vertical + const startPos = firstItem[0 /* pos */]; + + if (Math.abs(startPos.x - worldPos.x) < 0.001) { + drawStackProp = "x"; + } else if (Math.abs(startPos.y - worldPos.y) < 0.001) { + drawStackProp = "y"; + } else { + // Start a new stack + this.drawDrawStack(drawStack, parameters, drawStackProp); + drawStack = []; + drawStackProp = ""; + } + } else { + // Start a new stack, since item doesn't equal + this.drawDrawStack(drawStack, parameters, drawStackProp); + drawStack = []; + drawStackProp = ""; + } + } else { + // First item of stack, do nothing + } + + drawStack.push([worldPos, item]); // Check for the next item - currentItemPos += distanceAndItem[0 /* nextDistance */]; + currentItemPos += nextItemDistance; ++currentItemIndex; + if (nextItemDistance > globalConfig.itemSpacingOnBelts + 0.002 || drawStack.length > 20) { + // If next item is not directly following, abort drawing + this.drawDrawStack(drawStack, parameters, drawStackProp); + drawStack = []; + drawStackProp = ""; + } + if (currentItemIndex >= this.items.length) { // We rendered all items + + this.drawDrawStack(drawStack, parameters, drawStackProp); return; } } trackPos += beltLength; } + + this.drawDrawStack(drawStack, parameters, drawStackProp); + } + + /** + * + * @param {HTMLCanvasElement} canvas + * @param {CanvasRenderingContext2D} context + * @param {number} w + * @param {number} h + * @param {number} dpi + * @param {object} param0 + * @param {string} param0.direction + * @param {Array<[Vector, BaseItem]>} param0.stack + * @param {GameRoot} param0.root + * @param {number} param0.zoomLevel + */ + drawShapesInARow(canvas, context, w, h, dpi, { direction, stack, root, zoomLevel }) { + context.scale(dpi, dpi); + context.fillStyle = "rgba(0, 0, 255, 0.1)"; + context.fillRect(1, 1, w - 2, h - 2); + + const parameters = new DrawParameters({ + context, + desiredAtlasScale: ORIGINAL_SPRITE_SCALE, + root, + visibleRect: new Rectangle(-1000, -1000, 2000, 2000), + zoomLevel, + }); + + const itemSize = globalConfig.itemSpacingOnBelts * globalConfig.tileSize; + const item = stack[0]; + const pos = new Vector(itemSize / 2, itemSize / 2); + + for (let i = 0; i < stack.length; i++) { + item[1].drawItemCenteredClipped(pos.x, pos.y, parameters, globalConfig.defaultItemDiameter); + pos[direction] += globalConfig.itemSpacingOnBelts * globalConfig.tileSize; + } + } + + /** + * @param {Array<[Vector, BaseItem]>} stack + * @param {DrawParameters} parameters + */ + drawDrawStack(stack, parameters, directionProp) { + if (stack.length === 0) { + return; + } + + const firstItem = stack[0]; + const firstItemPos = firstItem[0]; + if (stack.length === 1) { + firstItem[1].drawItemCenteredClipped( + firstItemPos.x, + firstItemPos.y, + parameters, + globalConfig.defaultItemDiameter + ); + return; + } + + const itemSize = globalConfig.itemSpacingOnBelts * globalConfig.tileSize; + const inverseDirection = directionProp === "x" ? "y" : "x"; + + const dimensions = new Vector(itemSize, itemSize); + dimensions[inverseDirection] *= stack.length; + + const directionVector = firstItemPos.copy().sub(stack[1][0]); + + const dpi = smoothenDpi(globalConfig.shapesSharpness * parameters.zoomLevel); + + const sprite = this.root.buffers.getForKey({ + key: "beltpaths", + subKey: "stack-" + directionProp + "-" + dpi + "-" + stack.length + firstItem[1].serialize(), + dpi, + w: dimensions.x, + h: dimensions.y, + redrawMethod: this.drawShapesInARow.bind(this), + additionalParams: { + direction: inverseDirection, + stack, + root: this.root, + zoomLevel: parameters.zoomLevel, + }, + }); + + const anchor = directionVector[inverseDirection] < 0 ? firstItem : stack[stack.length - 1]; + + parameters.context.drawImage( + sprite, + anchor[0].x - itemSize / 2, + anchor[0].y - itemSize / 2, + dimensions.x, + dimensions.y + ); } } diff --git a/src/js/game/logic.js b/src/js/game/logic.js index 49bfb416..c7306dfc 100644 --- a/src/js/game/logic.js +++ b/src/js/game/logic.js @@ -58,7 +58,7 @@ export class GameLogic { * @param {Vector=} param0.offset Optional, move the entity by the given offset first * @returns {boolean} true if the entity could be placed there */ - checkCanPlaceEntity(entity, { allowReplaceBuildings = false, offset = null }) { + checkCanPlaceEntity(entity, { allowReplaceBuildings = true, offset = null }) { // Compute area of the building const rect = entity.components.StaticMapEntity.getTileSpaceBounds(); if (offset) { From 8c21d0e8e4cb9b6625592676ae1088317d39ab26 Mon Sep 17 00:00:00 2001 From: tobspr Date: Thu, 20 Jan 2022 17:21:35 +0100 Subject: [PATCH 14/17] Fix belt path optimization --- src/js/game/belt_path.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/js/game/belt_path.js b/src/js/game/belt_path.js index badcf3cb..95124cd7 100644 --- a/src/js/game/belt_path.js +++ b/src/js/game/belt_path.js @@ -1505,7 +1505,7 @@ export class BeltPath extends BasicSerializableObject { currentItemPos += nextItemDistance; ++currentItemIndex; - if (nextItemDistance > globalConfig.itemSpacingOnBelts + 0.002 || drawStack.length > 20) { + if (nextItemDistance > globalConfig.itemSpacingOnBelts + 0.001 || drawStack.length > 20) { // If next item is not directly following, abort drawing this.drawDrawStack(drawStack, parameters, drawStackProp); drawStack = []; @@ -1541,8 +1541,8 @@ export class BeltPath extends BasicSerializableObject { */ drawShapesInARow(canvas, context, w, h, dpi, { direction, stack, root, zoomLevel }) { context.scale(dpi, dpi); - context.fillStyle = "rgba(0, 0, 255, 0.1)"; - context.fillRect(1, 1, w - 2, h - 2); + context.fillStyle = "rgba(0, 0, 255, 0.5)"; + context.fillRect(0, 0, w, h); const parameters = new DrawParameters({ context, @@ -1554,6 +1554,7 @@ export class BeltPath extends BasicSerializableObject { const itemSize = globalConfig.itemSpacingOnBelts * globalConfig.tileSize; const item = stack[0]; + console.log(w, h, dpi, direction, item[1].serialize()); const pos = new Vector(itemSize / 2, itemSize / 2); for (let i = 0; i < stack.length; i++) { From 177450230451422be76129a79105afd711b41755 Mon Sep 17 00:00:00 2001 From: tobspr Date: Thu, 20 Jan 2022 17:37:23 +0100 Subject: [PATCH 15/17] Belt drawing improvements, again --- src/js/core/config.js | 6 +- src/js/core/config.local.template.js | 3 + src/js/game/belt_path.js | 91 +++++++++++++++++----------- 3 files changed, 62 insertions(+), 38 deletions(-) diff --git a/src/js/core/config.js b/src/js/core/config.js index bc2e2460..d64791c8 100644 --- a/src/js/core/config.js +++ b/src/js/core/config.js @@ -42,7 +42,7 @@ export const globalConfig = { // Which dpi the assets have assetsDpi: 192 / 32, assetsSharpness: 1.5, - shapesSharpness: 1.4, + shapesSharpness: 1.3, // Achievements achievementSliceDuration: 10, // Seconds @@ -58,9 +58,11 @@ export const globalConfig = { // Map mapChunkSize: 16, chunkAggregateSize: 4, - mapChunkOverviewMinZoom: 0.9, + mapChunkOverviewMinZoom: 0, mapChunkWorldSize: null, // COMPUTED + maxBeltShapeBundleSize: 20, + // Belt speeds // NOTICE: Update webpack.production.config too! beltSpeedItemsPerSecond: 2, diff --git a/src/js/core/config.local.template.js b/src/js/core/config.local.template.js index c2fe786c..9a432b56 100644 --- a/src/js/core/config.local.template.js +++ b/src/js/core/config.local.template.js @@ -119,5 +119,8 @@ export default { // Allows to load a mod from an external source for developing it // externalModUrl: "http://localhost:3005/combined.js", // ----------------------------------------------------------------------------------- + // Visualizes the shape grouping on belts + // showShapeGrouping: true + // ----------------------------------------------------------------------------------- /* dev:end */ }; diff --git a/src/js/game/belt_path.js b/src/js/game/belt_path.js index 95124cd7..7e2bfaf4 100644 --- a/src/js/game/belt_path.js +++ b/src/js/game/belt_path.js @@ -1460,52 +1460,69 @@ export class BeltPath extends BasicSerializableObject { const item = distanceAndItem[1 /* item */]; const nextItemDistance = distanceAndItem[0 /* nextDistance */]; - if (drawStack.length > 1) { - // Check if we can append to the stack, since its already a stack of two same items - const referenceItem = drawStack[0]; - if (Math.abs(referenceItem[0][drawStackProp] - worldPos[drawStackProp]) < 0.001) { - // Will continue stack - } else { - // Start a new stack, since item doesn't follow in row - this.drawDrawStack(drawStack, parameters, drawStackProp); - drawStack = []; - drawStackProp = ""; - } - } else if (drawStack.length === 1) { - const firstItem = drawStack[0]; - - // Check if we can make it a stack - if (firstItem[1 /* item */].equals(item)) { - // Same item, check if it is either horizontal or vertical - const startPos = firstItem[0 /* pos */]; - - if (Math.abs(startPos.x - worldPos.x) < 0.001) { - drawStackProp = "x"; - } else if (Math.abs(startPos.y - worldPos.y) < 0.001) { - drawStackProp = "y"; + if ( + !parameters.visibleRect.containsCircle( + worldPos.x, + worldPos.y, + globalConfig.defaultItemDiameter + ) + ) { + // this one isn't visible, do not append it + // Start a new stack + this.drawDrawStack(drawStack, parameters, drawStackProp); + drawStack = []; + drawStackProp = ""; + } else { + if (drawStack.length > 1) { + // Check if we can append to the stack, since its already a stack of two same items + const referenceItem = drawStack[0]; + if (Math.abs(referenceItem[0][drawStackProp] - worldPos[drawStackProp]) < 0.001) { + // Will continue stack } else { - // Start a new stack + // Start a new stack, since item doesn't follow in row + this.drawDrawStack(drawStack, parameters, drawStackProp); + drawStack = []; + drawStackProp = ""; + } + } else if (drawStack.length === 1) { + const firstItem = drawStack[0]; + + // Check if we can make it a stack + if (firstItem[1 /* item */].equals(item)) { + // Same item, check if it is either horizontal or vertical + const startPos = firstItem[0 /* pos */]; + + if (Math.abs(startPos.x - worldPos.x) < 0.001) { + drawStackProp = "x"; + } else if (Math.abs(startPos.y - worldPos.y) < 0.001) { + drawStackProp = "y"; + } else { + // Start a new stack + this.drawDrawStack(drawStack, parameters, drawStackProp); + drawStack = []; + drawStackProp = ""; + } + } else { + // Start a new stack, since item doesn't equal this.drawDrawStack(drawStack, parameters, drawStackProp); drawStack = []; drawStackProp = ""; } } else { - // Start a new stack, since item doesn't equal - this.drawDrawStack(drawStack, parameters, drawStackProp); - drawStack = []; - drawStackProp = ""; + // First item of stack, do nothing } - } else { - // First item of stack, do nothing - } - drawStack.push([worldPos, item]); + drawStack.push([worldPos, item]); + } // Check for the next item currentItemPos += nextItemDistance; ++currentItemIndex; - if (nextItemDistance > globalConfig.itemSpacingOnBelts + 0.001 || drawStack.length > 20) { + if ( + nextItemDistance > globalConfig.itemSpacingOnBelts + 0.001 || + drawStack.length > globalConfig.maxBeltShapeBundleSize + ) { // If next item is not directly following, abort drawing this.drawDrawStack(drawStack, parameters, drawStackProp); drawStack = []; @@ -1541,8 +1558,11 @@ export class BeltPath extends BasicSerializableObject { */ drawShapesInARow(canvas, context, w, h, dpi, { direction, stack, root, zoomLevel }) { context.scale(dpi, dpi); - context.fillStyle = "rgba(0, 0, 255, 0.5)"; - context.fillRect(0, 0, w, h); + + if (G_IS_DEV && globalConfig.debug.showShapeGrouping) { + context.fillStyle = "rgba(0, 0, 255, 0.5)"; + context.fillRect(0, 0, w, h); + } const parameters = new DrawParameters({ context, @@ -1554,7 +1574,6 @@ export class BeltPath extends BasicSerializableObject { const itemSize = globalConfig.itemSpacingOnBelts * globalConfig.tileSize; const item = stack[0]; - console.log(w, h, dpi, direction, item[1].serialize()); const pos = new Vector(itemSize / 2, itemSize / 2); for (let i = 0; i < stack.length; i++) { From 6a57448fab7ee6d0058e48a896e043ebe8d51bcb Mon Sep 17 00:00:00 2001 From: tobspr Date: Thu, 20 Jan 2022 17:52:49 +0100 Subject: [PATCH 16/17] =?UTF-8?q?Do=20not=20render=20belts=20in=20statics?= =?UTF-8?q?=20disabled=C2=A0view?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/js/game/systems/belt.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/js/game/systems/belt.js b/src/js/game/systems/belt.js index c19715a4..38ac9b0b 100644 --- a/src/js/game/systems/belt.js +++ b/src/js/game/systems/belt.js @@ -497,6 +497,10 @@ export class BeltSystem extends GameSystem { * @param {MapChunkView} chunk */ drawChunk(parameters, chunk) { + if (G_IS_DEV && globalConfig.debug.doNotRenderStatics) { + return; + } + // Limit speed to avoid belts going backwards const speedMultiplier = Math.min(this.root.hubGoals.getBeltBaseSpeed(), 10); From 30beb7df5fb43ea090e19d03d2ab0930093234f5 Mon Sep 17 00:00:00 2001 From: Bagel03 <70449196+Bagel03@users.noreply.github.com> Date: Thu, 20 Jan 2022 11:54:42 -0500 Subject: [PATCH 17/17] Allow external URL to load more than one mod (#1337) * Allow external URL to load more than one mod Instead of loading the text returned from the remote server, load a JSON object with a `mods` field, containing strings of all the mods. This lets us work on more than one mod at a time or without separate repos. This will break tooling such as `create-shapezio-mod` though. * Update modloader.js --- src/js/mods/modloader.js | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/src/js/mods/modloader.js b/src/js/mods/modloader.js index fa90b8a6..dc64eb6c 100644 --- a/src/js/mods/modloader.js +++ b/src/js/mods/modloader.js @@ -103,21 +103,25 @@ export class ModLoader { mods = await ipcRenderer.invoke("get-mods"); } if (G_IS_DEV && globalConfig.debug.externalModUrl) { - const response = await fetch(globalConfig.debug.externalModUrl, { - method: "GET", - }); - if (response.status !== 200) { - throw new Error( - "Failed to load " + - globalConfig.debug.externalModUrl + - ": " + - response.status + - " " + - response.statusText - ); + let modURLs = Array.isArray(globalConfig.debug.externalModUrl) ? + globalConfig.debug.externalModUrl : [globalConfig.debug.externalModUrl]; + + for(let i = 0; i < modURLs.length; i++) { + const response = await fetch(modURLs[i], { + method: "GET", + }); + if (response.status !== 200) { + throw new Error( + "Failed to load " + + modURLs[i] + + ": " + + response.status + + " " + + response.statusText + ); + } + mods.push(await response.text()); } - - mods.push(await response.text()); } window.$shapez_registerMod = (modClass, meta) => {