Merge remote-tracking branch 'template/main'

This commit is contained in:
thepaperpilot 2022-04-23 18:25:17 -05:00
commit df26b9b756
33 changed files with 1372 additions and 1142 deletions

View file

@ -6,6 +6,42 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
## [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 ## [0.2.0] - 2022-03-27
### Added ### Added
- Particles feature - Particles feature

1550
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{ {
"name": "profectus", "name": "profectus",
"version": "0.2.0", "version": "0.3.0",
"private": true, "private": true,
"scripts": { "scripts": {
"start": "vue-cli-service serve", "start": "vue-cli-service serve",
@ -9,11 +9,11 @@
"lint": "vue-cli-service lint" "lint": "vue-cli-service lint"
}, },
"dependencies": { "dependencies": {
"@pixi/particle-emitter": "^5.0.4",
"core-js": "^3.6.5", "core-js": "^3.6.5",
"lodash.clonedeep": "^4.5.0", "lodash.clonedeep": "^4.5.0",
"nanoevents": "^6.0.2", "nanoevents": "^6.0.2",
"particles.vue3": "^2.0.3", "pixi.js": "^6.3.0",
"tsparticles": "^2.0.3",
"vue": "^3.2.26", "vue": "^3.2.26",
"vue-next-select": "^2.10.2", "vue-next-select": "^2.10.2",
"vue-panzoom": "^1.1.6", "vue-panzoom": "^1.1.6",

View file

@ -1 +0,0 @@
eyJpZCI6InRtdC14LTEwNSIsIm5hbWUiOiJEZWZhdWx0IFNhZmZmZiAtIHNvbWV0aGluZyBlbHNlIiwidGFicyI6WyJtYWluIiwiYyJdLCJ0aW1lIjoxNjI0MjQ1MjYxMDg3LCJhdXRvc2F2ZSI6dHJ1ZSwib2ZmbGluZVByb2QiOnRydWUsInRpbWVQbGF5ZWQiOiIzNDQ4LjYxNTc4MTcwOTAxIiwia2VlcEdvaW5nIjpmYWxzZSwibGFzdFRlblRpY2tzIjpbMC4wNTEsMC4wNSwwLjA0OSwwLjA1LDAuMDUsMC4wNTEsMC4wNDksMC4wNSwwLjA1LDAuMDUxXSwic2hvd1RQUyI6dHJ1ZSwibXNEaXNwbGF5IjoiYWxsIiwiaGlkZUNoYWxsZW5nZXMiOmZhbHNlLCJ0aGVtZSI6InBhcGVyIiwic3VidGFicyI6e30sIm1pbmltaXplZCI6e30sIm1vZElEIjoidG10LXgiLCJtb2RWZXJzaW9uIjoiMC4wIiwicG9pbnRzIjoiMzMwMC4zNzc3NzM4NTkwNTUiLCJtYWluIjp7InVwZ3JhZGVzIjpbXSwiYWNoaWV2ZW1lbnRzIjpbXSwibWlsZXN0b25lcyI6W10sImluZm9ib3hlcyI6e319LCJmIjp7InVwZ3JhZGVzIjpbXSwiYWNoaWV2ZW1lbnRzIjpbXSwibWlsZXN0b25lcyI6W10sImluZm9ib3hlcyI6e30sImNsaWNrYWJsZXMiOnsiMTEiOiJTdGFydCJ9LCJ1bmxvY2tlZCI6ZmFsc2UsInBvaW50cyI6IjAiLCJib29wIjpmYWxzZX0sImMiOnsidXBncmFkZXMiOlsiMTEiXSwiYWNoaWV2ZW1lbnRzIjpbXSwibWlsZXN0b25lcyI6W10sImluZm9ib3hlcyI6e30sImJ1eWFibGVzIjp7IjExIjoiMCJ9LCJjaGFsbGVuZ2VzIjp7IjExIjoiMCJ9LCJ1bmxvY2tlZCI6dHJ1ZSwicG9pbnRzIjoiMCIsImJlc3QiOiIxIiwidG90YWwiOiIwIiwiYmVlcCI6ZmFsc2UsInRoaW5neSI6InBvaW50eSIsIm90aGVyVGhpbmd5IjoxMCwic3BlbnRPbkJ1eWFibGVzIjoiMCJ9LCJhIjp7InVwZ3JhZGVzIjpbXSwiYWNoaWV2ZW1lbnRzIjpbIjExIl0sIm1pbGVzdG9uZXMiOltdLCJpbmZvYm94ZXMiOnt9LCJ1bmxvY2tlZCI6dHJ1ZSwicG9pbnRzIjoiMCJ9LCJnIjp7InVwZ3JhZGVzIjpbXSwiYWNoaWV2ZW1lbnRzIjpbXSwibWlsZXN0b25lcyI6W10sImluZm9ib3hlcyI6e319LCJoIjp7InVwZ3JhZGVzIjpbXSwiYWNoaWV2ZW1lbnRzIjpbXSwibWlsZXN0b25lcyI6W10sImluZm9ib3hlcyI6e319LCJzcG9vayI6eyJ1cGdyYWRlcyI6W10sImFjaGlldmVtZW50cyI6W10sIm1pbGVzdG9uZXMiOltdLCJpbmZvYm94ZXMiOnt9fSwib29tcHNNYWciOjAsImxhc3RQb2ludHMiOiIzMzAwLjM3Nzc3Mzg1OTA1NSJ9

View file

@ -24,7 +24,7 @@
import projInfo from "data/projInfo.json"; import projInfo from "data/projInfo.json";
import { CoercableComponent, StyleValue } from "features/feature"; import { CoercableComponent, StyleValue } from "features/feature";
import { FeatureNode } from "game/layers"; import { FeatureNode } from "game/layers";
import { PersistentRef } from "game/persistence"; import { Persistent } from "game/persistence";
import player from "game/player"; import player from "game/player";
import { computeComponent, processedPropType, wrapRef } from "util/vue"; import { computeComponent, processedPropType, wrapRef } from "util/vue";
import { computed, defineComponent, nextTick, PropType, Ref, ref, toRefs, unref, watch } from "vue"; import { computed, defineComponent, nextTick, PropType, Ref, ref, toRefs, unref, watch } from "vue";
@ -46,7 +46,7 @@ export default defineComponent({
required: true required: true
}, },
minimized: { minimized: {
type: Object as PropType<PersistentRef<boolean>>, type: Object as PropType<Persistent<boolean>>,
required: true required: true
}, },
minWidth: { minWidth: {

View file

@ -29,6 +29,7 @@ function updateTop() {
} }
nextTick(updateTop); nextTick(updateTop);
document.fonts.ready.then(updateTop);
onMounted(() => { onMounted(() => {
const el = element.value?.parentElement; const el = element.value?.parentElement;

View file

@ -5,7 +5,7 @@ import {
GenericClickable GenericClickable
} from "features/clickables/clickable"; } from "features/clickables/clickable";
import { GenericConversion } from "features/conversion"; import { GenericConversion } from "features/conversion";
import { CoercableComponent, jsx, Replace, setDefault } from "features/feature"; import { CoercableComponent, OptionsFunc, jsx, Replace, setDefault } from "features/feature";
import { displayResource } from "features/resources/resource"; import { displayResource } from "features/resources/resource";
import { import {
createTreeNode, createTreeNode,
@ -15,7 +15,7 @@ import {
TreeNodeOptions TreeNodeOptions
} from "features/trees/tree"; } from "features/trees/tree";
import player from "game/player"; import player from "game/player";
import Decimal from "util/bignum"; import Decimal, { DecimalSource } from "util/bignum";
import { import {
Computable, Computable,
GetComputableType, GetComputableType,
@ -33,6 +33,7 @@ export interface ResetButtonOptions extends ClickableOptions {
showNextAt?: Computable<boolean>; showNextAt?: Computable<boolean>;
display?: Computable<CoercableComponent>; display?: Computable<CoercableComponent>;
canClick?: Computable<boolean>; canClick?: Computable<boolean>;
minimumGain?: Computable<DecimalSource>;
} }
export type ResetButton<T extends ResetButtonOptions> = Replace< export type ResetButton<T extends ResetButtonOptions> = Replace<
@ -42,6 +43,7 @@ export type ResetButton<T extends ResetButtonOptions> = Replace<
showNextAt: GetComputableTypeWithDefault<T["showNextAt"], true>; showNextAt: GetComputableTypeWithDefault<T["showNextAt"], true>;
display: GetComputableTypeWithDefault<T["display"], Ref<JSX.Element>>; display: GetComputableTypeWithDefault<T["display"], Ref<JSX.Element>>;
canClick: GetComputableTypeWithDefault<T["canClick"], Ref<boolean>>; canClick: GetComputableTypeWithDefault<T["canClick"], Ref<boolean>>;
minimumGain: GetComputableTypeWithDefault<T["minimumGain"], 1>;
onClick: VoidFunction; onClick: VoidFunction;
} }
>; >;
@ -53,17 +55,19 @@ export type GenericResetButton = Replace<
showNextAt: ProcessedComputable<boolean>; showNextAt: ProcessedComputable<boolean>;
display: ProcessedComputable<CoercableComponent>; display: ProcessedComputable<CoercableComponent>;
canClick: ProcessedComputable<boolean>; canClick: ProcessedComputable<boolean>;
minimumGain: ProcessedComputable<DecimalSource>;
} }
>; >;
export function createResetButton<T extends ClickableOptions & ResetButtonOptions>( export function createResetButton<T extends ClickableOptions & ResetButtonOptions>(
optionsFunc: () => T optionsFunc: OptionsFunc<T>
): ResetButton<T> { ): ResetButton<T> {
return createClickable(() => { return createClickable(() => {
const resetButton = optionsFunc(); const resetButton = optionsFunc();
processComputable(resetButton as T, "showNextAt"); processComputable(resetButton as T, "showNextAt");
setDefault(resetButton, "showNextAt", true); setDefault(resetButton, "showNextAt", true);
setDefault(resetButton, "minimumGain", 1);
if (resetButton.resetDescription == null) { if (resetButton.resetDescription == null) {
resetButton.resetDescription = computed(() => resetButton.resetDescription = computed(() =>
@ -80,16 +84,22 @@ export function createResetButton<T extends ClickableOptions & ResetButtonOption
<b> <b>
{displayResource( {displayResource(
resetButton.conversion.gainResource, resetButton.conversion.gainResource,
unref(resetButton.conversion.currentGain) Decimal.max(
unref(resetButton.conversion.actualGain),
unref(resetButton.minimumGain as ProcessedComputable<DecimalSource>)
)
)} )}
</b>{" "} </b>{" "}
{resetButton.conversion.gainResource.displayName} {resetButton.conversion.gainResource.displayName}
<div v-show={unref(resetButton.showNextAt)}> <div v-show={unref(resetButton.showNextAt)}>
<br /> <br />
Next:{" "} {resetButton.conversion.buyMax ? "Next:" : "Req:"}{" "}
{displayResource( {displayResource(
resetButton.conversion.baseResource, resetButton.conversion.baseResource,
unref(resetButton.conversion.nextAt) resetButton.conversion.buyMax ||
Decimal.floor(unref(resetButton.conversion.actualGain)).neq(1)
? unref(resetButton.conversion.nextAt)
: unref(resetButton.conversion.currentAt)
)}{" "} )}{" "}
{resetButton.conversion.baseResource.displayName} {resetButton.conversion.baseResource.displayName}
</div> </div>
@ -99,7 +109,10 @@ export function createResetButton<T extends ClickableOptions & ResetButtonOption
if (resetButton.canClick == null) { if (resetButton.canClick == null) {
resetButton.canClick = computed(() => resetButton.canClick = computed(() =>
Decimal.gt(unref(resetButton.conversion.currentGain), 0) Decimal.gte(
unref(resetButton.conversion.actualGain),
unref(resetButton.minimumGain as ProcessedComputable<DecimalSource>)
)
); );
} }
@ -139,7 +152,7 @@ export type GenericLayerTreeNode = Replace<
>; >;
export function createLayerTreeNode<T extends LayerTreeNodeOptions>( export function createLayerTreeNode<T extends LayerTreeNodeOptions>(
optionsFunc: () => T optionsFunc: OptionsFunc<T>
): LayerTreeNode<T> { ): LayerTreeNode<T> {
return createTreeNode(() => { return createTreeNode(() => {
const options = optionsFunc(); const options = optionsFunc();

View file

@ -13,8 +13,8 @@ import { DecimalSource } from "util/bignum";
import { render } from "util/vue"; import { render } from "util/vue";
import { createLayerTreeNode, createResetButton } from "../common"; import { createLayerTreeNode, createResetButton } from "../common";
const layer = createLayer(() => { const id = "p";
const id = "p"; const layer = createLayer(id, () => {
const name = "Prestige"; const name = "Prestige";
const color = "#4BDC13"; const color = "#4BDC13";
const points = createResource<DecimalSource>(0, "prestige points"); const points = createResource<DecimalSource>(0, "prestige points");
@ -43,7 +43,6 @@ const layer = createLayer(() => {
})); }));
return { return {
id,
name, name,
color, color,
points, points,

View file

@ -16,7 +16,7 @@ import f from "./layers/aca/f";
/** /**
* @hidden * @hidden
*/ */
export const main = createLayer(() => { export const main = createLayer("main", () => {
const points = createResource<DecimalSource>(10); const points = createResource<DecimalSource>(10);
const best = trackBest(points); const best = trackBest(points);
const total = trackTotal(points); const total = trackTotal(points);
@ -62,7 +62,6 @@ export const main = createLayer(() => {
// but I'd recommend it over trying to remember what does and doesn't need to be included. // but I'd recommend it over trying to remember what does and doesn't need to be included.
// Officially all you need are anything with persistency or that you want to access elsewhere // Officially all you need are anything with persistency or that you want to access elsewhere
return { return {
id: "main",
name: "Tree", name: "Tree",
display: jsx(() => ( display: jsx(() => (
<> <>

View file

@ -2,6 +2,7 @@ import AchievementComponent from "features/achievements/Achievement.vue";
import { import {
CoercableComponent, CoercableComponent,
Component, Component,
OptionsFunc,
GatherProps, GatherProps,
getUniqueID, getUniqueID,
Replace, Replace,
@ -10,7 +11,7 @@ import {
Visibility Visibility
} from "features/feature"; } from "features/feature";
import "game/notifications"; import "game/notifications";
import { Persistent, makePersistent, PersistentState } from "game/persistence"; import { Persistent, PersistentState, persistent } from "game/persistence";
import { import {
Computable, Computable,
GetComputableType, GetComputableType,
@ -67,11 +68,10 @@ export type GenericAchievement = Replace<
>; >;
export function createAchievement<T extends AchievementOptions>( export function createAchievement<T extends AchievementOptions>(
optionsFunc: () => T & ThisType<Achievement<T>> optionsFunc: OptionsFunc<T, Achievement<T>, BaseAchievement>
): Achievement<T> { ): Achievement<T> {
return createLazyProxy(() => { return createLazyProxy(persistent => {
const achievement: T & Partial<BaseAchievement> = optionsFunc(); const achievement = Object.assign(persistent, optionsFunc());
makePersistent<boolean>(achievement, false);
achievement.id = getUniqueID("achievement-"); achievement.id = getUniqueID("achievement-");
achievement.type = AchievementType; achievement.type = AchievementType;
achievement[Component] = AchievementComponent; achievement[Component] = AchievementComponent;
@ -122,5 +122,5 @@ export function createAchievement<T extends AchievementOptions>(
} }
return achievement as unknown as Achievement<T>; return achievement as unknown as Achievement<T>;
}); }, persistent<boolean>(false));
} }

View file

@ -2,6 +2,7 @@ import BarComponent from "features/bars/Bar.vue";
import { import {
CoercableComponent, CoercableComponent,
Component, Component,
OptionsFunc,
GatherProps, GatherProps,
getUniqueID, getUniqueID,
Replace, Replace,
@ -79,9 +80,11 @@ export type GenericBar = Replace<
} }
>; >;
export function createBar<T extends BarOptions>(optionsFunc: () => T & ThisType<Bar<T>>): Bar<T> { export function createBar<T extends BarOptions>(
optionsFunc: OptionsFunc<T, Bar<T>, BaseBar>
): Bar<T> {
return createLazyProxy(() => { return createLazyProxy(() => {
const bar: T & Partial<BaseBar> = optionsFunc(); const bar = optionsFunc();
bar.id = getUniqueID("bar-"); bar.id = getUniqueID("bar-");
bar.type = BarType; bar.type = BarType;
bar[Component] = BarComponent; bar[Component] = BarComponent;

View file

@ -1,6 +1,7 @@
import BoardComponent from "features/boards/Board.vue"; import BoardComponent from "features/boards/Board.vue";
import { import {
Component, Component,
OptionsFunc,
findFeatures, findFeatures,
GatherProps, GatherProps,
getUniqueID, getUniqueID,
@ -10,7 +11,7 @@ import {
Visibility Visibility
} from "features/feature"; } from "features/feature";
import { globalBus } from "game/events"; import { globalBus } from "game/events";
import { State, Persistent, makePersistent, PersistentState } from "game/persistence"; import { State, Persistent, PersistentState, persistent } from "game/persistence";
import { isFunction } from "util/common"; import { isFunction } from "util/common";
import { import {
Computable, Computable,
@ -197,137 +198,142 @@ export type GenericBoard = Replace<
>; >;
export function createBoard<T extends BoardOptions>( export function createBoard<T extends BoardOptions>(
optionsFunc: () => T & ThisType<Board<T>> optionsFunc: OptionsFunc<T, Board<T>, BaseBoard>
): Board<T> { ): Board<T> {
return createLazyProxy(() => { return createLazyProxy(
const board: T & Partial<BaseBoard> = optionsFunc(); persistent => {
makePersistent<BoardData>(board, { const board = Object.assign(persistent, optionsFunc());
board.id = getUniqueID("board-");
board.type = BoardType;
board[Component] = BoardComponent;
board.nodes = computed(() => processedBoard[PersistentState].value.nodes);
board.selectedNode = computed(
() =>
processedBoard.nodes.value.find(
node => node.id === board[PersistentState].value.selectedNode
) || null
);
board.selectedAction = computed(() => {
const selectedNode = processedBoard.selectedNode.value;
if (selectedNode == null) {
return null;
}
const type = processedBoard.types[selectedNode.type];
if (type.actions == null) {
return null;
}
return (
type.actions.find(
action => action.id === processedBoard[PersistentState].value.selectedAction
) || null
);
});
board.links = computed(() => {
if (processedBoard.selectedAction.value == null) {
return null;
}
if (
processedBoard.selectedAction.value.links &&
processedBoard.selectedNode.value
) {
return getNodeProperty(
processedBoard.selectedAction.value.links,
processedBoard.selectedNode.value
);
}
return null;
});
processComputable(board as T, "visibility");
setDefault(board, "visibility", Visibility.Visible);
processComputable(board as T, "width");
setDefault(board, "width", "100%");
processComputable(board as T, "height");
setDefault(board, "height", "400px");
processComputable(board as T, "classes");
processComputable(board as T, "style");
for (const type in board.types) {
const nodeType: NodeTypeOptions & Partial<BaseNodeType> = board.types[type];
processComputable(nodeType as NodeTypeOptions, "title");
processComputable(nodeType as NodeTypeOptions, "label");
processComputable(nodeType as NodeTypeOptions, "size");
setDefault(nodeType, "size", 50);
processComputable(nodeType as NodeTypeOptions, "draggable");
setDefault(nodeType, "draggable", false);
processComputable(nodeType as NodeTypeOptions, "shape");
setDefault(nodeType, "shape", Shape.Circle);
processComputable(nodeType as NodeTypeOptions, "canAccept");
setDefault(nodeType, "canAccept", false);
processComputable(nodeType as NodeTypeOptions, "progress");
processComputable(nodeType as NodeTypeOptions, "progressDisplay");
setDefault(nodeType, "progressDisplay", ProgressDisplay.Fill);
processComputable(nodeType as NodeTypeOptions, "progressColor");
setDefault(nodeType, "progressColor", "none");
processComputable(nodeType as NodeTypeOptions, "fillColor");
processComputable(nodeType as NodeTypeOptions, "outlineColor");
processComputable(nodeType as NodeTypeOptions, "titleColor");
processComputable(nodeType as NodeTypeOptions, "actionDistance");
setDefault(nodeType, "actionDistance", Math.PI / 6);
nodeType.nodes = computed(() =>
board[PersistentState].value.nodes.filter(node => node.type === type)
);
setDefault(nodeType, "onClick", function (node: BoardNode) {
board[PersistentState].value.selectedNode = node.id;
});
if (nodeType.actions) {
for (const action of nodeType.actions) {
processComputable(action, "visibility");
setDefault(action, "visibility", Visibility.Visible);
processComputable(action, "icon");
processComputable(action, "fillColor");
processComputable(action, "tooltip");
processComputable(action, "links");
}
}
}
board[GatherProps] = function (this: GenericBoard) {
const {
nodes,
types,
[PersistentState]: state,
visibility,
width,
height,
style,
classes,
links,
selectedAction,
selectedNode
} = this;
return {
nodes,
types,
[PersistentState]: state,
visibility,
width,
height,
style: unref(style),
classes,
links,
selectedAction,
selectedNode
};
};
// This is necessary because board.types is different from T and Board
const processedBoard = board as unknown as Board<T>;
return processedBoard;
},
persistent<BoardData>({
nodes: [], nodes: [],
selectedNode: null, selectedNode: null,
selectedAction: null selectedAction: null
}); })
board.id = getUniqueID("board-"); );
board.type = BoardType;
board[Component] = BoardComponent;
board.nodes = computed(() => processedBoard[PersistentState].value.nodes);
board.selectedNode = computed(
() =>
processedBoard.nodes.value.find(
node => node.id === board[PersistentState].value.selectedNode
) || null
);
board.selectedAction = computed(() => {
const selectedNode = processedBoard.selectedNode.value;
if (selectedNode == null) {
return null;
}
const type = processedBoard.types[selectedNode.type];
if (type.actions == null) {
return null;
}
return (
type.actions.find(
action => action.id === processedBoard[PersistentState].value.selectedAction
) || null
);
});
board.links = computed(() => {
if (processedBoard.selectedAction.value == null) {
return null;
}
if (processedBoard.selectedAction.value.links && processedBoard.selectedNode.value) {
return getNodeProperty(
processedBoard.selectedAction.value.links,
processedBoard.selectedNode.value
);
}
return null;
});
processComputable(board as T, "visibility");
setDefault(board, "visibility", Visibility.Visible);
processComputable(board as T, "width");
setDefault(board, "width", "100%");
processComputable(board as T, "height");
setDefault(board, "height", "400px");
processComputable(board as T, "classes");
processComputable(board as T, "style");
for (const type in board.types) {
const nodeType: NodeTypeOptions & Partial<BaseNodeType> = board.types[type];
processComputable(nodeType as NodeTypeOptions, "title");
processComputable(nodeType as NodeTypeOptions, "label");
processComputable(nodeType as NodeTypeOptions, "size");
setDefault(nodeType, "size", 50);
processComputable(nodeType as NodeTypeOptions, "draggable");
setDefault(nodeType, "draggable", false);
processComputable(nodeType as NodeTypeOptions, "shape");
setDefault(nodeType, "shape", Shape.Circle);
processComputable(nodeType as NodeTypeOptions, "canAccept");
setDefault(nodeType, "canAccept", false);
processComputable(nodeType as NodeTypeOptions, "progress");
processComputable(nodeType as NodeTypeOptions, "progressDisplay");
setDefault(nodeType, "progressDisplay", ProgressDisplay.Fill);
processComputable(nodeType as NodeTypeOptions, "progressColor");
setDefault(nodeType, "progressColor", "none");
processComputable(nodeType as NodeTypeOptions, "fillColor");
processComputable(nodeType as NodeTypeOptions, "outlineColor");
processComputable(nodeType as NodeTypeOptions, "titleColor");
processComputable(nodeType as NodeTypeOptions, "actionDistance");
setDefault(nodeType, "actionDistance", Math.PI / 6);
nodeType.nodes = computed(() =>
board[PersistentState].value.nodes.filter(node => node.type === type)
);
setDefault(nodeType, "onClick", function (node: BoardNode) {
board[PersistentState].value.selectedNode = node.id;
});
if (nodeType.actions) {
for (const action of nodeType.actions) {
processComputable(action, "visibility");
setDefault(action, "visibility", Visibility.Visible);
processComputable(action, "icon");
processComputable(action, "fillColor");
processComputable(action, "tooltip");
processComputable(action, "links");
}
}
}
board[GatherProps] = function (this: GenericBoard) {
const {
nodes,
types,
[PersistentState]: state,
visibility,
width,
height,
style,
classes,
links,
selectedAction,
selectedNode
} = this;
return {
nodes,
types,
[PersistentState]: state,
visibility,
width,
height,
style: unref(style),
classes,
links,
selectedAction,
selectedNode
};
};
// This is necessary because board.types is different from T and Board
const processedBoard = board as unknown as Board<T>;
return processedBoard;
});
} }
export function getNodeProperty<T>(property: NodeComputable<T>, node: BoardNode): T { export function getNodeProperty<T>(property: NodeComputable<T>, node: BoardNode): T {

View file

@ -1,6 +1,6 @@
import ClickableComponent from "features/clickables/Clickable.vue"; import ClickableComponent from "features/clickables/Clickable.vue";
import { Resource } from "features/resources/resource"; import { Resource } from "features/resources/resource";
import { Persistent, makePersistent, PersistentState } from "game/persistence"; import { Persistent, PersistentState, persistent } from "game/persistence";
import Decimal, { DecimalSource, format, formatWhole } from "util/bignum"; import Decimal, { DecimalSource, format, formatWhole } from "util/bignum";
import { import {
Computable, Computable,
@ -15,6 +15,7 @@ import { computed, Ref, unref } from "vue";
import { import {
CoercableComponent, CoercableComponent,
Component, Component,
OptionsFunc,
GatherProps, GatherProps,
getUniqueID, getUniqueID,
jsx, jsx,
@ -87,10 +88,10 @@ export type GenericBuyable = Replace<
>; >;
export function createBuyable<T extends BuyableOptions>( export function createBuyable<T extends BuyableOptions>(
optionsFunc: () => T & ThisType<Buyable<T>> optionsFunc: OptionsFunc<T, Buyable<T>, BaseBuyable>
): Buyable<T> { ): Buyable<T> {
return createLazyProxy(() => { return createLazyProxy(persistent => {
const buyable: T & Partial<BaseBuyable> = optionsFunc(); const buyable = Object.assign(persistent, optionsFunc());
if (buyable.canPurchase == null && (buyable.resource == null || buyable.cost == null)) { if (buyable.canPurchase == null && (buyable.resource == null || buyable.cost == null)) {
console.warn( console.warn(
@ -100,7 +101,6 @@ export function createBuyable<T extends BuyableOptions>(
throw "Cannot create buyable without a canPurchase property or a resource and cost property"; throw "Cannot create buyable without a canPurchase property or a resource and cost property";
} }
makePersistent<DecimalSource>(buyable, 0);
buyable.id = getUniqueID("buyable-"); buyable.id = getUniqueID("buyable-");
buyable.type = BuyableType; buyable.type = BuyableType;
buyable[Component] = ClickableComponent; buyable[Component] = ClickableComponent;
@ -239,5 +239,5 @@ export function createBuyable<T extends BuyableOptions>(
}; };
return buyable as unknown as Buyable<T>; return buyable as unknown as Buyable<T>;
}); }, persistent<DecimalSource>(0));
} }

View file

@ -4,6 +4,7 @@ import ChallengeComponent from "features/challenges/Challenge.vue";
import { import {
CoercableComponent, CoercableComponent,
Component, Component,
OptionsFunc,
GatherProps, GatherProps,
getUniqueID, getUniqueID,
jsx, jsx,
@ -15,7 +16,7 @@ import {
import { GenericReset } from "features/reset"; import { GenericReset } from "features/reset";
import { Resource } from "features/resources/resource"; import { Resource } from "features/resources/resource";
import { globalBus } from "game/events"; import { globalBus } from "game/events";
import { persistent, PersistentRef } from "game/persistence"; import { Persistent, persistent } from "game/persistence";
import settings, { registerSettingField } from "game/settings"; import settings, { registerSettingField } from "game/settings";
import Decimal, { DecimalSource } from "util/bignum"; import Decimal, { DecimalSource } from "util/bignum";
import { import {
@ -58,10 +59,10 @@ export interface ChallengeOptions {
export interface BaseChallenge { export interface BaseChallenge {
id: string; id: string;
completions: PersistentRef<DecimalSource>; completions: Persistent<DecimalSource>;
completed: Ref<boolean>; completed: Ref<boolean>;
maxed: Ref<boolean>; maxed: Ref<boolean>;
active: PersistentRef<boolean>; active: Persistent<boolean>;
toggle: VoidFunction; toggle: VoidFunction;
complete: (remainInChallenge?: boolean) => void; complete: (remainInChallenge?: boolean) => void;
type: typeof ChallengeType; type: typeof ChallengeType;
@ -96,10 +97,12 @@ export type GenericChallenge = Replace<
>; >;
export function createChallenge<T extends ChallengeOptions>( export function createChallenge<T extends ChallengeOptions>(
optionsFunc: () => T & ThisType<Challenge<T>> optionsFunc: OptionsFunc<T, Challenge<T>, BaseChallenge>
): Challenge<T> { ): Challenge<T> {
const completions = persistent(0);
const active = persistent(false);
return createLazyProxy(() => { return createLazyProxy(() => {
const challenge: T & Partial<BaseChallenge> = optionsFunc(); const challenge = optionsFunc();
if ( if (
challenge.canComplete == null && challenge.canComplete == null &&
@ -116,8 +119,8 @@ export function createChallenge<T extends ChallengeOptions>(
challenge.type = ChallengeType; challenge.type = ChallengeType;
challenge[Component] = ChallengeComponent; challenge[Component] = ChallengeComponent;
challenge.completions = persistent(0); challenge.completions = completions;
challenge.active = persistent(false); challenge.active = active;
challenge.completed = computed(() => challenge.completed = computed(() =>
Decimal.gt((challenge as GenericChallenge).completions.value, 0) Decimal.gt((challenge as GenericChallenge).completions.value, 0)
); );

View file

@ -2,6 +2,7 @@ import ClickableComponent from "features/clickables/Clickable.vue";
import { import {
CoercableComponent, CoercableComponent,
Component, Component,
OptionsFunc,
GatherProps, GatherProps,
getUniqueID, getUniqueID,
Replace, Replace,
@ -69,10 +70,10 @@ export type GenericClickable = Replace<
>; >;
export function createClickable<T extends ClickableOptions>( export function createClickable<T extends ClickableOptions>(
optionsFunc: () => T & ThisType<Clickable<T>> optionsFunc: OptionsFunc<T, Clickable<T>, BaseClickable>
): Clickable<T> { ): Clickable<T> {
return createLazyProxy(() => { return createLazyProxy(() => {
const clickable: T & Partial<BaseClickable> = optionsFunc(); const clickable = optionsFunc();
clickable.id = getUniqueID("clickable-"); clickable.id = getUniqueID("clickable-");
clickable.type = ClickableType; clickable.type = ClickableType;
clickable[Component] = ClickableComponent; clickable[Component] = ClickableComponent;

View file

@ -4,19 +4,20 @@ import { isFunction } from "util/common";
import { import {
Computable, Computable,
convertComputable, convertComputable,
DoNotCache,
GetComputableTypeWithDefault, GetComputableTypeWithDefault,
processComputable, processComputable,
ProcessedComputable ProcessedComputable
} from "util/computed"; } from "util/computed";
import { createLazyProxy } from "util/proxies"; import { createLazyProxy } from "util/proxies";
import { computed, isRef, Ref, unref } from "vue"; import { computed, isRef, Ref, unref } from "vue";
import { Replace, setDefault } from "./feature"; import { OptionsFunc, Replace, setDefault } from "./feature";
import { Resource } from "./resources/resource"; import { Resource } from "./resources/resource";
export interface ConversionOptions { export interface ConversionOptions {
scaling: ScalingFunction; scaling: ScalingFunction;
currentGain?: Computable<DecimalSource>; currentGain?: Computable<DecimalSource>;
actualGain?: Computable<DecimalSource>;
currentAt?: Computable<DecimalSource>;
nextAt?: Computable<DecimalSource>; nextAt?: Computable<DecimalSource>;
baseResource: Resource; baseResource: Resource;
gainResource: Resource; gainResource: Resource;
@ -34,6 +35,8 @@ export type Conversion<T extends ConversionOptions> = Replace<
T & BaseConversion, T & BaseConversion,
{ {
currentGain: GetComputableTypeWithDefault<T["currentGain"], Ref<DecimalSource>>; currentGain: GetComputableTypeWithDefault<T["currentGain"], Ref<DecimalSource>>;
actualGain: GetComputableTypeWithDefault<T["actualGain"], Ref<DecimalSource>>;
currentAt: GetComputableTypeWithDefault<T["currentAt"], Ref<DecimalSource>>;
nextAt: GetComputableTypeWithDefault<T["nextAt"], Ref<DecimalSource>>; nextAt: GetComputableTypeWithDefault<T["nextAt"], Ref<DecimalSource>>;
buyMax: GetComputableTypeWithDefault<T["buyMax"], true>; buyMax: GetComputableTypeWithDefault<T["buyMax"], true>;
roundUpCost: GetComputableTypeWithDefault<T["roundUpCost"], true>; roundUpCost: GetComputableTypeWithDefault<T["roundUpCost"], true>;
@ -44,6 +47,8 @@ export type GenericConversion = Replace<
Conversion<ConversionOptions>, Conversion<ConversionOptions>,
{ {
currentGain: ProcessedComputable<DecimalSource>; currentGain: ProcessedComputable<DecimalSource>;
actualGain: ProcessedComputable<DecimalSource>;
currentAt: ProcessedComputable<DecimalSource>;
nextAt: ProcessedComputable<DecimalSource>; nextAt: ProcessedComputable<DecimalSource>;
buyMax: ProcessedComputable<boolean>; buyMax: ProcessedComputable<boolean>;
roundUpCost: ProcessedComputable<boolean>; roundUpCost: ProcessedComputable<boolean>;
@ -56,10 +61,10 @@ export interface GainModifier {
} }
export function createConversion<T extends ConversionOptions>( export function createConversion<T extends ConversionOptions>(
optionsFunc: () => T & ThisType<Conversion<T>> optionsFunc: OptionsFunc<T, Conversion<T>, BaseConversion>
): Conversion<T> { ): Conversion<T> {
return createLazyProxy(() => { return createLazyProxy(() => {
const conversion: T = optionsFunc(); const conversion = optionsFunc();
if (conversion.currentGain == null) { if (conversion.currentGain == null) {
conversion.currentGain = computed(() => { conversion.currentGain = computed(() => {
@ -70,12 +75,22 @@ export function createConversion<T extends ConversionOptions>(
: conversion.scaling.currentGain(conversion as GenericConversion); : conversion.scaling.currentGain(conversion as GenericConversion);
gain = Decimal.floor(gain).max(0); gain = Decimal.floor(gain).max(0);
if (!conversion.buyMax) { if (!unref(conversion.buyMax)) {
gain = gain.min(1); gain = gain.min(1);
} }
return gain; return gain;
}); });
} }
if (conversion.actualGain == null) {
conversion.actualGain = conversion.currentGain;
}
if (conversion.currentAt == null) {
conversion.currentAt = computed(() => {
let current = conversion.scaling.currentAt(conversion as GenericConversion);
if (conversion.roundUpCost) current = Decimal.ceil(current);
return current;
});
}
if (conversion.nextAt == null) { if (conversion.nextAt == null) {
conversion.nextAt = computed(() => { conversion.nextAt = computed(() => {
let next = conversion.scaling.nextAt(conversion as GenericConversion); let next = conversion.scaling.nextAt(conversion as GenericConversion);
@ -96,6 +111,8 @@ export function createConversion<T extends ConversionOptions>(
} }
processComputable(conversion as T, "currentGain"); processComputable(conversion as T, "currentGain");
processComputable(conversion as T, "actualGain");
processComputable(conversion as T, "currentAt");
processComputable(conversion as T, "nextAt"); processComputable(conversion as T, "nextAt");
processComputable(conversion as T, "buyMax"); processComputable(conversion as T, "buyMax");
setDefault(conversion, "buyMax", true); setDefault(conversion, "buyMax", true);
@ -108,6 +125,7 @@ export function createConversion<T extends ConversionOptions>(
export type ScalingFunction = { export type ScalingFunction = {
currentGain: (conversion: GenericConversion) => DecimalSource; currentGain: (conversion: GenericConversion) => DecimalSource;
currentAt: (conversion: GenericConversion) => DecimalSource;
nextAt: (conversion: GenericConversion) => DecimalSource; nextAt: (conversion: GenericConversion) => DecimalSource;
}; };
@ -128,11 +146,20 @@ export function createLinearScaling(
.times(unref(coefficient)) .times(unref(coefficient))
.add(1); .add(1);
}, },
currentAt(conversion) {
let current: DecimalSource = unref(conversion.currentGain);
if (conversion.gainModifier) {
current = conversion.gainModifier.revert(current);
}
current = Decimal.max(0, current);
return Decimal.times(current, unref(coefficient)).add(unref(base));
},
nextAt(conversion) { nextAt(conversion) {
let next: DecimalSource = Decimal.add(unref(conversion.currentGain), 1); let next: DecimalSource = Decimal.add(unref(conversion.currentGain), 1);
if (conversion.gainModifier) { if (conversion.gainModifier) {
next = conversion.gainModifier.revert(next); next = conversion.gainModifier.revert(next);
} }
next = Decimal.max(0, next);
return Decimal.times(next, unref(coefficient)).add(unref(base)).max(unref(base)); return Decimal.times(next, unref(coefficient)).add(unref(base)).max(unref(base));
} }
}; };
@ -155,24 +182,33 @@ export function createPolynomialScaling(
} }
return gain; return gain;
}, },
currentAt(conversion) {
let current: DecimalSource = unref(conversion.currentGain);
if (conversion.gainModifier) {
current = conversion.gainModifier.revert(current);
}
current = Decimal.max(0, current);
return Decimal.root(current, unref(exponent)).times(unref(base));
},
nextAt(conversion) { nextAt(conversion) {
let next: DecimalSource = Decimal.add(unref(conversion.currentGain), 1); let next: DecimalSource = Decimal.add(unref(conversion.currentGain), 1);
if (conversion.gainModifier) { if (conversion.gainModifier) {
next = conversion.gainModifier.revert(next); next = conversion.gainModifier.revert(next);
} }
next = Decimal.max(0, next);
return Decimal.root(next, unref(exponent)).times(unref(base)).max(unref(base)); return Decimal.root(next, unref(exponent)).times(unref(base)).max(unref(base));
} }
}; };
} }
export function createCumulativeConversion<S extends ConversionOptions>( export function createCumulativeConversion<S extends ConversionOptions>(
optionsFunc: () => S & ThisType<Conversion<S>> optionsFunc: OptionsFunc<S, Conversion<S>>
): Conversion<S> { ): Conversion<S> {
return createConversion(optionsFunc); return createConversion(optionsFunc);
} }
export function createIndependentConversion<S extends ConversionOptions>( export function createIndependentConversion<S extends ConversionOptions>(
optionsFunc: () => S & ThisType<Conversion<S>> optionsFunc: OptionsFunc<S, Conversion<S>>
): Conversion<S> { ): Conversion<S> {
return createConversion(() => { return createConversion(() => {
const conversion: S = optionsFunc(); const conversion: S = optionsFunc();
@ -180,14 +216,32 @@ export function createIndependentConversion<S extends ConversionOptions>(
setDefault(conversion, "buyMax", false); setDefault(conversion, "buyMax", false);
if (conversion.currentGain == null) { if (conversion.currentGain == null) {
conversion.currentGain = computed(() => conversion.currentGain = computed(() => {
Decimal.sub( let gain = conversion.gainModifier
? conversion.gainModifier.apply(
conversion.scaling.currentGain(conversion as GenericConversion)
)
: conversion.scaling.currentGain(conversion as GenericConversion);
gain = Decimal.floor(gain).max(conversion.gainResource.value);
if (!unref(conversion.buyMax)) {
gain = gain.min(Decimal.add(conversion.gainResource.value, 1));
}
return gain;
});
}
if (conversion.actualGain == null) {
conversion.actualGain = computed(() => {
let gain = Decimal.sub(
conversion.scaling.currentGain(conversion as GenericConversion), conversion.scaling.currentGain(conversion as GenericConversion),
conversion.gainResource.value conversion.gainResource.value
) ).max(0);
.add(1)
.max(1) if (!unref(conversion.buyMax)) {
); gain = gain.min(1);
}
return gain;
});
} }
setDefault(conversion, "convert", function () { setDefault(conversion, "convert", function () {
conversion.gainResource.value = conversion.gainModifier conversion.gainResource.value = conversion.gainModifier

View file

@ -24,6 +24,8 @@ export type FeatureComponent<T> = Omit<
export type Replace<T, S> = S & Omit<T, keyof S>; export type Replace<T, S> = S & Omit<T, keyof S>;
export type OptionsFunc<T, S = T, R = Record<string, unknown>> = () => T & ThisType<S> & Partial<R>;
let id = 0; let id = 0;
// Get a unique ID to allow a feature to be found for creating branches // Get a unique ID to allow a feature to be found for creating branches
// and any other uses requiring unique identifiers for each feature // and any other uses requiring unique identifiers for each feature

View file

@ -2,6 +2,7 @@ import GridComponent from "features/grids/Grid.vue";
import { import {
CoercableComponent, CoercableComponent,
Component, Component,
OptionsFunc,
GatherProps, GatherProps,
getUniqueID, getUniqueID,
Replace, Replace,
@ -19,7 +20,7 @@ import {
} from "util/computed"; } from "util/computed";
import { createLazyProxy } from "util/proxies"; import { createLazyProxy } from "util/proxies";
import { computed, Ref, unref } from "vue"; import { computed, Ref, unref } from "vue";
import { State, Persistent, makePersistent, PersistentState } from "game/persistence"; import { State, Persistent, PersistentState, persistent } from "game/persistence";
export const GridType = Symbol("Grid"); export const GridType = Symbol("Grid");
@ -241,11 +242,10 @@ export type GenericGrid = Replace<
>; >;
export function createGrid<T extends GridOptions>( export function createGrid<T extends GridOptions>(
optionsFunc: () => T & ThisType<Grid<T>> optionsFunc: OptionsFunc<T, Grid<T>, BaseGrid>
): Grid<T> { ): Grid<T> {
return createLazyProxy(() => { return createLazyProxy(persistent => {
const grid: T & Partial<BaseGrid> = optionsFunc(); const grid = Object.assign(persistent, optionsFunc());
makePersistent(grid, {});
grid.id = getUniqueID("grid-"); grid.id = getUniqueID("grid-");
grid[Component] = GridComponent; grid[Component] = GridComponent;
@ -301,5 +301,5 @@ export function createGrid<T extends GridOptions>(
}; };
return grid as unknown as Grid<T>; return grid as unknown as Grid<T>;
}); }, persistent({}));
} }

View file

@ -11,7 +11,7 @@ import {
} from "util/computed"; } from "util/computed";
import { createLazyProxy } from "util/proxies"; import { createLazyProxy } from "util/proxies";
import { shallowReactive, unref } from "vue"; import { shallowReactive, unref } from "vue";
import { findFeatures, jsx, Replace, setDefault } from "./feature"; import { OptionsFunc, findFeatures, jsx, Replace, setDefault } from "./feature";
export const hotkeys: Record<string, GenericHotkey | undefined> = shallowReactive({}); export const hotkeys: Record<string, GenericHotkey | undefined> = shallowReactive({});
export const HotkeyType = Symbol("Hotkey"); export const HotkeyType = Symbol("Hotkey");
@ -43,10 +43,10 @@ export type GenericHotkey = Replace<
>; >;
export function createHotkey<T extends HotkeyOptions>( export function createHotkey<T extends HotkeyOptions>(
optionsFunc: () => T & ThisType<Hotkey<T>> optionsFunc: OptionsFunc<T, Hotkey<T>, BaseHotkey>
): Hotkey<T> { ): Hotkey<T> {
return createLazyProxy(() => { return createLazyProxy(() => {
const hotkey: T & Partial<BaseHotkey> = optionsFunc(); const hotkey = optionsFunc();
hotkey.type = HotkeyType; hotkey.type = HotkeyType;
processComputable(hotkey as T, "enabled"); processComputable(hotkey as T, "enabled");

View file

@ -2,6 +2,7 @@ import InfoboxComponent from "features/infoboxes/Infobox.vue";
import { import {
CoercableComponent, CoercableComponent,
Component, Component,
OptionsFunc,
GatherProps, GatherProps,
getUniqueID, getUniqueID,
Replace, Replace,
@ -18,7 +19,7 @@ import {
} from "util/computed"; } from "util/computed";
import { createLazyProxy } from "util/proxies"; import { createLazyProxy } from "util/proxies";
import { Ref, unref } from "vue"; import { Ref, unref } from "vue";
import { Persistent, makePersistent, PersistentState } from "game/persistence"; import { Persistent, PersistentState, persistent } from "game/persistence";
export const InfoboxType = Symbol("Infobox"); export const InfoboxType = Symbol("Infobox");
@ -63,11 +64,10 @@ export type GenericInfobox = Replace<
>; >;
export function createInfobox<T extends InfoboxOptions>( export function createInfobox<T extends InfoboxOptions>(
optionsFunc: () => T & ThisType<Infobox<T>> optionsFunc: OptionsFunc<T, Infobox<T>, BaseInfobox>
): Infobox<T> { ): Infobox<T> {
return createLazyProxy(() => { return createLazyProxy(persistent => {
const infobox: T & Partial<BaseInfobox> = optionsFunc(); const infobox = Object.assign(persistent, optionsFunc());
makePersistent<boolean>(infobox, false);
infobox.id = getUniqueID("infobox-"); infobox.id = getUniqueID("infobox-");
infobox.type = InfoboxType; infobox.type = InfoboxType;
infobox[Component] = InfoboxComponent; infobox[Component] = InfoboxComponent;
@ -112,5 +112,5 @@ export function createInfobox<T extends InfoboxOptions>(
}; };
return infobox as unknown as Infobox<T>; return infobox as unknown as Infobox<T>;
}); }, persistent<boolean>(false));
} }

View file

@ -50,6 +50,7 @@ function updateNodes() {
}); });
} }
} }
document.fonts.ready.then(updateNodes);
const validLinks = computed(() => { const validLinks = computed(() => {
const n = nodes.value; const n = nodes.value;

View file

@ -1,5 +1,5 @@
import LinksComponent from "./Links.vue"; import LinksComponent from "./Links.vue";
import { Component, GatherProps, Replace } from "features/feature"; import { Component, OptionsFunc, GatherProps, Replace } from "features/feature";
import { Position } from "game/layers"; import { Position } from "game/layers";
import { import {
Computable, Computable,
@ -44,10 +44,10 @@ export type GenericLinks = Replace<
>; >;
export function createLinks<T extends LinksOptions>( export function createLinks<T extends LinksOptions>(
optionsFunc: (() => T) & ThisType<Links<T>> optionsFunc: OptionsFunc<T, Links<T>, BaseLinks>
): Links<T> { ): Links<T> {
return createLazyProxy(() => { return createLazyProxy(() => {
const links: T & Partial<BaseLinks> = optionsFunc(); const links = optionsFunc();
links.type = LinksType; links.type = LinksType;
links[Component] = LinksComponent; links[Component] = LinksComponent;

View file

@ -2,6 +2,7 @@ import Select from "components/fields/Select.vue";
import { import {
CoercableComponent, CoercableComponent,
Component, Component,
OptionsFunc,
GatherProps, GatherProps,
getUniqueID, getUniqueID,
jsx, jsx,
@ -13,7 +14,7 @@ import {
import MilestoneComponent from "features/milestones/Milestone.vue"; import MilestoneComponent from "features/milestones/Milestone.vue";
import { globalBus } from "game/events"; import { globalBus } from "game/events";
import "game/notifications"; import "game/notifications";
import { makePersistent, Persistent, PersistentState } from "game/persistence"; import { persistent, Persistent, PersistentState } from "game/persistence";
import settings, { registerSettingField } from "game/settings"; import settings, { registerSettingField } from "game/settings";
import { camelToTitle } from "util/common"; import { camelToTitle } from "util/common";
import { import {
@ -83,11 +84,10 @@ export type GenericMilestone = Replace<
>; >;
export function createMilestone<T extends MilestoneOptions>( export function createMilestone<T extends MilestoneOptions>(
optionsFunc: () => T & ThisType<Milestone<T>> optionsFunc: OptionsFunc<T, Milestone<T>, BaseMilestone>
): Milestone<T> { ): Milestone<T> {
return createLazyProxy(() => { return createLazyProxy(persistent => {
const milestone: T & Partial<BaseMilestone> = optionsFunc(); const milestone = Object.assign(persistent, optionsFunc());
makePersistent<boolean>(milestone, false);
milestone.id = getUniqueID("milestone-"); milestone.id = getUniqueID("milestone-");
milestone.type = MilestoneType; milestone.type = MilestoneType;
milestone[Component] = MilestoneComponent; milestone[Component] = MilestoneComponent;
@ -168,7 +168,7 @@ export function createMilestone<T extends MilestoneOptions>(
} }
return milestone as unknown as Milestone<T>; return milestone as unknown as Milestone<T>;
}); }, persistent<boolean>(false));
} }
declare module "game/settings" { declare module "game/settings" {

View file

@ -1,89 +1,74 @@
onMounted, onMounted,
<template> <template>
<Particles <div
:id="id" ref="resizeListener"
:class="{ class="resize-listener"
'not-fullscreen': !fullscreen :style="unref(style)"
}" :class="unref(classes)"
:style="{
zIndex
}"
ref="particles"
:particlesInit="particlesInit"
:particlesLoaded="particlesLoaded"
:options="{
fpsLimit: 60,
fullScreen: { enable: fullscreen, zIndex },
particles: {
number: {
value: 0
}
},
emitters: {
autoPlay: false
}
}"
v-bind="$attrs"
/> />
<div ref="resizeListener" class="resize-listener" />
</template> </template>
<script lang="tsx"> <script lang="tsx">
import { loadFull } from "tsparticles"; import { StyleValue } from "features/feature";
import { Engine, Container } from "tsparticles-engine";
import { Emitters } from "tsparticles-plugin-emitters/Emitters";
import { EmitterContainer } from "tsparticles-plugin-emitters/EmitterContainer";
import { defineComponent, inject, nextTick, onMounted, PropType, ref } from "vue";
import { ParticlesComponent } from "particles.vue3";
import { FeatureNode, NodesInjectionKey } from "game/layers"; import { FeatureNode, NodesInjectionKey } from "game/layers";
import { Application } from "pixi.js";
import { processedPropType } from "util/vue";
import {
defineComponent,
inject,
nextTick,
onBeforeUnmount,
onMounted,
PropType,
ref,
unref
} from "vue";
// TODO get typing support on the Particles component // TODO get typing support on the Particles component
export default defineComponent({ export default defineComponent({
props: { props: {
zIndex: { style: processedPropType<StyleValue>(String, Object, Array),
type: Number, classes: processedPropType<Record<string, boolean>>(Object),
required: true
},
fullscreen: {
type: Boolean,
required: true
},
onInit: { onInit: {
type: Function as PropType<(container: EmitterContainer & Container) => void>, type: Function as PropType<(app: Application) => void>,
required: true required: true
}, },
id: { id: {
type: String, type: String,
required: true required: true
}, },
onContainerResized: Function as PropType<(rect: DOMRect) => void> onContainerResized: Function as PropType<(rect: DOMRect) => void>,
onHotReload: Function as PropType<VoidFunction>
}, },
components: { Particles: ParticlesComponent },
setup(props) { setup(props) {
const particles = ref<null | { particles: { container: Emitters } }>(null); const app = ref<null | Application>(null);
async function particlesInit(engine: Engine) {
await loadFull(engine);
}
function particlesLoaded(container: EmitterContainer & Container) {
props.onInit(container);
}
const resizeObserver = new ResizeObserver(updateBounds); const resizeObserver = new ResizeObserver(updateBounds);
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const nodes = inject(NodesInjectionKey)!; const nodes = inject(NodesInjectionKey)!;
const resizeListener = ref<Element | null>(null); const resizeListener = ref<HTMLElement | null>(null);
onMounted(() => { onMounted(() => {
// ResizeListener exists because ResizeObserver's don't work when told to observe an SVG element // ResizeListener exists because ResizeObserver's don't work when told to observe an SVG element
const resListener = resizeListener.value; const resListener = resizeListener.value;
if (resListener != null) { if (resListener != null) {
resizeObserver.observe(resListener); resizeObserver.observe(resListener);
app.value = new Application({
resizeTo: resListener,
backgroundAlpha: 0
});
resizeListener.value?.appendChild(app.value.view);
props.onInit(app.value as Application);
} }
updateBounds(); updateBounds();
if (module.hot?.status() === "apply" && props.onHotReload) {
nextTick(props.onHotReload);
}
});
onBeforeUnmount(() => {
app.value?.destroy();
}); });
let isDirty = true; let isDirty = true;
@ -93,20 +78,20 @@ export default defineComponent({
nextTick(() => { nextTick(() => {
if (resizeListener.value != null && props.onContainerResized) { if (resizeListener.value != null && props.onContainerResized) {
// TODO don't overlap with Links.vue // TODO don't overlap with Links.vue
(Object.values(nodes.value) as FeatureNode[]).forEach( (Object.values(nodes.value).filter(n => n) as FeatureNode[]).forEach(
node => (node.rect = node.element.getBoundingClientRect()) node => (node.rect = node.element.getBoundingClientRect())
); );
props.onContainerResized(resizeListener.value.getBoundingClientRect()); props.onContainerResized(resizeListener.value.getBoundingClientRect());
app.value?.resize();
} }
isDirty = true; isDirty = true;
}); });
} }
} }
document.fonts.ready.then(updateBounds);
return { return {
particles, unref,
particlesInit,
particlesLoaded,
resizeListener resizeListener
}; };
} }

View file

@ -1,27 +1,31 @@
import ParticlesComponent from "features/particles/Particles.vue"; import ParticlesComponent from "features/particles/Particles.vue";
import { Container } from "tsparticles-engine"; import { Ref, shallowRef, unref } from "vue";
import { IEmitter } from "tsparticles-plugin-emitters/Options/Interfaces/IEmitter"; import {
import { EmitterInstance } from "tsparticles-plugin-emitters/EmitterInstance"; Component,
import { EmitterContainer } from "tsparticles-plugin-emitters/EmitterContainer"; OptionsFunc,
import { Ref, shallowRef } from "vue"; GatherProps,
import { Component, GatherProps, getUniqueID, Replace, setDefault } from "features/feature"; getUniqueID,
Replace,
StyleValue
} from "features/feature";
import { createLazyProxy } from "util/proxies"; import { createLazyProxy } from "util/proxies";
import { Application } from "pixi.js";
import { Emitter, EmitterConfigV3, upgradeConfig } from "@pixi/particle-emitter";
import { Computable, GetComputableType } from "util/computed";
export const ParticlesType = Symbol("Particles"); export const ParticlesType = Symbol("Particles");
export interface ParticlesOptions { export interface ParticlesOptions {
fullscreen?: boolean; classes?: Computable<Record<string, boolean>>;
zIndex?: number; style?: Computable<StyleValue>;
onContainerResized?: (boundingRect: DOMRect) => void; onContainerResized?: (boundingRect: DOMRect) => void;
onHotReload?: VoidFunction;
} }
export interface BaseParticles { export interface BaseParticles {
id: string; id: string;
containerRef: Ref<null | (EmitterContainer & Container)>; app: Ref<null | Application>;
addEmitter: ( addEmitter: (config: EmitterConfigV3) => Promise<Emitter>;
options: IEmitter & { particles: Required<IEmitter>["particles"] }
) => Promise<EmitterInstance>;
removeEmitter: (emitter: EmitterInstance) => void;
type: typeof ParticlesType; type: typeof ParticlesType;
[Component]: typeof ParticlesComponent; [Component]: typeof ParticlesComponent;
[GatherProps]: () => Record<string, unknown>; [GatherProps]: () => Record<string, unknown>;
@ -30,68 +34,54 @@ export interface BaseParticles {
export type Particles<T extends ParticlesOptions> = Replace< export type Particles<T extends ParticlesOptions> = Replace<
T & BaseParticles, T & BaseParticles,
{ {
fullscreen: undefined extends T["fullscreen"] ? true : T["fullscreen"]; classes: GetComputableType<T["classes"]>;
zIndex: undefined extends T["zIndex"] ? 1 : T["zIndex"]; style: GetComputableType<T["style"]>;
} }
>; >;
export type GenericParticles = Replace< export type GenericParticles = Particles<ParticlesOptions>;
Particles<ParticlesOptions>,
{
fullscreen: boolean;
zIndex: number;
}
>;
export function createParticles<T extends ParticlesOptions>( export function createParticles<T extends ParticlesOptions>(
optionsFunc: () => T & ThisType<Particles<T>> optionsFunc: OptionsFunc<T, Particles<T>, BaseParticles>
): Particles<T> { ): Particles<T> {
return createLazyProxy(() => { return createLazyProxy(() => {
const particles: T & Partial<BaseParticles> = optionsFunc(); const particles = optionsFunc();
particles.id = getUniqueID("particles-"); particles.id = getUniqueID("particles-");
particles.type = ParticlesType; particles.type = ParticlesType;
particles[Component] = ParticlesComponent; particles[Component] = ParticlesComponent;
particles.containerRef = shallowRef(null); particles.app = shallowRef(null);
particles.addEmitter = ( particles.addEmitter = (config: EmitterConfigV3): Promise<Emitter> => {
options: IEmitter & { particles: Required<IEmitter>["particles"] }
): Promise<EmitterInstance> => {
const genericParticles = particles as GenericParticles; const genericParticles = particles as GenericParticles;
if (genericParticles.containerRef.value) { if (genericParticles.app.value) {
// TODO why does addEmitter require a position parameter return Promise.resolve(new Emitter(genericParticles.app.value.stage, config));
return Promise.resolve(genericParticles.containerRef.value.addEmitter(options));
} }
return new Promise<EmitterInstance>(resolve => { return new Promise<Emitter>(resolve => {
emittersToAdd.push({ resolve, options }); emittersToAdd.push({ resolve, config });
}); });
}; };
particles.removeEmitter = (emitter: EmitterInstance) => {
// TODO I can't find a proper way to remove an emitter without accessing private functions
emitter.emitters.removeEmitter(emitter);
};
let emittersToAdd: { let emittersToAdd: {
resolve: (value: EmitterInstance | PromiseLike<EmitterInstance>) => void; resolve: (value: Emitter | PromiseLike<Emitter>) => void;
options: IEmitter & { particles: Required<IEmitter>["particles"] }; config: EmitterConfigV3;
}[] = []; }[] = [];
function onInit(container: EmitterContainer & Container) { function onInit(app: Application) {
(particles as GenericParticles).containerRef.value = container; (particles as GenericParticles).app.value = app;
emittersToAdd.forEach(({ resolve, options }) => resolve(container.addEmitter(options))); emittersToAdd.forEach(({ resolve, config }) => resolve(new Emitter(app.stage, config)));
emittersToAdd = []; emittersToAdd = [];
} }
setDefault(particles, "fullscreen", true);
setDefault(particles, "zIndex", 1);
particles.onContainerResized = particles.onContainerResized?.bind(particles); particles.onContainerResized = particles.onContainerResized?.bind(particles);
particles[GatherProps] = function (this: GenericParticles) { particles[GatherProps] = function (this: GenericParticles) {
const { id, fullscreen, zIndex, onContainerResized } = this; const { id, style, classes, onContainerResized, onHotReload } = this;
return { return {
id, id,
fullscreen, style: unref(style),
zIndex, classes,
onContainerResized, onContainerResized,
onHotReload,
onInit onInit
}; };
}; };
@ -99,3 +89,10 @@ export function createParticles<T extends ParticlesOptions>(
return particles as unknown as Particles<T>; return particles as unknown as Particles<T>;
}); });
} }
declare global {
interface Window {
upgradeConfig: typeof upgradeConfig;
}
}
window.upgradeConfig = upgradeConfig;

View file

@ -1,13 +1,7 @@
import { getUniqueID, Replace } from "features/feature"; import { OptionsFunc, getUniqueID, Replace } from "features/feature";
import { globalBus } from "game/events"; import { globalBus } from "game/events";
import { GenericLayer } from "game/layers"; import { GenericLayer } from "game/layers";
import { import { DefaultValue, Persistent, persistent, PersistentState } from "game/persistence";
DefaultValue,
Persistent,
persistent,
PersistentRef,
PersistentState
} from "game/persistence";
import Decimal from "util/bignum"; import Decimal from "util/bignum";
import { Computable, GetComputableType, processComputable } from "util/computed"; import { Computable, GetComputableType, processComputable } from "util/computed";
import { createLazyProxy } from "util/proxies"; import { createLazyProxy } from "util/proxies";
@ -37,10 +31,10 @@ export type Reset<T extends ResetOptions> = Replace<
export type GenericReset = Reset<ResetOptions>; export type GenericReset = Reset<ResetOptions>;
export function createReset<T extends ResetOptions>( export function createReset<T extends ResetOptions>(
optionsFunc: () => T & ThisType<Reset<T>> optionsFunc: OptionsFunc<T, Reset<T>, BaseReset>
): Reset<T> { ): Reset<T> {
return createLazyProxy(() => { return createLazyProxy(() => {
const reset: T & Partial<BaseReset> = optionsFunc(); const reset = optionsFunc();
reset.id = getUniqueID("reset-"); reset.id = getUniqueID("reset-");
reset.type = ResetType; reset.type = ResetType;
@ -70,7 +64,7 @@ export function createReset<T extends ResetOptions>(
} }
const listeners: Record<string, Unsubscribe | undefined> = {}; const listeners: Record<string, Unsubscribe | undefined> = {};
export function trackResetTime(layer: GenericLayer, reset: GenericReset): PersistentRef<Decimal> { export function trackResetTime(layer: GenericLayer, reset: GenericReset): Persistent<Decimal> {
const resetTime = persistent<Decimal>(new Decimal(0)); const resetTime = persistent<Decimal>(new Decimal(0));
listeners[layer.id] = layer.on("preUpdate", diff => { listeners[layer.id] = layer.on("preUpdate", diff => {
resetTime.value = Decimal.add(resetTime.value, diff); resetTime.value = Decimal.add(resetTime.value, diff);

View file

@ -1,6 +1,7 @@
import { import {
CoercableComponent, CoercableComponent,
Component, Component,
OptionsFunc,
GatherProps, GatherProps,
getUniqueID, getUniqueID,
Replace, Replace,
@ -36,9 +37,11 @@ export type Tab<T extends TabOptions> = Replace<
export type GenericTab = Tab<TabOptions>; export type GenericTab = Tab<TabOptions>;
export function createTab<T extends TabOptions>(optionsFunc: () => T & ThisType<Tab<T>>): Tab<T> { export function createTab<T extends TabOptions>(
optionsFunc: OptionsFunc<T, Tab<T>, BaseTab>
): Tab<T> {
return createLazyProxy(() => { return createLazyProxy(() => {
const tab: T & Partial<BaseTab> = optionsFunc(); const tab = optionsFunc();
tab.id = getUniqueID("tab-"); tab.id = getUniqueID("tab-");
tab.type = TabType; tab.type = TabType;
tab[Component] = TabComponent; tab[Component] = TabComponent;

View file

@ -1,6 +1,7 @@
import { import {
CoercableComponent, CoercableComponent,
Component, Component,
OptionsFunc,
GatherProps, GatherProps,
getUniqueID, getUniqueID,
Replace, Replace,
@ -10,7 +11,7 @@ import {
} from "features/feature"; } from "features/feature";
import TabButtonComponent from "features/tabs/TabButton.vue"; import TabButtonComponent from "features/tabs/TabButton.vue";
import TabFamilyComponent from "features/tabs/TabFamily.vue"; import TabFamilyComponent from "features/tabs/TabFamily.vue";
import { Persistent, makePersistent, PersistentState } from "game/persistence"; import { Persistent, PersistentState, persistent } from "game/persistence";
import { import {
Computable, Computable,
GetComputableType, GetComputableType,
@ -60,13 +61,13 @@ export type GenericTabButton = Replace<
export interface TabFamilyOptions { export interface TabFamilyOptions {
visibility?: Computable<Visibility>; visibility?: Computable<Visibility>;
tabs: Record<string, TabButtonOptions>;
classes?: Computable<Record<string, boolean>>; classes?: Computable<Record<string, boolean>>;
style?: Computable<StyleValue>; style?: Computable<StyleValue>;
} }
export interface BaseTabFamily extends Persistent<string> { export interface BaseTabFamily extends Persistent<string> {
id: string; id: string;
tabs: Record<string, TabButtonOptions>;
activeTab: Ref<GenericTab | CoercableComponent | null>; activeTab: Ref<GenericTab | CoercableComponent | null>;
selected: Ref<string>; selected: Ref<string>;
type: typeof TabFamilyType; type: typeof TabFamilyType;
@ -90,21 +91,39 @@ export type GenericTabFamily = Replace<
>; >;
export function createTabFamily<T extends TabFamilyOptions>( export function createTabFamily<T extends TabFamilyOptions>(
optionsFunc: () => T & ThisType<TabFamily<T>> tabs: Record<string, () => TabButtonOptions>,
optionsFunc: OptionsFunc<T, TabFamily<T>, BaseTabFamily>
): TabFamily<T> { ): TabFamily<T> {
return createLazyProxy(() => { if (Object.keys(tabs).length === 0) {
const tabFamily: T & Partial<BaseTabFamily> = optionsFunc(); console.warn("Cannot create tab family with 0 tabs");
throw "Cannot create tab family with 0 tabs";
}
if (Object.keys(tabFamily.tabs).length === 0) { return createLazyProxy(persistent => {
console.warn("Cannot create tab family with 0 tabs", tabFamily); const tabFamily = Object.assign(persistent, optionsFunc());
throw "Cannot create tab family with 0 tabs";
}
tabFamily.id = getUniqueID("tabFamily-"); tabFamily.id = getUniqueID("tabFamily-");
tabFamily.type = TabFamilyType; tabFamily.type = TabFamilyType;
tabFamily[Component] = TabFamilyComponent; tabFamily[Component] = TabFamilyComponent;
makePersistent<string>(tabFamily, Object.keys(tabFamily.tabs)[0]); tabFamily.tabs = Object.keys(tabs).reduce<Record<string, GenericTabButton>>(
(parsedTabs, tab) => {
const tabButton: TabButtonOptions & Partial<BaseTabButton> = tabs[tab]();
tabButton.type = TabButtonType;
tabButton[Component] = TabButtonComponent;
processComputable(tabButton as TabButtonOptions, "visibility");
setDefault(tabButton, "visibility", Visibility.Visible);
processComputable(tabButton as TabButtonOptions, "tab");
processComputable(tabButton as TabButtonOptions, "display");
processComputable(tabButton as TabButtonOptions, "classes");
processComputable(tabButton as TabButtonOptions, "style");
processComputable(tabButton as TabButtonOptions, "glowColor");
parsedTabs[tab] = tabButton as GenericTabButton;
return parsedTabs;
},
{}
);
tabFamily.selected = tabFamily[PersistentState]; tabFamily.selected = tabFamily[PersistentState];
tabFamily.activeTab = computed(() => { tabFamily.activeTab = computed(() => {
const tabs = unref(processedTabFamily.tabs); const tabs = unref(processedTabFamily.tabs);
@ -129,20 +148,6 @@ export function createTabFamily<T extends TabFamilyOptions>(
processComputable(tabFamily as T, "classes"); processComputable(tabFamily as T, "classes");
processComputable(tabFamily as T, "style"); processComputable(tabFamily as T, "style");
for (const tab in tabFamily.tabs) {
const tabButton: TabButtonOptions & Partial<BaseTabButton> = tabFamily.tabs[tab];
tabButton.type = TabButtonType;
tabButton[Component] = TabButtonComponent;
processComputable(tabButton as TabButtonOptions, "visibility");
setDefault(tabButton, "visibility", Visibility.Visible);
processComputable(tabButton as TabButtonOptions, "tab");
processComputable(tabButton as TabButtonOptions, "display");
processComputable(tabButton as TabButtonOptions, "classes");
processComputable(tabButton as TabButtonOptions, "style");
processComputable(tabButton as TabButtonOptions, "glowColor");
}
tabFamily[GatherProps] = function (this: GenericTabFamily) { tabFamily[GatherProps] = function (this: GenericTabFamily) {
const { visibility, activeTab, selected, tabs, style, classes } = this; const { visibility, activeTab, selected, tabs, style, classes } = this;
return { visibility, activeTab, selected, tabs, style: unref(style), classes }; return { visibility, activeTab, selected, tabs, style: unref(style), classes };
@ -151,5 +156,5 @@ export function createTabFamily<T extends TabFamilyOptions>(
// This is necessary because board.types is different from T and TabFamily // This is necessary because board.types is different from T and TabFamily
const processedTabFamily = tabFamily as unknown as TabFamily<T>; const processedTabFamily = tabFamily as unknown as TabFamily<T>;
return processedTabFamily; return processedTabFamily;
}); }, persistent(Object.keys(tabs)[0]));
} }

View file

@ -1,6 +1,7 @@
import { import {
CoercableComponent, CoercableComponent,
Component, Component,
OptionsFunc,
GatherProps, GatherProps,
getUniqueID, getUniqueID,
Replace, Replace,
@ -13,7 +14,7 @@ import { GenericReset } from "features/reset";
import { displayResource, Resource } from "features/resources/resource"; import { displayResource, Resource } from "features/resources/resource";
import { Tooltip } from "features/tooltip"; import { Tooltip } from "features/tooltip";
import TreeComponent from "features/trees/Tree.vue"; import TreeComponent from "features/trees/Tree.vue";
import { persistent } from "game/persistence"; import { deletePersistent, persistent } from "game/persistence";
import Decimal, { DecimalSource, format, formatWhole } from "util/bignum"; import Decimal, { DecimalSource, format, formatWhole } from "util/bignum";
import { import {
Computable, Computable,
@ -74,18 +75,20 @@ export type GenericTreeNode = Replace<
>; >;
export function createTreeNode<T extends TreeNodeOptions>( export function createTreeNode<T extends TreeNodeOptions>(
optionsFunc: () => T & ThisType<TreeNode<T>> optionsFunc: OptionsFunc<T, TreeNode<T>, BaseTreeNode>
): TreeNode<T> { ): TreeNode<T> {
const forceTooltip = persistent(false);
return createLazyProxy(() => { return createLazyProxy(() => {
const treeNode: T & Partial<BaseTreeNode> = optionsFunc(); const treeNode = optionsFunc();
treeNode.id = getUniqueID("treeNode-"); treeNode.id = getUniqueID("treeNode-");
treeNode.type = TreeNodeType; treeNode.type = TreeNodeType;
if (treeNode.tooltip) { if (treeNode.tooltip) {
treeNode.forceTooltip = persistent(false); treeNode.forceTooltip = forceTooltip;
} else { } else {
// If we don't have a tooltip, no point in making this persistent // If we don't have a tooltip, no point in making this persistent
treeNode.forceTooltip = ref(false); treeNode.forceTooltip = ref(false);
deletePersistent(forceTooltip);
} }
processComputable(treeNode as T, "visibility"); processComputable(treeNode as T, "visibility");
@ -166,10 +169,10 @@ export type GenericTree = Replace<
>; >;
export function createTree<T extends TreeOptions>( export function createTree<T extends TreeOptions>(
optionsFunc: () => T & ThisType<Tree<T>> optionsFunc: OptionsFunc<T, Tree<T>, BaseTree>
): Tree<T> { ): Tree<T> {
return createLazyProxy(() => { return createLazyProxy(() => {
const tree: T & Partial<BaseTree> = optionsFunc(); const tree = optionsFunc();
tree.id = getUniqueID("tree-"); tree.id = getUniqueID("tree-");
tree.type = TreeType; tree.type = TreeType;
tree[Component] = TreeComponent; tree[Component] = TreeComponent;

View file

@ -2,6 +2,7 @@ import UpgradeComponent from "features/upgrades/Upgrade.vue";
import { import {
CoercableComponent, CoercableComponent,
Component, Component,
OptionsFunc,
findFeatures, findFeatures,
GatherProps, GatherProps,
getUniqueID, getUniqueID,
@ -23,7 +24,7 @@ import {
} from "util/computed"; } from "util/computed";
import { createLazyProxy } from "util/proxies"; import { createLazyProxy } from "util/proxies";
import { computed, Ref, unref } from "vue"; import { computed, Ref, unref } from "vue";
import { Persistent, makePersistent, PersistentState } from "game/persistence"; import { persistent, Persistent, PersistentState } from "game/persistence";
export const UpgradeType = Symbol("Upgrade"); export const UpgradeType = Symbol("Upgrade");
@ -78,11 +79,10 @@ export type GenericUpgrade = Replace<
>; >;
export function createUpgrade<T extends UpgradeOptions>( export function createUpgrade<T extends UpgradeOptions>(
optionsFunc: () => T & ThisType<Upgrade<T>> optionsFunc: OptionsFunc<T, Upgrade<T>, BaseUpgrade>
): Upgrade<T> { ): Upgrade<T> {
return createLazyProxy(() => { return createLazyProxy(persistent => {
const upgrade: T & Partial<BaseUpgrade> = optionsFunc(); const upgrade = Object.assign(persistent, optionsFunc());
makePersistent<boolean>(upgrade, false);
upgrade.id = getUniqueID("upgrade-"); upgrade.id = getUniqueID("upgrade-");
upgrade.type = UpgradeType; upgrade.type = UpgradeType;
upgrade[Component] = UpgradeComponent; upgrade[Component] = UpgradeComponent;
@ -167,7 +167,7 @@ export function createUpgrade<T extends UpgradeOptions>(
}; };
return upgrade as unknown as Upgrade<T>; return upgrade as unknown as Upgrade<T>;
}); }, persistent<boolean>(false));
} }
export function setupAutoPurchase( export function setupAutoPurchase(

View file

@ -1,6 +1,7 @@
import Modal from "components/Modal.vue"; import Modal from "components/Modal.vue";
import { import {
CoercableComponent, CoercableComponent,
OptionsFunc,
jsx, jsx,
JSXFunction, JSXFunction,
Replace, Replace,
@ -18,7 +19,7 @@ import { createLazyProxy } from "util/proxies";
import { createNanoEvents, Emitter } from "nanoevents"; import { createNanoEvents, Emitter } from "nanoevents";
import { InjectionKey, Ref, ref, unref } from "vue"; import { InjectionKey, Ref, ref, unref } from "vue";
import { globalBus } from "./events"; import { globalBus } from "./events";
import { persistent, PersistentRef } from "./persistence"; import { Persistent, persistent } from "./persistence";
import player from "./player"; import player from "./player";
export interface FeatureNode { export interface FeatureNode {
@ -58,7 +59,6 @@ export interface Position {
} }
export interface LayerOptions { export interface LayerOptions {
id: string;
color?: Computable<string>; color?: Computable<string>;
display: Computable<CoercableComponent>; display: Computable<CoercableComponent>;
classes?: Computable<Record<string, boolean>>; classes?: Computable<Record<string, boolean>>;
@ -70,7 +70,8 @@ export interface LayerOptions {
} }
export interface BaseLayer { export interface BaseLayer {
minimized: PersistentRef<boolean>; id: string;
minimized: Persistent<boolean>;
emitter: Emitter<LayerEvents>; emitter: Emitter<LayerEvents>;
on: OmitThisParameter<Emitter<LayerEvents>["on"]>; on: OmitThisParameter<Emitter<LayerEvents>["on"]>;
emit: <K extends keyof LayerEvents>(event: K, ...args: Parameters<LayerEvents[K]>) => void; emit: <K extends keyof LayerEvents>(event: K, ...args: Parameters<LayerEvents[K]>) => void;
@ -84,7 +85,7 @@ export type Layer<T extends LayerOptions> = Replace<
display: GetComputableType<T["display"]>; display: GetComputableType<T["display"]>;
classes: GetComputableType<T["classes"]>; classes: GetComputableType<T["classes"]>;
style: GetComputableType<T["style"]>; style: GetComputableType<T["style"]>;
name: GetComputableTypeWithDefault<T["name"], T["id"]>; name: GetComputableTypeWithDefault<T["name"], string>;
minWidth: GetComputableTypeWithDefault<T["minWidth"], 600>; minWidth: GetComputableTypeWithDefault<T["minWidth"], 600>;
minimizable: GetComputableTypeWithDefault<T["minimizable"], true>; minimizable: GetComputableTypeWithDefault<T["minimizable"], true>;
forceHideGoBack: GetComputableType<T["forceHideGoBack"]>; forceHideGoBack: GetComputableType<T["forceHideGoBack"]>;
@ -100,8 +101,11 @@ export type GenericLayer = Replace<
} }
>; >;
export const persistentRefs: Record<string, Set<Persistent>> = {};
export const addingLayers: string[] = [];
export function createLayer<T extends LayerOptions>( export function createLayer<T extends LayerOptions>(
optionsFunc: (() => T) & ThisType<BaseLayer> id: string,
optionsFunc: OptionsFunc<T, BaseLayer, BaseLayer>
): Layer<T> { ): Layer<T> {
return createLazyProxy(() => { return createLazyProxy(() => {
const layer = {} as T & Partial<BaseLayer>; const layer = {} as T & Partial<BaseLayer>;
@ -109,10 +113,19 @@ export function createLayer<T extends LayerOptions>(
layer.on = emitter.on.bind(emitter); layer.on = emitter.on.bind(emitter);
layer.emit = emitter.emit.bind(emitter); layer.emit = emitter.emit.bind(emitter);
layer.nodes = ref({}); layer.nodes = ref({});
layer.id = id;
addingLayers.push(id);
persistentRefs[id] = new Set();
layer.minimized = persistent(false); layer.minimized = persistent(false);
Object.assign(layer, optionsFunc.call(layer)); Object.assign(layer, optionsFunc.call(layer));
if (
addingLayers[addingLayers.length - 1] == null ||
addingLayers[addingLayers.length - 1] !== id
) {
throw `Adding layers stack in invalid state. This should not happen\nStack: ${addingLayers}\nTrying to pop ${layer.id}`;
}
addingLayers.pop();
processComputable(layer as T, "color"); processComputable(layer as T, "color");
processComputable(layer as T, "display"); processComputable(layer as T, "display");

View file

@ -2,11 +2,13 @@ import { globalBus } from "game/events";
import Decimal, { DecimalSource } from "util/bignum"; import Decimal, { DecimalSource } from "util/bignum";
import { ProxyState } from "util/proxies"; import { ProxyState } from "util/proxies";
import { isArray } from "@vue/shared"; import { isArray } from "@vue/shared";
import { isRef, Ref, ref } from "vue"; import { isReactive, isRef, Ref, ref } from "vue";
import { GenericLayer } from "./layers"; import { addingLayers, GenericLayer, persistentRefs } from "./layers";
export const PersistentState = Symbol("PersistentState"); export const PersistentState = Symbol("PersistentState");
export const DefaultValue = Symbol("DefaultValue"); export const DefaultValue = Symbol("DefaultValue");
export const StackTrace = Symbol("StackTrace");
export const Deleted = Symbol("Deleted");
// Note: This is a union of things that should be safely stringifiable without needing // Note: This is a union of things that should be safely stringifiable without needing
// special processes for knowing what to load them in as // special processes for knowing what to load them in as
@ -20,31 +22,53 @@ export type State =
| { [key: string]: State } | { [key: string]: State }
| { [key: number]: State }; | { [key: number]: State };
export type Persistent<T extends State = State> = { export type Persistent<T extends State = State> = Ref<T> & {
[PersistentState]: Ref<T>; [PersistentState]: Ref<T>;
[DefaultValue]: T; [DefaultValue]: T;
[StackTrace]: string;
[Deleted]: boolean;
}; };
export type PersistentRef<T extends State = State> = Ref<T> & Persistent<T>;
export function persistent<T extends State>(defaultValue: T | Ref<T>): PersistentRef<T> { function getStackTrace() {
return (
new Error().stack
?.split("\n")
.slice(3, 5)
.map(line => line.trim())
.join("\n") || ""
);
}
export function persistent<T extends State>(defaultValue: T | Ref<T>): Persistent<T> {
const persistent = ( const persistent = (
isRef(defaultValue) ? defaultValue : (ref<T>(defaultValue) as unknown) isRef(defaultValue) ? defaultValue : (ref<T>(defaultValue) as unknown)
) as PersistentRef<T>; ) as Persistent<T>;
persistent[PersistentState] = persistent; persistent[PersistentState] = persistent;
persistent[DefaultValue] = isRef(defaultValue) ? defaultValue.value : defaultValue; persistent[DefaultValue] = isRef(defaultValue) ? defaultValue.value : defaultValue;
return persistent as PersistentRef<T>; persistent[StackTrace] = getStackTrace();
persistent[Deleted] = false;
if (addingLayers.length === 0) {
console.warn(
"Creating a persistent ref outside of a layer. This is not officially supported",
persistent,
"\nCreated at:\n" + persistent[StackTrace]
);
} else {
persistentRefs[addingLayers[addingLayers.length - 1]].add(persistent);
}
return persistent as Persistent<T>;
} }
export function makePersistent<T extends State>( export function deletePersistent(persistent: Persistent) {
obj: unknown, if (addingLayers.length === 0) {
defaultValue: T console.warn("Deleting a persistent ref outside of a layer. Ignoring...", persistent);
): asserts obj is Persistent<T> { } else {
const persistent = obj as Partial<Persistent<T>>; persistentRefs[addingLayers[addingLayers.length - 1]].delete(persistent);
const state = ref(defaultValue) as Ref<T>; }
persistent[Deleted] = true;
persistent[PersistentState] = state;
persistent[DefaultValue] = isRef(defaultValue) ? (defaultValue.value as T) : defaultValue;
} }
globalBus.on("addLayer", (layer: GenericLayer, saveData: Record<string, unknown>) => { globalBus.on("addLayer", (layer: GenericLayer, saveData: Record<string, unknown>) => {
@ -56,6 +80,20 @@ globalBus.on("addLayer", (layer: GenericLayer, saveData: Record<string, unknown>
if (value && typeof value === "object") { if (value && typeof value === "object") {
if (PersistentState in value) { if (PersistentState in value) {
foundPersistent = true; foundPersistent = true;
if ((value as Persistent)[Deleted]) {
console.warn(
"Deleted persistent ref present in returned object. Ignoring...",
value,
"\nCreated at:\n" + (value as Persistent)[StackTrace]
);
return;
}
persistentRefs[layer.id].delete(
ProxyState in value
? // eslint-disable-next-line @typescript-eslint/no-explicit-any
((value as any)[ProxyState] as Persistent)
: (value as Persistent)
);
// Construct save path if it doesn't exist // Construct save path if it doesn't exist
const persistentState = path.reduce<Record<string, unknown>>((acc, curr) => { const persistentState = path.reduce<Record<string, unknown>>((acc, curr) => {
@ -70,12 +108,20 @@ globalBus.on("addLayer", (layer: GenericLayer, saveData: Record<string, unknown>
// Add ref to save data // Add ref to save data
persistentState[key] = (value as Persistent)[PersistentState]; persistentState[key] = (value as Persistent)[PersistentState];
// Load previously saved value // Load previously saved value
if (savedValue != null) { if (isReactive(persistentState)) {
(persistentState[key] as Ref<unknown>).value = savedValue; if (savedValue != null) {
persistentState[key] = savedValue;
} else {
persistentState[key] = (value as Persistent)[DefaultValue];
}
} else { } else {
(persistentState[key] as Ref<unknown>).value = (value as Persistent)[ if (savedValue != null) {
DefaultValue (persistentState[key] as Ref<unknown>).value = savedValue;
]; } else {
(persistentState[key] as Ref<unknown>).value = (value as Persistent)[
DefaultValue
];
}
} }
} else if ( } else if (
!(value instanceof Decimal) && !(value instanceof Decimal) &&
@ -114,4 +160,12 @@ globalBus.on("addLayer", (layer: GenericLayer, saveData: Record<string, unknown>
return foundPersistent; return foundPersistent;
}; };
handleObject(layer); handleObject(layer);
persistentRefs[layer.id].forEach(persistent => {
console.error(
`Created persistent ref in ${layer.id} without registering it to the layer! Make sure to include everything persistent in the returned object`,
persistent,
"\nCreated at:\n" + persistent[StackTrace]
);
});
persistentRefs[layer.id].clear();
}); });

View file

@ -17,15 +17,18 @@ export type ProxiedWithState<T> = NonNullable<T> extends Record<PropertyKey, any
// Takes a function that returns an object and pretends to be that object // Takes a function that returns an object and pretends to be that object
// Note that the object is lazily calculated // Note that the object is lazily calculated
export function createLazyProxy<T extends object>(objectFunc: () => T): T { export function createLazyProxy<T extends object, S>(
const obj: T | Record<string, never> = {}; objectFunc: (baseObject: S) => T & S,
baseObject: S = {} as S
): T {
const obj: S & Partial<T> = baseObject;
let calculated = false; let calculated = false;
function calculateObj(): T { function calculateObj(): T {
if (!calculated) { if (!calculated) {
Object.assign(obj, objectFunc()); Object.assign(obj, objectFunc(obj));
calculated = true; calculated = true;
} }
return obj as T; return obj as S & T;
} }
return new Proxy(obj, { return new Proxy(obj, {
@ -36,8 +39,8 @@ export function createLazyProxy<T extends object>(objectFunc: () => T): T {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
return (calculateObj() as any)[key]; return (calculateObj() as any)[key];
}, },
set() { set(target, key, value) {
console.error("Layers and features are shallow readonly"); console.error("Layers and features are shallow readonly", key, value);
return false; return false;
}, },
has(target, key) { has(target, key) {
@ -51,10 +54,10 @@ export function createLazyProxy<T extends object>(objectFunc: () => T): T {
}, },
getOwnPropertyDescriptor(target, key) { getOwnPropertyDescriptor(target, key) {
if (!calculated) { if (!calculated) {
Object.assign(obj, objectFunc()); Object.assign(obj, objectFunc(obj));
calculated = true; calculated = true;
} }
return Object.getOwnPropertyDescriptor(target, key); return Object.getOwnPropertyDescriptor(target, key);
} }
}) as T; }) as S & T;
} }