From 27db6fe693fc764412a45c025a8d7dccafaa6d7a Mon Sep 17 00:00:00 2001 From: tobspr Date: Tue, 18 Jan 2022 10:13:25 +0100 Subject: [PATCH] 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 zcmaJ_XH-*Lw~cfu(nNt!Lg+Oi)KH~L5$Q$25<=)95JHz;1SE)nQU#<)yYwO*M5PLd z2uLv?BB4v~<>CGAz2E!s?mJ_gv-e(O&AHkfy6(x z6~)D~#L0tjv9RH^tZ=3ndz=pfivp^Km!0us=n?BqzeiMvPC&KxhaExHMfI7PIk&*OBn;Gfx8CE(MiW2 zi!$}UZHDxBLBj38swyBQUxf<-Gzy0R`J!FjJQaME!GH29TaGzgTf>sP-%#yv^W&1AO%;DmInQGfiJ>g z?d%myG`0TO_kU>JJpby`MaLk%2zQ921oU@Je>)l& z{Qs_K^grI7I1|)=^Zh@GJMi$R0_tXje8-*bWgE8kyOyMNC`?O(Bwi)0|bXZycq{kQ2tK)<*DNcZCKkN8n; z7mCMTNE^E;90C9^?&@l)nfXq(Skok%kNTaR2hRB1@1P~!VrS3}qmB%jbiCEX=M-1^ zskSs9v?rq*o1^cTZ*0oOYc;}7GR#URlQ^mwIqPhTsCz9UfY--ckk_eb-+1U7F%E?+ zv<*$1o(B##dalEdGXriW66)5|6{U#lt?lQhlR^f}wGhn}Vp}*U*pvF9t5i>xO2=!8 zjw9(FDThn3{LlbO05v&7*}$^m!Yo5a2Ppc5B%n%&^_bo;Y2rj_`%~4#&gw0=uN!Rs ziFhrB)D9~QxSXJ8FUiCfp}9&c`-Ph^57_+vOAH1nQY8q>Tg#bzyzk-)7&f9b(Mu@w z2QYhd7~=uMRG>A%$JHPw495tW`;pizc^P1BM2usH^P4DjJN`N&j%B z;!-`xuY0bU%cp<%%^xVi6E58mVGN8es-n6PQO`of`gF5*@sox#oe&#{E?hz1>zw!?baj=kFPdZFg0s*1F2O5+|Ww{`xx0hucx7A9Aq&CtTP-z^2rim zIT%zQXcGtgp+RefvUB+!;eVuWF!I~lk&po1zHB@QM-I(rQ!Ix{4eNCm1bjtLy5=bDu-!#Eq5522efDE=FvvrE0sus-~ zZCdEfl04*8{l#ZH&3gV(Pywl5{*A=Ij^+vnTb1U+0qx4PhHt6>+rjSWm2Ufa*;9A@ z@7F7Ac++)SboayIA?zEDoL6PeQ_?G5If$zOTYsjhKq&{+PSQoQ49gE!SmeFtg0_R3 zb$%}Q-rgCtBXi;HsA2}7ldg{ILs~pE+_vf8FkQFJMTf!c-w{)SUR&^LPh5+jirU8v zn&b=E(S<6}2E4M}8jFu#6eOxr94hs^>jntMSLgwZ&9!?(M8|I)-S?gEJ%*iEE_K6_ zp2RFAp_NRAzC$-Yf3T?iaG!$e>2bIQ1M$~q+pJU-@}AKy*n^p!ADz64>!jG;NA@bm zhov38BYlMn+*6I>`v>I(Vbcj%zo;d+-KV7j?bUCZ3GZsvAjEIpJUS&D7Sy0FfFDRl z;yiCH26#>U$5-+$ys{yS&^6Hb=I+bG2W?G4Ff>!zfMeR${o?}V%$;Sas7h;!#lR4M zzy0~TUTap$r|NU`5;M=I{eRg;e<&>qK^ut>$}QWrLpcw>wF_({Jzp6Kqm=6=;p09f zNF-P-p61bBhgsTmI?UH>r5&!Sy!I!k-@)u>XiW*IZ@%i5UErFiis&@HYvo%M{dP|1 z-4&5=^KOlkJ})C)&U8AnI4}r?q@L&U!(K)TRiJ$a&~v*7^Q@9LGb|Y^5GHC_|FCW8Nq>0hrq9M1v`#d95D$O0(h@94$tK_Sfu2C@R`y>G)LdQ-=Qz8Wb1&ih z+q^vvF62${)2oRY5&VX!VoSGgut~L&8&MY%CNguaoQUfX7N0ZQHb=o$IlWPM9J|l^ zTnnM%A(XSCNjT3@Sov(v;mh$HoK@KppIP)<3BwRh!$%wJ8j(9kgXP+xuhm>`Ll4pmbtq@w^r>I4Mlr9Fwtj*WYfG~{CUtC zy2mD1RA-R35o#h5$$HB$|9$P^66Nr;S;=wlDc&f1aEmK{M+-$8+w5H--J}vH!svOW zEcvTAvM@uo7{&-r!PehIKqUDk4DeMl58FzXQCOHut#~~ zK`A$E9 zTzbZ3wrr)OhQKma%Mr9Fe#NlIRK8B6>ilrnZ21^V@MCFfr>hnnD{kds+efCTO=P3# z**{-fu(_1G(0p9T`mKA)oK5$p(&eAaXt@b*-Z z8}yP7Tzf|ol-nTg!^&9wCa_;m^~?!9kO>KQ_;HW%b$>{ak^g6}(&QnEWTnpM##?yL z#;9woX*|mxber_1+`!YjA$N1rxuh876_D?w!5cqQJ2&{WZ#Uq-vp636^?JR_YHKv! z-kI<&m1ju)*smuScB#vlkq5A9i4UxJ{TS{0W zcq6z}W!Of94W`52KF`R>k88~33q^O5%CL1k3m?BmA0GCi?|mOWe#~|?th(dcbBdIH zgjF$xRa)z^>AmrpG{viTYqF{hV3dEUl+vvwCN?6Sf#~153yW3F#y;{alCCHCFdfa1eNvBoKkQ z!5W>F9+aSWp!W$@E5=EvbI;eeaH=CW%S7$!Rfm|th*v2e@h^aYrfOb0-Ls3-7*+ZF zla}g(Xqgag9iy}CBVxPdOysM$x)(coc2rMV=PFzbnCxTuJZ+ej3Zs}Z&0u1gW2l~{ zu~fO1I@0WCEpwtTj%s=sE>Z2?F8a=4nL^bJ63GmGG|ap}A+3<-fq>+5J8~xFaB%RshLB}UO(lAqZ+J2&{0%N>T@m9g-&jI$d zkVyrRj!{sj#QU$q$6}IQm1oI?QsF_Wt06O%iU%iAlsv)vKC0>>oE}Sx{;Vea{1TMt zT3Q5=>Fa%S(F#Xzoxtf|oQ^l}gP0=af&uijv-rrQQni=~zsBu{^UDw&Dz_YeN{A5N zes#DSgf2hFW%fMyBrc%*;@uJ>aWblzGYKC@;F4!jw8a#!YItk1ChOt-S68{a-O zzWX7=$Ww<3pPX{`ZDK8aA|vuKZ1x&JlgO}O7e>Mp8-vVlas>`;-|_M;rjO3=D*8Hi zz18(LdbU-W1Xou(;$+R_+z-y9vA1@R`uLrLLPmR9pZ-b+mCMd@f4!u)3^zz6s8{q^ zV@=aQl5+W^LX=s=+EXPF4Y2R}BqVgROK}t){EChM$7y{Sdl9jOHm}k7f%kD5x}tw_ zNUOF~`36|#3{YlBlPk6U*gnq^c`q_ujGRW*Blr&8bn9Wxy7=A8^l~P?-f}fNzrMV6 zW;*Vut`mInSR%~A<&SPR zJh?2FOpQ{BB91Acvww%VY0SzR#nzu~X*o#uqFLjyi!0vTsX+l%Su@QJG{1Pe_^`iv zCDAV#33v@oN$HO7?ZQ~$vi(kyy+WwhksV}@Tjk%R3gAsC`%ebe9=3fWFDT41*T!qz zW&A2F4cELeG2lMR+P-;D=yZn-IyD%KP@eu)T%=!@;Tm|1}HQ;+rtzJ56I))%RMQr?a0&h0^XIme;Q)*ZV*G}K!zZFyAJ^h}QMYg##vRX{9 zdb+sxZ0%(Q%h`{1;!XQ|j)vCDQ5?$P*PJ-&-p{p1>`-48>4Z>Ni;k^+!6D&N^u7J1 zanp0`P#AXAxP04AS;ItkJR#YMUiW!i-(+*)aE{>{yM`U#4_B+|?~uKeyduI>GnuKB zmwNXXt**z0nK7g7CZye1%i`qW(Ugza4;RTTn=aW{6K2o;*%Gm_hf*yA*V*Q~)CKgz z@}Jq$l)u+7~14^5$yZ3Ot zkT}^I)yh>86L%5&NX}uV?saTbXY&UlV&jT_uxHgAPRG3IDDYBjR^ppLl*gmP+_jd< zzK-b{6-4bu=(95cJ-V+a0gfFt6@@iy7oWI^gXt}6{Z9^8NAJrphJ;7i5lc|Lkn@Q* z^G>k1!T@nE_l}0shKPh``ysORv`MUAR9DWf4Dlyc{qWpmV_vZkH+^P{nZbktR`rf3 zM?a$HL?4{oJt9<`kw*?{jWp)Wm-8Gnbz5b`njOcnUw-@DH)JTR*Y4R+m&1I$V)h+= zC87bq{dp5Tcle@LcF-SZl>0Uu%?q(ax}%p4zxtByvd+`l-o15TN|T@3?( zkDroe7pxBlNG#O!zPnlWR@fL=J6SQX`I>_N)p7GBhg%cbcZ2&o7Yo)zsfSMeTZeZM zrA-{YmgBXpos|!Dno2&J=ibQpQT>!J(PY2pV{u_3P5UP{>n%Sobo|h3uyUx(JNAiI zwc0qkOb{cGBjjuL+4mQVC;Hh!CnuIJ`T>OI3HeXTQOqEs3PvkuzKk!g(6_jh(BzV^ zI(|$ULYV;`5zNL1ttZiUsRr+AyJkR())|Shd$izqS`%}*gSTqb__h8KhhqQ`2w3^d z7vv!P9a4g@myjG*FetE`v9NT&=Lr%^jysHBNhper3oFUBcdpY+qF`;9kGyE-O#IlZ zrGbycFJ_Y|h5CCxod(a?M;OwiJPq@`c4~6b zor8-lPaeE4PWtlG>4ygDfJD!BL(wFZfwyWe87*&gQwz{WfG}JF$OOiFY;DY-oQsn1mK^CYo;E 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()),