From af758ef83742881994089898620194df301423be Mon Sep 17 00:00:00 2001 From: Brent Call <90073067+Brent-Call@users.noreply.github.com> Date: Fri, 17 Jan 2025 17:35:30 -0500 Subject: [PATCH] Persist resource settings (#109) * fix: Broken font references (#108) Every time these themes are loaded, a 400 error is produced for these invalid font requests. * Improve workshop crafting speed tooltip * Make auto-pin Leviathans to persist between save/load cycles * Make hidden pseudo-resources persist between save/load cycles * Set unlocked flag for pseudo-resources * Write mediocre shouldComponentUpdate that works well enough for now * Fix meaningless resPercent if maxValue was zero * Fix misspelling in PA description * Replace dojo.some with Array.prototype.some --------- Co-authored-by: Oliver Salzburg --- js/diplomacy.js | 4 ++ js/jsx/left.jsx.js | 156 ++++++++++++++++++++++++------------------ js/resources.js | 85 ++++++++++++++++++++--- js/workshop.js | 11 ++- res/i18n/en.json | 5 +- res/theme_arctic.css | 2 +- res/theme_factory.css | 2 +- 7 files changed, 184 insertions(+), 81 deletions(-) diff --git a/js/diplomacy.js b/js/diplomacy.js index d64a463ada..0672ffa78c 100644 --- a/js/diplomacy.js +++ b/js/diplomacy.js @@ -237,11 +237,15 @@ dojo.declare("classes.managers.DiplomacyManager", null, { "name", "embassyLevel", "unlocked", "collapsed", "energy", "duration", "pinned" ]) }; + if (this.get("leviathans").autoPinned) { + saveData.diplomacy.autoPinLeviathans = true; + } }, load: function(saveData){ if (saveData.diplomacy) { this.game.bld.loadMetadata(this.races, saveData.diplomacy.races); + this.get("leviathans").autoPinned = saveData.diplomacy.autoPinLeviathans || false; } }, diff --git a/js/jsx/left.jsx.js b/js/jsx/left.jsx.js index 94c4f0902a..0c72eeb1fe 100644 --- a/js/jsx/left.jsx.js +++ b/js/jsx/left.jsx.js @@ -47,54 +47,33 @@ WCollapsiblePanel = React.createClass({ WResourceRow = React.createClass({ getDefaultProperties: function(){ - return {resource: null, isEditMode: false, isRequired: false, showHiddenResources: false}; + return {resource: null, isEditMode: false, isRequired: false, showHiddenResources: false, isTemporalParadox: false}; }, - //this is a bit ugly, probably React.PureComponent + immutable would be a much better approach - //TODO: this function needs a proper rewrite. I'm pretty sure it doesn't work at all as intended. + //I'm going for a solution that is technically less accurate, but is much simpler to understand. + //This function tells React to skip rendering for invisible resources. + //I think it's good enough for now. shouldComponentUpdate: function(nextProp, nextState){ - var oldRes = this.oldRes || {}, - newRes = nextProp.resource; - - var isEqual = - oldRes.value == newRes.value && - oldRes.maxValue == newRes.maxValue && - oldRes.perTickCached == newRes.perTickCached && - this.props.isEditMode == nextProp.isEditMode && - this.props.isRequired == nextProp.isRequired && - this.props.showHiddenResources == nextProp.showHiddenResources && - this.props.isTemporalParadox != nextProp.isTemporalParadox && - true /*this.state.visible == nextState.visible*/; - - if (isEqual){ - return false; + var newVisibility = this.getIsResInMainTable(nextProp.resource, nextProp); + if (this.oldVisibility !== newVisibility) { + //Remember new visibility state + this.oldVisibility = newVisibility; + //Resource will appear/disappear, therefore component should update + return true; } - this.oldRes = { - value: newRes.value, - maxValue: newRes.maxValue, - perTickCached: newRes.perTickCached - }; - return true; + //Update component if it will be displayed + //Don't update component if it won't be displayed + return newVisibility; }, render: function(){ var res = this.props.resource; - if (!res.visible && !this.props.showHiddenResources){ + //Only render this resource if it's unlocked, visible, etc. + if (!this.getIsResInMainTable()) { return null; } - var hasVisibility = (res.unlocked || (res.name == "kittens" && res.maxValue)); - - if (!hasVisibility || (!this.getIsVisible() && !this.props.isEditMode)){ - return null; - } - - //migrate dual resources (e.g. blueprint) to lower table once craft recipe is unlocked - if (game.resPool.isNormalCraftableResource(res) && game.workshop.getCraft(res.name).unlocked){ - return null; - } - //wtf is this code var isTimeParadox = this.props.isTemporalParadox; @@ -207,7 +186,13 @@ WResourceRow = React.createClass({ (!res.visible ? " hidden" : "") ; - var resPercent = ((res.value / res.maxValue) * 100).toFixed(); + var resPercent = ""; + if (res.maxValue) { + resPercent = ((res.value / res.maxValue) * 100).toFixed() + "%/" + game.getDisplayValueExt(res.maxValue); + } else { + //Display value with 1 digit of precision + resPercent = game.getDisplayValueExt(Math.round(res.value), false /*prefix*/, false /*perTickHack*/, 1); + } return $r("div", {role: "row", className: resRowClass}, [ this.props.isEditMode ? @@ -225,7 +210,7 @@ WResourceRow = React.createClass({ className:"res-cell resource-name", style:resNameCss, onClick: this.onClickName, - title: (res.title || res.name) + " " + resPercent + "%/" + game.getDisplayValueExt(res.maxValue) + " " + perTickVal, + title: (res.title || res.name) + " " + resPercent + " " + perTickVal, role: "gridcell", userFocus:"normal", tabIndex: 0, @@ -248,11 +233,35 @@ WResourceRow = React.createClass({ }, toggleView: function(){ - this.props.resource.isHidden = !this.props.resource.isHidden; + game.resPool.setResourceIsHidden(this.props.resource.name, !this.props.resource.isHidden); }, - getIsVisible: function() { - return !this.props.resource.isHidden; + //Parameter is optional. + //If not given, defaults to this.props.resource + getIsVisible: function(res) { + res = res || this.props.resource; + return !res.isHidden; + }, + + //Both parameters are optional. + //If not given, they default to this.props + getIsResInMainTable: function(res, props) { + res = res || this.props.resource; + props = props || this.props; + + if (!res.visible && !props.showHiddenResources){ + return false; + } + var hasVisibility = (res.unlocked || (res.name == "kittens" && res.maxValue)); + if (!hasVisibility || (!this.getIsVisible(res) && !props.isEditMode)){ + return false; + } + //migrate dual resources (e.g. blueprint) to lower table once craft recipe is unlocked + if (game.resPool.isNormalCraftableResource(res) && game.workshop.getCraft(res.name).unlocked){ + return false; + } + //Else, resource is visible && unlocked && not displayed somewhere else instead: + return true; }, componentDidMount: function(){ @@ -405,33 +414,31 @@ WCraftShortcut = React.createClass({ WCraftRow = React.createClass({ getDefaultProperties: function(){ - return {resource: null, isEditMode: false}; + return {resource: null, isEditMode: false, isRequired: false}; }, + //I'm going for a solution that is technically less accurate, but is much simpler to understand. + //This function tells React to skip rendering for invisible resources. + //I think it's good enough for now. shouldComponentUpdate: function(nextProp, nextState){ - var newRes = nextProp.resource; - - /*var isEqual = - oldRes.value == newRes.value && - this.props.isEditMode == nextProp.isEditMode && - this.props.isRequired == nextProp.isRequired && - this.state.visible == nextState.visible; - - if (isEqual){ - return false; - }*/ - this.oldRes = { - value: newRes.value, - }; - return true; + var newVisibility = this.getIsResInCraftTable(nextProp.resource, nextProp); + if (this.oldVisibility !== newVisibility) { + //Remember new visibility state + this.oldVisibility = newVisibility; + //Resource will appear/disappear, therefore component should update + return true; + } + //Update component if it will be displayed + //Don't update component if it won't be displayed + return newVisibility; }, render: function(){ var res = this.props.resource; - var recipe = game.workshop.getCraft(res.name); - var hasVisibility = (res.unlocked && recipe.unlocked /*&& this.workshop.on > 0*/); - if (!hasVisibility || (!this.getIsVisible() && !this.props.isEditMode)){ + + //Only render if this resource is unlocked, not marked as hidden, etc. + if (!this.getIsResInCraftTable()) { return null; } @@ -502,8 +509,10 @@ WCraftRow = React.createClass({ } }, - getIsVisible: function() { - var res = this.props.resource; + //Parameter is optional. + //If not given, defaults to this.props.resource + getIsVisible: function(res) { + res = res || this.props.resource; if (res.name == "wood") { //(Wood is special because it appears twice, separately) return !res.isHiddenFromCrafting; @@ -511,6 +520,20 @@ WCraftRow = React.createClass({ return !res.isHidden; }, + //Both parameters are optional. + //If not given, they default to this.props + getIsResInCraftTable: function(res, props) { + res = res || this.props.resource; + props = props || this.props; + + var recipe = game.workshop.getCraft(res.name); + var hasVisibility = (res.unlocked && recipe.unlocked); + if (!hasVisibility || (!this.getIsVisible(res) && !props.isEditMode)){ + return false; + } + return true; + }, + componentDidMount: function(){ var node = React.findDOMNode(this.refs.perTickNode); if (node){ @@ -542,8 +565,7 @@ WResourceTable = React.createClass({ getInitialState: function(){ return { isEditMode: false, - isCollapsed: false, - showHiddenResources: false + isCollapsed: false }; }, render: function(){ @@ -557,7 +579,7 @@ WResourceTable = React.createClass({ resource: res, isEditMode: this.state.isEditMode, isRequired: isRequired, - showHiddenResources: this.state.showHiddenResources, + showHiddenResources: game.resPool.showHiddenResources, isTemporalParadox: game.calendar.day < 0 }) ); @@ -607,7 +629,7 @@ WResourceTable = React.createClass({ $r("div", {className:"res-toggle-hidden"}, [ $r("input", { type:"checkbox", - checked: this.state.showHiddenResources, + checked: game.resPool.showHiddenResources, onClick: this.toggleHiddenResources, style:{display:"inline-block"}, }), @@ -631,7 +653,7 @@ WResourceTable = React.createClass({ }, toggleHiddenResources: function(e){ - this.setState({showHiddenResources: e.target.checked}); + game.resPool.showHiddenResources = e.target.checked; } }); diff --git a/js/resources.js b/js/resources.js index d914533595..66e354ffd5 100644 --- a/js/resources.js +++ b/js/resources.js @@ -528,6 +528,8 @@ dojo.declare("classes.managers.ResourceManager", com.nuclearunicorn.core.TabMana energyCons: 0, isLocked: false, + showHiddenResources: false, //Whether to show stuff like flux, gflops, & pseudo-resources + hiddenPseudoResources: null, //Array of names of pseudo-resources to mark as hidden constructor: function(game){ this.game = game; @@ -535,6 +537,8 @@ dojo.declare("classes.managers.ResourceManager", com.nuclearunicorn.core.TabMana this.resources = []; this.resourceMap = {}; + this.hiddenPseudoResources = []; + for (var i = 0; i < this.resourceData.length; i++){ var res = dojo.clone(this.resourceData[i]); res.value = 0; @@ -564,30 +568,42 @@ dojo.declare("classes.managers.ResourceManager", com.nuclearunicorn.core.TabMana //put your custom fake resources there getPseudoResources: function(){ - return [ + var pactsManager = this.game.religion.pactsManager; + var pseudoResArr = [ { name: "worship", title: $I("resources.worship.title"), value: this.game.religion.faith, - unlocked: true, - visible: false + unlocked: this.game.religion.faith > 0, + visible: false //Doesn't normally appear in resource table }, { name: "epiphany", title: $I("resources.epiphany.title"), value: this.game.religion.faithRatio, - unlocked: true, + unlocked: this.game.religion.faithRatio > 0 && + this.game.science.get("theology").researched && + !this.game.challenges.isActive("atheism"), visible: false }, { name: "necrocornDeficit", title: $I("resources.necrocornDeficit.title"), - value: this.game.religion.pactsManager.necrocornDeficit, - unlocked: true, + value: pactsManager.necrocornDeficit, + maxValue: pactsManager.fractureNecrocornDeficit, + unlocked: pactsManager.necrocornDeficit > 0 || + //Do we have at least 1 pact purchased? + pactsManager.pacts.some(function(pact) { + return pact.val > 0; + }), visible: false, color: "#E00000"} ]; - //TODO: mixin unlocked and visible automatically + //Apply settings to mark as hidden: + for (var i = 0; i < pseudoResArr.length; i += 1) { + pseudoResArr[i].isHidden = this.hiddenPseudoResources.includes(pseudoResArr[i].name); + } + return pseudoResArr; }, createResource: function(name){ @@ -926,6 +942,8 @@ dojo.declare("classes.managers.ResourceManager", com.nuclearunicorn.core.TabMana resetState: function(){ this.isLocked = false; + this.showHiddenResources = false; + this.hiddenPseudoResources = []; for (var i = 0; i < this.resources.length; i++){ var res = this.resources[i]; res.value = 0; @@ -942,8 +960,22 @@ dojo.declare("classes.managers.ResourceManager", com.nuclearunicorn.core.TabMana save: function(saveData){ saveData.res = { - isLocked: this.isLocked + isLocked: this.isLocked, + showHiddenResources: this.showHiddenResources || undefined }; + //Save flags on which pseudo-resources to hide + //But for sanity purposes, only save a pseudo-resource if it actually exists! + var pseudoResources = this.getPseudoResources(); + for (var i = 0; i < pseudoResources.length; i += 1) { + var resName = pseudoResources[i].name; + if (this.hiddenPseudoResources.includes(resName)) { + //We've found a legit pseudo-resource to be marked as hidden + if (!saveData.res.hiddenPseudoResources) { + saveData.res.hiddenPseudoResources = []; + } + saveData.res.hiddenPseudoResources.push(resName); + } + } }, load: function(saveData){ @@ -951,6 +983,8 @@ dojo.declare("classes.managers.ResourceManager", com.nuclearunicorn.core.TabMana if (saveData.res){ this.isLocked = Boolean(saveData.res.isLocked); + this.showHiddenResources = Boolean(saveData.res.showHiddenResources); + this.hiddenPseudoResources = saveData.res.hiddenPseudoResources || []; } }, @@ -1054,6 +1088,41 @@ dojo.declare("classes.managers.ResourceManager", com.nuclearunicorn.core.TabMana } }, + //Sets the "isHidden" flag of a resource or a pseudo-resource. + //This exists because the UI code can't know if a resource is real or pseudo. + //Wood is a special case because it has 2 flags. We don't handle that special case here. + setResourceIsHidden: function(resName, hide) { + if (typeof(resName) != "string") { + console.error("Can't set res.isHidden; resource name must be a string."); + return; + } + if (typeof(hide) != "boolean") { + console.error("Can't set res.isHidden; flag must be a boolean."); + return; + } + + var res = this.get(resName); + if (res) { + //We have a real resource! + res.isHidden = hide; + return; + } + //Else, assume we're dealing with a pseudo-resource. + if (this.hiddenPseudoResources.includes(resName)) { + if (!hide) { + //Remove by value + this.hiddenPseudoResources = this.hiddenPseudoResources.filter(function(elem) { + return elem != resName; + }); + } + } else { + //Pseudo-resource is not already in array. + if (hide) { + this.hiddenPseudoResources.push(resName); + } + } + }, + toggleLock: function(){ this.isLocked = !this.isLocked; }, diff --git a/js/workshop.js b/js/workshop.js index 35209de16a..eb7d2f820f 100644 --- a/js/workshop.js +++ b/js/workshop.js @@ -2865,8 +2865,15 @@ dojo.declare("com.nuclearunicorn.game.ui.CraftButtonController", com.nuclearunic } if (craft.value != 0) { - var countdown = (1 / (this.game.workshop.getEffectEngineer(craft.name, false) * this.game.getTicksPerSecondUI())).toFixed(0); - desc += "
=> " + $I("workshop.craftBtn.desc.countdown", [countdown]); + var craftsPerSecond = (this.game.workshop.getEffectEngineer(craft.name, false) * this.game.getTicksPerSecondUI()); + if (craftsPerSecond <= 0) { + desc += "
=> " + $I("workshop.craftBtn.desc.countdown", ["∞"]); + } else if (craftsPerSecond <= 0.5) { + var countdown = (1 / craftsPerSecond).toFixed(0); + desc += "
=> " + $I("workshop.craftBtn.desc.countdown", [countdown]); + } else { + desc += "
=> " + $I("workshop.craftBtn.desc.craftsPerSecond", [this.game.getDisplayValueExt(craftsPerSecond)]); + } } } return desc; diff --git a/res/i18n/en.json b/res/i18n/en.json index 9ebc65d059..ab4a785846 100644 --- a/res/i18n/en.json +++ b/res/i18n/en.json @@ -336,7 +336,7 @@ "challendge.pacifism.effect.desc": "You gain improved trade based on trade posts.", "challendge.postApocalypse.label": "Post Apocalypse", "challendge.postApocalypse.desc": "You found a broken timeline with extreme pollution. You can send your strongest kittens to clear this mess. Pollution has more negative effects.
WARNING: you have limited time to establish food production after starting this challenge.", - "challendge.postApocalypse.effect.desc": "Increases amount of cryochaimbers that can be operational. Unlocks special policies for this run.", + "challendge.postApocalypse.effect.desc": "Increases amount of cryochambers that can be operational. Unlocks special policies for this run.", "challendge.postApocalypse.flavor": "'Surely it's not that bad' they thought", "challendge.reservesReclaimed.msg": "Reserves were reclaimed!", "challendge.unicornTears.label": "Unicorn Tears", @@ -2192,8 +2192,9 @@ "workshop.concreteWarehouses.label": "Concrete Warehouses", "workshop.concreteWarehouses.desc": "Storage facilities store 35% more resources", "workshop.craft.effectiveness": "Craft effectiveness: +{0}%", - "workshop.craftBtn.desc.countdown": "One craft every: {0}sec", + "workshop.craftBtn.desc.countdown": "One craft every: {0} sec", "workshop.craftBtn.desc.craftRatio": "Engineers expertise", + "workshop.craftBtn.desc.craftsPerSecond": "{0} crafts per second", "workshop.craftBtn.desc.effectivenessBonus": "Craft effectiveness bonus: +{0}%", "workshop.craftBtn.desc.progressHandicap": "Craft difficulty", "workshop.craftBtn.desc.tier": "Engineer's optimal rank", diff --git a/res/theme_arctic.css b/res/theme_arctic.css index 35a1b74b51..08c95c963d 100644 --- a/res/theme_arctic.css +++ b/res/theme_arctic.css @@ -18,7 +18,7 @@ /* ************* */ /* *** FONTS *** */ /* ************* */ -@import url(https://fonts.googleapis.com/css?family=Source+Sans+KR:wght@300,400,700&display=swap); +@import url(https://fonts.googleapis.com/css?family=Source+Sans+Pro:wght@300,400,700&display=swap); /* ******************************************** */ /* ******************* GAME ******************* */ diff --git a/res/theme_factory.css b/res/theme_factory.css index 9f9d14435b..fe2f7a450b 100644 --- a/res/theme_factory.css +++ b/res/theme_factory.css @@ -5,7 +5,7 @@ /* ************* */ /* *** FONTS *** */ /* ************* */ -@import url("https://fonts.googleapis.com/css2?family=Libre+Franklin:wght,CASL@,300..700,0..1&display=swap"); +@import url("https://fonts.googleapis.com/css2?family=Libre+Franklin:ital,wght@0,100..900;1,100..900&display=swap"); /* ******************************************** */ /* ******************* GAME ******************* */