Merge remote-tracking branch 'template/main'

This commit is contained in:
thepaperpilot 2022-04-24 21:02:58 -05:00
commit 44901754f4
28 changed files with 339 additions and 145 deletions

View file

@ -6,6 +6,35 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [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)

31
package-lock.json generated
View file

@ -1,16 +1,17 @@
{
"name": "profectus",
"version": "0.3.0",
"version": "0.3.3",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"name": "profectus",
"version": "0.3.0",
"version": "0.3.3",
"dependencies": {
"@pixi/particle-emitter": "^5.0.4",
"core-js": "^3.6.5",
"lodash.clonedeep": "^4.5.0",
"lz-string": "^1.4.4",
"nanoevents": "^6.0.2",
"pixi.js": "^6.3.0",
"vue": "^3.2.26",
@ -26,6 +27,7 @@
"@jetblack/operator-overloading": "^0.2.0",
"@rushstack/eslint-patch": "^1.1.0",
"@types/lodash.clonedeep": "^4.5.6",
"@types/lz-string": "^1.3.34",
"@vue/babel-plugin-jsx": "^1.1.1",
"@vue/cli-plugin-babel": "^5.0.3",
"@vue/cli-plugin-eslint": "^5.0.3",
@ -2461,6 +2463,12 @@
"@types/lodash": "*"
}
},
"node_modules/@types/lz-string": {
"version": "1.3.34",
"resolved": "https://registry.npmjs.org/@types/lz-string/-/lz-string-1.3.34.tgz",
"integrity": "sha512-j6G1e8DULJx3ONf6NdR5JiR2ZY3K3PaaqiEuKYkLQO0Czfi1AzrtjfnfCROyWGeDd5IVMKCwsgSmMip9OWijow==",
"dev": true
},
"node_modules/@types/mime": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz",
@ -8592,6 +8600,14 @@
"node": ">=10"
}
},
"node_modules/lz-string": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz",
"integrity": "sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY=",
"bin": {
"lz-string": "bin/bin.js"
}
},
"node_modules/magic-string": {
"version": "0.25.7",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz",
@ -15010,6 +15026,12 @@
"@types/lodash": "*"
}
},
"@types/lz-string": {
"version": "1.3.34",
"resolved": "https://registry.npmjs.org/@types/lz-string/-/lz-string-1.3.34.tgz",
"integrity": "sha512-j6G1e8DULJx3ONf6NdR5JiR2ZY3K3PaaqiEuKYkLQO0Czfi1AzrtjfnfCROyWGeDd5IVMKCwsgSmMip9OWijow==",
"dev": true
},
"@types/mime": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz",
@ -19594,6 +19616,11 @@
"yallist": "^4.0.0"
}
},
"lz-string": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.4.4.tgz",
"integrity": "sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY="
},
"magic-string": {
"version": "0.25.7",
"resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.7.tgz",

View file

@ -1,6 +1,6 @@
{
"name": "profectus",
"version": "0.3.0",
"version": "0.3.3",
"private": true,
"scripts": {
"start": "vue-cli-service serve",
@ -12,6 +12,7 @@
"@pixi/particle-emitter": "^5.0.4",
"core-js": "^3.6.5",
"lodash.clonedeep": "^4.5.0",
"lz-string": "^1.4.4",
"nanoevents": "^6.0.2",
"pixi.js": "^6.3.0",
"vue": "^3.2.26",
@ -27,6 +28,7 @@
"@jetblack/operator-overloading": "^0.2.0",
"@rushstack/eslint-patch": "^1.1.0",
"@types/lodash.clonedeep": "^4.5.6",
"@types/lz-string": "^1.3.34",
"@vue/babel-plugin-jsx": "^1.1.1",
"@vue/cli-plugin-babel": "^5.0.3",
"@vue/cli-plugin-eslint": "^5.0.3",

View file

@ -57,6 +57,7 @@
</template>
<script setup lang="ts">
import projInfo from "data/projInfo.json";
import Modal from "components/Modal.vue";
import player, { PlayerData } from "game/player";
import settings from "game/settings";
@ -66,6 +67,7 @@ import Select from "./fields/Select.vue";
import Text from "./fields/Text.vue";
import Save from "./Save.vue";
import Draggable from "vuedraggable";
import LZString from "lz-string";
export type LoadablePlayerData = Omit<Partial<PlayerData>, "id"> & { id: string; error?: unknown };
@ -81,21 +83,32 @@ defineExpose({
const importingFailed = ref(false);
const saveToImport = ref("");
watch(saveToImport, save => {
if (save) {
watch(saveToImport, importedSave => {
if (importedSave) {
nextTick(() => {
try {
const playerData = JSON.parse(decodeURIComponent(escape(atob(save))));
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;
localStorage.setItem(
id,
btoa(unescape(encodeURIComponent(JSON.stringify(playerData))))
);
save(playerData);
saveToImport.value = "";
importingFailed.value = false;
@ -124,14 +137,30 @@ let bank = ref(
const cachedSaves = shallowReactive<Record<string, LoadablePlayerData | undefined>>({});
function getCachedSave(id: string) {
if (cachedSaves[id] == null) {
const save = localStorage.getItem(id);
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 {
cachedSaves[id] = { ...JSON.parse(decodeURIComponent(escape(atob(save)))), id };
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(
@ -162,7 +191,19 @@ function exportSave(id: string) {
if (player.id === id) {
saveToExport = save();
} else {
saveToExport = btoa(unescape(encodeURIComponent(JSON.stringify(saves.value[id]))));
saveToExport = JSON.stringify(saves.value[id]);
switch (projInfo.saveEncoding) {
default:
console.warn(`Unknown save encoding: ${projInfo.saveEncoding}. 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
@ -181,10 +222,7 @@ function duplicateSave(id: string) {
}
const playerData = { ...saves.value[id], id: getUniqueID() };
localStorage.setItem(
playerData.id,
btoa(unescape(encodeURIComponent(JSON.stringify(playerData))))
);
save(playerData as PlayerData);
settings.saves.push(playerData.id);
}
@ -207,12 +245,22 @@ function openSave(id: string) {
}
function newFromPreset(preset: string) {
const playerData = JSON.parse(decodeURIComponent(escape(atob(preset))));
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();
localStorage.setItem(
playerData.id,
btoa(unescape(encodeURIComponent(JSON.stringify(playerData))))
);
save(playerData as PlayerData);
settings.saves.push(playerData.id);
}
@ -225,7 +273,7 @@ function editSave(id: string, newName: string) {
player.name = newName;
save();
} else {
localStorage.setItem(id, btoa(unescape(encodeURIComponent(JSON.stringify(currSave)))));
save(currSave as PlayerData);
cachedSaves[id] = undefined;
}
}

View file

@ -70,7 +70,7 @@ function onUpdate(value: SelectOption) {
}
.vue-dropdown-item {
color: var(--feature-foreground);
color: var(--foreground);
}
.vue-dropdown-item,

View file

@ -133,7 +133,7 @@ export function createResetButton<T extends ClickableOptions & ResetButtonOption
export interface LayerTreeNodeOptions extends TreeNodeOptions {
layerID: string;
color: Computable<string>; // marking as required
display?: Computable<string>;
display?: Computable<CoercableComponent>;
append?: Computable<boolean>;
}
export type LayerTreeNode<T extends LayerTreeNodeOptions> = Replace<
@ -146,7 +146,7 @@ export type LayerTreeNode<T extends LayerTreeNodeOptions> = Replace<
export type GenericLayerTreeNode = Replace<
LayerTreeNode<LayerTreeNodeOptions>,
{
display: ProcessedComputable<string>;
display: ProcessedComputable<CoercableComponent>;
append?: ProcessedComputable<boolean>;
}
>;
@ -161,7 +161,7 @@ export function createLayerTreeNode<T extends LayerTreeNodeOptions>(
processComputable(options as T, "append");
return {
...options,
display: options.layerID,
display: options.display,
onClick: unref((options as unknown as GenericLayerTreeNode).append)
? function () {
if (player.tabs.includes(options.layerID)) {

View file

@ -8,6 +8,8 @@ import { jsx } from "features/feature";
import { createReset } from "features/reset";
import MainDisplay from "features/resources/MainDisplay.vue";
import { createResource } from "features/resources/resource";
import { addTooltip } from "features/tooltips/tooltip";
import { createResourceTooltip } from "features/trees/tree";
import { createLayer } from "game/layers";
import { DecimalSource } from "util/bignum";
import { render } from "util/vue";
@ -35,6 +37,10 @@ const layer = createLayer(id, () => {
color,
reset
}));
addTooltip(treeNode, {
display: createResourceTooltip(points),
pinnable: true
});
const resetButton = createResetButton(() => ({
conversion,

View file

@ -18,5 +18,6 @@
"maxTickLength": 3600,
"offlineLimit": 1,
"enablePausing": true
"enablePausing": true,
"saveEncoding": "lz"
}

View file

@ -35,7 +35,7 @@ const defaultTheme: Theme = {
variables: {
"--foreground": "#dfdfdf",
"--background": "#0f0f0f",
"--feature-foreground": "#eee",
"--feature-foreground": "#0f0f0f",
"--tooltip-background": "rgba(0, 0, 0, 0.75)",
"--raised-background": "#0f0f0f",
"--points": "#ffffff",

View file

@ -11,7 +11,7 @@ import {
Visibility
} from "features/feature";
import "game/notifications";
import { Persistent, PersistentState, persistent } from "game/persistence";
import { Persistent, persistent } from "game/persistence";
import {
Computable,
GetComputableType,
@ -21,7 +21,7 @@ import {
} from "util/computed";
import { createLazyProxy } from "util/proxies";
import { coerceComponent } from "util/vue";
import { Ref, unref, watchEffect } from "vue";
import { unref, watchEffect } from "vue";
import { useToast } from "vue-toastification";
const toast = useToast();
@ -39,9 +39,9 @@ export interface AchievementOptions {
onComplete?: VoidFunction;
}
export interface BaseAchievement extends Persistent<boolean> {
export interface BaseAchievement {
id: string;
earned: Ref<boolean>;
earned: Persistent<boolean>;
complete: VoidFunction;
type: typeof AchievementType;
[Component]: typeof AchievementComponent;
@ -68,17 +68,18 @@ export type GenericAchievement = Replace<
>;
export function createAchievement<T extends AchievementOptions>(
optionsFunc: OptionsFunc<T, Achievement<T>, BaseAchievement>
optionsFunc?: OptionsFunc<T, Achievement<T>, BaseAchievement>
): Achievement<T> {
return createLazyProxy(persistent => {
const achievement = Object.assign(persistent, optionsFunc());
const earned = persistent<boolean>(false);
return createLazyProxy(() => {
const achievement = optionsFunc?.() ?? ({} as ReturnType<NonNullable<typeof optionsFunc>>);
achievement.id = getUniqueID("achievement-");
achievement.type = AchievementType;
achievement[Component] = AchievementComponent;
achievement.earned = achievement[PersistentState];
achievement.earned = earned;
achievement.complete = function () {
achievement[PersistentState].value = true;
earned.value = true;
};
processComputable(achievement as T, "visibility");
@ -122,5 +123,5 @@ export function createAchievement<T extends AchievementOptions>(
}
return achievement as unknown as Achievement<T>;
}, persistent<boolean>(false));
});
}

View file

@ -1,6 +1,6 @@
import ClickableComponent from "features/clickables/Clickable.vue";
import { Resource } from "features/resources/resource";
import { Persistent, PersistentState, persistent } from "game/persistence";
import { Persistent, persistent } from "game/persistence";
import Decimal, { DecimalSource, format, formatWhole } from "util/bignum";
import {
Computable,
@ -49,9 +49,9 @@ export interface BuyableOptions {
onPurchase?: (cost: DecimalSource) => void;
}
export interface BaseBuyable extends Persistent<DecimalSource> {
export interface BaseBuyable {
id: string;
amount: Ref<DecimalSource>;
amount: Persistent<DecimalSource>;
maxed: Ref<boolean>;
canAfford: Ref<boolean>;
canClick: ProcessedComputable<boolean>;
@ -90,8 +90,9 @@ export type GenericBuyable = Replace<
export function createBuyable<T extends BuyableOptions>(
optionsFunc: OptionsFunc<T, Buyable<T>, BaseBuyable>
): Buyable<T> {
return createLazyProxy(persistent => {
const buyable = Object.assign(persistent, optionsFunc());
const amount = persistent<DecimalSource>(0);
return createLazyProxy(() => {
const buyable = optionsFunc();
if (buyable.canPurchase == null && (buyable.resource == null || buyable.cost == null)) {
console.warn(
@ -105,7 +106,7 @@ export function createBuyable<T extends BuyableOptions>(
buyable.type = BuyableType;
buyable[Component] = ClickableComponent;
buyable.amount = buyable[PersistentState];
buyable.amount = amount;
buyable.canAfford = computed(() => {
const genericBuyable = buyable as GenericBuyable;
const cost = unref(genericBuyable.cost);
@ -239,5 +240,5 @@ export function createBuyable<T extends BuyableOptions>(
};
return buyable as unknown as Buyable<T>;
}, persistent<DecimalSource>(0));
});
}

View file

@ -138,4 +138,8 @@ export default defineComponent({
.clickable.small {
min-height: unset;
}
.clickable > * {
pointer-events: none;
}
</style>

View file

@ -70,10 +70,10 @@ export type GenericClickable = Replace<
>;
export function createClickable<T extends ClickableOptions>(
optionsFunc: OptionsFunc<T, Clickable<T>, BaseClickable>
optionsFunc?: OptionsFunc<T, Clickable<T>, BaseClickable>
): Clickable<T> {
return createLazyProxy(() => {
const clickable = optionsFunc();
const clickable = optionsFunc?.() ?? ({} as ReturnType<NonNullable<typeof optionsFunc>>);
clickable.id = getUniqueID("clickable-");
clickable.type = ClickableType;
clickable[Component] = ClickableComponent;

View file

@ -87,4 +87,8 @@ export default defineComponent({
font-size: 10px;
background-color: var(--layer-color);
}
.tile > * {
pointer-events: none;
}
</style>

View file

@ -20,7 +20,7 @@ import {
} from "util/computed";
import { createLazyProxy } from "util/proxies";
import { computed, Ref, unref } from "vue";
import { State, Persistent, PersistentState, persistent } from "game/persistence";
import { State, Persistent, persistent } from "game/persistence";
export const GridType = Symbol("Grid");
@ -205,12 +205,13 @@ export interface GridOptions {
onHold?: (id: string | number, state: State) => void;
}
export interface BaseGrid extends Persistent<Record<string | number, State>> {
export interface BaseGrid {
id: string;
getID: (id: string | number, state: State) => string;
getState: (id: string | number) => State;
setState: (id: string | number, state: State) => void;
cells: Record<string | number, GridCell>;
cellState: Persistent<Record<string | number, State>>;
type: typeof GridType;
[Component]: typeof GridComponent;
[GatherProps]: () => Record<string, unknown>;
@ -244,22 +245,25 @@ export type GenericGrid = Replace<
export function createGrid<T extends GridOptions>(
optionsFunc: OptionsFunc<T, Grid<T>, BaseGrid>
): Grid<T> {
return createLazyProxy(persistent => {
const grid = Object.assign(persistent, optionsFunc());
const cellState = persistent<Record<string | number, State>>({});
return createLazyProxy(() => {
const grid = optionsFunc();
grid.id = getUniqueID("grid-");
grid[Component] = GridComponent;
grid.cellState = cellState;
grid.getID = function (this: GenericGrid, cell: string | number) {
return grid.id + "-" + cell;
};
grid.getState = function (this: GenericGrid, cell: string | number) {
if (this[PersistentState].value[cell] != undefined) {
return this[PersistentState].value[cell];
if (this.cellState.value[cell] != undefined) {
return cellState.value[cell];
}
return this.cells[cell].startState;
};
grid.setState = function (this: GenericGrid, cell: string | number, state: State) {
this[PersistentState].value[cell] = state;
cellState.value[cell] = state;
};
grid.cells = createGridProxy(grid as GenericGrid);
@ -301,5 +305,5 @@ export function createGrid<T extends GridOptions>(
};
return grid as unknown as Grid<T>;
}, persistent({}));
});
}

View file

@ -1,15 +1,16 @@
import InfoboxComponent from "features/infoboxes/Infobox.vue";
import {
CoercableComponent,
Component,
OptionsFunc,
GatherProps,
getUniqueID,
OptionsFunc,
Replace,
setDefault,
StyleValue,
Visibility
} from "features/feature";
import InfoboxComponent from "features/infoboxes/Infobox.vue";
import { Persistent, persistent } from "game/persistence";
import {
Computable,
GetComputableType,
@ -18,8 +19,7 @@ import {
ProcessedComputable
} from "util/computed";
import { createLazyProxy } from "util/proxies";
import { Ref, unref } from "vue";
import { Persistent, PersistentState, persistent } from "game/persistence";
import { unref } from "vue";
export const InfoboxType = Symbol("Infobox");
@ -34,9 +34,9 @@ export interface InfoboxOptions {
display: Computable<CoercableComponent>;
}
export interface BaseInfobox extends Persistent<boolean> {
export interface BaseInfobox {
id: string;
collapsed: Ref<boolean>;
collapsed: Persistent<boolean>;
type: typeof InfoboxType;
[Component]: typeof InfoboxComponent;
[GatherProps]: () => Record<string, unknown>;
@ -66,13 +66,14 @@ export type GenericInfobox = Replace<
export function createInfobox<T extends InfoboxOptions>(
optionsFunc: OptionsFunc<T, Infobox<T>, BaseInfobox>
): Infobox<T> {
return createLazyProxy(persistent => {
const infobox = Object.assign(persistent, optionsFunc());
const collapsed = persistent<boolean>(false);
return createLazyProxy(() => {
const infobox = optionsFunc();
infobox.id = getUniqueID("infobox-");
infobox.type = InfoboxType;
infobox[Component] = InfoboxComponent;
infobox.collapsed = infobox[PersistentState];
infobox.collapsed = collapsed;
processComputable(infobox as T, "visibility");
setDefault(infobox, "visibility", Visibility.Visible);
@ -112,5 +113,5 @@ export function createInfobox<T extends InfoboxOptions>(
};
return infobox as unknown as Infobox<T>;
}, persistent<boolean>(false));
});
}

View file

@ -20,7 +20,7 @@ export interface Link extends SVGAttributes {
}
export interface LinksOptions {
links?: Computable<Link[]>;
links: Computable<Link[]>;
}
export interface BaseLinks {

View file

@ -14,7 +14,7 @@ import {
import MilestoneComponent from "features/milestones/Milestone.vue";
import { globalBus } from "game/events";
import "game/notifications";
import { persistent, Persistent, PersistentState } from "game/persistence";
import { persistent, Persistent } from "game/persistence";
import settings, { registerSettingField } from "game/settings";
import { camelToTitle } from "util/common";
import {
@ -26,7 +26,7 @@ import {
} from "util/computed";
import { createLazyProxy } from "util/proxies";
import { coerceComponent, isCoercableComponent } from "util/vue";
import { computed, Ref, unref, watchEffect } from "vue";
import { computed, unref, watchEffect } from "vue";
import { useToast } from "vue-toastification";
const toast = useToast();
@ -57,9 +57,9 @@ export interface MilestoneOptions {
onComplete?: VoidFunction;
}
export interface BaseMilestone extends Persistent<boolean> {
export interface BaseMilestone {
id: string;
earned: Ref<boolean>;
earned: Persistent<boolean>;
complete: VoidFunction;
type: typeof MilestoneType;
[Component]: typeof MilestoneComponent;
@ -84,17 +84,18 @@ export type GenericMilestone = Replace<
>;
export function createMilestone<T extends MilestoneOptions>(
optionsFunc: OptionsFunc<T, Milestone<T>, BaseMilestone>
optionsFunc?: OptionsFunc<T, Milestone<T>, BaseMilestone>
): Milestone<T> {
return createLazyProxy(persistent => {
const milestone = Object.assign(persistent, optionsFunc());
const earned = persistent<boolean>(false);
return createLazyProxy(() => {
const milestone = optionsFunc?.() ?? ({} as ReturnType<NonNullable<typeof optionsFunc>>);
milestone.id = getUniqueID("milestone-");
milestone.type = MilestoneType;
milestone[Component] = MilestoneComponent;
milestone.earned = milestone[PersistentState];
milestone.earned = earned;
milestone.complete = function () {
milestone[PersistentState].value = true;
earned.value = true;
};
processComputable(milestone as T, "visibility");
@ -168,7 +169,7 @@ export function createMilestone<T extends MilestoneOptions>(
}
return milestone as unknown as Milestone<T>;
}, persistent<boolean>(false));
});
}
declare module "game/settings" {

View file

@ -42,10 +42,10 @@ export type Particles<T extends ParticlesOptions> = Replace<
export type GenericParticles = Particles<ParticlesOptions>;
export function createParticles<T extends ParticlesOptions>(
optionsFunc: OptionsFunc<T, Particles<T>, BaseParticles>
optionsFunc?: OptionsFunc<T, Particles<T>, BaseParticles>
): Particles<T> {
return createLazyProxy(() => {
const particles = optionsFunc();
const particles = optionsFunc?.() ?? ({} as ReturnType<NonNullable<typeof optionsFunc>>);
particles.id = getUniqueID("particles-");
particles.type = ParticlesType;
particles[Component] = ParticlesComponent;

View file

@ -106,4 +106,8 @@ export default defineComponent({
:not(.floating) .tabButton:not(.active) {
border-bottom-color: transparent;
}
.tabButton > * {
pointer-events: none;
}
</style>

View file

@ -11,7 +11,7 @@ import {
} from "features/feature";
import TabButtonComponent from "features/tabs/TabButton.vue";
import TabFamilyComponent from "features/tabs/TabFamily.vue";
import { Persistent, PersistentState, persistent } from "game/persistence";
import { Persistent, persistent } from "game/persistence";
import {
Computable,
GetComputableType,
@ -65,11 +65,11 @@ export interface TabFamilyOptions {
style?: Computable<StyleValue>;
}
export interface BaseTabFamily extends Persistent<string> {
export interface BaseTabFamily {
id: string;
tabs: Record<string, TabButtonOptions>;
activeTab: Ref<GenericTab | CoercableComponent | null>;
selected: Ref<string>;
selected: Persistent<string>;
type: typeof TabFamilyType;
[Component]: typeof TabFamilyComponent;
[GatherProps]: () => Record<string, unknown>;
@ -99,8 +99,9 @@ export function createTabFamily<T extends TabFamilyOptions>(
throw "Cannot create tab family with 0 tabs";
}
return createLazyProxy(persistent => {
const tabFamily = Object.assign(persistent, optionsFunc?.());
const selected = persistent(Object.keys(tabs)[0]);
return createLazyProxy(() => {
const tabFamily = optionsFunc?.() ?? ({} as ReturnType<NonNullable<typeof optionsFunc>>);
tabFamily.id = getUniqueID("tabFamily-");
tabFamily.type = TabFamilyType;
@ -124,15 +125,14 @@ export function createTabFamily<T extends TabFamilyOptions>(
},
{}
);
tabFamily.selected = tabFamily[PersistentState];
tabFamily.selected = selected;
tabFamily.activeTab = computed(() => {
const tabs = unref(processedTabFamily.tabs);
if (
tabFamily[PersistentState].value in tabs &&
unref(tabs[processedTabFamily[PersistentState].value].visibility) ===
Visibility.Visible
selected.value in tabs &&
unref(tabs[selected.value].visibility) === Visibility.Visible
) {
return unref(tabs[processedTabFamily[PersistentState].value].tab);
return unref(tabs[selected.value].tab);
}
const firstTab = Object.values(tabs).find(
tab => unref(tab.visibility) === Visibility.Visible
@ -156,5 +156,5 @@ export function createTabFamily<T extends TabFamilyOptions>(
// This is necessary because board.types is different from T and TabFamily
const processedTabFamily = tabFamily as unknown as TabFamily<T>;
return processedTabFamily;
}, persistent(Object.keys(tabs)[0]));
});
}

View file

@ -15,7 +15,7 @@ import {
ProcessedComputable
} from "util/computed";
import { VueFeature } from "util/vue";
import { Ref } from "vue";
import { nextTick, Ref, unref } from "vue";
import { persistent } from "game/persistence";
declare module "@vue/runtime-dom" {
@ -73,18 +73,6 @@ export function addTooltip<T extends TooltipOptions>(
element: VueFeature,
options: T & ThisType<Tooltip<T>> & Partial<BaseTooltip>
): Tooltip<T> {
if (options.pinnable) {
if ("pinned" in element) {
console.error(
"Cannot add pinnable tooltip to element that already has a property called 'pinned'"
);
options.pinnable = false;
} else {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(element as any).pinned = options.pinned = persistent<boolean>(false);
}
}
processComputable(options as T, "display");
processComputable(options as T, "classes");
processComputable(options as T, "style");
@ -93,25 +81,39 @@ export function addTooltip<T extends TooltipOptions>(
processComputable(options as T, "xoffset");
processComputable(options as T, "yoffset");
const elementComponent = element[Component];
element[Component] = TooltipComponent;
const elementGratherProps = element[GatherProps].bind(element);
element[GatherProps] = function gatherTooltipProps(this: GenericTooltip) {
const { display, classes, style, direction, xoffset, yoffset, pinned } = this;
return {
element: {
[Component]: elementComponent,
[GatherProps]: elementGratherProps
},
display,
classes,
style,
direction,
xoffset,
yoffset,
pinned
};
}.bind(options as GenericTooltip);
nextTick(() => {
if (options.pinnable) {
if ("pinned" in element) {
console.error(
"Cannot add pinnable tooltip to element that already has a property called 'pinned'"
);
options.pinnable = false;
} else {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(element as any).pinned = options.pinned = persistent<boolean>(false);
}
}
const elementComponent = element[Component];
element[Component] = TooltipComponent;
const elementGratherProps = element[GatherProps].bind(element);
element[GatherProps] = function gatherTooltipProps(this: GenericTooltip) {
const { display, classes, style, direction, xoffset, yoffset, pinned } = this;
return {
element: {
[Component]: elementComponent,
[GatherProps]: elementGratherProps
},
display,
classes,
style: unref(style),
direction,
xoffset,
yoffset,
pinned
};
}.bind(options as GenericTooltip);
});
return options as unknown as Tooltip<T>;
}

View file

@ -112,4 +112,8 @@ export default defineComponent({
text-transform: capitalize;
display: flex;
}
.treeNode > *:first-child > * {
pointer-events: none;
}
</style>

View file

@ -73,10 +73,10 @@ export type GenericTreeNode = Replace<
>;
export function createTreeNode<T extends TreeNodeOptions>(
optionsFunc: OptionsFunc<T, TreeNode<T>, BaseTreeNode>
optionsFunc?: OptionsFunc<T, TreeNode<T>, BaseTreeNode>
): TreeNode<T> {
return createLazyProxy(() => {
const treeNode = optionsFunc();
const treeNode = optionsFunc?.() ?? ({} as ReturnType<NonNullable<typeof optionsFunc>>);
treeNode.id = getUniqueID("treeNode-");
treeNode.type = TreeNodeType;
treeNode[Component] = TreeNodeComponent;

View file

@ -141,4 +141,8 @@ export default defineComponent({
width: 120px;
font-size: 10px;
}
.upgrade > * {
pointer-events: none;
}
</style>

View file

@ -24,7 +24,7 @@ import {
} from "util/computed";
import { createLazyProxy } from "util/proxies";
import { computed, Ref, unref } from "vue";
import { persistent, Persistent, PersistentState } from "game/persistence";
import { persistent, Persistent } from "game/persistence";
export const UpgradeType = Symbol("Upgrade");
@ -47,9 +47,9 @@ export interface UpgradeOptions {
onPurchase?: VoidFunction;
}
export interface BaseUpgrade extends Persistent<boolean> {
export interface BaseUpgrade {
id: string;
bought: Ref<boolean>;
bought: Persistent<boolean>;
canPurchase: Ref<boolean>;
purchase: VoidFunction;
type: typeof UpgradeType;
@ -81,8 +81,9 @@ export type GenericUpgrade = Replace<
export function createUpgrade<T extends UpgradeOptions>(
optionsFunc: OptionsFunc<T, Upgrade<T>, BaseUpgrade>
): Upgrade<T> {
return createLazyProxy(persistent => {
const upgrade = Object.assign(persistent, optionsFunc());
const bought = persistent<boolean>(false);
return createLazyProxy(() => {
const upgrade = optionsFunc();
upgrade.id = getUniqueID("upgrade-");
upgrade.type = UpgradeType;
upgrade[Component] = UpgradeComponent;
@ -94,7 +95,7 @@ export function createUpgrade<T extends UpgradeOptions>(
);
}
upgrade.bought = upgrade[PersistentState];
upgrade.bought = bought;
if (upgrade.canAfford == null) {
upgrade.canAfford = computed(() => {
const genericUpgrade = upgrade as GenericUpgrade;
@ -124,7 +125,7 @@ export function createUpgrade<T extends UpgradeOptions>(
unref(genericUpgrade.cost)
);
}
genericUpgrade[PersistentState].value = true;
bought.value = true;
genericUpgrade.onPurchase?.();
};
@ -167,7 +168,7 @@ export function createUpgrade<T extends UpgradeOptions>(
};
return upgrade as unknown as Upgrade<T>;
}, persistent<boolean>(false));
});
}
export function setupAutoPurchase(

View file

@ -2,6 +2,7 @@ import projInfo from "data/projInfo.json";
import { Themes } from "data/themes";
import { CoercableComponent } from "features/feature";
import { globalBus } from "game/events";
import LZString from "lz-string";
import { hardReset } from "util/save";
import { reactive, watch } from "vue";
@ -23,20 +24,44 @@ const state = reactive<Partial<Settings>>({
watch(
state,
state =>
localStorage.setItem(
projInfo.id,
btoa(unescape(encodeURIComponent(JSON.stringify(state))))
),
state => {
let stringifiedSettings = JSON.stringify(state);
switch (projInfo.saveEncoding) {
default:
console.warn(`Unknown save encoding: ${projInfo.saveEncoding}. Defaulting to lz`);
case "lz":
stringifiedSettings = LZString.compressToUTF16(stringifiedSettings);
break;
case "base64":
stringifiedSettings = btoa(unescape(encodeURIComponent(stringifiedSettings)));
break;
case "plain":
break;
}
localStorage.setItem(projInfo.id, stringifiedSettings);
},
{ deep: true }
);
export default window.settings = state as Settings;
export function loadSettings(): void {
try {
const item: string | null = localStorage.getItem(projInfo.id);
let item: string | null = localStorage.getItem(projInfo.id);
if (item != null && item !== "") {
const settings = JSON.parse(decodeURIComponent(escape(atob(item))));
if (item[0] === "{") {
// plaintext. No processing needed
} else if (item[0] === "e") {
// Assumed to be base64, which starts with e
item = decodeURIComponent(escape(atob(item)));
} else if (item[0] === "ᯡ") {
// Assumed to be lz, which starts with ᯡ
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
item = LZString.decompressFromUTF16(item)!;
} else {
console.warn("Unable to determine settings encoding", item);
return;
}
const settings = JSON.parse(item);
if (typeof settings === "object") {
Object.assign(state, settings);
}

View file

@ -2,6 +2,7 @@ import projInfo from "data/projInfo.json";
import player, { Player, PlayerData, stringifySave } from "game/player";
import settings, { loadSettings } from "game/settings";
import { ProxyState } from "./proxies";
import LZString from "lz-string";
export function setupInitialStore(player: Partial<PlayerData> = {}): Player {
return Object.assign(
@ -23,9 +24,21 @@ export function setupInitialStore(player: Partial<PlayerData> = {}): Player {
) as Player;
}
export function save(): string {
const stringifiedSave = btoa(unescape(encodeURIComponent(stringifySave(player[ProxyState]))));
localStorage.setItem(player.id, stringifiedSave);
export function save(playerData?: PlayerData): string {
let stringifiedSave = stringifySave(playerData ?? player[ProxyState]);
switch (projInfo.saveEncoding) {
default:
console.warn(`Unknown save encoding: ${projInfo.saveEncoding}. Defaulting to lz`);
case "lz":
stringifiedSave = LZString.compressToUTF16(stringifiedSave);
break;
case "base64":
stringifiedSave = btoa(unescape(encodeURIComponent(stringifiedSave)));
break;
case "plain":
break;
}
localStorage.setItem((playerData ?? player[ProxyState]).id, stringifiedSave);
return stringifiedSave;
}
@ -34,12 +47,24 @@ export async function load(): Promise<void> {
loadSettings();
try {
const save = localStorage.getItem(settings.active);
let save = localStorage.getItem(settings.active);
if (save == null) {
await loadSave(newSave());
return;
}
const player = JSON.parse(decodeURIComponent(escape(atob(save))));
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 {
throw `Unable to determine save encoding`;
}
const player = JSON.parse(save);
if (player.modID !== projInfo.id) {
await loadSave(newSave());
return;
@ -55,7 +80,7 @@ export async function load(): Promise<void> {
export function newSave(): PlayerData {
const id = getUniqueID();
const player = setupInitialStore({ id });
localStorage.setItem(id, btoa(unescape(encodeURIComponent(stringifySave(player)))));
save(player);
settings.saves.push(id);