From 3f8de45f2a6b597189808c528795facb00d7a11c Mon Sep 17 00:00:00 2001 From: Nif Date: Sun, 3 Mar 2024 11:53:50 +0000 Subject: [PATCH] Initial commit --- LICENSE | 21 + Old Things/2.0-format-changes.md | 27 + Prestige-tree-license | 21 + README.md | 7 + changelog.md | 542 +++++ css/bars.css | 38 + css/components.css | 166 ++ css/general-style.css | 41 + css/misc.css | 83 + css/other-tabs.css | 111 ++ css/popup.css | 52 + css/system-style.css | 251 +++ css/tree-node.css | 108 + demo.html | 140 ++ discord.png | Bin 0 -> 1990 bytes docs/!general-info.md | 59 + docs/achievements.md | 54 + docs/bars.md | 40 + docs/basic-layer-breakdown.md | 38 + docs/buyables.md | 86 + docs/challenges.md | 80 + docs/clickables.md | 63 + docs/custom-tab-layouts.md | 85 + docs/grids.md | 70 + docs/infoboxes.md | 32 + docs/layer-features.md | 192 ++ docs/main-mod-info.md | 63 + docs/milestones.md | 42 + docs/other.md | 8 + docs/particles.md | 63 + docs/subtabs-and-microtabs.md | 59 + docs/trees-and-tree-customization.md | 54 + docs/tutorials/getting-started.md | 48 + docs/tutorials/making-a-mod.md | 104 + docs/tutorials/updating-tmt.md | 21 + docs/upgrades.md | 70 + index.html | 139 ++ js/Demo/demoMod.js | 83 + js/Demo/demoTree.js | 55 + js/Demo/layers/a.js | 65 + js/Demo/layers/c.js | 396 ++++ js/Demo/layers/f.js | 147 ++ js/components.js | 665 +++++++ js/game.js | 430 ++++ js/layers.js | 28 + js/mod.js | 79 + js/technical/break_eternity.js | 2742 ++++++++++++++++++++++++++ js/technical/canvas.js | 88 + js/technical/displays.js | 195 ++ js/technical/layerSupport.js | 294 +++ js/technical/loader.js | 9 + js/technical/particleSystem.js | 186 ++ js/technical/systemComponents.js | 220 +++ js/technical/temp.js | 179 ++ js/tree.js | 23 + js/utils.js | 412 ++++ js/utils/NumberFormating.js | 111 ++ js/utils/easyAccess.js | 75 + js/utils/options.js | 78 + js/utils/save.js | 322 +++ js/utils/themes.js | 51 + options_wheel.png | Bin 0 -> 4419 bytes remove.png | Bin 0 -> 6017 bytes resources/genericParticle.png | Bin 0 -> 65336 bytes 64 files changed, 10011 insertions(+) create mode 100644 LICENSE create mode 100644 Old Things/2.0-format-changes.md create mode 100644 Prestige-tree-license create mode 100644 README.md create mode 100644 changelog.md create mode 100644 css/bars.css create mode 100644 css/components.css create mode 100644 css/general-style.css create mode 100644 css/misc.css create mode 100644 css/other-tabs.css create mode 100644 css/popup.css create mode 100644 css/system-style.css create mode 100644 css/tree-node.css create mode 100644 demo.html create mode 100644 discord.png create mode 100644 docs/!general-info.md create mode 100644 docs/achievements.md create mode 100644 docs/bars.md create mode 100644 docs/basic-layer-breakdown.md create mode 100644 docs/buyables.md create mode 100644 docs/challenges.md create mode 100644 docs/clickables.md create mode 100644 docs/custom-tab-layouts.md create mode 100644 docs/grids.md create mode 100644 docs/infoboxes.md create mode 100644 docs/layer-features.md create mode 100644 docs/main-mod-info.md create mode 100644 docs/milestones.md create mode 100644 docs/other.md create mode 100644 docs/particles.md create mode 100644 docs/subtabs-and-microtabs.md create mode 100644 docs/trees-and-tree-customization.md create mode 100644 docs/tutorials/getting-started.md create mode 100644 docs/tutorials/making-a-mod.md create mode 100644 docs/tutorials/updating-tmt.md create mode 100644 docs/upgrades.md create mode 100644 index.html create mode 100644 js/Demo/demoMod.js create mode 100644 js/Demo/demoTree.js create mode 100644 js/Demo/layers/a.js create mode 100644 js/Demo/layers/c.js create mode 100644 js/Demo/layers/f.js create mode 100644 js/components.js create mode 100644 js/game.js create mode 100644 js/layers.js create mode 100644 js/mod.js create mode 100644 js/technical/break_eternity.js create mode 100644 js/technical/canvas.js create mode 100644 js/technical/displays.js create mode 100644 js/technical/layerSupport.js create mode 100644 js/technical/loader.js create mode 100644 js/technical/particleSystem.js create mode 100644 js/technical/systemComponents.js create mode 100644 js/technical/temp.js create mode 100644 js/tree.js create mode 100644 js/utils.js create mode 100644 js/utils/NumberFormating.js create mode 100644 js/utils/easyAccess.js create mode 100644 js/utils/options.js create mode 100644 js/utils/save.js create mode 100644 js/utils/themes.js create mode 100644 options_wheel.png create mode 100644 remove.png create mode 100644 resources/genericParticle.png diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..eb81bae --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Modding Tree Copyright (c) 2020 Acamaeda + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Old Things/2.0-format-changes.md b/Old Things/2.0-format-changes.md new file mode 100644 index 0000000..8d712f1 --- /dev/null +++ b/Old Things/2.0-format-changes.md @@ -0,0 +1,27 @@ +# 2.0 format changes + +- Temp format is changed from `temp.something[layer]` to `temp[layer].something`, for consistency +- Challenges are now saved as an object with the amount of completions in each spot. (This will break saves.) +- `effectDisplay` in Challenges and Upgrades no longer takes an argument, and neither does `effect` for Buyables +- Buyable cost can take an argument for amount of buyables, but it needs to function if no argument is supplied (it should do the cost for the next purchase). +- Generation of Points now happens in the main game loop (not in a layer update function), enabled by `canGenPoints` in [game.js](js/game.js). +- Changed `fullLayerReset` to `layerDataReset`, which takes an array of names of values to keep + +In addition, many names were changed, mostly expanding abbreviations: + +All instances of: +- chall -> challenge +- unl -> unlocked +- upg -> upgrade (besides CSS) +- amt -> amount +- desc -> description +- resCeil -> roundUpCost +- order -> unlockOrder +- incr_order -> increaseUnlockOrder + +Challenges: +- desc -> challengeDescription +- reward -> rewardDescription +- effect -> rewardEffect +- effectDisplay -> rewardDisplay +- active -> challengeActive diff --git a/Prestige-tree-license b/Prestige-tree-license new file mode 100644 index 0000000..888b2b0 --- /dev/null +++ b/Prestige-tree-license @@ -0,0 +1,21 @@ +MIT License + +Prestige Tree Copyright (c) 2020 Jacorb + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..7e9f5dd --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# The-Modding-Tree + +An incremental game engine based on The Prestige Tree. It still requires programming knowledge, but it's mostly pretty easy things and copy/pasting. + +[Look here for a tutorial on getting started with modding with TMT](docs/tutorials/getting-started.md) + +You can look in the [documentation](docs/!general-info.md) for more information on how it all works, or look at the code in layers.js to see what it all looks like. \ No newline at end of file diff --git a/changelog.md b/changelog.md new file mode 100644 index 0000000..91fb07b --- /dev/null +++ b/changelog.md @@ -0,0 +1,542 @@ +# The Modding Tree changelog: + +# v2.6.6.2 = 9/9/21 +- nodeStyle can now be used to set fonts. + +# v2.6.6.1 = 9/8/21 +- Fixed options not updating when new ones are added. + +# v2.6.6 - 9/7/21 +- Added option for shift-clicking nodes toggling their tooltips. +- Fixed NaN check for setting Decimal values with text boxes. +- Added display-image, h-line, and v-line to documentation. +- Fixed an issue with subtab glow colors. +- Locked/hidden subtabs can't cause node glowing. +- Fixed being able to buy upgrades on deactivated layers. +- Updated break_eternity library. +- Cleaned up buyable/clickable code. + +# v2.6.5.1 - 7/13/21 +- Fixed offline production more. + +# v2.6.5 - 7/7/21 +- Fixed offline production. +- Fixed formatting for small negative numbers. +- Fixed divide by zero when a 0-second tick occurs. +- "deactivated" now also affects achievement/milestone unlocking. +- Locked challenges cannot be entered. +- Fixed a bug with subtab glow colors. + +# v2.6.4.2 - 6/17/21 +- Fixed a bug with the endgame screen. +- Fixed hotkey-related crash. +- Fixed resetting not working correctly. + +# v2.6.4 - 6/17/21 +- The game now autosaves before closing, if autosave is on. (Thank you to thepaperpilot for this!) +- More Anti-NaN safety. +- Fixed challenges glowing from countsAs. +- Improved tooltip centering (thanks to Scarlettt!) +- canReset now works properly for non-custom layers. +- Fixed baseAmount being set to 0 even when a layer resets nothing. +- Fixed centering on tooltips. +- Changed some default values on startup to prevent potential issues. +- Cleaned up resetting. + +# v2.6.3 - 6/11/21 +- Added better support for using multiple layer files and similar. See modFiles in modInfo. +- The demo now has each layer in its own file as well. + +# v2.6.2.2 - 6/10/21 +- Fixed an error message regarding popup.css. + +# v2.6.2.1 - 6/10/21 +- Fixed a visual bug with milestones. + +## v2.6.2 - 6/10/21 +- Broke up style.css into many files to make it easier to find and customize what matters. If you already have custom CSS, keep that and ignore the new ones maybe? +- Added buyable and clickable trees. +- Added optional tooltips to upgrades, buyables, clickables, milestones, and gridables. +- Fixed the passiveGeneration display. +- Fixed "marked" feature. +- doReset now will function on non-numeric rows besides "side". + +## v2.6.1 - 6/7/21 +- Added global background style to mod.js. +- Tree branches can have custom line widths. +- If an upgrade has both canAfford and cost, it checks both. (So you can use canAfford for other things) +- Releasing a held buyable/clickable with onHold doesn't click it again. +- Fixed hard resetting while NaN'ed and exporting NaN saves for debugging. +- Attempt to fix buttons sometimes not updating. +- Added "instant" feature for bars. (not useful for most people) +- Improvements to theme code, partially by Cubedey. + +## v2.6.0.1 - 6/4/21 +- Removed excess NaN alerts (now only checks player, not temp). +- Fixed background images covering up tree branches. + +## v2.6: Fixed Reality - 6/3/21 +- Fixed issues with NaN checking. The game also will not save if the save is broken. +- Added a drop-down menu component! +- Added upgrade-tree component! +- Options are now saved separately, and not affected by hard resetting or importing saves. +- Fixed demo.html +- Fixed branches not working on the right tab. +- Fixed background color not working on the left tab. +- Fixed branches not updating when tree tab is not shown. +- You can now use "this" in tabFormat! +- Added per-row displaying for achievements, challenges, milestones, grids, buyables, and clickables. THIS WILL BREAK BUYABLES/CLICKABLES THAT PREVIOUSLY USED THEIR TABFORMAT ARGUMENT FOR SIZE. +- Added onComplete for milestones. +- Added addBuyables. +- The prestige/sec display now shows non-whole numbers. +- resetsNothing now works immediately on a reset that enables it. +- Made the star on maxed challenges larger. + +- diff can no longer be negative. +- Fixed challenges with no currencyDisplayName using "points" instead of the mod's pointsName. +- inChallenge no longer can return undefined. +- Fixed certain things skipping negative rows (now they are treated like non-numeric rows, and don't appear in the tree still). +- Things are 0.2% more optimized. +- Fixed problems in the documentation. +- Added more customization to the "mark" component (but not an easy way to access it) + + +### v2.5.11.1 - 5/27/21 +- Fixed issues caused when the tree tab is disabled. + +### v2.5.11 - 5/27/21 +- Finished part 1 of the "making a mod" tutorial. +- The challenge that you are currently in is highlighted, and will not be hidden if "hide completed challenges" is on and it is already completed. +- Added leftTab, which makes a layer use the left tab instead of the right one (good for trees-within-trees and such) +- Added startNavTab, which lets you choose which tab starts out on the left side. +- Fixed the infobox not appearing in default tabFormat. +- Fixed upgrade/buyable layering when they are hovered over. +- Fixed devSpeed being applied twice. + +### v2.5.10.2 - 5/24/21 +- Fixed some things in the tree tab not being clickable. + +### v2.5.10.1 - 5/23/21 +- Actually fixed the tooltip issue. + +### v2.5.10 - 5/22/21 +- Tooltips can now show over the top overlay again. +- Tweaked number formatting (e1000's keep the decimal places on the mantissa.) +- Fixed text on two settings buttons not changing. +- Started making a new tutorial. + +### v2.5.9.2 - 5/19/21 +- Fixed many issues with things not updating. + +### v2.5.9.1 - 5/18/21 +- Made text inputs never give NaNs. + +### v2.5.9 - 5/18/21 +- Fixed issue when using text inputs for Numbers. +- Added particle color feature. +- Particle speed and dir are updated as it moves. +- Added setSpeed and setDir for particles. +- Added more trig functions. + +### v2.5.8 - 5/17/21 +- Added makeShinies, which creates a stationary particle in a random spot. +- Bars will visually update more quickly. +- Fixed a major particle-related issue. +- Fixed autoUpgrade. +- Fixed a minor visual issue with tree nodes. + +### v2.5.7 - 5/15/21 +- Added a particle system! Not only can it be used for visual effects, but particles can interact with the mouse. They could be used to create golden cookies or collectables, for example. +- Added marked feature to buyables, clickables, and challenges. By default, stars multi-completion challenges when maxed. +- Added 'deactivated' feature to layers, which disables many features. +- Improved number formatting slightly. + +### v2.5.6 - 5/14/21 +- You can now use non-numeric ids for upgrades, buyables, etc. +- Fixed an exploit that let you buy an extra buyable. +- Moved basic getter/setter functions to easyAccess.js. + +### v2.5.5.2 - 5/12/21 +- Fixed a major issue with buyables. +- Fixed a variety of tabFormat-related issues. +- Fixed commas appearing in decimal places (thanks to pg132!) + +### v2.5.5.1 - 5/12/21 +- Fixed clickables. + +### v2.5.5 - 5/12/21 +- Added grids! They are a grid of buttons which behave the same, but have their own data. Good for inventory grids, map tiles, and more! +- Added "marked" feature to add a mark to a node. Can be an image instead of a star. (Originally by Jacorb) +- Added "layer-proxy" component that lets you use components from another layer. +- Added the ability to display non-whole numbers in main-display. + +### v2.5.4 - 5/10/21 +- Added a setting to always use single-tab mode. +- Added directMult, which multiplies prestige gain after exponents and softcaps. It actually multiplies gain for static layers. +- Added onEnter and onExit for challenges. +- Improved displaying numbers between 0.0001 and 0.1. +- Added documentation on how gainMult/Exp work for static layers. +- Fixed a visual issue on mobile, thanks to thepaperpilot. +- Improved documentation in general. + +### v2.5.3 - 5/8/21 +- Improved performance of tab formats and bars. +- Respec confirmation settings are now kept on resets. +- Improved compatibility with older browsers. +- Fixed missing pixel on vertical bars. + +### v2.5.2.1 - 5/7/21 +- Fixed microtabs making layers highlight incorrectly. + +### v2.5.2 - 5/7/21 +- Added glowColor for subtabs. +- Improved the display for extremely small numbers. +- Fixed issues in the buyable docs. + +### v2.5.1 - 5/7/21 +- Fixed dynamic things in tabFormat not updating. + +## v2.5: Dreams Really Do Come True - 5/7/21 +- Optimizations, hopefully a significant amount. +- Added OOM/s point gen display at high values (thanks to Ducdat!) +- Only one tab will display if the window is not wide enough (also thanks to Ducdat!) +- Holding down a buyable's button now buys it continuously. +- New milestone setting will also show the most recently unlocked milestone. (Also renamed all settings to be clearer) +- Added an onHold feature for clickables. +- Layer nodes will be highlighted even if the player is on the same tab. +- Added customizable node glowColor. +- Added buyable purchaseLimit. +- Amount is automatically supplied to buyable cost and effect functions. +- Locked (not yet visible) milestones no longer take up space. Also fixed hidden milestones taking a tiny bit of space. +- Re-centered respec buttons. +- Force-displayed tooltips are not hidden by resets. +- Added formatting support for very small numbers. Disabled in most places by default because rounding errors might cause issues. Access it with formatSmall, or enable it globally by adding "allowSmall: true" to modInfo. + + +### v2.4.1 - 4/29/21 +- A number of minor fixes, many thanks to thepaperpilot. +- The respec confirmation checkbox is now part of the respec-button component. + (This also fixes the checkbox appearing when there is no respec button) +- Added a few undocumented changes to the 2.4 changelog (the two at the bottom) + +## v2.4: Rationalized Edition - 4/29/21 +- Completely reworked tooltips. Shift-click a node to force its tooltip to stay displayed. (And hopefully finally fixed flickering!) +- Added text-input and slider components. +- Added the ability to toggle respec confirmations. +- Added custom respec confirmation messages. +- The red layer highlight will not appear before a layer is unlocked. +- Added unlocking hotkeys. +- You no longer need to supply 'rows' and 'cols' for any Big Features. +- Node symbols can use HTML. +- Added documentation for the respec button. +- Added prestigeNotify to subtabs, and prestigeNotify in subtabs also highlights the layer node. +- The version number no longer contains special characters or irrational numbers. + +- Added ctrlDown and shiftDown variables. +- Tooltips now use HTML (this means you need to replace any newlines with
) + + +### v2.π.1 - 4/7/21 +- Fixed formatting for some larger numbers. +- Upgrades will expand if there is too much text to display. +- Fixed styling challenges. +- No longer attempts to display a base currency when there is none. + +## v2.π: Incrementally Updated - 2/5/21 +- Performance improvements. +- Fixed tooltips overlapping with the top display. +- Clicking a popup dismisses it immediately. +- Added support for bulk challenge completions. +- "Best" is updated automatically. +- Fixed keeping Decimal values on reset. +- Code reorganization and style improvements by fudo. + + +### v2.3.5 - 12/21/20 +- Added resetTime, which tracks the time since a layer prestiged or was reset. +- A layer node will be highlighted red if one of its subtabs is highlighted red. +- Fixed issues with keeping challenges, buyables, and clickables on reset. +- Improved the unlocking of custom layers. +- Other minor fixes. + +### v2.3.4 - 12/16/20 +- Added a node image feature. +- Resource display now always shows the amount of the currency the layer's gain is based on. +- Added spacing between tree nodes. +- Another attempt to fix tooltip flickering. + +### v2.3.3 - 12/13/20 +- Fixed the first node in a row always taking up space. +- layerShown is now optional. +- All prestige types can now use features for custom prestige types. + +### v2.3.2 - 12/13/20 +- Fixed achievement/milestone popups. + +### v2.3.1 - 12/12/20 +- Another attempt to fix flickering tooltips. +- The "this" keyword should work everywhere except tabFormat arrays (although I may have missed some things). +- Fixed tree branches not updating when scrolling on the right-side tab. +- Fixed a spacing issue when a node's symbol is "" +- Removed some old, unneeded files. + +## v2.3: Cooler and Newer Edition - 12/10/20 +- Added achievement/milestone popups (thank you to Jacorb for this contribution!) +- The changelog tab is back, and can be set in mod.js. +- Layer nodes and respec buttons will not be clicked by pressing "enter". +- Possible fix for flickering tooltips and strange transitions. +- The victory screen text is configurable. +- Added image and textStyle features to achievements. +- Added an argument to use specific rows in an "upgrades" component. +- Fixed the comma appearing in the main display when there was no effectDescription +- Added the ability to easily make a tab that is a collection of layers in subtabs. +- Improved spacing for embedding layers with subtabs into subtabs. + + +### v2.2.8 - 12/03/20 +- Double-clicking a layer node brings you to the main subtab for that layer. +- Attempted to fix challenges visually updating a different way. +- Added a softcap function for use in formulas. +- Added displayRow feature, which lets layers be shown somewhere separate from where they are in the reset order (e.g. side layers) +- Fixed autoupgrade issue. + +### v2.2.7 - 11/30/20 +- Added autoUpgrade feature. +- resource-display now shows resource gain per second if passiveGain is active. +- Fixed formatting issues on some large numbers. +- Better support for using classed objects in player and in layers/tmp. +- Made hard resetting more effective. +- Removed Herobrine from getStartClickables. + +### v2.2.6 - 11/30/20 +- Added goalDescription for challenges and made the new "canComplete" system the standard. +- Another attempt to fix challenges not visually updating. +- Fixed side layers not appearing. +- Fixed getStartClickables again. + +### v2.2.5 - 11/29/20 +- Added features for overriding the displays and costs/goals of upgrades and challenges to make them fully custom. +- best, total, and unlocked are always automatically added to layerData (but best and total will only display if you add them yourself). +- Fixed getStartClickables. + +### v2.2.4 - 11/28/20 +- Added softcap and softcapPower features (for Normal layers) +- Offline time limit and default max tick length were fixed (previously the limits were 1000x too large) +- Added fixOldSaves. +- You can use HTML in main-display. +- Fixed a number of minor oddities. + +### v2.2.3 - 11/28/20 +- Layers will be highlighted if you can finish a challenge. +- The "can complete challenge" color now overrides the "already completed" color. +- Button nodes now work as side "layers". +- Setting a tooltip to "" hides it entirely. + +### v2.2.2 - 11/22/20 +- Fixed right half of the screen being unclickable in some circumstances. +- Fixed tree branches being offset. +- Fix to lastSafeTab. + +### v2.2.1 - 11/7/20 +- Added a small highlight to layers you can meaningfully prestige on. +- Added passiveGeneration and autoPrestige features to standardize prestige automation. (The old ways still work, but the new ones work better with other things) +- Improved milestones visually a bit. +- "best" and "total" are now only displayed if present in startData. +- Fixed issues with things not updating visually. (Thank you to to Jacorb!) +- Side layers and button nodes can now be highlighted. +- Updated docs on the new tree-related features. + +## v2.2: Uprooted - 11/7/20 +- You can now embed a layer inside of a subtab or microtab! +- Added support for hiding or reformatting the tree tab +- Added non-layer button nodes +- Added shouldNotify to subtab/microtab buttons. (You can make them highlighted) +- Added commas to large exponents. +- Upgrades now only show "currently" if they have an effectDisplay (so not for constant effects). +- Achievements are part of the default tab format. +- NaN is now handled more intelligently. +- Renamed files, and moved less relevant ones to another folder. +- The "hide completed challenges" setting now only hides challenges at max completions. +- Thank you to thepaperpilot for fixing errors in docs and improving the infobox appearance! +- Many other minor fixes. + + +### v2.1.4 - 10/25/20 +- Added an infobox component. Thank you to thepaperpilot for this contribution! +- Layer type is now optional, and defaults to "none". +- Improved the look of bars and tab buttons. +- Improved spacing between layer nodes (also thanks to thepaperpilot!) +- Fixed the "blank" component breaking if only specifying the height. +- Fixed some numbers not displaying with enough digits. +- Made a few more things able to be functions. +- A few other minor fixes. + +### v2.1.3.1 - 10/21/20 +- Fixed the update function. + +### v2.1.3 - 10/21/20 +- gainMult and gainExp are now optional. +- Layer unlocking is now kept on reset. +- Game should start up faster. +- Layer updates now have a determined order and starts with earlier-rowed layers. +- Automation now has a determined order and starts with later-rowed layers. +- Fixed issues with resetting clickables and challenges. +- Commas should no longer appear in the decimal places of a number. +- Fixed potential issue in displaying the tree. + +### v2.1.2 - 10/19/20 +- Added buyUpgrade function (buyUpg still works though) +- Added author name to modInfo. +- Fix to crash caused when the name of a subtab or microtab is changed. +- Fixes to outdated information in docs. +- Improvements to Discord links. +- Thank you to thepaperpilot for contributing to this update! + +### v2.1.1 - 10/17/20 +- Added resource-display component, which displays the base currency for the prestige layer, as well as the best + and/or total of this layer's prestige currency. +- Fixed the value for the base currency not updating in resource-display. + +## v2.1: We should have thought of this sooner! - 10/17/20 +- Moved most of the code users will want to edit to mod.js, added documentation for it. + - Specifically, modInfo, VERSION, canGenPoints, getPointGen, and maxTickLength +- Added getStartPoints() +- Added the ability to store non-layer-related data +- Added the ability to display more things at the top of the tree tab below points. +- Made the endgame condition customizable +- Added "sell one" and "sell all" buttons for buyables. +- Moved the old "game" to demo.js, and replaced it with a minimal game that won't cause issues when edited. +- Fixed issues with version number +- Fixed number formatting issue making things like "10e9" appear. + + +### v2.0.5 - 10/16/20 +- Made more features (including prestige parameters) able to be dynamic. +- Layer nodes can be hidden but still take up space with "ghost" visibility +- Added clickableEffect for real. +- Fixed some visual issues with bars. +- A few other minor tweaks and improvements. + +### v2.0.4 - 10/16/20 +- Fixed HTML on buttons interfering with clicking on them. + +### v2.0.3 - 10/16/20 +- Fixed hotkeys not displaying in info. +- Fixed the game supressing all external hotkeys. +- You can use more things as currencies for upgrade costs and challenge goals using currencyLocation. +- Added maxTickLength, which can be used to prevent offline time or tab-switching from breaking time-limit based mechanics. +- Made buyable respec buttons and clickable "master" buttons their own components, and gave them a hide/show feature. +- Added a general "tooltip" feature for achievements. + +### v2.0.2 - 10/15/20 +- Branches are now dynamic (they can be functions). +- Fixed a crash related to offline time. +- Fixed links being too wide. + +### v2.0.1 - 10/15/20 +- Fixed side layers appearing multiple times. + +## v2.0: The Pinnacle of Achievement Mountain - 10/15/20 +- Added progress bars, which are highly customizable and can be horizontal or vertical! +- Added "side layers", displayed smaller and off to the side, and don't get reset by default. + They can be used for global achievements and statistics. Speaking of which... +- Added achievements! +- Added clickables, a more generalized variant of buyables. +- Almost every value in layer data can be either a function or a constant value! +- Added support for multiple completions of challenges. +- Added "none" prestige type, which removes the need for any other prestige-related features. +- The points display and other gui elements stay at the top of the screen when the tree scrolls. +- Added getter/setter functions for the amounts and effects of most Big Features +- Moved modInfo to game.js, added a spot in modInfo for a Discord link, changelog link. + Also added a separate mod version from the TMT version in VERSION. +- Tree structure is based on layer data, no index.html editing is needed. +- Tmp does not need to be manually updated. +- You don't have to have the same amount of upgrades in every row (and challs and buyables) +- "unlocked" is optional for all Big Components (defaults to true). +- All displays will update correctly. +- Changelog is no longer in index.html at all. +- Generation of Points now happens in the main game loop +- Changed the reset functions to make keeping things easier +- Renamed many things to increase readability (see the list in the link below) +- Improved documentation based on feedback + + [For a full list of changes to the format and functionality of existing things, click here.](2.0-format-changes.md) + + + +### v1.3.5: + +- Completely automated convertToDecimal, now you never have to worry about it again. +- Branches can be defined without a color id. But they can also use hex values for color ids! +- Created a tutorial for getting started with TMT and Github. +- Page title is now automatically taken from mod name. + +### v1.3.4 - 10/8/20 + +- Added "midsection" feature to add things to a tab's layout while still keeping the standard layout. +- Fix for being able to buy more buyables than you should. + +### v1.3.3 - 10/7/20 +- Fix for the "order of operations" issue in temp. + +### v1.3.1 - 10/7/20 + +- Added custom CSS and tooltips for Layer Nodes. +- Added custom CSS for upgrades, buyables, milestones, and challenges, both individually and layer-wide. +- You can now use HTML in most display text! +- You can now make milestones unlockable and not display immediately. +- Fixed importing saves, and issue with upgrades not appearing, and probably more. +- Optional "name" layer feature, used in confirmation messages. + +## v1.3: Tabception... ception! - 10/7/20 + +- Added subtabs! And also a Micro-tab component to let you make smaller subtab-esque areas anywhere. +- Added a "custom" prestige formula type, and a number of features to support it. +- Added points/sec display (can be disabled). +- Added h-line, v-line and image-display components, plus components for individual upgrades, challenges, and milestones. +- Added upgEffect, buyableEffect, and challEffect functions. +- Added "hide completed challenges" setting. +- Moved old changelogs to a separate place. +- Fixed hasMilestone and incr_order. +- Static layers now show the currency amount needed for the next one if you can buy max. + + +### v1.2.4 - 10/4/20 + +- Layers are now highlighted if you can buy an upgrade, and a new feature, shouldNotify, +lets you make it highlight other ways. +- Fixed bugs with hasUpg, hasChall, hasMilestone, and inChallenge. +- Changed the sample code to use the above functions for convenience. + +### v1.2.3 - 10/3/20 + +- Added a row component, which displays a list of objects in a row. +- Added a column component, which displays a list of objects in a column (useful within a row). +- Changed blanks to have a customizable width and height. + +## v1.2: This Changes Everything! - 10/3/20 + +- Many layer features can now be static values or functions. (This made some formats change, +which will break old things) +- You can now use the "this" keyword, to make code easier to transfer when making new layers. +- Also added "this.layer", which is the current layer's name, and works on existing subfeatures +(e.g. individual upgrades) as well! Subfeatures also have "this.id". +- Fixed a big save issue. If you use a unique mod id, your save will never conflict with other mods. +- Added a configurable offline time limit in modinfo at the top of index.html. (default 1 hour) +- Added a few minor features, and updated the docs with new information. + + +### v1.1.1 - 9/30/20 + +- You can define hotkeys directly from layer config. + +## v1.1: Enhanced Edition - 9/30/20 + +- Added "Buyables", which can function like Space Buildings or Enhancers. +- Custom CSS can now be used on any component! Make the third argument an object with CSS +parameters. +- Lots of minor good things. + + +## v1.0 - 9/27/20 +- First release. diff --git a/css/bars.css b/css/bars.css new file mode 100644 index 0000000..3bcf74d --- /dev/null +++ b/css/bars.css @@ -0,0 +1,38 @@ +.barBase { + overflow: hidden; + -webkit-mask-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAA5JREFUeNpiYGBgAAgwAAAEAAGbA+oJAAAAAElFTkSuQmCC); + display:table +} + +.barBorder { + border: 2px solid; + border-radius: 10px; + border-color: var(--color); + overflow: hidden; + -webkit-mask-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAA5JREFUeNpiYGBgAAgwAAAEAAGbA+oJAAAAAElFTkSuQmCC); + margin:0 +} + +.overlayTextContainer { + z-index: 3; + border-radius: 10px; + vertical-align: middle; + display: flex; + justify-content: center; + align-items: left; + position: absolute; +} + +.fill { + background-color: var(--color); + z-index:2; + position: absolute; + overflow: hidden; + margin-left: -0.5px; + transition-duration: 0.2s; + +} + +.overlayText { + z-index: 6; +} \ No newline at end of file diff --git a/css/components.css b/css/components.css new file mode 100644 index 0000000..ac42fd6 --- /dev/null +++ b/css/components.css @@ -0,0 +1,166 @@ +.upg { + min-height: 120px; + width: 120px; + border-radius: 25%; + border: 2px solid; + border-color: rgba(0, 0, 0, 0.125); + font-size: 10px; + overflow: visible; +} + +.achievement { + height: 90px; + width: 90px; + border-radius: 25%; + border: 2px solid; + border-color: rgba(0, 0, 0, 0.125); + font-size: 10px; + color: white; + text-shadow: 0 0 2px #000000; +} +.achievement:hover { + box-shadow: 0 0 10px var(--points); + z-index: 7; +} + +.buyable { + height: 200px; + width: 200px; + border-radius: 25%; + border: 2px solid; + border-color: rgba(0, 0, 0, 0.125); + font-size: 10px; + position:relative; +} + +.milestone { + width: 100%; + min-width: 120px; + padding-left: 5px; + padding-right: 5px; + height: 75px; + background-color: #bf8f8f; + border: 4px solid; + border-radius: 4px; + border-color: rgba(0, 0, 0, 0.125); + color: rgba(0, 0, 0, 0.5); +} + +.milestoneDone { + width: 100%; + min-width: 120px; + padding-left: 5px; + padding-right: 5px; + + height: 75px; + background-color: #77bf5f; + border: 4px solid; + border-radius: 4px; + border-color: rgba(0, 0, 0, 0.125); + color: rgba(0, 0, 0, 0.5); +} + +.challenge { + background-color: #bf8f8f; + position: relative; + border: 4px solid; + border-color: rgba(0, 0, 0, 0.125); + color: rgba(0, 0, 0, 0.5); + width: 300px; + height: 300px; + font-size: 15px; + border-radius: 33.33%; +} + +.challenge.done { + background-color: #77bf5f; +} + +.challenge.canComplete { + background-color: #ffbf00; +} + +/* Infoboxes */ +.story { + width: 600px; + max-width: 95%; + border-bottom: solid 4px; + border-radius: 8px; + margin-bottom: 8px; + text-align: left; +} + +.story-title { + text-align: left; + font-size: 24px; + color: black; + cursor: pointer; + border: none; + padding: 2px; + border-radius: 8px 8px 0 0; +} + +.story-toggle { + border: none; + background: black; + color: white; + font-size: 20px; + pointer-events: none; + width: 1em; + display: inline-block; +} + +.story-text { + padding: 2px; + border: solid 4px; + border-color: inherit; + border-radius: inherit; + border-top-left-radius: 0; + margin-bottom: -2px; +} + + +/* Tiles are gridables */ +.tile { + height: 80px; + width: 80px; + border-radius: 15%; + border: 2px solid; + border-color: rgba(0, 0, 0, 0.125); + font-size: 10px; + overflow: visible; +} + +.tile.can:hover { + box-shadow: 0 0 10px var(--points); + transform: scale(1.1, 1.1); + z-index: 7; +} + +.upgBig { + height: 200px; + width: 200px; + border-radius: 25%; + border: 2px solid; + border-color: rgba(0, 0, 0, 0.125); +} + +/* Used for respec button and similar */ +.longUpg { + height: 50px; + width: 120px; + background: var(--points); + border-radius: 50%; + border: 2px solid; + border-color: rgba(0, 0, 0, 0.125); + font-size: 10px; +} + +.smallUpg { + height: 40px; + width: 40px; + border-radius: 25%; + border: 2px solid; + border-color: rgba(0, 0, 0, 0.125); +} + diff --git a/css/general-style.css b/css/general-style.css new file mode 100644 index 0000000..0173a2d --- /dev/null +++ b/css/general-style.css @@ -0,0 +1,41 @@ +/* Global things */ +body { + color: var(--color); + overflow: hidden; + --background: #0f0f0f; + --color: #dfdfdf; + --points: #ffffff; + background: var(--background) +} + +/* General text */ +h1, h2, h3, b, input { + display: inline; + font-family: "Lucida Console", "Courier New", monospace +} + +/* These are styles for different states of components. You can make layer-specific versions with .c.locked, for example */ +.locked { + background-color: #bf8f8f; + cursor: not-allowed; +} + +/* Can meens can be clicked/bought/etc */ +.can { + cursor: pointer; +} + +.can:hover { + transform: scale(1.15, 1.15); + box-shadow: 0 0 20px var(--points) +} + +.bought { + background-color: #77bf5f; + cursor: default; +} + +#points { + color: var(--points); + text-shadow: 0 0 10px var(--points); +} diff --git a/css/misc.css b/css/misc.css new file mode 100644 index 0000000..1a9ec6e --- /dev/null +++ b/css/misc.css @@ -0,0 +1,83 @@ + +/* Prestige button */ +.reset { + height: 120px; + width: 180px; + border-radius: 25%; + border: 4px solid; + border-color: rgba(0, 0, 0, 0.125); +} + +.tabButton { + background-color: transparent; + color: var(--color); + font-size: 20px; + cursor: pointer; + padding: 5px 20px; + margin: 5px; + border-radius: 10px; + border: 2px solid; + color: var(--color); + +} + +.tabButton:hover { + transform: scale(1.1, 1.1); + text-shadow: 0 0 7px var(--color); +} + +.tooltip { + pointer-events: none; + text-align: center; + width: 150px; + font-size: 16px; + line-height: 1.2; + bottom: 100%; + left: 50%; + margin-bottom: 5px; + transform: translateX(-50%); + padding: 7px; + font-family: "Lucida Console", "Courier New", monospace; + -webkit-border-radius: 3px; + -moz-border-radius: 3px; + border-radius: 3px; + opacity: 0; + transition: opacity 0.5s; + + position: absolute; + z-index: 20000; + + background-color: var(--background_tooltip); + color: var(--points); + content: attr(tooltip); + font-size:14px; + +} + +.tooltip::after { + content: " "; + position: absolute; + bottom: 100%; + left: 50%; + + top: 100%; + left: 50%; + margin-left: -5px; + border-width: 5px; + border-style: solid; + border-color: var(--background_tooltip) transparent transparent transparent; +} + +/* Horizontal/vertical lines */ +.vl { + border-left: 6px solid var(--color); + height: 100%; + position: absolute; + left: 50%; + margin-left: -3px; + top: 0 +} + +.hl { + border-top: 3px solid var(--color); +} diff --git a/css/other-tabs.css b/css/other-tabs.css new file mode 100644 index 0000000..e6f3a58 --- /dev/null +++ b/css/other-tabs.css @@ -0,0 +1,111 @@ + +#optionWheel { + position: absolute; + top: 0; + left: 0; + height: 50px; + width: 50px; + cursor: pointer; +} + +#optionWheel:hover { + transform: rotate(360deg); +} + +#info { + font-size: 20px; + color: white; + position: absolute; + top: 50px; + left: 4px; + cursor: pointer; + width: 40px; + height: 40px; + -webkit-text-stroke-width: 1px; + -webkit-text-stroke-color: #02f2f2; +} + +#info:hover { + transform: scale(1.2, 1.2); + text-shadow: 5px 0 10px #02f2f2, + -3px 0 12px #02f2f2; +} + +#discord { + position: absolute; + top: 120px; + left: 4px; + width: 40px; + height: 40px; +} + +#discord img { + width: 100%; + height: 100%; +} + +#discord-links { + position: absolute; + top: 0; + padding-top: 44px; + left: -244px; + width: 200px; + transition: left .3s ease; +} + +#discord:hover #discord-links { + left: -4px; +} + +#version { + position: absolute; + right: 4px; + top: 4px; + text-align: right; + color: var(--points); + text-shadow: 0 0 10px var(--points); + cursor: pointer; +} + +#version:hover { + transform: scale(1.1, 1.1); + right: 4.8px; +} + +a { + color: #007fff; + text-decoration-line: none; + cursor: pointer +} + +.link { + display: block; + font-size: 20px; + color: #41f5f5; + cursor: pointer; + font-family: "Lucida Console", "Courier New", monospace; + -webkit-text-stroke-width: 1px; + -webkit-text-stroke-color: #02f2f2; + text-decoration: none; +} + +.link:hover { + transform: scale(1.2, 1.2); + text-shadow: 5px 0 10px #02f2f2, + -3px 0 12px #02f2f2; +} + +.opt { + height: 100px; + width: 100px; + border-radius: 25%; + border: 4px solid; + background-color: var(--color); + border-color: rgba(0, 0, 0, 0.125); + color: rgba(0, 0, 0, 0.5); + cursor: pointer; +} + +.opt:hover { + background-color: #439ea3; +} \ No newline at end of file diff --git a/css/popup.css b/css/popup.css new file mode 100644 index 0000000..c66835e --- /dev/null +++ b/css/popup.css @@ -0,0 +1,52 @@ +.popup { + border: 4px solid; + border-radius: 7px; + width: 300px; + min-height: 60px; + color: #000000; + display: block; + margin-top: 30px; + padding-top: 15px; + padding-bottom: 15px; + border-color: rgba(0, 0, 0, 0.25); +} + + +.popup-container { + position: absolute; + z-index: 9999999999999999999999999999999999; + right: 30px; + width: 300px; +} + +.achievement-popup { + background: #7182BC; +} + +.milestone-popup { + background: #D1C23C; +} + +.fade-enter-active, .fade-leave-active { + transition: opacity .3s +} + +.fade-transition { + transition: opacity .3s +} + +.fade-enter, .fade-leave-to /* .fade-leave-active below version 2.1.8 */ { + opacity: 0 +} + +.redtext { + color: red; +} + +.particle { + background-color: transparent; + display: block; + position:absolute; + z-index: 99999; + background-size:contain; +} \ No newline at end of file diff --git a/css/system-style.css b/css/system-style.css new file mode 100644 index 0000000..5943ae0 --- /dev/null +++ b/css/system-style.css @@ -0,0 +1,251 @@ +* { + transition-duration: 0.5s; + text-align: center; + font-family: "Inconsolata", monospace; + font-weight: bold; + table-align: center; + margin: auto; + -webkit-text-size-adjust: none; + text-size-adjust: none; + +} + +*:focus { + outline: none; + webkit-outline: none; +} + +html, body { + min-height: 100%; + height: 100%; +} + +td { + padding: 0 +} + +.upgTable { + display: flex !important; + flex-flow: column wrap; + justify-content: center; + align-items: center; + max-width: 100%; + margin: 0 auto; +} + +.upgRow { + display: flex !important; + flex-flow: row wrap; + justify-content: center; + align-items: center; + max-width: 100%; + margin: 0 auto; +} + +.upgAlign { + vertical-align: 0 +} + +.bigUpgAlign { + vertical-align: 0 +} + +.treeThing { + margin: 0 10px 0 10px; + vertical-align: middle; +} + +.can.upg:hover { + z-index: 2 +} + +.can.buyable:hover { + z-index: 2 +} + +.back { + position: absolute; + top: 0; + left: 0; + background-color: transparent; + border: 1px solid transparent; + color: var(--color); + font-size: 40px; + cursor: pointer; +} + +.other-back { + position: absolute; + top: 0; + left: 60px; + background-color: transparent; + border: 1px solid transparent; + color: var(--color); + font-size: 60px; + cursor: pointer; +} + +.back:hover { + transform: scale(1.1, 1.1); + text-shadow: 0 0 7px var(--color); +} + +.hidden { + visibility: hidden; + height: 50px !important; +} + +.canvas { + top: 0; + left: 0; + position: absolute; + z-index: -999; +} + +.left { + position: absolute; + left: 0; +} + +.remove { + height: 24px; + width: 24px; + cursor: pointer; +} + +.remove:hover { + transform: scale(1.1, 1.1); +} + +.col { + min-width: 49.5%; + max-width: 49.5%; + width: 49.5%; + height: 100%; + min-height: 100%; + column-span: 1; + position: absolute; + overflow-y: auto; + overflow-x: auto; + transition-duration: 0s +} + +.instant { + transition-duration: 0s !important +} + +.fast { + transition:color none +} + +.col.right { + top: 0; + right: 0; +} + +#app { + column-count: 2; + column-width: 50%; + min-height: 100%; +} + +.vl2 { + border-left: 3px solid var(--color); + height: 100%; +} + +ul { + list-style-type: none; +} + +.fullWidth { + position: absolute; + height: 100%; + width: 100%; + min-width: 100%; + overflow-y: auto; + overflow-x: auto; + transition-duration: 0s +} + +.tooltipBox { + position: relative; + } + + .tooltipBox:hover .tooltip{ + opacity: 1; + } + + .forceTooltip .tooltip{ + opacity: 1; + } + +.respecCheckbox { + display: inline-block; +} + + +#loadingSection { + display: flex; + flex-direction: column; + justify-content: center; +} + +.treeOverlay { + pointer-events:none; + overflow:hidden; +} + +.front { + z-index: 30000 +} + +.overlayThing { + z-index: 10000; + pointer-events:auto; +} + +.sideLayers { + z-index: 10000; + pointer-events:auto; + position: absolute; + right: 55px; + top: 65px; + background-color: transparent; +} + +button > * { + pointer-events:none; +} + +.ghost { + visibility: hidden +} + +#treeTab td button { + margin: 0 10px; +} + +.bg { + z-index: -9000; + width: 100%; + height: 100%; + position: absolute; + background-color: transparent; + top: 0 +} + +.bg2 { + z-index: -9009; + width: 100%; + height: 100%; + position: absolute; + top: 0 +} + +.noBackground { + background: transparent !important; + background-image: none !important; + --background: transparent !important; +} + diff --git a/css/tree-node.css b/css/tree-node.css new file mode 100644 index 0000000..8e11d00 --- /dev/null +++ b/css/tree-node.css @@ -0,0 +1,108 @@ +/* Tree nodes, button nodes, and side layers */ +.treeNode { + height: 100px; + width: 100px; + border: var(--hqProperty1); + border-color: rgba(0, 0, 0, 0.125); + border-radius: 50%; + box-shadow: var(--hqProperty2a), var(--hqProperty2b); + font-size: 40px; + font-family: "Lucida Console", "Courier New", monospace; + color: rgba(0, 0, 0, 0.5); + text-shadow: var(--hqProperty3); + padding: 0; + margin: 0 10px 0 10px; +} + +.nodeLabel { + font: inherit; + font-family: inherit; +} + +.treeButton { + height: 100px; + width: 100px; + border: var(--hqProperty1); + border-color: rgba(0, 0, 0, 0.125); + border-radius: 33%; + box-shadow: var(--hqProperty2a), var(--hqProperty2b); + font-size: 40px; + font-family: "Lucida Console", "Courier New", monospace; + color: rgba(0, 0, 0, 0.5); + text-shadow: var(--hqProperty3); + padding: 0; +} + +.smallNode { + height: 60px; + width: 60px; + font-size: 30px; +} + +.resetNotify { + box-shadow: var(--hqProperty2a), 0 0 8px #ffffff; +} + +.treeNode.can:hover { + transform: scale(1.15, 1.15); + box-shadow: var(--hqProperty2a), 0 0 20px var(--points); +} + +.notify { + transform: scale(1.05, 1.05); + border-color: rgba(0, 0, 0, 0.125); + box-shadow: var(--hqProperty2a), 0 0 20px #ff0000; +} + + + +.mark { + position: relative; + display: inline-block; + width: 30px; + height: 30px; + z-index: 10000; + margin-left: 0.9em; + margin-right: 0.9em; + margin-bottom: 1.2em; + border-right: 0.3em solid transparent; + border-bottom: 0.7em solid transparent; + border-left: 0.3em solid transparent; + font-size: 10px; + overflow:auto; + pointer-events: none; +} + +.star { + position: relative; + display: inline-block; + width: 0; + height: 0; + z-index: 10000; + margin-left: 0.9em; + margin-right: 0.9em; + margin-bottom: 1.2em; + border-right: 0.3em solid transparent; + border-bottom: 0.7em solid #ffcc00; + border-left: 0.3em solid transparent; + font-size: 10px; + pointer-events: none; + +} + +.star:before, .star:after { + content: ""; + width: 0; + height: 0; + position: absolute; + top: .6em; + left: -1em; + border-right: 1em solid transparent; + border-bottom: 0.7em solid #ffcc00; + border-left: 1em solid transparent; + transform: rotate(-35deg); +} + +.star:after { + transform: rotate(35deg); +} \ No newline at end of file diff --git a/demo.html b/demo.html new file mode 100644 index 0000000..6bdc9ee --- /dev/null +++ b/demo.html @@ -0,0 +1,140 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+

Loading... (If this takes too long it means there was a serious error!)←

+
+
+
+
+

{{modInfo.name}} {{VERSION.withoutName}}



+


+

Please check the Discord to see if there are new content updates!



+
It took you {{formatTime(player.timePlayed)}} to beat the game.
+
+      +


+ {{modInfo.discordName}}
+ The Modding Tree Discord
+ Main + Prestige Tree server
+

+
+ +
+
+ {{VERSION.withoutName}}
+ + +

i
+ + +
+
+ +
+
+
+ +
+



+ + + +
+ + + +
+
+ +
+
+ +
+
+
+ +
+
+ + +
+
+ +
+ \ No newline at end of file diff --git a/discord.png b/discord.png new file mode 100644 index 0000000000000000000000000000000000000000..14fc5779958d5a9f9f6df3d0cdd39b23af1c991d GIT binary patch literal 1990 zcmV;%2RZnOP)Px#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D2U|%*K~#8N?VSm% z9a9{KTRRm?iYOIpiV&eBmJr06T4O1RS}U}KG(}46X@pve*a@nZD6zFPYAdZ!L93{x zB#otZ4YAYW`|h3ByuRLh-^|=Q=gsRozvRoAnfvD6Grv1?=A8fe&wah3qN1XrqN1Xr zmL>I0%BD4P%-n4$wu6me1-Er`{Q~n~#@JB?qjDL| z*}Nvjt*}yML(kFXa7A)8XHwoX`e^W`*AsOl^sg?aS6-tJOv;|9wPDxlVtQpZn#;!K zVzcU6kJXk*nOb?y=;fiOwap#1Vp677-liieqoUD)6pap~XmlV&qXQ`#9Z1pWK#E2O zQW=f@8y3JUc+U+P^e)VVAHer1jml#5zwkPo2gVWhf=A&uF!-KUemUpE#;`M-4X?l= z=u&mH(SN~s*a`N9kKrnK6GnnV-ACYRI12{D;q5RGe+IbEW3VGU4zpqZWZOQlJ=_Ps zLYF9WTy^A_xvNrW#5R=;`Q#b=8jOOsK~pr=NrY$M1<>>bV@D16hccCtx~&Ud6^Fpq z@HE^6Hb(>DO3?h=vrA|+^|~ZkPNl9g`g*t+)=J8s!Lx5x7GpaTb?r-GNY|^y_n!=2 zC6|ItOkT6Qh0$Mv(?T17XV?@f?F49{qTZh6UNJ9$v-I9Htw$bZHac`0?Tzgg@Fq8p z%1#Jjvf^{R+1ey6Wcd%u8z9S#wkd@+T2se!pdR?b>ZWkwGZ#Udxlh3S>SFN2`v9#E(vDbk8;vsM>+zu0Xtq)}y`Nb5f5JtC!KpP5Etjo1vB4K;22G_x+m9?d1 z9K*<7$E)*T7+ziMRK~Fjy-|lS+RJxNb+Mt$X)}sfOO0MLbXxB?2J;1`X&%k&x0xzT zA&fS0JI!$0rv)^$o;FO%jxs`5PrkNFb+Nvv-=7q<+ol#NgwYNP(#Sy1x7Ed(`ZOs! z%4lFc&BsLFH;vL}i~d^%85WYhkhJqK&q1e6j~zU0qTfw7msyWOf0cgJ=0$hueK2SKZi8 zR)W1*y%%h@Z03gYc{Oi%{H^sH3aeK(6rjwPES1U{RI)d50bIj@HVO=xgW-+r2(ZbS z4pU(U*mi!+{x#zz?CUVy$60+A-7F`E<;G3yk(gXFsgut>hr+*0dqtHt-9Y2+fD5T$ z-C1wm+p~BRxD!lrQSR%R??dokgFO?MHP`mGADA>_z1rH}ci=Rb&F9t4!Q=BxKd?(_ z@{SKdrB!L8cOHbPTT?l>K~VFCl$>=AwN346llvNlGkm%Oc*dHA*(1MA$cUv6d1|n;DrZ8e6VVMno&Y}G)ZMfv z^hqWf?tD7eii(OVrF!-H YA9w!&c$<<$hyVZp07*qoM6N<$f((q%8~^|S literal 0 HcmV?d00001 diff --git a/docs/!general-info.md b/docs/!general-info.md new file mode 100644 index 0000000..1b17850 --- /dev/null +++ b/docs/!general-info.md @@ -0,0 +1,59 @@ +# The-Modding-Tree + +Making a game in The Modding Tree mostly involves defining parameters or functions on objects. If you aren't following the [getting started guide](tutorials/getting-started.md), you should start by [setting up your basic mod info](main-mod-info.md) in [mod.js](/js/mod.js). It's important to set a mod id to ensure saving works properly. + +Beyond that, the main way to add content is through creating layers. You can add new layers by calling `addLayer(layername, layerdata)`. There is an example of a basic layer in [layers.js](/js/layers.js). It is just an example and can be freely deleted. You can also use it as a reference or a base for your own layers. + +You can test your mod by opening the [index.html](/index.html) file in your browser. + +Most of the time, you won't need to dive deep into the code to create things, but you still can if you really want to, for example to add new Vue components in [components.js](/js/components.js). + +The Modding Tree uses [break\_eternity.js](https://github.com/Patashu/break_eternity.js) to store large values. This means that many numbers are `Decimal` objects, and must be treated differently. For example, you have to use `new Decimal(x)` to create a `Decimal` value instead of a plain number (x can be a number or a string for larger values). You perform operations on them by calling functions. e.g, instead of `x = x + y`, use `x = x.add(y)`. Keep in mind this also applies to comparison operators, which should be replaced with calling the `.gt`, `.gte`, `.lt`, `.lte`, `.eq`, and `.neq` functions. See the [break\_eternity.js](https://github.com/Patashu/break_eternity.js) docs for more details on working with `Decimal` values. + +Almost all values can be either a constant value, or a dynamic value. Dynamic values are defined by putting a function that returns what the value should be at any given time. + +All display text can use basic HTML elements (But you can't use most Vue features there). + +While reading this documentation, the following key will be used when describing features: + +- No label: This is required and the game may crash if it isn't included. +- **sometimes required**: This is may be required, depending on other things in the layer. +- **optional**: You can leave this out if you don't intend to use that feature for the layer. +- **assigned automagically**: This value will be set automatically and override any value you set. +- **deprecated**: This feature is not recommended to be used, because newer features are able to achieve the same thing in a better, easier way. + +## Table of Contents + + + +### General + +- [Getting Started](tutorials/getting-started.md): A guide to getting your own copy of the code set up with Github Desktop. +- [Making a Mod](tutorials/making-a-mod.md): A guide to using TMT to make a basic mod. +- [Main mod info](main-mod-info.md): How to set up general things for your mod in [mod.js](/js/mod.js). +- [Basic layer breakdown](basic-layer-breakdown.md): Breaking down the components of a layer with minimal features. +- [Layer features](layer-features.md): Explanations of all of the different properties that you can give a layer. +- [Custom Tab Layouts](custom-tab-layouts.md): An optional way to give your tabs a different layout. You can even create entirely new components to use. +- [Custom Game Layouts](trees-and-tree-customization.md): You can get rid of the tree tab, add buttons and other things to the tree, + or even customize the tab's layout like a layer tab. +- [Updating TMT](tutorials/updating-tmt.md): Using Github Desktop to update your mod's version of TMT. +- [Other Things](other.md): Other neat features that TMT has that don't merit their own page. + +### Common components + +- [Upgrades](upgrades.md): How to create upgrades for a layer. +- [Milestones](milestones.md): How to create milestones for a layer. +- [Buyables](buyables.md): Create rebuyable upgrades for your layer (with the option to make them respec-able). Can be used to make Enhancers or Space Buildings, for example. +- [Clickables](clickables.md): A more generalized variant of buyables, for any kind of thing that is sometimes clickable. Between these and Buyables, you can do just about anything. +- [Achievements](achievements.md): How to create achievements for a layer (or for the whole game). + +### Other components and features + +- [Challenges](challenges.md): How to create challenges for a layer. +- [Bars](bars.md): Display some information as a progress bar, gauge, or similar. They are highly customizable, and can be horizontal and vertical as well. +- [Subtabs and Microtabs](subtabs-and-microtabs.md): Create subtabs for your tabs, as well as "microtab" components that you can put inside the tabs. + You can even use them to embed a layer inside another layer! +- [Grids](grids.md): Create a group buttons that behave the same, but have their own data. Good for map tiles, an inventory grid, and more! +- [Infoboxes](infoboxes.md): Boxes containing text that can be shown or hidden. +- [Trees](trees-and-tree-customization.md): Make your own trees. You can make non-layer button nodes too! +- [Particle system](particles.md): Can be used to create particles for visual effects, but also interactable things like golden cookies or collectables. diff --git a/docs/achievements.md b/docs/achievements.md new file mode 100644 index 0000000..e01fbfd --- /dev/null +++ b/docs/achievements.md @@ -0,0 +1,54 @@ +# Achievements + +Achievements are awarded to the player when they meet a certain goal, and optionally give some benefit. + +You can make global achievements by putting them in a side layer by making its row equal to "side" instead of a number. + +Useful functions for dealing with achievements and implementing their effects: + +- hasAchievement(layer, id): determine if the player has the Achievement. +- achievementEffect(layer, id): Returns the current effects of the achievement, if any. + +Achievements should be formatted like this: + +```js +achievements: { + 11: { + name: "Blah", + more features + }, + etc +} +``` + +Usually, each achievement should have an id where the first digit is the row and the second digit is the column. + +Individual achievement can have these features: + +- name: **optional**. displayed at the top of the achievement. The only visible text. It can also be a function that returns updating text. Can use basic HTML. + +- done(): A function returning a boolean to determine if the achievement should be awarded. + +- tooltip: Default tooltip for the achievement, appears when it is hovered over. Should convey the goal and any reward for completing the achievement. It can also be a function that returns updating text. Can use basic HTML. Setting this to "" disables the tooltip. + +- effect(): **optional**. A function that calculates and returns the current values of any bonuses from the achievement. Can return a value or an object containing multiple values. + +- unlocked(): **optional**. A function returning a bool to determine if the achievement is visible or not. Default is unlocked. + +- onComplete() - **optional**. this function will be called when the achievement is completed. + +- image: **optional**, puts the image from the given URL (relative or absolute) in the achievement + +- style: **optional**. Applies CSS to this achievement, in the form of an object where the keys are CSS attributes, and the values are the values for those attributes (both as strings). + +- textStyle: **optional**. Applies CSS to the text, in the form of an object where the keys are CSS attributes, and the values are the values for those attributes (both as strings). + +- layer: **assigned automagically**. It's the same value as the name of this layer, so you can do `player[this.layer].points` or similar. + +- id: **assigned automagically**. It's the "key" which the achievement was stored under, for convenient access. The achievement in the example's id is 11. + +- goalTooltip: **optional, deprecated**. Appears when the achievement is hovered over and locked, overrides the basic tooltip. This is to display the goal (or a hint). It can also be a function that returns updating text. Can use basic HTML. + +- doneTooltip: **optional, deprecated**. Appears when the achievement is hovered over and completed, overrides the basic tooltip. This can display what the player achieved (the goal), and the rewards, if any. It can also be a function that returns updating text. Can use basic HTML. + +Disable achievement popups by adding `achievementsPopups: false` to the layer. diff --git a/docs/bars.md b/docs/bars.md new file mode 100644 index 0000000..6e85937 --- /dev/null +++ b/docs/bars.md @@ -0,0 +1,40 @@ +# Bars + +Bars let you display information in a more direct way. It can be a progress bar, health bar, capacity gauge, or anything else. + +Bars are defined like other Big Features: + +```js +bars: { + bigBar: { + direction: RIGHT, + width: 200, + height: 50, + progress() { return 0 }, + etc + }, + etc +} +``` + +Features: + +- direction: UP, DOWN, LEFT, or RIGHT (not strings). Determines the direction that the bar is filled as it progresses. RIGHT means from left to right. + +- width, height: The size in pixels of the bar, but as numbers (no "px" at the end). + +- progress(): A function that returns the portion of the bar that is filled, from "empty" at 0 to "full" at 1, updating automatically. + (Nothing bad happens if the value goes out of these bounds, and it can be a number or `Decimal`) + +- display(): **optional**. A function that returns text to be displayed on top of the bar, can use HTML. + +- unlocked(): **optional**. A function returning a bool to determine if the bar is visible or not. Default is unlocked. + +- baseStyle, fillStyle, borderStyle, textStyle: **Optional**, Apply CSS to the unfilled portion, filled portion, border, and display text on the bar, in the form of an object where the keys are CSS attributes, and the values are the values for those attributes (both as strings). + +- layer: **assigned automagically**. It's the same value as the name of this layer, so you can do `player[this.layer].points` or similar. + +- id: **assigned automagically**. It's the "key" which the bar was stored under, for convenient access. The bar in the example's id is "bigBar". + + +- instant: **very optional**. If this is true, the bar will instantly snap to the current value instead of animating in between. Good for things involving precise timing. \ No newline at end of file diff --git a/docs/basic-layer-breakdown.md b/docs/basic-layer-breakdown.md new file mode 100644 index 0000000..b054aba --- /dev/null +++ b/docs/basic-layer-breakdown.md @@ -0,0 +1,38 @@ +# Basic layer breakdown + +This is a relatively minimal layer with few features. Most things will require additional features. + +```js +addLayer("p", { + startData() { return { // startData is a function that returns default data for a layer. + unlocked: true, // You can add more variables here to add them to your layer. + points: new Decimal(0), // "points" is the internal name for the main resource of the layer. + }}, + + color: "#4BDC13", // The color for this layer, which affects many elements. + resource: "prestige points", // The name of this layer's main prestige resource. + row: 0, // The row this layer is on (0 is the first row). + + baseResource: "points", // The name of the resource your prestige gain is based on. + baseAmount() { return player.points }, // A function to return the current amount of baseResource. + + requires: new Decimal(10), // The amount of the base needed to gain 1 of the prestige currency. + // Also the amount required to unlock the layer. + + type: "normal", // Determines the formula used for calculating prestige currency. + exponent: 0.5, // "normal" prestige gain is (currency^exponent). + + gainMult() { // Returns your multiplier to your gain of the prestige resource. + return new Decimal(1) // Factor in any bonuses multiplying gain here. + }, + gainExp() { // Returns the exponent to your gain of the prestige resource. + return new Decimal(1) + }, + + layerShown() { return true }, // Returns a bool for if this layer's node should be visible in the tree. + + upgrades: { + // Look in the upgrades docs to see what goes here! + }, +}) +``` diff --git a/docs/buyables.md b/docs/buyables.md new file mode 100644 index 0000000..6a28fd0 --- /dev/null +++ b/docs/buyables.md @@ -0,0 +1,86 @@ +# Buyables + +Buyables are usually things that can be bought multiple times with scaling costs. They come with optional buttons that can be used for respeccing or selling buyables, among other things. + +The amount of a buyable owned is a `Decimal`. + +Useful functions for dealing with buyables and implementing their effects: + +- getBuyableAmount(layer, id): get the amount of the buyable the player has +- addBuyables(layer, id, amount): add to the amount of the buyable +- setBuyableAmount(layer, id, amount): set the amount of the buyable the player has +- buyableEffect(layer, id): Returns the current effects of the buyable, if any. + +Buyables should be formatted like this: + +```js +buyables: { + 11: { + cost(x) { return new Decimal(1).mul(x) }, + display() { return "Blah" }, + canAfford() { return player[this.layer].points.gte(this.cost()) }, + buy() { + player[this.layer].points = player[this.layer].points.sub(this.cost()) + setBuyableAmount(this.layer, this.id, getBuyableAmount(this.layer, this.id).add(1)) + }, + etc + }, + etc +} +``` + +Features: + +- title: **optional**. displayed at the top in a larger font. It can also be a function that returns updating text. + +- cost(): cost for buying the next buyable. Can have an optional argument "x" to calculate the cost of the x+1th purchase. (x is a `Decimal`). + Can return an object if there are multiple currencies. + +- effect(): **optional**. A function that calculates and returns the current values of bonuses of this buyable. Can have an optional argument "x" to calculate the effect of having x of the buyable.. + Can return a value or an object containing multiple values. + +- display(): A function returning everything that should be displayed on the buyable after the title, likely including the description, amount bought, cost, and current effect. Can use basic HTML. + +- unlocked(): **optional**. A function returning a bool to determine if the buyable is visible or not. Default is unlocked. + +- canAfford(): A function returning a bool to determine if you can buy one of the buyables. + +- buy(): A function that implements buying one of the buyable, including spending the currency. + +- buyMax(): **optional**. A function that implements buying as many of the buyable as possible. + +- style: **optional**. Applies CSS to this buyable, in the form of an object where the keys are CSS attributes, and the values are the values for those attributes (both as strings). + +- purchaseLimit: **optional**. The limit on how many of the buyable can be bought. The default is no limit. + +- marked: **optional** Adds a mark to the corner of the buyable. If it's "true" it will be a star, but it can also be an image URL. + +- tooltip: **optional**. Adds a tooltip to this buyable, appears when it is hovered over. Can use basic HTML. Default is no tooltip. If this returns an empty value, that also disables the tooltip. + +- layer: **assigned automagically**. It's the same value as the name of this layer, so you can do `player[this.layer].points` or similar. + +- id: **assigned automagically**. It's the "key" which the buyable was stored under, for convenient access. The buyable in the example's id is 11. + +Sell One/Sell All: + +Including a `sellOne` or `sellAll` function will cause an additional button to appear beneath the buyable. They are functionally identical, but "sell one" appears above "sell all". You can also use them for other things. + +- sellOne/sellAll(): **optional**. Called when the button is pressed. The standard use would be to decrease/reset the amount of the buyable, and possibly return some currency to the player. + +- canSellOne/canSellAll(): **optional**. booleans determining whether or not to show the buttons. If "canSellOne/All" is absent but "sellOne/All" is present, the appropriate button will always show. + + +To add a respec button, or something similar, add the respecBuyables function to the main buyables object (not individual buyables). +You can use these features along with it: + +- respec(): **optional**. This is called when the button is pressed (after a toggleable confirmation message). + +- respecText: **optional**. Text to display on the respec Button. + +- showRespec(): **optional**. A function determining whether or not to show the button, if respecBuyables is defined. Defaults to true if absent. + +- respecMessage: **optional**. A custom confirmation message on respec, in place of the default one. + + + +- branches: **optional**, This is primarially useful for buyable trees. An array of buyable ids. A line will appear from this buyable to all of the buyables in the list. Alternatively, an entry in the array can be a 2-element array consisting of the buyable id and a color value. The color value can either be a string with a hex color code, or a number from 1-3 (theme-affected colors). A third element in the array optionally specifies line width. \ No newline at end of file diff --git a/docs/challenges.md b/docs/challenges.md new file mode 100644 index 0000000..e61e04b --- /dev/null +++ b/docs/challenges.md @@ -0,0 +1,80 @@ +# Challenges + +Challenges can have fully customizable win conditions. Useful functions for dealing with Challenges and implementing their effects: + +- inChallenge(layer, id): determine if the player is in a given challenge (or another challenge on the same layer that counts as this one). +- hasChallenge(layer, id): determine if the player has completed the challenge. +- challengeCompletions(layer, id): determine how many times the player completed the challenge. +- maxedChallenge(layer, id): determines if the player has reached the maximum completions. +- challengeEffect(layer, id): Returns the current effects of the challenge, if any. + +Challenges are stored in the following format: + +```js +challenges: { + 11: { + name: "Ouch", + challengeDescription: "description of ouchie", + canComplete: function() {return player.points.gte(100)}, + etc + }, + etc +} +``` + +Usually, each challenge should have an id where the first digit is the row and the second digit is the column. + +Individual Challenges can have these features: + +- name: Name of the challenge, can be a string or a function. Can use basic HTML. + +- challengeDescription: A description of what makes the challenge a challenge. *You will need to implement these elsewhere.* It can also be a function that returns updating text. Can use basic HTML. + +- goalDescription: A description of the win condition for the challenge. It can also be a function that returns updating text. + Can use basic HTML. (Optional if using the old goal system) + +- canComplete(): A function that returns true if you meet the win condition for the challenge. Returning a number will allow bulk completing the challenge. + (Optional if using the old goal system) + +- rewardDescription: A description of the reward's effect. *You will also have to implement the effect where it is applied.* It can also be a function that returns updating text. Can use basic HTML. + +- rewardEffect(): **optional**. A function that calculates and returns the current values of any bonuses from the reward. Can return a value or an object containing multiple values. Can use basic HTML. + +- rewardDisplay(): **optional**. A function that returns a display of the current effects of the reward with formatting. Default behavior is to just display the a number appropriately formatted. + +- fullDisplay(): **OVERRIDE**. Overrides the other displays and descriptions, and lets you set the full text for the challenge. Can use basic HTML. + +- unlocked(): **optional**. A function returning a bool to determine if the challenge is visible or not. Default is unlocked. + +- onComplete() - **optional**. this function will be called when the challenge is completed when previously incomplete. + +- onEnter() - **optional**. this function will be called when entering the challenge + +- onExit() - **optional**. this function will be called when exiting the challenge in any way + +- countsAs: **optional**. If a challenge combines the effects of other challenges in this layer, you can use this. An array of challenge ids. The player is effectively in all of those challenges when in the current one. + +- completionLimit: **optional**. the amount of times you can complete this challenge. Default is 1 completion. + +- style: **optional**. Applies CSS to this challenge, in the form of an object where the keys are CSS attributes, and the values are the values for those attributes (both as strings). + +- marked: **optional** Adds a mark to the corner of the challenge. If it's "true" it will be a star, but it can also be an image URL. By default, if the challenge has multiple completions, it will be starred at max completions. + +- layer: **assigned automagically**. It's the same value as the name of this layer, so you can do player[this.layer].points or similar + +- id: **assigned automagically**. It's the "key" which the challenge was stored under, for convenient access. The challenge in the example's id is 11. + + + +The old goal system uses these features: + +- goal: **deprecated**, A Decimal for the amount of currency required to beat the challenge. By default, the goal is in basic Points. The goal can also be a function if its value changes. + +- currencyDisplayName: **deprecated**. the name to display for the currency for the goal + +- currencyInternalName: **deprecated**. the internal name for that currency + +- currencyLayer: **deprecated**. the internal name of the layer that currency is stored in. If it's not in a layer, omit. If it's not stored directly in a layer, instead use the next feature. + +- currencyLocation(): **deprecated**. if your currency is stored in something inside a layer (e.g. a buyable's amount), you can access it this way. This is a function returning the object in "player" that contains the value (like `player[this.layer].buyables`) + diff --git a/docs/clickables.md b/docs/clickables.md new file mode 100644 index 0000000..d0874c4 --- /dev/null +++ b/docs/clickables.md @@ -0,0 +1,63 @@ +# Clickables + +Clickables are any kind of thing that you can click for an effect. They're a more generalized version of Buyables. + +DO NOT USE THESE TO MAKE THINGS THAT YOU CLICK REPEATEDLY FOR A BONUS BECAUSE THOSE ARE AWFUL. + +There are several differences between the two. One is that a buyable's saved data is its amount as a `Decimal`, while Clickables store a "state" which can be a number or string, but not `Decimal`, array, or object). Buyables have a number of extra features which you can see on their page. Clickables also have a smaller default size. + +Useful functions for dealing with clickables and implementing their effects: + +- getClickableState(layer, id): get the state of the clickable the player has +- setClickableState(layer, id, state): set the state of the clickable the player has +- clickableEffect(layer, id): Returns the current effects of the clickable, if any. + +Clickables should be formatted like this: + +```js +clickables: { + 11: { + display() {return "Blah"}, + etc + } + etc +} +``` + +Features: + +- title: **optional**. displayed at the top in a larger font. It can also be a function that returns updating text. + +- effect(): **optional**. A function that calculates and returns the current values of bonuses of this clickable. Can return a value or an object containing multiple values. + +- display(): A function returning everything that should be displayed on the clickable after the title, likely changing based on its state. Can use basic HTML. + +- unlocked(): **optional**. A function returning a bool to determine if the clickable is visible or not. Default is unlocked. + +- canClick(): A function returning a bool to determine if you can click the clickable. + +- onClick(): A function that implements clicking the clickable. + +- onHold(): **optional** A function that is called 20x/sec when the button is held for at least 0.25 seconds. + +- style: **optional**. Applies CSS to this clickable, in the form of an object where the keys are CSS attributes, and the values are the values for those attributes (both as strings). + +- marked: **optional** Adds a mark to the corner of the clickable. If it's "true" it will be a star, but it can also be an image URL. + +- tooltip: **optional**. Adds a tooltip to this clickable, appears when it is hovered over. Can use basic HTML. Default is no tooltip. If this returns an empty value, that also disables the tooltip. + +- layer: **assigned automagically**. It's the same value as the name of this layer, so you can do `player[this.layer].points` or similar. + +- id: **assigned automagically**. It's the "key" which the clickable was stored under, for convenient access. The clickable in the example's id is 11. + +You can also use these features on the clickables object to add a button above all the clickables, for implementing a respec button or similar. + +- masterButtonPress(): **optional**. If present, an additional button will appear above the clickables. Pressing it will call this function. + +- masterButtonText: **optional**. Text to display on the Master Button. + +- showMasterButton(): **optional**. A function determining whether or not to show the button, if masterButtonPress is defined. Defaults to true if absent. + + + +- branches: **optional**, This is primarially useful for clickable trees. An array of clickable ids. A line will appear from this clickable to all of the clickables in the list. Alternatively, an entry in the array can be a 2-element array consisting of the clickable id and a color value. The color value can either be a string with a hex color code, or a number from 1-3 (theme-affected colors). A third element in the array optionally specifies line width. \ No newline at end of file diff --git a/docs/custom-tab-layouts.md b/docs/custom-tab-layouts.md new file mode 100644 index 0000000..a20727d --- /dev/null +++ b/docs/custom-tab-layouts.md @@ -0,0 +1,85 @@ +# Custom tab layouts + +Note: If you are using subtabs, `tabFormat` is used differently, but the same format is used for defining their layouts. [See here for more on subtabs](subtabs-and-microtabs.md). + +Custom tab layouts can be used to do basically anything in a tab window, especially combined with the "style" layer feature. The `tabFormat` feature is an array of things, like this: + +```js +tabFormat: [ + "main-display", + ["prestige-button"], + "blank", + ["display-text", + function() { return 'I have ' + format(player.points) + ' pointy points!' }, + { "color": "red", "font-size": "32px", "font-family": "Comic Sans MS" }], + "blank", + ["toggle", ["c", "beep"]], + "milestones", + "blank", + "blank", + "upgrades" +] +``` + +It is a list of components, which can be either just a name, or an array with arguments. If it's an array, the first item is the name of the component, the second is the data passed into it, and the third (optional) applies a CSS style to it with a "CSS object", where the keys are CSS attributes. + +These are the existing components, but you can create more in [components.js](/js/components.js): + +- display-text: Displays some text (can use basic HTML). The argument is the text to display. It can also be a function that returns updating text. + +- display-image: Displays an image. The argument is the url of the image. + +- h-line, v-line: Display a horizontal or vertical divider line, respectively. + +- raw-html: Displays some basic HTML, can also be a function. + +- blank: Adds empty space. The default dimensions are 8px x 17px. The argument changes the dimensions. If it's a single value (e.g. "20px"), that determines the height. If you have a pair of arguments, the first is width and the second is height. + +- row: Display a list of components horizontally. The argument is an array of components in the tab layout format. + +- column: Display a list of components vertically. The argument is an array of components in the tab layout format. This is useful to display columns within a row. + +- main-display: The text that displays the main currency for the layer and its effects. The argument is the amount of precision to use, allowing it to display non-whole numbers. + +- resource-display: The text that displays the currency that this layer is based on, as well as the best and/or total values for this layer's prestige currency (if they are put in `startData` for this layer). + +- prestige-button: The button to reset for a currency in this layer. + +- text-input: A text input box. The argument is the name of the variable in player[layer] that the input is for, player[layer][argument] + (Works with strings, numbers, and Decimals!) + +- slider: Lets the user input a value with a slider. The argument a 3-element array: [name, min, max]. + The name is the name of the variable in player[layer] that the input is for, and min and max are the limits of the slider. + (Does not work for Decimal values) + +- drop-down: Lets the user input a value with a dropdown menu. The argument a 2-element array: [name, options]. + The name is the name of the variable in player[layer] that the input is for, and options is an array of strings for options you can use. + +- upgrades, milestones, challenges, achievements, buyables, clickables: Displays the layers upgrades/challenges/etc, as appropriate. The argument is optional, and is a the list of rows this component should include, if it doesn't have all of them. + +- microtabs: Display a set of subtabs for an area. The argument is the name of the set of microtabs in the "microtabs" feature. + +- bar: Display a bar. The argument is the id of the bar to display. + +- infobox: Display an infobox. The argument is the id of the infobox to display. + +- tree: Displays a tree. The argument is an array of arrays containing the names of the nodes in the tree (first by row, then by column) + [See here for more information on tree layouts and nodes!](trees-and-tree-customization.md) + +- upgrade-tree, buyable-tree, clickable-tree: Displays a tree of upgrades/buyables/clickables from this layer. The argument is an array of arrays containing the ids of the upgrade/etc in the tree (first by row, then by column). A tree can only have one type of component in it. + +- toggle: A toggle button that toggles a bool value. The argument is a pair that identifies the location in player of the bool to toggle, e.g. `[layer, id]`. 'layer' also affects the color of the toggle. + +- grid: Displays the gridable grid for the layer. If you need more than one grid, use a layer proxy. The argument is optional, and is a the list of rows this component should include, if it doesn't have all of them. + +- layer-proxy: Lets you use components from another layer. The argument is a pair, `[layer, data]`, consisting of the id of the layer to proxy from, and the tabFormat for the components to show. + (Note: you cannot use a microtab within a layer proxy) + + +The rest of the components are sub-components. They can be used just like other components, but are typically part of another component. + +- upgrade, milestone, challenge, buyable, clickable, achievement, gridable: An individual upgrade, challenge, etc. The argument is the id. This can be used if you want to have upgrades split up across multiple subtabs, for example. + +- respec-button, master-button: The respec and master buttons for buyables and clickables, respectively. + +- sell-one, sell-all: The "sell one" and "sell all" for buyables, respectively. The argument is the id of the buyable. diff --git a/docs/grids.md b/docs/grids.md new file mode 100644 index 0000000..a71883e --- /dev/null +++ b/docs/grids.md @@ -0,0 +1,70 @@ +# Grids + +Grids are an easier way of making a group of similar clickables. They all have the same behavior, but are different based on their data. + +**NOTE: Gridables are similar to clickables in some respects, but are fundamentally different from normal TMT Big Features in quite a few ways. Be sure to keep these in mind:** + - Gridable ids use base 100 instead of base 10, so you can have more than 10 tiles in a row. This means that a grid might look like this: + 101 102 + 201 202 + - Individual gridables are not defined individually. All properties go directly into the "grid" object. Functions are called with arguments for the id of the gridables and its associated data, so you can give them the appropriate appearance and properties based on that. + - If you need two unrelated grids in a layer, you'll need to use a layer proxy component. + +Useful functions for dealing with grids: + +- getGridData(layer, id): get the data for the chosen gridable +- setGridData(layer, id, state): set the data for the chosen gridable +- gridEffect(layer, id): get the effect for the chosen gridable + +The grid should be formatted like this: + +```js +grid: { + rows: 4, // If these are dynamic make sure to have a max value as well! + cols: 5, + getStartData(id) { + return 0 + }, + getUnlocked(id) { // Default + return true + }, + getCanClick(data, id) { + return true + }, + onClick(data, id) { + player[this.layer].grid[id]++ + }, + getDisplay(data, id) { + return data + }, + + etc +} +``` + +Features: + +- rows, cols: The amount of rows and columns of gridable to display. + +- maxRows, maxCols: **sometimes needed**. If rows or cols are dynamic, you need to define the maximum amount that there can be (you can increase it when you update the game though). These CANNOT be dynamic. + +- getStartData(id): Creates the default data for the gridable at this position. This can be an object, or a regular value. + +- getUnlocked(id): **optional**. Returns true if the gridable at this position should be visible. + +- getTitle(data, id): **optional**. Returns text that should displayed at the top in a larger font, based on the position and data of the gridable. + +- getDisplay(data, id): **optional**. Returns everything that should be displayed on the gridable after the title, based on the position and data of the gridable. + +- getStyle(data, id): **optional**. Returns CSS to apply to this gridable, in the form of an object where the keys are CSS attributes, and the values are the values for those attributes (both as strings). + +- getCanClick(data, id): **optional**. A function returning a bool to determine if you can click a gridable, based on its data and position. If absent, you can always click it. + +- onClick(data, id): A function that implements clicking on the gridable, based on its position and data. + +- onHold(data, id): **optional** A function that is called 20x/sec when the button is held for at least 0.25 seconds. + +- getEffect(data, id): **optional**. A function that calculates and returns a gridable's effect, based on its position and data. (Whatever that means for a gridable) + +- getTooltip(data, id): **optional**. Adds a tooltip to the gridables, appears when they hovered over. Can use basic HTML. Default is no tooltip. If this returns an empty value, that also disables the tooltip. + +- layer: **assigned automagically**. It's the same value as the name of this layer, so you can do `player[this.layer].points` or similar. \ No newline at end of file diff --git a/docs/infoboxes.md b/docs/infoboxes.md new file mode 100644 index 0000000..79b06d5 --- /dev/null +++ b/docs/infoboxes.md @@ -0,0 +1,32 @@ +# Infoboxes + +Infoboxes are good for displaying "lore", or story elements, as well as for explaining complicated things. + +In the default tab layout, the first infobox will be displayed at the very top of the tab. + +Infoboxes are defined like other Big Features: + +```js +infoboxes: { + lore: { + title: "foo", + body() { return "bar" }, + etc + }, + etc +} +``` + +Features: + +- title: The text displayed above the main box. Can be a function to be dynamic, and can use basic HTML. + +- body: The text displayed inside the box. Can be a function to be dynamic, and can use basic HTML. + +- style, titleStyle, bodyStyle: **optional**. Apply CSS to the infobox, or to the title button or body of the infobox, in the form of an object where the keys are CSS attributes, and the values are the values for those attributes (both as strings). + +- unlocked(): **optional**. A function returning a bool to determine if the infobox is visible or not. Default is unlocked. + +- layer: **assigned automagically**. It's the same value as the name of this layer, so you can do `player[this.layer].points` or similar + +- id: **assigned automagically**. It's the "key" which the bar was stored under, for convenient access. The infobox in the example's id is "lore". \ No newline at end of file diff --git a/docs/layer-features.md b/docs/layer-features.md new file mode 100644 index 0000000..bcf0b11 --- /dev/null +++ b/docs/layer-features.md @@ -0,0 +1,192 @@ +# Layer Features + +This is a more comprehensive list of established features to add to layers. You can add more freely, if you want to have other functions or values associated with your layer. These have special functionality, though. + +You can make almost any value dynamic by using a function in its place, including all display strings and styling/color features. + +## Layer Definition features + +- layer: **assigned automagically**. It's the same value as the name of this layer, so you can do `player[this.layer].points` or similar to access the saved value. It makes copying code to new layers easier. It is also assigned to all upgrades and buyables and such. + +- name: **optional**. used in reset confirmations (and the default infobox title). If absent, it just uses the layer's id. + +- startData(): A function to return the default save data for this layer. Add any variables you have to it. Make sure to use `Decimal` values rather than normal numbers. + + Standard values: + - Required: + - unlocked: a bool determining if this layer is unlocked or not + - points: a Decimal, the main currency for the layer + - Optional: + - total: A Decimal, tracks total amount of main prestige currency. Always tracked, but only shown if you add it here. + - best: A Decimal, tracks highest amount of main prestige currency. Always tracked, but only shown if you add it here. + - unlockOrder: used to keep track of relevant layers unlocked before this one. + - resetTime: A number, time since this layer was last prestiged (or reset by another layer) + +- color: A color associated with this layer, used in many places. (A string in hex format with a #) + +- row: The row of the layer, starting at 0. This affects where the node appears on the standard tree, and which resets affect the layer. + + Using "side" instead of a number will cause the layer to appear off to the side as a smaller node (useful for achievements and statistics). Side layers are not affected by resets unless you add a doReset to them. + +- displayRow: **OVERRIDE** Changes where the layer node appears without changing where it is in the reset order. + +- resource: Name of the main currency you gain by resetting on this layer. + +- effect(): **optional**. A function that calculates and returns the current values of any bonuses inherent to the main currency. Can return a value or an object containing multiple values. *You will also have to implement the effect where it is applied.* + +- effectDescription: **optional**. A function that returns a description of this effect. If the text stays constant, it can just be a string. + +- layerShown(): **optional**, A function returning a bool which determines if this layer's node should be visible on the tree. It can also return "ghost", which will hide the layer, but its node will still take up space in the tree. + Defaults to true. + +- hotkeys: **optional**. An array containing information on any hotkeys associated with this layer: + + ```js + hotkeys: [ + { + key: "p", // What the hotkey button is. Use uppercase if it's combined with shift, or "ctrl+x" for holding down ctrl. + description: "p: reset your points for prestige points", // The description of the hotkey that is displayed in the game's How To Play tab + onPress() { if (player.p.unlocked) doReset("p") }, + unlocked() {return hasMilestone('p', 3)} // Determines if you can use the hotkey, optional + } + ] + ``` + +- style: **optional**. a "CSS object" where the keys are CSS attributes, containing any CSS that should affect this layer's entire tab. + +- tabFormat: **optional**. use this if you want to add extra things to your tab or change the layout. [See here for more info.](custom-tab-layouts.md) + +- midsection: **optional**, an alternative to `tabFormat`, which is inserted in between Milestones and Buyables in the standard tab layout. (cannot do subtabs) + +## Big features (all optional) + +- upgrades: A set of one-time purchases which can have unique upgrade conditions, currency costs, and bonuses. [See here for more info.](upgrades.md) + +- milestones: A list of bonuses gained upon reaching certain thresholds of a resource. Often used for automation/QOL. [See here for more info.](milestones.md) + +- challenges: The player can enter challenges, which make the game harder. If they reach a goal and beat the challenge, they recieve a bonus. [See here for more info.](challenges.md) + +- buyables: Effectively upgrades that can be bought multiple times, and are optionally respeccable. Many uses. [See here for more info.](buyables.md) + +- clickables: Extremely versatile and generalized buttons which can only be clicked sometimes. [See here for more info.](clickables.md) + +- microtabs: An area that functions like a set of subtabs, with buttons at the top changing the content within. (Advanced) [See here for more info.](subtabs-and-microtabs.md) + +- bars: Display some information as a progress bar, gague, or similar. They are highly customizable, and can be vertical as well. [See here for more info.](bars.md) + +- achievements: Kind of like milestones, but with a different display style and some other differences. Extra features are on the way at a later date! [See here for more info.](achievements.md) + +- achievementPopups, milestonePopups: **optional**, If false, disables popup message when you get the achievement/milestone. True by default. + +- infoboxes: Displays some text in a box that can be shown or hidden. [See here for more info.](infoboxes.md) + +- grid: A grid of buttons that behave the same, but have their own data.[See here for more info.](grids.md) + +## Prestige formula features + +- type: **optional**. Determines which prestige formula you use. Defaults to "none". + + - "normal": The amount of currency you gain is independent of its current amount (like Prestige). The formula before bonuses is based on `baseResource^exponent` + - "static": The cost is dependent on your total after reset. The formula before bonuses is based on `base^(x^exponent)` + - "custom": You can define everything, from the calculations to the text on the button, yourself. (See more at the bottom) + - "none": This layer does not prestige, and therefore does not need any of the other features in this section. + +- baseResource: The name of the resource that determines how much of the main currency you gain on reset. + +- baseAmount(): A function that gets the current value of the base resource. + +- requires: A Decimal, the amount of the base needed to gain 1 of the prestige currency. Also the amount required to unlock the layer. You can instead make this a function, to make it harder if another layer was unlocked first (based on unlockOrder). + +- exponent: Used as described above. + +- base: **sometimes required**. required for "static" layers, used as described above. If absent, defaults to 2. Must be greater than 1. + +- roundUpCost: **optional**. a bool, which is true if the resource cost needs to be rounded up. (use if the base resource is a "static" currency.) + +- gainMult(), gainExp(): **optional**. For normal layers, these functions calculate the multiplier and exponent on resource gain from upgrades and boosts and such. Plug in most bonuses here. + For static layers, they instead multiply and roots the cost of the resource. (So to make a boost you want to make gainMult smaller and gainExp larger.) + +- directMult(): **optional**. Directly multiplies the resource gain, after exponents and softcaps. For static layers, actually multiplies resource gain instead of reducing the cost. + +- softcap, softcapPower: **optional**. For normal layers, gain beyond [softcap] points is put to the [softcapPower]th power + Default for softcap is e1e7, and for power is 0.5. + +## Other prestige-related features + +- canBuyMax(): **sometimes required**. required for static layers, function used to determine if buying max is permitted. + +- onPrestige(gain): **optional**. A function that triggers when this layer prestiges, just before you gain the currency. Can be used to have secondary resource gain on prestige, or to recalculate things or whatnot. + +- resetDescription: **optional**. Use this to replace "Reset for " on the Prestige button with something else. + +- prestigeButtonText(): **sometimes required**. Use this to make the entirety of the text a Prestige button contains. Only required for custom layers, but usable by all types. + +- passiveGeneration(): **optional**, returns a regular number. You automatically generate your gain times this number every second (does nothing if absent) + This is good for automating Normal layers. + +- autoPrestige(): **optional**, returns a boolean, if true, the layer will always automatically do a prestige if it can. + This is good for automating Static layers. + +## Tree/node features + +- symbol: **optional**. The text that appears on this layer's node. Default is the layer id with the first letter capitalized. + +- image: **override**. The url (local or global) of an image that goes on the node. (Overrides symbol) + +- position: **optional**. Determines the horizontal position of the layer in its row in a standard tree. By default, it uses the layer id, and layers are sorted in alphabetical order. + +- branches: **optional**. An array of layer/node ids. On a tree, a line will appear from this layer to all of the layers in the list. Alternatively, an entry in the array can be a 2-element array consisting of the layer id and a color value. The color value can either be a string with a hex color code, or a number from 1-3 (theme-affected colors). A third element in the array optionally specifies line width. + +- nodeStyle: **optional**. A CSS object, where the keys are CSS attributes, which styles this layer's node on the tree. + +- tooltip() / tooltipLocked(): **optional**. Functions that return text, which is the tooltip for the node when the layer is unlocked or locked, respectively. By default the tooltips behave the same as in the original Prestige Tree. + If the value is "", the tooltip will be disabled. + +- marked: **optional** Adds a mark to the corner of the node. If it's "true" it will be a star, but it can also be an image URL. + +## Other features + +- doReset(resettingLayer): **optional**. Is triggered when a layer on a row greater than or equal to this one does a reset. The default behavior is to reset everything on the row, but only if it was triggered by a layer in a higher row. `doReset` is always called for side layers, but for these the default behavior is to reset nothing. + + If you want to keep things, determine what to keep based on `resettingLayer`, `milestones`, and such, then call `layerDataReset(layer, keep)`, where `layer` is this layer, and `keep` is an array of the names of things to keep. It can include things like "points", "best", "total" (for this layer's prestige currency), "upgrades", any unique variables like "generatorPower", etc. If you want to only keep specific upgrades or something like that, save them in a separate variable, then call `layerDataReset`, and then set `player[this.layer].upgrades` to the saved upgrades. + +- update(diff): **optional**. This function is called every game tick. Use it for any passive resource production or time-based things. `diff` is the time since the last tick. + +- autoUpgrade: **optional**, a boolean value, if true, the game will attempt to buy this layer's upgrades every tick. Defaults to false. + +- automate(): **optional**. This function is called every game tick, after production. Use it to activate automation things that aren't otherwise supported. + +- resetsNothing: **optional**. Returns true if this layer shouldn't trigger any resets when you prestige. + +- increaseUnlockOrder: **optional**. An array of layer ids. When this layer is unlocked for the first time, the `unlockOrder` value for any not-yet-unlocked layers in this list increases. This can be used to make them harder to unlock. + +- shouldNotify: **optional**. A function to return true if this layer should be highlighted in the tree. The layer will automatically be highlighted if you can buy an upgrade whether you have this or not. + +- glowColor: **optional**. The color that this layer will be highlighted if it should notify. The default is red. You can use this if you want several different notification types! + +- componentStyles: **optional**. An object that contains a set of functions returning CSS objects. Each of these will be applied to any components on the layer with the type of its id. Example: + +```js +componentStyles: { + "challenge"() { return {'height': '200px'} }, + "prestige-button"() { return {'color': '#AA66AA'} } +} +``` + +- leftTab: **optional**, if true, this layer will use the left tab instead of the right tab. + +- previousTab: **optional**, a layer's id. If a layer has a previousTab, the layer will always have a back arrow and pressing the back arrow on this layer will take you to the layer with this id. + +- deactivated: **optional**, if this is true, hasUpgrade, hasChallenge, hasAchievement, and hasMilestone will return false for things in the layer, and you will be unable to buy or click things, or gain achievements/milestones on the layer. You will have to disable effects of buyables, the innate layer effect, and possibly other things yourself. + +## Custom Prestige type +(All of these can also be used by other prestige types) + +- getResetGain(): **mostly for custom prestige type**. Returns how many points you should get if you reset now. You can call `getResetGain(this.layer, useType = "static")` or similar to calculate what your gain would be under another prestige type (provided you have all of the required features in the layer). + +- getNextAt(canMax=false): **mostly for custom prestige type**. Returns how many of the base currency you need to get to the next point. `canMax` is an optional variable used with Static-ish layers to differentiate between if it's looking for the first point you can reset at, or the requirement for any gain at all (Supporting both is good). You can also call `getNextAt(this.layer, canMax=false, useType = "static")` or similar to calculate what your next at would be under another prestige type (provided you have all of the required features in the layer). + +- canReset(): **mostly for custom prestige type**. Return true only if you have the resources required to do a prestige here. + +- prestigeNotify(): **mostly for custom prestige types**, returns true if this layer should be subtly highlighted to indicate you + can prestige for a meaningful gain. \ No newline at end of file diff --git a/docs/main-mod-info.md b/docs/main-mod-info.md new file mode 100644 index 0000000..cf2bc21 --- /dev/null +++ b/docs/main-mod-info.md @@ -0,0 +1,63 @@ +# mod.js + +Most of the non-layer code and data that you're likely to edit is here in [mod.js](/js/mod.js). +Everything in [mod.js](/js/mod.js) will not be altered by updates, besides the addition of new things. + +Here's a breakdown of what's in it: + +- modInfo is where most of the basic configuration for the mod is. It contains: + - name: The name of your mod. (a string) + - id: The id for your mod, a unique string that is used to determine savefile location. Be sure to set it when you start making a mod, and don't change it later because it will erase all saves. + - author: The name of the author, displayed in the info tab. + - pointsName: This changes what is displayed instead of "points" for the main currency. (It does not affect it in the code.) + - modFiles: An array of file addresses which will be loaded for this mod. Using smaller files makes it easier to find what you're looking for. + + - discordName, discordLink: If you have a Discord server or other discussion place, you can add a link to it. + + "discordName" is the text on the link, and "discordLink" is the url of an invite. If you're using a Discord invite, please make sure it's set to never expire. + + - offlineLimit: The maximum amount of offline time that the player can accumulate, in hours. Any extra time is lost. (a number) + + This is useful because most of these mods are fast-paced enough that too much offline time ruins the balance, such as the time in between updates. That is why I suggest developers disable offline time on their own savefile. + + - initialStartPoints: A Decimal for the amount of points a new player should start with. + +- VERSION is used to describe the current version of your mod. It contains: + - num: The mod's version number, displayed at the top right of the tree tab. + - name: The version's name, displayed alongside the number in the info tab. + +- changelog is the HTML displayed in the changelog tab. If this gets particularly long, it might be good to put in a separate file (be sure to add the file to index.html) + +- doNotCallTheseFunctionsEveryTick is very important, if you are adding non-standard functions. TMT calls every function anywhere in "layers" every tick to store the result, unless specifically told not to. Functions that have are used to do an action need to be identified. "Official" functions (those in the documentation) are all fine, but if you make any new ones, add their names to this array. + +```js +// (The ones here are examples, all official functions are already taken care of) +var doNotCallTheseFunctionsEveryTick = ["doReset", "buy", "onPurchase", "blowUpEverything"] +``` + +- getStartPoints(): A function to determine the amount of points the player starts with after a reset. (returns a Decimal value) + +- canGenPoints(): A function returning a boolean for if points should be generated. Use this if you want an upgrade to unlock generating points. + +- getPointGen(): A function that calculates your points per second. Anything that affects your point gain should go into the calculation here. + +- addedPlayerData(): A function that returns any non-layer-related data that you want to be added to the save data and "player" object. + +```js +function addedPlayerData() { return { + weather: "Yes", + happiness: new Decimal(72), +}} +``` + +- displayThings: An array of functions used to display extra things at the top of the tree tab. Each function returns a string, which is a line to display (with basic HTML support). If a function returns nothing, nothing is displayed (and it doesn't take up a line). + +- isEndgame(): A function to determine if the player has reached the end of the game, at which point the "you win!" screen appears. + +Less important things beyond this point! + +- backgroundStyle: A CSS object containing the styling for the background of the full game. Can be a function! + +- maxTickLength(): Returns the maximum tick length, in milliseconds. Only really useful if you have something that reduces over time, which long ticks mess up (usually a challenge). + +- fixOldSave(): Can be used to modify a save file when loading into a new version of the game. Use this to undo inflation, never forcibly hard reset your players. \ No newline at end of file diff --git a/docs/milestones.md b/docs/milestones.md new file mode 100644 index 0000000..8af1f8f --- /dev/null +++ b/docs/milestones.md @@ -0,0 +1,42 @@ +# Milestones + +Milestones are awarded to the player when they meet a certain goal, and give some benefit. Milestones should be formatted like this: + +```js +milestones: { + 0: { + requirementDescription: "123 waffles", + effectDescription: "blah", + done() { return player.w.points.gte(123) } + } + etc +} +``` + +You can use `hasMilestone(layer, id)` to determine if the player has a given milestone + +Milestone features: + +- requirementDescription: A string describing the requirement for unlocking this milestone. Suggestion: Use a "total". It can also be a function that returns updating text. Can use basic HTML. + +- effectDescription: A string describing the reward for having the milestone. *You will have to implement the reward elsewhere.* It can also be a function that returns updating text. Can use basic HTML. + +- done(): A function returning a boolean to determine if the milestone should be awarded. + +- onComplete() - **optional**. this function will be called when the milestone is completed. + +- toggles: **optional**. Creates toggle buttons that appear on the milestone when it is unlocked. The toggles can toggle a given boolean value in a layer. It is defined as an array of paired items, one pair per toggle. The first is the internal name of the layer the value being toggled is stored in, and the second is the internal name of the variable to toggle. (e.g. [["b", "auto"], ["g", "auto"]) + + **Tip:** Toggles are not de-set if the milestone becomes locked! In this case, you should also check if the player has the milestone. + +- style: **optional**. Applies CSS to this milestone, in the form of an object where the keys are CSS attributes, and the values are the values for those attributes (both as strings). + +- unlocked(): **optional**. A function returning a boolean to determine if the milestone should be shown. If absent, it is always shown. + +- tooltip: **optional**. Adds a tooltip to this milestone, appears when it is hovered over. Can use basic HTML. Default is no tooltip. If this returns an empty value, that also disables the tooltip. + +- layer: **assigned automagically**. It's the same value as the name of this layer, so you can do `player[this.layer].points` or similar. + +- id: **assigned automagically**. It's the "key" which the milestone was stored under, for convenient access. The milestone in the example's id is 0. + +Disable milestone popups by adding `milestonePopups: false` to the layer. diff --git a/docs/other.md b/docs/other.md new file mode 100644 index 0000000..34fb791 --- /dev/null +++ b/docs/other.md @@ -0,0 +1,8 @@ +# Other Things + +## CSS +A good way to give your game a unique feel is to customize the appearance of it. Modifying the CSS files helps you to do that on a global level. The CSS is broken up into several files to make it easier to find what is relevant to you. +CSS tip: Every component is automatically given a CSS class with the same name as its layer id. You can use this toapply something specifically for a single layer! You can also combine classes, such as .p.achievement or .c.locked, to change specific things in specific layers. + +## Temp +temp/tmp (either works) is a data structure that is a copy of layers (which contains all of the layer data you defined plus default things), but it replaces most functions with the result of calling those functions. e.g. if layer p's baseAmount is based on points, layers.p.baseAmount is a function that returns player.points. The player currently has 54 points, so temp.p.baseAmount is 54 (as a Decimal). You can use temp to improve performance. \ No newline at end of file diff --git a/docs/particles.md b/docs/particles.md new file mode 100644 index 0000000..d627011 --- /dev/null +++ b/docs/particles.md @@ -0,0 +1,63 @@ +# Particles + +Particles are free-floating elements that can move and have many different behaviors. They can also interact with the mouse. + +To make particles, use `makeParticles(particle, amount)`. `particle` is a particle-defining object, with features as explained below. There is also `makeShinies`, which uses different defaults and creates stationary particles at a random location. There are also a few other useful things listed at the end. + +```js + +const myParticle { + image:"options_wheel.png", + spread: 20, + gravity: 2, + time: 3, + speed() { // Randomize speed a bit + return (Math.random() + 1.2) * 8 + }, + etc... +} +``` + +Features can be functions or constant. These features will be called when each particle is made, with an `id` argument, which is assigned based on which of the `amount` particles being spawned this is. **All of these are optional**, with a default value. + +All distances are in pixels and angles are in degrees, with 0 being up and going clockwise. + +- time: The amount of time, in seconds, that the particle will last. Default is 3. +- fadeOutTime: The amount of seconds that fading out at the end should take (part of the total lifetime). Default is 1. +- fadeInTime: The amount of seconds that fading in should take (part of the total lifetime). Default is 0. + +- image: The image the particle should display. `""` will display no image. Default is a generic particle. +- text: Displays text on the particle. Can use basic HTML. +- style: Lets you apply other CSS styling to the particle. +- width, height: The dimensions of the particle. Default is 35 and 35. +- color: Sets the color of the image to this color. + +- angle: The angle that the particle should face. Default is 0. +- dir: The initial angle that the particles should move in, before spread is factored in. Default is whatever angle is. +- spread: If there are several particles, they will be spread out by this many degrees, centered on dir. Default is 30. + +- rotation: The amount that the (visual) angle of the particle should change by. Default is 0. +- speed: The starting speed of the particle. Default is 15. +- gravity: The amount the particle should accelerate downwards. Default is 0. + +- x, y: The starting coordinates of the particle. Default is at the mouse position. +- offset: How far from the start each particle should appear. Default is 10. +- xVel, yVel: Set initially based on other properties, then used to update movement. + +- layer: When changing tabs, if leaving the `layer` tab, this particle will be erased. +- You can add other features to particles, but you must impliment their effects yourself. + +Function features: These stay as functions and are for more advanced things. They are optional. + +- update(): Called each tick. Lets you do more advanced visual and movement behaviors by changing other properties. +- onClick(), onMouseOver(), onMouseLeave(): Called when the particle is interacted with. + + +Other useful things that are not features of the particle object: + +- setDir(particle, dir), setSpeed(particle, speed): Set the speed/direction on a particle. +- clearParticles(check): Function to delete particles. With no check, it deletes all particles. Check is a function that takes a particle, and returns true if that particle should be deleted. +- You can use Vue.delete(particles, this.id) to make a particle delete itself. +- mouseX and mouseY are variables that track the mouse position. +- sin(x), cos(x), tan(x): functions that do these operations, with x in degrees. (Instead of radians). +- asin(x), acos(x), atan(x): functions that do these operations, with the returned value in degrees. (instead of radians). \ No newline at end of file diff --git a/docs/subtabs-and-microtabs.md b/docs/subtabs-and-microtabs.md new file mode 100644 index 0000000..7382396 --- /dev/null +++ b/docs/subtabs-and-microtabs.md @@ -0,0 +1,59 @@ +# Subtabs and Microtabs + +Subtabs are separate sections of a tab that you can view by selecting one at the top of the tab. Microtabs are smaller areas that function in much the same way. You can also embed layers inside of subtabs/microtabs. + +Subtabs are defined by using the tab format like this, where each element of tabFormat is given the name of that subtab: + +```js +tabFormat: { + "Main tab": { + content: [tab format things], + *subtab features* + }, + "Other tab": { + content: [tab format things], + *subtab features* + }, + etc +} +``` + +Microtabs are defined similarly, and use the same features, but are defined in the "microtabs" feature. Each entry is a group of tabs which will appear in a microtabs component. The first set, "stuff", has 2 tabs, and the second, "otherStuff", has none. + +```js +microtabs: { + stuff: { + first: { + content: [tab format things], + *subtab features* + }, + second: { + content: [tab format things], + *subtab features* + } + }, + otherStuff: { + // There could be another set of microtabs here + } +} +``` + +Normal subtabs and microtab subtabs both use the same features: + +# Features: + +- content: The tab layout code for the subtab, in [the tab layout format](custom-tab-layouts.md). + +- style: **optional**. Applies CSS to the whole subtab when switched to, in the form of an "CSS Object", where the keys are CSS attributes, and the values are the values for those attributes (both as strings). + +- buttonStyle: **optional**. A CSS object, which affects the appearance of the button for that subtab. + +- unlocked(): **optional**. a function to determine if the button for this subtab should be visible. By default, a subtab is always unlocked. You can't use the "this" keyword in this function. + +- shouldNotify()/prestigeNotify(): **optional**, if true, the tab button will be highlighted to notify the player that there is something there. + +- glowColor: **optional**, specifies the color that the subtab glows. If this subtab is causing the main layer to node glow + (and it would't otherwise) the node also glows this color. Is NOT overridden by embedding a layer. + +- embedLayer: **SIGNIFICANT**, the id of another layer. If you have this, it will override "content", "style" and "shouldNotify", + instead displaying the entire layer in the subtab. \ No newline at end of file diff --git a/docs/trees-and-tree-customization.md b/docs/trees-and-tree-customization.md new file mode 100644 index 0000000..ec340f0 --- /dev/null +++ b/docs/trees-and-tree-customization.md @@ -0,0 +1,54 @@ +# Trees and tree customization + +If you want to have something beyond the standard tree on the left tab, you can do that in tree.js. You can change the layout +of the tree, including making non-layer nodes, change it into something other than a tree, or hide the left tab altogether. +This also introduces the "tree" component, which can be used in your layers as well. + +## layoutInfo +The most important part is layoutInfo, containing: +- startTab: The id of the default tab to show on the right at the start. +- startNavTab: The id of the default tab to show on the left at the start. + +- showTree: True if the tree tab should be shown at the start of the game. (The other tab will fill the whole page) +- treeLayout: If present, overrides the tree layout and places nodes as you describe instead (explained in the next section). + +Additionally, if you want the main layout to not be a tree, you can edit the "tree-tab" layer at the bottom of tree.js to modify it just like a normal layer's tab. You can even switch between left tabs, using showNavTab(layer) to make that layer appear on the left. + +## Trees + +The tree component is defined as an array of arrays of names of layers or nodes to show in the tree. They work just like layers/ +nodes in the main tree (but branches between nodes will only work on the first node if you have duplicates.) + +Here is an example tree: +```js +[["p"], + ["left", "blank", "right", "blank"] + ["a", "b", "blank", "c", "weirdButton"]] +``` + +## Nodes + +Nodes are non-layer buttons that can go in trees. They are defined similarly to layers, but with addNode instead of addLayer. + +Features: + +- color: **optional**, The node's color. (A string in hex format with a #) + +- symbol: **optional** The text on the button (The id capitalized by default) + +- canClick(): Returns true if the player can click the node. () + +- onClick(): The function called when the node is clicked. + +- layerShown(): **optional**, A function returning a bool which determines if this node should be visible. It can also return "ghost", which will hide the layer, but its node will still take up space in its tree. + +- branches: **optional**. An array of layer/node ids. On a tree, a line will appear from this node to all of the nodes in the list. Alternatively, an entry in the array can be a 2-element array consisting of the id and a color value. The color value can either be a string with a hex color code, or a number from 1-3 (theme-affected colors). + +- nodeStyle: **optional**. A CSS object, where the keys are CSS attributes, which styles this node on the tree. + +- tooltip() / tooltipLocked(): **optional**. Functions that return text, which is the tooltip for the node when the layer is unlocked or locked, respectively. By default the tooltips behave the same as in the original Prestige Tree. + +- row: **optional**, the row that this node appears in (for the default tree). + +- position: **optional**, Determines the horizontal position of the layer in its row in a default tree. By default, it uses the id, +and layers/nodes are sorted in alphabetical order. diff --git a/docs/tutorials/getting-started.md b/docs/tutorials/getting-started.md new file mode 100644 index 0000000..ed3bc48 --- /dev/null +++ b/docs/tutorials/getting-started.md @@ -0,0 +1,48 @@ +# Getting started + +Welcome to The Modding Tree! + +Using the Modding Tree, at its simplest level, just requires getting a copy of it onto your computer. However, if you do it the right way, it will help in many ways. + +Don't let the word "Github" scare you away. It's actually much easier to use than most people think, especially because most people use it the hard way. The key is Github Desktop, which lets you do everything you need to, without even touching the command line. + +The benefits of using Github: + +- It makes it much, much easier to update The Modding Tree. +- You can share your work without any extra effort using githack, or with a bit more effort, set up a github.io site. +- It lets you undo changes to your code, and to have multiple versions of it. +- It lets you collaborate with other people, if you want to. + +## Getting set up with Github Desktop, Visual Studio Code, and The Modding Tree: + +1. Install [Github Desktop](https://desktop.github.com/) and [Visual Studio Code](https://code.visualstudio.com/). + +2. Make a Github account. You can handle this on your own. + +3. Log in on your browser, and go back to [The Modding Tree page](https://github.com/Acamaeda/The-Modding-Tree). At the top right, there should be a button that says "fork". Click on it, and then on your username. You now have your own fork, or copy, of The Modding Tree. + +4. Open Github Desktop and log in. Ignore everything else and choose "clone a repository". A "repository" is basically a "Github project", like The Modding Tree. "Cloning" is downloading a copy of the repository to your computer. + +5. Look for The Modding Tree in the list of repositiories (it should be the only one) and click "clone". + +6. Select that you're using it for your own purposes, and click continue. It will download the files and handle everything. + +### Using your repository + +1. Click on "show in explorer/finder" to the right, and then open the index.html file in the folder. The page should open up on your browser. This will let you view and test your project locally! + +2. To edit your project, click "open in VSCode" in Github Desktop. + +3. Open [mod.js](/js/mod.js) in VSCode, and look at the top part where it has a "modInfo" object. Fill in your mod's name to whatever you want, and change the id as well. (It can be any string value, and it's used to determine where the savefile is. Make it something that's probably unique, and don't change it again later or else it'll effectively wipe existing saves) + +4. Save [mod.js](/js/mod.js), and then reload [index.html](/index.html) in your browser. The title on the tab, as well as on the info page, will now be updated! **You can reload the page every time you change the code to test it quickly and easily.** + +5. Go back to Github Desktop. It's time to save your changes into the git system by making a "commit". This basically saves your work and creates a snapshot of what your code looks like at this moment, allowing you to look back at it later. + +6. At the bottom right corner, add a summary of your changes, and then click "commit to master". + +7. Finally, at the top middle, click "push origin" to push your changes out onto the online repository. + +8. You can view your project online, or share it with others, by going to https://raw.githack.com/[YOUR-GITHUB-USERNAME]/The-Modding-Tree/master/index.html. **You do NOT need to do this to test your mod locally.** + +And now, you have successfully used Github! You can look at the next tutorial on [making a mod](making-a-mod.md), or look at the [documentation](/documentation/!general-info.md) to see how The Modding Tree's system works and to make your mod a reality. diff --git a/docs/tutorials/making-a-mod.md b/docs/tutorials/making-a-mod.md new file mode 100644 index 0000000..d8548ff --- /dev/null +++ b/docs/tutorials/making-a-mod.md @@ -0,0 +1,104 @@ +# Making a Mod + +This guide assumes you have already gone through the [getting started guide](getting-started.md). It will walk you through the basics of using TMT to create a mod. Let's get started! + +## Setting up mod info + +Open mod.js. This is where you define things that are for the mod in general as opposed to layer-specific. For now, modInfo, you can set the mod name and author name, and also the points name, which changes what the game calls your basic points (but they're still referred to as `player.points` in the code). **Be sure that you set a mod id as well**. + +One suggestion: When you're testing your mod, you should turn off offline progress in the in-game settings, and don't leave the game running when you aren't using it. You could unintentionally balance your game with large timewalls caused by this extra time. + +## Making a layer + +Now for the good stuff! Head into layers.js. There is a basic layer already there (although it has a few redundant things in it.) Let's see what it's doing... + +The most important thing is on the first line, where it says `addLayer("p", {` . This is how you create a new layer. The "p" here becomes the layer id, which is used throughout TMT to refer to the layer. You can change the id, but you will have to replace instances of "p" in the tutorial code as well. + +A layer is basically a big object with lots of different properties that you can set to create features. For fun customization, you can change a few things: + - name: Your layer's name! + - color: Sets the color of a lot of things for this layer. (Can be a hex code or the name of a color) + - symbol: The text that appears on this layer's node. + - resource: The name of this layer's main currency. + +Reload the page to see your newly customized layer! You can ignore the other features in the layer, for now. Most of it is involved in calculating how many prestige points you get. For now, let's make an upgrade! + +## Upgrades + +Upgrades are one of several Big Features in TMT, and most of them work the same way. Most of what applies to upgrades applies to milestones, buyables, etc. To add upgrades to your layer, after all of the other features, add a comma to the end of the last one, and then put: + +```js + upgrades: { + + }, +``` + +"upgrades" is an object, which contains an object for each upgrade. Each upgrade has an id that corresponds to its position. The upgrade "12" will appear as the second upgrade in the first row. + +Given that, let's make our first upgrade! Insert this line in between the brackets after "upgrades": + +```js + 11: { + + }, +``` + +Reload the page, and an upgrade will appear in the layer's tab! It will just be blank though. We need to fill out its features, which works similarly to giving a layer features. Here are the features we'll need: + +```js + title: "Make this whatever you want!", + description: "Double your point gain.", + cost: new Decimal(1), +``` + +Reload the page, and the upgrade will appear, fully formed! But it won't have any effect when you buy it! To impliment a boost, we need to go to the place where it is calculated. In this case, point gain is calculated in getPointGen in mod.js, so let's head over there. + +It's time to explain Decimals. Decimals are a special way of handling numbers over the normal Javascript limit. They are handled in a very different way. To perform any operations, instead of doing x = x + y, you have to do x = x.add(y). x has to be a Decimal, but y can be either a Decimal or Number (regular javascript number). A more detailed description is in [!general-info.md](/documentation/!general-info.md) + +With that knowledge in hand, what we need to do is check if the player has the upgrade, and then boost point gain if so. We can do that by inserting this line between gain being defined and returned: +```js +if (hasUpgrade('p', 11)) gain = gain.times(2) +``` + +Refresh the page again, and it should work! You are gaining 2 points per second! + + +## Upgraded upgrades + +Now that you know how to make a simple upgrade, let's make a more interesting one, that scales its effect based on your prestige points! + +Copying things is often the easiest way to do things, so copy upgrade 11 and paste it afterwards. Replace the 11 with a 12, and change the name and description as you see fit, and bump the cost up to 2. Now, let's add an effect. effect is a function that calculates the bonus from an upgrade, and effectDisplay lets you display the effect. + +```js + effect() { + return player[this.layer].points.add(1).pow(0.5) + }, + effectDisplay() { return format(upgradeEffect(this.layer, this.id))+"x" }, // Add formatting to the effect +``` + +this.layer and this.id are automatically set to the layer that the upgrade is in, and the id of the upgrade (in this case "12"). Using them makes it much easier to reuse code. You can also see that player[this.layer].points gets the prestige currency amount for this layer. + +Now, in mod.js, under the last line you added, you can apply the effect with + +```js + if (hasUpgrade('p', 12)) gain = gain.times(upgradeEffect('p', 12)) +``` + +Refresh it to see that it works! Now, for one last upgrade, let's make points boost prestige point gain! Copy the last upgrade, and change the number to 13. Change the title and name, set the cost to 5. (This mod is balanced to be fast-paced and easy to test). We can reuse the effectDisplay, so we just need to change the effect: + +```js + effect() { + return player.points.add(1).pow(0.15) + }, +``` + +To implement this effect, we modify gainMult, which returns the multiplier to this layer's prestige gain. + +```js + gainMult() { + let mult = new Decimal(1) + if (hasUpgrade('p', 13)) mult = mult.times(upgradeEffect('p', 13)) + return mult + }, +``` + +Refresh the page and see your new upgrade! Next time: a new layer... diff --git a/docs/tutorials/updating-tmt.md b/docs/tutorials/updating-tmt.md new file mode 100644 index 0000000..7ce0c02 --- /dev/null +++ b/docs/tutorials/updating-tmt.md @@ -0,0 +1,21 @@ +# Updating The Modding Tree + +This tutorial assumes that you have used [the Getting Started Tutorial](getting-started.md), and are using Github Desktop and VSCode for your mod. + +Here's what you have to do when there's a TMT update: + +1. Look at the changelog. It will warn you if the update will break anything or require any changes. Decide if you want to try to update. + +2. Open Github Desktop, and at the top middle, click "fetch origin". This will make Github Desktop get information about the update. + +3. Click where it says "current branch: master" at the top middle, and at the bottom of the thing that appears, click "choose a branch to merge into master". + +4. Select upstream/master. It will likely say there are conflicts, but you have tools to resolve them. Click "Merge upstream/master into master". + +5. A conflict happens when the things you're trying to merge have both made changes in the same place. Click "open in Visual Studio Code" next to the first file. + +6. Scroll down through the file, and look for the parts highlighted in red and green. One of these is your code, and the other is some code that will be modified by the update. Do your best to try to edit things to keep the updated changes, but keep your content. + +7. Continue to do this for all remaining changes. + +8. Do any other changes required by the update, run the game, fix issues, etc. diff --git a/docs/upgrades.md b/docs/upgrades.md new file mode 100644 index 0000000..e778fef --- /dev/null +++ b/docs/upgrades.md @@ -0,0 +1,70 @@ +# Upgrades + +Useful functions for dealing with Upgrades and implementing their effects: + +- hasUpgrade(layer, id): determine if the player has the upgrade +- upgradeEffect(layer, id): Returns the current effects of the upgrade, if any +- buyUpgrade(layer, id): Buys an upgrade directly (if affordable) + +Hint: Basic point gain is calculated in [mod.js](/js/mod.js)'s "getPointGen" function. + +Upgrades are stored in the following format: + +```js +upgrades: { + 11: { + description: "Blah", + cost: new Decimal(100), + etc + }, + etc +} +``` + +Usually, upgrades should have an id where the first digit is the row and the second digit is the column. + +Individual upgrades can have these features: + +- title: **optional**. Displayed at the top in a larger font. It can also be a function that returns updating text. Can use basic HTML. + +- description: A description of the upgrade's effect. *You will also have to implement the effect where it is applied.* It can also be a function that returns updating text. Can use basic HTML. + +- effect(): **optional**. A function that calculates and returns the current values of any bonuses from the upgrade. Can return a value or an object containing multiple values. + +- effectDisplay(): **optional**. A function that returns a display of the current effects of the upgrade with formatting. Default displays nothing. Can use basic HTML. + +- fullDisplay(): **OVERRIDE**. Overrides the other displays and descriptions, and lets you set the full text for the upgrade. Can use basic HTML. + +- cost: **sort of optional** A Decimal for the cost of the upgrade. By default, upgrades cost the main prestige currency for the layer. + +- unlocked(): **optional**. A function returning a bool to determine if the upgrade is visible or not. Default is unlocked. + +- onPurchase(): **optional**. This function will be called when the upgrade is purchased. Good for upgrades like "makes this layer act like it was unlocked first". + +- style: **optional**. Applies CSS to this upgrade, in the form of an object where the keys are CSS attributes, and the values are the values for those attributes (both as strings). + +- tooltip: **optional**. Adds a tooltip to this upgrade, appears when it is hovered over. Can use basic HTML. Default is no tooltip. If this returns an empty value, that also disables the tooltip. + +- layer: **assigned automagically**. It's the same value as the name of this layer, so you can do `player[this.layer].points` or similar. + +- id: **assigned automagically**. It's the "key" which the upgrade was stored under, for convenient access. The upgrade in the example's id is 11. + +By default, upgrades use the main prestige currency for the layer. You can include these to change them (but it needs to be a Decimal): + +- currencyDisplayName: **optional**. The name to display for the currency for the upgrade. + +- currencyInternalName: **optional**. The internal name for that currency. + +- currencyLayer: **optional**. The internal name of the layer that currency is stored in. If it's not in a layer (like Points), omit. If it's not stored directly in a layer, instead use the next feature. + +- currencyLocation: **optional**. If your currency is stored in something inside a layer (e.g. a buyable's amount), you can access it this way. This is a function returning the object in "player" that contains the value (like `player[this.layer].buyables`) + +If you want to do something more complicated like upgrades that cost two currencies, or have extra requirements, you can override the purchase system with these. (and you need to use fullDisplay if you don't use "cost") + +- canAfford(): **OVERRIDE**, a function determining if you are able to buy the upgrade. (If you also have a cost, it will check both the cost and this function) + +- pay(): **OVERRIDE**, a function that reduces your currencies when you buy the upgrade + + + +- branches: **optional**, This is primarially useful for upgrade trees. An array of upgrade ids. A line will appear from this upgrade to all of the upgrades in the list. Alternatively, an entry in the array can be a 2-element array consisting of the upgrade id and a color value. The color value can either be a string with a hex color code, or a number from 1-3 (theme-affected colors). A third element in the array optionally specifies line width. \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..827a067 --- /dev/null +++ b/index.html @@ -0,0 +1,139 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+

Loading... (If this takes too long it means there was a serious error!)←

+
+
+
+
+

{{modInfo.name}} {{VERSION.withoutName}}



+


+

Please check the Discord to see if there are new content updates!



+
It took you {{formatTime(player.timePlayed)}} to beat the game.
+
+      +


+ {{modInfo.discordName}}
+ The Modding Tree Discord
+ Main + Prestige Tree server
+

+
+ +
+
+ {{VERSION.withoutName}}
+ + +

i
+ + +
+
+ +
+
+
+ +
+



+ + + +
+ + + +
+
+ +
+
+ +
+
+
+ +
+
+ + +
+
+ +
+ \ No newline at end of file diff --git a/js/Demo/demoMod.js b/js/Demo/demoMod.js new file mode 100644 index 0000000..ee3c3ee --- /dev/null +++ b/js/Demo/demoMod.js @@ -0,0 +1,83 @@ +let modInfo = { + name: "The Modding Tree", + id: "modbase", + pointsName: "points", + modFiles: ["Demo/layers/c.js", "Demo/layers/f.js", "Demo/layers/a.js", "Demo/demoTree.js"], + + + discordName: "", + discordLink: "", + initialStartPoints: new Decimal (10), // Used for hard resets and new players + offlineLimit: 1, // In hours +} + +// Set your version in num and name +let VERSION = { + num: "2.6.6", + name: "Fixed Reality", +} + +let changelog = `

Changelog:


+

v0.0


+ - Added things.
+ - Added stuff.` + +let winText = `Congratulations! You have reached the end and beaten this game, but for now...` +// If you add new functions anywhere inside of a layer, and those functions have an effect when called, add them here. +// (The ones here are examples, all official functions are already taken care of) +var doNotCallTheseFunctionsEveryTick = ["doReset", "buy", "onPurchase", "blowUpEverything"] + +function getStartPoints(){ + return new Decimal(modInfo.initialStartPoints) +} + +// Determines if it should show points/sec +function canGenPoints(){ + return hasUpgrade("c", 11) +} + +// Calculate points/sec! +function getPointGen() { + if(!canGenPoints()) + return new Decimal(0) + + let gain = new Decimal(1) + if (hasUpgrade("c", 12)) gain = gain.times(upgradeEffect("c", 12)) + return gain +} + +// You can add non-layer related variables that should to into "player" and be saved here, along with default values +function addedPlayerData() { return { + weather: "Yes", + happiness: new Decimal(72), +}} + +// Display extra things at the top of the page +var displayThings = [ + function() {if (player.points.eq(69)) return "Tee hee!"}, + function() {if (player.f.points.gt(1)) return `You have ${player.f.points} farm points. (Which do nothing.)`}, + function() {if (inChallenge("c", 11)) return "The game is currently

0%

harder."}, +] + +// Determines when the game "ends" +function isEndgame() { + return player.points.gte(new Decimal("11")) +} + + + +// Less important things beyond this point! + +// Style for the background, can be a function +var backgroundStyle = { +} + +// You can change this if you have things that can be messed up by long tick lengths +function maxTickLength() { + return(3600) // Default is 1 hour which is just arbitrarily large +} + +// Use this if you need to undo inflation from an older version. If the version is older than the version that fixed the issue, +// you can cap their current resources with this. +function fixOldSave(oldVersion){ +} diff --git a/js/Demo/demoTree.js b/js/Demo/demoTree.js new file mode 100644 index 0000000..c60cfe0 --- /dev/null +++ b/js/Demo/demoTree.js @@ -0,0 +1,55 @@ +// treeLayout will override the default tree's layout if used +var layoutInfo = { + startTab: "c", + startNavTab: "tree-tab", + + showTree: true, + + //treeLayout: "" + + +} + +// A "ghost" layer which offsets f in the tree +addNode("spook", { + row: 1, + layerShown: "ghost", +}, +) + + +// A "ghost" layer which offsets f in the tree +addNode("g", { + symbol: "TH", + branches: [["c", "red", 4]], + color: '#6d3678', + layerShown: true, + canClick() {return player.points.gte(10)}, + tooltip: "Thanos your points", + tooltipLocked: "Thanos your points", + onClick() {player.points = player.points.div(2) + console.log(this.layer)} + +}, +) + + +// A "ghost" layer which offsets f in the tree +addNode("h", { + branches: ["g"], + layerShown: true, + tooltip() {return "Restore your points to " + player.c.otherThingy}, + tooltipLocked() {return "Restore your points to " + player.c.otherThingy}, + row: "side", + canClick() {return player.points.lt(player.c.otherThingy)}, + onClick() {player.points = new Decimal(player.c.otherThingy)} +}, +) + +addLayer("tree-tab", { + tabFormat: [["tree", function() {return (layoutInfo.treeLayout ? layoutInfo.treeLayout : TREE_LAYERS)}]], + previousTab: "", + leftTab: true, + style() {return {'background-color': '#222222'}}, + +}) \ No newline at end of file diff --git a/js/Demo/layers/a.js b/js/Demo/layers/a.js new file mode 100644 index 0000000..6e391c9 --- /dev/null +++ b/js/Demo/layers/a.js @@ -0,0 +1,65 @@ + +// A side layer with achievements, with no prestige +addLayer("a", { + startData() { return { + unlocked: true, + points: new Decimal(0), + }}, + color: "yellow", + resource: "achievement power", + row: "side", + tooltip() { // Optional, tooltip displays when the layer is locked + return ("Achievements") + }, + achievementPopups: true, + achievements: { + 11: { + image: "discord.png", + name: "Get me!", + done() {return true}, // This one is a freebie + goalTooltip: "How did this happen?", // Shows when achievement is not completed + doneTooltip: "You did it!", // Showed when the achievement is completed + }, + 12: { + name: "Impossible!", + done() {return false}, + goalTooltip: "Mwahahaha!", // Shows when achievement is not completed + doneTooltip: "HOW????", // Showed when the achievement is completed + textStyle: {'color': '#04e050'}, + }, + 13: { + name: "EIEIO", + done() {return player.f.points.gte(1)}, + tooltip: "Get a farm point.\n\nReward: The dinosaur is now your friend (you can max Farm Points).", // Showed when the achievement is completed + onComplete() {console.log("Bork bork bork!")} + }, + }, + midsection: ["grid", "blank"], + grid: { + maxRows: 3, + rows: 2, + cols: 2, + getStartData(id) { + return id + }, + getUnlocked(id) { // Default + return true + }, + getCanClick(data, id) { + return player.points.eq(10) + }, + getStyle(data, id) { + return {'background-color': '#'+ (data*1234%999999)} + }, + onClick(data, id) { // Don't forget onHold + player[this.layer].grid[id]++ + }, + getTitle(data, id) { + return "Gridable #" + id + }, + getDisplay(data, id) { + return data + }, + }, +}, +) \ No newline at end of file diff --git a/js/Demo/layers/c.js b/js/Demo/layers/c.js new file mode 100644 index 0000000..5ab4685 --- /dev/null +++ b/js/Demo/layers/c.js @@ -0,0 +1,396 @@ +var testTree = [["f", "c"], +["g", "spook", "h"]] + +addLayer("c", { + layer: "c", // This is assigned automatically, both to the layer and all upgrades, etc. Shown here so you know about it + name: "Candies", // This is optional, only used in a few places, If absent it just uses the layer id. + symbol: "C", // This appears on the layer's node. Default is the id with the first letter capitalized + position: 0, // Horizontal position within a row. By default it uses the layer id and sorts in alphabetical order + startData() { return { + unlocked: true, + points: new Decimal(0), + best: new Decimal(0), + total: new Decimal(0), + buyables: {}, // You don't actually have to initialize this one + beep: false, + thingy: "pointy", + otherThingy: 10, + drop: "drip", + }}, + color: "#4BDC13", + requires: new Decimal(10), // Can be a function that takes requirement increases into account + resource: "lollipops", // Name of prestige currency + baseResource: "points", // Name of resource prestige is based on + baseAmount() {return player.points}, // Get the current amount of baseResource + type: "normal", // normal: cost to gain currency depends on amount gained. static: cost depends on how much you already have + exponent: 0.5, // Prestige currency exponent + base: 5, // Only needed for static layers, base of the formula (b^(x^exp)) + roundUpCost: false, // True if the cost needs to be rounded up (use when baseResource is static?) + + // For normal layers, gain beyond [softcap] points is put to the [softcapPower]th power + softcap: new Decimal(1e100), + softcapPower: new Decimal(0.5), + canBuyMax() {}, // Only needed for static layers with buy max + gainMult() { // Calculate the multiplier for main currency from bonuses + mult = new Decimal(1) + if (hasUpgrade(this.layer, 166)) mult = mult.times(2) // These upgrades don't exist + if (hasUpgrade(this.layer, 120)) mult = mult.times(upgradeEffect(this.layer, 120)) + return mult + }, + gainExp() { // Calculate the exponent on main currency from bonuses + return new Decimal(1) + }, + row: 0, // Row the layer is in on the tree (0 is the first row) + effect() { + return { // Formulas for any boosts inherent to resources in the layer. Can return a single value instead of an object if there is just one effect + waffleBoost: (true == false ? 0 : Decimal.pow(player[this.layer].points, 0.2)), + icecreamCap: (player[this.layer].points * 10) + }}, + effectDescription() { // Optional text to describe the effects + eff = this.effect(); + eff.waffleBoost = eff.waffleBoost.times(buyableEffect(this.layer, 11).first) + return "which are boosting waffles by "+format(eff.waffleBoost)+" and increasing the Ice Cream cap by "+format(eff.icecreamCap) + }, + infoboxes:{ + coolInfo: { + title: "Lore", + titleStyle: {'color': '#FE0000'}, + body: "DEEP LORE!", + bodyStyle: {'background-color': "#0000EE"} + } + }, + milestones: { + 0: {requirementDescription: "3 Lollipops", + done() {return player[this.layer].best.gte(3)}, // Used to determine when to give the milestone + effectDescription: "Unlock the next milestone", + }, + 1: {requirementDescription: "4 Lollipops", + unlocked() {return hasMilestone(this.layer, 0)}, + done() {return player[this.layer].best.gte(4)}, + effectDescription: "You can toggle beep and boop (which do nothing)", + toggles: [ + ["c", "beep"], // Each toggle is defined by a layer and the data toggled for that layer + ["f", "boop"]], + style() { + if(hasMilestone(this.layer, this.id)) return { + 'background-color': '#1111DD' + }}, + }, + }, + challenges: { + + 11: { + name: "Fun", + completionLimit: 3, + challengeDescription() {return "Makes the game 0% harder
"+challengeCompletions(this.layer, this.id) + "/" + this.completionLimit + " completions"}, + unlocked() { return player[this.layer].best.gt(0) }, + goalDescription: 'Have 20 points I guess', + canComplete() { + return player.points.gte(20) + }, + rewardEffect() { + let ret = player[this.layer].points.add(1).tetrate(0.02) + return ret; + }, + rewardDisplay() { return format(this.rewardEffect())+"x" }, + countsAs: [12, 21], // Use this for if a challenge includes the effects of other challenges. Being in this challenge "counts as" being in these. + rewardDescription: "Says hi", + onComplete() {console.log("hiii")}, // Called when you successfully complete the challenge + onEnter() {console.log("So challenging")}, + onExit() {console.log("Sweet freedom!")}, + + }, + }, + upgrades: { + + 11: { + title: "Generator of Genericness", + description: "Gain 1 Point every second.", + cost: new Decimal(1), + unlocked() { return player[this.layer].unlocked }, // The upgrade is only visible when this is true + branches: [12], + tooltip: "hi", + }, + 12: { + description: "Point generation is faster based on your unspent Lollipops.", + cost: new Decimal(1), + unlocked() { return (hasUpgrade(this.layer, 11))}, + effect() { // Calculate bonuses from the upgrade. Can return a single value or an object with multiple values + let ret = player[this.layer].points.add(1).pow(player[this.layer].upgrades.includes(24)?1.1:(player[this.layer].upgrades.includes(14)?0.75:0.5)) + if (ret.gte("1e20000000")) ret = ret.sqrt().times("1e10000000") + return ret; + }, + effectDisplay() { return format(this.effect())+"x" }, // Add formatting to the effect + }, + 13: { + unlocked() { return (hasUpgrade(this.layer, 12))}, + onPurchase() { // This function triggers when the upgrade is purchased + player[this.layer].unlockOrder = 0 + }, + style() { + if (hasUpgrade(this.layer, this.id)) return { + 'background-color': '#1111dd' + } + else if (!canAffordUpgrade(this.layer, this.id)) { + return { + 'background-color': '#dd1111' + } + } // Otherwise use the default + }, + canAfford(){return player.points.lte(7)}, + pay(){player.points = player.points.add(7)}, + fullDisplay: "Only buyable with less than 7 points, and gives you 7 more. Unlocks a secret subtab." + }, + 22: { + title: "This upgrade doesn't exist", + description: "Or does it?.", + currencyLocation() {return player[this.layer].buyables}, // The object in player data that the currency is contained in + currencyDisplayName: "exhancers", // Use if using a nonstandard currency + currencyInternalName: 11, // Use if using a nonstandard currency + + cost: new Decimal(3), + unlocked() { return player[this.layer].unlocked }, // The upgrade is only visible when this is true + }, + }, + buyables: { + showRespec: true, + respec() { // Optional, reset things and give back your currency. Having this function makes a respec button appear + player[this.layer].points = player[this.layer].points.add(player[this.layer].spentOnBuyables) // A built-in thing to keep track of this but only keeps a single value + resetBuyables(this.layer) + doReset(this.layer, true) // Force a reset + }, + respecText: "Respec Thingies", // Text on Respec button, optional + respecMessage: "Are you sure? Respeccing these doesn't accomplish much.", + 11: { + title: "Exhancers", // Optional, displayed at the top in a larger font + cost(x) { // cost for buying xth buyable, can be an object if there are multiple currencies + if (x.gte(25)) x = x.pow(2).div(25) + let cost = Decimal.pow(2, x.pow(1.5)) + return cost.floor() + }, + effect(x) { // Effects of owning x of the items, x is a decimal + let eff = {} + if (x.gte(0)) eff.first = Decimal.pow(25, x.pow(1.1)) + else eff.first = Decimal.pow(1/25, x.times(-1).pow(1.1)) + + if (x.gte(0)) eff.second = x.pow(0.8) + else eff.second = x.times(-1).pow(0.8).times(-1) + return eff; + }, + display() { // Everything else displayed in the buyable button after the title + let data = tmp[this.layer].buyables[this.id] + return "Cost: " + format(data.cost) + " lollipops\n\ + Amount: " + player[this.layer].buyables[this.id] + "/4\n\ + Adds + " + format(data.effect.first) + " things and multiplies stuff by " + format(data.effect.second) + }, + unlocked() { return player[this.layer].unlocked }, + canAfford() { + return player[this.layer].points.gte(tmp[this.layer].buyables[this.id].cost)}, + buy() { + cost = tmp[this.layer].buyables[this.id].cost + player[this.layer].points = player[this.layer].points.sub(cost) + player[this.layer].buyables[this.id] = player[this.layer].buyables[this.id].add(1) + player[this.layer].spentOnBuyables = player[this.layer].spentOnBuyables.add(cost) // This is a built-in system that you can use for respeccing but it only works with a single Decimal value + }, + buyMax() {}, // You'll have to handle this yourself if you want + style: {'height':'222px'}, + purchaseLimit: new Decimal(4), + sellOne() { + let amount = getBuyableAmount(this.layer, this.id) + if (amount.lte(0)) return // Only sell one if there is at least one + setBuyableAmount(this.layer, this.id, amount.sub(1)) + player[this.layer].points = player[this.layer].points.add(this.cost) + }, + }, + }, + doReset(resettingLayer){ // Triggers when this layer is being reset, along with the layer doing the resetting. Not triggered by lower layers resetting, but is by layers on the same row. + if(layers[resettingLayer].row > this.row) layerDataReset(this.layer, ["points"]) + }, + layerShown() {return true}, // Condition for when layer appears on the tree + automate() { + }, // Do any automation inherent to this layer if appropriate + resetsNothing() {return false}, + onPrestige(gain) { + return + }, // Useful for if you gain secondary resources or have other interesting things happen to this layer when you reset it. You gain the currency after this function ends. + + hotkeys: [ + {key: "c", description: "C: reset for lollipops or whatever", onPress(){if (canReset(this.layer)) doReset(this.layer)}}, + {key: "ctrl+c", description: "Ctrl+c: respec things", onPress(){respecBuyables(this.layer)}, unlocked() {return hasUpgrade('c', '22')}} , + ], + increaseUnlockOrder: [], // Array of layer names to have their order increased when this one is first unlocked + + microtabs: { + stuff: { + first: { + content: ["upgrades", ["display-text", function() {return "confirmed
" + player.c.drop}], ["drop-down", ["drop", ["drip", "drop"]]]] + }, + second: { + embedLayer: "f", + + content: [["upgrade", 11], + ["row", [["upgrade", 11], "blank", "blank", ["upgrade", 11],]], + + ["display-text", function() {return "double confirmed"}]] + }, + }, + otherStuff: { + // There could be another set of microtabs here + } + }, + + bars: { + longBoi: { + fillStyle: {'background-color' : "#FFFFFF"}, + baseStyle: {'background-color' : "#696969"}, + textStyle: {'color': '#04e050'}, + borderStyle() {return {}}, + direction: RIGHT, + width: 300, + height: 30, + progress() { + return (player.points.add(1).log(10).div(10)).toNumber() + }, + display() { + return format(player.points) + " / 1e10 points" + }, + unlocked: true, + + }, + tallBoi: { + fillStyle: {'background-color' : "#4BEC13"}, + baseStyle: {'background-color' : "#000000"}, + textStyle: {'text-shadow': '0px 0px 2px #000000'}, + + borderStyle() {return {'border-width': "7px"}}, + direction: UP, + width: 50, + height: 200, + progress() { + return player.points.div(100) + }, + display() { + return formatWhole((player.points.div(1)).min(100)) + "%" + }, + unlocked: true, + + }, + flatBoi: { + fillStyle: {'background-color' : "#FE0102"}, + baseStyle: {'background-color' : "#222222"}, + textStyle: {'text-shadow': '0px 0px 2px #000000'}, + + borderStyle() {return {}}, + direction: UP, + width: 100, + height: 30, + progress() { + return player.c.points.div(50) + }, + unlocked: true, + + }, + }, + + // Optional, lets you format the tab yourself by listing components. You can create your own components in v.js. + tabFormat: { + "main tab": { + buttonStyle() {return {'color': 'orange'}}, + shouldNotify: true, + content: + ["main-display", + "prestige-button", "resource-display", + ["blank", "5px"], // Height + ["raw-html", function() {return ""}], + ["display-text", "Name your points!"], + ["text-input", "thingy"], + ["display-text", + function() {return 'I have ' + format(player.points) + ' ' + player[this.layer].thingy + ' points!'}, + {"color": "red", "font-size": "32px", "font-family": "Comic Sans MS"}], + "h-line", "milestones", "blank", "upgrades", "challenges"], + glowColor: "blue", + + }, + thingies: { + prestigeNotify: true, + style() {return {'background-color': '#222222'}}, + buttonStyle() {return {'border-color': 'orange'}}, + content:[ + "buyables", "blank", + ["row", [ + ["toggle", ["c", "beep"]], ["blank", ["30px", "10px"]], // Width, height + ["display-text", function() {return "Beep"}], "blank", ["v-line", "200px"], + ["column", [ + ["prestige-button", "", {'width': '150px', 'height': '80px'}], + ["prestige-button", "", {'width': '100px', 'height': '150px'}], + ]], + ], {'width': '600px', 'height': '350px', 'background-color': 'green', 'border-style': 'solid'}], + "blank", + ["display-image", "discord.png"],], + }, + jail: { + style() {return {'background-color': '#222222'}}, + + content: [ + ["infobox", "coolInfo"], + ["bar", "longBoi"], "blank", + ["row", [ + ["column", [ + ["display-text", "Sugar level:", {'color': 'teal'}], "blank", ["bar", "tallBoi"]], + {'background-color': '#555555', 'padding': '15px'}], + "blank", + ["column", [ + ["display-text", "idk"], + ["blank", ['0', '50px']], ["bar", "flatBoi"] + ]], + ]], + "blank", ["display-text", "It's jail because \"bars\"! So funny! Ha ha!"],["tree", testTree], + ], + }, + illuminati: { + unlocked() {return (hasUpgrade("c", 13))}, + content:[ + ["raw-html", function() {return "

C O N F I R M E D

"}], "blank", + ["microtabs", "stuff", {'width': '600px', 'height': '350px', 'background-color': 'brown', 'border-style': 'solid'}], + ["display-text", "Adjust how many points H gives you!"], + ["slider", ["otherThingy", 1, 30]], "blank", ["upgrade-tree", [[11], + [12, 22, 22, 11]]] + ] + } + + }, + style() {return { + //'background-color': '#3325CC' + }}, + nodeStyle() {return { // Style on the layer node + 'color': '#3325CC', + 'text-decoration': 'underline', + 'font-family': 'cursive' + }}, + glowColor: "orange", // If the node is highlighted, it will be this color (default is red) + componentStyles: { + "challenge"() {return {'height': '200px'}}, + "prestige-button"() {return {'color': '#AA66AA'}}, + }, + tooltip() { // Optional, tooltip displays when the layer is unlocked + let tooltip = formatWhole(player[this.layer].points) + " " + this.resource + if (player[this.layer].buyables[11].gt(0)) tooltip += "



" + formatWhole(player[this.layer].buyables[11]) + " Exhancers
" + return tooltip + }, + shouldNotify() { // Optional, layer will be highlighted on the tree if true. + // Layer will automatically highlight if an upgrade is purchasable. + return (player.c.buyables[11] == 1) + }, + marked: "discord.png", + resetDescription: "Melt your points into ", +}) + +const textParticle = { + spread: 20, + gravity: 0, + time: 3, + speed: 0, + text: function() { return "

" + format(player.points)}, + offset: 30, + fadeInTime: 1, +} \ No newline at end of file diff --git a/js/Demo/layers/f.js b/js/Demo/layers/f.js new file mode 100644 index 0000000..7bfce45 --- /dev/null +++ b/js/Demo/layers/f.js @@ -0,0 +1,147 @@ +// This layer is mostly minimal but it uses a custom prestige type and a clickable +addLayer("f", { + infoboxes:{ + coolInfo: { + title: "Lore", + titleStyle: {'color': '#FE0000'}, + body: "DEEP LORE!", + bodyStyle: {'background-color': "#0000EE"} + } + }, + + startData() { return { + unlocked: false, + points: new Decimal(0), + boop: false, + clickables: {[11]: "Start"}, // Optional default Clickable state + }}, + color: "#FE0102", + requires() {return new Decimal(10)}, + resource: "farm points", + baseResource: "points", + baseAmount() {return player.points}, + type: "static", + exponent: 0.5, + base: 3, + roundUpCost: true, + canBuyMax() {return false}, + //directMult() {return new Decimal(player.c.otherThingy)}, + + row: 1, + layerShown() {return true}, + branches: ["c"], // When this layer appears, a branch will appear from this layer to any layers here. Each entry can be a pair consisting of a layer id and a color. + + tooltipLocked() { // Optional, tooltip displays when the layer is locked + return ("This weird farmer dinosaur will only see you if you have at least " + this.requires() + " points. You only have " + formatWhole(player.points)) + }, + midsection: [ + "blank", ['display-image', 'https://images.beano.com/store/24ab3094eb95e5373bca1ccd6f330d4406db8d1f517fc4170b32e146f80d?auto=compress%2Cformat&dpr=1&w=390'], + ["display-text", "Bork bork!"] + ], + // The following are only currently used for "custom" Prestige type: + prestigeButtonText() { //Is secretly HTML + if (!this.canBuyMax()) return "Hi! I'm a weird dinosaur and I'll give you a Farm Point in exchange for all of your points and lollipops! (At least " + formatWhole(tmp[this.layer].nextAt) + " points)" + if (this.canBuyMax()) return "Hi! I'm a weird dinosaur and I'll give you " + formatWhole(tmp[this.layer].resetGain) + " Farm Points in exchange for all of your points and lollipops! (You'll get another one at " + formatWhole(tmp[this.layer].nextAtDisp) + " points)" + }, + getResetGain() { + return getResetGain(this.layer, useType = "static") + }, + getNextAt(canMax=false) { // + return getNextAt(this.layer, canMax, useType = "static") + }, + canReset() { + return tmp[this.layer].baseAmount.gte(tmp[this.layer].nextAt) + }, + // This is also non minimal, a Clickable! + clickables: { + + masterButtonPress() { + if (getClickableState(this.layer, 11) == "Borkened...") + player[this.layer].clickables[11] = "Start" + }, + masterButtonText() {return (getClickableState(this.layer, 11) == "Borkened...") ? "Fix the clickable!" : "Does nothing"}, // Text on Respec button, optional + 11: { + title: "Clicky clicky!", // Optional, displayed at the top in a larger font + display() { // Everything else displayed in the buyable button after the title + let data = getClickableState(this.layer, this.id) + return "Current state:
" + data + }, + unlocked() { return player[this.layer].unlocked }, + canClick() { + return getClickableState(this.layer, this.id) !== "Borkened..."}, + onClick() { + switch(getClickableState(this.layer, this.id)){ + case "Start": + player[this.layer].clickables[this.id] = "A new state!" + break; + case "A new state!": + player[this.layer].clickables[this.id] = "Keep going!" + break; + case "Keep going!": + player[this.layer].clickables[this.id] = "Maybe that's a bit too far..." + break; + case "Maybe that's a bit too far...": + makeParticles(coolParticle, 4) + player[this.layer].clickables[this.id] = "Borkened..." + break; + default: + player[this.layer].clickables[this.id] = "Start" + break; + } + }, + onHold(){ + console.log("Clickkkkk...") + }, + style() { + switch(getClickableState(this.layer, this.id)){ + case "Start": + return {'background-color': 'green'} + break; + case "A new state!": + return {'background-color': 'yellow'} + break; + case "Keep going!": + return {'background-color': 'orange'} + break; + case "Maybe that's a bit too far...": + return {'background-color': 'red'} + break; + default: + return {} + break; + }}, + }, + }, + +}, +) + +const coolParticle = { + image:"options_wheel.png", + spread: 20, + gravity: 2, + time: 3, + rotation (id) { + return 20 * (id - 1.5) + (Math.random() - 0.5) * 10 + }, + dir() { + return (Math.random() - 0.5) * 10 + }, + speed() { + return (Math.random() + 1.2) * 8 + }, + onClick() { + console.log("yay") + }, + onMouseOver() { + console.log("hi") + }, + onMouseLeave() { + console.log("bye") + }, + update() { + //this.width += 1 + //setDir(this, 135) + }, + layer: 'f', +} \ No newline at end of file diff --git a/js/components.js b/js/components.js new file mode 100644 index 0000000..aa3a4fa --- /dev/null +++ b/js/components.js @@ -0,0 +1,665 @@ +var app; + +function loadVue() { + // data = a function returning the content (actually HTML) + Vue.component('display-text', { + props: ['layer', 'data'], + template: ` + + ` + }) + +// data = a function returning the content (actually HTML) + Vue.component('raw-html', { + props: ['layer', 'data'], + template: ` + + ` + }) + + // Blank space, data = optional height in px or pair with width and height in px + Vue.component('blank', { + props: ['layer', 'data'], + template: ` +
+
+
+

+
+ ` + }) + + // Displays an image, data is the URL + Vue.component('display-image', { + props: ['layer', 'data'], + template: ` + + ` + }) + + // data = an array of Components to be displayed in a row + Vue.component('row', { + props: ['layer', 'data'], + computed: { + key() {return this.$vnode.key} + }, + template: ` +
+
+
+
+
+
+
+
+
+ ` + }) + + // data = an array of Components to be displayed in a column + Vue.component('column', { + props: ['layer', 'data'], + computed: { + key() {return this.$vnode.key} + }, + template: ` +
+
+
+
+
+
+
+
+
+ ` + }) + + // data [other layer, tabformat for within proxy] + Vue.component('layer-proxy', { + props: ['layer', 'data'], + computed: { + key() {return this.$vnode.key} + }, + template: ` +
+ +
+ ` + }) + Vue.component('infobox', { + props: ['layer', 'data'], + template: ` +
+ +
+ +
+
+ ` + }) + + + // Data = width in px, by default fills the full area + Vue.component('h-line', { + props: ['layer', 'data'], + template:` +
+ ` + }) + + // Data = height in px, by default is bad + Vue.component('v-line', { + props: ['layer', 'data'], + template: ` +
+ ` + }) + + Vue.component('challenges', { + props: ['layer', 'data'], + template: ` +
+
+
+ +
+
+
+ ` + }) + + // data = id + Vue.component('challenge', { + props: ['layer', 'data'], + template: ` +
+



+

+ + +
+ Goal: {{format(tmp[layer].challenges[data].goal)}} {{tmp[layer].challenges[data].currencyDisplayName ? tmp[layer].challenges[data].currencyDisplayName : modInfo.pointsName}}
+ Reward:
+ Currently: +
+ + +
+ ` + }) + + Vue.component('upgrades', { + props: ['layer', 'data'], + template: ` +
+
+
+ +
+
+
+
+ ` + }) + + // data = id + Vue.component('upgrade', { + props: ['layer', 'data'], + template: ` + + ` + }) + + Vue.component('milestones', { + props: ['layer', 'data'], + template: ` +
+ + + + +
+
+
+ ` + }) + + // data = id + Vue.component('milestone', { + props: ['layer', 'data'], + template: ` + +


+
+ + +   + ` + }) + + Vue.component('toggle', { + props: ['layer', 'data'], + template: ` + + ` + }) + + Vue.component('prestige-button', { + props: ['layer', 'data'], + template: ` + + ` + + }) + + // Displays the main resource for the layer + Vue.component('main-display', { + props: ['layer', 'data'], + template: ` +
You have

{{data ? format(player[layer].points, data) : formatWhole(player[layer].points)}}

{{tmp[layer].resource}},

+ ` + }) + + // Displays the base resource for the layer, as well as the best and total values for the layer's currency, if tracked + Vue.component('resource-display', { + props: ['layer'], + template: ` +
+
You have {{formatWhole(tmp[layer].baseAmount)}} {{tmp[layer].baseResource}}
+
You are gaining {{format(tmp[layer].resetGain.times(tmp[layer].passiveGeneration))}} {{tmp[layer].resource}} per second
+

+ Your best {{tmp[layer].resource}} is {{formatWhole(player[layer].best)}}
+ You have made a total of {{formatWhole(player[layer].total)}} {{tmp[layer].resource}}
+
+ ` + }) + + Vue.component('buyables', { + props: ['layer', 'data'], + template: ` +
+ +
+
+ +
+
+
+
+ ` + }) + + Vue.component('buyable', { + props: ['layer', 'data'], + template: ` +
+ +
+ + +
+ `, + data() { return { interval: false, time: 0,}}, + methods: { + start() { + if (!this.interval) { + this.interval = setInterval((function() { + if(this.time >= 5) + buyBuyable(this.layer, this.data) + this.time = this.time+1 + }).bind(this), 50)} + }, + stop() { + clearInterval(this.interval) + this.interval = false + this.time = 0 + } + }, + }) + + Vue.component('respec-button', { + props: ['layer', 'data'], + template: ` +
+
+ +
+ ` + }) + + Vue.component('clickables', { + props: ['layer', 'data'], + template: ` +
+ +
+
+ +
+
+
+
+ ` + }) + + // data = id of clickable + Vue.component('clickable', { + props: ['layer', 'data'], + template: ` + + `, + data() { return { interval: false, time: 0,}}, + methods: { + start() { + if (!this.interval && layers[this.layer].clickables[this.data].onHold) { + this.interval = setInterval((function() { + let c = layers[this.layer].clickables[this.data] + if(this.time >= 5 && run(c.canClick, c)) { + run(c.onHold, c) + } + this.time = this.time+1 + }).bind(this), 50)} + }, + stop() { + clearInterval(this.interval) + this.interval = false + this.time = 0 + } + }, + }) + + Vue.component('master-button', { + props: ['layer', 'data'], + template: ` + + ` + }) + + + // data = optionally, array of rows for the grid to show + Vue.component('grid', { + props: ['layer', 'data'], + template: ` +
+
+
+ +
+
+
+
+ ` + }) + + Vue.component('gridable', { + props: ['layer', 'data'], + template: ` + + `, + data() { return { interval: false, time: 0,}}, + computed: { + canClick() { + return gridRun(this.layer, 'getCanClick', player[this.layer].grid[this.data], this.data)} + }, + methods: { + start() { + if (!this.interval && layers[this.layer].grid.onHold) { + this.interval = setInterval((function() { + if(this.time >= 5 && gridRun(this.layer, 'getCanClick', player[this.layer].grid[this.data], this.data)) { + gridRun(this.layer, 'onHold', player[this.layer].grid[this.data], this.data) } + this.time = this.time+1 + }).bind(this), 50)} + }, + stop() { + clearInterval(this.interval) + this.interval = false + this.time = 0 + } + }, + }) + + // data = id of microtab family + Vue.component('microtabs', { + props: ['layer', 'data'], + computed: { + currentTab() {return player.subtabs[layer][data]} + }, + template: ` +
+
+ +
+ + + +
+ ` + }) + + + // data = id of the bar + Vue.component('bar', { + props: ['layer', 'data'], + computed: { + style() {return constructBarStyle(this.layer, this.data)} + }, + template: ` +
+
+ +
+
+
+
+
+ ` + }) + + + Vue.component('achievements', { + props: ['layer', 'data'], + template: ` +
+
+
+ +
+
+
+
+ ` + }) + + // data = id + Vue.component('achievement', { + props: ['layer', 'data'], + template: ` +
+ +


+
+ ` + }) + + // Data is an array with the structure of the tree + Vue.component('tree', { + props: ['layer', 'data'], + computed: { + key() {return this.$vnode.key} + }, + template: `
+ + + + +
+
+ + ` + }) + + // Data is an array with the structure of the tree + Vue.component('upgrade-tree', { + props: ['layer', 'data'], + computed: { + key() {return this.$vnode.key} + }, + template: `` + }) + + // Data is an array with the structure of the tree + Vue.component('buyable-tree', { + props: ['layer', 'data'], + computed: { + key() {return this.$vnode.key} + }, + template: `` + }) + + // Data is an array with the structure of the tree + Vue.component('clickable-tree', { + props: ['layer', 'data'], + computed: { + key() {return this.$vnode.key} + }, + template: `` + }) + + Vue.component('thing-tree', { + props: ['layer', 'data', 'type'], + computed: { + key() {return this.$vnode.key} + }, + template: `
+ + +
+
+
+
+ ` + }) + + + // Updates the value in player[layer][data] + Vue.component('text-input', { + props: ['layer', 'data'], + template: ` + + ` + }) + + // Updates the value in player[layer][data][0] + Vue.component('slider', { + props: ['layer', 'data'], + template: ` +
+
+ ` + }) + + // Updates the value in player[layer][data[0]], options are an array in data[1] + Vue.component('drop-down', { + props: ['layer', 'data'], + template: ` + + ` + }) + // These are for buyables, data is the id of the corresponding buyable + Vue.component('sell-one', { + props: ['layer', 'data'], + template: ` + + ` + }) + Vue.component('sell-all', { + props: ['layer', 'data'], + template: ` + + ` + }) + + // SYSTEM COMPONENTS + Vue.component('node-mark', systemComponents['node-mark']) + Vue.component('tab-buttons', systemComponents['tab-buttons']) + Vue.component('tree-node', systemComponents['tree-node']) + Vue.component('layer-tab', systemComponents['layer-tab']) + Vue.component('overlay-head', systemComponents['overlay-head']) + Vue.component('info-tab', systemComponents['info-tab']) + Vue.component('options-tab', systemComponents['options-tab']) + Vue.component('tooltip', systemComponents['tooltip']) + Vue.component('particle', systemComponents['particle']) + Vue.component('bg', systemComponents['bg']) + + + app = new Vue({ + el: "#app", + data: { + player, + tmp, + options, + Decimal, + format, + formatWhole, + formatTime, + formatSmall, + focused, + getThemeName, + layerunlocked, + doReset, + buyUpg, + buyUpgrade, + startChallenge, + milestoneShown, + keepGoing, + hasUpgrade, + hasMilestone, + hasAchievement, + hasChallenge, + maxedChallenge, + getBuyableAmount, + getClickableState, + inChallenge, + canAffordUpgrade, + canBuyBuyable, + canCompleteChallenge, + subtabShouldNotify, + subtabResetNotify, + challengeStyle, + challengeButtonText, + constructBarStyle, + constructParticleStyle, + VERSION, + LAYERS, + hotkeys, + activePopups, + particles, + mouseX, + mouseY, + shiftDown, + ctrlDown, + run, + gridRun, + }, + }) +} + + diff --git a/js/game.js b/js/game.js new file mode 100644 index 0000000..dc90a68 --- /dev/null +++ b/js/game.js @@ -0,0 +1,430 @@ +var player; +var needCanvasUpdate = true; + +// Don't change this +const TMT_VERSION = { + tmtNum: "2.6.6.2", + tmtName: "Fixed Reality" +} + +function getResetGain(layer, useType = null) { + let type = useType + if (!useType){ + type = tmp[layer].type + if (layers[layer].getResetGain !== undefined) + return layers[layer].getResetGain() + } + if(tmp[layer].type == "none") + return new Decimal (0) + if (tmp[layer].gainExp.eq(0)) return decimalZero + if (type=="static") { + if ((!tmp[layer].canBuyMax) || tmp[layer].baseAmount.lt(tmp[layer].requires)) return decimalOne + let gain = tmp[layer].baseAmount.div(tmp[layer].requires).div(tmp[layer].gainMult).max(1).log(tmp[layer].base).times(tmp[layer].gainExp).pow(Decimal.pow(tmp[layer].exponent, -1)) + gain = gain.times(tmp[layer].directMult) + return gain.floor().sub(player[layer].points).add(1).max(1); + } else if (type=="normal"){ + if (tmp[layer].baseAmount.lt(tmp[layer].requires)) return decimalZero + let gain = tmp[layer].baseAmount.div(tmp[layer].requires).pow(tmp[layer].exponent).times(tmp[layer].gainMult).pow(tmp[layer].gainExp) + if (gain.gte(tmp[layer].softcap)) gain = gain.pow(tmp[layer].softcapPower).times(tmp[layer].softcap.pow(decimalOne.sub(tmp[layer].softcapPower))) + gain = gain.times(tmp[layer].directMult) + return gain.floor().max(0); + } else if (type=="custom"){ + return layers[layer].getResetGain() + } else { + return decimalZero + } +} + +function getNextAt(layer, canMax=false, useType = null) { + let type = useType + if (!useType) { + type = tmp[layer].type + if (layers[layer].getNextAt !== undefined) + return layers[layer].getNextAt(canMax) + + } + if(tmp[layer].type == "none") + return new Decimal (Infinity) + + if (tmp[layer].gainMult.lte(0)) return new Decimal(Infinity) + if (tmp[layer].gainExp.lte(0)) return new Decimal(Infinity) + + if (type=="static") + { + if (!tmp[layer].canBuyMax) canMax = false + let amt = player[layer].points.plus((canMax&&tmp[layer].baseAmount.gte(tmp[layer].nextAt))?tmp[layer].resetGain:0).div(tmp[layer].directMult) + let extraCost = Decimal.pow(tmp[layer].base, amt.pow(tmp[layer].exponent).div(tmp[layer].gainExp)).times(tmp[layer].gainMult) + let cost = extraCost.times(tmp[layer].requires).max(tmp[layer].requires) + if (tmp[layer].roundUpCost) cost = cost.ceil() + return cost; + } else if (type=="normal"){ + let next = tmp[layer].resetGain.add(1).div(tmp[layer].directMult) + if (next.gte(tmp[layer].softcap)) next = next.div(tmp[layer].softcap.pow(decimalOne.sub(tmp[layer].softcapPower))).pow(decimalOne.div(tmp[layer].softcapPower)) + next = next.root(tmp[layer].gainExp).div(tmp[layer].gainMult).root(tmp[layer].exponent).times(tmp[layer].requires).max(tmp[layer].requires) + if (tmp[layer].roundUpCost) next = next.ceil() + return next; + } else if (type=="custom"){ + return layers[layer].getNextAt(canMax) + } else { + return decimalZero + }} + +function softcap(value, cap, power = 0.5) { + if (value.lte(cap)) return value + else + return value.pow(power).times(cap.pow(decimalOne.sub(power))) +} + +// Return true if the layer should be highlighted. By default checks for upgrades only. +function shouldNotify(layer){ + for (id in tmp[layer].upgrades){ + if (isPlainObject(layers[layer].upgrades[id])){ + if (canAffordUpgrade(layer, id) && !hasUpgrade(layer, id) && tmp[layer].upgrades[id].unlocked){ + return true + } + } + } + if (player[layer].activeChallenge && canCompleteChallenge(layer, player[layer].activeChallenge)) { + return true + } + + if (tmp[layer].shouldNotify) + return true + + if (isPlainObject(tmp[layer].tabFormat)) { + for (subtab in tmp[layer].tabFormat){ + if (subtabShouldNotify(layer, 'mainTabs', subtab)) { + tmp[layer].trueGlowColor = tmp[layer].tabFormat[subtab].glowColor || defaultGlow + + return true + } + } + } + + for (family in tmp[layer].microtabs) { + for (subtab in tmp[layer].microtabs[family]){ + if (subtabShouldNotify(layer, family, subtab)) { + tmp[layer].trueGlowColor = tmp[layer].microtabs[family][subtab].glowColor + return true + } + } + } + + return false + +} + +function canReset(layer) +{ + if (layers[layer].canReset!== undefined) + return run(layers[layer].canReset, layers[layer]) + else if(tmp[layer].type == "normal") + return tmp[layer].baseAmount.gte(tmp[layer].requires) + else if(tmp[layer].type== "static") + return tmp[layer].baseAmount.gte(tmp[layer].nextAt) + else + return false +} + +function rowReset(row, layer) { + for (lr in ROW_LAYERS[row]){ + if(layers[lr].doReset) { + if (!isNaN(row)) Vue.set(player[lr], "activeChallenge", null) // Exit challenges on any row reset on an equal or higher row + run(layers[lr].doReset, layers[lr], layer) + } + else + if(tmp[layer].row > tmp[lr].row && !isNaN(row)) layerDataReset(lr) + } +} + +function layerDataReset(layer, keep = []) { + let storedData = {unlocked: player[layer].unlocked, forceTooltip: player[layer].forceTooltip, noRespecConfirm: player[layer].noRespecConfirm, prevTab:player[layer].prevTab} // Always keep these + + for (thing in keep) { + if (player[layer][keep[thing]] !== undefined) + storedData[keep[thing]] = player[layer][keep[thing]] + } + + Vue.set(player[layer], "buyables", getStartBuyables(layer)) + Vue.set(player[layer], "clickables", getStartClickables(layer)) + Vue.set(player[layer], "challenges", getStartChallenges(layer)) + Vue.set(player[layer], "grid", getStartGrid(layer)) + + layOver(player[layer], getStartLayerData(layer)) + player[layer].upgrades = [] + player[layer].milestones = [] + player[layer].achievements = [] + + for (thing in storedData) { + player[layer][thing] =storedData[thing] + } +} + + + +function addPoints(layer, gain) { + player[layer].points = player[layer].points.add(gain).max(0) + if (player[layer].best) player[layer].best = player[layer].best.max(player[layer].points) + if (player[layer].total) player[layer].total = player[layer].total.add(gain) +} + +function generatePoints(layer, diff) { + addPoints(layer, tmp[layer].resetGain.times(diff)) +} + +function doReset(layer, force=false) { + if (tmp[layer].type == "none") return + let row = tmp[layer].row + if (!force) { + + if (tmp[layer].canReset === false) return; + + if (tmp[layer].baseAmount.lt(tmp[layer].requires)) return; + let gain = tmp[layer].resetGain + if (tmp[layer].type=="static") { + if (tmp[layer].baseAmount.lt(tmp[layer].nextAt)) return; + gain =(tmp[layer].canBuyMax ? gain : 1) + } + + + if (layers[layer].onPrestige) + run(layers[layer].onPrestige, layers[layer], gain) + + addPoints(layer, gain) + updateMilestones(layer) + updateAchievements(layer) + + if (!player[layer].unlocked) { + player[layer].unlocked = true; + needCanvasUpdate = true; + + if (tmp[layer].increaseUnlockOrder){ + lrs = tmp[layer].increaseUnlockOrder + for (lr in lrs) + if (!player[lrs[lr]].unlocked) player[lrs[lr]].unlockOrder++ + } + } + + } + + if (run(layers[layer].resetsNothing, layers[layer])) return + tmp[layer].baseAmount = decimalZero // quick fix + + + for (layerResetting in layers) { + if (row >= layers[layerResetting].row && (!force || layerResetting != layer)) completeChallenge(layerResetting) + } + + player.points = (row == 0 ? decimalZero : getStartPoints()) + + for (let x = row; x >= 0; x--) rowReset(x, layer) + for (r in OTHER_LAYERS){ + rowReset(r, layer) + } + + player[layer].resetTime = 0 + + updateTemp() + updateTemp() +} + +function resetRow(row) { + if (prompt('Are you sure you want to reset this row? It is highly recommended that you wait until the end of your current run before doing this! Type "I WANT TO RESET THIS" to confirm')!="I WANT TO RESET THIS") return + let pre_layers = ROW_LAYERS[row-1] + let layers = ROW_LAYERS[row] + let post_layers = ROW_LAYERS[row+1] + rowReset(row+1, post_layers[0]) + doReset(pre_layers[0], true) + for (let layer in layers) { + player[layer].unlocked = false + if (player[layer].unlockOrder) player[layer].unlockOrder = 0 + } + player.points = getStartPoints() + updateTemp(); + resizeCanvas(); +} + +function startChallenge(layer, x) { + let enter = false + if (!player[layer].unlocked || !tmp[layer].challenges[x].unlocked) return + if (player[layer].activeChallenge == x) { + completeChallenge(layer, x) + Vue.set(player[layer], "activeChallenge", null) + } else { + enter = true + } + doReset(layer, true) + if(enter) { + Vue.set(player[layer], "activeChallenge", x) + run(layers[layer].challenges[x].onEnter, layers[layer].challenges[x]) + } + updateChallengeTemp(layer) +} + +function canCompleteChallenge(layer, x) +{ + if (x != player[layer].activeChallenge) return + let challenge = tmp[layer].challenges[x] + if (challenge.canComplete !== undefined) return challenge.canComplete + + if (challenge.currencyInternalName){ + let name = challenge.currencyInternalName + if (challenge.currencyLocation){ + return !(challenge.currencyLocation[name].lt(challenge.goal)) + } + else if (challenge.currencyLayer){ + let lr = challenge.currencyLayer + return !(player[lr][name].lt(challenge.goal)) + } + else { + return !(player[name].lt(challenge.goal)) + } + } + else { + return !(player.points.lt(challenge.goal)) + } + +} + +function completeChallenge(layer, x) { + var x = player[layer].activeChallenge + if (!x) return + + let completions = canCompleteChallenge(layer, x) + if (!completions){ + Vue.set(player[layer], "activeChallenge", null) + run(layers[layer].challenges[x].onExit, layers[layer].challenges[x]) + return + } + if (player[layer].challenges[x] < tmp[layer].challenges[x].completionLimit) { + needCanvasUpdate = true + player[layer].challenges[x] += completions + player[layer].challenges[x] = Math.min(player[layer].challenges[x], tmp[layer].challenges[x].completionLimit) + if (layers[layer].challenges[x].onComplete) run(layers[layer].challenges[x].onComplete, layers[layer].challenges[x]) + } + Vue.set(player[layer], "activeChallenge", null) + run(layers[layer].challenges[x].onExit, layers[layer].challenges[x]) + updateChallengeTemp(layer) +} + +VERSION.withoutName = "v" + VERSION.num + (VERSION.pre ? " Pre-Release " + VERSION.pre : VERSION.pre ? " Beta " + VERSION.beta : "") +VERSION.withName = VERSION.withoutName + (VERSION.name ? ": " + VERSION.name : "") + + +function autobuyUpgrades(layer){ + if (!tmp[layer].upgrades) return + for (id in tmp[layer].upgrades) + if (isPlainObject(tmp[layer].upgrades[id]) && (layers[layer].upgrades[id].canAfford === undefined || layers[layer].upgrades[id].canAfford() === true)) + buyUpg(layer, id) +} + +function gameLoop(diff) { + if (isEndgame() || tmp.gameEnded){ + tmp.gameEnded = true + clearParticles() + } + + if (isNaN(diff) || diff < 0) diff = 0 + if (tmp.gameEnded && !player.keepGoing) { + diff = 0 + //player.tab = "tmp.gameEnded" + clearParticles() + } + + if (maxTickLength) { + let limit = maxTickLength() + if(diff > limit) + diff = limit + } + addTime(diff) + player.points = player.points.add(tmp.pointGen.times(diff)).max(0) + + for (let x = 0; x <= maxRow; x++){ + for (item in TREE_LAYERS[x]) { + let layer = TREE_LAYERS[x][item] + player[layer].resetTime += diff + if (tmp[layer].passiveGeneration) generatePoints(layer, diff*tmp[layer].passiveGeneration); + if (layers[layer].update) layers[layer].update(diff); + } + } + + for (row in OTHER_LAYERS){ + for (item in OTHER_LAYERS[row]) { + let layer = OTHER_LAYERS[row][item] + player[layer].resetTime += diff + if (tmp[layer].passiveGeneration) generatePoints(layer, diff*tmp[layer].passiveGeneration); + if (layers[layer].update) layers[layer].update(diff); + } + } + + for (let x = maxRow; x >= 0; x--){ + for (item in TREE_LAYERS[x]) { + let layer = TREE_LAYERS[x][item] + if (tmp[layer].autoPrestige && tmp[layer].canReset) doReset(layer); + if (layers[layer].automate) layers[layer].automate(); + if (tmp[layer].autoUpgrade) autobuyUpgrades(layer) + } + } + + for (row in OTHER_LAYERS){ + for (item in OTHER_LAYERS[row]) { + let layer = OTHER_LAYERS[row][item] + if (tmp[layer].autoPrestige && tmp[layer].canReset) doReset(layer); + if (layers[layer].automate) layers[layer].automate(); + player[layer].best = player[layer].best.max(player[layer].points) + if (tmp[layer].autoUpgrade) autobuyUpgrades(layer) + } + } + + for (layer in layers){ + if (layers[layer].milestones) updateMilestones(layer); + if (layers[layer].achievements) updateAchievements(layer) + } + +} + +function hardReset(resetOptions) { + if (!confirm("Are you sure you want to do this? You will lose all your progress!")) return + player = null + if(resetOptions) options = null + save(true); + window.location.reload(); +} + +var ticking = false + +var interval = setInterval(function() { + if (player===undefined||tmp===undefined) return; + if (ticking) return; + if (tmp.gameEnded&&!player.keepGoing) return; + ticking = true + let now = Date.now() + let diff = (now - player.time) / 1e3 + let trueDiff = diff + if (player.offTime !== undefined) { + if (player.offTime.remain > modInfo.offlineLimit * 3600) player.offTime.remain = modInfo.offlineLimit * 3600 + if (player.offTime.remain > 0) { + let offlineDiff = Math.max(player.offTime.remain / 10, diff) + player.offTime.remain -= offlineDiff + diff += offlineDiff + } + if (!options.offlineProd || player.offTime.remain <= 0) player.offTime = undefined + } + if (player.devSpeed) diff *= player.devSpeed + player.time = now + if (needCanvasUpdate){ resizeCanvas(); + needCanvasUpdate = false; + } + tmp.scrolled = document.getElementById('treeTab') && document.getElementById('treeTab').scrollTop > 30 + updateTemp(); + updateOomps(diff); + updateWidth() + updateTabFormats() + gameLoop(diff) + fixNaNs() + adjustPopupTime(trueDiff) + updateParticles(trueDiff) + ticking = false +}, 50) + +setInterval(function() {needCanvasUpdate = true}, 500) \ No newline at end of file diff --git a/js/layers.js b/js/layers.js new file mode 100644 index 0000000..f9488fb --- /dev/null +++ b/js/layers.js @@ -0,0 +1,28 @@ +addLayer("p", { + name: "prestige", // This is optional, only used in a few places, If absent it just uses the layer id. + symbol: "P", // This appears on the layer's node. Default is the id with the first letter capitalized + position: 0, // Horizontal position within a row. By default it uses the layer id and sorts in alphabetical order + startData() { return { + unlocked: true, + points: new Decimal(0), + }}, + color: "#4BDC13", + requires: new Decimal(10), // Can be a function that takes requirement increases into account + resource: "prestige points", // Name of prestige currency + baseResource: "points", // Name of resource prestige is based on + baseAmount() {return player.points}, // Get the current amount of baseResource + type: "normal", // normal: cost to gain currency depends on amount gained. static: cost depends on how much you already have + exponent: 0.5, // Prestige currency exponent + gainMult() { // Calculate the multiplier for main currency from bonuses + mult = new Decimal(1) + return mult + }, + gainExp() { // Calculate the exponent on main currency from bonuses + return new Decimal(1) + }, + row: 0, // Row the layer is in on the tree (0 is the first row) + hotkeys: [ + {key: "p", description: "P: Reset for prestige points", onPress(){if (canReset(this.layer)) doReset(this.layer)}}, + ], + layerShown(){return true} +}) diff --git a/js/mod.js b/js/mod.js new file mode 100644 index 0000000..6175517 --- /dev/null +++ b/js/mod.js @@ -0,0 +1,79 @@ +let modInfo = { + name: "The ??? Tree", + id: "mymod", + author: "nobody", + pointsName: "points", + modFiles: ["layers.js", "tree.js"], + + discordName: "", + discordLink: "", + initialStartPoints: new Decimal (10), // Used for hard resets and new players + offlineLimit: 1, // In hours +} + +// Set your version in num and name +let VERSION = { + num: "0.0", + name: "Literally nothing", +} + +let changelog = `

Changelog:


+

v0.0


+ - Added things.
+ - Added stuff.` + +let winText = `Congratulations! You have reached the end and beaten this game, but for now...` + +// If you add new functions anywhere inside of a layer, and those functions have an effect when called, add them here. +// (The ones here are examples, all official functions are already taken care of) +var doNotCallTheseFunctionsEveryTick = ["blowUpEverything"] + +function getStartPoints(){ + return new Decimal(modInfo.initialStartPoints) +} + +// Determines if it should show points/sec +function canGenPoints(){ + return true +} + +// Calculate points/sec! +function getPointGen() { + if(!canGenPoints()) + return new Decimal(0) + + let gain = new Decimal(1) + return gain +} + +// You can add non-layer related variables that should to into "player" and be saved here, along with default values +function addedPlayerData() { return { +}} + +// Display extra things at the top of the page +var displayThings = [ +] + +// Determines when the game "ends" +function isEndgame() { + return player.points.gte(new Decimal("e280000000")) +} + + + +// Less important things beyond this point! + +// Style for the background, can be a function +var backgroundStyle = { + +} + +// You can change this if you have things that can be messed up by long tick lengths +function maxTickLength() { + return(3600) // Default is 1 hour which is just arbitrarily large +} + +// Use this if you need to undo inflation from an older version. If the version is older than the version that fixed the issue, +// you can cap their current resources with this. +function fixOldSave(oldVersion){ +} \ No newline at end of file diff --git a/js/technical/break_eternity.js b/js/technical/break_eternity.js new file mode 100644 index 0000000..a162439 --- /dev/null +++ b/js/technical/break_eternity.js @@ -0,0 +1,2742 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : + typeof define === 'function' && define.amd ? define(factory) : + (global = global || self, global.Decimal = factory()); +}(this, function () { 'use strict'; + + var padEnd = function (string, maxLength, fillString) { + + if (string === null || maxLength === null) { + return string; + } + + var result = String(string); + var targetLen = typeof maxLength === 'number' + ? maxLength + : parseInt(maxLength, 10); + + if (isNaN(targetLen) || !isFinite(targetLen)) { + return result; + } + + + var length = result.length; + if (length >= targetLen) { + return result; + } + + + var filled = fillString === null ? '' : String(fillString); + if (filled === '') { + filled = ' '; + } + + + var fillLen = targetLen - length; + + while (filled.length < fillLen) { + filled += filled; + } + + var truncated = filled.length > fillLen ? filled.substr(0, fillLen) : filled; + + return result + truncated; + }; + + var MAX_SIGNIFICANT_DIGITS = 17; //Maximum number of digits of precision to assume in Number + + var EXP_LIMIT = 9e15; //If we're ABOVE this value, increase a layer. (9e15 is close to the largest integer that can fit in a Number.) + + var LAYER_DOWN = Math.log10(9e15); //If we're BELOW this value, drop down a layer. About 15.954. + + var FIRST_NEG_LAYER = 1/9e15; //At layer 0, smaller non-zero numbers than this become layer 1 numbers with negative mag. After that the pattern continues as normal. + + var NUMBER_EXP_MAX = 308; //The largest exponent that can appear in a Number, though not all mantissas are valid here. + + var NUMBER_EXP_MIN = -324; //The smallest exponent that can appear in a Number, though not all mantissas are valid here. + + var MAX_ES_IN_A_ROW = 5; //For default toString behaviour, when to swap from eee... to (e^n) syntax. + + var powerOf10 = function () { + // We need this lookup table because Math.pow(10, exponent) + // when exponent's absolute value is large is slightly inaccurate. + // You can fix it with the power of math... or just make a lookup table. + // Faster AND simpler + var powersOf10 = []; + + for (var i = NUMBER_EXP_MIN + 1; i <= NUMBER_EXP_MAX; i++) { + powersOf10.push(Number("1e" + i)); + } + + var indexOf0InPowersOf10 = 323; + return function (power) { + return powersOf10[power + indexOf0InPowersOf10]; + }; + }(); + + var D = function D(value) { + return Decimal.fromValue_noAlloc(value); + }; + + var FC = function FC(sign, layer, mag) { + return Decimal.fromComponents(sign, layer, mag); + }; + + var FC_NN = function FC_NN(sign, layer, mag) { + return Decimal.fromComponents_noNormalize(sign, layer, mag); + }; + + var ME = function ME(mantissa, exponent) { + return Decimal.fromMantissaExponent(mantissa, exponent); + }; + + var ME_NN = function ME_NN(mantissa, exponent) { + return Decimal.fromMantissaExponent_noNormalize(mantissa, exponent); + }; + + var decimalPlaces = function decimalPlaces(value, places) { + var len = places + 1; + var numDigits = Math.ceil(Math.log10(Math.abs(value))); + var rounded = Math.round(value * Math.pow(10, len - numDigits)) * Math.pow(10, numDigits - len); + return parseFloat(rounded.toFixed(Math.max(len - numDigits, 0))); + }; + + var f_maglog10 = function(n) { + return Math.sign(n)*Math.log10(Math.abs(n)); + } + + //from HyperCalc source code + var f_gamma = function(n) { + if (!isFinite(n)) { return n; } + if (n < -50) + { + if (n === Math.trunc(n)) { return Number.NEGATIVE_INFINITY; } + return 0; + } + + var scal1 = 1; + while (n < 10) + { + scal1 = scal1*n; + ++n; + } + + n -= 1; + var l = 0.9189385332046727; //0.5*Math.log(2*Math.PI) + l = l + (n+0.5)*Math.log(n); + l = l - n; + var n2 = n*n; + var np = n; + l = l+1/(12*np); + np = np*n2; + l = l+1/(360*np); + np = np*n2; + l = l+1/(1260*np); + np = np*n2; + l = l+1/(1680*np); + np = np*n2; + l = l+1/(1188*np); + np = np*n2; + l = l+691/(360360*np); + np = np*n2; + l = l+7/(1092*np); + np = np*n2; + l = l+3617/(122400*np); + + return Math.exp(l)/scal1; + }; + + var twopi = 6.2831853071795864769252842; // 2*pi + var EXPN1 = 0.36787944117144232159553; // exp(-1) + var OMEGA = 0.56714329040978387299997; // W(1, 0) + //from https://math.stackexchange.com/a/465183 + // The evaluation can become inaccurate very close to the branch point + var f_lambertw = function(z, tol = 1e-10) { + var w; + var wn; + + if (!Number.isFinite(z)) { return z; } + if (z === 0) + { + return z; + } + if (z === 1) + { + return OMEGA; + } + + if (z < 10) + { + w = 0; + } + else + { + w = Math.log(z)-Math.log(Math.log(z)); + } + + for (var i = 0; i < 100; ++i) + { + wn = (z * Math.exp(-w) + w * w)/(w + 1); + if (Math.abs(wn - w) < tol*Math.abs(wn)) + { + return wn; + } + else + { + w = wn; + } + } + + throw Error("Iteration failed to converge: " + z); + //return Number.NaN; + } + + var Decimal = + /** @class */ + function () { + + function Decimal(value) { + + this.sign = Number.NaN; + this.layer = Number.NaN; + this.mag = Number.NaN; + + if (value instanceof Decimal) { + this.fromDecimal(value); + } else if (typeof value === "number") { + this.fromNumber(value); + } else if (typeof value === "string") { + this.fromString(value); + } else { + this.sign = 0; + this.layer = 0; + this.mag = 0; + } + } + + Object.defineProperty(Decimal.prototype, "m", { + get: function get() { + if (this.sign === 0) + { + return 0; + } + else if (this.layer === 0) + { + var exp = Math.floor(Math.log10(this.mag)); + //handle special case 5e-324 + var man; + if (this.mag === 5e-324) + { + man = 5; + } + else + { + man = this.mag / powerOf10(exp); + } + return this.sign*man; + } + else if (this.layer === 1) + { + var residue = this.mag-Math.floor(this.mag); + return this.sign*Math.pow(10, residue); + } + else + { + //mantissa stops being relevant past 1e9e15 / ee15.954 + return this.sign; + } + }, + set: function set(value) { + if (this.layer <= 2) + { + this.fromMantissaExponent(value, this.e); + } + else + { + //don't even pretend mantissa is meaningful + this.sign = Math.sign(value); + if (this.sign === 0) { this.layer === 0; this.exponent === 0; } + } + }, + enumerable: true, + configurable: true + }); + Object.defineProperty(Decimal.prototype, "e", { + get: function get() { + if (this.sign === 0) + { + return 0; + } + else if (this.layer === 0) + { + return Math.floor(Math.log10(this.mag)); + } + else if (this.layer === 1) + { + return Math.floor(this.mag); + } + else if (this.layer === 2) + { + return Math.floor(Math.sign(this.mag)*Math.pow(10, Math.abs(this.mag))); + } + else + { + return this.mag*Number.POSITIVE_INFINITY; + } + }, + set: function set(value) { + this.fromMantissaExponent(this.m, value); + }, + enumerable: true, + configurable: true + }); + Object.defineProperty(Decimal.prototype, "s", { + get: function get() { + return this.sign; + }, + set: function set(value) { + if (value === 0) { + this.sign = 0; + this.layer = 0; + this.mag = 0; + } + else + { + this.sign = value; + } + }, + enumerable: true, + configurable: true + }); + + Object.defineProperty(Decimal.prototype, "mantissa", { + get: function get() { + return this.m; + }, + set: function set(value) { + this.m = value; + }, + enumerable: true, + configurable: true + }); + + Object.defineProperty(Decimal.prototype, "exponent", { + get: function get() { + return this.e; + }, + set: function set(value) { + this.e = value; + }, + enumerable: true, + configurable: true + }); + + Decimal.fromComponents = function (sign, layer, mag) { + return new Decimal().fromComponents(sign, layer, mag); + }; + + Decimal.fromComponents_noNormalize = function (sign, layer, mag) { + return new Decimal().fromComponents_noNormalize(sign, layer, mag); + }; + + Decimal.fromMantissaExponent = function (mantissa, exponent) { + return new Decimal().fromMantissaExponent(mantissa, exponent); + }; + + Decimal.fromMantissaExponent_noNormalize = function (mantissa, exponent) { + return new Decimal().fromMantissaExponent_noNormalize(mantissa, exponent); + }; + + Decimal.fromDecimal = function (value) { + return new Decimal().fromDecimal(value); + }; + + Decimal.fromNumber = function (value) { + return new Decimal().fromNumber(value); + }; + + Decimal.fromString = function (value) { + return new Decimal().fromString(value); + }; + + Decimal.fromValue = function (value) { + return new Decimal().fromValue(value); + }; + + Decimal.fromValue_noAlloc = function (value) { + return value instanceof Decimal ? value : new Decimal(value); + }; + + Decimal.abs = function (value) { + return D(value).abs(); + }; + + Decimal.neg = function (value) { + return D(value).neg(); + }; + + Decimal.negate = function (value) { + return D(value).neg(); + }; + + Decimal.negated = function (value) { + return D(value).neg(); + }; + + Decimal.sign = function (value) { + return D(value).sign(); + }; + + Decimal.sgn = function (value) { + return D(value).sign(); + }; + + Decimal.round = function (value) { + return D(value).round(); + }; + + Decimal.floor = function (value) { + return D(value).floor(); + }; + + Decimal.ceil = function (value) { + return D(value).ceil(); + }; + + Decimal.trunc = function (value) { + return D(value).trunc(); + }; + + Decimal.add = function (value, other) { + return D(value).add(other); + }; + + Decimal.plus = function (value, other) { + return D(value).add(other); + }; + + Decimal.sub = function (value, other) { + return D(value).sub(other); + }; + + Decimal.subtract = function (value, other) { + return D(value).sub(other); + }; + + Decimal.minus = function (value, other) { + return D(value).sub(other); + }; + + Decimal.mul = function (value, other) { + return D(value).mul(other); + }; + + Decimal.multiply = function (value, other) { + return D(value).mul(other); + }; + + Decimal.times = function (value, other) { + return D(value).mul(other); + }; + + Decimal.div = function (value, other) { + return D(value).div(other); + }; + + Decimal.divide = function (value, other) { + return D(value).div(other); + }; + + Decimal.recip = function (value) { + return D(value).recip(); + }; + + Decimal.reciprocal = function (value) { + return D(value).recip(); + }; + + Decimal.reciprocate = function (value) { + return D(value).reciprocate(); + }; + + Decimal.cmp = function (value, other) { + return D(value).cmp(other); + }; + + Decimal.cmpabs = function (value, other) { + return D(value).cmpabs(other); + }; + + Decimal.compare = function (value, other) { + return D(value).cmp(other); + }; + + Decimal.eq = function (value, other) { + return D(value).eq(other); + }; + + Decimal.equals = function (value, other) { + return D(value).eq(other); + }; + + Decimal.neq = function (value, other) { + return D(value).neq(other); + }; + + Decimal.notEquals = function (value, other) { + return D(value).notEquals(other); + }; + + Decimal.lt = function (value, other) { + return D(value).lt(other); + }; + + Decimal.lte = function (value, other) { + return D(value).lte(other); + }; + + Decimal.gt = function (value, other) { + return D(value).gt(other); + }; + + Decimal.gte = function (value, other) { + return D(value).gte(other); + }; + + Decimal.max = function (value, other) { + return D(value).max(other); + }; + + Decimal.min = function (value, other) { + return D(value).min(other); + }; + + Decimal.minabs = function (value, other) { + return D(value).minabs(other); + }; + + Decimal.maxabs = function (value, other) { + return D(value).maxabs(other); + }; + + Decimal.clamp = function(value, min, max) { + return D(value).clamp(min, max); + } + + Decimal.clampMin = function(value, min) { + return D(value).clampMin(min); + } + + Decimal.clampMax = function(value, max) { + return D(value).clampMax(max); + } + + Decimal.cmp_tolerance = function (value, other, tolerance) { + return D(value).cmp_tolerance(other, tolerance); + }; + + Decimal.compare_tolerance = function (value, other, tolerance) { + return D(value).cmp_tolerance(other, tolerance); + }; + + Decimal.eq_tolerance = function (value, other, tolerance) { + return D(value).eq_tolerance(other, tolerance); + }; + + Decimal.equals_tolerance = function (value, other, tolerance) { + return D(value).eq_tolerance(other, tolerance); + }; + + Decimal.neq_tolerance = function (value, other, tolerance) { + return D(value).neq_tolerance(other, tolerance); + }; + + Decimal.notEquals_tolerance = function (value, other, tolerance) { + return D(value).notEquals_tolerance(other, tolerance); + }; + + Decimal.lt_tolerance = function (value, other, tolerance) { + return D(value).lt_tolerance(other, tolerance); + }; + + Decimal.lte_tolerance = function (value, other, tolerance) { + return D(value).lte_tolerance(other, tolerance); + }; + + Decimal.gt_tolerance = function (value, other, tolerance) { + return D(value).gt_tolerance(other, tolerance); + }; + + Decimal.gte_tolerance = function (value, other, tolerance) { + return D(value).gte_tolerance(other, tolerance); + }; + + Decimal.pLog10 = function (value) { + return D(value).pLog10(); + }; + + Decimal.absLog10 = function (value) { + return D(value).absLog10(); + }; + + Decimal.log10 = function (value) { + return D(value).log10(); + }; + + Decimal.log = function (value, base) { + return D(value).log(base); + }; + + Decimal.log2 = function (value) { + return D(value).log2(); + }; + + Decimal.ln = function (value) { + return D(value).ln(); + }; + + Decimal.logarithm = function (value, base) { + return D(value).logarithm(base); + }; + + Decimal.pow = function (value, other) { + return D(value).pow(other); + }; + + Decimal.pow10 = function (value) { + return D(value).pow10(); + }; + + Decimal.root = function (value, other) { + return D(value).root(other); + }; + + Decimal.factorial = function (value, other) { + return D(value).factorial(); + }; + + Decimal.gamma = function (value, other) { + return D(value).gamma(); + }; + + Decimal.lngamma = function (value, other) { + return D(value).lngamma(); + }; + + Decimal.exp = function (value) { + return D(value).exp(); + }; + + Decimal.sqr = function (value) { + return D(value).sqr(); + }; + + Decimal.sqrt = function (value) { + return D(value).sqrt(); + }; + + Decimal.cube = function (value) { + return D(value).cube(); + }; + + Decimal.cbrt = function (value) { + return D(value).cbrt(); + }; + + Decimal.tetrate = function (value, height = 2, payload = FC_NN(1, 0, 1)) { + return D(value).tetrate(height, payload); + } + + Decimal.iteratedexp = function (value, height = 2, payload = FC_NN(1, 0, 1)) { + return D(value).iteratedexp(height, payload); + } + + Decimal.iteratedlog = function (value, base = 10, times = 1) { + return D(value).iteratedlog(base, times); + } + + Decimal.layeradd10 = function (value, diff) { + return D(value).layeradd10(diff); + } + + Decimal.layeradd = function (value, diff, base = 10) { + return D(value).layeradd(diff, base); + } + + Decimal.slog = function (value, base = 10) { + return D(value).slog(base); + } + + Decimal.lambertw = function(value) { + return D(value).lambertw(); + } + + Decimal.ssqrt = function(value) { + return D(value).ssqrt(); + } + + Decimal.pentate = function (value, height = 2, payload = FC_NN(1, 0, 1)) { + return D(value).pentate(height, payload); + } + + /** + * If you're willing to spend 'resourcesAvailable' and want to buy something + * with exponentially increasing cost each purchase (start at priceStart, + * multiply by priceRatio, already own currentOwned), how much of it can you buy? + * Adapted from Trimps source code. + */ + + + Decimal.affordGeometricSeries = function (resourcesAvailable, priceStart, priceRatio, currentOwned) { + return this.affordGeometricSeries_core(D(resourcesAvailable), D(priceStart), D(priceRatio), currentOwned); + }; + /** + * How much resource would it cost to buy (numItems) items if you already have currentOwned, + * the initial price is priceStart and it multiplies by priceRatio each purchase? + */ + + + Decimal.sumGeometricSeries = function (numItems, priceStart, priceRatio, currentOwned) { + return this.sumGeometricSeries_core(numItems, D(priceStart), D(priceRatio), currentOwned); + }; + /** + * If you're willing to spend 'resourcesAvailable' and want to buy something with additively + * increasing cost each purchase (start at priceStart, add by priceAdd, already own currentOwned), + * how much of it can you buy? + */ + + + Decimal.affordArithmeticSeries = function (resourcesAvailable, priceStart, priceAdd, currentOwned) { + return this.affordArithmeticSeries_core(D(resourcesAvailable), D(priceStart), D(priceAdd), D(currentOwned)); + }; + /** + * How much resource would it cost to buy (numItems) items if you already have currentOwned, + * the initial price is priceStart and it adds priceAdd each purchase? + * Adapted from http://www.mathwords.com/a/arithmetic_series.htm + */ + + + Decimal.sumArithmeticSeries = function (numItems, priceStart, priceAdd, currentOwned) { + return this.sumArithmeticSeries_core(D(numItems), D(priceStart), D(priceAdd), D(currentOwned)); + }; + /** + * When comparing two purchases that cost (resource) and increase your resource/sec by (deltaRpS), + * the lowest efficiency score is the better one to purchase. + * From Frozen Cookies: + * http://cookieclicker.wikia.com/wiki/Frozen_Cookies_(JavaScript_Add-on)#Efficiency.3F_What.27s_that.3F + */ + + + Decimal.efficiencyOfPurchase = function (cost, currentRpS, deltaRpS) { + return this.efficiencyOfPurchase_core(D(cost), D(currentRpS), D(deltaRpS)); + }; + + Decimal.randomDecimalForTesting = function (maxLayers) { + // NOTE: This doesn't follow any kind of sane random distribution, so use this for testing purposes only. + //5% of the time, return 0 + if (Math.random() * 20 < 1) { + return FC_NN(0, 0, 0); + } + + var randomsign = Math.random() > 0.5 ? 1 : -1; + + //5% of the time, return 1 or -1 + if (Math.random() * 20 < 1) { + return FC_NN(randomsign, 0, 1); + } + + //pick a random layer + var layer = Math.floor(Math.random()*(maxLayers+1)); + + var randomexp = layer === 0 ? Math.random()*616-308 : Math.random()*16; + //10% of the time, make it a simple power of 10 + if (Math.random() > 0.9) { randomexp = Math.trunc(randomexp); } + var randommag = Math.pow(10, randomexp); + //10% of the time, trunc mag + if (Math.random() > 0.9) { randommag = Math.trunc(randommag); } + return FC(randomsign, layer, randommag); + }; + + Decimal.affordGeometricSeries_core = function (resourcesAvailable, priceStart, priceRatio, currentOwned) { + var actualStart = priceStart.mul(priceRatio.pow(currentOwned)); + return Decimal.floor(resourcesAvailable.div(actualStart).mul(priceRatio.sub(1)).add(1).log10().div(priceRatio.log10())); + }; + + Decimal.sumGeometricSeries_core = function (numItems, priceStart, priceRatio, currentOwned) { + return priceStart.mul(priceRatio.pow(currentOwned)).mul(Decimal.sub(1, priceRatio.pow(numItems))).div(Decimal.sub(1, priceRatio)); + }; + + Decimal.affordArithmeticSeries_core = function (resourcesAvailable, priceStart, priceAdd, currentOwned) { + // n = (-(a-d/2) + sqrt((a-d/2)^2+2dS))/d + // where a is actualStart, d is priceAdd and S is resourcesAvailable + // then floor it and you're done! + var actualStart = priceStart.add(currentOwned.mul(priceAdd)); + var b = actualStart.sub(priceAdd.div(2)); + var b2 = b.pow(2); + return b.neg().add(b2.add(priceAdd.mul(resourcesAvailable).mul(2)).sqrt()).div(priceAdd).floor(); + }; + + Decimal.sumArithmeticSeries_core = function (numItems, priceStart, priceAdd, currentOwned) { + var actualStart = priceStart.add(currentOwned.mul(priceAdd)); // (n/2)*(2*a+(n-1)*d) + + return numItems.div(2).mul(actualStart.mul(2).plus(numItems.sub(1).mul(priceAdd))); + }; + + Decimal.efficiencyOfPurchase_core = function (cost, currentRpS, deltaRpS) { + return cost.div(currentRpS).add(cost.div(deltaRpS)); + }; + + Decimal.prototype.normalize = function () { + /* + PSEUDOCODE: + Whenever we are partially 0 (sign is 0 or mag and layer is 0), make it fully 0. + Whenever we are at or hit layer 0, extract sign from negative mag. + If layer === 0 and mag < FIRST_NEG_LAYER (1/9e15), shift to 'first negative layer' (add layer, log10 mag). + While abs(mag) > EXP_LIMIT (9e15), layer += 1, mag = maglog10(mag). + While abs(mag) < LAYER_DOWN (15.954) and layer > 0, layer -= 1, mag = pow(10, mag). + + When we're done, all of the following should be true OR one of the numbers is not IsFinite OR layer is not IsInteger (error state): + Any 0 is totally zero (0, 0, 0). + Anything layer 0 has mag 0 OR mag > 1/9e15 and < 9e15. + Anything layer 1 or higher has abs(mag) >= 15.954 and < 9e15. + We will assume in calculations that all Decimals are either erroneous or satisfy these criteria. (Otherwise: Garbage in, garbage out.) + */ + if (this.sign === 0 || (this.mag === 0 && this.layer === 0)) + { + this.sign = 0; + this.mag = 0; + this.layer = 0; + return this; + } + + if (this.layer === 0 && this.mag < 0) + { + //extract sign from negative mag at layer 0 + this.mag = -this.mag; + this.sign = -this.sign; + } + + //Handle shifting from layer 0 to negative layers. + if (this.layer === 0 && this.mag < FIRST_NEG_LAYER) + { + this.layer += 1; + this.mag = Math.log10(this.mag); + return this; + } + + var absmag = Math.abs(this.mag); + var signmag = Math.sign(this.mag); + + if (absmag >= EXP_LIMIT) + { + this.layer += 1; + this.mag = signmag*Math.log10(absmag); + return this; + } + else + { + while (absmag < LAYER_DOWN && this.layer > 0) + { + this.layer -= 1; + if (this.layer === 0) + { + this.mag = Math.pow(10, this.mag); + } + else + { + this.mag = signmag*Math.pow(10, absmag); + absmag = Math.abs(this.mag); + signmag = Math.sign(this.mag); + } + } + if (this.layer === 0) + { + if (this.mag < 0) + { + //extract sign from negative mag at layer 0 + this.mag = -this.mag; + this.sign = -this.sign; + } + else if (this.mag === 0) + { + //excessive rounding can give us all zeroes + this.sign = 0; + } + } + } + + return this; + }; + + Decimal.prototype.fromComponents = function (sign, layer, mag) { + this.sign = sign; + this.layer = layer; + this.mag = mag; + + this.normalize(); + return this; + }; + + Decimal.prototype.fromComponents_noNormalize = function (sign, layer, mag) { + this.sign = sign; + this.layer = layer; + this.mag = mag; + return this; + }; + + Decimal.prototype.fromMantissaExponent = function (mantissa, exponent) { + this.layer = 1; + this.sign = Math.sign(mantissa); + mantissa = Math.abs(mantissa); + this.mag = exponent + Math.log10(mantissa); + + this.normalize(); + return this; + }; + + + Decimal.prototype.fromMantissaExponent_noNormalize = function (mantissa, exponent) { + //The idea of 'normalizing' a break_infinity.js style Decimal doesn't really apply. So just do the same thing. + this.fromMantissaExponent(mantissa, exponent); + return this; + }; + + Decimal.prototype.fromDecimal = function (value) { + this.sign = value.sign; + this.layer = value.layer; + this.mag = value.mag; + return this; + }; + + Decimal.prototype.fromNumber = function (value) { + this.mag = Math.abs(value); + this.sign = Math.sign(value); + this.layer = 0; + this.normalize(); + return this; + }; + + var IGNORE_COMMAS = true; + var COMMAS_ARE_DECIMAL_POINTS = false; + + Decimal.prototype.fromString = function (value) { + if (IGNORE_COMMAS) { value = value.replace(",", ""); } + else if (COMMAS_ARE_DECIMAL_POINTS) { value = value.replace(",", "."); } + + //Handle x^^^y format. + var pentationparts = value.split("^^^"); + if (pentationparts.length === 2) + { + var base = parseFloat(pentationparts[0]); + var height = parseFloat(pentationparts[1]); + var payload = 1; + var heightparts = pentationparts[1].split(";"); + if (heightparts.length === 2) + { + var payload = parseFloat(heightparts[1]); + if (!isFinite(payload)) { payload = 1; } + } + if (isFinite(base) && isFinite(height)) + { + var result = Decimal.pentate(base, height, payload); + this.sign = result.sign; + this.layer = result.layer; + this.mag = result.mag; + return this; + } + } + + //Handle x^^y format. + var tetrationparts = value.split("^^"); + if (tetrationparts.length === 2) + { + var base = parseFloat(tetrationparts[0]); + var height = parseFloat(tetrationparts[1]); + var heightparts = tetrationparts[1].split(";"); + if (heightparts.length === 2) + { + var payload = parseFloat(heightparts[1]); + if (!isFinite(payload)) { payload = 1; } + } + if (isFinite(base) && isFinite(height)) + { + var result = Decimal.tetrate(base, height, payload); + this.sign = result.sign; + this.layer = result.layer; + this.mag = result.mag; + return this; + } + } + + //Handle x^y format. + var powparts = value.split("^"); + if (powparts.length === 2) + { + var base = parseFloat(powparts[0]); + var exponent = parseFloat(powparts[1]); + if (isFinite(base) && isFinite(exponent)) + { + var result = Decimal.pow(base, exponent); + this.sign = result.sign; + this.layer = result.layer; + this.mag = result.mag; + return this; + } + } + + //Handle various cases involving it being a Big Number. + value = value.trim().toLowerCase(); + + //handle X PT Y format. + var ptparts = value.split("pt"); + if (ptparts.length === 2) + { + base = 10; + height = parseFloat(ptparts[0]); + ptparts[1] = ptparts[1].replace("(", ""); + ptparts[1] = ptparts[1].replace(")", ""); + var payload = parseFloat(ptparts[1]); + if (!isFinite(payload)) { payload = 1; } + if (isFinite(base) && isFinite(height)) + { + var result = Decimal.tetrate(base, height, payload); + this.sign = result.sign; + this.layer = result.layer; + this.mag = result.mag; + return this; + } + } + + //handle XpY format (it's the same thing just with p). + var ptparts = value.split("p"); + if (ptparts.length === 2) + { + base = 10; + height = parseFloat(ptparts[0]); + ptparts[1] = ptparts[1].replace("(", ""); + ptparts[1] = ptparts[1].replace(")", ""); + var payload = parseFloat(ptparts[1]); + if (!isFinite(payload)) { payload = 1; } + if (isFinite(base) && isFinite(height)) + { + var result = Decimal.tetrate(base, height, payload); + this.sign = result.sign; + this.layer = result.layer; + this.mag = result.mag; + return this; + } + } + + var parts = value.split("e"); + var ecount = parts.length-1; + + //Handle numbers that are exactly floats (0 or 1 es). + if (ecount === 0) + { + var numberAttempt = parseFloat(value); + if (isFinite(numberAttempt)) + { + return this.fromNumber(numberAttempt); + } + } + else if (ecount === 1) + { + //Very small numbers ("2e-3000" and so on) may look like valid floats but round to 0. + var numberAttempt = parseFloat(value); + if (isFinite(numberAttempt) && numberAttempt !== 0) + { + return this.fromNumber(numberAttempt); + } + } + + //Handle new (e^N)X format. + var newparts = value.split("e^"); + if (newparts.length === 2) + { + this.sign = 1; + if (newparts[0].charAt(0) == "-") + { + this.sign = -1; + } + var layerstring = ""; + for (var i = 0; i < newparts[1].length; ++i) + { + var chrcode = newparts[1].charCodeAt(i); + if ((chrcode >= 43 && chrcode <= 57) || chrcode === 101) //is "0" to "9" or "+" or "-" or "." or "e" (or "," or "/") + { + layerstring += newparts[1].charAt(i); + } + else //we found the end of the layer count + { + this.layer = parseFloat(layerstring); + this.mag = parseFloat(newparts[1].substr(i+1)); + this.normalize(); + return this; + } + } + } + + if (ecount < 1) { this.sign = 0; this.layer = 0; this.mag = 0; return this; } + var mantissa = parseFloat(parts[0]); + if (mantissa === 0) { this.sign = 0; this.layer = 0; this.mag = 0; return this; } + var exponent = parseFloat(parts[parts.length-1]); + //handle numbers like AeBeC and AeeeeBeC + if (ecount >= 2) + { + var me = parseFloat(parts[parts.length-2]); + if (isFinite(me)) + { + exponent *= Math.sign(me); + exponent += f_maglog10(me); + } + } + + //Handle numbers written like eee... (N es) X + if (!isFinite(mantissa)) + { + this.sign = (parts[0] === "-") ? -1 : 1; + this.layer = ecount; + this.mag = exponent; + } + //Handle numbers written like XeY + else if (ecount === 1) + { + this.sign = Math.sign(mantissa); + this.layer = 1; + //Example: 2e10 is equal to 10^log10(2e10) which is equal to 10^(10+log10(2)) + this.mag = exponent + Math.log10(Math.abs(mantissa)); + } + //Handle numbers written like Xeee... (N es) Y + else + { + this.sign = Math.sign(mantissa); + this.layer = ecount; + if (ecount === 2) + { + var result = Decimal.mul(FC(1, 2, exponent), D(mantissa)); + this.sign = result.sign; + this.layer = result.layer; + this.mag = result.mag; + return this; + } + else + { + //at eee and above, mantissa is too small to be recognizable! + this.mag = exponent; + } + } + + this.normalize(); + return this; + }; + + Decimal.prototype.fromValue = function (value) { + if (value instanceof Decimal) { + return this.fromDecimal(value); + } + + if (typeof value === "number") { + return this.fromNumber(value); + } + + if (typeof value === "string") { + return this.fromString(value); + } + + this.sign = 0; + this.layer = 0; + this.mag = 0; + return this; + }; + + Decimal.prototype.toNumber = function () { + if (!Number.isFinite(this.layer)) { return Number.NaN; } + if (this.layer === 0) + { + return this.sign*this.mag; + } + else if (this.layer === 1) + { + return this.sign*Math.pow(10, this.mag); + } + else //overflow for any normalized Decimal + { + return this.mag > 0 ? (this.sign > 0 ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY) : 0; + } + }; + + Decimal.prototype.mantissaWithDecimalPlaces = function (places) { + // https://stackoverflow.com/a/37425022 + if (isNaN(this.m)) { + return Number.NaN; + } + + if (this.m === 0) { + return 0; + } + + return decimalPlaces(this.m, places); + }; + + Decimal.prototype.magnitudeWithDecimalPlaces = function (places) { + // https://stackoverflow.com/a/37425022 + if (isNaN(this.mag)) { + return Number.NaN; + } + + if (this.mag === 0) { + return 0; + } + + return decimalPlaces(this.mag, places); + }; + + Decimal.prototype.toString = function () { + if (this.layer === 0) + { + if ((this.mag < 1e21 && this.mag > 1e-7) || this.mag === 0) + { + return (this.sign*this.mag).toString(); + } + return this.m + "e" + this.e; + } + else if (this.layer === 1) + { + return this.m + "e" + this.e; + } + else + { + //layer 2+ + if (this.layer <= MAX_ES_IN_A_ROW) + { + return (this.sign === -1 ? "-" : "") + "e".repeat(this.layer) + this.mag; + } + else + { + return (this.sign === -1 ? "-" : "") + "(e^" + this.layer + ")" + this.mag; + } + } + }; + + Decimal.prototype.toExponential = function (places) { + if (this.layer === 0) + { + return (this.sign*this.mag).toExponential(places); + } + return this.toStringWithDecimalPlaces(places); + }; + + Decimal.prototype.toFixed = function (places) { + if (this.layer === 0) + { + return (this.sign*this.mag).toFixed(places); + } + return this.toStringWithDecimalPlaces(places); + }; + + Decimal.prototype.toPrecision = function (places) { + if (this.e <= -7) { + return this.toExponential(places - 1); + } + + if (places > this.e) { + return this.toFixed(places - this.exponent - 1); + } + + return this.toExponential(places - 1); + }; + + Decimal.prototype.valueOf = function () { + return this.toString(); + }; + + Decimal.prototype.toJSON = function () { + return this.toString(); + }; + + Decimal.prototype.toStringWithDecimalPlaces = function (places) { + if (this.layer === 0) + { + if ((this.mag < 1e21 && this.mag > 1e-7) || this.mag === 0) + { + return (this.sign*this.mag).toFixed(places); + } + return decimalPlaces(this.m, places) + "e" + decimalPlaces(this.e, places); + } + else if (this.layer === 1) + { + return decimalPlaces(this.m, places) + "e" + decimalPlaces(this.e, places); + } + else + { + //layer 2+ + if (this.layer <= MAX_ES_IN_A_ROW) + { + return (this.sign === -1 ? "-" : "") + "e".repeat(this.layer) + decimalPlaces(this.mag, places); + } + else + { + return (this.sign === -1 ? "-" : "") + "(e^" + this.layer + ")" + decimalPlaces(this.mag, places); + } + } + }; + + Decimal.prototype.abs = function () { + return FC_NN(this.sign === 0 ? 0 : 1, this.layer, this.mag); + }; + + Decimal.prototype.neg = function () { + return FC_NN(-this.sign, this.layer, this.mag); + }; + + Decimal.prototype.negate = function () { + return this.neg(); + }; + + Decimal.prototype.negated = function () { + return this.neg(); + }; + + Decimal.prototype.sign = function () { + return this.sign; + }; + + Decimal.prototype.sgn = function () { + return this.sign; + }; + + Decimal.prototype.round = function () { + if (this.mag < 0) + { + return Decimal.dZero; + } + if (this.layer === 0) + { + return FC(this.sign, 0, Math.round(this.mag)); + } + return this; + }; + + Decimal.prototype.floor = function () { + if (this.mag < 0) + { + return Decimal.dZero; + } + if (this.layer === 0) + { + return FC(this.sign, 0, Math.floor(this.mag)); + } + return this; + }; + + Decimal.prototype.ceil = function () { + if (this.mag < 0) + { + return Decimal.dZero; + } + if (this.layer === 0) + { + return FC(this.sign, 0, Math.ceil(this.mag)); + } + return this; + }; + + Decimal.prototype.trunc = function () { + if (this.mag < 0) + { + return Decimal.dZero; + } + if (this.layer === 0) + { + return FC(this.sign, 0, Math.trunc(this.mag)); + } + return this; + }; + + Decimal.prototype.add = function (value) { + var decimal = D(value); + + //inf/nan check + if (!Number.isFinite(this.layer)) { return this; } + if (!Number.isFinite(decimal.layer)) { return decimal; } + + //Special case - if one of the numbers is 0, return the other number. + if (this.sign === 0) { return decimal; } + if (decimal.sign === 0) { return this; } + + //Special case - Adding a number to its negation produces 0, no matter how large. + if (this.sign === -(decimal.sign) && this.layer === decimal.layer && this.mag === decimal.mag) { return FC_NN(0, 0, 0); } + + var a; + var b; + + //Special case: If one of the numbers is layer 2 or higher, just take the bigger number. + if ((this.layer >= 2 || decimal.layer >= 2)) { return this.maxabs(decimal); } + + if (Decimal.cmpabs(this, decimal) > 0) + { + a = this; + b = decimal; + } + else + { + a = decimal; + b = this; + } + + if (a.layer === 0 && b.layer === 0) { return D(a.sign*a.mag + b.sign*b.mag); } + + var layera = a.layer*Math.sign(a.mag); + var layerb = b.layer*Math.sign(b.mag); + + //If one of the numbers is 2+ layers higher than the other, just take the bigger number. + if (layera - layerb >= 2) { return a; } + + if (layera === 0 && layerb === -1) + { + if (Math.abs(b.mag-Math.log10(a.mag)) > MAX_SIGNIFICANT_DIGITS) + { + return a; + } + else + { + var magdiff = Math.pow(10, Math.log10(a.mag)-b.mag); + var mantissa = (b.sign)+(a.sign*magdiff); + return FC(Math.sign(mantissa), 1, b.mag+Math.log10(Math.abs(mantissa))); + } + } + + if (layera === 1 && layerb === 0) + { + if (Math.abs(a.mag-Math.log10(b.mag)) > MAX_SIGNIFICANT_DIGITS) + { + return a; + } + else + { + var magdiff = Math.pow(10, a.mag-Math.log10(b.mag)); + var mantissa = (b.sign)+(a.sign*magdiff); + return FC(Math.sign(mantissa), 1, Math.log10(b.mag)+Math.log10(Math.abs(mantissa))); + } + } + + if (Math.abs(a.mag-b.mag) > MAX_SIGNIFICANT_DIGITS) + { + return a; + } + else + { + var magdiff = Math.pow(10, a.mag-b.mag); + var mantissa = (b.sign)+(a.sign*magdiff); + return FC(Math.sign(mantissa), 1, b.mag+Math.log10(Math.abs(mantissa))); + } + + throw Error("Bad arguments to add: " + this + ", " + value); + }; + + Decimal.prototype.plus = function (value) { + return this.add(value); + }; + + Decimal.prototype.sub = function (value) { + return this.add(D(value).neg()); + }; + + Decimal.prototype.subtract = function (value) { + return this.sub(value); + }; + + Decimal.prototype.minus = function (value) { + return this.sub(value); + }; + + Decimal.prototype.mul = function (value) { + var decimal = D(value); + + //inf/nan check + if (!Number.isFinite(this.layer)) { return this; } + if (!Number.isFinite(decimal.layer)) { return decimal; } + + //Special case - if one of the numbers is 0, return 0. + if (this.sign === 0 || decimal.sign === 0) { return FC_NN(0, 0, 0); } + + //Special case - Multiplying a number by its own reciprocal yields +/- 1, no matter how large. + if (this.layer === decimal.layer && this.mag === -decimal.mag) { return FC_NN(this.sign*decimal.sign, 0, 1); } + + var a; + var b; + + //Which number is bigger in terms of its multiplicative distance from 1? + if ((this.layer > decimal.layer) || (this.layer == decimal.layer && Math.abs(this.mag) > Math.abs(decimal.mag))) + { + a = this; + b = decimal; + } + else + { + a = decimal; + b = this; + } + + if (a.layer === 0 && b.layer === 0) { return D(a.sign*b.sign*a.mag*b.mag); } + + //Special case: If one of the numbers is layer 3 or higher or one of the numbers is 2+ layers bigger than the other, just take the bigger number. + if (a.layer >= 3 || (a.layer - b.layer >= 2)) { return FC(a.sign*b.sign, a.layer, a.mag); } + + if (a.layer === 1 && b.layer === 0) + { + return FC(a.sign*b.sign, 1, a.mag+Math.log10(b.mag)); + } + + if (a.layer === 1 && b.layer === 1) + { + return FC(a.sign*b.sign, 1, a.mag+b.mag); + } + + if (a.layer === 2 && b.layer === 1) + { + var newmag = FC(Math.sign(a.mag), a.layer-1, Math.abs(a.mag)).add(FC(Math.sign(b.mag), b.layer-1, Math.abs(b.mag))); + return FC(a.sign*b.sign, newmag.layer+1, newmag.sign*newmag.mag); + } + + if (a.layer === 2 && b.layer === 2) + { + var newmag = FC(Math.sign(a.mag), a.layer-1, Math.abs(a.mag)).add(FC(Math.sign(b.mag), b.layer-1, Math.abs(b.mag))); + return FC(a.sign*b.sign, newmag.layer+1, newmag.sign*newmag.mag); + } + + throw Error("Bad arguments to mul: " + this + ", " + value); + }; + + Decimal.prototype.multiply = function (value) { + return this.mul(value); + }; + + Decimal.prototype.times = function (value) { + return this.mul(value); + }; + + Decimal.prototype.div = function (value) { + var decimal = D(value); + return this.mul(decimal.recip()); + }; + + Decimal.prototype.divide = function (value) { + return this.div(value); + }; + + Decimal.prototype.divideBy = function (value) { + return this.div(value); + }; + + Decimal.prototype.dividedBy = function (value) { + return this.div(value); + }; + + Decimal.prototype.recip = function () { + if (this.mag === 0) + { + return Decimal.dNaN; + } + else if (this.layer === 0) + { + return FC(this.sign, 0, 1/this.mag); + } + else + { + return FC(this.sign, this.layer, -this.mag); + } + }; + + Decimal.prototype.reciprocal = function () { + return this.recip(); + }; + + Decimal.prototype.reciprocate = function () { + return this.recip(); + }; + + /** + * -1 for less than value, 0 for equals value, 1 for greater than value + */ + Decimal.prototype.cmp = function (value) { + var decimal = D(value); + if (this.sign > decimal.sign) { return 1; } + if (this.sign < decimal.sign) { return -1; } + return this.sign*this.cmpabs(value); + }; + + Decimal.prototype.cmpabs = function (value) { + var decimal = D(value); + var layera = this.mag > 0 ? this.layer : -this.layer; + var layerb = decimal.mag > 0 ? decimal.layer : -decimal.layer; + if (layera > layerb) { return 1; } + if (layera < layerb) { return -1; } + if (this.mag > decimal.mag) { return 1; } + if (this.mag < decimal.mag) { return -1; } + return 0; + }; + + Decimal.prototype.compare = function (value) { + return this.cmp(value); + }; + + Decimal.prototype.eq = function (value) { + var decimal = D(value); + return this.sign === decimal.sign && this.layer === decimal.layer && this.mag === decimal.mag; + }; + + Decimal.prototype.equals = function (value) { + return this.eq(value); + }; + + Decimal.prototype.neq = function (value) { + return !this.eq(value); + }; + + Decimal.prototype.notEquals = function (value) { + return this.neq(value); + }; + + Decimal.prototype.lt = function (value) { + var decimal = D(value); + return this.cmp(value) === -1; + }; + + Decimal.prototype.lte = function (value) { + return !this.gt(value); + }; + + Decimal.prototype.gt = function (value) { + var decimal = D(value); + return this.cmp(value) === 1; + }; + + Decimal.prototype.gte = function (value) { + return !this.lt(value); + }; + + Decimal.prototype.max = function (value) { + var decimal = D(value); + return this.lt(decimal) ? decimal : this; + }; + + Decimal.prototype.min = function (value) { + var decimal = D(value); + return this.gt(decimal) ? decimal : this; + }; + + Decimal.prototype.maxabs = function (value) { + var decimal = D(value); + return this.cmpabs(decimal) < 0 ? decimal : this; + }; + + Decimal.prototype.minabs = function (value) { + var decimal = D(value); + return this.cmpabs(decimal) > 0 ? decimal : this; + }; + + Decimal.prototype.clamp = function(min, max) { + return this.max(min).min(max); + } + + Decimal.prototype.clampMin = function(min) { + return this.max(min); + } + + Decimal.prototype.clampMax = function(max) { + return this.min(max); + } + + Decimal.prototype.cmp_tolerance = function (value, tolerance) { + var decimal = D(value); + return this.eq_tolerance(decimal, tolerance) ? 0 : this.cmp(decimal); + }; + + Decimal.prototype.compare_tolerance = function (value, tolerance) { + return this.cmp_tolerance(value, tolerance); + }; + + /** + * Tolerance is a relative tolerance, multiplied by the greater of the magnitudes of the two arguments. + * For example, if you put in 1e-9, then any number closer to the + * larger number than (larger number)*1e-9 will be considered equal. + */ + Decimal.prototype.eq_tolerance = function (value, tolerance) { + var decimal = D(value); // https://stackoverflow.com/a/33024979 + if (tolerance == null) { tolerance = 1e-7; } + //Numbers that are too far away are never close. + if (this.sign !== decimal.sign) { return false; } + if (Math.abs(this.layer - decimal.layer) > 1) { return false; } + // return abs(a-b) <= tolerance * max(abs(a), abs(b)) + var magA = this.mag; + var magB = decimal.mag; + if (this.layer > decimal.layer) { magB = f_maglog10(magB); } + if (this.layer < decimal.layer) { magA = f_maglog10(magA); } + return Math.abs(magA-magB) <= tolerance*Math.max(Math.abs(magA), Math.abs(magB)); + }; + + Decimal.prototype.equals_tolerance = function (value, tolerance) { + return this.eq_tolerance(value, tolerance); + }; + + Decimal.prototype.neq_tolerance = function (value, tolerance) { + return !this.eq_tolerance(value, tolerance); + }; + + Decimal.prototype.notEquals_tolerance = function (value, tolerance) { + return this.neq_tolerance(value, tolerance); + }; + + Decimal.prototype.lt_tolerance = function (value, tolerance) { + var decimal = D(value); + return !this.eq_tolerance(decimal, tolerance) && this.lt(decimal); + }; + + Decimal.prototype.lte_tolerance = function (value, tolerance) { + var decimal = D(value); + return this.eq_tolerance(decimal, tolerance) || this.lt(decimal); + }; + + Decimal.prototype.gt_tolerance = function (value, tolerance) { + var decimal = D(value); + return !this.eq_tolerance(decimal, tolerance) && this.gt(decimal); + }; + + Decimal.prototype.gte_tolerance = function (value, tolerance) { + var decimal = D(value); + return this.eq_tolerance(decimal, tolerance) || this.gt(decimal); + }; + + Decimal.prototype.pLog10 = function() { + if (this.lt(Decimal.dZero)) { return Decimal.dZero; } + return this.log10(); + } + + Decimal.prototype.absLog10 = function () { + if (this.sign === 0) + { + return Decimal.dNaN; + } + else if (this.layer > 0) + { + return FC(Math.sign(this.mag), this.layer-1, Math.abs(this.mag)); + } + else + { + return FC(1, 0, Math.log10(this.mag)); + } + }; + + Decimal.prototype.log10 = function () { + if (this.sign <= 0) + { + return Decimal.dNaN; + } + else if (this.layer > 0) + { + return FC(Math.sign(this.mag), this.layer-1, Math.abs(this.mag)); + } + else + { + return FC(this.sign, 0, Math.log10(this.mag)); + } + }; + + Decimal.prototype.log = function (base) { + base = D(base); + if (this.sign <= 0) + { + return Decimal.dNaN; + } + if (base.sign <= 0) + { + return Decimal.dNaN; + } + if (base.sign === 1 && base.layer === 0 && base.mag === 1) + { + return Decimal.dNaN; + } + else if (this.layer === 0 && base.layer === 0) + { + return FC(this.sign, 0, Math.log(this.mag)/Math.log(base.mag)); + } + + return Decimal.div(this.log10(), base.log10()); + }; + + Decimal.prototype.log2 = function () { + if (this.sign <= 0) + { + return Decimal.dNaN; + } + else if (this.layer === 0) + { + return FC(this.sign, 0, Math.log2(this.mag)); + } + else if (this.layer === 1) + { + return FC(Math.sign(this.mag), 0, Math.abs(this.mag)*3.321928094887362); //log2(10) + } + else if (this.layer === 2) + { + return FC(Math.sign(this.mag), 1, Math.abs(this.mag)+0.5213902276543247); //-log10(log10(2)) + } + else + { + return FC(Math.sign(this.mag), this.layer-1, Math.abs(this.mag)); + } + }; + + Decimal.prototype.ln = function () { + if (this.sign <= 0) + { + return Decimal.dNaN; + } + else if (this.layer === 0) + { + return FC(this.sign, 0, Math.log(this.mag)); + } + else if (this.layer === 1) + { + return FC(Math.sign(this.mag), 0, Math.abs(this.mag)*2.302585092994046); //ln(10) + } + else if (this.layer === 2) + { + return FC(Math.sign(this.mag), 1, Math.abs(this.mag)+0.36221568869946325); //log10(log10(e)) + } + else + { + return FC(Math.sign(this.mag), this.layer-1, Math.abs(this.mag)); + } + }; + + Decimal.prototype.logarithm = function (base) { + return this.log(base); + }; + + Decimal.prototype.pow = function (value) { + var decimal = D(value); + var a = this; + var b = decimal; + + //special case: if a is 0, then return 0 (UNLESS b is 0, then return 1) + if (a.sign === 0) { return b.eq(0) ? FC_NN(1, 0, 1) : a; } + //special case: if a is 1, then return 1 + if (a.sign === 1 && a.layer === 0 && a.mag === 1) { return a; } + //special case: if b is 0, then return 1 + if (b.sign === 0) { return FC_NN(1, 0, 1); } + //special case: if b is 1, then return a + if (b.sign === 1 && b.layer === 0 && b.mag === 1) { return a; } + + var result = (a.absLog10().mul(b)).pow10(); + + if (this.sign === -1 && Math.abs(b.toNumber() % 2) === 1) { + return result.neg(); + } + + return result; + }; + + Decimal.prototype.pow10 = function() { + /* + There are four cases we need to consider: + 1) positive sign, positive mag (e15, ee15): +1 layer (e.g. 10^15 becomes e15, 10^e15 becomes ee15) + 2) negative sign, positive mag (-e15, -ee15): +1 layer but sign and mag sign are flipped (e.g. 10^-15 becomes e-15, 10^-e15 becomes ee-15) + 3) positive sign, negative mag (e-15, ee-15): layer 0 case would have been handled in the Math.pow check, so just return 1 + 4) negative sign, negative mag (-e-15, -ee-15): layer 0 case would have been handled in the Math.pow check, so just return 1 + */ + + if (!Number.isFinite(this.layer) || !Number.isFinite(this.mag)) { return Decimal.dNaN; } + + var a = this; + + //handle layer 0 case - if no precision is lost just use Math.pow, else promote one layer + if (a.layer === 0) + { + var newmag = Math.pow(10, a.sign*a.mag); + if (Number.isFinite(newmag) && Math.abs(newmag) > 0.1) { return FC(1, 0, newmag); } + else + { + if (a.sign === 0) { return Decimal.dOne; } + else { a = FC_NN(a.sign, a.layer+1, Math.log10(a.mag)); } + } + } + + //handle all 4 layer 1+ cases individually + if (a.sign > 0 && a.mag > 0) + { + return FC(a.sign, a.layer+1, a.mag); + } + if (a.sign < 0 && a.mag > 0) + { + return FC(-a.sign, a.layer+1, -a.mag); + } + //both the negative mag cases are identical: one +/- rounding error + return Decimal.dOne; + } + + Decimal.prototype.pow_base = function (value) { + return D(value).pow(this); + }; + + Decimal.prototype.root = function (value) { + var decimal = D(value); + return this.pow(decimal.recip()); + } + + Decimal.prototype.factorial = function () { + if (this.mag < 0) + { + return this.toNumber().add(1).gamma(); + } + else if (this.layer === 0) + { + return this.add(1).gamma(); + } + else if (this.layer === 1) + { + return Decimal.exp(Decimal.mul(this, Decimal.ln(this).sub(1))); + } + else + { + return Decimal.exp(this); + } + }; + + //from HyperCalc source code + Decimal.prototype.gamma = function () { + if (this.mag < 0) + { + return this.recip(); + } + else if (this.layer === 0) + { + if (this.lt(FC_NN(1, 0, 24))) + { + return D(f_gamma(this.sign*this.mag)); + } + + var t = this.mag - 1; + var l = 0.9189385332046727; //0.5*Math.log(2*Math.PI) + l = (l+((t+0.5)*Math.log(t))); + l = l-t; + var n2 = t*t; + var np = t; + var lm = 12*np; + var adj = 1/lm; + var l2 = l+adj; + if (l2 === l) + { + return Decimal.exp(l); + } + + l = l2; + np = np*n2; + lm = 360*np; + adj = 1/lm; + l2 = l-adj; + if (l2 === l) + { + return Decimal.exp(l); + } + + l = l2; + np = np*n2; + lm = 1260*np; + var lt = 1/lm; + l = l+lt; + np = np*n2; + lm = 1680*np; + lt = 1/lm; + l = l-lt; + return Decimal.exp(l); + } + else if (this.layer === 1) + { + return Decimal.exp(Decimal.mul(this, Decimal.ln(this).sub(1))); + } + else + { + return Decimal.exp(this); + } + }; + + Decimal.prototype.lngamma = function () { + return this.gamma().ln(); + } + + Decimal.prototype.exp = function () { + if (this.mag < 0) { return Decimal.dOne; } + if (this.layer === 0 && this.mag <= 709.7) { return D(Math.exp(this.sign*this.mag)); } + else if (this.layer === 0) { return FC(1, 1, this.sign*Math.log10(Math.E)*this.mag); } + else if (this.layer === 1) { return FC(1, 2, this.sign*(Math.log10(0.4342944819032518)+this.mag)); } + else { return FC(1, this.layer+1, this.sign*this.mag); } + }; + + Decimal.prototype.sqr = function () { + return this.pow(2); + }; + + Decimal.prototype.sqrt = function () { + if (this.layer === 0) { return D(Math.sqrt(this.sign*this.mag)); } + else if (this.layer === 1) { return FC(1, 2, Math.log10(this.mag)-0.3010299956639812); } + else + { + var result = Decimal.div(FC_NN(this.sign, this.layer-1, this.mag), FC_NN(1, 0, 2)); + result.layer += 1; + result.normalize(); + return result; + } + }; + + Decimal.prototype.cube = function () { + return this.pow(3); + }; + + Decimal.prototype.cbrt = function () { + return this.pow(1/3); + }; + + //Tetration/tetrate: The result of exponentiating 'this' to 'this' 'height' times in a row. https://en.wikipedia.org/wiki/Tetration + //If payload != 1, then this is 'iterated exponentiation', the result of exping (payload) to base (this) (height) times. https://andydude.github.io/tetration/archives/tetration2/ident.html + //Works with negative and positive real heights. + Decimal.prototype.tetrate = function(height = 2, payload = FC_NN(1, 0, 1)) { + if (height === Number.POSITIVE_INFINITY) + { + //Formula for infinite height power tower. + var negln = Decimal.ln(this).neg(); + return negln.lambertw().div(negln); + } + + if (height < 0) + { + return Decimal.iteratedlog(payload, this, -height); + } + + payload = D(payload); + var oldheight = height; + height = Math.trunc(height); + var fracheight = oldheight-height; + + if (fracheight !== 0) + { + if (payload.eq(Decimal.dOne)) + { + ++height; + payload = new Decimal(fracheight); + } + else + { + if (this.eq(10)) + { + payload = payload.layeradd10(fracheight); + } + else + { + payload = payload.layeradd(fracheight, this); + } + } + } + + for (var i = 0; i < height; ++i) + { + payload = this.pow(payload); + //bail if we're NaN + if (!isFinite(payload.layer) || !isFinite(payload.mag)) { return payload; } + //shortcut + if (payload.layer - this.layer > 3) { return FC_NN(payload.sign, payload.layer + (height - i - 1), payload.mag); } + //give up after 100 iterations if nothing is happening + if (i > 100) { return payload; } + } + return payload; + } + + //iteratedexp/iterated exponentiation: - all cases handled in tetrate, so just call it + Decimal.prototype.iteratedexp = function(height = 2, payload = FC_NN(1, 0, 1)) { + return this.tetrate(height, payload); + } + + //iterated log/repeated log: The result of applying log(base) 'times' times in a row. Approximately equal to subtracting (times) from the number's slog representation. Equivalent to tetrating to a negative height. + //Works with negative and positive real heights. + Decimal.prototype.iteratedlog = function(base = 10, times = 1) { + if (times < 0) + { + return Decimal.tetrate(base, -times, this); + } + + base = D(base); + var result = D(this); + var fulltimes = times; + times = Math.trunc(times); + var fraction = fulltimes - times; + if (result.layer - base.layer > 3) + { + var layerloss = Math.min(times, (result.layer - base.layer - 3)); + times -= layerloss; + result.layer -= layerloss; + } + + for (var i = 0; i < times; ++i) + { + result = result.log(base); + //bail if we're NaN + if (!isFinite(result.layer) || !isFinite(result.mag)) { return result; } + //give up after 100 iterations if nothing is happening + if (i > 100) { return result; } + } + + //handle fractional part + if (fraction > 0 && fraction < 1) + { + if (base.eq(10)) + { + result = result.layeradd10(-fraction); + } + else + { + result = result.layeradd(-fraction, base); + } + } + + return result; + } + + //Super-logarithm, one of tetration's inverses, tells you what size power tower you'd have to tetrate base to to get number. By definition, will never be higher than 1.8e308 in break_eternity.js, since a power tower 1.8e308 numbers tall is the largest representable number. + // https://en.wikipedia.org/wiki/Super-logarithm + Decimal.prototype.slog = function(base = 10) { + if (this.mag < 0) { return Decimal.dNegOne; } + + base = D(base); + + var result = 0; + var copy = D(this); + if (copy.layer - base.layer > 3) + { + var layerloss = (copy.layer - base.layer - 3); + result += layerloss; + copy.layer -= layerloss; + } + + for (var i = 0; i < 100; ++i) + { + if (copy.lt(Decimal.dZero)) + { + copy = Decimal.pow(base, copy); + result -= 1; + } + else if (copy.lte(Decimal.dOne)) + { + return D(result + copy.toNumber() - 1); //<-- THIS IS THE CRITICAL FUNCTION + //^ Also have to change tetrate payload handling and layeradd10 if this is changed! + } + else + { + result += 1; + copy = Decimal.log(copy, base); + } + } + return D(result); + } + + //Approximations taken from the excellent paper https://web.archive.org/web/20090201164836/http://tetration.itgo.com/paper.html ! + //Not using for now unless I can figure out how to use it in all the related functions. + /*var slog_criticalfunction_1 = function(x, z) { + z = z.toNumber(); + return -1 + z; + } + + var slog_criticalfunction_2 = function(x, z) { + z = z.toNumber(); + var lnx = x.ln(); + if (lnx.layer === 0) + { + lnx = lnx.toNumber(); + return -1 + z*2*lnx/(1+lnx) - z*z*(1-lnx)/(1+lnx); + } + else + { + var term1 = lnx.mul(z*2).div(lnx.add(1)); + var term2 = Decimal.sub(1, lnx).mul(z*z).div(lnx.add(1)); + Decimal.dNegOne.add(Decimal.sub(term1, term2)); + } + } + + var slog_criticalfunction_3 = function(x, z) { + z = z.toNumber(); + var lnx = x.ln(); + var lnx2 = lnx.sqr(); + var lnx3 = lnx.cube(); + if (lnx.layer === 0 && lnx2.layer === 0 && lnx3.layer === 0) + { + lnx = lnx.toNumber(); + lnx2 = lnx2.toNumber(); + lnx3 = lnx3.toNumber(); + + var term1 = 6*z*(lnx+lnx3); + var term2 = 3*z*z*(3*lnx2-2*lnx3); + var term3 = 2*z*z*z*(1-lnx-2*lnx2+lnx3); + var top = term1+term2+term3; + var bottom = 2+4*lnx+5*lnx2+2*lnx3; + + return -1 + top/bottom; + } + else + { + var term1 = (lnx.add(lnx3)).mul(6*z); + var term2 = (lnx2.mul(3).sub(lnx3.mul(2))).mul(3*z*z); + var term3 = (Decimal.dOne.sub(lnx).sub(lnx2.mul(2)).add(lnx3)).mul(2*z*z*z); + var top = term1.add(term2).add(term3); + var bottom = new Decimal(2).add(lnx.mul(4)).add(lnx2.mul(5)).add(lnx3.mul(2)); + + return Decimal.dNegOne.add(top.div(bottom)); + } + }*/ + + //Function for adding/removing layers from a Decimal, even fractional layers (e.g. its slog10 representation). + //Everything continues to use the linear approximation ATM. + Decimal.prototype.layeradd10 = function(diff) { + diff = Decimal.fromValue_noAlloc(diff).toNumber(); + var result = D(this); + if (diff >= 1) + { + var layeradd = Math.trunc(diff); + diff -= layeradd; + result.layer += layeradd; + } + if (diff <= -1) + { + var layeradd = Math.trunc(diff); + diff -= layeradd; + result.layer += layeradd; + if (result.layer < 0) + { + for (var i = 0; i < 100; ++i) + { + result.layer++; + result.mag = Math.log10(result.mag); + if (!isFinite(result.mag)) { return result; } + if (result.layer >= 0) { break; } + } + } + } + + //layeradd10: like adding 'diff' to the number's slog(base) representation. Very similar to tetrate base 10 and iterated log base 10. Also equivalent to adding a fractional amount to the number's layer in its break_eternity.js representation. + if (diff > 0) + { + var subtractlayerslater = 0; + //Ironically, this edge case would be unnecessary if we had 'negative layers'. + while (Number.isFinite(result.mag) && result.mag < 10) + { + result.mag = Math.pow(10, result.mag); + ++subtractlayerslater; + } + + //A^(10^B) === C, solve for B + //B === log10(logA(C)) + + if (result.mag > 1e10) + { + result.mag = Math.log10(result.mag); + result.layer++; + } + + //Note that every integer slog10 value, the formula changes, so if we're near such a number, we have to spend exactly enough layerdiff to hit it, and then use the new formula. + var diffToNextSlog = Math.log10(Math.log(1e10)/Math.log(result.mag), 10); + if (diffToNextSlog < diff) + { + result.mag = Math.log10(1e10); + result.layer++; + diff -= diffToNextSlog; + } + + result.mag = Math.pow(result.mag, Math.pow(10, diff)); + + while (subtractlayerslater > 0) + { + result.mag = Math.log10(result.mag); + --subtractlayerslater; + } + } + else if (diff < 0) + { + var subtractlayerslater = 0; + + while (Number.isFinite(result.mag) && result.mag < 10) + { + result.mag = Math.pow(10, result.mag); + ++subtractlayerslater; + } + + if (result.mag > 1e10) + { + result.mag = Math.log10(result.mag); + result.layer++; + } + + var diffToNextSlog = Math.log10(1/Math.log10(result.mag)); + if (diffToNextSlog > diff) + { + result.mag = 1e10; + result.layer--; + diff -= diffToNextSlog; + } + + result.mag = Math.pow(result.mag, Math.pow(10, diff)); + + while (subtractlayerslater > 0) + { + result.mag = Math.log10(result.mag); + --subtractlayerslater; + } + } + + while (result.layer < 0) + { + result.layer++; + result.mag = Math.log10(result.mag); + } + result.normalize(); + return result; + } + + //layeradd: like adding 'diff' to the number's slog(base) representation. Very similar to tetrate base 'base' and iterated log base 'base'. + Decimal.prototype.layeradd = function(diff, base) { + var slogthis = this.slog(base).toNumber(); + var slogdest = slogthis+diff; + if (slogdest >= 0) + { + return Decimal.tetrate(base, slogdest); + } + else if (!Number.isFinite(slogdest)) + { + return Decimal.dNaN; + } + else if (slogdest >= -1) + { + return Decimal.log(Decimal.tetrate(base, slogdest+1), base); + } + else + { + Decimal.log(Decimal.log(Decimal.tetrate(base, slogdest+2), base), base); + } + } + + //The Lambert W function, also called the omega function or product logarithm, is the solution W(x) === x*e^x. + // https://en.wikipedia.org/wiki/Lambert_W_function + //Some special values, for testing: https://en.wikipedia.org/wiki/Lambert_W_function#Special_values + Decimal.prototype.lambertw = function() { + if (this.lt(-0.3678794411710499)) + { + throw Error("lambertw is unimplemented for results less than -1, sorry!"); + } + else if (this.mag < 0) + { + return D(f_lambertw(this.toNumber())); + } + else if (this.layer === 0) + { + return D(f_lambertw(this.sign*this.mag)); + } + else if (this.layer === 1) + { + return d_lambertw(this); + } + else if (this.layer === 2) + { + return d_lambertw(this); + } + if (this.layer >= 3) + { + return FC_NN(this.sign, this.layer-1, this.mag); + } + } + + //from https://github.com/scipy/scipy/blob/8dba340293fe20e62e173bdf2c10ae208286692f/scipy/special/lambertw.pxd + // The evaluation can become inaccurate very close to the branch point + // at ``-1/e``. In some corner cases, `lambertw` might currently + // fail to converge, or can end up on the wrong branch. + var d_lambertw = function(z, tol = 1e-10) { + var w; + var ew, wew, wewz, wn; + + if (!Number.isFinite(z.mag)) { return z; } + if (z === 0) + { + return z; + } + if (z === 1) + { + //Split out this case because the asymptotic series blows up + return OMEGA; + } + + var absz = Decimal.abs(z); + //Get an initial guess for Halley's method + w = Decimal.ln(z); + + //Halley's method; see 5.9 in [1] + + for (var i = 0; i < 100; ++i) + { + ew = Decimal.exp(-w); + wewz = w.sub(z.mul(ew)); + wn = w.sub(wewz.div(w.add(1).sub((w.add(2)).mul(wewz).div((Decimal.mul(2, w).add(2)))))); + if (Decimal.abs(wn.sub(w)).lt(Decimal.abs(wn).mul(tol))) + { + return wn; + } + else + { + w = wn; + } + } + + throw Error("Iteration failed to converge: " + z); + //return Decimal.dNaN; + } + + //The super square-root function - what number, tetrated to height 2, equals this? + //Other sroots are possible to calculate probably through guess and check methods, this one is easy though. + // https://en.wikipedia.org/wiki/Tetration#Super-root + Decimal.prototype.ssqrt = function() { + if (this.sign == 1 && this.layer >= 3) + { + return FC_NN(this.sign, this.layer-1, this.mag) + } + var lnx = this.ln(); + return lnx.div(lnx.lambertw()); + } +/* + +Unit tests for tetrate/iteratedexp/iteratedlog/layeradd10/layeradd/slog: + +for (var i = 0; i < 1000; ++i) +{ + var first = Math.random()*100; + var both = Math.random()*100; + var expected = first+both+1; + var result = new Decimal(10).layeradd10(first).layeradd10(both).slog(); + if (Number.isFinite(result.mag) && !Decimal.eq_tolerance(expected, result)) + { + console.log(first + ", " + both); + } +} + +for (var i = 0; i < 1000; ++i) +{ + var first = Math.random()*100; + var both = Math.random()*100; + first += both; + var expected = first-both+1; + var result = new Decimal(10).layeradd10(first).layeradd10(-both).slog(); + if (Number.isFinite(result.mag) && !Decimal.eq_tolerance(expected, result)) + { + console.log(first + ", " + both); + } +} + +for (var i = 0; i < 1000; ++i) +{ + var first = Math.random()*100; + var both = Math.random()*100; + var base = Math.random()*8+2; + var expected = first+both+1; + var result = new Decimal(base).layeradd(first, base).layeradd(both, base).slog(base); + if (Number.isFinite(result.mag) && !Decimal.eq_tolerance(expected, result)) + { + console.log(first + ", " + both); + } +} + +for (var i = 0; i < 1000; ++i) +{ + var first = Math.random()*100; + var both = Math.random()*100; + var base = Math.random()*8+2; + first += both; + var expected = first-both+1; + var result = new Decimal(base).layeradd(first, base).layeradd(-both, base).slog(base); + if (Number.isFinite(result.mag) && !Decimal.eq_tolerance(expected, result)) + { + console.log(first + ", " + both); + } +} + +for (var i = 0; i < 1000; ++i) +{ + var first = Math.round((Math.random()*30))/10; + var both = Math.round((Math.random()*30))/10; + var tetrateonly = Decimal.tetrate(10, first); + var tetrateandlog = Decimal.tetrate(10, first+both).iteratedlog(10, both); + if (!Decimal.eq_tolerance(tetrateonly, tetrateandlog)) + { + console.log(first + ", " + both); + } +} + +for (var i = 0; i < 1000; ++i) +{ + var first = Math.round((Math.random()*30))/10; + var both = Math.round((Math.random()*30))/10; + var base = Math.random()*8+2; + var tetrateonly = Decimal.tetrate(base, first); + var tetrateandlog = Decimal.tetrate(base, first+both).iteratedlog(base, both); + if (!Decimal.eq_tolerance(tetrateonly, tetrateandlog)) + { + console.log(first + ", " + both); + } +} + +for (var i = 0; i < 1000; ++i) +{ + var first = Math.round((Math.random()*30))/10; + var both = Math.round((Math.random()*30))/10; + var base = Math.random()*8+2; + var tetrateonly = Decimal.tetrate(base, first, base); + var tetrateandlog = Decimal.tetrate(base, first+both, base).iteratedlog(base, both); + if (!Decimal.eq_tolerance(tetrateonly, tetrateandlog)) + { + console.log(first + ", " + both); + } +} + +for (var i = 0; i < 1000; ++i) +{ + var xex = new Decimal(-0.3678794411710499+Math.random()*100); + var x = Decimal.lambertw(xex); + if (!Decimal.eq_tolerance(xex, x.mul(Decimal.exp(x)))) + { + console.log(xex); + } +} + +for (var i = 0; i < 1000; ++i) +{ + var xex = new Decimal(-0.3678794411710499+Math.exp(Math.random()*100)); + var x = Decimal.lambertw(xex); + if (!Decimal.eq_tolerance(xex, x.mul(Decimal.exp(x)))) + { + console.log(xex); + } +} + +for (var i = 0; i < 1000; ++i) +{ + var a = Decimal.randomDecimalForTesting(Math.random() > 0.5 ? 0 : 1); + var b = Decimal.randomDecimalForTesting(Math.random() > 0.5 ? 0 : 1); + if (Math.random() > 0.5) { a = a.recip(); } + if (Math.random() > 0.5) { b = b.recip(); } + var c = a.add(b).toNumber(); + if (Number.isFinite(c) && !Decimal.eq_tolerance(c, a.toNumber()+b.toNumber())) + { + console.log(a + ", " + b); + } +} + +for (var i = 0; i < 100; ++i) +{ + var a = Decimal.randomDecimalForTesting(Math.round(Math.random()*4)); + var b = Decimal.randomDecimalForTesting(Math.round(Math.random()*4)); + if (Math.random() > 0.5) { a = a.recip(); } + if (Math.random() > 0.5) { b = b.recip(); } + var c = a.mul(b).toNumber(); + if (Number.isFinite(c) && Number.isFinite(a.toNumber()) && Number.isFinite(b.toNumber()) && a.toNumber() != 0 && b.toNumber() != 0 && c != 0 && !Decimal.eq_tolerance(c, a.toNumber()*b.toNumber())) + { + console.log("Test 1: " + a + ", " + b); + } + else if (!Decimal.mul(a.recip(), b.recip()).eq_tolerance(Decimal.mul(a, b).recip())) + { + console.log("Test 3: " + a + ", " + b); + } +} + +for (var i = 0; i < 10; ++i) +{ + var a = Decimal.randomDecimalForTesting(Math.round(Math.random()*4)); + var b = Decimal.randomDecimalForTesting(Math.round(Math.random()*4)); + if (Math.random() > 0.5 && a.sign !== 0) { a = a.recip(); } + if (Math.random() > 0.5 && b.sign !== 0) { b = b.recip(); } + var c = a.pow(b); + var d = a.root(b.recip()); + var e = a.pow(b.recip()); + var f = a.root(b); + + if (!c.eq_tolerance(d) && a.sign !== 0 && b.sign !== 0) + { + console.log("Test 1: " + a + ", " + b); + } + if (!e.eq_tolerance(f) && a.sign !== 0 && b.sign !== 0) + { + console.log("Test 2: " + a + ", " + b); + } +} + +for (var i = 0; i < 10; ++i) +{ + var a = Math.round(Math.random()*18-9); + var b = Math.round(Math.random()*100-50); + var c = Math.round(Math.random()*18-9); + var d = Math.round(Math.random()*100-50); + console.log("Decimal.pow(Decimal.fromMantissaExponent(" + a + ", " + b + "), Decimal.fromMantissaExponent(" + c + ", " + d + ")).toString()"); +} + +*/ + + //Pentation/pentate: The result of tetrating 'height' times in a row. An absurdly strong operator - Decimal.pentate(2, 4.28) and Decimal.pentate(10, 2.37) are already too huge for break_eternity.js! + // https://en.wikipedia.org/wiki/Pentation + Decimal.prototype.pentate = function(height = 2, payload = FC_NN(1, 0, 1)) { + payload = D(payload); + var oldheight = height; + height = Math.trunc(height); + var fracheight = oldheight-height; + + //I have no idea if this is a meaningful approximation for pentation to continuous heights, but it is monotonic and continuous. + if (fracheight !== 0) + { + if (payload.eq(Decimal.dOne)) + { + ++height; + payload = new Decimal(fracheight); + } + else + { + if (this.eq(10)) + { + payload = payload.layeradd10(fracheight); + } + else + { + payload = payload.layeradd(fracheight, this); + } + } + } + + for (var i = 0; i < height; ++i) + { + payload = this.tetrate(payload); + //bail if we're NaN + if (!isFinite(payload.layer) || !isFinite(payload.mag)) { return payload; } + //give up after 10 iterations if nothing is happening + if (i > 10) { return payload; } + } + + return payload; + } + + // trig functions! + Decimal.prototype.sin = function () { + if (this.mag < 0) { return this; } + if (this.layer === 0) { return D(Math.sin(this.sign*this.mag)); } + return FC_NN(0, 0, 0); + }; + + Decimal.prototype.cos = function () { + if (this.mag < 0) { return Decimal.dOne; } + if (this.layer === 0) { return D(Math.cos(this.sign*this.mag)); } + return FC_NN(0, 0, 0); + }; + + Decimal.prototype.tan = function () { + if (this.mag < 0) { return this; } + if (this.layer === 0) { return D(Math.tan(this.sign*this.mag)); } + return FC_NN(0, 0, 0); + }; + + Decimal.prototype.asin = function () { + if (this.mag < 0) { return this; } + if (this.layer === 0) { return D(Math.asin(this.sign*this.mag)); } + return FC_NN(Number.NaN, Number.NaN, Number.NaN); + }; + + Decimal.prototype.acos = function () { + if (this.mag < 0) { return D(Math.acos(this.toNumber())); } + if (this.layer === 0) { return D(Math.acos(this.sign*this.mag)); } + return FC_NN(Number.NaN, Number.NaN, Number.NaN); + }; + + Decimal.prototype.atan = function () { + if (this.mag < 0) { return this; } + if (this.layer === 0) { return D(Math.atan(this.sign*this.mag)); } + return D(Math.atan(this.sign*1.8e308)); + }; + + Decimal.prototype.sinh = function () { + return this.exp().sub(this.negate().exp()).div(2); + }; + + Decimal.prototype.cosh = function () { + return this.exp().add(this.negate().exp()).div(2); + }; + + Decimal.prototype.tanh = function () { + return this.sinh().div(this.cosh()); + }; + + Decimal.prototype.asinh = function () { + return Decimal.ln(this.add(this.sqr().add(1).sqrt())); + }; + + Decimal.prototype.acosh = function () { + return Decimal.ln(this.add(this.sqr().sub(1).sqrt())); + }; + + Decimal.prototype.atanh = function () { + if (this.abs().gte(1)) { + return FC_NN(Number.NaN, Number.NaN, Number.NaN); + } + + return Decimal.ln(this.add(1).div(D(1).sub(this))).div(2); + }; + + /** + * Joke function from Realm Grinder + */ + Decimal.prototype.ascensionPenalty = function (ascensions) { + if (ascensions === 0) { + return this; + } + + return this.root(Decimal.pow(10, ascensions)); + }; + + /** + * Joke function from Cookie Clicker. It's 'egg' + */ + Decimal.prototype.egg = function () { + return this.add(9); + }; + + Decimal.prototype.lessThanOrEqualTo = function (other) { + return this.cmp(other) < 1; + }; + + Decimal.prototype.lessThan = function (other) { + return this.cmp(other) < 0; + }; + + Decimal.prototype.greaterThanOrEqualTo = function (other) { + return this.cmp(other) > -1; + }; + + Decimal.prototype.greaterThan = function (other) { + return this.cmp(other) > 0; + }; + + return Decimal; + }(); + + Decimal.dZero = FC_NN(0, 0, 0); + Decimal.dOne = FC_NN(1, 0, 1); + Decimal.dNegOne = FC_NN(-1, 0, 1); + Decimal.dTwo = FC_NN(1, 0, 2); + Decimal.dTen = FC_NN(1, 0, 10); + Decimal.dNaN = FC_NN(Number.NaN, Number.NaN, Number.NaN); + Decimal.dInf = FC_NN(1, Number.POSITIVE_INFINITY, Number.POSITIVE_INFINITY); + Decimal.dNegInf = FC_NN(-1, Number.NEGATIVE_INFINITY, Number.NEGATIVE_INFINITY); + Decimal.dNumberMax = FC(1, 0, Number.MAX_VALUE); + Decimal.dNumberMin = FC(1, 0, Number.MIN_VALUE); + + return Decimal; + +})); diff --git a/js/technical/canvas.js b/js/technical/canvas.js new file mode 100644 index 0000000..a5f64fa --- /dev/null +++ b/js/technical/canvas.js @@ -0,0 +1,88 @@ +var canvas; +var ctx; + +window.addEventListener("resize", (_=>resizeCanvas())); + +function retrieveCanvasData() { + let treeCanv = document.getElementById("treeCanvas") + let treeTab = document.getElementById("treeTab") + if (treeCanv===undefined||treeCanv===null) return false; + canvas = treeCanv; + ctx = canvas.getContext("2d"); + return true; +} + +function resizeCanvas() { + if (!retrieveCanvasData()) return + canvas.width = 0; + canvas.height = 0; + canvas.width = window.innerWidth; + canvas.height = window.innerHeight; + drawTree(); +} + + +var colors_theme + +function drawTree() { + if (!retrieveCanvasData()) return; + ctx.clearRect(0, 0, canvas.width, canvas.height); + for (layer in layers){ + if (tmp[layer].layerShown == true && tmp[layer].branches){ + for (branch in tmp[layer].branches) + { + drawTreeBranch(layer, tmp[layer].branches[branch]) + } + } + drawComponentBranches(layer, tmp[layer].upgrades, "upgrade-") + drawComponentBranches(layer, tmp[layer].buyables, "buyable-") + drawComponentBranches(layer, tmp[layer].clickables, "clickable-") + + } +} + +function drawComponentBranches(layer, data, prefix) { + for(id in data) { + if (data[id].branches) { + for (branch in data[id].branches) + { + drawTreeBranch(id, data[id].branches[branch], prefix + layer + "-") + } + + } + } + +} + +function drawTreeBranch(num1, data, prefix) { // taken from Antimatter Dimensions & adjusted slightly + let num2 = data + let color_id = 1 + let width = 15 + if (Array.isArray(data)){ + num2 = data[0] + color_id = data[1] + width = data[2] || width + } + + if(typeof(color_id) == "number") + color_id = colors_theme[color_id] + if (prefix) { + num1 = prefix + num1 + num2 = prefix + num2 + } + if (document.getElementById(num1) == null || document.getElementById(num2) == null) + return + + let start = document.getElementById(num1).getBoundingClientRect(); + let end = document.getElementById(num2).getBoundingClientRect(); + let x1 = start.left + (start.width / 2) + document.body.scrollLeft; + let y1 = start.top + (start.height / 2) + document.body.scrollTop; + let x2 = end.left + (end.width / 2) + document.body.scrollLeft; + let y2 = end.top + (end.height / 2) + document.body.scrollTop; + ctx.lineWidth = width; + ctx.beginPath(); + ctx.strokeStyle = color_id + ctx.moveTo(x1, y1); + ctx.lineTo(x2, y2); + ctx.stroke(); +} diff --git a/js/technical/displays.js b/js/technical/displays.js new file mode 100644 index 0000000..28c8cff --- /dev/null +++ b/js/technical/displays.js @@ -0,0 +1,195 @@ +function prestigeButtonText(layer) { + if (layers[layer].prestigeButtonText !== undefined) + return run(layers[layer].prestigeButtonText(), layers[layer]) + if (tmp[layer].type == "normal") + return `${player[layer].points.lt(1e3) ? (tmp[layer].resetDescription !== undefined ? tmp[layer].resetDescription : "Reset for ") : ""}+${formatWhole(tmp[layer].resetGain)} ${tmp[layer].resource} ${tmp[layer].resetGain.lt(100) && player[layer].points.lt(1e3) ? `

Next at ${(tmp[layer].roundUpCost ? formatWhole(tmp[layer].nextAt) : format(tmp[layer].nextAt))} ${tmp[layer].baseResource}` : ""}` + if (tmp[layer].type == "static") + return `${tmp[layer].resetDescription !== undefined ? tmp[layer].resetDescription : "Reset for "}+${formatWhole(tmp[layer].resetGain)} ${tmp[layer].resource}

${player[layer].points.lt(30) ? (tmp[layer].baseAmount.gte(tmp[layer].nextAt) && (tmp[layer].canBuyMax !== undefined) && tmp[layer].canBuyMax ? "Next:" : "Req:") : ""} ${formatWhole(tmp[layer].baseAmount)} / ${(tmp[layer].roundUpCost ? formatWhole(tmp[layer].nextAtDisp) : format(tmp[layer].nextAtDisp))} ${tmp[layer].baseResource} + ` + if (tmp[layer].type == "none") + return "" + + return "You need prestige button text" +} + +function constructNodeStyle(layer){ + let style = [] + if ((tmp[layer].isLayer && layerunlocked(layer)) || (!tmp[layer].isLayer && tmp[layer].canClick)) + style.push({'background-color': tmp[layer].color}) + if (tmp[layer].image !== undefined) + style.push({'background-image': 'url("' + tmp[layer].image + '")'}) + if(tmp[layer].notify && player[layer].unlocked) + style.push({'box-shadow': 'var(--hqProperty2a), 0 0 20px ' + tmp[layer].trueGlowColor}) + style.push(tmp[layer].nodeStyle) + return style +} + + +function challengeStyle(layer, id) { + if (player[layer].activeChallenge == id && canCompleteChallenge(layer, id)) return "canComplete" + else if (hasChallenge(layer, id)) return "done" + return "locked" +} + +function challengeButtonText(layer, id) { + return (player[layer].activeChallenge==(id)?(canCompleteChallenge(layer, id)?"Finish":"Exit Early"):(hasChallenge(layer, id)?"Completed":"Start")) + +} + +function achievementStyle(layer, id){ + ach = tmp[layer].achievements[id] + let style = [] + if (ach.image){ + style.push({'background-image': 'url("' + ach.image + '")'}) + } + if (!ach.unlocked) style.push({'visibility': 'hidden'}) + style.push(ach.style) + return style +} + + + +function updateWidth() { + let screenWidth = window.innerWidth + let splitScreen = screenWidth >= 1024 + if (options.forceOneTab) splitScreen = false + if (player.navTab == "none") splitScreen = true + tmp.other.screenWidth = screenWidth + tmp.other.screenHeight = window.innerHeight + + tmp.other.splitScreen = splitScreen + tmp.other.lastPoints = player.points +} + +function updateOomps(diff) +{ + tmp.other.oompsMag = 0 + if (player.points.lte(new Decimal(1e100)) || diff == 0) return + + var pp = new Decimal(player.points); + var lp = tmp.other.lastPoints || new Decimal(0); + if (pp.gt(lp)) { + if (pp.gte("10^^8")) { + pp = pp.slog(1e10) + lp = lp.slog(1e10) + tmp.other.oomps = pp.sub(lp).div(diff) + tmp.other.oompsMag = -1; + } else { + while (pp.div(lp).log(10).div(diff).gte("100") && tmp.other.oompsMag <= 5 && lp.gt(0)) { + pp = pp.log(10) + lp = lp.log(10) + tmp.other.oomps = pp.sub(lp).div(diff) + tmp.other.oompsMag++; + } + } + } + +} + +function constructBarStyle(layer, id) { + let bar = tmp[layer].bars[id] + let style = {} + if (bar.progress instanceof Decimal) + bar.progress = bar.progress.toNumber() + bar.progress = (1 -Math.min(Math.max(bar.progress, 0), 1)) * 100 + + style.dims = {'width': bar.width + "px", 'height': bar.height + "px"} + let dir = bar.direction + style.fillDims = {'width': (bar.width + 0.5) + "px", 'height': (bar.height + 0.5) + "px"} + + switch(bar.direction) { + case UP: + style.fillDims['clip-path'] = 'inset(' + bar.progress + '% 0% 0% 0%)' + style.fillDims.width = bar.width + 1 + 'px' + break; + case DOWN: + style.fillDims['clip-path'] = 'inset(0% 0% ' + bar.progress + '% 0%)' + style.fillDims.width = bar.width + 1 + 'px' + + break; + case RIGHT: + style.fillDims['clip-path'] = 'inset(0% ' + bar.progress + '% 0% 0%)' + break; + case LEFT: + style.fillDims['clip-path'] = 'inset(0% 0% 0% ' + bar.progress + '%)' + break; + case DEFAULT: + style.fillDims['clip-path'] = 'inset(0% 50% 0% 0%)' + } + + if (bar.instant) { + style.fillDims['transition-duration'] = '0s' + } + return style +} + +function constructTabFormat(layer, id, family){ + let tabTemp, tabLayer, tabFunc, location, key + if (id === undefined){ + tabTemp = tmp[layer].tabFormat + tabLayer = layers[layer].tabFormat + tabFunc = funcs[layer].tabFormat + location = tmp[layer] + key = "tabFormat" + } + else if (family === undefined) { + tabTemp = tmp[layer].tabFormat[id].content + tabLayer = layers[layer].tabFormat[id].content + tabFunc = funcs[layer].tabFormat[id].content + location = tmp[layer].tabFormat[id] + key = "content" + + } + else { + tabTemp = tmp[layer].microtabs[family][id].content + tabLayer = layers[layer].microtabs[family][id].content + tabFunc = funcs[layer].microtabs[family][id].content + location = tmp[layer].microtabs[family][id] + key = "tabFormat" + + } + if (isFunction(tabLayer)) { + return tabLayer.bind(location)() + } + updateTempData(tabLayer, tabTemp, tabFunc, {layer, id, family}) + return tabTemp +} + +function updateTabFormats() { + updateTabFormat(player.tab) + updateTabFormat(player.navTab) +} + +function updateTabFormat(layer) { + if (layers[layer]?.tabFormat === undefined) return + + let tab = player.subtabs[layer]?.mainTabs + if (isFunction(layers[layer].tabFormat)) { + Vue.set(temp[layer], 'tabFormat', layers[layer].tabFormat()) + } + else if (Array.isArray(layers[layer].tabFormat)) { + Vue.set(temp[layer], 'tabFormat', constructTabFormat(layer)) + } + else if (isPlainObject(layers[layer].tabFormat)) { + if (layers[layer].tabFormat[tab].embedLayer === undefined) + Vue.set(temp[layer].tabFormat[tab], 'content', constructTabFormat(layer, tab)) + } + + // Check for embedded layer + if (isPlainObject(tmp[layer].tabFormat) && tmp[layer].tabFormat[tab].embedLayer !== undefined) { + updateTabFormat(tmp[layer].tabFormat[tab].embedLayer) + } + + // Update microtabs + for (family in layers[layer].microtabs) { + tab = player.subtabs[layer][family] + + if (tmp[layer].microtabs[family][tab]) { + + if (tmp[layer].microtabs[family][tab].embedLayer) + updateTabFormat(tmp[layer].microtabs[family][tab].embedLayer) + else + Vue.set(temp[layer].microtabs[family][tab], 'content', constructTabFormat(layer, tab, family)) + } + } +} \ No newline at end of file diff --git a/js/technical/layerSupport.js b/js/technical/layerSupport.js new file mode 100644 index 0000000..745906f --- /dev/null +++ b/js/technical/layerSupport.js @@ -0,0 +1,294 @@ +var layers = {} + +const decimalZero = new Decimal(0) +const decimalOne = new Decimal(1) +const decimalNaN = new Decimal(NaN) + +const defaultGlow = "#ff0000" + + +function layerShown(layer){ + return tmp[layer].layerShown; +} + +var LAYERS = Object.keys(layers); + +var hotkeys = {}; + +var maxRow = 0; + +function updateHotkeys() +{ + hotkeys = {}; + for (layer in layers){ + hk = layers[layer].hotkeys + if (hk){ + for (id in hk){ + hotkeys[hk[id].key] = hk[id] + hotkeys[hk[id].key].layer = layer + hotkeys[hk[id].key].id = id + if (hk[id].unlocked === undefined) + hk[id].unlocked = true + } + } + } +} + +var ROW_LAYERS = {} +var TREE_LAYERS = {} +var OTHER_LAYERS = {} + +function updateLayers(){ + LAYERS = Object.keys(layers); + ROW_LAYERS = {} + TREE_LAYERS = {} + OTHER_LAYERS = {} + for (layer in layers){ + setupLayer(layer) + } + for (row in OTHER_LAYERS) { + OTHER_LAYERS[row].sort((a, b) => (a.position > b.position) ? 1 : -1) + for (layer in OTHER_LAYERS[row]) + OTHER_LAYERS[row][layer] = OTHER_LAYERS[row][layer].layer + } + for (row in TREE_LAYERS) { + TREE_LAYERS[row].sort((a, b) => (a.position > b.position) ? 1 : -1) + for (layer in TREE_LAYERS[row]) + TREE_LAYERS[row][layer] = TREE_LAYERS[row][layer].layer + } + let treeLayers2 = [] + for (x = 0; x < maxRow + 1; x++) { + if (TREE_LAYERS[x]) treeLayers2.push(TREE_LAYERS[x]) + } + TREE_LAYERS = treeLayers2 + updateHotkeys() +} + +function setupLayer(layer){ + layers[layer].layer = layer + if (layers[layer].upgrades){ + setRowCol(layers[layer].upgrades) + for (thing in layers[layer].upgrades){ + if (isPlainObject(layers[layer].upgrades[thing])){ + layers[layer].upgrades[thing].id = thing + layers[layer].upgrades[thing].layer = layer + if (layers[layer].upgrades[thing].unlocked === undefined) + layers[layer].upgrades[thing].unlocked = true + } + } + } + if (layers[layer].milestones){ + for (thing in layers[layer].milestones){ + if (isPlainObject(layers[layer].milestones[thing])){ + layers[layer].milestones[thing].id = thing + layers[layer].milestones[thing].layer = layer + if (layers[layer].milestones[thing].unlocked === undefined) + layers[layer].milestones[thing].unlocked = true + } + } + } + if (layers[layer].achievements){ + setRowCol(layers[layer].achievements) + for (thing in layers[layer].achievements){ + if (isPlainObject(layers[layer].achievements[thing])){ + layers[layer].achievements[thing].id = thing + layers[layer].achievements[thing].layer = layer + if (layers[layer].achievements[thing].unlocked === undefined) + layers[layer].achievements[thing].unlocked = true + } + } + } + if (layers[layer].challenges){ + setRowCol(layers[layer].challenges) + for (thing in layers[layer].challenges){ + if (isPlainObject(layers[layer].challenges[thing])){ + layers[layer].challenges[thing].id = thing + layers[layer].challenges[thing].layer = layer + if (layers[layer].challenges[thing].unlocked === undefined) + layers[layer].challenges[thing].unlocked = true + if (layers[layer].challenges[thing].completionLimit === undefined) + layers[layer].challenges[thing].completionLimit = 1 + else if (layers[layer].challenges[thing].marked === undefined) + layers[layer].challenges[thing].marked = function() {return maxedChallenge(this.layer, this.id)} + + } + } + } + if (layers[layer].buyables){ + layers[layer].buyables.layer = layer + setRowCol(layers[layer].buyables) + for (thing in layers[layer].buyables){ + if (isPlainObject(layers[layer].buyables[thing])){ + layers[layer].buyables[thing].id = thing + layers[layer].buyables[thing].layer = layer + if (layers[layer].buyables[thing].unlocked === undefined) + layers[layer].buyables[thing].unlocked = true + layers[layer].buyables[thing].canBuy = function() {return canBuyBuyable(this.layer, this.id)} + if (layers[layer].buyables[thing].purchaseLimit === undefined) layers[layer].buyables[thing].purchaseLimit = new Decimal(Infinity) + + } + + } + } + + if (layers[layer].clickables){ + layers[layer].clickables.layer = layer + setRowCol(layers[layer].clickables) + for (thing in layers[layer].clickables){ + if (isPlainObject(layers[layer].clickables[thing])){ + layers[layer].clickables[thing].id = thing + layers[layer].clickables[thing].layer = layer + if (layers[layer].clickables[thing].unlocked === undefined) + layers[layer].clickables[thing].unlocked = true + } + } + } + + if (layers[layer].bars){ + layers[layer].bars.layer = layer + for (thing in layers[layer].bars){ + layers[layer].bars[thing].id = thing + layers[layer].bars[thing].layer = layer + if (layers[layer].bars[thing].unlocked === undefined) + layers[layer].bars[thing].unlocked = true + } + } + + if (layers[layer].infoboxes){ + for (thing in layers[layer].infoboxes){ + layers[layer].infoboxes[thing].id = thing + layers[layer].infoboxes[thing].layer = layer + if (layers[layer].infoboxes[thing].unlocked === undefined) + layers[layer].infoboxes[thing].unlocked = true + } + } + + if (layers[layer].grid) { + layers[layer].grid.layer = layer + if (layers[layer].grid.getUnlocked === undefined) + layers[layer].grid.getUnlocked = true + if (layers[layer].grid.getCanClick === undefined) + layers[layer].grid.getCanClick = true + + } + if (layers[layer].startData) { + data = layers[layer].startData() + if (data.best !== undefined && data.showBest === undefined) layers[layer].showBest = true + if (data.total !== undefined && data.showTotal === undefined) layers[layer].showTotal = true + } + + if(!layers[layer].componentStyles) layers[layer].componentStyles = {} + if(layers[layer].symbol === undefined) layers[layer].symbol = layer.charAt(0).toUpperCase() + layer.slice(1) + if(layers[layer].unlockOrder === undefined) layers[layer].unlockOrder = [] + if(layers[layer].gainMult === undefined) layers[layer].gainMult = decimalOne + if(layers[layer].gainExp === undefined) layers[layer].gainExp = decimalOne + if(layers[layer].directMult === undefined) layers[layer].directMult = decimalOne + if(layers[layer].type === undefined) layers[layer].type = "none" + if(layers[layer].base === undefined || layers[layer].base <= 1) layers[layer].base = 2 + if(layers[layer].softcap === undefined) layers[layer].softcap = new Decimal("e1e7") + if(layers[layer].softcapPower === undefined) layers[layer].softcapPower = new Decimal("0.5") + if(layers[layer].displayRow === undefined) layers[layer].displayRow = layers[layer].row + if(layers[layer].name === undefined) layers[layer].name = layer + if(layers[layer].layerShown === undefined) layers[layer].layerShown = true + if(layers[layer].glowColor === undefined) layers[layer].glowColor = defaultGlow + + let row = layers[layer].row + + let displayRow = layers[layer].displayRow + + if(!ROW_LAYERS[row]) ROW_LAYERS[row] = {} + if(!TREE_LAYERS[displayRow] && !isNaN(displayRow)) TREE_LAYERS[displayRow] = [] + if(!OTHER_LAYERS[displayRow] && isNaN(displayRow)) OTHER_LAYERS[displayRow] = [] + + ROW_LAYERS[row][layer]=layer; + let position = (layers[layer].position !== undefined ? layers[layer].position : layer) + + if (!isNaN(displayRow) || displayRow < 0) TREE_LAYERS[displayRow].push({layer: layer, position: position}) + else OTHER_LAYERS[displayRow].push({layer: layer, position: position}) + + if (maxRow < layers[layer].displayRow) maxRow = layers[layer].displayRow + +} + + +function addLayer(layerName, layerData, tabLayers = null){ // Call this to add layers from a different file! + layers[layerName] = layerData + layers[layerName].isLayer = true + if (tabLayers !== null) + { + let format = {} + for (id in tabLayers) { + layer = tabLayers[id] + format[(layers[layer].name ? layers[layer].name : layer)] = { + embedLayer: layer, + buttonStyle() { + if (!tmp[this.embedLayer].nodeStyle) return {'border-color': tmp[this.embedLayer].color} + else { + style = tmp[this.embedLayer].nodeStyle + if (style['border-color'] === undefined) style['border-color'] = tmp[this.embedLayer].color + return style + } + }, + unlocked() {return tmp[this.embedLayer].layerShown}, + } + } + layers[layerName].tabFormat = format + } +} + +function addNode(layerName, layerData){ // Does the same thing, but for non-layer nodes + layers[layerName] = layerData + layers[layerName].isLayer = false +} + +// If data is a function, return the result of calling it. Otherwise, return the data. +function readData(data, args=null){ + if ((!!data && data.constructor && data.call && data.apply)) + return data(args); + else + return data; +} + +function setRowCol(upgrades) { + if (upgrades.rows && upgrades.cols) return + let maxRow = 0 + let maxCol = 0 + for (up in upgrades) { + if (!isNaN(up)) { + if (Math.floor(up/10) > maxRow) maxRow = Math.floor(up/10) + if (up%10 > maxCol) maxCol = up%10 + } + } + upgrades.rows = maxRow + upgrades.cols = maxCol +} + +function someLayerUnlocked(row){ + for (layer in ROW_LAYERS[row]) + if (player[layer].unlocked) + return true + return false +} + + +// This isn't worth making a .ts file over +const UP = 0 +const DOWN = 1 +const LEFT = 2 +const RIGHT = 3 + + +addLayer("info-tab", { + tabFormat: ["info-tab"], + row: "otherside" +}) + +addLayer("options-tab", { + tabFormat: ["options-tab"], + row: "otherside" +}) + +addLayer("changelog-tab", { + tabFormat() {return ([["raw-html", modInfo.changelog]])}, + row: "otherside" +}) \ No newline at end of file diff --git a/js/technical/loader.js b/js/technical/loader.js new file mode 100644 index 0000000..e7c2f91 --- /dev/null +++ b/js/technical/loader.js @@ -0,0 +1,9 @@ +// Load files + +for (file in modInfo.modFiles) { + let script = document.createElement("script"); + script.setAttribute("src", "js/" + modInfo.modFiles[file]); + script.setAttribute("async", "false"); + document.head.insertBefore(script, document.getElementById("temp")); +} + diff --git a/js/technical/particleSystem.js b/js/technical/particleSystem.js new file mode 100644 index 0000000..ba5a4b8 --- /dev/null +++ b/js/technical/particleSystem.js @@ -0,0 +1,186 @@ +var particles = {}; +var particleID = 0; +var mouseX = 0; +var mouseY = 0; + +function makeParticles(data, amount=1, type = "normal") { + for (let x = 0; x < amount; x++) { + let particle = newParticles[type]() + for (thing in data) { + + switch(thing) { + case 'onClick': // Functions that should be copied over + case 'onMouseEnter': + case 'onMouseLeave': + case 'update': + particle[thing] = data[thing] + break; + default: + particle[thing]=run(data[thing], data, x) + + } + } + if (data.dir === undefined) { + particle.dir = particle.angle + } + particle.dir = particle.dir + (particle.spread * (x- amount/2 + 0.5)) + + if(particle.offset) { + particle.x += particle.offset * sin(particle.dir) + particle.y += particle.offset * cos(particle.dir) * -1 + } + + particle.xVel = particle.speed * sin(particle.dir) + particle.yVel = particle.speed * cos(particle.dir) * -1 + particle.fadeInTimer = particle.fadeInTime + Vue.set(particles, particle.id, particle) + + } +} + +// Makes a particle at a random location that stays still until it despawns +function makeShinies(data, amount=1) { + makeParticles(data, amount, "shiny") +} + + +function updateParticles(diff) { + for (p in particles) { + let particle = particles[p] + particle.time -= diff; + particle.fadeInTimer -= diff; + if (particle["time"] < 0) { + Vue.delete(particles, p); + + } + else { + if (particle.update) run(particle.update, particle) + particle.angle += particle.rotation + particle.x += particle.xVel + particle.y += particle.yVel + particle.speed = Math.sqrt(Math.pow(particle.xVel, 2) + Math.pow(particle.yVel, 2)) + particle.dir = atan(-particle.xVel/particle.yVel) + particle.yVel += particle.gravity + } + } +} + +function setDir(particle, dir) { + particle.dir = dir + particle.xVel = particle.speed * sin(particle.dir) + particle.yVel = particle.speed * cos(particle.dir) * -1 +} + +function setSpeed(particle, speed) { + particle.speed = speed + particle.xVel = particle.speed * sin(particle.dir) + particle.yVel = particle.speed * cos(particle.dir) * -1 +} + +const newParticles = { + normal() { + particleID++ + return { + time: 3, + id: particleID, + x: mouseX, + y: mouseY, + width: 35, + height: 35, + image: "resources/genericParticle.png", + angle: 0, + spread: 30, + offset: 10, + speed: 15, + xVel: 0, + yVel: 0, + rotation: 0, + gravity: 0, + fadeOutTime: 1, + fadeInTimer: 0, + fadeInTime: 0, + } + }, + shiny() { + particleID++ + return { + time: 10, + id: particleID, + x: Math.random() * (tmp.other.screenWidth - 100) + 50, + y: Math.random() * (tmp.other.screenHeight - 100) + 50, + width: 50, + height: 50, + image: "resources/genericParticle.png", + angle: 0, + spread: 0, + offset: 0, + speed: 0, + xVel: 0, + yVel: 0, + rotation: 0, + gravity: 0, + fadeOutTime: 1, + fadeInTimer: 0, + fadeInTime: 0.5, + } + }, +} + + + +function updateMouse(event) { + mouseX = event.clientX + mouseY = event.clientY +} + +function getOpacity(particle) { + if ((particle.time < particle.fadeOutTime) && particle.fadeOutTime) + return particle.time / particle.fadeOutTime + if (particle.fadeInTimer > 0) + return 1 - (particle.fadeInTimer / particle.fadeInTime) + + return 1 +} + +function constructParticleStyle(particle){ + let style = { + left: (particle.x - particle.height/2) + 'px', + top: (particle.y - particle.height/2) + 'px', + width: particle.width + 'px', + height: particle.height + 'px', + transform: "rotate(" + particle.angle + "deg)", + opacity: getOpacity(particle), + "pointer-events": (particle.onClick || particle.onHover) ? 'auto' : 'none', + } + if (particle.color) { + style["background-color"] = particle.color + style.mask = "url(#pmask" + particle.id + ")" + style["-webkit-mask-box-image"] = "url(" + particle.image + ")" + } + else + style["background-image"] = "url(" + particle.image + ")" + return style +} + +function clearParticles(check) { + if (!check) check = true + + for (p in particles) { + if (run(check, particles[p], particles[p])){ + Vue.delete(particles, p) + } + } +} + +// Trig with degrees +function sin(x) { return Math.sin(x*Math.PI/180)} + +function cos(x) {return Math.cos(x*Math.PI/180)} + +function tan(x) {return Math.tan(x*Math.PI/180)} + +function asin(x) { return Math.asin(x)*180/Math.PI} + +function acos(x) { return Math.acos(x)*180/Math.PI} + +function atan(x) { return Math.atan(x)*180/Math.PI} diff --git a/js/technical/systemComponents.js b/js/technical/systemComponents.js new file mode 100644 index 0000000..867eebb --- /dev/null +++ b/js/technical/systemComponents.js @@ -0,0 +1,220 @@ +var systemComponents = { + 'tab-buttons': { + props: ['layer', 'data', 'name'], + template: ` +
+
+ +
+
+ ` + }, + + 'tree-node': { + props: ['layer', 'abb', 'size', 'prev'], + template: ` + + ` + }, + + + 'layer-tab': { + props: ['layer', 'back', 'spacing', 'embedded'], + template: `
+
+
+
+ + +
+ +
+ + +
+ +
+ + + + + +

+
+
+
+ +
+
+
+ +
+ + +
+
+ ` + }, + + 'overlay-head': { + template: ` +
+ +
Dev Speed: {{format(player.devSpeed)}}x
+
+ +
Offline Time: {{formatTime(player.offTime.remain)}}
+
+
+ You have +

{{format(player.points)}}

+ {{modInfo.pointsName}} +
+ ({{tmp.other.oompsMag != 0 ? format(tmp.other.oomps) + " OOM" + (tmp.other.oompsMag < 0 ? "^OOM" : tmp.other.oompsMag > 1 ? "^" + tmp.other.oompsMag : "") + "s" : formatSmall(getPointGen())}}/sec) +
+
+ ` + }, + + 'info-tab': { + template: ` +
+

{{modInfo.name}}

+
+

{{VERSION.withName}}

+ +
+ Made by {{modInfo.author}} +
+
+ The Modding Tree {{TMT_VERSION.tmtNum}} by Acamaeda +
+ The Prestige Tree made by Jacorb and Aarex +

+
+ {{modInfo.discordName}}
+ The Modding Tree Discord
+ Main Prestige Tree server
+

+ Time Played: {{ formatTime(player.timePlayed) }}

+

Hotkeys


+
{{key.description}}
+ ` + }, + + 'options-tab': { + template: ` + + + + + + + + + + + + + + + + + + + + + +
` + }, + + 'back-button': { + template: ` + + ` + }, + + + 'tooltip' : { + props: ['text'], + template: `
+ ` + }, + + 'node-mark': { + props: {'layer': {}, data: {}, offset: {default: 0}, scale: {default: 1}}, + template: `
+
+
+ + ` + }, + + 'particle': { + props: ['data', 'index'], + template: `
+
+ + + + + +
+ ` + }, + + 'bg': { + props: ['layer'], + template: `
+ ` + } + +} + diff --git a/js/technical/temp.js b/js/technical/temp.js new file mode 100644 index 0000000..eab66e2 --- /dev/null +++ b/js/technical/temp.js @@ -0,0 +1,179 @@ +var tmp = {} +var temp = tmp // Proxy for tmp +var funcs = {} +var NaNalert = false; + +// Tmp will not call these +var activeFunctions = [ + "startData", "onPrestige", "doReset", "update", "automate", + "buy", "buyMax", "respec", "onPress", "onClick", "onHold", "masterButtonPress", + "sellOne", "sellAll", "pay", "actualCostFunction", "actualEffectFunction", + "effectDescription", "display", "fullDisplay", "effectDisplay", "rewardDisplay", + "tabFormat", "content", + "onComplete", "onPurchase", "onEnter", "onExit", "done", + "getUnlocked", "getStyle", "getCanClick", "getTitle", "getDisplay" +] + +var noCall = doNotCallTheseFunctionsEveryTick +for (item in noCall) { + activeFunctions.push(noCall[item]) +} + +// Add the names of classes to traverse +var traversableClasses = [] + +function setupTemp() { + tmp = {} + tmp.pointGen = {} + tmp.backgroundStyle = {} + tmp.displayThings = [] + tmp.scrolled = 0 + tmp.gameEnded = false + funcs = {} + + setupTempData(layers, tmp, funcs) + for (layer in layers){ + tmp[layer].resetGain = {} + tmp[layer].nextAt = {} + tmp[layer].nextAtDisp = {} + tmp[layer].canReset = {} + tmp[layer].notify = {} + tmp[layer].prestigeNotify = {} + tmp[layer].computedNodeStyle = [] + setupBuyables(layer) + tmp[layer].trueGlowColor = [] + } + + tmp.other = { + lastPoints: player.points || decimalZero, + oomps: decimalZero, + screenWidth: 0, + screenHeight: 0, + } + + updateWidth() + + temp = tmp +} + +const boolNames = ["unlocked", "deactivated"] + +function setupTempData(layerData, tmpData, funcsData) { + for (item in layerData){ + if (layerData[item] == null) { + tmpData[item] = null + } + else if (layerData[item] instanceof Decimal) + tmpData[item] = layerData[item] + else if (Array.isArray(layerData[item])) { + tmpData[item] = [] + funcsData[item] = [] + setupTempData(layerData[item], tmpData[item], funcsData[item]) + } + else if ((!!layerData[item]) && (layerData[item].constructor === Object)) { + tmpData[item] = {} + funcsData[item] = [] + setupTempData(layerData[item], tmpData[item], funcsData[item]) + } + else if ((!!layerData[item]) && (typeof layerData[item] === "object") && traversableClasses.includes(layerData[item].constructor.name)) { + tmpData[item] = new layerData[item].constructor() + funcsData[item] = new layerData[item].constructor() + } + else if (isFunction(layerData[item]) && !activeFunctions.includes(item)){ + funcsData[item] = layerData[item] + if (boolNames.includes(item)) + tmpData[item] = false + else + tmpData[item] = decimalOne // The safest thing to put probably? + } else { + tmpData[item] = layerData[item] + } + } +} + + +function updateTemp() { + if (tmp === undefined) + setupTemp() + + updateTempData(layers, tmp, funcs) + + for (layer in layers){ + tmp[layer].resetGain = getResetGain(layer) + tmp[layer].nextAt = getNextAt(layer) + tmp[layer].nextAtDisp = getNextAt(layer, true) + tmp[layer].canReset = canReset(layer) + tmp[layer].trueGlowColor = tmp[layer].glowColor + tmp[layer].notify = shouldNotify(layer) + tmp[layer].prestigeNotify = prestigeNotify(layer) + if (tmp[layer].passiveGeneration === true) tmp[layer].passiveGeneration = 1 // new Decimal(true) = decimalZero + + } + + tmp.pointGen = getPointGen() + tmp.backgroundStyle = readData(backgroundStyle) + + tmp.displayThings = [] + for (thing in displayThings){ + let text = displayThings[thing] + if (isFunction(text)) text = text() + tmp.displayThings.push(text) + } +} + +function updateTempData(layerData, tmpData, funcsData, useThis) { + for (item in funcsData){ + if (Array.isArray(layerData[item])) { + if (item !== "tabFormat" && item !== "content") // These are only updated when needed + updateTempData(layerData[item], tmpData[item], funcsData[item], useThis) + } + else if ((!!layerData[item]) && (layerData[item].constructor === Object) || (typeof layerData[item] === "object") && traversableClasses.includes(layerData[item].constructor.name)){ + updateTempData(layerData[item], tmpData[item], funcsData[item], useThis) + } + else if (isFunction(layerData[item]) && !isFunction(tmpData[item])){ + let value + + if (useThis !== undefined) value = layerData[item].bind(useThis)() + else value = layerData[item]() + Vue.set(tmpData, item, value) + } + } +} + +function updateChallengeTemp(layer) +{ + updateTempData(layers[layer].challenges, tmp[layer].challenges, funcs[layer].challenges) +} + + +function updateBuyableTemp(layer) +{ + updateTempData(layers[layer].buyables, tmp[layer].buyables, funcs[layer].buyables) +} + +function updateClickableTemp(layer) +{ + updateTempData(layers[layer].clickables, tmp[layer].clickables, funcs[layer].clickables) +} + +function setupBuyables(layer) { + for (id in layers[layer].buyables) { + if (isPlainObject(layers[layer].buyables[id])) { + let b = layers[layer].buyables[id] + b.actualCostFunction = b.cost + b.cost = function(x) { + x = (x === undefined ? player[this.layer].buyables[this.id] : x) + return layers[this.layer].buyables[this.id].actualCostFunction(x) + } + b.actualEffectFunction = b.effect + b.effect = function(x) { + x = (x === undefined ? player[this.layer].buyables[this.id] : x) + return layers[this.layer].buyables[this.id].actualEffectFunction(x) + } + } + } +} + +function checkDecimalNaN(x) { + return (x instanceof Decimal) && !x.eq(x) +} \ No newline at end of file diff --git a/js/tree.js b/js/tree.js new file mode 100644 index 0000000..4b24f8c --- /dev/null +++ b/js/tree.js @@ -0,0 +1,23 @@ +var layoutInfo = { + startTab: "none", + startNavTab: "tree-tab", + showTree: true, + + treeLayout: "" + + +} + + +// A "ghost" layer which offsets other layers in the tree +addNode("blank", { + layerShown: "ghost", +}, +) + + +addLayer("tree-tab", { + tabFormat: [["tree", function() {return (layoutInfo.treeLayout ? layoutInfo.treeLayout : TREE_LAYERS)}]], + previousTab: "", + leftTab: true, +}) \ No newline at end of file diff --git a/js/utils.js b/js/utils.js new file mode 100644 index 0000000..f56ca9c --- /dev/null +++ b/js/utils.js @@ -0,0 +1,412 @@ +// ************ Big Feature related ************ + +function respecBuyables(layer) { + if (!layers[layer].buyables) return + if (!layers[layer].buyables.respec) return + if (!player[layer].noRespecConfirm && !confirm(tmp[layer].buyables.respecMessage || "Are you sure you want to respec? This will force you to do a \"" + (tmp[layer].name ? tmp[layer].name : layer) + "\" reset as well!")) return + run(layers[layer].buyables.respec, layers[layer].buyables) + updateBuyableTemp(layer) + document.activeElement.blur() +} + +function canAffordUpgrade(layer, id) { + let upg = tmp[layer].upgrades[id] + if(tmp[layer].deactivated) return false + if (tmp[layer].upgrades[id].canAfford === false) return false + let cost = tmp[layer].upgrades[id].cost + if (cost !== undefined) + return canAffordPurchase(layer, upg, cost) + + return true +} + +function canBuyBuyable(layer, id) { + let b = temp[layer].buyables[id] + return (b.unlocked && run(b.canAfford, b) && player[layer].buyables[id].lt(b.purchaseLimit) && !tmp[layer].deactivated) +} + + + +function canAffordPurchase(layer, thing, cost) { + if (thing.currencyInternalName) { + let name = thing.currencyInternalName + if (thing.currencyLocation) { + return !(thing.currencyLocation[name].lt(cost)) + } + else if (thing.currencyLayer) { + let lr = thing.currencyLayer + return !(player[lr][name].lt(cost)) + } + else { + return !(player[name].lt(cost)) + } + } + else { + return !(player[layer].points.lt(cost)) + } +} + +function buyUpgrade(layer, id) { + buyUpg(layer, id) +} + +function buyUpg(layer, id) { + if (!tmp[layer].upgrades || !tmp[layer].upgrades[id]) return + let upg = tmp[layer].upgrades[id] + if (!player[layer].unlocked || player[layer].deactivated) return + if (!tmp[layer].upgrades[id].unlocked) return + if (player[layer].upgrades.includes(id)) return + if (upg.canAfford === false) return + let pay = layers[layer].upgrades[id].pay + if (pay !== undefined) + run(pay, layers[layer].upgrades[id]) + else { + let cost = tmp[layer].upgrades[id].cost + + if (upg.currencyInternalName) { + let name = upg.currencyInternalName + if (upg.currencyLocation) { + if (upg.currencyLocation[name].lt(cost)) return + upg.currencyLocation[name] = upg.currencyLocation[name].sub(cost) + } + else if (upg.currencyLayer) { + let lr = upg.currencyLayer + if (player[lr][name].lt(cost)) return + player[lr][name] = player[lr][name].sub(cost) + } + else { + if (player[name].lt(cost)) return + player[name] = player[name].sub(cost) + } + } + else { + if (player[layer].points.lt(cost)) return + player[layer].points = player[layer].points.sub(cost) + } + } + player[layer].upgrades.push(id); + if (upg.onPurchase != undefined) + run(upg.onPurchase, upg) + needCanvasUpdate = true +} + +function buyMaxBuyable(layer, id) { + if (!player[layer].unlocked) return + if (!tmp[layer].buyables[id].unlocked) return + if (!tmp[layer].buyables[id].canBuy) return + if (!layers[layer].buyables[id].buyMax) return + + run(layers[layer].buyables[id].buyMax, layers[layer].buyables[id]) + updateBuyableTemp(layer) +} + +function buyBuyable(layer, id) { + if (!player[layer].unlocked) return + if (!tmp[layer].buyables[id].unlocked) return + if (!tmp[layer].buyables[id].canBuy) return + + run(layers[layer].buyables[id].buy, layers[layer].buyables[id]) + updateBuyableTemp(layer) +} + +function clickClickable(layer, id) { + if (!player[layer].unlocked || tmp[layer].deactivated) return + if (!tmp[layer].clickables[id].unlocked) return + if (!tmp[layer].clickables[id].canClick) return + + run(layers[layer].clickables[id].onClick, layers[layer].clickables[id]) + updateClickableTemp(layer) +} + +function clickGrid(layer, id) { + if (!player[layer].unlocked || tmp[layer].deactivated) return + if (!run(layers[layer].grid.getUnlocked, layers[layer].grid, id)) return + if (!gridRun(layer, 'getCanClick', player[layer].grid[id], id)) return + + gridRun(layer, 'onClick', player[layer].grid[id], id) +} + +// Function to determine if the player is in a challenge +function inChallenge(layer, id) { + let challenge = player[layer].activeChallenge + if (!challenge) return false + id = toNumber(id) + if (challenge == id) return true + + if (layers[layer].challenges[challenge].countsAs) + return tmp[layer].challenges[challenge].countsAs.includes(id) || false + return false +} + +// ************ Misc ************ + +var onTreeTab = true + +function showTab(name, prev) { + if (LAYERS.includes(name) && !layerunlocked(name)) return + if (player.tab !== name) clearParticles(function(p) {return p.layer === player.tab}) + if (tmp[name] && player.tab === name && isPlainObject(tmp[name].tabFormat)) { + player.subtabs[name].mainTabs = Object.keys(layers[name].tabFormat)[0] + } + var toTreeTab = name == "none" + player.tab = name + if (tmp[name] && (tmp[name].row !== "side") && (tmp[name].row !== "otherside")) player.lastSafeTab = name + updateTabFormats() + needCanvasUpdate = true + document.activeElement.blur() + +} + +function showNavTab(name, prev) { + console.log(prev) + if (LAYERS.includes(name) && !layerunlocked(name)) return + if (player.navTab !== name) clearParticles(function(p) {return p.layer === player.navTab}) + if (tmp[name] && tmp[name].previousTab !== undefined) prev = tmp[name].previousTab + var toTreeTab = name == "tree-tab" + console.log(name + prev) + if (name!== "none" && prev && !tmp[prev]?.leftTab == !tmp[name]?.leftTab) player[name].prevTab = prev + else if (player[name]) + player[name].prevTab = "" + player.navTab = name + updateTabFormats() + needCanvasUpdate = true +} + + +function goBack(layer) { + let nextTab = "none" + + if (player[layer].prevTab) nextTab = player[layer].prevTab + if (player.navTab === "none" && (tmp[layer]?.row == "side" || tmp[layer].row == "otherside")) nextTab = player.lastSafeTab + + if (tmp[layer].leftTab) showNavTab(nextTab, layer) + else showTab(nextTab, layer) + +} + +function layOver(obj1, obj2) { + for (let x in obj2) { + if (obj2[x] instanceof Decimal) obj1[x] = new Decimal(obj2[x]) + else if (obj2[x] instanceof Object) layOver(obj1[x], obj2[x]); + else obj1[x] = obj2[x]; + } +} + +function prestigeNotify(layer) { + if (layers[layer].prestigeNotify) return layers[layer].prestigeNotify() + + if (isPlainObject(tmp[layer].tabFormat)) { + for (subtab in tmp[layer].tabFormat){ + if (subtabResetNotify(layer, 'mainTabs', subtab)) + return true + } + } + for (family in tmp[layer].microtabs) { + for (subtab in tmp[layer].microtabs[family]){ + if (subtabResetNotify(layer, family, subtab)) + return true + } + } + if (tmp[layer].autoPrestige || tmp[layer].passiveGeneration) return false + else if (tmp[layer].type == "static") return tmp[layer].canReset + else if (tmp[layer].type == "normal") return (tmp[layer].canReset && (tmp[layer].resetGain.gte(player[layer].points.div(10)))) + else return false +} + +function notifyLayer(name) { + if (player.tab == name || !layerunlocked(name)) return + player.notify[name] = 1 +} + +function subtabShouldNotify(layer, family, id) { + let subtab = {} + if (family == "mainTabs") subtab = tmp[layer].tabFormat[id] + else subtab = tmp[layer].microtabs[family][id] + if (!subtab.unlocked) return false + if (subtab.embedLayer) return tmp[subtab.embedLayer].notify + else return subtab.shouldNotify +} + +function subtabResetNotify(layer, family, id) { + let subtab = {} + if (family == "mainTabs") subtab = tmp[layer].tabFormat[id] + else subtab = tmp[layer].microtabs[family][id] + if (subtab.embedLayer) return tmp[subtab.embedLayer].prestigeNotify + else return subtab.prestigeNotify +} + +function nodeShown(layer) { + return layerShown(layer) +} + +function layerunlocked(layer) { + if (tmp[layer] && tmp[layer].type == "none") return (player[layer].unlocked) + return LAYERS.includes(layer) && (player[layer].unlocked || (tmp[layer].canReset && tmp[layer].layerShown)) +} + +function keepGoing() { + player.keepGoing = true; + needCanvasUpdate = true; +} + +function toNumber(x) { + if (x.mag !== undefined) return x.toNumber() + if (x + 0 !== x) return parseFloat(x) + return x +} + +function updateMilestones(layer) { + if (tmp[layer].deactivated) return + for (id in layers[layer].milestones) { + if (!(hasMilestone(layer, id)) && layers[layer].milestones[id].done()) { + player[layer].milestones.push(id) + if (layers[layer].milestones[id].onComplete) layers[layer].milestones[id].onComplete() + if (tmp[layer].milestonePopups || tmp[layer].milestonePopups === undefined) doPopup("milestone", tmp[layer].milestones[id].requirementDescription, "Milestone Gotten!", 3, tmp[layer].color); + player[layer].lastMilestone = id + } + } +} + +function updateAchievements(layer) { + if (tmp[layer].deactivated) return + for (id in layers[layer].achievements) { + if (isPlainObject(layers[layer].achievements[id]) && !(hasAchievement(layer, id)) && layers[layer].achievements[id].done()) { + player[layer].achievements.push(id) + if (layers[layer].achievements[id].onComplete) layers[layer].achievements[id].onComplete() + if (tmp[layer].achievementPopups || tmp[layer].achievementPopups === undefined) doPopup("achievement", tmp[layer].achievements[id].name, "Achievement Gotten!", 3, tmp[layer].color); + } + } +} + +function addTime(diff, layer) { + let data = player + let time = data.timePlayed + if (layer) { + data = data[layer] + time = data.time + } + + //I am not that good to perfectly fix that leak. ~ DB Aarex + if (time + 0 !== time) { + console.log("Memory leak detected. Trying to fix...") + time = toNumber(time) + if (isNaN(time) || time == 0) { + console.log("Couldn't fix! Resetting...") + time = layer ? player.timePlayed : 0 + if (!layer) player.timePlayedReset = true + } + } + time += toNumber(diff) + + if (layer) data.time = time + else data.timePlayed = time +} + +shiftDown = false +ctrlDown = false + +document.onkeydown = function (e) { + if (player === undefined) return; + shiftDown = e.shiftKey + ctrlDown = e.ctrlKey + if (tmp.gameEnded && !player.keepGoing) return; + let key = e.key + if (ctrlDown) key = "ctrl+" + key + if (onFocused) return + if (ctrlDown && hotkeys[key]) e.preventDefault() + if (hotkeys[key]) { + let k = hotkeys[key] + if (player[k.layer].unlocked && tmp[k.layer].hotkeys[k.id].unlocked) + k.onPress() + } +} + +document.onkeyup = function (e) { + shiftDown = e.shiftKey + ctrlDown = e.ctrlKey +} + +var onFocused = false +function focused(x) { + onFocused = x +} + + +function isFunction(obj) { + return !!(obj && obj.constructor && obj.call && obj.apply); +}; + +function isPlainObject(obj) { + return (!!obj) && (obj.constructor === Object) +} + +document.title = modInfo.name + +// Converts a string value to whatever it's supposed to be +function toValue(value, oldValue) { + if (oldValue instanceof Decimal) { + value = new Decimal (value) + if (checkDecimalNaN(value)) return decimalZero + return value + } + if (!isNaN(oldValue)) + return parseFloat(value) || 0 + return value +} + +// Variables that must be defined to display popups +var activePopups = []; +var popupID = 0; + +// Function to show popups +function doPopup(type = "none", text = "This is a test popup.", title = "", timer = 3, color = "") { + switch (type) { + case "achievement": + popupTitle = "Achievement Unlocked!"; + popupType = "achievement-popup" + break; + case "challenge": + popupTitle = "Challenge Complete"; + popupType = "challenge-popup" + break; + default: + popupTitle = "Something Happened?"; + popupType = "default-popup" + break; + } + if (title != "") popupTitle = title; + popupMessage = text; + popupTimer = timer; + + activePopups.push({ "time": popupTimer, "type": popupType, "title": popupTitle, "message": (popupMessage + "\n"), "id": popupID, "color": color }) + popupID++; +} + + +//Function to reduce time on active popups +function adjustPopupTime(diff) { + for (popup in activePopups) { + activePopups[popup].time -= diff; + if (activePopups[popup]["time"] < 0) { + activePopups.splice(popup, 1); // Remove popup when time hits 0 + } + } +} + +function run(func, target, args = null) { + if (isFunction(func)) { + let bound = func.bind(target) + return bound(args) + } + else + return func; +} + +function gridRun(layer, func, data, id) { + if (isFunction(layers[layer].grid[func])) { + let bound = layers[layer].grid[func].bind(layers[layer].grid) + return bound(data, id) + } + else + return layers[layer].grid[func]; +} \ No newline at end of file diff --git a/js/utils/NumberFormating.js b/js/utils/NumberFormating.js new file mode 100644 index 0000000..06b158b --- /dev/null +++ b/js/utils/NumberFormating.js @@ -0,0 +1,111 @@ + +function exponentialFormat(num, precision, mantissa = true) { + let e = num.log10().floor() + let m = num.div(Decimal.pow(10, e)) + if (m.toStringWithDecimalPlaces(precision) == 10) { + m = decimalOne + e = e.add(1) + } + e = (e.gte(1e9) ? format(e, 3) : (e.gte(10000) ? commaFormat(e, 0) : e.toStringWithDecimalPlaces(0))) + if (mantissa) + return m.toStringWithDecimalPlaces(precision) + "e" + e + else return "e" + e +} + +function commaFormat(num, precision) { + if (num === null || num === undefined) return "NaN" + if (num.mag < 0.001) return (0).toFixed(precision) + let init = num.toStringWithDecimalPlaces(precision) + let portions = init.split(".") + portions[0] = portions[0].replace(/(\d)(?=(\d\d\d)+(?!\d))/g, "$1,") + if (portions.length == 1) return portions[0] + return portions[0] + "." + portions[1] +} + + +function regularFormat(num, precision) { + if (num === null || num === undefined) return "NaN" + if (num.mag < 0.0001) return (0).toFixed(precision) + if (num.mag < 0.1 && precision !==0) precision = Math.max(precision, 4) + return num.toStringWithDecimalPlaces(precision) +} + +function fixValue(x, y = 0) { + return x || new Decimal(y) +} + +function sumValues(x) { + x = Object.values(x) + if (!x[0]) return decimalZero + return x.reduce((a, b) => Decimal.add(a, b)) +} + +function format(decimal, precision = 2, small) { + small = small || modInfo.allowSmall + decimal = new Decimal(decimal) + if (isNaN(decimal.sign) || isNaN(decimal.layer) || isNaN(decimal.mag)) { + player.hasNaN = true; + return "NaN" + } + if (decimal.sign < 0) return "-" + format(decimal.neg(), precision, small) + if (decimal.mag == Number.POSITIVE_INFINITY) return "Infinity" + if (decimal.gte("eeee1000")) { + var slog = decimal.slog() + if (slog.gte(1e6)) return "F" + format(slog.floor()) + else return Decimal.pow(10, slog.sub(slog.floor())).toStringWithDecimalPlaces(3) + "F" + commaFormat(slog.floor(), 0) + } + else if (decimal.gte("1e1000000")) return exponentialFormat(decimal, 0, false) + else if (decimal.gte("1e10000")) return exponentialFormat(decimal, 0) + else if (decimal.gte(1e9)) return exponentialFormat(decimal, precision) + else if (decimal.gte(1e3)) return commaFormat(decimal, 0) + else if (decimal.gte(0.0001) || !small) return regularFormat(decimal, precision) + else if (decimal.eq(0)) return (0).toFixed(precision) + + decimal = invertOOM(decimal) + let val = "" + if (decimal.lt("1e1000")){ + val = exponentialFormat(decimal, precision) + return val.replace(/([^(?:e|F)]*)$/, '-$1') + } + else + return format(decimal, precision) + "⁻¹" + +} + +function formatWhole(decimal) { + decimal = new Decimal(decimal) + if (decimal.gte(1e9)) return format(decimal, 2) + if (decimal.lte(0.99) && !decimal.eq(0)) return format(decimal, 2) + return format(decimal, 0) +} + +function formatTime(s) { + if (s < 60) return format(s) + "s" + else if (s < 3600) return formatWhole(Math.floor(s / 60)) + "m " + format(s % 60) + "s" + else if (s < 86400) return formatWhole(Math.floor(s / 3600)) + "h " + formatWhole(Math.floor(s / 60) % 60) + "m " + format(s % 60) + "s" + else if (s < 31536000) return formatWhole(Math.floor(s / 86400) % 365) + "d " + formatWhole(Math.floor(s / 3600) % 24) + "h " + formatWhole(Math.floor(s / 60) % 60) + "m " + format(s % 60) + "s" + else return formatWhole(Math.floor(s / 31536000)) + "y " + formatWhole(Math.floor(s / 86400) % 365) + "d " + formatWhole(Math.floor(s / 3600) % 24) + "h " + formatWhole(Math.floor(s / 60) % 60) + "m " + format(s % 60) + "s" +} + +function toPlaces(x, precision, maxAccepted) { + x = new Decimal(x) + let result = x.toStringWithDecimalPlaces(precision) + if (new Decimal(result).gte(maxAccepted)) { + result = new Decimal(maxAccepted - Math.pow(0.1, precision)).toStringWithDecimalPlaces(precision) + } + return result +} + +// Will also display very small numbers +function formatSmall(x, precision=2) { + return format(x, precision, true) +} + +function invertOOM(x){ + let e = x.log10().ceil() + let m = x.div(Decimal.pow(10, e)) + e = e.neg() + x = new Decimal(10).pow(e).times(m) + + return x +} \ No newline at end of file diff --git a/js/utils/easyAccess.js b/js/utils/easyAccess.js new file mode 100644 index 0000000..b6c4afc --- /dev/null +++ b/js/utils/easyAccess.js @@ -0,0 +1,75 @@ +function hasUpgrade(layer, id) { + return ((player[layer].upgrades.includes(toNumber(id)) || player[layer].upgrades.includes(id.toString())) && !tmp[layer].deactivated) +} + +function hasMilestone(layer, id) { + return ((player[layer].milestones.includes(toNumber(id)) || player[layer].milestones.includes(id.toString())) && !tmp[layer].deactivated) +} + +function hasAchievement(layer, id) { + return ((player[layer].achievements.includes(toNumber(id)) || player[layer].achievements.includes(id.toString())) && !tmp[layer].deactivated) +} + +function hasChallenge(layer, id) { + return ((player[layer].challenges[id]) && !tmp[layer].deactivated) +} + +function maxedChallenge(layer, id) { + return ((player[layer].challenges[id] >= tmp[layer].challenges[id].completionLimit) && !tmp[layer].deactivated) +} + +function challengeCompletions(layer, id) { + return (player[layer].challenges[id]) +} + +function getBuyableAmount(layer, id) { + return (player[layer].buyables[id]) +} + +function setBuyableAmount(layer, id, amt) { + player[layer].buyables[id] = amt +} + +function addBuyables(layer, id, amt) { + player[layer].buyables[id] = player[layer].buyables[id].add(amt) +} + +function getClickableState(layer, id) { + return (player[layer].clickables[id]) +} + +function setClickableState(layer, id, state) { + player[layer].clickables[id] = state +} + +function getGridData(layer, id) { + return (player[layer].grid[id]) +} + +function setGridData(layer, id, data) { + player[layer].grid[id] = data +} + +function upgradeEffect(layer, id) { + return (tmp[layer].upgrades[id].effect) +} + +function challengeEffect(layer, id) { + return (tmp[layer].challenges[id].rewardEffect) +} + +function buyableEffect(layer, id) { + return (tmp[layer].buyables[id].effect) +} + +function clickableEffect(layer, id) { + return (tmp[layer].clickables[id].effect) +} + +function achievementEffect(layer, id) { + return (tmp[layer].achievements[id].effect) +} + +function gridEffect(layer, id) { + return (gridRun(layer, 'getEffect', player[layer].grid[id], id)) +} \ No newline at end of file diff --git a/js/utils/options.js b/js/utils/options.js new file mode 100644 index 0000000..fa575a9 --- /dev/null +++ b/js/utils/options.js @@ -0,0 +1,78 @@ +// ************ Options ************ + +let options = {} + +function getStartOptions() { + return { + autosave: true, + msDisplay: "always", + theme: "default", + hqTree: false, + offlineProd: true, + hideChallenges: false, + showStory: true, + forceOneTab: false, + oldStyle: false, + tooltipForcing: true, + } +} + +function toggleOpt(name) { + if (name == "oldStyle" && styleCooldown > 0) + return; + + options[name] = !options[name]; + if (name == "hqTree") + changeTreeQuality(); + if (name == "oldStyle") + updateStyle(); +} +var styleCooldown = 0; +function updateStyle() { + styleCooldown = 1; + let css = document.getElementById("styleStuff"); + css.href = options.oldStyle ? "oldStyle.css" : "style.css"; + needCanvasUpdate = true; +} +function changeTreeQuality() { + var on = options.hqTree; + document.body.style.setProperty('--hqProperty1', on ? "2px solid" : "4px solid"); + document.body.style.setProperty('--hqProperty2a', on ? "-4px -4px 4px rgba(0, 0, 0, 0.25) inset" : "-4px -4px 4px rgba(0, 0, 0, 0) inset"); + document.body.style.setProperty('--hqProperty2b', on ? "0px 0px 20px var(--background)" : ""); + document.body.style.setProperty('--hqProperty3', on ? "2px 2px 4px rgba(0, 0, 0, 0.25)" : "none"); +} +function toggleAuto(toggle) { + Vue.set(player[toggle[0]], [toggle[1]], !player[toggle[0]][toggle[1]]); + needCanvasUpdate=true +} + +const MS_DISPLAYS = ["ALL", "LAST, AUTO, INCOMPLETE", "AUTOMATION, INCOMPLETE", "INCOMPLETE", "NONE"]; + +const MS_SETTINGS = ["always", "last", "automation", "incomplete", "never"]; + +function adjustMSDisp() { + options.msDisplay = MS_SETTINGS[(MS_SETTINGS.indexOf(options.msDisplay) + 1) % 5]; +} +function milestoneShown(layer, id) { + complete = player[layer].milestones.includes(id); + auto = layers[layer].milestones[id].toggles; + + switch (options.msDisplay) { + case "always": + return true; + break; + case "last": + return (auto) || !complete || player[layer].lastMilestone === id; + break; + case "automation": + return (auto) || !complete; + break; + case "incomplete": + return !complete; + break; + case "never": + return false; + break; + } + return false; +} diff --git a/js/utils/save.js b/js/utils/save.js new file mode 100644 index 0000000..b362c3b --- /dev/null +++ b/js/utils/save.js @@ -0,0 +1,322 @@ +// ************ Save stuff ************ +function save(force) { + NaNcheck(player) + if (NaNalert && !force) return + localStorage.setItem(modInfo.id, btoa(unescape(encodeURIComponent(JSON.stringify(player))))); + localStorage.setItem(modInfo.id+"_options", btoa(unescape(encodeURIComponent(JSON.stringify(options))))); + +} +function startPlayerBase() { + return { + tab: layoutInfo.startTab, + navTab: (layoutInfo.showTree ? layoutInfo.startNavTab : "none"), + time: Date.now(), + notify: {}, + versionType: modInfo.id, + version: VERSION.num, + beta: VERSION.beta, + timePlayed: 0, + keepGoing: false, + hasNaN: false, + + points: modInfo.initialStartPoints, + subtabs: {}, + lastSafeTab: (readData(layoutInfo.showTree) ? "none" : layoutInfo.startTab) + }; +} +function getStartPlayer() { + playerdata = startPlayerBase(); + + if (addedPlayerData) { + extradata = addedPlayerData(); + for (thing in extradata) + playerdata[thing] = extradata[thing]; + } + + playerdata.infoboxes = {}; + for (layer in layers) { + playerdata[layer] = getStartLayerData(layer); + + if (layers[layer].tabFormat && !Array.isArray(layers[layer].tabFormat)) { + playerdata.subtabs[layer] = {}; + playerdata.subtabs[layer].mainTabs = Object.keys(layers[layer].tabFormat)[0]; + } + if (layers[layer].microtabs) { + if (playerdata.subtabs[layer] == undefined) + playerdata.subtabs[layer] = {}; + for (item in layers[layer].microtabs) + playerdata.subtabs[layer][item] = Object.keys(layers[layer].microtabs[item])[0]; + } + if (layers[layer].infoboxes) { + if (playerdata.infoboxes[layer] == undefined) + playerdata.infoboxes[layer] = {}; + for (item in layers[layer].infoboxes) + playerdata.infoboxes[layer][item] = false; + } + + } + return playerdata; +} +function getStartLayerData(layer) { + layerdata = {}; + if (layers[layer].startData) + layerdata = layers[layer].startData(); + + if (layerdata.unlocked === undefined) + layerdata.unlocked = true; + if (layerdata.total === undefined) + layerdata.total = decimalZero; + if (layerdata.best === undefined) + layerdata.best = decimalZero; + if (layerdata.resetTime === undefined) + layerdata.resetTime = 0; + if (layerdata.forceTooltip === undefined) + layerdata.forceTooltip = false; + + layerdata.buyables = getStartBuyables(layer); + if (layerdata.noRespecConfirm === undefined) layerdata.noRespecConfirm = false + if (layerdata.clickables == undefined) + layerdata.clickables = getStartClickables(layer); + layerdata.spentOnBuyables = decimalZero; + layerdata.upgrades = []; + layerdata.milestones = []; + layerdata.lastMilestone = null; + layerdata.achievements = []; + layerdata.challenges = getStartChallenges(layer); + layerdata.grid = getStartGrid(layer); + layerdata.prevTab = "" + + return layerdata; +} +function getStartBuyables(layer) { + let data = {}; + if (layers[layer].buyables) { + for (id in layers[layer].buyables) + if (isPlainObject(layers[layer].buyables[id])) + data[id] = decimalZero; + } + return data; +} +function getStartClickables(layer) { + let data = {}; + if (layers[layer].clickables) { + for (id in layers[layer].clickables) + if (isPlainObject(layers[layer].clickables[id])) + data[id] = ""; + } + return data; +} +function getStartChallenges(layer) { + let data = {}; + if (layers[layer].challenges) { + for (id in layers[layer].challenges) + if (isPlainObject(layers[layer].challenges[id])) + data[id] = 0; + } + return data; +} +function getStartGrid(layer) { + let data = {}; + if (! layers[layer].grid) return data + if (layers[layer].grid.maxRows === undefined) layers[layer].grid.maxRows=layers[layer].grid.rows + if (layers[layer].grid.maxCols === undefined) layers[layer].grid.maxCols=layers[layer].grid.cols + + for (let y = 1; y <= layers[layer].grid.maxRows; y++) { + for (let x = 1; x <= layers[layer].grid.maxCols; x++) { + data[100*y + x] = layers[layer].grid.getStartData(100*y + x) + } + } + return data; +} + +function fixSave() { + defaultData = getStartPlayer(); + fixData(defaultData, player); + + for (layer in layers) { + if (player[layer].best !== undefined) + player[layer].best = new Decimal(player[layer].best); + if (player[layer].total !== undefined) + player[layer].total = new Decimal(player[layer].total); + + if (layers[layer].tabFormat && !Array.isArray(layers[layer].tabFormat)) { + + if (!Object.keys(layers[layer].tabFormat).includes(player.subtabs[layer].mainTabs)) + player.subtabs[layer].mainTabs = Object.keys(layers[layer].tabFormat)[0]; + } + if (layers[layer].microtabs) { + for (item in layers[layer].microtabs) + if (!Object.keys(layers[layer].microtabs[item]).includes(player.subtabs[layer][item])) + player.subtabs[layer][item] = Object.keys(layers[layer].microtabs[item])[0]; + } + } +} +function fixData(defaultData, newData) { + for (item in defaultData) { + if (defaultData[item] == null) { + if (newData[item] === undefined) + newData[item] = null; + } + else if (Array.isArray(defaultData[item])) { + if (newData[item] === undefined) + newData[item] = defaultData[item]; + + else + fixData(defaultData[item], newData[item]); + } + else if (defaultData[item] instanceof Decimal) { // Convert to Decimal + if (newData[item] === undefined) + newData[item] = defaultData[item]; + + else + newData[item] = new Decimal(newData[item]); + } + else if ((!!defaultData[item]) && (typeof defaultData[item] === "object")) { + if (newData[item] === undefined || (typeof defaultData[item] !== "object")) + newData[item] = defaultData[item]; + + else + fixData(defaultData[item], newData[item]); + } + else { + if (newData[item] === undefined) + newData[item] = defaultData[item]; + } + } +} +function load() { + let get = localStorage.getItem(modInfo.id); + + if (get === null || get === undefined) { + player = getStartPlayer(); + options = getStartOptions(); + } + else { + player = Object.assign(getStartPlayer(), JSON.parse(decodeURIComponent(escape(atob(get))))); + fixSave(); + loadOptions(); + } + + if (options.offlineProd) { + if (player.offTime === undefined) + player.offTime = { remain: 0 }; + player.offTime.remain += (Date.now() - player.time) / 1000; + } + player.time = Date.now(); + versionCheck(); + changeTheme(); + changeTreeQuality(); + updateLayers(); + setupModInfo(); + + setupTemp(); + updateTemp(); + updateTemp(); + updateTabFormats() + loadVue(); +} + +function loadOptions() { + let get2 = localStorage.getItem(modInfo.id+"_options"); + if (get2) + options = Object.assign(getStartOptions(), JSON.parse(decodeURIComponent(escape(atob(get2))))); + else + options = getStartOptions() + if (themes.indexOf(options.theme) < 0) theme = "default" + fixData(options, getStartOptions()) + +} + +function setupModInfo() { + modInfo.changelog = changelog; + modInfo.winText = winText ? winText : `Congratulations! You have reached the end and beaten this game, but for now...`; + +} +function fixNaNs() { + NaNcheck(player); +} +function NaNcheck(data) { + for (item in data) { + if (data[item] == null) { + } + else if (Array.isArray(data[item])) { + NaNcheck(data[item]); + } + else if (data[item] !== data[item] || checkDecimalNaN(data[item])) { + if (!NaNalert) { + clearInterval(interval); + NaNalert = true; + alert("Invalid value found in player, named '" + item + "'. Please let the creator of this mod know! You can refresh the page, and you will be un-NaNed.") + return + } + } + else if (data[item] instanceof Decimal) { + } + else if ((!!data[item]) && (data[item].constructor === Object)) { + NaNcheck(data[item]); + } + } +} +function exportSave() { + //if (NaNalert) return + let str = btoa(JSON.stringify(player)); + + const el = document.createElement("textarea"); + el.value = str; + document.body.appendChild(el); + el.select(); + el.setSelectionRange(0, 99999); + document.execCommand("copy"); + document.body.removeChild(el); +} +function importSave(imported = undefined, forced = false) { + if (imported === undefined) + imported = prompt("Paste your save here"); + try { + tempPlr = Object.assign(getStartPlayer(), JSON.parse(atob(imported))); + if (tempPlr.versionType != modInfo.id && !forced && !confirm("This save appears to be for a different mod! Are you sure you want to import?")) // Wrong save (use "Forced" to force it to accept.) + return; + player = tempPlr; + player.versionType = modInfo.id; + fixSave(); + versionCheck(); + NaNcheck(save) + save(); + window.location.reload(); + } catch (e) { + return; + } +} +function versionCheck() { + let setVersion = true; + + if (player.versionType === undefined || player.version === undefined) { + player.versionType = modInfo.id; + player.version = 0; + } + + if (setVersion) { + if (player.versionType == modInfo.id && VERSION.num > player.version) { + player.keepGoing = false; + if (fixOldSave) + fixOldSave(player.version); + } + player.versionType = getStartPlayer().versionType; + player.version = VERSION.num; + player.beta = VERSION.beta; + } +} +var saveInterval = setInterval(function () { + if (player === undefined) + return; + if (tmp.gameEnded && !player.keepGoing) + return; + if (options.autosave) + save(); +}, 5000); + +window.onbeforeunload = () => { + if (player.autosave) { + save(); + } +}; \ No newline at end of file diff --git a/js/utils/themes.js b/js/utils/themes.js new file mode 100644 index 0000000..36a5c6e --- /dev/null +++ b/js/utils/themes.js @@ -0,0 +1,51 @@ +// ************ Themes ************ +var themes = ["default", "aqua"] + +var colors = { + default: { + 1: "#ffffff",//Branch color 1 + 2: "#bfbfbf",//Branch color 2 + 3: "#7f7f7f",//Branch color 3 + color: "#dfdfdf", + points: "#ffffff", + locked: "#bf8f8f", + background: "#0f0f0f", + background_tooltip: "rgba(0, 0, 0, 0.75)", + }, + aqua: { + 1: "#bfdfff", + 2: "#8fa7bf", + 3: "#5f6f7f", + color: "#bfdfff", + points: "#dfefff", + locked: "#c4a7b3", + background: "#001f3f", + background_tooltip: "rgba(0, 15, 31, 0.75)", + }, +} +function changeTheme() { + + colors_theme = colors[options.theme || "default"]; + document.body.style.setProperty('--background', colors_theme["background"]); + document.body.style.setProperty('--background_tooltip', colors_theme["background_tooltip"]); + document.body.style.setProperty('--color', colors_theme["color"]); + document.body.style.setProperty('--points', colors_theme["points"]); + document.body.style.setProperty("--locked", colors_theme["locked"]); +} +function getThemeName() { + return options.theme? options.theme : "default"; +} + +function switchTheme() { + let index = themes.indexOf(options.theme) + if (options.theme === null || index >= themes.length-1 || index < 0) { + options.theme = themes[0]; + } + else { + index ++; + options.theme = themes[index]; + options.theme = themes[1]; + } + changeTheme(); + resizeCanvas(); +} diff --git a/options_wheel.png b/options_wheel.png new file mode 100644 index 0000000000000000000000000000000000000000..9a60fff4c724a67754ac451c3d0e157b59fed738 GIT binary patch literal 4419 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 literal 0 HcmV?d00001 diff --git a/remove.png b/remove.png new file mode 100644 index 0000000000000000000000000000000000000000..6c51c6848fabcd63cefac8dbfc9448a22859253d GIT binary patch literal 6017 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⪙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 ItM4BBf4scnD*ylh literal 0 HcmV?d00001