Merge remote-tracking branch 'template/main'
This commit is contained in:
commit
df26b9b756
33 changed files with 1372 additions and 1142 deletions
36
CHANGELOG.md
36
CHANGELOG.md
|
@ -6,6 +6,42 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
## [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
|
||||
### Added
|
||||
- Particles feature
|
||||
|
|
1550
package-lock.json
generated
1550
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "profectus",
|
||||
"version": "0.2.0",
|
||||
"version": "0.3.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "vue-cli-service serve",
|
||||
|
@ -9,11 +9,11 @@
|
|||
"lint": "vue-cli-service lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@pixi/particle-emitter": "^5.0.4",
|
||||
"core-js": "^3.6.5",
|
||||
"lodash.clonedeep": "^4.5.0",
|
||||
"nanoevents": "^6.0.2",
|
||||
"particles.vue3": "^2.0.3",
|
||||
"tsparticles": "^2.0.3",
|
||||
"pixi.js": "^6.3.0",
|
||||
"vue": "^3.2.26",
|
||||
"vue-next-select": "^2.10.2",
|
||||
"vue-panzoom": "^1.1.6",
|
||||
|
|
|
@ -1 +0,0 @@
|
|||
eyJpZCI6InRtdC14LTEwNSIsIm5hbWUiOiJEZWZhdWx0IFNhZmZmZiAtIHNvbWV0aGluZyBlbHNlIiwidGFicyI6WyJtYWluIiwiYyJdLCJ0aW1lIjoxNjI0MjQ1MjYxMDg3LCJhdXRvc2F2ZSI6dHJ1ZSwib2ZmbGluZVByb2QiOnRydWUsInRpbWVQbGF5ZWQiOiIzNDQ4LjYxNTc4MTcwOTAxIiwia2VlcEdvaW5nIjpmYWxzZSwibGFzdFRlblRpY2tzIjpbMC4wNTEsMC4wNSwwLjA0OSwwLjA1LDAuMDUsMC4wNTEsMC4wNDksMC4wNSwwLjA1LDAuMDUxXSwic2hvd1RQUyI6dHJ1ZSwibXNEaXNwbGF5IjoiYWxsIiwiaGlkZUNoYWxsZW5nZXMiOmZhbHNlLCJ0aGVtZSI6InBhcGVyIiwic3VidGFicyI6e30sIm1pbmltaXplZCI6e30sIm1vZElEIjoidG10LXgiLCJtb2RWZXJzaW9uIjoiMC4wIiwicG9pbnRzIjoiMzMwMC4zNzc3NzM4NTkwNTUiLCJtYWluIjp7InVwZ3JhZGVzIjpbXSwiYWNoaWV2ZW1lbnRzIjpbXSwibWlsZXN0b25lcyI6W10sImluZm9ib3hlcyI6e319LCJmIjp7InVwZ3JhZGVzIjpbXSwiYWNoaWV2ZW1lbnRzIjpbXSwibWlsZXN0b25lcyI6W10sImluZm9ib3hlcyI6e30sImNsaWNrYWJsZXMiOnsiMTEiOiJTdGFydCJ9LCJ1bmxvY2tlZCI6ZmFsc2UsInBvaW50cyI6IjAiLCJib29wIjpmYWxzZX0sImMiOnsidXBncmFkZXMiOlsiMTEiXSwiYWNoaWV2ZW1lbnRzIjpbXSwibWlsZXN0b25lcyI6W10sImluZm9ib3hlcyI6e30sImJ1eWFibGVzIjp7IjExIjoiMCJ9LCJjaGFsbGVuZ2VzIjp7IjExIjoiMCJ9LCJ1bmxvY2tlZCI6dHJ1ZSwicG9pbnRzIjoiMCIsImJlc3QiOiIxIiwidG90YWwiOiIwIiwiYmVlcCI6ZmFsc2UsInRoaW5neSI6InBvaW50eSIsIm90aGVyVGhpbmd5IjoxMCwic3BlbnRPbkJ1eWFibGVzIjoiMCJ9LCJhIjp7InVwZ3JhZGVzIjpbXSwiYWNoaWV2ZW1lbnRzIjpbIjExIl0sIm1pbGVzdG9uZXMiOltdLCJpbmZvYm94ZXMiOnt9LCJ1bmxvY2tlZCI6dHJ1ZSwicG9pbnRzIjoiMCJ9LCJnIjp7InVwZ3JhZGVzIjpbXSwiYWNoaWV2ZW1lbnRzIjpbXSwibWlsZXN0b25lcyI6W10sImluZm9ib3hlcyI6e319LCJoIjp7InVwZ3JhZGVzIjpbXSwiYWNoaWV2ZW1lbnRzIjpbXSwibWlsZXN0b25lcyI6W10sImluZm9ib3hlcyI6e319LCJzcG9vayI6eyJ1cGdyYWRlcyI6W10sImFjaGlldmVtZW50cyI6W10sIm1pbGVzdG9uZXMiOltdLCJpbmZvYm94ZXMiOnt9fSwib29tcHNNYWciOjAsImxhc3RQb2ludHMiOiIzMzAwLjM3Nzc3Mzg1OTA1NSJ9
|
|
@ -24,7 +24,7 @@
|
|||
import projInfo from "data/projInfo.json";
|
||||
import { CoercableComponent, StyleValue } from "features/feature";
|
||||
import { FeatureNode } from "game/layers";
|
||||
import { PersistentRef } from "game/persistence";
|
||||
import { Persistent } from "game/persistence";
|
||||
import player from "game/player";
|
||||
import { computeComponent, processedPropType, wrapRef } from "util/vue";
|
||||
import { computed, defineComponent, nextTick, PropType, Ref, ref, toRefs, unref, watch } from "vue";
|
||||
|
@ -46,7 +46,7 @@ export default defineComponent({
|
|||
required: true
|
||||
},
|
||||
minimized: {
|
||||
type: Object as PropType<PersistentRef<boolean>>,
|
||||
type: Object as PropType<Persistent<boolean>>,
|
||||
required: true
|
||||
},
|
||||
minWidth: {
|
||||
|
|
|
@ -29,6 +29,7 @@ function updateTop() {
|
|||
}
|
||||
|
||||
nextTick(updateTop);
|
||||
document.fonts.ready.then(updateTop);
|
||||
|
||||
onMounted(() => {
|
||||
const el = element.value?.parentElement;
|
||||
|
|
|
@ -5,7 +5,7 @@ import {
|
|||
GenericClickable
|
||||
} from "features/clickables/clickable";
|
||||
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 {
|
||||
createTreeNode,
|
||||
|
@ -15,7 +15,7 @@ import {
|
|||
TreeNodeOptions
|
||||
} from "features/trees/tree";
|
||||
import player from "game/player";
|
||||
import Decimal from "util/bignum";
|
||||
import Decimal, { DecimalSource } from "util/bignum";
|
||||
import {
|
||||
Computable,
|
||||
GetComputableType,
|
||||
|
@ -33,6 +33,7 @@ export interface ResetButtonOptions extends ClickableOptions {
|
|||
showNextAt?: Computable<boolean>;
|
||||
display?: Computable<CoercableComponent>;
|
||||
canClick?: Computable<boolean>;
|
||||
minimumGain?: Computable<DecimalSource>;
|
||||
}
|
||||
|
||||
export type ResetButton<T extends ResetButtonOptions> = Replace<
|
||||
|
@ -42,6 +43,7 @@ export type ResetButton<T extends ResetButtonOptions> = Replace<
|
|||
showNextAt: GetComputableTypeWithDefault<T["showNextAt"], true>;
|
||||
display: GetComputableTypeWithDefault<T["display"], Ref<JSX.Element>>;
|
||||
canClick: GetComputableTypeWithDefault<T["canClick"], Ref<boolean>>;
|
||||
minimumGain: GetComputableTypeWithDefault<T["minimumGain"], 1>;
|
||||
onClick: VoidFunction;
|
||||
}
|
||||
>;
|
||||
|
@ -53,17 +55,19 @@ export type GenericResetButton = Replace<
|
|||
showNextAt: ProcessedComputable<boolean>;
|
||||
display: ProcessedComputable<CoercableComponent>;
|
||||
canClick: ProcessedComputable<boolean>;
|
||||
minimumGain: ProcessedComputable<DecimalSource>;
|
||||
}
|
||||
>;
|
||||
|
||||
export function createResetButton<T extends ClickableOptions & ResetButtonOptions>(
|
||||
optionsFunc: () => T
|
||||
optionsFunc: OptionsFunc<T>
|
||||
): ResetButton<T> {
|
||||
return createClickable(() => {
|
||||
const resetButton = optionsFunc();
|
||||
|
||||
processComputable(resetButton as T, "showNextAt");
|
||||
setDefault(resetButton, "showNextAt", true);
|
||||
setDefault(resetButton, "minimumGain", 1);
|
||||
|
||||
if (resetButton.resetDescription == null) {
|
||||
resetButton.resetDescription = computed(() =>
|
||||
|
@ -80,16 +84,22 @@ export function createResetButton<T extends ClickableOptions & ResetButtonOption
|
|||
<b>
|
||||
{displayResource(
|
||||
resetButton.conversion.gainResource,
|
||||
unref(resetButton.conversion.currentGain)
|
||||
Decimal.max(
|
||||
unref(resetButton.conversion.actualGain),
|
||||
unref(resetButton.minimumGain as ProcessedComputable<DecimalSource>)
|
||||
)
|
||||
)}
|
||||
</b>{" "}
|
||||
{resetButton.conversion.gainResource.displayName}
|
||||
<div v-show={unref(resetButton.showNextAt)}>
|
||||
<br />
|
||||
Next:{" "}
|
||||
{resetButton.conversion.buyMax ? "Next:" : "Req:"}{" "}
|
||||
{displayResource(
|
||||
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}
|
||||
</div>
|
||||
|
@ -99,7 +109,10 @@ export function createResetButton<T extends ClickableOptions & ResetButtonOption
|
|||
|
||||
if (resetButton.canClick == null) {
|
||||
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>(
|
||||
optionsFunc: () => T
|
||||
optionsFunc: OptionsFunc<T>
|
||||
): LayerTreeNode<T> {
|
||||
return createTreeNode(() => {
|
||||
const options = optionsFunc();
|
||||
|
|
|
@ -13,8 +13,8 @@ import { DecimalSource } from "util/bignum";
|
|||
import { render } from "util/vue";
|
||||
import { createLayerTreeNode, createResetButton } from "../common";
|
||||
|
||||
const layer = createLayer(() => {
|
||||
const id = "p";
|
||||
const id = "p";
|
||||
const layer = createLayer(id, () => {
|
||||
const name = "Prestige";
|
||||
const color = "#4BDC13";
|
||||
const points = createResource<DecimalSource>(0, "prestige points");
|
||||
|
@ -43,7 +43,6 @@ const layer = createLayer(() => {
|
|||
}));
|
||||
|
||||
return {
|
||||
id,
|
||||
name,
|
||||
color,
|
||||
points,
|
||||
|
|
|
@ -16,7 +16,7 @@ import f from "./layers/aca/f";
|
|||
/**
|
||||
* @hidden
|
||||
*/
|
||||
export const main = createLayer(() => {
|
||||
export const main = createLayer("main", () => {
|
||||
const points = createResource<DecimalSource>(10);
|
||||
const best = trackBest(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.
|
||||
// Officially all you need are anything with persistency or that you want to access elsewhere
|
||||
return {
|
||||
id: "main",
|
||||
name: "Tree",
|
||||
display: jsx(() => (
|
||||
<>
|
||||
|
|
|
@ -2,6 +2,7 @@ import AchievementComponent from "features/achievements/Achievement.vue";
|
|||
import {
|
||||
CoercableComponent,
|
||||
Component,
|
||||
OptionsFunc,
|
||||
GatherProps,
|
||||
getUniqueID,
|
||||
Replace,
|
||||
|
@ -10,7 +11,7 @@ import {
|
|||
Visibility
|
||||
} from "features/feature";
|
||||
import "game/notifications";
|
||||
import { Persistent, makePersistent, PersistentState } from "game/persistence";
|
||||
import { Persistent, PersistentState, persistent } from "game/persistence";
|
||||
import {
|
||||
Computable,
|
||||
GetComputableType,
|
||||
|
@ -67,11 +68,10 @@ export type GenericAchievement = Replace<
|
|||
>;
|
||||
|
||||
export function createAchievement<T extends AchievementOptions>(
|
||||
optionsFunc: () => T & ThisType<Achievement<T>>
|
||||
optionsFunc: OptionsFunc<T, Achievement<T>, BaseAchievement>
|
||||
): Achievement<T> {
|
||||
return createLazyProxy(() => {
|
||||
const achievement: T & Partial<BaseAchievement> = optionsFunc();
|
||||
makePersistent<boolean>(achievement, false);
|
||||
return createLazyProxy(persistent => {
|
||||
const achievement = Object.assign(persistent, optionsFunc());
|
||||
achievement.id = getUniqueID("achievement-");
|
||||
achievement.type = AchievementType;
|
||||
achievement[Component] = AchievementComponent;
|
||||
|
@ -122,5 +122,5 @@ export function createAchievement<T extends AchievementOptions>(
|
|||
}
|
||||
|
||||
return achievement as unknown as Achievement<T>;
|
||||
});
|
||||
}, persistent<boolean>(false));
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ import BarComponent from "features/bars/Bar.vue";
|
|||
import {
|
||||
CoercableComponent,
|
||||
Component,
|
||||
OptionsFunc,
|
||||
GatherProps,
|
||||
getUniqueID,
|
||||
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(() => {
|
||||
const bar: T & Partial<BaseBar> = optionsFunc();
|
||||
const bar = optionsFunc();
|
||||
bar.id = getUniqueID("bar-");
|
||||
bar.type = BarType;
|
||||
bar[Component] = BarComponent;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import BoardComponent from "features/boards/Board.vue";
|
||||
import {
|
||||
Component,
|
||||
OptionsFunc,
|
||||
findFeatures,
|
||||
GatherProps,
|
||||
getUniqueID,
|
||||
|
@ -10,7 +11,7 @@ import {
|
|||
Visibility
|
||||
} from "features/feature";
|
||||
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 {
|
||||
Computable,
|
||||
|
@ -197,137 +198,142 @@ export type GenericBoard = Replace<
|
|||
>;
|
||||
|
||||
export function createBoard<T extends BoardOptions>(
|
||||
optionsFunc: () => T & ThisType<Board<T>>
|
||||
optionsFunc: OptionsFunc<T, Board<T>, BaseBoard>
|
||||
): Board<T> {
|
||||
return createLazyProxy(() => {
|
||||
const board: T & Partial<BaseBoard> = optionsFunc();
|
||||
makePersistent<BoardData>(board, {
|
||||
return createLazyProxy(
|
||||
persistent => {
|
||||
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: [],
|
||||
selectedNode: 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 {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import ClickableComponent from "features/clickables/Clickable.vue";
|
||||
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 {
|
||||
Computable,
|
||||
|
@ -15,6 +15,7 @@ import { computed, Ref, unref } from "vue";
|
|||
import {
|
||||
CoercableComponent,
|
||||
Component,
|
||||
OptionsFunc,
|
||||
GatherProps,
|
||||
getUniqueID,
|
||||
jsx,
|
||||
|
@ -87,10 +88,10 @@ export type GenericBuyable = Replace<
|
|||
>;
|
||||
|
||||
export function createBuyable<T extends BuyableOptions>(
|
||||
optionsFunc: () => T & ThisType<Buyable<T>>
|
||||
optionsFunc: OptionsFunc<T, Buyable<T>, BaseBuyable>
|
||||
): Buyable<T> {
|
||||
return createLazyProxy(() => {
|
||||
const buyable: T & Partial<BaseBuyable> = optionsFunc();
|
||||
return createLazyProxy(persistent => {
|
||||
const buyable = Object.assign(persistent, optionsFunc());
|
||||
|
||||
if (buyable.canPurchase == null && (buyable.resource == null || buyable.cost == null)) {
|
||||
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";
|
||||
}
|
||||
|
||||
makePersistent<DecimalSource>(buyable, 0);
|
||||
buyable.id = getUniqueID("buyable-");
|
||||
buyable.type = BuyableType;
|
||||
buyable[Component] = ClickableComponent;
|
||||
|
@ -239,5 +239,5 @@ export function createBuyable<T extends BuyableOptions>(
|
|||
};
|
||||
|
||||
return buyable as unknown as Buyable<T>;
|
||||
});
|
||||
}, persistent<DecimalSource>(0));
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import ChallengeComponent from "features/challenges/Challenge.vue";
|
|||
import {
|
||||
CoercableComponent,
|
||||
Component,
|
||||
OptionsFunc,
|
||||
GatherProps,
|
||||
getUniqueID,
|
||||
jsx,
|
||||
|
@ -15,7 +16,7 @@ import {
|
|||
import { GenericReset } from "features/reset";
|
||||
import { Resource } from "features/resources/resource";
|
||||
import { globalBus } from "game/events";
|
||||
import { persistent, PersistentRef } from "game/persistence";
|
||||
import { Persistent, persistent } from "game/persistence";
|
||||
import settings, { registerSettingField } from "game/settings";
|
||||
import Decimal, { DecimalSource } from "util/bignum";
|
||||
import {
|
||||
|
@ -58,10 +59,10 @@ export interface ChallengeOptions {
|
|||
|
||||
export interface BaseChallenge {
|
||||
id: string;
|
||||
completions: PersistentRef<DecimalSource>;
|
||||
completions: Persistent<DecimalSource>;
|
||||
completed: Ref<boolean>;
|
||||
maxed: Ref<boolean>;
|
||||
active: PersistentRef<boolean>;
|
||||
active: Persistent<boolean>;
|
||||
toggle: VoidFunction;
|
||||
complete: (remainInChallenge?: boolean) => void;
|
||||
type: typeof ChallengeType;
|
||||
|
@ -96,10 +97,12 @@ export type GenericChallenge = Replace<
|
|||
>;
|
||||
|
||||
export function createChallenge<T extends ChallengeOptions>(
|
||||
optionsFunc: () => T & ThisType<Challenge<T>>
|
||||
optionsFunc: OptionsFunc<T, Challenge<T>, BaseChallenge>
|
||||
): Challenge<T> {
|
||||
const completions = persistent(0);
|
||||
const active = persistent(false);
|
||||
return createLazyProxy(() => {
|
||||
const challenge: T & Partial<BaseChallenge> = optionsFunc();
|
||||
const challenge = optionsFunc();
|
||||
|
||||
if (
|
||||
challenge.canComplete == null &&
|
||||
|
@ -116,8 +119,8 @@ export function createChallenge<T extends ChallengeOptions>(
|
|||
challenge.type = ChallengeType;
|
||||
challenge[Component] = ChallengeComponent;
|
||||
|
||||
challenge.completions = persistent(0);
|
||||
challenge.active = persistent(false);
|
||||
challenge.completions = completions;
|
||||
challenge.active = active;
|
||||
challenge.completed = computed(() =>
|
||||
Decimal.gt((challenge as GenericChallenge).completions.value, 0)
|
||||
);
|
||||
|
|
|
@ -2,6 +2,7 @@ import ClickableComponent from "features/clickables/Clickable.vue";
|
|||
import {
|
||||
CoercableComponent,
|
||||
Component,
|
||||
OptionsFunc,
|
||||
GatherProps,
|
||||
getUniqueID,
|
||||
Replace,
|
||||
|
@ -69,10 +70,10 @@ export type GenericClickable = Replace<
|
|||
>;
|
||||
|
||||
export function createClickable<T extends ClickableOptions>(
|
||||
optionsFunc: () => T & ThisType<Clickable<T>>
|
||||
optionsFunc: OptionsFunc<T, Clickable<T>, BaseClickable>
|
||||
): Clickable<T> {
|
||||
return createLazyProxy(() => {
|
||||
const clickable: T & Partial<BaseClickable> = optionsFunc();
|
||||
const clickable = optionsFunc();
|
||||
clickable.id = getUniqueID("clickable-");
|
||||
clickable.type = ClickableType;
|
||||
clickable[Component] = ClickableComponent;
|
||||
|
|
|
@ -4,19 +4,20 @@ import { isFunction } from "util/common";
|
|||
import {
|
||||
Computable,
|
||||
convertComputable,
|
||||
DoNotCache,
|
||||
GetComputableTypeWithDefault,
|
||||
processComputable,
|
||||
ProcessedComputable
|
||||
} from "util/computed";
|
||||
import { createLazyProxy } from "util/proxies";
|
||||
import { computed, isRef, Ref, unref } from "vue";
|
||||
import { Replace, setDefault } from "./feature";
|
||||
import { OptionsFunc, Replace, setDefault } from "./feature";
|
||||
import { Resource } from "./resources/resource";
|
||||
|
||||
export interface ConversionOptions {
|
||||
scaling: ScalingFunction;
|
||||
currentGain?: Computable<DecimalSource>;
|
||||
actualGain?: Computable<DecimalSource>;
|
||||
currentAt?: Computable<DecimalSource>;
|
||||
nextAt?: Computable<DecimalSource>;
|
||||
baseResource: Resource;
|
||||
gainResource: Resource;
|
||||
|
@ -34,6 +35,8 @@ export type Conversion<T extends ConversionOptions> = Replace<
|
|||
T & BaseConversion,
|
||||
{
|
||||
currentGain: GetComputableTypeWithDefault<T["currentGain"], Ref<DecimalSource>>;
|
||||
actualGain: GetComputableTypeWithDefault<T["actualGain"], Ref<DecimalSource>>;
|
||||
currentAt: GetComputableTypeWithDefault<T["currentAt"], Ref<DecimalSource>>;
|
||||
nextAt: GetComputableTypeWithDefault<T["nextAt"], Ref<DecimalSource>>;
|
||||
buyMax: GetComputableTypeWithDefault<T["buyMax"], true>;
|
||||
roundUpCost: GetComputableTypeWithDefault<T["roundUpCost"], true>;
|
||||
|
@ -44,6 +47,8 @@ export type GenericConversion = Replace<
|
|||
Conversion<ConversionOptions>,
|
||||
{
|
||||
currentGain: ProcessedComputable<DecimalSource>;
|
||||
actualGain: ProcessedComputable<DecimalSource>;
|
||||
currentAt: ProcessedComputable<DecimalSource>;
|
||||
nextAt: ProcessedComputable<DecimalSource>;
|
||||
buyMax: ProcessedComputable<boolean>;
|
||||
roundUpCost: ProcessedComputable<boolean>;
|
||||
|
@ -56,10 +61,10 @@ export interface GainModifier {
|
|||
}
|
||||
|
||||
export function createConversion<T extends ConversionOptions>(
|
||||
optionsFunc: () => T & ThisType<Conversion<T>>
|
||||
optionsFunc: OptionsFunc<T, Conversion<T>, BaseConversion>
|
||||
): Conversion<T> {
|
||||
return createLazyProxy(() => {
|
||||
const conversion: T = optionsFunc();
|
||||
const conversion = optionsFunc();
|
||||
|
||||
if (conversion.currentGain == null) {
|
||||
conversion.currentGain = computed(() => {
|
||||
|
@ -70,12 +75,22 @@ export function createConversion<T extends ConversionOptions>(
|
|||
: conversion.scaling.currentGain(conversion as GenericConversion);
|
||||
gain = Decimal.floor(gain).max(0);
|
||||
|
||||
if (!conversion.buyMax) {
|
||||
if (!unref(conversion.buyMax)) {
|
||||
gain = gain.min(1);
|
||||
}
|
||||
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) {
|
||||
conversion.nextAt = computed(() => {
|
||||
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, "actualGain");
|
||||
processComputable(conversion as T, "currentAt");
|
||||
processComputable(conversion as T, "nextAt");
|
||||
processComputable(conversion as T, "buyMax");
|
||||
setDefault(conversion, "buyMax", true);
|
||||
|
@ -108,6 +125,7 @@ export function createConversion<T extends ConversionOptions>(
|
|||
|
||||
export type ScalingFunction = {
|
||||
currentGain: (conversion: GenericConversion) => DecimalSource;
|
||||
currentAt: (conversion: GenericConversion) => DecimalSource;
|
||||
nextAt: (conversion: GenericConversion) => DecimalSource;
|
||||
};
|
||||
|
||||
|
@ -128,11 +146,20 @@ export function createLinearScaling(
|
|||
.times(unref(coefficient))
|
||||
.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) {
|
||||
let next: DecimalSource = Decimal.add(unref(conversion.currentGain), 1);
|
||||
if (conversion.gainModifier) {
|
||||
next = conversion.gainModifier.revert(next);
|
||||
}
|
||||
next = Decimal.max(0, next);
|
||||
return Decimal.times(next, unref(coefficient)).add(unref(base)).max(unref(base));
|
||||
}
|
||||
};
|
||||
|
@ -155,24 +182,33 @@ export function createPolynomialScaling(
|
|||
}
|
||||
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) {
|
||||
let next: DecimalSource = Decimal.add(unref(conversion.currentGain), 1);
|
||||
if (conversion.gainModifier) {
|
||||
next = conversion.gainModifier.revert(next);
|
||||
}
|
||||
next = Decimal.max(0, next);
|
||||
return Decimal.root(next, unref(exponent)).times(unref(base)).max(unref(base));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
export function createCumulativeConversion<S extends ConversionOptions>(
|
||||
optionsFunc: () => S & ThisType<Conversion<S>>
|
||||
optionsFunc: OptionsFunc<S, Conversion<S>>
|
||||
): Conversion<S> {
|
||||
return createConversion(optionsFunc);
|
||||
}
|
||||
|
||||
export function createIndependentConversion<S extends ConversionOptions>(
|
||||
optionsFunc: () => S & ThisType<Conversion<S>>
|
||||
optionsFunc: OptionsFunc<S, Conversion<S>>
|
||||
): Conversion<S> {
|
||||
return createConversion(() => {
|
||||
const conversion: S = optionsFunc();
|
||||
|
@ -180,14 +216,32 @@ export function createIndependentConversion<S extends ConversionOptions>(
|
|||
setDefault(conversion, "buyMax", false);
|
||||
|
||||
if (conversion.currentGain == null) {
|
||||
conversion.currentGain = computed(() =>
|
||||
Decimal.sub(
|
||||
conversion.currentGain = computed(() => {
|
||||
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.gainResource.value
|
||||
)
|
||||
.add(1)
|
||||
.max(1)
|
||||
);
|
||||
).max(0);
|
||||
|
||||
if (!unref(conversion.buyMax)) {
|
||||
gain = gain.min(1);
|
||||
}
|
||||
return gain;
|
||||
});
|
||||
}
|
||||
setDefault(conversion, "convert", function () {
|
||||
conversion.gainResource.value = conversion.gainModifier
|
||||
|
|
|
@ -24,6 +24,8 @@ export type FeatureComponent<T> = Omit<
|
|||
|
||||
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;
|
||||
// Get a unique ID to allow a feature to be found for creating branches
|
||||
// and any other uses requiring unique identifiers for each feature
|
||||
|
|
|
@ -2,6 +2,7 @@ import GridComponent from "features/grids/Grid.vue";
|
|||
import {
|
||||
CoercableComponent,
|
||||
Component,
|
||||
OptionsFunc,
|
||||
GatherProps,
|
||||
getUniqueID,
|
||||
Replace,
|
||||
|
@ -19,7 +20,7 @@ import {
|
|||
} from "util/computed";
|
||||
import { createLazyProxy } from "util/proxies";
|
||||
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");
|
||||
|
||||
|
@ -241,11 +242,10 @@ export type GenericGrid = Replace<
|
|||
>;
|
||||
|
||||
export function createGrid<T extends GridOptions>(
|
||||
optionsFunc: () => T & ThisType<Grid<T>>
|
||||
optionsFunc: OptionsFunc<T, Grid<T>, BaseGrid>
|
||||
): Grid<T> {
|
||||
return createLazyProxy(() => {
|
||||
const grid: T & Partial<BaseGrid> = optionsFunc();
|
||||
makePersistent(grid, {});
|
||||
return createLazyProxy(persistent => {
|
||||
const grid = Object.assign(persistent, optionsFunc());
|
||||
grid.id = getUniqueID("grid-");
|
||||
grid[Component] = GridComponent;
|
||||
|
||||
|
@ -301,5 +301,5 @@ export function createGrid<T extends GridOptions>(
|
|||
};
|
||||
|
||||
return grid as unknown as Grid<T>;
|
||||
});
|
||||
}, persistent({}));
|
||||
}
|
||||
|
|
|
@ -11,7 +11,7 @@ import {
|
|||
} from "util/computed";
|
||||
import { createLazyProxy } from "util/proxies";
|
||||
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 HotkeyType = Symbol("Hotkey");
|
||||
|
@ -43,10 +43,10 @@ export type GenericHotkey = Replace<
|
|||
>;
|
||||
|
||||
export function createHotkey<T extends HotkeyOptions>(
|
||||
optionsFunc: () => T & ThisType<Hotkey<T>>
|
||||
optionsFunc: OptionsFunc<T, Hotkey<T>, BaseHotkey>
|
||||
): Hotkey<T> {
|
||||
return createLazyProxy(() => {
|
||||
const hotkey: T & Partial<BaseHotkey> = optionsFunc();
|
||||
const hotkey = optionsFunc();
|
||||
hotkey.type = HotkeyType;
|
||||
|
||||
processComputable(hotkey as T, "enabled");
|
||||
|
|
|
@ -2,6 +2,7 @@ import InfoboxComponent from "features/infoboxes/Infobox.vue";
|
|||
import {
|
||||
CoercableComponent,
|
||||
Component,
|
||||
OptionsFunc,
|
||||
GatherProps,
|
||||
getUniqueID,
|
||||
Replace,
|
||||
|
@ -18,7 +19,7 @@ import {
|
|||
} from "util/computed";
|
||||
import { createLazyProxy } from "util/proxies";
|
||||
import { Ref, unref } from "vue";
|
||||
import { Persistent, makePersistent, PersistentState } from "game/persistence";
|
||||
import { Persistent, PersistentState, persistent } from "game/persistence";
|
||||
|
||||
export const InfoboxType = Symbol("Infobox");
|
||||
|
||||
|
@ -63,11 +64,10 @@ export type GenericInfobox = Replace<
|
|||
>;
|
||||
|
||||
export function createInfobox<T extends InfoboxOptions>(
|
||||
optionsFunc: () => T & ThisType<Infobox<T>>
|
||||
optionsFunc: OptionsFunc<T, Infobox<T>, BaseInfobox>
|
||||
): Infobox<T> {
|
||||
return createLazyProxy(() => {
|
||||
const infobox: T & Partial<BaseInfobox> = optionsFunc();
|
||||
makePersistent<boolean>(infobox, false);
|
||||
return createLazyProxy(persistent => {
|
||||
const infobox = Object.assign(persistent, optionsFunc());
|
||||
infobox.id = getUniqueID("infobox-");
|
||||
infobox.type = InfoboxType;
|
||||
infobox[Component] = InfoboxComponent;
|
||||
|
@ -112,5 +112,5 @@ export function createInfobox<T extends InfoboxOptions>(
|
|||
};
|
||||
|
||||
return infobox as unknown as Infobox<T>;
|
||||
});
|
||||
}, persistent<boolean>(false));
|
||||
}
|
||||
|
|
|
@ -50,6 +50,7 @@ function updateNodes() {
|
|||
});
|
||||
}
|
||||
}
|
||||
document.fonts.ready.then(updateNodes);
|
||||
|
||||
const validLinks = computed(() => {
|
||||
const n = nodes.value;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
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 {
|
||||
Computable,
|
||||
|
@ -44,10 +44,10 @@ export type GenericLinks = Replace<
|
|||
>;
|
||||
|
||||
export function createLinks<T extends LinksOptions>(
|
||||
optionsFunc: (() => T) & ThisType<Links<T>>
|
||||
optionsFunc: OptionsFunc<T, Links<T>, BaseLinks>
|
||||
): Links<T> {
|
||||
return createLazyProxy(() => {
|
||||
const links: T & Partial<BaseLinks> = optionsFunc();
|
||||
const links = optionsFunc();
|
||||
links.type = LinksType;
|
||||
links[Component] = LinksComponent;
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ import Select from "components/fields/Select.vue";
|
|||
import {
|
||||
CoercableComponent,
|
||||
Component,
|
||||
OptionsFunc,
|
||||
GatherProps,
|
||||
getUniqueID,
|
||||
jsx,
|
||||
|
@ -13,7 +14,7 @@ import {
|
|||
import MilestoneComponent from "features/milestones/Milestone.vue";
|
||||
import { globalBus } from "game/events";
|
||||
import "game/notifications";
|
||||
import { makePersistent, Persistent, PersistentState } from "game/persistence";
|
||||
import { persistent, Persistent, PersistentState } from "game/persistence";
|
||||
import settings, { registerSettingField } from "game/settings";
|
||||
import { camelToTitle } from "util/common";
|
||||
import {
|
||||
|
@ -83,11 +84,10 @@ export type GenericMilestone = Replace<
|
|||
>;
|
||||
|
||||
export function createMilestone<T extends MilestoneOptions>(
|
||||
optionsFunc: () => T & ThisType<Milestone<T>>
|
||||
optionsFunc: OptionsFunc<T, Milestone<T>, BaseMilestone>
|
||||
): Milestone<T> {
|
||||
return createLazyProxy(() => {
|
||||
const milestone: T & Partial<BaseMilestone> = optionsFunc();
|
||||
makePersistent<boolean>(milestone, false);
|
||||
return createLazyProxy(persistent => {
|
||||
const milestone = Object.assign(persistent, optionsFunc());
|
||||
milestone.id = getUniqueID("milestone-");
|
||||
milestone.type = MilestoneType;
|
||||
milestone[Component] = MilestoneComponent;
|
||||
|
@ -168,7 +168,7 @@ export function createMilestone<T extends MilestoneOptions>(
|
|||
}
|
||||
|
||||
return milestone as unknown as Milestone<T>;
|
||||
});
|
||||
}, persistent<boolean>(false));
|
||||
}
|
||||
|
||||
declare module "game/settings" {
|
||||
|
|
|
@ -1,89 +1,74 @@
|
|||
onMounted,
|
||||
<template>
|
||||
<Particles
|
||||
:id="id"
|
||||
:class="{
|
||||
'not-fullscreen': !fullscreen
|
||||
}"
|
||||
: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"
|
||||
:style="unref(style)"
|
||||
:class="unref(classes)"
|
||||
/>
|
||||
<div ref="resizeListener" class="resize-listener" />
|
||||
</template>
|
||||
|
||||
<script lang="tsx">
|
||||
import { loadFull } from "tsparticles";
|
||||
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 { StyleValue } from "features/feature";
|
||||
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
|
||||
export default defineComponent({
|
||||
props: {
|
||||
zIndex: {
|
||||
type: Number,
|
||||
required: true
|
||||
},
|
||||
fullscreen: {
|
||||
type: Boolean,
|
||||
required: true
|
||||
},
|
||||
style: processedPropType<StyleValue>(String, Object, Array),
|
||||
classes: processedPropType<Record<string, boolean>>(Object),
|
||||
onInit: {
|
||||
type: Function as PropType<(container: EmitterContainer & Container) => void>,
|
||||
type: Function as PropType<(app: Application) => void>,
|
||||
required: true
|
||||
},
|
||||
id: {
|
||||
type: String,
|
||||
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) {
|
||||
const particles = ref<null | { particles: { container: Emitters } }>(null);
|
||||
|
||||
async function particlesInit(engine: Engine) {
|
||||
await loadFull(engine);
|
||||
}
|
||||
|
||||
function particlesLoaded(container: EmitterContainer & Container) {
|
||||
props.onInit(container);
|
||||
}
|
||||
const app = ref<null | Application>(null);
|
||||
|
||||
const resizeObserver = new ResizeObserver(updateBounds);
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const nodes = inject(NodesInjectionKey)!;
|
||||
|
||||
const resizeListener = ref<Element | null>(null);
|
||||
const resizeListener = ref<HTMLElement | null>(null);
|
||||
|
||||
onMounted(() => {
|
||||
// ResizeListener exists because ResizeObserver's don't work when told to observe an SVG element
|
||||
const resListener = resizeListener.value;
|
||||
if (resListener != null) {
|
||||
resizeObserver.observe(resListener);
|
||||
app.value = new Application({
|
||||
resizeTo: resListener,
|
||||
backgroundAlpha: 0
|
||||
});
|
||||
resizeListener.value?.appendChild(app.value.view);
|
||||
props.onInit(app.value as Application);
|
||||
}
|
||||
updateBounds();
|
||||
if (module.hot?.status() === "apply" && props.onHotReload) {
|
||||
nextTick(props.onHotReload);
|
||||
}
|
||||
});
|
||||
onBeforeUnmount(() => {
|
||||
app.value?.destroy();
|
||||
});
|
||||
|
||||
let isDirty = true;
|
||||
|
@ -93,20 +78,20 @@ export default defineComponent({
|
|||
nextTick(() => {
|
||||
if (resizeListener.value != null && props.onContainerResized) {
|
||||
// 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())
|
||||
);
|
||||
props.onContainerResized(resizeListener.value.getBoundingClientRect());
|
||||
app.value?.resize();
|
||||
}
|
||||
isDirty = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
document.fonts.ready.then(updateBounds);
|
||||
|
||||
return {
|
||||
particles,
|
||||
particlesInit,
|
||||
particlesLoaded,
|
||||
unref,
|
||||
resizeListener
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,27 +1,31 @@
|
|||
import ParticlesComponent from "features/particles/Particles.vue";
|
||||
import { Container } from "tsparticles-engine";
|
||||
import { IEmitter } from "tsparticles-plugin-emitters/Options/Interfaces/IEmitter";
|
||||
import { EmitterInstance } from "tsparticles-plugin-emitters/EmitterInstance";
|
||||
import { EmitterContainer } from "tsparticles-plugin-emitters/EmitterContainer";
|
||||
import { Ref, shallowRef } from "vue";
|
||||
import { Component, GatherProps, getUniqueID, Replace, setDefault } from "features/feature";
|
||||
import { Ref, shallowRef, unref } from "vue";
|
||||
import {
|
||||
Component,
|
||||
OptionsFunc,
|
||||
GatherProps,
|
||||
getUniqueID,
|
||||
Replace,
|
||||
StyleValue
|
||||
} from "features/feature";
|
||||
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 interface ParticlesOptions {
|
||||
fullscreen?: boolean;
|
||||
zIndex?: number;
|
||||
classes?: Computable<Record<string, boolean>>;
|
||||
style?: Computable<StyleValue>;
|
||||
onContainerResized?: (boundingRect: DOMRect) => void;
|
||||
onHotReload?: VoidFunction;
|
||||
}
|
||||
|
||||
export interface BaseParticles {
|
||||
id: string;
|
||||
containerRef: Ref<null | (EmitterContainer & Container)>;
|
||||
addEmitter: (
|
||||
options: IEmitter & { particles: Required<IEmitter>["particles"] }
|
||||
) => Promise<EmitterInstance>;
|
||||
removeEmitter: (emitter: EmitterInstance) => void;
|
||||
app: Ref<null | Application>;
|
||||
addEmitter: (config: EmitterConfigV3) => Promise<Emitter>;
|
||||
type: typeof ParticlesType;
|
||||
[Component]: typeof ParticlesComponent;
|
||||
[GatherProps]: () => Record<string, unknown>;
|
||||
|
@ -30,68 +34,54 @@ export interface BaseParticles {
|
|||
export type Particles<T extends ParticlesOptions> = Replace<
|
||||
T & BaseParticles,
|
||||
{
|
||||
fullscreen: undefined extends T["fullscreen"] ? true : T["fullscreen"];
|
||||
zIndex: undefined extends T["zIndex"] ? 1 : T["zIndex"];
|
||||
classes: GetComputableType<T["classes"]>;
|
||||
style: GetComputableType<T["style"]>;
|
||||
}
|
||||
>;
|
||||
|
||||
export type GenericParticles = Replace<
|
||||
Particles<ParticlesOptions>,
|
||||
{
|
||||
fullscreen: boolean;
|
||||
zIndex: number;
|
||||
}
|
||||
>;
|
||||
export type GenericParticles = Particles<ParticlesOptions>;
|
||||
|
||||
export function createParticles<T extends ParticlesOptions>(
|
||||
optionsFunc: () => T & ThisType<Particles<T>>
|
||||
optionsFunc: OptionsFunc<T, Particles<T>, BaseParticles>
|
||||
): Particles<T> {
|
||||
return createLazyProxy(() => {
|
||||
const particles: T & Partial<BaseParticles> = optionsFunc();
|
||||
const particles = optionsFunc();
|
||||
particles.id = getUniqueID("particles-");
|
||||
particles.type = ParticlesType;
|
||||
particles[Component] = ParticlesComponent;
|
||||
|
||||
particles.containerRef = shallowRef(null);
|
||||
particles.addEmitter = (
|
||||
options: IEmitter & { particles: Required<IEmitter>["particles"] }
|
||||
): Promise<EmitterInstance> => {
|
||||
particles.app = shallowRef(null);
|
||||
particles.addEmitter = (config: EmitterConfigV3): Promise<Emitter> => {
|
||||
const genericParticles = particles as GenericParticles;
|
||||
if (genericParticles.containerRef.value) {
|
||||
// TODO why does addEmitter require a position parameter
|
||||
return Promise.resolve(genericParticles.containerRef.value.addEmitter(options));
|
||||
if (genericParticles.app.value) {
|
||||
return Promise.resolve(new Emitter(genericParticles.app.value.stage, config));
|
||||
}
|
||||
return new Promise<EmitterInstance>(resolve => {
|
||||
emittersToAdd.push({ resolve, options });
|
||||
return new Promise<Emitter>(resolve => {
|
||||
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: {
|
||||
resolve: (value: EmitterInstance | PromiseLike<EmitterInstance>) => void;
|
||||
options: IEmitter & { particles: Required<IEmitter>["particles"] };
|
||||
resolve: (value: Emitter | PromiseLike<Emitter>) => void;
|
||||
config: EmitterConfigV3;
|
||||
}[] = [];
|
||||
|
||||
function onInit(container: EmitterContainer & Container) {
|
||||
(particles as GenericParticles).containerRef.value = container;
|
||||
emittersToAdd.forEach(({ resolve, options }) => resolve(container.addEmitter(options)));
|
||||
function onInit(app: Application) {
|
||||
(particles as GenericParticles).app.value = app;
|
||||
emittersToAdd.forEach(({ resolve, config }) => resolve(new Emitter(app.stage, config)));
|
||||
emittersToAdd = [];
|
||||
}
|
||||
|
||||
setDefault(particles, "fullscreen", true);
|
||||
setDefault(particles, "zIndex", 1);
|
||||
particles.onContainerResized = particles.onContainerResized?.bind(particles);
|
||||
|
||||
particles[GatherProps] = function (this: GenericParticles) {
|
||||
const { id, fullscreen, zIndex, onContainerResized } = this;
|
||||
const { id, style, classes, onContainerResized, onHotReload } = this;
|
||||
return {
|
||||
id,
|
||||
fullscreen,
|
||||
zIndex,
|
||||
style: unref(style),
|
||||
classes,
|
||||
onContainerResized,
|
||||
onHotReload,
|
||||
onInit
|
||||
};
|
||||
};
|
||||
|
@ -99,3 +89,10 @@ export function createParticles<T extends ParticlesOptions>(
|
|||
return particles as unknown as Particles<T>;
|
||||
});
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
upgradeConfig: typeof upgradeConfig;
|
||||
}
|
||||
}
|
||||
window.upgradeConfig = upgradeConfig;
|
||||
|
|
|
@ -1,13 +1,7 @@
|
|||
import { getUniqueID, Replace } from "features/feature";
|
||||
import { OptionsFunc, getUniqueID, Replace } from "features/feature";
|
||||
import { globalBus } from "game/events";
|
||||
import { GenericLayer } from "game/layers";
|
||||
import {
|
||||
DefaultValue,
|
||||
Persistent,
|
||||
persistent,
|
||||
PersistentRef,
|
||||
PersistentState
|
||||
} from "game/persistence";
|
||||
import { DefaultValue, Persistent, persistent, PersistentState } from "game/persistence";
|
||||
import Decimal from "util/bignum";
|
||||
import { Computable, GetComputableType, processComputable } from "util/computed";
|
||||
import { createLazyProxy } from "util/proxies";
|
||||
|
@ -37,10 +31,10 @@ export type Reset<T extends ResetOptions> = Replace<
|
|||
export type GenericReset = Reset<ResetOptions>;
|
||||
|
||||
export function createReset<T extends ResetOptions>(
|
||||
optionsFunc: () => T & ThisType<Reset<T>>
|
||||
optionsFunc: OptionsFunc<T, Reset<T>, BaseReset>
|
||||
): Reset<T> {
|
||||
return createLazyProxy(() => {
|
||||
const reset: T & Partial<BaseReset> = optionsFunc();
|
||||
const reset = optionsFunc();
|
||||
reset.id = getUniqueID("reset-");
|
||||
reset.type = ResetType;
|
||||
|
||||
|
@ -70,7 +64,7 @@ export function createReset<T extends ResetOptions>(
|
|||
}
|
||||
|
||||
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));
|
||||
listeners[layer.id] = layer.on("preUpdate", diff => {
|
||||
resetTime.value = Decimal.add(resetTime.value, diff);
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {
|
||||
CoercableComponent,
|
||||
Component,
|
||||
OptionsFunc,
|
||||
GatherProps,
|
||||
getUniqueID,
|
||||
Replace,
|
||||
|
@ -36,9 +37,11 @@ export type Tab<T extends TabOptions> = Replace<
|
|||
|
||||
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(() => {
|
||||
const tab: T & Partial<BaseTab> = optionsFunc();
|
||||
const tab = optionsFunc();
|
||||
tab.id = getUniqueID("tab-");
|
||||
tab.type = TabType;
|
||||
tab[Component] = TabComponent;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {
|
||||
CoercableComponent,
|
||||
Component,
|
||||
OptionsFunc,
|
||||
GatherProps,
|
||||
getUniqueID,
|
||||
Replace,
|
||||
|
@ -10,7 +11,7 @@ import {
|
|||
} from "features/feature";
|
||||
import TabButtonComponent from "features/tabs/TabButton.vue";
|
||||
import TabFamilyComponent from "features/tabs/TabFamily.vue";
|
||||
import { Persistent, makePersistent, PersistentState } from "game/persistence";
|
||||
import { Persistent, PersistentState, persistent } from "game/persistence";
|
||||
import {
|
||||
Computable,
|
||||
GetComputableType,
|
||||
|
@ -60,13 +61,13 @@ export type GenericTabButton = Replace<
|
|||
|
||||
export interface TabFamilyOptions {
|
||||
visibility?: Computable<Visibility>;
|
||||
tabs: Record<string, TabButtonOptions>;
|
||||
classes?: Computable<Record<string, boolean>>;
|
||||
style?: Computable<StyleValue>;
|
||||
}
|
||||
|
||||
export interface BaseTabFamily extends Persistent<string> {
|
||||
id: string;
|
||||
tabs: Record<string, TabButtonOptions>;
|
||||
activeTab: Ref<GenericTab | CoercableComponent | null>;
|
||||
selected: Ref<string>;
|
||||
type: typeof TabFamilyType;
|
||||
|
@ -90,21 +91,39 @@ export type GenericTabFamily = Replace<
|
|||
>;
|
||||
|
||||
export function createTabFamily<T extends TabFamilyOptions>(
|
||||
optionsFunc: () => T & ThisType<TabFamily<T>>
|
||||
tabs: Record<string, () => TabButtonOptions>,
|
||||
optionsFunc: OptionsFunc<T, TabFamily<T>, BaseTabFamily>
|
||||
): TabFamily<T> {
|
||||
return createLazyProxy(() => {
|
||||
const tabFamily: T & Partial<BaseTabFamily> = optionsFunc();
|
||||
if (Object.keys(tabs).length === 0) {
|
||||
console.warn("Cannot create tab family with 0 tabs");
|
||||
throw "Cannot create tab family with 0 tabs";
|
||||
}
|
||||
|
||||
if (Object.keys(tabFamily.tabs).length === 0) {
|
||||
console.warn("Cannot create tab family with 0 tabs", tabFamily);
|
||||
throw "Cannot create tab family with 0 tabs";
|
||||
}
|
||||
return createLazyProxy(persistent => {
|
||||
const tabFamily = Object.assign(persistent, optionsFunc());
|
||||
|
||||
tabFamily.id = getUniqueID("tabFamily-");
|
||||
tabFamily.type = TabFamilyType;
|
||||
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.activeTab = computed(() => {
|
||||
const tabs = unref(processedTabFamily.tabs);
|
||||
|
@ -129,20 +148,6 @@ export function createTabFamily<T extends TabFamilyOptions>(
|
|||
processComputable(tabFamily as T, "classes");
|
||||
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) {
|
||||
const { visibility, activeTab, selected, tabs, style, classes } = this;
|
||||
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
|
||||
const processedTabFamily = tabFamily as unknown as TabFamily<T>;
|
||||
return processedTabFamily;
|
||||
});
|
||||
}, persistent(Object.keys(tabs)[0]));
|
||||
}
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import {
|
||||
CoercableComponent,
|
||||
Component,
|
||||
OptionsFunc,
|
||||
GatherProps,
|
||||
getUniqueID,
|
||||
Replace,
|
||||
|
@ -13,7 +14,7 @@ import { GenericReset } from "features/reset";
|
|||
import { displayResource, Resource } from "features/resources/resource";
|
||||
import { Tooltip } from "features/tooltip";
|
||||
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 {
|
||||
Computable,
|
||||
|
@ -74,18 +75,20 @@ export type GenericTreeNode = Replace<
|
|||
>;
|
||||
|
||||
export function createTreeNode<T extends TreeNodeOptions>(
|
||||
optionsFunc: () => T & ThisType<TreeNode<T>>
|
||||
optionsFunc: OptionsFunc<T, TreeNode<T>, BaseTreeNode>
|
||||
): TreeNode<T> {
|
||||
const forceTooltip = persistent(false);
|
||||
return createLazyProxy(() => {
|
||||
const treeNode: T & Partial<BaseTreeNode> = optionsFunc();
|
||||
const treeNode = optionsFunc();
|
||||
treeNode.id = getUniqueID("treeNode-");
|
||||
treeNode.type = TreeNodeType;
|
||||
|
||||
if (treeNode.tooltip) {
|
||||
treeNode.forceTooltip = persistent(false);
|
||||
treeNode.forceTooltip = forceTooltip;
|
||||
} else {
|
||||
// If we don't have a tooltip, no point in making this persistent
|
||||
treeNode.forceTooltip = ref(false);
|
||||
deletePersistent(forceTooltip);
|
||||
}
|
||||
|
||||
processComputable(treeNode as T, "visibility");
|
||||
|
@ -166,10 +169,10 @@ export type GenericTree = Replace<
|
|||
>;
|
||||
|
||||
export function createTree<T extends TreeOptions>(
|
||||
optionsFunc: () => T & ThisType<Tree<T>>
|
||||
optionsFunc: OptionsFunc<T, Tree<T>, BaseTree>
|
||||
): Tree<T> {
|
||||
return createLazyProxy(() => {
|
||||
const tree: T & Partial<BaseTree> = optionsFunc();
|
||||
const tree = optionsFunc();
|
||||
tree.id = getUniqueID("tree-");
|
||||
tree.type = TreeType;
|
||||
tree[Component] = TreeComponent;
|
||||
|
|
|
@ -2,6 +2,7 @@ import UpgradeComponent from "features/upgrades/Upgrade.vue";
|
|||
import {
|
||||
CoercableComponent,
|
||||
Component,
|
||||
OptionsFunc,
|
||||
findFeatures,
|
||||
GatherProps,
|
||||
getUniqueID,
|
||||
|
@ -23,7 +24,7 @@ import {
|
|||
} from "util/computed";
|
||||
import { createLazyProxy } from "util/proxies";
|
||||
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");
|
||||
|
||||
|
@ -78,11 +79,10 @@ export type GenericUpgrade = Replace<
|
|||
>;
|
||||
|
||||
export function createUpgrade<T extends UpgradeOptions>(
|
||||
optionsFunc: () => T & ThisType<Upgrade<T>>
|
||||
optionsFunc: OptionsFunc<T, Upgrade<T>, BaseUpgrade>
|
||||
): Upgrade<T> {
|
||||
return createLazyProxy(() => {
|
||||
const upgrade: T & Partial<BaseUpgrade> = optionsFunc();
|
||||
makePersistent<boolean>(upgrade, false);
|
||||
return createLazyProxy(persistent => {
|
||||
const upgrade = Object.assign(persistent, optionsFunc());
|
||||
upgrade.id = getUniqueID("upgrade-");
|
||||
upgrade.type = UpgradeType;
|
||||
upgrade[Component] = UpgradeComponent;
|
||||
|
@ -167,7 +167,7 @@ export function createUpgrade<T extends UpgradeOptions>(
|
|||
};
|
||||
|
||||
return upgrade as unknown as Upgrade<T>;
|
||||
});
|
||||
}, persistent<boolean>(false));
|
||||
}
|
||||
|
||||
export function setupAutoPurchase(
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import Modal from "components/Modal.vue";
|
||||
import {
|
||||
CoercableComponent,
|
||||
OptionsFunc,
|
||||
jsx,
|
||||
JSXFunction,
|
||||
Replace,
|
||||
|
@ -18,7 +19,7 @@ import { createLazyProxy } from "util/proxies";
|
|||
import { createNanoEvents, Emitter } from "nanoevents";
|
||||
import { InjectionKey, Ref, ref, unref } from "vue";
|
||||
import { globalBus } from "./events";
|
||||
import { persistent, PersistentRef } from "./persistence";
|
||||
import { Persistent, persistent } from "./persistence";
|
||||
import player from "./player";
|
||||
|
||||
export interface FeatureNode {
|
||||
|
@ -58,7 +59,6 @@ export interface Position {
|
|||
}
|
||||
|
||||
export interface LayerOptions {
|
||||
id: string;
|
||||
color?: Computable<string>;
|
||||
display: Computable<CoercableComponent>;
|
||||
classes?: Computable<Record<string, boolean>>;
|
||||
|
@ -70,7 +70,8 @@ export interface LayerOptions {
|
|||
}
|
||||
|
||||
export interface BaseLayer {
|
||||
minimized: PersistentRef<boolean>;
|
||||
id: string;
|
||||
minimized: Persistent<boolean>;
|
||||
emitter: Emitter<LayerEvents>;
|
||||
on: OmitThisParameter<Emitter<LayerEvents>["on"]>;
|
||||
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"]>;
|
||||
classes: GetComputableType<T["classes"]>;
|
||||
style: GetComputableType<T["style"]>;
|
||||
name: GetComputableTypeWithDefault<T["name"], T["id"]>;
|
||||
name: GetComputableTypeWithDefault<T["name"], string>;
|
||||
minWidth: GetComputableTypeWithDefault<T["minWidth"], 600>;
|
||||
minimizable: GetComputableTypeWithDefault<T["minimizable"], true>;
|
||||
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>(
|
||||
optionsFunc: (() => T) & ThisType<BaseLayer>
|
||||
id: string,
|
||||
optionsFunc: OptionsFunc<T, BaseLayer, BaseLayer>
|
||||
): Layer<T> {
|
||||
return createLazyProxy(() => {
|
||||
const layer = {} as T & Partial<BaseLayer>;
|
||||
|
@ -109,10 +113,19 @@ export function createLayer<T extends LayerOptions>(
|
|||
layer.on = emitter.on.bind(emitter);
|
||||
layer.emit = emitter.emit.bind(emitter);
|
||||
layer.nodes = ref({});
|
||||
layer.id = id;
|
||||
|
||||
addingLayers.push(id);
|
||||
persistentRefs[id] = new Set();
|
||||
layer.minimized = persistent(false);
|
||||
|
||||
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, "display");
|
||||
|
|
|
@ -2,11 +2,13 @@ import { globalBus } from "game/events";
|
|||
import Decimal, { DecimalSource } from "util/bignum";
|
||||
import { ProxyState } from "util/proxies";
|
||||
import { isArray } from "@vue/shared";
|
||||
import { isRef, Ref, ref } from "vue";
|
||||
import { GenericLayer } from "./layers";
|
||||
import { isReactive, isRef, Ref, ref } from "vue";
|
||||
import { addingLayers, GenericLayer, persistentRefs } from "./layers";
|
||||
|
||||
export const PersistentState = Symbol("PersistentState");
|
||||
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
|
||||
// special processes for knowing what to load them in as
|
||||
|
@ -20,31 +22,53 @@ export type State =
|
|||
| { [key: string]: State }
|
||||
| { [key: number]: State };
|
||||
|
||||
export type Persistent<T extends State = State> = {
|
||||
export type Persistent<T extends State = State> = Ref<T> & {
|
||||
[PersistentState]: Ref<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 = (
|
||||
isRef(defaultValue) ? defaultValue : (ref<T>(defaultValue) as unknown)
|
||||
) as PersistentRef<T>;
|
||||
) as Persistent<T>;
|
||||
|
||||
persistent[PersistentState] = persistent;
|
||||
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>(
|
||||
obj: unknown,
|
||||
defaultValue: T
|
||||
): asserts obj is Persistent<T> {
|
||||
const persistent = obj as Partial<Persistent<T>>;
|
||||
const state = ref(defaultValue) as Ref<T>;
|
||||
|
||||
persistent[PersistentState] = state;
|
||||
persistent[DefaultValue] = isRef(defaultValue) ? (defaultValue.value as T) : defaultValue;
|
||||
export function deletePersistent(persistent: Persistent) {
|
||||
if (addingLayers.length === 0) {
|
||||
console.warn("Deleting a persistent ref outside of a layer. Ignoring...", persistent);
|
||||
} else {
|
||||
persistentRefs[addingLayers[addingLayers.length - 1]].delete(persistent);
|
||||
}
|
||||
persistent[Deleted] = true;
|
||||
}
|
||||
|
||||
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 (PersistentState in value) {
|
||||
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
|
||||
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
|
||||
persistentState[key] = (value as Persistent)[PersistentState];
|
||||
// Load previously saved value
|
||||
if (savedValue != null) {
|
||||
(persistentState[key] as Ref<unknown>).value = savedValue;
|
||||
if (isReactive(persistentState)) {
|
||||
if (savedValue != null) {
|
||||
persistentState[key] = savedValue;
|
||||
} else {
|
||||
persistentState[key] = (value as Persistent)[DefaultValue];
|
||||
}
|
||||
} else {
|
||||
(persistentState[key] as Ref<unknown>).value = (value as Persistent)[
|
||||
DefaultValue
|
||||
];
|
||||
if (savedValue != null) {
|
||||
(persistentState[key] as Ref<unknown>).value = savedValue;
|
||||
} else {
|
||||
(persistentState[key] as Ref<unknown>).value = (value as Persistent)[
|
||||
DefaultValue
|
||||
];
|
||||
}
|
||||
}
|
||||
} else if (
|
||||
!(value instanceof Decimal) &&
|
||||
|
@ -114,4 +160,12 @@ globalBus.on("addLayer", (layer: GenericLayer, saveData: Record<string, unknown>
|
|||
return foundPersistent;
|
||||
};
|
||||
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();
|
||||
});
|
||||
|
|
|
@ -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
|
||||
// Note that the object is lazily calculated
|
||||
export function createLazyProxy<T extends object>(objectFunc: () => T): T {
|
||||
const obj: T | Record<string, never> = {};
|
||||
export function createLazyProxy<T extends object, S>(
|
||||
objectFunc: (baseObject: S) => T & S,
|
||||
baseObject: S = {} as S
|
||||
): T {
|
||||
const obj: S & Partial<T> = baseObject;
|
||||
let calculated = false;
|
||||
function calculateObj(): T {
|
||||
if (!calculated) {
|
||||
Object.assign(obj, objectFunc());
|
||||
Object.assign(obj, objectFunc(obj));
|
||||
calculated = true;
|
||||
}
|
||||
return obj as T;
|
||||
return obj as S & T;
|
||||
}
|
||||
|
||||
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
|
||||
return (calculateObj() as any)[key];
|
||||
},
|
||||
set() {
|
||||
console.error("Layers and features are shallow readonly");
|
||||
set(target, key, value) {
|
||||
console.error("Layers and features are shallow readonly", key, value);
|
||||
return false;
|
||||
},
|
||||
has(target, key) {
|
||||
|
@ -51,10 +54,10 @@ export function createLazyProxy<T extends object>(objectFunc: () => T): T {
|
|||
},
|
||||
getOwnPropertyDescriptor(target, key) {
|
||||
if (!calculated) {
|
||||
Object.assign(obj, objectFunc());
|
||||
Object.assign(obj, objectFunc(obj));
|
||||
calculated = true;
|
||||
}
|
||||
return Object.getOwnPropertyDescriptor(target, key);
|
||||
}
|
||||
}) as T;
|
||||
}) as S & T;
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue