Compare commits

..

No commits in common. "pages" and "main" have entirely different histories.
pages ... main

199 changed files with 54106 additions and 99 deletions

35
.eslintrc.js Normal file
View file

@ -0,0 +1,35 @@
require("@rushstack/eslint-patch/modern-module-resolution");
module.exports = {
root: true,
env: {
node: true
},
extends: [
"plugin:vue/vue3-essential",
"@vue/eslint-config-typescript/recommended",
"@vue/eslint-config-prettier"
],
parserOptions: {
ecmaVersion: 2020,
},
ignorePatterns: ["src/lib"],
rules: {
"no-console": process.env.NODE_ENV === "production" ? "warn" : "off",
"no-debugger": process.env.NODE_ENV === "production" ? "warn" : "off",
"vue/script-setup-uses-vars": "warn",
"vue/no-mutating-props": "off",
"vue/multi-word-component-names": "off",
"@typescript-eslint/strict-boolean-expressions": [
"error",
{
allowNullableObject: true,
allowNullableBoolean: true
}
]
},
globals: {
defineProps: "readonly",
defineEmits: "readonly"
}
};

View file

@ -0,0 +1,31 @@
name: Build and Deploy
on:
push:
branches:
- 'main'
workflow_dispatch:
jobs:
build-and-deploy:
if: github.repository != 'profectus-engine/Profectus' # Don't build placeholder mod on main repo
runs-on: docker
steps:
- name: Setup RSync
run: |
apt-get update
apt-get install -y rsync
- name: Checkout 🛎️
uses: actions/checkout@v2
with:
submodules: recursive
- name: Install and Build 🔧 # This example project is built using npm and outputs the result to the 'build' folder. Replace with the commands required to build your project, or remove this step entirely if your site is pre-built.
run: |
npm ci
npm run build
- name: Deploy 🚀
uses: https://github.com/JamesIves/github-pages-deploy-action@v4.2.5
with:
branch: pages # The branch the action should deploy to.
folder: dist # The folder the action should deploy.

View file

@ -0,0 +1,21 @@
name: Run Tests
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: docker
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
- name: Use Node.js 16.x
uses: actions/setup-node@v3
with:
node-version: 16.x
- run: npm ci
- run: npm run build --if-present
- run: npm test

26
.github/workflows/deploy.yml vendored Normal file
View file

@ -0,0 +1,26 @@
name: Build and Deploy
on:
push:
branches:
- 'main'
workflow_dispatch:
jobs:
build-and-deploy:
if: github.repository != 'profectus-engine/Profectus' # Don't build placeholder mod on main repo
runs-on: ubuntu-latest
steps:
- name: Checkout 🛎️
uses: actions/checkout@v2
with:
submodules: recursive
- name: Install and Build 🔧 # This example project is built using npm and outputs the result to the 'build' folder. Replace with the commands required to build your project, or remove this step entirely if your site is pre-built.
run: |
npm ci
npm run build
- name: Deploy 🚀
uses: JamesIves/github-pages-deploy-action@v4.2.5
with:
branch: gh-pages # The branch the action should deploy to.
folder: dist # The folder the action should deploy.

21
.github/workflows/test.yml vendored Normal file
View file

@ -0,0 +1,21 @@
name: Run Tests
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
submodules: recursive
- name: Use Node.js 16.x
uses: actions/setup-node@v3
with:
node-version: 16.x
- run: npm ci
- run: npm run build --if-present
- run: npm test

25
.gitignore vendored Normal file
View file

@ -0,0 +1,25 @@
.DS_Store
node_modules
/dist
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
**/*.json.d.ts

7
.prettierrc.json Normal file
View file

@ -0,0 +1,7 @@
{
"arrowParens": "avoid",
"endOfLine": "auto",
"printWidth": 100,
"tabWidth": 4,
"trailingComma": "none"
}

13
.replit Normal file
View file

@ -0,0 +1,13 @@
run = "npm install; npm run dev"
[packager]
language = "nodejs"
[packager.features]
packageSearch = true
guessImports = false
[languages.javascript]
pattern = "**/{*.js,*.jsx,*.ts,*.tsx}"
[languages.javascript.languageServer]
start = [ "typescript-language-server", "--stdio" ]

23
.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,23 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "pwa-node",
"request": "launch",
"name": "Debug Current Test File",
"autoAttachChildProcesses": true,
"skipFiles": [
"<node_internals>/**",
"**/node_modules/**"
],
"program": "${workspaceRoot}/node_modules/vitest/vitest.mjs",
"args": [
"run",
"${relativeFile}"
],
"smartStep": true,
"console": "integratedTerminal"
}
]
}

12
.vscode/settings.json vendored Normal file
View file

@ -0,0 +1,12 @@
{
"vitest.commandLine": "npx vitest",
"editor.codeActionsOnSave": {
"source.fixAll.eslint": "explicit"
},
"editor.defaultFormatter": "esbenp.prettier-vscode",
"git.ignoreLimitWarning": true,
"[typescriptreact]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"typescript.tsdk": "node_modules/typescript/lib"
}

431
CHANGELOG.md Normal file
View file

@ -0,0 +1,431 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [0.6.1] - 2023-05-17
### Added
- Error boundaries around each layer, and errors now display on the page when in development
- Utility for creating requirement based on whether a conversion has met a requirement
### Changed
- **BREAKING** Formulas/requirements refactor
- spendResources renamed to cumulativeCost
- summedPurchases renamed to directSum
- calculateMaxAffordable now takes optional 'maxBulkAmount' parameter
- cost requirements now pass cumulativeCost, maxBulkAmount, and directSum to calculateMaxAffordable
- Non-integrable and non-invertible formulas will now work in more situations
- Repeatable.maximize is removed
- Challenge.maximize is removed
- Formulas have better typing information now
- Integrate functions now log errors if the variable input is not integrable
- Cyclical proxies now throw errors
- createFormulaPreview is now a JSX function
- Tree nodes are not automatically capitalized anymore
- upgrade.canPurchase now returns false if the upgrade is already bought
- TPS display is simplified and more performant now
### Fixed
- Actions could not be constructed
- Progress bar on actions was misaligned
- Many different issues the Board features (and many changes/improvements)
- Calculating max affordable could sometimes infinite loop
- Non-integrable formulas could cause errors in cost requirements
- estimateTime would not show "never" when production is 0
- isInvertible and isIntegrable now properly handle nested formulas
- Repeatables' amount display would show the literal text "joinJSX"
- Repeatables would not buy max properly
- Reset buttons were showing wrong "currentAt" vs "nextAt"
- Step-wise formulas not updating their value correctly
- Bonus amount decorator now checks for `amount` property in the post construct callback
### Documentation
- Various typos fixed and a few sections made more thorough
## [0.6.0] - 2023-04-20
### Added
- **BREAKING** New requirements system
- Replaces many features' existing requirements with new generic form
- **BREAKING** Formulas, which can be used to calculate buy max for you
- Requirements can use them so repeatables and challenges can be "buy max" without any extra effort
- Conversions now use formulas instead of the old scaling functions system, allowing for arbitrary functions that are much easier to follow
- Modifiers have a new getFormula property
- Feature decorators, which simplify the process of adding extra values to features
- Action feature, which is a clickable with a cooldown
- ETA util (calculates time until a specific amount of a resource, based on its current gain rate)
- createCollapsibleAchievements util
- deleteLowerSaves util
- Minimized layers can now display a component
- submitOnBlur property to Text fields
- showPopups property to achievements
- Mouse/touch events to more onClick listeners
- Example hotkey to starting layer
- Schema for projInfo.json
### Changes
- **BREAKING** Buyables renamed to Repeatables
- Renamed purchaseLimit to limit
- Renamed buyMax to maximize
- Added initialAmount property
- **BREAKING** Persistent refs no longer have redundancies in save object
- Requires referencing persistent refs either through a proxy or by wrapping in `noPersist()`
- **BREAKING** Visibility properties can now take booleans
- Removed showIf util
- **BREAKING** Lazy proxies and options functions now pass the base object in as `this` as well as the first parameter.
- Tweaked settings display
- setupPassiveGeneration will no longer lower the resource
- displayResource now floors resource amounts
- Tweaked modifier displays, incl showing negative modifiers in red
- Hotkeys now appear on key graphic
- Mofifier sections now accept computable strings for title and subtitle
- Every VueFeature's `[Component]` property is now typed as GenericComponent
- Make errors throw objects instead of strings
- Updated b_e
### Fixed
- NaN detection stopped working
- Now specifically only checks persistent refs
- trackTotal would increase the total when loading the save
- PWAs wouldn't show updates
- Board feature no longer working at all
- Some discord links didn't open in new tab
- Adjacent grid cells wouldn't merge
- When fixing old saves, the modVersion would not be updated
- Default layer would display `Dev Speed: 0x` when paused
- Fixed hotkeys not working with shift + numbers
- Fixed console errors about deleted persistent refs not being included in the layer object
- Modifiers wouldn't display small numbers
- Conversions' addSoftcap wouldn't affect currentAt or nextAt
- MainDisplay not respecting style and classes props
- Tabs could sometimes not update correctly
- offlineTime not capping properly
- Tooltips being user-selectable
- Pinnable tooltips causing stack overflow
- Workflows not working with submodules
- Various minor typing issues
### Removed
- **BREAKING** Removed milestones (achievements now have small and large displays)
### Documented
- every single feature
- formulas
- requirements
### Tests
- conversions
- formulas
- modifiers
- requirements
Contributors: thepaperpilot, escapee, adsaf, ducdat
## [0.5.2] - 2022-08-22
### Added
- onLoad event
- fontsLoaded event
- Dismissable notification you can add to VueFeatures when they're interactable
- Option on exponential modifiers to better support numbers less than 1
- Utility function to track if a VueFeature is being hovered over
- Utility to unwrap Resources that may be in refs
- Utility to join JSX elements together with a joiner
- Type for converting readonly string arrays into a union of string values
### Changed
- The main and prestige layers no longer use arrow functions for their options functions
- Modifiers are now lazily loaded
- Collapsible modifier sections are now lazily loaded
- Converted several refs into shallow refs for improved performance
- Roboto Mono and Material Icons fonts are now bundled instead of downloaded from the web, so they work with PWAs
- Node bounds are now updated whenever that context has a node removed or added, fixing many issues with incorrect bounds
### Fixed
- trackResetTime not updating
- colorText prepending $s
- Default .replit config was broken
- Pixi.js canvases no longer rendering
- Node positions being shifted on initial page load due to fonts loading on firefox
- Modifier sections looked wrong if the topmost section wasn't visible
## [0.5.1] - 2022-07-17
### Added
- Notif component that displays a jumping exclamation point
- showAmount boolean to buyable displays
- Tab families now take option to style the tab buttons container
- Utility for creating text of a certain color
### Changed
- Improved typing of player.layers
- Improved typing of createCollapsibleModifierSections's parameters
- Made Particles vue component typed as GenericComponent due to issues generating documentation
- Minimized how much of pixi.js is included in the built site
- Split bundles into smaller bundles for faster loading
- Updated TypeScript
- Descriptions on buyables are now optional
- Improved tooltips performance
- Improved how MainDisplay displays effect strings
- MainDisplays are now sticky
- processComputable now binds uncached functions as well
### Fixed
- trackResetTime stopped working once its layer was removed and re-added
- Runtime compilation was disabled in vite config
- Websites had to be hosted on root directory to have assets load correctly
- Tooltips' persistent ref was lazily created
- In some situations Links would not update its bounding rect
- Achievements' and milestones' onComplete callbacks were firing on load
- Processed JSXFunctions were not considered coercable components by isCoercableComponent
- Error from passing in overlay text to bar component
### Removed
- lodash.cloneDeep dependency, which hasn't been used in awhile
- Some unused configs from vue-cli-service
### Documented
- Update vitepress, and updated the content of many pages
- Rest of /game
- Rest of /data
- layers.tsx
- Any type augmentations to Window object
- Various cleanup of docs comments
- Fixed doc generation being broken from switch to vite
### Tests
- Switched from jest to vitest
## [0.5.0] - 2022-06-27
### Added
- Projects now cache for offline play, and show notification when an update is available
- Projects can now be "installed" as a Progressive Web App
- Conversions can now be given a custom spend function, which defaults to setting the base resource amount to 0
- Components for displaying Floor and Square Root symbols
### Changed
- **BREAKING** Several projInfo properties now default to empty strings, to prevent things like reusing project IDs
- **BREAKING** Replaced vue-cli-service with vite (should not break most projects)
- Updated dependencies
- Made all type-only imports explicit
- setupPassiveGeneration now works properly on independent conversions
- setupPassiveGeneration now takes an option cap it can't go over
- Improved typing for PlayerData.layers
- Options Functions have an improved `this` type - it now includes the options themselves
- Removed v-show being used in data/common.tsx
### Tests
- Implement Jest, and running tests automatically on push
- Tests written for utils/common.ts
## [0.4.2] - 2022-05-23
### Added
- costModifier to conversions
- onConvert(amountGained) to conversions
### Changed
- **BREAKING** getFirstFeature has a new signature, that will lead to improved performance
- trackResetTime is now intended to be used with a reset button
- regularFormat handles small numbers better
- Slider tooltips now appear below the slider, not above
- Node's mutation observers now ignore attributes. This shouldn't have issues with links/particle effect positions, but prevents a _lot_ of unnecessary node updates
- OptionsFunc no longer takes its S type parameter, as it was unnecessary. Layer options functions now have proper `this` typing
- Several functions have been updated to take BaseLayer instead of GenericLayer, to allow them to work with `this` inside layer options functions
### Fixed
- Particle effects and links would not always appear on reload or when switching layers
- Particle effects and links no longer appear in wrong spot after nodes are added or removed
- Collapsibles having wrong widths on the button and collapsed content sections
- Additive modifiers with negative values appeared like "+-" instead of "-"
- Buyables' onPurchase was not being called
- Reset button would display "Next:" if the buyMax property is a ref
## [0.4.1] - 2022-05-10
### Added
- findFeatures can now accept multiple feature types
- excludeFeatures can now be used to find features with a feature type _blacklist_
- All the icons in the saves manager now have tooltips
### Changed
- All touch events that can be passive now are
- Layers' style and classes attributes are now applied to the tab element rather than the layer-tab
- Saving now always uses lz-string, and saveEncoding has been renamed to exportEncoding
- The property will now only affect exports, and defaults to base64 so exports can be shared in more places without issues
- Buyables can now have their onClick/purchase function overwritten
### Fixed
- Arrays in player were not being wrapped in proxies for things like NaN detection
- Error when switching between saves with different layers
- Links would sometimes error from trying to use nodes that were removed earlier that frame
- createModifierSection would require modifiers to have revert and enabled properties despite not using them
- Tab buttons would not use the style property if it was a ref
- Typings on the Board vue component were incorrect
- Offline time would always show, if offlineLimit is set to 0
- Buyables will now call onPurchase() when cost and/or resource were not set
- Presets dropdown wouldn't deselect the option after creating the save
### Documented
- feature.ts
## [0.4.0] - 2022-05-01
### Added
- Saves can now be encoded in two new options: plaintext and lz compressed, determined by a new "saveEncoding" property in projInfo
- Saves will be loaded in whatever format is detected. The setting only applies when writing saves
- createModifierSection has new parameter to override the label used for the base value
- createCollapsibleModifierSections utility function to display `createModifierSection`s in collapsible forms
### Fixed
- Saves manager would not clear the current save from its cache when switching saves, leading to progress loss if flipping between saves
- Layer.minWidth being ignored
- Separators between tabs (player.tabs) would not extend to the bottom of the screen when scrolling
- Tree nodes not being clicked on their edges
### Changed
- **BREAKING** No features extend persistent anymore
- This will break ALL existing saves that aren't manually dealt with in fixOldSave
- Affected features: Achievement, Buyable, Grid, Infobox, Milestone, TabFamily, and Upgrade
- Affected features will now have a property within them where the persistent ref is stored. This means new persistent refs can now be safely added to these features
- Features with option functions with 0 required properties now don't require passing in an options function
- Improved the look of the goBack and minimize buttons (and made them more consistent with each other)
- Newly created saves are immediately switched to
- TooltipDirection and Direction have been merged into one enum
- Made layers shallow reactive, so it works better with dynamic layers
- Modifier functions all have more explicit types now
- Scaling functions take computables instead of processed computables
### Removed
- Unused tsParticles.d.ts file
### Documented
- modifiers.ts
- conversions.ts
## [0.3.3] - 2022-04-24
### Fixed
- Spacing between rows in Tree components
- Computed style attributes on tooltips were ignored
- Tooltips could cause infinite loops due to cyclical dependencies
## [0.3.2] - 2022-04-23
### Fixed
- Clickables and several other elements would not register clicks sometimes, if the display is updating rapidly
- createLayerTreeNode wasn't using display option correctly
## [0.3.1] - 2022-04-23
### Added
- Render utility methods that always return JSX Elements
### Changed
- **BREAKING** Tooltips overhaul
- Tree Nodes no longer have tooltips related properties
- Tooltips can now be added to any feature with a Vue component using the `addTooltip` function
- Any tooltip can be made pinnable by setting pinnable to true in the addTooltip options, or by passing a `Ref<boolean>` to a Tooltip component
- Pinned tooltips have an icon to represent that. It can be disabled by setting the theme's `showPin` property to false
- Modifiers are now their own features rather than a part of conversions
- Including utilities to display the current state of all the modifiers
- TabFamilies' options function is now optional
- Layer.minWidth can take string values
- If parseable into a number, it'll have "px" appended. Otherwise it'll be un-processed
- TreeNodes now have Vue components attached to them
- `createResourceTooltip` now shows the resource name
- Made classic and aqua theme's `feature-foreground` color dark rather than light
## [0.3.0] - 2022-04-10
### Added
- conversion.currentAt [#4](https://github.com/profectus-engine/Profectus/pull/4)
- OptionsFunc utility type, improving type inferencing in feature types
- minimumGain property to ResetButton, defaulting to 1
### Changed
- **BREAKING** Major persistence rework
- Removed makePersistent
- Removed old Persistent, and renamed PersistentRef to Persistent
- createLazyProxy now takes optional base object (replacing use cases for makePersistent)
- Added warnings when creating refs outside a layer
- Added warnings when persistent refs aren't included in their layer object
- **BREAKING** createLayer now takes id as the first param, rather than inside the option function
- resetButton now shows "Req:" instead of "Next:" when conversion.buyMax is false
- Conversion nextAt and currentAt now cap at 0 after reverting modifier
### Fixed
- Independent conversion gain calculation [#4](https://github.com/profectus-engine/Profectus/pull/4)
- Persistence issue when loading layer dynamically
- resetButton's gain and requirement display being incorrect when conversion.buyMax is false
- Independent conversions with buyMax false capping incorrectly
## [0.2.2] - 2022-04-01
Unironically posting an update on April Fool's Day ;)
### Changed
- **BREAKING** Replaced tsparticles with pixi-emitter. Different options, and behaves differently.
- Print key and value in lazy proxy's setter message
- Update bounding boxes after web fonts load in
### Removed
- safff.txt
## [0.2.1] - 2022-03-29
### Changed
- **BREAKING** Reworked conversion.modifyGainAmount into conversion.gainModifier, with several utility functions. This makes nextAt accurate with modified gain
### Fixed
- Made overlay nav not overlap leftmost layer
## [0.2.0] - 2022-03-27
### Added
- Particles feature
- Collapsible layout component
- Utility function for splitting off the first from the list of features that meets a given filter
### Changed
- **BREAKING** Reworked most of the code from Links into a generic Context component that manages the positions of features in the DOM
- Updated vue-cli and TS dependencies
- Challenges cannot be started when maxed, and `canStart` now defaults to `true`
- onClick listeners on various features now get passed a MouseEvent or TouchEvent when possible
- Minor style changes to Milestones, most notably removing min-height
### Fixed
- Buyables didn't support CoercableComponents for displays
- TreeNodes would have a double glow effect on hover
### Removed
- Unused mousemove listener attached to App.vue
## [0.1.4] - 2022-03-13
### Added
- You can now access this.on() from within a createLayer function (and other BaseLayer properties)
- Support for passing non-persistent refs to createResource
- dontMerge class to allow features to ignore mergeAdjacent
### Fixed
- Clickables would not merge adjacent
- onClick and onHold functions would not be bound to their object when being called
- Refs passed to a components style prop would be ignored
- Fixed z-index issue when stopping hovering over features with .can class
## [0.1.3] - 2022-03-11
### Added
- Milestone.complete
- Challenge.complete
- setupAutoClick function to run a clickable's onClick every tick
- setupAutoComplete function to attempt to complete a challenge every tick
- isAnyChallengeActive function to query if any challenge from a given list is active
- Hotkeys now appear in info modal, if any exist
- projInfo.json now includes a "enablePausing" option that can be used to prevent the player from pausing the game
- Added a "gameWon" global event
### Changed
- **BREAKING** Buyables now default to an infinite purchase limit
- **BREAKING** devSpeed, playedTime, offlineTime, and diff now use numbers instead of Decimals
- **BREAKING** Achievements and milestones now use watchEffect to check for completion, instead of polling each tick. shouldEarn properties now only accept functions
- Cached more decimal values for optimization
### Fixed
- Many types not being exported
- setupHoldToClick wouldn't stop clicking after a component is unmounted
- Header's banner would not have correct width
### Removed
- **BREAKING** Removed setupAutoReset
### Documentation
- Support for documentation generation using typedoc
- Hide main layer from docs
- Hide prestige layer from docs
- Use stub declaration files for libs that don't provide types (vue-panzoom and vue-textarea-autosize)
## [0.1.2] - 2022-03-05
### Changed
- **BREAKING** Removed "@" path alias, and used baseUrl instead
- **BREAKING** Renamed createExponentialScaling to createPolynomialScaling and removed coefficient parameter
- Changed options passed into createLayerTreeNode; now allows overriding display
- App component is no longer cloned before being passed to `createApp`
- Changed TS version from ^4.5.4 to ~4.5.5
### Fixed
- Document title is set as soon as possible now
## [0.1.1] - 2022-03-02
### Added
- Configuration for Glitch projects
- Configuration for Replit projects
- Hide versionTitle if blank
### Changed
- **BREAKING** Renamed modInfo.json -> projInfo.json
- **BREAKING** Renamed mod.tsx -> projEntry.tsx
- Improved performance of branch drawing code
- Improved performance of formatting numbers
- Changed some projInfo default values to empty strings
- Renamed projInfo.allowSmall -> projInfo.defaultShowSmall
### Fixed
- Spacing on discord logo in NaN screen
- Some files accessing old location for persistence code
- Fixed lint-staged not being listed in devDependencies
- Branch locations were not accurate after scrolling
- Saves Manager displayed "default body" while closing
- Reset buttons activating when held down when canClick is false
- Lifting up on auto clickable elements not stopping the auto clicker
### Removed
- Removed Theme.stackedInfoboxes
- Removed Theme.showSingleTab
## [0.1.0] - Initial Release

21
LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2021 Anthony Lawn
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.

32
README.md Normal file
View file

@ -0,0 +1,32 @@
# Profectus
A game engine that grows with you
[![Run on Repl.it](https://repl.it/badge/github/profectus-engine/Profectus)](https://repl.it/github/profectus-engine/Profectus)
[Read the docs](https://moddingtree.com)
## Project setup
```
npm install
```
### Hosts dev server and hot-reloads modules as they're changed
```
npm start
```
### Compiles and minifies for production
```
npm run build
```
### Hosts the production build
```
npm run preview
```
### Runs the tests using vite-jest
```
npm run test
```

File diff suppressed because one or more lines are too long

View file

@ -1 +0,0 @@
function r(t){if(t)return n(t)}function n(t){for(var s in r.prototype)t[s]=r.prototype[s];return t}r.prototype.on=r.prototype.addEventListener=function(t,s){return this._callbacks=this._callbacks||{},(this._callbacks["$"+t]=this._callbacks["$"+t]||[]).push(s),this};r.prototype.once=function(t,s){function e(){this.off(t,e),s.apply(this,arguments)}return e.fn=s,this.on(t,e),this};r.prototype.off=r.prototype.removeListener=r.prototype.removeAllListeners=r.prototype.removeEventListener=function(t,s){if(this._callbacks=this._callbacks||{},arguments.length==0)return this._callbacks={},this;var e=this._callbacks["$"+t];if(!e)return this;if(arguments.length==1)return delete this._callbacks["$"+t],this;for(var a,i=0;i<e.length;i++)if(a=e[i],a===s||a.fn===s){e.splice(i,1);break}return e.length===0&&delete this._callbacks["$"+t],this};r.prototype.emit=function(t){this._callbacks=this._callbacks||{};for(var s=new Array(arguments.length-1),e=this._callbacks["$"+t],a=1;a<arguments.length;a++)s[a-1]=arguments[a];if(e){e=e.slice(0);for(var a=0,i=e.length;a<i;++a)e[a].apply(this,s)}return this};r.prototype.emitReserved=r.prototype.emit;r.prototype.listeners=function(t){return this._callbacks=this._callbacks||{},this._callbacks["$"+t]||[]};r.prototype.hasListeners=function(t){return!!this.listeners(t).length};export{r as E};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1 +0,0 @@
const o=Object.create(null);o.open="0";o.close="1";o.ping="2";o.pong="3";o.message="4";o.upgrade="5";o.noop="6";const a=Object.create(null);Object.keys(o).forEach(e=>{a[o[e]]=e});const B={type:"error",data:"parser error"},w=typeof Blob=="function"||typeof Blob!="undefined"&&Object.prototype.toString.call(Blob)==="[object BlobConstructor]",b=typeof ArrayBuffer=="function",E=e=>typeof ArrayBuffer.isView=="function"?ArrayBuffer.isView(e):e&&e.buffer instanceof ArrayBuffer,C=({type:e,data:t},n,r)=>w&&t instanceof Blob?n?r(t):d(t,r):b&&(t instanceof ArrayBuffer||E(t))?n?r(t):d(new Blob([t]),r):r(o[e]+(t||"")),d=(e,t)=>{const n=new FileReader;return n.onload=function(){const r=n.result.split(",")[1];t("b"+r)},n.readAsDataURL(e)},A="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",u=typeof Uint8Array=="undefined"?[]:new Uint8Array(256);for(let e=0;e<A.length;e++)u[A.charCodeAt(e)]=e;const R=e=>{let t=e.length*.75,n=e.length,r,c=0,f,s,i,y;e[e.length-1]==="="&&(t--,e[e.length-2]==="="&&t--);const p=new ArrayBuffer(t),l=new Uint8Array(p);for(r=0;r<n;r+=4)f=u[e.charCodeAt(r)],s=u[e.charCodeAt(r+1)],i=u[e.charCodeAt(r+2)],y=u[e.charCodeAt(r+3)],l[c++]=f<<2|s>>4,l[c++]=(s&15)<<4|i>>2,l[c++]=(i&3)<<6|y&63;return p},P=typeof ArrayBuffer=="function",T=(e,t)=>{if(typeof e!="string")return{type:"message",data:h(e,t)};const n=e.charAt(0);return n==="b"?{type:"message",data:O(e.substring(1),t)}:a[n]?e.length>1?{type:a[n],data:e.substring(1)}:{type:a[n]}:B},O=(e,t)=>{if(P){const n=R(e);return h(n,t)}else return{base64:!0,data:e}},h=(e,t)=>{switch(t){case"blob":return e instanceof ArrayBuffer?new Blob([e]):e;case"arraybuffer":default:return e}},g=String.fromCharCode(30),S=(e,t)=>{const n=e.length,r=new Array(n);let c=0;e.forEach((f,s)=>{C(f,!1,i=>{r[s]=i,++c===n&&t(r.join(g))})})},m=(e,t)=>{const n=e.split(g),r=[];for(let c=0;c<n.length;c++){const f=T(n[c],t);if(r.push(f),f.type==="error")break}return r},j=4;export{m as a,C as b,T as d,S as e,j as p};

View file

@ -1 +0,0 @@
import{_ as d,s as p,p as e,a as n,l as T,D as l,b as r,g as u}from"./index.609ecad2.js";import"./vue.228877f7.js";import{b1 as c}from"./@vue.8948d9b0.js";/* empty css */import"./lz-string.f2f3b7cf.js";import"./nanoevents.1080beb7.js";import"./socket.io-client.03bb8f3a.js";import"./engine.io-client.6ba5801d.js";import"./engine.io-parser.730afdce.js";import"./@socket.io.aec831e2.js";import"./socket.io-parser.0ab387d5.js";import"./unique-names-generator.9178d3e3.js";import"./vue-toastification.97914fdb.js";import"./semver.334eb41f.js";import"./lru-cache.9506e0ec.js";import"./yallist.fd762fe7.js";import"./vue-textarea-autosize.35804eaf.js";import"./vue-next-select.f2be13cc.js";import"./vuedraggable.5218041c.js";import"./sortablejs.692999e9.js";import"./workbox-window.8d14e8b7.js";let o=null,f=null;function m(){const t=Date.now();let i=(t-e.time)/1e3;e.time=t;const a=i;if(n.lastTenTicks.push(a),n.lastTenTicks.length>10&&(n.lastTenTicks=n.lastTenTicks.slice(1)),!((f==null?void 0:f.value)&&!e.keepGoing)&&!n.hasNaN&&(i=Math.max(i,0),e.devSpeed!==0)){if(T.value=!1,e.offlineTime!=null){if(l.gt(e.offlineTime,r.offlineLimit*3600)&&(e.offlineTime=r.offlineLimit*3600),l.gt(e.offlineTime,0)&&e.devSpeed!==0){const s=Math.max(e.offlineTime/10,i);e.offlineTime=e.offlineTime-s,i+=s}else e.devSpeed===0&&(e.offlineTime+=i);(!e.offlineProd||l.lt(e.offlineTime,0))&&(e.offlineTime=null)}i=Math.min(i,r.maxTickLength),e.devSpeed!=null&&(i*=e.devSpeed),Number.isFinite(i)||(i=1e308),!l.eq(i,0)&&(e.timePlayed+=i,Number.isFinite(e.timePlayed)||(e.timePlayed=1e308),u.emit("update",i,a),p.unthrottled?(requestAnimationFrame(m),o!=null&&(clearInterval(o),o=null)):o==null&&(o=setInterval(m,50)))}}async function j(){f=(await d(()=>import("./index.609ecad2.js").then(function(t){return t.c}),["assets/index.609ecad2.js","assets/index.2e66f9e5.css","assets/@fontsource.f66d05e7.css","assets/vue.228877f7.js","assets/lru-cache.9506e0ec.js","assets/yallist.fd762fe7.js","assets/@vue.8948d9b0.js","assets/lz-string.f2f3b7cf.js","assets/nanoevents.1080beb7.js","assets/socket.io-client.03bb8f3a.js","assets/engine.io-client.6ba5801d.js","assets/engine.io-parser.730afdce.js","assets/@socket.io.aec831e2.js","assets/socket.io-parser.0ab387d5.js","assets/unique-names-generator.9178d3e3.js","assets/vue-toastification.97914fdb.js","assets/vue-toastification.4b5f8ac8.css","assets/semver.334eb41f.js","assets/vue-textarea-autosize.35804eaf.js","assets/vue-next-select.f2be13cc.js","assets/vue-next-select.9e6f4164.css","assets/vuedraggable.5218041c.js","assets/sortablejs.692999e9.js","assets/workbox-window.8d14e8b7.js"])).hasWon,c(f,t=>{t&&u.emit("gameWon")}),p.unthrottled?requestAnimationFrame(m):o=setInterval(m,50)}export{j as startGameLoop};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1 +0,0 @@
import{y as x}from"./yallist.fd762fe7.js";var C=typeof globalThis!="undefined"?globalThis:typeof window!="undefined"?window:typeof global!="undefined"?global:typeof self!="undefined"?self:{};function G(n){return n&&n.__esModule&&Object.prototype.hasOwnProperty.call(n,"default")?n.default:n}function N(n){if(n.__esModule)return n;var t=Object.defineProperty({},"__esModule",{value:!0});return Object.keys(n).forEach(function(e){var r=Object.getOwnPropertyDescriptor(n,e);Object.defineProperty(t,e,r.get?r:{enumerable:!0,get:function(){return n[e]}})}),t}const O=x,u=Symbol("max"),l=Symbol("length"),g=Symbol("lengthCalculator"),b=Symbol("allowStale"),c=Symbol("maxAge"),o=Symbol("dispose"),A=Symbol("noDisposeOnSet"),i=Symbol("lruList"),a=Symbol("cache"),S=Symbol("updateAgeOnGet"),v=()=>1;class _{constructor(t){if(typeof t=="number"&&(t={max:t}),t||(t={}),t.max&&(typeof t.max!="number"||t.max<0))throw new TypeError("max must be a non-negative number");this[u]=t.max||1/0;const e=t.length||v;if(this[g]=typeof e!="function"?v:e,this[b]=t.stale||!1,t.maxAge&&typeof t.maxAge!="number")throw new TypeError("maxAge must be a number");this[c]=t.maxAge||0,this[o]=t.dispose,this[A]=t.noDisposeOnSet||!1,this[S]=t.updateAgeOnGet||!1,this.reset()}set max(t){if(typeof t!="number"||t<0)throw new TypeError("max must be a non-negative number");this[u]=t||1/0,d(this)}get max(){return this[u]}set allowStale(t){this[b]=!!t}get allowStale(){return this[b]}set maxAge(t){if(typeof t!="number")throw new TypeError("maxAge must be a non-negative number");this[c]=t,d(this)}get maxAge(){return this[c]}set lengthCalculator(t){typeof t!="function"&&(t=v),t!==this[g]&&(this[g]=t,this[l]=0,this[i].forEach(e=>{e.length=this[g](e.value,e.key),this[l]+=e.length})),d(this)}get lengthCalculator(){return this[g]}get length(){return this[l]}get itemCount(){return this[i].length}rforEach(t,e){e=e||this;for(let r=this[i].tail;r!==null;){const s=r.prev;E(this,t,r,e),r=s}}forEach(t,e){e=e||this;for(let r=this[i].head;r!==null;){const s=r.next;E(this,t,r,e),r=s}}keys(){return this[i].toArray().map(t=>t.key)}values(){return this[i].toArray().map(t=>t.value)}reset(){this[o]&&this[i]&&this[i].length&&this[i].forEach(t=>this[o](t.key,t.value)),this[a]=new Map,this[i]=new O,this[l]=0}dump(){return this[i].map(t=>w(this,t)?!1:{k:t.key,v:t.value,e:t.now+(t.maxAge||0)}).toArray().filter(t=>t)}dumpLru(){return this[i]}set(t,e,r){if(r=r||this[c],r&&typeof r!="number")throw new TypeError("maxAge must be a number");const s=r?Date.now():0,h=this[g](e,t);if(this[a].has(t)){if(h>this[u])return y(this,this[a].get(t)),!1;const m=this[a].get(t).value;return this[o]&&(this[A]||this[o](t,m.value)),m.now=s,m.maxAge=r,m.value=e,this[l]+=h-m.length,m.length=h,this.get(t),d(this),!0}const f=new T(t,e,h,s,r);return f.length>this[u]?(this[o]&&this[o](t,e),!1):(this[l]+=f.length,this[i].unshift(f),this[a].set(t,this[i].head),d(this),!0)}has(t){if(!this[a].has(t))return!1;const e=this[a].get(t).value;return!w(this,e)}get(t){return p(this,t,!0)}peek(t){return p(this,t,!1)}pop(){const t=this[i].tail;return t?(y(this,t),t.value):null}del(t){y(this,this[a].get(t))}load(t){this.reset();const e=Date.now();for(let r=t.length-1;r>=0;r--){const s=t[r],h=s.e||0;if(h===0)this.set(s.k,s.v);else{const f=h-e;f>0&&this.set(s.k,s.v,f)}}}prune(){this[a].forEach((t,e)=>p(this,e,!1))}}const p=(n,t,e)=>{const r=n[a].get(t);if(r){const s=r.value;if(w(n,s)){if(y(n,r),!n[b])return}else e&&(n[S]&&(r.value.now=Date.now()),n[i].unshiftNode(r));return s.value}},w=(n,t)=>{if(!t||!t.maxAge&&!n[c])return!1;const e=Date.now()-t.now;return t.maxAge?e>t.maxAge:n[c]&&e>n[c]},d=n=>{if(n[l]>n[u])for(let t=n[i].tail;n[l]>n[u]&&t!==null;){const e=t.prev;y(n,t),t=e}},y=(n,t)=>{if(t){const e=t.value;n[o]&&n[o](e.key,e.value),n[l]-=e.length,n[a].delete(e.key),n[i].removeNode(t)}};class T{constructor(t,e,r,s,h){this.key=t,this.value=e,this.length=r,this.now=s,this.maxAge=h||0}}const E=(n,t,e,r)=>{let s=e.value;w(n,s)&&(y(n,e),n[b]||(s=void 0)),s&&t.call(r,s.value,s.key,n)};var j=_;export{G as a,C as c,N as g,j as l};

View file

@ -1 +0,0 @@
var O={exports:{}};(function(y){var U=function(){var _=String.fromCharCode,M="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",S="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+-$",x={};function m(o,r){if(!x[o]){x[o]={};for(var c=0;c<o.length;c++)x[o][o.charAt(c)]=c}return x[o][r]}var d={compressToBase64:function(o){if(o==null)return"";var r=d._compress(o,6,function(c){return M.charAt(c)});switch(r.length%4){default:case 0:return r;case 1:return r+"===";case 2:return r+"==";case 3:return r+"="}},decompressFromBase64:function(o){return o==null?"":o==""?null:d._decompress(o.length,32,function(r){return m(M,o.charAt(r))})},compressToUTF16:function(o){return o==null?"":d._compress(o,15,function(r){return _(r+32)})+" "},decompressFromUTF16:function(o){return o==null?"":o==""?null:d._decompress(o.length,16384,function(r){return o.charCodeAt(r)-32})},compressToUint8Array:function(o){for(var r=d.compress(o),c=new Uint8Array(r.length*2),e=0,t=r.length;e<t;e++){var p=r.charCodeAt(e);c[e*2]=p>>>8,c[e*2+1]=p%256}return c},decompressFromUint8Array:function(o){if(o==null)return d.decompress(o);for(var r=new Array(o.length/2),c=0,e=r.length;c<e;c++)r[c]=o[c*2]*256+o[c*2+1];var t=[];return r.forEach(function(p){t.push(_(p))}),d.decompress(t.join(""))},compressToEncodedURIComponent:function(o){return o==null?"":d._compress(o,6,function(r){return S.charAt(r)})},decompressFromEncodedURIComponent:function(o){return o==null?"":o==""?null:(o=o.replace(/ /g,"+"),d._decompress(o.length,32,function(r){return m(S,o.charAt(r))}))},compress:function(o){return d._compress(o,16,function(r){return _(r)})},_compress:function(o,r,c){if(o==null)return"";var e,t,p={},w={},v="",A="",u="",h=2,a=3,f=2,l=[],n=0,s=0,i;for(i=0;i<o.length;i+=1)if(v=o.charAt(i),Object.prototype.hasOwnProperty.call(p,v)||(p[v]=a++,w[v]=!0),A=u+v,Object.prototype.hasOwnProperty.call(p,A))u=A;else{if(Object.prototype.hasOwnProperty.call(w,u)){if(u.charCodeAt(0)<256){for(e=0;e<f;e++)n=n<<1,s==r-1?(s=0,l.push(c(n)),n=0):s++;for(t=u.charCodeAt(0),e=0;e<8;e++)n=n<<1|t&1,s==r-1?(s=0,l.push(c(n)),n=0):s++,t=t>>1}else{for(t=1,e=0;e<f;e++)n=n<<1|t,s==r-1?(s=0,l.push(c(n)),n=0):s++,t=0;for(t=u.charCodeAt(0),e=0;e<16;e++)n=n<<1|t&1,s==r-1?(s=0,l.push(c(n)),n=0):s++,t=t>>1}h--,h==0&&(h=Math.pow(2,f),f++),delete w[u]}else for(t=p[u],e=0;e<f;e++)n=n<<1|t&1,s==r-1?(s=0,l.push(c(n)),n=0):s++,t=t>>1;h--,h==0&&(h=Math.pow(2,f),f++),p[A]=a++,u=String(v)}if(u!==""){if(Object.prototype.hasOwnProperty.call(w,u)){if(u.charCodeAt(0)<256){for(e=0;e<f;e++)n=n<<1,s==r-1?(s=0,l.push(c(n)),n=0):s++;for(t=u.charCodeAt(0),e=0;e<8;e++)n=n<<1|t&1,s==r-1?(s=0,l.push(c(n)),n=0):s++,t=t>>1}else{for(t=1,e=0;e<f;e++)n=n<<1|t,s==r-1?(s=0,l.push(c(n)),n=0):s++,t=0;for(t=u.charCodeAt(0),e=0;e<16;e++)n=n<<1|t&1,s==r-1?(s=0,l.push(c(n)),n=0):s++,t=t>>1}h--,h==0&&(h=Math.pow(2,f),f++),delete w[u]}else for(t=p[u],e=0;e<f;e++)n=n<<1|t&1,s==r-1?(s=0,l.push(c(n)),n=0):s++,t=t>>1;h--,h==0&&(h=Math.pow(2,f),f++)}for(t=2,e=0;e<f;e++)n=n<<1|t&1,s==r-1?(s=0,l.push(c(n)),n=0):s++,t=t>>1;for(;;)if(n=n<<1,s==r-1){l.push(c(n));break}else s++;return l.join("")},decompress:function(o){return o==null?"":o==""?null:d._decompress(o.length,32768,function(r){return o.charCodeAt(r)})},_decompress:function(o,r,c){var e=[],t=4,p=4,w=3,v="",A=[],u,h,a,f,l,n,s,i={val:c(0),position:r,index:1};for(u=0;u<3;u+=1)e[u]=u;for(a=0,l=Math.pow(2,2),n=1;n!=l;)f=i.val&i.position,i.position>>=1,i.position==0&&(i.position=r,i.val=c(i.index++)),a|=(f>0?1:0)*n,n<<=1;switch(a){case 0:for(a=0,l=Math.pow(2,8),n=1;n!=l;)f=i.val&i.position,i.position>>=1,i.position==0&&(i.position=r,i.val=c(i.index++)),a|=(f>0?1:0)*n,n<<=1;s=_(a);break;case 1:for(a=0,l=Math.pow(2,16),n=1;n!=l;)f=i.val&i.position,i.position>>=1,i.position==0&&(i.position=r,i.val=c(i.index++)),a|=(f>0?1:0)*n,n<<=1;s=_(a);break;case 2:return""}for(e[3]=s,h=s,A.push(s);;){if(i.index>o)return"";for(a=0,l=Math.pow(2,w),n=1;n!=l;)f=i.val&i.position,i.position>>=1,i.position==0&&(i.position=r,i.val=c(i.index++)),a|=(f>0?1:0)*n,n<<=1;switch(s=a){case 0:for(a=0,l=Math.pow(2,8),n=1;n!=l;)f=i.val&i.position,i.position>>=1,i.position==0&&(i.position=r,i.val=c(i.index++)),a|=(f>0?1:0)*n,n<<=1;e[p++]=_(a),s=p-1,t--;break;case 1:for(a=0,l=Math.pow(2,16),n=1;n!=l;)f=i.val&i.position,i.position>>=1,i.position==0&&(i.position=r,i.val=c(i.index++)),a|=(f>0?1:0)*n,n<<=1;e[p++]=_(a),s=p-1,t--;break;case 2:return A.join("")}if(t==0&&(t=Math.pow(2,w),w++),e[s])v=e[s];else if(s===p)v=h+h.charAt(0);else return null;A.push(v),e[p++]=h+v.charAt(0),t--,h=v,t==0&&(t=Math.pow(2,w),w++)}}};return d}();y!=null&&(y.exports=U)})(O);var j=O.exports;export{j as L};

View file

@ -1 +0,0 @@
let h=()=>({events:{},emit(t,...e){(this.events[t]||[]).forEach(s=>s(...e))},on(t,e){return(this.events[t]=this.events[t]||[]).push(e),()=>this.events[t]=(this.events[t]||[]).filter(s=>s!==e)}});export{h as c};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1 +0,0 @@
import{E as y}from"./@socket.io.aec831e2.js";const N=typeof ArrayBuffer=="function",A=e=>typeof ArrayBuffer.isView=="function"?ArrayBuffer.isView(e):e.buffer instanceof ArrayBuffer,p=Object.prototype.toString,E=typeof Blob=="function"||typeof Blob!="undefined"&&p.call(Blob)==="[object BlobConstructor]",d=typeof File=="function"||typeof File!="undefined"&&p.call(File)==="[object FileConstructor]";function a(e){return N&&(e instanceof ArrayBuffer||A(e))||E&&e instanceof Blob||d&&e instanceof File}function u(e,r){if(!e||typeof e!="object")return!1;if(Array.isArray(e)){for(let t=0,n=e.length;t<n;t++)if(u(e[t]))return!0;return!1}if(a(e))return!0;if(e.toJSON&&typeof e.toJSON=="function"&&arguments.length===1)return u(e.toJSON(),!0);for(const t in e)if(Object.prototype.hasOwnProperty.call(e,t)&&u(e[t]))return!0;return!1}function g(e){const r=[],t=e.data,n=e;return n.data=f(t,r),n.attachments=r.length,{packet:n,buffers:r}}function f(e,r){if(!e)return e;if(a(e)){const t={_placeholder:!0,num:r.length};return r.push(e),t}else if(Array.isArray(e)){const t=new Array(e.length);for(let n=0;n<e.length;n++)t[n]=f(e[n],r);return t}else if(typeof e=="object"&&!(e instanceof Date)){const t={};for(const n in e)Object.prototype.hasOwnProperty.call(e,n)&&(t[n]=f(e[n],r));return t}return e}function w(e,r){return e.data=l(e.data,r),e.attachments=void 0,e}function l(e,r){if(!e)return e;if(e&&e._placeholder===!0){if(typeof e.num=="number"&&e.num>=0&&e.num<r.length)return r[e.num];throw new Error("illegal attachments")}else if(Array.isArray(e))for(let t=0;t<e.length;t++)e[t]=l(e[t],r);else if(typeof e=="object")for(const t in e)Object.prototype.hasOwnProperty.call(e,t)&&(e[t]=l(e[t],r));return e}const B=5;var i;(function(e){e[e.CONNECT=0]="CONNECT",e[e.DISCONNECT=1]="DISCONNECT",e[e.EVENT=2]="EVENT",e[e.ACK=3]="ACK",e[e.CONNECT_ERROR=4]="CONNECT_ERROR",e[e.BINARY_EVENT=5]="BINARY_EVENT",e[e.BINARY_ACK=6]="BINARY_ACK"})(i||(i={}));class C{constructor(r){this.replacer=r}encode(r){return(r.type===i.EVENT||r.type===i.ACK)&&u(r)?(r.type=r.type===i.EVENT?i.BINARY_EVENT:i.BINARY_ACK,this.encodeAsBinary(r)):[this.encodeAsString(r)]}encodeAsString(r){let t=""+r.type;return(r.type===i.BINARY_EVENT||r.type===i.BINARY_ACK)&&(t+=r.attachments+"-"),r.nsp&&r.nsp!=="/"&&(t+=r.nsp+","),r.id!=null&&(t+=r.id),r.data!=null&&(t+=JSON.stringify(r.data,this.replacer)),t}encodeAsBinary(r){const t=g(r),n=this.encodeAsString(t.packet),o=t.buffers;return o.unshift(n),o}}class h extends y{constructor(r){super(),this.reviver=r}add(r){let t;if(typeof r=="string"){if(this.reconstructor)throw new Error("got plaintext data when reconstructing a packet");t=this.decodeString(r),t.type===i.BINARY_EVENT||t.type===i.BINARY_ACK?(this.reconstructor=new R(t),t.attachments===0&&super.emitReserved("decoded",t)):super.emitReserved("decoded",t)}else if(a(r)||r.base64)if(this.reconstructor)t=this.reconstructor.takeBinaryData(r),t&&(this.reconstructor=null,super.emitReserved("decoded",t));else throw new Error("got binary data when not reconstructing a packet");else throw new Error("Unknown type: "+r)}decodeString(r){let t=0;const n={type:Number(r.charAt(0))};if(i[n.type]===void 0)throw new Error("unknown packet type "+n.type);if(n.type===i.BINARY_EVENT||n.type===i.BINARY_ACK){const s=t+1;for(;r.charAt(++t)!=="-"&&t!=r.length;);const c=r.substring(s,t);if(c!=Number(c)||r.charAt(t)!=="-")throw new Error("Illegal attachments");n.attachments=Number(c)}if(r.charAt(t+1)==="/"){const s=t+1;for(;++t&&!(r.charAt(t)===","||t===r.length););n.nsp=r.substring(s,t)}else n.nsp="/";const o=r.charAt(t+1);if(o!==""&&Number(o)==o){const s=t+1;for(;++t;){const c=r.charAt(t);if(c==null||Number(c)!=c){--t;break}if(t===r.length)break}n.id=Number(r.substring(s,t+1))}if(r.charAt(++t)){const s=this.tryParse(r.substr(t));if(h.isPayloadValid(n.type,s))n.data=s;else throw new Error("invalid payload")}return n}tryParse(r){try{return JSON.parse(r,this.reviver)}catch{return!1}}static isPayloadValid(r,t){switch(r){case i.CONNECT:return typeof t=="object";case i.DISCONNECT:return t===void 0;case i.CONNECT_ERROR:return typeof t=="string"||typeof t=="object";case i.EVENT:case i.BINARY_EVENT:return Array.isArray(t)&&t.length>0;case i.ACK:case i.BINARY_ACK:return Array.isArray(t)}}destroy(){this.reconstructor&&this.reconstructor.finishedReconstruction()}}class R{constructor(r){this.packet=r,this.buffers=[],this.reconPack=r}takeBinaryData(r){if(this.buffers.push(r),this.buffers.length===this.reconPack.attachments){const t=w(this.reconPack,this.buffers);return this.finishedReconstruction(),t}return null}finishedReconstruction(){this.reconPack=null,this.buffers=[]}}var O=Object.freeze(Object.defineProperty({__proto__:null,protocol:B,get PacketType(){return i},Encoder:C,Decoder:h},Symbol.toStringTag,{value:"Module"}));export{i as P,O as p};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1 +0,0 @@
.icon.delete{display:flex;justify-content:center;align-items:center;padding:0;margin:0;border:none;background:none;height:8px;width:8px;min-height:8px;min-width:8px;max-height:8px;max-width:8px;cursor:pointer}.icon.arrow-downward{color:#999;border-style:solid;border-width:4px 4px 0;border-color:#999 transparent transparent;content:"";transition:transform .2s linear;cursor:pointer}.icon.arrow-downward.active{transform:rotate(180deg)}.vue-select{position:relative;display:flex;align-items:flex-start;justify-content:flex-start;flex-direction:column;width:150px;border-radius:4px;border:1px solid #999;box-sizing:border-box;outline:none}.vue-select[aria-disabled=true]{background-color:#efefef}.vue-select[aria-disabled=true] *,.vue-select[aria-disabled=true] input{cursor:not-allowed}.vue-select-header{display:flex;width:100%;align-items:center;justify-content:space-between}.vue-select-header .icon.loading,.vue-select-header .icon.arrow-downward{margin-right:4px}.vue-tags{display:flex;flex-wrap:wrap;margin:0;padding:2px;min-height:calc(1rem + 4px);user-select:none}.vue-tags.collapsed{flex-wrap:nowrap;overflow:auto}.vue-tag{display:none;align-items:center;justify-content:center;list-style-type:none;border-radius:4px;background-color:#999;padding:0 4px;margin:2px;min-height:1rem;font-size:.8rem}.vue-tag span{margin-right:4px}.vue-tag.selected{display:flex;align-items:center;justify-content:center;background-color:#999;border-radius:4px;padding:0 4px;font-size:.8rem}.vue-tags[data-removable=false] .vue-tag.selected img:hover{cursor:not-allowed}.vue-select-input-wrapper{position:relative;display:flex;width:100%;align-items:center;justify-content:space-between}.vue-select-input-wrapper .icon.loading{margin-right:4px}.vue-input{display:inline-flex;align-items:center;border-radius:4px;border:none;outline:none;max-width:100%;min-width:0;width:100%;box-sizing:border-box;padding:4px}.vue-select[data-is-focusing=false][aria-disabled=false] .vue-input input,input[readonly]{cursor:default}.vue-input input{border:none;outline:none;width:100%;min-width:0;font-size:.8rem;padding:0}.vue-input input[disabled]{background-color:#efefef}.vue-input input[readonly],.vue-select-header .vue-input input[disabled]{background-color:unset}.vue-dropdown{display:none;position:absolute;background-color:#fff;z-index:1;overflow-y:auto;width:100%;min-width:0;margin:0;padding:0;left:-1px;box-sizing:content-box;border:1px solid #999;list-style-type:none}.vue-select[aria-expanded=true] .vue-dropdown{display:unset}.vue-dropdown[data-visible-length="0"]{border:none}.vue-dropdown-item{list-style-type:none;padding:4px;cursor:pointer;min-height:1rem}.vue-dropdown-item.highlighted{background-color:#41b883}.vue-dropdown-item.disabled{background-color:#efefef;cursor:not-allowed}.vue-dropdown-item.selected{background-color:#f3f3f3}.vue-dropdown-item.selected.highlighted{background-color:#ff6a6a}.vue-dropdown[data-removable=false] .vue-dropdown-item.selected:hover{cursor:not-allowed}.vue-dropdown[data-addable=false][data-multiple=true] .vue-dropdown-item:not(.selected):hover{cursor:not-allowed}.icon.loading{display:inline-block;position:relative;width:8px;min-width:8px;height:8px;min-height:8px}.icon.loading div{box-sizing:border-box;display:block;position:absolute;border:1px solid #999;width:8px;height:8px;border-radius:50%;animation:loading 1s cubic-bezier(.5,0,.5,1) infinite;border-color:#999 transparent transparent transparent}.icon.loading div:nth-child(1){animation-delay:-.08s}.icon.loading div:nth-child(2){animation-delay:-.16s}@keyframes loading{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.inline-flex{display:inline-flex}.vue-select[aria-expanded=true].direction-bottom{border-bottom-left-radius:0;border-bottom-right-radius:0}.vue-select[aria-expanded=true].direction-top{border-top-left-radius:0;border-top-right-radius:0}.vue-select.direction-top .vue-dropdown{bottom:100%;border-top-left-radius:3px;border-top-right-radius:3px}.vue-select.direction-bottom .vue-dropdown{top:100%;border-bottom-left-radius:3px;border-bottom-right-radius:3px}

File diff suppressed because one or more lines are too long

View file

@ -1,5 +0,0 @@
/*!
* vue-textarea-autosize v1.1.1
* (c) 2019 Saymon
* Released under the MIT License.
*/var p={name:"TextareaAutosize",props:{value:{type:[String,Number],default:""},autosize:{type:Boolean,default:!0},minHeight:{type:[Number],default:null},maxHeight:{type:[Number],default:null},important:{type:[Boolean,Array],default:!1}},data:function(){return{val:null,maxHeightScroll:!1,height:"auto"}},computed:{computedStyles:function(){return this.autosize?{resize:this.isResizeImportant?"none !important":"none",height:this.height,overflow:this.maxHeightScroll?"auto":this.isOverflowImportant?"hidden !important":"hidden"}:{}},isResizeImportant:function(){var e=this.important;return e===!0||Array.isArray(e)&&e.includes("resize")},isOverflowImportant:function(){var e=this.important;return e===!0||Array.isArray(e)&&e.includes("overflow")},isHeightImportant:function(){var e=this.important;return e===!0||Array.isArray(e)&&e.includes("height")}},watch:{value:function(e){this.val=e},val:function(e){this.$nextTick(this.resize),this.$emit("input",e)},minHeight:function(){this.$nextTick(this.resize)},maxHeight:function(){this.$nextTick(this.resize)},autosize:function(e){e&&this.resize()}},methods:{resize:function(){var e=this,a=this.isHeightImportant?"important":"";return this.height="auto".concat(a?" !important":""),this.$nextTick(function(){var i=e.$el.scrollHeight+1;e.minHeight&&(i=i<e.minHeight?e.minHeight:i),e.maxHeight&&(i>e.maxHeight?(i=e.maxHeight,e.maxHeightScroll=!0):e.maxHeightScroll=!1);var u=i+"px";e.height="".concat(u).concat(a?" !important":"")}),this}},created:function(){this.val=this.value},mounted:function(){this.resize()}};function g(t,e,a,i,u,f,s,l,m,d){typeof s!="boolean"&&(m=l,l=s,s=!1);var n=typeof a=="function"?a.options:a;t&&t.render&&(n.render=t.render,n.staticRenderFns=t.staticRenderFns,n._compiled=!0,u&&(n.functional=!0)),i&&(n._scopeId=i);var o;if(f?(o=function(r){r=r||this.$vnode&&this.$vnode.ssrContext||this.parent&&this.parent.$vnode&&this.parent.$vnode.ssrContext,!r&&typeof __VUE_SSR_CONTEXT__!="undefined"&&(r=__VUE_SSR_CONTEXT__),e&&e.call(this,m(r)),r&&r._registeredComponents&&r._registeredComponents.add(f)},n._ssrRegister=o):e&&(o=s?function(){e.call(this,d(this.$root.$options.shadowRoot))}:function(h){e.call(this,l(h))}),o)if(n.functional){var v=n.render;n.render=function(r,c){return o.call(c),v(r,c)}}else{var _=n.beforeCreate;n.beforeCreate=_?[].concat(_,o):[o]}return a}var z=g;const H=p;var y=function(){var t=this,e=t.$createElement,a=t._self._c||e;return a("textarea",{directives:[{name:"model",rawName:"v-model",value:t.val,expression:"val"}],style:t.computedStyles,domProps:{value:t.val},on:{focus:t.resize,input:function(i){i.target.composing||(t.val=i.target.value)}}})},x=[];const $=void 0,w=void 0,S=void 0,T=!1;var A=z({render:y,staticRenderFns:x},$,H,w,T,S,void 0,void 0),C="1.1.1",R=function(e){e.component("TextareaAutosize",A)},b={install:R,version:C};typeof window!="undefined"&&window.Vue&&window.Vue.use(b);export{b as p};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1 +0,0 @@
import{g as u}from"./lru-cache.9506e0ec.js";import{r as c,i as f,N as n,e as p,c as S,a as h,E as v,R,b as C,d as y,f as g,g as w,h as b,j as E,k as T,l as x,m as k,n as M,o as D,p as P,q as V,s as A,t as B,u as H,v as N,w as F,x as U,y as z,z as I,A as K,B as O,C as j,D as _,F as q,G as W,H as L,I as G,J as $,K as J,L as Q,M as X,O as Y,P as Z,S as ee,Q as ae,T as se,U as te,V as oe,W as re,X as ne,Y as ie,Z as ce,_ as le,$ as de,a0 as me,a1 as ue,a2 as fe,a3 as pe,a4 as Se,a5 as he,a6 as ve,a7 as Re,a8 as Ce,a9 as ye,aa as ge,ab as we,ac as be,ad as Ee,ae as Te,af as xe,ag as ke,ah as Me,ai as De,aj as Pe,ak as Ve,al as Ae,am as Be,an as He,ao as Ne,ap as Fe,aq as Ue,ar as ze,as as Ie,at as Ke,au as Oe,av as je,aw as _e,ax as qe,ay as We,az as Le,aA as Ge,aB as $e,aC as Je,aD as Qe,aE as Xe,aF as Ye,aG as Ze,aH as ea,aI as aa,aJ as sa,aK as ta,aL as oa,aM as ra,aN as na,aO as ia,aP as ca,aQ as la,aR as da,aS as ma,aT as ua,aU as fa,aV as pa,aW as Sa,aX as ha,aY as va,aZ as Ra,a_ as Ca,a$ as ya,b0 as ga,b1 as wa,b2 as ba,b3 as Ea,b4 as Ta,b5 as xa,b6 as ka,b7 as Ma,b8 as Da,b9 as Pa,ba as Va,bb as Aa,bc as Ba,bd as Ha,be as Na,bf as Fa,bg as Ua,bh as za,bi as Ia,bj as Ka,bk as Oa,bl as ja,bm as _a,bn as qa,bo as Wa,bp as La,bq as Ga,br as $a,bs as Ja,bt as Qa,bu as Xa}from"./@vue.8948d9b0.js";const i=Object.create(null);function l(e,d){if(!f(e))if(e.nodeType)e=e.innerHTML;else return n;const t=e,o=i[t];if(o)return o;if(e[0]==="#"){const a=document.querySelector(e);e=a?a.innerHTML:""}const s=p({hoistStatic:!0,onError:void 0,onWarn:n},d);!s.isCustomElement&&typeof customElements!="undefined"&&(s.isCustomElement=a=>!!customElements.get(a));const{code:m}=S(e,s),r=new Function("Vue",m)(h);return r._rc=!0,i[t]=r}c(l);var Ya=Object.freeze(Object.defineProperty({__proto__:null,compile:l,EffectScope:v,ReactiveEffect:R,customRef:C,effect:y,effectScope:g,getCurrentScope:w,isProxy:b,isReactive:E,isReadonly:T,isRef:x,isShallow:k,markRaw:M,onScopeDispose:D,proxyRefs:P,reactive:V,readonly:A,ref:B,shallowReactive:H,shallowReadonly:N,shallowRef:F,stop:U,toRaw:z,toRef:I,toRefs:K,triggerRef:O,unref:j,camelize:_,capitalize:q,normalizeClass:W,normalizeProps:L,normalizeStyle:G,toDisplayString:$,toHandlerKey:J,BaseTransition:Q,Comment:X,Fragment:Y,KeepAlive:Z,Static:ee,Suspense:ae,Teleport:se,Text:te,callWithAsyncErrorHandling:oe,callWithErrorHandling:re,cloneVNode:ne,compatUtils:ie,computed:ce,createBlock:le,createCommentVNode:de,createElementBlock:me,createElementVNode:ue,createHydrationRenderer:fe,createPropsRestProxy:pe,createRenderer:Se,createSlots:he,createStaticVNode:ve,createTextVNode:Re,createVNode:Ce,defineAsyncComponent:ye,defineComponent:ge,defineEmits:we,defineExpose:be,defineProps:Ee,get devtools(){return Te},getCurrentInstance:xe,getTransitionRawChildren:ke,guardReactiveProps:Me,h:De,handleError:Pe,initCustomFormatter:Ve,inject:Ae,isMemoSame:Be,isRuntimeOnly:He,isVNode:Ne,mergeDefaults:Fe,mergeProps:Ue,nextTick:ze,onActivated:Ie,onBeforeMount:Ke,onBeforeUnmount:Oe,onBeforeUpdate:je,onDeactivated:_e,onErrorCaptured:qe,onMounted:We,onRenderTracked:Le,onRenderTriggered:Ge,onServerPrefetch:$e,onUnmounted:Je,onUpdated:Qe,openBlock:Xe,popScopeId:Ye,provide:Ze,pushScopeId:ea,queuePostFlushCb:aa,registerRuntimeCompiler:c,renderList:sa,renderSlot:ta,resolveComponent:oa,resolveDirective:ra,resolveDynamicComponent:na,resolveFilter:ia,resolveTransitionHooks:ca,setBlockTracking:la,setDevtoolsHook:da,setTransitionHooks:ma,ssrContextKey:ua,ssrUtils:fa,toHandlers:pa,transformVNodeArgs:Sa,useAttrs:ha,useSSRContext:va,useSlots:Ra,useTransitionState:Ca,version:ya,warn:ga,watch:wa,watchEffect:ba,watchPostEffect:Ea,watchSyncEffect:Ta,withAsyncContext:xa,withCtx:ka,withDefaults:Ma,withDirectives:Da,withMemo:Pa,withScopeId:Va,Transition:Aa,TransitionGroup:Ba,VueElement:Ha,createApp:Na,createSSRApp:Fa,defineCustomElement:Ua,defineSSRCustomElement:za,hydrate:Ia,initDirectivesForSSR:Ka,render:Oa,useCssModule:ja,useCssVars:_a,vModelCheckbox:qa,vModelDynamic:Wa,vModelRadio:La,vModelSelect:Ga,vModelText:$a,vShow:Ja,withKeys:Qa,withModifiers:Xa},Symbol.toStringTag,{value:"Module"})),as=u(Ya);export{as as r};

File diff suppressed because one or more lines are too long

View file

@ -1,2 +0,0 @@
try{self["workbox:window:6.5.3"]&&_()}catch{}function S(t,r){return new Promise(function(e){var o=new MessageChannel;o.port1.onmessage=function(v){e(v.data)},t.postMessage(r,[o.port2])})}function L(t,r){for(var e=0;e<r.length;e++){var o=r[e];o.enumerable=o.enumerable||!1,o.configurable=!0,"value"in o&&(o.writable=!0),Object.defineProperty(t,o.key,o)}}function E(t,r){(r==null||r>t.length)&&(r=t.length);for(var e=0,o=new Array(r);e<r;e++)o[e]=t[e];return o}function W(t,r){var e;if(typeof Symbol=="undefined"||t[Symbol.iterator]==null){if(Array.isArray(t)||(e=function(v,d){if(v){if(typeof v=="string")return E(v,d);var h=Object.prototype.toString.call(v).slice(8,-1);return h==="Object"&&v.constructor&&(h=v.constructor.name),h==="Map"||h==="Set"?Array.from(v):h==="Arguments"||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(h)?E(v,d):void 0}}(t))||r&&t&&typeof t.length=="number"){e&&(t=e);var o=0;return function(){return o>=t.length?{done:!0}:{done:!1,value:t[o++]}}}throw new TypeError(`Invalid attempt to iterate non-iterable instance.
In order to be iterable, non-array objects must have a [Symbol.iterator]() method.`)}return(e=t[Symbol.iterator]()).next.bind(e)}try{self["workbox:core:6.5.3"]&&_()}catch{}var y=function(){var t=this;this.promise=new Promise(function(r,e){t.resolve=r,t.reject=e})};function b(t,r){var e=location.href;return new URL(t,e).href===new URL(r,e).href}var g=function(t,r){this.type=t,Object.assign(this,r)};function p(t,r,e){return e?r?r(t):t:(t&&t.then||(t=Promise.resolve(t)),r?t.then(r):t)}function j(){}var k={type:"SKIP_WAITING"};function P(t,r){if(!r)return t&&t.then?t.then(j):Promise.resolve()}var U=function(t){var r,e;function o(f,c){var n,i;return c===void 0&&(c={}),(n=t.call(this)||this).nn={},n.tn=0,n.rn=new y,n.en=new y,n.on=new y,n.un=0,n.an=new Set,n.cn=function(){var s=n.fn,a=s.installing;n.tn>0||!b(a.scriptURL,n.sn.toString())||performance.now()>n.un+6e4?(n.vn=a,s.removeEventListener("updatefound",n.cn)):(n.hn=a,n.an.add(a),n.rn.resolve(a)),++n.tn,a.addEventListener("statechange",n.ln)},n.ln=function(s){var a=n.fn,u=s.target,l=u.state,m=u===n.vn,w={sw:u,isExternal:m,originalEvent:s};!m&&n.mn&&(w.isUpdate=!0),n.dispatchEvent(new g(l,w)),l==="installed"?n.wn=self.setTimeout(function(){l==="installed"&&a.waiting===u&&n.dispatchEvent(new g("waiting",w))},200):l==="activating"&&(clearTimeout(n.wn),m||n.en.resolve(u))},n.dn=function(s){var a=n.hn,u=a!==navigator.serviceWorker.controller;n.dispatchEvent(new g("controlling",{isExternal:u,originalEvent:s,sw:a,isUpdate:n.mn})),u||n.on.resolve(a)},n.gn=(i=function(s){var a=s.data,u=s.ports,l=s.source;return p(n.getSW(),function(){n.an.has(l)&&n.dispatchEvent(new g("message",{data:a,originalEvent:s,ports:u,sw:l}))})},function(){for(var s=[],a=0;a<arguments.length;a++)s[a]=arguments[a];try{return Promise.resolve(i.apply(this,s))}catch(u){return Promise.reject(u)}}),n.sn=f,n.nn=c,navigator.serviceWorker.addEventListener("message",n.gn),n}e=t,(r=o).prototype=Object.create(e.prototype),r.prototype.constructor=r,r.__proto__=e;var v,d,h=o.prototype;return h.register=function(f){var c=(f===void 0?{}:f).immediate,n=c!==void 0&&c;try{var i=this;return function(s,a){var u=s();return u&&u.then?u.then(a):a(u)}(function(){if(!n&&document.readyState!=="complete")return P(new Promise(function(s){return window.addEventListener("load",s)}))},function(){return i.mn=Boolean(navigator.serviceWorker.controller),i.yn=i.pn(),p(i.bn(),function(s){i.fn=s,i.yn&&(i.hn=i.yn,i.en.resolve(i.yn),i.on.resolve(i.yn),i.yn.addEventListener("statechange",i.ln,{once:!0}));var a=i.fn.waiting;return a&&b(a.scriptURL,i.sn.toString())&&(i.hn=a,Promise.resolve().then(function(){i.dispatchEvent(new g("waiting",{sw:a,wasWaitingBeforeRegister:!0}))}).then(function(){})),i.hn&&(i.rn.resolve(i.hn),i.an.add(i.hn)),i.fn.addEventListener("updatefound",i.cn),navigator.serviceWorker.addEventListener("controllerchange",i.dn),i.fn})})}catch(s){return Promise.reject(s)}},h.update=function(){try{return this.fn?P(this.fn.update()):void 0}catch(f){return Promise.reject(f)}},h.getSW=function(){return this.hn!==void 0?Promise.resolve(this.hn):this.rn.promise},h.messageSW=function(f){try{return p(this.getSW(),function(c){return S(c,f)})}catch(c){return Promise.reject(c)}},h.messageSkipWaiting=function(){this.fn&&this.fn.waiting&&S(this.fn.waiting,k)},h.pn=function(){var f=navigator.serviceWorker.controller;return f&&b(f.scriptURL,this.sn.toString())?f:void 0},h.bn=function(){try{var f=this;return function(c,n){try{var i=c()}catch(s){return n(s)}return i&&i.then?i.then(void 0,n):i}(function(){return p(navigator.serviceWorker.register(f.sn,f.nn),function(c){return f.un=performance.now(),c})},function(c){throw c})}catch(c){return Promise.reject(c)}},v=o,(d=[{key:"active",get:function(){return this.en.promise}},{key:"controlling",get:function(){return this.on.promise}}])&&L(v.prototype,d),o}(function(){function t(){this.Pn=new Map}var r=t.prototype;return r.addEventListener=function(e,o){this.Sn(e).add(o)},r.removeEventListener=function(e,o){this.Sn(e).delete(o)},r.dispatchEvent=function(e){e.target=this;for(var o,v=W(this.Sn(e.type));!(o=v()).done;)(0,o.value)(e)},r.Sn=function(e){return this.Pn.has(e)||this.Pn.set(e,new Set),this.Pn.get(e)},t}());export{S as n,U as v};

View file

@ -1 +0,0 @@
var f=r;r.Node=s;r.create=r;function r(t){var i=this;if(i instanceof r||(i=new r),i.tail=null,i.head=null,i.length=0,t&&typeof t.forEach=="function")t.forEach(function(n){i.push(n)});else if(arguments.length>0)for(var e=0,h=arguments.length;e<h;e++)i.push(arguments[e]);return i}r.prototype.removeNode=function(t){if(t.list!==this)throw new Error("removing node which does not belong to this list");var i=t.next,e=t.prev;return i&&(i.prev=e),e&&(e.next=i),t===this.head&&(this.head=i),t===this.tail&&(this.tail=e),t.list.length--,t.next=null,t.prev=null,t.list=null,i};r.prototype.unshiftNode=function(t){if(t!==this.head){t.list&&t.list.removeNode(t);var i=this.head;t.list=this,t.next=i,i&&(i.prev=t),this.head=t,this.tail||(this.tail=t),this.length++}};r.prototype.pushNode=function(t){if(t!==this.tail){t.list&&t.list.removeNode(t);var i=this.tail;t.list=this,t.prev=i,i&&(i.next=t),this.tail=t,this.head||(this.head=t),this.length++}};r.prototype.push=function(){for(var t=0,i=arguments.length;t<i;t++)a(this,arguments[t]);return this.length};r.prototype.unshift=function(){for(var t=0,i=arguments.length;t<i;t++)v(this,arguments[t]);return this.length};r.prototype.pop=function(){if(!!this.tail){var t=this.tail.value;return this.tail=this.tail.prev,this.tail?this.tail.next=null:this.head=null,this.length--,t}};r.prototype.shift=function(){if(!!this.head){var t=this.head.value;return this.head=this.head.next,this.head?this.head.prev=null:this.tail=null,this.length--,t}};r.prototype.forEach=function(t,i){i=i||this;for(var e=this.head,h=0;e!==null;h++)t.call(i,e.value,h,this),e=e.next};r.prototype.forEachReverse=function(t,i){i=i||this;for(var e=this.tail,h=this.length-1;e!==null;h--)t.call(i,e.value,h,this),e=e.prev};r.prototype.get=function(t){for(var i=0,e=this.head;e!==null&&i<t;i++)e=e.next;if(i===t&&e!==null)return e.value};r.prototype.getReverse=function(t){for(var i=0,e=this.tail;e!==null&&i<t;i++)e=e.prev;if(i===t&&e!==null)return e.value};r.prototype.map=function(t,i){i=i||this;for(var e=new r,h=this.head;h!==null;)e.push(t.call(i,h.value,this)),h=h.next;return e};r.prototype.mapReverse=function(t,i){i=i||this;for(var e=new r,h=this.tail;h!==null;)e.push(t.call(i,h.value,this)),h=h.prev;return e};r.prototype.reduce=function(t,i){var e,h=this.head;if(arguments.length>1)e=i;else if(this.head)h=this.head.next,e=this.head.value;else throw new TypeError("Reduce of empty list with no initial value");for(var n=0;h!==null;n++)e=t(e,h.value,n),h=h.next;return e};r.prototype.reduceReverse=function(t,i){var e,h=this.tail;if(arguments.length>1)e=i;else if(this.tail)h=this.tail.prev,e=this.tail.value;else throw new TypeError("Reduce of empty list with no initial value");for(var n=this.length-1;h!==null;n--)e=t(e,h.value,n),h=h.prev;return e};r.prototype.toArray=function(){for(var t=new Array(this.length),i=0,e=this.head;e!==null;i++)t[i]=e.value,e=e.next;return t};r.prototype.toArrayReverse=function(){for(var t=new Array(this.length),i=0,e=this.tail;e!==null;i++)t[i]=e.value,e=e.prev;return t};r.prototype.slice=function(t,i){i=i||this.length,i<0&&(i+=this.length),t=t||0,t<0&&(t+=this.length);var e=new r;if(i<t||i<0)return e;t<0&&(t=0),i>this.length&&(i=this.length);for(var h=0,n=this.head;n!==null&&h<t;h++)n=n.next;for(;n!==null&&h<i;h++,n=n.next)e.push(n.value);return e};r.prototype.sliceReverse=function(t,i){i=i||this.length,i<0&&(i+=this.length),t=t||0,t<0&&(t+=this.length);var e=new r;if(i<t||i<0)return e;t<0&&(t=0),i>this.length&&(i=this.length);for(var h=this.length,n=this.tail;n!==null&&h>i;h--)n=n.prev;for(;n!==null&&h>t;h--,n=n.prev)e.push(n.value);return e};r.prototype.splice=function(t,i,...e){t>this.length&&(t=this.length-1),t<0&&(t=this.length+t);for(var h=0,n=this.head;n!==null&&h<t;h++)n=n.next;for(var u=[],h=0;n&&h<i;h++)u.push(n.value),n=this.removeNode(n);n===null&&(n=this.tail),n!==this.head&&n!==this.tail&&(n=n.prev);for(var h=0;h<e.length;h++)n=l(this,n,e[h]);return u};r.prototype.reverse=function(){for(var t=this.head,i=this.tail,e=t;e!==null;e=e.prev){var h=e.prev;e.prev=e.next,e.next=h}return this.head=i,this.tail=t,this};function l(t,i,e){var h=i===t.head?new s(e,null,i,t):new s(e,i,i.next,t);return h.next===null&&(t.tail=h),h.prev===null&&(t.head=h),t.length++,h}function a(t,i){t.tail=new s(i,t.tail,null,t),t.head||(t.head=t.tail),t.length++}function v(t,i){t.head=new s(i,null,t.head,t),t.tail||(t.tail=t.head),t.length++}function s(t,i,e,h){if(!(this instanceof s))return new s(t,i,e,h);this.list=h,this.value=t,i?(i.next=this,this.prev=i):this.prev=null,e?(e.prev=this,this.next=e):this.next=null}try{require("./iterator.js")(r)}catch{}export{f as y};

View file

@ -5,44 +5,20 @@
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<link rel="apple-touch-icon" sizes="180x180" href="./apple-touch-icon.png">
<link rel="icon" type="image/svg+xml" href="./favicon.svg">
<link rel="alternate icon" type="image/png" sizes="48x48" href="./favicon.ico">
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
<link rel="icon" type="image/svg+xml" href="/favicon.svg">
<link rel="alternate icon" type="image/png" sizes="48x48" href="/favicon.ico">
<meta name="theme-color" content="#2E3440">
<title>Profectus</title>
<meta name="description" content="A project made in Profectus"/>
<script type="module" crossorigin src="./assets/index.609ecad2.js"></script>
<link rel="modulepreload" href="./assets/yallist.fd762fe7.js">
<link rel="modulepreload" href="./assets/lru-cache.9506e0ec.js">
<link rel="modulepreload" href="./assets/@vue.8948d9b0.js">
<link rel="modulepreload" href="./assets/vue.228877f7.js">
<link rel="modulepreload" href="./assets/lz-string.f2f3b7cf.js">
<link rel="modulepreload" href="./assets/nanoevents.1080beb7.js">
<link rel="modulepreload" href="./assets/engine.io-parser.730afdce.js">
<link rel="modulepreload" href="./assets/@socket.io.aec831e2.js">
<link rel="modulepreload" href="./assets/engine.io-client.6ba5801d.js">
<link rel="modulepreload" href="./assets/socket.io-parser.0ab387d5.js">
<link rel="modulepreload" href="./assets/socket.io-client.03bb8f3a.js">
<link rel="modulepreload" href="./assets/unique-names-generator.9178d3e3.js">
<link rel="modulepreload" href="./assets/vue-toastification.97914fdb.js">
<link rel="modulepreload" href="./assets/semver.334eb41f.js">
<link rel="modulepreload" href="./assets/vue-textarea-autosize.35804eaf.js">
<link rel="modulepreload" href="./assets/vue-next-select.f2be13cc.js">
<link rel="modulepreload" href="./assets/sortablejs.692999e9.js">
<link rel="modulepreload" href="./assets/vuedraggable.5218041c.js">
<link rel="modulepreload" href="./assets/workbox-window.8d14e8b7.js">
<link rel="stylesheet" href="./assets/vue-toastification.4b5f8ac8.css">
<link rel="stylesheet" href="./assets/vue-next-select.9e6f4164.css">
<link rel="stylesheet" href="./assets/index.2e66f9e5.css">
<link rel="stylesheet" href="./assets/@fontsource.f66d05e7.css">
<link rel="manifest" href="./manifest.webmanifest"></head>
</head>
<body>
<noscript>
<strong>We're sorry but Profectus doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<script type="module" src="./src/main.ts"></script>
</body>
</html>

View file

@ -1 +0,0 @@
{"name":"Chromatic Latice","short_name":"Chromatic Latice","start_url":"./","display":"standalone","background_color":"#ffffff","lang":"en","scope":"./","description":"A multiplayer game about light and hexagons","theme_color":"#2E3440","icons":[{"src":"pwa-192x192.png","sizes":"192x192","type":"image/png"},{"src":"pwa-512x512.png","sizes":"512x512","type":"image/png"},{"src":"pwa-512x512.png","sizes":"512x512","type":"image/png","purpose":"any maskable"}]}

14080
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

61
package.json Normal file
View file

@ -0,0 +1,61 @@
{
"name": "profectus",
"private": true,
"scripts": {
"start": "vite",
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"test": "vitest run",
"testw": "vitest",
"serve": "vite preview --host"
},
"dependencies": {
"@fontsource/material-icons": "^4.5.4",
"@fontsource/roboto-mono": "^4.5.8",
"@pixi/app": "~6.3.2",
"@pixi/constants": "~6.3.2",
"@pixi/core": "~6.3.2",
"@pixi/display": "~6.3.2",
"@pixi/math": "~6.3.2",
"@pixi/particle-emitter": "^5.0.7",
"@pixi/sprite": "~6.3.2",
"@pixi/ticker": "~6.3.2",
"@vitejs/plugin-vue": "^2.3.3",
"@vitejs/plugin-vue-jsx": "^1.3.10",
"is-plain-object": "^5.0.0",
"lz-string": "^1.4.4",
"nanoevents": "^6.0.2",
"semver": "^7.3.7",
"socket.io-client": "^4.5.2",
"unique-names-generator": "^4.7.1",
"vite": "^2.9.12",
"vite-plugin-pwa": "^0.12.0",
"vite-tsconfig-paths": "^3.5.0",
"vue": "^3.2.26",
"vue-next-select": "^2.10.2",
"vue-panzoom": "https://github.com/thepaperpilot/vue-panzoom.git",
"vue-textarea-autosize": "^1.1.1",
"vue-toastification": "^2.0.0-rc.1",
"vue-transition-expand": "^0.1.0",
"vuedraggable": "^4.1.0"
},
"devDependencies": {
"@ivanv/vue-collapse-transition": "^1.0.2",
"@rushstack/eslint-patch": "^1.1.0",
"@types/lz-string": "^1.3.34",
"@types/semver": "^7.3.12",
"@vue/eslint-config-prettier": "^7.0.0",
"@vue/eslint-config-typescript": "^10.0.0",
"eslint": "^8.6.0",
"jsdom": "^20.0.0",
"prettier": "^2.5.1",
"typescript": "^5.0.2",
"unplugin-json-dts": "^1.2.0",
"vitest": "^0.29.3",
"vue-tsc": "^0.38.1"
},
"engines": {
"node": "19.x"
}
}

View file

Before

Width:  |  Height:  |  Size: 9.5 KiB

After

Width:  |  Height:  |  Size: 9.5 KiB

View file

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View file

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View file

Before

Width:  |  Height:  |  Size: 10 KiB

After

Width:  |  Height:  |  Size: 10 KiB

View file

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 38 KiB

7
replit.nix Normal file
View file

@ -0,0 +1,7 @@
{ pkgs }: {
deps = [
pkgs.nodejs-16_x
pkgs.nodePackages.typescript-language-server
pkgs.nodePackages.npm
];
}

0
saves/.placehold Normal file
View file

71
src/App.vue Normal file
View file

@ -0,0 +1,71 @@
<template>
<div v-if="appErrors.length > 0" class="error-container" :style="theme"><Error :errors="appErrors" /></div>
<template v-else>
<div id="modal-root" :style="theme" />
<div class="app" :style="theme" :class="{ useHeader }">
<Nav v-if="useHeader" />
<Game />
<TPS v-if="unref(showTPS)" />
<GameOverScreen />
<NaNScreen />
<component :is="gameComponent" />
</div>
</template>
</template>
<script setup lang="tsx">
import "@fontsource/roboto-mono";
import Error from "components/Error.vue";
import { jsx } from "features/feature";
import state from "game/state";
import { coerceComponent, render } from "util/vue";
import { CSSProperties, watch } from "vue";
import { computed, toRef, unref } from "vue";
import Game from "./components/Game.vue";
import GameOverScreen from "./components/GameOverScreen.vue";
import NaNScreen from "./components/NaNScreen.vue";
import Nav from "./components/Nav.vue";
import TPS from "./components/TPS.vue";
import projInfo from "./data/projInfo.json";
import themes from "./data/themes";
import settings, { gameComponents } from "./game/settings";
import "./main.css";
const useHeader = projInfo.useHeader;
const theme = computed(() => themes[settings.theme].variables as CSSProperties);
const showTPS = toRef(settings, "showTPS");
const appErrors = toRef(state, "errors");
const gameComponent = computed(() => {
return coerceComponent(jsx(() => (<>{gameComponents.map(render)}</>)));
});
</script>
<style scoped>
.app {
background-color: var(--background);
color: var(--foreground);
display: flex;
flex-flow: column;
min-height: 100%;
height: 100%;
}
#modal-root {
position: absolute;
min-height: 100%;
height: 100%;
color: var(--foreground);
}
.error-container {
background: var(--background);
overflow: auto;
width: 100%;
height: 100%;
}
.error-container > .error {
position: static;
}
</style>

View file

@ -0,0 +1,89 @@
<template>
<slot />
<div ref="resizeListener" class="resize-listener" />
</template>
<script setup lang="ts">
import {
RegisterNodeInjectionKey,
UnregisterNodeInjectionKey,
NodesInjectionKey,
BoundsInjectionKey
} from "game/layers";
import type { FeatureNode } from "game/layers";
import { nextTick, onMounted, provide, ref } from "vue";
import { globalBus } from "game/events";
const emit = defineEmits<{
(e: "updateNodes", nodes: Record<string, FeatureNode | undefined>): void;
}>();
const nodes = ref<Record<string, FeatureNode | undefined>>({});
const resizeObserver = new ResizeObserver(updateBounds);
const resizeListener = ref<Element | null>(null);
onMounted(() => {
// ResizeListener exists because ResizeObserver's don't work when told to observe an SVG element
const resListener = resizeListener.value;
if (resListener != null) {
resizeObserver.observe(resListener);
}
});
let isDirty = true;
let boundingRect = ref(resizeListener.value?.getBoundingClientRect());
function updateBounds() {
if (isDirty) {
isDirty = false;
nextTick(() => {
boundingRect.value = resizeListener.value?.getBoundingClientRect();
(Object.values(nodes.value) as FeatureNode[])
.filter(n => n) // Sometimes the values become undefined
.forEach(node => (node.rect = node.element.getBoundingClientRect()));
emit("updateNodes", nodes.value);
isDirty = true;
});
}
}
globalBus.on("fontsLoaded", updateBounds);
const observerOptions = {
attributes: false,
childList: true,
subtree: false
};
provide(RegisterNodeInjectionKey, (id, element) => {
const observer = new MutationObserver(() => updateNode(id));
observer.observe(element, observerOptions);
nodes.value[id] = { element, observer, rect: element.getBoundingClientRect() };
updateBounds();
});
provide(UnregisterNodeInjectionKey, id => {
nodes.value[id]?.observer.disconnect();
nodes.value[id] = undefined;
updateBounds();
});
provide(NodesInjectionKey, nodes);
provide(BoundsInjectionKey, boundingRect);
function updateNode(id: string) {
const node = nodes.value[id];
if (node == null) {
return;
}
node.rect = node.element.getBoundingClientRect();
emit("updateNodes", nodes.value);
}
</script>
<style scoped>
.resize-listener {
position: absolute;
top: 0px;
left: 0;
right: -4px;
bottom: 5px;
z-index: -10;
pointer-events: none;
}
</style>

135
src/components/Error.vue Normal file
View file

@ -0,0 +1,135 @@
<template>
<div class="error">
<h1 class="error-title">{{ firstError.name }}: {{ firstError.message }}</h1>
<div class="error-details" style="margin-top: -10px">
<div v-if="firstError.cause">
<div v-for="row in causes[0]" :key="row">{{ row }}</div>
</div>
<div v-if="firstError.stack" :style="firstError.cause ? 'margin-top: 10px' : ''">
<div v-for="row in stacks[0]" :key="row">{{ row }}</div>
</div>
</div>
<div class="instructions">
Check the console for more details, and consider sharing it with the developers on
<a :href="projInfo.discordLink || 'https://discord.gg/yJ4fjnjU54'" class="discord-link"
>discord</a
>!
<FeedbackButton @click="exportSave" class="button" style="display: inline-flex"
><span class="material-icons" style="font-size: 16px">content_paste</span
><span style="margin-left: 8px; font-size: medium">Copy Save</span></FeedbackButton
><br />
<div v-if="errors.length > 1" style="margin-top: 20px"><h3>Other errors</h3></div>
<div v-for="(error, i) in errors.slice(1)" :key="i" style="margin-top: 20px">
<details class="error-details">
<summary>{{ error.name }}: {{ error.message }}</summary>
<div v-if="error.cause" style="margin-top: 10px">
<div v-for="row in causes[i + 1]" :key="row">{{ row }}</div>
</div>
<div v-if="error.stack" style="margin-top: 10px">
<div v-for="row in stacks[i + 1]" :key="row">{{ row }}</div>
</div>
</details>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import projInfo from "data/projInfo.json";
import player, { stringifySave } from "game/player";
import LZString from "lz-string";
import { computed, onMounted } from "vue";
import FeedbackButton from "./fields/FeedbackButton.vue";
const props = defineProps<{
errors: Error[];
}>();
const firstError = computed(() => props.errors[0]);
const stacks = computed(() =>
props.errors.map(error => (error.stack == null ? [] : error.stack.split("\n")))
);
const causes = computed(() =>
props.errors.map(error =>
error.cause == null
? []
: (typeof error.cause === "string" ? error.cause : JSON.stringify(error.cause)).split(
"\n"
)
)
);
function exportSave() {
let saveToExport = stringifySave(player);
switch (projInfo.exportEncoding) {
default:
console.warn(`Unknown save encoding: ${projInfo.exportEncoding}. Defaulting to lz`);
case "lz":
saveToExport = LZString.compressToUTF16(saveToExport);
break;
case "base64":
saveToExport = btoa(unescape(encodeURIComponent(saveToExport)));
break;
case "plain":
break;
}
console.log(saveToExport);
// Put on clipboard. Using the clipboard API asks for permissions and stuff
const el = document.createElement("textarea");
el.value = saveToExport;
document.body.appendChild(el);
el.select();
el.setSelectionRange(0, 99999);
document.execCommand("copy");
document.body.removeChild(el);
}
onMounted(() => {
player.autosave = false;
player.devSpeed = 0;
});
</script>
<style scoped>
.error {
border: solid 10px var(--danger);
position: absolute;
top: 0;
left: 0;
right: 0;
text-align: left;
min-height: calc(100% - 20px);
text-align: left;
color: var(--foreground);
}
.error-title {
background: var(--danger);
color: var(--feature-foreground);
display: block;
margin: -10px 0 10px 0;
position: sticky;
top: 0;
}
.error-details {
white-space: nowrap;
overflow: auto;
padding: 10px;
background-color: var(--raised-background);
}
.instructions {
padding: 10px;
}
.discord-link {
display: inline;
}
summary {
cursor: pointer;
user-select: none;
}
</style>

91
src/components/Game.vue Normal file
View file

@ -0,0 +1,91 @@
<template>
<div class="tabs-container" :class="{ useHeader }">
<div
v-for="(tab, index) in tabs"
:key="index"
class="tab"
:style="unref(layers[tab]?.style)"
:class="unref(layers[tab]?.classes)"
>
<Nav v-if="index === 0 && !useHeader" />
<div class="inner-tab">
<Layer
v-if="layerKeys.includes(tab)"
v-bind="gatherLayerProps(layers[tab]!)"
:index="index"
@set-minimized="(value: boolean) => (layers[tab]!.minimized.value = value)"
/>
<component :is="tab" :index="index" v-else />
</div>
</div>
</div>
</template>
<script setup lang="ts">
import projInfo from "data/projInfo.json";
import type { GenericLayer } from "game/layers";
import { layers } from "game/layers";
import player from "game/player";
import { computed, toRef, unref } from "vue";
import Layer from "./Layer.vue";
import Nav from "./Nav.vue";
const tabs = toRef(player, "tabs");
const layerKeys = computed(() => Object.keys(layers));
const useHeader = projInfo.useHeader;
function gatherLayerProps(layer: GenericLayer) {
const { display, minimized, name, color, minimizable, nodes, minimizedDisplay } = layer;
return { display, minimized, name, color, minimizable, nodes, minimizedDisplay };
}
</script>
<style scoped>
.tabs-container {
width: 100vw;
flex-grow: 1;
overflow-x: auto;
overflow-y: hidden;
display: flex;
}
.tabs-container:not(.useHeader) {
width: calc(100vw - 50px);
margin-left: 50px;
}
.tab {
position: relative;
height: 100%;
flex-grow: 1;
transition-duration: 0s;
overflow-y: auto;
overflow-x: hidden;
}
.inner-tab {
padding: 50px 10px;
min-height: calc(100% - 100px);
display: flex;
flex-direction: column;
margin: 0;
flex-grow: 1;
}
.tab + .tab > .inner-tab {
border-left: solid 4px var(--outline);
}
</style>
<style>
.tab hr {
height: 4px;
border: none;
background: var(--outline);
margin: var(--feature-margin) 0;
}
.tab .modal-body hr {
margin: 7px 0;
}
</style>

View file

@ -0,0 +1,107 @@
<template>
<Modal :model-value="isOpen">
<template v-slot:header>
<div class="game-over-modal-header">
<img class="game-over-modal-logo" v-if="logo" :src="logo" :alt="title" />
<div class="game-over-modal-title">
<h2>Congratulations!</h2>
<h4>You've beaten {{ title }} v{{ versionNumber }}: {{ versionTitle }}</h4>
</div>
</div>
</template>
<template v-slot:body="{ shown }">
<div v-if="shown">
<div>It took you {{ timePlayed }} to beat the game.</div>
<br />
<div>
Please check the Discord to discuss the game or to check for new content
updates!
</div>
<br />
<div>
<a :href="discordLink" class="game-over-modal-discord-link">
<span class="material-icons game-over-modal-discord">discord</span>
{{ discordName }}
</a>
</div>
<Toggle title="Autosave" v-model="autosave" />
</div>
</template>
<template v-slot:footer>
<div class="game-over-footer">
<button @click="keepGoing" class="button">Keep Going</button>
<button @click="playAgain" class="button danger">Play Again</button>
</div>
</template>
</Modal>
</template>
<script setup lang="ts">
import Modal from "components/Modal.vue";
import { hasWon } from "data/projEntry";
import projInfo from "data/projInfo.json";
import player from "game/player";
import { formatTime } from "util/bignum";
import { loadSave, newSave } from "util/save";
import { computed, toRef } from "vue";
import Toggle from "./fields/Toggle.vue";
const { title, logo, discordName, discordLink, versionNumber, versionTitle } = projInfo;
const timePlayed = computed(() => formatTime(player.timePlayed));
const isOpen = computed(() => hasWon.value && !player.keepGoing);
const autosave = toRef(player, "autosave");
function keepGoing() {
player.keepGoing = true;
}
function playAgain() {
loadSave(newSave());
}
</script>
<style scoped>
.game-over-modal-header {
display: flex;
margin: -20px;
margin-bottom: 0;
background: var(--raised-background);
align-items: center;
}
.game-over-modal-header * {
margin: 0;
}
.game-over-modal-logo {
height: 4em;
width: 4em;
}
.game-over-modal-title {
display: flex;
flex-direction: column;
padding: 10px 0;
margin-left: 10px;
}
.game-over-footer {
display: flex;
justify-content: flex-end;
}
.game-over-footer button {
margin: 0 10px;
}
.game-over-modal-discord-link {
display: flex;
align-items: center;
}
.game-over-modal-discord {
margin: 0;
margin-right: 4px;
}
</style>

70
src/components/Hotkey.vue Normal file
View file

@ -0,0 +1,70 @@
<!-- Make eslint not whine about putting spaces before the +'s -->
<!-- eslint-disable prettier/prettier -->
<template>
<template v-if="isCtrl"
><div class="key">Ctrl</div
>+</template
><template v-if="isShift"
><div class="key">Shift</div
>+</template
>
<div class="key">{{ key }}</div>
</template>
<script setup lang="ts">
import { GenericHotkey } from "features/hotkey";
import { watchEffect } from "vue";
const props = defineProps<{
hotkey: GenericHotkey;
}>();
let key = "";
let isCtrl = false;
let isShift = false;
let isAlpha = false;
watchEffect(() => {
key = props.hotkey.key;
isCtrl = key.startsWith("ctrl+");
if (isCtrl) {
key = key.slice(5);
}
isShift = key.startsWith("shift+");
if (isShift) {
key = key.slice(6);
}
isAlpha = key.length == 1 && key.toLowerCase() != key.toUpperCase();
if (isAlpha) {
key = key.toUpperCase();
}
});
</script>
<style scoped>
.key {
display: inline-block;
height: 1.4em;
min-width: 1em;
margin-block: 0.1em;
padding-inline: 0.2em;
vertical-align: 0.1em;
background: var(--foreground);
color: var(--feature-foreground);
border: 1px solid #0007;
border-radius: 0.3em;
box-shadow: 0 0.1em #0007, 0 0.1em var(--foreground);
font-size: smaller;
text-align: center;
user-select: none;
transition: transform 0s, box-shadow 0s;
}
.key:active {
transform: translateY(0.1em);
box-shadow: none;
}
</style>

131
src/components/Info.vue Normal file
View file

@ -0,0 +1,131 @@
<template>
<Modal v-model="isOpen">
<template v-slot:header>
<div class="info-modal-header">
<img class="info-modal-logo" v-if="logo" :src="logo" :alt="title" />
<div class="info-modal-title">
<h2>{{ title }}</h2>
<h4>
v{{ versionNumber }}<span v-if="versionTitle">: {{ versionTitle }}</span>
</h4>
</div>
</div>
</template>
<template v-slot:body="{ shown }">
<div v-if="shown">
<div v-if="author">By {{ author }}</div>
<div>
Made in Profectus, by thepaperpilot with inspiration from Acameada and Jacorb
</div>
<br />
<div class="link" @click="openChangelog">Changelog</div>
<br />
<div>
<a
:href="discordLink"
v-if="discordLink"
class="info-modal-discord-link"
target="_blank"
>
<span class="material-icons info-modal-discord">discord</span>
{{ discordName }}
</a>
</div>
<div>
<a
href="https://discord.gg/yJ4fjnjU54"
class="info-modal-discord-link"
target="_blank"
>
<span class="material-icons info-modal-discord">discord</span>
Profectus & Friends
</a>
</div>
<div>
<a
href="https://discord.gg/F3xveHV"
class="info-modal-discord-link"
target="_blank"
>
<span class="material-icons info-modal-discord">discord</span>
The Modding Tree
</a>
</div>
<br />
<div>Time Played: {{ timePlayed }}</div>
<component :is="infoComponent" />
</div>
</template>
</Modal>
</template>
<script setup lang="tsx">
import Modal from "components/Modal.vue";
import type Changelog from "data/Changelog.vue";
import projInfo from "data/projInfo.json";
import { jsx } from "features/feature";
import player from "game/player";
import { infoComponents } from "game/settings";
import { formatTime } from "util/bignum";
import { coerceComponent, render } from "util/vue";
import { computed, ref, toRefs, unref } from "vue";
const { title, logo, author, discordName, discordLink, versionNumber, versionTitle } = projInfo;
const _props = defineProps<{ changelog: typeof Changelog | null }>();
const props = toRefs(_props);
const isOpen = ref(false);
const timePlayed = computed(() => formatTime(player.timePlayed));
const infoComponent = computed(() => {
return coerceComponent(jsx(() => (<>{infoComponents.map(render)}</>)));
});
defineExpose({
open() {
isOpen.value = true;
}
});
function openChangelog() {
unref(props.changelog)?.open();
}
</script>
<style scoped>
.info-modal-header {
display: flex;
margin: -20px;
margin-bottom: 0;
background: var(--raised-background);
align-items: center;
}
.info-modal-header * {
margin: 0;
}
.info-modal-logo {
height: 4em;
width: 4em;
}
.info-modal-title {
display: flex;
flex-direction: column;
padding: 10px 0;
margin-left: 10px;
}
.info-modal-discord-link {
display: flex;
align-items: center;
}
.info-modal-discord {
margin: 0;
margin-right: 4px;
}
</style>

216
src/components/Layer.vue Normal file
View file

@ -0,0 +1,216 @@
<template>
<ErrorVue v-if="errors.length > 0" :errors="errors" />
<div class="layer-container" :style="{ '--layer-color': unref(color) }" v-bind="$attrs" v-else>
<button v-if="showGoBack" class="goBack" @click="goBack"></button>
<button
class="layer-tab minimized"
v-if="unref(minimized)"
@click="$emit('setMinimized', false)"
>
<component v-if="minimizedComponent" :is="minimizedComponent" />
<div v-else>{{ unref(name) }}</div>
</button>
<div class="layer-tab" :class="{ showGoBack }" v-else>
<Context @update-nodes="updateNodes">
<component :is="component" />
</Context>
</div>
<button v-if="unref(minimizable)" class="minimize" @click="$emit('setMinimized', true)">
</button>
</div>
</template>
<script lang="ts">
import projInfo from "data/projInfo.json";
import type { CoercableComponent } from "features/feature";
import type { FeatureNode } from "game/layers";
import player from "game/player";
import { computeComponent, computeOptionalComponent, processedPropType, unwrapRef } from "util/vue";
import { PropType, Ref, computed, defineComponent, onErrorCaptured, ref, toRefs, unref } from "vue";
import Context from "./Context.vue";
import ErrorVue from "./Error.vue";
export default defineComponent({
components: { Context, ErrorVue },
props: {
index: {
type: Number,
required: true
},
display: {
type: processedPropType<CoercableComponent>(Object, String, Function),
required: true
},
minimizedDisplay: processedPropType<CoercableComponent>(Object, String, Function),
minimized: {
type: Object as PropType<Ref<boolean>>,
required: true
},
name: {
type: processedPropType<string>(String),
required: true
},
color: processedPropType<string>(String),
minimizable: processedPropType<boolean>(Boolean),
nodes: {
type: Object as PropType<Ref<Record<string, FeatureNode | undefined>>>,
required: true
}
},
emits: ["setMinimized"],
setup(props) {
const { display, index, minimized, minimizedDisplay } = toRefs(props);
const component = computeComponent(display);
const minimizedComponent = computeOptionalComponent(minimizedDisplay);
const showGoBack = computed(
() => projInfo.allowGoBack && index.value > 0 && !unwrapRef(minimized)
);
function goBack() {
player.tabs.splice(unref(props.index), Infinity);
}
function updateNodes(nodes: Record<string, FeatureNode | undefined>) {
props.nodes.value = nodes;
}
const errors = ref<Error[]>([]);
onErrorCaptured((err, instance, info) => {
console.warn(`Error caught in "${props.name}" layer`, err, instance, info);
errors.value.push(
err instanceof Error ? (err as Error) : new Error(JSON.stringify(err))
);
return false;
});
return {
component,
minimizedComponent,
showGoBack,
updateNodes,
unref,
goBack,
errors
};
}
});
</script>
<style scoped>
.layer-container {
min-width: 100%;
min-height: 100%;
margin: 0;
flex-grow: 1;
display: flex;
isolation: isolate;
}
.layer-tab:not(.minimized) {
padding-top: 20px;
padding-bottom: 20px;
min-height: 100%;
flex-grow: 1;
text-align: center;
position: relative;
}
.inner-tab > .layer-container > .layer-tab:not(.minimized) {
padding-top: 50px;
}
.layer-tab.minimized {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
display: flex;
padding: 0;
padding-top: 55px;
margin: 0;
cursor: pointer;
font-size: 40px;
color: var(--foreground);
border: none;
background-color: transparent;
}
.layer-tab.minimized > * {
margin: 0;
writing-mode: vertical-rl;
text-align: left;
padding-left: 10px;
width: 50px;
}
.inner-tab > .layer-container > .layer-tab:not(.minimized) {
margin: -50px -10px;
padding: 50px 10px;
}
.modal-body .layer-tab {
padding-bottom: 0;
}
.modal-body .layer-tab:not(.hasSubtabs) {
padding-top: 0;
}
.minimize {
position: sticky;
top: 6px;
right: 9px;
z-index: 7;
line-height: 30px;
border: none;
background: var(--background);
box-shadow: var(--background) 0 2px 3px 5px;
border-radius: 50%;
color: var(--foreground);
font-size: 40px;
cursor: pointer;
margin-top: -44px;
margin-right: -30px;
}
.minimized + .minimize {
transform: rotate(-90deg);
top: 10px;
right: 18px;
pointer-events: none;
}
.goBack {
position: sticky;
top: 10px;
left: 10px;
line-height: 30px;
margin-top: -50px;
margin-left: -35px;
border: none;
background: var(--background);
box-shadow: var(--background) 0 2px 3px 5px;
border-radius: 50%;
color: var(--foreground);
font-size: 30px;
cursor: pointer;
z-index: 7;
}
.goBack:hover {
transform: scale(1.1, 1.1);
text-shadow: 0 0 7px var(--foreground);
}
</style>
<style>
.layer-tab.minimized > * > .desc {
color: var(--accent1);
font-size: 30px;
}
</style>

View file

@ -0,0 +1,61 @@
<template>
<div v-if="mark">
<div v-if="mark === true" class="mark star"></div>
<img v-else class="mark" :src="mark" />
</div>
</template>
<script setup lang="ts">
defineProps<{ mark?: boolean | string }>();
</script>
<style scoped>
.mark {
position: absolute;
left: -25px;
top: -10px;
width: 30px;
height: 30px;
z-index: 1;
pointer-events: none;
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;
}
.star {
left: -10px;
width: 0;
height: 0;
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: 0.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);
}
</style>

139
src/components/Modal.vue Normal file
View file

@ -0,0 +1,139 @@
<template>
<teleport to="#modal-root">
<transition
name="modal"
@before-enter="isAnimating = true"
@after-leave="isAnimating = false"
>
<div
class="modal-mask"
v-show="modelValue"
v-on:pointerdown.self="close"
v-bind="$attrs"
>
<div class="modal-wrapper">
<div class="modal-container">
<div class="modal-header">
<slot name="header" :shown="isOpen"> default header </slot>
</div>
<div class="modal-body">
<Context ref="contextRef">
<slot name="body" :shown="isOpen"> default body </slot>
</Context>
</div>
<div class="modal-footer">
<slot name="footer" :shown="isOpen">
<div class="modal-default-footer">
<div class="modal-default-flex-grow"></div>
<button class="button modal-default-button" @click="close">
Close
</button>
</div>
</slot>
</div>
</div>
</div>
</div>
</transition>
</teleport>
</template>
<script setup lang="ts">
import type { FeatureNode } from "game/layers";
import { computed, ref, toRefs, unref } from "vue";
import Context from "./Context.vue";
const _props = defineProps<{
modelValue: boolean;
}>();
const props = toRefs(_props);
const emit = defineEmits<{
(e: "update:modelValue", value: boolean): void;
}>();
const isOpen = computed(() => unref(props.modelValue) || isAnimating.value);
function close() {
emit("update:modelValue", false);
}
const isAnimating = ref(false);
const contextRef = ref<typeof Context | null>(null);
const nodes = computed<Record<string, FeatureNode | undefined> | null>(
() => contextRef.value?.nodes ?? null
);
defineExpose({ isOpen, nodes });
</script>
<style>
.modal-mask {
position: fixed;
z-index: 9998;
top: 0;
left: 0;
bottom: 0;
right: 0;
background-color: rgba(0, 0, 0, 0.5);
transition: opacity 0.3s ease;
}
.modal-wrapper {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
}
.modal-container {
width: 640px;
max-width: 95vw;
max-height: 95vh;
background-color: var(--background);
padding: 20px;
border-radius: 5px;
transition: all 0.3s ease;
text-align: left;
border: var(--modal-border);
box-sizing: border-box;
display: flex;
flex-direction: column;
}
.modal-header {
width: 100%;
}
.modal-body {
margin: 20px 0;
width: 100%;
overflow-y: auto;
overflow-x: hidden;
}
.modal-footer {
width: 100%;
}
.modal-default-footer {
display: flex;
}
.modal-default-flex-grow {
flex-grow: 1;
}
.modal-enter-from {
opacity: 0;
}
.modal-leave-active {
opacity: 0;
}
.modal-enter-from .modal-container,
.modal-leave-active .modal-container {
-webkit-transform: scale(1.1);
transform: scale(1.1);
}
</style>

View file

@ -0,0 +1,130 @@
<template>
<Modal v-model="hasNaN" v-bind="$attrs">
<template v-slot:header>
<div class="nan-modal-header">
<h2>NaN value detected!</h2>
</div>
</template>
<template v-slot:body>
<div>
Attempted to assign "{{ path }}" to NaN<span v-if="previous">
{{ " " }}(previously {{ format(previous) }})</span
>. Auto-saving has been {{ autosave ? "enabled" : "disabled" }}. Check the console
for more details, and consider sharing it with the developers on discord.
</div>
<br />
<div>
<a
:href="discordLink || 'https://discord.gg/yJ4fjnjU54'"
class="nan-modal-discord-link"
>
<span class="material-icons nan-modal-discord">discord</span>
{{ discordName || "Profectus & Friends" }}
</a>
</div>
<br />
<Toggle title="Autosave" v-model="autosave" />
<Toggle v-if="projInfo.enablePausing" title="Pause game" v-model="isPaused" />
</template>
<template v-slot:footer>
<div class="nan-footer">
<button @click="savesManager?.open()" class="button">Open Saves Manager</button>
<button @click="setZero" class="button">Set to 0</button>
<button @click="setOne" class="button">Set to 1</button>
<button
@click="hasNaN = false"
class="button"
v-if="previous && Decimal.neq(previous, 0) && Decimal.neq(previous, 1)"
>
Set to previous
</button>
<button @click="ignore" class="button danger">Ignore</button>
</div>
</template>
</Modal>
<SavesManager ref="savesManager" />
</template>
<script setup lang="ts">
import Modal from "components/Modal.vue";
import projInfo from "data/projInfo.json";
import player from "game/player";
import state from "game/state";
import type { DecimalSource } from "util/bignum";
import Decimal, { format } from "util/bignum";
import type { ComponentPublicInstance } from "vue";
import { computed, ref, toRef, watch } from "vue";
import Toggle from "./fields/Toggle.vue";
import SavesManager from "./SavesManager.vue";
const { discordName, discordLink } = projInfo;
const autosave = ref(true);
const isPaused = ref(true);
const hasNaN = toRef(state, "hasNaN");
const savesManager = ref<ComponentPublicInstance<typeof SavesManager> | null>(null);
watch(hasNaN, hasNaN => {
if (hasNaN) {
autosave.value = player.autosave;
isPaused.value = player.devSpeed === 0;
} else {
player.autosave = autosave.value;
player.devSpeed = isPaused.value ? 0 : null;
}
});
const path = computed(() => state.NaNPath?.join("."));
const previous = computed<DecimalSource | null>(() => {
if (state.NaNPersistent != null) {
return state.NaNPersistent.value;
}
return null;
});
function setZero() {
if (state.NaNPersistent != null) {
state.NaNPersistent.value = new Decimal(0);
state.hasNaN = false;
}
}
function setOne() {
if (state.NaNPersistent) {
state.NaNPersistent.value = new Decimal(1);
state.hasNaN = false;
}
}
function ignore() {
if (state.NaNPersistent) {
state.NaNPersistent.value = new Decimal(NaN);
state.hasNaN = false;
}
}
</script>
<style scoped>
.nan-modal-header {
padding: 10px 0;
margin-left: 10px;
}
.nan-footer {
display: flex;
justify-content: flex-end;
}
.nan-footer button {
margin: 0 10px;
}
.nan-modal-discord-link {
display: flex;
align-items: center;
}
.nan-modal-discord {
margin: 0;
margin-right: 4px;
}
</style>

284
src/components/Nav.vue Normal file
View file

@ -0,0 +1,284 @@
<template>
<div class="nav" v-if="useHeader" v-bind="$attrs">
<img v-if="banner" :src="banner" class="banner" :alt="title" />
<div v-else class="title">{{ title }}</div>
<div @click="changelog?.open()" class="version-container">
<Tooltip display="Changelog" :direction="Direction.Down" class="version"
><span>v{{ versionNumber }}</span></Tooltip
>
</div>
<div style="flex-grow: 1; cursor: unset"></div>
<div class="discord">
<span @click="openDiscord" class="material-icons">discord</span>
<ul class="discord-links">
<li v-if="discordLink">
<a :href="discordLink" target="_blank">{{ discordName }}</a>
</li>
<li>
<a href="https://discord.gg/yJ4fjnjU54" target="_blank">Profectus & Friends</a>
</li>
<li>
<a href="https://discord.gg/F3xveHV" target="_blank">The Modding Tree</a>
</li>
</ul>
</div>
<div>
<a href="https://forums.moddingtree.com/" target="_blank">
<Tooltip display="Forums" :direction="Direction.Down" yoffset="5px">
<span class="material-icons">forum</span>
</Tooltip>
</a>
</div>
<div @click="info?.open()">
<Tooltip display="Info" :direction="Direction.Down" class="info">
<span class="material-icons">info</span>
</Tooltip>
</div>
<div @click="savesManager?.open()">
<Tooltip display="Saves" :direction="Direction.Down" xoffset="-20px">
<span class="material-icons">library_books</span>
</Tooltip>
</div>
<div @click="roomsDialog?.open()">
<Tooltip display="Multiplayer" :direction="Direction.Down" xoffset="-20px">
<span class="material-icons">group</span>
</Tooltip>
</div>
<div @click="options?.open()">
<Tooltip display="Settings" :direction="Direction.Down" xoffset="-66px">
<span class="material-icons">settings</span>
</Tooltip>
</div>
</div>
<div v-else class="overlay-nav" v-bind="$attrs">
<div @click="changelog?.open()" class="version-container">
<Tooltip display="Changelog" :direction="Direction.Right" xoffset="25%" class="version">
<span>v{{ versionNumber }}</span>
</Tooltip>
</div>
<div @click="savesManager?.open()">
<Tooltip display="Saves" :direction="Direction.Right">
<span class="material-icons">library_books</span>
</Tooltip>
</div>
<div @click="roomsDialog?.open()">
<Tooltip display="Multiplayer" :direction="Direction.Right">
<span class="material-icons">group</span>
</Tooltip>
</div>
<div @click="options?.open()">
<Tooltip display="Settings" :direction="Direction.Right">
<span class="material-icons">settings</span>
</Tooltip>
</div>
<div @click="info?.open()">
<Tooltip display="Info" :direction="Direction.Right">
<span class="material-icons">info</span>
</Tooltip>
</div>
<div>
<a href="https://forums.moddingtree.com/" target="_blank">
<Tooltip display="Forums" :direction="Direction.Right" xoffset="7px">
<span class="material-icons">forum</span>
</Tooltip>
</a>
</div>
<div class="discord">
<span @click="openDiscord" class="material-icons">discord</span>
<ul class="discord-links">
<li v-if="discordLink">
<a :href="discordLink" target="_blank">{{ discordName }}</a>
</li>
<li>
<a href="https://discord.gg/yJ4fjnjU54" target="_blank">Profectus & Friends</a>
</li>
<li>
<a href="https://discord.gg/F3xveHV" target="_blank">The Modding Tree</a>
</li>
</ul>
</div>
</div>
<Info ref="info" :changelog="changelog" />
<SavesManager ref="savesManager" />
<Options ref="options" />
<Changelog ref="changelog" />
<RoomsDialog ref="roomsDialog" />
</template>
<script setup lang="ts">
import Changelog from "data/Changelog.vue";
import projInfo from "data/projInfo.json";
import RoomsDialog from "data/RoomsDialog.vue";
import Tooltip from "features/tooltips/Tooltip.vue";
import { globalBus } from "game/events";
import { Direction } from "util/common";
import type { ComponentPublicInstance } from "vue";
import { ref } from "vue";
import Info from "./Info.vue";
import Options from "./Options.vue";
import SavesManager from "./SavesManager.vue";
const info = ref<ComponentPublicInstance<typeof Info> | null>(null);
const savesManager = ref<ComponentPublicInstance<typeof SavesManager> | null>(null);
const options = ref<ComponentPublicInstance<typeof Options> | null>(null);
// For some reason Info won't accept the changelog unless I do this:
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const changelog = ref<ComponentPublicInstance<any> | null>(null);
const roomsDialog = ref<ComponentPublicInstance<typeof RoomsDialog> | null>(null);
globalBus.on("openMultiplayer", () => roomsDialog.value?.open());
const { useHeader, banner, title, discordName, discordLink, versionNumber } = projInfo;
function openDiscord() {
window.open(discordLink, "mywindow");
}
</script>
<style scoped>
.nav {
background-color: var(--raised-background);
display: flex;
left: 0;
right: 0;
top: 0;
height: 46px;
width: 100%;
border-bottom: 4px solid var(--outline);
z-index: 2;
}
.nav > * {
height: 46px;
width: 46px;
display: flex;
cursor: pointer;
flex-shrink: 0;
}
.nav > .banner {
height: 100%;
width: unset;
}
.overlay-nav {
position: fixed;
top: 10px;
left: 10px;
display: flex;
flex-direction: column;
z-index: 2;
}
.overlay-nav > * {
height: 50px;
width: 50px;
display: flex;
cursor: pointer;
margin: 0;
align-items: center;
justify-content: center;
}
.title {
font-size: 36px;
text-align: left;
margin-left: 12px;
cursor: unset;
}
.nav > .title {
width: unset;
flex-shrink: 1;
overflow: hidden;
white-space: nowrap;
}
.nav .saves,
.nav .info {
display: flex;
}
.tooltip-container {
width: 100%;
height: 100%;
display: flex;
}
.overlay-nav .discord {
position: relative;
}
.discord img {
width: 100%;
height: 100%;
}
.discord-links {
position: fixed;
top: 45px;
padding: 20px;
right: -340px;
width: 300px;
transition: right 0.25s ease;
background: var(--raised-background);
z-index: 10;
}
.overlay-nav .discord-links {
position: absolute;
left: -280px;
right: unset;
transition: left 0.25s ease;
}
.overlay-nav .discord:hover .discord-links {
left: -10px;
}
.discord-links li {
margin-bottom: 4px;
}
.discord-links li:first-child {
font-size: 1.2em;
}
*:not(.overlay-nav) .discord:hover .discord-links {
right: 0;
}
.material-icons {
font-size: 36px;
}
.material-icons:hover {
text-shadow: 5px 0 10px var(--link), -3px 0 12px var(--foreground);
}
.nav .version-container {
display: flex;
height: 25px;
margin-bottom: 0;
margin-left: 10px;
}
.overlay-nav .version-container {
width: unset;
height: 25px;
}
.version {
color: var(--points);
}
.version:hover span {
text-shadow: 5px 0 10px var(--points), -3px 0 12px var(--points);
}
.nav > div > a,
.overlay-nav > div > a {
color: var(--foreground);
text-shadow: none;
}
</style>

41
src/components/Node.vue Normal file
View file

@ -0,0 +1,41 @@
<template>
<div class="node" ref="node"></div>
</template>
<script setup lang="ts">
import { RegisterNodeInjectionKey, UnregisterNodeInjectionKey } from "game/layers";
import { computed, inject, onUnmounted, shallowRef, toRefs, unref, watch } from "vue";
const _props = defineProps<{ id: string }>();
const props = toRefs(_props);
// eslint-disable-next-line @typescript-eslint/no-empty-function
const register = inject(RegisterNodeInjectionKey, () => {});
// eslint-disable-next-line @typescript-eslint/no-empty-function
const unregister = inject(UnregisterNodeInjectionKey, () => {});
const node = shallowRef<HTMLElement | null>(null);
const parentNode = computed(() => node.value && node.value.parentElement);
watch([parentNode, props.id], ([newNode, newID], [prevNode, prevID]) => {
if (prevNode) {
unregister(unref(prevID));
}
if (newNode) {
register(newID, newNode);
}
});
onUnmounted(() => unregister(unref(props.id)));
</script>
<style scoped>
.node {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
</style>

43
src/components/Notif.vue Normal file
View file

@ -0,0 +1,43 @@
<template>
<div class="notif">!</div>
</template>
<script setup lang="ts"></script>
<style scoped>
.notif {
position: absolute;
top: 0;
left: 5px;
z-index: 10;
pointer-events: none;
user-select: none;
color: var(--accent3);
font-size: x-large;
animation: 1s linear infinite bounce;
border-radius: var(--border-radius);
background: var(--locked);
}
@keyframes bounce {
0% {
animation-timing-function: cubic-bezier(0.1361, 0.2514, 0.2175, 0.8786);
transform: translate(0, 0px) scaleY(1);
}
37% {
animation-timing-function: cubic-bezier(0.7674, 0.1844, 0.8382, 0.7157);
transform: translate(0, -20px) scaleY(1);
}
72% {
animation-timing-function: cubic-bezier(0.1118, 0.2149, 0.2172, 0.941);
transform: translate(0, 0px) scaleY(1);
}
87% {
animation-timing-function: cubic-bezier(0.7494, 0.2259, 0.8209, 0.6963);
transform: translate(0, 10px) scaleY(0.602);
}
100% {
transform: translate(0, 0px) scaleY(1);
}
}
</style>

154
src/components/Options.vue Normal file
View file

@ -0,0 +1,154 @@
<template>
<Modal v-model="isOpen">
<template v-slot:header>
<div class="header">
<h2>Settings</h2>
</div>
</template>
<template v-slot:body>
<Select :title="themeTitle" :options="themes" v-model="theme" />
<component :is="settingFieldsComponent" />
<Toggle :title="showTPSTitle" v-model="showTPS" />
<Toggle :title="alignModifierUnitsTitle" v-model="alignUnits" />
</template>
</Modal>
</template>
<script setup lang="tsx">
import Modal from "components/Modal.vue";
import projInfo from "data/projInfo.json";
import { save } from "util/save";
import rawThemes from "data/themes";
import { jsx } from "features/feature";
import Tooltip from "features/tooltips/Tooltip.vue";
import player from "game/player";
import settings, { settingFields } from "game/settings";
import { camelToTitle, Direction } from "util/common";
import { coerceComponent, render } from "util/vue";
import { computed, ref, toRefs } from "vue";
import Select from "./fields/Select.vue";
import Toggle from "./fields/Toggle.vue";
import FeedbackButton from "./fields/FeedbackButton.vue";
const isOpen = ref(false);
const currentTab = ref("behaviour");
function isTab(tab: string): boolean {
return tab == currentTab.value;
}
function setTab(tab: string) {
currentTab.value = tab;
}
defineExpose({
isTab,
setTab,
save,
open() {
isOpen.value = true;
}
});
const themes = Object.keys(rawThemes).map(theme => ({
label: camelToTitle(theme),
value: theme
}));
const settingFieldsComponent = computed(() => {
return coerceComponent(jsx(() => (<>{settingFields.map(render)}</>)));
});
const { showTPS, theme, unthrottled, alignUnits } = toRefs(settings);
const { autosave, offlineProd } = toRefs(player);
const isPaused = computed({
get() {
return player.devSpeed === 0;
},
set(value: boolean) {
player.devSpeed = value ? 0 : null;
}
});
const unthrottledTitle = jsx(() => (
<span class="option-title">
Unthrottled
<desc>Allow the game to run as fast as possible. Not battery friendly.</desc>
</span>
));
const offlineProdTitle = jsx(() => (
<span class="option-title">
Offline Production<Tooltip display="Save-specific" direction={Direction.Right}>*</Tooltip>
<desc>Simulate production that occurs while the game is closed.</desc>
</span>
));
const autosaveTitle = jsx(() => (
<span class="option-title">
Autosave<Tooltip display="Save-specific" direction={Direction.Right}>*</Tooltip>
<desc>Automatically save the game every second or when the game is closed.</desc>
</span>
));
const isPausedTitle = jsx(() => (
<span class="option-title">
Pause game<Tooltip display="Save-specific" direction={Direction.Right}>*</Tooltip>
<desc>Stop everything from moving.</desc>
</span>
));
const themeTitle = jsx(() => (
<span class="option-title">
Theme
<desc>How the game looks.</desc>
</span>
));
const showTPSTitle = jsx(() => (
<span class="option-title">
Show TPS
<desc>Show TPS meter at the bottom-left corner of the page.</desc>
</span>
));
const alignModifierUnitsTitle = jsx(() => (
<span class="option-title">
Align modifier units
<desc>Align numbers to the beginning of the unit in modifier view.</desc>
</span>
));
</script>
<style>
.option-tabs {
border-bottom: 2px solid var(--outline);
margin-top: 10px;
margin-bottom: -10px;
}
.option-tabs button {
background-color: transparent;
color: var(--foreground);
margin-bottom: -2px;
font-size: 14px;
cursor: pointer;
padding: 5px 20px;
border: none;
border-bottom: 2px solid var(--foreground);
}
.option-tabs button:not(.selected) {
border-bottom-color: transparent;
}
.option-title .tooltip-container {
display: inline;
margin-left: 5px;
}
.option-title desc {
display: block;
opacity: 0.6;
font-size: small;
width: 300px;
margin-left: 0;
}
.save-button {
text-align: right;
}
</style>

View file

@ -0,0 +1,195 @@
<template>
<transition appear>
<svg
id="eaRe02fYmMp1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
viewBox="0 0 228 521"
shape-rendering="geometricPrecision"
text-rendering="geometricPrecision"
>
<g id="P">
<path
d="m 101,512.877 c -17.547386,-5.3519 -50.794681,-10.26296 -80,0 10.737201,-217.43031 5.7244,-300.999 0,-464.9995 0,0 46.6144,-37.1164 80,-42.00002 33.386,-4.883633 86.025,10.45942 120,50.00002 5,30 -4.353,106.6565 -44,156.0005 -34.149,42.5 -130,38.48 -130,92.999 0,102 54,208 54,208 z"
style="
display: inline;
fill: none;
stroke: rgb(163, 190, 140);
stroke-width: 10;
stroke-linecap: round;
stroke-miterlimit: 16;
"
id="trunk"
class="svg-elem-1"
></path>
<path
d="M 221,55.8775 C 209.023,126.453 185.39,166.835 158.997,191.5 93.783098,252.444 11.718998,217.436 46.999998,304.877"
style="
display: inline;
fill: none;
stroke: rgb(163, 190, 140);
stroke-width: 5;
stroke-linecap: round;
stroke-miterlimit: 16;
"
id="vine2"
class="svg-elem-2"
></path>
<path
d="m 194.5,188 c -11.225,4.447 -19.066,5.134 -35.503,3.5"
style="
display: inline;
fill: none;
stroke: rgb(163, 190, 140);
stroke-width: 5;
stroke-linecap: round;
stroke-miterlimit: 16;
"
id="short_vine4"
class="svg-elem-3"
></path>
<path
d="M 73.499996,246.5 C 111.145,245.626 127.011,238.775 156.5,228"
style="
display: inline;
fill: none;
stroke: rgb(163, 190, 140);
stroke-width: 5;
stroke-linecap: round;
stroke-miterlimit: 16;
"
id="short_vine3"
class="svg-elem-4"
></path>
<path
d="M 221,55.8775 C 169.5,17.8262 86.0943,44.9468 47,107 c -4.743,7.528 -7.1041,15.373 -8.326,24 -3.5282,24.91 2.4426,56.34 -12.0011,105.5"
style="
display: inline;
fill: none;
stroke: rgb(163, 190, 140);
stroke-width: 5;
stroke-linecap: round;
stroke-miterlimit: 16;
"
id="vine1"
class="svg-elem-5"
></path>
<path
d="M 21,47.8775 38.674,131"
style="
display: inline;
fill: none;
stroke: rgb(163, 190, 140);
stroke-width: 5;
stroke-linecap: round;
stroke-miterlimit: 16;
"
id="short_vine2"
class="svg-elem-6"
></path>
<path
d="m 3,326.5 c 13.1783,22.208 16.4863,42.834 21.6997,81"
style="
display: inline;
fill: none;
stroke: rgb(163, 190, 140);
stroke-width: 5;
stroke-linecap: round;
stroke-miterlimit: 16;
"
id="short_vine1"
class="svg-elem-7"
></path>
</g>
</svg>
</transition>
</template>
<style scoped>
svg {
background: #2e3440;
}
/***************************************************
* Generated by SVG Artista on 1/7/2022, 4:39:47 PM
* MIT license (https://opensource.org/licenses/MIT)
* W. https://svgartista.net
**************************************************/
svg .svg-elem-1 {
stroke-dashoffset: 2648.758056640625px;
stroke-dasharray: 1324.3790283203125px;
transition: stroke-dashoffset 1s cubic-bezier(0.47, 0, 0.745, 0.715) 0s;
}
svg.v-enter-from .svg-elem-1,
svg.v-leave-to .svg-elem-1 {
stroke-dashoffset: 1324.3790283203125px;
}
svg .svg-elem-2 {
stroke-dashoffset: 680.4000854492188px;
stroke-dasharray: 340.2000427246094px;
transition: stroke-dashoffset 1s ease-out 0.4s;
}
svg.v-enter-from .svg-elem-2,
svg.v-leave-to .svg-elem-2 {
stroke-dashoffset: 340.2000427246094px;
}
svg .svg-elem-3 {
stroke-dashoffset: 76.21031951904297px;
stroke-dasharray: 38.105159759521484px;
transition: stroke-dashoffset 1s ease-out 0.8s;
}
svg.v-enter-from .svg-elem-3,
svg.v-leave-to .svg-elem-3 {
stroke-dashoffset: 38.105159759521484px;
}
svg .svg-elem-4 {
stroke-dashoffset: 175.18072509765625px;
stroke-dasharray: 87.59036254882812px;
transition: stroke-dashoffset 1s cubic-bezier(0.47, 0, 0.745, 0.715) 0.36s;
}
svg.v-enter-from .svg-elem-4,
svg.v-leave-to .svg-elem-4 {
stroke-dashoffset: 87.59036254882812px;
}
svg .svg-elem-5 {
stroke-dashoffset: 671.9447021484375px;
stroke-dasharray: 335.97235107421875px;
transition: stroke-dashoffset 1s ease-out 0.8s;
}
svg.v-enter-from .svg-elem-5,
svg.v-leave-to .svg-elem-5 {
stroke-dashoffset: 335.97235107421875px;
}
svg .svg-elem-6 {
stroke-dashoffset: 173.96141052246094px;
stroke-dasharray: 86.98070526123047px;
transition: stroke-dashoffset 1s ease-out 1s;
}
svg.v-enter-from .svg-elem-6,
svg.v-leave-to .svg-elem-6 {
stroke-dashoffset: 86.98070526123047px;
}
svg .svg-elem-7 {
stroke-dashoffset: 172.99151611328125px;
stroke-dasharray: 86.49575805664062px;
transition: stroke-dashoffset 1s ease-out 0.85s;
}
svg.v-enter-from .svg-elem-7,
svg.v-leave-to .svg-elem-7 {
stroke-dashoffset: 86.49575805664062px;
}
</style>

204
src/components/Save.vue Normal file
View file

@ -0,0 +1,204 @@
<template>
<div class="save" :class="{ active: isActive }">
<div class="handle material-icons">drag_handle</div>
<div class="actions" v-if="!isEditing">
<FeedbackButton
@click="emit('export')"
class="button"
left
v-if="save.error == undefined && !isConfirming"
>
<Tooltip display="Export" :direction="Direction.Left" class="info">
<span class="material-icons">content_paste</span>
</Tooltip>
</FeedbackButton>
<button
@click="emit('duplicate')"
class="button"
v-if="save.error == undefined && !isConfirming"
>
<Tooltip display="Duplicate" :direction="Direction.Left" class="info">
<span class="material-icons">content_copy</span>
</Tooltip>
</button>
<button
@click="isEditing = !isEditing"
class="button"
v-if="save.error == undefined && !isConfirming"
>
<Tooltip display="Edit Name" :direction="Direction.Left" class="info">
<span class="material-icons">edit</span>
</Tooltip>
</button>
<DangerButton
:disabled="isActive"
@click="emit('delete')"
@confirmingChanged="(value: boolean) => (isConfirming = value)"
>
<Tooltip display="Delete" :direction="Direction.Left" class="info">
<span class="material-icons" style="margin: -2px">delete</span>
</Tooltip>
</DangerButton>
</div>
<div class="actions" v-else>
<button @click="changeName" class="button">
<Tooltip display="Save" :direction="Direction.Left" class="info">
<span class="material-icons">check</span>
</Tooltip>
</button>
<button @click="isEditing = !isEditing" class="button">
<Tooltip display="Cancel" :direction="Direction.Left" class="info">
<span class="material-icons">close</span>
</Tooltip>
</button>
</div>
<div class="details" v-if="save.error == undefined && !isEditing">
<button class="button open" @click="emit('open')">
<h3>{{ save.name }}</h3>
</button>
<span class="save-version">v{{ save.modVersion }}</span
><br />
<div v-if="currentTime">Last played {{ dateFormat.format(currentTime) }}</div>
</div>
<div class="details" v-else-if="save.error == undefined && isEditing">
<Text v-model="newName" class="editname" @submit="changeName" />
</div>
<div v-else class="details error">
Error: Failed to load save with id {{ save.id }}<br />{{ save.error }}
</div>
</div>
</template>
<script setup lang="ts">
import Tooltip from "features/tooltips/Tooltip.vue";
import player from "game/player";
import { Direction } from "util/common";
import { computed, ref, toRefs, watch } from "vue";
import DangerButton from "./fields/DangerButton.vue";
import FeedbackButton from "./fields/FeedbackButton.vue";
import Text from "./fields/Text.vue";
import type { LoadablePlayerData } from "./SavesManager.vue";
const _props = defineProps<{
save: LoadablePlayerData;
}>();
const { save } = toRefs(_props);
const emit = defineEmits<{
(e: "export"): void;
(e: "open"): void;
(e: "duplicate"): void;
(e: "delete"): void;
(e: "editName", name: string): void;
}>();
const dateFormat = new Intl.DateTimeFormat("en-US", {
year: "numeric",
month: "numeric",
day: "numeric",
hour: "numeric",
minute: "numeric",
second: "numeric"
});
const isEditing = ref(false);
const isConfirming = ref(false);
const newName = ref("");
watch(isEditing, () => (newName.value = save.value.name ?? ""));
const isActive = computed(() => save.value != null && save.value.id === player.id);
const currentTime = computed(() =>
isActive.value ? player.time : (save.value != null && save.value.time) ?? 0
);
function changeName() {
emit("editName", newName.value);
isEditing.value = false;
}
</script>
<style scoped>
.save {
position: relative;
border: solid 4px var(--outline);
padding: 4px;
background: var(--raised-background);
margin: var(--feature-margin);
display: flex;
align-items: center;
min-height: 30px;
}
.save.active {
border-color: var(--bought);
}
.open {
display: inline;
margin: 0;
padding-left: 0;
}
.handle {
flex-grow: 0;
margin-right: 8px;
margin-left: 0;
cursor: pointer;
}
.details {
margin: 0;
flex-grow: 1;
margin-right: 80px;
}
.error {
font-size: 0.8em;
color: var(--danger);
}
.save-version {
margin-left: 4px;
font-size: 0.7em;
opacity: 0.7;
}
.actions {
position: absolute;
top: 0;
bottom: 0;
right: 4px;
display: flex;
padding: 4px;
z-index: 1;
}
.editname {
margin: 0;
}
</style>
<style>
.save button {
transition-duration: 0s;
}
.save .actions button {
display: flex;
font-size: 1.2em;
}
.save .actions button .material-icons {
font-size: unset;
}
.save .button.danger {
display: flex;
align-items: center;
padding: 4px;
}
.save .field {
margin: 0;
}
</style>

View file

@ -0,0 +1,337 @@
<template>
<Modal v-model="isOpen" ref="modal">
<template v-slot:header>
<h2>Saves Manager</h2>
</template>
<template #body="{ shown }">
<Draggable
:list="settings.saves"
handle=".handle"
v-if="shown && (!room || isHosting)"
:itemKey="(save: string) => save"
>
<template #item="{ element }">
<Save
:save="saves[element]"
@open="openSave(element)"
@export="exportSave(element)"
@editName="(name: string) => editSave(element, name)"
@duplicate="duplicateSave(element)"
@delete="deleteSave(element)"
/>
</template>
</Draggable>
<div v-else>You are connected to a server - cannot change saves</div>
</template>
<template v-slot:footer>
<div class="modal-footer">
<Text
v-if="!room || isHosting"
v-model="saveToImport"
title="Import Save"
placeholder="Paste your save here!"
:class="{ importingFailed }"
/>
<div class="field" v-if="!room || isHosting">
<span class="field-title">Create Save</span>
<div class="field-buttons">
<button class="button" @click="openSave(newSave().id)">New Game</button>
<Select
v-if="Object.keys(bank).length > 0"
:options="bank"
:modelValue="selectedPreset"
@update:modelValue="(preset: unknown) => newFromPreset(preset as string)"
closeOnSelect
placeholder="Select preset"
class="presets"
/>
</div>
</div>
<div class="footer">
<div style="flex-grow: 1"></div>
<button class="button modal-default-button" @click="isOpen = false">
Close
</button>
</div>
</div>
</template>
</Modal>
</template>
<script setup lang="ts">
import Modal from "components/Modal.vue";
import projInfo from "data/projInfo.json";
import { isHosting, room } from "data/socket";
import type { Player } from "game/player";
import player, { stringifySave } from "game/player";
import settings from "game/settings";
import LZString from "lz-string";
import { getUniqueID, loadSave, newSave, save } from "util/save";
import type { ComponentPublicInstance } from "vue";
import { computed, nextTick, ref, shallowReactive, watch } from "vue";
import Draggable from "vuedraggable";
import Select from "./fields/Select.vue";
import Text from "./fields/Text.vue";
import Save from "./Save.vue";
export type LoadablePlayerData = Omit<Partial<Player>, "id"> & { id: string; error?: unknown };
const isOpen = ref(false);
const modal = ref<ComponentPublicInstance<typeof Modal> | null>(null);
defineExpose({
open() {
isOpen.value = true;
}
});
const importingFailed = ref(false);
const saveToImport = ref("");
const selectedPreset = ref<string | null>(null);
watch(saveToImport, importedSave => {
if (importedSave) {
nextTick(() => {
try {
if (importedSave[0] === "{") {
// plaintext. No processing needed
} else if (importedSave[0] === "e") {
// Assumed to be base64, which starts with e
importedSave = decodeURIComponent(escape(atob(importedSave)));
} else if (importedSave[0] === "ᯡ") {
// Assumed to be lz, which starts with
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
importedSave = LZString.decompressFromUTF16(importedSave)!;
} else {
console.warn("Unable to determine preset encoding", importedSave);
importingFailed.value = true;
return;
}
const playerData = JSON.parse(importedSave);
if (typeof playerData !== "object") {
importingFailed.value = true;
return;
}
const id = getUniqueID();
playerData.id = id;
save(playerData);
saveToImport.value = "";
importingFailed.value = false;
settings.saves.push(id);
} catch (e) {
importingFailed.value = true;
}
});
} else {
importingFailed.value = false;
}
});
let bankContext = import.meta.globEager("./../../saves/*.txt", { as: "raw" });
let bank = ref(
Object.keys(bankContext).reduce((acc: Array<{ label: string; value: string }>, curr) => {
acc.push({
// .slice(2, -4) strips the leading ./ and the trailing .txt
label: curr.split("/").slice(-1)[0].slice(0, -4),
// Have to perform this unholy cast because globEager's typing doesn't appear to know
// adding { as: "raw" } will make the object contain strings rather than modules
value: bankContext[curr] as unknown as string
});
return acc;
}, [])
);
const cachedSaves = shallowReactive<Record<string, LoadablePlayerData | undefined>>({});
function getCachedSave(id: string) {
if (cachedSaves[id] == null) {
let save = localStorage.getItem(id);
if (save == null) {
cachedSaves[id] = { error: `Save doesn't exist in localStorage`, id };
} else if (save === "dW5kZWZpbmVk") {
cachedSaves[id] = { error: `Save is undefined`, id };
} else {
try {
if (save[0] === "{") {
// plaintext. No processing needed
} else if (save[0] === "e") {
// Assumed to be base64, which starts with e
save = decodeURIComponent(escape(atob(save)));
} else if (save[0] === "ᯡ") {
// Assumed to be lz, which starts with
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
save = LZString.decompressFromUTF16(save)!;
} else {
console.warn("Unable to determine preset encoding", save);
importingFailed.value = true;
cachedSaves[id] = { error: "Unable to determine preset encoding", id };
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return cachedSaves[id]!;
}
cachedSaves[id] = { ...JSON.parse(save), id };
} catch (error) {
cachedSaves[id] = { error, id };
console.warn(
`SavesManager: Failed to load info about save with id ${id}:\n${error}\n${save}`
);
}
}
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return cachedSaves[id]!;
}
// Wipe cache whenever the modal is opened
watch(isOpen, isOpen => {
if (isOpen) {
Object.keys(cachedSaves).forEach(key => delete cachedSaves[key]);
}
});
const saves = computed(() =>
settings.saves.reduce((acc: Record<string, LoadablePlayerData>, curr: string) => {
acc[curr] = getCachedSave(curr);
return acc;
}, {})
);
function exportSave(id: string) {
let saveToExport;
if (player.id === id) {
saveToExport = stringifySave(player);
} else {
saveToExport = JSON.stringify(saves.value[id]);
}
switch (projInfo.exportEncoding as string) {
default:
console.warn(`Unknown save encoding: ${projInfo.exportEncoding}. Defaulting to lz`);
case "lz":
saveToExport = LZString.compressToUTF16(saveToExport);
break;
case "base64":
saveToExport = btoa(unescape(encodeURIComponent(saveToExport)));
break;
case "plain":
break;
}
// Put on clipboard. Using the clipboard API asks for permissions and stuff
const el = document.createElement("textarea");
el.value = saveToExport;
document.body.appendChild(el);
el.select();
el.setSelectionRange(0, 99999);
document.execCommand("copy");
document.body.removeChild(el);
}
function duplicateSave(id: string) {
if (player.id === id) {
save();
}
const playerData = { ...saves.value[id], id: getUniqueID() };
save(playerData as Player);
settings.saves.push(playerData.id);
}
function deleteSave(id: string) {
settings.saves = settings.saves.filter((save: string) => save !== id);
localStorage.removeItem(id);
cachedSaves[id] = undefined;
}
function openSave(id: string) {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
saves.value[player.id]!.time = player.time;
save();
cachedSaves[player.id] = undefined;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
loadSave(saves.value[id]!);
// Delete cached version in case of opening it again
cachedSaves[id] = undefined;
}
function newFromPreset(preset: string) {
// Reset preset dropdown
selectedPreset.value = preset;
nextTick(() => {
selectedPreset.value = null;
});
if (preset[0] === "{") {
// plaintext. No processing needed
} else if (preset[0] === "e") {
// Assumed to be base64, which starts with e
preset = decodeURIComponent(escape(atob(preset)));
} else if (preset[0] === "ᯡ") {
// Assumed to be lz, which starts with
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
preset = LZString.decompressFromUTF16(preset)!;
} else {
console.warn("Unable to determine preset encoding", preset);
return;
}
const playerData = JSON.parse(preset);
playerData.id = getUniqueID();
save(playerData as Player);
settings.saves.push(playerData.id);
openSave(playerData.id);
}
function editSave(id: string, newName: string) {
const currSave = saves.value[id];
if (currSave != null) {
currSave.name = newName;
if (player.id === id) {
player.name = newName;
save();
} else {
save(currSave as Player);
cachedSaves[id] = undefined;
}
}
}
</script>
<style scoped>
.field form,
.field .field-title,
.field .field-buttons {
margin: 0;
}
.field-buttons {
display: flex;
}
.field-buttons .field {
margin: 0;
margin-left: 8px;
}
.modal-footer {
margin-top: -20px;
}
.footer {
display: flex;
margin-top: 20px;
}
</style>
<style>
.importingFailed input {
color: red;
}
.field-buttons .v-select {
width: 220px;
}
.presets .vue-select[aria-expanded="true"] vue-dropdown {
visibility: hidden;
}
</style>

33
src/components/TPS.vue Normal file
View file

@ -0,0 +1,33 @@
<template>
<div class="tpsDisplay" v-if="!tps.isNan()">TPS: {{ formatWhole(tps) }}</div>
</template>
<script setup lang="ts">
import state from "game/state";
import Decimal, { formatWhole } from "util/bignum";
import { computed } from "vue";
const tps = computed(() =>
Decimal.div(
state.lastTenTicks.length,
state.lastTenTicks.reduce((acc, curr) => acc + curr, 0)
)
);
</script>
<style scoped>
.tpsDisplay {
position: absolute;
left: 10px;
bottom: 10px;
z-index: 100;
}
.low {
color: var(--danger);
}
.fade-leave-to {
opacity: 0;
}
</style>

View file

@ -0,0 +1,38 @@
.feature:not(li),
.feature:not(li) button {
position: relative;
padding: 5px;
border-radius: var(--border-radius);
border: 2px solid rgba(0, 0, 0, 0.125);
margin: var(--feature-margin);
box-sizing: border-box;
color: var(--feature-foreground);
z-index: 0;
transition: all 0.5s, z-index 0s 0.5s;
}
.can,
.can button {
background-color: var(--layer-color);
cursor: pointer;
}
.can:hover,
.can:hover button {
transform: scale(1.15, 1.15);
box-shadow: 0 0 20px var(--points);
z-index: 1;
transition: all 0.5s, z-index 0s;
}
.locked,
.locked button {
background-color: var(--locked);
cursor: not-allowed;
}
.bought,
.bought button {
background-color: var(--bought);
cursor: default;
}

View file

@ -0,0 +1,13 @@
.field {
display: flex;
position: relative;
min-height: 2em;
margin: 10px 0;
user-select: none;
justify-content: space-between;
align-items: center;
}
.field > * {
margin: 0;
}

View file

@ -0,0 +1,21 @@
.modifier-container {
display: flex;
padding: 1px 8px;
}
.modifier-container:nth-child(2n) {
background: var(--raised-background);
}
.modifier-amount {
flex-shrink: 0;
text-align: right;
}
:not(:first-of-type, :last-of-type) > .modifier-amount::after {
content: var(--unit);
opacity: 0;
}
.modifier-description {
flex-grow: 1;
}

View file

@ -0,0 +1,128 @@
.table {
display: flex;
flex-flow: column wrap;
justify-content: center;
align-items: center;
max-width: 100%;
margin: 0 auto;
}
.table + .table {
margin-top: 10px;
}
.row {
display: flex;
flex-flow: row wrap;
justify-content: center;
align-items: stretch;
max-width: 100%;
margin: 0 10px;
}
.row > :not(.feature) {
margin: 0;
display: flex;
}
.col {
display: flex;
flex-flow: column wrap;
justify-content: center;
align-items: center;
height: 100%;
margin: 10px 0;
}
.row.mergeAdjacent > .feature:not(.dontMerge),
.row.mergeAdjacent > .tooltip-container > .feature:not(.dontMerge) {
margin-left: 0;
margin-right: 0;
border-radius: 0;
}
.row.mergeAdjacent > .feature:not(.dontMerge):first-child,
.row.mergeAdjacent > .tooltip-container:first-child > .feature:not(.dontMerge) {
border-radius: var(--border-radius) 0 0 var(--border-radius);
}
.row.mergeAdjacent > .feature:not(.dontMerge):last-child,
.row.mergeAdjacent > .tooltip-container:last-child > .feature:not(.dontMerge) {
border-radius: 0 var(--border-radius) var(--border-radius) 0;
}
.row.mergeAdjacent > .feature:not(.dontMerge):first-child:last-child,
.row.mergeAdjacent > .tooltip-container:first-child:last-child > .feature:not(.dontMerge) {
border-radius: var(--border-radius);
}
.row-grid.mergeAdjacent > .feature:not(.dontMerge),
.row-grid.mergeAdjacent > .tooltip-container > .feature:not(.dontMerge) {
margin-left: 0;
margin-right: 0;
margin-bottom: 0;
margin-top: 0;
border-radius: 0;
}
.row-grid.mergeAdjacent > .feature:not(.dontMerge):last-child,
.row-grid.mergeAdjacent > .tooltip-container:last-child > .feature:not(.dontMerge) {
border-radius: 0 0 0 0;
}
.row-grid.mergeAdjacent > .feature:not(.dontMerge):first-child,
.row-grid.mergeAdjacent > .tooltip-container:first-child > .feature:not(.dontMerge) {
border-radius: 0 0 0 0;
}
.table-grid > .row-grid.mergeAdjacent:last-child > .feature:not(.dontMerge):first-child {
border-radius: 0 0 0 var(--border-radius);
}
.table-grid > .row-grid.mergeAdjacent:first-child > .feature:not(.dontMerge):last-child {
border-radius: 0 var(--border-radius) 0 0;
}
.table-grid > .row-grid.mergeAdjacent:first-child > .feature:not(.dontMerge):first-child {
border-radius: var(--border-radius) 0 0 0;
}
.table-grid > .row-grid.mergeAdjacent:last-child > .feature:not(.dontMerge):last-child {
border-radius: 0 0 var(--border-radius) 0;
}
/*
TODO how to implement mergeAdjacent for grids?
.row.mergeAdjacent + .row.mergeAdjacent > .feature:not(.dontMerge) {
border-top-left-radius: 0;
border-top-right-radius: 0;
}
*/
.col.mergeAdjacent .feature:not(.dontMerge) {
margin-top: 0;
margin-bottom: 0;
border-radius: 0;
}
.col.mergeAdjacent .feature:not(.dontMerge):first-child {
border-radius: var(--border-radius) var(--border-radius) 0 0;
}
.col.mergeAdjacent .feature:not(.dontMerge):last-child {
border-radius: 0 0 var(--border-radius) var(--border-radius);
}
.col.mergeAdjacent .feature:not(.dontMerge):first-child:last-child {
border-radius: var(--border-radius);
}
/*
TODO how to implement mergeAdjacent for grids?
.col.mergeAdjacent + .col.mergeAdjacent > .feature:not(.dontMerge) {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
*/

View file

@ -0,0 +1,78 @@
<template>
<span class="container" :class="{ confirming: isConfirming }">
<span v-if="isConfirming">Are you sure?</span>
<button @click.stop="click" class="button danger" :disabled="disabled">
<span v-if="isConfirming">Yes</span>
<slot v-else />
</button>
<button v-if="isConfirming" class="button" @click.stop="cancel">No</button>
</span>
</template>
<script setup lang="ts">
import { ref, toRefs, unref, watch } from "vue";
const _props = defineProps<{
disabled?: boolean;
skipConfirm?: boolean;
}>();
const props = toRefs(_props);
const emit = defineEmits<{
(e: "click"): void;
(e: "confirmingChanged", value: boolean): void;
}>();
const isConfirming = ref(false);
watch(isConfirming, isConfirming => {
emit("confirmingChanged", isConfirming);
});
function click() {
if (unref(props.skipConfirm)) {
emit("click");
return;
}
if (isConfirming.value) {
emit("click");
}
isConfirming.value = !isConfirming.value;
}
function cancel() {
isConfirming.value = false;
}
</script>
<style scoped>
.container {
display: flex;
align-items: center;
background: var(--raised-background);
box-shadow: var(--raised-background) 0 2px 3px 5px;
}
.container.confirming button {
font-size: 1em;
}
.container > * {
margin: 0 4px;
}
</style>
<style>
.danger,
.button.danger {
position: relative;
border: solid 2px var(--danger);
border-right-width: 16px;
}
.danger::after {
position: absolute;
content: "!";
color: white;
right: -13px;
}
</style>

View file

@ -0,0 +1,74 @@
<template>
<button @click.stop="click" class="feedback" :class="{ activated, left }">
<slot />
</button>
</template>
<script setup lang="ts">
import { nextTick, ref } from "vue";
defineProps<{
left?: boolean;
}>();
const emit = defineEmits<{
(e: "click"): void;
}>();
const activated = ref(false);
const activatedTimeout = ref<NodeJS.Timer | null>(null);
function click() {
emit("click");
// Give feedback to user
if (activatedTimeout.value) {
clearTimeout(activatedTimeout.value);
}
activated.value = false;
nextTick(() => {
activated.value = true;
activatedTimeout.value = setTimeout(() => (activated.value = false), 500);
});
}
</script>
<style scoped>
.feedback {
position: relative;
}
.feedback::after {
position: absolute;
left: calc(100% + 5px);
top: 50%;
transform: translateY(-50%);
content: "✔";
opacity: 0;
pointer-events: none;
box-shadow: inset 0 0 0 35px rgba(111, 148, 182, 0);
text-shadow: none;
}
.feedback.left::after {
left: unset;
right: calc(100% + 5px);
}
.feedback.activated::after {
animation: feedback 0.5s ease-out forwards;
}
@keyframes feedback {
0% {
opacity: 1;
transform: scale3d(0.4, 0.4, 1), translateY(-50%);
}
80% {
opacity: 0.1;
}
100% {
opacity: 0;
transform: scale3d(1.2, 1.2, 1), translateY(-50%);
}
}
</style>

View file

@ -0,0 +1,97 @@
<template>
<div class="field">
<span class="field-title" v-if="titleComponent"><component :is="titleComponent" /></span>
<VueNextSelect
:options="options"
v-model="value"
@update:model-value="onUpdate"
:min="1"
label-by="label"
:placeholder="placeholder"
:close-on-select="closeOnSelect"
/>
</div>
</template>
<script setup lang="ts">
import "components/common/fields.css";
import type { CoercableComponent } from "features/feature";
import { computeOptionalComponent, unwrapRef } from "util/vue";
import { ref, toRef, watch } from "vue";
import VueNextSelect from "vue-next-select";
import "vue-next-select/dist/index.css";
export type SelectOption = { label: string; value: unknown };
const props = defineProps<{
title?: CoercableComponent;
modelValue?: unknown;
options: SelectOption[];
placeholder?: string;
closeOnSelect?: boolean;
}>();
const emit = defineEmits<{
(e: "update:modelValue", value: unknown): void;
}>();
const titleComponent = computeOptionalComponent(toRef(props, "title"), "span");
const value = ref<SelectOption | null>(
props.options.find(option => option.value === props.modelValue) ?? null
);
watch(toRef(props, "modelValue"), modelValue => {
if (unwrapRef(value) !== modelValue) {
value.value = props.options.find(option => option.value === modelValue) ?? null;
}
});
function onUpdate(value: SelectOption) {
emit("update:modelValue", value.value);
}
</script>
<style>
.vue-select {
width: 50%;
border-radius: var(--border-radius);
}
.field-buttons .vue-select {
width: unset;
}
.vue-select,
.vue-dropdown {
border-color: var(--outline);
}
.vue-dropdown {
background: var(--raised-background);
}
.vue-dropdown-item {
color: var(--foreground);
}
.vue-dropdown-item,
.vue-dropdown-item * {
transition-duration: 0s;
}
.vue-dropdown-item.highlighted {
background-color: var(--highlighted);
}
.vue-dropdown-item.selected,
.vue-dropdown-item.highlighted.selected {
background-color: var(--bought);
}
.vue-input input {
font-size: inherit;
}
.vue-input input::placeholder {
color: var(--link);
}
</style>

View file

@ -0,0 +1,41 @@
<template>
<div class="field">
<span class="field-title" v-if="title">{{ title }}</span>
<Tooltip :display="`${value}`" :class="{ fullWidth: !title }" :direction="Direction.Down">
<input type="range" v-model="value" :min="min" :max="max" />
</Tooltip>
</div>
</template>
<script setup lang="ts">
import "components/common/fields.css";
import Tooltip from "features/tooltips/Tooltip.vue";
import { Direction } from "util/common";
import { computed, toRefs, unref } from "vue";
const _props = defineProps<{
title?: string;
modelValue?: number;
min?: number;
max?: number;
}>();
const props = toRefs(_props);
const emit = defineEmits<{
(e: "update:modelValue", value: number): void;
}>();
const value = computed({
get() {
return String(unref(props.modelValue) ?? 0);
},
set(value: string) {
emit("update:modelValue", Number(value));
}
});
</script>
<style scoped>
.fullWidth {
width: 100%;
}
</style>

View file

@ -0,0 +1,99 @@
<template>
<form @submit.prevent="submit">
<div class="field">
<span class="field-title" v-if="titleComponent"
><component :is="titleComponent"
/></span>
<VueTextareaAutosize
v-if="textArea"
v-model="value"
:placeholder="placeholder"
:maxHeight="maxHeight"
@blur="blur"
ref="field"
/>
<input
v-else
type="text"
v-model="value"
:placeholder="placeholder"
:class="{ fullWidth: !title }"
@blur="blur"
ref="field"
/>
</div>
</form>
</template>
<script setup lang="ts">
import "components/common/fields.css";
import type { CoercableComponent } from "features/feature";
import { computeOptionalComponent } from "util/vue";
import { computed, onMounted, shallowRef, toRef, unref } from "vue";
import VueTextareaAutosize from "vue-textarea-autosize";
const props = defineProps<{
title?: CoercableComponent;
modelValue?: string;
textArea?: boolean;
placeholder?: string;
maxHeight?: number;
submitOnBlur?: boolean;
}>();
const emit = defineEmits<{
(e: "update:modelValue", value: string): void;
(e: "submit"): void;
(e: "cancel"): void;
}>();
const titleComponent = computeOptionalComponent(toRef(props, "title"), "span");
const field = shallowRef<HTMLElement | null>(null);
onMounted(() => {
field.value?.focus();
});
const value = computed({
get() {
return unref(props.modelValue) ?? "";
},
set(value: string) {
emit("update:modelValue", value);
}
});
function submit() {
emit("submit");
}
function blur() {
if (props.submitOnBlur !== false) {
emit("submit");
} else {
emit("cancel");
}
}
</script>
<style scoped>
form {
margin: 0;
width: 100%;
}
.field > * {
margin: 0;
}
input {
width: 50%;
outline: none;
border: solid 1px var(--outline);
background-color: unset;
border-radius: var(--border-radius);
}
.fullWidth {
width: 100%;
}
</style>

View file

@ -0,0 +1,117 @@
<template>
<label class="field">
<input type="checkbox" class="toggle" v-model="value" />
<component :is="component" />
</label>
</template>
<script setup lang="ts">
import "components/common/fields.css";
import type { CoercableComponent } from "features/feature";
import { coerceComponent } from "util/vue";
import { computed, unref } from "vue";
const props = defineProps<{
title?: CoercableComponent;
modelValue?: boolean;
}>();
const emit = defineEmits<{
(e: "update:modelValue", value: boolean): void;
}>();
const component = computed(() => coerceComponent(unref(props.title) ?? "<span></span>", "span"));
const value = computed({
get() {
return !!props.modelValue;
},
set(value: boolean) {
emit("update:modelValue", value);
}
});
</script>
<style scoped>
.field {
cursor: pointer;
}
input {
appearance: none;
pointer-events: none;
}
span {
width: 100%;
padding-right: 41px;
position: relative;
}
/* track */
input + span::before {
content: "";
position: absolute;
top: calc(50% - 7px);
right: 0px;
border-radius: 7px;
width: 36px;
height: 14px;
background-color: var(--outline);
opacity: 0.38;
vertical-align: top;
transition: background-color 0.2s, opacity 0.2s;
}
/* thumb */
input + span::after {
content: "";
position: absolute;
top: calc(50% - 10px);
right: 16px;
border-radius: 50%;
width: 20px;
height: 20px;
background-color: var(--locked);
box-shadow: 0 3px 1px -2px rgba(0, 0, 0, 0.2), 0 2px 2px 0 rgba(0, 0, 0, 0.14),
0 1px 5px 0 rgba(0, 0, 0, 0.12);
transition: background-color 0.2s, transform 0.2s;
}
input:checked + span::before {
background-color: var(--link);
opacity: 0.6;
}
input:checked + span::after {
background-color: var(--link);
transform: translateX(16px);
}
/* active */
input:active + span::before {
background-color: var(--link);
opacity: 0.6;
}
input:checked:active + span::before {
background-color: var(--outline);
opacity: 0.38;
}
/* disabled */
input:disabled + span {
color: black;
opacity: 0.38;
cursor: default;
}
input:disabled + span::before {
background-color: var(--outline);
opacity: 0.38;
}
input:checked:disabled + span::before {
background-color: var(--link);
opacity: 0.6;
}
</style>

View file

@ -0,0 +1,70 @@
<template>
<Col class="collapsible-container">
<button @click="collapsed.value = !collapsed.value" class="feature collapsible-toggle">
<component :is="displayComponent" />
</button>
<component v-if="!collapsed.value" :is="contentComponent" />
</Col>
</template>
<script setup lang="ts">
import type { CoercableComponent } from "features/feature";
import { computeComponent } from "util/vue";
import type { Ref } from "vue";
import { toRef } from "vue";
import Col from "./Column.vue";
const props = defineProps<{
collapsed: Ref<boolean>;
display: CoercableComponent;
content: CoercableComponent;
}>();
const displayComponent = computeComponent(toRef(props, "display"));
const contentComponent = computeComponent(toRef(props, "content"));
</script>
<style scoped>
.collapsible-container {
width: calc(100% - 10px);
}
.collapsible-toggle {
max-width: unset;
width: calc(100% + 0px);
margin: 0;
margin-left: -5px;
background: var(--raised-background);
padding: var(--feature-margin);
color: var(--foreground);
cursor: pointer;
transition-duration: 0s;
}
.collapsible-toggle:last-child {
margin-left: unset;
}
:deep(.collapsible-toggle + .table) {
max-width: unset;
width: calc(100% + 10px);
margin-left: -5px;
}
:deep(.col) {
margin-top: 0;
margin-bottom: 0;
width: 100%;
}
.mergeAdjacent .collapsible-toggle {
border: 0;
border-top-left-radius: 0 !important;
border-top-right-radius: 0 !important;
}
:deep(.mergeAdjacent .feature:not(.dontMerge):first-child) {
border-top-left-radius: 0 !important;
border-top-right-radius: 0 !important;
}
</style>

View file

@ -0,0 +1,16 @@
<template>
<div class="table">
<div class="col" :class="{ mergeAdjacent }">
<slot />
</div>
</div>
</template>
<script setup lang="ts">
import "components/common/table.css";
import themes from "data/themes";
import settings from "game/settings";
import { computed } from "vue";
const mergeAdjacent = computed(() => themes[settings.theme].mergeAdjacent);
</script>

View file

@ -0,0 +1,16 @@
<template>
<div class="table">
<div class="row" :class="{ mergeAdjacent }">
<slot />
</div>
</div>
</template>
<script setup lang="ts">
import "components/common/table.css";
import themes from "data/themes";
import settings from "game/settings";
import { computed } from "vue";
const mergeAdjacent = computed(() => themes[settings.theme].mergeAdjacent);
</script>

View file

@ -0,0 +1,16 @@
<template>
<div :style="{ width, height }"></div>
</template>
<script setup lang="ts">
withDefaults(
defineProps<{
width?: string;
height?: string;
}>(),
{
width: "8px",
height: "17px"
}
);
</script>

View file

@ -0,0 +1,49 @@
<template>
<div class="sticky" :style="{ top }" ref="element" data-v-sticky>
<slot />
</div>
</template>
<script setup lang="ts">
import { nextTick, onMounted, ref, shallowRef } from "vue";
const top = ref("0");
const observer = new ResizeObserver(updateTop);
const element = shallowRef<HTMLElement | null>(null);
function updateTop() {
let el = element.value;
if (el == undefined) {
return;
}
let newTop = 0;
while (el.previousSibling) {
const sibling = el.previousSibling as HTMLElement;
if (sibling.dataset && "vSticky" in sibling.dataset) {
newTop += sibling.offsetHeight;
}
el = sibling;
}
top.value = newTop + "px";
}
nextTick(updateTop);
document.fonts.ready.then(updateTop);
onMounted(() => {
const el = element.value?.parentElement;
if (el) {
observer.observe(el);
}
});
</script>
<style scoped>
.sticky {
position: sticky;
background: var(--background);
width: calc(100% - 2px);
z-index: 3;
}
</style>

View file

@ -0,0 +1,18 @@
<template>
<div class="vr" :style="{ height }"></div>
</template>
<script setup lang="ts">
defineProps<{
height?: string;
}>();
</script>
<style scoped>
.vr {
width: 4px;
background: var(--outline);
height: 100%;
margin: auto var(--feature-margin);
}
</style>

View file

@ -0,0 +1,5 @@
<template>
<span style="white-space: nowrap; font-size: larger; font-family: initial">
&nbsp;<slot />&nbsp;
</span>
</template>

View file

@ -0,0 +1 @@

View file

@ -0,0 +1,8 @@
<template>
<span style="white-space: nowrap">
<span style="font-size: larger; font-family: initial">&radic;</span>
<div style="display: inline-block; border-top: 1px solid; padding-left: 0.2em">
<slot />
</div>
</span>
</template>

83
src/data/Changelog.vue Normal file
View file

@ -0,0 +1,83 @@
<template>
<Modal v-model="isOpen">
<template v-slot:header>
<h2>Changelog</h2>
</template>
<template v-slot:body>
<details open>
<summary>v0.0 Initial Commit - <time>2021-09-04</time></summary>
This is the first release :D
<ul>
<li class="feature">Did everything</li>
<li class="fix">Had some fun</li>
<li class="breaking">Removed everything</li>
<li class="balancing">Created some bugs to fix later</li>
</ul>
</details>
</template>
</Modal>
</template>
<script setup lang="ts">
import Modal from "components/Modal.vue";
import { ref } from "vue";
const isOpen = ref(false);
defineExpose({
open() {
isOpen.value = true;
}
});
</script>
<style scoped>
details {
margin: 10px 0;
padding-left: 18px;
}
summary {
cursor: pointer;
margin-bottom: 10px;
margin-left: -18px;
}
ul {
margin: var(--feature-margin) 0;
background: var(--raised-background);
border: 2px solid rgba(0, 0, 0, 0.125);
padding: 5px 5px 5px 15px;
list-style: inside;
}
li {
margin: 8px 0;
}
li::before {
padding: 2px 8px;
margin-right: 8px;
border-radius: var(--border-radius);
}
.feature::before {
content: "Feature";
background: var(--accent1);
}
.fix::before {
content: "Fix";
background: var(--accent2);
}
.balancing::before {
content: "Balancing";
background: var(--accent3);
}
.breaking::before {
content: "Breaking";
background: var(--danger);
}
</style>

139
src/data/Chat.vue Normal file
View file

@ -0,0 +1,139 @@
<template>
<div class="chat" :class="{ open }" v-show="room">
<div class="chat-toggle" @click="open = !open">
<span>Chat</span>
<span v-if="unread" style="margin-left: 10px">[{{ unread > 9 ? "9+" : unread }}]</span>
</div>
<div class="chat-messages" ref="scroll" @scroll.passive="onScroll">
<div v-for="(message, i) in messages" :key="i" class="chat-message-container">
<span class="chat-user" v-if="message.user">{{ nicknames[message.user] }}</span>
<span class="chat-message" :style="message.user ? '' : 'font-style: italic'">{{
message.message
}}</span>
</div>
</div>
<hr style="margin-top: 0" />
<div class="chat-submit">
<Text v-model="message" @submit="submit" :submitOnBlur="false" />
<button @pointerdown="submit" class="button">
<span class="material-icons">check</span>
</button>
</div>
</div>
</template>
<script setup lang="ts">
import Text from "components/fields/Text.vue";
import { globalBus } from "game/events";
import { nextTick, reactive, ref, watch } from "vue";
import { emit, nicknames, room } from "./socket";
const open = ref<boolean>(false);
const unread = ref<number>(0);
const message = ref("");
const messages = reactive<{ user?: string; message: string }[]>([]);
const scroll = ref<HTMLElement | null>(null);
function submit() {
emit("chat", message.value);
message.value = "";
}
globalBus.on("chat", (user, message) => {
messages.push({ user, message });
const atBottom =
scroll.value &&
scroll.value.scrollTop >= scroll.value.scrollHeight - scroll.value.clientHeight;
if (atBottom) {
nextTick(() => {
if (scroll.value) {
scroll.value.scrollTop = scroll.value.scrollHeight - scroll.value.clientHeight;
}
});
}
if ((!atBottom || !open.value) && user) {
unread.value++;
}
});
function onScroll() {
nextTick(() => {
if (
scroll.value &&
unread.value > 0 &&
scroll.value.scrollTop >= scroll.value.scrollHeight - scroll.value.clientHeight
) {
unread.value = 0;
}
});
}
watch(open, open => {
if (open) {
unread.value = 0;
}
});
</script>
<style scoped>
.chat {
position: fixed;
top: 100%;
right: 4px;
height: 500px;
width: 300px;
border: solid 1px var(--outline);
display: flex;
flex-flow: column;
background: var(--background);
}
.chat.open {
top: calc(100% - 500px);
}
.chat-toggle {
margin-top: -23px;
margin-left: -1px;
margin-right: -1px;
border-top-left-radius: var(--border-radius);
border-top-right-radius: var(--border-radius);
border: solid 1px var(--outline);
cursor: pointer;
user-select: none;
background: var(--background);
}
.chat-messages {
flex-grow: 1;
width: calc(100% - 8px);
padding: 4px;
word-break: break-all;
overflow-y: auto;
}
.chat-message-container {
font-size: smaller;
text-align: left;
margin-bottom: 4px;
}
.chat-user {
font-weight: bolder;
margin-right: 10px;
}
.chat-message {
font-weight: lighter;
}
.chat-submit {
display: flex;
width: 100%;
}
.chat-submit > form {
margin-left: 10px;
}
</style>

156
src/data/HexGrid.vue Normal file
View file

@ -0,0 +1,156 @@
<template>
<div class="grid">
<div v-for="(row, rowIndex) in gridData" :key="rowIndex" class="row">
<div
v-for="(color, colIndex) in row"
:key="`${rowIndex}-${colIndex}-${color}`"
:class="{ 'hexagon-wrapper': true, [color]: true }"
@click="updateColor(rowIndex, colIndex)"
@mouseover="setHoveredCell(rowIndex, colIndex)"
@mouseout="resetHoveredCell"
>
<div class="hexagon"></div>
<transition name="fade">
<div
v-if="
hoveredCell?.row === rowIndex &&
hoveredCell?.col === colIndex &&
color !== selectedColor
"
:class="{ 'hexagon-overlay': true, [selectedColor]: true }"
>
<div class="hexagon"></div>
</div>
</transition>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, toRefs } from "vue";
const props = defineProps<{
gridData: string[][];
selectedColor: string;
}>();
const emit = defineEmits<{
(event: "updateColor", row: number, col: number, color: string): void;
}>();
const { selectedColor } = toRefs(props);
const hoveredCell = ref<{ row: number; col: number } | null>(null);
function updateColor(row: number, col: number) {
emit("updateColor", row, col, selectedColor.value);
}
function setHoveredCell(row: number, col: number) {
hoveredCell.value = { row, col };
}
function resetHoveredCell() {
hoveredCell.value = null;
}
</script>
<style scoped>
.grid {
display: flex;
flex-direction: column;
align-items: center;
}
.row {
margin-top: -1vw;
}
.hexagon-wrapper {
position: relative;
width: 5vw;
height: 5vw;
margin: 0.2vw;
}
.hexagon-wrapper::before,
.hexagon-wrapper::after,
.hexagon-overlay::before,
.hexagon-overlay::after {
content: "";
position: absolute;
bottom: 0.1vw;
width: 50%;
height: 0.6vw;
backdrop-filter: blur(3px);
background-color: var(--color);
transform-origin: bottom;
pointer-events: none;
z-index: -1;
}
.hexagon-wrapper::before,
.hexagon-overlay::before {
left: 0;
transform: skewY(26.5deg);
filter: brightness(85%);
}
.hexagon-wrapper::after,
.hexagon-overlay::after {
right: 0;
transform: skewY(-26.5deg);
filter: brightness(65%);
}
.hexagon {
position: relative;
width: 100%;
height: 100%;
backdrop-filter: blur(3px);
clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%);
background-color: var(--color);
}
.hexagon-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
opacity: 0.5;
pointer-events: none;
animation: bounce 1s infinite;
}
/* for some reason the transition is instant sometimes when hovering stops */
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
@keyframes bounce {
0%, 100% {
transform: translateY(-15px);
}
50% {
transform: translateY(-7.5px);
}
}
.gray {
--color: rgba(64, 64, 64, 0.6);
}
.red {
--color: rgba(255, 0, 0, 0.86);
}
.green {
--color: rgba(0, 255, 0, 0.6);
}
.blue {
--color: rgba(0, 0, 255, 0.6);
}
</style>

158
src/data/Room.vue Normal file
View file

@ -0,0 +1,158 @@
<template>
<div class="room">
<div class="actions" v-if="enteringPassword">
<button @pointerdown="submitPassword" class="button">
<Tooltip display="Join" :direction="Direction.Left" class="info">
<span class="material-icons">check</span>
</Tooltip>
</button>
<button @pointerdown="enteringPassword = !enteringPassword" class="button">
<Tooltip display="Cancel" :direction="Direction.Left" class="info">
<span class="material-icons">close</span>
</Tooltip>
</button>
</div>
<div class="details" v-if="!enteringPassword">
<span class="material-icons" v-if="room.hasPassword">lock</span>
<span class="material-icons" v-if="isPrivate">visibility_off</span>
<button class="button open" @click="startConnecting">
<h3>{{ room.name }}</h3>
</button>
<div class="room-host">Hosted by {{ room.host }}</div>
</div>
<div v-else class="details" style="display: flex">
<span>Password:</span>
<Text
v-model="password"
class="editname"
@submit="submitPassword"
@cancel="enteringPassword = !enteringPassword"
:submitOnBlur="false"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, toRefs, watch } from "vue";
import Text from "components/fields/Text.vue";
import { Direction } from "util/common";
import Tooltip from "features/tooltips/Tooltip.vue";
// For some reason it won't find the interface from chromatic-common
interface ClientRoomData {
name: string;
host: string;
hasPassword: boolean;
}
const _props = defineProps<{
isPrivate: boolean;
room: ClientRoomData;
}>();
const { room } = toRefs(_props);
const emit = defineEmits<{
(e: "connect", password?: string): void;
}>();
const enteringPassword = ref(false);
const password = ref("");
watch(enteringPassword, enteringPassword => {
if (enteringPassword) {
password.value = "";
}
});
function startConnecting() {
if (room.value.hasPassword) {
enteringPassword.value = true;
} else {
emit("connect");
}
}
function submitPassword() {
emit("connect", password.value);
enteringPassword.value = false;
}
</script>
<style scoped>
.room {
position: relative;
border: solid 4px var(--outline);
padding: 4px;
background: var(--raised-background);
margin: var(--feature-margin);
display: flex;
align-items: center;
min-height: 30px;
}
.room.active {
border-color: var(--bought);
}
.open {
display: inline;
margin: 0;
padding-left: 0;
}
.details {
margin: 0;
flex-grow: 1;
margin-right: 80px;
}
.details .material-icons {
font-size: 20px;
margin-right: 4px;
transform: translateY(2px);
}
.room-host {
font-size: 0.7em;
opacity: 0.7;
}
.actions {
position: absolute;
top: 0;
bottom: 0;
right: 4px;
display: flex;
padding: 4px;
z-index: 1;
}
.editname {
margin: 0;
}
</style>
<style>
.room button {
transition-duration: 0s;
}
.room .actions button {
display: flex;
font-size: 1.2em;
}
.room .actions button .material-icons {
font-size: unset;
}
.room .button.danger {
display: flex;
align-items: center;
padding: 4px;
}
.room .field {
margin: 0;
}
</style>

Some files were not shown because too many files have changed in this diff Show more