Compare commits

..

1 commit

Author SHA1 Message Date
ducdat0507
46306ee0e7
Fix repeatables buying more than intended when cumulativeCost is false 2023-05-19 17:12:53 +07:00
27 changed files with 122 additions and 398 deletions

View file

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

View file

@ -1,21 +0,0 @@
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

View file

@ -3,7 +3,6 @@ on:
push: push:
branches: branches:
- 'main' - 'main'
workflow_dispatch:
jobs: jobs:
build-and-deploy: build-and-deploy:
if: github.repository != 'profectus-engine/Profectus' # Don't build placeholder mod on main repo if: github.repository != 'profectus-engine/Profectus' # Don't build placeholder mod on main repo

View file

@ -1,11 +1,11 @@
name: Run Tests name: Build and Deploy
on: on:
push: push:
branches: [ main ] branches: [ main ]
pull_request: pull_request:
branches: [ main ] branches: [ main ]
jobs: jobs:
test: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:

View file

@ -38,7 +38,7 @@
target="_blank" target="_blank"
> >
<span class="material-icons info-modal-discord">discord</span> <span class="material-icons info-modal-discord">discord</span>
Profectus & Friends The Paper Pilot Community
</a> </a>
</div> </div>
<div> <div>

View file

@ -19,7 +19,7 @@
class="nan-modal-discord-link" class="nan-modal-discord-link"
> >
<span class="material-icons nan-modal-discord">discord</span> <span class="material-icons nan-modal-discord">discord</span>
{{ discordName || "Profectus & Friends" }} {{ discordName || "The Paper Pilot Community" }}
</a> </a>
</div> </div>
<br /> <br />

View file

@ -15,7 +15,9 @@
<a :href="discordLink" target="_blank">{{ discordName }}</a> <a :href="discordLink" target="_blank">{{ discordName }}</a>
</li> </li>
<li> <li>
<a href="https://discord.gg/yJ4fjnjU54" target="_blank">Profectus & Friends</a> <a href="https://discord.gg/yJ4fjnjU54" target="_blank"
>The Paper Pilot Community</a
>
</li> </li>
<li> <li>
<a href="https://discord.gg/F3xveHV" target="_blank">The Modding Tree</a> <a href="https://discord.gg/F3xveHV" target="_blank">The Modding Tree</a>
@ -80,7 +82,9 @@
<a :href="discordLink" target="_blank">{{ discordName }}</a> <a :href="discordLink" target="_blank">{{ discordName }}</a>
</li> </li>
<li> <li>
<a href="https://discord.gg/yJ4fjnjU54" target="_blank">Profectus & Friends</a> <a href="https://discord.gg/yJ4fjnjU54" target="_blank"
>The Paper Pilot Community</a
>
</li> </li>
<li> <li>
<a href="https://discord.gg/F3xveHV" target="_blank">The Modding Tree</a> <a href="https://discord.gg/F3xveHV" target="_blank">The Modding Tree</a>

View file

@ -9,7 +9,6 @@ import { Resource, displayResource } from "features/resources/resource";
import type { GenericTree, GenericTreeNode, TreeNode, TreeNodeOptions } from "features/trees/tree"; import type { GenericTree, GenericTreeNode, TreeNode, TreeNodeOptions } from "features/trees/tree";
import { createTreeNode } from "features/trees/tree"; import { createTreeNode } from "features/trees/tree";
import type { GenericFormula } from "game/formulas/types"; import type { GenericFormula } from "game/formulas/types";
import { BaseLayer } from "game/layers";
import type { Modifier } from "game/modifiers"; import type { Modifier } from "game/modifiers";
import type { Persistent } from "game/persistence"; import type { Persistent } from "game/persistence";
import { DefaultValue, persistent } from "game/persistence"; import { DefaultValue, persistent } from "game/persistence";
@ -134,8 +133,8 @@ export function createResetButton<T extends ClickableOptions & ResetButtonOption
{unref(resetButton.conversion.buyMax) ? "Next:" : "Req:"}{" "} {unref(resetButton.conversion.buyMax) ? "Next:" : "Req:"}{" "}
{displayResource( {displayResource(
resetButton.conversion.baseResource, resetButton.conversion.baseResource,
!unref(resetButton.conversion.buyMax) && !unref(resetButton.conversion.buyMax) ||
Decimal.gte(unref(resetButton.conversion.actualGain), 1) Decimal.lt(unref(resetButton.conversion.actualGain), 1)
? unref(resetButton.conversion.currentAt) ? unref(resetButton.conversion.currentAt)
: unref(resetButton.conversion.nextAt) : unref(resetButton.conversion.nextAt)
)}{" "} )}{" "}
@ -486,22 +485,3 @@ export function createFormulaPreview(
return <>{formatSmall(formula.evaluate())}</>; 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);
}

View file

@ -37,7 +37,7 @@ const layer = createLayer(id, function (this: BaseLayer) {
color, color,
reset reset
})); }));
const tooltip = addTooltip(treeNode, { addTooltip(treeNode, {
display: createResourceTooltip(points), display: createResourceTooltip(points),
pinnable: true pinnable: true
}); });
@ -58,7 +58,6 @@ const layer = createLayer(id, function (this: BaseLayer) {
name, name,
color, color,
points, points,
tooltip,
display: jsx(() => ( display: jsx(() => (
<> <>
<MainDisplay resource={points} color={color} /> <MainDisplay resource={points} color={color} />

View file

@ -1,4 +1,3 @@
import Node from "components/Node.vue";
import Spacer from "components/layout/Spacer.vue"; import Spacer from "components/layout/Spacer.vue";
import { jsx } from "features/feature"; import { jsx } from "features/feature";
import { createResource, trackBest, trackOOMPS, trackTotal } from "features/resources/resource"; import { createResource, trackBest, trackOOMPS, trackTotal } from "features/resources/resource";
@ -49,35 +48,19 @@ export const main = createLayer("main", function (this: BaseLayer) {
links: tree.links, links: tree.links,
display: jsx(() => ( display: jsx(() => (
<> <>
{player.devSpeed === 0 ? ( {player.devSpeed === 0 ? <div>Game Paused</div> : null}
<div>
Game Paused
<Node id="paused" />
</div>
) : null}
{player.devSpeed != null && player.devSpeed !== 0 && player.devSpeed !== 1 ? ( {player.devSpeed != null && player.devSpeed !== 0 && player.devSpeed !== 1 ? (
<div> <div>Dev Speed: {format(player.devSpeed)}x</div>
Dev Speed: {format(player.devSpeed)}x
<Node id="devspeed" />
</div>
) : null} ) : null}
{player.offlineTime != null && player.offlineTime !== 0 ? ( {player.offlineTime != null && player.offlineTime !== 0 ? (
<div> <div>Offline Time: {formatTime(player.offlineTime)}</div>
Offline Time: {formatTime(player.offlineTime)}
<Node id="offline" />
</div>
) : null} ) : null}
<div> <div>
{Decimal.lt(points.value, "1e1000") ? <span>You have </span> : null} {Decimal.lt(points.value, "1e1000") ? <span>You have </span> : null}
<h2>{format(points.value)}</h2> <h2>{format(points.value)}</h2>
{Decimal.lt(points.value, "1e1e6") ? <span> points</span> : null} {Decimal.lt(points.value, "1e1e6") ? <span> points</span> : null}
</div> </div>
{Decimal.gt(pointGain.value, 0) ? ( {Decimal.gt(pointGain.value, 0) ? <div>({oomps.value})</div> : null}
<div>
({oomps.value})
<Node id="oomps" />
</div>
) : null}
<Spacer /> <Spacer />
{render(tree)} {render(tree)}
</> </>

View file

@ -120,7 +120,7 @@ export default defineComponent({
barStyle.clipPath = `inset(0% ${normalizedProgress.value}% 0% 0%)`; barStyle.clipPath = `inset(0% ${normalizedProgress.value}% 0% 0%)`;
break; break;
case Direction.Left: case Direction.Left:
barStyle.clipPath = `inset(0% 0% 0% ${normalizedProgress.value}%)`; barStyle.clipPath = `inset(0% 0% 0% ${normalizedProgress.value} + '%)`;
break; break;
case Direction.Default: case Direction.Default:
barStyle.clipPath = "inset(0% 50% 0% 0%)"; barStyle.clipPath = "inset(0% 50% 0% 0%)";

View file

@ -1,7 +1,7 @@
<template> <template>
<line <line
class="link" class="link"
v-bind="linkProps" v-bind="link"
:class="{ pulsing: link.pulsing }" :class="{ pulsing: link.pulsing }"
:x1="startPosition.x" :x1="startPosition.x"
:y1="startPosition.y" :y1="startPosition.y"
@ -12,7 +12,6 @@
<script setup lang="ts"> <script setup lang="ts">
import type { BoardNode, BoardNodeLink } from "features/boards/board"; import type { BoardNode, BoardNodeLink } from "features/boards/board";
import { kebabifyObject } from "util/vue";
import { computed, toRefs, unref } from "vue"; import { computed, toRefs, unref } from "vue";
const _props = defineProps<{ const _props = defineProps<{
@ -50,14 +49,11 @@ const endPosition = computed(() => {
} }
return position; return position;
}); });
const linkProps = computed(() => kebabifyObject(_props.link as unknown as Record<string, unknown>));
</script> </script>
<style scoped> <style scoped>
.link { .link {
transition-duration: 0s; transition-duration: 0s;
pointer-events: none;
} }
.link.pulsing { .link.pulsing {

View file

@ -108,7 +108,7 @@ document.onkeydown = function (e) {
if (e.ctrlKey) { if (e.ctrlKey) {
key = "ctrl+" + key; key = "ctrl+" + key;
} }
const hotkey = hotkeys[key] ?? hotkeys[key.toLowerCase()]; const hotkey = hotkeys[key];
if (hotkey && unref(hotkey.enabled)) { if (hotkey && unref(hotkey.enabled)) {
e.preventDefault(); e.preventDefault();
hotkey.onPress(); hotkey.onPress();

View file

@ -2,7 +2,7 @@
<line <line
stroke-width="15px" stroke-width="15px"
stroke="white" stroke="white"
v-bind="linkProps" v-bind="link"
:x1="startPosition.x" :x1="startPosition.x"
:y1="startPosition.y" :y1="startPosition.y"
:x2="endPosition.x" :x2="endPosition.x"
@ -13,7 +13,6 @@
<script setup lang="ts"> <script setup lang="ts">
import type { Link } from "features/links/links"; import type { Link } from "features/links/links";
import type { FeatureNode } from "game/layers"; import type { FeatureNode } from "game/layers";
import { kebabifyObject } from "util/vue";
import { computed, toRefs } from "vue"; import { computed, toRefs } from "vue";
const _props = defineProps<{ const _props = defineProps<{
@ -55,6 +54,4 @@ const endPosition = computed(() => {
} }
return position; return position;
}); });
const linkProps = computed(() => kebabifyObject(_props.link as unknown as Record<string, unknown>));
</script> </script>

View file

@ -95,6 +95,18 @@ export function addTooltip<T extends TooltipOptions>(
} }
nextTick(() => { 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]; const elementComponent = element[Component];
element[Component] = TooltipComponent as GenericComponent; element[Component] = TooltipComponent as GenericComponent;
const elementGatherProps = element[GatherProps].bind(element); const elementGatherProps = element[GatherProps].bind(element);

View file

@ -224,7 +224,7 @@ export interface BaseTree {
id: string; id: string;
/** The link objects for each of the branches of the tree. */ /** The link objects for each of the branches of the tree. */
links: Ref<Link[]>; links: Ref<Link[]>;
/** Cause a reset on this node and propagate it through the tree according to {@link TreeOptions.resetPropagation}. */ /** Cause a reset on this node and propagate it through the tree according to {@link resetPropagation}. */
reset: (node: GenericTreeNode) => void; reset: (node: GenericTreeNode) => void;
/** A flag that is true while the reset is still propagating through the tree. */ /** A flag that is true while the reset is still propagating through the tree. */
isResetting: Ref<boolean>; isResetting: Ref<boolean>;
@ -338,21 +338,34 @@ export const branchedResetPropagation = function (
tree: GenericTree, tree: GenericTree,
resettingNode: GenericTreeNode resettingNode: GenericTreeNode
): void { ): void {
const links = unref(tree.branches); const visitedNodes = [resettingNode];
if (links == null) return; let currentNodes = [resettingNode];
const reset: GenericTreeNode[] = []; if (tree.branches != null) {
let current = [resettingNode]; const branches = unref(tree.branches);
while (current.length != 0) { while (currentNodes.length > 0) {
const next: GenericTreeNode[] = []; const nextNodes: GenericTreeNode[] = [];
for (const node of current) { currentNodes.forEach(node => {
for (const link of links.filter(link => link.startNode === node)) { branches
if ([...reset, ...current].includes(link.endNode)) continue .filter(branch => branch.startNode === node || branch.endNode === node)
next.push(link.endNode); .map(branch => {
link.endNode.reset?.reset(); if (branch.startNode === node) {
} return branch.endNode;
}; }
reset.push(...current); return branch.startNode;
current = next; })
.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);
}
} }
}; };

View file

@ -345,35 +345,19 @@ export abstract class InternalFormula<T extends [FormulaSource] | FormulaSource[
public static sgn = InternalFormula.sign; public static sgn = InternalFormula.sign;
public static round(value: FormulaSource) { public static round(value: FormulaSource) {
return new Formula({ return new Formula({ inputs: [value], evaluate: Decimal.round });
inputs: [value],
evaluate: Decimal.round,
invert: ops.invertPassthrough
});
} }
public static floor(value: FormulaSource) { public static floor(value: FormulaSource) {
return new Formula({ return new Formula({ inputs: [value], evaluate: Decimal.floor });
inputs: [value],
evaluate: Decimal.floor,
invert: ops.invertPassthrough
});
} }
public static ceil(value: FormulaSource) { public static ceil(value: FormulaSource) {
return new Formula({ return new Formula({ inputs: [value], evaluate: Decimal.ceil });
inputs: [value],
evaluate: Decimal.ceil,
invert: ops.invertPassthrough
});
} }
public static trunc(value: FormulaSource) { public static trunc(value: FormulaSource) {
return new Formula({ return new Formula({ inputs: [value], evaluate: Decimal.trunc });
inputs: [value],
evaluate: Decimal.trunc,
invert: ops.invertPassthrough
});
} }
public static add<T extends GenericFormula>(value: T, other: FormulaSource): T; public static add<T extends GenericFormula>(value: T, other: FormulaSource): T;
@ -475,7 +459,7 @@ export abstract class InternalFormula<T extends [FormulaSource] | FormulaSource[
return new Formula({ return new Formula({
inputs: [value, min, max], inputs: [value, min, max],
evaluate: Decimal.clamp, evaluate: Decimal.clamp,
invert: ops.invertPassthrough invert: ops.passthrough as InvertFunction<[FormulaSource, FormulaSource, FormulaSource]>
}); });
} }
@ -1469,7 +1453,9 @@ export function calculateMaxAffordable(
formula.invertIntegral(Decimal.add(resource.value, formula.evaluateIntegral())) formula.invertIntegral(Decimal.add(resource.value, formula.evaluateIntegral()))
).sub(unref(formula.innermostVariable) ?? 0); ).sub(unref(formula.innermostVariable) ?? 0);
} else { } else {
affordable = Decimal.floor(formula.invert(resource.value)); affordable = Decimal.floor(
formula.invert(resource.value)
).add(1).sub(unref(formula.innermostVariable) ?? 0);
} }
} }
affordable = Decimal.clampMax(affordable, maxBulkAmount); affordable = Decimal.clampMax(affordable, maxBulkAmount);

View file

@ -1,12 +1,6 @@
import Decimal, { DecimalSource } from "util/bignum"; import Decimal, { DecimalSource } from "util/bignum";
import Formula, { hasVariable, unrefFormulaSource } from "./formulas"; import Formula, { hasVariable, unrefFormulaSource } from "./formulas";
import { import { FormulaSource, GenericFormula, InvertFunction, SubstitutionStack } from "./types";
FormulaSource,
GenericFormula,
InvertFunction,
InvertibleFormula,
SubstitutionStack
} from "./types";
const ln10 = Decimal.ln(10); const ln10 = Decimal.ln(10);
@ -14,15 +8,6 @@ export function passthrough<T extends GenericFormula | DecimalSource>(value: T):
return value; 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) { export function invertNeg(value: DecimalSource, lhs: FormulaSource) {
if (hasVariable(lhs)) { if (hasVariable(lhs)) {
return lhs.invert(Decimal.neg(value)); return lhs.invert(Decimal.neg(value));

View file

@ -4,7 +4,7 @@ import { jsx } from "features/feature";
import settings from "game/settings"; import settings from "game/settings";
import type { DecimalSource } from "util/bignum"; import type { DecimalSource } from "util/bignum";
import Decimal, { formatSmall } from "util/bignum"; import Decimal, { formatSmall } from "util/bignum";
import type { RequiredKeys, WithRequired } from "util/common"; import type { WithRequired } from "util/common";
import type { Computable, ProcessedComputable } from "util/computed"; import type { Computable, ProcessedComputable } from "util/computed";
import { convertComputable } from "util/computed"; import { convertComputable } from "util/computed";
import { createLazyProxy } from "util/proxies"; import { createLazyProxy } from "util/proxies";
@ -38,11 +38,16 @@ export interface Modifier {
description?: ProcessedComputable<CoercableComponent>; description?: ProcessedComputable<CoercableComponent>;
} }
/** Utility type that represents the output of all modifiers that represent a single operation. */ /**
export type OperationModifier<T> = WithRequired< * 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).
Modifier, */
"invert" | "getFormula" | Extract<RequiredKeys<T>, keyof Modifier> 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">;
/** An object that configures an additive modifier via {@link createAdditiveModifier}. */ /** An object that configures an additive modifier via {@link createAdditiveModifier}. */
export interface AdditiveModifierOptions { export interface AdditiveModifierOptions {
@ -60,9 +65,9 @@ export interface AdditiveModifierOptions {
* Create a modifier that adds some value to the input value. * Create a modifier that adds some value to the input value.
* @param optionsFunc Additive modifier options. * @param optionsFunc Additive modifier options.
*/ */
export function createAdditiveModifier<T extends AdditiveModifierOptions, S = OperationModifier<T>>( export function createAdditiveModifier<T extends AdditiveModifierOptions>(
optionsFunc: OptionsFunc<T> optionsFunc: OptionsFunc<T>
) { ): ModifierFromOptionalParams<T["description"], T["enabled"]> {
return createLazyProxy(feature => { return createLazyProxy(feature => {
const { addend, description, enabled, smallerIsBetter } = optionsFunc.call( const { addend, description, enabled, smallerIsBetter } = optionsFunc.call(
feature, feature,
@ -106,7 +111,7 @@ export function createAdditiveModifier<T extends AdditiveModifierOptions, S = Op
</div> </div>
)) ))
}; };
}) as S; }) as unknown as ModifierFromOptionalParams<T["description"], T["enabled"]>;
} }
/** An object that configures an multiplicative modifier via {@link createMultiplicativeModifier}. */ /** An object that configures an multiplicative modifier via {@link createMultiplicativeModifier}. */
@ -125,10 +130,9 @@ export interface MultiplicativeModifierOptions {
* Create a modifier that multiplies the input value by some value. * Create a modifier that multiplies the input value by some value.
* @param optionsFunc Multiplicative modifier options. * @param optionsFunc Multiplicative modifier options.
*/ */
export function createMultiplicativeModifier< export function createMultiplicativeModifier<T extends MultiplicativeModifierOptions>(
T extends MultiplicativeModifierOptions, optionsFunc: OptionsFunc<T>
S = OperationModifier<T> ): ModifierFromOptionalParams<T["description"], T["enabled"]> {
>(optionsFunc: OptionsFunc<T>) {
return createLazyProxy(feature => { return createLazyProxy(feature => {
const { multiplier, description, enabled, smallerIsBetter } = optionsFunc.call( const { multiplier, description, enabled, smallerIsBetter } = optionsFunc.call(
feature, feature,
@ -171,7 +175,7 @@ export function createMultiplicativeModifier<
</div> </div>
)) ))
}; };
}) as S; }) as unknown as ModifierFromOptionalParams<T["description"], T["enabled"]>;
} }
/** An object that configures an exponential modifier via {@link createExponentialModifier}. */ /** An object that configures an exponential modifier via {@link createExponentialModifier}. */
@ -192,10 +196,9 @@ export interface ExponentialModifierOptions {
* Create a modifier that raises the input value to the power of some value. * Create a modifier that raises the input value to the power of some value.
* @param optionsFunc Exponential modifier options. * @param optionsFunc Exponential modifier options.
*/ */
export function createExponentialModifier< export function createExponentialModifier<T extends ExponentialModifierOptions>(
T extends ExponentialModifierOptions, optionsFunc: OptionsFunc<T>
S = OperationModifier<T> ): ModifierFromOptionalParams<T["description"], T["enabled"]> {
>(optionsFunc: OptionsFunc<T>) {
return createLazyProxy(feature => { return createLazyProxy(feature => {
const { exponent, description, enabled, supportLowNumbers, smallerIsBetter } = const { exponent, description, enabled, supportLowNumbers, smallerIsBetter } =
optionsFunc.call(feature, feature); optionsFunc.call(feature, feature);
@ -260,7 +263,7 @@ export function createExponentialModifier<
</div> </div>
)) ))
}; };
}) as S; }) as unknown as ModifierFromOptionalParams<T["description"], T["enabled"]>;
} }
/** /**
@ -271,9 +274,11 @@ export function createExponentialModifier<
* @see {@link createModifierSection}. * @see {@link createModifierSection}.
*/ */
export function createSequentialModifier< export function createSequentialModifier<
T extends Modifier, T extends Modifier[],
S = WithRequired<Modifier, Extract<RequiredKeys<T>, keyof Modifier>> S = T extends WithRequired<Modifier, "invert">[]
>(modifiersFunc: () => T[]) { ? WithRequired<Modifier, "description" | "invert">
: Omit<WithRequired<Modifier, "description">, "invert">
>(modifiersFunc: () => T): S {
return createLazyProxy(() => { return createLazyProxy(() => {
const modifiers = modifiersFunc(); const modifiers = modifiersFunc();
@ -291,14 +296,10 @@ export function createSequentialModifier<
: undefined, : undefined,
getFormula: modifiers.every(m => m.getFormula != null) getFormula: modifiers.every(m => m.getFormula != null)
? (gain: FormulaSource) => ? (gain: FormulaSource) =>
modifiers.reduce((acc, curr) => { modifiers
if (curr.enabled == null || curr.enabled === true) { .filter(m => unref(m.enabled) !== false)
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return curr.getFormula!(acc);
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
return Formula.if(acc, curr.enabled, acc => curr.getFormula!(acc)); .reduce((acc, curr) => curr.getFormula!(acc), gain)
}, gain)
: undefined, : undefined,
enabled: modifiers.some(m => m.enabled != null) enabled: modifiers.some(m => m.enabled != null)
? computed(() => modifiers.filter(m => unref(m.enabled) !== false).length > 0) ? computed(() => modifiers.filter(m => unref(m.enabled) !== false).length > 0)
@ -316,7 +317,7 @@ export function createSequentialModifier<
)) ))
: undefined : undefined
}; };
}) as S; }) as unknown as S;
} }
/** An object that configures a modifier section via {@link createModifierSection}. */ /** An object that configures a modifier section via {@link createModifierSection}. */

View file

@ -222,7 +222,7 @@ export function createCostRequirement<T extends CostRequirementOptions>(
Decimal.gte( Decimal.gte(
req.resource.value, req.resource.value,
unref(req.cost as ProcessedComputable<DecimalSource>) unref(req.cost as ProcessedComputable<DecimalSource>)
) ? 1 : 0 )
); );
} }

View file

@ -1,11 +1,3 @@
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 WithRequired<T, K extends keyof T> = T & { [P in K]-?: T[P] };
export type ArrayElements<T extends ReadonlyArray<unknown>> = T extends ReadonlyArray<infer S> export type ArrayElements<T extends ReadonlyArray<unknown>> = T extends ReadonlyArray<infer S>
@ -20,11 +12,6 @@ export function camelToTitle(camel: string): string {
return title; 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>( export function isFunction<T, S extends ReadonlyArray<unknown>, R>(
functionOrValue: ((...args: S) => T) | R functionOrValue: ((...args: S) => T) | R
): functionOrValue is (...args: S) => T { ): functionOrValue is (...args: S) => T {

View file

@ -21,7 +21,6 @@ import {
unref, unref,
watchEffect watchEffect
} from "vue"; } from "vue";
import { camelToKebab } from "./common";
export function coerceComponent( export function coerceComponent(
component: CoercableComponent, component: CoercableComponent,
@ -242,10 +241,3 @@ export function trackHover(element: VueFeature): Ref<boolean> {
return isHovered; 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>);
}

View file

@ -47,10 +47,6 @@ describe("Creating conversion", () => {
baseResource.value = Decimal.pow(100, 2).times(10).add(1); baseResource.value = Decimal.pow(100, 2).times(10).add(1);
expect(unref(conversion.currentGain)).compare_tolerance(100); expect(unref(conversion.currentGain)).compare_tolerance(100);
}); });
test("Zero", () => {
baseResource.value = Decimal.dZero;
expect(unref(conversion.currentGain)).compare_tolerance(0);
});
}); });
describe("Calculates actualGain correctly", () => { describe("Calculates actualGain correctly", () => {
let conversion: GenericConversion; let conversion: GenericConversion;
@ -73,10 +69,6 @@ describe("Creating conversion", () => {
baseResource.value = Decimal.pow(100, 2).times(10).add(1); baseResource.value = Decimal.pow(100, 2).times(10).add(1);
expect(unref(conversion.actualGain)).compare_tolerance(100); expect(unref(conversion.actualGain)).compare_tolerance(100);
}); });
test("Zero", () => {
baseResource.value = Decimal.dZero;
expect(unref(conversion.actualGain)).compare_tolerance(0);
});
}); });
describe("Calculates currentAt correctly", () => { describe("Calculates currentAt correctly", () => {
let conversion: GenericConversion; let conversion: GenericConversion;
@ -103,10 +95,6 @@ describe("Creating conversion", () => {
Decimal.pow(100, 2).times(10) Decimal.pow(100, 2).times(10)
); );
}); });
test("Zero", () => {
baseResource.value = Decimal.dZero;
expect(unref(conversion.currentAt)).compare_tolerance(0);
});
}); });
describe("Calculates nextAt correctly", () => { describe("Calculates nextAt correctly", () => {
let conversion: GenericConversion; let conversion: GenericConversion;
@ -129,10 +117,6 @@ describe("Creating conversion", () => {
baseResource.value = Decimal.pow(100, 2).times(10).add(1); baseResource.value = Decimal.pow(100, 2).times(10).add(1);
expect(unref(conversion.nextAt)).compare_tolerance(Decimal.pow(101, 2).times(10)); 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", () => { test("Converts correctly", () => {
const conversion = createCumulativeConversion(() => ({ const conversion = createCumulativeConversion(() => ({
@ -209,10 +193,6 @@ describe("Creating conversion", () => {
baseResource.value = Decimal.pow(100, 2).times(10).add(1); baseResource.value = Decimal.pow(100, 2).times(10).add(1);
expect(unref(conversion.currentGain)).compare_tolerance(100); expect(unref(conversion.currentGain)).compare_tolerance(100);
}); });
test("Zero", () => {
baseResource.value = Decimal.dZero;
expect(unref(conversion.currentGain)).compare_tolerance(1);
});
}); });
describe("Calculates actualGain correctly", () => { describe("Calculates actualGain correctly", () => {
let conversion: GenericConversion; let conversion: GenericConversion;
@ -236,10 +216,6 @@ describe("Creating conversion", () => {
baseResource.value = Decimal.pow(100, 2).times(10).add(1); baseResource.value = Decimal.pow(100, 2).times(10).add(1);
expect(unref(conversion.actualGain)).compare_tolerance(99); expect(unref(conversion.actualGain)).compare_tolerance(99);
}); });
test("Zero", () => {
baseResource.value = Decimal.dZero;
expect(unref(conversion.actualGain)).compare_tolerance(0);
});
}); });
describe("Calculates currentAt correctly", () => { describe("Calculates currentAt correctly", () => {
let conversion: GenericConversion; let conversion: GenericConversion;
@ -267,10 +243,6 @@ describe("Creating conversion", () => {
Decimal.pow(100, 2).times(10) 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", () => { describe("Calculates nextAt correctly", () => {
let conversion: GenericConversion; let conversion: GenericConversion;
@ -294,10 +266,6 @@ describe("Creating conversion", () => {
baseResource.value = Decimal.pow(100, 2).times(10).add(1); baseResource.value = Decimal.pow(100, 2).times(10).add(1);
expect(unref(conversion.nextAt)).compare_tolerance(Decimal.pow(101, 2).times(10)); 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", () => { test("Converts correctly", () => {
const conversion = createIndependentConversion(() => ({ const conversion = createIndependentConversion(() => ({

View file

@ -1,111 +0,0 @@
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);
});
});

View file

@ -13,13 +13,9 @@ import { InvertibleIntegralFormula } from "game/formulas/types";
type FormulaFunctions = keyof GenericFormula & keyof typeof Formula & keyof typeof Decimal; type FormulaFunctions = keyof GenericFormula & keyof typeof Formula & keyof typeof Decimal;
const testValues = [-2, "0", new Decimal(10.5)] as const; const testValues = [-1, "0", Decimal.dOne] as const;
const invertibleZeroParamFunctionNames = [ const invertibleZeroParamFunctionNames = [
"round",
"floor",
"ceil",
"trunc",
"neg", "neg",
"recip", "recip",
"log10", "log10",
@ -52,6 +48,10 @@ const invertibleZeroParamFunctionNames = [
const nonInvertibleZeroParamFunctionNames = [ const nonInvertibleZeroParamFunctionNames = [
"abs", "abs",
"sign", "sign",
"round",
"floor",
"ceil",
"trunc",
"pLog10", "pLog10",
"absLog10", "absLog10",
"factorial", "factorial",
@ -85,10 +85,6 @@ const integrableZeroParamFunctionNames = [
] as const; ] as const;
const nonIntegrableZeroParamFunctionNames = [ const nonIntegrableZeroParamFunctionNames = [
...nonInvertibleZeroParamFunctionNames, ...nonInvertibleZeroParamFunctionNames,
"round",
"floor",
"ceil",
"trunc",
"lambertw", "lambertw",
"ssqrt" "ssqrt"
] as const; ] as const;
@ -155,7 +151,7 @@ describe("Formula Equality Checking", () => {
describe("Formula aliases", () => { describe("Formula aliases", () => {
function testAliases<T extends FormulaFunctions>( function testAliases<T extends FormulaFunctions>(
aliases: T[], aliases: T[],
args: Parameters<typeof Formula[T]> args: Parameters<(typeof Formula)[T]>
) { ) {
describe(aliases[0], () => { describe(aliases[0], () => {
let formula: GenericFormula; let formula: GenericFormula;
@ -250,7 +246,7 @@ describe("Creating Formulas", () => {
function checkFormula<T extends FormulaFunctions>( function checkFormula<T extends FormulaFunctions>(
functionName: T, functionName: T,
args: Readonly<Parameters<typeof Formula[T]>> args: Readonly<Parameters<(typeof Formula)[T]>>
) { ) {
let formula: GenericFormula; let formula: GenericFormula;
beforeAll(() => { beforeAll(() => {
@ -274,7 +270,7 @@ describe("Creating Formulas", () => {
// It's a lot of tests, but I'd rather be exhaustive // It's a lot of tests, but I'd rather be exhaustive
function testFormulaCall<T extends FormulaFunctions>( function testFormulaCall<T extends FormulaFunctions>(
functionName: T, functionName: T,
args: Readonly<Parameters<typeof Formula[T]>> args: Readonly<Parameters<(typeof Formula)[T]>>
) { ) {
if ((functionName === "slog" || functionName === "layeradd") && args[0] === -1) { if ((functionName === "slog" || functionName === "layeradd") && args[0] === -1) {
// These cases in particular take a long time, so skip them // These cases in particular take a long time, so skip them
@ -492,18 +488,18 @@ describe("Inverting", () => {
}); });
test("Inverting nested formulas", () => { test("Inverting nested formulas", () => {
const formula = Formula.add(variable, constant).times(constant).floor(); const formula = Formula.add(variable, constant).times(constant);
expect(formula.invert(100)).compare_tolerance(0); expect(formula.invert(100)).compare_tolerance(0);
}); });
describe("Inverting with non-invertible sections", () => { describe("Inverting with non-invertible sections", () => {
test("Non-invertible constant", () => { test("Non-invertible constant", () => {
const formula = Formula.add(variable, constant.sign()); const formula = Formula.add(variable, constant.ceil());
expect(formula.isInvertible()).toBe(true); expect(formula.isInvertible()).toBe(true);
expect(() => formula.invert(10)).not.toLogError(); expect(() => formula.invert(10)).not.toLogError();
}); });
test("Non-invertible variable", () => { test("Non-invertible variable", () => {
const formula = Formula.add(variable.sign(), constant); const formula = Formula.add(variable.ceil(), constant);
expect(formula.isInvertible()).toBe(false); expect(formula.isInvertible()).toBe(false);
expect(() => formula.invert(10)).toLogError(); expect(() => formula.invert(10)).toLogError();
}); });

View file

@ -133,14 +133,14 @@ describe("Exponential Modifiers", () =>
testModifiers(createExponentialModifier, "exponent", Decimal.pow)); testModifiers(createExponentialModifier, "exponent", Decimal.pow));
describe("Sequential Modifiers", () => { describe("Sequential Modifiers", () => {
function createModifier<T extends Partial<ModifierConstructorOptions>>( function createModifier(
value: Computable<DecimalSource>, value: Computable<DecimalSource>,
options?: T options: Partial<ModifierConstructorOptions> = {}
) { ): WithRequired<Modifier, "invert" | "getFormula"> {
return createSequentialModifier(() => [ return createSequentialModifier(() => [
createAdditiveModifier(() => ({ ...(options ?? {}), addend: value })), createAdditiveModifier(() => ({ ...options, addend: value })),
createMultiplicativeModifier(() => ({ ...(options ?? {}), multiplier: value })), createMultiplicativeModifier(() => ({ ...options, multiplier: value })),
createExponentialModifier(() => ({ ...(options ?? {}), exponent: value })) createExponentialModifier(() => ({ ...options, exponent: value }))
]); ]);
} }
@ -199,17 +199,6 @@ describe("Sequential Modifiers", () => {
// So long as one is true or undefined, enable should be true // So long as one is true or undefined, enable should be true
expect(unref(modifier.enabled)).toBe(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", () => { describe("applies smallerIsBetter correctly", () => {

View file

@ -83,7 +83,7 @@ describe("Creating cost requirement", () => {
cost: 10, cost: 10,
cumulativeCost: false cumulativeCost: false
})); }));
expect(unref(requirement.requirementMet)).toBe(1); expect(unref(requirement.requirementMet)).toBe(true);
}); });
test("Requirement not met when not meeting the cost", () => { test("Requirement not met when not meeting the cost", () => {
@ -92,7 +92,7 @@ describe("Creating cost requirement", () => {
cost: 100, cost: 100,
cumulativeCost: false cumulativeCost: false
})); }));
expect(unref(requirement.requirementMet)).toBe(0); expect(unref(requirement.requirementMet)).toBe(false);
}); });
describe("canMaximize works correctly", () => { describe("canMaximize works correctly", () => {