forked from profectus/Profectus
Compare commits
48 commits
ducdat0507
...
main
Author | SHA1 | Date | |
---|---|---|---|
62794b81a1 | |||
|
b40d4bef32 | ||
1f22f506dd | |||
d3faec6a66 | |||
a39e65852d | |||
1e2b20a70f | |||
2e0e221010 | |||
4092cd6d56 | |||
cba79df80d | |||
04a5e963ab | |||
263c951cf8 | |||
1b809a9550 | |||
5e32fa4985 | |||
|
cf6265d8ce | ||
4f807aaf96 | |||
ffc42a5745 | |||
|
8811996f64 | ||
|
7750a3368d | ||
2495dc9783 | |||
e66daad7a2 | |||
8065f8efa4 | |||
e9283b5cca | |||
2c615ea524 | |||
953cd8047e | |||
766c600a70 | |||
c1d0b7eec6 | |||
52b500c9d8 | |||
aabb0a1bba | |||
005bf5da9a | |||
7330a6bda4 | |||
65ff440e25 | |||
acf1d24c15 | |||
d16bb55c3c | |||
312cab1347 | |||
6d148da260 | |||
a5204106aa | |||
0cccf7aecc | |||
d0fffd3b89 | |||
3fe0311331 | |||
eee5ac3e2d | |||
9edda4d957 | |||
6ad08c4052 | |||
e0f1296b35 | |||
c8ba77b89b | |||
63dcad4c12 | |||
d6c9f95851 | |||
210c2290f0 | |||
e896fd84cf |
27 changed files with 397 additions and 119 deletions
31
.forgejo/workflows/deploy.yaml
Normal file
31
.forgejo/workflows/deploy.yaml
Normal file
|
@ -0,0 +1,31 @@
|
|||
name: Build and Deploy
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
if: github.repository != 'profectus-engine/Profectus' # Don't build placeholder mod on main repo
|
||||
runs-on: docker
|
||||
steps:
|
||||
- name: Setup RSync
|
||||
run: |
|
||||
apt-get update
|
||||
apt-get install -y rsync
|
||||
|
||||
- name: Checkout 🛎️
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
submodules: recursive
|
||||
|
||||
- name: Install and Build 🔧 # This example project is built using npm and outputs the result to the 'build' folder. Replace with the commands required to build your project, or remove this step entirely if your site is pre-built.
|
||||
run: |
|
||||
npm ci
|
||||
npm run build
|
||||
|
||||
- name: Deploy 🚀
|
||||
uses: https://github.com/JamesIves/github-pages-deploy-action@v4.2.5
|
||||
with:
|
||||
branch: pages # The branch the action should deploy to.
|
||||
folder: dist # The folder the action should deploy.
|
21
.forgejo/workflows/test.yaml
Normal file
21
.forgejo/workflows/test.yaml
Normal file
|
@ -0,0 +1,21 @@
|
|||
name: Run Tests
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
jobs:
|
||||
test:
|
||||
runs-on: docker
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
submodules: recursive
|
||||
- name: Use Node.js 16.x
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: 16.x
|
||||
- run: npm ci
|
||||
- run: npm run build --if-present
|
||||
- run: npm test
|
1
.github/workflows/deploy.yml
vendored
1
.github/workflows/deploy.yml
vendored
|
@ -3,6 +3,7 @@ on:
|
|||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
workflow_dispatch:
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
if: github.repository != 'profectus-engine/Profectus' # Don't build placeholder mod on main repo
|
||||
|
|
4
.github/workflows/test.yml
vendored
4
.github/workflows/test.yml
vendored
|
@ -1,11 +1,11 @@
|
|||
name: Build and Deploy
|
||||
name: Run Tests
|
||||
on:
|
||||
push:
|
||||
branches: [ main ]
|
||||
pull_request:
|
||||
branches: [ main ]
|
||||
jobs:
|
||||
build:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
|
|
|
@ -38,7 +38,7 @@
|
|||
target="_blank"
|
||||
>
|
||||
<span class="material-icons info-modal-discord">discord</span>
|
||||
The Paper Pilot Community
|
||||
Profectus & Friends
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
class="nan-modal-discord-link"
|
||||
>
|
||||
<span class="material-icons nan-modal-discord">discord</span>
|
||||
{{ discordName || "The Paper Pilot Community" }}
|
||||
{{ discordName || "Profectus & Friends" }}
|
||||
</a>
|
||||
</div>
|
||||
<br />
|
||||
|
|
|
@ -15,9 +15,7 @@
|
|||
<a :href="discordLink" target="_blank">{{ discordName }}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://discord.gg/yJ4fjnjU54" target="_blank"
|
||||
>The Paper Pilot Community</a
|
||||
>
|
||||
<a href="https://discord.gg/yJ4fjnjU54" target="_blank">Profectus & Friends</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://discord.gg/F3xveHV" target="_blank">The Modding Tree</a>
|
||||
|
@ -82,9 +80,7 @@
|
|||
<a :href="discordLink" target="_blank">{{ discordName }}</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://discord.gg/yJ4fjnjU54" target="_blank"
|
||||
>The Paper Pilot Community</a
|
||||
>
|
||||
<a href="https://discord.gg/yJ4fjnjU54" target="_blank">Profectus & Friends</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="https://discord.gg/F3xveHV" target="_blank">The Modding Tree</a>
|
||||
|
|
|
@ -9,6 +9,7 @@ import { Resource, displayResource } from "features/resources/resource";
|
|||
import type { GenericTree, GenericTreeNode, TreeNode, TreeNodeOptions } from "features/trees/tree";
|
||||
import { createTreeNode } from "features/trees/tree";
|
||||
import type { GenericFormula } from "game/formulas/types";
|
||||
import { BaseLayer } from "game/layers";
|
||||
import type { Modifier } from "game/modifiers";
|
||||
import type { Persistent } from "game/persistence";
|
||||
import { DefaultValue, persistent } from "game/persistence";
|
||||
|
@ -133,8 +134,8 @@ export function createResetButton<T extends ClickableOptions & ResetButtonOption
|
|||
{unref(resetButton.conversion.buyMax) ? "Next:" : "Req:"}{" "}
|
||||
{displayResource(
|
||||
resetButton.conversion.baseResource,
|
||||
!unref(resetButton.conversion.buyMax) ||
|
||||
Decimal.lt(unref(resetButton.conversion.actualGain), 1)
|
||||
!unref(resetButton.conversion.buyMax) &&
|
||||
Decimal.gte(unref(resetButton.conversion.actualGain), 1)
|
||||
? unref(resetButton.conversion.currentAt)
|
||||
: unref(resetButton.conversion.nextAt)
|
||||
)}{" "}
|
||||
|
@ -485,3 +486,22 @@ export function createFormulaPreview(
|
|||
return <>{formatSmall(formula.evaluate())}</>;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility function for getting a computed boolean for whether or not a given feature is currently rendered in the DOM.
|
||||
* Note it will have a true value even if the feature is off screen.
|
||||
* @param layer The layer the feature appears within
|
||||
* @param id The ID of the feature
|
||||
*/
|
||||
export function isRendered(layer: BaseLayer, id: string): ComputedRef<boolean>;
|
||||
/**
|
||||
* Utility function for getting a computed boolean for whether or not a given feature is currently rendered in the DOM.
|
||||
* Note it will have a true value even if the feature is off screen.
|
||||
* @param layer The layer the feature appears within
|
||||
* @param feature The feature that may be rendered
|
||||
*/
|
||||
export function isRendered(layer: BaseLayer, feature: { id: string }): ComputedRef<boolean>;
|
||||
export function isRendered(layer: BaseLayer, idOrFeature: string | { id: string }) {
|
||||
const id = typeof idOrFeature === "string" ? idOrFeature : idOrFeature.id;
|
||||
return computed(() => id in layer.nodes.value);
|
||||
}
|
||||
|
|
|
@ -37,7 +37,7 @@ const layer = createLayer(id, function (this: BaseLayer) {
|
|||
color,
|
||||
reset
|
||||
}));
|
||||
addTooltip(treeNode, {
|
||||
const tooltip = addTooltip(treeNode, {
|
||||
display: createResourceTooltip(points),
|
||||
pinnable: true
|
||||
});
|
||||
|
@ -58,6 +58,7 @@ const layer = createLayer(id, function (this: BaseLayer) {
|
|||
name,
|
||||
color,
|
||||
points,
|
||||
tooltip,
|
||||
display: jsx(() => (
|
||||
<>
|
||||
<MainDisplay resource={points} color={color} />
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import Node from "components/Node.vue";
|
||||
import Spacer from "components/layout/Spacer.vue";
|
||||
import { jsx } from "features/feature";
|
||||
import { createResource, trackBest, trackOOMPS, trackTotal } from "features/resources/resource";
|
||||
|
@ -48,19 +49,35 @@ export const main = createLayer("main", function (this: BaseLayer) {
|
|||
links: tree.links,
|
||||
display: jsx(() => (
|
||||
<>
|
||||
{player.devSpeed === 0 ? <div>Game Paused</div> : null}
|
||||
{player.devSpeed === 0 ? (
|
||||
<div>
|
||||
Game Paused
|
||||
<Node id="paused" />
|
||||
</div>
|
||||
) : null}
|
||||
{player.devSpeed != null && player.devSpeed !== 0 && player.devSpeed !== 1 ? (
|
||||
<div>Dev Speed: {format(player.devSpeed)}x</div>
|
||||
<div>
|
||||
Dev Speed: {format(player.devSpeed)}x
|
||||
<Node id="devspeed" />
|
||||
</div>
|
||||
) : null}
|
||||
{player.offlineTime != null && player.offlineTime !== 0 ? (
|
||||
<div>Offline Time: {formatTime(player.offlineTime)}</div>
|
||||
<div>
|
||||
Offline Time: {formatTime(player.offlineTime)}
|
||||
<Node id="offline" />
|
||||
</div>
|
||||
) : null}
|
||||
<div>
|
||||
{Decimal.lt(points.value, "1e1000") ? <span>You have </span> : null}
|
||||
<h2>{format(points.value)}</h2>
|
||||
{Decimal.lt(points.value, "1e1e6") ? <span> points</span> : null}
|
||||
</div>
|
||||
{Decimal.gt(pointGain.value, 0) ? <div>({oomps.value})</div> : null}
|
||||
{Decimal.gt(pointGain.value, 0) ? (
|
||||
<div>
|
||||
({oomps.value})
|
||||
<Node id="oomps" />
|
||||
</div>
|
||||
) : null}
|
||||
<Spacer />
|
||||
{render(tree)}
|
||||
</>
|
||||
|
|
|
@ -120,7 +120,7 @@ export default defineComponent({
|
|||
barStyle.clipPath = `inset(0% ${normalizedProgress.value}% 0% 0%)`;
|
||||
break;
|
||||
case Direction.Left:
|
||||
barStyle.clipPath = `inset(0% 0% 0% ${normalizedProgress.value} + '%)`;
|
||||
barStyle.clipPath = `inset(0% 0% 0% ${normalizedProgress.value}%)`;
|
||||
break;
|
||||
case Direction.Default:
|
||||
barStyle.clipPath = "inset(0% 50% 0% 0%)";
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<line
|
||||
class="link"
|
||||
v-bind="link"
|
||||
v-bind="linkProps"
|
||||
:class="{ pulsing: link.pulsing }"
|
||||
:x1="startPosition.x"
|
||||
:y1="startPosition.y"
|
||||
|
@ -12,6 +12,7 @@
|
|||
|
||||
<script setup lang="ts">
|
||||
import type { BoardNode, BoardNodeLink } from "features/boards/board";
|
||||
import { kebabifyObject } from "util/vue";
|
||||
import { computed, toRefs, unref } from "vue";
|
||||
|
||||
const _props = defineProps<{
|
||||
|
@ -49,11 +50,14 @@ const endPosition = computed(() => {
|
|||
}
|
||||
return position;
|
||||
});
|
||||
|
||||
const linkProps = computed(() => kebabifyObject(_props.link as unknown as Record<string, unknown>));
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.link {
|
||||
transition-duration: 0s;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.link.pulsing {
|
||||
|
|
|
@ -108,7 +108,7 @@ document.onkeydown = function (e) {
|
|||
if (e.ctrlKey) {
|
||||
key = "ctrl+" + key;
|
||||
}
|
||||
const hotkey = hotkeys[key];
|
||||
const hotkey = hotkeys[key] ?? hotkeys[key.toLowerCase()];
|
||||
if (hotkey && unref(hotkey.enabled)) {
|
||||
e.preventDefault();
|
||||
hotkey.onPress();
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<line
|
||||
stroke-width="15px"
|
||||
stroke="white"
|
||||
v-bind="link"
|
||||
v-bind="linkProps"
|
||||
:x1="startPosition.x"
|
||||
:y1="startPosition.y"
|
||||
:x2="endPosition.x"
|
||||
|
@ -13,6 +13,7 @@
|
|||
<script setup lang="ts">
|
||||
import type { Link } from "features/links/links";
|
||||
import type { FeatureNode } from "game/layers";
|
||||
import { kebabifyObject } from "util/vue";
|
||||
import { computed, toRefs } from "vue";
|
||||
|
||||
const _props = defineProps<{
|
||||
|
@ -54,4 +55,6 @@ const endPosition = computed(() => {
|
|||
}
|
||||
return position;
|
||||
});
|
||||
|
||||
const linkProps = computed(() => kebabifyObject(_props.link as unknown as Record<string, unknown>));
|
||||
</script>
|
||||
|
|
|
@ -95,18 +95,6 @@ export function addTooltip<T extends TooltipOptions>(
|
|||
}
|
||||
|
||||
nextTick(() => {
|
||||
if (options.pinnable) {
|
||||
if ("pinned" in element) {
|
||||
console.error(
|
||||
"Cannot add pinnable tooltip to element that already has a property called 'pinned'"
|
||||
);
|
||||
options.pinnable = false;
|
||||
deletePersistent(options.pinned as Persistent<boolean>);
|
||||
} else {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(element as any).pinned = options.pinned;
|
||||
}
|
||||
}
|
||||
const elementComponent = element[Component];
|
||||
element[Component] = TooltipComponent as GenericComponent;
|
||||
const elementGatherProps = element[GatherProps].bind(element);
|
||||
|
|
|
@ -224,7 +224,7 @@ export interface BaseTree {
|
|||
id: string;
|
||||
/** The link objects for each of the branches of the tree. */
|
||||
links: Ref<Link[]>;
|
||||
/** Cause a reset on this node and propagate it through the tree according to {@link resetPropagation}. */
|
||||
/** Cause a reset on this node and propagate it through the tree according to {@link TreeOptions.resetPropagation}. */
|
||||
reset: (node: GenericTreeNode) => void;
|
||||
/** A flag that is true while the reset is still propagating through the tree. */
|
||||
isResetting: Ref<boolean>;
|
||||
|
@ -338,34 +338,21 @@ export const branchedResetPropagation = function (
|
|||
tree: GenericTree,
|
||||
resettingNode: GenericTreeNode
|
||||
): void {
|
||||
const visitedNodes = [resettingNode];
|
||||
let currentNodes = [resettingNode];
|
||||
if (tree.branches != null) {
|
||||
const branches = unref(tree.branches);
|
||||
while (currentNodes.length > 0) {
|
||||
const nextNodes: GenericTreeNode[] = [];
|
||||
currentNodes.forEach(node => {
|
||||
branches
|
||||
.filter(branch => branch.startNode === node || branch.endNode === node)
|
||||
.map(branch => {
|
||||
if (branch.startNode === node) {
|
||||
return branch.endNode;
|
||||
}
|
||||
return branch.startNode;
|
||||
})
|
||||
.filter(node => !visitedNodes.includes(node))
|
||||
.forEach(node => {
|
||||
// Check here instead of in the filter because this check's results may
|
||||
// change as we go through each node
|
||||
if (!nextNodes.includes(node)) {
|
||||
nextNodes.push(node);
|
||||
node.reset?.reset();
|
||||
}
|
||||
});
|
||||
});
|
||||
currentNodes = nextNodes;
|
||||
visitedNodes.push(...currentNodes);
|
||||
}
|
||||
const links = unref(tree.branches);
|
||||
if (links == null) return;
|
||||
const reset: GenericTreeNode[] = [];
|
||||
let current = [resettingNode];
|
||||
while (current.length != 0) {
|
||||
const next: GenericTreeNode[] = [];
|
||||
for (const node of current) {
|
||||
for (const link of links.filter(link => link.startNode === node)) {
|
||||
if ([...reset, ...current].includes(link.endNode)) continue
|
||||
next.push(link.endNode);
|
||||
link.endNode.reset?.reset();
|
||||
}
|
||||
};
|
||||
reset.push(...current);
|
||||
current = next;
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -345,19 +345,35 @@ export abstract class InternalFormula<T extends [FormulaSource] | FormulaSource[
|
|||
public static sgn = InternalFormula.sign;
|
||||
|
||||
public static round(value: FormulaSource) {
|
||||
return new Formula({ inputs: [value], evaluate: Decimal.round });
|
||||
return new Formula({
|
||||
inputs: [value],
|
||||
evaluate: Decimal.round,
|
||||
invert: ops.invertPassthrough
|
||||
});
|
||||
}
|
||||
|
||||
public static floor(value: FormulaSource) {
|
||||
return new Formula({ inputs: [value], evaluate: Decimal.floor });
|
||||
return new Formula({
|
||||
inputs: [value],
|
||||
evaluate: Decimal.floor,
|
||||
invert: ops.invertPassthrough
|
||||
});
|
||||
}
|
||||
|
||||
public static ceil(value: FormulaSource) {
|
||||
return new Formula({ inputs: [value], evaluate: Decimal.ceil });
|
||||
return new Formula({
|
||||
inputs: [value],
|
||||
evaluate: Decimal.ceil,
|
||||
invert: ops.invertPassthrough
|
||||
});
|
||||
}
|
||||
|
||||
public static trunc(value: FormulaSource) {
|
||||
return new Formula({ inputs: [value], evaluate: Decimal.trunc });
|
||||
return new Formula({
|
||||
inputs: [value],
|
||||
evaluate: Decimal.trunc,
|
||||
invert: ops.invertPassthrough
|
||||
});
|
||||
}
|
||||
|
||||
public static add<T extends GenericFormula>(value: T, other: FormulaSource): T;
|
||||
|
@ -459,7 +475,7 @@ export abstract class InternalFormula<T extends [FormulaSource] | FormulaSource[
|
|||
return new Formula({
|
||||
inputs: [value, min, max],
|
||||
evaluate: Decimal.clamp,
|
||||
invert: ops.passthrough as InvertFunction<[FormulaSource, FormulaSource, FormulaSource]>
|
||||
invert: ops.invertPassthrough
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,12 @@
|
|||
import Decimal, { DecimalSource } from "util/bignum";
|
||||
import Formula, { hasVariable, unrefFormulaSource } from "./formulas";
|
||||
import { FormulaSource, GenericFormula, InvertFunction, SubstitutionStack } from "./types";
|
||||
import {
|
||||
FormulaSource,
|
||||
GenericFormula,
|
||||
InvertFunction,
|
||||
InvertibleFormula,
|
||||
SubstitutionStack
|
||||
} from "./types";
|
||||
|
||||
const ln10 = Decimal.ln(10);
|
||||
|
||||
|
@ -8,6 +14,15 @@ export function passthrough<T extends GenericFormula | DecimalSource>(value: T):
|
|||
return value;
|
||||
}
|
||||
|
||||
export function invertPassthrough(value: DecimalSource, ...inputs: FormulaSource[]) {
|
||||
const variable = inputs.find(input => hasVariable(input)) as InvertibleFormula | undefined;
|
||||
if (variable == null) {
|
||||
console.error("Could not invert due to no input being a variable");
|
||||
return 0;
|
||||
}
|
||||
return variable.invert(value);
|
||||
}
|
||||
|
||||
export function invertNeg(value: DecimalSource, lhs: FormulaSource) {
|
||||
if (hasVariable(lhs)) {
|
||||
return lhs.invert(Decimal.neg(value));
|
||||
|
|
|
@ -4,7 +4,7 @@ import { jsx } from "features/feature";
|
|||
import settings from "game/settings";
|
||||
import type { DecimalSource } from "util/bignum";
|
||||
import Decimal, { formatSmall } from "util/bignum";
|
||||
import type { WithRequired } from "util/common";
|
||||
import type { RequiredKeys, WithRequired } from "util/common";
|
||||
import type { Computable, ProcessedComputable } from "util/computed";
|
||||
import { convertComputable } from "util/computed";
|
||||
import { createLazyProxy } from "util/proxies";
|
||||
|
@ -38,16 +38,11 @@ export interface Modifier {
|
|||
description?: ProcessedComputable<CoercableComponent>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility type used to narrow down a modifier type that will have a description and/or enabled property based on optional parameters, T and S (respectively).
|
||||
*/
|
||||
export type ModifierFromOptionalParams<T, S> = undefined extends T
|
||||
? undefined extends S
|
||||
? Omit<WithRequired<Modifier, "invert" | "getFormula">, "description" | "enabled">
|
||||
: Omit<WithRequired<Modifier, "invert" | "enabled" | "getFormula">, "description">
|
||||
: undefined extends S
|
||||
? Omit<WithRequired<Modifier, "invert" | "description" | "getFormula">, "enabled">
|
||||
: WithRequired<Modifier, "invert" | "enabled" | "description" | "getFormula">;
|
||||
/** Utility type that represents the output of all modifiers that represent a single operation. */
|
||||
export type OperationModifier<T> = WithRequired<
|
||||
Modifier,
|
||||
"invert" | "getFormula" | Extract<RequiredKeys<T>, keyof Modifier>
|
||||
>;
|
||||
|
||||
/** An object that configures an additive modifier via {@link createAdditiveModifier}. */
|
||||
export interface AdditiveModifierOptions {
|
||||
|
@ -65,9 +60,9 @@ export interface AdditiveModifierOptions {
|
|||
* Create a modifier that adds some value to the input value.
|
||||
* @param optionsFunc Additive modifier options.
|
||||
*/
|
||||
export function createAdditiveModifier<T extends AdditiveModifierOptions>(
|
||||
export function createAdditiveModifier<T extends AdditiveModifierOptions, S = OperationModifier<T>>(
|
||||
optionsFunc: OptionsFunc<T>
|
||||
): ModifierFromOptionalParams<T["description"], T["enabled"]> {
|
||||
) {
|
||||
return createLazyProxy(feature => {
|
||||
const { addend, description, enabled, smallerIsBetter } = optionsFunc.call(
|
||||
feature,
|
||||
|
@ -111,7 +106,7 @@ export function createAdditiveModifier<T extends AdditiveModifierOptions>(
|
|||
</div>
|
||||
))
|
||||
};
|
||||
}) as unknown as ModifierFromOptionalParams<T["description"], T["enabled"]>;
|
||||
}) as S;
|
||||
}
|
||||
|
||||
/** An object that configures an multiplicative modifier via {@link createMultiplicativeModifier}. */
|
||||
|
@ -130,9 +125,10 @@ export interface MultiplicativeModifierOptions {
|
|||
* Create a modifier that multiplies the input value by some value.
|
||||
* @param optionsFunc Multiplicative modifier options.
|
||||
*/
|
||||
export function createMultiplicativeModifier<T extends MultiplicativeModifierOptions>(
|
||||
optionsFunc: OptionsFunc<T>
|
||||
): ModifierFromOptionalParams<T["description"], T["enabled"]> {
|
||||
export function createMultiplicativeModifier<
|
||||
T extends MultiplicativeModifierOptions,
|
||||
S = OperationModifier<T>
|
||||
>(optionsFunc: OptionsFunc<T>) {
|
||||
return createLazyProxy(feature => {
|
||||
const { multiplier, description, enabled, smallerIsBetter } = optionsFunc.call(
|
||||
feature,
|
||||
|
@ -175,7 +171,7 @@ export function createMultiplicativeModifier<T extends MultiplicativeModifierOpt
|
|||
</div>
|
||||
))
|
||||
};
|
||||
}) as unknown as ModifierFromOptionalParams<T["description"], T["enabled"]>;
|
||||
}) as S;
|
||||
}
|
||||
|
||||
/** An object that configures an exponential modifier via {@link createExponentialModifier}. */
|
||||
|
@ -196,9 +192,10 @@ export interface ExponentialModifierOptions {
|
|||
* Create a modifier that raises the input value to the power of some value.
|
||||
* @param optionsFunc Exponential modifier options.
|
||||
*/
|
||||
export function createExponentialModifier<T extends ExponentialModifierOptions>(
|
||||
optionsFunc: OptionsFunc<T>
|
||||
): ModifierFromOptionalParams<T["description"], T["enabled"]> {
|
||||
export function createExponentialModifier<
|
||||
T extends ExponentialModifierOptions,
|
||||
S = OperationModifier<T>
|
||||
>(optionsFunc: OptionsFunc<T>) {
|
||||
return createLazyProxy(feature => {
|
||||
const { exponent, description, enabled, supportLowNumbers, smallerIsBetter } =
|
||||
optionsFunc.call(feature, feature);
|
||||
|
@ -263,7 +260,7 @@ export function createExponentialModifier<T extends ExponentialModifierOptions>(
|
|||
</div>
|
||||
))
|
||||
};
|
||||
}) as unknown as ModifierFromOptionalParams<T["description"], T["enabled"]>;
|
||||
}) as S;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -274,11 +271,9 @@ export function createExponentialModifier<T extends ExponentialModifierOptions>(
|
|||
* @see {@link createModifierSection}.
|
||||
*/
|
||||
export function createSequentialModifier<
|
||||
T extends Modifier[],
|
||||
S = T extends WithRequired<Modifier, "invert">[]
|
||||
? WithRequired<Modifier, "description" | "invert">
|
||||
: Omit<WithRequired<Modifier, "description">, "invert">
|
||||
>(modifiersFunc: () => T): S {
|
||||
T extends Modifier,
|
||||
S = WithRequired<Modifier, Extract<RequiredKeys<T>, keyof Modifier>>
|
||||
>(modifiersFunc: () => T[]) {
|
||||
return createLazyProxy(() => {
|
||||
const modifiers = modifiersFunc();
|
||||
|
||||
|
@ -296,10 +291,14 @@ export function createSequentialModifier<
|
|||
: undefined,
|
||||
getFormula: modifiers.every(m => m.getFormula != null)
|
||||
? (gain: FormulaSource) =>
|
||||
modifiers
|
||||
.filter(m => unref(m.enabled) !== false)
|
||||
modifiers.reduce((acc, curr) => {
|
||||
if (curr.enabled == null || curr.enabled === true) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
return curr.getFormula!(acc);
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
.reduce((acc, curr) => curr.getFormula!(acc), gain)
|
||||
return Formula.if(acc, curr.enabled, acc => curr.getFormula!(acc));
|
||||
}, gain)
|
||||
: undefined,
|
||||
enabled: modifiers.some(m => m.enabled != null)
|
||||
? computed(() => modifiers.filter(m => unref(m.enabled) !== false).length > 0)
|
||||
|
@ -317,7 +316,7 @@ export function createSequentialModifier<
|
|||
))
|
||||
: undefined
|
||||
};
|
||||
}) as unknown as S;
|
||||
}) as S;
|
||||
}
|
||||
|
||||
/** An object that configures a modifier section via {@link createModifierSection}. */
|
||||
|
|
|
@ -222,7 +222,7 @@ export function createCostRequirement<T extends CostRequirementOptions>(
|
|||
Decimal.gte(
|
||||
req.resource.value,
|
||||
unref(req.cost as ProcessedComputable<DecimalSource>)
|
||||
)
|
||||
) ? 1 : 0
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,11 @@
|
|||
export type RequiredKeys<T> = {
|
||||
[K in keyof T]-?: NonNullable<unknown> extends Pick<T, K> ? never : K;
|
||||
}[keyof T];
|
||||
export type OptionalKeys<T> = {
|
||||
[K in keyof T]-?: NonNullable<unknown> extends Pick<T, K> ? K : never;
|
||||
}[keyof T];
|
||||
|
||||
export type OmitOptional<T> = Pick<T, RequiredKeys<T>>;
|
||||
export type WithRequired<T, K extends keyof T> = T & { [P in K]-?: T[P] };
|
||||
|
||||
export type ArrayElements<T extends ReadonlyArray<unknown>> = T extends ReadonlyArray<infer S>
|
||||
|
@ -12,6 +20,11 @@ export function camelToTitle(camel: string): string {
|
|||
return title;
|
||||
}
|
||||
|
||||
export function camelToKebab(camel: string) {
|
||||
// Split off first character so function works on upper camel (pascal) case
|
||||
return (camel[0] + camel.slice(1).replace(/[A-Z]/g, c => `-${c}`)).toLowerCase();
|
||||
}
|
||||
|
||||
export function isFunction<T, S extends ReadonlyArray<unknown>, R>(
|
||||
functionOrValue: ((...args: S) => T) | R
|
||||
): functionOrValue is (...args: S) => T {
|
||||
|
|
|
@ -21,6 +21,7 @@ import {
|
|||
unref,
|
||||
watchEffect
|
||||
} from "vue";
|
||||
import { camelToKebab } from "./common";
|
||||
|
||||
export function coerceComponent(
|
||||
component: CoercableComponent,
|
||||
|
@ -241,3 +242,10 @@ export function trackHover(element: VueFeature): Ref<boolean> {
|
|||
|
||||
return isHovered;
|
||||
}
|
||||
|
||||
export function kebabifyObject(object: Record<string, unknown>) {
|
||||
return Object.keys(object).reduce((acc, curr) => {
|
||||
acc[camelToKebab(curr)] = object[curr];
|
||||
return acc;
|
||||
}, {} as Record<string, unknown>);
|
||||
}
|
||||
|
|
|
@ -47,6 +47,10 @@ describe("Creating conversion", () => {
|
|||
baseResource.value = Decimal.pow(100, 2).times(10).add(1);
|
||||
expect(unref(conversion.currentGain)).compare_tolerance(100);
|
||||
});
|
||||
test("Zero", () => {
|
||||
baseResource.value = Decimal.dZero;
|
||||
expect(unref(conversion.currentGain)).compare_tolerance(0);
|
||||
});
|
||||
});
|
||||
describe("Calculates actualGain correctly", () => {
|
||||
let conversion: GenericConversion;
|
||||
|
@ -69,6 +73,10 @@ describe("Creating conversion", () => {
|
|||
baseResource.value = Decimal.pow(100, 2).times(10).add(1);
|
||||
expect(unref(conversion.actualGain)).compare_tolerance(100);
|
||||
});
|
||||
test("Zero", () => {
|
||||
baseResource.value = Decimal.dZero;
|
||||
expect(unref(conversion.actualGain)).compare_tolerance(0);
|
||||
});
|
||||
});
|
||||
describe("Calculates currentAt correctly", () => {
|
||||
let conversion: GenericConversion;
|
||||
|
@ -95,6 +103,10 @@ describe("Creating conversion", () => {
|
|||
Decimal.pow(100, 2).times(10)
|
||||
);
|
||||
});
|
||||
test("Zero", () => {
|
||||
baseResource.value = Decimal.dZero;
|
||||
expect(unref(conversion.currentAt)).compare_tolerance(0);
|
||||
});
|
||||
});
|
||||
describe("Calculates nextAt correctly", () => {
|
||||
let conversion: GenericConversion;
|
||||
|
@ -117,6 +129,10 @@ describe("Creating conversion", () => {
|
|||
baseResource.value = Decimal.pow(100, 2).times(10).add(1);
|
||||
expect(unref(conversion.nextAt)).compare_tolerance(Decimal.pow(101, 2).times(10));
|
||||
});
|
||||
test("Zero", () => {
|
||||
baseResource.value = Decimal.dZero;
|
||||
expect(unref(conversion.nextAt)).compare_tolerance(Decimal.dTen);
|
||||
});
|
||||
});
|
||||
test("Converts correctly", () => {
|
||||
const conversion = createCumulativeConversion(() => ({
|
||||
|
@ -193,6 +209,10 @@ describe("Creating conversion", () => {
|
|||
baseResource.value = Decimal.pow(100, 2).times(10).add(1);
|
||||
expect(unref(conversion.currentGain)).compare_tolerance(100);
|
||||
});
|
||||
test("Zero", () => {
|
||||
baseResource.value = Decimal.dZero;
|
||||
expect(unref(conversion.currentGain)).compare_tolerance(1);
|
||||
});
|
||||
});
|
||||
describe("Calculates actualGain correctly", () => {
|
||||
let conversion: GenericConversion;
|
||||
|
@ -216,6 +236,10 @@ describe("Creating conversion", () => {
|
|||
baseResource.value = Decimal.pow(100, 2).times(10).add(1);
|
||||
expect(unref(conversion.actualGain)).compare_tolerance(99);
|
||||
});
|
||||
test("Zero", () => {
|
||||
baseResource.value = Decimal.dZero;
|
||||
expect(unref(conversion.actualGain)).compare_tolerance(0);
|
||||
});
|
||||
});
|
||||
describe("Calculates currentAt correctly", () => {
|
||||
let conversion: GenericConversion;
|
||||
|
@ -243,6 +267,10 @@ describe("Creating conversion", () => {
|
|||
Decimal.pow(100, 2).times(10)
|
||||
);
|
||||
});
|
||||
test("Zero", () => {
|
||||
baseResource.value = Decimal.dZero;
|
||||
expect(unref(conversion.currentAt)).compare_tolerance(Decimal.pow(1, 2).times(10));
|
||||
});
|
||||
});
|
||||
describe("Calculates nextAt correctly", () => {
|
||||
let conversion: GenericConversion;
|
||||
|
@ -266,6 +294,10 @@ describe("Creating conversion", () => {
|
|||
baseResource.value = Decimal.pow(100, 2).times(10).add(1);
|
||||
expect(unref(conversion.nextAt)).compare_tolerance(Decimal.pow(101, 2).times(10));
|
||||
});
|
||||
test("Zero", () => {
|
||||
baseResource.value = Decimal.dZero;
|
||||
expect(unref(conversion.nextAt)).compare_tolerance(Decimal.pow(2, 2).times(10));
|
||||
});
|
||||
});
|
||||
test("Converts correctly", () => {
|
||||
const conversion = createIndependentConversion(() => ({
|
||||
|
|
111
tests/features/tree.test.ts
Normal file
111
tests/features/tree.test.ts
Normal file
|
@ -0,0 +1,111 @@
|
|||
import { beforeAll, beforeEach, describe, expect, test, vi } from "vitest";
|
||||
import { Ref, ref } from "vue";
|
||||
import "../utils";
|
||||
import {
|
||||
createTree,
|
||||
createTreeNode,
|
||||
defaultResetPropagation,
|
||||
invertedResetPropagation,
|
||||
branchedResetPropagation
|
||||
} from "features/trees/tree";
|
||||
import { createReset, GenericReset } from "features/reset";
|
||||
|
||||
describe("Reset propagation", () => {
|
||||
let shouldReset: Ref<boolean>, shouldNotReset: Ref<boolean>;
|
||||
let goodReset: GenericReset, badReset: GenericReset;
|
||||
beforeAll(() => {
|
||||
shouldReset = ref(false);
|
||||
shouldNotReset = ref(false);
|
||||
goodReset = createReset(() => ({
|
||||
thingsToReset: [],
|
||||
onReset() {
|
||||
shouldReset.value = true;
|
||||
}
|
||||
}));
|
||||
badReset = createReset(() => ({
|
||||
thingsToReset: [],
|
||||
onReset() {
|
||||
shouldNotReset.value = true;
|
||||
}
|
||||
}));
|
||||
});
|
||||
beforeEach(() => {
|
||||
shouldReset.value = false;
|
||||
shouldNotReset.value = false;
|
||||
});
|
||||
test("No resets", () => {
|
||||
expect(() => {
|
||||
const a = createTreeNode(() => ({}));
|
||||
const b = createTreeNode(() => ({}));
|
||||
const c = createTreeNode(() => ({}));
|
||||
const tree = createTree(() => ({
|
||||
nodes: [[a], [b], [c]]
|
||||
}));
|
||||
tree.reset(a);
|
||||
}).not.toThrowError();
|
||||
});
|
||||
|
||||
test("Do not propagate resets", () => {
|
||||
const a = createTreeNode(() => ({ reset: badReset }));
|
||||
const b = createTreeNode(() => ({ reset: badReset }));
|
||||
const c = createTreeNode(() => ({ reset: badReset }));
|
||||
const tree = createTree(() => ({
|
||||
nodes: [[a], [b], [c]]
|
||||
}));
|
||||
tree.reset(b);
|
||||
expect(shouldNotReset.value).toBe(false);
|
||||
});
|
||||
|
||||
test("Default propagation", () => {
|
||||
const a = createTreeNode(() => ({ reset: goodReset }));
|
||||
const b = createTreeNode(() => ({}));
|
||||
const c = createTreeNode(() => ({ reset: badReset }));
|
||||
const tree = createTree(() => ({
|
||||
nodes: [[a], [b], [c]],
|
||||
resetPropagation: defaultResetPropagation
|
||||
}));
|
||||
tree.reset(b);
|
||||
expect(shouldReset.value).toBe(true);
|
||||
expect(shouldNotReset.value).toBe(false);
|
||||
});
|
||||
|
||||
test("Inverted propagation", () => {
|
||||
const a = createTreeNode(() => ({ reset: badReset }));
|
||||
const b = createTreeNode(() => ({}));
|
||||
const c = createTreeNode(() => ({ reset: goodReset }));
|
||||
const tree = createTree(() => ({
|
||||
nodes: [[a], [b], [c]],
|
||||
resetPropagation: invertedResetPropagation
|
||||
}));
|
||||
tree.reset(b);
|
||||
expect(shouldReset.value).toBe(true);
|
||||
expect(shouldNotReset.value).toBe(false);
|
||||
});
|
||||
|
||||
test("Branched propagation", () => {
|
||||
const a = createTreeNode(() => ({ reset: badReset }));
|
||||
const b = createTreeNode(() => ({}));
|
||||
const c = createTreeNode(() => ({ reset: goodReset }));
|
||||
const tree = createTree(() => ({
|
||||
nodes: [[a, b, c]],
|
||||
resetPropagation: branchedResetPropagation,
|
||||
branches: [{ startNode: b, endNode: c }]
|
||||
}));
|
||||
tree.reset(b);
|
||||
expect(shouldReset.value).toBe(true);
|
||||
expect(shouldNotReset.value).toBe(false);
|
||||
});
|
||||
|
||||
test("Branched propagation not bi-directional", () => {
|
||||
const a = createTreeNode(() => ({ reset: badReset }));
|
||||
const b = createTreeNode(() => ({}));
|
||||
const c = createTreeNode(() => ({ reset: badReset }));
|
||||
const tree = createTree(() => ({
|
||||
nodes: [[a, b, c]],
|
||||
resetPropagation: branchedResetPropagation,
|
||||
branches: [{ startNode: c, endNode: b }]
|
||||
}));
|
||||
tree.reset(b);
|
||||
expect(shouldNotReset.value).toBe(false);
|
||||
});
|
||||
});
|
|
@ -13,9 +13,13 @@ import { InvertibleIntegralFormula } from "game/formulas/types";
|
|||
|
||||
type FormulaFunctions = keyof GenericFormula & keyof typeof Formula & keyof typeof Decimal;
|
||||
|
||||
const testValues = [-1, "0", Decimal.dOne] as const;
|
||||
const testValues = [-2, "0", new Decimal(10.5)] as const;
|
||||
|
||||
const invertibleZeroParamFunctionNames = [
|
||||
"round",
|
||||
"floor",
|
||||
"ceil",
|
||||
"trunc",
|
||||
"neg",
|
||||
"recip",
|
||||
"log10",
|
||||
|
@ -48,10 +52,6 @@ const invertibleZeroParamFunctionNames = [
|
|||
const nonInvertibleZeroParamFunctionNames = [
|
||||
"abs",
|
||||
"sign",
|
||||
"round",
|
||||
"floor",
|
||||
"ceil",
|
||||
"trunc",
|
||||
"pLog10",
|
||||
"absLog10",
|
||||
"factorial",
|
||||
|
@ -85,6 +85,10 @@ const integrableZeroParamFunctionNames = [
|
|||
] as const;
|
||||
const nonIntegrableZeroParamFunctionNames = [
|
||||
...nonInvertibleZeroParamFunctionNames,
|
||||
"round",
|
||||
"floor",
|
||||
"ceil",
|
||||
"trunc",
|
||||
"lambertw",
|
||||
"ssqrt"
|
||||
] as const;
|
||||
|
@ -151,7 +155,7 @@ describe("Formula Equality Checking", () => {
|
|||
describe("Formula aliases", () => {
|
||||
function testAliases<T extends FormulaFunctions>(
|
||||
aliases: T[],
|
||||
args: Parameters<(typeof Formula)[T]>
|
||||
args: Parameters<typeof Formula[T]>
|
||||
) {
|
||||
describe(aliases[0], () => {
|
||||
let formula: GenericFormula;
|
||||
|
@ -246,7 +250,7 @@ describe("Creating Formulas", () => {
|
|||
|
||||
function checkFormula<T extends FormulaFunctions>(
|
||||
functionName: T,
|
||||
args: Readonly<Parameters<(typeof Formula)[T]>>
|
||||
args: Readonly<Parameters<typeof Formula[T]>>
|
||||
) {
|
||||
let formula: GenericFormula;
|
||||
beforeAll(() => {
|
||||
|
@ -270,7 +274,7 @@ describe("Creating Formulas", () => {
|
|||
// It's a lot of tests, but I'd rather be exhaustive
|
||||
function testFormulaCall<T extends FormulaFunctions>(
|
||||
functionName: T,
|
||||
args: Readonly<Parameters<(typeof Formula)[T]>>
|
||||
args: Readonly<Parameters<typeof Formula[T]>>
|
||||
) {
|
||||
if ((functionName === "slog" || functionName === "layeradd") && args[0] === -1) {
|
||||
// These cases in particular take a long time, so skip them
|
||||
|
@ -488,18 +492,18 @@ describe("Inverting", () => {
|
|||
});
|
||||
|
||||
test("Inverting nested formulas", () => {
|
||||
const formula = Formula.add(variable, constant).times(constant);
|
||||
const formula = Formula.add(variable, constant).times(constant).floor();
|
||||
expect(formula.invert(100)).compare_tolerance(0);
|
||||
});
|
||||
|
||||
describe("Inverting with non-invertible sections", () => {
|
||||
test("Non-invertible constant", () => {
|
||||
const formula = Formula.add(variable, constant.ceil());
|
||||
const formula = Formula.add(variable, constant.sign());
|
||||
expect(formula.isInvertible()).toBe(true);
|
||||
expect(() => formula.invert(10)).not.toLogError();
|
||||
});
|
||||
test("Non-invertible variable", () => {
|
||||
const formula = Formula.add(variable.ceil(), constant);
|
||||
const formula = Formula.add(variable.sign(), constant);
|
||||
expect(formula.isInvertible()).toBe(false);
|
||||
expect(() => formula.invert(10)).toLogError();
|
||||
});
|
||||
|
|
|
@ -133,14 +133,14 @@ describe("Exponential Modifiers", () =>
|
|||
testModifiers(createExponentialModifier, "exponent", Decimal.pow));
|
||||
|
||||
describe("Sequential Modifiers", () => {
|
||||
function createModifier(
|
||||
function createModifier<T extends Partial<ModifierConstructorOptions>>(
|
||||
value: Computable<DecimalSource>,
|
||||
options: Partial<ModifierConstructorOptions> = {}
|
||||
): WithRequired<Modifier, "invert" | "getFormula"> {
|
||||
options?: T
|
||||
) {
|
||||
return createSequentialModifier(() => [
|
||||
createAdditiveModifier(() => ({ ...options, addend: value })),
|
||||
createMultiplicativeModifier(() => ({ ...options, multiplier: value })),
|
||||
createExponentialModifier(() => ({ ...options, exponent: value }))
|
||||
createAdditiveModifier(() => ({ ...(options ?? {}), addend: value })),
|
||||
createMultiplicativeModifier(() => ({ ...(options ?? {}), multiplier: value })),
|
||||
createExponentialModifier(() => ({ ...(options ?? {}), exponent: value }))
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -199,6 +199,17 @@ describe("Sequential Modifiers", () => {
|
|||
// So long as one is true or undefined, enable should be true
|
||||
expect(unref(modifier.enabled)).toBe(true);
|
||||
});
|
||||
test("respects enabled", () => {
|
||||
const value = ref(10);
|
||||
const enabled = ref(false);
|
||||
const modifier = createSequentialModifier(() => [
|
||||
createMultiplicativeModifier(() => ({ multiplier: 5, enabled }))
|
||||
]);
|
||||
const formula = modifier.getFormula(Formula.variable(value));
|
||||
expect(formula.evaluate()).compare_tolerance(value.value);
|
||||
enabled.value = true;
|
||||
expect(formula.evaluate()).not.compare_tolerance(value.value);
|
||||
});
|
||||
});
|
||||
|
||||
describe("applies smallerIsBetter correctly", () => {
|
||||
|
|
|
@ -83,7 +83,7 @@ describe("Creating cost requirement", () => {
|
|||
cost: 10,
|
||||
cumulativeCost: false
|
||||
}));
|
||||
expect(unref(requirement.requirementMet)).toBe(true);
|
||||
expect(unref(requirement.requirementMet)).toBe(1);
|
||||
});
|
||||
|
||||
test("Requirement not met when not meeting the cost", () => {
|
||||
|
@ -92,7 +92,7 @@ describe("Creating cost requirement", () => {
|
|||
cost: 100,
|
||||
cumulativeCost: false
|
||||
}));
|
||||
expect(unref(requirement.requirementMet)).toBe(false);
|
||||
expect(unref(requirement.requirementMet)).toBe(0);
|
||||
});
|
||||
|
||||
describe("canMaximize works correctly", () => {
|
||||
|
|
Loading…
Reference in a new issue