Compare commits

...

48 commits

Author SHA1 Message Date
Nif
62794b81a1 Merge pull request 'pull current changes' (#1) from profectus/Profectus:main into main
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 57s
Run Tests / test (push) Successful in 1m56s
Reviewed-on: #1
2024-03-02 19:40:14 +00:00
escapee
b40d4bef32 Allow both cases in shift+hotkeys
All checks were successful
Run Tests / test (pull_request) Successful in 1m59s
2024-02-21 19:21:18 +00:00
1f22f506dd Add tests for tree reset propagation 2024-02-21 04:15:49 +00:00
d3faec6a66 Add Nodes to the text that can disappear in projEntry 2024-02-21 04:08:59 +00:00
a39e65852d Remove unused imports 2024-02-21 01:25:52 +00:00
1e2b20a70f PR feedback 2024-02-21 01:25:52 +00:00
2e0e221010 Made modifier typing a lot less nasty 2024-02-21 01:25:52 +00:00
4092cd6d56 Add regression test for modifier.getFormula respecting enabled 2024-02-21 01:25:52 +00:00
cba79df80d Merge pull request 'Fix branchedResetPropagation' (#57) from nif/Profectus-Niffix:main into main
Reviewed-on: profectus/Profectus#57
2024-02-14 17:39:06 +00:00
04a5e963ab Merge branch 'main' into main
Some checks failed
Build and Deploy / build-and-deploy (push) Successful in 1m1s
Run Tests / test (push) Failing after 2m7s
2024-02-14 17:38:53 +00:00
263c951cf8 Requested changes
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m3s
Run Tests / test (push) Successful in 2m12s
2024-02-14 15:56:18 +00:00
1b809a9550 Merge pull request 'Keep disabled modifiers when making formulas' (#56) from escapee/profectus-template:main into main
Reviewed-on: profectus/Profectus#56
2024-02-13 05:18:01 +00:00
nif
5e32fa4985 Fix branchedResetPropagation
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m3s
Run Tests / test (push) Successful in 2m10s
BREAKING CHANGE - Forces branches to be directed

Signed-off-by: nif <nif@incremental.social>
2024-02-12 19:46:31 +00:00
Seth Posner
cf6265d8ce Keep disabled modifiers when making formulas 2024-02-12 07:58:39 -08:00
4f807aaf96 Merge pull request 'Add forgejo workflows support' (#53) from thepaperpilot/Profectus:main into main
Reviewed-on: profectus/Profectus#53
2024-02-11 19:43:37 +00:00
ffc42a5745 Merge pull request 'Fix reset button using currentAt when not yet gaining' (#25) from sethposner/profectus-template:main into main
Reviewed-on: profectus/Profectus#25
2023-11-13 22:29:49 +00:00
Seth Posner
8811996f64 Add tests confirming low-input conversion values 2023-11-13 14:10:00 -08:00
Seth Posner
7750a3368d Swap logic for nextAt display 2023-11-13 14:09:48 -08:00
2495dc9783 Implement forgejo actions workflows (#24)
Reviewed-on: profectus/Profectus#24
Co-authored-by: thepaperpilot <thepaperpilot@gmail.com>
Co-committed-by: thepaperpilot <thepaperpilot@gmail.com>
2023-11-05 17:00:28 +00:00
e66daad7a2 Merge pull request 'Implement isRednered' (#23) from thepaperpilot/Profectus:feature/is-rendered into main
Reviewed-on: profectus/Profectus#23
2023-11-05 16:59:37 +00:00
8065f8efa4 Approve install 2023-11-05 10:17:23 -06:00
e9283b5cca Update repos first 2023-11-05 10:16:38 -06:00
2c615ea524 Typo 2023-11-05 10:07:06 -06:00
953cd8047e Install rsync the normal way 2023-11-05 10:06:26 -06:00
766c600a70 Were the rsync logs wrong 2023-11-05 09:57:05 -06:00
c1d0b7eec6 Fully qualify >.< 2023-11-05 09:55:52 -06:00
52b500c9d8 setup rsync 2023-11-05 09:49:02 -06:00
aabb0a1bba Setup node 2023-11-05 09:13:56 -06:00
005bf5da9a Try alpine instead 2023-11-05 09:11:10 -06:00
7330a6bda4 Switch image to ubuntu:latest 2023-11-05 09:09:47 -06:00
65ff440e25 Fully qualify pages deploy action 2023-11-05 08:42:39 -06:00
acf1d24c15 Changed one of the wrong runs-on 2023-11-05 08:27:35 -06:00
d16bb55c3c Update runs-on 2023-11-05 07:46:30 -06:00
312cab1347 Rename workflow files 2023-11-05 07:42:28 -06:00
6d148da260 Add forgejo workflows 2023-11-05 07:28:43 -06:00
a5204106aa Forgot to comment the other signature 2023-10-11 21:44:02 -05:00
0cccf7aecc Add isRendered utility 2023-10-11 21:39:01 -05:00
d0fffd3b89 Update test.yml 2023-06-09 15:52:26 -05:00
3fe0311331 Try to allow actions to be run manually 2023-06-09 15:52:22 -05:00
eee5ac3e2d Fix passthroughs for inversions and make more operations invertible 2023-05-30 22:54:46 -05:00
9edda4d957 Make links ignore pointer events 2023-05-22 21:41:53 -05:00
6ad08c4052 Fix camelCase props not working on links 2023-05-21 17:27:29 -05:00
e0f1296b35 Rename "The Paper Pilot Community" to "Profectus & Friends" 2023-05-20 21:56:46 -05:00
c8ba77b89b Fix Direction.Left bars 2023-05-20 21:24:59 -05:00
63dcad4c12 Fix requirements tests 2023-05-20 08:30:07 -05:00
d6c9f95851 Fix error about pinnable tooltips 2023-05-20 08:28:27 -05:00
210c2290f0
Fix #9 2023-05-19 10:12:24 -05:00
e896fd84cf Change formula testing values to hopefully catch any other miscalculations 2023-05-19 08:04:20 -05:00
27 changed files with 397 additions and 119 deletions

View 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.

View 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

View file

@ -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

View file

@ -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:

View file

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

View file

@ -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 />

View file

@ -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>

View file

@ -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);
}

View file

@ -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} />

View file

@ -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)}
</>

View file

@ -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%)";

View file

@ -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 {

View file

@ -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();

View file

@ -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>

View file

@ -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);

View file

@ -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;
}
};

View file

@ -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
});
}

View file

@ -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));

View file

@ -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}. */

View file

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

View file

@ -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 {

View file

@ -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>);
}

View file

@ -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
View 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);
});
});

View file

@ -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();
});

View file

@ -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", () => {

View file

@ -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", () => {